From aaf8a4dee0eb454d5f62311df78acb35b47e79a0 Mon Sep 17 00:00:00 2001 From: Likith Auropro Date: Wed, 17 Jun 2026 11:12:59 +0530 Subject: [PATCH 1/2] Add LLMProvider Protocol, request/response types, and error hierarchy (T040) Introduces iris_engine/contracts/llm_provider.py with the LLMProvider Protocol, LLMRequest, LLMResponse, LLMUsage dataclasses, and the full typed error hierarchy (LLMUnavailable, LLMRateLimited, LLMAuthenticationFailed, LLMSchemaViolation, LLMContextWindowExceeded, LLMContentFiltered, LLMInvalidRequest). No adapter imports; the module is importable in a bare iris-engine install. --- .../src/iris_engine/contracts/llm_provider.py | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/iris-engine/src/iris_engine/contracts/llm_provider.py diff --git a/packages/iris-engine/src/iris_engine/contracts/llm_provider.py b/packages/iris-engine/src/iris_engine/contracts/llm_provider.py new file mode 100644 index 0000000..91e9a57 --- /dev/null +++ b/packages/iris-engine/src/iris_engine/contracts/llm_provider.py @@ -0,0 +1,125 @@ +"""LLMProvider Protocol, request/response types, and typed errors. + +No adapter imports. This module must remain importable without any LLM +dependency installed. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + +from pydantic import BaseModel + +from iris_engine.contracts.ocr_engine import TenantContext + +__all__ = [ + "VALID_ADAPTER_IDS", + "LLMError", + "LLMUnavailable", + "LLMRateLimited", + "LLMAuthenticationFailed", + "LLMSchemaViolation", + "LLMContextWindowExceeded", + "LLMContentFiltered", + "LLMInvalidRequest", + "LLMUsage", + "LLMResponse", + "LLMRequest", + "LLMProvider", + "TenantContext", +] + +VALID_ADAPTER_IDS: frozenset[str] = frozenset({"azure-openai", "openai", "anthropic", "local"}) + + +# ── error hierarchy ─────────────────────────────────────────────────────────── + + +class LLMError(Exception): + """Base for all typed LLM errors.""" + + +class LLMUnavailable(LLMError): + """Network failure, 5xx, or timeout.""" + + +class LLMRateLimited(LLMError): + """429 or quota exhausted.""" + + +class LLMAuthenticationFailed(LLMError): + """401 or 403; invalid or expired credentials.""" + + +class LLMSchemaViolation(LLMError): + """Structured output did not match the requested schema.""" + + +class LLMContextWindowExceeded(LLMError): + """Prompt + max_output_tokens exceeds the model context limit.""" + + +class LLMContentFiltered(LLMError): + """Provider content policy blocked the request.""" + + +class LLMInvalidRequest(LLMError): + """400 from the provider; request is malformed.""" + + +# ── data types ──────────────────────────────────────────────────────────────── + + +@dataclass(frozen=True) +class LLMUsage: + input_tokens: int + output_tokens: int + total_tokens: int # must equal input_tokens + output_tokens + + +@dataclass(frozen=True) +class LLMResponse: + text: str + structured: BaseModel | None # populated when schema was supplied in the request + model: str # model identifier the adapter used + adapter_id: str # one of VALID_ADAPTER_IDS or "in-memory" for the stub + usage: LLMUsage + latency_ms: int + + +@dataclass(frozen=True) +class LLMRequest: + prompt: str + system: str | None = None + schema: type[BaseModel] | None = None + temperature: float = 0.0 + max_output_tokens: int | None = None + stop: list[str] | None = None + model_hint: str | None = None # "extraction" | "classification" | "summary" | "chat" + + +# ── Protocol ────────────────────────────────────────────────────────────────── + + +class LLMProvider(Protocol): + id: str + version: str + + async def complete( + self, + ctx: TenantContext, + request: LLMRequest, + ) -> LLMResponse: + """Call the LLM and return a structured response. + + Raises: + LLMUnavailable: network or service failure. + LLMRateLimited: quota exceeded. + LLMAuthenticationFailed: invalid or expired credentials. + LLMSchemaViolation: structured output did not match the schema. + LLMContextWindowExceeded: prompt exceeds model context limit. + LLMContentFiltered: provider content policy blocked the request. + LLMInvalidRequest: malformed request (400). + """ + ... From 8627f29a9b29e459370c033c413c711134b1ee4c Mon Sep 17 00:00:00 2001 From: Likith Auropro Date: Wed, 17 Jun 2026 16:34:08 +0530 Subject: [PATCH 2/2] Flip T040 checkbox to done in tasks.md --- tasks/004-llm-adapter-set/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/004-llm-adapter-set/tasks.md b/tasks/004-llm-adapter-set/tasks.md index 1cb66aa..8127008 100644 --- a/tasks/004-llm-adapter-set/tasks.md +++ b/tasks/004-llm-adapter-set/tasks.md @@ -6,7 +6,7 @@ ## Sprint 1: Protocol + selector -- [ ] **T040** `[US2] [US8] [size: S] [owner: AuroPro]` Add `iris_engine/contracts/llm_provider.py` with the Protocol, `LLMRequest`, `LLMResponse`, `LLMUsage`, and the error types from the contract document. +- [x] **T040** `[US2] [US8] [size: S] [owner: AuroPro]` Add `iris_engine/contracts/llm_provider.py` with the Protocol, `LLMRequest`, `LLMResponse`, `LLMUsage`, and the error types from the contract document. **Acceptance**: mypy strict passes; the module imports with zero adapter dependencies. - [ ] **T041** `[P] [US1] [size: M] [owner: AuroPro]` Implement `iris_engine/llm/selector.py`. Reads `ProductConfig.adapters.llm`, returns the configured `LLMProvider`. Optional fallback path.