@runnerpro/backend 1.19.8 → 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.
package/lib/cjs/prompt/ai.js
CHANGED
|
@@ -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
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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
|
|
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.
|
package/lib/cjs/slack/index.js
CHANGED
|
@@ -25,6 +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
|
+
SOPORTE: 'T05JCPFMXE0/B0AS6LGEWC9/ZnXA4AOgaJhZCjsvYi0JCny0',
|
|
29
29
|
};
|
|
30
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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;
|
|
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"}
|