ai-explain.ts109 lines · main
1import { env } from '../env.js';
2import { log } from '../lib/logger.js';
3
4import { AiNotConfiguredError } from './ai-schema-gen.js';
5
6/**
7 * AI explain code — third member of the briven AI trifecta (schema +
8 * function + explain). Takes a snippet of briven schema/function code
9 * and returns a plain-english explanation framed in briven idioms (what
10 * the wrapper means, which db calls happen, where the reactivity hooks
11 * in, what would change if you flipped it from query() to mutation()).
12 *
13 * Same posture as the other two generators: forwards to Ollama on the
14 * DGX VPS; prompts and outputs are not logged; returns the shared
15 * AiNotConfiguredError when BRIVEN_OLLAMA_URL is unset so callers can
16 * collapse the 503 path.
17 */
18
19export const EXPLAIN_SYSTEM_PROMPT = `You are a briven code explainer. Given a snippet of typescript using briven's schema dsl or function wrappers, explain what it does in plain english.
20
21Rules:
22- Explain at the level of "a developer new to briven, comfortable with typescript". Don't restate the obvious (e.g. "this imports a function from a package").
23- Call out the briven-specific decisions:
24 - query() vs mutation() vs action() — what does the wrapper mean for reactivity, transactions, retries?
25 - ctx.db chains — which tables are touched, which indexes would matter?
26 - brivenError vs throwing a plain Error — what code/status does the client receive?
27 - schema column choices — when does notNull / references / unique matter?
28 - jsonb<T> type args — what does the type assertion give you on reads?
29- If the snippet has a bug or a sharp edge, point it out (e.g. unbounded result sets, missing input validation, n+1 patterns).
30- Keep the explanation under 250 words. Use short paragraphs and bullet lists, not walls of prose. No markdown code fences.
31- If the snippet isn't briven code (e.g. unrelated typescript, sql, prose) say so in one sentence and stop — don't try to make sense of it.`;
32
33export interface AiExplainInput {
34 /** The code to explain. Capped at 8 KB at the route to keep the model context bounded. */
35 code: string;
36 /** Optional caller note: "I'm new to briven", "I migrated from prisma", etc. Shapes the depth. */
37 perspective?: string;
38 timeoutMs?: number;
39}
40
41export interface AiExplainResult {
42 explanation: string;
43 model: string;
44 elapsedMs: number;
45}
46
47export async function explainCode(input: AiExplainInput): Promise<AiExplainResult> {
48 if (!env.BRIVEN_OLLAMA_URL) {
49 throw new AiNotConfiguredError();
50 }
51
52 const userMessage =
53 input.perspective && input.perspective.trim().length > 0
54 ? `Perspective: ${input.perspective.trim()}\n\nExplain this code:\n\`\`\`ts\n${input.code}\n\`\`\``
55 : `Explain this code:\n\`\`\`ts\n${input.code}\n\`\`\``;
56
57 // Per-feature model override per docs/AI.md — explain benefits most
58 // from a reasoning-tuned variant (e.g. deepseek-r1-distill-qwen-32b).
59 const model = env.BRIVEN_OLLAMA_MODEL_EXPLAIN ?? env.BRIVEN_OLLAMA_MODEL;
60 const t0 = Date.now();
61 const url = `${env.BRIVEN_OLLAMA_URL.replace(/\/$/, '')}/api/generate`;
62 const headers: Record<string, string> = { 'content-type': 'application/json' };
63 if (env.BRIVEN_OLLAMA_API_KEY) {
64 headers['x-api-key'] = env.BRIVEN_OLLAMA_API_KEY;
65 }
66 const res = await fetch(url, {
67 method: 'POST',
68 headers,
69 body: JSON.stringify({
70 model,
71 system: EXPLAIN_SYSTEM_PROMPT,
72 prompt: userMessage,
73 // Higher than schema/function generators — explanation is prose,
74 // where a touch more variation reads as natural rather than
75 // robotic. Still well below "creative writing" temps.
76 options: { temperature: 0.4 },
77 stream: false,
78 }),
79 signal: AbortSignal.timeout(input.timeoutMs ?? 60_000),
80 });
81
82 const elapsedMs = Date.now() - t0;
83 if (!res.ok) {
84 const body = await res.text().catch(() => '');
85 log.warn('ai_explain_upstream_error', {
86 status: res.status,
87 elapsedMs,
88 bodyPreview: body.slice(0, 240),
89 });
90 throw new Error(`Ollama returned ${res.status}`);
91 }
92
93 const data = (await res.json()) as { response?: string };
94 const explanation = (data.response ?? '').trim();
95
96 log.info('ai_explain_ok', {
97 codeLen: input.code.length,
98 perspectiveLen: input.perspective?.length ?? 0,
99 explanationLen: explanation.length,
100 model,
101 elapsedMs,
102 });
103
104 return {
105 explanation,
106 model,
107 elapsedMs,
108 };
109}