diff --git a/Server/app/worker-service/services/rag.service.ts b/Server/app/worker-service/services/rag.service.ts index ab3e41e..94d97e9 100644 --- a/Server/app/worker-service/services/rag.service.ts +++ b/Server/app/worker-service/services/rag.service.ts @@ -308,6 +308,24 @@ export async function vectorDBExists(DBName: string): Promise { return indexes?.some((idx) => idx.name === DBName) ?? false; } +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export async function waitForVectorIndex( + indexName: string, + options: { timeoutMs?: number; intervalMs?: number } = {}, +): Promise { + const { timeoutMs = 10 * 60 * 1000, intervalMs = 5000 } = options; + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + if (await vectorDBExists(indexName)) return true; + console.log(`Waiting for vector index "${indexName}"...`); + await sleep(intervalMs); + } + + return false; +} + export async function runRAGPipeline(data: object) { const { installationId, owner, repo } = data as RAGPipelineInput; diff --git a/Server/app/worker-service/services/review.service.ts b/Server/app/worker-service/services/review.service.ts index 68cb965..61da9dc 100644 --- a/Server/app/worker-service/services/review.service.ts +++ b/Server/app/worker-service/services/review.service.ts @@ -15,9 +15,8 @@ import { import { db } from "../../../package/db/prisma.js"; import { vectorDBExists, - createEmbeddings, - saveToVectorDB, - searchRelevantEmbeddings, + waitForVectorIndex, + retrieveContextForDiff, toIndexName, } from "./rag.service.js"; import { findGIF } from "./gif.service.js"; @@ -137,17 +136,6 @@ async function resolveUserId( export async function runPRReview(data: PRReviewJobData) { const { installationId, owner, repo, prNumber, prTitle, prAuthor } = data; - const dbName = ""; - - const createVectorDB = (await vectorDBExists(dbName)) as boolean; - - if (createVectorDB) { - const values = {}; - - const embeddings = (await createEmbeddings(values)) as any; - const finalSaved = await saveToVectorDB(embeddings); - console.log(finalSaved); - } const octokit = await getOctokit(installationId); const userId = await resolveUserId(owner, repo, data.userId); @@ -180,17 +168,34 @@ export async function runPRReview(data: PRReviewJobData) { const rules = await getReviewRules(octokit, owner, repo, prNumber); const reviewType = (await getRevieType(difference, apiKey, tokenUsage)) as any; const indexName = toIndexName(owner, repo); - const DBExsists = await vectorDBExists(indexName); - let relevantCode: any; + let relevantCode: Awaited> | undefined; const processedDiff = (await getCodeDiff(difference)) as any; - if (reviewType === "feature" && DBExsists) { - const searchQuery = await generateRelevantSearchQuery(processedDiff, apiKey, tokenUsage); - const query = { - text: searchQuery, - indexName: indexName, - topK: 5, - }; - relevantCode = await searchRelevantEmbeddings(query); + + if (reviewType === "feature") { + const indexReady = + (await vectorDBExists(indexName)) || + (await waitForVectorIndex(indexName)); + + if (indexReady) { + const searchQuery = await generateRelevantSearchQuery( + processedDiff, + apiKey, + tokenUsage, + ); + relevantCode = await retrieveContextForDiff({ + diff: difference, + semanticQuery: searchQuery, + indexName, + topK: 8, + }); + console.log( + `RAG context for ${owner}/${repo}#${prNumber}: ${relevantCode.length} AST chunks`, + ); + } else { + console.log( + `Vector index "${indexName}" not ready — reviewing without RAG context`, + ); + } } const prompt = reviewPrompt(difference, rules, relevantCode); diff --git a/Server/app/worker-service/workers/review.worker.ts b/Server/app/worker-service/workers/review.worker.ts index 506bdad..3bfe1e7 100644 --- a/Server/app/worker-service/workers/review.worker.ts +++ b/Server/app/worker-service/workers/review.worker.ts @@ -5,8 +5,12 @@ export async function workersOn(){ const client = await createWorkerRedisClient() console.log("webhook for PR is on") - while(true){ - const gettingData = await client.brPop("reviewQueue", 0) - processPRReviewJob(JSON.parse(gettingData!.element)) + while (true) { + const gettingData = await client.brPop("reviewQueue", 0); + try { + await processPRReviewJob(JSON.parse(gettingData!.element)); + } catch (err) { + console.error("PR review job error:", err); + } } } \ No newline at end of file diff --git a/Server/package/ai/deployment.ts b/Server/package/ai/deployment.ts index edd2849..763aec8 100644 --- a/Server/package/ai/deployment.ts +++ b/Server/package/ai/deployment.ts @@ -1,4 +1,5 @@ import Anthropic from "@anthropic-ai/sdk"; +import { CLAUDE_MODEL } from "./models.js"; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, @@ -6,7 +7,7 @@ const anthropic = new Anthropic({ export async function analyzeDeploymentLogs(logs: string, provider: string) { const res = await anthropic.messages.create({ - model: "claude-sonnet-4-6", + model: CLAUDE_MODEL, max_tokens: 1500, system: `You are a senior DevOps engineer analyzing CI/CD deployment failure logs. @@ -44,7 +45,7 @@ Do NOT include markdown or text outside the JSON.`, export async function generateDeploymentFix(fileContent: string, cause: string, fix: string, fileName: string) { const res = await anthropic.messages.create({ - model: "claude-sonnet-4-6", + model: CLAUDE_MODEL, max_tokens: 4000, system: `You are a senior software engineer. You will be given a file and a description of a bug causing a deployment failure. Apply the fix and return the complete corrected file content. diff --git a/Server/package/ai/gif.ts b/Server/package/ai/gif.ts index 5e1e5b6..bf69f53 100644 --- a/Server/package/ai/gif.ts +++ b/Server/package/ai/gif.ts @@ -1,5 +1,6 @@ import Anthropic from "@anthropic-ai/sdk"; import { addUsage, createTokenAccumulator, type TokenAccumulator } from "./review.js"; +import { CLAUDE_MODEL } from "./models.js"; export async function getGifName( summary: string | null | undefined, @@ -13,7 +14,7 @@ export async function getGifName( const anthropic = new Anthropic({ apiKey }); const res = await anthropic.messages.create({ - model: "claude-sonnet-4-5", + model: CLAUDE_MODEL, max_tokens: 40, temperature: 0, system: diff --git a/Server/package/ai/models.ts b/Server/package/ai/models.ts new file mode 100644 index 0000000..d5d7c0a --- /dev/null +++ b/Server/package/ai/models.ts @@ -0,0 +1,2 @@ +export const CLAUDE_MODEL = + process.env.CLAUDE_MODEL?.trim() || "claude-haiku-4-5"; diff --git a/Server/package/ai/review.ts b/Server/package/ai/review.ts index 4a7a8e4..7ee873a 100644 --- a/Server/package/ai/review.ts +++ b/Server/package/ai/review.ts @@ -1,4 +1,5 @@ import Anthropic from "@anthropic-ai/sdk"; +import { CLAUDE_MODEL } from "./models.js"; export type TokenAccumulator = { inputTokens: number; @@ -21,15 +22,16 @@ function createClient(apiKey: string) { export async function getRevieType(diff: string, apiKey: string, usage?: TokenAccumulator) { const anthropic = createClient(apiKey); - const prompt = `You are supposed to see the Code Diff ${diff} and identify the PR Type if it is a Bug Fix / New Feature / Code Update , etc and only return the name of the type in response . "Return ONLY: feature | bugfix | refactor"`; + const diffPreview = diff.slice(0, 3000); + const prompt = `Identify the PR type from this code diff snippet. Return ONLY one word: feature | bugfix | refactor\n\n${diffPreview}`; const res = await anthropic.messages.create({ - model: "claude-sonnet-4-5", - max_tokens: 1500, + model: CLAUDE_MODEL, + max_tokens: 16, temperature: 0, messages: [ { role: "user", - content: prompt ? prompt : "", + content: prompt, }, ], }); @@ -53,8 +55,8 @@ export async function generateRelevantSearchQuery( ) { const anthropic = createClient(apiKey); const res = await anthropic.messages.create({ - model: "claude-sonnet-4-5", - max_tokens: 1500, + model: CLAUDE_MODEL, + max_tokens: 100, temperature: 0, system: "Convert the given git diff into a short semantic search query to find relevant code in a repository Return ONLY a short phrase.", @@ -97,8 +99,8 @@ export async function getAIReview( async function _callAI(prompt: string | null, apiKey: string, usage?: TokenAccumulator) { const anthropic = createClient(apiKey); const res = await anthropic.messages.create({ - model: "claude-sonnet-4-5", - max_tokens: 1500, + model: CLAUDE_MODEL, + max_tokens: 4096, temperature: 0, system: `You are a strict senior software engineer performing code reviews. @@ -167,6 +169,10 @@ Do NOT include markdown, explanations, or text outside JSON.`, }); addUsage(usage ?? createTokenAccumulator(), res.usage); + if (res.stop_reason === "max_tokens") { + console.warn("AI review response was truncated (max_tokens reached)"); + } + const block = res.content.find((b) => b.type === "text"); if (!block || block.type !== "text") throw new Error("No text block in AI response"); @@ -199,6 +205,8 @@ function formatRelevantCode(matches: RelevantCodeMatch[]): string { .join("\n\n---\n\n") } +const MAX_DIFF_CHARS = 80_000; + export function reviewPrompt( diff: string, rules: any, @@ -209,6 +217,11 @@ export function reviewPrompt( ? `\nEXISTING CODEBASE CONTEXT (retrieved for this feature):\nUse this to check for consistency, duplication, or conflicts with existing patterns.\n\n${formatRelevantCode(relevantCode)}\n` : ""; + const truncatedDiff = + diff.length > MAX_DIFF_CHARS + ? `${diff.slice(0, MAX_DIFF_CHARS)}\n\n[... diff truncated at ${MAX_DIFF_CHARS} chars ...]` + : diff; + return ` REPOSITORY RULES (HIGHEST PRIORITY): ${JSON.stringify(rules, null, 2)} @@ -222,7 +235,7 @@ Check for: - Maintainability (naming, duplication) ${relevantSection} GIT DIFF: -${diff} +${truncatedDiff} Analyze ONLY the changed code. @@ -230,11 +243,29 @@ Return strictly valid JSON. `; } +function extractJsonObject(text: string): string | null { + const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); + if (fenced?.[1]) return fenced[1].trim(); + + const start = text.indexOf("{"); + const end = text.lastIndexOf("}"); + if (start !== -1 && end > start) return text.slice(start, end + 1); + + return null; +} + export function parseAIResponse(text: string) { - try { - return JSON.parse(text); - } catch (err) { - console.log("❌ Failed to parse AI response"); - return null; + const candidates = [text.trim(), extractJsonObject(text)].filter(Boolean) as string[]; + + for (const candidate of candidates) { + try { + return JSON.parse(candidate); + } catch { + // try next candidate + } } + + console.log("❌ Failed to parse AI response"); + console.log("Response preview:", text.slice(0, 500)); + return null; }