@objectstack/service-ai 4.0.1 → 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 +9 -0
- package/dist/index.cjs +1120 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -78
- package/dist/index.d.ts +316 -78
- package/dist/index.js +1105 -63
- 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 +30 -28
- 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 +68 -36
- package/src/conversation/in-memory-conversation-service.ts +2 -2
- package/src/conversation/objectql-conversation-service.ts +67 -18
- package/src/index.ts +21 -2
- package/src/plugin.ts +166 -9
- package/src/routes/agent-routes.ts +26 -3
- package/src/routes/ai-routes.ts +156 -13
- 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
|
*
|
|
@@ -277,23 +312,26 @@ var _AIService = class _AIService {
|
|
|
277
312
|
}
|
|
278
313
|
this.logger.debug("[AI] chatWithTools tool calls", {
|
|
279
314
|
iteration,
|
|
280
|
-
calls: result.toolCalls.map((tc) => tc.
|
|
315
|
+
calls: result.toolCalls.map((tc) => tc.toolName)
|
|
281
316
|
});
|
|
317
|
+
const assistantContent = [];
|
|
318
|
+
if (result.content) assistantContent.push({ type: "text", text: result.content });
|
|
319
|
+
assistantContent.push(...result.toolCalls);
|
|
282
320
|
conversation.push({
|
|
283
321
|
role: "assistant",
|
|
284
|
-
content:
|
|
285
|
-
toolCalls: result.toolCalls
|
|
322
|
+
content: assistantContent
|
|
286
323
|
});
|
|
287
324
|
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
288
325
|
for (const tr of toolResults) {
|
|
289
326
|
if (tr.isError) {
|
|
290
|
-
const matchedCall = result.toolCalls.find((tc) => tc.
|
|
291
|
-
const toolName = matchedCall?.
|
|
292
|
-
const
|
|
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 };
|
|
293
331
|
toolErrors.push(errorEntry);
|
|
294
332
|
this.logger.warn("[AI] chatWithTools tool error", errorEntry);
|
|
295
333
|
if (onToolError && matchedCall) {
|
|
296
|
-
const action = onToolError(matchedCall,
|
|
334
|
+
const action = onToolError(matchedCall, errorText);
|
|
297
335
|
if (action === "abort") {
|
|
298
336
|
abortedByCallback = true;
|
|
299
337
|
}
|
|
@@ -301,8 +339,7 @@ var _AIService = class _AIService {
|
|
|
301
339
|
}
|
|
302
340
|
conversation.push({
|
|
303
341
|
role: "tool",
|
|
304
|
-
content: tr
|
|
305
|
-
toolCallId: tr.toolCallId
|
|
342
|
+
content: [tr]
|
|
306
343
|
});
|
|
307
344
|
}
|
|
308
345
|
if (abortedByCallback) {
|
|
@@ -348,24 +385,27 @@ var _AIService = class _AIService {
|
|
|
348
385
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
349
386
|
const result2 = await this.adapter.chat(conversation, chatOptions);
|
|
350
387
|
if (!result2.toolCalls || result2.toolCalls.length === 0) {
|
|
351
|
-
yield
|
|
352
|
-
yield
|
|
388
|
+
yield textDeltaPart("stream", result2.content);
|
|
389
|
+
yield finishPart(result2);
|
|
353
390
|
return;
|
|
354
391
|
}
|
|
355
392
|
for (const tc of result2.toolCalls) {
|
|
356
|
-
yield { type: "tool-call",
|
|
393
|
+
yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
|
|
357
394
|
}
|
|
395
|
+
const assistantContent = [];
|
|
396
|
+
if (result2.content) assistantContent.push({ type: "text", text: result2.content });
|
|
397
|
+
assistantContent.push(...result2.toolCalls);
|
|
358
398
|
conversation.push({
|
|
359
399
|
role: "assistant",
|
|
360
|
-
content:
|
|
361
|
-
toolCalls: result2.toolCalls
|
|
400
|
+
content: assistantContent
|
|
362
401
|
});
|
|
363
402
|
const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
|
|
364
403
|
for (const tr of toolResults) {
|
|
365
404
|
if (tr.isError && onToolError) {
|
|
366
|
-
const matchedCall = result2.toolCalls.find((tc) => tc.
|
|
405
|
+
const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
367
406
|
if (matchedCall) {
|
|
368
|
-
const
|
|
407
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
408
|
+
const action = onToolError(matchedCall, errorText);
|
|
369
409
|
if (action === "abort") {
|
|
370
410
|
abortedByCallback = true;
|
|
371
411
|
}
|
|
@@ -373,8 +413,7 @@ var _AIService = class _AIService {
|
|
|
373
413
|
}
|
|
374
414
|
conversation.push({
|
|
375
415
|
role: "tool",
|
|
376
|
-
content: tr
|
|
377
|
-
toolCallId: tr.toolCallId
|
|
416
|
+
content: [tr]
|
|
378
417
|
});
|
|
379
418
|
}
|
|
380
419
|
if (abortedByCallback) {
|
|
@@ -388,8 +427,8 @@ var _AIService = class _AIService {
|
|
|
388
427
|
}
|
|
389
428
|
const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
|
|
390
429
|
const result = await this.adapter.chat(conversation, finalOptions);
|
|
391
|
-
yield
|
|
392
|
-
yield
|
|
430
|
+
yield textDeltaPart("stream", result.content);
|
|
431
|
+
yield finishPart(result);
|
|
393
432
|
}
|
|
394
433
|
};
|
|
395
434
|
// ── Tool Call Loop ────────────────────────────────────────────
|
|
@@ -397,8 +436,99 @@ var _AIService = class _AIService {
|
|
|
397
436
|
_AIService.DEFAULT_MAX_ITERATIONS = 10;
|
|
398
437
|
var AIService = _AIService;
|
|
399
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
|
+
|
|
400
514
|
// src/routes/ai-routes.ts
|
|
401
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
|
+
}
|
|
402
532
|
function validateMessage(raw) {
|
|
403
533
|
if (typeof raw !== "object" || raw === null) {
|
|
404
534
|
return "each message must be an object";
|
|
@@ -407,22 +537,56 @@ function validateMessage(raw) {
|
|
|
407
537
|
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
408
538
|
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
409
539
|
}
|
|
410
|
-
|
|
411
|
-
|
|
540
|
+
const content = msg.content;
|
|
541
|
+
if (Array.isArray(msg.parts)) {
|
|
542
|
+
return null;
|
|
412
543
|
}
|
|
413
|
-
|
|
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";
|
|
414
568
|
}
|
|
415
569
|
function buildAIRoutes(aiService, conversationService, logger) {
|
|
416
570
|
return [
|
|
417
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
|
+
//
|
|
418
581
|
{
|
|
419
582
|
method: "POST",
|
|
420
583
|
path: "/api/v1/ai/chat",
|
|
421
|
-
description: "
|
|
584
|
+
description: "Chat completion (supports Vercel AI Data Stream Protocol)",
|
|
422
585
|
auth: true,
|
|
423
586
|
permissions: ["ai:chat"],
|
|
424
587
|
handler: async (req) => {
|
|
425
|
-
const
|
|
588
|
+
const body = req.body ?? {};
|
|
589
|
+
const messages = body.messages;
|
|
426
590
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
427
591
|
return { status: 400, body: { error: "messages array is required" } };
|
|
428
592
|
}
|
|
@@ -430,8 +594,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
430
594
|
const err = validateMessage(msg);
|
|
431
595
|
if (err) return { status: 400, body: { error: err } };
|
|
432
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
|
+
}
|
|
433
638
|
try {
|
|
434
|
-
const result = await aiService.
|
|
639
|
+
const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
|
|
435
640
|
return { status: 200, body: result };
|
|
436
641
|
} catch (err) {
|
|
437
642
|
logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
|
|
@@ -459,7 +664,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
459
664
|
if (!aiService.streamChat) {
|
|
460
665
|
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
461
666
|
}
|
|
462
|
-
const events = aiService.streamChat(messages, options);
|
|
667
|
+
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
463
668
|
return { status: 200, stream: true, events };
|
|
464
669
|
} catch (err) {
|
|
465
670
|
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
@@ -644,6 +849,27 @@ function validateAgentMessage(raw) {
|
|
|
644
849
|
}
|
|
645
850
|
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
646
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 ──────────────────────────────
|
|
647
873
|
{
|
|
648
874
|
method: "POST",
|
|
649
875
|
path: "/api/v1/ai/agents/:agentName/chat",
|
|
@@ -803,13 +1029,35 @@ var ObjectQLConversationService = class {
|
|
|
803
1029
|
}
|
|
804
1030
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
805
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
|
+
}
|
|
806
1054
|
await this.engine.insert(MESSAGES_OBJECT, {
|
|
807
1055
|
id: msgId,
|
|
808
1056
|
conversation_id: conversationId,
|
|
809
1057
|
role: message.role,
|
|
810
|
-
content:
|
|
811
|
-
tool_calls:
|
|
812
|
-
tool_call_id:
|
|
1058
|
+
content: contentStr,
|
|
1059
|
+
tool_calls: toolCallsJson,
|
|
1060
|
+
tool_call_id: toolCallId,
|
|
813
1061
|
created_at: now
|
|
814
1062
|
});
|
|
815
1063
|
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
@@ -854,21 +1102,42 @@ var ObjectQLConversationService = class {
|
|
|
854
1102
|
};
|
|
855
1103
|
}
|
|
856
1104
|
/**
|
|
857
|
-
* Map a database row to
|
|
1105
|
+
* Map a database row to a ModelMessage.
|
|
858
1106
|
*/
|
|
859
1107
|
toMessage(row) {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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 };
|
|
870
1140
|
}
|
|
871
|
-
return msg;
|
|
872
1141
|
}
|
|
873
1142
|
};
|
|
874
1143
|
|
|
@@ -1265,6 +1534,479 @@ function registerDataTools(registry, context) {
|
|
|
1265
1534
|
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
1266
1535
|
}
|
|
1267
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
|
+
|
|
1268
2010
|
// src/agent-runtime.ts
|
|
1269
2011
|
import { AgentSchema } from "@objectstack/spec/ai";
|
|
1270
2012
|
var AgentRuntime = class {
|
|
@@ -1272,6 +2014,27 @@ var AgentRuntime = class {
|
|
|
1272
2014
|
this.metadataService = metadataService;
|
|
1273
2015
|
}
|
|
1274
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
|
+
}
|
|
1275
2038
|
/**
|
|
1276
2039
|
* Load and validate an agent definition by name.
|
|
1277
2040
|
*
|
|
@@ -1400,15 +2163,233 @@ Guidelines:
|
|
|
1400
2163
|
}
|
|
1401
2164
|
};
|
|
1402
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
|
+
|
|
1403
2307
|
// src/plugin.ts
|
|
1404
2308
|
var AIServicePlugin = class {
|
|
1405
2309
|
constructor(options = {}) {
|
|
1406
2310
|
this.name = "com.objectstack.service-ai";
|
|
1407
2311
|
this.version = "1.0.0";
|
|
1408
2312
|
this.type = "standard";
|
|
1409
|
-
this.dependencies = [];
|
|
2313
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
1410
2314
|
this.options = options;
|
|
1411
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
|
+
}
|
|
1412
2393
|
async init(ctx) {
|
|
1413
2394
|
let hasExisting = false;
|
|
1414
2395
|
try {
|
|
@@ -1430,8 +2411,19 @@ var AIServicePlugin = class {
|
|
|
1430
2411
|
} catch {
|
|
1431
2412
|
}
|
|
1432
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}`);
|
|
1433
2425
|
const config = {
|
|
1434
|
-
adapter
|
|
2426
|
+
adapter,
|
|
1435
2427
|
logger: ctx.logger,
|
|
1436
2428
|
conversationService
|
|
1437
2429
|
};
|
|
@@ -1441,7 +2433,7 @@ var AIServicePlugin = class {
|
|
|
1441
2433
|
} else {
|
|
1442
2434
|
ctx.registerService("ai", this.service);
|
|
1443
2435
|
}
|
|
1444
|
-
ctx.
|
|
2436
|
+
ctx.getService("manifest").register({
|
|
1445
2437
|
id: "com.objectstack.service-ai",
|
|
1446
2438
|
name: "AI Service",
|
|
1447
2439
|
version: "1.0.0",
|
|
@@ -1454,13 +2446,32 @@ var AIServicePlugin = class {
|
|
|
1454
2446
|
ctx.logger.debug("[AI] Before chat", { messages });
|
|
1455
2447
|
});
|
|
1456
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
|
+
}
|
|
1457
2463
|
ctx.logger.info("[AI] Service initialized");
|
|
1458
2464
|
}
|
|
1459
2465
|
async start(ctx) {
|
|
1460
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
|
+
}
|
|
1461
2473
|
try {
|
|
1462
2474
|
const dataEngine = ctx.getService("data");
|
|
1463
|
-
const metadataService = ctx.getService("metadata");
|
|
1464
2475
|
if (dataEngine && metadataService) {
|
|
1465
2476
|
registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
|
|
1466
2477
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
@@ -1473,14 +2484,29 @@ var AIServicePlugin = class {
|
|
|
1473
2484
|
}
|
|
1474
2485
|
}
|
|
1475
2486
|
} catch {
|
|
1476
|
-
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
|
+
}
|
|
1477
2503
|
}
|
|
1478
2504
|
await ctx.trigger("ai:ready", this.service);
|
|
1479
2505
|
const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
|
|
1480
2506
|
try {
|
|
1481
|
-
const
|
|
1482
|
-
if (
|
|
1483
|
-
const agentRuntime = new AgentRuntime(
|
|
2507
|
+
const metadataService2 = ctx.getService("metadata");
|
|
2508
|
+
if (metadataService2) {
|
|
2509
|
+
const agentRuntime = new AgentRuntime(metadataService2);
|
|
1484
2510
|
const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
|
|
1485
2511
|
routes.push(...agentRoutes);
|
|
1486
2512
|
}
|
|
@@ -1488,6 +2514,10 @@ var AIServicePlugin = class {
|
|
|
1488
2514
|
ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
|
|
1489
2515
|
}
|
|
1490
2516
|
await ctx.trigger("ai:routes", routes);
|
|
2517
|
+
const kernel = ctx.getKernel();
|
|
2518
|
+
if (kernel) {
|
|
2519
|
+
kernel.__aiRoutes = routes;
|
|
2520
|
+
}
|
|
1491
2521
|
ctx.logger.info(
|
|
1492
2522
|
`[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
|
|
1493
2523
|
);
|
|
@@ -1505,11 +2535,23 @@ export {
|
|
|
1505
2535
|
DATA_CHAT_AGENT,
|
|
1506
2536
|
DATA_TOOL_DEFINITIONS,
|
|
1507
2537
|
InMemoryConversationService,
|
|
2538
|
+
METADATA_ASSISTANT_AGENT,
|
|
2539
|
+
METADATA_TOOL_DEFINITIONS,
|
|
1508
2540
|
MemoryLLMAdapter,
|
|
1509
2541
|
ObjectQLConversationService,
|
|
1510
2542
|
ToolRegistry,
|
|
2543
|
+
VercelLLMAdapter,
|
|
2544
|
+
addFieldTool,
|
|
1511
2545
|
buildAIRoutes,
|
|
1512
2546
|
buildAgentRoutes,
|
|
1513
|
-
|
|
2547
|
+
createObjectTool,
|
|
2548
|
+
deleteFieldTool,
|
|
2549
|
+
describeMetadataObjectTool,
|
|
2550
|
+
encodeStreamPart,
|
|
2551
|
+
encodeVercelDataStream,
|
|
2552
|
+
listMetadataObjectsTool,
|
|
2553
|
+
modifyFieldTool,
|
|
2554
|
+
registerDataTools,
|
|
2555
|
+
registerMetadataTools
|
|
1514
2556
|
};
|
|
1515
2557
|
//# sourceMappingURL=index.js.map
|