diff --git a/src/bidmaster/agents/__init__.py b/src/bidmaster/agents/__init__.py index 75553e4..593c1ba 100644 --- a/src/bidmaster/agents/__init__.py +++ b/src/bidmaster/agents/__init__.py @@ -1,14 +1,12 @@ """Agent层 - LangGraph编排""" from .analysis import AnalysisAgent -from .toc_generator import TocGeneratorAgent from .builders import TocAgentBuilder from .builders.toc_builder import TocAgent from .interaction import InteractionHandler, InteractionMode __all__ = [ "AnalysisAgent", - "TocGeneratorAgent", "TocAgentBuilder", "TocAgent", "InteractionHandler", diff --git a/src/bidmaster/agents/analysis.py b/src/bidmaster/agents/analysis.py index 388db65..9d153ae 100644 --- a/src/bidmaster/agents/analysis.py +++ b/src/bidmaster/agents/analysis.py @@ -254,8 +254,8 @@ def parse_content_node(state: AnalysisAgentState) -> AnalysisAgentState: def generate_toc_with_agent_node(state: AnalysisAgentState) -> AnalysisAgentState: - """节点5:使用TocGeneratorAgent生成目录结构""" - logger.info("开始使用TocGeneratorAgent生成目录...") + """节点5:使用TocAgent生成目录结构""" + logger.info("开始使用TocAgent生成目录...") try: technical_criteria = state["technical_criteria"] @@ -306,7 +306,7 @@ def generate_toc_with_agent_node(state: AnalysisAgentState) -> AnalysisAgentStat # 更新状态 state["preliminary_chapters"] = chapters state["technical_criteria"] = technical_criteria - state["structure_review"] = {} # TocGeneratorAgent已包含审查 + state["structure_review"] = {} # TocAgent已包含审查 state["current_step"] = "generate_toc" state["progress"] = 0.85 @@ -336,7 +336,7 @@ def finalize_structure_node(state: AnalysisAgentState) -> AnalysisAgentState: project_name=f"标书项目-{Path(state['source_file']).stem}", scoring_criteria=technical_criteria, deviation_items=state["deviation_items"], - chapters=preliminary_chapters, # 使用TocGeneratorAgent生成的章节 + chapters=preliminary_chapters, # 使用TocAgent生成的章节 scoring_file=state["source_file"] ) diff --git a/src/bidmaster/agents/toc_generator.py b/src/bidmaster/agents/toc_generator.py deleted file mode 100644 index 598e252..0000000 --- a/src/bidmaster/agents/toc_generator.py +++ /dev/null @@ -1,585 +0,0 @@ -"""目录生成Agent - 专门负责生成标书目录结构 - -基于LangGraph实现的目录生成Agent,负责: -1. 根据评分项类别生成一级章节 -2. AI智能生成二三级子标题 -3. 目录结构合理性审查与优化 -""" - -import asyncio -import json -import logging -from typing import List, Dict, Any, TypedDict, Optional - -from langgraph.graph import StateGraph, END -from pydantic import BaseModel, Field - -from ..tools.parser import ScoringCriteria, DocumentChapter -from ..tools.llm import llm_service -from ..config import get_settings - -logger = logging.getLogger(__name__) - -# 统一的类别名称映射 -CATEGORY_NAMES = { - "compliance": "合规响应", - "technical_solution": "技术方案", - "equipment_spec": "设备规格", - "quality_safety": "质量安全", - "after_sales": "售后服务", - "implementation": "实施方案" -} - - -class TocGeneratorState(TypedDict): - """目录生成Agent的状态定义""" - - # 输入参数 - technical_criteria: List[ScoringCriteria] - generation_mode: str # "ai" 或 "template" - template_file: Optional[str] - - # 执行状态 - current_step: str - should_continue: bool - - # 中间数据 - category_groups: Dict[str, List[ScoringCriteria]] # 按类别分组的评分项 - preliminary_chapters: List[DocumentChapter] # 初步生成的章节 - structure_review: Dict[str, Any] # AI审查结果 - - # 最终输出 - final_chapters: List[DocumentChapter] - - # 错误处理 - error: str - warnings: List[str] - - -class TocGeneratorResult(BaseModel): - """目录生成结果""" - - success: bool = Field(description="是否执行成功") - chapters: List[DocumentChapter] = Field(default_factory=list, description="生成的章节结构") - error_message: Optional[str] = Field(default=None, description="错误信息") - warnings: List[str] = Field(default_factory=list, description="警告信息") - - -# ========== LangGraph节点函数 ========== - -def group_criteria_node(state: TocGeneratorState) -> TocGeneratorState: - """节点1:按类别对评分项分组""" - logger.info("开始对评分项按类别分组...") - - try: - technical_criteria = state["technical_criteria"] - - if not technical_criteria: - raise ValueError("缺少技术评分项") - - # 按类别分组 - category_groups = { - "technical_solution": [], - "equipment_spec": [], - "implementation": [], - "quality_safety": [], - "after_sales": [], - "compliance": [] - } - - for criteria in technical_criteria: - category_key = criteria.category.value - if category_key in category_groups: - category_groups[category_key].append(criteria) - else: - category_groups["technical_solution"].append(criteria) - - # 只保留有评分项的类别 - category_groups = {k: v for k, v in category_groups.items() if v} - - logger.info(f"分组完成: {len(category_groups)}个类别") - state["category_groups"] = category_groups - state["current_step"] = "group_criteria" - - except Exception as e: - logger.error(f"评分项分组失败: {e}") - state["error"] = str(e) - state["should_continue"] = False - - return state - - -def generate_first_level_node(state: TocGeneratorState) -> TocGeneratorState: - """节点2:生成一级章节""" - logger.info("开始生成一级章节...") - - try: - category_groups = state["category_groups"] - technical_criteria = state["technical_criteria"] - - chapters = [] - chapter_index = 1 - - # 按原始顺序确定章节顺序 - def get_category_first_index(category): - for i, criteria in enumerate(technical_criteria): - if criteria.category.value == category: - return i - return 999 - - sorted_categories = sorted(category_groups.keys(), key=get_category_first_index) - - # 为每个类别创建一级章节 - for category in sorted_categories: - criteria_list = category_groups[category] - if not criteria_list: - continue - - total_score = sum(c.max_score for c in criteria_list) - chapter_id = f"chapter_{chapter_index:02d}_{category}" - category_name = CATEGORY_NAMES.get(category, category) - chapter_title = category_name - - if total_score > 0: - chapter_title += f" ({total_score}分)" - - chapter = DocumentChapter( - id=chapter_id, - title=chapter_title, - level=1, - score=total_score, - template_placeholder=f"{{{{{chapter_id}_content}}}}" - ) - - chapters.append(chapter) - chapter_index += 1 - - logger.info(f"生成{len(chapters)}个一级章节") - state["preliminary_chapters"] = chapters - state["current_step"] = "generate_first_level" - - except Exception as e: - logger.error(f"一级章节生成失败: {e}") - state["error"] = str(e) - state["should_continue"] = False - - return state - - -def generate_sub_chapters_node(state: TocGeneratorState) -> TocGeneratorState: - """节点3:生成二三级子标题""" - logger.info("开始生成子标题...") - - try: - preliminary_chapters = state["preliminary_chapters"] - technical_criteria = state["technical_criteria"] - generation_mode = state.get("generation_mode", "ai") - template_file = state.get("template_file") - - enhanced_chapters = [] - - for chapter in preliminary_chapters: - # 找到该章节对应的评分项 - # 从章节ID提取类别 - if "_" in chapter.id: - parts = chapter.id.split("_") - if len(parts) >= 3: - category = "_".join(parts[2:]) - - # 找到该类别的所有评分项 - corresponding_criteria = [ - c for c in technical_criteria - if c.category.value == category - ] - - if corresponding_criteria: - if generation_mode == "ai": - sub_chapters = _generate_ai_sub_chapters(corresponding_criteria, chapter) - else: - sub_chapters = _generate_template_sub_chapters( - corresponding_criteria[0], chapter, template_file - ) - - chapter.children = sub_chapters - logger.info(f"章节 {chapter.title} 生成了 {len(sub_chapters)} 个子标题") - - enhanced_chapters.append(chapter) - - state["preliminary_chapters"] = enhanced_chapters - state["current_step"] = "generate_sub_chapters" - - except Exception as e: - logger.error(f"子标题生成失败: {e}") - state["error"] = str(e) - state["should_continue"] = False - - return state - - -def review_structure_node(state: TocGeneratorState) -> TocGeneratorState: - """节点4:AI审查目录结构""" - logger.info("开始AI审查目录结构...") - - try: - technical_criteria = state["technical_criteria"] - preliminary_chapters = state["preliminary_chapters"] - - # 构建审查提示词 - criteria_summary = _format_criteria_for_review(technical_criteria) - chapters_summary = _format_chapters_for_review(preliminary_chapters) - - review_prompt = f""" -请审查这个标书目录结构的合理性和完整性。 - -【技术评分项分布】: -{criteria_summary} - -【当前生成的章节结构】: -{chapters_summary} - -【审查要求】: -1. 是否缺少重要的标准章节? -2. 章节顺序是否合理? -3. 每个评分项是否都有对应章节? - -返回JSON格式: -{{ - "overall_assessment": "总体评价", - "suggestions": [ - {{"type": "add/modify/reorder", "description": "建议内容", "priority": "high/medium/low"}} - ], - "optimization_score": 85 -}} - -只返回JSON:""" - - # 使用统一的LLM服务 - response = llm_service.call(review_prompt) - - if response: - try: - clean_response = response.strip() - if clean_response.startswith("```json"): - clean_response = clean_response[7:] - if clean_response.endswith("```"): - clean_response = clean_response[:-3] - review_result = json.loads(clean_response.strip()) - except json.JSONDecodeError: - review_result = {"overall_assessment": "解析失败", "suggestions": []} - else: - review_result = {"overall_assessment": "AI审查失败", "suggestions": []} - - state["structure_review"] = review_result - state["current_step"] = "review_structure" - - # 根据审查结果添加警告 - if review_result.get("suggestions"): - state["warnings"].append(f"AI审查: {len(review_result['suggestions'])}条优化建议") - - except Exception as e: - logger.error(f"AI审查失败: {e}") - state["warnings"].append(f"AI审查跳过: {str(e)}") - state["structure_review"] = {} - - return state - - -def finalize_chapters_node(state: TocGeneratorState) -> TocGeneratorState: - """节点5:最终确定章节结构""" - logger.info("最终确定章节结构...") - - try: - preliminary_chapters = state["preliminary_chapters"] - structure_review = state.get("structure_review", {}) - - final_chapters = preliminary_chapters # 直接使用,不做防护编程 - - # 应用高优先级建议 - suggestions = structure_review.get("suggestions", []) - for suggestion in suggestions: - if suggestion.get("priority") == "high": - # 这里可以根据建议类型进行调整 - logger.info(f"应用高优先级建议: {suggestion.get('description')}") - - # 确保评标索引表存在(作为第一章) - has_index = any("评标索引" in ch.title for ch in final_chapters) - if not has_index: - index_chapter = DocumentChapter( - id="evaluation_index", - title="评标索引表(技术评分完全对应)", # 不包含编号 - level=1, - template_placeholder="{{evaluation_index_content}}" - ) - final_chapters.insert(0, index_chapter) - - state["final_chapters"] = final_chapters - state["current_step"] = "finalize" - state["should_continue"] = False - - logger.info(f"最终生成{len(final_chapters)}个章节") - - except Exception as e: - logger.error(f"章节最终确定失败: {e}") - state["error"] = str(e) - state["should_continue"] = False - - return state - - -# ========== 辅助函数 ========== - -def _generate_ai_sub_chapters(criteria_list: List[ScoringCriteria], - parent_chapter: DocumentChapter) -> List[DocumentChapter]: - """AI生成子标题""" - try: - criteria_info = [] - for criteria in criteria_list: - criteria_info.append(f"- {criteria.item_name} ({criteria.max_score}分)") - - prompt = f""" -为以下大类别生成专业的标书子标题: - -【大类别】: {parent_chapter.title} -【评分项】: -{chr(10).join(criteria_info)} - -生成要求: -1. 为每个评分项生成对应的子标题名称(不要包含编号) -2. 重要评分项可添加三级子标题(不要包含编号) -3. 只返回标题文本,编号由Word自动管理 - -返回JSON格式: -{{ - "sub_chapters": [ - {{"title": "技术架构设计", "level": 2, "score": 5, "children": []}} - ] -}} - -只返回JSON:""" - - # 使用统一的LLM服务 - response = llm_service.call(prompt) - if not response: - raise ValueError("AI生成子标题失败: API无响应") - - try: - clean_response = response.strip() - if clean_response.startswith("```json"): - clean_response = clean_response[7:] - if clean_response.endswith("```"): - clean_response = clean_response[:-3] - - result_data = json.loads(clean_response.strip()) - sub_chapters_data = result_data.get("sub_chapters", []) - - sub_chapters = [] - - for i, sub_data in enumerate(sub_chapters_data, 1): - # 直接使用标题文本,不添加编号 - title = sub_data.get("title", f"子标题{i}") - - sub_chapter = DocumentChapter( - id=f"{parent_chapter.id}_sub_{i:02d}", - title=title, # 不添加编号 - level=sub_data.get("level", 2), - score=sub_data.get("score", 0), - template_placeholder=f"{{{{{parent_chapter.id}_sub_{i:02d}_content}}}}" - ) - - # 处理三级标题 - for j, child_data in enumerate(sub_data.get("children", []), 1): - child_title = child_data.get("title", f"三级标题{j}") - - child_chapter = DocumentChapter( - id=f"{parent_chapter.id}_sub_{i:02d}_{j:02d}", - title=child_title, # 不添加编号 - level=child_data.get("level", 3), - template_placeholder=f"{{{{{parent_chapter.id}_sub_{i:02d}_{j:02d}_content}}}}" - ) - sub_chapter.children.append(child_chapter) - - sub_chapters.append(sub_chapter) - - return sub_chapters - - except (json.JSONDecodeError, KeyError) as e: - logger.error(f"解析AI响应失败: {e}") - raise ValueError(f"解析AI响应失败: {e}") - - except Exception as e: - logger.error(f"AI生成子标题失败: {e}") - raise # 立即失败,不掩盖错误 - - -def _generate_template_sub_chapters(criteria: ScoringCriteria, - parent_chapter: DocumentChapter, - template_file: Optional[str]) -> List[DocumentChapter]: - """基于模板生成子标题""" - # 提供默认结构(不包含编号) - default_sub_chapters = [ - DocumentChapter( - id=f"{parent_chapter.id}_def_01", - title="方案概述", # 不包含编号 - level=2, - template_placeholder=f"{{{{{parent_chapter.id}_def_01_content}}}}" - ), - DocumentChapter( - id=f"{parent_chapter.id}_def_02", - title="具体实施", # 不包含编号 - level=2, - template_placeholder=f"{{{{{parent_chapter.id}_def_02_content}}}}" - ), - DocumentChapter( - id=f"{parent_chapter.id}_def_03", - title="保障措施", # 不包含编号 - level=2, - template_placeholder=f"{{{{{parent_chapter.id}_def_03_content}}}}" - ) - ] - - return default_sub_chapters - - -def _format_criteria_for_review(technical_criteria: List[ScoringCriteria]) -> str: - """格式化评分项用于审查""" - lines = [] - - category_groups = {} - for criteria in technical_criteria: - category = criteria.category.value - if category not in category_groups: - category_groups[category] = [] - category_groups[category].append(criteria) - - for category, items in category_groups.items(): - category_name = CATEGORY_NAMES.get(category, category) - lines.append(f"【{category_name}】({len(items)}项):") - for item in items: - lines.append(f" - {item.item_name} ({item.max_score}分)") - - return "\n".join(lines) - - -def _format_chapters_for_review(chapters: List[DocumentChapter]) -> str: - """格式化章节用于审查""" - lines = [] - for chapter in chapters: - lines.append(chapter.title) - for sub in chapter.children: - lines.append(f" {sub.title}") - for child in sub.children: - lines.append(f" {child.title}") - return "\n".join(lines) - - -# ========== 条件判断 ========== - -def should_continue(state: TocGeneratorState) -> str: - """判断是否继续""" - if not state.get("should_continue", True) or state.get("error"): - return "end" - return "continue" - - -class TocGeneratorAgent: - """目录生成Agent""" - - def __init__(self): - self.settings = get_settings() - self.graph = self._build_graph() - - def _build_graph(self) -> StateGraph: - """构建工作流""" - workflow = StateGraph(TocGeneratorState) - - # 添加节点 - workflow.add_node("group_criteria", group_criteria_node) - workflow.add_node("generate_first_level", generate_first_level_node) - workflow.add_node("generate_sub_chapters", generate_sub_chapters_node) - workflow.add_node("review_structure", review_structure_node) - workflow.add_node("finalize_chapters", finalize_chapters_node) - - # 设置入口 - workflow.set_entry_point("group_criteria") - - # 添加边 - workflow.add_conditional_edges( - "group_criteria", - should_continue, - {"continue": "generate_first_level", "end": END} - ) - - workflow.add_conditional_edges( - "generate_first_level", - should_continue, - {"continue": "generate_sub_chapters", "end": END} - ) - - workflow.add_conditional_edges( - "generate_sub_chapters", - should_continue, - {"continue": "review_structure", "end": END} - ) - - workflow.add_conditional_edges( - "review_structure", - should_continue, - {"continue": "finalize_chapters", "end": END} - ) - - workflow.add_edge("finalize_chapters", END) - - return workflow.compile() - - async def generate(self, - technical_criteria: List[ScoringCriteria], - generation_mode: str = "ai", - template_file: Optional[str] = None) -> TocGeneratorResult: - """生成目录结构""" - logger.info("开始执行TocGeneratorAgent") - - # 初始化状态 - initial_state = TocGeneratorState( - technical_criteria=technical_criteria, - generation_mode=generation_mode, - template_file=template_file, - current_step="", - should_continue=True, - category_groups={}, - preliminary_chapters=[], - structure_review={}, - final_chapters=[], - error="", - warnings=[] - ) - - try: - # 执行工作流 - final_state = await self.graph.ainvoke(initial_state) - - if final_state.get("error"): - return TocGeneratorResult( - success=False, - error_message=final_state["error"], - warnings=final_state.get("warnings", []) - ) - - return TocGeneratorResult( - success=True, - chapters=final_state["final_chapters"], - warnings=final_state.get("warnings", []) - ) - - except Exception as e: - logger.error(f"TocGeneratorAgent执行异常: {e}") - return TocGeneratorResult( - success=False, - error_message=str(e) - ) - - def generate_sync(self, - technical_criteria: List[ScoringCriteria], - generation_mode: str = "ai", - template_file: Optional[str] = None) -> TocGeneratorResult: - """同步接口""" - return asyncio.run(self.generate(technical_criteria, generation_mode, template_file)) \ No newline at end of file diff --git a/src/bidmaster/nodes/toc/generate_sub_chapters.py b/src/bidmaster/nodes/toc/generate_sub_chapters.py index 183cfc6..55b9141 100644 --- a/src/bidmaster/nodes/toc/generate_sub_chapters.py +++ b/src/bidmaster/nodes/toc/generate_sub_chapters.py @@ -9,7 +9,7 @@ from typing import Dict, List, Any, Optional from ..base import BaseNode, NodeContext from ...tools.parser import ScoringCriteria, DocumentChapter -from ...tools.llm import llm_service +from ...tools.llm import LLMService logger = logging.getLogger(__name__) @@ -168,7 +168,7 @@ class GenerateSubChaptersNode(BaseNode): 只返回JSON:""" # 使用统一的LLM服务 - response = llm_service.call(prompt) + response = LLMService().call(prompt) if not response: raise ValueError("AI生成子标题失败: API无响应") diff --git a/src/bidmaster/nodes/toc/review_structure.py b/src/bidmaster/nodes/toc/review_structure.py index eaba659..28265fa 100644 --- a/src/bidmaster/nodes/toc/review_structure.py +++ b/src/bidmaster/nodes/toc/review_structure.py @@ -9,7 +9,7 @@ from typing import Dict, List, Any from ..base import BaseNode, NodeContext from ...tools.parser import ScoringCriteria, DocumentChapter -from ...tools.llm import llm_service +from ...tools.llm import LLMService logger = logging.getLogger(__name__) @@ -114,7 +114,7 @@ class ReviewStructureNode(BaseNode): try: # 使用统一的LLM服务 - response = llm_service.call(review_prompt) + response = LLMService().call(review_prompt) if response: return self._parse_review_response(response) diff --git a/src/bidmaster/tools/__init__.py b/src/bidmaster/tools/__init__.py index 64c8e3f..a9b78fd 100644 --- a/src/bidmaster/tools/__init__.py +++ b/src/bidmaster/tools/__init__.py @@ -1,13 +1,13 @@ """工具层 - 原子化工具集""" -from .llm import llm_service +from .llm import LLMService from .parser import BidParser from .rag import RAGTool from .table import TableGenerator from .word import WordProcessor __all__ = [ - "llm_service", + "LLMService", "BidParser", "RAGTool", "TableGenerator", diff --git a/src/bidmaster/tools/llm.py b/src/bidmaster/tools/llm.py index 675b966..365008f 100644 --- a/src/bidmaster/tools/llm.py +++ b/src/bidmaster/tools/llm.py @@ -65,5 +65,4 @@ class LLMService: raise # 立即失败 -# 全局单例 -llm_service = LLMService() \ No newline at end of file +# 移除模块级实例化,使用标准单例模式:LLMService() \ No newline at end of file