@duckcodeailabs/dql-cli 1.6.27 → 1.6.29
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 +93 -3
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +575 -11
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{index-A4ATUOrs.js → index-QJKCrLXe.js} +307 -307
- 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 +384 -36
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- 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 +11 -11
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
|
-
import {
|
|
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 }) => {
|
|
@@ -672,18 +753,29 @@ export async function startLocalServer(opts) {
|
|
|
672
753
|
app_build: async ({ request, routeDecision, emit }) => {
|
|
673
754
|
emit({
|
|
674
755
|
type: 'executor.started',
|
|
675
|
-
message: '
|
|
756
|
+
message: 'Planning the app: matching certified blocks and finding coverage gaps.',
|
|
676
757
|
route: 'app_build',
|
|
677
758
|
});
|
|
759
|
+
// Two-phase build: this run only PROPOSES the content list (no files). The
|
|
760
|
+
// user confirms the selection and the UI calls the commit endpoint directly.
|
|
678
761
|
let session;
|
|
679
762
|
try {
|
|
680
|
-
session = await
|
|
763
|
+
session = await proposeAppAiBuild(projectRoot, {
|
|
681
764
|
prompt: request.question,
|
|
682
765
|
domain: agentRunWorkspaceValue(request, 'domain'),
|
|
683
766
|
owner: agentRunWorkspaceValue(request, 'owner'),
|
|
684
767
|
notebookPath: agentRunWorkspaceValue(request, 'notebookPath') ?? request.selectedObject?.path,
|
|
685
768
|
selectedBlockIds: parseAgentRunSelectedBlockIds(request),
|
|
686
769
|
plannerMode: 'deterministic',
|
|
770
|
+
}, {
|
|
771
|
+
generateGovernedAnswer: async (question) => {
|
|
772
|
+
emit({
|
|
773
|
+
type: 'executor.started',
|
|
774
|
+
message: `Generating review-required SQL for an uncovered question: ${question}`,
|
|
775
|
+
route: 'app_build',
|
|
776
|
+
});
|
|
777
|
+
return runGovernedAgentAnswerForRun({ question });
|
|
778
|
+
},
|
|
687
779
|
});
|
|
688
780
|
}
|
|
689
781
|
catch (error) {
|
|
@@ -703,37 +795,46 @@ export async function startLocalServer(opts) {
|
|
|
703
795
|
],
|
|
704
796
|
};
|
|
705
797
|
}
|
|
706
|
-
const
|
|
798
|
+
const proposal = session.status === 'proposed' ? session.proposal : undefined;
|
|
799
|
+
// A real app needs a certified anchor: only offer confirmation when at least
|
|
800
|
+
// one certified tile is present. Otherwise fall through to the escalate path
|
|
801
|
+
// (research / draft the missing blocks) so the user isn't sent to a dead end.
|
|
802
|
+
const proposed = Boolean(proposal && proposal.tiles.some((tile) => tile.certification === 'certified' && !tile.error));
|
|
707
803
|
const sessionPlan = agentRunRecord(session.plan);
|
|
708
|
-
const appTitle = agentRunString(sessionPlan?.name) ?? 'App
|
|
804
|
+
const appTitle = agentRunString(sessionPlan?.name) ?? 'App proposal';
|
|
805
|
+
const certifiedCount = proposal?.coverage.certifiedTiles ?? 0;
|
|
806
|
+
const generatedCount = proposal?.coverage.generatedTiles ?? 0;
|
|
807
|
+
const gapCount = proposal?.coverage.gaps ?? 0;
|
|
709
808
|
// A coverage gap is NOT terminal — leave status open so the gate can escalate to
|
|
710
809
|
// drafting the missing blocks. Only genuine infra errors (the catch above) block.
|
|
711
810
|
return {
|
|
712
|
-
summary:
|
|
713
|
-
?
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
811
|
+
summary: proposed
|
|
812
|
+
? `Proposed ${certifiedCount} certified tile${certifiedCount === 1 ? '' : 's'}`
|
|
813
|
+
+ (generatedCount > 0 ? ` + ${generatedCount} AI-generated (review required)` : '')
|
|
814
|
+
+ (gapCount > 0 ? `; ${gapCount} question${gapCount === 1 ? '' : 's'} still uncovered` : '')
|
|
815
|
+
+ '. Review the list and confirm to create the app.'
|
|
816
|
+
: 'App build needs more certified DQL coverage before a proposal can be assembled.',
|
|
817
|
+
status: proposed ? 'needs_review' : undefined,
|
|
818
|
+
trustState: proposed ? 'review_required' : undefined,
|
|
819
|
+
stopReason: proposed ? 'human_review_required' : undefined,
|
|
820
|
+
artifacts: proposed ? [agentRunArtifact('app_proposal', appTitle, {
|
|
720
821
|
sessionId: session.id,
|
|
721
822
|
appId: session.appId,
|
|
722
823
|
dashboardId: session.dashboardId,
|
|
723
|
-
generatedPaths: session.generatedPaths,
|
|
724
824
|
plan: session.plan,
|
|
725
|
-
|
|
825
|
+
proposal,
|
|
826
|
+
warnings: session.warnings,
|
|
726
827
|
}, session.appId)] : [],
|
|
727
828
|
evaluations: [
|
|
728
829
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to app build.'),
|
|
729
|
-
agentRunEvaluation('app-coverage', 'Certified coverage',
|
|
730
|
-
?
|
|
830
|
+
agentRunEvaluation('app-coverage', 'Certified coverage', proposed, proposed ? 'info' : 'blocking', proposed
|
|
831
|
+
? `Proposal is backed by ${certifiedCount} certified tile${certifiedCount === 1 ? '' : 's'}; nothing is created until you confirm.`
|
|
731
832
|
: session.error ?? 'No certified app tiles matched the request.', session),
|
|
732
833
|
],
|
|
733
|
-
nextActions:
|
|
834
|
+
nextActions: proposed
|
|
734
835
|
? [
|
|
735
|
-
{ id: '
|
|
736
|
-
{ id: '
|
|
836
|
+
{ id: 'confirm-app-build', label: 'Create this app', artifactKind: 'app_proposal' },
|
|
837
|
+
{ id: 'adjust-app-build', label: 'Adjust the plan', route: 'app_build', artifactKind: 'app_proposal' },
|
|
737
838
|
]
|
|
738
839
|
: [
|
|
739
840
|
{ id: 'research-coverage', label: 'Research missing coverage', route: 'research', artifactKind: 'research_run' },
|
|
@@ -780,12 +881,29 @@ export async function startLocalServer(opts) {
|
|
|
780
881
|
},
|
|
781
882
|
getCatalogContext: buildAgentRunCatalogContext,
|
|
782
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
|
+
});
|
|
783
900
|
const agentRunStore = new FileAgentRunStore({ path: defaultAgentRunStorePath(projectRoot) });
|
|
784
901
|
const agentRunEngine = new AgentRunEngine({
|
|
785
902
|
store: agentRunStore,
|
|
786
903
|
executors: agentRunExecutors,
|
|
787
904
|
gates: defaultAgentRunGates,
|
|
788
905
|
planner: agentRunPlanner,
|
|
906
|
+
router: agentRunRouter,
|
|
789
907
|
});
|
|
790
908
|
const runNotebookForApp = async (appId, notebookPath) => {
|
|
791
909
|
const absPath = safeJoin(projectRoot, notebookPath);
|
|
@@ -967,10 +1085,11 @@ export async function startLocalServer(opts) {
|
|
|
967
1085
|
}
|
|
968
1086
|
};
|
|
969
1087
|
const generateInvestigationSqlForApp = async (input) => {
|
|
970
|
-
const
|
|
971
|
-
const
|
|
1088
|
+
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
1089
|
+
const resolvedProvider = governed?.provider ?? null;
|
|
1090
|
+
const runner = governed?.runner ?? null;
|
|
972
1091
|
if (!resolvedProvider || !runner) {
|
|
973
|
-
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.');
|
|
974
1093
|
}
|
|
975
1094
|
let governedAnswer;
|
|
976
1095
|
let providerError;
|
|
@@ -1182,8 +1301,9 @@ export async function startLocalServer(opts) {
|
|
|
1182
1301
|
let providerError;
|
|
1183
1302
|
const generationWarnings = [];
|
|
1184
1303
|
if (!generatedSql && !reviewedSql) {
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1304
|
+
const governedResearch = resolveGovernedAnswerRunner(projectRoot);
|
|
1305
|
+
const resolvedProvider = governedResearch?.provider ?? null;
|
|
1306
|
+
const runner = governedResearch?.runner ?? null;
|
|
1187
1307
|
if (!resolvedProvider || !runner) {
|
|
1188
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.');
|
|
1189
1309
|
}
|
|
@@ -1934,6 +2054,11 @@ export async function startLocalServer(opts) {
|
|
|
1934
2054
|
res.end(serializeJSON({ error: parsed.error ?? 'Invalid agent run request.' }));
|
|
1935
2055
|
return;
|
|
1936
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
|
+
}
|
|
1937
2062
|
const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
|
|
1938
2063
|
if (wantsStream) {
|
|
1939
2064
|
res.writeHead(200, {
|
|
@@ -1944,6 +2069,8 @@ export async function startLocalServer(opts) {
|
|
|
1944
2069
|
});
|
|
1945
2070
|
const run = await agentRunEngine.run(parsed.request, (event) => {
|
|
1946
2071
|
writeAgentRunSse(res, 'agent-run-event', event);
|
|
2072
|
+
}, (delta) => {
|
|
2073
|
+
writeAgentRunSse(res, 'agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
1947
2074
|
});
|
|
1948
2075
|
writeAgentRunSse(res, 'agent-run-complete', run);
|
|
1949
2076
|
res.end();
|
|
@@ -2453,6 +2580,24 @@ export async function startLocalServer(opts) {
|
|
|
2453
2580
|
res.end(serializeJSON({ providers: listProviderSettings(projectRoot) }));
|
|
2454
2581
|
return;
|
|
2455
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
|
+
}
|
|
2456
2601
|
if (req.method === 'POST' && path === '/api/settings/providers') {
|
|
2457
2602
|
try {
|
|
2458
2603
|
const body = await readJSON(req);
|
|
@@ -2861,6 +3006,12 @@ export async function startLocalServer(opts) {
|
|
|
2861
3006
|
blocks = collectPlanBlocks(projectRoot, { certifiedOnly: false });
|
|
2862
3007
|
return planResearch({ question, metrics, blocks, isFollowUp });
|
|
2863
3008
|
},
|
|
3009
|
+
// Two-phase app build gap-fill: bounded governed answers for uncovered
|
|
3010
|
+
// questions. Throws when no provider is configured — propose degrades
|
|
3011
|
+
// gracefully by listing the gap instead.
|
|
3012
|
+
generateGovernedAnswer: (question) => runGovernedAgentAnswerForRun({ question }),
|
|
3013
|
+
// Story narration for commit — LLM-backed with deterministic fallback.
|
|
3014
|
+
narrate: narrateForAgentRun,
|
|
2864
3015
|
});
|
|
2865
3016
|
if (handled)
|
|
2866
3017
|
return;
|
|
@@ -6482,6 +6633,7 @@ function isLLMProviderId(value) {
|
|
|
6482
6633
|
return value === 'anthropic'
|
|
6483
6634
|
|| value === 'claude-agent-sdk'
|
|
6484
6635
|
|| value === 'claude-code'
|
|
6636
|
+
|| value === 'codex'
|
|
6485
6637
|
|| value === 'openai'
|
|
6486
6638
|
|| value === 'gemini'
|
|
6487
6639
|
|| value === 'ollama'
|
|
@@ -6492,7 +6644,9 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
6492
6644
|
const activeProvider = getActiveProvider(projectRoot);
|
|
6493
6645
|
if (activeProvider) {
|
|
6494
6646
|
const active = settings.find((item) => item.id === activeProvider);
|
|
6495
|
-
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))
|
|
6496
6650
|
return activeProvider;
|
|
6497
6651
|
}
|
|
6498
6652
|
const preferred = ['openai', 'gemini', 'anthropic', 'custom-openai', 'ollama'];
|
|
@@ -6503,6 +6657,23 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
6503
6657
|
}
|
|
6504
6658
|
return null;
|
|
6505
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
|
+
}
|
|
6506
6677
|
function loadAppDashboard(projectRoot, appId, dashboardId) {
|
|
6507
6678
|
for (const p of findAppDocuments(projectRoot)) {
|
|
6508
6679
|
const { document: app } = loadAppDocument(p);
|
|
@@ -9887,10 +10058,13 @@ async function buildBlockStudioAiAssistSummary(projectRoot, action, candidate, v
|
|
|
9887
10058
|
async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
9888
10059
|
const settings = listProviderSettings(projectRoot);
|
|
9889
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');
|
|
9890
10064
|
const selected = requestedProvider
|
|
9891
|
-
? settings.find((provider) => provider.id === requestedProvider && provider
|
|
10065
|
+
? settings.find((provider) => provider.id === requestedProvider && isUsable(provider))
|
|
9892
10066
|
: settings.find((provider) => provider.id === activeProvider && provider.enabled)
|
|
9893
|
-
?? settings.find(
|
|
10067
|
+
?? settings.find(isUsable);
|
|
9894
10068
|
if (!selected)
|
|
9895
10069
|
return null;
|
|
9896
10070
|
const config = getEffectiveProviderConfig(projectRoot, selected.id);
|
|
@@ -9911,11 +10085,171 @@ async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
|
9911
10085
|
case 'custom-openai':
|
|
9912
10086
|
provider = new OpenAIProvider({ apiKey: config.apiKey, baseUrl: config.baseUrl, model: config.model, allowNoApiKey: true });
|
|
9913
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;
|
|
9914
10094
|
default:
|
|
9915
10095
|
return null;
|
|
9916
10096
|
}
|
|
9917
10097
|
return await provider.available() ? provider : null;
|
|
9918
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
|
+
}
|
|
9919
10253
|
function buildDeterministicAiAssistSummary(action, candidate, validation) {
|
|
9920
10254
|
const tables = candidate.lineage.sourceTables.length > 0 ? candidate.lineage.sourceTables.join(', ') : 'no source tables detected';
|
|
9921
10255
|
const params = candidate.lineage.parameters.length > 0 ? candidate.lineage.parameters.join(', ') : 'no parameters detected';
|
|
@@ -11687,7 +12021,10 @@ async function testProviderConfig(projectRoot, id, overrides) {
|
|
|
11687
12021
|
};
|
|
11688
12022
|
const label = providerSettingsLabel(id);
|
|
11689
12023
|
const details = providerConfigDetails(id, config);
|
|
11690
|
-
|
|
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) {
|
|
11691
12028
|
return { ok: false, message: `${label} is disabled. Enable it (or Save) before testing.` };
|
|
11692
12029
|
}
|
|
11693
12030
|
if (id === 'openai')
|
|
@@ -11702,6 +12039,12 @@ async function testProviderConfig(projectRoot, id, overrides) {
|
|
|
11702
12039
|
case 'ollama':
|
|
11703
12040
|
provider = new OllamaProvider({ baseUrl: config.baseUrl, model: config.model });
|
|
11704
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;
|
|
11705
12048
|
case 'custom-openai':
|
|
11706
12049
|
default:
|
|
11707
12050
|
provider = new OpenAIProvider({ apiKey: config.apiKey, baseUrl: config.baseUrl, model: config.model, allowNoApiKey: true });
|
|
@@ -11709,9 +12052,12 @@ async function testProviderConfig(projectRoot, id, overrides) {
|
|
|
11709
12052
|
}
|
|
11710
12053
|
const available = await provider.available().catch(() => false);
|
|
11711
12054
|
if (!available) {
|
|
12055
|
+
const cli = id === 'claude-code' ? 'claude' : 'codex';
|
|
11712
12056
|
return {
|
|
11713
12057
|
ok: false,
|
|
11714
|
-
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.`,
|
|
11715
12061
|
};
|
|
11716
12062
|
}
|
|
11717
12063
|
try {
|
|
@@ -11788,6 +12134,8 @@ function providerSettingsLabel(id) {
|
|
|
11788
12134
|
case 'gemini': return 'Gemini';
|
|
11789
12135
|
case 'ollama': return 'Ollama';
|
|
11790
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)';
|
|
11791
12139
|
}
|
|
11792
12140
|
}
|
|
11793
12141
|
function providerConfigDetails(id, config) {
|