Skip to content

Commit

Permalink
feat(autofix): Refactor plan+code into a Solution step and a Code Cha…
Browse files Browse the repository at this point in the history
…nges step (#1870)

Adds a Solution step that auto-runs after the Root Cause step and
outputs a similar timeline. This is internally the same as the Plan part
of the old Plan+Code. This also makes the Plan+Code step just Code.

The frontend PR should go in first.
  • Loading branch information
roaga authored Feb 5, 2025
1 parent d31fb5a commit cf8ca04
Show file tree
Hide file tree
Showing 29 changed files with 952 additions and 616 deletions.
7 changes: 5 additions & 2 deletions src/seer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@
get_autofix_state_from_pr_id,
receive_user_message,
restart_from_point_with_feedback,
run_autofix_coding,
run_autofix_evaluation,
run_autofix_execution,
run_autofix_push_changes,
run_autofix_root_cause,
run_autofix_solution,
update_code_change,
)
from seer.automation.codebase.models import RepoAccessCheckRequest, RepoAccessCheckResponse
Expand Down Expand Up @@ -176,7 +177,9 @@ def autofix_update_endpoint(
data: AutofixUpdateRequest,
) -> AutofixEndpointResponse:
if data.payload.type == AutofixUpdateType.SELECT_ROOT_CAUSE:
run_autofix_execution(data)
run_autofix_solution(data)
elif data.payload.type == AutofixUpdateType.SELECT_SOLUTION:
run_autofix_coding(data)
elif data.payload.type == AutofixUpdateType.CREATE_PR:
run_autofix_push_changes(data)
elif data.payload.type == AutofixUpdateType.CREATE_BRANCH:
Expand Down
206 changes: 35 additions & 171 deletions src/seer/automation/autofix/components/coding/component.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging

from langfuse.decorators import observe
Expand All @@ -7,23 +6,21 @@

from seer.automation.agent.agent import AgentConfig, RunConfig
from seer.automation.agent.client import AnthropicProvider, LlmClient, OpenAiProvider
from seer.automation.agent.models import Message, ToolCall
from seer.automation.agent.models import Message
from seer.automation.autofix.autofix_agent import AutofixAgent
from seer.automation.autofix.autofix_context import AutofixContext
from seer.automation.autofix.components.coding.models import (
CodingOutput,
CodingRequest,
FileMissingObj,
PlanStepsPromptXml,
SimpleChangeOutputXml,
)
from seer.automation.autofix.components.coding.prompts import CodingPrompts
from seer.automation.autofix.components.coding.utils import (
task_to_file_change,
task_to_file_create,
task_to_file_delete,
)
from seer.automation.autofix.components.root_cause.models import RootCauseAnalysisItem
from seer.automation.autofix.tools import BaseTools
from seer.automation.component import BaseComponent
from seer.automation.models import FileChange
Expand Down Expand Up @@ -88,111 +85,15 @@ def _handle_missing_file_changes(
for change in changes:
self._append_file_change(repo_client.repo_external_id, change)

@observe(name="Simple fixer")
@ai_track(description="Simple fixer")
@inject
def _handle_simple_fix(
self,
request: CodingRequest,
memory: list[Message],
append_initial_prompt: bool = True,
):
state = self.context.state.get()

agent = AutofixAgent(
config=AgentConfig(interactive=True),
memory=memory,
context=self.context,
name="Plan+Code Simple fixer",
)
response = agent.run(
run_config=RunConfig(
model=AnthropicProvider.model("claude-3-5-sonnet-v2@20241022"),
system_prompt=CodingPrompts.format_system_msg(has_tools=False),
prompt=(
CodingPrompts.format_single_simple_change_msg(
event=request.event_details.format_event(),
root_cause=request.root_cause_and_fix,
summary=request.summary,
repo_names=[repo.full_name for repo in state.request.repos],
original_instruction=request.original_instruction,
root_cause_extra_instruction=request.root_cause_extra_instruction,
)
if append_initial_prompt
else CodingPrompts.format_single_simple_change_msg_formatting_instructions() # when we have initial memory, in case the previous step was the complex fix, we tell the model of the simple fix output format requirements
),
temperature=0.0,
run_name="Simple fixer",
)
)

if not response:
raise RuntimeError("No response from simple fixer llm call")

self.context.store_memory("plan_and_code", agent.memory)

output = SimpleChangeOutputXml.from_xml(
f"<output>{escape_multi_xml(response, ['unified_diff', 'description', 'commit_message'])}</output>"
)

return CodingOutput(
tasks=[file_change.to_plan_task_model() for file_change in output.file_changes]
)

def _prefill_initial_memory(self, request: CodingRequest) -> list[Message]:
def _prefill_initial_memory(self) -> list[Message]:
memory: list[Message] = []

if isinstance(request.root_cause_and_fix, RootCauseAnalysisItem):
expanded_files_messages = []
relevant_files = list(
{
(event.relevant_code_file.file_path, event.relevant_code_file.repo_name): {
"file_path": event.relevant_code_file.file_path,
"repo_name": event.relevant_code_file.repo_name,
}
for event in (
request.root_cause_and_fix.root_cause_reproduction
if request.root_cause_and_fix.root_cause_reproduction
else []
)
if event.relevant_code_file
}.values()
)

for i, file in enumerate(relevant_files):
file_content = (
self.context.get_file_contents(
path=file["file_path"], repo_name=file["repo_name"]
)
if file["file_path"]
else None
)
if file_content:
agent_message = Message(
role="tool_use",
content=f"Expand document: {file['file_path']} in {file['repo_name']}",
tool_calls=[
ToolCall(
id=str(i),
function="expand_document",
args=json.dumps(
{"file_path": file["file_path"], "repo_name": file["repo_name"]}
),
)
],
)
user_message = Message(
role="tool",
content=file_content,
tool_call_id=str(i),
)
memory.append(agent_message)
memory.append(user_message)
expanded_files_messages.append(agent_message)
expanded_files_messages.append(user_message)
solution_memory = self.context.get_memory("solution")
if solution_memory:
memory.extend(solution_memory)

with self.context.state.update() as cur:
cur.steps[-1].initial_memory_length = len(expanded_files_messages) + 1
with self.context.state.update() as cur:
cur.steps[-1].initial_memory_length = len(memory) + 1

return memory

Expand All @@ -215,9 +116,10 @@ class IsObviousOutput(BaseModel):
prompt=CodingPrompts.format_is_obvious_msg(
summary=request.summary,
event_details=request.event_details,
root_cause=request.root_cause_and_fix,
root_cause=request.root_cause,
original_instruction=request.original_instruction,
root_cause_extra_instruction=request.root_cause_extra_instruction,
custom_solution=request.solution if isinstance(request.solution, str) else None,
),
model=OpenAiProvider.model("gpt-4o-mini"),
response_format=IsObviousOutput,
Expand Down Expand Up @@ -250,93 +152,55 @@ class NeedToSearchCodebaseOutput(BaseModel):

return False

@observe(name="Plan+Code")
@ai_track(description="Plan+Code")
@observe(name="Code")
@ai_track(description="Code")
def invoke(self, request: CodingRequest) -> CodingOutput | None:
with BaseTools(self.context) as tools:

memory = request.initial_memory

is_obvious = False

if not memory:
memory = self._prefill_initial_memory(request)
memory = self._prefill_initial_memory()
is_obvious = self._is_obvious(request, memory)
else:
is_obvious = self._is_feedback_obvious(memory)

agent = AutofixAgent(
tools=tools.get_tools(),
tools=tools.get_tools() if not is_obvious else None,
config=AgentConfig(interactive=True),
memory=memory,
context=self.context,
name="Plan+Code",
name="Code",
)

if is_obvious:
coding_output = self._handle_simple_fix(
request, memory, append_initial_prompt=not bool(request.initial_memory)
)
else:
state = self.context.state.get()

if not request.initial_memory:
agent.memory.insert(
0,
Message(
role="user",
content=CodingPrompts.format_fix_discovery_msg(
event=request.event_details.format_event(),
root_cause=request.root_cause_and_fix,
summary=request.summary,
repo_names=[repo.full_name for repo in state.request.repos],
original_instruction=request.original_instruction,
root_cause_extra_instruction=request.root_cause_extra_instruction,
code_map=request.profile,
has_tools=True,
),
),
)
custom_solution = request.solution if isinstance(request.solution, str) else None

response = agent.run(
run_config=RunConfig(
system_prompt=CodingPrompts.format_system_msg(has_tools=True),
model=AnthropicProvider.model("claude-3-5-sonnet-v2@20241022"),
memory_storage_key="plan_and_code",
run_name="Plan",
response = agent.run(
RunConfig(
system_prompt=CodingPrompts.format_system_msg(has_tools=not is_obvious),
prompt=CodingPrompts.format_fix_msg(
has_tools=not is_obvious, custom_solution=custom_solution
),
)

prev_usage = agent.usage
with self.context.state.update() as cur:
cur.usage += agent.usage

if not response:
self.context.store_memory("plan_and_code", agent.memory)
return None

final_response = agent.run(
RunConfig(
prompt=CodingPrompts.format_fix_msg(),
model=AnthropicProvider.model("claude-3-5-sonnet-v2@20241022"),
memory_storage_key="plan_and_code",
run_name="Code",
),
)
model=AnthropicProvider.model("claude-3-5-sonnet-v2@20241022"),
memory_storage_key="code",
run_name="Code",
),
)

self.context.store_memory("plan_and_code", agent.memory)
self.context.store_memory("code", agent.memory)

with self.context.state.update() as cur:
cur.usage += agent.usage - prev_usage
with self.context.state.update() as cur:
cur.usage += agent.usage

if not final_response:
return None
if not response:
return None

plan_steps_content = extract_text_inside_tags(final_response, "plan_steps")
plan_steps_content = extract_text_inside_tags(response, "plan_steps")

coding_output = PlanStepsPromptXml.from_xml(
f"<plan_steps>{escape_multi_xml(plan_steps_content, ['diff', 'description', 'commit_message'])}</plan_steps>"
).to_model()
coding_output = PlanStepsPromptXml.from_xml(
f"<plan_steps>{escape_multi_xml(plan_steps_content, ['diff', 'description', 'commit_message'])}</plan_steps>"
).to_model()

# We only do this once, if it still errors, we just let it go
missing_files_errors = []
Expand All @@ -359,7 +223,7 @@ def invoke(self, request: CodingRequest) -> CodingOutput | None:
missing_files_errors, file_exist_errors
),
model=AnthropicProvider.model("claude-3-5-sonnet-v2@20241022"),
memory_storage_key="plan_and_code",
memory_storage_key="code",
run_name="Missing File Fix",
),
)
Expand Down
4 changes: 3 additions & 1 deletion src/seer/automation/autofix/components/coding/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
RootCauseAnalysisItem,
TimelineEvent,
)
from seer.automation.autofix.components.solution.models import SolutionTimelineEvent
from seer.automation.autofix.utils import remove_code_backticks
from seer.automation.component import BaseComponentOutput, BaseComponentRequest
from seer.automation.models import EventDetails, Profile, PromptXmlModel
Expand All @@ -17,7 +18,8 @@

class CodingRequest(BaseComponentRequest):
event_details: EventDetails
root_cause_and_fix: RootCauseAnalysisItem | str
root_cause: RootCauseAnalysisItem | str
solution: list[SolutionTimelineEvent] | str
original_instruction: str | None = None
root_cause_extra_instruction: str | None = None
summary: Optional[IssueSummary] = None
Expand Down
Loading

0 comments on commit cf8ca04

Please sign in to comment.