diff --git a/agentrun/__init__.py b/agentrun/__init__.py index 1c94f5d..bf93517 100644 --- a/agentrun/__init__.py +++ b/agentrun/__init__.py @@ -16,6 +16,8 @@ - Integration: 框架集成 / Framework integration """ +from typing import TYPE_CHECKING + __version__ = "0.0.8" # Agent Runtime @@ -87,27 +89,50 @@ SandboxClient, Template, ) -# Server -from agentrun.server import ( - AgentRequest, - AgentResult, - AgentRunServer, - AsyncInvokeAgentHandler, - InvokeAgentHandler, - Message, - MessageRole, - OpenAIProtocolHandler, - ProtocolHandler, - SyncInvokeAgentHandler, -) # ToolSet from agentrun.toolset import ToolSet, ToolSetClient +from agentrun.utils.config import Config from agentrun.utils.exception import ( ResourceAlreadyExistError, ResourceNotExistError, ) from agentrun.utils.model import Status +# Server - 延迟导入以避免可选依赖问题 +# Type hints for IDE and type checkers +if TYPE_CHECKING: + from agentrun.server import ( + AgentEvent, + AgentEventItem, + AgentRequest, + AgentResult, + AgentResultItem, + AgentReturnType, + AgentRunServer, + AguiEventNormalizer, + AGUIProtocolConfig, + AGUIProtocolHandler, + AsyncAgentEventGenerator, + AsyncAgentResultGenerator, + AsyncInvokeAgentHandler, + BaseProtocolHandler, + EventType, + InvokeAgentHandler, + MergeOptions, + Message, + MessageRole, + OpenAIProtocolConfig, + OpenAIProtocolHandler, + ProtocolConfig, + ProtocolHandler, + ServerConfig, + SyncAgentEventGenerator, + SyncAgentResultGenerator, + SyncInvokeAgentHandler, + Tool, + ToolCall, + ) + __all__ = [ ######## Agent Runtime ######## # base @@ -185,22 +210,116 @@ ######## ToolSet ######## "ToolSetClient", "ToolSet", - ######## Server ######## + ######## Server (延迟加载) ######## + # Server "AgentRunServer", + # Config + "ServerConfig", + "ProtocolConfig", + "OpenAIProtocolConfig", + "AGUIProtocolConfig", + # Request/Response Models "AgentRequest", - "AgentResponse", + "AgentEvent", "AgentResult", - "AgentStreamResponse", + "Message", + "MessageRole", + "Tool", + "ToolCall", + # Event Types + "EventType", + # Type Aliases + "AgentEventItem", + "AgentResultItem", + "AgentReturnType", + "SyncAgentEventGenerator", + "SyncAgentResultGenerator", + "AsyncAgentEventGenerator", + "AsyncAgentResultGenerator", "InvokeAgentHandler", "AsyncInvokeAgentHandler", "SyncInvokeAgentHandler", - "Message", - "MessageRole", + # Protocol Base "ProtocolHandler", + "BaseProtocolHandler", + # Protocol - OpenAI "OpenAIProtocolHandler", - "AgentStreamIterator", + # Protocol - AG-UI + "AGUIProtocolHandler", + # Event Normalizer + "AguiEventNormalizer", + # Helpers + "MergeOptions", ######## Others ######## - "Status", "ResourceNotExistError", "ResourceAlreadyExistError", + "Config", ] + +# Server 模块的所有导出 +_SERVER_EXPORTS = { + "AgentRunServer", + "ServerConfig", + "ProtocolConfig", + "OpenAIProtocolConfig", + "AGUIProtocolConfig", + "AgentRequest", + "AgentEvent", + "AgentResult", + "Message", + "MessageRole", + "Tool", + "ToolCall", + "EventType", + "AgentEventItem", + "AgentResultItem", + "AgentReturnType", + "SyncAgentEventGenerator", + "SyncAgentResultGenerator", + "AsyncAgentEventGenerator", + "AsyncAgentResultGenerator", + "InvokeAgentHandler", + "AsyncInvokeAgentHandler", + "SyncInvokeAgentHandler", + "ProtocolHandler", + "BaseProtocolHandler", + "OpenAIProtocolHandler", + "AGUIProtocolHandler", + "AguiEventNormalizer", + "MergeOptions", +} + +# 可选依赖包映射:安装命令 -> 导入错误的包名列表 +# Optional dependency mapping: installation command -> list of import error package names +# 将使用相同安装命令的包合并到一起 / Group packages with the same installation command +_OPTIONAL_PACKAGES = { + "agentrun-sdk[server]": ["fastapi", "uvicorn", "ag_ui"], +} + + +def __getattr__(name: str): + """延迟加载 server 模块的导出,避免可选依赖导致导入失败 + + 当用户访问 server 相关的类时,才尝试导入 server 模块。 + 如果 server 可选依赖未安装,会抛出清晰的错误提示。 + """ + if name in _SERVER_EXPORTS: + try: + from agentrun import server + + return getattr(server, name) + except ImportError as e: + # 检查是否是缺少可选依赖导致的错误 + error_str = str(e) + for install_cmd, package_names in _OPTIONAL_PACKAGES.items(): + for package_name in package_names: + if package_name in error_str: + raise ImportError( + f"'{name}' requires the 'server' optional dependencies. " + f"Install with: pip install {install_cmd}\n" + f"Original error: {e}" + ) from e + # 其他导入错误继续抛出 + raise + + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/agentrun/credential/model.py b/agentrun/credential/model.py index 8413127..63e8097 100644 --- a/agentrun/credential/model.py +++ b/agentrun/credential/model.py @@ -134,7 +134,7 @@ def outbound_tool_ak_sk( cls, provider: str, access_key_id: str, - access_key_secred: str, + access_key_secret: str, account_id: str, ): """配置访问第三方工具的 ak/sk 凭证""" @@ -148,7 +148,7 @@ def outbound_tool_ak_sk( "accountId": account_id, }, }, - credential_secret=access_key_secred, + credential_secret=access_key_secret, ) @classmethod diff --git a/agentrun/model/model.py b/agentrun/model/model.py index 73a5e2d..cc6157d 100644 --- a/agentrun/model/model.py +++ b/agentrun/model/model.py @@ -131,6 +131,7 @@ class ProxyConfigTokenRateLimiter(BaseModel): class ProxyConfigAIGuardrailConfig(BaseModel): """AI 防护配置""" + check_request: Optional[bool] = None check_response: Optional[bool] = None diff --git a/agentrun/server/agui_protocol.py b/agentrun/server/agui_protocol.py index 2d08810..164dc66 100644 --- a/agentrun/server/agui_protocol.py +++ b/agentrun/server/agui_protocol.py @@ -19,36 +19,12 @@ ) import uuid -from ag_ui.core import AssistantMessage -from ag_ui.core import CustomEvent as AguiCustomEvent -from ag_ui.core import EventType as AguiEventType -from ag_ui.core import Message as AguiMessage -from ag_ui.core import MessagesSnapshotEvent -from ag_ui.core import RawEvent as AguiRawEvent -from ag_ui.core import ( - RunErrorEvent, - RunFinishedEvent, - RunStartedEvent, - StateDeltaEvent, - StateSnapshotEvent, - StepFinishedEvent, - StepStartedEvent, - SystemMessage, - TextMessageContentEvent, - TextMessageEndEvent, - TextMessageStartEvent, -) -from ag_ui.core import Tool as AguiTool -from ag_ui.core import ToolCall as AguiToolCall -from ag_ui.core import ( - ToolCallArgsEvent, - ToolCallEndEvent, - ToolCallResultEvent, - ToolCallStartEvent, -) -from ag_ui.core import ToolMessage as AguiToolMessage -from ag_ui.core import UserMessage -from ag_ui.encoder import EventEncoder +if TYPE_CHECKING: + from ag_ui.core import ( + Message as AguiMessage, + ) + from ag_ui.encoder import EventEncoder + from fastapi import APIRouter, Request from fastapi.responses import StreamingResponse import pydash @@ -101,8 +77,10 @@ class StreamStateMachine: run_errored: bool = False def end_all_tools( - self, encoder: EventEncoder, exclude: Optional[str] = None + self, encoder: "EventEncoder", exclude: Optional[str] = None ) -> Iterator[str]: + from ag_ui.core import ToolCallEndEvent + for tool_id, state in self.tool_call_states.items(): if exclude and tool_id == exclude: continue @@ -110,7 +88,9 @@ def end_all_tools( yield encoder.encode(ToolCallEndEvent(tool_call_id=tool_id)) state.ended = True - def ensure_text_started(self, encoder: EventEncoder) -> Iterator[str]: + def ensure_text_started(self, encoder: "EventEncoder") -> Iterator[str]: + from ag_ui.core import TextMessageStartEvent + if not self.text.started or self.text.ended: if self.text.ended: self.text = TextState() @@ -123,7 +103,9 @@ def ensure_text_started(self, encoder: EventEncoder) -> Iterator[str]: self.text.started = True self.text.ended = False - def end_text_if_open(self, encoder: EventEncoder) -> Iterator[str]: + def end_text_if_open(self, encoder: "EventEncoder") -> Iterator[str]: + from ag_ui.core import TextMessageEndEvent + if self.text.started and not self.text.ended: yield encoder.encode( TextMessageEndEvent(message_id=self.text.message_id) @@ -168,6 +150,8 @@ class AGUIProtocolHandler(BaseProtocolHandler): name = "ag-ui" def __init__(self, config: Optional[ServerConfig] = None): + from ag_ui.encoder import EventEncoder + self._config = config.agui if config else None self._encoder = EventEncoder() @@ -363,6 +347,8 @@ async def _format_stream( Yields: SSE 格式的字符串 """ + from ag_ui.core import RunFinishedEvent, RunStartedEvent + state = StreamStateMachine() # 发送 RUN_STARTED @@ -420,6 +406,18 @@ def _process_event_with_boundaries( """处理事件并注入边界事件""" import json + from ag_ui.core import CustomEvent as AguiCustomEvent + from ag_ui.core import ( + RunErrorEvent, + StateDeltaEvent, + StateSnapshotEvent, + TextMessageContentEvent, + ToolCallArgsEvent, + ToolCallEndEvent, + ToolCallResultEvent, + ToolCallStartEvent, + ) + # RAW 事件直接透传 if event.event == EventType.RAW: raw_data = event.data.get("raw", "") @@ -703,7 +701,7 @@ def _process_event_with_boundaries( def _convert_messages_for_snapshot( self, messages: List[Dict[str, Any]] - ) -> List[AguiMessage]: + ) -> List["AguiMessage"]: """将消息列表转换为 ag-ui-protocol 格式 Args: @@ -712,6 +710,10 @@ def _convert_messages_for_snapshot( Returns: ag-ui-protocol 消息列表 """ + from ag_ui.core import AssistantMessage, SystemMessage + from ag_ui.core import ToolMessage as AguiToolMessage + from ag_ui.core import UserMessage + result = [] for msg in messages: if not isinstance(msg, dict): @@ -779,6 +781,8 @@ async def _error_stream(self, message: str) -> AsyncIterator[str]: Yields: SSE 格式的错误事件 """ + from ag_ui.core import RunErrorEvent, RunStartedEvent + thread_id = str(uuid.uuid4()) run_id = str(uuid.uuid4())