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,566 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Quality and render automation handlers for the optional MCP adapter."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated, Any
|
|
12
|
+
|
|
13
|
+
import mcp.types as mcp_types
|
|
14
|
+
from hwpx import (
|
|
15
|
+
doc_diff as build_hwpx_doc_diff,
|
|
16
|
+
)
|
|
17
|
+
from hwpx import (
|
|
18
|
+
inspect_reference_consistency as inspect_hwpx_reference_consistency,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from ..configuration import env_float, env_value
|
|
22
|
+
from ..identity import product_identity
|
|
23
|
+
from ..fastmcp_adapter import snapshot_runtime_tools
|
|
24
|
+
from ..office.authoring import (
|
|
25
|
+
inspect_operating_plan_quality as inspect_operating_plan_document_quality,
|
|
26
|
+
)
|
|
27
|
+
from ..office.authoring.presets import (
|
|
28
|
+
inspect_proposal_quality as inspect_proposal_document_quality,
|
|
29
|
+
)
|
|
30
|
+
from ..office.compliance import (
|
|
31
|
+
inspect_official_document_style as inspect_hwpx_official_document_style,
|
|
32
|
+
)
|
|
33
|
+
from ..preview_output_models import RenderPreviewOutput
|
|
34
|
+
from ..runtime_services import RUNTIME_SERVICES
|
|
35
|
+
from ..tool_contract import (
|
|
36
|
+
contract_hash as tool_contract_hash,
|
|
37
|
+
)
|
|
38
|
+
from ..tool_contract import (
|
|
39
|
+
expected_tool_names,
|
|
40
|
+
expected_tool_order,
|
|
41
|
+
skill_required_tool_names,
|
|
42
|
+
validate_registered_tools,
|
|
43
|
+
)
|
|
44
|
+
from ..utils.helpers import default_max_chars, resolve_path
|
|
45
|
+
from ..workflow.render_queue import RenderQueueError
|
|
46
|
+
from ..workflow.rendering import NullRenderClientV2, RenderJobV2
|
|
47
|
+
from ..workspace import (
|
|
48
|
+
WORKSPACE_ROOTS_ENV,
|
|
49
|
+
WorkspaceConfigurationError,
|
|
50
|
+
WorkspaceResolver,
|
|
51
|
+
)
|
|
52
|
+
from ._shared import (
|
|
53
|
+
_capability_block,
|
|
54
|
+
_diff_sources,
|
|
55
|
+
_inspect_authoring_quality,
|
|
56
|
+
_package_version,
|
|
57
|
+
_proposal_quality_fallback,
|
|
58
|
+
_render_client,
|
|
59
|
+
_with_document_state,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_DEFAULT_FETCH_TIMEOUT_SECONDS = 20.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fastmcp_tool_names() -> list[str]:
|
|
66
|
+
"""Return names observed through the isolated FastMCP adapter seam."""
|
|
67
|
+
|
|
68
|
+
return list(snapshot_runtime_tools(RUNTIME_SERVICES.require_mcp()))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def inspect_document_authoring_quality(
|
|
72
|
+
filename: str,
|
|
73
|
+
document_plan: dict = None,
|
|
74
|
+
quality_profile: str = None,
|
|
75
|
+
profile: dict = None,
|
|
76
|
+
) -> dict:
|
|
77
|
+
"""document-plan 기반 생성물의 reopen/package/schema 품질 근거를 조회합니다."""
|
|
78
|
+
path = resolve_path(filename)
|
|
79
|
+
return _inspect_authoring_quality(
|
|
80
|
+
path,
|
|
81
|
+
document_plan=document_plan,
|
|
82
|
+
quality_profile=quality_profile,
|
|
83
|
+
profile=profile,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def inspect_operating_plan_quality(
|
|
88
|
+
filename: str,
|
|
89
|
+
document_plan: dict = None,
|
|
90
|
+
profile: dict = None,
|
|
91
|
+
) -> dict:
|
|
92
|
+
"""운영 계획서 제출 후보의 file-only 품질 프로필을 반환합니다."""
|
|
93
|
+
path = resolve_path(filename)
|
|
94
|
+
if inspect_operating_plan_document_quality is not None:
|
|
95
|
+
return inspect_operating_plan_document_quality(
|
|
96
|
+
path, plan=document_plan, profile=profile
|
|
97
|
+
)
|
|
98
|
+
report = _inspect_authoring_quality(
|
|
99
|
+
path,
|
|
100
|
+
document_plan=document_plan,
|
|
101
|
+
quality_profile={"name": "operating_plan", **dict(profile or {})},
|
|
102
|
+
)
|
|
103
|
+
return report.get("profiles", {}).get("operating_plan", report)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def inspect_official_document_style(
|
|
107
|
+
filename: str = None,
|
|
108
|
+
paragraphs: list[str] = None,
|
|
109
|
+
document_plan: dict = None,
|
|
110
|
+
) -> dict:
|
|
111
|
+
"""공문서 작성규정 lint를 실행하고 위반별 수정 제안을 반환합니다."""
|
|
112
|
+
if inspect_hwpx_official_document_style is None:
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
"installed python-hwpx does not provide official-document lint"
|
|
115
|
+
)
|
|
116
|
+
if filename:
|
|
117
|
+
path = resolve_path(filename)
|
|
118
|
+
return _with_document_state(inspect_hwpx_official_document_style(path), path)
|
|
119
|
+
if document_plan is not None:
|
|
120
|
+
return inspect_hwpx_official_document_style(document_plan or {})
|
|
121
|
+
if paragraphs is not None:
|
|
122
|
+
return inspect_hwpx_official_document_style(paragraphs or [])
|
|
123
|
+
raise ValueError("filename, document_plan, or paragraphs is required")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def doc_diff(
|
|
127
|
+
old_filename: str = None,
|
|
128
|
+
new_filename: str = None,
|
|
129
|
+
old_paragraphs: list[str] = None,
|
|
130
|
+
new_paragraphs: list[str] = None,
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""두 문서 또는 문단 목록의 LCS 기반 신구 paragraph diff를 반환합니다."""
|
|
133
|
+
if build_hwpx_doc_diff is None:
|
|
134
|
+
raise RuntimeError("installed python-hwpx does not provide doc_diff")
|
|
135
|
+
old_source, new_source = _diff_sources(
|
|
136
|
+
old_filename=old_filename,
|
|
137
|
+
new_filename=new_filename,
|
|
138
|
+
old_paragraphs=old_paragraphs,
|
|
139
|
+
new_paragraphs=new_paragraphs,
|
|
140
|
+
)
|
|
141
|
+
return build_hwpx_doc_diff(old_source, new_source)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def inspect_reference_consistency(
|
|
145
|
+
filename: str = None,
|
|
146
|
+
paragraphs: list[str] = None,
|
|
147
|
+
document_plan: dict = None,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""붙임 참조와 표/그림 번호 연속성의 의미 수준 정합성을 검사합니다."""
|
|
150
|
+
if inspect_hwpx_reference_consistency is None:
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
"installed python-hwpx does not provide reference consistency lint"
|
|
153
|
+
)
|
|
154
|
+
if filename:
|
|
155
|
+
path = resolve_path(filename)
|
|
156
|
+
return _with_document_state(inspect_hwpx_reference_consistency(path), path)
|
|
157
|
+
if document_plan is not None:
|
|
158
|
+
return inspect_hwpx_reference_consistency(document_plan or {})
|
|
159
|
+
if paragraphs is not None:
|
|
160
|
+
return inspect_hwpx_reference_consistency(paragraphs or [])
|
|
161
|
+
raise ValueError("filename, document_plan, or paragraphs is required")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def inspect_document_quality(filename: str, rubric: str = "proposal") -> dict:
|
|
165
|
+
"""생성된 HWPX 문서를 제안서 품질 루브릭으로 점검합니다."""
|
|
166
|
+
if rubric != "proposal":
|
|
167
|
+
raise ValueError("rubric must be 'proposal'")
|
|
168
|
+
path = resolve_path(filename)
|
|
169
|
+
if inspect_proposal_document_quality is not None:
|
|
170
|
+
return inspect_proposal_document_quality(path)
|
|
171
|
+
return _proposal_quality_fallback(path)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def render_preview(
|
|
175
|
+
filename: str,
|
|
176
|
+
output_dir: str | None = None,
|
|
177
|
+
mode: str = "pages",
|
|
178
|
+
screenshot: str = "auto",
|
|
179
|
+
max_pages: int | None = None,
|
|
180
|
+
embed_images: bool = True,
|
|
181
|
+
max_image_bytes: int | None = None,
|
|
182
|
+
viewer: bool = False,
|
|
183
|
+
) -> Annotated[mcp_types.CallToolResult, RenderPreviewOutput]:
|
|
184
|
+
"""레이아웃 충실 HTML과 headless browser PNG 프리뷰 산출물을 생성합니다.
|
|
185
|
+
|
|
186
|
+
embed_images 가 참이면 각 페이지 PNG 를 인라인 이미지 콘텐츠 블록으로 함께
|
|
187
|
+
반환해 (한컴/ComputerUse 없이) 모델이 레이아웃을 직접 볼 수 있습니다. 구조화
|
|
188
|
+
매니페스트(JSON)는 structuredContent 로 그대로 유지됩니다.
|
|
189
|
+
|
|
190
|
+
viewer 가 참이면 매니페스트에 자체완결 스크롤 문서 뷰어(수식은
|
|
191
|
+
python-hwpx[preview] 설치 시 네이티브 MathML)를 workspace 안의
|
|
192
|
+
viewer.html 로 쓰고 structuredContent.viewer 로 함께 반환합니다. 뷰어는
|
|
193
|
+
래스터화와 독립이므로 가벼운 텍스트 경로에는 screenshot="off" 와 함께
|
|
194
|
+
쓰세요. viewer.equationRendering 은 MathML/latex/script 폴백 개수로 충실도
|
|
195
|
+
티어를 정직하게 표면화합니다.
|
|
196
|
+
"""
|
|
197
|
+
manifest = RUNTIME_SERVICES.ops.render_preview(
|
|
198
|
+
path=filename,
|
|
199
|
+
output_dir=output_dir,
|
|
200
|
+
mode=mode,
|
|
201
|
+
screenshot=screenshot,
|
|
202
|
+
max_pages=max_pages,
|
|
203
|
+
embed_images=embed_images,
|
|
204
|
+
max_image_bytes=max_image_bytes,
|
|
205
|
+
viewer=viewer,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
images: list[mcp_types.ImageContent] = []
|
|
209
|
+
for shot in manifest.get("screenshots", []):
|
|
210
|
+
data = shot.pop("imageBase64", None)
|
|
211
|
+
mime = shot.pop("imageMime", "image/png")
|
|
212
|
+
if data:
|
|
213
|
+
images.append(
|
|
214
|
+
mcp_types.ImageContent(type="image", data=data, mimeType=mime)
|
|
215
|
+
)
|
|
216
|
+
shot["imageEmbedded"] = True
|
|
217
|
+
elif embed_images:
|
|
218
|
+
shot["imageEmbedded"] = False
|
|
219
|
+
|
|
220
|
+
content: list[mcp_types.ContentBlock] = [
|
|
221
|
+
mcp_types.TextContent(
|
|
222
|
+
type="text", text=json.dumps(manifest, ensure_ascii=False, indent=2)
|
|
223
|
+
)
|
|
224
|
+
]
|
|
225
|
+
content.extend(images)
|
|
226
|
+
return mcp_types.CallToolResult(
|
|
227
|
+
content=content,
|
|
228
|
+
structuredContent=manifest,
|
|
229
|
+
isError=False,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def describe_capabilities(domain: str | None = None) -> dict:
|
|
234
|
+
"""이 HWPX 툴킷이 무엇을 할 수 있는지 작업 종류별로 요약한 능력 지도를 반환합니다.
|
|
235
|
+
|
|
236
|
+
도구 등록을 단일 ToolSpec 계약에서 생성하므로 실제 FastMCP 표면과
|
|
237
|
+
capability map이 항상 같은 진실을 반영합니다. 이 도구를 한 번
|
|
238
|
+
부르면 작업군(읽기·양식채움·생성·편집·표·서식·차례·PII·레드라인·시험지·직인·
|
|
239
|
+
대량생산·메모·검증·패키지)별 intent + 언제 쓰는지 + 진입점 도구가 나옵니다.
|
|
240
|
+
domain 인자로 한 작업군 상세만 필터할 수 있습니다(예: domain="form_fill").
|
|
241
|
+
처음 이 서버로 HWPX 작업을 시작하는 에이전트는 이걸 먼저 부르면 오리엔테이션이
|
|
242
|
+
됩니다. coverage에 등록 도구 대비 미매핑이 표시되면 그건 이 지도의 드리프트입니다."""
|
|
243
|
+
from ..capabilities import build_capability_report, coverage_against
|
|
244
|
+
|
|
245
|
+
advanced = RUNTIME_SERVICES.active_advanced
|
|
246
|
+
report = build_capability_report(domain if domain else None, advanced=advanced)
|
|
247
|
+
live = set(_fastmcp_tool_names())
|
|
248
|
+
report["toolCount"] = len(live)
|
|
249
|
+
validation = (
|
|
250
|
+
validate_registered_tools(
|
|
251
|
+
RUNTIME_SERVICES.require_mcp(), RUNTIME_SERVICES.tool_registry
|
|
252
|
+
)
|
|
253
|
+
if RUNTIME_SERVICES.tool_registry is not None
|
|
254
|
+
else {"ok": False, "missing": sorted(expected_tool_names(advanced=advanced))}
|
|
255
|
+
)
|
|
256
|
+
report["coverage"] = coverage_against(
|
|
257
|
+
live,
|
|
258
|
+
advanced=advanced,
|
|
259
|
+
registry_validation=validation,
|
|
260
|
+
)
|
|
261
|
+
return report
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def mcp_server_health() -> dict:
|
|
265
|
+
"""MCP 서버 transport와 timeout/keepalive 점검 정보를 반환합니다."""
|
|
266
|
+
transport = env_value("TRANSPORT", "stdio")
|
|
267
|
+
try:
|
|
268
|
+
workspace = WorkspaceResolver.from_environment().describe()
|
|
269
|
+
except WorkspaceConfigurationError as exc:
|
|
270
|
+
workspace = {
|
|
271
|
+
"source": "invalid",
|
|
272
|
+
"roots": [],
|
|
273
|
+
"rootCount": 0,
|
|
274
|
+
"relativePathRoot": None,
|
|
275
|
+
"failClosed": True,
|
|
276
|
+
"configurationError": str(exc),
|
|
277
|
+
}
|
|
278
|
+
advanced = RUNTIME_SERVICES.active_advanced
|
|
279
|
+
registry_validation: dict[str, Any] = (
|
|
280
|
+
validate_registered_tools(
|
|
281
|
+
RUNTIME_SERVICES.require_mcp(), RUNTIME_SERVICES.tool_registry
|
|
282
|
+
)
|
|
283
|
+
if RUNTIME_SERVICES.tool_registry is not None
|
|
284
|
+
else {
|
|
285
|
+
"ok": False,
|
|
286
|
+
"missing": sorted(expected_tool_names(advanced=advanced)),
|
|
287
|
+
"unexpected": [],
|
|
288
|
+
"callableMismatches": [],
|
|
289
|
+
"inputSchemaMismatches": [],
|
|
290
|
+
"outputSchemaMismatches": [],
|
|
291
|
+
"descriptionMismatches": [],
|
|
292
|
+
"unavailable": [],
|
|
293
|
+
"orderMismatch": False,
|
|
294
|
+
"expectedOrder": list(expected_tool_order(advanced=advanced)),
|
|
295
|
+
"actualOrder": [],
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
fastmcp_tool_names = set(registry_validation["actualOrder"])
|
|
299
|
+
expected = expected_tool_names(advanced=advanced)
|
|
300
|
+
active_required = skill_required_tool_names() & expected
|
|
301
|
+
missing_expected = sorted(expected - fastmcp_tool_names)
|
|
302
|
+
unexpected_registered = sorted(fastmcp_tool_names - expected)
|
|
303
|
+
missing_required = sorted(active_required - fastmcp_tool_names)
|
|
304
|
+
skew_detected = bool(
|
|
305
|
+
missing_expected
|
|
306
|
+
or unexpected_registered
|
|
307
|
+
or missing_required
|
|
308
|
+
or not registry_validation["ok"]
|
|
309
|
+
)
|
|
310
|
+
surface_details = []
|
|
311
|
+
if missing_expected:
|
|
312
|
+
surface_details.append(f"missing expected: {', '.join(missing_expected)}")
|
|
313
|
+
if unexpected_registered:
|
|
314
|
+
surface_details.append(
|
|
315
|
+
f"unexpected registered: {', '.join(unexpected_registered)}"
|
|
316
|
+
)
|
|
317
|
+
if missing_required:
|
|
318
|
+
surface_details.append(f"missing skill-required: {', '.join(missing_required)}")
|
|
319
|
+
if registry_validation["orderMismatch"]:
|
|
320
|
+
surface_details.append("registered tool order differs from ToolSpec order")
|
|
321
|
+
for key, label in (
|
|
322
|
+
("callableMismatches", "callable mismatch"),
|
|
323
|
+
("inputSchemaMismatches", "input schema mismatch"),
|
|
324
|
+
("outputSchemaMismatches", "output schema mismatch"),
|
|
325
|
+
("descriptionMismatches", "description mismatch"),
|
|
326
|
+
("unavailable", "unavailable"),
|
|
327
|
+
):
|
|
328
|
+
values = registry_validation[key]
|
|
329
|
+
if values:
|
|
330
|
+
surface_details.append(f"{label}: {', '.join(values)}")
|
|
331
|
+
return {
|
|
332
|
+
"server": "python-hwpx-automation",
|
|
333
|
+
"serverInfo": {
|
|
334
|
+
"name": "python-hwpx-automation",
|
|
335
|
+
"canonicalMcpConsole": "hwpx-automation-mcp",
|
|
336
|
+
"compatibilityMcpConsoles": ["hwpx-mcp-server"],
|
|
337
|
+
"hostConfigKeyRole": "host-local-alias",
|
|
338
|
+
},
|
|
339
|
+
"productIdentity": product_identity(),
|
|
340
|
+
"version": _package_version("python-hwpx-automation"),
|
|
341
|
+
"pythonHwpxVersion": _package_version("python-hwpx"),
|
|
342
|
+
"skillBundleVersion": os.environ.get("HWPX_SKILL_VERSION", "unknown"),
|
|
343
|
+
"pluginRoot": os.environ.get("HWPX_PLUGIN_ROOT"),
|
|
344
|
+
"transport": transport,
|
|
345
|
+
"streamable_http_available": callable(
|
|
346
|
+
getattr(RUNTIME_SERVICES.require_mcp(), "streamable_http_app", None)
|
|
347
|
+
),
|
|
348
|
+
"toolSurface": {
|
|
349
|
+
"status": "skewed" if skew_detected else "ok",
|
|
350
|
+
"profile": "advanced" if advanced else "default",
|
|
351
|
+
"contractHash": tool_contract_hash(),
|
|
352
|
+
"bindingHash": RUNTIME_SERVICES.tool_registry.binding_hash()
|
|
353
|
+
if RUNTIME_SERVICES.tool_registry
|
|
354
|
+
else None,
|
|
355
|
+
"bindingStatus": "bound" if RUNTIME_SERVICES.tool_registry else "unbound",
|
|
356
|
+
"expectedFastMcpToolCount": len(expected),
|
|
357
|
+
"actualFastMcpToolCount": len(fastmcp_tool_names),
|
|
358
|
+
"missingExpectedTools": missing_expected,
|
|
359
|
+
"unexpectedRegisteredTools": unexpected_registered,
|
|
360
|
+
"missingSkillRequiredTools": missing_required,
|
|
361
|
+
"callableMismatches": registry_validation["callableMismatches"],
|
|
362
|
+
"inputSchemaMismatches": registry_validation["inputSchemaMismatches"],
|
|
363
|
+
"outputSchemaMismatches": registry_validation["outputSchemaMismatches"],
|
|
364
|
+
"descriptionMismatches": registry_validation["descriptionMismatches"],
|
|
365
|
+
"unavailableTools": registry_validation["unavailable"],
|
|
366
|
+
"orderMismatch": registry_validation["orderMismatch"],
|
|
367
|
+
# Compatibility alias for older skill startup checks.
|
|
368
|
+
"missingKeyTools": missing_required,
|
|
369
|
+
"keyTools": sorted(active_required),
|
|
370
|
+
"diagnosis": (
|
|
371
|
+
"Installed MCP surface is missing expected tools; reinstall the hwpx plugin, "
|
|
372
|
+
"remove stale plugin venv/cache, then start a fresh host session."
|
|
373
|
+
if skew_detected
|
|
374
|
+
else "Installed MCP surface exactly matches the active ToolSpec contract."
|
|
375
|
+
),
|
|
376
|
+
},
|
|
377
|
+
"capability": _capability_block(skew_detected, surface_details),
|
|
378
|
+
"unitPolicy": {
|
|
379
|
+
"status": "audited",
|
|
380
|
+
"fontSize": "points",
|
|
381
|
+
"paragraphLineSpacing": "percent",
|
|
382
|
+
"paragraphIndent": "millimeters",
|
|
383
|
+
"paragraphSpacing": "points",
|
|
384
|
+
"pageSizeAndMargins": "millimeters",
|
|
385
|
+
"borderWidth": "human value: number/string accepted; prefer pt or mm suffix when supported",
|
|
386
|
+
"fileSizeLimits": "bytes",
|
|
387
|
+
"pageAndTableInternals": "HWP units are internal implementation details; MCP tools should prefer mm/pt/% labels.",
|
|
388
|
+
"verification": "public unit conversions are enforced by the automated test suite",
|
|
389
|
+
},
|
|
390
|
+
"fetch_timeout_seconds": env_float(
|
|
391
|
+
"FETCH_TIMEOUT_SECONDS", _DEFAULT_FETCH_TIMEOUT_SECONDS
|
|
392
|
+
),
|
|
393
|
+
"max_chars": default_max_chars(),
|
|
394
|
+
"workspace": workspace,
|
|
395
|
+
"sandbox": {
|
|
396
|
+
"root": workspace["relativePathRoot"],
|
|
397
|
+
"roots": workspace["roots"],
|
|
398
|
+
"absolute_paths_inside_root_allowed": bool(workspace["rootCount"]),
|
|
399
|
+
"path_guidance": (
|
|
400
|
+
"Use a relative path under the primary workspace or an absolute path inside an authorized root. "
|
|
401
|
+
f"Set {WORKSPACE_ROOTS_ENV} to a JSON array for deterministic multi-root launches."
|
|
402
|
+
),
|
|
403
|
+
},
|
|
404
|
+
"disconnect_diagnostics": {
|
|
405
|
+
"likely_conditions": [
|
|
406
|
+
"large document extraction exceeding client/tool timeout",
|
|
407
|
+
"idle stdio client session termination",
|
|
408
|
+
"remote URL fetch timeout",
|
|
409
|
+
],
|
|
410
|
+
"keepalive_check": "streamable_http_app constructibility is covered by smoke tests; stdio keepalive is client-controlled.",
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def repair_hwpx(
|
|
416
|
+
source_filename: str,
|
|
417
|
+
output_filename: str,
|
|
418
|
+
recover: bool = False,
|
|
419
|
+
overwrite: bool = False,
|
|
420
|
+
max_entry_size: int = 64 * 1024 * 1024,
|
|
421
|
+
max_total_size: int = 512 * 1024 * 1024,
|
|
422
|
+
max_source_size: int = 512 * 1024 * 1024,
|
|
423
|
+
) -> dict:
|
|
424
|
+
"""HWPX ZIP 패키지를 repair-repack하거나, recover=true일 때 Local File Header 스캔으로 복구합니다."""
|
|
425
|
+
return RUNTIME_SERVICES.ops.repair_hwpx(
|
|
426
|
+
source=resolve_path(source_filename),
|
|
427
|
+
output=resolve_path(output_filename),
|
|
428
|
+
recover=recover,
|
|
429
|
+
overwrite=overwrite,
|
|
430
|
+
max_entry_size=max_entry_size,
|
|
431
|
+
max_total_size=max_total_size,
|
|
432
|
+
max_source_size=max_source_size,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def render_submit(
|
|
437
|
+
filename: str,
|
|
438
|
+
idempotency_key: str,
|
|
439
|
+
workflow_id: str = None,
|
|
440
|
+
dpi: int = 144,
|
|
441
|
+
) -> dict:
|
|
442
|
+
"""실한컴 렌더 큐에 비동기로 제출하고 즉시 receipt를 반환합니다."""
|
|
443
|
+
|
|
444
|
+
source = Path(resolve_path(filename))
|
|
445
|
+
data = source.read_bytes()
|
|
446
|
+
digest = "sha256:" + hashlib.sha256(data).hexdigest()
|
|
447
|
+
stable = hashlib.sha256(idempotency_key.encode("utf-8")).hexdigest()[:24]
|
|
448
|
+
safe_workflow = workflow_id or f"direct-{stable}"
|
|
449
|
+
client = _render_client()
|
|
450
|
+
if not isinstance(client, NullRenderClientV2):
|
|
451
|
+
try:
|
|
452
|
+
existing = client.get(f"render-{stable}")
|
|
453
|
+
except RenderQueueError:
|
|
454
|
+
existing = None
|
|
455
|
+
if existing is not None:
|
|
456
|
+
if existing.input_content_hash != digest:
|
|
457
|
+
return {
|
|
458
|
+
"ok": False,
|
|
459
|
+
"errorCode": "IDEMPOTENCY_CONFLICT",
|
|
460
|
+
"jobId": existing.job_id,
|
|
461
|
+
}
|
|
462
|
+
return {"ok": True, "receipt": existing.model_dump(mode="json")}
|
|
463
|
+
job = RenderJobV2(
|
|
464
|
+
job_id=f"render-{stable}",
|
|
465
|
+
workflow_id=safe_workflow,
|
|
466
|
+
idempotency_key=idempotency_key,
|
|
467
|
+
source_content_hash=digest,
|
|
468
|
+
source_size_bytes=len(data),
|
|
469
|
+
submitted_at=datetime.now(timezone.utc),
|
|
470
|
+
dpi=dpi,
|
|
471
|
+
)
|
|
472
|
+
receipt = client.submit(job, source)
|
|
473
|
+
return {
|
|
474
|
+
"ok": receipt.status.value not in {"failed", "unavailable"},
|
|
475
|
+
"receipt": receipt.model_dump(mode="json"),
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def render_status(job_id: str, output_dir: str = None) -> dict:
|
|
480
|
+
"""렌더 job 상태를 한 번 조회합니다. 서버는 poll 동안 호출을 점유하지 않습니다."""
|
|
481
|
+
|
|
482
|
+
client = _render_client()
|
|
483
|
+
try:
|
|
484
|
+
receipt = client.get(job_id)
|
|
485
|
+
except (KeyError, RenderQueueError):
|
|
486
|
+
return {
|
|
487
|
+
"ok": False,
|
|
488
|
+
"jobId": job_id,
|
|
489
|
+
"status": "unverified",
|
|
490
|
+
"errorCode": "RENDER_JOB_NOT_FOUND_OR_UNAVAILABLE",
|
|
491
|
+
}
|
|
492
|
+
response = {"ok": True, "receipt": receipt.model_dump(mode="json")}
|
|
493
|
+
if output_dir and receipt.status.value == "succeeded":
|
|
494
|
+
destination = Path(resolve_path(output_dir))
|
|
495
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
496
|
+
saved = []
|
|
497
|
+
for artifact in receipt.artifacts:
|
|
498
|
+
name = (
|
|
499
|
+
"document.pdf"
|
|
500
|
+
if artifact.kind.value == "pdf"
|
|
501
|
+
else f"page-{artifact.page_number:04d}.png"
|
|
502
|
+
)
|
|
503
|
+
data = client.fetch_artifact(job_id, artifact.content_hash)
|
|
504
|
+
target = destination / name
|
|
505
|
+
target.write_bytes(data)
|
|
506
|
+
saved.append(
|
|
507
|
+
{
|
|
508
|
+
"path": str(target),
|
|
509
|
+
"contentHash": artifact.content_hash,
|
|
510
|
+
"kind": artifact.kind.value,
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
response["savedArtifacts"] = saved
|
|
514
|
+
return response
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def render_cancel(job_id: str) -> dict:
|
|
518
|
+
"""대기 job을 취소하거나 실행 worker에 취소 요청을 기록합니다."""
|
|
519
|
+
|
|
520
|
+
client = _render_client()
|
|
521
|
+
try:
|
|
522
|
+
receipt = client.cancel(job_id)
|
|
523
|
+
except (KeyError, RenderQueueError):
|
|
524
|
+
return {
|
|
525
|
+
"ok": False,
|
|
526
|
+
"jobId": job_id,
|
|
527
|
+
"status": "unverified",
|
|
528
|
+
"errorCode": "RENDER_JOB_NOT_FOUND_OR_UNAVAILABLE",
|
|
529
|
+
}
|
|
530
|
+
return {"ok": True, "receipt": receipt.model_dump(mode="json")}
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def render_health() -> dict:
|
|
534
|
+
"""큐/worker/Hancom 상태와 적체를 반환하며 미구성·stale heartbeat는 degraded입니다."""
|
|
535
|
+
|
|
536
|
+
return _render_client().capabilities()
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def validate_structure(filename: str) -> dict:
|
|
540
|
+
"""[고급] HWPX 구조 유효성을 검사합니다."""
|
|
541
|
+
return RUNTIME_SERVICES.ops.validate_structure(resolve_path(filename))
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def lint_text_conventions(filename: str) -> dict:
|
|
545
|
+
"""[고급] 텍스트 규칙 위반 여부를 검사합니다."""
|
|
546
|
+
return RUNTIME_SERVICES.ops.lint_text_conventions(resolve_path(filename))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
__all__ = [
|
|
550
|
+
"render_submit",
|
|
551
|
+
"render_status",
|
|
552
|
+
"render_cancel",
|
|
553
|
+
"render_health",
|
|
554
|
+
"render_preview",
|
|
555
|
+
"repair_hwpx",
|
|
556
|
+
"mcp_server_health",
|
|
557
|
+
"describe_capabilities",
|
|
558
|
+
"doc_diff",
|
|
559
|
+
"validate_structure",
|
|
560
|
+
"lint_text_conventions",
|
|
561
|
+
"inspect_document_quality",
|
|
562
|
+
"inspect_document_authoring_quality",
|
|
563
|
+
"inspect_operating_plan_quality",
|
|
564
|
+
"inspect_official_document_style",
|
|
565
|
+
"inspect_reference_consistency",
|
|
566
|
+
]
|