feat: enrich complex contract relationship rules

This commit is contained in:
Codex
2026-05-04 16:12:09 +08:00
parent fda8b4d0a0
commit 2c3167b652
7 changed files with 379 additions and 3 deletions
+88 -3
View File
@@ -185,6 +185,7 @@ CLAUSE_TYPE_DEFINITIONS = [
{"type": "payment", "label": "付款条款", "keywords": ["付款", "价款", "金额", "发票", "invoice", "payment", "milestone", "billing", "fees"]},
{"type": "acceptance", "label": "验收条款", "keywords": ["验收", "acceptance", "测试", "试运行", "上线", "验收标准"]},
{"type": "confidentiality", "label": "保密条款", "keywords": ["保密", "confidential", "non-disclosure", "nda", "商业秘密"]},
{"type": "intellectual_property", "label": "知识产权", "keywords": ["知识产权", "著作权", "源代码", "软件著作权", "专利", "商标", "技术成果", "intellectual property", "copyright", "ip"]},
{"type": "breach", "label": "违约条款", "keywords": ["违约", "违约金", "赔偿", "liquidated damages", "default", "penalty"]},
{"type": "delivery", "label": "交付条款", "keywords": ["交付", "交货", "deliver", "delivery", "成果", "上线", "部署"]},
{"type": "liability", "label": "责任限制", "keywords": ["免责", "责任限制", "赔偿上限", "liability cap", "disclaimer", "exclusion of liability"]},
@@ -193,7 +194,7 @@ CLAUSE_TYPE_DEFINITIONS = [
{"type": "dispute_resolution", "label": "争议解决", "keywords": ["争议解决", "仲裁", "管辖", "法院", "arbitration", "jurisdiction"]},
]
CLAUSE_TYPE_LABEL_MAP = {item["type"]: item["label"] for item in CLAUSE_TYPE_DEFINITIONS}
STRUCTURE_SECTION_ORDER = ["payment", "acceptance", "delivery", "confidentiality", "breach", "liability", "renewal", "termination", "dispute_resolution"]
STRUCTURE_SECTION_ORDER = ["payment", "acceptance", "delivery", "confidentiality", "intellectual_property", "breach", "liability", "renewal", "termination", "dispute_resolution"]
STRUCTURE_MISSING_SECTION_RULES = [
{"type": "payment", "message": "付款与结算安排"},
{"type": "acceptance", "message": "验收标准或验收流程"},
@@ -232,6 +233,9 @@ PAYMENT_TRIGGER_HINTS = [
("交货", "delivery_completed"),
("交付", "delivery_completed"),
("上线", "go_live_completed"),
("质保期满", "warranty_period_ended"),
("质保期结束", "warranty_period_ended"),
("质保金", "warranty_period_ended"),
]
RISK_CERTAINTY_CONFIRMED = "confirmed"
RISK_CERTAINTY_POSSIBLE = "possible"
@@ -247,6 +251,11 @@ NON_CLAUSE_METADATA_PATTERNS = [
]
RELATION_STATUS_INFO = "info"
RELATION_STATUS_ATTENTION = "attention"
PAYMENT_STAGE_ADVANCE = "advance"
PAYMENT_STAGE_ACCEPTANCE = "acceptance"
PAYMENT_STAGE_GO_LIVE = "go_live"
PAYMENT_STAGE_FINAL = "final"
PAYMENT_STAGE_WARRANTY = "warranty_retention"
pdfmetrics.registerFont(UnicodeCIDFont(PDF_FONT_NAME))
@@ -821,11 +830,12 @@ class ReviewService:
payment_terms = key_sections.get("payment") or []
acceptance_terms = key_sections.get("acceptance") or []
confidentiality_terms = key_sections.get("confidentiality") or []
intellectual_property_terms = key_sections.get("intellectual_property") or []
breach_terms = key_sections.get("breach") or []
dispute_terms = key_sections.get("dispute_resolution") or []
payment_milestones = self._extract_payment_milestones(clauses)
clause_relationships = self._build_clause_relationships(clauses, payment_milestones)
missing_sections = self._detect_missing_sections(clause_type_counts)
missing_sections = self._detect_missing_sections(clause_type_counts, detected_contract_type)
return {
"title": basic_info.get("contract_title") or review.title,
"detected_contract_type": detected_contract_type,
@@ -842,6 +852,7 @@ class ReviewService:
"payment_terms": payment_terms,
"acceptance_terms": acceptance_terms,
"confidentiality_terms": confidentiality_terms,
"intellectual_property_terms": intellectual_property_terms,
"breach_terms": breach_terms,
"dispute_resolution_terms": dispute_terms,
"payment_milestones": payment_milestones,
@@ -853,6 +864,7 @@ class ReviewService:
payment_terms=payment_terms,
acceptance_terms=acceptance_terms,
confidentiality_terms=confidentiality_terms,
intellectual_property_terms=intellectual_property_terms,
breach_terms=breach_terms,
payment_milestones=payment_milestones,
clause_relationships=clause_relationships,
@@ -1004,13 +1016,15 @@ class ReviewService:
return title, content or normalized
return None
def _detect_missing_sections(self, clause_type_counts: Dict[str, int]) -> List[str]:
def _detect_missing_sections(self, clause_type_counts: Dict[str, int], contract_type: str) -> List[str]:
missing: List[str] = []
for item in STRUCTURE_MISSING_SECTION_RULES:
if not clause_type_counts.get(item["type"]):
missing.append(item["message"])
if clause_type_counts.get("payment") and not clause_type_counts.get("acceptance") and "验收标准或验收流程" not in missing:
missing.append("验收标准或验收流程")
if contract_type in {"development", "service", "license"} and not clause_type_counts.get("intellectual_property"):
missing.append("知识产权归属或许可安排")
return missing
def _extract_payment_milestones(self, clauses: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@@ -1028,12 +1042,14 @@ class ReviewService:
deduped_dependencies.append(dependency)
due_match = PAYMENT_DUE_PATTERN.search(segment)
ratio_match = PAYMENT_RATIO_PATTERN.search(segment)
stage = self._classify_payment_stage(segment, deduped_dependencies)
milestones.append(
{
"title": f"{clause.get('title') or '付款条款'}-节点{index}",
"summary": self._summarize_clause_text(segment),
"clause_title": clause.get("title"),
"trigger_text": segment,
"stage": stage,
"due_within": due_match.group(0) if due_match else None,
"due_days": int(due_match.group(1)) if due_match else None,
"due_unit": due_match.group(3) if due_match else None,
@@ -1043,6 +1059,22 @@ class ReviewService:
)
return milestones
def _classify_payment_stage(self, segment: str, dependencies: List[str]) -> str:
lowered = segment.lower()
if any(token in segment for token in ["质保金", "质保期满", "质保期结束"]):
return PAYMENT_STAGE_WARRANTY
if "尾款" in segment:
return PAYMENT_STAGE_FINAL
if "go_live_completed" in dependencies or "上线" in segment:
return PAYMENT_STAGE_GO_LIVE
if "acceptance_passed" in dependencies:
return PAYMENT_STAGE_ACCEPTANCE
if "contract_signed" in dependencies or any(token in segment for token in ["预付款", "首期款", "定金", "签署后"]):
return PAYMENT_STAGE_ADVANCE
if "invoice_received" in dependencies:
return PAYMENT_STAGE_FINAL
return PAYMENT_STAGE_FINAL
def _build_clause_relationships(
self,
clauses: List[Dict[str, Any]],
@@ -1080,8 +1112,34 @@ class ReviewService:
"reason": milestone.get("trigger_text"),
}
)
if "go_live_completed" in milestone.get("dependencies", []):
relationships.append(
{
"type": "payment_depends_on_go_live",
"label": "付款依赖上线稳定运行",
"status": RELATION_STATUS_INFO,
"source_clause_title": milestone.get("clause_title"),
"target_clause_title": None,
"summary": "付款节点包含上线或稳定运行前置条件。",
"reason": milestone.get("trigger_text"),
}
)
if milestone.get("stage") == PAYMENT_STAGE_WARRANTY or "warranty_period_ended" in milestone.get("dependencies", []):
relationships.append(
{
"type": "payment_after_warranty_period",
"label": "付款依赖质保期结束",
"status": RELATION_STATUS_ATTENTION,
"source_clause_title": milestone.get("clause_title"),
"target_clause_title": None,
"summary": "付款节点与质保期或质保金释放挂钩,需要关注质保终止条件是否明确。",
"reason": milestone.get("trigger_text"),
}
)
liability_clause = (clause_by_type.get("liability") or [None])[0]
confidentiality_clause = (clause_by_type.get("confidentiality") or [None])[0]
intellectual_property_clause = (clause_by_type.get("intellectual_property") or [None])[0]
breach_clause = (clause_by_type.get("breach") or [None])[0]
if liability_clause and breach_clause:
liability_content = liability_clause.get("content") or ""
@@ -1097,6 +1155,30 @@ class ReviewService:
"reason": self._summarize_clause_text(liability_content),
}
)
if confidentiality_clause and not any(token in liability_content for token in ["保密", "商业秘密"]):
relationships.append(
{
"type": "liability_cap_conflicts_with_confidentiality",
"label": "责任限制可能冲突保密义务",
"status": RELATION_STATUS_ATTENTION,
"source_clause_title": liability_clause.get("title"),
"target_clause_title": confidentiality_clause.get("title"),
"summary": "责任限制条款未显式排除保密义务风险,可能压缩保密违约后的救济范围。",
"reason": self._summarize_clause_text(liability_content),
}
)
if intellectual_property_clause and not any(token in liability_content for token in ["知识产权", "著作权", "源代码", "软件著作权"]):
relationships.append(
{
"type": "liability_cap_conflicts_with_ip",
"label": "责任限制可能冲突知识产权责任",
"status": RELATION_STATUS_ATTENTION,
"source_clause_title": liability_clause.get("title"),
"target_clause_title": intellectual_property_clause.get("title"),
"summary": "责任限制条款未显式排除知识产权侵权或成果归属争议责任,需重点复核。",
"reason": self._summarize_clause_text(liability_content),
}
)
return relationships
def _compose_contract_summary(
@@ -1106,6 +1188,7 @@ class ReviewService:
payment_terms: List[str],
acceptance_terms: List[str],
confidentiality_terms: List[str],
intellectual_property_terms: List[str],
breach_terms: List[str],
payment_milestones: List[Dict[str, Any]],
clause_relationships: List[Dict[str, Any]],
@@ -1118,6 +1201,8 @@ class ReviewService:
observed.append("验收安排")
if confidentiality_terms:
observed.append("保密义务")
if intellectual_property_terms:
observed.append("知识产权")
if breach_terms:
observed.append("违约责任")
observed_text = "、".join(observed) if observed else "关键条款仍较少"
+32
View File
@@ -395,6 +395,38 @@ class ReviewServiceTestCase(unittest.TestCase):
self.assertIn("invoice_received", milestones[1]["dependencies"])
self.assertIn("payment_depends_on_acceptance", {item["type"] for item in relationships})
def test_relationships_cover_confidentiality_ip_and_warranty_retention(self):
text = "\n".join(
[
"软件实施服务合同",
"合同金额:人民币1500000元",
"第一条 付款条款",
"甲方应在合同签署后5个工作日内支付预付款20%;系统验收合格并收到增值税专用发票后10个工作日内支付实施款50%;系统上线稳定运行30日后支付尾款20%;剩余10%作为质保金,在质保期满后10个工作日内支付。",
"第二条 验收条款",
"乙方完成实施并通过甲方书面验收后,方可进入后续付款节点。",
"第三条 保密条款",
"乙方应对甲方商业秘密承担保密义务。",
"第四条 知识产权",
"项目交付成果、源代码及相关文档的知识产权归甲方所有。",
"第五条 责任限制",
"任何一方因本合同产生的赔偿责任以合同总金额为上限,但故意或重大过失除外。",
"第六条 违约责任",
"乙方延迟交付、泄露保密信息或侵犯第三方知识产权的,应承担违约责任并赔偿损失。",
]
)
clauses = self.service._enrich_clauses([], text)
milestones = self.service._extract_payment_milestones(clauses)
relationships = self.service._build_clause_relationships(clauses, milestones)
relationship_types = {item["type"] for item in relationships}
self.assertGreaterEqual(len(milestones), 4)
self.assertIn("payment_depends_on_go_live", relationship_types)
self.assertIn("payment_after_warranty_period", relationship_types)
self.assertIn("liability_cap_conflicts_with_confidentiality", relationship_types)
self.assertIn("liability_cap_conflicts_with_ip", relationship_types)
self.assertIn("liability_cap_conflicts_with_breach", relationship_types)
def test_validate_review_upload_accepts_supported_suffix(self):
self.service.validate_review_upload("contract.pdf", "application/pdf", b"%PDF-1.7")
docx_buffer = io.BytesIO()
@@ -159,6 +159,50 @@ CASES = [
expected_payment_milestones=2,
expected_relationship_types=["payment_depends_on_acceptance", "payment_depends_on_invoice"],
),
StructuredReviewCase(
case_id="S-006",
label="复杂实施合同关系深化",
contract_type=None,
review_as="party_a",
file_name="structured-implementation.txt",
content="\n".join(
[
"软件实施服务合同",
"甲方:甲方集团有限公司",
"乙方:乙方实施服务有限公司",
"合同金额:人民币1500000元",
"服务期限:自2026年8月1日至2027年8月1日",
"第一条 付款条款",
"甲方应在合同签署后5个工作日内支付预付款20%;系统验收合格并收到增值税专用发票后10个工作日内支付实施款50%;系统上线稳定运行30日后支付尾款20%;剩余10%作为质保金,在质保期满后10个工作日内支付。",
"第二条 验收条款",
"乙方完成实施并通过甲方书面验收后,方可进入后续付款节点。",
"第三条 保密条款",
"乙方应对甲方商业秘密、客户数据和项目资料承担严格保密义务。",
"第四条 知识产权",
"项目交付成果、源代码及相关文档的知识产权归甲方所有。",
"第五条 责任限制",
"任何一方因本合同产生的赔偿责任以合同总金额为上限,但故意或重大过失除外。",
"第六条 违约责任",
"乙方延迟交付、泄露保密信息或侵犯第三方知识产权的,应承担违约责任并赔偿损失。",
]
),
expected_contract_type="service",
expected_clause_types=["payment", "acceptance", "confidentiality", "intellectual_property", "liability", "breach"],
expected_missing_sections=["争议解决条款"],
expected_amount_contains="1500000",
expected_term_start="2026-08-01",
expected_term_end="2027-08-01",
expected_payment_milestones=4,
expected_relationship_types=[
"payment_depends_on_acceptance",
"payment_depends_on_invoice",
"payment_depends_on_go_live",
"payment_after_warranty_period",
"liability_cap_conflicts_with_breach",
"liability_cap_conflicts_with_confidentiality",
"liability_cap_conflicts_with_ip",
],
),
]
+14
View File
@@ -207,6 +207,18 @@ function getRelationshipStatusColor(value: string | null | undefined) {
return "default";
}
function formatPaymentStage(value: string | null | undefined) {
if (!value) return "付款节点";
const stageMap: Record<string, string> = {
advance: "预付款/首期款",
acceptance: "验收后付款",
go_live: "上线后付款",
final: "尾款/最终付款",
warranty_retention: "质保金/质保期尾款",
};
return stageMap[value] ?? value;
}
function buildReviewChatRestorePayload(
detail: ReviewDetail | null,
result: ReviewResult | null,
@@ -482,6 +494,7 @@ export default function ReviewDetailPage({ params }: { params: Promise<{ id: str
{ key: "payment_terms", label: "付款安排", items: contractStructure?.payment_terms ?? [] },
{ key: "acceptance_terms", label: "验收安排", items: contractStructure?.acceptance_terms ?? [] },
{ key: "confidentiality_terms", label: "保密义务", items: contractStructure?.confidentiality_terms ?? [] },
{ key: "intellectual_property_terms", label: "知识产权", items: contractStructure?.intellectual_property_terms ?? [] },
{ key: "breach_terms", label: "违约责任", items: contractStructure?.breach_terms ?? [] },
{ key: "dispute_resolution_terms", label: "争议解决", items: contractStructure?.dispute_resolution_terms ?? [] },
].filter((item) => item.items.length > 0);
@@ -936,6 +949,7 @@ export default function ReviewDetailPage({ params }: { params: Promise<{ id: str
<strong>{item.title ?? `付款节点${index + 1}`}</strong>
<Paragraph className="review-detail-structure-section-text">{item.summary ?? item.trigger_text ?? "-"}</Paragraph>
<Space wrap size={[8, 8]}>
<Tag color="blue">{formatPaymentStage(item.stage)}</Tag>
{item.due_within ? <Tag color="purple">{item.due_within}</Tag> : null}
{item.ratio_percent != null ? <Tag color="blue">{item.ratio_percent}%</Tag> : null}
{(item.dependencies ?? []).map((dependency) => (
+2
View File
@@ -84,6 +84,7 @@ export type ReviewResult = {
payment_terms?: string[];
acceptance_terms?: string[];
confidentiality_terms?: string[];
intellectual_property_terms?: string[];
breach_terms?: string[];
dispute_resolution_terms?: string[];
payment_milestones?: Array<{
@@ -91,6 +92,7 @@ export type ReviewResult = {
summary?: string;
clause_title?: string | null;
trigger_text?: string;
stage?: string;
due_within?: string | null;
due_days?: number | null;
due_unit?: string | null;
@@ -0,0 +1,192 @@
# OCR 后续能力扩展阶段 1 第四批执行记录(2026-05-04)
## 1. 本批目标
基于阶段 1 第三批已经完成的字段抽取深化与条款关系理解,继续推进第四批:
1. 把更真实、更复杂的合同形态拉入结构化样本库。
2. 深化付款分期、尾款、质保金等付款节点识别。
3. 深化责任限制与保密、知识产权、违约之间的冲突关系规则。
4. 让这些复杂关系在线上页面可直接观察和验证。
## 2. 本批改造
### 后端
1. `backend/app/services/review_service.py`
- 新增条款类型:
- `intellectual_property`
- 结构化结果新增:
- `contract_structure.intellectual_property_terms`
- `contract_structure.amount_details`
- `contract_structure.term_details`
- `contract_structure.payment_milestones`
- `contract_structure.clause_relationships`
- 新增金额明细抽取:
- 原始金额
- 标准币种
- 数值
- 单位
- 归一化人民币金额
- 新增期限明细抽取:
- 原始期限
- 开始日期
- 结束日期
- 新增付款节点深化:
- 预付款/首期款
- 验收后付款
- 上线后付款
- 尾款
- 质保金/质保期尾款
- 新增付款节点依赖识别:
- `contract_signed`
- `acceptance_passed`
- `invoice_received`
- `go_live_completed`
- `warranty_period_ended`
- 新增条款关系:
- `payment_depends_on_acceptance`
- `payment_depends_on_invoice`
- `payment_depends_on_go_live`
- `payment_after_warranty_period`
- `liability_cap_conflicts_with_breach`
- `liability_cap_conflicts_with_confidentiality`
- `liability_cap_conflicts_with_ip`
- 结构化分析元数据新增:
- `payment_milestone_count`
- `relationship_count`
- 审查报告导出同步增强:
- 付款节点列表
- 条款关系列表
2. 条款分段与分类继续精修:
- `第一条 交货条款` 这类标题语义进一步保留。
- `交货` 明确纳入交付条款识别。
- `责任限制` 明确优先归入责任限制,而不是被违约语义抢走。
### 门禁与样本
1. `backend/tools/review_structured_acceptance_gate.py`
- 结构化验收门禁扩展到 6 个样本。
- 断言范围从:
- 合同类型
- 条款类型
- 缺失条款
- 扩展到:
- 金额片段
- 期限起止
- 付款节点数量
- 关系类型
2. 新增复杂真实形态样本:
- `S-006` 复杂实施合同关系深化
3. 新增样本清单:
- [结构化复杂合同样本清单-2026-05-04.csv](D:/ai-contract-platform/project-docs/结构化复杂合同样本清单-2026-05-04.csv)
### 前端
1. `frontend/src/services/review.ts`
- 承接第四批新增字段:
- `intellectual_property_terms`
- `amount_details`
- `term_details`
- `payment_milestones[].stage`
2. `frontend/src/app/reviews/[id]/page.tsx`
- 结构化摘要区新增:
- 标准金额
- 开始日期 / 结束日期
- 付款节点数
- 新增付款节点展示:
- 节点阶段
- 支付时限
- 支付比例
- 依赖条件
- 新增条款关系展示:
- 关系标签
- 来源条款
- 目标条款
- 风险强度
## 3. 样本深化结果
当前结构化样本已经覆盖:
1. 服务合同基础摘要
2. 采购合同缺失条款
3. 购销合同编号条款切分
4. 租赁合同责任限制识别
5. 开发合同付款节点拆解
6. 复杂实施合同:
- 多阶段付款
- 验收依赖
- 发票依赖
- 上线依赖
- 质保金释放
- 保密
- 知识产权
- 责任限制
- 违约责任
## 4. 验证结果
### 本地验证
1. 后端定向:
- `backend\\.venv\\Scripts\\python.exe -m unittest tests.test_review_service tests.test_review_service_rules tests.test_reviews_api`
- `PASS`
2. 前端构建:
- `frontend\\npm.cmd run build`
- `PASS`
### 线上验证
1. 后端健康检查:
- `http://192.168.3.90:8001/health`
- `PASS`
2. 结构化审查门禁:
- `project-docs/qa-artifacts/review-structured-acceptance-gate/2026-05-04T16-08-05-607/`
- 结果:`PASS`
3. PDF 验收门禁:
- `project-docs/qa-artifacts/pdf-acceptance-gate/2026-05-04T16-08-06-016/`
- 结果:`PASS`
4. 前端烟雾验证:
- `project-docs/qa-artifacts/stage1-batch4-front-smoke/2026-05-04T08-09-30-879Z/`
- 已确认:
- 结构化字段卡片可见
- 付款节点区可见
- 条款关系区可见
- 证据关键词标签可见
## 5. 部署记录
1. 联合部署:
- `project-docs/qa-artifacts/stage1-batch4-full-deploy/2026-05-04T16-05-01/`
## 6. 当前结论
阶段 1 第四批已经闭环:
1. 样本库从“结构化演示样本”升级到了“更接近真实生产形态的复杂样本”。
2. 付款节点已经可以拆出预付款、尾款、质保金等关键节点。
3. 责任限制与保密、知识产权、违约之间的冲突关系已经进入规则层。
4. 前端详情页已经能直接展示这些复杂结构和关系。
5. 扩展后的结构化门禁和 PDF 门禁均已通过。
## 7. 下一步建议
下一步最自然的是进入:
**阶段 1 第五批:真实业务合同批量入样与规则置信度校正**
建议优先处理:
1. 引入更多真实业务来源的合同文本样本。
2. 建立字段抽取准确率与关系命中准确率对照表。
3. 区分高置信关系与弱提示关系。
4. 对付款节点、知识产权归属、责任限制 carve-out 继续做更细规则。
@@ -0,0 +1,7 @@
sample_id,title,contract_family,shape,current_focus,expected_highlights
S-001,服务合同结构化摘要,service,text_contract,基础结构化摘要,合同类型/金额/期限/付款/保密/违约
S-002,采购合同缺失条款提示,procurement,text_contract,缺失条款识别,付款/交付/缺失验收/缺失保密
S-003,购销合同编号条款分段,sales,numbered_contract,编号条款切分,交货/付款/争议解决
S-004,租赁合同责任限制识别,lease,liability_cap_contract,责任限制关系,交付/责任限制/违约
S-005,开发合同付款节点拆解,development,milestone_contract,付款节点拆解,首期款/尾款/验收依赖/发票依赖
S-006,复杂实施合同关系深化,service,complex_realistic_contract,复杂关系规则,付款分期/质保金/保密/知识产权/责任限制/违约
1 sample_id title contract_family shape current_focus expected_highlights
2 S-001 服务合同结构化摘要 service text_contract 基础结构化摘要 合同类型/金额/期限/付款/保密/违约
3 S-002 采购合同缺失条款提示 procurement text_contract 缺失条款识别 付款/交付/缺失验收/缺失保密
4 S-003 购销合同编号条款分段 sales numbered_contract 编号条款切分 交货/付款/争议解决
5 S-004 租赁合同责任限制识别 lease liability_cap_contract 责任限制关系 交付/责任限制/违约
6 S-005 开发合同付款节点拆解 development milestone_contract 付款节点拆解 首期款/尾款/验收依赖/发票依赖
7 S-006 复杂实施合同关系深化 service complex_realistic_contract 复杂关系规则 付款分期/质保金/保密/知识产权/责任限制/违约