@h-ai/ai 0.1.0-alpha.56 → 0.1.0-alpha.58
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.en.md +574 -0
- package/README.ja.md +574 -0
- package/README.md +4 -0
- package/dist/{ai-audio-ws-protocol-BcF5gqtG.d.ts → ai-audio-ws-protocol-S4N8-LBa.d.ts} +1 -1
- package/dist/browser.d.ts +4 -4
- package/dist/client/index.d.ts +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +19 -19
- package/dist/{ai-reasoning-types-D9UyA9Yc.d.ts → ai-reasoning-types-DZd_6Umd.d.ts} +191 -191
package/README.en.md
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# @h-ai/ai
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[Framework home](../../README.en.md) · Related modules: [@h-ai/api-contract](../api-contract/README.en.md) · [@h-ai/core](../core/README.en.md) · [@h-ai/datapipe](../datapipe/README.en.md) · [@h-ai/reldb](../reldb/README.en.md) · [@h-ai/vecdb](../vecdb/README.en.md)
|
|
6
|
+
|
|
7
|
+
AI capabilities through a unified `ai` service: LLM chat, image generation, tools, MCP, embeddings, memory, retrieval/RAG, knowledge bases, context management, file parsing, reranking, audio (ASR/TTS), and A2A. Initialize with `ai.init()` on Node.js; browsers access it through APIs/clients.
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
- `ai.tools`: tool definitions, registries, and batch execution; no initialization required.
|
|
12
|
+
- `ai.stream`: ChatCompletion stream chunk processing; no initialization required.
|
|
13
|
+
- `ai.llm`: non-streaming/streaming chat, model lists, and history.
|
|
14
|
+
- `ai.mcp` / `createMcpServer`: an in-process MCP registry and a standalone MCP server.
|
|
15
|
+
- `ai.embedding`: individual/batch text embeddings.
|
|
16
|
+
- `ai.memory`: memory extraction, storage, recall, and injection.
|
|
17
|
+
- `ai.persona`: AI persona profiles (system prompts + traits) and prompt composition.
|
|
18
|
+
- `ai.retrieval` / `ai.rag`: multi-source vector retrieval and retrieval-augmented answers.
|
|
19
|
+
- `ai.knowledge`: document ingestion, entity-enhanced retrieval, and knowledge answers.
|
|
20
|
+
- `ai.context`: integrated sessions combining LLM + Memory + RAG + compression.
|
|
21
|
+
- `ai.file` / `ai.rerank`: file parsing/OCR and text reranking.
|
|
22
|
+
- `ai.audio`: ASR and TTS with full and streaming calls for OpenAI / MiMo / Qwen / Doubao.
|
|
23
|
+
- `ai.image`: OpenAI GPT Image, Google Gemini Image / Nano Banana, Qwen-Image 2.0/3.0, Seedream 4.x/5.x, and Pollinations free-quota models.
|
|
24
|
+
- `ai.a2a`: Agent-to-Agent request handling and remote calls.
|
|
25
|
+
- `@h-ai/ai/client`: lightweight frontend client for API services.
|
|
26
|
+
- `AIStoreProvider`: shared storage abstraction. Defaults to a temporary in-process store without databases; automatically uses persistent DB storage when reldb + vecdb are initialized.
|
|
27
|
+
|
|
28
|
+
For the full method list, error codes, and longer examples, see [REFERENCE.md (Chinese)](./REFERENCE.md).
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
### LLM-only (no database dependencies)
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { ai } from '@h-ai/ai'
|
|
36
|
+
|
|
37
|
+
const init = await ai.init({
|
|
38
|
+
llm: {
|
|
39
|
+
model: 'gpt-4o-mini',
|
|
40
|
+
apiKey: process.env.HAI_AI_LLM_APIKEY,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
if (!init.success)
|
|
44
|
+
return init
|
|
45
|
+
|
|
46
|
+
const result = await ai.llm.chat({
|
|
47
|
+
messages: [{ role: 'user', content: '你好!' }],
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await ai.close()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Without initialized reldb/vecdb, AI stores session/runtime state in a temporary in-process store. Data does not survive `ai.close()`, process exit, or instance switching. Use the DB provider below for persistent Memory, Context, Persona, or sessions across restarts.
|
|
54
|
+
|
|
55
|
+
### Persistent DB provider (reldb + vecdb)
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { ai } from '@h-ai/ai'
|
|
59
|
+
import { reldb } from '@h-ai/reldb'
|
|
60
|
+
import { vecdb } from '@h-ai/vecdb'
|
|
61
|
+
|
|
62
|
+
await reldb.init({ type: 'sqlite', database: './ai.db' })
|
|
63
|
+
await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
|
|
64
|
+
|
|
65
|
+
const init = await ai.init({
|
|
66
|
+
llm: {
|
|
67
|
+
model: 'gpt-4o-mini',
|
|
68
|
+
apiKey: process.env.HAI_AI_LLM_APIKEY,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
if (!init.success) {
|
|
72
|
+
// 按 init.error.code 处理配置/依赖错误
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await ai.llm.chat({
|
|
76
|
+
messages: [{ role: 'user', content: '你好!' }],
|
|
77
|
+
})
|
|
78
|
+
if (result.success) {
|
|
79
|
+
const reply = result.data.choices[0]?.message.content ?? ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await ai.close()
|
|
83
|
+
await vecdb.close()
|
|
84
|
+
await reldb.close()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom StoreProvider
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import type { AIStoreProvider } from '@h-ai/ai'
|
|
91
|
+
import { ai } from '@h-ai/ai'
|
|
92
|
+
|
|
93
|
+
const storeProvider: AIStoreProvider = createMyStoreProvider()
|
|
94
|
+
|
|
95
|
+
await ai.init(
|
|
96
|
+
{ llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY } },
|
|
97
|
+
{ storeProvider },
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
await ai.close()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API contracts
|
|
104
|
+
|
|
105
|
+
- Public access uses the `ai` service and a few standalone factories such as `createMcpServer`.
|
|
106
|
+
- Lifecycle: `await ai.init(config, options?)` / `await ai.close()`; closure waits for custom `AIStoreProvider.close()`.
|
|
107
|
+
- Domain methods return `HaiResult<T>` or `Promise<HaiResult<T>>`, with failures expressed by `result.success === false` and `result.error.code`. Streaming `AsyncIterable`, client transports, and third-party callbacks can throw during establishment or iteration.
|
|
108
|
+
- `ai.tools` and `ai.stream` are pure-function subsystems usable without initialization.
|
|
109
|
+
- Without reldb/vecdb, storage is temporary and in-process; with both initialized, DB storage is automatic. Custom providers can encapsulate other backends.
|
|
110
|
+
|
|
111
|
+
## API overview
|
|
112
|
+
|
|
113
|
+
### LLM
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const result = await ai.llm.chat({ messages })
|
|
117
|
+
for await (const chunk of ai.llm.chatStream({ messages })) {
|
|
118
|
+
const delta = chunk.choices[0]?.delta?.content
|
|
119
|
+
if (delta) {
|
|
120
|
+
process.stdout.write(delta)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 请求取消:传入 AbortSignal,主持人打断/用户切换时 abort() 立即停止上游生成与计费
|
|
125
|
+
const controller = new AbortController()
|
|
126
|
+
const cancellable = ai.llm.chat({ messages, signal: controller.signal })
|
|
127
|
+
// controller.abort()
|
|
128
|
+
|
|
129
|
+
// 多协议:模型的 api 决定底层走 Chat Completions / Responses / Anthropic,公共请求响应形状不变
|
|
130
|
+
// - chat(默认):OpenAI Chat Completions(兼容绝大多数厂商)
|
|
131
|
+
// - responses:OpenAI Responses API(/v1/responses)
|
|
132
|
+
// - anthropic:Anthropic Messages API(Claude 原生协议,环境变量 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL)
|
|
133
|
+
const claude = await ai.llm.chat({ messages, model: 'claude' }) // 该模型配置 api: anthropic
|
|
134
|
+
|
|
135
|
+
// 临时模型:单次请求绕过配置注册模型,直接指定端点与凭据(chat/chatStream/ask/askStream 均支持)
|
|
136
|
+
// 临时客户端按 TTL 缓存(llm.tempModelCacheTtl,默认 10 分钟),与常驻模型客户端隔离
|
|
137
|
+
const temp = await ai.llm.chat({
|
|
138
|
+
messages,
|
|
139
|
+
tempModel: { model: 'claude-3-5-sonnet', api: 'anthropic', apiKey: 'sk-temp' },
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Tool calls
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { z } from 'zod'
|
|
147
|
+
|
|
148
|
+
const registry = ai.tools.createRegistry({
|
|
149
|
+
// 统一授权在 Zod 校验后、handler 前执行;false/异常均 fail-closed
|
|
150
|
+
authorize: ({ toolName, context }) => canExecuteTool(context.objectId, toolName),
|
|
151
|
+
})
|
|
152
|
+
const registered = registry.register(ai.tools.define({
|
|
153
|
+
name: 'get_weather',
|
|
154
|
+
description: '获取天气',
|
|
155
|
+
parameters: z.object({ city: z.string() }),
|
|
156
|
+
// handler 第二参为执行上下文:可响应取消(打断/超时)、感知截止时间与交互主体
|
|
157
|
+
handler: async ({ city }, { signal }) => {
|
|
158
|
+
const res = await fetch(`https://api.example.com/weather?city=${city}`, { signal })
|
|
159
|
+
return res.json()
|
|
160
|
+
},
|
|
161
|
+
timeoutMs: 10_000, // 本工具默认超时(可被 execute 的 deadline / timeoutMs 覆盖)
|
|
162
|
+
}))
|
|
163
|
+
if (!registered.success)
|
|
164
|
+
return registered
|
|
165
|
+
|
|
166
|
+
// 执行时可传入取消信号 / 超时 / 作用域;取消或超时返回 TOOL_TIMEOUT,且不再等待未响应的 handler
|
|
167
|
+
const result = await registry.execute(toolCall, { signal: controller.signal, objectId: 'user-001', timeoutMs: 30_000 })
|
|
168
|
+
|
|
169
|
+
// 批量调用默认串行,避免副作用工具并发启动;纯读取且确认安全时才显式并行
|
|
170
|
+
const batch = await registry.executeAll(toolCalls)
|
|
171
|
+
|
|
172
|
+
const chat = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### A2A protocol boundaries
|
|
176
|
+
|
|
177
|
+
`ai.a2a.getAgentCard()` returns a complete A2A 0.3 Agent Card with `protocolVersion`, MIME, and streaming capabilities. Kit discovery defaults to `/.well-known/agent-card.json`. `handleRequest()` always returns JSON-RPC/SSE results; uninitialized calls return JSON-RPC errors. Trusted HTTP `agentId` maps to executor `context.user.userName`. JSON-RPC/SSE are supported; push notifications, gRPC, and REST are not claimed. Applications remain responsible for business resource authorization.
|
|
178
|
+
|
|
179
|
+
A2A task/message tables initialize during `ai.init()`. Tasks are read in trusted `agentId` scopes, including get/cancel/continuation. Anonymous requests share one scope, so deployments requiring isolation must enable authentication. `listMessages` is a server administration API with `callerId`, `contextId`, and `since` filters; never expose it to unauthorized clients. Legacy tasks lack reliable subjects; upgrades neither infer ownership nor delete data. After review, applications must migrate `object_id` to `user:<agentId>` (`anonymous` for anonymous records). Legacy messages lacking subject/context fields do not match corresponding filters.
|
|
180
|
+
|
|
181
|
+
### MCP Server
|
|
182
|
+
|
|
183
|
+
`ai.mcp` is an in-process registry; standalone `createMcpServer` uses SDK transports, with no automatic registration between them. `ai.mcp` compiles tool JSON schemas at registration and validates call inputs. Resources must contain exactly one of `text` / `blob`. Prompt arguments and SDK message outputs are validated, with image/audio support. HTTP examples show transport connection only; applications must mount request handling/authentication and close the server on exit.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { createMcpServer, StreamableHTTPServerTransport } from '@h-ai/ai'
|
|
187
|
+
import { z } from 'zod'
|
|
188
|
+
|
|
189
|
+
const server = createMcpServer({ name: 'my-server', version: '1.0.0' })
|
|
190
|
+
server.registerTool('search', {
|
|
191
|
+
description: '搜索',
|
|
192
|
+
inputSchema: { query: z.string() },
|
|
193
|
+
}, async ({ query }) => ({
|
|
194
|
+
content: [{ type: 'text', text: `Results for ${query}` }],
|
|
195
|
+
}))
|
|
196
|
+
await server.connect(new StreamableHTTPServerTransport({ sessionIdGenerator: crypto.randomUUID }))
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Memory / RAG / Knowledge
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const enriched = await ai.memory.injectMemories(messages, { objectId: 'user-001', topK: 5 })
|
|
203
|
+
if (!enriched.success) {
|
|
204
|
+
return enriched
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 多租户推荐入口:scoped() 绑定主体与作用域,所有操作自动携带 objectId / scope(含归属校验),杜绝「忘记传 objectId」越权
|
|
208
|
+
const memory = ai.memory.scoped({ objectId: 'user-001', scope: { topicId: 't-1', personaId: 'p-1' } })
|
|
209
|
+
await memory.add({ content: '用户偏好中文', type: 'preference' })
|
|
210
|
+
const recalled = await memory.recall('语言偏好')
|
|
211
|
+
|
|
212
|
+
// clear 拒绝空过滤(防误清全局);全局清空只能显式走管理接口
|
|
213
|
+
await memory.clear({ types: ['event'] }) // 仅清该作用域内的 event
|
|
214
|
+
await ai.memory.admin.clearAll({ confirm: true }) // 危险:清空整个记忆后端,需显式确认
|
|
215
|
+
|
|
216
|
+
const rag = await ai.rag.query('核心架构是什么?', { sources: ['docs'], topK: 5 })
|
|
217
|
+
|
|
218
|
+
const setup = await ai.knowledge.setup()
|
|
219
|
+
if (setup.success) {
|
|
220
|
+
await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Audio
|
|
225
|
+
|
|
226
|
+
Register audio models in `ai.init()` and map the default transcription/synthesis models first. Credentials fall back to platform environment variables. Enable `inheritLlmApiKey` explicitly only when LLM and audio models are confirmed to share credentials:
|
|
227
|
+
|
|
228
|
+
`provider` identifies the protocol, not deployment location. `openai` / `mimo` / `qwen` / `doubao` are cloud platforms; `whisper` (ASR) / `indextts` (TTS) are self-hostable protocols with no canonical endpoint and require explicit `baseUrl`. One protocol can switch among cloud, intranet, and local endpoints without changing business code. Missing cloud credentials return `CONFIGURATION_ERROR`. Self-hosted services may omit credentials; configured `apiKey` is sent as `Authorization: Bearer`.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
await ai.init({
|
|
232
|
+
llm: {
|
|
233
|
+
apiKey: process.env.HAI_AI_LLM_APIKEY,
|
|
234
|
+
},
|
|
235
|
+
audio: {
|
|
236
|
+
inheritLlmApiKey: true,
|
|
237
|
+
models: [
|
|
238
|
+
{ id: 'asr', provider: 'qwen', model: 'qwen3-asr-flash-realtime', operations: ['transcribe'] },
|
|
239
|
+
{ id: 'tts', provider: 'qwen', model: 'qwen3-tts-flash-realtime', operations: ['synthesize'] },
|
|
240
|
+
// 自托管:whisper / indextts 必须显式配置 baseUrl
|
|
241
|
+
{ id: 'whisper', provider: 'whisper', model: 'faster-whisper-large-v3', operations: ['transcribe'], baseUrl: 'http://127.0.0.1:8101/v1' },
|
|
242
|
+
{ id: 'indextts', provider: 'indextts', model: 'indextts-2.5', operations: ['synthesize'], baseUrl: 'http://127.0.0.1:8102/v1' },
|
|
243
|
+
],
|
|
244
|
+
transcribeModel: 'asr',
|
|
245
|
+
synthesizeModel: 'tts',
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// 完整识别(可选热词、时间戳粒度与 VAD;strictCapabilities 要求模型真实支持所请求能力)
|
|
250
|
+
const result = await ai.audio.transcribe({
|
|
251
|
+
audio: { data: wavBytes, format: 'wav' },
|
|
252
|
+
language: 'zh',
|
|
253
|
+
contextHints: ['专有名词'],
|
|
254
|
+
timestampGranularities: ['segment', 'word'],
|
|
255
|
+
vad: true,
|
|
256
|
+
model: 'whisper',
|
|
257
|
+
})
|
|
258
|
+
if (result.success) {
|
|
259
|
+
const text = result.data.text
|
|
260
|
+
const language = result.data.language // 实际检测语言
|
|
261
|
+
const words = result.data.segments?.[0]?.words // 词级时间轴(毫秒整数)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 说话人 / 风格参考合成(IndexTTS):speakerReference 表达“谁在说”,styleReference 表达“怎么说”
|
|
265
|
+
const cloned = await ai.audio.synthesize({
|
|
266
|
+
text: '我们下午三点出发。',
|
|
267
|
+
language: 'zh',
|
|
268
|
+
model: 'indextts',
|
|
269
|
+
speakerReference: { audio: speakerWav, language: 'ja' },
|
|
270
|
+
styleReference: { audio: styleWav },
|
|
271
|
+
styleStrength: 0.8,
|
|
272
|
+
targetDurationMs: 3280, // 目标时长(与 speed 互斥),metadata.durationMatched 反馈是否达标
|
|
273
|
+
durationToleranceMs: 120,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// 实时识别(持续音频输入 → 领域事件流:speech_started / transcript / speech_stopped)
|
|
277
|
+
for await (const event of ai.audio.transcribeStream({
|
|
278
|
+
audio: { chunks: microphoneChunks, format: 'pcm16', sampleRate: 16000 },
|
|
279
|
+
})) {
|
|
280
|
+
if (event.type === 'speech_started')
|
|
281
|
+
onSpeechStart() // 支持服务端 VAD 的平台会在检测到说话时立即产出,可据此取消上游生成
|
|
282
|
+
else if (event.type === 'transcript')
|
|
283
|
+
updateTranscript(event.text, event.final)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 流式合成:调用方为文本段分配稳定 ID,事件可精确关联文本与音频;signal 可随时打断
|
|
287
|
+
const controller = new AbortController()
|
|
288
|
+
for await (const event of ai.audio.synthesizeStream({
|
|
289
|
+
text: { id: 'answer-1', text: '欢迎参加访谈。' },
|
|
290
|
+
voice: 'Cherry',
|
|
291
|
+
instruction: '用轻快的语气',
|
|
292
|
+
signal: controller.signal,
|
|
293
|
+
})) {
|
|
294
|
+
if (event.type === 'segment_started')
|
|
295
|
+
prepareDecoder(event.format, event.sampleRate, event.channels) // 真实输出格式来自服务端解析后的 Provider 输出
|
|
296
|
+
else if (event.type === 'audio')
|
|
297
|
+
await player.write(event.data)
|
|
298
|
+
else if (event.type === 'segment_done')
|
|
299
|
+
markSegmentReadyToCommit(event.segmentId)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 实时会话启动前按操作校验模型能力
|
|
303
|
+
const caps = ai.audio.getCapabilities({ operation: 'synthesize', model: 'tts' })
|
|
304
|
+
if (caps.success && caps.data.synthesize?.streamingAudioOutput) { /* 可实时 TTS */ }
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`ai.audio.synthesize` returns normalized `AudioContent` bytes. To return browser-playable Base64 over an API, use `serializePlayableAudio(result.data)`, which wraps `pcm16` in a WAV container and passes WAV/MP3 through:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { serializePlayableAudio } from '@h-ai/ai'
|
|
311
|
+
|
|
312
|
+
const synthesized = await ai.audio.synthesize({ text: '欢迎参加访谈。', voice: 'Cherry' })
|
|
313
|
+
if (!synthesized.success)
|
|
314
|
+
return synthesized
|
|
315
|
+
|
|
316
|
+
const playable = serializePlayableAudio(synthesized.data)
|
|
317
|
+
if (!playable.success)
|
|
318
|
+
return playable
|
|
319
|
+
|
|
320
|
+
return playable.data
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`OptionalSecretSchema` covers optional LLM, Audio, and Image secrets: YAML `null`, empty strings, and whitespace-only strings normalize to `undefined`. Audio key precedence is model `apiKey` → inherited LLM `apiKey` when enabled → platform environment variables. `inheritLlmApiKey` is off by default to avoid using keys across vendors.
|
|
324
|
+
|
|
325
|
+
> `synthesizeStream` emits `segment_started → audio* → segment_done` strictly. `segment_started` carries **actual output audio parameters** (`format` / `sampleRate` / `channels`) resolved by the server/provider. Players must decode from these rather than guess from requested parameters, and add segment text to `spokenText` only after its audio has actually finished playing. Applications manage playback state.
|
|
326
|
+
|
|
327
|
+
Cancellation, timeout, and connection problems become domain errors: `AbortSignal` → `AUDIO_CANCELLED`, timeout → `AUDIO_TIMEOUT`, connection failure or abnormal disconnect before `end` → `AUDIO_CONNECTION_FAILED`. `audio.maxStreamDurationMs` limits real-time connections (default five minutes).
|
|
328
|
+
|
|
329
|
+
Browsers/mobile clients use the unified audio WebSocket exposed by `@h-ai/serv`. `@h-ai/ai/client` offers the same `audio.*` API as Node, hiding transport details. Browser clients distinguish completion, cancellation (`AUDIO_CANCELLED`), and abnormal disconnect (`AUDIO_CONNECTION_FAILED`); cancellation or disconnect before `end` throws the domain code. `synthesize` never treats incomplete audio as success.
|
|
330
|
+
|
|
331
|
+
> For self-hosted CPU/GPU model services (faster-whisper / IndexTTS / Qwen3-4B), weight downloads, and offline bundles, see [models/ (Chinese)](./models/README.md). Images and weights prefer ModelScope, with automatic HuggingFace mirror fallback.
|
|
332
|
+
|
|
333
|
+
### Image generation
|
|
334
|
+
|
|
335
|
+
After model registration in `ai.init()`, callers receive normalized image bytes without vendor-specific Base64, inline data, or temporary URLs:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
await ai.init({
|
|
339
|
+
image: {
|
|
340
|
+
models: [
|
|
341
|
+
{ id: 'image', provider: 'openai', model: 'gpt-image-2' },
|
|
342
|
+
{ id: 'free', provider: 'pollinations', model: 'zimage' },
|
|
343
|
+
],
|
|
344
|
+
generateModel: 'image',
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const result = await ai.image.generate({
|
|
349
|
+
prompt: '一个正在构建开源框架的友好机器人',
|
|
350
|
+
size: { width: 1024, height: 1024 },
|
|
351
|
+
referenceImages: [
|
|
352
|
+
{ data: await readImage('character.png'), mimeType: 'image/png' },
|
|
353
|
+
],
|
|
354
|
+
})
|
|
355
|
+
if (result.success) {
|
|
356
|
+
const { data, mimeType } = result.data.images[0]!
|
|
357
|
+
await saveImage(data, mimeType)
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`referenceImages` is optional. Public APIs accept only image bytes and MIME; providers convert multipart, `inlineData`, and Data URLs. Set credentials on model entries or with `HAI_AI_IMAGE_<PROVIDER>_API_KEY` / vendor environment variables. Qwen and Seedream temporary URLs are downloaded immediately inside providers and never exposed publicly. See [REFERENCE.md (Chinese)](./REFERENCE.md#image) for model details and API differences.
|
|
362
|
+
|
|
363
|
+
### Context manager
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
const manager = ai.context.createManager({
|
|
367
|
+
scope: { objectId: 'user-001', sessionId: 'sess-001' },
|
|
368
|
+
compress: { auto: true, strategy: 'hybrid', maxTokens: 8000 },
|
|
369
|
+
memory: { enable: true, enableExtract: true },
|
|
370
|
+
concurrency: 'reject', // 单活动生成(默认):活动生成期间的新 chat 返回 CONTEXT_BUSY;'queue' 则排队
|
|
371
|
+
})
|
|
372
|
+
if (manager.success) {
|
|
373
|
+
const reply = await manager.data.chat('你好')
|
|
374
|
+
await manager.data.save()
|
|
375
|
+
// reset 生命周期完整:终止活动生成、清空消息/摘要/轮次,默认保留系统提示词
|
|
376
|
+
await manager.data.reset() // 可传 { preserveSystemPrompt, cancelActiveTurn, waitForMemoryTasks }
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
A manager permits **one active generation** by default, preventing out-of-order messages when the next user message is written before the previous AI run exits. `reset()` is asynchronous: it stops active generation, releases concurrency barriers, clears turns, and rewrites Persona/System Prompt by default.
|
|
381
|
+
|
|
382
|
+
#### Actual conversation state (Conversation Commit Layer)
|
|
383
|
+
|
|
384
|
+
By default (`turnCommit: 'auto'`), `chat` / `chatStream` store the **full generated text** in context. In a generation → TTS → playback flow, however, speech can be interrupted halfway. The next turn's shared conversation state should contain **what was actually played**, rather than everything the model intended to say.
|
|
385
|
+
|
|
386
|
+
With `turnCommit: 'manual'`, results are not stored automatically. A `turnId` is returned so the caller can explicitly commit the text reflecting what actually happened.
|
|
387
|
+
|
|
388
|
+
`chatStream` registers a turn and emits `turn_started` **before calling the model** (`turn_started → delta* → done`, or `turn_started → delta* → cancelled` on cancellation). Even if `AbortSignal` cancels generation midway, the caller gets `turnId` and generated text for committing actual content:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const m = ai.context.createManager({ turnCommit: 'manual' /* ... */ }).data
|
|
392
|
+
const controller = new AbortController()
|
|
393
|
+
|
|
394
|
+
for await (const ev of m.chatStream('请展开讲讲', { signal: controller.signal })) {
|
|
395
|
+
if (ev.type === 'turn_started') {
|
|
396
|
+
m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
|
|
397
|
+
}
|
|
398
|
+
else if (ev.type === 'delta') {
|
|
399
|
+
feedTts(ev.text) // 边生成边合成播放
|
|
400
|
+
}
|
|
401
|
+
else if (ev.type === 'done') {
|
|
402
|
+
await m.commitTurn(ev.turnId) // 完整提交
|
|
403
|
+
}
|
|
404
|
+
else if (ev.type === 'cancelled') {
|
|
405
|
+
// 生成被 controller.abort() 取消:轮次保留,只提交实际播放出去的部分
|
|
406
|
+
await m.interruptTurn(ev.turnId, { text: actuallySpokenText })
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 观测每一轮的 generated / committed / status
|
|
411
|
+
const turns = m.getTurns()
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
- `commitTurn(turnId, { text? })`: commit actual text (defaults to full generated text), setting state to `completed`.
|
|
415
|
+
- `interruptTurn(turnId, { text? })`: store only what was actually expressed (omission means nothing was expressed, so nothing is stored), setting state to `interrupted`.
|
|
416
|
+
- Only `committed` content enters context and memory extraction. Uncommitted/discarded text does not affect future turns.
|
|
417
|
+
- If `interruptTurn` interrupts before stream completion (for example a host interrupts just as the model finishes), `chatStream` **does not emit `done`**, preventing accidental post-completion commits.
|
|
418
|
+
|
|
419
|
+
#### Session consolidation (Memory lifecycle)
|
|
420
|
+
|
|
421
|
+
During a session, each turn extracts **short-term session memory** with `scope: { sessionId }`. At session end, `consolidate()`
|
|
422
|
+
turns short-term memory + summaries into **cross-session long-term memory**, completing `Session Memory → Summary → Long-term Memory`:
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
// 会话结束时固化:整合摘要 → 提取长期记忆(写入不含 sessionId 的持久作用域)
|
|
426
|
+
const result = await manager.consolidate({ scope: { userId: 'user-001', personaId: 'xiaoq' } })
|
|
427
|
+
if (result.success) {
|
|
428
|
+
// result.data.summary —— 本次会话整合摘要
|
|
429
|
+
// result.data.memories —— 固化到长期记忆的条目
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Persona (AI character profiles)
|
|
434
|
+
|
|
435
|
+
Memory uses `objectId` / `scope` to identify whose memories they are; Persona defines who the AI is through
|
|
436
|
+
stable system prompts and character traits, linked to long-term memory through `scope: { personaId }`:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
await ai.persona.save({
|
|
440
|
+
id: 'xiaoq',
|
|
441
|
+
name: '小Q',
|
|
442
|
+
systemPrompt: '你是一位经济学家,善于从长期视角分析问题。',
|
|
443
|
+
traits: ['数据驱动', '偏好引用真实案例'],
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
// 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
|
|
447
|
+
const composed = await ai.persona.compose('xiaoq')
|
|
448
|
+
const manager = ai.context.createManager({
|
|
449
|
+
systemPrompt: composed.data,
|
|
450
|
+
memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
|
|
451
|
+
})
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`ai.persona` exposes `save` / `get` / `update` / `remove` / `list` / `compose`. Profiles are shared globally, not isolated by `objectId`.
|
|
455
|
+
|
|
456
|
+
```yaml
|
|
457
|
+
llm:
|
|
458
|
+
apiKey: ''
|
|
459
|
+
baseUrl: https://api.openai.com/v1
|
|
460
|
+
model: gpt-4o-mini
|
|
461
|
+
api: chat # chat(默认)| responses | anthropic —— 底层 API 协议,对使用方透明
|
|
462
|
+
timeout: 60000
|
|
463
|
+
tempModelCacheTtl: 600000 # 临时模型客户端缓存 TTL(毫秒,默认 10 分钟)
|
|
464
|
+
models: # 可为每个模型单独指定协议
|
|
465
|
+
- {id: fast, model: gpt-4o-mini}
|
|
466
|
+
- {id: strong, model: gpt-4.1, api: responses}
|
|
467
|
+
- {id: claude, model: claude-3-5-sonnet-latest, api: anthropic}
|
|
468
|
+
scenarios:
|
|
469
|
+
chat: fast
|
|
470
|
+
reasoning: strong
|
|
471
|
+
embedding: embed
|
|
472
|
+
|
|
473
|
+
embedding:
|
|
474
|
+
dimensions: 1536
|
|
475
|
+
batchSize: 100
|
|
476
|
+
|
|
477
|
+
knowledge:
|
|
478
|
+
collection: hai_ai_knowledge
|
|
479
|
+
dimension: 1536
|
|
480
|
+
cleanOptions:
|
|
481
|
+
removeHtml: true
|
|
482
|
+
normalizeWhitespace: true
|
|
483
|
+
chunkOptions:
|
|
484
|
+
mode: markdown
|
|
485
|
+
maxSize: 1500
|
|
486
|
+
overlap: 200
|
|
487
|
+
|
|
488
|
+
memory:
|
|
489
|
+
provider: native # native | mem0
|
|
490
|
+
maxEntriesPerObject: 1000 # 单主体(objectId)最大记忆条数
|
|
491
|
+
maxEntriesGlobal: 100000 # 跨所有主体的全局上限
|
|
492
|
+
recencyDecay: 0.95
|
|
493
|
+
embeddingEnabled: true
|
|
494
|
+
defaultTopK: 10
|
|
495
|
+
candidateMultiplier: 5 # 候选池倍数:先取回 topK×倍数 条候选,再按 scope/重要性过滤,最后截取 topK
|
|
496
|
+
writebackRelatedTopK: 20
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
When loading through `core.config.load('ai', ...)`, convention-based environment variables override YAML:
|
|
500
|
+
for example `HAI_AI_LLM_APIKEY`, `HAI_AI_LLM_BASEURL`, and `HAI_AI_LLM_MODEL`.
|
|
501
|
+
|
|
502
|
+
Enable mem0 (the embedded mem0ai/oss engine):
|
|
503
|
+
|
|
504
|
+
```yaml
|
|
505
|
+
memory:
|
|
506
|
+
provider: mem0
|
|
507
|
+
defaultTopK: 10
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- **`native` (default, recommended)**: HAI's native engine reuses vecdb, reldb, LLM, and Embedding. `extract` uses **Mem0-style batch merging**: one LLM call decides ADD / UPDATE / DELETE / NONE for extracted facts and related existing memories, providing incremental updates, deduplication, contradiction removal, and `category` labels. `maxEntriesPerObject`, `maxEntriesGlobal`, `recencyDecay`, `embeddingEnabled`, and `writebackRelatedTopK` apply. Eviction is partitioned by `objectId`, so heavy writes by one subject do not evict others' memories. `scope` candidates are narrowed by `objectId` (≤ `maxEntriesPerObject`); PostgreSQL also pushes scope into a JSONB containment query using a **GIN index**. SQLite/MySQL fall back to equivalent in-memory matching.
|
|
511
|
+
- **`mem0` (mem0ai/oss)**: directly embeds the `mem0ai/oss` `Memory` engine without cloud services. LLM/Embedder come from `llm` configuration; `qdrant` / `pgvector` can reuse vecdb. Unmappable backends such as `lancedb` / `chroma` fail fast unless `memory.allowEphemeralFallback: true` explicitly allows mem0 in-memory storage, preventing silent memory loss after restart. History is disabled by default.
|
|
512
|
+
|
|
513
|
+
Both providers share `ai.memory.*` (`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`) with `objectId` isolation and key-value `scope` filters such as `{ topicId, personaId }`. `recall` / `list` / `listPage` / `clear` strictly filter scope; `clear` with `types` / `scope` deletes only jointly matching entries. mem0 `extract` uses the shared extractor for classification/scoring (`types` / `model` / `minImportance` / `systemPrompt`), then writes with `infer:false`, preserving `hai_type` / `hai_importance`. Its `recall` supports `types` and `recencyWeight` decay, matching native behavior. One difference: mem0 `update` of type/importance/metadata rebuilds memory and changes `id`; native keeps IDs stable.
|
|
514
|
+
|
|
515
|
+
**Candidate pools and scope recall misses**: filtering scope after truncating to `topK` can let higher-ranked memories from other topics/personas displace the target scope. `recall` / `injectMemories` therefore fetch `topK × candidateMultiplier` candidates (default multiplier 5), filter, then take `topK`. Increase `candidateMultiplier` for finer isolation such as `topicId` + `personaId`:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const memories = await ai.memory.recall('经济发展', {
|
|
519
|
+
objectId: 'user-001',
|
|
520
|
+
scope: { topicId: 'C' },
|
|
521
|
+
topK: 10,
|
|
522
|
+
candidateMultiplier: 8, // 覆盖配置默认值,扩大候选池
|
|
523
|
+
})
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`ai.config` returns a redacted snapshot without exposing original `apiKey`, `privateKey`, embedded URL credentials, or other secrets.
|
|
527
|
+
|
|
528
|
+
## Security boundaries
|
|
529
|
+
|
|
530
|
+
- Treat prompts, retrieved documents, and model outputs as untrusted. Do not insert user content into non-overridable system rules.
|
|
531
|
+
- Zod validates tool argument shapes, not caller permissions. High-privilege handlers must verify identity, tenant, ownership, and quota, and expose only tools allowed to execute automatically.
|
|
532
|
+
- `callRemoteAgent()` is an independent client requiring only `ai.init()`, without Agent Card or local executors. It rejects non-HTTP(S) and embedded URL credentials; applications must still allowlist origins and enforce egress protection against DNS rebinding, private-network redirects, and cloud metadata access.
|
|
533
|
+
- Do not log full prompts, tool arguments, A2A headers, temporary model credentials, or remote URLs with queries. Audits should retain redacted summaries only.
|
|
534
|
+
|
|
535
|
+
## Error handling
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
const result = await ai.llm.chat({ messages })
|
|
539
|
+
if (!result.success) {
|
|
540
|
+
switch (result.error.code) {
|
|
541
|
+
case HaiAIError.NOT_INITIALIZED.code:
|
|
542
|
+
// 先调用 ai.init()
|
|
543
|
+
break
|
|
544
|
+
case HaiAIError.API_ERROR.code:
|
|
545
|
+
// 上游模型服务失败,可重试或降级
|
|
546
|
+
break
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Common error ranges:
|
|
552
|
+
|
|
553
|
+
- `hai:ai:010-012`: initialization/configuration.
|
|
554
|
+
- `hai:ai:100-107`: LLM and history.
|
|
555
|
+
- `hai:ai:300-302`:Embedding。
|
|
556
|
+
- `hai:ai:600-701`:Retrieval/RAG。
|
|
557
|
+
- `hai:ai:800-805`:Knowledge。
|
|
558
|
+
- `hai:ai:050-059`:Audio。
|
|
559
|
+
- `hai:ai:060-065`:Image。
|
|
560
|
+
- `hai:ai:900-905`:Memory。
|
|
561
|
+
- `hai:ai:980-984`:A2A。
|
|
562
|
+
|
|
563
|
+
## Tests
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
pnpm --filter @h-ai/ai typecheck
|
|
567
|
+
pnpm --filter @h-ai/ai lint
|
|
568
|
+
pnpm --filter @h-ai/ai test
|
|
569
|
+
pnpm --filter @h-ai/ai build
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## License
|
|
573
|
+
|
|
574
|
+
Apache-2.0
|