@objectstack/service-ai 4.0.4 → 4.0.5

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.
Files changed (52) hide show
  1. package/dist/index.cjs +1176 -135
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1225 -430
  4. package/dist/index.d.ts +1225 -430
  5. package/dist/index.js +1160 -128
  6. package/dist/index.js.map +1 -1
  7. package/package.json +35 -8
  8. package/.turbo/turbo-build.log +0 -22
  9. package/CHANGELOG.md +0 -61
  10. package/src/__tests__/ai-service.test.ts +0 -981
  11. package/src/__tests__/auth-and-toolcalling.test.ts +0 -677
  12. package/src/__tests__/chatbot-features.test.ts +0 -1116
  13. package/src/__tests__/metadata-tools.test.ts +0 -970
  14. package/src/__tests__/objectql-conversation-service.test.ts +0 -382
  15. package/src/__tests__/tool-routes.test.ts +0 -191
  16. package/src/__tests__/vercel-stream-encoder.test.ts +0 -310
  17. package/src/adapters/index.ts +0 -6
  18. package/src/adapters/memory-adapter.ts +0 -72
  19. package/src/adapters/types.ts +0 -3
  20. package/src/adapters/vercel-adapter.ts +0 -148
  21. package/src/agent-runtime.ts +0 -154
  22. package/src/agents/data-chat-agent.ts +0 -79
  23. package/src/agents/index.ts +0 -4
  24. package/src/agents/metadata-assistant-agent.ts +0 -87
  25. package/src/ai-service.ts +0 -364
  26. package/src/conversation/in-memory-conversation-service.ts +0 -103
  27. package/src/conversation/index.ts +0 -4
  28. package/src/conversation/objectql-conversation-service.ts +0 -301
  29. package/src/index.ts +0 -60
  30. package/src/objects/ai-conversation.object.ts +0 -86
  31. package/src/objects/ai-message.object.ts +0 -86
  32. package/src/objects/index.ts +0 -10
  33. package/src/plugin.ts +0 -391
  34. package/src/routes/agent-routes.ts +0 -190
  35. package/src/routes/ai-routes.ts +0 -439
  36. package/src/routes/index.ts +0 -5
  37. package/src/routes/message-utils.ts +0 -90
  38. package/src/routes/tool-routes.ts +0 -142
  39. package/src/stream/index.ts +0 -3
  40. package/src/stream/vercel-stream-encoder.ts +0 -153
  41. package/src/tools/add-field.tool.ts +0 -70
  42. package/src/tools/create-object.tool.ts +0 -66
  43. package/src/tools/data-tools.ts +0 -293
  44. package/src/tools/delete-field.tool.ts +0 -38
  45. package/src/tools/describe-object.tool.ts +0 -31
  46. package/src/tools/index.ts +0 -18
  47. package/src/tools/list-objects.tool.ts +0 -34
  48. package/src/tools/metadata-tools.ts +0 -430
  49. package/src/tools/modify-field.tool.ts +0 -44
  50. package/src/tools/tool-registry.ts +0 -132
  51. package/tsconfig.json +0 -17
  52. package/vitest.config.ts +0 -23
@@ -1,301 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { randomUUID } from 'node:crypto';
4
- import type {
5
- AIConversation,
6
- ModelMessage,
7
- ToolCallPart,
8
- ToolResultPart,
9
- IAIConversationService,
10
- IDataEngine,
11
- } from '@objectstack/spec/contracts';
12
-
13
- /** Object names used for persistence. */
14
- const CONVERSATIONS_OBJECT = 'ai_conversations';
15
- const MESSAGES_OBJECT = 'ai_messages';
16
-
17
- /** Database row shape for ai_conversations. */
18
- interface DbConversationRow {
19
- id: string;
20
- title: string | null;
21
- agent_id: string | null;
22
- user_id: string | null;
23
- metadata: string | null;
24
- created_at: string;
25
- updated_at: string;
26
- }
27
-
28
- /** Database row shape for ai_messages. */
29
- interface DbMessageRow {
30
- id: string;
31
- conversation_id: string;
32
- role: 'system' | 'user' | 'assistant' | 'tool';
33
- content: string;
34
- tool_calls: string | null;
35
- tool_call_id: string | null;
36
- created_at: string;
37
- }
38
-
39
- /** Deterministic ordering for conversations (total order). */
40
- const CONVERSATION_ORDER = [
41
- { field: 'created_at', order: 'asc' as const },
42
- { field: 'id', order: 'asc' as const },
43
- ];
44
-
45
- /** Deterministic ordering for messages within a conversation. */
46
- const MESSAGE_ORDER = [
47
- { field: 'created_at', order: 'asc' as const },
48
- { field: 'id', order: 'asc' as const },
49
- ];
50
-
51
- /**
52
- * ObjectQLConversationService — Persistent implementation of IAIConversationService.
53
- *
54
- * Delegates all storage to an {@link IDataEngine} instance, using the
55
- * `ai_conversations` and `ai_messages` objects. This decouples the service
56
- * from any specific database driver (Turso, Postgres, SQLite, etc.).
57
- *
58
- * Production environments should use this implementation to ensure
59
- * conversation history survives service restarts.
60
- */
61
- export class ObjectQLConversationService implements IAIConversationService {
62
- private readonly engine: IDataEngine;
63
-
64
- constructor(engine: IDataEngine) {
65
- this.engine = engine;
66
- }
67
-
68
- async create(options: {
69
- title?: string;
70
- agentId?: string;
71
- userId?: string;
72
- metadata?: Record<string, unknown>;
73
- } = {}): Promise<AIConversation> {
74
- const now = new Date().toISOString();
75
- const id = `conv_${randomUUID()}`;
76
-
77
- const record = {
78
- id,
79
- title: options.title ?? null,
80
- agent_id: options.agentId ?? null,
81
- user_id: options.userId ?? null,
82
- metadata: options.metadata ? JSON.stringify(options.metadata) : null,
83
- created_at: now,
84
- updated_at: now,
85
- };
86
-
87
- await this.engine.insert(CONVERSATIONS_OBJECT, record);
88
-
89
- return {
90
- id,
91
- title: options.title,
92
- agentId: options.agentId,
93
- userId: options.userId,
94
- messages: [],
95
- createdAt: now,
96
- updatedAt: now,
97
- metadata: options.metadata,
98
- };
99
- }
100
-
101
- async get(conversationId: string): Promise<AIConversation | null> {
102
- const row: DbConversationRow | null = await this.engine.findOne(CONVERSATIONS_OBJECT, {
103
- where: { id: conversationId },
104
- });
105
-
106
- if (!row) return null;
107
-
108
- const messages: DbMessageRow[] = await this.engine.find(MESSAGES_OBJECT, {
109
- where: { conversation_id: conversationId },
110
- orderBy: MESSAGE_ORDER,
111
- });
112
-
113
- return this.toConversation(row, messages);
114
- }
115
-
116
- async list(options: {
117
- userId?: string;
118
- agentId?: string;
119
- limit?: number;
120
- cursor?: string;
121
- } = {}): Promise<AIConversation[]> {
122
- const where: Record<string, unknown> = {};
123
- if (options.userId) where.user_id = options.userId;
124
- if (options.agentId) where.agent_id = options.agentId;
125
-
126
- // Stable cursor-based pagination using composite (created_at, id) order.
127
- // This avoids skips/duplicates when multiple conversations share a timestamp.
128
- if (options.cursor) {
129
- const cursorRow = await this.engine.findOne(CONVERSATIONS_OBJECT, {
130
- where: { id: options.cursor },
131
- fields: ['created_at', 'id'],
132
- });
133
- if (cursorRow) {
134
- where.$or = [
135
- { created_at: { $gt: cursorRow.created_at } },
136
- { created_at: cursorRow.created_at, id: { $gt: cursorRow.id } },
137
- ];
138
- }
139
- }
140
-
141
- const rows: DbConversationRow[] = await this.engine.find(CONVERSATIONS_OBJECT, {
142
- where: Object.keys(where).length > 0 ? where : undefined,
143
- orderBy: CONVERSATION_ORDER,
144
- limit: options.limit && options.limit > 0 ? options.limit : undefined,
145
- });
146
-
147
- // Load messages per conversation in parallel.
148
- // N+1 is bounded by the pagination limit; driver-agnostic $in is not guaranteed.
149
- const conversations: AIConversation[] = await Promise.all(
150
- rows.map(async (row) => {
151
- const messages: DbMessageRow[] = await this.engine.find(MESSAGES_OBJECT, {
152
- where: { conversation_id: row.id },
153
- orderBy: MESSAGE_ORDER,
154
- });
155
- return this.toConversation(row, messages);
156
- }),
157
- );
158
-
159
- return conversations;
160
- }
161
-
162
- async addMessage(conversationId: string, message: ModelMessage): Promise<AIConversation> {
163
- // Verify conversation exists
164
- const row: DbConversationRow | null = await this.engine.findOne(CONVERSATIONS_OBJECT, {
165
- where: { id: conversationId },
166
- });
167
- if (!row) {
168
- throw new Error(`Conversation "${conversationId}" not found`);
169
- }
170
-
171
- const now = new Date().toISOString();
172
- const msgId = `msg_${randomUUID()}`;
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
-
199
- // Insert the message
200
- await this.engine.insert(MESSAGES_OBJECT, {
201
- id: msgId,
202
- conversation_id: conversationId,
203
- role: message.role,
204
- content: contentStr,
205
- tool_calls: toolCallsJson,
206
- tool_call_id: toolCallId,
207
- created_at: now,
208
- });
209
-
210
- // Update conversation timestamp
211
- await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
212
- where: { id: conversationId },
213
- });
214
-
215
- // Return the full updated conversation
216
- return (await this.get(conversationId))!;
217
- }
218
-
219
- async delete(conversationId: string): Promise<void> {
220
- // Delete messages first (child records)
221
- await this.engine.delete(MESSAGES_OBJECT, {
222
- where: { conversation_id: conversationId },
223
- multi: true,
224
- });
225
-
226
- // Delete the conversation
227
- await this.engine.delete(CONVERSATIONS_OBJECT, {
228
- where: { id: conversationId },
229
- });
230
- }
231
-
232
- // ── Private helpers ──────────────────────────────────────────────
233
-
234
- /**
235
- * Safely parse a JSON string, returning `undefined` on failure.
236
- */
237
- private safeParse<T>(value: string | null, fallback?: T): T | undefined {
238
- if (!value) return undefined;
239
- try {
240
- return JSON.parse(value) as T;
241
- } catch {
242
- return fallback;
243
- }
244
- }
245
-
246
- /**
247
- * Map a database row + message rows to an AIConversation.
248
- */
249
- private toConversation(row: DbConversationRow, messageRows: DbMessageRow[]): AIConversation {
250
- return {
251
- id: row.id,
252
- title: row.title ?? undefined,
253
- agentId: row.agent_id ?? undefined,
254
- userId: row.user_id ?? undefined,
255
- messages: messageRows.map(m => this.toMessage(m)),
256
- createdAt: row.created_at,
257
- updatedAt: row.updated_at,
258
- metadata: this.safeParse<Record<string, unknown>>(row.metadata),
259
- };
260
- }
261
-
262
- /**
263
- * Map a database row to a ModelMessage.
264
- */
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 };
299
- }
300
- }
301
- }
package/src/index.ts DELETED
@@ -1,60 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- // Core service
4
- export { AIService } from './ai-service.js';
5
- export type { AIServiceConfig } from './ai-service.js';
6
-
7
- // Kernel plugin
8
- export { AIServicePlugin } from './plugin.js';
9
- export type { AIServicePluginOptions } from './plugin.js';
10
-
11
- // Adapters
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';
15
- export type { LLMAdapter } from '@objectstack/spec/contracts';
16
-
17
- // Vercel Data Stream encoder
18
- export { encodeStreamPart, encodeVercelDataStream } from './stream/vercel-stream-encoder.js';
19
-
20
- // Conversation
21
- export { InMemoryConversationService } from './conversation/in-memory-conversation-service.js';
22
- export { ObjectQLConversationService } from './conversation/objectql-conversation-service.js';
23
-
24
- // Tool registry
25
- export { ToolRegistry } from './tools/tool-registry.js';
26
- export type { ToolHandler, ToolExecutionResult } from './tools/tool-registry.js';
27
-
28
- // Data tools
29
- export { registerDataTools, DATA_TOOL_DEFINITIONS } from './tools/data-tools.js';
30
- export type { DataToolContext } from './tools/data-tools.js';
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
- listObjectsTool,
43
- describeObjectTool,
44
- } from './tools/metadata-tools.js';
45
-
46
- // Agent runtime
47
- export { AgentRuntime } from './agent-runtime.js';
48
- export type { AgentChatContext } from './agent-runtime.js';
49
-
50
- // Built-in agents
51
- export { DATA_CHAT_AGENT, METADATA_ASSISTANT_AGENT } from './agents/index.js';
52
-
53
- // Object definitions
54
- export { AiConversationObject, AiMessageObject } from './objects/index.js';
55
-
56
- // Routes
57
- export { buildAIRoutes } from './routes/ai-routes.js';
58
- export { buildAgentRoutes } from './routes/agent-routes.js';
59
- export { buildToolRoutes } from './routes/tool-routes.js';
60
- export type { RouteDefinition, RouteRequest, RouteResponse, RouteUserContext } from './routes/ai-routes.js';
@@ -1,86 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { ObjectSchema, Field } from '@objectstack/spec/data';
4
-
5
- /**
6
- * ai_conversations — AI Conversation Object
7
- *
8
- * Stores conversation metadata for persistent AI conversation management.
9
- * Messages are stored separately in `ai_messages` to support efficient
10
- * querying and pagination.
11
- *
12
- * @namespace ai
13
- */
14
- export const AiConversationObject = ObjectSchema.create({
15
- namespace: 'ai',
16
- name: 'conversations',
17
- label: 'AI Conversation',
18
- pluralLabel: 'AI Conversations',
19
- icon: 'message-square',
20
- isSystem: true,
21
- description: 'Persistent AI conversation metadata',
22
-
23
- fields: {
24
- id: Field.text({
25
- label: 'Conversation ID',
26
- required: true,
27
- readonly: true,
28
- }),
29
-
30
- title: Field.text({
31
- label: 'Title',
32
- required: false,
33
- maxLength: 500,
34
- description: 'Conversation title or summary',
35
- }),
36
-
37
- agent_id: Field.text({
38
- label: 'Agent ID',
39
- required: false,
40
- maxLength: 255,
41
- description: 'Associated AI agent identifier',
42
- }),
43
-
44
- user_id: Field.text({
45
- label: 'User ID',
46
- required: false,
47
- maxLength: 255,
48
- description: 'User who owns the conversation',
49
- }),
50
-
51
- metadata: Field.textarea({
52
- label: 'Metadata',
53
- required: false,
54
- description: 'JSON-serialized conversation metadata',
55
- }),
56
-
57
- created_at: Field.datetime({
58
- label: 'Created At',
59
- required: true,
60
- defaultValue: 'NOW()',
61
- readonly: true,
62
- }),
63
-
64
- updated_at: Field.datetime({
65
- label: 'Updated At',
66
- required: true,
67
- defaultValue: 'NOW()',
68
- readonly: true,
69
- }),
70
- },
71
-
72
- indexes: [
73
- { fields: ['user_id'] },
74
- { fields: ['agent_id'] },
75
- { fields: ['created_at'] },
76
- ],
77
-
78
- enable: {
79
- trackHistory: false,
80
- searchable: false,
81
- apiEnabled: true,
82
- apiMethods: ['get', 'list', 'create', 'update', 'delete'],
83
- trash: false,
84
- mru: false,
85
- },
86
- });
@@ -1,86 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { ObjectSchema, Field } from '@objectstack/spec/data';
4
-
5
- /**
6
- * ai_messages — AI Message Object
7
- *
8
- * Stores individual messages within an AI conversation.
9
- * Each message belongs to a conversation via `conversation_id` foreign key.
10
- *
11
- * @namespace ai
12
- */
13
- export const AiMessageObject = ObjectSchema.create({
14
- namespace: 'ai',
15
- name: 'messages',
16
- label: 'AI Message',
17
- pluralLabel: 'AI Messages',
18
- icon: 'message-circle',
19
- isSystem: true,
20
- description: 'Individual messages within AI conversations',
21
-
22
- fields: {
23
- id: Field.text({
24
- label: 'Message ID',
25
- required: true,
26
- readonly: true,
27
- }),
28
-
29
- conversation_id: Field.text({
30
- label: 'Conversation ID',
31
- required: true,
32
- description: 'Foreign key to ai_conversations',
33
- }),
34
-
35
- role: Field.select({
36
- label: 'Role',
37
- required: true,
38
- options: [
39
- { label: 'System', value: 'system' },
40
- { label: 'User', value: 'user' },
41
- { label: 'Assistant', value: 'assistant' },
42
- { label: 'Tool', value: 'tool' },
43
- ],
44
- }),
45
-
46
- content: Field.textarea({
47
- label: 'Content',
48
- required: true,
49
- description: 'Message content',
50
- }),
51
-
52
- tool_calls: Field.textarea({
53
- label: 'Tool Calls',
54
- required: false,
55
- description: 'JSON-serialized tool calls (when role=assistant)',
56
- }),
57
-
58
- tool_call_id: Field.text({
59
- label: 'Tool Call ID',
60
- required: false,
61
- maxLength: 255,
62
- description: 'ID of the tool call this message responds to (when role=tool)',
63
- }),
64
-
65
- created_at: Field.datetime({
66
- label: 'Created At',
67
- required: true,
68
- defaultValue: 'NOW()',
69
- readonly: true,
70
- }),
71
- },
72
-
73
- indexes: [
74
- { fields: ['conversation_id'] },
75
- { fields: ['conversation_id', 'created_at'] },
76
- ],
77
-
78
- enable: {
79
- trackHistory: false,
80
- searchable: false,
81
- apiEnabled: true,
82
- apiMethods: ['get', 'list', 'create'],
83
- trash: false,
84
- mru: false,
85
- },
86
- });
@@ -1,10 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- /**
4
- * AI Service — System Object Definitions (ai namespace)
5
- *
6
- * Canonical ObjectSchema definitions for AI conversation persistence.
7
- */
8
-
9
- export { AiConversationObject } from './ai-conversation.object.js';
10
- export { AiMessageObject } from './ai-message.object.js';