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