Commit 8371d9cd by ccran

feat: fix bugs for bookmark;

parent 1152e468
......@@ -18,6 +18,12 @@ class LLMConfig:
api_key: str = "none"
model: str = "Qwen2-72B-Instruct"
# @dataclass
# class LLMConfig:
# base_url: str = "http://192.168.252.71:9002/v1"
# api_key: str = "none"
# model: str = "Qwen2-72B-Instruct"
# 最大分片数量
min_single_chunk_size = 2000
max_single_chunk_size = 100000
......
No preview for this file type
File added
from PIL import Image
import pytesseract
img = Image.open("ocr.png")
text = pytesseract.image_to_string(img,lang="chi_sim+eng")
print(text)
\ No newline at end of file
from utils.spire_word_util import SpireWordDoc
if __name__ == "__main__":
doc = SpireWordDoc()
doc.load('demo/限制版-模板-采购合同-2025-11.doc')
doc.add_chunk_comment(0,[
{
"id": 0, # 必填,规则/评论唯一标识,和 key_points 共同组成批注作者键
"key_points": "审核要点1", # 必填,审核要点,和 id 共同用于去重、更新、删除批注
"result": "不合格", # 必填,仅 result == "不合格" 的评论会被添加/更新批注
"suggest": "这是第一条批注", # 可选,批注正文;缺省为空字符串
"original_text": "③分期付款:(适用于货物直接发到买方指定现场的),发货前支付 %预付款 元;到货款自买方签署到货签收单后 个工作日内支付 %到货款 元; 验收款自买方(项目经理或工程人员)签署验收单后 个工作日内支付 %验收款 元; %作为质保金,合计 元,合同产品质保期届满后 个工作日支付。", # 可选,优先用于定位原文;为空时批注落在文档首个可用段落
"chunk_id": 0, # 可选,0 基 chunk 下标;有效时优先于入参 chunk_id
}
])
doc.to_file('demo/限制版-模板-采购合同-2025-11-添加批注.doc')
\ No newline at end of file
import argparse
import difflib
from itertools import zip_longest
from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Compare Word revision text and write diff rows to Excel.")
parser.add_argument("--input_file", default=r'D:\VsProject\lufa-contract\demo\5、[修订后复审版]麓谷发展集团2026-2027年质量.docx',
help="Path to the Word document with revisions.")
parser.add_argument(
"-o",
"--output",
help="Path to the output Excel file. Defaults to '<input_stem>_修订对比.xlsx'.",
)
parser.add_argument(
"--show-text",
action="store_true",
help="Print full text before and after accepting revisions.",
)
parser.add_argument(
"--no-diff-print",
action="store_true",
help="Do not print unified diff to the console.",
)
return parser.parse_args()
def get_default_output_file(input_file: Path) -> Path:
return input_file.with_name(f"{input_file.stem}_修订对比.xlsx")
def read_revision_texts(input_file: Path) -> tuple[str, str]:
from spire.doc import Document
doc = Document()
try:
doc.LoadFromFile(str(input_file))
before_text = doc.GetText()
if doc.HasChanges:
doc.AcceptChanges()
after_text = doc.GetText()
return before_text, after_text
finally:
doc.Close()
def build_unified_diff(before_lines: list[str], after_lines: list[str]) -> list[str]:
return list(
difflib.unified_diff(
before_lines,
after_lines,
fromfile="before_revision",
tofile="after_revision",
lineterm="",
)
)
def build_diff_rows(before_lines: list[str], after_lines: list[str]) -> list[tuple[str, str]]:
rows: list[tuple[str, str]] = []
matcher = difflib.SequenceMatcher(None, before_lines, after_lines)
for tag, before_start, before_end, after_start, after_end in matcher.get_opcodes():
if tag == "equal":
continue
before_part = before_lines[before_start:before_end]
after_part = after_lines[after_start:after_end]
if tag == "replace":
rows.extend(
(before_line or "", after_line or "")
for before_line, after_line in zip_longest(before_part, after_part)
)
elif tag == "delete":
rows.extend((before_line, "") for before_line in before_part)
elif tag == "insert":
rows.extend(("", after_line) for after_line in after_part)
return rows
def write_diff_rows_to_excel(diff_rows: list[tuple[str, str]], output_file: Path) -> None:
wb = Workbook()
ws = wb.active
ws.title = "revision_compare"
ws.append(["修订前", "修订后"])
for cell in ws[1]:
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center", vertical="center")
for before_line, after_line in diff_rows:
ws.append([before_line, after_line])
for row in ws.iter_rows(min_row=2):
for cell in row:
cell.alignment = Alignment(wrap_text=True, vertical="top")
if row[1].value:
row[1].font = Font(color="FF0000")
ws.column_dimensions["A"].width = 80
ws.column_dimensions["B"].width = 80
ws.freeze_panes = "A2"
wb.save(output_file)
def compare_revisions(input_file: Path, output_file: Path, show_text: bool, print_diff: bool) -> bool:
before_text, after_text = read_revision_texts(input_file)
if show_text:
print("===== 修订前文本 =====")
print(before_text)
print("===== 修订后文本 =====")
print(after_text)
if before_text == after_text:
print("修订前后文本一致,无差异,不写入 Excel。")
return False
before_lines = before_text.splitlines()
after_lines = after_text.splitlines()
diff_lines = build_unified_diff(before_lines, after_lines)
diff_rows = build_diff_rows(before_lines, after_lines)
if not diff_rows:
print("没有检测到真实差异,不写入 Excel。")
return False
if print_diff:
print("===== Diff 对比结果 =====")
print("\n".join(diff_lines))
write_diff_rows_to_excel(diff_rows, output_file)
print(f"Excel Diff 对比结果已写入: {output_file}")
return True
def main() -> None:
args = parse_args()
input_file = Path(args.input_file)
output_file = Path(args.output) if args.output else get_default_output_file(input_file)
compare_revisions(
input_file=input_file,
output_file=output_file,
show_text=args.show_text,
print_diff=not args.no_diff_print,
)
if __name__ == "__main__":
main()
File added
---
name: review-flow-skill
description: 合同审查编排 Skill。用于把 URL 或本地合同文件下载/解析为 txt,逐条覆盖 rules.xlsx 中的审查规则,调用 review-llm-skill 完成路由、审查、反思、合并,并将结果文件上传。
---
# Review Flow Skill
## 定位
`review-flow-skill` 是合同审查的流程编排层。它不直接实现 HTTP、文档解析、Excel 读写或 LLM 提示词,而是按固定顺序组合以下 Skill:
- `http-skill`:URL 输入时下载源文件;审查完成后上传结果文件。
- `doc-excel-skill`:把 Word/PDF/Excel/CSV 解析成 UTF-8 txt;读取 `assets/rules.xlsx`;把 JSON 结果写入 Excel。
- `review-llm-skill`:执行规则路由、具体审查、反思复核、结果合并。
## 标准流程
1. 输入识别
- 如果输入是 `http://``https://` URL,必须先调用 `http-skill` 下载到本地工作目录。
- 如果输入是本地路径,直接进入解析步骤。
2. 文档解析
- 优先把待审文件解析为 `.txt`,减少后续上下文占用。
- `.txt` 直接复用。
- `.doc``.docx``.wps``.pdf` 使用 `doc-excel-skill/scripts/doc_tool.py` 转 txt。
- `.xlsx``.xls``.csv``.tsv` 使用 `doc-excel-skill/scripts/excel_tool.py` 读取各 sheet 后序列化为 txt。
3. 规则加载
- 默认从 `skills/review-flow-skill/assets/rules.xlsx` 读取审查规则。
- 未指定 sheet 时读取所有 sheet。
- 必须覆盖到每一条非空规则。流程不得因为路由未命中而跳过“规则覆盖记录”。
4. 状态记录
- 每次运行在工作目录生成 `state.json`
- 状态至少记录:源文件、txt 文件、规则文件、规则总数、当前规则序号、已完成规则、输出文件路径。
- 每完成一条规则立即写回状态,用于断点续审。
5. 逐条规则审查
- 按规则顺序逐条推进。
- 对每条规则,先对 txt 分块调用 `review-llm-skill``router` 动作。
- 只有路由命中的分块才调用 `review` 动作。
- 即使所有分块均未命中,该规则也必须标记为已覆盖,并记录路由未命中。
6. 单规则反思
- 每条规则的分块审查完成后,将该规则的原始 findings 放入 `context.findings`,调用 `reflect` 动作复核。
- 反思结果写入中间结果文件。
7. 全局合并
- 所有规则审查完毕后,对反思后的 findings 按 `rule_name``result``original_text` 聚合。
- 同组多条 findings 调用 `merge` 动作融合 `issue``suggestion`
- 输出最终 JSON 和 Excel 汇总文件。
8. 结果上传
- 默认使用 `http-skill` 上传最终 Excel 文件。
- 如调用方只需要本地文件,可传 `--no-upload`
## 工具文件
- `scripts/review_flow.py`:审查流程编排 CLI。
- `assets/rules.xlsx`:默认审查规则库。
## 输出文件
运行目录默认位于 `skills/review-flow-skill/outputs/run-YYYYmmdd-HHMMSS/`,包含:
- `input.txt`:解析后的合同文本。
- `state.json`:断点续审状态。
- `rule_progress.json`:每条规则的覆盖、路由、审查统计。
- `findings_raw.json`:分块审查原始结果。
- `findings_reflected.json`:按规则反思后的结果。
- `final_findings.json`:全局合并后的最终 JSON 结果。
- `review_result.xlsx`:最终 Excel 汇总文件。
- `upload_result.json`:上传结果,仅在上传成功后生成。
## 使用示例
查看帮助:
```bash
python skills/review-flow-skill/scripts/review_flow.py --help
```
审查 URL 文件并上传结果:
```bash
python skills/review-flow-skill/scripts/review_flow.py \
https://example.com/contract.docx
```
审查本地文件,不上传:
```bash
python skills/review-flow-skill/scripts/review_flow.py \
demo/example.docx \
--no-upload
```
只审查指定 sheet:
```bash
python skills/review-flow-skill/scripts/review_flow.py \
demo/example.pdf \
--sheet-name 通用 \
--sheet-name 借款
```
从已有运行目录继续:
```bash
python skills/review-flow-skill/scripts/review_flow.py \
--resume skills/review-flow-skill/outputs/run-20260612-173000
```
调试时限制本次最多推进 3 条规则:
```bash
python skills/review-flow-skill/scripts/review_flow.py \
demo/example.docx \
--max-rules 3 \
--no-upload
```
## 约束
- 不在该 Skill 中重写下载、上传、文档解析、Excel 操作或 LLM 提示词能力。
- 审查过程必须逐条规则推进并持久化状态。
- 规则覆盖记录和最终结果必须落盘,避免只保存在上下文中。
- 默认使用 txt 分块输入 LLM,避免一次性塞入完整合同造成上下文过大。
#!/usr/bin/env python3
"""Backward-compatible entry point for segment_llm_action.py."""
from __future__ import annotations
from segment_llm_action import * # noqa: F403
from segment_llm_action import main
if __name__ == "__main__":
raise SystemExit(main())
......@@ -19,7 +19,6 @@ try:
REFLECT_SYSTEM_PROMPT,
REVIEW_SYSTEM_PROMPT,
ROUTER_SYSTEM_PROMPT,
SUMMARY_ROUTER_SYSTEM_PROMPT,
SUMMARY_SYSTEM_PROMPT,
)
except ImportError:
......@@ -28,7 +27,6 @@ except ImportError:
REFLECT_SYSTEM_PROMPT,
REVIEW_SYSTEM_PROMPT,
ROUTER_SYSTEM_PROMPT,
SUMMARY_ROUTER_SYSTEM_PROMPT,
SUMMARY_SYSTEM_PROMPT,
)
......
......@@ -470,6 +470,7 @@ class SpireWordDoc(DocBase):
current_child_text = self._resolve_table(table)
# 跳过其他非文本子对象
else:
print(child_obj)
continue
# 添加新对象
if (
......@@ -794,6 +795,22 @@ class SpireWordDoc(DocBase):
def add_chunk_comment(self, chunk_id, comments):
"""
为 chunk 添加批注(保证每条评论只批注一次)。
comments 为评论字典列表,每个元素结构如下:
{
"id": str | int, # 必填,规则/评论唯一标识,和 key_points 共同组成批注作者键
"key_points": str, # 必填,审核要点,和 id 共同用于去重、更新、删除批注
"result": "不合格" | str, # 必填,仅 result == "不合格" 的评论会被添加/更新批注
"suggest": str, # 可选,批注正文;缺省为空字符串
"original_text": str, # 可选,优先用于定位原文;为空时批注落在文档首个可用段落
"chunk_id": int, # 可选,0 基 chunk 下标;有效时优先于入参 chunk_id
}
说明:
- 批注作者会格式化为 "{id}|{key_points}",用于识别同一条评论并避免重复插入。
- original_text 非空时,先在目标 chunk 内精确匹配,失败后再做模糊匹配。
- comment["chunk_id"] 缺失或无效时,使用入参 chunk_id。
执行顺序:
1) 过滤非“不合格”项;
2) 先按作者标识查重,命中则更新内容;
......@@ -936,6 +953,9 @@ class SpireWordDoc(DocBase):
if remove_prefix:
self.remove_comment_prefix()
self._doc.Replace('!Undefined Bookmark,','',False,True)
self._doc.Replace('!Undefined Book','',False,True)
self._doc.Replace('!Undefined B','',False,True)
self._doc.SaveToFile(path)
def release(self):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment