""" adapters/llm/anthropic.py Anthropic Claude LLM adapter — Phase 2 implementation. Uses the ``anthropic`` SDK to call Claude models. Model selection is driven by the capability_map in team.yaml so the adapter stays provider-agnostic in configuration. """ from __future__ import annotations import os import anthropic from adapters.base.llm import LLMAdapter class AnthropicAdapter(LLMAdapter): """ LLM adapter for Anthropic Claude models. Reads model configuration from the loaded team.yaml config dict:: models: default_max_tokens: 4096 # fallback max_tokens for all calls default_temperature: 0 # fallback temperature for all calls capability_map: reasoning-heavy: anthropic: claude-opus-4-6 capable: anthropic: claude-sonnet-4-6 fast-cheap: anthropic: claude-haiku-3-5 The provider key used when looking up ``capability_map`` is hardcoded to ``"anthropic"`` — the adapter knows its own provider; there is no need for a separate ``models.provider`` config field. Both ``default_max_tokens`` and ``default_temperature`` can be overridden per-call via the ``context`` dict passed to :meth:`complete`. Environment variables --------------------- ANTHROPIC_API_KEY : Required. Authenticates with the Anthropic API. """ def __init__(self, config: dict) -> None: """ Initialise the Anthropic adapter. Parameters ---------- config : Loaded team.yaml config dict. Raises ------ ValueError If ANTHROPIC_API_KEY is not set in the environment. """ self._config = config api_key = os.environ.get("ANTHROPIC_API_KEY") if not api_key: raise ValueError( "ANTHROPIC_API_KEY environment variable is not set. " "Export it before running the-agency." ) self._client = anthropic.Anthropic(api_key=api_key) self._models_cfg: dict = config.get("models", {}) self._default_max_tokens: int = self._models_cfg.get("default_max_tokens", 4096) self._default_temperature: float = self._models_cfg.get("default_temperature", 0) def complete(self, prompt: str, capability: str, context: dict) -> str: """ Send a prompt to a Claude model and return the text response. Parameters ---------- prompt : User-role prompt content. capability : One of "reasoning-heavy" | "capable" | "fast-cheap". context : Optional per-call overrides: system_prompt (str) — prepended as the system turn. max_tokens (int) — defaults to models.default_max_tokens in team.yaml. temperature (float) — defaults to models.default_temperature in team.yaml. Returns ------- The model's text completion as a plain string. """ model = self.resolve_model(capability) max_tokens: int = context.get("max_tokens", self._default_max_tokens) temperature: float = context.get("temperature", self._default_temperature) system_prompt: str = context.get("system_prompt", "") create_kwargs: dict = { "model": model, "max_tokens": max_tokens, "messages": [{"role": "user", "content": prompt}], } if system_prompt: create_kwargs["system"] = system_prompt if temperature != 0.0: create_kwargs["temperature"] = temperature response = self._client.messages.create(**create_kwargs) return response.content[0].text def resolve_model(self, capability: str) -> str: """ Map a capability string to the Anthropic model identifier. Looks up ``config.models.capability_map[capability][provider]``. Falls back to the "capable" tier model if the capability is unknown. Parameters ---------- capability : One of "reasoning-heavy" | "capable" | "fast-cheap". Returns ------- Anthropic model identifier (e.g. "claude-opus-4-6"). """ # The adapter knows its own provider — no need to read it from config. cap_map: dict = self._models_cfg.get("capability_map", {}) if capability in cap_map and "anthropic" in cap_map[capability]: return cap_map[capability]["anthropic"] # Fall back to "capable" tier if "capable" in cap_map and "anthropic" in cap_map["capable"]: return cap_map["capable"]["anthropic"] # Hard-coded last resort return "claude-sonnet-4-6"