"""Reading a plain-language reply as a go-ahead (L5, D1). "Yep, that's it" in reply to a restatement may be taken as `go`, because a wrong reading costs only time. One short model call decides, and it decides conservatively: only a clear yes counts. A question is never a go-ahead and never reaches the model (L5: questions never change state). The driver's model reads for now; the classifier role takes over in M2.6. """ from typing import Any from nisse_dev.canonical import StoredEvent, ToolCall, UserMessage from nisse_dev.llm import LLMClient, LLMError def command_form(text: str) -> str: """How a message reads as a command: case, surrounding whitespace, runs of inner whitespace and trailing '.' or '!' do not matter, so a phone's "Go." is still a go-ahead (#64). Only for matching; what the operator wrote is what gets recorded and passed on.""" return " ".join(text.strip().rstrip(".!").split()).lower() VERDICT = { "type": "function", "function": { "name": "verdict", "description": "Your reading of the operator's reply.", "parameters": { "type": "object", "properties": { "answer": { "type": "string", "enum": ["yes", "no", "unclear"], "description": "yes: a clear go-ahead on the proposal as it " "stands. no: it asks for changes, disagrees or says to wait. " "unclear: anything else.", } }, "required": ["answer"], }, }, } SYSTEM = """You read one reply from an operator to a software agent. The agent put a proposal up for agreement; decide whether the reply clearly agrees to the proposal as it stands, so the agent may start. A reply that asks for any change, asks a question, adds a condition, or is about something else is not a go-ahead. When in doubt, it is not. Answer with the verdict tool.""" def is_question(text: str) -> bool: return text.rstrip().endswith("?") class GoAheadReader: def __init__(self, client: LLMClient, model: str) -> None: self._client = client self._model = model def agrees(self, proposal: str, reply: str, model: str = "") -> bool: """``model``: the work item's pinned classifier (L9).""" if is_question(reply): return False prompt = f"The proposal:\n\n{proposal}\n\nThe operator's reply:\n\n{reply}" try: produced = self._client.complete( [ StoredEvent( id=0, ts="", event=UserMessage(text=prompt, source="go-ahead reader"), actor="orchestrator", ) ], tools=[VERDICT], model=model or self._model, system=SYSTEM, ) except LLMError: return False # unread: the driver reads it as an answer instead return any(_yes(event) for event in produced) def _yes(event: Any) -> bool: return ( isinstance(event, ToolCall) and event.tool == "verdict" and event.arguments.get("answer") == "yes" )