diff --git a/111 - 副本/analysis_result.json b/111 - 副本/analysis_result.json new file mode 100644 index 0000000..732cb45 --- /dev/null +++ b/111 - 副本/analysis_result.json @@ -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": [] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/111 - 副本/tasks.json b/111 - 副本/tasks.json new file mode 100644 index 0000000..cfd617c --- /dev/null +++ b/111 - 副本/tasks.json @@ -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}}" + } +] \ No newline at end of file diff --git a/src/bidmaster/agents/__init__.py b/src/bidmaster/agents/__init__.py index 593c1ba..fec7dee 100644 --- a/src/bidmaster/agents/__init__.py +++ b/src/bidmaster/agents/__init__.py @@ -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", ] \ No newline at end of file diff --git a/src/bidmaster/agents/builders/__init__.py b/src/bidmaster/agents/builders/__init__.py index 26eebbf..42a3a91 100644 --- a/src/bidmaster/agents/builders/__init__.py +++ b/src/bidmaster/agents/builders/__init__.py @@ -3,8 +3,7 @@ 包含各种Agent的构建器实现。 """ +from .content_builder import ContentWriterAgentBuilder from .toc_builder import TocAgentBuilder -__all__ = [ - "TocAgentBuilder" -] \ No newline at end of file +__all__ = ["TocAgentBuilder", "ContentWriterAgentBuilder"] \ No newline at end of file diff --git a/src/bidmaster/agents/builders/content_builder.py b/src/bidmaster/agents/builders/content_builder.py new file mode 100644 index 0000000..6d00a64 --- /dev/null +++ b/src/bidmaster/agents/builders/content_builder.py @@ -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工作流配置完成") \ No newline at end of file diff --git a/src/bidmaster/agents/content_writer.py b/src/bidmaster/agents/content_writer.py new file mode 100644 index 0000000..0542e7f --- /dev/null +++ b/src/bidmaster/agents/content_writer.py @@ -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) \ No newline at end of file diff --git a/src/bidmaster/agents/interaction.py b/src/bidmaster/agents/interaction.py index c057c8e..31aeb03 100644 --- a/src/bidmaster/agents/interaction.py +++ b/src/bidmaster/agents/interaction.py @@ -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: diff --git a/src/bidmaster/agents/single_chapter_agent.py b/src/bidmaster/agents/single_chapter_agent.py new file mode 100644 index 0000000..50d97db --- /dev/null +++ b/src/bidmaster/agents/single_chapter_agent.py @@ -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 \ No newline at end of file diff --git a/src/bidmaster/cli/main.py b/src/bidmaster/cli/main.py index a8933e1..db2c12f 100644 --- a/src/bidmaster/cli/main.py +++ b/src/bidmaster/cli/main.py @@ -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 - 显示系统信息") diff --git a/src/bidmaster/cli/write.py b/src/bidmaster/cli/write.py new file mode 100644 index 0000000..5627cd4 --- /dev/null +++ b/src/bidmaster/cli/write.py @@ -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() \ No newline at end of file diff --git a/src/bidmaster/nodes/content/__init__.py b/src/bidmaster/nodes/content/__init__.py new file mode 100644 index 0000000..dd2287c --- /dev/null +++ b/src/bidmaster/nodes/content/__init__.py @@ -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", +] \ No newline at end of file diff --git a/src/bidmaster/nodes/content/collect_results.py b/src/bidmaster/nodes/content/collect_results.py new file mode 100644 index 0000000..6eb3a7c --- /dev/null +++ b/src/bidmaster/nodes/content/collect_results.py @@ -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) \ No newline at end of file diff --git a/src/bidmaster/nodes/content/generate_content.py b/src/bidmaster/nodes/content/generate_content.py new file mode 100644 index 0000000..0e6666e --- /dev/null +++ b/src/bidmaster/nodes/content/generate_content.py @@ -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 \ No newline at end of file diff --git a/src/bidmaster/nodes/content/init_config.py b/src/bidmaster/nodes/content/init_config.py new file mode 100644 index 0000000..0ca6070 --- /dev/null +++ b/src/bidmaster/nodes/content/init_config.py @@ -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) + diff --git a/src/bidmaster/nodes/content/interact_user.py b/src/bidmaster/nodes/content/interact_user.py new file mode 100644 index 0000000..ebad82a --- /dev/null +++ b/src/bidmaster/nodes/content/interact_user.py @@ -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" \ No newline at end of file diff --git a/src/bidmaster/nodes/content/prepare_chapter.py b/src/bidmaster/nodes/content/prepare_chapter.py new file mode 100644 index 0000000..73ada00 --- /dev/null +++ b/src/bidmaster/nodes/content/prepare_chapter.py @@ -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 + ) \ No newline at end of file diff --git a/src/bidmaster/nodes/content/save_to_word.py b/src/bidmaster/nodes/content/save_to_word.py new file mode 100644 index 0000000..d7ec642 --- /dev/null +++ b/src/bidmaster/nodes/content/save_to_word.py @@ -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 \ No newline at end of file diff --git a/src/bidmaster/tools/rag.py b/src/bidmaster/tools/rag.py index 0fca2ef..e4962fc 100644 --- a/src/bidmaster/tools/rag.py +++ b/src/bidmaster/tools/rag.py @@ -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 diff --git a/src/bidmaster/tools/word.py b/src/bidmaster/tools/word.py index c10e55b..b283595 100644 --- a/src/bidmaster/tools/word.py +++ b/src/bidmaster/tools/word.py @@ -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): """配置标题的多级编号样式""" # 确保标题样式存在并配置编号格式