feat: add validation and toc pipeline upgrades
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
e028e4fa96
commit
c1292fcacc
@ -1,501 +0,0 @@
|
||||
{
|
||||
"project_name": "111",
|
||||
"source_file": "C:\\Users\\sladr\\Downloads\\test2.docx",
|
||||
"success": true,
|
||||
"technical_count": 6,
|
||||
"commercial_count": 3,
|
||||
"deviation_count": 0,
|
||||
"chapter_count": 7,
|
||||
"execution_time": 415.01412439346313,
|
||||
"warnings": [
|
||||
"AI审查: 5条优化建议"
|
||||
],
|
||||
"bid_structure": {
|
||||
"scoring_criteria": [
|
||||
{
|
||||
"item_name": "质保期",
|
||||
"max_score": 3.0,
|
||||
"description": "售后质保期2年,得1分。售后质保期延长至3年,得2分。售后质保期延长至4年,得3分。",
|
||||
"category": "after_sales",
|
||||
"chapter_id": "chapter_02"
|
||||
},
|
||||
{
|
||||
"item_name": "硬件配置",
|
||||
"max_score": 8.0,
|
||||
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分,扣完为止。(招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供,否则评委不予采信)",
|
||||
"category": "equipment_spec",
|
||||
"chapter_id": "chapter_04"
|
||||
},
|
||||
{
|
||||
"item_name": "软件功能",
|
||||
"max_score": 8.0,
|
||||
"description": "应按照磋商文件功能要求实现,各项软件功能完成率,软件功能按每1项不满足扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"chapter_id": "chapter_05"
|
||||
},
|
||||
{
|
||||
"item_name": "系统架构",
|
||||
"max_score": 5.0,
|
||||
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明,缺少1项扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"chapter_id": "chapter_06"
|
||||
},
|
||||
{
|
||||
"item_name": "网路安全防护",
|
||||
"max_score": 5.0,
|
||||
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况,可以综合评估组织的网络安全防护水平,缺少1项扣1分,扣完为止。",
|
||||
"category": "quality_safety",
|
||||
"chapter_id": "chapter_07"
|
||||
},
|
||||
{
|
||||
"item_name": "技术实力",
|
||||
"max_score": 3.0,
|
||||
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利,每提供一个得1分,共3分。(提供证书原件的扫描件并加盖公章)",
|
||||
"category": "compliance",
|
||||
"chapter_id": "chapter_08"
|
||||
}
|
||||
],
|
||||
"deviation_items": [],
|
||||
"chapters": [
|
||||
{
|
||||
"id": "chapter_1",
|
||||
"title": "评标索引表(技术评分完全对应)",
|
||||
"level": 1,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2",
|
||||
"title": "设备规格 (8.0分)",
|
||||
"level": 1,
|
||||
"score": 8.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_2_1",
|
||||
"title": "硬件配置方案",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_2_1_1",
|
||||
"title": "核心硬件技术参数",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2_1_2",
|
||||
"title": "设备性能指标说明",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2_1_3",
|
||||
"title": "硬件兼容性分析",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2_1_4",
|
||||
"title": "配置方案优势阐述",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_3",
|
||||
"title": "技术方案 (13.0分)",
|
||||
"level": 1,
|
||||
"score": 13.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_3_1",
|
||||
"title": "软件功能设计",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_3_1_1",
|
||||
"title": "核心功能模块设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_1_2",
|
||||
"title": "用户交互界面设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_1_3",
|
||||
"title": "数据处理与分析功能",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_1_4",
|
||||
"title": "系统集成与接口设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_2",
|
||||
"title": "系统架构设计",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_3_2_1",
|
||||
"title": "总体架构设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_2_2",
|
||||
"title": "技术选型与框架设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_2_3",
|
||||
"title": "性能与扩展性设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_4",
|
||||
"title": "质量安全 (5.0分)",
|
||||
"level": 1,
|
||||
"score": 5.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_4_1",
|
||||
"title": "网络安全防护体系",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_4_1_1",
|
||||
"title": "网络安全架构设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_2",
|
||||
"title": "网络安全防护措施",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_3",
|
||||
"title": "网络安全监控与预警",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_4",
|
||||
"title": "网络安全应急响应",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_5",
|
||||
"title": "网络安全管理制度",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_5",
|
||||
"title": "合规响应 (3.0分)",
|
||||
"level": 1,
|
||||
"score": 3.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_5_1",
|
||||
"title": "技术实力",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_5_1_1",
|
||||
"title": "核心技术团队介绍",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_2",
|
||||
"title": "技术研发能力展示",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_3",
|
||||
"title": "技术设备与工具配置",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_4",
|
||||
"title": "技术成果与专利情况",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_5",
|
||||
"title": "技术质量管理体系",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_6",
|
||||
"title": "培训服务",
|
||||
"level": 1,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_6_1",
|
||||
"title": "培训计划",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_6_2",
|
||||
"title": "培训内容",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_6_3",
|
||||
"title": "培训方式",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7",
|
||||
"title": "售后服务 (3.0分)",
|
||||
"level": 1,
|
||||
"score": 3.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_1",
|
||||
"title": "质保期服务承诺",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_1_1",
|
||||
"title": "质保期限与范围",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_1_2",
|
||||
"title": "质保期内服务内容",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_1_3",
|
||||
"title": "质保期响应机制",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_1_4",
|
||||
"title": "质保期满后服务延续方案",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_2",
|
||||
"title": "技术支持服务",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_2_1",
|
||||
"title": "技术支持团队配置",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_2_2",
|
||||
"title": "技术支持响应时间",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_2_3",
|
||||
"title": "技术支持服务方式",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_3",
|
||||
"title": "维护服务",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_3_1",
|
||||
"title": "定期维护计划",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_3_2",
|
||||
"title": "故障处理流程",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_3_3",
|
||||
"title": "备品备件保障",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_4",
|
||||
"title": "客户服务保障",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_4_1",
|
||||
"title": "客户服务热线",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_4_2",
|
||||
"title": "客户满意度调查",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_4_3",
|
||||
"title": "客户投诉处理机制",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "质保期",
|
||||
"chapter_id": "chapter_02",
|
||||
"score": 3.0,
|
||||
"description": "售后质保期2年,得1分。售后质保期延长至3年,得2分。售后质保期延长至4年,得3分。",
|
||||
"category": "after_sales",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_02_content}}"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "硬件配置",
|
||||
"chapter_id": "chapter_04",
|
||||
"score": 8.0,
|
||||
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分,扣完为止。(招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供,否则评委不予采信)",
|
||||
"category": "equipment_spec",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_04_content}}"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "软件功能",
|
||||
"chapter_id": "chapter_05",
|
||||
"score": 8.0,
|
||||
"description": "应按照磋商文件功能要求实现,各项软件功能完成率,软件功能按每1项不满足扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_05_content}}"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "系统架构",
|
||||
"chapter_id": "chapter_06",
|
||||
"score": 5.0,
|
||||
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明,缺少1项扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_06_content}}"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "网路安全防护",
|
||||
"chapter_id": "chapter_07",
|
||||
"score": 5.0,
|
||||
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况,可以综合评估组织的网络安全防护水平,缺少1项扣1分,扣完为止。",
|
||||
"category": "quality_safety",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_07_content}}"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "技术实力",
|
||||
"chapter_id": "chapter_08",
|
||||
"score": 3.0,
|
||||
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利,每提供一个得1分,共3分。(提供证书原件的扫描件并加盖公章)",
|
||||
"category": "compliance",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_08_content}}"
|
||||
}
|
||||
]
|
||||
@ -1,501 +0,0 @@
|
||||
{
|
||||
"project_name": "111",
|
||||
"source_file": "C:\\Users\\sladr\\Downloads\\test2.docx",
|
||||
"success": true,
|
||||
"technical_count": 6,
|
||||
"commercial_count": 3,
|
||||
"deviation_count": 0,
|
||||
"chapter_count": 7,
|
||||
"execution_time": 415.01412439346313,
|
||||
"warnings": [
|
||||
"AI审查: 5条优化建议"
|
||||
],
|
||||
"bid_structure": {
|
||||
"scoring_criteria": [
|
||||
{
|
||||
"item_name": "质保期",
|
||||
"max_score": 3.0,
|
||||
"description": "售后质保期2年,得1分。售后质保期延长至3年,得2分。售后质保期延长至4年,得3分。",
|
||||
"category": "after_sales",
|
||||
"chapter_id": "chapter_02"
|
||||
},
|
||||
{
|
||||
"item_name": "硬件配置",
|
||||
"max_score": 8.0,
|
||||
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分,扣完为止。(招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供,否则评委不予采信)",
|
||||
"category": "equipment_spec",
|
||||
"chapter_id": "chapter_04"
|
||||
},
|
||||
{
|
||||
"item_name": "软件功能",
|
||||
"max_score": 8.0,
|
||||
"description": "应按照磋商文件功能要求实现,各项软件功能完成率,软件功能按每1项不满足扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"chapter_id": "chapter_05"
|
||||
},
|
||||
{
|
||||
"item_name": "系统架构",
|
||||
"max_score": 5.0,
|
||||
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明,缺少1项扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"chapter_id": "chapter_06"
|
||||
},
|
||||
{
|
||||
"item_name": "网路安全防护",
|
||||
"max_score": 5.0,
|
||||
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况,可以综合评估组织的网络安全防护水平,缺少1项扣1分,扣完为止。",
|
||||
"category": "quality_safety",
|
||||
"chapter_id": "chapter_07"
|
||||
},
|
||||
{
|
||||
"item_name": "技术实力",
|
||||
"max_score": 3.0,
|
||||
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利,每提供一个得1分,共3分。(提供证书原件的扫描件并加盖公章)",
|
||||
"category": "compliance",
|
||||
"chapter_id": "chapter_08"
|
||||
}
|
||||
],
|
||||
"deviation_items": [],
|
||||
"chapters": [
|
||||
{
|
||||
"id": "chapter_1",
|
||||
"title": "评标索引表(技术评分完全对应)",
|
||||
"level": 1,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2",
|
||||
"title": "设备规格 (8.0分)",
|
||||
"level": 1,
|
||||
"score": 8.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_2_1",
|
||||
"title": "硬件配置方案",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_2_1_1",
|
||||
"title": "核心硬件技术参数",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2_1_2",
|
||||
"title": "设备性能指标说明",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2_1_3",
|
||||
"title": "硬件兼容性分析",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_2_1_4",
|
||||
"title": "配置方案优势阐述",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_3",
|
||||
"title": "技术方案 (13.0分)",
|
||||
"level": 1,
|
||||
"score": 13.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_3_1",
|
||||
"title": "软件功能设计",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_3_1_1",
|
||||
"title": "核心功能模块设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_1_2",
|
||||
"title": "用户交互界面设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_1_3",
|
||||
"title": "数据处理与分析功能",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_1_4",
|
||||
"title": "系统集成与接口设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_2",
|
||||
"title": "系统架构设计",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_3_2_1",
|
||||
"title": "总体架构设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_2_2",
|
||||
"title": "技术选型与框架设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_3_2_3",
|
||||
"title": "性能与扩展性设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_4",
|
||||
"title": "质量安全 (5.0分)",
|
||||
"level": 1,
|
||||
"score": 5.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_4_1",
|
||||
"title": "网络安全防护体系",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_4_1_1",
|
||||
"title": "网络安全架构设计",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_2",
|
||||
"title": "网络安全防护措施",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_3",
|
||||
"title": "网络安全监控与预警",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_4",
|
||||
"title": "网络安全应急响应",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_4_1_5",
|
||||
"title": "网络安全管理制度",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_5",
|
||||
"title": "合规响应 (3.0分)",
|
||||
"level": 1,
|
||||
"score": 3.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_5_1",
|
||||
"title": "技术实力",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_5_1_1",
|
||||
"title": "核心技术团队介绍",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_2",
|
||||
"title": "技术研发能力展示",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_3",
|
||||
"title": "技术设备与工具配置",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_4",
|
||||
"title": "技术成果与专利情况",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_5_1_5",
|
||||
"title": "技术质量管理体系",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_6",
|
||||
"title": "培训服务",
|
||||
"level": 1,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_6_1",
|
||||
"title": "培训计划",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_6_2",
|
||||
"title": "培训内容",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_6_3",
|
||||
"title": "培训方式",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7",
|
||||
"title": "售后服务 (3.0分)",
|
||||
"level": 1,
|
||||
"score": 3.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_1",
|
||||
"title": "质保期服务承诺",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_1_1",
|
||||
"title": "质保期限与范围",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_1_2",
|
||||
"title": "质保期内服务内容",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_1_3",
|
||||
"title": "质保期响应机制",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_1_4",
|
||||
"title": "质保期满后服务延续方案",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_2",
|
||||
"title": "技术支持服务",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_2_1",
|
||||
"title": "技术支持团队配置",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_2_2",
|
||||
"title": "技术支持响应时间",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_2_3",
|
||||
"title": "技术支持服务方式",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_3",
|
||||
"title": "维护服务",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_3_1",
|
||||
"title": "定期维护计划",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_3_2",
|
||||
"title": "故障处理流程",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_3_3",
|
||||
"title": "备品备件保障",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_4",
|
||||
"title": "客户服务保障",
|
||||
"level": 2,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": [
|
||||
{
|
||||
"id": "chapter_7_4_1",
|
||||
"title": "客户服务热线",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_4_2",
|
||||
"title": "客户满意度调查",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "chapter_7_4_3",
|
||||
"title": "客户投诉处理机制",
|
||||
"level": 3,
|
||||
"score": 0.0,
|
||||
"template_placeholder": null,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "质保期",
|
||||
"chapter_id": "chapter_02",
|
||||
"score": 3.0,
|
||||
"description": "售后质保期2年,得1分。售后质保期延长至3年,得2分。售后质保期延长至4年,得3分。",
|
||||
"category": "after_sales",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_02_content}}"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "硬件配置",
|
||||
"chapter_id": "chapter_04",
|
||||
"score": 8.0,
|
||||
"description": "以确保设备性配置不低于招标文件硬件配置要求。打分标准设备硬件的品牌、硬件配置高低、技术先进性、稳定性、兼容性以及售后服务质量等每项指标综合打分。硬件未满足技术要求按每1项不满足扣1分,扣完为止。(招标文件中参数要求提供的检测报告、功能截图、证书等证明材料供应商必须按要求提供,否则评委不予采信)",
|
||||
"category": "equipment_spec",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_04_content}}"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "软件功能",
|
||||
"chapter_id": "chapter_05",
|
||||
"score": 8.0,
|
||||
"description": "应按照磋商文件功能要求实现,各项软件功能完成率,软件功能按每1项不满足扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_05_content}}"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "系统架构",
|
||||
"chapter_id": "chapter_06",
|
||||
"score": 5.0,
|
||||
"description": "对系统架构、实施内容、技术特点、安装规范、售后服务等方面进行说明,缺少1项扣1分,扣完为止。",
|
||||
"category": "technical_solution",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_06_content}}"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "网路安全防护",
|
||||
"chapter_id": "chapter_07",
|
||||
"score": 5.0,
|
||||
"description": "1.防火墙配置、入侵检测系统、安全更新和补丁管理、用户权限管理、安全培训和意识提升、数据加密根据以上各项内容的打分情况,可以综合评估组织的网络安全防护水平,缺少1项扣1分,扣完为止。",
|
||||
"category": "quality_safety",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_07_content}}"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "技术实力",
|
||||
"chapter_id": "chapter_08",
|
||||
"score": 3.0,
|
||||
"description": "投标人具有自主研发的实时视频分析平台系统的知识产权和AI识别方法的发明专利,每提供一个得1分,共3分。(提供证书原件的扫描件并加盖公章)",
|
||||
"category": "compliance",
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": "{{chapter_08_content}}"
|
||||
}
|
||||
]
|
||||
@ -286,6 +286,9 @@ content_prompts:
|
||||
上下文参考:
|
||||
{context_summary}
|
||||
|
||||
重点提示:
|
||||
{guidance_part}
|
||||
|
||||
评分说明:
|
||||
{requirements_summary}{emphasis_part}{rag_part}
|
||||
|
||||
|
||||
128
doc/江南造船厂前端.md
128
doc/江南造船厂前端.md
@ -1,128 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于Three.js的3D模型查看器和船厂三维数据管理展示系统。项目包含两个主要页面:
|
||||
- `index.html`: 主要的3D模型查看器界面,支持模型加载、选择、测量、标注等功能
|
||||
- `1.html`: 船舶建造精度数字化平台的数据分析界面,通过iframe嵌入3D查看器
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
- **Three.js**: 核心3D渲染引擎
|
||||
- **原生JavaScript**: ES6+ 模块化架构,无框架依赖
|
||||
- **HTML5/CSS3**: 响应式布局,使用Flexbox和Grid
|
||||
- **Tailwind CSS**: 样式框架(仅在1.html中使用)
|
||||
- **ECharts**: 数据可视化图表(仅在1.html中使用)
|
||||
|
||||
### 核心模块结构
|
||||
|
||||
所有JavaScript模块位于 `js/` 目录下,采用模块化设计:
|
||||
|
||||
#### 核心管理器
|
||||
- **ModelViewer** (`main.js`): 主应用入口,管理整个3D场景
|
||||
- **ModelManager** (`ModelManager.js`): 模型管理,处理模型的加载和切换
|
||||
- **ModelLoader** (`ModelLoader.js`): 模型加载器,支持GLTF格式
|
||||
- **ModelAPI** (`ModelAPI.js`): 模型API接口,提供程序化控制
|
||||
|
||||
#### 功能管理器
|
||||
- **SelectionManager** (`SelectionManager.js`): 零件选择和高亮
|
||||
- **MeasurementManager** (`MeasurementManager.js`): 3D测量工具(距离、角度、面积)
|
||||
- **ClippingManager** (`ClippingManager.js`): 切面控制
|
||||
- **AnnotationManager** (`AnnotationManager.js`): 3D标注功能
|
||||
- **ModelTreeManager** (`ModelTreeManager.js`): 模型结构树管理
|
||||
- **AnomalyDisplayManager** (`AnomalyDisplayManager.js`): 异常状态显示
|
||||
- **LayerDisplayManager** (`LayerDisplayManager.js`): 分层显示管理
|
||||
|
||||
#### UI和状态管理
|
||||
- **UIController** (`UIController.js`): UI控制器,管理所有界面交互
|
||||
- **ObjectStateManager** (`ObjectStateManager.js`): 对象状态管理
|
||||
- **StorageManager** (`StorageManager.js`): 本地存储管理
|
||||
- **OutlineManager** (`OutlineManager.js`): 边缘高亮效果
|
||||
|
||||
### 资源结构
|
||||
- **models/**: 3D模型文件(.glb格式)
|
||||
- **css/**: 样式文件(主要是style.css)
|
||||
- **image/**: UI图标和图片资源
|
||||
- **three/**: Three.js库文件
|
||||
|
||||
## 开发环境
|
||||
|
||||
### 启动开发服务器
|
||||
由于项目使用ES6模块和本地文件引用,需要通过HTTP服务器运行:
|
||||
|
||||
```bash
|
||||
# 使用Live Server(推荐)
|
||||
# VS Code安装Live Server扩展,配置端口5501
|
||||
# 右键点击index.html选择"Open with Live Server"
|
||||
|
||||
# 或使用Python内置服务器
|
||||
python -m http.server 5501
|
||||
|
||||
# 或使用Node.js http-server
|
||||
npx http-server -p 5501
|
||||
```
|
||||
|
||||
### 访问页面
|
||||
- 3D模型查看器: `http://localhost:5501/index.html`
|
||||
- 数据分析平台: `http://localhost:5501/1.html`
|
||||
|
||||
## 开发约定
|
||||
|
||||
### 模型文件管理
|
||||
- 支持的格式: .glb (推荐)
|
||||
- 模型文件存放在 `models/` 目录
|
||||
- 默认加载模型: `CX0856_CB01C.glb`
|
||||
|
||||
### 配置和状态管理
|
||||
- 使用 `StorageManager` 进行本地存储
|
||||
- 配置项通过 `ObjectStateManager` 管理
|
||||
- 模型状态通过事件系统同步
|
||||
|
||||
### 代码风格
|
||||
- 使用ES6+语法和模块
|
||||
- 采用面向对象设计模式
|
||||
- 每个功能模块独立文件
|
||||
- 使用驼峰命名法
|
||||
- 保持代码注释的完整性
|
||||
|
||||
### 事件通信
|
||||
项目使用自定义事件系统进行模块间通信:
|
||||
- 模型加载完成事件
|
||||
- 选择状态变化事件
|
||||
- UI状态更新事件
|
||||
- 测量结果事件
|
||||
|
||||
### 浏览器兼容性
|
||||
- 支持现代浏览器(Chrome 88+, Firefox 85+, Safari 14+)
|
||||
- 依赖ES6模块支持
|
||||
- 需要WebGL 2.0支持
|
||||
|
||||
## 调试和测试
|
||||
|
||||
### 控制台调试
|
||||
- 使用 `window.modelViewer` 访问主应用实例
|
||||
- 各管理器实例可通过modelViewer实例访问
|
||||
- 开启浏览器开发者工具查看Three.js性能面板
|
||||
|
||||
### 性能优化
|
||||
- 大模型文件建议压缩优化
|
||||
- 使用LOD (Level of Detail) 技术
|
||||
- 合理使用材质和纹理
|
||||
- 避免过度的实时计算
|
||||
|
||||
## 常见问题
|
||||
|
||||
### CORS错误
|
||||
确保通过HTTP服务器访问,而不是直接打开HTML文件
|
||||
|
||||
### 模型加载失败
|
||||
检查模型文件路径和格式,确保.glb文件完整性
|
||||
|
||||
### Three.js版本兼容
|
||||
项目基于Three.js r150+,更新版本时注意API变更
|
||||
|
||||
### 内存泄漏
|
||||
及时清理不需要的几何体、材质和纹理对象
|
||||
@ -1,20 +1,18 @@
|
||||
"""BidMaster-CLI 主包 - 统一导出接口
|
||||
|
||||
按照规范要求,导出三个核心工具函数。
|
||||
"""
|
||||
|
||||
__all__ = ["parse", "generate", "assemble"]
|
||||
|
||||
# CLI入口
|
||||
def main():
|
||||
"""CLI主入口函数"""
|
||||
from .cli.main import cli
|
||||
cli()
|
||||
"""BidMaster-CLI 主包 - 统一导出接口"""
|
||||
|
||||
from .tools.parser import BidParser
|
||||
from .tools.rag import RAGTool
|
||||
from .tools.table import TableGenerator
|
||||
|
||||
__all__ = ["parse", "generate", "assemble"]
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI主入口函数"""
|
||||
from .cli.main import cli
|
||||
|
||||
cli()
|
||||
|
||||
# 创建工具函数的便捷接口
|
||||
def parse(file_path: str, **kwargs):
|
||||
"""解析招标文件
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, TypedDict
|
||||
import asyncio
|
||||
@ -14,7 +15,15 @@ import asyncio
|
||||
from langgraph.graph import StateGraph, END
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..tools.parser import BidParser, BidStructure, ScoringCriteria, DeviationItem, DocumentChapter
|
||||
from ..tools.parser import (
|
||||
BidParser,
|
||||
BidStructure,
|
||||
ScoringCriteria,
|
||||
DeviationItem,
|
||||
DocumentChapter,
|
||||
TechnicalCategory,
|
||||
ChapterGuidance,
|
||||
)
|
||||
from ..config import get_settings
|
||||
from ..nodes.toc.base_mixins import WorkflowUtilsMixin
|
||||
from .base import BaseAgentFactory
|
||||
@ -39,12 +48,30 @@ def _validate_template_file(file_path: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class SectionScope(str, Enum):
|
||||
"""章节范围配置"""
|
||||
|
||||
TECHNICAL_ONLY = "technical_only"
|
||||
FULL = "full"
|
||||
|
||||
|
||||
TECHNICAL_SCOPE_WHITELIST = {
|
||||
TechnicalCategory.TECHNICAL_SOLUTION,
|
||||
TechnicalCategory.EQUIPMENT_SPEC,
|
||||
TechnicalCategory.IMPLEMENTATION,
|
||||
TechnicalCategory.QUALITY_SAFETY,
|
||||
TechnicalCategory.AFTER_SALES,
|
||||
TechnicalCategory.COMPLIANCE,
|
||||
}
|
||||
|
||||
|
||||
class AnalysisAgentState(TypedDict):
|
||||
"""Analysis Agent的状态定义"""
|
||||
|
||||
# 输入参数
|
||||
source_file: str
|
||||
interaction_handler: Any # InteractionHandler实例
|
||||
section_scope: str # 技术章节范围
|
||||
|
||||
# 执行状态
|
||||
current_step: str
|
||||
@ -67,6 +94,7 @@ class AnalysisAgentState(TypedDict):
|
||||
pending_suggestions: List[Dict[str, Any]] # 待处理的AI建议
|
||||
auto_optimization_rounds: int # 自动优化迭代次数
|
||||
auto_toc_max_rounds: int # 最大自动优化次数
|
||||
chapter_guidance_map: Dict[str, ChapterGuidance]
|
||||
|
||||
# 最终输出
|
||||
bid_structure: BidStructure | None
|
||||
@ -88,6 +116,7 @@ class AnalysisResult(BaseModel):
|
||||
error_message: str | None = Field(default=None, description="错误信息")
|
||||
warnings: List[str] = Field(default_factory=list, description="警告信息")
|
||||
execution_time: float = Field(default=0.0, description="执行时间(秒)")
|
||||
section_scope: str = Field(default=SectionScope.TECHNICAL_ONLY.value, description="章节范围")
|
||||
|
||||
|
||||
# ========== LangGraph 节点函数 ==========
|
||||
@ -233,6 +262,10 @@ def parse_content_node(state: AnalysisAgentState) -> AnalysisAgentState:
|
||||
technical_criteria = []
|
||||
commercial_criteria = []
|
||||
deviation_items = []
|
||||
try:
|
||||
section_scope = SectionScope(state.get("section_scope", SectionScope.TECHNICAL_ONLY.value))
|
||||
except ValueError:
|
||||
section_scope = SectionScope.TECHNICAL_ONLY
|
||||
|
||||
# 解析评分表
|
||||
scoring_tables = state["classified_tables"].get("scoring", [])
|
||||
@ -258,7 +291,14 @@ def parse_content_node(state: AnalysisAgentState) -> AnalysisAgentState:
|
||||
|
||||
logger.info(f"内容解析完成: 技术项{len(technical_criteria)}个, 商务项{len(commercial_criteria)}个, 偏离项{len(deviation_items)}个")
|
||||
|
||||
state["technical_criteria"] = technical_criteria
|
||||
if section_scope == SectionScope.FULL:
|
||||
scoped_criteria = technical_criteria + commercial_criteria
|
||||
else:
|
||||
scoped_criteria = [c for c in technical_criteria if c.category in TECHNICAL_SCOPE_WHITELIST]
|
||||
if not scoped_criteria:
|
||||
state.setdefault("warnings", []).append("技术范围过滤后未找到可用的评分项")
|
||||
|
||||
state["technical_criteria"] = scoped_criteria
|
||||
state["commercial_criteria"] = commercial_criteria
|
||||
state["deviation_items"] = deviation_items
|
||||
state["current_step"] = "parse_content"
|
||||
@ -401,9 +441,13 @@ def finalize_structure_node(state: AnalysisAgentState) -> AnalysisAgentState:
|
||||
class AnalysisAgent(BaseAgentFactory):
|
||||
"""Analysis Agent - 第一阶段分析Agent"""
|
||||
|
||||
def __init__(self, interaction_handler=None):
|
||||
def __init__(self, interaction_handler=None, section_scope: str = SectionScope.TECHNICAL_ONLY.value):
|
||||
self.settings = get_settings()
|
||||
self.interaction_handler = interaction_handler
|
||||
try:
|
||||
self.section_scope = SectionScope(section_scope).value
|
||||
except ValueError:
|
||||
self.section_scope = SectionScope.TECHNICAL_ONLY.value
|
||||
self.graph = self._build_graph()
|
||||
|
||||
def _build_graph(self) -> StateGraph:
|
||||
@ -482,6 +526,7 @@ class AnalysisAgent(BaseAgentFactory):
|
||||
initial_state = AnalysisAgentState(
|
||||
source_file=source_file,
|
||||
interaction_handler=self.interaction_handler,
|
||||
section_scope=self.section_scope,
|
||||
current_step="",
|
||||
progress=0.0,
|
||||
should_continue=True,
|
||||
@ -498,6 +543,7 @@ class AnalysisAgent(BaseAgentFactory):
|
||||
pending_suggestions=[],
|
||||
auto_optimization_rounds=0,
|
||||
auto_toc_max_rounds=self.settings.auto_toc_max_rounds,
|
||||
chapter_guidance_map={},
|
||||
bid_structure=None,
|
||||
error="",
|
||||
warnings=[]
|
||||
@ -516,7 +562,8 @@ class AnalysisAgent(BaseAgentFactory):
|
||||
success=False,
|
||||
error_message=final_state["error"],
|
||||
warnings=final_state.get("warnings", []),
|
||||
execution_time=time.time() - start_time
|
||||
execution_time=time.time() - start_time,
|
||||
section_scope=final_state.get("section_scope", self.section_scope)
|
||||
)
|
||||
else:
|
||||
bid_structure = final_state["bid_structure"]
|
||||
@ -528,7 +575,8 @@ class AnalysisAgent(BaseAgentFactory):
|
||||
deviation_count=len(final_state.get("deviation_items", [])),
|
||||
chapter_count=len(bid_structure.chapters) if bid_structure else 0,
|
||||
warnings=final_state.get("warnings", []),
|
||||
execution_time=time.time() - start_time
|
||||
execution_time=time.time() - start_time,
|
||||
section_scope=final_state.get("section_scope", self.section_scope)
|
||||
)
|
||||
|
||||
logger.info(f"Analysis Agent执行完成,耗时{result.execution_time:.2f}秒")
|
||||
@ -546,4 +594,25 @@ class AnalysisAgent(BaseAgentFactory):
|
||||
"""同步执行接口(用于CLI调用)"""
|
||||
return asyncio.run(self.execute(source_file))
|
||||
|
||||
@classmethod
|
||||
def create_silent(cls, section_scope: str = SectionScope.TECHNICAL_ONLY.value):
|
||||
from .interaction import InteractionHandler, InteractionMode
|
||||
|
||||
handler = InteractionHandler(mode=InteractionMode.SILENT)
|
||||
return cls(handler, section_scope=section_scope)
|
||||
|
||||
@classmethod
|
||||
def create_programmatic(cls, presets: Dict[str, Any], section_scope: str = SectionScope.TECHNICAL_ONLY.value):
|
||||
from .interaction import InteractionHandler, InteractionMode
|
||||
|
||||
handler = InteractionHandler(mode=InteractionMode.PROGRAMMATIC, presets=presets)
|
||||
return cls(handler, section_scope=section_scope)
|
||||
|
||||
@classmethod
|
||||
def create_interactive(cls, section_scope: str = SectionScope.TECHNICAL_ONLY.value):
|
||||
from .interaction import InteractionHandler, InteractionMode
|
||||
|
||||
handler = InteractionHandler(mode=InteractionMode.INTERACTIVE)
|
||||
return cls(handler, section_scope=section_scope)
|
||||
|
||||
|
||||
|
||||
@ -4,9 +4,8 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from langgraph.graph import END
|
||||
|
||||
from ..base import AgentBuilder, BaseAgent, BaseAgentFactory
|
||||
from ...nodes.toc import (
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict
|
||||
|
||||
from .single_chapter_agent import SingleChapterAgent
|
||||
|
||||
|
||||
@ -283,7 +283,7 @@ class InteractionHandler:
|
||||
if "pattern" in validation:
|
||||
import re
|
||||
if not re.match(validation["pattern"], text):
|
||||
return f"文本格式不符合要求"
|
||||
return "文本格式不符合要求"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
@ -135,7 +136,11 @@ def parse(scoring_file: str, deviation_file: str | None, template_file: str | No
|
||||
@click.option("--presets", "-p",
|
||||
type=str,
|
||||
help="程序化模式的预设配置JSON字符串")
|
||||
def new(mode: str, presets: str):
|
||||
@click.option("--section-scope",
|
||||
type=click.Choice(['technical_only', 'full']),
|
||||
default='technical_only',
|
||||
help="章节范围:technical_only只保留技术部分,full包含全部评分项")
|
||||
def new(mode: str, presets: str, section_scope: str):
|
||||
"""核心命令:解析招标文件,生成任务清单和Word框架"""
|
||||
try:
|
||||
console.print("🎯 BidMaster 标书项目创建向导", style="bold blue")
|
||||
@ -177,7 +182,7 @@ def new(mode: str, presets: str):
|
||||
|
||||
# 根据模式创建AnalysisAgent
|
||||
if mode == 'silent':
|
||||
analysis_agent = AnalysisAgent.create_silent()
|
||||
analysis_agent = AnalysisAgent.create_silent(section_scope=section_scope)
|
||||
console.print("📌 使用静默模式(自动选择默认值)", style="dim")
|
||||
elif mode == 'programmatic':
|
||||
if not presets:
|
||||
@ -185,13 +190,13 @@ def new(mode: str, presets: str):
|
||||
return
|
||||
try:
|
||||
preset_dict = json.loads(presets)
|
||||
analysis_agent = AnalysisAgent.create_programmatic(preset_dict)
|
||||
analysis_agent = AnalysisAgent.create_programmatic(preset_dict, section_scope=section_scope)
|
||||
console.print(f"📌 使用程序化模式,预设配置: {list(preset_dict.keys())}", style="dim")
|
||||
except json.JSONDecodeError:
|
||||
console.print("❌ 预设配置JSON格式错误", style="red")
|
||||
return
|
||||
else: # interactive
|
||||
analysis_agent = AnalysisAgent.create_interactive()
|
||||
analysis_agent = AnalysisAgent.create_interactive(section_scope=section_scope)
|
||||
console.print("📌 使用交互模式", style="dim")
|
||||
|
||||
# 执行完整的分析流程(包括目录生成)
|
||||
@ -214,6 +219,7 @@ def new(mode: str, presets: str):
|
||||
data = {
|
||||
"project_name": project_name,
|
||||
"source_file": str(Path(bidding_file).absolute()),
|
||||
"section_scope": result.section_scope,
|
||||
"success": result.success,
|
||||
"technical_count": result.technical_count,
|
||||
"commercial_count": result.commercial_count,
|
||||
@ -249,6 +255,17 @@ def new(mode: str, presets: str):
|
||||
tasks = []
|
||||
task_id = 1
|
||||
|
||||
guidance_map: Dict[str, Any] = {}
|
||||
|
||||
def _collect_guidance(chapter_obj):
|
||||
if chapter_obj.guidance:
|
||||
guidance_map[chapter_obj.id] = chapter_obj.guidance.dict()
|
||||
for child in chapter_obj.children:
|
||||
_collect_guidance(child)
|
||||
|
||||
for chapter in bid_structure.chapters:
|
||||
_collect_guidance(chapter)
|
||||
|
||||
# 为每个评分项创建任务
|
||||
for criteria in bid_structure.scoring_criteria:
|
||||
tasks.append({
|
||||
@ -260,7 +277,8 @@ def new(mode: str, presets: str):
|
||||
"category": criteria.category.value,
|
||||
"status": "pending",
|
||||
"content": "",
|
||||
"placeholder": f"{{{{{criteria.chapter_id}_content}}}}"
|
||||
"placeholder": f"{{{{{criteria.chapter_id}_content}}}}",
|
||||
"guidance": guidance_map.get(criteria.chapter_id)
|
||||
})
|
||||
task_id += 1
|
||||
|
||||
@ -338,10 +356,11 @@ def new(mode: str, presets: str):
|
||||
console.print(f"📚 生成章节: {result.chapter_count}个")
|
||||
console.print(f"📝 生成任务: {len(tasks)}个")
|
||||
console.print(f"⏱️ 处理耗时: {result.execution_time:.2f}秒")
|
||||
console.print(f"⚙️ 目录范围: {result.section_scope}")
|
||||
|
||||
# 显示警告信息(如果有)
|
||||
if result.warnings:
|
||||
console.print(f"\n⚠️ 警告信息:", style="yellow")
|
||||
console.print("\n⚠️ 警告信息:", style="yellow")
|
||||
for warning in result.warnings:
|
||||
console.print(f" • {warning}", style="yellow")
|
||||
|
||||
@ -356,9 +375,9 @@ def new(mode: str, presets: str):
|
||||
console.print("\n📚 项目目录结构:")
|
||||
_display_chapters_recursive(bid_structure.chapters)
|
||||
|
||||
console.print(f"\n🎯 下一步:", style="dim")
|
||||
console.print("\n🎯 下一步:", style="dim")
|
||||
console.print(f" • 打开 {template_file.name} 开始编写标书", style="dim")
|
||||
console.print(f" • 使用 'generate task <id>' 为具体任务生成内容", style="dim")
|
||||
console.print(" • 使用 'generate task <id>' 为具体任务生成内容", style="dim")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"❌ 项目创建失败: {e}", style="red")
|
||||
@ -372,6 +391,7 @@ def _serialize_chapter(chapter):
|
||||
"level": chapter.level,
|
||||
"score": chapter.score,
|
||||
"template_placeholder": chapter.template_placeholder,
|
||||
"guidance": chapter.guidance.dict() if getattr(chapter, "guidance", None) else None,
|
||||
"children": [_serialize_chapter(child) for child in chapter.children]
|
||||
}
|
||||
|
||||
@ -411,6 +431,7 @@ def status(project_dir: str):
|
||||
console.print(f"📋 {analysis_data.get('project_name', '标书项目')}", style="bold blue")
|
||||
console.print(f"📁 项目路径: {project_path.absolute()}")
|
||||
console.print(f"📄 源文件: {analysis_data.get('source_file', 'N/A')}")
|
||||
console.print(f"⚙️ 目录范围: {analysis_data.get('section_scope', 'technical_only')}")
|
||||
|
||||
# 统计信息
|
||||
technical_count = len(analysis_data.get('technical_criteria', []))
|
||||
@ -418,7 +439,7 @@ def status(project_dir: str):
|
||||
deviation_count = len(analysis_data.get('deviation_items', []))
|
||||
total_tasks = len(tasks_data)
|
||||
|
||||
console.print(f"\n📊 项目概况:")
|
||||
console.print("\n📊 项目概况:")
|
||||
console.print(f" • 技术评分项: {technical_count}项")
|
||||
console.print(f" • 商务评分项: {commercial_count}项(已排除)")
|
||||
console.print(f" • 偏离项: {deviation_count}项")
|
||||
@ -430,7 +451,7 @@ def status(project_dir: str):
|
||||
status = task.get('status', 'unknown')
|
||||
status_count[status] = status_count.get(status, 0) + 1
|
||||
|
||||
console.print(f"\n✅ 任务进度:")
|
||||
console.print("\n✅ 任务进度:")
|
||||
status_names = {
|
||||
'pending': '待处理',
|
||||
'in_progress': '进行中',
|
||||
@ -443,7 +464,7 @@ def status(project_dir: str):
|
||||
console.print(f" • {name}: {count}个")
|
||||
|
||||
# 显示任务清单
|
||||
console.print(f"\n📝 任务清单:")
|
||||
console.print("\n📝 任务清单:")
|
||||
task_table = Table(title="项目任务列表")
|
||||
task_table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
||||
task_table.add_column("标题", style="yellow")
|
||||
@ -451,18 +472,19 @@ def status(project_dir: str):
|
||||
task_table.add_column("状态", style="green")
|
||||
|
||||
for task in tasks_data[:10]: # 只显示前10个任务
|
||||
status_text = task.get('status', 'pending')
|
||||
status_style = {
|
||||
'pending': 'yellow',
|
||||
'in_progress': 'blue',
|
||||
'completed': 'green',
|
||||
'failed': 'red'
|
||||
}.get(task.get('status'), 'white')
|
||||
}.get(status_text, 'white')
|
||||
|
||||
task_table.add_row(
|
||||
str(task['id']),
|
||||
task['title'][:50] + "..." if len(task['title']) > 50 else task['title'],
|
||||
f"{task['score']}分",
|
||||
task.get('status', 'pending')
|
||||
f"[{status_style}]{status_text}[/]"
|
||||
)
|
||||
|
||||
console.print(task_table)
|
||||
@ -470,9 +492,9 @@ def status(project_dir: str):
|
||||
if len(tasks_data) > 10:
|
||||
console.print(f" ... 还有{len(tasks_data) - 10}个任务")
|
||||
|
||||
console.print(f"\n🎯 下一步操作:", style="dim")
|
||||
console.print(f" • generate task <id> - 为具体任务生成内容", style="dim")
|
||||
console.print(f" • generate full - 为所有待处理任务生成内容", style="dim")
|
||||
console.print("\n🎯 下一步操作:", style="dim")
|
||||
console.print(" • generate task <id> - 为具体任务生成内容", style="dim")
|
||||
console.print(" • generate full - 为所有待处理任务生成内容", style="dim")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"❌ 查看项目状态失败: {e}", style="red")
|
||||
@ -520,16 +542,17 @@ def smart_parse(word_file: str):
|
||||
console.print(f"📋 偏离项: {result.deviation_count}项")
|
||||
console.print(f"📚 生成章节: {result.chapter_count}个")
|
||||
console.print(f"⏱️ 处理耗时: {result.execution_time:.2f}秒")
|
||||
console.print(f"⚙️ 目录范围: {result.section_scope}")
|
||||
|
||||
# 显示警告信息(如果有)
|
||||
if result.warnings:
|
||||
console.print(f"\n⚠️ 警告信息:", style="yellow")
|
||||
console.print("\n⚠️ 警告信息:", style="yellow")
|
||||
for warning in result.warnings:
|
||||
console.print(f" • {warning}", style="yellow")
|
||||
|
||||
# 显示技术评分标准
|
||||
if bid_structure.scoring_criteria:
|
||||
console.print(f"\n📊 技术评分标准详情:")
|
||||
console.print("\n📊 技术评分标准详情:")
|
||||
table = Table(title="技术评分标准解析结果")
|
||||
table.add_column("技术类别", style="green")
|
||||
table.add_column("评分项", style="yellow")
|
||||
|
||||
@ -66,7 +66,7 @@ def start(project_dir: str, mode: str):
|
||||
console.print("❌ 未找到Word文档", style="red")
|
||||
return
|
||||
console.print(f"📄 找到Word文档: {Path(word_file).name}", style="green")
|
||||
console.print(f"📋 将直接从Word文档提取章节结构", style="dim")
|
||||
console.print("📋 将直接从Word文档提取章节结构", style="dim")
|
||||
|
||||
# 创建Agent
|
||||
if mode == "silent":
|
||||
@ -111,9 +111,9 @@ def start(project_dir: str, mode: str):
|
||||
)
|
||||
console.print("\n", stats_panel)
|
||||
|
||||
console.print(f"\n🎯 下一步:", style="dim")
|
||||
console.print("\n🎯 下一步:", style="dim")
|
||||
console.print(f" • 打开 {Path(result['word_file']).name} 查看生成的内容", style="dim")
|
||||
console.print(f" • 使用 Ctrl+A -> F9 刷新Word中的目录和页码", style="dim")
|
||||
console.print(" • 使用 Ctrl+A -> F9 刷新Word中的目录和页码", style="dim")
|
||||
else:
|
||||
console.print("\n❌ 内容填写失败", style="red")
|
||||
|
||||
@ -166,7 +166,7 @@ def _find_word_file(project_path: Path) -> str:
|
||||
if len(docx_files) == 1:
|
||||
return str(docx_files[0])
|
||||
elif len(docx_files) > 1:
|
||||
console.print(f"\n⚠️ 找到多个Word文档:", style="yellow")
|
||||
console.print("\n⚠️ 找到多个Word文档:", style="yellow")
|
||||
for i, f in enumerate(docx_files, 1):
|
||||
console.print(f" {i}. {f.name}")
|
||||
console.print("\n提示:将只有一个.docx文件放在项目目录中", style="yellow")
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
|
||||
import yaml
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
"""Internal!W - …èž°<0C>bèimport
|
||||
"""Experimental internal utilities module placeholder."""
|
||||
|
||||
dîU+yî„…èž°Æ‚
”«èô¥(
|
||||
@ lq¥ã”Ç
|
||||
B!W´2
|
||||
"""
|
||||
|
||||
__all__ = [] #
üúûU…¹
|
||||
__all__: list[str] = []
|
||||
|
||||
@ -5,8 +5,6 @@
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@ -327,6 +327,7 @@ class InitConfigNode(BaseNode):
|
||||
return {}
|
||||
|
||||
chapter_titles: Dict[str, str] = {}
|
||||
chapter_guidance: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def _collect_titles(nodes: List[Dict[str, Any]] | None) -> None:
|
||||
if not nodes:
|
||||
@ -336,6 +337,9 @@ class InitConfigNode(BaseNode):
|
||||
title = node.get("title", "")
|
||||
if chapter_id and title:
|
||||
chapter_titles[chapter_id] = title
|
||||
guidance = node.get("guidance")
|
||||
if chapter_id and guidance:
|
||||
chapter_guidance[chapter_id] = guidance
|
||||
_collect_titles(node.get("children"))
|
||||
|
||||
_collect_titles(data.get("chapters", []))
|
||||
@ -373,6 +377,10 @@ class InitConfigNode(BaseNode):
|
||||
entry["requirements"].append(description)
|
||||
entry["rubric_points"].extend(self._split_rubric_points(description))
|
||||
|
||||
self._merge_guidance(entry, chapter_guidance.get(chapter_id))
|
||||
|
||||
return self._finalize_metadata_entries(metadata)
|
||||
|
||||
return self._finalize_metadata_entries(metadata)
|
||||
|
||||
def _parse_tasks_metadata(self, file_path: Path) -> Dict[str, Dict[str, Any]]:
|
||||
@ -408,6 +416,8 @@ class InitConfigNode(BaseNode):
|
||||
entry["requirements"].append(description)
|
||||
entry["rubric_points"].extend(self._split_rubric_points(description))
|
||||
|
||||
self._merge_guidance(entry, task.get("guidance"))
|
||||
|
||||
return self._finalize_metadata_entries(metadata)
|
||||
|
||||
def _split_rubric_points(self, text: str) -> List[str]:
|
||||
@ -418,6 +428,44 @@ class InitConfigNode(BaseNode):
|
||||
clean_parts = [p.strip().strip("::·•") for p in parts if len(p.strip()) >= 3]
|
||||
return clean_parts
|
||||
|
||||
def _merge_guidance(self,
|
||||
entry: Dict[str, Any],
|
||||
guidance: Optional[Dict[str, Any]]) -> None:
|
||||
if not guidance:
|
||||
return
|
||||
|
||||
target = entry.setdefault(
|
||||
"guidance",
|
||||
{
|
||||
"key_requirements": [],
|
||||
"suggested_evidence": [],
|
||||
"context_snippets": [],
|
||||
}
|
||||
)
|
||||
|
||||
for key in ("key_requirements", "suggested_evidence", "context_snippets"):
|
||||
values = guidance.get(key) or []
|
||||
for value in values:
|
||||
if value and value not in target[key]:
|
||||
target[key].append(value)
|
||||
|
||||
def _normalize_guidance(self, guidance: Optional[Dict[str, Any]]) -> Optional[Dict[str, List[str]]]:
|
||||
if not guidance:
|
||||
return None
|
||||
|
||||
normalized: Dict[str, List[str]] = {}
|
||||
for key in ("key_requirements", "suggested_evidence", "context_snippets"):
|
||||
values = guidance.get(key) or []
|
||||
dedup: List[str] = []
|
||||
for value in values:
|
||||
clean = (value or "").strip()
|
||||
if clean and clean not in dedup:
|
||||
dedup.append(clean)
|
||||
if dedup:
|
||||
normalized[key] = dedup
|
||||
|
||||
return normalized or None
|
||||
|
||||
def _finalize_metadata_entries(self, metadata: Dict[str, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
|
||||
finalized: Dict[str, Dict[str, Any]] = {}
|
||||
for key, entry in metadata.items():
|
||||
@ -441,6 +489,7 @@ class InitConfigNode(BaseNode):
|
||||
"rubric_points": dedup_points,
|
||||
"source": entry.get("source"),
|
||||
"chapter_id_source": entry.get("chapter_id_source"),
|
||||
"guidance": self._normalize_guidance(entry.get("guidance")),
|
||||
}
|
||||
|
||||
return finalized
|
||||
@ -473,6 +522,7 @@ class InitConfigNode(BaseNode):
|
||||
"rubric_points": meta.get("rubric_points", []),
|
||||
"source": meta.get("source"),
|
||||
"chapter_id_source": meta.get("chapter_id_source"),
|
||||
"guidance": meta.get("guidance"),
|
||||
}
|
||||
|
||||
chapter["requirements"] = enriched["requirements"]
|
||||
@ -480,6 +530,8 @@ class InitConfigNode(BaseNode):
|
||||
chapter["score"] = enriched["score"]
|
||||
if enriched["category"]:
|
||||
chapter["category"] = enriched["category"]
|
||||
if enriched.get("guidance"):
|
||||
chapter["guidance"] = enriched["guidance"]
|
||||
|
||||
chapter_metadata[chapter["id"]] = enriched
|
||||
|
||||
|
||||
@ -128,7 +128,7 @@ class InteractWithUserNode(BaseNode):
|
||||
|
||||
choice = interaction_handler(
|
||||
interaction_type="choice",
|
||||
prompt=f"选择知识库",
|
||||
prompt="选择知识库",
|
||||
options=[
|
||||
("default", store_info),
|
||||
("none", "不使用RAG")
|
||||
|
||||
@ -8,6 +8,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
from ...utils.validation import ContentValidationError, ContentValidator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -21,6 +22,9 @@ class SaveToWordNode(BaseNode):
|
||||
3. 更新处理进度
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._content_validator = ContentValidator()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "save_to_word"
|
||||
@ -53,7 +57,21 @@ class SaveToWordNode(BaseNode):
|
||||
if not content:
|
||||
raise ValueError(f"章节 {chapter_id} 的内容未生成")
|
||||
|
||||
logger.info(f"开始保存章节内容到Word: {chapter_id} - {chapter['title']}")
|
||||
current_index = state.get("current_chapter_index", 0)
|
||||
total_chapters = len(state.get("chapter_queue", [])) or 1
|
||||
logger.info(
|
||||
"保存章节内容 [%s/%s]: %s - %s",
|
||||
current_index + 1,
|
||||
total_chapters,
|
||||
chapter_id,
|
||||
chapter.get("title"),
|
||||
)
|
||||
|
||||
try:
|
||||
self._content_validator.validate(chapter, content)
|
||||
except ContentValidationError as exc:
|
||||
logger.error("章节 %s 内容未通过合规校验: %s", chapter_id, exc)
|
||||
raise ValueError(f"章节 {chapter_id} 内容未通过合规校验: {exc}") from exc
|
||||
|
||||
# 填充到Word文档
|
||||
self._fill_word_document(word_file, chapter, content)
|
||||
@ -62,9 +80,6 @@ class SaveToWordNode(BaseNode):
|
||||
completed_chapters = state.get("completed_chapters", [])
|
||||
completed_chapters.append(chapter_id)
|
||||
|
||||
current_index = state.get("current_chapter_index", 0)
|
||||
total_chapters = len(state.get("chapter_queue", []))
|
||||
|
||||
logger.info(f"章节保存完成 [{current_index + 1}/{total_chapters}]: {chapter_id}")
|
||||
|
||||
# 直接在state上更新,确保下次循环能读取到最新值
|
||||
@ -100,8 +115,8 @@ class SaveToWordNode(BaseNode):
|
||||
placeholder = f"{{{{{chapter['id']}_content}}}}"
|
||||
|
||||
try:
|
||||
# 填充占位符
|
||||
word_processor.fill_placeholder(word_path, placeholder, content)
|
||||
# 填充章节内容
|
||||
word_processor.fill_chapter_content(word_path, chapter, content)
|
||||
logger.info(f"成功填充占位符: {placeholder}")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@ -75,7 +75,7 @@ class ApplyReviewSuggestionsNode(BaseNode, TocNodeBase):
|
||||
|
||||
try:
|
||||
# 展示AI的完整评估结果
|
||||
assessment_text = f"AI审查评估:\n"
|
||||
assessment_text = "AI审查评估:\n"
|
||||
if overall_assessment:
|
||||
assessment_text += f"总体评价: {overall_assessment}\n"
|
||||
if optimization_score:
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
from abc import ABC
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
from typing import Dict, Any, Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any
|
||||
from typing import Dict, List
|
||||
|
||||
from ...tools.parser import ScoringCriteria, DocumentChapter
|
||||
from .constants import CATEGORY_NAMES, CATEGORY_ORDER
|
||||
from .constants import CATEGORY_NAMES, CATEGORY_ORDER, CATEGORY_TITLE_HINTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -103,6 +103,27 @@ class CategoryManager:
|
||||
|
||||
return "_".join(parts[2:])
|
||||
|
||||
@staticmethod
|
||||
def infer_category_for_chapter(chapter: DocumentChapter) -> str:
|
||||
"""通过章节ID或标题推断类别"""
|
||||
|
||||
category = CategoryManager.extract_category_from_chapter_id(chapter.id)
|
||||
if category:
|
||||
return category
|
||||
|
||||
title = (chapter.title or "").lower()
|
||||
if not title:
|
||||
return ""
|
||||
|
||||
for category_key, hints in CATEGORY_TITLE_HINTS.items():
|
||||
for hint in hints:
|
||||
if not hint:
|
||||
continue
|
||||
if hint.lower() in title:
|
||||
return category_key
|
||||
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def find_corresponding_criteria(chapter: DocumentChapter,
|
||||
technical_criteria: List[ScoringCriteria]) -> List[ScoringCriteria]:
|
||||
@ -115,7 +136,7 @@ class CategoryManager:
|
||||
Returns:
|
||||
对应的评分项列表
|
||||
"""
|
||||
category = CategoryManager.extract_category_from_chapter_id(chapter.id)
|
||||
category = CategoryManager.infer_category_for_chapter(chapter)
|
||||
if not category:
|
||||
return []
|
||||
|
||||
|
||||
@ -10,7 +10,8 @@ CATEGORY_NAMES = {
|
||||
"equipment_spec": "设备规格",
|
||||
"quality_safety": "质量安全",
|
||||
"after_sales": "售后服务",
|
||||
"implementation": "实施方案"
|
||||
"implementation": "实施方案",
|
||||
"commercial": "商务响应"
|
||||
}
|
||||
|
||||
# 预定义类别顺序
|
||||
@ -20,9 +21,21 @@ CATEGORY_ORDER = [
|
||||
"implementation",
|
||||
"quality_safety",
|
||||
"after_sales",
|
||||
"compliance"
|
||||
"compliance",
|
||||
"commercial"
|
||||
]
|
||||
|
||||
# 章节标题关键字提示
|
||||
CATEGORY_TITLE_HINTS = {
|
||||
"technical_solution": [CATEGORY_NAMES["technical_solution"], "技术部分", "总体要求"],
|
||||
"equipment_spec": [CATEGORY_NAMES["equipment_spec"], "供货", "设备"],
|
||||
"implementation": [CATEGORY_NAMES["implementation"], "实施方案", "安装调试"],
|
||||
"quality_safety": [CATEGORY_NAMES["quality_safety"], "质量", "安全"],
|
||||
"after_sales": [CATEGORY_NAMES["after_sales"], "售后", "培训"],
|
||||
"compliance": [CATEGORY_NAMES["compliance"], "合规", "条款"],
|
||||
"commercial": [CATEGORY_NAMES["commercial"], "商务"],
|
||||
}
|
||||
|
||||
# 默认子章节模板
|
||||
DEFAULT_SUB_CHAPTERS_TEMPLATE = [
|
||||
{"title": "方案概述", "level": 2},
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from ...tools.parser import DocumentChapter, ScoringCriteria
|
||||
from ...tools.parser import DocumentChapter
|
||||
from .constants import CATEGORY_NAMES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -10,6 +10,7 @@ from ..base import BaseNode, NodeContext
|
||||
from ...tools.parser import DocumentChapter
|
||||
from .factories import ChapterFactory
|
||||
from .base_mixins import TocNodeBase
|
||||
from .category_manager import CategoryManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -44,6 +45,8 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase):
|
||||
# 确保标准章节存在
|
||||
final_chapters = self._ensure_standard_chapters(final_chapters)
|
||||
|
||||
self._apply_saved_guidance(final_chapters, state.get("chapter_guidance_map"))
|
||||
|
||||
self.log_step_info("finalize_chapters", f"最终生成{len(final_chapters)}个章节")
|
||||
|
||||
return self._update_state(state,
|
||||
@ -76,4 +79,19 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase):
|
||||
# 可以在此添加其他必要的标准章节
|
||||
# 例如:封面、目录、声明等
|
||||
|
||||
return final_chapters
|
||||
return final_chapters
|
||||
|
||||
def _apply_saved_guidance(self,
|
||||
chapters: List[DocumentChapter],
|
||||
guidance_map: Dict[str, Any] | None) -> None:
|
||||
if not guidance_map:
|
||||
return
|
||||
|
||||
for chapter in chapters:
|
||||
category_key = CategoryManager.infer_category_for_chapter(chapter)
|
||||
guidance = guidance_map.get(category_key)
|
||||
if guidance:
|
||||
chapter.guidance = guidance
|
||||
|
||||
if chapter.children:
|
||||
self._apply_saved_guidance(chapter.children, guidance_map)
|
||||
@ -13,6 +13,7 @@ from .factories import ChapterFactory
|
||||
from .llm_helper import LLMHelper
|
||||
from .base_mixins import TocNodeBase
|
||||
from ...utils.document_context import DocumentContextSearcher
|
||||
from ...utils.guidance import build_chapter_guidance
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -50,6 +51,8 @@ class GenerateSubChaptersNode(BaseNode, TocNodeBase):
|
||||
except Exception as exc:
|
||||
self.log_step_info("context_searcher", f"上下文检索器初始化失败: {exc}")
|
||||
|
||||
guidance_map = dict(state.get("chapter_guidance_map", {}))
|
||||
|
||||
# 为每个章节生成子标题
|
||||
enhanced_chapters = []
|
||||
for chapter in preliminary_chapters:
|
||||
@ -60,9 +63,14 @@ class GenerateSubChaptersNode(BaseNode, TocNodeBase):
|
||||
)
|
||||
enhanced_chapters.append(enhanced_chapter)
|
||||
|
||||
if getattr(enhanced_chapter, "guidance", None):
|
||||
category_key = CategoryManager.infer_category_for_chapter(enhanced_chapter)
|
||||
if category_key:
|
||||
guidance_map[category_key] = enhanced_chapter.guidance
|
||||
|
||||
self.log_step_info("generate_sub_chapters", f"完成{len(enhanced_chapters)}个章节的子标题生成")
|
||||
|
||||
return self._update_state(state, preliminary_chapters=enhanced_chapters)
|
||||
return self._update_state(state, preliminary_chapters=enhanced_chapters, chapter_guidance_map=guidance_map)
|
||||
|
||||
def _enhance_chapter_with_subs(self,
|
||||
chapter: DocumentChapter,
|
||||
@ -102,6 +110,10 @@ class GenerateSubChaptersNode(BaseNode, TocNodeBase):
|
||||
self.log_step_info("enhance_chapter", f"章节 {chapter.title} AI生成子标题失败")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
guidance = build_chapter_guidance(chapter, corresponding_criteria, context_snippets)
|
||||
if guidance:
|
||||
chapter.guidance = guidance
|
||||
|
||||
return chapter
|
||||
|
||||
def _collect_context_snippets(self,
|
||||
|
||||
@ -4,10 +4,9 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
from ...tools.parser import ScoringCriteria
|
||||
from .category_manager import CategoryManager
|
||||
from .base_mixins import TocNodeBase
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ from ..base import BaseNode, NodeContext
|
||||
from ...tools.parser import DocumentChapter
|
||||
from .base_mixins import TocNodeBase
|
||||
from .llm_helper import LLMHelper
|
||||
from .factories import ChapterFactory
|
||||
from ...config.prompt_manager import get_prompt_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -4,10 +4,9 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
from ...tools.parser import ScoringCriteria, DocumentChapter
|
||||
from .llm_helper import LLMHelper
|
||||
from .base_mixins import TocNodeBase
|
||||
from ...utils.document_context import DocumentContextSearcher
|
||||
|
||||
@ -11,7 +11,6 @@ from enum import Enum
|
||||
|
||||
import pandas as pd
|
||||
from docx import Document
|
||||
from openai import OpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..config import get_settings
|
||||
@ -51,6 +50,14 @@ class DeviationItem(BaseModel):
|
||||
chapter_id: str = Field(..., description="对应章节ID")
|
||||
|
||||
|
||||
class ChapterGuidance(BaseModel):
|
||||
"""章节写作提示"""
|
||||
|
||||
key_requirements: List[str] = Field(default_factory=list, description="关键评分要求")
|
||||
suggested_evidence: List[str] = Field(default_factory=list, description="建议支撑材料")
|
||||
context_snippets: List[str] = Field(default_factory=list, description="上下文摘录")
|
||||
|
||||
|
||||
class DocumentChapter(BaseModel):
|
||||
"""文档章节"""
|
||||
|
||||
@ -60,6 +67,7 @@ class DocumentChapter(BaseModel):
|
||||
score: float | None = Field(default=None, description="评分值")
|
||||
children: List['DocumentChapter'] = Field(default_factory=list, description="子章节")
|
||||
template_placeholder: str | None = Field(default=None, description="模板占位符")
|
||||
guidance: ChapterGuidance | None = Field(default=None, description="章节写作提示")
|
||||
|
||||
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ import chromadb
|
||||
from chromadb.config import Settings as ChromaSettings
|
||||
from chromadb.utils import embedding_functions
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from langchain_community.document_loaders import (
|
||||
PyPDFLoader,
|
||||
TextLoader,
|
||||
@ -215,6 +214,8 @@ class RAGTool:
|
||||
# 构建提示词变量
|
||||
emphasis_part = f'\n特别强调:{emphasis}' if emphasis else ''
|
||||
rag_part = f'\n\n参考资料:\n{rag_context}' if rag_context else ''
|
||||
guidance_notes = (context.get('guidance_notes') or '').strip()
|
||||
guidance_part = guidance_notes or '(暂无重点提示)'
|
||||
|
||||
prompt_variables = {
|
||||
"title": task_title,
|
||||
@ -226,6 +227,7 @@ class RAGTool:
|
||||
"consistency_rules": context.get('consistency_rules', '1. 保持章节语气与格式一致'),
|
||||
"context_summary": context.get('context_summary', '(暂无可引用的上下文)'),
|
||||
"emphasis_part": emphasis_part,
|
||||
"guidance_part": guidance_part,
|
||||
"rag_part": rag_part,
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TableGenerator:
|
||||
|
||||
@ -5,17 +5,18 @@
|
||||
|
||||
import logging
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Inches, Pt, Cm
|
||||
from docx.enum.style import WD_STYLE_TYPE
|
||||
from docx.shared import Pt, Cm
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.text.paragraph import Paragraph
|
||||
|
||||
from .parser import DocumentChapter
|
||||
from .parser import DocumentChapter, ChapterGuidance
|
||||
from ..config.settings import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -35,12 +36,12 @@ NUMBERING_PATTERNS = [
|
||||
re.compile(r'^([A-Z]\.\s+)'), # A.
|
||||
]
|
||||
|
||||
MARKDOWN_HEADING_PATTERN = re.compile(r'^#{2,4}\s+')
|
||||
MARKDOWN_HEADING_PATTERN = re.compile(r'^(#{2,})\s+')
|
||||
ORDERED_LIST_PATTERN = re.compile(r'^(\d+[\.))]|(\d+))\s+')
|
||||
UNORDERED_LIST_PATTERN = re.compile(r'^[-*•]\s+')
|
||||
BOLD_TEXT_PATTERN = re.compile(r'\*\*(.+?)\*\*')
|
||||
ITALIC_TEXT_PATTERN = re.compile(r'\*(.+?)\*')
|
||||
CHAPTER_ID_PATTERN = re.compile(r'chapter_(\d+(?:_\d+)*)')
|
||||
CHAPTER_ID_PATTERN = re.compile(r'chapter_([a-zA-Z0-9_]+)')
|
||||
HEADING_LEVEL_PATTERN = re.compile(r'^(#+)')
|
||||
|
||||
# 字体常量(非格式参数)
|
||||
@ -110,6 +111,9 @@ class WordProcessor:
|
||||
run = para.add_run(title_text)
|
||||
run.bold = True
|
||||
|
||||
if getattr(chapter, "guidance", None):
|
||||
self._add_guidance_block(doc, chapter.guidance)
|
||||
|
||||
# 为有内容的章节添加占位符
|
||||
if chapter.template_placeholder:
|
||||
content_para = doc.add_paragraph(f"\n{chapter.template_placeholder}\n")
|
||||
@ -134,6 +138,25 @@ class WordProcessor:
|
||||
if chapter.level == PRIMARY_CHAPTER_LEVEL:
|
||||
doc.add_paragraph() # 添加空行
|
||||
|
||||
def _add_guidance_block(self, doc: Document, guidance: ChapterGuidance) -> None:
|
||||
"""在章节下方插入写作提示"""
|
||||
|
||||
lines = ["【重点提示】"]
|
||||
if guidance.key_requirements:
|
||||
lines.append("关键要求:")
|
||||
lines.extend([f" • {item}" for item in guidance.key_requirements])
|
||||
if guidance.suggested_evidence:
|
||||
lines.append("建议支撑:")
|
||||
lines.extend([f" • {item}" for item in guidance.suggested_evidence])
|
||||
if guidance.context_snippets:
|
||||
lines.append("招标摘录:")
|
||||
lines.extend([f" • {item}" for item in guidance.context_snippets])
|
||||
|
||||
block_text = "\n".join(lines)
|
||||
para = doc.add_paragraph(block_text)
|
||||
for run in para.runs:
|
||||
run.italic = True
|
||||
|
||||
def _check_has_numbering(self, title: str) -> bool:
|
||||
"""检查标题是否包含编号
|
||||
|
||||
@ -154,37 +177,100 @@ class WordProcessor:
|
||||
return NUMBERING_PATTERNS[0].sub('', title.strip())
|
||||
|
||||
def fill_placeholder(self, doc_path: str, placeholder: str, content: str) -> None:
|
||||
"""填充Word文档:在对应标题后插入内容
|
||||
"""兼容旧接口, 根据占位符填充Word文档内容"""
|
||||
|
||||
Args:
|
||||
doc_path: Word文档路径
|
||||
placeholder: 占位符(用于解析章节编号,如 {{chapter_2_1_1_content}})
|
||||
content: 要填充的内容
|
||||
match = CHAPTER_ID_PATTERN.search(placeholder)
|
||||
if not match:
|
||||
raise ValueError(f"无法从占位符解析章节编号: {placeholder}")
|
||||
|
||||
Raises:
|
||||
ValueError: 占位符格式错误或未找到对应标题
|
||||
FileNotFoundError: 文档不存在
|
||||
"""
|
||||
doc = Document(doc_path)
|
||||
chapter_id = match.group(0)
|
||||
chapter_meta = {
|
||||
"id": chapter_id,
|
||||
"placeholder": placeholder,
|
||||
"title": chapter_id,
|
||||
"normalized_title": self._normalize_heading_text(chapter_id),
|
||||
"level": None,
|
||||
"heading_number": None,
|
||||
"order_index": None,
|
||||
}
|
||||
|
||||
# 1. 解析占位符提取章节编号
|
||||
chapter_number = self._parse_chapter_number_from_placeholder(placeholder)
|
||||
try:
|
||||
chapter_number = self._parse_chapter_number_from_placeholder(placeholder)
|
||||
except ValueError:
|
||||
chapter_number = None
|
||||
|
||||
# 2. 查找对应标题
|
||||
target_paragraph, _ = self._find_heading_by_chapter_id(doc, chapter_number)
|
||||
if chapter_number:
|
||||
chapter_meta["level"] = chapter_number.count(".") + 1
|
||||
|
||||
# 3. 获取章节层级
|
||||
chapter_level = self._get_chapter_level_from_heading(target_paragraph)
|
||||
self.fill_chapter_content(doc_path, chapter_meta, content)
|
||||
|
||||
def fill_chapter_content(self, doc_path: str | Path, chapter_meta: Dict[str, Any], content: str) -> None:
|
||||
"""填充指定章节的内容到Word文档"""
|
||||
|
||||
if not Path(doc_path).exists():
|
||||
raise FileNotFoundError(f"Word文档不存在: {doc_path}")
|
||||
|
||||
doc = Document(str(doc_path))
|
||||
headings_index = self._extract_document_headings(doc)
|
||||
|
||||
placeholder = chapter_meta.get("placeholder")
|
||||
chapter_number = None
|
||||
if placeholder:
|
||||
try:
|
||||
chapter_number = self._parse_chapter_number_from_placeholder(placeholder)
|
||||
except ValueError:
|
||||
chapter_number = None
|
||||
|
||||
target_paragraph, target_index, matched_strategy = self._locate_heading(
|
||||
chapter_meta, chapter_number, headings_index
|
||||
)
|
||||
|
||||
# 确定章节层级
|
||||
chapter_level = chapter_meta.get("level")
|
||||
if not chapter_level and target_paragraph:
|
||||
chapter_level = self._get_chapter_level_from_heading(target_paragraph)
|
||||
chapter_level = chapter_level or DEFAULT_CHAPTER_LEVEL
|
||||
|
||||
# 4. 解析Markdown内容为段落结构
|
||||
parsed_paragraphs = self._parse_markdown_to_paragraphs(content, chapter_level)
|
||||
|
||||
# 5. 在标题后插入段落
|
||||
self._insert_parsed_paragraphs(doc, target_paragraph, parsed_paragraphs)
|
||||
if matched_strategy == "any_heading_fallback":
|
||||
notice_text = (
|
||||
f"【系统提示】章节“{chapter_meta.get('title', chapter_meta.get('id', ''))}”未能精准定位,"
|
||||
"已将内容插入至文档末尾标题,请检查模板结构。"
|
||||
)
|
||||
parsed_paragraphs = [{'type': 'notice', 'text': notice_text}] + parsed_paragraphs
|
||||
|
||||
# 6. 保存文档
|
||||
doc.save(doc_path)
|
||||
logger.info(f"成功填充内容到文档: {placeholder}")
|
||||
if target_paragraph is None:
|
||||
logger.warning(
|
||||
"章节 %s 未能定位原始标题, 将内容追加至文末", chapter_meta.get("id")
|
||||
)
|
||||
self._append_content_to_document_end(doc, chapter_meta, parsed_paragraphs, chapter_level)
|
||||
else:
|
||||
if matched_strategy == "any_heading_fallback":
|
||||
logger.warning(
|
||||
"章节 %s 未精准匹配标题,已退回到最后一个标题",
|
||||
chapter_meta.get("id"),
|
||||
)
|
||||
elif matched_strategy:
|
||||
logger.info(
|
||||
"章节 %s 通过策略 %s 匹配到标题: %s",
|
||||
chapter_meta.get("id"),
|
||||
matched_strategy,
|
||||
target_paragraph.text.strip(),
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"章节 %s 匹配到标题: %s",
|
||||
chapter_meta.get("id"),
|
||||
target_paragraph.text.strip(),
|
||||
)
|
||||
|
||||
self._insert_parsed_paragraphs(doc, target_paragraph, parsed_paragraphs)
|
||||
|
||||
doc.save(str(doc_path))
|
||||
logger.info(
|
||||
"成功填充内容到文档: %s", chapter_meta.get("placeholder", chapter_meta.get("id"))
|
||||
)
|
||||
|
||||
def _parse_markdown_to_paragraphs(self, content: str, chapter_level: int) -> list:
|
||||
"""解析Markdown内容为段落结构列表
|
||||
@ -209,30 +295,14 @@ class WordProcessor:
|
||||
|
||||
# Markdown标题:## 标题
|
||||
if MARKDOWN_HEADING_PATTERN.match(line):
|
||||
level = len(HEADING_LEVEL_PATTERN.match(line).group(1))
|
||||
title_text = MARKDOWN_HEADING_PATTERN.sub('', line)
|
||||
|
||||
# 3级标题内容:## → 加粗段落(非标题样式)
|
||||
if chapter_level == DEFAULT_CHAPTER_LEVEL:
|
||||
paragraphs.append({
|
||||
'type': 'bold_paragraph',
|
||||
'text': title_text,
|
||||
'indent': False
|
||||
})
|
||||
# 2级标题内容:## → Heading 4
|
||||
elif chapter_level == 2:
|
||||
paragraphs.append({
|
||||
'type': 'heading',
|
||||
'text': title_text,
|
||||
'level': 4
|
||||
})
|
||||
# 1级标题内容:## → Heading 3
|
||||
else:
|
||||
paragraphs.append({
|
||||
'type': 'heading',
|
||||
'text': title_text,
|
||||
'level': 3
|
||||
})
|
||||
title_text = MARKDOWN_HEADING_PATTERN.sub('', line).strip()
|
||||
if not title_text:
|
||||
i += 1
|
||||
continue
|
||||
paragraphs.append({
|
||||
'type': 'bold_paragraph',
|
||||
'text': title_text,
|
||||
})
|
||||
|
||||
# 有序列表:1. / 1) / (1)
|
||||
elif ORDERED_LIST_PATTERN.match(line):
|
||||
@ -267,6 +337,212 @@ class WordProcessor:
|
||||
|
||||
return paragraphs
|
||||
|
||||
def _extract_document_headings(self, doc: Document) -> List[Dict[str, Any]]:
|
||||
"""提取文档中所有标题段落,包含任意层级"""
|
||||
|
||||
headings: List[Dict[str, Any]] = []
|
||||
level_counters: defaultdict[int, int] = defaultdict(int)
|
||||
|
||||
for index, para in enumerate(doc.paragraphs):
|
||||
style_name = getattr(para.style, 'name', '') or ''
|
||||
if not style_name.lower().startswith('heading'):
|
||||
continue
|
||||
|
||||
level = self._infer_heading_level(para)
|
||||
level_key = level if level is not None else 0
|
||||
level_counters[level_key] += 1
|
||||
|
||||
if level is not None:
|
||||
for deeper_level in list(level_counters.keys()):
|
||||
if isinstance(deeper_level, int) and deeper_level > level:
|
||||
level_counters[deeper_level] = 0
|
||||
|
||||
headings.append({
|
||||
'paragraph': para,
|
||||
'index': index,
|
||||
'level': level,
|
||||
'style_name': style_name,
|
||||
'normalized_text': self._normalize_heading_text(para.text),
|
||||
'raw_text': para.text.strip(),
|
||||
'number': self._extract_leading_number(para.text),
|
||||
'order_index': level_counters[level_key],
|
||||
})
|
||||
|
||||
return headings
|
||||
|
||||
def _locate_heading(
|
||||
self,
|
||||
chapter_meta: Dict[str, Any],
|
||||
chapter_number: Optional[str],
|
||||
headings_index: List[Dict[str, Any]],
|
||||
) -> Tuple[Optional[Paragraph], int, Optional[str]]:
|
||||
"""根据章节元数据定位标题段落"""
|
||||
|
||||
attempts: List[Tuple[str, callable]] = []
|
||||
|
||||
if chapter_number:
|
||||
attempts.append(
|
||||
(
|
||||
"placeholder_number",
|
||||
lambda: self._match_heading_by_number(headings_index, chapter_number),
|
||||
)
|
||||
)
|
||||
|
||||
meta_number = chapter_meta.get("heading_number")
|
||||
if meta_number and meta_number != chapter_number:
|
||||
attempts.append(
|
||||
(
|
||||
"chapter_number",
|
||||
lambda: self._match_heading_by_number(headings_index, meta_number),
|
||||
)
|
||||
)
|
||||
|
||||
normalized_title = chapter_meta.get("normalized_title")
|
||||
if normalized_title:
|
||||
attempts.append(
|
||||
(
|
||||
"normalized_title",
|
||||
lambda: self._match_heading_by_title(headings_index, normalized_title),
|
||||
)
|
||||
)
|
||||
|
||||
title = chapter_meta.get("title")
|
||||
if title and (not normalized_title or self._normalize_heading_text(title) != normalized_title):
|
||||
attempts.append(
|
||||
(
|
||||
"raw_title",
|
||||
lambda: self._match_heading_by_title(
|
||||
headings_index, self._normalize_heading_text(title)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
level = chapter_meta.get("level")
|
||||
order_index = chapter_meta.get("order_index")
|
||||
if isinstance(order_index, int) and order_index > 0:
|
||||
attempts.append(
|
||||
(
|
||||
"sequence_index",
|
||||
lambda: self._match_heading_by_sequence(headings_index, level, order_index),
|
||||
)
|
||||
)
|
||||
|
||||
for strategy, matcher in attempts:
|
||||
paragraph, index = matcher()
|
||||
if paragraph is not None:
|
||||
return paragraph, index, strategy
|
||||
|
||||
if headings_index:
|
||||
fallback = headings_index[-1]
|
||||
return fallback['paragraph'], fallback['index'], "any_heading_fallback"
|
||||
|
||||
return None, -1, None
|
||||
|
||||
def _match_heading_by_number(
|
||||
self, headings_index: List[Dict[str, Any]], chapter_number: str
|
||||
) -> Tuple[Optional[Paragraph], int]:
|
||||
for heading in headings_index:
|
||||
text = heading['raw_text']
|
||||
stored_number = heading.get('number')
|
||||
if stored_number and stored_number == chapter_number:
|
||||
return heading['paragraph'], heading['index']
|
||||
if self._paragraph_matches_number(text, chapter_number):
|
||||
return heading['paragraph'], heading['index']
|
||||
return None, -1
|
||||
|
||||
def _match_heading_by_title(
|
||||
self, headings_index: List[Dict[str, Any]], normalized_title: str
|
||||
) -> Tuple[Optional[Paragraph], int]:
|
||||
if not normalized_title:
|
||||
return None, -1
|
||||
|
||||
for heading in headings_index:
|
||||
if heading['normalized_text'] == normalized_title:
|
||||
return heading['paragraph'], heading['index']
|
||||
|
||||
return None, -1
|
||||
|
||||
def _match_heading_by_sequence(
|
||||
self, headings_index: List[Dict[str, Any]], level: Optional[int], order_index: int
|
||||
) -> Tuple[Optional[Paragraph], int]:
|
||||
count = 0
|
||||
for heading in headings_index:
|
||||
heading_level = heading.get('level')
|
||||
if level is not None and heading_level != level:
|
||||
continue
|
||||
count += 1
|
||||
if count == order_index:
|
||||
return heading['paragraph'], heading['index']
|
||||
|
||||
return None, -1
|
||||
|
||||
@staticmethod
|
||||
def _paragraph_matches_number(text: str, chapter_number: str) -> bool:
|
||||
target = text.strip()
|
||||
candidates = [
|
||||
f"{chapter_number} ",
|
||||
f"{chapter_number}.",
|
||||
f"{chapter_number}。",
|
||||
f"{chapter_number}、",
|
||||
f"{chapter_number})",
|
||||
f"{chapter_number})",
|
||||
f"{chapter_number}.",
|
||||
]
|
||||
|
||||
return any(target.startswith(candidate) for candidate in candidates)
|
||||
|
||||
@staticmethod
|
||||
def _extract_leading_number(text: str | None) -> Optional[str]:
|
||||
if not text:
|
||||
return None
|
||||
|
||||
stripped = text.strip()
|
||||
match = re.match(r'^(\d+(?:\.\d+)*)', stripped)
|
||||
if match:
|
||||
return match.group(1).rstrip('.')
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _infer_heading_level(paragraph: Paragraph) -> Optional[int]:
|
||||
style_name = paragraph.style.name
|
||||
match = re.search(r"(\d+)$", style_name)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_heading_text(text: str | None) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
normalized = unicodedata.normalize("NFKC", text)
|
||||
normalized = re.sub(r"\s+", "", normalized)
|
||||
return normalized.lower()
|
||||
|
||||
def _append_content_to_document_end(
|
||||
self,
|
||||
doc: Document,
|
||||
chapter_meta: Dict[str, Any],
|
||||
parsed_paragraphs: List[dict],
|
||||
chapter_level: int,
|
||||
) -> None:
|
||||
notice_text = (
|
||||
f"【系统提示】未能定位原始章节“{chapter_meta.get('title', chapter_meta.get('id', ''))}”,"
|
||||
"以下内容已自动追加,请检查模板标题格式。"
|
||||
)
|
||||
notice_para = doc.add_paragraph(notice_text)
|
||||
for run in notice_para.runs:
|
||||
run.italic = True
|
||||
|
||||
fallback_heading = doc.add_paragraph()
|
||||
heading_text = chapter_meta.get('title') or chapter_meta.get('id') or "未命名章节"
|
||||
run = fallback_heading.add_run(heading_text)
|
||||
run.bold = True
|
||||
run.font.name = FONT_HEITI
|
||||
run.font.size = Pt(settings.heading_font_size)
|
||||
|
||||
self._insert_parsed_paragraphs(doc, fallback_heading, parsed_paragraphs)
|
||||
|
||||
def _find_heading_by_chapter_id(self, doc: Document, chapter_number: str) -> tuple[Paragraph, int]:
|
||||
"""根据章节编号查找标题段落
|
||||
|
||||
@ -282,9 +558,8 @@ class WordProcessor:
|
||||
"""
|
||||
for i, para in enumerate(doc.paragraphs):
|
||||
if para.style.name.startswith('Heading'):
|
||||
text = para.text.strip()
|
||||
if text.startswith(f"{chapter_number} ") or text.startswith(f"{chapter_number}."):
|
||||
logger.info(f"找到匹配标题: {text}")
|
||||
if self._paragraph_matches_number(para.text, chapter_number):
|
||||
logger.info(f"找到匹配标题: {para.text.strip()}")
|
||||
return para, i
|
||||
|
||||
raise ValueError(f"未找到编号为 {chapter_number} 的标题")
|
||||
@ -320,9 +595,16 @@ class WordProcessor:
|
||||
"""
|
||||
match = CHAPTER_ID_PATTERN.search(placeholder)
|
||||
if not match:
|
||||
# 支持自定义章节ID,但无法解析出数字编号时返回None
|
||||
if 'chapter_' in placeholder:
|
||||
return None
|
||||
raise ValueError(f"无法从占位符解析章节编号: {placeholder}")
|
||||
|
||||
chapter_number = match.group(1).replace('_', '.')
|
||||
chapter_body = match.group(1)
|
||||
if not chapter_body or not re.search(r'\d', chapter_body):
|
||||
return None
|
||||
|
||||
chapter_number = chapter_body.replace('_', '.')
|
||||
return chapter_number
|
||||
|
||||
def _create_paragraph_element(self, doc: Document, current_element, para_struct: dict) -> Paragraph:
|
||||
@ -347,19 +629,11 @@ class WordProcessor:
|
||||
para_type = para_struct['type']
|
||||
text = para_struct['text']
|
||||
|
||||
# 根据类型应用不同样式
|
||||
if para_type == 'heading':
|
||||
# 子标题:使用Heading样式
|
||||
level = para_struct.get('level', 4)
|
||||
try:
|
||||
new_para.style = f'Heading {level}'
|
||||
new_para.text = text
|
||||
except KeyError:
|
||||
run = new_para.add_run(text)
|
||||
run.bold = True
|
||||
run.font.size = Pt(settings.heading_font_size)
|
||||
para_type = 'bold_paragraph'
|
||||
|
||||
elif para_type == 'bold_paragraph':
|
||||
# 根据类型应用不同样式
|
||||
if para_type == 'bold_paragraph':
|
||||
# 加粗段落(3级标题内的小标题)
|
||||
try:
|
||||
new_para.style = 'Normal'
|
||||
@ -404,6 +678,20 @@ class WordProcessor:
|
||||
run.font.name = FONT_SONGTI
|
||||
run.font.size = Pt(settings.normal_font_size)
|
||||
|
||||
elif para_type == 'notice':
|
||||
try:
|
||||
new_para.style = 'Normal'
|
||||
except KeyError:
|
||||
pass
|
||||
para_format.first_line_indent = Cm(0)
|
||||
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
para_format.space_after = Pt(settings.space_after_small)
|
||||
|
||||
run = new_para.add_run(text)
|
||||
run.italic = True
|
||||
run.font.name = FONT_SONGTI
|
||||
run.font.size = Pt(settings.normal_font_size)
|
||||
|
||||
else: # paragraph
|
||||
# 普通段落
|
||||
try:
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from enum import Enum
|
||||
|
||||
@ -84,7 +83,7 @@ class WordFormatter:
|
||||
logger.warning(f"无法识别格式: {part}")
|
||||
|
||||
if not format_dict:
|
||||
logger.warning(f"未识别到任何格式,使用默认")
|
||||
logger.warning("未识别到任何格式,使用默认")
|
||||
return WordFormatter.get_default_format()
|
||||
|
||||
return format_dict
|
||||
|
||||
47
src/bidmaster/utils/guidance.py
Normal file
47
src/bidmaster/utils/guidance.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""章节写作提示构建工具。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
from ..tools.parser import ChapterGuidance, DocumentChapter, ScoringCriteria
|
||||
|
||||
|
||||
def _format_requirement(criterion: ScoringCriteria) -> str:
|
||||
base = f"{criterion.item_name}({criterion.max_score:g}分)"
|
||||
description = (criterion.description or "").strip().replace("\n", " ")
|
||||
if description:
|
||||
trimmed = description[:120].rstrip()
|
||||
if len(description) > 120:
|
||||
trimmed += "..."
|
||||
return f"{base}:{trimmed}"
|
||||
return base
|
||||
|
||||
|
||||
def _format_evidence(criterion: ScoringCriteria) -> str:
|
||||
return f"提供{criterion.item_name}相关的方案、记录或佐证材料"
|
||||
|
||||
|
||||
def build_chapter_guidance(
|
||||
chapter: DocumentChapter,
|
||||
criteria: Iterable[ScoringCriteria],
|
||||
context_snippets: Optional[List[str]] = None,
|
||||
) -> ChapterGuidance | None:
|
||||
"""根据评分项和上下文生成章节提示。"""
|
||||
|
||||
criteria_list = list(criteria)
|
||||
if not criteria_list:
|
||||
return None
|
||||
|
||||
key_requirements = [_format_requirement(c) for c in criteria_list[:3]]
|
||||
suggested_evidence = [_format_evidence(c) for c in criteria_list[:3]]
|
||||
snippets = (context_snippets or [])[:3]
|
||||
|
||||
return ChapterGuidance(
|
||||
key_requirements=key_requirements,
|
||||
suggested_evidence=suggested_evidence,
|
||||
context_snippets=snippets,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["build_chapter_guidance"]
|
||||
49
src/bidmaster/utils/monitoring.py
Normal file
49
src/bidmaster/utils/monitoring.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""基础监控与指标采集。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MetricRecord:
|
||||
name: str
|
||||
duration: float
|
||||
details: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
class MetricsRecorder:
|
||||
"""事件耗时记录器。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._records: List[MetricRecord] = []
|
||||
|
||||
@contextmanager
|
||||
def track(self, name: str, **details: str):
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
duration = time.perf_counter() - start
|
||||
self._records.append(MetricRecord(name=name, duration=duration, details=details))
|
||||
|
||||
def add(self, name: str, duration: float, **details: str) -> None:
|
||||
self._records.append(MetricRecord(name=name, duration=duration, details=details))
|
||||
|
||||
def to_dict(self) -> Dict[str, List[dict]]:
|
||||
return {
|
||||
"records": [
|
||||
{
|
||||
"name": record.name,
|
||||
"duration": round(record.duration, 4),
|
||||
"details": record.details,
|
||||
}
|
||||
for record in self._records
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["MetricsRecorder", "MetricRecord"]
|
||||
@ -46,6 +46,7 @@ class PromptPlanner:
|
||||
|
||||
objectives = self._build_objectives(requirements, metadata, emphasis)
|
||||
consistency_rules = self._build_consistency_rules(chapter_path, parent_context)
|
||||
guidance_notes = self._format_guidance_notes((metadata or {}).get("guidance") or chapter.get("guidance"))
|
||||
|
||||
context_parts: List[str] = []
|
||||
if parent_context:
|
||||
@ -68,6 +69,7 @@ class PromptPlanner:
|
||||
"context_summary": context_summary,
|
||||
"emphasis": emphasis or "",
|
||||
"category": (metadata or {}).get("category"),
|
||||
"guidance_notes": guidance_notes,
|
||||
}
|
||||
|
||||
return spec
|
||||
@ -175,6 +177,28 @@ class PromptPlanner:
|
||||
|
||||
return info
|
||||
|
||||
def _format_guidance_notes(self, guidance: Optional[Dict[str, Any]]) -> str:
|
||||
if not guidance:
|
||||
return ""
|
||||
|
||||
lines: List[str] = []
|
||||
key_requirements = guidance.get("key_requirements") or []
|
||||
if key_requirements:
|
||||
lines.append("【关键要求】")
|
||||
lines.extend(f"- {item}" for item in key_requirements)
|
||||
|
||||
suggested_evidence = guidance.get("suggested_evidence") or []
|
||||
if suggested_evidence:
|
||||
lines.append("【建议支撑】")
|
||||
lines.extend(f"- {item}" for item in suggested_evidence)
|
||||
|
||||
context_snippets = guidance.get("context_snippets") or []
|
||||
if context_snippets:
|
||||
lines.append("【招标摘录】")
|
||||
lines.extend(f"- {item}" for item in context_snippets)
|
||||
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
def _split_requirements(self, requirements: str) -> List[str]:
|
||||
if not requirements:
|
||||
return []
|
||||
|
||||
81
src/bidmaster/utils/quality.py
Normal file
81
src/bidmaster/utils/quality.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""质量检查工具。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class QualityIssue:
|
||||
code: str
|
||||
message: str
|
||||
severity: str = "warning" # info | warning | error
|
||||
|
||||
|
||||
class QualityChecklist:
|
||||
"""提供基础质量检查。"""
|
||||
|
||||
def __init__(self, *, min_content_len: int = 120) -> None:
|
||||
self.min_content_len = min_content_len
|
||||
|
||||
def check_generation(self, task: Dict[str, Any], content: str) -> List[QualityIssue]:
|
||||
issues: List[QualityIssue] = []
|
||||
|
||||
if not content or len(content.strip()) < self.min_content_len:
|
||||
issues.append(
|
||||
QualityIssue(
|
||||
code="content_too_short",
|
||||
message=f"生成内容长度不足 {self.min_content_len} 字符",
|
||||
)
|
||||
)
|
||||
|
||||
required_keywords = task.get("quality_keywords") or []
|
||||
missing = [kw for kw in required_keywords if kw not in content]
|
||||
if missing:
|
||||
issues.append(
|
||||
QualityIssue(
|
||||
code="missing_keywords",
|
||||
message=f"缺少关键字: {', '.join(missing)}",
|
||||
)
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
def check_assembly(self, chapter_map: Dict[str, Any], tasks: List[Dict[str, Any]]) -> List[QualityIssue]:
|
||||
issues: List[QualityIssue] = []
|
||||
|
||||
for task in tasks:
|
||||
chapter_id = task.get("chapter_id")
|
||||
if not chapter_id:
|
||||
issues.append(
|
||||
QualityIssue(
|
||||
code="missing_chapter",
|
||||
message=f"任务 {task.get('id')} 缺少章节映射",
|
||||
severity="error",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if chapter_id not in chapter_map:
|
||||
issues.append(
|
||||
QualityIssue(
|
||||
code="chapter_not_found",
|
||||
message=f"章节 {chapter_id} 在模板中未找到",
|
||||
severity="error",
|
||||
)
|
||||
)
|
||||
|
||||
status = task.get("status")
|
||||
if status not in {"completed", "approved"}:
|
||||
issues.append(
|
||||
QualityIssue(
|
||||
code="task_not_completed",
|
||||
message=f"任务 {task.get('id')} 状态为 {status}",
|
||||
)
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
__all__ = ["QualityChecklist", "QualityIssue"]
|
||||
62
src/bidmaster/utils/retry.py
Normal file
62
src/bidmaster/utils/retry.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""轻量重试策略实现。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, Iterable, List, Tuple, Type
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RetryAttempt:
|
||||
"""单次尝试记录。"""
|
||||
|
||||
attempt: int
|
||||
succeeded: bool
|
||||
error: Exception | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RetryReport:
|
||||
"""重试执行结果。"""
|
||||
|
||||
attempts: List[RetryAttempt] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def succeeded(self) -> bool:
|
||||
return any(item.succeeded for item in self.attempts)
|
||||
|
||||
|
||||
class RetryPolicy:
|
||||
"""简单重试策略。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
max_attempts: int = 2,
|
||||
retry_exceptions: Iterable[Type[Exception]] | None = None,
|
||||
) -> None:
|
||||
if max_attempts < 1:
|
||||
raise ValueError("max_attempts 必须 >= 1")
|
||||
self.max_attempts = max_attempts
|
||||
self.retry_exceptions = tuple(retry_exceptions or (Exception,))
|
||||
|
||||
def run(self, func: Callable, *args, **kwargs) -> Tuple[RetryReport, object | None]:
|
||||
report = RetryReport()
|
||||
last_exception: Exception | None = None
|
||||
|
||||
for attempt in range(1, self.max_attempts + 1):
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
report.attempts.append(RetryAttempt(attempt=attempt, succeeded=True))
|
||||
return report, result
|
||||
except self.retry_exceptions as exc: # type: ignore[misc]
|
||||
report.attempts.append(
|
||||
RetryAttempt(attempt=attempt, succeeded=False, error=exc)
|
||||
)
|
||||
last_exception = exc
|
||||
if last_exception is not None:
|
||||
raise last_exception
|
||||
raise RuntimeError("RetryPolicy未捕获到异常但执行失败")
|
||||
|
||||
|
||||
__all__ = ["RetryPolicy", "RetryReport", "RetryAttempt"]
|
||||
@ -339,8 +339,6 @@ def _interactive_readline_windows(prompt: str, timeout: int) -> Tuple[str, bool]
|
||||
|
||||
def _interactive_readline_posix(prompt: str, timeout: int) -> Tuple[str, bool]:
|
||||
import select
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
if fd < 0:
|
||||
|
||||
254
src/bidmaster/utils/validation.py
Normal file
254
src/bidmaster/utils/validation.py
Normal file
@ -0,0 +1,254 @@
|
||||
"""文件校验工具集。
|
||||
|
||||
提供对招标文件进行基础校验的能力,包括:
|
||||
- 路径有效性与文件大小检查
|
||||
- 格式支持与转换提示
|
||||
- 统一的告警与问题描述结构
|
||||
|
||||
该模块仅做轻量规则校验,不负责实际格式转换。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
SUPPORTED_PRIMARY_SUFFIXES = {".docx"}
|
||||
SUPPORTED_CONVERTIBLE_SUFFIXES = {".doc", ".pdf"}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FileValidationIssue:
|
||||
"""单条校验问题。"""
|
||||
|
||||
code: str
|
||||
message: str
|
||||
severity: str = "warning" # warning | error
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FileValidationReport:
|
||||
"""文件校验报告。"""
|
||||
|
||||
path: Path
|
||||
size_mb: float
|
||||
processable: bool
|
||||
requires_conversion: bool
|
||||
issues: List[FileValidationIssue] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""是否无阻塞问题。"""
|
||||
|
||||
return all(issue.severity != "error" for issue in self.issues)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为可序列化字典。"""
|
||||
|
||||
return {
|
||||
"path": str(self.path),
|
||||
"size_mb": round(self.size_mb, 2),
|
||||
"processable": self.processable,
|
||||
"requires_conversion": self.requires_conversion,
|
||||
"issues": [
|
||||
{
|
||||
"code": issue.code,
|
||||
"message": issue.message,
|
||||
"severity": issue.severity,
|
||||
}
|
||||
for issue in self.issues
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class FileValidator:
|
||||
"""招标文件校验器。"""
|
||||
|
||||
def __init__(self, *, max_size_mb: float = 80.0) -> None:
|
||||
self._max_size_mb = max_size_mb
|
||||
|
||||
def validate(self, file_path: str | Path) -> FileValidationReport:
|
||||
"""执行校验。
|
||||
|
||||
Args:
|
||||
file_path: 待检验文件路径
|
||||
|
||||
Returns:
|
||||
FileValidationReport
|
||||
"""
|
||||
|
||||
path = Path(file_path).expanduser().resolve()
|
||||
issues: List[FileValidationIssue] = []
|
||||
|
||||
if not path.exists():
|
||||
issues.append(
|
||||
FileValidationIssue(
|
||||
code="file_not_found",
|
||||
message=f"文件不存在: {path}",
|
||||
severity="error",
|
||||
)
|
||||
)
|
||||
return FileValidationReport(
|
||||
path=path,
|
||||
size_mb=0.0,
|
||||
processable=False,
|
||||
requires_conversion=False,
|
||||
issues=issues,
|
||||
)
|
||||
|
||||
size_mb = path.stat().st_size / (1024 * 1024)
|
||||
if size_mb == 0:
|
||||
issues.append(
|
||||
FileValidationIssue(
|
||||
code="empty_file",
|
||||
message="文件大小为0,无法解析",
|
||||
severity="error",
|
||||
)
|
||||
)
|
||||
|
||||
if size_mb > self._max_size_mb:
|
||||
issues.append(
|
||||
FileValidationIssue(
|
||||
code="file_too_large",
|
||||
message=f"文件大小 {size_mb:.1f}MB 超出限制({self._max_size_mb}MB)",
|
||||
)
|
||||
)
|
||||
|
||||
suffix = path.suffix.lower()
|
||||
processable = suffix in SUPPORTED_PRIMARY_SUFFIXES
|
||||
requires_conversion = False
|
||||
|
||||
if suffix in SUPPORTED_PRIMARY_SUFFIXES:
|
||||
pass
|
||||
elif suffix in SUPPORTED_CONVERTIBLE_SUFFIXES:
|
||||
requires_conversion = True
|
||||
issues.append(
|
||||
FileValidationIssue(
|
||||
code="needs_conversion",
|
||||
message=f"检测到 {suffix} 文件,建议先转换为 .docx 再解析",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issues.append(
|
||||
FileValidationIssue(
|
||||
code="unsupported_format",
|
||||
message=f"不支持的文件格式: {suffix}",
|
||||
severity="error",
|
||||
)
|
||||
)
|
||||
|
||||
return FileValidationReport(
|
||||
path=path,
|
||||
size_mb=size_mb,
|
||||
processable=processable,
|
||||
requires_conversion=requires_conversion,
|
||||
issues=issues,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ContentValidationIssue:
|
||||
"""章节内容校验问题"""
|
||||
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
class ContentValidationError(ValueError):
|
||||
"""章节内容校验异常"""
|
||||
|
||||
def __init__(self, issues: List[ContentValidationIssue]):
|
||||
self.issues = issues
|
||||
message = "; ".join(issue.message for issue in issues)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ContentValidator:
|
||||
"""章节内容合规校验器"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._opening_patterns = [
|
||||
re.compile(r"^第[一二三四五六七八九十百千0-9]+章"),
|
||||
re.compile(r"^[一二三四五六七八九十]+、"),
|
||||
]
|
||||
self._opening_blacklist = (
|
||||
"商务条款响应情况",
|
||||
"技术规格响应说明",
|
||||
"商务和技术方面存在的偏差说明",
|
||||
)
|
||||
self._forbidden_phrases = (
|
||||
"经认真研读招标文件要求",
|
||||
"现就商务和技术方面存在的偏差说明",
|
||||
"商务条款响应情况",
|
||||
"技术规格响应说明",
|
||||
)
|
||||
|
||||
def validate(self, chapter: Dict[str, Any], content: str) -> None:
|
||||
issues: List[ContentValidationIssue] = []
|
||||
|
||||
heading_issue = self.ensure_heading_alignment(chapter, content)
|
||||
if heading_issue:
|
||||
issues.append(ContentValidationIssue(code="heading_misaligned", message=heading_issue))
|
||||
|
||||
template_issue = self.detect_cross_chapter_signature(content)
|
||||
if template_issue:
|
||||
issues.append(ContentValidationIssue(code="template_detected", message=template_issue))
|
||||
|
||||
if issues:
|
||||
raise ContentValidationError(issues)
|
||||
|
||||
def ensure_heading_alignment(self, chapter: Dict[str, Any], content: str) -> Optional[str]:
|
||||
line = self._first_meaningful_line(content)
|
||||
if not line:
|
||||
return None
|
||||
|
||||
cleaned = self._normalize_line(line)
|
||||
|
||||
for pattern in self._opening_patterns:
|
||||
if pattern.match(cleaned):
|
||||
return (
|
||||
f"检测到新的章节标题“{line}”,请直接编写《{chapter.get('title', chapter.get('id'))}》正文内容"
|
||||
)
|
||||
|
||||
for keyword in self._opening_blacklist:
|
||||
if cleaned.startswith(keyword):
|
||||
return (
|
||||
f"检测到与章节无关的套话“{line}”,请按章节《{chapter.get('title', chapter.get('id'))}》展开"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def detect_cross_chapter_signature(self, content: str) -> Optional[str]:
|
||||
if not content:
|
||||
return None
|
||||
|
||||
lowered = content.strip()
|
||||
for phrase in self._forbidden_phrases:
|
||||
if phrase and phrase in lowered:
|
||||
return f"检测到跨章节模板短语“{phrase}”,请使用本章节专属内容"
|
||||
return None
|
||||
|
||||
def _first_meaningful_line(self, content: str) -> Optional[str]:
|
||||
for raw_line in content.splitlines():
|
||||
stripped = raw_line.strip()
|
||||
if stripped:
|
||||
return stripped
|
||||
return None
|
||||
|
||||
def _normalize_line(self, line: str) -> str:
|
||||
normalized = line.lstrip('#').strip()
|
||||
normalized = re.sub(r'^[\d\.\-•\s、()()]+', '', normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FileValidator",
|
||||
"FileValidationReport",
|
||||
"FileValidationIssue",
|
||||
"ContentValidator",
|
||||
"ContentValidationIssue",
|
||||
"ContentValidationError",
|
||||
]
|
||||
@ -1,7 +1,6 @@
|
||||
"""timeout_input 实用函数的单元测试"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
40
tests/unit/test_validation.py
Normal file
40
tests/unit/test_validation.py
Normal file
@ -0,0 +1,40 @@
|
||||
from bidmaster.utils.validation import ContentValidationError, ContentValidator
|
||||
|
||||
|
||||
def test_content_validator_rejects_cross_chapter_phrase() -> None:
|
||||
validator = ContentValidator()
|
||||
chapter = {"id": "chapter_1", "title": "服务方案"}
|
||||
content = (
|
||||
"经认真研读招标文件要求,结合我司实际情况,现就商务和技术方面存在的偏差说明如下:\n"
|
||||
"商务条款响应情况\n我司完全响应……"
|
||||
)
|
||||
|
||||
try:
|
||||
validator.validate(chapter, content)
|
||||
except ContentValidationError as exc:
|
||||
assert "跨章节模板" in str(exc)
|
||||
else:
|
||||
raise AssertionError("内容校验应当失败")
|
||||
|
||||
|
||||
def test_content_validator_accepts_normal_content() -> None:
|
||||
validator = ContentValidator()
|
||||
chapter = {"id": "chapter_2", "title": "技术路线"}
|
||||
content = (
|
||||
"我们以矿山智慧化平台为核心,构建“实时感知—风险预警—指挥调度”闭环。\n"
|
||||
"- 通过多源感知网络实现危险场景秒级采集;\n"
|
||||
"- 以知识图谱关联隐患并生成操作工序清单。"
|
||||
)
|
||||
|
||||
validator.validate(chapter, content)
|
||||
|
||||
|
||||
def test_content_validator_allows_legitimate_intro() -> None:
|
||||
validator = ContentValidator()
|
||||
chapter = {"id": "chapter_a", "title": "商务和技术偏差表"}
|
||||
content = (
|
||||
"在本次投标过程中,我们对招标文件进行了全面分析,并结合我司情况编制偏差说明。\n"
|
||||
"我们将在响应表中逐条列出商务及技术条款的差异与说明。"
|
||||
)
|
||||
|
||||
validator.validate(chapter, content)
|
||||
@ -1 +1,197 @@
|
||||
# Word处理器测试
|
||||
"""Word处理器章节填充逻辑单元测试"""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
from docx import Document
|
||||
|
||||
from bidmaster.tools.word import WordProcessor
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def word_processor() -> WordProcessor:
|
||||
return WordProcessor()
|
||||
|
||||
|
||||
def _create_document(path: Path, headings: list[tuple[str, int]]) -> None:
|
||||
doc = Document()
|
||||
for text, level in headings:
|
||||
doc.add_heading(text, level=level)
|
||||
doc.save(path)
|
||||
|
||||
|
||||
def _count_heading_paragraphs(doc: Document) -> int:
|
||||
return sum(1 for para in doc.paragraphs if para.style.name.startswith("Heading"))
|
||||
|
||||
|
||||
def test_fill_chapter_with_numbered_heading(word_processor: WordProcessor) -> None:
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
doc_path = Path(tmp_dir) / "numbered.docx"
|
||||
_create_document(doc_path, [("1 商务和技术偏差表", 1)])
|
||||
|
||||
chapter_meta = {
|
||||
"id": "chapter_1",
|
||||
"title": "商务和技术偏差表",
|
||||
"level": 1,
|
||||
"placeholder": "{{chapter_1_content}}",
|
||||
"normalized_title": word_processor._normalize_heading_text("商务和技术偏差表"),
|
||||
"heading_number": "1",
|
||||
"order_index": 1,
|
||||
}
|
||||
|
||||
word_processor.fill_chapter_content(doc_path, chapter_meta, "测试段落内容")
|
||||
|
||||
doc = Document(doc_path)
|
||||
assert any("测试段落内容" in para.text for para in doc.paragraphs)
|
||||
|
||||
|
||||
def test_fill_chapter_without_number_uses_title(word_processor: WordProcessor) -> None:
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
doc_path = Path(tmp_dir) / "title_only.docx"
|
||||
_create_document(doc_path, [("商务和技术偏差表", 1)])
|
||||
|
||||
chapter_meta = {
|
||||
"id": "chapter_1",
|
||||
"title": "商务和技术偏差表",
|
||||
"level": 1,
|
||||
"placeholder": "{{chapter_1_content}}",
|
||||
"normalized_title": word_processor._normalize_heading_text("商务和技术偏差表"),
|
||||
"heading_number": None,
|
||||
"order_index": 1,
|
||||
}
|
||||
|
||||
word_processor.fill_chapter_content(doc_path, chapter_meta, "服务方案内容")
|
||||
|
||||
doc = Document(doc_path)
|
||||
assert any("服务方案内容" in para.text for para in doc.paragraphs)
|
||||
|
||||
|
||||
def test_fill_chapter_sequence_fallback(word_processor: WordProcessor) -> None:
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
doc_path = Path(tmp_dir) / "sequence.docx"
|
||||
_create_document(
|
||||
doc_path,
|
||||
[
|
||||
("项目背景", 1),
|
||||
("售后服务和质保期服务计划", 1),
|
||||
],
|
||||
)
|
||||
|
||||
chapter_meta = {
|
||||
"id": "chapter_11",
|
||||
"title": "售后服务和质保期服务计划",
|
||||
"level": 1,
|
||||
"placeholder": "{{chapter_11_content}}",
|
||||
"normalized_title": word_processor._normalize_heading_text("售后服务和质保期服务计划"),
|
||||
"heading_number": None,
|
||||
"order_index": 2,
|
||||
}
|
||||
|
||||
word_processor.fill_chapter_content(doc_path, chapter_meta, "售后保障措施")
|
||||
|
||||
doc = Document(doc_path)
|
||||
assert any("售后保障措施" in para.text for para in doc.paragraphs)
|
||||
|
||||
|
||||
def test_fill_chapter_appends_when_not_found(word_processor: WordProcessor) -> None:
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
doc_path = Path(tmp_dir) / "fallback.docx"
|
||||
_create_document(doc_path, [("项目概述", 1)])
|
||||
|
||||
chapter_meta = {
|
||||
"id": "chapter_missing",
|
||||
"title": "未匹配章节",
|
||||
"level": 1,
|
||||
"placeholder": "{{chapter_missing_content}}",
|
||||
"normalized_title": word_processor._normalize_heading_text("未匹配章节"),
|
||||
"heading_number": None,
|
||||
"order_index": 2,
|
||||
}
|
||||
|
||||
word_processor.fill_chapter_content(doc_path, chapter_meta, "新增内容")
|
||||
|
||||
doc = Document(doc_path)
|
||||
assert any("【系统提示】" in para.text for para in doc.paragraphs)
|
||||
assert any("新增内容" in para.text for para in doc.paragraphs)
|
||||
|
||||
|
||||
def test_fill_does_not_create_additional_headings(word_processor: WordProcessor) -> None:
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
doc_path = Path(tmp_dir) / "no_new_headings.docx"
|
||||
_create_document(
|
||||
doc_path,
|
||||
[
|
||||
("1 总体方案", 1),
|
||||
("1.1 服务范围", 2),
|
||||
],
|
||||
)
|
||||
|
||||
before = Document(doc_path)
|
||||
before_count = _count_heading_paragraphs(before)
|
||||
|
||||
chapter_meta = {
|
||||
"id": "chapter_1_1",
|
||||
"title": "服务范围",
|
||||
"level": 2,
|
||||
"placeholder": "{{chapter_1_1_content}}",
|
||||
"normalized_title": word_processor._normalize_heading_text("服务范围"),
|
||||
"heading_number": "1.1",
|
||||
"order_index": 1,
|
||||
}
|
||||
|
||||
content = "## 小节标题\n服务内容描述"
|
||||
word_processor.fill_chapter_content(doc_path, chapter_meta, content)
|
||||
|
||||
after = Document(doc_path)
|
||||
after_count = _count_heading_paragraphs(after)
|
||||
assert after_count == before_count
|
||||
|
||||
|
||||
def test_markdown_headings_render_as_bold_paragraphs(word_processor: WordProcessor) -> None:
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
doc_path = Path(tmp_dir) / "markdown.docx"
|
||||
_create_document(doc_path, [("1 技术方案", 1)])
|
||||
|
||||
chapter_meta = {
|
||||
"id": "chapter_1",
|
||||
"title": "技术方案",
|
||||
"level": 1,
|
||||
"placeholder": "{{chapter_1_content}}",
|
||||
"normalized_title": word_processor._normalize_heading_text("技术方案"),
|
||||
"heading_number": "1",
|
||||
"order_index": 1,
|
||||
}
|
||||
|
||||
word_processor.fill_chapter_content(
|
||||
doc_path,
|
||||
chapter_meta,
|
||||
"## 子标题\n段落内容",
|
||||
)
|
||||
|
||||
doc = Document(doc_path)
|
||||
bold_paras = [para for para in doc.paragraphs if para.text.strip() == "子标题"]
|
||||
assert bold_paras, "应生成加粗段落"
|
||||
assert all(not para.style.name.startswith("Heading") for para in bold_paras)
|
||||
assert any(any(run.bold for run in para.runs) for para in bold_paras)
|
||||
|
||||
|
||||
def test_fill_matches_higher_level_heading(word_processor: WordProcessor) -> None:
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
doc_path = Path(tmp_dir) / "deep_heading.docx"
|
||||
_create_document(doc_path, [("1.1.1.1 深度章节", 4)])
|
||||
|
||||
chapter_meta = {
|
||||
"id": "chapter_1_1_1_1",
|
||||
"title": "深度章节",
|
||||
"level": 4,
|
||||
"placeholder": "{{chapter_1_1_1_1_content}}",
|
||||
"normalized_title": word_processor._normalize_heading_text("深度章节"),
|
||||
"heading_number": "1.1.1.1",
|
||||
"order_index": 1,
|
||||
}
|
||||
|
||||
word_processor.fill_chapter_content(doc_path, chapter_meta, "深度内容")
|
||||
|
||||
doc = Document(doc_path)
|
||||
assert any("深度内容" in para.text for para in doc.paragraphs)
|
||||
Loading…
Reference in New Issue
Block a user