@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.cjs
CHANGED
|
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
6
9
|
var __export = (target, all) => {
|
|
7
10
|
for (var name in all)
|
|
8
11
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -17,6 +20,752 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
20
|
};
|
|
18
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
22
|
|
|
23
|
+
// src/tools/data-tools.ts
|
|
24
|
+
var data_tools_exports = {};
|
|
25
|
+
__export(data_tools_exports, {
|
|
26
|
+
AGGREGATE_DATA_TOOL: () => AGGREGATE_DATA_TOOL,
|
|
27
|
+
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
28
|
+
GET_RECORD_TOOL: () => GET_RECORD_TOOL,
|
|
29
|
+
QUERY_RECORDS_TOOL: () => QUERY_RECORDS_TOOL,
|
|
30
|
+
registerDataTools: () => registerDataTools
|
|
31
|
+
});
|
|
32
|
+
function createQueryRecordsHandler(ctx) {
|
|
33
|
+
return async (args) => {
|
|
34
|
+
const {
|
|
35
|
+
objectName,
|
|
36
|
+
where,
|
|
37
|
+
fields,
|
|
38
|
+
orderBy,
|
|
39
|
+
limit,
|
|
40
|
+
offset
|
|
41
|
+
} = args;
|
|
42
|
+
const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
|
|
43
|
+
const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
|
|
44
|
+
const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
|
|
45
|
+
const records = await ctx.dataEngine.find(objectName, {
|
|
46
|
+
where,
|
|
47
|
+
fields,
|
|
48
|
+
orderBy,
|
|
49
|
+
limit: safeLimit,
|
|
50
|
+
offset: safeOffset
|
|
51
|
+
});
|
|
52
|
+
return JSON.stringify({ count: records.length, records });
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createGetRecordHandler(ctx) {
|
|
56
|
+
return async (args) => {
|
|
57
|
+
const { objectName, recordId, fields } = args;
|
|
58
|
+
const record = await ctx.dataEngine.findOne(objectName, {
|
|
59
|
+
where: { id: recordId },
|
|
60
|
+
fields
|
|
61
|
+
});
|
|
62
|
+
if (!record) {
|
|
63
|
+
return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify(record);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createAggregateDataHandler(ctx) {
|
|
69
|
+
return async (args) => {
|
|
70
|
+
const { objectName, aggregations, groupBy, where } = args;
|
|
71
|
+
for (const a of aggregations) {
|
|
72
|
+
if (!VALID_AGG_FUNCTIONS.has(a.function)) {
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const result = await ctx.dataEngine.aggregate(objectName, {
|
|
79
|
+
where,
|
|
80
|
+
groupBy,
|
|
81
|
+
aggregations: aggregations.map((a) => ({
|
|
82
|
+
function: a.function,
|
|
83
|
+
field: a.field,
|
|
84
|
+
alias: a.alias
|
|
85
|
+
}))
|
|
86
|
+
});
|
|
87
|
+
return JSON.stringify(result);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function registerDataTools(registry, context) {
|
|
91
|
+
registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
|
|
92
|
+
registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
|
|
93
|
+
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
94
|
+
}
|
|
95
|
+
var MAX_QUERY_LIMIT, DEFAULT_QUERY_LIMIT, QUERY_RECORDS_TOOL, GET_RECORD_TOOL, AGGREGATE_DATA_TOOL, DATA_TOOL_DEFINITIONS, VALID_AGG_FUNCTIONS;
|
|
96
|
+
var init_data_tools = __esm({
|
|
97
|
+
"src/tools/data-tools.ts"() {
|
|
98
|
+
"use strict";
|
|
99
|
+
MAX_QUERY_LIMIT = 200;
|
|
100
|
+
DEFAULT_QUERY_LIMIT = 20;
|
|
101
|
+
QUERY_RECORDS_TOOL = {
|
|
102
|
+
name: "query_records",
|
|
103
|
+
description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
|
|
104
|
+
parameters: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
objectName: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "The snake_case name of the object to query"
|
|
110
|
+
},
|
|
111
|
+
where: {
|
|
112
|
+
type: "object",
|
|
113
|
+
description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
|
|
114
|
+
},
|
|
115
|
+
fields: {
|
|
116
|
+
type: "array",
|
|
117
|
+
items: { type: "string" },
|
|
118
|
+
description: "List of field names to return (omit for all fields)"
|
|
119
|
+
},
|
|
120
|
+
orderBy: {
|
|
121
|
+
type: "array",
|
|
122
|
+
items: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
field: { type: "string" },
|
|
126
|
+
order: { type: "string", enum: ["asc", "desc"] }
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
|
|
130
|
+
},
|
|
131
|
+
limit: {
|
|
132
|
+
type: "number",
|
|
133
|
+
description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
|
|
134
|
+
},
|
|
135
|
+
offset: {
|
|
136
|
+
type: "number",
|
|
137
|
+
description: "Number of records to skip for pagination"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
required: ["objectName"],
|
|
141
|
+
additionalProperties: false
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
GET_RECORD_TOOL = {
|
|
145
|
+
name: "get_record",
|
|
146
|
+
description: "Get a single record by its ID from a data object.",
|
|
147
|
+
parameters: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
objectName: {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "The snake_case name of the object"
|
|
153
|
+
},
|
|
154
|
+
recordId: {
|
|
155
|
+
type: "string",
|
|
156
|
+
description: "The unique ID of the record"
|
|
157
|
+
},
|
|
158
|
+
fields: {
|
|
159
|
+
type: "array",
|
|
160
|
+
items: { type: "string" },
|
|
161
|
+
description: "List of field names to return (omit for all fields)"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
required: ["objectName", "recordId"],
|
|
165
|
+
additionalProperties: false
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
AGGREGATE_DATA_TOOL = {
|
|
169
|
+
name: "aggregate_data",
|
|
170
|
+
description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
|
|
171
|
+
parameters: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
objectName: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "The snake_case name of the object to aggregate"
|
|
177
|
+
},
|
|
178
|
+
aggregations: {
|
|
179
|
+
type: "array",
|
|
180
|
+
items: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
function: {
|
|
184
|
+
type: "string",
|
|
185
|
+
enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
|
|
186
|
+
description: "Aggregation function"
|
|
187
|
+
},
|
|
188
|
+
field: {
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "Field to aggregate (optional for count)"
|
|
191
|
+
},
|
|
192
|
+
alias: {
|
|
193
|
+
type: "string",
|
|
194
|
+
description: "Result column alias"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
required: ["function", "alias"]
|
|
198
|
+
},
|
|
199
|
+
description: "Aggregation definitions"
|
|
200
|
+
},
|
|
201
|
+
groupBy: {
|
|
202
|
+
type: "array",
|
|
203
|
+
items: { type: "string" },
|
|
204
|
+
description: "Fields to group by"
|
|
205
|
+
},
|
|
206
|
+
where: {
|
|
207
|
+
type: "object",
|
|
208
|
+
description: "Filter conditions applied before aggregation"
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
required: ["objectName", "aggregations"],
|
|
212
|
+
additionalProperties: false
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
DATA_TOOL_DEFINITIONS = [
|
|
216
|
+
QUERY_RECORDS_TOOL,
|
|
217
|
+
GET_RECORD_TOOL,
|
|
218
|
+
AGGREGATE_DATA_TOOL
|
|
219
|
+
];
|
|
220
|
+
VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
221
|
+
"count",
|
|
222
|
+
"sum",
|
|
223
|
+
"avg",
|
|
224
|
+
"min",
|
|
225
|
+
"max",
|
|
226
|
+
"count_distinct"
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// src/tools/create-object.tool.ts
|
|
232
|
+
var import_ai, createObjectTool;
|
|
233
|
+
var init_create_object_tool = __esm({
|
|
234
|
+
"src/tools/create-object.tool.ts"() {
|
|
235
|
+
"use strict";
|
|
236
|
+
import_ai = require("@objectstack/spec/ai");
|
|
237
|
+
createObjectTool = (0, import_ai.defineTool)({
|
|
238
|
+
name: "create_object",
|
|
239
|
+
label: "Create Object",
|
|
240
|
+
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.",
|
|
241
|
+
category: "data",
|
|
242
|
+
builtIn: true,
|
|
243
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
244
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
245
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
246
|
+
// should only be set once server-side approval gating is implemented to
|
|
247
|
+
// avoid giving users a false sense of safety.
|
|
248
|
+
parameters: {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
name: {
|
|
252
|
+
type: "string",
|
|
253
|
+
description: "Machine name for the object (snake_case, e.g. project_task)"
|
|
254
|
+
},
|
|
255
|
+
label: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "Human-readable display name (e.g. Project Task)"
|
|
258
|
+
},
|
|
259
|
+
fields: {
|
|
260
|
+
type: "array",
|
|
261
|
+
description: "Initial fields to create with the object",
|
|
262
|
+
items: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
name: { type: "string", description: "Field machine name (snake_case)" },
|
|
266
|
+
label: { type: "string", description: "Field display name" },
|
|
267
|
+
type: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Field data type",
|
|
270
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
271
|
+
},
|
|
272
|
+
required: { type: "boolean", description: "Whether the field is required" }
|
|
273
|
+
},
|
|
274
|
+
required: ["name", "type"]
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
enableFeatures: {
|
|
278
|
+
type: "object",
|
|
279
|
+
description: "Object capability flags",
|
|
280
|
+
properties: {
|
|
281
|
+
trackHistory: { type: "boolean" },
|
|
282
|
+
apiEnabled: { type: "boolean" }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
required: ["name", "label"],
|
|
287
|
+
additionalProperties: false
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// src/tools/add-field.tool.ts
|
|
294
|
+
var import_ai2, addFieldTool;
|
|
295
|
+
var init_add_field_tool = __esm({
|
|
296
|
+
"src/tools/add-field.tool.ts"() {
|
|
297
|
+
"use strict";
|
|
298
|
+
import_ai2 = require("@objectstack/spec/ai");
|
|
299
|
+
addFieldTool = (0, import_ai2.defineTool)({
|
|
300
|
+
name: "add_field",
|
|
301
|
+
label: "Add Field",
|
|
302
|
+
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.",
|
|
303
|
+
category: "data",
|
|
304
|
+
builtIn: true,
|
|
305
|
+
parameters: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
objectName: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Target object machine name (snake_case)"
|
|
311
|
+
},
|
|
312
|
+
name: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "Field machine name (snake_case, e.g. due_date)"
|
|
315
|
+
},
|
|
316
|
+
label: {
|
|
317
|
+
type: "string",
|
|
318
|
+
description: "Human-readable field label (e.g. Due Date)"
|
|
319
|
+
},
|
|
320
|
+
type: {
|
|
321
|
+
type: "string",
|
|
322
|
+
description: "Field data type",
|
|
323
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
324
|
+
},
|
|
325
|
+
required: {
|
|
326
|
+
type: "boolean",
|
|
327
|
+
description: "Whether the field is required"
|
|
328
|
+
},
|
|
329
|
+
defaultValue: {
|
|
330
|
+
description: "Default value for the field"
|
|
331
|
+
},
|
|
332
|
+
options: {
|
|
333
|
+
type: "array",
|
|
334
|
+
description: "Options for select/picklist fields",
|
|
335
|
+
items: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {
|
|
338
|
+
label: { type: "string" },
|
|
339
|
+
value: {
|
|
340
|
+
type: "string",
|
|
341
|
+
description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
|
|
342
|
+
pattern: "^[a-z_][a-z0-9_]*$"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
reference: {
|
|
348
|
+
type: "string",
|
|
349
|
+
description: "Referenced object name for lookup fields (snake_case, e.g. account)"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
required: ["objectName", "name", "type"],
|
|
353
|
+
additionalProperties: false
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// src/tools/modify-field.tool.ts
|
|
360
|
+
var import_ai3, modifyFieldTool;
|
|
361
|
+
var init_modify_field_tool = __esm({
|
|
362
|
+
"src/tools/modify-field.tool.ts"() {
|
|
363
|
+
"use strict";
|
|
364
|
+
import_ai3 = require("@objectstack/spec/ai");
|
|
365
|
+
modifyFieldTool = (0, import_ai3.defineTool)({
|
|
366
|
+
name: "modify_field",
|
|
367
|
+
label: "Modify Field",
|
|
368
|
+
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).",
|
|
369
|
+
category: "data",
|
|
370
|
+
builtIn: true,
|
|
371
|
+
parameters: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
objectName: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description: "Target object machine name (snake_case)"
|
|
377
|
+
},
|
|
378
|
+
fieldName: {
|
|
379
|
+
type: "string",
|
|
380
|
+
description: "Existing field machine name to modify (snake_case)"
|
|
381
|
+
},
|
|
382
|
+
changes: {
|
|
383
|
+
type: "object",
|
|
384
|
+
description: "Field properties to update (partial patch)",
|
|
385
|
+
properties: {
|
|
386
|
+
label: { type: "string", description: "New display label" },
|
|
387
|
+
type: { type: "string", description: "New field type" },
|
|
388
|
+
required: { type: "boolean", description: "Update required constraint" },
|
|
389
|
+
defaultValue: { description: "New default value" }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
required: ["objectName", "fieldName", "changes"],
|
|
394
|
+
additionalProperties: false
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// src/tools/delete-field.tool.ts
|
|
401
|
+
var import_ai4, deleteFieldTool;
|
|
402
|
+
var init_delete_field_tool = __esm({
|
|
403
|
+
"src/tools/delete-field.tool.ts"() {
|
|
404
|
+
"use strict";
|
|
405
|
+
import_ai4 = require("@objectstack/spec/ai");
|
|
406
|
+
deleteFieldTool = (0, import_ai4.defineTool)({
|
|
407
|
+
name: "delete_field",
|
|
408
|
+
label: "Delete Field",
|
|
409
|
+
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.",
|
|
410
|
+
category: "data",
|
|
411
|
+
builtIn: true,
|
|
412
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
413
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
414
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
415
|
+
// should only be set once server-side approval gating is implemented.
|
|
416
|
+
parameters: {
|
|
417
|
+
type: "object",
|
|
418
|
+
properties: {
|
|
419
|
+
objectName: {
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "Target object machine name (snake_case)"
|
|
422
|
+
},
|
|
423
|
+
fieldName: {
|
|
424
|
+
type: "string",
|
|
425
|
+
description: "Field machine name to delete (snake_case)"
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
required: ["objectName", "fieldName"],
|
|
429
|
+
additionalProperties: false
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// src/tools/list-objects.tool.ts
|
|
436
|
+
var import_ai5, listObjectsTool;
|
|
437
|
+
var init_list_objects_tool = __esm({
|
|
438
|
+
"src/tools/list-objects.tool.ts"() {
|
|
439
|
+
"use strict";
|
|
440
|
+
import_ai5 = require("@objectstack/spec/ai");
|
|
441
|
+
listObjectsTool = (0, import_ai5.defineTool)({
|
|
442
|
+
name: "list_objects",
|
|
443
|
+
label: "List Objects",
|
|
444
|
+
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.",
|
|
445
|
+
category: "data",
|
|
446
|
+
builtIn: true,
|
|
447
|
+
parameters: {
|
|
448
|
+
type: "object",
|
|
449
|
+
properties: {
|
|
450
|
+
filter: {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "Optional name or label substring to filter objects"
|
|
453
|
+
},
|
|
454
|
+
includeFields: {
|
|
455
|
+
type: "boolean",
|
|
456
|
+
description: "Whether to include field summaries for each object (default: false)"
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
additionalProperties: false
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// src/tools/describe-object.tool.ts
|
|
466
|
+
var import_ai6, describeObjectTool;
|
|
467
|
+
var init_describe_object_tool = __esm({
|
|
468
|
+
"src/tools/describe-object.tool.ts"() {
|
|
469
|
+
"use strict";
|
|
470
|
+
import_ai6 = require("@objectstack/spec/ai");
|
|
471
|
+
describeObjectTool = (0, import_ai6.defineTool)({
|
|
472
|
+
name: "describe_object",
|
|
473
|
+
label: "Describe Object",
|
|
474
|
+
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.",
|
|
475
|
+
category: "data",
|
|
476
|
+
builtIn: true,
|
|
477
|
+
parameters: {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {
|
|
480
|
+
objectName: {
|
|
481
|
+
type: "string",
|
|
482
|
+
description: "Object machine name to describe (snake_case)"
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
required: ["objectName"],
|
|
486
|
+
additionalProperties: false
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// src/tools/metadata-tools.ts
|
|
493
|
+
var metadata_tools_exports = {};
|
|
494
|
+
__export(metadata_tools_exports, {
|
|
495
|
+
METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
|
|
496
|
+
addFieldTool: () => addFieldTool,
|
|
497
|
+
createObjectTool: () => createObjectTool,
|
|
498
|
+
deleteFieldTool: () => deleteFieldTool,
|
|
499
|
+
describeObjectTool: () => describeObjectTool,
|
|
500
|
+
listObjectsTool: () => listObjectsTool,
|
|
501
|
+
modifyFieldTool: () => modifyFieldTool,
|
|
502
|
+
registerMetadataTools: () => registerMetadataTools
|
|
503
|
+
});
|
|
504
|
+
function isSnakeCase(value) {
|
|
505
|
+
return SNAKE_CASE_RE.test(value);
|
|
506
|
+
}
|
|
507
|
+
function createCreateObjectHandler(ctx) {
|
|
508
|
+
return async (args) => {
|
|
509
|
+
const { name, label, fields, enableFeatures } = args;
|
|
510
|
+
if (!name || !label) {
|
|
511
|
+
return JSON.stringify({ error: 'Both "name" and "label" are required' });
|
|
512
|
+
}
|
|
513
|
+
if (!isSnakeCase(name)) {
|
|
514
|
+
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
|
|
515
|
+
}
|
|
516
|
+
const existing = await ctx.metadataService.getObject(name);
|
|
517
|
+
if (existing) {
|
|
518
|
+
return JSON.stringify({ error: `Object "${name}" already exists` });
|
|
519
|
+
}
|
|
520
|
+
const fieldMap = {};
|
|
521
|
+
if (fields && Array.isArray(fields)) {
|
|
522
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
523
|
+
for (const f of fields) {
|
|
524
|
+
if (!f.name) {
|
|
525
|
+
return JSON.stringify({ error: 'Each field must have a "name" property' });
|
|
526
|
+
}
|
|
527
|
+
if (!isSnakeCase(f.name)) {
|
|
528
|
+
return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
|
|
529
|
+
}
|
|
530
|
+
if (seenNames.has(f.name)) {
|
|
531
|
+
return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
|
|
532
|
+
}
|
|
533
|
+
seenNames.add(f.name);
|
|
534
|
+
fieldMap[f.name] = {
|
|
535
|
+
type: f.type,
|
|
536
|
+
...f.label ? { label: f.label } : {},
|
|
537
|
+
...f.required !== void 0 ? { required: f.required } : {}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const objectDef = {
|
|
542
|
+
name,
|
|
543
|
+
label,
|
|
544
|
+
...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
|
|
545
|
+
...enableFeatures ? { enable: enableFeatures } : {}
|
|
546
|
+
};
|
|
547
|
+
await ctx.metadataService.register("object", name, objectDef);
|
|
548
|
+
return JSON.stringify({
|
|
549
|
+
name,
|
|
550
|
+
label,
|
|
551
|
+
fieldCount: Object.keys(fieldMap).length
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function createAddFieldHandler(ctx) {
|
|
556
|
+
return async (args) => {
|
|
557
|
+
const { objectName, name, label, type, required, defaultValue, options, reference } = args;
|
|
558
|
+
if (!objectName || !name || !type) {
|
|
559
|
+
return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
|
|
560
|
+
}
|
|
561
|
+
if (!isSnakeCase(objectName)) {
|
|
562
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
563
|
+
}
|
|
564
|
+
if (!isSnakeCase(name)) {
|
|
565
|
+
return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
|
|
566
|
+
}
|
|
567
|
+
if (reference && !isSnakeCase(reference)) {
|
|
568
|
+
return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
|
|
569
|
+
}
|
|
570
|
+
if (options && Array.isArray(options)) {
|
|
571
|
+
for (const opt of options) {
|
|
572
|
+
if (opt.value && !isSnakeCase(opt.value)) {
|
|
573
|
+
return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
578
|
+
if (!objectDef) {
|
|
579
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
580
|
+
}
|
|
581
|
+
const def = objectDef;
|
|
582
|
+
if (def.fields && def.fields[name]) {
|
|
583
|
+
return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
|
|
584
|
+
}
|
|
585
|
+
const fieldDef = {
|
|
586
|
+
type,
|
|
587
|
+
...label ? { label } : {},
|
|
588
|
+
...required !== void 0 ? { required } : {},
|
|
589
|
+
...defaultValue !== void 0 ? { defaultValue } : {},
|
|
590
|
+
...options ? { options } : {},
|
|
591
|
+
...reference ? { reference } : {}
|
|
592
|
+
};
|
|
593
|
+
const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
|
|
594
|
+
await ctx.metadataService.register("object", objectName, {
|
|
595
|
+
...def,
|
|
596
|
+
fields: updatedFields
|
|
597
|
+
});
|
|
598
|
+
return JSON.stringify({
|
|
599
|
+
objectName,
|
|
600
|
+
fieldName: name,
|
|
601
|
+
fieldType: type
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function createModifyFieldHandler(ctx) {
|
|
606
|
+
return async (args) => {
|
|
607
|
+
const { objectName, fieldName, changes } = args;
|
|
608
|
+
if (!objectName || !fieldName || !changes) {
|
|
609
|
+
return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
|
|
610
|
+
}
|
|
611
|
+
if (!isSnakeCase(objectName)) {
|
|
612
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
613
|
+
}
|
|
614
|
+
if (!isSnakeCase(fieldName)) {
|
|
615
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
616
|
+
}
|
|
617
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
618
|
+
if (!objectDef) {
|
|
619
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
620
|
+
}
|
|
621
|
+
const def = objectDef;
|
|
622
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
623
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
624
|
+
}
|
|
625
|
+
const existingField = def.fields[fieldName];
|
|
626
|
+
const updatedField = { ...existingField, ...changes };
|
|
627
|
+
const updatedFields = { ...def.fields, [fieldName]: updatedField };
|
|
628
|
+
await ctx.metadataService.register("object", objectName, {
|
|
629
|
+
...def,
|
|
630
|
+
fields: updatedFields
|
|
631
|
+
});
|
|
632
|
+
return JSON.stringify({
|
|
633
|
+
objectName,
|
|
634
|
+
fieldName,
|
|
635
|
+
updatedProperties: Object.keys(changes)
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function createDeleteFieldHandler(ctx) {
|
|
640
|
+
return async (args) => {
|
|
641
|
+
const { objectName, fieldName } = args;
|
|
642
|
+
if (!objectName || !fieldName) {
|
|
643
|
+
return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
|
|
644
|
+
}
|
|
645
|
+
if (!isSnakeCase(objectName)) {
|
|
646
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
647
|
+
}
|
|
648
|
+
if (!isSnakeCase(fieldName)) {
|
|
649
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
650
|
+
}
|
|
651
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
652
|
+
if (!objectDef) {
|
|
653
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
654
|
+
}
|
|
655
|
+
const def = objectDef;
|
|
656
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
657
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
658
|
+
}
|
|
659
|
+
const { [fieldName]: _removed, ...remainingFields } = def.fields;
|
|
660
|
+
await ctx.metadataService.register("object", objectName, {
|
|
661
|
+
...def,
|
|
662
|
+
fields: remainingFields
|
|
663
|
+
});
|
|
664
|
+
return JSON.stringify({
|
|
665
|
+
objectName,
|
|
666
|
+
fieldName,
|
|
667
|
+
success: true
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function createListObjectsHandler(ctx) {
|
|
672
|
+
return async (args) => {
|
|
673
|
+
const { filter, includeFields } = args ?? {};
|
|
674
|
+
const objects = await ctx.metadataService.listObjects();
|
|
675
|
+
let result = objects.map((o) => {
|
|
676
|
+
const base = {
|
|
677
|
+
name: o.name,
|
|
678
|
+
label: o.label ?? o.name,
|
|
679
|
+
fieldCount: o.fields ? Object.keys(o.fields).length : 0
|
|
680
|
+
};
|
|
681
|
+
if (includeFields && o.fields) {
|
|
682
|
+
base.fields = Object.entries(o.fields).map(([key, f]) => ({
|
|
683
|
+
name: key,
|
|
684
|
+
type: f.type,
|
|
685
|
+
label: f.label ?? key
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
return base;
|
|
689
|
+
});
|
|
690
|
+
if (filter) {
|
|
691
|
+
const lower = filter.toLowerCase();
|
|
692
|
+
result = result.filter(
|
|
693
|
+
(o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return JSON.stringify({
|
|
697
|
+
objects: result,
|
|
698
|
+
totalCount: result.length
|
|
699
|
+
});
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function createDescribeObjectHandler(ctx) {
|
|
703
|
+
return async (args) => {
|
|
704
|
+
const { objectName } = args;
|
|
705
|
+
if (!objectName) {
|
|
706
|
+
return JSON.stringify({ error: '"objectName" is required' });
|
|
707
|
+
}
|
|
708
|
+
if (!isSnakeCase(objectName)) {
|
|
709
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
710
|
+
}
|
|
711
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
712
|
+
if (!objectDef) {
|
|
713
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
714
|
+
}
|
|
715
|
+
const def = objectDef;
|
|
716
|
+
const fields = def.fields ?? {};
|
|
717
|
+
const fieldSummary = Object.entries(fields).map(([key, f]) => ({
|
|
718
|
+
name: key,
|
|
719
|
+
type: f.type,
|
|
720
|
+
label: f.label ?? key,
|
|
721
|
+
required: f.required ?? false,
|
|
722
|
+
...f.reference ? { reference: f.reference } : {},
|
|
723
|
+
...f.options ? { options: f.options } : {}
|
|
724
|
+
}));
|
|
725
|
+
return JSON.stringify({
|
|
726
|
+
name: def.name,
|
|
727
|
+
label: def.label ?? def.name,
|
|
728
|
+
fields: fieldSummary,
|
|
729
|
+
enableFeatures: def.enable ?? {}
|
|
730
|
+
});
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function registerMetadataTools(registry, context) {
|
|
734
|
+
registry.register(createObjectTool, createCreateObjectHandler(context));
|
|
735
|
+
registry.register(addFieldTool, createAddFieldHandler(context));
|
|
736
|
+
registry.register(modifyFieldTool, createModifyFieldHandler(context));
|
|
737
|
+
registry.register(deleteFieldTool, createDeleteFieldHandler(context));
|
|
738
|
+
registry.register(listObjectsTool, createListObjectsHandler(context));
|
|
739
|
+
registry.register(describeObjectTool, createDescribeObjectHandler(context));
|
|
740
|
+
}
|
|
741
|
+
var METADATA_TOOL_DEFINITIONS, SNAKE_CASE_RE;
|
|
742
|
+
var init_metadata_tools = __esm({
|
|
743
|
+
"src/tools/metadata-tools.ts"() {
|
|
744
|
+
"use strict";
|
|
745
|
+
init_create_object_tool();
|
|
746
|
+
init_add_field_tool();
|
|
747
|
+
init_modify_field_tool();
|
|
748
|
+
init_delete_field_tool();
|
|
749
|
+
init_list_objects_tool();
|
|
750
|
+
init_describe_object_tool();
|
|
751
|
+
init_create_object_tool();
|
|
752
|
+
init_add_field_tool();
|
|
753
|
+
init_modify_field_tool();
|
|
754
|
+
init_delete_field_tool();
|
|
755
|
+
init_list_objects_tool();
|
|
756
|
+
init_describe_object_tool();
|
|
757
|
+
METADATA_TOOL_DEFINITIONS = [
|
|
758
|
+
createObjectTool,
|
|
759
|
+
addFieldTool,
|
|
760
|
+
modifyFieldTool,
|
|
761
|
+
deleteFieldTool,
|
|
762
|
+
listObjectsTool,
|
|
763
|
+
describeObjectTool
|
|
764
|
+
];
|
|
765
|
+
SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
20
769
|
// src/index.ts
|
|
21
770
|
var index_exports = {};
|
|
22
771
|
__export(index_exports, {
|
|
@@ -28,12 +777,25 @@ __export(index_exports, {
|
|
|
28
777
|
DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
|
|
29
778
|
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
30
779
|
InMemoryConversationService: () => InMemoryConversationService,
|
|
780
|
+
METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
|
|
781
|
+
METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
|
|
31
782
|
MemoryLLMAdapter: () => MemoryLLMAdapter,
|
|
32
783
|
ObjectQLConversationService: () => ObjectQLConversationService,
|
|
33
784
|
ToolRegistry: () => ToolRegistry,
|
|
785
|
+
VercelLLMAdapter: () => VercelLLMAdapter,
|
|
786
|
+
addFieldTool: () => addFieldTool,
|
|
34
787
|
buildAIRoutes: () => buildAIRoutes,
|
|
35
788
|
buildAgentRoutes: () => buildAgentRoutes,
|
|
36
|
-
|
|
789
|
+
buildToolRoutes: () => buildToolRoutes,
|
|
790
|
+
createObjectTool: () => createObjectTool,
|
|
791
|
+
deleteFieldTool: () => deleteFieldTool,
|
|
792
|
+
describeObjectTool: () => describeObjectTool,
|
|
793
|
+
encodeStreamPart: () => encodeStreamPart,
|
|
794
|
+
encodeVercelDataStream: () => encodeVercelDataStream,
|
|
795
|
+
listObjectsTool: () => listObjectsTool,
|
|
796
|
+
modifyFieldTool: () => modifyFieldTool,
|
|
797
|
+
registerDataTools: () => registerDataTools,
|
|
798
|
+
registerMetadataTools: () => registerMetadataTools
|
|
37
799
|
});
|
|
38
800
|
module.exports = __toCommonJS(index_exports);
|
|
39
801
|
|
|
@@ -47,7 +809,9 @@ var MemoryLLMAdapter = class {
|
|
|
47
809
|
}
|
|
48
810
|
async chat(messages, options) {
|
|
49
811
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
50
|
-
const
|
|
812
|
+
const userContent = lastUserMessage?.content;
|
|
813
|
+
const text = typeof userContent === "string" ? userContent : "(complex content)";
|
|
814
|
+
const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
|
|
51
815
|
return {
|
|
52
816
|
content,
|
|
53
817
|
model: options?.model ?? "memory",
|
|
@@ -65,10 +829,15 @@ var MemoryLLMAdapter = class {
|
|
|
65
829
|
const result = await this.chat(messages);
|
|
66
830
|
const words = result.content.split(" ");
|
|
67
831
|
for (let i = 0; i < words.length; i++) {
|
|
68
|
-
const
|
|
69
|
-
yield { type: "text-delta",
|
|
832
|
+
const wordText = i === 0 ? words[i] : ` ${words[i]}`;
|
|
833
|
+
yield { type: "text-delta", id: `delta_${i}`, text: wordText };
|
|
70
834
|
}
|
|
71
|
-
yield {
|
|
835
|
+
yield {
|
|
836
|
+
type: "finish",
|
|
837
|
+
finishReason: "stop",
|
|
838
|
+
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
839
|
+
rawFinishReason: "stop"
|
|
840
|
+
};
|
|
72
841
|
}
|
|
73
842
|
async embed(input) {
|
|
74
843
|
const texts = Array.isArray(input) ? input : [input];
|
|
@@ -131,21 +900,34 @@ var ToolRegistry = class {
|
|
|
131
900
|
* Execute a tool call and return the result.
|
|
132
901
|
*/
|
|
133
902
|
async execute(toolCall) {
|
|
134
|
-
const handler = this.handlers.get(toolCall.
|
|
903
|
+
const handler = this.handlers.get(toolCall.toolName);
|
|
135
904
|
if (!handler) {
|
|
136
905
|
return {
|
|
137
|
-
|
|
138
|
-
|
|
906
|
+
type: "tool-result",
|
|
907
|
+
toolCallId: toolCall.toolCallId,
|
|
908
|
+
toolName: toolCall.toolName,
|
|
909
|
+
output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
|
|
139
910
|
isError: true
|
|
140
911
|
};
|
|
141
912
|
}
|
|
142
913
|
try {
|
|
143
|
-
const args = JSON.parse(toolCall.
|
|
914
|
+
const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
|
|
144
915
|
const content = await handler(args);
|
|
145
|
-
return {
|
|
916
|
+
return {
|
|
917
|
+
type: "tool-result",
|
|
918
|
+
toolCallId: toolCall.toolCallId,
|
|
919
|
+
toolName: toolCall.toolName,
|
|
920
|
+
output: { type: "text", value: content }
|
|
921
|
+
};
|
|
146
922
|
} catch (err) {
|
|
147
923
|
const message = err instanceof Error ? err.message : String(err);
|
|
148
|
-
return {
|
|
924
|
+
return {
|
|
925
|
+
type: "tool-result",
|
|
926
|
+
toolCallId: toolCall.toolCallId,
|
|
927
|
+
toolName: toolCall.toolName,
|
|
928
|
+
output: { type: "text", value: message },
|
|
929
|
+
isError: true
|
|
930
|
+
};
|
|
149
931
|
}
|
|
150
932
|
}
|
|
151
933
|
/**
|
|
@@ -231,6 +1013,17 @@ var InMemoryConversationService = class {
|
|
|
231
1013
|
};
|
|
232
1014
|
|
|
233
1015
|
// src/ai-service.ts
|
|
1016
|
+
function textDeltaPart(id, text) {
|
|
1017
|
+
return { type: "text-delta", id, text };
|
|
1018
|
+
}
|
|
1019
|
+
function finishPart(result) {
|
|
1020
|
+
return {
|
|
1021
|
+
type: "finish",
|
|
1022
|
+
finishReason: "stop",
|
|
1023
|
+
totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
1024
|
+
rawFinishReason: "stop"
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
234
1027
|
var _AIService = class _AIService {
|
|
235
1028
|
constructor(config = {}) {
|
|
236
1029
|
this.adapter = config.adapter ?? new MemoryLLMAdapter();
|
|
@@ -258,8 +1051,8 @@ var _AIService = class _AIService {
|
|
|
258
1051
|
this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
|
|
259
1052
|
if (!this.adapter.streamChat) {
|
|
260
1053
|
const result = await this.adapter.chat(messages, options);
|
|
261
|
-
yield
|
|
262
|
-
yield
|
|
1054
|
+
yield textDeltaPart("fallback", result.content);
|
|
1055
|
+
yield finishPart(result);
|
|
263
1056
|
return;
|
|
264
1057
|
}
|
|
265
1058
|
yield* this.adapter.streamChat(messages, options);
|
|
@@ -276,6 +1069,10 @@ var _AIService = class _AIService {
|
|
|
276
1069
|
}
|
|
277
1070
|
return this.adapter.listModels();
|
|
278
1071
|
}
|
|
1072
|
+
/** Extract the text value from a ToolExecutionResult's output. */
|
|
1073
|
+
static extractOutputText(tr) {
|
|
1074
|
+
return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
|
|
1075
|
+
}
|
|
279
1076
|
/**
|
|
280
1077
|
* Chat with automatic tool call resolution.
|
|
281
1078
|
*
|
|
@@ -316,23 +1113,26 @@ var _AIService = class _AIService {
|
|
|
316
1113
|
}
|
|
317
1114
|
this.logger.debug("[AI] chatWithTools tool calls", {
|
|
318
1115
|
iteration,
|
|
319
|
-
calls: result.toolCalls.map((tc) => tc.
|
|
1116
|
+
calls: result.toolCalls.map((tc) => tc.toolName)
|
|
320
1117
|
});
|
|
1118
|
+
const assistantContent = [];
|
|
1119
|
+
if (result.content) assistantContent.push({ type: "text", text: result.content });
|
|
1120
|
+
assistantContent.push(...result.toolCalls);
|
|
321
1121
|
conversation.push({
|
|
322
1122
|
role: "assistant",
|
|
323
|
-
content:
|
|
324
|
-
toolCalls: result.toolCalls
|
|
1123
|
+
content: assistantContent
|
|
325
1124
|
});
|
|
326
1125
|
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
327
1126
|
for (const tr of toolResults) {
|
|
328
1127
|
if (tr.isError) {
|
|
329
|
-
const matchedCall = result.toolCalls.find((tc) => tc.
|
|
330
|
-
const toolName = matchedCall?.
|
|
331
|
-
const
|
|
1128
|
+
const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
1129
|
+
const toolName = matchedCall?.toolName ?? "unknown";
|
|
1130
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
1131
|
+
const errorEntry = { iteration, toolName, error: errorText };
|
|
332
1132
|
toolErrors.push(errorEntry);
|
|
333
1133
|
this.logger.warn("[AI] chatWithTools tool error", errorEntry);
|
|
334
1134
|
if (onToolError && matchedCall) {
|
|
335
|
-
const action = onToolError(matchedCall,
|
|
1135
|
+
const action = onToolError(matchedCall, errorText);
|
|
336
1136
|
if (action === "abort") {
|
|
337
1137
|
abortedByCallback = true;
|
|
338
1138
|
}
|
|
@@ -340,8 +1140,7 @@ var _AIService = class _AIService {
|
|
|
340
1140
|
}
|
|
341
1141
|
conversation.push({
|
|
342
1142
|
role: "tool",
|
|
343
|
-
content: tr
|
|
344
|
-
toolCallId: tr.toolCallId
|
|
1143
|
+
content: [tr]
|
|
345
1144
|
});
|
|
346
1145
|
}
|
|
347
1146
|
if (abortedByCallback) {
|
|
@@ -387,33 +1186,41 @@ var _AIService = class _AIService {
|
|
|
387
1186
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
388
1187
|
const result2 = await this.adapter.chat(conversation, chatOptions);
|
|
389
1188
|
if (!result2.toolCalls || result2.toolCalls.length === 0) {
|
|
390
|
-
yield
|
|
391
|
-
yield
|
|
1189
|
+
yield textDeltaPart("stream", result2.content);
|
|
1190
|
+
yield finishPart(result2);
|
|
392
1191
|
return;
|
|
393
1192
|
}
|
|
394
1193
|
for (const tc of result2.toolCalls) {
|
|
395
|
-
yield { type: "tool-call",
|
|
1194
|
+
yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
|
|
396
1195
|
}
|
|
1196
|
+
const assistantContent = [];
|
|
1197
|
+
if (result2.content) assistantContent.push({ type: "text", text: result2.content });
|
|
1198
|
+
assistantContent.push(...result2.toolCalls);
|
|
397
1199
|
conversation.push({
|
|
398
1200
|
role: "assistant",
|
|
399
|
-
content:
|
|
400
|
-
toolCalls: result2.toolCalls
|
|
1201
|
+
content: assistantContent
|
|
401
1202
|
});
|
|
402
1203
|
const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
|
|
403
1204
|
for (const tr of toolResults) {
|
|
404
1205
|
if (tr.isError && onToolError) {
|
|
405
|
-
const matchedCall = result2.toolCalls.find((tc) => tc.
|
|
1206
|
+
const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
406
1207
|
if (matchedCall) {
|
|
407
|
-
const
|
|
1208
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
1209
|
+
const action = onToolError(matchedCall, errorText);
|
|
408
1210
|
if (action === "abort") {
|
|
409
1211
|
abortedByCallback = true;
|
|
410
1212
|
}
|
|
411
1213
|
}
|
|
412
1214
|
}
|
|
1215
|
+
yield {
|
|
1216
|
+
type: "tool-result",
|
|
1217
|
+
toolCallId: tr.toolCallId,
|
|
1218
|
+
toolName: tr.toolName,
|
|
1219
|
+
output: tr.output
|
|
1220
|
+
};
|
|
413
1221
|
conversation.push({
|
|
414
1222
|
role: "tool",
|
|
415
|
-
content: tr
|
|
416
|
-
toolCallId: tr.toolCallId
|
|
1223
|
+
content: [tr]
|
|
417
1224
|
});
|
|
418
1225
|
}
|
|
419
1226
|
if (abortedByCallback) {
|
|
@@ -427,8 +1234,8 @@ var _AIService = class _AIService {
|
|
|
427
1234
|
}
|
|
428
1235
|
const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
|
|
429
1236
|
const result = await this.adapter.chat(conversation, finalOptions);
|
|
430
|
-
yield
|
|
431
|
-
yield
|
|
1237
|
+
yield textDeltaPart("stream", result.content);
|
|
1238
|
+
yield finishPart(result);
|
|
432
1239
|
}
|
|
433
1240
|
};
|
|
434
1241
|
// ── Tool Call Loop ────────────────────────────────────────────
|
|
@@ -436,6 +1243,143 @@ var _AIService = class _AIService {
|
|
|
436
1243
|
_AIService.DEFAULT_MAX_ITERATIONS = 10;
|
|
437
1244
|
var AIService = _AIService;
|
|
438
1245
|
|
|
1246
|
+
// src/stream/vercel-stream-encoder.ts
|
|
1247
|
+
function sse(data) {
|
|
1248
|
+
return `data: ${JSON.stringify(data)}
|
|
1249
|
+
|
|
1250
|
+
`;
|
|
1251
|
+
}
|
|
1252
|
+
function dataStreamLine(prefix, data) {
|
|
1253
|
+
return `${prefix}:${JSON.stringify(data)}
|
|
1254
|
+
`;
|
|
1255
|
+
}
|
|
1256
|
+
function encodeStreamPart(part) {
|
|
1257
|
+
switch (part.type) {
|
|
1258
|
+
case "text-delta":
|
|
1259
|
+
return sse({ type: "text-delta", id: "0", delta: part.text });
|
|
1260
|
+
case "tool-input-start":
|
|
1261
|
+
return sse({
|
|
1262
|
+
type: "tool-input-start",
|
|
1263
|
+
toolCallId: part.id,
|
|
1264
|
+
toolName: part.toolName
|
|
1265
|
+
});
|
|
1266
|
+
case "tool-input-delta":
|
|
1267
|
+
return sse({
|
|
1268
|
+
type: "tool-input-delta",
|
|
1269
|
+
toolCallId: part.id,
|
|
1270
|
+
inputTextDelta: part.delta
|
|
1271
|
+
});
|
|
1272
|
+
case "tool-call":
|
|
1273
|
+
return sse({
|
|
1274
|
+
type: "tool-input-available",
|
|
1275
|
+
toolCallId: part.toolCallId,
|
|
1276
|
+
toolName: part.toolName,
|
|
1277
|
+
input: part.input
|
|
1278
|
+
});
|
|
1279
|
+
case "tool-result":
|
|
1280
|
+
return sse({
|
|
1281
|
+
type: "tool-output-available",
|
|
1282
|
+
toolCallId: part.toolCallId,
|
|
1283
|
+
output: part.output
|
|
1284
|
+
});
|
|
1285
|
+
case "error":
|
|
1286
|
+
return sse({
|
|
1287
|
+
type: "error",
|
|
1288
|
+
errorText: String(part.error)
|
|
1289
|
+
});
|
|
1290
|
+
// Handle reasoning/thinking streams (DeepSeek R1, o1-style models)
|
|
1291
|
+
// Use 'g:' prefix for reasoning content per Vercel AI SDK protocol
|
|
1292
|
+
case "reasoning-start":
|
|
1293
|
+
return dataStreamLine("g", { text: "" });
|
|
1294
|
+
case "reasoning-delta":
|
|
1295
|
+
return dataStreamLine("g", { text: part.text });
|
|
1296
|
+
case "reasoning-end":
|
|
1297
|
+
return "";
|
|
1298
|
+
// No specific end marker needed for reasoning
|
|
1299
|
+
// finish-step and finish are handled by the generator, not here
|
|
1300
|
+
default:
|
|
1301
|
+
if (part.type?.startsWith("step-")) {
|
|
1302
|
+
return sse(part);
|
|
1303
|
+
}
|
|
1304
|
+
return "";
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async function* encodeVercelDataStream(events) {
|
|
1308
|
+
yield sse({ type: "start" });
|
|
1309
|
+
yield sse({ type: "start-step" });
|
|
1310
|
+
yield sse({ type: "text-start", id: "0" });
|
|
1311
|
+
let textOpen = true;
|
|
1312
|
+
let finishReason = "stop";
|
|
1313
|
+
for await (const part of events) {
|
|
1314
|
+
if (part.type === "finish") {
|
|
1315
|
+
finishReason = part.finishReason ?? "stop";
|
|
1316
|
+
}
|
|
1317
|
+
if (part.type === "finish-step" || part.type === "finish") {
|
|
1318
|
+
if (textOpen) {
|
|
1319
|
+
yield sse({ type: "text-end", id: "0" });
|
|
1320
|
+
textOpen = false;
|
|
1321
|
+
}
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const frame = encodeStreamPart(part);
|
|
1325
|
+
if (frame) {
|
|
1326
|
+
yield frame;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (textOpen) {
|
|
1330
|
+
yield sse({ type: "text-end", id: "0" });
|
|
1331
|
+
}
|
|
1332
|
+
yield sse({ type: "finish-step" });
|
|
1333
|
+
yield sse({ type: "finish", finishReason });
|
|
1334
|
+
yield "data: [DONE]\n\n";
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/routes/message-utils.ts
|
|
1338
|
+
function normalizeMessage(raw) {
|
|
1339
|
+
const role = raw.role;
|
|
1340
|
+
if (typeof raw.content === "string") {
|
|
1341
|
+
return { role, content: raw.content };
|
|
1342
|
+
}
|
|
1343
|
+
if (Array.isArray(raw.content)) {
|
|
1344
|
+
return { role, content: raw.content };
|
|
1345
|
+
}
|
|
1346
|
+
if (Array.isArray(raw.parts)) {
|
|
1347
|
+
const textParts = raw.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text);
|
|
1348
|
+
if (textParts.length > 0) {
|
|
1349
|
+
return { role, content: textParts.join("") };
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return { role, content: "" };
|
|
1353
|
+
}
|
|
1354
|
+
function validateMessageContent(msg, opts) {
|
|
1355
|
+
const content = msg.content;
|
|
1356
|
+
if (Array.isArray(msg.parts)) {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
if (typeof content === "string") {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
if (Array.isArray(content)) {
|
|
1363
|
+
for (const part of content) {
|
|
1364
|
+
if (typeof part !== "object" || part === null) {
|
|
1365
|
+
return "message.content array elements must be non-null objects";
|
|
1366
|
+
}
|
|
1367
|
+
const partObj = part;
|
|
1368
|
+
if (typeof partObj.type !== "string") {
|
|
1369
|
+
return 'each message.content array element must have a string "type" property';
|
|
1370
|
+
}
|
|
1371
|
+
if (partObj.type === "text" && typeof partObj.text !== "string") {
|
|
1372
|
+
return 'message.content elements with type "text" must have a string "text" property';
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
if ((content === null || content === void 0) && opts?.allowEmptyContent) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
return "message.content must be a string, an array, or include parts";
|
|
1381
|
+
}
|
|
1382
|
+
|
|
439
1383
|
// src/routes/ai-routes.ts
|
|
440
1384
|
var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
441
1385
|
function validateMessage(raw) {
|
|
@@ -446,22 +1390,30 @@ function validateMessage(raw) {
|
|
|
446
1390
|
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
447
1391
|
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
448
1392
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
return null;
|
|
1393
|
+
const allowEmpty = msg.role === "assistant" || msg.role === "tool";
|
|
1394
|
+
return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
|
|
453
1395
|
}
|
|
454
1396
|
function buildAIRoutes(aiService, conversationService, logger) {
|
|
455
1397
|
return [
|
|
456
1398
|
// ── Chat ────────────────────────────────────────────────────
|
|
1399
|
+
//
|
|
1400
|
+
// Dual-mode endpoint compatible with both the legacy ObjectStack
|
|
1401
|
+
// format (`{ messages, options }`) and the Vercel AI SDK useChat
|
|
1402
|
+
// flat format (`{ messages, system, model, stream, … }`).
|
|
1403
|
+
//
|
|
1404
|
+
// Behaviour:
|
|
1405
|
+
// • `stream !== false` → Vercel Data Stream Protocol (SSE)
|
|
1406
|
+
// • `stream === false` → JSON response (legacy)
|
|
1407
|
+
//
|
|
457
1408
|
{
|
|
458
1409
|
method: "POST",
|
|
459
1410
|
path: "/api/v1/ai/chat",
|
|
460
|
-
description: "
|
|
1411
|
+
description: "Chat completion (supports Vercel AI Data Stream Protocol)",
|
|
461
1412
|
auth: true,
|
|
462
1413
|
permissions: ["ai:chat"],
|
|
463
1414
|
handler: async (req) => {
|
|
464
|
-
const
|
|
1415
|
+
const body = req.body ?? {};
|
|
1416
|
+
const messages = body.messages;
|
|
465
1417
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
466
1418
|
return { status: 400, body: { error: "messages array is required" } };
|
|
467
1419
|
}
|
|
@@ -469,8 +1421,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
469
1421
|
const err = validateMessage(msg);
|
|
470
1422
|
if (err) return { status: 400, body: { error: err } };
|
|
471
1423
|
}
|
|
1424
|
+
const nested = body.options ?? {};
|
|
1425
|
+
const resolvedOptions = {
|
|
1426
|
+
...nested,
|
|
1427
|
+
...body.model != null && { model: body.model },
|
|
1428
|
+
...body.temperature != null && { temperature: body.temperature },
|
|
1429
|
+
...body.maxTokens != null && { maxTokens: body.maxTokens }
|
|
1430
|
+
};
|
|
1431
|
+
const rawSystemPrompt = body.system ?? body.systemPrompt;
|
|
1432
|
+
if (rawSystemPrompt != null && typeof rawSystemPrompt !== "string") {
|
|
1433
|
+
return { status: 400, body: { error: "system/systemPrompt must be a string" } };
|
|
1434
|
+
}
|
|
1435
|
+
const systemPrompt = rawSystemPrompt;
|
|
1436
|
+
const finalMessages = [
|
|
1437
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
1438
|
+
...messages.map((m) => normalizeMessage(m))
|
|
1439
|
+
];
|
|
1440
|
+
const wantStream = body.stream !== false;
|
|
1441
|
+
if (wantStream) {
|
|
1442
|
+
try {
|
|
1443
|
+
if (!aiService.streamChatWithTools) {
|
|
1444
|
+
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
1445
|
+
}
|
|
1446
|
+
const events = aiService.streamChatWithTools(finalMessages, resolvedOptions);
|
|
1447
|
+
return {
|
|
1448
|
+
status: 200,
|
|
1449
|
+
stream: true,
|
|
1450
|
+
vercelDataStream: true,
|
|
1451
|
+
contentType: "text/event-stream",
|
|
1452
|
+
headers: {
|
|
1453
|
+
"Content-Type": "text/event-stream",
|
|
1454
|
+
"Cache-Control": "no-cache",
|
|
1455
|
+
"Connection": "keep-alive",
|
|
1456
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
1457
|
+
},
|
|
1458
|
+
events: encodeVercelDataStream(events)
|
|
1459
|
+
};
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
logger.error("[AI Route] /chat stream error", err instanceof Error ? err : void 0);
|
|
1462
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
472
1465
|
try {
|
|
473
|
-
const result = await aiService.
|
|
1466
|
+
const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
|
|
474
1467
|
return { status: 200, body: result };
|
|
475
1468
|
} catch (err) {
|
|
476
1469
|
logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
|
|
@@ -498,7 +1491,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
498
1491
|
if (!aiService.streamChat) {
|
|
499
1492
|
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
500
1493
|
}
|
|
501
|
-
const events = aiService.streamChat(messages, options);
|
|
1494
|
+
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
502
1495
|
return { status: 200, stream: true, events };
|
|
503
1496
|
} catch (err) {
|
|
504
1497
|
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
@@ -676,17 +1669,41 @@ function validateAgentMessage(raw) {
|
|
|
676
1669
|
if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
|
|
677
1670
|
return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
|
|
678
1671
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
return null;
|
|
1672
|
+
const allowEmpty = msg.role === "assistant";
|
|
1673
|
+
return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
|
|
683
1674
|
}
|
|
684
1675
|
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
685
1676
|
return [
|
|
1677
|
+
// ── List active agents ──────────────────────────────────────
|
|
1678
|
+
{
|
|
1679
|
+
method: "GET",
|
|
1680
|
+
path: "/api/v1/ai/agents",
|
|
1681
|
+
description: "List all active AI agents",
|
|
1682
|
+
auth: true,
|
|
1683
|
+
permissions: ["ai:chat"],
|
|
1684
|
+
handler: async () => {
|
|
1685
|
+
try {
|
|
1686
|
+
const agents = await agentRuntime.listAgents();
|
|
1687
|
+
return { status: 200, body: { agents } };
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
logger.error(
|
|
1690
|
+
"[AI Route] /agents list error",
|
|
1691
|
+
err instanceof Error ? err : void 0
|
|
1692
|
+
);
|
|
1693
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
},
|
|
1697
|
+
// ── Chat with a specific agent ──────────────────────────────
|
|
1698
|
+
//
|
|
1699
|
+
// Dual-mode endpoint matching the general chat route behaviour:
|
|
1700
|
+
// • `stream !== false` → Vercel Data Stream Protocol (SSE)
|
|
1701
|
+
// • `stream === false` → JSON response (legacy)
|
|
1702
|
+
//
|
|
686
1703
|
{
|
|
687
1704
|
method: "POST",
|
|
688
1705
|
path: "/api/v1/ai/agents/:agentName/chat",
|
|
689
|
-
description: "Chat with a specific AI agent",
|
|
1706
|
+
description: "Chat with a specific AI agent (supports Vercel AI Data Stream Protocol)",
|
|
690
1707
|
auth: true,
|
|
691
1708
|
permissions: ["ai:chat", "ai:agents"],
|
|
692
1709
|
handler: async (req) => {
|
|
@@ -694,11 +1711,12 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
|
694
1711
|
if (!agentName) {
|
|
695
1712
|
return { status: 400, body: { error: "agentName parameter is required" } };
|
|
696
1713
|
}
|
|
1714
|
+
const body = req.body ?? {};
|
|
697
1715
|
const {
|
|
698
1716
|
messages: rawMessages,
|
|
699
1717
|
context: chatContext,
|
|
700
1718
|
options: extraOptions
|
|
701
|
-
} =
|
|
1719
|
+
} = body;
|
|
702
1720
|
if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
|
|
703
1721
|
return { status: 400, body: { error: "messages array is required" } };
|
|
704
1722
|
}
|
|
@@ -726,21 +1744,150 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
|
726
1744
|
if (ALLOWED_KEYS.has(key)) {
|
|
727
1745
|
safeOverrides[key] = extraOptions[key];
|
|
728
1746
|
}
|
|
729
|
-
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const mergedOptions = { ...agentOptions, ...safeOverrides };
|
|
1750
|
+
const fullMessages = [
|
|
1751
|
+
...systemMessages,
|
|
1752
|
+
...rawMessages.map((m) => normalizeMessage(m))
|
|
1753
|
+
];
|
|
1754
|
+
const chatWithToolsOptions = {
|
|
1755
|
+
...mergedOptions,
|
|
1756
|
+
maxIterations: agent.planning?.maxIterations
|
|
1757
|
+
};
|
|
1758
|
+
const wantStream = body.stream !== false;
|
|
1759
|
+
if (wantStream) {
|
|
1760
|
+
if (!aiService.streamChatWithTools) {
|
|
1761
|
+
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
1762
|
+
}
|
|
1763
|
+
const events = aiService.streamChatWithTools(fullMessages, chatWithToolsOptions);
|
|
1764
|
+
return {
|
|
1765
|
+
status: 200,
|
|
1766
|
+
stream: true,
|
|
1767
|
+
vercelDataStream: true,
|
|
1768
|
+
contentType: "text/event-stream",
|
|
1769
|
+
headers: {
|
|
1770
|
+
"Content-Type": "text/event-stream",
|
|
1771
|
+
"Cache-Control": "no-cache",
|
|
1772
|
+
"Connection": "keep-alive",
|
|
1773
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
1774
|
+
},
|
|
1775
|
+
events: encodeVercelDataStream(events)
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
const result = await aiService.chatWithTools(fullMessages, chatWithToolsOptions);
|
|
1779
|
+
return { status: 200, body: result };
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
logger.error(
|
|
1782
|
+
"[AI Route] /agents/:agentName/chat error",
|
|
1783
|
+
err instanceof Error ? err : void 0
|
|
1784
|
+
);
|
|
1785
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
];
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// src/routes/tool-routes.ts
|
|
1793
|
+
function extractOutputValue(output) {
|
|
1794
|
+
if (!output) return "";
|
|
1795
|
+
if (typeof output === "string") return output;
|
|
1796
|
+
if (typeof output === "object" && "value" in output) {
|
|
1797
|
+
return String(output.value ?? "");
|
|
1798
|
+
}
|
|
1799
|
+
return JSON.stringify(output);
|
|
1800
|
+
}
|
|
1801
|
+
function buildToolRoutes(aiService, logger) {
|
|
1802
|
+
return [
|
|
1803
|
+
// ── List registered tools ──────────────────────────────────────
|
|
1804
|
+
{
|
|
1805
|
+
method: "GET",
|
|
1806
|
+
path: "/api/v1/ai/tools",
|
|
1807
|
+
description: "List all registered AI tools",
|
|
1808
|
+
auth: true,
|
|
1809
|
+
permissions: ["ai:tools"],
|
|
1810
|
+
handler: async () => {
|
|
1811
|
+
try {
|
|
1812
|
+
const tools = aiService.toolRegistry.getAll();
|
|
1813
|
+
return {
|
|
1814
|
+
status: 200,
|
|
1815
|
+
body: {
|
|
1816
|
+
tools: tools.map((t) => ({
|
|
1817
|
+
name: t.name,
|
|
1818
|
+
description: t.description,
|
|
1819
|
+
category: t.category
|
|
1820
|
+
}))
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
logger.error(
|
|
1825
|
+
"[AI Route] /tools list error",
|
|
1826
|
+
err instanceof Error ? err : void 0
|
|
1827
|
+
);
|
|
1828
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1832
|
+
// ── Execute a tool ──────────────────────────────────────────────
|
|
1833
|
+
//
|
|
1834
|
+
// Executes a tool with the provided parameters.
|
|
1835
|
+
// This is intended for testing/playground use.
|
|
1836
|
+
//
|
|
1837
|
+
{
|
|
1838
|
+
method: "POST",
|
|
1839
|
+
path: "/api/v1/ai/tools/:toolName/execute",
|
|
1840
|
+
description: "Execute a tool with parameters (playground/testing)",
|
|
1841
|
+
auth: true,
|
|
1842
|
+
permissions: ["ai:tools", "ai:execute"],
|
|
1843
|
+
handler: async (req) => {
|
|
1844
|
+
const toolName = req.params?.toolName;
|
|
1845
|
+
if (!toolName) {
|
|
1846
|
+
return { status: 400, body: { error: "toolName parameter is required" } };
|
|
1847
|
+
}
|
|
1848
|
+
const body = req.body ?? {};
|
|
1849
|
+
const { parameters } = body;
|
|
1850
|
+
if (!parameters || typeof parameters !== "object") {
|
|
1851
|
+
return { status: 400, body: { error: "parameters object is required" } };
|
|
1852
|
+
}
|
|
1853
|
+
try {
|
|
1854
|
+
if (!aiService.toolRegistry.has(toolName)) {
|
|
1855
|
+
return { status: 404, body: { error: `Tool "${toolName}" not found` } };
|
|
1856
|
+
}
|
|
1857
|
+
const startTime = Date.now();
|
|
1858
|
+
const toolCallPart = {
|
|
1859
|
+
type: "tool-call",
|
|
1860
|
+
toolCallId: `playground-${Date.now()}`,
|
|
1861
|
+
toolName,
|
|
1862
|
+
input: parameters
|
|
1863
|
+
};
|
|
1864
|
+
const result = await aiService.toolRegistry.execute(toolCallPart);
|
|
1865
|
+
const duration = Date.now() - startTime;
|
|
1866
|
+
if (result.isError) {
|
|
1867
|
+
const errorMsg = extractOutputValue(result.output);
|
|
1868
|
+
logger.error(
|
|
1869
|
+
`[AI Route] Tool execution error: ${toolName}`,
|
|
1870
|
+
new Error(errorMsg)
|
|
1871
|
+
);
|
|
1872
|
+
return {
|
|
1873
|
+
status: 500,
|
|
1874
|
+
body: {
|
|
1875
|
+
error: errorMsg,
|
|
1876
|
+
duration
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
730
1879
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
});
|
|
740
|
-
return { status: 200, body: result };
|
|
1880
|
+
return {
|
|
1881
|
+
status: 200,
|
|
1882
|
+
body: {
|
|
1883
|
+
result: extractOutputValue(result.output),
|
|
1884
|
+
duration,
|
|
1885
|
+
toolName
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
741
1888
|
} catch (err) {
|
|
742
1889
|
logger.error(
|
|
743
|
-
"[AI Route] /
|
|
1890
|
+
"[AI Route] /tools/:toolName/execute error",
|
|
744
1891
|
err instanceof Error ? err : void 0
|
|
745
1892
|
);
|
|
746
1893
|
return { status: 500, body: { error: "Internal AI service error" } };
|
|
@@ -842,13 +1989,35 @@ var ObjectQLConversationService = class {
|
|
|
842
1989
|
}
|
|
843
1990
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
844
1991
|
const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
|
|
1992
|
+
let contentStr;
|
|
1993
|
+
let toolCallsJson = null;
|
|
1994
|
+
let toolCallId = null;
|
|
1995
|
+
if (message.role === "system" || message.role === "user") {
|
|
1996
|
+
contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
1997
|
+
} else if (message.role === "assistant") {
|
|
1998
|
+
if (typeof message.content === "string") {
|
|
1999
|
+
contentStr = message.content;
|
|
2000
|
+
} else {
|
|
2001
|
+
const parts = message.content;
|
|
2002
|
+
const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
|
|
2003
|
+
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
|
2004
|
+
contentStr = textParts.join("");
|
|
2005
|
+
if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
|
|
2006
|
+
}
|
|
2007
|
+
} else if (message.role === "tool") {
|
|
2008
|
+
contentStr = JSON.stringify(message.content);
|
|
2009
|
+
const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
|
|
2010
|
+
if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
|
|
2011
|
+
} else {
|
|
2012
|
+
contentStr = "";
|
|
2013
|
+
}
|
|
845
2014
|
await this.engine.insert(MESSAGES_OBJECT, {
|
|
846
2015
|
id: msgId,
|
|
847
2016
|
conversation_id: conversationId,
|
|
848
2017
|
role: message.role,
|
|
849
|
-
content:
|
|
850
|
-
tool_calls:
|
|
851
|
-
tool_call_id:
|
|
2018
|
+
content: contentStr,
|
|
2019
|
+
tool_calls: toolCallsJson,
|
|
2020
|
+
tool_call_id: toolCallId,
|
|
852
2021
|
created_at: now
|
|
853
2022
|
});
|
|
854
2023
|
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
@@ -893,21 +2062,42 @@ var ObjectQLConversationService = class {
|
|
|
893
2062
|
};
|
|
894
2063
|
}
|
|
895
2064
|
/**
|
|
896
|
-
* Map a database row to
|
|
2065
|
+
* Map a database row to a ModelMessage.
|
|
897
2066
|
*/
|
|
898
2067
|
toMessage(row) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
2068
|
+
switch (row.role) {
|
|
2069
|
+
case "system":
|
|
2070
|
+
return { role: "system", content: row.content };
|
|
2071
|
+
case "user":
|
|
2072
|
+
return { role: "user", content: row.content };
|
|
2073
|
+
case "assistant": {
|
|
2074
|
+
const toolCalls = this.safeParse(row.tool_calls);
|
|
2075
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
2076
|
+
const content = [];
|
|
2077
|
+
if (row.content) content.push({ type: "text", text: row.content });
|
|
2078
|
+
content.push(...toolCalls);
|
|
2079
|
+
return { role: "assistant", content };
|
|
2080
|
+
}
|
|
2081
|
+
return { role: "assistant", content: row.content };
|
|
2082
|
+
}
|
|
2083
|
+
case "tool": {
|
|
2084
|
+
const toolResults = this.safeParse(row.content);
|
|
2085
|
+
if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
|
|
2086
|
+
return { role: "tool", content: toolResults };
|
|
2087
|
+
}
|
|
2088
|
+
return {
|
|
2089
|
+
role: "tool",
|
|
2090
|
+
content: [{
|
|
2091
|
+
type: "tool-result",
|
|
2092
|
+
toolCallId: row.tool_call_id ?? "",
|
|
2093
|
+
toolName: "unknown",
|
|
2094
|
+
output: { type: "text", value: row.content }
|
|
2095
|
+
}]
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
default:
|
|
2099
|
+
return { role: "user", content: row.content };
|
|
909
2100
|
}
|
|
910
|
-
return msg;
|
|
911
2101
|
}
|
|
912
2102
|
};
|
|
913
2103
|
|
|
@@ -1046,271 +2236,38 @@ var AiMessageObject = import_data2.ObjectSchema.create({
|
|
|
1046
2236
|
}
|
|
1047
2237
|
});
|
|
1048
2238
|
|
|
1049
|
-
// src/
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
var LIST_OBJECTS_TOOL = {
|
|
1053
|
-
name: "list_objects",
|
|
1054
|
-
description: "List all available data objects (tables) in the system. Returns object names and labels.",
|
|
1055
|
-
parameters: {
|
|
1056
|
-
type: "object",
|
|
1057
|
-
properties: {},
|
|
1058
|
-
additionalProperties: false
|
|
1059
|
-
}
|
|
1060
|
-
};
|
|
1061
|
-
var DESCRIBE_OBJECT_TOOL = {
|
|
1062
|
-
name: "describe_object",
|
|
1063
|
-
description: "Get the schema (fields, types, labels) of a specific data object. Use this to understand the structure of a table before querying it.",
|
|
1064
|
-
parameters: {
|
|
1065
|
-
type: "object",
|
|
1066
|
-
properties: {
|
|
1067
|
-
objectName: {
|
|
1068
|
-
type: "string",
|
|
1069
|
-
description: "The snake_case name of the object to describe"
|
|
1070
|
-
}
|
|
1071
|
-
},
|
|
1072
|
-
required: ["objectName"],
|
|
1073
|
-
additionalProperties: false
|
|
1074
|
-
}
|
|
1075
|
-
};
|
|
1076
|
-
var QUERY_RECORDS_TOOL = {
|
|
1077
|
-
name: "query_records",
|
|
1078
|
-
description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
|
|
1079
|
-
parameters: {
|
|
1080
|
-
type: "object",
|
|
1081
|
-
properties: {
|
|
1082
|
-
objectName: {
|
|
1083
|
-
type: "string",
|
|
1084
|
-
description: "The snake_case name of the object to query"
|
|
1085
|
-
},
|
|
1086
|
-
where: {
|
|
1087
|
-
type: "object",
|
|
1088
|
-
description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
|
|
1089
|
-
},
|
|
1090
|
-
fields: {
|
|
1091
|
-
type: "array",
|
|
1092
|
-
items: { type: "string" },
|
|
1093
|
-
description: "List of field names to return (omit for all fields)"
|
|
1094
|
-
},
|
|
1095
|
-
orderBy: {
|
|
1096
|
-
type: "array",
|
|
1097
|
-
items: {
|
|
1098
|
-
type: "object",
|
|
1099
|
-
properties: {
|
|
1100
|
-
field: { type: "string" },
|
|
1101
|
-
order: { type: "string", enum: ["asc", "desc"] }
|
|
1102
|
-
}
|
|
1103
|
-
},
|
|
1104
|
-
description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
|
|
1105
|
-
},
|
|
1106
|
-
limit: {
|
|
1107
|
-
type: "number",
|
|
1108
|
-
description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
|
|
1109
|
-
},
|
|
1110
|
-
offset: {
|
|
1111
|
-
type: "number",
|
|
1112
|
-
description: "Number of records to skip for pagination"
|
|
1113
|
-
}
|
|
1114
|
-
},
|
|
1115
|
-
required: ["objectName"],
|
|
1116
|
-
additionalProperties: false
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
1119
|
-
var GET_RECORD_TOOL = {
|
|
1120
|
-
name: "get_record",
|
|
1121
|
-
description: "Get a single record by its ID from a data object.",
|
|
1122
|
-
parameters: {
|
|
1123
|
-
type: "object",
|
|
1124
|
-
properties: {
|
|
1125
|
-
objectName: {
|
|
1126
|
-
type: "string",
|
|
1127
|
-
description: "The snake_case name of the object"
|
|
1128
|
-
},
|
|
1129
|
-
recordId: {
|
|
1130
|
-
type: "string",
|
|
1131
|
-
description: "The unique ID of the record"
|
|
1132
|
-
},
|
|
1133
|
-
fields: {
|
|
1134
|
-
type: "array",
|
|
1135
|
-
items: { type: "string" },
|
|
1136
|
-
description: "List of field names to return (omit for all fields)"
|
|
1137
|
-
}
|
|
1138
|
-
},
|
|
1139
|
-
required: ["objectName", "recordId"],
|
|
1140
|
-
additionalProperties: false
|
|
1141
|
-
}
|
|
1142
|
-
};
|
|
1143
|
-
var AGGREGATE_DATA_TOOL = {
|
|
1144
|
-
name: "aggregate_data",
|
|
1145
|
-
description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
|
|
1146
|
-
parameters: {
|
|
1147
|
-
type: "object",
|
|
1148
|
-
properties: {
|
|
1149
|
-
objectName: {
|
|
1150
|
-
type: "string",
|
|
1151
|
-
description: "The snake_case name of the object to aggregate"
|
|
1152
|
-
},
|
|
1153
|
-
aggregations: {
|
|
1154
|
-
type: "array",
|
|
1155
|
-
items: {
|
|
1156
|
-
type: "object",
|
|
1157
|
-
properties: {
|
|
1158
|
-
function: {
|
|
1159
|
-
type: "string",
|
|
1160
|
-
enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
|
|
1161
|
-
description: "Aggregation function"
|
|
1162
|
-
},
|
|
1163
|
-
field: {
|
|
1164
|
-
type: "string",
|
|
1165
|
-
description: "Field to aggregate (optional for count)"
|
|
1166
|
-
},
|
|
1167
|
-
alias: {
|
|
1168
|
-
type: "string",
|
|
1169
|
-
description: "Result column alias"
|
|
1170
|
-
}
|
|
1171
|
-
},
|
|
1172
|
-
required: ["function", "alias"]
|
|
1173
|
-
},
|
|
1174
|
-
description: "Aggregation definitions"
|
|
1175
|
-
},
|
|
1176
|
-
groupBy: {
|
|
1177
|
-
type: "array",
|
|
1178
|
-
items: { type: "string" },
|
|
1179
|
-
description: "Fields to group by"
|
|
1180
|
-
},
|
|
1181
|
-
where: {
|
|
1182
|
-
type: "object",
|
|
1183
|
-
description: "Filter conditions applied before aggregation"
|
|
1184
|
-
}
|
|
1185
|
-
},
|
|
1186
|
-
required: ["objectName", "aggregations"],
|
|
1187
|
-
additionalProperties: false
|
|
1188
|
-
}
|
|
1189
|
-
};
|
|
1190
|
-
var DATA_TOOL_DEFINITIONS = [
|
|
1191
|
-
LIST_OBJECTS_TOOL,
|
|
1192
|
-
DESCRIBE_OBJECT_TOOL,
|
|
1193
|
-
QUERY_RECORDS_TOOL,
|
|
1194
|
-
GET_RECORD_TOOL,
|
|
1195
|
-
AGGREGATE_DATA_TOOL
|
|
1196
|
-
];
|
|
1197
|
-
function createListObjectsHandler(ctx) {
|
|
1198
|
-
return async () => {
|
|
1199
|
-
const objects = await ctx.metadataService.listObjects();
|
|
1200
|
-
const summary = objects.map((o) => ({
|
|
1201
|
-
name: o.name,
|
|
1202
|
-
label: o.label ?? o.name
|
|
1203
|
-
}));
|
|
1204
|
-
return JSON.stringify(summary);
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
|
-
function createDescribeObjectHandler(ctx) {
|
|
1208
|
-
return async (args) => {
|
|
1209
|
-
const { objectName } = args;
|
|
1210
|
-
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1211
|
-
if (!objectDef) {
|
|
1212
|
-
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1213
|
-
}
|
|
1214
|
-
const def = objectDef;
|
|
1215
|
-
const fields = def.fields ?? {};
|
|
1216
|
-
const fieldSummary = {};
|
|
1217
|
-
for (const [key, f] of Object.entries(fields)) {
|
|
1218
|
-
fieldSummary[key] = {
|
|
1219
|
-
type: f.type,
|
|
1220
|
-
label: f.label ?? key,
|
|
1221
|
-
required: f.required ?? false,
|
|
1222
|
-
...f.reference ? { reference: f.reference } : {},
|
|
1223
|
-
...f.options ? { options: f.options } : {}
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
return JSON.stringify({
|
|
1227
|
-
name: def.name,
|
|
1228
|
-
label: def.label ?? def.name,
|
|
1229
|
-
fields: fieldSummary
|
|
1230
|
-
});
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
function createQueryRecordsHandler(ctx) {
|
|
1234
|
-
return async (args) => {
|
|
1235
|
-
const {
|
|
1236
|
-
objectName,
|
|
1237
|
-
where,
|
|
1238
|
-
fields,
|
|
1239
|
-
orderBy,
|
|
1240
|
-
limit,
|
|
1241
|
-
offset
|
|
1242
|
-
} = args;
|
|
1243
|
-
const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
|
|
1244
|
-
const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
|
|
1245
|
-
const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
|
|
1246
|
-
const records = await ctx.dataEngine.find(objectName, {
|
|
1247
|
-
where,
|
|
1248
|
-
fields,
|
|
1249
|
-
orderBy,
|
|
1250
|
-
limit: safeLimit,
|
|
1251
|
-
offset: safeOffset
|
|
1252
|
-
});
|
|
1253
|
-
return JSON.stringify({ count: records.length, records });
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
function createGetRecordHandler(ctx) {
|
|
1257
|
-
return async (args) => {
|
|
1258
|
-
const { objectName, recordId, fields } = args;
|
|
1259
|
-
const record = await ctx.dataEngine.findOne(objectName, {
|
|
1260
|
-
where: { id: recordId },
|
|
1261
|
-
fields
|
|
1262
|
-
});
|
|
1263
|
-
if (!record) {
|
|
1264
|
-
return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
|
|
1265
|
-
}
|
|
1266
|
-
return JSON.stringify(record);
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
var VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1270
|
-
"count",
|
|
1271
|
-
"sum",
|
|
1272
|
-
"avg",
|
|
1273
|
-
"min",
|
|
1274
|
-
"max",
|
|
1275
|
-
"count_distinct"
|
|
1276
|
-
]);
|
|
1277
|
-
function createAggregateDataHandler(ctx) {
|
|
1278
|
-
return async (args) => {
|
|
1279
|
-
const { objectName, aggregations, groupBy, where } = args;
|
|
1280
|
-
for (const a of aggregations) {
|
|
1281
|
-
if (!VALID_AGG_FUNCTIONS.has(a.function)) {
|
|
1282
|
-
return JSON.stringify({
|
|
1283
|
-
error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
const result = await ctx.dataEngine.aggregate(objectName, {
|
|
1288
|
-
where,
|
|
1289
|
-
groupBy,
|
|
1290
|
-
aggregations: aggregations.map((a) => ({
|
|
1291
|
-
function: a.function,
|
|
1292
|
-
field: a.field,
|
|
1293
|
-
alias: a.alias
|
|
1294
|
-
}))
|
|
1295
|
-
});
|
|
1296
|
-
return JSON.stringify(result);
|
|
1297
|
-
};
|
|
1298
|
-
}
|
|
1299
|
-
function registerDataTools(registry, context) {
|
|
1300
|
-
registry.register(LIST_OBJECTS_TOOL, createListObjectsHandler(context));
|
|
1301
|
-
registry.register(DESCRIBE_OBJECT_TOOL, createDescribeObjectHandler(context));
|
|
1302
|
-
registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
|
|
1303
|
-
registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
|
|
1304
|
-
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
1305
|
-
}
|
|
2239
|
+
// src/plugin.ts
|
|
2240
|
+
init_data_tools();
|
|
2241
|
+
init_metadata_tools();
|
|
1306
2242
|
|
|
1307
2243
|
// src/agent-runtime.ts
|
|
1308
|
-
var
|
|
2244
|
+
var import_ai7 = require("@objectstack/spec/ai");
|
|
1309
2245
|
var AgentRuntime = class {
|
|
1310
2246
|
constructor(metadataService) {
|
|
1311
2247
|
this.metadataService = metadataService;
|
|
1312
2248
|
}
|
|
1313
2249
|
// ── Public API ────────────────────────────────────────────────
|
|
2250
|
+
/**
|
|
2251
|
+
* List all active agents registered in the metadata service.
|
|
2252
|
+
*
|
|
2253
|
+
* Returns a summary for each agent (name, label, role) suitable
|
|
2254
|
+
* for populating an agent selector dropdown in the UI.
|
|
2255
|
+
*/
|
|
2256
|
+
async listAgents() {
|
|
2257
|
+
const rawItems = await this.metadataService.list("agent");
|
|
2258
|
+
const agents = [];
|
|
2259
|
+
for (const raw of rawItems) {
|
|
2260
|
+
const result = import_ai7.AgentSchema.safeParse(raw);
|
|
2261
|
+
if (result.success && result.data.active) {
|
|
2262
|
+
agents.push({
|
|
2263
|
+
name: result.data.name,
|
|
2264
|
+
label: result.data.label,
|
|
2265
|
+
role: result.data.role
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return agents;
|
|
2270
|
+
}
|
|
1314
2271
|
/**
|
|
1315
2272
|
* Load and validate an agent definition by name.
|
|
1316
2273
|
*
|
|
@@ -1322,7 +2279,7 @@ var AgentRuntime = class {
|
|
|
1322
2279
|
async loadAgent(agentName) {
|
|
1323
2280
|
const raw = await this.metadataService.get("agent", agentName);
|
|
1324
2281
|
if (!raw) return void 0;
|
|
1325
|
-
const result =
|
|
2282
|
+
const result = import_ai7.AgentSchema.safeParse(raw);
|
|
1326
2283
|
if (!result.success) {
|
|
1327
2284
|
return void 0;
|
|
1328
2285
|
}
|
|
@@ -1439,15 +2396,233 @@ Guidelines:
|
|
|
1439
2396
|
}
|
|
1440
2397
|
};
|
|
1441
2398
|
|
|
2399
|
+
// src/agents/metadata-assistant-agent.ts
|
|
2400
|
+
var METADATA_ASSISTANT_AGENT = {
|
|
2401
|
+
name: "metadata_assistant",
|
|
2402
|
+
label: "Metadata Assistant",
|
|
2403
|
+
role: "Schema Architect",
|
|
2404
|
+
instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
|
|
2405
|
+
|
|
2406
|
+
Capabilities:
|
|
2407
|
+
- Create new data objects (tables) with fields
|
|
2408
|
+
- Add fields (columns) to existing objects
|
|
2409
|
+
- Modify field properties (label, type, required, default value)
|
|
2410
|
+
- Delete fields from objects
|
|
2411
|
+
- List all registered metadata objects and their schemas
|
|
2412
|
+
- Describe the full schema of a specific object
|
|
2413
|
+
|
|
2414
|
+
Guidelines:
|
|
2415
|
+
1. Before creating a new object, use list_objects to check if a similar one already exists.
|
|
2416
|
+
2. Before modifying or deleting fields, use describe_object to understand the current schema.
|
|
2417
|
+
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
|
|
2418
|
+
4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
|
|
2419
|
+
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
|
|
2420
|
+
6. Explain what changes you are about to make before executing them.
|
|
2421
|
+
7. After making changes, confirm the result by describing the updated schema.
|
|
2422
|
+
8. For destructive operations (deleting fields), always warn the user about potential data loss.
|
|
2423
|
+
9. Always answer in the same language the user is using.
|
|
2424
|
+
10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
|
|
2425
|
+
model: {
|
|
2426
|
+
provider: "openai",
|
|
2427
|
+
model: "gpt-4",
|
|
2428
|
+
temperature: 0.2,
|
|
2429
|
+
maxTokens: 4096
|
|
2430
|
+
},
|
|
2431
|
+
tools: [
|
|
2432
|
+
{ type: "action", name: "create_object", description: "Create a new data object (table)" },
|
|
2433
|
+
{ type: "action", name: "add_field", description: "Add a field to an existing object" },
|
|
2434
|
+
{ type: "action", name: "modify_field", description: "Modify an existing field definition" },
|
|
2435
|
+
{ type: "action", name: "delete_field", description: "Delete a field from an object" },
|
|
2436
|
+
{ type: "query", name: "list_objects", description: "List all data objects" },
|
|
2437
|
+
{ type: "query", name: "describe_object", description: "Describe an object schema" }
|
|
2438
|
+
],
|
|
2439
|
+
active: true,
|
|
2440
|
+
visibility: "global",
|
|
2441
|
+
guardrails: {
|
|
2442
|
+
maxTokensPerInvocation: 8192,
|
|
2443
|
+
maxExecutionTimeSec: 60,
|
|
2444
|
+
blockedTopics: ["drop_database", "raw_sql", "system_tables"]
|
|
2445
|
+
},
|
|
2446
|
+
planning: {
|
|
2447
|
+
strategy: "react",
|
|
2448
|
+
maxIterations: 10,
|
|
2449
|
+
allowReplan: true
|
|
2450
|
+
},
|
|
2451
|
+
memory: {
|
|
2452
|
+
shortTerm: {
|
|
2453
|
+
maxMessages: 30,
|
|
2454
|
+
maxTokens: 8192
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
|
|
2459
|
+
// src/adapters/vercel-adapter.ts
|
|
2460
|
+
var import_ai8 = require("ai");
|
|
2461
|
+
function buildVercelOptions(options) {
|
|
2462
|
+
if (!options) return {};
|
|
2463
|
+
const opts = {};
|
|
2464
|
+
if (options.temperature != null) opts.temperature = options.temperature;
|
|
2465
|
+
if (options.maxTokens != null) opts.maxTokens = options.maxTokens;
|
|
2466
|
+
if (options.stop?.length) opts.stopSequences = options.stop;
|
|
2467
|
+
if (options.tools?.length) {
|
|
2468
|
+
const tools = {};
|
|
2469
|
+
for (const t of options.tools) {
|
|
2470
|
+
tools[t.name] = (0, import_ai8.tool)({
|
|
2471
|
+
description: t.description,
|
|
2472
|
+
inputSchema: (0, import_ai8.jsonSchema)(t.parameters)
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
opts.tools = tools;
|
|
2476
|
+
}
|
|
2477
|
+
if (options.toolChoice != null) {
|
|
2478
|
+
opts.toolChoice = options.toolChoice;
|
|
2479
|
+
}
|
|
2480
|
+
return opts;
|
|
2481
|
+
}
|
|
2482
|
+
var VercelLLMAdapter = class {
|
|
2483
|
+
constructor(config) {
|
|
2484
|
+
this.name = "vercel";
|
|
2485
|
+
this.model = config.model;
|
|
2486
|
+
}
|
|
2487
|
+
async chat(messages, options) {
|
|
2488
|
+
const result = await (0, import_ai8.generateText)({
|
|
2489
|
+
model: this.model,
|
|
2490
|
+
messages,
|
|
2491
|
+
...buildVercelOptions(options)
|
|
2492
|
+
});
|
|
2493
|
+
return {
|
|
2494
|
+
content: result.text,
|
|
2495
|
+
model: result.response?.modelId,
|
|
2496
|
+
toolCalls: result.toolCalls?.length ? result.toolCalls : void 0,
|
|
2497
|
+
usage: result.usage ? {
|
|
2498
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2499
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2500
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2501
|
+
} : void 0
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
async complete(prompt, options) {
|
|
2505
|
+
const result = await (0, import_ai8.generateText)({
|
|
2506
|
+
model: this.model,
|
|
2507
|
+
prompt,
|
|
2508
|
+
...buildVercelOptions(options)
|
|
2509
|
+
});
|
|
2510
|
+
return {
|
|
2511
|
+
content: result.text,
|
|
2512
|
+
model: result.response?.modelId,
|
|
2513
|
+
usage: result.usage ? {
|
|
2514
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2515
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2516
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2517
|
+
} : void 0
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
async *streamChat(messages, options) {
|
|
2521
|
+
const result = (0, import_ai8.streamText)({
|
|
2522
|
+
model: this.model,
|
|
2523
|
+
messages,
|
|
2524
|
+
...buildVercelOptions(options)
|
|
2525
|
+
});
|
|
2526
|
+
for await (const part of result.fullStream) {
|
|
2527
|
+
yield part;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
async embed(_input) {
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
"[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
async listModels() {
|
|
2536
|
+
return [];
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2539
|
+
|
|
1442
2540
|
// src/plugin.ts
|
|
1443
2541
|
var AIServicePlugin = class {
|
|
1444
2542
|
constructor(options = {}) {
|
|
1445
2543
|
this.name = "com.objectstack.service-ai";
|
|
1446
2544
|
this.version = "1.0.0";
|
|
1447
2545
|
this.type = "standard";
|
|
1448
|
-
this.dependencies = [];
|
|
2546
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
1449
2547
|
this.options = options;
|
|
1450
2548
|
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Auto-detect LLM provider from environment variables.
|
|
2551
|
+
*
|
|
2552
|
+
* Priority order:
|
|
2553
|
+
* 1. AI_GATEWAY_MODEL → Vercel AI Gateway
|
|
2554
|
+
* 2. OPENAI_API_KEY → OpenAI
|
|
2555
|
+
* 3. ANTHROPIC_API_KEY → Anthropic
|
|
2556
|
+
* 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
|
|
2557
|
+
* 5. Fallback → MemoryLLMAdapter
|
|
2558
|
+
*
|
|
2559
|
+
* Returns the adapter and a description for logging.
|
|
2560
|
+
*/
|
|
2561
|
+
async detectAdapter(ctx) {
|
|
2562
|
+
const gatewayModel = process.env.AI_GATEWAY_MODEL;
|
|
2563
|
+
if (gatewayModel) {
|
|
2564
|
+
try {
|
|
2565
|
+
const gatewayPkg = "@ai-sdk/gateway";
|
|
2566
|
+
const { gateway } = await import(
|
|
2567
|
+
/* webpackIgnore: true */
|
|
2568
|
+
gatewayPkg
|
|
2569
|
+
);
|
|
2570
|
+
const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
|
|
2571
|
+
return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
|
|
2572
|
+
} catch (err) {
|
|
2573
|
+
ctx.logger.warn(
|
|
2574
|
+
`[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
|
|
2575
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
const providerConfigs = [
|
|
2580
|
+
{
|
|
2581
|
+
envKey: "OPENAI_API_KEY",
|
|
2582
|
+
pkg: "@ai-sdk/openai",
|
|
2583
|
+
factory: "openai",
|
|
2584
|
+
defaultModel: "gpt-4o",
|
|
2585
|
+
displayName: "OpenAI"
|
|
2586
|
+
},
|
|
2587
|
+
{
|
|
2588
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
2589
|
+
pkg: "@ai-sdk/anthropic",
|
|
2590
|
+
factory: "anthropic",
|
|
2591
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
2592
|
+
displayName: "Anthropic"
|
|
2593
|
+
},
|
|
2594
|
+
{
|
|
2595
|
+
envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
2596
|
+
pkg: "@ai-sdk/google",
|
|
2597
|
+
factory: "google",
|
|
2598
|
+
defaultModel: "gemini-2.0-flash",
|
|
2599
|
+
displayName: "Google"
|
|
2600
|
+
}
|
|
2601
|
+
];
|
|
2602
|
+
for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
|
|
2603
|
+
if (process.env[envKey]) {
|
|
2604
|
+
try {
|
|
2605
|
+
const mod = await import(
|
|
2606
|
+
/* webpackIgnore: true */
|
|
2607
|
+
pkg
|
|
2608
|
+
);
|
|
2609
|
+
const createModel = mod[factory] ?? mod.default;
|
|
2610
|
+
if (typeof createModel === "function") {
|
|
2611
|
+
const modelId = process.env.AI_MODEL ?? defaultModel;
|
|
2612
|
+
const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
|
|
2613
|
+
return { adapter, description: `${displayName} (model: ${modelId})` };
|
|
2614
|
+
}
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
ctx.logger.warn(
|
|
2617
|
+
`[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
|
|
2618
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
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.");
|
|
2624
|
+
return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode - for testing only)" };
|
|
2625
|
+
}
|
|
1451
2626
|
async init(ctx) {
|
|
1452
2627
|
let hasExisting = false;
|
|
1453
2628
|
try {
|
|
@@ -1469,8 +2644,19 @@ var AIServicePlugin = class {
|
|
|
1469
2644
|
} catch {
|
|
1470
2645
|
}
|
|
1471
2646
|
}
|
|
2647
|
+
let adapter;
|
|
2648
|
+
let adapterDescription;
|
|
2649
|
+
if (this.options.adapter) {
|
|
2650
|
+
adapter = this.options.adapter;
|
|
2651
|
+
adapterDescription = `${adapter.name} (explicitly configured)`;
|
|
2652
|
+
} else {
|
|
2653
|
+
const detected = await this.detectAdapter(ctx);
|
|
2654
|
+
adapter = detected.adapter;
|
|
2655
|
+
adapterDescription = detected.description;
|
|
2656
|
+
}
|
|
2657
|
+
ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
|
|
1472
2658
|
const config = {
|
|
1473
|
-
adapter
|
|
2659
|
+
adapter,
|
|
1474
2660
|
logger: ctx.logger,
|
|
1475
2661
|
conversationService
|
|
1476
2662
|
};
|
|
@@ -1480,7 +2666,7 @@ var AIServicePlugin = class {
|
|
|
1480
2666
|
} else {
|
|
1481
2667
|
ctx.registerService("ai", this.service);
|
|
1482
2668
|
}
|
|
1483
|
-
ctx.
|
|
2669
|
+
ctx.getService("manifest").register({
|
|
1484
2670
|
id: "com.objectstack.service-ai",
|
|
1485
2671
|
name: "AI Service",
|
|
1486
2672
|
version: "1.0.0",
|
|
@@ -1493,40 +2679,113 @@ var AIServicePlugin = class {
|
|
|
1493
2679
|
ctx.logger.debug("[AI] Before chat", { messages });
|
|
1494
2680
|
});
|
|
1495
2681
|
}
|
|
2682
|
+
try {
|
|
2683
|
+
const setupNav = ctx.getService("setupNav");
|
|
2684
|
+
if (setupNav) {
|
|
2685
|
+
setupNav.contribute({
|
|
2686
|
+
areaId: "area_ai",
|
|
2687
|
+
items: [
|
|
2688
|
+
{ id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
|
|
2689
|
+
{ id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
|
|
2690
|
+
]
|
|
2691
|
+
});
|
|
2692
|
+
ctx.logger.info("[AI] Navigation items contributed to Setup App");
|
|
2693
|
+
}
|
|
2694
|
+
} catch {
|
|
2695
|
+
}
|
|
1496
2696
|
ctx.logger.info("[AI] Service initialized");
|
|
1497
2697
|
}
|
|
1498
2698
|
async start(ctx) {
|
|
1499
2699
|
if (!this.service) return;
|
|
2700
|
+
let metadataService;
|
|
2701
|
+
try {
|
|
2702
|
+
metadataService = ctx.getService("metadata");
|
|
2703
|
+
console.log("[AI Plugin] Retrieved metadata service:", !!metadataService, "has getRegisteredTypes:", typeof metadataService?.getRegisteredTypes);
|
|
2704
|
+
} catch (e) {
|
|
2705
|
+
console.log("[AI] Metadata service not available:", e.message);
|
|
2706
|
+
ctx.logger.debug("[AI] Metadata service not available");
|
|
2707
|
+
}
|
|
1500
2708
|
try {
|
|
1501
2709
|
const dataEngine = ctx.getService("data");
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
|
|
2710
|
+
if (dataEngine) {
|
|
2711
|
+
registerDataTools(this.service.toolRegistry, { dataEngine });
|
|
1505
2712
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
2713
|
+
if (metadataService) {
|
|
2714
|
+
const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
|
|
2715
|
+
for (const toolDef of DATA_TOOL_DEFINITIONS2) {
|
|
2716
|
+
const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
|
|
2717
|
+
if (!toolExists) {
|
|
2718
|
+
await metadataService.register("tool", toolDef.name, toolDef);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS2.length} data tools registered as metadata`);
|
|
2722
|
+
}
|
|
2723
|
+
if (metadataService) {
|
|
2724
|
+
try {
|
|
2725
|
+
const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
|
|
2726
|
+
if (!agentExists) {
|
|
2727
|
+
await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
|
|
2728
|
+
console.log("[AI] Registered data_chat agent to metadataService");
|
|
2729
|
+
ctx.logger.info("[AI] data_chat agent registered");
|
|
2730
|
+
} else {
|
|
2731
|
+
console.log("[AI] data_chat agent already exists, skipping");
|
|
2732
|
+
ctx.logger.debug("[AI] data_chat agent already exists, skipping auto-registration");
|
|
2733
|
+
}
|
|
2734
|
+
} catch (err) {
|
|
2735
|
+
ctx.logger.warn("[AI] Failed to register data_chat agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
|
|
2736
|
+
}
|
|
1512
2737
|
}
|
|
1513
2738
|
}
|
|
1514
2739
|
} catch {
|
|
1515
|
-
ctx.logger.debug("[AI] Data engine
|
|
2740
|
+
ctx.logger.debug("[AI] Data engine not available, skipping data tools");
|
|
2741
|
+
}
|
|
2742
|
+
if (metadataService) {
|
|
2743
|
+
try {
|
|
2744
|
+
registerMetadataTools(this.service.toolRegistry, { metadataService });
|
|
2745
|
+
ctx.logger.info("[AI] Built-in metadata tools registered");
|
|
2746
|
+
const { METADATA_TOOL_DEFINITIONS: METADATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_metadata_tools(), metadata_tools_exports));
|
|
2747
|
+
for (const toolDef of METADATA_TOOL_DEFINITIONS2) {
|
|
2748
|
+
const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
|
|
2749
|
+
if (!toolExists) {
|
|
2750
|
+
await metadataService.register("tool", toolDef.name, toolDef);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
ctx.logger.info(`[AI] ${METADATA_TOOL_DEFINITIONS2.length} metadata tools registered as metadata`);
|
|
2754
|
+
try {
|
|
2755
|
+
const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
|
|
2756
|
+
if (!agentExists) {
|
|
2757
|
+
await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
|
|
2758
|
+
console.log("[AI] Registered metadata_assistant agent to metadataService");
|
|
2759
|
+
ctx.logger.info("[AI] metadata_assistant agent registered");
|
|
2760
|
+
} else {
|
|
2761
|
+
console.log("[AI] metadata_assistant agent already exists, skipping");
|
|
2762
|
+
ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
|
|
2763
|
+
}
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
ctx.logger.warn("[AI] Failed to register metadata_assistant agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
|
|
2766
|
+
}
|
|
2767
|
+
} catch (err) {
|
|
2768
|
+
ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
|
|
2769
|
+
}
|
|
1516
2770
|
}
|
|
1517
2771
|
await ctx.trigger("ai:ready", this.service);
|
|
1518
2772
|
const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2773
|
+
const toolRoutes = buildToolRoutes(this.service, ctx.logger);
|
|
2774
|
+
routes.push(...toolRoutes);
|
|
2775
|
+
ctx.logger.info(`[AI] Tool routes registered (${toolRoutes.length} routes)`);
|
|
2776
|
+
if (metadataService) {
|
|
2777
|
+
const agentRuntime = new AgentRuntime(metadataService);
|
|
2778
|
+
const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
|
|
2779
|
+
routes.push(...agentRoutes);
|
|
2780
|
+
ctx.logger.info(`[AI] Agent routes registered (${agentRoutes.length} routes)`);
|
|
2781
|
+
} else {
|
|
1527
2782
|
ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
|
|
1528
2783
|
}
|
|
1529
2784
|
await ctx.trigger("ai:routes", routes);
|
|
2785
|
+
const kernel = ctx.getKernel();
|
|
2786
|
+
if (kernel) {
|
|
2787
|
+
kernel.__aiRoutes = routes;
|
|
2788
|
+
}
|
|
1530
2789
|
ctx.logger.info(
|
|
1531
2790
|
`[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
|
|
1532
2791
|
);
|
|
@@ -1535,6 +2794,11 @@ var AIServicePlugin = class {
|
|
|
1535
2794
|
this.service = void 0;
|
|
1536
2795
|
}
|
|
1537
2796
|
};
|
|
2797
|
+
|
|
2798
|
+
// src/index.ts
|
|
2799
|
+
init_data_tools();
|
|
2800
|
+
init_metadata_tools();
|
|
2801
|
+
init_metadata_tools();
|
|
1538
2802
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1539
2803
|
0 && (module.exports = {
|
|
1540
2804
|
AIService,
|
|
@@ -1545,11 +2809,24 @@ var AIServicePlugin = class {
|
|
|
1545
2809
|
DATA_CHAT_AGENT,
|
|
1546
2810
|
DATA_TOOL_DEFINITIONS,
|
|
1547
2811
|
InMemoryConversationService,
|
|
2812
|
+
METADATA_ASSISTANT_AGENT,
|
|
2813
|
+
METADATA_TOOL_DEFINITIONS,
|
|
1548
2814
|
MemoryLLMAdapter,
|
|
1549
2815
|
ObjectQLConversationService,
|
|
1550
2816
|
ToolRegistry,
|
|
2817
|
+
VercelLLMAdapter,
|
|
2818
|
+
addFieldTool,
|
|
1551
2819
|
buildAIRoutes,
|
|
1552
2820
|
buildAgentRoutes,
|
|
1553
|
-
|
|
2821
|
+
buildToolRoutes,
|
|
2822
|
+
createObjectTool,
|
|
2823
|
+
deleteFieldTool,
|
|
2824
|
+
describeObjectTool,
|
|
2825
|
+
encodeStreamPart,
|
|
2826
|
+
encodeVercelDataStream,
|
|
2827
|
+
listObjectsTool,
|
|
2828
|
+
modifyFieldTool,
|
|
2829
|
+
registerDataTools,
|
|
2830
|
+
registerMetadataTools
|
|
1554
2831
|
});
|
|
1555
2832
|
//# sourceMappingURL=index.cjs.map
|