mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-08-21 06:34:29 +00:00
feat(review): add --summary flag for project-level comment consolidation
Add a new --summary flag to `ocr review` that generates a project-level summary after all per-file comments are collected. When enabled, an extra LLM call synthesizes the review findings into a structured markdown overview (Top Issues, Cross-Cutting Concerns, Quick Wins, Overall Assessment). Implementation details: - Add PROJECT_SUMMARY_TASK to the review Template struct with dedicated prompt files (system + user) embedded via task_template.json - Add maybeRunProjectSummary() to the review Agent, mirroring the scan agent's existing pattern - Guard against budget-exceeded and context-cancelled states before the summary LLM call - Use rune-based truncation in buildSummaryCommentsList to safely handle multi-byte characters (CJK, emoji) - Output layer requires no changes: existing ProjectSummary() plumbing in text and JSON formats handles the new content automatically When no comments are produced, no summary is generated and existing "Looks good to me" behavior is preserved unchanged.
This commit is contained in:
parent
4b6874bd23
commit
1cca7f45f3
7 changed files with 139 additions and 4 deletions
|
|
@ -49,6 +49,7 @@ type reviewOptions struct {
|
|||
maxTokensBudget int
|
||||
noFilter bool
|
||||
preview bool
|
||||
summary bool
|
||||
}
|
||||
|
||||
var reviewOpts reviewOptions
|
||||
|
|
@ -212,6 +213,7 @@ func executeReviewContext(ctx context.Context, opts reviewOptions) error {
|
|||
SealedInput: sealedInput,
|
||||
MaxTokensBudget: int64(opts.maxTokensBudget),
|
||||
SkipFilter: opts.noFilter,
|
||||
SummaryEnabled: opts.summary,
|
||||
RuntimeConfig: rt.RuntimeConfig,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ func registerReviewFlags(cmd *cobra.Command, opts *reviewOptions) {
|
|||
addProviderFlag(cmd, &opts.provider)
|
||||
addModelFlag(cmd, &opts.model)
|
||||
cmd.Flags().BoolVar(&opts.noFilter, "no-filter", false, "keep all review comments without LLM post-filtering")
|
||||
cmd.Flags().BoolVar(&opts.summary, "summary", false, "generate a project-level summary consolidating all review comments")
|
||||
addPreviewFlag(cmd, &opts.preview)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -154,6 +154,10 @@ type Args struct {
|
|||
// defines one. Set via the --no-filter CLI flag.
|
||||
SkipFilter bool
|
||||
|
||||
// SummaryEnabled enables the post-run PROJECT_SUMMARY_TASK that consolidates
|
||||
// all per-file comments into a project-level summary. Set via --summary.
|
||||
SummaryEnabled bool
|
||||
|
||||
// RuntimeConfig carries the non-secret, allowlisted runtime settings that
|
||||
// identify how this run was configured, for the manifest's
|
||||
// runtime_config_sha256. It is populated by the cmd layer from the resolved
|
||||
|
|
@ -189,6 +193,7 @@ type Agent struct {
|
|||
runner *llmloop.Runner
|
||||
resumeInfo *ResumeInfo
|
||||
budgetExceeded bool // set when a token/tool-call budget gate stopped dispatch
|
||||
projectSummary string
|
||||
|
||||
// inputResolution holds this run's frozen commit endpoints (resolved_base/
|
||||
// head/exact_range), and repoRemoteIdentity the credential-free repository
|
||||
|
|
@ -376,6 +381,10 @@ func (a *Agent) Run(ctx context.Context) ([]model.LlmComment, error) {
|
|||
if len(comments) > 0 {
|
||||
telemetry.RecordCommentsGenerated(ctx, int64(len(comments)))
|
||||
}
|
||||
|
||||
// Step 3: Generate project-level summary if enabled.
|
||||
a.maybeRunProjectSummary(ctx, comments)
|
||||
|
||||
// Join background memory compression before anything freezes run-level
|
||||
// state. Those jobs are cancelled rather than awaited when a conversation
|
||||
// ends, so their LLM request can still be in flight here; a retry report
|
||||
|
|
@ -462,10 +471,9 @@ func (a *Agent) TotalCacheReadTokens() int64 { return a.runner.TotalCacheReadTok
|
|||
// TotalCacheWriteTokens returns the accumulated cache write tokens from all LLM calls.
|
||||
func (a *Agent) TotalCacheWriteTokens() int64 { return a.runner.TotalCacheWriteTokens() }
|
||||
|
||||
// ProjectSummary returns the markdown project-level summary. Always empty
|
||||
// for the diff-review path; defined so *Agent satisfies the
|
||||
// cmd/opencodereview.ResultProvider interface that scan.Agent also implements.
|
||||
func (a *Agent) ProjectSummary() string { return "" }
|
||||
// ProjectSummary returns the markdown project-level summary generated when
|
||||
// --summary is enabled. Empty when summary is disabled or no comments were produced.
|
||||
func (a *Agent) ProjectSummary() string { return a.projectSummary }
|
||||
|
||||
// Warnings returns a copy of non-fatal warnings recorded during review.
|
||||
func (a *Agent) Warnings() []AgentWarning { return a.runner.Warnings() }
|
||||
|
|
@ -1897,3 +1905,82 @@ func BuildToolDefs(entries []toolsconfig.ToolConfigEntry, planOnly bool) []llm.T
|
|||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
// maybeRunProjectSummary runs the PROJECT_SUMMARY_TASK over the collected
|
||||
// comments when --summary is enabled. Best-effort: any error or empty input
|
||||
// silently leaves projectSummary unset.
|
||||
func (a *Agent) maybeRunProjectSummary(ctx context.Context, comments []model.LlmComment) {
|
||||
if !a.args.SummaryEnabled {
|
||||
return
|
||||
}
|
||||
pt := a.args.Template.ProjectSummaryTask
|
||||
if pt == nil || len(pt.Messages) == 0 {
|
||||
return
|
||||
}
|
||||
if len(comments) == 0 {
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil || a.budgetExceeded {
|
||||
return
|
||||
}
|
||||
|
||||
fileSet := make(map[string]struct{}, len(comments))
|
||||
for _, c := range comments {
|
||||
fileSet[c.Path] = struct{}{}
|
||||
}
|
||||
payload := buildSummaryCommentsList(comments)
|
||||
|
||||
messages := make([]llm.Message, 0, len(pt.Messages))
|
||||
for _, m := range pt.Messages {
|
||||
content := m.Content
|
||||
content = strings.ReplaceAll(content, "{{comment_count}}", fmt.Sprintf("%d", len(comments)))
|
||||
content = strings.ReplaceAll(content, "{{file_count}}", fmt.Sprintf("%d", len(fileSet)))
|
||||
content = strings.ReplaceAll(content, "{{all_comments}}", payload)
|
||||
messages = append(messages, llm.NewTextMessage(m.Role, content))
|
||||
}
|
||||
|
||||
const pathKey = "__review_project_summary__"
|
||||
fs := a.session.GetOrCreateFileSession(pathKey)
|
||||
rec := fs.AppendTaskRecord(session.MemoryCompressionTask, messages)
|
||||
ctx = llm.ContextWithSessionKey(ctx,
|
||||
llm.SessionTaskKey(a.session.SessionID, string(session.MemoryCompressionTask), pathKey))
|
||||
startTime := time.Now()
|
||||
|
||||
resp, err := a.args.LLMClient.CompletionsWithCtx(ctx, llm.ChatRequest{
|
||||
Model: a.args.Model,
|
||||
Messages: messages,
|
||||
MaxTokens: a.args.Template.CompletionTokenLimit(),
|
||||
})
|
||||
if err != nil {
|
||||
rec.SetError(err, time.Since(startTime))
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] project summary failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
rec.SetResponse(resp, time.Since(startTime))
|
||||
a.runner.RecordUsage(resp.Usage)
|
||||
|
||||
body := strings.TrimSpace(llmloop.StripMarkdownFences(resp.Content()))
|
||||
if body == "" {
|
||||
return
|
||||
}
|
||||
a.projectSummary = body
|
||||
}
|
||||
|
||||
// buildSummaryCommentsList renders comments as a compact path-anchored
|
||||
// markdown list for embedding in the PROJECT_SUMMARY_TASK prompt.
|
||||
func buildSummaryCommentsList(comments []model.LlmComment) string {
|
||||
const maxRunes = 280
|
||||
var sb strings.Builder
|
||||
for _, c := range comments {
|
||||
sb.WriteString("- `")
|
||||
sb.WriteString(c.Path)
|
||||
sb.WriteString("`: ")
|
||||
oneLine := strings.ReplaceAll(c.Content, "\n", " ")
|
||||
if r := []rune(oneLine); len(r) > maxRunes {
|
||||
oneLine = string(r[:maxRunes]) + "..."
|
||||
}
|
||||
sb.WriteString(oneLine)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
You are a senior code reviewer producing a project-level summary of a diff-based code review.
|
||||
|
||||
You are given the union of per-file review comments generated for a pull request or commit. Your job is to identify cross-cutting patterns and highlight the most important findings. Do NOT restate every individual comment verbatim.
|
||||
|
||||
## Output Format
|
||||
|
||||
Produce a single Markdown document with these sections. Omit any section that has nothing meaningful to say.
|
||||
|
||||
### Top Issues
|
||||
The 3-5 most consequential findings, ranked by impact. Group findings when the same root cause repeats across files. Reference file paths.
|
||||
|
||||
### Cross-Cutting Concerns
|
||||
Patterns that appear across multiple files (e.g. inconsistent error handling, missing validation, repeated unsafe patterns). Cite representative file paths.
|
||||
|
||||
### Quick Wins
|
||||
Low-effort, high-leverage fixes based on the review comments.
|
||||
|
||||
### Overall Assessment
|
||||
One or two sentences on whether the change is ready to merge, needs minor fixes, or has blocking issues.
|
||||
|
||||
## Guidelines
|
||||
- Be concrete; always reference file paths when citing an issue.
|
||||
- Do not include generic praise or filler text.
|
||||
- Keep the summary concise — aim for under 500 words total.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Total comments: {{comment_count}} across {{file_count}} file(s).
|
||||
|
||||
<all_comments>
|
||||
{{all_comments}}
|
||||
</all_comments>
|
||||
|
||||
Produce the project-level summary now.
|
||||
|
|
@ -29,6 +29,12 @@
|
|||
{ "role": "user", "prompt_file": "re_location_task_user.md" }
|
||||
]
|
||||
},
|
||||
"PROJECT_SUMMARY_TASK": {
|
||||
"messages": [
|
||||
{ "role": "system", "prompt_file": "project_summary_task_system.md" },
|
||||
{ "role": "user", "prompt_file": "project_summary_task_user.md" }
|
||||
]
|
||||
},
|
||||
"MAX_TOOL_REQUEST_TIMES": 30,
|
||||
"PLAN_MODE_LINE_THRESHOLD": 50,
|
||||
"MAX_TOKENS": 58888
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type Template struct {
|
|||
PlanModeLineThreshold int `json:"PLAN_MODE_LINE_THRESHOLD"`
|
||||
ReLocationTask *LlmConversation `json:"RE_LOCATION_TASK,omitempty"`
|
||||
ReviewFilterTask *LlmConversation `json:"REVIEW_FILTER_TASK,omitempty"`
|
||||
ProjectSummaryTask *LlmConversation `json:"PROJECT_SUMMARY_TASK,omitempty"`
|
||||
}
|
||||
|
||||
// ScanTemplate holds the full-file scan task template configuration loaded
|
||||
|
|
@ -90,6 +91,7 @@ type templateManifest struct {
|
|||
PlanModeLineThreshold int `json:"PLAN_MODE_LINE_THRESHOLD"`
|
||||
ReLocationTask *manifestConversation `json:"RE_LOCATION_TASK,omitempty"`
|
||||
ReviewFilterTask *manifestConversation `json:"REVIEW_FILTER_TASK,omitempty"`
|
||||
ProjectSummaryTask *manifestConversation `json:"PROJECT_SUMMARY_TASK,omitempty"`
|
||||
}
|
||||
|
||||
func resolveConversation(m manifestConversation) (LlmConversation, error) {
|
||||
|
|
@ -150,6 +152,9 @@ func LoadDefault() (*Template, error) {
|
|||
if tpl.ReviewFilterTask, err = resolveOptionalConversation(m.ReviewFilterTask, "REVIEW_FILTER_TASK"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tpl.ProjectSummaryTask, err = resolveOptionalConversation(m.ProjectSummaryTask, "PROJECT_SUMMARY_TASK"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tpl, nil
|
||||
}
|
||||
|
||||
|
|
@ -188,6 +193,9 @@ func (t *Template) ApplyLanguage(lang string) {
|
|||
applyLanguage(t.PlanTask, instruction)
|
||||
}
|
||||
applyLanguage(&t.MemoryCompressionTask, instruction)
|
||||
if t.ProjectSummaryTask != nil {
|
||||
applyLanguage(t.ProjectSummaryTask, instruction)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyLanguage injects a language directive into all system-role messages
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue