drivecore / src /lib /incidents.functions.ts
gpt-engineer-app[bot]
Changes
58360ec
Raw
History Blame Contribute Delete
6.55 kB
import { createServerFn } from "@tanstack/react-start";
import { supabaseAdmin } from "@/integrations/supabase/client.server";
import { fetchAIWithFallback, getAIConfig } from "./ai-config.server";
import { AV_KNOWLEDGE_BASE } from "./av-knowledge.server";
import { z } from "zod";
const InputSchema = z.object({
incidentId: z.string().uuid(),
});
type AgentResult = {
summary: string;
events: string[];
rootCauses: string[];
complianceFlags: { code: string; description: string; severity: "low" | "medium" | "high" }[];
coachingRecommendations: string[];
severity: "low" | "medium" | "high" | "critical" | "unknown";
reportMarkdown: string;
};
const analysisTool = {
type: "function" as const,
function: {
name: "submit_analysis",
description: "Submit structured AV incident analysis from a multi-agent safety review.",
parameters: {
type: "object",
properties: {
summary: { type: "string", description: "2-4 sentence executive summary of the incident." },
events: { type: "array", items: { type: "string" }, description: "Key timestamped or sequential events extracted (Event Extraction Agent)." },
rootCauses: { type: "array", items: { type: "string" }, description: "Probable root causes (Risk Agent)." },
complianceFlags: {
type: "array",
items: {
type: "object",
properties: {
code: { type: "string", description: "Standard / regulation, e.g. NHTSA-AV-4.1.2, ISO 26262, SAE J3016" },
description: { type: "string" },
severity: { type: "string", enum: ["low", "medium", "high"] },
},
required: ["code", "description", "severity"],
additionalProperties: false,
},
description: "Compliance concerns identified by the Safety Agent.",
},
coachingRecommendations: { type: "array", items: { type: "string" }, description: "Operator/engineer coaching actions." },
severity: { type: "string", enum: ["low", "medium", "high", "critical", "unknown"] },
reportMarkdown: { type: "string", description: "A polished safety report in Markdown for export (Documentation Agent)." },
},
required: ["summary", "events", "rootCauses", "complianceFlags", "coachingRecommendations", "severity", "reportMarkdown"],
additionalProperties: false,
},
},
};
const SYSTEM_PROMPT = `You are DriveCore Incident Bot, a multi-agent AI safety analyst powered by Qwen3 reasoning. You orchestrate four specialised agents on every input:
1. EVENT EXTRACTION AGENT β€” pulls discrete timeline events from logs, transcripts, sensor data, or free-form notes.
2. SAFETY AGENT β€” identifies compliance concerns referencing standards (NHTSA AV Policy, ISO 26262, SAE J3016, FMVSS, UN R157) when relevant.
3. RISK AGENT β€” identifies probable root causes (sensor failure, perception, planning, control, environmental, operator) or general risk factors.
4. DOCUMENTATION AGENT β€” drafts a clean Markdown report with sections (Summary, Timeline, Root Causes, Compliance, Recommendations).
The user may submit ANY free-form text β€” full incident reports, brief notes, questions, partial logs, or general descriptions. Reason freely with your full intelligence: infer context, fill gaps with plausible domain knowledge, and produce useful analysis even when input is sparse or ambiguous. Mark uncertainty where appropriate. Always call submit_analysis with the structured result.`;
export const analyzeIncident = createServerFn({ method: "POST" })
.inputValidator((d: unknown) => InputSchema.parse(d))
.handler(async ({ data }) => {
const supabase = supabaseAdmin;
getAIConfig(); // validates env config early
const { data: incident, error: fetchErr } = await supabase
.from("incidents")
.select("*")
.eq("id", data.incidentId)
.single();
if (fetchErr || !incident) throw new Response("Incident not found", { status: 404 });
await supabase.from("incidents").update({ status: "analyzing", error: null }).eq("id", data.incidentId);
try {
const { data: learnings } = await supabase
.from("qwen_learnings")
.select("category, content, context")
.order("created_at", { ascending: false })
.limit(20);
const learningsBlock = learnings && learnings.length
? `PRIOR LEARNINGS (operator corrections, insights, past errors β€” apply these going forward):\n${learnings
.map((l: any, i: number) => `${i + 1}. [${l.category}] ${l.content}${l.context ? ` (context: ${l.context})` : ""}`)
.join("\n")}`
: "PRIOR LEARNINGS: (none yet)";
const userContent = `INCIDENT TITLE: ${incident.title}\nSOURCE TYPE: ${incident.source_type}\nFILE: ${incident.file_name ?? "(none)"}\n\n--- RAW INPUT ---\n${incident.raw_text ?? "(no text content provided)"}`;
const requestBody = JSON.stringify({
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "system", content: AV_KNOWLEDGE_BASE },
{ role: "system", content: learningsBlock },
{ role: "user", content: userContent },
],
tools: [analysisTool],
tool_choice: { type: "function", function: { name: "submit_analysis" } },
});
const resp = await fetchAIWithFallback(requestBody, "google/gemini-2.5-flash", "analyzeIncident");
if (!resp.ok) {
const text = await resp.text();
if (resp.status === 429) throw new Error("Rate limit reached. Try again shortly.");
if (resp.status === 402) throw new Error("AI credits exhausted. Add credits in Workspace > Usage.");
throw new Error(`AI gateway error ${resp.status}: ${text.slice(0, 200)}`);
}
const json = await resp.json();
const toolCall = json.choices?.[0]?.message?.tool_calls?.[0];
if (!toolCall?.function?.arguments) throw new Error("AI did not return structured analysis.");
const analysis: AgentResult = JSON.parse(toolCall.function.arguments);
await supabase
.from("incidents")
.update({
analysis: analysis as any,
severity: analysis.severity,
status: "complete",
})
.eq("id", data.incidentId);
return { ok: true, analysis };
} catch (e: any) {
await supabase
.from("incidents")
.update({ status: "failed", error: e.message ?? "Unknown error" })
.eq("id", data.incidentId);
throw e;
}
});