@duckcodeailabs/dql-cli 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/apps-api.d.ts +1 -1
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +88 -9
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{index-g0QVJ0wA.js → index-QJKCrLXe.js} +290 -290
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +4 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +15 -2
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +50 -1
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +1 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +338 -16
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +11 -11
- package/dist/providers/subscription-cli.d.ts +66 -0
- package/dist/providers/subscription-cli.d.ts.map +1 -0
- package/dist/providers/subscription-cli.js +314 -0
- package/dist/providers/subscription-cli.js.map +1 -0
- package/dist/settings/provider-settings.d.ts +13 -1
- package/dist/settings/provider-settings.d.ts.map +1 -1
- package/dist/settings/provider-settings.js +34 -6
- package/dist/settings/provider-settings.js.map +1 -1
- package/package.json +12 -12
package/dist/local-runtime.js
CHANGED
|
@@ -11,11 +11,13 @@ import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, getDialect, Parser
|
|
|
11
11
|
import { load as loadYaml } from 'js-yaml';
|
|
12
12
|
import { listBlockTemplates } from './block-templates.js';
|
|
13
13
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
14
|
+
import { createDqlAgentProviderRunner } from './llm/providers/dql-agent-provider.js';
|
|
14
15
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
15
|
-
import { ClaudeProvider, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildLocalContextPack, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, propose, proposePlan, recordCorrectionTrace, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, narrateResult, normalizeAnthropicBaseUrl, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, loadSkills, writeSkill, deleteSkill, reindexProject, defaultKgPath, planApp, planResearch, loadSemanticMetrics, } from '@duckcodeailabs/dql-agent';
|
|
16
|
+
import { ClaudeProvider, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildLocalContextPack, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, propose, proposePlan, recordCorrectionTrace, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, synthesizeAnswer, streamOrGenerate, narrateResult, normalizeAnthropicBaseUrl, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, loadSkills, writeSkill, deleteSkill, reindexProject, defaultKgPath, planApp, planResearch, loadSemanticMetrics, } from '@duckcodeailabs/dql-agent';
|
|
16
17
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
17
18
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
|
|
18
19
|
import { getActiveProvider, getEffectiveProviderConfig, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
|
|
20
|
+
import { ClaudeCodeCliProvider, CodexCliProvider } from './providers/subscription-cli.js';
|
|
19
21
|
import { DQLAccessDeniedError, activePersonaAppId, assertAppAccess, loadRuntimeApp, runtimeVariables, } from './governance-runtime.js';
|
|
20
22
|
import { LocalAppStorage, LocalNotebookResearchStorage, defaultLocalAppsDbPath, defaultNotebookResearchDbPath } from '@duckcodeailabs/dql-project';
|
|
21
23
|
import { Certifier, ENTERPRISE_RULES, evaluateInvariants, hasInvariantViolation, } from '@duckcodeailabs/dql-governance';
|
|
@@ -365,10 +367,11 @@ export async function startLocalServer(opts) {
|
|
|
365
367
|
},
|
|
366
368
|
});
|
|
367
369
|
async function runGovernedAgentAnswerForRun(request, repair) {
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
+
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
371
|
+
const resolvedProvider = governed?.provider ?? null;
|
|
372
|
+
const runner = governed?.runner ?? null;
|
|
370
373
|
if (!resolvedProvider || !runner) {
|
|
371
|
-
throw new Error('No AI provider is configured. Configure OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
|
|
374
|
+
throw new Error('No AI provider is configured. Configure a subscription (Claude Code / Codex), OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
|
|
372
375
|
}
|
|
373
376
|
let governedAnswer;
|
|
374
377
|
let providerError;
|
|
@@ -413,7 +416,7 @@ export async function startLocalServer(opts) {
|
|
|
413
416
|
}
|
|
414
417
|
return governedAnswer;
|
|
415
418
|
}
|
|
416
|
-
const answerRunExecutor = async ({ request, routeDecision, attempt, repairHint }) => {
|
|
419
|
+
const answerRunExecutor = async ({ request, routeDecision, attempt, repairHint, emitAnswerDelta }) => {
|
|
417
420
|
let governedAnswer;
|
|
418
421
|
try {
|
|
419
422
|
governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint });
|
|
@@ -443,6 +446,37 @@ export async function startLocalServer(opts) {
|
|
|
443
446
|
const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
444
447
|
const needsClarification = governedAnswer.kind === 'no_answer';
|
|
445
448
|
const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
|
|
449
|
+
// Synthesis: for a genuinely generated (non-certified) answer, compose an
|
|
450
|
+
// adaptive, well-formatted reply over the executed rows — streamed — instead of
|
|
451
|
+
// the loop's raw draft. Certified answers keep their fast path (no extra call).
|
|
452
|
+
let synthesizedAnswer;
|
|
453
|
+
if (!needsClarification && !isCertified) {
|
|
454
|
+
try {
|
|
455
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
456
|
+
if (provider) {
|
|
457
|
+
const preview = agentResultToSynthesisPreview(governedAnswer.result);
|
|
458
|
+
const draft = governedAnswer.answer ?? governedAnswer.text;
|
|
459
|
+
const result = await synthesizeAnswer({
|
|
460
|
+
question: request.question,
|
|
461
|
+
category: routeDecision?.category,
|
|
462
|
+
audience: request.audience ?? 'analyst',
|
|
463
|
+
resultPreview: preview,
|
|
464
|
+
sql: sql,
|
|
465
|
+
draftText: draft,
|
|
466
|
+
gaps: governedAnswer.validationWarnings,
|
|
467
|
+
}, {
|
|
468
|
+
onDelta: emitAnswerDelta,
|
|
469
|
+
complete: ({ system, user, signal, onDelta }) => streamOrGenerate(provider, [{ role: 'system', content: system }, { role: 'user', content: user }], { maxTokens: 350, temperature: 0.3, signal }, onDelta ?? (() => { })),
|
|
470
|
+
});
|
|
471
|
+
if (result.text)
|
|
472
|
+
synthesizedAnswer = result.text;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Keep the governed draft on any synthesis failure.
|
|
477
|
+
synthesizedAnswer = undefined;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
446
480
|
const status = needsClarification ? 'needs_clarification' : isCertified ? 'completed' : 'needs_review';
|
|
447
481
|
const trustState = needsClarification ? 'not_applicable' : isCertified ? 'certified' : 'review_required';
|
|
448
482
|
const stopReason = needsClarification ? 'needs_clarification' : isCertified ? 'certified_answer_found' : 'human_review_required';
|
|
@@ -455,7 +489,7 @@ export async function startLocalServer(opts) {
|
|
|
455
489
|
];
|
|
456
490
|
return {
|
|
457
491
|
summary: governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : 'Answered with review-required generated analysis.'),
|
|
458
|
-
answer: governedAnswer.answer ?? governedAnswer.text,
|
|
492
|
+
answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
|
|
459
493
|
status,
|
|
460
494
|
trustState,
|
|
461
495
|
stopReason,
|
|
@@ -476,7 +510,54 @@ export async function startLocalServer(opts) {
|
|
|
476
510
|
nextActions,
|
|
477
511
|
};
|
|
478
512
|
};
|
|
513
|
+
const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
|
|
514
|
+
const kind = routeDecision?.conversationalKind ?? 'smalltalk';
|
|
515
|
+
const isGeneralKnowledge = routeDecision?.category === 'general_knowledge';
|
|
516
|
+
const answerKind = isGeneralKnowledge ? 'general_knowledge' : 'conversational';
|
|
517
|
+
const catalogContext = buildAgentRunCatalogContext();
|
|
518
|
+
const suggestions = buildConversationSuggestions(projectRoot, kind);
|
|
519
|
+
const nextActions = suggestions.map((prompt, index) => ({
|
|
520
|
+
id: `suggest-question-${index + 1}`,
|
|
521
|
+
label: prompt,
|
|
522
|
+
}));
|
|
523
|
+
let text;
|
|
524
|
+
try {
|
|
525
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
526
|
+
if (provider) {
|
|
527
|
+
const system = buildConversationSystemPrompt(kind, isGeneralKnowledge, catalogContext, request.audience ?? 'analyst');
|
|
528
|
+
const messages = [
|
|
529
|
+
{ role: 'system', content: system },
|
|
530
|
+
...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
|
|
531
|
+
{ role: 'user', content: request.question },
|
|
532
|
+
];
|
|
533
|
+
text = (await provider.generate(messages, { maxTokens: 320, temperature: 0.6 })).trim();
|
|
534
|
+
// Perceived-latency: surface the reply as one delta for surfaces wired to stream.
|
|
535
|
+
if (text)
|
|
536
|
+
emitAnswerDelta?.(text);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
// Provider unavailable / errored — fall through to the deterministic reply.
|
|
541
|
+
text = undefined;
|
|
542
|
+
}
|
|
543
|
+
if (!text) {
|
|
544
|
+
text = buildConversationalFallback(kind, isGeneralKnowledge, request.question, suggestions);
|
|
545
|
+
emitAnswerDelta?.(text);
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
summary: 'Replied conversationally.',
|
|
549
|
+
answer: text,
|
|
550
|
+
answerKind,
|
|
551
|
+
status: 'completed',
|
|
552
|
+
trustState: 'not_applicable',
|
|
553
|
+
stopReason: 'conversational_reply',
|
|
554
|
+
artifacts: [],
|
|
555
|
+
evaluations: [],
|
|
556
|
+
nextActions,
|
|
557
|
+
};
|
|
558
|
+
};
|
|
479
559
|
const agentRunExecutors = {
|
|
560
|
+
conversation: conversationRunExecutor,
|
|
480
561
|
certified_answer: answerRunExecutor,
|
|
481
562
|
generated_answer: answerRunExecutor,
|
|
482
563
|
research: async ({ runId, request, routeDecision, emit }) => {
|
|
@@ -800,12 +881,29 @@ export async function startLocalServer(opts) {
|
|
|
800
881
|
},
|
|
801
882
|
getCatalogContext: buildAgentRunCatalogContext,
|
|
802
883
|
});
|
|
884
|
+
// Hybrid router: keep the deterministic decision when it is confident (certified
|
|
885
|
+
// fast paths + greetings stay 0-LLM); spend one cheap classification call only for
|
|
886
|
+
// the ambiguous middle so Auto reliably picks quick-answer vs deep research without
|
|
887
|
+
// the user clicking "Dig deeper". Same provider completion as the planner.
|
|
888
|
+
const agentRunRouter = createHybridRouter({
|
|
889
|
+
complete: async ({ system, user, signal }) => {
|
|
890
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
891
|
+
if (!provider)
|
|
892
|
+
throw new Error('No AI provider configured for routing.');
|
|
893
|
+
return provider.generate([
|
|
894
|
+
{ role: 'system', content: system },
|
|
895
|
+
{ role: 'user', content: user },
|
|
896
|
+
], { maxTokens: 250, temperature: 0, signal });
|
|
897
|
+
},
|
|
898
|
+
getCatalogContext: () => buildAgentRunCatalogContext(),
|
|
899
|
+
});
|
|
803
900
|
const agentRunStore = new FileAgentRunStore({ path: defaultAgentRunStorePath(projectRoot) });
|
|
804
901
|
const agentRunEngine = new AgentRunEngine({
|
|
805
902
|
store: agentRunStore,
|
|
806
903
|
executors: agentRunExecutors,
|
|
807
904
|
gates: defaultAgentRunGates,
|
|
808
905
|
planner: agentRunPlanner,
|
|
906
|
+
router: agentRunRouter,
|
|
809
907
|
});
|
|
810
908
|
const runNotebookForApp = async (appId, notebookPath) => {
|
|
811
909
|
const absPath = safeJoin(projectRoot, notebookPath);
|
|
@@ -987,10 +1085,11 @@ export async function startLocalServer(opts) {
|
|
|
987
1085
|
}
|
|
988
1086
|
};
|
|
989
1087
|
const generateInvestigationSqlForApp = async (input) => {
|
|
990
|
-
const
|
|
991
|
-
const
|
|
1088
|
+
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
1089
|
+
const resolvedProvider = governed?.provider ?? null;
|
|
1090
|
+
const runner = governed?.runner ?? null;
|
|
992
1091
|
if (!resolvedProvider || !runner) {
|
|
993
|
-
throw new Error('No AI provider is configured. Configure OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
|
|
1092
|
+
throw new Error('No AI provider is configured. Configure a subscription (Claude Code / Codex), OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
|
|
994
1093
|
}
|
|
995
1094
|
let governedAnswer;
|
|
996
1095
|
let providerError;
|
|
@@ -1202,8 +1301,9 @@ export async function startLocalServer(opts) {
|
|
|
1202
1301
|
let providerError;
|
|
1203
1302
|
const generationWarnings = [];
|
|
1204
1303
|
if (!generatedSql && !reviewedSql) {
|
|
1205
|
-
const
|
|
1206
|
-
const
|
|
1304
|
+
const governedResearch = resolveGovernedAnswerRunner(projectRoot);
|
|
1305
|
+
const resolvedProvider = governedResearch?.provider ?? null;
|
|
1306
|
+
const runner = governedResearch?.runner ?? null;
|
|
1207
1307
|
if (!resolvedProvider || !runner) {
|
|
1208
1308
|
generationWarnings.push('No AI provider is configured. Metadata context was saved as a research plan; paste SQL or configure an AI provider to generate candidate SQL.');
|
|
1209
1309
|
}
|
|
@@ -1954,6 +2054,11 @@ export async function startLocalServer(opts) {
|
|
|
1954
2054
|
res.end(serializeJSON({ error: parsed.error ?? 'Invalid agent run request.' }));
|
|
1955
2055
|
return;
|
|
1956
2056
|
}
|
|
2057
|
+
// The UI does not send retrieval signals; probe the local catalog so the
|
|
2058
|
+
// router can short-circuit obvious governed matches without an LLM call.
|
|
2059
|
+
if (!parsed.request.signals && !parsed.request.requestedMode) {
|
|
2060
|
+
parsed.request.signals = computeAgentRunSignals(projectRoot, parsed.request.question);
|
|
2061
|
+
}
|
|
1957
2062
|
const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
|
|
1958
2063
|
if (wantsStream) {
|
|
1959
2064
|
res.writeHead(200, {
|
|
@@ -1964,6 +2069,8 @@ export async function startLocalServer(opts) {
|
|
|
1964
2069
|
});
|
|
1965
2070
|
const run = await agentRunEngine.run(parsed.request, (event) => {
|
|
1966
2071
|
writeAgentRunSse(res, 'agent-run-event', event);
|
|
2072
|
+
}, (delta) => {
|
|
2073
|
+
writeAgentRunSse(res, 'agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
1967
2074
|
});
|
|
1968
2075
|
writeAgentRunSse(res, 'agent-run-complete', run);
|
|
1969
2076
|
res.end();
|
|
@@ -2473,6 +2580,24 @@ export async function startLocalServer(opts) {
|
|
|
2473
2580
|
res.end(serializeJSON({ providers: listProviderSettings(projectRoot) }));
|
|
2474
2581
|
return;
|
|
2475
2582
|
}
|
|
2583
|
+
// Live detection for subscription-CLI providers: is the CLI installed and logged
|
|
2584
|
+
// in, and (for Claude) which account/plan. Lets Settings show real status instead
|
|
2585
|
+
// of a static hint. Spawns the CLIs, so it's a separate async call from the list.
|
|
2586
|
+
if (req.method === 'GET' && path === '/api/settings/providers/cli-status') {
|
|
2587
|
+
try {
|
|
2588
|
+
const [claudeCode, codex] = await Promise.all([
|
|
2589
|
+
ClaudeCodeCliProvider.detect(),
|
|
2590
|
+
CodexCliProvider.detect(),
|
|
2591
|
+
]);
|
|
2592
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
2593
|
+
res.end(serializeJSON({ status: { 'claude-code': claudeCode, codex } }));
|
|
2594
|
+
}
|
|
2595
|
+
catch (error) {
|
|
2596
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
2597
|
+
res.end(serializeJSON({ status: {}, error: error instanceof Error ? error.message : String(error) }));
|
|
2598
|
+
}
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2476
2601
|
if (req.method === 'POST' && path === '/api/settings/providers') {
|
|
2477
2602
|
try {
|
|
2478
2603
|
const body = await readJSON(req);
|
|
@@ -6508,6 +6633,7 @@ function isLLMProviderId(value) {
|
|
|
6508
6633
|
return value === 'anthropic'
|
|
6509
6634
|
|| value === 'claude-agent-sdk'
|
|
6510
6635
|
|| value === 'claude-code'
|
|
6636
|
+
|| value === 'codex'
|
|
6511
6637
|
|| value === 'openai'
|
|
6512
6638
|
|| value === 'gemini'
|
|
6513
6639
|
|| value === 'ollama'
|
|
@@ -6518,7 +6644,9 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
6518
6644
|
const activeProvider = getActiveProvider(projectRoot);
|
|
6519
6645
|
if (activeProvider) {
|
|
6520
6646
|
const active = settings.find((item) => item.id === activeProvider);
|
|
6521
|
-
if
|
|
6647
|
+
// Only return it for the chat-cell runner path if it's a runner provider id
|
|
6648
|
+
// (subscription-only ids like `codex` aren't runners; they fall through).
|
|
6649
|
+
if (active?.enabled && isLLMProviderId(activeProvider))
|
|
6522
6650
|
return activeProvider;
|
|
6523
6651
|
}
|
|
6524
6652
|
const preferred = ['openai', 'gemini', 'anthropic', 'custom-openai', 'ollama'];
|
|
@@ -6529,6 +6657,23 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
6529
6657
|
}
|
|
6530
6658
|
return null;
|
|
6531
6659
|
}
|
|
6660
|
+
/**
|
|
6661
|
+
* Resolve the runner that produces a GOVERNED answer (answer-loop with a completion
|
|
6662
|
+
* provider). Subscription CLI providers (Claude Code / Codex) are forced through the
|
|
6663
|
+
* answer-loop runner — never the MCP `claudeCodeRunner`, which doesn't emit a governed
|
|
6664
|
+
* answer envelope. Everything else uses the Settings-resolved default runner.
|
|
6665
|
+
*/
|
|
6666
|
+
function resolveGovernedAnswerRunner(projectRoot) {
|
|
6667
|
+
const active = getActiveProvider(projectRoot);
|
|
6668
|
+
if (active === 'claude-code' || active === 'codex') {
|
|
6669
|
+
return { provider: active, runner: createDqlAgentProviderRunner(active) };
|
|
6670
|
+
}
|
|
6671
|
+
const resolved = resolveDefaultLLMProvider(projectRoot);
|
|
6672
|
+
if (!resolved)
|
|
6673
|
+
return null;
|
|
6674
|
+
const runner = getLLMRunner(resolved);
|
|
6675
|
+
return runner ? { provider: resolved, runner } : null;
|
|
6676
|
+
}
|
|
6532
6677
|
function loadAppDashboard(projectRoot, appId, dashboardId) {
|
|
6533
6678
|
for (const p of findAppDocuments(projectRoot)) {
|
|
6534
6679
|
const { document: app } = loadAppDocument(p);
|
|
@@ -9913,10 +10058,13 @@ async function buildBlockStudioAiAssistSummary(projectRoot, action, candidate, v
|
|
|
9913
10058
|
async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
9914
10059
|
const settings = listProviderSettings(projectRoot);
|
|
9915
10060
|
const activeProvider = getActiveProvider(projectRoot);
|
|
10061
|
+
// Subscription CLI providers (Claude Code / Codex) carry no API key — they're
|
|
10062
|
+
// usable when enabled; their real "installed + logged in" check runs in available().
|
|
10063
|
+
const isUsable = (provider) => provider.enabled && (provider.hasApiKey || provider.authMode === 'subscription_cli');
|
|
9916
10064
|
const selected = requestedProvider
|
|
9917
|
-
? settings.find((provider) => provider.id === requestedProvider && provider
|
|
10065
|
+
? settings.find((provider) => provider.id === requestedProvider && isUsable(provider))
|
|
9918
10066
|
: settings.find((provider) => provider.id === activeProvider && provider.enabled)
|
|
9919
|
-
?? settings.find(
|
|
10067
|
+
?? settings.find(isUsable);
|
|
9920
10068
|
if (!selected)
|
|
9921
10069
|
return null;
|
|
9922
10070
|
const config = getEffectiveProviderConfig(projectRoot, selected.id);
|
|
@@ -9937,11 +10085,171 @@ async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
|
9937
10085
|
case 'custom-openai':
|
|
9938
10086
|
provider = new OpenAIProvider({ apiKey: config.apiKey, baseUrl: config.baseUrl, model: config.model, allowNoApiKey: true });
|
|
9939
10087
|
break;
|
|
10088
|
+
case 'claude-code':
|
|
10089
|
+
provider = new ClaudeCodeCliProvider({ model: config.model });
|
|
10090
|
+
break;
|
|
10091
|
+
case 'codex':
|
|
10092
|
+
provider = new CodexCliProvider({ model: config.model });
|
|
10093
|
+
break;
|
|
9940
10094
|
default:
|
|
9941
10095
|
return null;
|
|
9942
10096
|
}
|
|
9943
10097
|
return await provider.available() ? provider : null;
|
|
9944
10098
|
}
|
|
10099
|
+
/** Convert a governed answer's result payload into a bounded synthesis preview. */
|
|
10100
|
+
function agentResultToSynthesisPreview(result) {
|
|
10101
|
+
if (!result)
|
|
10102
|
+
return undefined;
|
|
10103
|
+
const columns = Array.isArray(result.columns)
|
|
10104
|
+
? result.columns.map((col) => typeof col === 'string' ? col : col?.name ?? String(col))
|
|
10105
|
+
: [];
|
|
10106
|
+
const rawRows = Array.isArray(result.rows) ? result.rows.slice(0, 20) : [];
|
|
10107
|
+
const rows = rawRows.map((row) => {
|
|
10108
|
+
if (row && typeof row === 'object' && !Array.isArray(row))
|
|
10109
|
+
return row;
|
|
10110
|
+
// Array-shaped rows → zip with column names.
|
|
10111
|
+
if (Array.isArray(row)) {
|
|
10112
|
+
const obj = {};
|
|
10113
|
+
row.forEach((value, index) => { obj[columns[index] ?? `col${index}`] = value; });
|
|
10114
|
+
return obj;
|
|
10115
|
+
}
|
|
10116
|
+
return { value: row };
|
|
10117
|
+
});
|
|
10118
|
+
return {
|
|
10119
|
+
columns: columns.length > 0 ? columns : Object.keys(rows[0] ?? {}),
|
|
10120
|
+
rows,
|
|
10121
|
+
rowCount: typeof result.rowCount === 'number' ? result.rowCount : rows.length,
|
|
10122
|
+
};
|
|
10123
|
+
}
|
|
10124
|
+
const SIGNAL_STOPWORDS = new Set([
|
|
10125
|
+
'the', 'a', 'an', 'of', 'for', 'to', 'by', 'in', 'on', 'and', 'or', 'is', 'are', 'was', 'were',
|
|
10126
|
+
'what', 'which', 'how', 'show', 'me', 'my', 'our', 'this', 'that', 'top', 'give', 'get', 'list',
|
|
10127
|
+
]);
|
|
10128
|
+
function signalTokens(text) {
|
|
10129
|
+
return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/)
|
|
10130
|
+
.filter((token) => token.length > 2 && !SIGNAL_STOPWORDS.has(token)));
|
|
10131
|
+
}
|
|
10132
|
+
/** Best token-overlap ratio between the question and any candidate name/description. */
|
|
10133
|
+
function bestOverlap(questionTokens, candidates) {
|
|
10134
|
+
if (questionTokens.size === 0)
|
|
10135
|
+
return 0;
|
|
10136
|
+
let best = 0;
|
|
10137
|
+
for (const candidate of candidates) {
|
|
10138
|
+
const candTokens = signalTokens(candidate);
|
|
10139
|
+
if (candTokens.size === 0)
|
|
10140
|
+
continue;
|
|
10141
|
+
let hits = 0;
|
|
10142
|
+
for (const token of candTokens)
|
|
10143
|
+
if (questionTokens.has(token))
|
|
10144
|
+
hits += 1;
|
|
10145
|
+
if (hits === 0)
|
|
10146
|
+
continue;
|
|
10147
|
+
// Ratio of the candidate's tokens the question covers — rewards a tight match.
|
|
10148
|
+
const ratio = hits / Math.min(candTokens.size, questionTokens.size);
|
|
10149
|
+
if (ratio > best)
|
|
10150
|
+
best = ratio;
|
|
10151
|
+
}
|
|
10152
|
+
return Math.min(1, best);
|
|
10153
|
+
}
|
|
10154
|
+
/**
|
|
10155
|
+
* Cheap, offline retrieval-signal probe. Fills certifiedScore/metricScore/hasRetrieval
|
|
10156
|
+
* from local catalog name/description overlap so the deterministic router can reach
|
|
10157
|
+
* confidence (and skip the LLM) on obvious governed matches. Best-effort — any failure
|
|
10158
|
+
* yields empty signals and the router simply leans on its LLM classification.
|
|
10159
|
+
*/
|
|
10160
|
+
function computeAgentRunSignals(projectRoot, question) {
|
|
10161
|
+
try {
|
|
10162
|
+
const tokens = signalTokens(question);
|
|
10163
|
+
if (tokens.size === 0)
|
|
10164
|
+
return { certifiedScore: 0, metricScore: 0, hasRetrieval: false };
|
|
10165
|
+
const certifiedBlocks = collectPlanBlocks(projectRoot, { certifiedOnly: true });
|
|
10166
|
+
const blockStrings = certifiedBlocks.flatMap((block) => [block.name, block.description ?? ''].filter(Boolean));
|
|
10167
|
+
const metrics = loadSemanticMetrics(projectRoot);
|
|
10168
|
+
const metricStrings = metrics.map((metric) => {
|
|
10169
|
+
const node = metric;
|
|
10170
|
+
return node.name ?? node.label ?? node.id ?? '';
|
|
10171
|
+
}).filter(Boolean);
|
|
10172
|
+
const certifiedScore = bestOverlap(tokens, blockStrings);
|
|
10173
|
+
const metricScore = bestOverlap(tokens, metricStrings);
|
|
10174
|
+
return { certifiedScore, metricScore, hasRetrieval: certifiedScore > 0 || metricScore > 0 };
|
|
10175
|
+
}
|
|
10176
|
+
catch {
|
|
10177
|
+
return { certifiedScore: 0, metricScore: 0, hasRetrieval: false };
|
|
10178
|
+
}
|
|
10179
|
+
}
|
|
10180
|
+
/** Up to three example questions built from real catalog blocks (falls back to generic asks). */
|
|
10181
|
+
function buildConversationSuggestions(projectRoot, kind) {
|
|
10182
|
+
if (kind === 'gratitude')
|
|
10183
|
+
return [];
|
|
10184
|
+
let names = [];
|
|
10185
|
+
try {
|
|
10186
|
+
let blocks = collectPlanBlocks(projectRoot, { certifiedOnly: true });
|
|
10187
|
+
if (blocks.length === 0)
|
|
10188
|
+
blocks = collectPlanBlocks(projectRoot, { certifiedOnly: false });
|
|
10189
|
+
names = blocks.slice(0, 3).map((block) => block.name).filter(Boolean);
|
|
10190
|
+
}
|
|
10191
|
+
catch {
|
|
10192
|
+
names = [];
|
|
10193
|
+
}
|
|
10194
|
+
if (names.length === 0) {
|
|
10195
|
+
return [
|
|
10196
|
+
'What is total revenue?',
|
|
10197
|
+
'Top customers by revenue this quarter',
|
|
10198
|
+
'Why is revenue down by region?',
|
|
10199
|
+
];
|
|
10200
|
+
}
|
|
10201
|
+
const humanize = (name) => name.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
10202
|
+
return names.map((name) => `Show me ${humanize(name)}`);
|
|
10203
|
+
}
|
|
10204
|
+
/** The system prompt for a conversational reply — persona + capability card + catalog. */
|
|
10205
|
+
function buildConversationSystemPrompt(kind, isGeneralKnowledge, catalogContext, audience) {
|
|
10206
|
+
const capabilities = audience === 'stakeholder'
|
|
10207
|
+
? [
|
|
10208
|
+
'answer questions about their data, grounded in certified metrics and dbt lineage',
|
|
10209
|
+
'run a deeper investigation when a question needs it ("dig deeper")',
|
|
10210
|
+
]
|
|
10211
|
+
: [
|
|
10212
|
+
'answer questions about their data, grounded in certified metrics and dbt lineage',
|
|
10213
|
+
'run a deeper multi-step investigation ("dig deeper")',
|
|
10214
|
+
'draft SQL cells and DQL blocks for review',
|
|
10215
|
+
'build dashboards and apps from certified blocks',
|
|
10216
|
+
];
|
|
10217
|
+
const lines = [
|
|
10218
|
+
'You are DQL\'s assistant inside a governed analytics notebook. Be warm, concise, and helpful.',
|
|
10219
|
+
`You can: ${capabilities.map((c) => `(${c})`).join(', ')}.`,
|
|
10220
|
+
'Reply in at most 2-3 short sentences. Do NOT invent data values, metrics, or numbers.',
|
|
10221
|
+
'If the user seems to want data, invite them to ask — their question will run through the governed loop.',
|
|
10222
|
+
];
|
|
10223
|
+
if (isGeneralKnowledge) {
|
|
10224
|
+
lines.push('This is a general-knowledge question. Answer from your own knowledge, plainly and briefly.', 'Do NOT imply the answer comes from the user\'s data or warehouse. If it relates to their work, offer to look at their governed data next.');
|
|
10225
|
+
}
|
|
10226
|
+
else if (kind === 'meta_capability') {
|
|
10227
|
+
lines.push('The user is asking what you can do — briefly describe your capabilities and suggest a concrete first question.');
|
|
10228
|
+
}
|
|
10229
|
+
if (catalogContext) {
|
|
10230
|
+
lines.push('', 'When suggesting example questions, prefer these real objects in their workspace:', catalogContext);
|
|
10231
|
+
}
|
|
10232
|
+
return lines.join('\n');
|
|
10233
|
+
}
|
|
10234
|
+
/** Deterministic, warm reply when no AI provider is configured — never governance-speak. */
|
|
10235
|
+
function buildConversationalFallback(kind, isGeneralKnowledge, question, suggestions) {
|
|
10236
|
+
const examples = suggestions.length > 0
|
|
10237
|
+
? ` For example: ${suggestions.map((s) => `"${s}"`).join(', ')}.`
|
|
10238
|
+
: '';
|
|
10239
|
+
if (isGeneralKnowledge) {
|
|
10240
|
+
return `That's a general-knowledge question, so I can't answer it from your governed data. Ask me about your metrics or dbt models and I'll ground the answer in your workspace.${examples}`;
|
|
10241
|
+
}
|
|
10242
|
+
switch (kind) {
|
|
10243
|
+
case 'greeting':
|
|
10244
|
+
return `Hi! I'm your DQL assistant — I answer questions about your data, grounded in your certified metrics and dbt lineage.${examples}`;
|
|
10245
|
+
case 'gratitude':
|
|
10246
|
+
return "You're welcome! Ask me anything else about your data whenever you're ready.";
|
|
10247
|
+
case 'meta_capability':
|
|
10248
|
+
return `I can answer questions about your data (grounded in certified metrics and dbt lineage), dig deeper with a multi-step investigation, draft SQL and DQL blocks, and build apps from certified blocks.${examples}`;
|
|
10249
|
+
default:
|
|
10250
|
+
return `I'm here to help you explore your data. Ask me a question and I'll ground the answer in your certified metrics and dbt models.${examples}`;
|
|
10251
|
+
}
|
|
10252
|
+
}
|
|
9945
10253
|
function buildDeterministicAiAssistSummary(action, candidate, validation) {
|
|
9946
10254
|
const tables = candidate.lineage.sourceTables.length > 0 ? candidate.lineage.sourceTables.join(', ') : 'no source tables detected';
|
|
9947
10255
|
const params = candidate.lineage.parameters.length > 0 ? candidate.lineage.parameters.join(', ') : 'no parameters detected';
|
|
@@ -11713,7 +12021,10 @@ async function testProviderConfig(projectRoot, id, overrides) {
|
|
|
11713
12021
|
};
|
|
11714
12022
|
const label = providerSettingsLabel(id);
|
|
11715
12023
|
const details = providerConfigDetails(id, config);
|
|
11716
|
-
|
|
12024
|
+
const isSubscriptionCli = id === 'claude-code' || id === 'codex';
|
|
12025
|
+
// Subscription CLI providers are always testable — detection (installed + logged in)
|
|
12026
|
+
// needs no saved config; the others require enabling/saving first.
|
|
12027
|
+
if (!config.enabled && !isSubscriptionCli) {
|
|
11717
12028
|
return { ok: false, message: `${label} is disabled. Enable it (or Save) before testing.` };
|
|
11718
12029
|
}
|
|
11719
12030
|
if (id === 'openai')
|
|
@@ -11728,6 +12039,12 @@ async function testProviderConfig(projectRoot, id, overrides) {
|
|
|
11728
12039
|
case 'ollama':
|
|
11729
12040
|
provider = new OllamaProvider({ baseUrl: config.baseUrl, model: config.model });
|
|
11730
12041
|
break;
|
|
12042
|
+
case 'claude-code':
|
|
12043
|
+
provider = new ClaudeCodeCliProvider({ model: config.model });
|
|
12044
|
+
break;
|
|
12045
|
+
case 'codex':
|
|
12046
|
+
provider = new CodexCliProvider({ model: config.model });
|
|
12047
|
+
break;
|
|
11731
12048
|
case 'custom-openai':
|
|
11732
12049
|
default:
|
|
11733
12050
|
provider = new OpenAIProvider({ apiKey: config.apiKey, baseUrl: config.baseUrl, model: config.model, allowNoApiKey: true });
|
|
@@ -11735,9 +12052,12 @@ async function testProviderConfig(projectRoot, id, overrides) {
|
|
|
11735
12052
|
}
|
|
11736
12053
|
const available = await provider.available().catch(() => false);
|
|
11737
12054
|
if (!available) {
|
|
12055
|
+
const cli = id === 'claude-code' ? 'claude' : 'codex';
|
|
11738
12056
|
return {
|
|
11739
12057
|
ok: false,
|
|
11740
|
-
message:
|
|
12058
|
+
message: isSubscriptionCli
|
|
12059
|
+
? `${label} is not ready. Install the \`${cli}\` CLI and run \`${cli} ${id === 'claude-code' ? '/login' : 'login'}\` with your subscription, then test again.`
|
|
12060
|
+
: `${label} is not configured or reachable${details}. Check API key, base URL, and local service state.`,
|
|
11741
12061
|
};
|
|
11742
12062
|
}
|
|
11743
12063
|
try {
|
|
@@ -11814,6 +12134,8 @@ function providerSettingsLabel(id) {
|
|
|
11814
12134
|
case 'gemini': return 'Gemini';
|
|
11815
12135
|
case 'ollama': return 'Ollama';
|
|
11816
12136
|
case 'custom-openai': return 'Custom OpenAI-compatible provider';
|
|
12137
|
+
case 'claude-code': return 'Claude subscription (Claude Code CLI)';
|
|
12138
|
+
case 'codex': return 'ChatGPT subscription (Codex CLI)';
|
|
11817
12139
|
}
|
|
11818
12140
|
}
|
|
11819
12141
|
function providerConfigDetails(id, config) {
|