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,324 @@
1
+ from __future__ import annotations
2
+
3
+ """Command line entrypoints for reusable workflow ingest runners.
4
+
5
+ The CLI is intentionally thin: it only parses arguments, builds provider
6
+ settings, and calls the reusable runner helpers in `kg_doc_parser.workflow_ingest.runners`.
7
+ That keeps the API-first surface available to tests and orchestration code
8
+ while still giving humans a simple command family to run locally.
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Iterable
15
+
16
+ from .demo_harness import DemoHarnessConfig
17
+ from .providers import WorkflowProviderSettings
18
+ from .runners import (
19
+ run_demo_harness_workflow,
20
+ run_layerwise_batch_workflow,
21
+ run_layerwise_source_workflow,
22
+ build_legacy_parse_semantic_fn,
23
+ run_ocr_batch_workflow,
24
+ run_ocr_source_workflow,
25
+ run_page_index_batch_workflow,
26
+ run_page_index_source_workflow,
27
+ )
28
+ from .runners import _fallback_parse_semantic_fn
29
+ from .probe import WorkflowProbe
30
+ from .smoke_assets import generate_ocr_smoke_assets
31
+
32
+
33
+ def _provider_settings_from_args(args: argparse.Namespace) -> WorkflowProviderSettings | None:
34
+ ocr_values = {
35
+ "provider": args.ocr_provider,
36
+ "model": args.ocr_model,
37
+ "base_url": args.ocr_base_url,
38
+ "api_key_env": args.ocr_api_key_env,
39
+ }
40
+ parser_values = {
41
+ "provider": args.parser_provider,
42
+ "model": args.parser_model,
43
+ "base_url": args.parser_base_url,
44
+ "api_key_env": args.parser_api_key_env,
45
+ }
46
+ ocr_override = any(value is not None for value in ocr_values.values())
47
+ parser_override = any(value is not None for value in parser_values.values())
48
+ if not ocr_override and not parser_override:
49
+ return None
50
+ settings = WorkflowProviderSettings.from_env()
51
+ if ocr_override:
52
+ settings = settings.model_copy(
53
+ update={
54
+ "ocr": settings.ocr.model_copy(
55
+ update={key: value for key, value in ocr_values.items() if value is not None}
56
+ )
57
+ }
58
+ )
59
+ if parser_override:
60
+ settings = settings.model_copy(
61
+ update={
62
+ "parser": settings.parser.model_copy(
63
+ update={key: value for key, value in parser_values.items() if value is not None}
64
+ )
65
+ }
66
+ )
67
+ return settings
68
+
69
+
70
+ def _add_provider_args(parser: argparse.ArgumentParser) -> None:
71
+ group = parser.add_argument_group("provider overrides")
72
+ group.add_argument("--ocr-provider", default=None)
73
+ group.add_argument("--ocr-model", default=None)
74
+ group.add_argument("--ocr-base-url", default=None)
75
+ group.add_argument("--ocr-api-key-env", default=None)
76
+ group.add_argument("--parser-provider", default=None)
77
+ group.add_argument("--parser-model", default=None)
78
+ group.add_argument("--parser-base-url", default=None)
79
+ group.add_argument("--parser-api-key-env", default=None)
80
+
81
+
82
+ def _emit_result(result) -> None:
83
+ payload = {
84
+ "kind": result.kind,
85
+ "input_path": str(result.input_path),
86
+ "output_dir": str(result.output_dir),
87
+ "status": result.status,
88
+ "probe_path": str(result.probe_path) if result.probe_path else None,
89
+ "summary_path": str(result.summary_path) if result.summary_path else None,
90
+ "extra": result.extra,
91
+ }
92
+ print(json.dumps(payload, indent=2, sort_keys=True))
93
+
94
+
95
+ def _emit_result_list(results) -> None:
96
+ print(
97
+ json.dumps(
98
+ [
99
+ {
100
+ "kind": result.kind,
101
+ "input_path": str(result.input_path),
102
+ "output_dir": str(result.output_dir),
103
+ "status": result.status,
104
+ "probe_path": str(result.probe_path) if result.probe_path else None,
105
+ "summary_path": str(result.summary_path) if result.summary_path else None,
106
+ "extra": result.extra,
107
+ }
108
+ for result in results
109
+ ],
110
+ indent=2,
111
+ sort_keys=True,
112
+ )
113
+ )
114
+
115
+
116
+ def _build_probe(output_dir: Path, *, command: str) -> WorkflowProbe:
117
+ probe = WorkflowProbe(output_dir / "workflow-events.jsonl")
118
+ probe.emit("cli.command_started", command=command, output_dir=str(output_dir))
119
+ return probe
120
+
121
+
122
+ def _add_common_output_args(parser: argparse.ArgumentParser) -> None:
123
+ parser.add_argument("--output-dir", required=True)
124
+
125
+
126
+ def _ocr_command(args: argparse.Namespace) -> int:
127
+ provider_settings = _provider_settings_from_args(args)
128
+ output_dir = Path(args.output_dir)
129
+ output_dir.mkdir(parents=True, exist_ok=True)
130
+ probe = _build_probe(output_dir, command="ocr")
131
+ deps: dict[str, object] = {"parse_semantic_fn": _fallback_parse_semantic_fn}
132
+ if any(
133
+ value is not None
134
+ for value in (
135
+ args.parser_provider,
136
+ args.parser_model,
137
+ args.parser_base_url,
138
+ args.parser_api_key_env,
139
+ )
140
+ ):
141
+ deps = {
142
+ "parse_semantic_fn": build_legacy_parse_semantic_fn(
143
+ provider_settings=provider_settings,
144
+ model_names=[args.parser_model] if args.parser_model else None,
145
+ )
146
+ }
147
+ try:
148
+ inputs = [Path(item) for item in args.inputs]
149
+ if len(inputs) == 1 and inputs[0].is_file():
150
+ result = run_ocr_source_workflow(
151
+ inputs[0],
152
+ output_dir=output_dir,
153
+ provider_settings=provider_settings,
154
+ ocr_candidate_models=args.ocr_candidate_model or None,
155
+ probe=probe,
156
+ deps=deps,
157
+ )
158
+ _emit_result(result)
159
+ else:
160
+ result = run_ocr_batch_workflow(
161
+ inputs,
162
+ output_dir=output_dir,
163
+ provider_settings=provider_settings,
164
+ ocr_candidate_models=args.ocr_candidate_model or None,
165
+ probe=probe,
166
+ deps=deps,
167
+ )
168
+ _emit_result_list(result)
169
+ probe.emit("cli.command_finished", status="ok")
170
+ return 0
171
+ finally:
172
+ probe.close()
173
+
174
+
175
+ def _page_index_command(args: argparse.Namespace) -> int:
176
+ provider_settings = _provider_settings_from_args(args)
177
+ output_dir = Path(args.output_dir)
178
+ output_dir.mkdir(parents=True, exist_ok=True)
179
+ probe = _build_probe(output_dir, command="page-index")
180
+ try:
181
+ inputs = [Path(item) for item in args.inputs]
182
+ if len(inputs) == 1 and inputs[0].is_file():
183
+ result = run_page_index_source_workflow(
184
+ inputs[0],
185
+ output_dir=output_dir,
186
+ mode=args.mode,
187
+ source_format=args.source_format,
188
+ provider_settings=provider_settings,
189
+ probe=probe,
190
+ )
191
+ _emit_result(result)
192
+ else:
193
+ results = run_page_index_batch_workflow(
194
+ inputs,
195
+ output_dir=output_dir,
196
+ mode=args.mode,
197
+ source_format=args.source_format,
198
+ provider_settings=provider_settings,
199
+ probe=probe,
200
+ )
201
+ _emit_result_list(results)
202
+ probe.emit("cli.command_finished", status="ok")
203
+ return 0
204
+ finally:
205
+ probe.close()
206
+
207
+
208
+ def _layerwise_command(args: argparse.Namespace) -> int:
209
+ output_dir = Path(args.output_dir)
210
+ output_dir.mkdir(parents=True, exist_ok=True)
211
+ probe = _build_probe(output_dir, command="layerwise")
212
+ try:
213
+ inputs = [Path(item) for item in args.inputs]
214
+ if len(inputs) == 1 and inputs[0].is_dir():
215
+ result = run_layerwise_source_workflow(
216
+ inputs[0],
217
+ output_dir=output_dir,
218
+ parsing_mode=args.parsing_mode,
219
+ max_depth=args.max_depth,
220
+ probe=probe,
221
+ )
222
+ _emit_result(result)
223
+ else:
224
+ results = run_layerwise_batch_workflow(
225
+ inputs,
226
+ output_dir=output_dir,
227
+ parsing_mode=args.parsing_mode,
228
+ max_depth=args.max_depth,
229
+ probe=probe,
230
+ )
231
+ _emit_result_list(results)
232
+ probe.emit("cli.command_finished", status="ok")
233
+ return 0
234
+ finally:
235
+ probe.close()
236
+
237
+
238
+ def _demo_command(args: argparse.Namespace) -> int:
239
+ output_dir = Path(args.output_dir)
240
+ output_dir.mkdir(parents=True, exist_ok=True)
241
+ probe = _build_probe(output_dir, command="demo")
242
+ try:
243
+ config = DemoHarnessConfig(
244
+ output_dir=output_dir,
245
+ document_id=args.document_id,
246
+ title=args.title,
247
+ text=args.text,
248
+ parser_mode=args.parser_mode,
249
+ server_mode=args.server_mode,
250
+ external_base_url=args.external_base_url,
251
+ enable_sys_monitoring=not args.disable_sys_monitoring,
252
+ )
253
+ artifacts = run_demo_harness_workflow(config)
254
+ print(json.dumps(artifacts.__dict__, indent=2, sort_keys=True, default=str))
255
+ probe.emit("cli.command_finished", status="ok")
256
+ return 0
257
+ finally:
258
+ probe.close()
259
+
260
+
261
+ def _ocr_smoke_assets_command(args: argparse.Namespace) -> int:
262
+ output_dir = Path(args.output_dir)
263
+ output_dir.mkdir(parents=True, exist_ok=True)
264
+ outputs = generate_ocr_smoke_assets(output_dir)
265
+ print(json.dumps({"output_dir": str(output_dir), **outputs}, indent=2, sort_keys=True))
266
+ return 0
267
+
268
+
269
+ def build_parser() -> argparse.ArgumentParser:
270
+ parser = argparse.ArgumentParser(prog="workflow-ingest", description="Reusable workflow ingest commands")
271
+ subparsers = parser.add_subparsers(dest="command", required=True)
272
+
273
+ ocr = subparsers.add_parser("ocr", help="Run OCR workflow for a file or folder")
274
+ ocr.add_argument("inputs", nargs="+", help="One image/PDF file or a folder of files")
275
+ _add_common_output_args(ocr)
276
+ _add_provider_args(ocr)
277
+ ocr.add_argument("--ocr-candidate-model", action="append", default=[], help="Candidate OCR model names")
278
+ ocr.set_defaults(func=_ocr_command)
279
+
280
+ page_index = subparsers.add_parser("page-index", help="Parse text/markdown into a page-index tree")
281
+ page_index.add_argument("inputs", nargs="+", help="One file or a folder of text/markdown files")
282
+ _add_common_output_args(page_index)
283
+ _add_provider_args(page_index)
284
+ page_index.add_argument("--mode", choices=["heuristic", "ollama"], default="heuristic")
285
+ page_index.add_argument("--source-format", choices=["auto", "text", "markdown"], default="auto")
286
+ page_index.set_defaults(func=_page_index_command)
287
+
288
+ layerwise = subparsers.add_parser("layerwise", help="Run the legacy recursive layerwise parser")
289
+ layerwise.add_argument("inputs", nargs="+", help="One legacy OCR folder or a folder batch")
290
+ _add_common_output_args(layerwise)
291
+ layerwise.add_argument("--parsing-mode", default="snippet")
292
+ layerwise.add_argument("--max-depth", type=int, default=10)
293
+ layerwise.set_defaults(func=_layerwise_command)
294
+
295
+ demo = subparsers.add_parser("demo", help="Run the workflow ingest demo harness")
296
+ _add_common_output_args(demo)
297
+ demo.add_argument("--document-id", default="demo-doc")
298
+ demo.add_argument("--title", default="Workflow Ingest Demo")
299
+ demo.add_argument("--text", default="Alpha clause\nBeta clause\nGamma clause")
300
+ demo.add_argument("--parser-mode", choices=["fake_layered", "legacy_cached"], default="fake_layered")
301
+ demo.add_argument(
302
+ "--server-mode",
303
+ choices=["testclient", "subprocess_http", "external_http"],
304
+ default="testclient",
305
+ )
306
+ demo.add_argument("--external-base-url", default=None)
307
+ demo.add_argument("--disable-sys-monitoring", action="store_true")
308
+ demo.set_defaults(func=_demo_command)
309
+
310
+ smoke = subparsers.add_parser("ocr-smoke-assets", help="Generate OCR smoke assets")
311
+ smoke.add_argument("--output-dir", required=True)
312
+ smoke.set_defaults(func=_ocr_smoke_assets_command)
313
+
314
+ return parser
315
+
316
+
317
+ def main(argv: Iterable[str] | None = None) -> int:
318
+ parser = build_parser()
319
+ args = parser.parse_args(list(argv) if argv is not None else None)
320
+ return int(args.func(args))
321
+
322
+
323
+ if __name__ == "__main__": # pragma: no cover
324
+ raise SystemExit(main())