document-adapter 0.1.0__tar.gz
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.
- document_adapter-0.1.0/LICENSE +21 -0
- document_adapter-0.1.0/PKG-INFO +252 -0
- document_adapter-0.1.0/README.md +223 -0
- document_adapter-0.1.0/document_adapter/__init__.py +45 -0
- document_adapter-0.1.0/document_adapter/base.py +108 -0
- document_adapter-0.1.0/document_adapter/docx_adapter.py +75 -0
- document_adapter-0.1.0/document_adapter/hwpx_adapter.py +126 -0
- document_adapter-0.1.0/document_adapter/mcp_server.py +64 -0
- document_adapter-0.1.0/document_adapter/pptx_adapter.py +109 -0
- document_adapter-0.1.0/document_adapter/tools.py +232 -0
- document_adapter-0.1.0/document_adapter.egg-info/PKG-INFO +252 -0
- document_adapter-0.1.0/document_adapter.egg-info/SOURCES.txt +17 -0
- document_adapter-0.1.0/document_adapter.egg-info/dependency_links.txt +1 -0
- document_adapter-0.1.0/document_adapter.egg-info/entry_points.txt +2 -0
- document_adapter-0.1.0/document_adapter.egg-info/requires.txt +11 -0
- document_adapter-0.1.0/document_adapter.egg-info/top_level.txt +1 -0
- document_adapter-0.1.0/pyproject.toml +46 -0
- document_adapter-0.1.0/setup.cfg +4 -0
- document_adapter-0.1.0/tests/test_smoke.py +104 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Plateer Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: document-adapter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM-friendly document template editing (DOCX/PPTX/HWPX) with MCP server and Claude API tool-use support
|
|
5
|
+
Author-email: Son Seongjun <sonsj97@plateer.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PlateerLab/document-adapter
|
|
8
|
+
Project-URL: Repository, https://github.com/PlateerLab/document-adapter
|
|
9
|
+
Keywords: mcp,llm,document,docx,pptx,hwpx,template,claude
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: python-docx>=1.1
|
|
20
|
+
Requires-Dist: docxtpl>=0.20
|
|
21
|
+
Requires-Dist: python-pptx>=1.0
|
|
22
|
+
Requires-Dist: python-hwpx>=2.9
|
|
23
|
+
Requires-Dist: mcp>=1.0
|
|
24
|
+
Provides-Extra: claude
|
|
25
|
+
Requires-Dist: anthropic>=0.40; extra == "claude"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# document-adapter
|
|
31
|
+
|
|
32
|
+
**LLM이 DOCX / PPTX / HWPX 문서를 직접 편집할 수 있게 해주는 통합 어댑터 + MCP 서버.**
|
|
33
|
+
|
|
34
|
+
세 가지 오피스 포맷을 하나의 파이썬 인터페이스로 추상화하고, Claude Desktop / Claude Code / Anthropic API Tool Use에서 바로 호출할 수 있는 MCP 도구로 노출합니다. 양식 문서의 빈 셀을 자동으로 채우거나, 템플릿의 `{{key}}`를 치환하거나, 기존 표의 내용을 수정하는 작업을 LLM 에이전트가 수행할 수 있습니다.
|
|
35
|
+
|
|
36
|
+
## 지원 포맷
|
|
37
|
+
|
|
38
|
+
| 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 셀 수정 | 행 추가 |
|
|
39
|
+
|---|---|---|---|---|---|
|
|
40
|
+
| `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ |
|
|
41
|
+
| `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | ❌ (미지원) |
|
|
42
|
+
| `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ❌ (미지원) |
|
|
43
|
+
|
|
44
|
+
- HWPX는 한컴오피스 설치가 **불필요**합니다 (macOS/Linux 서버에서 그대로 동작).
|
|
45
|
+
- 구버전 `.hwp`(바이너리 포맷)는 지원하지 않습니다 — `.hwpx`로 변환 후 사용하세요.
|
|
46
|
+
|
|
47
|
+
## 설치
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
|
|
52
|
+
# Claude API 예시 스크립트까지 쓰려면
|
|
53
|
+
pip install -e ".[claude]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Python 3.10+ 필요.
|
|
57
|
+
|
|
58
|
+
## 빠른 시작 — 파이썬 API
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from document_adapter import load
|
|
62
|
+
|
|
63
|
+
doc = load("report_template.docx")
|
|
64
|
+
|
|
65
|
+
# 1. 구조 파악
|
|
66
|
+
schema = doc.get_schema()
|
|
67
|
+
print(schema.placeholders) # ['author', 'date', 'title']
|
|
68
|
+
print(schema.tables) # [TableSchema(index=0, rows=7, cols=2, ...), ...]
|
|
69
|
+
|
|
70
|
+
# 2. 템플릿 렌더
|
|
71
|
+
doc.render_template({
|
|
72
|
+
"title": "Q1 운영 리포트",
|
|
73
|
+
"author": "손성준",
|
|
74
|
+
"date": "2026-04-15",
|
|
75
|
+
})
|
|
76
|
+
doc.save("report_filled.docx")
|
|
77
|
+
|
|
78
|
+
# 3. 기존 양식 파일의 표 셀 수정
|
|
79
|
+
doc = load("checklist.docx")
|
|
80
|
+
old = doc.set_cell(table_index=1, row=1, col=1, value="○○전자")
|
|
81
|
+
doc.append_row(1, ["새 항목", "값"]) # DOCX만 지원
|
|
82
|
+
doc.save("checklist_filled.docx")
|
|
83
|
+
doc.close()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
확장자로 자동 분기되므로 `.pptx` / `.hwpx`도 동일한 API를 사용합니다.
|
|
87
|
+
|
|
88
|
+
## MCP 서버로 사용 — Claude Desktop / Claude Code
|
|
89
|
+
|
|
90
|
+
### 실행
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -m document_adapter.mcp_server
|
|
94
|
+
# 또는 설치 후
|
|
95
|
+
document-adapter-mcp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Claude Desktop 설정
|
|
99
|
+
|
|
100
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"document-adapter": {
|
|
106
|
+
"command": "/absolute/path/to/venv/bin/python",
|
|
107
|
+
"args": ["-m", "document_adapter.mcp_server"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
재시작하면 Claude Desktop에서 아래 4개 도구를 사용할 수 있습니다.
|
|
114
|
+
|
|
115
|
+
### Claude Code 설정
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
claude mcp add document-adapter \
|
|
119
|
+
/absolute/path/to/venv/bin/python -m document_adapter.mcp_server
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Anthropic API Tool Use로 사용
|
|
123
|
+
|
|
124
|
+
`document_adapter.tools`가 Claude API의 tool schema 형식과 그대로 호환됩니다.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import anthropic
|
|
128
|
+
from document_adapter.tools import TOOL_DEFINITIONS, call_tool
|
|
129
|
+
|
|
130
|
+
client = anthropic.Anthropic()
|
|
131
|
+
|
|
132
|
+
resp = client.messages.create(
|
|
133
|
+
model="claude-opus-4-6",
|
|
134
|
+
max_tokens=4096,
|
|
135
|
+
tools=[{
|
|
136
|
+
"name": t["name"],
|
|
137
|
+
"description": t["description"],
|
|
138
|
+
"input_schema": t["input_schema"],
|
|
139
|
+
} for t in TOOL_DEFINITIONS],
|
|
140
|
+
messages=[{
|
|
141
|
+
"role": "user",
|
|
142
|
+
"content": "report_template.docx의 표 구조를 확인하고 빈 셀을 적절히 채워줘",
|
|
143
|
+
}],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# tool_use 블록을 받으면 call_tool(name, args)로 실행 후 결과 반환
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
전체 agent loop 예시는 [`examples/claude_api_example.py`](examples/claude_api_example.py) 참고.
|
|
150
|
+
|
|
151
|
+
## 노출되는 4개 도구
|
|
152
|
+
|
|
153
|
+
| 도구 | 설명 |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `inspect_document` | 문서 구조(placeholders, tables)를 JSON으로 반환. **항상 첫 호출로 사용** |
|
|
156
|
+
| `render_template` | `{{key}}`를 context dict 값으로 치환해 새 파일 저장 |
|
|
157
|
+
| `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 |
|
|
158
|
+
| `append_row` | 표 끝에 새 행 추가 (DOCX 전용) |
|
|
159
|
+
|
|
160
|
+
### `inspect_document` 반환 예시
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"format": "docx",
|
|
165
|
+
"source": "/path/to/checklist.docx",
|
|
166
|
+
"placeholders": [],
|
|
167
|
+
"tables": [
|
|
168
|
+
{
|
|
169
|
+
"index": 1,
|
|
170
|
+
"rows": 7,
|
|
171
|
+
"cols": 2,
|
|
172
|
+
"location": null,
|
|
173
|
+
"preview": [
|
|
174
|
+
{"row": 0, "cells": ["항목", "기입 내용"]},
|
|
175
|
+
{"row": 1, "cells": ["고객사 / 조직", ""]},
|
|
176
|
+
{"row": 2, "cells": ["현업 담당부서 / 책임자", ""]}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell`을 호출합니다.
|
|
184
|
+
|
|
185
|
+
## 템플릿 작성 규칙
|
|
186
|
+
|
|
187
|
+
### DOCX — Jinja2 전체 문법 사용 가능
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
{{ report_title }}
|
|
191
|
+
작성자: {{ author }}
|
|
192
|
+
|
|
193
|
+
{% for item in items %}- {{ item.name }}: {{ item.value }}
|
|
194
|
+
{% endfor %}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**표 행 반복은 `{%tr for ... %}` / `{%tr endfor %}`를 각각 별도 행에 두어야 합니다.**
|
|
198
|
+
같은 행에 두 태그를 넣으면 `<w:tr>` 전체가 `{% for %}`로 교체되어 `endfor`가 손실됩니다.
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
┌─────────────────────┬─────┬─────┐
|
|
202
|
+
│ 항목 │ 목표 │ 실적 │ <- 헤더
|
|
203
|
+
├─────────────────────┼─────┼─────┤
|
|
204
|
+
│ {%tr for r in rows %} │ <- for 행
|
|
205
|
+
├─────────────────────┼─────┼─────┤
|
|
206
|
+
│ {{ r.name }} │ {{ r.target }} │ {{ r.actual }} │ <- 반복 본문
|
|
207
|
+
├─────────────────────┼─────┼─────┤
|
|
208
|
+
│ {%tr endfor %} │ <- endfor 행
|
|
209
|
+
└─────────────────────┴─────┴─────┘
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### PPTX / HWPX — 단순 `{{key}}` 치환만
|
|
213
|
+
|
|
214
|
+
loop / if / filter는 지원하지 않습니다. PPTX는 placeholder가 여러 `run`으로 쪼개질 수 있어, 어댑터가 paragraph 전체 텍스트를 재조립한 뒤 첫 `run`에 다시 담는 방식으로 처리합니다 (서식 일부 손실 가능).
|
|
215
|
+
|
|
216
|
+
## 내장된 버그 회피
|
|
217
|
+
|
|
218
|
+
| 포맷 | 문제 | 어댑터의 처리 |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| HWPX | `python-hwpx 2.9.0`의 `set_cell_text()`가 빈 셀에서 lxml/ElementTree 혼용 `TypeError` 발생 | `paragraphs[0].text = value` 직접 할당으로 우회 |
|
|
221
|
+
| HWPX | `replace_text_in_runs()`가 한글 공백이 run으로 쪼개진 경우 매칭 실패 | 위치 기반 API만 사용 |
|
|
222
|
+
| HWPX | `manifest fallback` 경고 로그가 과도하게 출력됨 | `logging.getLogger("hwpx")` 레벨을 `ERROR`로 조정 |
|
|
223
|
+
| PPTX | placeholder가 여러 `run`으로 쪼개져 단순 `run.text` 치환이 실패 | paragraph 전체 재조립 |
|
|
224
|
+
| DOCX | `docxtpl`의 `{%tr%}`를 같은 행에 두면 파싱 에러 | README에 배치 규칙 명시 |
|
|
225
|
+
|
|
226
|
+
## 프로젝트 구조
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
document_adapter/
|
|
230
|
+
├── __init__.py # load() dispatcher
|
|
231
|
+
├── base.py # DocumentAdapter ABC, TableSchema, DocumentSchema
|
|
232
|
+
├── docx_adapter.py # DocxAdapter
|
|
233
|
+
├── pptx_adapter.py # PptxAdapter
|
|
234
|
+
├── hwpx_adapter.py # HwpxAdapter (버그 회피 포함)
|
|
235
|
+
├── tools.py # Tool 정의 + call_tool dispatcher
|
|
236
|
+
└── mcp_server.py # MCP stdio server
|
|
237
|
+
|
|
238
|
+
examples/
|
|
239
|
+
└── claude_api_example.py
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 라이선스
|
|
243
|
+
|
|
244
|
+
MIT
|
|
245
|
+
|
|
246
|
+
## Credits
|
|
247
|
+
|
|
248
|
+
- [`python-docx`](https://github.com/python-openxml/python-docx)
|
|
249
|
+
- [`docxtpl`](https://github.com/elapouya/python-docx-template)
|
|
250
|
+
- [`python-pptx`](https://github.com/scanny/python-pptx)
|
|
251
|
+
- [`python-hwpx`](https://github.com/airmang/python-hwpx)
|
|
252
|
+
- [`mcp`](https://github.com/modelcontextprotocol/python-sdk)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# document-adapter
|
|
2
|
+
|
|
3
|
+
**LLM이 DOCX / PPTX / HWPX 문서를 직접 편집할 수 있게 해주는 통합 어댑터 + MCP 서버.**
|
|
4
|
+
|
|
5
|
+
세 가지 오피스 포맷을 하나의 파이썬 인터페이스로 추상화하고, Claude Desktop / Claude Code / Anthropic API Tool Use에서 바로 호출할 수 있는 MCP 도구로 노출합니다. 양식 문서의 빈 셀을 자동으로 채우거나, 템플릿의 `{{key}}`를 치환하거나, 기존 표의 내용을 수정하는 작업을 LLM 에이전트가 수행할 수 있습니다.
|
|
6
|
+
|
|
7
|
+
## 지원 포맷
|
|
8
|
+
|
|
9
|
+
| 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 셀 수정 | 행 추가 |
|
|
10
|
+
|---|---|---|---|---|---|
|
|
11
|
+
| `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ |
|
|
12
|
+
| `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | ❌ (미지원) |
|
|
13
|
+
| `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ❌ (미지원) |
|
|
14
|
+
|
|
15
|
+
- HWPX는 한컴오피스 설치가 **불필요**합니다 (macOS/Linux 서버에서 그대로 동작).
|
|
16
|
+
- 구버전 `.hwp`(바이너리 포맷)는 지원하지 않습니다 — `.hwpx`로 변환 후 사용하세요.
|
|
17
|
+
|
|
18
|
+
## 설치
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e .
|
|
22
|
+
|
|
23
|
+
# Claude API 예시 스크립트까지 쓰려면
|
|
24
|
+
pip install -e ".[claude]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Python 3.10+ 필요.
|
|
28
|
+
|
|
29
|
+
## 빠른 시작 — 파이썬 API
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from document_adapter import load
|
|
33
|
+
|
|
34
|
+
doc = load("report_template.docx")
|
|
35
|
+
|
|
36
|
+
# 1. 구조 파악
|
|
37
|
+
schema = doc.get_schema()
|
|
38
|
+
print(schema.placeholders) # ['author', 'date', 'title']
|
|
39
|
+
print(schema.tables) # [TableSchema(index=0, rows=7, cols=2, ...), ...]
|
|
40
|
+
|
|
41
|
+
# 2. 템플릿 렌더
|
|
42
|
+
doc.render_template({
|
|
43
|
+
"title": "Q1 운영 리포트",
|
|
44
|
+
"author": "손성준",
|
|
45
|
+
"date": "2026-04-15",
|
|
46
|
+
})
|
|
47
|
+
doc.save("report_filled.docx")
|
|
48
|
+
|
|
49
|
+
# 3. 기존 양식 파일의 표 셀 수정
|
|
50
|
+
doc = load("checklist.docx")
|
|
51
|
+
old = doc.set_cell(table_index=1, row=1, col=1, value="○○전자")
|
|
52
|
+
doc.append_row(1, ["새 항목", "값"]) # DOCX만 지원
|
|
53
|
+
doc.save("checklist_filled.docx")
|
|
54
|
+
doc.close()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
확장자로 자동 분기되므로 `.pptx` / `.hwpx`도 동일한 API를 사용합니다.
|
|
58
|
+
|
|
59
|
+
## MCP 서버로 사용 — Claude Desktop / Claude Code
|
|
60
|
+
|
|
61
|
+
### 실행
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m document_adapter.mcp_server
|
|
65
|
+
# 또는 설치 후
|
|
66
|
+
document-adapter-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Claude Desktop 설정
|
|
70
|
+
|
|
71
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"document-adapter": {
|
|
77
|
+
"command": "/absolute/path/to/venv/bin/python",
|
|
78
|
+
"args": ["-m", "document_adapter.mcp_server"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
재시작하면 Claude Desktop에서 아래 4개 도구를 사용할 수 있습니다.
|
|
85
|
+
|
|
86
|
+
### Claude Code 설정
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
claude mcp add document-adapter \
|
|
90
|
+
/absolute/path/to/venv/bin/python -m document_adapter.mcp_server
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Anthropic API Tool Use로 사용
|
|
94
|
+
|
|
95
|
+
`document_adapter.tools`가 Claude API의 tool schema 형식과 그대로 호환됩니다.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import anthropic
|
|
99
|
+
from document_adapter.tools import TOOL_DEFINITIONS, call_tool
|
|
100
|
+
|
|
101
|
+
client = anthropic.Anthropic()
|
|
102
|
+
|
|
103
|
+
resp = client.messages.create(
|
|
104
|
+
model="claude-opus-4-6",
|
|
105
|
+
max_tokens=4096,
|
|
106
|
+
tools=[{
|
|
107
|
+
"name": t["name"],
|
|
108
|
+
"description": t["description"],
|
|
109
|
+
"input_schema": t["input_schema"],
|
|
110
|
+
} for t in TOOL_DEFINITIONS],
|
|
111
|
+
messages=[{
|
|
112
|
+
"role": "user",
|
|
113
|
+
"content": "report_template.docx의 표 구조를 확인하고 빈 셀을 적절히 채워줘",
|
|
114
|
+
}],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# tool_use 블록을 받으면 call_tool(name, args)로 실행 후 결과 반환
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
전체 agent loop 예시는 [`examples/claude_api_example.py`](examples/claude_api_example.py) 참고.
|
|
121
|
+
|
|
122
|
+
## 노출되는 4개 도구
|
|
123
|
+
|
|
124
|
+
| 도구 | 설명 |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `inspect_document` | 문서 구조(placeholders, tables)를 JSON으로 반환. **항상 첫 호출로 사용** |
|
|
127
|
+
| `render_template` | `{{key}}`를 context dict 값으로 치환해 새 파일 저장 |
|
|
128
|
+
| `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 |
|
|
129
|
+
| `append_row` | 표 끝에 새 행 추가 (DOCX 전용) |
|
|
130
|
+
|
|
131
|
+
### `inspect_document` 반환 예시
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"format": "docx",
|
|
136
|
+
"source": "/path/to/checklist.docx",
|
|
137
|
+
"placeholders": [],
|
|
138
|
+
"tables": [
|
|
139
|
+
{
|
|
140
|
+
"index": 1,
|
|
141
|
+
"rows": 7,
|
|
142
|
+
"cols": 2,
|
|
143
|
+
"location": null,
|
|
144
|
+
"preview": [
|
|
145
|
+
{"row": 0, "cells": ["항목", "기입 내용"]},
|
|
146
|
+
{"row": 1, "cells": ["고객사 / 조직", ""]},
|
|
147
|
+
{"row": 2, "cells": ["현업 담당부서 / 책임자", ""]}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell`을 호출합니다.
|
|
155
|
+
|
|
156
|
+
## 템플릿 작성 규칙
|
|
157
|
+
|
|
158
|
+
### DOCX — Jinja2 전체 문법 사용 가능
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
{{ report_title }}
|
|
162
|
+
작성자: {{ author }}
|
|
163
|
+
|
|
164
|
+
{% for item in items %}- {{ item.name }}: {{ item.value }}
|
|
165
|
+
{% endfor %}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**표 행 반복은 `{%tr for ... %}` / `{%tr endfor %}`를 각각 별도 행에 두어야 합니다.**
|
|
169
|
+
같은 행에 두 태그를 넣으면 `<w:tr>` 전체가 `{% for %}`로 교체되어 `endfor`가 손실됩니다.
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
┌─────────────────────┬─────┬─────┐
|
|
173
|
+
│ 항목 │ 목표 │ 실적 │ <- 헤더
|
|
174
|
+
├─────────────────────┼─────┼─────┤
|
|
175
|
+
│ {%tr for r in rows %} │ <- for 행
|
|
176
|
+
├─────────────────────┼─────┼─────┤
|
|
177
|
+
│ {{ r.name }} │ {{ r.target }} │ {{ r.actual }} │ <- 반복 본문
|
|
178
|
+
├─────────────────────┼─────┼─────┤
|
|
179
|
+
│ {%tr endfor %} │ <- endfor 행
|
|
180
|
+
└─────────────────────┴─────┴─────┘
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### PPTX / HWPX — 단순 `{{key}}` 치환만
|
|
184
|
+
|
|
185
|
+
loop / if / filter는 지원하지 않습니다. PPTX는 placeholder가 여러 `run`으로 쪼개질 수 있어, 어댑터가 paragraph 전체 텍스트를 재조립한 뒤 첫 `run`에 다시 담는 방식으로 처리합니다 (서식 일부 손실 가능).
|
|
186
|
+
|
|
187
|
+
## 내장된 버그 회피
|
|
188
|
+
|
|
189
|
+
| 포맷 | 문제 | 어댑터의 처리 |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| HWPX | `python-hwpx 2.9.0`의 `set_cell_text()`가 빈 셀에서 lxml/ElementTree 혼용 `TypeError` 발생 | `paragraphs[0].text = value` 직접 할당으로 우회 |
|
|
192
|
+
| HWPX | `replace_text_in_runs()`가 한글 공백이 run으로 쪼개진 경우 매칭 실패 | 위치 기반 API만 사용 |
|
|
193
|
+
| HWPX | `manifest fallback` 경고 로그가 과도하게 출력됨 | `logging.getLogger("hwpx")` 레벨을 `ERROR`로 조정 |
|
|
194
|
+
| PPTX | placeholder가 여러 `run`으로 쪼개져 단순 `run.text` 치환이 실패 | paragraph 전체 재조립 |
|
|
195
|
+
| DOCX | `docxtpl`의 `{%tr%}`를 같은 행에 두면 파싱 에러 | README에 배치 규칙 명시 |
|
|
196
|
+
|
|
197
|
+
## 프로젝트 구조
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
document_adapter/
|
|
201
|
+
├── __init__.py # load() dispatcher
|
|
202
|
+
├── base.py # DocumentAdapter ABC, TableSchema, DocumentSchema
|
|
203
|
+
├── docx_adapter.py # DocxAdapter
|
|
204
|
+
├── pptx_adapter.py # PptxAdapter
|
|
205
|
+
├── hwpx_adapter.py # HwpxAdapter (버그 회피 포함)
|
|
206
|
+
├── tools.py # Tool 정의 + call_tool dispatcher
|
|
207
|
+
└── mcp_server.py # MCP stdio server
|
|
208
|
+
|
|
209
|
+
examples/
|
|
210
|
+
└── claude_api_example.py
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 라이선스
|
|
214
|
+
|
|
215
|
+
MIT
|
|
216
|
+
|
|
217
|
+
## Credits
|
|
218
|
+
|
|
219
|
+
- [`python-docx`](https://github.com/python-openxml/python-docx)
|
|
220
|
+
- [`docxtpl`](https://github.com/elapouya/python-docx-template)
|
|
221
|
+
- [`python-pptx`](https://github.com/scanny/python-pptx)
|
|
222
|
+
- [`python-hwpx`](https://github.com/airmang/python-hwpx)
|
|
223
|
+
- [`mcp`](https://github.com/modelcontextprotocol/python-sdk)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Document template editing — 통합 어댑터.
|
|
2
|
+
|
|
3
|
+
사용법:
|
|
4
|
+
from document_adapter import load
|
|
5
|
+
doc = load("report.docx")
|
|
6
|
+
schema = doc.get_schema()
|
|
7
|
+
doc.set_cell(0, 1, 1, "홍길동")
|
|
8
|
+
doc.save("report_filled.docx")
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .base import DocumentAdapter, DocumentSchema, TableSchema
|
|
15
|
+
from .docx_adapter import DocxAdapter
|
|
16
|
+
from .hwpx_adapter import HwpxAdapter
|
|
17
|
+
from .pptx_adapter import PptxAdapter
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"load",
|
|
21
|
+
"DocumentAdapter",
|
|
22
|
+
"DocumentSchema",
|
|
23
|
+
"TableSchema",
|
|
24
|
+
"DocxAdapter",
|
|
25
|
+
"PptxAdapter",
|
|
26
|
+
"HwpxAdapter",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
_ADAPTERS: dict[str, type[DocumentAdapter]] = {
|
|
30
|
+
".docx": DocxAdapter,
|
|
31
|
+
".pptx": PptxAdapter,
|
|
32
|
+
".hwpx": HwpxAdapter,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load(path: str | Path) -> DocumentAdapter:
|
|
37
|
+
"""확장자로 적절한 어댑터를 선택해 문서를 연다."""
|
|
38
|
+
p = Path(path)
|
|
39
|
+
suffix = p.suffix.lower()
|
|
40
|
+
cls = _ADAPTERS.get(suffix)
|
|
41
|
+
if cls is None:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"지원하지 않는 포맷: {suffix}. 지원: {sorted(_ADAPTERS.keys())}"
|
|
44
|
+
)
|
|
45
|
+
return cls(p)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""DocumentAdapter 공통 인터페이스.
|
|
2
|
+
|
|
3
|
+
세 포맷(DOCX/PPTX/HWPX)의 공통 작업을 추상화:
|
|
4
|
+
- 템플릿 렌더링 ({{key}} 치환)
|
|
5
|
+
- 표 스키마 추출 (LLM 입력용)
|
|
6
|
+
- 셀 수정 / 행 추가
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TableSchema:
|
|
18
|
+
"""표 한 개의 구조 (LLM에게 넘길 형태)."""
|
|
19
|
+
index: int
|
|
20
|
+
rows: int
|
|
21
|
+
cols: int
|
|
22
|
+
preview: list[list[str]]
|
|
23
|
+
location: str | None = None
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"index": self.index,
|
|
28
|
+
"rows": self.rows,
|
|
29
|
+
"cols": self.cols,
|
|
30
|
+
"location": self.location,
|
|
31
|
+
"preview": self.preview,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DocumentSchema:
|
|
37
|
+
"""문서 전체 스키마."""
|
|
38
|
+
format: str
|
|
39
|
+
source: str
|
|
40
|
+
placeholders: list[str] = field(default_factory=list)
|
|
41
|
+
tables: list[TableSchema] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"format": self.format,
|
|
46
|
+
"source": self.source,
|
|
47
|
+
"placeholders": self.placeholders,
|
|
48
|
+
"tables": [t.to_dict() for t in self.tables],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DocumentAdapter(ABC):
|
|
53
|
+
"""모든 포맷 어댑터의 공통 부모."""
|
|
54
|
+
|
|
55
|
+
format: str = ""
|
|
56
|
+
|
|
57
|
+
def __init__(self, path: Path) -> None:
|
|
58
|
+
self.path = Path(path)
|
|
59
|
+
self._open()
|
|
60
|
+
|
|
61
|
+
# ---- lifecycle ----
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def _open(self) -> None: ...
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def save(self, path: Path | str | None = None) -> Path: ...
|
|
67
|
+
|
|
68
|
+
def close(self) -> None:
|
|
69
|
+
"""일부 포맷(HWPX)은 명시적 close 필요."""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# ---- inspection ----
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def get_placeholders(self) -> list[str]:
|
|
75
|
+
"""본문에서 사용된 {{key}} 목록 반환."""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def get_tables(self, min_rows: int = 1, min_cols: int = 1,
|
|
79
|
+
preview_rows: int = 4, max_cell_len: int = 40) -> list[TableSchema]:
|
|
80
|
+
"""필터 조건을 만족하는 표 스키마 목록."""
|
|
81
|
+
|
|
82
|
+
def get_schema(self) -> DocumentSchema:
|
|
83
|
+
return DocumentSchema(
|
|
84
|
+
format=self.format,
|
|
85
|
+
source=str(self.path),
|
|
86
|
+
placeholders=self.get_placeholders(),
|
|
87
|
+
tables=self.get_tables(),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# ---- editing ----
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def render_template(self, context: dict[str, Any]) -> None:
|
|
93
|
+
"""템플릿의 {{key}}를 context 값으로 치환."""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def set_cell(self, table_index: int, row: int, col: int, value: str) -> str:
|
|
97
|
+
"""셀 값 교체. 원래 값 반환."""
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def append_row(self, table_index: int, values: list[str]) -> None:
|
|
101
|
+
"""표 끝에 새 행 추가."""
|
|
102
|
+
|
|
103
|
+
# ---- context manager ----
|
|
104
|
+
def __enter__(self) -> "DocumentAdapter":
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
108
|
+
self.close()
|