SPB Git

spb/wp26_uqo

Public
3.1 KB · 82 lines python
1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""One agent turn: (role, prompt, schema, model) -> AgentTurn, through the provider interface.34Failures (provider error, validation failure after retries, spend cap) are recorded in ``AgentTurn.error``5with ``estimate=None`` / ``critiques=None``; nothing is ever fabricated. ``SpendCapExceeded`` is re-raised6so that the experiment runner stops gracefully.7"""8from __future__ import annotations910import builtins11from typing import Any1213from pydantic import BaseModel1415from ..providers import CostLedger, LLMProvider, get_provider16from ..providers.ledger import SpendCapExceeded17from ..schemas import AgentEstimate, AgentTurn, CritiqueSet18from .roles import Role192021class ProviderPool:22    """One provider adapter per model id (each with its own concurrency semaphore), sharing one ledger."""2324    def __init__(self, ledger: CostLedger | None = None, force_mock: bool | None = None):25        self.ledger = ledger or CostLedger()26        self.force_mock = force_mock27        self._providers: dict[str, LLMProvider] = {}2829    def get(self, model: str) -> LLMProvider:30        if model not in self._providers:31            self._providers[model] = get_provider(model, self.ledger, force_mock=self.force_mock)32        return self._providers[model]3334    @property35    def mode(self) -> str:36        return "mock" if self.force_mock else "live"373839async def run_turn(40    provider: LLMProvider,41    role: Role,42    view_prompt: str,43    schema: type[BaseModel],44    model: str,45    effort: str = "medium",46    round: int = 0,  # noqa: A002 - mirrors AgentTurn.round47    kind: str = "estimate",48    *,49    max_tokens: int = 4096,50    tag: str = "",51) -> AgentTurn:52    """Run one structured call and wrap it as an AgentTurn. Never raises except for SpendCapExceeded."""53    turn = AgentTurn(role=role.id, round=round, kind=kind, model=model, provider=getattr(provider, "name", "unknown"))  # type: ignore[arg-type]54    try:55        resp = await provider.complete(56            system=role.system_prompt(), user=view_prompt, schema=schema, model=model, effort=effort, max_tokens=max_tokens, tag=tag or f"{role.id}:{kind}:{round}"57        )58    except SpendCapExceeded:59        raise60    except Exception as e:  # defensive: provider.complete already catches most errors61        turn.error = f"{type(e).__name__}: {e}"[:500]62        return turn63    turn.input_tokens = int(resp.input_tokens)64    turn.output_tokens = int(resp.output_tokens)65    turn.cached_tokens = int(resp.cached_tokens)66    turn.cost_usd = float(resp.cost_usd)67    turn.latency_s = builtins.round(float(resp.latency_s), 4)68    turn.prompt_hash = resp.prompt_hash69    turn.request_id = resp.request_id70    turn.validation_retries = int(resp.validation_retries)71    turn.error = resp.error72    parsed: Any = resp.parsed73    if parsed is None:74        turn.error = turn.error or "no parsed output"75    elif isinstance(parsed, AgentEstimate):76        turn.estimate = parsed77    elif isinstance(parsed, CritiqueSet):78        turn.critiques = parsed79    else:80        turn.error = f"unexpected schema {type(parsed).__name__}"81    return turn82
Files · main
Menu

Theme