diff --git a/src/app/mcp_widgets.py b/src/app/mcp_widgets.py index 7fb5d27f126fc0eb9063e97d7dfcd0aee782f9b0..2ccb3795cec77c0b74c76fdc12d58f6051e8ff49 100644 --- a/src/app/mcp_widgets.py +++ b/src/app/mcp_widgets.py @@ -84,12 +84,12 @@ class MCPConfirmWidget(Container): @on(Button.Pressed, "#mcp-confirm-yes") def confirm_execution(self) -> None: """确认执行""" - self.post_message(MCPConfirmResult(confirmed=True, task_id=self.event.get_task_id())) + self.post_message(MCPConfirmResult(confirmed=True, conversation_id=self.event.get_conversation_id())) @on(Button.Pressed, "#mcp-confirm-no") def cancel_execution(self) -> None: """取消执行""" - self.post_message(MCPConfirmResult(confirmed=False, task_id=self.event.get_task_id())) + self.post_message(MCPConfirmResult(confirmed=False, conversation_id=self.event.get_conversation_id())) def on_key(self, event) -> None: # noqa: ANN001 """处理键盘事件""" @@ -185,16 +185,6 @@ class MCPParameterWidget(Container): self.param_inputs[param_name] = param_input yield param_input - # 简化的补充说明输入 - if params: # 只有在有其他参数时才显示补充说明 - description_input = Input( - placeholder=_("补充说明(可选)"), - id="param_description", - classes="param-input-compact", - ) - self.param_inputs["description"] = description_input - yield description_input - # 紧凑的按钮行 with Horizontal(classes="param-buttons"): yield Button(_("✓ 提交"), variant="success", id="mcp-param-submit") @@ -204,45 +194,36 @@ class MCPParameterWidget(Container): def submit_parameters(self) -> None: """提交参数""" # 收集用户输入的参数 - content_params = {} - description = "" + params = {} for param_name, input_widget in self.param_inputs.items(): value = input_widget.value.strip() - if param_name == "description": - description = value - elif value: - content_params[param_name] = value - - # 构建参数结构 - params = { - "content": content_params, - "description": description, - } + if value: + params[param_name] = value - self.post_message(MCPParameterResult(params=params, task_id=self.event.get_task_id())) + self.post_message(MCPParameterResult(params=params, conversation_id=self.event.get_conversation_id())) @on(Button.Pressed, "#mcp-param-cancel") def cancel_parameters(self) -> None: """取消参数输入""" - self.post_message(MCPParameterResult(params=None, task_id=self.event.get_task_id())) + self.post_message(MCPParameterResult(params=None, conversation_id=self.event.get_conversation_id())) class MCPConfirmResult(Message): """MCP 确认结果消息""" - def __init__(self, *, confirmed: bool, task_id: str) -> None: + def __init__(self, *, confirmed: bool, conversation_id: str) -> None: """初始化确认结果""" super().__init__() self.confirmed = confirmed - self.task_id = task_id + self.conversation_id = conversation_id class MCPParameterResult(Message): """MCP 参数结果消息""" - def __init__(self, *, params: dict | None, task_id: str) -> None: + def __init__(self, *, params: dict | None, conversation_id: str) -> None: """初始化参数结果""" super().__init__() self.params = params - self.task_id = task_id + self.conversation_id = conversation_id diff --git a/src/app/tui.py b/src/app/tui.py index dbc6ba4cc58820e898d873b0f1581e077158e5c3..7af0c18b67eb809770ce762936bdec18d058b698 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -262,7 +262,7 @@ class IntelligentTerminal(App): self.current_agent: tuple[str, str] = self._get_initial_agent() # MCP 状态 self._mcp_mode: str = "normal" # "normal", "confirm", "parameter" - self._current_mcp_task_id: str = "" + self._current_mcp_conversation_id: str = "" # 创建日志实例 self.logger = get_logger(__name__) # 进度消息跟踪 @@ -302,6 +302,8 @@ class IntelligentTerminal(App): output_container.remove_children() # 清理进度消息跟踪 self._current_progress_lines.clear() + # 清理 MCP 会话 ID + self._current_mcp_conversation_id = "" def action_choose_agent(self) -> None: """选择智能体的动作""" @@ -378,7 +380,7 @@ class IntelligentTerminal(App): """初始化完成时设置焦点和绑定""" # 确保初始状态是正常模式 self._mcp_mode = "normal" - self._current_mcp_task_id = "" + self._current_mcp_conversation_id = "" # 清理任何可能的重复组件 try: @@ -497,40 +499,40 @@ class IntelligentTerminal(App): def handle_switch_to_mcp_confirm(self, message: SwitchToMCPConfirm) -> None: """处理切换到 MCP 确认界面的消息""" self._mcp_mode = "confirm" - self._current_mcp_task_id = message.event.get_task_id() + self._current_mcp_conversation_id = message.event.get_conversation_id() self._replace_input_with_mcp_widget(MCPConfirmWidget(message.event, widget_id="mcp-confirm")) @on(SwitchToMCPParameter) def handle_switch_to_mcp_parameter(self, message: SwitchToMCPParameter) -> None: """处理切换到 MCP 参数输入界面的消息""" self._mcp_mode = "parameter" - self._current_mcp_task_id = message.event.get_task_id() + self._current_mcp_conversation_id = message.event.get_conversation_id() self._replace_input_with_mcp_widget(MCPParameterWidget(message.event, widget_id="mcp-parameter")) @on(MCPConfirmResult) def handle_mcp_confirm_result(self, message: MCPConfirmResult) -> None: """处理 MCP 确认结果""" - # 检查是否是当前任务且未在处理中 - if message.task_id == self._current_mcp_task_id and not self.processing: + # 检查是否是当前会话且未在处理中 + if message.conversation_id == self._current_mcp_conversation_id and not self.processing: self.processing = True # 设置处理标志,防止重复处理 # 立即恢复正常输入界面 self._restore_normal_input() # 发送 MCP 响应并处理结果 - task = asyncio.create_task(self._send_mcp_response(message.task_id, params=message.confirmed)) + task = asyncio.create_task(self._send_mcp_response(message.conversation_id, params=message.confirmed)) self.background_tasks.add(task) task.add_done_callback(self._task_done_callback) @on(MCPParameterResult) def handle_mcp_parameter_result(self, message: MCPParameterResult) -> None: """处理 MCP 参数结果""" - # 检查是否是当前任务且未在处理中 - if message.task_id == self._current_mcp_task_id and not self.processing: + # 检查是否是当前会话且未在处理中 + if message.conversation_id == self._current_mcp_conversation_id and not self.processing: self.processing = True # 设置处理标志,防止重复处理 # 立即恢复正常输入界面 self._restore_normal_input() # 发送 MCP 响应并处理结果 params = message.params if message.params is not None else False - task = asyncio.create_task(self._send_mcp_response(message.task_id, params=params)) + task = asyncio.create_task(self._send_mcp_response(message.conversation_id, params=params)) self.background_tasks.add(task) task.add_done_callback(self._task_done_callback) @@ -1168,7 +1170,7 @@ class IntelligentTerminal(App): # 重置 MCP 状态 self._mcp_mode = "normal" - self._current_mcp_task_id = "" + self._current_mcp_conversation_id = "" # 切换回正常模式样式 input_container.remove_class("mcp-mode") @@ -1188,9 +1190,9 @@ class IntelligentTerminal(App): self.logger.exception("恢复正常输入组件失败") # 如果恢复失败,至少要重置状态 self._mcp_mode = "normal" - self._current_mcp_task_id = "" + self._current_mcp_conversation_id = "" - async def _send_mcp_response(self, task_id: str, *, params: bool | dict[str, Any]) -> None: + async def _send_mcp_response(self, conversation_id: str, *, params: bool | dict[str, Any]) -> None: """发送 MCP 响应并处理结果""" output_container: Container | None = None @@ -1202,7 +1204,7 @@ class IntelligentTerminal(App): llm_client = self.get_llm_client() if hasattr(llm_client, "send_mcp_response"): success = await self._handle_mcp_response_stream( - task_id, + conversation_id, params=params, output_container=output_container, llm_client=llm_client, @@ -1232,7 +1234,7 @@ class IntelligentTerminal(App): async def _handle_mcp_response_stream( self, - task_id: str, + conversation_id: str, *, params: bool | dict[str, Any], output_container: Container, @@ -1251,7 +1253,7 @@ class IntelligentTerminal(App): try: # 使用 asyncio.wait_for 包装整个流处理过程 async def _process_stream() -> bool: - async for content in llm_client.send_mcp_response(task_id, params=params): + async for content in llm_client.send_mcp_response(conversation_id, params=params): if not content.strip(): continue diff --git a/src/backend/factory.py b/src/backend/factory.py index e5f2dfc8864684a4f563c6315dba5859da8a4c77..7dfa80c766e2bf9e25a231ad64884fe77cf67ea2 100644 --- a/src/backend/factory.py +++ b/src/backend/factory.py @@ -43,6 +43,7 @@ class BackendFactory: return HermesChatClient( base_url=config_manager.get_eulerintelli_url(), auth_token=config_manager.get_eulerintelli_key(), + config_manager=config_manager, ) msg = f"不支持的后端类型: {backend}" raise ValueError(msg) diff --git a/src/backend/hermes/__init__.py b/src/backend/hermes/__init__.py index 4e2b2e33e22ac70c002fb4622d71c82b4eb3ab10..3e7ae0f1e0cfbbd9543f90c3d8e5cee9e7761d7b 100644 --- a/src/backend/hermes/__init__.py +++ b/src/backend/hermes/__init__.py @@ -2,7 +2,7 @@ from .client import HermesChatClient from .exceptions import HermesAPIError -from .models import HermesAgent, HermesApp, HermesChatRequest, HermesFeatures, HermesMessage +from .models import HermesAgent, HermesApp, HermesChatRequest, HermesMessage from .services.agent import HermesAgentManager from .services.conversation import HermesConversationManager from .services.http import HermesHttpManager @@ -17,7 +17,6 @@ __all__ = [ "HermesChatClient", "HermesChatRequest", "HermesConversationManager", - "HermesFeatures", "HermesHttpManager", "HermesMessage", "HermesModelManager", diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index 7672aecc93004e58d970f0addf77917781d0028f..e82bb0fab003c914670e95d61723b7e3fe339720 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -10,12 +10,12 @@ from urllib.parse import urljoin import httpx from backend.base import LLMClientBase -from i18n.manager import get_locale +from i18n.manager import _, get_locale from log.manager import get_logger, log_exception from .constants import HTTP_OK from .exceptions import HermesAPIError -from .models import HermesApp, HermesChatRequest, HermesFeatures +from .models import HermesApp, HermesChatRequest from .services import ( HermesAgentManager, HermesConversationManager, @@ -31,6 +31,7 @@ if TYPE_CHECKING: from backend.mcp_handler import MCPEventHandler from backend.models import ModelInfo + from config.manager import ConfigManager from .models import HermesAgent @@ -38,12 +39,20 @@ if TYPE_CHECKING: class HermesChatClient(LLMClientBase): """Hermes Chat API 客户端 - 重构版本""" - def __init__(self, base_url: str, auth_token: str = "") -> None: - """初始化 Hermes Chat API 客户端""" + def __init__(self, base_url: str, auth_token: str = "", config_manager: ConfigManager | None = None) -> None: + """ + 初始化 Hermes Chat API 客户端 + + Args: + base_url: API 基础 URL + auth_token: 认证令牌 + config_manager: 配置管理器(用于动态获取 llm_id) + + """ self.logger = get_logger(__name__) self.current_agent_id: str = "" # 当前选择的智能体 ID - self.current_task_id: str = "" # 当前正在运行的任务 ID + self.config_manager = config_manager # 配置管理器,用于动态获取 llm_id # HTTP 管理器 - 立即初始化 self.http_manager = HermesHttpManager(base_url, auth_token) @@ -219,9 +228,12 @@ class HermesChatClient(LLMClientBase): str: 流式响应的文本内容 Raises: - HermesAPIError: 当 API 调用失败时 + HermesAPIError: 当 API 调用失败时或 llm_id 未配置时 """ + # 验证 llm_id 是否已配置 + self._validate_llm_id() + # 如果有未完成的会话,先停止它 await self._stop() @@ -233,9 +245,12 @@ class HermesChatClient(LLMClientBase): start_time = time.time() try: - # 确保有会话 ID - conversation_id = await self.conversation_manager.ensure_conversation() - self.logger.info("使用会话ID: %s", conversation_id) + # 获取当前会话ID(可能为空) + conversation_id = self.conversation_manager.get_conversation_id() + if conversation_id: + self.logger.info("使用现有会话ID: %s", conversation_id) + else: + self.logger.info("没有会话ID,后端将自动创建新会话") # 创建聊天请求 app = HermesApp(self.current_agent_id) @@ -246,10 +261,10 @@ class HermesChatClient(LLMClientBase): request = HermesChatRequest( app=app, - conversation_id=conversation_id, question=prompt, - features=HermesFeatures(), + conversation_id=conversation_id, language=language, + llm_id=self._get_llm_id(), ) # 直接传递异常,不在这里处理 @@ -288,12 +303,12 @@ class HermesChatClient(LLMClientBase): """ return await self.agent_manager.get_available_agents() - async def send_mcp_response(self, task_id: str, *, params: bool | dict) -> AsyncGenerator[str, None]: + async def send_mcp_response(self, conversation_id: str, *, params: bool | dict) -> AsyncGenerator[str, None]: """ 发送 MCP 响应并获取流式回复 Args: - task_id: 任务ID + conversation_id: 会话ID params: 响应参数(bool 表示确认/取消,dict 表示参数补全) Yields: @@ -304,34 +319,28 @@ class HermesChatClient(LLMClientBase): """ # 不在 MCP 响应时重置状态跟踪,保持去重机制有效 - self.logger.info("发送 MCP 响应 - 任务ID: %s", task_id) + self.logger.info("发送 MCP 响应 - 会话ID: %s, 参数类型: %s", conversation_id, type(params).__name__) start_time = time.time() try: # 构建 MCP 响应请求 - client = await self.http_manager.get_client() - chat_url = urljoin(self.http_manager.base_url, "/api/chat") - headers = self.http_manager.build_headers() + app = HermesApp(self.current_agent_id, params=params) - request_data = { - "taskId": task_id, - "params": params, - } + current_locale = get_locale() + language = "zh" if current_locale.startswith("zh") else "en" - self.logger.info("准备发送 MCP 响应请求 - URL: %s, 任务ID: %s", chat_url, task_id) - self.logger.debug("请求头: %s", headers) - self.logger.debug("请求内容: %s", request_data) + request = HermesChatRequest( + app=app, + question="", + conversation_id=conversation_id, + language=language, + llm_id=self._get_llm_id(), + ) - async with client.stream( - "POST", - chat_url, - json=request_data, - headers=headers, - ) as response: - self.logger.info("收到 MCP 响应 - 状态码: %d", response.status_code) - await self._validate_chat_response(response) - async for text in self._process_stream_events(response): - yield text + self.logger.debug("MCP 响应请求数据: %s", request.to_dict()) + + async for text in self._chat_stream(request): + yield text duration = time.time() - start_time self.logger.info("MCP 响应请求完成 - 耗时: %.3fs", duration) @@ -361,6 +370,46 @@ class HermesChatClient(LLMClientBase): log_exception(self.logger, "关闭 Hermes 客户端失败", e) raise + def _get_llm_id(self) -> str: + """ + 从配置管理器获取当前的 llm_id + + Returns: + str: 当前配置的 llm_id,如果未配置则返回空字符串 + + """ + if self.config_manager is None: + return "" + return self.config_manager.get_llm_chat_model() + + def _validate_llm_id(self) -> None: + """ + 验证 llm_id 是否已配置 + + Raises: + HermesAPIError: 当 llm_id 未配置时 + + """ + llm_id = self._get_llm_id() + if not llm_id: + main_message = _("未配置 Chat 模型") + hint_prefix = _("配置步骤") + step1 = _("按 Ctrl+S 打开设置") + step2 = _("确认后端为 openEuler Intelligence") + step3 = _('点击 "更改用户设置" 按钮') + step4 = _('切换到 "大模型设置" 标签页') + step5 = _("使用 ↑↓ 键选择模型,空格激活,回车保存") + error_message = ( + f"{main_message}\n\n" + f"{hint_prefix}:\n" + f" 1. {step1}\n" + f" 2. {step2}\n" + f" 3. {step3}\n" + f" 4. {step4}\n" + f" 5. {step5}" + ) + raise HermesAPIError(400, error_message) + async def _chat_stream( self, request: HermesChatRequest, @@ -429,13 +478,12 @@ class HermesChatClient(LLMClientBase): event_count += 1 self.logger.info("解析到事件 #%d - 类型: %s", event_count, event.event_type) - # 处理任务ID - self._handle_task_id(event) + # 处理会话ID + self._handle_conversation_id(event) # 处理特殊事件类型 should_break, break_message = self.stream_processor.handle_special_events(event) if should_break: - self._cleanup_task_id("回答结束") if break_message: has_error_message = True yield break_message @@ -455,7 +503,6 @@ class HermesChatClient(LLMClientBase): except Exception: self.logger.exception("处理流式响应事件时出错") - self._cleanup_task_id("发生异常") raise # 只有在没有内容且没有错误消息的情况下才显示无内容消息 @@ -474,18 +521,12 @@ class HermesChatClient(LLMClientBase): self.logger.warning("无法解析 SSE 事件") return event - def _handle_task_id(self, event: HermesStreamEvent) -> None: - """处理事件中的任务ID""" - task_id = event.get_task_id() - if task_id and not self.current_task_id: - self.current_task_id = task_id - self.logger.debug("设置当前任务ID: %s", task_id) - - def _cleanup_task_id(self, context: str) -> None: - """清理任务ID""" - if self.current_task_id: - self.logger.debug("%s清理任务ID: %s", context, self.current_task_id) - self.current_task_id = "" + def _handle_conversation_id(self, event: HermesStreamEvent) -> None: + """处理事件中的会话ID""" + conversation_id = event.get_conversation_id() + if conversation_id: + # 通过 conversation_manager 存储会话ID + self.conversation_manager.set_conversation_id(conversation_id) async def _handle_event_content(self, event: HermesStreamEvent) -> AsyncGenerator[str, None]: """处理单个事件的内容""" @@ -514,9 +555,7 @@ class HermesChatClient(LLMClientBase): async def _stop(self) -> None: """停止当前会话""" if self._conversation_manager is not None: - await self._conversation_manager.stop_conversation(self.current_task_id) - # 停止后清理任务ID - self._cleanup_task_id("手动停止") + await self._conversation_manager.stop_conversation() async def __aenter__(self) -> Self: """异步上下文管理器入口""" diff --git a/src/backend/hermes/models.py b/src/backend/hermes/models.py index c7b891915f52cdd78c481ec941f0e90643117ca1..0a9dc80bbbe2f3d2be295ad2c8f88326ed0dadb2 100644 --- a/src/backend/hermes/models.py +++ b/src/backend/hermes/models.py @@ -58,64 +58,92 @@ class HermesMessage: return {"role": self.role, "content": self.content} -class HermesFeatures: - """Hermes 功能特性配置""" - - def __init__(self, max_tokens: int = 8192, context_num: int = 10) -> None: - """初始化功能特性配置""" - self.max_tokens = max_tokens - self.context_num = context_num - - def to_dict(self) -> dict[str, int]: - """转换为字典格式""" - return { - "max_tokens": self.max_tokens, - "context_num": self.context_num, - } - - class HermesApp: """Hermes 应用配置""" - def __init__(self, app_id: str, flow_id: str = "") -> None: - """初始化应用配置""" + def __init__( + self, + app_id: str, + flow_id: str = "", + *, + params: dict[str, Any] | bool | None = None, + ) -> None: + """ + 初始化应用配置 + + Args: + app_id: 应用ID + flow_id: 流ID + params: MCP 响应参数(bool 表示确认/取消,dict 表示参数补全内容) + + """ self.app_id = app_id self.flow_id = flow_id + self.params = params def to_dict(self) -> dict[str, Any]: """转换为字典格式""" - return { + app_dict: dict[str, Any] = { "appId": self.app_id, - "auth": {}, "flowId": self.flow_id, - "params": {}, } + # 如果有 MCP 响应参数,直接使用 params 的值 + if self.params is not None: + app_dict["params"] = self.params + else: + # 没有 params 时,添加空的 params 字段(保持向后兼容) + app_dict["params"] = {} + + return app_dict + class HermesChatRequest: """Hermes Chat 请求类""" - def __init__( + def __init__( # noqa: PLR0913 self, app: HermesApp, - conversation_id: str, question: str, - features: HermesFeatures | None = None, - language: str = "zh_cn", + conversation_id: str = "", + language: str = "zh", + llm_id: str = "", + kb_ids: list[str] | None = None, ) -> None: - """初始化 Hermes Chat 请求""" + """ + 初始化 Hermes Chat 请求 + + Args: + app: 应用配置 + question: 用户问题 + conversation_id: 会话ID + language: 语言 + llm_id: 大模型ID + kb_ids: 知识库ID列表 + + """ self.app = app self.conversation_id = conversation_id self.question = question - self.features = features or HermesFeatures() self.language = language + self.llm_id = llm_id + self.kb_ids = kb_ids or [] def to_dict(self) -> dict[str, Any]: """转换为请求字典格式""" - return { - "app": self.app.to_dict(), - "conversationId": self.conversation_id, - "features": self.features.to_dict(), - "language": self.language, + request_dict: dict[str, Any] = { "question": self.question, + "language": self.language, + "llmId": self.llm_id, } + + if self.app and self.app.app_id: + request_dict["app"] = self.app.to_dict() + + if self.conversation_id: + request_dict["conversationId"] = self.conversation_id + + if self.kb_ids: + request_dict["kbIds"] = self.kb_ids + + return request_dict diff --git a/src/backend/hermes/services/conversation.py b/src/backend/hermes/services/conversation.py index 86c2aecb48a69a85bfb48dc4773d8ef5c326f1d8..048071a2af5443b72eb378b26477e1c23dd0277f 100644 --- a/src/backend/hermes/services/conversation.py +++ b/src/backend/hermes/services/conversation.py @@ -27,59 +27,37 @@ class HermesConversationManager: self._conversation_id: str | None = None def reset_conversation(self) -> None: - """重置会话,下次聊天时会创建新的会话""" + """重置会话,清除当前会话ID,下次聊天时后端会自动创建新的会话""" + if self._conversation_id: + self.logger.info("重置会话 - 清除会话ID: %s", self._conversation_id) self._conversation_id = None - async def ensure_conversation(self, llm_id: str = "") -> str: + def get_conversation_id(self) -> str: """ - 确保有可用的会话 ID,智能重用空对话或创建新会话 - - 优先使用已存在的空对话,如果没有空对话或获取失败,则创建新对话。 - 这样可以避免产生过多的空对话记录。 - - Args: - llm_id: 指定的 LLM ID + 获取当前会话ID Returns: - str: 可用的会话 ID + str: 当前会话ID,如果没有会话则返回空字符串 """ - if self._conversation_id is None: - try: - # 先尝试获取现有对话列表 - conversation_list = await self._get_conversation_list() - - # 如果有对话,检查最新的对话是否为空 - if conversation_list: - latest_conversation_id = conversation_list[0] # 已经按时间排序,第一个是最新的 - try: - # 检查最新对话是否为空 - if await self._is_conversation_empty(latest_conversation_id): - self.logger.info("重用空对话 - ID: %s", latest_conversation_id) - self._conversation_id = latest_conversation_id - return self._conversation_id - except HermesAPIError: - # 如果检查对话记录失败,继续创建新对话 - self.logger.warning("检查对话记录失败,将创建新对话") - - # 如果没有对话或最新对话不为空,创建新对话 - self._conversation_id = await self._create_conversation(llm_id) - - except HermesAPIError: - # 如果获取对话列表失败,直接创建新对话 - self.logger.warning("获取对话列表失败,将创建新对话") - self._conversation_id = await self._create_conversation(llm_id) - - return self._conversation_id - - async def stop_conversation(self, task_id: str = "") -> None: + return self._conversation_id or "" + + def set_conversation_id(self, conversation_id: str) -> None: """ - 停止当前会话 + 设置会话ID + + 当从后端 SSE 流中接收到会话ID时调用此方法存储。 Args: - task_id: 可选的任务ID,如果提供且非空,则作为查询参数发送 + conversation_id: 从后端接收到的会话ID """ + if conversation_id and self._conversation_id != conversation_id: + self.logger.info("更新会话ID: %s -> %s", self._conversation_id or "空", conversation_id) + self._conversation_id = conversation_id + + async def stop_conversation(self) -> None: + """停止当前会话""" if self.http_manager.client is None or self.http_manager.client.is_closed: return @@ -87,12 +65,7 @@ class HermesConversationManager: stop_url = urljoin(self.http_manager.base_url, "/api/stop") headers = self.http_manager.build_headers() - # 构建请求参数 - params = {} - if task_id: - params["taskId"] = task_id - - response = await self.http_manager.client.post(stop_url, headers=headers, params=params) + response = await self.http_manager.client.post(stop_url, headers=headers) if response.status_code != HTTP_OK: error_text = await response.aread() diff --git a/src/backend/hermes/stream.py b/src/backend/hermes/stream.py index 020c089e8799759fff5d56a0f663ceab9ef48caa..a2c9d7e0dadbe49cd0283eeb871aaf6cf08a9f71 100644 --- a/src/backend/hermes/stream.py +++ b/src/backend/hermes/stream.py @@ -81,10 +81,6 @@ class HermesStreamEvent: """获取会话ID""" return self.data.get("conversationId", "") - def get_task_id(self) -> str: - """获取任务ID""" - return self.data.get("taskId", "") - def get_content(self) -> dict[str, Any]: """获取内容部分""" return self.data.get("content", {}) @@ -93,16 +89,16 @@ class HermesStreamEvent: """判断是否为 MCP 步骤相关事件""" return self.event_type in MCPEventTypes.ALL_STEP_EVENTS - def is_flow_event(self) -> bool: + def is_executor_event(self) -> bool: """判断是否为流相关事件""" - flow_events = { - "flow.start", - "flow.stop", - "flow.failed", - "flow.success", - "flow.cancel", + executor_events = { + "executor.start", + "executor.stop", + "executor.failed", + "executor.success", + "executor.cancel", } - return self.event_type in flow_events + return self.event_type in executor_events class HermesStreamProcessor: @@ -151,8 +147,8 @@ class HermesStreamProcessor: def format_mcp_status(self, event: HermesStreamEvent) -> str | None: """格式化 MCP 状态信息为可读文本""" - # 忽略 flow 事件 - if event.is_flow_event(): + # 忽略 executor 事件 + if event.is_executor_event(): return None # 只处理 step 事件 diff --git a/tests/backend/test_llm_id_validation.py b/tests/backend/test_llm_id_validation.py new file mode 100644 index 0000000000000000000000000000000000000000..adb850f81bae4b284cd7c89504dbb8c1ed8bfc06 --- /dev/null +++ b/tests/backend/test_llm_id_validation.py @@ -0,0 +1,57 @@ +"""测试 HermesChatClient 的 llm_id 验证功能""" + +import asyncio +from unittest.mock import Mock + +from backend.hermes.client import HermesChatClient +from backend.hermes.exceptions import HermesAPIError + + +async def test_llm_id_validation(): + """测试 llm_id 验证""" + print("=" * 60) + print("测试 llm_id 验证功能") + print("=" * 60 + "\n") + + # 测试 1: 没有 llm_id 应该抛出异常 + print("测试 1: 创建没有 llm_id 的客户端(模拟未配置)") + # 创建一个模拟的 ConfigManager,返回空的 llm_id + mock_config_empty = Mock() + mock_config_empty.get_llm_chat_model.return_value = "" + + client = HermesChatClient( + base_url="http://localhost:8000", + config_manager=mock_config_empty, + ) + print(f" 配置的 llm_id: '{client._get_llm_id()}'") + + try: + # 尝试生成响应,应该抛出异常 + print(" 尝试生成响应...") + async for _ in client.get_llm_response("测试"): + pass + print(" ✗ 应该抛出异常但没有") + except HermesAPIError as e: + print(f" ✓ 成功捕获异常 (状态码: {e.status_code})") + print(f"\n错误消息:\n{e.message}\n") + + # 测试 2: 有 llm_id 的客户端不应该在验证阶段抛出异常 + print("测试 2: 创建有 llm_id 的客户端(模拟已配置)") + # 创建一个模拟的 ConfigManager,返回有效的 llm_id + mock_config_with_llm = Mock() + mock_config_with_llm.get_llm_chat_model.return_value = "test-model-id" + + client_with_llm = HermesChatClient( + base_url="http://localhost:8000", + config_manager=mock_config_with_llm, + ) + print(f" 配置的 llm_id: '{client_with_llm._get_llm_id()}'") + print(" ✓ llm_id 验证应该通过(实际请求会因为连接问题失败)\n") + + print("=" * 60) + print("测试完成") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(test_llm_id_validation())