From a55424b51b1c717e1255609c6c19a9f24c49eca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=90=EC=84=B1=EC=A4=80?= Date: Tue, 2 Jun 2026 10:06:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20get=5Fform=5Ffields=20+=20inspect=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=9D=BC=EB=B2=A8=20=ED=9E=8C=ED=8A=B8=20?= =?UTF-8?q?(dot-path=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EC=A0=88=EA=B0=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 궁극 로드맵 #4(신뢰성 — inspect 구조 힌트). fill_form 의 가장 흔한 실패는 LLM 이 중복 라벨을 모르고 dot-path 없이 채워 ambiguous 에 부딪혀 재시도하는 것. - base.get_form_fields(): 표 셀의 라벨 후보와 ambiguous(중복) 여부 + 위치 반환. - inspect_document: duplicate_labels + dot-path 힌트를 응답에 추가 → LLM 이 채우기 *전에* dot-path 필요를 인지. 회귀 테스트 추가. 83개 통과, ruff·mypy 클린. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 3 +++ document_adapter/base.py | 33 +++++++++++++++++++++++++++++++++ document_adapter/tools.py | 15 +++++++++++++++ tests/test_scenarios.py | 21 +++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e38666..e8205ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: ## [Unreleased] ### Added +- **`get_form_fields()` + inspect 중복 라벨 힌트** — 라벨 후보와 *중복(dot-path 필요)* + 여부를 미리 보여준다. `inspect_document` 응답에 `duplicate_labels` + 힌트를 추가해 + LLM 이 fill_form 전에 dot-path 필요를 인지(ambiguous 재시도 라운드 절감). - **`diff_documents(path_a, path_b)`** — 편집 전/후 문서를 셀 단위로 비교해 변경된 셀의 before/after 와 overflow_risk 를 반환하는 **검증 도구**(MCP 도구 → 총 12 개). fill 후 원본과 diff 해 "무엇이 어디서 바뀌었고 깨짐 위험은 없나" 를 LLM 이 스스로 diff --git a/document_adapter/base.py b/document_adapter/base.py index 5c3503b..42322d8 100644 --- a/document_adapter/base.py +++ b/document_adapter/base.py @@ -376,6 +376,39 @@ def get_schema(self) -> DocumentSchema: tables=self.get_tables(), ) + def get_form_fields(self) -> list[dict[str, Any]]: + """표 셀 텍스트에서 라벨 후보와 **중복 여부(dot-path 필요)**를 미리 보여준다. + + fill_form 호출 *전에* LLM 이 "어떤 라벨이 여러 곳에 있어 dot-path 로 구분해야 + 하는지" 를 알 수 있게 한다 — ambiguous 응답을 받고 재시도하는 라운드를 줄인다. + + 각 항목: {label, normalized, ambiguous, occurrences:[{table_index,row,col}]}. + (라벨로 보이는 비어있지 않은 anchor 셀 기준 — 빈 양식에서 가장 유용.) + """ + index: dict[str, list[dict[str, Any]]] = {} + labels: dict[str, str] = {} + for t in self.get_tables(preview_rows=10_000, max_cell_len=200): + for r, row in enumerate(t.preview): + for c, val in enumerate(row): + if not val: + continue + norm = _normalize_label(val) + if not norm: + continue + index.setdefault(norm, []).append( + {"table_index": t.index, "row": r, "col": c}) + labels.setdefault(norm, val) + fields = [ + { + "label": labels[norm], + "normalized": norm, + "ambiguous": len(occ) > 1, + "occurrences": occ, + } + for norm, occ in index.items() + ] + return sorted(fields, key=lambda f: f["label"]) + # ---- editing ---- @abstractmethod def render_template(self, context: dict[str, Any], *, diff --git a/document_adapter/tools.py b/document_adapter/tools.py index 8f183cf..415ad75 100644 --- a/document_adapter/tools.py +++ b/document_adapter/tools.py @@ -382,6 +382,21 @@ def inspect_document(path: str, min_rows: int = 1, min_cols: int = 1) -> dict[st except NotImplementedError: pass # DOCX / HWPX 는 shape 개념 약함 — skip result["tables"] = [t.to_dict() for t in filtered] + + # 중복 라벨 힌트: 여러 곳에 같은 라벨이 있으면 fill_form 에서 ambiguous + # 가 되므로, 미리 dot-path 를 쓰라고 신호한다 (재시도 라운드 절감). + dups = [f for f in doc.get_form_fields() if f["ambiguous"]] + if dups: + result["duplicate_labels"] = [ + {"label": f["label"], + "count": len(f["occurrences"]), + "locations": f["occurrences"]} + for f in dups + ] + result["duplicate_labels_hint"] = ( + "같은 라벨이 여러 곳에 있습니다. fill_form 에서 이 라벨들은 " + "dot-path(예: '피해자.금액')로 섹션을 구분해 채우세요." + ) return result finally: doc.close() diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 95ca0a3..37cf523 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -714,6 +714,27 @@ def test_pptx_notes_placeholders_and_render(tmp_path: Path) -> None: assert any("확인됨" in n for n in notes) +def test_get_form_fields_and_duplicate_hint(tmp_path: Path) -> None: + """get_form_fields 가 중복 라벨을 ambiguous 로 표시하고, inspect_document 가 + duplicate_labels 힌트를 미리 제공해야 한다 (dot-path 재시도 절감).""" + from document_adapter.tools import call_tool + + src = tmp_path / "dup.hwpx" + _make_form_hwpx(src, [("피해자정보", ""), ("금액", ""), + ("지급정지", ""), ("금액", "")]) + ad = load(src) + fields = {f["label"]: f for f in ad.get_form_fields()} + ad.close() + assert fields["금액"]["ambiguous"] is True + assert len(fields["금액"]["occurrences"]) == 2 + assert fields["지급정지"]["ambiguous"] is False + + r = call_tool("inspect_document", {"path": str(src)}) + dups = {d["label"]: d for d in r.get("duplicate_labels", [])} + assert "금액" in dups and dups["금액"]["count"] == 2 + assert "duplicate_labels_hint" in r + + def test_diff_documents(tmp_path: Path) -> None: """diff_documents: 편집 전/후 변경 셀을 before/after + overflow 로 반환.""" import shutil