docs: document parse_document and bump version to 1.3.11
Changelog, README (install pin, capability table, usage, deployment recipe, operations notes, known limits), env example, architecture, roadmap, the M3 design erratum and the CHSAnalyzer3 consumption notes.
This commit is contained in:
@@ -210,6 +210,8 @@ HTTP API → arq 队列 → worker 协程 脚本 → asyncio.gather 协
|
||||
|
||||
**背景(调研结论)**: Video-Tree 用 `/ocr/text` 把帧文字作为"硬证据"并置进 VLM 提示词,**裸调**(无重试/限流/熔断,单帧失败跳过,手写双端点轮询)——迁移后免费升级为全治理。CHSAnalyzer 用 `/parse` 定位表格 bbox,已全治理。GovDoc 无 OCR 且是明确架构否决(旧系统被"OCR 挂死"拖垮,教训反哺 §6.4 取消语义)。多后端预留有直接证据:CHSAnalyzer provider 白名单已含 `glm`(延后),设计文档明确三种后端图片输入形态各异(VLM=base64、Monkey=multipart、GLM=URL)——因此端口收 `bytes`,编码差异封装在 invoker 内部。
|
||||
|
||||
**1.3.11 增补(#34,整份文档解析)**: 端口族增第三成员 `OcrDocumentPort.parse_document(bytes)`(PDF 或图片整份 → 逐块 `OcrDocumentElement` + `page_sizes` + 整篇 `markdown`)。根因与取舍见 `designs/2026-10-21-issue34-ocr-document-design.md`: M3 的"服务按内容处理"只在 35 张图片样本上验证过,而 MonkeyOCR **按上传文件名扩展名分派**(PDF 标 `image.jpg` 时 `/ocr/text` 返回 `success:true` 的幻觉文本);库把输入建模成"一张图 + 写死的标签"、输出建模成"只有框",没有"文档"这个概念。决策要点: ① 新 Protocol `OcrDocumentTransport` **独立于** `OcrTransport`(旧 `runtime_checkable` Protocol 不加成员);② 上传类型由库按内容判定(图片魔数优先 → 前 1KB 内 `%PDF-`),不提供调用方声明参数;③ 内容只取自 `_middle.json` + 配对 `.md`,不读 `_content_list.json`(展示格式,转义 `* ` ~ $`);④ 确定性坏输入的 500 只在新路径窄化为"请求被拒",共享分类不变(#35)。
|
||||
|
||||
**影响**: 协议细节与后处理分界见 §7.10;`TableLocator` 这类业务化端口留在业务侧,由库端口结果组装。
|
||||
|
||||
### D10 音频只留端口
|
||||
@@ -424,13 +426,13 @@ flowchart TB
|
||||
|
||||
### 5.2 其他类型与 `chat()` 公共签名
|
||||
|
||||
`ChatRequest`(model/messages/结构化输出参数/per-call 覆盖项)、`Usage`(tokens + elapsed,OCR 无计费填 0)、`OcrTextResult`(text + 溯源五件 source_name/usage/latency_ms/call_id/raw)、`OcrLayoutResult`(elements 含 bbox/type/page_index + page_sizes + 同套溯源五件;元素自 `_middle.json` para_blocks 全量提取,M3 设计 §1.2 取证)。全部 frozen dataclass。空结果语义:合法"无内容"用空值/None 表达,调用失败必须走异常——二者严格区分。
|
||||
`ChatRequest`(model/messages/结构化输出参数/per-call 覆盖项)、`Usage`(tokens + elapsed,OCR 无计费填 0)、`OcrTextResult`(text + 溯源五件 source_name/usage/latency_ms/call_id/raw)、`OcrLayoutResult`(elements 含 bbox/type/page_index + page_sizes + 同套溯源五件;元素自 `_middle.json` para_blocks 全量提取,M3 设计 §1.2 取证)、`OcrDocumentResult`(1.3.11,#34: `OcrDocumentElement`=版面元素+`text`,加 page_sizes、整篇 `markdown` 与同套溯源五件;**独立定义不继承** `OcrLayoutResult`——父类 `call_stats` 带默认值,子类无默认字段会在导入期 `TypeError`)。全部 frozen dataclass。空结果语义:合法"无内容"用空值/None 表达,调用失败必须走异常——二者严格区分。
|
||||
|
||||
**`chat()` 公共签名定稿(2026-07-20,GovDoc 迁移缺口 G1/G2)**: `chat(messages, *, session_id=None, parent_call_id=None, cache_salt=None, cache_namespace=None, structured=None, stream=True)`。要点: ① `session_id`/`parent_call_id` 与三项目现有 `LLMProvider.chat` Protocol 逐字兼容——这是"调用点零改动"承诺的前提;② **per-call `cache_namespace`**: GovDoc 是单 client 服务多租户、tenant 每请求变化,装配级 namespace 只是默认值,per-call 传入时覆盖并进入缓存 key(§7.5);③ `cache_salt` per-call 可传(Video-Tree 跨 epoch 重采样);④ `structured` 三档语义(D14),类型定稿 `type[BaseModel] | Literal["json"] | None`(M1 设计): 不传 = 原始文本,`"json"` = 仅修复,pydantic 模型 = 完整阶梯(修复+形态校验+有界带反馈重问)。
|
||||
|
||||
**`overlay` 追加(2026-07-31,issue #4)**: 签名末尾增 `overlay: Mapping[str, Any] | None = None`,承载采样参数(`temperature`/`seed`/`max_tokens` 等)。带默认值的 keyword-only 参数不改变既有调用点,"签名冻结"承诺不破。要点: ① 优先级 **结构化注入 > 调用级 overlay > 源级 `extra_body`**,由 `StructuredMW` 的 `{**request.overlay, **strategy_overlay}` 与 transport `_build_payload` 的 update 顺序天然给出,无新机制;② 保护键 `{model, messages, stream, stream_options}` 与不可 JSON 序列化的值在**进洋葱之前**报 `ValueError`(前者被覆盖会击穿成本换算/缓存口径/流式看门狗/usage 帧,后者会在 `CacheMW` 的降级 try 之外抛裸 `TypeError` 且一行遥测都没有);③ 同时填 `ChatRequest.sampling` 快照字段——`overlay` 在洋葱不同深度取值不同(内层含 `response_format`),缓存 key 与遥测需要一个跨层恒定的读取点,否则同一列在不同行口径分叉。
|
||||
|
||||
**调用方维度追加(2026-08-17,issue #11)**: 四个公共方法(`chat` / `embed` / `recognize_text` / `parse_layout`)签名末尾各增 `tenant_id: str | None = None` 与 `meta: Mapping[str, Any] | None = None`。同 `overlay` 的形态——带默认值的 keyword-only,既有调用点零改动,"签名冻结"承诺不破。要点:
|
||||
**调用方维度追加(2026-08-17,issue #11)**: 五个公共方法(`chat` / `embed` / `recognize_text` / `parse_layout` / `parse_document`)签名末尾各增 `tenant_id: str | None = None` 与 `meta: Mapping[str, Any] | None = None`。同 `overlay` 的形态——带默认值的 keyword-only,既有调用点零改动,"签名冻结"承诺不破。要点:
|
||||
|
||||
① **校验在公共入口抛 `ValueError`,不静默丢弃**(与 `overlay` 保护键同一先例:构造期错误,发生在洋葱之外,不入四分类)。规则:`tenant_id` ≤128 字符、不含首尾空白(**拒绝而非 strip**——`" t1"` 与 `"t1"` 在 RLS 的等值比较下是两个租户,替调用方改写等于把行藏进另一个租户且不报错)、不得空串(空串是"未归属"哨兵);`meta` ≤16 键,键匹配 `[a-z0-9_.]{1,64}` 且 `pg_` 前缀保留给库,值仅限 `str`/`int`/`float`/`bool`,字符串值 ≤256 字符,**非有限 float 必须挡在入口**(`json.dumps` 会把它写成裸 `NaN`/`Infinity` 字面量——不是合法 JSON,PG 的 JSONB 拒收;放行则写入失败被遥测的降级 try 吞成 warning,即调用方的输入错误转成静默丢遥测)。emitter 侧 `allow_nan=False` 是第二道闸,它保的是 **SQLite**:那边 `meta` 是 TEXT 列不做 JSON 校验,没有这道闸会把非法 JSON 静默存进去,而它抛出的异常同样被降级 try 接住 → 丢一行而非报错。
|
||||
|
||||
@@ -594,7 +596,7 @@ flowchart TB
|
||||
| 列 | 语义 |
|
||||
| --- | --- |
|
||||
| `scope` | 池名,`TelemetryEmitter` 构造期注入(三个 client 各一行),三类行都带;**不拿 `source_name` 顶替**(终态失败可能根本没选出源) |
|
||||
| `operation` | `chat` / `embed` / `recognize_text` / `parse_layout`,**由调用点给定**。它与 `PolyGatewayError.operation` 是两个语义: 后者是异常自报的出错环节(可为 `download_result` 这类子步骤),链路上任何位置不得读它来填本列 |
|
||||
| `operation` | `chat` / `embed` / `recognize_text` / `parse_layout` / `parse_document`,**由调用点给定**。它与 `PolyGatewayError.operation` 是两个语义: 后者是异常自报的出错环节(可为 `download_result` 这类子步骤),链路上任何位置不得读它来填本列 |
|
||||
| `logical_call_id` | 一次逻辑调用一个 ID;上下文缺席(库内现场构造的请求)落 NULL,**不造 ID** |
|
||||
| `event_kind` | 三态: `attempt` / `cache_hit` / `terminal_failure`,三类行的唯一机械判据 |
|
||||
| `http_status_code` / `cause_type` / `error_body` | 只在失败的 `attempt` 行上非空(成功行不统一填 200: 那会让"有状态码"不再等价于"失败了");终态行三列恒 NULL(**C1 红线**) |
|
||||
@@ -668,6 +670,7 @@ flowchart TB
|
||||
| --- | --- | --- | --- |
|
||||
| `OcrTextPort.recognize_text(image: bytes)` | `POST /ocr/text` | multipart 上传 → JSON `{content}` | `OcrTextResult`(多行纯文本) |
|
||||
| `OcrLayoutPort.parse_layout(image: bytes)` | `POST /parse` | multipart → JSON(download_url) → GET ZIP → 解包 `*_middle.json` | `OcrLayoutResult`(elements 含 bbox + page_size) |
|
||||
| `OcrDocumentPort.parse_document(document: bytes)`(1.3.11,#34) | `POST /parse`(同上两段协议) | 上传类型按内容判定;流式 GET(整包 ≤256 MiB)→ 线程内解析 `*_middle.json` + 配对 `.md` | `OcrDocumentResult`(逐块 text + bbox + 页码,整篇 markdown) |
|
||||
|
||||
设计要点:输入统一 `bytes`(路径读取/多帧批量拼接留业务侧);bbox 返回 OCR 原生页面坐标,几何映射(裁剪偏移/归一化/marker 推算)留业务侧;`None`/空表达"合法无内容",异常表达"调用失败";ZIP 内容不可解析抛 `ResultInvalidError`(坏图≠坏服务);OCR 复用同一套治理算法件、循环形态同 EmbeddingClient 先例(见 D9 修订;无 token 计费,Usage 填 0,latency_ms 照记);多后端经 provider 注册表扩展(GLM 已在 CHSAnalyzer 白名单,输入形态为 URL,届时封装在其 invoker 内部,端口签名不变)。数值防御(bbox 有限性/顺序/退化校验)随协议解析下沉进库。OCR transport 支持 per-source `trust_env` 开关(LAN 直连绕过本地代理,Video-Tree `ocr.py:46` 教训;2026-07-20 缺口 R9);客户端暴露 `check_health() -> dict[str, bool]` 逐源健康预检(M3 设计 §3.3 细化:bool 无法承载逐源结果,业务 `all()` 即得启动门布尔;Video-Tree A/B 评测消费;缺口 R10 已闭)。协议事实(2026-07-21 实测): `/parse` 的 download_url 为**相对路径**(`/static/*.zip`),transport 相对 base_url 解析并兼容绝对 URL。
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@
|
||||
|
||||
**已交付增补(2026-09-14)**: 流式中段沉默计量与对冲(issue #27/#28,1.3.8)——`generation_ms` 口径重定义为**净生成时间**(墙钟 − Σ超阈值中段沉默,仍含 TTFT 与尾部;1.3.7 墙钟口径由 `generation_ms + stall_ms` 还原,披露见 CHANGELOG 未发布段),`CallStats` 增 `stall_ms`/`stall_count` 归因字段,遥测表加同两列(INSERT 字段 36→38);`{SCOPE}__HEDGE__STALL_AFTER_S` 中段沉默对冲(独立于 `HEDGE__AFTER_S` 生效、默认关闭;无业界公开先例,超业界现状能力),`{SCOPE}__STREAM__STALL_THRESHOLD_MS` 计量阈值(默认 1000,三层可调)。设计 designs/2026-09-11-streaming-stall-design.md(人类已批准 + 独立审查补遗 B1-B4/N1-N9),实施 plans/2026-09-11-streaming-stall.md,验证 findings/2026-09-11-streaming-stall-validation.md。后续储备:自适应分位阈值(防反馈环,现不做)、流式透传下的 commit 点重审、embedding/OCR 对冲(现不做)。
|
||||
|
||||
**已交付增补(2026-10-22,1.3.11 开发中,未发布)**: OCR 整份文档解析(issue #34,1.3.11)——`OcrClient.parse_document` + `OcrDocumentPort`/`OcrDocumentTransport`,PDF 上传按内容标注(同时修复 `recognize_text(PDF)` 的静默幻觉文本),结果只取自 `_middle.json` + 配对 `.md`;新路径对确定性坏输入归"请求被拒"。后续: #35(共享分类)、#36(`recognize_text` 不校验 `success`)。设计/计划见 `designs/2026-10-21-issue34-ocr-document-design.md`、`plans/2026-10-21-issue34-ocr-document.md`。
|
||||
|
||||
**已交付增补(2026-10-15,1.3.10 已发布)**: 统一完成判定(issue #30/#31,1.3.10)——以上游 `finish_reason` 与传输完整性判定流完成与结局,流式/非流式/缓存读写共用内核纯函数;缺 `[DONE]` 但 finish+实测 usage 完整即成功(两模式),文本 length/过滤无正文改 `ResultInvalidError`,保真模式 null/未知字段无损合并;`LLMResponse` 增 `finish_reason`/`stream_end`,遥测加 `stream_end` 列。设计 designs/2026-10-14-issue30-31-completion-verdict-design.md(人类授权 Claude 与独立 subagent 两轮审查后直接执行),实施 plans/2026-10-14-issue30-31-completion-verdict.md。后续储备:文本 n>1 混流修复、文本"只有思考+stop"证据收集。
|
||||
|
||||
## 7. 开放决策依赖
|
||||
|
||||
@@ -85,7 +85,7 @@ class OcrTransport(Protocol): # 协议细节;不含治理
|
||||
| 关切 | 决策 | 蓝本出处 |
|
||||
|---|---|---|
|
||||
| HTTP 客户端 | per-source `httpx.AsyncClient`(惰性建,keyed by source.name),`trust_env=source.trust_env`、`timeout=source.timeout_s` | **R9 销账**: SourceConfig.trust_env + `TRUST_ENV` 配置键 M1 已落地,openai_compat.py:203 已尊重,本 transport 同款;VT LAN 直连绕代理场景配 `OCR__MONKEY__N__TRUST_ENV=false` |
|
||||
| multipart 上传 | `files={"file": ("image.jpg", image, "image/jpeg")}` 固定名(CHS invokers.py:496 原样;服务按内容处理,35 样本实测通过,核验输出存档 `data/soak/m3_xcheck_results.txt`) | CHS |
|
||||
| multipart 上传 | `files={"file": ("image.jpg", image, "image/jpeg")}` 固定名(CHS invokers.py:496 原样;服务按内容处理,35 样本实测通过,核验输出存档 `data/soak/m3_xcheck_results.txt`)。**勘误(2026-10-22,#34)**: "服务按内容处理"只在图片样本上验证过,**不适用于 PDF**——服务按文件名扩展名分派(`.pdf`/`.jpg`/`.jpeg`/`.png`),PDF 标 `image.jpg` 时 `/ocr/text` 返回 `success:true` 的幻觉文本;上传标签现按内容判定,见 `2026-10-21-issue34-ocr-document-design.md` §1 E3/E4/E10 | CHS |
|
||||
| `/ocr/text` 解析 | 2xx + JSON + `content` 必须为 str,缺失/非 str → `ResultInvalidError`;空串合法 | VT(`.get("content","")` 的静默兜底**有意替换**为显式校验——P5 严禁默认值掩盖错误) |
|
||||
| `/parse` 两段协议 | POST `/parse` → 校验 `success is True` + `download_url` 非空 str → GET(相对路径 urljoin base_url,绝对 URL 原样)→ ZIP | CHS invokers.py:489-530 逐段保真;相对路径显式化(取证 §1.2) |
|
||||
| `_middle.json` 校验 | ZIP 内首个 `*_middle.json`;`pdf_info` 必须 list;每页: dict、`page_size` 为 2 正有限数、`para_blocks` 缺省按空列表、每块 type 为 str、bbox 为 4 有限数且 x2>x1、y2>y1 且**整数化后不退化**(库返回 float 原生 bbox,此校验专为 CHS shim 的 `int()` 裁剪路径兜底——亚像素宽的退化框裁剪即空图,实现中须注释此由来防止被当死代码删除) | CHS `_finite_number`/`_parse_table_result`(invokers.py:427-479)数值防御**全量下沉**,提取面从"首表"泛化为"全元素"(取证 §1.2 证明无损) |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Research Wiki 索引
|
||||
|
||||
> 自动生成,更新时间:2026-10-22 04:02 UTC
|
||||
> 自动生成,更新时间:2026-10-22 04:23 UTC
|
||||
|
||||
## design (49)
|
||||
- [1.3.4 推理意图与测试证据设计](designs/2026-09-09-134-thinking-contracts-design.md) `design:2026-09-09-134-thinking-contracts-design`
|
||||
|
||||
@@ -175,3 +175,4 @@
|
||||
- [2026-10-22 02:45 UTC] 新增边: plan:2026-10-21-issue34-ocr-document --implements--> design:2026-10-21-issue34-ocr-document-design
|
||||
- [2026-10-22 02:45 UTC] 重建索引: 124 篇页面
|
||||
- [2026-10-22 04:02 UTC] 重建索引: 124 篇页面
|
||||
- [2026-10-22 04:23 UTC] 重建索引: 124 篇页面
|
||||
|
||||
@@ -206,3 +206,15 @@ stack = ExtractionProviderStack(
|
||||
## 1.3.10 影响核对(2026-10-14,#30/#31)
|
||||
|
||||
CHS 已把 `ResultInvalidError` 作终态(坏结果≠坏服务,熔断记成功),1.3.10 文本 length/过滤且无正文落该分类与既有语义同向,worker `_TERMINAL` 分支无需改;缺 `[DONE]` 而完整的流从此直接成功。
|
||||
|
||||
## 1.3.11 CHSAnalyzer3 的整份文档消费路径(2026-10-22,#34)
|
||||
|
||||
CHSAnalyzer3 要把几百篇医学论文 PDF(每篇约 8–15 页,并发 ≤2,与医院线共用 MonkeyOCR)整份转成带页码的正文与表格。库侧提供 `OcrClient.parse_document`(设计 `designs/2026-10-21-issue34-ocr-document-design.md`):
|
||||
|
||||
| 事项 | 结论 |
|
||||
| --- | --- |
|
||||
| 入口 | `await ocr.parse_document(pdf_bytes, tenant_id=..., meta=...)`;结果 `elements[]`(`type`/`page_index`/`bbox`/`text`)、`page_sizes`、`markdown`。`parse_layout` 医院线路径**不动** |
|
||||
| 表格 | `type=="table"` 的元素 `text` 是 HTML 表体;**表格脚注、页眉页脚、页码在服务端即被丢弃**(显著性/缩写说明会缺失),库无从补偿——抽取逻辑不要假设能拿到 |
|
||||
| 部署 | 文档线用独立 scope(如 `OCR_DOC`),`MAX_CONCURRENCY=2`、`RETRY__MAX_ATTEMPTS=2`、`CALL_DEADLINE_S ≤ PGW_LEASE_TTL_S`、`TIMEOUT_S` 按最大页数估算;配方见 README「1.3.11 OCR 整份文档解析」。若服务为 sync-only 后端,每实例并发 1 并错峰,避免队头阻塞医院线 |
|
||||
| 坏文件 | 无法识别的字节本地拒绝;截断/伪造 PDF 的已知确定性 500 归 `RequestRejectedError`(`status_code=500`),不重试、不计熔断。**页数过滤、可读性预检归调用方** |
|
||||
| ⚠️ 待确认(H3) | 本文件 §9 记录 CHSAnalyzer 把库侧「服务不可用」一族(`GatewayUnavailableError`:`CircuitOpenError`/`AllSourcesExhausted`)翻译成 `ProviderUnavailableError` → arq 延期重投且不耗业务失败预算。**CHSAnalyzer3 是否沿用该语义未向下游证实**: 若沿用,一份永久损坏的 PDF 若落入 `AllSourcesExhausted(retry_exhausted)`(例如共享分类对未收录的确定性 500 仍归瞬时,见 #35)会被无限延期——请在 CHSAnalyzer3 侧为「同一文档连续 N 次延期」设上限 |
|
||||
|
||||
Reference in New Issue
Block a user