Phase 7: AI-assisted query authoring (autocomplete, explain, fix, optimize, NL translation)
Adds a self-hosted (Ollama, qwen2.5-coder) model provider abstraction with a pluggable opt-in cloud adapter, schema grounding, and a shared cost/safety guard every AI-suggested query is assessed against -- compiling to and executing through the same unchanged Phase 2 IR/ compiler and Phase 4 tenant scoping as a hand-written query, no parallel execution path. Track A (built into the query bar): inline ghost-text autocomplete, "Explain this query", "Fix this query" with a diff view, and a rule-based "Optimize" suggestion. Track B: natural-language-to-query translation, always a separate review step from execution, with `sentryctl query --nl` requiring explicit confirmation to run. Every accepted/dismissed translate-fix-optimize interaction is logged into the same append-only audit_log table Phase 4 built. Two real product bugs were found and fixed via live browser verification (a Svelte effect re-running on every keystroke that silently cancelled the ghost-text debounce; a ghost-text widget positioned at document offset 0 instead of the cursor), and a real costguard logic bug (unbounded-aggregation vs. raw-row) was caught by its own test suite. New integration tests wire a real Ollama client through the real HTTP handler against a mock server matching Ollama's wire contract (hack/mock-ollama), keeping model-quality verification out of CI as a disclosed, periodic human-run check instead. See /docs/phase-7-ai-design.md and /docs/phase-7-runbook.md.
This commit is contained in:
+119
-1
@@ -16,7 +16,13 @@ export const enterpriseAuthBase = import.meta.env.VITE_ENTERPRISE_AUTH_BASE_URL
|
||||
|
||||
export type Language = '' | 'sql' | 'spl';
|
||||
|
||||
export type QueryResult = { columns: string[]; rows: unknown[][] };
|
||||
// warnings (Phase 7) is populated by the shared costguard package's
|
||||
// assessment of every query, hand-written or AI-suggested alike --
|
||||
// informational only, never a reason a query didn't run. Optional/absent
|
||||
// (not an empty array) when there's nothing to say, matching the
|
||||
// backend's `omitempty` -- see api/queryapi/handler.go's doc comment on
|
||||
// why this is additive, not new enforcement.
|
||||
export type QueryResult = { columns: string[]; rows: unknown[][]; warnings?: string[] };
|
||||
|
||||
export type VizType = 'table' | 'line' | 'bar' | 'single_stat' | 'top_n' | 'heatmap';
|
||||
|
||||
@@ -83,6 +89,117 @@ export function runQuery(query: string, language: Language): Promise<QueryResult
|
||||
return request('/query', { method: 'POST', body: JSON.stringify({ query, language }) });
|
||||
}
|
||||
|
||||
// ---- AI-assisted query authoring (Phase 7 Track A) ----
|
||||
// Every function here returns text/suggestions only -- running anything
|
||||
// still goes through runQuery above, unchanged, per the phase's
|
||||
// non-negotiable "no parallel execution path" design principle. A
|
||||
// deployment with no OLLAMA_BASE_URL configured has these routes
|
||||
// entirely unregistered server-side (api/cmd/api's main.go), so a 404
|
||||
// here is a normal, expected "AI isn't enabled" response, not a bug --
|
||||
// callers (QueryBar.svelte) treat any error from these functions as
|
||||
// "AI unavailable right now," never a user-facing failure.
|
||||
|
||||
export type Confidence = 'high' | 'medium' | 'low';
|
||||
|
||||
export function aiComplete(queryPrefix: string, language: string): Promise<{ suggestion: string }> {
|
||||
return request('/ai/complete', { method: 'POST', body: JSON.stringify({ queryPrefix, language }) });
|
||||
}
|
||||
|
||||
export function aiExplain(
|
||||
query: string,
|
||||
language: string,
|
||||
originalIntent?: string
|
||||
): Promise<{ explanation: string }> {
|
||||
return request('/ai/explain', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query, language, originalIntent: originalIntent ?? '' })
|
||||
});
|
||||
}
|
||||
|
||||
export type FixResponse = {
|
||||
suggestedQuery: string;
|
||||
explanation: string;
|
||||
confidence: Confidence | '';
|
||||
blocked: boolean;
|
||||
costWarnings?: string[];
|
||||
};
|
||||
|
||||
export function aiFix(
|
||||
query: string,
|
||||
language: string,
|
||||
opts: { parseError?: string; executionError?: string }
|
||||
): Promise<FixResponse> {
|
||||
return request('/ai/fix', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
language,
|
||||
parseError: opts.parseError ?? '',
|
||||
executionError: opts.executionError ?? ''
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export type OptimizeResponse = {
|
||||
findings: string[];
|
||||
phrased: string;
|
||||
suggestedQuery?: string;
|
||||
};
|
||||
|
||||
export function aiOptimize(query: string, language: string): Promise<OptimizeResponse> {
|
||||
return request('/ai/optimize', { method: 'POST', body: JSON.stringify({ query, language }) });
|
||||
}
|
||||
|
||||
// ---- Natural language translation (Phase 7 Track B) ----
|
||||
// Same non-negotiable split as every other AI operation: this returns a
|
||||
// query for review, never executes it. Running the result is the exact
|
||||
// same runQuery() above, reused unchanged -- task 9's explicit
|
||||
// requirement.
|
||||
export type TranslateResponse = {
|
||||
query: string;
|
||||
confidence: Confidence | '';
|
||||
lowConfidenceReason?: string;
|
||||
compiles: boolean;
|
||||
compileError?: string;
|
||||
blocked: boolean;
|
||||
costWarnings?: string[];
|
||||
};
|
||||
|
||||
export function aiTranslate(nlQuery: string): Promise<TranslateResponse> {
|
||||
return request('/ai/translate', { method: 'POST', body: JSON.stringify({ nlQuery }) });
|
||||
}
|
||||
|
||||
// ---- Interaction audit logging (task 12) ----
|
||||
// Fire-and-forget: a failure here (including AI being unconfigured
|
||||
// server-side, same 404-is-normal posture as every other /ai/* call)
|
||||
// must never block or surface an error for the accept/dismiss action
|
||||
// that triggered it, so callers should not await rejection handling
|
||||
// beyond a swallowed .catch(() => {}).
|
||||
export type InteractionOperation = 'translate' | 'fix' | 'optimize';
|
||||
|
||||
export function logInteraction(entry: {
|
||||
operation: InteractionOperation;
|
||||
input: string;
|
||||
output: string;
|
||||
confidence?: Confidence | '';
|
||||
accepted: boolean;
|
||||
edited: boolean;
|
||||
finalQuery?: string;
|
||||
}): Promise<void> {
|
||||
return request('/ai/log-interaction', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operation: entry.operation,
|
||||
input: entry.input,
|
||||
output: entry.output,
|
||||
confidence: entry.confidence ?? '',
|
||||
accepted: entry.accepted,
|
||||
edited: entry.edited,
|
||||
finalQuery: entry.finalQuery ?? ''
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function listDashboards(): Promise<Dashboard[]> {
|
||||
return request('/dashboards').then((d) => (d as Dashboard[]) ?? []);
|
||||
}
|
||||
@@ -355,3 +472,4 @@ export function createNotificationTarget(input: {
|
||||
}): Promise<NotificationTarget> {
|
||||
return alertingRequest('/targets', { method: 'POST', body: JSON.stringify(input) });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user