import type { Data, Layout } from "plotly.js"; import { getEnv } from "@/env"; const AGENTIC_BASE_URL = getEnv("VITE_AGENTIC_API_BASE_URL"); export class AgenticApiError extends Error { status: number; data: unknown; constructor(message: string, status: number, data?: unknown) { super(message); this.name = "AgenticApiError"; this.status = status; this.data = data; } } export type AgentStreamEvent = | { type: "sources"; data: unknown[] } | { type: "status"; data: string } | { type: "chunk"; data: string } | { type: "done"; data: { message_id?: string; [key: string]: unknown } } | { type: "error"; data: string }; export interface AgentChatRequest { user_id: string; analysis_id: string; message: string; } export interface Tool { command: "/help" | "/report" | string; name: "help" | "report" | string; type: "skill" | string; description: string; } export interface ObservabilityPlanningStep { step?: number; stage?: string; objective?: string; } export interface ObservabilityPlanning { goal_restated?: string; assumptions?: string[]; steps?: ObservabilityPlanningStep[]; } export interface ObservabilityToolCall { order?: number; task_id?: string | null; name: string; summary?: string; input?: unknown; output?: unknown; status?: string; error?: string | null; } export interface ObservabilityDataUsedSource { id: string; name: string; type: string; } export interface ObservabilityDataUsedTable { id: string; name: string; role?: string; } export interface ObservabilityDataUsedColumn { id: string; name: string; table?: string; data_type?: string; pii?: boolean; roles?: string[]; } export interface ObservabilityDataUsedOutputColumn { name: string; kind?: string; from?: string; formula?: string; } export interface ObservabilityDataUsed { source: ObservabilityDataUsedSource; tables?: ObservabilityDataUsedTable[]; joins?: unknown[]; columns_read?: ObservabilityDataUsedColumn[]; output_columns?: ObservabilityDataUsedOutputColumn[]; filters?: unknown[]; group_by?: string[]; order_by?: unknown[]; limit?: number | null; rows_returned?: number; query?: string; } export interface ChartSpec { schema: string; chart_type: string; title?: string | null; plotly: { data: Data[]; layout?: Partial; }; } export interface ChartItem { chart_id: string; chart_type: string; title?: string | null; spec: ChartSpec; created_at: string; } export interface ChartsResponse { status: "success" | "empty" | "not_found"; message: string; count: number; charts: ChartItem[]; } export interface Observability { analysis_id: string; message_id: string; intent?: string; generated_at?: string; planning: ObservabilityPlanning | null; thinking: string | null; tool_calls: ObservabilityToolCall[]; data_used?: ObservabilityDataUsed[]; sources: unknown[]; } export interface ReportSummary { report_id: string; version: number; generated_at: string; record_count?: number; } export interface ReportFinding { text: string; record_ids?: string[]; supporting_data?: unknown; } export interface ReportDetail { report_id: string; analysis_id: string; user_id: string; version: number; generated_at: string; problem_statement?: { objective?: string; business_questions?: string[]; }; record_ids?: string[]; executive_summary?: string; findings?: ReportFinding[]; caveats?: ReportFinding[]; open_questions?: ReportFinding[]; data_sources?: unknown[]; method_steps?: unknown[]; rendered_markdown?: string; } async function parseAgenticError(res: Response): Promise { const body = await res.json().catch(() => null); const message = (body && typeof body === "object" && "detail" in body && String((body as { detail?: string }).detail)) || (body && typeof body === "object" && "message" in body && String((body as { message?: string }).message)) || `HTTP ${res.status}`; return new AgenticApiError(message, res.status, body); } async function agenticJson(path: string, options: RequestInit = {}): Promise { const headers = new Headers(options.headers); if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json"); const res = await fetch(`${AGENTIC_BASE_URL}${path}`, { ...options, headers, }); if (!res.ok) throw await parseAgenticError(res); return (await res.json()) as T; } function parseSseData(eventName: string, data: string): AgentStreamEvent { if (eventName === "sources") { try { const parsed = JSON.parse(data); return { type: "sources", data: Array.isArray(parsed) ? parsed : [] }; } catch { return { type: "sources", data: [] }; } } if (eventName === "done") { try { return { type: "done", data: JSON.parse(data) }; } catch { return { type: "done", data: {} }; } } if (eventName === "error") return { type: "error", data }; if (eventName === "status") return { type: "status", data }; return { type: "chunk", data }; } async function readEventStream(res: Response, onEvent: (event: AgentStreamEvent) => void): Promise { if (!res.ok) throw await parseAgenticError(res); if (!res.body) throw new AgenticApiError("Streaming is not supported by this browser", 0); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; const emitBlock = (rawBlock: string) => { const lines = rawBlock.split(/\r?\n/); let eventName = "chunk"; const dataLines: string[] = []; for (const line of lines) { if (line.startsWith("event:")) eventName = line.slice(6).trim(); if (line.startsWith("data:")) { const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5); dataLines.push(value); } } if (dataLines.length === 0) return; onEvent(parseSseData(eventName, dataLines.join("\n"))); }; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split(/\r?\n\r?\n/); buffer = parts.pop() ?? ""; parts.forEach(emitBlock); } if (buffer.trim()) emitBlock(buffer); } export async function streamChat(request: AgentChatRequest, onEvent: (event: AgentStreamEvent) => void): Promise { const res = await fetch(`${AGENTIC_BASE_URL}/api/v2/chat/stream`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), }); await readEventStream(res, onEvent); } export async function streamHelp( request: { user_id: string; analysis_id: string }, onEvent: (event: AgentStreamEvent) => void ): Promise { const res = await fetch(`${AGENTIC_BASE_URL}/api/v1/tools/help`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), }); await readEventStream(res, onEvent); } export const listTools = (): Promise<{ count: number; tools: Tool[] }> => agenticJson("/api/v1/tools/list"); export const getObservability = (analysisId: string, messageId: string): Promise => agenticJson( `/api/v1/traceability?analysis_id=${encodeURIComponent(analysisId)}&message_id=${encodeURIComponent(messageId)}` ); export const getCharts = (messageId: string): Promise => agenticJson(`/api/v1/charts?message_id=${encodeURIComponent(messageId)}`); export const generateReport = (analysisId: string, userId: string): Promise => agenticJson( `/api/v1/tools/report?analysis_id=${encodeURIComponent(analysisId)}&user_id=${encodeURIComponent(userId)}`, { method: "POST" } ); export const listReportVersions = (analysisId: string): Promise => agenticJson(`/api/v1/tools/report/${encodeURIComponent(analysisId)}`); export const getReportVersion = (analysisId: string, version: number): Promise => agenticJson(`/api/v1/tools/report/${encodeURIComponent(analysisId)}/${encodeURIComponent(String(version))}`);