Skip to content

feat(ocr): pluggable engine architecture + LLM vision OCR + configurable threshold#548

Merged
warren618 merged 4 commits into
HKUDS:mainfrom
shadowinlife:feat/ocr-plugin-architecture
Jul 20, 2026
Merged

feat(ocr): pluggable engine architecture + LLM vision OCR + configurable threshold#548
warren618 merged 4 commits into
HKUDS:mainfrom
shadowinlife:feat/ocr-plugin-architecture

Conversation

@shadowinlife

@shadowinlife shadowinlife commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of Issue #547 — Milestone M0 + M1 + M2

Delivers OCR engine plugin infrastructure (M0), threshold parameterization (M1), and quality metrics with agent awareness (M2) in a single focused change set.

  • Protocol + registry + entry_points plugin discovery (replaces hardcoded _VALID_ENGINES factory)
  • Generic LlmVisionOcrEngine for any OpenAI-compatible provider (replaces DashScope-only QwenVisionOcrEngine)
  • Parameterized OCR trigger threshold (min_text_per_page) + quality metrics (ocr_quality field) + agent-aware SKILL.md updates
  • Backward compatibility: VIBE_TRADING_OCR_ENGINE=qwen-vl and VIBE_TRADING_OCR_QWEN_MODEL alias with deprecation warnings

Not in this PR (subsequent PRs in the #547 series):

  • M3: Professional PDF OCR integration (Docling / Nougat / Marker / LlamaParse) — separate PR for complex-layout academic PDF parsing beyond the current LLM-vision engine
  • M4: Academic PDF parser benchmark (CN broker reports + EN papers) — separate research issue
  • M5: Structured output (table detection + LaTeX + output_format param) — separate feature issue

Part of #547

Why

The strategy-dev-manager skill (#455, merged via #457) introduced a pluggable OCR engine interface with two built-in engines. Five problems remained unresolved:

  1. rapidocr_onnxruntime not declared as a dependency — fresh installs silently fail on scanned PDFs
  2. OCR trigger threshold hardcoded at 50 chars/page
  3. No OCR quality metrics — agent cannot judge extraction quality
  4. QwenVisionOcrEngine hardcoded to DashScope — users with OpenAI/Anthropic/Google cannot use their provider
  5. Hardcoded _VALID_ENGINES tuple — third parties cannot publish OCR plugins

This PR resolves #1#5. The plugin scaling concern (Issue #547 main motivation) is addressed by the entry_points mechanism, which allows any third-party package to publish OCR engines without modifying core.

Changes

Core architecture (Issue #547 M0)

  • agent/src/tools/ocr/engine.py — Rewritten: OcrEngine Protocol now requires is_cloud and install_hint fields. Replaced _VALID_ENGINES tuple + _try_rapid()/_try_qwen_vl() factory functions with _BUILTIN_ENGINES registry + _discover_plugins() via importlib.metadata.entry_points(group="vibe_trading.ocr_engines"). Added _select_first_local() that filters is_cloud=False engines for auto mode. Added _CHOICE_ALIASES for backward-compat engine name mapping with deprecation warnings (Issue [Feature] OCR Enhancement: Pluggable Engine Architecture with entry_points Discovery #547). Added optional confidence() method to Protocol. Plugin discovery cached via @lru_cache(maxsize=1).
  • agent/src/tools/ocr/llm_vision_ocr.py — New: LlmVisionOcrEngine with is_cloud=True. Uses _resolve_provider_config() to reuse existing provider_env_names() — no separate provider mapping table. Converts images to base64 JPEG (quality=85) for API upload. 1 retry for transient network errors. Detailed _OCR_SYSTEM_PROMPT specifying output format (Markdown/HTML tables/LaTeX formulas).
  • agent/src/tools/ocr/qwen_vision_ocr.py — Deleted. Users with VIBE_TRADING_OCR_ENGINE=qwen-vl are transparently aliased to llm-vision with a deprecation warning (see Backward Compatibility section).
  • agent/src/tools/ocr/rapid_ocr.py — Added is_cloud=False, install_hint, optional confidence() method (mean RapidOCR confidence), and self-registration via register_builtin().

Threshold parameterization (Issue #547 M1)

  • agent/src/tools/doc_reader_tool.py — Added min_text_per_page parameter to read_document() (default 50, previously hardcoded _MIN_TEXT_PER_PAGE). Parameterized through _read_pdf(). Added ocr_engine and ocr_quality fields to PDF and image responses.
  • agent/src/config/env_schema.py — Renamed VIBE_TRADING_OCR_QWEN_MODEL -> VIBE_TRADING_OCR_LLM_MODEL in OcrConfig. Added Pydantic model_validator that reads the legacy env var, forwards it to the new field, and emits a deprecation warning (new env var takes precedence when both are set).
  • agent/.env.example — Added VIBE_TRADING_OCR_ENGINE and VIBE_TRADING_OCR_LLM_MODEL documentation with engine descriptions.

Quality metrics & agent awareness (Issue #547 M2)

  • agent/src/tools/doc_reader_tool.pyocr_quality is a dict with quality_flag (good/degraded/no_ocr_engine/no_ocr_needed), ocr_pages, and text_density (chars/page). Returned for both PDF and image paths.
  • agent/src/skills/doc-reader/SKILL.md — Added OCR Configuration section: threshold, engine selection, response fields documentation.
  • agent/src/skills/strategy-dev-manager/SKILL.md — Added OCR Quality Check step after read_document calls (flag interpretation + text_density < 100 warning).

Tests

  • agent/tests/test_ocr_engine.py — Rewritten: TestEngineSelection (7 tests: privacy guarantee, fallback behavior, explicit cloud choice), TestRegistry (5 tests: builtin registration, plugin override, cache reset), TestInstallHints (3 tests), TestProviderConfigResolution (5 tests: provider env vars, fallback chains, model override), TestRecognizeNoAdvisory (1 test: no advisory warnings on explicit choice), TestBackwardCompatAliases (6 tests: engine-name alias, deprecation warning emission, env-var alias, precedence, ollama-only fallback, unknown-provider empty-key).
  • agent/tests/test_ocr_integration.py — New: synthetic scanned PDF -> _read_pdf() -> OCR -> text verification. Requires rapidocr_onnxruntime (skipped if not installed).

Affected Areas

  • agent/src/tools/ocr/ — complete rewrite of OCR engine factory
  • agent/src/tools/doc_reader_tool.py — new parameters and response fields (PDF + image paths only)
  • agent/src/config/env_schema.py — one field rename + backward-compat validator in OcrConfig
  • agent/src/skills/doc-reader/SKILL.md + strategy-dev-manager/SKILL.md — documentation
  • agent/tests/test_ocr_engine.py + agent/tests/test_ocr_integration.py — test rewrite

Backward Compatibility

All backward compatibility requirements from Issue #547's design-decision table are implemented:

Old config New behavior Implementation
VIBE_TRADING_OCR_ENGINE=auto Unchanged — local engines only n/a
VIBE_TRADING_OCR_ENGINE=rapid Unchanged — RapidOCR n/a
VIBE_TRADING_OCR_ENGINE=qwen-vl Alias -> llm-vision + deprecation warning _CHOICE_ALIASES in engine.py, applied in get_ocr_engine() before engine lookup
VIBE_TRADING_OCR_ENGINE=none Unchanged — disabled n/a
VIBE_TRADING_OCR_QWEN_MODEL Alias -> VIBE_TRADING_OCR_LLM_MODEL + deprecation warning _alias_legacy_env_vars Pydantic model_validator(mode="before") in OcrConfig; new env var takes precedence

Design divergence from Issue #547

Issue #547 design Actual implementation Rationale
_VISION_MODEL_KEYWORDS heuristic (40+ models) Vision capability NOT gated; is_available() checks API key only Explicit user choice (VIBE_TRADING_OCR_ENGINE=llm-vision) is the strongest signal. A failed API call is clearer feedback than a heuristic refusal that second-guesses the user. Documented in llm_vision_ocr.py module docstring.

Out of Scope

  • M3: Professional PDF OCR integration — The LLM-vision engine in this PR covers scanned-page text extraction via multimodal APIs. Evaluating dedicated complex-layout PDF parsers (Docling, Nougat, Marker, LlamaParse) for academic broker reports and mixed-layout papers is planned as a separate PR.
  • M4: Academic PDF parser evaluation — Benchmark on CN broker reports + EN papers. Separate research issue.
  • M5: Structured output — Table detection + LaTeX + output_format param. Separate feature issue.
  • Performance benchmarking — Comparative accuracy/speed benchmarks across engines deferred.
  • Built-in OCR model bundlingrapidocr_onnxruntime remains optional; engine fails gracefully with a clear install hint.
  • Multi-page batch OCR — Each page processed individually; streaming/batching left for follow-up.

Test Plan

  • Existing tests pass: pytest --ignore=agent/tests/e2e_backtest --ignore=agent/tests/test_e2e_harness_v2.py --tb=short -q5412 passed, 4 failed (all 4 pre-existing tushare E2E auth failures, unrelated to OCR)
  • OCR tests: 30 passed, 1 skipped (synthetic PDF e2e requires rapidocr_onnxruntime)
  • 27 unit tests + 3 integration tests, covering selection, privacy, registry, install hints, provider config resolution, advisory-warning suppression, and backward-compat aliases
  • Backward compatibility verified: aliases map old names -> new names with deprecation warnings; legacy env var forwarded to new field when only legacy is set; new env var takes precedence when both are set
  • No degradation to existing OCR behavior — rapid engine path preserved, auto mode privacy guarantee strengthened
  • CI env var gate passes (all os.getenv calls use # noqa: env-gate)

Risk Assessment

  • Network/secret risk — LOW: LLM vision engine reads provider credentials via os.getenv() with # noqa: env-gate, matching codebase convention. No new credential paths.
  • Deployment risk — LOW: Both legacy configs aliased with deprecation warnings — preserves existing behavior, guides users to new names. No silent breakage.
  • Privacy risk — NONE: is_cloud data field ensures auto mode never sends pages to cloud. Explicit VIBE_TRADING_OCR_ENGINE=llm-vision required for cloud OCR.
  • MCP risk — NONE: No MCP tool changes. OCR engine is called internally by doc_reader_tool.
  • Broker/trading risk — NONE: No changes to broker connectors, order gates, mandate enforcement, or trading paths.
  • Supply chain risk — LOW: entry_points discovery loads installed packages. Same trust model as pip install.

Rollback

  • Revert path: git revert <merge-commit-sha>. Additive plugin system — no DB migrations or persistent state.
  • Partial: Set VIBE_TRADING_OCR_ENGINE=rapid to use only RapidOCR (local). Engine name is "rapid", not "rapid-ocr".
  • Legacy-config users: VIBE_TRADING_OCR_ENGINE=qwen-vl continues to work (aliased to llm-vision with log warning). VIBE_TRADING_OCR_QWEN_MODEL continues to work (forwarded to new field with log warning). Migration optional but recommended.

Milestone Roadmap (Issue #547 series)

PR Milestone Status
#548 (this PR) M0 + M1 + M2 — plugin infra + threshold + quality metrics + backward compat Review
TBD M3 — Professional PDF OCR integration (Docling / Nougat / Marker / LlamaParse evaluation) Planned
TBD M4 — Academic PDF parser benchmark (research issue) Planned
TBD M5 — Structured output (feature issue) Planned

Checklist

  • No changes to protected areas (src/agent/, src/session/, src/providers/) without prior discussion
  • No hardcoded values (API keys, file paths, magic numbers)
  • Code follows CONTRIBUTING.md guidelines
  • Documentation updated (SKILL.md for doc-reader and strategy-dev-manager, .env.example)
  • All commits carry Signed-off-by: DCO trailer
  • No AI-assistant attribution trailers in commit messages
  • Backward compatibility table from Issue [Feature] OCR Enhancement: Pluggable Engine Architecture with entry_points Discovery #547 fully implemented with regression tests

@shadowinlife
shadowinlife marked this pull request as draft July 15, 2026 06:13
@shadowinlife
shadowinlife force-pushed the feat/ocr-plugin-architecture branch 4 times, most recently from c6efc36 to 82f223d Compare July 15, 2026 10:28
@shadowinlife
shadowinlife force-pushed the feat/ocr-plugin-architecture branch 2 times, most recently from e9d1ee0 to c8b3bd3 Compare July 15, 2026 13:00
Replace hard-coded QwenVisionOCR with a plugin-based engine that discovers
OCR backends via Python entry_points. Ships with:
- LLMDocumentOCR: multi-modal LLM vision (Qwen/OpenAI/Gemini)
- RapidDocumentOCR: offline RapidOCR adapter
- Extensible base class for custom engines

Includes comprehensive test coverage, env-schema config for engine selection,
and updated doc-reader skill documentation.

Signed-off-by: shadowinlife <shadowinlife@gmail.com>
@shadowinlife
shadowinlife force-pushed the feat/ocr-plugin-architecture branch from c8b3bd3 to 28b0575 Compare July 15, 2026 13:19
@shadowinlife
shadowinlife marked this pull request as ready for review July 15, 2026 13:21
@shadowinlife shadowinlife changed the title feat(ocr): pluggable OCR engine architecture with entry_points discovery feat(ocr, m0): plugin infrastructure with entry_points discovery + backward compat shim Jul 15, 2026
Implement the backward compatibility layer mandated by Issue HKUDS#547
(design-decision table):

- engine.py: add _CHOICE_ALIASES table ("qwen-vl" -> "llm-vision") applied
  in get_ocr_engine() before engine lookup, emitting a deprecation warning.
- env_schema.py: add Pydantic model_validator(mode="before") on OcrConfig
  that reads legacy VIBE_TRADING_OCR_QWEN_MODEL, forwards to the new
  VIBE_TRADING_OCR_LLM_MODEL field, and emits a deprecation warning.
  New env var takes precedence when both are set.
- llm_vision_ocr.py: restrict the "ollama" placeholder api_key to the
  ollama provider only. Unknown providers now get empty string so they
  no longer silently misconfigure (previously any unknown provider
  without OPENAI_API_KEY was given "ollama" as api_key, corrupting the
  OpenAI client for non-ollama providers).
- test_ocr_engine.py: TestBackwardCompatAliases (6 new tests): alias
  mapping, deprecation warning emission, env-var precedence,
  new-takes-precedence over legacy, ollama-only fallback, and
  unknown-provider empty-key behavior.

Tests: 30 passed, 1 skipped (rapidocr_onnxruntime not installed).
Full suite: 5412 passed, 4 failed (pre-existing tushare E2E auth, unrelated).

Part of HKUDS#547.

Signed-off-by: shadowinlife <shadowinlife@gmail.com>
@shadowinlife shadowinlife changed the title feat(ocr, m0): plugin infrastructure with entry_points discovery + backward compat shim feat(ocr, 547): M0+M1+M2 — plugin infra + threshold + quality metrics + backward compat Jul 15, 2026
@shadowinlife shadowinlife changed the title feat(ocr, 547): M0+M1+M2 — plugin infra + threshold + quality metrics + backward compat feat(ocr): pluggable engine architecture + LLM vision OCR + configurable threshold Jul 16, 2026

@shadowinlife shadowinlife left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — pluggable OCR engine architecture

Reviewed the full diff against the linked Issue #547 and the current main integration points (doc_reader_tool.py, env_schema.py, provider_env_names). No clone was needed — everything was resolvable from the diff plus surrounding source. Skipping pure formatting/lint/import-ordering nits (left to tooling).

What's done well

🎉 The privacy red line is modeled cleanly as data (is_cloud) rather than scattered if choice == "qwen-vl" checks. _select_first_local() enforces it uniformly, and even third-party plugins inherit the guarantee. Well covered by test_auto_never_selects_cloud_engine and friends.

🎉 The backward-compat layer is solid: both the engine-name alias (_CHOICE_ALIASES) and the env-var alias (Pydantic model_validator) emit deprecation warnings and are backed by regression tests (TestBackwardCompatAliases), including new-takes-precedence semantics.

🎉 Keeping RapidOcrEngine.__init__ cheap with an explicit comment about _select_first_local() probing every engine shows good awareness of the lazy-init contract.

Decision: 💬 Comment (recommend changes before merge)

Architecture direction is right, the privacy design is excellent, and tests/backward-compat are thorough. No blocking crash/data-loss issues. But three [important] items are worth resolving before merge — the quality_flag misreport is highest priority because it directly undermines this PR's M2 goal (agent quality awareness). Inline comments below.

Comment thread agent/src/tools/doc_reader_tool.py Outdated
Comment thread agent/src/tools/ocr/llm_vision_ocr.py
Comment thread agent/src/tools/ocr/llm_vision_ocr.py
Comment thread agent/src/tools/ocr/rapid_ocr.py Outdated
Comment thread agent/src/config/env_schema.py Outdated
Comment thread agent/src/tools/ocr/llm_vision_ocr.py Outdated
Comment thread agent/src/tools/doc_reader_tool.py Outdated
…stants, nit

- Track ocr_attempted_pages to distinguish "OCR attempted but empty"
  from "no OCR needed"; map former to quality_flag="degraded"
- Promote inline magic numbers to named module constants with rationale
  comments (_OCR_MAX_TOKENS, _OCR_TEMPERATURE, _OCR_JPEG_QUALITY)
- Replace no-op round(len(text) / 1, 1) with float(len(text))

Signed-off-by: shadowinlife <shadowinlife@gmail.com>

AI-Contributed/Feature: 0/40
AI-Contributed/UT: 0/0
…nfidence(), deduplicate import

- Only retry transient errors (timeout/connection/5xx) in LLM vision OCR;
  deterministic 4xx failures now raise immediately without delay
- Remove unused confidence() method that hid a redundant OCR call
- Remove duplicate model_validator import in env_schema.py

Signed-off-by: shadowinlife <shadowinlife@gmail.com>

AI-Contributed/Feature: 0/46
AI-Contributed/UT: 0/0
@warren618
warren618 merged commit eb4f49f into HKUDS:main Jul 20, 2026
1 check passed
@warren618

Copy link
Copy Markdown
Collaborator

Thanks @shadowinlife — merged. The provider-agnostic llm-vision engine, entry-point plugin discovery, and configurable OCR threshold are real improvements over the DashScope-locked qwen-vl. On merge I extended the deprecation warning to call out the credential-source change (DASHSCOPE_API_KEY → configured LANGCHAIN_PROVIDER) so existing DashScope users aren't surprised. Closes #547.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants