@runnerpro/backend 1.19.7 → 1.19.9

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.
@@ -16,7 +16,23 @@ const modelPricing_1 = require("./modelPricing");
16
16
  const constants_1 = require("./constants");
17
17
  const googleModel_1 = require("./googleModel");
18
18
  const bedrockModel_1 = require("./bedrockModel");
19
+ const slack_1 = require("../slack");
19
20
  const MIN_RETRIES = 1;
21
+ // ✅ Detección de Chain-of-Thought (CoT) que se cuela en la respuesta final del modelo
22
+ const COT_HTML_TAG = /<\/?(thinking|thought|reasoning)\b/i;
23
+ const COT_STRONG_TAG = /(^|\n)\s*(thought|thinking|reasoning|chain[- ]of[- ]thought|final answer|let me think)\s*[:\n]/i;
24
+ const COT_META_BULLET = /^\s*\*\s+\*[A-Z][^*\n]{1,40}\*/gm;
25
+ const COT_META_PATTERNS = [
26
+ /\battempt\s*\d+\b/i,
27
+ /\bcritique\s*\d+\b/i,
28
+ /\bself[- ]correction\b/i,
29
+ /\b(refining|refined|final polish|final version|final check|revised (draft|version))\b/i,
30
+ /\bword count(\s*check)?\b/i,
31
+ /\blet'?s (check|see|count|verify|recount)\b/i,
32
+ /\b(okay|wait|actually|hmm),?\s+(let|so|now|the|i|that|maybe)\b/i,
33
+ ];
34
+ const COT_ENGLISH_FUNCTION_WORDS = /\b(the|and|with|that|this|would|should|because|let'?s|we'?ll|maybe|okay)\b/gi;
35
+ const COT_SCORE_THRESHOLD = 3;
20
36
  const costTrackingStorage = new node_async_hooks_1.AsyncLocalStorage();
21
37
  /**
22
38
  * Indica si conviene saltar inmediatamente al siguiente modelo de la cadena de fallback.
@@ -63,6 +79,111 @@ function extractModelName(model) {
63
79
  }
64
80
  // eslint-disable-next-line no-console
65
81
  const log = (...args) => console.error(...args);
82
+ /**
83
+ * Detecta si un texto contiene chain-of-thought (razonamiento interno del modelo)
84
+ * que NO debe llegar al usuario final. Usa una combinación de:
85
+ * - Señales fuertes: etiquetas explícitas tipo `<thinking>`, `thought:` o bullets meta
86
+ * tipo `* *Draft:*`. Cualquiera dispara directamente.
87
+ * - Señales débiles: vocabulario meta (Attempt N, Critique N, word count...), densidad
88
+ * de asteriscos/bullets/comillas y palabras funcionales en inglés (las respuestas
89
+ * legítimas siempre van en español). Suman puntos hasta un umbral.
90
+ *
91
+ * @param text - Texto devuelto por el modelo
92
+ * @returns true si el texto parece contener CoT y debería ser saneado
93
+ */
94
+ function looksLikeChainOfThought(text) {
95
+ if (!text || typeof text !== 'string')
96
+ return false;
97
+ const trimmed = text.trim();
98
+ if (!trimmed)
99
+ return false;
100
+ // ⭐ Señales fuertes — cualquiera dispara
101
+ if (COT_HTML_TAG.test(trimmed))
102
+ return true;
103
+ if (COT_STRONG_TAG.test(trimmed))
104
+ return true;
105
+ const metaBullets = (trimmed.match(COT_META_BULLET) || []).length;
106
+ if (metaBullets >= 2)
107
+ return true;
108
+ // ⭐ Señales débiles — score acumulativo
109
+ let score = 0;
110
+ for (const re of COT_META_PATTERNS)
111
+ if (re.test(trimmed))
112
+ score++;
113
+ const asteriskRatio = (trimmed.match(/\*/g) || []).length / trimmed.length;
114
+ if (asteriskRatio > 0.02)
115
+ score++;
116
+ const bulletLines = trimmed.split('\n').filter((l) => /^\s*\*\s/.test(l)).length;
117
+ if (bulletLines >= 6)
118
+ score++;
119
+ const quoteCount = (trimmed.match(/"/g) || []).length;
120
+ if (quoteCount >= 8)
121
+ score++;
122
+ const englishHits = (trimmed.match(COT_ENGLISH_FUNCTION_WORDS) || []).length;
123
+ if (englishHits >= 6)
124
+ score += 2;
125
+ else if (englishHits >= 3)
126
+ score++;
127
+ return score >= COT_SCORE_THRESHOLD;
128
+ }
129
+ /**
130
+ * Refuerza el `system` original con instrucciones explícitas anti chain-of-thought
131
+ * para usar en el reintento cuando se detectó CoT en la primera respuesta.
132
+ */
133
+ function buildAntiCotSystem(originalSystem) {
134
+ const guard = '\n\nIMPORTANTE: Devuelve SOLO el mensaje final listo para enviar. No incluyas razonamiento, borradores, etiquetas tipo "thought" o "thinking", listas de intentos, autocríticas, conteo de palabras ni ningún meta-contenido. Responde siempre en español.';
135
+ return (originalSystem || '') + guard;
136
+ }
137
+ /**
138
+ * Sanitizer de último recurso: pide a un modelo LITE que extraiga del texto
139
+ * contaminado únicamente la respuesta final, sin razonamiento ni etiquetas.
140
+ *
141
+ * @param rawText - Texto contaminado con CoT
142
+ * @returns Texto limpio + cost de la llamada, o null si no se pudo sanear
143
+ */
144
+ function sanitizeWithLite(rawText) {
145
+ return __awaiter(this, void 0, void 0, function* () {
146
+ try {
147
+ const liteModel = (0, googleModel_1.createGoogleModel)('LITE');
148
+ const { text, usage } = yield (0, ai_1.generateText)({
149
+ model: liteModel,
150
+ system: `Eres un extractor de respuestas. Recibes un texto que contiene el razonamiento interno de otro modelo (borradores, autocríticas, intentos, conteos de palabras, etiquetas tipo "thought", etc.) seguido de la respuesta final que el modelo quería devolver al usuario.
151
+
152
+ Tu única tarea es devolver EXACTAMENTE esa respuesta final, sin razonamiento, sin etiquetas, sin comillas envolventes y sin meta-contenido.
153
+
154
+ Reglas:
155
+ 1. Devuelve solo texto plano en español, listo para enviar al usuario.
156
+ 2. NO añadas explicaciones, encabezados ni introducciones.
157
+ 3. NO inventes contenido: extrae lo que ya está.
158
+ 4. Si no puedes identificar una respuesta final clara, devuelve cadena vacía.`,
159
+ prompt: rawText,
160
+ });
161
+ const modelName = extractModelName(liteModel);
162
+ const cost = (0, modelPricing_1.calculateCost)(modelName, usage === null || usage === void 0 ? void 0 : usage.inputTokens, usage === null || usage === void 0 ? void 0 : usage.outputTokens);
163
+ return { text: text.trim(), cost };
164
+ }
165
+ catch (error) {
166
+ log(`[AI] sanitizeWithLite falló: ${error === null || error === void 0 ? void 0 : error.message}`);
167
+ return null;
168
+ }
169
+ });
170
+ }
171
+ /**
172
+ * Notifica a Slack cuando una respuesta contaminada con CoT no se ha podido sanear.
173
+ * No bloquea el flujo si el envío a Slack falla.
174
+ */
175
+ function notifyCotFailure(modelLabel, rawText) {
176
+ try {
177
+ const preview = rawText.slice(0, 1000);
178
+ (0, slack_1.notifySlack)({
179
+ text: `⚠️ *generateText: chain-of-thought no saneable*\n*Modelo:* ${modelLabel}\n*Preview (1000 chars):*\n\`\`\`${preview}\`\`\``,
180
+ channel: slack_1.CHANNEL_SLACK.BRUTAL,
181
+ });
182
+ }
183
+ catch (error) {
184
+ // Silenciar: la notificación es best-effort
185
+ }
186
+ }
66
187
  /**
67
188
  * Wrapper de generateObject con retry automático y cadena de fallbacks.
68
189
  * Calcula el coste de producción de la llamada y lo acumula automáticamente
@@ -133,12 +254,22 @@ function generateObject(options) {
133
254
  }
134
255
  exports.generateObject = generateObject;
135
256
  /**
136
- * Wrapper de generateText con retry automático y cadena de fallbacks.
137
- * Calcula el coste de producción de la llamada y lo acumula automáticamente
138
- * si se ejecuta dentro de runWithCostTracking.
257
+ * Wrapper de generateText con retry automático, cadena de fallbacks y saneamiento
258
+ * de chain-of-thought.
259
+ *
260
+ * Defensa en profundidad contra CoT que se cuela en la respuesta final del modelo:
261
+ * 1. Tras cada respuesta exitosa, valida con `looksLikeChainOfThought`.
262
+ * 2. Si detecta CoT → reintenta 1 vez con `system` reforzado y temperatura +0.1.
263
+ * 3. Si el retry sigue contaminado → llama a un modelo LITE para extraer la
264
+ * respuesta final del CoT (`sanitizeWithLite`).
265
+ * 4. Si nada limpia el output → notifica a Slack y lanza error para evitar enviar CoT.
266
+ *
267
+ * Los costes de retry y sanitizer se acumulan al cost original y se reportan a
268
+ * `runWithCostTracking`.
139
269
  *
140
270
  * @param options - Opciones para generateText (model, system, prompt, temperature, etc.)
141
- * @returns Texto generado y coste de la llamada
271
+ * @returns Texto generado limpio (sin CoT) y coste total de la llamada
272
+ * @throws Error si tras retry + sanitizer sigue habiendo CoT, para evitar que llegue al cliente.
142
273
  *
143
274
  * @example
144
275
  * ```typescript
@@ -162,10 +293,11 @@ function generateText(options) {
162
293
  const { text, usage } = yield (0, ai_1.generateText)(currentOptions);
163
294
  const modelName = extractModelName(currentOptions.model);
164
295
  const cost = (0, modelPricing_1.calculateCost)(modelName, usage === null || usage === void 0 ? void 0 : usage.inputTokens, usage === null || usage === void 0 ? void 0 : usage.outputTokens);
296
+ const cleanResult = yield ensureNoChainOfThought({ rawText: text, baseCost: cost, originalOptions: options, modelLabel: modelName });
165
297
  const tracker = costTrackingStorage.getStore();
166
298
  if (tracker)
167
- tracker.push(cost);
168
- return { text: text.trim(), cost };
299
+ tracker.push(cleanResult.cost);
300
+ return cleanResult;
169
301
  }
170
302
  catch (error) {
171
303
  lastError = error;
@@ -195,6 +327,62 @@ function generateText(options) {
195
327
  });
196
328
  }
197
329
  exports.generateText = generateText;
330
+ /**
331
+ * Garantiza que el texto devuelto no contenga chain-of-thought.
332
+ *
333
+ * Flujo (defensa en profundidad):
334
+ * 1. Si el texto está limpio → se devuelve tal cual.
335
+ * 2. Si tiene CoT → 1 retry con `system` reforzado y temperatura ligeramente superior.
336
+ * 3. Si el retry sigue contaminado → se delega a `sanitizeWithLite` para extraer la
337
+ * respuesta final del CoT con un modelo LITE.
338
+ * 4. Si todo falla → se notifica a Slack y se lanza error para evitar enviar CoT al cliente.
339
+ *
340
+ * Los costes de retry y sanitizer se acumulan al cost base para que `runWithCostTracking`
341
+ * los recoja correctamente.
342
+ */
343
+ function ensureNoChainOfThought(params) {
344
+ return __awaiter(this, void 0, void 0, function* () {
345
+ const { rawText, baseCost, originalOptions, modelLabel } = params;
346
+ const trimmed = (rawText || '').trim();
347
+ if (!looksLikeChainOfThought(trimmed)) {
348
+ return { text: trimmed, cost: baseCost };
349
+ }
350
+ log(`[AI] generateText — chain-of-thought detectado en ${modelLabel}, reintentando con system reforzado`);
351
+ const accumulatedCost = Object.assign({}, baseCost);
352
+ const baseTemperature = typeof originalOptions.temperature === 'number' ? originalOptions.temperature : 0.7;
353
+ const retryTemperature = Math.min(1, baseTemperature + 0.1);
354
+ // Paso 2: retry con system anti-CoT y temperatura ligeramente superior
355
+ try {
356
+ const { text: retryText, usage: retryUsage } = yield (0, ai_1.generateText)(Object.assign(Object.assign({}, originalOptions), { system: buildAntiCotSystem(originalOptions.system), temperature: retryTemperature }));
357
+ const retryModelLabel = extractModelName(originalOptions.model);
358
+ const retryCost = (0, modelPricing_1.calculateCost)(retryModelLabel, retryUsage === null || retryUsage === void 0 ? void 0 : retryUsage.inputTokens, retryUsage === null || retryUsage === void 0 ? void 0 : retryUsage.outputTokens);
359
+ accumulatedCost.inputTokens = (accumulatedCost.inputTokens || 0) + (retryCost.inputTokens || 0);
360
+ accumulatedCost.outputTokens = (accumulatedCost.outputTokens || 0) + (retryCost.outputTokens || 0);
361
+ accumulatedCost.cost = (accumulatedCost.cost || 0) + (retryCost.cost || 0);
362
+ const retryTrimmed = (retryText || '').trim();
363
+ if (retryTrimmed && !looksLikeChainOfThought(retryTrimmed)) {
364
+ return { text: retryTrimmed, cost: accumulatedCost };
365
+ }
366
+ log(`[AI] generateText — el retry en ${retryModelLabel} sigue contaminado, delegando a sanitizer LITE`);
367
+ }
368
+ catch (error) {
369
+ log(`[AI] generateText — retry anti-CoT falló en ${modelLabel}: ${error === null || error === void 0 ? void 0 : error.message}`);
370
+ }
371
+ // Paso 3: sanitizer LITE como último recurso
372
+ const sanitized = yield sanitizeWithLite(rawText);
373
+ if (sanitized) {
374
+ accumulatedCost.inputTokens = (accumulatedCost.inputTokens || 0) + (sanitized.cost.inputTokens || 0);
375
+ accumulatedCost.outputTokens = (accumulatedCost.outputTokens || 0) + (sanitized.cost.outputTokens || 0);
376
+ accumulatedCost.cost = (accumulatedCost.cost || 0) + (sanitized.cost.cost || 0);
377
+ if (sanitized.text && !looksLikeChainOfThought(sanitized.text)) {
378
+ return { text: sanitized.text, cost: accumulatedCost };
379
+ }
380
+ }
381
+ // Paso 4: imposible sanear — notificar y abortar
382
+ notifyCotFailure(modelLabel, rawText);
383
+ throw new Error(`[AI] generateText devolvió chain-of-thought no saneable en ${modelLabel}`);
384
+ });
385
+ }
198
386
  /**
199
387
  * Ejecuta una función async acumulando los costes de todas las llamadas a IA.
200
388
  * Al finalizar, devuelve el resultado original junto con el coste total agregado.
@@ -25,5 +25,6 @@ const CHANNEL_SLACK = {
25
25
  COACH_LOGS: 'T05JCPFMXE0/B0AEP9THSFJ/x1ea4vRfUN63NHOPvBvombQB',
26
26
  BRUTAL: 'T05JCPFMXE0/B05RDSHESGZ/lHGRox6UiOkioUMHQscap9pA',
27
27
  TEST: 'T05JCPFMXE0/B0601LMECSW/RsTOfjgMrxCPIvHAIttjwIPj',
28
+ SOPORTE: 'T05JCPFMXE0/B0AS6LGEWC9/ZnXA4AOgaJhZCjsvYi0JCny0',
28
29
  };
29
30
  exports.CHANNEL_SLACK = CHANNEL_SLACK;
@@ -27,12 +27,22 @@ declare function generateObject(options: Parameters<typeof originalGenerateObjec
27
27
  cost: CostResult;
28
28
  }>;
29
29
  /**
30
- * Wrapper de generateText con retry automático y cadena de fallbacks.
31
- * Calcula el coste de producción de la llamada y lo acumula automáticamente
32
- * si se ejecuta dentro de runWithCostTracking.
30
+ * Wrapper de generateText con retry automático, cadena de fallbacks y saneamiento
31
+ * de chain-of-thought.
32
+ *
33
+ * Defensa en profundidad contra CoT que se cuela en la respuesta final del modelo:
34
+ * 1. Tras cada respuesta exitosa, valida con `looksLikeChainOfThought`.
35
+ * 2. Si detecta CoT → reintenta 1 vez con `system` reforzado y temperatura +0.1.
36
+ * 3. Si el retry sigue contaminado → llama a un modelo LITE para extraer la
37
+ * respuesta final del CoT (`sanitizeWithLite`).
38
+ * 4. Si nada limpia el output → notifica a Slack y lanza error para evitar enviar CoT.
39
+ *
40
+ * Los costes de retry y sanitizer se acumulan al cost original y se reportan a
41
+ * `runWithCostTracking`.
33
42
  *
34
43
  * @param options - Opciones para generateText (model, system, prompt, temperature, etc.)
35
- * @returns Texto generado y coste de la llamada
44
+ * @returns Texto generado limpio (sin CoT) y coste total de la llamada
45
+ * @throws Error si tras retry + sanitizer sigue habiendo CoT, para evitar que llegue al cliente.
36
46
  *
37
47
  * @example
38
48
  * ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../../../src/prompt/ai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,IAAI,sBAAsB,EAAE,YAAY,IAAI,oBAAoB,EAAE,MAAM,IAAI,CAAC;AAEpG,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAsDhE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,iBAAe,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,CAyC/H;AAED;;;;;;;;;;;;;;;GAeG;AACH,iBAAe,YAAY,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,CAyC5H;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,iBAAe,mBAAmB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,SAAS,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAc5H;AAED,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC"}
1
+ {"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../../../src/prompt/ai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,IAAI,sBAAsB,EAAE,YAAY,IAAI,oBAAoB,EAAE,MAAM,IAAI,CAAC;AAEpG,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA+KhE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,iBAAe,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,CAyC/H;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,iBAAe,YAAY,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,CA0C5H;AA2ED;;;;;;;;;;;;;;;;;GAiBG;AACH,iBAAe,mBAAmB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,SAAS,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAc5H;AAED,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC"}
@@ -7,6 +7,7 @@ declare const CHANNEL_SLACK: {
7
7
  COACH_LOGS: string;
8
8
  BRUTAL: string;
9
9
  TEST: string;
10
+ SOPORTE: string;
10
11
  };
11
12
  export { notifySlack, CHANNEL_SLACK, };
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/slack/index.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,WAAW;;;UAYhB,CAAC;AAQF,QAAA,MAAM,aAAa;;;;;CAKlB,CAAC;AAEF,OAAO,EACL,WAAW,EACX,aAAa,GACd,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/slack/index.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,WAAW;;;UAYhB,CAAC;AAQF,QAAA,MAAM,aAAa;;;;;;CAMlB,CAAC;AAEF,OAAO,EACL,WAAW,EACX,aAAa,GACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runnerpro/backend",
3
- "version": "1.19.7",
3
+ "version": "1.19.9",
4
4
  "description": "A collection of common backend functions",
5
5
  "exports": {
6
6
  ".": "./lib/cjs/index.js"