Skip to content

Commit b5bc86e

Browse files
authored
Merge pull request #32 from Andrei-Constantin-Programmer/Explainer-Debacle
Explainer debacle
2 parents 4b2c453 + b8b7739 commit b5bc86e

14 files changed

Lines changed: 461 additions & 75 deletions

File tree

AntiPattern_Remediator/full_repo_workflow.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def process_java_files_with_workflow(file_paths: list, settings, db_manager, pro
108108
"code_review_times": 0,
109109
"msgs": [],
110110
"answer": None,
111-
"current_file_path": file_path # Track current file being processed
111+
"current_file_path": file_path, # Track current file being processed
112+
"explanation_response_raw": None,
113+
"explanation_json": None
112114
}
113115

114116
try:

AntiPattern_Remediator/main.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from colorama import Fore, Style
1010
import os
1111
from pathlib import Path
12+
import json
13+
1214
from full_repo_workflow import run_full_repo_workflow
15+
from workflow.results_manager import save_intermediate_results
1316

1417

1518
def run_code_snippet_workflow(settings, db_manager, prompt_manager, langgraph):
@@ -66,7 +69,10 @@ def run_code_snippet_workflow(settings, db_manager, prompt_manager, langgraph):
6669
"code_review_results": None,
6770
"code_review_times": 0,
6871
"msgs": [],
69-
"answer": None
72+
"answer": None,
73+
"current_file_path": None, # Track current file being processed
74+
"explanation_response_raw": None,
75+
"explanation_json": None,
7076
}
7177

7278
final_state = langgraph.invoke(initial_state)
@@ -78,6 +84,20 @@ def run_code_snippet_workflow(settings, db_manager, prompt_manager, langgraph):
7884
print(f"Refactored code: {'Yes' if final_state.get('refactored_code') else 'No'}")
7985
print(f"Code review results: {final_state.get('code_review_times')}")
8086

87+
# Show explanation from ExplainerAgent
88+
if final_state.get("explanation_json"):
89+
print(Fore.CYAN + "\n=== Explanation (JSON) ===" + Style.RESET_ALL)
90+
print(json.dumps(final_state["explanation_json"], indent=2, ensure_ascii=False))
91+
else:
92+
print(Fore.RED + "\nNo explanation was generated." + Style.RESET_ALL)
93+
94+
save_intermediate_results(
95+
file_path="java_code_snippet",
96+
final_state=final_state,
97+
settings=settings
98+
)
99+
100+
81101

82102
def main():
83103
"""Main function: Choose between code snippet analysis or full repository run"""

AntiPattern_Remediator/src/core/agents/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
from .refactor_strategist import RefactorStrategist
88
from .code_transformer import CodeTransformer
99
from .code_reviewer import CodeReviewerAgent
10+
from .explainer import ExplainerAgent
1011

1112
__all__ = [
12-
"AntipatternScanner",
13+
"AntipatternScanner",
1314
"RefactorStrategist",
1415
"CodeTransformer",
15-
"CodeReviewerAgent"
16+
"CodeReviewerAgent",
17+
"ExplainerAgent"
1618
]

AntiPattern_Remediator/src/core/agents/antipattern_scanner.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class AntipatternScanner:
1717
"""Antipattern scanner agent"""
18-
18+
1919
def __init__(self, tool, model, prompt_manager: PromptManager):
2020
self.prompt_manager = prompt_manager
2121
self.tool = tool
@@ -28,33 +28,37 @@ def retrieve_context(self, state: AgentState):
2828
search_query = f"Java antipatterns code analysis: {state['code'][:50]}"
2929
# Use retriever_tool to get relevant context
3030
context = self.tool.invoke({"query": search_query})
31-
31+
3232
# Get current file path from state
3333
current_file_path = state['current_file_path']
34-
34+
3535
# Extract project key and relative file path from the current file path
3636
project_key = None
3737
relative_file_path = None
38-
38+
3939
if current_file_path:
4040
path_obj = Path(current_file_path)
41-
41+
4242
# Find the repository name (project key) by looking for 'clones' directory
4343
for i, part in enumerate(path_obj.parts):
4444
if part == 'clones' and i + 1 < len(path_obj.parts):
4545
project_key = path_obj.parts[i + 1] # Repository name as project key
4646
# Get the relative path from the repository root
4747
relative_file_path = str(Path(*path_obj.parts[i + 2:]))
4848
break
49-
50-
api = SonarQubeAPI()
51-
print(Fore.CYAN + f"Using SonarQube project: {project_key}, file: {relative_file_path}" + Style.RESET_ALL)
52-
issues = api.get_issues_for_file(project_key=project_key, file_path=relative_file_path)
53-
solutions = []
54-
for issue in issues["issues"]:
55-
solutions.append(api.get_rules_and_fix_method(rule_key=issue['rule']))
56-
state["context"] = {"sonarqube_issues": issues, "search_context": context, "solutions": solutions}
49+
50+
api = SonarQubeAPI()
51+
print(Fore.CYAN + f"Using SonarQube project: {project_key}, file: {relative_file_path}" + Style.RESET_ALL)
52+
issues = api.get_issues_for_file(project_key=project_key, file_path=relative_file_path)
53+
solutions = []
54+
for issue in issues["issues"]:
55+
solutions.append(api.get_rules_and_fix_method(rule_key=issue['rule']))
56+
state["context"] = {"sonarqube_issues": issues, "search_context": context, "solutions": solutions}
57+
else:
58+
state["context"] = {"sonarqube_issues": None, "search_context": context, "solutions": []}
59+
5760
print(Fore.GREEN + f"Successfully retrieved relevant context" + Style.RESET_ALL)
61+
5862
except Exception as e:
5963
print(Fore.RED + f"Error retrieving context: {e}" + Style.RESET_ALL)
6064
state["context"] = "No additional context available due to retrieval error."
@@ -64,7 +68,7 @@ def analyze_antipatterns(self, state: AgentState):
6468
print("Analyzing code for antipatterns...")
6569
try:
6670
prompt_template = self.prompt_manager.get_prompt(self.prompt_manager.ANTIPATTERN_SCANNER)
67-
71+
6872
# Get historical messages from state, or use empty list if none exist
6973
msgs = state.get('msgs', [])
7074

@@ -74,20 +78,19 @@ def analyze_antipatterns(self, state: AgentState):
7478
sonarqube_issues=state['context'].get('solutions', ''),
7579
msgs=msgs
7680
)
77-
81+
7882
response = self.llm.invoke(formatted_messages)
7983
state["antipatterns_scanner_results"] = response.content if hasattr(response, 'content') else str(response)
8084
print(Fore.GREEN + "Analysis completed successfully" + Style.RESET_ALL)
8185
except Exception as e:
8286
print(Fore.RED + f"Error during analysis: {e}" + Style.RESET_ALL)
8387
state["antipatterns_scanner_results"] = f"Error occurred during analysis: {e}"
8488
return state
85-
86-
def display_antipatterns_results(self, state: AgentState):
89+
90+
def display_antipatterns_results(self, state: AgentState):
8791
"""Display the final analysis results"""
8892
print("\nANTIPATTERN ANALYSIS RESULTS")
8993
print("=" * 60)
9094
print(state.get("antipatterns_scanner_results", "No analysis results available."))
9195
print("=" * 60)
9296
return state
93-
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""
2+
ExplainerAgent — full-state returns, collision-safe
3+
- Returns a full state dict but NEVER writes 'code' back to the graph.
4+
- Uses PromptManager if available; otherwise falls back to an inline prompt.
5+
"""
6+
from __future__ import annotations
7+
from typing import Dict, Any
8+
import json
9+
10+
from langchain_core.language_models import BaseLanguageModel
11+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
12+
from langchain_core.prompts import PromptTemplate
13+
from ..prompt import PromptManager
14+
from src.core.utils import extract_first_json
15+
16+
PROMPT_KEY = "explainer"
17+
18+
19+
class ExplainerAgent:
20+
def __init__(self, llm: BaseLanguageModel, prompt_manager: PromptManager):
21+
self.llm = llm
22+
self.prompt_manager = prompt_manager
23+
24+
# Merge helper: return a FULL state but drop keys we must not rewrite.
25+
@staticmethod
26+
def _merge_return_state(state: Dict[str, Any], updates: Dict[str, Any], drop_keys=("code",)) -> Dict[str, Any]:
27+
merged = dict(state)
28+
# don't write to LastValue 'code' to avoid concurrent updates
29+
for k in drop_keys:
30+
if k in merged:
31+
merged.pop(k)
32+
merged.update(updates or {})
33+
return merged
34+
35+
def explain_antipattern(self, state: Dict[str, Any]) -> Dict[str, Any]:
36+
"""Generate explanation JSON for detected antipatterns and refactor."""
37+
print("Preparing to Explain...")
38+
kwargs = dict(
39+
code=state.get("code", ""),
40+
context=state.get("context", ""),
41+
refactored_code=state.get("refactored_code", ""),
42+
refactoring_strategy=state.get("refactoring_strategy_results", ""),
43+
antipattern_name=state.get("antipatterns_scanner_results", "Unknown antipattern"),
44+
antipatterns_json=json.dumps(state.get("antipatterns_json", []), ensure_ascii=False),
45+
msgs=state.get("msgs", []),
46+
)
47+
48+
messages = self._build_messages(**kwargs)
49+
50+
try:
51+
response = self.llm.invoke(messages)
52+
raw = getattr(response, "content", None) or str(response)
53+
except Exception as e:
54+
raw = f"LLM error: {e}"
55+
56+
# Robust parse
57+
try:
58+
parsed = extract_first_json(raw)
59+
except Exception:
60+
parsed = None
61+
62+
if isinstance(parsed, dict):
63+
exp_json = parsed
64+
elif isinstance(parsed, list):
65+
exp_json = {"items": parsed}
66+
else:
67+
exp_json = self._fallback_payload(state)
68+
69+
updates = {
70+
"explanation_response_raw": raw,
71+
"explanation_json": exp_json,
72+
}
73+
# Return FULL state but with 'code' removed to avoid LastValue collision
74+
return self._merge_return_state(state, updates, drop_keys=("code",))
75+
76+
def display_explanation(self, state: Dict[str, Any]) -> Dict[str, Any]:
77+
print("\n=== Explanation (raw) ===\n", state.get("explanation_response_raw", "N/A"))
78+
if state.get("explanation_json"):
79+
print(
80+
"\n=== Explanation (JSON) ===\n",
81+
json.dumps(state["explanation_json"], indent=2, ensure_ascii=False),
82+
)
83+
# Return FULL state but again ensure 'code' isn't echoed back
84+
return self._merge_return_state(state, {}, drop_keys=("code",))
85+
86+
def _build_messages(self, **kwargs) -> Any:
87+
"""
88+
Build a list of messages for the LLM.
89+
Uses the user‑supplied prompt if available; otherwise falls back to an
90+
inline prompt that safely injects JSON strings via placeholders.
91+
"""
92+
if "msgs" not in kwargs or kwargs["msgs"] is None:
93+
kwargs = {**kwargs, "msgs": []}
94+
95+
# Try to get a prompt from the PromptManager
96+
prompt = None
97+
getp = getattr(self.prompt_manager, "get_prompt", None)
98+
if callable(getp):
99+
prompt = getp(PROMPT_KEY)
100+
101+
if prompt is not None:
102+
# The prompt from PromptManager should already be a PromptTemplate
103+
# or a string that can be formatted safely.
104+
try:
105+
return prompt.format_messages(**kwargs)
106+
except KeyError:
107+
# Fall back if the prompt contains unexpected placeholders
108+
pass
109+
110+
# ------------------------------------------------------------------
111+
# Inline fallback – use direct string template instead of nested PromptTemplate
112+
# ------------------------------------------------------------------
113+
schema = {
114+
"items": [{
115+
"antipattern_name": "",
116+
"antipattern_description": "",
117+
"impact": "",
118+
"why_it_is_bad": "",
119+
"how_we_fixed_it": "",
120+
"refactored_code": "",
121+
"summary": ""
122+
}],
123+
"what_changed": [],
124+
"why_better": [],
125+
"principles_applied": [],
126+
"trade_offs": [],
127+
"closing_summary": ""
128+
}
129+
130+
# Prepare the JSON strings that will be inserted via placeholders
131+
json_input = json.dumps(
132+
{k: v for k, v in kwargs.items() if k != "msgs"},
133+
ensure_ascii=False
134+
)
135+
json_schema = json.dumps(schema, ensure_ascii=False)
136+
137+
# Create the ChatPromptTemplate directly with string templates
138+
fallback = ChatPromptTemplate.from_messages([
139+
("system", "Return STRICT JSON only. No commentary."),
140+
("user", "Given inputs (JSON):\n{json_input}\nRespond with STRICT JSON using exactly this schema:\n{json_schema}"),
141+
MessagesPlaceholder("msgs"),
142+
])
143+
144+
# Format the messages with the prepared JSON strings
145+
return fallback.format_messages(
146+
json_input=json_input,
147+
json_schema=json_schema,
148+
msgs=kwargs["msgs"]
149+
)
150+
151+
@staticmethod
152+
def _fallback_payload(state: Dict[str, Any]) -> Dict[str, Any]:
153+
return {
154+
"items": [{
155+
"antipattern_name": state.get("antipattern_name", "Unknown antipattern"),
156+
"antipattern_description": state.get("antipattern_description", ""),
157+
"impact": "",
158+
"why_it_is_bad": "",
159+
"how_we_fixed_it": state.get("refactor_rationale", ""),
160+
"refactored_code": state.get("refactored_code", ""),
161+
"summary": "Auto-generated minimal explanation (parser fallback)."
162+
}],
163+
"what_changed": [],
164+
"why_better": [],
165+
"principles_applied": [],
166+
"trade_offs": [],
167+
"closing_summary": ""
168+
}

0 commit comments

Comments
 (0)