trellis 1.0.6 → 1.0.8

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.
@@ -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
+ };