@objectstack/service-ai 4.0.1 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +9 -0
- package/dist/index.cjs +1120 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -78
- package/dist/index.d.ts +316 -78
- package/dist/index.js +1105 -63
- package/dist/index.js.map +1 -1
- package/package.json +26 -4
- package/src/__tests__/ai-service.test.ts +248 -27
- package/src/__tests__/auth-and-toolcalling.test.ts +30 -28
- package/src/__tests__/chatbot-features.test.ts +229 -82
- package/src/__tests__/metadata-tools.test.ts +964 -0
- package/src/__tests__/objectql-conversation-service.test.ts +34 -16
- package/src/__tests__/vercel-stream-encoder.test.ts +263 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/memory-adapter.ts +17 -9
- package/src/adapters/vercel-adapter.ts +148 -0
- package/src/agent-runtime.ts +27 -3
- package/src/agents/index.ts +1 -0
- package/src/agents/metadata-assistant-agent.ts +87 -0
- package/src/ai-service.ts +68 -36
- package/src/conversation/in-memory-conversation-service.ts +2 -2
- package/src/conversation/objectql-conversation-service.ts +67 -18
- package/src/index.ts +21 -2
- package/src/plugin.ts +166 -9
- package/src/routes/agent-routes.ts +26 -3
- package/src/routes/ai-routes.ts +156 -13
- package/src/stream/index.ts +3 -0
- package/src/stream/vercel-stream-encoder.ts +129 -0
- package/src/tools/add-field.tool.ts +70 -0
- package/src/tools/create-object.tool.ts +66 -0
- package/src/tools/delete-field.tool.ts +38 -0
- package/src/tools/describe-metadata-object.tool.ts +32 -0
- package/src/tools/index.ts +12 -1
- package/src/tools/list-metadata-objects.tool.ts +34 -0
- package/src/tools/metadata-tools.ts +430 -0
- package/src/tools/modify-field.tool.ts +44 -0
- package/src/tools/tool-registry.ts +32 -9
|
@@ -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,7 +156,7 @@ 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
162
|
// Destructure loop-specific options so they are never forwarded to the adapter
|
|
@@ -174,31 +202,34 @@ export class AIService implements IAIService {
|
|
|
174
202
|
|
|
175
203
|
this.logger.debug('[AI] chatWithTools tool calls', {
|
|
176
204
|
iteration,
|
|
177
|
-
calls: result.toolCalls.map(tc => tc.
|
|
205
|
+
calls: result.toolCalls.map(tc => tc.toolName),
|
|
178
206
|
});
|
|
179
207
|
|
|
180
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);
|
|
181
212
|
conversation.push({
|
|
182
213
|
role: 'assistant',
|
|
183
|
-
content:
|
|
184
|
-
|
|
185
|
-
});
|
|
214
|
+
content: assistantContent,
|
|
215
|
+
} as ModelMessage);
|
|
186
216
|
|
|
187
217
|
// Execute all tool calls in parallel
|
|
188
|
-
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
218
|
+
const toolResults: ToolExecutionResult[] = await this.toolRegistry.executeAll(result.toolCalls);
|
|
189
219
|
|
|
190
220
|
// Process results: track errors and honour onToolError callback
|
|
191
221
|
for (const tr of toolResults) {
|
|
192
222
|
if (tr.isError) {
|
|
193
223
|
// Match tool call by toolCallId for robust attribution
|
|
194
|
-
const matchedCall = result.toolCalls!.find(tc => tc.
|
|
195
|
-
const toolName = matchedCall?.
|
|
196
|
-
const
|
|
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 };
|
|
197
228
|
toolErrors.push(errorEntry);
|
|
198
229
|
this.logger.warn('[AI] chatWithTools tool error', errorEntry);
|
|
199
230
|
|
|
200
231
|
if (onToolError && matchedCall) {
|
|
201
|
-
const action = onToolError(matchedCall,
|
|
232
|
+
const action = onToolError(matchedCall, errorText);
|
|
202
233
|
if (action === 'abort') {
|
|
203
234
|
abortedByCallback = true;
|
|
204
235
|
}
|
|
@@ -208,9 +239,8 @@ export class AIService implements IAIService {
|
|
|
208
239
|
// Append each tool result as a `role: 'tool'` message
|
|
209
240
|
conversation.push({
|
|
210
241
|
role: 'tool',
|
|
211
|
-
content: tr
|
|
212
|
-
|
|
213
|
-
});
|
|
242
|
+
content: [tr],
|
|
243
|
+
} as ModelMessage);
|
|
214
244
|
}
|
|
215
245
|
|
|
216
246
|
if (abortedByCallback) {
|
|
@@ -244,9 +274,9 @@ export class AIService implements IAIService {
|
|
|
244
274
|
* fed back until a final text stream is produced.
|
|
245
275
|
*/
|
|
246
276
|
async *streamChatWithTools(
|
|
247
|
-
messages:
|
|
277
|
+
messages: ModelMessage[],
|
|
248
278
|
options?: ChatWithToolsOptions,
|
|
249
|
-
): AsyncIterable<
|
|
279
|
+
): AsyncIterable<TextStreamPart<ToolSet>> {
|
|
250
280
|
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
251
281
|
const maxIterations = maxIter ?? AIService.DEFAULT_MAX_ITERATIONS;
|
|
252
282
|
const registeredTools = this.toolRegistry.getAll();
|
|
@@ -271,29 +301,32 @@ export class AIService implements IAIService {
|
|
|
271
301
|
|
|
272
302
|
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
273
303
|
// Final round — return the probed result without an extra model call
|
|
274
|
-
yield
|
|
275
|
-
yield
|
|
304
|
+
yield textDeltaPart('stream', result.content);
|
|
305
|
+
yield finishPart(result);
|
|
276
306
|
return;
|
|
277
307
|
}
|
|
278
308
|
|
|
279
309
|
// Emit tool-call events so the client can see tool execution progress
|
|
280
310
|
for (const tc of result.toolCalls) {
|
|
281
|
-
yield { type: 'tool-call',
|
|
311
|
+
yield { type: 'tool-call', toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input } as TextStreamPart<ToolSet>;
|
|
282
312
|
}
|
|
283
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);
|
|
284
317
|
conversation.push({
|
|
285
318
|
role: 'assistant',
|
|
286
|
-
content:
|
|
287
|
-
|
|
288
|
-
});
|
|
319
|
+
content: assistantContent,
|
|
320
|
+
} as ModelMessage);
|
|
289
321
|
|
|
290
|
-
const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
|
|
322
|
+
const toolResults: ToolExecutionResult[] = await this.toolRegistry.executeAll(result.toolCalls);
|
|
291
323
|
|
|
292
324
|
for (const tr of toolResults) {
|
|
293
325
|
if (tr.isError && onToolError) {
|
|
294
|
-
const matchedCall = result.toolCalls!.find(tc => tc.
|
|
326
|
+
const matchedCall = result.toolCalls!.find(tc => tc.toolCallId === tr.toolCallId);
|
|
295
327
|
if (matchedCall) {
|
|
296
|
-
const
|
|
328
|
+
const errorText = AIService.extractOutputText(tr);
|
|
329
|
+
const action = onToolError(matchedCall, errorText);
|
|
297
330
|
if (action === 'abort') {
|
|
298
331
|
abortedByCallback = true;
|
|
299
332
|
}
|
|
@@ -301,9 +334,8 @@ export class AIService implements IAIService {
|
|
|
301
334
|
}
|
|
302
335
|
conversation.push({
|
|
303
336
|
role: 'tool',
|
|
304
|
-
content: tr
|
|
305
|
-
|
|
306
|
-
});
|
|
337
|
+
content: [tr],
|
|
338
|
+
} as ModelMessage);
|
|
307
339
|
}
|
|
308
340
|
|
|
309
341
|
if (abortedByCallback) {
|
|
@@ -319,7 +351,7 @@ export class AIService implements IAIService {
|
|
|
319
351
|
}
|
|
320
352
|
const finalOptions = { ...chatOptions, tools: undefined, toolChoice: undefined };
|
|
321
353
|
const result = await this.adapter.chat(conversation, finalOptions);
|
|
322
|
-
yield
|
|
323
|
-
yield
|
|
354
|
+
yield textDeltaPart('stream', result.content);
|
|
355
|
+
yield finishPart(result);
|
|
324
356
|
}
|
|
325
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';
|