@objectstack/service-ai 4.0.2 → 4.0.4
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 +10 -10
- package/CHANGELOG.md +16 -0
- package/README.md +293 -0
- package/dist/index.cjs +1871 -1648
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -27
- package/dist/index.d.ts +32 -27
- package/dist/index.js +2137 -1908
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/ai-service.test.ts +30 -1
- package/src/__tests__/auth-and-toolcalling.test.ts +51 -1
- package/src/__tests__/chatbot-features.test.ts +172 -24
- package/src/__tests__/metadata-tools.test.ts +35 -29
- package/src/__tests__/tool-routes.test.ts +191 -0
- package/src/__tests__/vercel-stream-encoder.test.ts +47 -0
- package/src/agents/metadata-assistant-agent.ts +4 -4
- package/src/ai-service.ts +7 -0
- package/src/index.ts +3 -2
- package/src/plugin.ts +85 -35
- package/src/routes/agent-routes.ts +43 -10
- package/src/routes/ai-routes.ts +3 -67
- 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/vercel-stream-encoder.ts +24 -0
- package/src/tools/data-tools.ts +4 -101
- package/src/tools/describe-object.tool.ts +31 -0
- package/src/tools/index.ts +2 -2
- package/src/tools/{list-metadata-objects.tool.ts → list-objects.tool.ts} +9 -9
- package/src/tools/metadata-tools.ts +8 -8
- package/vitest.config.ts +23 -0
- package/src/tools/describe-metadata-object.tool.ts +0 -32
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,330 +20,1077 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
20
|
};
|
|
18
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
22
|
|
|
20
|
-
// src/
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
23
|
-
|
|
24
|
-
AIServicePlugin: () => AIServicePlugin,
|
|
25
|
-
AgentRuntime: () => AgentRuntime,
|
|
26
|
-
AiConversationObject: () => AiConversationObject,
|
|
27
|
-
AiMessageObject: () => AiMessageObject,
|
|
28
|
-
DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
|
|
23
|
+
// src/tools/data-tools.ts
|
|
24
|
+
var data_tools_exports = {};
|
|
25
|
+
__export(data_tools_exports, {
|
|
26
|
+
AGGREGATE_DATA_TOOL: () => AGGREGATE_DATA_TOOL,
|
|
29
27
|
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
MemoryLLMAdapter: () => MemoryLLMAdapter,
|
|
34
|
-
ObjectQLConversationService: () => ObjectQLConversationService,
|
|
35
|
-
ToolRegistry: () => ToolRegistry,
|
|
36
|
-
VercelLLMAdapter: () => VercelLLMAdapter,
|
|
37
|
-
addFieldTool: () => addFieldTool,
|
|
38
|
-
buildAIRoutes: () => buildAIRoutes,
|
|
39
|
-
buildAgentRoutes: () => buildAgentRoutes,
|
|
40
|
-
createObjectTool: () => createObjectTool,
|
|
41
|
-
deleteFieldTool: () => deleteFieldTool,
|
|
42
|
-
describeMetadataObjectTool: () => describeMetadataObjectTool,
|
|
43
|
-
encodeStreamPart: () => encodeStreamPart,
|
|
44
|
-
encodeVercelDataStream: () => encodeVercelDataStream,
|
|
45
|
-
listMetadataObjectsTool: () => listMetadataObjectsTool,
|
|
46
|
-
modifyFieldTool: () => modifyFieldTool,
|
|
47
|
-
registerDataTools: () => registerDataTools,
|
|
48
|
-
registerMetadataTools: () => registerMetadataTools
|
|
28
|
+
GET_RECORD_TOOL: () => GET_RECORD_TOOL,
|
|
29
|
+
QUERY_RECORDS_TOOL: () => QUERY_RECORDS_TOOL,
|
|
30
|
+
registerDataTools: () => registerDataTools
|
|
49
31
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
143
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
}
|
|
76
167
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
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
|
+
]);
|
|
91
228
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
});
|
|
98
290
|
}
|
|
99
|
-
};
|
|
291
|
+
});
|
|
100
292
|
|
|
101
|
-
// src/tools/
|
|
102
|
-
var
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
});
|
|
106
356
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
});
|
|
115
397
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
});
|
|
122
432
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
});
|
|
128
462
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
});
|
|
148
489
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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' });
|
|
162
512
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const content = await handler(args);
|
|
166
|
-
return {
|
|
167
|
-
type: "tool-result",
|
|
168
|
-
toolCallId: toolCall.toolCallId,
|
|
169
|
-
toolName: toolCall.toolName,
|
|
170
|
-
output: { type: "text", value: content }
|
|
171
|
-
};
|
|
172
|
-
} catch (err) {
|
|
173
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
-
return {
|
|
175
|
-
type: "tool-result",
|
|
176
|
-
toolCallId: toolCall.toolCallId,
|
|
177
|
-
toolName: toolCall.toolName,
|
|
178
|
-
output: { type: "text", value: message },
|
|
179
|
-
isError: true
|
|
180
|
-
};
|
|
513
|
+
if (!isSnakeCase(name)) {
|
|
514
|
+
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
|
|
181
515
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
messages: [],
|
|
213
|
-
createdAt: now,
|
|
214
|
-
updatedAt: now,
|
|
215
|
-
metadata: options.metadata
|
|
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 } : {}
|
|
216
546
|
};
|
|
217
|
-
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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' });
|
|
227
560
|
}
|
|
228
|
-
if (
|
|
229
|
-
|
|
561
|
+
if (!isSnakeCase(objectName)) {
|
|
562
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
230
563
|
}
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
235
575
|
}
|
|
236
576
|
}
|
|
237
|
-
|
|
238
|
-
|
|
577
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
578
|
+
if (!objectDef) {
|
|
579
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
239
580
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const conversation = this.store.get(conversationId);
|
|
244
|
-
if (!conversation) {
|
|
245
|
-
throw new Error(`Conversation "${conversationId}" not found`);
|
|
581
|
+
const def = objectDef;
|
|
582
|
+
if (def.fields && def.fields[name]) {
|
|
583
|
+
return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
|
|
246
584
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// src/ai-service.ts
|
|
266
|
-
function textDeltaPart(id, text) {
|
|
267
|
-
return { type: "text-delta", id, text };
|
|
268
|
-
}
|
|
269
|
-
function finishPart(result) {
|
|
270
|
-
return {
|
|
271
|
-
type: "finish",
|
|
272
|
-
finishReason: "stop",
|
|
273
|
-
totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
274
|
-
rawFinishReason: "stop"
|
|
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
|
+
});
|
|
275
603
|
};
|
|
276
604
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
`
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
|
|
769
|
+
// src/index.ts
|
|
770
|
+
var index_exports = {};
|
|
771
|
+
__export(index_exports, {
|
|
772
|
+
AIService: () => AIService,
|
|
773
|
+
AIServicePlugin: () => AIServicePlugin,
|
|
774
|
+
AgentRuntime: () => AgentRuntime,
|
|
775
|
+
AiConversationObject: () => AiConversationObject,
|
|
776
|
+
AiMessageObject: () => AiMessageObject,
|
|
777
|
+
DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
|
|
778
|
+
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
779
|
+
InMemoryConversationService: () => InMemoryConversationService,
|
|
780
|
+
METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
|
|
781
|
+
METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
|
|
782
|
+
MemoryLLMAdapter: () => MemoryLLMAdapter,
|
|
783
|
+
ObjectQLConversationService: () => ObjectQLConversationService,
|
|
784
|
+
ToolRegistry: () => ToolRegistry,
|
|
785
|
+
VercelLLMAdapter: () => VercelLLMAdapter,
|
|
786
|
+
addFieldTool: () => addFieldTool,
|
|
787
|
+
buildAIRoutes: () => buildAIRoutes,
|
|
788
|
+
buildAgentRoutes: () => buildAgentRoutes,
|
|
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
|
|
799
|
+
});
|
|
800
|
+
module.exports = __toCommonJS(index_exports);
|
|
801
|
+
|
|
802
|
+
// src/ai-service.ts
|
|
803
|
+
var import_core = require("@objectstack/core");
|
|
804
|
+
|
|
805
|
+
// src/adapters/memory-adapter.ts
|
|
806
|
+
var MemoryLLMAdapter = class {
|
|
807
|
+
constructor() {
|
|
808
|
+
this.name = "memory";
|
|
809
|
+
}
|
|
292
810
|
async chat(messages, options) {
|
|
293
|
-
|
|
294
|
-
|
|
811
|
+
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
812
|
+
const userContent = lastUserMessage?.content;
|
|
813
|
+
const text = typeof userContent === "string" ? userContent : "(complex content)";
|
|
814
|
+
const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
|
|
815
|
+
return {
|
|
816
|
+
content,
|
|
817
|
+
model: options?.model ?? "memory",
|
|
818
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
819
|
+
};
|
|
295
820
|
}
|
|
296
821
|
async complete(prompt, options) {
|
|
297
|
-
|
|
298
|
-
|
|
822
|
+
return {
|
|
823
|
+
content: `[memory] ${prompt}`,
|
|
824
|
+
model: options?.model ?? "memory",
|
|
825
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
826
|
+
};
|
|
299
827
|
}
|
|
300
|
-
async *streamChat(messages,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
yield
|
|
306
|
-
return;
|
|
828
|
+
async *streamChat(messages, _options) {
|
|
829
|
+
const result = await this.chat(messages);
|
|
830
|
+
const words = result.content.split(" ");
|
|
831
|
+
for (let i = 0; i < words.length; i++) {
|
|
832
|
+
const wordText = i === 0 ? words[i] : ` ${words[i]}`;
|
|
833
|
+
yield { type: "text-delta", id: `delta_${i}`, text: wordText };
|
|
307
834
|
}
|
|
308
|
-
yield
|
|
835
|
+
yield {
|
|
836
|
+
type: "finish",
|
|
837
|
+
finishReason: "stop",
|
|
838
|
+
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
839
|
+
rawFinishReason: "stop"
|
|
840
|
+
};
|
|
309
841
|
}
|
|
310
|
-
async embed(input
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
return this.adapter.embed(input, model);
|
|
842
|
+
async embed(input) {
|
|
843
|
+
const texts = Array.isArray(input) ? input : [input];
|
|
844
|
+
return texts.map(() => [0, 0, 0]);
|
|
315
845
|
}
|
|
316
846
|
async listModels() {
|
|
317
|
-
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
320
|
-
return this.adapter.listModels();
|
|
847
|
+
return ["memory"];
|
|
321
848
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/tools/tool-registry.ts
|
|
852
|
+
var ToolRegistry = class {
|
|
853
|
+
constructor() {
|
|
854
|
+
this.definitions = /* @__PURE__ */ new Map();
|
|
855
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
325
856
|
}
|
|
326
857
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
* 2. Calls the LLM adapter.
|
|
331
|
-
* 3. If the response contains `toolCalls`, executes them via the
|
|
332
|
-
* {@link ToolRegistry}, appends tool results as `role: 'tool'`
|
|
333
|
-
* messages, and loops back to step 2.
|
|
334
|
-
* 4. Repeats until the model produces a final text response or the
|
|
335
|
-
* maximum number of iterations (`maxIterations`) is reached.
|
|
858
|
+
* Register a tool with its definition and handler.
|
|
859
|
+
* @param definition - Tool definition (name, description, parameters schema)
|
|
860
|
+
* @param handler - Async function that executes the tool
|
|
336
861
|
*/
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
862
|
+
register(definition, handler) {
|
|
863
|
+
this.definitions.set(definition.name, definition);
|
|
864
|
+
this.handlers.set(definition.name, handler);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Unregister a tool by name.
|
|
868
|
+
*/
|
|
869
|
+
unregister(name) {
|
|
870
|
+
this.definitions.delete(name);
|
|
871
|
+
this.handlers.delete(name);
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Check whether a tool is registered.
|
|
875
|
+
*/
|
|
876
|
+
has(name) {
|
|
877
|
+
return this.definitions.has(name);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Get the definition for a registered tool.
|
|
881
|
+
*/
|
|
882
|
+
getDefinition(name) {
|
|
883
|
+
return this.definitions.get(name);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Return all registered tool definitions.
|
|
887
|
+
*/
|
|
888
|
+
getAll() {
|
|
889
|
+
return Array.from(this.definitions.values());
|
|
890
|
+
}
|
|
891
|
+
/** Number of registered tools. */
|
|
892
|
+
get size() {
|
|
893
|
+
return this.definitions.size;
|
|
894
|
+
}
|
|
895
|
+
/** All registered tool names. */
|
|
896
|
+
names() {
|
|
897
|
+
return Array.from(this.definitions.keys());
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Execute a tool call and return the result.
|
|
901
|
+
*/
|
|
902
|
+
async execute(toolCall) {
|
|
903
|
+
const handler = this.handlers.get(toolCall.toolName);
|
|
904
|
+
if (!handler) {
|
|
905
|
+
return {
|
|
906
|
+
type: "tool-result",
|
|
907
|
+
toolCallId: toolCall.toolCallId,
|
|
908
|
+
toolName: toolCall.toolName,
|
|
909
|
+
output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
|
|
910
|
+
isError: true
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
|
|
915
|
+
const content = await handler(args);
|
|
916
|
+
return {
|
|
917
|
+
type: "tool-result",
|
|
918
|
+
toolCallId: toolCall.toolCallId,
|
|
919
|
+
toolName: toolCall.toolName,
|
|
920
|
+
output: { type: "text", value: content }
|
|
921
|
+
};
|
|
922
|
+
} catch (err) {
|
|
923
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
924
|
+
return {
|
|
925
|
+
type: "tool-result",
|
|
926
|
+
toolCallId: toolCall.toolCallId,
|
|
927
|
+
toolName: toolCall.toolName,
|
|
928
|
+
output: { type: "text", value: message },
|
|
929
|
+
isError: true
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Execute multiple tool calls in parallel.
|
|
935
|
+
*/
|
|
936
|
+
async executeAll(toolCalls) {
|
|
937
|
+
return Promise.all(toolCalls.map((tc) => this.execute(tc)));
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Clear all registered tools.
|
|
941
|
+
*/
|
|
942
|
+
clear() {
|
|
943
|
+
this.definitions.clear();
|
|
944
|
+
this.handlers.clear();
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// src/conversation/in-memory-conversation-service.ts
|
|
949
|
+
var InMemoryConversationService = class {
|
|
950
|
+
constructor() {
|
|
951
|
+
this.store = /* @__PURE__ */ new Map();
|
|
952
|
+
this.counter = 0;
|
|
953
|
+
}
|
|
954
|
+
async create(options = {}) {
|
|
955
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
956
|
+
const id = `conv_${++this.counter}`;
|
|
957
|
+
const conversation = {
|
|
958
|
+
id,
|
|
959
|
+
title: options.title,
|
|
960
|
+
agentId: options.agentId,
|
|
961
|
+
userId: options.userId,
|
|
962
|
+
messages: [],
|
|
963
|
+
createdAt: now,
|
|
964
|
+
updatedAt: now,
|
|
965
|
+
metadata: options.metadata
|
|
966
|
+
};
|
|
967
|
+
this.store.set(id, conversation);
|
|
968
|
+
return conversation;
|
|
969
|
+
}
|
|
970
|
+
async get(conversationId) {
|
|
971
|
+
return this.store.get(conversationId) ?? null;
|
|
972
|
+
}
|
|
973
|
+
async list(options = {}) {
|
|
974
|
+
let results = Array.from(this.store.values());
|
|
975
|
+
if (options.userId) {
|
|
976
|
+
results = results.filter((c) => c.userId === options.userId);
|
|
977
|
+
}
|
|
978
|
+
if (options.agentId) {
|
|
979
|
+
results = results.filter((c) => c.agentId === options.agentId);
|
|
980
|
+
}
|
|
981
|
+
if (options.cursor) {
|
|
982
|
+
const idx = results.findIndex((c) => c.id === options.cursor);
|
|
983
|
+
if (idx >= 0) {
|
|
984
|
+
results = results.slice(idx + 1);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (options.limit && options.limit > 0) {
|
|
988
|
+
results = results.slice(0, options.limit);
|
|
989
|
+
}
|
|
990
|
+
return results;
|
|
991
|
+
}
|
|
992
|
+
async addMessage(conversationId, message) {
|
|
993
|
+
const conversation = this.store.get(conversationId);
|
|
994
|
+
if (!conversation) {
|
|
995
|
+
throw new Error(`Conversation "${conversationId}" not found`);
|
|
996
|
+
}
|
|
997
|
+
conversation.messages.push(message);
|
|
998
|
+
conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
999
|
+
return conversation;
|
|
1000
|
+
}
|
|
1001
|
+
async delete(conversationId) {
|
|
1002
|
+
this.store.delete(conversationId);
|
|
1003
|
+
}
|
|
1004
|
+
/** Total number of stored conversations. */
|
|
1005
|
+
get size() {
|
|
1006
|
+
return this.store.size;
|
|
1007
|
+
}
|
|
1008
|
+
/** Clear all conversations. */
|
|
1009
|
+
clear() {
|
|
1010
|
+
this.store.clear();
|
|
1011
|
+
this.counter = 0;
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
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
|
+
}
|
|
1027
|
+
var _AIService = class _AIService {
|
|
1028
|
+
constructor(config = {}) {
|
|
1029
|
+
this.adapter = config.adapter ?? new MemoryLLMAdapter();
|
|
1030
|
+
this.logger = config.logger ?? (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
1031
|
+
this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
|
|
1032
|
+
this.conversationService = config.conversationService ?? new InMemoryConversationService();
|
|
1033
|
+
this.logger.info(
|
|
1034
|
+
`[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
/** The name of the active LLM adapter. */
|
|
1038
|
+
get adapterName() {
|
|
1039
|
+
return this.adapter.name;
|
|
1040
|
+
}
|
|
1041
|
+
// ── IAIService implementation ──────────────────────────────────
|
|
1042
|
+
async chat(messages, options) {
|
|
1043
|
+
this.logger.debug("[AI] chat", { messageCount: messages.length, model: options?.model });
|
|
1044
|
+
return this.adapter.chat(messages, options);
|
|
1045
|
+
}
|
|
1046
|
+
async complete(prompt, options) {
|
|
1047
|
+
this.logger.debug("[AI] complete", { promptLength: prompt.length, model: options?.model });
|
|
1048
|
+
return this.adapter.complete(prompt, options);
|
|
1049
|
+
}
|
|
1050
|
+
async *streamChat(messages, options) {
|
|
1051
|
+
this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
|
|
1052
|
+
if (!this.adapter.streamChat) {
|
|
1053
|
+
const result = await this.adapter.chat(messages, options);
|
|
1054
|
+
yield textDeltaPart("fallback", result.content);
|
|
1055
|
+
yield finishPart(result);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
yield* this.adapter.streamChat(messages, options);
|
|
1059
|
+
}
|
|
1060
|
+
async embed(input, model) {
|
|
1061
|
+
if (!this.adapter.embed) {
|
|
1062
|
+
throw new Error(`[AI] Adapter "${this.adapter.name}" does not support embeddings`);
|
|
1063
|
+
}
|
|
1064
|
+
return this.adapter.embed(input, model);
|
|
1065
|
+
}
|
|
1066
|
+
async listModels() {
|
|
1067
|
+
if (!this.adapter.listModels) {
|
|
1068
|
+
return [];
|
|
1069
|
+
}
|
|
1070
|
+
return this.adapter.listModels();
|
|
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
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Chat with automatic tool call resolution.
|
|
1078
|
+
*
|
|
1079
|
+
* 1. Merges registered tool definitions into `options.tools`.
|
|
1080
|
+
* 2. Calls the LLM adapter.
|
|
1081
|
+
* 3. If the response contains `toolCalls`, executes them via the
|
|
1082
|
+
* {@link ToolRegistry}, appends tool results as `role: 'tool'`
|
|
1083
|
+
* messages, and loops back to step 2.
|
|
1084
|
+
* 4. Repeats until the model produces a final text response or the
|
|
1085
|
+
* maximum number of iterations (`maxIterations`) is reached.
|
|
1086
|
+
*/
|
|
1087
|
+
async chatWithTools(messages, options) {
|
|
1088
|
+
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
1089
|
+
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
1090
|
+
const registeredTools = this.toolRegistry.getAll();
|
|
1091
|
+
const mergedTools = [
|
|
1092
|
+
...registeredTools,
|
|
1093
|
+
...restOptions.tools ?? []
|
|
344
1094
|
];
|
|
345
1095
|
const chatOptions = {
|
|
346
1096
|
...restOptions,
|
|
@@ -462,6 +1212,12 @@ var _AIService = class _AIService {
|
|
|
462
1212
|
}
|
|
463
1213
|
}
|
|
464
1214
|
}
|
|
1215
|
+
yield {
|
|
1216
|
+
type: "tool-result",
|
|
1217
|
+
toolCallId: tr.toolCallId,
|
|
1218
|
+
toolName: tr.toolName,
|
|
1219
|
+
output: tr.output
|
|
1220
|
+
};
|
|
465
1221
|
conversation.push({
|
|
466
1222
|
role: "tool",
|
|
467
1223
|
content: [tr]
|
|
@@ -493,6 +1249,10 @@ function sse(data) {
|
|
|
493
1249
|
|
|
494
1250
|
`;
|
|
495
1251
|
}
|
|
1252
|
+
function dataStreamLine(prefix, data) {
|
|
1253
|
+
return `${prefix}:${JSON.stringify(data)}
|
|
1254
|
+
`;
|
|
1255
|
+
}
|
|
496
1256
|
function encodeStreamPart(part) {
|
|
497
1257
|
switch (part.type) {
|
|
498
1258
|
case "text-delta":
|
|
@@ -527,8 +1287,20 @@ function encodeStreamPart(part) {
|
|
|
527
1287
|
type: "error",
|
|
528
1288
|
errorText: String(part.error)
|
|
529
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
|
|
530
1299
|
// finish-step and finish are handled by the generator, not here
|
|
531
1300
|
default:
|
|
1301
|
+
if (part.type?.startsWith("step-")) {
|
|
1302
|
+
return sse(part);
|
|
1303
|
+
}
|
|
532
1304
|
return "";
|
|
533
1305
|
}
|
|
534
1306
|
}
|
|
@@ -562,8 +1334,7 @@ async function* encodeVercelDataStream(events) {
|
|
|
562
1334
|
yield "data: [DONE]\n\n";
|
|
563
1335
|
}
|
|
564
1336
|
|
|
565
|
-
// src/routes/
|
|
566
|
-
var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
1337
|
+
// src/routes/message-utils.ts
|
|
567
1338
|
function normalizeMessage(raw) {
|
|
568
1339
|
const role = raw.role;
|
|
569
1340
|
if (typeof raw.content === "string") {
|
|
@@ -580,14 +1351,7 @@ function normalizeMessage(raw) {
|
|
|
580
1351
|
}
|
|
581
1352
|
return { role, content: "" };
|
|
582
1353
|
}
|
|
583
|
-
function
|
|
584
|
-
if (typeof raw !== "object" || raw === null) {
|
|
585
|
-
return "each message must be an object";
|
|
586
|
-
}
|
|
587
|
-
const msg = raw;
|
|
588
|
-
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
589
|
-
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
590
|
-
}
|
|
1354
|
+
function validateMessageContent(msg, opts) {
|
|
591
1355
|
const content = msg.content;
|
|
592
1356
|
if (Array.isArray(msg.parts)) {
|
|
593
1357
|
return null;
|
|
@@ -610,20 +1374,32 @@ function validateMessage(raw) {
|
|
|
610
1374
|
}
|
|
611
1375
|
return null;
|
|
612
1376
|
}
|
|
613
|
-
if (content === null || content === void 0) {
|
|
614
|
-
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
1377
|
+
if ((content === null || content === void 0) && opts?.allowEmptyContent) {
|
|
1378
|
+
return null;
|
|
617
1379
|
}
|
|
618
1380
|
return "message.content must be a string, an array, or include parts";
|
|
619
1381
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1382
|
+
|
|
1383
|
+
// src/routes/ai-routes.ts
|
|
1384
|
+
var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
1385
|
+
function validateMessage(raw) {
|
|
1386
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1387
|
+
return "each message must be an object";
|
|
1388
|
+
}
|
|
1389
|
+
const msg = raw;
|
|
1390
|
+
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
1391
|
+
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
1392
|
+
}
|
|
1393
|
+
const allowEmpty = msg.role === "assistant" || msg.role === "tool";
|
|
1394
|
+
return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
|
|
1395
|
+
}
|
|
1396
|
+
function buildAIRoutes(aiService, conversationService, logger) {
|
|
1397
|
+
return [
|
|
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, … }`).
|
|
627
1403
|
//
|
|
628
1404
|
// Behaviour:
|
|
629
1405
|
// • `stream !== false` → Vercel Data Stream Protocol (SSE)
|
|
@@ -705,1358 +1481,764 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
705
1481
|
handler: async (req) => {
|
|
706
1482
|
const { messages, options } = req.body ?? {};
|
|
707
1483
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
708
|
-
return { status: 400, body: { error: "messages array is required" } };
|
|
709
|
-
}
|
|
710
|
-
for (const msg of messages) {
|
|
711
|
-
const err = validateMessage(msg);
|
|
712
|
-
if (err) return { status: 400, body: { error: err } };
|
|
713
|
-
}
|
|
714
|
-
try {
|
|
715
|
-
if (!aiService.streamChat) {
|
|
716
|
-
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
717
|
-
}
|
|
718
|
-
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
719
|
-
return { status: 200, stream: true, events };
|
|
720
|
-
} catch (err) {
|
|
721
|
-
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
722
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
},
|
|
726
|
-
// ── Complete ────────────────────────────────────────────────
|
|
727
|
-
{
|
|
728
|
-
method: "POST",
|
|
729
|
-
path: "/api/v1/ai/complete",
|
|
730
|
-
description: "Text completion",
|
|
731
|
-
auth: true,
|
|
732
|
-
permissions: ["ai:complete"],
|
|
733
|
-
handler: async (req) => {
|
|
734
|
-
const { prompt, options } = req.body ?? {};
|
|
735
|
-
if (!prompt || typeof prompt !== "string") {
|
|
736
|
-
return { status: 400, body: { error: "prompt string is required" } };
|
|
737
|
-
}
|
|
738
|
-
try {
|
|
739
|
-
const result = await aiService.complete(prompt, options);
|
|
740
|
-
return { status: 200, body: result };
|
|
741
|
-
} catch (err) {
|
|
742
|
-
logger.error("[AI Route] /complete error", err instanceof Error ? err : void 0);
|
|
743
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
},
|
|
747
|
-
// ── Models ──────────────────────────────────────────────────
|
|
748
|
-
{
|
|
749
|
-
method: "GET",
|
|
750
|
-
path: "/api/v1/ai/models",
|
|
751
|
-
description: "List available models",
|
|
752
|
-
auth: true,
|
|
753
|
-
permissions: ["ai:read"],
|
|
754
|
-
handler: async () => {
|
|
755
|
-
try {
|
|
756
|
-
const models = aiService.listModels ? await aiService.listModels() : [];
|
|
757
|
-
return { status: 200, body: { models } };
|
|
758
|
-
} catch (err) {
|
|
759
|
-
logger.error("[AI Route] /models error", err instanceof Error ? err : void 0);
|
|
760
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
},
|
|
764
|
-
// ── Conversations ──────────────────────────────────────────
|
|
765
|
-
{
|
|
766
|
-
method: "POST",
|
|
767
|
-
path: "/api/v1/ai/conversations",
|
|
768
|
-
description: "Create a conversation",
|
|
769
|
-
auth: true,
|
|
770
|
-
permissions: ["ai:conversations"],
|
|
771
|
-
handler: async (req) => {
|
|
772
|
-
try {
|
|
773
|
-
if (req.body !== void 0 && req.body !== null && (typeof req.body !== "object" || Array.isArray(req.body))) {
|
|
774
|
-
return { status: 400, body: { error: "Invalid request payload" } };
|
|
775
|
-
}
|
|
776
|
-
const options = { ...req.body ?? {} };
|
|
777
|
-
if (req.user?.userId) {
|
|
778
|
-
options.userId = req.user.userId;
|
|
779
|
-
}
|
|
780
|
-
const conversation = await conversationService.create(options);
|
|
781
|
-
return { status: 201, body: conversation };
|
|
782
|
-
} catch (err) {
|
|
783
|
-
logger.error("[AI Route] POST /conversations error", err instanceof Error ? err : void 0);
|
|
784
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
method: "GET",
|
|
790
|
-
path: "/api/v1/ai/conversations",
|
|
791
|
-
description: "List conversations",
|
|
792
|
-
auth: true,
|
|
793
|
-
permissions: ["ai:conversations"],
|
|
794
|
-
handler: async (req) => {
|
|
795
|
-
try {
|
|
796
|
-
const rawQuery = req.query ?? {};
|
|
797
|
-
const options = { ...rawQuery };
|
|
798
|
-
if (typeof rawQuery.limit === "string") {
|
|
799
|
-
const parsedLimit = Number(rawQuery.limit);
|
|
800
|
-
if (!Number.isFinite(parsedLimit) || parsedLimit <= 0 || !Number.isInteger(parsedLimit)) {
|
|
801
|
-
return { status: 400, body: { error: "Invalid limit parameter" } };
|
|
802
|
-
}
|
|
803
|
-
options.limit = parsedLimit;
|
|
804
|
-
}
|
|
805
|
-
if (req.user?.userId) {
|
|
806
|
-
options.userId = req.user.userId;
|
|
807
|
-
}
|
|
808
|
-
const conversations = await conversationService.list(options);
|
|
809
|
-
return { status: 200, body: { conversations } };
|
|
810
|
-
} catch (err) {
|
|
811
|
-
logger.error("[AI Route] GET /conversations error", err instanceof Error ? err : void 0);
|
|
812
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
},
|
|
816
|
-
{
|
|
817
|
-
method: "POST",
|
|
818
|
-
path: "/api/v1/ai/conversations/:id/messages",
|
|
819
|
-
description: "Add message to a conversation",
|
|
820
|
-
auth: true,
|
|
821
|
-
permissions: ["ai:conversations"],
|
|
822
|
-
handler: async (req) => {
|
|
823
|
-
const id = req.params?.id;
|
|
824
|
-
if (!id) {
|
|
825
|
-
return { status: 400, body: { error: "conversation id is required" } };
|
|
826
|
-
}
|
|
827
|
-
const message = req.body;
|
|
828
|
-
const validationError = validateMessage(message);
|
|
829
|
-
if (validationError) {
|
|
830
|
-
return { status: 400, body: { error: validationError } };
|
|
831
|
-
}
|
|
832
|
-
try {
|
|
833
|
-
if (req.user?.userId) {
|
|
834
|
-
const existing = await conversationService.get(id);
|
|
835
|
-
if (!existing) {
|
|
836
|
-
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
837
|
-
}
|
|
838
|
-
if (existing.userId && existing.userId !== req.user.userId) {
|
|
839
|
-
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
const conversation = await conversationService.addMessage(id, message);
|
|
843
|
-
return { status: 200, body: conversation };
|
|
844
|
-
} catch (err) {
|
|
845
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
846
|
-
if (msg.includes("not found")) {
|
|
847
|
-
return { status: 404, body: { error: msg } };
|
|
848
|
-
}
|
|
849
|
-
logger.error("[AI Route] POST /conversations/:id/messages error", err instanceof Error ? err : void 0);
|
|
850
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
},
|
|
854
|
-
{
|
|
855
|
-
method: "DELETE",
|
|
856
|
-
path: "/api/v1/ai/conversations/:id",
|
|
857
|
-
description: "Delete a conversation",
|
|
858
|
-
auth: true,
|
|
859
|
-
permissions: ["ai:conversations"],
|
|
860
|
-
handler: async (req) => {
|
|
861
|
-
const id = req.params?.id;
|
|
862
|
-
if (!id) {
|
|
863
|
-
return { status: 400, body: { error: "conversation id is required" } };
|
|
864
|
-
}
|
|
865
|
-
try {
|
|
866
|
-
if (req.user?.userId) {
|
|
867
|
-
const existing = await conversationService.get(id);
|
|
868
|
-
if (!existing) {
|
|
869
|
-
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
870
|
-
}
|
|
871
|
-
if (existing.userId && existing.userId !== req.user.userId) {
|
|
872
|
-
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
await conversationService.delete(id);
|
|
876
|
-
return { status: 204 };
|
|
877
|
-
} catch (err) {
|
|
878
|
-
logger.error("[AI Route] DELETE /conversations/:id error", err instanceof Error ? err : void 0);
|
|
879
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
];
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// src/routes/agent-routes.ts
|
|
887
|
-
var ALLOWED_AGENT_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
888
|
-
function validateAgentMessage(raw) {
|
|
889
|
-
if (typeof raw !== "object" || raw === null) {
|
|
890
|
-
return "each message must be an object";
|
|
891
|
-
}
|
|
892
|
-
const msg = raw;
|
|
893
|
-
if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
|
|
894
|
-
return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
|
|
895
|
-
}
|
|
896
|
-
if (typeof msg.content !== "string") {
|
|
897
|
-
return "message.content must be a string";
|
|
898
|
-
}
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
902
|
-
return [
|
|
903
|
-
// ── List active agents ──────────────────────────────────────
|
|
904
|
-
{
|
|
905
|
-
method: "GET",
|
|
906
|
-
path: "/api/v1/ai/agents",
|
|
907
|
-
description: "List all active AI agents",
|
|
908
|
-
auth: true,
|
|
909
|
-
permissions: ["ai:chat"],
|
|
910
|
-
handler: async () => {
|
|
911
|
-
try {
|
|
912
|
-
const agents = await agentRuntime.listAgents();
|
|
913
|
-
return { status: 200, body: { agents } };
|
|
914
|
-
} catch (err) {
|
|
915
|
-
logger.error(
|
|
916
|
-
"[AI Route] /agents list error",
|
|
917
|
-
err instanceof Error ? err : void 0
|
|
918
|
-
);
|
|
919
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
},
|
|
923
|
-
// ── Chat with a specific agent ──────────────────────────────
|
|
924
|
-
{
|
|
925
|
-
method: "POST",
|
|
926
|
-
path: "/api/v1/ai/agents/:agentName/chat",
|
|
927
|
-
description: "Chat with a specific AI agent",
|
|
928
|
-
auth: true,
|
|
929
|
-
permissions: ["ai:chat", "ai:agents"],
|
|
930
|
-
handler: async (req) => {
|
|
931
|
-
const agentName = req.params?.agentName;
|
|
932
|
-
if (!agentName) {
|
|
933
|
-
return { status: 400, body: { error: "agentName parameter is required" } };
|
|
934
|
-
}
|
|
935
|
-
const {
|
|
936
|
-
messages: rawMessages,
|
|
937
|
-
context: chatContext,
|
|
938
|
-
options: extraOptions
|
|
939
|
-
} = req.body ?? {};
|
|
940
|
-
if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
|
|
941
|
-
return { status: 400, body: { error: "messages array is required" } };
|
|
942
|
-
}
|
|
943
|
-
for (const msg of rawMessages) {
|
|
944
|
-
const err = validateAgentMessage(msg);
|
|
945
|
-
if (err) return { status: 400, body: { error: err } };
|
|
946
|
-
}
|
|
947
|
-
const agent = await agentRuntime.loadAgent(agentName);
|
|
948
|
-
if (!agent) {
|
|
949
|
-
return { status: 404, body: { error: `Agent "${agentName}" not found` } };
|
|
950
|
-
}
|
|
951
|
-
if (!agent.active) {
|
|
952
|
-
return { status: 403, body: { error: `Agent "${agentName}" is not active` } };
|
|
953
|
-
}
|
|
954
|
-
try {
|
|
955
|
-
const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext);
|
|
956
|
-
const agentOptions = agentRuntime.buildRequestOptions(
|
|
957
|
-
agent,
|
|
958
|
-
aiService.toolRegistry.getAll()
|
|
959
|
-
);
|
|
960
|
-
const safeOverrides = {};
|
|
961
|
-
if (extraOptions) {
|
|
962
|
-
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["temperature", "maxTokens", "stop"]);
|
|
963
|
-
for (const key of Object.keys(extraOptions)) {
|
|
964
|
-
if (ALLOWED_KEYS.has(key)) {
|
|
965
|
-
safeOverrides[key] = extraOptions[key];
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
const mergedOptions = { ...agentOptions, ...safeOverrides };
|
|
970
|
-
const fullMessages = [
|
|
971
|
-
...systemMessages,
|
|
972
|
-
...rawMessages
|
|
973
|
-
];
|
|
974
|
-
const result = await aiService.chatWithTools(fullMessages, {
|
|
975
|
-
...mergedOptions,
|
|
976
|
-
maxIterations: agent.planning?.maxIterations
|
|
977
|
-
});
|
|
978
|
-
return { status: 200, body: result };
|
|
979
|
-
} catch (err) {
|
|
980
|
-
logger.error(
|
|
981
|
-
"[AI Route] /agents/:agentName/chat error",
|
|
982
|
-
err instanceof Error ? err : void 0
|
|
983
|
-
);
|
|
984
|
-
return { status: 500, body: { error: "Internal AI service error" } };
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
];
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// src/conversation/objectql-conversation-service.ts
|
|
992
|
-
var import_node_crypto = require("crypto");
|
|
993
|
-
var CONVERSATIONS_OBJECT = "ai_conversations";
|
|
994
|
-
var MESSAGES_OBJECT = "ai_messages";
|
|
995
|
-
var CONVERSATION_ORDER = [
|
|
996
|
-
{ field: "created_at", order: "asc" },
|
|
997
|
-
{ field: "id", order: "asc" }
|
|
998
|
-
];
|
|
999
|
-
var MESSAGE_ORDER = [
|
|
1000
|
-
{ field: "created_at", order: "asc" },
|
|
1001
|
-
{ field: "id", order: "asc" }
|
|
1002
|
-
];
|
|
1003
|
-
var ObjectQLConversationService = class {
|
|
1004
|
-
constructor(engine) {
|
|
1005
|
-
this.engine = engine;
|
|
1006
|
-
}
|
|
1007
|
-
async create(options = {}) {
|
|
1008
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1009
|
-
const id = `conv_${(0, import_node_crypto.randomUUID)()}`;
|
|
1010
|
-
const record = {
|
|
1011
|
-
id,
|
|
1012
|
-
title: options.title ?? null,
|
|
1013
|
-
agent_id: options.agentId ?? null,
|
|
1014
|
-
user_id: options.userId ?? null,
|
|
1015
|
-
metadata: options.metadata ? JSON.stringify(options.metadata) : null,
|
|
1016
|
-
created_at: now,
|
|
1017
|
-
updated_at: now
|
|
1018
|
-
};
|
|
1019
|
-
await this.engine.insert(CONVERSATIONS_OBJECT, record);
|
|
1020
|
-
return {
|
|
1021
|
-
id,
|
|
1022
|
-
title: options.title,
|
|
1023
|
-
agentId: options.agentId,
|
|
1024
|
-
userId: options.userId,
|
|
1025
|
-
messages: [],
|
|
1026
|
-
createdAt: now,
|
|
1027
|
-
updatedAt: now,
|
|
1028
|
-
metadata: options.metadata
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
async get(conversationId) {
|
|
1032
|
-
const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
|
|
1033
|
-
where: { id: conversationId }
|
|
1034
|
-
});
|
|
1035
|
-
if (!row) return null;
|
|
1036
|
-
const messages = await this.engine.find(MESSAGES_OBJECT, {
|
|
1037
|
-
where: { conversation_id: conversationId },
|
|
1038
|
-
orderBy: MESSAGE_ORDER
|
|
1039
|
-
});
|
|
1040
|
-
return this.toConversation(row, messages);
|
|
1041
|
-
}
|
|
1042
|
-
async list(options = {}) {
|
|
1043
|
-
const where = {};
|
|
1044
|
-
if (options.userId) where.user_id = options.userId;
|
|
1045
|
-
if (options.agentId) where.agent_id = options.agentId;
|
|
1046
|
-
if (options.cursor) {
|
|
1047
|
-
const cursorRow = await this.engine.findOne(CONVERSATIONS_OBJECT, {
|
|
1048
|
-
where: { id: options.cursor },
|
|
1049
|
-
fields: ["created_at", "id"]
|
|
1050
|
-
});
|
|
1051
|
-
if (cursorRow) {
|
|
1052
|
-
where.$or = [
|
|
1053
|
-
{ created_at: { $gt: cursorRow.created_at } },
|
|
1054
|
-
{ created_at: cursorRow.created_at, id: { $gt: cursorRow.id } }
|
|
1055
|
-
];
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
const rows = await this.engine.find(CONVERSATIONS_OBJECT, {
|
|
1059
|
-
where: Object.keys(where).length > 0 ? where : void 0,
|
|
1060
|
-
orderBy: CONVERSATION_ORDER,
|
|
1061
|
-
limit: options.limit && options.limit > 0 ? options.limit : void 0
|
|
1062
|
-
});
|
|
1063
|
-
const conversations = await Promise.all(
|
|
1064
|
-
rows.map(async (row) => {
|
|
1065
|
-
const messages = await this.engine.find(MESSAGES_OBJECT, {
|
|
1066
|
-
where: { conversation_id: row.id },
|
|
1067
|
-
orderBy: MESSAGE_ORDER
|
|
1068
|
-
});
|
|
1069
|
-
return this.toConversation(row, messages);
|
|
1070
|
-
})
|
|
1071
|
-
);
|
|
1072
|
-
return conversations;
|
|
1073
|
-
}
|
|
1074
|
-
async addMessage(conversationId, message) {
|
|
1075
|
-
const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
|
|
1076
|
-
where: { id: conversationId }
|
|
1077
|
-
});
|
|
1078
|
-
if (!row) {
|
|
1079
|
-
throw new Error(`Conversation "${conversationId}" not found`);
|
|
1080
|
-
}
|
|
1081
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1082
|
-
const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
|
|
1083
|
-
let contentStr;
|
|
1084
|
-
let toolCallsJson = null;
|
|
1085
|
-
let toolCallId = null;
|
|
1086
|
-
if (message.role === "system" || message.role === "user") {
|
|
1087
|
-
contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
1088
|
-
} else if (message.role === "assistant") {
|
|
1089
|
-
if (typeof message.content === "string") {
|
|
1090
|
-
contentStr = message.content;
|
|
1091
|
-
} else {
|
|
1092
|
-
const parts = message.content;
|
|
1093
|
-
const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
|
|
1094
|
-
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
|
1095
|
-
contentStr = textParts.join("");
|
|
1096
|
-
if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
|
|
1097
|
-
}
|
|
1098
|
-
} else if (message.role === "tool") {
|
|
1099
|
-
contentStr = JSON.stringify(message.content);
|
|
1100
|
-
const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
|
|
1101
|
-
if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
|
|
1102
|
-
} else {
|
|
1103
|
-
contentStr = "";
|
|
1104
|
-
}
|
|
1105
|
-
await this.engine.insert(MESSAGES_OBJECT, {
|
|
1106
|
-
id: msgId,
|
|
1107
|
-
conversation_id: conversationId,
|
|
1108
|
-
role: message.role,
|
|
1109
|
-
content: contentStr,
|
|
1110
|
-
tool_calls: toolCallsJson,
|
|
1111
|
-
tool_call_id: toolCallId,
|
|
1112
|
-
created_at: now
|
|
1113
|
-
});
|
|
1114
|
-
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
1115
|
-
where: { id: conversationId }
|
|
1116
|
-
});
|
|
1117
|
-
return await this.get(conversationId);
|
|
1118
|
-
}
|
|
1119
|
-
async delete(conversationId) {
|
|
1120
|
-
await this.engine.delete(MESSAGES_OBJECT, {
|
|
1121
|
-
where: { conversation_id: conversationId },
|
|
1122
|
-
multi: true
|
|
1123
|
-
});
|
|
1124
|
-
await this.engine.delete(CONVERSATIONS_OBJECT, {
|
|
1125
|
-
where: { id: conversationId }
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
// ── Private helpers ──────────────────────────────────────────────
|
|
1129
|
-
/**
|
|
1130
|
-
* Safely parse a JSON string, returning `undefined` on failure.
|
|
1131
|
-
*/
|
|
1132
|
-
safeParse(value, fallback) {
|
|
1133
|
-
if (!value) return void 0;
|
|
1134
|
-
try {
|
|
1135
|
-
return JSON.parse(value);
|
|
1136
|
-
} catch {
|
|
1137
|
-
return fallback;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Map a database row + message rows to an AIConversation.
|
|
1142
|
-
*/
|
|
1143
|
-
toConversation(row, messageRows) {
|
|
1144
|
-
return {
|
|
1145
|
-
id: row.id,
|
|
1146
|
-
title: row.title ?? void 0,
|
|
1147
|
-
agentId: row.agent_id ?? void 0,
|
|
1148
|
-
userId: row.user_id ?? void 0,
|
|
1149
|
-
messages: messageRows.map((m) => this.toMessage(m)),
|
|
1150
|
-
createdAt: row.created_at,
|
|
1151
|
-
updatedAt: row.updated_at,
|
|
1152
|
-
metadata: this.safeParse(row.metadata)
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Map a database row to a ModelMessage.
|
|
1157
|
-
*/
|
|
1158
|
-
toMessage(row) {
|
|
1159
|
-
switch (row.role) {
|
|
1160
|
-
case "system":
|
|
1161
|
-
return { role: "system", content: row.content };
|
|
1162
|
-
case "user":
|
|
1163
|
-
return { role: "user", content: row.content };
|
|
1164
|
-
case "assistant": {
|
|
1165
|
-
const toolCalls = this.safeParse(row.tool_calls);
|
|
1166
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
1167
|
-
const content = [];
|
|
1168
|
-
if (row.content) content.push({ type: "text", text: row.content });
|
|
1169
|
-
content.push(...toolCalls);
|
|
1170
|
-
return { role: "assistant", content };
|
|
1171
|
-
}
|
|
1172
|
-
return { role: "assistant", content: row.content };
|
|
1173
|
-
}
|
|
1174
|
-
case "tool": {
|
|
1175
|
-
const toolResults = this.safeParse(row.content);
|
|
1176
|
-
if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
|
|
1177
|
-
return { role: "tool", content: toolResults };
|
|
1178
|
-
}
|
|
1179
|
-
return {
|
|
1180
|
-
role: "tool",
|
|
1181
|
-
content: [{
|
|
1182
|
-
type: "tool-result",
|
|
1183
|
-
toolCallId: row.tool_call_id ?? "",
|
|
1184
|
-
toolName: "unknown",
|
|
1185
|
-
output: { type: "text", value: row.content }
|
|
1186
|
-
}]
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
default:
|
|
1190
|
-
return { role: "user", content: row.content };
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
};
|
|
1194
|
-
|
|
1195
|
-
// src/objects/ai-conversation.object.ts
|
|
1196
|
-
var import_data = require("@objectstack/spec/data");
|
|
1197
|
-
var AiConversationObject = import_data.ObjectSchema.create({
|
|
1198
|
-
namespace: "ai",
|
|
1199
|
-
name: "conversations",
|
|
1200
|
-
label: "AI Conversation",
|
|
1201
|
-
pluralLabel: "AI Conversations",
|
|
1202
|
-
icon: "message-square",
|
|
1203
|
-
isSystem: true,
|
|
1204
|
-
description: "Persistent AI conversation metadata",
|
|
1205
|
-
fields: {
|
|
1206
|
-
id: import_data.Field.text({
|
|
1207
|
-
label: "Conversation ID",
|
|
1208
|
-
required: true,
|
|
1209
|
-
readonly: true
|
|
1210
|
-
}),
|
|
1211
|
-
title: import_data.Field.text({
|
|
1212
|
-
label: "Title",
|
|
1213
|
-
required: false,
|
|
1214
|
-
maxLength: 500,
|
|
1215
|
-
description: "Conversation title or summary"
|
|
1216
|
-
}),
|
|
1217
|
-
agent_id: import_data.Field.text({
|
|
1218
|
-
label: "Agent ID",
|
|
1219
|
-
required: false,
|
|
1220
|
-
maxLength: 255,
|
|
1221
|
-
description: "Associated AI agent identifier"
|
|
1222
|
-
}),
|
|
1223
|
-
user_id: import_data.Field.text({
|
|
1224
|
-
label: "User ID",
|
|
1225
|
-
required: false,
|
|
1226
|
-
maxLength: 255,
|
|
1227
|
-
description: "User who owns the conversation"
|
|
1228
|
-
}),
|
|
1229
|
-
metadata: import_data.Field.textarea({
|
|
1230
|
-
label: "Metadata",
|
|
1231
|
-
required: false,
|
|
1232
|
-
description: "JSON-serialized conversation metadata"
|
|
1233
|
-
}),
|
|
1234
|
-
created_at: import_data.Field.datetime({
|
|
1235
|
-
label: "Created At",
|
|
1236
|
-
required: true,
|
|
1237
|
-
defaultValue: "NOW()",
|
|
1238
|
-
readonly: true
|
|
1239
|
-
}),
|
|
1240
|
-
updated_at: import_data.Field.datetime({
|
|
1241
|
-
label: "Updated At",
|
|
1242
|
-
required: true,
|
|
1243
|
-
defaultValue: "NOW()",
|
|
1244
|
-
readonly: true
|
|
1245
|
-
})
|
|
1246
|
-
},
|
|
1247
|
-
indexes: [
|
|
1248
|
-
{ fields: ["user_id"] },
|
|
1249
|
-
{ fields: ["agent_id"] },
|
|
1250
|
-
{ fields: ["created_at"] }
|
|
1251
|
-
],
|
|
1252
|
-
enable: {
|
|
1253
|
-
trackHistory: false,
|
|
1254
|
-
searchable: false,
|
|
1255
|
-
apiEnabled: true,
|
|
1256
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
1257
|
-
trash: false,
|
|
1258
|
-
mru: false
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
// src/objects/ai-message.object.ts
|
|
1263
|
-
var import_data2 = require("@objectstack/spec/data");
|
|
1264
|
-
var AiMessageObject = import_data2.ObjectSchema.create({
|
|
1265
|
-
namespace: "ai",
|
|
1266
|
-
name: "messages",
|
|
1267
|
-
label: "AI Message",
|
|
1268
|
-
pluralLabel: "AI Messages",
|
|
1269
|
-
icon: "message-circle",
|
|
1270
|
-
isSystem: true,
|
|
1271
|
-
description: "Individual messages within AI conversations",
|
|
1272
|
-
fields: {
|
|
1273
|
-
id: import_data2.Field.text({
|
|
1274
|
-
label: "Message ID",
|
|
1275
|
-
required: true,
|
|
1276
|
-
readonly: true
|
|
1277
|
-
}),
|
|
1278
|
-
conversation_id: import_data2.Field.text({
|
|
1279
|
-
label: "Conversation ID",
|
|
1280
|
-
required: true,
|
|
1281
|
-
description: "Foreign key to ai_conversations"
|
|
1282
|
-
}),
|
|
1283
|
-
role: import_data2.Field.select({
|
|
1284
|
-
label: "Role",
|
|
1285
|
-
required: true,
|
|
1286
|
-
options: [
|
|
1287
|
-
{ label: "System", value: "system" },
|
|
1288
|
-
{ label: "User", value: "user" },
|
|
1289
|
-
{ label: "Assistant", value: "assistant" },
|
|
1290
|
-
{ label: "Tool", value: "tool" }
|
|
1291
|
-
]
|
|
1292
|
-
}),
|
|
1293
|
-
content: import_data2.Field.textarea({
|
|
1294
|
-
label: "Content",
|
|
1295
|
-
required: true,
|
|
1296
|
-
description: "Message content"
|
|
1297
|
-
}),
|
|
1298
|
-
tool_calls: import_data2.Field.textarea({
|
|
1299
|
-
label: "Tool Calls",
|
|
1300
|
-
required: false,
|
|
1301
|
-
description: "JSON-serialized tool calls (when role=assistant)"
|
|
1302
|
-
}),
|
|
1303
|
-
tool_call_id: import_data2.Field.text({
|
|
1304
|
-
label: "Tool Call ID",
|
|
1305
|
-
required: false,
|
|
1306
|
-
maxLength: 255,
|
|
1307
|
-
description: "ID of the tool call this message responds to (when role=tool)"
|
|
1308
|
-
}),
|
|
1309
|
-
created_at: import_data2.Field.datetime({
|
|
1310
|
-
label: "Created At",
|
|
1311
|
-
required: true,
|
|
1312
|
-
defaultValue: "NOW()",
|
|
1313
|
-
readonly: true
|
|
1314
|
-
})
|
|
1315
|
-
},
|
|
1316
|
-
indexes: [
|
|
1317
|
-
{ fields: ["conversation_id"] },
|
|
1318
|
-
{ fields: ["conversation_id", "created_at"] }
|
|
1319
|
-
],
|
|
1320
|
-
enable: {
|
|
1321
|
-
trackHistory: false,
|
|
1322
|
-
searchable: false,
|
|
1323
|
-
apiEnabled: true,
|
|
1324
|
-
apiMethods: ["get", "list", "create"],
|
|
1325
|
-
trash: false,
|
|
1326
|
-
mru: false
|
|
1327
|
-
}
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
// src/tools/data-tools.ts
|
|
1331
|
-
var MAX_QUERY_LIMIT = 200;
|
|
1332
|
-
var DEFAULT_QUERY_LIMIT = 20;
|
|
1333
|
-
var LIST_OBJECTS_TOOL = {
|
|
1334
|
-
name: "list_objects",
|
|
1335
|
-
description: "List all available data objects (tables) in the system. Returns object names and labels.",
|
|
1336
|
-
parameters: {
|
|
1337
|
-
type: "object",
|
|
1338
|
-
properties: {},
|
|
1339
|
-
additionalProperties: false
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
var DESCRIBE_OBJECT_TOOL = {
|
|
1343
|
-
name: "describe_object",
|
|
1344
|
-
description: "Get the schema (fields, types, labels) of a specific data object. Use this to understand the structure of a table before querying it.",
|
|
1345
|
-
parameters: {
|
|
1346
|
-
type: "object",
|
|
1347
|
-
properties: {
|
|
1348
|
-
objectName: {
|
|
1349
|
-
type: "string",
|
|
1350
|
-
description: "The snake_case name of the object to describe"
|
|
1351
|
-
}
|
|
1352
|
-
},
|
|
1353
|
-
required: ["objectName"],
|
|
1354
|
-
additionalProperties: false
|
|
1355
|
-
}
|
|
1356
|
-
};
|
|
1357
|
-
var QUERY_RECORDS_TOOL = {
|
|
1358
|
-
name: "query_records",
|
|
1359
|
-
description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
|
|
1360
|
-
parameters: {
|
|
1361
|
-
type: "object",
|
|
1362
|
-
properties: {
|
|
1363
|
-
objectName: {
|
|
1364
|
-
type: "string",
|
|
1365
|
-
description: "The snake_case name of the object to query"
|
|
1366
|
-
},
|
|
1367
|
-
where: {
|
|
1368
|
-
type: "object",
|
|
1369
|
-
description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
|
|
1370
|
-
},
|
|
1371
|
-
fields: {
|
|
1372
|
-
type: "array",
|
|
1373
|
-
items: { type: "string" },
|
|
1374
|
-
description: "List of field names to return (omit for all fields)"
|
|
1375
|
-
},
|
|
1376
|
-
orderBy: {
|
|
1377
|
-
type: "array",
|
|
1378
|
-
items: {
|
|
1379
|
-
type: "object",
|
|
1380
|
-
properties: {
|
|
1381
|
-
field: { type: "string" },
|
|
1382
|
-
order: { type: "string", enum: ["asc", "desc"] }
|
|
1484
|
+
return { status: 400, body: { error: "messages array is required" } };
|
|
1485
|
+
}
|
|
1486
|
+
for (const msg of messages) {
|
|
1487
|
+
const err = validateMessage(msg);
|
|
1488
|
+
if (err) return { status: 400, body: { error: err } };
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
if (!aiService.streamChat) {
|
|
1492
|
+
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
1383
1493
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
},
|
|
1391
|
-
offset: {
|
|
1392
|
-
type: "number",
|
|
1393
|
-
description: "Number of records to skip for pagination"
|
|
1494
|
+
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
1495
|
+
return { status: 200, stream: true, events };
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
1498
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1499
|
+
}
|
|
1394
1500
|
}
|
|
1395
1501
|
},
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
type: "array",
|
|
1416
|
-
items: { type: "string" },
|
|
1417
|
-
description: "List of field names to return (omit for all fields)"
|
|
1502
|
+
// ── Complete ────────────────────────────────────────────────
|
|
1503
|
+
{
|
|
1504
|
+
method: "POST",
|
|
1505
|
+
path: "/api/v1/ai/complete",
|
|
1506
|
+
description: "Text completion",
|
|
1507
|
+
auth: true,
|
|
1508
|
+
permissions: ["ai:complete"],
|
|
1509
|
+
handler: async (req) => {
|
|
1510
|
+
const { prompt, options } = req.body ?? {};
|
|
1511
|
+
if (!prompt || typeof prompt !== "string") {
|
|
1512
|
+
return { status: 400, body: { error: "prompt string is required" } };
|
|
1513
|
+
}
|
|
1514
|
+
try {
|
|
1515
|
+
const result = await aiService.complete(prompt, options);
|
|
1516
|
+
return { status: 200, body: result };
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
logger.error("[AI Route] /complete error", err instanceof Error ? err : void 0);
|
|
1519
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1520
|
+
}
|
|
1418
1521
|
}
|
|
1419
1522
|
},
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
type: "array",
|
|
1436
|
-
items: {
|
|
1437
|
-
type: "object",
|
|
1438
|
-
properties: {
|
|
1439
|
-
function: {
|
|
1440
|
-
type: "string",
|
|
1441
|
-
enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
|
|
1442
|
-
description: "Aggregation function"
|
|
1443
|
-
},
|
|
1444
|
-
field: {
|
|
1445
|
-
type: "string",
|
|
1446
|
-
description: "Field to aggregate (optional for count)"
|
|
1447
|
-
},
|
|
1448
|
-
alias: {
|
|
1449
|
-
type: "string",
|
|
1450
|
-
description: "Result column alias"
|
|
1451
|
-
}
|
|
1452
|
-
},
|
|
1453
|
-
required: ["function", "alias"]
|
|
1454
|
-
},
|
|
1455
|
-
description: "Aggregation definitions"
|
|
1456
|
-
},
|
|
1457
|
-
groupBy: {
|
|
1458
|
-
type: "array",
|
|
1459
|
-
items: { type: "string" },
|
|
1460
|
-
description: "Fields to group by"
|
|
1461
|
-
},
|
|
1462
|
-
where: {
|
|
1463
|
-
type: "object",
|
|
1464
|
-
description: "Filter conditions applied before aggregation"
|
|
1523
|
+
// ── Models ──────────────────────────────────────────────────
|
|
1524
|
+
{
|
|
1525
|
+
method: "GET",
|
|
1526
|
+
path: "/api/v1/ai/models",
|
|
1527
|
+
description: "List available models",
|
|
1528
|
+
auth: true,
|
|
1529
|
+
permissions: ["ai:read"],
|
|
1530
|
+
handler: async () => {
|
|
1531
|
+
try {
|
|
1532
|
+
const models = aiService.listModels ? await aiService.listModels() : [];
|
|
1533
|
+
return { status: 200, body: { models } };
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
logger.error("[AI Route] /models error", err instanceof Error ? err : void 0);
|
|
1536
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1537
|
+
}
|
|
1465
1538
|
}
|
|
1466
1539
|
},
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1488
|
-
function createDescribeObjectHandler(ctx) {
|
|
1489
|
-
return async (args) => {
|
|
1490
|
-
const { objectName } = args;
|
|
1491
|
-
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1492
|
-
if (!objectDef) {
|
|
1493
|
-
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1494
|
-
}
|
|
1495
|
-
const def = objectDef;
|
|
1496
|
-
const fields = def.fields ?? {};
|
|
1497
|
-
const fieldSummary = {};
|
|
1498
|
-
for (const [key, f] of Object.entries(fields)) {
|
|
1499
|
-
fieldSummary[key] = {
|
|
1500
|
-
type: f.type,
|
|
1501
|
-
label: f.label ?? key,
|
|
1502
|
-
required: f.required ?? false,
|
|
1503
|
-
...f.reference ? { reference: f.reference } : {},
|
|
1504
|
-
...f.options ? { options: f.options } : {}
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
return JSON.stringify({
|
|
1508
|
-
name: def.name,
|
|
1509
|
-
label: def.label ?? def.name,
|
|
1510
|
-
fields: fieldSummary
|
|
1511
|
-
});
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
function createQueryRecordsHandler(ctx) {
|
|
1515
|
-
return async (args) => {
|
|
1516
|
-
const {
|
|
1517
|
-
objectName,
|
|
1518
|
-
where,
|
|
1519
|
-
fields,
|
|
1520
|
-
orderBy,
|
|
1521
|
-
limit,
|
|
1522
|
-
offset
|
|
1523
|
-
} = args;
|
|
1524
|
-
const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
|
|
1525
|
-
const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
|
|
1526
|
-
const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
|
|
1527
|
-
const records = await ctx.dataEngine.find(objectName, {
|
|
1528
|
-
where,
|
|
1529
|
-
fields,
|
|
1530
|
-
orderBy,
|
|
1531
|
-
limit: safeLimit,
|
|
1532
|
-
offset: safeOffset
|
|
1533
|
-
});
|
|
1534
|
-
return JSON.stringify({ count: records.length, records });
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
function createGetRecordHandler(ctx) {
|
|
1538
|
-
return async (args) => {
|
|
1539
|
-
const { objectName, recordId, fields } = args;
|
|
1540
|
-
const record = await ctx.dataEngine.findOne(objectName, {
|
|
1541
|
-
where: { id: recordId },
|
|
1542
|
-
fields
|
|
1543
|
-
});
|
|
1544
|
-
if (!record) {
|
|
1545
|
-
return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
|
|
1546
|
-
}
|
|
1547
|
-
return JSON.stringify(record);
|
|
1548
|
-
};
|
|
1549
|
-
}
|
|
1550
|
-
var VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1551
|
-
"count",
|
|
1552
|
-
"sum",
|
|
1553
|
-
"avg",
|
|
1554
|
-
"min",
|
|
1555
|
-
"max",
|
|
1556
|
-
"count_distinct"
|
|
1557
|
-
]);
|
|
1558
|
-
function createAggregateDataHandler(ctx) {
|
|
1559
|
-
return async (args) => {
|
|
1560
|
-
const { objectName, aggregations, groupBy, where } = args;
|
|
1561
|
-
for (const a of aggregations) {
|
|
1562
|
-
if (!VALID_AGG_FUNCTIONS.has(a.function)) {
|
|
1563
|
-
return JSON.stringify({
|
|
1564
|
-
error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
const result = await ctx.dataEngine.aggregate(objectName, {
|
|
1569
|
-
where,
|
|
1570
|
-
groupBy,
|
|
1571
|
-
aggregations: aggregations.map((a) => ({
|
|
1572
|
-
function: a.function,
|
|
1573
|
-
field: a.field,
|
|
1574
|
-
alias: a.alias
|
|
1575
|
-
}))
|
|
1576
|
-
});
|
|
1577
|
-
return JSON.stringify(result);
|
|
1578
|
-
};
|
|
1579
|
-
}
|
|
1580
|
-
function registerDataTools(registry, context) {
|
|
1581
|
-
registry.register(LIST_OBJECTS_TOOL, createListObjectsHandler(context));
|
|
1582
|
-
registry.register(DESCRIBE_OBJECT_TOOL, createDescribeObjectHandler(context));
|
|
1583
|
-
registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
|
|
1584
|
-
registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
|
|
1585
|
-
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// src/tools/create-object.tool.ts
|
|
1589
|
-
var import_ai = require("@objectstack/spec/ai");
|
|
1590
|
-
var createObjectTool = (0, import_ai.defineTool)({
|
|
1591
|
-
name: "create_object",
|
|
1592
|
-
label: "Create Object",
|
|
1593
|
-
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.",
|
|
1594
|
-
category: "data",
|
|
1595
|
-
builtIn: true,
|
|
1596
|
-
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
1597
|
-
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
1598
|
-
// executes tool calls immediately without checking this flag. The flag
|
|
1599
|
-
// should only be set once server-side approval gating is implemented to
|
|
1600
|
-
// avoid giving users a false sense of safety.
|
|
1601
|
-
parameters: {
|
|
1602
|
-
type: "object",
|
|
1603
|
-
properties: {
|
|
1604
|
-
name: {
|
|
1605
|
-
type: "string",
|
|
1606
|
-
description: "Machine name for the object (snake_case, e.g. project_task)"
|
|
1607
|
-
},
|
|
1608
|
-
label: {
|
|
1609
|
-
type: "string",
|
|
1610
|
-
description: "Human-readable display name (e.g. Project Task)"
|
|
1611
|
-
},
|
|
1612
|
-
fields: {
|
|
1613
|
-
type: "array",
|
|
1614
|
-
description: "Initial fields to create with the object",
|
|
1615
|
-
items: {
|
|
1616
|
-
type: "object",
|
|
1617
|
-
properties: {
|
|
1618
|
-
name: { type: "string", description: "Field machine name (snake_case)" },
|
|
1619
|
-
label: { type: "string", description: "Field display name" },
|
|
1620
|
-
type: {
|
|
1621
|
-
type: "string",
|
|
1622
|
-
description: "Field data type",
|
|
1623
|
-
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
1624
|
-
},
|
|
1625
|
-
required: { type: "boolean", description: "Whether the field is required" }
|
|
1626
|
-
},
|
|
1627
|
-
required: ["name", "type"]
|
|
1540
|
+
// ── Conversations ──────────────────────────────────────────
|
|
1541
|
+
{
|
|
1542
|
+
method: "POST",
|
|
1543
|
+
path: "/api/v1/ai/conversations",
|
|
1544
|
+
description: "Create a conversation",
|
|
1545
|
+
auth: true,
|
|
1546
|
+
permissions: ["ai:conversations"],
|
|
1547
|
+
handler: async (req) => {
|
|
1548
|
+
try {
|
|
1549
|
+
if (req.body !== void 0 && req.body !== null && (typeof req.body !== "object" || Array.isArray(req.body))) {
|
|
1550
|
+
return { status: 400, body: { error: "Invalid request payload" } };
|
|
1551
|
+
}
|
|
1552
|
+
const options = { ...req.body ?? {} };
|
|
1553
|
+
if (req.user?.userId) {
|
|
1554
|
+
options.userId = req.user.userId;
|
|
1555
|
+
}
|
|
1556
|
+
const conversation = await conversationService.create(options);
|
|
1557
|
+
return { status: 201, body: conversation };
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
logger.error("[AI Route] POST /conversations error", err instanceof Error ? err : void 0);
|
|
1560
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1628
1561
|
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1562
|
+
}
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
method: "GET",
|
|
1566
|
+
path: "/api/v1/ai/conversations",
|
|
1567
|
+
description: "List conversations",
|
|
1568
|
+
auth: true,
|
|
1569
|
+
permissions: ["ai:conversations"],
|
|
1570
|
+
handler: async (req) => {
|
|
1571
|
+
try {
|
|
1572
|
+
const rawQuery = req.query ?? {};
|
|
1573
|
+
const options = { ...rawQuery };
|
|
1574
|
+
if (typeof rawQuery.limit === "string") {
|
|
1575
|
+
const parsedLimit = Number(rawQuery.limit);
|
|
1576
|
+
if (!Number.isFinite(parsedLimit) || parsedLimit <= 0 || !Number.isInteger(parsedLimit)) {
|
|
1577
|
+
return { status: 400, body: { error: "Invalid limit parameter" } };
|
|
1578
|
+
}
|
|
1579
|
+
options.limit = parsedLimit;
|
|
1580
|
+
}
|
|
1581
|
+
if (req.user?.userId) {
|
|
1582
|
+
options.userId = req.user.userId;
|
|
1583
|
+
}
|
|
1584
|
+
const conversations = await conversationService.list(options);
|
|
1585
|
+
return { status: 200, body: { conversations } };
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
logger.error("[AI Route] GET /conversations error", err instanceof Error ? err : void 0);
|
|
1588
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1636
1589
|
}
|
|
1637
1590
|
}
|
|
1638
1591
|
},
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
label: {
|
|
1664
|
-
type: "string",
|
|
1665
|
-
description: "Human-readable field label (e.g. Due Date)"
|
|
1666
|
-
},
|
|
1667
|
-
type: {
|
|
1668
|
-
type: "string",
|
|
1669
|
-
description: "Field data type",
|
|
1670
|
-
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
1671
|
-
},
|
|
1672
|
-
required: {
|
|
1673
|
-
type: "boolean",
|
|
1674
|
-
description: "Whether the field is required"
|
|
1675
|
-
},
|
|
1676
|
-
defaultValue: {
|
|
1677
|
-
description: "Default value for the field"
|
|
1678
|
-
},
|
|
1679
|
-
options: {
|
|
1680
|
-
type: "array",
|
|
1681
|
-
description: "Options for select/picklist fields",
|
|
1682
|
-
items: {
|
|
1683
|
-
type: "object",
|
|
1684
|
-
properties: {
|
|
1685
|
-
label: { type: "string" },
|
|
1686
|
-
value: {
|
|
1687
|
-
type: "string",
|
|
1688
|
-
description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
|
|
1689
|
-
pattern: "^[a-z_][a-z0-9_]*$"
|
|
1592
|
+
{
|
|
1593
|
+
method: "POST",
|
|
1594
|
+
path: "/api/v1/ai/conversations/:id/messages",
|
|
1595
|
+
description: "Add message to a conversation",
|
|
1596
|
+
auth: true,
|
|
1597
|
+
permissions: ["ai:conversations"],
|
|
1598
|
+
handler: async (req) => {
|
|
1599
|
+
const id = req.params?.id;
|
|
1600
|
+
if (!id) {
|
|
1601
|
+
return { status: 400, body: { error: "conversation id is required" } };
|
|
1602
|
+
}
|
|
1603
|
+
const message = req.body;
|
|
1604
|
+
const validationError = validateMessage(message);
|
|
1605
|
+
if (validationError) {
|
|
1606
|
+
return { status: 400, body: { error: validationError } };
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
if (req.user?.userId) {
|
|
1610
|
+
const existing = await conversationService.get(id);
|
|
1611
|
+
if (!existing) {
|
|
1612
|
+
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
1613
|
+
}
|
|
1614
|
+
if (existing.userId && existing.userId !== req.user.userId) {
|
|
1615
|
+
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
1690
1616
|
}
|
|
1691
1617
|
}
|
|
1618
|
+
const conversation = await conversationService.addMessage(id, message);
|
|
1619
|
+
return { status: 200, body: conversation };
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1622
|
+
if (msg.includes("not found")) {
|
|
1623
|
+
return { status: 404, body: { error: msg } };
|
|
1624
|
+
}
|
|
1625
|
+
logger.error("[AI Route] POST /conversations/:id/messages error", err instanceof Error ? err : void 0);
|
|
1626
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1692
1627
|
}
|
|
1693
|
-
},
|
|
1694
|
-
reference: {
|
|
1695
|
-
type: "string",
|
|
1696
|
-
description: "Referenced object name for lookup fields (snake_case, e.g. account)"
|
|
1697
1628
|
}
|
|
1698
1629
|
},
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
description: "Field properties to update (partial patch)",
|
|
1726
|
-
properties: {
|
|
1727
|
-
label: { type: "string", description: "New display label" },
|
|
1728
|
-
type: { type: "string", description: "New field type" },
|
|
1729
|
-
required: { type: "boolean", description: "Update required constraint" },
|
|
1730
|
-
defaultValue: { description: "New default value" }
|
|
1630
|
+
{
|
|
1631
|
+
method: "DELETE",
|
|
1632
|
+
path: "/api/v1/ai/conversations/:id",
|
|
1633
|
+
description: "Delete a conversation",
|
|
1634
|
+
auth: true,
|
|
1635
|
+
permissions: ["ai:conversations"],
|
|
1636
|
+
handler: async (req) => {
|
|
1637
|
+
const id = req.params?.id;
|
|
1638
|
+
if (!id) {
|
|
1639
|
+
return { status: 400, body: { error: "conversation id is required" } };
|
|
1640
|
+
}
|
|
1641
|
+
try {
|
|
1642
|
+
if (req.user?.userId) {
|
|
1643
|
+
const existing = await conversationService.get(id);
|
|
1644
|
+
if (!existing) {
|
|
1645
|
+
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
1646
|
+
}
|
|
1647
|
+
if (existing.userId && existing.userId !== req.user.userId) {
|
|
1648
|
+
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
await conversationService.delete(id);
|
|
1652
|
+
return { status: 204 };
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
logger.error("[AI Route] DELETE /conversations/:id error", err instanceof Error ? err : void 0);
|
|
1655
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1731
1656
|
}
|
|
1732
1657
|
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
}
|
|
1737
|
-
});
|
|
1658
|
+
}
|
|
1659
|
+
];
|
|
1660
|
+
}
|
|
1738
1661
|
|
|
1739
|
-
// src/
|
|
1740
|
-
var
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
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.",
|
|
1745
|
-
category: "data",
|
|
1746
|
-
builtIn: true,
|
|
1747
|
-
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
1748
|
-
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
1749
|
-
// executes tool calls immediately without checking this flag. The flag
|
|
1750
|
-
// should only be set once server-side approval gating is implemented.
|
|
1751
|
-
parameters: {
|
|
1752
|
-
type: "object",
|
|
1753
|
-
properties: {
|
|
1754
|
-
objectName: {
|
|
1755
|
-
type: "string",
|
|
1756
|
-
description: "Target object machine name (snake_case)"
|
|
1757
|
-
},
|
|
1758
|
-
fieldName: {
|
|
1759
|
-
type: "string",
|
|
1760
|
-
description: "Field machine name to delete (snake_case)"
|
|
1761
|
-
}
|
|
1762
|
-
},
|
|
1763
|
-
required: ["objectName", "fieldName"],
|
|
1764
|
-
additionalProperties: false
|
|
1662
|
+
// src/routes/agent-routes.ts
|
|
1663
|
+
var ALLOWED_AGENT_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
1664
|
+
function validateAgentMessage(raw) {
|
|
1665
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1666
|
+
return "each message must be an object";
|
|
1765
1667
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
var import_ai5 = require("@objectstack/spec/ai");
|
|
1770
|
-
var listMetadataObjectsTool = (0, import_ai5.defineTool)({
|
|
1771
|
-
name: "list_metadata_objects",
|
|
1772
|
-
label: "List Metadata Objects",
|
|
1773
|
-
description: "Lists all registered metadata objects (tables) in the current environment. Use this when the user wants to see what tables, entities, or data models are defined in metadata.",
|
|
1774
|
-
category: "data",
|
|
1775
|
-
builtIn: true,
|
|
1776
|
-
parameters: {
|
|
1777
|
-
type: "object",
|
|
1778
|
-
properties: {
|
|
1779
|
-
filter: {
|
|
1780
|
-
type: "string",
|
|
1781
|
-
description: "Optional name or label substring to filter objects"
|
|
1782
|
-
},
|
|
1783
|
-
includeFields: {
|
|
1784
|
-
type: "boolean",
|
|
1785
|
-
description: "Whether to include field summaries for each object (default: false)"
|
|
1786
|
-
}
|
|
1787
|
-
},
|
|
1788
|
-
additionalProperties: false
|
|
1668
|
+
const msg = raw;
|
|
1669
|
+
if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
|
|
1670
|
+
return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
|
|
1789
1671
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1672
|
+
const allowEmpty = msg.role === "assistant";
|
|
1673
|
+
return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
|
|
1674
|
+
}
|
|
1675
|
+
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
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
|
+
}
|
|
1806
1695
|
}
|
|
1807
1696
|
},
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
//
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
return SNAKE_CASE_RE.test(value);
|
|
1825
|
-
}
|
|
1826
|
-
function createCreateObjectHandler(ctx) {
|
|
1827
|
-
return async (args) => {
|
|
1828
|
-
const { name, label, fields, enableFeatures } = args;
|
|
1829
|
-
if (!name || !label) {
|
|
1830
|
-
return JSON.stringify({ error: 'Both "name" and "label" are required' });
|
|
1831
|
-
}
|
|
1832
|
-
if (!isSnakeCase(name)) {
|
|
1833
|
-
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
|
|
1834
|
-
}
|
|
1835
|
-
const existing = await ctx.metadataService.getObject(name);
|
|
1836
|
-
if (existing) {
|
|
1837
|
-
return JSON.stringify({ error: `Object "${name}" already exists` });
|
|
1838
|
-
}
|
|
1839
|
-
const fieldMap = {};
|
|
1840
|
-
if (fields && Array.isArray(fields)) {
|
|
1841
|
-
const seenNames = /* @__PURE__ */ new Set();
|
|
1842
|
-
for (const f of fields) {
|
|
1843
|
-
if (!f.name) {
|
|
1844
|
-
return JSON.stringify({ error: 'Each field must have a "name" property' });
|
|
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
|
+
//
|
|
1703
|
+
{
|
|
1704
|
+
method: "POST",
|
|
1705
|
+
path: "/api/v1/ai/agents/:agentName/chat",
|
|
1706
|
+
description: "Chat with a specific AI agent (supports Vercel AI Data Stream Protocol)",
|
|
1707
|
+
auth: true,
|
|
1708
|
+
permissions: ["ai:chat", "ai:agents"],
|
|
1709
|
+
handler: async (req) => {
|
|
1710
|
+
const agentName = req.params?.agentName;
|
|
1711
|
+
if (!agentName) {
|
|
1712
|
+
return { status: 400, body: { error: "agentName parameter is required" } };
|
|
1845
1713
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1714
|
+
const body = req.body ?? {};
|
|
1715
|
+
const {
|
|
1716
|
+
messages: rawMessages,
|
|
1717
|
+
context: chatContext,
|
|
1718
|
+
options: extraOptions
|
|
1719
|
+
} = body;
|
|
1720
|
+
if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
|
|
1721
|
+
return { status: 400, body: { error: "messages array is required" } };
|
|
1848
1722
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1723
|
+
for (const msg of rawMessages) {
|
|
1724
|
+
const err = validateAgentMessage(msg);
|
|
1725
|
+
if (err) return { status: 400, body: { error: err } };
|
|
1726
|
+
}
|
|
1727
|
+
const agent = await agentRuntime.loadAgent(agentName);
|
|
1728
|
+
if (!agent) {
|
|
1729
|
+
return { status: 404, body: { error: `Agent "${agentName}" not found` } };
|
|
1730
|
+
}
|
|
1731
|
+
if (!agent.active) {
|
|
1732
|
+
return { status: 403, body: { error: `Agent "${agentName}" is not active` } };
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext);
|
|
1736
|
+
const agentOptions = agentRuntime.buildRequestOptions(
|
|
1737
|
+
agent,
|
|
1738
|
+
aiService.toolRegistry.getAll()
|
|
1739
|
+
);
|
|
1740
|
+
const safeOverrides = {};
|
|
1741
|
+
if (extraOptions) {
|
|
1742
|
+
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["temperature", "maxTokens", "stop"]);
|
|
1743
|
+
for (const key of Object.keys(extraOptions)) {
|
|
1744
|
+
if (ALLOWED_KEYS.has(key)) {
|
|
1745
|
+
safeOverrides[key] = extraOptions[key];
|
|
1746
|
+
}
|
|
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" } };
|
|
1851
1786
|
}
|
|
1852
|
-
seenNames.add(f.name);
|
|
1853
|
-
fieldMap[f.name] = {
|
|
1854
|
-
type: f.type,
|
|
1855
|
-
...f.label ? { label: f.label } : {},
|
|
1856
|
-
...f.required !== void 0 ? { required: f.required } : {}
|
|
1857
|
-
};
|
|
1858
1787
|
}
|
|
1859
1788
|
}
|
|
1860
|
-
|
|
1861
|
-
name,
|
|
1862
|
-
label,
|
|
1863
|
-
...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
|
|
1864
|
-
...enableFeatures ? { enable: enableFeatures } : {}
|
|
1865
|
-
};
|
|
1866
|
-
await ctx.metadataService.register("object", name, objectDef);
|
|
1867
|
-
return JSON.stringify({
|
|
1868
|
-
name,
|
|
1869
|
-
label,
|
|
1870
|
-
fieldCount: Object.keys(fieldMap).length
|
|
1871
|
-
});
|
|
1872
|
-
};
|
|
1789
|
+
];
|
|
1873
1790
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
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
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
status: 200,
|
|
1882
|
+
body: {
|
|
1883
|
+
result: extractOutputValue(result.output),
|
|
1884
|
+
duration,
|
|
1885
|
+
toolName
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
logger.error(
|
|
1890
|
+
"[AI Route] /tools/:toolName/execute error",
|
|
1891
|
+
err instanceof Error ? err : void 0
|
|
1892
|
+
);
|
|
1893
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
1893
1894
|
}
|
|
1894
1895
|
}
|
|
1895
1896
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1897
|
+
];
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/conversation/objectql-conversation-service.ts
|
|
1901
|
+
var import_node_crypto = require("crypto");
|
|
1902
|
+
var CONVERSATIONS_OBJECT = "ai_conversations";
|
|
1903
|
+
var MESSAGES_OBJECT = "ai_messages";
|
|
1904
|
+
var CONVERSATION_ORDER = [
|
|
1905
|
+
{ field: "created_at", order: "asc" },
|
|
1906
|
+
{ field: "id", order: "asc" }
|
|
1907
|
+
];
|
|
1908
|
+
var MESSAGE_ORDER = [
|
|
1909
|
+
{ field: "created_at", order: "asc" },
|
|
1910
|
+
{ field: "id", order: "asc" }
|
|
1911
|
+
];
|
|
1912
|
+
var ObjectQLConversationService = class {
|
|
1913
|
+
constructor(engine) {
|
|
1914
|
+
this.engine = engine;
|
|
1915
|
+
}
|
|
1916
|
+
async create(options = {}) {
|
|
1917
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1918
|
+
const id = `conv_${(0, import_node_crypto.randomUUID)()}`;
|
|
1919
|
+
const record = {
|
|
1920
|
+
id,
|
|
1921
|
+
title: options.title ?? null,
|
|
1922
|
+
agent_id: options.agentId ?? null,
|
|
1923
|
+
user_id: options.userId ?? null,
|
|
1924
|
+
metadata: options.metadata ? JSON.stringify(options.metadata) : null,
|
|
1925
|
+
created_at: now,
|
|
1926
|
+
updated_at: now
|
|
1911
1927
|
};
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1928
|
+
await this.engine.insert(CONVERSATIONS_OBJECT, record);
|
|
1929
|
+
return {
|
|
1930
|
+
id,
|
|
1931
|
+
title: options.title,
|
|
1932
|
+
agentId: options.agentId,
|
|
1933
|
+
userId: options.userId,
|
|
1934
|
+
messages: [],
|
|
1935
|
+
createdAt: now,
|
|
1936
|
+
updatedAt: now,
|
|
1937
|
+
metadata: options.metadata
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
async get(conversationId) {
|
|
1941
|
+
const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
|
|
1942
|
+
where: { id: conversationId }
|
|
1916
1943
|
});
|
|
1917
|
-
return
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1944
|
+
if (!row) return null;
|
|
1945
|
+
const messages = await this.engine.find(MESSAGES_OBJECT, {
|
|
1946
|
+
where: { conversation_id: conversationId },
|
|
1947
|
+
orderBy: MESSAGE_ORDER
|
|
1921
1948
|
});
|
|
1922
|
-
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
if (
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
}
|
|
1940
|
-
const def = objectDef;
|
|
1941
|
-
if (!def.fields || !def.fields[fieldName]) {
|
|
1942
|
-
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
1949
|
+
return this.toConversation(row, messages);
|
|
1950
|
+
}
|
|
1951
|
+
async list(options = {}) {
|
|
1952
|
+
const where = {};
|
|
1953
|
+
if (options.userId) where.user_id = options.userId;
|
|
1954
|
+
if (options.agentId) where.agent_id = options.agentId;
|
|
1955
|
+
if (options.cursor) {
|
|
1956
|
+
const cursorRow = await this.engine.findOne(CONVERSATIONS_OBJECT, {
|
|
1957
|
+
where: { id: options.cursor },
|
|
1958
|
+
fields: ["created_at", "id"]
|
|
1959
|
+
});
|
|
1960
|
+
if (cursorRow) {
|
|
1961
|
+
where.$or = [
|
|
1962
|
+
{ created_at: { $gt: cursorRow.created_at } },
|
|
1963
|
+
{ created_at: cursorRow.created_at, id: { $gt: cursorRow.id } }
|
|
1964
|
+
];
|
|
1965
|
+
}
|
|
1943
1966
|
}
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
...def,
|
|
1949
|
-
fields: updatedFields
|
|
1967
|
+
const rows = await this.engine.find(CONVERSATIONS_OBJECT, {
|
|
1968
|
+
where: Object.keys(where).length > 0 ? where : void 0,
|
|
1969
|
+
orderBy: CONVERSATION_ORDER,
|
|
1970
|
+
limit: options.limit && options.limit > 0 ? options.limit : void 0
|
|
1950
1971
|
});
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1972
|
+
const conversations = await Promise.all(
|
|
1973
|
+
rows.map(async (row) => {
|
|
1974
|
+
const messages = await this.engine.find(MESSAGES_OBJECT, {
|
|
1975
|
+
where: { conversation_id: row.id },
|
|
1976
|
+
orderBy: MESSAGE_ORDER
|
|
1977
|
+
});
|
|
1978
|
+
return this.toConversation(row, messages);
|
|
1979
|
+
})
|
|
1980
|
+
);
|
|
1981
|
+
return conversations;
|
|
1982
|
+
}
|
|
1983
|
+
async addMessage(conversationId, message) {
|
|
1984
|
+
const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
|
|
1985
|
+
where: { id: conversationId }
|
|
1955
1986
|
});
|
|
1956
|
-
|
|
1957
|
-
}
|
|
1958
|
-
function createDeleteFieldHandler(ctx) {
|
|
1959
|
-
return async (args) => {
|
|
1960
|
-
const { objectName, fieldName } = args;
|
|
1961
|
-
if (!objectName || !fieldName) {
|
|
1962
|
-
return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
|
|
1963
|
-
}
|
|
1964
|
-
if (!isSnakeCase(objectName)) {
|
|
1965
|
-
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1966
|
-
}
|
|
1967
|
-
if (!isSnakeCase(fieldName)) {
|
|
1968
|
-
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
1969
|
-
}
|
|
1970
|
-
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1971
|
-
if (!objectDef) {
|
|
1972
|
-
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1973
|
-
}
|
|
1974
|
-
const def = objectDef;
|
|
1975
|
-
if (!def.fields || !def.fields[fieldName]) {
|
|
1976
|
-
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
1987
|
+
if (!row) {
|
|
1988
|
+
throw new Error(`Conversation "${conversationId}" not found`);
|
|
1977
1989
|
}
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
let result = objects.map((o) => {
|
|
1995
|
-
const base = {
|
|
1996
|
-
name: o.name,
|
|
1997
|
-
label: o.label ?? o.name,
|
|
1998
|
-
fieldCount: o.fields ? Object.keys(o.fields).length : 0
|
|
1999
|
-
};
|
|
2000
|
-
if (includeFields && o.fields) {
|
|
2001
|
-
base.fields = Object.entries(o.fields).map(([key, f]) => ({
|
|
2002
|
-
name: key,
|
|
2003
|
-
type: f.type,
|
|
2004
|
-
label: f.label ?? key
|
|
2005
|
-
}));
|
|
1990
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
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
2006
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
);
|
|
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 = "";
|
|
2014
2013
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2014
|
+
await this.engine.insert(MESSAGES_OBJECT, {
|
|
2015
|
+
id: msgId,
|
|
2016
|
+
conversation_id: conversationId,
|
|
2017
|
+
role: message.role,
|
|
2018
|
+
content: contentStr,
|
|
2019
|
+
tool_calls: toolCallsJson,
|
|
2020
|
+
tool_call_id: toolCallId,
|
|
2021
|
+
created_at: now
|
|
2018
2022
|
});
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2023
|
+
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
2024
|
+
where: { id: conversationId }
|
|
2025
|
+
});
|
|
2026
|
+
return await this.get(conversationId);
|
|
2027
|
+
}
|
|
2028
|
+
async delete(conversationId) {
|
|
2029
|
+
await this.engine.delete(MESSAGES_OBJECT, {
|
|
2030
|
+
where: { conversation_id: conversationId },
|
|
2031
|
+
multi: true
|
|
2032
|
+
});
|
|
2033
|
+
await this.engine.delete(CONVERSATIONS_OBJECT, {
|
|
2034
|
+
where: { id: conversationId }
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
2038
|
+
/**
|
|
2039
|
+
* Safely parse a JSON string, returning `undefined` on failure.
|
|
2040
|
+
*/
|
|
2041
|
+
safeParse(value, fallback) {
|
|
2042
|
+
if (!value) return void 0;
|
|
2043
|
+
try {
|
|
2044
|
+
return JSON.parse(value);
|
|
2045
|
+
} catch {
|
|
2046
|
+
return fallback;
|
|
2029
2047
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Map a database row + message rows to an AIConversation.
|
|
2051
|
+
*/
|
|
2052
|
+
toConversation(row, messageRows) {
|
|
2053
|
+
return {
|
|
2054
|
+
id: row.id,
|
|
2055
|
+
title: row.title ?? void 0,
|
|
2056
|
+
agentId: row.agent_id ?? void 0,
|
|
2057
|
+
userId: row.user_id ?? void 0,
|
|
2058
|
+
messages: messageRows.map((m) => this.toMessage(m)),
|
|
2059
|
+
createdAt: row.created_at,
|
|
2060
|
+
updatedAt: row.updated_at,
|
|
2061
|
+
metadata: this.safeParse(row.metadata)
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Map a database row to a ModelMessage.
|
|
2066
|
+
*/
|
|
2067
|
+
toMessage(row) {
|
|
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 };
|
|
2033
2100
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
// src/objects/ai-conversation.object.ts
|
|
2105
|
+
var import_data = require("@objectstack/spec/data");
|
|
2106
|
+
var AiConversationObject = import_data.ObjectSchema.create({
|
|
2107
|
+
namespace: "ai",
|
|
2108
|
+
name: "conversations",
|
|
2109
|
+
label: "AI Conversation",
|
|
2110
|
+
pluralLabel: "AI Conversations",
|
|
2111
|
+
icon: "message-square",
|
|
2112
|
+
isSystem: true,
|
|
2113
|
+
description: "Persistent AI conversation metadata",
|
|
2114
|
+
fields: {
|
|
2115
|
+
id: import_data.Field.text({
|
|
2116
|
+
label: "Conversation ID",
|
|
2117
|
+
required: true,
|
|
2118
|
+
readonly: true
|
|
2119
|
+
}),
|
|
2120
|
+
title: import_data.Field.text({
|
|
2121
|
+
label: "Title",
|
|
2122
|
+
required: false,
|
|
2123
|
+
maxLength: 500,
|
|
2124
|
+
description: "Conversation title or summary"
|
|
2125
|
+
}),
|
|
2126
|
+
agent_id: import_data.Field.text({
|
|
2127
|
+
label: "Agent ID",
|
|
2128
|
+
required: false,
|
|
2129
|
+
maxLength: 255,
|
|
2130
|
+
description: "Associated AI agent identifier"
|
|
2131
|
+
}),
|
|
2132
|
+
user_id: import_data.Field.text({
|
|
2133
|
+
label: "User ID",
|
|
2134
|
+
required: false,
|
|
2135
|
+
maxLength: 255,
|
|
2136
|
+
description: "User who owns the conversation"
|
|
2137
|
+
}),
|
|
2138
|
+
metadata: import_data.Field.textarea({
|
|
2139
|
+
label: "Metadata",
|
|
2140
|
+
required: false,
|
|
2141
|
+
description: "JSON-serialized conversation metadata"
|
|
2142
|
+
}),
|
|
2143
|
+
created_at: import_data.Field.datetime({
|
|
2144
|
+
label: "Created At",
|
|
2145
|
+
required: true,
|
|
2146
|
+
defaultValue: "NOW()",
|
|
2147
|
+
readonly: true
|
|
2148
|
+
}),
|
|
2149
|
+
updated_at: import_data.Field.datetime({
|
|
2150
|
+
label: "Updated At",
|
|
2151
|
+
required: true,
|
|
2152
|
+
defaultValue: "NOW()",
|
|
2153
|
+
readonly: true
|
|
2154
|
+
})
|
|
2155
|
+
},
|
|
2156
|
+
indexes: [
|
|
2157
|
+
{ fields: ["user_id"] },
|
|
2158
|
+
{ fields: ["agent_id"] },
|
|
2159
|
+
{ fields: ["created_at"] }
|
|
2160
|
+
],
|
|
2161
|
+
enable: {
|
|
2162
|
+
trackHistory: false,
|
|
2163
|
+
searchable: false,
|
|
2164
|
+
apiEnabled: true,
|
|
2165
|
+
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
2166
|
+
trash: false,
|
|
2167
|
+
mru: false
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
// src/objects/ai-message.object.ts
|
|
2172
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
2173
|
+
var AiMessageObject = import_data2.ObjectSchema.create({
|
|
2174
|
+
namespace: "ai",
|
|
2175
|
+
name: "messages",
|
|
2176
|
+
label: "AI Message",
|
|
2177
|
+
pluralLabel: "AI Messages",
|
|
2178
|
+
icon: "message-circle",
|
|
2179
|
+
isSystem: true,
|
|
2180
|
+
description: "Individual messages within AI conversations",
|
|
2181
|
+
fields: {
|
|
2182
|
+
id: import_data2.Field.text({
|
|
2183
|
+
label: "Message ID",
|
|
2184
|
+
required: true,
|
|
2185
|
+
readonly: true
|
|
2186
|
+
}),
|
|
2187
|
+
conversation_id: import_data2.Field.text({
|
|
2188
|
+
label: "Conversation ID",
|
|
2189
|
+
required: true,
|
|
2190
|
+
description: "Foreign key to ai_conversations"
|
|
2191
|
+
}),
|
|
2192
|
+
role: import_data2.Field.select({
|
|
2193
|
+
label: "Role",
|
|
2194
|
+
required: true,
|
|
2195
|
+
options: [
|
|
2196
|
+
{ label: "System", value: "system" },
|
|
2197
|
+
{ label: "User", value: "user" },
|
|
2198
|
+
{ label: "Assistant", value: "assistant" },
|
|
2199
|
+
{ label: "Tool", value: "tool" }
|
|
2200
|
+
]
|
|
2201
|
+
}),
|
|
2202
|
+
content: import_data2.Field.textarea({
|
|
2203
|
+
label: "Content",
|
|
2204
|
+
required: true,
|
|
2205
|
+
description: "Message content"
|
|
2206
|
+
}),
|
|
2207
|
+
tool_calls: import_data2.Field.textarea({
|
|
2208
|
+
label: "Tool Calls",
|
|
2209
|
+
required: false,
|
|
2210
|
+
description: "JSON-serialized tool calls (when role=assistant)"
|
|
2211
|
+
}),
|
|
2212
|
+
tool_call_id: import_data2.Field.text({
|
|
2213
|
+
label: "Tool Call ID",
|
|
2214
|
+
required: false,
|
|
2215
|
+
maxLength: 255,
|
|
2216
|
+
description: "ID of the tool call this message responds to (when role=tool)"
|
|
2217
|
+
}),
|
|
2218
|
+
created_at: import_data2.Field.datetime({
|
|
2219
|
+
label: "Created At",
|
|
2220
|
+
required: true,
|
|
2221
|
+
defaultValue: "NOW()",
|
|
2222
|
+
readonly: true
|
|
2223
|
+
})
|
|
2224
|
+
},
|
|
2225
|
+
indexes: [
|
|
2226
|
+
{ fields: ["conversation_id"] },
|
|
2227
|
+
{ fields: ["conversation_id", "created_at"] }
|
|
2228
|
+
],
|
|
2229
|
+
enable: {
|
|
2230
|
+
trackHistory: false,
|
|
2231
|
+
searchable: false,
|
|
2232
|
+
apiEnabled: true,
|
|
2233
|
+
apiMethods: ["get", "list", "create"],
|
|
2234
|
+
trash: false,
|
|
2235
|
+
mru: false
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
// src/plugin.ts
|
|
2240
|
+
init_data_tools();
|
|
2241
|
+
init_metadata_tools();
|
|
2060
2242
|
|
|
2061
2243
|
// src/agent-runtime.ts
|
|
2062
2244
|
var import_ai7 = require("@objectstack/spec/ai");
|
|
@@ -2230,8 +2412,8 @@ Capabilities:
|
|
|
2230
2412
|
- Describe the full schema of a specific object
|
|
2231
2413
|
|
|
2232
2414
|
Guidelines:
|
|
2233
|
-
1. Before creating a new object, use
|
|
2234
|
-
2. Before modifying or deleting fields, use
|
|
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.
|
|
2235
2417
|
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
|
|
2236
2418
|
4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
|
|
2237
2419
|
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
|
|
@@ -2251,8 +2433,8 @@ Guidelines:
|
|
|
2251
2433
|
{ type: "action", name: "add_field", description: "Add a field to an existing object" },
|
|
2252
2434
|
{ type: "action", name: "modify_field", description: "Modify an existing field definition" },
|
|
2253
2435
|
{ type: "action", name: "delete_field", description: "Delete a field from an object" },
|
|
2254
|
-
{ type: "query", name: "
|
|
2255
|
-
{ type: "query", name: "
|
|
2436
|
+
{ type: "query", name: "list_objects", description: "List all data objects" },
|
|
2437
|
+
{ type: "query", name: "describe_object", description: "Describe an object schema" }
|
|
2256
2438
|
],
|
|
2257
2439
|
active: true,
|
|
2258
2440
|
visibility: "global",
|
|
@@ -2503,8 +2685,8 @@ var AIServicePlugin = class {
|
|
|
2503
2685
|
setupNav.contribute({
|
|
2504
2686
|
areaId: "area_ai",
|
|
2505
2687
|
items: [
|
|
2506
|
-
{ id: "nav_ai_conversations", type: "object", label:
|
|
2507
|
-
{ id: "nav_ai_messages", type: "object", label:
|
|
2688
|
+
{ id: "nav_ai_conversations", type: "object", label: "Conversations", objectName: "conversations", icon: "message-square", order: 10 },
|
|
2689
|
+
{ id: "nav_ai_messages", type: "object", label: "Messages", objectName: "messages", icon: "messages-square", order: 20 }
|
|
2508
2690
|
]
|
|
2509
2691
|
});
|
|
2510
2692
|
ctx.logger.info("[AI] Navigation items contributed to Setup App");
|
|
@@ -2518,20 +2700,40 @@ var AIServicePlugin = class {
|
|
|
2518
2700
|
let metadataService;
|
|
2519
2701
|
try {
|
|
2520
2702
|
metadataService = ctx.getService("metadata");
|
|
2521
|
-
|
|
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);
|
|
2522
2706
|
ctx.logger.debug("[AI] Metadata service not available");
|
|
2523
2707
|
}
|
|
2524
2708
|
try {
|
|
2525
2709
|
const dataEngine = ctx.getService("data");
|
|
2526
|
-
if (dataEngine
|
|
2527
|
-
registerDataTools(this.service.toolRegistry, { dataEngine
|
|
2710
|
+
if (dataEngine) {
|
|
2711
|
+
registerDataTools(this.service.toolRegistry, { dataEngine });
|
|
2528
2712
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
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
|
+
}
|
|
2535
2737
|
}
|
|
2536
2738
|
}
|
|
2537
2739
|
} catch {
|
|
@@ -2541,12 +2743,26 @@ var AIServicePlugin = class {
|
|
|
2541
2743
|
try {
|
|
2542
2744
|
registerMetadataTools(this.service.toolRegistry, { metadataService });
|
|
2543
2745
|
ctx.logger.info("[AI] Built-in metadata tools registered");
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
await metadataService.
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
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) });
|
|
2550
2766
|
}
|
|
2551
2767
|
} catch (err) {
|
|
2552
2768
|
ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
|
|
@@ -2554,14 +2770,15 @@ var AIServicePlugin = class {
|
|
|
2554
2770
|
}
|
|
2555
2771
|
await ctx.trigger("ai:ready", this.service);
|
|
2556
2772
|
const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
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 {
|
|
2565
2782
|
ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
|
|
2566
2783
|
}
|
|
2567
2784
|
await ctx.trigger("ai:routes", routes);
|
|
@@ -2577,6 +2794,11 @@ var AIServicePlugin = class {
|
|
|
2577
2794
|
this.service = void 0;
|
|
2578
2795
|
}
|
|
2579
2796
|
};
|
|
2797
|
+
|
|
2798
|
+
// src/index.ts
|
|
2799
|
+
init_data_tools();
|
|
2800
|
+
init_metadata_tools();
|
|
2801
|
+
init_metadata_tools();
|
|
2580
2802
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2581
2803
|
0 && (module.exports = {
|
|
2582
2804
|
AIService,
|
|
@@ -2596,12 +2818,13 @@ var AIServicePlugin = class {
|
|
|
2596
2818
|
addFieldTool,
|
|
2597
2819
|
buildAIRoutes,
|
|
2598
2820
|
buildAgentRoutes,
|
|
2821
|
+
buildToolRoutes,
|
|
2599
2822
|
createObjectTool,
|
|
2600
2823
|
deleteFieldTool,
|
|
2601
|
-
|
|
2824
|
+
describeObjectTool,
|
|
2602
2825
|
encodeStreamPart,
|
|
2603
2826
|
encodeVercelDataStream,
|
|
2604
|
-
|
|
2827
|
+
listObjectsTool,
|
|
2605
2828
|
modifyFieldTool,
|
|
2606
2829
|
registerDataTools,
|
|
2607
2830
|
registerMetadataTools
|