@easbot/llm 0.3.19
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/LICENSE +21 -0
- package/README.en.md +419 -0
- package/README.md +420 -0
- package/dist/assets/txt/model/graph-summary.txt +54 -0
- package/dist/assets/txt/model/knowledge-summary.txt +81 -0
- package/dist/assets/txt/model/memory-summary.txt +80 -0
- package/dist/assets/txt/model/summary.txt +53 -0
- package/dist/chunks/chunk-3BANRW36.mjs +2 -0
- package/dist/chunks/chunk-3IYCLIOJ.cjs +1 -0
- package/dist/chunks/chunk-S4DPVKMV.mjs +1 -0
- package/dist/chunks/chunk-YDIWY3EH.cjs +2 -0
- package/dist/chunks/copilot-5N6U442L.cjs +1 -0
- package/dist/chunks/copilot-TJATIV2S.mjs +1 -0
- package/dist/chunks/models-snapshot-3EXCDFMS.mjs +2 -0
- package/dist/chunks/models-snapshot-U7TQPEPU.cjs +2 -0
- package/dist/index.cjs +73 -0
- package/dist/index.d.cts +1903 -0
- package/dist/index.d.ts +1903 -0
- package/dist/index.mjs +73 -0
- package/package.json +113 -0
package/README.md
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# @easbot/llm
|
|
2
|
+
|
|
3
|
+
[English](./README.en.md) | 中文
|
|
4
|
+
|
|
5
|
+
`@easbot/llm` 是 EASBot 生态系统的 **AI 模型聚合层**,负责多厂商 SDK 的动态加载、模型路由、配置管理与高层 Model Provider 的能力封装。给业务包(agent / codebase / memory / note / skills / gateway)提供统一的 LLM 调用抽象,避免每个工具包重复实现 provider 路由、auth 管理和错误归一化。
|
|
6
|
+
|
|
7
|
+
## 当前状态(V4 重构后)
|
|
8
|
+
|
|
9
|
+
本包已从 `@easbot/types` 的早期占位实现演化为承载 **provider/* + model/* 两套核心模块** 的生产级 AI 网关层:
|
|
10
|
+
|
|
11
|
+
- ✅ **`src/provider/`**(7 文件):完整的工业级 provider 抽象
|
|
12
|
+
- `Provider` 命名空间 —— 25+ 内置 SDK(OpenAI / Anthropic / Azure / Bedrock / Vertex / OpenRouter / Copilot / Ollama / 本地模型等)的动态加载器、`ModelsDev` catalog 合并、模型缓存、SDK 实例池化
|
|
13
|
+
- `ProviderError` 命名空间 —— APICallError 解析(含 25+ 种"上下文溢出"模式正则识别)
|
|
14
|
+
- `ProviderAuth` 命名空间 —— OAuth + API key 双轨认证
|
|
15
|
+
- `ProviderTransform` 命名空间 —— 模型 variant / tool / 限制处理
|
|
16
|
+
- `ModelsDev` 命名空间 —— models.dev catalog 的 zod schema + 加载 + 远程刷新
|
|
17
|
+
- `definition.ts` —— DEFAULT_ENABLED_PROVIDERS 白名单 + CUSTOM_PROVIDER_DEFINITIONS 本地 SDK 快照 + PROVIDER_DISPLAY_NAMES 友好名映射
|
|
18
|
+
- `sdk/copilot/` —— 自研 GitHub Copilot 兼容 SDK(chat + responses + 6 个 tool)
|
|
19
|
+
- ✅ **`src/model/`**(4 命名空间):高层 Model Provider
|
|
20
|
+
- `RerankProvider` 命名空间 —— LLM 驱动的文档重排序(与 Memory 混合搜索权重统一)
|
|
21
|
+
- `GraphProvider` 命名空间 —— 从非结构化文本提取实体关系(11 类 EntityType + 9 类 RelationType)
|
|
22
|
+
- `SummaryProvider` 命名空间 —— note / memory 摘要(LLM-based)
|
|
23
|
+
- `EmbeddingProvider` 命名空间 —— 文本嵌入(单文本 / 批量;走 AI SDK 标准 `embed` / `embedMany`;默认模型 `easbot-local/bge-base-zh-v1.5`)
|
|
24
|
+
- ✅ **`src/config/`**(V3 新增):独立 LLM Config 模块,仿 `packages/gateway/src/config/` 模式
|
|
25
|
+
- `LLMConfig / ProviderConfig / ProviderAuthMethod` —— zod schema(独立可用,也可作为注入 Config 的实现来源)
|
|
26
|
+
- `Config` namespace —— `get()` / `provider(id)`(优先走注入,降级到本地 loader)
|
|
27
|
+
- ✅ **`src/interfaces.ts`**(V3 新增 / V4 精简):**Adapter Registry** + Env / Flag namespace
|
|
28
|
+
- 4 个 Provider 接口(`IConfigProvider` / `IInstanceProvider` / `IGlobalPathProvider` / `IInstallationProvider`;Auth 已下沉到 `@easbot/utils.Auth`)
|
|
29
|
+
- `setAdapterRegistry(...)` —— agent 包启动时一次注入,**完全解耦**本包对 agent 内部模块的所有依赖
|
|
30
|
+
- `getI<X>Provider()` 便捷访问器 —— 未注入时 fail-fast 抛错(**不静默退化**)
|
|
31
|
+
- `readEnvString / readFlagBool / readFlagNumber` —— 静态读取,避免对注入的依赖
|
|
32
|
+
- `Env / Flag` namespace —— 镜像 `process.env` 的静态字面(用于 IDE 提示 / 文档生成 / 配置检查)
|
|
33
|
+
- ✅ **`src/index.ts`** —— 聚合导出入口(4 provider 命名空间 + 3 model 命名空间 + 自研 Copilot SDK + 3 常量 + Config / interfaces 接入点 + AdapterRegistry + 4 Provider 接口)
|
|
34
|
+
|
|
35
|
+
## 核心特性
|
|
36
|
+
|
|
37
|
+
- **25+ 内置 AI SDK 动态加载**:按需 import,避免打包产物膨胀
|
|
38
|
+
- **三层数据源融合**:models.dev 远程 catalog + 构建时 snapshot + 本地 CUSTOM_PROVIDER_DEFINITIONS(用于 ollama / easbot-local 这类不出现在 models.dev 的本仓自研 SDK)
|
|
39
|
+
- **统一 Provider 命名空间**:`Provider.list() / getModel() / parseModel()` 一行调用,与具体 SDK 解耦
|
|
40
|
+
- **OAuth + API key 双轨**:`ProviderAuth` namespace 管理 OAuth flow / API key 持久化 / 注入到 SDK options
|
|
41
|
+
- **Context Overflow 自动识别**:跨厂商统一的"上下文超限"语义(25+ 正则模式 + 4 种 HTTP status 兜底)
|
|
42
|
+
- **自研 GitHub Copilot 兼容 SDK**:完整实现 chat + responses API + 6 个 tool(code-interpreter / file-search / web-search / image-generation / local-shell / web-search-preview)
|
|
43
|
+
- **持久化模型缓存**:构建时生成 `models-snapshot.ts`,运行时优先用本地,避免冷启动连网
|
|
44
|
+
- **极简聚合导出**:`@easbot/llm` 一行引入 5+3 个命名空间 + 自研 SDK + 关键常量
|
|
45
|
+
|
|
46
|
+
## 安装
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm add @easbot/llm
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 快速上手
|
|
53
|
+
|
|
54
|
+
### 1. 列出所有已配置的 provider
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Provider } from '@easbot/llm';
|
|
58
|
+
|
|
59
|
+
const allProviders = await Provider.list();
|
|
60
|
+
// { openai: { id, name, source, env, models, ... }, anthropic: {...}, ... }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. 获取特定模型(按 `provider/model` 字符串路由)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { Provider } from '@easbot/llm';
|
|
67
|
+
|
|
68
|
+
const gpt4 = await Provider.getModel('openai/gpt-4');
|
|
69
|
+
// gpt4: LanguageModelV3(AI SDK v6 规范)
|
|
70
|
+
|
|
71
|
+
const embed = await Provider.getModel('openai/text-embedding-3-small');
|
|
72
|
+
// embed: EmbeddingModelV3
|
|
73
|
+
|
|
74
|
+
const rerank = await Provider.getModel('easbot-local/rerank-proxy');
|
|
75
|
+
// rerank: RerankingModelV3
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. 解析模型 ID(安全拆分,不抛错)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { Provider } from '@easbot/llm';
|
|
82
|
+
|
|
83
|
+
const { providerId, modelId } = Provider.parseModel('openai/gpt-4o');
|
|
84
|
+
// { providerId: 'openai', modelId: 'gpt-4o' }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 4. 解析 API 错误(含 context overflow 识别)
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { ProviderError, APICallError } from 'ai';
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await someProvider.doCall(...);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (e instanceof APICallError) {
|
|
96
|
+
const parsed = ProviderError.parseAPICallError({ providerId: 'openai', error: e });
|
|
97
|
+
if (parsed.type === 'context_overflow') {
|
|
98
|
+
// UI: "上下文超限,请缩短输入或切换模型"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 5. 高层 Model Provider 直接调用
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { RerankProvider, GraphProvider, SummaryProvider } from '@easbot/llm';
|
|
108
|
+
|
|
109
|
+
// 文档重排序
|
|
110
|
+
const reranked = await RerankProvider.rerank({
|
|
111
|
+
query: '...',
|
|
112
|
+
documents: ['...', '...'],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 图谱实体提取
|
|
116
|
+
const graph = await GraphProvider.generateGraph({
|
|
117
|
+
chunks: ['chunk1...', 'chunk2...', '...'], // 任意长度数组
|
|
118
|
+
prompt: { maxChunks: 5, maxConcurrency: 3 }, // v0.8:per-call 切片 + 3 并发
|
|
119
|
+
});
|
|
120
|
+
// graph.entities / relations / summary 直接入库
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**v0.8 行为契约**(`packages/llm/src/model/graph.ts`):
|
|
124
|
+
|
|
125
|
+
- **所有 chunks 都被尝试抽取**:chunks 数组按 `prompt.maxChunks`(默认 5)切片成多组,每组调一次 LLM;不再像 v0.7 那样硬截断到前 5 个 chunk。
|
|
126
|
+
- **chunk 级 3 并发**:多组用轻量级 semaphore 并发跑(`prompt.maxConcurrency`,默认 3),避免触发 provider 限流。
|
|
127
|
+
- **局部失败容错**:单组 LLM 失败 → 仅丢该组结果,其他组继续。不会因一次失败让整个文档 KG 留空。
|
|
128
|
+
- **边界感知截断**:超长 chunk 走 `splitByBoundary` 进一步切分(按 `\n\n` / `\n` / 空白找最近边界),保证 entity name 不被切碎。
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// Note 知识库摘要
|
|
132
|
+
const summary = await SummaryProvider.summarizeNote({
|
|
133
|
+
query: '...',
|
|
134
|
+
chunks: [...],
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 6. 文本嵌入(EmbeddingProvider)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { EmbeddingProvider } from '@easbot/llm';
|
|
142
|
+
|
|
143
|
+
// 单文本嵌入 — 返回 EmbedOutput(含 embedding + usage.tokens)
|
|
144
|
+
const single = await EmbeddingProvider.embedText({ value: '一段中文文本' });
|
|
145
|
+
// single.embedding: number[](默认 bge-base-zh-v1.5 输出 512 维)
|
|
146
|
+
// single.usage.tokens: number
|
|
147
|
+
|
|
148
|
+
// 批量嵌入(推荐:单次 API 调用比 N 次 aiEmbed 更高效)
|
|
149
|
+
const batch = await EmbeddingProvider.embedTexts({ values: ['a', 'b', 'c'] });
|
|
150
|
+
// batch.embeddings: number[][](与 values 一一对应)
|
|
151
|
+
// batch.usage.tokens: number
|
|
152
|
+
|
|
153
|
+
// 显式指定模型(跳过 Provider.defaultModel)
|
|
154
|
+
import { Provider } from '@easbot/llm';
|
|
155
|
+
const customModel = Provider.parseModel('openai/text-embedding-3-small');
|
|
156
|
+
const r = await EmbeddingProvider.embedText({ value: 'hi', model: customModel });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 7. 使用自研 GitHub Copilot SDK
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { createOpenaiCompatible, openaiCompatible } from '@easbot/llm';
|
|
163
|
+
|
|
164
|
+
const copilot = createOpenaiCompatible({
|
|
165
|
+
apiKey: process.env.GITHUB_TOKEN,
|
|
166
|
+
});
|
|
167
|
+
const model = copilot('gpt-4o');
|
|
168
|
+
// 或使用默认实例
|
|
169
|
+
const model2 = openaiCompatible('gpt-4o');
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 顶层导出清单
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Provider 核心命名空间
|
|
176
|
+
export * as Provider from './provider/provider';
|
|
177
|
+
export * as ProviderError from './provider/error';
|
|
178
|
+
export * as ProviderAuth from './provider/auth';
|
|
179
|
+
export * as ProviderTransform from './provider/transform';
|
|
180
|
+
export * as ModelsDev from './provider/models';
|
|
181
|
+
|
|
182
|
+
// Provider 常量
|
|
183
|
+
export {
|
|
184
|
+
DEFAULT_ENABLED_PROVIDERS,
|
|
185
|
+
CUSTOM_PROVIDER_DEFINITIONS,
|
|
186
|
+
PROVIDER_DISPLAY_NAMES,
|
|
187
|
+
} from './provider/definition';
|
|
188
|
+
|
|
189
|
+
// 顶层函数
|
|
190
|
+
export { initModelsRefresh } from './provider/models';
|
|
191
|
+
|
|
192
|
+
// 自研 GitHub Copilot SDK
|
|
193
|
+
export {
|
|
194
|
+
createOpenaiCompatible,
|
|
195
|
+
openaiCompatible,
|
|
196
|
+
} from './provider/sdk/copilot';
|
|
197
|
+
export type {
|
|
198
|
+
OpenaiCompatibleProvider,
|
|
199
|
+
OpenaiCompatibleProviderSettings,
|
|
200
|
+
OpenaiCompatibleModelId,
|
|
201
|
+
} from './provider/sdk/copilot';
|
|
202
|
+
|
|
203
|
+
// 高层 Model Provider 命名空间
|
|
204
|
+
export * as RerankProvider from './model/rerank';
|
|
205
|
+
export * as GraphProvider from './model/graph';
|
|
206
|
+
export * as SummaryProvider from './model/summary';
|
|
207
|
+
export * as EmbeddingProvider from './model/embedding';
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**EmbeddingProvider 公开契约**(`packages/llm/src/model/embedding.ts`):
|
|
211
|
+
|
|
212
|
+
- `embedText(input, abortSignal?) → Promise<EmbedOutput>`
|
|
213
|
+
- `EmbedOutput = { embedding: number[]; usage: { tokens: number } }`
|
|
214
|
+
- 空 `value` 返回 `{ embedding: [], usage: { tokens: 0 } }`,**不**调用 `ai.embed`
|
|
215
|
+
- `embedTexts(inputs, abortSignal?) → Promise<EmbedBatchOutput>`
|
|
216
|
+
- `EmbedBatchOutput = { embeddings: number[][]; usage: { tokens: number } }`
|
|
217
|
+
- 空 `values` 返回 `{ embeddings: [], usage: { tokens: 0 } }`,**不**调用 `ai.embedMany`
|
|
218
|
+
- 默认模型:`easbot-local/bge-base-zh-v1.5`(导出常量 `EmbeddingProvider.DEFAULT_MODEL`)
|
|
219
|
+
- `model` 入参支持三种形态:省略 → `Provider.defaultModel` 链路;`Provider.Model` → `Provider.getEmbedding`;直接传 `EmbeddingModel` 实例(duck-type 由 `specificationVersion` 字段判定)→ 跳过 Provider 链路
|
|
220
|
+
|
|
221
|
+
## 配置注入(切换 LLM 实现的迁移指南)
|
|
222
|
+
|
|
223
|
+
V4 重构后,`Provider` / `ModelsDev` / `ProviderAuth` / `ProviderTransform` 命名空间已通过 **Adapter Registry** 模式与 agent 包完全解耦,**不再直接 `import` agent 包的任何模块**。所有运行时依赖都通过 4 个 Provider 接口显式注入(Auth 已下沉到 `@easbot/utils.Auth`)。
|
|
224
|
+
|
|
225
|
+
### 1. Adapter Registry 总览
|
|
226
|
+
|
|
227
|
+
agent 包启动时调用一次 `setAdapterRegistry(...)`,注入 4 个 Provider 实现:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import {
|
|
231
|
+
setAdapterRegistry,
|
|
232
|
+
type AdapterRegistry,
|
|
233
|
+
} from '@easbot/llm';
|
|
234
|
+
|
|
235
|
+
// 在 agent 包启动入口处(例如 Instance.provide 之前)
|
|
236
|
+
setAdapterRegistry({
|
|
237
|
+
config: Config, // IConfigProvider — get / provider
|
|
238
|
+
instance: Instance, // IInstanceProvider — getDirectory / getWorktree
|
|
239
|
+
global: Global.Path, // IGlobalPathProvider — cache / data / config
|
|
240
|
+
installation: Installation, // IInstallationProvider — getVersion
|
|
241
|
+
// V4 起 Auth 已下沉到 `@easbot/utils.Auth`,不再通过 AdapterRegistry 注入。
|
|
242
|
+
// LLM 包内部直接 `import { Auth } from '@easbot/utils'` 调用即可。
|
|
243
|
+
} satisfies AdapterRegistry);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**未注入时,所有 `getI<X>Provider()` 会 fail-fast 抛错**:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
[LLM] adapter 'config' (IConfigProvider) not registered.
|
|
250
|
+
Call setAdapterRegistry({ config: ... }) before using LLM.
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
> 设计意图:**不静默退化**。若忘注入就抛错,避免线上"undefined config"导致模型路由静默失效。
|
|
254
|
+
|
|
255
|
+
### 2. 4 个 Provider 接口契约
|
|
256
|
+
|
|
257
|
+
| 接口 | 方法 | 原 agent 模块 | 用途 |
|
|
258
|
+
|------|------|---------------|------|
|
|
259
|
+
| `IConfigProvider` | `get<T>(): Promise<T>` / `provider<T>(id): Promise<T \| undefined>` | `Config` | 用户 `~/.config/easbot/config.json` 的 llm 块(whitelist / blacklist / variant override) |
|
|
260
|
+
| `IInstanceProvider` | `getDirectory(): string` / `getWorktree(): string` | `Instance` | 实例路径与 worktree 信息 |
|
|
261
|
+
| `IGlobalPathProvider` | `cache: string` / `data: string` / `config: string` | `Global.Path` | 模型快照缓存目录 / 用户数据目录 / 全局配置目录 |
|
|
262
|
+
| `IInstallationProvider` | `getVersion(): string` | `Installation` | User-Agent / 兼容性检查 / 版本信息 |
|
|
263
|
+
|
|
264
|
+
> **V4 变更**:`IInstanceProvider.state(init)` 与 `IPluginProvider` 已被**拆分下沉**(ADR 0071)。
|
|
265
|
+
> - Provider 内部 state 缓存改用 `@easbot/utils.lazyAsync(init)`(per-process 单例 + 显式 reset)
|
|
266
|
+
> - Plugin 注册通过模块级 `_pluginAuthRegistry: Map<providerId, methods[]>` + `ProviderAuth.registerPluginAuthProvider()` 入口注入
|
|
267
|
+
|
|
268
|
+
### 3. LLM Config(独立工作 + 集成工作)
|
|
269
|
+
|
|
270
|
+
V3 新增 `src/config/` 模块,仿 `packages/gateway/src/config/` 模式:
|
|
271
|
+
|
|
272
|
+
- **独立工作**:从 `easbot.json` / `easbot.jsonc` 直接读 `llm` 字段(模型、provider、auth 等)
|
|
273
|
+
- **集成工作**:当 AdapterRegistry 已注入时,`Config.get()` 优先走注入的实现(让 agent 包能复用)
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { Config, LLMConfig } from '@easbot/llm';
|
|
277
|
+
|
|
278
|
+
const cfg = await Config.get(); // 走注入或 fallback loader
|
|
279
|
+
const openaiCfg = await Config.provider('openai');
|
|
280
|
+
|
|
281
|
+
// Schema 校验(可单独使用)
|
|
282
|
+
const parsed = LLMConfig.parse({
|
|
283
|
+
model: 'openai/gpt-4o',
|
|
284
|
+
provider: {
|
|
285
|
+
openai: {
|
|
286
|
+
npm: '@ai-sdk/openai',
|
|
287
|
+
options: { apiKey: 'sk-...' },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 4. Env / Flag namespace(镜像 process.env)
|
|
294
|
+
|
|
295
|
+
`Flag` namespace 与读取函数 (`readEnvString` / `readFlagBool`) 仍由 llm 包提供;`Env` 已下沉到 `@easbot/utils`,llm 仅透传 re-export,调用方也可直接 import `@easbot/utils` 的 `Env`:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { Flag, readEnvString, readFlagBool } from '@easbot/llm';
|
|
299
|
+
import { Env } from '@easbot/llm'; // 等价于 import { Env } from '@easbot/utils'
|
|
300
|
+
|
|
301
|
+
Flag.EASBOT_OUTPUT_TOKEN_MAX; // 数值字段,内部动态计算
|
|
302
|
+
readEnvString('OPENAI_API_KEY'); // 包装 process.env
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### 5. 调用点改造对照表
|
|
306
|
+
|
|
307
|
+
| 模块 | V3 之前 | V3 | V4(本次重构) |
|
|
308
|
+
|------|--------|----|----------------|
|
|
309
|
+
| `provider/auth.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(init)` + `_pluginAuthRegistry` 模块级 list |
|
|
310
|
+
| `provider/auth.ts` | `Auth.set(providerId, info)` | `Auth.set(...)` 直接调用 `@easbot/utils.Auth`(下沉到 utils) | `Auth.set(...)` 直接调用 `@easbot/utils.Auth`(下沉到 utils) |
|
|
311
|
+
| `provider/auth.ts` | `Plugin.definitions()` | `getIPluginProvider().definitions()` | 删除依赖,改用 `registerPluginAuthProvider()` 入口注入 |
|
|
312
|
+
| `provider/provider.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(createState)`(模块级 per-process 单例) |
|
|
313
|
+
| `config/loader.ts` | `Instance.state(...)` | `getIInstanceProvider().state(init)` | `lazyAsync(createState)`(模块级 per-process 单例) |
|
|
314
|
+
| `provider/models.ts` | `Global.Path.cache` | `getIGlobalPathProvider().cache` | (同 V3) |
|
|
315
|
+
| `config/index.ts` | `Config.get()` | `getIConfigProvider().get()` | (同 V3) |
|
|
316
|
+
|
|
317
|
+
**V4 重构动机**:`IInstanceProvider.state(init)` 把 "缓存" 与 "Instance 抽象" 耦合在一起,
|
|
318
|
+
但 state 缓存本质上是 **per-process 单例 + 显式 reset**,与 Instance.directory 无关。
|
|
319
|
+
下层到 `@easbot/utils.lazyAsync()` 后:
|
|
320
|
+
- **解耦**:`IInstanceProvider` 只剩 2 个方法(getDirectory / getWorktree),职责清晰
|
|
321
|
+
- **统一**:Project 所有需要 state 缓存的子系统(skills / mcp / plugin 等)复用同一工具
|
|
322
|
+
- **可测**:`lazyAsync().reset()` 比 mock 一个 state factory 更直接
|
|
323
|
+
|
|
324
|
+
### 6. 架构优势
|
|
325
|
+
|
|
326
|
+
- **零硬编码依赖**:`provider/*` 与 `model/*` 文件不直接 `import` agent 包的任何模块
|
|
327
|
+
- **测试友好**:测试用例用 mock AdapterRegistry 注入,无需启动 agent 实例(`tests/setup.ts` 已提供完整 mock 范例)
|
|
328
|
+
- **多入口复用**:同一套 LLM 实现可被 web / cli / gateway / monitor 等任何进程复用,只需各自注入 AdapterRegistry
|
|
329
|
+
- **契约清晰**:与 ADR 0044 (skills) / 0045 (mcp) / 0046 (plugin) 的 AdapterRegistry 模式完全一致
|
|
330
|
+
- **fail-fast 安全**:未注入立即抛错,避免线上静默退化
|
|
331
|
+
|
|
332
|
+
## Provider 路由优先级
|
|
333
|
+
|
|
334
|
+
`Provider` 在 `createState` 时按以下顺序合并 provider 注册表:
|
|
335
|
+
|
|
336
|
+
1. **models.dev 远程 catalog**:启动时若 `Flag.EASBOT_MODELS_FETCH` 开启,从 `https://models.dev/api.json` 拉取(带本地缓存)
|
|
337
|
+
2. **构建时 snapshot**:若远程拉取失败或缓存不存在,使用 `models-snapshot.ts`(构建时静态注入)
|
|
338
|
+
3. **CUSTOM_PROVIDER_DEFINITIONS**:本仓自研 SDK(`@easbot/ollama-sdk` / `@easbot/local-model-sdk`)的本地定义快照
|
|
339
|
+
4. **config.provider**:用户 `~/.config/easbot/config.json` 中的 provider 块覆盖(如自定义 baseURL / model options)
|
|
340
|
+
5. **环境变量 + Auth 持久化**:env vars + `easbot auth login` 持久化的 API key
|
|
341
|
+
6. **Plugin 注入**:插件可通过 `plugin.auth.loader` 注入特殊 provider(如 github-copilot 的 OAuth)
|
|
342
|
+
7. **CUSTOM_LOADERS**:每个 provider 的特殊初始化(anthropic headers、azure baseURL、amazon-bedrock AWS credentials 等)
|
|
343
|
+
|
|
344
|
+
## 测试
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
pnpm --filter @easbot/llm test:run
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
测试文件位于 `tests/`:
|
|
351
|
+
|
|
352
|
+
- `provider-smoke.test.ts` —— 验证 `definition.ts` 的纯常量(白名单 / 本地 SDK 快照 / 友好名映射)+ **AdapterRegistry 注入/读取/fail-fast 抛错** + Env/Flag 静态字段 + LLMConfig/ProviderConfig/ProviderAuthMethod zod schema
|
|
353
|
+
- `interfaces.test.ts` —— V4 后新增 AdapterRegistry 接口 5 个 + 静态读取函数(`readEnvString` / `readFlagBool` / `readFlagNumber`)+ `lazyAsync()` reset 行为
|
|
354
|
+
- `model-smoke.test.ts` —— 验证 `model/types.ts` 的类型导出路径稳定性
|
|
355
|
+
- `setup.ts` —— 全局测试环境(设 NODE_ENV=development + **注入 mock AdapterRegistry 全套**,避免 chain import 触发 fail-fast)
|
|
356
|
+
|
|
357
|
+
**当前 75 个测试全绿**(3 test files)。覆盖范围:
|
|
358
|
+
|
|
359
|
+
| 测试大类 | 覆盖 case 数 | 验证要点 |
|
|
360
|
+
|---------|-------------|----------|
|
|
361
|
+
| AdapterRegistry 接口(V4 新增) | 11 | 4 Provider 接口契约 / `setAdapterRegistry` 注入 / `hasAdapterRegistry` 布尔 / fail-fast 抛错 / 静态读取函数 |
|
|
362
|
+
| Provider 常量纯函数 | 11 | DEFAULT_ENABLED_PROVIDERS / CUSTOM_PROVIDER_DEFINITIONS / PROVIDER_DISPLAY_NAMES |
|
|
363
|
+
| ProviderAuth lazyAsync 行为(V4 新增) | 3 | `registerPluginAuthProvider` 触发 `_state.reset()` / `clearPluginAuthProviders` 重置缓存 / `ProviderAuth.resetState()` 清空缓存 |
|
|
364
|
+
| 模型类型导出稳定性 | 6 | RerankInput/Result/Response + Entity/Relation/GraphInput + Note/MemorySummary |
|
|
365
|
+
| Env / Flag namespace 静态字段 | 6 | Env 镜像 process.env / Flag 包含 LLM 包内部 flag / 读取函数签名(空字符串 vs undefined) |
|
|
366
|
+
| LLMConfig schema | 4 | 空对象通过 / 完整字段通过 / 非法 model 拒绝 / 非数组 enabled_providers 拒绝 |
|
|
367
|
+
| ProviderConfig schema | 3 | 最小 config 通过 / partial 设计(所有字段可选) / options.apiKey 类型校验 |
|
|
368
|
+
| ProviderAuthMethod schema | 4 | api / oauth 类型通过 / 非法 type 拒绝 / 缺 label 拒绝 |
|
|
369
|
+
|
|
370
|
+
**完整集成测试边界**:AdapterRegistry 解耦后,llm 包可独立 mock 全部 4 个 Provider,完整跑 `Provider.list / getModel / parseModel` 的端到端流程不依赖 agent 包。后续任务:在 agent 包切换 LLM 实现时,补跨包集成测试(验证 setAdapterRegistry 真实注入的 Provider 与 llm 包契约匹配)。
|
|
371
|
+
|
|
372
|
+
## 开发命令
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# 构建(tsup,输出 ESM + CJS + DTS)
|
|
376
|
+
pnpm --filter @easbot/llm build
|
|
377
|
+
|
|
378
|
+
# 监听模式构建
|
|
379
|
+
pnpm --filter @easbot/llm dev
|
|
380
|
+
|
|
381
|
+
# 重新生成 models.dev snapshot(需联网)
|
|
382
|
+
pnpm --filter @easbot/llm build:snapshot
|
|
383
|
+
|
|
384
|
+
# 类型检查
|
|
385
|
+
pnpm --filter @easbot/llm type-check
|
|
386
|
+
|
|
387
|
+
# Lint(biome)
|
|
388
|
+
pnpm --filter @easbot/llm lint
|
|
389
|
+
|
|
390
|
+
# Lint fix
|
|
391
|
+
pnpm --filter @easbot/llm lint:fix
|
|
392
|
+
|
|
393
|
+
# 单测
|
|
394
|
+
pnpm --filter @easbot/llm test:run
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## 设计原则
|
|
398
|
+
|
|
399
|
+
1. **Provider namespace 是唯一入口**:业务代码不应直接 import `@ai-sdk/openai` 等具体 SDK,统一通过 `Provider.getModel('openai/gpt-4')` 取模型
|
|
400
|
+
2. **配置 vs 注入分离**:`Config`(持久化配置)和 `Env/Auth/Plugin`(运行时注入)互不混用
|
|
401
|
+
3. **错误归一化**:`ProviderError.parseAPICallError` 把 25+ 厂商的不同错误格式统一成 `context_overflow | api_error` 二分类
|
|
402
|
+
4. **本地 SDK 优先于远程**:本仓自研 SDK(ollama / easbot-local)不出现在 models.dev,因此用 `CUSTOM_PROVIDER_DEFINITIONS` 静态注入,避免远程 catalog 漂移
|
|
403
|
+
5. **懒加载 SDK**:每个 provider 的 `BUNDLED_PROVIDERS[].loader()` 是 `() => import('@ai-sdk/openai')` 形式的动态 import,业务代码不引用某 provider 时不下载其 SDK
|
|
404
|
+
|
|
405
|
+
## 与历史版本的差异
|
|
406
|
+
|
|
407
|
+
| 版本 | 关键差异 |
|
|
408
|
+
|------|----------|
|
|
409
|
+
| **早期占位(@easbot/types 残留)** | 仅 5 行注释的 index.ts,无业务能力 |
|
|
410
|
+
| **V3(ADR 0070)** | 新增 `src/interfaces.ts`(6 Provider 接口 + AdapterRegistry)+ `src/config/` 三文件模块;重构 provider/{auth,models,provider,transform}.ts 全部走 AdapterRegistry |
|
|
411
|
+
| **V4(ADR 0071)** | 删除 `IInstanceProvider.state()`(下沉到 `@easbot/utils.lazyAsync()`)+ 删除 `IPluginProvider` 依赖(改用模块级 `_pluginAuthRegistry` + `ProviderAuth.registerPluginAuthProvider()` 入口);Provider 接口 6→5 个 |
|
|
412
|
+
|
|
413
|
+
**V3 → V4 重构动机**:
|
|
414
|
+
- `IInstanceProvider.state()` 把"per-process 单例 + 显式 reset"与 `Instance.directory` 抽象耦合在一起,但 state 缓存与 Instance 路径无关
|
|
415
|
+
- 缓存下沉到 `@easbot/utils.lazyAsync()` 后,`IInstanceProvider` 收缩为 2 个方法(`getDirectory` / `getWorktree`),职责清晰
|
|
416
|
+
- `ProviderAuth` 不再依赖 `IPluginProvider`,改用模块级 list + 注册入口,概念清晰化(plugin 注册 = 推入 list)
|
|
417
|
+
|
|
418
|
+
详见:
|
|
419
|
+
- [ADR 0070 llm-adapter-registry](../../docs/decisions/0070-llm-adapter-registry.md) —— AdapterRegistry 注入模式
|
|
420
|
+
- [ADR 0071 llm-instance-state-decompose](../../docs/decisions/0071-llm-instance-state-decompose.md) —— state 拆分下沉
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Knowledge Graph Summary Prompt
|
|
2
|
+
|
|
3
|
+
## Task
|
|
4
|
+
Generate ONE combined summary from MULTIPLE knowledge graph entities.
|
|
5
|
+
|
|
6
|
+
**CRITICAL RULES:**
|
|
7
|
+
- NEVER generate relationships not present in the input
|
|
8
|
+
- NEVER invent connections or attributes
|
|
9
|
+
- NEVER elaborate beyond provided information
|
|
10
|
+
- NEVER guess entity properties
|
|
11
|
+
- If input has no valuable information: ONLY output "No relevant knowledge found."
|
|
12
|
+
|
|
13
|
+
## Input Format
|
|
14
|
+
```
|
|
15
|
+
[Entity 1]
|
|
16
|
+
Entity: {entity1}
|
|
17
|
+
Type: {entity_type1}
|
|
18
|
+
Description: {description1}
|
|
19
|
+
Related Entities: {related_entities1}
|
|
20
|
+
Relation Types: {relation_types1}
|
|
21
|
+
|
|
22
|
+
[Entity 2]
|
|
23
|
+
Entity: {entity2}
|
|
24
|
+
...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Output Format
|
|
28
|
+
**Length: 1-3 entities maximum**
|
|
29
|
+
- Lead with most relevant entities
|
|
30
|
+
- Group information clearly
|
|
31
|
+
- Cite source as "Knowledge Graph"
|
|
32
|
+
|
|
33
|
+
```markdown
|
|
34
|
+
## [Entity Name] (Priority: {priority})
|
|
35
|
+
**Type:** {entity_type}
|
|
36
|
+
**Source:** Knowledge Graph
|
|
37
|
+
**Description:** ...
|
|
38
|
+
**Related Entities:**
|
|
39
|
+
→ [{relation}] → {target_entity}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Fixed Response
|
|
43
|
+
If entities are irrelevant or insufficient:
|
|
44
|
+
```
|
|
45
|
+
No relevant knowledge found.
|
|
46
|
+
```
|
|
47
|
+
**DO NOT generate any other content.**
|
|
48
|
+
|
|
49
|
+
## Rules
|
|
50
|
+
- Output ONE combined summary only
|
|
51
|
+
- Never separate summaries per entity
|
|
52
|
+
- Never invent attributes or relationships
|
|
53
|
+
- Never exceed provided information
|
|
54
|
+
- If uncertain: "No relevant knowledge found."
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Knowledge Summary Prompt
|
|
2
|
+
|
|
3
|
+
## Task
|
|
4
|
+
Synthesize document chunks into a concise summary relevant to the query.
|
|
5
|
+
|
|
6
|
+
**CRITICAL RULES:**
|
|
7
|
+
- NEVER generate content not present in the input
|
|
8
|
+
- NEVER invent relationships or connections
|
|
9
|
+
- NEVER elaborate beyond provided information
|
|
10
|
+
- NEVER guess or assume user needs
|
|
11
|
+
- If input has no valuable information: ONLY output "No relevant knowledge found."
|
|
12
|
+
|
|
13
|
+
## Priority Score
|
|
14
|
+
Priority = 0.6 × relevance + 0.4 × recency
|
|
15
|
+
- Priority > 0.6: high-priority (include)
|
|
16
|
+
- Priority < 0.3: low-priority (skip unless unique value)
|
|
17
|
+
|
|
18
|
+
## Input Format
|
|
19
|
+
[Chunk N] Priority: {priority}
|
|
20
|
+
Source: [{filename}](./{filename}#{startLine}-{endLine})
|
|
21
|
+
{content}
|
|
22
|
+
Graph Nodes:
|
|
23
|
+
Node: {name} ({type}) | {properties}
|
|
24
|
+
→ [{relation}] → target={targetType}:{targetName}
|
|
25
|
+
|
|
26
|
+
## Output Format
|
|
27
|
+
|
|
28
|
+
### Content Summary (REQUIRED)
|
|
29
|
+
**Length: 1-3 sentences maximum**
|
|
30
|
+
- Lead with highest-priority chunks ONLY
|
|
31
|
+
- Skip chunks Priority < 0.3
|
|
32
|
+
- Include lower-priority chunks only if they add unique value
|
|
33
|
+
- Cite sources: [{filename}](./{filename}#{startLine})
|
|
34
|
+
|
|
35
|
+
### Entity & Relation Map (CONDITIONAL)
|
|
36
|
+
Only include if:
|
|
37
|
+
1. Graph nodes are present AND
|
|
38
|
+
2. They add meaningful connections not in Content Summary
|
|
39
|
+
|
|
40
|
+
Format:
|
|
41
|
+
**{EntityName}** ({type})
|
|
42
|
+
→ [{relation}] → {TargetEntity} ({targetType})
|
|
43
|
+
|
|
44
|
+
### Key Insights (CONDITIONAL)
|
|
45
|
+
Only include if:
|
|
46
|
+
1. Notable patterns exist AND
|
|
47
|
+
2. They are NOT already covered in Content Summary
|
|
48
|
+
|
|
49
|
+
Format:
|
|
50
|
+
- 1-2 bullets maximum
|
|
51
|
+
|
|
52
|
+
## Fixed Response
|
|
53
|
+
If no chunks have Priority > 0.3 or all chunks are irrelevant:
|
|
54
|
+
```
|
|
55
|
+
No relevant knowledge found.
|
|
56
|
+
```
|
|
57
|
+
**DO NOT generate any other content.**
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
### Good Output (relevant chunks)
|
|
62
|
+
```
|
|
63
|
+
The codebase uses React for frontend (Priority: 0.78).
|
|
64
|
+
Source: [frontend/index.tsx](./frontend/index.tsx#L10-L50)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Bad Output (NEVER generate)
|
|
68
|
+
```
|
|
69
|
+
Hello! Based on the documents, I can help you with React setup.
|
|
70
|
+
The project structure follows a standard pattern.
|
|
71
|
+
You should use TypeScript for type safety.
|
|
72
|
+
```
|
|
73
|
+
(These weren't in the input!)
|
|
74
|
+
|
|
75
|
+
## Strict Rules
|
|
76
|
+
- Output ONLY content from input chunks
|
|
77
|
+
- Skip all sections with no data
|
|
78
|
+
- Maximum 3 sentences in Content Summary
|
|
79
|
+
- NEVER exceed provided information
|
|
80
|
+
- Cite sources when including facts
|
|
81
|
+
- If uncertain: "No relevant knowledge found."
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Memory Summary Prompt
|
|
2
|
+
|
|
3
|
+
## Task
|
|
4
|
+
Synthesize memory facts into a concise summary relevant to the query.
|
|
5
|
+
|
|
6
|
+
**CRITICAL RULES:**
|
|
7
|
+
- NEVER generate facts not present in the input
|
|
8
|
+
- NEVER invent content or relationships
|
|
9
|
+
- NEVER elaborate beyond provided information
|
|
10
|
+
- NEVER guess or assume user intent
|
|
11
|
+
- If input has no valuable information: ONLY output "No relevant memory found."
|
|
12
|
+
|
|
13
|
+
## Priority Score
|
|
14
|
+
Priority = 0.4 × relevance + 0.3 × recency + 0.3 × importance
|
|
15
|
+
- Priority > 0.6: high-priority (include)
|
|
16
|
+
- Priority < 0.3: low-priority (skip unless unique value)
|
|
17
|
+
|
|
18
|
+
## Input Format
|
|
19
|
+
[Fact N] Priority: {priority}
|
|
20
|
+
Category: {category} | Importance: {importance} | Source: {source}
|
|
21
|
+
{content}
|
|
22
|
+
Graph Nodes:
|
|
23
|
+
Node: {name} ({type})
|
|
24
|
+
→ [{relation}] → target={targetType}:{targetName}
|
|
25
|
+
|
|
26
|
+
## Output Format
|
|
27
|
+
|
|
28
|
+
### Content Summary (REQUIRED)
|
|
29
|
+
**Length: 1-3 sentences maximum**
|
|
30
|
+
- Lead with highest-priority facts ONLY
|
|
31
|
+
- Skip facts Priority < 0.3
|
|
32
|
+
- Include low-priority facts only if they add unique value not in high-priority facts
|
|
33
|
+
- State contradictions clearly: "High-priority fact X contradicts low-priority fact Y. Current truth: X."
|
|
34
|
+
|
|
35
|
+
### Entity & Relation Map (CONDITIONAL)
|
|
36
|
+
Only include if:
|
|
37
|
+
1. Graph nodes are present AND
|
|
38
|
+
2. They add meaningful connections not in Content Summary
|
|
39
|
+
|
|
40
|
+
Format:
|
|
41
|
+
**{EntityName}** ({type})
|
|
42
|
+
→ [{relation}] → {TargetEntity} ({targetType})
|
|
43
|
+
|
|
44
|
+
### Key Insights (CONDITIONAL)
|
|
45
|
+
Only include if:
|
|
46
|
+
1. Meaningful patterns exist AND
|
|
47
|
+
2. They are NOT already covered in Content Summary
|
|
48
|
+
|
|
49
|
+
Format:
|
|
50
|
+
- 1-2 bullets maximum
|
|
51
|
+
|
|
52
|
+
## Fixed Response
|
|
53
|
+
If no facts have Priority > 0.3 or all facts are irrelevant:
|
|
54
|
+
```
|
|
55
|
+
No relevant memory found.
|
|
56
|
+
```
|
|
57
|
+
**DO NOT generate any other content.**
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
### Good Output (relevant facts)
|
|
62
|
+
```
|
|
63
|
+
The user prefers dark mode (Priority: 0.72).
|
|
64
|
+
High-priority fact contradicts earlier light mode preference.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Bad Output (NEVER generate)
|
|
68
|
+
```
|
|
69
|
+
Hello! I can help you with dark mode settings.
|
|
70
|
+
The user's name is John and they work at Company X.
|
|
71
|
+
They like TypeScript and use VSCode daily.
|
|
72
|
+
```
|
|
73
|
+
(These facts weren't in the input!)
|
|
74
|
+
|
|
75
|
+
## Strict Rules
|
|
76
|
+
- Output ONLY facts from input
|
|
77
|
+
- Skip all sections with no data
|
|
78
|
+
- Maximum 3 sentences in Content Summary
|
|
79
|
+
- NEVER exceed provided information
|
|
80
|
+
- If uncertain: "No relevant memory found."
|