Skip to content

Commit d900f39

Browse files
committed
Add normalized patch processing: simplified patch normalization and deduplication
1 parent f123e07 commit d900f39

2 files changed

Lines changed: 388 additions & 0 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Patch Normalization and Selection Node
2+
3+
This module implements simplified patch normalization and direct selection functionality.
4+
Provides standardized patch candidates with direct best patch selection.
5+
"""
6+
7+
import logging
8+
import re
9+
import threading
10+
from collections import defaultdict
11+
from dataclasses import dataclass
12+
from typing import Dict, List
13+
14+
15+
@dataclass
16+
class PatchMetrics:
17+
"""Patch basic metrics"""
18+
occurrence_count: int = 1
19+
20+
21+
@dataclass
22+
class NormalizedPatch:
23+
"""Normalized patch data structure"""
24+
original_index: int
25+
original_content: str
26+
normalized_content: str
27+
metrics: PatchMetrics
28+
29+
30+
class PatchNormalizationNode:
31+
"""Patch Normalization and Direct Selection Node
32+
33+
Implements patch normalization, deduplication and direct best patch selection.
34+
Simplified approach without complex voting mechanisms.
35+
"""
36+
37+
def __init__(self):
38+
self._logger = logging.getLogger(
39+
f"thread-{threading.get_ident()}.prometheus.lang_graph.nodes.patch_normalization_node"
40+
)
41+
42+
def normalize_patch(self, raw_patch: str) -> str:
43+
"""Normalize patch content for deduplication
44+
45+
Removes metadata lines and standardizes formatting to enable
46+
accurate patch comparison and deduplication.
47+
"""
48+
if not raw_patch:
49+
return ""
50+
51+
lines = raw_patch.split('\n')
52+
normalized_lines = []
53+
54+
for line in lines:
55+
# Skip metadata lines
56+
if self._is_metadata_line(line):
57+
continue
58+
59+
# Normalize file paths
60+
if line.startswith('--- ') or line.startswith('+++ '):
61+
line = self._normalize_file_path(line)
62+
63+
normalized_lines.append(line)
64+
65+
return '\n'.join(normalized_lines)
66+
67+
def _is_metadata_line(self, line: str) -> bool:
68+
"""Check if line is metadata that should be ignored"""
69+
metadata_patterns = [
70+
r'^diff --git',
71+
r'^index [a-f0-9]+\.\.[a-f0-9]+',
72+
r'^new file mode \d+',
73+
r'^deleted file mode \d+',
74+
r'^similarity index \d+%',
75+
r'^rename from ',
76+
r'^rename to ',
77+
r'^Binary files ',
78+
]
79+
80+
return any(re.match(pattern, line) for pattern in metadata_patterns)
81+
82+
def _normalize_file_path(self, line: str) -> str:
83+
"""Normalize file path in diff header"""
84+
# Remove timestamp and mode information
85+
line = re.sub(r'\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)? \+\d{4}', '', line)
86+
line = re.sub(r'\s+\d{6}', '', line)
87+
88+
return line
89+
90+
def calculate_patch_metrics(self, normalized_patch: str) -> PatchMetrics:
91+
"""Calculate basic metrics for a patch"""
92+
return PatchMetrics()
93+
94+
95+
def deduplicate_patches(self, patches: List[str]) -> List[NormalizedPatch]:
96+
"""Deduplicate patches using normalization
97+
98+
Returns list of unique normalized patches with occurrence counts.
99+
"""
100+
if not patches:
101+
return []
102+
103+
# Normalize all patches
104+
normalized_patches = []
105+
for i, patch in enumerate(patches):
106+
normalized_content = self.normalize_patch(patch)
107+
metrics = self.calculate_patch_metrics(normalized_content)
108+
109+
normalized_patches.append(NormalizedPatch(
110+
original_index=i,
111+
original_content=patch,
112+
normalized_content=normalized_content,
113+
metrics=metrics
114+
))
115+
116+
# Group by normalized content
117+
patch_groups = defaultdict(list)
118+
for patch in normalized_patches:
119+
patch_groups[patch.normalized_content].append(patch)
120+
121+
# Create deduplicated list with occurrence counts
122+
deduplicated = []
123+
for normalized_content, group in patch_groups.items():
124+
# Use the first patch in the group as representative
125+
representative = group[0]
126+
# Update occurrence count
127+
representative.metrics.occurrence_count = len(group)
128+
deduplicated.append(representative)
129+
130+
self._logger.info(f"Deduplication complete: {len(patches)} -> {len(deduplicated)} unique patches")
131+
132+
return deduplicated
133+
134+
def __call__(self, state: Dict) -> Dict:
135+
"""Node call interface
136+
137+
Process edit_patches in state, return normalized, deduplicated patches and selected best patch
138+
"""
139+
patches = state.get("edit_patches", [])
140+
141+
if not patches:
142+
self._logger.warning("No patches found to process")
143+
return {
144+
"normalized_patches": [],
145+
"final_patch": "",
146+
"original_patch_count": 0,
147+
"unique_patch_count": 0
148+
}
149+
150+
self._logger.info(f"Starting to process {len(patches)} patches")
151+
152+
# Execute deduplication and normalization
153+
normalized_patches = self.deduplicate_patches(patches)
154+
155+
# Return deduplicated patches (selection will be done by final_patch_selection_node)
156+
deduplicated_patches = [patch.original_content for patch in normalized_patches]
157+
158+
self._logger.info(f"Patch processing complete, deduplicated to {len(normalized_patches)} unique patches")
159+
160+
return {
161+
"normalized_patches": normalized_patches,
162+
"edit_patches": deduplicated_patches, # Return deduplicated patches for selection
163+
"original_patch_count": len(patches),
164+
"unique_patch_count": len(normalized_patches)
165+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""Normalized Not Verified Bug Subgraph
2+
3+
This module implements a simplified enhanced issue not verified bug subgraph
4+
with patch normalization and deduplication, using standard final patch selection.
5+
"""
6+
7+
import logging
8+
import threading
9+
from typing import Optional, Sequence, Mapping
10+
11+
import neo4j
12+
from langchain_core.language_models import BaseChatModel
13+
from langgraph.graph import StateGraph, END
14+
15+
from prometheus.knowledge_graph.knowledge_graph import KnowledgeGraph
16+
from prometheus.lang_graph.graphs.issue_state import IssueNotVerifiedBugState
17+
from prometheus.lang_graph.nodes.context_retrieval_subgraph_node import ContextRetrievalSubgraphNode
18+
from prometheus.lang_graph.nodes.edit_message_node import EditMessageNode
19+
from prometheus.lang_graph.nodes.edit_node import EditNode
20+
from prometheus.lang_graph.nodes.git_diff_node import GitDiffNode
21+
from prometheus.lang_graph.nodes.git_reset_node import GitResetNode
22+
from prometheus.lang_graph.nodes.issue_bug_analyzer_message_node import IssueBugAnalyzerMessageNode
23+
from prometheus.lang_graph.nodes.issue_bug_analyzer_node import IssueBugAnalyzerNode
24+
from prometheus.lang_graph.nodes.issue_bug_context_message_node import IssueBugContextMessageNode
25+
from prometheus.lang_graph.nodes.patch_normalization_node import PatchNormalizationNode
26+
from prometheus.lang_graph.nodes.final_patch_selection_node import FinalPatchSelectionNode
27+
from prometheus.lang_graph.nodes.reset_messages_node import ResetMessagesNode
28+
from prometheus.repository.git_repository import GitRepository
29+
from prometheus.container.base_container import BaseContainer
30+
31+
32+
class NormalizedNotVerifiedBugSubgraph:
33+
"""Simplified Enhanced Issue Not Verified Bug Subgraph
34+
35+
Simplified workflow with patch normalization and deduplication:
36+
1. Original context retrieval and bug analysis
37+
2. Patch generation and diff
38+
3. Patch normalization and deduplication
39+
4. Standard final patch selection
40+
"""
41+
42+
def __init__(
43+
self,
44+
advanced_model: BaseChatModel,
45+
base_model: BaseChatModel,
46+
kg: KnowledgeGraph,
47+
git_repo: GitRepository,
48+
neo4j_driver: neo4j.Driver,
49+
max_token_per_neo4j_result: int,
50+
container: Optional[BaseContainer] = None,
51+
):
52+
self._logger = logging.getLogger(
53+
f"thread-{threading.get_ident()}.prometheus.lang_graph.subgraphs.normalized_not_verified_bug_subgraph"
54+
)
55+
56+
# === Initialize Nodes ===
57+
# Context retrieval subgraph node
58+
context_retrieval_subgraph_node = ContextRetrievalSubgraphNode(
59+
advanced_model=advanced_model,
60+
base_model=base_model,
61+
kg=kg,
62+
git_repo=git_repo,
63+
neo4j_driver=neo4j_driver,
64+
max_token_per_neo4j_result=max_token_per_neo4j_result,
65+
container=container,
66+
)
67+
68+
# Issue bug context message node
69+
issue_bug_context_message_node = IssueBugContextMessageNode(
70+
advanced_model=advanced_model,
71+
base_model=base_model,
72+
)
73+
74+
# Issue bug analyzer message node
75+
issue_bug_analyzer_message_node = IssueBugAnalyzerMessageNode(
76+
advanced_model=advanced_model,
77+
base_model=base_model,
78+
)
79+
80+
# Issue bug analyzer node
81+
issue_bug_analyzer_node = IssueBugAnalyzerNode(
82+
advanced_model=advanced_model,
83+
base_model=base_model,
84+
)
85+
86+
# Edit message node
87+
edit_message_node = EditMessageNode(
88+
advanced_model=advanced_model,
89+
base_model=base_model,
90+
)
91+
92+
# Edit node
93+
edit_node = EditNode(
94+
advanced_model=advanced_model,
95+
base_model=base_model,
96+
)
97+
98+
# Git diff node
99+
git_diff_node = GitDiffNode(
100+
git_repo=git_repo,
101+
)
102+
103+
# Git reset node
104+
git_reset_node = GitResetNode(
105+
git_repo=git_repo,
106+
)
107+
108+
# Reset messages nodes
109+
reset_issue_bug_analyzer_messages_node = ResetMessagesNode(
110+
message_key="issue_bug_analyzer_messages"
111+
)
112+
reset_edit_messages_node = ResetMessagesNode(
113+
message_key="edit_messages"
114+
)
115+
116+
# Patch normalization node (only deduplication)
117+
patch_normalization_node = PatchNormalizationNode()
118+
119+
# Final patch selection node (intelligent selection)
120+
final_patch_selection_node = FinalPatchSelectionNode(
121+
model=advanced_model,
122+
max_retries=2
123+
)
124+
125+
# === Build Workflow Graph ===
126+
workflow = StateGraph(IssueNotVerifiedBugState)
127+
128+
# Add nodes
129+
workflow.add_node("context_retrieval_subgraph_node", context_retrieval_subgraph_node)
130+
workflow.add_node("issue_bug_context_message_node", issue_bug_context_message_node)
131+
workflow.add_node("issue_bug_analyzer_message_node", issue_bug_analyzer_message_node)
132+
workflow.add_node("issue_bug_analyzer_node", issue_bug_analyzer_node)
133+
workflow.add_node("edit_message_node", edit_message_node)
134+
workflow.add_node("edit_node", edit_node)
135+
workflow.add_node("git_diff_node", git_diff_node)
136+
workflow.add_node("git_reset_node", git_reset_node)
137+
workflow.add_node("reset_issue_bug_analyzer_messages_node", reset_issue_bug_analyzer_messages_node)
138+
workflow.add_node("reset_edit_messages_node", reset_edit_messages_node)
139+
workflow.add_node("patch_normalization_node", patch_normalization_node)
140+
workflow.add_node("final_patch_selection_node", final_patch_selection_node)
141+
142+
# === Build Workflow Edges ===
143+
# Start with context retrieval
144+
workflow.add_edge("context_retrieval_subgraph_node", "issue_bug_context_message_node")
145+
workflow.add_edge("issue_bug_context_message_node", "issue_bug_analyzer_message_node")
146+
workflow.add_edge("issue_bug_analyzer_message_node", "issue_bug_analyzer_node")
147+
workflow.add_edge("issue_bug_analyzer_node", "edit_message_node")
148+
workflow.add_edge("edit_message_node", "edit_node")
149+
workflow.add_edge("edit_node", "git_diff_node")
150+
151+
# === Decision Point: Continue Generation or Process Patches ===
152+
workflow.add_conditional_edges(
153+
"git_diff_node",
154+
self._routing_logic,
155+
{
156+
"continue_generation": "git_reset_node", # Continue generating more patches
157+
"process_patches": "patch_normalization_node", # Process patches with normalization
158+
}
159+
)
160+
161+
# Continue generating patches - original flow
162+
workflow.add_edge("git_reset_node", "reset_issue_bug_analyzer_messages_node")
163+
workflow.add_edge("reset_issue_bug_analyzer_messages_node", "reset_edit_messages_node")
164+
workflow.add_edge("reset_edit_messages_node", "issue_bug_analyzer_message_node")
165+
166+
# === Patch Processing Flow ===
167+
# Flow: normalization -> final selection -> END
168+
workflow.add_edge("patch_normalization_node", "final_patch_selection_node")
169+
workflow.add_edge("final_patch_selection_node", END)
170+
171+
self.subgraph = workflow.compile()
172+
173+
def _routing_logic(self, state: IssueNotVerifiedBugState) -> str:
174+
"""Routing logic to decide whether to continue generation or process patches"""
175+
patches = state.get("edit_patches", [])
176+
target_patch_count = state.get("number_of_candidate_patch", 1)
177+
current_patch_count = len(patches)
178+
179+
if current_patch_count < target_patch_count:
180+
return "continue_generation"
181+
182+
return "process_patches"
183+
184+
def invoke(
185+
self,
186+
issue_title: str,
187+
issue_body: str,
188+
issue_comments: Sequence[Mapping[str, str]],
189+
number_of_candidate_patch: int,
190+
recursion_limit: int = 500,
191+
):
192+
"""Invoke the subgraph with issue information"""
193+
# Prepare initial state
194+
initial_state = {
195+
"issue_title": issue_title,
196+
"issue_body": issue_body,
197+
"issue_comments": issue_comments,
198+
"number_of_candidate_patch": number_of_candidate_patch,
199+
"edit_patches": [],
200+
"issue_bug_analyzer_messages": [],
201+
"edit_messages": [],
202+
}
203+
204+
# Execute the workflow
205+
output_state = self.subgraph.invoke(
206+
initial_state,
207+
config={"recursion_limit": recursion_limit}
208+
)
209+
210+
# Extract results
211+
result = {
212+
"final_patch": output_state.get("final_patch", ""),
213+
}
214+
215+
# Add patch statistics if available
216+
if "unique_patch_count" in output_state:
217+
result["patch_statistics"] = {
218+
"original_patch_count": output_state.get("original_patch_count", 0),
219+
"unique_patch_count": output_state.get("unique_patch_count", 0),
220+
"deduplication_ratio": output_state.get("unique_patch_count", 0) / max(output_state.get("original_patch_count", 1), 1)
221+
}
222+
223+
return result

0 commit comments

Comments
 (0)