"""Minimal, vendor-neutral tool-using agent loop.

Run with: python3 agent-loop.py
The demo uses a deterministic fake model, so it needs no API key or package.
Replace ScriptedModel.decide with an adapter for your LLM provider.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Protocol


@dataclass(frozen=True)
class Decision:
    kind: str
    tool_name: str | None = None
    arguments: dict[str, Any] | None = None
    answer: str | None = None


class Model(Protocol):
    def decide(self, messages: list[dict[str, Any]]) -> Decision: ...


Tool = Callable[..., str]


def run_agent(
    task: str,
    model: Model,
    tools: dict[str, Tool],
    *,
    max_steps: int = 6,
) -> str:
    messages: list[dict[str, Any]] = [{"role": "user", "content": task}]

    for step in range(1, max_steps + 1):
        decision = model.decide(messages)

        if decision.kind == "final" and decision.answer:
            return decision.answer

        if decision.kind != "tool" or not decision.tool_name:
            raise ValueError(f"invalid decision at step {step}: {decision}")

        tool = tools.get(decision.tool_name)
        if tool is None:
            raise PermissionError(f"tool is not allowed: {decision.tool_name}")

        arguments = decision.arguments or {}
        try:
            observation = tool(**arguments)
        except (TypeError, ValueError) as exc:
            observation = f"tool_error: {type(exc).__name__}: {exc}"

        messages.append(
            {
                "role": "assistant",
                "tool_call": {
                    "name": decision.tool_name,
                    "arguments": arguments,
                },
            }
        )
        messages.append(
            {
                "role": "tool",
                "name": decision.tool_name,
                "content": observation,
            }
        )

    raise RuntimeError(f"agent exceeded max_steps={max_steps}")


class ScriptedModel:
    """Deterministic stand-in for an LLM, useful for tests and examples."""

    def __init__(self) -> None:
        self.turn = 0

    def decide(self, messages: list[dict[str, Any]]) -> Decision:
        self.turn += 1
        if self.turn == 1:
            return Decision(
                kind="tool",
                tool_name="lookup_inventory",
                arguments={"sku": "A-101"},
            )

        observation = messages[-1]["content"]
        return Decision(kind="final", answer=f"在庫照会の結果: {observation}")


def lookup_inventory(sku: str) -> str:
    inventory = {"A-101": 12, "B-205": 0}
    if sku not in inventory:
        raise ValueError("unknown sku")
    return f"{sku} は {inventory[sku]} 個"


def self_test() -> None:
    answer = run_agent(
        "A-101の在庫を確認して",
        ScriptedModel(),
        {"lookup_inventory": lookup_inventory},
    )
    assert answer == "在庫照会の結果: A-101 は 12 個"
    print(answer)


if __name__ == "__main__":
    self_test()
