Coverage for src / quber / core / extractors / camelot / llm / orchestrator.py: 97%
37 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
1"""
2CamelotLLMTableExtractor: the public face of the camelot.llm package.
4DEPRECATED and dormant: superseded by `SetOfMarkExtractor` (vision-guided,
5in-region Camelot, grounded correction). Kept importable for comparison only;
6do not build new work on it.
8Wires run-scoped dependencies (LLM clients, semaphores, temp dir) into
9`PipelineDeps` and runs the extraction graph built in `pipeline`.
10"""
12from __future__ import annotations
14import asyncio
15import tempfile
16from pathlib import Path
17from typing import List, Optional
19from quber.agents.classifier import TableClassifier, get_classifier
20from quber.agents.llm_client import LLMClient, get_llm_client
21from quber.agents.unifier import TableUnifier, get_unifier
22from quber.core.extractors.base import ExtractedTable
23from quber.core.extractors.camelot.acquire import CAMELOT_FLAVOR_TIMEOUT_S
24from quber.core.extractors.camelot.llm.pipeline import (
25 PipelineDeps,
26 PipelineState,
27 build_pipeline_graph,
28)
31class CamelotLLMTableExtractor:
32 llm: LLMClient
33 classifier: TableClassifier
34 unifier: TableUnifier
35 dpi: int
36 run_llm_correction: bool
37 max_concurrent: int
38 flavor_timeout_s: float
40 def __init__(
41 self,
42 llm_client: Optional[LLMClient] = None,
43 classifier: Optional[TableClassifier] = None,
44 unifier: Optional[TableUnifier] = None,
45 dpi: int = 200,
46 run_llm_correction: bool = True,
47 max_concurrent: int = 5,
48 flavor_timeout_s: float = CAMELOT_FLAVOR_TIMEOUT_S,
49 ) -> None:
50 self.llm = llm_client or get_llm_client()
51 self.classifier = classifier or get_classifier()
52 self.unifier = unifier or get_unifier()
53 self.dpi = dpi
54 self.run_llm_correction = run_llm_correction
55 self.max_concurrent = max_concurrent
56 self.flavor_timeout_s = flavor_timeout_s
58 async def extract_tables(self, source: Path) -> List[ExtractedTable]:
59 source = Path(source)
60 if not source.exists():
61 raise FileNotFoundError(source)
63 graph = build_pipeline_graph()
64 with tempfile.TemporaryDirectory(prefix="quber-camelot-") as tmpdir:
65 deps = PipelineDeps(
66 llm=self.llm,
67 classifier=self.classifier,
68 unifier=self.unifier,
69 source=source,
70 tmp_dir=Path(tmpdir),
71 dpi=self.dpi,
72 run_llm_correction=self.run_llm_correction,
73 flavor_timeout_s=self.flavor_timeout_s,
74 classify_sem=asyncio.Semaphore(self.max_concurrent),
75 unify_sem=asyncio.Semaphore(self.max_concurrent),
76 correct_sem=asyncio.Semaphore(self.max_concurrent),
77 )
78 return await graph.run(state=PipelineState(), deps=deps, inputs=source)
80 def extract_tables_sync(self, source: Path) -> List[ExtractedTable]:
81 """Sync entry point for callers without an event loop (CLI,
82 scripts). Wraps the async path with `asyncio.run`. Not callable
83 from inside an existing event loop — async callers should use
84 `await extract_tables(...)` directly.
85 """
86 return asyncio.run(self.extract_tables(source))