@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.cjs
CHANGED
|
@@ -28,12 +28,24 @@ __export(index_exports, {
|
|
|
28
28
|
DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
|
|
29
29
|
DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
|
|
30
30
|
InMemoryConversationService: () => InMemoryConversationService,
|
|
31
|
+
METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
|
|
32
|
+
METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
|
|
31
33
|
MemoryLLMAdapter: () => MemoryLLMAdapter,
|
|
32
34
|
ObjectQLConversationService: () => ObjectQLConversationService,
|
|
33
35
|
ToolRegistry: () => ToolRegistry,
|
|
36
|
+
VercelLLMAdapter: () => VercelLLMAdapter,
|
|
37
|
+
addFieldTool: () => addFieldTool,
|
|
34
38
|
buildAIRoutes: () => buildAIRoutes,
|
|
35
39
|
buildAgentRoutes: () => buildAgentRoutes,
|
|
36
|
-
|
|
40
|
+
createObjectTool: () => createObjectTool,
|
|
41
|
+
deleteFieldTool: () => deleteFieldTool,
|
|
42
|
+
describeMetadataObjectTool: () => describeMetadataObjectTool,
|
|
43
|
+
encodeStreamPart: () => encodeStreamPart,
|
|
44
|
+
encodeVercelDataStream: () => encodeVercelDataStream,
|
|
45
|
+
listMetadataObjectsTool: () => listMetadataObjectsTool,
|
|
46
|
+
modifyFieldTool: () => modifyFieldTool,
|
|
47
|
+
registerDataTools: () => registerDataTools,
|
|
48
|
+
registerMetadataTools: () => registerMetadataTools
|
|
37
49
|
});
|
|
38
50
|
module.exports = __toCommonJS(index_exports);
|
|
39
51
|
|
|
@@ -47,7 +59,9 @@ var MemoryLLMAdapter = class {
|
|
|
47
59
|
}
|
|
48
60
|
async chat(messages, options) {
|
|
49
61
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
50
|
-
const
|
|
62
|
+
const userContent = lastUserMessage?.content;
|
|
63
|
+
const text = typeof userContent === "string" ? userContent : "(complex content)";
|
|
64
|
+
const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
|
|
51
65
|
return {
|
|
52
66
|
content,
|
|
53
67
|
model: options?.model ?? "memory",
|
|
@@ -65,10 +79,15 @@ var MemoryLLMAdapter = class {
|
|
|
65
79
|
const result = await this.chat(messages);
|
|
66
80
|
const words = result.content.split(" ");
|
|
67
81
|
for (let i = 0; i < words.length; i++) {
|
|
68
|
-
const
|
|
69
|
-
yield { type: "text-delta",
|
|
82
|
+
const wordText = i === 0 ? words[i] : ` ${words[i]}`;
|
|
83
|
+
yield { type: "text-delta", id: `delta_${i}`, text: wordText };
|
|
70
84
|
}
|
|
71
|
-
yield {
|
|
85
|
+
yield {
|
|
86
|
+
type: "finish",
|
|
87
|
+
finishReason: "stop",
|
|
88
|
+
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
89
|
+
rawFinishReason: "stop"
|
|
90
|
+
};
|
|
72
91
|
}
|
|
73
92
|
async embed(input) {
|
|
74
93
|
const texts = Array.isArray(input) ? input : [input];
|
|
@@ -131,21 +150,34 @@ var ToolRegistry = class {
|
|
|
131
150
|
* Execute a tool call and return the result.
|
|
132
151
|
*/
|
|
133
152
|
async execute(toolCall) {
|
|
134
|
-
const handler = this.handlers.get(toolCall.
|
|
153
|
+
const handler = this.handlers.get(toolCall.toolName);
|
|
135
154
|
if (!handler) {
|
|
136
155
|
return {
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
type: "tool-result",
|
|
157
|
+
toolCallId: toolCall.toolCallId,
|
|
158
|
+
toolName: toolCall.toolName,
|
|
159
|
+
output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
|
|
139
160
|
isError: true
|
|
140
161
|
};
|
|
141
162
|
}
|
|
142
163
|
try {
|
|
143
|
-
const args = JSON.parse(toolCall.
|
|
164
|
+
const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
|
|
144
165
|
const content = await handler(args);
|
|
145
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
type: "tool-result",
|
|
168
|
+
toolCallId: toolCall.toolCallId,
|
|
169
|
+
toolName: toolCall.toolName,
|
|
170
|
+
output: { type: "text", value: content }
|
|
171
|
+
};
|
|
146
172
|
} catch (err) {
|
|
147
173
|
const message = err instanceof Error ? err.message : String(err);
|
|
148
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
type: "tool-result",
|
|
176
|
+
toolCallId: toolCall.toolCallId,
|
|
177
|
+
toolName: toolCall.toolName,
|
|
178
|
+
output: { type: "text", value: message },
|
|
179
|
+
isError: true
|
|
180
|
+
};
|
|
149
181
|
}
|
|
150
182
|
}
|
|
151
183
|
/**
|
|
@@ -231,6 +263,17 @@ var InMemoryConversationService = class {
|
|
|
231
263
|
};
|
|
232
264
|
|
|
233
265
|
// src/ai-service.ts
|
|
266
|
+
function textDeltaPart(id, text) {
|
|
267
|
+
return { type: "text-delta", id, text };
|
|
268
|
+
}
|
|
269
|
+
function finishPart(result) {
|
|
270
|
+
return {
|
|
271
|
+
type: "finish",
|
|
272
|
+
finishReason: "stop",
|
|
273
|
+
totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
274
|
+
rawFinishReason: "stop"
|
|
275
|
+
};
|
|
276
|
+
}
|
|
234
277
|
var _AIService = class _AIService {
|
|
235
278
|
constructor(config = {}) {
|
|
236
279
|
this.adapter = config.adapter ?? new MemoryLLMAdapter();
|
|
@@ -258,8 +301,8 @@ var _AIService = class _AIService {
|
|
|
258
301
|
this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
|
|
259
302
|
if (!this.adapter.streamChat) {
|
|
260
303
|
const result = await this.adapter.chat(messages, options);
|
|
261
|
-
yield
|
|
262
|
-
yield
|
|
304
|
+
yield textDeltaPart("fallback", result.content);
|
|
305
|
+
yield finishPart(result);
|
|
263
306
|
return;
|
|
264
307
|
}
|
|
265
308
|
yield* this.adapter.streamChat(messages, options);
|
|
@@ -276,6 +319,10 @@ var _AIService = class _AIService {
|
|
|
276
319
|
}
|
|
277
320
|
return this.adapter.listModels();
|
|
278
321
|
}
|
|
322
|
+
/** Extract the text value from a ToolExecutionResult's output. */
|
|
323
|
+
static extractOutputText(tr) {
|
|
324
|
+
return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
|
|
325
|
+
}
|
|
279
326
|
/**
|
|
280
327
|
* Chat with automatic tool call resolution.
|
|
281
328
|
*
|
|
@@ -316,23 +363,26 @@ var _AIService = class _AIService {
|
|
|
316
363
|
}
|
|
317
364
|
this.logger.debug("[AI] chatWithTools tool calls", {
|
|
318
365
|
iteration,
|
|
319
|
-
calls: result.toolCalls.map((tc) => tc.
|
|
366
|
+
calls: result.toolCalls.map((tc) => tc.toolName)
|
|
320
367
|
});
|
|
368
|
+
const assistantContent = [];
|
|
369
|
+
if (result.content) assistantContent.push({ type: "text", text: result.content });
|
|
370
|
+
assistantContent.push(...result.toolCalls);
|
|
321
371
|
conversation.push({
|
|
322
372
|
role: "assistant",
|
|
323
|
-
content:
|
|
324
|
-
toolCalls: result.toolCalls
|
|
373
|
+
content: assistantContent
|
|
325
374
|
});
|
|
326
375
|
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
327
376
|
for (const tr of toolResults) {
|
|
328
377
|
if (tr.isError) {
|
|
329
|
-
const matchedCall = result.toolCalls.find((tc) => tc.
|
|
330
|
-
const toolName = matchedCall?.
|
|
331
|
-
const
|
|
378
|
+
const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
379
|
+
const toolName = matchedCall?.toolName ?? "unknown";
|
|
380
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
381
|
+
const errorEntry = { iteration, toolName, error: errorText };
|
|
332
382
|
toolErrors.push(errorEntry);
|
|
333
383
|
this.logger.warn("[AI] chatWithTools tool error", errorEntry);
|
|
334
384
|
if (onToolError && matchedCall) {
|
|
335
|
-
const action = onToolError(matchedCall,
|
|
385
|
+
const action = onToolError(matchedCall, errorText);
|
|
336
386
|
if (action === "abort") {
|
|
337
387
|
abortedByCallback = true;
|
|
338
388
|
}
|
|
@@ -340,8 +390,7 @@ var _AIService = class _AIService {
|
|
|
340
390
|
}
|
|
341
391
|
conversation.push({
|
|
342
392
|
role: "tool",
|
|
343
|
-
content: tr
|
|
344
|
-
toolCallId: tr.toolCallId
|
|
393
|
+
content: [tr]
|
|
345
394
|
});
|
|
346
395
|
}
|
|
347
396
|
if (abortedByCallback) {
|
|
@@ -387,24 +436,27 @@ var _AIService = class _AIService {
|
|
|
387
436
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
388
437
|
const result2 = await this.adapter.chat(conversation, chatOptions);
|
|
389
438
|
if (!result2.toolCalls || result2.toolCalls.length === 0) {
|
|
390
|
-
yield
|
|
391
|
-
yield
|
|
439
|
+
yield textDeltaPart("stream", result2.content);
|
|
440
|
+
yield finishPart(result2);
|
|
392
441
|
return;
|
|
393
442
|
}
|
|
394
443
|
for (const tc of result2.toolCalls) {
|
|
395
|
-
yield { type: "tool-call",
|
|
444
|
+
yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
|
|
396
445
|
}
|
|
446
|
+
const assistantContent = [];
|
|
447
|
+
if (result2.content) assistantContent.push({ type: "text", text: result2.content });
|
|
448
|
+
assistantContent.push(...result2.toolCalls);
|
|
397
449
|
conversation.push({
|
|
398
450
|
role: "assistant",
|
|
399
|
-
content:
|
|
400
|
-
toolCalls: result2.toolCalls
|
|
451
|
+
content: assistantContent
|
|
401
452
|
});
|
|
402
453
|
const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
|
|
403
454
|
for (const tr of toolResults) {
|
|
404
455
|
if (tr.isError && onToolError) {
|
|
405
|
-
const matchedCall = result2.toolCalls.find((tc) => tc.
|
|
456
|
+
const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
406
457
|
if (matchedCall) {
|
|
407
|
-
const
|
|
458
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
459
|
+
const action = onToolError(matchedCall, errorText);
|
|
408
460
|
if (action === "abort") {
|
|
409
461
|
abortedByCallback = true;
|
|
410
462
|
}
|
|
@@ -412,8 +464,7 @@ var _AIService = class _AIService {
|
|
|
412
464
|
}
|
|
413
465
|
conversation.push({
|
|
414
466
|
role: "tool",
|
|
415
|
-
content: tr
|
|
416
|
-
toolCallId: tr.toolCallId
|
|
467
|
+
content: [tr]
|
|
417
468
|
});
|
|
418
469
|
}
|
|
419
470
|
if (abortedByCallback) {
|
|
@@ -427,8 +478,8 @@ var _AIService = class _AIService {
|
|
|
427
478
|
}
|
|
428
479
|
const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
|
|
429
480
|
const result = await this.adapter.chat(conversation, finalOptions);
|
|
430
|
-
yield
|
|
431
|
-
yield
|
|
481
|
+
yield textDeltaPart("stream", result.content);
|
|
482
|
+
yield finishPart(result);
|
|
432
483
|
}
|
|
433
484
|
};
|
|
434
485
|
// ── Tool Call Loop ────────────────────────────────────────────
|
|
@@ -436,8 +487,99 @@ var _AIService = class _AIService {
|
|
|
436
487
|
_AIService.DEFAULT_MAX_ITERATIONS = 10;
|
|
437
488
|
var AIService = _AIService;
|
|
438
489
|
|
|
490
|
+
// src/stream/vercel-stream-encoder.ts
|
|
491
|
+
function sse(data) {
|
|
492
|
+
return `data: ${JSON.stringify(data)}
|
|
493
|
+
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
function encodeStreamPart(part) {
|
|
497
|
+
switch (part.type) {
|
|
498
|
+
case "text-delta":
|
|
499
|
+
return sse({ type: "text-delta", id: "0", delta: part.text });
|
|
500
|
+
case "tool-input-start":
|
|
501
|
+
return sse({
|
|
502
|
+
type: "tool-input-start",
|
|
503
|
+
toolCallId: part.id,
|
|
504
|
+
toolName: part.toolName
|
|
505
|
+
});
|
|
506
|
+
case "tool-input-delta":
|
|
507
|
+
return sse({
|
|
508
|
+
type: "tool-input-delta",
|
|
509
|
+
toolCallId: part.id,
|
|
510
|
+
inputTextDelta: part.delta
|
|
511
|
+
});
|
|
512
|
+
case "tool-call":
|
|
513
|
+
return sse({
|
|
514
|
+
type: "tool-input-available",
|
|
515
|
+
toolCallId: part.toolCallId,
|
|
516
|
+
toolName: part.toolName,
|
|
517
|
+
input: part.input
|
|
518
|
+
});
|
|
519
|
+
case "tool-result":
|
|
520
|
+
return sse({
|
|
521
|
+
type: "tool-output-available",
|
|
522
|
+
toolCallId: part.toolCallId,
|
|
523
|
+
output: part.output
|
|
524
|
+
});
|
|
525
|
+
case "error":
|
|
526
|
+
return sse({
|
|
527
|
+
type: "error",
|
|
528
|
+
errorText: String(part.error)
|
|
529
|
+
});
|
|
530
|
+
// finish-step and finish are handled by the generator, not here
|
|
531
|
+
default:
|
|
532
|
+
return "";
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function* encodeVercelDataStream(events) {
|
|
536
|
+
yield sse({ type: "start" });
|
|
537
|
+
yield sse({ type: "start-step" });
|
|
538
|
+
yield sse({ type: "text-start", id: "0" });
|
|
539
|
+
let textOpen = true;
|
|
540
|
+
let finishReason = "stop";
|
|
541
|
+
for await (const part of events) {
|
|
542
|
+
if (part.type === "finish") {
|
|
543
|
+
finishReason = part.finishReason ?? "stop";
|
|
544
|
+
}
|
|
545
|
+
if (part.type === "finish-step" || part.type === "finish") {
|
|
546
|
+
if (textOpen) {
|
|
547
|
+
yield sse({ type: "text-end", id: "0" });
|
|
548
|
+
textOpen = false;
|
|
549
|
+
}
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const frame = encodeStreamPart(part);
|
|
553
|
+
if (frame) {
|
|
554
|
+
yield frame;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (textOpen) {
|
|
558
|
+
yield sse({ type: "text-end", id: "0" });
|
|
559
|
+
}
|
|
560
|
+
yield sse({ type: "finish-step" });
|
|
561
|
+
yield sse({ type: "finish", finishReason });
|
|
562
|
+
yield "data: [DONE]\n\n";
|
|
563
|
+
}
|
|
564
|
+
|
|
439
565
|
// src/routes/ai-routes.ts
|
|
440
566
|
var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
567
|
+
function normalizeMessage(raw) {
|
|
568
|
+
const role = raw.role;
|
|
569
|
+
if (typeof raw.content === "string") {
|
|
570
|
+
return { role, content: raw.content };
|
|
571
|
+
}
|
|
572
|
+
if (Array.isArray(raw.content)) {
|
|
573
|
+
return { role, content: raw.content };
|
|
574
|
+
}
|
|
575
|
+
if (Array.isArray(raw.parts)) {
|
|
576
|
+
const textParts = raw.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text);
|
|
577
|
+
if (textParts.length > 0) {
|
|
578
|
+
return { role, content: textParts.join("") };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return { role, content: "" };
|
|
582
|
+
}
|
|
441
583
|
function validateMessage(raw) {
|
|
442
584
|
if (typeof raw !== "object" || raw === null) {
|
|
443
585
|
return "each message must be an object";
|
|
@@ -446,22 +588,56 @@ function validateMessage(raw) {
|
|
|
446
588
|
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
447
589
|
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
448
590
|
}
|
|
449
|
-
|
|
450
|
-
|
|
591
|
+
const content = msg.content;
|
|
592
|
+
if (Array.isArray(msg.parts)) {
|
|
593
|
+
return null;
|
|
451
594
|
}
|
|
452
|
-
|
|
595
|
+
if (typeof content === "string") {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if (Array.isArray(content)) {
|
|
599
|
+
for (const part of content) {
|
|
600
|
+
if (typeof part !== "object" || part === null) {
|
|
601
|
+
return "message.content array elements must be non-null objects";
|
|
602
|
+
}
|
|
603
|
+
const partObj = part;
|
|
604
|
+
if (typeof partObj.type !== "string") {
|
|
605
|
+
return 'each message.content array element must have a string "type" property';
|
|
606
|
+
}
|
|
607
|
+
if (partObj.type === "text" && typeof partObj.text !== "string") {
|
|
608
|
+
return 'message.content elements with type "text" must have a string "text" property';
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
if (content === null || content === void 0) {
|
|
614
|
+
if (msg.role === "assistant" || msg.role === "tool") {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return "message.content must be a string, an array, or include parts";
|
|
453
619
|
}
|
|
454
620
|
function buildAIRoutes(aiService, conversationService, logger) {
|
|
455
621
|
return [
|
|
456
622
|
// ── Chat ────────────────────────────────────────────────────
|
|
623
|
+
//
|
|
624
|
+
// Dual-mode endpoint compatible with both the legacy ObjectStack
|
|
625
|
+
// format (`{ messages, options }`) and the Vercel AI SDK useChat
|
|
626
|
+
// flat format (`{ messages, system, model, stream, … }`).
|
|
627
|
+
//
|
|
628
|
+
// Behaviour:
|
|
629
|
+
// • `stream !== false` → Vercel Data Stream Protocol (SSE)
|
|
630
|
+
// • `stream === false` → JSON response (legacy)
|
|
631
|
+
//
|
|
457
632
|
{
|
|
458
633
|
method: "POST",
|
|
459
634
|
path: "/api/v1/ai/chat",
|
|
460
|
-
description: "
|
|
635
|
+
description: "Chat completion (supports Vercel AI Data Stream Protocol)",
|
|
461
636
|
auth: true,
|
|
462
637
|
permissions: ["ai:chat"],
|
|
463
638
|
handler: async (req) => {
|
|
464
|
-
const
|
|
639
|
+
const body = req.body ?? {};
|
|
640
|
+
const messages = body.messages;
|
|
465
641
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
466
642
|
return { status: 400, body: { error: "messages array is required" } };
|
|
467
643
|
}
|
|
@@ -469,8 +645,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
469
645
|
const err = validateMessage(msg);
|
|
470
646
|
if (err) return { status: 400, body: { error: err } };
|
|
471
647
|
}
|
|
648
|
+
const nested = body.options ?? {};
|
|
649
|
+
const resolvedOptions = {
|
|
650
|
+
...nested,
|
|
651
|
+
...body.model != null && { model: body.model },
|
|
652
|
+
...body.temperature != null && { temperature: body.temperature },
|
|
653
|
+
...body.maxTokens != null && { maxTokens: body.maxTokens }
|
|
654
|
+
};
|
|
655
|
+
const rawSystemPrompt = body.system ?? body.systemPrompt;
|
|
656
|
+
if (rawSystemPrompt != null && typeof rawSystemPrompt !== "string") {
|
|
657
|
+
return { status: 400, body: { error: "system/systemPrompt must be a string" } };
|
|
658
|
+
}
|
|
659
|
+
const systemPrompt = rawSystemPrompt;
|
|
660
|
+
const finalMessages = [
|
|
661
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
662
|
+
...messages.map((m) => normalizeMessage(m))
|
|
663
|
+
];
|
|
664
|
+
const wantStream = body.stream !== false;
|
|
665
|
+
if (wantStream) {
|
|
666
|
+
try {
|
|
667
|
+
if (!aiService.streamChatWithTools) {
|
|
668
|
+
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
669
|
+
}
|
|
670
|
+
const events = aiService.streamChatWithTools(finalMessages, resolvedOptions);
|
|
671
|
+
return {
|
|
672
|
+
status: 200,
|
|
673
|
+
stream: true,
|
|
674
|
+
vercelDataStream: true,
|
|
675
|
+
contentType: "text/event-stream",
|
|
676
|
+
headers: {
|
|
677
|
+
"Content-Type": "text/event-stream",
|
|
678
|
+
"Cache-Control": "no-cache",
|
|
679
|
+
"Connection": "keep-alive",
|
|
680
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
681
|
+
},
|
|
682
|
+
events: encodeVercelDataStream(events)
|
|
683
|
+
};
|
|
684
|
+
} catch (err) {
|
|
685
|
+
logger.error("[AI Route] /chat stream error", err instanceof Error ? err : void 0);
|
|
686
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
472
689
|
try {
|
|
473
|
-
const result = await aiService.
|
|
690
|
+
const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
|
|
474
691
|
return { status: 200, body: result };
|
|
475
692
|
} catch (err) {
|
|
476
693
|
logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
|
|
@@ -498,7 +715,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
498
715
|
if (!aiService.streamChat) {
|
|
499
716
|
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
500
717
|
}
|
|
501
|
-
const events = aiService.streamChat(messages, options);
|
|
718
|
+
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
502
719
|
return { status: 200, stream: true, events };
|
|
503
720
|
} catch (err) {
|
|
504
721
|
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
@@ -683,6 +900,27 @@ function validateAgentMessage(raw) {
|
|
|
683
900
|
}
|
|
684
901
|
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
685
902
|
return [
|
|
903
|
+
// ── List active agents ──────────────────────────────────────
|
|
904
|
+
{
|
|
905
|
+
method: "GET",
|
|
906
|
+
path: "/api/v1/ai/agents",
|
|
907
|
+
description: "List all active AI agents",
|
|
908
|
+
auth: true,
|
|
909
|
+
permissions: ["ai:chat"],
|
|
910
|
+
handler: async () => {
|
|
911
|
+
try {
|
|
912
|
+
const agents = await agentRuntime.listAgents();
|
|
913
|
+
return { status: 200, body: { agents } };
|
|
914
|
+
} catch (err) {
|
|
915
|
+
logger.error(
|
|
916
|
+
"[AI Route] /agents list error",
|
|
917
|
+
err instanceof Error ? err : void 0
|
|
918
|
+
);
|
|
919
|
+
return { status: 500, body: { error: "Internal AI service error" } };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
// ── Chat with a specific agent ──────────────────────────────
|
|
686
924
|
{
|
|
687
925
|
method: "POST",
|
|
688
926
|
path: "/api/v1/ai/agents/:agentName/chat",
|
|
@@ -842,13 +1080,35 @@ var ObjectQLConversationService = class {
|
|
|
842
1080
|
}
|
|
843
1081
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
844
1082
|
const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
|
|
1083
|
+
let contentStr;
|
|
1084
|
+
let toolCallsJson = null;
|
|
1085
|
+
let toolCallId = null;
|
|
1086
|
+
if (message.role === "system" || message.role === "user") {
|
|
1087
|
+
contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
1088
|
+
} else if (message.role === "assistant") {
|
|
1089
|
+
if (typeof message.content === "string") {
|
|
1090
|
+
contentStr = message.content;
|
|
1091
|
+
} else {
|
|
1092
|
+
const parts = message.content;
|
|
1093
|
+
const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
|
|
1094
|
+
const toolCalls = parts.filter((p) => p.type === "tool-call");
|
|
1095
|
+
contentStr = textParts.join("");
|
|
1096
|
+
if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
|
|
1097
|
+
}
|
|
1098
|
+
} else if (message.role === "tool") {
|
|
1099
|
+
contentStr = JSON.stringify(message.content);
|
|
1100
|
+
const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
|
|
1101
|
+
if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
|
|
1102
|
+
} else {
|
|
1103
|
+
contentStr = "";
|
|
1104
|
+
}
|
|
845
1105
|
await this.engine.insert(MESSAGES_OBJECT, {
|
|
846
1106
|
id: msgId,
|
|
847
1107
|
conversation_id: conversationId,
|
|
848
1108
|
role: message.role,
|
|
849
|
-
content:
|
|
850
|
-
tool_calls:
|
|
851
|
-
tool_call_id:
|
|
1109
|
+
content: contentStr,
|
|
1110
|
+
tool_calls: toolCallsJson,
|
|
1111
|
+
tool_call_id: toolCallId,
|
|
852
1112
|
created_at: now
|
|
853
1113
|
});
|
|
854
1114
|
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
@@ -893,21 +1153,42 @@ var ObjectQLConversationService = class {
|
|
|
893
1153
|
};
|
|
894
1154
|
}
|
|
895
1155
|
/**
|
|
896
|
-
* Map a database row to
|
|
1156
|
+
* Map a database row to a ModelMessage.
|
|
897
1157
|
*/
|
|
898
1158
|
toMessage(row) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1159
|
+
switch (row.role) {
|
|
1160
|
+
case "system":
|
|
1161
|
+
return { role: "system", content: row.content };
|
|
1162
|
+
case "user":
|
|
1163
|
+
return { role: "user", content: row.content };
|
|
1164
|
+
case "assistant": {
|
|
1165
|
+
const toolCalls = this.safeParse(row.tool_calls);
|
|
1166
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
1167
|
+
const content = [];
|
|
1168
|
+
if (row.content) content.push({ type: "text", text: row.content });
|
|
1169
|
+
content.push(...toolCalls);
|
|
1170
|
+
return { role: "assistant", content };
|
|
1171
|
+
}
|
|
1172
|
+
return { role: "assistant", content: row.content };
|
|
1173
|
+
}
|
|
1174
|
+
case "tool": {
|
|
1175
|
+
const toolResults = this.safeParse(row.content);
|
|
1176
|
+
if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
|
|
1177
|
+
return { role: "tool", content: toolResults };
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
role: "tool",
|
|
1181
|
+
content: [{
|
|
1182
|
+
type: "tool-result",
|
|
1183
|
+
toolCallId: row.tool_call_id ?? "",
|
|
1184
|
+
toolName: "unknown",
|
|
1185
|
+
output: { type: "text", value: row.content }
|
|
1186
|
+
}]
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
default:
|
|
1190
|
+
return { role: "user", content: row.content };
|
|
909
1191
|
}
|
|
910
|
-
return msg;
|
|
911
1192
|
}
|
|
912
1193
|
};
|
|
913
1194
|
|
|
@@ -1304,13 +1585,507 @@ function registerDataTools(registry, context) {
|
|
|
1304
1585
|
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
1305
1586
|
}
|
|
1306
1587
|
|
|
1307
|
-
// src/
|
|
1588
|
+
// src/tools/create-object.tool.ts
|
|
1308
1589
|
var import_ai = require("@objectstack/spec/ai");
|
|
1590
|
+
var createObjectTool = (0, import_ai.defineTool)({
|
|
1591
|
+
name: "create_object",
|
|
1592
|
+
label: "Create Object",
|
|
1593
|
+
description: "Creates a new data object (table) with the specified name, label, and optional field definitions. Use this when the user wants to create a new entity, table, or data model.",
|
|
1594
|
+
category: "data",
|
|
1595
|
+
builtIn: true,
|
|
1596
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
1597
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
1598
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
1599
|
+
// should only be set once server-side approval gating is implemented to
|
|
1600
|
+
// avoid giving users a false sense of safety.
|
|
1601
|
+
parameters: {
|
|
1602
|
+
type: "object",
|
|
1603
|
+
properties: {
|
|
1604
|
+
name: {
|
|
1605
|
+
type: "string",
|
|
1606
|
+
description: "Machine name for the object (snake_case, e.g. project_task)"
|
|
1607
|
+
},
|
|
1608
|
+
label: {
|
|
1609
|
+
type: "string",
|
|
1610
|
+
description: "Human-readable display name (e.g. Project Task)"
|
|
1611
|
+
},
|
|
1612
|
+
fields: {
|
|
1613
|
+
type: "array",
|
|
1614
|
+
description: "Initial fields to create with the object",
|
|
1615
|
+
items: {
|
|
1616
|
+
type: "object",
|
|
1617
|
+
properties: {
|
|
1618
|
+
name: { type: "string", description: "Field machine name (snake_case)" },
|
|
1619
|
+
label: { type: "string", description: "Field display name" },
|
|
1620
|
+
type: {
|
|
1621
|
+
type: "string",
|
|
1622
|
+
description: "Field data type",
|
|
1623
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
1624
|
+
},
|
|
1625
|
+
required: { type: "boolean", description: "Whether the field is required" }
|
|
1626
|
+
},
|
|
1627
|
+
required: ["name", "type"]
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1630
|
+
enableFeatures: {
|
|
1631
|
+
type: "object",
|
|
1632
|
+
description: "Object capability flags",
|
|
1633
|
+
properties: {
|
|
1634
|
+
trackHistory: { type: "boolean" },
|
|
1635
|
+
apiEnabled: { type: "boolean" }
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
required: ["name", "label"],
|
|
1640
|
+
additionalProperties: false
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// src/tools/add-field.tool.ts
|
|
1645
|
+
var import_ai2 = require("@objectstack/spec/ai");
|
|
1646
|
+
var addFieldTool = (0, import_ai2.defineTool)({
|
|
1647
|
+
name: "add_field",
|
|
1648
|
+
label: "Add Field",
|
|
1649
|
+
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.",
|
|
1650
|
+
category: "data",
|
|
1651
|
+
builtIn: true,
|
|
1652
|
+
parameters: {
|
|
1653
|
+
type: "object",
|
|
1654
|
+
properties: {
|
|
1655
|
+
objectName: {
|
|
1656
|
+
type: "string",
|
|
1657
|
+
description: "Target object machine name (snake_case)"
|
|
1658
|
+
},
|
|
1659
|
+
name: {
|
|
1660
|
+
type: "string",
|
|
1661
|
+
description: "Field machine name (snake_case, e.g. due_date)"
|
|
1662
|
+
},
|
|
1663
|
+
label: {
|
|
1664
|
+
type: "string",
|
|
1665
|
+
description: "Human-readable field label (e.g. Due Date)"
|
|
1666
|
+
},
|
|
1667
|
+
type: {
|
|
1668
|
+
type: "string",
|
|
1669
|
+
description: "Field data type",
|
|
1670
|
+
enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
|
|
1671
|
+
},
|
|
1672
|
+
required: {
|
|
1673
|
+
type: "boolean",
|
|
1674
|
+
description: "Whether the field is required"
|
|
1675
|
+
},
|
|
1676
|
+
defaultValue: {
|
|
1677
|
+
description: "Default value for the field"
|
|
1678
|
+
},
|
|
1679
|
+
options: {
|
|
1680
|
+
type: "array",
|
|
1681
|
+
description: "Options for select/picklist fields",
|
|
1682
|
+
items: {
|
|
1683
|
+
type: "object",
|
|
1684
|
+
properties: {
|
|
1685
|
+
label: { type: "string" },
|
|
1686
|
+
value: {
|
|
1687
|
+
type: "string",
|
|
1688
|
+
description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
|
|
1689
|
+
pattern: "^[a-z_][a-z0-9_]*$"
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
reference: {
|
|
1695
|
+
type: "string",
|
|
1696
|
+
description: "Referenced object name for lookup fields (snake_case, e.g. account)"
|
|
1697
|
+
}
|
|
1698
|
+
},
|
|
1699
|
+
required: ["objectName", "name", "type"],
|
|
1700
|
+
additionalProperties: false
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
// src/tools/modify-field.tool.ts
|
|
1705
|
+
var import_ai3 = require("@objectstack/spec/ai");
|
|
1706
|
+
var modifyFieldTool = (0, import_ai3.defineTool)({
|
|
1707
|
+
name: "modify_field",
|
|
1708
|
+
label: "Modify Field",
|
|
1709
|
+
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).",
|
|
1710
|
+
category: "data",
|
|
1711
|
+
builtIn: true,
|
|
1712
|
+
parameters: {
|
|
1713
|
+
type: "object",
|
|
1714
|
+
properties: {
|
|
1715
|
+
objectName: {
|
|
1716
|
+
type: "string",
|
|
1717
|
+
description: "Target object machine name (snake_case)"
|
|
1718
|
+
},
|
|
1719
|
+
fieldName: {
|
|
1720
|
+
type: "string",
|
|
1721
|
+
description: "Existing field machine name to modify (snake_case)"
|
|
1722
|
+
},
|
|
1723
|
+
changes: {
|
|
1724
|
+
type: "object",
|
|
1725
|
+
description: "Field properties to update (partial patch)",
|
|
1726
|
+
properties: {
|
|
1727
|
+
label: { type: "string", description: "New display label" },
|
|
1728
|
+
type: { type: "string", description: "New field type" },
|
|
1729
|
+
required: { type: "boolean", description: "Update required constraint" },
|
|
1730
|
+
defaultValue: { description: "New default value" }
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
required: ["objectName", "fieldName", "changes"],
|
|
1735
|
+
additionalProperties: false
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
// src/tools/delete-field.tool.ts
|
|
1740
|
+
var import_ai4 = require("@objectstack/spec/ai");
|
|
1741
|
+
var deleteFieldTool = (0, import_ai4.defineTool)({
|
|
1742
|
+
name: "delete_field",
|
|
1743
|
+
label: "Delete Field",
|
|
1744
|
+
description: "Removes a field (column) from an existing data object. This is a destructive operation. Use this when the user explicitly wants to remove an attribute or column from a table.",
|
|
1745
|
+
category: "data",
|
|
1746
|
+
builtIn: true,
|
|
1747
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
1748
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
1749
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
1750
|
+
// should only be set once server-side approval gating is implemented.
|
|
1751
|
+
parameters: {
|
|
1752
|
+
type: "object",
|
|
1753
|
+
properties: {
|
|
1754
|
+
objectName: {
|
|
1755
|
+
type: "string",
|
|
1756
|
+
description: "Target object machine name (snake_case)"
|
|
1757
|
+
},
|
|
1758
|
+
fieldName: {
|
|
1759
|
+
type: "string",
|
|
1760
|
+
description: "Field machine name to delete (snake_case)"
|
|
1761
|
+
}
|
|
1762
|
+
},
|
|
1763
|
+
required: ["objectName", "fieldName"],
|
|
1764
|
+
additionalProperties: false
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
// src/tools/list-metadata-objects.tool.ts
|
|
1769
|
+
var import_ai5 = require("@objectstack/spec/ai");
|
|
1770
|
+
var listMetadataObjectsTool = (0, import_ai5.defineTool)({
|
|
1771
|
+
name: "list_metadata_objects",
|
|
1772
|
+
label: "List Metadata Objects",
|
|
1773
|
+
description: "Lists all registered metadata objects (tables) in the current environment. Use this when the user wants to see what tables, entities, or data models are defined in metadata.",
|
|
1774
|
+
category: "data",
|
|
1775
|
+
builtIn: true,
|
|
1776
|
+
parameters: {
|
|
1777
|
+
type: "object",
|
|
1778
|
+
properties: {
|
|
1779
|
+
filter: {
|
|
1780
|
+
type: "string",
|
|
1781
|
+
description: "Optional name or label substring to filter objects"
|
|
1782
|
+
},
|
|
1783
|
+
includeFields: {
|
|
1784
|
+
type: "boolean",
|
|
1785
|
+
description: "Whether to include field summaries for each object (default: false)"
|
|
1786
|
+
}
|
|
1787
|
+
},
|
|
1788
|
+
additionalProperties: false
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
// src/tools/describe-metadata-object.tool.ts
|
|
1793
|
+
var import_ai6 = require("@objectstack/spec/ai");
|
|
1794
|
+
var describeMetadataObjectTool = (0, import_ai6.defineTool)({
|
|
1795
|
+
name: "describe_metadata_object",
|
|
1796
|
+
label: "Describe Metadata Object",
|
|
1797
|
+
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.",
|
|
1798
|
+
category: "data",
|
|
1799
|
+
builtIn: true,
|
|
1800
|
+
parameters: {
|
|
1801
|
+
type: "object",
|
|
1802
|
+
properties: {
|
|
1803
|
+
objectName: {
|
|
1804
|
+
type: "string",
|
|
1805
|
+
description: "Object machine name to describe (snake_case)"
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
required: ["objectName"],
|
|
1809
|
+
additionalProperties: false
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
// src/tools/metadata-tools.ts
|
|
1814
|
+
var METADATA_TOOL_DEFINITIONS = [
|
|
1815
|
+
createObjectTool,
|
|
1816
|
+
addFieldTool,
|
|
1817
|
+
modifyFieldTool,
|
|
1818
|
+
deleteFieldTool,
|
|
1819
|
+
listMetadataObjectsTool,
|
|
1820
|
+
describeMetadataObjectTool
|
|
1821
|
+
];
|
|
1822
|
+
var SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
|
|
1823
|
+
function isSnakeCase(value) {
|
|
1824
|
+
return SNAKE_CASE_RE.test(value);
|
|
1825
|
+
}
|
|
1826
|
+
function createCreateObjectHandler(ctx) {
|
|
1827
|
+
return async (args) => {
|
|
1828
|
+
const { name, label, fields, enableFeatures } = args;
|
|
1829
|
+
if (!name || !label) {
|
|
1830
|
+
return JSON.stringify({ error: 'Both "name" and "label" are required' });
|
|
1831
|
+
}
|
|
1832
|
+
if (!isSnakeCase(name)) {
|
|
1833
|
+
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
|
|
1834
|
+
}
|
|
1835
|
+
const existing = await ctx.metadataService.getObject(name);
|
|
1836
|
+
if (existing) {
|
|
1837
|
+
return JSON.stringify({ error: `Object "${name}" already exists` });
|
|
1838
|
+
}
|
|
1839
|
+
const fieldMap = {};
|
|
1840
|
+
if (fields && Array.isArray(fields)) {
|
|
1841
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1842
|
+
for (const f of fields) {
|
|
1843
|
+
if (!f.name) {
|
|
1844
|
+
return JSON.stringify({ error: 'Each field must have a "name" property' });
|
|
1845
|
+
}
|
|
1846
|
+
if (!isSnakeCase(f.name)) {
|
|
1847
|
+
return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
|
|
1848
|
+
}
|
|
1849
|
+
if (seenNames.has(f.name)) {
|
|
1850
|
+
return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
|
|
1851
|
+
}
|
|
1852
|
+
seenNames.add(f.name);
|
|
1853
|
+
fieldMap[f.name] = {
|
|
1854
|
+
type: f.type,
|
|
1855
|
+
...f.label ? { label: f.label } : {},
|
|
1856
|
+
...f.required !== void 0 ? { required: f.required } : {}
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
const objectDef = {
|
|
1861
|
+
name,
|
|
1862
|
+
label,
|
|
1863
|
+
...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
|
|
1864
|
+
...enableFeatures ? { enable: enableFeatures } : {}
|
|
1865
|
+
};
|
|
1866
|
+
await ctx.metadataService.register("object", name, objectDef);
|
|
1867
|
+
return JSON.stringify({
|
|
1868
|
+
name,
|
|
1869
|
+
label,
|
|
1870
|
+
fieldCount: Object.keys(fieldMap).length
|
|
1871
|
+
});
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
function createAddFieldHandler(ctx) {
|
|
1875
|
+
return async (args) => {
|
|
1876
|
+
const { objectName, name, label, type, required, defaultValue, options, reference } = args;
|
|
1877
|
+
if (!objectName || !name || !type) {
|
|
1878
|
+
return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
|
|
1879
|
+
}
|
|
1880
|
+
if (!isSnakeCase(objectName)) {
|
|
1881
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1882
|
+
}
|
|
1883
|
+
if (!isSnakeCase(name)) {
|
|
1884
|
+
return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
|
|
1885
|
+
}
|
|
1886
|
+
if (reference && !isSnakeCase(reference)) {
|
|
1887
|
+
return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
|
|
1888
|
+
}
|
|
1889
|
+
if (options && Array.isArray(options)) {
|
|
1890
|
+
for (const opt of options) {
|
|
1891
|
+
if (opt.value && !isSnakeCase(opt.value)) {
|
|
1892
|
+
return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1897
|
+
if (!objectDef) {
|
|
1898
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1899
|
+
}
|
|
1900
|
+
const def = objectDef;
|
|
1901
|
+
if (def.fields && def.fields[name]) {
|
|
1902
|
+
return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
|
|
1903
|
+
}
|
|
1904
|
+
const fieldDef = {
|
|
1905
|
+
type,
|
|
1906
|
+
...label ? { label } : {},
|
|
1907
|
+
...required !== void 0 ? { required } : {},
|
|
1908
|
+
...defaultValue !== void 0 ? { defaultValue } : {},
|
|
1909
|
+
...options ? { options } : {},
|
|
1910
|
+
...reference ? { reference } : {}
|
|
1911
|
+
};
|
|
1912
|
+
const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
|
|
1913
|
+
await ctx.metadataService.register("object", objectName, {
|
|
1914
|
+
...def,
|
|
1915
|
+
fields: updatedFields
|
|
1916
|
+
});
|
|
1917
|
+
return JSON.stringify({
|
|
1918
|
+
objectName,
|
|
1919
|
+
fieldName: name,
|
|
1920
|
+
fieldType: type
|
|
1921
|
+
});
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
function createModifyFieldHandler(ctx) {
|
|
1925
|
+
return async (args) => {
|
|
1926
|
+
const { objectName, fieldName, changes } = args;
|
|
1927
|
+
if (!objectName || !fieldName || !changes) {
|
|
1928
|
+
return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
|
|
1929
|
+
}
|
|
1930
|
+
if (!isSnakeCase(objectName)) {
|
|
1931
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1932
|
+
}
|
|
1933
|
+
if (!isSnakeCase(fieldName)) {
|
|
1934
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
1935
|
+
}
|
|
1936
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1937
|
+
if (!objectDef) {
|
|
1938
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1939
|
+
}
|
|
1940
|
+
const def = objectDef;
|
|
1941
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
1942
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
1943
|
+
}
|
|
1944
|
+
const existingField = def.fields[fieldName];
|
|
1945
|
+
const updatedField = { ...existingField, ...changes };
|
|
1946
|
+
const updatedFields = { ...def.fields, [fieldName]: updatedField };
|
|
1947
|
+
await ctx.metadataService.register("object", objectName, {
|
|
1948
|
+
...def,
|
|
1949
|
+
fields: updatedFields
|
|
1950
|
+
});
|
|
1951
|
+
return JSON.stringify({
|
|
1952
|
+
objectName,
|
|
1953
|
+
fieldName,
|
|
1954
|
+
updatedProperties: Object.keys(changes)
|
|
1955
|
+
});
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function createDeleteFieldHandler(ctx) {
|
|
1959
|
+
return async (args) => {
|
|
1960
|
+
const { objectName, fieldName } = args;
|
|
1961
|
+
if (!objectName || !fieldName) {
|
|
1962
|
+
return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
|
|
1963
|
+
}
|
|
1964
|
+
if (!isSnakeCase(objectName)) {
|
|
1965
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
1966
|
+
}
|
|
1967
|
+
if (!isSnakeCase(fieldName)) {
|
|
1968
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
1969
|
+
}
|
|
1970
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
1971
|
+
if (!objectDef) {
|
|
1972
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
1973
|
+
}
|
|
1974
|
+
const def = objectDef;
|
|
1975
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
1976
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
1977
|
+
}
|
|
1978
|
+
const { [fieldName]: _removed, ...remainingFields } = def.fields;
|
|
1979
|
+
await ctx.metadataService.register("object", objectName, {
|
|
1980
|
+
...def,
|
|
1981
|
+
fields: remainingFields
|
|
1982
|
+
});
|
|
1983
|
+
return JSON.stringify({
|
|
1984
|
+
objectName,
|
|
1985
|
+
fieldName,
|
|
1986
|
+
success: true
|
|
1987
|
+
});
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function createListObjectsHandler2(ctx) {
|
|
1991
|
+
return async (args) => {
|
|
1992
|
+
const { filter, includeFields } = args ?? {};
|
|
1993
|
+
const objects = await ctx.metadataService.listObjects();
|
|
1994
|
+
let result = objects.map((o) => {
|
|
1995
|
+
const base = {
|
|
1996
|
+
name: o.name,
|
|
1997
|
+
label: o.label ?? o.name,
|
|
1998
|
+
fieldCount: o.fields ? Object.keys(o.fields).length : 0
|
|
1999
|
+
};
|
|
2000
|
+
if (includeFields && o.fields) {
|
|
2001
|
+
base.fields = Object.entries(o.fields).map(([key, f]) => ({
|
|
2002
|
+
name: key,
|
|
2003
|
+
type: f.type,
|
|
2004
|
+
label: f.label ?? key
|
|
2005
|
+
}));
|
|
2006
|
+
}
|
|
2007
|
+
return base;
|
|
2008
|
+
});
|
|
2009
|
+
if (filter) {
|
|
2010
|
+
const lower = filter.toLowerCase();
|
|
2011
|
+
result = result.filter(
|
|
2012
|
+
(o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
return JSON.stringify({
|
|
2016
|
+
objects: result,
|
|
2017
|
+
totalCount: result.length
|
|
2018
|
+
});
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
function createDescribeObjectHandler2(ctx) {
|
|
2022
|
+
return async (args) => {
|
|
2023
|
+
const { objectName } = args;
|
|
2024
|
+
if (!objectName) {
|
|
2025
|
+
return JSON.stringify({ error: '"objectName" is required' });
|
|
2026
|
+
}
|
|
2027
|
+
if (!isSnakeCase(objectName)) {
|
|
2028
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
2029
|
+
}
|
|
2030
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
2031
|
+
if (!objectDef) {
|
|
2032
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
2033
|
+
}
|
|
2034
|
+
const def = objectDef;
|
|
2035
|
+
const fields = def.fields ?? {};
|
|
2036
|
+
const fieldSummary = Object.entries(fields).map(([key, f]) => ({
|
|
2037
|
+
name: key,
|
|
2038
|
+
type: f.type,
|
|
2039
|
+
label: f.label ?? key,
|
|
2040
|
+
required: f.required ?? false,
|
|
2041
|
+
...f.reference ? { reference: f.reference } : {},
|
|
2042
|
+
...f.options ? { options: f.options } : {}
|
|
2043
|
+
}));
|
|
2044
|
+
return JSON.stringify({
|
|
2045
|
+
name: def.name,
|
|
2046
|
+
label: def.label ?? def.name,
|
|
2047
|
+
fields: fieldSummary,
|
|
2048
|
+
enableFeatures: def.enable ?? {}
|
|
2049
|
+
});
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function registerMetadataTools(registry, context) {
|
|
2053
|
+
registry.register(createObjectTool, createCreateObjectHandler(context));
|
|
2054
|
+
registry.register(addFieldTool, createAddFieldHandler(context));
|
|
2055
|
+
registry.register(modifyFieldTool, createModifyFieldHandler(context));
|
|
2056
|
+
registry.register(deleteFieldTool, createDeleteFieldHandler(context));
|
|
2057
|
+
registry.register(listMetadataObjectsTool, createListObjectsHandler2(context));
|
|
2058
|
+
registry.register(describeMetadataObjectTool, createDescribeObjectHandler2(context));
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// src/agent-runtime.ts
|
|
2062
|
+
var import_ai7 = require("@objectstack/spec/ai");
|
|
1309
2063
|
var AgentRuntime = class {
|
|
1310
2064
|
constructor(metadataService) {
|
|
1311
2065
|
this.metadataService = metadataService;
|
|
1312
2066
|
}
|
|
1313
2067
|
// ── Public API ────────────────────────────────────────────────
|
|
2068
|
+
/**
|
|
2069
|
+
* List all active agents registered in the metadata service.
|
|
2070
|
+
*
|
|
2071
|
+
* Returns a summary for each agent (name, label, role) suitable
|
|
2072
|
+
* for populating an agent selector dropdown in the UI.
|
|
2073
|
+
*/
|
|
2074
|
+
async listAgents() {
|
|
2075
|
+
const rawItems = await this.metadataService.list("agent");
|
|
2076
|
+
const agents = [];
|
|
2077
|
+
for (const raw of rawItems) {
|
|
2078
|
+
const result = import_ai7.AgentSchema.safeParse(raw);
|
|
2079
|
+
if (result.success && result.data.active) {
|
|
2080
|
+
agents.push({
|
|
2081
|
+
name: result.data.name,
|
|
2082
|
+
label: result.data.label,
|
|
2083
|
+
role: result.data.role
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return agents;
|
|
2088
|
+
}
|
|
1314
2089
|
/**
|
|
1315
2090
|
* Load and validate an agent definition by name.
|
|
1316
2091
|
*
|
|
@@ -1322,7 +2097,7 @@ var AgentRuntime = class {
|
|
|
1322
2097
|
async loadAgent(agentName) {
|
|
1323
2098
|
const raw = await this.metadataService.get("agent", agentName);
|
|
1324
2099
|
if (!raw) return void 0;
|
|
1325
|
-
const result =
|
|
2100
|
+
const result = import_ai7.AgentSchema.safeParse(raw);
|
|
1326
2101
|
if (!result.success) {
|
|
1327
2102
|
return void 0;
|
|
1328
2103
|
}
|
|
@@ -1439,15 +2214,233 @@ Guidelines:
|
|
|
1439
2214
|
}
|
|
1440
2215
|
};
|
|
1441
2216
|
|
|
2217
|
+
// src/agents/metadata-assistant-agent.ts
|
|
2218
|
+
var METADATA_ASSISTANT_AGENT = {
|
|
2219
|
+
name: "metadata_assistant",
|
|
2220
|
+
label: "Metadata Assistant",
|
|
2221
|
+
role: "Schema Architect",
|
|
2222
|
+
instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
|
|
2223
|
+
|
|
2224
|
+
Capabilities:
|
|
2225
|
+
- Create new data objects (tables) with fields
|
|
2226
|
+
- Add fields (columns) to existing objects
|
|
2227
|
+
- Modify field properties (label, type, required, default value)
|
|
2228
|
+
- Delete fields from objects
|
|
2229
|
+
- List all registered metadata objects and their schemas
|
|
2230
|
+
- Describe the full schema of a specific object
|
|
2231
|
+
|
|
2232
|
+
Guidelines:
|
|
2233
|
+
1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
|
|
2234
|
+
2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
|
|
2235
|
+
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
|
|
2236
|
+
4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
|
|
2237
|
+
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
|
|
2238
|
+
6. Explain what changes you are about to make before executing them.
|
|
2239
|
+
7. After making changes, confirm the result by describing the updated schema.
|
|
2240
|
+
8. For destructive operations (deleting fields), always warn the user about potential data loss.
|
|
2241
|
+
9. Always answer in the same language the user is using.
|
|
2242
|
+
10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
|
|
2243
|
+
model: {
|
|
2244
|
+
provider: "openai",
|
|
2245
|
+
model: "gpt-4",
|
|
2246
|
+
temperature: 0.2,
|
|
2247
|
+
maxTokens: 4096
|
|
2248
|
+
},
|
|
2249
|
+
tools: [
|
|
2250
|
+
{ type: "action", name: "create_object", description: "Create a new data object (table)" },
|
|
2251
|
+
{ type: "action", name: "add_field", description: "Add a field to an existing object" },
|
|
2252
|
+
{ type: "action", name: "modify_field", description: "Modify an existing field definition" },
|
|
2253
|
+
{ type: "action", name: "delete_field", description: "Delete a field from an object" },
|
|
2254
|
+
{ type: "query", name: "list_metadata_objects", description: "List all metadata objects" },
|
|
2255
|
+
{ type: "query", name: "describe_metadata_object", description: "Describe an object schema" }
|
|
2256
|
+
],
|
|
2257
|
+
active: true,
|
|
2258
|
+
visibility: "global",
|
|
2259
|
+
guardrails: {
|
|
2260
|
+
maxTokensPerInvocation: 8192,
|
|
2261
|
+
maxExecutionTimeSec: 60,
|
|
2262
|
+
blockedTopics: ["drop_database", "raw_sql", "system_tables"]
|
|
2263
|
+
},
|
|
2264
|
+
planning: {
|
|
2265
|
+
strategy: "react",
|
|
2266
|
+
maxIterations: 10,
|
|
2267
|
+
allowReplan: true
|
|
2268
|
+
},
|
|
2269
|
+
memory: {
|
|
2270
|
+
shortTerm: {
|
|
2271
|
+
maxMessages: 30,
|
|
2272
|
+
maxTokens: 8192
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2277
|
+
// src/adapters/vercel-adapter.ts
|
|
2278
|
+
var import_ai8 = require("ai");
|
|
2279
|
+
function buildVercelOptions(options) {
|
|
2280
|
+
if (!options) return {};
|
|
2281
|
+
const opts = {};
|
|
2282
|
+
if (options.temperature != null) opts.temperature = options.temperature;
|
|
2283
|
+
if (options.maxTokens != null) opts.maxTokens = options.maxTokens;
|
|
2284
|
+
if (options.stop?.length) opts.stopSequences = options.stop;
|
|
2285
|
+
if (options.tools?.length) {
|
|
2286
|
+
const tools = {};
|
|
2287
|
+
for (const t of options.tools) {
|
|
2288
|
+
tools[t.name] = (0, import_ai8.tool)({
|
|
2289
|
+
description: t.description,
|
|
2290
|
+
inputSchema: (0, import_ai8.jsonSchema)(t.parameters)
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
opts.tools = tools;
|
|
2294
|
+
}
|
|
2295
|
+
if (options.toolChoice != null) {
|
|
2296
|
+
opts.toolChoice = options.toolChoice;
|
|
2297
|
+
}
|
|
2298
|
+
return opts;
|
|
2299
|
+
}
|
|
2300
|
+
var VercelLLMAdapter = class {
|
|
2301
|
+
constructor(config) {
|
|
2302
|
+
this.name = "vercel";
|
|
2303
|
+
this.model = config.model;
|
|
2304
|
+
}
|
|
2305
|
+
async chat(messages, options) {
|
|
2306
|
+
const result = await (0, import_ai8.generateText)({
|
|
2307
|
+
model: this.model,
|
|
2308
|
+
messages,
|
|
2309
|
+
...buildVercelOptions(options)
|
|
2310
|
+
});
|
|
2311
|
+
return {
|
|
2312
|
+
content: result.text,
|
|
2313
|
+
model: result.response?.modelId,
|
|
2314
|
+
toolCalls: result.toolCalls?.length ? result.toolCalls : void 0,
|
|
2315
|
+
usage: result.usage ? {
|
|
2316
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2317
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2318
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2319
|
+
} : void 0
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
async complete(prompt, options) {
|
|
2323
|
+
const result = await (0, import_ai8.generateText)({
|
|
2324
|
+
model: this.model,
|
|
2325
|
+
prompt,
|
|
2326
|
+
...buildVercelOptions(options)
|
|
2327
|
+
});
|
|
2328
|
+
return {
|
|
2329
|
+
content: result.text,
|
|
2330
|
+
model: result.response?.modelId,
|
|
2331
|
+
usage: result.usage ? {
|
|
2332
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
2333
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
2334
|
+
totalTokens: result.usage.totalTokens ?? 0
|
|
2335
|
+
} : void 0
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
async *streamChat(messages, options) {
|
|
2339
|
+
const result = (0, import_ai8.streamText)({
|
|
2340
|
+
model: this.model,
|
|
2341
|
+
messages,
|
|
2342
|
+
...buildVercelOptions(options)
|
|
2343
|
+
});
|
|
2344
|
+
for await (const part of result.fullStream) {
|
|
2345
|
+
yield part;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
async embed(_input) {
|
|
2349
|
+
throw new Error(
|
|
2350
|
+
"[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
async listModels() {
|
|
2354
|
+
return [];
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
|
|
1442
2358
|
// src/plugin.ts
|
|
1443
2359
|
var AIServicePlugin = class {
|
|
1444
2360
|
constructor(options = {}) {
|
|
1445
2361
|
this.name = "com.objectstack.service-ai";
|
|
1446
2362
|
this.version = "1.0.0";
|
|
1447
2363
|
this.type = "standard";
|
|
1448
|
-
this.dependencies = [];
|
|
2364
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
1449
2365
|
this.options = options;
|
|
1450
2366
|
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Auto-detect LLM provider from environment variables.
|
|
2369
|
+
*
|
|
2370
|
+
* Priority order:
|
|
2371
|
+
* 1. AI_GATEWAY_MODEL → Vercel AI Gateway
|
|
2372
|
+
* 2. OPENAI_API_KEY → OpenAI
|
|
2373
|
+
* 3. ANTHROPIC_API_KEY → Anthropic
|
|
2374
|
+
* 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
|
|
2375
|
+
* 5. Fallback → MemoryLLMAdapter
|
|
2376
|
+
*
|
|
2377
|
+
* Returns the adapter and a description for logging.
|
|
2378
|
+
*/
|
|
2379
|
+
async detectAdapter(ctx) {
|
|
2380
|
+
const gatewayModel = process.env.AI_GATEWAY_MODEL;
|
|
2381
|
+
if (gatewayModel) {
|
|
2382
|
+
try {
|
|
2383
|
+
const gatewayPkg = "@ai-sdk/gateway";
|
|
2384
|
+
const { gateway } = await import(
|
|
2385
|
+
/* webpackIgnore: true */
|
|
2386
|
+
gatewayPkg
|
|
2387
|
+
);
|
|
2388
|
+
const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
|
|
2389
|
+
return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
|
|
2390
|
+
} catch (err) {
|
|
2391
|
+
ctx.logger.warn(
|
|
2392
|
+
`[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
|
|
2393
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
const providerConfigs = [
|
|
2398
|
+
{
|
|
2399
|
+
envKey: "OPENAI_API_KEY",
|
|
2400
|
+
pkg: "@ai-sdk/openai",
|
|
2401
|
+
factory: "openai",
|
|
2402
|
+
defaultModel: "gpt-4o",
|
|
2403
|
+
displayName: "OpenAI"
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
2407
|
+
pkg: "@ai-sdk/anthropic",
|
|
2408
|
+
factory: "anthropic",
|
|
2409
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
2410
|
+
displayName: "Anthropic"
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
2414
|
+
pkg: "@ai-sdk/google",
|
|
2415
|
+
factory: "google",
|
|
2416
|
+
defaultModel: "gemini-2.0-flash",
|
|
2417
|
+
displayName: "Google"
|
|
2418
|
+
}
|
|
2419
|
+
];
|
|
2420
|
+
for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
|
|
2421
|
+
if (process.env[envKey]) {
|
|
2422
|
+
try {
|
|
2423
|
+
const mod = await import(
|
|
2424
|
+
/* webpackIgnore: true */
|
|
2425
|
+
pkg
|
|
2426
|
+
);
|
|
2427
|
+
const createModel = mod[factory] ?? mod.default;
|
|
2428
|
+
if (typeof createModel === "function") {
|
|
2429
|
+
const modelId = process.env.AI_MODEL ?? defaultModel;
|
|
2430
|
+
const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
|
|
2431
|
+
return { adapter, description: `${displayName} (model: ${modelId})` };
|
|
2432
|
+
}
|
|
2433
|
+
} catch (err) {
|
|
2434
|
+
ctx.logger.warn(
|
|
2435
|
+
`[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
|
|
2436
|
+
err instanceof Error ? { error: err.message } : void 0
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
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.");
|
|
2442
|
+
return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode - for testing only)" };
|
|
2443
|
+
}
|
|
1451
2444
|
async init(ctx) {
|
|
1452
2445
|
let hasExisting = false;
|
|
1453
2446
|
try {
|
|
@@ -1469,8 +2462,19 @@ var AIServicePlugin = class {
|
|
|
1469
2462
|
} catch {
|
|
1470
2463
|
}
|
|
1471
2464
|
}
|
|
2465
|
+
let adapter;
|
|
2466
|
+
let adapterDescription;
|
|
2467
|
+
if (this.options.adapter) {
|
|
2468
|
+
adapter = this.options.adapter;
|
|
2469
|
+
adapterDescription = `${adapter.name} (explicitly configured)`;
|
|
2470
|
+
} else {
|
|
2471
|
+
const detected = await this.detectAdapter(ctx);
|
|
2472
|
+
adapter = detected.adapter;
|
|
2473
|
+
adapterDescription = detected.description;
|
|
2474
|
+
}
|
|
2475
|
+
ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
|
|
1472
2476
|
const config = {
|
|
1473
|
-
adapter
|
|
2477
|
+
adapter,
|
|
1474
2478
|
logger: ctx.logger,
|
|
1475
2479
|
conversationService
|
|
1476
2480
|
};
|
|
@@ -1480,7 +2484,7 @@ var AIServicePlugin = class {
|
|
|
1480
2484
|
} else {
|
|
1481
2485
|
ctx.registerService("ai", this.service);
|
|
1482
2486
|
}
|
|
1483
|
-
ctx.
|
|
2487
|
+
ctx.getService("manifest").register({
|
|
1484
2488
|
id: "com.objectstack.service-ai",
|
|
1485
2489
|
name: "AI Service",
|
|
1486
2490
|
version: "1.0.0",
|
|
@@ -1493,13 +2497,32 @@ var AIServicePlugin = class {
|
|
|
1493
2497
|
ctx.logger.debug("[AI] Before chat", { messages });
|
|
1494
2498
|
});
|
|
1495
2499
|
}
|
|
2500
|
+
try {
|
|
2501
|
+
const setupNav = ctx.getService("setupNav");
|
|
2502
|
+
if (setupNav) {
|
|
2503
|
+
setupNav.contribute({
|
|
2504
|
+
areaId: "area_ai",
|
|
2505
|
+
items: [
|
|
2506
|
+
{ id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
|
|
2507
|
+
{ id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
|
|
2508
|
+
]
|
|
2509
|
+
});
|
|
2510
|
+
ctx.logger.info("[AI] Navigation items contributed to Setup App");
|
|
2511
|
+
}
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
1496
2514
|
ctx.logger.info("[AI] Service initialized");
|
|
1497
2515
|
}
|
|
1498
2516
|
async start(ctx) {
|
|
1499
2517
|
if (!this.service) return;
|
|
2518
|
+
let metadataService;
|
|
2519
|
+
try {
|
|
2520
|
+
metadataService = ctx.getService("metadata");
|
|
2521
|
+
} catch {
|
|
2522
|
+
ctx.logger.debug("[AI] Metadata service not available");
|
|
2523
|
+
}
|
|
1500
2524
|
try {
|
|
1501
2525
|
const dataEngine = ctx.getService("data");
|
|
1502
|
-
const metadataService = ctx.getService("metadata");
|
|
1503
2526
|
if (dataEngine && metadataService) {
|
|
1504
2527
|
registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
|
|
1505
2528
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
@@ -1512,14 +2535,29 @@ var AIServicePlugin = class {
|
|
|
1512
2535
|
}
|
|
1513
2536
|
}
|
|
1514
2537
|
} catch {
|
|
1515
|
-
ctx.logger.debug("[AI] Data engine
|
|
2538
|
+
ctx.logger.debug("[AI] Data engine not available, skipping data tools");
|
|
2539
|
+
}
|
|
2540
|
+
if (metadataService) {
|
|
2541
|
+
try {
|
|
2542
|
+
registerMetadataTools(this.service.toolRegistry, { metadataService });
|
|
2543
|
+
ctx.logger.info("[AI] Built-in metadata tools registered");
|
|
2544
|
+
const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
|
|
2545
|
+
if (!agentExists) {
|
|
2546
|
+
await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
|
|
2547
|
+
ctx.logger.info("[AI] metadata_assistant agent registered");
|
|
2548
|
+
} else {
|
|
2549
|
+
ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
|
|
2550
|
+
}
|
|
2551
|
+
} catch (err) {
|
|
2552
|
+
ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
|
|
2553
|
+
}
|
|
1516
2554
|
}
|
|
1517
2555
|
await ctx.trigger("ai:ready", this.service);
|
|
1518
2556
|
const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
|
|
1519
2557
|
try {
|
|
1520
|
-
const
|
|
1521
|
-
if (
|
|
1522
|
-
const agentRuntime = new AgentRuntime(
|
|
2558
|
+
const metadataService2 = ctx.getService("metadata");
|
|
2559
|
+
if (metadataService2) {
|
|
2560
|
+
const agentRuntime = new AgentRuntime(metadataService2);
|
|
1523
2561
|
const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
|
|
1524
2562
|
routes.push(...agentRoutes);
|
|
1525
2563
|
}
|
|
@@ -1527,6 +2565,10 @@ var AIServicePlugin = class {
|
|
|
1527
2565
|
ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
|
|
1528
2566
|
}
|
|
1529
2567
|
await ctx.trigger("ai:routes", routes);
|
|
2568
|
+
const kernel = ctx.getKernel();
|
|
2569
|
+
if (kernel) {
|
|
2570
|
+
kernel.__aiRoutes = routes;
|
|
2571
|
+
}
|
|
1530
2572
|
ctx.logger.info(
|
|
1531
2573
|
`[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
|
|
1532
2574
|
);
|
|
@@ -1545,11 +2587,23 @@ var AIServicePlugin = class {
|
|
|
1545
2587
|
DATA_CHAT_AGENT,
|
|
1546
2588
|
DATA_TOOL_DEFINITIONS,
|
|
1547
2589
|
InMemoryConversationService,
|
|
2590
|
+
METADATA_ASSISTANT_AGENT,
|
|
2591
|
+
METADATA_TOOL_DEFINITIONS,
|
|
1548
2592
|
MemoryLLMAdapter,
|
|
1549
2593
|
ObjectQLConversationService,
|
|
1550
2594
|
ToolRegistry,
|
|
2595
|
+
VercelLLMAdapter,
|
|
2596
|
+
addFieldTool,
|
|
1551
2597
|
buildAIRoutes,
|
|
1552
2598
|
buildAgentRoutes,
|
|
1553
|
-
|
|
2599
|
+
createObjectTool,
|
|
2600
|
+
deleteFieldTool,
|
|
2601
|
+
describeMetadataObjectTool,
|
|
2602
|
+
encodeStreamPart,
|
|
2603
|
+
encodeVercelDataStream,
|
|
2604
|
+
listMetadataObjectsTool,
|
|
2605
|
+
modifyFieldTool,
|
|
2606
|
+
registerDataTools,
|
|
2607
|
+
registerMetadataTools
|
|
1554
2608
|
});
|
|
1555
2609
|
//# sourceMappingURL=index.cjs.map
|