startx 0.9.4 → 0.9.9
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/apps/cli/package.json +3 -1
- package/apps/cli/src/commands/common/hashing.ts +34 -0
- package/apps/cli/src/commands/common/ping.ts +11 -0
- package/apps/cli/src/commands/common/random.ts +41 -0
- package/apps/cli/src/commands/i-command.ts +6 -0
- package/apps/cli/src/commands/index.ts +6 -0
- package/apps/cli/src/index.ts +6 -4
- package/apps/cli/tsconfig.json +4 -6
- package/apps/startx-cli/dist/index.mjs +52 -52
- package/configs/eslint-config/src/configs/base.ts +26 -11
- package/package.json +2 -2
- package/packages/@db/drizzle/tsconfig.json +2 -8
- package/packages/@db/sqlite/tsconfig.json +2 -8
- package/packages/@repo/env/package.json +2 -1
- package/packages/@repo/env/src/utils.ts +7 -6
- package/packages/@repo/lib/src/events/i-event.ts +1 -1
- package/packages/aix/eslint.config.ts +4 -0
- package/packages/aix/package.json +53 -0
- package/packages/aix/src/aix.ts +519 -0
- package/packages/aix/src/index.ts +3 -0
- package/packages/aix/src/lib/convertor/schema-convertor.ts +108 -0
- package/packages/aix/src/lib/convertor/variable-resolver.ts +161 -0
- package/packages/aix/src/lib/tokenizer/index.ts +1 -0
- package/packages/aix/src/lib/tokenizer/tokenizer.ts +42 -0
- package/packages/aix/src/providers/ai-chat.ts +25 -0
- package/packages/aix/src/providers/ai-event.ts +21 -0
- package/packages/aix/src/providers/ai-interface.ts +236 -0
- package/packages/aix/src/providers/ai-prompt.ts +14 -0
- package/packages/aix/src/providers/default-models.ts +471 -0
- package/packages/aix/src/providers/index.ts +1 -0
- package/packages/aix/src/providers/openai/openai.ts +139 -0
- package/packages/aix/src/providers/providers.ts +39 -0
- package/packages/aix/src/providers/types.ts +54 -0
- package/packages/aix/src/tools/generic/database.ts +290 -0
- package/packages/aix/src/tools/generic/forecast.ts +216 -0
- package/packages/aix/src/tools/generic/index.ts +4 -0
- package/packages/aix/src/tools/generic/planner.ts +114 -0
- package/packages/aix/src/tools/generic/summarizer.ts +101 -0
- package/packages/aix/src/tools/i-tool.ts +33 -0
- package/packages/aix/src/tools/index.ts +2 -0
- package/packages/aix/src/tools/system/index.ts +297 -0
- package/packages/aix/src/tools/tool-manager.ts +241 -0
- package/packages/aix/src/tools/types.ts +109 -0
- package/packages/aix/tsconfig.json +7 -0
- package/packages/aix/vitest.config.ts +3 -0
- package/pnpm-workspace.yaml +7 -0
- package/turbo.json +0 -1
- package/apps/cli/src/commands/ping.ts +0 -10
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ITool } from "../i-tool.js";
|
|
3
|
+
|
|
4
|
+
export const SummarizerTool: ITool[] = [
|
|
5
|
+
new ITool({
|
|
6
|
+
title: "query-summarizer",
|
|
7
|
+
description:
|
|
8
|
+
"Acts as a professional query architect. Evaluates the user's raw prompt for feasibility, summarizes the core intent, and outputs a highly optimized, dense step-by-step execution plan along with the specific tools required. If the request is unfeasible, ambiguous, or out of scope, it provides a clear abort reason.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
isFeasible: z
|
|
11
|
+
.boolean()
|
|
12
|
+
.describe("True if the query can be executed with available tools, false if it must be aborted."),
|
|
13
|
+
intentSummary: z.string().describe("A concise, professional summary of the user's core objective."),
|
|
14
|
+
executionPlan: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe(
|
|
18
|
+
"A dense, highly compressed step-by-step execution plan or raw database query. Omit all filler words."
|
|
19
|
+
),
|
|
20
|
+
requiredTools: z
|
|
21
|
+
.array(z.string())
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("An array of specific tool names required to complete the execution plan."),
|
|
24
|
+
abortReason: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("If isFeasible is false, provide a clear, actionable reason why the query cannot be executed."),
|
|
28
|
+
}),
|
|
29
|
+
run: ({ isFeasible, intentSummary, executionPlan, requiredTools, abortReason }) => {
|
|
30
|
+
try {
|
|
31
|
+
const payload = isFeasible
|
|
32
|
+
? { isFeasible, intentSummary, executionPlan, requiredTools }
|
|
33
|
+
: { isFeasible, intentSummary, abortReason };
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: JSON.stringify(payload),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "resource_link",
|
|
42
|
+
uri: "return",
|
|
43
|
+
name: isFeasible ? "PlanGenerated" : "ExecutionAborted",
|
|
44
|
+
_meta: {
|
|
45
|
+
return: { isCompleted: true, isError: false },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
} catch (err: unknown) {
|
|
50
|
+
return [
|
|
51
|
+
{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` },
|
|
52
|
+
{
|
|
53
|
+
type: "resource_link",
|
|
54
|
+
uri: "return",
|
|
55
|
+
name: "ToolFailure",
|
|
56
|
+
_meta: {
|
|
57
|
+
return: { isCompleted: true, isError: true },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
new ITool({
|
|
65
|
+
title: "general-summarizer",
|
|
66
|
+
description: "Summarizes a given query or text into a short and precise format.",
|
|
67
|
+
schema: z.object({
|
|
68
|
+
summary: z.string().describe("The short, precise summary of the input query."),
|
|
69
|
+
}),
|
|
70
|
+
run: ({ summary }) => {
|
|
71
|
+
try {
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: JSON.stringify({ summary }),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "resource_link",
|
|
79
|
+
uri: "return",
|
|
80
|
+
name: "Summarized",
|
|
81
|
+
_meta: {
|
|
82
|
+
return: { isCompleted: true, isError: false },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
} catch (err: unknown) {
|
|
87
|
+
return [
|
|
88
|
+
{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` },
|
|
89
|
+
{
|
|
90
|
+
type: "resource_link",
|
|
91
|
+
uri: "return",
|
|
92
|
+
name: "Failed",
|
|
93
|
+
_meta: {
|
|
94
|
+
return: { isCompleted: true, isError: true },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { TInternal, ToolReturn } from "./types.js";
|
|
3
|
+
|
|
4
|
+
type Awaitable<T> = T | PromiseLike<T>;
|
|
5
|
+
|
|
6
|
+
export class ITool<S extends z.ZodObject = z.ZodObject> {
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly schema: S;
|
|
10
|
+
|
|
11
|
+
private _run: (args: z.infer<S>, internal?: TInternal) => Awaitable<ToolReturn>;
|
|
12
|
+
|
|
13
|
+
public run = async (rawArgs: unknown, internal: TInternal): Promise<ToolReturn> => {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = this.schema.parse(rawArgs);
|
|
16
|
+
return await this._run(parsed, internal);
|
|
17
|
+
} catch (err: unknown) {
|
|
18
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
|
|
19
|
+
return [{ type: "text", text: message }];
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
constructor(def: {
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
schema: S;
|
|
26
|
+
run: (args: z.infer<S>, internal?: TInternal) => Awaitable<ToolReturn>;
|
|
27
|
+
}) {
|
|
28
|
+
this.title = def.title;
|
|
29
|
+
this.description = def.description;
|
|
30
|
+
this.schema = def.schema;
|
|
31
|
+
this._run = def.run;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import * as vm from "node:vm";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ITool } from "../i-tool.js";
|
|
4
|
+
|
|
5
|
+
export const SystemInternalTools: ITool[] = [
|
|
6
|
+
new ITool({
|
|
7
|
+
title: "end_response",
|
|
8
|
+
description: "Call this tool if you want to end the response to the user.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
note: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe(
|
|
13
|
+
"Answer the user query with a note in professional way that has the task summary and the response of what user asked you can also use variable and perform js-operations on them. A valid variable format is {{message.VARIABLE_NAME}}."
|
|
14
|
+
),
|
|
15
|
+
}),
|
|
16
|
+
run: ({ note }: { note: string }) => {
|
|
17
|
+
try {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: `${note}`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: "resource_link",
|
|
25
|
+
uri: "return",
|
|
26
|
+
name: note,
|
|
27
|
+
_meta: {
|
|
28
|
+
return: {
|
|
29
|
+
isCompleted: true,
|
|
30
|
+
isError: false,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
return [{ type: "text", text: `Error fetching forecast: ${message}` }];
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
new ITool({
|
|
43
|
+
title: "no_relevant_tool",
|
|
44
|
+
description:
|
|
45
|
+
"Call this tool if there is no relevant tool that can help you to complete this task. Please respond to user query in professional way.",
|
|
46
|
+
schema: z.object({
|
|
47
|
+
note: z.string().describe("Please respond to the user query."),
|
|
48
|
+
}),
|
|
49
|
+
run: ({ note }: { note: string }) => {
|
|
50
|
+
try {
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `${note}`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: "resource_link",
|
|
58
|
+
uri: "return",
|
|
59
|
+
name: note,
|
|
60
|
+
_meta: {
|
|
61
|
+
return: {
|
|
62
|
+
isCompleted: true,
|
|
63
|
+
isError: false,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70
|
+
return [{ type: "text", text: `Error fetching forecast: ${message}` }];
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
// new ITool({
|
|
76
|
+
// title: "continue_response",
|
|
77
|
+
// description:
|
|
78
|
+
// "Call this tool if some parts of job is done and there is next action to be taken make sure it doesn't involve user interaction.",
|
|
79
|
+
// schema: z.object({
|
|
80
|
+
// note: z.string().describe("Please provide a proper note of the next execution"),
|
|
81
|
+
// }),
|
|
82
|
+
// run: ({ note }: { note: string }) => {
|
|
83
|
+
// try {
|
|
84
|
+
// return [
|
|
85
|
+
// {
|
|
86
|
+
// type: "text",
|
|
87
|
+
// text: `${note}`,
|
|
88
|
+
// },
|
|
89
|
+
// ];
|
|
90
|
+
// } catch (err: unknown) {
|
|
91
|
+
// const message = err instanceof Error ? err.message : String(err);
|
|
92
|
+
// return [{ type: "text", text: `Error fetching forecast: ${message}` }];
|
|
93
|
+
// }
|
|
94
|
+
// },
|
|
95
|
+
// }),
|
|
96
|
+
|
|
97
|
+
new ITool({
|
|
98
|
+
title: "error_response",
|
|
99
|
+
description: "Call this tool if the job is failed because of some errors.",
|
|
100
|
+
schema: z.object({
|
|
101
|
+
note: z.string().describe("Please provide a proper error with a note how to solve this error."),
|
|
102
|
+
}),
|
|
103
|
+
run: ({ note }: { note: string }) => {
|
|
104
|
+
try {
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `${note}`,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: "resource_link",
|
|
112
|
+
uri: "return",
|
|
113
|
+
name: note,
|
|
114
|
+
_meta: {
|
|
115
|
+
return: {
|
|
116
|
+
isCompleted: true,
|
|
117
|
+
isError: true,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
} catch (err: unknown) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
+
return [{ type: "text", text: `Error fetching forecast: ${message}` }];
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
new ITool({
|
|
130
|
+
title: "get_data_by_tool_id",
|
|
131
|
+
description: `
|
|
132
|
+
Write JavaScript logic using {{data}} as the dataset variable.
|
|
133
|
+
You can use top‑level statements (const, loops, etc.).
|
|
134
|
+
**You must include a return statement** in your operation.
|
|
135
|
+
|
|
136
|
+
When performing operation make everything **case insensitive** so that 10mg or 10 mg or 10Mg are all considered the same.
|
|
137
|
+
Always handle variable that has null or undefined values.
|
|
138
|
+
Example:
|
|
139
|
+
const filtered = {{data}}.filter(x => x.active);
|
|
140
|
+
return filtered.map(x => x.name);
|
|
141
|
+
`,
|
|
142
|
+
schema: z.object({
|
|
143
|
+
toolId: z.string().describe("ID of the structure data to fetch."),
|
|
144
|
+
operation: z.string().describe("JS function body using {{data}}. Must include return."),
|
|
145
|
+
}),
|
|
146
|
+
run: async ({ toolId, operation }, internal) => {
|
|
147
|
+
try {
|
|
148
|
+
const DATA_VAR = "DATA_PLACEHOLDER";
|
|
149
|
+
const data = internal?.toolData?.[toolId];
|
|
150
|
+
|
|
151
|
+
if (!data) {
|
|
152
|
+
throw new Error(`No data found for structureDataId: ${toolId}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const functionBody = operation.replaceAll("{{data}}", DATA_VAR);
|
|
156
|
+
|
|
157
|
+
const context = vm.createContext({
|
|
158
|
+
[DATA_VAR]: data,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const script = new vm.Script(`
|
|
162
|
+
(async () => {
|
|
163
|
+
${functionBody}
|
|
164
|
+
})();
|
|
165
|
+
`);
|
|
166
|
+
|
|
167
|
+
const result = (await script.runInContext(context)) as unknown;
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: `✅ Success: structureDataId \`${toolId}\`.`,
|
|
173
|
+
},
|
|
174
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
175
|
+
];
|
|
176
|
+
} catch (err: unknown) {
|
|
177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
178
|
+
return [{ type: "text", text: `❌ Error: ${message}` }];
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
new ITool({
|
|
183
|
+
title: "execute_javascript",
|
|
184
|
+
description: `
|
|
185
|
+
Executes JavaScript code in a sandboxed VM. Use this tool for ANY JavaScript execution, including mathematical calculations, logical operations, data processing, and resolving variables provided in the user's prompt.
|
|
186
|
+
|
|
187
|
+
CRITICAL INSTRUCTIONS:
|
|
188
|
+
1. SYNTAX FOR VARIABLES: If referencing dataset variables from the prompt, you MUST use the exact format: {{vars.VARIABLE_NAME}}. Only use this syntax if a variable actually exists in the prompt.
|
|
189
|
+
2. RETURN STATEMENT: Your code is executed in an async IIFE and MUST include a top-level 'return' statement with the final result.
|
|
190
|
+
3. RESILIENCE: Always handle potential null, undefined, or edge cases gracefully.
|
|
191
|
+
4. COMPLETION FLAG ('isCompleted'):
|
|
192
|
+
- Set to TRUE if this execution fully resolves the user's query. If true, your script MUST return a cleanly formatted Markdown string ready to be shown to the user.
|
|
193
|
+
- Set to FALSE if you only need to extract/calculate intermediate data for your own further reasoning.
|
|
194
|
+
|
|
195
|
+
EXAMPLE 1 (Math):
|
|
196
|
+
return Math.sqrt(144) + 5;
|
|
197
|
+
|
|
198
|
+
EXAMPLE 2 (Variables):
|
|
199
|
+
const data = {{vars.MEDICATIONS}} || [];
|
|
200
|
+
const filtered = data.filter(item => item && item.dose && item.dose.toLowerCase().replace(' ', '') === '10mg');
|
|
201
|
+
return filtered.map(item => \`- \${item.name}\`).join('\\n');
|
|
202
|
+
|
|
203
|
+
Mark isCompleted as true if this operation fully resolves the user's request (script must return formatted Markdown).
|
|
204
|
+
`.trim(),
|
|
205
|
+
schema: z.object({
|
|
206
|
+
operation: z
|
|
207
|
+
.string()
|
|
208
|
+
.describe(
|
|
209
|
+
"Valid JavaScript code. MUST contain a 'return' statement. Do not include markdown code blocks (```javascript) in this string."
|
|
210
|
+
),
|
|
211
|
+
isCompleted: z
|
|
212
|
+
.boolean()
|
|
213
|
+
.describe(
|
|
214
|
+
"Set to true if this operation fully resolves the user's request (script must return formatted Markdown). Set to false if extracting intermediate data."
|
|
215
|
+
),
|
|
216
|
+
}),
|
|
217
|
+
run: async ({ isCompleted, operation }, internal) => {
|
|
218
|
+
try {
|
|
219
|
+
const contextObj: Record<string, unknown> = {};
|
|
220
|
+
let resolvedOp = operation;
|
|
221
|
+
|
|
222
|
+
resolvedOp = resolvedOp
|
|
223
|
+
.replace(/^```javascript\n/, "")
|
|
224
|
+
.replace(/^```js\n/, "")
|
|
225
|
+
.replace(/\n```$/, "");
|
|
226
|
+
|
|
227
|
+
for (const [key, value] of internal?.vars.entries() || []) {
|
|
228
|
+
const sanitized = key;
|
|
229
|
+
|
|
230
|
+
// Attempt to parse stringified JSON data before injecting into the VM
|
|
231
|
+
let contextValue = value.data;
|
|
232
|
+
if (typeof contextValue === "string") {
|
|
233
|
+
try {
|
|
234
|
+
contextValue = JSON.parse(contextValue);
|
|
235
|
+
} catch {
|
|
236
|
+
// Leave as string if not valid JSON
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
contextObj[sanitized] = contextValue;
|
|
241
|
+
|
|
242
|
+
const placeholder = new RegExp(`\\{\\{vars\\.${key}\\}\\}`, "g");
|
|
243
|
+
resolvedOp = resolvedOp.replace(placeholder, sanitized);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const context = vm.createContext(contextObj);
|
|
247
|
+
|
|
248
|
+
const script = new vm.Script(`
|
|
249
|
+
(async () => {
|
|
250
|
+
${resolvedOp}
|
|
251
|
+
})();
|
|
252
|
+
`);
|
|
253
|
+
|
|
254
|
+
const result = (await script.runInContext(context)) as unknown;
|
|
255
|
+
const textResult = JSON.stringify(result)?.length > 3 ? result : "No data found";
|
|
256
|
+
|
|
257
|
+
if (isCompleted) {
|
|
258
|
+
return [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: String(textResult),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
type: "resource_link",
|
|
265
|
+
uri: "return",
|
|
266
|
+
name: "Execution completed",
|
|
267
|
+
_meta: {
|
|
268
|
+
return: {
|
|
269
|
+
isCompleted: true,
|
|
270
|
+
isError: false,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return [{ type: "text", text: JSON.stringify(textResult, null, 2) }];
|
|
278
|
+
} catch (err: unknown) {
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
return [
|
|
281
|
+
{ type: "text", text: `❌ Error: ${message}` },
|
|
282
|
+
{
|
|
283
|
+
type: "resource_link",
|
|
284
|
+
uri: "return",
|
|
285
|
+
name: "Execution failed",
|
|
286
|
+
_meta: {
|
|
287
|
+
return: {
|
|
288
|
+
isCompleted: false,
|
|
289
|
+
isError: true,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
];
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { IEvent } from "@repo/lib/events";
|
|
2
|
+
import { jsonrepair } from "jsonrepair";
|
|
3
|
+
import type z from "zod";
|
|
4
|
+
import type { GenericTool, TInternal, ToolConnector } from "./types.js";
|
|
5
|
+
import type { AiEventType } from "../providers/ai-event.js";
|
|
6
|
+
import type { AiChatMessage, AiResource } from "../providers/types.js";
|
|
7
|
+
|
|
8
|
+
export class ToolManager {
|
|
9
|
+
public whitelist: string[] = [];
|
|
10
|
+
private events: IEvent<AiEventType>;
|
|
11
|
+
public registered: Map<string, GenericTool> = new Map();
|
|
12
|
+
public tools: Map<string, GenericTool> = new Map();
|
|
13
|
+
|
|
14
|
+
constructor(props?: { whitelist?: string[]; events?: IEvent<AiEventType> }) {
|
|
15
|
+
this.whitelist = props?.whitelist ?? [];
|
|
16
|
+
this.events = props?.events || new IEvent();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async attachTools(props: ToolConnector) {
|
|
20
|
+
switch (props.type) {
|
|
21
|
+
case "internal": {
|
|
22
|
+
for (const t of props.tool) {
|
|
23
|
+
this.registered.set(t.title, {
|
|
24
|
+
name: t.title
|
|
25
|
+
.replace(/^[^a-zA-Z_]+/, "")
|
|
26
|
+
.replace(/[^a-zA-Z0-9_.-]/g, "_")
|
|
27
|
+
.slice(0, 64),
|
|
28
|
+
description: t.description,
|
|
29
|
+
schema: t.schema,
|
|
30
|
+
run: t.run,
|
|
31
|
+
});
|
|
32
|
+
if (!this.whitelist.includes(t.title)) {
|
|
33
|
+
this.events.emit("log", {
|
|
34
|
+
type: "warn",
|
|
35
|
+
message: `Tool ${t.title} is not whitelisted. Skipping...`,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (this.tools.has(t.title)) {
|
|
40
|
+
this.events.emit("log", {
|
|
41
|
+
type: "warn",
|
|
42
|
+
message: `Tool ${t.title} is already registered. Skipping...`,
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
this.events.emit("log", {
|
|
47
|
+
type: "info",
|
|
48
|
+
message: `Registering tool ${t.title}`,
|
|
49
|
+
});
|
|
50
|
+
this.tools.set(t.title, {
|
|
51
|
+
name: t.title
|
|
52
|
+
.replace(/^[^a-zA-Z_]+/, "")
|
|
53
|
+
.replace(/[^a-zA-Z0-9_.-]/g, "_")
|
|
54
|
+
.slice(0, 64),
|
|
55
|
+
description: t.description,
|
|
56
|
+
schema: t.schema,
|
|
57
|
+
run: t.run,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "mcp": {
|
|
64
|
+
// TODO: implement mcp tool call
|
|
65
|
+
// Remove this
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
67
|
+
throw new Error("MCP tool call not implemented yet!");
|
|
68
|
+
}
|
|
69
|
+
default: {
|
|
70
|
+
throw new Error(`Unknown tool type: ${JSON.stringify(props)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
public getActiveTools() {
|
|
75
|
+
return Array.from(this.tools.values()).map(e => ({
|
|
76
|
+
name: e.name,
|
|
77
|
+
description: e.description,
|
|
78
|
+
input_schema: e.schema,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
public getRegisteredTools() {
|
|
82
|
+
return Array.from(this.registered.values()).map(e => ({
|
|
83
|
+
name: e.name,
|
|
84
|
+
description: e.description,
|
|
85
|
+
input_schema: e.schema,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
public removeTool(title: string) {
|
|
89
|
+
if (this.tools.has(title)) {
|
|
90
|
+
this.events.emit("log", {
|
|
91
|
+
type: "info",
|
|
92
|
+
message: `Unregistering tool ${title}`,
|
|
93
|
+
});
|
|
94
|
+
this.tools.delete(title);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
public async callTool(props: {
|
|
98
|
+
name: string;
|
|
99
|
+
args: z.infer<GenericTool["schema"]>;
|
|
100
|
+
toolId: string;
|
|
101
|
+
internal?: TInternal;
|
|
102
|
+
}) {
|
|
103
|
+
try {
|
|
104
|
+
const tool = this.tools.get(props.name);
|
|
105
|
+
if (!tool) {
|
|
106
|
+
throw new Error(`Tool ${props.name} not found`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.events.emit("tool.start", {
|
|
110
|
+
name: tool.name,
|
|
111
|
+
args: props.args,
|
|
112
|
+
});
|
|
113
|
+
const result = await tool.run(
|
|
114
|
+
ToolManager.parseArgs(props.args) as z.infer<GenericTool["schema"]>,
|
|
115
|
+
props.internal
|
|
116
|
+
);
|
|
117
|
+
this.events.emit("tool.finish", result);
|
|
118
|
+
|
|
119
|
+
if (!result.length) {
|
|
120
|
+
throw new Error("Tool result not returned anything. Please try again.");
|
|
121
|
+
}
|
|
122
|
+
const content: AiChatMessage[] = [];
|
|
123
|
+
let isCompleted = false;
|
|
124
|
+
let isError = false;
|
|
125
|
+
|
|
126
|
+
const resources: AiResource[] = [];
|
|
127
|
+
for (const item of result) {
|
|
128
|
+
if (item.type === "text") {
|
|
129
|
+
content.push({
|
|
130
|
+
role: "tool",
|
|
131
|
+
content: item.text,
|
|
132
|
+
timestamp: new Date(),
|
|
133
|
+
tool_call_id: props.toolId,
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (item._meta.return) {
|
|
138
|
+
isCompleted = !!item._meta.return.isCompleted;
|
|
139
|
+
isError = !!item._meta.return.isError;
|
|
140
|
+
}
|
|
141
|
+
switch (item.uri) {
|
|
142
|
+
case "return": {
|
|
143
|
+
const returnMeta = item._meta.return;
|
|
144
|
+
if (returnMeta?.isCompleted) isCompleted = true;
|
|
145
|
+
if (returnMeta?.isError) isError = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "table": {
|
|
149
|
+
resources.push({
|
|
150
|
+
type: "table",
|
|
151
|
+
data: item._meta.data,
|
|
152
|
+
columns: item._meta.columns,
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "image": {
|
|
157
|
+
resources.push({
|
|
158
|
+
type: "image",
|
|
159
|
+
source: item._meta.source,
|
|
160
|
+
alt: item._meta.alt,
|
|
161
|
+
});
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case "file": {
|
|
165
|
+
resources.push({
|
|
166
|
+
type: "file",
|
|
167
|
+
source: item._meta.source,
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case "user_message": {
|
|
172
|
+
content.push({
|
|
173
|
+
role: "user",
|
|
174
|
+
content: item._meta.message,
|
|
175
|
+
timestamp: new Date(),
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
isCompleted,
|
|
183
|
+
isError,
|
|
184
|
+
content,
|
|
185
|
+
resources,
|
|
186
|
+
};
|
|
187
|
+
} catch (error: unknown) {
|
|
188
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
189
|
+
const message = `Tool ${props.name} failed: ${err.message}`;
|
|
190
|
+
this.events.emit("log", {
|
|
191
|
+
type: "error",
|
|
192
|
+
message,
|
|
193
|
+
stack: err.stack,
|
|
194
|
+
meta: err.cause as Record<string, unknown>,
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
isCompleted: false,
|
|
198
|
+
isError: true,
|
|
199
|
+
resources: [],
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
role: "tool",
|
|
203
|
+
content: message,
|
|
204
|
+
},
|
|
205
|
+
] as AiChatMessage[],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
protected static parseArgs = (obj: any): any => {
|
|
210
|
+
if (typeof obj !== "object" || obj === null) return obj;
|
|
211
|
+
|
|
212
|
+
// Create a new object/array to avoid mutating the original payload
|
|
213
|
+
const parsedObj = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
214
|
+
|
|
215
|
+
for (const key in parsedObj) {
|
|
216
|
+
const value = parsedObj[key];
|
|
217
|
+
|
|
218
|
+
if (typeof value === "string" && (value.trim().startsWith("[") || value.trim().startsWith("{"))) {
|
|
219
|
+
try {
|
|
220
|
+
// 1. Try a standard parse first
|
|
221
|
+
parsedObj[key] = JSON.parse(value);
|
|
222
|
+
} catch {
|
|
223
|
+
try {
|
|
224
|
+
// 2. If it fails (due to unescaped quotes, etc.), repair it and try again
|
|
225
|
+
parsedObj[key] = JSON.parse(jsonrepair(value));
|
|
226
|
+
} catch {
|
|
227
|
+
// 3. If it still fails, it's not JSON. Leave it as a string.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Recursively parse the newly parsed object (in case of deep nesting)
|
|
232
|
+
if (typeof parsedObj[key] === "object") {
|
|
233
|
+
parsedObj[key] = this.parseArgs(parsedObj[key]);
|
|
234
|
+
}
|
|
235
|
+
} else if (typeof value === "object") {
|
|
236
|
+
parsedObj[key] = this.parseArgs(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return parsedObj;
|
|
240
|
+
};
|
|
241
|
+
}
|