matrix-builder / web /src /lib /ai-refine.ts
matrix-builder-bot
deploy: matrix-builder@ae902df6985a
66d0c75
Raw
History Blame Contribute Delete
3.1 kB
// C3 β€” Optional AI enhancement for the Blueprint workspace.
//
// After the in-browser engine has ALREADY applied a chat instruction (instant, governed), this
// makes ONE provider-agnostic call (through OllaBridge, which can bridge to any model) to refine
// only the EXPLANATION β€” `overview` / `designBrain` prose and a short `reply`. It is:
// β€’ optional β€” returns null when assist is off / no user (deterministic result stands)
// β€’ bounded β€” a hard timeout; a slow model never blocks the UI
// β€’ fail-open β€” any error/parse-failure returns null
// β€’ safe β€” it can never change architecture, batches, or files (governance unchanged)
import { isOllaBridgeAssistAvailable, sendAIMessage } from "@/lib/ai-provider-manager";
import type { BlueprintDetailsData } from "@/types/blueprint-state";
export interface AIRefinement {
reply?: string;
overview?: string;
designBrain?: string;
}
const SYSTEM =
"You refine the EXPLANATION of a software blueprint after a change was applied. You may improve " +
"the 'overview' and 'designBrain' prose and write a short 'reply' confirming the change. You MUST " +
"NOT invent or change architecture, batches, files, or stack. Return ONLY compact JSON of the form " +
'{"reply":"...","overview":"...","designBrain":"..."} with no extra text.';
export async function refineWithAI(
data: BlueprintDetailsData,
instruction: string,
opts: { timeoutMs?: number } = {},
): Promise<AIRefinement | null> {
if (!isOllaBridgeAssistAvailable()) return null;
const timeoutMs = opts.timeoutMs ?? 6000;
const user =
`Instruction just applied: ${instruction}\n` +
`Current overview: ${data.overview}\n` +
`Batches: ${data.batches.map((b) => b.name).join(", ")}\n` +
`Architecture: ${data.architecture.map((n) => n.name).join(", ")}`;
try {
const text = await withTimeout(
sendAIMessage([
{ role: "system", content: SYSTEM },
{ role: "user", content: user },
]),
timeoutMs,
);
const json = extractJson(text);
if (!json) return null;
const out: AIRefinement = {};
if (typeof json.reply === "string" && json.reply.trim()) out.reply = json.reply.trim();
if (typeof json.overview === "string" && json.overview.trim()) out.overview = json.overview.trim();
if (typeof json.designBrain === "string" && json.designBrain.trim()) out.designBrain = json.designBrain.trim();
return Object.keys(out).length ? out : null;
} catch {
return null; // fail-open: the deterministic local result stands
}
}
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const t = setTimeout(() => reject(new Error("ai-refine timeout")), ms);
p.then((v) => { clearTimeout(t); resolve(v); }, (e) => { clearTimeout(t); reject(e); });
});
}
function extractJson(text: string): Record<string, unknown> | null {
const m = text.match(/\{[\s\S]*\}/);
if (!m) return null;
try {
return JSON.parse(m[0]) as Record<string, unknown>;
} catch {
return null;
}
}