trellis 1.0.6 → 1.0.7
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/ai/index.js +688 -0
- package/dist/cli/server.js +141 -27258
- package/dist/cli/tql.js +2889 -45710
- package/dist/graph/index.js +2248 -0
- package/dist/index.js +88 -12417
- package/dist/kernel/logic-middleware.js +179 -0
- package/dist/kernel/middleware.js +0 -0
- package/dist/kernel/operations.js +32 -0
- package/dist/kernel/schema-middleware.js +34 -0
- package/dist/kernel/security-middleware.js +53 -0
- package/dist/kernel/trellis-kernel.js +2239 -0
- package/dist/kernel/workspace.js +91 -0
- package/dist/persist/backend.js +0 -0
- package/dist/persist/sqlite-backend.js +123 -0
- package/dist/query/index.js +1643 -0
- package/dist/server/index.js +3309 -0
- package/dist/store/eav-store.js +323 -0
- package/dist/workflows/index.js +3160 -0
- package/package.json +9 -3
- package/.//out//windows-style.json +0 -602
- package/bun.lock +0 -350
- package/dist/cli/iroh.linux-x64-gnu-2y4tmrmh.node +0 -0
- package/dist/cli/iroh.linux-x64-musl-50ncx5bz.node +0 -0
- package/index.ts +0 -29
- package/run-server.sh +0 -5
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
// src/ai/orchestrator.ts
|
|
2
|
+
import { openai } from "@ai-sdk/openai";
|
|
3
|
+
import { generateText, streamText, generateObject } from "ai";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
var IntentSchema = z.enum([
|
|
7
|
+
"conversation",
|
|
8
|
+
"question",
|
|
9
|
+
"task",
|
|
10
|
+
"query",
|
|
11
|
+
"command",
|
|
12
|
+
"help",
|
|
13
|
+
"error"
|
|
14
|
+
]);
|
|
15
|
+
var ActionSchema = z.enum([
|
|
16
|
+
"streamResponse",
|
|
17
|
+
"generateResponse",
|
|
18
|
+
"executeTask",
|
|
19
|
+
"runQuery",
|
|
20
|
+
"showHelp",
|
|
21
|
+
"none"
|
|
22
|
+
]);
|
|
23
|
+
var TaskTypeSchema = z.enum([
|
|
24
|
+
"creative_writing",
|
|
25
|
+
"analysis",
|
|
26
|
+
"calculation",
|
|
27
|
+
"code_generation",
|
|
28
|
+
"data_processing",
|
|
29
|
+
"general"
|
|
30
|
+
]);
|
|
31
|
+
var ComplexitySchema = z.enum(["simple", "moderate", "complex"]);
|
|
32
|
+
var SentimentSchema = z.enum(["positive", "negative", "neutral"]).optional();
|
|
33
|
+
var ToneSchema = z.enum(["formal", "casual", "urgent", "friendly", "technical"]).optional();
|
|
34
|
+
var MetaSchema = z.object({
|
|
35
|
+
intent: IntentSchema,
|
|
36
|
+
reason: z.string(),
|
|
37
|
+
action: ActionSchema,
|
|
38
|
+
confidence: z.number().min(0).max(1),
|
|
39
|
+
sentiment: SentimentSchema,
|
|
40
|
+
tone: ToneSchema,
|
|
41
|
+
taskType: TaskTypeSchema.optional(),
|
|
42
|
+
complexity: ComplexitySchema.optional(),
|
|
43
|
+
estimatedTime: z.number().optional()
|
|
44
|
+
});
|
|
45
|
+
var PlanSchema = z.object({
|
|
46
|
+
rationale: z.string(),
|
|
47
|
+
steps: z.array(z.string()).min(1),
|
|
48
|
+
assumptions: z.array(z.string()).optional(),
|
|
49
|
+
risks: z.array(z.string()).optional(),
|
|
50
|
+
revisedPrompt: z.string().optional(),
|
|
51
|
+
toolHints: z.array(z.string()).optional()
|
|
52
|
+
});
|
|
53
|
+
var PlanVoteSchema = z.object({
|
|
54
|
+
winningIndex: z.number().int().min(0),
|
|
55
|
+
reason: z.string()
|
|
56
|
+
});
|
|
57
|
+
var OrchestratorOptionsSchema = z.object({
|
|
58
|
+
stream: z.boolean().optional(),
|
|
59
|
+
maxRetries: z.number().min(0).max(5).default(2),
|
|
60
|
+
timeoutMs: z.number().positive().default(30000),
|
|
61
|
+
includeAnalysis: z.boolean().default(true),
|
|
62
|
+
fallbackToConversation: z.boolean().default(true),
|
|
63
|
+
useToT: z.boolean().default(false),
|
|
64
|
+
plannerModel: z.string().default("gpt-4o-mini"),
|
|
65
|
+
numThoughts: z.number().int().min(2).max(8).default(3)
|
|
66
|
+
}).passthrough();
|
|
67
|
+
var OrchestratorOk = z.object({
|
|
68
|
+
status: z.literal("ok"),
|
|
69
|
+
meta: MetaSchema,
|
|
70
|
+
payload: z.object({
|
|
71
|
+
requestId: z.string().uuid(),
|
|
72
|
+
prompt: z.string(),
|
|
73
|
+
options: z.record(z.string(), z.unknown()),
|
|
74
|
+
response: z.any(),
|
|
75
|
+
processingTimeMs: z.number(),
|
|
76
|
+
planning: z.object({
|
|
77
|
+
winningIndex: z.number(),
|
|
78
|
+
reason: z.string(),
|
|
79
|
+
plan: PlanSchema,
|
|
80
|
+
allPlansCount: z.number()
|
|
81
|
+
}).optional()
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
var OrchestratorErr = z.object({
|
|
85
|
+
status: z.literal("error"),
|
|
86
|
+
meta: MetaSchema,
|
|
87
|
+
payload: z.object({
|
|
88
|
+
requestId: z.string().uuid(),
|
|
89
|
+
prompt: z.string(),
|
|
90
|
+
options: z.record(z.string(), z.unknown()),
|
|
91
|
+
error: z.string(),
|
|
92
|
+
processingTimeMs: z.number(),
|
|
93
|
+
planning: z.object({
|
|
94
|
+
winningIndex: z.number(),
|
|
95
|
+
reason: z.string(),
|
|
96
|
+
plan: PlanSchema,
|
|
97
|
+
allPlansCount: z.number()
|
|
98
|
+
}).optional()
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
var OrchestratorResultSchema = z.union([OrchestratorOk, OrchestratorErr]);
|
|
102
|
+
var DEFAULT_MODEL = openai("gpt-4o-mini");
|
|
103
|
+
var CLASSIFICATION_PROMPT = `You are an intent classifier for a conversational AI orchestrator.
|
|
104
|
+
Analyze the user prompt and return ONLY a JSON object with these fields:
|
|
105
|
+
|
|
106
|
+
- intent: one of ${IntentSchema.options.join(", ")}
|
|
107
|
+
- reason: brief explanation (max 50 chars)
|
|
108
|
+
- action: one of ${ActionSchema.options.join(", ")}
|
|
109
|
+
- confidence: number 0-1
|
|
110
|
+
- sentiment: optional, one of positive/negative/neutral
|
|
111
|
+
- tone: optional, one of formal/casual/urgent/friendly/technical
|
|
112
|
+
- taskType: optional, one of ${TaskTypeSchema.options.join(", ")} (only for task intent)
|
|
113
|
+
- complexity: optional, one of ${ComplexitySchema.options.join(", ")} (only for task intent)
|
|
114
|
+
- estimatedTime: optional, rough time estimate in seconds (only for task intent)
|
|
115
|
+
|
|
116
|
+
Guidelines:
|
|
117
|
+
- "conversation": general chat, greetings, casual talk → streamResponse
|
|
118
|
+
- "question": seeking information → streamResponse
|
|
119
|
+
- "task": action requests → executeTask (include taskType, complexity, estimatedTime)
|
|
120
|
+
- "query": data/search requests → runQuery
|
|
121
|
+
- "command": system commands → executeTask
|
|
122
|
+
- "help": assistance requests → showHelp
|
|
123
|
+
|
|
124
|
+
Task type guidelines:
|
|
125
|
+
- "creative_writing": stories, poems, creative content
|
|
126
|
+
- "analysis": data analysis, research, evaluation
|
|
127
|
+
- "calculation": math, computations, formulas
|
|
128
|
+
- "code_generation": programming, scripts, technical solutions
|
|
129
|
+
- "data_processing": data manipulation, transformation
|
|
130
|
+
- "general": other tasks
|
|
131
|
+
|
|
132
|
+
User prompt: """{{PROMPT}}"""
|
|
133
|
+
|
|
134
|
+
Respond with valid JSON only:`;
|
|
135
|
+
|
|
136
|
+
class OrchestratorError extends Error {
|
|
137
|
+
code;
|
|
138
|
+
recoverable;
|
|
139
|
+
constructor(message, code, recoverable = true) {
|
|
140
|
+
super(message);
|
|
141
|
+
this.code = code;
|
|
142
|
+
this.recoverable = recoverable;
|
|
143
|
+
this.name = "OrchestratorError";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
var withTimeout = async (promise, timeoutMs) => {
|
|
147
|
+
const timeout = new Promise((_, reject) => {
|
|
148
|
+
setTimeout(() => reject(new OrchestratorError(`Operation timed out after ${timeoutMs}ms`, "TIMEOUT", true)), timeoutMs);
|
|
149
|
+
});
|
|
150
|
+
return Promise.race([promise, timeout]);
|
|
151
|
+
};
|
|
152
|
+
var parseJsonSafely = (text) => {
|
|
153
|
+
const cleaned = text.replace(/^```json\s*/, "").replace(/\s*```$/, "").replace(/^```\s*/, "").trim();
|
|
154
|
+
return JSON.parse(cleaned);
|
|
155
|
+
};
|
|
156
|
+
var word = (w) => new RegExp(`\\b${w}\\b`, "i");
|
|
157
|
+
var quickClassify = (prompt) => {
|
|
158
|
+
const p = prompt.trim();
|
|
159
|
+
if (word("help").test(p)) {
|
|
160
|
+
return {
|
|
161
|
+
intent: "help",
|
|
162
|
+
reason: "help keyword",
|
|
163
|
+
action: "showHelp",
|
|
164
|
+
confidence: 0.9
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (/[?]\s*$/.test(p) || /^(what|how|why|when|where)\b/i.test(p)) {
|
|
168
|
+
return {
|
|
169
|
+
intent: "question",
|
|
170
|
+
reason: "question pattern",
|
|
171
|
+
action: "streamResponse",
|
|
172
|
+
confidence: 0.8
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (word("write").test(p) || word("generate").test(p) || word("create").test(p)) {
|
|
176
|
+
const isCode = /(code|function|script|program)\b/i.test(p);
|
|
177
|
+
const isStory = /\b(story|poem|narrative|lyrics?)\b/i.test(p);
|
|
178
|
+
return {
|
|
179
|
+
intent: "task",
|
|
180
|
+
reason: "creation keyword",
|
|
181
|
+
action: "executeTask",
|
|
182
|
+
confidence: 0.85,
|
|
183
|
+
taskType: isCode ? "code_generation" : isStory ? "creative_writing" : "general",
|
|
184
|
+
complexity: /\b(short|simple)\b/i.test(p) ? "simple" : "moderate",
|
|
185
|
+
estimatedTime: isCode ? 30 : isStory ? 15 : 20
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
};
|
|
190
|
+
async function proposePlan({
|
|
191
|
+
plannerModel,
|
|
192
|
+
prompt
|
|
193
|
+
}) {
|
|
194
|
+
try {
|
|
195
|
+
const { object } = await generateObject({
|
|
196
|
+
model: openai(plannerModel),
|
|
197
|
+
temperature: 0.7,
|
|
198
|
+
maxRetries: 1,
|
|
199
|
+
prompt: [
|
|
200
|
+
"You are a planning assistant. Produce a concise plan to solve the user's request.",
|
|
201
|
+
"Prefer short, actionable steps. Include a revisedPrompt only if it materially improves the request.",
|
|
202
|
+
"Return a JSON object with: rationale (string), steps (array of strings), assumptions (optional array), risks (optional array), revisedPrompt (optional string).",
|
|
203
|
+
"",
|
|
204
|
+
`User prompt:
|
|
205
|
+
${prompt}`
|
|
206
|
+
].join(`
|
|
207
|
+
`),
|
|
208
|
+
schema: PlanSchema
|
|
209
|
+
});
|
|
210
|
+
return object;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn("[planner] Plan generation failed:", error instanceof Error ? error.message : String(error));
|
|
213
|
+
return {
|
|
214
|
+
rationale: "Fallback plan due to generation error",
|
|
215
|
+
steps: ["Analyze the request", "Provide a comprehensive response"],
|
|
216
|
+
assumptions: [],
|
|
217
|
+
risks: ["Planning failed, using fallback approach"]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function runToT({
|
|
222
|
+
plannerModel,
|
|
223
|
+
prompt,
|
|
224
|
+
numThoughts
|
|
225
|
+
}) {
|
|
226
|
+
try {
|
|
227
|
+
const candidates = await Promise.all(Array.from({ length: numThoughts }, () => proposePlan({ plannerModel, prompt })));
|
|
228
|
+
const indexed = candidates.map((p, i) => `#${i}
|
|
229
|
+
Rationale: ${p.rationale}
|
|
230
|
+
Steps:
|
|
231
|
+
- ${p.steps.join(`
|
|
232
|
+
- `)}
|
|
233
|
+
`).join(`
|
|
234
|
+
`);
|
|
235
|
+
const { object: vote } = await generateObject({
|
|
236
|
+
model: openai(plannerModel),
|
|
237
|
+
temperature: 0.2,
|
|
238
|
+
prompt: [
|
|
239
|
+
"You are a strict judge. Choose the single best plan index for correctness, clarity, and feasibility.",
|
|
240
|
+
"Return only JSON with the winningIndex (number) and reason (string).",
|
|
241
|
+
indexed
|
|
242
|
+
].join(`
|
|
243
|
+
|
|
244
|
+
`),
|
|
245
|
+
schema: PlanVoteSchema
|
|
246
|
+
});
|
|
247
|
+
const winningIndex = Math.max(0, Math.min(candidates.length - 1, vote.winningIndex));
|
|
248
|
+
const winningPlan = candidates[winningIndex];
|
|
249
|
+
if (!winningPlan) {
|
|
250
|
+
throw new Error("No winning plan found");
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
plan: winningPlan,
|
|
254
|
+
allPlans: candidates,
|
|
255
|
+
winningIndex,
|
|
256
|
+
reason: vote.reason
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.warn("[planner] ToT evaluation failed:", error instanceof Error ? error.message : String(error));
|
|
260
|
+
const fallbackPlan = await proposePlan({ plannerModel, prompt });
|
|
261
|
+
return {
|
|
262
|
+
plan: fallbackPlan,
|
|
263
|
+
allPlans: [fallbackPlan],
|
|
264
|
+
winningIndex: 0,
|
|
265
|
+
reason: "Fallback selection due to evaluation error"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
var generateResponse = async (prompt, options = {}) => {
|
|
270
|
+
const { stream = false, timeoutMs = 30000 } = options;
|
|
271
|
+
if (stream) {
|
|
272
|
+
const result = await streamText({ model: DEFAULT_MODEL, prompt });
|
|
273
|
+
return result.textStream;
|
|
274
|
+
} else {
|
|
275
|
+
const { text } = await withTimeout(generateText({ model: DEFAULT_MODEL, prompt }), timeoutMs);
|
|
276
|
+
return text;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var classifyIntentLLM = async (prompt, timeoutMs) => {
|
|
280
|
+
const classificationPrompt = CLASSIFICATION_PROMPT.replace("{{PROMPT}}", prompt);
|
|
281
|
+
const { text } = await withTimeout(generateText({
|
|
282
|
+
model: DEFAULT_MODEL,
|
|
283
|
+
maxRetries: 1,
|
|
284
|
+
prompt: classificationPrompt
|
|
285
|
+
}), timeoutMs);
|
|
286
|
+
const parsed = parseJsonSafely(text);
|
|
287
|
+
return MetaSchema.parse(parsed);
|
|
288
|
+
};
|
|
289
|
+
var generateFallbackMeta = (prompt, error) => {
|
|
290
|
+
const lowerPrompt = prompt.toLowerCase().trim();
|
|
291
|
+
if (lowerPrompt.includes("?") || lowerPrompt.startsWith("what") || lowerPrompt.startsWith("how")) {
|
|
292
|
+
return {
|
|
293
|
+
intent: "question",
|
|
294
|
+
reason: error ? `Fallback: question pattern (${error})` : "Fallback: question pattern detected",
|
|
295
|
+
action: "streamResponse",
|
|
296
|
+
confidence: 0.6
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (lowerPrompt.startsWith("help") || lowerPrompt.includes("help")) {
|
|
300
|
+
return {
|
|
301
|
+
intent: "help",
|
|
302
|
+
reason: error ? `Fallback: help request (${error})` : "Fallback: help pattern detected",
|
|
303
|
+
action: "showHelp",
|
|
304
|
+
confidence: 0.7
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
intent: "conversation",
|
|
309
|
+
reason: error ? `Fallback: default conversation (${error})` : "Fallback: default conversation",
|
|
310
|
+
action: "streamResponse",
|
|
311
|
+
confidence: error ? 0.3 : 0.5
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
var taskHandlers = {
|
|
315
|
+
code_generation: async ({ prompt, options }) => ({
|
|
316
|
+
kind: "ok",
|
|
317
|
+
response: await generateResponse(prompt, { timeoutMs: options.timeoutMs })
|
|
318
|
+
}),
|
|
319
|
+
creative_writing: async ({ prompt, meta, options }) => ({
|
|
320
|
+
kind: "ok",
|
|
321
|
+
response: await generateResponse(prompt, {
|
|
322
|
+
stream: meta.complexity === "complex" || /long|novella/i.test(prompt),
|
|
323
|
+
timeoutMs: options.timeoutMs
|
|
324
|
+
})
|
|
325
|
+
}),
|
|
326
|
+
analysis: async ({ prompt, options }) => ({
|
|
327
|
+
kind: "ok",
|
|
328
|
+
response: await generateResponse(prompt, { timeoutMs: options.timeoutMs })
|
|
329
|
+
}),
|
|
330
|
+
calculation: async ({ prompt, options }) => ({
|
|
331
|
+
kind: "ok",
|
|
332
|
+
response: await generateResponse(prompt, { timeoutMs: options.timeoutMs })
|
|
333
|
+
}),
|
|
334
|
+
general: async ({ prompt, options }) => ({
|
|
335
|
+
kind: "ok",
|
|
336
|
+
response: await generateResponse(prompt, { timeoutMs: options.timeoutMs })
|
|
337
|
+
})
|
|
338
|
+
};
|
|
339
|
+
async function routeTask(ctx) {
|
|
340
|
+
const { meta } = ctx;
|
|
341
|
+
const handler = meta.taskType && taskHandlers[meta.taskType] || taskHandlers.general;
|
|
342
|
+
if (!handler)
|
|
343
|
+
return { kind: "err", error: "No task handler." };
|
|
344
|
+
return handler(ctx);
|
|
345
|
+
}
|
|
346
|
+
var handlers = {
|
|
347
|
+
streamResponse: async ({ prompt, options }) => ({
|
|
348
|
+
kind: "ok",
|
|
349
|
+
response: await generateResponse(prompt, {
|
|
350
|
+
stream: true,
|
|
351
|
+
timeoutMs: options.timeoutMs
|
|
352
|
+
})
|
|
353
|
+
}),
|
|
354
|
+
generateResponse: async ({ prompt, options }) => ({
|
|
355
|
+
kind: "ok",
|
|
356
|
+
response: await generateResponse(prompt, {
|
|
357
|
+
stream: false,
|
|
358
|
+
timeoutMs: options.timeoutMs
|
|
359
|
+
})
|
|
360
|
+
}),
|
|
361
|
+
showHelp: async () => ({
|
|
362
|
+
kind: "ok",
|
|
363
|
+
response: "I can chat, answer questions, run tasks, or queries. What do you need?"
|
|
364
|
+
}),
|
|
365
|
+
runQuery: async () => ({
|
|
366
|
+
kind: "ok",
|
|
367
|
+
response: "[runQuery] Not implemented yet."
|
|
368
|
+
}),
|
|
369
|
+
none: async () => ({
|
|
370
|
+
kind: "ok",
|
|
371
|
+
response: "No action required."
|
|
372
|
+
}),
|
|
373
|
+
executeTask: routeTask
|
|
374
|
+
};
|
|
375
|
+
var orchestrate = async (prompt, rawOptions = {}) => {
|
|
376
|
+
const t0 = Date.now();
|
|
377
|
+
const requestId = randomUUID();
|
|
378
|
+
const options = OrchestratorOptionsSchema.parse(rawOptions);
|
|
379
|
+
if (!prompt?.trim())
|
|
380
|
+
throw new OrchestratorError("Prompt cannot be empty", "EMPTY_PROMPT", false);
|
|
381
|
+
let meta;
|
|
382
|
+
const tClassify0 = Date.now();
|
|
383
|
+
try {
|
|
384
|
+
meta = options.includeAnalysis ? quickClassify(prompt) ?? await classifyIntentLLM(prompt, options.timeoutMs) : generateFallbackMeta(prompt);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
meta = options.fallbackToConversation ? generateFallbackMeta(prompt, e instanceof Error ? e.message : String(e)) : (() => {
|
|
387
|
+
throw e;
|
|
388
|
+
})();
|
|
389
|
+
}
|
|
390
|
+
const classifyMs = Date.now() - tClassify0;
|
|
391
|
+
let effectivePrompt = prompt;
|
|
392
|
+
let planningInfo;
|
|
393
|
+
const shouldPlan = options.useToT && (meta.intent === "task" || meta.intent === "question" && (meta.complexity === "complex" || /plan|design|evaluate/i.test(prompt)));
|
|
394
|
+
if (shouldPlan) {
|
|
395
|
+
try {
|
|
396
|
+
const { plan, allPlans, winningIndex, reason } = await runToT({
|
|
397
|
+
plannerModel: options.plannerModel,
|
|
398
|
+
prompt,
|
|
399
|
+
numThoughts: options.numThoughts
|
|
400
|
+
});
|
|
401
|
+
if (plan.revisedPrompt && plan.revisedPrompt.trim().length > 0) {
|
|
402
|
+
effectivePrompt = plan.revisedPrompt;
|
|
403
|
+
}
|
|
404
|
+
planningInfo = {
|
|
405
|
+
winningIndex,
|
|
406
|
+
reason,
|
|
407
|
+
plan,
|
|
408
|
+
allPlansCount: allPlans.length
|
|
409
|
+
};
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.warn("[planner] ToT failed:", e instanceof Error ? e.message : String(e));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const tExec0 = Date.now();
|
|
415
|
+
let result;
|
|
416
|
+
try {
|
|
417
|
+
const handler = handlers[meta.action] ?? handlers.generateResponse;
|
|
418
|
+
result = await handler({ prompt: effectivePrompt, meta, options });
|
|
419
|
+
} catch (e) {
|
|
420
|
+
result = {
|
|
421
|
+
kind: "err",
|
|
422
|
+
error: `Action failed: ${e instanceof Error ? e.message : String(e)}`
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const execMs = Date.now() - tExec0;
|
|
426
|
+
const processingTimeMs = Date.now() - t0;
|
|
427
|
+
console.debug(JSON.stringify({
|
|
428
|
+
requestId,
|
|
429
|
+
classifyMs,
|
|
430
|
+
execMs,
|
|
431
|
+
processingTimeMs,
|
|
432
|
+
action: meta.action,
|
|
433
|
+
taskType: meta.taskType
|
|
434
|
+
}));
|
|
435
|
+
if (result.kind === "ok") {
|
|
436
|
+
return {
|
|
437
|
+
status: "ok",
|
|
438
|
+
meta,
|
|
439
|
+
payload: {
|
|
440
|
+
requestId,
|
|
441
|
+
prompt,
|
|
442
|
+
options: rawOptions,
|
|
443
|
+
response: result.response,
|
|
444
|
+
processingTimeMs,
|
|
445
|
+
planning: planningInfo
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
} else {
|
|
449
|
+
return {
|
|
450
|
+
status: "error",
|
|
451
|
+
meta,
|
|
452
|
+
payload: {
|
|
453
|
+
requestId,
|
|
454
|
+
prompt,
|
|
455
|
+
options: rawOptions,
|
|
456
|
+
error: result.error,
|
|
457
|
+
processingTimeMs,
|
|
458
|
+
planning: planningInfo
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
var quickOrchestrate = async (prompt, stream = false) => {
|
|
464
|
+
const result = await orchestrate(prompt, { stream, includeAnalysis: false });
|
|
465
|
+
if (result.status === "error") {
|
|
466
|
+
throw new Error(result.payload.error);
|
|
467
|
+
}
|
|
468
|
+
return typeof result.payload.response === "string" ? result.payload.response : "No response generated";
|
|
469
|
+
};
|
|
470
|
+
var analyzeIntent = async (prompt) => {
|
|
471
|
+
const result = await orchestrate(prompt, { includeAnalysis: true });
|
|
472
|
+
return result.meta;
|
|
473
|
+
};
|
|
474
|
+
var processQuery = async (query, context) => {
|
|
475
|
+
try {
|
|
476
|
+
const processedQuery = handleCommonPatterns(query);
|
|
477
|
+
const entityType = identifyEntityType(processedQuery);
|
|
478
|
+
const attributes = Array.isArray(context.catalog) ? context.catalog.map((a) => a.attribute).filter(Boolean) : [];
|
|
479
|
+
const aliasPairs = [];
|
|
480
|
+
const findAttr = (needle) => attributes.find((a) => needle.test(a));
|
|
481
|
+
const questionAttr = findAttr(/question$/i);
|
|
482
|
+
const responseAttr = findAttr(/response$/i);
|
|
483
|
+
const cotAttr = findAttr(/(complex[_\-.]?cot|chain[_\-.]?of[_\-.]?thought)/i);
|
|
484
|
+
if (questionAttr)
|
|
485
|
+
aliasPairs.push({ alias: "question", attr: questionAttr });
|
|
486
|
+
if (responseAttr)
|
|
487
|
+
aliasPairs.push({ alias: "response", attr: responseAttr });
|
|
488
|
+
if (cotAttr)
|
|
489
|
+
aliasPairs.push({ alias: "reasoning", attr: cotAttr });
|
|
490
|
+
const aliasInfo = aliasPairs.map(({ alias, attr }) => `- ${alias} ⇒ ${attr}`).join(`
|
|
491
|
+
`);
|
|
492
|
+
const catalogInfo = context.catalog.map((attr) => `- ${attr.attribute}:${attr.type} e.g. ${attr.examples.join(", ")}`).join(`
|
|
493
|
+
`);
|
|
494
|
+
const prompt = `You are an expert at converting natural language queries to EQL-S (EAV Query Language - Strict).
|
|
495
|
+
|
|
496
|
+
Available attributes in the dataset:
|
|
497
|
+
${catalogInfo}
|
|
498
|
+
|
|
499
|
+
Attribute aliases you MUST use if the user mentions the alias term:
|
|
500
|
+
${aliasInfo || "- (none)"}
|
|
501
|
+
|
|
502
|
+
Data statistics: ${JSON.stringify(context.dataStats, null, 2)}
|
|
503
|
+
|
|
504
|
+
EQL-S Grammar Rules:
|
|
505
|
+
- Use FIND <type> AS ?var to specify entity type and variable
|
|
506
|
+
- Use WHERE clause for conditions
|
|
507
|
+
- Use RETURN clause to specify output fields
|
|
508
|
+
- Use ORDER BY for sorting, LIMIT for result limits
|
|
509
|
+
- Operators: = != > >= < <= BETWEEN ... AND ... CONTAINS MATCHES
|
|
510
|
+
- For string pattern matching use MATCHES with regex patterns: MATCHES /pattern/
|
|
511
|
+
- For "starts with", use MATCHES /^prefix/
|
|
512
|
+
- For "ends with", use MATCHES /suffix$/
|
|
513
|
+
- For "contains", use MATCHES /text/ or CONTAINS "text"
|
|
514
|
+
- Regex literals must use forward slashes: /pattern/
|
|
515
|
+
|
|
516
|
+
STRICT formatting rules for this dataset:
|
|
517
|
+
- Entity type MUST be "default" unless explicitly specified. Use: FIND default AS ?e
|
|
518
|
+
- Only use attribute names EXACTLY as listed above (e.g., ${attributes.slice(0, 3).join(", ")} ...). Do not invent attributes.
|
|
519
|
+
- NEVER use numeric property access like ?e.0 or ?e.6 — that is invalid.
|
|
520
|
+
- If the user refers to "question", "answer/response", or "reasoning/chain of thought",
|
|
521
|
+
use these attributes if available: ${[
|
|
522
|
+
questionAttr,
|
|
523
|
+
responseAttr,
|
|
524
|
+
cotAttr
|
|
525
|
+
].filter(Boolean).join(", ") || "(none)"}
|
|
526
|
+
|
|
527
|
+
Entity type detected: ${entityType || "item"}
|
|
528
|
+
- Avoid using the IN operator for string matching
|
|
529
|
+
- Variables must start with ? (e.g., ?p, ?user)
|
|
530
|
+
- Strings must be in double quotes
|
|
531
|
+
- Regex patterns should be in /pattern/ format but PREFER CONTAINS when possible
|
|
532
|
+
|
|
533
|
+
Query Pattern Examples (adapt to available attributes):
|
|
534
|
+
- "show me [entities] with more than X [numeric_attribute]" → FIND <type> AS ?e WHERE ?e.<attribute> > X RETURN ?e
|
|
535
|
+
- "find [entities] containing [text]" → FIND <type> AS ?e WHERE ?e.<text_attribute> CONTAINS "text" RETURN ?e
|
|
536
|
+
- "[entities] tagged with [value]" → FIND <type> AS ?e WHERE ?e.<tag_attribute> = "value" RETURN ?e
|
|
537
|
+
- "[entities] between X and Y [units]" → FIND <type> AS ?e WHERE ?e.<numeric_attribute> BETWEEN X AND Y RETURN ?e
|
|
538
|
+
- "[entities] that start with [prefix]" → FIND <type> AS ?e WHERE ?e.<text_attribute> MATCHES /^prefix/ RETURN ?e.<text_attribute>
|
|
539
|
+
- "list [category] [entities]" → FIND <type> AS ?e WHERE ?e.<category_attribute> = "category" RETURN ?e
|
|
540
|
+
|
|
541
|
+
Convert this natural language query to EQL-S: "${processedQuery}"
|
|
542
|
+
|
|
543
|
+
Output ONLY the EQL-S query, no explanations or additional text.`;
|
|
544
|
+
const result = await generateText({
|
|
545
|
+
model: openai("gpt-4o-mini"),
|
|
546
|
+
prompt,
|
|
547
|
+
temperature: 0.1
|
|
548
|
+
});
|
|
549
|
+
let eqlsQuery = result.text.trim();
|
|
550
|
+
if (eqlsQuery.startsWith("```") && eqlsQuery.endsWith("```")) {
|
|
551
|
+
eqlsQuery = eqlsQuery.slice(3, -3).trim();
|
|
552
|
+
const firstLine = eqlsQuery.split(`
|
|
553
|
+
`)[0];
|
|
554
|
+
if (firstLine && !firstLine.includes(" ") && firstLine.length < 20) {
|
|
555
|
+
eqlsQuery = eqlsQuery.split(`
|
|
556
|
+
`).slice(1).join(`
|
|
557
|
+
`).trim();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (!eqlsQuery.startsWith("FIND")) {
|
|
561
|
+
return { error: "Generated query does not start with FIND" };
|
|
562
|
+
}
|
|
563
|
+
eqlsQuery = eqlsQuery.replace(/^(FIND)\s+([A-Za-z_][A-Za-z0-9_-]*)\s+AS\s+\?[A-Za-z_][A-Za-z0-9_]*/i, (m, findKw) => {
|
|
564
|
+
const varMatch = eqlsQuery.match(/AS\s+(\?[A-Za-z_][A-Za-z0-9_]*)/i);
|
|
565
|
+
const v = varMatch ? varMatch[1] : "?e";
|
|
566
|
+
return `${findKw} default AS ${v}`;
|
|
567
|
+
});
|
|
568
|
+
if (/\?[a-zA-Z_]\w*\.\d+/.test(eqlsQuery)) {
|
|
569
|
+
const wantsQuestion = /hallucinat|question|prompt/i.test(processedQuery);
|
|
570
|
+
const replacementAttr = wantsQuestion ? questionAttr : responseAttr || questionAttr || attributes[0];
|
|
571
|
+
if (replacementAttr) {
|
|
572
|
+
eqlsQuery = eqlsQuery.replace(/(\?[a-zA-Z_]\w*)\.\d+/g, `$1.${replacementAttr}`);
|
|
573
|
+
} else {
|
|
574
|
+
return { error: "Generated query used numeric property access (invalid)" };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const attrRefs = Array.from(eqlsQuery.matchAll(/\?[a-zA-Z_]\w*\.([A-Za-z0-9_.-]+)/g)).map((m) => m[1]).filter((a) => typeof a === "string");
|
|
578
|
+
const unknown = attrRefs.filter((a) => !attributes.includes(a));
|
|
579
|
+
if (unknown.length) {
|
|
580
|
+
let repaired = eqlsQuery;
|
|
581
|
+
for (const unk of unknown) {
|
|
582
|
+
const lower = unk.toLowerCase();
|
|
583
|
+
const alias = aliasPairs.find(({ alias: alias2 }) => lower.includes(alias2));
|
|
584
|
+
if (alias) {
|
|
585
|
+
repaired = repaired.replace(new RegExp(`(?[a-zA-Z_]\\w*.)${unk.replace(/[-/\\.^$*+?()|[\]{}]/g, "\\$&")}`, "g"), `$1${alias.attr}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (repaired !== eqlsQuery) {
|
|
589
|
+
eqlsQuery = repaired;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { eqlsQuery };
|
|
593
|
+
} catch (error) {
|
|
594
|
+
return {
|
|
595
|
+
error: `Failed to process query: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
function identifyEntityType(query) {
|
|
600
|
+
const normalizedQuery = query.toLowerCase();
|
|
601
|
+
const entityPatterns = [
|
|
602
|
+
/(?:find|show|list|get|search)\s+([a-z]+s?)\b/i,
|
|
603
|
+
/\b([a-z]+s?)\s+(?:that|with|containing|having)\b/i,
|
|
604
|
+
/\b([a-z]+s?)'?s?\s+[a-z]+/i
|
|
605
|
+
];
|
|
606
|
+
for (const pattern of entityPatterns) {
|
|
607
|
+
const match = normalizedQuery.match(pattern);
|
|
608
|
+
if (match && match[1]) {
|
|
609
|
+
let entityType = match[1].toLowerCase();
|
|
610
|
+
if (entityType.endsWith("ies")) {
|
|
611
|
+
entityType = entityType.slice(0, -3) + "y";
|
|
612
|
+
} else if (entityType.endsWith("es")) {
|
|
613
|
+
entityType = entityType.slice(0, -2);
|
|
614
|
+
} else if (entityType.endsWith("s") && entityType.length > 3) {
|
|
615
|
+
entityType = entityType.slice(0, -1);
|
|
616
|
+
}
|
|
617
|
+
const excludeWords = ["this", "that", "the", "and", "or", "but", "with", "from", "by", "at", "in", "on", "to", "for", "of", "as", "is", "are", "was", "were", "be", "been", "have", "has", "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "can", "all", "any", "some", "more", "most", "less", "least", "first", "last", "next", "previous"];
|
|
618
|
+
if (!excludeWords.includes(entityType) && entityType.length > 2) {
|
|
619
|
+
return entityType;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return "item";
|
|
624
|
+
}
|
|
625
|
+
function handleCommonPatterns(query) {
|
|
626
|
+
let normalizedQuery = query.trim().toLowerCase();
|
|
627
|
+
if (normalizedQuery.includes("start with") || normalizedQuery.includes("starts with") || normalizedQuery.includes("beginning with") || normalizedQuery.includes("begins with")) {
|
|
628
|
+
const pattern = /(?:starts?|begins?|beginning) with\s+(?:the letter\s+)?['"]?([a-zA-Z0-9]+)['"]?/i;
|
|
629
|
+
const match = query.match(pattern);
|
|
630
|
+
if (match && match[1]) {
|
|
631
|
+
const prefix = match[1];
|
|
632
|
+
normalizedQuery = normalizedQuery.replace(/(?:starts?|begins?|beginning) with\s+(?:the letter\s+)?['"]?([a-z0-9]+)['"]?/i, `matches /^${prefix}/`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (normalizedQuery.includes("end with") || normalizedQuery.includes("ends with") || normalizedQuery.includes("ending with")) {
|
|
636
|
+
const pattern = /(?:ends?|ending) with\s+(?:the letter\s+)?['"]?([a-zA-Z0-9]+)['"]?/i;
|
|
637
|
+
const match = query.match(pattern);
|
|
638
|
+
if (match && match[1]) {
|
|
639
|
+
const suffix = match[1];
|
|
640
|
+
normalizedQuery = normalizedQuery.replace(/(?:ends?|ending) with\s+(?:the letter\s+)?['"]?([a-z0-9]+)['"]?/i, `matches /${suffix}$/`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (normalizedQuery.includes("contain") || normalizedQuery.includes("having") || normalizedQuery.includes("with") && normalizedQuery.includes("in the name")) {
|
|
644
|
+
const patterns = [
|
|
645
|
+
/containing\s+(?:the text\s+)?['"]([^'"]+)['"]/i,
|
|
646
|
+
/contains\s+(?:the text\s+)?['"]([^'"]+)['"]/i,
|
|
647
|
+
/with\s+['"]([^'"]+)['"]\s+in the (?:name|title|text)/i,
|
|
648
|
+
/having\s+['"]([^'"]+)['"]\s+in/i
|
|
649
|
+
];
|
|
650
|
+
for (const pattern of patterns) {
|
|
651
|
+
const match = query.match(pattern);
|
|
652
|
+
if (match && match[1]) {
|
|
653
|
+
const text = match[1];
|
|
654
|
+
normalizedQuery = normalizedQuery.replace(pattern, `matches /${text}/`);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return normalizedQuery;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/ai/nl-query-provider.ts
|
|
663
|
+
class DefaultNLQueryProvider {
|
|
664
|
+
options;
|
|
665
|
+
name = "default-orchestrator";
|
|
666
|
+
constructor(options = {}) {
|
|
667
|
+
this.options = options;
|
|
668
|
+
}
|
|
669
|
+
async translate(nl, context) {
|
|
670
|
+
const catalog = context?.catalog || this.options.catalog || [];
|
|
671
|
+
const dataStats = context?.dataStats || this.options.dataStats || { totalFacts: 0 };
|
|
672
|
+
const result = await processQuery(nl, { catalog, dataStats });
|
|
673
|
+
if (result.error) {
|
|
674
|
+
throw new Error(`NL translation failed: ${result.error}`);
|
|
675
|
+
}
|
|
676
|
+
if (!result.eqlsQuery) {
|
|
677
|
+
throw new Error("NL translation failed: no query generated");
|
|
678
|
+
}
|
|
679
|
+
return result.eqlsQuery;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
export {
|
|
683
|
+
quickOrchestrate,
|
|
684
|
+
orchestrate,
|
|
685
|
+
generateResponse,
|
|
686
|
+
analyzeIntent,
|
|
687
|
+
DefaultNLQueryProvider
|
|
688
|
+
};
|