Spaces:
Running on Zero
Running on Zero
| """Provider-neutral model interface and retry helpers.""" | |
| from __future__ import annotations | |
| import time | |
| from collections.abc import Mapping | |
| from dataclasses import dataclass, field, replace | |
| from enum import Enum | |
| from typing import Any, Generic, Protocol, TypeVar | |
| from pydantic import BaseModel | |
| from gcmd_classifier.config import ModelSettings | |
| from gcmd_classifier.errors import ModelRetriesExhaustedError, RetryableModelError | |
| StructuredResponseT = TypeVar("StructuredResponseT", bound=BaseModel) | |
| class ModelStage(str, Enum): # noqa: UP042 | |
| """Supported model-call stages for the MVP classifier.""" | |
| TOPIC = "topic" | |
| TERM = "term" | |
| VARIABLE = "variable" | |
| class TokenUsage: | |
| """Token usage metadata returned by a provider when available.""" | |
| input_tokens: int | None = None | |
| output_tokens: int | None = None | |
| total_tokens: int | None = None | |
| class ModelRequest(Generic[StructuredResponseT]): | |
| """Provider-neutral structured model request.""" | |
| stage: ModelStage | |
| prompt: str | |
| response_schema: type[StructuredResponseT] | |
| provider: str | |
| model_name: str | |
| prompt_version: str | |
| temperature: float | |
| timeout_seconds: float | |
| metadata: Mapping[str, Any] = field(default_factory=dict) | |
| def from_settings( | |
| cls, | |
| *, | |
| stage: ModelStage, | |
| prompt: str, | |
| response_schema: type[StructuredResponseT], | |
| settings: ModelSettings, | |
| metadata: Mapping[str, Any] | None = None, | |
| ) -> ModelRequest[StructuredResponseT]: | |
| """Create a request from model settings for a specific stage.""" | |
| return cls( | |
| stage=stage, | |
| prompt=prompt, | |
| response_schema=response_schema, | |
| provider=settings.provider, | |
| model_name=settings.model_name, | |
| prompt_version=settings.prompt_version_for_stage(stage.value), | |
| temperature=settings.temperature, | |
| timeout_seconds=settings.timeout_seconds, | |
| metadata={} if metadata is None else dict(metadata), | |
| ) | |
| class ModelResponse(Generic[StructuredResponseT]): | |
| """Provider-neutral structured model response.""" | |
| parsed: StructuredResponseT | |
| provider: str | |
| model_name: str | |
| prompt_version: str | |
| retry_count: int = 0 | |
| duration_seconds: float | None = None | |
| token_usage: TokenUsage | None = None | |
| estimated_cost: float | None = None | |
| raw_response: str | None = None | |
| warnings: tuple[str, ...] = () | |
| diagnostics: Mapping[str, Any] = field(default_factory=dict) | |
| class ModelClient(Protocol): | |
| """Provider-neutral interface used by future classification components.""" | |
| def generate_structured( | |
| self, | |
| request: ModelRequest[StructuredResponseT], | |
| ) -> ModelResponse[StructuredResponseT]: | |
| """Return a typed structured response for a model request.""" | |
| class RetryPolicy: | |
| """Retry settings for temporary model failures.""" | |
| max_retries: int = 2 | |
| def from_settings(cls, settings: ModelSettings) -> RetryPolicy: | |
| """Create a retry policy from model settings.""" | |
| return cls(max_retries=settings.max_retries) | |
| def generate_with_retries( | |
| client: ModelClient, | |
| request: ModelRequest[StructuredResponseT], | |
| policy: RetryPolicy, | |
| ) -> ModelResponse[StructuredResponseT]: | |
| """Call a model client, retrying only explicitly retryable failures.""" | |
| retry_count = 0 | |
| started = time.perf_counter() | |
| while True: | |
| try: | |
| response = client.generate_structured(request) | |
| duration = response.duration_seconds | |
| if duration is None: | |
| duration = time.perf_counter() - started | |
| return replace(response, retry_count=retry_count, duration_seconds=duration) | |
| except RetryableModelError as exc: | |
| if retry_count >= policy.max_retries: | |
| raise ModelRetriesExhaustedError( | |
| "Retryable model failure exceeded configured max retries.", | |
| retry_count=retry_count, | |
| ) from exc | |
| retry_count += 1 | |