diff --git a/src/bidmaster/agents/analysis.py b/src/bidmaster/agents/analysis.py index 9d153ae..d7b8ee7 100644 --- a/src/bidmaster/agents/analysis.py +++ b/src/bidmaster/agents/analysis.py @@ -16,6 +16,7 @@ from pydantic import BaseModel, Field from ..tools.parser import BidParser, BidStructure, ScoringCriteria, DeviationItem, DocumentChapter from ..config import get_settings +from ..nodes.toc.workflow_utils import should_continue_workflow logger = logging.getLogger(__name__) @@ -134,11 +135,13 @@ def extract_tables_node(state: AnalysisAgentState) -> AnalysisAgentState: continue # 提取表格文本 + from ..tools.parser import BidParser + parser = BidParser() table_data = { "index": i, "row_count": len(table.rows), "col_count": max(len(row.cells) for row in table.rows) if table.rows else 0, - "text_content": _extract_table_text(table) + "text_content": parser.extract_table_text(table) } raw_tables.append(table_data) @@ -359,39 +362,10 @@ def finalize_structure_node(state: AnalysisAgentState) -> AnalysisAgentState: return state -# ========== 辅助函数 =========== - - - -def _extract_table_text(table) -> str: - """提取表格内容为文本格式""" - lines = [] - max_cols = max(len(row.cells) for row in table.rows) if table.rows else 0 - - for i, row in enumerate(table.rows): - cells = [] - for j in range(max_cols): - if j < len(row.cells): - cell_text = row.cells[j].text.strip() - if not cell_text: - cell_text = "[空]" - cells.append(cell_text) - else: - cells.append("[空]") - - line = "\t".join(cells) - lines.append(f"行{i+1}: {line}") - - return "\n".join(lines) - - # ========== 条件判断函数 ========== -def should_continue_processing(state: AnalysisAgentState) -> str: - """判断是否继续处理""" - if not state.get("should_continue", True) or state.get("error"): - return "end" - return "continue" +# 使用通用的工作流条件判断函数 +should_continue_processing = should_continue_workflow class AnalysisAgent: diff --git a/src/bidmaster/agents/builders/toc_builder.py b/src/bidmaster/agents/builders/toc_builder.py index 75640b8..b489c88 100644 --- a/src/bidmaster/agents/builders/toc_builder.py +++ b/src/bidmaster/agents/builders/toc_builder.py @@ -16,22 +16,13 @@ from ...nodes.toc import ( ReviewStructureNode, FinalizeChaptersNode ) +from ...nodes.toc.workflow_utils import should_continue_workflow logger = logging.getLogger(__name__) -def should_continue(state: Dict[str, Any]) -> str: - """判断是否继续处理 - - Args: - state: 当前状态 - - Returns: - "continue" 或 "end" - """ - if state.get("error") or not state.get("should_continue", True): - return "end" - return "continue" +# 使用通用的工作流条件判断函数 +should_continue = should_continue_workflow class TocAgentBuilder(AgentBuilder): diff --git a/src/bidmaster/nodes/toc/llm_helper.py b/src/bidmaster/nodes/toc/llm_helper.py index 9d84920..8a311c4 100644 --- a/src/bidmaster/nodes/toc/llm_helper.py +++ b/src/bidmaster/nodes/toc/llm_helper.py @@ -52,24 +52,28 @@ class LLMHelper: raise ValueError(f"解析AI响应失败: {e}") @staticmethod - def call_llm_with_retry(prompt: str, max_retries: int = 2) -> Optional[str]: + def call_llm_with_retry(prompt: str, max_retries: int = 2, temperature: float = 0.7) -> Optional[str]: """带重试的LLM调用 Args: prompt: 提示词 max_retries: 最大重试次数 + temperature: 温度参数 Returns: LLM响应,失败时返回None """ for attempt in range(max_retries + 1): try: - response = LLMService().call(prompt) + response = LLMService().call(prompt, temperature) if response: return response logger.warning(f"LLM调用第{attempt + 1}次无响应") except Exception as e: logger.error(f"LLM调用第{attempt + 1}次失败: {e}") + # 最后一次重试失败才返回None,否则继续重试 + if attempt == max_retries: + return None return None diff --git a/src/bidmaster/nodes/toc/workflow_utils.py b/src/bidmaster/nodes/toc/workflow_utils.py new file mode 100644 index 0000000..5a83edf --- /dev/null +++ b/src/bidmaster/nodes/toc/workflow_utils.py @@ -0,0 +1,46 @@ +"""工作流通用工具函数 + +提供各种工作流中常用的条件判断、状态检查等通用功能。 +""" + +from typing import Dict, Any + + +def should_continue_workflow(state: Dict[str, Any]) -> str: + """通用的工作流继续条件判断 + + 检查工作流是否应该继续执行,基于错误状态和继续标志。 + + Args: + state: 工作流状态字典 + + Returns: + "continue" 如果应该继续,"end" 如果应该停止 + """ + if state.get("error") or not state.get("should_continue", True): + return "end" + return "continue" + + +def has_error(state: Dict[str, Any]) -> bool: + """检查状态中是否有错误 + + Args: + state: 工作流状态字典 + + Returns: + True 如果有错误,False 否则 + """ + return bool(state.get("error")) + + +def should_stop(state: Dict[str, Any]) -> bool: + """检查是否应该停止工作流 + + Args: + state: 工作流状态字典 + + Returns: + True 如果应该停止,False 否则 + """ + return has_error(state) or not state.get("should_continue", True) \ No newline at end of file diff --git a/src/bidmaster/tools/parser.py b/src/bidmaster/tools/parser.py index fa6ee38..a835899 100644 --- a/src/bidmaster/tools/parser.py +++ b/src/bidmaster/tools/parser.py @@ -4,7 +4,6 @@ 支持Excel、CSV、Word表格格式。 """ -import json import logging from pathlib import Path from typing import Any, List @@ -239,7 +238,7 @@ class BidParser: return criteria - def _extract_table_text(self, table) -> str: + def extract_table_text(self, table) -> str: """提取表格内容为文本格式,处理合并单元格""" lines = [] @@ -309,7 +308,7 @@ class BidParser: 只返回JSON,无其他文字:""" # 调用LLM API - response = self._call_llm_api(prompt) + response = self.call_llm(prompt) if not response: raise ValueError("AI解析表格失败:无响应") @@ -321,16 +320,9 @@ class BidParser: logger.error("AI返回空响应") return [] - # 尝试清理响应内容 - clean_response = response.strip() - if clean_response.startswith("```json"): - clean_response = clean_response[7:] - if clean_response.endswith("```"): - clean_response = clean_response[:-3] - clean_response = clean_response.strip() - - # 直接解析JSON,失败就抛出异常 - result_data = json.loads(clean_response) + # 使用统一的JSON解析方法 + from ..nodes.toc.llm_helper import LLMHelper + result_data = LLMHelper.parse_ai_json_response(response) scoring_data = result_data.get("scoring_criteria", []) @@ -363,7 +355,7 @@ class BidParser: return criteria - except (json.JSONDecodeError, ValueError, KeyError) as e: + except (ValueError, KeyError) as e: logger.error(f"解析AI响应失败: {e}") return [] @@ -373,35 +365,8 @@ class BidParser: def call_llm(self, prompt: str) -> str | None: """公共方法:调用LLM API""" - return self._call_llm_api(prompt) - - def _call_llm_api(self, prompt: str) -> str | None: - """调用LLM API""" - try: - # 使用OpenAI SDK调用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": "user", - "content": prompt - } - ], - temperature=self.settings.temperature, - max_tokens=self.settings.max_tokens - ) - - return response.choices[0].message.content - - except Exception as e: - logger.error(f"LLM API调用异常: {e}") - return None - + from ..nodes.toc.llm_helper import LLMHelper + return LLMHelper.call_llm_with_retry(prompt, max_retries=1) def _identify_table_type(self, table_text: str) -> str: """使用AI识别表格类型""" @@ -425,7 +390,7 @@ class BidParser: 只返回一个单词:scoring 或 deviation 或 other""" - response = self._call_llm_api(prompt) + response = self.call_llm(prompt) if not response: raise ValueError("AI识别表格类型失败:无响应") @@ -447,7 +412,7 @@ class BidParser: continue # 提取表格内容为文本 - table_text = self._extract_table_text(table) + table_text = self.extract_table_text(table) # 识别表格类型 table_type = self._identify_table_type(table_text) @@ -482,7 +447,7 @@ class BidParser: continue # 提取表格内容为文本 - table_text = self._extract_table_text(table) + table_text = self.extract_table_text(table) # 识别表格类型 table_type = self._identify_table_type(table_text) @@ -539,13 +504,14 @@ class BidParser: 只返回JSON,无其他文字:""" - response = self._call_llm_api(prompt) + response = self.call_llm(prompt) if not response: raise ValueError("AI解析偏离表失败:无响应") # 解析AI响应 try: - result_data = json.loads(response) + from ..nodes.toc.llm_helper import LLMHelper + result_data = LLMHelper.parse_ai_json_response(response) deviation_data = result_data.get("deviation_items", []) items = [] @@ -559,7 +525,7 @@ class BidParser: return items - except (json.JSONDecodeError, ValueError, KeyError) as e: + except (ValueError, KeyError) as e: logger.error(f"解析偏离表AI响应失败: {e}") return []