harryagasi
feat: fetch and render charts for chat answers via /api/v1/charts
b5e46e4
Raw
History Blame Contribute Delete
8.15 kB
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<Layout>;
};
}
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<AgenticApiError> {
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<T>(path: string, options: RequestInit = {}): Promise<T> {
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<void> {
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<void> {
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<void> {
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<Observability> =>
agenticJson(
`/api/v1/traceability?analysis_id=${encodeURIComponent(analysisId)}&message_id=${encodeURIComponent(messageId)}`
);
export const getCharts = (messageId: string): Promise<ChartsResponse> =>
agenticJson(`/api/v1/charts?message_id=${encodeURIComponent(messageId)}`);
export const generateReport = (analysisId: string, userId: string): Promise<ReportDetail> =>
agenticJson(
`/api/v1/tools/report?analysis_id=${encodeURIComponent(analysisId)}&user_id=${encodeURIComponent(userId)}`,
{ method: "POST" }
);
export const listReportVersions = (analysisId: string): Promise<ReportSummary[]> =>
agenticJson(`/api/v1/tools/report/${encodeURIComponent(analysisId)}`);
export const getReportVersion = (analysisId: string, version: number): Promise<ReportDetail> =>
agenticJson(`/api/v1/tools/report/${encodeURIComponent(analysisId)}/${encodeURIComponent(String(version))}`);