import type { UploadResponse, ScrapeAndIndexRequest, ScrapeAndIndexResponse, DeleteDocumentResponse, StreamQueryRequest, QueryRequest, QueryResponse, SummarizeRequest, SummarizeResponse, ResetResponse, StatusResponse, SessionSummary, SessionMessagesResponse, RenameSessionRequest, ApiErrorBody, StreamEvent, } from './types.ts' async function parseErrorDetail(response: Response): Promise { try { const body = (await response.json()) as ApiErrorBody if (body && typeof body.detail === 'string') { return body.detail } } catch { // fall through } return `Request failed with status ${response.status}` } async function requestJson(input: RequestInfo, init?: RequestInit): Promise { const response = await fetch(input, init) if (!response.ok) { throw new Error(await parseErrorDetail(response)) } return (await response.json()) as T } function jsonInit(body: unknown, signal?: AbortSignal): RequestInit { return { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal, } } export async function uploadFile(file: File, signal?: AbortSignal): Promise { const formData = new FormData() formData.append('file', file) return requestJson('/upload', { method: 'POST', body: formData, signal }) } export async function scrapeAndIndex( req: ScrapeAndIndexRequest, signal?: AbortSignal, ): Promise { return requestJson('/scrape_and_index', jsonInit(req, signal)) } export async function deleteDocument(documentName: string): Promise { return requestJson( `/documents/${encodeURIComponent(documentName)}`, { method: 'DELETE' }, ) } export async function query(req: QueryRequest, signal?: AbortSignal): Promise { return requestJson('/query', jsonInit(req, signal)) } export async function summarize( req: SummarizeRequest, signal?: AbortSignal, ): Promise { return requestJson('/summarize', jsonInit(req, signal)) } export async function resetDocuments(): Promise { return requestJson('/reset', { method: 'POST' }) } export async function getStatus(signal?: AbortSignal): Promise { return requestJson('/status', { signal }) } export async function listSessions(signal?: AbortSignal): Promise { return requestJson('/sessions', { signal }) } export async function getSessionMessages( sessionId: string, signal?: AbortSignal, ): Promise { return requestJson( `/sessions/${encodeURIComponent(sessionId)}/messages`, { signal }, ) } export async function renameSession( sessionId: string, req: RenameSessionRequest, ): Promise { return requestJson(`/sessions/${encodeURIComponent(sessionId)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }) } export async function deleteSession(sessionId: string): Promise { return requestJson(`/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE', }) } /** * Streams /stream_query as an async generator of parsed SSE frames. * Consumes the response body manually via a reader (not EventSource, since * this is a POST request). */ export async function* streamQuery( question: string, sessionId?: string, signal?: AbortSignal, ): AsyncGenerator { const body: StreamQueryRequest = { question, session_id: sessionId } const response = await fetch('/stream_query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal, }) if (!response.ok || !response.body) { const detail = await parseErrorDetail(response) yield { type: 'error', error: detail } return } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() ?? '' for (const line of lines) { if (!line.startsWith('data: ')) continue const data = line.slice(6) if (data === '[DONE]') continue let parsed: unknown try { parsed = JSON.parse(data) } catch { continue } if (parsed && typeof parsed === 'object') { const obj = parsed as Record if (typeof obj.session_id === 'string') { yield { type: 'session', sessionId: obj.session_id } } else if (typeof obj.token === 'string') { yield { type: 'token', token: obj.token } } else if (typeof obj.final_answer === 'string') { yield { type: 'final', answer: obj.final_answer, sources: Array.isArray(obj.sources) ? (obj.sources as QueryResponse['sources']) : [], } } else if (typeof obj.error === 'string') { yield { type: 'error', error: obj.error } } } } } } finally { reader.releaseLock() } }