import inspect import time from abc import ABC, abstractmethod from contextlib import contextmanager from typing import TypeVar from langgraph.errors import GraphInterrupt from loguru import logger from app.config import Settings, settings from app.processor.progress import node_progress T = TypeVar("T") class BaseNode(ABC): name: str = "base_node" def __init__(self, config: Settings | None = None): self.config = config if config is not None else settings self.logger = logger.bind(node=self.name) @contextmanager def _instrument(self, state): """统一记录日志、耗时和进度事件,异常保持原类型向上抛出""" start_time = time.perf_counter() self.logger.info("节点开始") try: with node_progress(self.name, state): yield except GraphInterrupt: raise except Exception: self.logger.error("节点执行失败") raise else: self.logger.info("节点完成") finally: self.logger.info(f"节点耗时 {time.perf_counter() - start_time:.3f} 秒") def __call__(self, state: T) -> T: # 异步节点直接调用时返回协程,日志与进度在 _acall 内等待完成后记录 if inspect.iscoroutinefunction(self.process): return self._acall(state) with self._instrument(state): return self.process(state) async def _acall(self, state: T) -> T: """异步节点入口,由图根据 process 是否为协程选择""" with self._instrument(state): return await self.process(state) @abstractmethod def process(self, state: T) -> T: """子类实现节点核心逻辑,返回状态更新""" def fail_on_invalid_output(self, state: T, error: ValueError, reason: str) -> T: """模型输出未通过校验时收尾为失败结果,原因交给失败收尾由模型向用户说明""" self.logger.warning(f"{reason}: {error}") return {**state, "status": "failed", "failure_reason": f"{reason}:{error}"} def log_step(self, step_name: str, message: str = ""): """记录步骤日志。""" log_msg = f"[{step_name}]" if message: log_msg += f" {message}" self.logger.info(log_msg)