feat: implement all adapter layers (#2)

Adapters implemented:
- adapters/llm/anthropic.py — Anthropic Claude SDK, capability-based model selection,
  max_tokens + temperature configurable via team.yaml, lazy SDK import
- adapters/vcs/github.py — GitHub PR/branch operations via gh CLI
- adapters/notify/openclaw.py — OpenClaw system event notifications
- adapters/runtime/openclaw.py — OpenClaw sessions_spawn for agent execution
- adapters/runtime/claude_code.py — Claude Code CLI for T4/T5 coding tasks

All adapters follow the abstract base interfaces from Phase 1.
Config-driven model selection via capability_map in team.yaml.
This commit is contained in:
2026-03-16 11:45:11 -04:00
committed by GitHub
parent ce1ce85b87
commit 084cfb0bb2
6 changed files with 742 additions and 126 deletions

View File

@@ -1,16 +1,15 @@
"""
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
from adapters.base.llm import LLMAdapter
@@ -18,27 +17,123 @@ 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:
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:
# 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.
"""
try:
import anthropic as _anthropic
except ModuleNotFoundError as exc:
raise ImportError(
"The 'anthropic' package is required for AnthropicAdapter. "
"Install it with: pip install anthropic"
) from exc
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:
# 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 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:
# 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").
"""
# 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"