Spaces:
Sleeping
Sleeping
Update
Browse files- server/aiCore.js +33 -18
- server/aiMixMiddleware.ts +23 -16
server/aiCore.js
CHANGED
|
@@ -68,14 +68,22 @@ function validateObservation({ text, temperatureC, hasHazard }) {
|
|
| 68 |
if (/[#`]/.test(t)) return { ok: false, reason: "contient du markdown" };
|
| 69 |
|
| 70 |
const sentenceCount = countSentences(t);
|
| 71 |
-
if (sentenceCount <
|
| 72 |
|
| 73 |
const tempOk =
|
| 74 |
-
/
|
|
|
|
|
|
|
|
|
|
| 75 |
if (!tempOk) return { ok: false, reason: "temperature non mentionnee" };
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
if (!/cas d'usage\s*:/i.test(t)) return { ok: false, reason: "cas d'usage manquant" };
|
| 78 |
-
|
|
|
|
|
|
|
| 79 |
|
| 80 |
if (hasHazard) {
|
| 81 |
const safetyOk = /secur|prud|attention|protection|danger|gants|lunettes|corros/i.test(t);
|
|
@@ -85,6 +93,13 @@ function validateObservation({ text, temperatureC, hasHazard }) {
|
|
| 85 |
return { ok: true, reason: null };
|
| 86 |
}
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
function buildPrompt({ temperatureC, substances }) {
|
| 89 |
const lines = substances.map((s) =>
|
| 90 |
[
|
|
@@ -110,18 +125,10 @@ function buildPrompt({ temperatureC, substances }) {
|
|
| 110 |
`- Substances presentes (a melanger):`,
|
| 111 |
...lines,
|
| 112 |
"",
|
| 113 |
-
"
|
| 114 |
-
"
|
| 115 |
-
"
|
| 116 |
-
"-
|
| 117 |
-
"- un rappel securite si au moins une substance est dangereuse.",
|
| 118 |
-
"Puis ajoute a la suite (dans le meme paragraphe, separe par 'Cas d'usage:') un micro-texte (2 a 3 phrases) qui explique:",
|
| 119 |
-
"- a quoi ce melange/reaction peut servir dans la vraie vie (industrie, analyse, menage, sante, environnement, etc.),",
|
| 120 |
-
"- une suggestion de 1 ou 2 melanges a explorer ensuite (utilise uniquement des substances de la liste donnee).",
|
| 121 |
-
"Contraintes de forme:",
|
| 122 |
-
"- un seul paragraphe (pas de listes, pas de sauts de ligne)",
|
| 123 |
-
"- 5 a 9 phrases completes au total (observations + cas d'usage)",
|
| 124 |
-
"- mention explicite de la temperature (en degres Celsius) et de son effet",
|
| 125 |
"- pas de markdown, pas d'emojis",
|
| 126 |
].join("\n");
|
| 127 |
|
|
@@ -260,7 +267,15 @@ async function callGroqChatCompletionsText({ apiKey, model, system, user }) {
|
|
| 260 |
}
|
| 261 |
|
| 262 |
if (typeof msg?.refusal === "string" && msg.refusal.trim()) throw new UpstreamError(422, "Model refusal", msg.refusal.trim());
|
| 263 |
-
if (typeof msg?.reasoning === "string" && msg.reasoning.trim())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
if (Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0) throw new UpstreamError(422, "Model returned tool_calls with no text");
|
| 265 |
|
| 266 |
throw new UpstreamError(502, "Groq Chat Completions returned no message content");
|
|
@@ -316,7 +331,7 @@ export function createAiCore(env = {}) {
|
|
| 316 |
const attemptUser =
|
| 317 |
attempt === 1
|
| 318 |
? user
|
| 319 |
-
: `${user}\n\nLe texte precedent ne respecte pas les contraintes (${lastValidation?.reason || "incomplet"}). Regenerate en respectant STRICTEMENT
|
| 320 |
|
| 321 |
try {
|
| 322 |
let text;
|
|
@@ -331,6 +346,7 @@ export function createAiCore(env = {}) {
|
|
| 331 |
}
|
| 332 |
}
|
| 333 |
|
|
|
|
| 334 |
const v = validateObservation({ text, temperatureC: safeTemp, hasHazard });
|
| 335 |
lastValidation = v;
|
| 336 |
if (v.ok) return { text, modelUsed: model, triedModels };
|
|
@@ -360,4 +376,3 @@ export function createAiCore(env = {}) {
|
|
| 360 |
},
|
| 361 |
};
|
| 362 |
}
|
| 363 |
-
|
|
|
|
| 68 |
if (/[#`]/.test(t)) return { ok: false, reason: "contient du markdown" };
|
| 69 |
|
| 70 |
const sentenceCount = countSentences(t);
|
| 71 |
+
if (sentenceCount < 6 || sentenceCount > 11) return { ok: false, reason: `nombre de phrases invalide (${sentenceCount})` };
|
| 72 |
|
| 73 |
const tempOk =
|
| 74 |
+
/temperature\s*:/i.test(t) ||
|
| 75 |
+
/temp/i.test(t) ||
|
| 76 |
+
new RegExp(String(Math.round(temperatureC))).test(t) ||
|
| 77 |
+
/degres|°c|celsius/i.test(t);
|
| 78 |
if (!tempOk) return { ok: false, reason: "temperature non mentionnee" };
|
| 79 |
|
| 80 |
+
if (!/equation\s*:/i.test(t)) return { ok: false, reason: "equation manquante" };
|
| 81 |
+
if (!/(->|→)/.test(t)) return { ok: false, reason: "equation sans fleche de reaction" };
|
| 82 |
+
|
| 83 |
if (!/cas d'usage\s*:/i.test(t)) return { ok: false, reason: "cas d'usage manquant" };
|
| 84 |
+
// Force concrete examples rather than generic statements.
|
| 85 |
+
if (!/par exemple|comme/i.test(t)) return { ok: false, reason: "cas d'usage pas assez concret (manque 'comme/par exemple')" };
|
| 86 |
+
if (!/suggestions?\s*:/i.test(t)) return { ok: false, reason: "suggestions manquantes" };
|
| 87 |
|
| 88 |
if (hasHazard) {
|
| 89 |
const safetyOk = /secur|prud|attention|protection|danger|gants|lunettes|corros/i.test(t);
|
|
|
|
| 93 |
return { ok: true, reason: null };
|
| 94 |
}
|
| 95 |
|
| 96 |
+
function normalizeOneParagraph(text) {
|
| 97 |
+
return (text || "")
|
| 98 |
+
.replace(/\s+/g, " ")
|
| 99 |
+
.replace(/\u00a0/g, " ")
|
| 100 |
+
.trim();
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
function buildPrompt({ temperatureC, substances }) {
|
| 104 |
const lines = substances.map((s) =>
|
| 105 |
[
|
|
|
|
| 125 |
`- Substances presentes (a melanger):`,
|
| 126 |
...lines,
|
| 127 |
"",
|
| 128 |
+
"Rends exactement ce format, en UN SEUL PARAGRAPHE (sans sauts de ligne, sans listes):",
|
| 129 |
+
"Observation: <6 a 8 phrases sur ce qu'on voit + securite si dangereux> Temperature: <1 phrase sur l'effet de la temperature> Equation: <equation avec -> ou →; si incertain, le dire brièvement> Cas d'usage: <2 a 3 phrases avec exemples concrets, comme/par exemple savon, encre, nettoyage, analyse, traitement de l'eau> Suggestions: <1 a 2 melanges a tester ensuite en citant uniquement des substances de la liste>",
|
| 130 |
+
"Contraintes:",
|
| 131 |
+
"- 6 a 11 phrases au total",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
"- pas de markdown, pas d'emojis",
|
| 133 |
].join("\n");
|
| 134 |
|
|
|
|
| 267 |
}
|
| 268 |
|
| 269 |
if (typeof msg?.refusal === "string" && msg.refusal.trim()) throw new UpstreamError(422, "Model refusal", msg.refusal.trim());
|
| 270 |
+
if (typeof msg?.reasoning === "string" && msg.reasoning.trim()) {
|
| 271 |
+
// Some models may place the final answer into "reasoning". Accept it only if it matches our constraints.
|
| 272 |
+
const candidate = normalizeOneParagraph(msg.reasoning);
|
| 273 |
+
// Full validation is done later with real temp/hazard. Here we only ensure it looks like a final answer.
|
| 274 |
+
if (/equation\s*:/i.test(candidate) && /cas d'usage\s*:/i.test(candidate) && /(->|→)/.test(candidate)) {
|
| 275 |
+
return candidate;
|
| 276 |
+
}
|
| 277 |
+
throw new UpstreamError(422, "Model returned reasoning without final content");
|
| 278 |
+
}
|
| 279 |
if (Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0) throw new UpstreamError(422, "Model returned tool_calls with no text");
|
| 280 |
|
| 281 |
throw new UpstreamError(502, "Groq Chat Completions returned no message content");
|
|
|
|
| 331 |
const attemptUser =
|
| 332 |
attempt === 1
|
| 333 |
? user
|
| 334 |
+
: `${user}\n\nLe texte precedent ne respecte pas les contraintes (${lastValidation?.reason || "incomplet"}). Regenerate en respectant STRICTEMENT le format avec les labels Observation:, Temperature:, Equation:, Cas d'usage:, Suggestions:, en un seul paragraphe, sans sauts de ligne.`;
|
| 335 |
|
| 336 |
try {
|
| 337 |
let text;
|
|
|
|
| 346 |
}
|
| 347 |
}
|
| 348 |
|
| 349 |
+
text = normalizeOneParagraph(text);
|
| 350 |
const v = validateObservation({ text, temperatureC: safeTemp, hasHazard });
|
| 351 |
lastValidation = v;
|
| 352 |
if (v.ok) return { text, modelUsed: model, triedModels };
|
|
|
|
| 376 |
},
|
| 377 |
};
|
| 378 |
}
|
|
|
server/aiMixMiddleware.ts
CHANGED
|
@@ -75,18 +75,10 @@ function buildPrompt(args: {
|
|
| 75 |
`- Substances presentes (a melanger):`,
|
| 76 |
...lines,
|
| 77 |
"",
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
"
|
| 81 |
-
"-
|
| 82 |
-
"- un rappel securite si au moins une substance est dangereuse.",
|
| 83 |
-
"Puis ajoute a la suite (dans le meme paragraphe, separe par 'Cas d'usage:') un micro-texte (2 a 3 phrases) qui explique:",
|
| 84 |
-
"- a quoi ce melange/reaction peut servir dans la vraie vie (industrie, analyse, menage, sante, environnement, etc.),",
|
| 85 |
-
"- une suggestion de 1 ou 2 melanges a explorer ensuite (utilise uniquement des substances de la liste donnee).",
|
| 86 |
-
"Contraintes de forme:",
|
| 87 |
-
"- un seul paragraphe (pas de listes, pas de sauts de ligne)",
|
| 88 |
-
"- 5 a 9 phrases completes au total (observations + cas d'usage)",
|
| 89 |
-
"- mention explicite de la temperature (en degres Celsius) et de son effet",
|
| 90 |
"- pas de markdown, pas d'emojis",
|
| 91 |
].join("\n");
|
| 92 |
|
|
@@ -99,6 +91,13 @@ function countSentences(text: string) {
|
|
| 99 |
return matches ? matches.length : 0;
|
| 100 |
}
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
function validateObservation(args: {
|
| 103 |
text: string;
|
| 104 |
temperatureC: number;
|
|
@@ -113,20 +112,24 @@ function validateObservation(args: {
|
|
| 113 |
if (/[#`]/.test(t)) return { ok: false, reason: "contient du markdown" as const };
|
| 114 |
|
| 115 |
const sentenceCount = countSentences(t);
|
| 116 |
-
if (sentenceCount <
|
| 117 |
return { ok: false, reason: `nombre de phrases invalide (${sentenceCount})` as const };
|
| 118 |
}
|
| 119 |
|
| 120 |
// Temperature must be mentioned.
|
| 121 |
const tempOk =
|
|
|
|
| 122 |
/temp/i.test(t) ||
|
| 123 |
new RegExp(String(Math.round(args.temperatureC))).test(t) ||
|
| 124 |
/degres|°c|celsius/i.test(t);
|
| 125 |
if (!tempOk) return { ok: false, reason: "temperature non mentionnee" as const };
|
| 126 |
|
|
|
|
|
|
|
|
|
|
| 127 |
if (!/cas d'usage\s*:/i.test(t)) return { ok: false, reason: "cas d'usage manquant" as const };
|
| 128 |
-
|
| 129 |
-
if (!
|
| 130 |
|
| 131 |
// Safety reminder required if any hazardous substance is involved.
|
| 132 |
if (args.hasHazard) {
|
|
@@ -295,6 +298,10 @@ async function callGroqChatCompletionsText(args: {
|
|
| 295 |
// Treat as recoverable; the caller will try another model.
|
| 296 |
const reasoning = msg?.reasoning;
|
| 297 |
if (typeof reasoning === "string" && reasoning.trim()) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
throw new UpstreamError(422, "Model returned reasoning without final content");
|
| 299 |
}
|
| 300 |
|
|
@@ -502,7 +509,7 @@ function addRoutes(server: ViteDevServer | PreviewServer, env: Env) {
|
|
| 502 |
const attemptUser =
|
| 503 |
attempt === 1
|
| 504 |
? user
|
| 505 |
-
: `${user}\n\nLe texte precedent ne respecte pas les contraintes (${lastValidation?.reason || "incomplet"}). Regenerate en respectant STRICTEMENT
|
| 506 |
|
| 507 |
const text = await callGroqChatCompletion({ apiKey, model, system, user: attemptUser });
|
| 508 |
const v = validateObservation({ text, temperatureC: safeTemp, hasHazard });
|
|
|
|
| 75 |
`- Substances presentes (a melanger):`,
|
| 76 |
...lines,
|
| 77 |
"",
|
| 78 |
+
"Rends exactement ce format, en UN SEUL PARAGRAPHE (sans sauts de ligne, sans listes):",
|
| 79 |
+
"Observation: <6 a 8 phrases sur ce qu'on voit + securite si dangereux> Temperature: <1 phrase sur l'effet de la temperature> Equation: <equation avec -> ou →; si incertain, le dire brièvement> Cas d'usage: <2 a 3 phrases avec exemples concrets, comme/par exemple savon, encre, nettoyage, analyse, traitement de l'eau> Suggestions: <1 a 2 melanges a tester ensuite en citant uniquement des substances de la liste>",
|
| 80 |
+
"Contraintes:",
|
| 81 |
+
"- 6 a 11 phrases au total",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
"- pas de markdown, pas d'emojis",
|
| 83 |
].join("\n");
|
| 84 |
|
|
|
|
| 91 |
return matches ? matches.length : 0;
|
| 92 |
}
|
| 93 |
|
| 94 |
+
function normalizeOneParagraph(text: string) {
|
| 95 |
+
return (text || "")
|
| 96 |
+
.replace(/\s+/g, " ")
|
| 97 |
+
.replace(/\u00a0/g, " ")
|
| 98 |
+
.trim();
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
function validateObservation(args: {
|
| 102 |
text: string;
|
| 103 |
temperatureC: number;
|
|
|
|
| 112 |
if (/[#`]/.test(t)) return { ok: false, reason: "contient du markdown" as const };
|
| 113 |
|
| 114 |
const sentenceCount = countSentences(t);
|
| 115 |
+
if (sentenceCount < 6 || sentenceCount > 11) {
|
| 116 |
return { ok: false, reason: `nombre de phrases invalide (${sentenceCount})` as const };
|
| 117 |
}
|
| 118 |
|
| 119 |
// Temperature must be mentioned.
|
| 120 |
const tempOk =
|
| 121 |
+
/temperature\s*:/i.test(t) ||
|
| 122 |
/temp/i.test(t) ||
|
| 123 |
new RegExp(String(Math.round(args.temperatureC))).test(t) ||
|
| 124 |
/degres|°c|celsius/i.test(t);
|
| 125 |
if (!tempOk) return { ok: false, reason: "temperature non mentionnee" as const };
|
| 126 |
|
| 127 |
+
if (!/equation\s*:/i.test(t)) return { ok: false, reason: "equation manquante" as const };
|
| 128 |
+
if (!/(->|→)/.test(t)) return { ok: false, reason: "equation sans fleche de reaction" as const };
|
| 129 |
+
|
| 130 |
if (!/cas d'usage\s*:/i.test(t)) return { ok: false, reason: "cas d'usage manquant" as const };
|
| 131 |
+
if (!/par exemple|comme/i.test(t)) return { ok: false, reason: "cas d'usage pas assez concret (manque 'comme/par exemple')" as const };
|
| 132 |
+
if (!/suggestions?\s*:/i.test(t)) return { ok: false, reason: "suggestions manquantes" as const };
|
| 133 |
|
| 134 |
// Safety reminder required if any hazardous substance is involved.
|
| 135 |
if (args.hasHazard) {
|
|
|
|
| 298 |
// Treat as recoverable; the caller will try another model.
|
| 299 |
const reasoning = msg?.reasoning;
|
| 300 |
if (typeof reasoning === "string" && reasoning.trim()) {
|
| 301 |
+
const candidate = normalizeOneParagraph(reasoning);
|
| 302 |
+
if (/equation\s*:/i.test(candidate) && /cas d'usage\s*:/i.test(candidate) && /(->|→)/.test(candidate)) {
|
| 303 |
+
return candidate;
|
| 304 |
+
}
|
| 305 |
throw new UpstreamError(422, "Model returned reasoning without final content");
|
| 306 |
}
|
| 307 |
|
|
|
|
| 509 |
const attemptUser =
|
| 510 |
attempt === 1
|
| 511 |
? user
|
| 512 |
+
: `${user}\n\nLe texte precedent ne respecte pas les contraintes (${lastValidation?.reason || "incomplet"}). Regenerate en respectant STRICTEMENT le format avec les labels Observation:, Temperature:, Equation:, Cas d'usage:, Suggestions:, en un seul paragraphe, sans sauts de ligne.`;
|
| 513 |
|
| 514 |
const text = await callGroqChatCompletion({ apiKey, model, system, user: attemptUser });
|
| 515 |
const v = validateObservation({ text, temperatureC: safeTemp, hasHazard });
|