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,444 @@
1
+ from __future__ import annotations
2
+
3
+ """Execution clients for the workflow ingest pipeline.
4
+
5
+ This module keeps the execution surface explicit and testable:
6
+ - `DirectRuntimeIngestClient` runs the local workflow engine end-to-end.
7
+ - `ServerCanonicalKgClient` runs the workflow but hands canonical graph
8
+ persistence to an external server client.
9
+ - `DocumentTreeApiPersistenceClient` adapts the export bundle into the server
10
+ tree-upsert API payload.
11
+
12
+ The classes here are intentionally thin wrappers around the workflow runtime so
13
+ tests can swap transport and persistence behavior without changing workflow
14
+ logic.
15
+ """
16
+
17
+ from abc import ABC, abstractmethod
18
+ from typing import Any
19
+ from uuid import uuid4
20
+
21
+ from kogwistar.engine_core.models import Edge, Node
22
+
23
+ from .design import DEFAULT_WORKFLOW_ID, ensure_ingest_workflow_design
24
+ from .models import (
25
+ CanonicalGraphWriteResult,
26
+ IngestRunHandle,
27
+ IngestRunResult,
28
+ WorkflowExportBundle,
29
+ WorkflowIngestInput,
30
+ )
31
+ from .probe import emit_probe_event
32
+
33
+
34
+ class UnsupportedClientOperation(RuntimeError):
35
+ """Raised when a client path is intentionally not implemented."""
36
+
37
+ pass
38
+
39
+
40
+ class CanonicalGraphPersistenceClient(ABC):
41
+ """Protocol for persisting an exported workflow graph bundle."""
42
+
43
+ @abstractmethod
44
+ def persist_graph_payload(self, bundle: WorkflowExportBundle) -> CanonicalGraphWriteResult:
45
+ raise NotImplementedError
46
+
47
+
48
+ def _jsonable_payload(value: Any) -> Any:
49
+ if hasattr(value, "model_dump"):
50
+ try:
51
+ return value.model_dump(field_mode="backend", dump_format="json")
52
+ except TypeError:
53
+ return value.model_dump()
54
+ if isinstance(value, dict):
55
+ return {str(k): _jsonable_payload(v) for k, v in value.items()}
56
+ if isinstance(value, list):
57
+ return [_jsonable_payload(item) for item in value]
58
+ if isinstance(value, tuple):
59
+ return [_jsonable_payload(item) for item in value]
60
+ return value
61
+
62
+
63
+ def _to_temp_id_graph_payload(graph_payload: dict[str, Any]) -> dict[str, Any]:
64
+ """Adapt a canonical export bundle into the server's batch-temp-id contract."""
65
+
66
+ nodes = [_jsonable_payload(node) for node in graph_payload.get("nodes", [])]
67
+ edges = [_jsonable_payload(edge) for edge in graph_payload.get("edges", [])]
68
+
69
+ node_id_map: dict[str, str] = {}
70
+ for idx, node in enumerate(nodes, start=1):
71
+ original_id = str(node.get("id") or "")
72
+ temp_id = f"nn:{idx}"
73
+ if original_id:
74
+ node_id_map[original_id] = temp_id
75
+ node["id"] = temp_id
76
+
77
+ edge_id_map: dict[str, str] = {}
78
+ for idx, edge in enumerate(edges, start=1):
79
+ original_id = str(edge.get("id") or "")
80
+ temp_id = f"ne:{idx}"
81
+ if original_id:
82
+ edge_id_map[original_id] = temp_id
83
+ edge["id"] = temp_id
84
+
85
+ for edge in edges:
86
+ edge["source_ids"] = [node_id_map.get(str(x), str(x)) for x in edge.get("source_ids", [])]
87
+ edge["target_ids"] = [node_id_map.get(str(x), str(x)) for x in edge.get("target_ids", [])]
88
+ edge["source_edge_ids"] = [
89
+ edge_id_map.get(str(x), str(x)) for x in edge.get("source_edge_ids", []) or []
90
+ ]
91
+ edge["target_edge_ids"] = [
92
+ edge_id_map.get(str(x), str(x)) for x in edge.get("target_edge_ids", []) or []
93
+ ]
94
+
95
+ return {
96
+ "doc_id": str(graph_payload.get("doc_id") or "workflow-ingest-doc"),
97
+ "insertion_method": str(graph_payload.get("insertion_method") or "workflow_ingest"),
98
+ "nodes": nodes,
99
+ "edges": edges,
100
+ }
101
+
102
+
103
+ class DocumentTreeApiPersistenceClient(CanonicalGraphPersistenceClient):
104
+ """Bridge an export bundle to the server document-tree upsert endpoint."""
105
+
106
+ def __init__(
107
+ self,
108
+ *,
109
+ client: Any,
110
+ endpoint: str = "/api/document.upsert_tree",
111
+ base_url: str = "",
112
+ transport: str = "server_http_document_tree",
113
+ server_parser_used: bool = False,
114
+ ) -> None:
115
+ self.client = client
116
+ self.endpoint = endpoint
117
+ self.base_url = base_url.rstrip("/")
118
+ self.transport = transport
119
+ self.server_parser_used = server_parser_used
120
+
121
+ def persist_graph_payload(self, bundle: WorkflowExportBundle) -> CanonicalGraphWriteResult:
122
+ payload = _to_temp_id_graph_payload(bundle.graph_payload)
123
+ endpoint = self.endpoint
124
+ if self.base_url and not endpoint.startswith("http://") and not endpoint.startswith("https://"):
125
+ endpoint = f"{self.base_url}{endpoint}"
126
+ response = self.client.post(endpoint, json=payload)
127
+ status_code = int(getattr(response, "status_code", 500))
128
+ if status_code >= 400:
129
+ body = getattr(response, "text", "")
130
+ raise RuntimeError(
131
+ f"canonical server persistence failed: status={status_code} body={body}"
132
+ )
133
+ response_json = response.json()
134
+ engine_result = response_json.get("engine_result") or {}
135
+ return CanonicalGraphWriteResult(
136
+ persistence_mode="server_canonical",
137
+ kg_authority="server",
138
+ canonical_write_confirmed=str(response_json.get("status") or "").lower() == "ok",
139
+ nodes_written=int(
140
+ engine_result.get("nodes_added")
141
+ or response_json.get("inserted_nodes")
142
+ or len(payload["nodes"])
143
+ ),
144
+ edges_written=int(
145
+ engine_result.get("edges_added")
146
+ or response_json.get("inserted_edges")
147
+ or len(payload["edges"])
148
+ ),
149
+ transport=self.transport,
150
+ server_parser_used=self.server_parser_used,
151
+ )
152
+
153
+
154
+ class IngestExecutionClient(ABC):
155
+ """Shared ingest client contract used by direct and server-backed flows."""
156
+
157
+ @abstractmethod
158
+ def run_ingest(
159
+ self,
160
+ *,
161
+ inp: WorkflowIngestInput,
162
+ workflow_id: str = DEFAULT_WORKFLOW_ID,
163
+ deps: dict[str, Any] | None = None,
164
+ ) -> IngestRunResult:
165
+ raise NotImplementedError
166
+
167
+ @abstractmethod
168
+ def resume_ingest(self, **kwargs) -> IngestRunResult:
169
+ raise NotImplementedError
170
+
171
+ @abstractmethod
172
+ def persist_graph_payload(self, bundle: WorkflowExportBundle) -> CanonicalGraphWriteResult:
173
+ raise NotImplementedError
174
+
175
+ @abstractmethod
176
+ def get_run_trace(self, *, run_id: str) -> list[Any]:
177
+ raise NotImplementedError
178
+
179
+ @abstractmethod
180
+ def get_latest_checkpoint(self, *, run_id: str) -> Any:
181
+ raise NotImplementedError
182
+
183
+
184
+ class DirectRuntimeIngestClient(IngestExecutionClient):
185
+ """Run ingest entirely against the local workflow and knowledge engines."""
186
+
187
+ def __init__(
188
+ self,
189
+ *,
190
+ workflow_engine,
191
+ conversation_engine,
192
+ knowledge_engine=None,
193
+ ) -> None:
194
+ self.workflow_engine = workflow_engine
195
+ self.conversation_engine = conversation_engine
196
+ self.knowledge_engine = knowledge_engine
197
+
198
+ def run_ingest(
199
+ self,
200
+ *,
201
+ inp: WorkflowIngestInput,
202
+ workflow_id: str = DEFAULT_WORKFLOW_ID,
203
+ deps: dict[str, Any] | None = None,
204
+ ) -> IngestRunResult:
205
+ ensure_ingest_workflow_design(self.workflow_engine, workflow_id=workflow_id)
206
+ from .service import build_runtime
207
+
208
+ probe = (deps or {}).get("probe")
209
+ runtime = build_runtime(
210
+ workflow_engine=self.workflow_engine,
211
+ conversation_engine=self.conversation_engine,
212
+ deps={
213
+ "knowledge_engine": self.knowledge_engine,
214
+ "persistence_mode": "local_debug",
215
+ "kg_authority": "local",
216
+ "graph_persistence_client": self,
217
+ **(deps or {}),
218
+ },
219
+ )
220
+ run_id = f"run|{inp.request_id}|{uuid4()}"
221
+ emit_probe_event(
222
+ probe,
223
+ "workflow.run_started",
224
+ request_id=inp.request_id,
225
+ workflow_id=workflow_id,
226
+ execution_mode="direct_runtime",
227
+ run_id=run_id,
228
+ )
229
+ run = runtime.run(
230
+ workflow_id=workflow_id,
231
+ conversation_id=f"ingest:{inp.request_id}",
232
+ turn_node_id=f"ingest:{inp.request_id}:turn:{uuid4()}",
233
+ initial_state={"input": inp.model_dump(field_mode="backend", dump_format="json")},
234
+ run_id=run_id,
235
+ )
236
+ bundle = None
237
+ if "export_bundle" in run.final_state:
238
+ bundle = WorkflowExportBundle.model_validate(run.final_state["export_bundle"])
239
+ emit_probe_event(
240
+ probe,
241
+ "workflow.run_finished",
242
+ request_id=inp.request_id,
243
+ workflow_id=workflow_id,
244
+ execution_mode="direct_runtime",
245
+ run_id=run.run_id,
246
+ status=run.status,
247
+ )
248
+ return IngestRunResult(
249
+ handle=IngestRunHandle(
250
+ run_id=run.run_id,
251
+ workflow_id=workflow_id,
252
+ execution_mode="direct_runtime",
253
+ ),
254
+ status=run.status,
255
+ bundle=bundle,
256
+ final_state=dict(run.final_state),
257
+ )
258
+
259
+ def resume_ingest(self, **kwargs) -> IngestRunResult:
260
+ from .service import build_runtime
261
+
262
+ deps = dict(kwargs.pop("deps", {}) or {})
263
+ probe = deps.get("probe")
264
+ runtime = build_runtime(
265
+ workflow_engine=self.workflow_engine,
266
+ conversation_engine=self.conversation_engine,
267
+ deps={
268
+ "knowledge_engine": self.knowledge_engine,
269
+ "persistence_mode": "local_debug",
270
+ "kg_authority": "local",
271
+ "graph_persistence_client": self,
272
+ **deps,
273
+ },
274
+ )
275
+ emit_probe_event(
276
+ probe,
277
+ "workflow.resume_started",
278
+ execution_mode="direct_runtime",
279
+ run_id=kwargs.get("run_id"),
280
+ )
281
+ resumed = runtime.resume_run(**kwargs)
282
+ bundle = None
283
+ if "export_bundle" in resumed.final_state:
284
+ bundle = WorkflowExportBundle.model_validate(resumed.final_state["export_bundle"])
285
+ workflow_id = kwargs.get("workflow_id", DEFAULT_WORKFLOW_ID)
286
+ emit_probe_event(
287
+ probe,
288
+ "workflow.resume_finished",
289
+ execution_mode="direct_runtime",
290
+ run_id=resumed.run_id,
291
+ status=resumed.status,
292
+ )
293
+ return IngestRunResult(
294
+ handle=IngestRunHandle(
295
+ run_id=resumed.run_id,
296
+ workflow_id=workflow_id,
297
+ execution_mode="direct_runtime",
298
+ ),
299
+ status=resumed.status,
300
+ bundle=bundle,
301
+ final_state=dict(resumed.final_state),
302
+ )
303
+
304
+ def persist_graph_payload(self, bundle: WorkflowExportBundle) -> CanonicalGraphWriteResult:
305
+ if self.knowledge_engine is None:
306
+ return CanonicalGraphWriteResult(
307
+ persistence_mode="local_debug",
308
+ kg_authority="local",
309
+ canonical_write_confirmed=False,
310
+ transport="direct_runtime",
311
+ server_parser_used=False,
312
+ )
313
+ nodes_written = 0
314
+ edges_written = 0
315
+ for node in bundle.graph_payload.get("nodes", []):
316
+ node_obj = node if isinstance(node, Node) else Node.model_validate(node)
317
+ if not self.knowledge_engine.persist.exists_node(str(node_obj.safe_get_id())):
318
+ self.knowledge_engine.write.add_node(node_obj)
319
+ nodes_written += 1
320
+ for edge in bundle.graph_payload.get("edges", []):
321
+ edge_obj = edge if isinstance(edge, Edge) else Edge.model_validate(edge)
322
+ if not self.knowledge_engine.persist.exists_edge(str(edge_obj.safe_get_id())):
323
+ self.knowledge_engine.write.add_edge(edge_obj)
324
+ edges_written += 1
325
+ return CanonicalGraphWriteResult(
326
+ persistence_mode="local_debug",
327
+ kg_authority="local",
328
+ canonical_write_confirmed=False,
329
+ nodes_written=nodes_written,
330
+ edges_written=edges_written,
331
+ transport="direct_runtime",
332
+ server_parser_used=False,
333
+ )
334
+
335
+ def get_run_trace(self, *, run_id: str) -> list[Any]:
336
+ return list(
337
+ self.conversation_engine.read.get_nodes(
338
+ where={"$and": [{"entity_type": "workflow_step_exec"}, {"run_id": str(run_id)}]}
339
+ )
340
+ )
341
+
342
+ def get_latest_checkpoint(self, *, run_id: str) -> Any:
343
+ checkpoints = list(
344
+ self.conversation_engine.read.get_nodes(
345
+ where={"$and": [{"entity_type": "workflow_checkpoint"}, {"run_id": str(run_id)}]}
346
+ )
347
+ )
348
+ if not checkpoints:
349
+ return None
350
+ return max(checkpoints, key=lambda node: int(node.metadata["step_seq"]))
351
+
352
+
353
+ class ServerCanonicalKgClient(IngestExecutionClient):
354
+ """Run ingest locally but delegate canonical graph persistence to a server."""
355
+
356
+ def __init__(
357
+ self,
358
+ *,
359
+ workflow_engine,
360
+ conversation_engine,
361
+ persistence_client: CanonicalGraphPersistenceClient,
362
+ ) -> None:
363
+ self.workflow_engine = workflow_engine
364
+ self.conversation_engine = conversation_engine
365
+ self.persistence_client = persistence_client
366
+
367
+ def run_ingest(
368
+ self,
369
+ *,
370
+ inp: WorkflowIngestInput,
371
+ workflow_id: str = DEFAULT_WORKFLOW_ID,
372
+ deps: dict[str, Any] | None = None,
373
+ ) -> IngestRunResult:
374
+ ensure_ingest_workflow_design(self.workflow_engine, workflow_id=workflow_id)
375
+ from .service import build_runtime
376
+
377
+ probe = (deps or {}).get("probe")
378
+ runtime = build_runtime(
379
+ workflow_engine=self.workflow_engine,
380
+ conversation_engine=self.conversation_engine,
381
+ deps={
382
+ "knowledge_engine": None,
383
+ "persistence_mode": "server_canonical",
384
+ "kg_authority": "server",
385
+ "graph_persistence_client": self.persistence_client,
386
+ **(deps or {}),
387
+ },
388
+ )
389
+ run_id = f"run|{inp.request_id}|{uuid4()}"
390
+ emit_probe_event(
391
+ probe,
392
+ "workflow.run_started",
393
+ request_id=inp.request_id,
394
+ workflow_id=workflow_id,
395
+ execution_mode="server_canonical_client",
396
+ run_id=run_id,
397
+ )
398
+ run = runtime.run(
399
+ workflow_id=workflow_id,
400
+ conversation_id=f"ingest:{inp.request_id}",
401
+ turn_node_id=f"ingest:{inp.request_id}:turn:{uuid4()}",
402
+ initial_state={"input": inp.model_dump(field_mode="backend", dump_format="json")},
403
+ run_id=run_id,
404
+ )
405
+ bundle = None
406
+ if "export_bundle" in run.final_state:
407
+ bundle = WorkflowExportBundle.model_validate(run.final_state["export_bundle"])
408
+ emit_probe_event(
409
+ probe,
410
+ "workflow.run_finished",
411
+ request_id=inp.request_id,
412
+ workflow_id=workflow_id,
413
+ execution_mode="server_canonical_client",
414
+ run_id=run.run_id,
415
+ status=run.status,
416
+ )
417
+ return IngestRunResult(
418
+ handle=IngestRunHandle(
419
+ run_id=run.run_id,
420
+ workflow_id=workflow_id,
421
+ execution_mode="server_canonical_client",
422
+ ),
423
+ status=run.status,
424
+ bundle=bundle,
425
+ final_state=dict(run.final_state),
426
+ )
427
+
428
+ def resume_ingest(self, **kwargs) -> IngestRunResult:
429
+ raise UnsupportedClientOperation(
430
+ "remote/server-backed runtime resume is not implemented in this repo"
431
+ )
432
+
433
+ def persist_graph_payload(self, bundle: WorkflowExportBundle) -> CanonicalGraphWriteResult:
434
+ return self.persistence_client.persist_graph_payload(bundle)
435
+
436
+ def get_run_trace(self, *, run_id: str) -> list[Any]:
437
+ raise UnsupportedClientOperation(
438
+ "server-backed trace retrieval is not implemented in this repo"
439
+ )
440
+
441
+ def get_latest_checkpoint(self, *, run_id: str) -> Any:
442
+ raise UnsupportedClientOperation(
443
+ "server-backed checkpoint retrieval is not implemented in this repo"
444
+ )