@krotovm/gitlab-ai-review 1.0.29 → 1.0.31
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/dist/cli/ci-review.js +28 -5
- package/dist/prompt/index.js +34 -15
- package/package.json +1 -1
package/dist/cli/ci-review.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import OpenAI from "openai";
|
|
3
|
-
import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, extractCompletionText, parseTriageResponse, } from "../prompt/index.js";
|
|
3
|
+
import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, extractCompletionText, parseTriageResponseDetailed, parseTriageResponse, } from "../prompt/index.js";
|
|
4
4
|
import { fetchFileAtRef, searchRepository, } from "../gitlab/services.js";
|
|
5
5
|
import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, MAX_VERIFICATION_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
|
|
6
6
|
async function appendDebugDump(_debugDumpFile, debugRecordWriter, record) {
|
|
@@ -164,6 +164,7 @@ async function mapWithConcurrency(items, concurrency, fn) {
|
|
|
164
164
|
export async function reviewMergeRequestWithTools(params) {
|
|
165
165
|
const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, projectId, headers, forceTools, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
166
166
|
const { logDebug, logStep } = loggers;
|
|
167
|
+
logStep(`Single-pass review started for ${changes.length} file(s) (fallback mode).`);
|
|
167
168
|
const messages = buildPrompt({
|
|
168
169
|
changes: changes.map((change) => ({ diff: change.diff })),
|
|
169
170
|
limits: promptLimits,
|
|
@@ -217,6 +218,7 @@ export async function reviewMergeRequestWithTools(params) {
|
|
|
217
218
|
},
|
|
218
219
|
];
|
|
219
220
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
221
|
+
logStep(`Single-pass round ${round + 1}/${MAX_TOOL_ROUNDS}: requesting model response...`);
|
|
220
222
|
const completion = await createCompletionWithDebug({
|
|
221
223
|
openaiInstance,
|
|
222
224
|
requestLabel: `main_review_round_${round + 1}`,
|
|
@@ -236,6 +238,7 @@ export async function reviewMergeRequestWithTools(params) {
|
|
|
236
238
|
return buildAnswer(completion);
|
|
237
239
|
const toolCalls = message.tool_calls ?? [];
|
|
238
240
|
logDebug(`main-review round=${round + 1} tool_calls=${toolCalls.length} finish_reason=${completion.choices[0]?.finish_reason ?? "unknown"}`);
|
|
241
|
+
logStep(`Single-pass round ${round + 1}: model returned ${toolCalls.length} tool call(s).`);
|
|
239
242
|
if (toolCalls.length === 0)
|
|
240
243
|
return buildAnswer(completion);
|
|
241
244
|
messages.push({
|
|
@@ -290,6 +293,7 @@ export async function reviewMergeRequestWithTools(params) {
|
|
|
290
293
|
role: "user",
|
|
291
294
|
content: `Tool-call limit reached (${MAX_TOOL_ROUNDS}). Do not call any tools. Provide your best-effort final review now, strictly following the required output format. If confidence is low, return the exact no-issues sentence.`,
|
|
292
295
|
});
|
|
296
|
+
logStep("Single-pass tool-call limit reached. Requesting final answer.");
|
|
293
297
|
const finalCompletion = await createCompletionWithDebug({
|
|
294
298
|
openaiInstance,
|
|
295
299
|
requestLabel: "main_review_final_after_tool_limit",
|
|
@@ -598,6 +602,9 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
598
602
|
}));
|
|
599
603
|
const triageMessages = buildTriagePrompt(triageInputs);
|
|
600
604
|
let triageResult = null;
|
|
605
|
+
let triageText = null;
|
|
606
|
+
let triageParseReason = null;
|
|
607
|
+
let triageParseError = null;
|
|
601
608
|
try {
|
|
602
609
|
const triageCompletion = await createCompletionWithDebug({
|
|
603
610
|
openaiInstance,
|
|
@@ -612,15 +619,31 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
612
619
|
response_format: { type: "json_object" },
|
|
613
620
|
},
|
|
614
621
|
});
|
|
615
|
-
|
|
616
|
-
if (triageText != null)
|
|
617
|
-
|
|
622
|
+
triageText = extractCompletionText(triageCompletion);
|
|
623
|
+
if (triageText != null) {
|
|
624
|
+
const triageParse = parseTriageResponseDetailed(triageText);
|
|
625
|
+
triageResult = triageParse.result;
|
|
626
|
+
triageParseReason = triageParse.reason;
|
|
627
|
+
triageParseError = triageParse.parseError;
|
|
628
|
+
}
|
|
618
629
|
}
|
|
619
630
|
catch (error) {
|
|
620
631
|
logStep(`Triage pass failed: ${error?.message ?? error}. Falling back to single-pass.`);
|
|
621
632
|
}
|
|
622
633
|
if (triageResult == null) {
|
|
623
|
-
|
|
634
|
+
if (triageText != null) {
|
|
635
|
+
const triagePreview = triageText.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
636
|
+
const looksLikeHtml = /<html|<!doctype html/i.test(triageText);
|
|
637
|
+
const parseReasonText = triageParseReason != null
|
|
638
|
+
? `reason=${triageParseReason}`
|
|
639
|
+
: `reason=${looksLikeHtml ? "html_response" : "unknown_non_json"}`;
|
|
640
|
+
const parseErrorText = triageParseError != null ? ` parse_error=${triageParseError}` : "";
|
|
641
|
+
logStep(`Triage parse failed: ${parseReasonText}.${parseErrorText} Preview: ${triagePreview || "<empty>"}`);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
logStep("Triage parse failed: model returned empty response body.");
|
|
645
|
+
}
|
|
646
|
+
logStep("Falling back to single-pass pipeline.");
|
|
624
647
|
return await reviewMergeRequestWithTools({
|
|
625
648
|
openaiInstance,
|
|
626
649
|
aiModel,
|
package/dist/prompt/index.js
CHANGED
|
@@ -58,29 +58,48 @@ export function buildTriagePrompt(changes) {
|
|
|
58
58
|
];
|
|
59
59
|
}
|
|
60
60
|
export function parseTriageResponse(text) {
|
|
61
|
+
return parseTriageResponseDetailed(text).result;
|
|
62
|
+
}
|
|
63
|
+
export function parseTriageResponseDetailed(text) {
|
|
64
|
+
const cleaned = text
|
|
65
|
+
.replace(/```json?\s*/g, "")
|
|
66
|
+
.replace(/```\s*/g, "")
|
|
67
|
+
.trim();
|
|
68
|
+
if (cleaned === "") {
|
|
69
|
+
return { result: null, reason: "empty_response", parseError: null };
|
|
70
|
+
}
|
|
61
71
|
try {
|
|
62
|
-
const cleaned = text
|
|
63
|
-
.replace(/```json?\s*/g, "")
|
|
64
|
-
.replace(/```\s*/g, "")
|
|
65
|
-
.trim();
|
|
66
72
|
const parsed = JSON.parse(cleaned);
|
|
67
73
|
if (typeof parsed.summary === "string" && Array.isArray(parsed.files)) {
|
|
68
74
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
result: {
|
|
76
|
+
summary: parsed.summary,
|
|
77
|
+
files: parsed.files
|
|
78
|
+
.filter((f) => typeof f.path === "string" &&
|
|
79
|
+
(f.verdict === "NEEDS_REVIEW" || f.verdict === "SKIP"))
|
|
80
|
+
.map((f) => ({
|
|
81
|
+
path: f.path,
|
|
82
|
+
verdict: f.verdict,
|
|
83
|
+
})),
|
|
84
|
+
},
|
|
85
|
+
reason: null,
|
|
86
|
+
parseError: null,
|
|
77
87
|
};
|
|
78
88
|
}
|
|
89
|
+
return { result: null, reason: "invalid_schema", parseError: null };
|
|
79
90
|
}
|
|
80
|
-
catch {
|
|
91
|
+
catch (error) {
|
|
81
92
|
// JSON parse failure — caller will fall back to single-pass.
|
|
93
|
+
return {
|
|
94
|
+
result: null,
|
|
95
|
+
reason: "invalid_json",
|
|
96
|
+
parseError: error instanceof Error
|
|
97
|
+
? error.message
|
|
98
|
+
: typeof error === "string"
|
|
99
|
+
? error
|
|
100
|
+
: JSON.stringify(error),
|
|
101
|
+
};
|
|
82
102
|
}
|
|
83
|
-
return null;
|
|
84
103
|
}
|
|
85
104
|
export function buildFileReviewPrompt(params) {
|
|
86
105
|
const { filePath, fileDiff, summary, otherChangedFiles, allowTools = false, } = params;
|
|
@@ -126,7 +145,7 @@ export function buildConsolidatePrompt(params) {
|
|
|
126
145
|
];
|
|
127
146
|
}
|
|
128
147
|
export function buildVerificationPrompt(params) {
|
|
129
|
-
const { perFileFindings, summary, consolidatedFindings, maxFindings, refs
|
|
148
|
+
const { perFileFindings, summary, consolidatedFindings, maxFindings, refs } = params;
|
|
130
149
|
const findingsText = perFileFindings
|
|
131
150
|
.map((f) => `### ${f.path}\n${f.findings}`)
|
|
132
151
|
.join("\n\n");
|