Spaces:
Running
Running
File size: 6,138 Bytes
fccd7f6 96e57e5 01c9843 878b473 ee30fd7 878b473 ee30fd7 878b473 96e57e5 fccd7f6 878b473 0450a06 878b473 96e57e5 01c9843 878b473 01c9843 c6231ab 96e57e5 c6231ab 878b473 c6231ab b40eaae c6231ab 878b473 96e57e5 878b473 96e57e5 878b473 01c9843 878b473 96e57e5 878b473 96e57e5 878b473 01c9843 878b473 96e57e5 fccd7f6 96e57e5 c6231ab 96e57e5 878b473 c6231ab 96e57e5 c6231ab 96e57e5 c6231ab 01c9843 96e57e5 878b473 c6231ab fccd7f6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | 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"]
)
)
|