diff --git a/features/auto_debug_graph.feature b/features/auto_debug_graph.feature index e5e9e5f52..c68bb9ee1 100644 --- a/features/auto_debug_graph.feature +++ b/features/auto_debug_graph.feature @@ -9,3 +9,13 @@ Feature: AutoDebug graph JSON parsing Then the provided LLM instance should be used And the fix JSON should populate the current fix And the validation JSON should mark the fix as valid + + Scenario: AutoDebugAgent retries after failed validation + Given a retrying auto debug LLM yields a failed then successful validation + When I run the auto debug agent graph with a retry + Then the agent should retry and capture the failed fix + + Scenario: AutoDebugAgent exposes async invoke and streaming interfaces + Given a simple auto debug LLM for async calls + When I run the auto debug async interfaces + Then the async auto debug results should complete diff --git a/features/steps/auto_debug_graph_steps.py b/features/steps/auto_debug_graph_steps.py index 0b2a5a892..d8d0d1b4f 100644 --- a/features/steps/auto_debug_graph_steps.py +++ b/features/steps/auto_debug_graph_steps.py @@ -1,6 +1,8 @@ from __future__ import annotations +import asyncio import json +from copy import deepcopy from typing import Any from behave import given, then, when @@ -88,3 +90,163 @@ def step_assert_validation_parsed(context): assert result["success"] is True assert result["fix"] == context.final_state["current_fix"] assert result["attempts"] == 0 + + +@given("a retrying auto debug LLM yields a failed then successful validation") +def step_retrying_llm(context): + context.retry_llm = _SequentialLLM( + [ + "Initial analysis output", + json.dumps( + { + "description": "First fix", + "code": "# first attempt", + "files_to_modify": ["first.py"], + } + ), + json.dumps( + { + "is_valid": False, + "reasoning": "Validation failed", + "issues": ["still broken"], + } + ), + json.dumps( + { + "description": "Second fix", + "code": "# second attempt", + "files_to_modify": ["second.py"], + } + ), + json.dumps( + { + "is_valid": True, + "reasoning": "Validation succeeded", + "issues": [], + } + ), + ] + ) + + +@when("I run the auto debug agent graph with a retry") +def step_run_auto_debug_with_retry(context): + agent = AutoDebugAgent(llm=context.retry_llm, max_fix_attempts=2) + + input_state: AutoDebugState = { + "messages": [], + "context": {}, + "result": None, + "error": None, + "metadata": {}, + "error_message": "NameError: missing value", + "code_context": "value = missing", + "attempted_fixes": [], + "current_fix": {}, + "fix_validated": False, + } + + context.retry_state = agent.invoke(input_state) + + +@then("the agent should retry and capture the failed fix") +def step_assert_retry_behavior(context): + state = context.retry_state + assert state["fix_validated"] is True + assert len(state["attempted_fixes"]) == 1 + assert state["attempted_fixes"][0]["description"] == "First fix" + assert state["current_fix"]["description"] == "Second fix" + result = state.get("result") + assert result is not None + assert result["success"] is True + assert result["attempts"] == 1 + + +@given("a simple auto debug LLM for async calls") +def step_async_llm(context): + context.async_llm_responses = [ + "Async analysis output", + json.dumps( + { + "description": "Async fix", + "code": "# async patched", + "files_to_modify": ["async.py"], + } + ), + json.dumps( + { + "is_valid": True, + "reasoning": "Async validation succeeded", + "issues": [], + } + ), + ] + + +@when("I run the auto debug async interfaces") +def step_run_async_interfaces(context): + base_state: AutoDebugState = { + "messages": [], + "context": {}, + "result": None, + "error": None, + "metadata": {}, + "error_message": "NameError: missing value", + "code_context": "value = missing", + "attempted_fixes": [], + "current_fix": {}, + "fix_validated": False, + } + + def make_agent() -> AutoDebugAgent: + return AutoDebugAgent( + llm=_SequentialLLM(list(context.async_llm_responses)), max_fix_attempts=1 + ) + + async def collect_async_results(): + agent_for_ainvoke = make_agent() + context.async_invoke_state = await agent_for_ainvoke.ainvoke( + deepcopy(base_state) + ) + + agent_for_stream = make_agent() + context.stream_events = list(agent_for_stream.stream(deepcopy(base_state))) + + agent_for_astream = make_agent() + async_events: list[dict[str, Any]] = [] + async for event in agent_for_astream.astream(deepcopy(base_state)): + async_events.append(event) + context.async_stream_events = async_events + + asyncio.run(collect_async_results()) + + +@then("the async auto debug results should complete") +def step_assert_async_results(context): + assert context.async_invoke_state["result"]["success"] is True + + assert context.stream_events + assert isinstance(context.stream_events[-1], dict) + + assert context.async_stream_events + assert isinstance(context.async_stream_events[-1], dict) + + def extract_success(events): + for event in events: + if not isinstance(event, dict): + continue + for value in event.values(): + if not isinstance(value, dict): + continue + result = value.get("result") + if isinstance(result, dict) and "success" in result: + return result.get("success") + return None + + stream_success = extract_success(context.stream_events) + if stream_success is not None: + assert stream_success is True + + async_success = extract_success(context.async_stream_events) + if async_success is not None: + assert async_success is True diff --git a/src/cleveragents/application/services/project_service.py b/src/cleveragents/application/services/project_service.py index ee8477353..8e00d5311 100644 --- a/src/cleveragents/application/services/project_service.py +++ b/src/cleveragents/application/services/project_service.py @@ -4,8 +4,8 @@ This service handles project initialization, configuration, and management. Uses repository pattern and Unit of Work for persistence (ADR-007). """ -from datetime import datetime import os +from datetime import datetime from pathlib import Path from typing import Any