python-hwpx-automation 6.0.3__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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Durable server-enforced HWPX workflow engine contracts."""
|
|
3
|
+
|
|
4
|
+
from .models import (
|
|
5
|
+
ActionResult,
|
|
6
|
+
WORKFLOW_SCHEMA_VERSION,
|
|
7
|
+
WorkFamily,
|
|
8
|
+
WorkOrder,
|
|
9
|
+
WorkflowBudget,
|
|
10
|
+
WorkflowEvent,
|
|
11
|
+
WorkflowPolicy,
|
|
12
|
+
WorkflowRecord,
|
|
13
|
+
WorkflowState,
|
|
14
|
+
)
|
|
15
|
+
from .dispatcher import AllowlistedDispatcher, DispatchOutcome, FAMILY_TOOL_ALLOWLISTS
|
|
16
|
+
from .policy import ActionRequest, PolicyViolation, WorkflowPolicyEngine
|
|
17
|
+
from .rendering import (
|
|
18
|
+
RENDER_SCHEMA_VERSION,
|
|
19
|
+
NullRenderBackend,
|
|
20
|
+
RenderBackend,
|
|
21
|
+
RenderJob,
|
|
22
|
+
RenderReceipt,
|
|
23
|
+
RenderStatus,
|
|
24
|
+
)
|
|
25
|
+
from .state_machine import ALLOWED_TRANSITIONS, InvalidTransition
|
|
26
|
+
from .store import WorkflowConflict, WorkflowNotFound, WorkflowStore
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ActionResult",
|
|
30
|
+
"ALLOWED_TRANSITIONS",
|
|
31
|
+
"ActionRequest",
|
|
32
|
+
"AllowlistedDispatcher",
|
|
33
|
+
"DispatchOutcome",
|
|
34
|
+
"FAMILY_TOOL_ALLOWLISTS",
|
|
35
|
+
"InvalidTransition",
|
|
36
|
+
"PolicyViolation",
|
|
37
|
+
"RENDER_SCHEMA_VERSION",
|
|
38
|
+
"NullRenderBackend",
|
|
39
|
+
"RenderBackend",
|
|
40
|
+
"RenderJob",
|
|
41
|
+
"RenderReceipt",
|
|
42
|
+
"RenderStatus",
|
|
43
|
+
"WORKFLOW_SCHEMA_VERSION",
|
|
44
|
+
"WorkFamily",
|
|
45
|
+
"WorkOrder",
|
|
46
|
+
"WorkflowBudget",
|
|
47
|
+
"WorkflowConflict",
|
|
48
|
+
"WorkflowEvent",
|
|
49
|
+
"WorkflowNotFound",
|
|
50
|
+
"WorkflowPolicy",
|
|
51
|
+
"WorkflowPolicyEngine",
|
|
52
|
+
"WorkflowRecord",
|
|
53
|
+
"WorkflowState",
|
|
54
|
+
"WorkflowStore",
|
|
55
|
+
]
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Typed family adapters for the high-level workflow service."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
|
|
11
|
+
from hwpx_automation.document_state import document_revision
|
|
12
|
+
|
|
13
|
+
from .models import WorkFamily, WorkflowRecord
|
|
14
|
+
from .policy import ActionRequest, PolicyViolation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AdapterAbstention(RuntimeError):
|
|
18
|
+
"""The typed request is unsupported or incomplete and needs human review."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, code: str, message: str) -> None:
|
|
21
|
+
self.code = code
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class WorkflowAdapter:
|
|
27
|
+
family: WorkFamily
|
|
28
|
+
|
|
29
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def execution_action(self, record: WorkflowRecord) -> ActionRequest | None:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
def prepare_execution(self, record: WorkflowRecord) -> None:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
|
|
39
|
+
"""Return the mandatory, read-only quality gates for this family."""
|
|
40
|
+
|
|
41
|
+
return ()
|
|
42
|
+
|
|
43
|
+
def execution_evidence_ok(self, record: WorkflowRecord, result: Any) -> bool:
|
|
44
|
+
return isinstance(result, Mapping) and result.get("ok") is not False
|
|
45
|
+
|
|
46
|
+
def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
|
|
47
|
+
"""Fail closed when a mandatory verifier is absent or explicitly fails."""
|
|
48
|
+
|
|
49
|
+
actions = self.verification_actions(record)
|
|
50
|
+
if len(results) != len(actions):
|
|
51
|
+
return False
|
|
52
|
+
for result in results:
|
|
53
|
+
if not isinstance(result, Mapping):
|
|
54
|
+
return False
|
|
55
|
+
if result.get("ok") is False or result.get("pass") is False:
|
|
56
|
+
return False
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def recon_ok(self, result: Any) -> bool:
|
|
60
|
+
return not isinstance(result, Mapping) or result.get("ok", True) is not False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ReadExtractAdapter(WorkflowAdapter):
|
|
64
|
+
_TOOLS = {
|
|
65
|
+
"text": "get_document_text",
|
|
66
|
+
"info": "get_document_info",
|
|
67
|
+
"outline": "get_document_outline",
|
|
68
|
+
"map": "get_document_map",
|
|
69
|
+
"markdown": "hwpx_to_markdown",
|
|
70
|
+
"json": "hwpx_extract_json",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
74
|
+
if not record.work_order.source_path:
|
|
75
|
+
raise AdapterAbstention("SOURCE_REQUIRED", "read/extract requires a source document")
|
|
76
|
+
operation = str(record.work_order.parameters.get("operation", "info"))
|
|
77
|
+
tool = self._TOOLS.get(operation)
|
|
78
|
+
if tool is None:
|
|
79
|
+
raise AdapterAbstention("UNSUPPORTED_READ_OPERATION", f"unsupported read operation: {operation}")
|
|
80
|
+
arguments = dict(record.work_order.parameters.get("arguments") or {})
|
|
81
|
+
arguments.setdefault("filename", record.work_order.source_path)
|
|
82
|
+
return ActionRequest(tool_name=tool, arguments=arguments)
|
|
83
|
+
|
|
84
|
+
def execution_action(self, record: WorkflowRecord) -> ActionRequest | None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TransactionalEditAdapter(WorkflowAdapter):
|
|
89
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
90
|
+
return ActionRequest(tool_name="get_document_info", arguments={"filename": record.work_order.source_path})
|
|
91
|
+
|
|
92
|
+
def prepare_execution(self, record: WorkflowRecord) -> None:
|
|
93
|
+
source = Path(record.work_order.source_path or "")
|
|
94
|
+
output = Path(record.work_order.output_path or "")
|
|
95
|
+
if output.exists():
|
|
96
|
+
if document_revision(output) != record.work_order.expected_revision:
|
|
97
|
+
raise PolicyViolation("OUTPUT_ALREADY_EXISTS", "workflow output exists with unrelated content")
|
|
98
|
+
return
|
|
99
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
shutil.copy2(source, output)
|
|
101
|
+
|
|
102
|
+
def execution_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
103
|
+
operations = record.work_order.parameters.get("operations")
|
|
104
|
+
if not isinstance(operations, list):
|
|
105
|
+
raise AdapterAbstention("OPERATIONS_REQUIRED", "transactional edit requires an operations array")
|
|
106
|
+
output = record.work_order.output_path
|
|
107
|
+
return ActionRequest(
|
|
108
|
+
tool_name="apply_edits",
|
|
109
|
+
arguments={
|
|
110
|
+
"filename": output,
|
|
111
|
+
"operations": operations,
|
|
112
|
+
"dry_run": False,
|
|
113
|
+
"expected_revision": record.work_order.expected_revision,
|
|
114
|
+
"idempotency_key": f"{record.work_order.idempotency_key}:apply",
|
|
115
|
+
},
|
|
116
|
+
destructive=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
|
|
120
|
+
return (
|
|
121
|
+
ActionRequest(
|
|
122
|
+
tool_name="doc_diff",
|
|
123
|
+
arguments={
|
|
124
|
+
"old_filename": record.work_order.source_path,
|
|
125
|
+
"new_filename": record.work_order.output_path,
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def execution_evidence_ok(self, record: WorkflowRecord, result: Any) -> bool:
|
|
131
|
+
return (
|
|
132
|
+
super().execution_evidence_ok(record, result)
|
|
133
|
+
and isinstance(result.get("semanticDiff"), Mapping)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class KnownTemplateFillAdapter(WorkflowAdapter):
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _coordinate_operations(record: WorkflowRecord) -> list[dict[str, Any]] | None:
|
|
140
|
+
if record.work_order.parameters.get("mode") != "coordinate_table":
|
|
141
|
+
return None
|
|
142
|
+
raw = record.work_order.parameters.get("operations")
|
|
143
|
+
if not isinstance(raw, list) or not raw:
|
|
144
|
+
raise AdapterAbstention(
|
|
145
|
+
"FROZEN_FILL_OPERATIONS_REQUIRED",
|
|
146
|
+
"coordinate-table fill requires frozen fill_cell operations",
|
|
147
|
+
)
|
|
148
|
+
operations: list[dict[str, Any]] = []
|
|
149
|
+
coordinates: set[tuple[int, int, int]] = set()
|
|
150
|
+
for value in raw:
|
|
151
|
+
if not isinstance(value, Mapping) or set(value) != {
|
|
152
|
+
"op",
|
|
153
|
+
"table_index",
|
|
154
|
+
"row",
|
|
155
|
+
"col",
|
|
156
|
+
"text",
|
|
157
|
+
}:
|
|
158
|
+
raise AdapterAbstention(
|
|
159
|
+
"FROZEN_FILL_OPERATIONS_REQUIRED",
|
|
160
|
+
"coordinate-table fill accepts exact fill_cell operations only",
|
|
161
|
+
)
|
|
162
|
+
operation = dict(value)
|
|
163
|
+
indices = tuple(operation[key] for key in ("table_index", "row", "col"))
|
|
164
|
+
if (
|
|
165
|
+
operation["op"] != "fill_cell"
|
|
166
|
+
or any(isinstance(item, bool) or not isinstance(item, int) or item < 0 for item in indices)
|
|
167
|
+
or not isinstance(operation["text"], str)
|
|
168
|
+
or not operation["text"]
|
|
169
|
+
or len(operation["text"]) > 4_096
|
|
170
|
+
or indices in coordinates
|
|
171
|
+
):
|
|
172
|
+
raise AdapterAbstention(
|
|
173
|
+
"FROZEN_FILL_OPERATIONS_REQUIRED",
|
|
174
|
+
"coordinate-table fill operation is invalid",
|
|
175
|
+
)
|
|
176
|
+
coordinates.add(indices)
|
|
177
|
+
operations.append(operation)
|
|
178
|
+
return operations
|
|
179
|
+
|
|
180
|
+
def _required(self, record: WorkflowRecord) -> tuple[Any, dict[str, Any]]:
|
|
181
|
+
baseline = record.work_order.parameters.get("baseline")
|
|
182
|
+
content = record.work_order.parameters.get("content")
|
|
183
|
+
if baseline is None or not isinstance(content, dict):
|
|
184
|
+
raise AdapterAbstention("TEMPLATE_INPUT_REQUIRED", "baseline and typed content are required")
|
|
185
|
+
return baseline, content
|
|
186
|
+
|
|
187
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
188
|
+
if self._coordinate_operations(record) is not None:
|
|
189
|
+
return ActionRequest(
|
|
190
|
+
tool_name="scan_form_guidance",
|
|
191
|
+
arguments={"filename": record.work_order.source_path},
|
|
192
|
+
)
|
|
193
|
+
baseline, content = self._required(record)
|
|
194
|
+
return ActionRequest(
|
|
195
|
+
tool_name="analyze_template_formfit",
|
|
196
|
+
arguments={
|
|
197
|
+
"source_filename": record.work_order.source_path,
|
|
198
|
+
"baseline": baseline,
|
|
199
|
+
"content": content,
|
|
200
|
+
"destination_filename": record.work_order.output_path,
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def execution_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
205
|
+
coordinate_operations = self._coordinate_operations(record)
|
|
206
|
+
if coordinate_operations is not None:
|
|
207
|
+
return ActionRequest(
|
|
208
|
+
tool_name="apply_table_ops",
|
|
209
|
+
arguments={
|
|
210
|
+
"filename": record.work_order.source_path,
|
|
211
|
+
"ops": coordinate_operations,
|
|
212
|
+
"output": record.work_order.output_path,
|
|
213
|
+
"dry_run": False,
|
|
214
|
+
"render_check": "off",
|
|
215
|
+
},
|
|
216
|
+
destructive=True,
|
|
217
|
+
)
|
|
218
|
+
baseline, content = self._required(record)
|
|
219
|
+
return ActionRequest(
|
|
220
|
+
tool_name="apply_template_formfit",
|
|
221
|
+
arguments={
|
|
222
|
+
"source_filename": record.work_order.source_path,
|
|
223
|
+
"baseline": baseline,
|
|
224
|
+
"content": content,
|
|
225
|
+
"destination_filename": record.work_order.output_path,
|
|
226
|
+
"confirm": True,
|
|
227
|
+
},
|
|
228
|
+
destructive=True,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
|
|
232
|
+
return (
|
|
233
|
+
ActionRequest(
|
|
234
|
+
tool_name="inspect_fill_residue",
|
|
235
|
+
arguments={
|
|
236
|
+
"filename": record.work_order.output_path,
|
|
237
|
+
"blank_path": record.work_order.source_path,
|
|
238
|
+
},
|
|
239
|
+
),
|
|
240
|
+
ActionRequest(
|
|
241
|
+
tool_name="verify_form_fill",
|
|
242
|
+
arguments={
|
|
243
|
+
"filename": record.work_order.output_path,
|
|
244
|
+
"before_path": record.work_order.source_path,
|
|
245
|
+
"require": False,
|
|
246
|
+
},
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
|
|
251
|
+
return len(results) == 2 and all(
|
|
252
|
+
isinstance(result, Mapping) and result.get("ok") is True for result in results
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class UnknownFormFillAdapter(WorkflowAdapter):
|
|
257
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
258
|
+
return ActionRequest(tool_name="scan_form_guidance", arguments={"filename": record.work_order.source_path})
|
|
259
|
+
|
|
260
|
+
def execution_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
261
|
+
operation_kind = str(record.work_order.parameters.get("operationKind", "table"))
|
|
262
|
+
tools = {"table": "apply_table_ops", "body": "apply_body_ops"}
|
|
263
|
+
tool = tools.get(operation_kind)
|
|
264
|
+
operations = record.work_order.parameters.get("operations")
|
|
265
|
+
if tool is None or not isinstance(operations, list):
|
|
266
|
+
raise AdapterAbstention(
|
|
267
|
+
"FORM_OPERATIONS_REQUIRED",
|
|
268
|
+
"unknown-form fill requires operationKind=table|body and an operations array",
|
|
269
|
+
)
|
|
270
|
+
arguments = {
|
|
271
|
+
"filename": record.work_order.source_path,
|
|
272
|
+
"ops": operations,
|
|
273
|
+
"output": record.work_order.output_path,
|
|
274
|
+
"dry_run": False,
|
|
275
|
+
}
|
|
276
|
+
if tool == "apply_table_ops":
|
|
277
|
+
arguments["render_check"] = "off"
|
|
278
|
+
return ActionRequest(tool_name=tool, arguments=arguments, destructive=True)
|
|
279
|
+
|
|
280
|
+
def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
|
|
281
|
+
return (
|
|
282
|
+
ActionRequest(
|
|
283
|
+
tool_name="inspect_fill_residue",
|
|
284
|
+
arguments={
|
|
285
|
+
"filename": record.work_order.output_path,
|
|
286
|
+
"blank_path": record.work_order.source_path,
|
|
287
|
+
},
|
|
288
|
+
),
|
|
289
|
+
ActionRequest(
|
|
290
|
+
tool_name="verify_form_fill",
|
|
291
|
+
arguments={
|
|
292
|
+
"filename": record.work_order.output_path,
|
|
293
|
+
"before_path": record.work_order.source_path,
|
|
294
|
+
"require": False,
|
|
295
|
+
},
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
|
|
300
|
+
return len(results) == 2 and all(
|
|
301
|
+
isinstance(result, Mapping) and result.get("ok") is True for result in results
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class StructuralTableEditAdapter(WorkflowAdapter):
|
|
306
|
+
"""Apply one frozen row clone plus coordinate fills through ToolSpec."""
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def _operations(record: WorkflowRecord) -> list[dict[str, Any]]:
|
|
310
|
+
raw = record.work_order.parameters.get("operations")
|
|
311
|
+
if not isinstance(raw, list) or len(raw) < 2:
|
|
312
|
+
raise AdapterAbstention(
|
|
313
|
+
"STRUCTURAL_OPERATIONS_REQUIRED",
|
|
314
|
+
"structural table edit requires a row clone and frozen fills",
|
|
315
|
+
)
|
|
316
|
+
operations = [dict(value) for value in raw if isinstance(value, Mapping)]
|
|
317
|
+
if len(operations) != len(raw):
|
|
318
|
+
raise AdapterAbstention(
|
|
319
|
+
"STRUCTURAL_OPERATIONS_REQUIRED", "structural operations are invalid"
|
|
320
|
+
)
|
|
321
|
+
clone = operations[0]
|
|
322
|
+
if (
|
|
323
|
+
set(clone) != {"op", "table_index", "ref_row", "count"}
|
|
324
|
+
or clone.get("op") != "insert_row_by_clone"
|
|
325
|
+
or clone.get("count") != 1
|
|
326
|
+
or any(
|
|
327
|
+
isinstance(clone.get(key), bool)
|
|
328
|
+
or not isinstance(clone.get(key), int)
|
|
329
|
+
or clone.get(key) < 0
|
|
330
|
+
for key in ("table_index", "ref_row")
|
|
331
|
+
)
|
|
332
|
+
):
|
|
333
|
+
raise AdapterAbstention(
|
|
334
|
+
"STRUCTURAL_OPERATIONS_REQUIRED", "row clone operation is invalid"
|
|
335
|
+
)
|
|
336
|
+
target_row = clone["ref_row"] + 1
|
|
337
|
+
columns: set[int] = set()
|
|
338
|
+
for fill in operations[1:]:
|
|
339
|
+
if (
|
|
340
|
+
set(fill) != {"op", "table_index", "row", "col", "text"}
|
|
341
|
+
or fill.get("op") != "fill_cell"
|
|
342
|
+
or fill.get("table_index") != clone["table_index"]
|
|
343
|
+
or fill.get("row") != target_row
|
|
344
|
+
or isinstance(fill.get("col"), bool)
|
|
345
|
+
or not isinstance(fill.get("col"), int)
|
|
346
|
+
or fill["col"] < 0
|
|
347
|
+
or fill["col"] in columns
|
|
348
|
+
or not isinstance(fill.get("text"), str)
|
|
349
|
+
or not fill["text"]
|
|
350
|
+
or len(fill["text"]) > 4_096
|
|
351
|
+
):
|
|
352
|
+
raise AdapterAbstention(
|
|
353
|
+
"STRUCTURAL_OPERATIONS_REQUIRED",
|
|
354
|
+
"frozen structural fill operation is invalid",
|
|
355
|
+
)
|
|
356
|
+
columns.add(fill["col"])
|
|
357
|
+
return operations
|
|
358
|
+
|
|
359
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
360
|
+
self._operations(record)
|
|
361
|
+
return ActionRequest(
|
|
362
|
+
tool_name="scan_form_guidance",
|
|
363
|
+
arguments={"filename": record.work_order.source_path},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def execution_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
367
|
+
return ActionRequest(
|
|
368
|
+
tool_name="apply_table_ops",
|
|
369
|
+
arguments={
|
|
370
|
+
"filename": record.work_order.source_path,
|
|
371
|
+
"ops": self._operations(record),
|
|
372
|
+
"output": record.work_order.output_path,
|
|
373
|
+
"dry_run": False,
|
|
374
|
+
"render_check": "off",
|
|
375
|
+
},
|
|
376
|
+
destructive=True,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
|
|
380
|
+
return (
|
|
381
|
+
ActionRequest(
|
|
382
|
+
tool_name="doc_diff",
|
|
383
|
+
arguments={
|
|
384
|
+
"old_filename": record.work_order.source_path,
|
|
385
|
+
"new_filename": record.work_order.output_path,
|
|
386
|
+
},
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class TypedAuthoringAdapter(WorkflowAdapter):
|
|
392
|
+
def _plan(self, record: WorkflowRecord) -> dict[str, Any]:
|
|
393
|
+
plan = record.work_order.parameters.get("documentPlan")
|
|
394
|
+
if not isinstance(plan, dict):
|
|
395
|
+
raise AdapterAbstention("DOCUMENT_PLAN_REQUIRED", "typed authoring requires documentPlan")
|
|
396
|
+
return plan
|
|
397
|
+
|
|
398
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
399
|
+
return ActionRequest(tool_name="validate_document_plan", arguments={"document_plan": self._plan(record)})
|
|
400
|
+
|
|
401
|
+
def recon_ok(self, result: Any) -> bool:
|
|
402
|
+
return isinstance(result, Mapping) and bool(result.get("ok", result.get("can_create", False)))
|
|
403
|
+
|
|
404
|
+
def prepare_execution(self, record: WorkflowRecord) -> None:
|
|
405
|
+
output = Path(record.work_order.output_path or "")
|
|
406
|
+
if output.exists():
|
|
407
|
+
raise PolicyViolation("OUTPUT_ALREADY_EXISTS", "typed authoring will not overwrite an existing output")
|
|
408
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
409
|
+
|
|
410
|
+
def execution_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
411
|
+
return ActionRequest(
|
|
412
|
+
tool_name="create_document_from_plan",
|
|
413
|
+
arguments={
|
|
414
|
+
"filename": record.work_order.output_path,
|
|
415
|
+
"document_plan": self._plan(record),
|
|
416
|
+
"verify_render": False,
|
|
417
|
+
},
|
|
418
|
+
destructive=True,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def verification_actions(self, record: WorkflowRecord) -> tuple[ActionRequest, ...]:
|
|
422
|
+
actions = [
|
|
423
|
+
ActionRequest(
|
|
424
|
+
tool_name="inspect_document_authoring_quality",
|
|
425
|
+
arguments={
|
|
426
|
+
"filename": record.work_order.output_path,
|
|
427
|
+
"document_plan": self._plan(record),
|
|
428
|
+
"quality_profile": record.work_order.parameters.get("qualityProfile"),
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
]
|
|
432
|
+
metadata = self._plan(record).get("metadata")
|
|
433
|
+
document_type = metadata.get("document_type") if isinstance(metadata, Mapping) else None
|
|
434
|
+
if document_type in {"gongmun", "official", "official_document", "공문"}:
|
|
435
|
+
actions.append(
|
|
436
|
+
ActionRequest(
|
|
437
|
+
tool_name="inspect_official_document_style",
|
|
438
|
+
arguments={"filename": record.work_order.output_path},
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
return tuple(actions)
|
|
442
|
+
|
|
443
|
+
def verification_ok(self, record: WorkflowRecord, results: tuple[Any, ...]) -> bool:
|
|
444
|
+
actions = self.verification_actions(record)
|
|
445
|
+
if len(results) != len(actions) or not results:
|
|
446
|
+
return False
|
|
447
|
+
quality = results[0]
|
|
448
|
+
if not isinstance(quality, Mapping) or quality.get("pass") is not True:
|
|
449
|
+
return False
|
|
450
|
+
return all(
|
|
451
|
+
isinstance(result, Mapping) and result.get("ok") is True
|
|
452
|
+
for result in results[1:]
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class MustAbstainAdapter(WorkflowAdapter):
|
|
457
|
+
"""A durable, no-tool family for an expected safety abstention."""
|
|
458
|
+
|
|
459
|
+
def recon_action(self, record: WorkflowRecord) -> ActionRequest:
|
|
460
|
+
raise AdapterAbstention(
|
|
461
|
+
"UNSUPPORTED_INTENT",
|
|
462
|
+
"must-abstain work orders never dispatch document tools",
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def execution_action(self, record: WorkflowRecord) -> None:
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
ADAPTERS: dict[WorkFamily, WorkflowAdapter] = {
|
|
470
|
+
WorkFamily.READ_EXTRACT: ReadExtractAdapter(WorkFamily.READ_EXTRACT),
|
|
471
|
+
WorkFamily.TRANSACTIONAL_EDIT: TransactionalEditAdapter(WorkFamily.TRANSACTIONAL_EDIT),
|
|
472
|
+
WorkFamily.KNOWN_TEMPLATE_FILL: KnownTemplateFillAdapter(WorkFamily.KNOWN_TEMPLATE_FILL),
|
|
473
|
+
WorkFamily.UNKNOWN_FORM_FILL: UnknownFormFillAdapter(WorkFamily.UNKNOWN_FORM_FILL),
|
|
474
|
+
WorkFamily.STRUCTURAL_TABLE_EDIT: StructuralTableEditAdapter(
|
|
475
|
+
WorkFamily.STRUCTURAL_TABLE_EDIT
|
|
476
|
+
),
|
|
477
|
+
WorkFamily.TYPED_AUTHORING: TypedAuthoringAdapter(WorkFamily.TYPED_AUTHORING),
|
|
478
|
+
WorkFamily.MUST_ABSTAIN: MustAbstainAdapter(WorkFamily.MUST_ABSTAIN),
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
__all__ = ["ADAPTERS", "AdapterAbstention", "WorkflowAdapter"]
|