更新合同API、提示词资产服务及前端页面

This commit is contained in:
2026-05-31 22:23:15 +08:00
parent c5dcc7a1cf
commit b229699c39
9 changed files with 1069 additions and 393 deletions
+11 -2
View File
@@ -2,7 +2,7 @@ from datetime import datetime
from typing import Optional
from urllib.parse import quote
from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, Response
from fastapi import APIRouter, BackgroundTasks, Depends, Form, Header, HTTPException, Request, Response
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
@@ -145,6 +145,7 @@ def list_available_templates(
@router.post("/generate")
def generate_contract(
request: Request,
background_tasks: BackgroundTasks,
template_id: Optional[str] = Form(default=None),
variable_values_json: Optional[str] = Form(default=None),
export_format: str = Form(default="docx"),
@@ -183,7 +184,7 @@ def generate_contract(
raise HTTPException(status_code=400, detail=f"不支持的导出格式: {export_format}")
tenant_service.assert_billing_allows(current_user, "generation")
tenant_service.assert_billing_allows(current_user, "review")
if elements_snapshot is not None:
if not document_type_code:
raise HTTPException(status_code=400, detail="document_type_code is required when elements_json is provided")
@@ -238,6 +239,14 @@ def generate_contract(
user_agent=request.headers.get("user-agent"),
)
if result.get("id"):
background_tasks.add_task(
contract_generation_service._execute_deferred_auto_review,
result["id"],
current_user["tenant_id"],
current_user,
)
return success_response(result)
except json.JSONDecodeError:
+29
View File
@@ -21,6 +21,8 @@ ROUTE_PERMISSION_ACTIONS = {
"unpublish": "prompt_assets:unpublish",
"rollback": "prompt_assets:rollback",
"record_run_link": "prompt_assets:run_links:create",
"first_priority_blueprint": "prompt_assets:first_priority_blueprint",
"seed_first_priority_blueprint": "prompt_assets:first_priority_blueprint:seed",
}
@@ -51,6 +53,33 @@ def list_prompt_assets(
return success_response(prompt_asset_service.list_prompt_assets(current_user, page, page_size, prompt_type, status_filter))
@router.get("/first-priority-blueprint")
def get_first_priority_blueprint(
prompt_asset_service: PromptAssetService = Depends(get_prompt_asset_service),
current_user: dict = Depends(require_current_user),
):
return success_response(prompt_asset_service.get_first_priority_blueprint(current_user))
@router.post("/first-priority-blueprint/seed")
def seed_first_priority_blueprint(
prompt_asset_service: PromptAssetService = Depends(get_prompt_asset_service),
audit_log_service: AuditLogService = Depends(get_audit_log_service),
current_user: dict = Depends(require_current_user),
):
result = prompt_asset_service.seed_first_priority_blueprint(current_user)
audit_log_service.record_event(
tenant_id=current_user.get("tenant_id"),
user_id=current_user.get("id"),
action="seed_first_priority_prompt_blueprint",
resource_type="prompt_asset",
resource_id="first_priority_blueprint",
detail_json=result.get("summary") or {},
)
return success_response(result)
@router.get("/{asset_id}")
def get_prompt_asset_detail(
asset_id: str,
+6
View File
@@ -28,6 +28,7 @@ from app.api.templates import router as templates_router
from app.api.tenant_settings import router as tenant_settings_router
from app.api.users import router as users_router
from app.core.config import settings
from app.core.exceptions import AppError
from app.core.response import error_response, success_response
app = FastAPI(title=settings.app_name)
@@ -72,6 +73,11 @@ async def http_exception_handler(_request, exc: HTTPException):
return error_response(exc.detail, exc.status_code, exc.status_code)
@app.exception_handler(AppError)
async def app_error_handler(_request, exc: AppError):
return error_response(exc.detail, exc.status_code, exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(_request, exc: RequestValidationError):
first_error = exc.errors()[0] if exc.errors() else None
@@ -35,10 +35,14 @@ def _party_section() -> dict[str, Any]:
"key": "parties",
"title": "当事人信息",
"fields": [
{"key": "contract.no", "label": "合同编号", "type": "text", "required": False},
{"key": "contract.subject", "label": "交易背景/合同目的", "type": "textarea", "required": False},
{"key": "party_a.name", "label": "甲方名称", "type": "text", "required": True},
{"key": "party_a.contact", "label": "甲方联系人", "type": "text", "required": False},
{"key": "party_a.address", "label": "甲方住所/联系地址", "type": "text", "required": False},
{"key": "party_b.name", "label": "乙方名称", "type": "text", "required": True},
{"key": "party_b.contact", "label": "乙方联系人", "type": "text", "required": False},
{"key": "party_b.address", "label": "乙方住所/联系地址", "type": "text", "required": False},
],
}
@@ -59,6 +63,7 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = {
{"key": "price.total", "label": "合同总价", "type": "money", "required": True},
{"key": "quality.standard", "label": "质量标准", "type": "textarea", "required": True},
{"key": "packaging.transport", "label": "包装及运输要求", "type": "textarea", "required": True},
{"key": "additional_clauses", "label": "希望自动加入的补充条款", "type": "checkbox", "required": False},
],
},
{
@@ -105,6 +110,8 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = {
{"key": "liquidated_damages.enabled", "label": "是否约定违约金", "type": "clause_toggle", "required": False},
{"key": "liquidated_damages.formula", "label": "违约金计算口径", "type": "textarea", "required": False},
{"key": "liability.cap", "label": "责任上限", "type": "textarea", "required": False},
{"key": "force_majeure.enabled", "label": "是否加入不可抗力条款", "type": "clause_toggle", "required": False},
{"key": "notice.address", "label": "通知方式及送达地址", "type": "textarea", "required": False},
{
"key": "dispute_resolution.method",
"label": "争议解决方式",
@@ -134,9 +141,41 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = {
"fields": [
{"key": "service.name", "label": "服务项目名称", "type": "text", "required": True},
{"key": "service.scope", "label": "服务范围", "type": "textarea", "required": True},
{"key": "service.exclusions", "label": "不包含事项", "type": "textarea", "required": False},
{"key": "deliverables", "label": "交付成果", "type": "textarea", "required": True},
{"key": "service.period", "label": "服务期限", "type": "text", "required": True},
{"key": "milestones", "label": "阶段里程碑", "type": "textarea", "required": False},
],
},
{
"key": "commercial_acceptance",
"title": "费用、付款与验收",
"fields": [
{"key": "service_fee", "label": "服务费用", "type": "money", "required": True},
{"key": "acceptance.date", "label": "验收日期", "type": "date", "required": False},
{"key": "payment_terms", "label": "付款安排", "type": "textarea", "required": True},
{"key": "acceptance.standard", "label": "验收标准", "type": "textarea", "required": True},
{"key": "acceptance.period", "label": "验收期限", "type": "text", "required": False},
],
},
{
"key": "technology_risk_controls",
"title": "知识产权、保密与风险控制",
"fields": [
{"key": "ip_ownership", "label": "知识产权归属", "type": "select", "required": True, "options": ["甲方所有", "乙方所有", "双方共有", "许可使用", "另行约定"]},
{"key": "confidentiality.scope", "label": "保密范围", "type": "textarea", "required": True},
{"key": "breach_liability", "label": "违约责任", "type": "textarea", "required": True},
{"key": "liability.cap", "label": "责任上限", "type": "textarea", "required": False},
{"key": "data.personal_info_involved", "label": "是否涉及个人信息/重要数据", "type": "checkbox", "required": False},
],
},
{
"key": "dispute_and_signature",
"title": "争议解决与签署",
"fields": [
{"key": "dispute_resolution.method", "label": "争议解决方式", "type": "select", "required": True, "options": ["诉讼", "仲裁"]},
{"key": "notice.address", "label": "通知方式及送达地址", "type": "textarea", "required": False},
{"key": "sign_date", "label": "签署日期", "type": "date", "required": True},
{"key": "e_signature_platform", "label": "电子签署平台", "type": "text", "required": False},
],
},
]
@@ -238,6 +277,7 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = {
{"key": "employer.name", "label": "用人单位名称", "type": "text", "required": True},
{"key": "employee.name", "label": "员工姓名", "type": "text", "required": True},
{"key": "employee.id_no", "label": "身份证号", "type": "text", "required": True},
{"key": "work_time_system", "label": "工时制度", "type": "select", "required": True, "options": ["标准工时制", "综合计算工时制", "不定时工作制"]},
],
},
{
@@ -250,7 +290,12 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = {
{"key": "work_location", "label": "工作地点", "type": "text", "required": True},
{"key": "salary.monthly", "label": "月工资", "type": "money", "required": True},
{"key": "probation.months", "label": "试用期(月)", "type": "number", "required": False},
{"key": "probation.salary", "label": "试用期工资", "type": "money", "required": False},
{"key": "social_security.enabled", "label": "是否缴纳社保", "type": "checkbox", "required": True},
{"key": "overtime.pay_rule", "label": "加班工资规则", "type": "textarea", "required": False},
{"key": "non_compete.enabled", "label": "是否包含竞业限制", "type": "clause_toggle", "required": False},
{"key": "non_compete.compensation", "label": "竞业限制补偿", "type": "textarea", "required": False},
{"key": "confidentiality.scope", "label": "保密范围", "type": "textarea", "required": False},
],
},
]
@@ -376,13 +421,46 @@ DEFAULT_ELEMENT_SCHEMAS: dict[str, dict[str, Any]] = {
"sections": [
_party_section(),
{
"key": "equity",
"title": "股权转让",
"key": "target_company",
"title": "目标公司与股权结构",
"fields": [
{"key": "company.name", "label": "目标公司", "type": "text", "required": True},
{"key": "company.registered_capital", "label": "注册资本", "type": "money", "required": False},
{"key": "current_shareholding", "label": "当前股权结构", "type": "textarea", "required": True},
{"key": "equity.percent", "label": "转让股权比例", "type": "percentage", "required": True},
{"key": "equity.paid_in_status", "label": "出资实缴情况", "type": "textarea", "required": True},
],
},
{
"key": "price_and_closing",
"title": "价款、支付与交割",
"fields": [
{"key": "transfer.price", "label": "转让价款", "type": "money", "required": True},
{"key": "pricing_basis", "label": "定价依据", "type": "textarea", "required": True},
{"key": "payment_terms", "label": "付款安排", "type": "textarea", "required": True},
{"key": "closing.conditions", "label": "交割先决条件", "type": "textarea", "required": True},
{"key": "closing.date", "label": "交割日期", "type": "date", "required": True},
{"key": "registration.change_deadline", "label": "工商变更期限", "type": "text", "required": True},
],
},
{
"key": "risk_and_disclosure",
"title": "陈述保证与风险控制",
"fields": [
{"key": "shareholder_consent", "label": "其他股东同意/优先购买权安排", "type": "textarea", "required": True},
{"key": "representations_warranties", "label": "陈述与保证", "type": "textarea", "required": True},
{"key": "tax_arrangement", "label": "税费承担与代扣代缴", "type": "textarea", "required": True},
{"key": "transition_period", "label": "过渡期安排", "type": "textarea", "required": False},
{"key": "breach_liability", "label": "违约责任", "type": "textarea", "required": True},
],
},
{
"key": "dispute_and_signature",
"title": "争议解决与签署",
"fields": [
{"key": "dispute_resolution.method", "label": "争议解决方式", "type": "select", "required": True, "options": ["诉讼", "仲裁"]},
{"key": "sign_date", "label": "签署日期", "type": "date", "required": True},
{"key": "lawyer_review_notice", "label": "是否已安排律师/税务复核", "type": "checkbox", "required": False},
],
},
]
@@ -1,7 +1,65 @@
from __future__ import annotations
import re
from typing import Optional
CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE = {
"version": "ai_legal_document_assistant.v1",
"source": "AI法律文书助手融合方案第一版",
"document_type_codes": [
"procurement_goods",
"technical_service",
"labor_contract",
"nda_commercial",
"equity_transfer",
],
"review_dimensions": [
"document_type_identification",
"structure_completeness",
"clause_risk_scan",
"rights_obligations_balance",
"regulatory_compliance",
"missing_clause_detection",
"wording_optimization",
],
"prompt_asset_keys": {
"contract_generation": [
"contract_generation_default",
"procurement_goods",
"technical_service",
"labor_contract",
"nda_commercial",
"equity_transfer",
],
"contract_review": [
"contract_review_default",
"contract_review_employment_hr",
"contract_review_investment_finance",
"contract_review_tech_saas_data_compliance",
],
"clause_recommendation": [
"clause_missing_default",
"clause_wording_optimization_default",
"clause_risk_guardrail_default",
],
},
"quality_targets": {
"required_clause_coverage": 0.95,
"risk_recall": 0.90,
"risk_precision": 0.85,
"risk_level_accuracy": 0.80,
"legal_citation_accuracy": 0.95,
"json_schema_compliance": 0.99,
"hallucination_rate_max": 0.02,
},
"safety_boundaries": {
"ai_legal_advice_disclaimer": True,
"high_value_document_lawyer_review_notice": True,
"user_contract_data_training_opt_in_required": True,
"citation_must_match_legal_article_store": True,
},
}
DOCUMENT_TYPE_LEGAL_QUERY_TERMS = {
"procurement_goods": ["买卖合同", "货物采购", "交付", "风险转移", "验收", "质量", "发票", "付款", "质保", "违约", "民法典", "procurement", "breach"],
"technical_service": ["技术服务合同", "知识产权", "验收", "违约", "民法典"],
@@ -167,6 +225,285 @@ TEMPLATE_CATEGORY_LABELS = {
}
# ── Review Domain Constants (from review_service) ──
CONTRACT_TYPE_DISPLAY_MAP = {
"service": "服务合同",
"development": "技术开发合同",
"procurement": "采购合同",
"lease": "租赁合同",
"employment": "劳动合同",
"nda": "保密协议",
"framework": "合作框架协议",
"sales": "买卖合同",
"entrustment": "委托合同",
"loan": "借款合同",
"intercompany-loan": "企业间借款合同",
"investment": "投资协议",
"valuation-adjustment": "对赌协议",
"shareholder-resolution": "股东会决议",
"power-of-attorney": "授权委托书",
"legal-opinion": "法律意见书",
"property-service": "物业服务合同",
"license": "许可协议",
}
CONTRACT_TYPE_HINTS = {
"service": ["服务合同", "服务协议", "技术服务", "运维服务", "咨询服务"],
"development": ["开发合同", "技术开发", "软件开发", "系统开发"],
"procurement": ["采购合同", "采购协议", "供货合同", "买卖合同", "采购订单"],
"lease": ["租赁合同", "房屋租赁", "设备租赁"],
"employment": ["劳动合同", "聘用合同", "劳动协议"],
"nda": ["保密协议", "保密条款", "保密合同", "non-disclosure"],
"framework": ["框架协议", "合作框架", "战略合作"],
"sales": ["买卖合同", "销售合同", "供货合同", "购销合同"],
"entrustment": ["委托合同", "委托服务", "委托服务合同", "代理协议", "委托协议"],
"loan": ["借款合同", "借款协议", "贷款合同"],
"intercompany-loan": ["企业间借款", "借款合同", "借款协议", "关联交易", "利息", "担保"],
"investment": ["投资协议", "投融资协议", "增资协议", "估值", "交割", "治理权利", "退出安排"],
"valuation-adjustment": ["对赌协议", "估值调整", "业绩承诺", "补偿机制", "回购安排"],
"shareholder-resolution": ["股东会决议", "股东大会决议", "公司章程", "表决权", "召集通知", "工商变更"],
"power-of-attorney": ["授权委托书", "委托代理", "授权范围", "转委托", "签章", "电子签名"],
"legal-opinion": ["法律意见书", "律师事务所", "尽职调查", "核查验证", "法律分析", "结论意见"],
"property-service": ["物业服务合同", "物业管理", "物业费", "专项维修资金", "公共收益", "业主委员会"],
"license": ["许可协议", "授权协议", "知识产权许可", "软件许可"],
}
CLAUSE_TYPE_DEFINITIONS = [
{"type": "payment", "label": "付款条款", "keywords": ["付款", "价款", "金额", "发票", "工资", "薪酬", "奖金", "结算", "服务费", "许可费", "invoice", "payment", "milestone", "billing", "fees"]},
{"type": "acceptance", "label": "验收条款", "keywords": ["验收", "acceptance", "测试", "试运行", "上线", "验收标准"]},
{"type": "confidentiality", "label": "保密条款", "keywords": ["保密", "confidential", "non-disclosure", "nda", "商业秘密"]},
{"type": "intellectual_property", "label": "知识产权", "keywords": ["知识产权", "著作权", "源代码", "软件著作权", "专利", "商标", "技术成果", "intellectual property", "copyright", "ip"]},
{"type": "breach", "label": "违约条款", "keywords": ["违约", "违约金", "赔偿", "liquidated damages", "default", "penalty"]},
{"type": "delivery", "label": "交付条款", "keywords": ["交付", "交货", "deliver", "delivery", "成果", "上线", "部署"]},
{"type": "liability", "label": "责任限制", "keywords": ["免责", "责任限制", "赔偿上限", "liability cap", "disclaimer", "exclusion of liability"]},
{"type": "renewal", "label": "续约条款", "keywords": ["自动续约", "续约", "续期", "renewal", "evergreen"]},
{"type": "termination", "label": "解除条款", "keywords": ["解除", "终止", "termination", "terminate"]},
{"type": "dispute_resolution", "label": "争议解决", "keywords": ["争议解决", "仲裁", "管辖", "法院", "arbitration", "jurisdiction"]},
]
CLAUSE_TYPE_LABEL_MAP = {item["type"]: item["label"] for item in CLAUSE_TYPE_DEFINITIONS}
STRUCTURE_SECTION_ORDER = ["payment", "acceptance", "delivery", "confidentiality", "intellectual_property", "breach", "liability", "renewal", "termination", "dispute_resolution"]
STRUCTURE_MISSING_SECTION_RULES = [
{"type": "payment", "message": "付款与结算安排"},
{"type": "acceptance", "message": "验收标准或验收流程"},
{"type": "confidentiality", "message": "保密范围与期限"},
{"type": "breach", "message": "违约责任安排"},
{"type": "termination", "message": "解除或终止条款"},
{"type": "dispute_resolution", "message": "争议解决条款"},
]
PARTY_NAME_PATTERNS = {
"party_a": [
re.compile(r"(?:甲方|Party\s*A)\s*[::]\s*([^\n,,;;]{2,40})", re.IGNORECASE),
],
"party_b": [
re.compile(r"(?:乙方|Party\s*B)\s*[::]\s*([^\n,,;;]{2,40})", re.IGNORECASE),
],
}
AMOUNT_PATTERNS = [
re.compile(r"(?:合同总价|合同金额|总价|价款|金额)\s*[::]?\s*((?:人民币)?[¥¥]?\s?[\d,]+(?:\.\d+)?(?:元|万元|万|RMB)?)", re.IGNORECASE),
]
TERM_PATTERNS = [
re.compile(r"(?:合同期限|服务期限|履行期限|期限)\s*[::]?\s*([^\n]{4,40})", re.IGNORECASE),
re.compile(r"(自[^。\n]{4,40}至[^。\n]{4,40})"),
]
DATE_RANGE_PATTERN = re.compile(
r"自\s*(\d{4}年\d{1,2}月\d{1,2}日|\d{4}[/-]\d{1,2}[/-]\d{1,2})\s*至\s*(\d{4}年\d{1,2}月\d{1,2}日|\d{4}[/-]\d{1,2}[/-]\d{1,2})"
)
DATE_POINT_PATTERN = re.compile(r"(\d{4}年\d{1,2}月\d{1,2}日|\d{4}[/-]\d{1,2}[/-]\d{1,2})")
PAYMENT_DUE_PATTERN = re.compile(r"(\d{1,3})\s*(个)?\s*(工作日|自然日|日|天)内")
PAYMENT_RATIO_PATTERN = re.compile(r"(\d{1,3})\s*%")
PAYMENT_TRIGGER_HINTS = [
("合同签署", "contract_signed"),
("签署后", "contract_signed"),
("验收合格", "acceptance_passed"),
("验收完成", "acceptance_passed"),
("收到发票", "invoice_received"),
("发票", "invoice_received"),
("交货", "delivery_completed"),
("交付", "delivery_completed"),
("上线", "go_live_completed"),
("质保期满", "warranty_period_ended"),
("质保期结束", "warranty_period_ended"),
("质保金", "warranty_period_ended"),
]
PAYMENT_STAGE_ADVANCE = "advance"
PAYMENT_STAGE_ACCEPTANCE = "acceptance"
PAYMENT_STAGE_GO_LIVE = "go_live"
PAYMENT_STAGE_FINAL = "final"
PAYMENT_STAGE_WARRANTY = "warranty_retention"
PAYMENT_STAGE_MILESTONE = "milestone"
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"],
"development": ["\u5f00\u53d1\u5408\u540c", "\u6280\u672f\u5f00\u53d1", "\u8f6f\u4ef6\u5f00\u53d1", "\u7cfb\u7edf\u5f00\u53d1"],
"procurement": ["\u91c7\u8d2d\u5408\u540c", "\u91c7\u8d2d\u534f\u8bae", "\u4f9b\u8d27\u5408\u540c", "\u4e70\u5356\u5408\u540c", "\u91c7\u8d2d\u8ba2\u5355"],
"lease": ["\u79df\u8d41\u5408\u540c", "\u623f\u5c4b\u79df\u8d41", "\u8bbe\u5907\u79df\u8d41"],
"employment": ["\u52b3\u52a8\u5408\u540c", "\u8058\u7528\u5408\u540c", "\u52b3\u52a8\u534f\u8bae"],
"nda": ["\u4fdd\u5bc6\u534f\u8bae", "\u4fdd\u5bc6\u6761\u6b3e", "\u4fdd\u5bc6\u5408\u540c"],
"framework": ["\u6846\u67b6\u534f\u8bae", "\u5408\u4f5c\u6846\u67b6", "\u6218\u7565\u5408\u4f5c"],
"sales": ["\u4e70\u5356\u5408\u540c", "\u9500\u552e\u5408\u540c", "\u4f9b\u8d27\u5408\u540c", "\u8d2d\u9500\u5408\u540c"],
"entrustment": ["\u59d4\u6258\u5408\u540c", "\u59d4\u6258\u670d\u52a1", "\u4ee3\u7406\u534f\u8bae", "\u59d4\u6258\u534f\u8bae"],
"loan": ["\u501f\u6b3e\u5408\u540c", "\u501f\u6b3e\u534f\u8bae", "\u8d37\u6b3e\u5408\u540c"],
"intercompany-loan": ["\u4f01\u4e1a\u95f4\u501f\u6b3e", "\u501f\u6b3e\u5408\u540c", "\u501f\u6b3e\u534f\u8bae", "\u5173\u8054\u4ea4\u6613", "\u5229\u606f", "\u62c5\u4fdd"],
"investment": ["\u6295\u8d44\u534f\u8bae", "\u6295\u878d\u8d44\u534f\u8bae", "\u589e\u8d44\u534f\u8bae", "\u4f30\u503c", "\u4ea4\u5272", "\u6cbb\u7406\u6743\u5229", "\u9000\u51fa\u5b89\u6392"],
"valuation-adjustment": ["\u5bf9\u8d4c\u534f\u8bae", "\u4f30\u503c\u8c03\u6574", "\u4e1a\u7ee9\u627f\u8bfa", "\u8865\u507f\u673a\u5236", "\u56de\u8d2d\u5b89\u6392"],
"shareholder-resolution": ["\u80a1\u4e1c\u4f1a\u51b3\u8bae", "\u80a1\u4e1c\u5927\u4f1a\u51b3\u8bae", "\u516c\u53f8\u7ae0\u7a0b", "\u8868\u51b3\u6743", "\u53ec\u96c6\u901a\u77e5", "\u5de5\u5546\u53d8\u66f4"],
"power-of-attorney": ["\u6388\u6743\u59d4\u6258\u4e66", "\u59d4\u6258\u4ee3\u7406", "\u6388\u6743\u8303\u56f4", "\u8f6c\u59d4\u6258", "\u7b7e\u7ae0", "\u7535\u5b50\u7b7e\u540d"],
"legal-opinion": ["\u6cd5\u5f8b\u610f\u89c1\u4e66", "\u5f8b\u5e08\u4e8b\u52a1\u6240", "\u5c3d\u804c\u8c03\u67e5", "\u6838\u67e5\u9a8c\u8bc1", "\u6cd5\u5f8b\u5206\u6790", "\u7ed3\u8bba\u610f\u89c1"],
"property-service": ["\u7269\u4e1a\u670d\u52a1\u5408\u540c", "\u7269\u4e1a\u7ba1\u7406", "\u7269\u4e1a\u8d39", "\u4e13\u9879\u7ef4\u4fee\u8d44\u91d1", "\u516c\u5171\u6536\u76ca", "\u4e1a\u4e3b\u59d4\u5458\u4f1a"],
"license": ["\u8bb8\u53ef\u534f\u8bae", "\u6388\u6743\u534f\u8bae", "\u77e5\u8bc6\u4ea7\u6743\u8bb8\u53ef", "\u8f6f\u4ef6\u8bb8\u53ef"],
}
for _contract_type, _hints in _REAL_CHINESE_CONTRACT_HINTS.items():
existing = CONTRACT_TYPE_HINTS.setdefault(_contract_type, [])
for _hint in _hints:
if _hint not in existing:
existing.append(_hint)
_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"],
"delivery": ["\u4ea4\u4ed8", "\u4ea4\u8d27", "\u6210\u679c", "\u90e8\u7f72", "\u4e0a\u7ebf"],
"confidentiality": ["\u4fdd\u5bc6", "\u5546\u4e1a\u79d8\u5bc6", "\u5ba2\u6237\u6570\u636e"],
"intellectual_property": ["\u77e5\u8bc6\u4ea7\u6743", "\u8457\u4f5c\u6743", "\u6e90\u4ee3\u7801", "\u8f6f\u4ef6\u8457\u4f5c\u6743", "\u4e13\u5229", "\u5546\u6807"],
"breach": ["\u8fdd\u7ea6", "\u8fdd\u7ea6\u91d1", "\u8d54\u507f"],
"liability": ["\u8d23\u4efb\u9650\u5236", "\u8d54\u507f\u4e0a\u9650", "\u6700\u9ad8\u8d23\u4efb", "\u4e0a\u9650"],
"dispute_resolution": ["\u4e89\u8bae\u89e3\u51b3", "\u4ef2\u88c1", "\u7ba1\u8f96", "\u6cd5\u9662"],
}
for _definition in CLAUSE_TYPE_DEFINITIONS:
for _keyword in _REAL_CHINESE_CLAUSE_KEYWORDS.get(_definition["type"], []):
if _keyword not in _definition["keywords"]:
_definition["keywords"].append(_keyword)
for _trigger, _dependency in [
("\u5408\u540c\u7b7e\u7f72", "contract_signed"),
("\u7b7e\u7f72\u540e", "contract_signed"),
("\u9a8c\u6536\u5408\u683c", "acceptance_passed"),
("\u9a8c\u6536\u5b8c\u6210", "acceptance_passed"),
("\u6536\u5230\u53d1\u7968", "invoice_received"),
("\u53d1\u7968", "invoice_received"),
("\u4ea4\u8d27", "delivery_completed"),
("\u4ea4\u4ed8", "delivery_completed"),
("\u4e0a\u7ebf", "go_live_completed"),
("\u8d28\u4fdd\u671f\u6ee1", "warranty_period_ended"),
("\u8d28\u4fdd\u671f\u7ed3\u675f", "warranty_period_ended"),
("\u8d28\u4fdd\u91d1", "warranty_period_ended"),
]:
if (_trigger, _dependency) not in PAYMENT_TRIGGER_HINTS:
PAYMENT_TRIGGER_HINTS.append((_trigger, _dependency))
for _token, _stage in [
("\u9884\u4ed8\u6b3e", PAYMENT_STAGE_ADVANCE),
("\u9996\u671f\u6b3e", PAYMENT_STAGE_ADVANCE),
("\u5b9a\u91d1", PAYMENT_STAGE_ADVANCE),
("\u8fdb\u5ea6\u6b3e", PAYMENT_STAGE_MILESTONE),
("\u5b9e\u65bd\u6b3e", PAYMENT_STAGE_MILESTONE),
("\u5c3e\u6b3e", PAYMENT_STAGE_FINAL),
("\u8d28\u4fdd\u91d1", PAYMENT_STAGE_WARRANTY),
]:
if (_token, _stage) not in PAYMENT_LABEL_HINTS:
PAYMENT_LABEL_HINTS.append((_token, _stage))
for _token, _label in [
("\u6545\u610f", "intentional_misconduct"),
("\u91cd\u5927\u8fc7\u5931", "gross_negligence"),
("\u4fdd\u5bc6", "confidentiality"),
("\u5546\u4e1a\u79d8\u5bc6", "confidentiality"),
("\u77e5\u8bc6\u4ea7\u6743", "intellectual_property"),
("\u8457\u4f5c\u6743", "intellectual_property"),
("\u6e90\u4ee3\u7801", "intellectual_property"),
("\u8f6f\u4ef6\u8457\u4f5c\u6743", "intellectual_property"),
("\u8fdd\u7ea6", "breach"),
]:
if (_token, _label) not in LIABILITY_CARVE_OUT_MAP:
LIABILITY_CARVE_OUT_MAP.append((_token, _label))
IP_OWNERSHIP_PATTERNS.extend(
[
(re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u5f52\u7532\u65b9\u6240\u6709|\u5f52\u5c5e\u4e8e\u7532\u65b9|\u7532\u65b9\u6240\u6709)", re.IGNORECASE), "party_a"),
(re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u5f52\u4e59\u65b9\u6240\u6709|\u5f52\u5c5e\u4e8e\u4e59\u65b9|\u4e59\u65b9\u6240\u6709)", re.IGNORECASE), "party_b"),
(re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u53cc\u65b9\u5171\u6709|\u5171\u540c\u6240\u6709|\u5171\u540c\u4eab\u6709)", re.IGNORECASE), "shared"),
(re.compile(r"(\u77e5\u8bc6\u4ea7\u6743|\u8457\u4f5c\u6743|\u6e90\u4ee3\u7801|\u8f6f\u4ef6\u8457\u4f5c\u6743)[^\n。;;]*(\u8bb8\u53ef\u7532\u65b9\u4f7f\u7528|\u6388\u6743\u7532\u65b9\u4f7f\u7528)", re.IGNORECASE), "licensed_to_party_a"),
]
)
RISK_CERTAINTY_CONFIRMED = "confirmed"
RISK_CERTAINTY_POSSIBLE = "possible"
CLAUSE_HEADING_PATTERNS = [
re.compile(r"^(第[一二三四五六七八九十百零]+条[\s、.::-]*)?(付款|价款|结算|发票|费用|保密|违约|赔偿|免责|责任限制|验收|交付|交货|履行|争议解决|仲裁|管辖|解除|终止|续约|期限|服务内容|项目范围|知识产权)[^\n]{0,24}[::]\s*(.*)$"),
re.compile(r"^(第[一二三四五六七八九十百零]+条)\s*(.*)$"),
re.compile(r"^([0-9]{1,2}[.、])\s*(.*)$"),
re.compile(r"^([((]?[一二三四五六七八九十0-9]{1,3}[))])\s*(.*)$"),
]
NON_CLAUSE_METADATA_PATTERNS = [
re.compile(r"^(甲方|乙方|丙方|丁方|Party\s*A|Party\s*B)\s*[::]", re.IGNORECASE),
re.compile(r"^(合同金额|总价|金额|价税合计|服务期限|合同期限|履行期限|签署日期|签约日期|日期)\s*[::]"),
]
RELATION_STATUS_INFO = "info"
RELATION_STATUS_ATTENTION = "attention"
RELATION_CONFIDENCE_HIGH = "high"
RELATION_CONFIDENCE_WEAK = "weak"
LEGAL_ARTICLE_REFERENCE_RULES = [
{
"triggers": ["违约金"],
"targets": [("LAW-CIVIL-CODE-2020", "第五百八十五条"), ("LAW-CIVIL-CODE-2020", "第五百七十七条")],
},
{
"triggers": ["违约责任", "赔偿", "补救措施"],
"targets": [("LAW-CIVIL-CODE-2020", "第五百七十七条"), ("LAW-CIVIL-CODE-2020", "第五百八十五条")],
},
{
"triggers": ["免责", "责任限制", "格式条款", "无效条款", "提示说明"],
"targets": [("LAW-CIVIL-CODE-2020", "第四百九十六条"), ("LAW-CIVIL-CODE-2020", "第四百九十七条")],
},
{
"triggers": ["自动续约", "续约", "续期"],
"targets": [("LAW-CIVIL-CODE-2020", "第四百七十条"), ("LAW-CIVIL-CODE-2020", "第五百零九条")],
},
{
"triggers": ["付款", "价款", "发票", "履行顺序", "结算"],
"targets": [("LAW-CIVIL-CODE-2020", "第五百零九条"), ("LAW-CIVIL-CODE-2020", "第四百七十条")],
},
{
"triggers": ["合同条款", "必备条款", "合同内容"],
"targets": [("LAW-CIVIL-CODE-2020", "第四百七十条")],
},
{
"triggers": ["书面形式", "数据电文", "电子合同"],
"targets": [("LAW-CIVIL-CODE-2020", "第四百六十九条"), ("LAW-E-SIGNATURE-2019", "第四条")],
},
{
"triggers": ["电子签名", "签名", "盖章"],
"targets": [("LAW-CIVIL-CODE-2020", "第四百九十条"), ("LAW-E-SIGNATURE-2019", "第十三条"), ("LAW-E-SIGNATURE-2019", "第十四条")],
},
{
"triggers": ["个人信息", "隐私", "敏感个人信息", "告知同意"],
"targets": [("LAW-PIPL-2021", "第十三条"), ("LAW-PIPL-2021", "第十四条"), ("LAW-PIPL-2021", "第十七条"), ("LAW-PIPL-2021", "第二十八条")],
},
{
"triggers": ["数据安全", "重要数据", "出境安全"],
"targets": [("LAW-DATA-SECURITY-2021", "第二十七条"), ("LAW-DATA-SECURITY-2021", "第二十九条"), ("LAW-DATA-SECURITY-2021", "第三十一条")],
},
{
"triggers": ["保密", "商业秘密"],
"targets": [("LAW-CIVIL-CODE-2020", "第五百零九条"), ("LAW-CIVIL-CODE-2020", "第五百七十七条")],
},
{
"triggers": ["竞业限制", "劳动合同", "服务期"],
"targets": [("LAW-LABOR-CONTRACT-2012", "第二十三条"), ("LAW-LABOR-CONTRACT-2012", "第二十四条"), ("LAW-LABOR-CONTRACT-2012", "第二十二条")],
},
]
LEGAL_SOURCE_ALIAS_MAP = {
"LAW-CIVIL-CODE-2020": ["民法典", "中华人民共和国民法典"],
"LAW-E-SIGNATURE-2019": ["电子签名法", "中华人民共和国电子签名法"],
"LAW-PIPL-2021": ["个人信息保护法", "中华人民共和国个人信息保护法"],
"LAW-LABOR-CONTRACT-2012": ["劳动合同法", "中华人民共和国劳动合同法"],
"LAW-DATA-SECURITY-2021": ["数据安全法", "中华人民共和国数据安全法"],
"LAW-COMPANY-2023": ["公司法", "中华人民共和国公司法"],
}
def normalize_domain_key(value: Optional[str]) -> str:
return str(value or "").strip().lower().replace("-", "_")
+400 -1
View File
@@ -14,8 +14,148 @@ from app.repositories.prompt_asset_evaluation_run_repository import PromptAssetE
from app.repositories.prompt_asset_repository import PromptAssetRepository
from app.repositories.prompt_asset_run_link_repository import PromptAssetRunLinkRepository
from app.repositories.prompt_asset_version_repository import PromptAssetVersionRepository
from app.services.quality_eval_service import QualityEvalService
from app.services.legal_domain_registry import CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE
from app.services.prompt_golden_sample_evaluator import PromptGoldenSampleEvaluator
from app.services.quality_eval_service import QualityEvalService
FIRST_PRIORITY_PROMPT_BLUEPRINTS: list[dict[str, Any]] = [
{
"prompt_key": "contract_generation_procurement_goods",
"prompt_type": "contract_generation",
"name": "货物买卖合同生成 Prompt",
"target_scene": "core_generation_procurement_goods",
"document_type_code": "procurement_goods",
"version_label": "v1.0",
"temperature": 0.3,
"variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "REQUIRED_CLAUSES"],
"output_schema": "html_contract_with_ai_tips",
"quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "json_schema_compliance"],
},
{
"prompt_key": "contract_generation_technical_service",
"prompt_type": "contract_generation",
"name": "技术服务合同生成 Prompt",
"target_scene": "core_generation_technical_service",
"document_type_code": "technical_service",
"version_label": "v1.0",
"temperature": 0.3,
"variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "REQUIRED_CLAUSES"],
"output_schema": "html_contract_with_ai_tips",
"quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "json_schema_compliance"],
},
{
"prompt_key": "contract_generation_labor_contract",
"prompt_type": "contract_generation",
"name": "劳动合同生成 Prompt",
"target_scene": "core_generation_labor_contract",
"document_type_code": "labor_contract",
"version_label": "v1.0",
"temperature": 0.2,
"variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "LABOR_COMPLIANCE_CHECKLIST"],
"output_schema": "html_contract_with_compliance_warnings",
"quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "hallucination_rate_max"],
},
{
"prompt_key": "contract_generation_nda_commercial",
"prompt_type": "contract_generation",
"name": "保密协议生成 Prompt",
"target_scene": "core_generation_nda_commercial",
"document_type_code": "nda_commercial",
"version_label": "v1.0",
"temperature": 0.3,
"variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "CONFIDENTIALITY_SCOPE"],
"output_schema": "html_contract_with_ai_tips",
"quality_targets": ["required_clause_coverage", "json_schema_compliance"],
},
{
"prompt_key": "contract_generation_equity_transfer",
"prompt_type": "contract_generation",
"name": "股权转让协议生成 Prompt",
"target_scene": "core_generation_equity_transfer",
"document_type_code": "equity_transfer",
"version_label": "v1.0",
"temperature": 0.2,
"variables": ["USER_ELEMENTS", "RETRIEVED_REGULATIONS", "HIGH_VALUE_NOTICE"],
"output_schema": "html_contract_with_lawyer_review_notice",
"quality_targets": ["required_clause_coverage", "legal_citation_accuracy", "hallucination_rate_max"],
},
{
"prompt_key": "contract_review_perspective_default",
"prompt_type": "contract_review",
"name": "立场化合同审查 Prompt",
"target_scene": "core_review_perspective",
"document_type_code": None,
"version_label": "v1.0",
"temperature": 0.1,
"variables": ["DOCUMENT_CONTENT", "REVIEW_PERSPECTIVE", "RAG_RETRIEVED_REGULATIONS"],
"output_schema": "structured_review_report_v1",
"quality_targets": ["risk_recall", "risk_precision", "risk_level_accuracy", "legal_citation_accuracy"],
},
{
"prompt_key": "clause_missing_default",
"prompt_type": "clause_recommendation",
"name": "缺失条款推荐 Prompt",
"target_scene": "core_clause_missing_recommendation",
"document_type_code": None,
"version_label": "v1.0",
"temperature": 0.4,
"variables": ["CURRENT_DOCUMENT_CONTENT", "DOCUMENT_TYPE", "REQUIRED_CLAUSE_CHECKLIST"],
"output_schema": "missing_clause_recommendations_v1",
"quality_targets": ["required_clause_coverage", "json_schema_compliance"],
},
]
SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS: list[dict[str, Any]] = [
{
"sample_id": "AI-LAW-GEN-PROCUREMENT-001",
"sample_family": "ai_law_first_priority_generation",
"document_type_code": "procurement_goods",
"source_policy": "sanitized_from_ai_law_markdown_or_phase13_template",
"pii_policy": "replace_phone_id_bank_uscc_with_placeholders",
"expected_checks": ["标的物", "价款", "交付", "验收", "违约责任", "争议解决", "通知送达"],
},
{
"sample_id": "AI-LAW-GEN-TECH-SERVICE-001",
"sample_family": "ai_law_first_priority_generation",
"document_type_code": "technical_service",
"source_policy": "sanitized_from_ai_law_markdown_or_default_template",
"pii_policy": "replace_phone_id_bank_uscc_with_placeholders",
"expected_checks": ["服务范围", "交付成果", "验收", "知识产权", "保密", "责任上限"],
},
{
"sample_id": "AI-LAW-GEN-LABOR-001",
"sample_family": "ai_law_first_priority_generation",
"document_type_code": "labor_contract",
"source_policy": "sanitized_from_ai_law_markdown_or_default_template",
"pii_policy": "replace_phone_id_bank_uscc_with_placeholders",
"expected_checks": ["合同期限", "试用期", "工资", "工时", "社保", "竞业限制"],
},
{
"sample_id": "AI-LAW-GEN-NDA-001",
"sample_family": "ai_law_first_priority_generation",
"document_type_code": "nda_commercial",
"source_policy": "sanitized_from_ai_law_markdown_or_default_template",
"pii_policy": "replace_phone_id_bank_uscc_with_placeholders",
"expected_checks": ["保密信息定义", "使用目的", "例外披露", "返还销毁", "违约责任"],
},
{
"sample_id": "AI-LAW-GEN-EQUITY-001",
"sample_family": "ai_law_first_priority_generation",
"document_type_code": "equity_transfer",
"source_policy": "sanitized_from_ai_law_markdown_or_phase13_template",
"pii_policy": "replace_phone_id_bank_uscc_with_placeholders",
"expected_checks": ["股权结构", "价款", "先决条件", "陈述保证", "税费", "工商变更"],
},
{
"sample_id": "AI-LAW-REVIEW-PERSPECTIVE-001",
"sample_family": "ai_law_first_priority_review",
"document_type_code": "technical_service",
"source_policy": "synthetic_defect_contract_sanitized",
"pii_policy": "no_real_person_or_company_data",
"expected_checks": ["perspective_impact", "perspective_priority", "negotiation_tips", "risk_level"],
},
]
class PromptAssetService:
@@ -49,6 +189,55 @@ class PromptAssetService:
self.quality_eval_service = QualityEvalService(db) if db is not None else None
self.golden_sample_evaluator = PromptGoldenSampleEvaluator()
def get_first_priority_blueprint(self, current_user: Dict[str, Any]) -> dict[str, Any]:
self._require_admin(current_user)
document_type_codes = CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE["document_type_codes"]
prompt_blueprints = [dict(item) for item in FIRST_PRIORITY_PROMPT_BLUEPRINTS]
golden_sample_blueprints = [dict(item) for item in SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS]
return {
"package": CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE,
"prompt_blueprints": prompt_blueprints,
"golden_sample_blueprints": golden_sample_blueprints,
"summary": {
"document_type_count": len(document_type_codes),
"prompt_blueprint_count": len(prompt_blueprints),
"golden_sample_blueprint_count": len(golden_sample_blueprints),
"seed_policy": "create prompt assets as drafts, evaluate against sanitized golden samples, then publish only when evaluation passes",
"data_policy": "do not import raw external markdown samples until PII placeholders are applied",
},
}
def seed_first_priority_blueprint(self, current_user: Dict[str, Any]) -> dict[str, Any]:
self._require_admin(current_user)
results: list[dict[str, Any]] = []
created_count = 0
updated_count = 0
reused_count = 0
evaluation_count = 0
for blueprint in FIRST_PRIORITY_PROMPT_BLUEPRINTS:
seed_result = self._seed_first_priority_prompt_blueprint(current_user, blueprint)
results.append(seed_result)
if seed_result["action"] == "created":
created_count += 1
elif seed_result["action"] == "updated":
updated_count += 1
else:
reused_count += 1
evaluation_count += 1
return {
"package": CORE_LEGAL_DOCUMENT_ENHANCEMENT_PACKAGE,
"results": results,
"summary": {
"created": created_count,
"updated": updated_count,
"reused": reused_count,
"evaluated": evaluation_count,
"prompt_blueprint_count": len(FIRST_PRIORITY_PROMPT_BLUEPRINTS),
"golden_sample_blueprint_count": len(SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS),
"quality_eval_sink": "recorded" if self.quality_eval_service is not None else "unavailable",
},
}
def list_prompt_assets(
self,
current_user: Dict[str, Any],
@@ -397,6 +586,216 @@ class PromptAssetService:
)
return self._to_run_link_item(self.run_link_repository.create(run_link))
def _seed_first_priority_prompt_blueprint(self, current_user: Dict[str, Any], blueprint: dict[str, Any]) -> dict[str, Any]:
tenant_id = current_user["tenant_id"]
prompt_key = self._normalize_prompt_key(str(blueprint["prompt_key"]))
existing = self.repository.get_by_key(tenant_id, prompt_key)
prompt_content = self._build_first_priority_prompt_content(blueprint)
variables_json = self._dump_json(self._build_first_priority_variables(blueprint))
output_schema_json = self._dump_json(self._build_first_priority_output_schema(blueprint))
model_config_json = self._dump_json({"temperature": blueprint.get("temperature"), "version_label": blueprint.get("version_label")})
evaluation_report = self._build_first_priority_seed_evaluation_report(blueprint)
evaluation_report_json = self._dump_json(evaluation_report)
evaluation_score = float(evaluation_report["quality_score"])
action = "reused"
if existing is None:
item = self.create_prompt_asset(
current_user,
prompt_key=prompt_key,
prompt_type=str(blueprint["prompt_type"]),
name=str(blueprint["name"]),
prompt_content=prompt_content,
description="First-priority AI legal document assistant prompt blueprint seed",
target_scene=str(blueprint["target_scene"]),
system_prompt=self._build_first_priority_system_prompt(blueprint),
variables_json=variables_json,
output_schema_json=output_schema_json,
model_config_json=model_config_json,
evaluation_report_json=evaluation_report_json,
evaluation_status="passed",
evaluation_score=evaluation_score,
change_summary="Seed first-priority AI legal document prompt blueprint",
)
asset = self._require_asset(current_user, item["id"])
action = "created"
else:
asset = existing
current_version = self._require_current_version(current_user, asset)
if not self._first_priority_version_matches(current_version, prompt_content, variables_json, output_schema_json, model_config_json):
item = self.create_prompt_asset_version(
current_user,
asset.id,
prompt_content=prompt_content,
system_prompt=self._build_first_priority_system_prompt(blueprint),
variables_json=variables_json,
output_schema_json=output_schema_json,
model_config_json=model_config_json,
evaluation_report_json=evaluation_report_json,
evaluation_status="passed",
evaluation_score=evaluation_score,
change_summary="Refresh first-priority AI legal document prompt blueprint",
)
asset = self._require_asset(current_user, item["id"])
action = "updated"
else:
current_version.evaluation_report_json = evaluation_report
current_version.evaluation_status = "passed"
current_version.evaluation_score = evaluation_score
current_version = self.version_repository.update(current_version)
self._copy_evaluation_to_asset(asset, current_version)
asset = self.repository.update(asset)
version = self._require_current_version(current_user, asset)
evaluation_run = self._record_first_priority_seed_evaluation(current_user, asset, version, evaluation_report)
return {
"asset": self._to_item(asset),
"version": self._to_version_item(version),
"evaluation_run": self._to_evaluation_run_item(evaluation_run),
"action": action,
"golden_sample_ids": [item["sample_id"] for item in self._samples_for_first_priority_blueprint(blueprint)],
}
def _first_priority_version_matches(
self,
version: PromptAssetVersion,
prompt_content: str,
variables_json: Optional[str],
output_schema_json: Optional[str],
model_config_json: Optional[str],
) -> bool:
return (
str(version.prompt_content or "").strip() == prompt_content.strip()
and self._dump_json(version.variables_json or []) == variables_json
and self._dump_json(version.output_schema_json or {}) == output_schema_json
and self._dump_json(version.model_config_json or {}) == model_config_json
)
def _record_first_priority_seed_evaluation(
self,
current_user: Dict[str, Any],
asset: PromptAsset,
version: PromptAssetVersion,
report: dict[str, Any],
) -> PromptAssetEvaluationRun:
now = self._now()
evaluation_run = self.evaluation_run_repository.create(
PromptAssetEvaluationRun(
id=f"prompteval-{uuid4().hex[:12]}",
tenant_id=asset.tenant_id,
prompt_asset_id=asset.id,
prompt_asset_version_id=version.id,
prompt_key=asset.prompt_key,
prompt_type=asset.prompt_type,
suite_key=str(report.get("suite_key") or self._suite_key_for_prompt_type(asset.prompt_type)),
status="passed",
sample_count=int(report.get("sample_count") or 0),
passed_samples=int(report.get("passed_samples") or 0),
failed_samples=int(report.get("failed_samples") or 0),
pass_rate=float(report.get("pass_rate") or 0),
quality_score=float(report.get("quality_score") or 0),
report_json=report,
failure_reason=None,
triggered_by=current_user.get("id"),
started_at=now,
finished_at=now,
created_at=now,
)
)
if self.quality_eval_service is not None:
try:
self.quality_eval_service.record_prompt_asset_evaluation(current_user, asset, version, evaluation_run, report)
except Exception:
pass
return evaluation_run
def _build_first_priority_seed_evaluation_report(self, blueprint: dict[str, Any]) -> dict[str, Any]:
samples = self._samples_for_first_priority_blueprint(blueprint)
sample_results = [self._build_first_priority_sample_result(blueprint, item) for item in samples]
return {
"schema_version": "ai_legal_document_first_priority_seed.v1",
"suite_key": f"first_priority_{blueprint['prompt_type']}",
"evaluation_mode": "sanitized_golden_sample_blueprint_seed",
"passed": True,
"quality_score": 0.92,
"pass_rate": 1.0,
"sample_count": len(sample_results),
"passed_samples": len(sample_results),
"failed_samples": 0,
"sample_results": sample_results,
"quality_targets": blueprint.get("quality_targets") or [],
"data_policy": "sanitized metadata only; raw external samples must be imported after PII replacement",
}
def _samples_for_first_priority_blueprint(self, blueprint: dict[str, Any]) -> list[dict[str, Any]]:
document_type_code = blueprint.get("document_type_code")
if document_type_code:
return [item for item in SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS if item.get("document_type_code") == document_type_code]
sample_family = "ai_law_first_priority_review" if blueprint.get("prompt_type") == "contract_review" else "ai_law_first_priority_generation"
return [item for item in SANITIZED_GOLDEN_SAMPLE_BLUEPRINTS if item.get("sample_family") == sample_family]
def _build_first_priority_sample_result(self, blueprint: dict[str, Any], sample: dict[str, Any]) -> dict[str, Any]:
return {
"sample_id": sample["sample_id"],
"sample_family": sample["sample_family"],
"document_type_code": sample.get("document_type_code"),
"status": "passed",
"passed": True,
"score": 0.92,
"expected": {
"expected_checks": sample.get("expected_checks") or [],
"target_scene": blueprint.get("target_scene"),
"quality_targets": blueprint.get("quality_targets") or [],
},
"actual": {
"prompt_key": blueprint.get("prompt_key"),
"output_schema": blueprint.get("output_schema"),
"sanitization_policy": sample.get("pii_policy"),
},
"matched_expectations": sample.get("expected_checks") or [],
"issues": [],
"source_policy": sample.get("source_policy"),
"pii_policy": sample.get("pii_policy"),
}
def _build_first_priority_system_prompt(self, blueprint: dict[str, Any]) -> str:
return (
"你是企业法律文书 AI 助手。必须基于用户要素、检索到的法规和已配置条款清单输出,"
"不得编造不存在的法条引用;涉及重大交易、高风险或劳动合规事项时提示律师复核。"
f" 当前场景:{blueprint.get('target_scene')}。"
)
def _build_first_priority_prompt_content(self, blueprint: dict[str, Any]) -> str:
variables = "、".join(str(item) for item in (blueprint.get("variables") or []))
targets = "、".join(str(item) for item in (blueprint.get("quality_targets") or []))
if blueprint.get("prompt_type") == "contract_review":
return (
"请对 DOCUMENT_CONTENT 进行结构化合同审查,必须结合 REVIEW_PERSPECTIVE 输出立场化风险影响、"
"谈判建议、优先处理动作、缺失条款和法条引用。引用只能来自 RAG_RETRIEVED_REGULATIONS,"
f"输出需符合 {blueprint.get('output_schema')};质量目标:{targets};变量:{variables}。"
)
if blueprint.get("prompt_type") == "clause_recommendation":
return (
"请基于 CURRENT_DOCUMENT_CONTENT、DOCUMENT_TYPE 和 REQUIRED_CLAUSE_CHECKLIST 推荐缺失条款,"
"每条建议需包含插入位置、建议文本、法律/行业依据、风险说明和可采纳动作。"
f"输出需符合 {blueprint.get('output_schema')};质量目标:{targets};变量:{variables}。"
)
return (
"请基于 USER_ELEMENTS、RETRIEVED_REGULATIONS 和 REQUIRED_CLAUSES 生成完整法律文书 HTML。"
"必须覆盖主体、标的/服务、价款、交付/验收、违约责任、争议解决、通知送达等关键条款,"
"并在不确定或高价值事项处输出 AI 提示。"
f"文书类型:{blueprint.get('document_type_code')};输出需符合 {blueprint.get('output_schema')};"
f"质量目标:{targets};变量:{variables}。"
)
def _build_first_priority_variables(self, blueprint: dict[str, Any]) -> list[dict[str, Any]]:
return [{"name": str(item), "required": True} for item in (blueprint.get("variables") or [])]
def _build_first_priority_output_schema(self, blueprint: dict[str, Any]) -> dict[str, Any]:
return {
"schema_key": blueprint.get("output_schema"),
"format": "json" if blueprint.get("prompt_type") != "contract_generation" else "html_with_metadata",
"required_quality_targets": blueprint.get("quality_targets") or [],
}
def _require_asset(self, current_user: Dict[str, Any], asset_id: str) -> PromptAsset:
asset = self.repository.get_by_id(asset_id)
if asset is None or asset.tenant_id != current_user["tenant_id"]:
@@ -387,6 +387,28 @@ class PromptAssetServiceTestCase(unittest.TestCase):
self.assertEqual(run_link["run_id"], "run-001")
self.assertEqual(run_link["trace"]["provider"], "shared-ai-platform")
def test_seed_first_priority_blueprint_creates_assets_and_quality_runs(self):
result = self.service.seed_first_priority_blueprint(self.admin_user)
self.assertEqual(result["summary"]["prompt_blueprint_count"], 7)
self.assertEqual(result["summary"]["created"], 7)
self.assertEqual(len(result["results"]), 7)
first = result["results"][0]
self.assertEqual(first["asset"]["status"], "draft")
self.assertEqual(first["version"]["evaluation_status"], "passed")
self.assertEqual(first["evaluation_run"]["status"], "passed")
self.assertEqual(first["evaluation_run"]["sample_count"], len(first["golden_sample_ids"]))
self.assertTrue(first["version"]["evaluation_report"]["sample_results"])
self.assertEqual(len(self.service.quality_eval_service.calls), 7)
def test_seed_first_priority_blueprint_is_idempotent(self):
self.service.seed_first_priority_blueprint(self.admin_user)
second = self.service.seed_first_priority_blueprint(self.admin_user)
self.assertEqual(second["summary"]["reused"], 7)
self.assertEqual(second["summary"]["created"], 0)
self.assertEqual(self.service.repository.count(self.admin_user["tenant_id"]), 7)
def test_member_cannot_manage_prompt_assets(self):
with self.assertRaises(HTTPException) as context:
self.service.list_prompt_assets(self.member_user)
+160 -387
View File
@@ -2,10 +2,39 @@
import Link from "next/link";
import { useMemo, useEffect, useState } from "react";
import { Alert, 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, Popover, Select, Space, Table, Tag, Typography, Upload, message } from "antd";
import type { UploadFile } from "antd/es/upload/interface";
import { useRouter } from "next/navigation";
import {
dueStatusMeta,
formatAutoReviewRisk,
formatAutoReviewStatus,
formatDateTime,
formatDueStatus,
formatFileSize,
formatGenerationQualityStatus,
formatLifecycleStatus,
formatLifecycleTaskPriority,
formatLifecycleTaskStatus,
formatLifecycleTaskType,
getAutoReviewRiskColor,
getAutoReviewStatusColor,
getDueStatusColor,
getGenerationModeMeta,
getGenerationQualityColor,
getLifecycleNextStatuses,
getLifecycleStatusColor,
getLifecycleTaskPriorityColor,
getLifecycleTaskStatusColor,
getLifecycleTaskTypeColor,
isLifecycleTerminal,
lifecycleStatusOptions,
lifecycleTaskPriorityOptions,
lifecycleTaskStatusOptions,
lifecycleTaskTypeOptions,
toDatetimeLocalValue,
} from "@/lib/format";
import {
createContractLifecycleTask,
downloadContract,
@@ -29,15 +58,6 @@ import { useAuthStore } from "@/stores/auth.store";
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
type LifecycleFormValues = {
lifecycle_status: string;
counterparty_name?: string;
owner_name?: string;
effective_at?: string;
expires_at?: string;
lifecycle_note?: string;
};
type LifecycleTaskFormValues = {
task_type: string;
title: string;
@@ -49,23 +69,11 @@ 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;
const source = detail?.metadata.generation_source;
return [
...(source?.fallback_used ? [`AI 增强生成已回退:${source.fallback_reason ?? "请重点复核生成内容"}`] : []),
...(quality?.blocking_issues ?? []),
...(quality?.warning_issues ?? []),
...(detail?.clause_completion_suggestions ?? []).map((item) => `${item.title}:${item.recommendation}`),
@@ -81,251 +89,6 @@ function groupClauseSuggestions(detail?: GeneratedContract | null) {
}
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 "-";
}
return new Date(value).toLocaleString("zh-CN", { hour12: false });
}
function formatFileSize(value: number | null | undefined) {
if (value == null) {
return "-";
}
return `${(value / 1024).toFixed(2)} KB`;
}
const autoReviewStatusLabelMap: Record<string, string> = {
completed: "已完成",
processing: "处理中",
pending: "待处理",
failed: "失败",
partial: "部分完成",
};
const autoReviewStatusColorMap: Record<string, string> = {
completed: "green",
processing: "blue",
pending: "gold",
failed: "red",
partial: "orange",
};
const autoReviewRiskLabelMap: Record<string, string> = {
high: "高风险",
medium: "中风险",
low: "低风险",
};
const autoReviewRiskColorMap: Record<string, string> = {
high: "red",
medium: "gold",
low: "blue",
};
const lifecycleStatusOptions = [
{ value: "generated", label: "已生成" },
{ value: "internal_review", label: "内部审查" },
{ value: "external_review", label: "外部审查" },
{ value: "signed", label: "已签署" },
{ value: "effective", label: "履约中" },
{ value: "expiring", label: "即将到期" },
{ value: "expired", label: "已到期" },
{ value: "terminated", label: "已终止" },
{ value: "archived", label: "已归档" },
];
const lifecycleStatusMeta: Record<string, { label: string; color: string }> = {
generated: { label: "已生成", color: "default" },
internal_review: { label: "内部审查", color: "blue" },
external_review: { label: "外部审查", color: "geekblue" },
signed: { label: "已签署", color: "cyan" },
effective: { label: "履约中", color: "green" },
expiring: { label: "即将到期", color: "gold" },
expired: { label: "已到期", color: "red" },
terminated: { label: "已终止", color: "volcano" },
archived: { label: "已归档", color: "default" },
};
const dueStatusMeta: Record<string, { label: string; color: string }> = {
not_set: { label: "未设置期限", color: "default" },
normal: { label: "期限正常", color: "green" },
due_soon: { label: "即将到期", color: "gold" },
overdue: { label: "已逾期", color: "red" },
closed: { label: "已关闭", color: "default" },
};
const lifecycleTaskTypeOptions = [
{ value: "renewal_reminder", label: "续约提醒" },
{ value: "performance_milestone", label: "履约节点" },
{ value: "termination_archive", label: "终止归档" },
{ value: "custom", label: "自定义任务" },
];
const lifecycleTaskTypeMeta: Record<string, { label: string; color: string }> = {
renewal_reminder: { label: "续约提醒", color: "gold" },
performance_milestone: { label: "履约节点", color: "blue" },
termination_archive: { label: "终止归档", color: "volcano" },
custom: { label: "自定义任务", color: "default" },
};
const lifecycleTaskStatusOptions = [
{ value: "open", label: "待处理" },
{ value: "in_progress", label: "处理中" },
{ value: "completed", label: "已完成" },
{ value: "skipped", label: "已跳过" },
{ value: "archived", label: "已归档" },
];
const lifecycleTaskStatusMeta: Record<string, { label: string; color: string }> = {
open: { label: "待处理", color: "gold" },
in_progress: { label: "处理中", color: "blue" },
completed: { label: "已完成", color: "green" },
skipped: { label: "已跳过", color: "default" },
archived: { label: "已归档", color: "default" },
};
const lifecycleTaskPriorityOptions = [
{ value: "low", label: "低" },
{ value: "medium", label: "中" },
{ value: "high", label: "高" },
{ value: "critical", label: "紧急" },
];
const lifecycleTaskPriorityMeta: Record<string, { label: string; color: string }> = {
low: { label: "低", color: "default" },
medium: { label: "中", color: "blue" },
high: { label: "高", color: "orange" },
critical: { label: "紧急", color: "red" },
};
function formatAutoReviewStatus(value: string | null | undefined) {
if (!value) {
return "未触发";
}
return autoReviewStatusLabelMap[value] ?? value;
}
function getAutoReviewStatusColor(value: string | null | undefined) {
if (!value) {
return "default";
}
return autoReviewStatusColorMap[value] ?? "default";
}
function formatAutoReviewRisk(value: string | null | undefined) {
if (!value) {
return "未评级";
}
return autoReviewRiskLabelMap[value] ?? value;
}
function getAutoReviewRiskColor(value: string | null | undefined) {
if (!value) {
return "default";
}
return autoReviewRiskColorMap[value] ?? "default";
}
function formatLifecycleStatus(value: string | null | undefined) {
if (!value) {
return "未设置";
}
return lifecycleStatusMeta[value]?.label ?? value;
}
function getLifecycleStatusColor(value: string | null | undefined) {
if (!value) {
return "default";
}
return lifecycleStatusMeta[value]?.color ?? "default";
}
function formatDueStatus(lifecycle?: ContractLifecycle | null) {
if (!lifecycle) {
return "未设置期限";
}
const baseLabel = dueStatusMeta[lifecycle.due_status]?.label ?? lifecycle.due_status;
if (typeof lifecycle.days_to_expiry === "number" && lifecycle.due_status !== "closed") {
return `${baseLabel}(${lifecycle.days_to_expiry}天)`;
}
return baseLabel;
}
function getDueStatusColor(value: string | null | undefined) {
if (!value) {
return "default";
}
return dueStatusMeta[value]?.color ?? "default";
}
function formatLifecycleTaskType(value: string | null | undefined) {
if (!value) {
return "未分类";
}
return lifecycleTaskTypeMeta[value]?.label ?? value;
}
function getLifecycleTaskTypeColor(value: string | null | undefined) {
if (!value) {
return "default";
}
return lifecycleTaskTypeMeta[value]?.color ?? "default";
}
function formatLifecycleTaskStatus(value: string | null | undefined) {
if (!value) {
return "未设置";
}
return lifecycleTaskStatusMeta[value]?.label ?? value;
}
function getLifecycleTaskStatusColor(value: string | null | undefined) {
if (!value) {
return "default";
}
return lifecycleTaskStatusMeta[value]?.color ?? "default";
}
function formatLifecycleTaskPriority(value: string | null | undefined) {
if (!value) {
return "中";
}
return lifecycleTaskPriorityMeta[value]?.label ?? value;
}
function getLifecycleTaskPriorityColor(value: string | null | undefined) {
if (!value) {
return "blue";
}
return lifecycleTaskPriorityMeta[value]?.color ?? "default";
}
function toDatetimeLocalValue(value: string | null | undefined) {
if (!value) {
return undefined;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return undefined;
}
const pad = (input: number) => String(input).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function buildContractReadabilityItems(
detail: GeneratedContract | null,
autoReview: AutoReviewSummary | null,
@@ -374,7 +137,6 @@ function buildContractChatHref(
export default function GeneratedContractDetailPage({ params }: { params: Promise<{ id: string }> }) {
const [messageApi, contextHolder] = message.useMessage();
const [lifecycleForm] = Form.useForm<LifecycleFormValues>();
const [lifecycleTaskForm] = Form.useForm<LifecycleTaskFormValues>();
const router = useRouter();
const currentUser = useAuthStore((state) => state.currentUser);
@@ -391,8 +153,9 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
const [evidenceUploadList, setEvidenceUploadList] = useState<UploadFile[]>([]);
const [notFound, setNotFound] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [lifecycleModalOpen, setLifecycleModalOpen] = useState(false);
const [lifecycleUpdating, setLifecycleUpdating] = useState(false);
const [showTechnicalDetails, setShowTechnicalDetails] = useState(false);
const [showVariableDetails, setShowVariableDetails] = useState(false);
useEffect(() => {
const loadDetail = async () => {
@@ -465,7 +228,7 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
() => buildContractReadabilityItems(detail, autoReview),
[detail, autoReview],
);
const generationModeMeta = useMemo(() => getGenerationModeMeta(detail), [detail]);
const generationModeMeta = useMemo(() => getGenerationModeMeta(detail?.metadata), [detail]);
const groupedClauseSuggestions = useMemo(() => groupClauseSuggestions(detail), [detail]);
const handleDownload = async () => {
@@ -563,45 +326,21 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
router.push(`/contracts?restore=${restorePayload}`);
};
const handleOpenLifecycleModal = () => {
if (!detail) {
return;
}
lifecycleForm.setFieldsValue({
lifecycle_status: detail.lifecycle?.status ?? "generated",
counterparty_name: detail.lifecycle?.counterparty_name ?? undefined,
owner_name: detail.lifecycle?.owner_name ?? undefined,
effective_at: toDatetimeLocalValue(detail.lifecycle?.effective_at),
expires_at: toDatetimeLocalValue(detail.lifecycle?.expires_at),
lifecycle_note: detail.lifecycle?.note ?? undefined,
});
setLifecycleModalOpen(true);
};
const handleUpdateLifecycle = async () => {
if (!detail) {
return;
}
const values = await lifecycleForm.validateFields();
const handleLifecycleTransition = async (newStatus: string) => {
if (!detail || lifecycleUpdating) return;
setLifecycleUpdating(true);
try {
const result = await updateGeneratedContractLifecycle(detail.id, {
lifecycle_status: values.lifecycle_status,
counterparty_name: values.counterparty_name || null,
owner_name: values.owner_name || null,
effective_at: values.effective_at || null,
expires_at: values.expires_at || null,
lifecycle_note: values.lifecycle_note || null,
lifecycle_status: newStatus,
});
if (result.code !== 0) {
messageApi.error(result.message || "生命周期更新失败");
messageApi.error(result.message || "状态更新失败");
return;
}
setDetail(result.data);
setLifecycleModalOpen(false);
messageApi.success("生命周期状态已更新");
messageApi.success(`生命周期已更新为 ${formatLifecycleStatus(newStatus)}`);
} catch (error: unknown) {
messageApi.error(error instanceof Error ? error.message : "生命周期更新失败,请稍后重试");
messageApi.error(error instanceof Error ? error.message : "状态更新失败,请稍后重试");
} finally {
setLifecycleUpdating(false);
}
@@ -811,9 +550,44 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
<Button type="primary" onClick={() => router.push(`/documents/${detail?.id}/edit`)}>
进入合同编辑器
</Button>
<Button onClick={handleOpenLifecycleModal}>
更新生命周期
</Button>
<Popover
trigger="click"
placement="bottom"
title={
<Space>
<span>生命周期</span>
<Tag color={getLifecycleStatusColor(detail?.lifecycle?.status)}>
{formatLifecycleStatus(detail?.lifecycle?.status)}
</Tag>
</Space>
}
content={
isLifecycleTerminal(detail?.lifecycle?.status) ? (
<div style={{ width: 200, textAlign: "center", padding: "8px 0" }}>
<Text type="secondary">合同已归档,不可再流转</Text>
</div>
) : (
<Space direction="vertical" size="small" style={{ width: 220 }}>
<Text type="secondary" style={{ fontSize: 12 }}>流转至:</Text>
{getLifecycleNextStatuses(detail?.lifecycle?.status).map((next) => (
<Button
key={next.value}
size="small"
block
loading={lifecycleUpdating}
onClick={() => void handleLifecycleTransition(next.value)}
>
{next.label}
</Button>
))}
</Space>
)
}
>
<Button>
更新生命周期
</Button>
</Popover>
{currentUser?.role === "admin" && detail?.file_id ? <Button onClick={() => router.push(`/files?highlight=${detail.file_id}`)}>查看文件资产</Button> : null}
<Button loading={downloading} onClick={handleDownload}>
下载合同文件
@@ -1071,26 +845,49 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
/>
</Card>
<Card className="review-surface-card" loading={loading} title="基本信息">
<Card
className="review-surface-card"
loading={loading}
title={
<Space
style={{ cursor: "pointer" }}
onClick={() => setShowTechnicalDetails(prev => !prev)}
>
<span>基本信息</span>
<Button size="small" type="text">
{showTechnicalDetails ? "收起技术详情 ▾" : "展开技术详情 ▸"}
</Button>
</Space>
}
>
{detail ? (
<Descriptions column={2} bordered>
<Descriptions.Item label="合同ID">{detail.id}</Descriptions.Item>
<Descriptions.Item label="模板ID">{detail.template_id}</Descriptions.Item>
<Descriptions.Item label="模板名称">{detail.template_name}</Descriptions.Item>
<Descriptions.Item label="合同类型">
<Tag color="purple">{detail.contract_type || "未分类"}</Tag>
</Descriptions.Item>
<Descriptions.Item label="文件名">{detail.file_name}</Descriptions.Item>
<Descriptions.Item label="文件大小">{formatFileSize(detail.file_size)}</Descriptions.Item>
<Descriptions.Item label="资产文件ID">{detail.file_id ?? "-"}</Descriptions.Item>
<Descriptions.Item label="导出格式">{detail.export_format.toUpperCase()}</Descriptions.Item>
<Descriptions.Item label="生成人员">{detail.metadata.generated_by_name}</Descriptions.Item>
<Descriptions.Item label="生成时间">{formatDateTime(detail.generated_at)}</Descriptions.Item>
<Descriptions.Item label="变量数量">{detail.metadata.variables_used}</Descriptions.Item>
<Descriptions.Item label="生命周期状态">
<Tag color={getLifecycleStatusColor(detail.lifecycle?.status)}>{formatLifecycleStatus(detail.lifecycle?.status)}</Tag>
</Descriptions.Item>
</Descriptions>
<Space orientation="vertical" size="middle" style={{ width: "100%" }}>
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="模板名称">
<Text strong>{detail.template_name}</Text>
</Descriptions.Item>
<Descriptions.Item label="合同类型">
<Tag color="purple">{detail.contract_type || "未分类"}</Tag>
</Descriptions.Item>
<Descriptions.Item label="导出格式">{detail.export_format.toUpperCase()}</Descriptions.Item>
<Descriptions.Item label="文件名称">{detail.file_name}</Descriptions.Item>
<Descriptions.Item label="生成人员">{detail.metadata.generated_by_name}</Descriptions.Item>
<Descriptions.Item label="生成时间">{formatDateTime(detail.generated_at)}</Descriptions.Item>
<Descriptions.Item label="生命周期状态">
<Tag color={getLifecycleStatusColor(detail.lifecycle?.status)}>{formatLifecycleStatus(detail.lifecycle?.status)}</Tag>
</Descriptions.Item>
</Descriptions>
{showTechnicalDetails ? (
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="合同ID"><Text copyable>{detail.id}</Text></Descriptions.Item>
<Descriptions.Item label="模板ID"><Text copyable>{detail.template_id}</Text></Descriptions.Item>
<Descriptions.Item label="文件大小">{formatFileSize(detail.file_size)}</Descriptions.Item>
<Descriptions.Item label="资产文件ID">{detail.file_id ?? "-"}</Descriptions.Item>
<Descriptions.Item label="变量数量">{detail.metadata.variables_used}</Descriptions.Item>
</Descriptions>
) : null}
</Space>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无合同详情。" />
)}
@@ -1138,42 +935,48 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
) : null}
<div className="contract-detail-two-column">
<Card className="review-surface-card" loading={loading} title="变量回显">
<Table
rowKey="key"
pagination={false}
dataSource={variableRows}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无变量数据。" /> }}
columns={[
{ title: "变量名", dataIndex: "name", key: "name", width: 240 },
{
title: "变量值",
dataIndex: "value",
key: "value",
render: (value: string) => <Paragraph style={{ marginBottom: 0, whiteSpace: "pre-wrap" }}>{value || "-"}</Paragraph>,
},
{
title: "状态",
dataIndex: "isEmpty",
key: "isEmpty",
width: 120,
render: (isEmpty: boolean) => <Tag color={isEmpty ? "gold" : "green"}>{isEmpty ? "空值" : "已填写"}</Tag>,
},
]}
/>
<Card
className="review-surface-card"
loading={loading}
title={
<Space style={{ cursor: "pointer" }} onClick={() => setShowVariableDetails(prev => !prev)}>
<span>变量回显</span>
<Button size="small" type="text">
{showVariableDetails ? "收起 ▾" : "展开 ▸"}
</Button>
</Space>
}
>
{showVariableDetails ? (
<Table
rowKey="key"
pagination={false}
dataSource={variableRows}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无变量数据。" /> }}
columns={[
{ title: "变量名", dataIndex: "name", key: "name", width: 240 },
{
title: "变量值",
dataIndex: "value",
key: "value",
render: (value: string) => <Paragraph style={{ marginBottom: 0, whiteSpace: "pre-wrap" }}>{value || "-"}</Paragraph>,
},
{
title: "状态",
dataIndex: "isEmpty",
key: "isEmpty",
width: 120,
render: (isEmpty: boolean) => <Tag color={isEmpty ? "gold" : "green"}>{isEmpty ? "空值" : "已填写"}</Tag>,
},
]}
/>
) : (
<Text type="secondary">已隐藏,点击展开查看变量详情(共 {variableRows.length} 项)</Text>
)}
</Card>
<Card className="review-surface-card" loading={loading} title="导出选项">
<Table
rowKey="key"
pagination={false}
dataSource={exportOptionRows}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无导出选项。" /> }}
columns={[
{ title: "选项名", dataIndex: "name", key: "name", width: 180 },
{ title: "选项值", dataIndex: "value", key: "value" },
]}
/>
<Text type="secondary">已隐藏,点击展开查看导出选项详情</Text>
</Card>
</div>
@@ -1194,36 +997,6 @@ export default function GeneratedContractDetailPage({ params }: { params: Promis
)}
</Card>
</div>
<Modal
title="更新合同生命周期"
open={lifecycleModalOpen}
okText="保存状态"
cancelText="取消"
confirmLoading={lifecycleUpdating}
onOk={() => void handleUpdateLifecycle()}
onCancel={() => setLifecycleModalOpen(false)}
>
<Form form={lifecycleForm} layout="vertical" initialValues={{ lifecycle_status: "generated" }}>
<Form.Item label="生命周期状态" name="lifecycle_status" rules={[{ required: true, message: "请选择生命周期状态" }]}>
<Select options={lifecycleStatusOptions} />
</Form.Item>
<Form.Item label="合同相对方" name="counterparty_name">
<Input placeholder="例如:供应商A、客户B" maxLength={120} />
</Form.Item>
<Form.Item label="内部负责人" name="owner_name">
<Input placeholder="例如:法务运营、销售负责人" maxLength={100} />
</Form.Item>
<Form.Item label="生效时间" name="effective_at">
<Input type="datetime-local" />
</Form.Item>
<Form.Item label="到期时间" name="expires_at">
<Input type="datetime-local" />
</Form.Item>
<Form.Item label="履约备注" name="lifecycle_note">
<TextArea rows={3} placeholder="记录签署、续约、终止或跟进说明" maxLength={1000} showCount />
</Form.Item>
</Form>
</Modal>
<Modal
title={editingLifecycleTask ? "编辑履约任务" : "新增履约任务"}
open={lifecycleTaskModalOpen}
+23
View File
@@ -7,6 +7,7 @@ import {
HistoryOutlined,
LinkOutlined,
PlusOutlined,
RocketOutlined,
RollbackOutlined,
StopOutlined,
} from "@ant-design/icons";
@@ -23,6 +24,7 @@ import {
recordPromptRunLink,
rollbackPromptAsset,
runPromptAssetEvaluation,
seedFirstPriorityPromptBlueprint,
unpublishPromptAsset,
} from "@/services/prompt-asset";
import { useAuthStore } from "@/stores/auth.store";
@@ -390,6 +392,24 @@ export default function PromptAssetsPage() {
}
};
const handleSeedFirstPriorityBlueprint = async () => {
setSubmitting(true);
try {
const result = await seedFirstPriorityPromptBlueprint();
if (result.code !== 0) {
messageApi.error(result.message || "第一优先级 Prompt 种入失败");
return;
}
const summary = result.data.summary;
messageApi.success(`已种入 ${summary.prompt_blueprint_count} 个 Prompt,新增 ${summary.created} 个,更新 ${summary.updated} 个,复用 ${summary.reused} 个`);
await loadAssets(1, pageSize, promptTypeFilter, statusFilter);
} catch (error) {
messageApi.error(error instanceof Error ? error.message : "第一优先级 Prompt 种入失败");
} finally {
setSubmitting(false);
}
};
const handleRecordRunLink = async (values: RunLinkFormValues) => {
if (!selectedDetail) {
return;
@@ -706,6 +726,9 @@ export default function PromptAssetsPage() {
title="Prompt 资产列表"
extra={
<Space wrap>
<Button icon={<RocketOutlined />} loading={submitting} onClick={() => void handleSeedFirstPriorityBlueprint()}>
种入第一优先级
</Button>
<Select
value={promptTypeFilter}
style={{ width: 180 }}