@h-ai/ai 0.1.0-alpha9 → 1.0.0-beta.10
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 +676 -0
- package/README.ja.md +676 -0
- package/README.md +491 -1678
- package/dist/ai-acp-types-DQ8jO9Ie.d.ts +9763 -0
- package/dist/ai-audio-types-4R5yUAVl.d.ts +614 -0
- package/dist/browser.d.ts +10 -6
- package/dist/browser.js +3 -2
- package/dist/chunk-HEYVFQC6.js +1232 -0
- package/dist/chunk-HEYVFQC6.js.map +1 -0
- package/dist/chunk-M77C3VAP.js +139 -0
- package/dist/chunk-M77C3VAP.js.map +1 -0
- package/dist/chunk-O6P4P5PC.js +254 -0
- package/dist/chunk-O6P4P5PC.js.map +1 -0
- package/dist/client/index.d.ts +63 -231
- package/dist/client/index.js +2 -1
- package/dist/index.d.ts +50 -23
- package/dist/index.js +9869 -1687
- package/dist/index.js.map +1 -1
- package/models/agents/README.md +48 -0
- package/models/agents/build.mjs +66 -0
- package/models/agents/codex-acp/Dockerfile +25 -0
- package/models/agents/codex-acp/agent.json +16 -0
- package/models/agents/deepseek-harness/Dockerfile +23 -0
- package/models/agents/deepseek-harness/agent.json +14 -0
- package/models/agents/goose/Dockerfile +34 -0
- package/models/agents/goose/agent.json +14 -0
- package/package.json +48 -22
- package/dist/ai-reasoning-types-CGUgkLF7.d.ts +0 -2823
- package/dist/ai-types-CRCjluXI.d.ts +0 -1095
- package/dist/api/index.d.ts +0 -628
- package/dist/api/index.js +0 -243
- package/dist/api/index.js.map +0 -1
- package/dist/chunk-6PPXWQ7I.js +0 -462
- package/dist/chunk-6PPXWQ7I.js.map +0 -1
- package/dist/chunk-ABP7RKIP.js +0 -311
- package/dist/chunk-ABP7RKIP.js.map +0 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
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
|
+
## Introduction
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
## Core features
|
|
12
|
+
|
|
13
|
+
- `ai.tools`: tool definitions, registries, and batch execution; no initialization required.
|
|
14
|
+
- `ai.stream`: ChatCompletion stream chunk processing; no initialization required.
|
|
15
|
+
- `ai.llm`: non-streaming/streaming chat, model lists, and history.
|
|
16
|
+
- `ai.mcp` / `createMcpServer`: an in-process MCP registry and a standalone MCP server.
|
|
17
|
+
- `ai.embedding`: individual/batch text embeddings.
|
|
18
|
+
- `ai.memory`: memory extraction, storage, recall, and injection.
|
|
19
|
+
- `ai.persona`: AI persona profiles (system prompts + traits) and prompt composition.
|
|
20
|
+
- `ai.retrieval` / `ai.rag`: multi-source vector retrieval and retrieval-augmented answers.
|
|
21
|
+
- `ai.knowledge`: document ingestion, entity-enhanced retrieval, and knowledge answers.
|
|
22
|
+
- `ai.context`: integrated sessions combining LLM + Memory + RAG + compression.
|
|
23
|
+
- `ai.file` / `ai.rerank`: file parsing/OCR and text reranking.
|
|
24
|
+
- `ai.audio`: ASR and TTS with full and streaming calls for OpenAI / MiMo / Qwen / Doubao.
|
|
25
|
+
- `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.
|
|
26
|
+
- `ai.a2a`: Agent-to-Agent request handling and remote calls.
|
|
27
|
+
- `@h-ai/ai/client`: standalone browser Audio WebSocket client.
|
|
28
|
+
|
|
29
|
+
`@h-ai/ai/client` exports only `createAudioClient({ url, getTicket })` and Audio client types. Use typed domain methods from `@h-ai/api-client` for HTTP. Default chat completions are non-streaming; bind application contracts for streaming and A2A. Redundant string-path clients and SSE helpers have been removed. The browser does not export the Node `ai` singleton.
|
|
30
|
+
|
|
31
|
+
- `AIStoreProvider`: storage abstraction. Production requires `stateScope: 'shared'`; the built-in provider needs initialized PostgreSQL/MySQL and a network vecdb. Development/explicit tests may use a temporary store.
|
|
32
|
+
|
|
33
|
+
For the full method list, error codes, and longer examples, see [REFERENCE.md (Chinese)](./REFERENCE.md).
|
|
34
|
+
|
|
35
|
+
## Getting started
|
|
36
|
+
|
|
37
|
+
### Quick start
|
|
38
|
+
|
|
39
|
+
#### Development LLM-only (no database dependencies)
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { ai } from '@h-ai/ai'
|
|
43
|
+
|
|
44
|
+
const init = await ai.init({
|
|
45
|
+
llm: {
|
|
46
|
+
model: 'gpt-4o-mini',
|
|
47
|
+
apiKey: process.env.HAI_AI_LLM_APIKEY,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
if (!init.success)
|
|
51
|
+
return init
|
|
52
|
+
|
|
53
|
+
const result = await ai.llm.chat({
|
|
54
|
+
messages: [{ role: 'user', content: '你好!' }],
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
await ai.close()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Development/explicit tests without initialized reldb/vecdb use a temporary process store, lost on close, exit, or instance switching. When either `NODE_ENV` or `HAI_CORE_ENV` is production, temporary stores, SQLite and local vector backends fail initialization. Initialize shared RelDB and network vecdb first, or supply a shared provider with equivalent atomic guarantees.
|
|
61
|
+
|
|
62
|
+
#### Persistent DB provider (reldb + vecdb)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { ai } from '@h-ai/ai'
|
|
66
|
+
import { reldb } from '@h-ai/reldb'
|
|
67
|
+
import { vecdb } from '@h-ai/vecdb'
|
|
68
|
+
|
|
69
|
+
// 开发/测试使用本地后端;生产改为共享 PostgreSQL/MySQL 与网络 vecdb。
|
|
70
|
+
const failures: unknown[] = []
|
|
71
|
+
try {
|
|
72
|
+
const database = await reldb.init({ type: 'sqlite', database: './ai.db' })
|
|
73
|
+
if (!database.success)
|
|
74
|
+
throw new Error(database.error.message)
|
|
75
|
+
const vectors = await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
|
|
76
|
+
if (!vectors.success)
|
|
77
|
+
throw new Error(vectors.error.message)
|
|
78
|
+
const initialized = await ai.init({
|
|
79
|
+
llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY },
|
|
80
|
+
})
|
|
81
|
+
if (!initialized.success)
|
|
82
|
+
throw new Error(initialized.error.message)
|
|
83
|
+
const result = await ai.llm.chat({ messages: [{ role: 'user', content: 'Hello!' }] })
|
|
84
|
+
if (!result.success)
|
|
85
|
+
throw new Error(result.error.message)
|
|
86
|
+
const reply = result.data.choices[0]?.message.content ?? ''
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
failures.push(error)
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await ai.close()
|
|
93
|
+
for (const service of [vecdb, reldb]) {
|
|
94
|
+
const closed = await service.close()
|
|
95
|
+
if (!closed.success)
|
|
96
|
+
failures.push(new Error(closed.error.message))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (failures.length)
|
|
100
|
+
throw new AggregateError(failures, 'AI operation or cleanup failed')
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Concurrent MySQL first startup accepts only the explicit mysql2 ER_DUP_KEYNAME/1061 error, then verifies the actual columns and order, non-unique full columns, ascending visible BTREE, and no expressions. An existing index with the wrong definition fails. The account needs table creation and INDEX privileges; metadata, permission, or other DDL failures return CONFIGURATION_ERROR from ai.init. All started DDL settles before return. MySQL DDL is not rolled back: repair privileges/schema before retrying partial initialization. Check each initialization result, reject business traffic on failure, and close AI, vecdb, and reldb in reverse order.
|
|
104
|
+
|
|
105
|
+
#### Custom StoreProvider
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import type { AIStoreProvider } from '@h-ai/ai'
|
|
109
|
+
import { ai } from '@h-ai/ai'
|
|
110
|
+
|
|
111
|
+
const storeProvider: AIStoreProvider = createMyStoreProvider()
|
|
112
|
+
|
|
113
|
+
await ai.init(
|
|
114
|
+
{ llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY } },
|
|
115
|
+
{ storeProvider },
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
await ai.close()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### API contracts
|
|
122
|
+
|
|
123
|
+
- Public access uses the `ai` service and a few standalone factories such as `createMcpServer`.
|
|
124
|
+
- Lifecycle: `await ai.init(config, options?)` / `await ai.close()`; closure waits for custom `AIStoreProvider.close()`.
|
|
125
|
+
- 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.
|
|
126
|
+
- `ai.tools` and `ai.stream` are pure-function subsystems usable without initialization.
|
|
127
|
+
- Development/tests without reldb/vecdb use temporary storage; both initialized selects DB storage. Production validates the real database/vector backend capability. Custom providers must accurately declare `stateScope`.
|
|
128
|
+
|
|
129
|
+
Inject stores through the public AIStoreProvider extension. Built-in LLM providers, factory dependencies and Context dependencies are internal. Configure model endpoints through configuration; history queries, AskOptions and ChatRecord remain public.
|
|
130
|
+
|
|
131
|
+
### API overview
|
|
132
|
+
|
|
133
|
+
#### LLM
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const result = await ai.llm.chat({ messages })
|
|
137
|
+
for await (const chunk of ai.llm.chatStream({ messages })) {
|
|
138
|
+
const delta = chunk.choices[0]?.delta?.content
|
|
139
|
+
if (delta) {
|
|
140
|
+
process.stdout.write(delta)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 请求取消:传入 AbortSignal,主持人打断/用户切换时 abort() 立即停止上游生成与计费
|
|
145
|
+
const controller = new AbortController()
|
|
146
|
+
const cancellable = ai.llm.chat({ messages, signal: controller.signal })
|
|
147
|
+
// controller.abort()
|
|
148
|
+
|
|
149
|
+
// 多协议:模型的 protocol 决定底层走 Chat Completions / Responses / Anthropic,公共请求响应形状不变
|
|
150
|
+
// - chat(默认):OpenAI Chat Completions(兼容绝大多数厂商)
|
|
151
|
+
// - responses:OpenAI Responses API(/v1/responses)
|
|
152
|
+
// - anthropic:Anthropic Messages API(Claude 原生协议,环境变量 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL)
|
|
153
|
+
const claude = await ai.llm.chat({ messages, model: 'claude' }) // 该模型配置 protocol: anthropic
|
|
154
|
+
|
|
155
|
+
// 临时模型:单次请求绕过配置注册模型,直接指定端点与凭据(chat/chatStream/ask/askStream 均支持)
|
|
156
|
+
// 临时客户端按 TTL 缓存(llm.tempModelCacheTtl,默认 10 分钟),与常驻模型客户端隔离
|
|
157
|
+
const temp = await ai.llm.chat({
|
|
158
|
+
messages,
|
|
159
|
+
tempModel: { model: 'claude-3-5-sonnet', protocol: 'anthropic', apiKey: 'sk-temp' },
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
SSE text decoding follows [WHATWG event-stream rules](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation): split LF/CRLF/CR, multiline data, one optional space, inherited valid id/retry, default message events, and no dispatch of unfinished EOF data. encodeSSE normalizes data line endings and explicitly throws for invalid metadata. Anthropic/MiMo share an internal UTF-8 reader that cancels/releases the body on exit, cancellation, termination or failure, without reconnecting or replaying business operations. Anthropic deadlines cover the body; MiMo retains separate deadlines per HTTP request.
|
|
164
|
+
|
|
165
|
+
#### Tool calls
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { z } from 'zod'
|
|
169
|
+
|
|
170
|
+
const registry = ai.tools.createRegistry({
|
|
171
|
+
// 统一授权在 Zod 校验后、handler 前执行;false/异常均 fail-closed
|
|
172
|
+
authorize: ({ toolName, context }) => canExecuteTool(context.objectId, toolName),
|
|
173
|
+
})
|
|
174
|
+
const registered = registry.register(ai.tools.define({
|
|
175
|
+
name: 'get_weather',
|
|
176
|
+
description: '获取天气',
|
|
177
|
+
parameters: z.object({ city: z.string() }),
|
|
178
|
+
// handler 第二参为执行上下文:可响应取消(打断/超时)、感知截止时间与交互主体
|
|
179
|
+
handler: async ({ city }, { signal }) => {
|
|
180
|
+
const res = await fetch(`https://api.example.com/weather?city=${city}`, { signal })
|
|
181
|
+
return res.json()
|
|
182
|
+
},
|
|
183
|
+
timeoutMs: 10_000, // 本工具默认超时(可被 execute 的 deadline / timeoutMs 覆盖)
|
|
184
|
+
}))
|
|
185
|
+
if (!registered.success)
|
|
186
|
+
return registered
|
|
187
|
+
|
|
188
|
+
// 执行时可传入取消信号 / 超时 / 作用域;取消或超时返回 TOOL_TIMEOUT,且不再等待未响应的 handler
|
|
189
|
+
const result = await registry.execute(toolCall, { signal: controller.signal, objectId: 'user-001', timeoutMs: 30_000 })
|
|
190
|
+
|
|
191
|
+
// 批量调用默认串行,避免副作用工具并发启动;纯读取且确认安全时才显式并行
|
|
192
|
+
const batch = await registry.executeAll(toolCalls)
|
|
193
|
+
|
|
194
|
+
const chat = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### ACP agents, workspaces and sessions
|
|
198
|
+
|
|
199
|
+
`ai.acp` runs on Node: `embedded` starts a local process, `builtin-service` starts a Podman/Docker container (ready Podman first), and `custom-service` connects to a native HTTP/WS ACP service. Codex/dsh use stdio; unsupported transports are not silently bridged.
|
|
200
|
+
|
|
201
|
+
Podman does not inherit proxy variables from its remote service. Configure proxies explicitly through agent/workspace `environmentRefs`, using addresses reachable from the container.
|
|
202
|
+
|
|
203
|
+
The Goose 1.57.0 image exposes its native `goose serve` HTTP/WS `/acp` service through custom-service with `X-Secret-Key`. Goose manages its models; deployment owns workspace/state volumes. Real remote E2E verifies native resume when advertised.
|
|
204
|
+
|
|
205
|
+
- `agents.register/get/list/update/remove/initialize/check` manage definitions. The `codex` and `deepseek-harness` adapters reference `llm.models`; use `generic` or trusted `AIInitOptions.acpAdapters` for other agents. External services own their models.
|
|
206
|
+
- `workspaces.register/prepare/importFiles` manage directories or persistent volumes, multiple Git repositories and plain files. Every repository requires `mainBranch`; prepare preserves dirty directories.
|
|
207
|
+
- `sessions.create/resume/get/list/close` manage native sessions. `newWorktree: true` creates matching branches from each committed local main branch. Prompt, stream, strict structured output, permission callbacks, MCP, configuration and cancellation use native ACP capabilities.
|
|
208
|
+
- `session.updateMain()` fetches and fast-forwards configured main branches. `publish({ mode: 'merge-main' | 'push-branch', message, author })` publishes managed isolated sessions only, with per-repository partial-failure reports and no force push. Both stop execution first; resume explicitly to continue.
|
|
209
|
+
|
|
210
|
+
Declare verified capacity, modalities and reasoning in `llm.models[].capabilities`. Codex needs native Responses; the existing Hai adapter is not a reverse gateway. Code and state persist after close. CAS ownership is released only after owned processes stop; worktrees are not security sandboxes.
|
|
211
|
+
|
|
212
|
+
See the [ACP guide](../cli/templates/skills/hai-ai/acp.md) and [agent images](models/agents/README.md). Images include Git/Node/Python, optional Java/Rust/Go, and both codex-acp and codex in the Codex image. Errors use `hai:ai:070`–`084`; ordinary methods return HaiResult, while stream failures throw.
|
|
213
|
+
|
|
214
|
+
Managed Git rejects submodules and Git LFS attributes with ACP_UNSUPPORTED instead of reporting incomplete checkouts as ready.
|
|
215
|
+
|
|
216
|
+
#### Configuration and database resource catalogs
|
|
217
|
+
|
|
218
|
+
`llm.models`, `audio.models`, `image.models`, `mcp.servers`, `retrieval.sources`, and `acp.agents/workspaces` merge configuration and database entries. Configuration wins duplicate IDs and is read-only. Database entries support list/get/register/update/remove with value/source/version metadata and optional version checks. Async calls read current definitions; synchronous audio capability checks use the latest local snapshot. Catalog errors use 090–094. Definitions may contain model credentials; expose only authorized, selected fields.
|
|
219
|
+
|
|
220
|
+
[Resource catalogs](../cli/templates/skills/hai-ai/resources.md) · [ACP](../cli/templates/skills/hai-ai/acp.md)
|
|
221
|
+
|
|
222
|
+
#### A2A protocol boundaries
|
|
223
|
+
|
|
224
|
+
`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.
|
|
225
|
+
|
|
226
|
+
A2A journal, context ownership and message tables initialize during `ai.init()`. Tasks use trusted `agentId` scopes for get/cancel/continuation. Anonymous requests share one principal; authenticate at the trusted transport boundary in production. `listMessages` is a server administration API with `callerId`, `contextId`, and `since`; never expose it to unauthorized clients.
|
|
227
|
+
|
|
228
|
+
Tasks and events use one shared journal, without a separate legacy TaskStore. Push notification configuration and authenticated extended card RPCs return standard SDK unsupported/unconfigured errors without executing tasks. Executor types remain public; internal handler/Store assembly types are outside the public contract.
|
|
229
|
+
|
|
230
|
+
Inbound messages must persist before the executor starts, with shared generation/deadline checks repeated after that wait. Agent messages await logging inside the existing bounded event queue before journal publication. Logging failure produces a failed/interrupted Task rather than a false success; retrying the same message does not rerun business effects. Logs and the journal are separate records: a log is not proof of delivery. Reconcile possible effects using the Task's `requiresEffectReconciliation` marker. Query failures return `A2A_LIST_MESSAGES_FAILED` (984) without raw SQL or connection details.
|
|
231
|
+
|
|
232
|
+
Task and events publish in one CAS, so any node can subscribe. A retry with identical message ID/content reuses the current result; changed content or an older round fails. Cross-node cancellation invokes the real execution owner's executor.cancelTask. Pending cancellation blocks new messages on the same Task so a late callback cannot affect a later execution. Input-required tasks can continue after node restart. Expired leases, absolute deadlines, or an executor returning only working become failed/interrupted with `metadata.haiExecution.requiresEffectReconciliation=true`; late owners cannot write and unknown external effects are never replayed automatically. `a2a.execution` defaults to `leaseMs: 30000`, `pollIntervalMs: 250`, `maxExecutionMs: 300000`. Limits are 1 MiB/message, 8 MiB/journal, 2048 events/round, 256 receipts/Task and 32 reference tasks. Use stable business idempotency keys and reconcile unknown effects. See the [state/deployment matrix (Chinese)](../../.agents/instructions/distributed-state.md) for guarantees and validation boundaries.
|
|
233
|
+
|
|
234
|
+
#### MCP Server
|
|
235
|
+
|
|
236
|
+
`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.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { createMcpServer, StreamableHTTPServerTransport } from '@h-ai/ai'
|
|
240
|
+
import { z } from 'zod'
|
|
241
|
+
|
|
242
|
+
const server = createMcpServer({ name: 'my-server', version: '1.0.0' })
|
|
243
|
+
server.registerTool('search', {
|
|
244
|
+
description: '搜索',
|
|
245
|
+
inputSchema: { query: z.string() },
|
|
246
|
+
}, async ({ query }) => ({
|
|
247
|
+
content: [{ type: 'text', text: `Results for ${query}` }],
|
|
248
|
+
}))
|
|
249
|
+
await server.connect(new StreamableHTTPServerTransport({ sessionIdGenerator: () => globalThis.crypto.randomUUID() }))
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Memory / RAG / Knowledge
|
|
253
|
+
|
|
254
|
+
Each Knowledge ingestion creates a unique `revision` and chunk IDs. It prepares all chunks, embeddings and entities, persists a staging manifest, writes vectors, then publishes the complete document and entity snapshot with shared Store CAS. Only one writer can rebuild a document at a time. Failure preserves the complete old version; shorter replacements cannot expose old trailing chunks. Retrieval filters unpublished revisions and retries around publication; sustained contention returns an error. Candidate expansion and cleanup backlogs can increase query cost.
|
|
255
|
+
|
|
256
|
+
Successful ingestion returns `revision` and `cleanupPending`. A true flag means the new version is published but old vectors still require cleanup. Paginate `repair({ collection, offset, limit })` regularly across every manifest; setup checks the first 100. Staging expires after five minutes according to the storage clock and an expired writer cannot publish. Abandoned writes retain permanent cleanup tombstones so later scans can remove delayed external writes; do not prune these with logs. SQL and vector storage do not share a transaction. Removal hides data first and reports cleanup failures. Collection deletion retains the empty vector collection and generation tombstones for compensation; setup never revives the previous generation.
|
|
257
|
+
|
|
258
|
+
The DB Store propagates collection and vector failures. LanceDB uses persistent primary-key atomic merge. Qdrant writes/deletes wait until applied and counts are exact. Multiple nodes must share relational storage and a server vector backend. Custom KnowledgeStore implementations must provide the same publication and compensation contract.
|
|
259
|
+
|
|
260
|
+
With LanceDB, collection dimensions and metrics persist in the Arrow schema; connection metrics only affect new collections. Reuse the same persistent directory after restart and rebuild collections when embedding dimensions change. Local paths on separate nodes do not share data; distributed Knowledge should use a server vector backend.
|
|
261
|
+
|
|
262
|
+
Knowledge `retrieve/ask` and the default DB Store forward `filter` to the vector provider and propagate validation/query failures. Knowledge returns `KNOWLEDGE_RETRIEVE_FAILED` (`RAG_FAILED` from ask), without disguising failure as empty results. LanceDB supports AND equality on literal top-level keys with string, finite number, boolean, or null values, such as `{ tenant: 'acme', active: true }`. Null never matches missing keys; objects, arrays, and undefined are unsupported. Filtering precedes vector topK. Check null/complex-value semantics separately for other backends. LanceDB metadata now uses native JSON fields and retains nested JSON; rebuild legacy string-metadata collections before writing or filtering.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const enriched = await ai.memory.injectMemories(messages, { objectId: 'user-001', topK: 5 })
|
|
266
|
+
if (!enriched.success) {
|
|
267
|
+
return enriched
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 多租户推荐入口:scoped() 绑定主体与作用域,所有操作自动携带 objectId / scope(含归属校验),杜绝「忘记传 objectId」越权
|
|
271
|
+
const memory = ai.memory.scoped({ objectId: 'user-001', scope: { topicId: 't-1', personaId: 'p-1' } })
|
|
272
|
+
await memory.add({ content: '用户偏好中文', type: 'preference' })
|
|
273
|
+
const recalled = await memory.recall('语言偏好')
|
|
274
|
+
|
|
275
|
+
// clear 拒绝空过滤(防误清全局);全局清空只能显式走管理接口
|
|
276
|
+
await memory.clear({ types: ['event'] }) // 仅清该作用域内的 event
|
|
277
|
+
await ai.memory.admin.clearAll({ confirm: true }) // 危险:清空整个记忆后端,需显式确认
|
|
278
|
+
|
|
279
|
+
const rag = await ai.rag.query('核心架构是什么?', { sources: ['docs'], topK: 5 })
|
|
280
|
+
|
|
281
|
+
const setup = await ai.knowledge.setup()
|
|
282
|
+
if (setup.success) {
|
|
283
|
+
const ingested = await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
|
|
284
|
+
if (!ingested.success)
|
|
285
|
+
return ingested
|
|
286
|
+
if (ingested.data.cleanupPending) {
|
|
287
|
+
const repaired = await ai.knowledge.repair({ offset: 0, limit: 100 })
|
|
288
|
+
if (!repaired.success)
|
|
289
|
+
return repaired
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Knowledge chunks use datapipe units and hard bounds: words for word mode, Unicode code points otherwise, retaining separators from cleaned text. AI defaults remain markdown/maxSize=1500/overlap=200; a smaller maxSize override must also keep inherited overlap below it. Invalid chunk configuration or expressions return KNOWLEDGE_INGEST_FAILED before embedding and revision publication, without an unbounded whole-document fallback.
|
|
295
|
+
|
|
296
|
+
#### Audio
|
|
297
|
+
|
|
298
|
+
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:
|
|
299
|
+
|
|
300
|
+
`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`.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
await ai.init({
|
|
304
|
+
llm: {
|
|
305
|
+
apiKey: process.env.HAI_AI_LLM_APIKEY,
|
|
306
|
+
},
|
|
307
|
+
audio: {
|
|
308
|
+
inheritLlmApiKey: true,
|
|
309
|
+
models: [
|
|
310
|
+
{ id: 'asr', provider: 'qwen', model: 'qwen3-asr-flash-realtime', operations: ['transcribe'] },
|
|
311
|
+
{ id: 'tts', provider: 'qwen', model: 'qwen3-tts-flash-realtime', operations: ['synthesize'] },
|
|
312
|
+
// 自托管:whisper / indextts 必须显式配置 baseUrl
|
|
313
|
+
{ id: 'whisper', provider: 'whisper', model: 'faster-whisper-large-v3', operations: ['transcribe'], baseUrl: 'http://127.0.0.1:8101/v1' },
|
|
314
|
+
{ id: 'indextts', provider: 'indextts', model: 'indextts-2.5', operations: ['synthesize'], baseUrl: 'http://127.0.0.1:8102/v1' },
|
|
315
|
+
],
|
|
316
|
+
transcribeModel: 'asr',
|
|
317
|
+
synthesizeModel: 'tts',
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// 完整识别(可选热词、时间戳粒度与 VAD;strictCapabilities 要求模型真实支持所请求能力)
|
|
322
|
+
const result = await ai.audio.transcribe({
|
|
323
|
+
audio: { data: wavBytes, format: 'wav' },
|
|
324
|
+
language: 'zh',
|
|
325
|
+
contextHints: ['专有名词'],
|
|
326
|
+
timestampGranularities: ['segment', 'word'],
|
|
327
|
+
vad: true,
|
|
328
|
+
model: 'whisper',
|
|
329
|
+
})
|
|
330
|
+
if (result.success) {
|
|
331
|
+
const text = result.data.text
|
|
332
|
+
const language = result.data.language // 实际检测语言
|
|
333
|
+
const words = result.data.segments?.[0]?.words // 词级时间轴(毫秒整数)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 说话人 / 风格参考合成(IndexTTS):speakerReference 表达“谁在说”,styleReference 表达“怎么说”
|
|
337
|
+
const cloned = await ai.audio.synthesize({
|
|
338
|
+
text: '我们下午三点出发。',
|
|
339
|
+
language: 'zh',
|
|
340
|
+
model: 'indextts',
|
|
341
|
+
speakerReference: { audio: speakerWav, language: 'ja' },
|
|
342
|
+
styleReference: { audio: styleWav },
|
|
343
|
+
styleStrength: 0.8,
|
|
344
|
+
targetDurationMs: 3280, // 目标时长(与 speed 互斥),metadata.durationMatched 反馈是否达标
|
|
345
|
+
durationToleranceMs: 120,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// 实时识别(持续音频输入 → 领域事件流:speech_started / transcript / speech_stopped)
|
|
349
|
+
for await (const event of ai.audio.transcribeStream({
|
|
350
|
+
audio: { chunks: microphoneChunks, format: 'pcm16', sampleRate: 16000 },
|
|
351
|
+
})) {
|
|
352
|
+
if (event.type === 'speech_started')
|
|
353
|
+
onSpeechStart() // 支持服务端 VAD 的平台会在检测到说话时立即产出,可据此取消上游生成
|
|
354
|
+
else if (event.type === 'transcript')
|
|
355
|
+
updateTranscript(event.text, event.final)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 流式合成:调用方为文本段分配稳定 ID,事件可精确关联文本与音频;signal 可随时打断
|
|
359
|
+
const controller = new AbortController()
|
|
360
|
+
for await (const event of ai.audio.synthesizeStream({
|
|
361
|
+
text: { id: 'answer-1', text: '欢迎参加访谈。' },
|
|
362
|
+
voice: 'Cherry',
|
|
363
|
+
instruction: '用轻快的语气',
|
|
364
|
+
signal: controller.signal,
|
|
365
|
+
})) {
|
|
366
|
+
if (event.type === 'segment_started')
|
|
367
|
+
prepareDecoder(event.format, event.sampleRate, event.channels) // 真实输出格式来自服务端解析后的 Provider 输出
|
|
368
|
+
else if (event.type === 'audio')
|
|
369
|
+
await player.write(event.data)
|
|
370
|
+
else if (event.type === 'segment_done')
|
|
371
|
+
markSegmentReadyToCommit(event.segmentId)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 实时会话启动前按操作校验模型能力
|
|
375
|
+
const caps = ai.audio.getCapabilities({ operation: 'synthesize', model: 'tts' })
|
|
376
|
+
if (caps.success && caps.data.synthesize?.streamingAudioOutput) { /* 可实时 TTS */ }
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`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:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { serializePlayableAudio } from '@h-ai/ai'
|
|
383
|
+
|
|
384
|
+
const synthesized = await ai.audio.synthesize({ text: '欢迎参加访谈。', voice: 'Cherry' })
|
|
385
|
+
if (!synthesized.success)
|
|
386
|
+
return synthesized
|
|
387
|
+
|
|
388
|
+
const playable = serializePlayableAudio(synthesized.data)
|
|
389
|
+
if (!playable.success)
|
|
390
|
+
return playable
|
|
391
|
+
|
|
392
|
+
return playable.data
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`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.
|
|
396
|
+
|
|
397
|
+
> `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.
|
|
398
|
+
|
|
399
|
+
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).
|
|
400
|
+
|
|
401
|
+
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.
|
|
402
|
+
|
|
403
|
+
IndexTTS pairs the same Undici version's fetch, Agent, and multipart types on Node. Model `timeout` covers response-header and body waits; user cancellation returns `AUDIO_CANCELLED`, and deadlines return `AUDIO_TIMEOUT`. Dedicated connections close after success, failure, or cancellation. Segmented streaming fallback retains errors during iteration.
|
|
404
|
+
|
|
405
|
+
> 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.
|
|
406
|
+
|
|
407
|
+
#### Image generation
|
|
408
|
+
|
|
409
|
+
After model registration in `ai.init()`, callers receive normalized image bytes without vendor-specific Base64, inline data, or temporary URLs:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
await ai.init({
|
|
413
|
+
image: {
|
|
414
|
+
models: [
|
|
415
|
+
{ id: 'image', provider: 'openai', model: 'gpt-image-2' },
|
|
416
|
+
{ id: 'free', provider: 'pollinations', model: 'zimage' },
|
|
417
|
+
],
|
|
418
|
+
generateModel: 'image',
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const result = await ai.image.generate({
|
|
423
|
+
prompt: '一个正在构建开源框架的友好机器人',
|
|
424
|
+
size: { width: 1024, height: 1024 },
|
|
425
|
+
referenceImages: [
|
|
426
|
+
{ data: await readImage('character.png'), mimeType: 'image/png' },
|
|
427
|
+
],
|
|
428
|
+
})
|
|
429
|
+
if (result.success) {
|
|
430
|
+
const { data, mimeType } = result.data.images[0]!
|
|
431
|
+
await saveImage(data, mimeType)
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`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>_APIKEY` / 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.
|
|
436
|
+
|
|
437
|
+
Downloads require public HTTPS on default port 443 without URL credentials. Socket DNS lookup validates every returned address and connects using those same results. Private, loopback, link-local, mapped/translated, and reserved IPv4/IPv6 addresses are blocked. Each of at most three redirects is checked again. Downloads use isolated connections without vendor authentication or application cookies. `baseUrl` is trusted operator configuration; vendor API calls reject redirects.
|
|
438
|
+
|
|
439
|
+
`image.maxImageBytes` defaults to 20971520 (20 MiB; maximum 100 MiB) per output/reference image. `image.maxImages` defaults to 4 (maximum 16) for output/reference counts, with sequential URL downloads. Actual streamed bytes, including decompressed bytes, are bounded and excess responses are cancelled. JSON responses are limited to `max(64 KiB, 2 × maxImageBytes)` and error bodies to 64 KiB. Model `timeout` defaults to 120000ms for the entire vendor request, redirects, and downloads; `signal` cancels the whole process. Outputs require matching PNG/JPEG/GIF/WebP MIME and magic bytes; SVG is rejected. Magic checks do not decode complete images or verify dimensions. Address rejection, limits, and deadlines return `IMAGE_DOWNLOAD_BLOCKED`, `IMAGE_TOO_LARGE`, and `IMAGE_TIMEOUT`; check success before use.
|
|
440
|
+
|
|
441
|
+
#### Context manager
|
|
442
|
+
|
|
443
|
+
Automatic memory extraction runs locally in the background; `pendingMemoryTasks=0` does not imply success. `flush()` waits for the batch and returns `MEMORY_EXTRACT_FAILED` with a count for thrown errors or failed HaiResults. Concurrent observers share that result. Returning acknowledges the report; later flushes observe new tasks without replaying partially completed effects. `save()`, `reset({ waitForMemoryTasks: true })`, and `consolidate()` return that failure before subsequent writes. Reconcile external effects after a process crash.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
const manager = ai.context.createManager({
|
|
447
|
+
scope: { objectId: 'user-001', sessionId: 'sess-001' },
|
|
448
|
+
compress: { auto: true, strategy: 'hybrid', maxTokens: 8000 },
|
|
449
|
+
memory: { enable: true, enableExtract: true },
|
|
450
|
+
concurrency: 'reject', // 单活动生成(默认):活动生成期间的新 chat 返回 CONTEXT_BUSY;'queue' 则排队
|
|
451
|
+
})
|
|
452
|
+
if (manager.success) {
|
|
453
|
+
const reply = await manager.data.chat('你好')
|
|
454
|
+
const saved = await manager.data.save()
|
|
455
|
+
if (!saved.success)
|
|
456
|
+
return saved
|
|
457
|
+
// reset 生命周期完整:终止活动生成、清空消息/摘要/轮次,默认保留系统提示词
|
|
458
|
+
await manager.data.reset() // 可传 { preserveSystemPrompt, cancelActiveTurn, waitForMemoryTasks }
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
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.
|
|
463
|
+
|
|
464
|
+
##### Actual conversation state (Conversation Commit Layer)
|
|
465
|
+
|
|
466
|
+
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.
|
|
467
|
+
|
|
468
|
+
With `turnCommit: 'manual'`, results are not stored automatically. A `turnId` is returned so the caller can explicitly commit the text reflecting what actually happened.
|
|
469
|
+
|
|
470
|
+
`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:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
const created = ai.context.createManager({ scope: { objectId: 'user-001', sessionId: 'sess-001' }, turnCommit: 'manual' })
|
|
474
|
+
if (!created.success)
|
|
475
|
+
return created
|
|
476
|
+
const m = created.data
|
|
477
|
+
const controller = new AbortController()
|
|
478
|
+
|
|
479
|
+
for await (const ev of m.chatStream('请展开讲讲', { signal: controller.signal })) {
|
|
480
|
+
if (ev.type === 'turn_started') {
|
|
481
|
+
await m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
|
|
482
|
+
}
|
|
483
|
+
else if (ev.type === 'delta') {
|
|
484
|
+
feedTts(ev.text) // 边生成边合成播放
|
|
485
|
+
}
|
|
486
|
+
else if (ev.type === 'done') {
|
|
487
|
+
await m.commitTurn(ev.turnId) // 完整提交
|
|
488
|
+
}
|
|
489
|
+
else if (ev.type === 'cancelled') {
|
|
490
|
+
// 生成被 controller.abort() 取消:轮次保留,只提交实际播放出去的部分
|
|
491
|
+
await m.interruptTurn(ev.turnId, { text: actuallySpokenText })
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 观测每一轮的 generated / committed / status
|
|
496
|
+
const turns = m.getTurns()
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
- `commitTurn(turnId, { text? })`: commit actual text (defaults to full generated text), setting state to `completed`.
|
|
500
|
+
- `interruptTurn(turnId, { text? })`: store only what was actually expressed (omission means nothing was expressed, so nothing is stored), setting state to `interrupted`.
|
|
501
|
+
- Only `committed` content enters context and memory extraction. Uncommitted/discarded text does not affect future turns.
|
|
502
|
+
- 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.
|
|
503
|
+
|
|
504
|
+
##### Session consolidation (Memory lifecycle)
|
|
505
|
+
|
|
506
|
+
During a session, each turn extracts **short-term session memory** with `scope: { sessionId }`. At session end, `consolidate()`
|
|
507
|
+
turns short-term memory + summaries into **cross-session long-term memory**, completing `Session Memory → Summary → Long-term Memory`:
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
// 会话结束时固化:整合摘要 → 提取长期记忆(写入不含 sessionId 的持久作用域)
|
|
511
|
+
const result = await manager.consolidate({ scope: { userId: 'user-001', personaId: 'xiaoq' } })
|
|
512
|
+
if (result.success) {
|
|
513
|
+
// result.data.summary —— 本次会话整合摘要
|
|
514
|
+
// result.data.memories —— 固化到长期记忆的条目
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### Persona (AI character profiles)
|
|
519
|
+
|
|
520
|
+
Memory uses `objectId` / `scope` to identify whose memories they are; Persona defines who the AI is through
|
|
521
|
+
stable system prompts and character traits, linked to long-term memory through `scope: { personaId }`:
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
await ai.persona.save({
|
|
525
|
+
id: 'xiaoq',
|
|
526
|
+
name: '小Q',
|
|
527
|
+
systemPrompt: '你是一位经济学家,善于从长期视角分析问题。',
|
|
528
|
+
traits: ['数据驱动', '偏好引用真实案例'],
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
|
|
532
|
+
const composed = await ai.persona.compose('xiaoq')
|
|
533
|
+
const manager = ai.context.createManager({
|
|
534
|
+
scope: { objectId: 'user-001', sessionId: 'persona-session' },
|
|
535
|
+
systemPrompt: composed.data,
|
|
536
|
+
memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
|
|
537
|
+
})
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
`ai.persona` exposes `save` / `get` / `update` / `remove` / `list` / `compose`. Profiles are shared globally, not isolated by `objectId`.
|
|
541
|
+
|
|
542
|
+
### Configuration
|
|
543
|
+
|
|
544
|
+
Rerank inherits the global LLM timeout with per-model overrides; the deadline covers the request and response body, returning RERANK_API_ERROR on timeout.
|
|
545
|
+
|
|
546
|
+
The LLM protocol field is now `protocol` in `llm`, `llm.models[]`, and `tempModel`, replacing `api`. Values remain `chat | responses | anthropic`. Use `HAI_AI_LLM_PROTOCOL` and `HAI_AI_LLM_MODELS_0_PROTOCOL` (adjust the index for other entries). Old fields and protocol variables fail with a migration message; `apiKey` and its variables are unchanged.
|
|
547
|
+
|
|
548
|
+
```yaml
|
|
549
|
+
llm:
|
|
550
|
+
apiKey: ''
|
|
551
|
+
baseUrl: https://api.openai.com/v1
|
|
552
|
+
model: gpt-4o-mini
|
|
553
|
+
protocol: chat # chat(默认)| responses | anthropic —— 底层 API 协议,对使用方透明
|
|
554
|
+
timeout: 60000
|
|
555
|
+
tempModelCacheTtl: 600000 # 临时模型客户端缓存 TTL(毫秒,默认 10 分钟)
|
|
556
|
+
models: # 可为每个模型单独指定协议
|
|
557
|
+
- {id: fast, model: gpt-4o-mini}
|
|
558
|
+
- {id: strong, model: gpt-4.1, protocol: responses}
|
|
559
|
+
- {id: claude, model: claude-3-5-sonnet-latest, protocol: anthropic}
|
|
560
|
+
scenarios:
|
|
561
|
+
chat: fast
|
|
562
|
+
reasoning: strong
|
|
563
|
+
embedding: embed
|
|
564
|
+
|
|
565
|
+
embedding:
|
|
566
|
+
dimensions: 1536
|
|
567
|
+
batchSize: 100
|
|
568
|
+
|
|
569
|
+
knowledge:
|
|
570
|
+
collection: hai_ai_knowledge
|
|
571
|
+
dimension: 1536
|
|
572
|
+
cleanOptions:
|
|
573
|
+
removeHtml: true
|
|
574
|
+
normalizeWhitespace: true
|
|
575
|
+
chunkOptions:
|
|
576
|
+
mode: markdown
|
|
577
|
+
maxSize: 1500
|
|
578
|
+
overlap: 200
|
|
579
|
+
|
|
580
|
+
memory:
|
|
581
|
+
provider: native # native | mem0
|
|
582
|
+
maxEntriesPerObject: 1000 # 单主体(objectId)最大记忆条数
|
|
583
|
+
maxEntriesGlobal: 100000 # 跨所有主体的全局上限
|
|
584
|
+
recencyDecay: 0.95
|
|
585
|
+
embeddingEnabled: true
|
|
586
|
+
defaultTopK: 10
|
|
587
|
+
candidateMultiplier: 5 # 候选池倍数:先取回 topK×倍数 条候选,再按 scope/重要性过滤,最后截取 topK
|
|
588
|
+
writebackRelatedTopK: 20
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
When loading through `core.config.load('ai', ...)`, convention-based environment variables override YAML:
|
|
592
|
+
for example `HAI_AI_LLM_APIKEY`, `HAI_AI_LLM_BASEURL`, and `HAI_AI_LLM_MODEL`.
|
|
593
|
+
|
|
594
|
+
Enable mem0 (the embedded mem0ai/oss engine):
|
|
595
|
+
|
|
596
|
+
```yaml
|
|
597
|
+
memory:
|
|
598
|
+
provider: mem0
|
|
599
|
+
defaultTopK: 10
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
- **`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.
|
|
603
|
+
- **`mem0` (mem0ai/oss)**: directly embeds the `mem0ai/oss` `Memory` engine without cloud services. LLM/Embedder come from `llm` configuration; `qdrant` can reuse vecdb. The adapter maps only Qdrant with complete connection settings. Production (`NODE_ENV` or `HAI_CORE_ENV=production`) rejects actual ephemeral fallback even when `memory.allowEphemeralFallback: true`; missing backends also fail. Explicit fallback is limited to development or `HAI_E2E=1` / `HAI_E2E_MOCK=1`. History is disabled by default.
|
|
604
|
+
|
|
605
|
+
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. Mem0 updates text and metadata in place, preserving `id` and creation time. SDK queries use `filters.user_id`; writes and whole-subject deletion use `userId`. With no SDK list cursor, `list`, `listPage`, and filtered `clear` support at most 1000 raw subject candidates, including expired records. One extra candidate detects truncation; exceeding the bound returns `MEMORY_LIST_INCOMPLETE` (906), without a fabricated total or partial filtered deletion. Complete candidates are filtered by UTC expiration date, type and scope before pagination. Whole-subject `deleteAll` is independent of this listing limit. Public empty-filter clear is rejected; only trusted administration may explicitly call `admin.clearAll({ confirm: true })`.
|
|
606
|
+
|
|
607
|
+
Mem0 3.3.1 still calls `QdrantClient.search`. The catalog and AI optional peer pin the tested `@qdrant/js-client-rest` version `1.18.0`; startup checks this public method and rejects incompatible installations. Do not override it with 1.19, which removed that method.
|
|
608
|
+
|
|
609
|
+
The adapter injects a dedicated client through the SDK's public `client` configuration. Mem0 Qdrant upserts and deletes use `wait: true`, awaiting server application before readback or the next deletion batch.
|
|
610
|
+
|
|
611
|
+
Mem0 with pgvector is rejected in every environment: the SDK privately owns a PostgreSQL connection without a public close API, so the framework cannot manage its lifecycle. Use `memory.provider: native` for pgvector; native support is retained. `disableHistory: true` selects a resource-free history implementation and creates no history SQLite file. The factory awaits backend readiness through one bounded public read before reporting success.
|
|
612
|
+
|
|
613
|
+
SDK add receipts omit full business fields, so `add` / `extract` read the stored record before returning. Extraction waits for all pending writes. A write or readback failure sets `error.ext.requiresEffectReconciliation=true` and retains known IDs in `memoryIds` (possibly empty if receipts were lost). Reconcile subject records before recovery; blindly repeating add/extract can duplicate committed writes. The framework does not replay automatically.
|
|
614
|
+
|
|
615
|
+
**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`:
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
const memories = await ai.memory.recall('经济发展', {
|
|
619
|
+
objectId: 'user-001',
|
|
620
|
+
scope: { topicId: 'C' },
|
|
621
|
+
topK: 10,
|
|
622
|
+
candidateMultiplier: 8, // 覆盖配置默认值,扩大候选池
|
|
623
|
+
})
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`ai.config` returns a redacted snapshot without exposing original `apiKey`, `privateKey`, embedded URL credentials, or other secrets.
|
|
627
|
+
|
|
628
|
+
### Security boundaries
|
|
629
|
+
|
|
630
|
+
- Treat prompts, retrieved documents, and model outputs as untrusted. Do not insert user content into non-overridable system rules.
|
|
631
|
+
- 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.
|
|
632
|
+
- `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.
|
|
633
|
+
- Do not log full prompts, tool arguments, A2A headers, temporary model credentials, or remote URLs with queries. Audits should retain redacted summaries only.
|
|
634
|
+
|
|
635
|
+
### Error handling
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
const result = await ai.llm.chat({ messages })
|
|
639
|
+
if (!result.success) {
|
|
640
|
+
switch (result.error.code) {
|
|
641
|
+
case HaiAIError.NOT_INITIALIZED.code:
|
|
642
|
+
// 先调用 ai.init()
|
|
643
|
+
break
|
|
644
|
+
case HaiAIError.API_ERROR.code:
|
|
645
|
+
// 上游模型服务失败,可重试或降级
|
|
646
|
+
break
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Common error ranges:
|
|
652
|
+
|
|
653
|
+
- `hai:ai:010-012`: initialization/configuration.
|
|
654
|
+
- `hai:ai:100-107`: LLM and history.
|
|
655
|
+
- `hai:ai:300-302`:Embedding。
|
|
656
|
+
- `hai:ai:600-701`:Retrieval/RAG。
|
|
657
|
+
- `hai:ai:800-805`:Knowledge。
|
|
658
|
+
- `hai:ai:050-059`:Audio。
|
|
659
|
+
- `hai:ai:060-068`: Image, including blocked downloads, resource limits, and total deadlines.
|
|
660
|
+
- `hai:ai:900-906`:Memory。
|
|
661
|
+
- `hai:ai:980-984`:A2A。
|
|
662
|
+
|
|
663
|
+
## Development and testing
|
|
664
|
+
|
|
665
|
+
```bash
|
|
666
|
+
pnpm --filter @h-ai/ai typecheck
|
|
667
|
+
pnpm --filter @h-ai/ai lint
|
|
668
|
+
pnpm --filter @h-ai/ai test
|
|
669
|
+
pnpm --filter @h-ai/ai build
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Public JSDoc examples import actual package entrypoints; internal factory examples are marked with their module context and compiled against real local declarations. Run `pnpm check:jsdoc --package ai` from the repository root to verify coverage and example types. Ordinary business calls check HaiResult; streams and the browser AudioClient retain their explicit throwing contracts.
|
|
673
|
+
|
|
674
|
+
## License
|
|
675
|
+
|
|
676
|
+
Apache-2.0
|