infoxtractor/src/ix/genai/client.py
Dirk Riemann 118a9abd09
All checks were successful
tests / test (push) Successful in 1m0s
tests / test (pull_request) Successful in 1m1s
feat(clients): OCRClient + GenAIClient protocols + fakes (spec §6.2, §6.3)
Adds the two Protocol-based client contracts the pipeline steps depend on,
plus test-oriented fakes. Real engines (Surya, Ollama) land in Chunk 4.

- ix.ocr.client.OCRClient — runtime_checkable Protocol with async ocr().
- ix.genai.client.GenAIClient — runtime_checkable Protocol with async
  invoke(); GenAIInvocationResult + GenAIUsage dataclasses carry the
  parsed model, token usage, and model name.
- FakeOCRClient / FakeGenAIClient: return canned results; both expose a
  raise_on_call hook for error-path tests.

8 unit tests across tests/unit/test_ocr_fake.py + test_genai_fake.py
confirm protocol conformance, canned-return behaviour, usage/model-name
defaults, and raise_on_call propagation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:08:24 +02:00

72 lines
2.1 KiB
Python

"""GenAIClient Protocol + invocation-result dataclasses (spec §6.3).
Structural typing: any object with an async
``invoke(request_kwargs, response_schema) -> GenAIInvocationResult``
method satisfies the Protocol. :class:`~ix.pipeline.genai_step.GenAIStep`
depends on the Protocol; swapping ``FakeGenAIClient`` in tests for
``OllamaClient`` in prod stays a wiring change.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Protocol, runtime_checkable
from pydantic import BaseModel
@dataclass(slots=True)
class GenAIUsage:
"""Token counters returned by the LLM backend.
Both fields default to 0 so fakes / degraded backends can omit them.
"""
prompt_tokens: int = 0
completion_tokens: int = 0
@dataclass(slots=True)
class GenAIInvocationResult:
"""One LLM call's full output.
Attributes
----------
parsed:
The Pydantic instance produced from the model's structured output
(typed as ``Any`` because the concrete class is the response schema
passed into :meth:`GenAIClient.invoke`).
usage:
Token usage counters. Fakes may return a zero-filled
:class:`GenAIUsage`.
model_name:
Echo of the model that served the request. Written to
``ix_result.meta_data['model_name']`` by
:class:`~ix.pipeline.genai_step.GenAIStep`.
"""
parsed: Any
usage: GenAIUsage
model_name: str
@runtime_checkable
class GenAIClient(Protocol):
"""Async LLM backend with structured-output support.
Implementations accept an already-assembled ``request_kwargs`` dict
(messages, model, format, etc.) and a Pydantic class describing the
expected structured-output schema, and return a
:class:`GenAIInvocationResult`.
"""
async def invoke(
self,
request_kwargs: dict[str, Any],
response_schema: type[BaseModel],
) -> GenAIInvocationResult:
"""Run the LLM; parse the response into ``response_schema``; return it."""
...
__all__ = ["GenAIClient", "GenAIInvocationResult", "GenAIUsage"]