Files
2026-01-17 07:37:59 +00:00

283 lines
9.1 KiB
Python

"""
LiteLLM Agent 核心模块
基于LiteLLM框架的Agent实现,支持A2A协议
"""
import asyncio
import json
import uuid
from typing import AsyncGenerator, Optional, Dict, Any, List
from dataclasses import dataclass, field
from datetime import datetime
import httpx
import structlog
from config import LiteLLMConfig, AgentConfig, get_config
# 配置日志
logger = structlog.get_logger()
@dataclass
class Message:
"""消息数据结构"""
role: str # user, assistant, system
content: str
message_id: str = field(default_factory=lambda: uuid.uuid4().hex)
timestamp: datetime = field(default_factory=datetime.now)
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class Conversation:
"""对话上下文"""
conversation_id: str = field(default_factory=lambda: uuid.uuid4().hex)
messages: List[Message] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.now)
def add_message(self, role: str, content: str) -> Message:
"""添加消息到对话"""
msg = Message(role=role, content=content)
self.messages.append(msg)
return msg
def to_openai_format(self) -> List[Dict[str, str]]:
"""转换为OpenAI格式的消息列表"""
return [{"role": m.role, "content": m.content} for m in self.messages]
class LiteLLMAgent:
"""
基于LiteLLM的Agent实现
支持功能:
- 多轮对话
- 流式响应
- 工具调用
- A2A协议兼容
"""
def __init__(
self,
api_key: Optional[str] = None,
model: Optional[str] = None,
litellm_config: Optional[LiteLLMConfig] = None,
agent_config: Optional[AgentConfig] = None
):
"""
初始化Agent
Args:
api_key: LiteLLM API密钥(可选,优先使用,否则从环境变量获取)
model: 模型名称(可选,优先使用,否则从环境变量获取)
litellm_config: LiteLLM配置对象
agent_config: Agent配置对象
"""
if litellm_config:
self.llm_config = litellm_config
else:
self.llm_config = LiteLLMConfig(api_key=api_key, model=model)
if agent_config:
self.agent_config = agent_config
else:
self.agent_config = AgentConfig()
# 验证配置
self.llm_config.validate()
# HTTP客户端
self._client: Optional[httpx.AsyncClient] = None
# 对话管理
self.conversations: Dict[str, Conversation] = {}
# 工具注册
self.tools: Dict[str, callable] = {}
logger.info(
"Agent初始化完成",
agent_name=self.agent_config.name,
model=self.llm_config.model,
base_url=self.llm_config.base_url
)
async def _get_client(self) -> httpx.AsyncClient:
"""获取或创建HTTP客户端"""
if self._client is None or self._client.is_closed:
self._client = httpx.AsyncClient(
timeout=httpx.Timeout(self.llm_config.timeout),
headers={
"Authorization": f"Bearer {self.llm_config.api_key}",
"Content-Type": "application/json"
}
)
return self._client
async def close(self):
"""关闭资源"""
if self._client and not self._client.is_closed:
await self._client.aclose()
def register_tool(self, name: str, func: callable, description: str = ""):
"""注册工具函数"""
self.tools[name] = {
"function": func,
"description": description
}
logger.info(f"注册工具: {name}")
def get_or_create_conversation(self, conversation_id: Optional[str] = None) -> Conversation:
"""获取或创建对话"""
if conversation_id and conversation_id in self.conversations:
return self.conversations[conversation_id]
conv = Conversation(conversation_id=conversation_id or uuid.uuid4().hex)
# 添加系统提示
conv.add_message("system", self.agent_config.system_prompt)
self.conversations[conv.conversation_id] = conv
return conv
async def chat(
self,
message: str,
conversation_id: Optional[str] = None,
stream: bool = False
) -> str | AsyncGenerator[str, None]:
"""
发送消息并获取回复
Args:
message: 用户消息
conversation_id: 对话ID(用于多轮对话)
stream: 是否流式响应
Returns:
如果stream=False,返回完整回复字符串
如果stream=True,返回异步生成器
"""
# 获取对话上下文
conversation = self.get_or_create_conversation(conversation_id)
conversation.add_message("user", message)
if stream:
return self._stream_chat(conversation)
else:
return await self._simple_chat(conversation)
async def _simple_chat(self, conversation: Conversation) -> str:
"""非流式对话"""
client = await self._get_client()
request_body = {
"model": self.llm_config.model,
"messages": conversation.to_openai_format(),
"temperature": self.llm_config.temperature,
"max_tokens": self.llm_config.max_tokens
}
logger.debug("发送请求", endpoint=self.llm_config.chat_endpoint)
try:
response = await client.post(
self.llm_config.chat_endpoint,
json=request_body
)
response.raise_for_status()
result = response.json()
assistant_message = result["choices"][0]["message"]["content"]
# 保存助手回复到对话
conversation.add_message("assistant", assistant_message)
logger.info("收到回复", length=len(assistant_message))
return assistant_message
except httpx.HTTPStatusError as e:
logger.error("HTTP错误", status_code=e.response.status_code, detail=e.response.text)
raise
except Exception as e:
logger.error("请求失败", error=str(e))
raise
async def _stream_chat(self, conversation: Conversation) -> AsyncGenerator[str, None]:
"""流式对话"""
client = await self._get_client()
request_body = {
"model": self.llm_config.model,
"messages": conversation.to_openai_format(),
"temperature": self.llm_config.temperature,
"max_tokens": self.llm_config.max_tokens,
"stream": True
}
full_response = ""
try:
async with client.stream(
"POST",
self.llm_config.chat_endpoint,
json=request_body
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if line.startswith("data: "):
data = line[6:]
if data == "[DONE]":
break
try:
chunk = json.loads(data)
delta = chunk.get("choices", [{}])[0].get("delta", {})
content = delta.get("content", "")
if content:
full_response += content
yield content
except json.JSONDecodeError:
continue
# 保存完整回复到对话
conversation.add_message("assistant", full_response)
except Exception as e:
logger.error("流式请求失败", error=str(e))
raise
async def invoke_tool(self, tool_name: str, **kwargs) -> Any:
"""调用注册的工具"""
if tool_name not in self.tools:
raise ValueError(f"未找到工具: {tool_name}")
tool = self.tools[tool_name]
func = tool["function"]
logger.info(f"调用工具: {tool_name}", kwargs=kwargs)
if asyncio.iscoroutinefunction(func):
return await func(**kwargs)
else:
return func(**kwargs)
# 示例工具函数
def tool_get_current_time() -> str:
"""获取当前时间"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def tool_calculate(expression: str) -> str:
"""计算数学表达式"""
try:
# 安全的数学计算
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return "错误: 不支持的字符"
result = eval(expression)
return str(result)
except Exception as e:
return f"计算错误: {str(e)}"