Enhance contract generation governance
This commit is contained in:
+31
-14
@@ -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):
|
||||
|
||||
@@ -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), [])
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%" }}>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user