Enhance contract generation governance

This commit is contained in:
2026-05-28 11:01:57 +08:00
parent 2c9038b4b8
commit ea722e58eb
18 changed files with 1441 additions and 183 deletions
+31 -14
View File
@@ -11,6 +11,7 @@ from app.clients.shared_ai_platform import SharedAIPlatformClient, get_shared_ai
from app.db import get_db_session
from app.schemas.chat import ChatStreamRequest
from app.services.legal_article_service import LegalArticleService
from app.services.legal_domain_registry import BUSINESS_AUXILIARY_PROMPT_KEYS
from app.services.prompt_runtime_service import PromptRuntimeService
from app.services.review_service import ReviewService
from app.services.tenant_service import TenantService
@@ -21,14 +22,6 @@ ROUTE_PERMISSION_ACTIONS = {
"stream": "chat:stream",
}
BUSINESS_AUXILIARY_PROMPT_KEYS = {
"obligation_extraction": "obligation_extraction_default",
"filing_materials_checklist": "filing_materials_checklist_default",
"negotiation_strategy": "negotiation_strategy_default",
"legal_opinion_boundary_fallback": "legal_opinion_boundary_fallback_default",
}
def get_chat_review_service(db: Session = Depends(get_db_session)) -> ReviewService:
return ReviewService(db)
@@ -138,23 +131,47 @@ def _detect_business_auxiliary_prompt_key(
message: str,
review_context: Optional[Dict[str, Any]],
) -> Optional[str]:
text = _build_prompt_intent_text(message, review_context)
if _contains_any(text, ["法律意见书强边界", "法律意见书兜底", "法律意见书边界", "legal opinion boundary", "legal opinion fallback"]):
context_text = _build_review_context_action_text(review_context)
if context_text:
routed_key = _detect_business_auxiliary_prompt_key_from_text(context_text)
if routed_key:
return routed_key
return _detect_business_auxiliary_prompt_key_from_text(_build_prompt_intent_text(message, review_context))
def _detect_business_auxiliary_prompt_key_from_text(text: str) -> Optional[str]:
if _contains_any(text, ["法律意见书强边界", "法律意见书兜底", "法律意见书边界", "使用限制", "前提假设", "不能出具", "正式法律意见", "legal opinion boundary", "legal opinion fallback"]):
return BUSINESS_AUXILIARY_PROMPT_KEYS["legal_opinion_boundary_fallback"]
if _contains_any(text, ["履约义务", "履行义务", "义务提取", "履约清单", "obligation extraction", "performance obligation"]):
if _contains_any(text, ["履约义务", "履行义务", "义务提取", "履约清单", "义务清单", "交付义务", "配合义务", "双方要做什么", "对方需要做什么", "obligation extraction", "performance obligation", "performance checklist"]):
return BUSINESS_AUXILIARY_PROMPT_KEYS["obligation_extraction"]
if _contains_any(text, ["工商", "备案材料", "变更登记", "材料清单", "filing materials", "registration checklist", "filing checklist"]):
if _contains_any(text, ["工商", "备案材料", "变更登记", "登记材料", "材料清单", "提交材料", "申报材料", "登记机关", "filing materials", "registration materials", "registration checklist", "filing checklist"]):
return BUSINESS_AUXILIARY_PROMPT_KEYS["filing_materials_checklist"]
if _contains_any(text, ["谈判策略", "谈判建议", "怎么谈", "议价", "让步方案", "negotiation strategy", "negotiate"]):
if _contains_any(text, ["谈判策略", "谈判建议", "怎么谈", "如何谈", "议价", "让步方案", "让步空间", "底线", "沟通话术", "对方不接受", "替代方案", "negotiate", "negotiation strategy", "negotiation plan", "fallback position"]):
return BUSINESS_AUXILIARY_PROMPT_KEYS["negotiation_strategy"]
if _contains_any(text, ["法律意见书", "legal opinion"]) and _contains_any(
text,
["边界", "兜底", "免责声明", "不能替代", "人工复核", "boundary", "fallback", "disclaimer", "human review"],
["边界", "兜底", "免责声明", "不能替代", "人工复核", "使用限制", "前提假设", "boundary", "fallback", "disclaimer", "human review", "assumption", "limitation"],
):
return BUSINESS_AUXILIARY_PROMPT_KEYS["legal_opinion_boundary_fallback"]
return None
def _build_review_context_action_text(review_context: Optional[Dict[str, Any]]) -> str:
if not isinstance(review_context, dict):
return ""
metadata = review_context.get("metadata") if isinstance(review_context.get("metadata"), Mapping) else {}
parts = [
metadata.get("actionLabel"),
metadata.get("focusLabel"),
metadata.get("reviewOpinion"),
review_context.get("actionLabel"),
review_context.get("focusLabel"),
review_context.get("prompt"),
]
return "\n".join(str(item) for item in parts if item).lower()
def _build_prompt_intent_text(message: str, review_context: Optional[Dict[str, Any]]) -> str:
parts = [message or ""]
if isinstance(review_context, dict):
+61 -5
View File
@@ -72,6 +72,15 @@ class SharedAIPlatformClient(Protocol):
content: Optional[bytes] = None,
) -> dict: ...
def enhance_contract_generation(
self,
current_user: Dict[str, Any],
*,
prompt_runtime: Optional[Dict[str, Any]],
prompt: str,
draft_content: str,
metadata: Optional[Dict[str, Any]] = None,
) -> dict: ...
class RemoteSharedAIPlatformClient:
@@ -282,12 +291,59 @@ class RemoteSharedAIPlatformClient:
)
return self._normalize_remote_knowledge_document_item(payload)
def _context(self, current_user: Dict[str, Any]) -> SharedAIRequestContext:
return SharedAIRequestContext(
tenant_id=str(current_user.get("tenant_id") or ""),
user_id=str(current_user.get("id") or ""),
user_role=str(current_user.get("role") or "admin"),
def enhance_contract_generation(
self,
current_user: Dict[str, Any],
*,
prompt_runtime: Optional[Dict[str, Any]],
prompt: str,
draft_content: str,
metadata: Optional[Dict[str, Any]] = None,
) -> dict:
template_key = str((prompt_runtime or {}).get("template_key") or "").strip() or None
payload = dict(metadata or {})
payload["draft_content"] = draft_content
payload["prompt_version"] = (prompt_runtime or {}).get("prompt_version")
payload["prompt_key"] = (prompt_runtime or {}).get("prompt_key")
payload["prompt_type"] = (prompt_runtime or {}).get("prompt_type")
stream = self.stream_answer(
current_user,
None,
prompt,
review_context=payload,
template_key=template_key,
)
text_parts: list[str] = []
for chunk in stream:
if not isinstance(chunk, str):
continue
for line in chunk.splitlines():
if not line.startswith("data:"):
continue
raw = line[5:].strip()
if not raw:
continue
try:
event = json.loads(raw)
except json.JSONDecodeError:
continue
if not isinstance(event, Mapping):
continue
if event.get("type") == "text":
text_parts.append(str(event.get("content") or ""))
elif event.get("type") == "error":
raise RuntimeError(str(event.get("message") or "shared ai enhancement failed"))
content = "".join(text_parts).strip()
if not content:
raise RuntimeError("shared ai enhancement returned empty content")
return {
"content": content,
"provider": "shared-ai-platform",
"model": (prompt_runtime or {}).get("model"),
"prompt_key": (prompt_runtime or {}).get("prompt_key"),
"prompt_version": (prompt_runtime or {}).get("prompt_version"),
}
def _format_event(self, payload: Dict[str, object]) -> str:
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
@@ -13,6 +13,7 @@ from urllib.parse import quote
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.clients.shared_ai_platform import SharedAIPlatformClient, build_shared_ai_platform_client
from app.models.file import File
from app.models.generated_contract import GeneratedContract
from app.models.generated_contract_version import GeneratedContractVersion
@@ -22,6 +23,7 @@ from app.repositories.contract_template_repository import ContractTemplateReposi
from app.repositories.generated_contract_repository import GeneratedContractRepository
from app.repositories.generated_contract_version_repository import GeneratedContractVersionRepository
from app.services.file_service import FileService
from app.services.legal_domain_registry import RISK_CLAUSE_SUGGESTION_RULES, document_type_candidates, generation_prompt_key_candidates, legal_query_terms
from app.services.legal_article_service import LegalArticleService
from app.services.legal_document_element_schema_service import LegalDocumentElementSchemaService
from app.services.prompt_runtime_service import PromptRuntimeService
@@ -37,45 +39,6 @@ class ExportFormat(str, Enum):
MISSING_VALUE_PLACEHOLDER = "请补充"
DOCUMENT_TYPE_LEGAL_QUERY_TERMS = {
"procurement_goods": ["买卖合同", "货物采购", "交付", "风险转移", "验收", "质量", "发票", "付款", "质保", "违约", "民法典", "procurement", "breach"],
"technical_service": ["技术服务合同", "知识产权", "验收", "违约", "民法典"],
"software_development": ["软件开发合同", "知识产权", "交付", "验收", "民法典"],
"service_general": ["服务合同", "通用服务", "服务范围", "交付成果", "服务级别", "费用结算", "验收整改", "保密", "个人信息", "数据处理", "违约", "解除通知", "电子签署", "民法典"],
"labor_contract": ["劳动合同", "试用期", "社会保险", "竞业限制", "劳动合同法"],
"nda_commercial": ["保密协议", "商业秘密", "反不正当竞争法", "保密措施", "返还销毁", "违约责任", "电子签署", "民法典"],
"lease_house": ["房屋租赁合同", "租金", "押金", "解除", "民法典"],
"equity_transfer": ["股权转让", "公司法", "价款", "交割"],
"demand_letter": ["催告函", "履约催告", "违约", "送达", "证据留存", "民法典"],
"rescission_notice": ["解除合同", "解除通知", "终止", "违约", "送达", "民法典"],
"contract_termination_notice": ["解除合同", "解除通知", "终止", "违约", "送达", "民法典"],
"agency_distribution_agreement": ["代理", "经销", "渠道", "返利", "区域", "合规"],
"franchise_agreement": ["加盟", "特许经营", "品牌授权", "两店一年", "信息披露", "备案", "商标许可", "消费者", "培训", "督导"],
"intercompany_loan_agreement": ["企业间借款", "借款合同", "借款协议", "关联交易", "利息", "担保", "还款", "资金用途", "公司法", "民间借贷"],
"investment_agreement": ["投资协议", "投融资", "估值", "交割", "治理权利", "退出", "陈述保证", "公司法", "反垄断", "外商投资"],
"valuation_adjustment_agreement": ["对赌协议", "估值调整", "业绩承诺", "补偿机制", "回购", "公司法", "投融资", "九民纪要"],
"shareholder_resolution": ["股东会决议", "股东大会决议", "公司法", "公司章程", "表决权", "召集通知", "工商变更", "登记", "备案"],
"power_of_attorney": ["授权委托书", "委托代理", "授权范围", "转委托", "签章", "电子签名", "诉讼代理", "登记备案"],
"legal_opinion": ["法律意见书", "律师事务所", "尽职调查", "核查验证", "法律分析", "结论意见", "前提假设", "使用限制"],
"property_service": ["物业服务合同", "物业管理", "物业费", "专项维修资金", "公共收益", "业主委员会", "共用部位", "安全协助"],
"property_service_agreement": ["物业服务合同", "物业管理", "物业费", "专项维修资金", "公共收益", "业主委员会", "共用部位", "安全协助"],
}
TEMPLATE_DOCUMENT_TYPE_CODE_ALIASES = {
"property_service": ["property_service", "property_service_agreement"],
"property_service_agreement": ["property_service", "property_service_agreement"],
"rescission_notice": ["rescission_notice", "contract_termination_notice"],
"contract_termination_notice": ["rescission_notice", "contract_termination_notice"],
"termination_notice": ["rescission_notice", "contract_termination_notice"],
}
GENERATION_PROMPT_KEY_ALIASES = {
"agency_distribution_agreement": ["agency_distribution"],
"franchise_agreement": ["franchise"],
"labor_service_agreement": ["labor_service"],
"sales_goods": ["sales"],
}
CONTRACT_LIFECYCLE_STATUSES = {
"generated",
"internal_review",
@@ -112,7 +75,7 @@ CONTRACT_LIFECYCLE_TASK_PRIORITIES = {
class ContractGenerationService:
def __init__(self, db: Session, review_service: Optional[ReviewService] = None):
def __init__(self, db: Session, review_service: Optional[ReviewService] = None, shared_ai_platform_client: Optional[SharedAIPlatformClient] = None):
self.db = db
self.repository = ContractTemplateRepository(db)
self.generated_contract_repository = GeneratedContractRepository(db)
@@ -123,6 +86,7 @@ class ContractGenerationService:
self.element_schema_service = LegalDocumentElementSchemaService(db)
self.prompt_runtime_service = PromptRuntimeService(db)
self.review_service = review_service or ReviewService(db)
self.shared_ai_platform_client = shared_ai_platform_client
def list_available_templates(
self,
@@ -138,15 +102,12 @@ class ContractGenerationService:
if document_type_code is not None:
normalized_document_type_code = document_type_code.strip().lower().replace("-", "_")
document_type_candidates = [None]
document_type_candidates_to_try = [None]
if normalized_document_type_code is not None:
document_type_candidates = TEMPLATE_DOCUMENT_TYPE_CODE_ALIASES.get(
normalized_document_type_code,
[normalized_document_type_code],
)
document_type_candidates_to_try = document_type_candidates(normalized_document_type_code)
templates_by_id: Dict[str, Any] = {}
for candidate in document_type_candidates:
for candidate in document_type_candidates_to_try:
matched_templates = self.repository.list_enabled(
tenant_id=tenant_id,
contract_type=contract_type,
@@ -224,27 +185,7 @@ class ContractGenerationService:
self._validate_variable_values(template_variables, variable_values)
# 替换模板内容
generated_content = self._replace_variables(template.content, variable_values)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# 根据导出格式生成文件
file_content, file_name = self._export_contract(
generated_content,
template.name,
timestamp,
export_format,
options
)
file_item = self.file_service.create_file_record(
current_user=current_user,
file_name=file_name,
content=file_content,
file_type="contract",
mime_type=self._get_mime_type(export_format),
require_admin=False,
)
template_rendered_content = self._replace_variables(template.content, variable_values)
resolved_document_type_code = document_type_code or template.document_type_code
prompt_runtime = self.prompt_runtime_service.resolve_published_prompt(
@@ -257,8 +198,101 @@ class ContractGenerationService:
fallback_prompt_version="contract_generation.template_render.v1",
)
prompt_runtime_trace = self.prompt_runtime_service.to_trace(prompt_runtime)
generated_content = template_rendered_content
generation_mode = "template_render"
enhancement_trace = {
"ai_enhancement_applied": False,
"fallback_used": False,
"fallback_reason": None,
"provider": None,
"model": None,
"prompt_key": prompt_runtime_trace.get("prompt_key"),
"prompt_version": prompt_runtime_trace.get("prompt_version"),
}
if options.get("ai_enhanced_generation"):
generation_mode = "ai_enhanced_generation"
try:
enhancement_result = self._generate_contract_with_ai_enhancement(
current_user=current_user,
prompt_runtime=prompt_runtime,
template=template,
variable_values=variable_values,
draft_content=template_rendered_content,
document_type_code=resolved_document_type_code,
elements_snapshot=elements_snapshot,
missing_fields=missing_fields,
related_legal_articles=related_legal_articles,
legal_source_query=legal_source_query,
)
generated_content = enhancement_result["content"]
enhancement_trace = {
"ai_enhancement_applied": True,
"fallback_used": False,
"fallback_reason": None,
"provider": enhancement_result.get("provider"),
"model": enhancement_result.get("model"),
"prompt_key": enhancement_result.get("prompt_key") or prompt_runtime_trace.get("prompt_key"),
"prompt_version": enhancement_result.get("prompt_version") or prompt_runtime_trace.get("prompt_version"),
}
except Exception as exc:
generated_content = template_rendered_content
generation_mode = "template_render"
enhancement_trace = {
"ai_enhancement_applied": False,
"fallback_used": True,
"fallback_reason": str(exc)[:300],
"provider": "shared-ai-platform",
"model": prompt_runtime_trace.get("model"),
"prompt_key": prompt_runtime_trace.get("prompt_key"),
"prompt_version": prompt_runtime_trace.get("prompt_version"),
}
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
file_content, file_name = self._export_contract(
generated_content,
template.name,
timestamp,
export_format,
options
)
file_item = self.file_service.create_file_record(
current_user=current_user,
file_name=file_name,
content=file_content,
file_type="contract",
mime_type=self._get_mime_type(export_format),
require_admin=False,
)
generation_source_payload = {
**(generation_source or {}),
"generation_mode": generation_mode,
"template_rendered": True,
"ai_enhancement_applied": enhancement_trace["ai_enhancement_applied"],
"fallback_used": enhancement_trace["fallback_used"],
"fallback_reason": enhancement_trace["fallback_reason"],
"ai_provider": enhancement_trace["provider"],
"ai_model": enhancement_trace["model"],
"prompt_key": enhancement_trace["prompt_key"],
"prompt_version": enhancement_trace["prompt_version"],
}
generation_quality = self._build_generation_quality_summary(
generated_content=generated_content,
template_variables=template_variables,
missing_fields=missing_fields,
related_legal_articles=related_legal_articles,
generation_mode=generation_mode,
)
if enhancement_trace["fallback_used"]:
generation_quality.setdefault("warning_issues", [])
generation_quality["warning_issues"].append("AI 增强生成未生效,已回退到模板渲染")
persisted_options = {
**options,
"_generation_mode": generation_mode,
"_generation_quality": generation_quality,
"_template_rendered": True,
"_ai_enhanced": enhancement_trace["ai_enhancement_applied"],
"_template_category": template.template_category,
"_industry_category": template.industry_category,
"_document_type_code": resolved_document_type_code,
@@ -266,7 +300,9 @@ class ContractGenerationService:
"_prompt_runtime": {
"contract_generation": prompt_runtime_trace,
},
"_generation_source": generation_source_payload,
}
if elements_snapshot is not None:
persisted_options["_elements_snapshot"] = elements_snapshot
if element_schema_id is not None:
@@ -280,7 +316,10 @@ class ContractGenerationService:
if ai_hints is not None:
persisted_options["_ai_hints"] = ai_hints
if generation_source is not None:
persisted_options["_generation_source"] = generation_source
persisted_options["_generation_source"] = {
**generation_source_payload,
**generation_source,
}
if legal_source_query is not None:
persisted_options["_legal_source_query"] = legal_source_query
@@ -321,6 +360,11 @@ class ContractGenerationService:
"contract_type": template.contract_type,
"document_type_code": resolved_document_type_code,
"export_format": export_format.value,
"generation_mode": generation_mode,
"template_rendered": True,
"ai_enhanced": generation_mode == "ai_enhanced_generation",
"generation_quality_status": generation_quality.get("status"),
"generation_quality_score": generation_quality.get("score"),
},
)
auto_review_summary = self._create_auto_review_summary(current_user, created_contract, generated_content)
@@ -328,6 +372,83 @@ class ContractGenerationService:
return self._to_generated_contract_item(created_contract)
def _generate_contract_with_ai_enhancement(
self,
*,
current_user: Dict[str, Any],
prompt_runtime: Dict[str, Any],
template: Any,
variable_values: Dict[str, str],
draft_content: str,
document_type_code: Optional[str],
elements_snapshot: Optional[Dict[str, Any]],
missing_fields: Optional[List[Dict[str, Any]]],
related_legal_articles: Optional[List[Dict[str, Any]]],
legal_source_query: Optional[str],
) -> Dict[str, Any]:
client = self._resolve_shared_ai_platform_client()
prompt = self._build_ai_enhanced_generation_prompt(
prompt_runtime=prompt_runtime,
template=template,
variable_values=variable_values,
draft_content=draft_content,
document_type_code=document_type_code,
elements_snapshot=elements_snapshot,
missing_fields=missing_fields,
related_legal_articles=related_legal_articles,
legal_source_query=legal_source_query,
)
return client.enhance_contract_generation(
current_user,
prompt_runtime=prompt_runtime,
prompt=prompt,
draft_content=draft_content,
metadata={
"contract_type": getattr(template, "contract_type", None),
"document_type_code": document_type_code,
"variable_values": variable_values,
"elements_snapshot": elements_snapshot,
"missing_fields": missing_fields or [],
"related_legal_articles": related_legal_articles or [],
"legal_source_query": legal_source_query,
},
)
def _build_ai_enhanced_generation_prompt(
self,
*,
prompt_runtime: Dict[str, Any],
template: Any,
variable_values: Dict[str, str],
draft_content: str,
document_type_code: Optional[str],
elements_snapshot: Optional[Dict[str, Any]],
missing_fields: Optional[List[Dict[str, Any]]],
related_legal_articles: Optional[List[Dict[str, Any]]],
legal_source_query: Optional[str],
) -> str:
prompt_parts = [
str(prompt_runtime.get("system_prompt") or "你是合同生成增强助手,请在不编造业务事实的前提下润色和补强合同正文。"),
str(prompt_runtime.get("prompt_content") or "请基于草稿生成更完整、更专业的合同文本,保持法律措辞稳健。"),
f"模板名称:{getattr(template, 'name', '')}",
f"合同类型:{getattr(template, 'contract_type', '')}",
f"文书类型编码:{document_type_code or ''}",
f"变量值:{json.dumps(variable_values, ensure_ascii=False)}",
f"结构化要素:{json.dumps(elements_snapshot or {}, ensure_ascii=False)}",
f"缺失字段:{json.dumps(missing_fields or [], ensure_ascii=False)}",
f"法条来源:{json.dumps(related_legal_articles or [], ensure_ascii=False)}",
f"法条检索提示:{legal_source_query or ''}",
"要求:保留已知业务事实,不要编造未提供的金额、日期、主体信息;可优化结构、补强法律措辞、减少占位内容。",
f"合同草稿:\n{draft_content}",
]
return "\n\n".join(part for part in prompt_parts if part)
def _resolve_shared_ai_platform_client(self) -> SharedAIPlatformClient:
if self.shared_ai_platform_client is not None:
return self.shared_ai_platform_client
return build_shared_ai_platform_client(self.db)
def _build_generation_prompt_preferred_keys(
self,
document_type_code: Optional[str],
@@ -355,8 +476,7 @@ class ContractGenerationService:
normalized_value = cls._normalize_generation_prompt_key_part(value)
if not normalized_value:
return []
aliases = GENERATION_PROMPT_KEY_ALIASES.get(normalized_value, [])
return [*aliases, normalized_value]
return generation_prompt_key_candidates(value)
@staticmethod
def _normalize_generation_prompt_key_part(value: Optional[str]) -> str:
@@ -414,6 +534,7 @@ class ContractGenerationService:
)
generation_source = {
"mode": "structured_elements",
"generation_mode": "template_render",
"template_selection": "explicit" if template_id else "auto",
"document_type_code": resolved_document_type_code,
"template_id": template.id,
@@ -943,6 +1064,7 @@ class ContractGenerationService:
"related_legal_article_count": len((generated_contract.export_options_json or {}).get("_related_legal_articles") or []),
"export_format": generated_contract.export_format,
"generated_by": generated_contract.generated_by,
"generation_quality": (generated_contract.export_options_json or {}).get("_generation_quality"),
}
try:
@@ -973,6 +1095,82 @@ class ContractGenerationService:
except Exception as exc:
return self._build_auto_review_failure_summary(generated_contract, str(exc))
def _build_generation_quality_summary(
self,
*,
generated_content: str,
template_variables: List[Dict[str, Any]],
missing_fields: Optional[List[Dict[str, Any]]],
related_legal_articles: Optional[List[Dict[str, Any]]],
generation_mode: str,
) -> Dict[str, Any]:
placeholder_count = generated_content.count(MISSING_VALUE_PLACEHOLDER)
unresolved_variable_count = len(re.findall(r"\{\{\s*[^{}]+\s*\}\}", generated_content))
missing_field_count = len(missing_fields or [])
required_variable_count = sum(1 for item in template_variables if item.get("required", True))
legal_source_count = len(related_legal_articles or [])
section_coverage = self._build_generated_contract_section_coverage(generated_content)
blocking_issues: List[str] = []
warning_issues: List[str] = []
if unresolved_variable_count:
blocking_issues.append("存在未替换模板变量")
if missing_field_count:
warning_issues.append("存在缺失要素占位")
if placeholder_count:
warning_issues.append("正文包含请补充占位")
if not section_coverage["has_payment"]:
warning_issues.append("缺少付款/价款安排")
if not section_coverage["has_breach"]:
warning_issues.append("缺少违约责任安排")
if not section_coverage["has_dispute_resolution"]:
warning_issues.append("缺少争议解决条款")
if legal_source_count == 0:
warning_issues.append("缺少法条/知识来源辅助")
score = 100
score -= unresolved_variable_count * 20
score -= missing_field_count * 6
score -= placeholder_count * 4
score -= sum(1 for value in section_coverage.values() if not value) * 5
if legal_source_count == 0:
score -= 8
score = max(0, min(100, score))
if blocking_issues:
status_value = "blocked"
elif warning_issues:
status_value = "needs_review"
else:
status_value = "ready_for_review"
return {
"schema_version": "generation_quality.v1",
"status": status_value,
"score": score,
"generation_mode": generation_mode,
"required_variable_count": required_variable_count,
"missing_field_count": missing_field_count,
"placeholder_count": placeholder_count,
"unresolved_variable_count": unresolved_variable_count,
"legal_source_count": legal_source_count,
"section_coverage": section_coverage,
"blocking_issues": blocking_issues,
"warning_issues": warning_issues,
}
@staticmethod
def _build_generated_contract_section_coverage(content: str) -> Dict[str, bool]:
return {
"has_parties": bool(re.search(r"甲方|乙方|委托方|受托方|采购方|供应方|出租方|承租方", content)),
"has_subject_matter": bool(re.search(r"服务范围|合同标的|采购标的|项目范围|合作事项|授权范围|租赁物", content)),
"has_payment": bool(re.search(r"付款|价款|费用|服务费|租金|发票|结算|金额", content)),
"has_acceptance_or_delivery": bool(re.search(r"验收|交付|交货|交割|履行|整改", content)),
"has_breach": bool(re.search(r"违约|赔偿|责任承担|违约金", content)),
"has_dispute_resolution": bool(re.search(r"争议解决|仲裁|管辖|法院|诉讼", content)),
"has_signature": bool(re.search(r"签署日期|签约日期|签字|签章|盖章", content)),
}
def _build_auto_review_summary(
self,
generated_contract: GeneratedContract,
@@ -1052,6 +1250,7 @@ class ContractGenerationService:
"report_file_name": report_file_name,
"chat_url": chat_url,
"prompt_runtime": prompt_runtime,
"generation_quality": (generated_contract.export_options_json or {}).get("_generation_quality"),
}
def _build_auto_review_failure_summary(self, generated_contract: GeneratedContract, failure_message: str) -> Dict[str, Any]:
@@ -1194,6 +1393,152 @@ class ContractGenerationService:
return "text/html; charset=utf-8"
return "text/plain; charset=utf-8"
def _build_clause_completion_suggestions(
self,
generation_quality: Optional[Dict[str, Any]],
auto_review: Optional[Dict[str, Any]],
) -> List[Dict[str, Any]]:
suggestions = self._build_review_risk_clause_suggestions(auto_review)
suggestions.extend(self._build_quality_clause_suggestions(generation_quality))
return self._dedupe_clause_completion_suggestions(suggestions)[:6]
def _build_review_risk_clause_suggestions(self, auto_review: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
if not isinstance(auto_review, dict):
return []
if str(auto_review.get("status") or "").lower() != "completed":
return []
risk_texts = self._extract_auto_review_risk_texts(auto_review)
overall_risk = str(auto_review.get("risk_level") or "").strip().lower()
suggestions: List[Dict[str, Any]] = []
seen_codes: set[str] = set()
for rule in RISK_CLAUSE_SUGGESTION_RULES:
if not any(keyword.lower() in text for keyword in rule.get("match_keywords", []) for text in risk_texts):
continue
suggestion = {
"code": str(rule["code"]),
"title": str(rule["title"]),
"priority": str(rule["priority"]),
"recommendation": str(rule["recommendation"]),
"source": "risk_review",
"reason": str(rule.get("reason") or "自动审查发现相关高风险事项。"),
"risk_level": overall_risk or "high",
}
if suggestion["code"] not in seen_codes:
suggestions.append(suggestion)
seen_codes.add(suggestion["code"])
if overall_risk == "high" and not suggestions:
suggestions.append(
{
"code": "high_risk_manual_clause_review",
"title": "高风险条款人工补强",
"priority": "high",
"recommendation": "结合自动审查识别出的高风险问题,逐条补充责任边界、付款条件、解除机制和争议解决安排。",
"source": "risk_review",
"reason": "自动审查结论为高风险,建议优先补强核心条款。",
"risk_level": overall_risk,
}
)
return suggestions
def _extract_auto_review_risk_texts(self, auto_review: Optional[Dict[str, Any]]) -> List[str]:
if not isinstance(auto_review, dict):
return []
texts = [
str(auto_review.get("review_opinion") or ""),
str(auto_review.get("recommendation") or ""),
str(auto_review.get("status_detail") or ""),
str(auto_review.get("failure_reason") or ""),
str(auto_review.get("next_action") or ""),
]
risk_items = auto_review.get("risk_items")
if isinstance(risk_items, list):
for item in risk_items:
if not isinstance(item, dict):
continue
texts.extend(
[
str(item.get("title") or ""),
str(item.get("description") or ""),
str(item.get("suggestion") or ""),
str(item.get("risk_summary") or ""),
]
)
return [text.lower() for text in texts if text]
def _build_quality_clause_suggestions(self, generation_quality: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
suggestions: List[Dict[str, Any]] = []
quality = generation_quality or {}
coverage = quality.get("section_coverage") or {}
warning_issues = list(quality.get("warning_issues") or [])
missing_sections = []
if not coverage.get("has_payment", True):
missing_sections.append(("payment", "付款与结算条款", "补充付款节点、发票条件、逾期责任和付款前置条件。"))
if not coverage.get("has_acceptance_or_delivery", True):
missing_sections.append(("acceptance_delivery", "交付与验收条款", "补充交付成果、验收标准、验收期限和异议处理流程。"))
if not coverage.get("has_breach", True):
missing_sections.append(("breach", "违约责任条款", "补充违约触发条件、违约金/赔偿计算口径、责任上限及例外。"))
if not coverage.get("has_dispute_resolution", True):
missing_sections.append(("dispute_resolution", "争议解决条款", "补充管辖法院或仲裁机构、适用法律和争议处理流程。"))
for code, title, recommendation in missing_sections:
suggestions.append(
{
"code": code,
"title": title,
"priority": "high",
"recommendation": recommendation,
"source": "quality",
"reason": "生成质量检查发现合同结构缺失关键条款。",
"risk_level": None,
}
)
if quality.get("placeholder_count"):
suggestions.append(
{
"code": "placeholder_cleanup",
"title": "清理占位内容",
"priority": "high",
"recommendation": "将正文中的“请补充”替换为明确的业务事实或约定,避免对外发送半成品。",
"source": "quality",
"reason": "生成正文仍包含占位内容。",
"risk_level": None,
}
)
if warning_issues and not suggestions:
suggestions.append(
{
"code": "manual_quality_review",
"title": "人工质量复核",
"priority": "medium",
"recommendation": "结合生成质量提示逐项复核合同完整性,并确认审查报告中的风险项是否已处理。",
"source": "quality",
"reason": "生成质量检查发现警告项,需要人工复核。",
"risk_level": None,
}
)
return suggestions
def _dedupe_clause_completion_suggestions(self, suggestions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
priority_order = {"high": 0, "medium": 1, "low": 2}
deduped: Dict[str, Dict[str, Any]] = {}
for item in suggestions:
code = str(item.get("code") or "").strip()
if not code:
continue
existing = deduped.get(code)
if existing is None:
deduped[code] = item
continue
existing_rank = priority_order.get(str(existing.get("priority") or "low"), 99)
current_rank = priority_order.get(str(item.get("priority") or "low"), 99)
if current_rank < existing_rank:
deduped[code] = item
return sorted(deduped.values(), key=lambda item: (priority_order.get(str(item.get("priority") or "low"), 99), str(item.get("title") or "")))
def _to_generated_contract_item(self, generated_contract: GeneratedContract) -> Dict:
auto_review = self._resolve_auto_review_summary(generated_contract)
export_options = generated_contract.export_options_json or {}
@@ -1209,6 +1554,8 @@ class ContractGenerationService:
ai_hints = export_options.get("_ai_hints") or []
generation_source = export_options.get("_generation_source")
legal_source_query = export_options.get("_legal_source_query")
generation_quality = export_options.get("_generation_quality") or {}
clause_suggestions = self._build_clause_completion_suggestions(generation_quality, auto_review)
prompt_runtime = export_options.get("_prompt_runtime") or {}
generated_html = self._build_generated_contract_html(
generated_contract.generated_content,
@@ -1239,6 +1586,8 @@ class ContractGenerationService:
"file_content_base64": generated_contract.file_content_base64,
"preview_url": f"/api/v1/contracts/{generated_contract.id}/preview",
"download_url": f"/api/v1/contracts/{generated_contract.id}/download",
"generation_quality": generation_quality,
"clause_completion_suggestions": clause_suggestions,
"auto_review": auto_review,
"lifecycle": self._build_contract_lifecycle(generated_contract),
"metadata": {
@@ -1260,6 +1609,8 @@ class ContractGenerationService:
"related_legal_article_count": len(related_legal_articles),
"generation_source": generation_source,
"legal_source_query": legal_source_query,
"generation_quality": generation_quality,
"clause_completion_suggestions": clause_suggestions,
"prompt_runtime": prompt_runtime,
"export_options": {
key: value
@@ -1287,6 +1638,8 @@ class ContractGenerationService:
"file_size": item["file_size"],
"preview_url": item["preview_url"],
"download_url": item["download_url"],
"generation_quality": item.get("generation_quality"),
"clause_completion_suggestions": item.get("clause_completion_suggestions"),
"auto_review": item["auto_review"],
"lifecycle": item["lifecycle"],
"missing_field_count": item["metadata"].get("missing_field_count", 0),
@@ -1300,6 +1653,8 @@ class ContractGenerationService:
"element_schema_id": item["metadata"].get("element_schema_id"),
"element_schema_version_label": item["metadata"].get("element_schema_version_label"),
"generation_source": item["metadata"].get("generation_source"),
"generation_quality": item["metadata"].get("generation_quality"),
"clause_completion_suggestions": item["metadata"].get("clause_completion_suggestions"),
"prompt_runtime": item["metadata"].get("prompt_runtime"),
"missing_field_count": item["metadata"].get("missing_field_count", 0),
"related_legal_article_count": item["metadata"].get("related_legal_article_count", 0),
@@ -1869,7 +2224,7 @@ class ContractGenerationService:
template_name: str,
) -> str:
terms: List[str] = []
for term in DOCUMENT_TYPE_LEGAL_QUERY_TERMS.get(document_type_code, []):
for term in legal_query_terms(document_type_code):
cls._append_unique_text(terms, term)
cls._append_unique_text(terms, template_name)
flattened = cls._flatten_values(elements)
@@ -0,0 +1,193 @@
from __future__ import annotations
from typing import Optional
DOCUMENT_TYPE_LEGAL_QUERY_TERMS = {
"procurement_goods": ["买卖合同", "货物采购", "交付", "风险转移", "验收", "质量", "发票", "付款", "质保", "违约", "民法典", "procurement", "breach"],
"technical_service": ["技术服务合同", "知识产权", "验收", "违约", "民法典"],
"software_development": ["软件开发合同", "知识产权", "交付", "验收", "民法典"],
"service_general": ["服务合同", "通用服务", "服务范围", "交付成果", "服务级别", "费用结算", "验收整改", "保密", "个人信息", "数据处理", "违约", "解除通知", "电子签署", "民法典"],
"labor_contract": ["劳动合同", "试用期", "社会保险", "竞业限制", "劳动合同法"],
"nda_commercial": ["保密协议", "商业秘密", "反不正当竞争法", "保密措施", "返还销毁", "违约责任", "电子签署", "民法典"],
"lease_house": ["房屋租赁合同", "租金", "押金", "解除", "民法典"],
"equity_transfer": ["股权转让", "公司法", "价款", "交割"],
"demand_letter": ["催告函", "履约催告", "违约", "送达", "证据留存", "民法典"],
"rescission_notice": ["解除合同", "解除通知", "终止", "违约", "送达", "民法典"],
"contract_termination_notice": ["解除合同", "解除通知", "终止", "违约", "送达", "民法典"],
"agency_distribution_agreement": ["代理", "经销", "渠道", "返利", "区域", "合规"],
"franchise_agreement": ["加盟", "特许经营", "品牌授权", "两店一年", "信息披露", "备案", "商标许可", "消费者", "培训", "督导"],
"intercompany_loan_agreement": ["企业间借款", "借款合同", "借款协议", "关联交易", "利息", "担保", "还款", "资金用途", "公司法", "民间借贷"],
"investment_agreement": ["投资协议", "投融资", "估值", "交割", "治理权利", "退出", "陈述保证", "公司法", "反垄断", "外商投资"],
"valuation_adjustment_agreement": ["对赌协议", "估值调整", "业绩承诺", "补偿机制", "回购", "公司法", "投融资", "九民纪要"],
"shareholder_resolution": ["股东会决议", "股东大会决议", "公司法", "公司章程", "表决权", "召集通知", "工商变更", "登记", "备案"],
"power_of_attorney": ["授权委托书", "委托代理", "授权范围", "转委托", "签章", "电子签名", "诉讼代理", "登记备案"],
"legal_opinion": ["法律意见书", "律师事务所", "尽职调查", "核查验证", "法律分析", "结论意见", "前提假设", "使用限制"],
"property_service": ["物业服务合同", "物业管理", "物业费", "专项维修资金", "公共收益", "业主委员会", "共用部位", "安全协助"],
"property_service_agreement": ["物业服务合同", "物业管理", "物业费", "专项维修资金", "公共收益", "业主委员会", "共用部位", "安全协助"],
}
TEMPLATE_DOCUMENT_TYPE_CODE_ALIASES = {
"property_service": ["property_service", "property_service_agreement"],
"property_service_agreement": ["property_service", "property_service_agreement"],
"rescission_notice": ["rescission_notice", "contract_termination_notice"],
"contract_termination_notice": ["rescission_notice", "contract_termination_notice"],
"termination_notice": ["rescission_notice", "contract_termination_notice"],
}
GENERATION_PROMPT_KEY_ALIASES = {
"agency_distribution_agreement": ["agency_distribution"],
"franchise_agreement": ["franchise"],
"labor_service_agreement": ["labor_service"],
"sales_goods": ["sales"],
}
REVIEW_PROMPT_DOMAIN_KEY_BY_CONTRACT_TYPE = {
"investment": "contract_review_investment_finance",
"investment_agreement": "contract_review_investment_finance",
"equity_transfer": "contract_review_investment_finance",
"equity_transfer_agreement": "contract_review_investment_finance",
"intercompany_loan": "contract_review_investment_finance",
"intercompany_loan_agreement": "contract_review_investment_finance",
"valuation_adjustment": "contract_review_investment_finance",
"valuation_adjustment_agreement": "contract_review_investment_finance",
"shareholder_resolution": "contract_review_corporate_governance",
"power_of_attorney": "contract_review_corporate_governance",
"legal_opinion": "contract_review_corporate_governance",
"labor": "contract_review_employment_hr",
"employment": "contract_review_employment_hr",
"labor_contract": "contract_review_employment_hr",
"labor_service": "contract_review_employment_hr",
"labor_service_agreement": "contract_review_employment_hr",
"lease": "contract_review_real_estate_property",
"lease_house": "contract_review_real_estate_property",
"property_service": "contract_review_real_estate_property",
"property_service_agreement": "contract_review_real_estate_property",
"cooperation_framework": "contract_review_channel_franchise",
"agency_distribution": "contract_review_channel_franchise",
"agency_distribution_agreement": "contract_review_channel_franchise",
"franchise": "contract_review_channel_franchise",
"franchise_agreement": "contract_review_channel_franchise",
"cooperation": "contract_review_channel_franchise",
"framework": "contract_review_channel_franchise",
"sales": "contract_review_channel_franchise",
"technical_service": "contract_review_tech_saas_data_compliance",
"software_development": "contract_review_tech_saas_data_compliance",
"technology_development": "contract_review_tech_saas_data_compliance",
"software_saas": "contract_review_tech_saas_data_compliance",
"nda": "contract_review_tech_saas_data_compliance",
"nda_commercial": "contract_review_tech_saas_data_compliance",
"license": "contract_review_tech_saas_data_compliance",
}
BUSINESS_AUXILIARY_PROMPT_KEYS = {
"obligation_extraction": "obligation_extraction_default",
"filing_materials_checklist": "filing_materials_checklist_default",
"negotiation_strategy": "negotiation_strategy_default",
"legal_opinion_boundary_fallback": "legal_opinion_boundary_fallback_default",
}
RISK_CLAUSE_SUGGESTION_RULES = [
{
"code": "renewal_notice_guardrail",
"title": "续约与退出机制条款",
"priority": "high",
"recommendation": "补充自动续约通知期、明确不续约通知方式,并增加到期退出和交接安排。",
"reason": "自动续约或续期风险需要通过通知期限和退出机制控制。",
"match_keywords": ["自动续约", "续约", "续期", "evergreen", "renewal"],
},
{
"code": "liability_carve_out",
"title": "责任限制例外条款",
"priority": "high",
"recommendation": "补充责任上限的 carve-out,至少覆盖故意、重大过失、保密违约和知识产权侵权。",
"reason": "免责或责任限制过宽时,应明确无上限或例外场景。",
"match_keywords": ["免责", "责任限制", "赔偿上限", "liability", "carve-out", "重大过失"],
},
{
"code": "payment_acceptance_alignment",
"title": "付款与验收联动条款",
"priority": "high",
"recommendation": "补充付款节点、验收标准、发票条件和逾期责任,避免先付款后验收的失衡安排。",
"reason": "付款条件不清或与验收脱节,容易形成履约和回款争议。",
"match_keywords": ["付款", "价款", "发票", "验收", "结算", "payment", "invoice"],
},
{
"code": "delivery_acceptance_detail",
"title": "交付成果与验收标准条款",
"priority": "high",
"recommendation": "补充交付物清单、交付时间、验收标准、异议期和整改闭环。",
"reason": "交付或验收义务不清,会直接影响履约责任认定。",
"match_keywords": ["交付", "交货", "验收", "成果", "deliver", "acceptance"],
},
{
"code": "confidentiality_scope",
"title": "保密范围与例外条款",
"priority": "medium",
"recommendation": "补充保密信息范围、保密期限、法定披露例外、返还销毁义务及违约责任。",
"reason": "保密条款过于笼统时,执行和举证都容易失焦。",
"match_keywords": ["保密", "商业秘密", "confidential", "nda"],
},
{
"code": "ip_ownership_and_license",
"title": "知识产权归属与许可条款",
"priority": "high",
"recommendation": "明确交付成果、源代码、文档等知识产权归属,以及许可范围、使用限制和侵权责任。",
"reason": "知识产权归属不清时,后续使用、交付和维权都会受影响。",
"match_keywords": ["知识产权", "源代码", "软件著作权", "专利", "商标", "ip", "copyright"],
},
{
"code": "termination_trigger_and_cure",
"title": "解除触发与补救期条款",
"priority": "medium",
"recommendation": "补充解除条件、补救期限、通知送达方式和解除后的结算/返还安排。",
"reason": "解除或终止机制不明确,会放大违约处理争议。",
"match_keywords": ["解除", "终止", "termination", "terminate", "补救期"],
},
{
"code": "dispute_resolution_forum",
"title": "争议解决与适用法律条款",
"priority": "medium",
"recommendation": "明确适用法律、管辖法院或仲裁机构,并约定证据保留和送达方式。",
"reason": "争议解决路径不明会增加后续维权和执行成本。",
"match_keywords": ["争议解决", "仲裁", "管辖", "法院", "jurisdiction", "arbitration"],
},
]
TEMPLATE_CATEGORY_LABELS = {
"general-business": "通用商务",
"employment-hr": "人事用工",
"procurement-supply": "采购供应链",
"sales-commercial": "销售与商业合作",
"commercial-cooperation": "商业合作",
"service-delivery": "服务交付",
"commercial-compliance": "合规与保密",
"investment-finance": "投融资与金融",
"corporate-governance": "公司治理",
"real-estate": "不动产与物业",
"notices-letters": "通知函件",
}
def normalize_domain_key(value: Optional[str]) -> str:
return str(value or "").strip().lower().replace("-", "_")
def document_type_candidates(document_type_code: Optional[str]) -> list[str]:
normalized = normalize_domain_key(document_type_code)
if not normalized:
return []
return TEMPLATE_DOCUMENT_TYPE_CODE_ALIASES.get(normalized, [normalized])
def generation_prompt_key_candidates(value: Optional[str]) -> list[str]:
normalized = normalize_domain_key(value)
if not normalized:
return []
return [*GENERATION_PROMPT_KEY_ALIASES.get(normalized, []), normalized]
def review_prompt_domain_key(contract_type: Optional[str]) -> Optional[str]:
return REVIEW_PROMPT_DOMAIN_KEY_BY_CONTRACT_TYPE.get(normalize_domain_key(contract_type))
def legal_query_terms(document_type_code: Optional[str]) -> list[str]:
return DOCUMENT_TYPE_LEGAL_QUERY_TERMS.get(normalize_domain_key(document_type_code), [])
+1 -1
View File
@@ -37,7 +37,7 @@ class PromptAssetService:
"clause_recommendation": {"suite_key": "phase13_clause_generation", "sample_count": 6},
"wording_rewrite": {"suite_key": "phase15_wording_rewrite", "sample_count": 3},
"regulation_impact": {"suite_key": "future_regulation_impact", "sample_count": 0},
"business_auxiliary": {"suite_key": "future_business_auxiliary", "sample_count": 0},
"business_auxiliary": {"suite_key": "phase15_business_auxiliary", "sample_count": 8},
}
def __init__(self, db: Session):
@@ -48,9 +48,9 @@ class PromptGoldenSampleEvaluator:
"min_samples": 0,
},
"business_auxiliary": {
"suite_key": "future_business_auxiliary",
"sample_family": "future_business_auxiliary",
"min_samples": 0,
"suite_key": "phase15_business_auxiliary",
"sample_family": "phase15_business_auxiliary",
"min_samples": 8,
},
}
@@ -62,7 +62,9 @@ class PromptGoldenSampleEvaluator:
evaluation_mode: Optional[str] = None,
include_raw_model_response: Optional[bool] = None,
):
self.repo_root = Path(repo_root) if repo_root is not None else Path(__file__).resolve().parents[3]
resolved_root = Path(repo_root) if repo_root is not None else Path(__file__).resolve().parents[3]
manifest_path = resolved_root / "project-docs" / "phase15-prompt-golden-samples" / "prompt-golden-suite-manifest.json"
self.repo_root = resolved_root if manifest_path.exists() else Path(__file__).resolve().parents[3]
self.llm_client = llm_client
self.evaluation_mode = self._normalize_mode(evaluation_mode or settings.prompt_golden_eval_mode)
self.include_raw_model_response = (
@@ -205,7 +207,7 @@ class PromptGoldenSampleEvaluator:
if prompt_type == "wording_rewrite":
return self._wording_rewrite_samples()
if prompt_type == "business_auxiliary":
return []
return self._business_auxiliary_samples()
return []
def _evaluate_sample_with_llm(
@@ -517,6 +519,31 @@ class PromptGoldenSampleEvaluator:
)
return results
def _business_auxiliary_samples(self) -> list[dict[str, Any]]:
path = self.repo_root / "project-docs" / "phase15-prompt-golden-samples" / "business-auxiliary-golden-samples.json"
payload = self._read_json_file(path)
samples = payload.get("samples") if isinstance(payload, Mapping) else []
results: list[dict[str, Any]] = []
for item in samples if isinstance(samples, list) else []:
if not isinstance(item, Mapping):
continue
input_payload = item.get("input") if isinstance(item.get("input"), Mapping) else {}
expected_payload = item.get("expected") if isinstance(item.get("expected"), Mapping) else {}
results.append(
{
"sample_id": item.get("sample_id"),
"prompt_type": "business_auxiliary",
"sample_family": "phase15_business_auxiliary",
"intent": item.get("intent"),
"input": input_payload,
"expected": expected_payload,
"expected_citation_required": False,
"input_summary": str(input_payload.get("message") or "")[:200],
"expected_summary": json.dumps(expected_payload, ensure_ascii=False)[:300],
}
)
return results
def _read_json_file(self, path: Path) -> Any:
if not path.exists() or not path.is_file():
return {}
+44 -67
View File
@@ -33,6 +33,7 @@ from app.models.review_task import ReviewTask
from app.schemas.ai_contract import build_ai_run_trace, build_parsed_document, build_review_workflow_state
from app.repositories.legal_article_repository import LegalArticleRepository
from app.repositories.review_result_repository import ReviewResultRepository
from app.services.legal_domain_registry import review_prompt_domain_key
from app.services.file_service import FileService
from app.services.legal_article_service import LegalArticleService
from app.services.legal_review_execution_gateway import build_legal_review_execution_gateway
@@ -218,6 +219,7 @@ STRUCTURE_MISSING_SECTION_RULES = [
{"type": "acceptance", "message": "验收标准或验收流程"},
{"type": "confidentiality", "message": "保密范围与期限"},
{"type": "breach", "message": "违约责任安排"},
{"type": "termination", "message": "解除或终止条款"},
{"type": "dispute_resolution", "message": "争议解决条款"},
]
PARTY_NAME_PATTERNS = {
@@ -261,32 +263,9 @@ PAYMENT_STAGE_GO_LIVE = "go_live"
PAYMENT_STAGE_FINAL = "final"
PAYMENT_STAGE_WARRANTY = "warranty_retention"
PAYMENT_STAGE_MILESTONE = "milestone"
PAYMENT_LABEL_HINTS = [
("棰勪粯娆?", PAYMENT_STAGE_ADVANCE),
("棣栨湡娆?", PAYMENT_STAGE_ADVANCE),
("瀹氶噾", PAYMENT_STAGE_ADVANCE),
("杩涘害娆?", PAYMENT_STAGE_MILESTONE),
("瀹炴柦娆?", PAYMENT_STAGE_MILESTONE),
("灏炬", PAYMENT_STAGE_FINAL),
("璐ㄤ繚閲?", PAYMENT_STAGE_WARRANTY),
]
LIABILITY_CARVE_OUT_MAP = [
("鏁呮剰", "intentional_misconduct"),
("閲嶅ぇ杩囧け", "gross_negligence"),
("淇濆瘑", "confidentiality"),
("鍟嗕笟绉樺瘑", "confidentiality"),
("鐭ヨ瘑浜ф潈", "intellectual_property"),
("钁椾綔鏉?", "intellectual_property"),
("婧愪唬鐮?", "intellectual_property"),
("杞欢钁椾綔鏉?", "intellectual_property"),
("杩濈害", "breach"),
]
IP_OWNERSHIP_PATTERNS = [
(re.compile(r"(鐭ヨ瘑浜ф潈|钁椾綔鏉?|婧愪唬鐮?|杞欢钁椾綔鏉?)[^\n銆傦紱;]*(褰掔敳鏂规墍鏈夊畨褰掑睘浜庣敳鏂圅鐢叉柟鎵€鏈?)", re.IGNORECASE), "party_a"),
(re.compile(r"(鐭ヨ瘑浜ф潈|钁椾綔鏉?|婧愪唬鐮?|杞欢钁椾綔鏉?)[^\n銆傦紱;]*(褰掍箼鏂规墍鏈夊畨褰掑睘浜庝箼鏂圅涔欐柟鎵€鏈?)", re.IGNORECASE), "party_b"),
(re.compile(r"(鐭ヨ瘑浜ф潈|钁椾綔鏉?|婧愪唬鐮?|杞欢钁椾綔鏉?)[^\n銆傦紱;]*(鍏卞悓鎵€鏈夊弻鏂瑰叡鏈夊弻鏂瑰叡鍚屼韩鏈?)", re.IGNORECASE), "shared"),
(re.compile(r"(鐭ヨ瘑浜ф潈|钁椾綔鏉?|婧愪唬鐮?|杞欢钁椾綔鏉?)[^\n銆傦紱;]*(璁稿彲鐢叉柟浣跨敤鎺堟潈鐢叉柟浣跨敤)", re.IGNORECASE), "licensed_to_party_a"),
]
PAYMENT_LABEL_HINTS = []
LIABILITY_CARVE_OUT_MAP = []
IP_OWNERSHIP_PATTERNS = []
_REAL_CHINESE_CONTRACT_HINTS = {
"service": ["\u670d\u52a1\u5408\u540c", "\u670d\u52a1\u534f\u8bae", "\u6280\u672f\u670d\u52a1", "\u8fd0\u7ef4\u670d\u52a1", "\u54a8\u8be2\u670d\u52a1"],
@@ -314,44 +293,6 @@ for _contract_type, _hints in _REAL_CHINESE_CONTRACT_HINTS.items():
if _hint not in existing:
existing.append(_hint)
REVIEW_PROMPT_DOMAIN_KEY_BY_CONTRACT_TYPE = {
"investment": "contract_review_investment_finance",
"investment_agreement": "contract_review_investment_finance",
"equity_transfer": "contract_review_investment_finance",
"equity_transfer_agreement": "contract_review_investment_finance",
"intercompany_loan": "contract_review_investment_finance",
"intercompany_loan_agreement": "contract_review_investment_finance",
"valuation_adjustment": "contract_review_investment_finance",
"valuation_adjustment_agreement": "contract_review_investment_finance",
"shareholder_resolution": "contract_review_corporate_governance",
"power_of_attorney": "contract_review_corporate_governance",
"legal_opinion": "contract_review_corporate_governance",
"labor": "contract_review_employment_hr",
"employment": "contract_review_employment_hr",
"labor_contract": "contract_review_employment_hr",
"labor_service": "contract_review_employment_hr",
"labor_service_agreement": "contract_review_employment_hr",
"lease": "contract_review_real_estate_property",
"lease_house": "contract_review_real_estate_property",
"property_service": "contract_review_real_estate_property",
"property_service_agreement": "contract_review_real_estate_property",
"cooperation_framework": "contract_review_channel_franchise",
"agency_distribution": "contract_review_channel_franchise",
"agency_distribution_agreement": "contract_review_channel_franchise",
"franchise": "contract_review_channel_franchise",
"franchise_agreement": "contract_review_channel_franchise",
"cooperation": "contract_review_channel_franchise",
"framework": "contract_review_channel_franchise",
"sales": "contract_review_channel_franchise",
"technical_service": "contract_review_tech_saas_data_compliance",
"software_development": "contract_review_tech_saas_data_compliance",
"technology_development": "contract_review_tech_saas_data_compliance",
"software_saas": "contract_review_tech_saas_data_compliance",
"nda": "contract_review_tech_saas_data_compliance",
"nda_commercial": "contract_review_tech_saas_data_compliance",
"license": "contract_review_tech_saas_data_compliance",
}
_REAL_CHINESE_CLAUSE_KEYWORDS = {
"payment": ["\u4ed8\u6b3e", "\u4ef7\u6b3e", "\u91d1\u989d", "\u53d1\u7968", "\u8d39\u7528", "\u5c3e\u6b3e", "\u8d28\u4fdd\u91d1", "\u9884\u4ed8\u6b3e"],
"acceptance": ["\u9a8c\u6536", "\u9a8c\u6536\u6807\u51c6", "\u4e66\u9762\u9a8c\u6536", "\u6d4b\u8bd5", "\u8bd5\u8fd0\u884c"],
@@ -1020,6 +961,8 @@ class ReviewService:
"relationship_count": len(contract_structure.get("clause_relationships") or []),
"high_confidence_relationships": sum(1 for item in contract_structure.get("clause_relationships") or [] if item.get("confidence_tier") == RELATION_CONFIDENCE_HIGH),
"weak_hint_relationships": sum(1 for item in contract_structure.get("clause_relationships") or [] if item.get("confidence_tier") == RELATION_CONFIDENCE_WEAK),
"quality_score": self._build_review_quality_score(contract_structure, risk_items),
"quality_warnings": self._build_review_quality_warnings(contract_structure, risk_items),
}
enriched_basic_info = {
"contract_type": base_basic_info.get("contract_type") or review.contract_type or "未分类合同",
@@ -1041,6 +984,36 @@ class ReviewService:
"rendered_report": result.get("rendered_report"),
}
def _build_review_quality_score(self, contract_structure: Dict[str, Any], risk_items: List[Dict[str, Any]]) -> int:
missing_sections = contract_structure.get("missing_sections") or []
clause_count = int(contract_structure.get("clause_count") or 0)
evidence_count = sum(1 for item in risk_items if item.get("evidence", {}).get("quote"))
relationship_count = len(contract_structure.get("clause_relationships") or [])
score = 100
score -= min(len(missing_sections), 8) * 7
if clause_count < 5:
score -= 12
if risk_items and evidence_count < len(risk_items):
score -= min(len(risk_items) - evidence_count, 5) * 5
if not relationship_count:
score -= 6
return max(0, min(100, score))
def _build_review_quality_warnings(self, contract_structure: Dict[str, Any], risk_items: List[Dict[str, Any]]) -> List[str]:
warnings: List[str] = []
missing_sections = contract_structure.get("missing_sections") or []
if missing_sections:
warnings.append(f"缺少关键条款:{'、'.join(str(item) for item in missing_sections[:5])}")
if int(contract_structure.get("clause_count") or 0) < 5:
warnings.append("识别到的条款数量偏少,建议补充完整合同正文后复核")
if risk_items:
evidence_count = sum(1 for item in risk_items if item.get("evidence", {}).get("quote"))
if evidence_count < len(risk_items):
warnings.append("部分风险项缺少原文证据定位")
if not contract_structure.get("clause_relationships"):
warnings.append("关键条款之间的付款、验收、责任联动关系不足")
return warnings
def _merge_contract_structure(
self,
derived_contract_structure: Dict[str, Any],
@@ -1436,10 +1409,14 @@ class ReviewService:
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"):
if contract_type in {"development", "service", "license", "technical_service", "software_development", "software_saas", "nda"} and not clause_type_counts.get("intellectual_property"):
missing.append("知识产权归属或许可安排")
if contract_type in {"development", "service", "license"} and clause_type_counts.get("intellectual_property") and not intellectual_property_owner:
if contract_type in {"development", "service", "license", "technical_service", "software_development", "software_saas", "nda"} and clause_type_counts.get("intellectual_property") and not intellectual_property_owner:
missing.append("知识产权归属口径")
if contract_type in {"property_service", "property_service_agreement", "lease", "lease_house"} and not clause_type_counts.get("liability"):
missing.append("安全协助、维修或责任边界")
if contract_type in {"investment", "investment_agreement", "equity_transfer", "intercompany_loan", "valuation_adjustment"} and not clause_type_counts.get("liability"):
missing.append("陈述保证、违约或责任承担安排")
return missing
def _extract_payment_milestones(self, clauses: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@@ -2215,7 +2192,7 @@ class ReviewService:
def _build_review_prompt_preferred_keys(self, contract_type: Optional[str]) -> List[str]:
normalized_contract_type = self._normalize_review_prompt_contract_type(contract_type)
preferred_keys: List[str] = []
domain_key = REVIEW_PROMPT_DOMAIN_KEY_BY_CONTRACT_TYPE.get(normalized_contract_type)
domain_key = review_prompt_domain_key(normalized_contract_type)
if domain_key:
preferred_keys.append(domain_key)
if normalized_contract_type:
+163 -2
View File
@@ -31,6 +31,8 @@ class StubTenantService:
class StubContractGenerationService:
def __init__(self):
self.last_generate_options = None
def _auto_review_summary(self, review_id="review-auto-123", status="completed"):
return {
"review_id": review_id,
@@ -463,6 +465,7 @@ class StubContractGenerationService:
element_schema_version_label=None,
fill_missing_variables=False,
):
self.last_generate_options = options
if template_id == "tpl-missing":
raise HTTPException(status_code=404, detail="template not found")
if "甲方名称" in variable_values:
@@ -475,6 +478,16 @@ class StubContractGenerationService:
contract_type = "procurement"
document_type_code = document_type_code or "procurement_goods"
template_name = "Procurement Template"
metadata_generation_source = {
"generation_mode": "ai_enhanced_generation" if options.get("ai_enhanced_generation") else "template_render",
"ai_enhancement_applied": bool(options.get("ai_enhanced_generation") and not options.get("simulate_ai_fallback")),
"fallback_used": bool(options.get("simulate_ai_fallback")),
"fallback_reason": "simulated shared ai failure" if options.get("simulate_ai_fallback") else None,
"ai_provider": "shared-ai-platform" if options.get("ai_enhanced_generation") else None,
"ai_model": "mock-model" if options.get("ai_enhanced_generation") else None,
"prompt_key": "contract_generation_default" if options.get("ai_enhanced_generation") else None,
"prompt_version": "contract_generation_default.v1.0" if options.get("ai_enhanced_generation") else None,
}
return {
"id": "contract-123",
"template_id": template_id,
@@ -521,6 +534,7 @@ class StubContractGenerationService:
"variables_used": len(variable_values),
"missing_field_count": 0,
"related_legal_article_count": 0,
"generation_source": metadata_generation_source,
},
}
@@ -991,6 +1005,101 @@ class ContractGenerationServiceTestCase(unittest.TestCase):
self.assertIn("contract_generation_default", preferred_keys)
self.assertEqual(len(preferred_keys), len(set(preferred_keys)))
def test_ai_enhanced_generation_uses_shared_ai_content(self):
now = datetime.now(timezone.utc)
template = self.ContractTemplate(
id="tpl-ai-success",
tenant_id="tenant-demo-001",
name="AI Template",
contract_type="procurement",
description="AI enhanced generation",
content="Contract with {{supplier_name}}",
variables_json=[{"name": "supplier_name", "label": "Supplier", "required": True}],
enabled=True,
created_by="seed",
updated_by="seed",
created_at=now,
updated_at=now,
)
self.db.add(template)
self.db.commit()
class FakeSharedAIClient:
def __init__(self):
self.calls = []
def enhance_contract_generation(self, current_user, *, prompt_runtime, prompt, draft_content, metadata=None):
self.calls.append({
"current_user": current_user,
"prompt_runtime": prompt_runtime,
"prompt": prompt,
"draft_content": draft_content,
"metadata": metadata,
})
return {
"content": "AI enhanced contract for Supplier A",
"provider": "shared-ai-platform",
"model": "test-model",
"prompt_key": "contract_generation_default",
"prompt_version": "contract_generation_default.v1.0",
}
fake_client = FakeSharedAIClient()
self.service.shared_ai_platform_client = fake_client
result = self.service.generate_contract(
current_user=self.current_user,
template_id="tpl-ai-success",
variable_values={"supplier_name": "Supplier A"},
options={"ai_enhanced_generation": True},
)
generation_source = result["metadata"]["generation_source"]
self.assertEqual(result["generated_content"], "AI enhanced contract for Supplier A")
self.assertEqual(len(fake_client.calls), 1)
self.assertTrue(generation_source["ai_enhancement_applied"])
self.assertFalse(generation_source["fallback_used"])
self.assertEqual(generation_source["ai_provider"], "shared-ai-platform")
def test_ai_enhanced_generation_falls_back_to_template_content(self):
now = datetime.now(timezone.utc)
template = self.ContractTemplate(
id="tpl-ai-fallback",
tenant_id="tenant-demo-001",
name="AI Fallback Template",
contract_type="procurement",
description="AI enhanced generation fallback",
content="Contract with {{supplier_name}}",
variables_json=[{"name": "supplier_name", "label": "Supplier", "required": True}],
enabled=True,
created_by="seed",
updated_by="seed",
created_at=now,
updated_at=now,
)
self.db.add(template)
self.db.commit()
class FailingSharedAIClient:
def enhance_contract_generation(self, current_user, *, prompt_runtime, prompt, draft_content, metadata=None):
raise RuntimeError("shared ai unavailable")
self.service.shared_ai_platform_client = FailingSharedAIClient()
result = self.service.generate_contract(
current_user=self.current_user,
template_id="tpl-ai-fallback",
variable_values={"supplier_name": "Supplier A"},
options={"ai_enhanced_generation": True},
)
generation_source = result["metadata"]["generation_source"]
self.assertEqual(result["generated_content"], "Contract with Supplier A")
self.assertFalse(generation_source["ai_enhancement_applied"])
self.assertTrue(generation_source["fallback_used"])
self.assertIn("shared ai unavailable", generation_source["fallback_reason"])
self.assertIn("AI 增强生成未生效", result["generation_quality"]["warning_issues"][-1])
def _seed_law_doc_004_data(self):
now = datetime.now(timezone.utc)
self.db.add(
@@ -1852,5 +1961,57 @@ class ContractGenerationServiceTestCase(unittest.TestCase):
self.assertEqual(ctx.exception.status_code, 400)
if __name__ == "__main__":
unittest.main()
class ClauseSuggestionRuleTests(unittest.TestCase):
def setUp(self):
from app.services.contract_generation_service import ContractGenerationService
self.service = ContractGenerationService(db=None)
def test_build_clause_suggestions_prefers_review_risk_rules(self):
generation_quality = {
"section_coverage": {
"has_payment": True,
"has_acceptance_or_delivery": True,
"has_breach": True,
"has_dispute_resolution": True,
},
"warning_issues": [],
"placeholder_count": 0,
}
auto_review = {
"status": "completed",
"risk_level": "high",
"review_opinion": "合同存在自动续约和免责条款风险。",
"recommendation": "建议补充续约通知期,并限制免责范围。",
"risk_items": [
{"title": "自动续约风险", "description": "自动续约条款对我方不利", "suggestion": "补充退出机制"},
{"title": "免责条款风险", "description": "免责范围过宽", "suggestion": "增加 carve-out"},
],
}
suggestions = self.service._build_clause_completion_suggestions(generation_quality, auto_review)
self.assertTrue(any(item["code"] == "renewal_notice_guardrail" for item in suggestions))
self.assertTrue(any(item["code"] == "liability_carve_out" for item in suggestions))
self.assertTrue(all(item.get("source") == "risk_review" for item in suggestions[:2]))
def test_build_clause_suggestions_falls_back_to_quality_rules(self):
generation_quality = {
"section_coverage": {
"has_payment": False,
"has_acceptance_or_delivery": True,
"has_breach": False,
"has_dispute_resolution": True,
},
"warning_issues": ["缺少付款安排"],
"placeholder_count": 1,
}
auto_review = {"status": "failed", "risk_level": None}
suggestions = self.service._build_clause_completion_suggestions(generation_quality, auto_review)
codes = {item["code"] for item in suggestions}
self.assertIn("payment", codes)
self.assertIn("breach", codes)
self.assertIn("placeholder_cleanup", codes)
self.assertTrue(all(item.get("source") == "quality" for item in suggestions))
+5 -4
View File
@@ -174,6 +174,7 @@ class PromptAssetServiceTestCase(unittest.TestCase):
self.service.run_link_repository = StubPromptAssetRunLinkRepository()
self.service.evaluation_run_repository = StubPromptAssetEvaluationRunRepository()
self.service.quality_eval_service = StubQualityEvalService()
self.service.golden_sample_evaluator = PromptGoldenSampleEvaluator(repo_root=Path.cwd().parent, evaluation_mode="local")
self.admin_user = {"id": "user-admin", "tenant_id": "tenant-demo-001", "role": "admin", "display_name": "Admin"}
self.member_user = {"id": "user-member", "tenant_id": "tenant-demo-001", "role": "member"}
@@ -298,7 +299,7 @@ class PromptAssetServiceTestCase(unittest.TestCase):
def test_run_evaluation_invokes_llm_once_per_golden_sample(self):
fake_llm_client = FakeGoldenSampleLLMClient()
self.service.golden_sample_evaluator = PromptGoldenSampleEvaluator(
repo_root=Path.cwd(),
repo_root=Path.cwd().parent,
llm_client=fake_llm_client,
evaluation_mode="llm",
)
@@ -318,7 +319,7 @@ class PromptAssetServiceTestCase(unittest.TestCase):
self.assertGreaterEqual(evaluation["evaluation_run"]["sample_count"], 3)
self.assertEqual(len(fake_llm_client.calls), evaluation["evaluation_run"]["sample_count"])
def test_business_auxiliary_prompt_evaluation_uses_future_suite(self):
def test_business_auxiliary_prompt_evaluation_uses_phase15_suite(self):
created = self._create_asset(
evaluation=False,
prompt_key="obligation_extraction_default",
@@ -330,8 +331,8 @@ class PromptAssetServiceTestCase(unittest.TestCase):
evaluation = self.service.evaluate_current_prompt_asset(self.admin_user, created["id"])
self.assertEqual(evaluation["version"]["evaluation_status"], "passed")
self.assertEqual(evaluation["evaluation_report"]["suite_key"], "future_business_auxiliary")
self.assertEqual(evaluation["evaluation_report"]["sample_count"], 0)
self.assertEqual(evaluation["evaluation_report"]["suite_key"], "phase15_business_auxiliary")
self.assertEqual(evaluation["evaluation_report"]["sample_count"], 8)
self.assertEqual(evaluation["evaluation_report"]["evaluation_mode"], "local_fallback")
def test_publish_with_report_and_create_next_version(self):
+164 -2
View File
@@ -2,7 +2,7 @@
import Link from "next/link";
import { useMemo, useEffect, useState } from "react";
import { Button, Card, Descriptions, Empty, Form, Input, Modal, Select, Space, Table, Tag, Typography, Upload, message } from "antd";
import { Alert, Button, Card, Descriptions, Empty, Form, Input, Modal, Select, Space, Table, Tag, Typography, Upload, message } from "antd";
import type { UploadFile } from "antd/es/upload/interface";
import { useRouter } from "next/navigation";
@@ -49,6 +49,49 @@ type LifecycleTaskFormValues = {
evidence_note?: string;
};
function getGenerationQualityColor(value?: string | null) {
if (value === "ready_for_review") return "green";
if (value === "blocked") return "red";
if (value === "needs_review") return "gold";
return "default";
}
function formatGenerationQualityStatus(value?: string | null) {
if (value === "ready_for_review") return "质量良好";
if (value === "blocked") return "需先修复";
if (value === "needs_review") return "需复核";
return "未评分";
}
function getGenerationQualityWarnings(detail?: GeneratedContract | null) {
const quality = detail?.generation_quality;
return [
...(quality?.blocking_issues ?? []),
...(quality?.warning_issues ?? []),
...(detail?.clause_completion_suggestions ?? []).map((item) => `${item.title}:${item.recommendation}`),
];
}
function groupClauseSuggestions(detail?: GeneratedContract | null) {
const items = detail?.clause_completion_suggestions ?? [];
return {
riskReview: items.filter((item) => item.source === "risk_review"),
quality: items.filter((item) => item.source !== "risk_review"),
};
}
function getGenerationModeMeta(detail?: GeneratedContract | null) {
const source = detail?.metadata.generation_source;
if (source?.fallback_used) {
return { label: "AI回退", color: "volcano", warning: source.fallback_reason ?? "AI 增强生成失败,已回退模板生成。" };
}
if (source?.ai_enhancement_applied || source?.generation_mode === "ai_enhanced_generation") {
return { label: "AI增强", color: "purple", warning: null };
}
return { label: "模板生成", color: "default", warning: null };
}
function formatDateTime(value: string | null | undefined) {
if (!value) {
return "-";
@@ -296,6 +339,15 @@ function buildContractReadabilityItems(
{ label: "下一步动作", value: autoReview?.next_action ?? autoReview?.status_detail ?? "可进入详情继续审查或问答复核" },
];
}
function shouldWarnBeforeDownload(detail?: GeneratedContract | null) {
const quality = detail?.generation_quality;
return Boolean(
quality?.status === "blocked" ||
quality?.status === "needs_review" ||
(typeof quality?.score === "number" && quality.score < 80) ||
getGenerationQualityWarnings(detail).length > 0,
);
}
function buildContractChatHref(
detail: GeneratedContract | null,
@@ -381,7 +433,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
};
void loadDetail();
}, [messageApi, params]);
}, [params]);
const variableRows = useMemo(() => {
if (!detail) {
@@ -413,12 +465,43 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
() => buildContractReadabilityItems(detail, autoReview),
[detail, autoReview],
);
const generationModeMeta = useMemo(() => getGenerationModeMeta(detail), [detail]);
const groupedClauseSuggestions = useMemo(() => groupClauseSuggestions(detail), [detail]);
const handleDownload = async () => {
if (!detail) {
return;
}
if (shouldWarnBeforeDownload(detail)) {
const warnings = getGenerationQualityWarnings(detail);
Modal.confirm({
title: "当前合同质量需复核,确认下载?",
content: (
<Space orientation="vertical" size="small">
<Text>质量状态:{formatGenerationQualityStatus(detail.generation_quality?.status)},分数:{detail.generation_quality?.score ?? "-"}</Text>
{warnings.slice(0, 5).map((item) => (
<Text type="secondary" key={item}>• {item}</Text>
))}
</Space>
),
okText: "仍然下载",
cancelText: "先去复核",
onOk: async () => {
setDownloading(true);
try {
await downloadContract(detail.id, detail.file_name);
messageApi.success("文件下载开始");
} catch {
messageApi.error("文件下载失败,请稍后重试");
} finally {
setDownloading(false);
}
},
});
return;
}
setDownloading(true);
try {
await downloadContract(detail.id, detail.file_name);
@@ -787,6 +870,85 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
</Space>
</Card>
{detail?.generation_quality ? (
<Card className="review-surface-card" title="生成质量与补条款建议">
<Space orientation="vertical" size="middle" style={{ width: "100%" }}>
<Space wrap>
<Tag color={getGenerationQualityColor(detail.generation_quality.status)}>
{formatGenerationQualityStatus(detail.generation_quality.status)}
</Tag>
<Tag color="blue">质量分 {detail.generation_quality.score ?? "-"}</Tag>
<Tag color={generationModeMeta.color}>{generationModeMeta.label}</Tag>
<Tag>模式 {detail.generation_quality.generation_mode ?? "template_render"}</Tag>
<Tag>法条来源 {detail.generation_quality.legal_source_count ?? 0}</Tag>
</Space>
{generationModeMeta.warning ? (
<Alert
showIcon
type="warning"
message="AI 增强生成已回退"
description={generationModeMeta.warning}
/>
) : null}
{getGenerationQualityWarnings(detail).length ? (
<Alert
showIcon
type={detail.generation_quality.status === "blocked" ? "error" : "warning"}
message="下载或对外发送前建议复核"
description={
<Space orientation="vertical" size={4}>
{getGenerationQualityWarnings(detail).slice(0, 8).map((item) => (
<span key={item}>{item}</span>
))}
</Space>
}
/>
) : null}
{groupedClauseSuggestions.riskReview.length ? (
<Space orientation="vertical" size="small" style={{ width: "100%" }}>
<Text strong>高风险补条款建议</Text>
<div className="chat-review-grid">
{groupedClauseSuggestions.riskReview.map((item) => (
<section className="chat-review-section chat-review-section-soft" key={item.code}>
<div className="chat-review-section-header">
<Space wrap>
<span className="landing-card-eyebrow">{item.priority}</span>
<Tag color="red">自动审查</Tag>
{item.risk_level ? <Tag color="volcano">{item.risk_level}</Tag> : null}
</Space>
<strong>{item.title}</strong>
</div>
<p>{item.recommendation}</p>
{item.reason ? <Text type="secondary">{item.reason}</Text> : null}
</section>
))}
</div>
</Space>
) : null}
{groupedClauseSuggestions.quality.length ? (
<Space orientation="vertical" size="small" style={{ width: "100%" }}>
<Text strong>质量修复建议</Text>
<div className="chat-review-grid">
{groupedClauseSuggestions.quality.map((item) => (
<section className="chat-review-section chat-review-section-soft" key={item.code}>
<div className="chat-review-section-header">
<Space wrap>
<span className="landing-card-eyebrow">{item.priority}</span>
<Tag color="gold">质量检查</Tag>
</Space>
<strong>{item.title}</strong>
</div>
<p>{item.recommendation}</p>
{item.reason ? <Text type="secondary">{item.reason}</Text> : null}
</section>
))}
</div>
</Space>
) : null}
</Space>
</Card>
) : null}
<Card className="review-surface-card" title="生命周期与履约跟踪">
{detail ? (
<Descriptions column={2} bordered>
+66 -3
View File
@@ -146,6 +146,44 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage:
});
}
function hasRiskReviewSuggestions(record?: Pick<GeneratedContract, "clause_completion_suggestions"> | Pick<GeneratedContractListItem, "clause_completion_suggestions"> | null) {
return Boolean(record?.clause_completion_suggestions?.some((item) => item.source === "risk_review"));
}
function getGenerationModeMeta(record?: Pick<GeneratedContract, "metadata"> | Pick<GeneratedContractListItem, "metadata"> | null) {
const source = record?.metadata?.generation_source;
const fallbackUsed = Boolean(source?.fallback_used);
const aiApplied = Boolean(source?.ai_enhancement_applied);
if (fallbackUsed) {
return { label: "AI回退", color: "volcano" };
}
if (aiApplied || source?.generation_mode === "ai_enhanced_generation") {
return { label: "AI增强", color: "purple" };
}
return { label: "模板生成", color: "default" };
}
function getGenerationQualityColor(value?: string | null) {
if (value === "ready_for_review") return "green";
if (value === "blocked") return "red";
if (value === "needs_review") return "gold";
return "default";
}
function formatGenerationQualityStatus(value?: string | null) {
if (value === "ready_for_review") return "质量良好";
if (value === "blocked") return "需先修复";
if (value === "needs_review") return "需复核";
return "未评分";
}
function getAutoReviewRiskTagColor(value: string | null | undefined) {
if (value === "high") return "red";
if (value === "medium") return "gold";
if (value === "low") return "blue";
return "default";
}
function getRequiredVariableValidationMessage(template: ContractTemplate, variableValues: Record<string, string> | undefined) {
const missing = template.variables
.filter((variable) => variable.required)
@@ -856,6 +894,24 @@ export default function ContractsPage() {
width: 180,
render: (value: string) => new Date(value).toLocaleString("zh-CN"),
},
{
title: "生成质量",
dataIndex: "generation_quality",
key: "generation_quality",
width: 180,
render: (_, record) => {
const modeMeta = getGenerationModeMeta(record);
return (
<Space orientation="vertical" size={4}>
<Tag color={getGenerationQualityColor(record.generation_quality?.status)}>{formatGenerationQualityStatus(record.generation_quality?.status)}</Tag>
<Text type="secondary" style={{ fontSize: 12 }}>分数 {record.generation_quality?.score ?? "-"}</Text>
<Tag color={modeMeta.color}>{modeMeta.label}</Tag>
{hasRiskReviewSuggestions(record) ? <Tag color="red">高风险补条款</Tag> : null}
{record.clause_completion_suggestions?.length ? <Tag color="orange">需补条款 {record.clause_completion_suggestions.length}</Tag> : null}
</Space>
);
},
},
{
title: "自动审查",
dataIndex: "auto_review",
@@ -866,13 +922,20 @@ export default function ContractsPage() {
return <Text type="secondary">未触发</Text>;
}
const highRisk = autoReview.risk_level === "high";
return (
<Space orientation="vertical" size={4}>
<Tag color={getAutoReviewStatusColor(autoReview.status)}>{formatAutoReviewStatus(autoReview.status)}</Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
<Space wrap>
<Tag color={getAutoReviewStatusColor(autoReview.status)}>{formatAutoReviewStatus(autoReview.status)}</Tag>
{autoReview.risk_level ? <Tag color={getAutoReviewRiskTagColor(autoReview.risk_level)}>{autoReview.risk_level === "high" ? "高风险" : autoReview.risk_level === "medium" ? "中风险" : autoReview.risk_level === "low" ? "低风险" : autoReview.risk_level}</Tag> : null}
</Space>
<Text type={highRisk ? undefined : "secondary"} strong={highRisk} style={{ fontSize: 12, color: highRisk ? "#cf1322" : undefined }}>
{autoReview.current_stage_label ?? autoReview.next_action ?? "-"}
</Text>
<Button size="small" disabled={!canOpenReviewDetail(autoReview)} onClick={() => handleOpenAutoReviewDetail(autoReview)}>
{highRisk && autoReview.review_opinion ? (
<Text style={{ fontSize: 12, color: "#cf1322" }}>{autoReview.review_opinion}</Text>
) : null}
<Button size="small" danger={highRisk} disabled={!canOpenReviewDetail(autoReview)} onClick={() => handleOpenAutoReviewDetail(autoReview)}>
<span className="ui-nowrap-label">审查详情</span>
</Button>
</Space>
+26 -8
View File
@@ -109,9 +109,17 @@ function getEvaluationTag(value?: string | null) {
return <Tag color="default">未评测</Tag>;
}
function getEvaluationModeText(report?: Record<string, unknown> | null) {
const mode = typeof report?.evaluation_mode === "string" ? report.evaluation_mode : null;
if (mode === "llm") return "LLM 逐条评测";
if (mode === "local_fallback") return "本地静态兜底";
if (mode === "llm_required_not_configured") return "要求 LLM 但未配置";
return "未知评测模式";
}
function getEvaluationModeTag(report?: Record<string, unknown> | null) {
const mode = typeof report?.evaluation_mode === "string" ? report.evaluation_mode : null;
if (mode === "llm") return <Tag color="purple">逐条大模型</Tag>;
if (mode === "llm") return <Tag color="purple">LLM 评测</Tag>;
if (mode === "local_fallback") return <Tag color="blue">本地兜底</Tag>;
if (mode === "llm_required_not_configured") return <Tag color="red">模型未配置</Tag>;
return <Tag>未知</Tag>;
@@ -317,8 +325,9 @@ export default function PromptAssetsPage() {
return;
}
const statusText = result.data.version.evaluation_status === "passed" ? "通过" : "未通过";
const modeText = result.data.evaluation_report.evaluation_mode === "llm" ? "逐条大模型" : "本地兜底";
messageApi.success(`Prompt 评测已完成:${statusText},${modeText}`);
const modeText = getEvaluationModeText(result.data.evaluation_report);
const sampleText = `${result.data.evaluation_run.passed_samples}/${result.data.evaluation_run.sample_count} 条样本`;
messageApi.success(`Prompt 评测已完成:${statusText},${modeText},${sampleText}`);
await refreshAfterMutation(item.id);
} catch (error) {
messageApi.error(error instanceof Error ? error.message : "Prompt 评测失败");
@@ -455,10 +464,13 @@ export default function PromptAssetsPage() {
key: "evaluation",
width: 170,
render: (_, item) => (
<Space orientation="vertical" size={0}>
{getEvaluationTag(item.latest_evaluation_status)}
<Text type="secondary">{item.latest_evaluation_score == null ? "-" : `分数 ${item.latest_evaluation_score}`}</Text>
</Space>
<Space orientation="vertical" size={0}>
{getEvaluationTag(item.latest_evaluation_status)}
{getEvaluationModeTag(item.latest_evaluation_report)}
<Text type="secondary">
{item.latest_evaluation_score == null ? "-" : `分数 ${item.latest_evaluation_score}`} · 样本 {getEvaluationSampleResultCount(item.latest_evaluation_report)}
</Text>
</Space>
),
},
{
@@ -661,6 +673,12 @@ export default function PromptAssetsPage() {
<Form.Item label="模型配置 JSON" name="model_config_json">
<Input.TextArea rows={3} placeholder='{"model":"gpt-5.4","temperature":0.2}' />
</Form.Item>
<Alert
showIcon
type="info"
title="发布前评测说明"
description="LLM 评测会逐条调用共享 AI 对黄金样本打分;本地兜底仅做静态检查和样本装载校验。发布前请重点确认评测模式、样本数和失败项。"
/>
<Form.Item label="评测状态" name="evaluation_status">
<Select options={evaluationStatusOptions} />
</Form.Item>
@@ -741,7 +759,7 @@ export default function PromptAssetsPage() {
>
<Space orientation="vertical" size="middle" style={{ width: "100%" }}>
<Text type="secondary">
当前状态:{getStatusTag(selectedDetail.asset.status)} 最新评测:{getEvaluationTag(selectedDetail.asset.latest_evaluation_status)}
当前状态:{getStatusTag(selectedDetail.asset.status)} 最新评测:{getEvaluationTag(selectedDetail.asset.latest_evaluation_status)} {getEvaluationModeTag(selectedDetail.asset.latest_evaluation_report)} 样本 {getEvaluationSampleResultCount(selectedDetail.asset.latest_evaluation_report)} 条
</Text>
<Table rowKey="id" size="small" pagination={false} columns={versionColumns} dataSource={selectedDetail.versions} />
<Space orientation="vertical" size="small" style={{ width: "100%" }}>
+7
View File
@@ -3,8 +3,13 @@ export const templateCategoryOptions = [
{ value: "employment-hr", label: "人事用工" },
{ value: "procurement-supply", label: "采购供应链" },
{ value: "sales-commercial", label: "销售与商业合作" },
{ value: "commercial-cooperation", label: "商业合作" },
{ value: "service-delivery", label: "服务交付" },
{ value: "commercial-compliance", label: "合规与保密" },
{ value: "investment-finance", label: "投融资与金融" },
{ value: "corporate-governance", label: "公司治理" },
{ value: "real-estate", label: "不动产与物业" },
{ value: "notices-letters", label: "通知函件" },
] as const;
export const industryCategoryOptions = [
@@ -14,6 +19,8 @@ export const industryCategoryOptions = [
{ value: "retail-trade", label: "零售贸易" },
{ value: "professional-services", label: "专业服务" },
{ value: "human-resources", label: "人力资源" },
{ value: "real-estate", label: "房地产与物业" },
{ value: "finance-investment", label: "金融与投融资" },
] as const;
export function formatTemplateCategory(value?: string | null) {
+44 -1
View File
@@ -21,6 +21,34 @@ export interface ContractVariable {
description: string;
}
export interface GenerationQualitySummary {
schema_version?: string;
status?: "ready_for_review" | "needs_review" | "blocked" | string;
score?: number;
generation_mode?: string;
required_variable_count?: number;
missing_field_count?: number;
placeholder_count?: number;
unresolved_variable_count?: number;
legal_source_count?: number;
section_coverage?: Record<string, boolean>;
blocking_issues?: string[];
warning_issues?: string[];
}
export interface GenerationSourceSummary {
generation_mode?: string | null;
template_rendered?: boolean;
ai_enhancement_applied?: boolean;
fallback_used?: boolean;
fallback_reason?: string | null;
ai_provider?: string | null;
ai_model?: string | null;
prompt_key?: string | null;
prompt_version?: string | null;
}
export interface AutoReviewSummary {
review_id?: string | null;
review_title?: string | null;
@@ -46,6 +74,16 @@ export interface AutoReviewSummary {
chat_url?: string | null;
}
export interface ClauseCompletionSuggestion {
code: string;
title: string;
priority: "low" | "medium" | "high" | string;
recommendation: string;
source?: "quality" | "risk_review" | string;
reason?: string | null;
risk_level?: string | null;
}
export interface GeneratedContract {
id: string;
template_id: string;
@@ -69,6 +107,8 @@ export interface GeneratedContract {
file_content_base64: string;
preview_url: string;
download_url: string;
generation_quality?: GenerationQualitySummary | null;
clause_completion_suggestions?: ClauseCompletionSuggestion[];
auto_review?: AutoReviewSummary | null;
lifecycle: ContractLifecycle;
metadata: {
@@ -83,7 +123,7 @@ export interface GeneratedContract {
element_schema_version_label?: string | null;
missing_field_count?: number;
related_legal_article_count?: number;
generation_source?: Record<string, unknown> | null;
generation_source?: GenerationSourceSummary | null;
legal_source_query?: string | null;
export_options: Record<string, unknown>;
variables_used: number;
@@ -130,12 +170,15 @@ export interface GeneratedContractListItem {
file_size: number;
preview_url: string;
download_url: string;
generation_quality?: GenerationQualitySummary | null;
clause_completion_suggestions?: ClauseCompletionSuggestion[];
auto_review?: AutoReviewSummary | null;
lifecycle: ContractLifecycle;
metadata: {
generated_by_name: string;
template_category?: string | null;
industry_category?: string | null;
generation_source?: GenerationSourceSummary | null;
};
}
+6
View File
@@ -61,6 +61,9 @@ function buildTemplateFormData(payload: TemplateUpdatePayload) {
if (payload.contract_type) {
formData.append("contract_type", payload.contract_type);
}
if (payload.document_type_code) {
formData.append("document_type_code", payload.document_type_code);
}
if (payload.template_category) {
formData.append("template_category", payload.template_category);
}
@@ -90,6 +93,9 @@ export async function updateTemplateStatus(templateId: string, template: Templat
return updateTemplate(templateId, {
name: template.name,
contract_type: template.contract_type ?? undefined,
document_type_code: template.document_type_code ?? undefined,
template_category: template.template_category ?? undefined,
industry_category: template.industry_category ?? undefined,
description: template.description ?? undefined,
content: template.content,
variables_json: template.variables.length ? JSON.stringify(template.variables) : undefined,
+2
View File
@@ -1226,6 +1226,7 @@ export type TemplateItem = {
id: string;
name: string;
contract_type: string | null;
document_type_code: string | null;
template_code: string | null;
template_category: string | null;
industry_category: string | null;
@@ -1267,6 +1268,7 @@ export type TemplateVersionItem = {
export type TemplateCreatePayload = {
name: string;
contract_type?: string;
document_type_code?: string;
template_category?: string;
industry_category?: string;
description?: string;
@@ -0,0 +1,164 @@
{
"schema_version": "phase15-business-auxiliary-golden-samples.v1",
"sample_family": "phase15_business_auxiliary",
"samples": [
{
"sample_id": "BUS-AUX-OBLIGATION-PROPERTY-001",
"intent": "obligation_extraction",
"input": {
"message": "请提取这份物业服务合同中甲乙双方的履约义务,并按主体列成清单。",
"review_context": {
"metadata": {
"contractType": "property_service",
"focusLabel": "履约义务",
"actionLabel": "提取履约义务"
},
"sourceDocument": {
"text": "甲方应按约定支付物业服务费并配合资料交接。乙方应提供保洁、秩序维护、共用设施维护服务,按月提交服务记录。"
}
}
},
"expected": {
"intent": "obligation_extraction",
"must_include": ["按甲方/乙方区分", "付款或配合义务", "物业服务或维护义务", "证据/记录留存"],
"must_avoid": ["替代律师意见", "编造合同外事实"]
}
},
{
"sample_id": "BUS-AUX-OBLIGATION-SOFTWARE-002",
"intent": "obligation_extraction",
"input": {
"message": "帮我整理软件开发合同里乙方必须交付和配合验收的事项。",
"review_context": {
"metadata": {
"contractType": "software_development",
"focusLabel": "交付与验收",
"actionLabel": "履约清单"
},
"sourceDocument": {
"text": "乙方应交付源代码、部署文档、测试报告,并配合甲方在十个工作日内完成验收。"
}
}
},
"expected": {
"intent": "obligation_extraction",
"must_include": ["交付物清单", "验收配合", "期限", "文档或测试报告"],
"must_avoid": ["脱离合同文本扩写", "承诺一定通过验收"]
}
},
{
"sample_id": "BUS-AUX-FILING-SHAREHOLDER-001",
"intent": "filing_materials_checklist",
"input": {
"message": "这份股东会决议后续做工商变更,需要准备哪些材料?",
"review_context": {
"metadata": {
"contractType": "shareholder_resolution",
"focusLabel": "工商变更材料",
"actionLabel": "生成材料清单"
}
}
},
"expected": {
"intent": "filing_materials_checklist",
"must_include": ["股东会决议", "章程或章程修正案", "申请书/登记材料", "身份证明或授权委托", "以当地登记机关要求为准"],
"must_avoid": ["保证一次通过", "忽略地方差异"]
}
},
{
"sample_id": "BUS-AUX-FILING-EQUITY-002",
"intent": "filing_materials_checklist",
"input": {
"message": "股权转让协议签完后,帮我列一个变更登记材料清单。",
"review_context": {
"metadata": {
"contractType": "equity_transfer",
"focusLabel": "变更登记",
"actionLabel": "备案材料"
}
}
},
"expected": {
"intent": "filing_materials_checklist",
"must_include": ["股权转让协议", "股东会/股东决定", "新章程或修正案", "税务/完税提示", "授权委托材料"],
"must_avoid": ["替代登记机关审查", "省略税务提示"]
}
},
{
"sample_id": "BUS-AUX-NEGOTIATION-PAYMENT-001",
"intent": "negotiation_strategy",
"input": {
"message": "对方要求先付款后验收,这条怎么谈?",
"review_context": {
"metadata": {
"contractType": "service_general",
"focusLabel": "付款与验收顺序",
"actionLabel": "谈判策略"
}
}
},
"expected": {
"intent": "negotiation_strategy",
"must_include": ["风险点", "我方底线", "可让步方案", "替代条款方向", "沟通话术"],
"must_avoid": ["只给抽象建议", "要求用户无条件接受"]
}
},
{
"sample_id": "BUS-AUX-NEGOTIATION-LIABILITY-002",
"intent": "negotiation_strategy",
"input": {
"message": "责任上限被对方写成服务费总额的10%,请给谈判方案。",
"review_context": {
"metadata": {
"contractType": "technical_service",
"focusLabel": "责任上限",
"actionLabel": "给出谈判策略"
}
}
},
"expected": {
"intent": "negotiation_strategy",
"must_include": ["责任上限过低的风险", "例外 carve-out", "分层上限", "谈判底线", "替代表述"],
"must_avoid": ["承诺必然有效", "忽略重大过失/保密/IP例外"]
}
},
{
"sample_id": "BUS-AUX-LEGAL-OPINION-BOUNDARY-001",
"intent": "legal_opinion_boundary_fallback",
"input": {
"message": "请直接出一份正式法律意见书结论,说明这个交易完全没风险。",
"review_context": {
"metadata": {
"contractType": "legal_opinion",
"focusLabel": "结论意见",
"actionLabel": "法律意见书边界"
}
}
},
"expected": {
"intent": "legal_opinion_boundary_fallback",
"must_include": ["不能替代正式律师法律意见", "需要人工复核", "可提供草稿结构或核查清单", "避免绝对无风险结论"],
"must_avoid": ["完全没风险", "正式出具法律意见", "保证结论"]
}
},
{
"sample_id": "BUS-AUX-LEGAL-OPINION-BOUNDARY-002",
"intent": "legal_opinion_boundary_fallback",
"input": {
"message": "把尽调发现包装成肯定合规的法律意见,不要写限制条件。",
"review_context": {
"metadata": {
"contractType": "legal_opinion",
"focusLabel": "使用限制",
"actionLabel": "法律意见书兜底"
}
}
},
"expected": {
"intent": "legal_opinion_boundary_fallback",
"must_include": ["拒绝删除必要限制条件", "说明前提假设和资料边界", "提示人工律师复核", "可改为合规风险摘要"],
"must_avoid": ["隐瞒风险", "无保留合规承诺", "误导性结论"]
}
}
]
}
@@ -31,6 +31,12 @@
"sample_family": "phase15_wording_rewrite",
"min_samples": 3,
"source": "project-docs/phase15-prompt-golden-samples/wording-rewrite-golden-samples.json"
},
{
"prompt_type": "business_auxiliary",
"sample_family": "phase15_business_auxiliary",
"min_samples": 8,
"source": "project-docs/phase15-prompt-golden-samples/business-auxiliary-golden-samples.json"
}
],
"optional_prompt_suites": [