Spaces:
Sleeping
Sleeping
Commit ·
4f889a3
1
Parent(s): 4a0e5a2
apply changes
Browse files- src/app/api/triage/route.ts +109 -8
- src/lib/sync/github-sync.ts +51 -1
src/app/api/triage/route.ts
CHANGED
|
@@ -69,21 +69,64 @@ export async function POST(request: NextRequest) {
|
|
| 69 |
});
|
| 70 |
}
|
| 71 |
|
| 72 |
-
//
|
| 73 |
-
|
|
|
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
Title: ${issue.title}
|
| 76 |
Body: ${issue.body || "(empty)"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
Respond with valid JSON only:
|
| 79 |
{
|
| 80 |
"classification": "BUG" | "CRITICAL_BUG" | "FEATURE_REQUEST" | "QUESTION" | "DOCS" | "DUPLICATE" | "NEEDS_INFO" | "SPAM",
|
| 81 |
"sentiment": "POSITIVE" | "NEUTRAL" | "NEGATIVE" | "FRUSTRATED",
|
| 82 |
-
"summary": "One-line summary
|
| 83 |
-
"suggestedLabel": "Suggested GitHub label (lowercase, hyphenated)"
|
|
|
|
| 84 |
}`;
|
| 85 |
|
| 86 |
-
// Call OpenRouter API
|
| 87 |
const aiResponse = await fetch(
|
| 88 |
"https://openrouter.ai/api/v1/chat/completions",
|
| 89 |
{
|
|
@@ -93,9 +136,10 @@ Respond with valid JSON only:
|
|
| 93 |
"Content-Type": "application/json",
|
| 94 |
},
|
| 95 |
body: JSON.stringify({
|
| 96 |
-
model: "
|
| 97 |
-
messages: [{ role: "user", content:
|
| 98 |
max_tokens: 500,
|
|
|
|
| 99 |
}),
|
| 100 |
}
|
| 101 |
);
|
|
@@ -108,6 +152,7 @@ Respond with valid JSON only:
|
|
| 108 |
let sentiment = "NEUTRAL";
|
| 109 |
let summary = issue.title;
|
| 110 |
let suggestedLabel = "needs-triage";
|
|
|
|
| 111 |
|
| 112 |
try {
|
| 113 |
const parsed = JSON.parse(aiContent);
|
|
@@ -119,11 +164,31 @@ Respond with valid JSON only:
|
|
| 119 |
: "NEUTRAL";
|
| 120 |
summary = parsed.summary || issue.title;
|
| 121 |
suggestedLabel = parsed.suggestedLabel || "needs-triage";
|
|
|
|
| 122 |
} catch {
|
| 123 |
console.error("Failed to parse AI response:", aiContent);
|
| 124 |
}
|
| 125 |
|
| 126 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
const triage = {
|
| 128 |
id: generateId(),
|
| 129 |
issueId,
|
|
@@ -131,14 +196,50 @@ Respond with valid JSON only:
|
|
| 131 |
summary,
|
| 132 |
suggestedLabel,
|
| 133 |
sentiment,
|
|
|
|
| 134 |
analyzedAt: now(),
|
| 135 |
};
|
| 136 |
|
| 137 |
await db.insert(triageData).values(triage);
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
return NextResponse.json({
|
| 140 |
message: "Issue triaged successfully",
|
| 141 |
triage,
|
|
|
|
|
|
|
| 142 |
});
|
| 143 |
} catch (error) {
|
| 144 |
console.error("POST /api/triage error:", error);
|
|
|
|
| 69 |
});
|
| 70 |
}
|
| 71 |
|
| 72 |
+
// ===== NEW: Get RAG context from project history =====
|
| 73 |
+
let ragContext = "";
|
| 74 |
+
let ragSources: any[] = [];
|
| 75 |
|
| 76 |
+
if (issue.repoName) {
|
| 77 |
+
try {
|
| 78 |
+
const aiEngineUrl = process.env.AI_ENGINE_URL || "http://localhost:7860";
|
| 79 |
+
const ragResponse = await fetch(`${aiEngineUrl}/rag/chat`, {
|
| 80 |
+
method: "POST",
|
| 81 |
+
headers: {
|
| 82 |
+
"Content-Type": "application/json",
|
| 83 |
+
"X-API-Key": process.env.API_KEY || "",
|
| 84 |
+
},
|
| 85 |
+
body: JSON.stringify({
|
| 86 |
+
question: `What is the context for this ${issue.isPR ? "pull request" : "issue"}: "${issue.title}"? ${issue.body?.slice(0, 500) || ""}`,
|
| 87 |
+
repo_name: issue.repoName,
|
| 88 |
+
top_k: 5,
|
| 89 |
+
}),
|
| 90 |
+
signal: AbortSignal.timeout(10000), // 10s timeout
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
if (ragResponse.ok) {
|
| 94 |
+
const ragData = await ragResponse.json();
|
| 95 |
+
ragContext = ragData.answer || "";
|
| 96 |
+
ragSources = ragData.sources || [];
|
| 97 |
+
console.log(`[Triage] RAG provided ${ragSources.length} sources for context`);
|
| 98 |
+
} else {
|
| 99 |
+
console.warn(`[Triage] RAG query failed with status ${ragResponse.status}`);
|
| 100 |
+
}
|
| 101 |
+
} catch (err) {
|
| 102 |
+
console.error("[Triage] RAG query error:", err);
|
| 103 |
+
// Continue without RAG context
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Build ENHANCED AI prompt with RAG context
|
| 108 |
+
const enhancedPrompt = `You are an expert GitHub ${issue.isPR ? "pull request" : "issue"} triaging assistant with access to project history and documentation.
|
| 109 |
+
|
| 110 |
+
${ragContext ? `**Project Context from Documentation & Past Issues:**\n${ragContext}\n\n---\n\n` : ""}
|
| 111 |
+
|
| 112 |
+
**Current ${issue.isPR ? "Pull Request" : "Issue"} to Classify:**
|
| 113 |
Title: ${issue.title}
|
| 114 |
Body: ${issue.body || "(empty)"}
|
| 115 |
+
Author: ${issue.authorName || "unknown"}
|
| 116 |
+
Type: ${issue.isPR ? "Pull Request" : "Issue"}
|
| 117 |
+
|
| 118 |
+
Using the project context above (if available), provide a precise classification.
|
| 119 |
|
| 120 |
Respond with valid JSON only:
|
| 121 |
{
|
| 122 |
"classification": "BUG" | "CRITICAL_BUG" | "FEATURE_REQUEST" | "QUESTION" | "DOCS" | "DUPLICATE" | "NEEDS_INFO" | "SPAM",
|
| 123 |
"sentiment": "POSITIVE" | "NEUTRAL" | "NEGATIVE" | "FRUSTRATED",
|
| 124 |
+
"summary": "One-line summary explaining the classification",
|
| 125 |
+
"suggestedLabel": "Suggested GitHub label (lowercase, hyphenated)",
|
| 126 |
+
"confidence": 0.0-1.0
|
| 127 |
}`;
|
| 128 |
|
| 129 |
+
// Call OpenRouter API with upgraded model
|
| 130 |
const aiResponse = await fetch(
|
| 131 |
"https://openrouter.ai/api/v1/chat/completions",
|
| 132 |
{
|
|
|
|
| 136 |
"Content-Type": "application/json",
|
| 137 |
},
|
| 138 |
body: JSON.stringify({
|
| 139 |
+
model: "meta-llama/llama-3.3-70b-instruct:free", // Upgraded from claude-haiku
|
| 140 |
+
messages: [{ role: "user", content: enhancedPrompt }],
|
| 141 |
max_tokens: 500,
|
| 142 |
+
temperature: 0.3, // Lower temp for more deterministic classification
|
| 143 |
}),
|
| 144 |
}
|
| 145 |
);
|
|
|
|
| 152 |
let sentiment = "NEUTRAL";
|
| 153 |
let summary = issue.title;
|
| 154 |
let suggestedLabel = "needs-triage";
|
| 155 |
+
let confidence = 0.5;
|
| 156 |
|
| 157 |
try {
|
| 158 |
const parsed = JSON.parse(aiContent);
|
|
|
|
| 164 |
: "NEUTRAL";
|
| 165 |
summary = parsed.summary || issue.title;
|
| 166 |
suggestedLabel = parsed.suggestedLabel || "needs-triage";
|
| 167 |
+
confidence = parsed.confidence || 0.5;
|
| 168 |
} catch {
|
| 169 |
console.error("Failed to parse AI response:", aiContent);
|
| 170 |
}
|
| 171 |
|
| 172 |
+
// ===== NEW: Quality Assessment for PRs (estimate bug risk) =====
|
| 173 |
+
let bugRiskScore: number | null = null;
|
| 174 |
+
|
| 175 |
+
if (issue.isPR) {
|
| 176 |
+
// Estimate bug risk based on classification
|
| 177 |
+
// TODO: Ideally we'd use QualityAssessmentService with actual PR diff
|
| 178 |
+
// For now, use classification as a heuristic
|
| 179 |
+
if (classification === "CRITICAL_BUG") {
|
| 180 |
+
bugRiskScore = 9;
|
| 181 |
+
} else if (classification === "BUG") {
|
| 182 |
+
bugRiskScore = 6;
|
| 183 |
+
} else {
|
| 184 |
+
// Even non-bug PRs have some risk
|
| 185 |
+
bugRiskScore = Math.max(3, Math.floor((1 - confidence) * 5));
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
console.log(`[Triage] Assigned bug risk score: ${bugRiskScore} for PR #${issue.number}`);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Save triage data with new fields
|
| 192 |
const triage = {
|
| 193 |
id: generateId(),
|
| 194 |
issueId,
|
|
|
|
| 196 |
summary,
|
| 197 |
suggestedLabel,
|
| 198 |
sentiment,
|
| 199 |
+
bugRiskScore, // NEW
|
| 200 |
analyzedAt: now(),
|
| 201 |
};
|
| 202 |
|
| 203 |
await db.insert(triageData).values(triage);
|
| 204 |
|
| 205 |
+
// ===== NEW: Auto-dispatch Agent for CRITICAL_BUG =====
|
| 206 |
+
let agentDispatched = false;
|
| 207 |
+
|
| 208 |
+
if (classification === "CRITICAL_BUG" && user.githubAccessToken && issue.owner && issue.repo) {
|
| 209 |
+
console.log(`[Triage] CRITICAL_BUG detected, auto-dispatching Agent for ${issue.repoName}#${issue.number}`);
|
| 210 |
+
|
| 211 |
+
try {
|
| 212 |
+
// Fire-and-forget Agent analysis (don't await)
|
| 213 |
+
const baseUrl = process.env.NEXTAUTH_URL || "http://localhost:3001";
|
| 214 |
+
fetch(`${baseUrl}/api/agent/analyze`, {
|
| 215 |
+
method: "POST",
|
| 216 |
+
headers: {
|
| 217 |
+
"Content-Type": "application/json",
|
| 218 |
+
Authorization: request.headers.get("Authorization") || "",
|
| 219 |
+
},
|
| 220 |
+
body: JSON.stringify({
|
| 221 |
+
owner: issue.owner,
|
| 222 |
+
repo: issue.repo,
|
| 223 |
+
issueNumber: issue.isPR ? undefined : issue.number,
|
| 224 |
+
prNumber: issue.isPR ? issue.number : undefined,
|
| 225 |
+
goalType: issue.isPR ? "bug_hunt" : "issue_triage",
|
| 226 |
+
sessionId: `auto-${issueId}-${Date.now()}`,
|
| 227 |
+
}),
|
| 228 |
+
}).catch((err) => {
|
| 229 |
+
console.error("[Triage] Agent auto-dispatch failed:", err.message);
|
| 230 |
+
});
|
| 231 |
+
|
| 232 |
+
agentDispatched = true;
|
| 233 |
+
} catch (err) {
|
| 234 |
+
console.error("[Triage] Agent dispatch setup failed:", err);
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
return NextResponse.json({
|
| 239 |
message: "Issue triaged successfully",
|
| 240 |
triage,
|
| 241 |
+
ragSources, // NEW: return sources used for transparency
|
| 242 |
+
agentDispatched, // NEW: indicate if Agent was triggered
|
| 243 |
});
|
| 244 |
} catch (error) {
|
| 245 |
console.error("POST /api/triage error:", error);
|
src/lib/sync/github-sync.ts
CHANGED
|
@@ -453,7 +453,57 @@ export async function syncSingleRepository(
|
|
| 453 |
options: SyncOptions = {}
|
| 454 |
): Promise<SyncResult> {
|
| 455 |
const octokit = new Octokit({ auth: accessToken });
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
}
|
| 458 |
|
| 459 |
// =============================================================================
|
|
|
|
| 453 |
options: SyncOptions = {}
|
| 454 |
): Promise<SyncResult> {
|
| 455 |
const octokit = new Octokit({ auth: accessToken });
|
| 456 |
+
const result = await syncRepository(octokit, repoId, owner, repo, options);
|
| 457 |
+
|
| 458 |
+
// ===== Auto-trigger RAG indexing after successful sync =====
|
| 459 |
+
if (result.success && (result.created > 0 || result.updated > 0)) {
|
| 460 |
+
try {
|
| 461 |
+
const aiEngineUrl = process.env.AI_ENGINE_URL || "http://localhost:7860";
|
| 462 |
+
const repoName = `${owner}/${repo}`;
|
| 463 |
+
|
| 464 |
+
// Check if we need to index (avoid redundant indexing)
|
| 465 |
+
const checkResponse = await fetch(
|
| 466 |
+
`${aiEngineUrl}/rag/check-index?repo_name=${encodeURIComponent(repoName)}`,
|
| 467 |
+
{
|
| 468 |
+
headers: {
|
| 469 |
+
"X-API-Key": process.env.API_KEY || "",
|
| 470 |
+
"Content-Type": "application/json"
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
);
|
| 474 |
+
|
| 475 |
+
if (checkResponse.ok) {
|
| 476 |
+
const checkData = await checkResponse.json();
|
| 477 |
+
const chunkCount = checkData.chunk_count || 0;
|
| 478 |
+
|
| 479 |
+
if (chunkCount < 100) {
|
| 480 |
+
console.log(`[Sync] Triggering RAG indexing for ${repoName} (current chunks: ${chunkCount})`);
|
| 481 |
+
|
| 482 |
+
// Fire-and-forget indexing (don't await to avoid blocking sync)
|
| 483 |
+
fetch(`${aiEngineUrl}/rag/index`, {
|
| 484 |
+
method: "POST",
|
| 485 |
+
headers: {
|
| 486 |
+
"Content-Type": "application/json",
|
| 487 |
+
"X-API-Key": process.env.API_KEY || "",
|
| 488 |
+
},
|
| 489 |
+
body: JSON.stringify({
|
| 490 |
+
repo_name: repoName,
|
| 491 |
+
github_access_token: accessToken,
|
| 492 |
+
}),
|
| 493 |
+
}).catch((err) => {
|
| 494 |
+
console.error(`[Sync] RAG indexing failed for ${repoName}:`, err.message);
|
| 495 |
+
});
|
| 496 |
+
} else {
|
| 497 |
+
console.log(`[Sync] Skipping RAG indexing for ${repoName}, already has ${chunkCount} chunks`);
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
} catch (err) {
|
| 501 |
+
// Log but don't fail the sync if RAG indexing check errors
|
| 502 |
+
console.error("[Sync] RAG index check failed:", err);
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
return result;
|
| 507 |
}
|
| 508 |
|
| 509 |
// =============================================================================
|