@objectstack/service-ai 4.0.0

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/dist/index.cjs ADDED
@@ -0,0 +1,1418 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AIService: () => AIService,
24
+ AIServicePlugin: () => AIServicePlugin,
25
+ AgentRuntime: () => AgentRuntime,
26
+ AiConversationObject: () => AiConversationObject,
27
+ AiMessageObject: () => AiMessageObject,
28
+ DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
29
+ DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
30
+ InMemoryConversationService: () => InMemoryConversationService,
31
+ MemoryLLMAdapter: () => MemoryLLMAdapter,
32
+ ObjectQLConversationService: () => ObjectQLConversationService,
33
+ ToolRegistry: () => ToolRegistry,
34
+ buildAIRoutes: () => buildAIRoutes,
35
+ buildAgentRoutes: () => buildAgentRoutes,
36
+ registerDataTools: () => registerDataTools
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/ai-service.ts
41
+ var import_core = require("@objectstack/core");
42
+
43
+ // src/adapters/memory-adapter.ts
44
+ var MemoryLLMAdapter = class {
45
+ constructor() {
46
+ this.name = "memory";
47
+ }
48
+ async chat(messages, options) {
49
+ const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
50
+ const content = lastUserMessage ? `[memory] ${lastUserMessage.content}` : "[memory] (no user message)";
51
+ return {
52
+ content,
53
+ model: options?.model ?? "memory",
54
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
55
+ };
56
+ }
57
+ async complete(prompt, options) {
58
+ return {
59
+ content: `[memory] ${prompt}`,
60
+ model: options?.model ?? "memory",
61
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
62
+ };
63
+ }
64
+ async *streamChat(messages, _options) {
65
+ const result = await this.chat(messages);
66
+ const words = result.content.split(" ");
67
+ for (let i = 0; i < words.length; i++) {
68
+ const textDelta = i === 0 ? words[i] : ` ${words[i]}`;
69
+ yield { type: "text-delta", textDelta };
70
+ }
71
+ yield { type: "finish", result };
72
+ }
73
+ async embed(input) {
74
+ const texts = Array.isArray(input) ? input : [input];
75
+ return texts.map(() => [0, 0, 0]);
76
+ }
77
+ async listModels() {
78
+ return ["memory"];
79
+ }
80
+ };
81
+
82
+ // src/tools/tool-registry.ts
83
+ var ToolRegistry = class {
84
+ constructor() {
85
+ this.definitions = /* @__PURE__ */ new Map();
86
+ this.handlers = /* @__PURE__ */ new Map();
87
+ }
88
+ /**
89
+ * Register a tool with its definition and handler.
90
+ * @param definition - Tool definition (name, description, parameters schema)
91
+ * @param handler - Async function that executes the tool
92
+ */
93
+ register(definition, handler) {
94
+ this.definitions.set(definition.name, definition);
95
+ this.handlers.set(definition.name, handler);
96
+ }
97
+ /**
98
+ * Unregister a tool by name.
99
+ */
100
+ unregister(name) {
101
+ this.definitions.delete(name);
102
+ this.handlers.delete(name);
103
+ }
104
+ /**
105
+ * Check whether a tool is registered.
106
+ */
107
+ has(name) {
108
+ return this.definitions.has(name);
109
+ }
110
+ /**
111
+ * Get the definition for a registered tool.
112
+ */
113
+ getDefinition(name) {
114
+ return this.definitions.get(name);
115
+ }
116
+ /**
117
+ * Return all registered tool definitions.
118
+ */
119
+ getAll() {
120
+ return Array.from(this.definitions.values());
121
+ }
122
+ /** Number of registered tools. */
123
+ get size() {
124
+ return this.definitions.size;
125
+ }
126
+ /** All registered tool names. */
127
+ names() {
128
+ return Array.from(this.definitions.keys());
129
+ }
130
+ /**
131
+ * Execute a tool call and return the result.
132
+ */
133
+ async execute(toolCall) {
134
+ const handler = this.handlers.get(toolCall.name);
135
+ if (!handler) {
136
+ return {
137
+ toolCallId: toolCall.id,
138
+ content: `Tool "${toolCall.name}" is not registered`,
139
+ isError: true
140
+ };
141
+ }
142
+ try {
143
+ const args = JSON.parse(toolCall.arguments);
144
+ const content = await handler(args);
145
+ return { toolCallId: toolCall.id, content };
146
+ } catch (err) {
147
+ const message = err instanceof Error ? err.message : String(err);
148
+ return { toolCallId: toolCall.id, content: message, isError: true };
149
+ }
150
+ }
151
+ /**
152
+ * Execute multiple tool calls in parallel.
153
+ */
154
+ async executeAll(toolCalls) {
155
+ return Promise.all(toolCalls.map((tc) => this.execute(tc)));
156
+ }
157
+ /**
158
+ * Clear all registered tools.
159
+ */
160
+ clear() {
161
+ this.definitions.clear();
162
+ this.handlers.clear();
163
+ }
164
+ };
165
+
166
+ // src/conversation/in-memory-conversation-service.ts
167
+ var InMemoryConversationService = class {
168
+ constructor() {
169
+ this.store = /* @__PURE__ */ new Map();
170
+ this.counter = 0;
171
+ }
172
+ async create(options = {}) {
173
+ const now = (/* @__PURE__ */ new Date()).toISOString();
174
+ const id = `conv_${++this.counter}`;
175
+ const conversation = {
176
+ id,
177
+ title: options.title,
178
+ agentId: options.agentId,
179
+ userId: options.userId,
180
+ messages: [],
181
+ createdAt: now,
182
+ updatedAt: now,
183
+ metadata: options.metadata
184
+ };
185
+ this.store.set(id, conversation);
186
+ return conversation;
187
+ }
188
+ async get(conversationId) {
189
+ return this.store.get(conversationId) ?? null;
190
+ }
191
+ async list(options = {}) {
192
+ let results = Array.from(this.store.values());
193
+ if (options.userId) {
194
+ results = results.filter((c) => c.userId === options.userId);
195
+ }
196
+ if (options.agentId) {
197
+ results = results.filter((c) => c.agentId === options.agentId);
198
+ }
199
+ if (options.cursor) {
200
+ const idx = results.findIndex((c) => c.id === options.cursor);
201
+ if (idx >= 0) {
202
+ results = results.slice(idx + 1);
203
+ }
204
+ }
205
+ if (options.limit && options.limit > 0) {
206
+ results = results.slice(0, options.limit);
207
+ }
208
+ return results;
209
+ }
210
+ async addMessage(conversationId, message) {
211
+ const conversation = this.store.get(conversationId);
212
+ if (!conversation) {
213
+ throw new Error(`Conversation "${conversationId}" not found`);
214
+ }
215
+ conversation.messages.push(message);
216
+ conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
217
+ return conversation;
218
+ }
219
+ async delete(conversationId) {
220
+ this.store.delete(conversationId);
221
+ }
222
+ /** Total number of stored conversations. */
223
+ get size() {
224
+ return this.store.size;
225
+ }
226
+ /** Clear all conversations. */
227
+ clear() {
228
+ this.store.clear();
229
+ this.counter = 0;
230
+ }
231
+ };
232
+
233
+ // src/ai-service.ts
234
+ var _AIService = class _AIService {
235
+ constructor(config = {}) {
236
+ this.adapter = config.adapter ?? new MemoryLLMAdapter();
237
+ this.logger = config.logger ?? (0, import_core.createLogger)({ level: "info", format: "pretty" });
238
+ this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
239
+ this.conversationService = config.conversationService ?? new InMemoryConversationService();
240
+ this.logger.info(
241
+ `[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}`
242
+ );
243
+ }
244
+ /** The name of the active LLM adapter. */
245
+ get adapterName() {
246
+ return this.adapter.name;
247
+ }
248
+ // ── IAIService implementation ──────────────────────────────────
249
+ async chat(messages, options) {
250
+ this.logger.debug("[AI] chat", { messageCount: messages.length, model: options?.model });
251
+ return this.adapter.chat(messages, options);
252
+ }
253
+ async complete(prompt, options) {
254
+ this.logger.debug("[AI] complete", { promptLength: prompt.length, model: options?.model });
255
+ return this.adapter.complete(prompt, options);
256
+ }
257
+ async *streamChat(messages, options) {
258
+ this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
259
+ if (!this.adapter.streamChat) {
260
+ const result = await this.adapter.chat(messages, options);
261
+ yield { type: "text-delta", textDelta: result.content };
262
+ yield { type: "finish", result };
263
+ return;
264
+ }
265
+ yield* this.adapter.streamChat(messages, options);
266
+ }
267
+ async embed(input, model) {
268
+ if (!this.adapter.embed) {
269
+ throw new Error(`[AI] Adapter "${this.adapter.name}" does not support embeddings`);
270
+ }
271
+ return this.adapter.embed(input, model);
272
+ }
273
+ async listModels() {
274
+ if (!this.adapter.listModels) {
275
+ return [];
276
+ }
277
+ return this.adapter.listModels();
278
+ }
279
+ /**
280
+ * Chat with automatic tool call resolution.
281
+ *
282
+ * 1. Merges registered tool definitions into `options.tools`.
283
+ * 2. Calls the LLM adapter.
284
+ * 3. If the response contains `toolCalls`, executes them via the
285
+ * {@link ToolRegistry}, appends tool results as `role: 'tool'`
286
+ * messages, and loops back to step 2.
287
+ * 4. Repeats until the model produces a final text response or the
288
+ * maximum number of iterations (`maxIterations`) is reached.
289
+ */
290
+ async chatWithTools(messages, options) {
291
+ const { maxIterations: maxIter, ...restOptions } = options ?? {};
292
+ const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
293
+ const registeredTools = this.toolRegistry.getAll();
294
+ const mergedTools = [
295
+ ...registeredTools,
296
+ ...restOptions.tools ?? []
297
+ ];
298
+ const chatOptions = {
299
+ ...restOptions,
300
+ tools: mergedTools.length > 0 ? mergedTools : void 0,
301
+ toolChoice: mergedTools.length > 0 ? restOptions.toolChoice ?? "auto" : void 0
302
+ };
303
+ const conversation = [...messages];
304
+ this.logger.debug("[AI] chatWithTools start", {
305
+ messageCount: conversation.length,
306
+ toolCount: mergedTools.length,
307
+ maxIterations
308
+ });
309
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
310
+ const result = await this.adapter.chat(conversation, chatOptions);
311
+ if (!result.toolCalls || result.toolCalls.length === 0) {
312
+ this.logger.debug("[AI] chatWithTools finished", { iteration, content: result.content.slice(0, 80) });
313
+ return result;
314
+ }
315
+ this.logger.debug("[AI] chatWithTools tool calls", {
316
+ iteration,
317
+ calls: result.toolCalls.map((tc) => tc.name)
318
+ });
319
+ conversation.push({
320
+ role: "assistant",
321
+ content: result.content ?? "",
322
+ toolCalls: result.toolCalls
323
+ });
324
+ const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
325
+ for (const tr of toolResults) {
326
+ conversation.push({
327
+ role: "tool",
328
+ content: tr.content,
329
+ toolCallId: tr.toolCallId
330
+ });
331
+ }
332
+ }
333
+ this.logger.warn("[AI] chatWithTools max iterations reached, forcing final response");
334
+ const finalResult = await this.adapter.chat(conversation, {
335
+ ...chatOptions,
336
+ tools: void 0,
337
+ toolChoice: void 0
338
+ });
339
+ return finalResult;
340
+ }
341
+ };
342
+ // ── Tool Call Loop ────────────────────────────────────────────
343
+ /** Default maximum iterations for the tool call loop. */
344
+ _AIService.DEFAULT_MAX_ITERATIONS = 10;
345
+ var AIService = _AIService;
346
+
347
+ // src/routes/ai-routes.ts
348
+ var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
349
+ function validateMessage(raw) {
350
+ if (typeof raw !== "object" || raw === null) {
351
+ return "each message must be an object";
352
+ }
353
+ const msg = raw;
354
+ if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
355
+ return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
356
+ }
357
+ if (typeof msg.content !== "string") {
358
+ return "message.content must be a string";
359
+ }
360
+ return null;
361
+ }
362
+ function buildAIRoutes(aiService, conversationService, logger) {
363
+ return [
364
+ // ── Chat ────────────────────────────────────────────────────
365
+ {
366
+ method: "POST",
367
+ path: "/api/v1/ai/chat",
368
+ description: "Synchronous chat completion",
369
+ handler: async (req) => {
370
+ const { messages, options } = req.body ?? {};
371
+ if (!Array.isArray(messages) || messages.length === 0) {
372
+ return { status: 400, body: { error: "messages array is required" } };
373
+ }
374
+ for (const msg of messages) {
375
+ const err = validateMessage(msg);
376
+ if (err) return { status: 400, body: { error: err } };
377
+ }
378
+ try {
379
+ const result = await aiService.chat(messages, options);
380
+ return { status: 200, body: result };
381
+ } catch (err) {
382
+ logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
383
+ return { status: 500, body: { error: "Internal AI service error" } };
384
+ }
385
+ }
386
+ },
387
+ // ── Stream Chat (SSE) ──────────────────────────────────────
388
+ {
389
+ method: "POST",
390
+ path: "/api/v1/ai/chat/stream",
391
+ description: "SSE streaming chat completion",
392
+ handler: async (req) => {
393
+ const { messages, options } = req.body ?? {};
394
+ if (!Array.isArray(messages) || messages.length === 0) {
395
+ return { status: 400, body: { error: "messages array is required" } };
396
+ }
397
+ for (const msg of messages) {
398
+ const err = validateMessage(msg);
399
+ if (err) return { status: 400, body: { error: err } };
400
+ }
401
+ try {
402
+ if (!aiService.streamChat) {
403
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
404
+ }
405
+ const events = aiService.streamChat(messages, options);
406
+ return { status: 200, stream: true, events };
407
+ } catch (err) {
408
+ logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
409
+ return { status: 500, body: { error: "Internal AI service error" } };
410
+ }
411
+ }
412
+ },
413
+ // ── Complete ────────────────────────────────────────────────
414
+ {
415
+ method: "POST",
416
+ path: "/api/v1/ai/complete",
417
+ description: "Text completion",
418
+ handler: async (req) => {
419
+ const { prompt, options } = req.body ?? {};
420
+ if (!prompt || typeof prompt !== "string") {
421
+ return { status: 400, body: { error: "prompt string is required" } };
422
+ }
423
+ try {
424
+ const result = await aiService.complete(prompt, options);
425
+ return { status: 200, body: result };
426
+ } catch (err) {
427
+ logger.error("[AI Route] /complete error", err instanceof Error ? err : void 0);
428
+ return { status: 500, body: { error: "Internal AI service error" } };
429
+ }
430
+ }
431
+ },
432
+ // ── Models ──────────────────────────────────────────────────
433
+ {
434
+ method: "GET",
435
+ path: "/api/v1/ai/models",
436
+ description: "List available models",
437
+ handler: async () => {
438
+ try {
439
+ const models = aiService.listModels ? await aiService.listModels() : [];
440
+ return { status: 200, body: { models } };
441
+ } catch (err) {
442
+ logger.error("[AI Route] /models error", err instanceof Error ? err : void 0);
443
+ return { status: 500, body: { error: "Internal AI service error" } };
444
+ }
445
+ }
446
+ },
447
+ // ── Conversations ──────────────────────────────────────────
448
+ {
449
+ method: "POST",
450
+ path: "/api/v1/ai/conversations",
451
+ description: "Create a conversation",
452
+ handler: async (req) => {
453
+ try {
454
+ const options = req.body ?? {};
455
+ const conversation = await conversationService.create(options);
456
+ return { status: 201, body: conversation };
457
+ } catch (err) {
458
+ logger.error("[AI Route] POST /conversations error", err instanceof Error ? err : void 0);
459
+ return { status: 500, body: { error: "Internal AI service error" } };
460
+ }
461
+ }
462
+ },
463
+ {
464
+ method: "GET",
465
+ path: "/api/v1/ai/conversations",
466
+ description: "List conversations",
467
+ handler: async (req) => {
468
+ try {
469
+ const rawQuery = req.query ?? {};
470
+ const options = { ...rawQuery };
471
+ if (typeof rawQuery.limit === "string") {
472
+ const parsedLimit = Number(rawQuery.limit);
473
+ if (!Number.isFinite(parsedLimit) || parsedLimit <= 0 || !Number.isInteger(parsedLimit)) {
474
+ return { status: 400, body: { error: "Invalid limit parameter" } };
475
+ }
476
+ options.limit = parsedLimit;
477
+ }
478
+ const conversations = await conversationService.list(options);
479
+ return { status: 200, body: { conversations } };
480
+ } catch (err) {
481
+ logger.error("[AI Route] GET /conversations error", err instanceof Error ? err : void 0);
482
+ return { status: 500, body: { error: "Internal AI service error" } };
483
+ }
484
+ }
485
+ },
486
+ {
487
+ method: "POST",
488
+ path: "/api/v1/ai/conversations/:id/messages",
489
+ description: "Add message to a conversation",
490
+ handler: async (req) => {
491
+ const id = req.params?.id;
492
+ if (!id) {
493
+ return { status: 400, body: { error: "conversation id is required" } };
494
+ }
495
+ const message = req.body;
496
+ const validationError = validateMessage(message);
497
+ if (validationError) {
498
+ return { status: 400, body: { error: validationError } };
499
+ }
500
+ try {
501
+ const conversation = await conversationService.addMessage(id, message);
502
+ return { status: 200, body: conversation };
503
+ } catch (err) {
504
+ const msg = err instanceof Error ? err.message : String(err);
505
+ if (msg.includes("not found")) {
506
+ return { status: 404, body: { error: msg } };
507
+ }
508
+ logger.error("[AI Route] POST /conversations/:id/messages error", err instanceof Error ? err : void 0);
509
+ return { status: 500, body: { error: "Internal AI service error" } };
510
+ }
511
+ }
512
+ },
513
+ {
514
+ method: "DELETE",
515
+ path: "/api/v1/ai/conversations/:id",
516
+ description: "Delete a conversation",
517
+ handler: async (req) => {
518
+ const id = req.params?.id;
519
+ if (!id) {
520
+ return { status: 400, body: { error: "conversation id is required" } };
521
+ }
522
+ try {
523
+ await conversationService.delete(id);
524
+ return { status: 204 };
525
+ } catch (err) {
526
+ logger.error("[AI Route] DELETE /conversations/:id error", err instanceof Error ? err : void 0);
527
+ return { status: 500, body: { error: "Internal AI service error" } };
528
+ }
529
+ }
530
+ }
531
+ ];
532
+ }
533
+
534
+ // src/routes/agent-routes.ts
535
+ var ALLOWED_AGENT_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
536
+ function validateAgentMessage(raw) {
537
+ if (typeof raw !== "object" || raw === null) {
538
+ return "each message must be an object";
539
+ }
540
+ const msg = raw;
541
+ if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
542
+ return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
543
+ }
544
+ if (typeof msg.content !== "string") {
545
+ return "message.content must be a string";
546
+ }
547
+ return null;
548
+ }
549
+ function buildAgentRoutes(aiService, agentRuntime, logger) {
550
+ return [
551
+ {
552
+ method: "POST",
553
+ path: "/api/v1/ai/agents/:agentName/chat",
554
+ description: "Chat with a specific AI agent",
555
+ handler: async (req) => {
556
+ const agentName = req.params?.agentName;
557
+ if (!agentName) {
558
+ return { status: 400, body: { error: "agentName parameter is required" } };
559
+ }
560
+ const {
561
+ messages: rawMessages,
562
+ context: chatContext,
563
+ options: extraOptions
564
+ } = req.body ?? {};
565
+ if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
566
+ return { status: 400, body: { error: "messages array is required" } };
567
+ }
568
+ for (const msg of rawMessages) {
569
+ const err = validateAgentMessage(msg);
570
+ if (err) return { status: 400, body: { error: err } };
571
+ }
572
+ const agent = await agentRuntime.loadAgent(agentName);
573
+ if (!agent) {
574
+ return { status: 404, body: { error: `Agent "${agentName}" not found` } };
575
+ }
576
+ if (!agent.active) {
577
+ return { status: 403, body: { error: `Agent "${agentName}" is not active` } };
578
+ }
579
+ try {
580
+ const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext);
581
+ const agentOptions = agentRuntime.buildRequestOptions(
582
+ agent,
583
+ aiService.toolRegistry.getAll()
584
+ );
585
+ const safeOverrides = {};
586
+ if (extraOptions) {
587
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["temperature", "maxTokens", "stop"]);
588
+ for (const key of Object.keys(extraOptions)) {
589
+ if (ALLOWED_KEYS.has(key)) {
590
+ safeOverrides[key] = extraOptions[key];
591
+ }
592
+ }
593
+ }
594
+ const mergedOptions = { ...agentOptions, ...safeOverrides };
595
+ const fullMessages = [
596
+ ...systemMessages,
597
+ ...rawMessages
598
+ ];
599
+ const result = await aiService.chatWithTools(fullMessages, {
600
+ ...mergedOptions,
601
+ maxIterations: agent.planning?.maxIterations
602
+ });
603
+ return { status: 200, body: result };
604
+ } catch (err) {
605
+ logger.error(
606
+ "[AI Route] /agents/:agentName/chat error",
607
+ err instanceof Error ? err : void 0
608
+ );
609
+ return { status: 500, body: { error: "Internal AI service error" } };
610
+ }
611
+ }
612
+ }
613
+ ];
614
+ }
615
+
616
+ // src/conversation/objectql-conversation-service.ts
617
+ var import_node_crypto = require("crypto");
618
+ var CONVERSATIONS_OBJECT = "ai_conversations";
619
+ var MESSAGES_OBJECT = "ai_messages";
620
+ var CONVERSATION_ORDER = [
621
+ { field: "created_at", order: "asc" },
622
+ { field: "id", order: "asc" }
623
+ ];
624
+ var MESSAGE_ORDER = [
625
+ { field: "created_at", order: "asc" },
626
+ { field: "id", order: "asc" }
627
+ ];
628
+ var ObjectQLConversationService = class {
629
+ constructor(engine) {
630
+ this.engine = engine;
631
+ }
632
+ async create(options = {}) {
633
+ const now = (/* @__PURE__ */ new Date()).toISOString();
634
+ const id = `conv_${(0, import_node_crypto.randomUUID)()}`;
635
+ const record = {
636
+ id,
637
+ title: options.title ?? null,
638
+ agent_id: options.agentId ?? null,
639
+ user_id: options.userId ?? null,
640
+ metadata: options.metadata ? JSON.stringify(options.metadata) : null,
641
+ created_at: now,
642
+ updated_at: now
643
+ };
644
+ await this.engine.insert(CONVERSATIONS_OBJECT, record);
645
+ return {
646
+ id,
647
+ title: options.title,
648
+ agentId: options.agentId,
649
+ userId: options.userId,
650
+ messages: [],
651
+ createdAt: now,
652
+ updatedAt: now,
653
+ metadata: options.metadata
654
+ };
655
+ }
656
+ async get(conversationId) {
657
+ const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
658
+ where: { id: conversationId }
659
+ });
660
+ if (!row) return null;
661
+ const messages = await this.engine.find(MESSAGES_OBJECT, {
662
+ where: { conversation_id: conversationId },
663
+ orderBy: MESSAGE_ORDER
664
+ });
665
+ return this.toConversation(row, messages);
666
+ }
667
+ async list(options = {}) {
668
+ const where = {};
669
+ if (options.userId) where.user_id = options.userId;
670
+ if (options.agentId) where.agent_id = options.agentId;
671
+ if (options.cursor) {
672
+ const cursorRow = await this.engine.findOne(CONVERSATIONS_OBJECT, {
673
+ where: { id: options.cursor },
674
+ fields: ["created_at", "id"]
675
+ });
676
+ if (cursorRow) {
677
+ where.$or = [
678
+ { created_at: { $gt: cursorRow.created_at } },
679
+ { created_at: cursorRow.created_at, id: { $gt: cursorRow.id } }
680
+ ];
681
+ }
682
+ }
683
+ const rows = await this.engine.find(CONVERSATIONS_OBJECT, {
684
+ where: Object.keys(where).length > 0 ? where : void 0,
685
+ orderBy: CONVERSATION_ORDER,
686
+ limit: options.limit && options.limit > 0 ? options.limit : void 0
687
+ });
688
+ const conversations = await Promise.all(
689
+ rows.map(async (row) => {
690
+ const messages = await this.engine.find(MESSAGES_OBJECT, {
691
+ where: { conversation_id: row.id },
692
+ orderBy: MESSAGE_ORDER
693
+ });
694
+ return this.toConversation(row, messages);
695
+ })
696
+ );
697
+ return conversations;
698
+ }
699
+ async addMessage(conversationId, message) {
700
+ const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
701
+ where: { id: conversationId }
702
+ });
703
+ if (!row) {
704
+ throw new Error(`Conversation "${conversationId}" not found`);
705
+ }
706
+ const now = (/* @__PURE__ */ new Date()).toISOString();
707
+ const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
708
+ await this.engine.insert(MESSAGES_OBJECT, {
709
+ id: msgId,
710
+ conversation_id: conversationId,
711
+ role: message.role,
712
+ content: message.content,
713
+ tool_calls: message.toolCalls ? JSON.stringify(message.toolCalls) : null,
714
+ tool_call_id: message.toolCallId ?? null,
715
+ created_at: now
716
+ });
717
+ await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
718
+ where: { id: conversationId }
719
+ });
720
+ return await this.get(conversationId);
721
+ }
722
+ async delete(conversationId) {
723
+ await this.engine.delete(MESSAGES_OBJECT, {
724
+ where: { conversation_id: conversationId },
725
+ multi: true
726
+ });
727
+ await this.engine.delete(CONVERSATIONS_OBJECT, {
728
+ where: { id: conversationId }
729
+ });
730
+ }
731
+ // ── Private helpers ──────────────────────────────────────────────
732
+ /**
733
+ * Safely parse a JSON string, returning `undefined` on failure.
734
+ */
735
+ safeParse(value, fallback) {
736
+ if (!value) return void 0;
737
+ try {
738
+ return JSON.parse(value);
739
+ } catch {
740
+ return fallback;
741
+ }
742
+ }
743
+ /**
744
+ * Map a database row + message rows to an AIConversation.
745
+ */
746
+ toConversation(row, messageRows) {
747
+ return {
748
+ id: row.id,
749
+ title: row.title ?? void 0,
750
+ agentId: row.agent_id ?? void 0,
751
+ userId: row.user_id ?? void 0,
752
+ messages: messageRows.map((m) => this.toMessage(m)),
753
+ createdAt: row.created_at,
754
+ updatedAt: row.updated_at,
755
+ metadata: this.safeParse(row.metadata)
756
+ };
757
+ }
758
+ /**
759
+ * Map a database row to an AIMessage.
760
+ */
761
+ toMessage(row) {
762
+ const msg = {
763
+ role: row.role,
764
+ content: row.content
765
+ };
766
+ const toolCalls = this.safeParse(row.tool_calls);
767
+ if (toolCalls) {
768
+ msg.toolCalls = toolCalls;
769
+ }
770
+ if (row.tool_call_id) {
771
+ msg.toolCallId = row.tool_call_id;
772
+ }
773
+ return msg;
774
+ }
775
+ };
776
+
777
+ // src/objects/ai-conversation.object.ts
778
+ var import_data = require("@objectstack/spec/data");
779
+ var AiConversationObject = import_data.ObjectSchema.create({
780
+ namespace: "ai",
781
+ name: "conversations",
782
+ label: "AI Conversation",
783
+ pluralLabel: "AI Conversations",
784
+ icon: "message-square",
785
+ isSystem: true,
786
+ description: "Persistent AI conversation metadata",
787
+ fields: {
788
+ id: import_data.Field.text({
789
+ label: "Conversation ID",
790
+ required: true,
791
+ readonly: true
792
+ }),
793
+ title: import_data.Field.text({
794
+ label: "Title",
795
+ required: false,
796
+ maxLength: 500,
797
+ description: "Conversation title or summary"
798
+ }),
799
+ agent_id: import_data.Field.text({
800
+ label: "Agent ID",
801
+ required: false,
802
+ maxLength: 255,
803
+ description: "Associated AI agent identifier"
804
+ }),
805
+ user_id: import_data.Field.text({
806
+ label: "User ID",
807
+ required: false,
808
+ maxLength: 255,
809
+ description: "User who owns the conversation"
810
+ }),
811
+ metadata: import_data.Field.textarea({
812
+ label: "Metadata",
813
+ required: false,
814
+ description: "JSON-serialized conversation metadata"
815
+ }),
816
+ created_at: import_data.Field.datetime({
817
+ label: "Created At",
818
+ required: true,
819
+ defaultValue: "NOW()",
820
+ readonly: true
821
+ }),
822
+ updated_at: import_data.Field.datetime({
823
+ label: "Updated At",
824
+ required: true,
825
+ defaultValue: "NOW()",
826
+ readonly: true
827
+ })
828
+ },
829
+ indexes: [
830
+ { fields: ["user_id"] },
831
+ { fields: ["agent_id"] },
832
+ { fields: ["created_at"] }
833
+ ],
834
+ enable: {
835
+ trackHistory: false,
836
+ searchable: false,
837
+ apiEnabled: true,
838
+ apiMethods: ["get", "list", "create", "update", "delete"],
839
+ trash: false,
840
+ mru: false
841
+ }
842
+ });
843
+
844
+ // src/objects/ai-message.object.ts
845
+ var import_data2 = require("@objectstack/spec/data");
846
+ var AiMessageObject = import_data2.ObjectSchema.create({
847
+ namespace: "ai",
848
+ name: "messages",
849
+ label: "AI Message",
850
+ pluralLabel: "AI Messages",
851
+ icon: "message-circle",
852
+ isSystem: true,
853
+ description: "Individual messages within AI conversations",
854
+ fields: {
855
+ id: import_data2.Field.text({
856
+ label: "Message ID",
857
+ required: true,
858
+ readonly: true
859
+ }),
860
+ conversation_id: import_data2.Field.text({
861
+ label: "Conversation ID",
862
+ required: true,
863
+ description: "Foreign key to ai_conversations"
864
+ }),
865
+ role: import_data2.Field.select({
866
+ label: "Role",
867
+ required: true,
868
+ options: [
869
+ { label: "System", value: "system" },
870
+ { label: "User", value: "user" },
871
+ { label: "Assistant", value: "assistant" },
872
+ { label: "Tool", value: "tool" }
873
+ ]
874
+ }),
875
+ content: import_data2.Field.textarea({
876
+ label: "Content",
877
+ required: true,
878
+ description: "Message content"
879
+ }),
880
+ tool_calls: import_data2.Field.textarea({
881
+ label: "Tool Calls",
882
+ required: false,
883
+ description: "JSON-serialized tool calls (when role=assistant)"
884
+ }),
885
+ tool_call_id: import_data2.Field.text({
886
+ label: "Tool Call ID",
887
+ required: false,
888
+ maxLength: 255,
889
+ description: "ID of the tool call this message responds to (when role=tool)"
890
+ }),
891
+ created_at: import_data2.Field.datetime({
892
+ label: "Created At",
893
+ required: true,
894
+ defaultValue: "NOW()",
895
+ readonly: true
896
+ })
897
+ },
898
+ indexes: [
899
+ { fields: ["conversation_id"] },
900
+ { fields: ["conversation_id", "created_at"] }
901
+ ],
902
+ enable: {
903
+ trackHistory: false,
904
+ searchable: false,
905
+ apiEnabled: true,
906
+ apiMethods: ["get", "list", "create"],
907
+ trash: false,
908
+ mru: false
909
+ }
910
+ });
911
+
912
+ // src/tools/data-tools.ts
913
+ var MAX_QUERY_LIMIT = 200;
914
+ var DEFAULT_QUERY_LIMIT = 20;
915
+ var LIST_OBJECTS_TOOL = {
916
+ name: "list_objects",
917
+ description: "List all available data objects (tables) in the system. Returns object names and labels.",
918
+ parameters: {
919
+ type: "object",
920
+ properties: {},
921
+ additionalProperties: false
922
+ }
923
+ };
924
+ var DESCRIBE_OBJECT_TOOL = {
925
+ name: "describe_object",
926
+ description: "Get the schema (fields, types, labels) of a specific data object. Use this to understand the structure of a table before querying it.",
927
+ parameters: {
928
+ type: "object",
929
+ properties: {
930
+ objectName: {
931
+ type: "string",
932
+ description: "The snake_case name of the object to describe"
933
+ }
934
+ },
935
+ required: ["objectName"],
936
+ additionalProperties: false
937
+ }
938
+ };
939
+ var QUERY_RECORDS_TOOL = {
940
+ name: "query_records",
941
+ description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
942
+ parameters: {
943
+ type: "object",
944
+ properties: {
945
+ objectName: {
946
+ type: "string",
947
+ description: "The snake_case name of the object to query"
948
+ },
949
+ where: {
950
+ type: "object",
951
+ description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
952
+ },
953
+ fields: {
954
+ type: "array",
955
+ items: { type: "string" },
956
+ description: "List of field names to return (omit for all fields)"
957
+ },
958
+ orderBy: {
959
+ type: "array",
960
+ items: {
961
+ type: "object",
962
+ properties: {
963
+ field: { type: "string" },
964
+ order: { type: "string", enum: ["asc", "desc"] }
965
+ }
966
+ },
967
+ description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
968
+ },
969
+ limit: {
970
+ type: "number",
971
+ description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
972
+ },
973
+ offset: {
974
+ type: "number",
975
+ description: "Number of records to skip for pagination"
976
+ }
977
+ },
978
+ required: ["objectName"],
979
+ additionalProperties: false
980
+ }
981
+ };
982
+ var GET_RECORD_TOOL = {
983
+ name: "get_record",
984
+ description: "Get a single record by its ID from a data object.",
985
+ parameters: {
986
+ type: "object",
987
+ properties: {
988
+ objectName: {
989
+ type: "string",
990
+ description: "The snake_case name of the object"
991
+ },
992
+ recordId: {
993
+ type: "string",
994
+ description: "The unique ID of the record"
995
+ },
996
+ fields: {
997
+ type: "array",
998
+ items: { type: "string" },
999
+ description: "List of field names to return (omit for all fields)"
1000
+ }
1001
+ },
1002
+ required: ["objectName", "recordId"],
1003
+ additionalProperties: false
1004
+ }
1005
+ };
1006
+ var AGGREGATE_DATA_TOOL = {
1007
+ name: "aggregate_data",
1008
+ description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
1009
+ parameters: {
1010
+ type: "object",
1011
+ properties: {
1012
+ objectName: {
1013
+ type: "string",
1014
+ description: "The snake_case name of the object to aggregate"
1015
+ },
1016
+ aggregations: {
1017
+ type: "array",
1018
+ items: {
1019
+ type: "object",
1020
+ properties: {
1021
+ function: {
1022
+ type: "string",
1023
+ enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
1024
+ description: "Aggregation function"
1025
+ },
1026
+ field: {
1027
+ type: "string",
1028
+ description: "Field to aggregate (optional for count)"
1029
+ },
1030
+ alias: {
1031
+ type: "string",
1032
+ description: "Result column alias"
1033
+ }
1034
+ },
1035
+ required: ["function", "alias"]
1036
+ },
1037
+ description: "Aggregation definitions"
1038
+ },
1039
+ groupBy: {
1040
+ type: "array",
1041
+ items: { type: "string" },
1042
+ description: "Fields to group by"
1043
+ },
1044
+ where: {
1045
+ type: "object",
1046
+ description: "Filter conditions applied before aggregation"
1047
+ }
1048
+ },
1049
+ required: ["objectName", "aggregations"],
1050
+ additionalProperties: false
1051
+ }
1052
+ };
1053
+ var DATA_TOOL_DEFINITIONS = [
1054
+ LIST_OBJECTS_TOOL,
1055
+ DESCRIBE_OBJECT_TOOL,
1056
+ QUERY_RECORDS_TOOL,
1057
+ GET_RECORD_TOOL,
1058
+ AGGREGATE_DATA_TOOL
1059
+ ];
1060
+ function createListObjectsHandler(ctx) {
1061
+ return async () => {
1062
+ const objects = await ctx.metadataService.listObjects();
1063
+ const summary = objects.map((o) => ({
1064
+ name: o.name,
1065
+ label: o.label ?? o.name
1066
+ }));
1067
+ return JSON.stringify(summary);
1068
+ };
1069
+ }
1070
+ function createDescribeObjectHandler(ctx) {
1071
+ return async (args) => {
1072
+ const { objectName } = args;
1073
+ const objectDef = await ctx.metadataService.getObject(objectName);
1074
+ if (!objectDef) {
1075
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1076
+ }
1077
+ const def = objectDef;
1078
+ const fields = def.fields ?? {};
1079
+ const fieldSummary = {};
1080
+ for (const [key, f] of Object.entries(fields)) {
1081
+ fieldSummary[key] = {
1082
+ type: f.type,
1083
+ label: f.label ?? key,
1084
+ required: f.required ?? false,
1085
+ ...f.reference ? { reference: f.reference } : {},
1086
+ ...f.options ? { options: f.options } : {}
1087
+ };
1088
+ }
1089
+ return JSON.stringify({
1090
+ name: def.name,
1091
+ label: def.label ?? def.name,
1092
+ fields: fieldSummary
1093
+ });
1094
+ };
1095
+ }
1096
+ function createQueryRecordsHandler(ctx) {
1097
+ return async (args) => {
1098
+ const {
1099
+ objectName,
1100
+ where,
1101
+ fields,
1102
+ orderBy,
1103
+ limit,
1104
+ offset
1105
+ } = args;
1106
+ const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
1107
+ const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
1108
+ const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
1109
+ const records = await ctx.dataEngine.find(objectName, {
1110
+ where,
1111
+ fields,
1112
+ orderBy,
1113
+ limit: safeLimit,
1114
+ offset: safeOffset
1115
+ });
1116
+ return JSON.stringify({ count: records.length, records });
1117
+ };
1118
+ }
1119
+ function createGetRecordHandler(ctx) {
1120
+ return async (args) => {
1121
+ const { objectName, recordId, fields } = args;
1122
+ const record = await ctx.dataEngine.findOne(objectName, {
1123
+ where: { id: recordId },
1124
+ fields
1125
+ });
1126
+ if (!record) {
1127
+ return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
1128
+ }
1129
+ return JSON.stringify(record);
1130
+ };
1131
+ }
1132
+ var VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
1133
+ "count",
1134
+ "sum",
1135
+ "avg",
1136
+ "min",
1137
+ "max",
1138
+ "count_distinct"
1139
+ ]);
1140
+ function createAggregateDataHandler(ctx) {
1141
+ return async (args) => {
1142
+ const { objectName, aggregations, groupBy, where } = args;
1143
+ for (const a of aggregations) {
1144
+ if (!VALID_AGG_FUNCTIONS.has(a.function)) {
1145
+ return JSON.stringify({
1146
+ error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
1147
+ });
1148
+ }
1149
+ }
1150
+ const result = await ctx.dataEngine.aggregate(objectName, {
1151
+ where,
1152
+ groupBy,
1153
+ aggregations: aggregations.map((a) => ({
1154
+ function: a.function,
1155
+ field: a.field,
1156
+ alias: a.alias
1157
+ }))
1158
+ });
1159
+ return JSON.stringify(result);
1160
+ };
1161
+ }
1162
+ function registerDataTools(registry, context) {
1163
+ registry.register(LIST_OBJECTS_TOOL, createListObjectsHandler(context));
1164
+ registry.register(DESCRIBE_OBJECT_TOOL, createDescribeObjectHandler(context));
1165
+ registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
1166
+ registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
1167
+ registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
1168
+ }
1169
+
1170
+ // src/agent-runtime.ts
1171
+ var import_ai = require("@objectstack/spec/ai");
1172
+ var AgentRuntime = class {
1173
+ constructor(metadataService) {
1174
+ this.metadataService = metadataService;
1175
+ }
1176
+ // ── Public API ────────────────────────────────────────────────
1177
+ /**
1178
+ * Load and validate an agent definition by name.
1179
+ *
1180
+ * The raw metadata is validated through {@link AgentSchema} to ensure
1181
+ * required fields (`instructions`, `name`, `role`, etc.) are present
1182
+ * and well-typed. Returns `undefined` when the agent does not exist
1183
+ * or validation fails.
1184
+ */
1185
+ async loadAgent(agentName) {
1186
+ const raw = await this.metadataService.get("agent", agentName);
1187
+ if (!raw) return void 0;
1188
+ const result = import_ai.AgentSchema.safeParse(raw);
1189
+ if (!result.success) {
1190
+ return void 0;
1191
+ }
1192
+ return result.data;
1193
+ }
1194
+ /**
1195
+ * Build the system message(s) that should be prepended to the
1196
+ * conversation when chatting with the given agent.
1197
+ */
1198
+ buildSystemMessages(agent, context) {
1199
+ const parts = [];
1200
+ parts.push(agent.instructions);
1201
+ if (context) {
1202
+ const ctx = [];
1203
+ if (context.objectName) ctx.push(`Current object: ${context.objectName}`);
1204
+ if (context.recordId) ctx.push(`Selected record ID: ${context.recordId}`);
1205
+ if (context.viewName) ctx.push(`Current view: ${context.viewName}`);
1206
+ if (ctx.length > 0) {
1207
+ parts.push("\n--- Current Context ---\n" + ctx.join("\n"));
1208
+ }
1209
+ }
1210
+ return [{ role: "system", content: parts.join("\n") }];
1211
+ }
1212
+ /**
1213
+ * Derive {@link AIRequestOptions} from an agent definition.
1214
+ *
1215
+ * Tool references declared in `agent.tools` are resolved by name against
1216
+ * `availableTools` (i.e. the full set of ToolRegistry definitions).
1217
+ * Any unresolved references (tools the agent declares but that are not
1218
+ * registered) are silently skipped — this is intentional so that agents
1219
+ * can be defined before all tools are available.
1220
+ *
1221
+ * @param agent - The agent definition to derive options from
1222
+ * @param availableTools - All tool definitions currently registered in the ToolRegistry
1223
+ * @returns Request options with model config and resolved tool definitions
1224
+ */
1225
+ buildRequestOptions(agent, availableTools) {
1226
+ const options = {};
1227
+ if (agent.model) {
1228
+ options.model = agent.model.model;
1229
+ options.temperature = agent.model.temperature;
1230
+ options.maxTokens = agent.model.maxTokens;
1231
+ }
1232
+ if (agent.tools && agent.tools.length > 0) {
1233
+ const toolMap = new Map(availableTools.map((t) => [t.name, t]));
1234
+ const resolved = [];
1235
+ for (const ref of agent.tools) {
1236
+ const def = toolMap.get(ref.name);
1237
+ if (def) {
1238
+ resolved.push(def);
1239
+ }
1240
+ }
1241
+ if (resolved.length > 0) {
1242
+ options.tools = resolved;
1243
+ options.toolChoice = "auto";
1244
+ }
1245
+ }
1246
+ return options;
1247
+ }
1248
+ };
1249
+
1250
+ // src/agents/data-chat-agent.ts
1251
+ var DATA_CHAT_AGENT = {
1252
+ name: "data_chat",
1253
+ label: "Data Assistant",
1254
+ role: "Business Data Analyst",
1255
+ instructions: `You are a helpful data assistant that helps users explore and understand their business data through natural language.
1256
+
1257
+ Capabilities:
1258
+ - List available data objects (tables) and their schemas
1259
+ - Query records with filters, sorting, and pagination
1260
+ - Look up individual records by ID
1261
+ - Perform aggregations and statistical analysis (count, sum, avg, min, max)
1262
+
1263
+ Guidelines:
1264
+ 1. Always use the describe_object tool first to understand a table's structure before querying it.
1265
+ 2. Respect the user's current context \u2014 if they are viewing a specific object or record, use that as the default scope.
1266
+ 3. When presenting data, format it in a clear and readable way using markdown tables or bullet lists.
1267
+ 4. For large result sets, summarize the data and mention the total count.
1268
+ 5. When performing aggregations, explain the results in plain language.
1269
+ 6. If a query returns no results, suggest possible reasons and alternative queries.
1270
+ 7. Never expose internal IDs unless the user explicitly asks for them.
1271
+ 8. Always answer in the same language the user is using.`,
1272
+ model: {
1273
+ provider: "openai",
1274
+ model: "gpt-4",
1275
+ temperature: 0.3,
1276
+ maxTokens: 4096
1277
+ },
1278
+ tools: [
1279
+ { type: "query", name: "list_objects", description: "List all available data objects" },
1280
+ { type: "query", name: "describe_object", description: "Get schema/fields of a data object" },
1281
+ { type: "query", name: "query_records", description: "Query records with filters and pagination" },
1282
+ { type: "query", name: "get_record", description: "Get a single record by ID" },
1283
+ { type: "query", name: "aggregate_data", description: "Aggregate/statistics on data" }
1284
+ ],
1285
+ active: true,
1286
+ visibility: "global",
1287
+ guardrails: {
1288
+ maxTokensPerInvocation: 8192,
1289
+ maxExecutionTimeSec: 30,
1290
+ blockedTopics: ["delete_records", "drop_table", "alter_schema"]
1291
+ },
1292
+ planning: {
1293
+ strategy: "react",
1294
+ maxIterations: 5,
1295
+ allowReplan: false
1296
+ },
1297
+ memory: {
1298
+ shortTerm: {
1299
+ maxMessages: 20,
1300
+ maxTokens: 4096
1301
+ }
1302
+ }
1303
+ };
1304
+
1305
+ // src/plugin.ts
1306
+ var AIServicePlugin = class {
1307
+ constructor(options = {}) {
1308
+ this.name = "com.objectstack.service-ai";
1309
+ this.version = "1.0.0";
1310
+ this.type = "standard";
1311
+ this.dependencies = [];
1312
+ this.options = options;
1313
+ }
1314
+ async init(ctx) {
1315
+ let hasExisting = false;
1316
+ try {
1317
+ const existing = ctx.getService("ai");
1318
+ if (existing && typeof existing.chat === "function") {
1319
+ hasExisting = true;
1320
+ ctx.logger.debug("[AI] Found existing AI service, replacing");
1321
+ }
1322
+ } catch {
1323
+ }
1324
+ let conversationService = this.options.conversationService;
1325
+ if (!conversationService) {
1326
+ try {
1327
+ const engine = ctx.getService("data");
1328
+ if (engine && typeof engine.find === "function") {
1329
+ conversationService = new ObjectQLConversationService(engine);
1330
+ ctx.logger.info("[AI] Using ObjectQLConversationService (IDataEngine detected)");
1331
+ }
1332
+ } catch {
1333
+ }
1334
+ }
1335
+ const config = {
1336
+ adapter: this.options.adapter,
1337
+ logger: ctx.logger,
1338
+ conversationService
1339
+ };
1340
+ this.service = new AIService(config);
1341
+ if (hasExisting) {
1342
+ ctx.replaceService("ai", this.service);
1343
+ } else {
1344
+ ctx.registerService("ai", this.service);
1345
+ }
1346
+ ctx.registerService("app.com.objectstack.service-ai", {
1347
+ id: "com.objectstack.service-ai",
1348
+ name: "AI Service",
1349
+ version: "1.0.0",
1350
+ type: "plugin",
1351
+ namespace: "ai",
1352
+ objects: [AiConversationObject, AiMessageObject]
1353
+ });
1354
+ if (this.options.debug) {
1355
+ ctx.hook("ai:beforeChat", async (messages) => {
1356
+ ctx.logger.debug("[AI] Before chat", { messages });
1357
+ });
1358
+ }
1359
+ ctx.logger.info("[AI] Service initialized");
1360
+ }
1361
+ async start(ctx) {
1362
+ if (!this.service) return;
1363
+ try {
1364
+ const dataEngine = ctx.getService("data");
1365
+ const metadataService = ctx.getService("metadata");
1366
+ if (dataEngine && metadataService) {
1367
+ registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
1368
+ ctx.logger.info("[AI] Built-in data tools registered");
1369
+ const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
1370
+ if (!agentExists) {
1371
+ await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
1372
+ ctx.logger.info("[AI] data_chat agent registered");
1373
+ } else {
1374
+ ctx.logger.debug("[AI] data_chat agent already exists, skipping auto-registration");
1375
+ }
1376
+ }
1377
+ } catch {
1378
+ ctx.logger.debug("[AI] Data engine or metadata service not available, skipping data tools");
1379
+ }
1380
+ await ctx.trigger("ai:ready", this.service);
1381
+ const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
1382
+ try {
1383
+ const metadataService = ctx.getService("metadata");
1384
+ if (metadataService) {
1385
+ const agentRuntime = new AgentRuntime(metadataService);
1386
+ const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
1387
+ routes.push(...agentRoutes);
1388
+ }
1389
+ } catch {
1390
+ ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
1391
+ }
1392
+ await ctx.trigger("ai:routes", routes);
1393
+ ctx.logger.info(
1394
+ `[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
1395
+ );
1396
+ }
1397
+ async destroy() {
1398
+ this.service = void 0;
1399
+ }
1400
+ };
1401
+ // Annotate the CommonJS export names for ESM import in node:
1402
+ 0 && (module.exports = {
1403
+ AIService,
1404
+ AIServicePlugin,
1405
+ AgentRuntime,
1406
+ AiConversationObject,
1407
+ AiMessageObject,
1408
+ DATA_CHAT_AGENT,
1409
+ DATA_TOOL_DEFINITIONS,
1410
+ InMemoryConversationService,
1411
+ MemoryLLMAdapter,
1412
+ ObjectQLConversationService,
1413
+ ToolRegistry,
1414
+ buildAIRoutes,
1415
+ buildAgentRoutes,
1416
+ registerDataTools
1417
+ });
1418
+ //# sourceMappingURL=index.cjs.map