@spec2tools/core 0.1.2 → 0.2.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 +33 -0
- package/dist/ai-tools.d.ts +13 -0
- package/dist/ai-tools.js +22 -0
- package/dist/code-mode.d.ts +12 -0
- package/dist/code-mode.js +233 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -64,6 +64,39 @@ const tools = createExecutableTools(toolDefs, baseUrl, authManager);
|
|
|
64
64
|
const result = await executeToolByName(tools, 'getUser', { id: '123' });
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### AI SDK Tools
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { toAISDKTools } from '@spec2tools/core';
|
|
71
|
+
|
|
72
|
+
// Convert core tools to AI SDK-compatible ToolSet
|
|
73
|
+
const aiTools = toAISDKTools(tools);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Code Mode
|
|
77
|
+
|
|
78
|
+
Collapse any AI SDK `ToolSet` into 2 tools (`search` + `execute`). The model discovers endpoints via keyword search and calls them by writing Python code, executed in a sandboxed [Monty](https://github.com/pydantic/monty) interpreter.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { toCodeModeTools } from '@spec2tools/core';
|
|
82
|
+
|
|
83
|
+
// Convert N tools into 2 code-mode tools
|
|
84
|
+
const codeModeTools = toCodeModeTools(aiTools);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This also works with tools from any MCP server via the AI SDK:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createMCPClient } from 'ai';
|
|
91
|
+
import { toCodeModeTools } from '@spec2tools/core';
|
|
92
|
+
|
|
93
|
+
const client = await createMCPClient({
|
|
94
|
+
transport: { type: 'sse', url: 'http://localhost:3000/sse' },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const tools = toCodeModeTools(await client.tools());
|
|
98
|
+
```
|
|
99
|
+
|
|
67
100
|
### Error Classes
|
|
68
101
|
|
|
69
102
|
```ts
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import type { Tool } from './types.js';
|
|
3
|
+
type ToolSet = NonNullable<Parameters<typeof generateText>[0]['tools']>;
|
|
4
|
+
/**
|
|
5
|
+
* Convert core Tool[] into AI SDK ToolSet.
|
|
6
|
+
*
|
|
7
|
+
* Wraps each core tool with the `ai` package's `tool()` helper,
|
|
8
|
+
* producing a `Record<string, CoreTool>` compatible with `generateText`,
|
|
9
|
+
* `streamText`, etc.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toAISDKTools(tools: Tool[]): ToolSet;
|
|
12
|
+
export type { ToolSet };
|
|
13
|
+
//# sourceMappingURL=ai-tools.d.ts.map
|
package/dist/ai-tools.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
/**
|
|
3
|
+
* Convert core Tool[] into AI SDK ToolSet.
|
|
4
|
+
*
|
|
5
|
+
* Wraps each core tool with the `ai` package's `tool()` helper,
|
|
6
|
+
* producing a `Record<string, CoreTool>` compatible with `generateText`,
|
|
7
|
+
* `streamText`, etc.
|
|
8
|
+
*/
|
|
9
|
+
export function toAISDKTools(tools) {
|
|
10
|
+
const aiTools = {};
|
|
11
|
+
for (const t of tools) {
|
|
12
|
+
aiTools[t.name] = tool({
|
|
13
|
+
description: t.description,
|
|
14
|
+
inputSchema: t.parameters,
|
|
15
|
+
execute: async (params) => {
|
|
16
|
+
return t.execute(params);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return aiTools;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=ai-tools.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
type ToolSet = NonNullable<Parameters<typeof generateText>[0]['tools']>;
|
|
3
|
+
/**
|
|
4
|
+
* Transform an AI SDK ToolSet into exactly 2 code-mode tools: `search` and `execute`.
|
|
5
|
+
*
|
|
6
|
+
* The model discovers endpoints via keyword search and calls them by writing
|
|
7
|
+
* Python code that is executed in a sandboxed Monty interpreter. This reduces
|
|
8
|
+
* token consumption by ~99.9% for large APIs (N tools → 2 tools).
|
|
9
|
+
*/
|
|
10
|
+
export declare function toCodeModeTools(tools: ToolSet): ToolSet;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=code-mode.d.ts.map
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
4
|
+
import { Monty, MontySnapshot, MontyRuntimeError, MontySyntaxError, MontyTypingError, } from '@pydantic/monty';
|
|
5
|
+
/**
|
|
6
|
+
* Convert a tool name into a valid Python identifier.
|
|
7
|
+
* Replaces hyphens, dots, spaces, etc. with underscores and
|
|
8
|
+
* prefixes with `_` if the name starts with a digit.
|
|
9
|
+
*/
|
|
10
|
+
function toPythonName(name) {
|
|
11
|
+
let py = name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
12
|
+
if (/^[0-9]/.test(py))
|
|
13
|
+
py = `_${py}`;
|
|
14
|
+
return py || '_unnamed';
|
|
15
|
+
}
|
|
16
|
+
function jsonSchemaTypeToPython(schema) {
|
|
17
|
+
if (!schema)
|
|
18
|
+
return 'any';
|
|
19
|
+
const type = schema.type;
|
|
20
|
+
if (type === 'string')
|
|
21
|
+
return 'str';
|
|
22
|
+
if (type === 'integer')
|
|
23
|
+
return 'int';
|
|
24
|
+
if (type === 'number')
|
|
25
|
+
return 'float';
|
|
26
|
+
if (type === 'boolean')
|
|
27
|
+
return 'bool';
|
|
28
|
+
if (type === 'array')
|
|
29
|
+
return 'list';
|
|
30
|
+
if (type === 'object')
|
|
31
|
+
return 'dict';
|
|
32
|
+
if (schema.enum)
|
|
33
|
+
return 'str';
|
|
34
|
+
return 'any';
|
|
35
|
+
}
|
|
36
|
+
function buildIndex(tools) {
|
|
37
|
+
const index = [];
|
|
38
|
+
for (const [name, t] of Object.entries(tools)) {
|
|
39
|
+
const pyName = toPythonName(name);
|
|
40
|
+
const description = 'description' in t ? (t.description || '') : '';
|
|
41
|
+
const params = [];
|
|
42
|
+
try {
|
|
43
|
+
// AI SDK tool() stores the schema as inputSchema (raw Zod)
|
|
44
|
+
// but other wrappers may use parameters
|
|
45
|
+
const schema = ('inputSchema' in t ? t.inputSchema : null) ??
|
|
46
|
+
('parameters' in t ? t.parameters : null);
|
|
47
|
+
if (schema && typeof schema === 'object') {
|
|
48
|
+
const schemaObj = schema;
|
|
49
|
+
let jsonSchema = null;
|
|
50
|
+
// AI SDK Schema wrapper — has a jsonSchema property
|
|
51
|
+
if ('jsonSchema' in schemaObj && schemaObj.jsonSchema) {
|
|
52
|
+
jsonSchema = schemaObj.jsonSchema;
|
|
53
|
+
}
|
|
54
|
+
// Raw Zod schema — has _def property
|
|
55
|
+
else if ('_def' in schemaObj) {
|
|
56
|
+
jsonSchema = zodToJsonSchema(schema, {
|
|
57
|
+
target: 'jsonSchema7',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (jsonSchema) {
|
|
61
|
+
const properties = jsonSchema.properties || {};
|
|
62
|
+
const required = new Set(jsonSchema.required || []);
|
|
63
|
+
for (const [pName, pSchema] of Object.entries(properties)) {
|
|
64
|
+
params.push({
|
|
65
|
+
name: pName,
|
|
66
|
+
type: jsonSchemaTypeToPython(pSchema),
|
|
67
|
+
required: required.has(pName),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Cannot introspect schema; proceed with empty params
|
|
75
|
+
}
|
|
76
|
+
const paramStrs = params.map((p) => {
|
|
77
|
+
if (p.required)
|
|
78
|
+
return `${p.name}: ${p.type}`;
|
|
79
|
+
return `${p.name}: ${p.type} = None`;
|
|
80
|
+
});
|
|
81
|
+
const signature = `${pyName}(${paramStrs.join(', ')})`;
|
|
82
|
+
const exampleParams = params
|
|
83
|
+
.filter((p) => p.required)
|
|
84
|
+
.slice(0, 2)
|
|
85
|
+
.map((p) => {
|
|
86
|
+
if (p.type === 'str')
|
|
87
|
+
return `${p.name}="..."`;
|
|
88
|
+
if (p.type === 'int' || p.type === 'float')
|
|
89
|
+
return `${p.name}=1`;
|
|
90
|
+
if (p.type === 'bool')
|
|
91
|
+
return `${p.name}=True`;
|
|
92
|
+
return `${p.name}=...`;
|
|
93
|
+
});
|
|
94
|
+
const example = `${pyName}(${exampleParams.join(', ')})`;
|
|
95
|
+
index.push({
|
|
96
|
+
originalName: name,
|
|
97
|
+
pyName,
|
|
98
|
+
description,
|
|
99
|
+
signature,
|
|
100
|
+
example,
|
|
101
|
+
searchText: `${name} ${pyName} ${description}`.toLowerCase(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return index;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Recursively convert Monty's Map objects (Python dicts) to plain JS objects
|
|
108
|
+
* so they JSON.stringify correctly.
|
|
109
|
+
*/
|
|
110
|
+
function montyToJson(value) {
|
|
111
|
+
if (value instanceof Map) {
|
|
112
|
+
const obj = {};
|
|
113
|
+
for (const [k, v] of value) {
|
|
114
|
+
obj[String(k)] = montyToJson(v);
|
|
115
|
+
}
|
|
116
|
+
return obj;
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
return value.map(montyToJson);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Transform an AI SDK ToolSet into exactly 2 code-mode tools: `search` and `execute`.
|
|
125
|
+
*
|
|
126
|
+
* The model discovers endpoints via keyword search and calls them by writing
|
|
127
|
+
* Python code that is executed in a sandboxed Monty interpreter. This reduces
|
|
128
|
+
* token consumption by ~99.9% for large APIs (N tools → 2 tools).
|
|
129
|
+
*/
|
|
130
|
+
export function toCodeModeTools(tools) {
|
|
131
|
+
const index = buildIndex(tools);
|
|
132
|
+
// Build a mapping from Python-safe names to original tool names
|
|
133
|
+
const pyToOriginal = new Map();
|
|
134
|
+
for (const entry of index) {
|
|
135
|
+
pyToOriginal.set(entry.pyName, entry.originalName);
|
|
136
|
+
}
|
|
137
|
+
const pythonNames = [...pyToOriginal.keys()];
|
|
138
|
+
const searchTool = tool({
|
|
139
|
+
description: 'Search for available API endpoints by keyword. Returns matching ' +
|
|
140
|
+
'function signatures with parameter types and usage examples. ' +
|
|
141
|
+
'Use this to discover what functions are available before writing code.',
|
|
142
|
+
inputSchema: z.object({
|
|
143
|
+
query: z
|
|
144
|
+
.string()
|
|
145
|
+
.describe('Keywords to search for in endpoint names and descriptions'),
|
|
146
|
+
}),
|
|
147
|
+
execute: async ({ query }) => {
|
|
148
|
+
const keywords = query
|
|
149
|
+
.toLowerCase()
|
|
150
|
+
.split(/\s+/)
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
const matches = index.filter((entry) => keywords.some((kw) => entry.searchText.includes(kw)));
|
|
153
|
+
if (matches.length === 0) {
|
|
154
|
+
return `No endpoints found matching "${query}". Try different keywords.`;
|
|
155
|
+
}
|
|
156
|
+
return matches
|
|
157
|
+
.map((m) => `${m.signature}\n ${m.description}\n Example: ${m.example}`)
|
|
158
|
+
.join('\n\n');
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const executeTool = tool({
|
|
162
|
+
description: 'Execute Python code that calls API endpoints. Functions discovered ' +
|
|
163
|
+
'via search are available to call directly. Always use keyword arguments.\n' +
|
|
164
|
+
'Example:\n' +
|
|
165
|
+
' users = listUsers(limit=10)\n' +
|
|
166
|
+
' user = getUser(id="123")\n' +
|
|
167
|
+
' users',
|
|
168
|
+
inputSchema: z.object({
|
|
169
|
+
code: z
|
|
170
|
+
.string()
|
|
171
|
+
.describe('Python code to execute. Use keyword arguments when calling API functions.'),
|
|
172
|
+
}),
|
|
173
|
+
execute: async ({ code }) => {
|
|
174
|
+
try {
|
|
175
|
+
const m = new Monty(code, { externalFunctions: pythonNames });
|
|
176
|
+
let progress = m.start({
|
|
177
|
+
limits: { maxDurationSecs: 30 },
|
|
178
|
+
});
|
|
179
|
+
while (progress instanceof MontySnapshot) {
|
|
180
|
+
const snapshot = progress;
|
|
181
|
+
const originalName = pyToOriginal.get(snapshot.functionName);
|
|
182
|
+
const toolEntry = originalName ? tools[originalName] : undefined;
|
|
183
|
+
if (!toolEntry ||
|
|
184
|
+
!('execute' in toolEntry) ||
|
|
185
|
+
!toolEntry.execute) {
|
|
186
|
+
// Should not happen since all tool names are registered,
|
|
187
|
+
// but handle gracefully
|
|
188
|
+
progress = snapshot.resume({
|
|
189
|
+
exception: {
|
|
190
|
+
type: 'ValueError',
|
|
191
|
+
message: `Unknown function: ${snapshot.functionName}`,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const params = {
|
|
197
|
+
...snapshot.kwargs,
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
const result = await toolEntry.execute(params, {});
|
|
201
|
+
progress = snapshot.resume({ returnValue: result });
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
progress = snapshot.resume({
|
|
205
|
+
exception: {
|
|
206
|
+
type: 'RuntimeError',
|
|
207
|
+
message: error instanceof Error ? error.message : String(error),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return montyToJson(progress.output);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof MontySyntaxError) {
|
|
216
|
+
return `Syntax Error: ${error.message}`;
|
|
217
|
+
}
|
|
218
|
+
if (error instanceof MontyRuntimeError) {
|
|
219
|
+
return `Runtime Error: ${error.message}\n${error.traceback()}`;
|
|
220
|
+
}
|
|
221
|
+
if (error instanceof MontyTypingError) {
|
|
222
|
+
return `Type Error: ${error.displayDiagnostics()}`;
|
|
223
|
+
}
|
|
224
|
+
return `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
search: searchTool,
|
|
230
|
+
execute: executeTool,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=code-mode.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,7 @@ export type { HttpMethod, Tool, AuthType, AuthConfig, Session, OpenAPISpec, Path
|
|
|
3
3
|
export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, extractOperationAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
|
|
4
4
|
export { AuthManager } from './auth-manager.js';
|
|
5
5
|
export { createExecutableTools, executeToolByName } from './tool-executor.js';
|
|
6
|
+
export { toAISDKTools } from './ai-tools.js';
|
|
7
|
+
export type { ToolSet } from './ai-tools.js';
|
|
8
|
+
export { toCodeModeTools } from './code-mode.js';
|
|
6
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -6,4 +6,8 @@ export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, extractOperationAut
|
|
|
6
6
|
export { AuthManager } from './auth-manager.js';
|
|
7
7
|
// Tool Executor
|
|
8
8
|
export { createExecutableTools, executeToolByName } from './tool-executor.js';
|
|
9
|
+
// AI SDK Tools
|
|
10
|
+
export { toAISDKTools } from './ai-tools.js';
|
|
11
|
+
// Code Mode
|
|
12
|
+
export { toCodeModeTools } from './code-mode.js';
|
|
9
13
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spec2tools/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Core utilities for OpenAPI parsing and authentication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"directory": "packages/core"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@pydantic/monty": "^0.0.7",
|
|
32
|
+
"ai": "^6.0.68",
|
|
31
33
|
"express": "^5.0.0",
|
|
32
34
|
"open": "^10.1.0",
|
|
33
35
|
"yaml": "^2.5.0",
|
|
@@ -37,7 +39,8 @@
|
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@types/express": "^5.0.0",
|
|
39
41
|
"@types/node": "^22.0.0",
|
|
40
|
-
"typescript": "^5.6.0"
|
|
42
|
+
"typescript": "^5.6.0",
|
|
43
|
+
"vitest": "^4.0.18"
|
|
41
44
|
},
|
|
42
45
|
"engines": {
|
|
43
46
|
"node": ">=18.0.0"
|
|
@@ -45,6 +48,8 @@
|
|
|
45
48
|
"scripts": {
|
|
46
49
|
"build": "tsc",
|
|
47
50
|
"dev": "tsc --watch",
|
|
48
|
-
"typecheck": "tsc --noEmit"
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest"
|
|
49
54
|
}
|
|
50
55
|
}
|