Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -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 []