@krotovm/gitlab-ai-review 1.0.26 → 1.0.27
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 +19 -1
- package/dist/cli/ci-review.js +151 -10
- package/dist/cli/debug-artifacts-html.js +29 -3
- package/dist/cli/tooling.js +2 -0
- package/dist/cli.js +1 -1
- package/dist/prompt/index.js +2 -1
- package/dist/prompt/templates/postprocess-system.js +3 -1
- package/dist/prompt/templates/user-prompts.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,24 @@ ai_review:
|
|
|
30
30
|
- npx -y @krotovm/gitlab-ai-review
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Save debug HTML as a CI artifact:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
stages: [review]
|
|
37
|
+
|
|
38
|
+
ai_review:
|
|
39
|
+
stage: review
|
|
40
|
+
image: node:20
|
|
41
|
+
rules:
|
|
42
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
43
|
+
script:
|
|
44
|
+
- npx -y @krotovm/gitlab-ai-review --include-artifacts
|
|
45
|
+
artifacts:
|
|
46
|
+
expire_in: 7 days
|
|
47
|
+
paths:
|
|
48
|
+
- ai-review-report.html
|
|
49
|
+
```
|
|
50
|
+
|
|
33
51
|
## Env variables
|
|
34
52
|
|
|
35
53
|
Set these in your project/group CI settings:
|
|
@@ -39,7 +57,7 @@ Set these in your project/group CI settings:
|
|
|
39
57
|
- `AI_MODEL` (optional, default: `gpt-4o-mini`; example: `gpt-4o`)
|
|
40
58
|
- `PROJECT_ACCESS_TOKEN` (optional for public projects, but required for most private projects; token with `api` scope)
|
|
41
59
|
- `GITLAB_TOKEN` (optional alias for `PROJECT_ACCESS_TOKEN`)
|
|
42
|
-
- `AI_REVIEW_ARTIFACT_HTML_FILE` (optional, default:
|
|
60
|
+
- `AI_REVIEW_ARTIFACT_HTML_FILE` (optional, default: `ai-review-report.html`; used with `--include-artifacts`)
|
|
43
61
|
|
|
44
62
|
`OPENAI_BASE_URL` is passed through to the `openai` SDK client, so you can use any OpenAI-compatible gateway/provider endpoint.
|
|
45
63
|
|
package/dist/cli/ci-review.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, extractCompletionText, parseTriageResponse, } from "../prompt/index.js";
|
|
4
4
|
import { fetchFileAtRef, searchRepository, } from "../gitlab/services.js";
|
|
5
|
-
import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
|
|
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) {
|
|
7
7
|
const withTs = { ts: new Date().toISOString(), ...record };
|
|
8
8
|
if (debugRecordWriter != null) {
|
|
@@ -447,6 +447,144 @@ async function runFileReviewWithTools(params) {
|
|
|
447
447
|
});
|
|
448
448
|
return extractCompletionText(final) ?? "No issues found.";
|
|
449
449
|
}
|
|
450
|
+
function draftHasStructuredFindings(consolidatedText) {
|
|
451
|
+
return /-\s*\[(?:high|medium)\]/i.test(consolidatedText);
|
|
452
|
+
}
|
|
453
|
+
async function runVerificationWithTools(params) {
|
|
454
|
+
const { openaiInstance, aiModel, baseMessages, refs, gitLabProjectApiUrl, projectId, headers, forceTools, consolidatedDraft, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
455
|
+
const { logDebug, logStep } = loggers;
|
|
456
|
+
const messages = [...baseMessages];
|
|
457
|
+
const tools = [
|
|
458
|
+
{
|
|
459
|
+
type: "function",
|
|
460
|
+
function: {
|
|
461
|
+
name: TOOL_NAME_GET_FILE,
|
|
462
|
+
description: "Fetch raw file content at a specific git ref for review context.",
|
|
463
|
+
parameters: {
|
|
464
|
+
type: "object",
|
|
465
|
+
additionalProperties: false,
|
|
466
|
+
properties: {
|
|
467
|
+
path: { type: "string", description: "Repository file path." },
|
|
468
|
+
ref: {
|
|
469
|
+
type: "string",
|
|
470
|
+
description: `Git ref or sha. Prefer "${refs.base}" (base) or "${refs.head}" (head).`,
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
required: ["path", "ref"],
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
type: "function",
|
|
479
|
+
function: {
|
|
480
|
+
name: TOOL_NAME_GREP,
|
|
481
|
+
description: "Search the repository for a keyword or pattern. Returns up to 10 matching code fragments with file paths and line numbers.",
|
|
482
|
+
parameters: {
|
|
483
|
+
type: "object",
|
|
484
|
+
additionalProperties: false,
|
|
485
|
+
properties: {
|
|
486
|
+
query: {
|
|
487
|
+
type: "string",
|
|
488
|
+
description: "Search string (keyword, function name, variable, etc.).",
|
|
489
|
+
},
|
|
490
|
+
ref: {
|
|
491
|
+
type: "string",
|
|
492
|
+
description: `Git ref to search in. Prefer "${refs.head}" (head).`,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
required: ["query"],
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
const verificationForceRound0 = forceTools && draftHasStructuredFindings(consolidatedDraft);
|
|
501
|
+
for (let round = 0; round < MAX_VERIFICATION_TOOL_ROUNDS; round += 1) {
|
|
502
|
+
const completion = await createCompletionWithDebug({
|
|
503
|
+
openaiInstance,
|
|
504
|
+
requestLabel: `verification_pass_round_${round + 1}`,
|
|
505
|
+
debugDumpFile,
|
|
506
|
+
debugRecordWriter,
|
|
507
|
+
request: {
|
|
508
|
+
model: aiModel,
|
|
509
|
+
temperature: 0,
|
|
510
|
+
stream: false,
|
|
511
|
+
messages,
|
|
512
|
+
tools,
|
|
513
|
+
tool_choice: verificationForceRound0 && round === 0 ? "required" : "auto",
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
const message = completion.choices[0]?.message;
|
|
517
|
+
if (message == null)
|
|
518
|
+
return completion;
|
|
519
|
+
const toolCalls = message.tool_calls ?? [];
|
|
520
|
+
logDebug(`verification round=${round + 1} tool_calls=${toolCalls.length} finish_reason=${completion.choices[0]?.finish_reason ?? "unknown"}`);
|
|
521
|
+
if (toolCalls.length === 0)
|
|
522
|
+
return completion;
|
|
523
|
+
messages.push({
|
|
524
|
+
role: "assistant",
|
|
525
|
+
content: message.content ?? "",
|
|
526
|
+
tool_calls: toolCalls,
|
|
527
|
+
});
|
|
528
|
+
for (const toolCall of toolCalls) {
|
|
529
|
+
if (toolCall.type !== "function")
|
|
530
|
+
continue;
|
|
531
|
+
const toolName = toolCall.function.name;
|
|
532
|
+
const argsRaw = toolCall.function.arguments ?? "{}";
|
|
533
|
+
await appendDebugDump(debugDumpFile, debugRecordWriter, {
|
|
534
|
+
kind: "tool_call",
|
|
535
|
+
phase: "verification",
|
|
536
|
+
round: round + 1,
|
|
537
|
+
id: toolCall.id,
|
|
538
|
+
name: toolName,
|
|
539
|
+
arguments: argsRaw,
|
|
540
|
+
});
|
|
541
|
+
logToolUsageMinimal(logStep, toolName, argsRaw, "(verify)");
|
|
542
|
+
let toolContent;
|
|
543
|
+
if (toolName === TOOL_NAME_GET_FILE) {
|
|
544
|
+
toolContent = await handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers);
|
|
545
|
+
}
|
|
546
|
+
else if (toolName === TOOL_NAME_GREP) {
|
|
547
|
+
toolContent = await handleGrepTool(argsRaw, refs.head, gitLabProjectApiUrl, headers, projectId);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
toolContent = JSON.stringify({
|
|
551
|
+
ok: false,
|
|
552
|
+
error: `Unknown tool "${toolName}"`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
messages.push({
|
|
556
|
+
role: "tool",
|
|
557
|
+
tool_call_id: toolCall.id,
|
|
558
|
+
content: toolContent,
|
|
559
|
+
});
|
|
560
|
+
await appendDebugDump(debugDumpFile, debugRecordWriter, {
|
|
561
|
+
kind: "tool_response",
|
|
562
|
+
phase: "verification",
|
|
563
|
+
round: round + 1,
|
|
564
|
+
id: toolCall.id,
|
|
565
|
+
name: toolName,
|
|
566
|
+
content: toolContent,
|
|
567
|
+
});
|
|
568
|
+
logDebug(`verification tool id=${toolCall.id} name=${toolName} payload=${toolContent.slice(0, 300)}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
messages.push({
|
|
572
|
+
role: "user",
|
|
573
|
+
content: `Tool-call limit reached (${MAX_VERIFICATION_TOOL_ROUNDS}). Do not call tools. Output only the verified findings in the required format.`,
|
|
574
|
+
});
|
|
575
|
+
return createCompletionWithDebug({
|
|
576
|
+
openaiInstance,
|
|
577
|
+
requestLabel: "verification_pass_final_after_tool_limit",
|
|
578
|
+
debugDumpFile,
|
|
579
|
+
debugRecordWriter,
|
|
580
|
+
request: {
|
|
581
|
+
model: aiModel,
|
|
582
|
+
temperature: 0,
|
|
583
|
+
stream: false,
|
|
584
|
+
messages,
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
}
|
|
450
588
|
export async function reviewMergeRequestMultiPass(params) {
|
|
451
589
|
const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, projectId, headers, maxFindings, reviewConcurrency, forceTools, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
452
590
|
const { logStep } = loggers;
|
|
@@ -555,25 +693,28 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
555
693
|
if (consolidatedText == null || consolidatedText.trim() === "") {
|
|
556
694
|
return buildAnswer(consolidateCompletion);
|
|
557
695
|
}
|
|
558
|
-
logStep("Pass 4/4: verifying consolidated findings");
|
|
696
|
+
logStep("Pass 4/4: verifying consolidated findings (repo tools)");
|
|
559
697
|
const verificationMessages = buildVerificationPrompt({
|
|
560
698
|
perFileFindings,
|
|
561
699
|
summary: triageResult.summary,
|
|
562
700
|
consolidatedFindings: consolidatedText,
|
|
563
701
|
maxFindings,
|
|
702
|
+
refs,
|
|
564
703
|
});
|
|
565
704
|
try {
|
|
566
|
-
const verificationCompletion = await
|
|
705
|
+
const verificationCompletion = await runVerificationWithTools({
|
|
567
706
|
openaiInstance,
|
|
568
|
-
|
|
707
|
+
aiModel,
|
|
708
|
+
baseMessages: verificationMessages,
|
|
709
|
+
refs,
|
|
710
|
+
gitLabProjectApiUrl,
|
|
711
|
+
projectId,
|
|
712
|
+
headers,
|
|
713
|
+
forceTools,
|
|
714
|
+
consolidatedDraft: consolidatedText,
|
|
715
|
+
loggers,
|
|
569
716
|
debugDumpFile,
|
|
570
717
|
debugRecordWriter,
|
|
571
|
-
request: {
|
|
572
|
-
model: aiModel,
|
|
573
|
-
temperature: 0.0,
|
|
574
|
-
stream: false,
|
|
575
|
-
messages: verificationMessages,
|
|
576
|
-
},
|
|
577
718
|
});
|
|
578
719
|
return buildAnswer(verificationCompletion);
|
|
579
720
|
}
|
|
@@ -76,8 +76,34 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
76
76
|
const fileServerLabel = Array.from(byLabel.keys()).find((k) => k.startsWith("file_review_server.js_round_"));
|
|
77
77
|
const fileCiLabel = Array.from(byLabel.keys()).find((k) => k.startsWith("file_review_.gitlab-ci.yml_round_"));
|
|
78
78
|
const consolidateLabel = "consolidate_pass";
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
function pickVerificationSection() {
|
|
80
|
+
const afterLimit = getContent("verification_pass_final_after_tool_limit");
|
|
81
|
+
if (afterLimit.trim() !== "")
|
|
82
|
+
return {
|
|
83
|
+
label: "verification_pass_final_after_tool_limit",
|
|
84
|
+
content: afterLimit,
|
|
85
|
+
};
|
|
86
|
+
const roundLabels = Array.from(byLabel.keys())
|
|
87
|
+
.filter((k) => k.startsWith("verification_pass_round_"))
|
|
88
|
+
.sort((a, b) => {
|
|
89
|
+
const na = Number(a.replace("verification_pass_round_", ""));
|
|
90
|
+
const nb = Number(b.replace("verification_pass_round_", ""));
|
|
91
|
+
return na - nb;
|
|
92
|
+
});
|
|
93
|
+
for (let i = roundLabels.length - 1; i >= 0; i--) {
|
|
94
|
+
const lbl = roundLabels[i];
|
|
95
|
+
const c = getContent(lbl);
|
|
96
|
+
if (c.trim() !== "")
|
|
97
|
+
return { label: lbl, content: c };
|
|
98
|
+
}
|
|
99
|
+
const legacy = getContent("verification_pass");
|
|
100
|
+
if (legacy.trim() !== "")
|
|
101
|
+
return { label: "verification_pass", content: legacy };
|
|
102
|
+
return { label: "verification_pass_round_1", content: "" };
|
|
103
|
+
}
|
|
104
|
+
const verificationSection = pickVerificationSection();
|
|
105
|
+
const verificationLabel = verificationSection.label;
|
|
106
|
+
const finalStatus = verificationSection.content.trim() !== "" ? "Verified" : "Fallback";
|
|
81
107
|
const html = `<!doctype html>
|
|
82
108
|
<html lang="en">
|
|
83
109
|
<head>
|
|
@@ -141,7 +167,7 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
141
167
|
<h2>Pass 4 — Verification</h2>
|
|
142
168
|
<div class="section">
|
|
143
169
|
<div class="row"><span class="badge">label: ${escapeHtml(verificationLabel)}</span><span class="tokens">${escapeHtml(getTokenTriplet(verificationLabel))}</span></div>
|
|
144
|
-
${renderFindings(
|
|
170
|
+
${renderFindings(verificationSection.content)}
|
|
145
171
|
</div>
|
|
146
172
|
</div>
|
|
147
173
|
</body>
|
package/dist/cli/tooling.js
CHANGED
|
@@ -3,6 +3,8 @@ export const TOOL_NAME_GET_FILE = "get_file_at_ref";
|
|
|
3
3
|
export const TOOL_NAME_GREP = "grep_repository";
|
|
4
4
|
export const MAX_TOOL_ROUNDS = 12;
|
|
5
5
|
export const MAX_FILE_TOOL_ROUNDS = 5;
|
|
6
|
+
/** Pass 4 (verification): confirm drafts against repo without duplicating main-review depth. */
|
|
7
|
+
export const MAX_VERIFICATION_TOOL_ROUNDS = 10;
|
|
6
8
|
export function logToolUsageMinimal(logStep, toolName, argsRaw, contextFile) {
|
|
7
9
|
try {
|
|
8
10
|
const parsed = JSON.parse(argsRaw);
|
package/dist/cli.js
CHANGED
|
@@ -78,7 +78,7 @@ async function main() {
|
|
|
78
78
|
const reviewConcurrency = parseNumberFlag(process.argv, "max-review-concurrency", DEFAULT_REVIEW_CONCURRENCY, 1);
|
|
79
79
|
const aiModel = envOrDefault("AI_MODEL", "gpt-4o-mini");
|
|
80
80
|
const artifactHtmlFile = INCLUDE_ARTIFACTS
|
|
81
|
-
? envOrDefault("AI_REVIEW_ARTIFACT_HTML_FILE", "
|
|
81
|
+
? envOrDefault("AI_REVIEW_ARTIFACT_HTML_FILE", "ai-review-report.html")
|
|
82
82
|
: undefined;
|
|
83
83
|
const artifactRecords = [];
|
|
84
84
|
const debugRecordWriter = INCLUDE_ARTIFACTS
|
package/dist/prompt/index.js
CHANGED
|
@@ -126,7 +126,7 @@ export function buildConsolidatePrompt(params) {
|
|
|
126
126
|
];
|
|
127
127
|
}
|
|
128
128
|
export function buildVerificationPrompt(params) {
|
|
129
|
-
const { perFileFindings, summary, consolidatedFindings, maxFindings } = params;
|
|
129
|
+
const { perFileFindings, summary, consolidatedFindings, maxFindings, refs, } = params;
|
|
130
130
|
const findingsText = perFileFindings
|
|
131
131
|
.map((f) => `### ${f.path}\n${f.findings}`)
|
|
132
132
|
.join("\n\n");
|
|
@@ -141,6 +141,7 @@ export function buildVerificationPrompt(params) {
|
|
|
141
141
|
summary,
|
|
142
142
|
findingsText,
|
|
143
143
|
consolidatedFindings,
|
|
144
|
+
refs,
|
|
144
145
|
}),
|
|
145
146
|
},
|
|
146
147
|
];
|
|
@@ -20,8 +20,10 @@ export function buildVerificationSystemLines(maxFindings) {
|
|
|
20
20
|
return [
|
|
21
21
|
"You are a skeptical verifier of a merge request review.",
|
|
22
22
|
"Your job is to remove weak, speculative, or unsupported findings from the draft list.",
|
|
23
|
+
"Tools get_file_at_ref and grep_repository are available. Use them to check claims about current code against the repository at the MR head ref.",
|
|
24
|
+
"Drop a finding if file contents at refs.head contradict it, or if it cannot be verified after reasonable tool use.",
|
|
23
25
|
"Do not add new findings. Keep, rewrite for clarity, or remove existing findings only.",
|
|
24
|
-
"A finding can stay only if
|
|
26
|
+
"A finding can stay only if supported by the per-file evidence pool and not contradicted by tools when the claim is about code that exists at refs.head.",
|
|
25
27
|
"If confidence is not high, drop the finding.",
|
|
26
28
|
"Preserve this exact per-finding markdown block:",
|
|
27
29
|
"`- [high|medium] <title>`",
|
|
@@ -57,10 +57,12 @@ export function buildConsolidateUserContent(params) {
|
|
|
57
57
|
].join("\n");
|
|
58
58
|
}
|
|
59
59
|
export function buildVerificationUserContent(params) {
|
|
60
|
-
const { summary, findingsText, consolidatedFindings } = params;
|
|
60
|
+
const { summary, findingsText, consolidatedFindings, refs } = params;
|
|
61
61
|
return [
|
|
62
62
|
`MR Summary: ${summary}`,
|
|
63
63
|
"",
|
|
64
|
+
`Refs for tools: head (post-change)="${refs.head}", base="${refs.base}". Prefer head when checking whether the issue exists in the MR.`,
|
|
65
|
+
"",
|
|
64
66
|
"Per-file findings (evidence pool):",
|
|
65
67
|
findingsText,
|
|
66
68
|
"",
|