Skip to content
Merged

Dev #94

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions prometheus/lang_graph/nodes/bug_reproducing_execute_node.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
import logging
from pathlib import Path
from typing import Optional, Sequence

from langchain.tools import StructuredTool
Expand All @@ -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 = """\
Expand Down Expand Up @@ -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"]
)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions prometheus/lang_graph/nodes/context_extraction_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
} ......]
}
```
Expand Down
100 changes: 73 additions & 27 deletions prometheus/lang_graph/subgraphs/bug_reproduction_subgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -52,21 +73,30 @@ 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(
tools=bug_reproducing_write_node.tools,
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
)
Expand All @@ -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)
Expand All @@ -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
)
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -148,34 +192,44 @@ 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(
self,
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 = {
Expand All @@ -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"],
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down