From 52733d68c2d1dc0a4ea983fd33812a972947f264 Mon Sep 17 00:00:00 2001 From: sladro Date: Mon, 29 Sep 2025 15:08:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9C=A8=E6=A0=87=E9=A2=98=E7=94=9F?= =?UTF-8?q?=E6=88=90Agent=E4=B8=AD=E5=A2=9E=E5=8A=A0AI=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增ApplyReviewSuggestionsNode节点 - 展示AI审查建议给用户 - 支持用户选择要应用的建议 - 支持交互/静默/程序化三种模式 2. 新增AdjustChaptersNode节点 - 根据用户选择的AI建议调整章节结构 - 完全基于AI的智能调整,无硬编码逻辑 - 安全的错误处理和回退机制 3. 更新工作流架构 - 新流程: ReviewStructure → ApplyReviewSuggestions → AdjustChapters → FinalizeChapters - 专业的单一职责原则,每个节点功能明确 - 完善的状态传递和错误处理 4. 修复AnalysisAgent集成问题 - 保留TocAgent的完整审查结果 - 正确使用调整后的章节结构 - 修复interaction_handler传递问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/bidmaster/agents/analysis.py | 19 +- src/bidmaster/agents/builders/toc_builder.py | 19 ++ src/bidmaster/nodes/toc/__init__.py | 4 + src/bidmaster/nodes/toc/adjust_chapters.py | 234 +++++++++++++++++++ src/bidmaster/nodes/toc/apply_suggestions.py | 124 ++++++++++ src/bidmaster/nodes/toc/base_mixins.py | 6 +- src/bidmaster/nodes/toc/finalize_chapters.py | 49 +--- 7 files changed, 404 insertions(+), 51 deletions(-) create mode 100644 src/bidmaster/nodes/toc/adjust_chapters.py create mode 100644 src/bidmaster/nodes/toc/apply_suggestions.py diff --git a/src/bidmaster/agents/analysis.py b/src/bidmaster/agents/analysis.py index 33373dd..8f86012 100644 --- a/src/bidmaster/agents/analysis.py +++ b/src/bidmaster/agents/analysis.py @@ -307,10 +307,18 @@ def generate_toc_with_agent_node(state: AnalysisAgentState) -> AnalysisAgentStat logger.info(f"目录生成完成: {len(chapters)}个章节") - # 更新状态 + # 更新状态 - 使用TocAgent的完整结果 state["preliminary_chapters"] = chapters state["technical_criteria"] = technical_criteria - state["structure_review"] = {} # TocAgent已包含审查 + + # 保留TocAgent的审查结果和其他状态 + if result.get("structure_review"): + state["structure_review"] = result["structure_review"] + if result.get("approved_suggestions"): + state["approved_suggestions"] = result["approved_suggestions"] + if result.get("adjusted_chapters"): + state["adjusted_chapters"] = result["adjusted_chapters"] + state["current_step"] = "generate_toc" state["progress"] = 0.85 @@ -335,12 +343,17 @@ def finalize_structure_node(state: AnalysisAgentState) -> AnalysisAgentState: preliminary_chapters = state["preliminary_chapters"] structure_review = state.get("structure_review", {}) + # 获取最终章节(优先使用调整后的章节) + final_chapters = state.get("adjusted_chapters") + if not final_chapters: + final_chapters = state.get("final_chapters", preliminary_chapters) + # 创建最终的标书结构 bid_structure = BidStructure( project_name=f"标书项目-{Path(state['source_file']).stem}", scoring_criteria=technical_criteria, deviation_items=state["deviation_items"], - chapters=preliminary_chapters, # 使用TocAgent生成的章节 + chapters=final_chapters, # 使用最终的章节(可能是调整后的) scoring_file=state["source_file"] ) diff --git a/src/bidmaster/agents/builders/toc_builder.py b/src/bidmaster/agents/builders/toc_builder.py index d10fd38..af0946d 100644 --- a/src/bidmaster/agents/builders/toc_builder.py +++ b/src/bidmaster/agents/builders/toc_builder.py @@ -14,6 +14,8 @@ from ...nodes.toc import ( GenerateFirstLevelNode, GenerateSubChaptersNode, ReviewStructureNode, + ApplyReviewSuggestionsNode, + AdjustChaptersNode, FinalizeChaptersNode ) from ...nodes.toc.base_mixins import WorkflowUtilsMixin @@ -45,6 +47,8 @@ class TocAgentBuilder(AgentBuilder): .add_node(GenerateFirstLevelNode()) \ .add_node(GenerateSubChaptersNode()) \ .add_node(ReviewStructureNode()) \ + .add_node(ApplyReviewSuggestionsNode()) \ + .add_node(AdjustChaptersNode()) \ .add_node(FinalizeChaptersNode()) # 设置入口点 @@ -80,6 +84,18 @@ class TocAgentBuilder(AgentBuilder): self.add_conditional_edge( "review_structure", should_continue, + {"continue": "apply_suggestions", "end": "END"} + ) + + self.add_conditional_edge( + "apply_suggestions", + should_continue, + {"continue": "adjust_chapters", "end": "END"} + ) + + self.add_conditional_edge( + "adjust_chapters", + should_continue, {"continue": "finalize_chapters", "end": "END"} ) @@ -134,6 +150,9 @@ class TocAgent(BaseAgent, BaseAgentFactory): if template_file: initial_state["preset_template_file"] = template_file + # 添加交互处理器到状态中 + initial_state["interaction_handler"] = self.builder.interaction_handler + return await self.execute(initial_state) def generate_toc_sync(self, diff --git a/src/bidmaster/nodes/toc/__init__.py b/src/bidmaster/nodes/toc/__init__.py index 9694505..866bbc3 100644 --- a/src/bidmaster/nodes/toc/__init__.py +++ b/src/bidmaster/nodes/toc/__init__.py @@ -8,6 +8,8 @@ from .group_criteria import GroupCriteriaNode from .generate_first_level import GenerateFirstLevelNode from .generate_sub_chapters import GenerateSubChaptersNode from .review_structure import ReviewStructureNode +from .apply_suggestions import ApplyReviewSuggestionsNode +from .adjust_chapters import AdjustChaptersNode from .finalize_chapters import FinalizeChaptersNode # 辅助组件 @@ -27,6 +29,8 @@ __all__ = [ "GenerateFirstLevelNode", "GenerateSubChaptersNode", "ReviewStructureNode", + "ApplyReviewSuggestionsNode", + "AdjustChaptersNode", "FinalizeChaptersNode", # 辅助组件 diff --git a/src/bidmaster/nodes/toc/adjust_chapters.py b/src/bidmaster/nodes/toc/adjust_chapters.py new file mode 100644 index 0000000..b04b7e7 --- /dev/null +++ b/src/bidmaster/nodes/toc/adjust_chapters.py @@ -0,0 +1,234 @@ +"""AI调整章节结构的节点 + +专门负责根据用户选择的AI建议调整章节结构。 +""" + +import logging +from typing import Dict, List, Any + +from ..base import BaseNode, NodeContext +from ...tools.parser import DocumentChapter +from .base_mixins import TocNodeBase + +logger = logging.getLogger(__name__) + + +class AdjustChaptersNode(BaseNode, TocNodeBase): + """AI调整章节结构的节点""" + + @property + def name(self) -> str: + return "adjust_chapters" + + @property + def description(self) -> str: + return "AI调整章节结构" + + def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]: + """执行AI章节调整""" + return self.safe_execute(self._do_adjust_chapters, state, "AI调整章节结构") + + def _do_adjust_chapters(self, state: Dict[str, Any]) -> Dict[str, Any]: + """执行实际的AI章节调整逻辑""" + # 验证必需字段 + required_fields = ["preliminary_chapters"] + if not self.validate_required_fields(state, required_fields): + raise ValueError("缺少初步章节数据") + + preliminary_chapters = state["preliminary_chapters"] + approved_suggestions = state.get("approved_suggestions", []) + + if not approved_suggestions: + # 没有用户选择的建议,直接返回原章节结构 + logger.info("用户未选择任何AI建议,保持原有章节结构") + return self._update_state(state, adjusted_chapters=preliminary_chapters) + + # 使用AI根据建议调整章节结构 + adjusted_chapters = self._ai_adjust_chapters_with_suggestions( + preliminary_chapters, approved_suggestions) + + self.log_step_info("adjust_chapters", + f"AI根据{len(approved_suggestions)}条建议调整章节,生成{len(adjusted_chapters)}个章节") + + return self._update_state(state, adjusted_chapters=adjusted_chapters) + + def _ai_adjust_chapters_with_suggestions(self, + preliminary_chapters: List[DocumentChapter], + approved_suggestions: List[Dict[str, Any]]) -> List[DocumentChapter]: + """让AI根据用户选择的建议调整章节结构 + + Args: + preliminary_chapters: 原始章节结构 + approved_suggestions: 用户批准的AI建议 + + Returns: + AI调整后的章节结构 + """ + try: + # 构建给AI的提示词 + adjustment_prompt = self._build_adjustment_prompt(preliminary_chapters, approved_suggestions) + + # 调用LLM进行章节调整 + from .llm_helper import LLMHelper + response = LLMHelper.call_llm_with_retry(adjustment_prompt) + + if not response: + logger.error("AI章节调整失败,使用原始章节结构") + return preliminary_chapters + + # 解析AI返回的调整结果 + adjusted_data = LLMHelper.parse_ai_json_response(response) + adjusted_chapters = self._parse_adjusted_chapters(adjusted_data) + + if adjusted_chapters: + logger.info(f"AI成功调整章节结构,共{len(adjusted_chapters)}个章节") + return adjusted_chapters + else: + logger.warning("AI调整结果解析失败,使用原始章节结构") + return preliminary_chapters + + except Exception as e: + logger.error(f"AI章节调整过程失败: {e}") + return preliminary_chapters + + def _build_adjustment_prompt(self, + chapters: List[DocumentChapter], + suggestions: List[Dict[str, Any]]) -> str: + """构建AI章节调整的提示词 + + Args: + chapters: 原始章节列表 + suggestions: 用户选择的建议 + + Returns: + 给AI的提示词 + """ + # 格式化原始章节结构 + chapters_text = self._format_chapters_for_prompt(chapters) + + # 格式化用户选择的建议 + suggestions_text = self._format_suggestions_for_prompt(suggestions) + + prompt = f""" +请根据用户选择的建议调整以下标书章节结构。 + +【当前章节结构】: +{chapters_text} + +【用户选择应用的建议】: +{suggestions_text} + +【调整要求】: +1. 根据用户选择的建议对章节结构进行合理调整 +2. 保持章节的逻辑性和完整性 +3. 每个章节都要有明确的标题和层级 +4. 保持专业的标书格式 + +返回JSON格式: +{{ + "adjusted_chapters": [ + {{ + "id": "chapter_1_technical_solution", + "title": "技术方案", + "level": 1, + "score": 0, + "children": [ + {{ + "id": "chapter_2_1_architecture", + "title": "系统架构设计", + "level": 2, + "score": 0, + "children": [] + }} + ] + }} + ] +}} + +只返回JSON:""" + + return prompt + + def _format_chapters_for_prompt(self, chapters: List[DocumentChapter]) -> str: + """格式化章节结构用于提示词""" + lines = [] + for chapter in chapters: + lines.append(f"{chapter.title} (level: {chapter.level})") + # 递归显示子章节 + self._format_children_for_prompt(chapter.children, lines, 1) + return "\n".join(lines) + + def _format_children_for_prompt(self, children: List[DocumentChapter], lines: List[str], indent: int): + """递归格式化子章节""" + for child in children: + indent_str = " " * indent + lines.append(f"{indent_str}{child.title} (level: {child.level})") + if child.children: + self._format_children_for_prompt(child.children, lines, indent + 1) + + def _format_suggestions_for_prompt(self, suggestions: List[Dict[str, Any]]) -> str: + """格式化建议列表用于提示词""" + lines = [] + for i, suggestion in enumerate(suggestions, 1): + # 动态格式化建议内容,不预设字段 + suggestion_text = f"{i}. " + for key, value in suggestion.items(): + if value: + suggestion_text += f"{key}: {value}, " + lines.append(suggestion_text.rstrip(", ")) + return "\n".join(lines) + + def _parse_adjusted_chapters(self, adjusted_data: Dict[str, Any]) -> List[DocumentChapter]: + """解析AI调整后的章节数据 + + Args: + adjusted_data: AI返回的调整数据 + + Returns: + 解析后的章节列表 + """ + try: + chapters_data = adjusted_data.get("adjusted_chapters", []) + chapters = [] + + for chapter_data in chapters_data: + chapter = self._create_chapter_from_data(chapter_data) + if chapter: + chapters.append(chapter) + + return chapters + + except Exception as e: + logger.error(f"解析AI调整章节失败: {e}") + return [] + + def _create_chapter_from_data(self, chapter_data: Dict[str, Any]) -> DocumentChapter: + """从数据创建章节对象 + + Args: + chapter_data: 章节数据 + + Returns: + 创建的章节对象 + """ + try: + chapter = DocumentChapter( + id=chapter_data.get("id", ""), + title=chapter_data.get("title", ""), + level=chapter_data.get("level", 1), + score=chapter_data.get("score", 0), + children=[] + ) + + # 递归处理子章节 + children_data = chapter_data.get("children", []) + for child_data in children_data: + child_chapter = self._create_chapter_from_data(child_data) + if child_chapter: + chapter.children.append(child_chapter) + + return chapter + + except Exception as e: + logger.error(f"创建章节对象失败: {e}") + return None \ No newline at end of file diff --git a/src/bidmaster/nodes/toc/apply_suggestions.py b/src/bidmaster/nodes/toc/apply_suggestions.py new file mode 100644 index 0000000..86facff --- /dev/null +++ b/src/bidmaster/nodes/toc/apply_suggestions.py @@ -0,0 +1,124 @@ +"""应用AI审查建议的节点 + +让用户选择是否应用AI审查建议,完全依赖AI的分类结果,不做任何人为预设。 +""" + +import logging +from typing import Dict, List, Any + +from ..base import BaseNode, NodeContext +from .base_mixins import TocNodeBase + +logger = logging.getLogger(__name__) + + +class ApplyReviewSuggestionsNode(BaseNode, TocNodeBase): + """应用AI审查建议的节点""" + + @property + def name(self) -> str: + return "apply_suggestions" + + @property + def description(self) -> str: + return "应用AI审查建议" + + def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]: + """执行建议应用交互""" + return self.safe_execute(self._do_apply_suggestions, state, "应用AI审查建议") + + def _do_apply_suggestions(self, state: Dict[str, Any]) -> Dict[str, Any]: + """执行实际的建议应用逻辑""" + logger.info("🔥🔥🔥 ApplyReviewSuggestionsNode 正在执行!!!") + # 验证必需字段 + required_fields = ["preliminary_chapters", "structure_review"] + if not self.validate_required_fields(state, required_fields): + raise ValueError("缺少章节数据或审查结果") + + structure_review = state["structure_review"] + + # 获取交互处理器 + interaction_handler = state.get("interaction_handler") + + if not interaction_handler: + # 如果没有交互处理器,跳过用户选择,不应用任何建议 + logger.warning("未找到交互处理器,跳过建议应用") + approved_suggestions = [] + else: + # 用户选择要应用的建议 + approved_suggestions = self._get_user_approval(structure_review, interaction_handler) + + # 将用户选择的建议保存到状态中,供FinalizeChaptersNode使用 + self.log_step_info("apply_suggestions", + f"用户选择应用{len(approved_suggestions)}条AI建议") + + return self._update_state(state, approved_suggestions=approved_suggestions) + + def _get_user_approval(self, structure_review: Dict[str, Any], + interaction_handler) -> List[Dict[str, Any]]: + """获取用户对AI建议的批准 + + Args: + structure_review: AI审查结果(完全使用原始数据) + interaction_handler: 交互处理器 + + Returns: + 用户批准的建议列表 + """ + # 直接使用AI返回的原始数据,不做任何预设 + suggestions = structure_review.get("suggestions", []) + overall_assessment = structure_review.get("overall_assessment", "") + optimization_score = structure_review.get("optimization_score", "") + + if not suggestions: + logger.info("AI未提供优化建议") + return [] + + try: + # 展示AI的完整评估结果 + assessment_text = f"AI审查评估:\n" + if overall_assessment: + assessment_text += f"总体评价: {overall_assessment}\n" + if optimization_score: + assessment_text += f"优化评分: {optimization_score}\n" + assessment_text += f"AI识别出{len(suggestions)}条建议" + + # 询问用户是否要查看并选择建议 + view_suggestions = interaction_handler( + interaction_type="confirm", + prompt=f"{assessment_text}\n是否查看具体建议并选择应用?", + default=True, + key="view_ai_suggestions" + ) + + if not view_suggestions: + return [] + + # 逐个展示AI建议,让用户选择 + approved_suggestions = [] + for i, suggestion in enumerate(suggestions, 1): + # 完全使用AI提供的原始建议内容,不做任何格式化预设 + suggestion_text = f"AI建议{i}:\n" + + # 动态展示AI建议中的所有字段,不预设字段名 + for key, value in suggestion.items(): + if value: # 只显示有值的字段 + suggestion_text += f"{key}: {value}\n" + + # 让用户选择是否应用这条AI建议 + apply_suggestion = interaction_handler( + interaction_type="confirm", + prompt=f"{suggestion_text}是否应用此AI建议?", + default=False, # 默认不应用,让用户主动选择 + key=f"apply_ai_suggestion_{i}" + ) + + if apply_suggestion: + approved_suggestions.append(suggestion) + + return approved_suggestions + + except Exception as e: + logger.error(f"获取用户建议选择失败: {e}") + # 出错时不应用任何建议,保持安全 + return [] \ No newline at end of file diff --git a/src/bidmaster/nodes/toc/base_mixins.py b/src/bidmaster/nodes/toc/base_mixins.py index 9a7f90d..b4ef7dc 100644 --- a/src/bidmaster/nodes/toc/base_mixins.py +++ b/src/bidmaster/nodes/toc/base_mixins.py @@ -153,9 +153,9 @@ class WorkflowUtilsMixin: Returns: "continue" 如果应该继续,"end" 如果应该停止 """ - if state.get("error") or not state.get("should_continue", True): - return "end" - return "continue" + result = "end" if (state.get("error") or not state.get("should_continue", True)) else "continue" + logger.info(f"🔥🔥🔥 should_continue_workflow: {result}, error={state.get('error')}, should_continue={state.get('should_continue', True)}") + return result @staticmethod def validate_required_fields(state: Dict[str, Any], diff --git a/src/bidmaster/nodes/toc/finalize_chapters.py b/src/bidmaster/nodes/toc/finalize_chapters.py index a2444b4..702087f 100644 --- a/src/bidmaster/nodes/toc/finalize_chapters.py +++ b/src/bidmaster/nodes/toc/finalize_chapters.py @@ -35,11 +35,11 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase): if not self.validate_required_fields(state, ["preliminary_chapters"]): raise ValueError("缺少初步章节数据") - preliminary_chapters = state["preliminary_chapters"] - structure_review = state.get("structure_review", {}) + # 获取调整后的章节(如果有)或原始章节 + adjusted_chapters = state.get("adjusted_chapters", state["preliminary_chapters"]) - # 应用审查建议并最终确定 - final_chapters = self._finalize_with_review(preliminary_chapters, structure_review) + # 应用最终确定逻辑(主要是确保标准章节存在) + final_chapters = adjusted_chapters # 确保标准章节存在 final_chapters = self._ensure_standard_chapters(final_chapters) @@ -50,47 +50,6 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase): final_chapters=final_chapters, should_continue=False) - def _finalize_with_review(self, - preliminary_chapters: List[DocumentChapter], - structure_review: Dict[str, Any]) -> List[DocumentChapter]: - """根据审查结果最终确定章节 - - Args: - preliminary_chapters: 初步章节列表 - structure_review: AI审查结果 - - Returns: - 最终章节列表 - """ - final_chapters = preliminary_chapters.copy() - - # 应用高优先级建议 - suggestions = structure_review.get("suggestions", []) - high_priority_suggestions = [ - s for s in suggestions - if s.get("priority") == "high" - ] - - for suggestion in high_priority_suggestions: - suggestion_type = suggestion.get("type", "") - description = suggestion.get("description", "") - - if suggestion_type == "add": - logger.info(f"应用高优先级建议-添加: {description}") - # 这里可以根据具体建议内容添加章节 - # 当前版本保持简单,只记录日志 - - elif suggestion_type == "modify": - logger.info(f"应用高优先级建议-修改: {description}") - # 这里可以根据具体建议内容修改章节 - # 当前版本保持简单,只记录日志 - - elif suggestion_type == "reorder": - logger.info(f"应用高优先级建议-重排: {description}") - # 这里可以根据具体建议内容重新排序 - # 当前版本保持简单,只记录日志 - - return final_chapters def _ensure_standard_chapters(self, chapters: List[DocumentChapter]) -> List[DocumentChapter]: """确保包含标准章节