1+ # Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+ #
3+ # Copyright (C) 2026 Tencent. All rights reserved.
4+ #
5+ # tRPC-Agent-Python is licensed under Apache-2.0.
6+ """Finding deduplication and noise reduction for the code review agent.
7+
8+ Provides:
9+ - Deduplicator: Removes duplicate findings (same file + same line + same category).
10+ - ConfidenceGrader: Classifies findings by confidence level.
11+ """
12+
13+ from __future__ import annotations
14+
15+ from typing import Optional
16+
17+ from .models import Confidence , Finding , Severity
18+
19+
20+ # ──────────────────────────────────────────────
21+ # Confidence scoring helpers
22+ # ──────────────────────────────────────────────
23+
24+ # Severity → numeric weight for tie-breaking
25+ _SEVERITY_WEIGHT : dict [str , int ] = {
26+ "critical" : 5 ,
27+ "high" : 4 ,
28+ "medium" : 3 ,
29+ "low" : 2 ,
30+ "warning" : 1 ,
31+ "info" : 0 ,
32+ }
33+
34+ _CONFIDENCE_ORDER : dict [str , int ] = {
35+ "high" : 3 ,
36+ "medium" : 2 ,
37+ "low" : 1 ,
38+ }
39+
40+
41+ def _finding_score (finding : Finding ) -> int :
42+ """Compute a numeric score for a finding (higher = more important)."""
43+ sev = _SEVERITY_WEIGHT .get (finding .severity .value , 0 )
44+ conf = _CONFIDENCE_ORDER .get (finding .confidence .value , 0 )
45+ return sev * 10 + conf
46+
47+
48+ # ──────────────────────────────────────────────
49+ # 6.1 + 6.2: Deduplicator
50+ # ──────────────────────────────────────────────
51+
52+
53+ class Deduplicator :
54+ """Deduplicates findings and classifies them by confidence.
55+
56+ Dedup rule: same file + same line + same category → keep only one (highest confidence).
57+ If confidence ties, keep the one with highest severity.
58+ """
59+
60+ def deduplicate (self , findings : list [Finding ]) -> list [Finding ]:
61+ """Remove duplicate findings.
62+
63+ Two findings are considered duplicates if they share the same
64+ ``file``, ``line``, and ``category``. Only the highest-scoring
65+ finding is retained.
66+
67+ Args:
68+ findings: List of findings to deduplicate.
69+
70+ Returns:
71+ Deduplicated list of findings.
72+ """
73+ if not findings :
74+ return []
75+
76+ # Group by dedup key
77+ groups : dict [str , list [Finding ]] = {}
78+ for f in findings :
79+ key = f"{ f .file } :{ f .line } :{ f .category .value } "
80+ groups .setdefault (key , []).append (f )
81+
82+ # Keep the best finding per group
83+ result : list [Finding ] = []
84+ for key , group in groups .items ():
85+ best = max (group , key = _finding_score )
86+ best .dedup_key = key
87+ result .append (best )
88+
89+ return result
90+
91+ def classify (
92+ self ,
93+ findings : list [Finding ],
94+ high_threshold : float = 0.7 ,
95+ warning_threshold : float = 0.5 ,
96+ review_threshold : float = 0.3 ,
97+ ) -> tuple [list [Finding ], list [Finding ], list [Finding ]]:
98+ """Classify findings into three confidence tiers.
99+
100+ The classification is based on the finding's ``confidence`` field:
101+ - ``high`` → ``findings`` (high-confidence findings)
102+ - ``medium`` → ``warnings`` (medium-confidence, needs attention)
103+ - ``low`` → ``needs_human_review`` (low-confidence, human must verify)
104+
105+ Args:
106+ findings: Deduplicated findings.
107+ high_threshold: Unused (reserved for future numeric scoring).
108+ warning_threshold: Unused (reserved for future numeric scoring).
109+ review_threshold: Unused (reserved for future numeric scoring).
110+
111+ Returns:
112+ Tuple of (high_confidence_findings, warnings, needs_human_review).
113+ """
114+ high_conf : list [Finding ] = []
115+ warnings : list [Finding ] = []
116+ needs_review : list [Finding ] = []
117+
118+ for f in findings :
119+ if f .confidence == Confidence .HIGH :
120+ high_conf .append (f )
121+ elif f .confidence == Confidence .MEDIUM :
122+ warnings .append (f )
123+ else :
124+ needs_review .append (f )
125+
126+ return high_conf , warnings , needs_review
127+
128+ def process (
129+ self ,
130+ findings : list [Finding ],
131+ ) -> tuple [list [Finding ], list [Finding ], list [Finding ]]:
132+ """Full pipeline: deduplicate then classify.
133+
134+ Args:
135+ findings: Raw findings from analysis.
136+
137+ Returns:
138+ Tuple of (findings, warnings, needs_human_review).
139+ """
140+ deduped = self .deduplicate (findings )
141+ return self .classify (deduped )
142+
143+
144+ # Convenience function
145+ def process_findings (
146+ findings : list [Finding ],
147+ ) -> tuple [list [Finding ], list [Finding ], list [Finding ]]:
148+ """Deduplicate and classify findings in one call."""
149+ return Deduplicator ().process (findings )
0 commit comments