Jai-rathore29's picture
Deploy: DocAgent backend (deterministic date-anomaly fix)
f65e025
Raw
History Blame Contribute Delete
4.42 kB
import type {
Classification,
DocumentDetail,
DocumentMeta,
ExtractionResult,
HealthInfo,
} from "./types";
// All requests go through Next's rewrite proxy to the FastAPI backend.
const BASE = "";
async function json<T>(res: Response): Promise<T> {
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`${res.status} ${res.statusText}: ${text}`);
}
return res.json() as Promise<T>;
}
export const api = {
health: () => fetch(`${BASE}/api/health`).then((r) => json<HealthInfo>(r)),
listDocuments: () =>
fetch(`${BASE}/api/documents`).then((r) => json<DocumentMeta[]>(r)),
getDocument: (id: string) =>
fetch(`${BASE}/api/documents/${id}`).then((r) => json<DocumentDetail>(r)),
upload: (file: File) => {
const fd = new FormData();
fd.append("file", file);
return fetch(`${BASE}/api/documents`, { method: "POST", body: fd }).then(
(r) => json<DocumentMeta>(r),
);
},
deleteDocument: (id: string) =>
fetch(`${BASE}/api/documents/${id}`, { method: "DELETE" }).then((r) =>
json<{ ok: boolean }>(r),
),
reExtract: (id: string, schema?: string, provider?: string) => {
const q = new URLSearchParams();
if (schema) q.set("schema", schema);
if (provider) q.set("provider", provider);
return fetch(`${BASE}/api/documents/${id}/extract?${q}`, {
method: "POST",
}).then((r) => json<ExtractionResult>(r));
},
reClassify: (id: string, provider?: string) => {
const q = new URLSearchParams();
if (provider) q.set("provider", provider);
return fetch(`${BASE}/api/documents/${id}/classify?${q}`, {
method: "POST",
}).then((r) => json<Classification>(r));
},
reSummarize: (id: string, provider?: string) => {
const q = new URLSearchParams();
if (provider) q.set("provider", provider);
return fetch(`${BASE}/api/documents/${id}/summarize?${q}`, {
method: "POST",
}).then((r) => json<{ summary: string }>(r));
},
exportUrl: (id: string, format: string) =>
`${BASE}/api/documents/${id}/export?format=${format}`,
schemas: () =>
fetch(`${BASE}/api/documents/-/schemas`).then((r) =>
json<{ schemas: string[] }>(r),
),
};
// --------------------------- streaming chat ---------------------------
export interface ChatStreamHandlers {
onToken: (text: string) => void;
onCitations: (citations: import("./types").Citation[]) => void;
onDone: () => void;
onError: (msg: string) => void;
}
/**
* POST to /api/chat and parse the SSE stream manually (EventSource only
* supports GET, so we read the response body as a stream ourselves).
*/
export async function streamChat(
body: {
document_id?: string | null;
messages: { role: string; content: string }[];
provider?: string | null;
},
handlers: ChatStreamHandlers,
signal?: AbortSignal,
) {
try {
const res = await fetch(`${BASE}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal,
});
if (!res.ok || !res.body) {
throw new Error(`Chat request failed: ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE frames are separated by a blank line
const frames = buffer.split("\n\n");
buffer = frames.pop() ?? "";
for (const frame of frames) {
let event = "message";
let data = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (!data) continue;
let parsed: any;
try {
parsed = JSON.parse(data);
} catch {
continue;
}
if (event === "token") handlers.onToken(parsed.text ?? "");
else if (event === "citations")
handlers.onCitations(parsed.citations ?? []);
else if (event === "done") handlers.onDone();
else if (event === "error") handlers.onError(parsed.message ?? "error");
}
}
handlers.onDone();
} catch (e: any) {
if (e?.name !== "AbortError") handlers.onError(e?.message ?? String(e));
}
}