Spaces:
Running
Running
| from multi_agent_sdlc.tools.tester.validation import ProjectVerificationResult | |
| from langchain_core.messages import HumanMessage | |
| from multi_agent_sdlc.models import TesterStatus | |
| from multi_agent_sdlc.models import CoderStatus | |
| from multi_agent_sdlc.models import VerificationCycle | |
| from multi_agent_sdlc.models import TesterSummary | |
| from multi_agent_sdlc.agents.tester.model import tester_llm | |
| from multi_agent_sdlc.agents.tester.context import build_tester_context | |
| from multi_agent_sdlc.agents.tester.prompt import ( | |
| TESTER_CHAT_PROMPT_TEMPLATE, | |
| TESTER_SYSTEM_RULES, | |
| ) | |
| from langchain_core.messages import AIMessage | |
| from multi_agent_sdlc.state import DevState | |
| import json | |
| def tester_node(state: DevState) -> dict: | |
| tester_messages = state.get("tester_messages", []) | |
| tester_status = state["tester_status"] | |
| current_coder_summary = state["current_coder_summary"] | |
| if not tester_messages: | |
| return _initialize_tester_conversation( | |
| state | |
| ) # Summary is sent from here. (Need to fix for the second iteration with the tester) | |
| if tester_status is TesterStatus.TESTING_PENDING: | |
| tester_messages.append( | |
| HumanMessage( | |
| content=( | |
| "The Coder has completed a production implementation or repair " | |
| "cycle. Independently verify the current project state and " | |
| "confirm whether the reported changes resolve the relevant " | |
| "requirements or previously reported defects.\n\n" | |
| "Do not treat the Coder's results as authoritative evidence. " | |
| "Inspect the affected files and rerun the applicable Tester-owned " | |
| "verification.\n\n" | |
| "Latest Coder handoff:\n" | |
| f"{current_coder_summary.model_dump_json(indent=2)}" | |
| ) | |
| ) | |
| ) | |
| response = tester_llm.invoke(tester_messages) | |
| if response.tool_calls: | |
| if response.tool_calls[0]["name"] == "submit_tester_summary": | |
| if len(response.tool_calls) != 1: | |
| raise ValueError("`submit_tester_summary` must be called alone.") | |
| return _process_tester_summary_call(state, response) | |
| return { | |
| "tester_messages": [response], | |
| } | |
| return { | |
| "tester_messages": [ | |
| response, | |
| HumanMessage( | |
| content=( | |
| "Invalid response." | |
| "Call exactly one approved Tester tool, or call " | |
| "`submit_tester_summary` alone." | |
| ) | |
| ), | |
| ], | |
| } | |
| def _initialize_tester_conversation( | |
| state: DevState, | |
| ) -> dict[str, object]: | |
| current_coder_summary = state.get("current_coder_summary") | |
| tester_context = build_tester_context(state) | |
| tester_context["current_coder_summary"] = current_coder_summary.model_dump( | |
| mode="json" | |
| ) | |
| prompt_value = TESTER_CHAT_PROMPT_TEMPLATE.invoke( | |
| { | |
| "tester_rules": TESTER_SYSTEM_RULES, | |
| "tester_context": json.dumps( | |
| tester_context, | |
| indent=2, | |
| ensure_ascii=False, | |
| ), | |
| } | |
| ) | |
| initial_messages = prompt_value.to_messages() | |
| response = tester_llm.invoke(initial_messages) | |
| return { | |
| "tester_messages": [ | |
| *initial_messages, | |
| response, | |
| ], | |
| "tester_status": TesterStatus.TESTING, | |
| } | |
| def _process_tester_summary_call( | |
| state: DevState, | |
| response: AIMessage, | |
| ) -> dict[str, object]: | |
| if len(response.tool_calls) != 1: | |
| raise ValueError("`submit_tester_summary` must be called alone.") | |
| tool_call = response.tool_calls[0] | |
| tester_summary = TesterSummary.model_validate(tool_call["args"]["summary"]) | |
| latest_project_verification = state.get("current_project_verification_result") | |
| if tester_summary.overall_status == "passed": | |
| if latest_project_verification is None or not project_verification_passed( | |
| latest_project_verification | |
| ): | |
| return { | |
| "tester_messages": [ | |
| response, | |
| HumanMessage( | |
| content=( | |
| "The Tester summary cannot report `passed`. " | |
| "Run `tester_run_project_verification` and ensure " | |
| "the complete Ruff, Mypy, and Pytest checks all " | |
| "finish successfully before submitting the " | |
| "summary again." | |
| ) | |
| ), | |
| ], | |
| } | |
| verification_history = state.get( | |
| "verification_history", | |
| [], | |
| ) | |
| verification_cycle = VerificationCycle( | |
| cycle_number=( | |
| verification_history[-1].cycle_number + 1 if verification_history else 1 | |
| ), | |
| tester_summary=tester_summary, | |
| ) | |
| if tester_summary.overall_status == "failed": | |
| tester_status = TesterStatus.REPAIR_REQUIRED | |
| elif tester_summary.overall_status == "blocked": | |
| tester_status = TesterStatus.BLOCKED | |
| elif tester_summary.overall_status == "passed": | |
| tester_status = TesterStatus.PASSED | |
| else: | |
| raise ValueError( | |
| f"Unsupported Tester status: " f"{tester_summary.overall_status!r}" | |
| ) | |
| update: dict[str, object] = { | |
| "tester_messages": [response], | |
| "current_tester_summary": tester_summary, | |
| "verification_history": [verification_cycle], | |
| "tester_status": tester_status, | |
| } | |
| if tester_status == TesterStatus.REPAIR_REQUIRED: | |
| update["coder_status"] = CoderStatus.REPAIRING | |
| return update | |
| def project_verification_passed( | |
| result: ProjectVerificationResult, | |
| ) -> bool: | |
| return ( | |
| result["verification_type"] == "complete_project_verification" | |
| and result["passed"] | |
| and result["overall_exit_code"] == 0 | |
| and all( | |
| not check["timed_out"] and check["exit_code"] == 0 | |
| for check in result["checks"] | |
| ) | |
| ) | |