import type { QuickQuestionsResponse, SSEMessage } from './index'; const AGENT_API_BASE = '/api/v1'; const TOKEN_KEY = 'auth_token'; function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } const _FALLBACK_QUESTIONS = [ { id: '1', question: '请总结最新入库法规对电池安全的核心要求', category: '法规解读' }, { id: '2', question: '我上传的制度文档与新能源法规有哪些潜在冲突?', category: '差距分析' }, { id: '3', question: '请给出法规依据,并按条款列出整改建议', category: '整改建议' }, { id: '4', question: '请解释 UN-ECE 与 GB 标准在网络安全方面的差异', category: '标准对比' }, ]; export async function getQuickQuestions(): Promise { try { const response = await fetch(`${AGENT_API_BASE}/rag/quick-questions`); if (!response.ok) throw new Error(`status ${response.status}`); return response.json() as Promise; } catch { return { questions: _FALLBACK_QUESTIONS }; } } function parseSSEChunk(raw: string, onMessage: (data: SSEMessage) => void) { const blocks = raw.split('\n\n'); for (const block of blocks) { if (!block.trim()) continue; let eventName = 'message'; const dataLines: string[] = []; for (const line of block.split('\n')) { if (line.startsWith('event:')) { eventName = line.slice(6).trim(); } else if (line.startsWith('data:')) { dataLines.push(line.slice(5).trim()); } } const joined = dataLines.join('\n'); if (!joined) continue; // /agent/chat/stream uses named events (sources, content, done, error, session, status) // /rag/chat wraps everything in event:message with type in JSON body — handle both if (eventName === 'session') { try { const payload = JSON.parse(joined) as Record; onMessage({ type: 'session', session_id: String(payload.session_id ?? '') }); } catch { /* ignore */ } } else if (eventName === 'sources') { try { const docs = JSON.parse(joined) as Array>; onMessage({ type: 'retrieved', docs: docs.map((doc, index) => ({ id: String(doc.doc_id || doc.index || index + 1), score: Number(doc.score || 0), preview: String(doc.content || doc.snippet || ''), doc_name: String(doc.doc_name || doc.filename || `引用 ${index + 1}`), clause: String(doc.clause_number || doc.section_title || '法规片段'), doc_id: doc.doc_id ? String(doc.doc_id) : undefined, download_url: doc.doc_id ? `${AGENT_API_BASE}/documents/download/${String(doc.doc_id)}` : undefined, })), }); } catch { /* ignore */ } } else if (eventName === 'content') { onMessage({ type: 'chunk', text: joined }); } else if (eventName === 'done') { try { const payload = JSON.parse(joined) as Record; onMessage({ type: 'done', session_id: payload.session_id ? String(payload.session_id) : undefined }); } catch { onMessage({ type: 'done' }); } } else if (eventName === 'error') { onMessage({ type: 'error', text: joined }); } else if (eventName === 'status') { onMessage({ type: 'status', text: joined }); } else if (eventName === 'thinking') { // P0-1: Agentic reasoning step events from /agent/agentic/stream try { const payload = JSON.parse(joined) as Record; onMessage({ type: 'thinking', step: payload.step as string | undefined, status: payload.status as string | undefined, intent_type: payload.intent_type as string | undefined, requires_decomposition: payload.requires_decomposition as boolean | undefined, reason: payload.reason as string | undefined, sub_queries: payload.sub_queries as string[] | undefined, query: payload.query as string | undefined, index: payload.index as number | undefined, total: payload.total as number | undefined, found: payload.found as number | undefined, retry: payload.retry as boolean | undefined, sufficient: payload.sufficient as boolean | undefined, confidence: payload.confidence as number | undefined, }); } catch { /* ignore */ } } else if (eventName === 'message') { // /rag/chat format: event:message + JSON body with type field try { const payload = JSON.parse(joined) as SSEMessage; onMessage(payload); } catch { /* ignore */ } } } } export async function ragChat( query: string, topK: number = 5, onMessage: (data: SSEMessage) => void, onError?: (error: Error) => void, onComplete?: () => void, filters?: string, sessionId?: string, signal?: AbortSignal, ): Promise { try { const response = await fetch(`${AGENT_API_BASE}/agent/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', ...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}), }, body: JSON.stringify({ query, top_k: topK, ...(filters ? { filters } : {}), ...(sessionId ? { session_id: sessionId } : {}), }), signal, }); if (!response.ok || !response.body) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n\n'); buffer = parts.pop() || ''; parseSSEChunk(parts.join('\n\n'), onMessage); } if (buffer.trim()) { parseSSEChunk(buffer, onMessage); } if (onComplete) { onComplete(); } } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') return; if (onError) { onError(error instanceof Error ? error : new Error(String(error))); } } } export type { QuickQuestionsResponse, SSEMessage }; /** * P0-1 Agentic RAG chat — calls /agent/agentic/stream which runs the full * intent-analysis → query-planning → retrieval → grounding-check → answer pipeline. * * The onMessage callback receives the same event types as ragChat plus * ``type: 'thinking'`` events that carry live reasoning-step progress. */ export async function agenticChat( query: string, topK: number = 5, onMessage: (data: SSEMessage) => void, onError?: (error: Error) => void, onComplete?: () => void, filters?: string, sessionId?: string, signal?: AbortSignal, contextText?: string, contextFilename?: string, ): Promise { try { const response = await fetch(`${AGENT_API_BASE}/agent/agentic/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', ...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}), }, body: JSON.stringify({ query, top_k: topK, ...(filters ? { filters } : {}), ...(sessionId ? { session_id: sessionId } : {}), ...(contextText ? { context_text: contextText, context_filename: contextFilename ?? '' } : {}), }), signal, }); if (!response.ok || !response.body) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n\n'); buffer = parts.pop() || ''; parseSSEChunk(parts.join('\n\n'), onMessage); } if (buffer.trim()) { parseSSEChunk(buffer, onMessage); } if (onComplete) { onComplete(); } } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') return; if (onError) { onError(error instanceof Error ? error : new Error(String(error))); } } }