graph-knowledge-doc-parser 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/PKG-INFO +2 -2
  2. graph_knowledge_doc_parser-0.2.2/kg_doc_parser/document_ingest_log_config.py +26 -0
  3. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/document_ingester_logger.py +6 -6
  4. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/llm_structured_output.py +23 -5
  5. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/models.py +7 -7
  6. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/ocr.py +78 -36
  7. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/pdf2png.py +74 -48
  8. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/semantic_document_splitting_layerwise_edits.py +24 -8
  9. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/bounded_threadpool_executor.py +13 -8
  10. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/file_loaders.py +24 -15
  11. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/langchain.py +53 -28
  12. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/log.py +3 -2
  13. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/version_chaining.py +18 -8
  14. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/adapters.py +29 -13
  15. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/cache.py +7 -6
  16. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/cli.py +4 -3
  17. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/clients.py +55 -19
  18. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/demo_harness.py +23 -12
  19. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/design.py +2 -3
  20. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/handlers.py +68 -16
  21. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/layered_contracts.py +32 -20
  22. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/layerwise_llm.py +91 -31
  23. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/ocr_pipeline.py +37 -29
  24. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/page_index.py +23 -14
  25. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/parser_core.py +142 -62
  26. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/parsing.py +23 -16
  27. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/providers.py +83 -41
  28. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/runners.py +15 -9
  29. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/service.py +22 -9
  30. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/pyproject.toml +2 -2
  31. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/README.md +0 -0
  32. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/__init__.py +0 -0
  33. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/cast_hinting.py +0 -0
  34. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/text_processing_utils.py +0 -0
  35. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/__init__.py +0 -0
  36. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/__init__.py +0 -0
  37. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/_kogwistar.py +0 -0
  38. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/models.py +0 -0
  39. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/probe.py +0 -0
  40. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/semantics.py +0 -0
  41. {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/smoke_assets.py +2 -2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graph-knowledge-doc-parser
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: doc parser using llm driven engine with graph knowledge awareness
5
5
  Author: humblemat810
6
6
  Author-email: 67593116+humblemat810@users.noreply.github.com
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
16
16
  Classifier: Topic :: Text Processing :: General
17
17
  Requires-Dist: diskcache (>=5.6,<6.0) ; platform_python_implementation == "PyPy"
18
18
  Requires-Dist: joblib (>=1.5.3,<2.0.0) ; platform_python_implementation == "CPython"
19
- Requires-Dist: kogwistar (==0.6.2)
19
+ Requires-Dist: kogwistar (==0.6.3)
20
20
  Requires-Dist: langchain-core (>=1.2.5,<2.0.0)
21
21
  Requires-Dist: langchain-google-genai (>=4.1.2,<5.0.0)
22
22
  Requires-Dist: mcp (>=2.2.0,<3.0.0)
@@ -0,0 +1,26 @@
1
+ """Configuration for the parser's durable ingest event log."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Mapping
7
+
8
+ DEFAULT_DOCUMENT_INGEST_LOG_DB = os.path.join("logs", "document_ingest.sqlite")
9
+ DOCUMENT_INGEST_LOG_DB_ENV = "KG_DOC_PARSER_DOCUMENT_INGEST_LOG_DB"
10
+
11
+
12
+ def configured_document_ingest_log_db(
13
+ environ: Mapping[str, str] | None = None,
14
+ ) -> str:
15
+ """Return the configured ingest log path, preserving the legacy default."""
16
+
17
+ values = os.environ if environ is None else environ
18
+ configured = str(values.get(DOCUMENT_INGEST_LOG_DB_ENV, "") or "").strip()
19
+ return configured or DEFAULT_DOCUMENT_INGEST_LOG_DB
20
+
21
+
22
+ __all__ = [
23
+ "DEFAULT_DOCUMENT_INGEST_LOG_DB",
24
+ "DOCUMENT_INGEST_LOG_DB_ENV",
25
+ "configured_document_ingest_log_db",
26
+ ]
@@ -545,8 +545,8 @@ class DocumentIngestSQLiteCallback(BaseCallbackHandler):
545
545
  serialized: Dict[str, Any],
546
546
  prompts: list[str],
547
547
  *,
548
- run_id: str,
549
- parent_run_id: Optional[str] = None,
548
+ run_id: UUID,
549
+ parent_run_id: Optional[UUID] = None,
550
550
  tags: Optional[list[str]] = None,
551
551
  metadata: Optional[Dict[str, Any]] = None,
552
552
  **kwargs: Any,
@@ -602,8 +602,8 @@ class DocumentIngestSQLiteCallback(BaseCallbackHandler):
602
602
  self,
603
603
  response: LLMResult,
604
604
  *,
605
- run_id: str,
606
- parent_run_id: Optional[str] = None,
605
+ run_id: UUID,
606
+ parent_run_id: Optional[UUID] = None,
607
607
  tags: Optional[list[str]] = None,
608
608
  metadata: Optional[Dict[str, Any]] = None,
609
609
  **kwargs: Any,
@@ -718,8 +718,8 @@ class DocumentIngestSQLiteCallback(BaseCallbackHandler):
718
718
  self,
719
719
  error: BaseException,
720
720
  *,
721
- run_id: str,
722
- parent_run_id: Optional[str] = None,
721
+ run_id: UUID,
722
+ parent_run_id: Optional[UUID] = None,
723
723
  tags: Optional[list[str]] = None,
724
724
  metadata: Optional[Dict[str, Any]] = None,
725
725
  **kwargs: Any,
@@ -1,17 +1,35 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ from typing import Protocol
4
+
5
+
6
+ class StructuredOutputRunnable(Protocol):
7
+ """Runnable returned by a structured-output model adapter."""
8
+
9
+ steps: list[object]
10
+
11
+ def invoke(self, *args: object, **kwargs: object) -> object: ...
12
+
13
+
14
+ class StructuredOutputModel(Protocol):
15
+ """Minimum model surface required by the parser's structured-output path."""
16
+
17
+ def with_structured_output(
18
+ self,
19
+ schema: object,
20
+ **kwargs: object,
21
+ ) -> StructuredOutputRunnable: ...
4
22
 
5
23
 
6
24
  def build_structured_output_runnable(
7
- model: Any,
8
- schema: Any,
25
+ model: StructuredOutputModel,
26
+ schema: object,
9
27
  *,
10
28
  include_raw: bool = True,
11
29
  prefer_json_schema: bool = True,
12
- ) -> Any:
30
+ ) -> StructuredOutputRunnable:
13
31
  """Build a structured-output runnable with strict-schema-first fallback."""
14
- attempts: list[dict[str, Any]] = []
32
+ attempts: list[dict[str, object]] = []
15
33
  if prefer_json_schema:
16
34
  attempts.append({"include_raw": include_raw, "method": "json_schema"})
17
35
  attempts.append({"include_raw": include_raw, "method": "function_calling"})
@@ -51,7 +51,7 @@ if True:
51
51
  logger = logging.getLogger(__name__)
52
52
  logger.addHandler(logging.NullHandler())
53
53
  logger.debug("loading models")
54
- from typing import List, Literal, Optional, Dict, Any, Type, Union, Annotated, ClassVar
54
+ from typing import List, Literal, Optional, Dict, Any, Type, Union, Annotated, ClassVar, Self
55
55
  try:
56
56
  from typing import TypeAlias
57
57
  except ImportError: # pragma: no cover
@@ -122,7 +122,7 @@ class OCRClusterResponse(ModeSlicingMixin, BaseModel):
122
122
  contains_table: DtoType[bool] = Field(description='Whether this page contains table. ')
123
123
 
124
124
  @model_validator(mode='after')
125
- def check_cluster_meaningful_ordering_agreement(self):
125
+ def check_cluster_meaningful_ordering_agreement(self) -> Self:
126
126
  assert bool(self.is_empty_page) ^ (len(self.OCR_text_clusters) > 0), f"is_empty_page value {self.is_empty_page} disagree with OCR_text_clusters len={len(self.OCR_text_clusters)}"
127
127
  overlap_id = set(i.cluster_number for i in self.OCR_text_clusters).intersection(set(i.cluster_number for i in self.non_text_objects))
128
128
  if overlap_id:
@@ -148,18 +148,18 @@ class SplitPageMeta(BaseModel):
148
148
  ocr_datetime: float = Field(description="unix timestamp when ocr is performed")
149
149
  ocr_json_version: str = Field(description = "the model does the OCR")
150
150
  @field_validator('ocr_json_version', mode = "before")
151
- def version_to_str(cls, v):
151
+ def version_to_str(cls, v: object) -> str:
152
152
  return str(v)
153
153
  class SplitPage(OCRClusterResponseBc):
154
154
  # model not for LLM response
155
155
  pdf_page_num: int
156
156
  metadata: SplitPageMeta
157
157
  refined_version: Optional[OCRClusterResponse[DtoField]] = Field(default = None, description = "refined processed/ grouped/ merged version of ocr text clusters. ")
158
- def model_dump(self, *arg, **kwarg):
158
+ def model_dump(self, *arg: Any, **kwarg: Any) -> dict[str, Any]:
159
159
  return self.to_doc()
160
- def dump_raw(self, *arg, **kwarg):
160
+ def dump_raw(self, *arg: Any, **kwarg: Any) -> dict[str, Any]:
161
161
  return super(SplitPage, self).model_dump(exclude = ["refined_version"], *arg, **kwarg)
162
- def dump_supercede_parse(self, *arg, **kwarg):
162
+ def dump_supercede_parse(self, *arg: Any, **kwarg: Any) -> dict[str, Any]:
163
163
  return super(SplitPage, self).model_dump(exclude = ["refined_version", "metadata"], *arg, **kwarg)
164
164
  @model_validator(mode="after")
165
165
  def roundtrip_invariant(self, info: ValidationInfo) -> "SplitPage":
@@ -185,7 +185,7 @@ class SplitPage(OCRClusterResponseBc):
185
185
  raise ValueError("Roundtrip invariant failed: dump->validate changed the model")
186
186
 
187
187
  return self
188
- def to_doc(self):
188
+ def to_doc(self) -> dict[str, Any]:
189
189
  """Model to llm one-way serializer with manual slicing logic, can refactor using sliced view
190
190
  with some token saving logic.
191
191
  """
@@ -12,7 +12,7 @@ from langchain_core.callbacks import BaseCallbackHandler
12
12
  from langchain_core.runnables import Runnable
13
13
  from .models import NonText_box_2d, OCRClusterResponse, SplitPage, SplitPageMeta, NonTextCluster, TextCluster
14
14
  from .llm_structured_output import build_structured_output_runnable
15
- from typing import Any, Iterable, cast, Callable, Optional, Literal, Union
15
+ from typing import Any, Iterable, cast, Callable, Optional, Literal, Union, NotRequired, TypedDict, Mapping, Iterator
16
16
  try:
17
17
  from typing import TypeAlias
18
18
  except ImportError: # pragma: no cover
@@ -27,26 +27,33 @@ from pydantic_extension.model_slicing import (ModeSlicingMixin, NotMode, Fronten
27
27
  from pydantic_extension.model_slicing.mixin import ExcludeMode, DtoField
28
28
  from pydantic import BaseModel, Field, model_validator, field_validator, field_serializer
29
29
  from langchain_core.messages import SystemMessage, BaseMessage, HumanMessage
30
- from langchain_core.language_models import BaseChatModel
31
- try:
32
- from .workflow_ingest.providers import WorkflowProviderSettings, build_chat_model
33
- except ImportError: # pragma: no cover
34
- from kg_doc_parser.pdf2png import RawFileLoader
30
+ from kogwistar.llm_tasks.providers import SupportsStructuredOutput
31
+ from .workflow_ingest.providers import WorkflowProviderSettings, build_chat_model
35
32
  from .pdf2png import RawFileLoader
36
33
  PastCompatibleSplitPage: TypeAlias = SplitPage
37
34
 
38
35
 
39
- def _build_ocr_llm(model_name: str, *, callbacks=None):
36
+ class StructuredOutputPayload(TypedDict):
37
+ parsed: NotRequired[BaseModel | None]
38
+ raw: NotRequired[object]
39
+ parsing_error: NotRequired[object]
40
+
41
+
42
+ def _build_ocr_llm(
43
+ model_name: str,
44
+ *,
45
+ callbacks: list[BaseCallbackHandler] | None = None,
46
+ ) -> SupportsStructuredOutput:
40
47
  settings = WorkflowProviderSettings.from_env()
41
48
  spec = settings.ocr.model_copy(update={"model": model_name})
42
49
  return build_chat_model(spec, callbacks=callbacks)
43
50
 
44
51
 
45
- def get_page_json(folder_path, page_num):
52
+ def get_page_json(folder_path: str, page_num: int) -> dict[str, Any]:
46
53
  with open(os.path.join(folder_path, 'page_'+str(page_num)+'.json'), 'r') as f:
47
54
  file_json_raw = json.load(f)
48
55
  return file_json_raw
49
- def regen_page(file_json_raw, use_raw):
56
+ def regen_page(file_json_raw: Mapping[str, Any], use_raw: bool) -> dict[str, Any]:
50
57
  # add compatible to union if want to compatible with past models
51
58
  """regen from json returned by SplitPage.to_doc(), can be view as SplitPage.FromJson(filepath)"""
52
59
  p = PastCompatibleSplitPage(**file_json_raw)
@@ -57,7 +64,7 @@ def regen_page(file_json_raw, use_raw):
57
64
  except:
58
65
  raise
59
66
  return res
60
- def regen_doc(folder_path, use_raw = False):
67
+ def regen_doc(folder_path: str, use_raw: bool = False) -> list[dict[str, Any]]:
61
68
  pages_nums = sorted((int(i.rsplit(".json",1)[0].split("page_",1)[1]) for i in os.listdir(folder_path) if i.endswith('.json') and i.startswith("page_")))
62
69
  pages = []
63
70
  split_pages = []
@@ -66,7 +73,6 @@ def regen_doc(folder_path, use_raw = False):
66
73
  pages.append(get_page_json(folder_path, pn))
67
74
  split_pages.append(regen_page(pages[-1], use_raw = use_raw))
68
75
  except Exception as e:
69
- folder_path,pn
70
76
  print(f'error at page {pn}')
71
77
  print(f'in file {folder_path}')
72
78
  logger.error(f'error at page {pn}')
@@ -163,7 +169,13 @@ class RawOCRResponseMetaless(ModeSlicingMixin, BaseModel):
163
169
  r"Can be null/none if there is no page order assigned and printed and found in the scanned texts. Do not assign page number. Only use page number found.")
164
170
  meaningful_ordering : DtoType[list[int]] = Field(description="The correct meaningful ordering of the identified text clusters. Must cover all OCR_text_clusters once and only once. ")
165
171
 
166
- def get_first_round_response(draft_responses, llm: BaseChatModel, model_name: str, cb: BaseCallbackHandler,
172
+ def _raw_content(value: object) -> object | None:
173
+ """Extract raw SDK content without pretending it is a parsed OCR model."""
174
+ content = getattr(value, "content", None)
175
+ return content if content is not None else value
176
+
177
+
178
+ def get_first_round_response(draft_responses: dict[str, str], llm: SupportsStructuredOutput, model_name: str, cb: BaseCallbackHandler,
167
179
  messages: list[BaseMessage], sys_message, img_message, usage_metadata) -> OCRClusterResponse | None:
168
180
 
169
181
  chain = build_structured_output_runnable(llm, RawOCRResponse, include_raw=True)
@@ -177,15 +189,18 @@ def get_first_round_response(draft_responses, llm: BaseChatModel, model_name: st
177
189
  usage_metadata.append(raw_response.usage_metadata)
178
190
  else:
179
191
  usage_metadata.append(None)
180
- response_with_raw: dict[str, RawOCRResponse] = after_parse.invoke(raw_response)
192
+ response_with_raw = cast(StructuredOutputPayload, after_parse.invoke(raw_response))
181
193
  response: RawOCRResponse | OCRClusterResponse | None
182
- response1 : RawOCRResponse | None = response_with_raw.get('parsed')
194
+ response1 = cast(RawOCRResponse | None, response_with_raw.get('parsed'))
183
195
  parsing_error = response_with_raw.get('parsing_error')
184
196
  if response1 is None:
185
197
 
186
198
  try:
187
- raw = response_with_raw.get('raw')
188
- temp = json.loads(raw.content[0]['text'])
199
+ raw = _raw_content(response_with_raw.get('raw'))
200
+ payload = raw[0].get('text') if isinstance(raw, list) and raw and isinstance(raw[0], dict) else raw
201
+ if not isinstance(payload, (str, bytes, bytearray)):
202
+ raise TypeError("raw OCR response did not contain JSON text")
203
+ temp = json.loads(payload)
189
204
  response1 = RawOCRResponse.model_validate(temp)
190
205
 
191
206
  except:
@@ -207,7 +222,7 @@ def get_first_round_response(draft_responses, llm: BaseChatModel, model_name: st
207
222
  draft_responses[model_name] = ocr_draft_response.text
208
223
  sys_message_2.content += ("If your internal OCR fails. Focus on table parsing mode because my error analysis modes often show that the failing OCR pages are usually highly complicated tables. "
209
224
  f"Try to put in as much data as possible given all text found by simple OCR for your reference:```{ocr_draft_response.text}```" if ocr_draft_response else"")
210
- response_with_raw = cast(dict[str, RawOCRResponse], build_structured_output_runnable(llm, RawOCRResponse, include_raw=True).invoke(
225
+ response_with_raw = cast(StructuredOutputPayload, build_structured_output_runnable(llm, RawOCRResponse, include_raw=True).invoke(
211
226
  [sys_message, img_message]
212
227
  ))
213
228
  raw_ocr_response : None | RawOCRResponse= None
@@ -215,7 +230,10 @@ def get_first_round_response(draft_responses, llm: BaseChatModel, model_name: st
215
230
  raw_ocr_response = cast(RawOCRResponse, response_with_raw.get('parsed'))
216
231
  else:
217
232
  try:
218
- raw_ocr_response = RawOCRResponse.model_validate(json.loads(response_with_raw.get('raw').content))
233
+ raw = _raw_content(response_with_raw.get('raw'))
234
+ if not isinstance(raw, (str, bytes, bytearray)):
235
+ raise TypeError("raw OCR response did not contain JSON text")
236
+ raw_ocr_response = RawOCRResponse.model_validate(json.loads(raw))
219
237
  except:
220
238
  pass
221
239
  if raw_ocr_response is not None:
@@ -223,7 +241,13 @@ def get_first_round_response(draft_responses, llm: BaseChatModel, model_name: st
223
241
  _raw = response_with_raw.get('raw')
224
242
  parsing_error = response_with_raw.get('parsing_error')
225
243
  return response
226
- def validate_response_mutate_inplace(response: OCRClusterResponse | None, response_dict: dict, image_file_path, model_name, page_file_name):
244
+ def validate_response_mutate_inplace(
245
+ response: OCRClusterResponse | None,
246
+ response_dict: dict[str, Any],
247
+ image_file_path: str,
248
+ model_name: str,
249
+ page_file_name: str,
250
+ ) -> SplitPage:
227
251
 
228
252
  if response is None:
229
253
  logger.error(f"LLM returned None as response, file name = {image_file_path}, {model_name=}")
@@ -286,7 +310,7 @@ def RawOCRResponse_to_OCRClusterResponse(raw_response: RawOCRResponse | RawOCRRe
286
310
  "bb_x_max" : i['box_2d'][3],
287
311
  "cluster_number" : i['id']}) for i in non_text_objects]
288
312
  return OCRClusterResponse.model_validate(temp)
289
- def final_resort(draft_responses: dict, messages, page_file_name, model_name, image_file_path):
313
+ def final_resort(draft_responses: dict[str, str], messages: list[BaseMessage], page_file_name: str, model_name: str, image_file_path: str, cb: BaseCallbackHandler) -> None:
290
314
  """
291
315
  One day gemini suddenly cannot run but return a totally different schema, ad hoc code fix to fit the transformed schema and
292
316
  break down document reading into 2 tasks, namely meta and ocr and non ocr recognition
@@ -397,7 +421,14 @@ def TextBoxResponsePlusMetaResponse_to_OCRClusterResponse(raw_response: TextBoxR
397
421
  "cluster_number" : i['id']}) for i in non_text_blocks]
398
422
  return OCRClusterResponse.model_validate(temp)
399
423
  from .utils.langchain import GeminiCostCallbackHandler
400
- def refine_image_response(ok2, response_dict, outfile_name, image_file_path, model_names, cb: GeminiCostCallbackHandler):
424
+ def refine_image_response(
425
+ ok2: bool,
426
+ response_dict: dict[str, Any],
427
+ outfile_name: str,
428
+ image_file_path: str,
429
+ model_names: list[str],
430
+ cb: GeminiCostCallbackHandler,
431
+ ) -> bool:
401
432
 
402
433
  # if allow_page_refine and (not preexisting):
403
434
  if not response_dict:
@@ -452,7 +483,7 @@ def refine_image_response(ok2, response_dict, outfile_name, image_file_path, mod
452
483
 
453
484
  print(response_dict)
454
485
  return refined
455
- def get_messages(image_file_path):
486
+ def get_messages(image_file_path: str) -> tuple[SystemMessage, HumanMessage]:
456
487
 
457
488
  # Open the image in binary mode and read its content.
458
489
  with open(image_file_path, "rb") as image_file:
@@ -543,7 +574,7 @@ def ocr_single_image(gemini_key: str, page_file_name, file_name,
543
574
  i_model += 1
544
575
  if i_model >= min(len(model_names), 20):
545
576
  logger.error(f"All LLM returned None as response, file name = {image_file_path}")
546
- final_resort(draft_responses, messages, page_file_name, model_name, image_file_path)
577
+ final_resort(draft_responses, messages, page_file_name, model_name, image_file_path, cb)
547
578
  finally:
548
579
  time.sleep(5)
549
580
  assert response_dict, Exception("response_dict unbound")
@@ -560,7 +591,12 @@ def ocr_single_image(gemini_key: str, page_file_name, file_name,
560
591
  time.sleep(5)
561
592
 
562
593
  OCRRefineResponse: TypeAlias = OCRClusterResponse[DtoField]
563
- def refine_table_ocr(response_dict, llm: BaseChatModel, cb, error_messages):
594
+ def refine_table_ocr(
595
+ response_dict: dict[str, Any],
596
+ llm: SupportsStructuredOutput,
597
+ cb: BaseCallbackHandler,
598
+ error_messages: list[BaseMessage],
599
+ ) -> bool:
564
600
  if response_dict.get('refined_version'):
565
601
  return False
566
602
  else:
@@ -618,7 +654,7 @@ def refine_table_ocr(response_dict, llm: BaseChatModel, cb, error_messages):
618
654
  return True
619
655
 
620
656
 
621
- def index_doc_group(doc_group_dumped):
657
+ def index_doc_group(doc_group_dumped: Mapping[str, Any]) -> dict[tuple[str, Any], Any]:
622
658
  doc_group_indexed = {(f,i['pdf_page_num']): i for f in doc_group_dumped['documents'] for i in doc_group_dumped['documents'][f]}
623
659
  return doc_group_indexed
624
660
  class Doc(BaseModel):
@@ -626,13 +662,13 @@ class Doc(BaseModel):
626
662
  pages: list[dict[str, Any]] = Field(description = "pages")
627
663
 
628
664
  class DocumentGroup(BaseModel):
629
- documents : dict[str, list|Doc] = Field(description = 'list of documents')
630
- def to_doc_group_indexed(self):
665
+ documents: dict[str, list[dict[str, Any]] | Doc] = Field(description="list of documents")
666
+ def to_doc_group_indexed(self) -> dict[tuple[str, Any], Any]:
631
667
  doc_group = self.model_dump()
632
668
  doc_group_indexed = index_doc_group(doc_group)
633
669
  return doc_group_indexed
634
670
  @staticmethod
635
- def from_doc_folder(folder_path):
671
+ def from_doc_folder(folder_path: str) -> "DocumentGroup":
636
672
  """ Assume the dir contains a list of folder with each folder with the filename
637
673
  each subfolder contains a list of pages
638
674
 
@@ -645,7 +681,7 @@ class DocumentGroup(BaseModel):
645
681
  doc = regen_doc(os.path.join(folder_path, d))
646
682
  doc_group[d] = doc
647
683
  return DocumentGroup(**{"documents": doc_group})
648
- def regen_doc_group(folder_path, use_raw = False):
684
+ def regen_doc_group(folder_path: str, use_raw: bool = False) -> DocumentGroup:
649
685
  """ Assume the dir contains a list of folder with each folder with the filename
650
686
  each subfolder contains a list of pages
651
687
 
@@ -665,7 +701,10 @@ except ImportError: # pragma: no cover
665
701
  from .utils.bounded_threadpool_executor import BoundedExecutor
666
702
 
667
703
 
668
- def get_legacy_loader_like(folder: str, allowed_relative_paths: str | Any):
704
+ def get_legacy_loader_like(
705
+ folder: str,
706
+ allowed_relative_paths: Iterable[str] | None,
707
+ ) -> Iterator[str]:
669
708
 
670
709
 
671
710
  # useful for old flat entry only
@@ -693,7 +732,6 @@ def get_legacy_loader_like(folder: str, allowed_relative_paths: str | Any):
693
732
  if dirs != []:
694
733
  continue
695
734
  for f in files:
696
- full_path = os.path.join(root, f)
697
735
  page_file_name = f
698
736
  if page_file_name.endswith('.png') and not os.path.exists(os.path.join(folder, pdf_fname, page_file_name.rsplit('.',1)[0] + ".json")):
699
737
  pass
@@ -702,11 +740,15 @@ def get_legacy_loader_like(folder: str, allowed_relative_paths: str | Any):
702
740
  yield page_file_name
703
741
  return local_loader()
704
742
 
705
- def batch_gemini_ocr_image(gemini_key, folder = "split_pages", exist_behavior: Literal["ok","skip","raise", 'rerun'] = 'skip',
706
- bounded_executor: Optional[BoundedExecutor] = None,
707
- allowed_relative_paths = None,
708
- loader : RawFileLoader | None= None,
709
- ocr_callback : Callable| None= None):
743
+ def batch_gemini_ocr_image(
744
+ gemini_key: str,
745
+ folder: str = "split_pages",
746
+ exist_behavior: Literal["ok", "skip", "raise", "rerun"] = "skip",
747
+ bounded_executor: Optional[BoundedExecutor] = None,
748
+ allowed_relative_paths: Iterable[str] | None = None,
749
+ loader: RawFileLoader | None = None,
750
+ ocr_callback: Callable[..., object] | None = None,
751
+ ) -> None:
710
752
  # page_file_name = "page_1.png"
711
753
  # file_name = "EXL-00-HI-MSA01-2017.PDF"
712
754