@littlebearapps/create-platform 1.0.0 → 1.1.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 +98 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +36 -6
- package/dist/prompts.d.ts +14 -2
- package/dist/prompts.js +29 -7
- package/dist/templates.js +78 -0
- package/package.json +3 -2
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Prompt for Pattern Suggestion
|
|
3
|
+
*
|
|
4
|
+
* Uses DeepSeek via AI Gateway to analyse error clusters
|
|
5
|
+
* and suggest transient error patterns.
|
|
6
|
+
*
|
|
7
|
+
* @module workers/lib/pattern-discovery/ai-prompt
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ErrorCluster, AISuggestionResponse, PatternType } from './types';
|
|
11
|
+
import type { Logger } from '@littlebearapps/platform-sdk';
|
|
12
|
+
import type { AggregatedPatternEvidence } from './storage';
|
|
13
|
+
|
|
14
|
+
/** DeepSeek AI Gateway URL */
|
|
15
|
+
const DEEPSEEK_GATEWAY_URL = 'https://gateway.ai.cloudflare.com/v1';
|
|
16
|
+
|
|
17
|
+
/** Maximum tokens for AI response */
|
|
18
|
+
const MAX_TOKENS = 1500;
|
|
19
|
+
|
|
20
|
+
/** Timeout for AI API calls (25s — well within Worker's 30s scheduled limit) */
|
|
21
|
+
const AI_FETCH_TIMEOUT_MS = 25_000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the prompt for pattern suggestion
|
|
25
|
+
*/
|
|
26
|
+
function buildPrompt(clusters: ErrorCluster[], sampleMessages: Map<string, string[]>): string {
|
|
27
|
+
const clusterDescriptions = clusters
|
|
28
|
+
.map((cluster, i) => {
|
|
29
|
+
const samples = sampleMessages.get(cluster.id) || [cluster.representativeMessage];
|
|
30
|
+
return `
|
|
31
|
+
### Cluster ${i + 1}
|
|
32
|
+
- **Occurrences**: ${cluster.occurrenceCount}
|
|
33
|
+
- **Unique fingerprints**: ${cluster.uniqueFingerprints}
|
|
34
|
+
- **Scripts**: ${cluster.scripts.join(', ')}
|
|
35
|
+
- **Sample messages**:
|
|
36
|
+
${samples.map((s, j) => ` ${j + 1}. "${s}"`).join('\n')}
|
|
37
|
+
`;
|
|
38
|
+
})
|
|
39
|
+
.join('\n');
|
|
40
|
+
|
|
41
|
+
return `You are analysing error messages from Cloudflare Workers to identify TRANSIENT errors.
|
|
42
|
+
|
|
43
|
+
## Definition of Transient Errors
|
|
44
|
+
Transient errors are expected operational issues that:
|
|
45
|
+
- Self-resolve over time (quota resets, rate limits lift, services recover)
|
|
46
|
+
- Are caused by external factors (API limits, network issues, deployments)
|
|
47
|
+
- Should NOT create duplicate GitHub issues
|
|
48
|
+
|
|
49
|
+
## Common Transient Categories
|
|
50
|
+
- \`quota-exhausted\`: API quotas, daily limits
|
|
51
|
+
- \`rate-limited\`: Rate limiting, 429 errors
|
|
52
|
+
- \`timeout\`: Request/connection timeouts
|
|
53
|
+
- \`service-unavailable\`: 502/503 errors
|
|
54
|
+
- \`connection-error\`: ECONNREFUSED, ETIMEDOUT, ECONNRESET
|
|
55
|
+
- \`deployment-related\`: Durable Object resets, code updates
|
|
56
|
+
|
|
57
|
+
## Error Clusters to Analyse
|
|
58
|
+
${clusterDescriptions}
|
|
59
|
+
|
|
60
|
+
## Your Task
|
|
61
|
+
For each cluster that represents a TRANSIENT error, suggest a pattern to match it.
|
|
62
|
+
|
|
63
|
+
**IMPORTANT**: Use the safest pattern type possible:
|
|
64
|
+
1. \`contains\` - Match if message contains specific tokens (SAFEST)
|
|
65
|
+
2. \`startsWith\` - Match if message starts with prefix
|
|
66
|
+
3. \`statusCode\` - Match HTTP status codes (e.g., "429", "503")
|
|
67
|
+
4. \`regex\` - Only if the above won't work (AVOID if possible)
|
|
68
|
+
|
|
69
|
+
If a cluster is NOT transient (actual bugs, logic errors), mark confidence as 0.
|
|
70
|
+
|
|
71
|
+
## Response Format (JSON only)
|
|
72
|
+
{
|
|
73
|
+
"patterns": [
|
|
74
|
+
{
|
|
75
|
+
"patternType": "contains",
|
|
76
|
+
"patternValue": "quota exceeded",
|
|
77
|
+
"category": "quota-exhausted",
|
|
78
|
+
"confidence": 0.9,
|
|
79
|
+
"reasoning": "Error mentions quota exceeded, typical API rate limit",
|
|
80
|
+
"positiveExamples": ["quota exceeded for API", "daily quota exceeded"],
|
|
81
|
+
"negativeExamples": ["quota configuration error"]
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"summary": "Found 2 transient patterns (quota, rate-limit), 1 cluster appears to be a real bug"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
IMPORTANT: Your response must be valid JSON. Do not include any text outside the JSON object.`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate AI response matches expected schema
|
|
92
|
+
*/
|
|
93
|
+
function validateResponse(data: unknown): AISuggestionResponse | null {
|
|
94
|
+
if (!data || typeof data !== 'object') return null;
|
|
95
|
+
|
|
96
|
+
const obj = data as Record<string, unknown>;
|
|
97
|
+
if (!Array.isArray(obj.patterns)) return null;
|
|
98
|
+
if (typeof obj.summary !== 'string') return null;
|
|
99
|
+
|
|
100
|
+
const validTypes: PatternType[] = ['contains', 'startsWith', 'statusCode', 'regex'];
|
|
101
|
+
|
|
102
|
+
for (const pattern of obj.patterns) {
|
|
103
|
+
if (!pattern || typeof pattern !== 'object') return null;
|
|
104
|
+
const p = pattern as Record<string, unknown>;
|
|
105
|
+
|
|
106
|
+
if (!validTypes.includes(p.patternType as PatternType)) return null;
|
|
107
|
+
if (typeof p.patternValue !== 'string') return null;
|
|
108
|
+
if (typeof p.category !== 'string') return null;
|
|
109
|
+
if (typeof p.confidence !== 'number') return null;
|
|
110
|
+
if (typeof p.reasoning !== 'string') return null;
|
|
111
|
+
if (!Array.isArray(p.positiveExamples)) return null;
|
|
112
|
+
if (!Array.isArray(p.negativeExamples)) return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return obj as unknown as AISuggestionResponse;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Call DeepSeek to analyse clusters and suggest patterns
|
|
120
|
+
*/
|
|
121
|
+
/** Static pattern evaluation request */
|
|
122
|
+
export interface StaticPatternInput {
|
|
123
|
+
pattern: string; // The regex pattern as a string
|
|
124
|
+
category: string;
|
|
125
|
+
index: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** AI evaluation response for static patterns */
|
|
129
|
+
export interface StaticPatternEvaluation {
|
|
130
|
+
evaluations: Array<{
|
|
131
|
+
index: number;
|
|
132
|
+
category: string;
|
|
133
|
+
verdict: 'keep-static' | 'migrate-dynamic' | 'merge' | 'deprecate';
|
|
134
|
+
convertedType?: PatternType;
|
|
135
|
+
convertedValue?: string;
|
|
136
|
+
reasoning: string;
|
|
137
|
+
confidenceScore: number;
|
|
138
|
+
}>;
|
|
139
|
+
summary: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build prompt for evaluating static patterns
|
|
144
|
+
*/
|
|
145
|
+
function buildStaticEvaluationPrompt(patterns: StaticPatternInput[]): string {
|
|
146
|
+
const patternList = patterns
|
|
147
|
+
.map(
|
|
148
|
+
(p) => `${p.index}. Category: "${p.category}"
|
|
149
|
+
Regex: \`${p.pattern}\``
|
|
150
|
+
)
|
|
151
|
+
.join('\n\n');
|
|
152
|
+
|
|
153
|
+
return `You are evaluating HARDCODED transient error patterns to determine if they should be migrated to a dynamic pattern system.
|
|
154
|
+
|
|
155
|
+
## Current Static Patterns
|
|
156
|
+
These patterns are compiled into production code. They detect transient errors (quota, rate limits, timeouts, etc.) that should NOT create duplicate GitHub issues.
|
|
157
|
+
|
|
158
|
+
${patternList}
|
|
159
|
+
|
|
160
|
+
## Your Task
|
|
161
|
+
For each pattern, evaluate whether it should:
|
|
162
|
+
1. **keep-static** - Keep as hardcoded (core infrastructure patterns that rarely change)
|
|
163
|
+
2. **migrate-dynamic** - Convert to dynamic DSL for better visibility/management
|
|
164
|
+
3. **merge** - Can be merged with another pattern
|
|
165
|
+
4. **deprecate** - Pattern is too broad, outdated, or problematic
|
|
166
|
+
|
|
167
|
+
If recommending migration, convert the regex to our safer DSL:
|
|
168
|
+
- \`contains\` - Match if message contains tokens (PREFERRED)
|
|
169
|
+
- \`startsWith\` - Match if message starts with prefix
|
|
170
|
+
- \`statusCode\` - Match HTTP status codes
|
|
171
|
+
- \`regex\` - Only if truly necessary
|
|
172
|
+
|
|
173
|
+
## Response Format (JSON only)
|
|
174
|
+
{
|
|
175
|
+
"evaluations": [
|
|
176
|
+
{
|
|
177
|
+
"index": 1,
|
|
178
|
+
"category": "quota-exhausted",
|
|
179
|
+
"verdict": "migrate-dynamic",
|
|
180
|
+
"convertedType": "contains",
|
|
181
|
+
"convertedValue": "quota exceeded",
|
|
182
|
+
"reasoning": "Can be expressed safely with contains, benefits from visibility in dashboard",
|
|
183
|
+
"confidenceScore": 0.9
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"index": 2,
|
|
187
|
+
"category": "rate-limited",
|
|
188
|
+
"verdict": "keep-static",
|
|
189
|
+
"reasoning": "Core infrastructure pattern, simple regex, low maintenance risk",
|
|
190
|
+
"confidenceScore": 0.85
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
"summary": "Recommend migrating 5 patterns, keeping 10 static, merging 2, deprecating 1"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
IMPORTANT: Respond with valid JSON only. No text outside the JSON object.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate static evaluation response
|
|
201
|
+
*/
|
|
202
|
+
function validateStaticEvaluationResponse(data: unknown): StaticPatternEvaluation | null {
|
|
203
|
+
if (!data || typeof data !== 'object') return null;
|
|
204
|
+
|
|
205
|
+
const obj = data as Record<string, unknown>;
|
|
206
|
+
if (!Array.isArray(obj.evaluations)) return null;
|
|
207
|
+
if (typeof obj.summary !== 'string') return null;
|
|
208
|
+
|
|
209
|
+
const validVerdicts = ['keep-static', 'migrate-dynamic', 'merge', 'deprecate'];
|
|
210
|
+
const validTypes: PatternType[] = ['contains', 'startsWith', 'statusCode', 'regex'];
|
|
211
|
+
|
|
212
|
+
for (const evaluation of obj.evaluations) {
|
|
213
|
+
if (!evaluation || typeof evaluation !== 'object') return null;
|
|
214
|
+
const e = evaluation as Record<string, unknown>;
|
|
215
|
+
|
|
216
|
+
if (typeof e.index !== 'number') return null;
|
|
217
|
+
if (typeof e.category !== 'string') return null;
|
|
218
|
+
if (!validVerdicts.includes(e.verdict as string)) return null;
|
|
219
|
+
if (typeof e.reasoning !== 'string') return null;
|
|
220
|
+
if (typeof e.confidenceScore !== 'number') return null;
|
|
221
|
+
|
|
222
|
+
// If migrating, must have converted type/value
|
|
223
|
+
if (e.verdict === 'migrate-dynamic') {
|
|
224
|
+
if (!validTypes.includes(e.convertedType as PatternType)) return null;
|
|
225
|
+
if (typeof e.convertedValue !== 'string') return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return obj as unknown as StaticPatternEvaluation;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Call DeepSeek to evaluate static patterns for potential migration
|
|
234
|
+
*/
|
|
235
|
+
export async function evaluateStaticPatterns(
|
|
236
|
+
patterns: StaticPatternInput[],
|
|
237
|
+
env: { CLOUDFLARE_ACCOUNT_ID: string; PLATFORM_AI_GATEWAY_KEY: string },
|
|
238
|
+
log: Logger
|
|
239
|
+
): Promise<StaticPatternEvaluation | null> {
|
|
240
|
+
if (patterns.length === 0) {
|
|
241
|
+
log.info('No patterns to evaluate');
|
|
242
|
+
return { evaluations: [], summary: 'No patterns provided' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const prompt = buildStaticEvaluationPrompt(patterns);
|
|
246
|
+
|
|
247
|
+
const controller = new AbortController();
|
|
248
|
+
const timeoutId = setTimeout(() => controller.abort(), AI_FETCH_TIMEOUT_MS);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(
|
|
252
|
+
`${DEEPSEEK_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/deepseek/chat/completions`,
|
|
253
|
+
{
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {
|
|
256
|
+
'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
},
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
model: 'deepseek-chat',
|
|
261
|
+
messages: [
|
|
262
|
+
{
|
|
263
|
+
role: 'system',
|
|
264
|
+
content:
|
|
265
|
+
'You are an expert at evaluating error patterns for production systems. Respond with valid JSON only.',
|
|
266
|
+
},
|
|
267
|
+
{ role: 'user', content: prompt },
|
|
268
|
+
],
|
|
269
|
+
temperature: 0.1,
|
|
270
|
+
max_tokens: 3000, // Larger for evaluating many patterns
|
|
271
|
+
response_format: { type: 'json_object' },
|
|
272
|
+
}),
|
|
273
|
+
signal: controller.signal,
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
const errorBody = await response.text().catch(() => 'Unknown error');
|
|
279
|
+
log.error('DeepSeek API error', new Error(`HTTP ${response.status}`), {
|
|
280
|
+
status: response.status,
|
|
281
|
+
errorBody: errorBody.slice(0, 500),
|
|
282
|
+
});
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const data = (await response.json()) as {
|
|
287
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const content = data.choices?.[0]?.message?.content;
|
|
291
|
+
if (!content) {
|
|
292
|
+
log.error('Empty response from DeepSeek');
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Parse JSON response
|
|
297
|
+
let parsed: unknown;
|
|
298
|
+
try {
|
|
299
|
+
parsed = JSON.parse(content);
|
|
300
|
+
} catch {
|
|
301
|
+
log.error('Invalid JSON from DeepSeek', new Error('Parse failed'), {
|
|
302
|
+
content: content.slice(0, 500),
|
|
303
|
+
});
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Validate response structure
|
|
308
|
+
const validated = validateStaticEvaluationResponse(parsed);
|
|
309
|
+
if (!validated) {
|
|
310
|
+
log.error('Response failed validation', new Error('Schema mismatch'), {
|
|
311
|
+
parsed: JSON.stringify(parsed).slice(0, 500),
|
|
312
|
+
});
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
log.info('Static pattern evaluation complete', {
|
|
317
|
+
patternsEvaluated: validated.evaluations.length,
|
|
318
|
+
summary: validated.summary.slice(0, 100),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return validated;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
log.error('DeepSeek request failed', error);
|
|
324
|
+
return null;
|
|
325
|
+
} finally {
|
|
326
|
+
clearTimeout(timeoutId);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Call DeepSeek to analyse clusters and suggest patterns
|
|
332
|
+
*/
|
|
333
|
+
export async function suggestPatterns(
|
|
334
|
+
clusters: ErrorCluster[],
|
|
335
|
+
sampleMessages: Map<string, string[]>,
|
|
336
|
+
env: { CLOUDFLARE_ACCOUNT_ID: string; PLATFORM_AI_GATEWAY_KEY: string },
|
|
337
|
+
log: Logger
|
|
338
|
+
): Promise<AISuggestionResponse | null> {
|
|
339
|
+
if (clusters.length === 0) {
|
|
340
|
+
log.info('No clusters to analyse');
|
|
341
|
+
return { patterns: [], summary: 'No clusters provided' };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const prompt = buildPrompt(clusters, sampleMessages);
|
|
345
|
+
|
|
346
|
+
const controller = new AbortController();
|
|
347
|
+
const timeoutId = setTimeout(() => controller.abort(), AI_FETCH_TIMEOUT_MS);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const response = await fetch(
|
|
351
|
+
`${DEEPSEEK_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/deepseek/chat/completions`,
|
|
352
|
+
{
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: {
|
|
355
|
+
'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify({
|
|
359
|
+
model: 'deepseek-chat',
|
|
360
|
+
messages: [
|
|
361
|
+
{
|
|
362
|
+
role: 'system',
|
|
363
|
+
content:
|
|
364
|
+
'You are an expert at identifying transient vs permanent errors in production systems. Respond with valid JSON only.',
|
|
365
|
+
},
|
|
366
|
+
{ role: 'user', content: prompt },
|
|
367
|
+
],
|
|
368
|
+
temperature: 0.1,
|
|
369
|
+
max_tokens: MAX_TOKENS,
|
|
370
|
+
response_format: { type: 'json_object' },
|
|
371
|
+
}),
|
|
372
|
+
signal: controller.signal,
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (!response.ok) {
|
|
377
|
+
const errorBody = await response.text().catch(() => 'Unknown error');
|
|
378
|
+
log.error('DeepSeek API error', new Error(`HTTP ${response.status}`), {
|
|
379
|
+
status: response.status,
|
|
380
|
+
errorBody: errorBody.slice(0, 500),
|
|
381
|
+
});
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const data = (await response.json()) as {
|
|
386
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const content = data.choices?.[0]?.message?.content;
|
|
390
|
+
if (!content) {
|
|
391
|
+
log.error('Empty response from DeepSeek');
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Parse JSON response
|
|
396
|
+
let parsed: unknown;
|
|
397
|
+
try {
|
|
398
|
+
parsed = JSON.parse(content);
|
|
399
|
+
} catch {
|
|
400
|
+
log.error('Invalid JSON from DeepSeek', new Error('Parse failed'), {
|
|
401
|
+
content: content.slice(0, 500),
|
|
402
|
+
});
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Validate response structure
|
|
407
|
+
const validated = validateResponse(parsed);
|
|
408
|
+
if (!validated) {
|
|
409
|
+
log.error('Response failed validation', new Error('Schema mismatch'), {
|
|
410
|
+
parsed: JSON.stringify(parsed).slice(0, 500),
|
|
411
|
+
});
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
log.info('DeepSeek analysis complete', {
|
|
416
|
+
patternsFound: validated.patterns.length,
|
|
417
|
+
summary: validated.summary.slice(0, 100),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return validated;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
log.error('DeepSeek request failed', error);
|
|
423
|
+
return null;
|
|
424
|
+
} finally {
|
|
425
|
+
clearTimeout(timeoutId);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Review context explainer response from AI
|
|
431
|
+
*/
|
|
432
|
+
export interface PatternReviewExplainer {
|
|
433
|
+
whatItCatches: string;
|
|
434
|
+
whyTransient: string;
|
|
435
|
+
affectedAreas: string;
|
|
436
|
+
recommendation: 'likely-approve' | 'needs-investigation' | 'likely-reject';
|
|
437
|
+
concerns: string[];
|
|
438
|
+
summary: string;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Build prompt for generating pattern review context
|
|
443
|
+
*/
|
|
444
|
+
function buildReviewContextPrompt(
|
|
445
|
+
pattern: {
|
|
446
|
+
patternType: string;
|
|
447
|
+
patternValue: string;
|
|
448
|
+
category: string;
|
|
449
|
+
confidenceScore: number;
|
|
450
|
+
aiReasoning?: string;
|
|
451
|
+
},
|
|
452
|
+
evidence: AggregatedPatternEvidence
|
|
453
|
+
): string {
|
|
454
|
+
const projectsList = Object.entries(evidence.matchesByProject)
|
|
455
|
+
.map(([project, count]) => `- ${project}: ${count} matches`)
|
|
456
|
+
.join('\n');
|
|
457
|
+
|
|
458
|
+
const scriptsList = Object.entries(evidence.matchesByScript)
|
|
459
|
+
.map(([script, count]) => `- ${script}: ${count} matches`)
|
|
460
|
+
.join('\n');
|
|
461
|
+
|
|
462
|
+
const samplesList = evidence.sampleMessages.slice(0, 5)
|
|
463
|
+
.map((msg, i) => `${i + 1}. "${msg}"`)
|
|
464
|
+
.join('\n');
|
|
465
|
+
|
|
466
|
+
return `You are helping a platform admin review a transient error pattern for approval.
|
|
467
|
+
|
|
468
|
+
## Pattern Details
|
|
469
|
+
- **Type**: ${pattern.patternType}
|
|
470
|
+
- **Value**: "${pattern.patternValue}"
|
|
471
|
+
- **Category**: ${pattern.category}
|
|
472
|
+
- **Initial AI Confidence**: ${Math.round(pattern.confidenceScore * 100)}%
|
|
473
|
+
${pattern.aiReasoning ? `- **Original Reasoning**: ${pattern.aiReasoning}` : ''}
|
|
474
|
+
|
|
475
|
+
## Match Evidence (collected over shadow period)
|
|
476
|
+
- **Total Matches**: ${evidence.totalMatches}
|
|
477
|
+
- **Distinct Days**: ${evidence.distinctDays}
|
|
478
|
+
- **First Match**: ${evidence.firstMatchAt ? new Date(evidence.firstMatchAt * 1000).toISOString() : 'N/A'}
|
|
479
|
+
- **Last Match**: ${evidence.lastMatchAt ? new Date(evidence.lastMatchAt * 1000).toISOString() : 'N/A'}
|
|
480
|
+
|
|
481
|
+
### Matches by Project
|
|
482
|
+
${projectsList || 'None recorded'}
|
|
483
|
+
|
|
484
|
+
### Matches by Worker Script
|
|
485
|
+
${scriptsList || 'None recorded'}
|
|
486
|
+
|
|
487
|
+
### Sample Error Messages
|
|
488
|
+
${samplesList || 'None available'}
|
|
489
|
+
|
|
490
|
+
## Your Task
|
|
491
|
+
Generate a review context to help the admin decide whether to approve this pattern.
|
|
492
|
+
|
|
493
|
+
Consider:
|
|
494
|
+
1. Does this pattern clearly catch transient errors (quota, rate limits, timeouts)?
|
|
495
|
+
2. Is there a risk of over-matching (catching real bugs as transient)?
|
|
496
|
+
3. Is there a risk of under-matching (missing similar errors)?
|
|
497
|
+
4. Which projects/workers are most affected?
|
|
498
|
+
|
|
499
|
+
## Response Format (JSON only)
|
|
500
|
+
{
|
|
501
|
+
"whatItCatches": "Brief 1-2 sentence description of what errors this pattern catches",
|
|
502
|
+
"whyTransient": "Brief explanation of why these errors are transient and self-resolving",
|
|
503
|
+
"affectedAreas": "Summary of which projects and workers are most affected",
|
|
504
|
+
"recommendation": "likely-approve" | "needs-investigation" | "likely-reject",
|
|
505
|
+
"concerns": ["List of any concerns about approving this pattern"],
|
|
506
|
+
"summary": "One paragraph summary for the admin dashboard"
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
IMPORTANT: Respond with valid JSON only.`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Validate review context response
|
|
514
|
+
*/
|
|
515
|
+
function validateReviewContextResponse(data: unknown): PatternReviewExplainer | null {
|
|
516
|
+
if (!data || typeof data !== 'object') return null;
|
|
517
|
+
|
|
518
|
+
const obj = data as Record<string, unknown>;
|
|
519
|
+
if (typeof obj.whatItCatches !== 'string') return null;
|
|
520
|
+
if (typeof obj.whyTransient !== 'string') return null;
|
|
521
|
+
if (typeof obj.affectedAreas !== 'string') return null;
|
|
522
|
+
if (!['likely-approve', 'needs-investigation', 'likely-reject'].includes(obj.recommendation as string)) return null;
|
|
523
|
+
if (!Array.isArray(obj.concerns)) return null;
|
|
524
|
+
if (typeof obj.summary !== 'string') return null;
|
|
525
|
+
|
|
526
|
+
return obj as unknown as PatternReviewExplainer;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Generate AI review context for a pattern ready for human review
|
|
531
|
+
*/
|
|
532
|
+
export async function generateReviewContext(
|
|
533
|
+
pattern: {
|
|
534
|
+
patternType: string;
|
|
535
|
+
patternValue: string;
|
|
536
|
+
category: string;
|
|
537
|
+
confidenceScore: number;
|
|
538
|
+
aiReasoning?: string;
|
|
539
|
+
},
|
|
540
|
+
evidence: AggregatedPatternEvidence,
|
|
541
|
+
env: { CLOUDFLARE_ACCOUNT_ID: string; PLATFORM_AI_GATEWAY_KEY: string },
|
|
542
|
+
log: Logger
|
|
543
|
+
): Promise<PatternReviewExplainer | null> {
|
|
544
|
+
// If no matches, generate a simple context without AI
|
|
545
|
+
if (evidence.totalMatches === 0) {
|
|
546
|
+
return {
|
|
547
|
+
whatItCatches: `Matches ${pattern.patternType} patterns containing "${pattern.patternValue}"`,
|
|
548
|
+
whyTransient: 'No real-world matches recorded during shadow period',
|
|
549
|
+
affectedAreas: 'No data available',
|
|
550
|
+
recommendation: 'needs-investigation',
|
|
551
|
+
concerns: ['No matches recorded - pattern may be too specific or not yet triggered'],
|
|
552
|
+
summary: 'This pattern has not matched any errors during the shadow evaluation period. Consider extending observation or reviewing the pattern logic.',
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const prompt = buildReviewContextPrompt(pattern, evidence);
|
|
557
|
+
|
|
558
|
+
const controller = new AbortController();
|
|
559
|
+
const timeoutId = setTimeout(() => controller.abort(), AI_FETCH_TIMEOUT_MS);
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
const response = await fetch(
|
|
563
|
+
`${DEEPSEEK_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/deepseek/chat/completions`,
|
|
564
|
+
{
|
|
565
|
+
method: 'POST',
|
|
566
|
+
headers: {
|
|
567
|
+
'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
|
|
568
|
+
'Content-Type': 'application/json',
|
|
569
|
+
},
|
|
570
|
+
body: JSON.stringify({
|
|
571
|
+
model: 'deepseek-chat',
|
|
572
|
+
messages: [
|
|
573
|
+
{
|
|
574
|
+
role: 'system',
|
|
575
|
+
content: 'You are an expert at evaluating transient error patterns for production systems. Help the admin understand this pattern clearly and concisely. Respond with valid JSON only.',
|
|
576
|
+
},
|
|
577
|
+
{ role: 'user', content: prompt },
|
|
578
|
+
],
|
|
579
|
+
temperature: 0.2,
|
|
580
|
+
max_tokens: 800,
|
|
581
|
+
response_format: { type: 'json_object' },
|
|
582
|
+
}),
|
|
583
|
+
signal: controller.signal,
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
if (!response.ok) {
|
|
588
|
+
const errorBody = await response.text().catch(() => 'Unknown error');
|
|
589
|
+
log.error('DeepSeek API error (review context)', new Error(`HTTP ${response.status}`), {
|
|
590
|
+
status: response.status,
|
|
591
|
+
errorBody: errorBody.slice(0, 500),
|
|
592
|
+
});
|
|
593
|
+
// Return a fallback context
|
|
594
|
+
return {
|
|
595
|
+
whatItCatches: `Matches "${pattern.patternValue}" errors in the ${pattern.category} category`,
|
|
596
|
+
whyTransient: 'AI analysis unavailable - review manually',
|
|
597
|
+
affectedAreas: `${Object.keys(evidence.matchesByProject).length} projects, ${Object.keys(evidence.matchesByScript).length} workers`,
|
|
598
|
+
recommendation: 'needs-investigation',
|
|
599
|
+
concerns: ['AI context generation failed - manual review recommended'],
|
|
600
|
+
summary: `Pattern matched ${evidence.totalMatches} times across ${evidence.distinctDays} days. Please review evidence manually.`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const data = (await response.json()) as {
|
|
605
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const content = data.choices?.[0]?.message?.content;
|
|
609
|
+
if (!content) {
|
|
610
|
+
log.error('Empty response from DeepSeek (review context)');
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let parsed: unknown;
|
|
615
|
+
try {
|
|
616
|
+
parsed = JSON.parse(content);
|
|
617
|
+
} catch {
|
|
618
|
+
log.error('Invalid JSON from DeepSeek (review context)', new Error('Parse failed'), {
|
|
619
|
+
content: content.slice(0, 500),
|
|
620
|
+
});
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const validated = validateReviewContextResponse(parsed);
|
|
625
|
+
if (!validated) {
|
|
626
|
+
log.error('Review context response failed validation', new Error('Schema mismatch'), {
|
|
627
|
+
parsed: JSON.stringify(parsed).slice(0, 500),
|
|
628
|
+
});
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
log.info('Generated review context', {
|
|
633
|
+
patternValue: pattern.patternValue,
|
|
634
|
+
recommendation: validated.recommendation,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
return validated;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
log.error('Failed to generate review context', error);
|
|
640
|
+
return null;
|
|
641
|
+
} finally {
|
|
642
|
+
clearTimeout(timeoutId);
|
|
643
|
+
}
|
|
644
|
+
}
|