Compare commits

..

1 Commits

Author SHA1 Message Date
u80864958
61d538d4a5 feat(pierre): add diff chunking and configurable review settings 2026-02-13 17:00:18 +01:00
4 changed files with 12 additions and 32 deletions

View File

@@ -37,18 +37,13 @@ type LLMConfig struct {
Model string `help:"Model to use" env:"LLM_MODEL"` Model string `help:"Model to use" env:"LLM_MODEL"`
} }
// ReviewConfig holds the reviewspecific CLI options.
// The `default:"60000"` tag sets an integer default of 60KB Kong parses the string value into the int field, which can be confusing for readers.
type ReviewConfig struct { type ReviewConfig struct {
MaxChunkSize int `help:"Maximum diff chunk size in bytes" default:"60000"` MaxChunkChars int `help:"Maximum diff chunk size in characters (default 60000)" default:"60000"`
Guidelines []string `help:"Project guidelines to prepend" sep:","` Guidelines []string `help:"Project-specific review guidelines"`
DisableComments bool `help:"Disable posting comments (dry run)"` DisableComments bool `help:"Do not post comments to the Git provider (dryrun mode)"`
} }
type Config struct { type Config struct {
// Embedding ReviewConfig with a prefix changes flag names to `--review-…`.
// Existing configuration files using the old flag names will need to be updated.
// Consider keeping backwards compatibility if required.
Review ReviewConfig `embed:"" prefix:"review-"` Review ReviewConfig `embed:"" prefix:"review-"`
GitProvider string `help:"Git provider (bitbucket or gitea)" env:"GIT_PROVIDER"` GitProvider string `help:"Git provider (bitbucket or gitea)" env:"GIT_PROVIDER"`
Bitbucket BitbucketConfig `embed:"" prefix:"bitbucket-"` Bitbucket BitbucketConfig `embed:"" prefix:"bitbucket-"`
@@ -129,7 +124,7 @@ func main() {
log.Fatalf("Error initializing AI: %v", err) log.Fatalf("Error initializing AI: %v", err)
} }
pierreService := pierre.New(ai, git, cfg.Review.MaxChunkSize, cfg.Review.Guidelines, cfg.Review.DisableComments) pierreService := pierre.New(ai, git, cfg.Review.MaxChunkChars, cfg.Review.Guidelines, cfg.Review.DisableComments)
if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil { if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil {
log.Fatalf("Error during review: %v", err) log.Fatalf("Error during review: %v", err)
} }

View File

@@ -9,8 +9,6 @@ import (
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
) )
const defaultChunkSize = 60000
type Comment struct { type Comment struct {
File string `json:"file"` File string `json:"file"`
Line int `json:"line"` Line int `json:"line"`
@@ -26,7 +24,7 @@ func (s *Service) judgePR(ctx context.Context, diff io.Reader) (comments []Comme
// Determine chunk size (use default if not set) // Determine chunk size (use default if not set)
maxSize := s.maxChunkSize maxSize := s.maxChunkSize
if maxSize <= 0 { if maxSize <= 0 {
maxSize = defaultChunkSize // default 60KB ~ 15k tokens maxSize = 60000 // default 60KB ~ 15k tokens
} }
chunks := splitDiffIntoChunks(diffBytes, maxSize) chunks := splitDiffIntoChunks(diffBytes, maxSize)
@@ -35,7 +33,7 @@ func (s *Service) judgePR(ctx context.Context, diff io.Reader) (comments []Comme
// Build optional guidelines text // Build optional guidelines text
guidelinesText := "" guidelinesText := ""
if len(s.guidelines) > 0 { if len(s.guidelines) > 0 {
guidelinesText = "Project guidelines:\n" guidelinesText = "\nProject guidelines:\n"
for _, g := range s.guidelines { for _, g := range s.guidelines {
guidelinesText += "- " + g + "\n" guidelinesText += "- " + g + "\n"
} }
@@ -104,13 +102,9 @@ func splitDiffIntoChunks(diff []byte, maxSize int) []string {
// Split further by hunks // Split further by hunks
hunks := strings.Split(seg, "\n@@ ") hunks := strings.Split(seg, "\n@@ ")
for j, h := range hunks { for j, h := range hunks {
var hseg string hseg := h
if j == 0 { if j != 0 {
// First hunk segment already contains the preceding content (including any needed newline) hseg = "@@ " + h
hseg = h
} else {
// Subsequent hunks need the leading newline and "@@ " marker restored
hseg = "\n@@ " + h
} }
if current.Len()+len(hseg) > maxSize && current.Len() > 0 { if current.Len()+len(hseg) > maxSize && current.Len() > 0 {
chunks = append(chunks, current.String()) chunks = append(chunks, current.String())
@@ -135,3 +129,4 @@ func splitDiffIntoChunks(diff []byte, maxSize int) []string {
} }
return chunks return chunks
} }

View File

@@ -12,10 +12,9 @@ import (
) )
// mockChat implements the ChatAdapter interface for testing. // mockChat implements the ChatAdapter interface for testing.
type mockChat struct{ callCount int } type mockChat struct{}
func (m *mockChat) GenerateStructured(ctx context.Context, msgs []chatter.Message, target interface{}) error { func (m *mockChat) GenerateStructured(ctx context.Context, msgs []chatter.Message, target interface{}) error {
m.callCount++
if cSlice, ok := target.(*[]Comment); ok { if cSlice, ok := target.(*[]Comment); ok {
*cSlice = []Comment{{File: "file.go", Line: 1, Message: "test comment"}} *cSlice = []Comment{{File: "file.go", Line: 1, Message: "test comment"}}
return nil return nil
@@ -107,12 +106,11 @@ func TestSplitDiffIntoChunks(t *testing.T) {
} }
func TestJudgePR_ChunkAggregationAndDeduplication(t *testing.T) { func TestJudgePR_ChunkAggregationAndDeduplication(t *testing.T) {
chatMock := &mockChat{}
svc := &Service{ svc := &Service{
maxChunkSize: 50, maxChunkSize: 50,
guidelines: nil, guidelines: nil,
git: &mockGit{}, git: &mockGit{},
chat: chatMock, chat: &mockChat{},
} }
diffReader, err := svc.git.GetDiff(context.Background(), "", "", 0) diffReader, err := svc.git.GetDiff(context.Background(), "", "", 0)
if err != nil { if err != nil {
@@ -126,7 +124,4 @@ func TestJudgePR_ChunkAggregationAndDeduplication(t *testing.T) {
if got, want := len(comments), 1; got != want { if got, want := len(comments), 1; got != want {
t.Fatalf("expected %d comment after deduplication, got %d", want, got) t.Fatalf("expected %d comment after deduplication, got %d", want, got)
} }
if chatMock.callCount != 2 {
t.Fatalf("expected mockChat to be called for each chunk (2), got %d", chatMock.callCount)
}
} }

View File

@@ -7,11 +7,6 @@ import (
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
) )
// Service holds the core collaborators and configuration for Pierre.
// The order of the fields is intentional: configuration fields first (used
// during initialization) followed by the adapters. This prevents accidental
// changes to the serialized layout if encoding/gob or encoding/json is used
// elsewhere in the future.
type Service struct { type Service struct {
maxChunkSize int maxChunkSize int
guidelines []string guidelines []string