knockagent 0.1.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.
@@ -0,0 +1,219 @@
1
+ import Loader from "./Loader.js";
2
+ import { Agent, Runner, setTracingDisabled } from "@openai/agents";
3
+ import { promptWithHandoffInstructions } from "@openai/agents-core/extensions";
4
+ import { Liquid } from "liquidjs";
5
+ function parseTemprature(t) {
6
+ if (typeof t === "number")
7
+ return t;
8
+ if (typeof t === "string") {
9
+ const n = Number.parseFloat(t);
10
+ return Number.isNaN(n) ? null : n;
11
+ }
12
+ return null;
13
+ }
14
+ function split(list) {
15
+ if (Array.isArray(list))
16
+ return list.map(item => String(item));
17
+ if (typeof list === "string") {
18
+ return list.split(",").map(l => l.trim()).filter(l => !!l);
19
+ }
20
+ return [];
21
+ }
22
+ setTracingDisabled(true);
23
+ export default class KnockAgent {
24
+ #loader;
25
+ #providers;
26
+ #models;
27
+ #runner;
28
+ #agents;
29
+ #agentsConfig;
30
+ #defaultModel;
31
+ #defaultTemperature;
32
+ #liquid;
33
+ #tools;
34
+ constructor(config) {
35
+ // init fields
36
+ this.#runner = new Runner({
37
+ callModelInputFilter: this.#runnerModelInputFilter.bind(this),
38
+ tracingDisabled: true
39
+ });
40
+ this.#liquid = new Liquid({
41
+ strictFilters: true,
42
+ strictVariables: true,
43
+ });
44
+ this.#liquid.registerFilter("json", (v) => JSON.stringify(v));
45
+ this.#agentsConfig = new Map();
46
+ this.#agents = new Map();
47
+ this.#models = new Map();
48
+ this.#tools = new Map();
49
+ this.#loader = new Loader(config.rootDir, config.fs);
50
+ this.#defaultModel = config.defaultModel;
51
+ this.#defaultTemperature = config.defaultTemperature || 1.0;
52
+ this.#providers = config.providers;
53
+ if (config.tools && config.tools.length) {
54
+ for (let tool of config.tools) {
55
+ this.#tools.set(tool.name, tool);
56
+ }
57
+ }
58
+ }
59
+ #runnerModelInputFilter(input) {
60
+ const agentConfig = this.#agentsConfig.get(input.agent);
61
+ if (!agentConfig || !agentConfig.input || (!agentConfig.input.prefix && !agentConfig.input.suffix)) {
62
+ return input.modelData;
63
+ }
64
+ const modelInput = input.modelData.input;
65
+ const lastInput = modelInput[modelInput.length - 1];
66
+ if (!lastInput)
67
+ return input.modelData;
68
+ if ("role" in lastInput && lastInput.role === "user") {
69
+ let content = lastInput.content;
70
+ if (agentConfig.input.prefix) {
71
+ content = agentConfig.input.prefix + "\n\n" + content;
72
+ }
73
+ if (agentConfig.input.suffix) {
74
+ content = content + "\n\n" + agentConfig.input.suffix;
75
+ }
76
+ return {
77
+ instructions: input.modelData.instructions,
78
+ input: [...modelInput.slice(0, modelInput.length - 1), {
79
+ role: "user",
80
+ content: content
81
+ }]
82
+ };
83
+ }
84
+ return input.modelData;
85
+ }
86
+ #getModel(model = this.#defaultModel, agentName) {
87
+ if (this.#models.has(model)) {
88
+ return this.#models.get(model);
89
+ }
90
+ const m = model.split("/");
91
+ const provider = m.shift();
92
+ const providerModel = m.join("/");
93
+ if (!this.#providers[provider]) {
94
+ throw new Error(`[KnockAgent] No Provider [${provider}] in agent [${agentName}]`);
95
+ }
96
+ const aiModel = this.#providers[provider].getModel(providerModel);
97
+ this.#models.set(model, aiModel);
98
+ return aiModel;
99
+ }
100
+ #getAgent(path) {
101
+ if (this.#agents.has(path)) {
102
+ return this.#agents.get(path);
103
+ }
104
+ const agentMeta = this.#loader.getAgent(path);
105
+ if (!agentMeta) {
106
+ console.warn(`[KnockAgent] Agent Name [${path}] Not Found`);
107
+ return null;
108
+ }
109
+ const data = agentMeta.data;
110
+ // Validation and Warnings
111
+ const warn = (field, expected, actual) => {
112
+ console.warn(`[KnockAgent] Agent [${path}] property "${field}" should be ${expected}, got ${typeof actual}`);
113
+ };
114
+ if (data.name !== undefined && typeof data.name !== "string")
115
+ warn("name", "string", data.name);
116
+ if (data.model !== undefined && typeof data.model !== "string")
117
+ warn("model", "string", data.model);
118
+ if (data.temperature !== undefined && typeof data.temperature !== "number" && typeof data.temperature !== "string")
119
+ warn("temperature", "number or string", data.temperature);
120
+ ["handoffs", "agents", "tools"].forEach(key => {
121
+ if (data[key] !== undefined && typeof data[key] !== "string" && !Array.isArray(data[key])) {
122
+ warn(key, "string or array", data[key]);
123
+ }
124
+ });
125
+ if (data.reasoning !== undefined) {
126
+ if (typeof data.reasoning !== "object" || data.reasoning === null) {
127
+ warn("reasoning", "object", data.reasoning);
128
+ }
129
+ else {
130
+ const r = data.reasoning;
131
+ if (r.effort !== undefined && typeof r.effort !== "string")
132
+ warn("reasoning.effort", "string", r.effort);
133
+ if (r.summary !== undefined && typeof r.summary !== "string")
134
+ warn("reasoning.summary", "string ('auto' | 'concise' | 'detailed')", r.summary);
135
+ }
136
+ }
137
+ if (data.input !== undefined) {
138
+ if (typeof data.input !== "object" || data.input === null) {
139
+ warn("input", "object", data.input);
140
+ }
141
+ else {
142
+ const i = data.input;
143
+ if (i.prefix !== undefined && typeof i.prefix !== "string")
144
+ warn("input.prefix", "string", i.prefix);
145
+ if (i.suffix !== undefined && typeof i.suffix !== "string")
146
+ warn("input.suffix", "string", i.suffix);
147
+ }
148
+ }
149
+ if (data.verbosity !== undefined && typeof data.verbosity !== "string")
150
+ warn("verbosity", "string", data.verbosity);
151
+ const agentDefineConfig = {
152
+ name: (typeof data.name === "string" ? data.name : "") || path.replace(/\//g, "_"),
153
+ desc: (typeof data.desc === "string" ? data.desc : (typeof data.description === "string" ? data.description : "")) || "",
154
+ model: (typeof data.model === "string" ? data.model : this.#defaultModel),
155
+ temperature: parseTemprature(data.temperature) ?? this.#defaultTemperature,
156
+ instructions: agentMeta.content,
157
+ handoffs: split(data.handoffs),
158
+ agents: split(data.agents),
159
+ tools: split(data.tools),
160
+ };
161
+ if (typeof data.reasoning === "object" && data.reasoning !== null) {
162
+ agentDefineConfig.reasoning = data.reasoning;
163
+ }
164
+ if (data.verbosity) {
165
+ agentDefineConfig.verbosity = data.verbosity;
166
+ }
167
+ if (typeof data.input === "object" && data.input !== null) {
168
+ agentDefineConfig.input = data.input;
169
+ }
170
+ const modelSettings = {
171
+ temperature: agentDefineConfig.temperature
172
+ };
173
+ if (agentDefineConfig.reasoning) {
174
+ modelSettings.reasoning = agentDefineConfig.reasoning;
175
+ }
176
+ if (agentDefineConfig.verbosity) {
177
+ modelSettings.text = { verbosity: agentDefineConfig.verbosity };
178
+ }
179
+ const model = this.#getModel(agentDefineConfig.model, path);
180
+ const agent = new Agent({
181
+ name: agentDefineConfig.name,
182
+ handoffDescription: agentDefineConfig.desc,
183
+ model,
184
+ modelSettings: model.mapModelSettings ? model.mapModelSettings(modelSettings) : modelSettings,
185
+ instructions: async (runContext) => {
186
+ const rawPrompt = await this.#liquid.parseAndRender(agentDefineConfig.instructions, runContext.context);
187
+ if (agentDefineConfig.handoffs.length) {
188
+ return promptWithHandoffInstructions(rawPrompt);
189
+ }
190
+ return rawPrompt;
191
+ }
192
+ });
193
+ this.#agents.set(path, agent);
194
+ this.#agentsConfig.set(agent, agentDefineConfig);
195
+ if (agentDefineConfig.handoffs.length) {
196
+ agent.handoffs = agentDefineConfig.handoffs.map(h => this.#getAgent(h)).filter(a => !!a);
197
+ }
198
+ if (agentDefineConfig.agents.length || agentDefineConfig.tools.length) {
199
+ agent.tools = [...agentDefineConfig.agents.map(h => this.#getAgent(h)).filter(a => !!a).map(a => a.asTool({
200
+ toolName: `agent_${a.name}`,
201
+ toolDescription: a.handoffDescription,
202
+ })), ...agentDefineConfig.tools.map(t => this.#tools.get(t)).filter(t => !!t)];
203
+ }
204
+ return agent;
205
+ }
206
+ run(input, context = {}) {
207
+ return this.runAgent("main", input, context);
208
+ }
209
+ runAgent(agentPath, input, context = {}) {
210
+ const agent = this.#getAgent(agentPath);
211
+ if (!agent) {
212
+ throw new Error(`[KnockAgent] Agent [${agentPath}] Not Exists`);
213
+ }
214
+ return this.#runner.run(agent, input, {
215
+ context,
216
+ stream: true
217
+ });
218
+ }
219
+ }
@@ -0,0 +1,13 @@
1
+ import { type GrayMatterFile } from "gray-matter";
2
+ export interface VirtualFS {
3
+ existsSync(path: string): boolean;
4
+ readFileSync(path: string, encoding?: BufferEncoding): string;
5
+ statSync(path: string): {
6
+ isFile(): boolean;
7
+ };
8
+ }
9
+ export default class Loader {
10
+ #private;
11
+ constructor(rootDir: string, fs?: VirtualFS);
12
+ getAgent(name: string): GrayMatterFile<string> | undefined;
13
+ }
package/dist/Loader.js ADDED
@@ -0,0 +1,106 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ export default class Loader {
5
+ #fs;
6
+ #rootDir;
7
+ #parsedCache = new Map();
8
+ constructor(rootDir, fs) {
9
+ this.#rootDir = path.resolve(rootDir);
10
+ this.#fs = fs ?? {
11
+ existsSync: (filePath) => existsSync(filePath),
12
+ readFileSync: (filePath, encoding = "utf-8") => readFileSync(filePath, encoding),
13
+ statSync: (filePath) => statSync(filePath)
14
+ };
15
+ }
16
+ getAgent(name) {
17
+ try {
18
+ const entryPath = this.#resolveEntryPath(name);
19
+ return this.#parseFile(entryPath, new Set());
20
+ }
21
+ catch (error) {
22
+ console.warn(`[Loader] Failed to load agent "${name}":`, error);
23
+ return undefined;
24
+ }
25
+ }
26
+ #parseFile(filePath, visiting) {
27
+ const normalized = path.normalize(filePath);
28
+ if (this.#parsedCache.has(normalized)) {
29
+ return this.#parsedCache.get(normalized);
30
+ }
31
+ if (visiting.has(normalized)) {
32
+ const cyclePath = [...visiting, normalized].join(" -> ");
33
+ throw new Error(`Circular reference detected: ${cyclePath}`);
34
+ }
35
+ visiting.add(normalized);
36
+ try {
37
+ const source = this.#fs.readFileSync(normalized, "utf-8");
38
+ const parsed = matter(source);
39
+ const resolvedContent = this.#resolveImports(parsed.content, normalized, visiting);
40
+ const resolvedPrompt = {
41
+ ...parsed,
42
+ content: resolvedContent
43
+ };
44
+ this.#parsedCache.set(normalized, resolvedPrompt);
45
+ return resolvedPrompt;
46
+ }
47
+ finally {
48
+ visiting.delete(normalized);
49
+ }
50
+ }
51
+ #resolveImports(content, currentFilePath, visiting) {
52
+ const importRegex = /@\(([^)]+)\)/g;
53
+ return content.replace(importRegex, (_full, importPath) => {
54
+ const targetPath = this.#resolveImportPath(currentFilePath, importPath.trim());
55
+ const importedPrompt = this.#parseFile(targetPath, visiting);
56
+ return importedPrompt.content;
57
+ });
58
+ }
59
+ #resolveEntryPath(name) {
60
+ const targetPath = path.resolve(this.#rootDir, name);
61
+ return this.#resolveMarkdownPath(targetPath, this.#rootDir);
62
+ }
63
+ #resolveImportPath(fromFilePath, importPath) {
64
+ if (!importPath) {
65
+ throw new Error("Import path cannot be empty");
66
+ }
67
+ let targetPath;
68
+ if (path.isAbsolute(importPath)) {
69
+ targetPath = path.resolve(this.#rootDir, `.${importPath}`);
70
+ }
71
+ else {
72
+ targetPath = path.resolve(path.dirname(fromFilePath), importPath);
73
+ }
74
+ return this.#resolveMarkdownPath(targetPath, this.#rootDir);
75
+ }
76
+ #resolveMarkdownPath(targetPath, baseDir) {
77
+ let resolvedPath = targetPath;
78
+ if (!resolvedPath.endsWith(".md")) {
79
+ resolvedPath += ".md";
80
+ }
81
+ this.#assertWithin(baseDir, resolvedPath);
82
+ this.#assertFileExists(resolvedPath);
83
+ return resolvedPath;
84
+ }
85
+ #assertWithin(baseDir, filePath) {
86
+ const normalizedBase = path.resolve(baseDir);
87
+ const normalizedTarget = path.resolve(filePath);
88
+ const targetRelative = path.relative(normalizedBase, normalizedTarget);
89
+ if (targetRelative === ".." || targetRelative.startsWith(`..${path.sep}`)) {
90
+ throw new Error(`Path escapes base directory: ${filePath}`);
91
+ }
92
+ }
93
+ #assertFileExists(filePath) {
94
+ if (!this.#fs.existsSync(filePath) || !this.#isFile(filePath)) {
95
+ throw new Error(`Markdown file not found: ${filePath}`);
96
+ }
97
+ }
98
+ #isFile(filePath) {
99
+ try {
100
+ return this.#fs.statSync(filePath).isFile();
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,5 @@
1
+ import KnockAgent from "./KnockAgent.ts";
2
+ export default KnockAgent;
3
+ export * from "./KnockAgent.ts";
4
+ export * from "./utils.ts";
5
+ export { VirtualFS } from "./Loader.ts";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import KnockAgent from "./KnockAgent.js";
2
+ export default KnockAgent;
3
+ export * from "./KnockAgent.js";
4
+ export * from "./utils.js";
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ export declare function createMoonshotSearch(apiKey: string): (config: {
3
+ model: string;
4
+ query: string;
5
+ deep?: boolean;
6
+ }) => Promise<string | null>;
7
+ export default function createSearchTool(config: {
8
+ openrouter?: {
9
+ apiKey: string;
10
+ engine?: "exa" | "native";
11
+ models: string[];
12
+ };
13
+ aliyun?: {
14
+ apiKey: string;
15
+ agent?: boolean;
16
+ models: string[];
17
+ };
18
+ moonshot?: {
19
+ apiKey: string;
20
+ models: string[];
21
+ };
22
+ }): import("@openai/agents").FunctionTool<unknown, z.ZodObject<{
23
+ query: z.ZodString;
24
+ deep: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
25
+ }, z.core.$strip>, string>;
@@ -0,0 +1,257 @@
1
+ import { tool } from "@openai/agents";
2
+ import { OpenRouter } from "@openrouter/sdk";
3
+ import { z } from "zod";
4
+ import OpenAI from "openai";
5
+ function createOpenrouterSearch(apiKey, engine) {
6
+ function getSearchDeep(headers) {
7
+ if (!headers)
8
+ return "medium";
9
+ const h = new Headers(headers);
10
+ return h.get("x-search-deep") === "high" ? "high" : "medium";
11
+ }
12
+ function removeSearchDeep(headers) {
13
+ if (!headers)
14
+ return;
15
+ if (Array.isArray(headers)) {
16
+ headers = headers.filter(h => h[0] !== "x-search-deep");
17
+ }
18
+ else if (headers instanceof Headers) {
19
+ headers.delete("x-search-deep");
20
+ }
21
+ else {
22
+ delete headers["x-search-deep"];
23
+ }
24
+ }
25
+ const openrouter = new OpenRouter({
26
+ apiKey,
27
+ hooks: [{
28
+ beforeCreateRequest(_, request) {
29
+ if (request.options) {
30
+ const body = request.options.body;
31
+ if (body && typeof body === "string") {
32
+ const json = JSON.parse(body);
33
+ json.web_search_options = {
34
+ "search_context_size": getSearchDeep(request.options.headers)
35
+ };
36
+ request.options.body = JSON.stringify(json);
37
+ }
38
+ removeSearchDeep(request.options.headers);
39
+ }
40
+ return request;
41
+ }
42
+ }]
43
+ });
44
+ return async function search(config) {
45
+ const isPerplexity = config.model.toLowerCase().includes('perplexity');
46
+ const response = await openrouter.beta.responses.send({
47
+ openResponsesRequest: {
48
+ model: config.model,
49
+ instructions: "Please use a deep web search to find the user's question.",
50
+ input: config.query,
51
+ plugins: [{
52
+ id: "web",
53
+ enabled: true,
54
+ engine: isPerplexity ? "native" : engine,
55
+ maxResults: 5
56
+ }]
57
+ }
58
+ }, {
59
+ headers: {
60
+ "x-search-deep": config.deep ? "high" : "medium"
61
+ }
62
+ });
63
+ if (response.output.length && response.output[0].type === "message" && response.output[0].content.length) {
64
+ const content = response.output[0].content[0];
65
+ if (content.type === "output_text") {
66
+ return content.text + "\n\n引用:\n\n" + content.annotations?.filter(a => a.type === "url_citation").map(a => `[${a.title}](${a.url})`).join("\n");
67
+ }
68
+ }
69
+ return null;
70
+ };
71
+ }
72
+ function createAliyunSearch(apiKey, agent) {
73
+ const searchStrategy = agent ? ["agent", "agent_max"] : ["turbo", "max"];
74
+ return async function search(config) {
75
+ const endpoint = (config.model.includes("qwen3.5") || config.model.includes("vl")) ? "multimodal-generation" : "text-generation";
76
+ const response = await fetch(`https://dashscope.aliyuncs.com/api/v1/services/aigc/${endpoint}/generation`, {
77
+ method: "POST",
78
+ headers: {
79
+ "Authorization": `Bearer ${apiKey}`,
80
+ "Content-Type": "application/json"
81
+ },
82
+ body: JSON.stringify({
83
+ model: config.model,
84
+ input: {
85
+ messages: [
86
+ { role: "user", content: config.query }
87
+ ]
88
+ },
89
+ parameters: {
90
+ enable_search: true,
91
+ search_options: {
92
+ search_strategy: config.deep ? searchStrategy[1] : searchStrategy[0],
93
+ enable_source: true,
94
+ forced_search: true,
95
+ enable_citation: true,
96
+ citation_format: "[ref_<number>]"
97
+ },
98
+ result_format: "message"
99
+ }
100
+ })
101
+ });
102
+ const data = await response.json();
103
+ if (data?.output?.choices?.length) {
104
+ const c = data.output.choices[0];
105
+ if (c.message) {
106
+ let content = "";
107
+ if (c.message.reasoning_content) {
108
+ content += c.message.reasoning_content + "\n\n";
109
+ }
110
+ if (c.message.content) {
111
+ content += c.message.content[0]?.text + "\n\n";
112
+ }
113
+ if (data.output.search_info?.search_results) {
114
+ for (const result of data.output.search_info.search_results) {
115
+ content += `ref_${result.index}: [${result.title}](${result.url})\n`;
116
+ }
117
+ }
118
+ return content;
119
+ }
120
+ }
121
+ return null;
122
+ };
123
+ }
124
+ // test baseURL https://ark.cn-beijing.volces.com/api/v3
125
+ // export function createVolcengineSearch(apiKey: string, baseURL: string) {
126
+ // return async function search(config: {
127
+ // model: string;
128
+ // query: string;
129
+ // deep?: boolean;
130
+ // }) {
131
+ // const response = await fetch(`${baseURL}/responses`, {
132
+ // method: "POST",
133
+ // headers: {
134
+ // "Authorization": `Bearer ${apiKey}`,
135
+ // "Content-Type": "application/json"
136
+ // },
137
+ // body: JSON.stringify({
138
+ // model: config.model,
139
+ // tools: [
140
+ // {
141
+ // type: "web_search",
142
+ // max_keyword: 3,
143
+ // sources: ["douyin", "moji", "toutiao"],
144
+ // limit: config.deep ? 20 : 10
145
+ // }
146
+ // ],
147
+ // max_tool_calls: config.deep ? 3 : 10,
148
+ // input: [
149
+ // {
150
+ // role: "user",
151
+ // content: config.query
152
+ // }
153
+ // ],
154
+ // instructions: "Please use a deep web search to find the user's question.\nA web search was conducted on `date`. Incorporate the following web search results into your response.\nIMPORTANT: Cite them using markdown links named using the domain of the source.\nExample: [nytimes.com](https://nytimes.com/some-page)."
155
+ // })
156
+ // });
157
+ // console.log(await response.json());
158
+ // }
159
+ // }
160
+ // @todo need test
161
+ export function createMoonshotSearch(apiKey) {
162
+ const client = new OpenAI({
163
+ apiKey,
164
+ baseURL: "https://api.moonshot.cn/v1"
165
+ });
166
+ return async function (config) {
167
+ let finishReason = null;
168
+ let content = null;
169
+ const messages = [
170
+ { "role": "system", "content": "Please use a deep web search to find the user's question.\nA web search was conducted on `date`. Incorporate the following web search results into your response.\nIMPORTANT: Cite them using markdown links named using the domain of the source.\nExample: [nytimes.com](https://nytimes.com/some-page)." },
171
+ { "role": "user", "content": "please web search: " + config.query } // 在提问中要求 Kimi 大模型联网搜索
172
+ ];
173
+ while (finishReason === null || finishReason === "tool_calls") {
174
+ const completion = await client.chat.completions.create({
175
+ model: config.model,
176
+ messages,
177
+ temperature: 0.6,
178
+ tools: [
179
+ {
180
+ // @ts-ignore
181
+ "type": "builtin_function",
182
+ "function": {
183
+ "name": "$web_search",
184
+ },
185
+ }
186
+ ],
187
+ });
188
+ const choice = completion.choices[0];
189
+ finishReason = choice.finish_reason;
190
+ if (finishReason === "tool_calls") {
191
+ messages.push(choice.message);
192
+ for (const toolCall of choice.message.tool_calls || []) {
193
+ // @ts-ignore
194
+ const tool_call_name = toolCall.function.name;
195
+ if (tool_call_name == "$web_search") {
196
+ // @ts-ignore
197
+ const tool_call_arguments = JSON.parse(toolCall.function.arguments);
198
+ messages.push({
199
+ "role": "tool",
200
+ "tool_call_id": toolCall.id,
201
+ //@ts-ignore
202
+ "name": tool_call_name,
203
+ "content": JSON.stringify(tool_call_arguments)
204
+ });
205
+ }
206
+ }
207
+ }
208
+ content = choice.message.content;
209
+ // console.log(choice.message.annotations);
210
+ }
211
+ return content;
212
+ };
213
+ }
214
+ export default function createSearchTool(config) {
215
+ const searchFns = [];
216
+ if (config.openrouter) {
217
+ const openrouterSearch = createOpenrouterSearch(config.openrouter.apiKey, config.openrouter.engine);
218
+ for (const model of config.openrouter.models) {
219
+ searchFns.push((query, deep) => openrouterSearch({
220
+ model,
221
+ query,
222
+ deep
223
+ }));
224
+ }
225
+ }
226
+ if (config.aliyun) {
227
+ const aliyunSearch = createAliyunSearch(config.aliyun.apiKey, config.aliyun.agent);
228
+ for (const model of config.aliyun.models) {
229
+ searchFns.push((query, deep) => aliyunSearch({
230
+ model,
231
+ query,
232
+ deep
233
+ }));
234
+ }
235
+ }
236
+ if (config.moonshot) {
237
+ const moonshotSearch = createMoonshotSearch(config.moonshot.apiKey);
238
+ for (const model of config.moonshot.models) {
239
+ searchFns.push((query, deep) => moonshotSearch({
240
+ model,
241
+ query,
242
+ deep
243
+ }));
244
+ }
245
+ }
246
+ return tool({
247
+ name: "search",
248
+ description: "Search the web for information",
249
+ parameters: z.object({
250
+ query: z.string().describe("The search query"),
251
+ deep: z.boolean().optional().describe("Whether to perform a deep search").default(false)
252
+ }),
253
+ execute: async ({ query, deep }) => {
254
+ return await Promise.all(searchFns.map(fn => fn(query, deep)));
255
+ }
256
+ });
257
+ }
@@ -0,0 +1,10 @@
1
+ import { ModelSettings } from "@openai/agents";
2
+ import { aisdk } from "@openai/agents-extensions";
3
+ import { AiProvider } from "./KnockAgent.ts";
4
+ export interface BaseAiProvider {
5
+ languageModel(modelName: string): Parameters<typeof aisdk>[0];
6
+ }
7
+ export interface MapModelSettings {
8
+ (modelSettings: ModelSettings, modelName: string): ModelSettings;
9
+ }
10
+ export declare function createAiProvider(baseProvider: BaseAiProvider, mapModelSettings?: MapModelSettings): AiProvider;
package/dist/utils.js ADDED
@@ -0,0 +1,40 @@
1
+ import { aisdk } from "@openai/agents-extensions";
2
+ /**
3
+ * A utility to handle values that might be promises.
4
+ * If the value is a promise, it waits for it and then applies the function.
5
+ * If it's not a promise, it applies the function immediately.
6
+ */
7
+ function withPotentialPromise(value, callback) {
8
+ if (value instanceof Promise || (value && typeof value.then === "function")) {
9
+ return value.then(callback);
10
+ }
11
+ return callback(value);
12
+ }
13
+ export function createAiProvider(baseProvider, mapModelSettings) {
14
+ return {
15
+ getModel(modelName) {
16
+ const model = baseProvider.languageModel(modelName);
17
+ // Intercept getArgs to support passing extra body parameters through settings.extra
18
+ if (model && "getArgs" in model && typeof model.getArgs === "function") {
19
+ const originalGetArgs = model.getArgs;
20
+ model.getArgs = function (settings, ...args) {
21
+ const result = originalGetArgs.apply(this, [settings, ...args]);
22
+ return withPotentialPromise(result, (rawArgs) => {
23
+ if (settings?.extra && rawArgs?.body) {
24
+ rawArgs.body = {
25
+ ...rawArgs.body,
26
+ ...settings.extra,
27
+ };
28
+ }
29
+ return rawArgs;
30
+ });
31
+ };
32
+ }
33
+ const aiSdkModel = aisdk(model);
34
+ if (mapModelSettings) {
35
+ aiSdkModel.mapModelSettings = (modelSettings) => mapModelSettings(modelSettings, modelName);
36
+ }
37
+ return aiSdkModel;
38
+ },
39
+ };
40
+ }