@duckcodeailabs/dql-agent 1.6.28 → 1.6.30
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/agent-run-engine.d.ts +37 -3
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +47 -3
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +30 -0
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +3 -1
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/answer-loop.d.ts +8 -0
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +14 -2
- package/dist/answer-loop.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +22 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +51 -0
- package/dist/intent-controller.js.map +1 -1
- package/dist/metadata/catalog.d.ts +1 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +12 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/providers/claude.d.ts +3 -0
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +69 -0
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts +1 -0
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +62 -0
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +42 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +12 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +14 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/router.d.ts +61 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +246 -0
- package/dist/router.js.map +1 -0
- package/dist/synthesize.d.ts +66 -0
- package/dist/synthesize.d.ts.map +1 -0
- package/dist/synthesize.js +137 -0
- package/dist/synthesize.js.map +1 -0
- package/package.json +6 -6
package/dist/router.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid router — deterministic-first, LLM-assisted for the ambiguous middle.
|
|
3
|
+
*
|
|
4
|
+
* The engine already routes deterministically via {@link decideAgentAction}. That
|
|
5
|
+
* cascade is fast, offline, and confident for the clear cases (a strong certified
|
|
6
|
+
* match, an explicit "build me a dashboard", an obvious greeting). But paraphrased
|
|
7
|
+
* or implicit analytical asks land at low confidence and misroute — which is why
|
|
8
|
+
* users end up clicking "Dig deeper" by hand.
|
|
9
|
+
*
|
|
10
|
+
* This router keeps the deterministic decision when it is confident (>= the
|
|
11
|
+
* threshold) — so certified fast paths and greetings stay 0-LLM — and only spends
|
|
12
|
+
* ONE cheap classification call when the heuristics are unsure. The completion is
|
|
13
|
+
* injected (provider-agnostic, like the planner and narrator); any failure falls
|
|
14
|
+
* back to the deterministic decision unchanged. Results are cached so a repeated
|
|
15
|
+
* question never pays twice.
|
|
16
|
+
*/
|
|
17
|
+
import { classifyConversationalTurn, decideAgentAction, } from "./intent-controller.js";
|
|
18
|
+
const DEFAULT_THRESHOLD = 0.7;
|
|
19
|
+
const DEFAULT_CACHE_SIZE = 200;
|
|
20
|
+
const DEFAULT_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Map a router category to the fine-grained {@link MetadataAgentIntent} so the
|
|
23
|
+
* downstream answer loop keeps its existing behavior. Only used to enrich the
|
|
24
|
+
* decision — routing itself keys off `action`/`category`.
|
|
25
|
+
*/
|
|
26
|
+
function intentForCategory(category) {
|
|
27
|
+
switch (category) {
|
|
28
|
+
case "data_lookup":
|
|
29
|
+
return "ad_hoc_ranking";
|
|
30
|
+
case "data_analysis":
|
|
31
|
+
return "driver_breakdown";
|
|
32
|
+
case "general_knowledge":
|
|
33
|
+
return "definition_lookup";
|
|
34
|
+
default:
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Translate a validated classification into an engine {@link IntentDecision}. */
|
|
39
|
+
function classificationToDecision(classification, base) {
|
|
40
|
+
const followsUp = base.followsUp;
|
|
41
|
+
const common = {
|
|
42
|
+
category: classification.category,
|
|
43
|
+
depth: classification.depth,
|
|
44
|
+
source: "llm",
|
|
45
|
+
followsUp,
|
|
46
|
+
};
|
|
47
|
+
switch (classification.category) {
|
|
48
|
+
case "conversational":
|
|
49
|
+
return { action: "converse", confidence: 0.9, reason: classification.rationale, conversationalKind: "smalltalk", ...common };
|
|
50
|
+
case "capability":
|
|
51
|
+
return { action: "converse", confidence: 0.9, reason: classification.rationale, conversationalKind: "meta_capability", ...common };
|
|
52
|
+
case "general_knowledge":
|
|
53
|
+
// Rendered as a conversation reply, but tagged general_knowledge downstream.
|
|
54
|
+
return { action: "converse", confidence: 0.85, reason: classification.rationale, ...common };
|
|
55
|
+
case "app":
|
|
56
|
+
return { action: "compose_app", confidence: 0.82, reason: classification.rationale, ...common };
|
|
57
|
+
case "data_analysis":
|
|
58
|
+
return { action: "investigate", confidence: 0.8, reason: classification.rationale, ...common };
|
|
59
|
+
case "authoring":
|
|
60
|
+
return { action: "answer", confidence: 0.75, reason: classification.rationale, ...common };
|
|
61
|
+
case "data_lookup":
|
|
62
|
+
return { action: "answer", confidence: 0.75, reason: classification.rationale, ...common };
|
|
63
|
+
case "unclear":
|
|
64
|
+
default:
|
|
65
|
+
return {
|
|
66
|
+
action: "clarify",
|
|
67
|
+
confidence: 0.6,
|
|
68
|
+
reason: classification.rationale,
|
|
69
|
+
clarifyingQuestion: classification.needsClarification && classification.clarifyingQuestion
|
|
70
|
+
? classification.clarifyingQuestion
|
|
71
|
+
: base.clarifyingQuestion,
|
|
72
|
+
...common,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function buildSystemPrompt() {
|
|
77
|
+
return [
|
|
78
|
+
"You classify a user's turn in DQL, a governed analytics notebook, so the agent routes it well.",
|
|
79
|
+
"Pick ONE category:",
|
|
80
|
+
"- conversational: greeting, thanks, small talk — no data or knowledge needed.",
|
|
81
|
+
"- capability: asking what the assistant/DQL can do.",
|
|
82
|
+
"- general_knowledge: a factual question answerable from world knowledge, NOT the user's data (e.g. 'what is dbt?').",
|
|
83
|
+
"- data_lookup: a specific value/ranking answerable from the user's governed data (e.g. 'total revenue', 'top 10 customers').",
|
|
84
|
+
"- data_analysis: why / root-cause / driver / breakdown / comparison / trend / anomaly — needs multi-step investigation.",
|
|
85
|
+
"- authoring: user wants a SQL cell or a DQL block created.",
|
|
86
|
+
"- app: user wants a dashboard / app / standing view assembled.",
|
|
87
|
+
"- unclear: a real request but missing the business object, measure, or grain needed to proceed.",
|
|
88
|
+
"Also pick depth: 'deep' for data_analysis or anything needing several steps; otherwise 'quick'.",
|
|
89
|
+
"If unclear, set needsClarification true and give ONE sharp clarifyingQuestion.",
|
|
90
|
+
"Respond with ONLY a JSON object, no prose, no code fences:",
|
|
91
|
+
'{"category": string, "depth": "quick"|"deep", "needsClarification": boolean, "clarifyingQuestion"?: string, "rationale": string}',
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
function buildUserPrompt(request, catalogContext) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
lines.push(`Turn: ${request.question}`);
|
|
97
|
+
if (request.history?.length) {
|
|
98
|
+
const recent = request.history.slice(-4).map((turn) => `${turn.role}: ${turn.text}`).join("\n");
|
|
99
|
+
lines.push(`Recent conversation:\n${recent}`);
|
|
100
|
+
}
|
|
101
|
+
if (request.signals)
|
|
102
|
+
lines.push(`Retrieval signals: ${JSON.stringify(request.signals)}`);
|
|
103
|
+
if (catalogContext)
|
|
104
|
+
lines.push(`Available governed data (so you can tell data from general knowledge):\n${catalogContext}`);
|
|
105
|
+
lines.push("Return the classification as JSON.");
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
const CATEGORIES = new Set([
|
|
109
|
+
"conversational",
|
|
110
|
+
"capability",
|
|
111
|
+
"general_knowledge",
|
|
112
|
+
"data_lookup",
|
|
113
|
+
"data_analysis",
|
|
114
|
+
"authoring",
|
|
115
|
+
"app",
|
|
116
|
+
"unclear",
|
|
117
|
+
]);
|
|
118
|
+
/** Extract the first balanced JSON object from model text (tolerant of fences/prose). */
|
|
119
|
+
function extractJsonObject(raw) {
|
|
120
|
+
const trimmed = raw.trim().replace(/^```(?:json)?/i, "").replace(/```$/i, "").trim();
|
|
121
|
+
const start = trimmed.indexOf("{");
|
|
122
|
+
if (start < 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
let depth = 0;
|
|
125
|
+
let inString = false;
|
|
126
|
+
let escaped = false;
|
|
127
|
+
for (let i = start; i < trimmed.length; i += 1) {
|
|
128
|
+
const char = trimmed[i];
|
|
129
|
+
if (inString) {
|
|
130
|
+
if (escaped)
|
|
131
|
+
escaped = false;
|
|
132
|
+
else if (char === "\\")
|
|
133
|
+
escaped = true;
|
|
134
|
+
else if (char === '"')
|
|
135
|
+
inString = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (char === '"')
|
|
139
|
+
inString = true;
|
|
140
|
+
else if (char === "{")
|
|
141
|
+
depth += 1;
|
|
142
|
+
else if (char === "}") {
|
|
143
|
+
depth -= 1;
|
|
144
|
+
if (depth === 0) {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(trimmed.slice(start, i + 1));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
function parseClassification(raw) {
|
|
157
|
+
const parsed = extractJsonObject(raw);
|
|
158
|
+
if (!parsed || typeof parsed !== "object")
|
|
159
|
+
return undefined;
|
|
160
|
+
const record = parsed;
|
|
161
|
+
const category = record.category;
|
|
162
|
+
if (typeof category !== "string" || !CATEGORIES.has(category))
|
|
163
|
+
return undefined;
|
|
164
|
+
const depth = record.depth === "deep" ? "deep" : "quick";
|
|
165
|
+
const needsClarification = record.needsClarification === true;
|
|
166
|
+
const clarifyingQuestion = typeof record.clarifyingQuestion === "string" && record.clarifyingQuestion.trim().length > 0
|
|
167
|
+
? record.clarifyingQuestion.trim()
|
|
168
|
+
: undefined;
|
|
169
|
+
const rationale = typeof record.rationale === "string" && record.rationale.trim().length > 0
|
|
170
|
+
? record.rationale.trim()
|
|
171
|
+
: "Classified by the AI router.";
|
|
172
|
+
return {
|
|
173
|
+
category: category,
|
|
174
|
+
depth,
|
|
175
|
+
needsClarification,
|
|
176
|
+
clarifyingQuestion,
|
|
177
|
+
rationale,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Normalize a question for cache keying (whitespace/case-insensitive). */
|
|
181
|
+
function cacheKey(request) {
|
|
182
|
+
const q = request.question.trim().toLowerCase().replace(/\s+/g, " ");
|
|
183
|
+
const last = request.history?.length ? request.history[request.history.length - 1].text.trim().toLowerCase() : "";
|
|
184
|
+
return `${q}${last}`;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Build a hybrid router. Runs the deterministic cascade first and returns it
|
|
188
|
+
* unchanged when confident; otherwise spends one cached LLM classification call,
|
|
189
|
+
* falling back to the deterministic decision on any failure.
|
|
190
|
+
*/
|
|
191
|
+
export function createHybridRouter(options = {}) {
|
|
192
|
+
const threshold = options.llmThreshold ?? DEFAULT_THRESHOLD;
|
|
193
|
+
const cacheSize = options.cacheSize ?? DEFAULT_CACHE_SIZE;
|
|
194
|
+
const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
195
|
+
const cache = new Map();
|
|
196
|
+
let tick = 0;
|
|
197
|
+
const now = options.now ?? (() => { tick += 1; return tick; });
|
|
198
|
+
const deterministic = (request) => {
|
|
199
|
+
const conversationalKind = classifyConversationalTurn(request.question, (request.history?.length ?? 0) > 0);
|
|
200
|
+
return decideAgentAction({
|
|
201
|
+
question: request.question,
|
|
202
|
+
intent: request.intent ?? (conversationalKind ? "clarify" : "ad_hoc_ranking"),
|
|
203
|
+
signals: request.signals,
|
|
204
|
+
history: request.history,
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
async decide(request) {
|
|
209
|
+
const base = deterministic(request);
|
|
210
|
+
// Confident heuristic (or no LLM available) → keep the fast, offline decision.
|
|
211
|
+
if (base.confidence >= threshold || !options.complete) {
|
|
212
|
+
return { ...base, source: base.source ?? "heuristic" };
|
|
213
|
+
}
|
|
214
|
+
const key = cacheKey(request);
|
|
215
|
+
const cached = cache.get(key);
|
|
216
|
+
if (cached && (options.cacheTtlMs === undefined || now() - cached.at < cacheTtlMs)) {
|
|
217
|
+
return { ...cached.decision, source: "cache" };
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const catalogContext = options.getCatalogContext ? await options.getCatalogContext(request) : undefined;
|
|
221
|
+
const raw = await options.complete({
|
|
222
|
+
system: buildSystemPrompt(),
|
|
223
|
+
user: buildUserPrompt(request, catalogContext),
|
|
224
|
+
signal: options.signal,
|
|
225
|
+
});
|
|
226
|
+
const classification = parseClassification(raw);
|
|
227
|
+
if (classification) {
|
|
228
|
+
const decision = classificationToDecision(classification, base);
|
|
229
|
+
cache.set(key, { decision, at: now() });
|
|
230
|
+
if (cache.size > cacheSize) {
|
|
231
|
+
const oldest = cache.keys().next().value;
|
|
232
|
+
if (oldest !== undefined)
|
|
233
|
+
cache.delete(oldest);
|
|
234
|
+
}
|
|
235
|
+
return decision;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// fall through to deterministic
|
|
240
|
+
}
|
|
241
|
+
return { ...base, source: "heuristic" };
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export { intentForCategory };
|
|
246
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answer synthesis — turn a governed result into a reply whose *shape* matches the
|
|
3
|
+
* question, instead of one static template for everything.
|
|
4
|
+
*
|
|
5
|
+
* The answer loop already produces a correct result (rows) and a draft summary from
|
|
6
|
+
* its SQL-generation call. This step composes the final prose the user reads, with
|
|
7
|
+
* format rules keyed on the question type and audience:
|
|
8
|
+
* - lookup → the number + one sentence, nothing else
|
|
9
|
+
* - comparison / breakdown → a short table (supplied rows only) + one insight
|
|
10
|
+
* - research → sections (What I found / What's driving it / Caveats / Next step)
|
|
11
|
+
* - stakeholder → plain language, no SQL talk; analyst → may reference SQL
|
|
12
|
+
* Trust boilerplate ("uncertified until reviewed") is NOT prose — the badge owns it.
|
|
13
|
+
*
|
|
14
|
+
* Provider-agnostic and offline-safe: the completion is injected (like `narrate`),
|
|
15
|
+
* and any failure falls back to a deterministic reply built from the draft + rows.
|
|
16
|
+
*/
|
|
17
|
+
export type SynthesisFormat = "lookup" | "comparison" | "research" | "prose";
|
|
18
|
+
export interface SynthesizeResultPreview {
|
|
19
|
+
columns: string[];
|
|
20
|
+
rows: Array<Record<string, unknown>>;
|
|
21
|
+
rowCount: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SynthesizeInput {
|
|
24
|
+
question: string;
|
|
25
|
+
/** Router/metadata category, if known (drives the default format). */
|
|
26
|
+
category?: string;
|
|
27
|
+
audience?: "analyst" | "stakeholder";
|
|
28
|
+
/** The bounded result the answer is grounded in (columns + up to ~20 rows). */
|
|
29
|
+
resultPreview?: SynthesizeResultPreview;
|
|
30
|
+
/** The SQL that produced the result (analyst-facing only). */
|
|
31
|
+
sql?: string;
|
|
32
|
+
/** The loop's draft summary — the deterministic floor when no LLM is available. */
|
|
33
|
+
draftText?: string;
|
|
34
|
+
/** Research findings (from narrateResult) to weave into a sectioned answer. */
|
|
35
|
+
findings?: string[];
|
|
36
|
+
/** Known gaps/caveats to surface honestly. */
|
|
37
|
+
gaps?: string[];
|
|
38
|
+
}
|
|
39
|
+
/** Injected text completion, optionally streaming token deltas. */
|
|
40
|
+
export type SynthesizeCompletion = (input: {
|
|
41
|
+
system: string;
|
|
42
|
+
user: string;
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
onDelta?: (delta: string) => void;
|
|
45
|
+
}) => Promise<string>;
|
|
46
|
+
export interface SynthesizeOptions {
|
|
47
|
+
complete?: SynthesizeCompletion;
|
|
48
|
+
onDelta?: (delta: string) => void;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
/** Force a format; otherwise inferred from category + result shape. */
|
|
51
|
+
format?: SynthesisFormat;
|
|
52
|
+
}
|
|
53
|
+
export interface SynthesizeResult {
|
|
54
|
+
text: string;
|
|
55
|
+
format: SynthesisFormat;
|
|
56
|
+
source: "llm" | "deterministic";
|
|
57
|
+
}
|
|
58
|
+
/** Pick the answer shape from category + question + result cardinality. */
|
|
59
|
+
export declare function inferFormat(input: SynthesizeInput): SynthesisFormat;
|
|
60
|
+
/**
|
|
61
|
+
* Compose the final answer. Streams token deltas through `options.onDelta` when the
|
|
62
|
+
* injected completion supports it; always returns the full text (and a deterministic
|
|
63
|
+
* floor on any failure).
|
|
64
|
+
*/
|
|
65
|
+
export declare function synthesizeAnswer(input: SynthesizeInput, options?: SynthesizeOptions): Promise<SynthesizeResult>;
|
|
66
|
+
//# sourceMappingURL=synthesize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synthesize.d.ts","sourceRoot":"","sources":["../src/synthesize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,CAAC;AAE7E,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC;IACrC,+EAA+E;IAC/E,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,8DAA8D;IAC9D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,mEAAmE;AACnE,MAAM,MAAM,oBAAoB,GAAG,CAAC,KAAK,EAAE;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEtB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uEAAuE;IACvE,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,eAAe,CAAC;CACjC;AAKD,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,CAOnE;AAoFD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiBzH"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answer synthesis — turn a governed result into a reply whose *shape* matches the
|
|
3
|
+
* question, instead of one static template for everything.
|
|
4
|
+
*
|
|
5
|
+
* The answer loop already produces a correct result (rows) and a draft summary from
|
|
6
|
+
* its SQL-generation call. This step composes the final prose the user reads, with
|
|
7
|
+
* format rules keyed on the question type and audience:
|
|
8
|
+
* - lookup → the number + one sentence, nothing else
|
|
9
|
+
* - comparison / breakdown → a short table (supplied rows only) + one insight
|
|
10
|
+
* - research → sections (What I found / What's driving it / Caveats / Next step)
|
|
11
|
+
* - stakeholder → plain language, no SQL talk; analyst → may reference SQL
|
|
12
|
+
* Trust boilerplate ("uncertified until reviewed") is NOT prose — the badge owns it.
|
|
13
|
+
*
|
|
14
|
+
* Provider-agnostic and offline-safe: the completion is injected (like `narrate`),
|
|
15
|
+
* and any failure falls back to a deterministic reply built from the draft + rows.
|
|
16
|
+
*/
|
|
17
|
+
const COMPARISON_RE = /\b(compare|versus|vs\.?|by |per |breakdown|break down|top \d+|rank|distribution|segment|cohort|across)\b/i;
|
|
18
|
+
const RESEARCH_RE = /\b(why|driver|drivers|root cause|what happened|what changed|investigate|anomal|trend|explain)\b/i;
|
|
19
|
+
/** Pick the answer shape from category + question + result cardinality. */
|
|
20
|
+
export function inferFormat(input) {
|
|
21
|
+
if (input.category === "data_analysis" || (input.findings && input.findings.length > 0))
|
|
22
|
+
return "research";
|
|
23
|
+
if (RESEARCH_RE.test(input.question))
|
|
24
|
+
return "research";
|
|
25
|
+
const rowCount = input.resultPreview?.rowCount ?? input.resultPreview?.rows.length ?? 0;
|
|
26
|
+
if (COMPARISON_RE.test(input.question) && rowCount > 1)
|
|
27
|
+
return "comparison";
|
|
28
|
+
if (rowCount <= 1)
|
|
29
|
+
return "lookup";
|
|
30
|
+
return "prose";
|
|
31
|
+
}
|
|
32
|
+
function buildSystemPrompt(format, audience) {
|
|
33
|
+
const lines = [
|
|
34
|
+
"You compose the final answer a user reads in DQL, a governed analytics tool.",
|
|
35
|
+
"Use ONLY numbers that appear in the provided rows — never invent or estimate values.",
|
|
36
|
+
"Do NOT add trust disclaimers (e.g. 'uncertified', 'review required') — the UI shows a trust badge separately.",
|
|
37
|
+
"Do NOT say 'As an AI' or narrate your process. Be direct and concrete.",
|
|
38
|
+
];
|
|
39
|
+
if (audience === "stakeholder") {
|
|
40
|
+
lines.push("Audience: a business stakeholder. Use plain language; do NOT mention SQL, tables, or columns.");
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lines.push("Audience: an analyst. You may reference tables/columns; keep SQL talk minimal.");
|
|
44
|
+
}
|
|
45
|
+
switch (format) {
|
|
46
|
+
case "lookup":
|
|
47
|
+
lines.push("Format: state the single value and one short sentence of context. Nothing else — no headings, no table.");
|
|
48
|
+
break;
|
|
49
|
+
case "comparison":
|
|
50
|
+
lines.push("Format: a short GitHub-flavored markdown table built ONLY from the provided rows, then ONE insight sentence beneath it.");
|
|
51
|
+
break;
|
|
52
|
+
case "research":
|
|
53
|
+
lines.push("Format: use these sections as `## ` headings, omitting any that would be empty:", "## What I found\n## What's driving it\n## Caveats\n## Suggested next step", "Keep each section to 1-3 sentences. Ground every claim in the provided findings/rows.");
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
lines.push("Format: 1-3 tight sentences. Add a small markdown table only if it genuinely aids clarity.");
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
function previewToText(preview, limit = 20) {
|
|
61
|
+
if (!preview || preview.rows.length === 0)
|
|
62
|
+
return "(no rows)";
|
|
63
|
+
const cols = preview.columns.length > 0 ? preview.columns : Object.keys(preview.rows[0] ?? {});
|
|
64
|
+
const header = `| ${cols.join(" | ")} |`;
|
|
65
|
+
const sep = `| ${cols.map(() => "---").join(" | ")} |`;
|
|
66
|
+
const body = preview.rows.slice(0, limit).map((row) => `| ${cols.map((col) => formatCell(row[col])).join(" | ")} |`);
|
|
67
|
+
const more = preview.rows.length > limit ? `\n(${preview.rows.length - limit} more rows not shown)` : "";
|
|
68
|
+
return [header, sep, ...body].join("\n") + more;
|
|
69
|
+
}
|
|
70
|
+
function formatCell(value) {
|
|
71
|
+
if (value === null || value === undefined)
|
|
72
|
+
return "";
|
|
73
|
+
if (typeof value === "number")
|
|
74
|
+
return String(value);
|
|
75
|
+
return String(value).replace(/\|/g, "\\|");
|
|
76
|
+
}
|
|
77
|
+
function buildUserPrompt(input) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(`Question: ${input.question}`);
|
|
80
|
+
if (input.resultPreview)
|
|
81
|
+
lines.push(`Result (${input.resultPreview.rowCount} rows):\n${previewToText(input.resultPreview)}`);
|
|
82
|
+
if (input.findings?.length)
|
|
83
|
+
lines.push(`Findings:\n${input.findings.map((f) => `- ${f}`).join("\n")}`);
|
|
84
|
+
if (input.gaps?.length)
|
|
85
|
+
lines.push(`Known gaps/caveats:\n${input.gaps.map((g) => `- ${g}`).join("\n")}`);
|
|
86
|
+
if (input.sql && input.audience !== "stakeholder")
|
|
87
|
+
lines.push(`SQL used:\n${input.sql}`);
|
|
88
|
+
lines.push("Write the answer now.");
|
|
89
|
+
return lines.join("\n\n");
|
|
90
|
+
}
|
|
91
|
+
/** Deterministic floor — the loop's draft, or a line built from the top row. */
|
|
92
|
+
function deterministicSynthesis(input, format) {
|
|
93
|
+
if (input.draftText && input.draftText.trim().length > 0) {
|
|
94
|
+
return { text: input.draftText.trim(), format, source: "deterministic" };
|
|
95
|
+
}
|
|
96
|
+
const preview = input.resultPreview;
|
|
97
|
+
if (format === "comparison" && preview && preview.rows.length > 1) {
|
|
98
|
+
return { text: previewToText(preview), format, source: "deterministic" };
|
|
99
|
+
}
|
|
100
|
+
if (preview && preview.rows.length >= 1) {
|
|
101
|
+
const cols = preview.columns.length > 0 ? preview.columns : Object.keys(preview.rows[0] ?? {});
|
|
102
|
+
const first = preview.rows[0];
|
|
103
|
+
const parts = cols.slice(0, 3).map((col) => `${col}: ${formatCell(first[col])}`);
|
|
104
|
+
return { text: parts.join(", "), format, source: "deterministic" };
|
|
105
|
+
}
|
|
106
|
+
if (input.findings?.length) {
|
|
107
|
+
return { text: input.findings.map((f) => `- ${f}`).join("\n"), format, source: "deterministic" };
|
|
108
|
+
}
|
|
109
|
+
return { text: "No result was available for this question.", format, source: "deterministic" };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Compose the final answer. Streams token deltas through `options.onDelta` when the
|
|
113
|
+
* injected completion supports it; always returns the full text (and a deterministic
|
|
114
|
+
* floor on any failure).
|
|
115
|
+
*/
|
|
116
|
+
export async function synthesizeAnswer(input, options = {}) {
|
|
117
|
+
const format = options.format ?? inferFormat(input);
|
|
118
|
+
const floor = deterministicSynthesis(input, format);
|
|
119
|
+
if (!options.complete)
|
|
120
|
+
return floor;
|
|
121
|
+
try {
|
|
122
|
+
const raw = await options.complete({
|
|
123
|
+
system: buildSystemPrompt(format, input.audience ?? "analyst"),
|
|
124
|
+
user: buildUserPrompt(input),
|
|
125
|
+
signal: options.signal,
|
|
126
|
+
onDelta: options.onDelta,
|
|
127
|
+
});
|
|
128
|
+
const text = raw.trim();
|
|
129
|
+
if (text.length > 0)
|
|
130
|
+
return { text, format, source: "llm" };
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// fall through to deterministic
|
|
134
|
+
}
|
|
135
|
+
return floor;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=synthesize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synthesize.js","sourceRoot":"","sources":["../src/synthesize.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duckcodeailabs/dql-agent",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.30",
|
|
4
4
|
"description": "DQL agent: knowledge graph, Skills, block-first answer loop, and pluggable LLM providers (Claude / OpenAI / Gemini / Ollama)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@duckcodeailabs/dql-core": "^1.6.
|
|
28
|
-
"@duckcodeailabs/dql-governance": "^1.6.
|
|
29
|
-
"@duckcodeailabs/dql-project": "^1.6.
|
|
30
|
-
"better-sqlite3": "^11.
|
|
27
|
+
"@duckcodeailabs/dql-core": "^1.6.30",
|
|
28
|
+
"@duckcodeailabs/dql-governance": "^1.6.30",
|
|
29
|
+
"@duckcodeailabs/dql-project": "^1.6.30",
|
|
30
|
+
"better-sqlite3": "^12.11.1",
|
|
31
31
|
"js-yaml": "^4.1.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"vitest": "^3.0.0"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
|
-
"node": ">=20
|
|
41
|
+
"node": ">=20"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc -b",
|