Spaces:
Running
Running
| """ | |
| FixFlow Core Agent β Multi-step autonomous bug resolution pipeline. | |
| Pipeline: | |
| Step 1: Issue Understanding β Structured bug summary | |
| Step 2: Codebase Mapping β Ranked list of suspect files | |
| Step 3: Deep Code Analysis β Root cause analysis + reasoning chain | |
| Step 4: Fix Generation β Corrected file contents | |
| Step 5: Diff & Explanation β PR-ready diff + human explanation | |
| """ | |
| import logging | |
| import time | |
| from dataclasses import dataclass, field | |
| from typing import Callable, Dict, Iterator, List, Optional | |
| from backend.config import MAX_FILES_TO_ANALYZE | |
| from backend.llm_client import GLMClient | |
| from backend.github_client import GitHubClient | |
| from backend.code_indexer import ( | |
| build_file_tree_string, | |
| extract_file_paths_from_llm_response, | |
| extract_keywords_from_issue, | |
| format_file_contents_for_prompt, | |
| rank_files_by_keyword_match, | |
| ) | |
| from backend.diff_generator import ( | |
| format_diff_for_display, | |
| generate_all_diffs, | |
| get_diff_stats, | |
| parse_fixed_files_from_llm_response, | |
| ) | |
| from backend.prompts import ( | |
| SYSTEM_MESSAGE, | |
| ISSUE_ANALYSIS_PROMPT, | |
| FILE_RELEVANCE_PROMPT, | |
| ROOT_CAUSE_PROMPT, | |
| FIX_GENERATION_PROMPT, | |
| FIX_EXPLANATION_PROMPT, | |
| CONFIDENCE_EVAL_PROMPT, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # ββ Result Dataclass ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class AgentResult: | |
| """Holds all outputs from the FixFlow pipeline.""" | |
| # Inputs | |
| issue_url: str = "" | |
| repo_url: str = "" | |
| issue_data: Dict = field(default_factory=dict) | |
| # Step outputs | |
| bug_summary: str = "" | |
| relevant_files_analysis: str = "" | |
| suspect_file_paths: List[str] = field(default_factory=list) | |
| root_cause_analysis: str = "" | |
| fix_generation_raw: str = "" | |
| fixed_files: Dict[str, str] = field(default_factory=dict) | |
| diffs: Dict[str, str] = field(default_factory=dict) | |
| diff_formatted: str = "" | |
| fix_explanation: str = "" | |
| confidence_eval: str = "" | |
| # Metadata | |
| step_timings: Dict[str, float] = field(default_factory=dict) | |
| step_errors: Dict[str, str] = field(default_factory=dict) | |
| diff_stats: Dict = field(default_factory=dict) | |
| file_tree: List[Dict] = field(default_factory=list) | |
| original_file_contents: Dict[str, str] = field(default_factory=dict) | |
| # Status callback type | |
| StatusCallback = Optional[Callable[[str, str, str], None]] | |
| # Args: (step_name, status, message) | |
| # status: "running" | "complete" | "error" | "info" | |
| # ββ FixFlow Agent βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class FixFlowAgent: | |
| """ | |
| Orchestrates the full bug-resolution pipeline. | |
| Usage: | |
| agent = FixFlowAgent(glm_client, github_client) | |
| result = agent.run(issue_url, repo_url, on_status=callback) | |
| """ | |
| def __init__( | |
| self, | |
| llm_client: GLMClient, | |
| github_client: GitHubClient, | |
| ): | |
| self.llm = llm_client | |
| self.gh = github_client | |
| # ββ Public entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run( | |
| self, | |
| issue_url: str, | |
| repo_url: str, | |
| on_status: StatusCallback = None, | |
| stream_callback: Optional[Callable[[str], None]] = None, | |
| run_confidence_eval: bool = False, | |
| ) -> AgentResult: | |
| """ | |
| Execute the full FixFlow pipeline. Returns an AgentResult. | |
| Args: | |
| issue_url: Full GitHub issue URL | |
| repo_url: Full GitHub repo URL | |
| on_status: Optional callback(step, status, message) for UI updates | |
| stream_callback: Optional callback(chunk) for streaming LLM output | |
| run_confidence_eval: Whether to run the optional confidence self-eval | |
| """ | |
| result = AgentResult(issue_url=issue_url, repo_url=repo_url) | |
| self._status = on_status or (lambda *a: None) | |
| try: | |
| # ββ Step 0: Fetch GitHub data βββββββββββββββββββββββββββββββββ | |
| self._emit("0_fetch", "running", "Fetching GitHub issue and repository data...") | |
| t0 = time.time() | |
| result.issue_data = self._fetch_issue(issue_url) | |
| result.file_tree = self._fetch_repo_tree(repo_url) | |
| result.step_timings["0_fetch"] = time.time() - t0 | |
| self._emit("0_fetch", "complete", | |
| f"Fetched issue #{result.issue_data['number']} + " | |
| f"{len(result.file_tree)} repo files in " | |
| f"{result.step_timings['0_fetch']:.1f}s") | |
| # ββ Step 1: Issue Understanding βββββββββββββββββββββββββββββββ | |
| self._emit("1_issue", "running", "Analyzing bug report with GLM...") | |
| t1 = time.time() | |
| result.bug_summary = self._step1_issue_understanding( | |
| result.issue_data, None # reasoning step β don't stream to UI | |
| ) | |
| result.step_timings["1_issue"] = time.time() - t1 | |
| self._emit("1_issue", "complete", | |
| f"Bug analysis complete in {result.step_timings['1_issue']:.1f}s") | |
| time.sleep(2) # avoid burst rate-limiting | |
| # ββ Step 2: Codebase Mapping ββββββββββββββββββββββββββββββββββ | |
| self._emit("2_mapping", "running", "Scanning codebase to identify suspect files...") | |
| t2 = time.time() | |
| result.relevant_files_analysis, result.suspect_file_paths = \ | |
| self._step2_codebase_mapping( | |
| result.bug_summary, | |
| result.file_tree, | |
| result.issue_data, | |
| None, # reasoning step β don't stream to UI | |
| repo_url=repo_url, | |
| ) | |
| result.step_timings["2_mapping"] = time.time() - t2 | |
| self._emit("2_mapping", "complete", | |
| f"Identified {len(result.suspect_file_paths)} suspect files in " | |
| f"{result.step_timings['2_mapping']:.1f}s") | |
| time.sleep(2) # avoid burst rate-limiting | |
| # ββ Step 3: Deep Code Analysis ββββββββββββββββββββββββββββββββ | |
| self._emit("3_analysis", "running", | |
| f"Reading {len(result.suspect_file_paths)} files + performing root cause analysis...") | |
| t3 = time.time() | |
| result.original_file_contents = self.gh.fetch_multiple_files( | |
| repo_url, result.suspect_file_paths | |
| ) | |
| result.root_cause_analysis = self._step3_deep_analysis( | |
| result.bug_summary, | |
| result.original_file_contents, | |
| None, # reasoning step β don't stream to UI | |
| ) | |
| result.step_timings["3_analysis"] = time.time() - t3 | |
| self._emit("3_analysis", "complete", | |
| f"Root cause identified in {result.step_timings['3_analysis']:.1f}s") | |
| time.sleep(2) # avoid burst rate-limiting | |
| # ββ Step 4: Fix Generation ββββββββββββββββββββββββββββββββββββ | |
| self._emit("4_fix", "running", "Generating corrected file contents...") | |
| t4 = time.time() | |
| result.fix_generation_raw = self._step4_fix_generation( | |
| result.root_cause_analysis, | |
| result.original_file_contents, | |
| stream_callback, | |
| ) | |
| result.fixed_files = parse_fixed_files_from_llm_response( | |
| result.fix_generation_raw, | |
| result.suspect_file_paths, | |
| ) | |
| result.step_timings["4_fix"] = time.time() - t4 | |
| self._emit("4_fix", "complete", | |
| f"Generated fixes for {len(result.fixed_files)} file(s) in " | |
| f"{result.step_timings['4_fix']:.1f}s") | |
| time.sleep(2) # avoid burst rate-limiting | |
| # ββ Step 5: Diff & Explanation ββββββββββββββββββββββββββββββββ | |
| self._emit("5_diff", "running", "Generating diff and PR explanation...") | |
| t5 = time.time() | |
| result.diffs = generate_all_diffs( | |
| result.original_file_contents, result.fixed_files | |
| ) | |
| result.diff_formatted = format_diff_for_display(result.diffs) | |
| result.diff_stats = get_diff_stats(result.diffs) | |
| result.fix_explanation = self._step5_explanation( | |
| result.bug_summary, | |
| result.root_cause_analysis, | |
| result.diff_formatted, | |
| stream_callback, | |
| ) | |
| result.step_timings["5_diff"] = time.time() - t5 | |
| self._emit("5_diff", "complete", | |
| f"PR explanation ready in {result.step_timings['5_diff']:.1f}s") | |
| # ββ Optional: Confidence Evaluation βββββββββββββββββββββββββββ | |
| if run_confidence_eval: | |
| self._emit("6_confidence", "running", "Running self-evaluation...") | |
| t6 = time.time() | |
| combined = ( | |
| f"# Bug Summary\n{result.bug_summary}\n\n" | |
| f"# Root Cause\n{result.root_cause_analysis}\n\n" | |
| f"# Fix Explanation\n{result.fix_explanation}" | |
| ) | |
| result.confidence_eval = self._run_confidence_eval(combined) # don't stream | |
| result.step_timings["6_confidence"] = time.time() - t6 | |
| self._emit("6_confidence", "complete", | |
| f"Confidence eval done in {result.step_timings['6_confidence']:.1f}s") | |
| except Exception as e: | |
| logger.exception("FixFlow pipeline failed") | |
| step = self._current_step or "unknown" | |
| result.step_errors[step] = str(e) | |
| self._emit(step, "error", f"β Pipeline failed: {e}") | |
| raise | |
| return result | |
| def refine_fix( | |
| self, | |
| feedback: str, | |
| result: AgentResult, | |
| on_status: StatusCallback = None, | |
| stream_callback: Optional[Callable[[str], None]] = None, | |
| ) -> AgentResult: | |
| """ | |
| Re-runs Step 4 and Step 5 by appending user feedback to the existing context. | |
| Modifies and returns the same AgentResult object. | |
| """ | |
| self._status = on_status or (lambda *a: None) | |
| try: | |
| # ββ Refine Fix Generation βββββββββββββββββββββββββββββββββββββ | |
| self._emit("4_refine", "running", "Refining the code based on your feedback...") | |
| t4 = time.time() | |
| # We append the feedback to the root cause to guide the fix generation | |
| refined_root_cause = ( | |
| result.root_cause_analysis + | |
| f"\n\n[USER FEEDBACK ON PREVIOUS FIX]:\n" | |
| f"The user reviewed the proposed fix and said:\n'{feedback}'\n\n" | |
| f"Please update and refine the code correction to satisfy this feedback." | |
| ) | |
| result.fix_generation_raw = self._step4_fix_generation( | |
| refined_root_cause, | |
| result.original_file_contents, | |
| stream_callback, | |
| ) | |
| result.fixed_files = parse_fixed_files_from_llm_response( | |
| result.fix_generation_raw, | |
| result.suspect_file_paths, | |
| ) | |
| result.step_timings["4_refine"] = time.time() - t4 | |
| self._emit("4_refine", "complete", | |
| f"Generated refined fixes for {len(result.fixed_files)} file(s) in " | |
| f"{result.step_timings['4_refine']:.1f}s") | |
| # ββ Regenerate Diff & Explanation βββββββββββββββββββββββββββββ | |
| self._emit("5_diff", "running", "Generating updated diff and PR explanation...") | |
| t5 = time.time() | |
| result.diffs = generate_all_diffs( | |
| result.original_file_contents, result.fixed_files | |
| ) | |
| result.diff_formatted = format_diff_for_display(result.diffs) | |
| result.diff_stats = get_diff_stats(result.diffs) | |
| result.fix_explanation = self._step5_explanation( | |
| result.bug_summary, | |
| refined_root_cause, | |
| result.diff_formatted, | |
| stream_callback, | |
| ) | |
| result.step_timings["5_diff_refined"] = time.time() - t5 | |
| self._emit("5_diff", "complete", | |
| f"Updated PR explanation ready in {result.step_timings['5_diff_refined']:.1f}s") | |
| except Exception as e: | |
| logger.exception("FixFlow refinement failed") | |
| step = self._current_step or "unknown" | |
| result.step_errors[step] = str(e) | |
| self._emit(step, "error", f"β Refinement failed: {e}") | |
| raise | |
| return result | |
| # ββ Pipeline Steps ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _step1_issue_understanding( | |
| self, | |
| issue_data: Dict, | |
| stream_cb: Optional[Callable] = None, | |
| ) -> str: | |
| self._current_step = "1_issue" | |
| comments_text = "" | |
| for c in issue_data.get("comments", [])[:5]: | |
| comments_text += f"**@{c['author']}:** {c['body'][:500]}\n\n" | |
| if not comments_text: | |
| comments_text = "No comments." | |
| prompt = ISSUE_ANALYSIS_PROMPT.format( | |
| title=issue_data.get("title", ""), | |
| body=issue_data.get("body", ""), | |
| labels=", ".join(issue_data.get("labels", [])) or "none", | |
| comments=comments_text, | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_MESSAGE}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| return self._llm_call(messages, stream_cb, temperature=0.2) | |
| def _step2_codebase_mapping( | |
| self, | |
| bug_summary: str, | |
| file_tree: List[Dict], | |
| issue_data: Dict, | |
| stream_cb: Optional[Callable] = None, | |
| repo_url: str = "", | |
| ): | |
| self._current_step = "2_mapping" | |
| # Pre-filter files by keyword match for large repos | |
| keywords = extract_keywords_from_issue(issue_data) | |
| ranked_files = rank_files_by_keyword_match(file_tree, keywords) | |
| tree_string = build_file_tree_string(ranked_files, max_lines=200) | |
| repo_name = repo_url.rstrip("/").split("/")[-2:] | |
| repo_display = "/".join(repo_name) if len(repo_name) == 2 else repo_url | |
| prompt = FILE_RELEVANCE_PROMPT.format( | |
| bug_summary=bug_summary, | |
| file_tree=tree_string, | |
| repo_name=repo_display, | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_MESSAGE}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| analysis = self._llm_call(messages, stream_cb, temperature=0.2) | |
| # Extract actual file paths from the response | |
| paths = extract_file_paths_from_llm_response(analysis) | |
| # Validate against actual tree (only keep paths that exist) | |
| known_paths = {f["path"] for f in file_tree} | |
| valid_paths = [p for p in paths if p in known_paths] | |
| # If LLM hallucinated paths, fall back to keyword-ranked files | |
| if not valid_paths: | |
| logger.warning("LLM returned no valid paths β falling back to keyword ranking") | |
| valid_paths = [f["path"] for f in ranked_files[:MAX_FILES_TO_ANALYZE]] | |
| return analysis, valid_paths[:MAX_FILES_TO_ANALYZE] | |
| def _step3_deep_analysis( | |
| self, | |
| bug_summary: str, | |
| file_contents: Dict[str, str], | |
| stream_cb: Optional[Callable] = None, | |
| ) -> str: | |
| self._current_step = "3_analysis" | |
| formatted = format_file_contents_for_prompt(file_contents) | |
| prompt = ROOT_CAUSE_PROMPT.format( | |
| bug_summary=bug_summary, | |
| file_contents=formatted, | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_MESSAGE}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| return self._llm_call(messages, stream_cb, temperature=0.15, max_tokens=6000) | |
| def _step4_fix_generation( | |
| self, | |
| root_cause: str, | |
| file_contents: Dict[str, str], | |
| stream_cb: Optional[Callable] = None, | |
| ) -> str: | |
| self._current_step = "4_fix" | |
| formatted = format_file_contents_for_prompt(file_contents) | |
| # Build list of filepaths for the placeholder | |
| filepaths = ", ".join(file_contents.keys()) or "affected_file.py" | |
| prompt = FIX_GENERATION_PROMPT.format( | |
| root_cause=root_cause, | |
| file_contents=formatted, | |
| filepath_placeholder=filepaths, | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_MESSAGE}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| return self._llm_call(messages, stream_cb, temperature=0.1, max_tokens=8000) | |
| def _step5_explanation( | |
| self, | |
| bug_summary: str, | |
| root_cause: str, | |
| diff_formatted: str, | |
| stream_cb: Optional[Callable] = None, | |
| ) -> str: | |
| self._current_step = "5_diff" | |
| # Shorten root cause for context | |
| root_cause_summary = root_cause[:2000] + ("..." if len(root_cause) > 2000 else "") | |
| prompt = FIX_EXPLANATION_PROMPT.format( | |
| bug_summary=bug_summary, | |
| root_cause_summary=root_cause_summary, | |
| unified_diff=diff_formatted[:3000], | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_MESSAGE}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| return self._llm_call(messages, stream_cb, temperature=0.3) | |
| def _run_confidence_eval(self, analysis: str, stream_cb: Optional[Callable] = None) -> str: | |
| self._current_step = "6_confidence" | |
| prompt = CONFIDENCE_EVAL_PROMPT.format(analysis=analysis[:4000]) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_MESSAGE}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| return self._llm_call(messages, None, temperature=0.2) | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _llm_call( | |
| self, | |
| messages: List[Dict], | |
| stream_cb: Optional[Callable], | |
| temperature: float = 0.3, | |
| max_tokens: int = 4096, | |
| ) -> str: | |
| """ | |
| Route to streaming or sync call depending on whether a stream callback is provided. | |
| """ | |
| if stream_cb: | |
| full_response = "" | |
| for chunk in self.llm.chat_stream(messages, temperature, max_tokens): | |
| stream_cb(chunk) | |
| full_response += chunk | |
| return full_response | |
| else: | |
| return self.llm.chat(messages, temperature, max_tokens) | |
| def _fetch_issue(self, issue_url: str) -> Dict: | |
| return self.gh.fetch_issue(issue_url) | |
| def _fetch_repo_tree(self, repo_url: str) -> List[Dict]: | |
| return self.gh.fetch_repo_tree(repo_url) | |
| def _emit(self, step: str, status: str, message: str) -> None: | |
| self._status(step, status, message) | |
| logger.info("[%s] %s: %s", step, status.upper(), message) | |
| _current_step: str = "init" | |
| # ββ Wrapper for full report generation βββββββββββββββββββββββββββββββββββββββ | |
| def generate_full_report(result: AgentResult) -> str: | |
| """ | |
| Generate a complete markdown report from an AgentResult. | |
| Suitable for download/export. | |
| """ | |
| total_time = sum(result.step_timings.values()) | |
| stats = result.diff_stats | |
| report = f"""# π§ FixFlow Autonomous Bug Resolution Report | |
| **Issue:** [{result.issue_data.get('title', 'Unknown')}]({result.issue_url}) | |
| **Repository:** {result.repo_url} | |
| **Analysis Date:** {time.strftime('%Y-%m-%d %H:%M UTC')} | |
| **Total Analysis Time:** {total_time:.1f}s | |
| --- | |
| ## π Step 1: Bug Summary | |
| {result.bug_summary} | |
| --- | |
| ## π Step 2: Relevant Files Analysis | |
| {result.relevant_files_analysis} | |
| **Files Selected for Analysis:** | |
| {chr(10).join(f'- `{p}`' for p in result.suspect_file_paths)} | |
| --- | |
| ## π¬ Step 3: Root Cause Analysis | |
| {result.root_cause_analysis} | |
| --- | |
| ## π§ Step 4: Proposed Fix | |
| **Diff Statistics:** | |
| - Files changed: {stats.get('files_changed', 0)} | |
| - Lines added: +{stats.get('lines_added', 0)} | |
| - Lines removed: -{stats.get('lines_removed', 0)} | |
| {result.diff_formatted} | |
| --- | |
| ## π Step 5: Fix Explanation (PR Description) | |
| {result.fix_explanation} | |
| --- | |
| {f"## π― Confidence Evaluation{chr(10)}{result.confidence_eval}{chr(10)}{chr(10)}---{chr(10)}" if result.confidence_eval else ""} | |
| ## β±οΈ Timing Breakdown | |
| | Step | Duration | | |
| |------|----------| | |
| {"".join(f"| {k} | {v:.1f}s |{chr(10)}" for k, v in result.step_timings.items())} | |
| --- | |
| *Generated by FixFlow β Autonomous Bug Resolution Agent powered by GLM 5.1* | |
| """ | |
| return report | |