"""agent.py: a RELIABLE tool-using agent.

Same ReAct loop as before, hardened for production:
  - every model call is wrapped in a timeout + retry-with-backoff (survives rate limits and blips),
  - if the model call still fails after all retries, the agent returns a safe message instead of crashing,
  - a step cap stops runaway loops,
  - risky tools (ones that change the world) must pass a human-approval gate before they run.

The client is injectable, so the lab injects FAULTS (a flaky model, a slow model, a looping model)
and watches each pattern handle them, all offline.
"""

import os
from dotenv import load_dotenv
import anthropic
from reliability import (retry, call_with_deadline, StepLimiter, approval_gate,
                         StepLimitExceeded, ApprovalDenied)

load_dotenv()
MODEL = "claude-opus-4-8"

# Two tools: a safe one and a risky one. Risky tools change the world and need approval.
SAFE_TOOLS = {"multiply"}
RISKY_TOOLS = {"send_email"}

TOOLS = [
    {"name": "multiply", "description": "Multiply two integers.",
     "input_schema": {"type": "object", "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
                      "required": ["a", "b"]}},
    {"name": "send_email", "description": "Send an email (a real-world action).",
     "input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "body": {"type": "string"}},
                      "required": ["to", "body"]}},
]


def multiply(a, b):
    return a * b


def send_email(to, body):
    return f"email sent to {to}"          # synthetic: no email actually leaves the building


def _deny_all(_action):
    return False                          # default approver: refuse risky actions (safe by default)


def run(task, client=None, approver=_deny_all, max_steps=6, timeout=10.0, attempts=3, sleep=None):
    """Run the agent reliably. Returns a dict: {answer, steps, blocked, degraded}."""
    client = client or anthropic.Anthropic()
    limiter = StepLimiter(max_steps=max_steps)
    messages = [{"role": "user", "content": task}]
    blocked = []
    sleep = sleep or __import__("time").sleep

    while True:
        try:
            limiter.tick()
        except StepLimitExceeded as e:
            return {"answer": f"Stopped: {e}", "steps": limiter.count, "blocked": blocked, "degraded": True}

        # reliable model call: timeout + retry-with-backoff; degrade gracefully if it still fails
        def call():
            return call_with_deadline(
                lambda: client.messages.create(model=MODEL, max_tokens=1024, tools=TOOLS, messages=messages),
                seconds=timeout)
        try:
            resp = retry(call, attempts=attempts, sleep=sleep)
        except Exception as e:
            return {"answer": f"Service unavailable, please try again later. ({type(e).__name__})",
                    "steps": limiter.count, "blocked": blocked, "degraded": True}

        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason == "tool_use":
            results = []
            for block in resp.content:
                if getattr(block, "type", None) != "tool_use":
                    continue
                risky = block.name in RISKY_TOOLS
                try:
                    approval_gate(f"{block.name}({block.input})", approver, is_risky=risky)
                except ApprovalDenied:
                    blocked.append(block.name)
                    results.append({"type": "tool_result", "tool_use_id": block.id,
                                    "content": "BLOCKED: this action needs human approval and was not approved."})
                    continue
                out = multiply(**block.input) if block.name == "multiply" else send_email(**block.input)
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(out)})
            messages.append({"role": "user", "content": results})
            continue

        answer = "".join(b.text for b in resp.content if getattr(b, "type", None) == "text")
        return {"answer": answer, "steps": limiter.count, "blocked": blocked, "degraded": False}


if __name__ == "__main__":
    # By default risky actions are denied; pass approver=lambda a: True to allow them.
    print(run("What is 23 times 17? Use the multiply tool."))
