@h-ai/ai 0.1.0-alpha.33 → 0.1.0-alpha.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @h-ai/ai
2
2
 
3
- AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、工具调用、MCP、Embedding、记忆、检索/RAG、知识库、上下文管理、文件解析、Rerank 与 A2A。Node.js 侧通过 `ai.init()` 初始化,浏览器侧通过 API/client 代理访问。
3
+ AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、工具调用、MCP、Embedding、记忆、检索/RAG、知识库、上下文管理、文件解析、Rerank、语音(ASR/TTS)与 A2A。Node.js 侧通过 `ai.init()` 初始化,浏览器侧通过 API/client 代理访问。
4
4
 
5
5
  ## 能力概览
6
6
 
@@ -10,10 +10,12 @@ AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、工具
10
10
  - `ai.mcp` / `createMcpServer`:内置 MCP 注册表与独立 MCP Server。
11
11
  - `ai.embedding`:单条/批量文本向量化。
12
12
  - `ai.memory`:记忆提取、存储、召回、注入。
13
+ - `ai.persona`:AI 角色人格档案(系统提示词 + 特征)与系统提示词组合。
13
14
  - `ai.retrieval` / `ai.rag`:多源向量检索与检索增强问答。
14
15
  - `ai.knowledge`:文档入库、实体增强检索、知识问答。
15
16
  - `ai.context`:LLM + Memory + RAG + 压缩的一体化会话管理。
16
17
  - `ai.file` / `ai.rerank`:文件解析/OCR 与文本重排序。
18
+ - `ai.audio`:语音识别(ASR)与语音合成(TTS),支持完整与流式调用,覆盖 OpenAI / MiMo / Qwen / 豆包平台。
17
19
  - `ai.a2a`:Agent-to-Agent 请求处理与远端调用。
18
20
  - `@h-ai/ai/client`:前端轻量客户端(配合 API 服务)。
19
21
  - `AIStoreProvider`:统一存储抽象;默认 DB Provider 基于 reldb + vecdb。
@@ -91,11 +93,22 @@ for await (const chunk of ai.llm.chatStream({ messages })) {
91
93
  }
92
94
  }
93
95
 
96
+ // 请求取消:传入 AbortSignal,主持人打断/用户切换时 abort() 立即停止上游生成与计费
97
+ const controller = new AbortController()
98
+ const cancellable = ai.llm.chat({ messages, signal: controller.signal })
99
+ // controller.abort()
100
+
101
+ // 多协议:模型的 api 决定底层走 Chat Completions / Responses / Anthropic,公共请求响应形状不变
102
+ // - chat(默认):OpenAI Chat Completions(兼容绝大多数厂商)
103
+ // - responses:OpenAI Responses API(/v1/responses)
104
+ // - anthropic:Anthropic Messages API(Claude 原生协议,环境变量 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL)
105
+ const claude = await ai.llm.chat({ messages, model: 'claude' }) // 该模型配置 api: anthropic
106
+
94
107
  // 临时模型:单次请求绕过配置注册模型,直接指定端点与凭据(chat/chatStream/ask/askStream 均支持)
95
108
  // 临时客户端按 TTL 缓存(llm.tempModelCacheTtl,默认 10 分钟),与常驻模型客户端隔离
96
109
  const temp = await ai.llm.chat({
97
110
  messages,
98
- tempModel: { model: 'claude-3-5-sonnet', apiKey: 'sk-temp', baseUrl: 'https://temp.endpoint/v1' },
111
+ tempModel: { model: 'claude-3-5-sonnet', api: 'anthropic', apiKey: 'sk-temp' },
99
112
  })
100
113
  ```
101
114
 
@@ -144,6 +157,49 @@ if (setup.success) {
144
157
  }
145
158
  ```
146
159
 
160
+ ### 语音(Audio)
161
+
162
+ 先在 `ai.init()` 中注册语音模型并映射默认识别/合成模型(凭据可回退到平台环境变量):
163
+
164
+ ```ts
165
+ await ai.init({
166
+ audio: {
167
+ models: [
168
+ { id: 'asr', provider: 'qwen', model: 'qwen3-asr-flash-realtime' },
169
+ { id: 'tts', provider: 'qwen', model: 'qwen3-tts-flash-realtime' },
170
+ ],
171
+ transcribeModel: 'asr',
172
+ synthesizeModel: 'tts',
173
+ },
174
+ })
175
+
176
+ // 完整识别(可选热词提示提升专有名词识别率)
177
+ const result = await ai.audio.transcribe({ audio: { data: wavBytes, format: 'wav' }, language: 'zh', contextHints: ['专有名词'] })
178
+ if (result.success) {
179
+ const text = result.data.text
180
+ }
181
+
182
+ // 实时识别(持续音频输入 → 领域事件流:speech_started / transcript / speech_stopped)
183
+ for await (const event of ai.audio.transcribeStream({
184
+ audio: { chunks: microphoneChunks, format: 'pcm16', sampleRate: 16000 },
185
+ })) {
186
+ if (event.type === 'speech_started')
187
+ onSpeechStart() // 支持服务端 VAD 的平台会在检测到说话时立即产出,可据此取消上游生成
188
+ else if (event.type === 'transcript')
189
+ updateTranscript(event.text, event.final)
190
+ }
191
+
192
+ // 流式合成:可带自然语言风格指令,并直接连接 LLM 文本流边生成边合成;signal 可随时打断
193
+ const controller = new AbortController()
194
+ for await (const audio of ai.audio.synthesizeStream({ text: ai.llm.askStream(question), voice: 'Cherry', instruction: '用轻快的语气', signal: controller.signal })) {
195
+ await player.write(audio)
196
+ }
197
+ ```
198
+
199
+ 取消/超时/连接错误统一为领域错误:`AbortSignal` 触发 → `AUDIO_CANCELLED`(超时 → `AUDIO_TIMEOUT`),连接失败 → `AUDIO_CONNECTION_FAILED`。实时连接时长受 `audio.maxStreamDurationMs`(默认 5 分钟)限制。
200
+
201
+ 浏览器 / 移动端通过 `@h-ai/serv` 暴露的统一语音 WebSocket 入口访问,`@h-ai/ai/client` 提供与 Node 端一致的 `audio.*` API(传输细节内部隐藏)。
202
+
147
203
  ### Context 管理器
148
204
 
149
205
  ```ts
@@ -158,15 +214,85 @@ if (manager.success) {
158
214
  }
159
215
  ```
160
216
 
161
- ## 配置
217
+ #### 真实对话状态(Conversation Commit Layer)
218
+
219
+ 默认(`turnCommit: 'auto'`)下,`chat` / `chatStream` 会把**模型生成的完整文本**写入上下文。但在「模型生成 → TTS 合成 → 实际播放」链路中,AI 可能说到一半就被打断——此时进入下一轮所有参与者可见的对话状态,应当是**实际播放出去的部分**,而非模型本想说完的全文。
220
+
221
+ 设置 `turnCommit: 'manual'` 后,生成结果不会自动写入上下文,而是返回一个 `turnId`;由调用方在确定「实际发生了什么」后显式提交真实文本:
222
+
223
+ ```ts
224
+ const m = ai.context.createManager({ turnCommit: 'manual' /* ... */ }).data
225
+
226
+ for await (const ev of m.chatStream('请展开讲讲')) {
227
+ if (ev.type === 'delta') {
228
+ feedTts(ev.text)
229
+ } // 边生成边合成播放
230
+ else if (ev.type === 'done') {
231
+ m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
232
+ if (interrupted)
233
+ await m.interruptTurn(ev.turnId, { text: actuallySpokenText }) // 只提交播放出去的部分
234
+ else
235
+ await m.commitTurn(ev.turnId) // 完整提交
236
+ }
237
+ }
238
+
239
+ // 观测每一轮的 generated / committed / status
240
+ const turns = m.getTurns()
241
+ ```
242
+
243
+ - `commitTurn(turnId, { text? })` — 提交真实文本(缺省用完整生成文本),状态转 `completed`。
244
+ - `interruptTurn(turnId, { text? })` — 只写入实际表达出去的部分(缺省视为未表达,不写入),状态转 `interrupted`。
245
+ - 只有 `committed` 的内容进入上下文与记忆提取;未提交/被打断丢弃的部分不会污染后续轮次。
246
+
247
+ #### 会话固化(Memory 生命周期)
248
+
249
+ 会话进行中,每轮对话按 `scope: { sessionId }` 提取**短期会话记忆**;会话结束时用 `consolidate()`
250
+ 把「短期记忆 + 摘要」沉淀为**跨会话长期记忆**,形成 `Session Memory → Summary → Long-term Memory` 闭环:
251
+
252
+ ```ts
253
+ // 会话结束时固化:整合摘要 → 提取长期记忆(写入不含 sessionId 的持久作用域)
254
+ const result = await manager.consolidate({ scope: { userId: 'user-001', personaId: 'xiaoq' } })
255
+ if (result.success) {
256
+ // result.data.summary —— 本次会话整合摘要
257
+ // result.data.memories —— 固化到长期记忆的条目
258
+ }
259
+ ```
260
+
261
+ ### Persona(AI 角色人格)
262
+
263
+ Memory 用 `objectId` / `scope` 回答「谁的记忆」;Persona 回答「AI 是谁」——为每个 AI 角色定义
264
+ 稳定的系统提示词与性格特征,并通过 `scope: { personaId }` 关联其长期记忆:
265
+
266
+ ```ts
267
+ await ai.persona.save({
268
+ id: 'xiaoq',
269
+ name: '小Q',
270
+ systemPrompt: '你是一位经济学家,善于从长期视角分析问题。',
271
+ traits: ['数据驱动', '偏好引用真实案例'],
272
+ })
273
+
274
+ // 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
275
+ const composed = await ai.persona.compose('xiaoq')
276
+ const manager = ai.context.createManager({
277
+ systemPrompt: composed.data,
278
+ memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
279
+ })
280
+ ```
281
+
282
+ `ai.persona` 提供 `save` / `get` / `update` / `remove` / `list` / `compose`;角色档案全局共享(不按 `objectId` 隔离)。
162
283
 
163
284
  ```yaml
164
285
  llm:
165
286
  apiKey: ${HAI_AI_LLM_API_KEY:}
166
287
  baseUrl: ${HAI_AI_LLM_BASE_URL:https://api.openai.com/v1}
167
288
  model: ${HAI_AI_LLM_MODEL:gpt-4o-mini}
289
+ api: chat # chat(默认)| responses | anthropic —— 底层 API 协议,对使用方透明
168
290
  timeout: 60000
169
291
  tempModelCacheTtl: 600000 # 临时模型客户端缓存 TTL(毫秒,默认 10 分钟)
292
+ models: # 可为每个模型单独指定协议
293
+ - {id: fast, model: gpt-4o-mini}
294
+ - {id: strong, model: gpt-4.1, api: responses}
295
+ - {id: claude, model: claude-3-5-sonnet-latest, api: anthropic}
170
296
  scenarios:
171
297
  chat: fast
172
298
  reasoning: strong
@@ -188,10 +314,38 @@ knowledge:
188
314
  overlap: 200
189
315
 
190
316
  memory:
191
- maxEntries: 1000
317
+ provider: native # native | mem0
318
+ maxEntriesPerObject: 1000 # 单主体(objectId)最大记忆条数
319
+ maxEntriesGlobal: 100000 # 跨所有主体的全局上限
192
320
  recencyDecay: 0.95
193
321
  embeddingEnabled: true
194
322
  defaultTopK: 10
323
+ candidateMultiplier: 5 # 候选池倍数:先取回 topK×倍数 条候选,再按 scope/重要性过滤,最后截取 topK
324
+ writebackRelatedTopK: 20
325
+ ```
326
+
327
+ 启用 mem0(真·嵌入式 mem0ai/oss 引擎):
328
+
329
+ ```yaml
330
+ memory:
331
+ provider: mem0
332
+ defaultTopK: 10
333
+ ```
334
+
335
+ - **`native`(默认,推荐)**:HAI 原生引擎,复用同一套 vecdb(向量库)、reldb(关系库)、LLM 与 Embedding。`extract` 采用 **Mem0 式批量合并**——一次 LLM 调用对整批抽取事实与相关既有记忆做 ADD / UPDATE / DELETE / NONE 决策,实现增量更新、跨条去重与矛盾删除,并支持 `category` 主题标签。`maxEntriesPerObject`、`maxEntriesGlobal`、`recencyDecay`、`embeddingEnabled`、`writebackRelatedTopK` 均作用于此后端;淘汰按 `objectId` 分区触发,不会因某一主体写入过多而淘汰其他主体的记忆。native 后端的 `scope` 过滤:候选集已被 `objectId` 索引收窄(≤ `maxEntriesPerObject`),PostgreSQL 上还会把 scope 下推为 `data @> '{"scope":...}'::jsonb` 包含查询并命中 JSONB **GIN 索引**(SQLite / MySQL 退回内存匹配,结果一致)。
336
+ - **`mem0`(真·mem0ai/oss)**:直接使用 `mem0ai/oss` 的 `Memory` 引擎(嵌入式,无云服务)。LLM / Embedder 从 `llm` 配置提取(OpenAI 兼容,走 `baseUrl` / `apiKey` / 场景模型);向量库从底层 vecdb 后端提取——`qdrant` / `pgvector` 直接复用同一后端,`lancedb` / `chroma`(mem0 TS 不支持)则退回 mem0 自带的 in-memory 存储。历史记录默认禁用。需安装 `mem0ai`(已内置为依赖);复用 qdrant/pgvector 时需对应客户端。
337
+
338
+ 两个 Provider 对外 `ai.memory.*` API 完全一致(`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`),均支持 `objectId`(主体隔离)与 `scope`(业务作用域 key-value 过滤,如 `{ topicId, personaId }`)。`recall` / `list` / `listPage` / `clear` 均按 `scope` 严格过滤,`clear` 在传入 `types` / `scope` 时仅删除同时匹配项(避免误删)。一个差异:mem0 后端在 `update` 涉及 type/importance/metadata 时会重建记忆并重新分配 `id`(native 后端保持 id 稳定)。
339
+
340
+ **候选池与 scope 漏召回**:`scope` 过滤在内存中完成,若先按 `topK` 截断再过滤,同一主体下相关度较高的其它主题/角色记忆会把目标 scope 的记忆挤出候选池,导致「明明有却召回 0 条」。为此 `recall` / `injectMemories` 先取回 `topK × candidateMultiplier`(默认 5)条候选,过滤后再截取 `topK`。scope 隔离越细(如按 `topicId` + `personaId`),可将 `candidateMultiplier` 调大:
341
+
342
+ ```ts
343
+ const memories = await ai.memory.recall('经济发展', {
344
+ objectId: 'user-001',
345
+ scope: { topicId: 'C' },
346
+ topK: 10,
347
+ candidateMultiplier: 8, // 覆盖配置默认值,扩大候选池
348
+ })
195
349
  ```
196
350
 
197
351
  `ai.config` 返回脱敏后的配置快照;`apiKey`、`privateKey`、URL 内嵌凭证等敏感字段不会原样暴露。