@objectstack/service-ai 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +20 -0
- package/dist/index.cjs +1245 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -77
- package/dist/index.d.ts +344 -77
- package/dist/index.js +1230 -51
- package/dist/index.js.map +1 -1
- package/package.json +26 -4
- package/src/__tests__/ai-service.test.ts +248 -27
- package/src/__tests__/auth-and-toolcalling.test.ts +627 -0
- package/src/__tests__/chatbot-features.test.ts +229 -82
- package/src/__tests__/metadata-tools.test.ts +964 -0
- package/src/__tests__/objectql-conversation-service.test.ts +34 -16
- package/src/__tests__/vercel-stream-encoder.test.ts +263 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/memory-adapter.ts +17 -9
- package/src/adapters/vercel-adapter.ts +148 -0
- package/src/agent-runtime.ts +27 -3
- package/src/agents/index.ts +1 -0
- package/src/agents/metadata-assistant-agent.ts +87 -0
- package/src/ai-service.ts +174 -22
- package/src/conversation/in-memory-conversation-service.ts +2 -2
- package/src/conversation/objectql-conversation-service.ts +67 -18
- package/src/index.ts +22 -3
- package/src/plugin.ts +166 -9
- package/src/routes/agent-routes.ts +28 -3
- package/src/routes/ai-routes.ts +231 -14
- package/src/routes/index.ts +1 -1
- package/src/stream/index.ts +3 -0
- package/src/stream/vercel-stream-encoder.ts +129 -0
- package/src/tools/add-field.tool.ts +70 -0
- package/src/tools/create-object.tool.ts +66 -0
- package/src/tools/delete-field.tool.ts +38 -0
- package/src/tools/describe-metadata-object.tool.ts +32 -0
- package/src/tools/index.ts +12 -1
- package/src/tools/list-metadata-objects.tool.ts +34 -0
- package/src/tools/metadata-tools.ts +430 -0
- package/src/tools/modify-field.tool.ts +44 -0
- package/src/tools/tool-registry.ts +32 -9
package/dist/index.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
|
*
|
|
@@ -288,7 +335,7 @@ var _AIService = class _AIService {
|
|
|
288
335
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
289
336
|
*/
|
|
290
337
|
async chatWithTools(messages, options) {
|
|
291
|
-
const { maxIterations: maxIter, ...restOptions } = options ?? {};
|
|
338
|
+
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
292
339
|
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
293
340
|
const registeredTools = this.toolRegistry.getAll();
|
|
294
341
|
const mergedTools = [
|
|
@@ -301,11 +348,13 @@ var _AIService = class _AIService {
|
|
|
301
348
|
toolChoice: mergedTools.length > 0 ? restOptions.toolChoice ?? "auto" : void 0
|
|
302
349
|
};
|
|
303
350
|
const conversation = [...messages];
|
|
351
|
+
const toolErrors = [];
|
|
304
352
|
this.logger.debug("[AI] chatWithTools start", {
|
|
305
353
|
messageCount: conversation.length,
|
|
306
354
|
toolCount: mergedTools.length,
|
|
307
355
|
maxIterations
|
|
308
356
|
});
|
|
357
|
+
let abortedByCallback = false;
|
|
309
358
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
310
359
|
const result = await this.adapter.chat(conversation, chatOptions);
|
|
311
360
|
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
@@ -314,23 +363,47 @@ var _AIService = class _AIService {
|
|
|
314
363
|
}
|
|
315
364
|
this.logger.debug("[AI] chatWithTools tool calls", {
|
|
316
365
|
iteration,
|
|
317
|
-
calls: result.toolCalls.map((tc) => tc.
|
|
366
|
+
calls: result.toolCalls.map((tc) => tc.toolName)
|
|
318
367
|
});
|
|
368
|
+
const assistantContent = [];
|
|
369
|
+
if (result.content) assistantContent.push({ type: "text", text: result.content });
|
|
370
|
+
assistantContent.push(...result.toolCalls);
|
|
319
371
|
conversation.push({
|
|
320
372
|
role: "assistant",
|
|
321
|
-
content:
|
|
322
|
-
toolCalls: result.toolCalls
|
|
373
|
+
content: assistantContent
|
|
323
374
|
});
|
|
324
375
|
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
325
376
|
for (const tr of toolResults) {
|
|
377
|
+
if (tr.isError) {
|
|
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 };
|
|
382
|
+
toolErrors.push(errorEntry);
|
|
383
|
+
this.logger.warn("[AI] chatWithTools tool error", errorEntry);
|
|
384
|
+
if (onToolError && matchedCall) {
|
|
385
|
+
const action = onToolError(matchedCall, errorText);
|
|
386
|
+
if (action === "abort") {
|
|
387
|
+
abortedByCallback = true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
326
391
|
conversation.push({
|
|
327
392
|
role: "tool",
|
|
328
|
-
content: tr
|
|
329
|
-
toolCallId: tr.toolCallId
|
|
393
|
+
content: [tr]
|
|
330
394
|
});
|
|
331
395
|
}
|
|
396
|
+
if (abortedByCallback) {
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (abortedByCallback) {
|
|
401
|
+
this.logger.warn("[AI] chatWithTools aborted by onToolError callback", { toolErrors });
|
|
402
|
+
} else {
|
|
403
|
+
this.logger.warn("[AI] chatWithTools max iterations reached, forcing final response", {
|
|
404
|
+
toolErrors: toolErrors.length > 0 ? toolErrors : void 0
|
|
405
|
+
});
|
|
332
406
|
}
|
|
333
|
-
this.logger.warn("[AI] chatWithTools max iterations reached, forcing final response");
|
|
334
407
|
const finalResult = await this.adapter.chat(conversation, {
|
|
335
408
|
...chatOptions,
|
|
336
409
|
tools: void 0,
|
|
@@ -338,14 +411,175 @@ var _AIService = class _AIService {
|
|
|
338
411
|
});
|
|
339
412
|
return finalResult;
|
|
340
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Stream chat with automatic tool call resolution.
|
|
416
|
+
*
|
|
417
|
+
* Works like {@link chatWithTools} but yields SSE events. When the model
|
|
418
|
+
* requests tool calls during streaming, they are executed and the results
|
|
419
|
+
* fed back until a final text stream is produced.
|
|
420
|
+
*/
|
|
421
|
+
async *streamChatWithTools(messages, options) {
|
|
422
|
+
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
423
|
+
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
424
|
+
const registeredTools = this.toolRegistry.getAll();
|
|
425
|
+
const mergedTools = [
|
|
426
|
+
...registeredTools,
|
|
427
|
+
...restOptions.tools ?? []
|
|
428
|
+
];
|
|
429
|
+
const chatOptions = {
|
|
430
|
+
...restOptions,
|
|
431
|
+
tools: mergedTools.length > 0 ? mergedTools : void 0,
|
|
432
|
+
toolChoice: mergedTools.length > 0 ? restOptions.toolChoice ?? "auto" : void 0
|
|
433
|
+
};
|
|
434
|
+
const conversation = [...messages];
|
|
435
|
+
let abortedByCallback = false;
|
|
436
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
437
|
+
const result2 = await this.adapter.chat(conversation, chatOptions);
|
|
438
|
+
if (!result2.toolCalls || result2.toolCalls.length === 0) {
|
|
439
|
+
yield textDeltaPart("stream", result2.content);
|
|
440
|
+
yield finishPart(result2);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
for (const tc of result2.toolCalls) {
|
|
444
|
+
yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
|
|
445
|
+
}
|
|
446
|
+
const assistantContent = [];
|
|
447
|
+
if (result2.content) assistantContent.push({ type: "text", text: result2.content });
|
|
448
|
+
assistantContent.push(...result2.toolCalls);
|
|
449
|
+
conversation.push({
|
|
450
|
+
role: "assistant",
|
|
451
|
+
content: assistantContent
|
|
452
|
+
});
|
|
453
|
+
const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
|
|
454
|
+
for (const tr of toolResults) {
|
|
455
|
+
if (tr.isError && onToolError) {
|
|
456
|
+
const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
|
|
457
|
+
if (matchedCall) {
|
|
458
|
+
const errorText = _AIService.extractOutputText(tr);
|
|
459
|
+
const action = onToolError(matchedCall, errorText);
|
|
460
|
+
if (action === "abort") {
|
|
461
|
+
abortedByCallback = true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
conversation.push({
|
|
466
|
+
role: "tool",
|
|
467
|
+
content: [tr]
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (abortedByCallback) {
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (abortedByCallback) {
|
|
475
|
+
this.logger.warn("[AI] streamChatWithTools aborted by onToolError callback");
|
|
476
|
+
} else {
|
|
477
|
+
this.logger.warn("[AI] streamChatWithTools max iterations reached");
|
|
478
|
+
}
|
|
479
|
+
const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
|
|
480
|
+
const result = await this.adapter.chat(conversation, finalOptions);
|
|
481
|
+
yield textDeltaPart("stream", result.content);
|
|
482
|
+
yield finishPart(result);
|
|
483
|
+
}
|
|
341
484
|
};
|
|
342
485
|
// ── Tool Call Loop ────────────────────────────────────────────
|
|
343
486
|
/** Default maximum iterations for the tool call loop. */
|
|
344
487
|
_AIService.DEFAULT_MAX_ITERATIONS = 10;
|
|
345
488
|
var AIService = _AIService;
|
|
346
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
|
+
|
|
347
565
|
// src/routes/ai-routes.ts
|
|
348
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
|
+
}
|
|
349
583
|
function validateMessage(raw) {
|
|
350
584
|
if (typeof raw !== "object" || raw === null) {
|
|
351
585
|
return "each message must be an object";
|
|
@@ -354,20 +588,56 @@ function validateMessage(raw) {
|
|
|
354
588
|
if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
|
|
355
589
|
return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
|
|
356
590
|
}
|
|
357
|
-
|
|
358
|
-
|
|
591
|
+
const content = msg.content;
|
|
592
|
+
if (Array.isArray(msg.parts)) {
|
|
593
|
+
return null;
|
|
359
594
|
}
|
|
360
|
-
|
|
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";
|
|
361
619
|
}
|
|
362
620
|
function buildAIRoutes(aiService, conversationService, logger) {
|
|
363
621
|
return [
|
|
364
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
|
+
//
|
|
365
632
|
{
|
|
366
633
|
method: "POST",
|
|
367
634
|
path: "/api/v1/ai/chat",
|
|
368
|
-
description: "
|
|
635
|
+
description: "Chat completion (supports Vercel AI Data Stream Protocol)",
|
|
636
|
+
auth: true,
|
|
637
|
+
permissions: ["ai:chat"],
|
|
369
638
|
handler: async (req) => {
|
|
370
|
-
const
|
|
639
|
+
const body = req.body ?? {};
|
|
640
|
+
const messages = body.messages;
|
|
371
641
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
372
642
|
return { status: 400, body: { error: "messages array is required" } };
|
|
373
643
|
}
|
|
@@ -375,8 +645,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
375
645
|
const err = validateMessage(msg);
|
|
376
646
|
if (err) return { status: 400, body: { error: err } };
|
|
377
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
|
+
}
|
|
378
689
|
try {
|
|
379
|
-
const result = await aiService.
|
|
690
|
+
const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
|
|
380
691
|
return { status: 200, body: result };
|
|
381
692
|
} catch (err) {
|
|
382
693
|
logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
|
|
@@ -389,6 +700,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
389
700
|
method: "POST",
|
|
390
701
|
path: "/api/v1/ai/chat/stream",
|
|
391
702
|
description: "SSE streaming chat completion",
|
|
703
|
+
auth: true,
|
|
704
|
+
permissions: ["ai:chat"],
|
|
392
705
|
handler: async (req) => {
|
|
393
706
|
const { messages, options } = req.body ?? {};
|
|
394
707
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
@@ -402,7 +715,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
402
715
|
if (!aiService.streamChat) {
|
|
403
716
|
return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
|
|
404
717
|
}
|
|
405
|
-
const events = aiService.streamChat(messages, options);
|
|
718
|
+
const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
|
|
406
719
|
return { status: 200, stream: true, events };
|
|
407
720
|
} catch (err) {
|
|
408
721
|
logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
|
|
@@ -415,6 +728,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
415
728
|
method: "POST",
|
|
416
729
|
path: "/api/v1/ai/complete",
|
|
417
730
|
description: "Text completion",
|
|
731
|
+
auth: true,
|
|
732
|
+
permissions: ["ai:complete"],
|
|
418
733
|
handler: async (req) => {
|
|
419
734
|
const { prompt, options } = req.body ?? {};
|
|
420
735
|
if (!prompt || typeof prompt !== "string") {
|
|
@@ -434,6 +749,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
434
749
|
method: "GET",
|
|
435
750
|
path: "/api/v1/ai/models",
|
|
436
751
|
description: "List available models",
|
|
752
|
+
auth: true,
|
|
753
|
+
permissions: ["ai:read"],
|
|
437
754
|
handler: async () => {
|
|
438
755
|
try {
|
|
439
756
|
const models = aiService.listModels ? await aiService.listModels() : [];
|
|
@@ -449,9 +766,17 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
449
766
|
method: "POST",
|
|
450
767
|
path: "/api/v1/ai/conversations",
|
|
451
768
|
description: "Create a conversation",
|
|
769
|
+
auth: true,
|
|
770
|
+
permissions: ["ai:conversations"],
|
|
452
771
|
handler: async (req) => {
|
|
453
772
|
try {
|
|
454
|
-
|
|
773
|
+
if (req.body !== void 0 && req.body !== null && (typeof req.body !== "object" || Array.isArray(req.body))) {
|
|
774
|
+
return { status: 400, body: { error: "Invalid request payload" } };
|
|
775
|
+
}
|
|
776
|
+
const options = { ...req.body ?? {} };
|
|
777
|
+
if (req.user?.userId) {
|
|
778
|
+
options.userId = req.user.userId;
|
|
779
|
+
}
|
|
455
780
|
const conversation = await conversationService.create(options);
|
|
456
781
|
return { status: 201, body: conversation };
|
|
457
782
|
} catch (err) {
|
|
@@ -464,6 +789,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
464
789
|
method: "GET",
|
|
465
790
|
path: "/api/v1/ai/conversations",
|
|
466
791
|
description: "List conversations",
|
|
792
|
+
auth: true,
|
|
793
|
+
permissions: ["ai:conversations"],
|
|
467
794
|
handler: async (req) => {
|
|
468
795
|
try {
|
|
469
796
|
const rawQuery = req.query ?? {};
|
|
@@ -475,6 +802,9 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
475
802
|
}
|
|
476
803
|
options.limit = parsedLimit;
|
|
477
804
|
}
|
|
805
|
+
if (req.user?.userId) {
|
|
806
|
+
options.userId = req.user.userId;
|
|
807
|
+
}
|
|
478
808
|
const conversations = await conversationService.list(options);
|
|
479
809
|
return { status: 200, body: { conversations } };
|
|
480
810
|
} catch (err) {
|
|
@@ -487,6 +817,8 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
487
817
|
method: "POST",
|
|
488
818
|
path: "/api/v1/ai/conversations/:id/messages",
|
|
489
819
|
description: "Add message to a conversation",
|
|
820
|
+
auth: true,
|
|
821
|
+
permissions: ["ai:conversations"],
|
|
490
822
|
handler: async (req) => {
|
|
491
823
|
const id = req.params?.id;
|
|
492
824
|
if (!id) {
|
|
@@ -498,6 +830,15 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
498
830
|
return { status: 400, body: { error: validationError } };
|
|
499
831
|
}
|
|
500
832
|
try {
|
|
833
|
+
if (req.user?.userId) {
|
|
834
|
+
const existing = await conversationService.get(id);
|
|
835
|
+
if (!existing) {
|
|
836
|
+
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
837
|
+
}
|
|
838
|
+
if (existing.userId && existing.userId !== req.user.userId) {
|
|
839
|
+
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
501
842
|
const conversation = await conversationService.addMessage(id, message);
|
|
502
843
|
return { status: 200, body: conversation };
|
|
503
844
|
} catch (err) {
|
|
@@ -514,12 +855,23 @@ function buildAIRoutes(aiService, conversationService, logger) {
|
|
|
514
855
|
method: "DELETE",
|
|
515
856
|
path: "/api/v1/ai/conversations/:id",
|
|
516
857
|
description: "Delete a conversation",
|
|
858
|
+
auth: true,
|
|
859
|
+
permissions: ["ai:conversations"],
|
|
517
860
|
handler: async (req) => {
|
|
518
861
|
const id = req.params?.id;
|
|
519
862
|
if (!id) {
|
|
520
863
|
return { status: 400, body: { error: "conversation id is required" } };
|
|
521
864
|
}
|
|
522
865
|
try {
|
|
866
|
+
if (req.user?.userId) {
|
|
867
|
+
const existing = await conversationService.get(id);
|
|
868
|
+
if (!existing) {
|
|
869
|
+
return { status: 404, body: { error: `Conversation "${id}" not found` } };
|
|
870
|
+
}
|
|
871
|
+
if (existing.userId && existing.userId !== req.user.userId) {
|
|
872
|
+
return { status: 403, body: { error: "You do not have access to this conversation" } };
|
|
873
|
+
}
|
|
874
|
+
}
|
|
523
875
|
await conversationService.delete(id);
|
|
524
876
|
return { status: 204 };
|
|
525
877
|
} catch (err) {
|
|
@@ -548,10 +900,33 @@ function validateAgentMessage(raw) {
|
|
|
548
900
|
}
|
|
549
901
|
function buildAgentRoutes(aiService, agentRuntime, logger) {
|
|
550
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 ──────────────────────────────
|
|
551
924
|
{
|
|
552
925
|
method: "POST",
|
|
553
926
|
path: "/api/v1/ai/agents/:agentName/chat",
|
|
554
927
|
description: "Chat with a specific AI agent",
|
|
928
|
+
auth: true,
|
|
929
|
+
permissions: ["ai:chat", "ai:agents"],
|
|
555
930
|
handler: async (req) => {
|
|
556
931
|
const agentName = req.params?.agentName;
|
|
557
932
|
if (!agentName) {
|
|
@@ -705,13 +1080,35 @@ var ObjectQLConversationService = class {
|
|
|
705
1080
|
}
|
|
706
1081
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
707
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
|
+
}
|
|
708
1105
|
await this.engine.insert(MESSAGES_OBJECT, {
|
|
709
1106
|
id: msgId,
|
|
710
1107
|
conversation_id: conversationId,
|
|
711
1108
|
role: message.role,
|
|
712
|
-
content:
|
|
713
|
-
tool_calls:
|
|
714
|
-
tool_call_id:
|
|
1109
|
+
content: contentStr,
|
|
1110
|
+
tool_calls: toolCallsJson,
|
|
1111
|
+
tool_call_id: toolCallId,
|
|
715
1112
|
created_at: now
|
|
716
1113
|
});
|
|
717
1114
|
await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
|
|
@@ -756,21 +1153,42 @@ var ObjectQLConversationService = class {
|
|
|
756
1153
|
};
|
|
757
1154
|
}
|
|
758
1155
|
/**
|
|
759
|
-
* Map a database row to
|
|
1156
|
+
* Map a database row to a ModelMessage.
|
|
760
1157
|
*/
|
|
761
1158
|
toMessage(row) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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 };
|
|
772
1191
|
}
|
|
773
|
-
return msg;
|
|
774
1192
|
}
|
|
775
1193
|
};
|
|
776
1194
|
|
|
@@ -1167,13 +1585,507 @@ function registerDataTools(registry, context) {
|
|
|
1167
1585
|
registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
|
|
1168
1586
|
}
|
|
1169
1587
|
|
|
1170
|
-
// src/
|
|
1588
|
+
// src/tools/create-object.tool.ts
|
|
1171
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");
|
|
1172
2063
|
var AgentRuntime = class {
|
|
1173
2064
|
constructor(metadataService) {
|
|
1174
2065
|
this.metadataService = metadataService;
|
|
1175
2066
|
}
|
|
1176
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
|
+
}
|
|
1177
2089
|
/**
|
|
1178
2090
|
* Load and validate an agent definition by name.
|
|
1179
2091
|
*
|
|
@@ -1185,7 +2097,7 @@ var AgentRuntime = class {
|
|
|
1185
2097
|
async loadAgent(agentName) {
|
|
1186
2098
|
const raw = await this.metadataService.get("agent", agentName);
|
|
1187
2099
|
if (!raw) return void 0;
|
|
1188
|
-
const result =
|
|
2100
|
+
const result = import_ai7.AgentSchema.safeParse(raw);
|
|
1189
2101
|
if (!result.success) {
|
|
1190
2102
|
return void 0;
|
|
1191
2103
|
}
|
|
@@ -1302,15 +2214,233 @@ Guidelines:
|
|
|
1302
2214
|
}
|
|
1303
2215
|
};
|
|
1304
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
|
+
|
|
1305
2358
|
// src/plugin.ts
|
|
1306
2359
|
var AIServicePlugin = class {
|
|
1307
2360
|
constructor(options = {}) {
|
|
1308
2361
|
this.name = "com.objectstack.service-ai";
|
|
1309
2362
|
this.version = "1.0.0";
|
|
1310
2363
|
this.type = "standard";
|
|
1311
|
-
this.dependencies = [];
|
|
2364
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
1312
2365
|
this.options = options;
|
|
1313
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
|
+
}
|
|
1314
2444
|
async init(ctx) {
|
|
1315
2445
|
let hasExisting = false;
|
|
1316
2446
|
try {
|
|
@@ -1332,8 +2462,19 @@ var AIServicePlugin = class {
|
|
|
1332
2462
|
} catch {
|
|
1333
2463
|
}
|
|
1334
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}`);
|
|
1335
2476
|
const config = {
|
|
1336
|
-
adapter
|
|
2477
|
+
adapter,
|
|
1337
2478
|
logger: ctx.logger,
|
|
1338
2479
|
conversationService
|
|
1339
2480
|
};
|
|
@@ -1343,7 +2484,7 @@ var AIServicePlugin = class {
|
|
|
1343
2484
|
} else {
|
|
1344
2485
|
ctx.registerService("ai", this.service);
|
|
1345
2486
|
}
|
|
1346
|
-
ctx.
|
|
2487
|
+
ctx.getService("manifest").register({
|
|
1347
2488
|
id: "com.objectstack.service-ai",
|
|
1348
2489
|
name: "AI Service",
|
|
1349
2490
|
version: "1.0.0",
|
|
@@ -1356,13 +2497,32 @@ var AIServicePlugin = class {
|
|
|
1356
2497
|
ctx.logger.debug("[AI] Before chat", { messages });
|
|
1357
2498
|
});
|
|
1358
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
|
+
}
|
|
1359
2514
|
ctx.logger.info("[AI] Service initialized");
|
|
1360
2515
|
}
|
|
1361
2516
|
async start(ctx) {
|
|
1362
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
|
+
}
|
|
1363
2524
|
try {
|
|
1364
2525
|
const dataEngine = ctx.getService("data");
|
|
1365
|
-
const metadataService = ctx.getService("metadata");
|
|
1366
2526
|
if (dataEngine && metadataService) {
|
|
1367
2527
|
registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
|
|
1368
2528
|
ctx.logger.info("[AI] Built-in data tools registered");
|
|
@@ -1375,14 +2535,29 @@ var AIServicePlugin = class {
|
|
|
1375
2535
|
}
|
|
1376
2536
|
}
|
|
1377
2537
|
} catch {
|
|
1378
|
-
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
|
+
}
|
|
1379
2554
|
}
|
|
1380
2555
|
await ctx.trigger("ai:ready", this.service);
|
|
1381
2556
|
const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
|
|
1382
2557
|
try {
|
|
1383
|
-
const
|
|
1384
|
-
if (
|
|
1385
|
-
const agentRuntime = new AgentRuntime(
|
|
2558
|
+
const metadataService2 = ctx.getService("metadata");
|
|
2559
|
+
if (metadataService2) {
|
|
2560
|
+
const agentRuntime = new AgentRuntime(metadataService2);
|
|
1386
2561
|
const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
|
|
1387
2562
|
routes.push(...agentRoutes);
|
|
1388
2563
|
}
|
|
@@ -1390,6 +2565,10 @@ var AIServicePlugin = class {
|
|
|
1390
2565
|
ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
|
|
1391
2566
|
}
|
|
1392
2567
|
await ctx.trigger("ai:routes", routes);
|
|
2568
|
+
const kernel = ctx.getKernel();
|
|
2569
|
+
if (kernel) {
|
|
2570
|
+
kernel.__aiRoutes = routes;
|
|
2571
|
+
}
|
|
1393
2572
|
ctx.logger.info(
|
|
1394
2573
|
`[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
|
|
1395
2574
|
);
|
|
@@ -1408,11 +2587,23 @@ var AIServicePlugin = class {
|
|
|
1408
2587
|
DATA_CHAT_AGENT,
|
|
1409
2588
|
DATA_TOOL_DEFINITIONS,
|
|
1410
2589
|
InMemoryConversationService,
|
|
2590
|
+
METADATA_ASSISTANT_AGENT,
|
|
2591
|
+
METADATA_TOOL_DEFINITIONS,
|
|
1411
2592
|
MemoryLLMAdapter,
|
|
1412
2593
|
ObjectQLConversationService,
|
|
1413
2594
|
ToolRegistry,
|
|
2595
|
+
VercelLLMAdapter,
|
|
2596
|
+
addFieldTool,
|
|
1414
2597
|
buildAIRoutes,
|
|
1415
2598
|
buildAgentRoutes,
|
|
1416
|
-
|
|
2599
|
+
createObjectTool,
|
|
2600
|
+
deleteFieldTool,
|
|
2601
|
+
describeMetadataObjectTool,
|
|
2602
|
+
encodeStreamPart,
|
|
2603
|
+
encodeVercelDataStream,
|
|
2604
|
+
listMetadataObjectsTool,
|
|
2605
|
+
modifyFieldTool,
|
|
2606
|
+
registerDataTools,
|
|
2607
|
+
registerMetadataTools
|
|
1417
2608
|
});
|
|
1418
2609
|
//# sourceMappingURL=index.cjs.map
|