feat: add OcrClient.parse_document with full governance

Reuses the existing retry, breaker, limiter and telemetry loop; the transport capability is checked once before the statistics boundary. Legacy entrances keep their error text. Tests cover the document path through the real transport so parser exceptions, deadlines and probe bookkeeping are proven end to end.
This commit is contained in:
2026-10-22 00:01:45 -04:00
parent 61cace71c4
commit 80bb8aba71
5 changed files with 524 additions and 50 deletions
+31 -1
View File
@@ -37,6 +37,8 @@ from polygateway.types import (
EmbeddingTransportResult,
GlobalLimits,
LLMResponse,
OcrDocumentElement,
OcrDocumentTransportResult,
OcrTextTransportResult,
RetryPolicy,
SourceConfig,
@@ -2422,6 +2424,18 @@ class _StubOcrTransport:
async def parse_layout(self, *, image, source, call_id):
raise NotImplementedError
async def parse_document(self, *, document, source, call_id):
return OcrDocumentTransportResult(
elements=[
OcrDocumentElement(
type="text", bbox=(0.0, 0.0, 1.0, 1.0), page_index=0, text="SYNTH-BODY"
)
],
page_sizes=[(612.0, 792.0)],
markdown="# SYNTH-MD",
raw={"success": True},
)
def _governance(scope, sources):
"""embed/OCR 两条链路共用的最小治理装配(真实内存后端,不 mock)。"""
@@ -2478,6 +2492,20 @@ class TestTextCapCoversEmbedAndOcrChains:
assert json.loads(row["messages"])[0]["content"] == "<ocr:tex…(略 16 字)"
assert row["response"] == "识别结果识别结果…(略 32 字)" # 先经 OCR 自有的 200 字上限
async def test_ocr_document_rows_are_capped(self):
"""第三个 OCR 入口的占位串同样受 text_cap 约束(单一遥测出口)。"""
rec = _MemoryRecorder()
client = OcrClient(
**_governance("ocr", [_source(name="m1", model="monkey-ocr")]),
transport=_StubOcrTransport(),
telemetry=rec,
text_cap=8,
)
await client.parse_document(b"%PDF-1.4")
row = rec.rows[0]
assert json.loads(row["messages"])[0]["content"] == "<ocr:doc…(略 20 字)"
assert row["operation"] == "parse_document"
# —— 遥测降级状态(issue #15 C 组): 共用 tracker + 只读快照 + 节流日志 ——
@@ -3436,7 +3464,9 @@ class TestRowLevelObservability:
assert error.count("v" * 200) == 3
assert len(error) < 1200
@pytest.mark.parametrize("operation", ["chat", "embed", "recognize_text", "parse_layout"])
@pytest.mark.parametrize(
"operation", ["chat", "embed", "recognize_text", "parse_layout", "parse_document"]
)
async def test_operation_is_given_by_the_call_site_not_the_exception(self, operation):
"""新列 `operation` 与 `exc.operation` 是两个语义(设计 §5 I1/I2)。"""
exc = TransientError("boom", status_code=500, operation="download_result")