docs: enterprise refactor plan with ralph specs
This commit is contained in:
71
dist/server/enrichment/AIEnrichmentService.js
vendored
Normal file
71
dist/server/enrichment/AIEnrichmentService.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
/**
|
||||
* AIEnrichmentService — selects AI provider and runs enrichment asynchronously.
|
||||
* Triggered manually (POST /api/anomalies/:id/enrich) or automatically for high/critical.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AIEnrichmentService = void 0;
|
||||
const ClaudeProvider_1 = require("./ClaudeProvider");
|
||||
const OpenAIProvider_1 = require("./OpenAIProvider");
|
||||
const OllamaProvider_1 = require("./OllamaProvider");
|
||||
const logger_1 = require("../logger");
|
||||
class AIEnrichmentService {
|
||||
constructor(emitter) {
|
||||
this.emitter = emitter;
|
||||
this.autoEnrich = process.env['ABE_AI_AUTO_ENRICH'] === 'true';
|
||||
const minSev = process.env['ABE_AI_MIN_SEVERITY'] ?? 'high';
|
||||
this.minSeverityRank = AIEnrichmentService.SEVERITY_RANK[minSev] ?? 2;
|
||||
this.provider = this.createProvider();
|
||||
}
|
||||
createProvider() {
|
||||
const providerName = process.env['ABE_AI_PROVIDER'] ?? 'none';
|
||||
const model = process.env['ABE_AI_MODEL'];
|
||||
if (providerName === 'claude') {
|
||||
const key = process.env['ABE_AI_API_KEY'];
|
||||
if (!key)
|
||||
return null;
|
||||
return new ClaudeProvider_1.ClaudeProvider(key, model);
|
||||
}
|
||||
if (providerName === 'openai') {
|
||||
const key = process.env['ABE_OPENAI_API_KEY'];
|
||||
if (!key)
|
||||
return null;
|
||||
return new OpenAIProvider_1.OpenAIProvider(key, model);
|
||||
}
|
||||
if (providerName === 'ollama') {
|
||||
const url = process.env['ABE_OLLAMA_URL'] ?? 'http://localhost:11434';
|
||||
return new OllamaProvider_1.OllamaProvider(url, model);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/** Check if auto-enrichment should run for this anomaly. */
|
||||
shouldAutoEnrich(anomaly) {
|
||||
if (!this.autoEnrich || !this.provider)
|
||||
return false;
|
||||
const rank = AIEnrichmentService.SEVERITY_RANK[anomaly.severity] ?? 0;
|
||||
return rank >= this.minSeverityRank;
|
||||
}
|
||||
/** Enrich an anomaly asynchronously and emit WebSocket event when done. */
|
||||
async enrich(anomaly, context) {
|
||||
if (!this.provider) {
|
||||
logger_1.log.warn({ anomalyId: anomaly.id }, 'No AI provider configured');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const enrichment = await this.provider.enrich(anomaly, context);
|
||||
anomaly.aiEnrichment = enrichment;
|
||||
this.emitter('anomaly:enriched', { anomalyId: anomaly.id, enrichment });
|
||||
logger_1.log.info({ anomalyId: anomaly.id, provider: this.provider.name }, 'Anomaly enriched');
|
||||
}
|
||||
catch (err) {
|
||||
logger_1.log.error({ anomalyId: anomaly.id, err: err instanceof Error ? err.message : String(err) }, 'AI enrichment failed');
|
||||
}
|
||||
}
|
||||
hasProvider() {
|
||||
return this.provider !== null;
|
||||
}
|
||||
}
|
||||
exports.AIEnrichmentService = AIEnrichmentService;
|
||||
AIEnrichmentService.SEVERITY_RANK = {
|
||||
low: 0, medium: 1, high: 2, critical: 3,
|
||||
};
|
||||
88
dist/server/enrichment/ClaudeProvider.js
vendored
Normal file
88
dist/server/enrichment/ClaudeProvider.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
"use strict";
|
||||
/**
|
||||
* ClaudeProvider — AI enrichment using Anthropic API.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ClaudeProvider = void 0;
|
||||
const DEFAULT_MODEL = 'claude-haiku-4-5-20251001';
|
||||
class ClaudeProvider {
|
||||
constructor(apiKey, model = DEFAULT_MODEL) {
|
||||
this.name = 'claude';
|
||||
this.apiKey = apiKey;
|
||||
this.model = model;
|
||||
}
|
||||
async enrich(anomaly, context) {
|
||||
const prompt = buildPrompt(anomaly, context);
|
||||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': this.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const text = data.content.find((c) => c.type === 'text')?.text ?? '';
|
||||
return parseEnrichment(text, this.name, this.model);
|
||||
}
|
||||
}
|
||||
exports.ClaudeProvider = ClaudeProvider;
|
||||
function buildPrompt(anomaly, context) {
|
||||
return `You are a senior software engineer analyzing a bug report from an automated web testing tool.
|
||||
|
||||
Bug Report:
|
||||
- Type: ${anomaly.type}
|
||||
- Severity: ${anomaly.severity}
|
||||
- Description: ${anomaly.description}
|
||||
- URL: ${context.url}
|
||||
- Page Title: ${context.pageTitle}
|
||||
- Action Trace: ${JSON.stringify(anomaly.actionTrace.slice(-5), null, 2)}
|
||||
${context.httpLog.length > 0 ? `- HTTP Log: ${JSON.stringify(context.httpLog.slice(-3), null, 2)}` : ''}
|
||||
${context.consoleErrors.length > 0 ? `- Console Errors: ${context.consoleErrors.slice(-3).join('\n')}` : ''}
|
||||
|
||||
Please provide a concise analysis in exactly this JSON format:
|
||||
{
|
||||
"rootCause": "One sentence explaining the likely root cause",
|
||||
"userImpact": "One sentence describing the impact on users",
|
||||
"suggestedFix": "One to two sentences with a concrete fix suggestion",
|
||||
"confidence": "low|medium|high"
|
||||
}`;
|
||||
}
|
||||
function parseEnrichment(text, provider, model) {
|
||||
const debugPrompt = `Bug analysis:\n${text}`;
|
||||
try {
|
||||
const match = text.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
const parsed = JSON.parse(match[0]);
|
||||
return {
|
||||
rootCause: parsed.rootCause ?? 'Unknown root cause',
|
||||
userImpact: parsed.userImpact ?? 'Unknown impact',
|
||||
suggestedFix: parsed.suggestedFix ?? 'No fix suggested',
|
||||
debugPrompt,
|
||||
confidence: parsed.confidence ?? 'medium',
|
||||
generatedAt: Date.now(),
|
||||
provider,
|
||||
model,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch { /* fallback below */ }
|
||||
return {
|
||||
rootCause: text.slice(0, 200) || 'Could not parse root cause',
|
||||
userImpact: 'See full response',
|
||||
suggestedFix: 'See full response',
|
||||
debugPrompt,
|
||||
confidence: 'low',
|
||||
generatedAt: Date.now(),
|
||||
provider,
|
||||
model,
|
||||
};
|
||||
}
|
||||
63
dist/server/enrichment/OllamaProvider.js
vendored
Normal file
63
dist/server/enrichment/OllamaProvider.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
/**
|
||||
* OllamaProvider — AI enrichment using local Ollama API.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OllamaProvider = void 0;
|
||||
const DEFAULT_MODEL = 'llama3.2';
|
||||
const DEFAULT_URL = 'http://localhost:11434';
|
||||
function buildPrompt(anomaly, context) {
|
||||
return `Analyze this bug and respond ONLY with JSON {"rootCause":"...","userImpact":"...","suggestedFix":"...","confidence":"low|medium|high"}.
|
||||
|
||||
Bug: ${anomaly.type} (${anomaly.severity}) at ${context.url}
|
||||
Description: ${anomaly.description}
|
||||
Last actions: ${anomaly.actionTrace.slice(-3).map((a) => `${a.type} ${a.selector ?? a.url ?? ''}`).join(' → ')}`;
|
||||
}
|
||||
class OllamaProvider {
|
||||
constructor(baseUrl = DEFAULT_URL, model = DEFAULT_MODEL) {
|
||||
this.name = 'ollama';
|
||||
this.baseUrl = baseUrl;
|
||||
this.model = model;
|
||||
}
|
||||
async enrich(anomaly, context) {
|
||||
const prompt = buildPrompt(anomaly, context);
|
||||
const res = await fetch(`${this.baseUrl}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: this.model, prompt, stream: false }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Ollama API error: ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const text = data.response ?? '';
|
||||
try {
|
||||
const match = text.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
const p = JSON.parse(match[0]);
|
||||
return {
|
||||
rootCause: p['rootCause'] ?? 'Unknown',
|
||||
userImpact: p['userImpact'] ?? 'Unknown',
|
||||
suggestedFix: p['suggestedFix'] ?? 'None',
|
||||
debugPrompt: text,
|
||||
confidence: p['confidence'] ?? 'low',
|
||||
generatedAt: Date.now(),
|
||||
provider: 'ollama',
|
||||
model: this.model,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch { /* fallback */ }
|
||||
return {
|
||||
rootCause: text.slice(0, 200),
|
||||
userImpact: 'See response',
|
||||
suggestedFix: 'See response',
|
||||
debugPrompt: text,
|
||||
confidence: 'low',
|
||||
generatedAt: Date.now(),
|
||||
provider: 'ollama',
|
||||
model: this.model,
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.OllamaProvider = OllamaProvider;
|
||||
81
dist/server/enrichment/OpenAIProvider.js
vendored
Normal file
81
dist/server/enrichment/OpenAIProvider.js
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
/**
|
||||
* OpenAIProvider — AI enrichment using OpenAI API.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OpenAIProvider = void 0;
|
||||
const DEFAULT_MODEL = 'gpt-4o-mini';
|
||||
function buildPrompt(anomaly, context) {
|
||||
return `You are a senior software engineer analyzing a bug report.
|
||||
|
||||
Bug:
|
||||
- Type: ${anomaly.type}
|
||||
- Severity: ${anomaly.severity}
|
||||
- Description: ${anomaly.description}
|
||||
- URL: ${context.url}
|
||||
- Actions: ${JSON.stringify(anomaly.actionTrace.slice(-5))}
|
||||
${context.httpLog.length > 0 ? `- HTTP: ${JSON.stringify(context.httpLog.slice(-3))}` : ''}
|
||||
${context.consoleErrors.length > 0 ? `- Errors: ${context.consoleErrors.slice(-3).join('; ')}` : ''}
|
||||
|
||||
Respond ONLY with JSON:
|
||||
{"rootCause":"...","userImpact":"...","suggestedFix":"...","confidence":"low|medium|high"}`;
|
||||
}
|
||||
function parseResponse(text, model) {
|
||||
try {
|
||||
const match = text.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
const p = JSON.parse(match[0]);
|
||||
return {
|
||||
rootCause: p['rootCause'] ?? 'Unknown',
|
||||
userImpact: p['userImpact'] ?? 'Unknown',
|
||||
suggestedFix: p['suggestedFix'] ?? 'None',
|
||||
debugPrompt: text,
|
||||
confidence: p['confidence'] ?? 'medium',
|
||||
generatedAt: Date.now(),
|
||||
provider: 'openai',
|
||||
model,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch { /* fallback */ }
|
||||
return {
|
||||
rootCause: text.slice(0, 200),
|
||||
userImpact: 'See response',
|
||||
suggestedFix: 'See response',
|
||||
debugPrompt: text,
|
||||
confidence: 'low',
|
||||
generatedAt: Date.now(),
|
||||
provider: 'openai',
|
||||
model,
|
||||
};
|
||||
}
|
||||
class OpenAIProvider {
|
||||
constructor(apiKey, model = DEFAULT_MODEL) {
|
||||
this.name = 'openai';
|
||||
this.apiKey = apiKey;
|
||||
this.model = model;
|
||||
}
|
||||
async enrich(anomaly, context) {
|
||||
const prompt = buildPrompt(anomaly, context);
|
||||
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 512,
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const text = data.choices[0]?.message?.content ?? '';
|
||||
return parseResponse(text, this.model);
|
||||
}
|
||||
}
|
||||
exports.OpenAIProvider = OpenAIProvider;
|
||||
Reference in New Issue
Block a user