From 9646a146bca2717c988b78c17e10bc6004a641a6 Mon Sep 17 00:00:00 2001 From: Hans Heinemann Date: Sun, 15 Mar 2026 03:15:01 -0400 Subject: [PATCH] feat(adapter/llm): implement AnthropicAdapter Implements AnthropicAdapter using the anthropic SDK. - Reads ANTHROPIC_API_KEY from env; raises ValueError if missing - resolve_model() looks up capability_map in team.yaml config, falls back to "capable" tier then hard-coded claude-sonnet-4-6 - complete() supports system_prompt, max_tokens (default 4096), and temperature (default 0) via the context dict - Adds PyGithub to requirements.txt (needed by GitHubAdapter) Co-Authored-By: Claude Sonnet 4.6 --- adapters/llm/anthropic.py | 127 +++++++++++++++++++++++++++++++------- requirements.txt | 3 + 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/adapters/llm/anthropic.py b/adapters/llm/anthropic.py index ced33e3..7cd835f 100644 --- a/adapters/llm/anthropic.py +++ b/adapters/llm/anthropic.py @@ -1,16 +1,17 @@ """ adapters/llm/anthropic.py -Anthropic Claude adapter — Phase 2 stub. +Anthropic Claude LLM adapter — Phase 2 implementation. -TODO (Phase 2): - - Implement complete() using the anthropic SDK (anthropic.Anthropic client). - - Implement resolve_model() by reading config/team.yaml capability_map. - - Handle streaming responses, rate-limit retries, and token counting. - - Support system-prompt injection via context["system_prompt"]. - - Map capability → model using the provider's capability_map config. +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 @@ -18,27 +19,105 @@ class AnthropicAdapter(LLMAdapter): """ LLM adapter for Anthropic Claude models. - Reads model configuration from config/team.yaml: - models.provider: anthropic - models.capability_map.reasoning-heavy.anthropic: claude-opus-4-6 - models.capability_map.capable.anthropic: claude-sonnet-4-6 - models.capability_map.fast-cheap.anthropic: claude-haiku-3-5 + Reads model configuration from the loaded team.yaml config dict:: + + models: + provider: anthropic + capability_map: + reasoning-heavy: + anthropic: claude-opus-4-6 + capable: + anthropic: claude-sonnet-4-6 + fast-cheap: + anthropic: claude-haiku-3-5 + + Environment variables + --------------------- + ANTHROPIC_API_KEY : Required. Authenticates with the Anthropic API. """ def __init__(self, config: dict) -> None: - # TODO (Phase 2): Accept loaded team.yaml config dict. - # Extract API key from environment (ANTHROPIC_API_KEY). - # Initialise the anthropic.Anthropic() client. - raise NotImplementedError("AnthropicAdapter.__init__ is not yet implemented.") + """ + 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", {}) def complete(self, prompt: str, capability: str, context: dict) -> str: - # TODO (Phase 2): Call anthropic client messages.create(). - # Use resolve_model(capability) to pick the model. - # Support context keys: system_prompt, max_tokens, temperature. - # Return response text as a plain string. - raise NotImplementedError("AnthropicAdapter.complete is not yet implemented.") + """ + 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 overrides: + system_prompt (str) — prepended as the system turn. + max_tokens (int) — defaults to 4096. + temperature (float) — defaults to 0. + + Returns + ------- + The model's text completion as a plain string. + """ + model = self.resolve_model(capability) + max_tokens: int = context.get("max_tokens", 4096) + temperature: float = context.get("temperature", 0) + 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: + create_kwargs["temperature"] = temperature + + response = self._client.messages.create(**create_kwargs) + return response.content[0].text def resolve_model(self, capability: str) -> str: - # TODO (Phase 2): Look up capability in team.yaml capability_map. - # Fall back to "capable" tier model if capability is unknown. - raise NotImplementedError("AnthropicAdapter.resolve_model is not yet implemented.") + """ + 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"). + """ + provider: str = self._models_cfg.get("provider", "anthropic") + cap_map: dict = self._models_cfg.get("capability_map", {}) + + if capability in cap_map and provider in cap_map[capability]: + return cap_map[capability][provider] + + # Fall back to "capable" tier + if "capable" in cap_map and provider in cap_map["capable"]: + return cap_map["capable"][provider] + + # Hard-coded last resort + return "claude-sonnet-4-6" diff --git a/requirements.txt b/requirements.txt index 0cac1d0..6076b6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,9 @@ pyyaml # Environment variable management python-dotenv +# GitHub VCS adapter +PyGithub + # --- stdlib-only (no pip install needed) --- # sqlite3 — blackboard persistence # dataclasses — task_brief schema