@objectstack/service-ai 4.0.1 → 4.0.3
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +17 -0
- package/dist/index.cjs +1632 -355
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +330 -87
- package/dist/index.d.ts +330 -87
- package/dist/index.js +1623 -352
- package/dist/index.js.map +1 -1
- package/package.json +27 -5
- package/src/__tests__/ai-service.test.ts +260 -27
- package/src/__tests__/auth-and-toolcalling.test.ts +81 -29
- package/src/__tests__/chatbot-features.test.ts +397 -102
- package/src/__tests__/metadata-tools.test.ts +970 -0
- package/src/__tests__/objectql-conversation-service.test.ts +34 -16
- package/src/__tests__/tool-routes.test.ts +191 -0
- package/src/__tests__/vercel-stream-encoder.test.ts +310 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/memory-adapter.ts +17 -9
- package/src/adapters/vercel-adapter.ts +148 -0
- package/src/agent-runtime.ts +27 -3
- package/src/agents/index.ts +1 -0
- package/src/agents/metadata-assistant-agent.ts +87 -0
- package/src/ai-service.ts +75 -36
- package/src/conversation/in-memory-conversation-service.ts +2 -2
- package/src/conversation/objectql-conversation-service.ts +67 -18
- package/src/index.ts +22 -2
- package/src/plugin.ts +237 -30
- package/src/routes/agent-routes.ts +68 -12
- package/src/routes/ai-routes.ts +93 -14
- package/src/routes/index.ts +1 -0
- package/src/routes/message-utils.ts +90 -0
- package/src/routes/tool-routes.ts +142 -0
- package/src/stream/index.ts +3 -0
- package/src/stream/vercel-stream-encoder.ts +153 -0
- package/src/tools/add-field.tool.ts +70 -0
- package/src/tools/create-object.tool.ts +66 -0
- package/src/tools/data-tools.ts +4 -101
- package/src/tools/delete-field.tool.ts +38 -0
- package/src/tools/describe-object.tool.ts +31 -0
- package/src/tools/index.ts +12 -1
- package/src/tools/list-objects.tool.ts +34 -0
- package/src/tools/metadata-tools.ts +430 -0
- package/src/tools/modify-field.tool.ts +44 -0
- package/src/tools/tool-registry.ts +32 -9
package/dist/index.js
CHANGED
|
@@ -1,3 +1,759 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/tools/data-tools.ts
|
|
12
|
+
var data_tools_exports = {};
|
|
13
|
+
__export(data_tools_exports, {
|
|
14
|
+
AGGREGATE_DATA_TOOL: () => AGGREGATE_DATA_TOOL,
|
|
15
|
+
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
16
|
+
GET_RECORD_TOOL: () => GET_RECORD_TOOL,
|
|
17
|
+
QUERY_RECORDS_TOOL: () => QUERY_RECORDS_TOOL,
|
|
18
|
+
registerDataTools: () => registerDataTools
|
|
19
|
+
});
|
|
20
|
+
function createQueryRecordsHandler(ctx) {
|
|
21
|
+
return async (args) => {
|
|
22
|
+
const {
|
|
23
|
+
objectName,
|
|
24
|
+
where,
|
|
25
|
+
fields,
|
|
26
|
+
orderBy,
|
|
27
|
+
limit,
|
|
28
|
+
offset
|
|
29
|
+
} = args;
|
|
30
|
+
const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
|
|
31
|
+
const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
|
|
32
|
+
const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
|
|
33
|
+
const records = await ctx.dataEngine.find(objectName, {
|
|
34
|
+
where,
|
|
35
|
+
fields,
|
|
36
|
+
orderBy,
|
|
37
|
+
limit: safeLimit,
|
|
38
|
+
offset: safeOffset
|
|
39
|
+
});
|
|
40
|
+
return JSON.stringify({ count: records.length, records });
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function createGetRecordHandler(ctx) {
|
|
44
|
+
return async (args) => {
|
|
45
|
+
const { objectName, recordId, fields } = args;
|
|
46
|
+
const record = await ctx.dataEngine.findOne(objectName, {
|
|
47
|
+
where: { id: recordId },
|
|
48
|
+
fields
|
|
49
|
+
});
|
|
50
|
+
if (!record) {
|
|
51
|
+
return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(record);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createAggregateDataHandler(ctx) {
|
|
57
|
+
return async (args) => {
|
|
58
|
+
const { objectName, aggregations, groupBy, where } = args;
|
|
59
|
+
for (const a of aggregations) {
|
|
60
|
+
if (!VALID_AGG_FUNCTIONS.has(a.function)) {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const result = await ctx.dataEngine.aggregate(objectName, {
|
|
67
|
+
where,
|
|
68
|
+
groupBy,
|
|
69
|
+
aggregations: aggregations.map((a) => ({
|
|
70
|
+
function: a.function,
|
|
71
|
+
field: a.field,
|
|
72
|
+
alias: a.alias
|
|
73
|
+
}))
|
|
74
|
+
});
|
|
75
|
+
return JSON.stringify(result);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function registerDataTools(registry, context) {
|
|
79
|
+
registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
|
|
80
|
+
registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
|
|
81
|
+
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
82
|
+
}
|
|
83
|
+
var MAX_QUERY_LIMIT, DEFAULT_QUERY_LIMIT, QUERY_RECORDS_TOOL, GET_RECORD_TOOL, AGGREGATE_DATA_TOOL, DATA_TOOL_DEFINITIONS, VALID_AGG_FUNCTIONS;
|
|
84
|
+
var init_data_tools = __esm({
|
|
85
|
+
"src/tools/data-tools.ts"() {
|
|
86
|
+
"use strict";
|
|
87
|
+
MAX_QUERY_LIMIT = 200;
|
|
88
|
+
DEFAULT_QUERY_LIMIT = 20;
|
|
89
|
+
QUERY_RECORDS_TOOL = {
|
|
90
|
+
name: "query_records",
|
|
91
|
+
description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
|
|
92
|
+
parameters: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
objectName: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "The snake_case name of the object to query"
|
|
98
|
+
},
|
|
99
|
+
where: {
|
|
100
|
+
type: "object",
|
|
101
|
+
description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
|
|
102
|
+
},
|
|
103
|
+
fields: {
|
|
104
|
+
type: "array",
|
|
105
|
+
items: { type: "string" },
|
|
106
|
+
description: "List of field names to return (omit for all fields)"
|
|
107
|
+
},
|
|
108
|
+
orderBy: {
|
|
109
|
+
type: "array",
|
|
110
|
+
items: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
field: { type: "string" },
|
|
114
|
+
order: { type: "string", enum: ["asc", "desc"] }
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
|
|
118
|
+
},
|
|
119
|
+
limit: {
|
|
120
|
+
type: "number",
|
|
121
|
+
description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
|
|
122
|
+
},
|
|
123
|
+
offset: {
|
|
124
|
+
type: "number",
|
|
125
|
+
description: "Number of records to skip for pagination"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
required: ["objectName"],
|
|
129
|
+
additionalProperties: false
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
GET_RECORD_TOOL = {
|
|
133
|
+
name: "get_record",
|
|
134
|
+
description: "Get a single record by its ID from a data object.",
|
|
135
|
+
parameters: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
objectName: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "The snake_case name of the object"
|
|
141
|
+
},
|
|
142
|
+
recordId: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "The unique ID of the record"
|
|
145
|
+
},
|
|
146
|
+
fields: {
|
|
147
|
+
type: "array",
|
|
148
|
+
items: { type: "string" },
|
|
149
|
+
description: "List of field names to return (omit for all fields)"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
required: ["objectName", "recordId"],
|
|
153
|
+
additionalProperties: false
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
AGGREGATE_DATA_TOOL = {
|
|
157
|
+
name: "aggregate_data",
|
|
158
|
+
description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
|
|
159
|
+
parameters: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
objectName: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "The snake_case name of the object to aggregate"
|
|
165
|
+
},
|
|
166
|
+
aggregations: {
|
|
167
|
+
type: "array",
|
|
168
|
+
items: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
function: {
|
|
172
|
+
type: "string",
|
|
173
|
+
enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
|
|
174
|
+
description: "Aggregation function"
|
|
175
|
+
},
|
|
176
|
+
field: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "Field to aggregate (optional for count)"
|
|
179
|
+
},
|
|
180
|
+
alias: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "Result column alias"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
required: ["function", "alias"]
|
|
186
|
+
},
|
|
187
|
+
description: "Aggregation definitions"
|
|
188
|
+
},
|
|
189
|
+
groupBy: {
|
|
190
|
+
type: "array",
|
|
191
|
+
items: { type: "string" },
|
|
192
|
+
description: "Fields to group by"
|
|
193
|
+
},
|
|
194
|
+
where: {
|
|
195
|
+
type: "object",
|
|
196
|
+
description: "Filter conditions applied before aggregation"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
required: ["objectName", "aggregations"],
|
|
200
|
+
additionalProperties: false
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
DATA_TOOL_DEFINITIONS = [
|
|
204
|
+
QUERY_RECORDS_TOOL,
|
|
205
|
+
GET_RECORD_TOOL,
|
|
206
|
+
AGGREGATE_DATA_TOOL
|
|
207
|
+
];
|
|
208
|
+
VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
209
|
+
"count",
|
|
210
|
+
"sum",
|
|
211
|
+
"avg",
|
|
212
|
+
"min",
|
|
213
|
+
"max",
|
|
214
|
+
"count_distinct"
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// src/tools/create-object.tool.ts
|
|
220
|
+
import { defineTool } from "@objectstack/spec/ai";
|
|
221
|
+
var createObjectTool;
|
|
222
|
+
var init_create_object_tool = __esm({
|
|
223
|
+
"src/tools/create-object.tool.ts"() {
|
|
224
|
+
"use strict";
|
|
225
|
+
createObjectTool = defineTool({
|
|
226
|
+
name: "create_object",
|
|
227
|
+
label: "Create Object",
|
|
228
|
+
description: "Creates a new data object (table) with the specified name, label, and optional field definitions. Use this when the user wants to create a new entity, table, or data model.",
|
|
229
|
+
category: "data",
|
|
230
|
+
builtIn: true,
|
|
231
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
232
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
233
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
234
|
+
// should only be set once server-side approval gating is implemented to
|
|
235
|
+
// avoid giving users a false sense of safety.
|
|
236
|
+
parameters: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
name: {
|
|
240
|
+
type: "string",
|
|
241
|
+
description: "Machine name for the object (snake_case, e.g. project_task)"
|
|
242
|
+
},
|
|
243
|
+
label: {
|
|
244
|
+
type: "string",
|
|
245
|
+
description: "Human-readable display name (e.g. Project Task)"
|
|
246
|
+
},
|
|
247
|
+
fields: {
|
|
248
|
+
type: "array",
|
|
249
|
+
description: "Initial fields to create with the object",
|
|
250
|
+
items: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
name: { type: "string", description: "Field machine name (snake_case)" },
|
|
254
|
+
label: { type: "string", description: "Field display name" },
|
|
255
|
+
type: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "Field data type",
|
|
258
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
259
|
+
},
|
|
260
|
+
required: { type: "boolean", description: "Whether the field is required" }
|
|
261
|
+
},
|
|
262
|
+
required: ["name", "type"]
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
enableFeatures: {
|
|
266
|
+
type: "object",
|
|
267
|
+
description: "Object capability flags",
|
|
268
|
+
properties: {
|
|
269
|
+
trackHistory: { type: "boolean" },
|
|
270
|
+
apiEnabled: { type: "boolean" }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
required: ["name", "label"],
|
|
275
|
+
additionalProperties: false
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// src/tools/add-field.tool.ts
|
|
282
|
+
import { defineTool as defineTool2 } from "@objectstack/spec/ai";
|
|
283
|
+
var addFieldTool;
|
|
284
|
+
var init_add_field_tool = __esm({
|
|
285
|
+
"src/tools/add-field.tool.ts"() {
|
|
286
|
+
"use strict";
|
|
287
|
+
addFieldTool = defineTool2({
|
|
288
|
+
name: "add_field",
|
|
289
|
+
label: "Add Field",
|
|
290
|
+
description: "Adds a new field (column) to an existing data object. Use this when the user wants to add a property, column, or attribute to a table.",
|
|
291
|
+
category: "data",
|
|
292
|
+
builtIn: true,
|
|
293
|
+
parameters: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
objectName: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "Target object machine name (snake_case)"
|
|
299
|
+
},
|
|
300
|
+
name: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "Field machine name (snake_case, e.g. due_date)"
|
|
303
|
+
},
|
|
304
|
+
label: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Human-readable field label (e.g. Due Date)"
|
|
307
|
+
},
|
|
308
|
+
type: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Field data type",
|
|
311
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
312
|
+
},
|
|
313
|
+
required: {
|
|
314
|
+
type: "boolean",
|
|
315
|
+
description: "Whether the field is required"
|
|
316
|
+
},
|
|
317
|
+
defaultValue: {
|
|
318
|
+
description: "Default value for the field"
|
|
319
|
+
},
|
|
320
|
+
options: {
|
|
321
|
+
type: "array",
|
|
322
|
+
description: "Options for select/picklist fields",
|
|
323
|
+
items: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
label: { type: "string" },
|
|
327
|
+
value: {
|
|
328
|
+
type: "string",
|
|
329
|
+
description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
|
|
330
|
+
pattern: "^[a-z_][a-z0-9_]*$"
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
reference: {
|
|
336
|
+
type: "string",
|
|
337
|
+
description: "Referenced object name for lookup fields (snake_case, e.g. account)"
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
required: ["objectName", "name", "type"],
|
|
341
|
+
additionalProperties: false
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// src/tools/modify-field.tool.ts
|
|
348
|
+
import { defineTool as defineTool3 } from "@objectstack/spec/ai";
|
|
349
|
+
var modifyFieldTool;
|
|
350
|
+
var init_modify_field_tool = __esm({
|
|
351
|
+
"src/tools/modify-field.tool.ts"() {
|
|
352
|
+
"use strict";
|
|
353
|
+
modifyFieldTool = defineTool3({
|
|
354
|
+
name: "modify_field",
|
|
355
|
+
label: "Modify Field",
|
|
356
|
+
description: "Modifies an existing field definition (label, type, required, default value, etc.) on a data object. Use this when the user wants to change or reconfigure an existing column or attribute (not rename it).",
|
|
357
|
+
category: "data",
|
|
358
|
+
builtIn: true,
|
|
359
|
+
parameters: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
objectName: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "Target object machine name (snake_case)"
|
|
365
|
+
},
|
|
366
|
+
fieldName: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Existing field machine name to modify (snake_case)"
|
|
369
|
+
},
|
|
370
|
+
changes: {
|
|
371
|
+
type: "object",
|
|
372
|
+
description: "Field properties to update (partial patch)",
|
|
373
|
+
properties: {
|
|
374
|
+
label: { type: "string", description: "New display label" },
|
|
375
|
+
type: { type: "string", description: "New field type" },
|
|
376
|
+
required: { type: "boolean", description: "Update required constraint" },
|
|
377
|
+
defaultValue: { description: "New default value" }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
required: ["objectName", "fieldName", "changes"],
|
|
382
|
+
additionalProperties: false
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/tools/delete-field.tool.ts
|
|
389
|
+
import { defineTool as defineTool4 } from "@objectstack/spec/ai";
|
|
390
|
+
var deleteFieldTool;
|
|
391
|
+
var init_delete_field_tool = __esm({
|
|
392
|
+
"src/tools/delete-field.tool.ts"() {
|
|
393
|
+
"use strict";
|
|
394
|
+
deleteFieldTool = defineTool4({
|
|
395
|
+
name: "delete_field",
|
|
396
|
+
label: "Delete Field",
|
|
397
|
+
description: "Removes a field (column) from an existing data object. This is a destructive operation. Use this when the user explicitly wants to remove an attribute or column from a table.",
|
|
398
|
+
category: "data",
|
|
399
|
+
builtIn: true,
|
|
400
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
401
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
402
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
403
|
+
// should only be set once server-side approval gating is implemented.
|
|
404
|
+
parameters: {
|
|
405
|
+
type: "object",
|
|
406
|
+
properties: {
|
|
407
|
+
objectName: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: "Target object machine name (snake_case)"
|
|
410
|
+
},
|
|
411
|
+
fieldName: {
|
|
412
|
+
type: "string",
|
|
413
|
+
description: "Field machine name to delete (snake_case)"
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
required: ["objectName", "fieldName"],
|
|
417
|
+
additionalProperties: false
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// src/tools/list-objects.tool.ts
|
|
424
|
+
import { defineTool as defineTool5 } from "@objectstack/spec/ai";
|
|
425
|
+
var listObjectsTool;
|
|
426
|
+
var init_list_objects_tool = __esm({
|
|
427
|
+
"src/tools/list-objects.tool.ts"() {
|
|
428
|
+
"use strict";
|
|
429
|
+
listObjectsTool = defineTool5({
|
|
430
|
+
name: "list_objects",
|
|
431
|
+
label: "List Objects",
|
|
432
|
+
description: "Lists all registered data objects (tables) in the current environment. Use this when the user wants to see what tables, entities, or data models are available.",
|
|
433
|
+
category: "data",
|
|
434
|
+
builtIn: true,
|
|
435
|
+
parameters: {
|
|
436
|
+
type: "object",
|
|
437
|
+
properties: {
|
|
438
|
+
filter: {
|
|
439
|
+
type: "string",
|
|
440
|
+
description: "Optional name or label substring to filter objects"
|
|
441
|
+
},
|
|
442
|
+
includeFields: {
|
|
443
|
+
type: "boolean",
|
|
444
|
+
description: "Whether to include field summaries for each object (default: false)"
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
additionalProperties: false
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// src/tools/describe-object.tool.ts
|
|
454
|
+
import { defineTool as defineTool6 } from "@objectstack/spec/ai";
|
|
455
|
+
var describeObjectTool;
|
|
456
|
+
var init_describe_object_tool = __esm({
|
|
457
|
+
"src/tools/describe-object.tool.ts"() {
|
|
458
|
+
"use strict";
|
|
459
|
+
describeObjectTool = defineTool6({
|
|
460
|
+
name: "describe_object",
|
|
461
|
+
label: "Describe Object",
|
|
462
|
+
description: "Returns the full schema details of a data object, including all fields, types, relationships, and configuration. Use this to understand the structure of a table before querying or modifying it.",
|
|
463
|
+
category: "data",
|
|
464
|
+
builtIn: true,
|
|
465
|
+
parameters: {
|
|
466
|
+
type: "object",
|
|
467
|
+
properties: {
|
|
468
|
+
objectName: {
|
|
469
|
+
type: "string",
|
|
470
|
+
description: "Object machine name to describe (snake_case)"
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
required: ["objectName"],
|
|
474
|
+
additionalProperties: false
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// src/tools/metadata-tools.ts
|
|
481
|
+
var metadata_tools_exports = {};
|
|
482
|
+
__export(metadata_tools_exports, {
|
|
483
|
+
METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
|
|
484
|
+
addFieldTool: () => addFieldTool,
|
|
485
|
+
createObjectTool: () => createObjectTool,
|
|
486
|
+
deleteFieldTool: () => deleteFieldTool,
|
|
487
|
+
describeObjectTool: () => describeObjectTool,
|
|
488
|
+
listObjectsTool: () => listObjectsTool,
|
|
489
|
+
modifyFieldTool: () => modifyFieldTool,
|
|
490
|
+
registerMetadataTools: () => registerMetadataTools
|
|
491
|
+
});
|
|
492
|
+
function isSnakeCase(value) {
|
|
493
|
+
return SNAKE_CASE_RE.test(value);
|
|
494
|
+
}
|
|
495
|
+
function createCreateObjectHandler(ctx) {
|
|
496
|
+
return async (args) => {
|
|
497
|
+
const { name, label, fields, enableFeatures } = args;
|
|
498
|
+
if (!name || !label) {
|
|
499
|
+
return JSON.stringify({ error: 'Both "name" and "label" are required' });
|
|
500
|
+
}
|
|
501
|
+
if (!isSnakeCase(name)) {
|
|
502
|
+
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
|
|
503
|
+
}
|
|
504
|
+
const existing = await ctx.metadataService.getObject(name);
|
|
505
|
+
if (existing) {
|
|
506
|
+
return JSON.stringify({ error: `Object "${name}" already exists` });
|
|
507
|
+
}
|
|
508
|
+
const fieldMap = {};
|
|
509
|
+
if (fields && Array.isArray(fields)) {
|
|
510
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const f of fields) {
|
|
512
|
+
if (!f.name) {
|
|
513
|
+
return JSON.stringify({ error: 'Each field must have a "name" property' });
|
|
514
|
+
}
|
|
515
|
+
if (!isSnakeCase(f.name)) {
|
|
516
|
+
return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
|
|
517
|
+
}
|
|
518
|
+
if (seenNames.has(f.name)) {
|
|
519
|
+
return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
|
|
520
|
+
}
|
|
521
|
+
seenNames.add(f.name);
|
|
522
|
+
fieldMap[f.name] = {
|
|
523
|
+
type: f.type,
|
|
524
|
+
...f.label ? { label: f.label } : {},
|
|
525
|
+
...f.required !== void 0 ? { required: f.required } : {}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const objectDef = {
|
|
530
|
+
name,
|
|
531
|
+
label,
|
|
532
|
+
...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
|
|
533
|
+
...enableFeatures ? { enable: enableFeatures } : {}
|
|
534
|
+
};
|
|
535
|
+
await ctx.metadataService.register("object", name, objectDef);
|
|
536
|
+
return JSON.stringify({
|
|
537
|
+
name,
|
|
538
|
+
label,
|
|
539
|
+
fieldCount: Object.keys(fieldMap).length
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function createAddFieldHandler(ctx) {
|
|
544
|
+
return async (args) => {
|
|
545
|
+
const { objectName, name, label, type, required, defaultValue, options, reference } = args;
|
|
546
|
+
if (!objectName || !name || !type) {
|
|
547
|
+
return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
|
|
548
|
+
}
|
|
549
|
+
if (!isSnakeCase(objectName)) {
|
|
550
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
551
|
+
}
|
|
552
|
+
if (!isSnakeCase(name)) {
|
|
553
|
+
return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
|
|
554
|
+
}
|
|
555
|
+
if (reference && !isSnakeCase(reference)) {
|
|
556
|
+
return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
|
|
557
|
+
}
|
|
558
|
+
if (options && Array.isArray(options)) {
|
|
559
|
+
for (const opt of options) {
|
|
560
|
+
if (opt.value && !isSnakeCase(opt.value)) {
|
|
561
|
+
return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
566
|
+
if (!objectDef) {
|
|
567
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
568
|
+
}
|
|
569
|
+
const def = objectDef;
|
|
570
|
+
if (def.fields && def.fields[name]) {
|
|
571
|
+
return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
|
|
572
|
+
}
|
|
573
|
+
const fieldDef = {
|
|
574
|
+
type,
|
|
575
|
+
...label ? { label } : {},
|
|
576
|
+
...required !== void 0 ? { required } : {},
|
|
577
|
+
...defaultValue !== void 0 ? { defaultValue } : {},
|
|
578
|
+
...options ? { options } : {},
|
|
579
|
+
...reference ? { reference } : {}
|
|
580
|
+
};
|
|
581
|
+
const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
|
|
582
|
+
await ctx.metadataService.register("object", objectName, {
|
|
583
|
+
...def,
|
|
584
|
+
fields: updatedFields
|
|
585
|
+
});
|
|
586
|
+
return JSON.stringify({
|
|
587
|
+
objectName,
|
|
588
|
+
fieldName: name,
|
|
589
|
+
fieldType: type
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function createModifyFieldHandler(ctx) {
|
|
594
|
+
return async (args) => {
|
|
595
|
+
const { objectName, fieldName, changes } = args;
|
|
596
|
+
if (!objectName || !fieldName || !changes) {
|
|
597
|
+
return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
|
|
598
|
+
}
|
|
599
|
+
if (!isSnakeCase(objectName)) {
|
|
600
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
601
|
+
}
|
|
602
|
+
if (!isSnakeCase(fieldName)) {
|
|
603
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
604
|
+
}
|
|
605
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
606
|
+
if (!objectDef) {
|
|
607
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
608
|
+
}
|
|
609
|
+
const def = objectDef;
|
|
610
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
611
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
612
|
+
}
|
|
613
|
+
const existingField = def.fields[fieldName];
|
|
614
|
+
const updatedField = { ...existingField, ...changes };
|
|
615
|
+
const updatedFields = { ...def.fields, [fieldName]: updatedField };
|
|
616
|
+
await ctx.metadataService.register("object", objectName, {
|
|
617
|
+
...def,
|
|
618
|
+
fields: updatedFields
|
|
619
|
+
});
|
|
620
|
+
return JSON.stringify({
|
|
621
|
+
objectName,
|
|
622
|
+
fieldName,
|
|
623
|
+
updatedProperties: Object.keys(changes)
|
|
624
|
+
});
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function createDeleteFieldHandler(ctx) {
|
|
628
|
+
return async (args) => {
|
|
629
|
+
const { objectName, fieldName } = args;
|
|
630
|
+
if (!objectName || !fieldName) {
|
|
631
|
+
return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
|
|
632
|
+
}
|
|
633
|
+
if (!isSnakeCase(objectName)) {
|
|
634
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
635
|
+
}
|
|
636
|
+
if (!isSnakeCase(fieldName)) {
|
|
637
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
638
|
+
}
|
|
639
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
640
|
+
if (!objectDef) {
|
|
641
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
642
|
+
}
|
|
643
|
+
const def = objectDef;
|
|
644
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
645
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
646
|
+
}
|
|
647
|
+
const { [fieldName]: _removed, ...remainingFields } = def.fields;
|
|
648
|
+
await ctx.metadataService.register("object", objectName, {
|
|
649
|
+
...def,
|
|
650
|
+
fields: remainingFields
|
|
651
|
+
});
|
|
652
|
+
return JSON.stringify({
|
|
653
|
+
objectName,
|
|
654
|
+
fieldName,
|
|
655
|
+
success: true
|
|
656
|
+
});
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function createListObjectsHandler(ctx) {
|
|
660
|
+
return async (args) => {
|
|
661
|
+
const { filter, includeFields } = args ?? {};
|
|
662
|
+
const objects = await ctx.metadataService.listObjects();
|
|
663
|
+
let result = objects.map((o) => {
|
|
664
|
+
const base = {
|
|
665
|
+
name: o.name,
|
|
666
|
+
label: o.label ?? o.name,
|
|
667
|
+
fieldCount: o.fields ? Object.keys(o.fields).length : 0
|
|
668
|
+
};
|
|
669
|
+
if (includeFields && o.fields) {
|
|
670
|
+
base.fields = Object.entries(o.fields).map(([key, f]) => ({
|
|
671
|
+
name: key,
|
|
672
|
+
type: f.type,
|
|
673
|
+
label: f.label ?? key
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
return base;
|
|
677
|
+
});
|
|
678
|
+
if (filter) {
|
|
679
|
+
const lower = filter.toLowerCase();
|
|
680
|
+
result = result.filter(
|
|
681
|
+
(o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return JSON.stringify({
|
|
685
|
+
objects: result,
|
|
686
|
+
totalCount: result.length
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function createDescribeObjectHandler(ctx) {
|
|
691
|
+
return async (args) => {
|
|
692
|
+
const { objectName } = args;
|
|
693
|
+
if (!objectName) {
|
|
694
|
+
return JSON.stringify({ error: '"objectName" is required' });
|
|
695
|
+
}
|
|
696
|
+
if (!isSnakeCase(objectName)) {
|
|
697
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
698
|
+
}
|
|
699
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
700
|
+
if (!objectDef) {
|
|
701
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
702
|
+
}
|
|
703
|
+
const def = objectDef;
|
|
704
|
+
const fields = def.fields ?? {};
|
|
705
|
+
const fieldSummary = Object.entries(fields).map(([key, f]) => ({
|
|
706
|
+
name: key,
|
|
707
|
+
type: f.type,
|
|
708
|
+
label: f.label ?? key,
|
|
709
|
+
required: f.required ?? false,
|
|
710
|
+
...f.reference ? { reference: f.reference } : {},
|
|
711
|
+
...f.options ? { options: f.options } : {}
|
|
712
|
+
}));
|
|
713
|
+
return JSON.stringify({
|
|
714
|
+
name: def.name,
|
|
715
|
+
label: def.label ?? def.name,
|
|
716
|
+
fields: fieldSummary,
|
|
717
|
+
enableFeatures: def.enable ?? {}
|
|
718
|
+
});
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function registerMetadataTools(registry, context) {
|
|
722
|
+
registry.register(createObjectTool, createCreateObjectHandler(context));
|
|
723
|
+
registry.register(addFieldTool, createAddFieldHandler(context));
|
|
724
|
+
registry.register(modifyFieldTool, createModifyFieldHandler(context));
|
|
725
|
+
registry.register(deleteFieldTool, createDeleteFieldHandler(context));
|
|
726
|
+
registry.register(listObjectsTool, createListObjectsHandler(context));
|
|
727
|
+
registry.register(describeObjectTool, createDescribeObjectHandler(context));
|
|
728
|
+
}
|
|
729
|
+
var METADATA_TOOL_DEFINITIONS, SNAKE_CASE_RE;
|
|
730
|
+
var init_metadata_tools = __esm({
|
|
731
|
+
"src/tools/metadata-tools.ts"() {
|
|
732
|
+
"use strict";
|
|
733
|
+
init_create_object_tool();
|
|
734
|
+
init_add_field_tool();
|
|
735
|
+
init_modify_field_tool();
|
|
736
|
+
init_delete_field_tool();
|
|
737
|
+
init_list_objects_tool();
|
|
738
|
+
init_describe_object_tool();
|
|
739
|
+
init_create_object_tool();
|
|
740
|
+
init_add_field_tool();
|
|
741
|
+
init_modify_field_tool();
|
|
742
|
+
init_delete_field_tool();
|
|
743
|
+
init_list_objects_tool();
|
|
744
|
+
init_describe_object_tool();
|
|
745
|
+
METADATA_TOOL_DEFINITIONS = [
|
|
746
|
+
createObjectTool,
|
|
747
|
+
addFieldTool,
|
|
748
|
+
modifyFieldTool,
|
|
749
|
+
deleteFieldTool,
|
|
750
|
+
listObjectsTool,
|
|
751
|
+
describeObjectTool
|
|
752
|
+
];
|
|
753
|
+
SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
1
757
|
// src/ai-service.ts
|
|
2
758
|
import { createLogger } from "@objectstack/core";
|
|
3
759
|
|
|
@@ -8,7 +764,9 @@ var MemoryLLMAdapter = class {
|
|
|
8
764
|
}
|
|
9
765
|
async chat(messages, options) {
|
|
10
766
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
11
|
-
const
|
|
767
|
+
const userContent = lastUserMessage?.content;
|
|
768
|
+
const text = typeof userContent === "string" ? userContent : "(complex content)";
|
|
769
|
+
const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
|
|
12
770
|
return {
|
|
13
771
|
content,
|
|
14
772
|
model: options?.model ?? "memory",
|
|
@@ -26,10 +784,15 @@ var MemoryLLMAdapter = class {
|
|
|
26
784
|
const result = await this.chat(messages);
|
|
27
785
|
const words = result.content.split(" ");
|
|
28
786
|
for (let i = 0; i < words.length; i++) {
|
|
29
|
-
const
|
|
30
|
-
yield { type: "text-delta",
|
|
787
|
+
const wordText = i === 0 ? words[i] : ` ${words[i]}`;
|
|
788
|
+
yield { type: "text-delta", id: `delta_${i}`, text: wordText };
|
|
31
789
|
}
|
|
32
|
-
yield {
|
|
790
|
+
yield {
|
|
791
|
+
type: "finish",
|
|
792
|
+
finishReason: "stop",
|
|
793
|
+
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
794
|
+
rawFinishReason: "stop"
|
|
795
|
+
};
|
|
33
796
|
}
|
|
34
797
|
async embed(input) {
|
|
35
798
|
const texts = Array.isArray(input) ? input : [input];
|
|
@@ -92,21 +855,34 @@ var ToolRegistry = class {
|
|
|
92
855
|
* Execute a tool call and return the result.
|
|
93
856
|
*/
|
|
94
857
|
async execute(toolCall) {
|
|
95
|
-
const handler = this.handlers.get(toolCall.
|
|
858
|
+
const handler = this.handlers.get(toolCall.toolName);
|
|
96
859
|
if (!handler) {
|
|
97
860
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
861
|
+
type: "tool-result",
|
|
862
|
+
toolCallId: toolCall.toolCallId,
|
|
863
|
+
toolName: toolCall.toolName,
|
|
864
|
+
output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
|
|
100
865
|
isError: true
|
|
101
866
|
};
|
|
102
867
|
}
|
|
103
868
|
try {
|
|
104
|
-
const args = JSON.parse(toolCall.
|
|
869
|
+
const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
|
|
105
870
|
const content = await handler(args);
|
|
106
|
-
return {
|
|
871
|
+
return {
|
|
872
|
+
type: "tool-result",
|
|
873
|
+
toolCallId: toolCall.toolCallId,
|
|
874
|
+
toolName: toolCall.toolName,
|
|
875
|
+
output: { type: "text", value: content }
|
|
876
|
+
};
|
|
107
877
|
} catch (err) {
|
|
108
878
|
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
-
return {
|
|
879
|
+
return {
|
|
880
|
+
type: "tool-result",
|
|
881
|
+
toolCallId: toolCall.toolCallId,
|
|
882
|
+
toolName: toolCall.toolName,
|
|
883
|
+
output: { type: "text", value: message },
|
|
884
|
+
isError: true
|
|
885
|
+
};
|
|
110
886
|
}
|
|
111
887
|
}
|
|
112
888
|
/**
|
|
@@ -192,6 +968,17 @@ var InMemoryConversationService = class {
|
|
|
192
968
|
};
|
|
193
969
|
|
|
194
970
|
// src/ai-service.ts
|
|
971
|
+
function textDeltaPart(id, text) {
|
|
972
|
+
return { type: "text-delta", id, text };
|
|
973
|
+
}
|
|
974
|
+
function finishPart(result) {
|
|
975
|
+
return {
|
|
976
|
+
type: "finish",
|
|
977
|
+
finishReason: "stop",
|
|
978
|
+
totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
979
|
+
rawFinishReason: "stop"
|
|
980
|
+
};
|
|
981
|
+
}
|
|
195
982
|
var _AIService = class _AIService {
|
|
196
983
|
constructor(config = {}) {
|
|
197
984
|
this.adapter = config.adapter ?? new MemoryLLMAdapter();
|
|
@@ -219,8 +1006,8 @@ var _AIService = class _AIService {
|
|
|
219
1006
|
this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
|
|
220
1007
|
if (!this.adapter.streamChat) {
|
|
221
1008
|
const result = await this.adapter.chat(messages, options);
|
|
222
|
-
yield
|
|
223
|
-
yield
|
|
1009
|
+
yield textDeltaPart("fallback", result.content);
|
|
1010
|
+
yield finishPart(result);
|
|
224
1011
|
return;
|
|
225
1012
|
}
|
|
226
1013
|
yield* this.adapter.streamChat(messages, options);
|
|
@@ -237,6 +1024,10 @@ var _AIService = class _AIService {
|
|
|
237
1024
|
}
|
|
238
1025
|
return this.adapter.listModels();
|
|
239
1026
|
}
|
|
1027
|
+
/** Extract the text value from a ToolExecutionResult's output. */
|
|
1028
|
+
static extractOutputText(tr) {
|
|
1029
|
+
return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
|
|
1030
|
+
}
|
|
240
1031
|
/**
|
|
241
1032
|
* Chat with automatic tool call resolution.
|
|
242
1033
|
*
|
|
@@ -277,23 +1068,26 @@ var _AIService = class _AIService {
|
|
|
277
1068
|
}
|
|
278
1069
|
this.logger.debug("[AI] chatWithTools tool calls", {
|
|
279
1070
|
iteration,
|
|
280
|
-
calls: result.toolCalls.map((tc) => tc.
|
|
1071
|
+
calls: result.toolCalls.map((tc) => tc.toolName)
|
|
281
1072
|
});
|
|
1073
|
+
const assistantContent = [];
|
|
1074
|
+
if (result.content) assistantContent.push({ type: "text", text: result.content });
|
|
1075
|
+
assistantContent.push(...result.toolCalls);
|
|
282
1076
|
conversation.push({
|
|
283
1077
|
role: "assistant",
|
|
284
|
-
content:
|
|
285
|
-
toolCalls: result.toolCalls
|
|
1078
|
+
content: assistantContent
|
|
286
1079
|
});
|
|
287
1080
|
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
288
1081
|
for (const tr of toolResults) {
|
|
289
1082
|
if (tr.isError) {
|
|
290
|
-
const matchedCall = result.toolCalls.find((tc) => tc.
|
|
291
|
-
const toolName = matchedCall?.
|
|
292
|
-
const
|
|
1083
|
+
const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
1084
|
+
const toolName = matchedCall?.toolName ?? "unknown";
|
|
1085
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
1086
|
+
const errorEntry = { iteration, toolName, error: errorText };
|
|
293
1087
|
toolErrors.push(errorEntry);
|
|
294
1088
|
this.logger.warn("[AI] chatWithTools tool error", errorEntry);
|
|
295
1089
|
if (onToolError && matchedCall) {
|
|
296
|
-
const action = onToolError(matchedCall,
|
|
1090
|
+
const action = onToolError(matchedCall, errorText);
|
|
297
1091
|
if (action === "abort") {
|
|
298
1092
|
abortedByCallback = true;
|
|
299
1093
|
}
|
|
@@ -301,8 +1095,7 @@ var _AIService = class _AIService {
|
|
|
301
1095
|
}
|
|
302
1096
|
conversation.push({
|
|
303
1097
|
role: "tool",
|
|
304
|
-
content: tr
|
|
305
|
-
toolCallId: tr.toolCallId
|
|
1098
|
+
content: [tr]
|
|
306
1099
|
});
|
|
307
1100
|
}
|
|
308
1101
|
if (abortedByCallback) {
|
|
@@ -348,33 +1141,41 @@ var _AIService = class _AIService {
|
|
|
348
1141
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
349
1142
|
const result2 = await this.adapter.chat(conversation, chatOptions);
|
|
350
1143
|
if (!result2.toolCalls || result2.toolCalls.length === 0) {
|
|
351
|
-
yield
|
|
352
|
-
yield
|
|
1144
|
+
yield textDeltaPart("stream", result2.content);
|
|
1145
|
+
yield finishPart(result2);
|
|
353
1146
|
return;
|
|
354
1147
|
}
|
|
355
1148
|
for (const tc of result2.toolCalls) {
|
|
356
|
-
yield { type: "tool-call",
|
|
1149
|
+
yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
|
|
357
1150
|
}
|
|
1151
|
+
const assistantContent = [];
|
|
1152
|
+
if (result2.content) assistantContent.push({ type: "text", text: result2.content });
|
|
1153
|
+
assistantContent.push(...result2.toolCalls);
|
|
358
1154
|
conversation.push({
|
|
359
1155
|
role: "assistant",
|
|
360
|
-
content:
|
|
361
|
-
toolCalls: result2.toolCalls
|
|
1156
|
+
content: assistantContent
|
|
362
1157
|
});
|
|
363
1158
|
const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
|
|
364
1159
|
for (const tr of toolResults) {
|
|
365
1160
|
if (tr.isError && onToolError) {
|
|
366
|
-
const matchedCall = result2.toolCalls.find((tc) => tc.
|
|
1161
|
+
const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
367
1162
|
if (matchedCall) {
|
|
368
|
-
const
|
|
1163
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
1164
|
+
const action = onToolError(matchedCall, errorText);
|
|
369
1165
|
if (action === "abort") {
|
|
370
1166
|
abortedByCallback = true;
|
|
371
1167
|
}
|
|
372
1168
|
}
|
|
373
1169
|
}
|
|
1170
|
+
yield {
|
|
1171
|
+
type: "tool-result",
|
|
1172
|
+
toolCallId: tr.toolCallId,
|
|
1173
|
+
toolName: tr.toolName,
|
|
1174
|
+
output: tr.output
|
|
1175
|
+
};
|
|
374
1176
|
conversation.push({
|
|
375
1177
|
role: "tool",
|
|
376
|
-
content: tr
|
|
377
|
-
toolCallId: tr.toolCallId
|
|
1178
|
+
content: [tr]
|
|
378
1179
|
});
|
|
379
1180
|
}
|
|
380
1181
|
if (abortedByCallback) {
|
|
@@ -388,8 +1189,8 @@ var _AIService = class _AIService {
|
|
|
388
1189
|
}
|
|
389
1190
|
const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
|
|
390
1191
|
const result = await this.adapter.chat(conversation, finalOptions);
|
|
391
|
-
yield
|
|
392
|
-
yield
|
|
1192
|
+
yield textDeltaPart("stream", result.content);
|
|
1193
|
+
yield finishPart(result);
|
|
393
1194
|
}
|
|
394
1195
|
};
|
|
395
1196
|
// ── Tool Call Loop ────────────────────────────────────────────
|
|
@@ -397,6 +1198,143 @@ var _AIService = class _AIService {
|
|
|
397
1198
|
_AIService.DEFAULT_MAX_ITERATIONS = 10;
|
|
398
1199
|
var AIService = _AIService;
|
|
399
1200
|
|
|
1201
|
+
// src/stream/vercel-stream-encoder.ts
|
|
1202
|
+
function sse(data) {
|
|
1203
|
+
return `data: ${JSON.stringify(data)}
|
|
1204
|
+
|
|
1205
|
+
`;
|
|
1206
|
+
}
|
|
1207
|
+
function dataStreamLine(prefix, data) {
|
|
1208
|
+
return `${prefix}:${JSON.stringify(data)}
|
|
1209
|
+
`;
|
|
1210
|
+
}
|
|
1211
|
+
function encodeStreamPart(part) {
|
|
1212
|
+
switch (part.type) {
|
|
1213
|
+
case "text-delta":
|
|
1214
|
+
return sse({ type: "text-delta", id: "0", delta: part.text });
|
|
1215
|
+
case "tool-input-start":
|
|
1216
|
+
return sse({
|
|
1217
|
+
type: "tool-input-start",
|
|
1218
|
+
toolCallId: part.id,
|
|
1219
|
+
toolName: part.toolName
|
|
1220
|
+
});
|
|
1221
|
+
case "tool-input-delta":
|
|
1222
|
+
return sse({
|
|
1223
|
+
type: "tool-input-delta",
|
|
1224
|
+
toolCallId: part.id,
|
|
1225
|
+
inputTextDelta: part.delta
|
|
1226
|
+
});
|
|
1227
|
+
case "tool-call":
|
|
1228
|
+
return sse({
|
|
1229
|
+
type: "tool-input-available",
|
|
1230
|
+
toolCallId: part.toolCallId,
|
|
1231
|
+
toolName: part.toolName,
|
|
1232
|
+
input: part.input
|
|
1233
|
+
});
|
|
1234
|
+
case "tool-result":
|
|
1235
|
+
return sse({
|
|
1236
|
+
type: "tool-output-available",
|
|
1237
|
+
toolCallId: part.toolCallId,
|
|
1238
|
+
output: part.output
|
|
1239
|
+
});
|
|
1240
|
+
case "error":
|
|
1241
|
+
return sse({
|
|
1242
|
+
type: "error",
|
|
1243
|
+
errorText: String(part.error)
|
|
1244
|
+
});
|
|
1245
|
+
// Handle reasoning/thinking streams (DeepSeek R1, o1-style models)
|
|
1246
|
+
// Use 'g:' prefix for reasoning content per Vercel AI SDK protocol
|
|
1247
|
+
case "reasoning-start":
|
|
1248
|
+
return dataStreamLine("g", { text: "" });
|
|
1249
|
+
case "reasoning-delta":
|
|
1250
|
+
return dataStreamLine("g", { text: part.text });
|
|
1251
|
+
case "reasoning-end":
|
|
1252
|
+
return "";
|
|
1253
|
+
// No specific end marker needed for reasoning
|
|
1254
|
+
// finish-step and finish are handled by the generator, not here
|
|
1255
|
+
default:
|
|
1256
|
+
if (part.type?.startsWith("step-")) {
|
|
1257
|
+
return sse(part);
|
|
1258
|
+
}
|
|
1259
|
+
return "";
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async function* encodeVercelDataStream(events) {
|
|
1263
|
+
yield sse({ type: "start" });
|
|
1264
|
+
yield sse({ type: "start-step" });
|
|
1265
|
+
yield sse({ type: "text-start", id: "0" });
|
|
1266
|
+
let textOpen = true;
|
|
1267
|
+
let finishReason = "stop";
|
|
1268
|
+
for await (const part of events) {
|
|
1269
|
+
if (part.type === "finish") {
|
|
1270
|
+
finishReason = part.finishReason ?? "stop";
|
|
1271
|
+
}
|
|
1272
|
+
if (part.type === "finish-step" || part.type === "finish") {
|
|
1273
|
+
if (textOpen) {
|
|
1274
|
+
yield sse({ type: "text-end", id: "0" });
|
|
1275
|
+
textOpen = false;
|
|
1276
|
+
}
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const frame = encodeStreamPart(part);
|
|
1280
|
+
if (frame) {
|
|
1281
|
+
yield frame;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (textOpen) {
|
|
1285
|
+
yield sse({ type: "text-end", id: "0" });
|
|
1286
|
+
}
|
|
1287
|
+
yield sse({ type: "finish-step" });
|
|
1288
|
+
yield sse({ type: "finish", finishReason });
|
|
1289
|
+
yield "data: [DONE]\n\n";
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/routes/message-utils.ts
|
|
1293
|
+
function normalizeMessage(raw) {
|
|
1294
|
+
const role = raw.role;
|
|
1295
|
+
if (typeof raw.content === "string") {
|
|
1296
|
+
return { role, content: raw.content };
|
|
1297
|
+
}
|
|
1298
|
+
if (Array.isArray(raw.content)) {
|
|
1299
|
+
return { role, content: raw.content };
|
|
1300
|
+
}
|
|
1301
|
+
if (Array.isArray(raw.parts)) {
|
|
1302
|
+
const textParts = raw.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text);
|
|
1303
|
+
if (textParts.length > 0) {
|
|
1304
|
+
return { role, content: textParts.join("") };
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return { role, content: "" };
|
|
1308
|
+
}
|
|
1309
|
+
function validateMessageContent(msg, opts) {
|
|
1310
|
+
const content = msg.content;
|
|
1311
|
+
if (Array.isArray(msg.parts)) {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
if (typeof content === "string") {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
if (Array.isArray(content)) {
|
|
1318
|
+
for (const part of content) {
|
|
1319
|
+
if (typeof part !== "object" || part === null) {
|
|
1320
|
+
return "message.content array elements must be non-null objects";
|
|
1321
|
+
}
|
|
1322
|
+
const partObj = part;
|
|
1323
|
+
if (typeof partObj.type !== "string") {
|
|
1324
|
+
return 'each message.content array element must have a string "type" property';
|
|
1325
|
+
}
|
|
1326
|
+
if (partObj.type === "text" && typeof partObj.text !== "string") {
|
|
1327
|
+
return 'message.content elements with type "text" must have a string "text" property';
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
if ((content === null || content === void 0) && opts?.allowEmptyContent) {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
return "message.content must be a string, an array, or include parts";
|
|
1336
|
+
}
|
|
1337
|
+
|
|
400
1338
|
// src/routes/ai-routes.ts
|
|
401
1339
|
var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
402
1340
|
function validateMessage(raw) {
|
|
@@ -407,22 +1345,30 @@ function validateMessage(raw) {
|
|
|
407
1345
|
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
408
1346
|
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
409
1347
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
return null;
|
|
1348
|
+
const allowEmpty = msg.role === "assistant" || msg.role === "tool";
|
|
1349
|
+
return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
|
|
414
1350
|
}
|
|
415
1351
|
function buildAIRoutes(aiService, conversationService, logger) {
|
|
416
1352
|
return [
|
|
417
1353
|
// ── Chat ────────────────────────────────────────────────────
|
|
1354
|
+
//
|
|
1355
|
+
// Dual-mode endpoint compatible with both the legacy ObjectStack
|
|
1356
|
+
// format (`{ messages, options }`) and the Vercel AI SDK useChat
|
|
1357
|
+
// flat format (`{ messages, system, model, stream, … }`).
|
|
1358
|
+
//
|
|
1359
|
+
// Behaviour:
|
|
1360
|
+
// • `stream !== false` → Vercel Data Stream Protocol (SSE)
|
|
1361
|
+
// • `stream === false` → JSON response (legacy)
|
|
1362
|
+
//
|
|
418
1363
|
{
|
|
419
1364
|
method: "POST",
|
|
420
1365
|
path: "/api/v1/ai/chat",
|
|
421
|
-
description: "
|
|
1366
|
+
description: "Chat completion (supports Vercel AI Data Stream Protocol)",
|
|
422
1367
|
auth: true,
|
|
423
1368
|
permissions: ["ai:chat"],
|
|
424
1369
|
handler: async (req) => {
|
|
425
|
-
const
|
|
1370
|
+
const body = req.body ?? {};
|
|
1371
|
+
const messages = body.messages;
|
|
426
1372
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
427
1373
|
return { status: 400, body: { error: "messages array is required" } };
|
|
428
1374
|
}
|
|
@@ -430,8 +1376,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
430
1376
|
const err = validateMessage(msg);
|
|
431
1377
|
if (err) return { status: 400, body: { error: err } };
|
|
432
1378
|
}
|
|
1379
|
+
const nested = body.options ?? {};
|
|
1380
|
+
const resolvedOptions = {
|
|
1381
|
+
...nested,
|
|
1382
|
+
...body.model != null && { model: body.model },
|
|
1383
|
+
...body.temperature != null && { temperature: body.temperature },
|
|
1384
|
+
...body.maxTokens != null && { maxTokens: body.maxTokens }
|
|
1385
|
+
};
|
|
1386
|
+
const rawSystemPrompt = body.system ?? body.systemPrompt;
|
|
1387
|
+
if (rawSystemPrompt != null && typeof rawSystemPrompt !== "string") {
|
|
1388
|
+
return { status: 400, body: { error: "system/systemPrompt must be a string" } };
|
|
1389
|
+
}
|
|
1390
|
+
const systemPrompt = rawSystemPrompt;
|
|
1391
|
+
const finalMessages = [
|
|
1392
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
1393
|
+
...messages.map((m) => normalizeMessage(m))
|
|
1394
|
+
];
|
|
1395
|
+
const wantStream = body.stream !== false;
|
|
1396
|
+
if (wantStream) {
|
|
1397
|
+
try {
|
|
1398
|
+
if (!aiService.streamChatWithTools) {
|
|
1399
|
+
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
1400
|
+
}
|
|
1401
|
+
const events = aiService.streamChatWithTools(finalMessages, resolvedOptions);
|
|
1402
|
+
return {
|
|
1403
|
+
status: 200,
|
|
1404
|
+
stream: true,
|
|
1405
|
+
vercelDataStream: true,
|
|
1406
|
+
contentType: "text/event-stream",
|
|
1407
|
+
headers: {
|
|
1408
|
+
"Content-Type": "text/event-stream",
|
|
1409
|
+
"Cache-Control": "no-cache",
|
|
1410
|
+
"Connection": "keep-alive",
|
|
1411
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
1412
|
+
},
|
|
1413
|
+
events: encodeVercelDataStream(events)
|
|
1414
|
+
};
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
logger.error("[AI Route] /chat stream error", err instanceof Error ? err : void 0);
|
|
1417
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
433
1420
|
try {
|
|
434
|
-
const result = await aiService.
|
|
1421
|
+
const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
|
|
435
1422
|
return { status: 200, body: result };
|
|
436
1423
|
} catch (err) {
|
|
437
1424
|
logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
|
|
@@ -459,7 +1446,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
459
1446
|
if (!aiService.streamChat) {
|
|
460
1447
|
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
461
1448
|
}
|
|
462
|
-
const events = aiService.streamChat(messages, options);
|
|
1449
|
+
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
463
1450
|
return { status: 200, stream: true, events };
|
|
464
1451
|
} catch (err) {
|
|
465
1452
|
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
@@ -637,17 +1624,41 @@ function validateAgentMessage(raw) {
|
|
|
637
1624
|
if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
|
|
638
1625
|
return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
|
|
639
1626
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
return null;
|
|
1627
|
+
const allowEmpty = msg.role === "assistant";
|
|
1628
|
+
return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
|
|
644
1629
|
}
|
|
645
1630
|
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
646
1631
|
return [
|
|
1632
|
+
// ── List active agents ──────────────────────────────────────
|
|
1633
|
+
{
|
|
1634
|
+
method: "GET",
|
|
1635
|
+
path: "/api/v1/ai/agents",
|
|
1636
|
+
description: "List all active AI agents",
|
|
1637
|
+
auth: true,
|
|
1638
|
+
permissions: ["ai:chat"],
|
|
1639
|
+
handler: async () => {
|
|
1640
|
+
try {
|
|
1641
|
+
const agents = await agentRuntime.listAgents();
|
|
1642
|
+
return { status: 200, body: { agents } };
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
logger.error(
|
|
1645
|
+
"[AI Route] /agents list error",
|
|
1646
|
+
err instanceof Error ? err : void 0
|
|
1647
|
+
);
|
|
1648
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
// ── Chat with a specific agent ──────────────────────────────
|
|
1653
|
+
//
|
|
1654
|
+
// Dual-mode endpoint matching the general chat route behaviour:
|
|
1655
|
+
// • `stream !== false` → Vercel Data Stream Protocol (SSE)
|
|
1656
|
+
// • `stream === false` → JSON response (legacy)
|
|
1657
|
+
//
|
|
647
1658
|
{
|
|
648
1659
|
method: "POST",
|
|
649
1660
|
path: "/api/v1/ai/agents/:agentName/chat",
|
|
650
|
-
description: "Chat with a specific AI agent",
|
|
1661
|
+
description: "Chat with a specific AI agent (supports Vercel AI Data Stream Protocol)",
|
|
651
1662
|
auth: true,
|
|
652
1663
|
permissions: ["ai:chat", "ai:agents"],
|
|
653
1664
|
handler: async (req) => {
|
|
@@ -655,11 +1666,12 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
|
655
1666
|
if (!agentName) {
|
|
656
1667
|
return { status: 400, body: { error: "agentName parameter is required" } };
|
|
657
1668
|
}
|
|
1669
|
+
const body = req.body ?? {};
|
|
658
1670
|
const {
|
|
659
1671
|
messages: rawMessages,
|
|
660
1672
|
context: chatContext,
|
|
661
1673
|
options: extraOptions
|
|
662
|
-
} =
|
|
1674
|
+
} = body;
|
|
663
1675
|
if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
|
|
664
1676
|
return { status: 400, body: { error: "messages array is required" } };
|
|
665
1677
|
}
|
|
@@ -687,21 +1699,150 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
|
687
1699
|
if (ALLOWED_KEYS.has(key)) {
|
|
688
1700
|
safeOverrides[key] = extraOptions[key];
|
|
689
1701
|
}
|
|
690
|
-
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
const mergedOptions = { ...agentOptions, ...safeOverrides };
|
|
1705
|
+
const fullMessages = [
|
|
1706
|
+
...systemMessages,
|
|
1707
|
+
...rawMessages.map((m) => normalizeMessage(m))
|
|
1708
|
+
];
|
|
1709
|
+
const chatWithToolsOptions = {
|
|
1710
|
+
...mergedOptions,
|
|
1711
|
+
maxIterations: agent.planning?.maxIterations
|
|
1712
|
+
};
|
|
1713
|
+
const wantStream = body.stream !== false;
|
|
1714
|
+
if (wantStream) {
|
|
1715
|
+
if (!aiService.streamChatWithTools) {
|
|
1716
|
+
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
1717
|
+
}
|
|
1718
|
+
const events = aiService.streamChatWithTools(fullMessages, chatWithToolsOptions);
|
|
1719
|
+
return {
|
|
1720
|
+
status: 200,
|
|
1721
|
+
stream: true,
|
|
1722
|
+
vercelDataStream: true,
|
|
1723
|
+
contentType: "text/event-stream",
|
|
1724
|
+
headers: {
|
|
1725
|
+
"Content-Type": "text/event-stream",
|
|
1726
|
+
"Cache-Control": "no-cache",
|
|
1727
|
+
"Connection": "keep-alive",
|
|
1728
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
1729
|
+
},
|
|
1730
|
+
events: encodeVercelDataStream(events)
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
const result = await aiService.chatWithTools(fullMessages, chatWithToolsOptions);
|
|
1734
|
+
return { status: 200, body: result };
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
logger.error(
|
|
1737
|
+
"[AI Route] /agents/:agentName/chat error",
|
|
1738
|
+
err instanceof Error ? err : void 0
|
|
1739
|
+
);
|
|
1740
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
];
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/routes/tool-routes.ts
|
|
1748
|
+
function extractOutputValue(output) {
|
|
1749
|
+
if (!output) return "";
|
|
1750
|
+
if (typeof output === "string") return output;
|
|
1751
|
+
if (typeof output === "object" && "value" in output) {
|
|
1752
|
+
return String(output.value ?? "");
|
|
1753
|
+
}
|
|
1754
|
+
return JSON.stringify(output);
|
|
1755
|
+
}
|
|
1756
|
+
function buildToolRoutes(aiService, logger) {
|
|
1757
|
+
return [
|
|
1758
|
+
// ── List registered tools ──────────────────────────────────────
|
|
1759
|
+
{
|
|
1760
|
+
method: "GET",
|
|
1761
|
+
path: "/api/v1/ai/tools",
|
|
1762
|
+
description: "List all registered AI tools",
|
|
1763
|
+
auth: true,
|
|
1764
|
+
permissions: ["ai:tools"],
|
|
1765
|
+
handler: async () => {
|
|
1766
|
+
try {
|
|
1767
|
+
const tools = aiService.toolRegistry.getAll();
|
|
1768
|
+
return {
|
|
1769
|
+
status: 200,
|
|
1770
|
+
body: {
|
|
1771
|
+
tools: tools.map((t) => ({
|
|
1772
|
+
name: t.name,
|
|
1773
|
+
description: t.description,
|
|
1774
|
+
category: t.category
|
|
1775
|
+
}))
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
logger.error(
|
|
1780
|
+
"[AI Route] /tools list error",
|
|
1781
|
+
err instanceof Error ? err : void 0
|
|
1782
|
+
);
|
|
1783
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
// ── Execute a tool ──────────────────────────────────────────────
|
|
1788
|
+
//
|
|
1789
|
+
// Executes a tool with the provided parameters.
|
|
1790
|
+
// This is intended for testing/playground use.
|
|
1791
|
+
//
|
|
1792
|
+
{
|
|
1793
|
+
method: "POST",
|
|
1794
|
+
path: "/api/v1/ai/tools/:toolName/execute",
|
|
1795
|
+
description: "Execute a tool with parameters (playground/testing)",
|
|
1796
|
+
auth: true,
|
|
1797
|
+
permissions: ["ai:tools", "ai:execute"],
|
|
1798
|
+
handler: async (req) => {
|
|
1799
|
+
const toolName = req.params?.toolName;
|
|
1800
|
+
if (!toolName) {
|
|
1801
|
+
return { status: 400, body: { error: "toolName parameter is required" } };
|
|
1802
|
+
}
|
|
1803
|
+
const body = req.body ?? {};
|
|
1804
|
+
const { parameters } = body;
|
|
1805
|
+
if (!parameters || typeof parameters !== "object") {
|
|
1806
|
+
return { status: 400, body: { error: "parameters object is required" } };
|
|
1807
|
+
}
|
|
1808
|
+
try {
|
|
1809
|
+
if (!aiService.toolRegistry.has(toolName)) {
|
|
1810
|
+
return { status: 404, body: { error: `Tool "${toolName}" not found` } };
|
|
1811
|
+
}
|
|
1812
|
+
const startTime = Date.now();
|
|
1813
|
+
const toolCallPart = {
|
|
1814
|
+
type: "tool-call",
|
|
1815
|
+
toolCallId: `playground-${Date.now()}`,
|
|
1816
|
+
toolName,
|
|
1817
|
+
input: parameters
|
|
1818
|
+
};
|
|
1819
|
+
const result = await aiService.toolRegistry.execute(toolCallPart);
|
|
1820
|
+
const duration = Date.now() - startTime;
|
|
1821
|
+
if (result.isError) {
|
|
1822
|
+
const errorMsg = extractOutputValue(result.output);
|
|
1823
|
+
logger.error(
|
|
1824
|
+
`[AI Route] Tool execution error: ${toolName}`,
|
|
1825
|
+
new Error(errorMsg)
|
|
1826
|
+
);
|
|
1827
|
+
return {
|
|
1828
|
+
status: 500,
|
|
1829
|
+
body: {
|
|
1830
|
+
error: errorMsg,
|
|
1831
|
+
duration
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
691
1834
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
});
|
|
701
|
-
return { status: 200, body: result };
|
|
1835
|
+
return {
|
|
1836
|
+
status: 200,
|
|
1837
|
+
body: {
|
|
1838
|
+
result: extractOutputValue(result.output),
|
|
1839
|
+
duration,
|
|
1840
|
+
toolName
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
702
1843
|
} catch (err) {
|
|
703
1844
|
logger.error(
|
|
704
|
-
"[AI Route] /
|
|
1845
|
+
"[AI Route] /tools/:toolName/execute error",
|
|
705
1846
|
err instanceof Error ? err : void 0
|
|
706
1847
|
);
|
|
707
1848
|
return { status: 500, body: { error: "Internal AI service error" } };
|
|
@@ -803,13 +1944,35 @@ var ObjectQLConversationService = class {
|
|
|
803
1944
|
}
|
|
804
1945
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
805
1946
|
const msgId = `msg_${randomUUID()}`;
|
|
1947
|
+
let contentStr;
|
|
1948
|
+
let toolCallsJson = null;
|
|
1949
|
+
let toolCallId = null;
|
|
1950
|
+
if (message.role === "system" || message.role === "user") {
|
|
1951
|
+
contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
1952
|
+
} else if (message.role === "assistant") {
|
|
1953
|
+
if (typeof message.content === "string") {
|
|
1954
|
+
contentStr = message.content;
|
|
1955
|
+
} else {
|
|
1956
|
+
const parts = message.content;
|
|
1957
|
+
const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
|
|
1958
|
+
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
|
1959
|
+
contentStr = textParts.join("");
|
|
1960
|
+
if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
|
|
1961
|
+
}
|
|
1962
|
+
} else if (message.role === "tool") {
|
|
1963
|
+
contentStr = JSON.stringify(message.content);
|
|
1964
|
+
const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
|
|
1965
|
+
if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
|
|
1966
|
+
} else {
|
|
1967
|
+
contentStr = "";
|
|
1968
|
+
}
|
|
806
1969
|
await this.engine.insert(MESSAGES_OBJECT, {
|
|
807
1970
|
id: msgId,
|
|
808
1971
|
conversation_id: conversationId,
|
|
809
1972
|
role: message.role,
|
|
810
|
-
content:
|
|
811
|
-
tool_calls:
|
|
812
|
-
tool_call_id:
|
|
1973
|
+
content: contentStr,
|
|
1974
|
+
tool_calls: toolCallsJson,
|
|
1975
|
+
tool_call_id: toolCallId,
|
|
813
1976
|
created_at: now
|
|
814
1977
|
});
|
|
815
1978
|
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
@@ -854,21 +2017,42 @@ var ObjectQLConversationService = class {
|
|
|
854
2017
|
};
|
|
855
2018
|
}
|
|
856
2019
|
/**
|
|
857
|
-
* Map a database row to
|
|
2020
|
+
* Map a database row to a ModelMessage.
|
|
858
2021
|
*/
|
|
859
2022
|
toMessage(row) {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
2023
|
+
switch (row.role) {
|
|
2024
|
+
case "system":
|
|
2025
|
+
return { role: "system", content: row.content };
|
|
2026
|
+
case "user":
|
|
2027
|
+
return { role: "user", content: row.content };
|
|
2028
|
+
case "assistant": {
|
|
2029
|
+
const toolCalls = this.safeParse(row.tool_calls);
|
|
2030
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
2031
|
+
const content = [];
|
|
2032
|
+
if (row.content) content.push({ type: "text", text: row.content });
|
|
2033
|
+
content.push(...toolCalls);
|
|
2034
|
+
return { role: "assistant", content };
|
|
2035
|
+
}
|
|
2036
|
+
return { role: "assistant", content: row.content };
|
|
2037
|
+
}
|
|
2038
|
+
case "tool": {
|
|
2039
|
+
const toolResults = this.safeParse(row.content);
|
|
2040
|
+
if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
|
|
2041
|
+
return { role: "tool", content: toolResults };
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
role: "tool",
|
|
2045
|
+
content: [{
|
|
2046
|
+
type: "tool-result",
|
|
2047
|
+
toolCallId: row.tool_call_id ?? "",
|
|
2048
|
+
toolName: "unknown",
|
|
2049
|
+
output: { type: "text", value: row.content }
|
|
2050
|
+
}]
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
default:
|
|
2054
|
+
return { role: "user", content: row.content };
|
|
870
2055
|
}
|
|
871
|
-
return msg;
|
|
872
2056
|
}
|
|
873
2057
|
};
|
|
874
2058
|
|
|
@@ -1007,263 +2191,9 @@ var AiMessageObject = ObjectSchema2.create({
|
|
|
1007
2191
|
}
|
|
1008
2192
|
});
|
|
1009
2193
|
|
|
1010
|
-
// src/
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
var LIST_OBJECTS_TOOL = {
|
|
1014
|
-
name: "list_objects",
|
|
1015
|
-
description: "List all available data objects (tables) in the system. Returns object names and labels.",
|
|
1016
|
-
parameters: {
|
|
1017
|
-
type: "object",
|
|
1018
|
-
properties: {},
|
|
1019
|
-
additionalProperties: false
|
|
1020
|
-
}
|
|
1021
|
-
};
|
|
1022
|
-
var DESCRIBE_OBJECT_TOOL = {
|
|
1023
|
-
name: "describe_object",
|
|
1024
|
-
description: "Get the schema (fields, types, labels) of a specific data object. Use this to understand the structure of a table before querying it.",
|
|
1025
|
-
parameters: {
|
|
1026
|
-
type: "object",
|
|
1027
|
-
properties: {
|
|
1028
|
-
objectName: {
|
|
1029
|
-
type: "string",
|
|
1030
|
-
description: "The snake_case name of the object to describe"
|
|
1031
|
-
}
|
|
1032
|
-
},
|
|
1033
|
-
required: ["objectName"],
|
|
1034
|
-
additionalProperties: false
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
var QUERY_RECORDS_TOOL = {
|
|
1038
|
-
name: "query_records",
|
|
1039
|
-
description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
|
|
1040
|
-
parameters: {
|
|
1041
|
-
type: "object",
|
|
1042
|
-
properties: {
|
|
1043
|
-
objectName: {
|
|
1044
|
-
type: "string",
|
|
1045
|
-
description: "The snake_case name of the object to query"
|
|
1046
|
-
},
|
|
1047
|
-
where: {
|
|
1048
|
-
type: "object",
|
|
1049
|
-
description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
|
|
1050
|
-
},
|
|
1051
|
-
fields: {
|
|
1052
|
-
type: "array",
|
|
1053
|
-
items: { type: "string" },
|
|
1054
|
-
description: "List of field names to return (omit for all fields)"
|
|
1055
|
-
},
|
|
1056
|
-
orderBy: {
|
|
1057
|
-
type: "array",
|
|
1058
|
-
items: {
|
|
1059
|
-
type: "object",
|
|
1060
|
-
properties: {
|
|
1061
|
-
field: { type: "string" },
|
|
1062
|
-
order: { type: "string", enum: ["asc", "desc"] }
|
|
1063
|
-
}
|
|
1064
|
-
},
|
|
1065
|
-
description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
|
|
1066
|
-
},
|
|
1067
|
-
limit: {
|
|
1068
|
-
type: "number",
|
|
1069
|
-
description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
|
|
1070
|
-
},
|
|
1071
|
-
offset: {
|
|
1072
|
-
type: "number",
|
|
1073
|
-
description: "Number of records to skip for pagination"
|
|
1074
|
-
}
|
|
1075
|
-
},
|
|
1076
|
-
required: ["objectName"],
|
|
1077
|
-
additionalProperties: false
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
var GET_RECORD_TOOL = {
|
|
1081
|
-
name: "get_record",
|
|
1082
|
-
description: "Get a single record by its ID from a data object.",
|
|
1083
|
-
parameters: {
|
|
1084
|
-
type: "object",
|
|
1085
|
-
properties: {
|
|
1086
|
-
objectName: {
|
|
1087
|
-
type: "string",
|
|
1088
|
-
description: "The snake_case name of the object"
|
|
1089
|
-
},
|
|
1090
|
-
recordId: {
|
|
1091
|
-
type: "string",
|
|
1092
|
-
description: "The unique ID of the record"
|
|
1093
|
-
},
|
|
1094
|
-
fields: {
|
|
1095
|
-
type: "array",
|
|
1096
|
-
items: { type: "string" },
|
|
1097
|
-
description: "List of field names to return (omit for all fields)"
|
|
1098
|
-
}
|
|
1099
|
-
},
|
|
1100
|
-
required: ["objectName", "recordId"],
|
|
1101
|
-
additionalProperties: false
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
var AGGREGATE_DATA_TOOL = {
|
|
1105
|
-
name: "aggregate_data",
|
|
1106
|
-
description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
|
|
1107
|
-
parameters: {
|
|
1108
|
-
type: "object",
|
|
1109
|
-
properties: {
|
|
1110
|
-
objectName: {
|
|
1111
|
-
type: "string",
|
|
1112
|
-
description: "The snake_case name of the object to aggregate"
|
|
1113
|
-
},
|
|
1114
|
-
aggregations: {
|
|
1115
|
-
type: "array",
|
|
1116
|
-
items: {
|
|
1117
|
-
type: "object",
|
|
1118
|
-
properties: {
|
|
1119
|
-
function: {
|
|
1120
|
-
type: "string",
|
|
1121
|
-
enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
|
|
1122
|
-
description: "Aggregation function"
|
|
1123
|
-
},
|
|
1124
|
-
field: {
|
|
1125
|
-
type: "string",
|
|
1126
|
-
description: "Field to aggregate (optional for count)"
|
|
1127
|
-
},
|
|
1128
|
-
alias: {
|
|
1129
|
-
type: "string",
|
|
1130
|
-
description: "Result column alias"
|
|
1131
|
-
}
|
|
1132
|
-
},
|
|
1133
|
-
required: ["function", "alias"]
|
|
1134
|
-
},
|
|
1135
|
-
description: "Aggregation definitions"
|
|
1136
|
-
},
|
|
1137
|
-
groupBy: {
|
|
1138
|
-
type: "array",
|
|
1139
|
-
items: { type: "string" },
|
|
1140
|
-
description: "Fields to group by"
|
|
1141
|
-
},
|
|
1142
|
-
where: {
|
|
1143
|
-
type: "object",
|
|
1144
|
-
description: "Filter conditions applied before aggregation"
|
|
1145
|
-
}
|
|
1146
|
-
},
|
|
1147
|
-
required: ["objectName", "aggregations"],
|
|
1148
|
-
additionalProperties: false
|
|
1149
|
-
}
|
|
1150
|
-
};
|
|
1151
|
-
var DATA_TOOL_DEFINITIONS = [
|
|
1152
|
-
LIST_OBJECTS_TOOL,
|
|
1153
|
-
DESCRIBE_OBJECT_TOOL,
|
|
1154
|
-
QUERY_RECORDS_TOOL,
|
|
1155
|
-
GET_RECORD_TOOL,
|
|
1156
|
-
AGGREGATE_DATA_TOOL
|
|
1157
|
-
];
|
|
1158
|
-
function createListObjectsHandler(ctx) {
|
|
1159
|
-
return async () => {
|
|
1160
|
-
const objects = await ctx.metadataService.listObjects();
|
|
1161
|
-
const summary = objects.map((o) => ({
|
|
1162
|
-
name: o.name,
|
|
1163
|
-
label: o.label ?? o.name
|
|
1164
|
-
}));
|
|
1165
|
-
return JSON.stringify(summary);
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
function createDescribeObjectHandler(ctx) {
|
|
1169
|
-
return async (args) => {
|
|
1170
|
-
const { objectName } = args;
|
|
1171
|
-
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1172
|
-
if (!objectDef) {
|
|
1173
|
-
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1174
|
-
}
|
|
1175
|
-
const def = objectDef;
|
|
1176
|
-
const fields = def.fields ?? {};
|
|
1177
|
-
const fieldSummary = {};
|
|
1178
|
-
for (const [key, f] of Object.entries(fields)) {
|
|
1179
|
-
fieldSummary[key] = {
|
|
1180
|
-
type: f.type,
|
|
1181
|
-
label: f.label ?? key,
|
|
1182
|
-
required: f.required ?? false,
|
|
1183
|
-
...f.reference ? { reference: f.reference } : {},
|
|
1184
|
-
...f.options ? { options: f.options } : {}
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
return JSON.stringify({
|
|
1188
|
-
name: def.name,
|
|
1189
|
-
label: def.label ?? def.name,
|
|
1190
|
-
fields: fieldSummary
|
|
1191
|
-
});
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
function createQueryRecordsHandler(ctx) {
|
|
1195
|
-
return async (args) => {
|
|
1196
|
-
const {
|
|
1197
|
-
objectName,
|
|
1198
|
-
where,
|
|
1199
|
-
fields,
|
|
1200
|
-
orderBy,
|
|
1201
|
-
limit,
|
|
1202
|
-
offset
|
|
1203
|
-
} = args;
|
|
1204
|
-
const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
|
|
1205
|
-
const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
|
|
1206
|
-
const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
|
|
1207
|
-
const records = await ctx.dataEngine.find(objectName, {
|
|
1208
|
-
where,
|
|
1209
|
-
fields,
|
|
1210
|
-
orderBy,
|
|
1211
|
-
limit: safeLimit,
|
|
1212
|
-
offset: safeOffset
|
|
1213
|
-
});
|
|
1214
|
-
return JSON.stringify({ count: records.length, records });
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
function createGetRecordHandler(ctx) {
|
|
1218
|
-
return async (args) => {
|
|
1219
|
-
const { objectName, recordId, fields } = args;
|
|
1220
|
-
const record = await ctx.dataEngine.findOne(objectName, {
|
|
1221
|
-
where: { id: recordId },
|
|
1222
|
-
fields
|
|
1223
|
-
});
|
|
1224
|
-
if (!record) {
|
|
1225
|
-
return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
|
|
1226
|
-
}
|
|
1227
|
-
return JSON.stringify(record);
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
var VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1231
|
-
"count",
|
|
1232
|
-
"sum",
|
|
1233
|
-
"avg",
|
|
1234
|
-
"min",
|
|
1235
|
-
"max",
|
|
1236
|
-
"count_distinct"
|
|
1237
|
-
]);
|
|
1238
|
-
function createAggregateDataHandler(ctx) {
|
|
1239
|
-
return async (args) => {
|
|
1240
|
-
const { objectName, aggregations, groupBy, where } = args;
|
|
1241
|
-
for (const a of aggregations) {
|
|
1242
|
-
if (!VALID_AGG_FUNCTIONS.has(a.function)) {
|
|
1243
|
-
return JSON.stringify({
|
|
1244
|
-
error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
const result = await ctx.dataEngine.aggregate(objectName, {
|
|
1249
|
-
where,
|
|
1250
|
-
groupBy,
|
|
1251
|
-
aggregations: aggregations.map((a) => ({
|
|
1252
|
-
function: a.function,
|
|
1253
|
-
field: a.field,
|
|
1254
|
-
alias: a.alias
|
|
1255
|
-
}))
|
|
1256
|
-
});
|
|
1257
|
-
return JSON.stringify(result);
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1260
|
-
function registerDataTools(registry, context) {
|
|
1261
|
-
registry.register(LIST_OBJECTS_TOOL, createListObjectsHandler(context));
|
|
1262
|
-
registry.register(DESCRIBE_OBJECT_TOOL, createDescribeObjectHandler(context));
|
|
1263
|
-
registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
|
|
1264
|
-
registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
|
|
1265
|
-
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
1266
|
-
}
|
|
2194
|
+
// src/plugin.ts
|
|
2195
|
+
init_data_tools();
|
|
2196
|
+
init_metadata_tools();
|
|
1267
2197
|
|
|
1268
2198
|
// src/agent-runtime.ts
|
|
1269
2199
|
import { AgentSchema } from "@objectstack/spec/ai";
|
|
@@ -1272,6 +2202,27 @@ var AgentRuntime = class {
|
|
|
1272
2202
|
this.metadataService = metadataService;
|
|
1273
2203
|
}
|
|
1274
2204
|
// ── Public API ────────────────────────────────────────────────
|
|
2205
|
+
/**
|
|
2206
|
+
* List all active agents registered in the metadata service.
|
|
2207
|
+
*
|
|
2208
|
+
* Returns a summary for each agent (name, label, role) suitable
|
|
2209
|
+
* for populating an agent selector dropdown in the UI.
|
|
2210
|
+
*/
|
|
2211
|
+
async listAgents() {
|
|
2212
|
+
const rawItems = await this.metadataService.list("agent");
|
|
2213
|
+
const agents = [];
|
|
2214
|
+
for (const raw of rawItems) {
|
|
2215
|
+
const result = AgentSchema.safeParse(raw);
|
|
2216
|
+
if (result.success && result.data.active) {
|
|
2217
|
+
agents.push({
|
|
2218
|
+
name: result.data.name,
|
|
2219
|
+
label: result.data.label,
|
|
2220
|
+
role: result.data.role
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
return agents;
|
|
2225
|
+
}
|
|
1275
2226
|
/**
|
|
1276
2227
|
* Load and validate an agent definition by name.
|
|
1277
2228
|
*
|
|
@@ -1400,15 +2351,233 @@ Guidelines:
|
|
|
1400
2351
|
}
|
|
1401
2352
|
};
|
|
1402
2353
|
|
|
2354
|
+
// src/agents/metadata-assistant-agent.ts
|
|
2355
|
+
var METADATA_ASSISTANT_AGENT = {
|
|
2356
|
+
name: "metadata_assistant",
|
|
2357
|
+
label: "Metadata Assistant",
|
|
2358
|
+
role: "Schema Architect",
|
|
2359
|
+
instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
|
|
2360
|
+
|
|
2361
|
+
Capabilities:
|
|
2362
|
+
- Create new data objects (tables) with fields
|
|
2363
|
+
- Add fields (columns) to existing objects
|
|
2364
|
+
- Modify field properties (label, type, required, default value)
|
|
2365
|
+
- Delete fields from objects
|
|
2366
|
+
- List all registered metadata objects and their schemas
|
|
2367
|
+
- Describe the full schema of a specific object
|
|
2368
|
+
|
|
2369
|
+
Guidelines:
|
|
2370
|
+
1. Before creating a new object, use list_objects to check if a similar one already exists.
|
|
2371
|
+
2. Before modifying or deleting fields, use describe_object to understand the current schema.
|
|
2372
|
+
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
|
|
2373
|
+
4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
|
|
2374
|
+
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
|
|
2375
|
+
6. Explain what changes you are about to make before executing them.
|
|
2376
|
+
7. After making changes, confirm the result by describing the updated schema.
|
|
2377
|
+
8. For destructive operations (deleting fields), always warn the user about potential data loss.
|
|
2378
|
+
9. Always answer in the same language the user is using.
|
|
2379
|
+
10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
|
|
2380
|
+
model: {
|
|
2381
|
+
provider: "openai",
|
|
2382
|
+
model: "gpt-4",
|
|
2383
|
+
temperature: 0.2,
|
|
2384
|
+
maxTokens: 4096
|
|
2385
|
+
},
|
|
2386
|
+
tools: [
|
|
2387
|
+
{ type: "action", name: "create_object", description: "Create a new data object (table)" },
|
|
2388
|
+
{ type: "action", name: "add_field", description: "Add a field to an existing object" },
|
|
2389
|
+
{ type: "action", name: "modify_field", description: "Modify an existing field definition" },
|
|
2390
|
+
{ type: "action", name: "delete_field", description: "Delete a field from an object" },
|
|
2391
|
+
{ type: "query", name: "list_objects", description: "List all data objects" },
|
|
2392
|
+
{ type: "query", name: "describe_object", description: "Describe an object schema" }
|
|
2393
|
+
],
|
|
2394
|
+
active: true,
|
|
2395
|
+
visibility: "global",
|
|
2396
|
+
guardrails: {
|
|
2397
|
+
maxTokensPerInvocation: 8192,
|
|
2398
|
+
maxExecutionTimeSec: 60,
|
|
2399
|
+
blockedTopics: ["drop_database", "raw_sql", "system_tables"]
|
|
2400
|
+
},
|
|
2401
|
+
planning: {
|
|
2402
|
+
strategy: "react",
|
|
2403
|
+
maxIterations: 10,
|
|
2404
|
+
allowReplan: true
|
|
2405
|
+
},
|
|
2406
|
+
memory: {
|
|
2407
|
+
shortTerm: {
|
|
2408
|
+
maxMessages: 30,
|
|
2409
|
+
maxTokens: 8192
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
// src/adapters/vercel-adapter.ts
|
|
2415
|
+
import { generateText, streamText, tool as vercelTool, jsonSchema } from "ai";
|
|
2416
|
+
function buildVercelOptions(options) {
|
|
2417
|
+
if (!options) return {};
|
|
2418
|
+
const opts = {};
|
|
2419
|
+
if (options.temperature != null) opts.temperature = options.temperature;
|
|
2420
|
+
if (options.maxTokens != null) opts.maxTokens = options.maxTokens;
|
|
2421
|
+
if (options.stop?.length) opts.stopSequences = options.stop;
|
|
2422
|
+
if (options.tools?.length) {
|
|
2423
|
+
const tools = {};
|
|
2424
|
+
for (const t of options.tools) {
|
|
2425
|
+
tools[t.name] = vercelTool({
|
|
2426
|
+
description: t.description,
|
|
2427
|
+
inputSchema: jsonSchema(t.parameters)
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
opts.tools = tools;
|
|
2431
|
+
}
|
|
2432
|
+
if (options.toolChoice != null) {
|
|
2433
|
+
opts.toolChoice = options.toolChoice;
|
|
2434
|
+
}
|
|
2435
|
+
return opts;
|
|
2436
|
+
}
|
|
2437
|
+
var VercelLLMAdapter = class {
|
|
2438
|
+
constructor(config) {
|
|
2439
|
+
this.name = "vercel";
|
|
2440
|
+
this.model = config.model;
|
|
2441
|
+
}
|
|
2442
|
+
async chat(messages, options) {
|
|
2443
|
+
const result = await generateText({
|
|
2444
|
+
model: this.model,
|
|
2445
|
+
messages,
|
|
2446
|
+
...buildVercelOptions(options)
|
|
2447
|
+
});
|
|
2448
|
+
return {
|
|
2449
|
+
content: result.text,
|
|
2450
|
+
model: result.response?.modelId,
|
|
2451
|
+
toolCalls: result.toolCalls?.length ? result.toolCalls : void 0,
|
|
2452
|
+
usage: result.usage ? {
|
|
2453
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2454
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2455
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2456
|
+
} : void 0
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
async complete(prompt, options) {
|
|
2460
|
+
const result = await generateText({
|
|
2461
|
+
model: this.model,
|
|
2462
|
+
prompt,
|
|
2463
|
+
...buildVercelOptions(options)
|
|
2464
|
+
});
|
|
2465
|
+
return {
|
|
2466
|
+
content: result.text,
|
|
2467
|
+
model: result.response?.modelId,
|
|
2468
|
+
usage: result.usage ? {
|
|
2469
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2470
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2471
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2472
|
+
} : void 0
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
async *streamChat(messages, options) {
|
|
2476
|
+
const result = streamText({
|
|
2477
|
+
model: this.model,
|
|
2478
|
+
messages,
|
|
2479
|
+
...buildVercelOptions(options)
|
|
2480
|
+
});
|
|
2481
|
+
for await (const part of result.fullStream) {
|
|
2482
|
+
yield part;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
async embed(_input) {
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
"[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
async listModels() {
|
|
2491
|
+
return [];
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
|
|
1403
2495
|
// src/plugin.ts
|
|
1404
2496
|
var AIServicePlugin = class {
|
|
1405
2497
|
constructor(options = {}) {
|
|
1406
2498
|
this.name = "com.objectstack.service-ai";
|
|
1407
2499
|
this.version = "1.0.0";
|
|
1408
2500
|
this.type = "standard";
|
|
1409
|
-
this.dependencies = [];
|
|
2501
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
1410
2502
|
this.options = options;
|
|
1411
2503
|
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Auto-detect LLM provider from environment variables.
|
|
2506
|
+
*
|
|
2507
|
+
* Priority order:
|
|
2508
|
+
* 1. AI_GATEWAY_MODEL → Vercel AI Gateway
|
|
2509
|
+
* 2. OPENAI_API_KEY → OpenAI
|
|
2510
|
+
* 3. ANTHROPIC_API_KEY → Anthropic
|
|
2511
|
+
* 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
|
|
2512
|
+
* 5. Fallback → MemoryLLMAdapter
|
|
2513
|
+
*
|
|
2514
|
+
* Returns the adapter and a description for logging.
|
|
2515
|
+
*/
|
|
2516
|
+
async detectAdapter(ctx) {
|
|
2517
|
+
const gatewayModel = process.env.AI_GATEWAY_MODEL;
|
|
2518
|
+
if (gatewayModel) {
|
|
2519
|
+
try {
|
|
2520
|
+
const gatewayPkg = "@ai-sdk/gateway";
|
|
2521
|
+
const { gateway } = await import(
|
|
2522
|
+
/* webpackIgnore: true */
|
|
2523
|
+
gatewayPkg
|
|
2524
|
+
);
|
|
2525
|
+
const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
|
|
2526
|
+
return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
|
|
2527
|
+
} catch (err) {
|
|
2528
|
+
ctx.logger.warn(
|
|
2529
|
+
`[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
|
|
2530
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const providerConfigs = [
|
|
2535
|
+
{
|
|
2536
|
+
envKey: "OPENAI_API_KEY",
|
|
2537
|
+
pkg: "@ai-sdk/openai",
|
|
2538
|
+
factory: "openai",
|
|
2539
|
+
defaultModel: "gpt-4o",
|
|
2540
|
+
displayName: "OpenAI"
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
2544
|
+
pkg: "@ai-sdk/anthropic",
|
|
2545
|
+
factory: "anthropic",
|
|
2546
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
2547
|
+
displayName: "Anthropic"
|
|
2548
|
+
},
|
|
2549
|
+
{
|
|
2550
|
+
envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
2551
|
+
pkg: "@ai-sdk/google",
|
|
2552
|
+
factory: "google",
|
|
2553
|
+
defaultModel: "gemini-2.0-flash",
|
|
2554
|
+
displayName: "Google"
|
|
2555
|
+
}
|
|
2556
|
+
];
|
|
2557
|
+
for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
|
|
2558
|
+
if (process.env[envKey]) {
|
|
2559
|
+
try {
|
|
2560
|
+
const mod = await import(
|
|
2561
|
+
/* webpackIgnore: true */
|
|
2562
|
+
pkg
|
|
2563
|
+
);
|
|
2564
|
+
const createModel = mod[factory] ?? mod.default;
|
|
2565
|
+
if (typeof createModel === "function") {
|
|
2566
|
+
const modelId = process.env.AI_MODEL ?? defaultModel;
|
|
2567
|
+
const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
|
|
2568
|
+
return { adapter, description: `${displayName} (model: ${modelId})` };
|
|
2569
|
+
}
|
|
2570
|
+
} catch (err) {
|
|
2571
|
+
ctx.logger.warn(
|
|
2572
|
+
`[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
|
|
2573
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
ctx.logger.warn("[AI] No LLM provider configured via environment variables. Falling back to MemoryLLMAdapter (echo mode). Set AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY to use a real LLM.");
|
|
2579
|
+
return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode - for testing only)" };
|
|
2580
|
+
}
|
|
1412
2581
|
async init(ctx) {
|
|
1413
2582
|
let hasExisting = false;
|
|
1414
2583
|
try {
|
|
@@ -1430,8 +2599,19 @@ var AIServicePlugin = class {
|
|
|
1430
2599
|
} catch {
|
|
1431
2600
|
}
|
|
1432
2601
|
}
|
|
2602
|
+
let adapter;
|
|
2603
|
+
let adapterDescription;
|
|
2604
|
+
if (this.options.adapter) {
|
|
2605
|
+
adapter = this.options.adapter;
|
|
2606
|
+
adapterDescription = `${adapter.name} (explicitly configured)`;
|
|
2607
|
+
} else {
|
|
2608
|
+
const detected = await this.detectAdapter(ctx);
|
|
2609
|
+
adapter = detected.adapter;
|
|
2610
|
+
adapterDescription = detected.description;
|
|
2611
|
+
}
|
|
2612
|
+
ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
|
|
1433
2613
|
const config = {
|
|
1434
|
-
adapter
|
|
2614
|
+
adapter,
|
|
1435
2615
|
logger: ctx.logger,
|
|
1436
2616
|
conversationService
|
|
1437
2617
|
};
|
|
@@ -1441,7 +2621,7 @@ var AIServicePlugin = class {
|
|
|
1441
2621
|
} else {
|
|
1442
2622
|
ctx.registerService("ai", this.service);
|
|
1443
2623
|
}
|
|
1444
|
-
ctx.
|
|
2624
|
+
ctx.getService("manifest").register({
|
|
1445
2625
|
id: "com.objectstack.service-ai",
|
|
1446
2626
|
name: "AI Service",
|
|
1447
2627
|
version: "1.0.0",
|
|
@@ -1454,40 +2634,113 @@ var AIServicePlugin = class {
|
|
|
1454
2634
|
ctx.logger.debug("[AI] Before chat", { messages });
|
|
1455
2635
|
});
|
|
1456
2636
|
}
|
|
2637
|
+
try {
|
|
2638
|
+
const setupNav = ctx.getService("setupNav");
|
|
2639
|
+
if (setupNav) {
|
|
2640
|
+
setupNav.contribute({
|
|
2641
|
+
areaId: "area_ai",
|
|
2642
|
+
items: [
|
|
2643
|
+
{ id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
|
|
2644
|
+
{ id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
|
|
2645
|
+
]
|
|
2646
|
+
});
|
|
2647
|
+
ctx.logger.info("[AI] Navigation items contributed to Setup App");
|
|
2648
|
+
}
|
|
2649
|
+
} catch {
|
|
2650
|
+
}
|
|
1457
2651
|
ctx.logger.info("[AI] Service initialized");
|
|
1458
2652
|
}
|
|
1459
2653
|
async start(ctx) {
|
|
1460
2654
|
if (!this.service) return;
|
|
2655
|
+
let metadataService;
|
|
2656
|
+
try {
|
|
2657
|
+
metadataService = ctx.getService("metadata");
|
|
2658
|
+
console.log("[AI Plugin] Retrieved metadata service:", !!metadataService, "has getRegisteredTypes:", typeof metadataService?.getRegisteredTypes);
|
|
2659
|
+
} catch (e) {
|
|
2660
|
+
console.log("[AI] Metadata service not available:", e.message);
|
|
2661
|
+
ctx.logger.debug("[AI] Metadata service not available");
|
|
2662
|
+
}
|
|
1461
2663
|
try {
|
|
1462
2664
|
const dataEngine = ctx.getService("data");
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
|
|
2665
|
+
if (dataEngine) {
|
|
2666
|
+
registerDataTools(this.service.toolRegistry, { dataEngine });
|
|
1466
2667
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
2668
|
+
if (metadataService) {
|
|
2669
|
+
const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
|
|
2670
|
+
for (const toolDef of DATA_TOOL_DEFINITIONS2) {
|
|
2671
|
+
const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
|
|
2672
|
+
if (!toolExists) {
|
|
2673
|
+
await metadataService.register("tool", toolDef.name, toolDef);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS2.length} data tools registered as metadata`);
|
|
2677
|
+
}
|
|
2678
|
+
if (metadataService) {
|
|
2679
|
+
try {
|
|
2680
|
+
const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
|
|
2681
|
+
if (!agentExists) {
|
|
2682
|
+
await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
|
|
2683
|
+
console.log("[AI] Registered data_chat agent to metadataService");
|
|
2684
|
+
ctx.logger.info("[AI] data_chat agent registered");
|
|
2685
|
+
} else {
|
|
2686
|
+
console.log("[AI] data_chat agent already exists, skipping");
|
|
2687
|
+
ctx.logger.debug("[AI] data_chat agent already exists, skipping auto-registration");
|
|
2688
|
+
}
|
|
2689
|
+
} catch (err) {
|
|
2690
|
+
ctx.logger.warn("[AI] Failed to register data_chat agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
|
|
2691
|
+
}
|
|
1473
2692
|
}
|
|
1474
2693
|
}
|
|
1475
2694
|
} catch {
|
|
1476
|
-
ctx.logger.debug("[AI] Data engine
|
|
2695
|
+
ctx.logger.debug("[AI] Data engine not available, skipping data tools");
|
|
2696
|
+
}
|
|
2697
|
+
if (metadataService) {
|
|
2698
|
+
try {
|
|
2699
|
+
registerMetadataTools(this.service.toolRegistry, { metadataService });
|
|
2700
|
+
ctx.logger.info("[AI] Built-in metadata tools registered");
|
|
2701
|
+
const { METADATA_TOOL_DEFINITIONS: METADATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_metadata_tools(), metadata_tools_exports));
|
|
2702
|
+
for (const toolDef of METADATA_TOOL_DEFINITIONS2) {
|
|
2703
|
+
const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
|
|
2704
|
+
if (!toolExists) {
|
|
2705
|
+
await metadataService.register("tool", toolDef.name, toolDef);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
ctx.logger.info(`[AI] ${METADATA_TOOL_DEFINITIONS2.length} metadata tools registered as metadata`);
|
|
2709
|
+
try {
|
|
2710
|
+
const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
|
|
2711
|
+
if (!agentExists) {
|
|
2712
|
+
await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
|
|
2713
|
+
console.log("[AI] Registered metadata_assistant agent to metadataService");
|
|
2714
|
+
ctx.logger.info("[AI] metadata_assistant agent registered");
|
|
2715
|
+
} else {
|
|
2716
|
+
console.log("[AI] metadata_assistant agent already exists, skipping");
|
|
2717
|
+
ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
|
|
2718
|
+
}
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
ctx.logger.warn("[AI] Failed to register metadata_assistant agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
|
|
2721
|
+
}
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
|
|
2724
|
+
}
|
|
1477
2725
|
}
|
|
1478
2726
|
await ctx.trigger("ai:ready", this.service);
|
|
1479
2727
|
const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2728
|
+
const toolRoutes = buildToolRoutes(this.service, ctx.logger);
|
|
2729
|
+
routes.push(...toolRoutes);
|
|
2730
|
+
ctx.logger.info(`[AI] Tool routes registered (${toolRoutes.length} routes)`);
|
|
2731
|
+
if (metadataService) {
|
|
2732
|
+
const agentRuntime = new AgentRuntime(metadataService);
|
|
2733
|
+
const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
|
|
2734
|
+
routes.push(...agentRoutes);
|
|
2735
|
+
ctx.logger.info(`[AI] Agent routes registered (${agentRoutes.length} routes)`);
|
|
2736
|
+
} else {
|
|
1488
2737
|
ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
|
|
1489
2738
|
}
|
|
1490
2739
|
await ctx.trigger("ai:routes", routes);
|
|
2740
|
+
const kernel = ctx.getKernel();
|
|
2741
|
+
if (kernel) {
|
|
2742
|
+
kernel.__aiRoutes = routes;
|
|
2743
|
+
}
|
|
1491
2744
|
ctx.logger.info(
|
|
1492
2745
|
`[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
|
|
1493
2746
|
);
|
|
@@ -1496,6 +2749,11 @@ var AIServicePlugin = class {
|
|
|
1496
2749
|
this.service = void 0;
|
|
1497
2750
|
}
|
|
1498
2751
|
};
|
|
2752
|
+
|
|
2753
|
+
// src/index.ts
|
|
2754
|
+
init_data_tools();
|
|
2755
|
+
init_metadata_tools();
|
|
2756
|
+
init_metadata_tools();
|
|
1499
2757
|
export {
|
|
1500
2758
|
AIService,
|
|
1501
2759
|
AIServicePlugin,
|
|
@@ -1505,11 +2763,24 @@ export {
|
|
|
1505
2763
|
DATA_CHAT_AGENT,
|
|
1506
2764
|
DATA_TOOL_DEFINITIONS,
|
|
1507
2765
|
InMemoryConversationService,
|
|
2766
|
+
METADATA_ASSISTANT_AGENT,
|
|
2767
|
+
METADATA_TOOL_DEFINITIONS,
|
|
1508
2768
|
MemoryLLMAdapter,
|
|
1509
2769
|
ObjectQLConversationService,
|
|
1510
2770
|
ToolRegistry,
|
|
2771
|
+
VercelLLMAdapter,
|
|
2772
|
+
addFieldTool,
|
|
1511
2773
|
buildAIRoutes,
|
|
1512
2774
|
buildAgentRoutes,
|
|
1513
|
-
|
|
2775
|
+
buildToolRoutes,
|
|
2776
|
+
createObjectTool,
|
|
2777
|
+
deleteFieldTool,
|
|
2778
|
+
describeObjectTool,
|
|
2779
|
+
encodeStreamPart,
|
|
2780
|
+
encodeVercelDataStream,
|
|
2781
|
+
listObjectsTool,
|
|
2782
|
+
modifyFieldTool,
|
|
2783
|
+
registerDataTools,
|
|
2784
|
+
registerMetadataTools
|
|
1514
2785
|
};
|
|
1515
2786
|
//# sourceMappingURL=index.js.map
|