@objectstack/service-ai 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +20 -0
- package/dist/index.cjs +1245 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -77
- package/dist/index.d.ts +344 -77
- package/dist/index.js +1230 -51
- package/dist/index.js.map +1 -1
- package/package.json +26 -4
- package/src/__tests__/ai-service.test.ts +248 -27
- package/src/__tests__/auth-and-toolcalling.test.ts +627 -0
- package/src/__tests__/chatbot-features.test.ts +229 -82
- package/src/__tests__/metadata-tools.test.ts +964 -0
- package/src/__tests__/objectql-conversation-service.test.ts +34 -16
- package/src/__tests__/vercel-stream-encoder.test.ts +263 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/memory-adapter.ts +17 -9
- package/src/adapters/vercel-adapter.ts +148 -0
- package/src/agent-runtime.ts +27 -3
- package/src/agents/index.ts +1 -0
- package/src/agents/metadata-assistant-agent.ts +87 -0
- package/src/ai-service.ts +174 -22
- package/src/conversation/in-memory-conversation-service.ts +2 -2
- package/src/conversation/objectql-conversation-service.ts +67 -18
- package/src/index.ts +22 -3
- package/src/plugin.ts +166 -9
- package/src/routes/agent-routes.ts +28 -3
- package/src/routes/ai-routes.ts +231 -14
- package/src/routes/index.ts +1 -1
- package/src/stream/index.ts +3 -0
- package/src/stream/vercel-stream-encoder.ts +129 -0
- package/src/tools/add-field.tool.ts +70 -0
- package/src/tools/create-object.tool.ts +66 -0
- package/src/tools/delete-field.tool.ts +38 -0
- package/src/tools/describe-metadata-object.tool.ts +32 -0
- package/src/tools/index.ts +12 -1
- package/src/tools/list-metadata-objects.tool.ts +34 -0
- package/src/tools/metadata-tools.ts +430 -0
- package/src/tools/modify-field.tool.ts +44 -0
- package/src/tools/tool-registry.ts +32 -9
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ var MemoryLLMAdapter = class {
|
|
|
8
8
|
}
|
|
9
9
|
async chat(messages, options) {
|
|
10
10
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
11
|
-
const
|
|
11
|
+
const userContent = lastUserMessage?.content;
|
|
12
|
+
const text = typeof userContent === "string" ? userContent : "(complex content)";
|
|
13
|
+
const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
|
|
12
14
|
return {
|
|
13
15
|
content,
|
|
14
16
|
model: options?.model ?? "memory",
|
|
@@ -26,10 +28,15 @@ var MemoryLLMAdapter = class {
|
|
|
26
28
|
const result = await this.chat(messages);
|
|
27
29
|
const words = result.content.split(" ");
|
|
28
30
|
for (let i = 0; i < words.length; i++) {
|
|
29
|
-
const
|
|
30
|
-
yield { type: "text-delta",
|
|
31
|
+
const wordText = i === 0 ? words[i] : ` ${words[i]}`;
|
|
32
|
+
yield { type: "text-delta", id: `delta_${i}`, text: wordText };
|
|
31
33
|
}
|
|
32
|
-
yield {
|
|
34
|
+
yield {
|
|
35
|
+
type: "finish",
|
|
36
|
+
finishReason: "stop",
|
|
37
|
+
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
38
|
+
rawFinishReason: "stop"
|
|
39
|
+
};
|
|
33
40
|
}
|
|
34
41
|
async embed(input) {
|
|
35
42
|
const texts = Array.isArray(input) ? input : [input];
|
|
@@ -92,21 +99,34 @@ var ToolRegistry = class {
|
|
|
92
99
|
* Execute a tool call and return the result.
|
|
93
100
|
*/
|
|
94
101
|
async execute(toolCall) {
|
|
95
|
-
const handler = this.handlers.get(toolCall.
|
|
102
|
+
const handler = this.handlers.get(toolCall.toolName);
|
|
96
103
|
if (!handler) {
|
|
97
104
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
type: "tool-result",
|
|
106
|
+
toolCallId: toolCall.toolCallId,
|
|
107
|
+
toolName: toolCall.toolName,
|
|
108
|
+
output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
|
|
100
109
|
isError: true
|
|
101
110
|
};
|
|
102
111
|
}
|
|
103
112
|
try {
|
|
104
|
-
const args = JSON.parse(toolCall.
|
|
113
|
+
const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
|
|
105
114
|
const content = await handler(args);
|
|
106
|
-
return {
|
|
115
|
+
return {
|
|
116
|
+
type: "tool-result",
|
|
117
|
+
toolCallId: toolCall.toolCallId,
|
|
118
|
+
toolName: toolCall.toolName,
|
|
119
|
+
output: { type: "text", value: content }
|
|
120
|
+
};
|
|
107
121
|
} catch (err) {
|
|
108
122
|
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
type: "tool-result",
|
|
125
|
+
toolCallId: toolCall.toolCallId,
|
|
126
|
+
toolName: toolCall.toolName,
|
|
127
|
+
output: { type: "text", value: message },
|
|
128
|
+
isError: true
|
|
129
|
+
};
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
/**
|
|
@@ -192,6 +212,17 @@ var InMemoryConversationService = class {
|
|
|
192
212
|
};
|
|
193
213
|
|
|
194
214
|
// src/ai-service.ts
|
|
215
|
+
function textDeltaPart(id, text) {
|
|
216
|
+
return { type: "text-delta", id, text };
|
|
217
|
+
}
|
|
218
|
+
function finishPart(result) {
|
|
219
|
+
return {
|
|
220
|
+
type: "finish",
|
|
221
|
+
finishReason: "stop",
|
|
222
|
+
totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
223
|
+
rawFinishReason: "stop"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
195
226
|
var _AIService = class _AIService {
|
|
196
227
|
constructor(config = {}) {
|
|
197
228
|
this.adapter = config.adapter ?? new MemoryLLMAdapter();
|
|
@@ -219,8 +250,8 @@ var _AIService = class _AIService {
|
|
|
219
250
|
this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
|
|
220
251
|
if (!this.adapter.streamChat) {
|
|
221
252
|
const result = await this.adapter.chat(messages, options);
|
|
222
|
-
yield
|
|
223
|
-
yield
|
|
253
|
+
yield textDeltaPart("fallback", result.content);
|
|
254
|
+
yield finishPart(result);
|
|
224
255
|
return;
|
|
225
256
|
}
|
|
226
257
|
yield* this.adapter.streamChat(messages, options);
|
|
@@ -237,6 +268,10 @@ var _AIService = class _AIService {
|
|
|
237
268
|
}
|
|
238
269
|
return this.adapter.listModels();
|
|
239
270
|
}
|
|
271
|
+
/** Extract the text value from a ToolExecutionResult's output. */
|
|
272
|
+
static extractOutputText(tr) {
|
|
273
|
+
return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
|
|
274
|
+
}
|
|
240
275
|
/**
|
|
241
276
|
* Chat with automatic tool call resolution.
|
|
242
277
|
*
|
|
@@ -249,7 +284,7 @@ var _AIService = class _AIService {
|
|
|
249
284
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
250
285
|
*/
|
|
251
286
|
async chatWithTools(messages, options) {
|
|
252
|
-
const { maxIterations: maxIter, ...restOptions } = options ?? {};
|
|
287
|
+
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
253
288
|
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
254
289
|
const registeredTools = this.toolRegistry.getAll();
|
|
255
290
|
const mergedTools = [
|
|
@@ -262,11 +297,13 @@ var _AIService = class _AIService {
|
|
|
262
297
|
toolChoice: mergedTools.length > 0 ? restOptions.toolChoice ?? "auto" : void 0
|
|
263
298
|
};
|
|
264
299
|
const conversation = [...messages];
|
|
300
|
+
const toolErrors = [];
|
|
265
301
|
this.logger.debug("[AI] chatWithTools start", {
|
|
266
302
|
messageCount: conversation.length,
|
|
267
303
|
toolCount: mergedTools.length,
|
|
268
304
|
maxIterations
|
|
269
305
|
});
|
|
306
|
+
let abortedByCallback = false;
|
|
270
307
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
271
308
|
const result = await this.adapter.chat(conversation, chatOptions);
|
|
272
309
|
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
@@ -275,23 +312,47 @@ var _AIService = class _AIService {
|
|
|
275
312
|
}
|
|
276
313
|
this.logger.debug("[AI] chatWithTools tool calls", {
|
|
277
314
|
iteration,
|
|
278
|
-
calls: result.toolCalls.map((tc) => tc.
|
|
315
|
+
calls: result.toolCalls.map((tc) => tc.toolName)
|
|
279
316
|
});
|
|
317
|
+
const assistantContent = [];
|
|
318
|
+
if (result.content) assistantContent.push({ type: "text", text: result.content });
|
|
319
|
+
assistantContent.push(...result.toolCalls);
|
|
280
320
|
conversation.push({
|
|
281
321
|
role: "assistant",
|
|
282
|
-
content:
|
|
283
|
-
toolCalls: result.toolCalls
|
|
322
|
+
content: assistantContent
|
|
284
323
|
});
|
|
285
324
|
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
286
325
|
for (const tr of toolResults) {
|
|
326
|
+
if (tr.isError) {
|
|
327
|
+
const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
328
|
+
const toolName = matchedCall?.toolName ?? "unknown";
|
|
329
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
330
|
+
const errorEntry = { iteration, toolName, error: errorText };
|
|
331
|
+
toolErrors.push(errorEntry);
|
|
332
|
+
this.logger.warn("[AI] chatWithTools tool error", errorEntry);
|
|
333
|
+
if (onToolError && matchedCall) {
|
|
334
|
+
const action = onToolError(matchedCall, errorText);
|
|
335
|
+
if (action === "abort") {
|
|
336
|
+
abortedByCallback = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
287
340
|
conversation.push({
|
|
288
341
|
role: "tool",
|
|
289
|
-
content: tr
|
|
290
|
-
toolCallId: tr.toolCallId
|
|
342
|
+
content: [tr]
|
|
291
343
|
});
|
|
292
344
|
}
|
|
345
|
+
if (abortedByCallback) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (abortedByCallback) {
|
|
350
|
+
this.logger.warn("[AI] chatWithTools aborted by onToolError callback", { toolErrors });
|
|
351
|
+
} else {
|
|
352
|
+
this.logger.warn("[AI] chatWithTools max iterations reached, forcing final response", {
|
|
353
|
+
toolErrors: toolErrors.length > 0 ? toolErrors : void 0
|
|
354
|
+
});
|
|
293
355
|
}
|
|
294
|
-
this.logger.warn("[AI] chatWithTools max iterations reached, forcing final response");
|
|
295
356
|
const finalResult = await this.adapter.chat(conversation, {
|
|
296
357
|
...chatOptions,
|
|
297
358
|
tools: void 0,
|
|
@@ -299,14 +360,175 @@ var _AIService = class _AIService {
|
|
|
299
360
|
});
|
|
300
361
|
return finalResult;
|
|
301
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Stream chat with automatic tool call resolution.
|
|
365
|
+
*
|
|
366
|
+
* Works like {@link chatWithTools} but yields SSE events. When the model
|
|
367
|
+
* requests tool calls during streaming, they are executed and the results
|
|
368
|
+
* fed back until a final text stream is produced.
|
|
369
|
+
*/
|
|
370
|
+
async *streamChatWithTools(messages, options) {
|
|
371
|
+
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
372
|
+
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
373
|
+
const registeredTools = this.toolRegistry.getAll();
|
|
374
|
+
const mergedTools = [
|
|
375
|
+
...registeredTools,
|
|
376
|
+
...restOptions.tools ?? []
|
|
377
|
+
];
|
|
378
|
+
const chatOptions = {
|
|
379
|
+
...restOptions,
|
|
380
|
+
tools: mergedTools.length > 0 ? mergedTools : void 0,
|
|
381
|
+
toolChoice: mergedTools.length > 0 ? restOptions.toolChoice ?? "auto" : void 0
|
|
382
|
+
};
|
|
383
|
+
const conversation = [...messages];
|
|
384
|
+
let abortedByCallback = false;
|
|
385
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
386
|
+
const result2 = await this.adapter.chat(conversation, chatOptions);
|
|
387
|
+
if (!result2.toolCalls || result2.toolCalls.length === 0) {
|
|
388
|
+
yield textDeltaPart("stream", result2.content);
|
|
389
|
+
yield finishPart(result2);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
for (const tc of result2.toolCalls) {
|
|
393
|
+
yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
|
|
394
|
+
}
|
|
395
|
+
const assistantContent = [];
|
|
396
|
+
if (result2.content) assistantContent.push({ type: "text", text: result2.content });
|
|
397
|
+
assistantContent.push(...result2.toolCalls);
|
|
398
|
+
conversation.push({
|
|
399
|
+
role: "assistant",
|
|
400
|
+
content: assistantContent
|
|
401
|
+
});
|
|
402
|
+
const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
|
|
403
|
+
for (const tr of toolResults) {
|
|
404
|
+
if (tr.isError && onToolError) {
|
|
405
|
+
const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
406
|
+
if (matchedCall) {
|
|
407
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
408
|
+
const action = onToolError(matchedCall, errorText);
|
|
409
|
+
if (action === "abort") {
|
|
410
|
+
abortedByCallback = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
conversation.push({
|
|
415
|
+
role: "tool",
|
|
416
|
+
content: [tr]
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (abortedByCallback) {
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (abortedByCallback) {
|
|
424
|
+
this.logger.warn("[AI] streamChatWithTools aborted by onToolError callback");
|
|
425
|
+
} else {
|
|
426
|
+
this.logger.warn("[AI] streamChatWithTools max iterations reached");
|
|
427
|
+
}
|
|
428
|
+
const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
|
|
429
|
+
const result = await this.adapter.chat(conversation, finalOptions);
|
|
430
|
+
yield textDeltaPart("stream", result.content);
|
|
431
|
+
yield finishPart(result);
|
|
432
|
+
}
|
|
302
433
|
};
|
|
303
434
|
// ── Tool Call Loop ────────────────────────────────────────────
|
|
304
435
|
/** Default maximum iterations for the tool call loop. */
|
|
305
436
|
_AIService.DEFAULT_MAX_ITERATIONS = 10;
|
|
306
437
|
var AIService = _AIService;
|
|
307
438
|
|
|
439
|
+
// src/stream/vercel-stream-encoder.ts
|
|
440
|
+
function sse(data) {
|
|
441
|
+
return `data: ${JSON.stringify(data)}
|
|
442
|
+
|
|
443
|
+
`;
|
|
444
|
+
}
|
|
445
|
+
function encodeStreamPart(part) {
|
|
446
|
+
switch (part.type) {
|
|
447
|
+
case "text-delta":
|
|
448
|
+
return sse({ type: "text-delta", id: "0", delta: part.text });
|
|
449
|
+
case "tool-input-start":
|
|
450
|
+
return sse({
|
|
451
|
+
type: "tool-input-start",
|
|
452
|
+
toolCallId: part.id,
|
|
453
|
+
toolName: part.toolName
|
|
454
|
+
});
|
|
455
|
+
case "tool-input-delta":
|
|
456
|
+
return sse({
|
|
457
|
+
type: "tool-input-delta",
|
|
458
|
+
toolCallId: part.id,
|
|
459
|
+
inputTextDelta: part.delta
|
|
460
|
+
});
|
|
461
|
+
case "tool-call":
|
|
462
|
+
return sse({
|
|
463
|
+
type: "tool-input-available",
|
|
464
|
+
toolCallId: part.toolCallId,
|
|
465
|
+
toolName: part.toolName,
|
|
466
|
+
input: part.input
|
|
467
|
+
});
|
|
468
|
+
case "tool-result":
|
|
469
|
+
return sse({
|
|
470
|
+
type: "tool-output-available",
|
|
471
|
+
toolCallId: part.toolCallId,
|
|
472
|
+
output: part.output
|
|
473
|
+
});
|
|
474
|
+
case "error":
|
|
475
|
+
return sse({
|
|
476
|
+
type: "error",
|
|
477
|
+
errorText: String(part.error)
|
|
478
|
+
});
|
|
479
|
+
// finish-step and finish are handled by the generator, not here
|
|
480
|
+
default:
|
|
481
|
+
return "";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function* encodeVercelDataStream(events) {
|
|
485
|
+
yield sse({ type: "start" });
|
|
486
|
+
yield sse({ type: "start-step" });
|
|
487
|
+
yield sse({ type: "text-start", id: "0" });
|
|
488
|
+
let textOpen = true;
|
|
489
|
+
let finishReason = "stop";
|
|
490
|
+
for await (const part of events) {
|
|
491
|
+
if (part.type === "finish") {
|
|
492
|
+
finishReason = part.finishReason ?? "stop";
|
|
493
|
+
}
|
|
494
|
+
if (part.type === "finish-step" || part.type === "finish") {
|
|
495
|
+
if (textOpen) {
|
|
496
|
+
yield sse({ type: "text-end", id: "0" });
|
|
497
|
+
textOpen = false;
|
|
498
|
+
}
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const frame = encodeStreamPart(part);
|
|
502
|
+
if (frame) {
|
|
503
|
+
yield frame;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (textOpen) {
|
|
507
|
+
yield sse({ type: "text-end", id: "0" });
|
|
508
|
+
}
|
|
509
|
+
yield sse({ type: "finish-step" });
|
|
510
|
+
yield sse({ type: "finish", finishReason });
|
|
511
|
+
yield "data: [DONE]\n\n";
|
|
512
|
+
}
|
|
513
|
+
|
|
308
514
|
// src/routes/ai-routes.ts
|
|
309
515
|
var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
516
|
+
function normalizeMessage(raw) {
|
|
517
|
+
const role = raw.role;
|
|
518
|
+
if (typeof raw.content === "string") {
|
|
519
|
+
return { role, content: raw.content };
|
|
520
|
+
}
|
|
521
|
+
if (Array.isArray(raw.content)) {
|
|
522
|
+
return { role, content: raw.content };
|
|
523
|
+
}
|
|
524
|
+
if (Array.isArray(raw.parts)) {
|
|
525
|
+
const textParts = raw.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text);
|
|
526
|
+
if (textParts.length > 0) {
|
|
527
|
+
return { role, content: textParts.join("") };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return { role, content: "" };
|
|
531
|
+
}
|
|
310
532
|
function validateMessage(raw) {
|
|
311
533
|
if (typeof raw !== "object" || raw === null) {
|
|
312
534
|
return "each message must be an object";
|
|
@@ -315,20 +537,56 @@ function validateMessage(raw) {
|
|
|
315
537
|
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
316
538
|
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
317
539
|
}
|
|
318
|
-
|
|
319
|
-
|
|
540
|
+
const content = msg.content;
|
|
541
|
+
if (Array.isArray(msg.parts)) {
|
|
542
|
+
return null;
|
|
320
543
|
}
|
|
321
|
-
|
|
544
|
+
if (typeof content === "string") {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
if (Array.isArray(content)) {
|
|
548
|
+
for (const part of content) {
|
|
549
|
+
if (typeof part !== "object" || part === null) {
|
|
550
|
+
return "message.content array elements must be non-null objects";
|
|
551
|
+
}
|
|
552
|
+
const partObj = part;
|
|
553
|
+
if (typeof partObj.type !== "string") {
|
|
554
|
+
return 'each message.content array element must have a string "type" property';
|
|
555
|
+
}
|
|
556
|
+
if (partObj.type === "text" && typeof partObj.text !== "string") {
|
|
557
|
+
return 'message.content elements with type "text" must have a string "text" property';
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (content === null || content === void 0) {
|
|
563
|
+
if (msg.role === "assistant" || msg.role === "tool") {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return "message.content must be a string, an array, or include parts";
|
|
322
568
|
}
|
|
323
569
|
function buildAIRoutes(aiService, conversationService, logger) {
|
|
324
570
|
return [
|
|
325
571
|
// ── Chat ────────────────────────────────────────────────────
|
|
572
|
+
//
|
|
573
|
+
// Dual-mode endpoint compatible with both the legacy ObjectStack
|
|
574
|
+
// format (`{ messages, options }`) and the Vercel AI SDK useChat
|
|
575
|
+
// flat format (`{ messages, system, model, stream, … }`).
|
|
576
|
+
//
|
|
577
|
+
// Behaviour:
|
|
578
|
+
// • `stream !== false` → Vercel Data Stream Protocol (SSE)
|
|
579
|
+
// • `stream === false` → JSON response (legacy)
|
|
580
|
+
//
|
|
326
581
|
{
|
|
327
582
|
method: "POST",
|
|
328
583
|
path: "/api/v1/ai/chat",
|
|
329
|
-
description: "
|
|
584
|
+
description: "Chat completion (supports Vercel AI Data Stream Protocol)",
|
|
585
|
+
auth: true,
|
|
586
|
+
permissions: ["ai:chat"],
|
|
330
587
|
handler: async (req) => {
|
|
331
|
-
const
|
|
588
|
+
const body = req.body ?? {};
|
|
589
|
+
const messages = body.messages;
|
|
332
590
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
333
591
|
return { status: 400, body: { error: "messages array is required" } };
|
|
334
592
|
}
|
|
@@ -336,8 +594,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
336
594
|
const err = validateMessage(msg);
|
|
337
595
|
if (err) return { status: 400, body: { error: err } };
|
|
338
596
|
}
|
|
597
|
+
const nested = body.options ?? {};
|
|
598
|
+
const resolvedOptions = {
|
|
599
|
+
...nested,
|
|
600
|
+
...body.model != null && { model: body.model },
|
|
601
|
+
...body.temperature != null && { temperature: body.temperature },
|
|
602
|
+
...body.maxTokens != null && { maxTokens: body.maxTokens }
|
|
603
|
+
};
|
|
604
|
+
const rawSystemPrompt = body.system ?? body.systemPrompt;
|
|
605
|
+
if (rawSystemPrompt != null && typeof rawSystemPrompt !== "string") {
|
|
606
|
+
return { status: 400, body: { error: "system/systemPrompt must be a string" } };
|
|
607
|
+
}
|
|
608
|
+
const systemPrompt = rawSystemPrompt;
|
|
609
|
+
const finalMessages = [
|
|
610
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
611
|
+
...messages.map((m) => normalizeMessage(m))
|
|
612
|
+
];
|
|
613
|
+
const wantStream = body.stream !== false;
|
|
614
|
+
if (wantStream) {
|
|
615
|
+
try {
|
|
616
|
+
if (!aiService.streamChatWithTools) {
|
|
617
|
+
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
618
|
+
}
|
|
619
|
+
const events = aiService.streamChatWithTools(finalMessages, resolvedOptions);
|
|
620
|
+
return {
|
|
621
|
+
status: 200,
|
|
622
|
+
stream: true,
|
|
623
|
+
vercelDataStream: true,
|
|
624
|
+
contentType: "text/event-stream",
|
|
625
|
+
headers: {
|
|
626
|
+
"Content-Type": "text/event-stream",
|
|
627
|
+
"Cache-Control": "no-cache",
|
|
628
|
+
"Connection": "keep-alive",
|
|
629
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
630
|
+
},
|
|
631
|
+
events: encodeVercelDataStream(events)
|
|
632
|
+
};
|
|
633
|
+
} catch (err) {
|
|
634
|
+
logger.error("[AI Route] /chat stream error", err instanceof Error ? err : void 0);
|
|
635
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
339
638
|
try {
|
|
340
|
-
const result = await aiService.
|
|
639
|
+
const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
|
|
341
640
|
return { status: 200, body: result };
|
|
342
641
|
} catch (err) {
|
|
343
642
|
logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
|
|
@@ -350,6 +649,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
350
649
|
method: "POST",
|
|
351
650
|
path: "/api/v1/ai/chat/stream",
|
|
352
651
|
description: "SSE streaming chat completion",
|
|
652
|
+
auth: true,
|
|
653
|
+
permissions: ["ai:chat"],
|
|
353
654
|
handler: async (req) => {
|
|
354
655
|
const { messages, options } = req.body ?? {};
|
|
355
656
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
@@ -363,7 +664,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
363
664
|
if (!aiService.streamChat) {
|
|
364
665
|
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
365
666
|
}
|
|
366
|
-
const events = aiService.streamChat(messages, options);
|
|
667
|
+
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
367
668
|
return { status: 200, stream: true, events };
|
|
368
669
|
} catch (err) {
|
|
369
670
|
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
@@ -376,6 +677,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
376
677
|
method: "POST",
|
|
377
678
|
path: "/api/v1/ai/complete",
|
|
378
679
|
description: "Text completion",
|
|
680
|
+
auth: true,
|
|
681
|
+
permissions: ["ai:complete"],
|
|
379
682
|
handler: async (req) => {
|
|
380
683
|
const { prompt, options } = req.body ?? {};
|
|
381
684
|
if (!prompt || typeof prompt !== "string") {
|
|
@@ -395,6 +698,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
395
698
|
method: "GET",
|
|
396
699
|
path: "/api/v1/ai/models",
|
|
397
700
|
description: "List available models",
|
|
701
|
+
auth: true,
|
|
702
|
+
permissions: ["ai:read"],
|
|
398
703
|
handler: async () => {
|
|
399
704
|
try {
|
|
400
705
|
const models = aiService.listModels ? await aiService.listModels() : [];
|
|
@@ -410,9 +715,17 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
410
715
|
method: "POST",
|
|
411
716
|
path: "/api/v1/ai/conversations",
|
|
412
717
|
description: "Create a conversation",
|
|
718
|
+
auth: true,
|
|
719
|
+
permissions: ["ai:conversations"],
|
|
413
720
|
handler: async (req) => {
|
|
414
721
|
try {
|
|
415
|
-
|
|
722
|
+
if (req.body !== void 0 && req.body !== null && (typeof req.body !== "object" || Array.isArray(req.body))) {
|
|
723
|
+
return { status: 400, body: { error: "Invalid request payload" } };
|
|
724
|
+
}
|
|
725
|
+
const options = { ...req.body ?? {} };
|
|
726
|
+
if (req.user?.userId) {
|
|
727
|
+
options.userId = req.user.userId;
|
|
728
|
+
}
|
|
416
729
|
const conversation = await conversationService.create(options);
|
|
417
730
|
return { status: 201, body: conversation };
|
|
418
731
|
} catch (err) {
|
|
@@ -425,6 +738,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
425
738
|
method: "GET",
|
|
426
739
|
path: "/api/v1/ai/conversations",
|
|
427
740
|
description: "List conversations",
|
|
741
|
+
auth: true,
|
|
742
|
+
permissions: ["ai:conversations"],
|
|
428
743
|
handler: async (req) => {
|
|
429
744
|
try {
|
|
430
745
|
const rawQuery = req.query ?? {};
|
|
@@ -436,6 +751,9 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
436
751
|
}
|
|
437
752
|
options.limit = parsedLimit;
|
|
438
753
|
}
|
|
754
|
+
if (req.user?.userId) {
|
|
755
|
+
options.userId = req.user.userId;
|
|
756
|
+
}
|
|
439
757
|
const conversations = await conversationService.list(options);
|
|
440
758
|
return { status: 200, body: { conversations } };
|
|
441
759
|
} catch (err) {
|
|
@@ -448,6 +766,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
448
766
|
method: "POST",
|
|
449
767
|
path: "/api/v1/ai/conversations/:id/messages",
|
|
450
768
|
description: "Add message to a conversation",
|
|
769
|
+
auth: true,
|
|
770
|
+
permissions: ["ai:conversations"],
|
|
451
771
|
handler: async (req) => {
|
|
452
772
|
const id = req.params?.id;
|
|
453
773
|
if (!id) {
|
|
@@ -459,6 +779,15 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
459
779
|
return { status: 400, body: { error: validationError } };
|
|
460
780
|
}
|
|
461
781
|
try {
|
|
782
|
+
if (req.user?.userId) {
|
|
783
|
+
const existing = await conversationService.get(id);
|
|
784
|
+
if (!existing) {
|
|
785
|
+
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
786
|
+
}
|
|
787
|
+
if (existing.userId && existing.userId !== req.user.userId) {
|
|
788
|
+
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
462
791
|
const conversation = await conversationService.addMessage(id, message);
|
|
463
792
|
return { status: 200, body: conversation };
|
|
464
793
|
} catch (err) {
|
|
@@ -475,12 +804,23 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
475
804
|
method: "DELETE",
|
|
476
805
|
path: "/api/v1/ai/conversations/:id",
|
|
477
806
|
description: "Delete a conversation",
|
|
807
|
+
auth: true,
|
|
808
|
+
permissions: ["ai:conversations"],
|
|
478
809
|
handler: async (req) => {
|
|
479
810
|
const id = req.params?.id;
|
|
480
811
|
if (!id) {
|
|
481
812
|
return { status: 400, body: { error: "conversation id is required" } };
|
|
482
813
|
}
|
|
483
814
|
try {
|
|
815
|
+
if (req.user?.userId) {
|
|
816
|
+
const existing = await conversationService.get(id);
|
|
817
|
+
if (!existing) {
|
|
818
|
+
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
819
|
+
}
|
|
820
|
+
if (existing.userId && existing.userId !== req.user.userId) {
|
|
821
|
+
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
484
824
|
await conversationService.delete(id);
|
|
485
825
|
return { status: 204 };
|
|
486
826
|
} catch (err) {
|
|
@@ -509,10 +849,33 @@ function validateAgentMessage(raw) {
|
|
|
509
849
|
}
|
|
510
850
|
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
511
851
|
return [
|
|
852
|
+
// ── List active agents ──────────────────────────────────────
|
|
853
|
+
{
|
|
854
|
+
method: "GET",
|
|
855
|
+
path: "/api/v1/ai/agents",
|
|
856
|
+
description: "List all active AI agents",
|
|
857
|
+
auth: true,
|
|
858
|
+
permissions: ["ai:chat"],
|
|
859
|
+
handler: async () => {
|
|
860
|
+
try {
|
|
861
|
+
const agents = await agentRuntime.listAgents();
|
|
862
|
+
return { status: 200, body: { agents } };
|
|
863
|
+
} catch (err) {
|
|
864
|
+
logger.error(
|
|
865
|
+
"[AI Route] /agents list error",
|
|
866
|
+
err instanceof Error ? err : void 0
|
|
867
|
+
);
|
|
868
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
// ── Chat with a specific agent ──────────────────────────────
|
|
512
873
|
{
|
|
513
874
|
method: "POST",
|
|
514
875
|
path: "/api/v1/ai/agents/:agentName/chat",
|
|
515
876
|
description: "Chat with a specific AI agent",
|
|
877
|
+
auth: true,
|
|
878
|
+
permissions: ["ai:chat", "ai:agents"],
|
|
516
879
|
handler: async (req) => {
|
|
517
880
|
const agentName = req.params?.agentName;
|
|
518
881
|
if (!agentName) {
|
|
@@ -666,13 +1029,35 @@ var ObjectQLConversationService = class {
|
|
|
666
1029
|
}
|
|
667
1030
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
668
1031
|
const msgId = `msg_${randomUUID()}`;
|
|
1032
|
+
let contentStr;
|
|
1033
|
+
let toolCallsJson = null;
|
|
1034
|
+
let toolCallId = null;
|
|
1035
|
+
if (message.role === "system" || message.role === "user") {
|
|
1036
|
+
contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
1037
|
+
} else if (message.role === "assistant") {
|
|
1038
|
+
if (typeof message.content === "string") {
|
|
1039
|
+
contentStr = message.content;
|
|
1040
|
+
} else {
|
|
1041
|
+
const parts = message.content;
|
|
1042
|
+
const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
|
|
1043
|
+
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
|
1044
|
+
contentStr = textParts.join("");
|
|
1045
|
+
if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
|
|
1046
|
+
}
|
|
1047
|
+
} else if (message.role === "tool") {
|
|
1048
|
+
contentStr = JSON.stringify(message.content);
|
|
1049
|
+
const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
|
|
1050
|
+
if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
|
|
1051
|
+
} else {
|
|
1052
|
+
contentStr = "";
|
|
1053
|
+
}
|
|
669
1054
|
await this.engine.insert(MESSAGES_OBJECT, {
|
|
670
1055
|
id: msgId,
|
|
671
1056
|
conversation_id: conversationId,
|
|
672
1057
|
role: message.role,
|
|
673
|
-
content:
|
|
674
|
-
tool_calls:
|
|
675
|
-
tool_call_id:
|
|
1058
|
+
content: contentStr,
|
|
1059
|
+
tool_calls: toolCallsJson,
|
|
1060
|
+
tool_call_id: toolCallId,
|
|
676
1061
|
created_at: now
|
|
677
1062
|
});
|
|
678
1063
|
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
@@ -717,21 +1102,42 @@ var ObjectQLConversationService = class {
|
|
|
717
1102
|
};
|
|
718
1103
|
}
|
|
719
1104
|
/**
|
|
720
|
-
* Map a database row to
|
|
1105
|
+
* Map a database row to a ModelMessage.
|
|
721
1106
|
*/
|
|
722
1107
|
toMessage(row) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1108
|
+
switch (row.role) {
|
|
1109
|
+
case "system":
|
|
1110
|
+
return { role: "system", content: row.content };
|
|
1111
|
+
case "user":
|
|
1112
|
+
return { role: "user", content: row.content };
|
|
1113
|
+
case "assistant": {
|
|
1114
|
+
const toolCalls = this.safeParse(row.tool_calls);
|
|
1115
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
1116
|
+
const content = [];
|
|
1117
|
+
if (row.content) content.push({ type: "text", text: row.content });
|
|
1118
|
+
content.push(...toolCalls);
|
|
1119
|
+
return { role: "assistant", content };
|
|
1120
|
+
}
|
|
1121
|
+
return { role: "assistant", content: row.content };
|
|
1122
|
+
}
|
|
1123
|
+
case "tool": {
|
|
1124
|
+
const toolResults = this.safeParse(row.content);
|
|
1125
|
+
if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
|
|
1126
|
+
return { role: "tool", content: toolResults };
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
role: "tool",
|
|
1130
|
+
content: [{
|
|
1131
|
+
type: "tool-result",
|
|
1132
|
+
toolCallId: row.tool_call_id ?? "",
|
|
1133
|
+
toolName: "unknown",
|
|
1134
|
+
output: { type: "text", value: row.content }
|
|
1135
|
+
}]
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
default:
|
|
1139
|
+
return { role: "user", content: row.content };
|
|
733
1140
|
}
|
|
734
|
-
return msg;
|
|
735
1141
|
}
|
|
736
1142
|
};
|
|
737
1143
|
|
|
@@ -1128,6 +1534,479 @@ function registerDataTools(registry, context) {
|
|
|
1128
1534
|
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
1129
1535
|
}
|
|
1130
1536
|
|
|
1537
|
+
// src/tools/create-object.tool.ts
|
|
1538
|
+
import { defineTool } from "@objectstack/spec/ai";
|
|
1539
|
+
var createObjectTool = defineTool({
|
|
1540
|
+
name: "create_object",
|
|
1541
|
+
label: "Create Object",
|
|
1542
|
+
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.",
|
|
1543
|
+
category: "data",
|
|
1544
|
+
builtIn: true,
|
|
1545
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
1546
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
1547
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
1548
|
+
// should only be set once server-side approval gating is implemented to
|
|
1549
|
+
// avoid giving users a false sense of safety.
|
|
1550
|
+
parameters: {
|
|
1551
|
+
type: "object",
|
|
1552
|
+
properties: {
|
|
1553
|
+
name: {
|
|
1554
|
+
type: "string",
|
|
1555
|
+
description: "Machine name for the object (snake_case, e.g. project_task)"
|
|
1556
|
+
},
|
|
1557
|
+
label: {
|
|
1558
|
+
type: "string",
|
|
1559
|
+
description: "Human-readable display name (e.g. Project Task)"
|
|
1560
|
+
},
|
|
1561
|
+
fields: {
|
|
1562
|
+
type: "array",
|
|
1563
|
+
description: "Initial fields to create with the object",
|
|
1564
|
+
items: {
|
|
1565
|
+
type: "object",
|
|
1566
|
+
properties: {
|
|
1567
|
+
name: { type: "string", description: "Field machine name (snake_case)" },
|
|
1568
|
+
label: { type: "string", description: "Field display name" },
|
|
1569
|
+
type: {
|
|
1570
|
+
type: "string",
|
|
1571
|
+
description: "Field data type",
|
|
1572
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
1573
|
+
},
|
|
1574
|
+
required: { type: "boolean", description: "Whether the field is required" }
|
|
1575
|
+
},
|
|
1576
|
+
required: ["name", "type"]
|
|
1577
|
+
}
|
|
1578
|
+
},
|
|
1579
|
+
enableFeatures: {
|
|
1580
|
+
type: "object",
|
|
1581
|
+
description: "Object capability flags",
|
|
1582
|
+
properties: {
|
|
1583
|
+
trackHistory: { type: "boolean" },
|
|
1584
|
+
apiEnabled: { type: "boolean" }
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
required: ["name", "label"],
|
|
1589
|
+
additionalProperties: false
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
// src/tools/add-field.tool.ts
|
|
1594
|
+
import { defineTool as defineTool2 } from "@objectstack/spec/ai";
|
|
1595
|
+
var addFieldTool = defineTool2({
|
|
1596
|
+
name: "add_field",
|
|
1597
|
+
label: "Add Field",
|
|
1598
|
+
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.",
|
|
1599
|
+
category: "data",
|
|
1600
|
+
builtIn: true,
|
|
1601
|
+
parameters: {
|
|
1602
|
+
type: "object",
|
|
1603
|
+
properties: {
|
|
1604
|
+
objectName: {
|
|
1605
|
+
type: "string",
|
|
1606
|
+
description: "Target object machine name (snake_case)"
|
|
1607
|
+
},
|
|
1608
|
+
name: {
|
|
1609
|
+
type: "string",
|
|
1610
|
+
description: "Field machine name (snake_case, e.g. due_date)"
|
|
1611
|
+
},
|
|
1612
|
+
label: {
|
|
1613
|
+
type: "string",
|
|
1614
|
+
description: "Human-readable field label (e.g. Due Date)"
|
|
1615
|
+
},
|
|
1616
|
+
type: {
|
|
1617
|
+
type: "string",
|
|
1618
|
+
description: "Field data type",
|
|
1619
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
1620
|
+
},
|
|
1621
|
+
required: {
|
|
1622
|
+
type: "boolean",
|
|
1623
|
+
description: "Whether the field is required"
|
|
1624
|
+
},
|
|
1625
|
+
defaultValue: {
|
|
1626
|
+
description: "Default value for the field"
|
|
1627
|
+
},
|
|
1628
|
+
options: {
|
|
1629
|
+
type: "array",
|
|
1630
|
+
description: "Options for select/picklist fields",
|
|
1631
|
+
items: {
|
|
1632
|
+
type: "object",
|
|
1633
|
+
properties: {
|
|
1634
|
+
label: { type: "string" },
|
|
1635
|
+
value: {
|
|
1636
|
+
type: "string",
|
|
1637
|
+
description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
|
|
1638
|
+
pattern: "^[a-z_][a-z0-9_]*$"
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
},
|
|
1643
|
+
reference: {
|
|
1644
|
+
type: "string",
|
|
1645
|
+
description: "Referenced object name for lookup fields (snake_case, e.g. account)"
|
|
1646
|
+
}
|
|
1647
|
+
},
|
|
1648
|
+
required: ["objectName", "name", "type"],
|
|
1649
|
+
additionalProperties: false
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
// src/tools/modify-field.tool.ts
|
|
1654
|
+
import { defineTool as defineTool3 } from "@objectstack/spec/ai";
|
|
1655
|
+
var modifyFieldTool = defineTool3({
|
|
1656
|
+
name: "modify_field",
|
|
1657
|
+
label: "Modify Field",
|
|
1658
|
+
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).",
|
|
1659
|
+
category: "data",
|
|
1660
|
+
builtIn: true,
|
|
1661
|
+
parameters: {
|
|
1662
|
+
type: "object",
|
|
1663
|
+
properties: {
|
|
1664
|
+
objectName: {
|
|
1665
|
+
type: "string",
|
|
1666
|
+
description: "Target object machine name (snake_case)"
|
|
1667
|
+
},
|
|
1668
|
+
fieldName: {
|
|
1669
|
+
type: "string",
|
|
1670
|
+
description: "Existing field machine name to modify (snake_case)"
|
|
1671
|
+
},
|
|
1672
|
+
changes: {
|
|
1673
|
+
type: "object",
|
|
1674
|
+
description: "Field properties to update (partial patch)",
|
|
1675
|
+
properties: {
|
|
1676
|
+
label: { type: "string", description: "New display label" },
|
|
1677
|
+
type: { type: "string", description: "New field type" },
|
|
1678
|
+
required: { type: "boolean", description: "Update required constraint" },
|
|
1679
|
+
defaultValue: { description: "New default value" }
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
required: ["objectName", "fieldName", "changes"],
|
|
1684
|
+
additionalProperties: false
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
// src/tools/delete-field.tool.ts
|
|
1689
|
+
import { defineTool as defineTool4 } from "@objectstack/spec/ai";
|
|
1690
|
+
var deleteFieldTool = defineTool4({
|
|
1691
|
+
name: "delete_field",
|
|
1692
|
+
label: "Delete Field",
|
|
1693
|
+
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.",
|
|
1694
|
+
category: "data",
|
|
1695
|
+
builtIn: true,
|
|
1696
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
1697
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
1698
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
1699
|
+
// should only be set once server-side approval gating is implemented.
|
|
1700
|
+
parameters: {
|
|
1701
|
+
type: "object",
|
|
1702
|
+
properties: {
|
|
1703
|
+
objectName: {
|
|
1704
|
+
type: "string",
|
|
1705
|
+
description: "Target object machine name (snake_case)"
|
|
1706
|
+
},
|
|
1707
|
+
fieldName: {
|
|
1708
|
+
type: "string",
|
|
1709
|
+
description: "Field machine name to delete (snake_case)"
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
required: ["objectName", "fieldName"],
|
|
1713
|
+
additionalProperties: false
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// src/tools/list-metadata-objects.tool.ts
|
|
1718
|
+
import { defineTool as defineTool5 } from "@objectstack/spec/ai";
|
|
1719
|
+
var listMetadataObjectsTool = defineTool5({
|
|
1720
|
+
name: "list_metadata_objects",
|
|
1721
|
+
label: "List Metadata Objects",
|
|
1722
|
+
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.",
|
|
1723
|
+
category: "data",
|
|
1724
|
+
builtIn: true,
|
|
1725
|
+
parameters: {
|
|
1726
|
+
type: "object",
|
|
1727
|
+
properties: {
|
|
1728
|
+
filter: {
|
|
1729
|
+
type: "string",
|
|
1730
|
+
description: "Optional name or label substring to filter objects"
|
|
1731
|
+
},
|
|
1732
|
+
includeFields: {
|
|
1733
|
+
type: "boolean",
|
|
1734
|
+
description: "Whether to include field summaries for each object (default: false)"
|
|
1735
|
+
}
|
|
1736
|
+
},
|
|
1737
|
+
additionalProperties: false
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
// src/tools/describe-metadata-object.tool.ts
|
|
1742
|
+
import { defineTool as defineTool6 } from "@objectstack/spec/ai";
|
|
1743
|
+
var describeMetadataObjectTool = defineTool6({
|
|
1744
|
+
name: "describe_metadata_object",
|
|
1745
|
+
label: "Describe Metadata Object",
|
|
1746
|
+
description: "Returns the full metadata schema details of a data object, including all fields, types, relationships, and configuration. Use this when the user wants to inspect or understand the metadata structure of a specific table or entity.",
|
|
1747
|
+
category: "data",
|
|
1748
|
+
builtIn: true,
|
|
1749
|
+
parameters: {
|
|
1750
|
+
type: "object",
|
|
1751
|
+
properties: {
|
|
1752
|
+
objectName: {
|
|
1753
|
+
type: "string",
|
|
1754
|
+
description: "Object machine name to describe (snake_case)"
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
required: ["objectName"],
|
|
1758
|
+
additionalProperties: false
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
// src/tools/metadata-tools.ts
|
|
1763
|
+
var METADATA_TOOL_DEFINITIONS = [
|
|
1764
|
+
createObjectTool,
|
|
1765
|
+
addFieldTool,
|
|
1766
|
+
modifyFieldTool,
|
|
1767
|
+
deleteFieldTool,
|
|
1768
|
+
listMetadataObjectsTool,
|
|
1769
|
+
describeMetadataObjectTool
|
|
1770
|
+
];
|
|
1771
|
+
var SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
|
|
1772
|
+
function isSnakeCase(value) {
|
|
1773
|
+
return SNAKE_CASE_RE.test(value);
|
|
1774
|
+
}
|
|
1775
|
+
function createCreateObjectHandler(ctx) {
|
|
1776
|
+
return async (args) => {
|
|
1777
|
+
const { name, label, fields, enableFeatures } = args;
|
|
1778
|
+
if (!name || !label) {
|
|
1779
|
+
return JSON.stringify({ error: 'Both "name" and "label" are required' });
|
|
1780
|
+
}
|
|
1781
|
+
if (!isSnakeCase(name)) {
|
|
1782
|
+
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
|
|
1783
|
+
}
|
|
1784
|
+
const existing = await ctx.metadataService.getObject(name);
|
|
1785
|
+
if (existing) {
|
|
1786
|
+
return JSON.stringify({ error: `Object "${name}" already exists` });
|
|
1787
|
+
}
|
|
1788
|
+
const fieldMap = {};
|
|
1789
|
+
if (fields && Array.isArray(fields)) {
|
|
1790
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1791
|
+
for (const f of fields) {
|
|
1792
|
+
if (!f.name) {
|
|
1793
|
+
return JSON.stringify({ error: 'Each field must have a "name" property' });
|
|
1794
|
+
}
|
|
1795
|
+
if (!isSnakeCase(f.name)) {
|
|
1796
|
+
return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
|
|
1797
|
+
}
|
|
1798
|
+
if (seenNames.has(f.name)) {
|
|
1799
|
+
return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
|
|
1800
|
+
}
|
|
1801
|
+
seenNames.add(f.name);
|
|
1802
|
+
fieldMap[f.name] = {
|
|
1803
|
+
type: f.type,
|
|
1804
|
+
...f.label ? { label: f.label } : {},
|
|
1805
|
+
...f.required !== void 0 ? { required: f.required } : {}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
const objectDef = {
|
|
1810
|
+
name,
|
|
1811
|
+
label,
|
|
1812
|
+
...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
|
|
1813
|
+
...enableFeatures ? { enable: enableFeatures } : {}
|
|
1814
|
+
};
|
|
1815
|
+
await ctx.metadataService.register("object", name, objectDef);
|
|
1816
|
+
return JSON.stringify({
|
|
1817
|
+
name,
|
|
1818
|
+
label,
|
|
1819
|
+
fieldCount: Object.keys(fieldMap).length
|
|
1820
|
+
});
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
function createAddFieldHandler(ctx) {
|
|
1824
|
+
return async (args) => {
|
|
1825
|
+
const { objectName, name, label, type, required, defaultValue, options, reference } = args;
|
|
1826
|
+
if (!objectName || !name || !type) {
|
|
1827
|
+
return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
|
|
1828
|
+
}
|
|
1829
|
+
if (!isSnakeCase(objectName)) {
|
|
1830
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1831
|
+
}
|
|
1832
|
+
if (!isSnakeCase(name)) {
|
|
1833
|
+
return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
|
|
1834
|
+
}
|
|
1835
|
+
if (reference && !isSnakeCase(reference)) {
|
|
1836
|
+
return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
|
|
1837
|
+
}
|
|
1838
|
+
if (options && Array.isArray(options)) {
|
|
1839
|
+
for (const opt of options) {
|
|
1840
|
+
if (opt.value && !isSnakeCase(opt.value)) {
|
|
1841
|
+
return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1846
|
+
if (!objectDef) {
|
|
1847
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1848
|
+
}
|
|
1849
|
+
const def = objectDef;
|
|
1850
|
+
if (def.fields && def.fields[name]) {
|
|
1851
|
+
return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
|
|
1852
|
+
}
|
|
1853
|
+
const fieldDef = {
|
|
1854
|
+
type,
|
|
1855
|
+
...label ? { label } : {},
|
|
1856
|
+
...required !== void 0 ? { required } : {},
|
|
1857
|
+
...defaultValue !== void 0 ? { defaultValue } : {},
|
|
1858
|
+
...options ? { options } : {},
|
|
1859
|
+
...reference ? { reference } : {}
|
|
1860
|
+
};
|
|
1861
|
+
const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
|
|
1862
|
+
await ctx.metadataService.register("object", objectName, {
|
|
1863
|
+
...def,
|
|
1864
|
+
fields: updatedFields
|
|
1865
|
+
});
|
|
1866
|
+
return JSON.stringify({
|
|
1867
|
+
objectName,
|
|
1868
|
+
fieldName: name,
|
|
1869
|
+
fieldType: type
|
|
1870
|
+
});
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
function createModifyFieldHandler(ctx) {
|
|
1874
|
+
return async (args) => {
|
|
1875
|
+
const { objectName, fieldName, changes } = args;
|
|
1876
|
+
if (!objectName || !fieldName || !changes) {
|
|
1877
|
+
return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
|
|
1878
|
+
}
|
|
1879
|
+
if (!isSnakeCase(objectName)) {
|
|
1880
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1881
|
+
}
|
|
1882
|
+
if (!isSnakeCase(fieldName)) {
|
|
1883
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
1884
|
+
}
|
|
1885
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1886
|
+
if (!objectDef) {
|
|
1887
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1888
|
+
}
|
|
1889
|
+
const def = objectDef;
|
|
1890
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
1891
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
1892
|
+
}
|
|
1893
|
+
const existingField = def.fields[fieldName];
|
|
1894
|
+
const updatedField = { ...existingField, ...changes };
|
|
1895
|
+
const updatedFields = { ...def.fields, [fieldName]: updatedField };
|
|
1896
|
+
await ctx.metadataService.register("object", objectName, {
|
|
1897
|
+
...def,
|
|
1898
|
+
fields: updatedFields
|
|
1899
|
+
});
|
|
1900
|
+
return JSON.stringify({
|
|
1901
|
+
objectName,
|
|
1902
|
+
fieldName,
|
|
1903
|
+
updatedProperties: Object.keys(changes)
|
|
1904
|
+
});
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
function createDeleteFieldHandler(ctx) {
|
|
1908
|
+
return async (args) => {
|
|
1909
|
+
const { objectName, fieldName } = args;
|
|
1910
|
+
if (!objectName || !fieldName) {
|
|
1911
|
+
return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
|
|
1912
|
+
}
|
|
1913
|
+
if (!isSnakeCase(objectName)) {
|
|
1914
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1915
|
+
}
|
|
1916
|
+
if (!isSnakeCase(fieldName)) {
|
|
1917
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
1918
|
+
}
|
|
1919
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1920
|
+
if (!objectDef) {
|
|
1921
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1922
|
+
}
|
|
1923
|
+
const def = objectDef;
|
|
1924
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
1925
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
1926
|
+
}
|
|
1927
|
+
const { [fieldName]: _removed, ...remainingFields } = def.fields;
|
|
1928
|
+
await ctx.metadataService.register("object", objectName, {
|
|
1929
|
+
...def,
|
|
1930
|
+
fields: remainingFields
|
|
1931
|
+
});
|
|
1932
|
+
return JSON.stringify({
|
|
1933
|
+
objectName,
|
|
1934
|
+
fieldName,
|
|
1935
|
+
success: true
|
|
1936
|
+
});
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
function createListObjectsHandler2(ctx) {
|
|
1940
|
+
return async (args) => {
|
|
1941
|
+
const { filter, includeFields } = args ?? {};
|
|
1942
|
+
const objects = await ctx.metadataService.listObjects();
|
|
1943
|
+
let result = objects.map((o) => {
|
|
1944
|
+
const base = {
|
|
1945
|
+
name: o.name,
|
|
1946
|
+
label: o.label ?? o.name,
|
|
1947
|
+
fieldCount: o.fields ? Object.keys(o.fields).length : 0
|
|
1948
|
+
};
|
|
1949
|
+
if (includeFields && o.fields) {
|
|
1950
|
+
base.fields = Object.entries(o.fields).map(([key, f]) => ({
|
|
1951
|
+
name: key,
|
|
1952
|
+
type: f.type,
|
|
1953
|
+
label: f.label ?? key
|
|
1954
|
+
}));
|
|
1955
|
+
}
|
|
1956
|
+
return base;
|
|
1957
|
+
});
|
|
1958
|
+
if (filter) {
|
|
1959
|
+
const lower = filter.toLowerCase();
|
|
1960
|
+
result = result.filter(
|
|
1961
|
+
(o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
return JSON.stringify({
|
|
1965
|
+
objects: result,
|
|
1966
|
+
totalCount: result.length
|
|
1967
|
+
});
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
function createDescribeObjectHandler2(ctx) {
|
|
1971
|
+
return async (args) => {
|
|
1972
|
+
const { objectName } = args;
|
|
1973
|
+
if (!objectName) {
|
|
1974
|
+
return JSON.stringify({ error: '"objectName" is required' });
|
|
1975
|
+
}
|
|
1976
|
+
if (!isSnakeCase(objectName)) {
|
|
1977
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1978
|
+
}
|
|
1979
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1980
|
+
if (!objectDef) {
|
|
1981
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1982
|
+
}
|
|
1983
|
+
const def = objectDef;
|
|
1984
|
+
const fields = def.fields ?? {};
|
|
1985
|
+
const fieldSummary = Object.entries(fields).map(([key, f]) => ({
|
|
1986
|
+
name: key,
|
|
1987
|
+
type: f.type,
|
|
1988
|
+
label: f.label ?? key,
|
|
1989
|
+
required: f.required ?? false,
|
|
1990
|
+
...f.reference ? { reference: f.reference } : {},
|
|
1991
|
+
...f.options ? { options: f.options } : {}
|
|
1992
|
+
}));
|
|
1993
|
+
return JSON.stringify({
|
|
1994
|
+
name: def.name,
|
|
1995
|
+
label: def.label ?? def.name,
|
|
1996
|
+
fields: fieldSummary,
|
|
1997
|
+
enableFeatures: def.enable ?? {}
|
|
1998
|
+
});
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function registerMetadataTools(registry, context) {
|
|
2002
|
+
registry.register(createObjectTool, createCreateObjectHandler(context));
|
|
2003
|
+
registry.register(addFieldTool, createAddFieldHandler(context));
|
|
2004
|
+
registry.register(modifyFieldTool, createModifyFieldHandler(context));
|
|
2005
|
+
registry.register(deleteFieldTool, createDeleteFieldHandler(context));
|
|
2006
|
+
registry.register(listMetadataObjectsTool, createListObjectsHandler2(context));
|
|
2007
|
+
registry.register(describeMetadataObjectTool, createDescribeObjectHandler2(context));
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1131
2010
|
// src/agent-runtime.ts
|
|
1132
2011
|
import { AgentSchema } from "@objectstack/spec/ai";
|
|
1133
2012
|
var AgentRuntime = class {
|
|
@@ -1135,6 +2014,27 @@ var AgentRuntime = class {
|
|
|
1135
2014
|
this.metadataService = metadataService;
|
|
1136
2015
|
}
|
|
1137
2016
|
// ── Public API ────────────────────────────────────────────────
|
|
2017
|
+
/**
|
|
2018
|
+
* List all active agents registered in the metadata service.
|
|
2019
|
+
*
|
|
2020
|
+
* Returns a summary for each agent (name, label, role) suitable
|
|
2021
|
+
* for populating an agent selector dropdown in the UI.
|
|
2022
|
+
*/
|
|
2023
|
+
async listAgents() {
|
|
2024
|
+
const rawItems = await this.metadataService.list("agent");
|
|
2025
|
+
const agents = [];
|
|
2026
|
+
for (const raw of rawItems) {
|
|
2027
|
+
const result = AgentSchema.safeParse(raw);
|
|
2028
|
+
if (result.success && result.data.active) {
|
|
2029
|
+
agents.push({
|
|
2030
|
+
name: result.data.name,
|
|
2031
|
+
label: result.data.label,
|
|
2032
|
+
role: result.data.role
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
return agents;
|
|
2037
|
+
}
|
|
1138
2038
|
/**
|
|
1139
2039
|
* Load and validate an agent definition by name.
|
|
1140
2040
|
*
|
|
@@ -1263,15 +2163,233 @@ Guidelines:
|
|
|
1263
2163
|
}
|
|
1264
2164
|
};
|
|
1265
2165
|
|
|
2166
|
+
// src/agents/metadata-assistant-agent.ts
|
|
2167
|
+
var METADATA_ASSISTANT_AGENT = {
|
|
2168
|
+
name: "metadata_assistant",
|
|
2169
|
+
label: "Metadata Assistant",
|
|
2170
|
+
role: "Schema Architect",
|
|
2171
|
+
instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
|
|
2172
|
+
|
|
2173
|
+
Capabilities:
|
|
2174
|
+
- Create new data objects (tables) with fields
|
|
2175
|
+
- Add fields (columns) to existing objects
|
|
2176
|
+
- Modify field properties (label, type, required, default value)
|
|
2177
|
+
- Delete fields from objects
|
|
2178
|
+
- List all registered metadata objects and their schemas
|
|
2179
|
+
- Describe the full schema of a specific object
|
|
2180
|
+
|
|
2181
|
+
Guidelines:
|
|
2182
|
+
1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
|
|
2183
|
+
2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
|
|
2184
|
+
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
|
|
2185
|
+
4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
|
|
2186
|
+
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
|
|
2187
|
+
6. Explain what changes you are about to make before executing them.
|
|
2188
|
+
7. After making changes, confirm the result by describing the updated schema.
|
|
2189
|
+
8. For destructive operations (deleting fields), always warn the user about potential data loss.
|
|
2190
|
+
9. Always answer in the same language the user is using.
|
|
2191
|
+
10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
|
|
2192
|
+
model: {
|
|
2193
|
+
provider: "openai",
|
|
2194
|
+
model: "gpt-4",
|
|
2195
|
+
temperature: 0.2,
|
|
2196
|
+
maxTokens: 4096
|
|
2197
|
+
},
|
|
2198
|
+
tools: [
|
|
2199
|
+
{ type: "action", name: "create_object", description: "Create a new data object (table)" },
|
|
2200
|
+
{ type: "action", name: "add_field", description: "Add a field to an existing object" },
|
|
2201
|
+
{ type: "action", name: "modify_field", description: "Modify an existing field definition" },
|
|
2202
|
+
{ type: "action", name: "delete_field", description: "Delete a field from an object" },
|
|
2203
|
+
{ type: "query", name: "list_metadata_objects", description: "List all metadata objects" },
|
|
2204
|
+
{ type: "query", name: "describe_metadata_object", description: "Describe an object schema" }
|
|
2205
|
+
],
|
|
2206
|
+
active: true,
|
|
2207
|
+
visibility: "global",
|
|
2208
|
+
guardrails: {
|
|
2209
|
+
maxTokensPerInvocation: 8192,
|
|
2210
|
+
maxExecutionTimeSec: 60,
|
|
2211
|
+
blockedTopics: ["drop_database", "raw_sql", "system_tables"]
|
|
2212
|
+
},
|
|
2213
|
+
planning: {
|
|
2214
|
+
strategy: "react",
|
|
2215
|
+
maxIterations: 10,
|
|
2216
|
+
allowReplan: true
|
|
2217
|
+
},
|
|
2218
|
+
memory: {
|
|
2219
|
+
shortTerm: {
|
|
2220
|
+
maxMessages: 30,
|
|
2221
|
+
maxTokens: 8192
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
// src/adapters/vercel-adapter.ts
|
|
2227
|
+
import { generateText, streamText, tool as vercelTool, jsonSchema } from "ai";
|
|
2228
|
+
function buildVercelOptions(options) {
|
|
2229
|
+
if (!options) return {};
|
|
2230
|
+
const opts = {};
|
|
2231
|
+
if (options.temperature != null) opts.temperature = options.temperature;
|
|
2232
|
+
if (options.maxTokens != null) opts.maxTokens = options.maxTokens;
|
|
2233
|
+
if (options.stop?.length) opts.stopSequences = options.stop;
|
|
2234
|
+
if (options.tools?.length) {
|
|
2235
|
+
const tools = {};
|
|
2236
|
+
for (const t of options.tools) {
|
|
2237
|
+
tools[t.name] = vercelTool({
|
|
2238
|
+
description: t.description,
|
|
2239
|
+
inputSchema: jsonSchema(t.parameters)
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
opts.tools = tools;
|
|
2243
|
+
}
|
|
2244
|
+
if (options.toolChoice != null) {
|
|
2245
|
+
opts.toolChoice = options.toolChoice;
|
|
2246
|
+
}
|
|
2247
|
+
return opts;
|
|
2248
|
+
}
|
|
2249
|
+
var VercelLLMAdapter = class {
|
|
2250
|
+
constructor(config) {
|
|
2251
|
+
this.name = "vercel";
|
|
2252
|
+
this.model = config.model;
|
|
2253
|
+
}
|
|
2254
|
+
async chat(messages, options) {
|
|
2255
|
+
const result = await generateText({
|
|
2256
|
+
model: this.model,
|
|
2257
|
+
messages,
|
|
2258
|
+
...buildVercelOptions(options)
|
|
2259
|
+
});
|
|
2260
|
+
return {
|
|
2261
|
+
content: result.text,
|
|
2262
|
+
model: result.response?.modelId,
|
|
2263
|
+
toolCalls: result.toolCalls?.length ? result.toolCalls : void 0,
|
|
2264
|
+
usage: result.usage ? {
|
|
2265
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2266
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2267
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2268
|
+
} : void 0
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
async complete(prompt, options) {
|
|
2272
|
+
const result = await generateText({
|
|
2273
|
+
model: this.model,
|
|
2274
|
+
prompt,
|
|
2275
|
+
...buildVercelOptions(options)
|
|
2276
|
+
});
|
|
2277
|
+
return {
|
|
2278
|
+
content: result.text,
|
|
2279
|
+
model: result.response?.modelId,
|
|
2280
|
+
usage: result.usage ? {
|
|
2281
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2282
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2283
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2284
|
+
} : void 0
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
async *streamChat(messages, options) {
|
|
2288
|
+
const result = streamText({
|
|
2289
|
+
model: this.model,
|
|
2290
|
+
messages,
|
|
2291
|
+
...buildVercelOptions(options)
|
|
2292
|
+
});
|
|
2293
|
+
for await (const part of result.fullStream) {
|
|
2294
|
+
yield part;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
async embed(_input) {
|
|
2298
|
+
throw new Error(
|
|
2299
|
+
"[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
async listModels() {
|
|
2303
|
+
return [];
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
|
|
1266
2307
|
// src/plugin.ts
|
|
1267
2308
|
var AIServicePlugin = class {
|
|
1268
2309
|
constructor(options = {}) {
|
|
1269
2310
|
this.name = "com.objectstack.service-ai";
|
|
1270
2311
|
this.version = "1.0.0";
|
|
1271
2312
|
this.type = "standard";
|
|
1272
|
-
this.dependencies = [];
|
|
2313
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
1273
2314
|
this.options = options;
|
|
1274
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Auto-detect LLM provider from environment variables.
|
|
2318
|
+
*
|
|
2319
|
+
* Priority order:
|
|
2320
|
+
* 1. AI_GATEWAY_MODEL → Vercel AI Gateway
|
|
2321
|
+
* 2. OPENAI_API_KEY → OpenAI
|
|
2322
|
+
* 3. ANTHROPIC_API_KEY → Anthropic
|
|
2323
|
+
* 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
|
|
2324
|
+
* 5. Fallback → MemoryLLMAdapter
|
|
2325
|
+
*
|
|
2326
|
+
* Returns the adapter and a description for logging.
|
|
2327
|
+
*/
|
|
2328
|
+
async detectAdapter(ctx) {
|
|
2329
|
+
const gatewayModel = process.env.AI_GATEWAY_MODEL;
|
|
2330
|
+
if (gatewayModel) {
|
|
2331
|
+
try {
|
|
2332
|
+
const gatewayPkg = "@ai-sdk/gateway";
|
|
2333
|
+
const { gateway } = await import(
|
|
2334
|
+
/* webpackIgnore: true */
|
|
2335
|
+
gatewayPkg
|
|
2336
|
+
);
|
|
2337
|
+
const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
|
|
2338
|
+
return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
ctx.logger.warn(
|
|
2341
|
+
`[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
|
|
2342
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
const providerConfigs = [
|
|
2347
|
+
{
|
|
2348
|
+
envKey: "OPENAI_API_KEY",
|
|
2349
|
+
pkg: "@ai-sdk/openai",
|
|
2350
|
+
factory: "openai",
|
|
2351
|
+
defaultModel: "gpt-4o",
|
|
2352
|
+
displayName: "OpenAI"
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
2356
|
+
pkg: "@ai-sdk/anthropic",
|
|
2357
|
+
factory: "anthropic",
|
|
2358
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
2359
|
+
displayName: "Anthropic"
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
2363
|
+
pkg: "@ai-sdk/google",
|
|
2364
|
+
factory: "google",
|
|
2365
|
+
defaultModel: "gemini-2.0-flash",
|
|
2366
|
+
displayName: "Google"
|
|
2367
|
+
}
|
|
2368
|
+
];
|
|
2369
|
+
for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
|
|
2370
|
+
if (process.env[envKey]) {
|
|
2371
|
+
try {
|
|
2372
|
+
const mod = await import(
|
|
2373
|
+
/* webpackIgnore: true */
|
|
2374
|
+
pkg
|
|
2375
|
+
);
|
|
2376
|
+
const createModel = mod[factory] ?? mod.default;
|
|
2377
|
+
if (typeof createModel === "function") {
|
|
2378
|
+
const modelId = process.env.AI_MODEL ?? defaultModel;
|
|
2379
|
+
const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
|
|
2380
|
+
return { adapter, description: `${displayName} (model: ${modelId})` };
|
|
2381
|
+
}
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
ctx.logger.warn(
|
|
2384
|
+
`[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
|
|
2385
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
ctx.logger.warn("[AI] No LLM provider configured via environment variables. Falling back to MemoryLLMAdapter (echo mode). Set AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY to use a real LLM.");
|
|
2391
|
+
return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode - for testing only)" };
|
|
2392
|
+
}
|
|
1275
2393
|
async init(ctx) {
|
|
1276
2394
|
let hasExisting = false;
|
|
1277
2395
|
try {
|
|
@@ -1293,8 +2411,19 @@ var AIServicePlugin = class {
|
|
|
1293
2411
|
} catch {
|
|
1294
2412
|
}
|
|
1295
2413
|
}
|
|
2414
|
+
let adapter;
|
|
2415
|
+
let adapterDescription;
|
|
2416
|
+
if (this.options.adapter) {
|
|
2417
|
+
adapter = this.options.adapter;
|
|
2418
|
+
adapterDescription = `${adapter.name} (explicitly configured)`;
|
|
2419
|
+
} else {
|
|
2420
|
+
const detected = await this.detectAdapter(ctx);
|
|
2421
|
+
adapter = detected.adapter;
|
|
2422
|
+
adapterDescription = detected.description;
|
|
2423
|
+
}
|
|
2424
|
+
ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
|
|
1296
2425
|
const config = {
|
|
1297
|
-
adapter
|
|
2426
|
+
adapter,
|
|
1298
2427
|
logger: ctx.logger,
|
|
1299
2428
|
conversationService
|
|
1300
2429
|
};
|
|
@@ -1304,7 +2433,7 @@ var AIServicePlugin = class {
|
|
|
1304
2433
|
} else {
|
|
1305
2434
|
ctx.registerService("ai", this.service);
|
|
1306
2435
|
}
|
|
1307
|
-
ctx.
|
|
2436
|
+
ctx.getService("manifest").register({
|
|
1308
2437
|
id: "com.objectstack.service-ai",
|
|
1309
2438
|
name: "AI Service",
|
|
1310
2439
|
version: "1.0.0",
|
|
@@ -1317,13 +2446,32 @@ var AIServicePlugin = class {
|
|
|
1317
2446
|
ctx.logger.debug("[AI] Before chat", { messages });
|
|
1318
2447
|
});
|
|
1319
2448
|
}
|
|
2449
|
+
try {
|
|
2450
|
+
const setupNav = ctx.getService("setupNav");
|
|
2451
|
+
if (setupNav) {
|
|
2452
|
+
setupNav.contribute({
|
|
2453
|
+
areaId: "area_ai",
|
|
2454
|
+
items: [
|
|
2455
|
+
{ id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
|
|
2456
|
+
{ id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
|
|
2457
|
+
]
|
|
2458
|
+
});
|
|
2459
|
+
ctx.logger.info("[AI] Navigation items contributed to Setup App");
|
|
2460
|
+
}
|
|
2461
|
+
} catch {
|
|
2462
|
+
}
|
|
1320
2463
|
ctx.logger.info("[AI] Service initialized");
|
|
1321
2464
|
}
|
|
1322
2465
|
async start(ctx) {
|
|
1323
2466
|
if (!this.service) return;
|
|
2467
|
+
let metadataService;
|
|
2468
|
+
try {
|
|
2469
|
+
metadataService = ctx.getService("metadata");
|
|
2470
|
+
} catch {
|
|
2471
|
+
ctx.logger.debug("[AI] Metadata service not available");
|
|
2472
|
+
}
|
|
1324
2473
|
try {
|
|
1325
2474
|
const dataEngine = ctx.getService("data");
|
|
1326
|
-
const metadataService = ctx.getService("metadata");
|
|
1327
2475
|
if (dataEngine && metadataService) {
|
|
1328
2476
|
registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
|
|
1329
2477
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
@@ -1336,14 +2484,29 @@ var AIServicePlugin = class {
|
|
|
1336
2484
|
}
|
|
1337
2485
|
}
|
|
1338
2486
|
} catch {
|
|
1339
|
-
ctx.logger.debug("[AI] Data engine
|
|
2487
|
+
ctx.logger.debug("[AI] Data engine not available, skipping data tools");
|
|
2488
|
+
}
|
|
2489
|
+
if (metadataService) {
|
|
2490
|
+
try {
|
|
2491
|
+
registerMetadataTools(this.service.toolRegistry, { metadataService });
|
|
2492
|
+
ctx.logger.info("[AI] Built-in metadata tools registered");
|
|
2493
|
+
const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
|
|
2494
|
+
if (!agentExists) {
|
|
2495
|
+
await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
|
|
2496
|
+
ctx.logger.info("[AI] metadata_assistant agent registered");
|
|
2497
|
+
} else {
|
|
2498
|
+
ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
|
|
2499
|
+
}
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
|
|
2502
|
+
}
|
|
1340
2503
|
}
|
|
1341
2504
|
await ctx.trigger("ai:ready", this.service);
|
|
1342
2505
|
const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
|
|
1343
2506
|
try {
|
|
1344
|
-
const
|
|
1345
|
-
if (
|
|
1346
|
-
const agentRuntime = new AgentRuntime(
|
|
2507
|
+
const metadataService2 = ctx.getService("metadata");
|
|
2508
|
+
if (metadataService2) {
|
|
2509
|
+
const agentRuntime = new AgentRuntime(metadataService2);
|
|
1347
2510
|
const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
|
|
1348
2511
|
routes.push(...agentRoutes);
|
|
1349
2512
|
}
|
|
@@ -1351,6 +2514,10 @@ var AIServicePlugin = class {
|
|
|
1351
2514
|
ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
|
|
1352
2515
|
}
|
|
1353
2516
|
await ctx.trigger("ai:routes", routes);
|
|
2517
|
+
const kernel = ctx.getKernel();
|
|
2518
|
+
if (kernel) {
|
|
2519
|
+
kernel.__aiRoutes = routes;
|
|
2520
|
+
}
|
|
1354
2521
|
ctx.logger.info(
|
|
1355
2522
|
`[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
|
|
1356
2523
|
);
|
|
@@ -1368,11 +2535,23 @@ export {
|
|
|
1368
2535
|
DATA_CHAT_AGENT,
|
|
1369
2536
|
DATA_TOOL_DEFINITIONS,
|
|
1370
2537
|
InMemoryConversationService,
|
|
2538
|
+
METADATA_ASSISTANT_AGENT,
|
|
2539
|
+
METADATA_TOOL_DEFINITIONS,
|
|
1371
2540
|
MemoryLLMAdapter,
|
|
1372
2541
|
ObjectQLConversationService,
|
|
1373
2542
|
ToolRegistry,
|
|
2543
|
+
VercelLLMAdapter,
|
|
2544
|
+
addFieldTool,
|
|
1374
2545
|
buildAIRoutes,
|
|
1375
2546
|
buildAgentRoutes,
|
|
1376
|
-
|
|
2547
|
+
createObjectTool,
|
|
2548
|
+
deleteFieldTool,
|
|
2549
|
+
describeMetadataObjectTool,
|
|
2550
|
+
encodeStreamPart,
|
|
2551
|
+
encodeVercelDataStream,
|
|
2552
|
+
listMetadataObjectsTool,
|
|
2553
|
+
modifyFieldTool,
|
|
2554
|
+
registerDataTools,
|
|
2555
|
+
registerMetadataTools
|
|
1377
2556
|
};
|
|
1378
2557
|
//# sourceMappingURL=index.js.map
|