@apowerb/apowerb-sdk 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api.js CHANGED
@@ -1,830 +1,830 @@
1
- import {
2
- apiUrl,
3
- clearAuth,
4
- getAuthToken,
5
- notifyUnauthorized,
6
- setAuthToken,
7
- } from "./config.js";
8
-
9
-
10
- function getAuthHeaders() {
11
- const token = getAuthToken();
12
- if (token) {
13
- return { Authorization: `Bearer ${token}` };
14
- }
15
- return {};
16
- }
17
-
18
- // Token refresh lock — only one refresh at a time
19
- let refreshPromise = null;
20
-
21
- async function attemptTokenRefresh() {
22
- if (refreshPromise) return refreshPromise;
23
- refreshPromise = (async () => {
24
- try {
25
- const response = await fetch(apiUrl(`/api/auth/refresh-token`), {
26
- method: "POST",
27
- credentials: "include",
28
- });
29
- if (!response.ok) return null;
30
- const data = await response.json();
31
- const newToken = data.access_token;
32
- if (newToken) {
33
- setAuthToken(newToken);
34
- return newToken;
35
- }
36
- return null;
37
- } catch {
38
- return null;
39
- } finally {
40
- refreshPromise = null;
41
- }
42
- })();
43
- return refreshPromise;
44
- }
45
-
46
- async function request(path, options = {}) {
47
- const { silent401, ...fetchOptions } = options;
48
- const url = apiUrl(`${path}`);
49
- const authHeaders = getAuthHeaders();
50
-
51
- // Only set Content-Type for requests with a body to avoid unnecessary CORS preflight
52
- const headers = {
53
- ...authHeaders,
54
- ...fetchOptions.headers,
55
- };
56
- if (fetchOptions.body) {
57
- headers["Content-Type"] = headers["Content-Type"] || "application/json";
58
- }
59
-
60
- const res = await fetch(url, {
61
- headers,
62
- ...fetchOptions,
63
- });
64
-
65
- const text = await res.text();
66
- let body;
67
- try {
68
- body = text ? JSON.parse(text) : {};
69
- } catch {
70
- // Reverse proxies (502/503/504) and some error pages return HTML or
71
- // plain text — don't explode on the parse, build a clean Error instead.
72
- if (!res.ok) {
73
- const snippet = (text || "").trim().slice(0, 200);
74
- const err = new Error(
75
- snippet
76
- ? `HTTP ${res.status}: ${snippet}`
77
- : `HTTP ${res.status} ${res.statusText || "error"}`,
78
- );
79
- err.status = res.status;
80
- throw err;
81
- }
82
- // Successful 2xx with a non-JSON body is an actual bug — surface it.
83
- console.error(`[API] Failed to parse JSON response from ${url}:`, text);
84
- throw new Error(
85
- `Invalid JSON response from server: ${text.slice(0, 100)}...`,
86
- );
87
- }
88
-
89
- if (!res.ok) {
90
- // Handle authentication errors
91
- if (res.status === 401 && !silent401) {
92
- // Try to refresh the token before giving up
93
- const newToken = await attemptTokenRefresh();
94
- if (newToken) {
95
- // Retry the original request with the fresh token
96
- const retryHeaders = {
97
- Authorization: `Bearer ${newToken}`,
98
- ...fetchOptions.headers,
99
- };
100
- if (fetchOptions.body) {
101
- retryHeaders["Content-Type"] = retryHeaders["Content-Type"] || "application/json";
102
- }
103
- const retryRes = await fetch(url, {
104
- headers: retryHeaders,
105
- ...fetchOptions,
106
- });
107
- if (retryRes.ok) {
108
- const retryText = await retryRes.text();
109
- try {
110
- return retryText ? JSON.parse(retryText) : {};
111
- } catch {
112
- return {};
113
- }
114
- }
115
- }
116
- // Refresh failed or retry still 401 — clear auth
117
- clearAuth();
118
- notifyUnauthorized();
119
- }
120
-
121
- const detail = body.detail;
122
- const errorMessage =
123
- typeof detail === "string"
124
- ? detail
125
- : detail
126
- ? JSON.stringify(detail)
127
- : body.message || `API error ${res.status}`;
128
-
129
- // Only log unexpected errors (not client-side conflicts or validation)
130
- if (res.status >= 500) {
131
- console.error(`[API] Server error ${url} (${res.status}):`, body);
132
- }
133
-
134
- const err = new Error(errorMessage);
135
- err.status = res.status;
136
- throw err;
137
- }
138
-
139
- return body;
140
- }
141
-
142
- // --- Agents ---
143
- export const listAgents = () => request("/api/agents");
144
-
145
- // th2prospect — profil emetteur (etape 0), owner-scope cote backend
146
- export const getProspectionProfile = () => request("/api/prospection/profile");
147
- export const setProspectionProfile = (data) =>
148
- request("/api/prospection/profile", { method: "POST", body: JSON.stringify(data) });
149
-
150
- export const listAgentsForBi = async () => {
151
- const agents = await listAgents();
152
- return agents.filter((a) => a.agent_type !== "sub_agent");
153
- };
154
-
155
- export const getAgent = (id) => request(`/api/agents/${id}`);
156
-
157
- export const createAgent = (data) =>
158
- request("/api/agents", { method: "POST", body: JSON.stringify(data) });
159
-
160
- export const updateAgent = (id, data) =>
161
- request(`/api/agents/${id}`, { method: "PUT", body: JSON.stringify(data) });
162
-
163
- export const deleteAgent = (id) =>
164
- request(`/api/agents/${id}`, { method: "DELETE" });
165
-
166
- // Hot-reload an agent's ADK runtime so open chat sessions pick up the latest
167
- // DB config (instruction, tools, model, ...) without requiring a new chat.
168
- export const reloadAgent = (id) =>
169
- request(`/api/agents/${id}/reload`, { method: "POST" });
170
-
171
- // Compare the agent's stored template snapshot against the live template.
172
- // Returns { agent_id, template_id, is_in_sync, stored_hash, current_hash, drift_fields }.
173
- // Used by TemplateDriftBanner to surface "template updated, click to sync".
174
- export const getAgentTemplateStatus = (id) =>
175
- request(`/api/agents/${id}/template-status`);
176
-
177
- // Overwrite the agent's instruction / tools / tags with the live template
178
- // (user-owned knobs like model, model_params, mcp_servers, guardrails are
179
- // left untouched). Returns the post-resync template-status payload.
180
- export const resyncAgentTemplate = (id) =>
181
- request(`/api/agents/${id}/resync-template`, { method: "POST" });
182
-
183
- // --- Tools ---
184
- export const listTools = () => request("/api/tools");
185
-
186
- export const listToolConfigs = () => request("/api/tools_config");
187
-
188
- export const getToolConfig = (id) => request(`/api/tools_config/${id}`);
189
-
190
- export const createToolConfig = (data) =>
191
- request("/api/tools_config", { method: "POST", body: JSON.stringify(data) });
192
-
193
- export const deleteToolConfig = (id) =>
194
- request(`/api/tools_config/${id}`, { method: "DELETE" });
195
-
196
- export const updateToolConfig = (id, data) =>
197
- request(`/api/tools_config/${id}`, { method: "PUT", body: JSON.stringify(data) });
198
-
199
- export const getToolExpectedParams = (toolName) =>
200
- request(`/api/tools/${encodeURIComponent(toolName)}/params`);
201
-
202
- export const getToolsDocs = () => request("/api/tools/docs");
203
-
204
- export const getModels = () => request("/api/models");
205
-
206
- // --- MCP Configs ---
207
- export const listMcpConfigs = () => request("/api/mcp_configs");
208
-
209
- export const saveMcpConfig = (data) =>
210
- request("/api/mcp_configs", { method: "POST", body: JSON.stringify(data) });
211
-
212
- export const updateMcpConfig = (mcpConfigId, data) =>
213
- request(`/api/mcp_configs/${mcpConfigId}`, {
214
- method: "PUT",
215
- body: JSON.stringify(data),
216
- });
217
-
218
- // --- Sessions ---
219
- export const createSession = (data) =>
220
- request("/api/adk/sessions", { method: "POST", body: JSON.stringify(data) });
221
-
222
- // Titre auto-généré d'une conversation à partir de son premier message.
223
- export const generateTitle = (message, agentId) =>
224
- request("/api/adk/generate_title", {
225
- method: "POST",
226
- body: JSON.stringify({ message, agent_id: agentId != null ? String(agentId) : null }),
227
- });
228
-
229
- export const updateSession = (agentName, userId, sessionId, data) =>
230
- request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}`, {
231
- method: "PATCH",
232
- body: JSON.stringify(data),
233
- });
234
-
235
- export const deleteSession = (agentName, userId, sessionId) =>
236
- request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}`, {
237
- method: "DELETE",
238
- });
239
-
240
- // --- Runs ---
241
- export const runAgent = (data) =>
242
- request("/api/adk/run", { method: "POST", body: JSON.stringify(data) });
243
-
244
- export const runAgentNow = (data) =>
245
- request("/api/adk/run_now", { method: "POST", body: JSON.stringify(data) });
246
-
247
- // --- Session History ---
248
- export const getSessionHistory = (agentName, userId, sessionId) =>
249
- request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}`);
250
-
251
- // --- Artifacts (silent401: don't clear auth on 401 — artifacts may fail independently) ---
252
- export const listArtifacts = (agentName, userId, sessionId) =>
253
- request(`/api/artifacts/${agentName}/${userId}/${sessionId}`, {
254
- silent401: true,
255
- });
256
-
257
- export const loadArtifact = (agentName, userId, sessionId, filename) =>
258
- request(`/api/artifacts/${agentName}/${userId}/${sessionId}/${filename}`, {
259
- silent401: true,
260
- });
261
-
262
- export const executeArtifact = (
263
- agentName,
264
- userId,
265
- sessionId,
266
- filename,
267
- options,
268
- ) =>
269
- request(
270
- `/api/artifacts/${agentName}/${userId}/${sessionId}/${filename}/execute`,
271
- {
272
- method: "POST",
273
- body: JSON.stringify(options || {}),
274
- silent401: true,
275
- },
276
- );
277
-
278
- // --- Scheduler ---
279
- export const listPipelines = () => request("/api/pipelines");
280
-
281
- export const listPipelineSchedules = (pipelineUuid) =>
282
- request(`/api/pipelines/${pipelineUuid}/schedules`);
283
-
284
- export const listScheduleRuns = (pipelineUuid, scheduleId) =>
285
- request(`/api/pipelines/${pipelineUuid}/schedules/${scheduleId}/runs`);
286
-
287
- export const scheduleAgentRun = (data) =>
288
- request("/api/adk/schedule_run", { method: "POST", body: JSON.stringify(data) });
289
-
290
- export const updatePipelineSchedule = (pipelineUuid, scheduleId, data) =>
291
- request(`/api/pipelines/${pipelineUuid}/schedules/${scheduleId}`, {
292
- method: "PUT",
293
- body: JSON.stringify(data),
294
- });
295
-
296
- export const createAgentTrigger = (data) =>
297
- request("/api/pipelines/agents/triggers", { method: "POST", body: JSON.stringify(data) });
298
-
299
- export const cancelPipelineRun = (runId) =>
300
- request(`/api/pipelines/runs/${runId}/cancel`, { method: "PUT" });
301
-
302
- export const getPipelineRun = (runId) =>
303
- request(`/api/pipelines/runs/${runId}`);
304
-
305
- export const getPipelineRunLogs = (runId) =>
306
- request(`/api/pipelines/runs/${runId}/logs`);
307
-
308
- // --- Files ---
309
- export const uploadFile = async (file, agentId) => {
310
- const formData = new FormData();
311
- formData.append("file", file);
312
- formData.append("agent_id", agentId);
313
- const token = getAuthToken();
314
- const res = await fetch(apiUrl(`/api/files/upload`), {
315
- method: "POST",
316
- headers: token ? { Authorization: `Bearer ${token}` } : {},
317
- body: formData,
318
- });
319
- if (!res.ok) {
320
- const body = await res.text();
321
- throw new Error(`Upload failed (${res.status}): ${body}`);
322
- }
323
- return res.json();
324
- };
325
-
326
- export const uploadFileChunked = async (file, agentId, onProgress) => {
327
- const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
328
- const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
329
- const uploadId = `upload_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
330
- const filename = file.name;
331
-
332
- // Si le fichier est petit (<= CHUNK_SIZE), utiliser l'upload classique
333
- if (totalChunks <= 1) {
334
- onProgress?.(50);
335
- const result = await uploadFile(file, agentId);
336
- onProgress?.(100);
337
- return result;
338
- }
339
-
340
- // Upload chunks sequentially
341
- for (let i = 0; i < totalChunks; i++) {
342
- const start = i * CHUNK_SIZE;
343
- const end = Math.min(start + CHUNK_SIZE, file.size);
344
- const chunk = file.slice(start, end);
345
-
346
- const formData = new FormData();
347
- formData.append("upload_id", uploadId);
348
- formData.append("agent_id", agentId);
349
- formData.append("chunk_index", String(i));
350
- formData.append("total_chunks", String(totalChunks));
351
- formData.append("filename", filename);
352
- formData.append("chunk", chunk, `${filename}.part${i}`);
353
-
354
- const token = getAuthToken();
355
- const res = await fetch(apiUrl(`/api/files/upload-chunk`), {
356
- method: "POST",
357
- headers: token ? { Authorization: `Bearer ${token}` } : {},
358
- body: formData,
359
- });
360
- if (!res.ok) {
361
- const body = await res.text();
362
- throw new Error(`Chunk upload failed (${res.status}): ${body}`);
363
- }
364
-
365
- onProgress?.(Math.round(((i + 1) / totalChunks) * 90)); // 0-90% pour les chunks
366
- }
367
-
368
- // Assembler
369
- const token = getAuthToken();
370
- const res = await fetch(apiUrl(`/api/files/upload-complete`), {
371
- method: "POST",
372
- headers: {
373
- "Content-Type": "application/json",
374
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
375
- },
376
- body: JSON.stringify({ upload_id: uploadId, agent_id: agentId, filename, total_chunks: totalChunks }),
377
- });
378
- if (!res.ok) {
379
- const body = await res.text();
380
- throw new Error(`Upload complete failed (${res.status}): ${body}`);
381
- }
382
-
383
- onProgress?.(100);
384
- return res.json();
385
- };
386
-
387
- // --- Supervision ---
388
- export const listAllSessions = () =>
389
- request("/api/adk/sessions/list");
390
-
391
- export const getSessionTrace = (agentName, userId, sessionId) =>
392
- request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}/trace`);
393
-
394
- // --- SuperAgents ---
395
- export const listSuperAgents = () => request("/api/superagents");
396
-
397
- export const getSuperAgent = (templateId) =>
398
- request(`/api/superagents/${templateId}`);
399
-
400
- // --- Agent Hub ---
401
- export const listHubAgents = () => request("/api/hub");
402
-
403
- export const getHubAgent = (hubId) => request(`/api/hub/${hubId}`);
404
-
405
- export const publishToHub = (data) =>
406
- request("/api/hub/publish", { method: "POST", body: JSON.stringify(data) });
407
-
408
- export const cloneFromHub = (data) =>
409
- request("/api/hub/clone", { method: "POST", body: JSON.stringify(data) });
410
-
411
- export const deleteFromHub = (hubId) =>
412
- request(`/api/hub/${hubId}`, { method: "DELETE" });
413
-
414
- // --- Billing ---
415
- export const getBillingPackages = () => request("/api/billing/packages");
416
-
417
- export const createBillingCheckout = (packageId, successUrl, cancelUrl) =>
418
- request("/api/billing/checkout", {
419
- method: "POST",
420
- body: JSON.stringify({
421
- package_id: packageId,
422
- success_url: successUrl,
423
- cancel_url: cancelUrl,
424
- }),
425
- });
426
-
427
- export const getBillingBalance = () => request("/api/billing/balance");
428
-
429
- export const getBillingTransactions = (limit = 20) =>
430
- request(`/api/billing/transactions?limit=${limit}`);
431
-
432
- export const getBillingPortal = (returnUrl) =>
433
- request(`/api/billing/portal?return_url=${encodeURIComponent(returnUrl)}`);
434
-
435
- // --- Usage ---
436
- export const getUsageSummary = (days = 30, { granularity, agentId } = {}) => {
437
- const params = new URLSearchParams({ days: String(days) });
438
- if (granularity) params.set("granularity", granularity);
439
- if (agentId != null) params.set("agent_id", String(agentId));
440
- return request(`/api/usage/summary?${params.toString()}`);
441
- };
442
-
443
- // Quota mensuel sur le modèle thaink2 mutualisé. `enabled: false` quand ce
444
- // serveur ne sert pas de modèle par défaut — il n'y a alors rien à afficher.
445
- export const getUsageQuota = () => request(`/api/usage/quota`);
446
-
447
- // ---- Logging (agent observability, per conversation) ----
448
-
449
- export const getLoggingConversations = (limit = 50) =>
450
- request(`/api/logging/conversations?limit=${limit}`);
451
-
452
- export const getLoggingLogs = ({ conversationId, service, level, limit = 500 } = {}) => {
453
- const params = new URLSearchParams({ limit: String(limit) });
454
- if (conversationId) params.set("conversation_id", conversationId);
455
- if (service) params.set("service", service);
456
- if (level) params.set("level", level);
457
- return request(`/api/logging/logs?${params.toString()}`);
458
- };
459
-
460
- export const getLoggingSpans = ({ conversationId, limit = 1000 } = {}) => {
461
- const params = new URLSearchParams({ limit: String(limit) });
462
- if (conversationId) params.set("conversation_id", conversationId);
463
- return request(`/api/logging/spans?${params.toString()}`);
464
- };
465
-
466
- export const getLoggingStats = (hours = 24) =>
467
- request(`/api/logging/stats?hours=${hours}`);
468
-
469
- export const getLoggingAlerts = () =>
470
- request("/api/logging/alerts?active=true");
471
-
472
- export const getLoggingAnnotations = (conversationId) =>
473
- request(`/api/logging/annotations?conversation_id=${encodeURIComponent(conversationId)}`);
474
-
475
- export const postLoggingAnnotation = ({ conversationId, traceId, note }) =>
476
- request("/api/logging/annotations", {
477
- method: "POST",
478
- body: JSON.stringify({
479
- conversation_id: conversationId,
480
- trace_id: traceId,
481
- note,
482
- }),
483
- });
484
-
485
- export const getUsageAgentDetail = (agentId, days = 30) =>
486
- request(`/api/usage/agents/${agentId}?days=${days}`);
487
-
488
- export const getPublicConfig = async () => {
489
- const res = await fetch(apiUrl(`/api/config`));
490
- if (!res.ok) return { billing_enabled: true };
491
- return res.json();
492
- };
493
-
494
- // --- RAG Knowledge Base ---
495
- export const indexRagFiles = async (files, agentId, sessionId) => {
496
- const formData = new FormData();
497
- formData.append("agent_id", agentId);
498
- if (sessionId) formData.append("session_id", sessionId);
499
- files.forEach((f) => formData.append("files", f));
500
- const token = getAuthToken();
501
- const res = await fetch(apiUrl(`/api/rag/index-files`), {
502
- method: "POST",
503
- headers: token ? { Authorization: `Bearer ${token}` } : {},
504
- body: formData,
505
- });
506
- if (!res.ok) {
507
- const body = await res.text();
508
- throw new Error(`RAG file indexing failed (${res.status}): ${body}`);
509
- }
510
- return res.json();
511
- };
512
-
513
- export const indexRagUrl = (data) =>
514
- request("/api/rag/index-url", { method: "POST", body: JSON.stringify(data) });
515
-
516
- export const indexRagDb = (data) =>
517
- request("/api/rag/index-db", { method: "POST", body: JSON.stringify(data) });
518
-
519
- export const indexRagDbNl = (data) =>
520
- request("/api/rag/index-db-nl", { method: "POST", body: JSON.stringify(data) });
521
-
522
- export const indexRagS3 = (data) =>
523
- request("/api/rag/index-s3", { method: "POST", body: JSON.stringify(data) });
524
-
525
- export const getRagStatus = (knowledgeId, agentId) =>
526
- request(`/api/rag/status/${knowledgeId}?agent_id=${encodeURIComponent(agentId)}`);
527
-
528
- export const listRagKnowledge = (agentId, sessionId) =>
529
- request(`/api/rag/knowledge/${agentId}${sessionId ? `?session_id=${encodeURIComponent(sessionId)}` : ""}`);
530
-
531
- // --- Saved API Keys ---
532
- export const listSavedApiKeys = () => request("/api/saved-api-keys");
533
-
534
- export const createSavedApiKey = (data) =>
535
- request("/api/saved-api-keys", { method: "POST", body: JSON.stringify(data) });
536
-
537
- export const deleteSavedApiKey = (id) =>
538
- request(`/api/saved-api-keys/${id}`, { method: "DELETE" });
539
-
540
- // --- Webhook Subscriptions ---
541
- export const listWebhookSubscriptions = () =>
542
- request("/api/webhooks/subscriptions");
543
-
544
- export const createWebhookSubscription = (data) =>
545
- request("/api/webhooks/subscriptions", { method: "POST", body: JSON.stringify(data) });
546
-
547
- export const updateWebhookSubscription = (id, data) =>
548
- request(`/api/webhooks/subscriptions/${id}`, { method: "PATCH", body: JSON.stringify(data) });
549
-
550
- export const deleteWebhookSubscription = (id) =>
551
- request(`/api/webhooks/subscriptions/${id}`, { method: "DELETE" });
552
-
553
- export const renewWebhookSubscription = (id) =>
554
- request(`/api/webhooks/subscriptions/${id}/renew`, { method: "POST" });
555
-
556
- export const listWebhookLogs = (params = {}) => {
557
- const query = new URLSearchParams(params).toString();
558
- return request(`/api/webhooks/logs${query ? `?${query}` : ""}`);
559
- };
560
-
561
- export const getWebhookLog = (id) =>
562
- request(`/api/webhooks/logs/${id}`);
563
-
564
- export const retriggerWebhookLog = (logId) =>
565
- request(`/api/webhooks/logs/${logId}/retrigger`, { method: "POST" });
566
-
567
- export const getWebhookLogBody = async (id) => {
568
- const token = getAuthToken();
569
- const res = await fetch(`/api/webhooks/logs/${id}/body`, {
570
- headers: token ? { Authorization: `Bearer ${token}` } : {},
571
- });
572
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
573
- return res.text();
574
- };
575
-
576
- // Fetch one webhook attachment WITH auth and return a short-lived object URL.
577
- //
578
- // The serve endpoint is Bearer-authenticated like every other API route.
579
- // A plain ``window.open(url)`` is a top-level navigation that carries no
580
- // Authorization header, so the backend answers 401 ("Not authenticated").
581
- // We therefore fetch the bytes with the token, wrap them in a Blob, and
582
- // hand back an object URL the caller can open in a new tab. The 401 ->
583
- // refresh -> retry dance mirrors ``request`` so an expired access token
584
- // doesn't break the preview.
585
- export const fetchWebhookLogAttachmentObjectUrl = async (logId, filename) => {
586
- const path = apiUrl(`/api/webhooks/logs/${logId}/attachments/${encodeURIComponent(filename)}`);
587
- let res = await fetch(path, { headers: getAuthHeaders() });
588
- if (res.status === 401) {
589
- const newToken = await attemptTokenRefresh();
590
- if (newToken) {
591
- res = await fetch(path, { headers: { Authorization: `Bearer ${newToken}` } });
592
- } else {
593
- // Refresh failed — clear auth and stop here instead of falling
594
- // through on the stale 401 ``res`` (mirrors ``request``).
595
- clearAuth();
596
- notifyUnauthorized();
597
- const err = new Error("HTTP 401");
598
- err.status = 401;
599
- throw err;
600
- }
601
- }
602
- if (!res.ok) {
603
- const err = new Error(`HTTP ${res.status}`);
604
- err.status = res.status;
605
- throw err;
606
- }
607
- const blob = await res.blob();
608
- return URL.createObjectURL(blob);
609
- };
610
-
611
- // --- Notifications ---
612
- export const listNotifications = (params = {}) => {
613
- const query = new URLSearchParams(params).toString();
614
- return request(`/api/notifications${query ? `?${query}` : ""}`);
615
- };
616
-
617
- export const getUnreadNotificationCount = () =>
618
- request("/api/notifications/unread-count");
619
-
620
- export const markNotificationRead = (id) =>
621
- request(`/api/notifications/${id}/read`, { method: "PATCH" });
622
-
623
- export const markAllNotificationsRead = () =>
624
- request("/api/notifications/read-all", { method: "POST" });
625
-
626
- // --- Emailing / Outlook ---
627
- export const getOutlookAuthUrl = () =>
628
- request("/api/emailing/microsoft/auth-url");
629
-
630
- export const getOutlookStatus = () =>
631
- request("/api/emailing/microsoft/status");
632
-
633
- // --- BI & Reporting ---
634
- export const listDashboards = (params = {}) => {
635
- const query = new URLSearchParams(params).toString();
636
- return request(`/api/v1/dashboards${query ? `?${query}` : ""}`);
637
- };
638
- export const getDashboard = (id) => request(`/api/v1/dashboards/${id}`);
639
- export const getDashboardBySlug = (slug) =>
640
- request(`/api/v1/dashboards/by-slug/${encodeURIComponent(slug)}`);
641
- // Add a chart to the user's chat dashboard (user-triggered "Send to dashboard").
642
- export const sendChartToDashboard = (chartId, sessionId) => {
643
- const qs = sessionId ? `?session_id=${encodeURIComponent(sessionId)}` : "";
644
- return request(`/api/v1/charts/${encodeURIComponent(chartId)}/send-to-dashboard${qs}`, {
645
- method: "POST",
646
- });
647
- };
648
- export const listSharedDashboards = (params = {}) => {
649
- const query = new URLSearchParams(params).toString();
650
- return request(`/api/v1/dashboards/shared${query ? `?${query}` : ""}`);
651
- };
652
- export const createDashboard = (data) =>
653
- request("/api/v1/dashboards", { method: "POST", body: JSON.stringify(data) });
654
- export const updateDashboard = (id, data) =>
655
- request(`/api/v1/dashboards/${id}`, { method: "PATCH", body: JSON.stringify(data) });
656
- export const deleteDashboard = (id) =>
657
- request(`/api/v1/dashboards/${id}`, { method: "DELETE" });
658
- export const publishDashboard = (id, data = {}) =>
659
- request(`/api/v1/dashboards/${id}/publish`, { method: "POST", body: JSON.stringify(data) });
660
-
661
- export const unpublishDashboard = (id) =>
662
- request(`/api/v1/dashboards/${id}/unpublish`, { method: "POST" });
663
-
664
- export const getPublicDashboard = (slug) =>
665
- request(`/api/v1/dashboards/public/${slug}`, { silent401: true });
666
-
667
- export const getPublicChartData = (chartId, params = {}) => {
668
- const query = new URLSearchParams(params).toString();
669
- return request(`/api/v1/public/charts/${chartId}/data${query ? `?${query}` : ""}`, { silent401: true });
670
- };
671
-
672
- export const listCharts = (params = {}) => {
673
- const query = new URLSearchParams(params).toString();
674
- return request(`/api/v1/charts${query ? `?${query}` : ""}`);
675
- };
676
- export const getChart = (id) => request(`/api/v1/charts/${id}`);
677
- export const createChart = (data) =>
678
- request("/api/v1/charts", { method: "POST", body: JSON.stringify(data) });
679
- export const updateChart = (id, data) =>
680
- request(`/api/v1/charts/${id}`, { method: "PATCH", body: JSON.stringify(data) });
681
- export const deleteChart = (id) =>
682
- request(`/api/v1/charts/${id}`, { method: "DELETE" });
683
- export const getChartData = (chartId, params = {}) => {
684
- const query = new URLSearchParams(params).toString();
685
- return request(`/api/v1/charts/${chartId}/data${query ? `?${query}` : ""}`);
686
- };
687
-
688
- export async function scheduleChartRefresh(chartId, { agentId, interval, startTime, messageTemplate }) {
689
- return request(`/api/v1/charts/${chartId}/schedule-refresh`, {
690
- method: "POST",
691
- body: JSON.stringify({
692
- agent_id: agentId,
693
- interval,
694
- start_time: startTime || undefined,
695
- message_template: messageTemplate || undefined,
696
- }),
697
- });
698
- }
699
-
700
- export const addDashboardComponent = (dashboardId, data) =>
701
- request(`/api/v1/dashboards/${dashboardId}/components`, { method: "POST", body: JSON.stringify(data) });
702
- export const removeDashboardComponent = (dashboardId, componentId) =>
703
- request(`/api/v1/dashboards/${dashboardId}/components/${componentId}`, { method: "DELETE" });
704
- export const moveDashboardComponent = (dashboardId, componentId, data) =>
705
- request(`/api/v1/dashboards/${dashboardId}/components/${componentId}/position`, { method: "PATCH", body: JSON.stringify(data) });
706
- export const updateDashboardComponent = (dashboardId, componentId, data) =>
707
- request(`/api/v1/dashboards/${dashboardId}/components/${componentId}`, { method: "PATCH", body: JSON.stringify(data) });
708
-
709
- // Link/unlink agent to dashboard
710
- export const linkAgentToDashboard = (dashboardId, agentId) =>
711
- request(`/api/v1/dashboards/${dashboardId}/agent`, {
712
- method: "PATCH",
713
- body: JSON.stringify({ agent_id: agentId }),
714
- });
715
-
716
- // Get agent linked to dashboard
717
- export const getDashboardAgent = (dashboardId) =>
718
- request(`/api/v1/dashboards/${dashboardId}/agent`);
719
-
720
- export const getBiStats = (organizationId) =>
721
- request(`/api/v1/bi/stats?organization_id=${encodeURIComponent(organizationId || "default")}`);
722
-
723
- export const listBiDatasets = (organizationId) =>
724
- request(`/api/v1/bi/datasets?organization_id=${encodeURIComponent(organizationId || "default")}`);
725
-
726
- export const previewBiDataset = (fileId, organizationId) =>
727
- request(`/api/v1/bi/datasets/${encodeURIComponent(fileId)}/preview?organization_id=${encodeURIComponent(organizationId || "default")}`);
728
-
729
- export const previewOnedriveSpreadsheet = ({ itemPath, itemId = null, sheetName = null }) =>
730
- request("/api/v1/bi/onedrive/preview", {
731
- method: "POST",
732
- body: JSON.stringify({
733
- item_path: itemPath,
734
- item_id: itemId,
735
- sheet_name: sheetName,
736
- }),
737
- });
738
-
739
- export const listBiDbConfigs = (organizationId) =>
740
- request(`/api/v1/bi/tool-configs/database?organization_id=${encodeURIComponent(organizationId || "default")}`);
741
-
742
- export const deleteBiDataset = (fileId, organizationId) =>
743
- request(`/api/v1/bi/datasets/${encodeURIComponent(fileId)}?organization_id=${encodeURIComponent(organizationId || "default")}`, { method: "DELETE" });
744
-
745
- // --- Skills ---
746
- export const listSkills = () => request("/api/skills");
747
- export const listPortfolioSkills = () => request("/api/skills/portfolio");
748
- export const getSkill = (id) => request(`/api/skills/${id}`);
749
- export const createSkill = (data) =>
750
- request("/api/skills", { method: "POST", body: JSON.stringify(data) });
751
- export const updateSkill = (id, data) =>
752
- request(`/api/skills/${id}`, { method: "PUT", body: JSON.stringify(data) });
753
- export const deleteSkill = (id) =>
754
- request(`/api/skills/${id}`, { method: "DELETE" });
755
-
756
- export const exportSkill = (id, format = "json") =>
757
- apiUrl(`/api/skills/${id}/export?format=${encodeURIComponent(format)}`);
758
-
759
- export const exportPortfolioSkill = (name, format = "json") =>
760
- apiUrl(`/api/skills/portfolio/${encodeURIComponent(name)}/export?format=${encodeURIComponent(format)}`);
761
-
762
- export const importSkill = async (file) => {
763
- const formData = new FormData();
764
- formData.append("file", file);
765
- const token = getAuthToken();
766
- const res = await fetch(apiUrl(`/api/skills/import`), {
767
- method: "POST",
768
- headers: token ? { Authorization: `Bearer ${token}` } : {},
769
- body: formData,
770
- });
771
- if (!res.ok) {
772
- const body = await res.text();
773
- let message;
774
- try {
775
- const parsed = JSON.parse(body);
776
- message = parsed.detail || `Import failed (${res.status})`;
777
- } catch {
778
- message = `Import failed (${res.status}): ${body}`;
779
- }
780
- throw new Error(message);
781
- }
782
- return res.json();
783
- };
784
-
785
- // --- Email Campaigns ---
786
- export const launchCampaign = (payload) =>
787
- request("/api/campaigns/launch-from-dashboard", {
788
- method: "POST",
789
- body: JSON.stringify(payload),
790
- });
791
-
792
- export const getCampaignStatus = (campaignId) =>
793
- request(`/api/campaigns/${encodeURIComponent(campaignId)}/status`, {
794
- method: "GET",
795
- });
796
-
797
- export async function getOnedriveExcelPreview(
798
- itemPath,
799
- { sheetName = null, limit = 5 } = {},
800
- ) {
801
- const params = new URLSearchParams({
802
- item_path: itemPath,
803
- limit: String(limit),
804
- });
805
- if (sheetName) params.append("sheet_name", String(sheetName));
806
- return await request(
807
- `/api/onedrivebrowser/excel-preview?${params.toString()}`,
808
- { method: "GET" },
809
- );
810
- }
811
-
812
- export const uploadBiCsv = async (file, separator = "auto", organizationId) => {
813
- const formData = new FormData();
814
- formData.append("file", file);
815
- formData.append("separator", separator);
816
- formData.append("organization_id", organizationId || "default");
817
- const token = getAuthToken();
818
- // Upload directly to backend to bypass Next.js proxy body size limit
819
- const backendUrl = process.env.NEXT_PUBLIC_API_URL || "";
820
- const url = backendUrl
821
- ? `${backendUrl}/api/v1/bi/upload-csv`
822
- : apiUrl(`/api/v1/bi/upload-csv`);
823
- const res = await fetch(url, {
824
- method: "POST",
825
- headers: token ? { Authorization: `Bearer ${token}` } : {},
826
- body: formData,
827
- });
828
- if (!res.ok) throw new Error(`CSV upload failed (${res.status})`);
829
- return res.json();
830
- };
1
+ import {
2
+ apiUrl,
3
+ clearAuth,
4
+ getAuthToken,
5
+ notifyUnauthorized,
6
+ setAuthToken,
7
+ } from "./config.js";
8
+
9
+
10
+ function getAuthHeaders() {
11
+ const token = getAuthToken();
12
+ if (token) {
13
+ return { Authorization: `Bearer ${token}` };
14
+ }
15
+ return {};
16
+ }
17
+
18
+ // Token refresh lock — only one refresh at a time
19
+ let refreshPromise = null;
20
+
21
+ async function attemptTokenRefresh() {
22
+ if (refreshPromise) return refreshPromise;
23
+ refreshPromise = (async () => {
24
+ try {
25
+ const response = await fetch(apiUrl(`/api/auth/refresh-token`), {
26
+ method: "POST",
27
+ credentials: "include",
28
+ });
29
+ if (!response.ok) return null;
30
+ const data = await response.json();
31
+ const newToken = data.access_token;
32
+ if (newToken) {
33
+ setAuthToken(newToken);
34
+ return newToken;
35
+ }
36
+ return null;
37
+ } catch {
38
+ return null;
39
+ } finally {
40
+ refreshPromise = null;
41
+ }
42
+ })();
43
+ return refreshPromise;
44
+ }
45
+
46
+ async function request(path, options = {}) {
47
+ const { silent401, ...fetchOptions } = options;
48
+ const url = apiUrl(`${path}`);
49
+ const authHeaders = getAuthHeaders();
50
+
51
+ // Only set Content-Type for requests with a body to avoid unnecessary CORS preflight
52
+ const headers = {
53
+ ...authHeaders,
54
+ ...fetchOptions.headers,
55
+ };
56
+ if (fetchOptions.body) {
57
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
58
+ }
59
+
60
+ const res = await fetch(url, {
61
+ headers,
62
+ ...fetchOptions,
63
+ });
64
+
65
+ const text = await res.text();
66
+ let body;
67
+ try {
68
+ body = text ? JSON.parse(text) : {};
69
+ } catch {
70
+ // Reverse proxies (502/503/504) and some error pages return HTML or
71
+ // plain text — don't explode on the parse, build a clean Error instead.
72
+ if (!res.ok) {
73
+ const snippet = (text || "").trim().slice(0, 200);
74
+ const err = new Error(
75
+ snippet
76
+ ? `HTTP ${res.status}: ${snippet}`
77
+ : `HTTP ${res.status} ${res.statusText || "error"}`,
78
+ );
79
+ err.status = res.status;
80
+ throw err;
81
+ }
82
+ // Successful 2xx with a non-JSON body is an actual bug — surface it.
83
+ console.error(`[API] Failed to parse JSON response from ${url}:`, text);
84
+ throw new Error(
85
+ `Invalid JSON response from server: ${text.slice(0, 100)}...`,
86
+ );
87
+ }
88
+
89
+ if (!res.ok) {
90
+ // Handle authentication errors
91
+ if (res.status === 401 && !silent401) {
92
+ // Try to refresh the token before giving up
93
+ const newToken = await attemptTokenRefresh();
94
+ if (newToken) {
95
+ // Retry the original request with the fresh token
96
+ const retryHeaders = {
97
+ Authorization: `Bearer ${newToken}`,
98
+ ...fetchOptions.headers,
99
+ };
100
+ if (fetchOptions.body) {
101
+ retryHeaders["Content-Type"] = retryHeaders["Content-Type"] || "application/json";
102
+ }
103
+ const retryRes = await fetch(url, {
104
+ headers: retryHeaders,
105
+ ...fetchOptions,
106
+ });
107
+ if (retryRes.ok) {
108
+ const retryText = await retryRes.text();
109
+ try {
110
+ return retryText ? JSON.parse(retryText) : {};
111
+ } catch {
112
+ return {};
113
+ }
114
+ }
115
+ }
116
+ // Refresh failed or retry still 401 — clear auth
117
+ clearAuth();
118
+ notifyUnauthorized();
119
+ }
120
+
121
+ const detail = body.detail;
122
+ const errorMessage =
123
+ typeof detail === "string"
124
+ ? detail
125
+ : detail
126
+ ? JSON.stringify(detail)
127
+ : body.message || `API error ${res.status}`;
128
+
129
+ // Only log unexpected errors (not client-side conflicts or validation)
130
+ if (res.status >= 500) {
131
+ console.error(`[API] Server error ${url} (${res.status}):`, body);
132
+ }
133
+
134
+ const err = new Error(errorMessage);
135
+ err.status = res.status;
136
+ throw err;
137
+ }
138
+
139
+ return body;
140
+ }
141
+
142
+ // --- Agents ---
143
+ export const listAgents = () => request("/api/agents");
144
+
145
+ // th2prospect — profil emetteur (etape 0), owner-scope cote backend
146
+ export const getProspectionProfile = () => request("/api/prospection/profile");
147
+ export const setProspectionProfile = (data) =>
148
+ request("/api/prospection/profile", { method: "POST", body: JSON.stringify(data) });
149
+
150
+ export const listAgentsForBi = async () => {
151
+ const agents = await listAgents();
152
+ return agents.filter((a) => a.agent_type !== "sub_agent");
153
+ };
154
+
155
+ export const getAgent = (id) => request(`/api/agents/${id}`);
156
+
157
+ export const createAgent = (data) =>
158
+ request("/api/agents", { method: "POST", body: JSON.stringify(data) });
159
+
160
+ export const updateAgent = (id, data) =>
161
+ request(`/api/agents/${id}`, { method: "PUT", body: JSON.stringify(data) });
162
+
163
+ export const deleteAgent = (id) =>
164
+ request(`/api/agents/${id}`, { method: "DELETE" });
165
+
166
+ // Hot-reload an agent's ADK runtime so open chat sessions pick up the latest
167
+ // DB config (instruction, tools, model, ...) without requiring a new chat.
168
+ export const reloadAgent = (id) =>
169
+ request(`/api/agents/${id}/reload`, { method: "POST" });
170
+
171
+ // Compare the agent's stored template snapshot against the live template.
172
+ // Returns { agent_id, template_id, is_in_sync, stored_hash, current_hash, drift_fields }.
173
+ // Used by TemplateDriftBanner to surface "template updated, click to sync".
174
+ export const getAgentTemplateStatus = (id) =>
175
+ request(`/api/agents/${id}/template-status`);
176
+
177
+ // Overwrite the agent's instruction / tools / tags with the live template
178
+ // (user-owned knobs like model, model_params, mcp_servers, guardrails are
179
+ // left untouched). Returns the post-resync template-status payload.
180
+ export const resyncAgentTemplate = (id) =>
181
+ request(`/api/agents/${id}/resync-template`, { method: "POST" });
182
+
183
+ // --- Tools ---
184
+ export const listTools = () => request("/api/tools");
185
+
186
+ export const listToolConfigs = () => request("/api/tools_config");
187
+
188
+ export const getToolConfig = (id) => request(`/api/tools_config/${id}`);
189
+
190
+ export const createToolConfig = (data) =>
191
+ request("/api/tools_config", { method: "POST", body: JSON.stringify(data) });
192
+
193
+ export const deleteToolConfig = (id) =>
194
+ request(`/api/tools_config/${id}`, { method: "DELETE" });
195
+
196
+ export const updateToolConfig = (id, data) =>
197
+ request(`/api/tools_config/${id}`, { method: "PUT", body: JSON.stringify(data) });
198
+
199
+ export const getToolExpectedParams = (toolName) =>
200
+ request(`/api/tools/${encodeURIComponent(toolName)}/params`);
201
+
202
+ export const getToolsDocs = () => request("/api/tools/docs");
203
+
204
+ export const getModels = () => request("/api/models");
205
+
206
+ // --- MCP Configs ---
207
+ export const listMcpConfigs = () => request("/api/mcp_configs");
208
+
209
+ export const saveMcpConfig = (data) =>
210
+ request("/api/mcp_configs", { method: "POST", body: JSON.stringify(data) });
211
+
212
+ export const updateMcpConfig = (mcpConfigId, data) =>
213
+ request(`/api/mcp_configs/${mcpConfigId}`, {
214
+ method: "PUT",
215
+ body: JSON.stringify(data),
216
+ });
217
+
218
+ // --- Sessions ---
219
+ export const createSession = (data) =>
220
+ request("/api/adk/sessions", { method: "POST", body: JSON.stringify(data) });
221
+
222
+ // Titre auto-généré d'une conversation à partir de son premier message.
223
+ export const generateTitle = (message, agentId) =>
224
+ request("/api/adk/generate_title", {
225
+ method: "POST",
226
+ body: JSON.stringify({ message, agent_id: agentId != null ? String(agentId) : null }),
227
+ });
228
+
229
+ export const updateSession = (agentName, userId, sessionId, data) =>
230
+ request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}`, {
231
+ method: "PATCH",
232
+ body: JSON.stringify(data),
233
+ });
234
+
235
+ export const deleteSession = (agentName, userId, sessionId) =>
236
+ request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}`, {
237
+ method: "DELETE",
238
+ });
239
+
240
+ // --- Runs ---
241
+ export const runAgent = (data) =>
242
+ request("/api/adk/run", { method: "POST", body: JSON.stringify(data) });
243
+
244
+ export const runAgentNow = (data) =>
245
+ request("/api/adk/run_now", { method: "POST", body: JSON.stringify(data) });
246
+
247
+ // --- Session History ---
248
+ export const getSessionHistory = (agentName, userId, sessionId) =>
249
+ request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}`);
250
+
251
+ // --- Artifacts (silent401: don't clear auth on 401 — artifacts may fail independently) ---
252
+ export const listArtifacts = (agentName, userId, sessionId) =>
253
+ request(`/api/artifacts/${agentName}/${userId}/${sessionId}`, {
254
+ silent401: true,
255
+ });
256
+
257
+ export const loadArtifact = (agentName, userId, sessionId, filename) =>
258
+ request(`/api/artifacts/${agentName}/${userId}/${sessionId}/${filename}`, {
259
+ silent401: true,
260
+ });
261
+
262
+ export const executeArtifact = (
263
+ agentName,
264
+ userId,
265
+ sessionId,
266
+ filename,
267
+ options,
268
+ ) =>
269
+ request(
270
+ `/api/artifacts/${agentName}/${userId}/${sessionId}/${filename}/execute`,
271
+ {
272
+ method: "POST",
273
+ body: JSON.stringify(options || {}),
274
+ silent401: true,
275
+ },
276
+ );
277
+
278
+ // --- Scheduler ---
279
+ export const listPipelines = () => request("/api/pipelines");
280
+
281
+ export const listPipelineSchedules = (pipelineUuid) =>
282
+ request(`/api/pipelines/${pipelineUuid}/schedules`);
283
+
284
+ export const listScheduleRuns = (pipelineUuid, scheduleId) =>
285
+ request(`/api/pipelines/${pipelineUuid}/schedules/${scheduleId}/runs`);
286
+
287
+ export const scheduleAgentRun = (data) =>
288
+ request("/api/adk/schedule_run", { method: "POST", body: JSON.stringify(data) });
289
+
290
+ export const updatePipelineSchedule = (pipelineUuid, scheduleId, data) =>
291
+ request(`/api/pipelines/${pipelineUuid}/schedules/${scheduleId}`, {
292
+ method: "PUT",
293
+ body: JSON.stringify(data),
294
+ });
295
+
296
+ export const createAgentTrigger = (data) =>
297
+ request("/api/pipelines/agents/triggers", { method: "POST", body: JSON.stringify(data) });
298
+
299
+ export const cancelPipelineRun = (runId) =>
300
+ request(`/api/pipelines/runs/${runId}/cancel`, { method: "PUT" });
301
+
302
+ export const getPipelineRun = (runId) =>
303
+ request(`/api/pipelines/runs/${runId}`);
304
+
305
+ export const getPipelineRunLogs = (runId) =>
306
+ request(`/api/pipelines/runs/${runId}/logs`);
307
+
308
+ // --- Files ---
309
+ export const uploadFile = async (file, agentId) => {
310
+ const formData = new FormData();
311
+ formData.append("file", file);
312
+ formData.append("agent_id", agentId);
313
+ const token = getAuthToken();
314
+ const res = await fetch(apiUrl(`/api/files/upload`), {
315
+ method: "POST",
316
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
317
+ body: formData,
318
+ });
319
+ if (!res.ok) {
320
+ const body = await res.text();
321
+ throw new Error(`Upload failed (${res.status}): ${body}`);
322
+ }
323
+ return res.json();
324
+ };
325
+
326
+ export const uploadFileChunked = async (file, agentId, onProgress) => {
327
+ const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
328
+ const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
329
+ const uploadId = `upload_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
330
+ const filename = file.name;
331
+
332
+ // Si le fichier est petit (<= CHUNK_SIZE), utiliser l'upload classique
333
+ if (totalChunks <= 1) {
334
+ onProgress?.(50);
335
+ const result = await uploadFile(file, agentId);
336
+ onProgress?.(100);
337
+ return result;
338
+ }
339
+
340
+ // Upload chunks sequentially
341
+ for (let i = 0; i < totalChunks; i++) {
342
+ const start = i * CHUNK_SIZE;
343
+ const end = Math.min(start + CHUNK_SIZE, file.size);
344
+ const chunk = file.slice(start, end);
345
+
346
+ const formData = new FormData();
347
+ formData.append("upload_id", uploadId);
348
+ formData.append("agent_id", agentId);
349
+ formData.append("chunk_index", String(i));
350
+ formData.append("total_chunks", String(totalChunks));
351
+ formData.append("filename", filename);
352
+ formData.append("chunk", chunk, `${filename}.part${i}`);
353
+
354
+ const token = getAuthToken();
355
+ const res = await fetch(apiUrl(`/api/files/upload-chunk`), {
356
+ method: "POST",
357
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
358
+ body: formData,
359
+ });
360
+ if (!res.ok) {
361
+ const body = await res.text();
362
+ throw new Error(`Chunk upload failed (${res.status}): ${body}`);
363
+ }
364
+
365
+ onProgress?.(Math.round(((i + 1) / totalChunks) * 90)); // 0-90% pour les chunks
366
+ }
367
+
368
+ // Assembler
369
+ const token = getAuthToken();
370
+ const res = await fetch(apiUrl(`/api/files/upload-complete`), {
371
+ method: "POST",
372
+ headers: {
373
+ "Content-Type": "application/json",
374
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
375
+ },
376
+ body: JSON.stringify({ upload_id: uploadId, agent_id: agentId, filename, total_chunks: totalChunks }),
377
+ });
378
+ if (!res.ok) {
379
+ const body = await res.text();
380
+ throw new Error(`Upload complete failed (${res.status}): ${body}`);
381
+ }
382
+
383
+ onProgress?.(100);
384
+ return res.json();
385
+ };
386
+
387
+ // --- Supervision ---
388
+ export const listAllSessions = () =>
389
+ request("/api/adk/sessions/list");
390
+
391
+ export const getSessionTrace = (agentName, userId, sessionId) =>
392
+ request(`/api/adk/sessions/${agentName}/${userId}/${sessionId}/trace`);
393
+
394
+ // --- SuperAgents ---
395
+ export const listSuperAgents = () => request("/api/superagents");
396
+
397
+ export const getSuperAgent = (templateId) =>
398
+ request(`/api/superagents/${templateId}`);
399
+
400
+ // --- Agent Hub ---
401
+ export const listHubAgents = () => request("/api/hub");
402
+
403
+ export const getHubAgent = (hubId) => request(`/api/hub/${hubId}`);
404
+
405
+ export const publishToHub = (data) =>
406
+ request("/api/hub/publish", { method: "POST", body: JSON.stringify(data) });
407
+
408
+ export const cloneFromHub = (data) =>
409
+ request("/api/hub/clone", { method: "POST", body: JSON.stringify(data) });
410
+
411
+ export const deleteFromHub = (hubId) =>
412
+ request(`/api/hub/${hubId}`, { method: "DELETE" });
413
+
414
+ // --- Billing ---
415
+ export const getBillingPackages = () => request("/api/billing/packages");
416
+
417
+ export const createBillingCheckout = (packageId, successUrl, cancelUrl) =>
418
+ request("/api/billing/checkout", {
419
+ method: "POST",
420
+ body: JSON.stringify({
421
+ package_id: packageId,
422
+ success_url: successUrl,
423
+ cancel_url: cancelUrl,
424
+ }),
425
+ });
426
+
427
+ export const getBillingBalance = () => request("/api/billing/balance");
428
+
429
+ export const getBillingTransactions = (limit = 20) =>
430
+ request(`/api/billing/transactions?limit=${limit}`);
431
+
432
+ export const getBillingPortal = (returnUrl) =>
433
+ request(`/api/billing/portal?return_url=${encodeURIComponent(returnUrl)}`);
434
+
435
+ // --- Usage ---
436
+ export const getUsageSummary = (days = 30, { granularity, agentId } = {}) => {
437
+ const params = new URLSearchParams({ days: String(days) });
438
+ if (granularity) params.set("granularity", granularity);
439
+ if (agentId != null) params.set("agent_id", String(agentId));
440
+ return request(`/api/usage/summary?${params.toString()}`);
441
+ };
442
+
443
+ // Quota mensuel sur le modèle thaink2 mutualisé. `enabled: false` quand ce
444
+ // serveur ne sert pas de modèle par défaut — il n'y a alors rien à afficher.
445
+ export const getUsageQuota = () => request(`/api/usage/quota`);
446
+
447
+ // ---- Logging (agent observability, per conversation) ----
448
+
449
+ export const getLoggingConversations = (limit = 50) =>
450
+ request(`/api/logging/conversations?limit=${limit}`);
451
+
452
+ export const getLoggingLogs = ({ conversationId, service, level, limit = 500 } = {}) => {
453
+ const params = new URLSearchParams({ limit: String(limit) });
454
+ if (conversationId) params.set("conversation_id", conversationId);
455
+ if (service) params.set("service", service);
456
+ if (level) params.set("level", level);
457
+ return request(`/api/logging/logs?${params.toString()}`);
458
+ };
459
+
460
+ export const getLoggingSpans = ({ conversationId, limit = 1000 } = {}) => {
461
+ const params = new URLSearchParams({ limit: String(limit) });
462
+ if (conversationId) params.set("conversation_id", conversationId);
463
+ return request(`/api/logging/spans?${params.toString()}`);
464
+ };
465
+
466
+ export const getLoggingStats = (hours = 24) =>
467
+ request(`/api/logging/stats?hours=${hours}`);
468
+
469
+ export const getLoggingAlerts = () =>
470
+ request("/api/logging/alerts?active=true");
471
+
472
+ export const getLoggingAnnotations = (conversationId) =>
473
+ request(`/api/logging/annotations?conversation_id=${encodeURIComponent(conversationId)}`);
474
+
475
+ export const postLoggingAnnotation = ({ conversationId, traceId, note }) =>
476
+ request("/api/logging/annotations", {
477
+ method: "POST",
478
+ body: JSON.stringify({
479
+ conversation_id: conversationId,
480
+ trace_id: traceId,
481
+ note,
482
+ }),
483
+ });
484
+
485
+ export const getUsageAgentDetail = (agentId, days = 30) =>
486
+ request(`/api/usage/agents/${agentId}?days=${days}`);
487
+
488
+ export const getPublicConfig = async () => {
489
+ const res = await fetch(apiUrl(`/api/config`));
490
+ if (!res.ok) return { billing_enabled: true };
491
+ return res.json();
492
+ };
493
+
494
+ // --- RAG Knowledge Base ---
495
+ export const indexRagFiles = async (files, agentId, sessionId) => {
496
+ const formData = new FormData();
497
+ formData.append("agent_id", agentId);
498
+ if (sessionId) formData.append("session_id", sessionId);
499
+ files.forEach((f) => formData.append("files", f));
500
+ const token = getAuthToken();
501
+ const res = await fetch(apiUrl(`/api/rag/index-files`), {
502
+ method: "POST",
503
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
504
+ body: formData,
505
+ });
506
+ if (!res.ok) {
507
+ const body = await res.text();
508
+ throw new Error(`RAG file indexing failed (${res.status}): ${body}`);
509
+ }
510
+ return res.json();
511
+ };
512
+
513
+ export const indexRagUrl = (data) =>
514
+ request("/api/rag/index-url", { method: "POST", body: JSON.stringify(data) });
515
+
516
+ export const indexRagDb = (data) =>
517
+ request("/api/rag/index-db", { method: "POST", body: JSON.stringify(data) });
518
+
519
+ export const indexRagDbNl = (data) =>
520
+ request("/api/rag/index-db-nl", { method: "POST", body: JSON.stringify(data) });
521
+
522
+ export const indexRagS3 = (data) =>
523
+ request("/api/rag/index-s3", { method: "POST", body: JSON.stringify(data) });
524
+
525
+ export const getRagStatus = (knowledgeId, agentId) =>
526
+ request(`/api/rag/status/${knowledgeId}?agent_id=${encodeURIComponent(agentId)}`);
527
+
528
+ export const listRagKnowledge = (agentId, sessionId) =>
529
+ request(`/api/rag/knowledge/${agentId}${sessionId ? `?session_id=${encodeURIComponent(sessionId)}` : ""}`);
530
+
531
+ // --- Saved API Keys ---
532
+ export const listSavedApiKeys = () => request("/api/saved-api-keys");
533
+
534
+ export const createSavedApiKey = (data) =>
535
+ request("/api/saved-api-keys", { method: "POST", body: JSON.stringify(data) });
536
+
537
+ export const deleteSavedApiKey = (id) =>
538
+ request(`/api/saved-api-keys/${id}`, { method: "DELETE" });
539
+
540
+ // --- Webhook Subscriptions ---
541
+ export const listWebhookSubscriptions = () =>
542
+ request("/api/webhooks/subscriptions");
543
+
544
+ export const createWebhookSubscription = (data) =>
545
+ request("/api/webhooks/subscriptions", { method: "POST", body: JSON.stringify(data) });
546
+
547
+ export const updateWebhookSubscription = (id, data) =>
548
+ request(`/api/webhooks/subscriptions/${id}`, { method: "PATCH", body: JSON.stringify(data) });
549
+
550
+ export const deleteWebhookSubscription = (id) =>
551
+ request(`/api/webhooks/subscriptions/${id}`, { method: "DELETE" });
552
+
553
+ export const renewWebhookSubscription = (id) =>
554
+ request(`/api/webhooks/subscriptions/${id}/renew`, { method: "POST" });
555
+
556
+ export const listWebhookLogs = (params = {}) => {
557
+ const query = new URLSearchParams(params).toString();
558
+ return request(`/api/webhooks/logs${query ? `?${query}` : ""}`);
559
+ };
560
+
561
+ export const getWebhookLog = (id) =>
562
+ request(`/api/webhooks/logs/${id}`);
563
+
564
+ export const retriggerWebhookLog = (logId) =>
565
+ request(`/api/webhooks/logs/${logId}/retrigger`, { method: "POST" });
566
+
567
+ export const getWebhookLogBody = async (id) => {
568
+ const token = getAuthToken();
569
+ const res = await fetch(`/api/webhooks/logs/${id}/body`, {
570
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
571
+ });
572
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
573
+ return res.text();
574
+ };
575
+
576
+ // Fetch one webhook attachment WITH auth and return a short-lived object URL.
577
+ //
578
+ // The serve endpoint is Bearer-authenticated like every other API route.
579
+ // A plain ``window.open(url)`` is a top-level navigation that carries no
580
+ // Authorization header, so the backend answers 401 ("Not authenticated").
581
+ // We therefore fetch the bytes with the token, wrap them in a Blob, and
582
+ // hand back an object URL the caller can open in a new tab. The 401 ->
583
+ // refresh -> retry dance mirrors ``request`` so an expired access token
584
+ // doesn't break the preview.
585
+ export const fetchWebhookLogAttachmentObjectUrl = async (logId, filename) => {
586
+ const path = apiUrl(`/api/webhooks/logs/${logId}/attachments/${encodeURIComponent(filename)}`);
587
+ let res = await fetch(path, { headers: getAuthHeaders() });
588
+ if (res.status === 401) {
589
+ const newToken = await attemptTokenRefresh();
590
+ if (newToken) {
591
+ res = await fetch(path, { headers: { Authorization: `Bearer ${newToken}` } });
592
+ } else {
593
+ // Refresh failed — clear auth and stop here instead of falling
594
+ // through on the stale 401 ``res`` (mirrors ``request``).
595
+ clearAuth();
596
+ notifyUnauthorized();
597
+ const err = new Error("HTTP 401");
598
+ err.status = 401;
599
+ throw err;
600
+ }
601
+ }
602
+ if (!res.ok) {
603
+ const err = new Error(`HTTP ${res.status}`);
604
+ err.status = res.status;
605
+ throw err;
606
+ }
607
+ const blob = await res.blob();
608
+ return URL.createObjectURL(blob);
609
+ };
610
+
611
+ // --- Notifications ---
612
+ export const listNotifications = (params = {}) => {
613
+ const query = new URLSearchParams(params).toString();
614
+ return request(`/api/notifications${query ? `?${query}` : ""}`);
615
+ };
616
+
617
+ export const getUnreadNotificationCount = () =>
618
+ request("/api/notifications/unread-count");
619
+
620
+ export const markNotificationRead = (id) =>
621
+ request(`/api/notifications/${id}/read`, { method: "PATCH" });
622
+
623
+ export const markAllNotificationsRead = () =>
624
+ request("/api/notifications/read-all", { method: "POST" });
625
+
626
+ // --- Emailing / Outlook ---
627
+ export const getOutlookAuthUrl = () =>
628
+ request("/api/emailing/microsoft/auth-url");
629
+
630
+ export const getOutlookStatus = () =>
631
+ request("/api/emailing/microsoft/status");
632
+
633
+ // --- BI & Reporting ---
634
+ export const listDashboards = (params = {}) => {
635
+ const query = new URLSearchParams(params).toString();
636
+ return request(`/api/v1/dashboards${query ? `?${query}` : ""}`);
637
+ };
638
+ export const getDashboard = (id) => request(`/api/v1/dashboards/${id}`);
639
+ export const getDashboardBySlug = (slug) =>
640
+ request(`/api/v1/dashboards/by-slug/${encodeURIComponent(slug)}`);
641
+ // Add a chart to the user's chat dashboard (user-triggered "Send to dashboard").
642
+ export const sendChartToDashboard = (chartId, sessionId) => {
643
+ const qs = sessionId ? `?session_id=${encodeURIComponent(sessionId)}` : "";
644
+ return request(`/api/v1/charts/${encodeURIComponent(chartId)}/send-to-dashboard${qs}`, {
645
+ method: "POST",
646
+ });
647
+ };
648
+ export const listSharedDashboards = (params = {}) => {
649
+ const query = new URLSearchParams(params).toString();
650
+ return request(`/api/v1/dashboards/shared${query ? `?${query}` : ""}`);
651
+ };
652
+ export const createDashboard = (data) =>
653
+ request("/api/v1/dashboards", { method: "POST", body: JSON.stringify(data) });
654
+ export const updateDashboard = (id, data) =>
655
+ request(`/api/v1/dashboards/${id}`, { method: "PATCH", body: JSON.stringify(data) });
656
+ export const deleteDashboard = (id) =>
657
+ request(`/api/v1/dashboards/${id}`, { method: "DELETE" });
658
+ export const publishDashboard = (id, data = {}) =>
659
+ request(`/api/v1/dashboards/${id}/publish`, { method: "POST", body: JSON.stringify(data) });
660
+
661
+ export const unpublishDashboard = (id) =>
662
+ request(`/api/v1/dashboards/${id}/unpublish`, { method: "POST" });
663
+
664
+ export const getPublicDashboard = (slug) =>
665
+ request(`/api/v1/dashboards/public/${slug}`, { silent401: true });
666
+
667
+ export const getPublicChartData = (chartId, params = {}) => {
668
+ const query = new URLSearchParams(params).toString();
669
+ return request(`/api/v1/public/charts/${chartId}/data${query ? `?${query}` : ""}`, { silent401: true });
670
+ };
671
+
672
+ export const listCharts = (params = {}) => {
673
+ const query = new URLSearchParams(params).toString();
674
+ return request(`/api/v1/charts${query ? `?${query}` : ""}`);
675
+ };
676
+ export const getChart = (id) => request(`/api/v1/charts/${id}`);
677
+ export const createChart = (data) =>
678
+ request("/api/v1/charts", { method: "POST", body: JSON.stringify(data) });
679
+ export const updateChart = (id, data) =>
680
+ request(`/api/v1/charts/${id}`, { method: "PATCH", body: JSON.stringify(data) });
681
+ export const deleteChart = (id) =>
682
+ request(`/api/v1/charts/${id}`, { method: "DELETE" });
683
+ export const getChartData = (chartId, params = {}) => {
684
+ const query = new URLSearchParams(params).toString();
685
+ return request(`/api/v1/charts/${chartId}/data${query ? `?${query}` : ""}`);
686
+ };
687
+
688
+ export async function scheduleChartRefresh(chartId, { agentId, interval, startTime, messageTemplate }) {
689
+ return request(`/api/v1/charts/${chartId}/schedule-refresh`, {
690
+ method: "POST",
691
+ body: JSON.stringify({
692
+ agent_id: agentId,
693
+ interval,
694
+ start_time: startTime || undefined,
695
+ message_template: messageTemplate || undefined,
696
+ }),
697
+ });
698
+ }
699
+
700
+ export const addDashboardComponent = (dashboardId, data) =>
701
+ request(`/api/v1/dashboards/${dashboardId}/components`, { method: "POST", body: JSON.stringify(data) });
702
+ export const removeDashboardComponent = (dashboardId, componentId) =>
703
+ request(`/api/v1/dashboards/${dashboardId}/components/${componentId}`, { method: "DELETE" });
704
+ export const moveDashboardComponent = (dashboardId, componentId, data) =>
705
+ request(`/api/v1/dashboards/${dashboardId}/components/${componentId}/position`, { method: "PATCH", body: JSON.stringify(data) });
706
+ export const updateDashboardComponent = (dashboardId, componentId, data) =>
707
+ request(`/api/v1/dashboards/${dashboardId}/components/${componentId}`, { method: "PATCH", body: JSON.stringify(data) });
708
+
709
+ // Link/unlink agent to dashboard
710
+ export const linkAgentToDashboard = (dashboardId, agentId) =>
711
+ request(`/api/v1/dashboards/${dashboardId}/agent`, {
712
+ method: "PATCH",
713
+ body: JSON.stringify({ agent_id: agentId }),
714
+ });
715
+
716
+ // Get agent linked to dashboard
717
+ export const getDashboardAgent = (dashboardId) =>
718
+ request(`/api/v1/dashboards/${dashboardId}/agent`);
719
+
720
+ export const getBiStats = (organizationId) =>
721
+ request(`/api/v1/bi/stats?organization_id=${encodeURIComponent(organizationId || "default")}`);
722
+
723
+ export const listBiDatasets = (organizationId) =>
724
+ request(`/api/v1/bi/datasets?organization_id=${encodeURIComponent(organizationId || "default")}`);
725
+
726
+ export const previewBiDataset = (fileId, organizationId) =>
727
+ request(`/api/v1/bi/datasets/${encodeURIComponent(fileId)}/preview?organization_id=${encodeURIComponent(organizationId || "default")}`);
728
+
729
+ export const previewOnedriveSpreadsheet = ({ itemPath, itemId = null, sheetName = null }) =>
730
+ request("/api/v1/bi/onedrive/preview", {
731
+ method: "POST",
732
+ body: JSON.stringify({
733
+ item_path: itemPath,
734
+ item_id: itemId,
735
+ sheet_name: sheetName,
736
+ }),
737
+ });
738
+
739
+ export const listBiDbConfigs = (organizationId) =>
740
+ request(`/api/v1/bi/tool-configs/database?organization_id=${encodeURIComponent(organizationId || "default")}`);
741
+
742
+ export const deleteBiDataset = (fileId, organizationId) =>
743
+ request(`/api/v1/bi/datasets/${encodeURIComponent(fileId)}?organization_id=${encodeURIComponent(organizationId || "default")}`, { method: "DELETE" });
744
+
745
+ // --- Skills ---
746
+ export const listSkills = () => request("/api/skills");
747
+ export const listPortfolioSkills = () => request("/api/skills/portfolio");
748
+ export const getSkill = (id) => request(`/api/skills/${id}`);
749
+ export const createSkill = (data) =>
750
+ request("/api/skills", { method: "POST", body: JSON.stringify(data) });
751
+ export const updateSkill = (id, data) =>
752
+ request(`/api/skills/${id}`, { method: "PUT", body: JSON.stringify(data) });
753
+ export const deleteSkill = (id) =>
754
+ request(`/api/skills/${id}`, { method: "DELETE" });
755
+
756
+ export const exportSkill = (id, format = "json") =>
757
+ apiUrl(`/api/skills/${id}/export?format=${encodeURIComponent(format)}`);
758
+
759
+ export const exportPortfolioSkill = (name, format = "json") =>
760
+ apiUrl(`/api/skills/portfolio/${encodeURIComponent(name)}/export?format=${encodeURIComponent(format)}`);
761
+
762
+ export const importSkill = async (file) => {
763
+ const formData = new FormData();
764
+ formData.append("file", file);
765
+ const token = getAuthToken();
766
+ const res = await fetch(apiUrl(`/api/skills/import`), {
767
+ method: "POST",
768
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
769
+ body: formData,
770
+ });
771
+ if (!res.ok) {
772
+ const body = await res.text();
773
+ let message;
774
+ try {
775
+ const parsed = JSON.parse(body);
776
+ message = parsed.detail || `Import failed (${res.status})`;
777
+ } catch {
778
+ message = `Import failed (${res.status}): ${body}`;
779
+ }
780
+ throw new Error(message);
781
+ }
782
+ return res.json();
783
+ };
784
+
785
+ // --- Email Campaigns ---
786
+ export const launchCampaign = (payload) =>
787
+ request("/api/campaigns/launch-from-dashboard", {
788
+ method: "POST",
789
+ body: JSON.stringify(payload),
790
+ });
791
+
792
+ export const getCampaignStatus = (campaignId) =>
793
+ request(`/api/campaigns/${encodeURIComponent(campaignId)}/status`, {
794
+ method: "GET",
795
+ });
796
+
797
+ export async function getOnedriveExcelPreview(
798
+ itemPath,
799
+ { sheetName = null, limit = 5 } = {},
800
+ ) {
801
+ const params = new URLSearchParams({
802
+ item_path: itemPath,
803
+ limit: String(limit),
804
+ });
805
+ if (sheetName) params.append("sheet_name", String(sheetName));
806
+ return await request(
807
+ `/api/onedrivebrowser/excel-preview?${params.toString()}`,
808
+ { method: "GET" },
809
+ );
810
+ }
811
+
812
+ export const uploadBiCsv = async (file, separator = "auto", organizationId) => {
813
+ const formData = new FormData();
814
+ formData.append("file", file);
815
+ formData.append("separator", separator);
816
+ formData.append("organization_id", organizationId || "default");
817
+ const token = getAuthToken();
818
+ // Upload directly to backend to bypass Next.js proxy body size limit
819
+ const backendUrl = process.env.NEXT_PUBLIC_API_URL || "";
820
+ const url = backendUrl
821
+ ? `${backendUrl}/api/v1/bi/upload-csv`
822
+ : apiUrl(`/api/v1/bi/upload-csv`);
823
+ const res = await fetch(url, {
824
+ method: "POST",
825
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
826
+ body: formData,
827
+ });
828
+ if (!res.ok) throw new Error(`CSV upload failed (${res.status})`);
829
+ return res.json();
830
+ };