Skip to content

M3.1 — Anthropic client wrapper #17

Description

@zestones

Note

Milestone: M3 — KB Builder Agent
Planning doc: docs/planning/M3-kb-builder/issues.md

Scope. Create backend/agents/anthropic_client.py with:

  • AsyncAnthropic singleton (timeout 60 s, max 2 retries)
  • model_for(use_case) helper — maps use cases to model slugs
  • parse_json_response(response) helper — strips fences, extracts JSON

Important

Audit finding (2026-04-22): anthropic is not in requirements.txt and
ANTHROPIC_API_KEY / ARIA_MODEL are absent from core/config.py.
Both are required before any M3 code can run. Add them in this issue.


1. backend/requirements.txt

Add:

anthropic==0.50.0

Verify the exact latest slug at https://docs.anthropic.com/en/docs/about-claude/models
on implementation day.


2. backend/core/config.py

Add to the Settings class:

# Anthropic
anthropic_api_key: str
aria_model: str = "sonnet"   # "sonnet" | "opus" — switch to "opus" on J6

Both are read from .env / Docker environment automatically by pydantic-settings.


3. backend/agents/anthropic_client.py

Singleton

import logging
from anthropic import AsyncAnthropic
from core.config import get_settings

log = logging.getLogger("aria.anthropic")

_settings = get_settings()
anthropic = AsyncAnthropic(
    api_key=_settings.anthropic_api_key,
    timeout=60.0,
    max_retries=2,
)

Note

Do NOT wrap messages.create(...) in a helper that forces stream=False.
M4.5 (Investigator extended thinking) calls anthropic.messages.create(stream=True)
directly on the singleton. Export the raw object.

model_for()

Warning

The original spec used Literal["dev", "vision", "agent"]. "agent" was
ambiguous — Investigator needs Opus for extended thinking, KB Builder Q&A needs
only Sonnet. Renamed to prevent coupling.

from typing import Literal

_SONNET = "claude-sonnet-4-5"
_OPUS   = "claude-opus-4-7"

def model_for(use_case: Literal["extraction", "vision", "reasoning", "chat"]) -> str:
    """Return the model slug for a given use case.

    - vision     : Opus   — PDF document vision (Opus only)
    - reasoning  : Opus   — Investigator extended thinking (Opus only)
    - extraction : Sonnet — onboarding patch extraction from free text
    - chat       : Sonnet — KB Builder answer_kb_question, interactive Q&A
                           (ALWAYS Sonnet regardless of ARIA_MODEL — calling KB
                           Builder with Opus when ARIA_MODEL=opus would be 10x
                           cost for no benefit)
    """
    if use_case in ("vision", "reasoning"):
        return _OPUS
    return _SONNET

parse_json_response()

Claude often wraps JSON in code fences or adds preamble text. This helper is
used by answer_kb_question (M3.5) and the onboarding patch extractor (M3.3).

import json
import re
from anthropic.types import Message

def parse_json_response(response: Message) -> dict:
    """Extract the first JSON object from a Claude Message.

    Handles raw JSON, ```json fences, and JSON preceded by preamble text.
    Raises ValueError if no valid JSON is found.
    """
    text = next(
        (block.text for block in response.content if hasattr(block, "text")),
        None,
    )
    if not text:
        raise ValueError("no text block in response")

    # 1. Direct parse
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # 2. Strip ```json ... ``` fences
    m = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
    if m:
        try:
            return json.loads(m.group(1).strip())
        except json.JSONDecodeError:
            pass

    # 3. Find first {...} block
    m = re.search(r"\{[\s\S]*\}", text)
    if m:
        try:
            return json.loads(m.group(0))
        except json.JSONDecodeError:
            pass

    raise ValueError(f"could not parse JSON from response: {text[:200]}")

4. Acceptance

  • await anthropic.messages.create(model=model_for("chat"), max_tokens=64, messages=[{"role": "user", "content": "ping"}]) returns a response
  • model_for("vision") always returns claude-opus-4-7
  • model_for("chat") always returns claude-sonnet-4-5 even when ARIA_MODEL=opus
  • parse_json_response correctly extracts JSON from fenced and unfenced responses
  • Missing ANTHROPIC_API_KEY in env raises a clear startup error (Pydantic handles automatically)
  • SDK client has timeout=60.0 and max_retries=2

Metadata

Metadata

Assignees

Labels

agentTouches an Anthropic agent loop or orchestrationbackendChangement on back side

Projects

  • Status
    ✅ Done

Relationships

None yet

Development

No branches or pull requests

Issue actions