Initial implementation: AI code review bot for Gitea
- CLI binary with flag/env var configuration - Gitea API client (PR metadata, diff, CI status, post review) - OpenAI-compatible LLM client - Structured review prompt with conventions support - JSON response parser with validation - Markdown review formatter for Gitea - CI failure auto-detection (REQUEST_CHANGES) - Dry-run mode for testing
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package review
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Finding represents a single code review finding.
|
||||
type Finding struct {
|
||||
Severity string `json:"severity"`
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
Finding string `json:"finding"`
|
||||
}
|
||||
|
||||
// ReviewResult is the structured output from the LLM.
|
||||
type ReviewResult struct {
|
||||
Verdict string `json:"verdict"`
|
||||
Summary string `json:"summary"`
|
||||
Findings []Finding `json:"findings"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
}
|
||||
|
||||
// ParseResponse parses the LLM response into a ReviewResult.
|
||||
func ParseResponse(response string) (*ReviewResult, error) {
|
||||
// Try to extract JSON from the response — the LLM might wrap it in markdown fences
|
||||
cleaned := extractJSON(response)
|
||||
|
||||
var result ReviewResult
|
||||
if err := json.Unmarshal([]byte(cleaned), &result); err != nil {
|
||||
return nil, fmt.Errorf("parse LLM response as JSON: %w\nRaw response: %s", err, response)
|
||||
}
|
||||
|
||||
// Validate verdict
|
||||
switch result.Verdict {
|
||||
case "APPROVE", "REQUEST_CHANGES":
|
||||
// valid
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid verdict %q (must be APPROVE or REQUEST_CHANGES)", result.Verdict)
|
||||
}
|
||||
|
||||
// Validate finding severities
|
||||
for i, f := range result.Findings {
|
||||
switch f.Severity {
|
||||
case "MAJOR", "MINOR", "NIT":
|
||||
// valid
|
||||
default:
|
||||
return nil, fmt.Errorf("finding %d has invalid severity %q", i, f.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// extractJSON attempts to pull JSON from a potentially markdown-wrapped response.
|
||||
func extractJSON(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
// Remove markdown code fences if present
|
||||
if strings.HasPrefix(s, "```") {
|
||||
lines := strings.Split(s, "\n")
|
||||
// Remove first line (```json or ```)
|
||||
if len(lines) > 2 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
// Remove last line (```)
|
||||
if len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "```" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
s = strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
s = strings.TrimSpace(s)
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user