diff --git a/prometheus/lang_graph/nodes/bug_reproducing_execute_node.py b/prometheus/lang_graph/nodes/bug_reproducing_execute_node.py index 4434984a..e7121965 100644 --- a/prometheus/lang_graph/nodes/bug_reproducing_execute_node.py +++ b/prometheus/lang_graph/nodes/bug_reproducing_execute_node.py @@ -1,5 +1,6 @@ import functools import logging +from pathlib import Path from typing import Optional, Sequence from langchain.tools import StructuredTool @@ -22,10 +23,13 @@ class BugReproducingExecuteNode: figure out what test framework it uses. Rules: -* DO NOT EXECUTE THE WHOLE TEST SUITE. ONLY EXECTUTE THE SINGLE BUG REPRODUCTION TEST FILE. +* DO NOT EXECUTE THE WHOLE TEST SUITE. ONLY EXECUTE THE SINGLE BUG REPRODUCTION TEST FILE. * DO NOT EDIT ANY FILES. -* ASSUME ALL DEPENDECIES ARE INSTALLED. +* DO NOT ASSUME ALL DEPENDENCIES ARE INSTALLED. * STOP TRYING IF THE TEST EXECUTES. + +REMINDER: +* Install dependencies if needed! """ HUMAN_PROMPT = """\ @@ -67,7 +71,7 @@ def _init_tools(self, container: BaseContainer): return tools - def added_test_filename(self, state: BugReproductionState) -> str: + def added_test_filename(self, state: BugReproductionState) -> Path: added_files, modified_file, removed_files = get_updated_files( state["bug_reproducing_patch"] ) @@ -108,7 +112,7 @@ def __call__(self, state: BugReproductionState): message_history = [ self.system_prompt, - self.format_human_message(state, reproduced_bug_file), + self.format_human_message(state, str(reproduced_bug_file)), ] + state["bug_reproducing_execute_messages"] response = self.model_with_tools.invoke(message_history) diff --git a/prometheus/lang_graph/nodes/context_extraction_node.py b/prometheus/lang_graph/nodes/context_extraction_node.py index bd1f7979..a569d8f3 100644 --- a/prometheus/lang_graph/nodes/context_extraction_node.py +++ b/prometheus/lang_graph/nodes/context_extraction_node.py @@ -51,8 +51,8 @@ "context": [{ "reasoning": "1. Query requirement analysis:\n - Query specifically asks about password validation\n - Context provides implementation details for password validation\n2. Extended relevance:\n - This function is essential for understanding how passwords are validated in the system", "relative_path": "pychemia/code/fireball/fireball.py", - "start_line": 270, - "end_line": 293 + "start_line": 270, # Must be greater than or equal to 1 + "end_line": 293 # Must be greater than or equal to start_line } ......] } ``` diff --git a/prometheus/lang_graph/subgraphs/bug_reproduction_subgraph.py b/prometheus/lang_graph/subgraphs/bug_reproduction_subgraph.py index 656c4adc..baa0574a 100644 --- a/prometheus/lang_graph/subgraphs/bug_reproduction_subgraph.py +++ b/prometheus/lang_graph/subgraphs/bug_reproduction_subgraph.py @@ -3,7 +3,6 @@ import neo4j from langchain_core.language_models.chat_models import BaseChatModel -from langgraph.errors import GraphRecursionError from langgraph.graph import END, StateGraph from langgraph.prebuilt import ToolNode, tools_condition @@ -29,6 +28,12 @@ class BugReproductionSubgraph: + """ + This class defines a LangGraph-based state machine that performs automatic bug reproduction + for GitHub issues. It orchestrates context retrieval, patch writing, file editing, + container execution, and feedback-based retry loops to reproduce bugs in codebases. + """ + def __init__( self, advanced_model: BaseChatModel, @@ -40,9 +45,25 @@ def __init__( max_token_per_neo4j_result: int, test_commands: Optional[Sequence[str]] = None, ): + """ + Initialize the bug reproduction pipeline with all necessary parts. + + Args: + advanced_model: More powerful LLM for structured reasoning and synthesis. + base_model: Lighter LLM for simpler tasks (e.g., file selection). + container: Docker-based sandbox for running code. + kg: Codebase knowledge graph used for context retrieval. + git_repo: Git repository interface for codebase manipulation. + neo4j_driver: Neo4j driver used for graph traversal. + max_token_per_neo4j_result: Truncation budget per retrieved context chunk. + test_commands: Optional list of test commands to verify reproduction success. + """ self.git_repo = git_repo + # Step 1: Generate initial system messages based on issue data issue_bug_reproduction_context_message_node = IssueBugReproductionContextMessageNode() + + # Step 2: Retrieve relevant code/documentation context from the knowledge graph context_retrieval_subgraph_node = ContextRetrievalSubgraphNode( base_model, kg, @@ -52,6 +73,7 @@ def __init__( "bug_reproducing_context", ) + # Step 3: Write a patch to reproduce the bug bug_reproducing_write_message_node = BugReproducingWriteMessageNode() bug_reproducing_write_node = BugReproducingWriteNode(advanced_model, kg) bug_reproducing_write_tools = ToolNode( @@ -59,14 +81,22 @@ def __init__( name="bug_reproducing_write_tools", messages_key="bug_reproducing_write_messages", ) + + # Step 4: Edit files if necessary (based on tool calls) bug_reproducing_file_node = BugReproducingFileNode(base_model, kg) bug_reproducing_file_tools = ToolNode( tools=bug_reproducing_file_node.tools, name="bug_reproducing_file_tools", messages_key="bug_reproducing_file_messages", ) + + # Step 5: Create a Git diff from modified files git_diff_node = GitDiffNode(git_repo, "bug_reproducing_patch") + + # Step 6: Update container with modified code update_container_node = UpdateContainerNode(container, git_repo) + + # Step 7: Run test commands to verify bug reproduction bug_reproducing_execute_node = BugReproducingExecuteNode( base_model, container, test_commands ) @@ -75,23 +105,30 @@ def __init__( name="bug_reproducing_execute_tools", messages_key="bug_reproducing_execute_messages", ) + + # Step 8: Decide whether the bug is reproduced or not bug_reproducing_structured_node = BugReproducingStructuredNode(advanced_model) + + # Step 9: Reset state if bug reproduction fails, for retry reset_bug_reproducing_file_messages_node = ResetMessagesNode( "bug_reproducing_file_messages" ) reset_bug_reproducing_execute_messages_node = ResetMessagesNode( "bug_reproducing_execute_messages" ) + + # Step 10: Git reset to revert changes git_reset_node = GitResetNode(git_repo) + # Define the state machine workflow = StateGraph(BugReproductionState) + # Add nodes to the state machine workflow.add_node( "issue_bug_reproduction_context_message_node", issue_bug_reproduction_context_message_node, ) workflow.add_node("context_retrieval_subgraph_node", context_retrieval_subgraph_node) - workflow.add_node("bug_reproducing_write_message_node", bug_reproducing_write_message_node) workflow.add_node("bug_reproducing_write_node", bug_reproducing_write_node) workflow.add_node("bug_reproducing_write_tools", bug_reproducing_write_tools) @@ -102,7 +139,6 @@ def __init__( workflow.add_node("bug_reproducing_execute_node", bug_reproducing_execute_node) workflow.add_node("bug_reproducing_execute_tools", bug_reproducing_execute_tools) workflow.add_node("bug_reproducing_structured_node", bug_reproducing_structured_node) - workflow.add_node( "reset_bug_reproducing_file_messages_node", reset_bug_reproducing_file_messages_node ) @@ -112,13 +148,15 @@ def __init__( ) workflow.add_node("git_reset_node", git_reset_node) + # Define transitions between nodes workflow.set_entry_point("issue_bug_reproduction_context_message_node") workflow.add_edge( "issue_bug_reproduction_context_message_node", "context_retrieval_subgraph_node" ) workflow.add_edge("context_retrieval_subgraph_node", "bug_reproducing_write_message_node") - workflow.add_edge("bug_reproducing_write_message_node", "bug_reproducing_write_node") + + # Handle patch-writing tool usage or fallback workflow.add_conditional_edges( "bug_reproducing_write_node", functools.partial(tools_condition, messages_key="bug_reproducing_write_messages"), @@ -128,6 +166,8 @@ def __init__( }, ) workflow.add_edge("bug_reproducing_write_tools", "bug_reproducing_write_node") + + # Handle file-editing tool usage or fallback workflow.add_conditional_edges( "bug_reproducing_file_node", functools.partial(tools_condition, messages_key="bug_reproducing_file_messages"), @@ -137,8 +177,12 @@ def __init__( }, ) workflow.add_edge("bug_reproducing_file_tools", "bug_reproducing_file_node") + + # Proceed to execution after code is updated workflow.add_edge("git_diff_node", "update_container_node") workflow.add_edge("update_container_node", "bug_reproducing_execute_node") + + # Handle command execution tool usage workflow.add_conditional_edges( "bug_reproducing_execute_node", functools.partial(tools_condition, messages_key="bug_reproducing_execute_messages"), @@ -148,25 +192,23 @@ def __init__( }, ) workflow.add_edge("bug_reproducing_execute_tools", "bug_reproducing_execute_node") + + # Decide whether to stop or retry if bug not reproduced workflow.add_conditional_edges( "bug_reproducing_structured_node", lambda state: state["reproduced_bug"], {True: END, False: "reset_bug_reproducing_file_messages_node"}, ) + # Retry loop: reset messages, revert repo, then go back to rewriting workflow.add_edge( "reset_bug_reproducing_file_messages_node", "reset_bug_reproducing_execute_messages_node", ) - workflow.add_edge( - "reset_bug_reproducing_execute_messages_node", - "git_reset_node", - ) - workflow.add_edge( - "git_reset_node", - "bug_reproducing_write_message_node", - ) + workflow.add_edge("reset_bug_reproducing_execute_messages_node", "git_reset_node") + workflow.add_edge("git_reset_node", "bug_reproducing_write_message_node") + # Compile the full LangGraph subgraph self.subgraph = workflow.compile() def invoke( @@ -174,8 +216,20 @@ def invoke( issue_title: str, issue_body: str, issue_comments: Sequence[Mapping[str, str]], - recursion_limit: int = 50, + recursion_limit: int = 100, ): + """ + Run the bug reproduction subgraph. + + Args: + issue_title: Title of the GitHub issue. + issue_body: Main body text describing the bug. + issue_comments: List of user/system comments for context. + recursion_limit: Max steps before triggering recovery fallback. + + Returns: + Dict with bug reproduction result, modified file (if any), and commands. + """ config = {"recursion_limit": recursion_limit} input_state = { @@ -185,17 +239,9 @@ def invoke( "max_refined_query_loop": 3, } - try: - output_state = self.subgraph.invoke(input_state, config) - return { - "reproduced_bug": output_state["reproduced_bug"], - "reproduced_bug_file": output_state["reproduced_bug_file"], - "reproduced_bug_commands": output_state["reproduced_bug_commands"], - } - except GraphRecursionError: - self.git_repo.reset_repository() - return { - "reproduced_bug": False, - "reproduced_bug_file": "", - "reproduced_bug_commands": "", - } + output_state = self.subgraph.invoke(input_state, config) + return { + "reproduced_bug": output_state["reproduced_bug"], + "reproduced_bug_file": output_state["reproduced_bug_file"], + "reproduced_bug_commands": output_state["reproduced_bug_commands"], + } diff --git a/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py b/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py index 2611402c..497f8151 100644 --- a/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py +++ b/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py @@ -185,7 +185,7 @@ def invoke( run_existing_test: bool, reproduced_bug_file: str, reproduced_bug_commands: Sequence[str], - recursion_limit: int = 80, + recursion_limit: int = 100, ): config = {"recursion_limit": recursion_limit}