File size: 6,528 Bytes
6b62834
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
const API_BASE = localStorage.getItem('agr_api_base') || '';

async function request(path, opts = {}) {
  const url = `${API_BASE}${path}`;
  const config = { headers: {}, ...opts };
  if (config.body && typeof config.body === 'object' && !(config.body instanceof FormData)) {
    config.headers['Content-Type'] = 'application/json';
    config.body = JSON.stringify(config.body);
  }
  const res = await fetch(url, config);
  if (!res.ok) {
    const errBody = await res.json().catch(() => null);
    throw new Error(errBody?.detail || errBody?.error || `HTTP ${res.status}`);
  }
  return res;
}

export const api = {
  health: () => request('/health').then(r => r.json()),
  readiness: () => request('/ready').then(r => r.json()),

  // Chat
  createSession: (userId = 'web_ui') =>
    request('/api/v1/session', { method: 'POST', body: { user_id: userId } }).then(r => r.json()),
  getSession: (id) => request(`/api/v1/session/${id}`).then(r => r.json()),
  getMessages: (id) => request(`/api/v1/session/${id}/messages`).then(r => r.json()),
  listSessions: (userId = 'default') => request(`/api/v1/sessions?user_id=${userId}`).then(r => r.json()),
  updateSessionTitle: (sessionId, title) =>
    request(`/api/v1/session/${sessionId}?title=${encodeURIComponent(title)}`, { method: 'PUT' }).then(r => r.json()),
  deleteSession: (sessionId) =>
    request(`/api/v1/session/${sessionId}`, { method: 'DELETE' }).then(r => r.json()),
  clearSessionMessages: (sessionId) =>
    request(`/api/v1/session/${sessionId}/messages`, { method: 'DELETE' }).then(r => r.json()),
  deleteAllSessions: () =>
    request('/api/v1/sessions', { method: 'DELETE' }).then(r => r.json()),

  // RAG
  search: (query, topK = 10, mode = 'hybrid', modality) =>
    request('/api/v1/rag/search', {
      method: 'POST',
      body: { query, top_k: topK, mode, ...(modality && modality !== 'all' ? { modality_filter: [modality] } : {}) },
    }).then(r => r.json()),
  stats: () => request('/api/v1/rag/stats').then(r => r.json()),
  listDocuments: () => request('/api/v1/rag/documents').then(r => r.json()),
  getDocument: (docId) => request(`/api/v1/rag/document/${docId}`).then(r => r.json()),
  deleteDocument: (docId) => request(`/api/v1/rag/document/${docId}`, { method: 'DELETE' }).then(r => r.json()),
  clearKB: () => request('/api/v1/rag/clear', { method: 'POST' }).then(r => r.json()),
  uploadFile: (file, source, ingestMode, mmMethod, chunkSize = 512, chunkOverlap = 50, enableKG = true) => {
    const fd = new FormData();
    fd.append('file', file);
    fd.append('source', source);
    fd.append('ingest_mode', ingestMode);
    fd.append('mm_method', mmMethod);
    fd.append('chunk_size', chunkSize);
    fd.append('chunk_overlap', chunkOverlap);
    fd.append('enable_kg', enableKG);
    return request('/api/v1/rag/upload', { method: 'POST', body: fd }).then(r => r.json());
  },
  serveFile: (path) => `${API_BASE}/api/v1/rag/file/${path.split('/').map(encodeURIComponent).join('/')}`,

  // Voice (streaming SSE)
  voiceChatStream: async (audioBlob, sessionId, mode, tts, onEvent, signal) => {
    const fd = new FormData();
    fd.append('audio', audioBlob, 'recording.webm');
    fd.append('session_id', sessionId || '');
    fd.append('mode', mode || '');
    fd.append('tts', tts !== false);
    const res = await fetch(`${API_BASE}/api/v1/chat/voice`, { method: 'POST', body: fd, signal });
    if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || `HTTP ${res.status}`); }
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '', currentEvent = '';
    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('event: ')) {
          currentEvent = line.slice(7).trim();
        } else if (line.startsWith('data: ') && currentEvent) {
          try {
            const data = JSON.parse(line.slice(6));
            onEvent({ type: currentEvent, data });
          } catch {}
          currentEvent = '';
        }
      }
    }
  },

  // Settings
  getSettings: () => request('/api/v1/settings').then(r => r.json()),
  saveSettings: (sections) => request('/api/v1/settings', { method: 'PUT', body: { sections } }).then(r => r.json()),
  testConnection: (payload) => request('/api/v1/settings/test', { method: 'POST', body: payload }).then(r => r.json()),

  // MCP
  getMcpConfig: () => request('/api/v1/mcp/servers').then(r => r.json()),
  saveMcpConfig: (servers) => request('/api/v1/mcp/servers', { method: 'PUT', body: { servers } }).then(r => r.json()),
};

export async function streamChat(message, sessionId, images, onEvent, signal) {
  const res = await fetch(`${API_BASE}/api/v1/chat/stream`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message, session_id: sessionId, stream: true, images }),
    signal,
  });

  // Check HTTP status before processing stream
  if (!res.ok) {
    const errBody = await res.json().catch(() => null);
    throw new Error(errBody?.detail || errBody?.error || `HTTP ${res.status}`);
  }

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let receivedDone = false;

  try {
    while (!receivedDone) {
      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 s = line.slice(6).trim();
        if (!s || s === '[DONE]') {
          receivedDone = true;
          break;
        }
        try {
          const parsed = JSON.parse(s);
          const type = parsed.event || parsed.event_type;
          let data = parsed.data || parsed;
          if (typeof data === 'string') {
            try { data = JSON.parse(data); } catch { /* keep raw string if unparseable */ }
          }
          onEvent({ type, data });
          // Stop reading when we receive the done event
          if (type === 'done') {
            receivedDone = true;
            break;
          }
        } catch { /* skip malformed */ }
      }
    }
  } finally {
    // Always release the reader to clean up resources
    reader.releaseLock();
  }
}