@h-ai/ai 0.1.0-alpha.56 → 0.1.0-alpha.57
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/package.json +6 -6
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
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# @h-ai/ai
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/api-contract](../api-contract/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/datapipe](../datapipe/README.ja.md) · [@h-ai/reldb](../reldb/README.ja.md) · [@h-ai/vecdb](../vecdb/README.ja.md)
|
|
6
|
+
|
|
7
|
+
共通の `ai` サービスで LLM 対話、画像生成、ツール、MCP、Embedding、記憶、検索/RAG、知識ベース、コンテキスト管理、ファイル解析、Rerank、音声(ASR/TTS)、A2A を提供します。Node.js では `ai.init()` で初期化し、ブラウザーからは API/client を経由します。
|
|
8
|
+
|
|
9
|
+
## 機能概要
|
|
10
|
+
|
|
11
|
+
- `ai.tools`:ツール定義・登録・一括実行。初期化不要。
|
|
12
|
+
- `ai.stream`:ChatCompletion のストリーミング chunk 処理。初期化不要。
|
|
13
|
+
- `ai.llm`:通常・ストリーミング対話、モデル一覧、履歴。
|
|
14
|
+
- `ai.mcp` / `createMcpServer`:組み込み MCP 登録機能と独立した MCP Server。
|
|
15
|
+
- `ai.embedding`:単一・一括テキストのベクトル化。
|
|
16
|
+
- `ai.memory`:記憶の抽出・保存・呼び出し・注入。
|
|
17
|
+
- `ai.persona`:AI の人格プロフィール(システムプロンプト + 特性)とプロンプトの合成。
|
|
18
|
+
- `ai.retrieval` / `ai.rag`:複数ソースのベクトル検索と検索拡張型の回答。
|
|
19
|
+
- `ai.knowledge`:文書の取り込み、エンティティを使う検索、知識ベースの回答。
|
|
20
|
+
- `ai.context`:LLM + Memory + RAG + 圧縮を組み合わせたセッション管理。
|
|
21
|
+
- `ai.file` / `ai.rerank`:ファイル解析/OCR とテキストの再順位付け。
|
|
22
|
+
- `ai.audio`:通常・ストリーミングの音声認識(ASR)と音声合成(TTS)。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、Pollinations の無料枠モデルによる画像生成。
|
|
24
|
+
- `ai.a2a`:Agent-to-Agent のリクエスト処理とリモート呼び出し。
|
|
25
|
+
- `@h-ai/ai/client`:API サービスと連携する軽量フロントエンドクライアント。
|
|
26
|
+
- `AIStoreProvider`:共通のストレージ抽象。DB がなければプロセス内の一時 Store、reldb + vecdb が初期化済みなら永続 DB Provider を自動使用。
|
|
27
|
+
|
|
28
|
+
詳しいメソッド一覧・エラーコード・長い例は [REFERENCE.md(中国語)](./REFERENCE.md) を参照してください。
|
|
29
|
+
|
|
30
|
+
## クイックスタート
|
|
31
|
+
|
|
32
|
+
### LLM のみ(DB 依存なし)
|
|
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
|
+
reldb/vecdb が未初期化の場合、AI はセッションなどの状態をプロセス内の一時 Store に保存します。`ai.close()`、プロセス終了、インスタンス切り替え後は保持されません。Memory、Context、Persona、セッションを再起動後も保持する場合は以下の DB Provider を使います。
|
|
54
|
+
|
|
55
|
+
### 永続 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
|
+
### 独自 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 契約
|
|
104
|
+
|
|
105
|
+
- 公開入口は `ai` サービスと `createMcpServer` など少数の独立ファクトリー。
|
|
106
|
+
- ライフサイクルは `await ai.init(config, options?)` / `await ai.close()`。終了時は独自 `AIStoreProvider.close()` を待ちます。
|
|
107
|
+
- ドメイン操作は `HaiResult<T>` または `Promise<HaiResult<T>>` を返し、`result.success === false` と `result.error.code` で失敗を表します。`AsyncIterable`、クライアント通信、外部コールバックは開始・反復時に例外を投げる場合があります。
|
|
108
|
+
- `ai.tools` と `ai.stream` は純粋関数で、初期化せず使用できます。
|
|
109
|
+
- reldb/vecdb がなければプロセス内の一時 Store、両方が初期化済みなら DB Provider を自動使用します。独自 Provider はほかの保存先を隠蔽できます。
|
|
110
|
+
|
|
111
|
+
## API 一覧
|
|
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
|
+
### ツール呼び出し
|
|
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 プロトコルの境界
|
|
176
|
+
|
|
177
|
+
`ai.a2a.getAgentCard()` は `protocolVersion`、MIME、streaming 機能を含む完全な A2A 0.3 Agent Card を返します。Kit の既定検出パスは `/.well-known/agent-card.json`。`handleRequest()` は常に JSON-RPC/SSE の結果を返し、未初期化時は JSON-RPC エラーです。信頼された HTTP の `agentId` を executor の `context.user.userName` に対応付けます。JSON-RPC/SSE に対応し、push notifications・gRPC・REST 対応は宣言しません。業務リソースの認可はアプリの責任です。
|
|
178
|
+
|
|
179
|
+
A2A のタスク・メッセージテーブルは `ai.init()` で初期化します。取得・キャンセル・継続を含め、タスクは信頼された `agentId` で分離します。匿名は共通 scope なので、分離が必要な環境は認証を有効にしてください。`listMessages` はサーバー管理 API で `callerId`・`contextId`・`since` を使えますが、未認可クライアントへ公開しません。旧タスクの主体は信頼できず、更新時に所有者の推定や自動削除はしません。アプリ側の審査後に `object_id` を `user:<agentId>`(匿名は `anonymous`)へ移行します。主体・context を持たない旧メッセージは対応フィルターに一致しません。
|
|
180
|
+
|
|
181
|
+
### MCP Server
|
|
182
|
+
|
|
183
|
+
`ai.mcp` はプロセス内の登録機能、独立 `createMcpServer` は SDK transport を使い、互いの自動登録はしません。`ai.mcp` は登録時にツールの JSON Schema をコンパイルし、呼び出し時に入力を検証します。リソースは `text` / `blob` のいずれか一方だけを持ちます。プロンプト引数・SDK メッセージ出力も検証し、画像・音声に対応します。HTTP 例は transport 接続のみで、アプリがリクエスト処理・認証を接続し、終了時に server を閉じる必要があります。
|
|
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
|
+
先に `ai.init()` で音声モデルを登録し、既定の認識・合成モデルを対応付けます。認証情報はプラットフォームの環境変数へフォールバックします。LLM と音声が同じ認証情報を使うと確認できた場合にのみ、`inheritLlmApiKey` を明示的に有効化してください:
|
|
227
|
+
|
|
228
|
+
`provider` は展開場所ではなく呼び出しプロトコルを表します。`openai` / `mimo` / `qwen` / `doubao` はクラウド、`whisper`(ASR)/ `indextts`(TTS)は自前配置可能なプロトコルです。既定 endpoint がないため `baseUrl` が必要で、同じ方式でクラウド・社内・ローカルを切り替えても業務コードは変わりません。クラウドの認証不足は `CONFIGURATION_ERROR`。自前サービスは認証情報を省略可能で、`apiKey` 指定時は `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` は共通の `AudioContent` バイト列を返します。API でブラウザー再生可能な Base64 を返すには `serializePlayableAudio(result.data)` を使い、`pcm16` を WAV コンテナーに包み、WAV/MP3 はそのまま渡します:
|
|
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` は LLM・Audio・Image の任意の鍵に共通で、YAML の `null`、空文字列、空白のみを `undefined` に正規化します。音声の優先順位はモデルの `apiKey` → 継承有効時の LLM `apiKey` → プラットフォーム環境変数です。異なるサービスの鍵の誤使用を防ぐため `inheritLlmApiKey` は既定で無効です。
|
|
324
|
+
|
|
325
|
+
> `synthesizeStream` は厳密に `segment_started → audio* → segment_done` を返します。`segment_started` はサーバーが Provider を解決した**実際の出力音声パラメーター**(`format` / `sampleRate` / `channels`)を持ちます。プレイヤーは要求値から推測せず、この値で復号し、実際の再生完了後にのみ該当テキストを `spokenText` へ追加してください。再生状態はアプリが管理します。
|
|
326
|
+
|
|
327
|
+
キャンセル・時間超過・接続障害はドメインエラーです。`AbortSignal` は `AUDIO_CANCELLED`、時間超過は `AUDIO_TIMEOUT`、接続失敗・`end` 前の異常切断は `AUDIO_CONNECTION_FAILED`。接続時間は `audio.maxStreamDurationMs`(既定 5 分)で制限します。
|
|
328
|
+
|
|
329
|
+
ブラウザー・モバイルは `@h-ai/serv` の共通音声 WebSocket を使用します。`@h-ai/ai/client` は通信詳細を隠して Node と同じ `audio.*` API を提供します。クライアントは正常終了・キャンセル(`AUDIO_CANCELLED`)・異常切断(`AUDIO_CONNECTION_FAILED`)を区別し、キャンセルや `end` 前の切断で対応コードを throw します。`synthesize` は未完了の部分音声を成功として返しません。
|
|
330
|
+
|
|
331
|
+
> 自前の CPU/GPU モデルサービス(faster-whisper / IndexTTS / Qwen3-4B)、重み取得、オフライン化は [models/(中国語)](./models/README.md) を参照してください。イメージ・重みは ModelScope を優先し、HuggingFace ミラーへ自動フォールバックします。
|
|
332
|
+
|
|
333
|
+
### 画像生成(Image)
|
|
334
|
+
|
|
335
|
+
`ai.init()` で登録後、呼び出し元は標準化された画像バイト列のみを受け取り、サービス固有の Base64・インラインデータ・一時 URL を意識しません:
|
|
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` は省略可能です。公開層は画像バイト列と MIME のみを受け付け、multipart・`inlineData`・Data URL は Provider が変換します。認証情報はモデル項目、`HAI_AI_IMAGE_<PROVIDER>_API_KEY`、サービス環境変数で指定できます。Qwen・Seedream の一時 URL は Provider 内ですぐダウンロードし、公開戻り値に出しません。詳細は [REFERENCE.md(中国語)](./REFERENCE.md#image) を参照してください。
|
|
362
|
+
|
|
363
|
+
### Context 管理
|
|
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
|
+
同じ管理器では既定で**同時に 1 つの生成のみ**を許可し、前の AI が終了する前に次の user が書き込まれる順序乱れを防ぎます。`reset()` は非同期で、活動中の生成を停止し、並行処理の障壁を解放、ターンを消去し、既定で Persona/System Prompt を再登録します。
|
|
381
|
+
|
|
382
|
+
#### 実際の対話状態(Conversation Commit Layer)
|
|
383
|
+
|
|
384
|
+
既定(`turnCommit: 'auto'`)では `chat` / `chatStream` が**生成した全文**を context に保存します。しかし生成 → TTS → 再生の途中で発話が遮られた場合、次のターンで共有すべき状態は全文ではなく**実際に再生された部分**です。
|
|
385
|
+
|
|
386
|
+
`turnCommit: 'manual'` では自動保存せず `turnId` を返し、呼び出し元が実際に起きた内容を確認して明示的に保存します。
|
|
387
|
+
|
|
388
|
+
`chatStream` は**モデル呼び出し前**にターン登録と `turn_started` を行います(`turn_started → delta* → done`、キャンセル時は `turn_started → delta* → cancelled`)。途中で `AbortSignal` により中止しても `turnId` と生成済みテキストを取得し、実際の内容を保存できます:
|
|
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? })`:実際の文を保存(省略時は生成全文)し、`completed` に設定。
|
|
415
|
+
- `interruptTurn(turnId, { text? })`:実際に表現した部分のみ保存(省略時は未発話として保存なし)し、`interrupted` に設定。
|
|
416
|
+
- `committed` の内容のみが context・記憶抽出に入り、未保存・破棄された部分は後続ターンへ影響しません。
|
|
417
|
+
- 流れの完了前に `interruptTurn` で遮った場合(司会が割り込む瞬間にモデルが終了する場合など)、`chatStream` は **`done` を返しません**。正常終了と誤認した追加保存を防ぎます。
|
|
418
|
+
|
|
419
|
+
#### セッションの定着(Memory ライフサイクル)
|
|
420
|
+
|
|
421
|
+
セッション中は各ターンで `scope: { sessionId }` の**短期セッション記憶**を抽出し、終了時に `consolidate()` で
|
|
422
|
+
短期記憶 + 要約を**セッションをまたぐ長期記憶**にします。`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 の人格)
|
|
434
|
+
|
|
435
|
+
Memory は `objectId` / `scope` で誰の記憶かを表し、Persona は AI が誰かを定義します。各 AI に
|
|
436
|
+
安定したシステムプロンプト・性格を定義し、`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` は `save` / `get` / `update` / `remove` / `list` / `compose` を提供します。プロフィールは全体で共有し、`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
|
+
`core.config.load('ai', ...)` では規約に沿う環境変数が YAML より優先されます:
|
|
500
|
+
例:`HAI_AI_LLM_APIKEY`、`HAI_AI_LLM_BASEURL`、`HAI_AI_LLM_MODEL`。
|
|
501
|
+
|
|
502
|
+
mem0(組み込み mem0ai/oss エンジン)の有効化:
|
|
503
|
+
|
|
504
|
+
```yaml
|
|
505
|
+
memory:
|
|
506
|
+
provider: mem0
|
|
507
|
+
defaultTopK: 10
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- **`native`(既定・推奨)**:HAI のネイティブエンジンは vecdb、reldb、LLM、Embedding を再利用します。`extract` は **Mem0 型の一括マージ**で、抽出した事実と既存記憶に 1 回の LLM 呼び出しで ADD / UPDATE / DELETE / NONE を決定し、増分更新・重複除去・矛盾削除・`category` 分類を実現します。`maxEntriesPerObject`、`maxEntriesGlobal`、`recencyDecay`、`embeddingEnabled`、`writebackRelatedTopK` が適用されます。削除は `objectId` ごとなので、ある主体の大量書き込みでほかの主体の記憶は失いません。scope の候補は `objectId` で絞り(≤ `maxEntriesPerObject`)、PostgreSQL はさらに JSONB 包含検索へ scope を渡して **GIN インデックス**を使います。SQLite/MySQL はメモリー照合で同じ結果を返します。
|
|
511
|
+
- **`mem0`(mem0ai/oss)**:`mem0ai/oss` の `Memory` エンジンを直接組み込み、クラウドを使いません。LLM/Embedder は `llm` 設定から取得し、`qdrant` / `pgvector` は vecdb を再利用できます。`lancedb` / `chroma` など対応できないバックエンドは即座に失敗し、明示的な `memory.allowEphemeralFallback: true` の場合のみメモリーへ移行します。再起動後の無通知の記憶消失を防ぎ、履歴は既定で無効です。
|
|
512
|
+
|
|
513
|
+
両 Provider は同じ `ai.memory.*`(`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`)を提供し、`objectId` の主体分離と `{ topicId, personaId }` などの `scope` をサポートします。`recall` / `list` / `listPage` / `clear` は scope を厳密に絞り、`clear` に `types` / `scope` を渡すと同時一致のみを削除します。mem0 の `extract` は共通抽出器で分類・採点(`types` / `model` / `minImportance` / `systemPrompt`)後、`infer:false` で `hai_type` / `hai_importance` を保存します。`recall` も `types` と `recencyWeight` 減衰に対応し、native と同じ動作です。ただし mem0 の type/importance/metadata 更新は記憶を再作成して `id` を変更し、native は維持します。
|
|
514
|
+
|
|
515
|
+
**候補集合と scope の検索漏れ**:`topK` で先に切り捨てると、別の話題・人格の関連性が高い記憶が対象 scope を押し出します。そのため `recall` / `injectMemories` は `topK × candidateMultiplier`(既定 5)件を取得してから絞り、`topK` 件にします。`topicId` + `personaId` など細かく分離する場合は `candidateMultiplier` を増やせます:
|
|
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` はマスク済み設定を返し、`apiKey`、`privateKey`、URL 内認証情報などをそのまま公開しません。
|
|
527
|
+
|
|
528
|
+
## セキュリティ上の境界
|
|
529
|
+
|
|
530
|
+
- Prompt・検索文書・モデル出力は信頼できない入力として扱い、変更不可のシステム規則へユーザー内容を組み込まないでください。
|
|
531
|
+
- Zod はツール引数の形を検証するだけで権限を保証しません。高権限 handler は主体・テナント・所有者・割当量を確認し、自動実行を許可したツールのみをモデルへ登録してください。
|
|
532
|
+
- `callRemoteAgent()` は `ai.init()` のみで使える独立クライアントで、Agent Card・ローカル executor は不要です。非 HTTP(S)・URL 内認証情報を拒否しますが、アプリは origin 許可リストと出口側での DNS リバインディング・内部網へのリダイレクト・クラウドメタデータ対策を行う必要があります。
|
|
533
|
+
- 完全な Prompt・ツール引数・A2A headers・一時認証情報・query 付き遠隔 URL を記録せず、監査が必要な場合もマスク済みの概要のみ保存してください。
|
|
534
|
+
|
|
535
|
+
## エラー処理
|
|
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
|
+
主なエラーコード範囲:
|
|
552
|
+
|
|
553
|
+
- `hai:ai:010-012`:初期化・設定。
|
|
554
|
+
- `hai:ai:100-107`:LLM・履歴。
|
|
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
|
+
## テスト
|
|
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
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @h-ai/ai
|
|
2
2
|
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[框架首页](../../README.md) · 相关模块:[@h-ai/api-contract](../api-contract/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/datapipe](../datapipe/README.md) · [@h-ai/reldb](../reldb/README.md) · [@h-ai/vecdb](../vecdb/README.md)
|
|
6
|
+
|
|
3
7
|
AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、文生图、工具调用、MCP、Embedding、记忆、检索/RAG、知识库、上下文管理、文件解析、Rerank、语音(ASR/TTS)与 A2A。Node.js 侧通过 `ai.init()` 初始化,浏览器侧通过 API/client 代理访问。
|
|
4
8
|
|
|
5
9
|
## 能力概览
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/ai",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.57",
|
|
5
5
|
"description": "Hai Framework AI module (LLM, MCP, tools, and clients).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"undici": "^7.24.7",
|
|
38
38
|
"ws": "^8.18.3",
|
|
39
39
|
"zod": "^4.4.3",
|
|
40
|
-
"@h-ai/
|
|
41
|
-
"@h-ai/
|
|
42
|
-
"@h-ai/
|
|
43
|
-
"@h-ai/
|
|
44
|
-
"@h-ai/
|
|
40
|
+
"@h-ai/core": "0.1.0-alpha.57",
|
|
41
|
+
"@h-ai/api-contract": "0.1.0-alpha.57",
|
|
42
|
+
"@h-ai/datapipe": "0.1.0-alpha.57",
|
|
43
|
+
"@h-ai/vecdb": "0.1.0-alpha.57",
|
|
44
|
+
"@h-ai/reldb": "0.1.0-alpha.57"
|
|
45
45
|
},
|
|
46
46
|
"optionalDependencies": {
|
|
47
47
|
"better-sqlite3": "^12.6.2"
|