feat: 实现标书内容填写完整流程并修复关键bug
新增功能: - 实现ContentWriterAgent内容填写协调器 - 实现SingleChapterAgent单章节处理流程 - 新增write命令组支持标书内容自动填写 - 支持深度优先遍历章节队列 - 实现每章节生成后立即保存到Word 核心修复: 1. 修复章节排序bug:改用数字排序避免 chapter_1_10 排在 chapter_1_2 前面 2. 修复占位符替换丢失格式:保留段落格式而非直接替换paragraph.text 3. 修复占位符缺失问题:添加降级策略,无占位符时自动在标题后插入 4. 实现Markdown到Word格式智能转换: - 根据章节层级自动映射样式 - ## 标题 → 3级内容用加粗段落,2级内容用Heading 4 - 有序列表 → 1)2)3)编号格式 - 无序列表 → • 实心圆点 - 自动应用正文格式:首行缩进、1.5倍行距、宋体12磅 技术改进: - 使用python-docx底层XML API精确控制段落插入位置 - 实现增量保存避免内容丢失 - 添加详细的段落格式配置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5d16d8badc
commit
5d319fd2a6
501
111 - 副本/analysis_result.json
Normal file
501
111 - 副本/analysis_result.json
Normal file
@ -0,0 +1,501 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
68
111 - 副本/tasks.json
Normal file
68
111 - 副本/tasks.json
Normal file
@ -0,0 +1,68 @@
|
||||
[
|
||||
{
|
||||
"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}}"
|
||||
}
|
||||
]
|
||||
@ -3,12 +3,14 @@
|
||||
from .analysis import AnalysisAgent
|
||||
from .builders import TocAgentBuilder
|
||||
from .builders.toc_builder import TocAgent
|
||||
from .content_writer import ContentWriterAgent
|
||||
from .interaction import InteractionHandler, InteractionMode
|
||||
|
||||
__all__ = [
|
||||
"AnalysisAgent",
|
||||
"TocAgentBuilder",
|
||||
"TocAgent",
|
||||
"ContentWriterAgent",
|
||||
"InteractionHandler",
|
||||
"InteractionMode"
|
||||
"InteractionMode",
|
||||
]
|
||||
@ -3,8 +3,7 @@
|
||||
包含各种Agent的构建器实现。
|
||||
"""
|
||||
|
||||
from .content_builder import ContentWriterAgentBuilder
|
||||
from .toc_builder import TocAgentBuilder
|
||||
|
||||
__all__ = [
|
||||
"TocAgentBuilder"
|
||||
]
|
||||
__all__ = ["TocAgentBuilder", "ContentWriterAgentBuilder"]
|
||||
125
src/bidmaster/agents/builders/content_builder.py
Normal file
125
src/bidmaster/agents/builders/content_builder.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""内容填写Agent构建器
|
||||
|
||||
构建内容填写工作流的Agent。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..base import AgentBuilder
|
||||
from ...nodes.content import (
|
||||
InitConfigNode,
|
||||
PrepareChapterNode,
|
||||
InteractWithUserNode,
|
||||
GenerateContentNode,
|
||||
SaveToWordNode,
|
||||
CollectResultsNode,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def need_user_interaction(state: Dict[str, Any]) -> str:
|
||||
"""判断是否需要用户交互
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
|
||||
Returns:
|
||||
路由键:"interact"或"generate"
|
||||
"""
|
||||
needs_interaction = state.get("needs_interaction", False)
|
||||
return "interact" if needs_interaction else "generate"
|
||||
|
||||
|
||||
def should_continue_loop(state: Dict[str, Any]) -> str:
|
||||
"""判断是否继续循环处理章节
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
|
||||
Returns:
|
||||
路由键:"continue"或"collect"
|
||||
"""
|
||||
current_index = state.get("current_chapter_index", 0)
|
||||
total = len(state.get("chapter_queue", []))
|
||||
|
||||
decision = "continue" if current_index < total else "collect"
|
||||
logger.info(f"循环判断: index={current_index}/{total} -> {decision}")
|
||||
|
||||
if current_index < total:
|
||||
return "continue"
|
||||
return "collect"
|
||||
|
||||
|
||||
class ContentWriterAgentBuilder(AgentBuilder):
|
||||
"""内容填写Agent构建器
|
||||
|
||||
构建包含以下流程的Agent:
|
||||
1. 初始化配置并生成章节队列
|
||||
2. 循环处理每个章节:
|
||||
- 准备章节
|
||||
- 用户交互(仅1级标题)
|
||||
- 生成内容
|
||||
- 保存到Word
|
||||
3. 收集结果
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create(cls, interaction_handler=None) -> "ContentWriterAgentBuilder":
|
||||
"""创建预配置的内容填写Agent构建器
|
||||
|
||||
Args:
|
||||
interaction_handler: 交互处理函数
|
||||
|
||||
Returns:
|
||||
配置完成的构建器实例
|
||||
"""
|
||||
builder = cls(interaction_handler)
|
||||
|
||||
# 添加所有节点
|
||||
builder.add_node(InitConfigNode()) \
|
||||
.add_node(PrepareChapterNode()) \
|
||||
.add_node(InteractWithUserNode()) \
|
||||
.add_node(GenerateContentNode()) \
|
||||
.add_node(SaveToWordNode()) \
|
||||
.add_node(CollectResultsNode())
|
||||
|
||||
# 设置入口点
|
||||
builder.set_entry("init_config")
|
||||
|
||||
# 配置工作流
|
||||
builder._configure_workflow()
|
||||
|
||||
logger.info("ContentWriterAgent构建器创建完成")
|
||||
return builder
|
||||
|
||||
def _configure_workflow(self) -> None:
|
||||
"""配置工作流程"""
|
||||
# init_config → prepare_chapter
|
||||
self.add_edge("init_config", "prepare_chapter")
|
||||
|
||||
# prepare_chapter → 条件分支(是否需要交互)
|
||||
self.add_conditional_edge(
|
||||
"prepare_chapter",
|
||||
need_user_interaction,
|
||||
{"interact": "interact_user", "generate": "generate_content"},
|
||||
)
|
||||
|
||||
# interact_user → generate_content
|
||||
self.add_edge("interact_user", "generate_content")
|
||||
|
||||
# generate_content → save_to_word
|
||||
self.add_edge("generate_content", "save_to_word")
|
||||
|
||||
# save_to_word → 条件分支(是否继续循环)
|
||||
self.add_conditional_edge(
|
||||
"save_to_word",
|
||||
should_continue_loop,
|
||||
{"continue": "prepare_chapter", "collect": "collect_results"},
|
||||
)
|
||||
|
||||
# collect_results → END
|
||||
self.add_edge("collect_results", "END")
|
||||
|
||||
logger.info("ContentWriterAgent工作流配置完成")
|
||||
125
src/bidmaster/agents/content_writer.py
Normal file
125
src/bidmaster/agents/content_writer.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""内容填写Agent
|
||||
|
||||
负责标书章节内容的自动填写(协调器模式)。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .single_chapter_agent import SingleChapterAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContentWriterAgent:
|
||||
"""内容填写Agent(协调器)
|
||||
|
||||
提供标书章节内容的自动填写功能:
|
||||
1. 读取章节队列
|
||||
2. 循环调用SingleChapterAgent处理每个章节
|
||||
3. 汇总结果
|
||||
"""
|
||||
|
||||
def __init__(self, interaction_handler=None):
|
||||
"""初始化ContentWriterAgent
|
||||
|
||||
Args:
|
||||
interaction_handler: 交互处理函数
|
||||
"""
|
||||
self.interaction_handler = interaction_handler
|
||||
|
||||
def write_content_sync(self, word_file: str) -> Dict[str, Any]:
|
||||
"""同步执行内容填写
|
||||
|
||||
Args:
|
||||
word_file: Word文档路径
|
||||
|
||||
Returns:
|
||||
执行结果字典
|
||||
"""
|
||||
# 验证参数
|
||||
word_path = Path(word_file)
|
||||
if not word_path.exists():
|
||||
raise FileNotFoundError(f"Word文档不存在: {word_file}")
|
||||
|
||||
logger.info(f"开始内容填写流程: {word_file}")
|
||||
|
||||
# 初始化章节队列(直接从Word读取)
|
||||
from ..nodes.content.init_config import InitConfigNode
|
||||
from ..nodes.base import NodeContext
|
||||
|
||||
init_node = InitConfigNode()
|
||||
context = NodeContext(self.interaction_handler)
|
||||
|
||||
init_state = {
|
||||
"word_file": word_file,
|
||||
}
|
||||
|
||||
state = init_node.execute(init_state, context)
|
||||
chapter_queue = state["chapter_queue"]
|
||||
|
||||
logger.info(f"共 {len(chapter_queue)} 个章节待处理")
|
||||
for i, ch in enumerate(chapter_queue):
|
||||
logger.info(f" [{i+1}] {ch['id']} - {ch['title']} (level={ch['level']})")
|
||||
|
||||
# 循环处理每个章节
|
||||
state["chapter_configs"] = {}
|
||||
state["generated_contents"] = {}
|
||||
state["completed_chapters"] = []
|
||||
state["interaction_handler"] = self.interaction_handler
|
||||
|
||||
for index, chapter in enumerate(chapter_queue):
|
||||
logger.info(f"处理章节 [{index + 1}/{len(chapter_queue)}]: {chapter['id']} - {chapter['title']}")
|
||||
|
||||
state["current_chapter_index"] = index
|
||||
|
||||
# 创建子Agent处理单个章节
|
||||
chapter_agent = SingleChapterAgent(self.interaction_handler)
|
||||
result_state = chapter_agent.process_chapter(chapter, state)
|
||||
|
||||
# 显示生成的内容
|
||||
chapter_id = chapter['id']
|
||||
generated_content = result_state.get("generated_contents", {}).get(chapter_id, "")
|
||||
if generated_content:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"✅ 章节 {chapter_id} - {chapter['title']} 生成完成")
|
||||
print(f"{'='*60}")
|
||||
print(generated_content)
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# 更新全局状态
|
||||
state["chapter_configs"].update(result_state.get("chapter_configs", {}))
|
||||
state["generated_contents"].update(result_state.get("generated_contents", {}))
|
||||
state["completed_chapters"] = result_state.get("completed_chapters", [])
|
||||
|
||||
# 汇总结果
|
||||
total_words = sum(len(content) for content in state["generated_contents"].values())
|
||||
|
||||
results = {
|
||||
"success": True,
|
||||
"total_chapters": len(chapter_queue),
|
||||
"completed_count": len(state["completed_chapters"]),
|
||||
"total_words": total_words,
|
||||
"word_file": word_file,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"内容填写完成: {results['completed_count']}/{results['total_chapters']} 章节, {total_words}字"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def create_silent(cls):
|
||||
"""创建静默模式Agent"""
|
||||
from .interaction import InteractionHandler, InteractionMode
|
||||
handler = InteractionHandler(mode=InteractionMode.SILENT)
|
||||
return cls(handler)
|
||||
|
||||
@classmethod
|
||||
def create_interactive(cls):
|
||||
"""创建交互模式Agent"""
|
||||
from .interaction import InteractionHandler, InteractionMode
|
||||
handler = InteractionHandler(mode=InteractionMode.INTERACTIVE)
|
||||
return cls(handler)
|
||||
@ -167,9 +167,13 @@ class InteractionHandler:
|
||||
Returns:
|
||||
输入的文本
|
||||
"""
|
||||
# 先用console.print显示格式化的提示信息
|
||||
self.console.print(f"\n{prompt}", style="blue")
|
||||
|
||||
while True:
|
||||
try:
|
||||
result = click.prompt(prompt, default=default, type=str)
|
||||
# 使用简化的prompt获取输入
|
||||
result = click.prompt("", default=default or "", type=str, show_default=False)
|
||||
|
||||
# 验证输入
|
||||
if validation:
|
||||
|
||||
88
src/bidmaster/agents/single_chapter_agent.py
Normal file
88
src/bidmaster/agents/single_chapter_agent.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""单章节处理Agent
|
||||
|
||||
负责处理单个章节的完整流程。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from .base import BaseAgent, AgentBuilder
|
||||
from ..nodes.content import (
|
||||
InteractWithUserNode,
|
||||
GenerateContentNode,
|
||||
SaveToWordNode,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def need_user_interaction(state: Dict[str, Any]) -> str:
|
||||
"""判断是否需要用户交互"""
|
||||
needs_interaction = state.get("needs_interaction", False)
|
||||
return "interact" if needs_interaction else "generate"
|
||||
|
||||
|
||||
def route_start(state: Dict[str, Any]) -> str:
|
||||
"""入口路由:判断是否需要交互"""
|
||||
needs_interaction = state.get("needs_interaction", False)
|
||||
return "interact" if needs_interaction else "generate"
|
||||
|
||||
|
||||
class SingleChapterAgentBuilder(AgentBuilder):
|
||||
"""单章节Agent构建器"""
|
||||
|
||||
@classmethod
|
||||
def create(cls, interaction_handler=None) -> "SingleChapterAgentBuilder":
|
||||
"""创建单章节Agent构建器"""
|
||||
builder = cls(interaction_handler)
|
||||
|
||||
# 添加节点(去掉PrepareChapterNode,因为外层已准备好)
|
||||
builder.add_node(InteractWithUserNode()) \
|
||||
.add_node(GenerateContentNode()) \
|
||||
.add_node(SaveToWordNode())
|
||||
|
||||
# 设置入口(interact_user内部会判断是否真的需要交互)
|
||||
builder.set_entry("interact_user")
|
||||
|
||||
# 配置流程:线性流程
|
||||
builder.add_edge("interact_user", "generate_content")
|
||||
builder.add_edge("generate_content", "save_to_word")
|
||||
builder.add_edge("save_to_word", "END")
|
||||
|
||||
return builder
|
||||
|
||||
|
||||
class SingleChapterAgent(BaseAgent):
|
||||
"""单章节处理Agent"""
|
||||
|
||||
def __init__(self, interaction_handler=None):
|
||||
builder = SingleChapterAgentBuilder.create(interaction_handler)
|
||||
super().__init__(builder)
|
||||
|
||||
def process_chapter(self, chapter: Dict[str, Any], state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""处理单个章节
|
||||
|
||||
Args:
|
||||
chapter: 章节信息
|
||||
state: 全局状态(包含word_file等)
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
# 准备单章节状态
|
||||
chapter_state = {
|
||||
"word_file": state["word_file"],
|
||||
"current_chapter": chapter,
|
||||
"current_chapter_index": state.get("current_chapter_index", 0),
|
||||
"chapter_queue": state.get("chapter_queue", []),
|
||||
"chapter_configs": state.get("chapter_configs", {}),
|
||||
"generated_contents": state.get("generated_contents", {}),
|
||||
"completed_chapters": state.get("completed_chapters", []),
|
||||
"needs_interaction": chapter["level"] == 1,
|
||||
"interaction_handler": state.get("interaction_handler"),
|
||||
}
|
||||
|
||||
# 执行
|
||||
result_state = self.execute_sync(chapter_state)
|
||||
|
||||
return result_state
|
||||
@ -7,10 +7,11 @@ import logging
|
||||
|
||||
import click
|
||||
|
||||
from .assemble import assemble
|
||||
from .generate import generate
|
||||
from .kb import kb
|
||||
from .project import project
|
||||
from .generate import generate
|
||||
from .assemble import assemble
|
||||
from .write import write
|
||||
|
||||
|
||||
@click.group()
|
||||
@ -31,6 +32,7 @@ def cli():
|
||||
cli.add_command(kb, name="kb")
|
||||
cli.add_command(project, name="project")
|
||||
cli.add_command(generate, name="generate")
|
||||
cli.add_command(write, name="write")
|
||||
cli.add_command(assemble, name="assemble")
|
||||
|
||||
|
||||
@ -46,6 +48,9 @@ def info():
|
||||
print(" status - 查看项目状态")
|
||||
print(" generate - 内容生成")
|
||||
print(" task - 生成任务内容")
|
||||
print(" write - 内容填写")
|
||||
print(" start - 开始填写标书内容")
|
||||
print(" chapter - 填写指定章节")
|
||||
print(" assemble - 合规组装")
|
||||
print(" tables - 生成表格")
|
||||
print(" info - 显示系统信息")
|
||||
|
||||
181
src/bidmaster/cli/write.py
Normal file
181
src/bidmaster/cli/write.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""内容填写命令
|
||||
|
||||
提供标书内容自动填写功能。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
|
||||
from ..agents import ContentWriterAgent
|
||||
|
||||
console = Console()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@click.group()
|
||||
def write():
|
||||
"""内容填写命令组"""
|
||||
pass
|
||||
|
||||
|
||||
@write.command()
|
||||
@click.argument(
|
||||
"project_dir",
|
||||
type=click.Path(exists=True),
|
||||
default=".",
|
||||
)
|
||||
@click.option(
|
||||
"--mode",
|
||||
"-m",
|
||||
type=click.Choice(["interactive", "silent"]),
|
||||
default="interactive",
|
||||
help="交互模式:interactive(交互)|silent(静默)",
|
||||
)
|
||||
def start(project_dir: str, mode: str):
|
||||
"""开始填写标书内容
|
||||
|
||||
PROJECT_DIR: 项目目录路径(默认当前目录)
|
||||
|
||||
示例:
|
||||
bidmaster write start # 在当前目录
|
||||
bidmaster write start ./my-project # 指定项目目录
|
||||
bidmaster write start ./my-project -m silent # 静默模式
|
||||
|
||||
深度优先遍历章节,仅在1级标题时与用户交互。
|
||||
"""
|
||||
try:
|
||||
console.print("📝 BidMaster 内容填写工具", style="bold blue")
|
||||
console.print()
|
||||
|
||||
project_path = Path(project_dir)
|
||||
|
||||
if not project_path.exists():
|
||||
console.print(f"❌ 项目目录不存在: {project_dir}", style="red")
|
||||
return
|
||||
|
||||
console.print(f"📁 项目目录: {project_path.absolute()}", style="dim")
|
||||
|
||||
# 自动查找Word文件
|
||||
word_file = _find_word_file(project_path)
|
||||
if not word_file:
|
||||
console.print("❌ 未找到Word文档", style="red")
|
||||
return
|
||||
console.print(f"📄 找到Word文档: {Path(word_file).name}", style="green")
|
||||
console.print(f"📋 将直接从Word文档提取章节结构", style="dim")
|
||||
|
||||
# 创建Agent
|
||||
if mode == "silent":
|
||||
agent = ContentWriterAgent.create_silent()
|
||||
console.print("📌 使用静默模式(自动选择默认值)", style="dim")
|
||||
else:
|
||||
agent = ContentWriterAgent.create_interactive()
|
||||
console.print("📌 使用交互模式", style="dim")
|
||||
|
||||
console.print()
|
||||
console.print("🚀 开始内容填写流程...", style="blue")
|
||||
if mode == "interactive":
|
||||
console.print("💡 提示:仅在1级标题时会询问您的意见", style="yellow")
|
||||
console.print()
|
||||
|
||||
# 执行内容填写
|
||||
# 注意:交互模式不使用Progress,避免干扰用户输入
|
||||
if mode == "silent":
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task("正在处理章节...", total=None)
|
||||
result = agent.write_content_sync(word_file=word_file)
|
||||
progress.update(task, completed=True)
|
||||
else:
|
||||
# 交互模式:不使用Progress,避免覆盖用户输入
|
||||
result = agent.write_content_sync(word_file=word_file)
|
||||
|
||||
# 显示结果
|
||||
if result.get("success"):
|
||||
console.print("\n✅ 内容填写完成!", style="bold green")
|
||||
|
||||
stats_panel = Panel(
|
||||
f"📊 总章节数: {result.get('total_chapters', 0)}\n"
|
||||
f"✓ 完成章节: {result.get('completed_count', 0)}\n"
|
||||
f"📝 总字数: {result.get('total_words', 0)}\n"
|
||||
f"📄 输出文件: {result.get('word_file')}",
|
||||
title="生成统计",
|
||||
title_align="left",
|
||||
)
|
||||
console.print("\n", stats_panel)
|
||||
|
||||
console.print(f"\n🎯 下一步:", style="dim")
|
||||
console.print(f" • 打开 {Path(result['word_file']).name} 查看生成的内容", style="dim")
|
||||
console.print(f" • 使用 Ctrl+A -> F9 刷新Word中的目录和页码", style="dim")
|
||||
else:
|
||||
console.print("\n❌ 内容填写失败", style="red")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"\n❌ 执行失败: {e}", style="red")
|
||||
logger.exception("内容填写失败")
|
||||
|
||||
|
||||
@write.command()
|
||||
@click.argument("chapter_id", type=str)
|
||||
@click.option(
|
||||
"--word-file",
|
||||
"-w",
|
||||
type=click.Path(exists=True),
|
||||
help="Word文档路径",
|
||||
)
|
||||
@click.option(
|
||||
"--project-dir",
|
||||
"-p",
|
||||
default=".",
|
||||
help="项目目录路径",
|
||||
)
|
||||
def chapter(chapter_id: str, word_file: str, project_dir: str):
|
||||
"""填写指定章节
|
||||
|
||||
示例:bidmaster write chapter "1.1"
|
||||
"""
|
||||
console.print(f"📝 填写章节: {chapter_id}", style="bold blue")
|
||||
console.print("\n⚠️ 此功能尚未实现,请使用 'write start' 命令", style="yellow")
|
||||
console.print(
|
||||
"提示:'write start' 会遍历所有章节,您可以在1级标题时选择性跳过", style="dim"
|
||||
)
|
||||
|
||||
|
||||
def _find_word_file(project_path: Path) -> str:
|
||||
"""在项目目录中查找Word文档
|
||||
|
||||
Args:
|
||||
project_path: 项目目录
|
||||
|
||||
Returns:
|
||||
Word文档路径,未找到返回None
|
||||
"""
|
||||
# 查找.docx文件
|
||||
docx_files = list(project_path.glob("*.docx"))
|
||||
|
||||
# 过滤掉临时文件(~$开头)
|
||||
docx_files = [f for f in docx_files if not f.name.startswith("~$")]
|
||||
|
||||
if len(docx_files) == 1:
|
||||
return str(docx_files[0])
|
||||
elif len(docx_files) > 1:
|
||||
console.print(f"\n⚠️ 找到多个Word文档:", style="yellow")
|
||||
for i, f in enumerate(docx_files, 1):
|
||||
console.print(f" {i}. {f.name}")
|
||||
console.print("\n提示:将只有一个.docx文件放在项目目录中", style="yellow")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
write()
|
||||
20
src/bidmaster/nodes/content/__init__.py
Normal file
20
src/bidmaster/nodes/content/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""内容填写节点模块
|
||||
|
||||
提供标书内容填写的完整工作流节点。
|
||||
"""
|
||||
|
||||
from .init_config import InitConfigNode
|
||||
from .prepare_chapter import PrepareChapterNode
|
||||
from .interact_user import InteractWithUserNode
|
||||
from .generate_content import GenerateContentNode
|
||||
from .save_to_word import SaveToWordNode
|
||||
from .collect_results import CollectResultsNode
|
||||
|
||||
__all__ = [
|
||||
"InitConfigNode",
|
||||
"PrepareChapterNode",
|
||||
"InteractWithUserNode",
|
||||
"GenerateContentNode",
|
||||
"SaveToWordNode",
|
||||
"CollectResultsNode",
|
||||
]
|
||||
65
src/bidmaster/nodes/content/collect_results.py
Normal file
65
src/bidmaster/nodes/content/collect_results.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""结果收集节点
|
||||
|
||||
汇总所有章节的生成结果和统计信息。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CollectResultsNode(BaseNode):
|
||||
"""结果收集节点
|
||||
|
||||
职责:
|
||||
1. 汇总所有已完成章节
|
||||
2. 统计生成结果
|
||||
3. 返回最终状态
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "collect_results"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "收集并汇总生成结果"
|
||||
|
||||
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
|
||||
"""执行结果收集
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
更新后的状态
|
||||
"""
|
||||
completed_chapters = state.get("completed_chapters", [])
|
||||
chapter_queue = state.get("chapter_queue", [])
|
||||
generated_contents = state.get("generated_contents", {})
|
||||
|
||||
total_chapters = len(chapter_queue)
|
||||
completed_count = len(completed_chapters)
|
||||
|
||||
# 统计字数
|
||||
total_words = sum(len(content) for content in generated_contents.values())
|
||||
|
||||
logger.info(
|
||||
f"内容生成完成!完成章节: {completed_count}/{total_chapters}, 总字数: {total_words}"
|
||||
)
|
||||
|
||||
# 构建结果摘要
|
||||
results_summary = {
|
||||
"success": True,
|
||||
"total_chapters": total_chapters,
|
||||
"completed_count": completed_count,
|
||||
"total_words": total_words,
|
||||
"completed_chapters": completed_chapters,
|
||||
"word_file": state.get("word_file"),
|
||||
}
|
||||
|
||||
return self._update_state(state, should_continue=False, results=results_summary)
|
||||
211
src/bidmaster/nodes/content/generate_content.py
Normal file
211
src/bidmaster/nodes/content/generate_content.py
Normal file
@ -0,0 +1,211 @@
|
||||
"""内容生成节点
|
||||
|
||||
调用RAG工具生成章节内容。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GenerateContentNode(BaseNode):
|
||||
"""内容生成节点
|
||||
|
||||
职责:
|
||||
1. 获取当前章节配置(如果是子章节,继承父章节配置)
|
||||
2. 调用RAGTool生成内容
|
||||
3. 保存生成结果到state
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "generate_content"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "生成章节内容"
|
||||
|
||||
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
|
||||
"""执行内容生成
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
更新后的状态
|
||||
"""
|
||||
chapter = state.get("current_chapter", {})
|
||||
chapter_id = chapter.get("id")
|
||||
chapter_title = chapter.get("title")
|
||||
|
||||
if not chapter_id:
|
||||
raise ValueError("当前章节信息缺失")
|
||||
|
||||
logger.info(f"开始生成章节内容: {chapter_id} - {chapter_title}")
|
||||
|
||||
# 获取章节配置(继承父章节)
|
||||
config = self._get_chapter_config(state, chapter)
|
||||
|
||||
# 查找当前章节的所有3级子标题
|
||||
sub_chapters = self._find_sub_chapters(state, chapter_id, max_level=3)
|
||||
|
||||
if sub_chapters:
|
||||
# 有子标题:逐个生成后拼接
|
||||
logger.info(f"章节 {chapter_id} 包含 {len(sub_chapters)} 个子标题,逐个生成")
|
||||
content_parts = []
|
||||
for sub in sub_chapters:
|
||||
sub_content = self._generate_with_rag(sub, config, state)
|
||||
content_parts.append(f"## {sub['title']}\n\n{sub_content}")
|
||||
content = "\n\n".join(content_parts)
|
||||
else:
|
||||
# 无子标题:直接生成
|
||||
content = self._generate_with_rag(chapter, config, state)
|
||||
|
||||
# 保存生成结果
|
||||
state.setdefault("generated_contents", {})[chapter_id] = content
|
||||
|
||||
logger.info(f"章节内容生成完成: {chapter_id}, 长度: {len(content)}字")
|
||||
|
||||
return state
|
||||
|
||||
def _get_chapter_config(
|
||||
self, state: Dict[str, Any], chapter: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""获取章节配置,子章节继承父章节配置
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
chapter: 当前章节
|
||||
|
||||
Returns:
|
||||
章节配置
|
||||
"""
|
||||
chapter_id = chapter["id"]
|
||||
chapter_configs = state.get("chapter_configs", {})
|
||||
|
||||
# 如果当前章节有配置,直接返回
|
||||
if chapter_id in chapter_configs:
|
||||
return chapter_configs[chapter_id]
|
||||
|
||||
# 查找父章节配置
|
||||
parent_id = chapter.get("parent_id")
|
||||
while parent_id:
|
||||
if parent_id in chapter_configs:
|
||||
logger.info(f"章节 {chapter_id} 继承父章节 {parent_id} 的配置")
|
||||
return chapter_configs[parent_id]
|
||||
|
||||
# 继续向上查找
|
||||
parent_chapter = self._find_chapter(state, parent_id)
|
||||
parent_id = parent_chapter.get("parent_id") if parent_chapter else None
|
||||
|
||||
# 返回默认配置
|
||||
logger.info(f"章节 {chapter_id} 使用默认配置")
|
||||
return {"emphasis": "", "rag_enabled": False, "rag_store": None}
|
||||
|
||||
def _find_chapter(self, state: Dict[str, Any], chapter_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""在队列中查找章节
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
chapter_id: 章节ID
|
||||
|
||||
Returns:
|
||||
章节信息或None
|
||||
"""
|
||||
queue = state.get("chapter_queue", [])
|
||||
for chapter in queue:
|
||||
if chapter["id"] == chapter_id:
|
||||
return chapter
|
||||
return None
|
||||
|
||||
def _find_sub_chapters(
|
||||
self, state: Dict[str, Any], parent_id: str, max_level: int = 3
|
||||
) -> list:
|
||||
"""查找指定章节的所有子标题(最深到3级)
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
parent_id: 父章节ID
|
||||
max_level: 最大层级(默认3级)
|
||||
|
||||
Returns:
|
||||
子章节列表
|
||||
"""
|
||||
queue = state.get("chapter_queue", [])
|
||||
parent = self._find_chapter(state, parent_id)
|
||||
if not parent:
|
||||
return []
|
||||
|
||||
parent_level = parent.get("level", 1)
|
||||
target_level = parent_level + 1
|
||||
|
||||
# 只查找直接子章节(比父章节深1层)且不超过max_level
|
||||
sub_chapters = []
|
||||
for chapter in queue:
|
||||
if (
|
||||
chapter.get("parent_id") == parent_id
|
||||
and chapter.get("level") == target_level
|
||||
and chapter.get("level") <= max_level
|
||||
):
|
||||
sub_chapters.append(chapter)
|
||||
|
||||
return sub_chapters
|
||||
|
||||
def _generate_with_rag(
|
||||
self, chapter: Dict[str, Any], config: Dict[str, Any], state: Dict[str, Any]
|
||||
) -> str:
|
||||
"""使用RAG生成内容
|
||||
|
||||
Args:
|
||||
chapter: 章节信息
|
||||
config: 章节配置
|
||||
state: 当前状态
|
||||
|
||||
Returns:
|
||||
生成的内容
|
||||
"""
|
||||
from ...tools.rag import RAGTool
|
||||
|
||||
rag_tool = RAGTool()
|
||||
|
||||
# 构建生成上下文
|
||||
generation_context = {
|
||||
"title": chapter["title"],
|
||||
"level": chapter["level"],
|
||||
"requirements": chapter.get("requirements", ""),
|
||||
"emphasis": config.get("emphasis", ""),
|
||||
}
|
||||
|
||||
# 如果启用RAG,添加上下文信息
|
||||
if config.get("rag_enabled"):
|
||||
# 检索相关内容
|
||||
query = f"{chapter['title']} {config.get('emphasis', '')}"
|
||||
search_results = rag_tool.search(query, k=3)
|
||||
|
||||
if search_results:
|
||||
relevant_context = "\n\n".join([r["content"] for r in search_results])
|
||||
generation_context["rag_context"] = relevant_context
|
||||
logger.info(f"RAG检索到 {len(search_results)} 条相关内容")
|
||||
else:
|
||||
logger.warning("RAG未检索到相关内容")
|
||||
generation_context["rag_context"] = ""
|
||||
else:
|
||||
generation_context["rag_context"] = ""
|
||||
|
||||
# 添加父章节上下文
|
||||
parent_context = state.get("last_generated_content", "")
|
||||
if parent_context and chapter["level"] > 1:
|
||||
generation_context["parent_context"] = parent_context[:500] # 限制长度
|
||||
|
||||
# 调用生成方法
|
||||
try:
|
||||
content = rag_tool.generate_content(chapter["id"], generation_context)
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.error(f"内容生成失败: {e}")
|
||||
# 立即失败,不提供后备方案
|
||||
raise
|
||||
206
src/bidmaster/nodes/content/init_config.py
Normal file
206
src/bidmaster/nodes/content/init_config.py
Normal file
@ -0,0 +1,206 @@
|
||||
"""初始化配置节点
|
||||
|
||||
读取Word文档结构并生成深度优先的章节队列。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitConfigNode(BaseNode):
|
||||
"""初始化配置节点
|
||||
|
||||
职责:
|
||||
1. 读取Word文档章节结构(从tasks.json或analysis_result.json)
|
||||
2. 深度优先遍历生成章节队列
|
||||
3. 初始化全局配置
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "init_config"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "初始化配置并生成章节队列"
|
||||
|
||||
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
|
||||
"""执行初始化配置
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
更新后的状态
|
||||
"""
|
||||
word_file = state.get("word_file")
|
||||
|
||||
if not word_file:
|
||||
raise ValueError("缺少word_file参数")
|
||||
|
||||
word_path = Path(word_file)
|
||||
if not word_path.exists():
|
||||
raise FileNotFoundError(f"Word文档不存在: {word_file}")
|
||||
|
||||
# 直接从Word文档读取章节结构
|
||||
chapters = self._load_from_word_document(word_file)
|
||||
|
||||
# 深度优先遍历生成队列
|
||||
chapter_queue = self._build_depth_first_queue(chapters)
|
||||
|
||||
logger.info(f"生成章节队列,共{len(chapter_queue)}个章节")
|
||||
|
||||
return self._update_state(
|
||||
state,
|
||||
chapter_queue=chapter_queue,
|
||||
chapter_configs={},
|
||||
generated_contents={},
|
||||
completed_chapters=[],
|
||||
current_chapter_index=0,
|
||||
)
|
||||
|
||||
def _load_from_word_document(self, word_file: str) -> List[Dict[str, Any]]:
|
||||
"""从Word文档读取章节结构
|
||||
|
||||
Args:
|
||||
word_file: Word文档路径
|
||||
|
||||
Returns:
|
||||
章节列表
|
||||
"""
|
||||
from docx import Document
|
||||
import re
|
||||
|
||||
doc = Document(word_file)
|
||||
chapters = []
|
||||
parent_stack = [] # 栈:存储各层级的父章节ID
|
||||
|
||||
for para in doc.paragraphs:
|
||||
style_name = para.style.name
|
||||
|
||||
# 只处理Heading 1/2/3
|
||||
if not style_name.startswith('Heading'):
|
||||
continue
|
||||
|
||||
# 提取层级
|
||||
level_match = re.match(r'Heading\s+(\d+)', style_name)
|
||||
if not level_match:
|
||||
continue
|
||||
|
||||
level = int(level_match.group(1))
|
||||
if level > 3: # 只处理到3级标题
|
||||
continue
|
||||
|
||||
# 提取标题文本和编号
|
||||
title_text = para.text.strip()
|
||||
if not title_text:
|
||||
continue
|
||||
|
||||
# 从标题中提取编号(如"2.1.3 标题" → "2_1_3")
|
||||
number_match = re.match(r'^([\d\.]+)\s+(.+)$', title_text)
|
||||
if number_match:
|
||||
number_str = number_match.group(1).rstrip('.')
|
||||
title = number_match.group(2)
|
||||
chapter_id = f"chapter_{number_str.replace('.', '_')}"
|
||||
else:
|
||||
# 无编号,使用索引
|
||||
chapter_id = f"chapter_{len(chapters) + 1}"
|
||||
title = title_text
|
||||
|
||||
# 确定父章节ID
|
||||
while parent_stack and parent_stack[-1]['level'] >= level:
|
||||
parent_stack.pop()
|
||||
|
||||
parent_id = parent_stack[-1]['id'] if parent_stack else None
|
||||
|
||||
# 创建章节信息
|
||||
chapter_info = {
|
||||
"id": chapter_id,
|
||||
"title": title,
|
||||
"level": level,
|
||||
"placeholder": f"{{{{{chapter_id}_content}}}}",
|
||||
"requirements": "",
|
||||
"score": 0,
|
||||
"parent_id": parent_id,
|
||||
}
|
||||
|
||||
chapters.append(chapter_info)
|
||||
|
||||
# 将当前章节加入栈
|
||||
parent_stack.append({'id': chapter_id, 'level': level})
|
||||
|
||||
logger.info(f"从Word文档读取到 {len(chapters)} 个章节")
|
||||
|
||||
return chapters
|
||||
|
||||
def _build_depth_first_queue(self, chapters: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""深度优先遍历生成章节队列
|
||||
|
||||
Args:
|
||||
chapters: 章节列表
|
||||
|
||||
Returns:
|
||||
深度优先排序的章节队列
|
||||
"""
|
||||
# 构建章节树
|
||||
chapter_map = {ch["id"]: ch for ch in chapters}
|
||||
|
||||
# 找出根节点(1级标题)
|
||||
root_chapters = [ch for ch in chapters if ch["level"] == 1]
|
||||
|
||||
# 深度优先遍历
|
||||
queue = []
|
||||
for root in root_chapters:
|
||||
self._dfs_traverse(root, chapter_map, queue, chapters)
|
||||
|
||||
return queue
|
||||
|
||||
def _extract_chapter_numbers(self, chapter_id: str) -> List[int]:
|
||||
"""从章节ID提取数字编号用于排序
|
||||
|
||||
Args:
|
||||
chapter_id: 章节ID,如 "chapter_1_2_3"
|
||||
|
||||
Returns:
|
||||
数字列表,如 [1, 2, 3]
|
||||
"""
|
||||
import re
|
||||
# 提取所有数字
|
||||
numbers = re.findall(r'\d+', chapter_id)
|
||||
return [int(n) for n in numbers]
|
||||
|
||||
def _dfs_traverse(
|
||||
self,
|
||||
chapter: Dict[str, Any],
|
||||
chapter_map: Dict[str, Dict],
|
||||
queue: List[Dict],
|
||||
all_chapters: List[Dict],
|
||||
) -> None:
|
||||
"""深度优先遍历
|
||||
|
||||
Args:
|
||||
chapter: 当前章节
|
||||
chapter_map: 章节映射
|
||||
queue: 结果队列
|
||||
all_chapters: 所有章节列表
|
||||
"""
|
||||
queue.append(chapter)
|
||||
|
||||
# 查找子章节
|
||||
chapter_id = chapter["id"]
|
||||
children = [ch for ch in all_chapters if ch.get("parent_id") == chapter_id]
|
||||
|
||||
# 按数字编号排序确保正确顺序(修复:避免字符串排序导致 1_10 < 1_2)
|
||||
children.sort(key=lambda x: self._extract_chapter_numbers(x["id"]))
|
||||
|
||||
# 递归遍历子章节
|
||||
for child in children:
|
||||
self._dfs_traverse(child, chapter_map, queue, all_chapters)
|
||||
|
||||
147
src/bidmaster/nodes/content/interact_user.py
Normal file
147
src/bidmaster/nodes/content/interact_user.py
Normal file
@ -0,0 +1,147 @@
|
||||
"""用户交互节点
|
||||
|
||||
与用户交互获取章节填写要求和RAG配置。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InteractWithUserNode(BaseNode):
|
||||
"""用户交互节点
|
||||
|
||||
职责:
|
||||
1. 询问用户该章节需要强调的内容
|
||||
2. 询问是否使用RAG知识库
|
||||
3. 如果使用RAG,选择具体的知识库
|
||||
4. 保存配置供后续子章节继承
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "interact_user"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "与用户交互获取章节配置"
|
||||
|
||||
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
|
||||
"""执行用户交互
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
更新后的状态
|
||||
"""
|
||||
chapter = state.get("current_chapter", {})
|
||||
chapter_id = chapter.get("id")
|
||||
chapter_title = chapter.get("title")
|
||||
|
||||
if not chapter_id:
|
||||
raise ValueError("当前章节信息缺失")
|
||||
|
||||
# 检查是否需要交互
|
||||
needs_interaction = state.get("needs_interaction", False)
|
||||
if not needs_interaction:
|
||||
# 不需要交互,直接返回
|
||||
return state
|
||||
|
||||
# 获取交互处理器
|
||||
interaction_handler = state.get("interaction_handler")
|
||||
|
||||
if not interaction_handler:
|
||||
# 无交互处理器时使用默认值(静默模式)
|
||||
logger.info("无交互处理器,使用默认配置")
|
||||
config = {"emphasis": "", "rag_enabled": False, "rag_store": None}
|
||||
state.setdefault("chapter_configs", {})[chapter_id] = config
|
||||
return state
|
||||
|
||||
logger.info(f"开始与用户交互,获取章节配置: {chapter_id} - {chapter_title}")
|
||||
|
||||
# 1. 询问需要强调的内容
|
||||
emphasis = interaction_handler(
|
||||
interaction_type="text",
|
||||
prompt=f"📝 正在处理章节:{chapter_id}. {chapter_title}\n"
|
||||
f" 是否有需要特别强调的内容?(直接回车跳过)",
|
||||
default="",
|
||||
key=f"emphasis_{chapter_id}",
|
||||
)
|
||||
|
||||
# 2. 询问是否使用RAG
|
||||
use_rag_response = interaction_handler(
|
||||
interaction_type="choice",
|
||||
prompt="是否使用RAG知识库辅助生成内容",
|
||||
options=["是", "否"],
|
||||
default="是",
|
||||
key=f"use_rag_{chapter_id}",
|
||||
)
|
||||
|
||||
use_rag = use_rag_response == "是"
|
||||
rag_store = None
|
||||
|
||||
# 3. 如果使用RAG,选择知识库
|
||||
if use_rag:
|
||||
rag_store = self._select_rag_store(interaction_handler, chapter_id)
|
||||
|
||||
# 保存章节配置
|
||||
config = {
|
||||
"emphasis": emphasis.strip() if emphasis else "",
|
||||
"rag_enabled": use_rag,
|
||||
"rag_store": rag_store,
|
||||
}
|
||||
|
||||
state.setdefault("chapter_configs", {})[chapter_id] = config
|
||||
|
||||
logger.info(
|
||||
f"章节配置已保存: 强调内容={'有' if config['emphasis'] else '无'}, "
|
||||
f"RAG={'启用' if use_rag else '禁用'}"
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
def _select_rag_store(self, interaction_handler, chapter_id: str) -> str:
|
||||
"""选择RAG知识库
|
||||
|
||||
Args:
|
||||
interaction_handler: 交互处理器
|
||||
chapter_id: 章节ID
|
||||
|
||||
Returns:
|
||||
知识库标识
|
||||
"""
|
||||
# 获取可用的知识库列表
|
||||
from ...tools.rag import RAGTool
|
||||
|
||||
try:
|
||||
rag_tool = RAGTool()
|
||||
stats = rag_tool.get_stats()
|
||||
total_chunks = stats.get("total_chunks", 0)
|
||||
total_files = stats.get("total_files", 0)
|
||||
|
||||
# 显示知识库统计信息
|
||||
store_info = f"默认知识库 ({total_files}个文档, {total_chunks}个文档块)"
|
||||
|
||||
choice = interaction_handler(
|
||||
interaction_type="choice",
|
||||
prompt=f"选择知识库",
|
||||
options=[
|
||||
("default", store_info),
|
||||
("none", "不使用RAG")
|
||||
],
|
||||
default="default",
|
||||
key=f"rag_store_{chapter_id}",
|
||||
)
|
||||
|
||||
if choice == "default":
|
||||
return "default"
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"获取知识库信息失败: {e},使用默认配置")
|
||||
return "default"
|
||||
60
src/bidmaster/nodes/content/prepare_chapter.py
Normal file
60
src/bidmaster/nodes/content/prepare_chapter.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""准备章节节点
|
||||
|
||||
从队列中取出当前待处理章节并设置上下文。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PrepareChapterNode(BaseNode):
|
||||
"""准备章节节点
|
||||
|
||||
职责:
|
||||
1. 从队列取当前章节
|
||||
2. 设置当前处理上下文
|
||||
3. 判断是否需要用户交互(仅1级标题需要)
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "prepare_chapter"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "准备当前待处理章节"
|
||||
|
||||
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
|
||||
"""执行准备章节逻辑
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
更新后的状态
|
||||
"""
|
||||
index = state.get("current_chapter_index", 0)
|
||||
queue = state.get("chapter_queue", [])
|
||||
|
||||
if index >= len(queue):
|
||||
logger.info("所有章节已处理完成")
|
||||
return self._update_state(state, should_continue=False)
|
||||
|
||||
current_chapter = queue[index]
|
||||
|
||||
logger.info(
|
||||
f"准备处理章节 [{index + 1}/{len(queue)}]: "
|
||||
f"{current_chapter['id']} - {current_chapter['title']}"
|
||||
)
|
||||
|
||||
# 判断是否需要用户交互(仅1级标题)
|
||||
needs_interaction = current_chapter["level"] == 1
|
||||
|
||||
return self._update_state(
|
||||
state, current_chapter=current_chapter, needs_interaction=needs_interaction
|
||||
)
|
||||
110
src/bidmaster/nodes/content/save_to_word.py
Normal file
110
src/bidmaster/nodes/content/save_to_word.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""保存到Word节点
|
||||
|
||||
将生成的内容填充到Word文档并保存。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..base import BaseNode, NodeContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaveToWordNode(BaseNode):
|
||||
"""保存到Word节点
|
||||
|
||||
职责:
|
||||
1. 将生成的内容填充到Word文档对应占位符
|
||||
2. 立即保存Word文件
|
||||
3. 更新处理进度
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "save_to_word"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "保存内容到Word文档"
|
||||
|
||||
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
|
||||
"""执行保存操作
|
||||
|
||||
Args:
|
||||
state: 当前状态
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
更新后的状态
|
||||
"""
|
||||
chapter = state.get("current_chapter", {})
|
||||
chapter_id = chapter.get("id")
|
||||
word_file = state.get("word_file")
|
||||
|
||||
if not chapter_id or not word_file:
|
||||
raise ValueError("缺少必要参数: chapter_id或word_file")
|
||||
|
||||
# 获取生成的内容
|
||||
generated_contents = state.get("generated_contents", {})
|
||||
content = generated_contents.get(chapter_id)
|
||||
|
||||
if not content:
|
||||
raise ValueError(f"章节 {chapter_id} 的内容未生成")
|
||||
|
||||
logger.info(f"开始保存章节内容到Word: {chapter_id} - {chapter['title']}")
|
||||
|
||||
# 填充到Word文档
|
||||
self._fill_word_document(word_file, chapter, content)
|
||||
|
||||
# 更新状态(直接修改state确保LangGraph正确传递)
|
||||
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上更新,确保下次循环能读取到最新值
|
||||
state["completed_chapters"] = completed_chapters
|
||||
state["current_chapter_index"] = current_index + 1
|
||||
state["last_generated_content"] = content
|
||||
state["current_step"] = self.name
|
||||
|
||||
return state
|
||||
|
||||
def _fill_word_document(
|
||||
self, word_file: str, chapter: Dict[str, Any], content: str
|
||||
) -> None:
|
||||
"""填充Word文档
|
||||
|
||||
Args:
|
||||
word_file: Word文档路径
|
||||
chapter: 章节信息
|
||||
content: 生成的内容
|
||||
"""
|
||||
from ...tools.word import WordProcessor
|
||||
|
||||
word_path = Path(word_file)
|
||||
if not word_path.exists():
|
||||
raise FileNotFoundError(f"Word文档不存在: {word_file}")
|
||||
|
||||
word_processor = WordProcessor()
|
||||
|
||||
# 获取占位符
|
||||
placeholder = chapter.get("placeholder")
|
||||
if not placeholder:
|
||||
# 如果没有占位符,使用章节ID构建默认占位符
|
||||
placeholder = f"{{{{{chapter['id']}_content}}}}"
|
||||
|
||||
try:
|
||||
# 填充占位符
|
||||
word_processor.fill_placeholder(str(word_path), placeholder, content)
|
||||
logger.info(f"成功填充占位符: {placeholder}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"填充Word文档失败: {e}")
|
||||
# 立即失败
|
||||
raise
|
||||
@ -192,34 +192,48 @@ class RAGTool:
|
||||
Returns:
|
||||
生成的内容
|
||||
"""
|
||||
from openai import OpenAI
|
||||
|
||||
# 从上下文中提取任务信息
|
||||
task_title = context.get('title', '任务')
|
||||
task_requirements = context.get('requirements', '')
|
||||
emphasis = context.get('emphasis', '')
|
||||
rag_context = context.get('rag_context', '')
|
||||
|
||||
# 搜索相关内容
|
||||
search_results = self.search(task_requirements or task_title, k=3)
|
||||
# 构建提示词
|
||||
emphasis_part = f'\n特别强调:{emphasis}' if emphasis else ''
|
||||
rag_part = f'\n\n参考资料:\n{rag_context}' if rag_context else ''
|
||||
|
||||
# 构建提示
|
||||
relevant_content = "\n".join([r['content'] for r in search_results])
|
||||
prompt = f"""你是一个专业的标书撰写助手。请根据以下信息生成标书章节内容:
|
||||
|
||||
# 直接生成内容
|
||||
generated_content = f"""# {task_title}
|
||||
章节标题:{task_title}
|
||||
评分要求:{task_requirements}{emphasis_part}{rag_part}
|
||||
|
||||
## 概述
|
||||
根据招标要求和知识库内容,本章节详细阐述实施方案。
|
||||
要求:
|
||||
1. 内容专业、详实,符合招标文件要求
|
||||
2. 突出技术优势和实施能力
|
||||
3. 语言正式、逻辑清晰
|
||||
4. 字数控制在500-800字
|
||||
|
||||
## 相关参考
|
||||
{relevant_content[:500] if relevant_content else '暂无相关参考内容'}
|
||||
请直接输出章节内容,不要包含章节标题。"""
|
||||
|
||||
## 实施方案
|
||||
1. 技术实现
|
||||
2. 质量保证
|
||||
3. 进度安排
|
||||
4. 资源配置
|
||||
# 调用DeepSeek生成
|
||||
client = OpenAI(
|
||||
api_key=self.settings.api_key,
|
||||
base_url=self.settings.base_url
|
||||
)
|
||||
|
||||
## 总结
|
||||
本方案充分考虑了各项要求,确保项目顺利实施。
|
||||
"""
|
||||
response = client.chat.completions.create(
|
||||
model=self.settings.model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是一个专业的标书撰写助手。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
generated_content = response.choices[0].message.content.strip()
|
||||
|
||||
return generated_content
|
||||
|
||||
|
||||
@ -128,6 +128,288 @@ class WordProcessor:
|
||||
pattern = r'^(\d+(?:\.\d+)*\.?\s+)'
|
||||
return re.sub(pattern, '', title.strip())
|
||||
|
||||
def fill_placeholder(self, doc_path: str, placeholder: str, content: str) -> bool:
|
||||
"""填充Word文档中的占位符
|
||||
|
||||
Args:
|
||||
doc_path: Word文档路径
|
||||
placeholder: 占位符文本(如 {{chapter_01_content}})
|
||||
content: 要填充的内容
|
||||
|
||||
Returns:
|
||||
是否成功填充
|
||||
"""
|
||||
try:
|
||||
doc = Document(doc_path)
|
||||
replaced = False
|
||||
|
||||
# 策略1: 查找并替换占位符
|
||||
for paragraph in doc.paragraphs:
|
||||
if placeholder in paragraph.text:
|
||||
# 保留段落格式的替换方法
|
||||
# 保存段落级别的格式属性
|
||||
para_format = paragraph.paragraph_format
|
||||
|
||||
# 清空段落内容但保留段落本身
|
||||
for run in paragraph.runs:
|
||||
run.text = ''
|
||||
|
||||
# 添加新内容(保留第一个run的格式,如果有的话)
|
||||
if paragraph.runs:
|
||||
paragraph.runs[0].text = content
|
||||
else:
|
||||
paragraph.add_run(content)
|
||||
|
||||
replaced = True
|
||||
logger.info(f"已替换占位符: {placeholder}")
|
||||
break
|
||||
|
||||
# 策略2: 降级处理 - 如果没有占位符,在对应标题后插入内容
|
||||
if not replaced:
|
||||
logger.warning(f"未找到占位符: {placeholder},尝试在对应标题后插入内容")
|
||||
replaced = self._insert_after_heading(doc, placeholder, content)
|
||||
|
||||
# 保存文档
|
||||
doc.save(doc_path)
|
||||
return replaced
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"填充占位符失败: {e}")
|
||||
raise
|
||||
|
||||
def _parse_markdown_to_paragraphs(self, content: str, chapter_level: int) -> list:
|
||||
"""解析Markdown内容为段落结构列表
|
||||
|
||||
Args:
|
||||
content: Markdown格式的内容
|
||||
chapter_level: 当前章节层级(1/2/3)
|
||||
|
||||
Returns:
|
||||
段落结构列表,每项包含 {'type': 'heading'|'paragraph'|'list', 'text': str, 'style': str}
|
||||
"""
|
||||
import re
|
||||
|
||||
paragraphs = []
|
||||
lines = content.split('\n')
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
|
||||
if not line:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Markdown标题:## 标题
|
||||
if re.match(r'^#{2,4}\s+', line):
|
||||
level = len(re.match(r'^(#+)', line).group(1))
|
||||
title_text = re.sub(r'^#{2,4}\s+', '', line)
|
||||
|
||||
# 3级标题内容:## → 加粗段落(非标题样式)
|
||||
if chapter_level == 3:
|
||||
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
|
||||
})
|
||||
|
||||
# 有序列表:1. / 1) / (1)
|
||||
elif re.match(r'^(\d+[\.))]|(\d+))\s+', line):
|
||||
text = re.sub(r'^(\d+[\.))]|(\d+))\s+', '', line)
|
||||
paragraphs.append({
|
||||
'type': 'ordered_list',
|
||||
'text': text
|
||||
})
|
||||
|
||||
# 无序列表:- / * / •
|
||||
elif re.match(r'^[-*•]\s+', line):
|
||||
text = re.sub(r'^[-*•]\s+', '', line)
|
||||
paragraphs.append({
|
||||
'type': 'unordered_list',
|
||||
'text': text
|
||||
})
|
||||
|
||||
# 普通段落
|
||||
else:
|
||||
# 处理行内格式:**粗体**
|
||||
text = re.sub(r'\*\*(.+?)\*\*', r'\1', line)
|
||||
# 处理行内格式:*斜体*(单星号,但避免误匹配列表)
|
||||
if not line.startswith('*'):
|
||||
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
||||
|
||||
paragraphs.append({
|
||||
'type': 'paragraph',
|
||||
'text': text
|
||||
})
|
||||
|
||||
i += 1
|
||||
|
||||
return paragraphs
|
||||
|
||||
def _insert_after_heading(self, doc: Document, placeholder: str, content: str) -> bool:
|
||||
"""在对应标题后插入内容(降级策略)
|
||||
|
||||
Args:
|
||||
doc: Word文档对象
|
||||
placeholder: 占位符(如 {{chapter_2_1_1_content}})
|
||||
content: 要插入的内容
|
||||
|
||||
Returns:
|
||||
是否成功插入
|
||||
"""
|
||||
import re
|
||||
|
||||
# 从占位符提取章节编号: {{chapter_2_1_1_content}} -> "2.1.1"
|
||||
match = re.search(r'chapter_(\d+(?:_\d+)*)', placeholder)
|
||||
if not match:
|
||||
logger.error(f"无法从占位符解析章节编号: {placeholder}")
|
||||
return False
|
||||
|
||||
chapter_number = match.group(1).replace('_', '.')
|
||||
|
||||
# 查找对应编号的标题
|
||||
target_paragraph = None
|
||||
target_index = -1
|
||||
|
||||
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}."):
|
||||
target_paragraph = para
|
||||
target_index = i
|
||||
logger.info(f"找到匹配标题: {text}")
|
||||
break
|
||||
|
||||
if target_paragraph is None:
|
||||
logger.error(f"未找到编号为 {chapter_number} 的标题")
|
||||
return False
|
||||
|
||||
# 获取章节层级(从标题样式推断)
|
||||
chapter_level = 3 # 默认3级
|
||||
if target_paragraph.style.name == 'Heading 1':
|
||||
chapter_level = 1
|
||||
elif target_paragraph.style.name == 'Heading 2':
|
||||
chapter_level = 2
|
||||
elif target_paragraph.style.name == 'Heading 3':
|
||||
chapter_level = 3
|
||||
|
||||
# 解析Markdown内容为段落结构
|
||||
parsed_paragraphs = self._parse_markdown_to_paragraphs(content, chapter_level)
|
||||
|
||||
# 在标题后逐个插入段落
|
||||
from docx.text.paragraph import Paragraph
|
||||
from docx.shared import Pt, Cm
|
||||
from docx.enum.text import WD_LINE_SPACING, WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
current_element = target_paragraph._element
|
||||
inserted_count = 0
|
||||
|
||||
for para_struct in parsed_paragraphs:
|
||||
para_type = para_struct['type']
|
||||
text = para_struct['text']
|
||||
|
||||
# 创建新段落XML元素
|
||||
new_p_element = OxmlElement('w:p')
|
||||
current_element.addnext(new_p_element)
|
||||
current_element = new_p_element
|
||||
|
||||
# 创建Paragraph对象
|
||||
new_para = Paragraph(new_p_element, doc)
|
||||
para_format = new_para.paragraph_format
|
||||
|
||||
# 根据类型应用不同样式
|
||||
if para_type == 'heading':
|
||||
# 子标题:使用Heading样式
|
||||
level = para_struct.get('level', 4)
|
||||
try:
|
||||
new_para.style = f'Heading {level}'
|
||||
except:
|
||||
run = new_para.add_run(text)
|
||||
run.bold = True
|
||||
run.font.size = Pt(13)
|
||||
else:
|
||||
new_para.text = text
|
||||
|
||||
elif para_type == 'bold_paragraph':
|
||||
# 加粗段落(3级标题内的小标题)
|
||||
try:
|
||||
new_para.style = 'Normal'
|
||||
except:
|
||||
pass
|
||||
para_format.space_before = Pt(6)
|
||||
para_format.space_after = Pt(3)
|
||||
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
run = new_para.add_run(text)
|
||||
run.bold = True
|
||||
run.font.name = '黑体'
|
||||
run.font.size = Pt(12)
|
||||
|
||||
elif para_type == 'ordered_list':
|
||||
# 有序列表:1)2)3)
|
||||
try:
|
||||
new_para.style = 'Normal'
|
||||
except:
|
||||
pass
|
||||
para_format.left_indent = Cm(0.74)
|
||||
para_format.space_after = Pt(3)
|
||||
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
# 手动添加编号(简化实现)
|
||||
list_index = sum(1 for p in parsed_paragraphs[:parsed_paragraphs.index(para_struct)] if p['type'] == 'ordered_list') + 1
|
||||
run = new_para.add_run(f"{list_index}){text}")
|
||||
run.font.name = '宋体'
|
||||
run.font.size = Pt(12)
|
||||
|
||||
elif para_type == 'unordered_list':
|
||||
# 无序列表:• 或 ○
|
||||
try:
|
||||
new_para.style = 'Normal'
|
||||
except:
|
||||
pass
|
||||
para_format.left_indent = Cm(0.74)
|
||||
para_format.space_after = Pt(3)
|
||||
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
run = new_para.add_run(f"• {text}")
|
||||
run.font.name = '宋体'
|
||||
run.font.size = Pt(12)
|
||||
|
||||
else: # paragraph
|
||||
# 普通段落
|
||||
try:
|
||||
new_para.style = 'Normal'
|
||||
except:
|
||||
pass
|
||||
para_format.first_line_indent = Cm(0.74)
|
||||
para_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
para_format.space_after = Pt(6)
|
||||
|
||||
run = new_para.add_run(text)
|
||||
run.font.name = '宋体'
|
||||
run.font.size = Pt(12)
|
||||
|
||||
inserted_count += 1
|
||||
|
||||
logger.info(f"成功在标题 {chapter_number} 后插入内容(共{inserted_count}个段落)")
|
||||
return True
|
||||
|
||||
def _setup_numbering_styles(self, doc: Document):
|
||||
"""配置标题的多级编号样式"""
|
||||
# 确保标题样式存在并配置编号格式
|
||||
|
||||
Loading…
Reference in New Issue
Block a user