Compare commits
No commits in common. "2709fb8d6b2be76b30e88da331059fcb5d5c2b2e" and "1344b9ddb48fb833e274db7d36d51d2b86a68e31" have entirely different histories.
2709fb8d6b
...
1344b9ddb4
8 changed files with 0 additions and 348 deletions
|
|
@ -1,18 +0,0 @@
|
||||||
"""GenAI subsystem: protocol + fake client + invocation-result dataclasses.
|
|
||||||
|
|
||||||
Real backends (Ollama, etc.) plug in behind :class:`GenAIClient`. The MVP
|
|
||||||
ships only :class:`FakeGenAIClient` from this package; the real Ollama
|
|
||||||
client lands in Chunk 4.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ix.genai.client import GenAIClient, GenAIInvocationResult, GenAIUsage
|
|
||||||
from ix.genai.fake import FakeGenAIClient
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"FakeGenAIClient",
|
|
||||||
"GenAIClient",
|
|
||||||
"GenAIInvocationResult",
|
|
||||||
"GenAIUsage",
|
|
||||||
]
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
"""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"]
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
"""FakeGenAIClient — returns a canned :class:`GenAIInvocationResult`.
|
|
||||||
|
|
||||||
Used by every pipeline unit test to avoid booting Ollama. The
|
|
||||||
``raise_on_call`` hook lets error-path tests exercise ``IX_002_000``-style
|
|
||||||
code paths without needing a real network error.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from ix.genai.client import GenAIInvocationResult, GenAIUsage
|
|
||||||
|
|
||||||
|
|
||||||
class FakeGenAIClient:
|
|
||||||
"""Satisfies :class:`~ix.genai.client.GenAIClient` structurally.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
parsed:
|
|
||||||
The pre-built model instance returned as ``result.parsed``.
|
|
||||||
usage:
|
|
||||||
Token usage counters. Defaults to a zero-filled
|
|
||||||
:class:`GenAIUsage`.
|
|
||||||
model_name:
|
|
||||||
Echoed on the result. Defaults to ``"fake"``.
|
|
||||||
raise_on_call:
|
|
||||||
If set, :meth:`invoke` raises this exception instead of returning.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
parsed: Any,
|
|
||||||
*,
|
|
||||||
usage: GenAIUsage | None = None,
|
|
||||||
model_name: str = "fake",
|
|
||||||
raise_on_call: BaseException | None = None,
|
|
||||||
) -> None:
|
|
||||||
self._parsed = parsed
|
|
||||||
self._usage = usage if usage is not None else GenAIUsage()
|
|
||||||
self._model_name = model_name
|
|
||||||
self._raise_on_call = raise_on_call
|
|
||||||
|
|
||||||
async def invoke(
|
|
||||||
self,
|
|
||||||
request_kwargs: dict[str, Any],
|
|
||||||
response_schema: type[BaseModel],
|
|
||||||
) -> GenAIInvocationResult:
|
|
||||||
"""Return the canned result or raise the configured error."""
|
|
||||||
if self._raise_on_call is not None:
|
|
||||||
raise self._raise_on_call
|
|
||||||
return GenAIInvocationResult(
|
|
||||||
parsed=self._parsed,
|
|
||||||
usage=self._usage,
|
|
||||||
model_name=self._model_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["FakeGenAIClient"]
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
"""OCR subsystem: protocol + fake client.
|
|
||||||
|
|
||||||
Real engines (Surya, Azure DI, …) plug in behind :class:`OCRClient`. The
|
|
||||||
MVP ships only :class:`FakeOCRClient` from this package; the real Surya
|
|
||||||
client lands in Chunk 4.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ix.ocr.client import OCRClient
|
|
||||||
from ix.ocr.fake import FakeOCRClient
|
|
||||||
|
|
||||||
__all__ = ["FakeOCRClient", "OCRClient"]
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
"""OCRClient Protocol (spec §6.2).
|
|
||||||
|
|
||||||
Structural typing: any object with an async ``ocr(pages) -> OCRResult``
|
|
||||||
method satisfies the Protocol. :class:`~ix.pipeline.ocr_step.OCRStep`
|
|
||||||
depends on the Protocol, not a concrete class, so swapping engines
|
|
||||||
(``FakeOCRClient`` in tests, ``SuryaOCRClient`` in prod) stays a wiring
|
|
||||||
change at the app factory.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Protocol, runtime_checkable
|
|
||||||
|
|
||||||
from ix.contracts import OCRResult, Page
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
|
||||||
class OCRClient(Protocol):
|
|
||||||
"""Async OCR backend.
|
|
||||||
|
|
||||||
Implementations receive the flat page list the pipeline built in
|
|
||||||
:class:`~ix.pipeline.setup_step.SetupStep` and return an
|
|
||||||
:class:`~ix.contracts.OCRResult` with one :class:`~ix.contracts.Page`
|
|
||||||
per input page (in the same order).
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def ocr(self, pages: list[Page]) -> OCRResult:
|
|
||||||
"""Run OCR over the input pages; return the structured result."""
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["OCRClient"]
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
"""FakeOCRClient — returns a canned :class:`OCRResult` for hermetic tests.
|
|
||||||
|
|
||||||
Used by every pipeline unit test to avoid booting Surya / CUDA. The
|
|
||||||
``raise_on_call`` hook lets error-path tests exercise ``IX_002_000``-style
|
|
||||||
code paths without needing to forge network errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ix.contracts import OCRResult, Page
|
|
||||||
|
|
||||||
|
|
||||||
class FakeOCRClient:
|
|
||||||
"""Satisfies :class:`~ix.ocr.client.OCRClient` structurally.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
canned:
|
|
||||||
The :class:`OCRResult` to return from every :meth:`ocr` call.
|
|
||||||
raise_on_call:
|
|
||||||
If set, :meth:`ocr` raises this exception instead of returning.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
canned: OCRResult,
|
|
||||||
*,
|
|
||||||
raise_on_call: BaseException | None = None,
|
|
||||||
) -> None:
|
|
||||||
self._canned = canned
|
|
||||||
self._raise_on_call = raise_on_call
|
|
||||||
|
|
||||||
async def ocr(self, pages: list[Page]) -> OCRResult:
|
|
||||||
"""Return the canned result or raise the configured error."""
|
|
||||||
if self._raise_on_call is not None:
|
|
||||||
raise self._raise_on_call
|
|
||||||
return self._canned
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["FakeOCRClient"]
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
"""Tests for GenAIClient Protocol + FakeGenAIClient (spec §6.3)."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from ix.genai import (
|
|
||||||
FakeGenAIClient,
|
|
||||||
GenAIClient,
|
|
||||||
GenAIInvocationResult,
|
|
||||||
GenAIUsage,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _Schema(BaseModel):
|
|
||||||
foo: str
|
|
||||||
bar: int
|
|
||||||
|
|
||||||
|
|
||||||
class TestProtocolConformance:
|
|
||||||
def test_fake_is_runtime_checkable_as_protocol(self) -> None:
|
|
||||||
client = FakeGenAIClient(parsed=_Schema(foo="x", bar=1))
|
|
||||||
assert isinstance(client, GenAIClient)
|
|
||||||
|
|
||||||
|
|
||||||
class TestReturnsCannedResult:
|
|
||||||
async def test_defaults_populate_usage_and_model(self) -> None:
|
|
||||||
parsed = _Schema(foo="bank", bar=2)
|
|
||||||
client = FakeGenAIClient(parsed=parsed)
|
|
||||||
result = await client.invoke(request_kwargs={"model": "x"}, response_schema=_Schema)
|
|
||||||
assert isinstance(result, GenAIInvocationResult)
|
|
||||||
assert result.parsed is parsed
|
|
||||||
assert isinstance(result.usage, GenAIUsage)
|
|
||||||
assert result.usage.prompt_tokens == 0
|
|
||||||
assert result.usage.completion_tokens == 0
|
|
||||||
assert result.model_name == "fake"
|
|
||||||
|
|
||||||
async def test_explicit_usage_and_model_passed_through(self) -> None:
|
|
||||||
parsed = _Schema(foo="k", bar=3)
|
|
||||||
usage = GenAIUsage(prompt_tokens=10, completion_tokens=20)
|
|
||||||
client = FakeGenAIClient(parsed=parsed, usage=usage, model_name="mock:0.1")
|
|
||||||
result = await client.invoke(request_kwargs={}, response_schema=_Schema)
|
|
||||||
assert result.usage is usage
|
|
||||||
assert result.model_name == "mock:0.1"
|
|
||||||
|
|
||||||
|
|
||||||
class TestRaiseOnCallHook:
|
|
||||||
async def test_raise_on_call_propagates(self) -> None:
|
|
||||||
err = RuntimeError("ollama is down")
|
|
||||||
client = FakeGenAIClient(
|
|
||||||
parsed=_Schema(foo="x", bar=1), raise_on_call=err
|
|
||||||
)
|
|
||||||
with pytest.raises(RuntimeError, match="ollama is down"):
|
|
||||||
await client.invoke(request_kwargs={}, response_schema=_Schema)
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
"""Tests for OCRClient Protocol + FakeOCRClient (spec §6.2)."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ix.contracts import Line, OCRDetails, OCRResult, Page
|
|
||||||
from ix.ocr import FakeOCRClient, OCRClient
|
|
||||||
|
|
||||||
|
|
||||||
def _canned() -> OCRResult:
|
|
||||||
return OCRResult(
|
|
||||||
result=OCRDetails(
|
|
||||||
text="hello world",
|
|
||||||
pages=[
|
|
||||||
Page(
|
|
||||||
page_no=1,
|
|
||||||
width=100.0,
|
|
||||||
height=200.0,
|
|
||||||
lines=[Line(text="hello world", bounding_box=[0, 0, 10, 0, 10, 5, 0, 5])],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
meta_data={"engine": "fake"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestProtocolConformance:
|
|
||||||
def test_fake_is_runtime_checkable_as_protocol(self) -> None:
|
|
||||||
client = FakeOCRClient(canned=_canned())
|
|
||||||
assert isinstance(client, OCRClient)
|
|
||||||
|
|
||||||
|
|
||||||
class TestReturnsCannedResult:
|
|
||||||
async def test_returns_exact_canned_result(self) -> None:
|
|
||||||
canned = _canned()
|
|
||||||
client = FakeOCRClient(canned=canned)
|
|
||||||
result = await client.ocr(pages=[])
|
|
||||||
assert result is canned
|
|
||||||
assert result.result.text == "hello world"
|
|
||||||
assert result.meta_data == {"engine": "fake"}
|
|
||||||
|
|
||||||
async def test_pages_argument_is_accepted_but_ignored(self) -> None:
|
|
||||||
canned = _canned()
|
|
||||||
client = FakeOCRClient(canned=canned)
|
|
||||||
result = await client.ocr(
|
|
||||||
pages=[Page(page_no=5, width=1.0, height=1.0, lines=[])]
|
|
||||||
)
|
|
||||||
assert result is canned
|
|
||||||
|
|
||||||
|
|
||||||
class TestRaiseOnCallHook:
|
|
||||||
async def test_raise_on_call_propagates(self) -> None:
|
|
||||||
err = RuntimeError("surya is down")
|
|
||||||
client = FakeOCRClient(canned=_canned(), raise_on_call=err)
|
|
||||||
with pytest.raises(RuntimeError, match="surya is down"):
|
|
||||||
await client.ocr(pages=[])
|
|
||||||
Loading…
Reference in a new issue