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.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,24 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Helpers for generating stable handles for logical nodes."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ from typing import Iterable, Tuple
8
+
9
+
10
+ def _normalize_parts(parts: Iterable[str | int]) -> Tuple[str, ...]:
11
+ normalized: list[str] = []
12
+ for part in parts:
13
+ text = str(part)
14
+ normalized.append(text)
15
+ return tuple(normalized)
16
+
17
+
18
+ def stable_node_id(parts: Iterable[str | int]) -> str:
19
+ """Return a deterministic identifier for the provided structural path."""
20
+
21
+ normalized = _normalize_parts(parts)
22
+ joined = "::".join(normalized)
23
+ digest = hashlib.sha256(joined.encode("utf-8")).hexdigest()[:16]
24
+ return f"n_{digest}"
@@ -0,0 +1,205 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Iterator
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ResolvedParagraph:
10
+ paragraph: Any
11
+ location: dict[str, Any]
12
+
13
+
14
+ def body_paragraph_location(paragraph_index: int) -> dict[str, Any]:
15
+ return {"kind": "body_paragraph", "paragraph_index": paragraph_index}
16
+
17
+
18
+ def table_cell_paragraph_location(
19
+ table_index: int,
20
+ row: int,
21
+ col: int,
22
+ cell_paragraph_index: int,
23
+ ) -> dict[str, Any]:
24
+ return {
25
+ "kind": "table_cell_paragraph",
26
+ "table_index": table_index,
27
+ "row": row,
28
+ "col": col,
29
+ "cell_paragraph_index": cell_paragraph_index,
30
+ }
31
+
32
+
33
+ def location_anchor(location: dict[str, Any], position: int | None = None) -> dict[str, Any]:
34
+ anchor_id = _anchor_id(location, position)
35
+ payload: dict[str, Any] = {"anchor_id": anchor_id, "location": dict(location)}
36
+ if position is not None:
37
+ payload["position"] = position
38
+ return payload
39
+
40
+
41
+ def location_from_anchor(anchor: dict[str, Any] | str) -> dict[str, Any]:
42
+ if isinstance(anchor, dict):
43
+ if isinstance(anchor.get("location"), dict):
44
+ return dict(anchor["location"])
45
+ if "kind" in anchor:
46
+ return dict(anchor)
47
+ if not isinstance(anchor, str):
48
+ raise ValueError("anchor must be an object with location or an anchor_id string")
49
+
50
+ head = anchor.split("@", 1)[0]
51
+ parts = head.split(":")
52
+ if parts[0] == "body" and len(parts) == 2:
53
+ return body_paragraph_location(_int(parts[1], "paragraph_index"))
54
+ if parts[0] == "table" and len(parts) == 5:
55
+ return table_cell_paragraph_location(
56
+ _int(parts[1], "table_index"),
57
+ _int(parts[2], "row"),
58
+ _int(parts[3], "col"),
59
+ _int(parts[4], "cell_paragraph_index"),
60
+ )
61
+ raise ValueError(f"unsupported anchor_id: {anchor}")
62
+
63
+
64
+ def iter_indexed_tables(doc: Any) -> Iterator[tuple[int, Any]]:
65
+ table_index = 0
66
+ for paragraph in getattr(doc, "paragraphs", []):
67
+ for table in _iter_tables_from_paragraph(paragraph):
68
+ yield table_index, table
69
+ table_index += 1
70
+
71
+
72
+ def iter_paragraph_locations(doc: Any) -> Iterator[ResolvedParagraph]:
73
+ for paragraph_index, paragraph in enumerate(getattr(doc, "paragraphs", [])):
74
+ yield ResolvedParagraph(paragraph, body_paragraph_location(paragraph_index))
75
+
76
+ for table_index, table in iter_indexed_tables(doc):
77
+ for row, col, cell in _iter_anchor_cells(table):
78
+ for cell_paragraph_index, paragraph in enumerate(getattr(cell, "paragraphs", []) or []):
79
+ yield ResolvedParagraph(
80
+ paragraph,
81
+ table_cell_paragraph_location(table_index, row, col, cell_paragraph_index),
82
+ )
83
+
84
+
85
+ def resolve_paragraph_reference(
86
+ doc: Any,
87
+ *,
88
+ paragraph_index: int | None = None,
89
+ location: dict[str, Any] | None = None,
90
+ create: bool = False,
91
+ ) -> ResolvedParagraph:
92
+ if location is None:
93
+ if paragraph_index is None:
94
+ raise ValueError("paragraph_index 또는 location 중 하나를 제공해야 합니다.")
95
+ return _resolve_body_paragraph(doc, paragraph_index)
96
+
97
+ kind = str(location.get("kind") or "").strip()
98
+ if kind in {"body", "paragraph", "body_paragraph"}:
99
+ index_value = location.get("paragraph_index", paragraph_index)
100
+ if index_value is None:
101
+ raise ValueError("body_paragraph location에는 paragraph_index가 필요합니다.")
102
+ return _resolve_body_paragraph(doc, int(index_value))
103
+
104
+ if kind in {"table_cell_paragraph", "cell_paragraph"}:
105
+ return _resolve_table_cell_paragraph(doc, location, create=create)
106
+
107
+ raise ValueError(f"지원하지 않는 location kind: {kind or '<missing>'}")
108
+
109
+
110
+ def _resolve_body_paragraph(doc: Any, paragraph_index: int) -> ResolvedParagraph:
111
+ paragraphs = list(getattr(doc, "paragraphs", []))
112
+ if paragraph_index < 0 or paragraph_index >= len(paragraphs):
113
+ raise ValueError(f"유효하지 않은 paragraph_index: {paragraph_index}")
114
+ return ResolvedParagraph(paragraphs[paragraph_index], body_paragraph_location(paragraph_index))
115
+
116
+
117
+ def _resolve_table_cell_paragraph(
118
+ doc: Any,
119
+ location: dict[str, Any],
120
+ *,
121
+ create: bool,
122
+ ) -> ResolvedParagraph:
123
+ table_index = _int(location.get("table_index"), "table_index")
124
+ row = _int(location.get("row"), "row")
125
+ col = _int(location.get("col"), "col")
126
+ cell_paragraph_index = _int(
127
+ location.get("cell_paragraph_index", location.get("p", 0)),
128
+ "cell_paragraph_index",
129
+ )
130
+
131
+ tables = [table for _, table in iter_indexed_tables(doc)]
132
+ if table_index < 0 or table_index >= len(tables):
133
+ raise ValueError(f"유효하지 않은 table_index: {table_index}")
134
+
135
+ try:
136
+ cell = tables[table_index].cell(row, col)
137
+ except (IndexError, ValueError) as exc:
138
+ raise ValueError(f"유효하지 않은 table cell 좌표: table={table_index}, row={row}, col={col}") from exc
139
+
140
+ paragraphs = list(getattr(cell, "paragraphs", []) or [])
141
+ if create and not paragraphs and hasattr(cell, "add_paragraph"):
142
+ paragraphs = [cell.add_paragraph("")]
143
+
144
+ if cell_paragraph_index < 0 or cell_paragraph_index >= len(paragraphs):
145
+ raise ValueError(
146
+ "유효하지 않은 cell_paragraph_index: "
147
+ f"{cell_paragraph_index} (table={table_index}, row={row}, col={col})"
148
+ )
149
+
150
+ normalized = table_cell_paragraph_location(table_index, row, col, cell_paragraph_index)
151
+ return ResolvedParagraph(paragraphs[cell_paragraph_index], normalized)
152
+
153
+
154
+ def _iter_tables_from_paragraph(paragraph: Any) -> Iterator[Any]:
155
+ for table in getattr(paragraph, "tables", []) or []:
156
+ yield table
157
+ yield from _iter_nested_tables(table)
158
+
159
+
160
+ def _iter_nested_tables(table: Any) -> Iterator[Any]:
161
+ for row in getattr(table, "rows", []) or []:
162
+ for cell in getattr(row, "cells", []) or []:
163
+ for paragraph in getattr(cell, "paragraphs", []) or []:
164
+ yield from _iter_tables_from_paragraph(paragraph)
165
+
166
+
167
+ def _iter_anchor_cells(table: Any) -> Iterator[tuple[int, int, Any]]:
168
+ if hasattr(table, "iter_grid"):
169
+ seen: set[int] = set()
170
+ for position in table.iter_grid():
171
+ if not getattr(position, "is_anchor", False):
172
+ continue
173
+ cell = position.cell
174
+ marker = id(getattr(cell, "element", cell))
175
+ if marker in seen:
176
+ continue
177
+ seen.add(marker)
178
+ row, col = getattr(position, "anchor", (position.row, position.column))
179
+ yield int(row), int(col), cell
180
+ return
181
+
182
+ for row_index, row in enumerate(getattr(table, "rows", []) or []):
183
+ for col_index, cell in enumerate(getattr(row, "cells", []) or []):
184
+ yield row_index, col_index, cell
185
+
186
+
187
+ def _int(value: Any, name: str) -> int:
188
+ try:
189
+ return int(value)
190
+ except (TypeError, ValueError) as exc:
191
+ raise ValueError(f"{name} must be an integer") from exc
192
+
193
+
194
+ def _anchor_id(location: dict[str, Any], position: int | None) -> str:
195
+ suffix = "" if position is None else f"@{position}"
196
+ kind = location.get("kind")
197
+ if kind == "body_paragraph":
198
+ return f"body:{location['paragraph_index']}{suffix}"
199
+ if kind == "table_cell_paragraph":
200
+ return (
201
+ "table:"
202
+ f"{location['table_index']}:{location['row']}:{location['col']}:"
203
+ f"{location['cell_paragraph_index']}{suffix}"
204
+ )
205
+ return f"{kind or 'unknown'}{suffix}"
@@ -0,0 +1,162 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Document locator models used across tool schemas."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Annotated, Dict, Optional
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
9
+ from typing_extensions import Literal
10
+
11
+
12
+ _LOCATOR_KEYS = {"type", "path", "uri", "backend", "handleId"}
13
+
14
+
15
+ class _LocatorModel(BaseModel):
16
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
17
+
18
+
19
+ class PathLocator(_LocatorModel):
20
+ type: Literal["path"] = Field("path", alias="type")
21
+ path: str
22
+ backend: Optional[str] = None
23
+
24
+ @model_validator(mode="before")
25
+ @classmethod
26
+ def _default_type(cls, data: object) -> object:
27
+ if isinstance(data, dict) and "type" not in data and "path" in data:
28
+ enriched = dict(data)
29
+ enriched.setdefault("type", "path")
30
+ return enriched
31
+ return data
32
+
33
+
34
+ class UriLocator(_LocatorModel):
35
+ type: Literal["uri"] = Field("uri", alias="type")
36
+ uri: str
37
+ backend: Optional[str] = None
38
+
39
+
40
+ class HandleLocator(_LocatorModel):
41
+ type: Literal["handle"] = Field("handle", alias="type")
42
+ handle_id: str = Field(alias="handleId")
43
+ backend: Optional[str] = None
44
+
45
+
46
+ class RegisteredHandle(_LocatorModel):
47
+ """서버가 관리하는 등록 핸들의 읽기 전용 직렬화 모델."""
48
+
49
+ type: Literal["handle"] = Field("handle", alias="type")
50
+ handle_id: str = Field(alias="handleId")
51
+ path: str
52
+ backend: Optional[str] = None
53
+
54
+
55
+ DocumentLocator = Annotated[
56
+ PathLocator | UriLocator | HandleLocator,
57
+ Field(discriminator="type"),
58
+ ]
59
+
60
+
61
+ def normalize_locator_payload(data: Dict[str, object], *, field_name: str = "document") -> Dict[str, object]:
62
+ """Return *data* with locator keys grouped under *field_name*.
63
+
64
+ The function accepts legacy payloads that specify ``path`` at the top level
65
+ while also supporting fully qualified discriminated union inputs.
66
+ """
67
+
68
+ if field_name in data:
69
+ return data
70
+
71
+ locator: Dict[str, object] = {}
72
+ remainder: Dict[str, object] = {}
73
+ for key, value in data.items():
74
+ if key in _LOCATOR_KEYS:
75
+ locator[key] = value
76
+ else:
77
+ remainder[key] = value
78
+
79
+ if not locator:
80
+ raise ValueError("document locator must include path, uri, or handleId")
81
+
82
+ if "type" not in locator:
83
+ if "path" in locator:
84
+ locator["type"] = "path"
85
+ elif "uri" in locator:
86
+ locator["type"] = "uri"
87
+ elif "handleId" in locator:
88
+ locator["type"] = "handle"
89
+ else: # pragma: no cover - defensive guard
90
+ raise ValueError("document locator requires a discriminator")
91
+
92
+ remainder[field_name] = locator
93
+ return remainder
94
+
95
+
96
+ def locator_identifier(locator: DocumentLocator) -> str:
97
+ """Return a stable identifier for *locator*."""
98
+
99
+ if isinstance(locator, PathLocator):
100
+ return locator.path
101
+ if isinstance(locator, UriLocator):
102
+ return locator.uri
103
+ if isinstance(locator, HandleLocator):
104
+ return locator.handle_id
105
+ raise TypeError(f"Unsupported locator type: {type(locator)!r}")
106
+
107
+
108
+ def locator_path(locator: DocumentLocator) -> Optional[str]:
109
+ """Return the preferred path-like value for *locator* if available."""
110
+
111
+ if isinstance(locator, PathLocator):
112
+ return locator.path
113
+ if isinstance(locator, UriLocator):
114
+ return locator.uri
115
+ return None
116
+
117
+
118
+ def locator_backend(locator: DocumentLocator) -> Optional[str]:
119
+ """Return the backend hint provided by *locator*, if any."""
120
+
121
+ return getattr(locator, "backend", None)
122
+
123
+
124
+ def document_locator_schema() -> Dict[str, object]:
125
+ """Return a sanitized JSON schema fragment for document locators."""
126
+
127
+ return {
128
+ "type": "object",
129
+ "description": (
130
+ "Discriminated locator supporting legacy paths, HTTP URIs, or opaque handles. "
131
+ "Top-level path/uri/handleId shorthands remain supported for backwards compatibility."
132
+ ),
133
+ "properties": {
134
+ "type": {
135
+ "type": "string",
136
+ "enum": ["path", "uri", "handle"],
137
+ "description": "Locator variant; defaults to 'path' when omitted.",
138
+ },
139
+ "path": {
140
+ "type": "string",
141
+ "minLength": 1,
142
+ "description": "Filesystem-relative path resolved by the configured storage backend.",
143
+ },
144
+ "uri": {
145
+ "type": "string",
146
+ "minLength": 1,
147
+ "description": "HTTP or backend-specific URI when using remote storage.",
148
+ },
149
+ "handleId": {
150
+ "type": "string",
151
+ "minLength": 1,
152
+ "description": "Opaque identifier for a previously registered document handle.",
153
+ },
154
+ "backend": {
155
+ "type": "string",
156
+ "minLength": 1,
157
+ "description": "Optional hint describing the storage backend to use (e.g. 'http').",
158
+ },
159
+ },
160
+ "required": ["type"],
161
+ "additionalProperties": False,
162
+ }