-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
397 lines (335 loc) · 17 KB
/
Copy pathserver.js
File metadata and controls
397 lines (335 loc) · 17 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
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
import express from "express";
import cors from "cors";
import path from "path";
import fs from "fs";
import https from "https";
import dns from "dns";
import { spawn } from "child_process";
import { fileURLToPath } from "url";
// Force Node.js to resolve IPv4 addresses first to avoid ETIMEDOUT bugs on dual-stack networks
dns.setDefaultResultOrder("ipv4first");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));
// Load token from environment or local .env file
let DEFAULT_HF_TOKEN = process.env.HF_TOKEN || "";
try {
const envPath = path.join(__dirname, ".env");
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, "utf8");
const match = envContent.match(/HF_TOKEN\s*=\s*(.*)/);
if (match && match[1]) {
DEFAULT_HF_TOKEN = match[1].trim().replace(/['"]/g, "");
}
}
} catch (e) {
console.error("Impossible de charger le fichier .env :", e);
}
const DEFAULT_HF_MODEL = "Qwen/Qwen2.5-Coder-7B-Instruct";
const SYSTEM_PROMPT = `Tu es Cypher AI, une intelligence artificielle d'élite spécialisée dans la programmation, l'algorithmie et la conception de logiciels.
Tu as été développé par DJAKOUA KWANKAM, étudiant en informatique à l'Institut Universitaire de Technologie de Douala (IUT).
Tu es direct, précis, rigoureux et tu t'exprimes de façon fluide sans fioritures superflues.
[INSTRUCTION TEMPORELLE IMPORTANTE] : L'année actuelle est STRICTEMENT 2026. Si l'utilisateur te demande la date actuelle ou l'année en cours, réponds avec assurance que nous sommes en 2026. Ne dis jamais que tu es en 2023 ou 2024.
Ne mentionne jamais tes dates de coupure de connaissances ou de fin d'entraînement (comme 2022). Utilise uniquement les informations actualisées fournies dans ton contexte de recherche en ligne si nécessaire.
[INSTRUCTIONS DE CONTEXTE INTERNET] :
Quand l'utilisateur te pose une question nécessitant des recherches actualisées ou de la documentation technique récente, des extraits de résultats de recherche DuckDuckGo seront injectés automatiquement en début de message. Utilise ces données pour enrichir et structurer tes explications de manière claire et actualisée.`;
/**
* Perform DuckDuckGo Search over HTTPS with IPv4 force
*/
function searchWeb(query) {
return new Promise((resolve) => {
console.log("🔍 Recherche DuckDuckGo pour :", query);
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const options = {
family: 4, // Force IPv4 to avoid Node.js/undici IPv6 ETIMEDOUT bugs
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
};
const req = https.get(url, options, (res) => {
let html = "";
res.on("data", (chunk) => { html += chunk; });
res.on("end", () => {
const results = [];
// DuckDuckGo HTML results are wrapped in web-result containers
const resultRegex = /<div class="[^"]*web-result[^"]*">([\s\S]*?)<div class="clear"><\/div>/g;
let match;
while ((match = resultRegex.exec(html)) !== null && results.length < 10) {
const body = match[1];
// Extract Title & URL
const titleMatch = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/.exec(body);
let url = titleMatch ? titleMatch[1] : "";
let title = titleMatch ? titleMatch[2].replace(/<[^>]*>/g, "").trim() : "";
if (url) {
const uddgMatch = /uddg=([^&]*)/.exec(url);
if (uddgMatch) {
url = decodeURIComponent(uddgMatch[1]);
} else if (url.startsWith("//")) {
url = "https:" + url;
}
}
// Extract Snippet
const snippetMatch = /<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/.exec(body);
let snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]*>/g, "").trim() : "";
if (title && snippet) {
title = title.replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, '&');
snippet = snippet.replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, '&');
results.push({ title, snippet, url });
}
}
resolve(results);
});
});
req.on("error", (e) => {
console.error("❌ Erreur HTTPS DuckDuckGo :", e);
resolve([]);
});
req.setTimeout(5000, () => {
console.error("⚠️ Timeout de 5 secondes atteint pour la recherche DuckDuckGo. Abandon.");
req.destroy();
resolve([]);
});
});
}
/**
* Triggers background python process to upload logs to HF datasets
*/
async function triggerTelemetry(username, message, responseText, token) {
const activeToken = token || DEFAULT_HF_TOKEN;
if (!activeToken) {
console.log("[Télémétrie] Aucun token disponible, abandon.");
return;
}
const logEntry = {
username: username,
timestamp: new Date().toISOString(),
message: message,
response: responseText,
client: "cypher-standalone-web"
};
// Exécuter en arrière-plan sans bloquer la requête client
(async () => {
try {
// 1. Récupérer le nom d'utilisateur Hugging Face
const whoamiRes = await fetch("https://huggingface.co/api/whoami-v2", {
headers: { "Authorization": `Bearer ${activeToken}` }
});
if (!whoamiRes.ok) {
throw new Error(`HF whoami a échoué: ${whoamiRes.statusText}`);
}
const whoami = await whoamiRes.json();
const hfUser = whoami.name;
const repoId = `${hfUser}/cypher-coder-logs`;
// 2. Créer le dépôt si nécessaire (ignore 409 Conflict s'il existe déjà)
await fetch("https://huggingface.co/api/repos/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${activeToken}`
},
body: JSON.stringify({
name: "cypher-coder-logs",
type: "dataset",
private: true
})
});
// 3. Commiter le fichier de log sur la branche main
const dateStr = new Date().toISOString().replace(/[-:T]/g, "").split(".")[0];
const randStr = Math.random().toString(36).substring(7);
const pathInRepo = `logs/${username}/${dateStr}_${randStr}.json`;
const commitRes = await fetch(`https://huggingface.co/api/datasets/${repoId}/commit/main`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${activeToken}`
},
body: JSON.stringify({
actions: [
{
action: "add",
path: pathInRepo,
content: Buffer.from(JSON.stringify(logEntry, null, 2)).toString("base64")
}
],
message: `Add telemetry log for ${username}`
})
});
if (!commitRes.ok) {
const errText = await commitRes.text();
throw new Error(`HF commit a échoué: ${errText}`);
}
console.log(`[Télémétrie] Log envoyé avec succès sur HF : ${pathInRepo}`);
} catch (e) {
console.error("[Télémétrie] Erreur lors de l'envoi :", e.message);
}
})();
}
/**
* POST /api/chat - Chat completion proxy with RAG search and SSE streaming
*/
app.post("/api/chat", async (req, res) => {
const { messages, webSearch, username, token, model, temperature, maxTokens, searchMode } = req.body;
const activeToken = token || DEFAULT_HF_TOKEN;
const activeUsername = username || "invité";
const activeModel = model || DEFAULT_HF_MODEL;
const activeTemperature = (temperature !== undefined) ? temperature : 0.7;
const activeMaxTokens = maxTokens || 2048;
const activeSearchMode = searchMode || "web";
console.log(`📨 [Chat] Requête reçue de "${activeUsername}" (Recherche Web active: ${webSearch}, Modèle: ${activeModel})`);
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: "Invalid messages history." });
}
// Set SSE headers immediately to support real-time logs before model streaming
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// Helper to send log messages
const sendLog = (message, status = "info", extra = {}) => {
res.write(`data: ${JSON.stringify({ type: "log", message, status, ...extra })}\n\n`);
};
try {
const lastMessage = messages[messages.length - 1]?.content || "";
let context = "";
console.log(`💬 Message reçu: "${lastMessage.substring(0, 60)}${lastMessage.length > 60 ? '...' : ''}"`);
// Check triggers for web search
let shouldSearch = webSearch;
const msgLower = lastMessage.toLowerCase();
const keywords = [
"cherche sur le web", "recherche sur le web", "cherche sur internet", "recherche sur internet",
"actualités", "actualité", "dernière version", "nouveautés de", "nouveautés sur",
"météo", "cours de l'action", "dernières nouvelles"
];
if (!shouldSearch) {
for (const kw of keywords) {
if (msgLower.includes(kw)) {
shouldSearch = true;
break;
}
}
}
if (lastMessage.startsWith("/web ") || lastMessage.startsWith("/search ")) {
shouldSearch = true;
}
// Avoid triggering RAG search for simple greetings or short phrases to act thoughtfully
const simpleGreetings = /^(bonjour|salut|hello|hi|hey|coucou|yo|bonsoir|test|testing|merci|thanks|thank you|ça va\s*\??|ca va\s*\??|comment ça va\s*\??|how are you\s*\??|qui es-tu\s*\??|tu es qui\s*\??)(\s*!*)?$/i;
if (shouldSearch && (msgLower.trim().length < 3 || simpleGreetings.test(msgLower.trim()))) {
shouldSearch = false;
}
if (shouldSearch) {
sendLog("🔍 Initialisation de la recherche en ligne...", "start");
let searchQuery = lastMessage;
for (const kw of keywords) {
const regex = new RegExp(kw, "gi");
searchQuery = searchQuery.replace(regex, "");
}
if (searchQuery.startsWith("/web ")) searchQuery = searchQuery.slice(5);
if (searchQuery.startsWith("/search ")) searchQuery = searchQuery.slice(8);
searchQuery = searchQuery.trim().replace(/^[:?./"']+|[:?./"']+$/g, "");
if (!searchQuery || searchQuery.length < 3) {
searchQuery = lastMessage;
}
// Apply search focus filters
if (activeSearchMode === "code") {
searchQuery = searchQuery + " site:github.com OR site:stackoverflow.com OR site:developer.mozilla.org OR site:npmjs.com";
} else if (activeSearchMode === "academic") {
searchQuery = searchQuery + " site:arxiv.org OR site:scholar.google.com OR site:wikipedia.org OR site:nature.com";
}
let displayQuery = searchQuery;
if (displayQuery.length > 50) {
displayQuery = displayQuery.substring(0, 50) + "...";
}
sendLog(`🔎 Recherche de : "${displayQuery}"...`, "searching");
const searchResults = await searchWeb(searchQuery);
if (searchResults.length > 0) {
sendLog(`📄 ${searchResults.length} sources trouvées sur le web. Lecture des pages...`, "reading");
// Stream source items
searchResults.forEach((r) => {
sendLog(`🔗 Source trouvée : ${r.title}`, "source", { source: r });
});
const formatted = searchResults.map((r, idx) => `[${idx + 1}] Titre: ${r.title}\nRésumé: ${r.snippet}\nLien: ${r.url}`).join("\n\n");
context = `\n\n[CONTEXTE DU WEB]\n${formatted}\n[FIN DU CONTEXTE]`;
} else {
sendLog("⚠️ Aucun résultat trouvé.", "warning");
}
}
sendLog("🧠 Synthèse des faits et génération de la réponse...", "thinking");
// Setup messages for HF API with temporal context
const dateString = new Date().toLocaleDateString("fr-FR", { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
const dateContext = `[INFO TEMPORELLE CRITIQUE] : Aujourd'hui nous sommes le ${dateString} (Année 2026). L'année en cours est STRICTEMENT 2026. Ignore toute donnée disant que nous sommes en 2023 ou 2024. Si on te demande la date ou l'année, réponds impérativement 2026.\n`;
const formattedMessages = [
{ role: "system", content: dateContext + SYSTEM_PROMPT + (context ? `\nTu as accès aux résultats de recherche suivants pour répondre à l'utilisateur : ${context}` : "") },
...messages
];
// Query Hugging Face Serverless API with streaming
const hfResponse = await fetch("https://router.huggingface.co/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${activeToken}`
},
body: JSON.stringify({
model: activeModel,
messages: formattedMessages,
temperature: activeTemperature,
max_tokens: activeMaxTokens,
stream: true
})
});
if (!hfResponse.ok) {
const errText = await hfResponse.text();
throw new Error(`Hugging Face API returned ${hfResponse.status}: ${errText}`);
}
let fullResponseText = "";
const decoder = new TextDecoder("utf-8");
// Stream chunks to client
for await (const chunk of hfResponse.body) {
const textChunk = decoder.decode(chunk, { stream: true });
res.write(textChunk);
// Extract tokens for background log
const lines = textChunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const dataStr = line.slice(6).trim();
if (dataStr === "[DONE]") continue;
try {
const parsed = JSON.parse(dataStr);
const content = parsed.choices[0]?.delta?.content || "";
fullResponseText += content;
} catch (e) {
// Suppress parsing errors of incomplete lines
}
}
}
}
res.end();
// Trigger telemetry logging in background
triggerTelemetry(activeUsername, lastMessage, fullResponseText, activeToken);
} catch (error) {
console.error("❌ Error in chat handler:", error);
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
// API models list endpoint
app.get("/api/models", (req, res) => {
res.json([
{ id: "Qwen/Qwen2.5-Coder-7B-Instruct", name: "Qwen 2.5 Coder 7B (Par défaut)" },
{ id: "meta-llama/Llama-3.2-3B-Instruct", name: "Llama 3.2 3B (Gratuit CPU)" },
{ id: "mistralai/Mistral-7B-Instruct-v0.3", name: "Mistral 7B (Gratuit CPU)" },
{ id: "microsoft/Phi-3-mini-4k-instruct", name: "Phi-3 Mini 4K (Gratuit CPU)" }
]);
});
// API server health check endpoint
app.get("/api/health", (req, res) => {
res.json({ status: "ok", time: new Date().toISOString() });
});
// Start Express Server
app.listen(PORT, "0.0.0.0", () => {
console.log(`⚡ Cypher App lancée localement !`);
console.log(`💻 Accès PC : http://localhost:${PORT}`);
console.log(`📱 Accès Android : Ouvrez http://<IP_DE_VOTRE_PC>:${PORT} sur votre téléphone`);
});