@agenticforge/skills 1.1.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -230
- package/README.zh_CN.md +227 -217
- package/dist/cjs/index.cjs +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/types/AgentSkill.d.ts +1 -2
- package/dist/types/AgentSkill.d.ts.map +1 -1
- package/dist/types/MarkdownSkill.d.ts.map +1 -1
- package/dist/types/SkillDispatcher.d.ts +57 -0
- package/dist/types/SkillDispatcher.d.ts.map +1 -0
- package/dist/types/SkillLoader.d.ts.map +1 -1
- package/dist/types/SkillRegistry.d.ts.map +1 -1
- package/dist/types/SkillRunner.d.ts +20 -24
- package/dist/types/SkillRunner.d.ts.map +1 -1
- package/dist/types/SkillTool.d.ts +105 -0
- package/dist/types/SkillTool.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +15 -14
- package/LICENSE +0 -437
package/README.zh_CN.md
CHANGED
|
@@ -1,217 +1,227 @@
|
|
|
1
|
-
# @agenticforge/skills
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@agenticforge/skills)
|
|
4
|
-
[](https://www.npmjs.com/package/@agenticforge/skills)
|
|
4
|
+
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
|
5
|
+
|
|
6
|
+
<p><strong>中文</strong> | <a href="./README.md">English</a></p>
|
|
7
|
+
|
|
8
|
+
AgenticFORGE 的可组合、可路由 Agent 能力系统。将每种能力定义为一个聚焦的 **Skill** —— 用 Markdown 或 TypeScript —— 让框架自动把用户请求路由到最合适的那个。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @agenticforge/skills
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 什么是 Skill?
|
|
19
|
+
|
|
20
|
+
Skill 是一个具名、自包含的能力单元。把它想象成一个专家,你的 Agent 可以把具体问题委托给他:
|
|
21
|
+
|
|
22
|
+
- 一个**天气 Skill**,只处理天气相关问题
|
|
23
|
+
- 一个**代码审查 Skill**,专门评审 TypeScript 代码
|
|
24
|
+
- 一个**股票查询 Skill**,查询实时行情数据
|
|
25
|
+
|
|
26
|
+
每个 Skill 拥有自己的系统提示词、工具集和执行逻辑。用户请求到来时,框架自动路由到最匹配的 Skill。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 定义 Skill
|
|
31
|
+
|
|
32
|
+
### 方式 1 — Markdown(大多数场景推荐)
|
|
33
|
+
|
|
34
|
+
创建一个 `SKILL.md` 文件。frontmatter 定义路由元数据,正文成为系统提示词。
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
---
|
|
38
|
+
name: code-reviewer
|
|
39
|
+
description: 审查 TypeScript 和 JavaScript 代码,发现 Bug、类型安全问题和性能隐患。
|
|
40
|
+
triggerHint: 当用户要求审查、检查或改善代码质量时
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
# 代码审查员
|
|
44
|
+
|
|
45
|
+
你是一位资深 TypeScript 工程师,正在进行严格的代码审查。
|
|
46
|
+
优先关注正确性,其次是性能,最后是代码风格。
|
|
47
|
+
|
|
48
|
+
## 审查清单
|
|
49
|
+
- 类型安全:无隐式 `any`,返回值类型明确
|
|
50
|
+
- 错误处理:无未处理的 Promise rejection
|
|
51
|
+
- 边界条件:null/undefined、空数组
|
|
52
|
+
|
|
53
|
+
## 输出格式
|
|
54
|
+
1. **总体评价** — 一句话总结
|
|
55
|
+
2. **问题列表** — 每条包含严重程度和修复建议
|
|
56
|
+
3. **改进后的代码** — 重写有问题的部分
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
加载并运行:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { SkillLoader, SkillRunner } from "@agenticforge/skills";
|
|
63
|
+
|
|
64
|
+
const skills = await SkillLoader.fromDirectory("./skills");
|
|
65
|
+
const runner = new SkillRunner({ llm, skills });
|
|
66
|
+
|
|
67
|
+
const result = await runner.run(
|
|
68
|
+
"帮我审查这个函数:async function fetchUser(id) { return fetch('/api/' + id).then(r => r.json()); }"
|
|
69
|
+
);
|
|
70
|
+
console.log(result.output);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 方式 2 — TypeScript 类
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { AgentSkill } from "@agenticforge/skills";
|
|
77
|
+
import type { SkillContext, SkillResult } from "@agenticforge/skills";
|
|
78
|
+
import type { LLMClient } from "@agenticforge/core";
|
|
79
|
+
|
|
80
|
+
class StockSkill extends AgentSkill {
|
|
81
|
+
constructor() {
|
|
82
|
+
super({
|
|
83
|
+
name: "stock-query",
|
|
84
|
+
description: "查询任意股票代码的实时价格和市场数据。",
|
|
85
|
+
triggerHint: "当用户询问股票价格、市值、代码或交易数据时",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override async execute(ctx: SkillContext, llm: LLMClient): Promise<SkillResult> {
|
|
90
|
+
const price = await fetchStockPrice(extractTicker(ctx.query));
|
|
91
|
+
const output = await llm.think([
|
|
92
|
+
{ role: "system", content: "用简洁友好的语言格式化股票数据。" },
|
|
93
|
+
{ role: "user", content: `行情数据:${JSON.stringify(price)}\n问题:${ctx.query}` },
|
|
94
|
+
]);
|
|
95
|
+
return { output };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
简单场景直接实例化,无需继承:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const translatorSkill = new AgentSkill({
|
|
104
|
+
name: "translator",
|
|
105
|
+
description: "在任意两种语言之间翻译文本。",
|
|
106
|
+
triggerHint: "当用户想翻译文本,或问某个词在另一种语言中怎么说时",
|
|
107
|
+
systemPrompt: "你是专业翻译。只输出翻译结果,不附加任何解释。",
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 多 Skill 自动路由
|
|
114
|
+
|
|
115
|
+
`SkillRunner`(和 `SkillAgent`)使用**两级路由策略**:
|
|
116
|
+
|
|
117
|
+
| 级别 | 工作方式 | LLM 调用次数 |
|
|
118
|
+
|------|---------|-------------|
|
|
119
|
+
| **规则路由**(优先) | 将 `triggerHint` 关键词与 query 匹配 | 0 |
|
|
120
|
+
| **LLM 路由**(兜底) | 把所有 Skill 描述发给 LLM 做意图分类 | 1 |
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const runner = new SkillRunner({
|
|
124
|
+
llm,
|
|
125
|
+
skills: [...skills, new StockSkill(), new CalendarSkill()],
|
|
126
|
+
fallbackPrompt: "你是一个通用助理。",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await runner.run("明天柏林天气如何?"); // => 天气 Skill(规则路由命中)
|
|
130
|
+
await runner.run("帮我审查一下这段 TS 代码。"); // => code-reviewer Skill
|
|
131
|
+
await runner.run("特斯拉现在股价多少?"); // => StockSkill
|
|
132
|
+
await runner.run("帮我约一个周五下午三点的会。"); // => CalendarSkill
|
|
133
|
+
|
|
134
|
+
// 跳过路由,直接调用指定 Skill
|
|
135
|
+
await runner.runSkill("stock-query", "AAPL 和 MSFT 本周走势对比");
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 进阶:SkillDispatcher
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { SkillDispatcher, SkillRegistry } from "@agenticforge/skills";
|
|
144
|
+
|
|
145
|
+
const registry = new SkillRegistry();
|
|
146
|
+
registry.register(weatherSkill);
|
|
147
|
+
registry.register(stockSkill);
|
|
148
|
+
|
|
149
|
+
const dispatcher = new SkillDispatcher(registry, llm);
|
|
150
|
+
const skill = await dispatcher.dispatch("东京明天下雨吗?");
|
|
151
|
+
if (skill) {
|
|
152
|
+
const result = await skill.execute({ query }, llm);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## API 参考
|
|
159
|
+
|
|
160
|
+
### `SkillRunner`
|
|
161
|
+
|
|
162
|
+
| 方法 | 说明 |
|
|
163
|
+
|------|------|
|
|
164
|
+
| `run(query, options?)` | 路由并执行最匹配的 Skill |
|
|
165
|
+
| `runSkill(name, query, options?)` | 直接执行指定 Skill(跳过路由)|
|
|
166
|
+
| `addSkill(skill)` | 运行时注册 Skill |
|
|
167
|
+
| `removeSkill(name)` | 注销 Skill |
|
|
168
|
+
| `listSkills()` | 列出所有已注册 Skill 名称 |
|
|
169
|
+
|
|
170
|
+
### `AgentSkill`
|
|
171
|
+
|
|
172
|
+
| 成员 | 说明 |
|
|
173
|
+
|------|------|
|
|
174
|
+
| `name` | 唯一标识 |
|
|
175
|
+
| `description` | 一句话描述,LLM 路由读取此字段 |
|
|
176
|
+
| `triggerHint` | 规则路由关键词(逗号分隔)|
|
|
177
|
+
| `systemPrompt` | 执行时注入的系统提示词 |
|
|
178
|
+
| `tools` | 该 Skill 专属工具集 |
|
|
179
|
+
| `execute(ctx, llm)` | Override 实现自定义执行逻辑 |
|
|
180
|
+
|
|
181
|
+
### `SkillLoader`
|
|
182
|
+
|
|
183
|
+
| 方法 | 说明 |
|
|
184
|
+
|------|------|
|
|
185
|
+
| `fromDirectory(dir)` | 递归扫描 `SKILL.md` 和 `*.skill.md` |
|
|
186
|
+
| `fromFiles(paths[])` | 从指定路径列表加载 |
|
|
187
|
+
| `fromSources(sources[])` | 从原始字符串解析 |
|
|
188
|
+
| `registryFromDirectory(dir)` | 一步完成扫描 + 封装为 `SkillRegistry` |
|
|
189
|
+
|
|
190
|
+
### `SkillRegistry`
|
|
191
|
+
|
|
192
|
+
| 方法 | 说明 |
|
|
193
|
+
|------|------|
|
|
194
|
+
| `register(skill)` | 注册 Skill |
|
|
195
|
+
| `get(name)` | 按名称查找 |
|
|
196
|
+
| `list()` | 所有已注册名称 |
|
|
197
|
+
| `visible()` | 对 LLM 路由器可见的 Skills |
|
|
198
|
+
| `describeAll()` | 生成路由 prompt 用的格式化列表 |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 与 SkillAgent 配合使用
|
|
203
|
+
|
|
204
|
+
如需有状态的 Agent 体验(对话历史、多轮),使用 `@agenticforge/agents` 中的 `SkillAgent`:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { SkillAgent } from "@agenticforge/agents";
|
|
208
|
+
import { SkillLoader } from "@agenticforge/skills";
|
|
209
|
+
|
|
210
|
+
const skills = await SkillLoader.fromDirectory("./skills");
|
|
211
|
+
const agent = new SkillAgent({ name: "assistant", llm, skills });
|
|
212
|
+
|
|
213
|
+
// 对话历史自动维护
|
|
214
|
+
await agent.run("伦敦今天天气怎么样?");
|
|
215
|
+
await agent.run("那东京呢?"); // 知道上一轮在问天气
|
|
216
|
+
await agent.run("哪个城市更暖和?"); // 继续路由到天气 Skill
|
|
217
|
+
|
|
218
|
+
agent.clearHistory(); // 会话结束时重置
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 链接
|
|
224
|
+
|
|
225
|
+
- [GitHub](https://github.com/LittleBlacky/AgenticFORGE/tree/main/packages/skills)
|
|
226
|
+
- [npm](https://www.npmjs.com/package/@agenticforge/skills)
|
|
227
|
+
- [主项目 README](https://github.com/LittleBlacky/AgenticFORGE)
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t=require("@agenticforge/tools");class
|
|
1
|
+
"use strict";var t=require("@agenticforge/tools"),e=require("@agenticforge/core");class s{name;description;triggerHint;systemPrompt;tools;visible;constructor(t){this.name=t.name,this.description=t.description,this.triggerHint=t.triggerHint,this.systemPrompt=t.systemPrompt,this.tools=t.tools??[],this.visible=t.visible??!0}_registry;get toolRegistry(){if(!this._registry){this._registry=new t.ToolRegistry;for(const e of this.tools)e instanceof t.Tool?this._registry.registerTool(e):this._registry.registerFunction(e.name,e.description,e.func,e.schema)}return this._registry}async execute(t,s){const r=[{role:"system",content:this.systemPrompt??`你是专门负责"${this.description}"的助理。`},...t.history??[],{role:"user",content:t.query}],i=new e.ToolCallExecutor({llm:s,maxIterations:3}),n=this.tools.length>0?this.toolRegistry.getOpenAISchemas():[];try{const t=await i.run({messages:r,tools:n,executor:(t,e)=>this.toolRegistry.execute(t,e)});return{output:t.output,...t.toolsUsed.length>0?{toolsUsed:t.toolsUsed}:{}}}catch(t){const e=t instanceof Error?t.message:String(t);if(e.includes("未暴露底层 OpenAI 客户端")||e.includes("does not expose"))return{output:await s.think(r)};throw t}}describe(){const t=[`- **${this.name}**: ${this.description}`];return this.triggerHint&&t.push(` 触发条件:${this.triggerHint}`),t.join("\n")}}class r{skills=new Map;register(t){this.skills.set(t.name,t)}unregister(t){return this.skills.delete(t)}get(t){return this.skills.get(t)}has(t){return this.skills.has(t)}list(){return Array.from(this.skills.keys())}all(){return Array.from(this.skills.values())}visible(){return this.all().filter(t=>!1!==t.visible)}size(){return this.skills.size}describeAll(){const t=this.visible();return 0===t.length?"(暂无可用 Skill)":t.map(t=>t instanceof s?t.describe():`- **${t.name}**: ${t.description}${t.triggerHint?`\n 触发条件:${t.triggerHint}`:""}`).join("\n")}}const i=["你是一个意图路由器。根据用户输入,从下面的 Skill 列表中选出最合适的一个。","只回答 Skill 的名称(name 字段),不要包含任何解释或标点。","如果没有合适的 Skill,回答 __none__。","","## 可用 Skills","{skills}","","## 用户输入","{query}","","## 你的选择(只填 Skill name 或 __none__):"].join("\n");class n{registry;llm;routerPromptTemplate;triggerHintSeparator;disableRuleRouting;constructor(t,e,s={}){this.registry=t,this.llm=e,this.routerPromptTemplate=s.routerPromptTemplate??i,this.triggerHintSeparator=s.triggerHintSeparator??/[,,、]/,this.disableRuleRouting=s.disableRuleRouting??!1}async dispatch(t){const e=this.registry.visible();if(0!==e.length){if(1===e.length)return e[0];if(!this.disableRuleRouting){const s=this.ruleRoute(t,e);if(s)return s}return this.llmRoute(t,e)}}ruleRoute(t,e){const s=t.toLowerCase();for(const t of e)if(t.triggerHint&&t.triggerHint.toLowerCase().split(this.triggerHintSeparator).map(t=>t.trim()).filter(Boolean).some(t=>s.includes(t)))return t}async llmRoute(t,e){const s=this.routerPromptTemplate.replace("{skills}",this.registry.describeAll()).replace("{query}",t),r=(await this.llm.think([{role:"user",content:s}])).trim().toLowerCase();if("__none__"!==r)return this.registry.get(r)??e.find(t=>t.name.toLowerCase().startsWith(r))??e.find(t=>r.includes(t.name.toLowerCase()))}}function o(t){const e=t.trim().match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);if(!e){const e=t.match(/^#\s+(.+)$/m);return{frontmatter:{name:e?l(e[1]):"unknown-skill",description:e?e[1]:"Unnamed skill"},body:t}}const[,s,r]=e,i={};for(const t of s.split(/\r?\n/)){const e=t.match(/^([\w-]+):\s*(.*)$/);if(!e)continue;const[,s,r]=e;i[s]="true"===r||"false"!==r&&r.replace(/^"|"$/g,"").replace(/^'|'$/g,"").trim()}if(!i.name){const t=r.match(/^#\s+(.+)$/m);i.name=t?l(t[1]):"unknown-skill"}return i.description||(i.description=i.name),{frontmatter:i,body:r.trim()}}function l(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}class a{name;description;triggerHint;visible;tools=[];systemPrompt;frontmatter;filePath;constructor(t,e,s){this.frontmatter=t,this.name=t.name,this.description=t.description,this.triggerHint=t.triggerHint,this.visible=!1!==t.visible,this.systemPrompt=e,this.filePath=s}static fromSource(t,e){const{frontmatter:s,body:r}=o(t);return new a(s,r,e)}static async fromFile(t){const{readFile:e}=await import("node:fs/promises"),s=await e(t,"utf8");return a.fromSource(s,t)}async execute(t,e){const s=[{role:"system",content:this.systemPrompt},...t.history??[],{role:"user",content:t.query}];return{output:await e.think(s)}}describe(){const t=[`- **${this.name}**: ${this.description}`];return this.triggerHint&&t.push(` 触发条件:${this.triggerHint}`),t.join("\n")}}class c{static async fromDirectory(t,e={}){const{readdir:s,stat:r}=await import("node:fs/promises"),{join:i,resolve:n}=await import("node:path"),o=e.recursive??!0,l=[],c=n(t);return await async function t(e){let n;try{n=await s(e)}catch{return}for(const s of n){const n=i(e,s);let c;try{c=await r(n)}catch{continue}if(c.isDirectory())o&&await t(n);else if(h(s))try{l.push(await a.fromFile(n))}catch(t){console.warn(`[SkillLoader] Failed to load ${n}: ${String(t)}`)}}}(c),l}static async fromFiles(t){const{resolve:e}=await import("node:path"),s=[];for(const r of t)try{s.push(await a.fromFile(e(r)))}catch(t){console.warn(`[SkillLoader] Failed to load ${r}: ${String(t)}`)}return s}static fromSources(t){return t.map(({source:t,filePath:e})=>a.fromSource(t,e))}static toRegistry(t,e){const s=e??new r;for(const e of t)s.register(e);return s}static async registryFromDirectory(t,e){const s=await c.fromDirectory(t,e);return c.toRegistry(s)}}function h(t){const e=t.toLowerCase();return"skill.md"===e||e.endsWith(".skill.md")}class u{maxSize;cache=new Map;constructor(t=100){this.maxSize=Math.max(1,t)}get size(){return this.cache.size}has(t){return this.cache.has(t)}get(t){const e=this.cache.get(t);if(e)return this.cache.delete(t),this.cache.set(t,e),e.value}set(t,e){if(this.cache.has(t)&&this.cache.delete(t),this.cache.set(t,{value:e,timestamp:Date.now()}),this.cache.size>this.maxSize){const t=this.cache.keys().next().value;t&&this.cache.delete(t)}}delete(t){return this.cache.delete(t)}clear(){this.cache.clear()}keys(){return[...this.cache.keys()]}values(){return[...this.cache.values()].map(t=>t.value)}entries(){return[...this.cache.entries()].map(([t,e])=>[t,e.value])}}class m extends t.Tool{skill;llm;cache;constructor(t,e,s){const r=t.triggerHint?`${t.description}\n触发条件:${t.triggerHint}`:t.description;super(t.name,r),this.skill=t,this.llm=e,s?.cacheSize&&s.cacheSize>0&&(this.cache=new u(s.cacheSize))}getParameters(){return[{name:"query",type:"string",description:"用户的完整问题或指令,原文传入即可",required:!0,default:null}]}async run(t){const e=String(t.query??"").trim();if(!e)return"错误:query 不能为空";if(this.cache){const t=this.cache.get(e);if(void 0!==t)return t}const s={query:e};try{const t=await this.skill.execute(s,this.llm),r=this.formatOutput(t);return this.cache&&this.cache.set(e,r),r}catch(t){return`Error: ${t instanceof Error?t.message:String(t)}`}}async runSkill(t){const e=t.trim();if(!e)throw new Error("query 不能为空");const s={query:e};return this.skill.execute(s,this.llm)}getSkill(){return this.skill}getCacheStats(){return this.cache?{size:this.cache.size,keys:this.cache.keys()}:null}clearCache(){this.cache?.clear()}formatOutput(t){return t.toolsUsed&&t.toolsUsed.length>0?`${t.output}\n\n[tools_used: ${t.toolsUsed.join(", ")}]`:t.output}}exports.AgentSkill=s,exports.MarkdownSkill=a,exports.SkillDispatcher=n,exports.SkillLoader=c,exports.SkillRegistry=r,exports.SkillRunner=class{skillRegistry;llm;fallbackPrompt;dispatcher;constructor(t){this.llm=t.llm,this.fallbackPrompt=t.fallbackPrompt??"你是一个通用AI助理,请回答用户的问题。",this.skillRegistry=new r;for(const e of t.skills??[])this.skillRegistry.register(e);this.dispatcher=new n(this.skillRegistry,this.llm,t.dispatcher)}addSkill(t){this.skillRegistry.register(t)}removeSkill(t){return this.skillRegistry.unregister(t)}listSkills(){return this.skillRegistry.list()}async run(t,e){const s=await this.resolveSkill(t,e?.skillName);if(!s)return{output:await this.llm.think([{role:"system",content:this.fallbackPrompt},...e?.history??[],{role:"user",content:t}])};const r={query:t,metadata:e?.metadata,history:e?.history?.filter(t=>"user"===t.role||"assistant"===t.role)};return s.execute(r,this.llm)}async runSkill(t,e,s){return this.run(e,{skillName:t,...s})}async resolveSkill(t,e){if(e){const t=this.skillRegistry.get(e);if(!t)throw new Error(`Skill "${e}" not found. Available: ${this.skillRegistry.list().join(", ")}`);return t}return this.dispatcher.dispatch(t)}},exports.SkillTool=m,exports.parseFrontmatter=o,exports.skillsToTools=function(t,e,s){return t.map(t=>new m(t,e,s))};
|
package/dist/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ToolRegistry as t,Tool as e}from"@agenticforge/tools";class
|
|
1
|
+
import{ToolRegistry as t,Tool as e}from"@agenticforge/tools";import{ToolCallExecutor as s}from"@agenticforge/core";class r{name;description;triggerHint;systemPrompt;tools;visible;constructor(t){this.name=t.name,this.description=t.description,this.triggerHint=t.triggerHint,this.systemPrompt=t.systemPrompt,this.tools=t.tools??[],this.visible=t.visible??!0}_registry;get toolRegistry(){if(!this._registry){this._registry=new t;for(const t of this.tools)t instanceof e?this._registry.registerTool(t):this._registry.registerFunction(t.name,t.description,t.func,t.schema)}return this._registry}async execute(t,e){const r=[{role:"system",content:this.systemPrompt??`你是专门负责"${this.description}"的助理。`},...t.history??[],{role:"user",content:t.query}],i=new s({llm:e,maxIterations:3}),n=this.tools.length>0?this.toolRegistry.getOpenAISchemas():[];try{const t=await i.run({messages:r,tools:n,executor:(t,e)=>this.toolRegistry.execute(t,e)});return{output:t.output,...t.toolsUsed.length>0?{toolsUsed:t.toolsUsed}:{}}}catch(t){const s=t instanceof Error?t.message:String(t);if(s.includes("未暴露底层 OpenAI 客户端")||s.includes("does not expose"))return{output:await e.think(r)};throw t}}describe(){const t=[`- **${this.name}**: ${this.description}`];return this.triggerHint&&t.push(` 触发条件:${this.triggerHint}`),t.join("\n")}}class i{skills=new Map;register(t){this.skills.set(t.name,t)}unregister(t){return this.skills.delete(t)}get(t){return this.skills.get(t)}has(t){return this.skills.has(t)}list(){return Array.from(this.skills.keys())}all(){return Array.from(this.skills.values())}visible(){return this.all().filter(t=>!1!==t.visible)}size(){return this.skills.size}describeAll(){const t=this.visible();return 0===t.length?"(暂无可用 Skill)":t.map(t=>t instanceof r?t.describe():`- **${t.name}**: ${t.description}${t.triggerHint?`\n 触发条件:${t.triggerHint}`:""}`).join("\n")}}const n=["你是一个意图路由器。根据用户输入,从下面的 Skill 列表中选出最合适的一个。","只回答 Skill 的名称(name 字段),不要包含任何解释或标点。","如果没有合适的 Skill,回答 __none__。","","## 可用 Skills","{skills}","","## 用户输入","{query}","","## 你的选择(只填 Skill name 或 __none__):"].join("\n");class o{registry;llm;routerPromptTemplate;triggerHintSeparator;disableRuleRouting;constructor(t,e,s={}){this.registry=t,this.llm=e,this.routerPromptTemplate=s.routerPromptTemplate??n,this.triggerHintSeparator=s.triggerHintSeparator??/[,,、]/,this.disableRuleRouting=s.disableRuleRouting??!1}async dispatch(t){const e=this.registry.visible();if(0!==e.length){if(1===e.length)return e[0];if(!this.disableRuleRouting){const s=this.ruleRoute(t,e);if(s)return s}return this.llmRoute(t,e)}}ruleRoute(t,e){const s=t.toLowerCase();for(const t of e)if(t.triggerHint&&t.triggerHint.toLowerCase().split(this.triggerHintSeparator).map(t=>t.trim()).filter(Boolean).some(t=>s.includes(t)))return t}async llmRoute(t,e){const s=this.routerPromptTemplate.replace("{skills}",this.registry.describeAll()).replace("{query}",t),r=(await this.llm.think([{role:"user",content:s}])).trim().toLowerCase();if("__none__"!==r)return this.registry.get(r)??e.find(t=>t.name.toLowerCase().startsWith(r))??e.find(t=>r.includes(t.name.toLowerCase()))}}class l{skillRegistry;llm;fallbackPrompt;dispatcher;constructor(t){this.llm=t.llm,this.fallbackPrompt=t.fallbackPrompt??"你是一个通用AI助理,请回答用户的问题。",this.skillRegistry=new i;for(const e of t.skills??[])this.skillRegistry.register(e);this.dispatcher=new o(this.skillRegistry,this.llm,t.dispatcher)}addSkill(t){this.skillRegistry.register(t)}removeSkill(t){return this.skillRegistry.unregister(t)}listSkills(){return this.skillRegistry.list()}async run(t,e){const s=await this.resolveSkill(t,e?.skillName);if(!s)return{output:await this.llm.think([{role:"system",content:this.fallbackPrompt},...e?.history??[],{role:"user",content:t}])};const r={query:t,metadata:e?.metadata,history:e?.history?.filter(t=>"user"===t.role||"assistant"===t.role)};return s.execute(r,this.llm)}async runSkill(t,e,s){return this.run(e,{skillName:t,...s})}async resolveSkill(t,e){if(e){const t=this.skillRegistry.get(e);if(!t)throw new Error(`Skill "${e}" not found. Available: ${this.skillRegistry.list().join(", ")}`);return t}return this.dispatcher.dispatch(t)}}function a(t){const e=t.trim().match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);if(!e){const e=t.match(/^#\s+(.+)$/m);return{frontmatter:{name:e?c(e[1]):"unknown-skill",description:e?e[1]:"Unnamed skill"},body:t}}const[,s,r]=e,i={};for(const t of s.split(/\r?\n/)){const e=t.match(/^([\w-]+):\s*(.*)$/);if(!e)continue;const[,s,r]=e;i[s]="true"===r||"false"!==r&&r.replace(/^"|"$/g,"").replace(/^'|'$/g,"").trim()}if(!i.name){const t=r.match(/^#\s+(.+)$/m);i.name=t?c(t[1]):"unknown-skill"}return i.description||(i.description=i.name),{frontmatter:i,body:r.trim()}}function c(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}class h{name;description;triggerHint;visible;tools=[];systemPrompt;frontmatter;filePath;constructor(t,e,s){this.frontmatter=t,this.name=t.name,this.description=t.description,this.triggerHint=t.triggerHint,this.visible=!1!==t.visible,this.systemPrompt=e,this.filePath=s}static fromSource(t,e){const{frontmatter:s,body:r}=a(t);return new h(s,r,e)}static async fromFile(t){const{readFile:e}=await import("node:fs/promises"),s=await e(t,"utf8");return h.fromSource(s,t)}async execute(t,e){const s=[{role:"system",content:this.systemPrompt},...t.history??[],{role:"user",content:t.query}];return{output:await e.think(s)}}describe(){const t=[`- **${this.name}**: ${this.description}`];return this.triggerHint&&t.push(` 触发条件:${this.triggerHint}`),t.join("\n")}}class u{static async fromDirectory(t,e={}){const{readdir:s,stat:r}=await import("node:fs/promises"),{join:i,resolve:n}=await import("node:path"),o=e.recursive??!0,l=[],a=n(t);return await async function t(e){let n;try{n=await s(e)}catch{return}for(const s of n){const n=i(e,s);let a;try{a=await r(n)}catch{continue}if(a.isDirectory())o&&await t(n);else if(m(s))try{l.push(await h.fromFile(n))}catch(t){console.warn(`[SkillLoader] Failed to load ${n}: ${String(t)}`)}}}(a),l}static async fromFiles(t){const{resolve:e}=await import("node:path"),s=[];for(const r of t)try{s.push(await h.fromFile(e(r)))}catch(t){console.warn(`[SkillLoader] Failed to load ${r}: ${String(t)}`)}return s}static fromSources(t){return t.map(({source:t,filePath:e})=>h.fromSource(t,e))}static toRegistry(t,e){const s=e??new i;for(const e of t)s.register(e);return s}static async registryFromDirectory(t,e){const s=await u.fromDirectory(t,e);return u.toRegistry(s)}}function m(t){const e=t.toLowerCase();return"skill.md"===e||e.endsWith(".skill.md")}class g{maxSize;cache=new Map;constructor(t=100){this.maxSize=Math.max(1,t)}get size(){return this.cache.size}has(t){return this.cache.has(t)}get(t){const e=this.cache.get(t);if(e)return this.cache.delete(t),this.cache.set(t,e),e.value}set(t,e){if(this.cache.has(t)&&this.cache.delete(t),this.cache.set(t,{value:e,timestamp:Date.now()}),this.cache.size>this.maxSize){const t=this.cache.keys().next().value;t&&this.cache.delete(t)}}delete(t){return this.cache.delete(t)}clear(){this.cache.clear()}keys(){return[...this.cache.keys()]}values(){return[...this.cache.values()].map(t=>t.value)}entries(){return[...this.cache.entries()].map(([t,e])=>[t,e.value])}}class p extends e{skill;llm;cache;constructor(t,e,s){const r=t.triggerHint?`${t.description}\n触发条件:${t.triggerHint}`:t.description;super(t.name,r),this.skill=t,this.llm=e,s?.cacheSize&&s.cacheSize>0&&(this.cache=new g(s.cacheSize))}getParameters(){return[{name:"query",type:"string",description:"用户的完整问题或指令,原文传入即可",required:!0,default:null}]}async run(t){const e=String(t.query??"").trim();if(!e)return"错误:query 不能为空";if(this.cache){const t=this.cache.get(e);if(void 0!==t)return t}const s={query:e};try{const t=await this.skill.execute(s,this.llm),r=this.formatOutput(t);return this.cache&&this.cache.set(e,r),r}catch(t){return`Error: ${t instanceof Error?t.message:String(t)}`}}async runSkill(t){const e=t.trim();if(!e)throw new Error("query 不能为空");const s={query:e};return this.skill.execute(s,this.llm)}getSkill(){return this.skill}getCacheStats(){return this.cache?{size:this.cache.size,keys:this.cache.keys()}:null}clearCache(){this.cache?.clear()}formatOutput(t){return t.toolsUsed&&t.toolsUsed.length>0?`${t.output}\n\n[tools_used: ${t.toolsUsed.join(", ")}]`:t.output}}function y(t,e,s){return t.map(t=>new p(t,e,s))}export{r as AgentSkill,h as MarkdownSkill,o as SkillDispatcher,u as SkillLoader,i as SkillRegistry,l as SkillRunner,p as SkillTool,a as parseFrontmatter,y as skillsToTools};
|
|
@@ -52,8 +52,7 @@ export declare class AgentSkill implements IAgentSkill {
|
|
|
52
52
|
protected get toolRegistry(): ToolRegistry;
|
|
53
53
|
/**
|
|
54
54
|
* Default implementation: builds messages from context + systemPrompt,
|
|
55
|
-
*
|
|
56
|
-
* and runs a tool-call loop (up to 3 iterations).
|
|
55
|
+
* delegates to ToolCallExecutor for the function-calling loop.
|
|
57
56
|
*
|
|
58
57
|
* Override this method for fully custom Skill logic.
|
|
59
58
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentSkill.d.ts","sourceRoot":"","sources":["../../src/AgentSkill.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"AgentSkill.d.ts","sourceRoot":"","sources":["../../src/AgentSkill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAMvF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;gBAEd,UAAU,EAAE,eAAe;IAavC,OAAO,CAAC,SAAS,CAAC,CAAe;IAEjC,SAAS,KAAK,YAAY,IAAI,YAAY,CAYzC;IAMD;;;;;OAKG;IACG,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IAsC1E,QAAQ,IAAI,MAAM;CAKnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkdownSkill.d.ts","sourceRoot":"","sources":["../../src/MarkdownSkill.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"MarkdownSkill.d.ts","sourceRoot":"","sources":["../../src/MarkdownSkill.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAMtE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG;IAChD,WAAW,EAAE,gBAAgB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd,CAgDA;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,aAAc,YAAW,WAAW;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAS,KAAK,EAAE,CAAC;IAE/B,uDAAuD;IACvD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IAEvC,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAE3B,OAAO;IAcP;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,aAAa;IAKnE;;;OAGG;WACU,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAU/D;;;OAGG;IACG,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IAe1E,QAAQ,IAAI,MAAM;CAKnB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { LLMClient } from "@agenticforge/core";
|
|
2
|
+
import type { IAgentSkill } from "./types";
|
|
3
|
+
import type { SkillRegistry } from "./SkillRegistry";
|
|
4
|
+
export interface SkillDispatcherOptions {
|
|
5
|
+
/** 自定义路由 prompt 模板,支持 {skills} 和 {query} 占位符 */
|
|
6
|
+
routerPromptTemplate?: string;
|
|
7
|
+
/**
|
|
8
|
+
* 规则路由分隔符(用于拆分 triggerHint),默认 /[,,、]/
|
|
9
|
+
* 示例 triggerHint:"当用户询问天气,温度,降雨时"
|
|
10
|
+
*/
|
|
11
|
+
triggerHintSeparator?: RegExp;
|
|
12
|
+
/** 是否禁用规则路由,只走 LLM 路由,默认 false */
|
|
13
|
+
disableRuleRouting?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 纯路由器:给定 query,从 SkillRegistry 找出最匹配的 Skill。
|
|
17
|
+
*
|
|
18
|
+
* 两级策略(依次尝试):
|
|
19
|
+
* 1. **规则路由**:遍历 Skill 的 `triggerHint`,关键词命中则直接返回,零 LLM 调用
|
|
20
|
+
* 2. **LLM 路由**:把所有可见 Skill 的描述给 LLM,让它选择最合适的一个
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const dispatcher = new SkillDispatcher(registry, llm);
|
|
25
|
+
* const skill = await dispatcher.dispatch("东京今天天气怎么样?");
|
|
26
|
+
* if (skill) {
|
|
27
|
+
* const result = await skill.execute({ query }, llm);
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class SkillDispatcher {
|
|
32
|
+
private readonly registry;
|
|
33
|
+
private readonly llm;
|
|
34
|
+
private readonly routerPromptTemplate;
|
|
35
|
+
private readonly triggerHintSeparator;
|
|
36
|
+
private readonly disableRuleRouting;
|
|
37
|
+
constructor(registry: SkillRegistry, llm: LLMClient, options?: SkillDispatcherOptions);
|
|
38
|
+
/**
|
|
39
|
+
* 路由:返回最匹配的 Skill,或 undefined(无匹配时)。
|
|
40
|
+
*
|
|
41
|
+
* - 只有 0 个可见 Skill → undefined
|
|
42
|
+
* - 只有 1 个可见 Skill → 直接返回(跳过路由开销)
|
|
43
|
+
* - 多个 Skill → 规则路由 → LLM 路由
|
|
44
|
+
*/
|
|
45
|
+
dispatch(query: string): Promise<IAgentSkill | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* 规则路由:基于 triggerHint 关键词匹配。
|
|
48
|
+
* 将 triggerHint 按分隔符拆分,只要 query 包含其中一个词组,即命中。
|
|
49
|
+
*/
|
|
50
|
+
private ruleRoute;
|
|
51
|
+
/**
|
|
52
|
+
* LLM 路由:把所有可见 Skill 描述给 LLM,让它选出 name。
|
|
53
|
+
* 三级兜底匹配:精确匹配 → startsWith → includes。
|
|
54
|
+
*/
|
|
55
|
+
private llmRoute;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=SkillDispatcher.d.ts.map
|