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.
- graph_knowledge_doc_parser-0.1.0.dist-info/METADATA +326 -0
- graph_knowledge_doc_parser-0.1.0.dist-info/RECORD +38 -0
- graph_knowledge_doc_parser-0.1.0.dist-info/WHEEL +4 -0
- graph_knowledge_doc_parser-0.1.0.dist-info/entry_points.txt +3 -0
- kg_doc_parser/__init__.py +9 -0
- kg_doc_parser/cast_hinting.py +19 -0
- kg_doc_parser/document_ingester_logger.py +766 -0
- kg_doc_parser/models.py +277 -0
- kg_doc_parser/ocr.py +752 -0
- kg_doc_parser/pdf2png.py +286 -0
- kg_doc_parser/semantic_document_splitting_layerwise_edits.py +3302 -0
- kg_doc_parser/text_processing_utils.py +30 -0
- kg_doc_parser/utils/__init__.py +0 -0
- kg_doc_parser/utils/bounded_threadpool_executor.py +37 -0
- kg_doc_parser/utils/file_loaders.py +405 -0
- kg_doc_parser/utils/langchain.py +220 -0
- kg_doc_parser/utils/log.py +135 -0
- kg_doc_parser/utils/version_chaining.py +1278 -0
- kg_doc_parser/workflow_ingest/__init__.py +187 -0
- kg_doc_parser/workflow_ingest/_kogwistar.py +13 -0
- kg_doc_parser/workflow_ingest/adapters.py +212 -0
- kg_doc_parser/workflow_ingest/cache.py +63 -0
- kg_doc_parser/workflow_ingest/cli.py +324 -0
- kg_doc_parser/workflow_ingest/clients.py +444 -0
- kg_doc_parser/workflow_ingest/demo_harness.py +427 -0
- kg_doc_parser/workflow_ingest/design.py +208 -0
- kg_doc_parser/workflow_ingest/handlers.py +617 -0
- kg_doc_parser/workflow_ingest/models.py +575 -0
- kg_doc_parser/workflow_ingest/ocr_pipeline.py +1581 -0
- kg_doc_parser/workflow_ingest/page_index.py +473 -0
- kg_doc_parser/workflow_ingest/parser_core.py +862 -0
- kg_doc_parser/workflow_ingest/parsing.py +249 -0
- kg_doc_parser/workflow_ingest/probe.py +164 -0
- kg_doc_parser/workflow_ingest/providers.py +412 -0
- kg_doc_parser/workflow_ingest/runners.py +546 -0
- kg_doc_parser/workflow_ingest/semantics.py +231 -0
- kg_doc_parser/workflow_ingest/service.py +112 -0
- 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)
|