Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
"""
|
||||
LLM 提供商基类
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMMessage:
|
||||
"""LLM 消息
|
||||
|
||||
支持多模态内容格式:
|
||||
- 纯文本: content 为字符串
|
||||
- 多模态: content 为列表 [{"type": "text", "text": "..."}, {"type": "image_url", "image_url": {"url": "..."}}]
|
||||
"""
|
||||
role: str # system, user, assistant, tool
|
||||
content: Any # str 或 List[Dict] (多模态内容)
|
||||
name: Optional[str] = None
|
||||
# Function Calling 相关
|
||||
tool_calls: Optional[List[Dict[str, Any]]] = None # assistant 消息中的工具调用
|
||||
tool_call_id: Optional[str] = None # tool 消息中的工具调用 ID
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
result = {'role': self.role, 'content': self.content}
|
||||
if self.name:
|
||||
result['name'] = self.name
|
||||
if self.tool_calls:
|
||||
result['tool_calls'] = self.tool_calls
|
||||
if self.tool_call_id:
|
||||
result['tool_call_id'] = self.tool_call_id
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def create_multimodal_content(text: str, attachments: List[Dict] = None) -> Any:
|
||||
"""
|
||||
创建多模态内容
|
||||
|
||||
Args:
|
||||
text: 文本内容
|
||||
attachments: 附件列表 [{"type": "image/file", "url": "...", "mime_type": "...", "base64": "..."}]
|
||||
|
||||
Returns:
|
||||
str 或 List[Dict] 格式的内容
|
||||
|
||||
Note:
|
||||
对于图片类型,优先使用 base64 格式(因为 LLM 无法访问内部 URL)
|
||||
base64 格式: data:<mime_type>;base64,<base64_content>
|
||||
"""
|
||||
if not attachments:
|
||||
return text
|
||||
|
||||
content = []
|
||||
|
||||
# 添加文本
|
||||
if text:
|
||||
content.append({"type": "text", "text": text})
|
||||
|
||||
# 添加附件
|
||||
for att in attachments:
|
||||
att_type = att.get('type', 'file')
|
||||
mime_type = att.get('mime_type', '')
|
||||
url = att.get('url', '')
|
||||
base64_content = att.get('base64', '')
|
||||
|
||||
if att_type == 'image' or mime_type.startswith('image/'):
|
||||
# 图片类型
|
||||
text_content = att.get('text_content', '')
|
||||
|
||||
if base64_content:
|
||||
# 多模态模型:使用 OpenAI 多模态格式
|
||||
# 使用 data URL 格式
|
||||
image_url = f"data:{mime_type};base64,{base64_content}"
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": image_url}
|
||||
})
|
||||
elif text_content:
|
||||
# 非多模态模型:使用 OCR 识别的文本内容
|
||||
file_name = att.get('name', 'unknown')
|
||||
content.append({
|
||||
"type": "text",
|
||||
"text": f"\n{text_content}"
|
||||
})
|
||||
elif url:
|
||||
# 回退到 URL(仅适用于公网可访问的 URL)
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": url}
|
||||
})
|
||||
else:
|
||||
# 其他文件类型 - 提取文本内容或显示文件信息
|
||||
file_name = att.get('name', 'unknown')
|
||||
text_content = att.get('text_content', '')
|
||||
|
||||
if text_content:
|
||||
# 有提取的文本内容,将其添加到消息中
|
||||
content.append({
|
||||
"type": "text",
|
||||
"text": f"\n--- 文件: {file_name} ---\n{text_content}\n--- 文件结束 ---"
|
||||
})
|
||||
else:
|
||||
# 无法提取内容,只显示文件信息
|
||||
file_size = att.get('size', 0)
|
||||
size_str = f"{file_size / 1024:.1f}KB" if file_size < 1024 * 1024 else f"{file_size / 1024 / 1024:.1f}MB"
|
||||
content.append({
|
||||
"type": "text",
|
||||
"text": f"\n[附件: {file_name} ({size_str}),该文件类型暂不支持内容提取]"
|
||||
})
|
||||
|
||||
return content if len(content) > 1 else (content[0].get('text', '') if content else text)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""工具调用"""
|
||||
id: str # 工具调用 ID
|
||||
name: str # 工具/函数名称
|
||||
arguments: Dict[str, Any] # 参数
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
import json
|
||||
return {
|
||||
'id': self.id,
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': self.name,
|
||||
'arguments': json.dumps(self.arguments, ensure_ascii=False) if isinstance(self.arguments, dict) else self.arguments,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
"""LLM 响应"""
|
||||
content: str
|
||||
model: str = ''
|
||||
prompt_tokens: int = 0
|
||||
completion_tokens: int = 0
|
||||
total_tokens: int = 0
|
||||
finish_reason: str = ''
|
||||
raw_response: Optional[Dict] = None
|
||||
# Function Calling 相关
|
||||
tool_calls: Optional[List[ToolCall]] = None # 工具调用列表
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
return self.finish_reason in ('stop', 'end_turn', 'length')
|
||||
|
||||
@property
|
||||
def has_tool_calls(self) -> bool:
|
||||
"""是否包含工具调用"""
|
||||
return bool(self.tool_calls)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMStreamChunk:
|
||||
"""LLM 流式响应块"""
|
||||
content: str = ''
|
||||
is_finished: bool = False
|
||||
finish_reason: str = ''
|
||||
prompt_tokens: int = 0
|
||||
completion_tokens: int = 0
|
||||
total_tokens: int = 0
|
||||
# Function Calling 相关
|
||||
tool_calls: Optional[List[ToolCall]] = None # 工具调用(流式时可能分多次返回)
|
||||
tool_call_delta: Optional[Dict[str, Any]] = None # 工具调用增量
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolDefinition:
|
||||
"""工具定义(用于 Function Calling)"""
|
||||
name: str # 工具名称
|
||||
description: str # 工具描述
|
||||
parameters: Dict[str, Any] # 参数 Schema (JSON Schema 格式)
|
||||
|
||||
def to_openai_format(self) -> Dict[str, Any]:
|
||||
"""转换为 OpenAI 格式"""
|
||||
return {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'parameters': self.parameters,
|
||||
}
|
||||
}
|
||||
|
||||
def to_claude_format(self) -> Dict[str, Any]:
|
||||
"""转换为 Claude 格式"""
|
||||
return {
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'input_schema': self.parameters,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMConfig:
|
||||
"""LLM 配置"""
|
||||
model: str
|
||||
temperature: float = 0.7
|
||||
top_p: float = 1.0
|
||||
max_tokens: int = 2048
|
||||
stop: Optional[List[str]] = None
|
||||
presence_penalty: float = 0.0
|
||||
frequency_penalty: float = 0.0
|
||||
extra_params: Dict[str, Any] = field(default_factory=dict)
|
||||
# Function Calling 相关
|
||||
tools: Optional[List[ToolDefinition]] = None # 工具定义列表
|
||||
tool_choice: str = 'auto' # 工具选择策略: auto, none, required, 或具体工具名
|
||||
|
||||
|
||||
class BaseLLMProvider(ABC):
|
||||
"""
|
||||
LLM 提供商基类
|
||||
|
||||
所有提供商适配器必须继承此类并实现抽象方法
|
||||
"""
|
||||
|
||||
# 提供商类型标识
|
||||
provider_type: str = ''
|
||||
|
||||
# 提供商显示名称
|
||||
provider_name: str = ''
|
||||
|
||||
# 支持的模型类型
|
||||
supported_model_types: List[str] = ['chat']
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str = '',
|
||||
api_base: str = '',
|
||||
**kwargs
|
||||
):
|
||||
self.api_key = api_key
|
||||
self.api_base = api_base
|
||||
self.extra_config = kwargs
|
||||
|
||||
@abstractmethod
|
||||
def chat(
|
||||
self,
|
||||
messages: List[LLMMessage],
|
||||
config: LLMConfig,
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
同步对话
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
config: 配置
|
||||
|
||||
Returns:
|
||||
LLMResponse
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def chat_async(
|
||||
self,
|
||||
messages: List[LLMMessage],
|
||||
config: LLMConfig,
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
异步对话
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
config: 配置
|
||||
|
||||
Returns:
|
||||
LLMResponse
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def chat_stream(
|
||||
self,
|
||||
messages: List[LLMMessage],
|
||||
config: LLMConfig,
|
||||
) -> AsyncGenerator[LLMStreamChunk, None]:
|
||||
"""
|
||||
异步流式对话
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
config: 配置
|
||||
|
||||
Yields:
|
||||
LLMStreamChunk
|
||||
"""
|
||||
pass
|
||||
|
||||
def chat_stream_sync(
|
||||
self,
|
||||
messages: List[LLMMessage],
|
||||
config: LLMConfig,
|
||||
):
|
||||
"""
|
||||
同步流式对话(生成器)
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
config: 配置
|
||||
|
||||
Yields:
|
||||
LLMStreamChunk
|
||||
"""
|
||||
# 默认实现:调用同步 chat 并返回单个结果
|
||||
response = self.chat(messages, config)
|
||||
yield LLMStreamChunk(
|
||||
content=response.content,
|
||||
is_finished=True,
|
||||
finish_reason=response.finish_reason,
|
||||
prompt_tokens=response.prompt_tokens,
|
||||
completion_tokens=response.completion_tokens,
|
||||
total_tokens=response.total_tokens,
|
||||
)
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""
|
||||
验证配置是否有效
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
return bool(self.api_key)
|
||||
|
||||
def get_available_models(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取可用模型列表
|
||||
|
||||
Returns:
|
||||
模型列表
|
||||
"""
|
||||
return []
|
||||
|
||||
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
从提供商 API 在线拉取最新模型列表
|
||||
|
||||
子类可覆盖此方法实现具体的 API 调用逻辑。
|
||||
默认返回空列表,表示该提供商不支持在线拉取。
|
||||
|
||||
Returns:
|
||||
模型列表,格式与 get_default_models 一致
|
||||
"""
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_default_models(cls) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取默认模型列表(用于初始化)
|
||||
|
||||
Returns:
|
||||
默认模型列表
|
||||
"""
|
||||
return []
|
||||
Reference in New Issue
Block a user