rinogeek commited on
Commit
18a6e87
·
1 Parent(s): f7333d7
Files changed (2) hide show
  1. server/aiCore.js +33 -18
  2. 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 < 5 || sentenceCount > 9) return { ok: false, reason: `nombre de phrases invalide (${sentenceCount})` };
72
 
73
  const tempOk =
74
- /temp/i.test(t) || new RegExp(String(Math.round(temperatureC))).test(t) || /degres|°c|celsius/i.test(t);
 
 
 
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
- if (!/explor|essaie|essayez|sugg|melang/i.test(t)) return { ok: false, reason: "suggestions de melange manquantes" };
 
 
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
- "Tache:",
114
- "Ecris UN court paragraphe en francais qui decrit:",
115
- "- les observations probables (changement de couleur, degagement de gaz, precipite, chaleur, odeur si pertinent),",
116
- "- l'impact de la temperature (si elle change la vitesse/violence de la reaction),",
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()) throw new UpstreamError(422, "Model returned reasoning without final content");
 
 
 
 
 
 
 
 
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 les contraintes. Un seul paragraphe, 5 a 9 phrases au total, inclure 'Cas d'usage:' et 1-2 suggestions de melanges, sans sauts de ligne.`;
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
- "Tache:",
79
- "Ecris UN court paragraphe en francais qui decrit:",
80
- "- les observations probables (changement de couleur, degagement de gaz, precipite, chaleur, odeur si pertinent),",
81
- "- l'impact de la temperature (si elle change la vitesse/violence de la reaction),",
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 < 5 || sentenceCount > 9) {
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
- const hasSuggestion = /explor|essaie|essayez|sugg|melang/i.test(t);
129
- if (!hasSuggestion) return { ok: false, reason: "suggestions de melange manquantes" as const };
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 les contraintes. Un seul paragraphe, 5 a 9 phrases au total, inclure 'Cas d'usage:' et 1-2 suggestions de melanges, sans sauts de ligne.`;
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 });