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