cdp-edge 1.23.2 → 1.24.0
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/README.md +82 -21
- package/bin/cdp-edge.js +10 -1
- package/contracts/agent-versions.json +42 -41
- package/contracts/types.ts +81 -0
- package/dist/commands/install.js +6 -1
- package/dist/commands/server.js +4 -4
- package/docs/whatsapp-ctwa.md +3 -2
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +5 -4
- package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +0 -1
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +4 -4
- package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +81 -70
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +6 -2
- package/extracted-skill/tracking-events-generator/cdpTrack.js +7 -0
- package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
- package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
- package/package.json +9 -5
- package/server-edge-tracker/INSTALAR.md +5 -5
- package/server-edge-tracker/{index.js → index.ts} +186 -72
- package/server-edge-tracker/modules/{db.js → db.ts} +180 -69
- package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
- package/server-edge-tracker/modules/dispatch/meta.ts +138 -0
- package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
- package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
- package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +59 -25
- package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
- package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
- package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +49 -56
- package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
- package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +179 -83
- package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
- package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
- package/server-edge-tracker/modules/utils.ts +186 -0
- package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
- package/server-edge-tracker/types.ts +251 -0
- package/server-edge-tracker/wrangler.toml +24 -6
- package/templates/lancamento-imobiliario.md +344 -0
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
- package/server-edge-tracker/modules/dispatch/meta.js +0 -119
- package/server-edge-tracker/modules/ml/segmentation.js +0 -316
- package/server-edge-tracker/modules/utils.js +0 -89
- package/server-edge-tracker/worker.js +0 -4577
|
@@ -4,12 +4,73 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { extractFeatures, predictWithWeights, loadActiveWeights } from './logistic.js';
|
|
7
|
+
import { Env, TrackPayload } from '../../types.js';
|
|
8
|
+
|
|
9
|
+
// ── Tipos ───────────────────────────────────────────────────────────────────────
|
|
10
|
+
export interface LtvResult {
|
|
11
|
+
score: number;
|
|
12
|
+
class: string;
|
|
13
|
+
value: number;
|
|
14
|
+
source?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AbVariation {
|
|
18
|
+
id: number;
|
|
19
|
+
test_id: number;
|
|
20
|
+
name: string;
|
|
21
|
+
system_prompt: string;
|
|
22
|
+
weight: number;
|
|
23
|
+
is_control: number;
|
|
24
|
+
total_assigned?: number;
|
|
25
|
+
accuracy_score?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AbTestCreate {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
min_sample?: number;
|
|
32
|
+
variations: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
system_prompt?: string;
|
|
35
|
+
weight?: number;
|
|
36
|
+
is_control?: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AutoDecideResult {
|
|
41
|
+
decided: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
test_id?: number;
|
|
44
|
+
test_name?: string;
|
|
45
|
+
winner_id?: number;
|
|
46
|
+
winner_name?: string;
|
|
47
|
+
improvement?: string;
|
|
48
|
+
is_control_winner?: boolean;
|
|
49
|
+
winning_prompt?: string;
|
|
50
|
+
}
|
|
7
51
|
|
|
8
52
|
// Cache key para o teste ativo (KV — evita hit no D1 a cada request /track)
|
|
9
53
|
const AB_LTV_CACHE_KEY = 'ab_ltv_active_test';
|
|
10
54
|
|
|
55
|
+
// ── Prompt especializado para imóveis ───────────────────────────────────────
|
|
56
|
+
// Ativado automaticamente quando property_lat/lng estão presentes no payload.
|
|
57
|
+
// Override por A/B test tem prioridade sobre este prompt.
|
|
58
|
+
const REAL_ESTATE_PROMPT = `You are a real estate lead scoring expert for the Brazilian market.
|
|
59
|
+
Reply ONLY with a JSON object {"adjustment": <integer between -15 and 15>} based on the lead data.
|
|
60
|
+
Scoring rules (apply additively):
|
|
61
|
+
- distance_km < 5: +12 (lives nearby, buys fast)
|
|
62
|
+
- distance_km 5-15: +8
|
|
63
|
+
- distance_km 15-30: +3
|
|
64
|
+
- distance_km > 30: 0
|
|
65
|
+
- distance_km unknown: +3 (gave intent signal without geo)
|
|
66
|
+
- event = Schedule or route click: +5 (physical visit intent)
|
|
67
|
+
- scroll_score >= 3 AND time_level = comprador: +4 (deep engagement)
|
|
68
|
+
- hour_brt between 18-22 (weekday): +3 (active decision window)
|
|
69
|
+
- has_phone = true: +2 (reachable for follow-up)
|
|
70
|
+
No explanation. JSON only.`;
|
|
71
|
+
|
|
11
72
|
// ── predictLtv — Heurística em 5 dimensões (0-100 pts) ───────────────────────
|
|
12
|
-
export async function predictLtv(env, payload, request, customSystemPrompt = null) {
|
|
73
|
+
export async function predictLtv(env: Env, payload: TrackPayload, request: Request | null, customSystemPrompt: string | null = null): Promise<LtvResult> {
|
|
13
74
|
// ── Tentar modelo treinado (regressão logística real) ─────────────────────
|
|
14
75
|
// Se existir modelo ativo no KV/D1, usa-o em vez da heurística manual.
|
|
15
76
|
// Fallback automático para heurística se modelo não disponível.
|
|
@@ -17,10 +78,10 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
17
78
|
const model = await loadActiveWeights(env);
|
|
18
79
|
if (model?.weights?.length) {
|
|
19
80
|
const hour = new Date().getUTCHours();
|
|
20
|
-
const country = (payload.country || request?.cf?.country || '').toUpperCase();
|
|
81
|
+
const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
|
|
21
82
|
const features = extractFeatures({
|
|
22
83
|
utm_source: payload.utmSource,
|
|
23
|
-
engagement_score: parseFloat(payload.engagementScore || 0),
|
|
84
|
+
engagement_score: parseFloat(String(payload.engagementScore || '0')),
|
|
24
85
|
intention_level: payload.intentionLevel,
|
|
25
86
|
days_since_lead: 0, // evento atual = recência máxima
|
|
26
87
|
has_email: !!payload.email,
|
|
@@ -32,7 +93,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
32
93
|
const score100 = predictWithWeights(model, features);
|
|
33
94
|
const ltvClass = score100 >= 70 ? 'High' : score100 >= 40 ? 'Medium' : 'Low';
|
|
34
95
|
const ltvMultiplier = score100 >= 70 ? 3.5 : score100 >= 40 ? 1.8 : 0.8;
|
|
35
|
-
const productValue = payload.value ? parseFloat(payload.value) : 0;
|
|
96
|
+
const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
|
|
36
97
|
const baseValue = productValue > 0 ? productValue : 197;
|
|
37
98
|
const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
|
|
38
99
|
|
|
@@ -43,14 +104,14 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
43
104
|
let score = 0;
|
|
44
105
|
|
|
45
106
|
// 1. Engajamento browser (0–30)
|
|
46
|
-
const engScore = parseFloat(payload.engagementScore || 0);
|
|
47
|
-
const userScore = parseFloat(payload.userScore || 0);
|
|
107
|
+
const engScore = parseFloat(String(payload.engagementScore || '0'));
|
|
108
|
+
const userScore = parseFloat(String((payload as any).userScore || '0'));
|
|
48
109
|
score += Math.min(15, Math.round((engScore / 5) * 15));
|
|
49
110
|
score += Math.min(15, Math.round((userScore / 100) * 15));
|
|
50
111
|
|
|
51
112
|
// 2. Origem de tráfego (0–25)
|
|
52
113
|
const src = (payload.utmSource || '').toLowerCase();
|
|
53
|
-
const utm_score_map = {
|
|
114
|
+
const utm_score_map: Record<string, number> = {
|
|
54
115
|
facebook: 25, instagram: 25, meta: 25,
|
|
55
116
|
google: 22, youtube: 22, tiktok: 20,
|
|
56
117
|
email: 18, sms: 18,
|
|
@@ -60,8 +121,8 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
60
121
|
|
|
61
122
|
// 3. Contexto de rede (0–15)
|
|
62
123
|
const hour = new Date().getUTCHours();
|
|
63
|
-
const country = (payload.country || request
|
|
64
|
-
const org = String(request.cf?.asOrganization || '').toLowerCase();
|
|
124
|
+
const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
|
|
125
|
+
const org = String((request as any).cf?.asOrganization || '').toLowerCase();
|
|
65
126
|
|
|
66
127
|
const isHighConvTime = hour >= 21 || hour <= 2;
|
|
67
128
|
score += isHighConvTime ? 8 : (hour >= 12 && hour <= 20 ? 4 : 1);
|
|
@@ -83,9 +144,29 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
83
144
|
if (payload.phone) score += 4;
|
|
84
145
|
if (payload.firstName) score += 2;
|
|
85
146
|
|
|
147
|
+
// 5b. Tipo de evento imobiliário (0–15) — sinais de intenção de compra física
|
|
148
|
+
const evType = ((payload as any).eventType || '').toLowerCase();
|
|
149
|
+
if (evType === 'customizeproduct') score += 15; // simulação de financiamento → intenção máxima
|
|
150
|
+
else if (evType === 'findlocation') score += 10; // viu mapa/localização → visita física iminente
|
|
151
|
+
else if (evType === 'addtowishlist') score += 8; // favoritou → interesse persistente
|
|
152
|
+
|
|
153
|
+
// 6. Proximidade ao imóvel físico (0–15) — apenas quando distância calculada
|
|
154
|
+
const distKm = parseFloat(String(payload.distanceKm || (payload as any).user_distance_km || '-1'));
|
|
155
|
+
if (distKm >= 0) {
|
|
156
|
+
if (distKm < 5) score += 15;
|
|
157
|
+
else if (distKm < 15) score += 10;
|
|
158
|
+
else if (distKm < 30) score += 6;
|
|
159
|
+
else if (distKm < 60) score += 3;
|
|
160
|
+
// > 60km: sem bônus — lead distante precisa de argumento diferente
|
|
161
|
+
} else if (payload.property_lat || (payload as any).propertyLat) {
|
|
162
|
+
// Coords no payload mas distância não resolvida: pequeno bônus por intenção de rota
|
|
163
|
+
score += 3;
|
|
164
|
+
}
|
|
165
|
+
|
|
86
166
|
score = Math.min(100, score);
|
|
87
167
|
|
|
88
|
-
let ltvClass
|
|
168
|
+
let ltvClass: string;
|
|
169
|
+
let ltvMultiplier: number;
|
|
89
170
|
if (score >= 70) {
|
|
90
171
|
ltvClass = 'High'; ltvMultiplier = 3.5;
|
|
91
172
|
} else if (score >= 40) {
|
|
@@ -94,7 +175,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
94
175
|
ltvClass = 'Low'; ltvMultiplier = 0.8;
|
|
95
176
|
}
|
|
96
177
|
|
|
97
|
-
const productValue = payload.value ? parseFloat(payload.value) : 0;
|
|
178
|
+
const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
|
|
98
179
|
const baseValue = productValue > 0 ? productValue : 197;
|
|
99
180
|
const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
|
|
100
181
|
|
|
@@ -102,22 +183,37 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
102
183
|
let aiAdjustment = 0;
|
|
103
184
|
if (env.AI && score >= 40) {
|
|
104
185
|
try {
|
|
186
|
+
const isRealEstate = !!(payload.property_lat || (payload as any).propertyLat);
|
|
105
187
|
const systemContent = customSystemPrompt ||
|
|
106
|
-
|
|
188
|
+
(isRealEstate
|
|
189
|
+
? REAL_ESTATE_PROMPT
|
|
190
|
+
: 'You are a conversion rate expert. Reply ONLY with a JSON object {"adjustment": <number between -10 and 10>} based on the lead data provided. No explanation.');
|
|
191
|
+
|
|
192
|
+
const userContext: Record<string, any> = {
|
|
193
|
+
utm_source: payload.utmSource,
|
|
194
|
+
intention: intentionLevel,
|
|
195
|
+
engagement: engScore,
|
|
196
|
+
hour_utc: hour,
|
|
197
|
+
country,
|
|
198
|
+
has_email: !!payload.email,
|
|
199
|
+
has_phone: !!payload.phone,
|
|
200
|
+
};
|
|
201
|
+
if (isRealEstate) {
|
|
202
|
+
userContext.event_type = 'real_estate_schedule';
|
|
203
|
+
userContext.distance_km = payload.distanceKm || (payload as any).user_distance_km || 'unknown';
|
|
204
|
+
userContext.distance_bucket = payload.distanceBucket || 'unknown';
|
|
205
|
+
userContext.scroll_score = payload.scrollScore || (payload as any).scroll_score || 0;
|
|
206
|
+
userContext.time_level = payload.timeLevel || (payload as any).timeLevel || 'unknown';
|
|
207
|
+
userContext.intent_score = payload.intentScoreNum || payload.intent_score || 'high';
|
|
208
|
+
userContext.hour_brt = (hour - 3 + 24) % 24; // UTC-3 aproximado
|
|
209
|
+
}
|
|
210
|
+
|
|
107
211
|
const prompt = [
|
|
108
212
|
{ role: 'system', content: systemContent },
|
|
109
|
-
{ role: 'user', content: JSON.stringify(
|
|
110
|
-
utm_source: payload.utmSource,
|
|
111
|
-
intention: intentionLevel,
|
|
112
|
-
engagement: engScore,
|
|
113
|
-
hour_utc: hour,
|
|
114
|
-
country,
|
|
115
|
-
has_email: !!payload.email,
|
|
116
|
-
has_phone: !!payload.phone,
|
|
117
|
-
})},
|
|
213
|
+
{ role: 'user', content: JSON.stringify(userContext) },
|
|
118
214
|
];
|
|
119
|
-
const aiRes = await env.AI.run('@cf/
|
|
120
|
-
const parsed = JSON.parse(aiRes.response.trim());
|
|
215
|
+
const aiRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: prompt, max_tokens: 32 });
|
|
216
|
+
const parsed = JSON.parse((aiRes as any).response.trim());
|
|
121
217
|
if (typeof parsed.adjustment === 'number') {
|
|
122
218
|
aiAdjustment = Math.max(-10, Math.min(10, parsed.adjustment));
|
|
123
219
|
}
|
|
@@ -132,13 +228,13 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
132
228
|
}
|
|
133
229
|
|
|
134
230
|
// ── getLtvAbVariation — busca variação ativa do A/B test ─────────────────────
|
|
135
|
-
export async function getLtvAbVariation(env) {
|
|
231
|
+
export async function getLtvAbVariation(env: Env): Promise<AbVariation | null> {
|
|
136
232
|
if (!env.DB) return null;
|
|
137
233
|
|
|
138
234
|
try {
|
|
139
|
-
let testData = null;
|
|
235
|
+
let testData: any = null;
|
|
140
236
|
if (env.GEO_CACHE) {
|
|
141
|
-
const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json');
|
|
237
|
+
const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json') as any;
|
|
142
238
|
if (cached) testData = cached;
|
|
143
239
|
}
|
|
144
240
|
|
|
@@ -163,7 +259,7 @@ export async function getLtvAbVariation(env) {
|
|
|
163
259
|
|
|
164
260
|
if (!testData || testData.length === 0) return null;
|
|
165
261
|
|
|
166
|
-
const totalWeight = testData.reduce((s, v) => s + (v.weight || 0.5), 0);
|
|
262
|
+
const totalWeight = testData.reduce((s: number, v: AbVariation) => s + (v.weight || 0.5), 0);
|
|
167
263
|
let rand = Math.random() * totalWeight;
|
|
168
264
|
for (const variation of testData) {
|
|
169
265
|
rand -= (variation.weight || 0.5);
|
|
@@ -171,14 +267,14 @@ export async function getLtvAbVariation(env) {
|
|
|
171
267
|
}
|
|
172
268
|
return testData[testData.length - 1];
|
|
173
269
|
|
|
174
|
-
} catch (err) {
|
|
175
|
-
console.error('[AB-LTV] getLtvAbVariation error:', err
|
|
270
|
+
} catch (err: any) {
|
|
271
|
+
console.error('[AB-LTV] getLtvAbVariation error:', err?.message || String(err));
|
|
176
272
|
return null;
|
|
177
273
|
}
|
|
178
274
|
}
|
|
179
275
|
|
|
180
276
|
// ── recordAbAssignment — registra variação usada para um lead ─────────────────
|
|
181
|
-
export async function recordAbAssignment(env, userId, variationId, testId, predictedLtv, predictedClass, emailHash) {
|
|
277
|
+
export async function recordAbAssignment(env: Env, userId: string, variationId: number, testId: number, predictedLtv: number | null, predictedClass: string | null, emailHash: string | null): Promise<void> {
|
|
182
278
|
if (!env.DB) return;
|
|
183
279
|
try {
|
|
184
280
|
await env.DB.prepare(`
|
|
@@ -187,17 +283,17 @@ export async function recordAbAssignment(env, userId, variationId, testId, predi
|
|
|
187
283
|
`).bind(testId, variationId, userId, emailHash || null, predictedLtv || null, predictedClass || null).run();
|
|
188
284
|
|
|
189
285
|
await env.DB.prepare(`UPDATE ltv_ab_variations SET total_assigned = total_assigned + 1 WHERE id = ?`).bind(variationId).run();
|
|
190
|
-
} catch (err) {
|
|
191
|
-
console.error('[AB-LTV] recordAbAssignment error:', err
|
|
286
|
+
} catch (err: any) {
|
|
287
|
+
console.error('[AB-LTV] recordAbAssignment error:', err?.message || String(err));
|
|
192
288
|
}
|
|
193
289
|
}
|
|
194
290
|
|
|
195
291
|
// ── POST /api/ltv/ab-test/create ─────────────────────────────────────────────
|
|
196
|
-
export async function handleLtvAbTestCreate(env, request, headers) {
|
|
292
|
+
export async function handleLtvAbTestCreate(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
197
293
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
198
294
|
|
|
199
|
-
let body;
|
|
200
|
-
try { body = await request.json(); }
|
|
295
|
+
let body: AbTestCreate;
|
|
296
|
+
try { body = await request.json() as AbTestCreate; }
|
|
201
297
|
catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
|
|
202
298
|
|
|
203
299
|
const { name, description, min_sample = 100, variations } = body;
|
|
@@ -208,7 +304,7 @@ export async function handleLtvAbTestCreate(env, request, headers) {
|
|
|
208
304
|
|
|
209
305
|
const running = await env.DB.prepare(`SELECT id FROM ltv_ab_tests WHERE status = 'running' LIMIT 1`).first();
|
|
210
306
|
if (running) {
|
|
211
|
-
return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: running.id }), { status: 409, headers });
|
|
307
|
+
return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: (running as any).id }), { status: 409, headers });
|
|
212
308
|
}
|
|
213
309
|
|
|
214
310
|
try {
|
|
@@ -225,28 +321,28 @@ export async function handleLtvAbTestCreate(env, request, headers) {
|
|
|
225
321
|
INSERT INTO ltv_ab_tests (name, description, status, min_sample, created_at) VALUES (?, ?, 'running', ?, ?)
|
|
226
322
|
`).bind(name, description || null, min_sample, now).run();
|
|
227
323
|
|
|
228
|
-
const testId = testRes.meta?.last_row_id;
|
|
324
|
+
const testId = (testRes as any).meta?.last_row_id;
|
|
229
325
|
if (!testId) throw new Error('Falha ao criar o teste no D1');
|
|
230
326
|
|
|
231
|
-
const createdVariations = [];
|
|
327
|
+
const createdVariations: Array<{ id: number; name: string; weight: number; is_control: boolean }> = [];
|
|
232
328
|
for (const v of variations) {
|
|
233
329
|
const vRes = await env.DB.prepare(`
|
|
234
330
|
INSERT INTO ltv_ab_variations (test_id, name, system_prompt, weight, is_control, created_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
235
331
|
`).bind(testId, v.name, v.system_prompt, v.weight || 0.5, v.is_control ? 1 : 0, now).run();
|
|
236
|
-
createdVariations.push({ id: vRes.meta?.last_row_id, name: v.name, weight: v.weight, is_control: !!v.is_control });
|
|
332
|
+
createdVariations.push({ id: (vRes as any).meta?.last_row_id, name: v.name, weight: v.weight || 0.5, is_control: !!v.is_control });
|
|
237
333
|
}
|
|
238
334
|
|
|
239
335
|
if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
|
|
240
336
|
|
|
241
337
|
return new Response(JSON.stringify({ success: true, test_id: testId, name, status: 'running', min_sample, variations: createdVariations, started_at: now }), { status: 201, headers });
|
|
242
|
-
} catch (err) {
|
|
243
|
-
console.error('[AB-LTV] create error:', err
|
|
244
|
-
return new Response(JSON.stringify({ error: err
|
|
338
|
+
} catch (err: any) {
|
|
339
|
+
console.error('[AB-LTV] create error:', err?.message || String(err));
|
|
340
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
245
341
|
}
|
|
246
342
|
}
|
|
247
343
|
|
|
248
344
|
// ── GET /api/ltv/ab-test/list ─────────────────────────────────────────────────
|
|
249
|
-
export async function handleLtvAbTestList(env, request, headers) {
|
|
345
|
+
export async function handleLtvAbTestList(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
250
346
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
251
347
|
|
|
252
348
|
const url = new URL(request.url);
|
|
@@ -255,7 +351,7 @@ export async function handleLtvAbTestList(env, request, headers) {
|
|
|
255
351
|
|
|
256
352
|
try {
|
|
257
353
|
const cond = status ? 'WHERE t.status = ?' : '';
|
|
258
|
-
const bindings = status ? [status, limit] : [limit];
|
|
354
|
+
const bindings: (string | number)[] = status ? [status, limit] : [limit];
|
|
259
355
|
|
|
260
356
|
const tests = await env.DB.prepare(`
|
|
261
357
|
SELECT t.id, t.name, t.description, t.status, t.winner_id,
|
|
@@ -271,14 +367,14 @@ export async function handleLtvAbTestList(env, request, headers) {
|
|
|
271
367
|
`).bind(...bindings).all();
|
|
272
368
|
|
|
273
369
|
return new Response(JSON.stringify({ success: true, total: (tests.results || []).length, tests: tests.results || [] }), { status: 200, headers });
|
|
274
|
-
} catch (err) {
|
|
275
|
-
console.error('[AB-LTV] list error:', err
|
|
276
|
-
return new Response(JSON.stringify({ error: err
|
|
370
|
+
} catch (err: any) {
|
|
371
|
+
console.error('[AB-LTV] list error:', err?.message || String(err));
|
|
372
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
277
373
|
}
|
|
278
374
|
}
|
|
279
375
|
|
|
280
376
|
// ── GET /api/ltv/ab-test/results ─────────────────────────────────────────────
|
|
281
|
-
export async function handleLtvAbTestResults(env, request, headers) {
|
|
377
|
+
export async function handleLtvAbTestResults(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
282
378
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
283
379
|
|
|
284
380
|
const url = new URL(request.url);
|
|
@@ -291,15 +387,15 @@ export async function handleLtvAbTestResults(env, request, headers) {
|
|
|
291
387
|
const testRes = await env.DB.prepare(`SELECT id, name, status, min_sample, winner_id, started_at FROM ltv_ab_tests ${cond} LIMIT 1`).bind(...testBind).first();
|
|
292
388
|
if (!testRes) return new Response(JSON.stringify({ error: 'Nenhum teste encontrado' }), { status: 404, headers });
|
|
293
389
|
|
|
294
|
-
const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind(testRes.id).all();
|
|
390
|
+
const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind((testRes as any).id).all();
|
|
295
391
|
const variations = perf.results || [];
|
|
296
|
-
const ready = variations.every(v => (v.total_assigned || 0) >= testRes.min_sample);
|
|
297
|
-
let recommendation = null;
|
|
392
|
+
const ready = variations.every((v: any) => (v.total_assigned || 0) >= (testRes as any).min_sample);
|
|
393
|
+
let recommendation: any = null;
|
|
298
394
|
|
|
299
395
|
if (ready && variations.length > 0) {
|
|
300
|
-
const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
|
|
301
|
-
const control = variations.find(v => v.is_control);
|
|
302
|
-
const improvement = control ? ((best.accuracy_score || 0) - (control.accuracy_score || 0)) * 100 : null;
|
|
396
|
+
const best = variations.reduce((a: any, b: any) => (Number(b.accuracy_score) || 0) > (Number(a.accuracy_score) || 0) ? b : a);
|
|
397
|
+
const control = variations.find((v: any) => v.is_control);
|
|
398
|
+
const improvement = control ? ((Number(best.accuracy_score) || 0) - (Number(control.accuracy_score) || 0)) * 100 : null;
|
|
303
399
|
recommendation = {
|
|
304
400
|
winner_variation_id: best.variation_id, winner_variation_name: best.variation_name,
|
|
305
401
|
accuracy_score: best.accuracy_score, improvement_vs_control: improvement ? `+${improvement.toFixed(1)}%` : null,
|
|
@@ -309,20 +405,20 @@ export async function handleLtvAbTestResults(env, request, headers) {
|
|
|
309
405
|
|
|
310
406
|
return new Response(JSON.stringify({
|
|
311
407
|
success: true,
|
|
312
|
-
test: { id: testRes.id, name: testRes.name, status: testRes.status, min_sample: testRes.min_sample, started_at: testRes.started_at, is_ready: ready },
|
|
408
|
+
test: { id: (testRes as any).id, name: (testRes as any).name, status: (testRes as any).status, min_sample: (testRes as any).min_sample, started_at: (testRes as any).started_at, is_ready: ready },
|
|
313
409
|
variations, recommendation,
|
|
314
410
|
}), { status: 200, headers });
|
|
315
|
-
} catch (err) {
|
|
316
|
-
console.error('[AB-LTV] results error:', err
|
|
317
|
-
return new Response(JSON.stringify({ error: err
|
|
411
|
+
} catch (err: any) {
|
|
412
|
+
console.error('[AB-LTV] results error:', err?.message || String(err));
|
|
413
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
318
414
|
}
|
|
319
415
|
}
|
|
320
416
|
|
|
321
417
|
// ── POST /api/ltv/ab-test/winner ──────────────────────────────────────────────
|
|
322
|
-
export async function handleLtvAbTestWinner(env, request, headers) {
|
|
418
|
+
export async function handleLtvAbTestWinner(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
323
419
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
324
420
|
|
|
325
|
-
let body;
|
|
421
|
+
let body: any;
|
|
326
422
|
try { body = await request.json(); }
|
|
327
423
|
catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
|
|
328
424
|
|
|
@@ -339,22 +435,22 @@ export async function handleLtvAbTestWinner(env, request, headers) {
|
|
|
339
435
|
if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
|
|
340
436
|
|
|
341
437
|
return new Response(JSON.stringify({
|
|
342
|
-
success: true, test_id, winner_variation_id: variation_id, winner_name: variation.name,
|
|
343
|
-
is_control: variation.is_control === 1, winning_prompt: variation.system_prompt,
|
|
344
|
-
message: variation.is_control === 1
|
|
438
|
+
success: true, test_id, winner_variation_id: variation_id, winner_name: (variation as any).name,
|
|
439
|
+
is_control: (variation as any).is_control === 1, winning_prompt: (variation as any).system_prompt,
|
|
440
|
+
message: (variation as any).is_control === 1
|
|
345
441
|
? 'O prompt original (controle) venceu. Nenhuma alteração necessária.'
|
|
346
442
|
: 'Novo prompt vencedor identificado. Copie o campo winning_prompt e aplique ao predictLtv() como novo default.',
|
|
347
443
|
}), { status: 200, headers });
|
|
348
|
-
} catch (err) {
|
|
349
|
-
console.error('[AB-LTV] winner error:', err
|
|
350
|
-
return new Response(JSON.stringify({ error: err
|
|
444
|
+
} catch (err: any) {
|
|
445
|
+
console.error('[AB-LTV] winner error:', err?.message || String(err));
|
|
446
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
351
447
|
}
|
|
352
448
|
}
|
|
353
449
|
|
|
354
450
|
// ── autoDecideAbWinner — declara winner automaticamente via cron ──────────────
|
|
355
451
|
// Critério: todas as variações com amostra >= min_sample
|
|
356
452
|
// E diferença de accuracy_score >= 5pp entre melhor e controle
|
|
357
|
-
export async function autoDecideAbWinner(env) {
|
|
453
|
+
export async function autoDecideAbWinner(env: Env): Promise<AutoDecideResult> {
|
|
358
454
|
if (!env.DB) return { decided: false, reason: 'no_db' };
|
|
359
455
|
|
|
360
456
|
try {
|
|
@@ -368,24 +464,24 @@ export async function autoDecideAbWinner(env) {
|
|
|
368
464
|
// Buscar performance das variações
|
|
369
465
|
const perf = await env.DB.prepare(
|
|
370
466
|
`SELECT * FROM v_ab_test_performance WHERE test_id = ?`
|
|
371
|
-
).bind(test.id).all();
|
|
467
|
+
).bind((test as any).id).all();
|
|
372
468
|
|
|
373
469
|
const variations = perf.results || [];
|
|
374
470
|
if (variations.length < 2) return { decided: false, reason: 'insufficient_variations' };
|
|
375
471
|
|
|
376
472
|
// Verificar se todas têm amostra suficiente
|
|
377
|
-
const allReady = variations.every(v => (v.total_assigned || 0) >= test.min_sample);
|
|
473
|
+
const allReady = variations.every((v: any) => (v.total_assigned || 0) >= (test as any).min_sample);
|
|
378
474
|
if (!allReady) {
|
|
379
|
-
const minAssigned = Math.min(...variations.map(v => v.total_assigned || 0));
|
|
380
|
-
return { decided: false, reason: `sample_insufficient (${minAssigned}/${test.min_sample})` };
|
|
475
|
+
const minAssigned = Math.min(...variations.map((v: any) => v.total_assigned || 0));
|
|
476
|
+
return { decided: false, reason: `sample_insufficient (${minAssigned}/${(test as any).min_sample})` };
|
|
381
477
|
}
|
|
382
478
|
|
|
383
479
|
// Encontrar melhor e controle
|
|
384
|
-
const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
|
|
385
|
-
const control = variations.find(v => v.is_control) || variations[0];
|
|
480
|
+
const best = variations.reduce((a: any, b: any) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
|
|
481
|
+
const control = variations.find((v: any) => v.is_control) || variations[0];
|
|
386
482
|
|
|
387
|
-
const bestScore = parseFloat(best.accuracy_score || 0);
|
|
388
|
-
const controlScore = parseFloat(control.accuracy_score || 0);
|
|
483
|
+
const bestScore = parseFloat(String(best.accuracy_score) || '0');
|
|
484
|
+
const controlScore = parseFloat(String(control.accuracy_score) || '0');
|
|
389
485
|
const diff = bestScore - controlScore;
|
|
390
486
|
|
|
391
487
|
// Empate técnico → controle vence (determinístico)
|
|
@@ -404,17 +500,17 @@ export async function autoDecideAbWinner(env) {
|
|
|
404
500
|
|
|
405
501
|
return {
|
|
406
502
|
decided: true,
|
|
407
|
-
test_id: test.id,
|
|
408
|
-
test_name: test.name,
|
|
409
|
-
winner_id: best.variation_id,
|
|
410
|
-
winner_name: best.variation_name,
|
|
503
|
+
test_id: (test as any).id,
|
|
504
|
+
test_name: (test as any).name,
|
|
505
|
+
winner_id: typeof best.variation_id === 'number' ? best.variation_id : undefined,
|
|
506
|
+
winner_name: typeof best.variation_name === 'string' ? best.variation_name : undefined,
|
|
411
507
|
improvement: `+${(diff * 100).toFixed(1)}pp`,
|
|
412
508
|
is_control_winner: best.variation_id === control.variation_id,
|
|
413
|
-
winning_prompt: best.system_prompt ||
|
|
509
|
+
winning_prompt: String(best.system_prompt || ''),
|
|
414
510
|
};
|
|
415
511
|
|
|
416
|
-
} catch (err) {
|
|
417
|
-
console.error('[AB-LTV] autoDecide error:', err
|
|
418
|
-
return { decided: false, reason: err
|
|
512
|
+
} catch (err: any) {
|
|
513
|
+
console.error('[AB-LTV] autoDecide error:', err?.message || String(err));
|
|
514
|
+
return { decided: false, reason: err?.message || String(err) };
|
|
419
515
|
}
|
|
420
516
|
}
|