@h-ai/ai 0.1.0-alpha.34 → 0.1.0-alpha.36
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 +158 -5
- package/dist/{ai-types-BZjo_rWW.d.ts → ai-audio-ws-protocol-CsmuWetO.d.ts} +438 -8
- package/dist/{ai-reasoning-types-Cm3-HVVN.d.ts → ai-reasoning-types-DLLzpn6T.d.ts} +480 -31
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +2 -2
- package/dist/{chunk-Y5BQR7QA.js → chunk-535CQURK.js} +249 -4
- package/dist/chunk-535CQURK.js.map +1 -0
- package/dist/{chunk-CXO3YSIG.js → chunk-UROBPUCF.js} +166 -4
- package/dist/chunk-UROBPUCF.js.map +1 -0
- package/dist/client/index.d.ts +56 -2
- package/dist/client/index.js +1 -1
- package/dist/index.d.ts +34 -4
- package/dist/index.js +2161 -718
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- package/dist/chunk-CXO3YSIG.js.map +0 -1
- package/dist/chunk-Y5BQR7QA.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @h-ai/ai
|
|
2
2
|
|
|
3
|
-
AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、工具调用、MCP、Embedding、记忆、检索/RAG、知识库、上下文管理、文件解析、Rerank
|
|
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。
|
|
@@ -113,6 +115,8 @@ const temp = await ai.llm.chat({
|
|
|
113
115
|
### 工具调用
|
|
114
116
|
|
|
115
117
|
```ts
|
|
118
|
+
import { z } from 'zod'
|
|
119
|
+
|
|
116
120
|
const registry = ai.tools.createRegistry()
|
|
117
121
|
registry.register(ai.tools.define({
|
|
118
122
|
name: 'get_weather',
|
|
@@ -128,6 +132,7 @@ const chat = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
|
|
|
128
132
|
|
|
129
133
|
```ts
|
|
130
134
|
import { createMcpServer, StreamableHTTPServerTransport } from '@h-ai/ai'
|
|
135
|
+
import { z } from 'zod'
|
|
131
136
|
|
|
132
137
|
const server = createMcpServer({ name: 'my-server', version: '1.0.0' })
|
|
133
138
|
server.registerTool('search', {
|
|
@@ -155,6 +160,63 @@ if (setup.success) {
|
|
|
155
160
|
}
|
|
156
161
|
```
|
|
157
162
|
|
|
163
|
+
### 语音(Audio)
|
|
164
|
+
|
|
165
|
+
先在 `ai.init()` 中注册语音模型并映射默认识别/合成模型(凭据可回退到平台环境变量):
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
await ai.init({
|
|
169
|
+
audio: {
|
|
170
|
+
models: [
|
|
171
|
+
{ id: 'asr', provider: 'qwen', model: 'qwen3-asr-flash-realtime', operations: ['transcribe'] },
|
|
172
|
+
{ id: 'tts', provider: 'qwen', model: 'qwen3-tts-flash-realtime', operations: ['synthesize'] },
|
|
173
|
+
],
|
|
174
|
+
transcribeModel: 'asr',
|
|
175
|
+
synthesizeModel: 'tts',
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// 完整识别(可选热词提示提升专有名词识别率)
|
|
180
|
+
const result = await ai.audio.transcribe({ audio: { data: wavBytes, format: 'wav' }, language: 'zh', contextHints: ['专有名词'] })
|
|
181
|
+
if (result.success) {
|
|
182
|
+
const text = result.data.text
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 实时识别(持续音频输入 → 领域事件流:speech_started / transcript / speech_stopped)
|
|
186
|
+
for await (const event of ai.audio.transcribeStream({
|
|
187
|
+
audio: { chunks: microphoneChunks, format: 'pcm16', sampleRate: 16000 },
|
|
188
|
+
})) {
|
|
189
|
+
if (event.type === 'speech_started')
|
|
190
|
+
onSpeechStart() // 支持服务端 VAD 的平台会在检测到说话时立即产出,可据此取消上游生成
|
|
191
|
+
else if (event.type === 'transcript')
|
|
192
|
+
updateTranscript(event.text, event.final)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 流式合成:调用方为文本段分配稳定 ID,事件可精确关联文本与音频;signal 可随时打断
|
|
196
|
+
const controller = new AbortController()
|
|
197
|
+
for await (const event of ai.audio.synthesizeStream({
|
|
198
|
+
text: { id: 'answer-1', text: '欢迎参加访谈。' },
|
|
199
|
+
voice: 'Cherry',
|
|
200
|
+
instruction: '用轻快的语气',
|
|
201
|
+
signal: controller.signal,
|
|
202
|
+
})) {
|
|
203
|
+
if (event.type === 'audio')
|
|
204
|
+
await player.write(event.data)
|
|
205
|
+
else if (event.type === 'segment_done')
|
|
206
|
+
markSegmentReadyToCommit(event.segmentId)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 实时会话启动前按操作校验模型能力
|
|
210
|
+
const caps = ai.audio.getCapabilities({ operation: 'synthesize', model: 'tts' })
|
|
211
|
+
if (caps.success && caps.data.synthesize?.streamingAudioOutput) { /* 可实时 TTS */ }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
> `synthesizeStream` 严格按 `segment_started → audio* → segment_done` 产出事件。播放器只有在对应音频真正播放完成后才应把该段文本计入 `spokenText`;播放状态仍由应用管理。
|
|
215
|
+
|
|
216
|
+
取消/超时/连接错误统一为领域错误:`AbortSignal` 触发 → `AUDIO_CANCELLED`(超时 → `AUDIO_TIMEOUT`),连接失败 → `AUDIO_CONNECTION_FAILED`。实时连接时长受 `audio.maxStreamDurationMs`(默认 5 分钟)限制。
|
|
217
|
+
|
|
218
|
+
浏览器 / 移动端通过 `@h-ai/serv` 暴露的统一语音 WebSocket 入口访问,`@h-ai/ai/client` 提供与 Node 端一致的 `audio.*` API(传输细节内部隐藏)。
|
|
219
|
+
|
|
158
220
|
### Context 管理器
|
|
159
221
|
|
|
160
222
|
```ts
|
|
@@ -169,7 +231,78 @@ if (manager.success) {
|
|
|
169
231
|
}
|
|
170
232
|
```
|
|
171
233
|
|
|
172
|
-
|
|
234
|
+
#### 真实对话状态(Conversation Commit Layer)
|
|
235
|
+
|
|
236
|
+
默认(`turnCommit: 'auto'`)下,`chat` / `chatStream` 会把**模型生成的完整文本**写入上下文。但在「模型生成 → TTS 合成 → 实际播放」链路中,AI 可能说到一半就被打断——此时进入下一轮所有参与者可见的对话状态,应当是**实际播放出去的部分**,而非模型本想说完的全文。
|
|
237
|
+
|
|
238
|
+
设置 `turnCommit: 'manual'` 后,生成结果不会自动写入上下文,而是返回一个 `turnId`;由调用方在确定「实际发生了什么」后显式提交真实文本。
|
|
239
|
+
|
|
240
|
+
`chatStream` 在**调用上游模型前**就登记轮次并产出 `turn_started`(事件序列 `turn_started → delta* → done`,中途取消时 `turn_started → delta* → cancelled`)。因此即使生成到一半被 `AbortSignal` 取消,也能拿到 `turnId` 与已生成文本,用真实内容提交:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const m = ai.context.createManager({ turnCommit: 'manual' /* ... */ }).data
|
|
244
|
+
const controller = new AbortController()
|
|
245
|
+
|
|
246
|
+
for await (const ev of m.chatStream('请展开讲讲', { signal: controller.signal })) {
|
|
247
|
+
if (ev.type === 'turn_started') {
|
|
248
|
+
m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
|
|
249
|
+
}
|
|
250
|
+
else if (ev.type === 'delta') {
|
|
251
|
+
feedTts(ev.text) // 边生成边合成播放
|
|
252
|
+
}
|
|
253
|
+
else if (ev.type === 'done') {
|
|
254
|
+
await m.commitTurn(ev.turnId) // 完整提交
|
|
255
|
+
}
|
|
256
|
+
else if (ev.type === 'cancelled') {
|
|
257
|
+
// 生成被 controller.abort() 取消:轮次保留,只提交实际播放出去的部分
|
|
258
|
+
await m.interruptTurn(ev.turnId, { text: actuallySpokenText })
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 观测每一轮的 generated / committed / status
|
|
263
|
+
const turns = m.getTurns()
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- `commitTurn(turnId, { text? })` — 提交真实文本(缺省用完整生成文本),状态转 `completed`。
|
|
267
|
+
- `interruptTurn(turnId, { text? })` — 只写入实际表达出去的部分(缺省视为未表达,不写入),状态转 `interrupted`。
|
|
268
|
+
- 只有 `committed` 的内容进入上下文与记忆提取;未提交/被打断丢弃的部分不会污染后续轮次。
|
|
269
|
+
|
|
270
|
+
#### 会话固化(Memory 生命周期)
|
|
271
|
+
|
|
272
|
+
会话进行中,每轮对话按 `scope: { sessionId }` 提取**短期会话记忆**;会话结束时用 `consolidate()`
|
|
273
|
+
把「短期记忆 + 摘要」沉淀为**跨会话长期记忆**,形成 `Session Memory → Summary → Long-term Memory` 闭环:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// 会话结束时固化:整合摘要 → 提取长期记忆(写入不含 sessionId 的持久作用域)
|
|
277
|
+
const result = await manager.consolidate({ scope: { userId: 'user-001', personaId: 'xiaoq' } })
|
|
278
|
+
if (result.success) {
|
|
279
|
+
// result.data.summary —— 本次会话整合摘要
|
|
280
|
+
// result.data.memories —— 固化到长期记忆的条目
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Persona(AI 角色人格)
|
|
285
|
+
|
|
286
|
+
Memory 用 `objectId` / `scope` 回答「谁的记忆」;Persona 回答「AI 是谁」——为每个 AI 角色定义
|
|
287
|
+
稳定的系统提示词与性格特征,并通过 `scope: { personaId }` 关联其长期记忆:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
await ai.persona.save({
|
|
291
|
+
id: 'xiaoq',
|
|
292
|
+
name: '小Q',
|
|
293
|
+
systemPrompt: '你是一位经济学家,善于从长期视角分析问题。',
|
|
294
|
+
traits: ['数据驱动', '偏好引用真实案例'],
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
|
|
298
|
+
const composed = await ai.persona.compose('xiaoq')
|
|
299
|
+
const manager = ai.context.createManager({
|
|
300
|
+
systemPrompt: composed.data,
|
|
301
|
+
memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`ai.persona` 提供 `save` / `get` / `update` / `remove` / `list` / `compose`;角色档案全局共享(不按 `objectId` 隔离)。
|
|
173
306
|
|
|
174
307
|
```yaml
|
|
175
308
|
llm:
|
|
@@ -210,6 +343,7 @@ memory:
|
|
|
210
343
|
recencyDecay: 0.95
|
|
211
344
|
embeddingEnabled: true
|
|
212
345
|
defaultTopK: 10
|
|
346
|
+
candidateMultiplier: 5 # 候选池倍数:先取回 topK×倍数 条候选,再按 scope/重要性过滤,最后截取 topK
|
|
213
347
|
writebackRelatedTopK: 20
|
|
214
348
|
```
|
|
215
349
|
|
|
@@ -221,13 +355,31 @@ memory:
|
|
|
221
355
|
defaultTopK: 10
|
|
222
356
|
```
|
|
223
357
|
|
|
224
|
-
- **`native`(默认,推荐)**:HAI 原生引擎,复用同一套 vecdb(向量库)、reldb(关系库)、LLM 与 Embedding。`extract` 采用 **Mem0 式批量合并**——一次 LLM 调用对整批抽取事实与相关既有记忆做 ADD / UPDATE / DELETE / NONE 决策,实现增量更新、跨条去重与矛盾删除,并支持 `category` 主题标签。`maxEntriesPerObject`、`maxEntriesGlobal`、`recencyDecay`、`embeddingEnabled`、`writebackRelatedTopK` 均作用于此后端;淘汰按 `objectId` 分区触发,不会因某一主体写入过多而淘汰其他主体的记忆。
|
|
358
|
+
- **`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 退回内存匹配,结果一致)。
|
|
225
359
|
- **`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 时需对应客户端。
|
|
226
360
|
|
|
227
|
-
两个 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`
|
|
361
|
+
两个 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 后端的 `extract` 在框架层用统一提取器完成分类与打分(honor `types` / `model` / `minImportance` / `systemPrompt`)后以 `infer:false` 写入,保留 `hai_type` / `hai_importance`;`recall` 同样支持 `types` 过滤与 `recencyWeight` 时间衰减——二者行为与 native 一致。一个差异:mem0 后端在 `update` 涉及 type/importance/metadata 时会重建记忆并重新分配 `id`(native 后端保持 id 稳定)。
|
|
362
|
+
|
|
363
|
+
**候选池与 scope 漏召回**:`scope` 过滤在内存中完成,若先按 `topK` 截断再过滤,同一主体下相关度较高的其它主题/角色记忆会把目标 scope 的记忆挤出候选池,导致「明明有却召回 0 条」。为此 `recall` / `injectMemories` 先取回 `topK × candidateMultiplier`(默认 5)条候选,过滤后再截取 `topK`。scope 隔离越细(如按 `topicId` + `personaId`),可将 `candidateMultiplier` 调大:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
const memories = await ai.memory.recall('经济发展', {
|
|
367
|
+
objectId: 'user-001',
|
|
368
|
+
scope: { topicId: 'C' },
|
|
369
|
+
topK: 10,
|
|
370
|
+
candidateMultiplier: 8, // 覆盖配置默认值,扩大候选池
|
|
371
|
+
})
|
|
372
|
+
```
|
|
228
373
|
|
|
229
374
|
`ai.config` 返回脱敏后的配置快照;`apiKey`、`privateKey`、URL 内嵌凭证等敏感字段不会原样暴露。
|
|
230
375
|
|
|
376
|
+
## 安全边界
|
|
377
|
+
|
|
378
|
+
- Prompt、检索文档和模型输出都按不可信输入处理;不要把用户内容拼进不可覆盖的系统规则。
|
|
379
|
+
- Zod 只校验工具参数形状,不代表调用者有权限。高权限工具必须在 handler 内再次校验身份、租户、资源归属与配额,并只把允许自动执行的工具注册给模型。
|
|
380
|
+
- `callRemoteAgent()` 是独立客户端能力,只依赖 `ai.init()`,不要求配置 Agent Card 或注册本地 executor。它拒绝非 HTTP(S) 和 URL 内嵌凭据,但应用仍必须对远端 origin 配置白名单,并在出口代理处限制 DNS 重绑定、重定向到私网和云元数据地址。
|
|
381
|
+
- 不记录完整 Prompt、工具参数、A2A headers、临时模型凭据或带 query 的远端 URL;确需审计时仅保存脱敏摘要。
|
|
382
|
+
|
|
231
383
|
## 错误处理
|
|
232
384
|
|
|
233
385
|
```ts
|
|
@@ -251,7 +403,8 @@ if (!result.success) {
|
|
|
251
403
|
- `hai:ai:300-302`:Embedding。
|
|
252
404
|
- `hai:ai:600-701`:Retrieval/RAG。
|
|
253
405
|
- `hai:ai:800-805`:Knowledge。
|
|
254
|
-
- `hai:ai:
|
|
406
|
+
- `hai:ai:050-059`:Audio。
|
|
407
|
+
- `hai:ai:900-905`:Memory。
|
|
255
408
|
- `hai:ai:980-984`:A2A。
|
|
256
409
|
|
|
257
410
|
## 测试
|