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.
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/PKG-INFO +2 -2
- graph_knowledge_doc_parser-0.2.2/kg_doc_parser/document_ingest_log_config.py +26 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/document_ingester_logger.py +6 -6
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/llm_structured_output.py +23 -5
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/models.py +7 -7
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/ocr.py +78 -36
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/pdf2png.py +74 -48
- {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
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/bounded_threadpool_executor.py +13 -8
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/file_loaders.py +24 -15
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/langchain.py +53 -28
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/log.py +3 -2
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/version_chaining.py +18 -8
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/adapters.py +29 -13
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/cache.py +7 -6
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/cli.py +4 -3
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/clients.py +55 -19
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/demo_harness.py +23 -12
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/design.py +2 -3
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/handlers.py +68 -16
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/layered_contracts.py +32 -20
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/layerwise_llm.py +91 -31
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/ocr_pipeline.py +37 -29
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/page_index.py +23 -14
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/parser_core.py +142 -62
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/parsing.py +23 -16
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/providers.py +83 -41
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/runners.py +15 -9
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/service.py +22 -9
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/pyproject.toml +2 -2
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/README.md +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/__init__.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/cast_hinting.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/text_processing_utils.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/utils/__init__.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/__init__.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/_kogwistar.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/models.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/probe.py +0 -0
- {graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/workflow_ingest/semantics.py +0 -0
- {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.
|
|
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.
|
|
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:
|
|
549
|
-
parent_run_id: Optional[
|
|
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:
|
|
606
|
-
parent_run_id: Optional[
|
|
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:
|
|
722
|
-
parent_run_id: Optional[
|
|
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
|
|
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:
|
|
8
|
-
schema:
|
|
25
|
+
model: StructuredOutputModel,
|
|
26
|
+
schema: object,
|
|
9
27
|
*,
|
|
10
28
|
include_raw: bool = True,
|
|
11
29
|
prefer_json_schema: bool = True,
|
|
12
|
-
) ->
|
|
30
|
+
) -> StructuredOutputRunnable:
|
|
13
31
|
"""Build a structured-output runnable with strict-schema-first fallback."""
|
|
14
|
-
attempts: list[dict[str,
|
|
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"})
|
{graph_knowledge_doc_parser-0.2.1 → graph_knowledge_doc_parser-0.2.2}/kg_doc_parser/models.py
RENAMED
|
@@ -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
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
192
|
+
response_with_raw = cast(StructuredOutputPayload, after_parse.invoke(raw_response))
|
|
181
193
|
response: RawOCRResponse | OCRClusterResponse | None
|
|
182
|
-
response1
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
|