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"]
        )
    )