graph-knowledge-doc-parser 0.1.0__py3-none-any.whl

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 (38) hide show
  1. graph_knowledge_doc_parser-0.1.0.dist-info/METADATA +326 -0
  2. graph_knowledge_doc_parser-0.1.0.dist-info/RECORD +38 -0
  3. graph_knowledge_doc_parser-0.1.0.dist-info/WHEEL +4 -0
  4. graph_knowledge_doc_parser-0.1.0.dist-info/entry_points.txt +3 -0
  5. kg_doc_parser/__init__.py +9 -0
  6. kg_doc_parser/cast_hinting.py +19 -0
  7. kg_doc_parser/document_ingester_logger.py +766 -0
  8. kg_doc_parser/models.py +277 -0
  9. kg_doc_parser/ocr.py +752 -0
  10. kg_doc_parser/pdf2png.py +286 -0
  11. kg_doc_parser/semantic_document_splitting_layerwise_edits.py +3302 -0
  12. kg_doc_parser/text_processing_utils.py +30 -0
  13. kg_doc_parser/utils/__init__.py +0 -0
  14. kg_doc_parser/utils/bounded_threadpool_executor.py +37 -0
  15. kg_doc_parser/utils/file_loaders.py +405 -0
  16. kg_doc_parser/utils/langchain.py +220 -0
  17. kg_doc_parser/utils/log.py +135 -0
  18. kg_doc_parser/utils/version_chaining.py +1278 -0
  19. kg_doc_parser/workflow_ingest/__init__.py +187 -0
  20. kg_doc_parser/workflow_ingest/_kogwistar.py +13 -0
  21. kg_doc_parser/workflow_ingest/adapters.py +212 -0
  22. kg_doc_parser/workflow_ingest/cache.py +63 -0
  23. kg_doc_parser/workflow_ingest/cli.py +324 -0
  24. kg_doc_parser/workflow_ingest/clients.py +444 -0
  25. kg_doc_parser/workflow_ingest/demo_harness.py +427 -0
  26. kg_doc_parser/workflow_ingest/design.py +208 -0
  27. kg_doc_parser/workflow_ingest/handlers.py +617 -0
  28. kg_doc_parser/workflow_ingest/models.py +575 -0
  29. kg_doc_parser/workflow_ingest/ocr_pipeline.py +1581 -0
  30. kg_doc_parser/workflow_ingest/page_index.py +473 -0
  31. kg_doc_parser/workflow_ingest/parser_core.py +862 -0
  32. kg_doc_parser/workflow_ingest/parsing.py +249 -0
  33. kg_doc_parser/workflow_ingest/probe.py +164 -0
  34. kg_doc_parser/workflow_ingest/providers.py +412 -0
  35. kg_doc_parser/workflow_ingest/runners.py +546 -0
  36. kg_doc_parser/workflow_ingest/semantics.py +231 -0
  37. kg_doc_parser/workflow_ingest/service.py +112 -0
  38. kg_doc_parser/workflow_ingest/smoke_assets.py +62 -0
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+
3
+ """Unified public parsing facade for workflow-ingest.
4
+
5
+ This module exposes one parse-first surface for the three parsing lanes that
6
+ already exist in the repo:
7
+
8
+ - ``ocr`` for OCR preparation and normalization
9
+ - ``page_index`` for text / Markdown page-index parsing
10
+ - ``tree`` for the semantic layerwise legacy parser
11
+
12
+ The facade keeps provider/model selection explicit while still falling back to
13
+ ``WorkflowProviderSettings.from_env()`` when callers omit overrides.
14
+ """
15
+
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Any, Literal, Sequence
19
+ from contextlib import contextmanager
20
+ import os
21
+
22
+ from .ocr_pipeline import OCRImagePayload, OCRWorkflowArtifacts, prepare_ocr_workflow_input
23
+ from .page_index import PageIndexParseResult, PageIndexSourceFormat, parse_page_index_document as _parse_page_index_document
24
+ from .providers import WorkflowProviderSettings
25
+ from .semantics import SemanticNode
26
+
27
+ ParseMode = Literal["ocr", "page_index", "tree"]
28
+ OCRParseResult = OCRWorkflowArtifacts
29
+ PageIndexParseResultType = PageIndexParseResult
30
+ TreeParseResult = tuple[SemanticNode, dict[str, Any]]
31
+ ParseDocumentResult = OCRParseResult | PageIndexParseResultType | TreeParseResult
32
+
33
+
34
+ @dataclass(slots=True)
35
+ class OCRParseRequest:
36
+ document_id: str
37
+ title: str
38
+ output_dir: str | Path
39
+ image_payloads: Sequence[OCRImagePayload] | None = None
40
+ pdf_path: str | Path | None = None
41
+ provider_settings: WorkflowProviderSettings | None = None
42
+ provider: str | None = None
43
+ model: str | None = None
44
+ ocr_runner: Any = None
45
+ pdf_rasterizer: Any = None
46
+ ocr_candidate_models: Sequence[str] | None = None
47
+ probe: Any = None
48
+
49
+
50
+ @dataclass(slots=True)
51
+ class PageIndexParseRequest:
52
+ document_id: str
53
+ title: str
54
+ raw_text: str
55
+ source_format: PageIndexSourceFormat = "text"
56
+ mode: Literal["heuristic", "ollama"] = "heuristic"
57
+ provider_settings: WorkflowProviderSettings | None = None
58
+ provider: str | None = None
59
+ model: str | None = None
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class TreeParseRequest:
64
+ doc_id: str
65
+ raw_doc_dict: dict[str, Any]
66
+ parsing_mode: Literal["snippet", "delimiter"] = "snippet"
67
+ max_depth: int = 10
68
+ model_names: Sequence[str] | None = None
69
+ provider_settings: WorkflowProviderSettings | None = None
70
+ provider: str | None = None
71
+ model: str | None = None
72
+
73
+
74
+ def _resolve_provider_settings(
75
+ *,
76
+ provider_settings: WorkflowProviderSettings | None,
77
+ role: Literal["ocr", "parser"],
78
+ provider: str | None = None,
79
+ model: str | None = None,
80
+ ) -> WorkflowProviderSettings:
81
+ base = provider_settings or WorkflowProviderSettings.from_env()
82
+ endpoint = base.ocr if role == "ocr" else base.parser
83
+ updates: dict[str, Any] = {}
84
+ if provider is not None:
85
+ updates["provider"] = provider
86
+ if model is not None:
87
+ updates["model"] = model
88
+ if not updates:
89
+ return base
90
+ return base.model_copy(
91
+ update={
92
+ role: endpoint.model_copy(update=updates),
93
+ }
94
+ )
95
+
96
+
97
+ @contextmanager
98
+ def _temporary_env(overrides: dict[str, str | None]):
99
+ previous: dict[str, str | None] = {}
100
+ try:
101
+ for key, value in overrides.items():
102
+ previous[key] = os.environ.get(key)
103
+ if value is None:
104
+ os.environ.pop(key, None)
105
+ else:
106
+ os.environ[key] = value
107
+ yield
108
+ finally:
109
+ for key, value in previous.items():
110
+ if value is None:
111
+ os.environ.pop(key, None)
112
+ else:
113
+ os.environ[key] = value
114
+
115
+
116
+ def parse_ocr_document(
117
+ *,
118
+ document_id: str,
119
+ title: str,
120
+ output_dir: str | Path,
121
+ image_payloads: Sequence[OCRImagePayload] | None = None,
122
+ pdf_path: str | Path | None = None,
123
+ provider_settings: WorkflowProviderSettings | None = None,
124
+ provider: str | None = None,
125
+ model: str | None = None,
126
+ ocr_runner=None,
127
+ pdf_rasterizer=None,
128
+ ocr_candidate_models: Sequence[str] | None = None,
129
+ probe=None,
130
+ ) -> OCRWorkflowArtifacts:
131
+ """Parse OCR inputs into normalized workflow-ingest artifacts."""
132
+
133
+ settings = _resolve_provider_settings(
134
+ provider_settings=provider_settings,
135
+ role="ocr",
136
+ provider=provider,
137
+ model=model,
138
+ )
139
+ return prepare_ocr_workflow_input(
140
+ document_id=document_id,
141
+ title=title,
142
+ output_dir=output_dir,
143
+ image_payloads=image_payloads,
144
+ pdf_path=pdf_path,
145
+ provider_settings=settings,
146
+ ocr_runner=ocr_runner,
147
+ pdf_rasterizer=pdf_rasterizer,
148
+ ocr_candidate_models=ocr_candidate_models,
149
+ probe=probe,
150
+ )
151
+
152
+
153
+ def parse_page_index_document(
154
+ *,
155
+ document_id: str,
156
+ title: str,
157
+ raw_text: str,
158
+ source_format: PageIndexSourceFormat = "text",
159
+ mode: Literal["heuristic", "ollama"] = "heuristic",
160
+ provider_settings: WorkflowProviderSettings | None = None,
161
+ provider: str | None = None,
162
+ model: str | None = None,
163
+ ) -> PageIndexParseResult:
164
+ """Parse a text / Markdown page-index document into a semantic tree."""
165
+
166
+ settings = _resolve_provider_settings(
167
+ provider_settings=provider_settings,
168
+ role="parser",
169
+ provider=provider,
170
+ model=model,
171
+ )
172
+ return _parse_page_index_document(
173
+ document_id=document_id,
174
+ title=title,
175
+ raw_text=raw_text,
176
+ source_format=source_format,
177
+ mode=mode,
178
+ provider_settings=settings,
179
+ )
180
+
181
+
182
+ def parse_tree_document(
183
+ *,
184
+ doc_id: str,
185
+ raw_doc_dict: dict[str, Any],
186
+ parsing_mode: Literal["snippet", "delimiter"] = "snippet",
187
+ max_depth: int = 10,
188
+ model_names: Sequence[str] | None = None,
189
+ provider_settings: WorkflowProviderSettings | None = None,
190
+ provider: str | None = None,
191
+ model: str | None = None,
192
+ ) -> tuple[SemanticNode, dict[str, Any]]:
193
+ """Parse a legacy split-page OCR document with the layerwise tree parser."""
194
+
195
+ settings = _resolve_provider_settings(
196
+ provider_settings=provider_settings,
197
+ role="parser",
198
+ provider=provider,
199
+ model=model,
200
+ )
201
+ parser_model_names = list(model_names) if model_names else [settings.parser.model]
202
+ env_overrides = {
203
+ "KG_DOC_PARSER_PROVIDER": settings.parser.provider,
204
+ "KG_DOC_PARSER_MODEL": settings.parser.model,
205
+ "KG_DOC_PARSER_TEMPERATURE": str(settings.parser.temperature),
206
+ "KG_DOC_PARSER_BASE_URL": settings.parser.base_url,
207
+ "KG_DOC_PARSER_API_KEY_ENV": settings.parser.api_key_env,
208
+ "KG_DOC_PARSER_PROJECT": settings.parser.project,
209
+ "KG_DOC_PARSER_LOCATION": settings.parser.location,
210
+ "KG_DOC_PARSER_MAX_RETRIES": str(settings.parser.max_retries),
211
+ }
212
+ from kg_doc_parser.semantic_document_splitting_layerwise_edits import parse_doc as legacy_parse_doc
213
+
214
+ with _temporary_env(env_overrides):
215
+ return legacy_parse_doc(
216
+ doc_id=doc_id,
217
+ raw_doc_dict=raw_doc_dict,
218
+ parsing_mode=parsing_mode,
219
+ max_depth=max_depth,
220
+ model_names=parser_model_names,
221
+ )
222
+
223
+
224
+ def parse_document(*, mode: ParseMode, **kwargs):
225
+ """Dispatch to the requested parse mode and return the mode-specific result."""
226
+
227
+ if mode == "ocr":
228
+ return parse_ocr_document(**kwargs)
229
+ if mode == "page_index":
230
+ return parse_page_index_document(**kwargs)
231
+ if mode == "tree":
232
+ return parse_tree_document(**kwargs)
233
+ raise ValueError(f"unsupported parse mode: {mode}")
234
+
235
+
236
+ __all__ = [
237
+ "OCRParseRequest",
238
+ "OCRParseResult",
239
+ "PageIndexParseRequest",
240
+ "PageIndexParseResultType",
241
+ "ParseMode",
242
+ "ParseDocumentResult",
243
+ "TreeParseRequest",
244
+ "TreeParseResult",
245
+ "parse_document",
246
+ "parse_ocr_document",
247
+ "parse_page_index_document",
248
+ "parse_tree_document",
249
+ ]
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ import threading
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ def _utc_now() -> str:
13
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
14
+
15
+
16
+ def _jsonable(value: Any) -> Any:
17
+ if hasattr(value, "model_dump"):
18
+ try:
19
+ return value.model_dump(field_mode="backend", dump_format="json")
20
+ except TypeError:
21
+ return value.model_dump()
22
+ if isinstance(value, dict):
23
+ return {str(k): _jsonable(v) for k, v in value.items()}
24
+ if isinstance(value, (list, tuple)):
25
+ return [_jsonable(v) for v in value]
26
+ if isinstance(value, Path):
27
+ return str(value)
28
+ return value
29
+
30
+
31
+ @dataclass
32
+ class _SysMonitoringState:
33
+ tool_id: int
34
+ code_name_allowlist: set[str]
35
+ file_substrings: tuple[str, ...]
36
+
37
+
38
+ class WorkflowProbe:
39
+ """Small JSONL probe sink for demo-friendly workflow event trails."""
40
+
41
+ def __init__(self, path: str | Path) -> None:
42
+ self.path = Path(path)
43
+ self.path.parent.mkdir(parents=True, exist_ok=True)
44
+ self._lock = threading.Lock()
45
+ self._sys_monitoring: _SysMonitoringState | None = None
46
+
47
+ def emit(self, kind: str, /, **payload: Any) -> None:
48
+ event = {"ts": _utc_now(), "kind": str(kind), **_jsonable(payload)}
49
+ line = json.dumps(event, ensure_ascii=True)
50
+ with self._lock:
51
+ with self.path.open("a", encoding="utf-8") as fh:
52
+ fh.write(line)
53
+ fh.write("\n")
54
+
55
+ def enable_sys_monitoring(
56
+ self,
57
+ *,
58
+ code_name_allowlist: set[str] | None = None,
59
+ file_substrings: tuple[str, ...] = ("workflow_ingest",),
60
+ ) -> bool:
61
+ monitoring = getattr(sys, "monitoring", None)
62
+ if monitoring is None:
63
+ self.emit("probe.sys_monitoring_unavailable", reason="sys.monitoring missing")
64
+ return False
65
+ try:
66
+ tool_id = 5
67
+ monitoring.use_tool_id(tool_id, "kg_doc_parser_demo_probe")
68
+ allowlist = set(code_name_allowlist or {
69
+ "_normalize_input",
70
+ "_build_source_map",
71
+ "_init_parse_session",
72
+ "_prepare_layer_frontier",
73
+ "_propose_layer_breakdown",
74
+ "_review_cud_proposal",
75
+ "_apply_cud_update",
76
+ "_check_layer_coverage",
77
+ "_check_layer_satisfaction",
78
+ "_repair_layer_pointers",
79
+ "_dedupe_and_filter_layer",
80
+ "_commit_layer_children",
81
+ "_enqueue_next_layer_frontier",
82
+ "_finalize_semantic_tree",
83
+ "_validate_tree",
84
+ "_export_graph",
85
+ "_persist_canonical_graph",
86
+ })
87
+ state = _SysMonitoringState(
88
+ tool_id=tool_id,
89
+ code_name_allowlist=allowlist,
90
+ file_substrings=file_substrings,
91
+ )
92
+ self._sys_monitoring = state
93
+
94
+ def _should_log(code: Any) -> bool:
95
+ filename = str(getattr(code, "co_filename", "") or "")
96
+ if state.code_name_allowlist and getattr(code, "co_name", "") not in state.code_name_allowlist:
97
+ return False
98
+ return any(part in filename for part in state.file_substrings)
99
+
100
+ def _on_start(code: Any, offset: int) -> None:
101
+ if _should_log(code):
102
+ self.emit(
103
+ "probe.sys_monitoring.start",
104
+ code_name=getattr(code, "co_name", None),
105
+ filename=getattr(code, "co_filename", None),
106
+ offset=int(offset),
107
+ )
108
+
109
+ def _on_return(code: Any, offset: int, value: Any) -> None:
110
+ if _should_log(code):
111
+ self.emit(
112
+ "probe.sys_monitoring.return",
113
+ code_name=getattr(code, "co_name", None),
114
+ filename=getattr(code, "co_filename", None),
115
+ offset=int(offset),
116
+ )
117
+
118
+ def _on_raise(code: Any, offset: int, exc: Any) -> None:
119
+ if _should_log(code):
120
+ self.emit(
121
+ "probe.sys_monitoring.raise",
122
+ code_name=getattr(code, "co_name", None),
123
+ filename=getattr(code, "co_filename", None),
124
+ offset=int(offset),
125
+ error=repr(exc),
126
+ )
127
+
128
+ monitoring.register_callback(tool_id, monitoring.events.PY_START, _on_start)
129
+ monitoring.register_callback(tool_id, monitoring.events.PY_RETURN, _on_return)
130
+ monitoring.register_callback(tool_id, monitoring.events.RAISE, _on_raise)
131
+ monitoring.set_events(
132
+ tool_id,
133
+ monitoring.events.PY_START | monitoring.events.PY_RETURN | monitoring.events.RAISE,
134
+ )
135
+ self.emit("probe.sys_monitoring_enabled", tool_id=tool_id)
136
+ return True
137
+ except Exception as exc: # noqa: BLE001
138
+ self.emit("probe.sys_monitoring_enable_failed", error=repr(exc))
139
+ self._sys_monitoring = None
140
+ return False
141
+
142
+ def disable_sys_monitoring(self) -> None:
143
+ monitoring = getattr(sys, "monitoring", None)
144
+ if monitoring is None or self._sys_monitoring is None:
145
+ return
146
+ state = self._sys_monitoring
147
+ try:
148
+ monitoring.set_events(state.tool_id, 0)
149
+ monitoring.free_tool_id(state.tool_id)
150
+ self.emit("probe.sys_monitoring_disabled", tool_id=state.tool_id)
151
+ except Exception as exc: # noqa: BLE001
152
+ self.emit("probe.sys_monitoring_disable_failed", error=repr(exc))
153
+ finally:
154
+ self._sys_monitoring = None
155
+
156
+ def close(self) -> None:
157
+ self.disable_sys_monitoring()
158
+ self.emit("probe.closed", path=str(self.path))
159
+
160
+
161
+ def emit_probe_event(probe: WorkflowProbe | None, kind: str, /, **payload: Any) -> None:
162
+ if probe is None:
163
+ return
164
+ probe.emit(kind, **payload)