-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
96 lines (78 loc) · 2.96 KB
/
Copy pathserver.ts
File metadata and controls
96 lines (78 loc) · 2.96 KB
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
import "dotenv/config";
import express from "express";
import { embed } from "./embed.js";
import { loadStore, search, storeSize } from "./store.js";
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
const PORT = process.env.PORT || 3000;
const MODEL = "claude-sonnet-5"; // ajustez selon vos besoins de coût/qualité
const app = express();
app.use(express.json());
app.use(express.static("public"));
await loadStore();
console.log(`Store chargé : ${storeSize()} chunk(s) indexé(s)`);
app.post("/api/chat", async (req, res) => {
const { message } = req.body as { message: string };
if (!message) {
return res.status(400).json({ error: "message manquant" });
}
try {
// 1. Embedder la question de l'utilisateur
const [queryEmbedding] = await embed([message], "query");
// 2. Retrouver les chunks les plus pertinents
const topChunks = search(queryEmbedding, 4);
const context = topChunks
.map(
(c, i) =>
`[Extrait ${i + 1} — source: ${c.source}]\n${c.text}`
)
.join("\n\n---\n\n");
// 3. Construire le prompt et appeler Claude avec streaming
const systemPrompt = `Tu réponds aux questions de l'utilisateur en te basant UNIQUEMENT sur les extraits de documents fournis ci-dessous. Si la réponse ne s'y trouve pas, dis clairement que l'information n'est pas dans les documents fournis plutôt que d'inventer.
EXTRAITS DE DOCUMENTS :
${context}`;
const anthropicRes = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": ANTHROPIC_API_KEY!,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: MODEL,
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: "user", content: message }],
stream: true,
}),
});
if (!anthropicRes.ok || !anthropicRes.body) {
const errText = await anthropicRes.text();
return res.status(500).json({ error: `Erreur API Claude: ${errText}` });
}
// 4. Relayer le stream SSE de Claude directement au client
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// On renvoie aussi les sources trouvées en tout premier événement,
// pour que le front puisse les afficher.
res.write(
`event: sources\ndata: ${JSON.stringify(
topChunks.map((c) => ({ source: c.source, score: c.score }))
)}\n\n`
);
const reader = anthropicRes.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(decoder.decode(value));
}
res.end();
} catch (err) {
console.error(err);
res.status(500).json({ error: "Erreur serveur" });
}
});
app.listen(PORT, () => {
console.log(`Serveur RAG démarré sur http://localhost:${PORT}`);
});