@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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { Agent } from '@objectstack/spec';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in `metadata_assistant` agent definition.
|
|
7
|
+
*
|
|
8
|
+
* This agent powers AI-driven metadata management — users can create objects,
|
|
9
|
+
* add/modify/delete fields, and inspect schema definitions through natural
|
|
10
|
+
* language conversation.
|
|
11
|
+
*
|
|
12
|
+
* It is registered automatically by the AI service plugin alongside the
|
|
13
|
+
* `data_chat` agent when the metadata service is available.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```
|
|
17
|
+
* POST /api/v1/ai/agents/metadata_assistant/chat
|
|
18
|
+
* {
|
|
19
|
+
* "messages": [{ "role": "user", "content": "Create a contracts table with name, value, and status fields" }],
|
|
20
|
+
* "context": {}
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const METADATA_ASSISTANT_AGENT: Agent = {
|
|
25
|
+
name: 'metadata_assistant',
|
|
26
|
+
label: 'Metadata Assistant',
|
|
27
|
+
role: 'Schema Architect',
|
|
28
|
+
instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
|
|
29
|
+
|
|
30
|
+
Capabilities:
|
|
31
|
+
- Create new data objects (tables) with fields
|
|
32
|
+
- Add fields (columns) to existing objects
|
|
33
|
+
- Modify field properties (label, type, required, default value)
|
|
34
|
+
- Delete fields from objects
|
|
35
|
+
- List all registered metadata objects and their schemas
|
|
36
|
+
- Describe the full schema of a specific object
|
|
37
|
+
|
|
38
|
+
Guidelines:
|
|
39
|
+
1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
|
|
40
|
+
2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
|
|
41
|
+
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
|
|
42
|
+
4. Suggest meaningful field types based on the user's description (e.g. "deadline" → date, "active" → boolean).
|
|
43
|
+
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
|
|
44
|
+
6. Explain what changes you are about to make before executing them.
|
|
45
|
+
7. After making changes, confirm the result by describing the updated schema.
|
|
46
|
+
8. For destructive operations (deleting fields), always warn the user about potential data loss.
|
|
47
|
+
9. Always answer in the same language the user is using.
|
|
48
|
+
10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
|
|
49
|
+
|
|
50
|
+
model: {
|
|
51
|
+
provider: 'openai',
|
|
52
|
+
model: 'gpt-4',
|
|
53
|
+
temperature: 0.2,
|
|
54
|
+
maxTokens: 4096,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
tools: [
|
|
58
|
+
{ type: 'action', name: 'create_object', description: 'Create a new data object (table)' },
|
|
59
|
+
{ type: 'action', name: 'add_field', description: 'Add a field to an existing object' },
|
|
60
|
+
{ type: 'action', name: 'modify_field', description: 'Modify an existing field definition' },
|
|
61
|
+
{ type: 'action', name: 'delete_field', description: 'Delete a field from an object' },
|
|
62
|
+
{ type: 'query', name: 'list_metadata_objects', description: 'List all metadata objects' },
|
|
63
|
+
{ type: 'query', name: 'describe_metadata_object', description: 'Describe an object schema' },
|
|
64
|
+
],
|
|
65
|
+
|
|
66
|
+
active: true,
|
|
67
|
+
visibility: 'global',
|
|
68
|
+
|
|
69
|
+
guardrails: {
|
|
70
|
+
maxTokensPerInvocation: 8192,
|
|
71
|
+
maxExecutionTimeSec: 60,
|
|
72
|
+
blockedTopics: ['drop_database', 'raw_sql', 'system_tables'],
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
planning: {
|
|
76
|
+
strategy: 'react',
|
|
77
|
+
maxIterations: 10,
|
|
78
|
+
allowReplan: true,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
memory: {
|
|
82
|
+
shortTerm: {
|
|
83
|
+
maxMessages: 30,
|
|
84
|
+
maxTokens: 8192,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
package/src/ai-service.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
ModelMessage,
|
|
5
|
+
ToolCallPart,
|
|
6
|
+
TextStreamPart,
|
|
7
|
+
ToolSet,
|
|
5
8
|
AIRequestOptions,
|
|
6
9
|
AIResult,
|
|
7
|
-
AIStreamEvent,
|
|
8
10
|
IAIService,
|
|
9
11
|
IAIConversationService,
|
|
10
12
|
ChatWithToolsOptions,
|
|
@@ -14,8 +16,28 @@ import type { Logger } from '@objectstack/spec/contracts';
|
|
|
14
16
|
import { createLogger } from '@objectstack/core';
|
|
15
17
|
import { MemoryLLMAdapter } from './adapters/memory-adapter.js';
|
|
16
18
|
import { ToolRegistry } from './tools/tool-registry.js';
|
|
19
|
+
import type { ToolExecutionResult } from './tools/tool-registry.js';
|
|
17
20
|
import { InMemoryConversationService } from './conversation/in-memory-conversation-service.js';
|
|
18
21
|
|
|
22
|
+
// ── Stream event helpers ──────────────────────────────────────────
|
|
23
|
+
// These helpers construct properly-typed Vercel AI SDK stream parts
|
|
24
|
+
// to avoid repeated `as unknown as TextStreamPart<ToolSet>` casts.
|
|
25
|
+
|
|
26
|
+
/** Create a text-delta stream part. */
|
|
27
|
+
function textDeltaPart(id: string, text: string): TextStreamPart<ToolSet> {
|
|
28
|
+
return { type: 'text-delta', id, text } as TextStreamPart<ToolSet>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Create a finish stream part from an AIResult. */
|
|
32
|
+
function finishPart(result?: AIResult): TextStreamPart<ToolSet> {
|
|
33
|
+
return {
|
|
34
|
+
type: 'finish',
|
|
35
|
+
finishReason: 'stop',
|
|
36
|
+
totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
37
|
+
rawFinishReason: 'stop',
|
|
38
|
+
} as unknown as TextStreamPart<ToolSet>;
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
/**
|
|
20
42
|
* Configuration for AIService.
|
|
21
43
|
*/
|
|
@@ -70,7 +92,7 @@ export class AIService implements IAIService {
|
|
|
70
92
|
|
|
71
93
|
// ── IAIService implementation ──────────────────────────────────
|
|
72
94
|
|
|
73
|
-
async chat(messages:
|
|
95
|
+
async chat(messages: ModelMessage[], options?: AIRequestOptions): Promise<AIResult> {
|
|
74
96
|
this.logger.debug('[AI] chat', { messageCount: messages.length, model: options?.model });
|
|
75
97
|
return this.adapter.chat(messages, options);
|
|
76
98
|
}
|
|
@@ -81,16 +103,16 @@ export class AIService implements IAIService {
|
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
async *streamChat(
|
|
84
|
-
messages:
|
|
106
|
+
messages: ModelMessage[],
|
|
85
107
|
options?: AIRequestOptions,
|
|
86
|
-
): AsyncIterable<
|
|
108
|
+
): AsyncIterable<TextStreamPart<ToolSet>> {
|
|
87
109
|
this.logger.debug('[AI] streamChat', { messageCount: messages.length, model: options?.model });
|
|
88
110
|
|
|
89
111
|
if (!this.adapter.streamChat) {
|
|
90
112
|
// Fallback: emit the entire response as a single text-delta + finish
|
|
91
113
|
const result = await this.adapter.chat(messages, options);
|
|
92
|
-
yield
|
|
93
|
-
yield
|
|
114
|
+
yield textDeltaPart('fallback', result.content);
|
|
115
|
+
yield finishPart(result);
|
|
94
116
|
return;
|
|
95
117
|
}
|
|
96
118
|
|
|
@@ -116,6 +138,12 @@ export class AIService implements IAIService {
|
|
|
116
138
|
/** Default maximum iterations for the tool call loop. */
|
|
117
139
|
static readonly DEFAULT_MAX_ITERATIONS = 10;
|
|
118
140
|
|
|
141
|
+
/** Extract the text value from a ToolExecutionResult's output. */
|
|
142
|
+
private static extractOutputText(tr: ToolExecutionResult): string {
|
|
143
|
+
return tr.output && typeof tr.output === 'object' && 'value' in tr.output
|
|
144
|
+
? String(tr.output.value) : 'unknown error';
|
|
145
|
+
}
|
|
146
|
+
|
|
119
147
|
/**
|
|
120
148
|
* Chat with automatic tool call resolution.
|
|
121
149
|
*
|
|
@@ -128,11 +156,11 @@ export class AIService implements IAIService {
|
|
|
128
156
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
129
157
|
*/
|
|
130
158
|
async chatWithTools(
|
|
131
|
-
messages:
|
|
159
|
+
messages: ModelMessage[],
|
|
132
160
|
options?: ChatWithToolsOptions,
|
|
133
161
|
): Promise<AIResult> {
|
|
134
|
-
// Destructure
|
|
135
|
-
const { maxIterations: maxIter, ...restOptions } = options ?? {};
|
|
162
|
+
// Destructure loop-specific options so they are never forwarded to the adapter
|
|
163
|
+
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
136
164
|
const maxIterations = maxIter ?? AIService.DEFAULT_MAX_ITERATIONS;
|
|
137
165
|
const registeredTools = this.toolRegistry.getAll();
|
|
138
166
|
|
|
@@ -152,12 +180,17 @@ export class AIService implements IAIService {
|
|
|
152
180
|
// Working copy of the conversation
|
|
153
181
|
const conversation = [...messages];
|
|
154
182
|
|
|
183
|
+
// Track errors across iterations for diagnostics
|
|
184
|
+
const toolErrors: Array<{ iteration: number; toolName: string; error: string }> = [];
|
|
185
|
+
|
|
155
186
|
this.logger.debug('[AI] chatWithTools start', {
|
|
156
187
|
messageCount: conversation.length,
|
|
157
188
|
toolCount: mergedTools.length,
|
|
158
189
|
maxIterations,
|
|
159
190
|
});
|
|
160
191
|
|
|
192
|
+
let abortedByCallback = false;
|
|
193
|
+
|
|
161
194
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
162
195
|
const result = await this.adapter.chat(conversation, chatOptions);
|
|
163
196
|
|
|
@@ -169,32 +202,62 @@ export class AIService implements IAIService {
|
|
|
169
202
|
|
|
170
203
|
this.logger.debug('[AI] chatWithTools tool calls', {
|
|
171
204
|
iteration,
|
|
172
|
-
calls: result.toolCalls.map(tc => tc.
|
|
205
|
+
calls: result.toolCalls.map(tc => tc.toolName),
|
|
173
206
|
});
|
|
174
207
|
|
|
175
208
|
// Append the assistant's response (with tool call metadata) to the conversation
|
|
209
|
+
const assistantContent: Array<{ type: 'text'; text: string } | ToolCallPart> = [];
|
|
210
|
+
if (result.content) assistantContent.push({ type: 'text', text: result.content });
|
|
211
|
+
assistantContent.push(...result.toolCalls);
|
|
176
212
|
conversation.push({
|
|
177
213
|
role: 'assistant',
|
|
178
|
-
content:
|
|
179
|
-
|
|
180
|
-
});
|
|
214
|
+
content: assistantContent,
|
|
215
|
+
} as ModelMessage);
|
|
181
216
|
|
|
182
217
|
// Execute all tool calls in parallel
|
|
183
|
-
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
218
|
+
const toolResults: ToolExecutionResult[] = await this.toolRegistry.executeAll(result.toolCalls);
|
|
184
219
|
|
|
185
|
-
//
|
|
220
|
+
// Process results: track errors and honour onToolError callback
|
|
186
221
|
for (const tr of toolResults) {
|
|
222
|
+
if (tr.isError) {
|
|
223
|
+
// Match tool call by toolCallId for robust attribution
|
|
224
|
+
const matchedCall = result.toolCalls!.find(tc => tc.toolCallId === tr.toolCallId);
|
|
225
|
+
const toolName = matchedCall?.toolName ?? 'unknown';
|
|
226
|
+
const errorText = AIService.extractOutputText(tr);
|
|
227
|
+
const errorEntry = { iteration, toolName, error: errorText };
|
|
228
|
+
toolErrors.push(errorEntry);
|
|
229
|
+
this.logger.warn('[AI] chatWithTools tool error', errorEntry);
|
|
230
|
+
|
|
231
|
+
if (onToolError && matchedCall) {
|
|
232
|
+
const action = onToolError(matchedCall, errorText);
|
|
233
|
+
if (action === 'abort') {
|
|
234
|
+
abortedByCallback = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Append each tool result as a `role: 'tool'` message
|
|
187
240
|
conversation.push({
|
|
188
241
|
role: 'tool',
|
|
189
|
-
content: tr
|
|
190
|
-
|
|
191
|
-
});
|
|
242
|
+
content: [tr],
|
|
243
|
+
} as ModelMessage);
|
|
192
244
|
}
|
|
245
|
+
|
|
246
|
+
if (abortedByCallback) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Distinguish user-driven abort from max-iterations exhaustion in logs
|
|
252
|
+
if (abortedByCallback) {
|
|
253
|
+
this.logger.warn('[AI] chatWithTools aborted by onToolError callback', { toolErrors });
|
|
254
|
+
} else {
|
|
255
|
+
this.logger.warn('[AI] chatWithTools max iterations reached, forcing final response', {
|
|
256
|
+
toolErrors: toolErrors.length > 0 ? toolErrors : undefined,
|
|
257
|
+
});
|
|
193
258
|
}
|
|
194
259
|
|
|
195
|
-
//
|
|
196
|
-
// call *without* tools so the model is forced to produce text.
|
|
197
|
-
this.logger.warn('[AI] chatWithTools max iterations reached, forcing final response');
|
|
260
|
+
// Make one last call *without* tools so the model is forced to produce text.
|
|
198
261
|
const finalResult = await this.adapter.chat(conversation, {
|
|
199
262
|
...chatOptions,
|
|
200
263
|
tools: undefined,
|
|
@@ -202,4 +265,93 @@ export class AIService implements IAIService {
|
|
|
202
265
|
});
|
|
203
266
|
return finalResult;
|
|
204
267
|
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Stream chat with automatic tool call resolution.
|
|
271
|
+
*
|
|
272
|
+
* Works like {@link chatWithTools} but yields SSE events. When the model
|
|
273
|
+
* requests tool calls during streaming, they are executed and the results
|
|
274
|
+
* fed back until a final text stream is produced.
|
|
275
|
+
*/
|
|
276
|
+
async *streamChatWithTools(
|
|
277
|
+
messages: ModelMessage[],
|
|
278
|
+
options?: ChatWithToolsOptions,
|
|
279
|
+
): AsyncIterable<TextStreamPart<ToolSet>> {
|
|
280
|
+
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
281
|
+
const maxIterations = maxIter ?? AIService.DEFAULT_MAX_ITERATIONS;
|
|
282
|
+
const registeredTools = this.toolRegistry.getAll();
|
|
283
|
+
|
|
284
|
+
const mergedTools = [
|
|
285
|
+
...registeredTools,
|
|
286
|
+
...(restOptions.tools ?? []),
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const chatOptions: AIRequestOptions = {
|
|
290
|
+
...restOptions,
|
|
291
|
+
tools: mergedTools.length > 0 ? mergedTools : undefined,
|
|
292
|
+
toolChoice: mergedTools.length > 0 ? (restOptions.toolChoice ?? 'auto') : undefined,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const conversation = [...messages];
|
|
296
|
+
let abortedByCallback = false;
|
|
297
|
+
|
|
298
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
299
|
+
// Use non-streaming chat for intermediate tool-call rounds
|
|
300
|
+
const result = await this.adapter.chat(conversation, chatOptions);
|
|
301
|
+
|
|
302
|
+
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
303
|
+
// Final round — return the probed result without an extra model call
|
|
304
|
+
yield textDeltaPart('stream', result.content);
|
|
305
|
+
yield finishPart(result);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Emit tool-call events so the client can see tool execution progress
|
|
310
|
+
for (const tc of result.toolCalls) {
|
|
311
|
+
yield { type: 'tool-call', toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input } as TextStreamPart<ToolSet>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const assistantContent: Array<{ type: 'text'; text: string } | ToolCallPart> = [];
|
|
315
|
+
if (result.content) assistantContent.push({ type: 'text', text: result.content });
|
|
316
|
+
assistantContent.push(...result.toolCalls);
|
|
317
|
+
conversation.push({
|
|
318
|
+
role: 'assistant',
|
|
319
|
+
content: assistantContent,
|
|
320
|
+
} as ModelMessage);
|
|
321
|
+
|
|
322
|
+
const toolResults: ToolExecutionResult[] = await this.toolRegistry.executeAll(result.toolCalls);
|
|
323
|
+
|
|
324
|
+
for (const tr of toolResults) {
|
|
325
|
+
if (tr.isError && onToolError) {
|
|
326
|
+
const matchedCall = result.toolCalls!.find(tc => tc.toolCallId === tr.toolCallId);
|
|
327
|
+
if (matchedCall) {
|
|
328
|
+
const errorText = AIService.extractOutputText(tr);
|
|
329
|
+
const action = onToolError(matchedCall, errorText);
|
|
330
|
+
if (action === 'abort') {
|
|
331
|
+
abortedByCallback = true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
conversation.push({
|
|
336
|
+
role: 'tool',
|
|
337
|
+
content: [tr],
|
|
338
|
+
} as ModelMessage);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (abortedByCallback) {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Forced final response (no tools) — either aborted or max iterations
|
|
347
|
+
if (abortedByCallback) {
|
|
348
|
+
this.logger.warn('[AI] streamChatWithTools aborted by onToolError callback');
|
|
349
|
+
} else {
|
|
350
|
+
this.logger.warn('[AI] streamChatWithTools max iterations reached');
|
|
351
|
+
}
|
|
352
|
+
const finalOptions = { ...chatOptions, tools: undefined, toolChoice: undefined };
|
|
353
|
+
const result = await this.adapter.chat(conversation, finalOptions);
|
|
354
|
+
yield textDeltaPart('stream', result.content);
|
|
355
|
+
yield finishPart(result);
|
|
356
|
+
}
|
|
205
357
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
AIConversation,
|
|
5
|
-
|
|
5
|
+
ModelMessage,
|
|
6
6
|
IAIConversationService,
|
|
7
7
|
} from '@objectstack/spec/contracts';
|
|
8
8
|
|
|
@@ -75,7 +75,7 @@ export class InMemoryConversationService implements IAIConversationService {
|
|
|
75
75
|
return results;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
async addMessage(conversationId: string, message:
|
|
78
|
+
async addMessage(conversationId: string, message: ModelMessage): Promise<AIConversation> {
|
|
79
79
|
const conversation = this.store.get(conversationId);
|
|
80
80
|
if (!conversation) {
|
|
81
81
|
throw new Error(`Conversation "${conversationId}" not found`);
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import type {
|
|
5
5
|
AIConversation,
|
|
6
|
-
|
|
6
|
+
ModelMessage,
|
|
7
|
+
ToolCallPart,
|
|
8
|
+
ToolResultPart,
|
|
7
9
|
IAIConversationService,
|
|
8
10
|
IDataEngine,
|
|
9
11
|
} from '@objectstack/spec/contracts';
|
|
@@ -157,7 +159,7 @@ export class ObjectQLConversationService implements IAIConversationService {
|
|
|
157
159
|
return conversations;
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
async addMessage(conversationId: string, message:
|
|
162
|
+
async addMessage(conversationId: string, message: ModelMessage): Promise<AIConversation> {
|
|
161
163
|
// Verify conversation exists
|
|
162
164
|
const row: DbConversationRow | null = await this.engine.findOne(CONVERSATIONS_OBJECT, {
|
|
163
165
|
where: { id: conversationId },
|
|
@@ -169,14 +171,39 @@ export class ObjectQLConversationService implements IAIConversationService {
|
|
|
169
171
|
const now = new Date().toISOString();
|
|
170
172
|
const msgId = `msg_${randomUUID()}`;
|
|
171
173
|
|
|
174
|
+
// Extract flat fields from the discriminated union
|
|
175
|
+
let contentStr: string;
|
|
176
|
+
let toolCallsJson: string | null = null;
|
|
177
|
+
let toolCallId: string | null = null;
|
|
178
|
+
|
|
179
|
+
if (message.role === 'system' || message.role === 'user') {
|
|
180
|
+
contentStr = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
|
|
181
|
+
} else if (message.role === 'assistant') {
|
|
182
|
+
if (typeof message.content === 'string') {
|
|
183
|
+
contentStr = message.content;
|
|
184
|
+
} else {
|
|
185
|
+
const parts = message.content;
|
|
186
|
+
const textParts = parts.filter((p): p is { type: 'text'; text: string } => p.type === 'text').map(p => p.text);
|
|
187
|
+
const toolCalls = parts.filter(p => p.type === 'tool-call');
|
|
188
|
+
contentStr = textParts.join('');
|
|
189
|
+
if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
|
|
190
|
+
}
|
|
191
|
+
} else if (message.role === 'tool') {
|
|
192
|
+
contentStr = JSON.stringify(message.content);
|
|
193
|
+
const firstResult = Array.isArray(message.content) ? message.content[0] : undefined;
|
|
194
|
+
if (firstResult && 'toolCallId' in firstResult) toolCallId = firstResult.toolCallId;
|
|
195
|
+
} else {
|
|
196
|
+
contentStr = '';
|
|
197
|
+
}
|
|
198
|
+
|
|
172
199
|
// Insert the message
|
|
173
200
|
await this.engine.insert(MESSAGES_OBJECT, {
|
|
174
201
|
id: msgId,
|
|
175
202
|
conversation_id: conversationId,
|
|
176
203
|
role: message.role,
|
|
177
|
-
content:
|
|
178
|
-
tool_calls:
|
|
179
|
-
tool_call_id:
|
|
204
|
+
content: contentStr,
|
|
205
|
+
tool_calls: toolCallsJson,
|
|
206
|
+
tool_call_id: toolCallId,
|
|
180
207
|
created_at: now,
|
|
181
208
|
});
|
|
182
209
|
|
|
@@ -233,20 +260,42 @@ export class ObjectQLConversationService implements IAIConversationService {
|
|
|
233
260
|
}
|
|
234
261
|
|
|
235
262
|
/**
|
|
236
|
-
* Map a database row to
|
|
263
|
+
* Map a database row to a ModelMessage.
|
|
237
264
|
*/
|
|
238
|
-
private toMessage(row: DbMessageRow):
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
private toMessage(row: DbMessageRow): ModelMessage {
|
|
266
|
+
switch (row.role) {
|
|
267
|
+
case 'system':
|
|
268
|
+
return { role: 'system', content: row.content };
|
|
269
|
+
case 'user':
|
|
270
|
+
return { role: 'user', content: row.content };
|
|
271
|
+
case 'assistant': {
|
|
272
|
+
const toolCalls = this.safeParse<ToolCallPart[]>(row.tool_calls);
|
|
273
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
274
|
+
const content: Array<{ type: 'text'; text: string } | ToolCallPart> = [];
|
|
275
|
+
if (row.content) content.push({ type: 'text', text: row.content });
|
|
276
|
+
content.push(...toolCalls);
|
|
277
|
+
return { role: 'assistant', content };
|
|
278
|
+
}
|
|
279
|
+
return { role: 'assistant', content: row.content };
|
|
280
|
+
}
|
|
281
|
+
case 'tool': {
|
|
282
|
+
const toolResults = this.safeParse<ToolResultPart[]>(row.content);
|
|
283
|
+
if (toolResults && toolResults.length > 0 && toolResults[0]?.type === 'tool-result') {
|
|
284
|
+
return { role: 'tool', content: toolResults };
|
|
285
|
+
}
|
|
286
|
+
// Backward compat: old format was a plain string
|
|
287
|
+
return {
|
|
288
|
+
role: 'tool',
|
|
289
|
+
content: [{
|
|
290
|
+
type: 'tool-result' as const,
|
|
291
|
+
toolCallId: row.tool_call_id ?? '',
|
|
292
|
+
toolName: 'unknown',
|
|
293
|
+
output: { type: 'text' as const, value: row.content },
|
|
294
|
+
}],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
default:
|
|
298
|
+
return { role: 'user', content: row.content };
|
|
249
299
|
}
|
|
250
|
-
return msg;
|
|
251
300
|
}
|
|
252
301
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,26 +10,45 @@ export type { AIServicePluginOptions } from './plugin.js';
|
|
|
10
10
|
|
|
11
11
|
// Adapters
|
|
12
12
|
export { MemoryLLMAdapter } from './adapters/memory-adapter.js';
|
|
13
|
+
export { VercelLLMAdapter } from './adapters/vercel-adapter.js';
|
|
14
|
+
export type { VercelLLMAdapterConfig } from './adapters/vercel-adapter.js';
|
|
13
15
|
export type { LLMAdapter } from '@objectstack/spec/contracts';
|
|
14
16
|
|
|
17
|
+
// Vercel Data Stream encoder
|
|
18
|
+
export { encodeStreamPart, encodeVercelDataStream } from './stream/vercel-stream-encoder.js';
|
|
19
|
+
|
|
15
20
|
// Conversation
|
|
16
21
|
export { InMemoryConversationService } from './conversation/in-memory-conversation-service.js';
|
|
17
22
|
export { ObjectQLConversationService } from './conversation/objectql-conversation-service.js';
|
|
18
23
|
|
|
19
24
|
// Tool registry
|
|
20
25
|
export { ToolRegistry } from './tools/tool-registry.js';
|
|
21
|
-
export type { ToolHandler } from './tools/tool-registry.js';
|
|
26
|
+
export type { ToolHandler, ToolExecutionResult } from './tools/tool-registry.js';
|
|
22
27
|
|
|
23
28
|
// Data tools
|
|
24
29
|
export { registerDataTools, DATA_TOOL_DEFINITIONS } from './tools/data-tools.js';
|
|
25
30
|
export type { DataToolContext } from './tools/data-tools.js';
|
|
26
31
|
|
|
32
|
+
// Metadata tools
|
|
33
|
+
export { registerMetadataTools, METADATA_TOOL_DEFINITIONS } from './tools/metadata-tools.js';
|
|
34
|
+
export type { MetadataToolContext } from './tools/metadata-tools.js';
|
|
35
|
+
|
|
36
|
+
// Individual tool metadata (first-class Tool definitions via defineTool)
|
|
37
|
+
export {
|
|
38
|
+
createObjectTool,
|
|
39
|
+
addFieldTool,
|
|
40
|
+
modifyFieldTool,
|
|
41
|
+
deleteFieldTool,
|
|
42
|
+
listMetadataObjectsTool,
|
|
43
|
+
describeMetadataObjectTool,
|
|
44
|
+
} from './tools/metadata-tools.js';
|
|
45
|
+
|
|
27
46
|
// Agent runtime
|
|
28
47
|
export { AgentRuntime } from './agent-runtime.js';
|
|
29
48
|
export type { AgentChatContext } from './agent-runtime.js';
|
|
30
49
|
|
|
31
50
|
// Built-in agents
|
|
32
|
-
export { DATA_CHAT_AGENT } from './agents/index.js';
|
|
51
|
+
export { DATA_CHAT_AGENT, METADATA_ASSISTANT_AGENT } from './agents/index.js';
|
|
33
52
|
|
|
34
53
|
// Object definitions
|
|
35
54
|
export { AiConversationObject, AiMessageObject } from './objects/index.js';
|
|
@@ -37,4 +56,4 @@ export { AiConversationObject, AiMessageObject } from './objects/index.js';
|
|
|
37
56
|
// Routes
|
|
38
57
|
export { buildAIRoutes } from './routes/ai-routes.js';
|
|
39
58
|
export { buildAgentRoutes } from './routes/agent-routes.js';
|
|
40
|
-
export type { RouteDefinition, RouteRequest, RouteResponse } from './routes/ai-routes.js';
|
|
59
|
+
export type { RouteDefinition, RouteRequest, RouteResponse, RouteUserContext } from './routes/ai-routes.js';
|