Compare commits
1 Commits
v0.2.2
...
revert-218
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
524aed19d4 |
@@ -872,44 +872,3 @@ async def update_workflow_config(
|
||||
workspace_id = current_user.current_workspace_id
|
||||
cfg = app_service.update_workflow_config(db, app_id=app_id, data=payload, workspace_id=workspace_id)
|
||||
return success(data=WorkflowConfigSchema.model_validate(cfg))
|
||||
|
||||
|
||||
@router.get("/{app_id}/statistics", summary="应用统计数据")
|
||||
@cur_workspace_access_guard()
|
||||
def get_app_statistics(
|
||||
app_id: uuid.UUID,
|
||||
start_date: int,
|
||||
end_date: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
"""获取应用统计数据
|
||||
|
||||
Args:
|
||||
app_id: 应用ID
|
||||
start_date: 开始时间戳(毫秒)
|
||||
end_date: 结束时间戳(毫秒)
|
||||
|
||||
Returns:
|
||||
- daily_conversations: 每日会话数统计
|
||||
- total_conversations: 总会话数
|
||||
- daily_new_users: 每日新增用户数
|
||||
- total_new_users: 总新增用户数
|
||||
- daily_api_calls: 每日API调用次数
|
||||
- total_api_calls: 总API调用次数
|
||||
- daily_tokens: 每日token消耗
|
||||
- total_tokens: 总token消耗
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
from app.services.app_statistics_service import AppStatisticsService
|
||||
stats_service = AppStatisticsService(db)
|
||||
|
||||
result = stats_service.get_app_statistics(
|
||||
app_id=app_id,
|
||||
workspace_id=workspace_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
return success(data=result)
|
||||
|
||||
@@ -7,11 +7,10 @@ Routes:
|
||||
GET /memory/config/emotion - 获取情绪引擎配置
|
||||
POST /memory/config/emotion - 更新情绪引擎配置
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from uuid import UUID
|
||||
|
||||
@@ -22,7 +21,6 @@ from app.schemas.response_schema import ApiResponse
|
||||
from app.services.emotion_config_service import EmotionConfigService
|
||||
from app.core.logging_config import get_api_logger
|
||||
from app.db import get_db
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
# 获取API专用日志器
|
||||
api_logger = get_api_logger()
|
||||
@@ -39,7 +37,7 @@ class EmotionConfigQuery(BaseModel):
|
||||
|
||||
class EmotionConfigUpdate(BaseModel):
|
||||
"""情绪配置更新请求模型"""
|
||||
config_id: Union[uuid.UUID, int, str]= Field(..., description="配置ID")
|
||||
config_id: UUID = Field(..., description="配置ID")
|
||||
emotion_enabled: bool = Field(..., description="是否启用情绪提取")
|
||||
emotion_model_id: Optional[str] = Field(None, description="情绪分析专用模型ID")
|
||||
emotion_extract_keywords: bool = Field(..., description="是否提取情绪关键词")
|
||||
@@ -48,7 +46,7 @@ class EmotionConfigUpdate(BaseModel):
|
||||
|
||||
@router.get("/read_config", response_model=ApiResponse)
|
||||
def get_emotion_config(
|
||||
config_id: UUID|int = Query(..., description="配置ID"),
|
||||
config_id: UUID = Query(..., description="配置ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@@ -81,7 +79,7 @@ def get_emotion_config(
|
||||
f"用户 {current_user.username} 请求获取情绪配置",
|
||||
extra={"config_id": config_id}
|
||||
)
|
||||
config_id=resolve_config_id(config_id, db)
|
||||
|
||||
# 初始化服务
|
||||
config_service = EmotionConfigService(db)
|
||||
|
||||
@@ -160,7 +158,6 @@ def update_emotion_config(
|
||||
}
|
||||
}
|
||||
"""
|
||||
config.config_id=resolve_config_id(config.config_id, db)
|
||||
try:
|
||||
api_logger.info(
|
||||
f"用户 {current_user.username} 请求更新情绪配置",
|
||||
|
||||
@@ -34,7 +34,7 @@ from app.schemas.memory_storage_schema import (
|
||||
)
|
||||
from app.schemas.response_schema import ApiResponse
|
||||
from app.services.memory_forget_service import MemoryForgetService
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
|
||||
# 获取API专用日志器
|
||||
api_logger = get_api_logger()
|
||||
@@ -84,8 +84,7 @@ async def trigger_forgetting_cycle(
|
||||
|
||||
connected_config = get_end_user_connected_config(end_user_id, db)
|
||||
config_id = connected_config.get("memory_config_id")
|
||||
config_id = resolve_config_id((config_id), db)
|
||||
|
||||
|
||||
if config_id is None:
|
||||
api_logger.warning(f"终端用户 {end_user_id} 未关联记忆配置")
|
||||
return fail(BizCode.INVALID_PARAMETER, f"终端用户 {end_user_id} 未关联记忆配置", "memory_config_id is None")
|
||||
@@ -130,7 +129,7 @@ async def trigger_forgetting_cycle(
|
||||
|
||||
@router.get("/read_config", response_model=ApiResponse)
|
||||
async def read_forgetting_config(
|
||||
config_id: UUID|int,
|
||||
config_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -159,7 +158,6 @@ async def read_forgetting_config(
|
||||
)
|
||||
|
||||
try:
|
||||
config_id=resolve_config_id(config_id, db)
|
||||
# 调用服务层读取配置
|
||||
config = forget_service.read_forgetting_config(db=db, config_id=config_id)
|
||||
|
||||
@@ -197,8 +195,6 @@ async def update_forgetting_config(
|
||||
ApiResponse: 包含更新结果的响应
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
payload.config_id=resolve_config_id((payload.config_id), db)
|
||||
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
@@ -259,10 +255,12 @@ async def get_forgetting_stats(
|
||||
ApiResponse: 包含统计信息的响应
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试获取遗忘引擎统计但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
# 如果提供了 end_user_id,通过它获取 config_id
|
||||
config_id = None
|
||||
if end_user_id:
|
||||
@@ -271,7 +269,6 @@ async def get_forgetting_stats(
|
||||
|
||||
connected_config = get_end_user_connected_config(end_user_id, db)
|
||||
config_id = connected_config.get("memory_config_id")
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
|
||||
if config_id is None:
|
||||
api_logger.warning(f"终端用户 {end_user_id} 未关联记忆配置")
|
||||
@@ -328,7 +325,7 @@ async def get_forgetting_curve(
|
||||
ApiResponse: 包含遗忘曲线数据的响应
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
request.config_id = resolve_config_id((request.config_id), db)
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试获取遗忘曲线但未选择工作空间")
|
||||
|
||||
@@ -25,8 +25,6 @@ from fastapi import APIRouter, Depends, HTTPException, status,Header
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
load_dotenv()
|
||||
api_logger = get_api_logger()
|
||||
|
||||
@@ -45,7 +43,6 @@ async def save_reflection_config(
|
||||
"""Save reflection configuration to data_comfig table"""
|
||||
try:
|
||||
config_id = request.config_id
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
if not config_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -160,20 +157,17 @@ async def start_workspace_reflection(
|
||||
|
||||
@router.get("/reflection/configs")
|
||||
async def start_reflection_configs(
|
||||
config_id: uuid.UUID|int,
|
||||
config_id: uuid.UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""通过config_id查询memory_config表中的反思配置信息"""
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
try:
|
||||
config_id=resolve_config_id(config_id,db)
|
||||
api_logger.info(f"用户 {current_user.username} 查询反思配置,config_id: {config_id}")
|
||||
result = MemoryConfigRepository.query_reflection_config_by_id(db, config_id)
|
||||
memory_config_id = resolve_config_id(result.config_id, db)
|
||||
# 构建返回数据
|
||||
reflection_config = {
|
||||
"config_id": memory_config_id,
|
||||
"config_id": result.config_id,
|
||||
"reflection_enabled": result.enable_self_reflexion,
|
||||
"reflection_period_in_hours": result.iteration_period,
|
||||
"reflexion_range": result.reflexion_range,
|
||||
@@ -198,7 +192,7 @@ async def start_reflection_configs(
|
||||
|
||||
@router.get("/reflection/run")
|
||||
async def reflection_run(
|
||||
config_id: UUID|int,
|
||||
config_id: UUID,
|
||||
language_type: str = Header(default="zh", alias="X-Language-Type"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -206,7 +200,7 @@ async def reflection_run(
|
||||
"""Activate the reflection function for all matching applications in the workspace"""
|
||||
|
||||
api_logger.info(f"用户 {current_user.username} 查询反思配置,config_id: {config_id}")
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
|
||||
# 使用MemoryConfigRepository查询反思配置
|
||||
result = MemoryConfigRepository.query_reflection_config_by_id(db, config_id)
|
||||
if not result:
|
||||
|
||||
@@ -35,8 +35,6 @@ from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
# Get API logger
|
||||
api_logger = get_api_logger()
|
||||
|
||||
@@ -143,6 +141,7 @@ def create_config(
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试创建配置但未选择工作空间")
|
||||
@@ -162,12 +161,12 @@ def create_config(
|
||||
|
||||
@router.delete("/delete_config", response_model=ApiResponse) # 删除数据库中的内容(按配置名称)
|
||||
def delete_config(
|
||||
config_id: UUID|int,
|
||||
config_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
workspace_id = current_user.current_workspace_id
|
||||
config_id=resolve_config_id(config_id, db)
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试删除配置但未选择工作空间")
|
||||
@@ -189,7 +188,7 @@ def update_config(
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
workspace_id = current_user.current_workspace_id
|
||||
payload.config_id = resolve_config_id(payload.config_id, db)
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试更新配置但未选择工作空间")
|
||||
@@ -212,7 +211,7 @@ def update_config_extracted(
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
workspace_id = current_user.current_workspace_id
|
||||
payload.config_id = resolve_config_id(payload.config_id, db)
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试更新提取配置但未选择工作空间")
|
||||
@@ -234,12 +233,12 @@ def update_config_extracted(
|
||||
|
||||
@router.get("/read_config_extracted", response_model=ApiResponse) # 通过查询参数读取某条配置(固定路径) 没有意义的话就删除
|
||||
def read_config_extracted(
|
||||
config_id: UUID | int,
|
||||
config_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
workspace_id = current_user.current_workspace_id
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试读取提取配置但未选择工作空间")
|
||||
@@ -287,7 +286,6 @@ async def pilot_run(
|
||||
f"Pilot run requested: config_id={payload.config_id}, "
|
||||
f"dialogue_text_length={len(payload.dialogue_text)}"
|
||||
)
|
||||
payload.config_id = resolve_config_id(payload.config_id, db)
|
||||
svc = DataConfigService(db)
|
||||
return StreamingResponse(
|
||||
svc.pilot_run_stream(payload),
|
||||
|
||||
@@ -3,17 +3,15 @@ from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.exceptions import BusinessException
|
||||
|
||||
from app.db import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models.models_model import ModelProvider, ModelType, LoadBalanceStrategy
|
||||
from app.models.models_model import ModelProvider, ModelType
|
||||
from app.models.user_model import User
|
||||
from app.repositories.model_repository import ModelConfigRepository
|
||||
from app.schemas import model_schema
|
||||
from app.core.response_utils import success
|
||||
from app.schemas.response_schema import ApiResponse, PageData
|
||||
from app.services.model_service import ModelConfigService, ModelApiKeyService, ModelBaseService
|
||||
from app.services.model_service import ModelConfigService, ModelApiKeyService
|
||||
from app.core.logging_config import get_api_logger
|
||||
|
||||
# 获取API专用日志器
|
||||
@@ -26,54 +24,44 @@ router = APIRouter(
|
||||
|
||||
@router.get("/type", response_model=ApiResponse)
|
||||
def get_model_types():
|
||||
|
||||
return success(msg="获取模型类型成功", data=list(ModelType))
|
||||
|
||||
|
||||
@router.get("/provider", response_model=ApiResponse)
|
||||
def get_model_providers():
|
||||
providers = [p for p in ModelProvider if p != ModelProvider.COMPOSITE]
|
||||
return success(msg="获取模型提供商成功", data=providers)
|
||||
|
||||
@router.get("/strategy", response_model=ApiResponse)
|
||||
def get_model_strategies():
|
||||
return success(msg="获取模型策略成功", data=list(LoadBalanceStrategy))
|
||||
return success(msg="获取模型提供商成功", data=list(ModelProvider))
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse)
|
||||
def get_model_list(
|
||||
type: Optional[list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"),
|
||||
provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于API Key)"),
|
||||
is_active: Optional[bool] = Query(None, description="激活状态筛选"),
|
||||
is_public: Optional[bool] = Query(None, description="公开状态筛选"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
pagesize: int = Query(10, ge=1, le=100, description="每页数量"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
type: Optional[str] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"),
|
||||
provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于API Key)"),
|
||||
is_active: Optional[bool] = Query(None, description="激活状态筛选"),
|
||||
is_public: Optional[bool] = Query(None, description="公开状态筛选"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
pagesize: int = Query(10, ge=1, le=100, description="每页数量"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取模型配置列表
|
||||
|
||||
|
||||
支持多个 type 参数:
|
||||
- 单个:?type=LLM
|
||||
- 多个(逗号分隔):?type=LLM,EMBEDDING
|
||||
- 多个(重复参数):?type=LLM&type=EMBEDDING
|
||||
"""
|
||||
api_logger.info(
|
||||
f"获取模型配置列表请求: type={type}, provider={provider}, page={page}, pagesize={pagesize}, tenant_id={current_user.tenant_id}")
|
||||
|
||||
api_logger.info(f"获取模型配置列表请求: type={type}, provider={provider}, page={page}, pagesize={pagesize}, tenant_id={current_user.tenant_id}")
|
||||
|
||||
try:
|
||||
# 解析 type 参数(支持逗号分隔)
|
||||
type_list = []
|
||||
if type is not None:
|
||||
flat_type = []
|
||||
for item in type:
|
||||
split_items = [t.strip() for t in item.split(',') if t.strip()]
|
||||
flat_type.extend(split_items)
|
||||
|
||||
unique_flat_type = list(dict.fromkeys(flat_type))
|
||||
type_list = [ModelType(t.lower()) for t in unique_flat_type]
|
||||
|
||||
type_list = None
|
||||
if type:
|
||||
type_values = [t.strip() for t in type.split(',')]
|
||||
type_list = [model_schema.ModelType(t.lower()) for t in type_values if t]
|
||||
|
||||
api_logger.error(f"获取模型type_list: {type_list}")
|
||||
query = model_schema.ModelConfigQuery(
|
||||
type=type_list,
|
||||
@@ -84,7 +72,7 @@ def get_model_list(
|
||||
page=page,
|
||||
pagesize=pagesize
|
||||
)
|
||||
|
||||
|
||||
api_logger.debug(f"开始获取模型配置列表: {query.dict()}")
|
||||
result_orm = ModelConfigService.get_model_list(db=db, query=query, tenant_id=current_user.tenant_id)
|
||||
result = PageData.model_validate(result_orm)
|
||||
@@ -95,146 +83,6 @@ def get_model_list(
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/new", response_model=ApiResponse)
|
||||
def get_model_list_new(
|
||||
type: Optional[list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"),
|
||||
provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于ModelConfig)"),
|
||||
is_active: Optional[bool] = Query(None, description="激活状态筛选"),
|
||||
is_public: Optional[bool] = Query(None, description="公开状态筛选"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
is_composite: Optional[bool] = Query(None, description="组合模型筛选"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取模型配置列表
|
||||
|
||||
支持多个 type 参数:
|
||||
- 单个:?type=LLM
|
||||
- 多个(逗号分隔):?type=LLM,EMBEDDING
|
||||
- 多个(重复参数):?type=LLM&type=EMBEDDING
|
||||
"""
|
||||
api_logger.info(f"获取模型配置列表请求: type={type}, provider={provider}, tenant_id={current_user.tenant_id}")
|
||||
|
||||
try:
|
||||
# 解析 type 参数(支持逗号分隔)
|
||||
type_list = []
|
||||
if type is not None:
|
||||
flat_type = []
|
||||
for item in type:
|
||||
split_items = [t.strip() for t in item.split(',') if t.strip()]
|
||||
flat_type.extend(split_items)
|
||||
|
||||
unique_flat_type = list(dict.fromkeys(flat_type))
|
||||
type_list = [ModelType(t.lower()) for t in unique_flat_type]
|
||||
|
||||
api_logger.info(f"获取模型type_list: {type_list}")
|
||||
query = model_schema.ModelConfigQueryNew(
|
||||
type=type_list,
|
||||
provider=provider,
|
||||
is_active=is_active,
|
||||
is_public=is_public,
|
||||
is_composite=is_composite,
|
||||
search=search
|
||||
)
|
||||
|
||||
api_logger.debug(f"开始获取模型配置列表: {query.model_dump()}")
|
||||
result = ModelConfigService.get_model_list_new(db=db, query=query, tenant_id=current_user.tenant_id)
|
||||
api_logger.info(f"模型配置列表获取成功: 分组数={len(result)}, 总模型数={sum(len(item['models']) for item in result)}")
|
||||
return success(data=result, msg="模型配置列表获取成功")
|
||||
except Exception as e:
|
||||
api_logger.error(f"获取模型配置列表失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/model_plaza", response_model=ApiResponse)
|
||||
def get_model_plaza_list(
|
||||
type: Optional[ModelType] = Query(None, description="模型类型"),
|
||||
provider: Optional[ModelProvider] = Query(None, description="供应商"),
|
||||
is_official: Optional[bool] = Query(None, description="是否官方模型"),
|
||||
is_deprecated: Optional[bool] = Query(None, description="是否弃用"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""模型广场查询接口(按供应商分组)"""
|
||||
|
||||
query = model_schema.ModelBaseQuery(
|
||||
type=type,
|
||||
provider=provider,
|
||||
is_official=is_official,
|
||||
is_deprecated=is_deprecated,
|
||||
search=search
|
||||
)
|
||||
result = ModelBaseService.get_model_base_list(db=db, query=query, tenant_id=current_user.tenant_id)
|
||||
return success(data=result, msg="模型广场列表获取成功")
|
||||
|
||||
|
||||
@router.get("/model_plaza/{model_base_id}", response_model=ApiResponse)
|
||||
def get_model_base_by_id(
|
||||
model_base_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取基础模型详情"""
|
||||
|
||||
result = ModelBaseService.get_model_base_by_id(db=db, model_base_id=model_base_id)
|
||||
return success(data=model_schema.ModelBase.model_validate(result), msg="基础模型获取成功")
|
||||
|
||||
|
||||
@router.post("/model_plaza", response_model=ApiResponse)
|
||||
def create_model_base(
|
||||
data: model_schema.ModelBaseCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""创建基础模型"""
|
||||
|
||||
result = ModelBaseService.create_model_base(db=db, data=data)
|
||||
return success(data=model_schema.ModelBase.model_validate(result), msg="基础模型创建成功")
|
||||
|
||||
|
||||
@router.put("/model_plaza/{model_base_id}", response_model=ApiResponse)
|
||||
def update_model_base(
|
||||
model_base_id: uuid.UUID,
|
||||
data: model_schema.ModelBaseUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新基础模型"""
|
||||
|
||||
# 不允许更改type类型
|
||||
if data.type is not None or data.provider is not None:
|
||||
raise BusinessException("不允许更改模型类型和供应商", BizCode.INVALID_PARAMETER)
|
||||
|
||||
result = ModelBaseService.update_model_base(db=db, model_base_id=model_base_id, data=data)
|
||||
return success(data=model_schema.ModelBase.model_validate(result), msg="基础模型更新成功")
|
||||
|
||||
|
||||
@router.delete("/model_plaza/{model_base_id}", response_model=ApiResponse)
|
||||
def delete_model_base(
|
||||
model_base_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""删除基础模型"""
|
||||
|
||||
ModelBaseService.delete_model_base(db=db, model_base_id=model_base_id)
|
||||
return success(msg="基础模型删除成功")
|
||||
|
||||
|
||||
@router.post("/model_plaza/{model_base_id}/add", response_model=ApiResponse)
|
||||
def add_model_from_plaza(
|
||||
model_base_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""从模型广场添加模型到模型列表"""
|
||||
|
||||
result = ModelBaseService.add_model_from_plaza(db=db, model_base_id=model_base_id, tenant_id=current_user.tenant_id)
|
||||
return success(data=model_schema.ModelConfig.model_validate(result), msg="模型添加成功")
|
||||
|
||||
|
||||
@router.get("/{model_id}", response_model=ApiResponse)
|
||||
def get_model_by_id(
|
||||
model_id: uuid.UUID,
|
||||
@@ -290,73 +138,6 @@ async def create_model(
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/composite", response_model=ApiResponse)
|
||||
async def create_composite_model(
|
||||
model_data: model_schema.CompositeModelCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
创建组合模型
|
||||
|
||||
- 绑定一个或多个现有的 API Key
|
||||
- 所有 API Key 必须来自非组合模型
|
||||
- 所有 API Key 关联的模型类型必须与组合模型类型一致
|
||||
"""
|
||||
api_logger.info(f"创建组合模型请求: {model_data.name}, 用户: {current_user.username}, tenant_id={current_user.tenant_id}")
|
||||
|
||||
try:
|
||||
result_orm = await ModelConfigService.create_composite_model(db=db, model_data=model_data, tenant_id=current_user.tenant_id)
|
||||
api_logger.info(f"组合模型创建成功: {result_orm.name} (ID: {result_orm.id})")
|
||||
|
||||
result = model_schema.ModelConfig.model_validate(result_orm)
|
||||
return success(data=result, msg="组合模型创建成功")
|
||||
except Exception as e:
|
||||
api_logger.error(f"创建组合模型失败: {model_data.name} - {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.put("/composite/{model_id}", response_model=ApiResponse)
|
||||
async def update_composite_model(
|
||||
model_id: uuid.UUID,
|
||||
model_data: model_schema.CompositeModelCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新组合模型"""
|
||||
api_logger.info(f"更新组合模型请求: model_id={model_id}, 用户: {current_user.username}")
|
||||
|
||||
try:
|
||||
if model_data.type is not None:
|
||||
raise BusinessException("不允许更改模型类型和供应商", BizCode.INVALID_PARAMETER)
|
||||
result_orm = await ModelConfigService.update_composite_model(db=db, model_id=model_id, model_data=model_data, tenant_id=current_user.tenant_id)
|
||||
api_logger.info(f"组合模型更新成功: {result_orm.name} (ID: {model_id})")
|
||||
|
||||
result = model_schema.ModelConfig.model_validate(result_orm)
|
||||
return success(data=result, msg="组合模型更新成功")
|
||||
except Exception as e:
|
||||
api_logger.error(f"更新组合模型失败: model_id={model_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.delete("/composite/{model_id}", response_model=ApiResponse)
|
||||
def delete_composite_model(
|
||||
model_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""删除组合模型"""
|
||||
api_logger.info(f"删除组合模型请求: model_id={model_id}, 用户: {current_user.username}")
|
||||
|
||||
try:
|
||||
ModelConfigService.delete_model(db=db, model_id=model_id, tenant_id=current_user.tenant_id)
|
||||
api_logger.info(f"组合模型删除成功: model_id={model_id}")
|
||||
return success(msg="组合模型删除成功")
|
||||
except Exception as e:
|
||||
api_logger.error(f"删除组合模型失败: model_id={model_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.put("/{model_id}", response_model=ApiResponse)
|
||||
def update_model(
|
||||
model_id: uuid.UUID,
|
||||
@@ -433,53 +214,6 @@ def get_model_api_keys(
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/provider/apikeys", response_model=ApiResponse)
|
||||
async def create_model_api_key_by_provider(
|
||||
api_key_data: model_schema.ModelApiKeyCreateByProvider,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
根据供应商为所有匹配的模型创建API Key
|
||||
"""
|
||||
api_logger.info(f"创建API Key请求: provider={api_key_data.provider}, 用户: {current_user.username}")
|
||||
|
||||
try:
|
||||
# 根据tenant_id和provider筛选model_config_id列表
|
||||
model_config_ids = api_key_data.model_config_ids
|
||||
if not model_config_ids:
|
||||
model_config_ids = ModelConfigRepository.get_model_config_ids_by_provider(
|
||||
db=db,
|
||||
tenant_id=current_user.tenant_id,
|
||||
provider=api_key_data.provider
|
||||
)
|
||||
|
||||
if not model_config_ids:
|
||||
raise BusinessException(f"未找到供应商 {api_key_data.provider} 的模型配置", BizCode.MODEL_NOT_FOUND)
|
||||
|
||||
# 构造schema并调用service
|
||||
create_data = model_schema.ModelApiKeyCreateByProvider(
|
||||
provider=api_key_data.provider,
|
||||
api_key=api_key_data.api_key,
|
||||
api_base=api_key_data.api_base,
|
||||
description=api_key_data.description,
|
||||
config=api_key_data.config,
|
||||
is_active=api_key_data.is_active,
|
||||
priority=api_key_data.priority,
|
||||
model_config_ids=model_config_ids
|
||||
)
|
||||
created_keys, failed_models = await ModelApiKeyService.create_api_key_by_provider(db=db, data=create_data)
|
||||
|
||||
api_logger.info(f"API Key创建成功: 关联{len(created_keys)}个模型")
|
||||
# result_list = [model_schema.ModelApiKey.model_validate(key) for key in created_keys]
|
||||
result = "API Key已存在" if len(created_keys) == 0 and len(failed_models) == 0 else \
|
||||
f"成功为 {len(created_keys)} 个模型创建API Key, 失败模型列表{failed_models}"
|
||||
return success(data=result, msg=f"成功为 {len(created_keys)} 个模型创建API Key")
|
||||
except Exception as e:
|
||||
api_logger.error(f"创建API Key失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/{model_id}/apikeys", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_model_api_key(
|
||||
model_id: uuid.UUID,
|
||||
@@ -494,12 +228,11 @@ async def create_model_api_key(
|
||||
|
||||
try:
|
||||
# 设置模型配置ID
|
||||
api_key_data.model_config_ids = [model_id]
|
||||
api_key_data.model_config_id = model_id
|
||||
|
||||
api_logger.debug(f"开始创建模型API Key: {api_key_data.model_name}")
|
||||
result_orm = await ModelApiKeyService.create_api_key(db=db, api_key_data=api_key_data)
|
||||
api_logger.info(f"模型API Key创建成功: {result_orm.model_name} (ID: {result_orm.id})")
|
||||
result = model_schema.ModelApiKey.model_validate(result_orm)
|
||||
result = await ModelApiKeyService.create_api_key(db=db, api_key_data=api_key_data)
|
||||
api_logger.info(f"模型API Key创建成功: {result.model_name} (ID: {result.id})")
|
||||
return success(data=result, msg="模型API Key创建成功")
|
||||
except Exception as e:
|
||||
api_logger.error(f"创建模型API Key失败: {api_key_data.model_name} - {str(e)}")
|
||||
@@ -601,3 +334,5 @@ async def validate_model_config(
|
||||
return success(data=model_schema.ModelValidateResponse(**result), msg="验证完成")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -235,11 +235,11 @@ async def chat(
|
||||
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=end_user_id, # 转换为字符串
|
||||
user_id=new_end_user.id, # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=web_search,
|
||||
memory=memory,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=app.id,
|
||||
@@ -268,11 +268,11 @@ async def chat(
|
||||
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=end_user_id, # 转换为字符串
|
||||
user_id=new_end_user.id, # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=web_search,
|
||||
memory=memory,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=app.id,
|
||||
|
||||
@@ -28,8 +28,6 @@ from langchain.agents import create_agent
|
||||
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
|
||||
@@ -177,6 +175,7 @@ class LangChainAgent:
|
||||
# messagss_list.append(f'用户:{query}。AI回复:{aimessages}')
|
||||
# retrieved_content.append({query: aimessages})
|
||||
# return messagss_list,retrieved_content
|
||||
|
||||
async def write(self, storage_type, end_user_id, user_message, ai_message, user_rag_memory_id, actual_end_user_id, actual_config_id):
|
||||
"""
|
||||
写入记忆(支持结构化消息)
|
||||
@@ -197,54 +196,48 @@ class LangChainAgent:
|
||||
2. 如果只有 user_message:创建单条用户消息 [user](用于历史记忆场景)
|
||||
3. 每条消息会被转换为独立的 Chunk,保留 speaker 字段
|
||||
"""
|
||||
if storage_type == "rag":
|
||||
# RAG 模式:组合消息为字符串格式(保持原有逻辑)
|
||||
combined_message = f"user: {user_message}\nassistant: {ai_message}"
|
||||
await write_rag(end_user_id, combined_message, user_rag_memory_id)
|
||||
logger.info(f'RAG_Agent:{end_user_id};{user_rag_memory_id}')
|
||||
else:
|
||||
# Neo4j 模式:使用结构化消息列表
|
||||
structured_messages = []
|
||||
|
||||
db = next(get_db())
|
||||
try:
|
||||
actual_config_id=resolve_config_id(actual_config_id, db)
|
||||
# 始终添加用户消息(如果不为空)
|
||||
if user_message:
|
||||
structured_messages.append({"role": "user", "content": user_message})
|
||||
|
||||
if storage_type == "rag":
|
||||
# RAG 模式:组合消息为字符串格式(保持原有逻辑)
|
||||
combined_message = f"user: {user_message}\nassistant: {ai_message}"
|
||||
await write_rag(end_user_id, combined_message, user_rag_memory_id)
|
||||
logger.info(f'RAG_Agent:{end_user_id};{user_rag_memory_id}')
|
||||
else:
|
||||
# Neo4j 模式:使用结构化消息列表
|
||||
structured_messages = []
|
||||
# 只有当 AI 回复不为空时才添加 assistant 消息
|
||||
if ai_message:
|
||||
structured_messages.append({"role": "assistant", "content": ai_message})
|
||||
|
||||
# 始终添加用户消息(如果不为空)
|
||||
if user_message:
|
||||
structured_messages.append({"role": "user", "content": user_message})
|
||||
# 如果没有消息,直接返回
|
||||
if not structured_messages:
|
||||
logger.warning(f"No messages to write for user {actual_end_user_id}")
|
||||
return
|
||||
|
||||
# 只有当 AI 回复不为空时才添加 assistant 消息
|
||||
if ai_message:
|
||||
structured_messages.append({"role": "assistant", "content": ai_message})
|
||||
# 调用 Celery 任务,传递结构化消息列表
|
||||
# 数据流:
|
||||
# 1. structured_messages 传递给 write_message_task
|
||||
# 2. write_message_task 调用 memory_agent_service.write_memory
|
||||
# 3. write_memory 调用 write_tools.write,传递 messages 参数
|
||||
# 4. write_tools.write 调用 get_chunked_dialogs,传递 messages 参数
|
||||
# 5. get_chunked_dialogs 为每条消息创建独立的 Chunk,设置 speaker 字段
|
||||
# 6. 每个 Chunk 保存到 Neo4j,包含 speaker 字段
|
||||
logger.info(f"[WRITE] Submitting Celery task - user={actual_end_user_id}, messages={len(structured_messages)}, config={actual_config_id}")
|
||||
write_id = write_message_task.delay(
|
||||
actual_end_user_id, # end_user_id: 用户ID
|
||||
structured_messages, # message: 结构化消息列表 [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
|
||||
actual_config_id, # config_id: 配置ID
|
||||
storage_type, # storage_type: "neo4j"
|
||||
user_rag_memory_id # user_rag_memory_id: RAG记忆ID(Neo4j模式下不使用)
|
||||
)
|
||||
logger.info(f"[WRITE] Celery task submitted - task_id={write_id}")
|
||||
write_status = get_task_memory_write_result(str(write_id))
|
||||
logger.info(f'[WRITE] Task result - user={actual_end_user_id}, status={write_status}')
|
||||
|
||||
# 如果没有消息,直接返回
|
||||
if not structured_messages:
|
||||
logger.warning(f"No messages to write for user {actual_end_user_id}")
|
||||
return
|
||||
|
||||
# 调用 Celery 任务,传递结构化消息列表
|
||||
# 数据流:
|
||||
# 1. structured_messages 传递给 write_message_task
|
||||
# 2. write_message_task 调用 memory_agent_service.write_memory
|
||||
# 3. write_memory 调用 write_tools.write,传递 messages 参数
|
||||
# 4. write_tools.write 调用 get_chunked_dialogs,传递 messages 参数
|
||||
# 5. get_chunked_dialogs 为每条消息创建独立的 Chunk,设置 speaker 字段
|
||||
# 6. 每个 Chunk 保存到 Neo4j,包含 speaker 字段
|
||||
logger.info(f"[WRITE] Submitting Celery task - user={actual_end_user_id}, messages={len(structured_messages)}, config={actual_config_id}")
|
||||
write_id = write_message_task.delay(
|
||||
actual_end_user_id, # end_user_id: 用户ID
|
||||
structured_messages, # message: 结构化消息列表 [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
|
||||
actual_config_id, # config_id: 配置ID
|
||||
storage_type, # storage_type: "neo4j"
|
||||
user_rag_memory_id # user_rag_memory_id: RAG记忆ID(Neo4j模式下不使用)
|
||||
)
|
||||
logger.info(f"[WRITE] Celery task submitted - task_id={write_id}")
|
||||
write_status = get_task_memory_write_result(str(write_id))
|
||||
logger.info(f'[WRITE] Task result - user={actual_end_user_id}, status={write_status}')
|
||||
finally:
|
||||
db.close()
|
||||
async def chat(
|
||||
self,
|
||||
message: str,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""模型配置脚本模块"""
|
||||
@@ -1,174 +0,0 @@
|
||||
provider: bedrock
|
||||
enabled: true
|
||||
models:
|
||||
- name: ai21
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: AI21 Labs大语言模型,completion生成模式,256000上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
logo: bedrock
|
||||
- name: amazon nova
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: Amazon Nova大语言模型,支持智能体思考、工具调用、流式工具调用、视觉能力,300000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- vision
|
||||
logo: bedrock
|
||||
- name: anthropic claude
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: Anthropic Claude大语言模型,支持智能体思考、视觉能力、工具调用、流式工具调用、文档处理,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
logo: bedrock
|
||||
- name: cohere
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: Cohere大语言模型,支持智能体思考、工具调用、流式工具调用,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
logo: bedrock
|
||||
- name: deepseek
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: DeepSeek大语言模型,支持智能体思考、视觉能力、工具调用、流式工具调用,32768上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
logo: bedrock
|
||||
- name: meta
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: Meta Llama大语言模型,支持智能体思考、工具调用,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
logo: bedrock
|
||||
- name: mistral
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: Mistral AI大语言模型,支持智能体思考、工具调用,32000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
logo: bedrock
|
||||
- name: openai
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: OpenAI大语言模型,支持智能体思考、工具调用、流式工具调用,32768上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
logo: bedrock
|
||||
- name: qwen
|
||||
type: llm
|
||||
provider: bedrock
|
||||
description: Qwen大语言模型,支持智能体思考、工具调用、流式工具调用,32768上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
logo: bedrock
|
||||
- name: amazon.rerank-v1:0
|
||||
type: rerank
|
||||
provider: bedrock
|
||||
description: amazon.rerank-v1:0重排序模型,5120上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 重排序模型
|
||||
logo: bedrock
|
||||
- name: cohere.rerank-v3-5:0
|
||||
type: rerank
|
||||
provider: bedrock
|
||||
description: cohere.rerank-v3-5:0重排序模型,5120上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 重排序模型
|
||||
logo: bedrock
|
||||
- name: amazon.nova-2-multimodal-embeddings-v1:0
|
||||
type: embedding
|
||||
provider: bedrock
|
||||
description: amazon.nova-2-multimodal-embeddings-v1:0文本嵌入模型,支持视觉能力,8192上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本嵌入模型
|
||||
- vision
|
||||
logo: bedrock
|
||||
- name: amazon.titan-embed-text-v1
|
||||
type: embedding
|
||||
provider: bedrock
|
||||
description: amazon.titan-embed-text-v1文本嵌入模型,8192上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本嵌入模型
|
||||
logo: bedrock
|
||||
- name: amazon.titan-embed-text-v2:0
|
||||
type: embedding
|
||||
provider: bedrock
|
||||
description: amazon.titan-embed-text-v2:0文本嵌入模型,8192上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本嵌入模型
|
||||
logo: bedrock
|
||||
- name: cohere.embed-english-v3
|
||||
type: embedding
|
||||
provider: bedrock
|
||||
description: Cohere Embed 3 English文本嵌入模型,512上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本嵌入模型
|
||||
logo: bedrock
|
||||
- name: cohere.embed-multilingual-v3
|
||||
type: embedding
|
||||
provider: bedrock
|
||||
description: Cohere Embed 3 Multilingual文本嵌入模型,512上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本嵌入模型
|
||||
logo: bedrock
|
||||
@@ -1,820 +0,0 @@
|
||||
provider: dashscope
|
||||
enabled: true
|
||||
models:
|
||||
- name: deepseek-r1-distill-qwen-14b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: DeepSeek-R1-Distill-Qwen-14B大语言模型,支持智能体思考,32000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: deepseek-r1-distill-qwen-32b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: DeepSeek-R1-Distill-Qwen-32B大语言模型,支持智能体思考,32000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: deepseek-r1
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: DeepSeek-R1大语言模型,支持智能体思考,131072超大上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: deepseek-v3.1
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: DeepSeek-V3.1大语言模型,支持智能体思考,131072超大上下文窗口,对话模式,支持丰富生成参数调节
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: deepseek-v3.2-exp
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: DeepSeek-V3.2-exp实验版大语言模型,支持智能体思考,131072超大上下文窗口,对话模式,支持丰富生成参数调节
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: deepseek-v3.2
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: DeepSeek-V3.2大语言模型,支持智能体思考,131072超大上下文窗口,对话模式,支持丰富生成参数调节
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: deepseek-v3
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: DeepSeek-V3大语言模型,支持智能体思考,64000上下文窗口,对话模式,支持文本与JSON格式输出
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: farui-plus
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: farui-plus大语言模型,支持多工具调用、智能体思考、流式工具调用,12288上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: glm-4.7
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: GLM-4.7大语言模型,支持多工具调用、智能体思考、流式工具调用,202752超大上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qvq-max-latest
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qvq-max-latest大语言模型,支持视觉、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qvq-max
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qvq-max大语言模型,支持视觉、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-coder-turbo-0919
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-coder-turbo-0919代码专用大语言模型,支持智能体思考,131072上下文窗口,对话模式,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 代码模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: qwen-max-latest
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-max-latest大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-max-longcontext
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-max-longcontext长上下文大语言模型,支持多工具调用、智能体思考、流式工具调用,32000上下文窗口,对话模式,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-max
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-max大语言模型,支持多工具调用、智能体思考、流式工具调用,32768上下文窗口,对话模式,支持联网搜索
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-mt-plus
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-mt-plus多语言翻译大语言模型,支持智能体思考,16384上下文窗口,对话模式,支持多语种互译与领域翻译适配
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 翻译模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: qwen-mt-turbo
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-mt-turbo轻量化多语言翻译大语言模型,支持智能体思考,16384上下文窗口,对话模式,支持多语种互译与领域翻译适配
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 翻译模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: qwen-plus-0112
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-0112大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-plus-0125
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-0125大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-plus-0723
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-0723大语言模型,支持多工具调用、智能体思考、流式工具调用,32000上下文窗口,对话模式,支持联网搜索,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-plus-0806
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-0806大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-plus-0919
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-0919大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-plus-1125
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-1125大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-plus-1127
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-1127大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-plus-1220
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-plus-1220大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen-vl-max
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-vl-max多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen-vl-plus-0809
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-vl-plus-0809多模态大模型,支持视觉理解、智能体思考、视频理解,32768上下文窗口,对话模式,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen-vl-plus-2025-01-02
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-vl-plus-2025-01-02多模态大模型,支持视觉理解、智能体思考、视频理解,32768上下文窗口,对话模式,未废弃
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen-vl-plus-2025-01-25
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-vl-plus-2025-01-25多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen-vl-plus-latest
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-vl-plus-latest多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen-vl-plus
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen-vl-plus多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen2.5-0.5b-instruct
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen2.5-0.5b-instruct大语言模型,支持多工具调用、智能体思考、流式工具调用,32768上下文窗口,对话模式,未废弃
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-14b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-14b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-235b-a22b-instruct-2507
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-235b-a22b-instruct-2507大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-235b-a22b-thinking-2507
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-235b-a22b-thinking-2507大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-235b-a22b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-235b-a22b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-30b-a3b-instruct-2507
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-30b-a3b-instruct-2507大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-30b-a3b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-30b-a3b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-32b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-32b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-4b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-4b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-8b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-8b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-coder-30b-a3b-instruct
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-coder-30b-a3b-instruct大语言模型,支持智能体思考,262144上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 代码模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: qwen3-coder-480b-a35b-instruct
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-coder-480b-a35b-instruct大语言模型,支持智能体思考,262144上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 代码模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: qwen3-coder-plus-2025-09-23
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-coder-plus-2025-09-23大语言模型,支持智能体思考,1000000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 代码模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: qwen3-coder-plus
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-coder-plus大语言模型,支持智能体思考,1000000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 代码模型
|
||||
- agent-thought
|
||||
logo: dashscope
|
||||
- name: qwen3-max-2025-09-23
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-max-2025-09-23大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式,支持联网搜索
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- 联网搜索
|
||||
logo: dashscope
|
||||
- name: qwen3-max-2026-01-23
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-max-2026-01-23大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式,支持联网搜索
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- 联网搜索
|
||||
logo: dashscope
|
||||
- name: qwen3-max-preview
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-max-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-max
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-max大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式,支持联网搜索
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- 联网搜索
|
||||
logo: dashscope
|
||||
- name: qwen3-next-80b-a3b-instruct
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-next-80b-a3b-instruct大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-next-80b-a3b-thinking
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-next-80b-a3b-thinking大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwen3-omni-flash-2025-12-01
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-omni-flash-2025-12-01多模态大语言模型,支持视觉、智能体思考、视频、音频能力,65536上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
- audio
|
||||
logo: dashscope
|
||||
- name: qwen3-vl-235b-a22b-instruct
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-vl-235b-a22b-instruct多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen3-vl-235b-a22b-thinking
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-vl-235b-a22b-thinking多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen3-vl-30b-a3b-instruct
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-vl-30b-a3b-instruct多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen3-vl-30b-a3b-thinking
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-vl-30b-a3b-thinking多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen3-vl-flash
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-vl-flash多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen3-vl-plus-2025-09-23
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-vl-plus-2025-09-23多模态大语言模型,支持视觉、智能体思考、视频能力,262144上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwen3-vl-plus
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwen3-vl-plus多模态大语言模型,支持视觉、智能体思考、视频能力,262144上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
- agent-thought
|
||||
- video
|
||||
logo: dashscope
|
||||
- name: qwq-32b
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwq-32b大语言模型,支持智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwq-plus-0305
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwq-plus-0305大语言模型,支持智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: qwq-plus
|
||||
type: llm
|
||||
provider: dashscope
|
||||
description: qwq-plus大语言模型,支持智能体思考、流式工具调用,131072上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: dashscope
|
||||
- name: gte-rerank-v2
|
||||
type: rerank
|
||||
provider: dashscope
|
||||
description: gte-rerank-v2重排序模型,4000上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 重排序模型
|
||||
logo: dashscope
|
||||
- name: gte-rerank
|
||||
type: rerank
|
||||
provider: dashscope
|
||||
description: gte-rerank重排序模型,4000上下文窗口
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 重排序模型
|
||||
logo: dashscope
|
||||
- name: multimodal-embedding-v1
|
||||
type: embedding
|
||||
provider: dashscope
|
||||
description: multimodal-embedding-v1多模态嵌入模型,支持视觉能力,8192上下文窗口,最大分块数10
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 嵌入模型
|
||||
- 多模态模型
|
||||
- vision
|
||||
logo: dashscope
|
||||
- name: text-embedding-v1
|
||||
type: embedding
|
||||
provider: dashscope
|
||||
description: text-embedding-v1文本嵌入模型,2048上下文窗口,最大分块数25
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 嵌入模型
|
||||
- 文本嵌入
|
||||
logo: dashscope
|
||||
- name: text-embedding-v2
|
||||
type: embedding
|
||||
provider: dashscope
|
||||
description: text-embedding-v2文本嵌入模型,2048上下文窗口,最大分块数25
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 嵌入模型
|
||||
- 文本嵌入
|
||||
logo: dashscope
|
||||
- name: text-embedding-v3
|
||||
type: embedding
|
||||
provider: dashscope
|
||||
description: text-embedding-v3文本嵌入模型,8192上下文窗口,最大分块数10
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 嵌入模型
|
||||
- 文本嵌入
|
||||
logo: dashscope
|
||||
- name: text-embedding-v4
|
||||
type: embedding
|
||||
provider: dashscope
|
||||
description: text-embedding-v4文本嵌入模型,8192上下文窗口,最大分块数10
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 嵌入模型
|
||||
- 文本嵌入
|
||||
logo: dashscope
|
||||
@@ -1,143 +0,0 @@
|
||||
"""模型配置加载器 - 用于将预定义模型批量导入到数据库"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import yaml
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.models_model import ModelBase, ModelProvider
|
||||
|
||||
|
||||
def _load_yaml_config(provider: ModelProvider) -> list[dict]:
|
||||
"""从YAML文件加载指定供应商的模型配置"""
|
||||
config_dir = Path(__file__).parent
|
||||
config_file = config_dir / f"{provider.value}_models.yaml"
|
||||
|
||||
if not config_file.exists():
|
||||
return []
|
||||
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 检查是否需要加载(默认为 true)
|
||||
if not data.get('enabled', True):
|
||||
return []
|
||||
|
||||
return data.get('models', [])
|
||||
|
||||
|
||||
def _disable_yaml_config(provider: ModelProvider) -> None:
|
||||
"""将YAML文件的enabled标志设置为false"""
|
||||
config_dir = Path(__file__).parent
|
||||
config_file = config_dir / f"{provider.value}_models.yaml"
|
||||
|
||||
if not config_file.exists():
|
||||
return
|
||||
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
data['enabled'] = False
|
||||
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(data, f, allow_unicode=True, sort_keys=False)
|
||||
|
||||
|
||||
def load_models(db: Session, providers: list[str] = None, silent: bool = False) -> dict:
|
||||
"""
|
||||
加载模型配置到数据库
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
providers: 要加载的供应商列表,None表示加载所有
|
||||
silent: 是否静默模式(不输出详细日志)
|
||||
|
||||
Returns:
|
||||
dict: 加载结果统计 {"success": int, "skipped": int, "failed": int}
|
||||
"""
|
||||
result = {"success": 0, "skipped": 0, "failed": 0}
|
||||
|
||||
# 确定要加载的供应商
|
||||
if providers:
|
||||
target_providers = [ModelProvider(p) if isinstance(p, str) else p for p in providers]
|
||||
else:
|
||||
target_providers = [p for p in ModelProvider if p != ModelProvider.COMPOSITE]
|
||||
|
||||
for provider in target_providers:
|
||||
# 从YAML文件加载模型配置
|
||||
models = _load_yaml_config(provider)
|
||||
|
||||
if not models:
|
||||
if not silent:
|
||||
print(f"警告: 供应商 '{provider.value}' 暂无预定义模型")
|
||||
continue
|
||||
|
||||
if not silent:
|
||||
print(f"\n正在加载 {provider.value} 的 {len(models)} 个模型...")
|
||||
|
||||
# provider_success = 0
|
||||
for model_data in models:
|
||||
try:
|
||||
# 检查模型是否已存在
|
||||
existing = db.query(ModelBase).filter(
|
||||
ModelBase.name == model_data["name"],
|
||||
ModelBase.provider == model_data["provider"]
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# 更新现有模型配置
|
||||
for key, value in model_data.items():
|
||||
setattr(existing, key, value)
|
||||
db.commit()
|
||||
if not silent:
|
||||
print(f"更新成功: {model_data['name']}")
|
||||
result["success"] += 1
|
||||
# provider_success += 1
|
||||
else:
|
||||
# 创建新模型
|
||||
model = ModelBase(**model_data)
|
||||
db.add(model)
|
||||
db.commit()
|
||||
if not silent:
|
||||
print(f"添加成功: {model_data['name']}")
|
||||
result["success"] += 1
|
||||
# provider_success += 1
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
if not silent:
|
||||
print(f"添加失败: {model_data['name']} - {str(e)}")
|
||||
result["failed"] += 1
|
||||
|
||||
# 如果该供应商的模型全部加载成功,将enabled设置为false
|
||||
# if provider_success == len(models):
|
||||
_disable_yaml_config(provider)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def load_models_by_provider(db: Session, provider: str) -> dict:
|
||||
"""
|
||||
加载指定供应商的模型配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
provider: 供应商名称(字符串或ModelProvider枚举)
|
||||
|
||||
Returns:
|
||||
dict: 加载结果统计
|
||||
"""
|
||||
provider_enum = ModelProvider(provider) if isinstance(provider, str) else provider
|
||||
return load_models(db, providers=[provider_enum])
|
||||
|
||||
|
||||
def get_available_providers() -> list[Callable[[], str]]:
|
||||
"""获取所有可用的供应商列表(从ModelProvider枚举获取,排除COMPOSITE)"""
|
||||
return [p.value for p in ModelProvider if p != ModelProvider.COMPOSITE]
|
||||
|
||||
|
||||
def get_models_by_provider(provider: str) -> list[dict]:
|
||||
"""获取指定供应商的模型配置列表"""
|
||||
provider_enum = ModelProvider(provider) if isinstance(provider, str) else provider
|
||||
return _load_yaml_config(provider_enum)
|
||||
@@ -1,294 +0,0 @@
|
||||
provider: openai
|
||||
enabled: true
|
||||
models:
|
||||
- name: chatgpt-4o-latest
|
||||
type: llm
|
||||
provider: openai
|
||||
description: chatgpt-4o-latest大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
logo: openai
|
||||
- name: gpt-3.5-turbo-0125
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-3.5-turbo-0125大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: openai
|
||||
- name: gpt-3.5-turbo-1106
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-3.5-turbo-1106大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: openai
|
||||
- name: gpt-3.5-turbo-16k
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-3.5-turbo-16k大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: openai
|
||||
- name: gpt-3.5-turbo-instruct
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-3.5-turbo-instruct大语言模型,4096上下文窗口,文本补全模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
logo: openai
|
||||
- name: gpt-3.5-turbo
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-3.5-turbo大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: openai
|
||||
- name: gpt-4-0125-preview
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-4-0125-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: openai
|
||||
- name: gpt-4-1106-preview
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-4-1106-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: openai
|
||||
- name: gpt-4-turbo-2024-04-09
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-4-turbo-2024-04-09大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
logo: openai
|
||||
- name: gpt-4-turbo-preview
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-4-turbo-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
logo: openai
|
||||
- name: gpt-4-turbo
|
||||
type: llm
|
||||
provider: openai
|
||||
description: gpt-4-turbo大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力,128000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
logo: openai
|
||||
- name: o1-preview
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o1-preview大语言模型,支持智能体思考,128000上下文窗口,对话模式,已废弃
|
||||
is_deprecated: true
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
logo: openai
|
||||
- name: o1
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o1大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o3-2025-04-16
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o3-2025-04-16大语言模型,支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- vision
|
||||
- stream-tool-call
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o3-mini-2025-01-31
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o3-mini-2025-01-31大语言模型,支持智能体思考、工具调用、流式工具调用、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o3-mini
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o3-mini大语言模型,支持智能体思考、工具调用、流式工具调用、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o3-pro-2025-06-10
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o3-pro-2025-06-10大语言模型,支持智能体思考、工具调用、视觉能力、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- vision
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o3-pro
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o3-pro大语言模型,支持智能体思考、工具调用、视觉能力、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- vision
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o3
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o3大语言模型,支持智能体思考、视觉能力、工具调用、流式工具调用、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o4-mini-2025-04-16
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o4-mini-2025-04-16大语言模型,支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- vision
|
||||
- stream-tool-call
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: o4-mini
|
||||
type: llm
|
||||
provider: openai
|
||||
description: o4-mini大语言模型,支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出,200000上下文窗口,对话模式
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 大语言模型
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- vision
|
||||
- stream-tool-call
|
||||
- structured-output
|
||||
logo: openai
|
||||
- name: text-embedding-3-large
|
||||
type: embedding
|
||||
provider: openai
|
||||
description: text-embedding-3-large文本向量模型,8191上下文窗口,最大分块数32
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本向量模型
|
||||
logo: openai
|
||||
- name: text-embedding-3-small
|
||||
type: embedding
|
||||
provider: openai
|
||||
description: text-embedding-3-small文本向量模型,8191上下文窗口,最大分块数32
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本向量模型
|
||||
logo: openai
|
||||
- name: text-embedding-ada-002
|
||||
type: embedding
|
||||
provider: openai
|
||||
description: text-embedding-ada-002文本向量模型,8097上下文窗口,最大分块数32
|
||||
is_deprecated: false
|
||||
is_official: true
|
||||
tags:
|
||||
- 文本向量模型
|
||||
logo: openai
|
||||
@@ -16,6 +16,7 @@ from app.core.workflow.graph_builder import GraphBuilder, StreamOutputConfig
|
||||
from app.core.workflow.nodes import WorkflowState
|
||||
from app.core.workflow.nodes.base_config import VariableType
|
||||
from app.core.workflow.nodes.enums import NodeType
|
||||
from app.core.workflow.template_renderer import render_template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -156,137 +157,12 @@ class WorkflowExecutor:
|
||||
"error": result.get("error"),
|
||||
}
|
||||
|
||||
def _update_scope_activate(self, scope, status=None):
|
||||
"""
|
||||
Update the activation state of all End nodes based on a completed scope (node or variable).
|
||||
|
||||
Iterates over all End nodes in `self.end_outputs` and calls
|
||||
`update_activate` on each, which may:
|
||||
- Activate variable segments that depend on the completed node/scope.
|
||||
- Activate the entire End node output if all control conditions are met.
|
||||
|
||||
If any End node becomes active and `self.activate_end` is not yet set,
|
||||
this node will be marked as the currently active End node.
|
||||
|
||||
Args:
|
||||
scope (str): The node ID or scope that has completed execution.
|
||||
status (str | None): Optional status of the node (used for branch/control nodes).
|
||||
"""
|
||||
def _update_end_activate(self, node_id):
|
||||
for node in self.end_outputs.keys():
|
||||
self.end_outputs[node].update_activate(scope, status)
|
||||
self.end_outputs[node].update_activate(node_id)
|
||||
if self.end_outputs[node].activate and self.activate_end is None:
|
||||
self.activate_end = node
|
||||
|
||||
def _update_stream_output_status(self, activate, data):
|
||||
"""
|
||||
Update the stream output state of End nodes based on workflow state updates.
|
||||
|
||||
This method checks which nodes/scopes are activated and propagates
|
||||
activation to End nodes accordingly.
|
||||
|
||||
Args:
|
||||
activate (dict): Mapping of node_id -> bool indicating which nodes/scopes are activated.
|
||||
data (dict): Mapping of node_id -> node runtime data, including outputs.
|
||||
|
||||
Behavior:
|
||||
For each node in `data`:
|
||||
1. If the node is activated (`activate[node_id]` is True),
|
||||
retrieve its output status from `runtime_vars`.
|
||||
2. Call `_update_scope_activate` to propagate the activation
|
||||
to all relevant End nodes and update `self.activate_end`.
|
||||
"""
|
||||
for node_id in data.keys():
|
||||
if activate.get(node_id):
|
||||
node_output_status = (
|
||||
data[node_id]
|
||||
.get('runtime_vars', {})
|
||||
.get(node_id)
|
||||
.get("output")
|
||||
)
|
||||
self._update_scope_activate(node_id, status=node_output_status)
|
||||
|
||||
async def _emit_active_chunks(
|
||||
self,
|
||||
node_outputs: dict,
|
||||
variables: dict,
|
||||
force=False
|
||||
):
|
||||
"""
|
||||
Process and yield all currently active output segments for the currently active End node.
|
||||
|
||||
This method handles stream-mode output for an End node by iterating through its output segments
|
||||
(`OutputContent`). Only segments marked as active (`activate=True`) are processed, unless
|
||||
`force=True`, which allows all segments to be processed regardless of their activation state.
|
||||
|
||||
Behavior:
|
||||
1. Iterates from the current `cursor` position to the end of the outputs list.
|
||||
2. For each segment:
|
||||
- If the segment is literal text (`is_variable=False`), append it directly.
|
||||
- If the segment is a variable (`is_variable=True`), evaluate it using
|
||||
`evaluate_expression` with the given `node_outputs` and `variables`,
|
||||
then transform the result with `_trans_output_string`.
|
||||
3. Yield a stream event of type "message" containing the processed chunk.
|
||||
4. Move the `cursor` forward after processing each segment.
|
||||
5. When all segments have been processed, remove this End node from `end_outputs`
|
||||
and reset `activate_end` to None.
|
||||
|
||||
Args:
|
||||
node_outputs (dict): Current runtime node outputs, used for variable evaluation.
|
||||
variables (dict): Current runtime variables, used for variable evaluation.
|
||||
force (bool, default=False): If True, process segments even if `activate=False`.
|
||||
|
||||
Yields:
|
||||
dict: A stream event of type "message" containing the processed chunk.
|
||||
|
||||
Notes:
|
||||
- Segments that fail evaluation (ValueError) are skipped with a warning logged.
|
||||
- This method only processes the currently active End node (`self.activate_end`).
|
||||
- Use `force=True` for final emission regardless of activation state.
|
||||
"""
|
||||
|
||||
end_info = self.end_outputs[self.activate_end]
|
||||
|
||||
while end_info.cursor < len(end_info.outputs):
|
||||
final_chunk = ''
|
||||
current_segment = end_info.outputs[end_info.cursor]
|
||||
|
||||
if not current_segment.activate and not force:
|
||||
# Stop processing until this segment becomes active
|
||||
break
|
||||
|
||||
# Literal segment
|
||||
if not current_segment.is_variable:
|
||||
final_chunk += current_segment.literal
|
||||
else:
|
||||
# Variable segment: evaluate and transform
|
||||
try:
|
||||
chunk = evaluate_expression(
|
||||
current_segment.literal,
|
||||
variables=variables,
|
||||
node_outputs=node_outputs
|
||||
)
|
||||
chunk = self._trans_output_string(chunk)
|
||||
final_chunk += chunk
|
||||
except ValueError:
|
||||
# Log failed evaluation but continue streaming
|
||||
logger.warning(f"[STREAM] Failed to evaluate segment: {current_segment.literal}")
|
||||
|
||||
if final_chunk:
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": {
|
||||
"chunk": final_chunk
|
||||
}
|
||||
}
|
||||
|
||||
# Advance cursor after processing
|
||||
end_info.cursor += 1
|
||||
|
||||
# Remove End node from active tracking if all segments have been processed
|
||||
if end_info.cursor >= len(end_info.outputs):
|
||||
self.end_outputs.pop(self.activate_end)
|
||||
self.activate_end = None
|
||||
|
||||
@staticmethod
|
||||
def _trans_output_string(content):
|
||||
if isinstance(content, str):
|
||||
@@ -342,8 +218,14 @@ class WorkflowExecutor:
|
||||
|
||||
result = await graph.ainvoke(initial_state, config=self.checkpoint_config)
|
||||
full_content = ''
|
||||
for end_id in self.end_outputs.keys():
|
||||
full_content += result.get('runtime_vars', {}).get(end_id, {}).get('output', '')
|
||||
for end_info in self.end_outputs.values():
|
||||
output_template = "".join([output.literal for output in end_info.outputs])
|
||||
full_content += render_template(
|
||||
output_template,
|
||||
result.get("variables", {}),
|
||||
result.get("runtime_vars", {}),
|
||||
strict=False
|
||||
)
|
||||
result["messages"].extend(
|
||||
[
|
||||
{
|
||||
@@ -424,7 +306,7 @@ class WorkflowExecutor:
|
||||
try:
|
||||
chunk_count = 0
|
||||
full_content = ''
|
||||
self._update_scope_activate("sys")
|
||||
|
||||
async for event in graph.astream(
|
||||
initial_state,
|
||||
stream_mode=["updates", "debug", "custom"], # Use updates + debug + custom mode
|
||||
@@ -451,12 +333,9 @@ class WorkflowExecutor:
|
||||
if not end_info or end_info.cursor >= len(end_info.outputs):
|
||||
continue
|
||||
current_output = end_info.outputs[end_info.cursor]
|
||||
if current_output.is_variable and current_output.depends_on_scope(node_id):
|
||||
if current_output.is_variable and current_output.depends_on_node(node_id):
|
||||
if data.get("done"):
|
||||
end_info.cursor += 1
|
||||
if end_info.cursor >= len(end_info.outputs):
|
||||
self.end_outputs.pop(self.activate_end)
|
||||
self.activate_end = None
|
||||
else:
|
||||
full_content += data.get("chunk")
|
||||
yield {
|
||||
@@ -536,53 +415,91 @@ class WorkflowExecutor:
|
||||
|
||||
elif mode == "updates":
|
||||
# Handle state updates - store final state
|
||||
state = graph.get_state(config=self.checkpoint_config).values
|
||||
node_outputs = state.get("runtime_vars", {})
|
||||
variables = state.get("variables", {})
|
||||
activate = state.get("activate", {})
|
||||
for _, node_data in data.items():
|
||||
node_outputs |= node_data.get("runtime_vars", {})
|
||||
variables |= node_data.get("variables", {})
|
||||
|
||||
self._update_stream_output_status(activate, data)
|
||||
for node_id in data.keys():
|
||||
self._update_end_activate(node_id)
|
||||
wait = False
|
||||
while self.activate_end and not wait:
|
||||
async for msg_event in self._emit_active_chunks(
|
||||
node_outputs=node_outputs,
|
||||
variables=variables
|
||||
):
|
||||
full_content += msg_event["data"]['chunk']
|
||||
yield msg_event
|
||||
state = graph.get_state(config=self.checkpoint_config)
|
||||
node_outputs = state.values.get("runtime_vars", {})
|
||||
for _ in data.keys():
|
||||
node_outputs = node_outputs | data.get(_).get("runtime_vars", {})
|
||||
|
||||
if self.activate_end:
|
||||
while self.activate_end and not wait:
|
||||
message = ''
|
||||
logger.info(self.activate_end)
|
||||
end_info = self.end_outputs[self.activate_end]
|
||||
content = end_info.outputs[end_info.cursor]
|
||||
while content.activate:
|
||||
if not content.is_variable:
|
||||
full_content += content.literal
|
||||
message += content.literal
|
||||
else:
|
||||
try:
|
||||
chunk = evaluate_expression(
|
||||
content.literal,
|
||||
variables={},
|
||||
node_outputs=node_outputs
|
||||
)
|
||||
chunk = self._trans_output_string(chunk)
|
||||
message += chunk
|
||||
full_content += chunk
|
||||
except ValueError:
|
||||
pass
|
||||
end_info.cursor += 1
|
||||
if end_info.cursor == len(end_info.outputs):
|
||||
break
|
||||
content = end_info.outputs[end_info.cursor]
|
||||
if end_info.cursor != len(end_info.outputs):
|
||||
wait = True
|
||||
else:
|
||||
self._update_stream_output_status(activate, data)
|
||||
self.end_outputs.pop(self.activate_end)
|
||||
self.activate_end = None
|
||||
for node_id in data.keys():
|
||||
self._update_end_activate(node_id)
|
||||
if message:
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": {
|
||||
"chunk": message
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())} "
|
||||
f"- execution_id: {self.execution_id}")
|
||||
|
||||
result = graph.get_state(self.checkpoint_config).values
|
||||
node_outputs = result.get("runtime_vars", {})
|
||||
variables = result.get("variables", {})
|
||||
self.end_outputs = {
|
||||
node_id: node_info
|
||||
for node_id, node_info in self.end_outputs.items()
|
||||
if node_info.activate
|
||||
}
|
||||
|
||||
if self.end_outputs or self.activate_end:
|
||||
while self.activate_end:
|
||||
async for msg_event in self._emit_active_chunks(
|
||||
node_outputs=node_outputs,
|
||||
while self.activate_end:
|
||||
message = ''
|
||||
end_info = self.end_outputs[self.activate_end]
|
||||
content = end_info.outputs[end_info.cursor]
|
||||
if not content.is_variable:
|
||||
message += content.literal
|
||||
else:
|
||||
node_outputs = result.get("runtime_vars", {})
|
||||
variables = result.get("variables", {})
|
||||
try:
|
||||
chunk = evaluate_expression(
|
||||
content.literal,
|
||||
variables=variables,
|
||||
force=True
|
||||
):
|
||||
full_content += msg_event["data"]['chunk']
|
||||
yield msg_event
|
||||
|
||||
if not self.activate_end and self.end_outputs:
|
||||
node_outputs=node_outputs
|
||||
)
|
||||
chunk = self._trans_output_string(chunk)
|
||||
message += chunk
|
||||
full_content += chunk
|
||||
except ValueError:
|
||||
pass
|
||||
end_info.cursor += 1
|
||||
if end_info.cursor == len(end_info.outputs):
|
||||
self.end_outputs.pop(self.activate_end)
|
||||
self.activate_end = None
|
||||
if self.end_outputs:
|
||||
self.activate_end = list(self.end_outputs.keys())[0]
|
||||
if message:
|
||||
yield {
|
||||
"event": "message",
|
||||
"data": {
|
||||
"chunk": message
|
||||
}
|
||||
}
|
||||
|
||||
# 计算耗时
|
||||
end_time = datetime.datetime.now()
|
||||
|
||||
@@ -53,110 +53,114 @@ class OutputContent(BaseModel):
|
||||
)
|
||||
)
|
||||
|
||||
def depends_on_scope(self, scope: str) -> bool:
|
||||
def depends_on_node(self, node_id: str) -> bool:
|
||||
"""
|
||||
Check if this segment depends on a given scope.
|
||||
Check if this output segment depends on a specific node's variable.
|
||||
|
||||
This method examines the `literal` of the output segment to see if it
|
||||
contains a variable placeholder referencing the given node in the form:
|
||||
|
||||
{{ node_id.field_name }}
|
||||
|
||||
It uses a regular expression to match the exact node ID, avoiding
|
||||
false positives from substring matches (e.g., 'node1' should not match 'node10').
|
||||
|
||||
Args:
|
||||
scope (str): Node ID or special variable prefix (e.g., "sys").
|
||||
node_id (str): The ID of the node to check for in this segment's variable placeholders.
|
||||
|
||||
Returns:
|
||||
bool: True if this segment references the given scope.
|
||||
bool:
|
||||
- True if the segment contains a variable referencing the given node.
|
||||
- False otherwise.
|
||||
|
||||
Example:
|
||||
literal = "{{node1.name}}"
|
||||
|
||||
depends_on_node("node1") -> True
|
||||
depends_on_node("node2") -> False
|
||||
|
||||
Usage:
|
||||
This method is primarily used in stream mode to determine whether
|
||||
a particular variable output segment should be activated when a
|
||||
specific upstream node completes execution.
|
||||
"""
|
||||
pattern = rf"\{{\{{\s*{re.escape(scope)}\.[a-zA-Z0-9_]+\s*\}}\}}"
|
||||
return bool(re.search(pattern, self.literal))
|
||||
variable_pattern = rf"\{{\{{\s*{re.escape(node_id)}\.[a-zA-Z0-9_]+\s*\}}\}}"
|
||||
pattern = re.compile(variable_pattern)
|
||||
match = pattern.search(self.literal)
|
||||
if match:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class StreamOutputConfig(BaseModel):
|
||||
"""
|
||||
Streaming output configuration for an End node.
|
||||
|
||||
This configuration describes how the End node output behaves in streaming mode,
|
||||
including:
|
||||
- whether output emission is globally activated
|
||||
- which upstream branch/control nodes gate the activation
|
||||
- how each parsed output segment is streamed and activated
|
||||
This structure controls:
|
||||
- whether the End node output is globally active
|
||||
- which upstream branch nodes are responsible for activation
|
||||
- how each output segment behaves in streaming mode
|
||||
"""
|
||||
|
||||
activate: bool = Field(
|
||||
...,
|
||||
description=(
|
||||
"Global activation flag for the End node output.\n"
|
||||
"When False, output segments should not be emitted even if available.\n"
|
||||
"This flag typically becomes True once required control branch conditions "
|
||||
"are satisfied."
|
||||
"Global activation state of the End node output.\n"
|
||||
"If False, no output should be emitted until all control nodes are resolved."
|
||||
)
|
||||
)
|
||||
|
||||
control_nodes: dict[str, str] = Field(
|
||||
control_nodes: list[str] = Field(
|
||||
...,
|
||||
description=(
|
||||
"Control branch conditions for this End node output.\n"
|
||||
"Mapping of `branch_node_id -> expected_branch_label`.\n"
|
||||
"The End node output becomes globally active when a controlling branch node "
|
||||
"reports a matching completion status."
|
||||
"List of upstream branch node IDs that control this End node.\n"
|
||||
"Each node must signal completion before output becomes active."
|
||||
)
|
||||
)
|
||||
|
||||
outputs: list[OutputContent] = Field(
|
||||
...,
|
||||
description=(
|
||||
"Ordered list of output segments parsed from the output template.\n"
|
||||
"Each segment represents either a literal text block or a variable placeholder "
|
||||
"that may be activated independently."
|
||||
)
|
||||
description="Ordered list of output segments parsed from the output template."
|
||||
)
|
||||
|
||||
cursor: int = Field(
|
||||
...,
|
||||
description=(
|
||||
"Streaming cursor index.\n"
|
||||
"Indicates the next output segment index to be emitted.\n"
|
||||
"Segments with index < cursor are considered already streamed."
|
||||
"Indicates how many output segments have already been emitted."
|
||||
)
|
||||
)
|
||||
|
||||
def update_activate(self, scope: str, status=None):
|
||||
def update_activate(self, node_id):
|
||||
"""
|
||||
Update streaming activation state based on an upstream node or special variable.
|
||||
Update activation state based on an upstream node completion.
|
||||
|
||||
Args:
|
||||
scope (str):
|
||||
Identifier of the completed upstream entity.
|
||||
- If a control branch node, it should match a key in `control_nodes`.
|
||||
- If a variable placeholder (e.g., "sys.xxx"), it may appear in output segments.
|
||||
status (optional):
|
||||
Completion status of the control branch node.
|
||||
Required when `scope` refers to a control node.
|
||||
This method is typically called when a branch/control node finishes execution.
|
||||
|
||||
Behavior:
|
||||
1. Control branch nodes:
|
||||
- If `scope` matches a key in `control_nodes` and `status` matches the expected
|
||||
branch label, the End node output becomes globally active (`activate = True`).
|
||||
1. If the node is a control node:
|
||||
- Remove it from `control_nodes`
|
||||
- If all control nodes are resolved, activate the entire output
|
||||
|
||||
2. Variable output segments:
|
||||
- For each segment that is a variable (`is_variable=True`):
|
||||
- If the segment literal references `scope`, mark the segment as active.
|
||||
- This applies both to regular node variables (e.g., "node_id.field")
|
||||
and special system variables (e.g., "sys.xxx").
|
||||
|
||||
Notes:
|
||||
- This method does not emit output or advance the streaming cursor.
|
||||
- It only updates activation flags based on upstream events or special variables.
|
||||
2. Activate variable output segments that depend on this node:
|
||||
- If an output segment is a variable
|
||||
- And its literal references the completed node_id
|
||||
- Mark that segment as active
|
||||
"""
|
||||
|
||||
# Case 1: resolve control branch dependency
|
||||
if scope in self.control_nodes.keys():
|
||||
if status is None:
|
||||
raise RuntimeError("[Stream Output] Control node activation status not provided")
|
||||
if status == self.control_nodes[scope]:
|
||||
if node_id in self.control_nodes:
|
||||
self.control_nodes.remove(node_id)
|
||||
|
||||
# All branch constraints resolved → enable output
|
||||
if not self.control_nodes:
|
||||
self.activate = True
|
||||
|
||||
# Case 2: activate variable segments related to this node
|
||||
for i in range(len(self.outputs)):
|
||||
if (
|
||||
self.outputs[i].is_variable
|
||||
and self.outputs[i].depends_on_scope(scope)
|
||||
and self.outputs[i].depends_on_node(node_id)
|
||||
):
|
||||
self.outputs[i].activate = True
|
||||
|
||||
@@ -180,11 +184,11 @@ class GraphBuilder:
|
||||
self._find_upstream_branch_node = lru_cache(
|
||||
maxsize=len(self.nodes) * 2
|
||||
)(self._find_upstream_branch_node)
|
||||
self._analyze_end_node_output()
|
||||
|
||||
self.graph = StateGraph(WorkflowState)
|
||||
self.add_nodes()
|
||||
self.add_edges()
|
||||
self._analyze_end_node_output()
|
||||
# EDGES MUST BE ADDED AFTER NODES ARE ADDED.
|
||||
|
||||
@property
|
||||
@@ -212,53 +216,30 @@ class GraphBuilder:
|
||||
except KeyError:
|
||||
raise RuntimeError(f"Node not found: Id={node_id}")
|
||||
|
||||
def _find_upstream_branch_node(self, target_node: str) -> tuple[bool, tuple[tuple[str, str]]]:
|
||||
"""
|
||||
Recursively find all upstream branch (control) nodes that influence the execution
|
||||
of the given target node.
|
||||
def _find_upstream_branch_node(self, target_node: str) -> tuple[bool, tuple[str]]:
|
||||
"""Find upstream branch nodes for a given target node in the workflow graph.
|
||||
|
||||
This method walks upstream along the workflow graph starting from `target_node`.
|
||||
It distinguishes between:
|
||||
- branch nodes (node types listed in `BRANCH_NODES`)
|
||||
- non-branch nodes (ordinary processing nodes)
|
||||
This method identifies all upstream control (branch) nodes that can affect
|
||||
the execution of `target_node`. If `target_node` is reachable from a start
|
||||
node (i.e., a node with no upstream nodes), the method returns an empty tuple.
|
||||
|
||||
Traversal rules:
|
||||
1. For each immediate upstream node:
|
||||
- If it is a branch node, it is recorded as an affecting control node.
|
||||
- If it is a non-branch node, the traversal continues recursively upstream.
|
||||
2. If ANY upstream path reaches a START / CYCLE_START node without encountering
|
||||
a branch node, the traversal is considered invalid:
|
||||
- `has_branch` will be False
|
||||
- no branch nodes are returned.
|
||||
3. Only when ALL upstream non-branch paths eventually lead to at least one
|
||||
branch node will `has_branch` be True.
|
||||
|
||||
Special case:
|
||||
- If `target_node` has no upstream nodes AND its type is START or CYCLE_START,
|
||||
it is considered directly reachable from the workflow entry, and therefore
|
||||
has no controlling branch nodes.
|
||||
The function distinguishes between branch nodes (defined in `BRANCH_NODES`)
|
||||
and non-branch nodes, recursively traversing upstream through non-branch
|
||||
nodes. If any non-branch upstream path does not lead to a branch node,
|
||||
the result will indicate that no valid upstream branch node exists.
|
||||
|
||||
Args:
|
||||
target_node (str):
|
||||
The identifier of the node whose upstream control branches
|
||||
are to be resolved.
|
||||
target_node (str): The identifier of the target node.
|
||||
|
||||
Returns:
|
||||
tuple[bool, tuple[tuple[str, str]]]:
|
||||
- has_branch (bool):
|
||||
True if every upstream path from `target_node` encounters
|
||||
at least one branch node.
|
||||
False if any path reaches a start node without a branch.
|
||||
- branch_nodes (tuple[tuple[str, str]]):
|
||||
A deduplicated tuple of `(branch_node_id, branch_label)` pairs
|
||||
representing all branch nodes that can influence `target_node`.
|
||||
Returns an empty tuple if `has_branch` is False.
|
||||
tuple[bool, tuple[str]]:
|
||||
- has_branch (bool): True if all upstream non-branch paths lead to at least
|
||||
one branch node; False if any path reaches a start node without a branch.
|
||||
- branch_nodes (tuple[str]): A deduplicated tuple of upstream branch node IDs
|
||||
affecting `target_node`. Returns an empty tuple if `has_branch` is False.
|
||||
"""
|
||||
source_nodes = [
|
||||
{
|
||||
"id": edge.get("source"),
|
||||
"branch": edge.get("label")
|
||||
}
|
||||
edge.get("source")
|
||||
for edge in self.edges
|
||||
if edge.get("target") == target_node
|
||||
]
|
||||
@@ -268,13 +249,11 @@ class GraphBuilder:
|
||||
branch_nodes = []
|
||||
non_branch_nodes = []
|
||||
|
||||
for node_info in source_nodes:
|
||||
if self.get_node_type(node_info["id"]) in BRANCH_NODES:
|
||||
branch_nodes.append(
|
||||
(node_info["id"], node_info["branch"])
|
||||
)
|
||||
for node_id in source_nodes:
|
||||
if self.get_node_type(node_id) in BRANCH_NODES:
|
||||
branch_nodes.append(node_id)
|
||||
else:
|
||||
non_branch_nodes.append(node_info["id"])
|
||||
non_branch_nodes.append(node_id)
|
||||
|
||||
has_branch = True
|
||||
for node_id in non_branch_nodes:
|
||||
@@ -355,7 +334,7 @@ class GraphBuilder:
|
||||
activate=not has_branch,
|
||||
|
||||
# Branch nodes that control activation of this End node
|
||||
control_nodes=dict(control_nodes),
|
||||
control_nodes=list(control_nodes),
|
||||
|
||||
# Convert output segments into OutputContent objects
|
||||
outputs=list(
|
||||
@@ -383,7 +362,7 @@ class GraphBuilder:
|
||||
else:
|
||||
self.end_node_map[end_node_id] = StreamOutputConfig(
|
||||
activate=True,
|
||||
control_nodes={},
|
||||
control_nodes=[],
|
||||
outputs=list(
|
||||
[
|
||||
OutputContent(
|
||||
|
||||
@@ -25,6 +25,6 @@ class MemoryWriteNodeConfig(BaseNodeConfig):
|
||||
...
|
||||
)
|
||||
|
||||
config_id: UUID | int = Field(
|
||||
config_id: UUID = Field(
|
||||
...
|
||||
)
|
||||
|
||||
@@ -36,10 +36,9 @@ class MemoryReadNode(BaseNode):
|
||||
class MemoryWriteNode(BaseNode):
|
||||
def __init__(self, node_config: dict[str, Any], workflow_config: dict[str, Any]):
|
||||
super().__init__(node_config, workflow_config)
|
||||
self.typed_config: MemoryWriteNodeConfig | None = None
|
||||
self.typed_config = MemoryWriteNodeConfig(**self.config)
|
||||
|
||||
async def execute(self, state: WorkflowState) -> Any:
|
||||
self.typed_config = MemoryWriteNodeConfig(**self.config)
|
||||
end_user_id = self.get_variable("sys.user_id", state)
|
||||
|
||||
if not end_user_id:
|
||||
|
||||
@@ -16,8 +16,6 @@ from app.core.error_codes import BizCode, HTTP_MAPPING
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.logging_config import LoggingConfig, get_logger
|
||||
from app.core.response_utils import fail
|
||||
from app.core.models.scripts.loader import load_models
|
||||
from app.db import get_db_context
|
||||
|
||||
# Initialize logging system
|
||||
LoggingConfig.setup_logging()
|
||||
@@ -49,15 +47,6 @@ async def lifespan(app: FastAPI):
|
||||
else:
|
||||
logger.info("自动数据库升级已禁用 (DB_AUTO_UPGRADE=false)")
|
||||
|
||||
# 加载预定义模型
|
||||
logger.info("开始加载预定义模型...")
|
||||
try:
|
||||
with get_db_context() as db:
|
||||
result = load_models(db, silent=True)
|
||||
logger.info(f"预定义模型加载完成: 成功{result['success']}个, 跳过{result['skipped']}个, 失败{result['failed']}个")
|
||||
except Exception as e:
|
||||
logger.warning(f"加载预定义模型时出错: {str(e)}")
|
||||
|
||||
logger.info("应用程序启动完成")
|
||||
yield
|
||||
# 应用关闭事件
|
||||
|
||||
@@ -6,7 +6,7 @@ from .document_model import Document
|
||||
from .file_model import File
|
||||
from .file_metadata_model import FileMetadata
|
||||
from .generic_file_model import GenericFile
|
||||
from .models_model import ModelConfig, ModelProvider, ModelType, ModelApiKey, ModelBase, LoadBalanceStrategy
|
||||
from .models_model import ModelConfig, ModelProvider, ModelType, ModelApiKey
|
||||
from .memory_short_model import ShortTermMemory, LongTermMemory
|
||||
from .knowledgeshare_model import KnowledgeShare
|
||||
from .app_model import App
|
||||
@@ -79,6 +79,4 @@ __all__ = [
|
||||
"AuthType",
|
||||
"ExecutionStatus",
|
||||
"MemoryPerceptualModel",
|
||||
"ModelBase",
|
||||
"LoadBalanceStrategy"
|
||||
]
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from enum import StrEnum
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum, UniqueConstraint, Integer, ARRAY, Table
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db import Base
|
||||
|
||||
|
||||
class BaseModel(Base):
|
||||
"""基础模型(抽象类,提取公共字段)"""
|
||||
__abstract__ = True # 标记为抽象类,不生成表
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间")
|
||||
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="更新时间")
|
||||
is_active = Column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
|
||||
|
||||
class ModelType(StrEnum):
|
||||
"""模型类型枚举"""
|
||||
LLM = "llm"
|
||||
CHAT = "chat"
|
||||
EMBEDDING = "embedding"
|
||||
RERANK = "rerank"
|
||||
# TTS = "tts"
|
||||
# SPEECH2TEXT = "speech2text"
|
||||
# IMAGE = "image"
|
||||
# AUDIO = "audio"
|
||||
# VISION = "vision"
|
||||
|
||||
|
||||
class ModelProvider(StrEnum):
|
||||
@@ -45,36 +30,16 @@ class ModelProvider(StrEnum):
|
||||
XINFERENCE = "xinference"
|
||||
GPUSTACK = "gpustack"
|
||||
BEDROCK = "bedrock"
|
||||
COMPOSITE = "composite"
|
||||
|
||||
|
||||
class LoadBalanceStrategy(StrEnum):
|
||||
"""API Key负载均衡策略枚举"""
|
||||
ROUND_ROBIN = "round_robin" # 轮询
|
||||
NONE = "none" # 无
|
||||
|
||||
|
||||
# 多对多关联表
|
||||
model_config_api_key_association = Table(
|
||||
'model_config_api_key_association',
|
||||
Base.metadata,
|
||||
Column('model_config_id', UUID(as_uuid=True), ForeignKey('model_configs.id'), primary_key=True),
|
||||
Column('api_key_id', UUID(as_uuid=True), ForeignKey('model_api_keys.id'), primary_key=True),
|
||||
Column('created_at', DateTime, default=datetime.datetime.now)
|
||||
)
|
||||
|
||||
|
||||
class ModelConfig(BaseModel):
|
||||
class ModelConfig(Base):
|
||||
"""模型配置表"""
|
||||
__tablename__ = "model_configs"
|
||||
|
||||
model_id = Column(UUID(as_uuid=True), ForeignKey("model_bases.id"), nullable=True, index=True, comment="基础模型ID")
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True, comment="租户ID")
|
||||
logo = Column(String(255), nullable=True, comment="模型logo图片URL")
|
||||
name = Column(String, nullable=False, comment="模型显示名称")
|
||||
provider = Column(String, nullable=False, comment="供应商", server_default=ModelProvider.COMPOSITE)
|
||||
type = Column(String, nullable=False, index=True, comment="模型类型")
|
||||
is_composite = Column(Boolean, default=False, server_default="true", nullable=False, comment="是否为组合模型")
|
||||
description = Column(String, comment="模型描述")
|
||||
|
||||
# 模型配置参数
|
||||
@@ -91,29 +56,29 @@ class ModelConfig(BaseModel):
|
||||
# context_length = Column(String, comment="上下文长度")
|
||||
|
||||
# 状态管理
|
||||
is_active = Column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
is_public = Column(Boolean, default=False, nullable=False, comment="是否公开")
|
||||
load_balance_strategy = Column(String, nullable=True, comment="负载均衡策略", default=LoadBalanceStrategy.NONE,
|
||||
server_default=LoadBalanceStrategy.NONE)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间")
|
||||
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="更新时间")
|
||||
|
||||
# 关联关系
|
||||
model_base = relationship("ModelBase", back_populates="configs")
|
||||
api_keys = relationship(
|
||||
"ModelApiKey",
|
||||
secondary=model_config_api_key_association,
|
||||
back_populates="model_configs"
|
||||
)
|
||||
api_keys = relationship("ModelApiKey", back_populates="model_config", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ModelConfig(id={self.id}, name={self.name}, type={self.type})>"
|
||||
|
||||
|
||||
class ModelApiKey(BaseModel):
|
||||
class ModelApiKey(Base):
|
||||
"""模型API密钥表"""
|
||||
__tablename__ = "model_api_keys"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||
model_config_id = Column(UUID(as_uuid=True), ForeignKey("model_configs.id"), nullable=False, comment="模型配置ID")
|
||||
|
||||
# API Key 信息
|
||||
model_name = Column(String, nullable=False, comment="模型实际名称")
|
||||
description = Column(String, comment="备注")
|
||||
provider = Column(String, nullable=False, comment="API Key提供商")
|
||||
api_key = Column(String, nullable=False, comment="API密钥")
|
||||
api_base = Column(String, comment="API基础URL")
|
||||
@@ -126,42 +91,15 @@ class ModelApiKey(BaseModel):
|
||||
last_used_at = Column(DateTime, comment="最后使用时间")
|
||||
|
||||
# 状态管理
|
||||
is_active = Column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
priority = Column(String, default="1", comment="优先级")
|
||||
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间")
|
||||
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="更新时间")
|
||||
|
||||
# 关联关系
|
||||
model_configs = relationship(
|
||||
"ModelConfig",
|
||||
secondary=model_config_api_key_association,
|
||||
back_populates="api_keys"
|
||||
)
|
||||
|
||||
model_config = relationship("ModelConfig", back_populates="api_keys")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ModelApiKey(id={self.id}, model_name={self.model_name}, provider={self.provider})>"
|
||||
|
||||
|
||||
class ModelBase(Base):
|
||||
"""基础模型信息表(模型广场)"""
|
||||
__tablename__ = "model_bases"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||
logo = Column(String(255), nullable=True, comment="模型logo图片URL")
|
||||
name = Column(String, nullable=False, comment="模型唯一标识(如gpt-3.5-turbo)")
|
||||
type = Column(String, nullable=False, index=True, comment="模型类型")
|
||||
provider = Column(String, nullable=False, index=True)
|
||||
description = Column(Text, comment="模型描述")
|
||||
is_deprecated = Column(Boolean, default=False, nullable=False, comment="是否弃用")
|
||||
is_official = Column(Boolean, default=True, comment="是否供应商官方模型(区分自定义)")
|
||||
tags = Column(ARRAY(String), default=list, nullable=False, comment="模型标签(如['聊天', '创作'])")
|
||||
add_count = Column(Integer, default=0, nullable=False, comment="模型被用户添加的次数")
|
||||
created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间", server_default=func.now())
|
||||
|
||||
# 关联关系
|
||||
configs = relationship("ModelConfig", back_populates="model_base", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("name", "provider", name="uk_model_name_provider"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ModelBase(name={self.name}, provider={self.provider}, type={self.type})>"
|
||||
return f"<ModelApiKey(id={self.id}, model_name={self.model_name}, provider={self.provider}, model_config_id={self.model_config_id})>"
|
||||
|
||||
@@ -24,16 +24,12 @@ from app.schemas.memory_storage_schema import (
|
||||
from sqlalchemy import desc, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
# 获取数据库专用日志器
|
||||
db_logger = get_db_logger()
|
||||
# 获取配置专用日志器
|
||||
config_logger = get_config_logger()
|
||||
|
||||
TABLE_NAME = "memory_config"
|
||||
|
||||
|
||||
class MemoryConfigRepository:
|
||||
"""记忆配置Repository
|
||||
|
||||
@@ -191,6 +187,7 @@ class MemoryConfigRepository:
|
||||
raise RuntimeError("reflection config not found")
|
||||
return memory_config
|
||||
|
||||
|
||||
@staticmethod
|
||||
def build_select_all(workspace_id: uuid.UUID) -> Tuple[str, Dict]:
|
||||
"""构建查询所有配置的语句(SQLAlchemy text() 命名参数)
|
||||
@@ -290,6 +287,7 @@ class MemoryConfigRepository:
|
||||
db_logger.error(f"更新记忆配置失败: config_id={update.config_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_extracted(db: Session, update: ConfigUpdateExtracted) -> Optional[MemoryConfig]:
|
||||
"""更新记忆萃取引擎配置
|
||||
@@ -412,7 +410,7 @@ class MemoryConfigRepository:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_extracted_config(db: Session, config_id: UUID | int) -> Optional[Dict]:
|
||||
def get_extracted_config(db: Session, config_id: UUID) -> Optional[Dict]:
|
||||
"""获取萃取配置,通过主键查询某条配置
|
||||
|
||||
Args:
|
||||
@@ -422,8 +420,8 @@ class MemoryConfigRepository:
|
||||
Returns:
|
||||
Optional[Dict]: 萃取配置字典,不存在则返回None
|
||||
"""
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
db_logger.debug(f"查询萃取配置: config_id={config_id}")
|
||||
|
||||
try:
|
||||
db_config = db.query(MemoryConfig).filter(MemoryConfig.config_id == config_id).first()
|
||||
if not db_config:
|
||||
@@ -516,27 +514,26 @@ class MemoryConfigRepository:
|
||||
except Exception as e:
|
||||
db_logger.error(f"根据ID查询记忆配置失败: config_id={config_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_config_with_workspace(db: Session, config_id: uuid.UUID | int | str) -> Optional[tuple]:
|
||||
def get_config_with_workspace(db: Session, config_id: uuid.UUID) -> Optional[tuple]:
|
||||
"""Get memory config and its associated workspace information
|
||||
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
config_id: Configuration ID
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[tuple]: (MemoryConfig, Workspace) tuple, None if not found
|
||||
|
||||
Raises:
|
||||
ValueError: Raised when config exists but workspace doesn't
|
||||
"""
|
||||
import time
|
||||
|
||||
from app.models.workspace_model import Workspace
|
||||
|
||||
|
||||
start_time = time.time()
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
|
||||
|
||||
# Log configuration loading start
|
||||
config_logger.info(
|
||||
"Loading configuration with workspace",
|
||||
@@ -545,16 +542,17 @@ class MemoryConfigRepository:
|
||||
"config_id": config_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
db_logger.debug(f"Querying memory config and workspace: config_id={config_id}")
|
||||
|
||||
|
||||
try:
|
||||
# Use join query to get both config and workspace
|
||||
result = db.query(MemoryConfig, Workspace).join(
|
||||
Workspace, MemoryConfig.workspace_id == Workspace.id
|
||||
).filter(MemoryConfig.config_id == config_id).first()
|
||||
|
||||
elapsed_ms = (time.time() - start_time) * 1000
|
||||
|
||||
|
||||
if not result:
|
||||
# Check if config exists but workspace is missing
|
||||
config_only = db.query(MemoryConfig).filter(MemoryConfig.config_id == config_id).first()
|
||||
@@ -583,11 +581,9 @@ class MemoryConfigRepository:
|
||||
"elapsed_ms": elapsed_ms
|
||||
}
|
||||
)
|
||||
db_logger.error(
|
||||
f"Memory config {config_id} references non-existent workspace {config_only.workspace_id}")
|
||||
raise ValueError(
|
||||
f"Workspace {config_only.workspace_id} not found for configuration {config_id}")
|
||||
|
||||
db_logger.error(f"Memory config {config_id} references non-existent workspace {config_only.workspace_id}")
|
||||
raise ValueError(f"Workspace {config_only.workspace_id} not found for configuration {config_id}")
|
||||
|
||||
config_logger.debug(
|
||||
"Configuration not found",
|
||||
extra={
|
||||
@@ -599,9 +595,9 @@ class MemoryConfigRepository:
|
||||
)
|
||||
db_logger.debug(f"Memory config not found: config_id={config_id}")
|
||||
return None
|
||||
|
||||
|
||||
config, workspace = result
|
||||
|
||||
|
||||
# Log successful configuration loading
|
||||
config_logger.info(
|
||||
"Configuration with workspace loaded successfully",
|
||||
@@ -616,17 +612,16 @@ class MemoryConfigRepository:
|
||||
"elapsed_ms": elapsed_ms
|
||||
}
|
||||
)
|
||||
|
||||
db_logger.debug(
|
||||
f"Memory config and workspace query successful: config={config.config_name}, workspace={workspace.name}")
|
||||
|
||||
db_logger.debug(f"Memory config and workspace query successful: config={config.config_name}, workspace={workspace.name}")
|
||||
return (config, workspace)
|
||||
|
||||
|
||||
except ValueError:
|
||||
# Re-raise known business exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
elapsed_ms = (time.time() - start_time) * 1000
|
||||
|
||||
|
||||
config_logger.error(
|
||||
"Failed to load configuration with workspace",
|
||||
extra={
|
||||
@@ -639,9 +634,9 @@ class MemoryConfigRepository:
|
||||
},
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
db_logger.error(f"Failed to query memory config and workspace: config_id={config_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_all(db: Session, workspace_id: Optional[uuid.UUID] = None) -> List[MemoryConfig]:
|
||||
"""获取所有配置参数
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||
from sqlalchemy import and_, or_, func, desc, select
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
import uuid
|
||||
|
||||
from app.models.models_model import ModelConfig, ModelApiKey, ModelType, ModelBase, model_config_api_key_association
|
||||
from app.models.models_model import ModelConfig, ModelApiKey, ModelType
|
||||
from app.schemas.model_schema import (
|
||||
ModelConfigUpdate, ModelApiKeyCreate, ModelApiKeyUpdate,
|
||||
ModelConfigQuery, ModelConfigQueryNew
|
||||
ModelConfigQuery
|
||||
)
|
||||
from app.core.logging_config import get_db_logger
|
||||
|
||||
@@ -107,80 +107,6 @@ class ModelConfigRepository:
|
||||
def get_list(db: Session, query: ModelConfigQuery, tenant_id: uuid.UUID | None = None) -> Tuple[List[ModelConfig], int]:
|
||||
"""获取模型配置列表"""
|
||||
db_logger.debug(f"查询模型配置列表: {query.dict()}, tenant_id={tenant_id}")
|
||||
|
||||
try:
|
||||
# 构建查询条件
|
||||
filters = []
|
||||
|
||||
# 添加租户过滤(查询本租户的模型或公开模型)
|
||||
if tenant_id:
|
||||
filters.append(
|
||||
or_(
|
||||
ModelConfig.tenant_id == tenant_id,
|
||||
ModelConfig.is_public
|
||||
)
|
||||
)
|
||||
|
||||
# 支持多个 type 值(使用 IN 查询)
|
||||
# 兼容 chat 和 llm 类型:如果查询包含其中一个,则同时匹配两者
|
||||
if query.type:
|
||||
type_values = list(query.type)
|
||||
# 如果包含 chat 或 llm,则同时包含两者
|
||||
if ModelType.CHAT in type_values or ModelType.LLM in type_values:
|
||||
if ModelType.CHAT not in type_values:
|
||||
type_values.append(ModelType.CHAT)
|
||||
if ModelType.LLM not in type_values:
|
||||
type_values.append(ModelType.LLM)
|
||||
filters.append(ModelConfig.type.in_(type_values))
|
||||
|
||||
if query.is_active is not None:
|
||||
filters.append(ModelConfig.is_active == query.is_active)
|
||||
|
||||
if query.is_public is not None:
|
||||
filters.append(ModelConfig.is_public == query.is_public)
|
||||
|
||||
if query.search:
|
||||
# 搜索逻辑需要join ModelApiKey表来搜索model_name
|
||||
search_filter = or_(
|
||||
ModelConfig.name.ilike(f"%{query.search}%"),
|
||||
# ModelConfig.description.ilike(f"%{query.search}%")
|
||||
)
|
||||
filters.append(search_filter)
|
||||
|
||||
# 构建基础查询
|
||||
base_query = db.query(ModelConfig).options(
|
||||
joinedload(ModelConfig.api_keys)
|
||||
)
|
||||
|
||||
# 如果需要按provider筛选,需要join ModelApiKey表
|
||||
if query.provider:
|
||||
base_query = base_query.join(ModelApiKey).filter(
|
||||
ModelApiKey.provider == query.provider
|
||||
).distinct()
|
||||
|
||||
if filters:
|
||||
base_query = base_query.filter(and_(*filters))
|
||||
|
||||
# 获取总数
|
||||
total = base_query.count()
|
||||
|
||||
# 分页查询
|
||||
models = base_query.order_by(desc(ModelConfig.created_at)).offset(
|
||||
(query.page - 1) * query.pagesize
|
||||
).limit(query.pagesize).all()
|
||||
|
||||
db_logger.debug(f"模型配置列表查询成功: 总数={total}, 当前页={len(models)}, type筛选={query.type}")
|
||||
return models, total
|
||||
|
||||
except Exception as e:
|
||||
db_logger.error(f"查询模型配置列表失败: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_list_new(db: Session, query: ModelConfigQueryNew, tenant_id: uuid.UUID | None = None) -> tuple[
|
||||
dict[str, list[ModelConfig]], Any]:
|
||||
"""获取模型配置列表"""
|
||||
db_logger.debug(f"查询模型配置列表: {query.model_dump()}, tenant_id={tenant_id}")
|
||||
|
||||
try:
|
||||
# 构建查询条件
|
||||
@@ -212,15 +138,13 @@ class ModelConfigRepository:
|
||||
|
||||
if query.is_public is not None:
|
||||
filters.append(ModelConfig.is_public == query.is_public)
|
||||
|
||||
if query.is_composite is not None:
|
||||
filters.append(ModelConfig.is_composite == query.is_composite)
|
||||
|
||||
if query.provider:
|
||||
filters.append(ModelConfig.provider == query.provider)
|
||||
|
||||
if query.search:
|
||||
search_filter = ModelConfig.name.ilike(f"%{query.search}%")
|
||||
# 搜索逻辑需要join ModelApiKey表来搜索model_name
|
||||
search_filter = or_(
|
||||
ModelConfig.name.ilike(f"%{query.search}%"),
|
||||
# ModelConfig.description.ilike(f"%{query.search}%")
|
||||
)
|
||||
filters.append(search_filter)
|
||||
|
||||
# 构建基础查询
|
||||
@@ -228,30 +152,28 @@ class ModelConfigRepository:
|
||||
joinedload(ModelConfig.api_keys)
|
||||
)
|
||||
|
||||
# 如果需要按provider筛选,需要join ModelApiKey表
|
||||
if query.provider:
|
||||
base_query = base_query.join(ModelApiKey).filter(
|
||||
ModelApiKey.provider == query.provider
|
||||
).distinct()
|
||||
|
||||
if filters:
|
||||
base_query = base_query.filter(and_(*filters))
|
||||
|
||||
# 获取总数
|
||||
total = base_query.count()
|
||||
|
||||
query_results = base_query.order_by(desc(ModelConfig.created_at)).all()
|
||||
|
||||
provider_groups: Dict[str, List[ModelConfig]] = {}
|
||||
for model_config in query_results:
|
||||
provider = model_config.provider
|
||||
if provider not in provider_groups:
|
||||
provider_groups[provider] = []
|
||||
provider_groups[provider].append(model_config)
|
||||
|
||||
db_logger.debug(
|
||||
f"模型配置列表查询成功: 总数={total}, "
|
||||
f"分组数={len(provider_groups)}, "
|
||||
f"各分组模型数={[len(v) for v in provider_groups.values()]}, "
|
||||
f"type筛选={query.type}")
|
||||
return provider_groups, total
|
||||
# 分页查询
|
||||
models = base_query.order_by(desc(ModelConfig.updated_at)).offset(
|
||||
(query.page - 1) * query.pagesize
|
||||
).limit(query.pagesize).all()
|
||||
|
||||
db_logger.debug(f"模型配置列表查询成功: 总数={total}, 当前页={len(models)}, type筛选={query.type}")
|
||||
return models, total
|
||||
|
||||
except Exception as e:
|
||||
db_logger.error(f"查询模型配置列表失败(按provider分组/无分页): {str(e)}")
|
||||
db_logger.error(f"查询模型配置列表失败: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@@ -319,7 +241,7 @@ class ModelConfigRepository:
|
||||
return None
|
||||
|
||||
# 更新字段
|
||||
update_data = model_data.model_dump(exclude_unset=True)
|
||||
update_data = model_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_model, field, value)
|
||||
|
||||
@@ -381,18 +303,8 @@ class ModelConfigRepository:
|
||||
# 按提供商统计 - 现在从ModelApiKey表获取
|
||||
provider_stats = {}
|
||||
provider_results = db.query(
|
||||
# 保留 provider 字段
|
||||
ModelApiKey.provider,
|
||||
# 统计中间表中 唯一的 model_config_id 数量(替换原 ModelApiKey.model_config_id)
|
||||
func.count(func.distinct(model_config_api_key_association.c.model_config_id))
|
||||
).join(
|
||||
# 联表:ModelApiKey <-> 中间表(多对多关联)
|
||||
model_config_api_key_association,
|
||||
ModelApiKey.id == model_config_api_key_association.c.api_key_id
|
||||
).group_by(
|
||||
# 按 provider 分组(保留原有逻辑)
|
||||
ModelApiKey.provider
|
||||
).all()
|
||||
ModelApiKey.provider, func.count(func.distinct(ModelApiKey.model_config_id))
|
||||
).group_by(ModelApiKey.provider).all()
|
||||
|
||||
for provider, count in provider_results:
|
||||
provider_stats[provider.value] = count
|
||||
@@ -413,38 +325,6 @@ class ModelConfigRepository:
|
||||
db_logger.error(f"获取模型统计信息失败: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_model_config_ids_by_provider(
|
||||
db: Session,
|
||||
tenant_id: uuid.UUID,
|
||||
provider: Any
|
||||
) -> List[uuid.UUID]:
|
||||
"""根据tenant_id和provider获取model_config_id列表"""
|
||||
db_logger.debug(f"查询model_config_id列表: tenant_id={tenant_id}, provider={provider}")
|
||||
|
||||
try:
|
||||
# 查询ModelConfig关联的ModelApiKey,筛选出匹配的model_config_id
|
||||
model_config_ids = db.query(ModelConfig.id).join(
|
||||
ModelBase, ModelConfig.model_id == ModelBase.id
|
||||
).filter(
|
||||
and_(
|
||||
or_(
|
||||
ModelConfig.tenant_id == tenant_id,
|
||||
ModelConfig.is_public
|
||||
),
|
||||
ModelBase.provider == provider,
|
||||
ModelConfig.is_active,
|
||||
~ModelConfig.is_composite
|
||||
)
|
||||
).distinct().all()
|
||||
|
||||
db_logger.debug(f"查询成功: 数量={len(model_config_ids)}")
|
||||
return [row[0] for row in model_config_ids]
|
||||
|
||||
except Exception as e:
|
||||
db_logger.error(f"查询model_config_id列表失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
class ModelApiKeyRepository:
|
||||
"""模型API Key Repository"""
|
||||
@@ -469,14 +349,7 @@ class ModelApiKeyRepository:
|
||||
db_logger.debug(f"根据模型配置ID查询API Key: model_config_id={model_config_id}")
|
||||
|
||||
try:
|
||||
from app.models.models_model import ModelConfig, model_config_api_key_association
|
||||
|
||||
query = db.query(ModelApiKey).join(
|
||||
model_config_api_key_association,
|
||||
ModelApiKey.id == model_config_api_key_association.c.api_key_id
|
||||
).filter(
|
||||
model_config_api_key_association.c.model_config_id == model_config_id
|
||||
)
|
||||
query = db.query(ModelApiKey).filter(ModelApiKey.model_config_id == model_config_id)
|
||||
|
||||
if is_active:
|
||||
query = query.filter(ModelApiKey.is_active)
|
||||
@@ -495,20 +368,8 @@ class ModelApiKeyRepository:
|
||||
db_logger.debug(f"创建API Key: {api_key_data.provider}")
|
||||
|
||||
try:
|
||||
from app.models.models_model import ModelConfig
|
||||
|
||||
# 创建API Key,不包含model_config_ids
|
||||
api_key_dict = api_key_data.model_dump(exclude={"model_config_ids"})
|
||||
db_api_key = ModelApiKey(**api_key_dict)
|
||||
db_api_key = ModelApiKey(**api_key_data.dict())
|
||||
db.add(db_api_key)
|
||||
db.flush() # 获取生成的ID
|
||||
|
||||
# 关联ModelConfig
|
||||
if api_key_data.model_config_ids:
|
||||
for model_config_id in api_key_data.model_config_ids:
|
||||
model_config = db.query(ModelConfig).filter(ModelConfig.id == model_config_id).first()
|
||||
if model_config:
|
||||
db_api_key.model_configs.append(model_config)
|
||||
|
||||
db_logger.info(f"API Key已添加到会话: {db_api_key.provider}")
|
||||
return db_api_key
|
||||
@@ -530,7 +391,7 @@ class ModelApiKeyRepository:
|
||||
return None
|
||||
|
||||
# 更新字段
|
||||
update_data = api_key_data.model_dump(exclude_unset=True)
|
||||
update_data = api_key_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_api_key, field, value)
|
||||
|
||||
@@ -590,92 +451,4 @@ class ModelApiKeyRepository:
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
db_logger.error(f"更新API Key使用统计失败: api_key_id={api_key_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
class ModelBaseRepository:
|
||||
"""基础模型Repository"""
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, model_base_id: uuid.UUID) -> Optional['ModelBase']:
|
||||
return db.query(ModelBase).filter(ModelBase.id == model_base_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_list(db: Session, query: 'ModelBaseQuery') -> List['ModelBase']:
|
||||
|
||||
filters = []
|
||||
if query.type:
|
||||
filters.append(ModelBase.type == query.type)
|
||||
if query.provider:
|
||||
filters.append(ModelBase.provider == query.provider)
|
||||
if query.is_official is not None:
|
||||
filters.append(ModelBase.is_official == query.is_official)
|
||||
if query.is_deprecated is not None:
|
||||
filters.append(ModelBase.is_deprecated == query.is_deprecated)
|
||||
if query.search:
|
||||
filters.append(or_(
|
||||
ModelBase.name.ilike(f"%{query.search}%"),
|
||||
# ModelBase.description.ilike(f"%{query.search}%")
|
||||
))
|
||||
|
||||
q = db.query(ModelBase)
|
||||
if filters:
|
||||
q = q.filter(and_(*filters))
|
||||
|
||||
return q.order_by(ModelBase.add_count.desc(), ModelBase.created_at.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, data: dict) -> 'ModelBase':
|
||||
model_base = ModelBase(**data)
|
||||
db.add(model_base)
|
||||
return model_base
|
||||
|
||||
@staticmethod
|
||||
def get_by_name_and_provider(db: Session, name: str, provider: str) -> Optional['ModelBase']:
|
||||
return db.query(ModelBase).filter(
|
||||
ModelBase.name == name,
|
||||
ModelBase.provider == provider
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, model_base_id: uuid.UUID, data: dict) -> Optional['ModelBase']:
|
||||
model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first()
|
||||
if not model_base:
|
||||
return None
|
||||
for key, value in data.items():
|
||||
setattr(model_base, key, value)
|
||||
|
||||
# 同步更新绑定的非组合模型配置
|
||||
if any(k in data for k in ['name', 'description', 'logo']):
|
||||
db.query(ModelConfig).filter(
|
||||
ModelConfig.model_id == model_base_id,
|
||||
ModelConfig.is_composite == False
|
||||
).update({
|
||||
k: v for k, v in data.items()
|
||||
if k in ['name', 'description', 'logo']
|
||||
}, synchronize_session=False)
|
||||
|
||||
return model_base
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, model_base_id: uuid.UUID) -> bool:
|
||||
model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first()
|
||||
if not model_base:
|
||||
return False
|
||||
db.delete(model_base)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def increment_add_count(db: Session, model_base_id: uuid.UUID) -> bool:
|
||||
model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first()
|
||||
if not model_base:
|
||||
return False
|
||||
model_base.add_count += 1
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def check_added_by_tenant(db: Session, model_base_id: uuid.UUID, tenant_id: uuid.UUID) -> bool:
|
||||
return db.query(ModelConfig).filter(
|
||||
ModelConfig.model_id == model_base_id,
|
||||
ModelConfig.tenant_id == tenant_id
|
||||
).first() is not None
|
||||
raise
|
||||
@@ -1,7 +1,5 @@
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
@@ -12,7 +10,7 @@ class OptimizationStrategy(str, Enum):
|
||||
ACCURACY_FIRST = "accuracy_first"
|
||||
BALANCED = "balanced"
|
||||
class Memory_Reflection(BaseModel):
|
||||
config_id: Union[uuid.UUID, int, str] = None
|
||||
config_id: Optional[UUID] = None
|
||||
reflection_enabled: bool
|
||||
reflection_period_in_hours: str
|
||||
reflexion_range: Optional[str] = "partial"
|
||||
|
||||
@@ -147,7 +147,7 @@ class ReflexionResultSchema(BaseModel):
|
||||
# Composite key identifying a config row
|
||||
class ConfigKey(BaseModel): # 配置参数键模型
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
config_id:Union[uuid.UUID, int, str] = Field(..., description="配置唯一标识(UUID或int)")
|
||||
config_id: uuid.UUID = Field("config_id", description="配置唯一标识(UUID)")
|
||||
user_id: str = Field("user_id", description="用户标识(字符串)")
|
||||
apply_id: str = Field("apply_id", description="应用或场景标识(字符串)")
|
||||
|
||||
@@ -238,17 +238,17 @@ class ConfigParamsCreate(BaseModel): # 创建配置参数模型(仅 body,
|
||||
class ConfigParamsDelete(BaseModel): # 删除配置参数模型(请求体)
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
# config_name: str = Field("配置名称", description="配置名称(字符串)")
|
||||
config_id:Union[uuid.UUID, int, str] = Field(..., description="配置ID(支持UUID、整数或字符串)")
|
||||
config_id: uuid.UUID = Field("配置ID", description="配置ID(UUID)")
|
||||
|
||||
|
||||
class ConfigUpdate(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
|
||||
config_id: Union[uuid.UUID, int, str] = None
|
||||
config_id: Optional[uuid.UUID] = None
|
||||
config_name: str = Field("配置名称", description="配置名称(字符串)")
|
||||
config_desc: str = Field("配置描述", description="配置描述(字符串)")
|
||||
|
||||
|
||||
class ConfigUpdateExtracted(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
|
||||
config_id:Union[uuid.UUID, int, str] = None
|
||||
config_id: Optional[uuid.UUID] = None
|
||||
llm_id: Optional[str] = Field(None, description="LLM模型配置ID")
|
||||
embedding_id: Optional[str] = Field(None, description="嵌入模型配置ID")
|
||||
rerank_id: Optional[str] = Field(None, description="重排序模型配置ID")
|
||||
@@ -315,14 +315,14 @@ class ConfigUpdateExtracted(BaseModel): # 更新记忆萃取引擎配置参数
|
||||
|
||||
class ConfigUpdateForget(BaseModel): # 更新遗忘引擎配置参数时使用的模型
|
||||
# 遗忘引擎配置参数更新模型
|
||||
config_id:Union[uuid.UUID, int, str] = None
|
||||
config_id: Optional[uuid.UUID] = None
|
||||
lambda_time: Optional[float] = Field(0.5, ge=0.0, le=1.0, description="最低保持度,0-1 小数;默认 0.5")
|
||||
lambda_mem: Optional[float] = Field(0.5, ge=0.0, le=1.0, description="遗忘率,0-1 小数;默认 0.5")
|
||||
offset: Optional[float] = Field(0.0, ge=0.0, le=1.0, description="偏移度,0-1 小数;默认 0.0")
|
||||
|
||||
|
||||
class ConfigPilotRun(BaseModel): # 试运行触发请求模型
|
||||
config_id:Union[uuid.UUID, int, str] = Field(..., description="配置ID(唯一,支持UUID、整数或字符串)")
|
||||
config_id: uuid.UUID = Field(..., description="配置ID(唯一)")
|
||||
dialogue_text: str = Field(..., description="前端传入的对话文本,格式如 '用户: ...\nAI: ...' 可多行,试运行必填")
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
@@ -330,7 +330,7 @@ class ConfigPilotRun(BaseModel): # 试运行触发请求模型
|
||||
class ConfigFilter(BaseModel): # 查询配置参数时使用的模型
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
config_id: Union[uuid.UUID, int, str] = None
|
||||
config_id: Optional[uuid.UUID] = None
|
||||
user_id: Optional[str] = None
|
||||
apply_id: Optional[str] = None
|
||||
|
||||
@@ -406,7 +406,7 @@ class ForgettingConfigResponse(BaseModel):
|
||||
"""遗忘引擎配置响应模型"""
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
config_id: Union[uuid.UUID, int, str] = Field(..., description="配置ID(支持UUID、整数或字符串)")
|
||||
config_id: uuid.UUID = Field(..., description="配置ID")
|
||||
decay_constant: float = Field(..., description="衰减常数 d")
|
||||
lambda_time: float = Field(..., description="时间衰减参数")
|
||||
lambda_mem: float = Field(..., description="记忆衰减参数")
|
||||
@@ -423,8 +423,8 @@ class ForgettingConfigResponse(BaseModel):
|
||||
class ForgettingConfigUpdateRequest(BaseModel):
|
||||
"""遗忘引擎配置更新请求模型"""
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
config_id: Union[uuid.UUID, int,str] = Field(..., description="配置唯一标识(UUID或int)")
|
||||
|
||||
config_id: uuid.UUID = Field(..., description="配置ID")
|
||||
decay_constant: Optional[float] = Field(None, ge=0.0, le=1.0, description="衰减常数 d")
|
||||
lambda_time: Optional[float] = Field(None, ge=0.0, le=1.0, description="时间衰减参数")
|
||||
lambda_mem: Optional[float] = Field(None, ge=0.0, le=1.0, description="记忆衰减参数")
|
||||
@@ -499,7 +499,7 @@ class ForgettingCurveRequest(BaseModel):
|
||||
|
||||
importance_score: float = Field(0.5, ge=0.0, le=1.0, description="重要性分数(0-1)")
|
||||
days: int = Field(60, ge=1, le=365, description="模拟天数(默认60天)")
|
||||
config_id: Union[uuid.UUID, int, str] = Field(..., description="配置唯一标识(UUID或int)")
|
||||
config_id: Optional[uuid.UUID] = Field(None, description="配置ID(可选,如果为None则使用默认配置)")
|
||||
|
||||
|
||||
class ForgettingCurveResponse(BaseModel):
|
||||
|
||||
@@ -3,10 +3,8 @@ from typing import Optional, List, Dict, Any
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from app.models.models_model import ModelProvider, ModelType, LoadBalanceStrategy
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.models.models_model import ModelProvider, ModelType
|
||||
|
||||
schema_logger = get_business_logger()
|
||||
|
||||
|
||||
# ModelConfig Schemas
|
||||
@@ -14,19 +12,15 @@ class ModelConfigBase(BaseModel):
|
||||
"""模型配置基础Schema"""
|
||||
name: str = Field(..., description="模型显示名称", max_length=255)
|
||||
type: ModelType = Field(..., description="模型类型")
|
||||
logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255)
|
||||
description: Optional[str] = Field(None, description="模型描述")
|
||||
provider: str = Field(..., description="供应商")
|
||||
config: Optional[Dict[str, Any]] = Field({}, description="模型配置参数")
|
||||
is_active: bool = Field(True, description="是否激活")
|
||||
is_public: bool = Field(False, description="是否公开")
|
||||
load_balance_strategy: Optional[str] = Field(LoadBalanceStrategy.NONE.value, description="负载均衡策略")
|
||||
|
||||
|
||||
class ApiKeyCreateNested(BaseModel):
|
||||
"""用于在创建模型时内嵌创建API Key的Schema"""
|
||||
model_name: str = Field(..., description="模型实际名称", max_length=255)
|
||||
description: Optional[str] = Field(None, description="备注")
|
||||
provider: ModelProvider = Field(..., description="API Key提供商")
|
||||
api_key: str = Field(..., description="API密钥", max_length=500)
|
||||
api_base: Optional[str] = Field(None, description="API基础URL", max_length=500)
|
||||
@@ -36,23 +30,10 @@ class ApiKeyCreateNested(BaseModel):
|
||||
|
||||
class ModelConfigCreate(ModelConfigBase):
|
||||
"""创建模型配置Schema"""
|
||||
api_keys: Optional[List[ApiKeyCreateNested]] = Field(None, description="同时创建的API Key配置")
|
||||
api_keys: Optional[ApiKeyCreateNested] = Field(None, description="同时创建的API Key配置")
|
||||
skip_validation: Optional[bool] = Field(False, description="是否跳过配置验证")
|
||||
|
||||
|
||||
class CompositeModelCreate(BaseModel):
|
||||
"""创建组合模型Schema"""
|
||||
name: str = Field(..., description="组合模型名称", max_length=255)
|
||||
type: Optional[ModelType] = Field(None, description="模型类型")
|
||||
logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255)
|
||||
description: Optional[str] = Field(None, description="模型描述")
|
||||
config: Optional[Dict[str, Any]] = Field({}, description="模型配置参数")
|
||||
is_active: bool = Field(True, description="是否激活")
|
||||
is_public: bool = Field(False, description="是否公开")
|
||||
api_key_ids: List[uuid.UUID] = Field(..., description="绑定的API Key ID列表")
|
||||
load_balance_strategy: Optional[str] = Field(default=LoadBalanceStrategy.NONE.value, description="负载均衡策略")
|
||||
|
||||
|
||||
class ModelConfigUpdate(BaseModel):
|
||||
"""更新模型配置Schema"""
|
||||
name: Optional[str] = Field(None, description="模型显示名称", max_length=255)
|
||||
@@ -72,48 +53,22 @@ class ModelConfig(ModelConfigBase):
|
||||
updated_at: datetime.datetime
|
||||
api_keys: List["ModelApiKey"] = []
|
||||
|
||||
@field_validator("api_keys", mode="after")
|
||||
@classmethod
|
||||
def filter_active_api_keys(cls, api_keys: List["ModelApiKey"]) -> List["ModelApiKey"]:
|
||||
return [key for key in api_keys if key.is_active]
|
||||
|
||||
@field_serializer("created_at", when_used="json")
|
||||
def _serialize_created_at(self, dt: datetime.datetime | None):
|
||||
return int(dt.timestamp() * 1000) if dt else None
|
||||
|
||||
@field_serializer("updated_at", when_used="json")
|
||||
def _serialize_updated_at(self, dt: datetime.datetime):
|
||||
return int(dt.timestamp() * 1000) if dt else None
|
||||
|
||||
|
||||
# ModelApiKey Schemas
|
||||
class ModelApiKeyCreateByProvider(BaseModel):
|
||||
"""基于供应商创建API Key Schema"""
|
||||
provider: ModelProvider = Field(..., description="API Key提供商")
|
||||
api_key: str = Field(..., description="API密钥", max_length=500)
|
||||
api_base: Optional[str] = Field(None, description="API基础URL", max_length=500)
|
||||
description: Optional[str] = Field(None, description="备注")
|
||||
config: Optional[Dict[str, Any]] = Field({}, description="API Key特定配置")
|
||||
is_active: bool = Field(True, description="是否激活")
|
||||
priority: str = Field("1", description="优先级", max_length=10)
|
||||
model_config_ids: Optional[List[uuid.UUID]] = Field(None, description="关联的模型配置ID列表")
|
||||
|
||||
|
||||
class ModelApiKeyBase(BaseModel):
|
||||
"""API Key基础Schema"""
|
||||
model_name: str = Field(..., description="模型实际名称", max_length=255)
|
||||
description: Optional[str] = Field(None, description="备注")
|
||||
provider: ModelProvider = Field(..., description="API Key提供商")
|
||||
api_key: str = Field(..., description="API密钥", max_length=500)
|
||||
api_base: Optional[str] = Field(None, description="API基础URL", max_length=500)
|
||||
config: Optional[Dict[str, Any]] = Field({}, description="API Key特定配置")
|
||||
config: Optional[Dict[str, Any]] = Field(None, description="API Key特定配置")
|
||||
is_active: bool = Field(True, description="是否激活")
|
||||
priority: str = Field("1", description="优先级", max_length=10)
|
||||
|
||||
|
||||
class ModelApiKeyCreate(ModelApiKeyBase):
|
||||
"""创建API Key Schema"""
|
||||
model_config_ids: Optional[List[uuid.UUID]] = Field(None, description="关联的模型配置ID列表")
|
||||
model_config_id: uuid.UUID = Field(..., description="模型配置ID")
|
||||
|
||||
|
||||
class ModelApiKeyUpdate(BaseModel):
|
||||
@@ -130,54 +85,23 @@ class ModelApiKeyUpdate(BaseModel):
|
||||
class ModelApiKey(ModelApiKeyBase):
|
||||
"""API Key Schema"""
|
||||
id: uuid.UUID
|
||||
model_config_id: uuid.UUID
|
||||
usage_count: str
|
||||
last_used_at: Optional[datetime.datetime]
|
||||
created_at: datetime.datetime
|
||||
updated_at: datetime.datetime
|
||||
model_configs: Any = Field(default=None, exclude=True)
|
||||
model_config_ids: List[uuid.UUID] = Field(default_factory=list, description="关联的模型配置ID列表")
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""实例化后强制提取 model_configs 的ID到 model_config_ids"""
|
||||
# 如果手动传入了 model_config_ids,不覆盖
|
||||
if self.model_config_ids and len(self.model_config_ids) > 0:
|
||||
return
|
||||
|
||||
# 从 model_configs 提取ID(只提取与 model_name 相同的非组合模型)
|
||||
if self.model_configs is not None:
|
||||
@field_validator("config", mode="before")
|
||||
@classmethod
|
||||
def parse_config(cls, v):
|
||||
"""处理 config 字段,如果是字符串则解析为字典"""
|
||||
if isinstance(v, str):
|
||||
import json
|
||||
try:
|
||||
# 情况1:ORM 对象列表(SQLAlchemy 关联)
|
||||
if hasattr(self.model_configs, '__iter__') and not isinstance(self.model_configs, dict):
|
||||
self.model_config_ids = [
|
||||
mc.id for mc in self.model_configs
|
||||
if hasattr(mc, 'id')
|
||||
and not getattr(mc, 'is_composite', False)
|
||||
and getattr(mc, 'name', None) == self.model_name
|
||||
]
|
||||
# 情况2:字典列表
|
||||
elif isinstance(self.model_configs, list):
|
||||
self.model_config_ids = [
|
||||
mc['id'] if isinstance(mc, dict) else mc.id
|
||||
for mc in self.model_configs
|
||||
if ((isinstance(mc, dict)
|
||||
and 'id' in mc
|
||||
and not mc.get('is_composite', False)
|
||||
and mc.get('name') == self.model_name) or
|
||||
(hasattr(mc, 'id')
|
||||
and not getattr(mc, 'is_composite', False)
|
||||
and getattr(mc, 'name', None) == self.model_name))
|
||||
]
|
||||
except Exception as e:
|
||||
schema_logger.warning(f"提取 model_config_ids 失败:{e}")
|
||||
self.model_config_ids = []
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, # 支持从 ORM 解析
|
||||
arbitrary_types_allowed=True, # 允许任意类型(ORM 对象)
|
||||
populate_by_name=True, # 按属性名匹配字段
|
||||
validate_assignment=True # 确保赋值触发校验
|
||||
)
|
||||
|
||||
return json.loads(v)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return v
|
||||
|
||||
@field_serializer("created_at", when_used="json")
|
||||
def _serialize_created_at(self, dt: datetime.datetime):
|
||||
@@ -186,12 +110,15 @@ class ModelApiKey(ModelApiKeyBase):
|
||||
@field_serializer("updated_at", when_used="json")
|
||||
def _serialize_updated_at(self, dt: datetime.datetime):
|
||||
return int(dt.timestamp() * 1000) if dt else None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@field_serializer("last_used_at", when_used="json")
|
||||
def _serialize_last_used_at(self, dt: datetime.datetime):
|
||||
return int(dt.timestamp() * 1000) if dt else None
|
||||
|
||||
|
||||
# 查询和响应Schemas
|
||||
class ModelConfigQuery(BaseModel):
|
||||
"""模型配置查询Schema"""
|
||||
type: Optional[List[ModelType]] = Field(None, description="模型类型筛选(支持多个)")
|
||||
@@ -202,17 +129,6 @@ class ModelConfigQuery(BaseModel):
|
||||
page: int = Field(1, description="页码", ge=1)
|
||||
pagesize: int = Field(10, description="每页数量", ge=1, le=100)
|
||||
|
||||
|
||||
# 查询和响应Schemas
|
||||
class ModelConfigQueryNew(BaseModel):
|
||||
"""模型配置查询Schema"""
|
||||
type: Optional[List[ModelType]] = Field(None, description="模型类型筛选(支持多个)")
|
||||
provider: Optional[ModelProvider] = Field(None, description="提供商筛选(通过API Key)")
|
||||
is_active: Optional[bool] = Field(None, description="激活状态筛选")
|
||||
is_public: Optional[bool] = Field(None, description="公开状态筛选")
|
||||
is_composite: Optional[bool] = Field(None, description="组合模型筛选")
|
||||
search: Optional[str] = Field(None, description="搜索关键词", max_length=255)
|
||||
|
||||
class ModelMarketplace(BaseModel):
|
||||
"""模型广场响应Schema"""
|
||||
llm_models: List[ModelConfig] = []
|
||||
@@ -255,53 +171,4 @@ class ModelValidateResponse(BaseModel):
|
||||
|
||||
|
||||
# 更新前向引用
|
||||
ModelConfig.model_rebuild()
|
||||
|
||||
|
||||
# ModelBase Schemas
|
||||
class ModelBaseCreate(BaseModel):
|
||||
"""创建基础模型Schema"""
|
||||
name: str = Field(..., description="模型唯一标识", max_length=255)
|
||||
type: ModelType = Field(..., description="模型类型")
|
||||
provider: ModelProvider = Field(..., description="提供商")
|
||||
logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255)
|
||||
description: Optional[str] = Field(None, description="模型描述")
|
||||
is_official: bool = Field(True, description="是否供应商官方模型")
|
||||
tags: List[str] = Field(default_factory=list, description="模型标签")
|
||||
|
||||
|
||||
class ModelBaseUpdate(BaseModel):
|
||||
"""更新基础模型Schema"""
|
||||
name: Optional[str] = Field(None, description="模型唯一标识", max_length=255)
|
||||
type: Optional[ModelType] = Field(None, description="模型类型")
|
||||
provider: Optional[ModelProvider] = Field(None, description="提供商")
|
||||
logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255)
|
||||
description: Optional[str] = Field(None, description="模型描述")
|
||||
is_deprecated: Optional[bool] = Field(None, description="是否弃用")
|
||||
is_official: Optional[bool] = Field(None, description="是否供应商官方模型")
|
||||
tags: Optional[List[str]] = Field(None, description="模型标签")
|
||||
|
||||
|
||||
class ModelBase(BaseModel):
|
||||
"""基础模型Schema"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
type: str
|
||||
provider: str
|
||||
logo: Optional[str]
|
||||
description: Optional[str]
|
||||
is_deprecated: bool
|
||||
is_official: bool
|
||||
tags: List[str]
|
||||
add_count: int
|
||||
|
||||
|
||||
class ModelBaseQuery(BaseModel):
|
||||
"""基础模型查询Schema"""
|
||||
type: Optional[ModelType] = Field(None, description="模型类型")
|
||||
provider: Optional[ModelProvider] = Field(None, description="提供商")
|
||||
is_official: Optional[bool] = Field(None, description="是否官方模型")
|
||||
is_deprecated: Optional[bool] = Field(None, description="是否弃用")
|
||||
search: Optional[str] = Field(None, description="搜索关键词", max_length=255)
|
||||
ModelConfig.model_rebuild()
|
||||
@@ -1,193 +0,0 @@
|
||||
"""应用统计服务"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List
|
||||
import uuid
|
||||
from sqlalchemy import func, and_, cast, Date
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.conversation_model import Conversation, Message
|
||||
from app.models.end_user_model import EndUser
|
||||
from app.models.api_key_model import ApiKey, ApiKeyLog
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.error_codes import BizCode
|
||||
|
||||
|
||||
class AppStatisticsService:
|
||||
"""应用统计服务"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_app_statistics(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
start_date: int,
|
||||
end_date: int
|
||||
) -> Dict[str, Any]:
|
||||
"""获取应用统计数据
|
||||
|
||||
Args:
|
||||
app_id: 应用ID
|
||||
workspace_id: 工作空间ID
|
||||
start_date: 开始时间戳(毫秒)
|
||||
end_date: 结束时间戳(毫秒)
|
||||
|
||||
Returns:
|
||||
统计数据字典
|
||||
"""
|
||||
# 将毫秒时间戳转换为 datetime
|
||||
start_dt = datetime.fromtimestamp(start_date / 1000)
|
||||
end_dt = datetime.fromtimestamp(end_date / 1000) + timedelta(days=1)
|
||||
|
||||
# 1. 会话统计
|
||||
conversations_stats = self._get_conversations_statistics(app_id, workspace_id, start_dt, end_dt)
|
||||
|
||||
# 2. 新增用户统计
|
||||
users_stats = self._get_new_users_statistics(app_id, start_dt, end_dt)
|
||||
|
||||
# 3. API调用统计
|
||||
api_stats = self._get_api_calls_statistics(app_id, start_dt, end_dt)
|
||||
|
||||
# 4. Token消耗统计
|
||||
token_stats = self._get_token_statistics(app_id, start_dt, end_dt)
|
||||
|
||||
return {
|
||||
"daily_conversations": conversations_stats["daily"],
|
||||
"total_conversations": conversations_stats["total"],
|
||||
"daily_new_users": users_stats["daily"],
|
||||
"total_new_users": users_stats["total"],
|
||||
"daily_api_calls": api_stats["daily"],
|
||||
"total_api_calls": api_stats["total"],
|
||||
"daily_tokens": token_stats["daily"],
|
||||
"total_tokens": token_stats["total"]
|
||||
}
|
||||
|
||||
def _get_conversations_statistics(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
start_dt: datetime,
|
||||
end_dt: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""获取会话统计"""
|
||||
# 每日会话数
|
||||
daily_query = self.db.query(
|
||||
cast(Conversation.created_at, Date).label('date'),
|
||||
func.count(Conversation.id).label('count')
|
||||
).filter(
|
||||
and_(
|
||||
Conversation.app_id == app_id,
|
||||
Conversation.workspace_id == workspace_id,
|
||||
Conversation.created_at >= start_dt,
|
||||
Conversation.created_at < end_dt
|
||||
)
|
||||
).group_by(cast(Conversation.created_at, Date)).all()
|
||||
|
||||
daily_data = [{"date": str(row.date), "count": row.count} for row in daily_query]
|
||||
total = sum(row["count"] for row in daily_data)
|
||||
|
||||
return {"daily": daily_data, "total": total}
|
||||
|
||||
def _get_new_users_statistics(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
start_dt: datetime,
|
||||
end_dt: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""获取新增用户统计"""
|
||||
# 每日新增用户数
|
||||
daily_query = self.db.query(
|
||||
cast(EndUser.created_at, Date).label('date'),
|
||||
func.count(EndUser.id).label('count')
|
||||
).filter(
|
||||
and_(
|
||||
EndUser.app_id == app_id,
|
||||
EndUser.created_at >= start_dt,
|
||||
EndUser.created_at < end_dt
|
||||
)
|
||||
).group_by(cast(EndUser.created_at, Date)).all()
|
||||
|
||||
daily_data = [{"date": str(row.date), "count": row.count} for row in daily_query]
|
||||
total = sum(row["count"] for row in daily_data)
|
||||
|
||||
return {"daily": daily_data, "total": total}
|
||||
|
||||
def _get_api_calls_statistics(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
start_dt: datetime,
|
||||
end_dt: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""获取API调用统计"""
|
||||
# 每日API调用次数
|
||||
daily_query = self.db.query(
|
||||
cast(ApiKeyLog.created_at, Date).label('date'),
|
||||
func.count(ApiKeyLog.id).label('count')
|
||||
).join(
|
||||
ApiKey, ApiKeyLog.api_key_id == ApiKey.id
|
||||
).filter(
|
||||
and_(
|
||||
ApiKey.resource_id == app_id,
|
||||
ApiKeyLog.created_at >= start_dt,
|
||||
ApiKeyLog.created_at < end_dt
|
||||
)
|
||||
).group_by(cast(ApiKeyLog.created_at, Date)).all()
|
||||
|
||||
daily_data = [{"date": str(row.date), "count": row.count} for row in daily_query]
|
||||
total = sum(row["count"] for row in daily_data)
|
||||
|
||||
return {"daily": daily_data, "total": total}
|
||||
|
||||
def _get_token_statistics(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
start_dt: datetime,
|
||||
end_dt: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""获取Token消耗统计(从Message的meta_data中提取)"""
|
||||
from sqlalchemy import text
|
||||
|
||||
# 查询所有相关消息的token使用情况
|
||||
# meta_data中可能包含: {"usage": {"total_tokens": 100}} 或 {"tokens": 100}
|
||||
daily_query = self.db.query(
|
||||
cast(Message.created_at, Date).label('date'),
|
||||
Message.meta_data
|
||||
).join(
|
||||
Conversation, Message.conversation_id == Conversation.id
|
||||
).filter(
|
||||
and_(
|
||||
Conversation.app_id == app_id,
|
||||
Message.created_at >= start_dt,
|
||||
Message.created_at < end_dt,
|
||||
Message.meta_data.isnot(None)
|
||||
)
|
||||
).all()
|
||||
|
||||
# 按日期聚合token
|
||||
daily_tokens = {}
|
||||
for row in daily_query:
|
||||
date_str = str(row.date)
|
||||
meta = row.meta_data or {}
|
||||
|
||||
# 提取token数量(支持多种格式)
|
||||
tokens = 0
|
||||
if isinstance(meta, dict):
|
||||
# 格式1: {"usage": {"total_tokens": 100}}
|
||||
if "usage" in meta and isinstance(meta["usage"], dict):
|
||||
tokens = meta["usage"].get("total_tokens", 0)
|
||||
# 格式2: {"tokens": 100}
|
||||
elif "tokens" in meta:
|
||||
tokens = meta.get("tokens", 0)
|
||||
# 格式3: {"total_tokens": 100}
|
||||
elif "total_tokens" in meta:
|
||||
tokens = meta.get("total_tokens", 0)
|
||||
|
||||
if date_str not in daily_tokens:
|
||||
daily_tokens[date_str] = 0
|
||||
daily_tokens[date_str] += int(tokens)
|
||||
|
||||
daily_data = [{"date": date, "tokens": tokens} for date, tokens in sorted(daily_tokens.items()) if tokens != 0]
|
||||
total = sum(row["tokens"] for row in daily_data)
|
||||
|
||||
return {"daily": daily_data, "total": total}
|
||||
@@ -16,7 +16,6 @@ from app.core.exceptions import BusinessException
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.core.rag.nlp.search import knowledge_retrieval
|
||||
from app.models import AgentConfig, ModelApiKey, ModelConfig
|
||||
from app.repositories.model_repository import ModelApiKeyRepository
|
||||
from app.repositories.tool_repository import ToolRepository
|
||||
from app.schemas.prompt_schema import PromptMessageRole, render_prompt_message
|
||||
from app.services import task_service
|
||||
@@ -725,21 +724,17 @@ class DraftRunService:
|
||||
Raises:
|
||||
BusinessException: 当没有可用的 API Key 时
|
||||
"""
|
||||
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config_id)
|
||||
# stmt = (
|
||||
# select(ModelApiKey).join(
|
||||
# ModelConfig, ModelApiKey.model_configs
|
||||
# )
|
||||
# .where(
|
||||
# ModelConfig.id == model_config_id,
|
||||
# ModelApiKey.is_active.is_(True)
|
||||
# )
|
||||
# .order_by(ModelApiKey.priority.desc())
|
||||
# .limit(1)
|
||||
# )
|
||||
#
|
||||
# api_key = self.db.scalars(stmt).first()
|
||||
api_key = api_keys[0] if api_keys else None
|
||||
stmt = (
|
||||
select(ModelApiKey)
|
||||
.where(
|
||||
ModelApiKey.model_config_id == model_config_id,
|
||||
ModelApiKey.is_active.is_(True)
|
||||
)
|
||||
.order_by(ModelApiKey.priority.desc())
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
api_key = self.db.scalars(stmt).first()
|
||||
|
||||
if not api_key:
|
||||
raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING)
|
||||
|
||||
@@ -17,15 +17,12 @@ from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
|
||||
class EmotionSuggestion(BaseModel):
|
||||
"""情绪建议模型"""
|
||||
type: str = Field(...,
|
||||
description="建议类型:emotion_balance/activity_recommendation/social_connection/stress_management")
|
||||
type: str = Field(..., description="建议类型:emotion_balance/activity_recommendation/social_connection/stress_management")
|
||||
title: str = Field(..., description="建议标题")
|
||||
content: str = Field(..., description="建议内容")
|
||||
priority: str = Field(..., description="优先级:high/medium/low")
|
||||
@@ -40,33 +37,33 @@ class EmotionSuggestionsResponse(BaseModel):
|
||||
|
||||
class EmotionAnalyticsService:
|
||||
"""情绪分析服务
|
||||
|
||||
|
||||
提供情绪数据的分析和统计功能,包括:
|
||||
- 情绪标签统计
|
||||
- 情绪词云数据
|
||||
- 情绪健康指数计算
|
||||
- 个性化情绪建议生成
|
||||
|
||||
|
||||
Attributes:
|
||||
emotion_repo: 情绪数据仓储实例
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""初始化情绪分析服务"""
|
||||
connector = Neo4jConnector()
|
||||
self.emotion_repo = EmotionRepository(connector)
|
||||
logger.info("情绪分析服务初始化完成")
|
||||
|
||||
|
||||
async def get_emotion_tags(
|
||||
self,
|
||||
end_user_id: str,
|
||||
emotion_type: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
limit: int = 10
|
||||
self,
|
||||
end_user_id: str,
|
||||
emotion_type: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
limit: int = 10
|
||||
) -> Dict[str, Any]:
|
||||
"""获取情绪标签统计
|
||||
|
||||
|
||||
查询指定用户的情绪类型分布,包括计数、百分比和平均强度。
|
||||
确保返回所有6个情绪维度(joy、sadness、anger、fear、surprise、neutral),
|
||||
即使某些维度没有数据也会返回count=0的记录。
|
||||
@@ -74,8 +71,8 @@ class EmotionAnalyticsService:
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取情绪标签统计: user={end_user_id}, type={emotion_type}, "
|
||||
f"start={start_date}, end={end_date}, limit={limit}")
|
||||
|
||||
f"start={start_date}, end={end_date}, limit={limit}")
|
||||
|
||||
# 调用仓储层查询
|
||||
tags = await self.emotion_repo.get_emotion_tags(
|
||||
end_user_id=end_user_id,
|
||||
@@ -84,13 +81,13 @@ class EmotionAnalyticsService:
|
||||
end_date=end_date,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
# 定义所有6个情绪维度
|
||||
all_emotion_types = ['joy', 'sadness', 'anger', 'fear', 'surprise', 'neutral']
|
||||
|
||||
|
||||
# 将查询结果转换为字典,方便查找
|
||||
tags_dict = {tag["emotion_type"]: tag for tag in tags}
|
||||
|
||||
|
||||
# 补全缺失的情绪维度
|
||||
complete_tags = []
|
||||
for emotion in all_emotion_types:
|
||||
@@ -104,52 +101,52 @@ class EmotionAnalyticsService:
|
||||
"percentage": 0.0,
|
||||
"avg_intensity": 0.0
|
||||
})
|
||||
|
||||
|
||||
# 计算总数
|
||||
total_count = sum(tag["count"] for tag in complete_tags)
|
||||
|
||||
|
||||
# 如果有数据,重新计算百分比(因为补全了0值项)
|
||||
if total_count > 0:
|
||||
for tag in complete_tags:
|
||||
if tag["count"] > 0:
|
||||
tag["percentage"] = round((tag["count"] / total_count) * 100, 2)
|
||||
|
||||
|
||||
# 构建时间范围信息
|
||||
time_range = {}
|
||||
if start_date:
|
||||
time_range["start_date"] = start_date
|
||||
if end_date:
|
||||
time_range["end_date"] = end_date
|
||||
|
||||
|
||||
# 格式化响应
|
||||
response = {
|
||||
"tags": complete_tags,
|
||||
"total_count": total_count,
|
||||
"time_range": time_range if time_range else None
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"情绪标签统计完成: total_count={total_count}, tags_count={len(complete_tags)}")
|
||||
return response
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取情绪标签统计失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
async def get_emotion_wordcloud(
|
||||
self,
|
||||
end_user_id: str,
|
||||
emotion_type: Optional[str] = None,
|
||||
limit: int = 50
|
||||
self,
|
||||
end_user_id: str,
|
||||
emotion_type: Optional[str] = None,
|
||||
limit: int = 50
|
||||
) -> Dict[str, Any]:
|
||||
"""获取情绪词云数据
|
||||
|
||||
|
||||
查询情绪关键词及其频率,用于生成词云可视化。
|
||||
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
emotion_type: 可选的情绪类型过滤
|
||||
limit: 返回关键词的最大数量
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪词云数据的响应:
|
||||
- keywords: 关键词列表
|
||||
@@ -157,39 +154,39 @@ class EmotionAnalyticsService:
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取情绪词云数据: user={end_user_id}, type={emotion_type}, limit={limit}")
|
||||
|
||||
|
||||
# 调用仓储层查询
|
||||
keywords = await self.emotion_repo.get_emotion_wordcloud(
|
||||
end_user_id=end_user_id,
|
||||
emotion_type=emotion_type,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
# 计算总关键词数量
|
||||
total_keywords = len(keywords)
|
||||
|
||||
|
||||
# 格式化响应
|
||||
response = {
|
||||
"keywords": keywords,
|
||||
"total_keywords": total_keywords
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"情绪词云数据获取完成: total_keywords={total_keywords}")
|
||||
return response
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取情绪词云数据失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def _calculate_positivity_rate(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""计算积极率
|
||||
|
||||
|
||||
根据情绪类型分类正面、负面和中性情绪,计算积极率。
|
||||
公式:(正面数 / (正面数 + 负面数)) * 100
|
||||
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_type 字段
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 包含积极率计算结果:
|
||||
- score: 积极率分数(0-100)
|
||||
@@ -200,38 +197,38 @@ class EmotionAnalyticsService:
|
||||
# 定义情绪分类
|
||||
positive_emotions = {'joy', 'surprise'}
|
||||
negative_emotions = {'sadness', 'anger', 'fear'}
|
||||
|
||||
|
||||
# 统计各类情绪数量
|
||||
positive_count = sum(1 for e in emotions if e.get('emotion_type') in positive_emotions)
|
||||
negative_count = sum(1 for e in emotions if e.get('emotion_type') in negative_emotions)
|
||||
neutral_count = sum(1 for e in emotions if e.get('emotion_type') == 'neutral')
|
||||
|
||||
|
||||
# 计算积极率
|
||||
total_non_neutral = positive_count + negative_count
|
||||
if total_non_neutral > 0:
|
||||
score = (positive_count / total_non_neutral) * 100
|
||||
else:
|
||||
score = 50.0 # 如果没有非中性情绪,默认为50
|
||||
|
||||
|
||||
logger.debug(f"积极率计算: positive={positive_count}, negative={negative_count}, "
|
||||
f"neutral={neutral_count}, score={score:.2f}")
|
||||
|
||||
f"neutral={neutral_count}, score={score:.2f}")
|
||||
|
||||
return {
|
||||
"score": round(score, 2),
|
||||
"positive_count": positive_count,
|
||||
"negative_count": negative_count,
|
||||
"neutral_count": neutral_count
|
||||
}
|
||||
|
||||
|
||||
def _calculate_stability(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""计算稳定性
|
||||
|
||||
|
||||
基于情绪强度的标准差计算情绪稳定性。
|
||||
公式:(1 - min(std_deviation, 1.0)) * 100
|
||||
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_intensity 字段
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 包含稳定性计算结果:
|
||||
- score: 稳定性分数(0-100)
|
||||
@@ -239,7 +236,7 @@ class EmotionAnalyticsService:
|
||||
"""
|
||||
# 提取所有情绪强度
|
||||
intensities = [e.get('emotion_intensity', 0.0) for e in emotions if e.get('emotion_intensity') is not None]
|
||||
|
||||
|
||||
# 计算标准差
|
||||
if len(intensities) >= 2:
|
||||
std_deviation = statistics.stdev(intensities)
|
||||
@@ -247,29 +244,29 @@ class EmotionAnalyticsService:
|
||||
std_deviation = 0.0 # 只有一个数据点,标准差为0
|
||||
else:
|
||||
std_deviation = 0.0 # 没有数据,标准差为0
|
||||
|
||||
|
||||
# 计算稳定性分数
|
||||
# 标准差越小,稳定性越高
|
||||
score = (1 - min(std_deviation, 1.0)) * 100
|
||||
|
||||
|
||||
logger.debug(f"稳定性计算: intensities_count={len(intensities)}, "
|
||||
f"std_deviation={std_deviation:.3f}, score={score:.2f}")
|
||||
|
||||
f"std_deviation={std_deviation:.3f}, score={score:.2f}")
|
||||
|
||||
return {
|
||||
"score": round(score, 2),
|
||||
"std_deviation": round(std_deviation, 3)
|
||||
}
|
||||
|
||||
|
||||
def _calculate_resilience(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""计算恢复力
|
||||
|
||||
|
||||
分析情绪转换模式,统计从负面情绪恢复到正面情绪的能力。
|
||||
公式:(负面到正面转换次数 / 总负面情绪数) * 100
|
||||
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_type 和 created_at 字段
|
||||
应该按时间顺序排列
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 包含恢复力计算结果:
|
||||
- score: 恢复力分数(0-100)
|
||||
@@ -278,24 +275,24 @@ class EmotionAnalyticsService:
|
||||
# 定义情绪分类
|
||||
positive_emotions = {'joy', 'surprise'}
|
||||
negative_emotions = {'sadness', 'anger', 'fear'}
|
||||
|
||||
|
||||
# 统计负面到正面的转换次数
|
||||
recovery_count = 0
|
||||
negative_count = 0
|
||||
|
||||
|
||||
for i in range(len(emotions)):
|
||||
current_emotion = emotions[i].get('emotion_type')
|
||||
|
||||
|
||||
# 统计负面情绪总数
|
||||
if current_emotion in negative_emotions:
|
||||
negative_count += 1
|
||||
|
||||
|
||||
# 检查下一个情绪是否为正面
|
||||
if i + 1 < len(emotions):
|
||||
next_emotion = emotions[i + 1].get('emotion_type')
|
||||
if next_emotion in positive_emotions:
|
||||
recovery_count += 1
|
||||
|
||||
|
||||
# 计算恢复力分数
|
||||
if negative_count > 0:
|
||||
recovery_rate = recovery_count / negative_count
|
||||
@@ -304,28 +301,28 @@ class EmotionAnalyticsService:
|
||||
# 如果没有负面情绪,恢复力设为100(最佳状态)
|
||||
recovery_rate = 1.0
|
||||
score = 100.0
|
||||
|
||||
|
||||
logger.debug(f"恢复力计算: negative_count={negative_count}, "
|
||||
f"recovery_count={recovery_count}, score={score:.2f}")
|
||||
|
||||
f"recovery_count={recovery_count}, score={score:.2f}")
|
||||
|
||||
return {
|
||||
"score": round(score, 2),
|
||||
"recovery_rate": round(recovery_rate, 3)
|
||||
}
|
||||
|
||||
|
||||
async def calculate_emotion_health_index(
|
||||
self,
|
||||
end_user_id: str,
|
||||
time_range: str = "30d"
|
||||
self,
|
||||
end_user_id: str,
|
||||
time_range: str = "30d"
|
||||
) -> Dict[str, Any]:
|
||||
"""计算情绪健康指数
|
||||
|
||||
|
||||
综合积极率、稳定性和恢复力计算情绪健康指数。
|
||||
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
time_range: 时间范围(7d/30d/90d)
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪健康指数的完整响应:
|
||||
- health_score: 综合健康分数(0-100)
|
||||
@@ -339,13 +336,13 @@ class EmotionAnalyticsService:
|
||||
"""
|
||||
try:
|
||||
logger.info(f"计算情绪健康指数: user={end_user_id}, time_range={time_range}")
|
||||
|
||||
|
||||
# 获取时间范围内的情绪数据
|
||||
emotions = await self.emotion_repo.get_emotions_in_range(
|
||||
end_user_id=end_user_id,
|
||||
time_range=time_range
|
||||
)
|
||||
|
||||
|
||||
# 如果没有数据,返回默认值
|
||||
if not emotions:
|
||||
logger.warning(f"用户 {end_user_id} 在时间范围 {time_range} 内没有情绪数据")
|
||||
@@ -360,20 +357,20 @@ class EmotionAnalyticsService:
|
||||
"emotion_distribution": {},
|
||||
"time_range": time_range
|
||||
}
|
||||
|
||||
|
||||
# 计算各维度指标
|
||||
positivity_rate = self._calculate_positivity_rate(emotions)
|
||||
stability = self._calculate_stability(emotions)
|
||||
resilience = self._calculate_resilience(emotions)
|
||||
|
||||
|
||||
# 计算综合健康分数
|
||||
# 公式:positivity_rate * 0.4 + stability * 0.3 + resilience * 0.3
|
||||
health_score = (
|
||||
positivity_rate["score"] * 0.4 +
|
||||
stability["score"] * 0.3 +
|
||||
resilience["score"] * 0.3
|
||||
positivity_rate["score"] * 0.4 +
|
||||
stability["score"] * 0.3 +
|
||||
resilience["score"] * 0.3
|
||||
)
|
||||
|
||||
|
||||
# 确定健康等级
|
||||
if health_score >= 80:
|
||||
level = "优秀"
|
||||
@@ -383,13 +380,13 @@ class EmotionAnalyticsService:
|
||||
level = "一般"
|
||||
else:
|
||||
level = "较差"
|
||||
|
||||
|
||||
# 统计情绪分布
|
||||
emotion_distribution = {}
|
||||
for emotion_type in ['joy', 'sadness', 'anger', 'fear', 'surprise', 'neutral']:
|
||||
count = sum(1 for e in emotions if e.get('emotion_type') == emotion_type)
|
||||
emotion_distribution[emotion_type] = count
|
||||
|
||||
|
||||
# 格式化响应
|
||||
response = {
|
||||
"health_score": round(health_score, 2),
|
||||
@@ -402,22 +399,22 @@ class EmotionAnalyticsService:
|
||||
"emotion_distribution": emotion_distribution,
|
||||
"time_range": time_range
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"情绪健康指数计算完成: score={health_score:.2f}, level={level}")
|
||||
return response
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"计算情绪健康指数失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def _analyze_emotion_patterns(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""分析情绪模式
|
||||
|
||||
|
||||
识别主要负面情绪、情绪触发因素和波动时段。
|
||||
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_type、emotion_intensity、created_at 字段
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪模式分析结果:
|
||||
- dominant_negative_emotion: 主要负面情绪类型
|
||||
@@ -425,19 +422,19 @@ class EmotionAnalyticsService:
|
||||
- emotion_volatility: 情绪波动性(高/中/低)
|
||||
"""
|
||||
negative_emotions = {'sadness', 'anger', 'fear'}
|
||||
|
||||
|
||||
# 统计负面情绪分布
|
||||
negative_emotion_counts = {}
|
||||
for emotion in emotions:
|
||||
emotion_type = emotion.get('emotion_type')
|
||||
if emotion_type in negative_emotions:
|
||||
negative_emotion_counts[emotion_type] = negative_emotion_counts.get(emotion_type, 0) + 1
|
||||
|
||||
|
||||
# 识别主要负面情绪
|
||||
dominant_negative_emotion = None
|
||||
if negative_emotion_counts:
|
||||
dominant_negative_emotion = max(negative_emotion_counts, key=negative_emotion_counts.get)
|
||||
|
||||
|
||||
# 识别高强度情绪(强度 >= 0.7)
|
||||
high_intensity_emotions = [
|
||||
{
|
||||
@@ -448,7 +445,7 @@ class EmotionAnalyticsService:
|
||||
for e in emotions
|
||||
if e.get('emotion_intensity', 0) >= 0.7
|
||||
]
|
||||
|
||||
|
||||
# 评估情绪波动性
|
||||
intensities = [e.get('emotion_intensity', 0.0) for e in emotions if e.get('emotion_intensity') is not None]
|
||||
if len(intensities) >= 2:
|
||||
@@ -461,29 +458,29 @@ class EmotionAnalyticsService:
|
||||
volatility = "低"
|
||||
else:
|
||||
volatility = "未知"
|
||||
|
||||
|
||||
logger.debug(f"情绪模式分析: dominant_negative={dominant_negative_emotion}, "
|
||||
f"high_intensity_count={len(high_intensity_emotions)}, volatility={volatility}")
|
||||
|
||||
f"high_intensity_count={len(high_intensity_emotions)}, volatility={volatility}")
|
||||
|
||||
return {
|
||||
"dominant_negative_emotion": dominant_negative_emotion,
|
||||
"high_intensity_emotions": high_intensity_emotions[:5], # 最多返回5个
|
||||
"emotion_volatility": volatility
|
||||
}
|
||||
|
||||
|
||||
async def generate_emotion_suggestions(
|
||||
self,
|
||||
end_user_id: str,
|
||||
db: Session,
|
||||
self,
|
||||
end_user_id: str,
|
||||
db: Session,
|
||||
) -> Dict[str, Any]:
|
||||
"""生成个性化情绪建议
|
||||
|
||||
|
||||
基于情绪健康数据和用户画像生成个性化建议。
|
||||
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
db: 数据库会话
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 包含个性化建议的响应:
|
||||
- health_summary: 健康状态摘要
|
||||
@@ -491,17 +488,17 @@ class EmotionAnalyticsService:
|
||||
"""
|
||||
try:
|
||||
logger.info(f"生成个性化情绪建议: user={end_user_id}")
|
||||
|
||||
|
||||
# 1. 从 end_user_id 获取关联的 memory_config_id
|
||||
llm_client = None
|
||||
try:
|
||||
from app.services.memory_agent_service import (
|
||||
get_end_user_connected_config,
|
||||
)
|
||||
|
||||
|
||||
connected_config = get_end_user_connected_config(end_user_id, db)
|
||||
config_id = connected_config.get("memory_config_id")
|
||||
config_id = resolve_config_id(config_id, db)
|
||||
|
||||
if config_id is not None:
|
||||
from app.services.memory_config_service import (
|
||||
MemoryConfigService,
|
||||
@@ -516,35 +513,35 @@ class EmotionAnalyticsService:
|
||||
llm_client = factory.get_llm_client(str(memory_config.llm_model_id))
|
||||
except Exception as e:
|
||||
logger.warning(f"无法获取 end_user {end_user_id} 的配置,将使用默认配置: {e}")
|
||||
|
||||
|
||||
# 2. 获取情绪健康数据
|
||||
health_data = await self.calculate_emotion_health_index(end_user_id, time_range="30d")
|
||||
|
||||
|
||||
# 3. 获取情绪数据用于模式分析
|
||||
emotions = await self.emotion_repo.get_emotions_in_range(
|
||||
end_user_id=end_user_id,
|
||||
time_range="30d"
|
||||
)
|
||||
|
||||
|
||||
# 4. 分析情绪模式
|
||||
patterns = self._analyze_emotion_patterns(emotions)
|
||||
|
||||
|
||||
# 5. 获取用户画像数据(简化版,直接从Neo4j获取)
|
||||
user_profile = await self._get_simple_user_profile(end_user_id)
|
||||
|
||||
|
||||
# 6. 构建LLM prompt
|
||||
prompt = await self._build_suggestion_prompt(health_data, patterns, user_profile)
|
||||
|
||||
|
||||
# 7. 调用LLM生成建议(使用配置中的LLM)
|
||||
if llm_client is None:
|
||||
# 无法获取配置时,抛出错误而不是使用默认配置
|
||||
raise ValueError("无法获取LLM配置,请确保end_user关联了有效的memory_config")
|
||||
|
||||
|
||||
# 将 prompt 转换为 messages 格式
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
|
||||
# 8. 使用结构化输出直接获取 Pydantic 模型
|
||||
try:
|
||||
suggestions_response = await llm_client.response_structured(
|
||||
@@ -555,7 +552,7 @@ class EmotionAnalyticsService:
|
||||
logger.error(f"LLM 结构化输出失败: {str(e)}")
|
||||
# 返回默认建议
|
||||
suggestions_response = self._get_default_suggestions(health_data)
|
||||
|
||||
|
||||
# 8. 验证建议数量(3-5条)
|
||||
if len(suggestions_response.suggestions) < 3:
|
||||
logger.warning(f"建议数量不足: {len(suggestions_response.suggestions)}")
|
||||
@@ -563,7 +560,7 @@ class EmotionAnalyticsService:
|
||||
elif len(suggestions_response.suggestions) > 5:
|
||||
logger.warning(f"建议数量过多: {len(suggestions_response.suggestions)}")
|
||||
suggestions_response.suggestions = suggestions_response.suggestions[:5]
|
||||
|
||||
|
||||
# 9. 格式化响应
|
||||
response = {
|
||||
"health_summary": suggestions_response.health_summary,
|
||||
@@ -578,26 +575,26 @@ class EmotionAnalyticsService:
|
||||
for s in suggestions_response.suggestions
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"个性化建议生成完成: suggestions_count={len(response['suggestions'])}")
|
||||
return response
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成个性化建议失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
async def _get_simple_user_profile(self, end_user_id: str) -> Dict[str, Any]:
|
||||
"""获取简化的用户画像数据
|
||||
|
||||
|
||||
Args:
|
||||
end_user_id: 用户ID
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 用户画像数据
|
||||
"""
|
||||
try:
|
||||
connector = Neo4jConnector()
|
||||
|
||||
|
||||
# 查询用户的实体和标签
|
||||
query = """
|
||||
MATCH (e:Entity)
|
||||
@@ -606,59 +603,59 @@ class EmotionAnalyticsService:
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
|
||||
|
||||
entities = await connector.execute_query(query, end_user_id=end_user_id)
|
||||
|
||||
|
||||
# 提取兴趣标签
|
||||
interests = [e["name"] for e in entities if e.get("type") in ["INTEREST", "HOBBY"]][:5]
|
||||
# 后期会引入用户的习惯。。
|
||||
return {
|
||||
"interests": interests if interests else ["未知"]
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户画像失败: {str(e)}")
|
||||
return {"interests": ["未知"]}
|
||||
|
||||
|
||||
async def _build_suggestion_prompt(
|
||||
self,
|
||||
health_data: Dict[str, Any],
|
||||
patterns: Dict[str, Any],
|
||||
user_profile: Dict[str, Any]
|
||||
self,
|
||||
health_data: Dict[str, Any],
|
||||
patterns: Dict[str, Any],
|
||||
user_profile: Dict[str, Any]
|
||||
) -> str:
|
||||
"""构建情绪建议生成的prompt
|
||||
|
||||
|
||||
Args:
|
||||
health_data: 情绪健康数据
|
||||
patterns: 情绪模式分析结果
|
||||
user_profile: 用户画像数据
|
||||
|
||||
|
||||
Returns:
|
||||
str: LLM prompt
|
||||
"""
|
||||
from app.core.memory.utils.prompt.prompt_utils import (
|
||||
render_emotion_suggestions_prompt,
|
||||
)
|
||||
|
||||
|
||||
prompt = await render_emotion_suggestions_prompt(
|
||||
health_data=health_data,
|
||||
patterns=patterns,
|
||||
user_profile=user_profile
|
||||
)
|
||||
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def _get_default_suggestions(self, health_data: Dict[str, Any]) -> EmotionSuggestionsResponse:
|
||||
"""获取默认建议(当LLM调用失败时使用)
|
||||
|
||||
|
||||
Args:
|
||||
health_data: 情绪健康数据
|
||||
|
||||
|
||||
Returns:
|
||||
EmotionSuggestionsResponse: 默认建议
|
||||
"""
|
||||
health_score = health_data.get('health_score', 0)
|
||||
|
||||
|
||||
if health_score >= 80:
|
||||
summary = "您的情绪健康状况优秀,请继续保持积极的生活态度。"
|
||||
elif health_score >= 60:
|
||||
@@ -667,7 +664,7 @@ class EmotionAnalyticsService:
|
||||
summary = "您的情绪健康需要关注,建议采取一些改善措施。"
|
||||
else:
|
||||
summary = "您的情绪健康需要重点关注,建议寻求专业帮助。"
|
||||
|
||||
|
||||
suggestions = [
|
||||
EmotionSuggestion(
|
||||
type="emotion_balance",
|
||||
@@ -703,54 +700,54 @@ class EmotionAnalyticsService:
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
return EmotionSuggestionsResponse(
|
||||
health_summary=summary,
|
||||
suggestions=suggestions
|
||||
)
|
||||
|
||||
|
||||
async def get_cached_suggestions(
|
||||
self,
|
||||
end_user_id: str,
|
||||
db: Session,
|
||||
self,
|
||||
end_user_id: str,
|
||||
db: Session,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""从 Redis 缓存获取个性化情绪建议
|
||||
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
|
||||
|
||||
Returns:
|
||||
Dict: 缓存的建议数据,如果不存在或已过期返回 None
|
||||
"""
|
||||
try:
|
||||
from app.cache.memory.emotion_memory import EmotionMemoryCache
|
||||
|
||||
|
||||
logger.info(f"尝试从 Redis 缓存获取情绪建议: user={end_user_id}")
|
||||
|
||||
|
||||
# 从 Redis 获取缓存
|
||||
cached_data = await EmotionMemoryCache.get_emotion_suggestions(end_user_id)
|
||||
|
||||
|
||||
if cached_data is None:
|
||||
logger.info(f"用户 {end_user_id} 的建议缓存不存在或已过期")
|
||||
return None
|
||||
|
||||
|
||||
logger.info(f"成功从 Redis 缓存获取建议: user={end_user_id}")
|
||||
return cached_data
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从 Redis 缓存获取建议失败: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def save_suggestions_cache(
|
||||
self,
|
||||
end_user_id: str,
|
||||
suggestions_data: Dict[str, Any],
|
||||
db: Session,
|
||||
expires_hours: int = 24
|
||||
self,
|
||||
end_user_id: str,
|
||||
suggestions_data: Dict[str, Any],
|
||||
db: Session,
|
||||
expires_hours: int = 24
|
||||
) -> None:
|
||||
"""保存建议到 Redis 缓存
|
||||
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
suggestions_data: 建议数据
|
||||
@@ -759,24 +756,24 @@ class EmotionAnalyticsService:
|
||||
"""
|
||||
try:
|
||||
from app.cache.memory.emotion_memory import EmotionMemoryCache
|
||||
|
||||
|
||||
logger.info(f"保存建议到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时")
|
||||
|
||||
|
||||
# 计算过期时间(秒)
|
||||
expire_seconds = expires_hours * 3600
|
||||
|
||||
|
||||
# 保存到 Redis
|
||||
success = await EmotionMemoryCache.set_emotion_suggestions(
|
||||
user_id=end_user_id,
|
||||
suggestions_data=suggestions_data,
|
||||
expire=expire_seconds
|
||||
)
|
||||
|
||||
|
||||
if success:
|
||||
logger.info(f"建议缓存保存成功: user={end_user_id}")
|
||||
else:
|
||||
logger.warning(f"建议缓存保存失败: user={end_user_id}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True)
|
||||
# 不抛出异常,缓存失败不应影响主流程
|
||||
@@ -5,7 +5,6 @@ import uuid
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.repositories.model_repository import ModelApiKeyRepository
|
||||
from app.services.conversation_state_manager import ConversationStateManager
|
||||
from app.models import ModelConfig, AgentConfig
|
||||
from app.core.logging_config import get_business_logger
|
||||
@@ -383,14 +382,11 @@ class LLMRouter:
|
||||
from app.core.models.base import RedBearModelConfig
|
||||
from app.models import ModelApiKey, ModelType
|
||||
|
||||
# 获取 API Key 配置(通过关联关系)
|
||||
# api_key_config = self.db.query(ModelApiKey).join(
|
||||
# ModelConfig, ModelApiKey.model_configs
|
||||
# ).filter(ModelConfig.id == self.routing_model_config.id,
|
||||
# ModelApiKey.is_active == True
|
||||
# ).first()
|
||||
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, self.routing_model_config.id)
|
||||
api_key_config = api_keys[0] if api_keys else None
|
||||
# 获取 API Key 配置
|
||||
api_key_config = self.db.query(ModelApiKey).filter(
|
||||
ModelApiKey.model_config_id == self.routing_model_config.id,
|
||||
ModelApiKey.is_active
|
||||
).first()
|
||||
|
||||
if not api_key_config:
|
||||
raise Exception("路由模型没有可用的 API Key")
|
||||
@@ -423,9 +419,6 @@ class LLMRouter:
|
||||
|
||||
# 调用模型
|
||||
response = await llm.ainvoke(prompt)
|
||||
|
||||
from app.services.model_service import ModelApiKeyService
|
||||
ModelApiKeyService.record_api_key_usage(self.db, api_key_config.id)
|
||||
|
||||
# 提取响应内容
|
||||
if hasattr(response, 'content'):
|
||||
|
||||
@@ -334,9 +334,7 @@ class MemoryAgentService:
|
||||
langchain_messages.append(HumanMessage(content=msg['content']))
|
||||
elif msg['role'] == 'assistant':
|
||||
langchain_messages.append(AIMessage(content=msg['content']))
|
||||
print(100*'-')
|
||||
print(langchain_messages)
|
||||
print(100*'-')
|
||||
|
||||
# 初始状态 - 包含所有必要字段
|
||||
initial_state = {
|
||||
"messages": langchain_messages,
|
||||
|
||||
@@ -338,7 +338,7 @@ class MemoryConfigService:
|
||||
"provider": api_config.provider,
|
||||
"api_key": api_config.api_key,
|
||||
"base_url": api_config.api_base,
|
||||
"model_config_id": str(config.id),
|
||||
"model_config_id": api_config.model_config_id,
|
||||
"type": config.type,
|
||||
"timeout": settings.LLM_TIMEOUT,
|
||||
"max_retries": settings.LLM_MAX_RETRIES,
|
||||
@@ -370,7 +370,7 @@ class MemoryConfigService:
|
||||
"provider": api_config.provider,
|
||||
"api_key": api_config.api_key,
|
||||
"base_url": api_config.api_base,
|
||||
"model_config_id": str(config.id),
|
||||
"model_config_id": api_config.model_config_id,
|
||||
"type": config.type,
|
||||
"timeout": 120.0,
|
||||
"max_retries": 5,
|
||||
|
||||
@@ -18,7 +18,6 @@ from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from app.models.app_model import App
|
||||
from app.models.app_release_model import AppRelease
|
||||
from app.models.end_user_model import EndUser
|
||||
from app.utils.config_utils import resolve_config_id
|
||||
|
||||
api_logger = get_api_logger()
|
||||
|
||||
@@ -89,36 +88,38 @@ class WorkspaceAppService:
|
||||
|
||||
for release in app_releases:
|
||||
memory_content = self._extract_memory_content(release.config)
|
||||
memory_content=resolve_config_id(memory_content, self.db)
|
||||
|
||||
|
||||
if memory_content and memory_content in processed_configs:
|
||||
continue
|
||||
|
||||
|
||||
release_info = {
|
||||
"app_id": str(release.app_id),
|
||||
"config": memory_content
|
||||
}
|
||||
|
||||
|
||||
|
||||
if memory_content:
|
||||
processed_configs.add(memory_content)
|
||||
memory_config_info = self._get_memory_config(memory_content)
|
||||
|
||||
if memory_config_info:
|
||||
if not any(dc["config_id"] == memory_config_info["config_id"] for dc in app_info["memory_configs"]):
|
||||
app_info["memory_configs"].append(memory_config_info)
|
||||
|
||||
|
||||
app_info["releases"].append(release_info)
|
||||
|
||||
|
||||
def _extract_memory_content(self, config: Any) -> str:
|
||||
"""Extract memory_comtent from config"""
|
||||
if not config or not isinstance(config, dict):
|
||||
return None
|
||||
|
||||
|
||||
memory_obj = config.get('memory')
|
||||
if memory_obj and isinstance(memory_obj, dict):
|
||||
return memory_obj.get('memory_content')
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_memory_config(self, memory_content: str) -> Dict[str, Any]:
|
||||
"""Retrieve memory_config information based on memory_content"""
|
||||
try:
|
||||
@@ -128,7 +129,7 @@ class WorkspaceAppService:
|
||||
# memory_config_result = self.db.execute(text(memory_config_query), memory_config_params).fetchone()
|
||||
# if memory_config_result is None:
|
||||
# return None
|
||||
|
||||
|
||||
if memory_config_result:
|
||||
return {
|
||||
"config_id": memory_config_result.config_id,
|
||||
@@ -143,22 +144,20 @@ class WorkspaceAppService:
|
||||
}
|
||||
except Exception as e:
|
||||
api_logger.warning(f"查询memory_config失败,memory_content: {memory_content}, 错误: {str(e)}")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _process_end_users(self, app: App, app_info: Dict[str, Any]) -> None:
|
||||
"""Processing end-user information for applications"""
|
||||
end_users = self.db.query(EndUser).filter(EndUser.app_id == app.id).all()
|
||||
|
||||
|
||||
for end_user in end_users:
|
||||
end_user_info = {
|
||||
"id": str(end_user.id),
|
||||
"app_id": str(end_user.app_id)
|
||||
}
|
||||
app_info["end_users"].append(end_user_info)
|
||||
print(100*'-')
|
||||
print(app_info)
|
||||
|
||||
|
||||
def get_end_user_reflection_time(self, end_user_id: str) -> Optional[Any]:
|
||||
"""
|
||||
Read the reflection time of end users
|
||||
@@ -177,7 +176,7 @@ class WorkspaceAppService:
|
||||
except Exception as e:
|
||||
api_logger.error(f"读取用户反思时间失败,end_user_id: {end_user_id}, 错误: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def update_end_user_reflection_time(self, end_user_id: str) -> bool:
|
||||
"""
|
||||
Update the reflection time of end users to the current time
|
||||
@@ -190,7 +189,7 @@ class WorkspaceAppService:
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
end_user = self.db.query(EndUser).filter(EndUser.id == end_user_id).first()
|
||||
if end_user:
|
||||
end_user.reflection_time = datetime.now()
|
||||
@@ -208,7 +207,7 @@ class WorkspaceAppService:
|
||||
|
||||
class MemoryReflectionService:
|
||||
"""Memory reflection service category"""
|
||||
|
||||
|
||||
def __init__(self,db: Session = Depends(get_db)):
|
||||
self.db=db
|
||||
|
||||
@@ -253,22 +252,22 @@ class MemoryReflectionService:
|
||||
"end_user_id": end_user_id,
|
||||
"config_data": config_data
|
||||
}
|
||||
|
||||
|
||||
async def start_reflection_from_data(self, config_data: Dict[str, Any], end_user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Starting Reflection from Configuration Data
|
||||
|
||||
|
||||
Args:
|
||||
config_data: Configure data dictionary, including reflective configuration information
|
||||
end_user_id: end_user_id
|
||||
|
||||
|
||||
Returns:
|
||||
Reflect on the execution results
|
||||
"""
|
||||
try:
|
||||
config_id = config_data.get("config_id")
|
||||
api_logger.info(f"从配置数据启动反思,config_id: {config_id}, end_user_id: {end_user_id}")
|
||||
|
||||
|
||||
|
||||
if not config_data.get("enable_self_reflexion", False):
|
||||
return {
|
||||
@@ -278,7 +277,7 @@ class MemoryReflectionService:
|
||||
"end_user_id": end_user_id,
|
||||
"config_data": config_data
|
||||
}
|
||||
|
||||
|
||||
|
||||
config_data_id=config_data['config_id']
|
||||
reflection_config=WorkspaceAppService(self.db)._get_memory_config(config_data_id)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
import uuid
|
||||
@@ -7,11 +6,11 @@ import time
|
||||
import asyncio
|
||||
|
||||
from app.models.models_model import ModelConfig, ModelApiKey, ModelType
|
||||
from app.repositories.model_repository import ModelConfigRepository, ModelApiKeyRepository, ModelBaseRepository
|
||||
from app.repositories.model_repository import ModelConfigRepository, ModelApiKeyRepository
|
||||
from app.schemas import model_schema
|
||||
from app.schemas.model_schema import (
|
||||
ModelConfigCreate, ModelConfigUpdate, ModelApiKeyCreate, ModelApiKeyUpdate,
|
||||
ModelConfigQuery, ModelStats, ModelConfigQueryNew
|
||||
ModelConfigQuery, ModelStats
|
||||
)
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.schemas.response_schema import PageData, PageMeta
|
||||
@@ -48,26 +47,6 @@ class ModelConfigService:
|
||||
items=[model_schema.ModelConfig.model_validate(model) for model in models]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_model_list_new(db: Session, query: ModelConfigQueryNew, tenant_id: uuid.UUID | None = None) -> List[dict]:
|
||||
"""获取模型配置列表"""
|
||||
provider_groups, total = ModelConfigRepository.get_list_new(db, query, tenant_id=tenant_id)
|
||||
|
||||
items = []
|
||||
for provider, models in provider_groups.items():
|
||||
# 验证每个模型并封装分组信息
|
||||
validated_models = [model_schema.ModelConfig.model_validate(model) for model in models]
|
||||
tags = list({model.type for model in validated_models})
|
||||
group_item = {
|
||||
"provider": provider, # 服务商名称
|
||||
"logo": validated_models[0].logo,
|
||||
"tags": tags,
|
||||
"models": validated_models # 该服务商下的所有模型
|
||||
}
|
||||
items.append(group_item)
|
||||
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def get_model_by_name(db: Session, name: str, tenant_id: uuid.UUID | None = None) -> ModelConfig:
|
||||
"""根据名称获取模型配置"""
|
||||
@@ -249,39 +228,37 @@ class ModelConfigService:
|
||||
|
||||
# 验证配置
|
||||
if not model_data.skip_validation and model_data.api_keys:
|
||||
api_key_data_list = model_data.api_keys
|
||||
for api_key_data in api_key_data_list:
|
||||
validation_result = await ModelConfigService.validate_model_config(
|
||||
db=db,
|
||||
model_name=api_key_data.model_name,
|
||||
provider=api_key_data.provider,
|
||||
api_key=api_key_data.api_key,
|
||||
api_base=api_key_data.api_base,
|
||||
model_type=model_data.type, # 传递模型类型
|
||||
test_message="Hello"
|
||||
api_key_data = model_data.api_keys
|
||||
validation_result = await ModelConfigService.validate_model_config(
|
||||
db=db,
|
||||
model_name=api_key_data.model_name,
|
||||
provider=api_key_data.provider,
|
||||
api_key=api_key_data.api_key,
|
||||
api_base=api_key_data.api_base,
|
||||
model_type=model_data.type, # 传递模型类型
|
||||
test_message="Hello"
|
||||
)
|
||||
if not validation_result["valid"]:
|
||||
raise BusinessException(
|
||||
f"模型配置验证失败: {validation_result['error']}",
|
||||
BizCode.INVALID_PARAMETER
|
||||
)
|
||||
if not validation_result["valid"]:
|
||||
raise BusinessException(
|
||||
f"模型配置验证失败: {validation_result['error']}",
|
||||
BizCode.INVALID_PARAMETER
|
||||
)
|
||||
|
||||
# 事务处理
|
||||
api_key_datas = model_data.api_keys
|
||||
model_config_data = model_data.model_dump(exclude={"api_keys", "skip_validation"})
|
||||
api_key_data = model_data.api_keys
|
||||
model_config_data = model_data.dict(exclude={"api_keys", "skip_validation"})
|
||||
# 添加租户ID
|
||||
model_config_data["tenant_id"] = tenant_id
|
||||
|
||||
model = ModelConfigRepository.create(db, model_config_data)
|
||||
db.flush() # 获取生成的 ID
|
||||
|
||||
if api_key_datas:
|
||||
for api_key_data in api_key_datas:
|
||||
api_key_create_schema = ModelApiKeyCreate(
|
||||
model_config_ids=[model.id],
|
||||
**api_key_data.model_dump()
|
||||
)
|
||||
ModelApiKeyRepository.create(db, api_key_create_schema)
|
||||
if api_key_data:
|
||||
api_key_create_schema = ModelApiKeyCreate(
|
||||
model_config_id=model.id,
|
||||
**api_key_data.dict()
|
||||
)
|
||||
ModelApiKeyRepository.create(db, api_key_create_schema)
|
||||
|
||||
db.commit()
|
||||
db.refresh(model)
|
||||
@@ -303,116 +280,6 @@ class ModelConfigService:
|
||||
db.refresh(model)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
async def create_composite_model(db: Session, model_data: model_schema.CompositeModelCreate, tenant_id: uuid.UUID) -> ModelConfig:
|
||||
"""创建组合模型"""
|
||||
if ModelConfigRepository.get_by_name(db, model_data.name, tenant_id=tenant_id):
|
||||
raise BusinessException("模型名称已存在", BizCode.DUPLICATE_NAME)
|
||||
|
||||
# 验证所有 API Key 存在且类型匹配
|
||||
for api_key_id in model_data.api_key_ids:
|
||||
api_key = ModelApiKeyRepository.get_by_id(db, api_key_id)
|
||||
if not api_key:
|
||||
raise BusinessException(f"API Key {api_key_id} 不存在", BizCode.NOT_FOUND)
|
||||
|
||||
# 检查 API Key 关联的模型配置类型
|
||||
for model_config in api_key.model_configs:
|
||||
# chat 和 llm 类型可以兼容
|
||||
compatible_types = {ModelType.LLM, ModelType.CHAT}
|
||||
config_type = model_config.type
|
||||
request_type = model_data.type
|
||||
|
||||
if not (config_type == request_type or
|
||||
(config_type in compatible_types and request_type in compatible_types)):
|
||||
raise BusinessException(
|
||||
f"API Key {api_key_id} 关联的模型类型 ({model_config.type}) 与组合模型类型 ({model_data.type}) 不匹配",
|
||||
BizCode.INVALID_PARAMETER
|
||||
)
|
||||
# if model_config.is_composite:
|
||||
# raise BusinessException(
|
||||
# f"API Key {api_key_id} 关联的模型是组合模型,不能用于创建新的组合模型",
|
||||
# BizCode.INVALID_PARAMETER
|
||||
# )
|
||||
|
||||
# 创建组合模型
|
||||
model_config_data = {
|
||||
"tenant_id": tenant_id,
|
||||
"name": model_data.name,
|
||||
"type": model_data.type,
|
||||
"logo": model_data.logo,
|
||||
"description": model_data.description,
|
||||
"provider": "composite",
|
||||
"config": model_data.config,
|
||||
"is_active": model_data.is_active,
|
||||
"is_public": model_data.is_public,
|
||||
"is_composite": True
|
||||
}
|
||||
if "load_balance_strategy" in model_data.model_fields_set:
|
||||
model_config_data["load_balance_strategy"] = model_data.load_balance_strategy
|
||||
|
||||
model = ModelConfigRepository.create(db, model_config_data)
|
||||
db.flush()
|
||||
|
||||
# 关联 API Keys
|
||||
for api_key_id in model_data.api_key_ids:
|
||||
api_key = ModelApiKeyRepository.get_by_id(db, api_key_id)
|
||||
if api_key:
|
||||
model.api_keys.append(api_key)
|
||||
|
||||
db.commit()
|
||||
db.refresh(model)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
async def update_composite_model(db: Session, model_id: uuid.UUID, model_data: model_schema.CompositeModelCreate, tenant_id: uuid.UUID) -> ModelConfig:
|
||||
"""更新组合模型"""
|
||||
existing_model = ModelConfigRepository.get_by_id(db, model_id, tenant_id=tenant_id)
|
||||
if not existing_model:
|
||||
raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND)
|
||||
|
||||
if not existing_model.is_composite:
|
||||
raise BusinessException("该模型不是组合模型", BizCode.INVALID_PARAMETER)
|
||||
|
||||
# 验证所有 API Key 存在且类型匹配
|
||||
for api_key_id in model_data.api_key_ids:
|
||||
api_key = ModelApiKeyRepository.get_by_id(db, api_key_id)
|
||||
if not api_key:
|
||||
raise BusinessException(f"API Key {api_key_id} 不存在", BizCode.NOT_FOUND)
|
||||
|
||||
for model_config in api_key.model_configs:
|
||||
compatible_types = {ModelType.LLM, ModelType.CHAT}
|
||||
config_type = model_config.type
|
||||
request_type = existing_model.type
|
||||
|
||||
if not (config_type == request_type or
|
||||
(config_type in compatible_types and request_type in compatible_types)):
|
||||
raise BusinessException(
|
||||
f"API Key {api_key_id} 关联的模型类型 ({model_config.type}) 与组合模型类型 ({model_data.type}) 不匹配",
|
||||
BizCode.INVALID_PARAMETER
|
||||
)
|
||||
|
||||
# 更新基本信息
|
||||
existing_model.name = model_data.name
|
||||
# existing_model.type = model_data.type
|
||||
existing_model.logo = model_data.logo
|
||||
existing_model.description = model_data.description
|
||||
existing_model.config = model_data.config
|
||||
existing_model.is_active = model_data.is_active
|
||||
existing_model.is_public = model_data.is_public
|
||||
if "load_balance_strategy" in model_data.model_fields_set:
|
||||
existing_model.load_balance_strategy = model_data.load_balance_strategy
|
||||
|
||||
# 更新 API Keys 关联
|
||||
existing_model.api_keys.clear()
|
||||
for api_key_id in model_data.api_key_ids:
|
||||
api_key = ModelApiKeyRepository.get_by_id(db, api_key_id)
|
||||
if api_key:
|
||||
existing_model.api_keys.append(api_key)
|
||||
|
||||
db.commit()
|
||||
db.refresh(existing_model)
|
||||
return existing_model
|
||||
|
||||
@staticmethod
|
||||
def delete_model(db: Session, model_id: uuid.UUID, tenant_id: uuid.UUID | None = None) -> bool:
|
||||
"""删除模型配置"""
|
||||
@@ -457,133 +324,27 @@ class ModelApiKeyService:
|
||||
return ModelApiKeyRepository.get_by_model_config(db, model_config_id, is_active)
|
||||
|
||||
@staticmethod
|
||||
async def create_api_key_by_provider(db: Session, data: model_schema.ModelApiKeyCreateByProvider) -> tuple[
|
||||
list[Any], list[Any]]:
|
||||
"""根据provider为多个ModelConfig创建API Key"""
|
||||
created_keys = []
|
||||
failed_models = [] # 记录验证失败的模型
|
||||
|
||||
for model_config_id in data.model_config_ids:
|
||||
model_config = ModelConfigRepository.get_by_id(db, model_config_id)
|
||||
if not model_config:
|
||||
continue
|
||||
|
||||
# 从ModelBase获取model_name
|
||||
model_name = model_config.model_base.name if model_config.model_base else model_config.name
|
||||
|
||||
# 检查是否存在API Key(包括软删除)
|
||||
existing_key = db.query(ModelApiKey).filter(
|
||||
ModelApiKey.api_key == data.api_key,
|
||||
ModelApiKey.provider == data.provider,
|
||||
ModelApiKey.model_name == model_name
|
||||
).first()
|
||||
|
||||
if existing_key:
|
||||
# 如果已存在,重新激活并更新
|
||||
if existing_key.is_active:
|
||||
continue
|
||||
existing_key.is_active = True
|
||||
existing_key.api_base = data.api_base
|
||||
existing_key.description = data.description
|
||||
existing_key.config = data.config
|
||||
existing_key.priority = data.priority
|
||||
existing_key.model_name = model_name
|
||||
|
||||
# 检查是否已关联该模型配置
|
||||
if model_config not in existing_key.model_configs:
|
||||
existing_key.model_configs.append(model_config)
|
||||
|
||||
created_keys.append(existing_key)
|
||||
continue
|
||||
|
||||
# 验证配置
|
||||
validation_result = await ModelConfigService.validate_model_config(
|
||||
async def create_api_key(db: Session, api_key_data: ModelApiKeyCreate) -> ModelApiKey:
|
||||
"""创建API Key"""
|
||||
model_config = ModelConfigRepository.get_by_id(db, api_key_data.model_config_id)
|
||||
if not model_config:
|
||||
raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND)
|
||||
|
||||
validation_result = await ModelConfigService.validate_model_config(
|
||||
db=db,
|
||||
model_name=model_name,
|
||||
provider=data.provider,
|
||||
api_key=data.api_key,
|
||||
api_base=data.api_base,
|
||||
model_type=model_config.type,
|
||||
model_name=api_key_data.model_name,
|
||||
provider=api_key_data.provider,
|
||||
api_key=api_key_data.api_key,
|
||||
api_base=api_key_data.api_base,
|
||||
model_type=model_config.type, # 传递模型类型
|
||||
test_message="Hello"
|
||||
)
|
||||
if not validation_result["valid"]:
|
||||
# 记录验证失败的模型,但不抛出异常
|
||||
failed_models.append(model_name)
|
||||
continue
|
||||
|
||||
# 创建API Key
|
||||
api_key_data = ModelApiKeyCreate(
|
||||
model_config_ids=[model_config_id],
|
||||
model_name=model_name,
|
||||
description=data.description,
|
||||
provider=data.provider,
|
||||
api_key=data.api_key,
|
||||
api_base=data.api_base,
|
||||
config=data.config,
|
||||
is_active=data.is_active,
|
||||
priority=data.priority
|
||||
)
|
||||
api_key_obj = ModelApiKeyRepository.create(db, api_key_data)
|
||||
created_keys.append(api_key_obj)
|
||||
|
||||
if created_keys:
|
||||
db.commit()
|
||||
for key in created_keys:
|
||||
db.refresh(key)
|
||||
|
||||
return created_keys, failed_models
|
||||
|
||||
@staticmethod
|
||||
async def create_api_key(db: Session, api_key_data: ModelApiKeyCreate) -> ModelApiKey:
|
||||
# 验证所有关联的模型配置是否存在
|
||||
if api_key_data.model_config_ids:
|
||||
for model_config_id in api_key_data.model_config_ids:
|
||||
model_config = ModelConfigRepository.get_by_id(db, model_config_id)
|
||||
if not model_config:
|
||||
raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND)
|
||||
|
||||
# 检查API Key是否已存在(包括软删除)
|
||||
existing_key = db.query(ModelApiKey).filter(
|
||||
ModelApiKey.api_key == api_key_data.api_key,
|
||||
ModelApiKey.provider == api_key_data.provider,
|
||||
ModelApiKey.model_name == api_key_data.model_name
|
||||
).first()
|
||||
|
||||
if existing_key:
|
||||
if existing_key.is_active:
|
||||
# 如果已激活,跳过
|
||||
raise BusinessException("该API Key已存在", BizCode.DUPLICATE_NAME)
|
||||
# 如果已存在,重新激活并更新
|
||||
existing_key.is_active = True
|
||||
existing_key.api_base = api_key_data.api_base
|
||||
existing_key.description = api_key_data.description
|
||||
existing_key.config = api_key_data.config
|
||||
existing_key.priority = api_key_data.priority
|
||||
existing_key.model_name = api_key_data.model_name
|
||||
|
||||
# 检查是否已关联该模型配置
|
||||
if model_config not in existing_key.model_configs:
|
||||
existing_key.model_configs.append(model_config)
|
||||
|
||||
db.commit()
|
||||
db.refresh(existing_key)
|
||||
return existing_key
|
||||
|
||||
# 验证配置
|
||||
validation_result = await ModelConfigService.validate_model_config(
|
||||
db=db,
|
||||
model_name=api_key_data.model_name,
|
||||
provider=api_key_data.provider,
|
||||
api_key=api_key_data.api_key,
|
||||
api_base=api_key_data.api_base,
|
||||
model_type=model_config.type,
|
||||
test_message="Hello"
|
||||
print(validation_result)
|
||||
if not validation_result["valid"]:
|
||||
raise BusinessException(
|
||||
f"模型配置验证失败: {validation_result['error']}",
|
||||
BizCode.INVALID_PARAMETER
|
||||
)
|
||||
if not validation_result["valid"]:
|
||||
raise BusinessException(
|
||||
f"模型配置验证失败: {validation_result['error']}",
|
||||
BizCode.INVALID_PARAMETER
|
||||
)
|
||||
|
||||
api_key = ModelApiKeyRepository.create(db, api_key_data)
|
||||
db.commit()
|
||||
@@ -598,19 +359,21 @@ class ModelApiKeyService:
|
||||
raise BusinessException("API Key不存在", BizCode.NOT_FOUND)
|
||||
|
||||
# 获取关联的模型配置以获取模型类型
|
||||
if existing_api_key.model_configs:
|
||||
model_config = existing_api_key.model_configs[0]
|
||||
|
||||
validation_result = await ModelConfigService.validate_model_config(
|
||||
model_config = ModelConfigRepository.get_by_id(db, existing_api_key.model_config_id)
|
||||
if not model_config:
|
||||
raise BusinessException("关联的模型配置不存在", BizCode.MODEL_NOT_FOUND)
|
||||
|
||||
validation_result = await ModelConfigService.validate_model_config(
|
||||
db=db,
|
||||
model_name=api_key_data.model_name or existing_api_key.model_name,
|
||||
provider=api_key_data.provider or existing_api_key.provider,
|
||||
api_key=api_key_data.api_key or existing_api_key.api_key,
|
||||
api_base=api_key_data.api_base or existing_api_key.api_base,
|
||||
model_type=model_config.type,
|
||||
model_name=api_key_data.model_name,
|
||||
provider=api_key_data.provider,
|
||||
api_key=api_key_data.api_key,
|
||||
api_base=api_key_data.api_base,
|
||||
model_type=model_config.type, # 传递模型类型
|
||||
test_message="Hello"
|
||||
)
|
||||
if not validation_result["valid"]:
|
||||
print(validation_result)
|
||||
if not validation_result["valid"]:
|
||||
raise BusinessException(
|
||||
f"模型配置验证失败: {validation_result['error']}",
|
||||
BizCode.INVALID_PARAMETER
|
||||
@@ -654,87 +417,3 @@ class ModelApiKeyService:
|
||||
if api_kes and len(api_kes) > 0:
|
||||
return api_kes[0]
|
||||
raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING)
|
||||
|
||||
|
||||
|
||||
class ModelBaseService:
|
||||
"""基础模型服务"""
|
||||
|
||||
@staticmethod
|
||||
def get_model_base_list(db: Session, query: model_schema.ModelBaseQuery, tenant_id: uuid.UUID = None) -> List:
|
||||
models = ModelBaseRepository.get_list(db, query)
|
||||
|
||||
provider_groups = {}
|
||||
for m in models:
|
||||
model_dict = model_schema.ModelBase.model_validate(m).model_dump()
|
||||
if tenant_id:
|
||||
model_dict['is_added'] = ModelBaseRepository.check_added_by_tenant(db, m.id, tenant_id)
|
||||
|
||||
provider = m.provider
|
||||
if provider not in provider_groups:
|
||||
provider_groups[provider] = {
|
||||
"provider": provider,
|
||||
"models": []
|
||||
}
|
||||
provider_groups[provider]["models"].append(model_dict)
|
||||
|
||||
return list(provider_groups.values())
|
||||
|
||||
@staticmethod
|
||||
def get_model_base_by_id(db: Session, model_base_id: uuid.UUID):
|
||||
model = ModelBaseRepository.get_by_id(db, model_base_id)
|
||||
if not model:
|
||||
raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def create_model_base(db: Session, data: model_schema.ModelBaseCreate):
|
||||
existing = ModelBaseRepository.get_by_name_and_provider(db, data.name, data.provider)
|
||||
if existing:
|
||||
raise BusinessException("模型已存在", BizCode.DUPLICATE_NAME)
|
||||
model_base = ModelBaseRepository.create(db, data.model_dump())
|
||||
db.commit()
|
||||
db.refresh(model_base)
|
||||
return model_base
|
||||
|
||||
@staticmethod
|
||||
def update_model_base(db: Session, model_base_id: uuid.UUID, data: model_schema.ModelBaseUpdate):
|
||||
model_base = ModelBaseRepository.update(db, model_base_id, data.model_dump(exclude_unset=True))
|
||||
if not model_base:
|
||||
raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND)
|
||||
db.commit()
|
||||
db.refresh(model_base)
|
||||
return model_base
|
||||
|
||||
@staticmethod
|
||||
def delete_model_base(db: Session, model_base_id: uuid.UUID) -> bool:
|
||||
success = ModelBaseRepository.delete(db, model_base_id)
|
||||
if not success:
|
||||
raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND)
|
||||
db.commit()
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
def add_model_from_plaza(db: Session, model_base_id: uuid.UUID, tenant_id: uuid.UUID) -> ModelConfig:
|
||||
model_base = ModelBaseRepository.get_by_id(db, model_base_id)
|
||||
if not model_base:
|
||||
raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND)
|
||||
|
||||
if ModelBaseRepository.check_added_by_tenant(db, model_base_id, tenant_id):
|
||||
raise BusinessException("模型已添加", BizCode.DUPLICATE_NAME)
|
||||
|
||||
model_config_data = {
|
||||
"model_id": model_base_id,
|
||||
"tenant_id": tenant_id,
|
||||
"name": model_base.name,
|
||||
"provider": model_base.provider,
|
||||
"type": model_base.type,
|
||||
"logo": model_base.logo,
|
||||
"description": model_base.description,
|
||||
"is_composite": False
|
||||
}
|
||||
model_config = ModelConfigRepository.create(db, model_config_data)
|
||||
ModelBaseRepository.increment_add_count(db, model_base_id)
|
||||
db.commit()
|
||||
db.refresh(model_config)
|
||||
return model_config
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import MultiAgentConfig, AgentConfig, ModelConfig
|
||||
from app.models.multi_agent_model import AggregationStrategy, OrchestrationMode
|
||||
from app.repositories.model_repository import ModelApiKeyRepository
|
||||
from app.services.agent_registry import AgentRegistry
|
||||
from app.services.master_agent_router import MasterAgentRouter
|
||||
from app.services.conversation_state_manager import ConversationStateManager
|
||||
@@ -2547,14 +2546,10 @@ class MultiAgentOrchestrator:
|
||||
return self._smart_merge_results(results, strategy)
|
||||
|
||||
# 获取 API Key 配置
|
||||
# api_key_config = self.db.query(ModelApiKey).join(
|
||||
# ModelConfig, ModelApiKey.model_configs
|
||||
# ).filter(
|
||||
# ModelConfig.id == default_model_config_id,
|
||||
# ModelApiKey.is_active.is_(True)
|
||||
# ).first()
|
||||
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, default_model_config_id)
|
||||
api_key_config = api_keys[0] if api_keys else None
|
||||
api_key_config = self.db.query(ModelApiKey).filter(
|
||||
ModelApiKey.model_config_id == default_model_config_id,
|
||||
ModelApiKey.is_active.is_(True)
|
||||
).first()
|
||||
|
||||
if not api_key_config:
|
||||
logger.warning("Master Agent 没有可用的 API Key,使用简单整合")
|
||||
@@ -2708,14 +2703,10 @@ class MultiAgentOrchestrator:
|
||||
return
|
||||
|
||||
# 获取 API Key 配置
|
||||
# api_key_config = self.db.query(ModelApiKey).join(
|
||||
# ModelConfig, ModelApiKey.model_configs
|
||||
# ).filter(
|
||||
# ModelConfig.id == default_model_config_id,
|
||||
# ModelApiKey.is_active.is_(True)
|
||||
# ).first()
|
||||
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, default_model_config_id)
|
||||
api_key_config = api_keys[0] if api_keys else None
|
||||
api_key_config = self.db.query(ModelApiKey).filter(
|
||||
ModelApiKey.model_config_id == default_model_config_id,
|
||||
ModelApiKey.is_active.is_(True)
|
||||
).first()
|
||||
|
||||
if not api_key_config:
|
||||
logger.warning("Master Agent 没有可用的 API Key,使用简单整合")
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.models.prompt_optimizer_model import (
|
||||
PromptOptimizerSession,
|
||||
RoleType
|
||||
)
|
||||
from app.repositories.model_repository import ModelConfigRepository, ModelApiKeyRepository
|
||||
from app.repositories.model_repository import ModelConfigRepository
|
||||
from app.repositories.prompt_optimizer_repository import (
|
||||
PromptOptimizerSessionRepository
|
||||
)
|
||||
@@ -168,8 +168,7 @@ class PromptOptimizerService:
|
||||
logger.info(f"Prompt optimization started, user_id={user_id}, session_id={session_id}")
|
||||
|
||||
# Create LLM instance
|
||||
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config.id)
|
||||
api_config: ModelApiKey = api_keys[0] if api_keys else None
|
||||
api_config: ModelApiKey = model_config.api_keys[0]
|
||||
llm = RedBearLLM(RedBearModelConfig(
|
||||
model_name=api_config.model_name,
|
||||
provider=api_config.provider,
|
||||
|
||||
@@ -4,8 +4,6 @@ import time
|
||||
import asyncio
|
||||
from typing import Optional, Dict, Any, AsyncGenerator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.repositories.model_repository import ModelApiKeyRepository
|
||||
from app.services.memory_konwledges_server import write_rag
|
||||
from app.models import ReleaseShare, AppRelease, Conversation
|
||||
from app.services.conversation_service import ConversationService
|
||||
@@ -166,20 +164,16 @@ class SharedChatService:
|
||||
raise ResourceNotFoundException("模型配置", str(model_config_id))
|
||||
|
||||
# 获取 API Key
|
||||
# stmt = (
|
||||
# select(ModelApiKey).join(
|
||||
# ModelConfig, ModelApiKey.model_configs
|
||||
# )
|
||||
# .where(
|
||||
# ModelConfig.id == model_config_id,
|
||||
# ModelApiKey.is_active.is_(True)
|
||||
# )
|
||||
# .order_by(ModelApiKey.priority.desc())
|
||||
# .limit(1)
|
||||
# )
|
||||
# api_key_obj = self.db.scalars(stmt).first()
|
||||
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config_id)
|
||||
api_key_obj = api_keys[0] if api_keys else None
|
||||
stmt = (
|
||||
select(ModelApiKey)
|
||||
.where(
|
||||
ModelApiKey.model_config_id == model_config_id,
|
||||
ModelApiKey.is_active.is_(True)
|
||||
)
|
||||
.order_by(ModelApiKey.priority.desc())
|
||||
.limit(1)
|
||||
)
|
||||
api_key_obj = self.db.scalars(stmt).first()
|
||||
if not api_key_obj:
|
||||
raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING)
|
||||
|
||||
@@ -364,20 +358,16 @@ class SharedChatService:
|
||||
raise ResourceNotFoundException("模型配置", str(model_config_id))
|
||||
|
||||
# 获取 API Key
|
||||
# stmt = (
|
||||
# select(ModelApiKey).join(
|
||||
# ModelConfig, ModelApiKey.model_configs
|
||||
# )
|
||||
# .where(
|
||||
# ModelConfig.id == model_config_id,
|
||||
# ModelApiKey.is_active.is_(True)
|
||||
# )
|
||||
# .order_by(ModelApiKey.priority.desc())
|
||||
# .limit(1)
|
||||
# )
|
||||
# api_key_obj = self.db.scalars(stmt).first()
|
||||
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config_id)
|
||||
api_key_obj = api_keys[0] if api_keys else None
|
||||
stmt = (
|
||||
select(ModelApiKey)
|
||||
.where(
|
||||
ModelApiKey.model_config_id == model_config_id,
|
||||
ModelApiKey.is_active.is_(True)
|
||||
)
|
||||
.order_by(ModelApiKey.priority.desc())
|
||||
.limit(1)
|
||||
)
|
||||
api_key_obj = self.db.scalars(stmt).first()
|
||||
if not api_key_obj:
|
||||
raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING)
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
"""
|
||||
Configuration utility functions
|
||||
|
||||
Shared utilities for configuration handling to avoid circular imports.
|
||||
"""
|
||||
from uuid import UUID
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
def resolve_config_id(config_id: UUID | int|str, db: Session) -> UUID:
|
||||
"""
|
||||
解析 config_id,如果是整数则通过 config_id_old 查找对应的 UUID
|
||||
|
||||
Args:
|
||||
config_id: 配置ID(UUID 或整数)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
UUID: 解析后的配置ID
|
||||
|
||||
Raises:
|
||||
ValueError: 当找不到对应的配置时
|
||||
"""
|
||||
|
||||
from app.models.memory_config_model import MemoryConfig
|
||||
if isinstance(config_id, UUID):
|
||||
return config_id
|
||||
if isinstance(config_id, str) and len(config_id)<=6:
|
||||
memory_config = db.query(MemoryConfig).filter(
|
||||
MemoryConfig.config_id_old == int(config_id)
|
||||
).first()
|
||||
print(memory_config)
|
||||
if not memory_config:
|
||||
raise ValueError(f"STR 未找到 config_id_old={config_id} 对应的配置")
|
||||
return memory_config.config_id
|
||||
if isinstance(config_id, int):
|
||||
memory_config = db.query(MemoryConfig).filter(
|
||||
MemoryConfig.config_id_old == config_id
|
||||
).first()
|
||||
|
||||
if not memory_config:
|
||||
raise ValueError(f"INT 未找到 config_id_old={config_id} 对应的配置")
|
||||
|
||||
return memory_config.config_id
|
||||
|
||||
return config_id
|
||||
@@ -5,12 +5,11 @@
|
||||
"releaseDate": "2026-1-23",
|
||||
"upgradePosition": "\uD83D\uDC3B 本次更新主要优化使用体验和修复已知问题,让系统更稳定、更好用。",
|
||||
"coreUpgrades": [
|
||||
"1. 工作流更好用了<br>* 界面更清晰,一眼看懂怎么配置<br>* 新增节点输出变量展示,方便其他节点引用<br>* 修复了几个影响体验的bug",
|
||||
"2. 智能体配置更简单<br>* 提示词和变量联动更顺畅<br>* 配置界面重新整理,找功能更方便",
|
||||
"3. 记忆系统更稳定<br>* 优化了情绪记忆和隐性记忆的缓存更新<br>* 修复了记忆配置页面的报错问题<br>* 现在能自动识别用户和AI的身份了",
|
||||
"4. 知识库体验提升<br>* 修复了文档解析异常的问题<br>* 上传文档时能看到处理进度了<br>* 取消了操作也不会报错了",
|
||||
"5. 系统整体更可靠<br>* 修复了新用户访问跳转问题<br>* 流式接口更稳定,长对话不断线<br>* 调整了菜单顺序,操作更顺手",
|
||||
"<br>",
|
||||
"1. 工作流更好用了\n* 界面更清晰,一眼看懂怎么配置\n* 新增节点输出变量展示,方便其他节点引用\n* 修复了几个影响体验的bug",
|
||||
"2. 智能体配置更简单\n* 提示词和变量联动更顺畅\n* 配置界面重新整理,找功能更方便",
|
||||
"3. 记忆系统更稳定\n* 优化了情绪记忆和隐性记忆的缓存更新\n* 修复了记忆配置页面的报错问题\n* 现在能自动识别用户和AI的身份了",
|
||||
"4. 知识库体验提升\n* 修复了文档解析异常的问题\n* 上传文档时能看到处理进度了\n* 取消了操作也不会报错了",
|
||||
"5. 系统整体更可靠\n* 修复了新用户访问跳转问题\n* 流式接口更稳定,长对话不断线\n* 调整了菜单顺序,操作更顺手\n",
|
||||
"这次更新虽然不大,但让记忆熊的基础更扎实、体验更流畅。我们继续努力,让AI记忆更好用!",
|
||||
"记忆熊,记得更牢,用得更好。\uD83D\uDC3B✨"
|
||||
]
|
||||
@@ -20,13 +19,12 @@
|
||||
"releaseDate": "2026-1-23",
|
||||
"upgradePosition": "\uD83D\uDC3B This update focuses on improving usability and fixing known issues, making the system more stable and easier to use overall.",
|
||||
"coreUpgrades": [
|
||||
"1. Improved Workflow Experience<br>* Cleaner, more intuitive UI for easier configuration at a glance<br>* Added visibility of node output variables, making them easier to reference in downstream nodes<br>* Fixed several usability-related bugs that affected the workflow experience",
|
||||
"2. Simpler Agent Configuration<br>* Smoother linkage between prompts and variables<br>* Reorganized configuration layout for easier navigation and better clarity",
|
||||
"3. More Stable Memory System<br>* Optimized cache refresh for emotional memory and implicit memory<br>* Fixed error issues on the memory configuration page<br>* The system can now automatically distinguish between user and AI roles",
|
||||
"4. Enhanced Knowledge Base Experience<br>* Fixed issues with document parsing failures<br>* Upload progress is now displayed during document processing<br>* Canceling an upload no longer triggers errors",
|
||||
"5. Overall System Reliability Improvements<br>* Fixed redirect issues affecting new users<br>* Improved stability of streaming APIs to prevent interruptions during long conversations<br>* Adjusted menu ordering for a smoother and more intuitive workflow",
|
||||
"<br>",
|
||||
"Although this is a relatively small update, it strengthens MemoryBear’s foundation and delivers a noticeably smoother experience. We’ll keep refining the system to make AI memory more powerful and easier to use.",
|
||||
"1. Improved Workflow Experience\nCleaner, more intuitive UI for easier configuration at a glance\nAdded visibility of node output variables, making them easier to reference in downstream nodes\nFixed several usability-related bugs that affected the workflow experience",
|
||||
"2. Simpler Agent Configuration\nSmoother linkage between prompts and variables\nReorganized configuration layout for easier navigation and better clarity",
|
||||
"3. More Stable Memory System\nOptimized cache refresh for emotional memory and implicit memory\nFixed error issues on the memory configuration page\nThe system can now automatically distinguish between user and AI roles",
|
||||
"4. Enhanced Knowledge Base Experience\nFixed issues with document parsing failures\nUpload progress is now displayed during document processing\nCanceling an upload no longer triggers errors",
|
||||
"5. Overall System Reliability Improvements\nFixed redirect issues affecting new users\nImproved stability of streaming APIs to prevent interruptions during long conversations\nAdjusted menu ordering for a smoother and more intuitive workflow\n",
|
||||
"Although this is a relatively small update, it strengthens MemoryBear’s foundation and delivers a noticeably smoother experience.\nWe’ll keep refining the system to make AI memory more powerful and easier to use.",
|
||||
"MemoryBear — remember better, work smarter. \uD83D\uDC3B✨"
|
||||
]
|
||||
}
|
||||
@@ -37,10 +35,10 @@
|
||||
"releaseDate": "2026-1-16",
|
||||
"upgradePosition": "本次为架构升级,核心目标是把\"被动存储\"升级为\"主动认知\",让系统具备情绪感知、情景理解与类人记忆机制,为后续多智能体协作与专业场景落地奠定底座。",
|
||||
"coreUpgrades": [
|
||||
"1. 记忆详情:拟人记忆——情绪引擎、情景记忆、短期记忆、工作记忆、感知记忆、显性记忆、隐性记忆,并配套类脑遗忘机制,实现从感知→情绪→情景→长期沉淀的完整人类记忆闭环",
|
||||
"2. 可视化工作流:拖拽式节点编排(LLM、知识库、逻辑、工具),业务落地周期由天缩至小时。",
|
||||
"3. 多模态知识处理:PDF、PPT、MP3、MP4 一键解析,时间感知检索准确率 94.3%,问答对数据即插即用。",
|
||||
"4. Agent集群内置\"记忆-知识-工具-审核\"四类角色模板,用户一键生成;主控Agent把复杂任务拆为子任务并行分发,再靠情景记忆统一消解冲突、校验一致性,输出完整报告。"
|
||||
"记忆详情:拟人记忆——情绪引擎、情景记忆、短期记忆、工作记忆、感知记忆、显性记忆、隐性记忆,并配套类脑遗忘机制,实现从感知→情绪→情景→长期沉淀的完整人类记忆闭环",
|
||||
"可视化工作流:拖拽式节点编排(LLM、知识库、逻辑、工具),业务落地周期由天缩至小时。",
|
||||
"多模态知识处理:PDF、PPT、MP3、MP4 一键解析,时间感知检索准确率 94.3%,问答对数据即插即用。",
|
||||
"Agent集群内置\"记忆-知识-工具-审核\"四类角色模板,用户一键生成;主控Agent把复杂任务拆为子任务并行分发,再靠情景记忆统一消解冲突、校验一致性,输出完整报告。"
|
||||
]
|
||||
},
|
||||
"introduction_en": {
|
||||
@@ -48,10 +46,10 @@
|
||||
"releaseDate": "2026-1-16",
|
||||
"upgradePosition": "This release marks a foundational upgrade to the system’s cognitive architecture. The core objective is to evolve the platform from passive information storage into active cognitive intelligence—enabling emotional awareness, situational understanding, and human-like memory mechanisms. This upgrade lays the groundwork for future multi-agent collaboration and domain-specific, production-grade AI applications.",
|
||||
"coreUpgrades": [
|
||||
"1. Human-Like Memory Architecture: A comprehensive, human-inspired memory system is introduced, encompassing emotional processing, situational memory, short-term and working memory, perceptual memory, as well as explicit and implicit memory. Combined with brain-inspired forgetting mechanisms, the system now supports a complete cognitive loop—from perception → emotion → context → long-term consolidation, closely mirroring human memory formation.",
|
||||
"2. Visual Workflow Orchestration: A fully visual, drag-and-drop workflow enables modular composition of LLMs, knowledge bases, logic, and tools. This dramatically reduces the time required to move from experimentation to production—from days to hours.",
|
||||
"3. Multimodal Knowledge Processing: The system now supports one-click parsing and ingestion of PDF, PPT, MP3, and MP4 content. With time-aware retrieval accuracy reaching 94.3%, structured Q&A data becomes instantly usable for downstream reasoning and generation.",
|
||||
"4. Built-in Agent Clusters: Predefined role templates across four categories—Memory, Knowledge, Tools, and Review—can be generated with a single click. A Coordinator Agent decomposes complex tasks into parallel subtasks, while situational memory is used to resolve conflicts, validate consistency, and synthesize outputs into a coherent, end-to-end report."
|
||||
"Human-Like Memory Architecture: A comprehensive, human-inspired memory system is introduced, encompassing emotional processing, situational memory, short-term and working memory, perceptual memory, as well as explicit and implicit memory. Combined with brain-inspired forgetting mechanisms, the system now supports a complete cognitive loop—from perception → emotion → context → long-term consolidation, closely mirroring human memory formation.",
|
||||
"Visual Workflow Orchestration: A fully visual, drag-and-drop workflow enables modular composition of LLMs, knowledge bases, logic, and tools. This dramatically reduces the time required to move from experimentation to production—from days to hours.",
|
||||
"Multimodal Knowledge Processing: The system now supports one-click parsing and ingestion of PDF, PPT, MP3, and MP4 content. With time-aware retrieval accuracy reaching 94.3%, structured Q&A data becomes instantly usable for downstream reasoning and generation.",
|
||||
"Built-in Agent Clusters: Predefined role templates across four categories—Memory, Knowledge, Tools, and Review—can be generated with a single click. A Coordinator Agent decomposes complex tasks into parallel subtasks, while situational memory is used to resolve conflicts, validate consistency, and synthesize outputs into a coherent, end-to-end report."
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -61,17 +59,16 @@
|
||||
"releaseDate": "2025-12-01",
|
||||
"upgradePosition": "这是一款专注于管理和利用AI记忆的工具,支持RAG和知识图谱两种主流存储方式,旨在为AI应用提供持久化、结构化的\"记忆\"能力。",
|
||||
"coreUpgrades": [
|
||||
"1. 记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。",
|
||||
"2. 记忆配置:简化了配置流程,内置自动提取关键信息的\"记忆萃取\"和管理生命周期的\"遗忘\"引擎。",
|
||||
"3. 知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。",
|
||||
"4. 全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。",
|
||||
"5. 测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。",
|
||||
"6. 记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解AI的\"记忆\"内容。",
|
||||
"7. 集成与管理:提供API Key用于系统集成,并包含基本的用户管理功能。",
|
||||
"8. 界面与体验:采用现代化的卡片式布局和渐变色设计,注重交互的流畅性和视觉美感。",
|
||||
"9. 起步与使用:文档中提供了清晰的基础使用流程,引导用户从创建空间、配置记忆到测试检索快速上手。",
|
||||
"10. 版本说明与限制: 记忆熊 v0.1.0 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。",
|
||||
"<br>",
|
||||
"记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。",
|
||||
"记忆配置:简化了配置流程,内置自动提取关键信息的\"记忆萃取\"和管理生命周期的\"遗忘\"引擎。",
|
||||
"知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。",
|
||||
"全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。",
|
||||
"测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。",
|
||||
"记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解AI的\"记忆\"内容。",
|
||||
"集成与管理:提供API Key用于系统集成,并包含基本的用户管理功能。",
|
||||
"界面与体验:采用现代化的卡片式布局和渐变色设计,注重交互的流畅性和视觉美感。",
|
||||
"起步与使用:文档中提供了清晰的基础使用流程,引导用户从创建空间、配置记忆到测试检索快速上手。",
|
||||
"版本说明与限制: 记忆熊 v0.1.0 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。",
|
||||
"文档资源:用户手册、API文档、FAQ",
|
||||
"问题反馈:GitHub Issues、邮件支持",
|
||||
"致谢:感谢所有参与测试和提供反馈的用户!"
|
||||
@@ -82,17 +79,16 @@
|
||||
"releaseDate": "2025-12-01",
|
||||
"upgradePosition": "A tool focused on managing and utilizing AI memory, supporting both RAG and knowledge graph storage methods, aiming to provide persistent and structured 'memory' capabilities for AI applications.",
|
||||
"coreUpgrades": [
|
||||
"1. Memory Space: Users can create independent spaces to isolate different memories and flexibly choose storage methods.",
|
||||
"2. Memory Configuration: Simplified configuration process with built-in 'memory extraction' for automatic key information extraction and 'forgetting' engine for lifecycle management.",
|
||||
"3. Knowledge Retrieval: Provides semantic, tokenization, and hybrid retrieval modes with various parameter tuning and result reranking to improve recall.",
|
||||
"4. Global Management: Supports unified default retrieval parameter settings with one-click application to all knowledge bases.",
|
||||
"5. Testing & Debugging: Built-in 'recall testing' for real-time verification of retrieval effects and parameter adjustment, with sharing code support for collaboration.",
|
||||
"6. Memory Insights: View detailed conversation records, user profiles, and analysis reports to understand AI 'memory' content.",
|
||||
"7. Integration & Management: Provides API Key for system integration with basic user management features.",
|
||||
"8. Interface & Experience: Modern card-based layout with gradient design, focusing on interaction fluidity and visual aesthetics.",
|
||||
"9. Getting Started: Documentation provides clear basic usage flow, guiding users from creating spaces, configuring memory to testing retrieval.",
|
||||
"10. Version Notes: MemoryBear v0.1.0 'Original Intent' encompasses core concepts and basic capabilities of intelligent memory management, laying foundation for future development.",
|
||||
"<br>",
|
||||
"Memory Space: Users can create independent spaces to isolate different memories and flexibly choose storage methods.",
|
||||
"Memory Configuration: Simplified configuration process with built-in 'memory extraction' for automatic key information extraction and 'forgetting' engine for lifecycle management.",
|
||||
"Knowledge Retrieval: Provides semantic, tokenization, and hybrid retrieval modes with various parameter tuning and result reranking to improve recall.",
|
||||
"Global Management: Supports unified default retrieval parameter settings with one-click application to all knowledge bases.",
|
||||
"Testing & Debugging: Built-in 'recall testing' for real-time verification of retrieval effects and parameter adjustment, with sharing code support for collaboration.",
|
||||
"Memory Insights: View detailed conversation records, user profiles, and analysis reports to understand AI 'memory' content.",
|
||||
"Integration & Management: Provides API Key for system integration with basic user management features.",
|
||||
"Interface & Experience: Modern card-based layout with gradient design, focusing on interaction fluidity and visual aesthetics.",
|
||||
"Getting Started: Documentation provides clear basic usage flow, guiding users from creating spaces, configuring memory to testing retrieval.",
|
||||
"Version Notes: MemoryBear v0.1.0 'Original Intent' encompasses core concepts and basic capabilities of intelligent memory management, laying foundation for future development.",
|
||||
"Documentation: User Manual, API Documentation, FAQ",
|
||||
"Feedback: GitHub Issues, Email Support",
|
||||
"Acknowledgments: Thanks to all users who participated in testing and provided feedback!"
|
||||
|
||||
@@ -28,15 +28,7 @@ def upgrade() -> None:
|
||||
op.drop_constraint('data_config_pkey', 'memory_config', type_='primary')
|
||||
op.alter_column('memory_config', 'config_id', new_column_name='config_id_old', nullable=True)
|
||||
op.add_column('memory_config', sa.Column('config_id', sa.UUID(), nullable=True))
|
||||
# Handle rows where apply_id might be NULL or invalid - generate new UUIDs for those
|
||||
op.execute("""
|
||||
UPDATE memory_config
|
||||
SET config_id = CASE
|
||||
WHEN apply_id IS NOT NULL AND apply_id ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
THEN apply_id::uuid
|
||||
ELSE gen_random_uuid()
|
||||
END
|
||||
""")
|
||||
op.execute("UPDATE memory_config SET config_id = apply_id::uuid")
|
||||
op.alter_column('memory_config', 'config_id', nullable=False)
|
||||
op.create_primary_key('memory_config_pkey', 'memory_config', ['config_id'])
|
||||
op.execute("ALTER TABLE memory_config ALTER COLUMN config_id_old DROP DEFAULT")
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
"""202601291352
|
||||
|
||||
Revision ID: 5ca246ee7dd4
|
||||
Revises: 915bed077f8d
|
||||
Create Date: 2026-01-29 13:52:47.647306
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5ca246ee7dd4'
|
||||
down_revision: Union[str, None] = '915bed077f8d'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('model_bases', sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('model_bases', 'created_at')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,80 +0,0 @@
|
||||
"""20260129212722
|
||||
|
||||
Revision ID: 5de9b1e28509
|
||||
Revises: 5ca246ee7dd4
|
||||
Create Date: 2026-01-29 21:34:30.978031
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5de9b1e28509'
|
||||
down_revision: Union[str, None] = '5ca246ee7dd4'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Neo4j migration: rename group_id to end_user_id
|
||||
import asyncio
|
||||
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
|
||||
async def run_neo4j_upgrade():
|
||||
connector = Neo4jConnector()
|
||||
try:
|
||||
async def transaction_func(tx):
|
||||
result = await tx.run("""
|
||||
MATCH (n)
|
||||
WHERE n.group_id IS NOT NULL
|
||||
SET n.end_user_id = n.group_id
|
||||
REMOVE n.group_id
|
||||
WITH count(n) AS node_count
|
||||
MATCH ()-[r]->()
|
||||
WHERE r.group_id IS NOT NULL
|
||||
SET r.end_user_id = r.group_id
|
||||
REMOVE r.group_id
|
||||
RETURN node_count, count(r) AS rel_count
|
||||
""")
|
||||
return await result.data()
|
||||
|
||||
await connector.execute_write_transaction(transaction_func)
|
||||
finally:
|
||||
await connector.close()
|
||||
|
||||
asyncio.run(run_neo4j_upgrade())
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Neo4j migration: rename end_user_id back to group_id
|
||||
import asyncio
|
||||
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
|
||||
async def run_neo4j_downgrade():
|
||||
connector = Neo4jConnector()
|
||||
try:
|
||||
async def transaction_func(tx):
|
||||
result = await tx.run("""
|
||||
MATCH (n)
|
||||
WHERE n.end_user_id IS NOT NULL
|
||||
SET n.group_id = n.end_user_id
|
||||
REMOVE n.end_user_id
|
||||
WITH count(n) AS node_count
|
||||
MATCH ()-[r]->()
|
||||
WHERE r.end_user_id IS NOT NULL
|
||||
SET r.group_id = r.end_user_id
|
||||
REMOVE r.end_user_id
|
||||
RETURN node_count, count(r) AS rel_count
|
||||
""")
|
||||
return await result.data()
|
||||
|
||||
await connector.execute_write_transaction(transaction_func)
|
||||
finally:
|
||||
await connector.close()
|
||||
|
||||
asyncio.run(run_neo4j_downgrade())
|
||||
@@ -1,224 +0,0 @@
|
||||
"""202601281340
|
||||
|
||||
Revision ID: 915bed077f8d
|
||||
Revises: 75f0ec80e50b
|
||||
Create Date: 2026-01-28 13:38:49.471560
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '915bed077f8d'
|
||||
down_revision: Union[str, None] = '75f0ec80e50b'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
BACKUP_TABLE_NAME = 'model_api_keys_backup_20260123'
|
||||
|
||||
def get_temp_models():
|
||||
"""创建临时模型,用于迁移过程中查询数据"""
|
||||
metadata = sa.MetaData()
|
||||
|
||||
# 临时ModelApiKey表(仅包含需要的字段)
|
||||
ModelApiKey = sa.Table(
|
||||
'model_api_keys', metadata,
|
||||
sa.Column('id', sa.UUID(), primary_key=True),
|
||||
sa.Column('model_config_id', sa.UUID(), nullable=True),
|
||||
)
|
||||
|
||||
# 临时关联表(和升级脚本创建的表结构一致)
|
||||
ModelConfigApiKeyAssociation = sa.Table(
|
||||
'model_config_api_key_association', metadata,
|
||||
sa.Column('model_config_id', sa.UUID(), nullable=False),
|
||||
sa.Column('api_key_id', sa.UUID(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
)
|
||||
|
||||
ModelApiKeyBackup = sa.Table(
|
||||
BACKUP_TABLE_NAME, metadata,
|
||||
sa.Column('id', sa.UUID(), primary_key=True),
|
||||
sa.Column('model_name', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('provider', sa.String(), nullable=False),
|
||||
sa.Column('api_key', sa.String(), nullable=False),
|
||||
sa.Column('api_base', sa.String(), nullable=True),
|
||||
sa.Column('config', sa.JSON(), nullable=True),
|
||||
sa.Column('usage_count', sa.String(), default="0"),
|
||||
sa.Column('last_used_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('priority', sa.String(), default="1"),
|
||||
sa.Column('model_config_id', sa.UUID(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), default=True),
|
||||
)
|
||||
|
||||
return ModelApiKey, ModelConfigApiKeyAssociation, ModelApiKeyBackup
|
||||
|
||||
|
||||
def backup_model_api_keys():
|
||||
"""备份model_api_keys表的结构和数据"""
|
||||
connection = op.get_bind()
|
||||
|
||||
# 检查备份表是否已存在
|
||||
result = connection.execute(sa.text(f"""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = '{BACKUP_TABLE_NAME}'
|
||||
);
|
||||
""")).scalar()
|
||||
|
||||
if result:
|
||||
# 备份表已存在,先删除再重建(确保结构一致)
|
||||
op.execute(f"DROP TABLE IF EXISTS {BACKUP_TABLE_NAME};")
|
||||
|
||||
# 直接复制表结构和数据(PostgreSQL专用,一步完成)
|
||||
op.execute(f"""
|
||||
CREATE TABLE {BACKUP_TABLE_NAME} AS
|
||||
SELECT * FROM model_api_keys;
|
||||
""")
|
||||
|
||||
# 统计行数
|
||||
backup_count = connection.execute(sa.text(f"SELECT COUNT(*) FROM {BACKUP_TABLE_NAME}")).scalar()
|
||||
original_count = connection.execute(sa.text("SELECT COUNT(*) FROM model_api_keys")).scalar()
|
||||
|
||||
print(
|
||||
f"已备份model_api_keys表到 {BACKUP_TABLE_NAME} \n"
|
||||
f" 原表数据行数:{original_count} | 备份表数据行数:{backup_count}"
|
||||
)
|
||||
|
||||
# def restore_model_api_keys_from_backup():
|
||||
# """从备份表恢复model_api_keys数据(可选,用于回滚失败时手动恢复)"""
|
||||
# # 1. 清空原表(谨慎使用!)
|
||||
# # op.execute("TRUNCATE TABLE model_api_keys;")
|
||||
#
|
||||
# # 2. 从备份表恢复数据
|
||||
# op.execute(f"""
|
||||
# INSERT INTO model_api_keys
|
||||
# SELECT * FROM {BACKUP_TABLE_NAME}
|
||||
# ON CONFLICT (id) DO UPDATE SET
|
||||
# model_name = EXCLUDED.model_name,
|
||||
# description = EXCLUDED.description,
|
||||
# provider = EXCLUDED.provider,
|
||||
# api_key = EXCLUDED.api_key,
|
||||
# api_base = EXCLUDED.api_base,
|
||||
# config = EXCLUDED.config,
|
||||
# usage_count = EXCLUDED.usage_count,
|
||||
# last_used_at = EXCLUDED.last_used_at,
|
||||
# priority = EXCLUDED.priority,
|
||||
# model_config_id = EXCLUDED.model_config_id,
|
||||
# created_at = EXCLUDED.created_at,
|
||||
# updated_at = EXCLUDED.updated_at,
|
||||
# is_active = EXCLUDED.is_active;
|
||||
# """)
|
||||
# print(f"✅ 已从 {BACKUP_TABLE_NAME} 恢复model_api_keys表数据")
|
||||
|
||||
def upgrade() -> None:
|
||||
backup_model_api_keys()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('model_bases',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('logo', sa.String(length=255), nullable=True, comment='模型logo图片URL'),
|
||||
sa.Column('name', sa.String(), nullable=False, comment='模型唯一标识(如gpt-3.5-turbo)'),
|
||||
sa.Column('type', sa.String(), nullable=False, comment='模型类型'),
|
||||
sa.Column('provider', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True, comment='模型描述'),
|
||||
sa.Column('is_deprecated', sa.Boolean(), nullable=False, comment='是否弃用'),
|
||||
sa.Column('is_official', sa.Boolean(), nullable=True, comment='是否供应商官方模型(区分自定义)'),
|
||||
sa.Column('tags', sa.ARRAY(sa.String()), nullable=False, comment="模型标签(如['聊天', '创作'])"),
|
||||
sa.Column('add_count', sa.Integer(), nullable=False, comment='模型被用户添加的次数'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name', 'provider', name='uk_model_name_provider')
|
||||
)
|
||||
op.create_index(op.f('ix_model_bases_id'), 'model_bases', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_model_bases_provider'), 'model_bases', ['provider'], unique=False)
|
||||
op.create_index(op.f('ix_model_bases_type'), 'model_bases', ['type'], unique=False)
|
||||
op.create_table('model_config_api_key_association',
|
||||
sa.Column('model_config_id', sa.UUID(), nullable=False),
|
||||
sa.Column('api_key_id', sa.UUID(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['api_key_id'], ['model_api_keys.id'], ),
|
||||
sa.ForeignKeyConstraint(['model_config_id'], ['model_configs.id'], ),
|
||||
sa.PrimaryKeyConstraint('model_config_id', 'api_key_id')
|
||||
)
|
||||
op.add_column('model_api_keys', sa.Column('description', sa.String(), nullable=True, comment='备注'))
|
||||
op.add_column('model_configs', sa.Column('model_id', sa.UUID(), nullable=True, comment='基础模型ID'))
|
||||
op.add_column('model_configs', sa.Column('logo', sa.String(length=255), nullable=True, comment='模型logo图片URL'))
|
||||
op.add_column('model_configs', sa.Column('provider', sa.String(), server_default='composite', nullable=False, comment='供应商'))
|
||||
op.add_column('model_configs', sa.Column('is_composite', sa.Boolean(), server_default='true', nullable=False, comment='是否为组合模型'))
|
||||
op.add_column('model_configs', sa.Column('load_balance_strategy', sa.String(), nullable=True, comment='负载均衡策略'))
|
||||
op.create_index(op.f('ix_model_configs_model_id'), 'model_configs', ['model_id'], unique=False)
|
||||
op.create_foreign_key("model_configs_model_id_fkey", 'model_configs', 'model_bases', ['model_id'], ['id'])
|
||||
connection = op.get_bind()
|
||||
ModelApiKey, ModelConfigApiKeyAssociation, _ = get_temp_models()
|
||||
|
||||
# 查询所有有model_config_id的API Key
|
||||
api_keys = connection.execute(
|
||||
sa.select(ModelApiKey.c.id, ModelApiKey.c.model_config_id)
|
||||
.where(ModelApiKey.c.model_config_id.isnot(None))
|
||||
).fetchall()
|
||||
|
||||
# 批量插入到多对多表
|
||||
if api_keys:
|
||||
association_data = [
|
||||
{
|
||||
'model_config_id': row.model_config_id,
|
||||
'api_key_id': row.id
|
||||
}
|
||||
for row in api_keys
|
||||
]
|
||||
connection.execute(ModelConfigApiKeyAssociation.insert(), association_data)
|
||||
op.drop_constraint(op.f('model_api_keys_model_config_id_fkey'), 'model_api_keys', type_='foreignkey')
|
||||
op.drop_column('model_api_keys', 'model_config_id')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint("model_configs_model_id_fkey", 'model_configs', type_='foreignkey')
|
||||
op.drop_index(op.f('ix_model_configs_model_id'), table_name='model_configs')
|
||||
op.drop_column('model_configs', 'load_balance_strategy')
|
||||
op.drop_column('model_configs', 'is_composite')
|
||||
op.drop_column('model_configs', 'provider')
|
||||
op.drop_column('model_configs', 'logo')
|
||||
op.drop_column('model_configs', 'model_id')
|
||||
op.add_column('model_api_keys', sa.Column('model_config_id', sa.UUID(), autoincrement=False, nullable=True, comment='模型配置ID'))
|
||||
connection = op.get_bind()
|
||||
ModelApiKey, ModelConfigApiKeyAssociation, _ = get_temp_models()
|
||||
|
||||
# 查询多对多表中的关联数据(取每个API Key的第一个关联的model_config_id)
|
||||
association_data = connection.execute(
|
||||
sa.select(
|
||||
ModelConfigApiKeyAssociation.c.api_key_id,
|
||||
ModelConfigApiKeyAssociation.c.model_config_id
|
||||
).distinct(ModelConfigApiKeyAssociation.c.api_key_id)
|
||||
).fetchall()
|
||||
|
||||
# 批量更新model_api_keys表
|
||||
if association_data:
|
||||
for api_key_id, model_config_id in association_data:
|
||||
connection.execute(
|
||||
sa.update(ModelApiKey)
|
||||
.where(ModelApiKey.c.id == api_key_id)
|
||||
.values(model_config_id=model_config_id)
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"UPDATE model_api_keys SET model_config_id = '00000000-0000-0000-0000-000000000000' WHERE model_config_id IS NULL")
|
||||
op.alter_column('model_api_keys', 'model_config_id', nullable=False)
|
||||
op.create_foreign_key(op.f('model_api_keys_model_config_id_fkey'), 'model_api_keys', 'model_configs', ['model_config_id'], ['id'])
|
||||
op.drop_column('model_api_keys', 'description')
|
||||
op.drop_table('model_config_api_key_association')
|
||||
# ### 可选:回滚时恢复备份(如需)###
|
||||
# restore_model_api_keys_from_backup()
|
||||
|
||||
print(
|
||||
f"回滚完成!备份表 {BACKUP_TABLE_NAME} 仍保留,如需手动恢复可执行 restore_model_api_keys_from_backup() 函数")
|
||||
op.drop_index(op.f('ix_model_bases_type'), table_name='model_bases')
|
||||
op.drop_index(op.f('ix_model_bases_provider'), table_name='model_bases')
|
||||
op.drop_index(op.f('ix_model_bases_id'), table_name='model_bases')
|
||||
op.drop_table('model_bases')
|
||||
# ### end Alembic commands ###
|
||||
38
api_key_mcp_server.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""API Key认证MCP服务器"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||
from typing import Optional
|
||||
import uvicorn
|
||||
from mcp_base import MCPRequest, handle_mcp_request, TOOLS
|
||||
|
||||
app = FastAPI(title="API Key MCP Server", version="1.0.0")
|
||||
|
||||
# API Key配置
|
||||
API_KEYS = {"test-api-key", "demo-key-123"}
|
||||
|
||||
def verify_api_key(x_api_key: Optional[str] = Header(None)):
|
||||
"""验证API Key"""
|
||||
if x_api_key and x_api_key in API_KEYS:
|
||||
return True
|
||||
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"name": "API Key MCP Server", "version": "1.0.0", "auth_type": "api_key"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "tools": len(TOOLS), "auth_type": "api_key"}
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_api_key)):
|
||||
return await handle_mcp_request(request, "API Key MCP Server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("启动API Key认证MCP服务器...")
|
||||
print("访问 http://localhost:8004 查看服务状态")
|
||||
print("MCP端点: http://localhost:8004/mcp")
|
||||
print("认证方式: API Key (Header: X-API-Key)")
|
||||
print("测试API Keys: test-api-key, demo-key-123")
|
||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||
45
basic_auth_mcp_server.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Basic Auth认证MCP服务器"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||
from typing import Optional
|
||||
import uvicorn
|
||||
import base64
|
||||
from mcp_base import MCPRequest, handle_mcp_request, TOOLS
|
||||
|
||||
app = FastAPI(title="Basic Auth MCP Server", version="1.0.0")
|
||||
|
||||
# Basic Auth配置
|
||||
BASIC_AUTH_USERS = {"admin": "password", "user": "secret"}
|
||||
|
||||
def verify_basic_auth(authorization: Optional[str] = Header(None)):
|
||||
"""验证Basic Auth"""
|
||||
if authorization and authorization.startswith("Basic "):
|
||||
try:
|
||||
credentials = base64.b64decode(authorization.split(" ")[1]).decode()
|
||||
username, password = credentials.split(":", 1)
|
||||
if username in BASIC_AUTH_USERS and BASIC_AUTH_USERS[username] == password:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
raise HTTPException(status_code=401, detail="Invalid Basic Auth")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"name": "Basic Auth MCP Server", "version": "1.0.0", "auth_type": "basic_auth"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "tools": len(TOOLS), "auth_type": "basic_auth"}
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_basic_auth)):
|
||||
return await handle_mcp_request(request, "Basic Auth MCP Server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("启动Basic Auth认证MCP服务器...")
|
||||
print("访问 http://localhost:8006 查看服务状态")
|
||||
print("MCP端点: http://localhost:8006/mcp")
|
||||
print("认证方式: Basic Auth (Header: Authorization: Basic <base64>)")
|
||||
print("测试用户: admin:password, user:secret")
|
||||
uvicorn.run(app, host="0.0.0.0", port=8006)
|
||||
40
bearer_token_mcp_server.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bearer Token认证MCP服务器"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||
from typing import Optional
|
||||
import uvicorn
|
||||
from mcp_base import MCPRequest, handle_mcp_request, TOOLS
|
||||
|
||||
app = FastAPI(title="Bearer Token MCP Server", version="1.0.0")
|
||||
|
||||
# Bearer Token配置
|
||||
BEARER_TOKENS = {"bearer-token-123", "demo-bearer-token"}
|
||||
|
||||
def verify_bearer_token(authorization: Optional[str] = Header(None)):
|
||||
"""验证Bearer Token"""
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
token = authorization.split(" ")[1]
|
||||
if token in BEARER_TOKENS:
|
||||
return True
|
||||
raise HTTPException(status_code=401, detail="Invalid Bearer Token")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"name": "Bearer Token MCP Server", "version": "1.0.0", "auth_type": "bearer_token"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "tools": len(TOOLS), "auth_type": "bearer_token"}
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_bearer_token)):
|
||||
return await handle_mcp_request(request, "Bearer Token MCP Server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("启动Bearer Token认证MCP服务器...")
|
||||
print("访问 http://localhost:8005 查看服务状态")
|
||||
print("MCP端点: http://localhost:8005/mcp")
|
||||
print("认证方式: Bearer Token (Header: Authorization: Bearer <token>)")
|
||||
print("测试Bearer Tokens: bearer-token-123, demo-bearer-token")
|
||||
uvicorn.run(app, host="0.0.0.0", port=8005)
|
||||
111
mcp_base.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP服务器基础模块 - 共享的模型和处理逻辑"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any
|
||||
|
||||
class MCPRequest(BaseModel):
|
||||
jsonrpc: str = "2.0"
|
||||
id: str
|
||||
method: str
|
||||
params: Dict[str, Any] = {}
|
||||
|
||||
class MCPResponse(BaseModel):
|
||||
jsonrpc: str = "2.0"
|
||||
id: str
|
||||
result: Any = None
|
||||
error: Dict[str, Any] = None
|
||||
|
||||
# 工具定义
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "calculator",
|
||||
"description": "简单计算器",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {"type": "string", "description": "数学表达式"}
|
||||
},
|
||||
"required": ["expression"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "echo",
|
||||
"description": "回显工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string", "description": "要回显的消息"}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
async def handle_mcp_request(request: MCPRequest, server_name: str = "MCP Server"):
|
||||
"""处理MCP请求"""
|
||||
try:
|
||||
if request.method == "initialize":
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {"tools": {"listChanged": True}},
|
||||
"serverInfo": {"name": server_name, "version": "1.0.0"}
|
||||
}
|
||||
)
|
||||
|
||||
elif request.method == "tools/list":
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"tools": TOOLS}
|
||||
)
|
||||
|
||||
elif request.method == "tools/call":
|
||||
tool_name = request.params.get("name")
|
||||
arguments = request.params.get("arguments", {})
|
||||
|
||||
if tool_name == "calculator":
|
||||
try:
|
||||
expression = arguments.get("expression", "")
|
||||
result = eval(expression)
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"content": [{"type": "text", "text": f"结果: {result}"}]}
|
||||
)
|
||||
except Exception as e:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": f"计算错误: {str(e)}"}
|
||||
)
|
||||
|
||||
elif tool_name == "echo":
|
||||
message = arguments.get("message", "")
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"content": [{"type": "text", "text": f"Echo: {message}"}]}
|
||||
)
|
||||
|
||||
else:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": f"未知工具: {tool_name}"}
|
||||
)
|
||||
|
||||
elif request.method == "ping":
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"status": "pong"}
|
||||
)
|
||||
|
||||
else:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": f"未知方法: {request.method}"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": str(e)}
|
||||
)
|
||||
130
simple_mcp_server.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""简化的MCP服务器 - 用于测试MCP工具集成"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any, List
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI(title="Simple MCP Server", version="1.0.0")
|
||||
|
||||
class MCPRequest(BaseModel):
|
||||
jsonrpc: str = "2.0"
|
||||
id: str
|
||||
method: str
|
||||
params: Dict[str, Any] = {}
|
||||
|
||||
class MCPResponse(BaseModel):
|
||||
jsonrpc: str = "2.0"
|
||||
id: str
|
||||
result: Any = None
|
||||
error: Dict[str, Any] = None
|
||||
|
||||
# 可用工具定义
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "calculator",
|
||||
"description": "简单计算器",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {"type": "string", "description": "数学表达式"}
|
||||
},
|
||||
"required": ["expression"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "echo",
|
||||
"description": "回显工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string", "description": "要回显的消息"}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"name": "Simple MCP Server", "version": "1.0.0"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "tools": len(TOOLS)}
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_handler(request: MCPRequest):
|
||||
"""处理MCP请求"""
|
||||
try:
|
||||
if request.method == "initialize":
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {"tools": {"listChanged": True}},
|
||||
"serverInfo": {"name": "Simple MCP Server", "version": "1.0.0"}
|
||||
}
|
||||
)
|
||||
|
||||
elif request.method == "tools/list":
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"tools": TOOLS}
|
||||
)
|
||||
|
||||
elif request.method == "tools/call":
|
||||
tool_name = request.params.get("name")
|
||||
arguments = request.params.get("arguments", {})
|
||||
|
||||
if tool_name == "calculator":
|
||||
try:
|
||||
expression = arguments.get("expression", "")
|
||||
result = eval(expression) # 注意:生产环境不要用eval
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"content": [{"type": "text", "text": f"结果: {result}"}]}
|
||||
)
|
||||
except Exception as e:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": f"计算错误: {str(e)}"}
|
||||
)
|
||||
|
||||
elif tool_name == "echo":
|
||||
message = arguments.get("message", "")
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"content": [{"type": "text", "text": f"Echo: {message}"}]}
|
||||
)
|
||||
|
||||
else:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": f"未知工具: {tool_name}"}
|
||||
)
|
||||
|
||||
elif request.method == "ping":
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
result={"status": "pong"}
|
||||
)
|
||||
|
||||
else:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": f"未知方法: {request.method}"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return MCPResponse(
|
||||
id=request.id,
|
||||
error={"code": -1, "message": str(e)}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("启动简化MCP服务器...")
|
||||
print("访问 http://localhost:8002 查看服务状态")
|
||||
print("MCP端点: http://localhost:8002/mcp")
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
@@ -108,8 +108,4 @@ export const getShareToken = (share_token: string, user_id: string) => {
|
||||
// 复制应用
|
||||
export const copyApplication = (app_id: string, new_name: string) => {
|
||||
return request.post(`/apps/${app_id}/copy?new_name=${new_name}`)
|
||||
}
|
||||
// 数据统计
|
||||
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
|
||||
return request.get(`/apps/${app_id}/statistics`, data)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { request, API_PREFIX } from '@/utils/request'
|
||||
|
||||
// Upload file,file storage has expiration period
|
||||
export const fileUploadUrl = `${API_PREFIX}/storage/files`
|
||||
export const fileUpload = (formData?: unknown) => {
|
||||
return request.uploadFile('/storage/files', formData)
|
||||
}
|
||||
|
||||
// Get file access URL (no token required)
|
||||
export const getFileUrl = (file_id: string) => `/storage/files/${file_id}/url`
|
||||
export const getFileLink = (fileId: string, data: { permanent?: boolean } = { permanent: true }) => {
|
||||
return request.get(getFileUrl(fileId), data)
|
||||
}
|
||||
|
||||
// Get file internally
|
||||
export const getInternalFileUrl = (file_id: string) => `/storage/files/${file_id}`
|
||||
export const getInternalFile = (fileId: string) => {
|
||||
return request.get(getInternalFileUrl(fileId))
|
||||
}
|
||||
|
||||
// Delete file
|
||||
export const deleteFileUrl = (file_id: string) => `/storage/files/${file_id}`
|
||||
export const deleteFile = (fileId: string) => {
|
||||
return request.delete(deleteFileUrl(fileId))
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export const getModelTypeList = async () => {
|
||||
};
|
||||
// 获取模型列表
|
||||
export const getModelList = async (pageInfo: PageRequest) => {
|
||||
const response = await request.get(`${apiPrefix}/models`, { ...pageInfo, is_active: true });
|
||||
const response = await request.get(`${apiPrefix}/models`, pageInfo);
|
||||
return response as any;
|
||||
};
|
||||
//获取模型提供者
|
||||
|
||||
@@ -1,68 +1,23 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { MultiKeyForm, Query, KeyConfigModalForm, CompositeModelForm, CustomModelForm } from '@/views/ModelManagement/types'
|
||||
import type { ModelFormData } from '@/views/ModelManagement/types'
|
||||
|
||||
// Model list
|
||||
// 模型列表
|
||||
export const getModelListUrl = '/models'
|
||||
export const getModelList = (data: Query) => {
|
||||
export const getModelList = (data: { type: string; pagesize: number; page: number; }) => {
|
||||
return request.get(getModelListUrl, data)
|
||||
}
|
||||
// Model type list
|
||||
// 创建模型
|
||||
export const addModel = (data: ModelFormData) => {
|
||||
return request.post('/models', data)
|
||||
}
|
||||
// 更新模型
|
||||
export const updateModel = (apiKeyId: string, data: ModelFormData) => {
|
||||
return request.put(`/models/apikeys/${apiKeyId}`, data)
|
||||
}
|
||||
// 模型类型列表
|
||||
export const modelTypeUrl = '/models/type'
|
||||
// Model provider list
|
||||
// 模型供应商列表
|
||||
export const modelProviderUrl = '/models/provider'
|
||||
export const getModelProviderList = () => {
|
||||
return request.get(modelProviderUrl)
|
||||
}
|
||||
// New model list
|
||||
export const getModelNewListUrl = '/models/new'
|
||||
export const getModelNewList = (data: Query) => {
|
||||
return request.get(getModelNewListUrl, data)
|
||||
}
|
||||
// Get model information
|
||||
export const getModelInfo = (model_id: string) => {
|
||||
return request.get(`/models/${model_id}`)
|
||||
}
|
||||
// Create composite model
|
||||
export const addCompositeModel = (data: CompositeModelForm) => {
|
||||
return request.post('/models/composite', data)
|
||||
}
|
||||
// Update composite model
|
||||
export const updateCompositeModel = (model_id: string, data: CompositeModelForm) => {
|
||||
return request.put(`/models/composite/${model_id}`, data)
|
||||
}
|
||||
// Delete composite model
|
||||
export const deleteCompositeModel = (model_id: string) => {
|
||||
return request.delete(`/models/composite/${model_id}`)
|
||||
}
|
||||
// Create API keys for all matching models by provider
|
||||
export const updateProviderApiKeys = (data: KeyConfigModalForm) => {
|
||||
return request.post('/models/provider/apikeys', data)
|
||||
}
|
||||
// Create model API key
|
||||
export const addModelApiKey = (model_id: string, data: MultiKeyForm) => {
|
||||
return request.post(`/models/${model_id}/apikeys`, data)
|
||||
}
|
||||
// Delete model API key
|
||||
export const deleteModelApiKey = (api_key_id: string) => {
|
||||
return request.delete(`/models/apikeys/${api_key_id}`)
|
||||
}
|
||||
// Update model status
|
||||
export const updateModelStatus = (model_id: string, data: { is_active: boolean; }) => {
|
||||
return request.put(`/models/${model_id}`, data)
|
||||
}
|
||||
// Model plaza list
|
||||
export const getModelPlaza = (data: { search?: string; provider?: string; }) => {
|
||||
return request.get('/models/model_plaza', data)
|
||||
}
|
||||
// Add model to plaza
|
||||
export const addModelPlaza = (model_base_id: string) => {
|
||||
return request.post(`/models/model_plaza/${model_base_id}/add`)
|
||||
}
|
||||
// Create custom model
|
||||
export const addCustomModel = (data: CustomModelForm) => {
|
||||
return request.post('/models/model_plaza', data)
|
||||
}
|
||||
// Update custom model
|
||||
export const updateCustomModel = (model_base_id: string, data: CustomModelForm) => {
|
||||
return request.put(`/models/model_plaza/${model_base_id}`, data)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 157 KiB |
@@ -1,15 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_16762_59518)">
|
||||
<path d="M12.6667 0H3.33333C1.49238 0 0 1.49238 0 3.33333V12.6667C0 14.5076 1.49238 16 3.33333 16H12.6667C14.5076 16 16 14.5076 16 12.6667V3.33333C16 1.49238 14.5076 0 12.6667 0Z" fill="url(#paint0_linear_16762_59518)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99984 12.093L6.3825 12.6323L5.75184 12.2116L6.4385 11.9823L6.22784 11.3503L5.04917 11.743L4.6665 11.4883V9.66631C4.6665 9.54031 4.59517 9.42497 4.4825 9.3683L3.33317 8.79364V7.20564L4.33317 6.70564L5.33317 7.20564V8.33297C5.33317 8.45964 5.4045 8.57497 5.51717 8.63164L6.8505 9.29831L7.14917 8.70164L5.99984 8.12697V7.20564L7.14917 6.63164C7.26184 6.57497 7.33317 6.45964 7.33317 6.33297V5.33297H6.6665V6.12697L5.6665 6.62697L4.6665 6.12697V4.51164L5.33317 4.06697V5.33297H5.99984V3.62297L6.3825 3.36764L7.99984 3.90697V12.093ZM11.6665 11.333C11.8498 11.333 11.9998 11.4823 11.9998 11.6663C11.9998 11.8503 11.8498 11.9996 11.6665 11.9996C11.4832 11.9996 11.3332 11.8503 11.3332 11.6663C11.3332 11.4823 11.4832 11.333 11.6665 11.333ZM10.9998 3.99964C11.1832 3.99964 11.3332 4.14897 11.3332 4.33297C11.3332 4.51697 11.1832 4.6663 10.9998 4.6663C10.8165 4.6663 10.6665 4.51697 10.6665 4.33297C10.6665 4.14897 10.8165 3.99964 10.9998 3.99964ZM12.3332 7.99964C12.5165 7.99964 12.6665 8.14897 12.6665 8.33297C12.6665 8.51697 12.5165 8.66631 12.3332 8.66631C12.1498 8.66631 11.9998 8.51697 11.9998 8.33297C11.9998 8.14897 12.1498 7.99964 12.3332 7.99964ZM11.3945 8.66631C11.5325 9.05364 11.8992 9.33297 12.3332 9.33297C12.8845 9.33297 13.3332 8.88497 13.3332 8.33297C13.3332 7.78164 12.8845 7.33297 12.3332 7.33297C11.8992 7.33297 11.5325 7.61297 11.3945 7.99964H8.6665V6.66631H10.9998C11.1838 6.66631 11.3332 6.51764 11.3332 6.33297V5.27164C11.7205 5.13364 11.9998 4.76697 11.9998 4.33297C11.9998 3.78164 11.5512 3.33297 10.9998 3.33297C10.4485 3.33297 9.99984 3.78164 9.99984 4.33297C9.99984 4.76697 10.2792 5.13364 10.6665 5.27164V5.99964H8.6665V3.6663C8.6665 3.52297 8.5745 3.39564 8.4385 3.3503L6.4385 2.68364C6.3405 2.65097 6.23384 2.66564 6.1485 2.7223L4.1485 4.05564C4.05584 4.11764 3.99984 4.22164 3.99984 4.33297V6.12697L2.8505 6.70164C2.73784 6.75831 2.6665 6.87364 2.6665 6.99964V8.99964C2.6665 9.12631 2.73784 9.24164 2.8505 9.29831L3.99984 9.87231V11.6663C3.99984 11.7776 4.05584 11.8823 4.1485 11.9436L6.1485 13.277C6.20384 13.3143 6.26784 13.333 6.33317 13.333C6.3685 13.333 6.40384 13.3276 6.4385 13.3156L8.4385 12.649C8.5745 12.6043 8.6665 12.477 8.6665 12.333V10.6663H10.1952L10.7638 11.2356L10.7725 11.227C10.7072 11.3603 10.6665 11.5083 10.6665 11.6663C10.6665 12.2176 11.1152 12.6663 11.6665 12.6663C12.2178 12.6663 12.6665 12.2176 12.6665 11.6663C12.6665 11.115 12.2178 10.6663 11.6665 10.6663C11.5078 10.6663 11.3598 10.707 11.2272 10.773L11.2358 10.7643L10.5692 10.0976C10.5065 10.035 10.4218 9.99964 10.3332 9.99964H8.6665V8.66631H11.3945Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_16762_59518" x1="0" y1="1600" x2="1600" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#055F4E"/>
|
||||
<stop offset="1" stop-color="#56C0A7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_16762_59518">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
@@ -1,24 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Xorbits Square" clip-path="url(#clip0_9850_26870)">
|
||||
<path id="Vector" d="M8.00391 12.3124C8.69334 13.0754 9.47526 13.7494 10.3316 14.3188C11.0667 14.8105 11.8509 15.2245 12.6716 15.5541C14.1617 14.1465 15.3959 12.4907 16.3192 10.6606L21.7051 0L12.3133 7.38353C10.5832 8.74456 9.12178 10.416 8.00391 12.3124Z" fill="url(#paint0_linear_9850_26870)"/>
|
||||
<path id="Vector_2" d="M7.23504 18.9512C6.56092 18.5012 5.92386 18.0265 5.3221 17.5394L2.06445 24L7.91975 19.3959C7.69034 19.2494 7.46092 19.103 7.23504 18.9512Z" fill="url(#paint1_linear_9850_26870)"/>
|
||||
<path id="Vector_3" d="M19.3161 8.57474C21.0808 10.9147 21.5961 13.5159 20.3996 15.3053C18.6526 17.9189 13.9161 17.8183 9.82024 15.0812C5.72435 12.3441 3.82024 8.0065 5.56729 5.39297C6.76377 3.60356 9.36318 3.0865 12.2008 3.81886C7.29318 1.73474 2.62376 1.94121 0.813177 4.64474C-1.45976 8.04709 1.64435 14.1177 7.74494 18.1889C13.8455 22.26 20.6361 22.8124 22.9091 19.4118C24.7179 16.703 23.1173 12.3106 19.3161 8.57474Z" fill="url(#paint2_linear_9850_26870)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_9850_26870" x1="2.15214" y1="24.3018" x2="21.2921" y2="0.0988218" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E9A85E"/>
|
||||
<stop offset="1" stop-color="#F52B76"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_9850_26870" x1="2.06269" y1="24.2294" x2="21.2027" y2="0.028252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E9A85E"/>
|
||||
<stop offset="1" stop-color="#F52B76"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_9850_26870" x1="-0.613606" y1="3.843" x2="21.4449" y2="18.7258" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6A0CF5"/>
|
||||
<stop offset="1" stop-color="#AB66F3"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_9850_26870">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -15,7 +15,7 @@ interface ApiResponse<T> {
|
||||
interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
|
||||
url: string;
|
||||
params?: Record<string, unknown>;
|
||||
valueKey?: string;
|
||||
valueKey?: string | string[];
|
||||
labelKey?: string;
|
||||
placeholder?: string;
|
||||
hasAll?: boolean;
|
||||
@@ -66,11 +66,18 @@ const CustomSelect: FC<CustomSelectProps> = ({
|
||||
{...props}
|
||||
>
|
||||
{hasAll && <Select.Option value={null}>{allTitle || t('common.all')}</Select.Option>}
|
||||
{displayOptions.map((option) => (
|
||||
<Select.Option key={option[valueKey]} value={option[valueKey]}>
|
||||
{String(option[labelKey])}
|
||||
</Select.Option>
|
||||
))}
|
||||
{displayOptions.map((option) => {
|
||||
const getValue = () => {
|
||||
if (typeof valueKey === 'string') return option[valueKey];
|
||||
return valueKey.find(key => option[key] != null) ? option[valueKey.find(key => option[key] != null)!] : undefined;
|
||||
};
|
||||
const value = getValue();
|
||||
return (
|
||||
<Select.Option key={value} value={value}>
|
||||
{String(option[labelKey])}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import pageEmptyIcon from '@/assets/images/empty/pageEmpty.png'
|
||||
import Empty from './index'
|
||||
const PageEmpty = ({ size = [240, 210] }: { size?: number | number[] }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Empty
|
||||
url={pageEmptyIcon}
|
||||
title={t('empty.pageEmpty')}
|
||||
subTitle={t('empty.pageEmptyDesc')}
|
||||
size={size}
|
||||
className="rb:h-full"
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PageEmpty;
|
||||
@@ -1,13 +0,0 @@
|
||||
.page-tabs:global(.ant-segmented) {
|
||||
background-color: rgba(91, 97, 103, 0.08);
|
||||
padding: 4px;
|
||||
}
|
||||
.page-tabs:global(.ant-segmented .ant-segmented-item-label) {
|
||||
line-height: 24px;
|
||||
min-height: 24px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.page-tabs:global(.ant-segmented .ant-segmented-item-selected) {
|
||||
box-shadow: 0px 2px 4px 0px rgba(33, 35, 50, 0.16);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { type FC } from 'react';
|
||||
import { Segmented, type SegmentedProps } from 'antd';
|
||||
import styles from './index.module.css';
|
||||
|
||||
const PageTabs: FC<SegmentedProps> = ({
|
||||
value,
|
||||
options,
|
||||
onChange
|
||||
}) => {
|
||||
return <Segmented
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
className={styles.pageTabs}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default PageTabs;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import { Card } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface RbCardProps {
|
||||
@@ -9,7 +9,7 @@ interface RbCardProps {
|
||||
extra?: ReactNode;
|
||||
children?: ReactNode;
|
||||
avatar?: ReactNode;
|
||||
avatarUrl?: string | null;
|
||||
avatarUrl?: string;
|
||||
bodyPadding?: string;
|
||||
bodyClassName?: string;
|
||||
headerType?: 'border' | 'borderless' | 'borderBL' | 'borderL';
|
||||
@@ -50,7 +50,7 @@ const RbCard: FC<RbCardProps> = ({
|
||||
<Card
|
||||
{...props}
|
||||
title={typeof title === 'function' ? title() : title ?
|
||||
<div className="rb:flex rb:items-center rb:gap-2">
|
||||
<div className="rb:flex rb:items-center">
|
||||
{avatarUrl
|
||||
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
|
||||
: avatar ? avatar : null
|
||||
@@ -59,11 +59,11 @@ const RbCard: FC<RbCardProps> = ({
|
||||
clsx(
|
||||
{
|
||||
'rb:max-w-full': !avatarUrl && !avatar,
|
||||
'rb:max-w-[calc(100%-80px)]': avatarUrl || avatar,
|
||||
'rb:max-w-[calc(100%-60px)]': avatarUrl || avatar,
|
||||
}
|
||||
)
|
||||
}>
|
||||
<Tooltip title={title}><div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div></Tooltip>
|
||||
<div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div>
|
||||
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
|
||||
</div>
|
||||
</div> : null
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Upload, Image, App } from 'antd';
|
||||
import { Upload, Modal, Image, App } from 'antd';
|
||||
import type { GetProp, UploadFile, UploadProps } from 'antd';
|
||||
// import { UploadOutlined, } from '@ant-design/icons';
|
||||
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PlusIcon from '@/assets/images/plus.svg'
|
||||
import { cookieUtils } from '@/utils/request'
|
||||
import { fileUploadUrl } from '@/api/fileStorage'
|
||||
import styles from './index.module.less'
|
||||
|
||||
interface UploadImagesProps extends Omit<UploadProps, 'onChange' | 'fileList'> {
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface UploadImagesProps extends Omit<UploadProps, 'onChange'> {
|
||||
/** 上传接口地址 */
|
||||
action?: string;
|
||||
/** 是否支持多选 */
|
||||
multiple?: boolean;
|
||||
/** 已上传的文件列表 */
|
||||
fileList?: UploadFile[] | UploadFile;
|
||||
fileList?: UploadFile[];
|
||||
/** 文件列表变化回调 */
|
||||
onChange?: (fileList?: UploadFile[] | UploadFile) => void;
|
||||
onChange?: (fileList: UploadFile[]) => void;
|
||||
/** 禁用上传 */
|
||||
disabled?: boolean;
|
||||
/** 文件大小限制(MB) */
|
||||
@@ -28,7 +28,6 @@ interface UploadImagesProps extends Omit<UploadProps, 'onChange' | 'fileList'> {
|
||||
isAutoUpload?: boolean;
|
||||
/** 最大上传文件数 */
|
||||
maxCount?: number;
|
||||
className?: string;
|
||||
}
|
||||
const ALL_FILE_TYPE: {
|
||||
[key: string]: string;
|
||||
@@ -60,7 +59,7 @@ const getBase64 = (file: FileType): Promise<string> => {
|
||||
* 支持单文件/多文件上传、拖拽上传、文件验证、预览等功能
|
||||
*/
|
||||
const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
action = fileUploadUrl,
|
||||
action = '/api/upload',
|
||||
multiple = false,
|
||||
fileList: propFileList = [],
|
||||
onChange,
|
||||
@@ -69,42 +68,27 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
fileType = ['png', 'jpg', 'gif'],
|
||||
isAutoUpload = true,
|
||||
maxCount = 1,
|
||||
className = 'rb:size-24! rb:leading-1!',
|
||||
...props
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message, modal } = App.useApp()
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const { message } = App.useApp()
|
||||
const [fileList, setFileList] = useState<UploadFile[]>(propFileList);
|
||||
const [accept, setAccept] = useState<string | undefined>();
|
||||
// const [loading, setLoading] = useState(false);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(propFileList) && typeof propFileList === 'object') {
|
||||
setFileList([propFileList]);
|
||||
}
|
||||
}, [propFileList])
|
||||
|
||||
const updateValue = (list: UploadFile[]) => {
|
||||
if (maxCount === 1) {
|
||||
onChange?.(list[0])
|
||||
} else {
|
||||
onChange?.(list)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件移除
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmRemoveFile'),
|
||||
okText: `${t('common.confirm')}`,
|
||||
confirm({
|
||||
title: '确定要删除此文件吗?',
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
cancelText: `${t('common.cancel')}`,
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const newFileList = fileList.filter((item) => item.uid !== file.uid);
|
||||
setFileList(newFileList);
|
||||
updateValue(newFileList)
|
||||
onChange?.(newFileList);
|
||||
},
|
||||
});
|
||||
return false; // 阻止默认删除行为,由confirm控制
|
||||
@@ -116,7 +100,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
if (fileSize && file.size) {
|
||||
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
|
||||
if (!isLtMaxSize) {
|
||||
message.error(t('common.fileSizeTip', { size: fileSize }));
|
||||
message.error(`文件大小不能超过 ${fileSize}MB`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +108,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
if (accept && accept.length > 0 && file.type) {
|
||||
const isAccept = accept.includes(file.type);
|
||||
if (!isAccept) {
|
||||
message.error(`${t('common.fileAcceptTip')}${file.type}`);
|
||||
message.error(`不支持的文件类型: ${file.type}`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +119,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
}
|
||||
const newFileList = [...fileList, file];
|
||||
setFileList(newFileList);
|
||||
updateValue(newFileList);
|
||||
onChange?.(newFileList);
|
||||
return Upload.LIST_IGNORE; // 阻止自动上传
|
||||
}
|
||||
|
||||
@@ -145,13 +129,17 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
// 处理上传状态变化
|
||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
|
||||
setFileList(newFileList);
|
||||
updateValue(newFileList);
|
||||
if (onChange) {
|
||||
onChange(newFileList);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空已上传文件
|
||||
const clearFiles = () => {
|
||||
setFileList([]);
|
||||
updateValue([]);
|
||||
if (onChange) {
|
||||
onChange([]);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
@@ -179,7 +167,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
fileList,
|
||||
beforeUpload,
|
||||
headers: {
|
||||
authorization: `Bearer ${cookieUtils.get('authToken') }`,
|
||||
authorization: cookieUtils.get('authToken') || '',
|
||||
},
|
||||
onPreview: handlePreview,
|
||||
onRemove: handleRemove,
|
||||
@@ -192,7 +180,6 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
showRemoveIcon: true,
|
||||
showDownloadIcon: false,
|
||||
},
|
||||
className: `${styles.imageUpload} ${className}`,
|
||||
...props,
|
||||
};
|
||||
|
||||
@@ -206,9 +193,16 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
|
||||
<>
|
||||
<Upload
|
||||
{...uploadProps}
|
||||
style={{
|
||||
width: '136px',
|
||||
height: '136px',
|
||||
}}
|
||||
>
|
||||
{fileList.length < maxCount && (
|
||||
<img src={PlusIcon} className="rb:size-7" />
|
||||
<div className="rb:flex rb:flex-wrap rb:items-center rb:justify-center">
|
||||
<img src={PlusIcon} className="rb:w-[32px] rb:h-[32px]" />
|
||||
<div className="rb:mt-[12px] rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]">{t('common.clickUploadIcon')}</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
{previewImage && (
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container),
|
||||
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container) {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
@@ -419,9 +419,6 @@ export const en = {
|
||||
statusEnabled: 'Available',
|
||||
statusDisabled: 'Unavailable',
|
||||
remove: 'Remove',
|
||||
|
||||
fileSizeTip: 'File size cannot exceed {{size}}MB',
|
||||
fileAcceptTip: 'Unsupported file type:'
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -513,64 +510,6 @@ export const en = {
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
modelNew: {
|
||||
group: 'Model Group',
|
||||
list: 'Model List',
|
||||
square: 'Model Plaza',
|
||||
createGroupModel: 'Create Model Group',
|
||||
groupSearchPlaceholder: 'Search model groups',
|
||||
listSearchPlaceholder: 'Search available models',
|
||||
squareSearchPlaceholder: 'Search platform models',
|
||||
status: 'Model Status',
|
||||
created_at: 'Created At',
|
||||
configureBtn: 'Click to Configure',
|
||||
showModel: 'Show Model',
|
||||
keyConfig: 'Configure KEY',
|
||||
|
||||
modelConfiguration: 'Model Configuration',
|
||||
logo: 'Model LOGO',
|
||||
name: 'Model Name',
|
||||
type: 'Model Type',
|
||||
modelImplement: 'Model Implementation',
|
||||
addImplement: 'Add Implementation',
|
||||
noAuth: 'Unauthorized (Limited to 1 implementation)',
|
||||
implementConfig: 'Configure Model Implementation',
|
||||
provider: 'Model Provider',
|
||||
api_key_ids: 'Select Model',
|
||||
viewAll: 'More',
|
||||
modelCount: 'Total {{count}} models',
|
||||
modelList: 'Model List',
|
||||
added: ' Added',
|
||||
addSuccess: 'Added successfully',
|
||||
model_name: 'Model Name',
|
||||
tags: 'Tags',
|
||||
createCustomModel: 'Add Custom Model',
|
||||
edit: 'Edit',
|
||||
selectOneTip: 'Model API KEY not configured, please configure it in the model list first',
|
||||
load_balance_strategy: 'Concurrency Strategy',
|
||||
round_robin: 'Sequential Execution - Call each model in order',
|
||||
none: 'None',
|
||||
|
||||
api_key: 'API KEY',
|
||||
api_base: 'API Base URL',
|
||||
description: 'Description',
|
||||
add: 'Add',
|
||||
item: 'item',
|
||||
apiKeyNum: ' API Keys',
|
||||
official: 'Official',
|
||||
deprecated: 'Deprecated',
|
||||
|
||||
llm: 'LLM',
|
||||
chat: 'Chat',
|
||||
embedding: 'Embedding',
|
||||
rerank: 'Rerank',
|
||||
openai: "Openai",
|
||||
dashscope: "Dashscope",
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
knowledgeBase: {
|
||||
pleaseUploadFileFirst: 'Please upload file first',
|
||||
shareSuccess: 'Share successfully',
|
||||
@@ -1236,12 +1175,6 @@ export const en = {
|
||||
priority: 'Structured Integration',
|
||||
addTool: 'Add Tool',
|
||||
tool: 'Tool',
|
||||
|
||||
statistics: 'Data Statistics',
|
||||
daily_conversations: 'Daily Conversations',
|
||||
daily_new_users: 'Daily New Users',
|
||||
daily_api_calls: 'Daily API Calls',
|
||||
daily_tokens: 'Token Consumption',
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -1601,9 +1534,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
noPermissionDesc: ' Please contact the administrator to grant permission',
|
||||
tableEmpty: 'No data available.',
|
||||
loadingEmpty: 'The content is loading…',
|
||||
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen',
|
||||
pageEmpty: 'Oops! No search results available at the moment',
|
||||
pageEmptyDesc: "Red Bear tilts its head and waits for you to change a new keyword, let's explore together.",
|
||||
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen'
|
||||
},
|
||||
apiKey: {
|
||||
name: 'Project Name',
|
||||
|
||||
@@ -658,13 +658,7 @@ export const zh = {
|
||||
priority: '结构化整合',
|
||||
addTool: '添加工具',
|
||||
tool: '工具',
|
||||
variableConfig: '配置变量',
|
||||
|
||||
statistics: '数据统计',
|
||||
daily_conversations: '消息会话数',
|
||||
daily_new_users: '新增用户数',
|
||||
daily_api_calls: '调用次数',
|
||||
daily_tokens: 'Token消耗',
|
||||
variableConfig: '配置变量'
|
||||
},
|
||||
role: {
|
||||
roleManagement: '角色管理',
|
||||
@@ -973,9 +967,6 @@ export const zh = {
|
||||
statusEnabled: '可用',
|
||||
statusDisabled: '不可用',
|
||||
remove: '删除',
|
||||
|
||||
fileSizeTip: '文件大小不能超过 {{size}}MB',
|
||||
fileAcceptTip: '不支持的文件类型:'
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -1085,64 +1076,6 @@ export const zh = {
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
modelNew: {
|
||||
group: '模型组合',
|
||||
list: '模型列表',
|
||||
square: '模型广场',
|
||||
createGroupModel: '创建模型组合',
|
||||
groupSearchPlaceholder: '搜索模型组合',
|
||||
listSearchPlaceholder: '搜索可用模型',
|
||||
squareSearchPlaceholder: '搜索平台模型',
|
||||
status: '模型状态',
|
||||
created_at: '创建时间',
|
||||
configureBtn: '点击配置',
|
||||
showModel: '显示模型',
|
||||
keyConfig: '配置 KEY',
|
||||
|
||||
modelConfiguration: '模型配置',
|
||||
logo: '模型LOGO',
|
||||
name: '模型名称',
|
||||
type: '模型类型',
|
||||
modelImplement: '模型实现',
|
||||
addImplement: '添加实现',
|
||||
noAuth: '未授权(限1个实现)',
|
||||
implementConfig: '配置模型实现',
|
||||
provider: '模型供应商',
|
||||
api_key_ids: '选择模型',
|
||||
viewAll: '更多',
|
||||
modelCount: '共 {{count}} 个模型',
|
||||
modelList: '模型列表',
|
||||
added: ' 已添加',
|
||||
addSuccess: '添加成功',
|
||||
model_name: '模型名称',
|
||||
tags: '标签',
|
||||
createCustomModel: '添加自定义模型',
|
||||
edit: '编辑',
|
||||
selectOneTip: '模型未配置API KEY,请先在模型列表配置',
|
||||
load_balance_strategy: '并发策略',
|
||||
round_robin: '顺序执行 - 按顺序依次调用每个模型',
|
||||
none: '无',
|
||||
|
||||
api_key: 'API KEY',
|
||||
api_base: 'API Base URL',
|
||||
description: '描述',
|
||||
add: '添加',
|
||||
item: '个',
|
||||
apiKeyNum: '个 API Key',
|
||||
official: '官方',
|
||||
deprecated: '已弃用',
|
||||
|
||||
llm: 'LLM',
|
||||
chat: 'Chat',
|
||||
embedding: 'Embedding',
|
||||
rerank: 'Rerank',
|
||||
openai: "Openai",
|
||||
dashscope: "Dashscope",
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
timezones: {
|
||||
'Asia/Shanghai': '中国标准时间 (UTC+8)',
|
||||
'Asia/Kolkata': '印度标准时间 (UTC+5:30)',
|
||||
@@ -1674,9 +1607,7 @@ export const zh = {
|
||||
noPermissionDesc: '请联系管理员授予权限',
|
||||
tableEmpty: '目前没有数据',
|
||||
loadingEmpty: '内容正在加载中…',
|
||||
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上',
|
||||
pageEmpty: '哎呀!暂无搜索结果',
|
||||
pageEmptyDesc: '红熊歪着头等待您更换新的关键词,让我们一起探索吧。',
|
||||
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上'
|
||||
},
|
||||
|
||||
home: {
|
||||
|
||||
@@ -22,7 +22,7 @@ export const lightTheme: ThemeConfig = {
|
||||
// colorBgContainer: '#FBFDFF',
|
||||
colorError: '#FF5D34',
|
||||
sizeSM: 12,
|
||||
fontSizeSM: 12,
|
||||
fontSizeSM: 12,
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
@@ -105,9 +105,6 @@ export const lightTheme: ThemeConfig = {
|
||||
},
|
||||
Select: {
|
||||
lineHeightSM: 26
|
||||
},
|
||||
Upload: {
|
||||
pictureCardSize: 96,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -23,10 +23,9 @@ interface data {
|
||||
}
|
||||
|
||||
|
||||
export const API_PREFIX = '/api'
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: API_PREFIX, // 与vite.config.ts中的代理配置对应
|
||||
baseURL: '/api', // 与vite.config.ts中的代理配置对应
|
||||
// timeout: 10000, // 请求超时时间
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
@@ -127,7 +126,7 @@ service.interceptors.response.use(
|
||||
if (axios.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
||||
// 处理网络错误、超时等
|
||||
let msg = error.response?.data?.error || error.response?.error;
|
||||
const status = error?.response ? error.response.status : error;
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
} from './types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import { getModelList } from '@/api/models';
|
||||
import { saveAgentConfig } from '@/api/application'
|
||||
import Knowledge from './components/Knowledge/Knowledge'
|
||||
@@ -79,7 +79,7 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string | string[],
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={url}
|
||||
hasAll={false}
|
||||
valueKey='config_id'
|
||||
valueKey={['config_id_old', 'config_id']}
|
||||
labelKey="config_name"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -96,8 +96,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<Config | null>(null);
|
||||
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
const [defaultModel, setDefaultModel] = useState<ModelListItem | null>(null)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
const [defaultModel, setDefaultModel] = useState<Model | null>(null)
|
||||
const [chatList, setChatList] = useState<ChatData[]>([])
|
||||
const values = Form.useWatch<Config>([], form)
|
||||
const [isSave, setIsSave] = useState(false)
|
||||
@@ -127,15 +127,13 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
const response = res as Config
|
||||
let allTools = Array.isArray(response.tools) ? response.tools : []
|
||||
const memoryContent = response.memory?.memory_content
|
||||
const parsedMemoryContent = memoryContent === null || memoryContent === ''
|
||||
? undefined
|
||||
: !isNaN(Number(memoryContent)) ? Number(memoryContent) : memoryContent
|
||||
const convertedMemoryContent = memoryContent && !isNaN(Number(memoryContent)) ? Number(memoryContent) : memoryContent
|
||||
form.setFieldsValue({
|
||||
...response,
|
||||
tools: allTools,
|
||||
memory: {
|
||||
...response.memory,
|
||||
memory_content: parsedMemoryContent
|
||||
memory_content: convertedMemoryContent
|
||||
}
|
||||
})
|
||||
setData({
|
||||
@@ -216,7 +214,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
...data.knowledge_retrieval,
|
||||
...knowledgeRest,
|
||||
knowledge_bases: knowledge_bases.map(item => ({
|
||||
kb_id: item.kb_id || item.id,
|
||||
kb_id: item.id,
|
||||
...(item.config || {})
|
||||
}))
|
||||
} as KnowledgeConfig : null,
|
||||
@@ -241,9 +239,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
})
|
||||
}
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
.then(res => {
|
||||
const response = res as { items: ModelListItem[] }
|
||||
const response = res as { items: Model[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
@@ -253,7 +251,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
useEffect(() => {
|
||||
if (values?.default_model_config_id && modelList.length > 0) {
|
||||
const filterValue = modelList.find(item => item.id === values.default_model_config_id)
|
||||
setDefaultModel(filterValue as ModelListItem | null)
|
||||
setDefaultModel(filterValue as Model | null)
|
||||
setChatList([{
|
||||
label: filterValue?.name || '',
|
||||
model_config_id: filterValue?.id || '',
|
||||
|
||||
@@ -225,7 +225,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
<Form.Item name="default_model_config_id" noStyle>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'llm,chat', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { type FC, useState, useEffect } from 'react';
|
||||
import { Row, Col, Flex, DatePicker } from 'antd';
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import { getAppStatistics } from '@/api/application';
|
||||
import LineCard from './components/LineCard'
|
||||
import type { StatisticsData, StatisticsItem } from './types'
|
||||
|
||||
const TotalObj: Record<string, keyof StatisticsData> = {
|
||||
daily_conversations: 'total_conversations',
|
||||
daily_new_users: 'total_new_users',
|
||||
daily_api_calls: 'total_api_calls',
|
||||
daily_tokens: 'total_tokens',
|
||||
}
|
||||
const Statistics: FC<{ application: Application | null }> = ({ application }) => {
|
||||
const [data, setData] = useState<StatisticsData>({
|
||||
daily_conversations: [],
|
||||
total_conversations: 0,
|
||||
daily_new_users: [],
|
||||
total_new_users: 0,
|
||||
daily_api_calls: [],
|
||||
total_api_calls: 0,
|
||||
daily_tokens: [],
|
||||
total_tokens: 0
|
||||
})
|
||||
const [query, setQuery] = useState({
|
||||
start_date: dayjs().subtract(6, 'd'),
|
||||
end_date: dayjs().subtract(0, 'd'),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [application, query])
|
||||
const getData = () => {
|
||||
if (!application?.id) {
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
start_date: query.start_date.startOf('d').valueOf(),
|
||||
end_date: query.end_date.endOf('d').valueOf(),
|
||||
}
|
||||
|
||||
getAppStatistics(application.id, params)
|
||||
.then(res => {
|
||||
setData(res as StatisticsData)
|
||||
})
|
||||
}
|
||||
const handleChange = (date: [Dayjs | null, Dayjs | null] | null) => {
|
||||
if (!date || !date[0] || !date[1]) return
|
||||
setQuery({
|
||||
start_date: date[0],
|
||||
end_date: date[1],
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Flex justify="end">
|
||||
<RangePicker defaultValue={[query.start_date, query.end_date]} onChange={handleChange} />
|
||||
</Flex>
|
||||
</Col>
|
||||
{Object.entries(data).map(([key, value]) => {
|
||||
if (key.includes('total')) {
|
||||
return null
|
||||
}
|
||||
const totalKey = TotalObj[key];
|
||||
return (
|
||||
<Col span={12} key={key}>
|
||||
<LineCard
|
||||
type={key}
|
||||
total={totalKey ? (data[totalKey] as number) : 0}
|
||||
chartData={value as StatisticsItem[]}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Statistics;
|
||||
@@ -181,7 +181,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'llm,chat', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -17,7 +17,7 @@ import CopyModal from './CopyModal'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
|
||||
const tabKeys = ['arrangement', 'api', 'release']
|
||||
const menuIcons: Record<string, string> = {
|
||||
edit: editIcon,
|
||||
copy: copyIcon,
|
||||
|
||||
@@ -97,7 +97,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { type FC, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import * as echarts from 'echarts';
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
import Card from './Card'
|
||||
import type { StatisticsItem } from '../types'
|
||||
|
||||
interface LineCardProps {
|
||||
chartData: StatisticsItem[];
|
||||
type: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const SeriesConfig = {
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3
|
||||
},
|
||||
showSymbol: true,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'top'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
}
|
||||
|
||||
const ColorObj: Record<string, string> = {
|
||||
daily_conversations: '#FFB048',
|
||||
daily_new_users: '#4DA8FF',
|
||||
daily_api_calls: '#155EEF',
|
||||
daily_tokens: '#AD88FF'
|
||||
}
|
||||
|
||||
const LineCard: FC<LineCardProps> = ({ chartData, type, total }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [chartData])
|
||||
|
||||
const getSeries = () => {
|
||||
return [{
|
||||
...SeriesConfig,
|
||||
name: t(`application.${type}`),
|
||||
data: chartData.map(vo => vo.count),
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: ColorObj[type] },
|
||||
{ offset: 1, color: '#FFFFFF' }
|
||||
])
|
||||
},
|
||||
}]
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<div>{t(`application.${type}`)} <span className="rb:text-[#155EEF] rb:font-medium rb:text-[18px]">{total}</span></div>}
|
||||
>
|
||||
{chartData && chartData.length > 0 ? (
|
||||
<ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: [ColorObj[type]],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
crossStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
grid: {
|
||||
top: 10,
|
||||
left: 15,
|
||||
right: 40,
|
||||
bottom: 0,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.date),
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
align: 'right',
|
||||
lineHeight: 17,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
) : <Empty size={120} className="rb:mt-12 rb:mb-20.25" />}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LineCard
|
||||
@@ -9,7 +9,6 @@ import ReleasePage from './ReleasePage'
|
||||
import Cluster from './Cluster'
|
||||
import { getApplication } from '@/api/application'
|
||||
import Workflow from '@/views/Workflow';
|
||||
import Statistics from './Statistics'
|
||||
|
||||
const ApplicationConfig: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
@@ -69,7 +68,6 @@ const ApplicationConfig: React.FC = () => {
|
||||
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
|
||||
{activeTab === 'api' && <Api application={application} />}
|
||||
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
|
||||
{activeTab === 'statistics' && <Statistics application={application} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,19 +150,4 @@ export interface AiPromptForm {
|
||||
}
|
||||
export interface ChatVariableConfigModalRef {
|
||||
handleOpen: (values: Variable[]) => void;
|
||||
}
|
||||
|
||||
export interface StatisticsItem {
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
export interface StatisticsData {
|
||||
daily_conversations: StatisticsItem[];
|
||||
daily_new_users: StatisticsItem[];
|
||||
daily_api_calls: StatisticsItem[];
|
||||
daily_tokens: StatisticsItem[];
|
||||
total_conversations: number;
|
||||
total_new_users: number;
|
||||
total_api_calls: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const configList = [
|
||||
key: 'emotion_model_id',
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
|
||||
},
|
||||
{
|
||||
key: 'emotion_min_intensity',
|
||||
|
||||
@@ -39,7 +39,7 @@ const MemberManagement: React.FC = () => {
|
||||
onOk: () => {
|
||||
deleteMember(member.id)
|
||||
.then(() => {
|
||||
message.success(t('common.deleteSuccess'));
|
||||
message.success(t('member.deleteSuccess'));
|
||||
refreshTable();
|
||||
})
|
||||
}
|
||||
@@ -93,7 +93,7 @@ const MemberManagement: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-end rb:mb-3">
|
||||
<div className="rb:flex rb:justify-end rb:mb-[12px]">
|
||||
<Button type="primary" onClick={() => handleEdit()}>
|
||||
{t('member.createMember')}
|
||||
</Button>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Space, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import { Row, Col, Space, Switch, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Card from './components/Card'
|
||||
import type { ConfigForm, Variable } from './types'
|
||||
import { getMemoryExtractionConfig, updateMemoryExtractionConfig } from '@/api/memory'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import { getModelList } from '@/api/models';
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import { configList } from './constant'
|
||||
import Result from './components/Result'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
@@ -43,7 +43,7 @@ const MemoryExtractionEngine: FC = () => {
|
||||
const values = Form.useWatch<ConfigForm>([], form)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false)
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (values?.reflexion_range === 'database') {
|
||||
@@ -55,9 +55,9 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}, [values])
|
||||
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
.then(res => {
|
||||
const response = res as { items: ModelListItem[] }
|
||||
const response = res as { items: Model[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ProviderModelItem, ModelListItem, DescriptionItem, BaseRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelNewList } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem) => void; }>(({ query, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [list, setList] = useState<ModelListItem[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelNewList({
|
||||
...query,
|
||||
is_composite: true,
|
||||
is_active: true,
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
setList(response[0]?.models || [])
|
||||
})
|
||||
}
|
||||
const formatData = (data: ModelListItem) => {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: t(`modelNew.type`),
|
||||
children: data.type ? t(`modelNew.${data.type}`) : '-',
|
||||
},
|
||||
{
|
||||
key: 'is_active',
|
||||
label: t(`modelNew.status`),
|
||||
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: t(`modelNew.created_at`),
|
||||
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getList,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
:(
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
avatarUrl={item.logo}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{formatData(item)?.map((description: DescriptionItem) => (
|
||||
<div
|
||||
key={description.key}
|
||||
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
|
||||
<span className={clsx({
|
||||
"rb:text-[#212332]": description.key !== 'is_active',
|
||||
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
|
||||
})}>{(description.children as string)}</span>
|
||||
</div>
|
||||
))}
|
||||
<Button className="rb:mt-2" type="primary" ghost block onClick={() => handleEdit(item)}>{t('modelNew.configureBtn')}</Button>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default Group
|
||||
@@ -1,86 +0,0 @@
|
||||
import { useRef, useState, useEffect, type FC } from 'react';
|
||||
import { Button, Flex, Row, Col } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ProviderModelItem, KeyConfigModalRef, ModelListDetailRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelNewList } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import Tag from '@/components/Tag';
|
||||
import KeyConfigModal from './components/KeyConfigModal'
|
||||
import ModelListDetail from './components/ModelListDetail'
|
||||
import { getLogoUrl } from './utils'
|
||||
|
||||
const ModelList: FC<{ query: any }> = ({ query }) => {
|
||||
const { t } = useTranslation();
|
||||
const keyConfigModalRef = useRef<KeyConfigModalRef>(null)
|
||||
const modelListDetailRef = useRef<ModelListDetailRef>(null)
|
||||
const [list, setList] = useState<ProviderModelItem[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelNewList({
|
||||
...query,
|
||||
is_composite: false,
|
||||
})
|
||||
.then(res => {
|
||||
setList((res || []) as ProviderModelItem[])
|
||||
})
|
||||
}
|
||||
|
||||
const handleShowModel = (vo: ProviderModelItem) => {
|
||||
modelListDetailRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleKeyConfig = (vo: ProviderModelItem) => {
|
||||
keyConfigModalRef.current?.handleOpen(vo)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
:(
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.provider}
|
||||
title={t(`modelNew.${item.provider}`)}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.provider[0].toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Flex gap={8} wrap>{item.tags.map(tag => <Tag key={tag}>{t(`modelNew.${tag}`)}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Button block onClick={() => handleShowModel(item)}>{t('modelNew.showModel')}</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<KeyConfigModal
|
||||
ref={keyConfigModalRef}
|
||||
refresh={getList}
|
||||
/>
|
||||
<ModelListDetail
|
||||
ref={modelListDetailRef}
|
||||
refresh={getList}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelList
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Button, Space, App, Divider, Flex, Tooltip } from 'antd'
|
||||
import { UsergroupAddOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef, BaseRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelPlaza, addModelPlaza } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import Tag from '@/components/Tag';
|
||||
import ModelSquareDetail from './components/ModelSquareDetail'
|
||||
import { getLogoUrl } from './utils'
|
||||
|
||||
const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPlazaItem) => void; }>(({ query, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const modelSquareDetailRef = useRef<ModelSquareDetailRef>(null)
|
||||
const [list, setList] = useState<ModelPlaza[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelPlaza(query)
|
||||
.then(res => {
|
||||
setList((res as ModelPlaza[]) || [])
|
||||
})
|
||||
}
|
||||
|
||||
const handleMore = (vo: ModelPlaza) => {
|
||||
modelSquareDetailRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleAdd = (item: ModelPlazaItem) => {
|
||||
addModelPlaza(item.id)
|
||||
.then(() => {
|
||||
message.success(`${item.name}${t('modelNew.addSuccess')}`)
|
||||
getList()
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getList,
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
: list.map(vo => (
|
||||
<div key={vo.provider}>
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">
|
||||
<div className="rb:font-medium">{t(`modelNew.${vo.provider}`)}</div>
|
||||
<Button type="link" onClick={() => handleMore(vo)}>{t('modelNew.viewAll')}({t(`modelNew.modelCount`, { count: vo.models.length })})></Button>
|
||||
</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
{vo.models.slice(0, 6).map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space size={8}>
|
||||
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
|
||||
</Tooltip>
|
||||
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Divider size="middle" />
|
||||
<Flex justify="space-between">
|
||||
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
|
||||
<Space>
|
||||
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
|
||||
{item.is_added
|
||||
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
|
||||
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<ModelSquareDetail
|
||||
ref={modelSquareDetailRef}
|
||||
refresh={getList}
|
||||
handleEdit={handleEdit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default ModelSquare
|
||||
171
web/src/views/ModelManagement/components/ConfigModal.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ModelFormData, Model, ConfigModalRef, ConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { updateModel, addModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
|
||||
const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<Model>({} as Model);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<ModelFormData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch<ModelFormData>([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setModel({} as Model);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: Model) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
// 设置表单值
|
||||
const apiKeyInfo = model.api_keys[0]
|
||||
form.setFieldsValue({
|
||||
provider: apiKeyInfo.provider,
|
||||
model_name: apiKeyInfo.model_name,
|
||||
api_key: apiKeyInfo.api_key,
|
||||
api_base: apiKeyInfo.api_base
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
const data = {
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
api_keys: {
|
||||
provider: values.provider,
|
||||
model_name: values.model_name,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base
|
||||
},
|
||||
}
|
||||
setLoading(true)
|
||||
const res = isEdit
|
||||
? updateModel(model.api_keys[0].id, {
|
||||
provider: values.provider,
|
||||
model_name: values.model_name,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base
|
||||
} as ModelFormData)
|
||||
: addModel(data as ModelFormData)
|
||||
|
||||
res.then(() => {
|
||||
if (refresh) {
|
||||
refresh();
|
||||
}
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('model.modelConfiguration')}` : t('model.createModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{}}
|
||||
>
|
||||
{!isEdit && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('model.displayName')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.displayName') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('model.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('model.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="model_name"
|
||||
label={t('model.modelName')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.modelName') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('model.apiKey')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.apiKey') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('model.apiEndpoint')}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ConfigModal;
|
||||
@@ -1,168 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CustomModelForm, ModelPlazaItem, CustomModelModalRef, CustomModelModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
|
||||
const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelPlazaItem>({} as ModelPlazaItem);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<CustomModelForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const formValues = Form.useWatch([], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelPlazaItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: ModelPlazaItem) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
const handleUpdate = (data: CustomModelForm) => {
|
||||
setLoading(true)
|
||||
const { type, provider, ...rest} = data
|
||||
const res = isEdit ? updateCustomModel(model.id, rest) : addCustomModel(data)
|
||||
|
||||
res.then(() => {
|
||||
refresh && refresh()
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
const { logo, ...rest } = values;
|
||||
let formData: CustomModelForm = {
|
||||
...rest
|
||||
}
|
||||
formData.is_official = false;
|
||||
|
||||
if (typeof logo === 'object' && logo?.response?.data.file_id) {
|
||||
getFileLink(logo?.response?.data.file_id)
|
||||
.then(res => {
|
||||
const logoRes = res as { url: string }
|
||||
formData.logo = logoRes.url
|
||||
handleUpdate(formData)
|
||||
})
|
||||
.catch(() => {
|
||||
handleUpdate(formData)
|
||||
})
|
||||
} else {
|
||||
formData.logo = typeof logo === 'string' ? logo : logo.url
|
||||
handleUpdate(formData)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
console.log('formValues', formValues)
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createCustomModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<UploadImages />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('modelNew.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
disabled={isEdit}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('modelNew.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
disabled={isEdit}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label={t('modelNew.tags')}
|
||||
>
|
||||
<Select mode="tags" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default CustomModelModal;
|
||||
@@ -1,173 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { updateCompositeModel, modelTypeUrl, addCompositeModel } from '@/api/models'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import ModelImplement from './ModelImplement'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
|
||||
const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<CompositeModelForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const type = Form.useWatch(['type'], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelListItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: ModelListItem) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
api_key_ids: model.api_keys,
|
||||
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
})
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const { api_key_ids = [], logo, ...rest } = values
|
||||
|
||||
const formData: CompositeModelForm = {
|
||||
...rest,
|
||||
api_key_ids: api_key_ids.map(vo => (vo as ModelApiKey).id)
|
||||
}
|
||||
|
||||
if (logo?.response?.data.file_id) {
|
||||
getFileLink(logo?.response?.data.file_id).then(res => {
|
||||
const logoRes = res as { url: string }
|
||||
formData.logo = logoRes.url
|
||||
handleUpdate(formData)
|
||||
}).catch(() => {
|
||||
handleUpdate(formData)
|
||||
})
|
||||
} else {
|
||||
formData.logo = typeof logo === 'string' ? logo : logo.url
|
||||
handleUpdate(formData)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
const handleUpdate = (data: CompositeModelForm) => {
|
||||
setLoading(true)
|
||||
const { type, ...rest } = data
|
||||
const res = isEdit
|
||||
? updateCompositeModel(model.id, { ...rest })
|
||||
: addCompositeModel(data)
|
||||
|
||||
res.then(() => {
|
||||
refresh?.();
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createGroupModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ balance_strategy: 'none' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<UploadImages />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('modelNew.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({
|
||||
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
|
||||
value: typeof item === 'object' ? item.value : item
|
||||
}))}
|
||||
disabled={isEdit}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="load_balance_strategy"
|
||||
label={t('modelNew.load_balance_strategy')}
|
||||
>
|
||||
<Select
|
||||
options={['round_robin', 'none'].map(key => ({
|
||||
label: t(`modelNew.${key}`),
|
||||
value: key
|
||||
}))}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_key_ids">
|
||||
<ModelImplement type={type} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default GroupModelModal;
|
||||
@@ -1,92 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { KeyConfigModalForm, ProviderModelItem, KeyConfigModalRef, KeyConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { updateProviderApiKeys } from '@/api/models'
|
||||
|
||||
const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ProviderModelItem>({} as ProviderModelItem);
|
||||
const [form] = Form.useForm<KeyConfigModalForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ProviderModelItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: ProviderModelItem) => {
|
||||
setVisible(true);
|
||||
setModel(vo);
|
||||
};
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
|
||||
updateProviderApiKeys({
|
||||
...values,
|
||||
provider: model.provider
|
||||
}).then((res) => {
|
||||
if (refresh) {
|
||||
refresh();
|
||||
}
|
||||
handleClose()
|
||||
message.success(res as string)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`${model.provider} - ${t('modelNew.keyConfig')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.save`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default KeyConfigModal;
|
||||
@@ -1,181 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, Cascader, App, type CascaderProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps } from './types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { modelProviderUrl, getModelNewList } from '@/api/models'
|
||||
import type { ProviderModelItem } from '../../types'
|
||||
|
||||
const { SHOW_CHILD } = Cascader;
|
||||
|
||||
interface Option {
|
||||
value: string | number;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
[key: string]: any;
|
||||
}
|
||||
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
|
||||
refresh,
|
||||
type,
|
||||
groupedByProvider
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<SubModelModalForm>();
|
||||
const [selecteds, setSelecteds] = useState<any[]>([])
|
||||
const [modelList, setModelList] = useState<Option[]>([])
|
||||
const provider = Form.useWatch(['provider'], form)
|
||||
|
||||
useEffect(() => {
|
||||
if (provider && groupedByProvider) {
|
||||
const lastModels = groupedByProvider[provider] || []
|
||||
const list = lastModels.map(vo => [{ name: vo.model_name, id: vo.model_config_ids[0], value: vo.model_config_ids[0], provider }, { value: vo.id }])
|
||||
setSelecteds(list)
|
||||
form.setFieldValue('api_key_ids', lastModels.map(vo => [vo.model_config_ids[0], vo.id]))
|
||||
}
|
||||
}, [groupedByProvider, provider])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
setSelecteds([])
|
||||
setModelList([])
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
form.resetFields()
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
refresh?.(selecteds.map(vo => ({
|
||||
...vo[0],
|
||||
model_name: vo[0].name,
|
||||
model_config_ids: [vo[0].id],
|
||||
id: vo[1].value,
|
||||
api_key: vo[1].label
|
||||
})))
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
|
||||
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
|
||||
const lastFilterLit = value.filter(vo => vo.length !== 1)
|
||||
if (filterList.length) {
|
||||
message.warning(`【${filterList.map(vo => vo.label)}】${t('modelNew.selectOneTip')}`)
|
||||
form.setFieldValue('api_key_ids', lastFilterLit)
|
||||
}
|
||||
setSelecteds(selectedOptions)
|
||||
}
|
||||
|
||||
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
|
||||
form.setFieldValue('api_key_ids', undefined)
|
||||
if (provider) {
|
||||
getModelNewList({
|
||||
provider: provider,
|
||||
is_composite: false,
|
||||
is_active: true,
|
||||
type
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
const list = response[0]?.models || []
|
||||
setModelList(list.map(vo => {
|
||||
const children = vo.api_keys.map(item => ({
|
||||
label: item.api_key,
|
||||
value: item.id,
|
||||
}))
|
||||
return {
|
||||
...vo,
|
||||
label: vo.name,
|
||||
value: vo.id,
|
||||
children: children
|
||||
}
|
||||
}))
|
||||
|
||||
if (api_key_ids?.length) {
|
||||
form.setFieldsValue({
|
||||
api_key_ids: api_key_ids
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setModelList([])
|
||||
}
|
||||
}
|
||||
const displayRender: CascaderProps<Option>['displayRender'] = (labels, selectedOptions = []) =>
|
||||
labels.map((label, i) => {
|
||||
const option = selectedOptions[i];
|
||||
if (i === labels.length - 1) {
|
||||
return (
|
||||
<span key={option?.value || i}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span key={option?.value || i}>{label} / </span>;
|
||||
});
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('modelNew.implementConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('modelNew.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({
|
||||
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
|
||||
value: typeof item === 'object' ? item.value : item
|
||||
}))}
|
||||
onChange={(value) => handleChangeProvider(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="api_key_ids"
|
||||
label={t('modelNew.api_key_ids')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.api_key_ids') }) }]}
|
||||
>
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={modelList}
|
||||
onChange={handleChange}
|
||||
multiple
|
||||
autoClearSearchValue
|
||||
className="rb:w-full!"
|
||||
showCheckedStrategy={SHOW_CHILD}
|
||||
changeOnSelect
|
||||
displayRender={displayRender}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default SubModelModal;
|
||||
@@ -1,99 +0,0 @@
|
||||
import { type FC, useRef } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flex, Button, Space, App } from 'antd'
|
||||
|
||||
import type { SubModelModalRef, ModelList } from './types'
|
||||
import SubModelModal from './SubModelModal'
|
||||
import Empty from '@/components/Empty'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
interface ModelImplementProps {
|
||||
type?: string;
|
||||
value?: any;
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const { modal, message } = App.useApp();
|
||||
const subModelModalRef = useRef<SubModelModalRef>(null)
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!type || type.trim() === '') {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('modelNew.type') }))
|
||||
return
|
||||
}
|
||||
subModelModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleDelete = (vo: any) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
onChange?.(value?.filter((item: any) => item.id !== vo.id))
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleRefresh = (list: ModelList[]) => {
|
||||
const existingModels = value || [];
|
||||
let updatedModels = [...existingModels];
|
||||
|
||||
const provider = list[0].provider
|
||||
|
||||
updatedModels = updatedModels.filter(item => item.provider !== provider)
|
||||
updatedModels = [...updatedModels, ...list]
|
||||
|
||||
onChange?.([...updatedModels]);
|
||||
}
|
||||
|
||||
const groupedByProvider: Record<string, ModelList[]> = (value || []).reduce((acc: Record<string, ModelList[]>, item: ModelList) => {
|
||||
const provider = item.provider || 'unknown';
|
||||
if (!acc[provider]) acc[provider] = [];
|
||||
acc[provider].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, ModelList[]>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center">
|
||||
{t('modelNew.modelImplement')}
|
||||
|
||||
<Space>
|
||||
<Button type="primary" onClick={handleAdd} className="rb:px-2! rb:h-6!">+ {t('modelNew.addImplement')}</Button>
|
||||
<Button size="small" className="rb:px-2! rb:h-6!">{t('modelNew.noAuth')}</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
|
||||
<div className="rb:bg-[#F5F6F7] rb:rounded-lg rb:p-3 rb:mt-2">
|
||||
{!value || value.length === 0
|
||||
? <Empty size={88} />
|
||||
: value.map((item: any) => {
|
||||
return (
|
||||
<div key={item.id} className="rb:mb-4 rb:last:rb:mb-0 rb:bg-[#FBFDFF] rb:rounded-lg rb:p-3">
|
||||
<Flex gap={8} justify="space-between" align="center" className="rb:mb-2 rb:last:rb:mb-0">
|
||||
<div className="rb:font-medium">{item.model_name}</div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>
|
||||
</Flex>
|
||||
<div className="rb:text-[#5B6167] rb:my-2">{item.api_key}</div>
|
||||
<Tag className="rb:mb-2">{t(`modelNew.${item.provider}`)}</Tag>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<SubModelModal
|
||||
ref={subModelModalRef}
|
||||
refresh={handleRefresh}
|
||||
type={type}
|
||||
groupedByProvider={groupedByProvider}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelImplement
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { ModelListItem } from '../../types'
|
||||
|
||||
export interface ModelList extends ModelListItem {
|
||||
api_key_id: string;
|
||||
}
|
||||
export interface SubModelModalForm {
|
||||
provider: string;
|
||||
api_key_ids: string[][];
|
||||
}
|
||||
export interface SubModelModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface SubModelModalProps {
|
||||
type?: string;
|
||||
refresh?: (vo: ModelList[]) => void;
|
||||
groupedByProvider?: Record<string, ModelList[]>
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import { useState, useImperativeHandle, forwardRef, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Switch, Row, Col, Space, Tooltip } from 'antd'
|
||||
|
||||
import type { ProviderModelItem, ModelListItem, ModelListDetailRef, MultiKeyConfigModalRef } from '../types';
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag';
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import MultiKeyConfigModal from './MultiKeyConfigModal'
|
||||
import { getModelNewList, updateModelStatus, modelTypeUrl } from '@/api/models'
|
||||
import { getLogoUrl } from '../utils'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
|
||||
interface ModelListDetailProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({ refresh }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [data, setData] = useState<ProviderModelItem>({} as ProviderModelItem)
|
||||
const [list, setList] = useState<ModelListItem[]>([])
|
||||
const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [type, setType] = useState<string | undefined | null>(null)
|
||||
|
||||
const handleOpen = (vo: ProviderModelItem) => {
|
||||
setType(null)
|
||||
setOpen(true)
|
||||
getData(vo)
|
||||
}
|
||||
|
||||
const getData = (vo: ProviderModelItem) => {
|
||||
if (!vo.provider) return
|
||||
|
||||
getModelNewList({
|
||||
provider: vo.provider
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
setData(response[0])
|
||||
setList(response[0].models)
|
||||
})
|
||||
}
|
||||
const handleKeyConfig = (vo: ModelListItem) => {
|
||||
multiKeyConfigModalRef.current?.handleOpen(vo, data.provider)
|
||||
}
|
||||
const handleChange = (vo: ModelListItem) => {
|
||||
setLoading(true)
|
||||
updateModelStatus(vo.id, { is_active: !vo.is_active })
|
||||
.finally(() => {
|
||||
getData(data)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setType(null)
|
||||
setOpen(false)
|
||||
refresh?.()
|
||||
}
|
||||
const handleRefresh = () => {
|
||||
getData(data)
|
||||
}
|
||||
const handleTypeChange = (value: string) => {
|
||||
setType(value)
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
const filterList = useMemo(() => {
|
||||
if (!type) return list
|
||||
return list.filter(vo => vo.type === type)
|
||||
}, [type, list])
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<>{t(`modelNew.${data.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<CustomSelect
|
||||
value={type}
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
onChange={handleTypeChange}
|
||||
className="rb:w-full"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.type')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{filterList.length === 0
|
||||
? <PageEmpty />
|
||||
: <div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
|
||||
{filterList.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space className="rb:mt-1!">
|
||||
<Tag>{t(`modelNew.${item.type}`)}</Tag>
|
||||
<Tag color="warning">{item.api_keys.length}{t('modelNew.apiKeyNum')}</Tag>
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
extra={<Switch defaultChecked={item.is_active} disabled={loading} onChange={() => handleChange(item)} />}
|
||||
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2">{item.description}</div>
|
||||
</Tooltip>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
<MultiKeyConfigModal
|
||||
ref={multiKeyConfigModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</RbDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelListDetail;
|
||||
@@ -1,106 +0,0 @@
|
||||
import { useState, useImperativeHandle, forwardRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Space, App, Flex, Tooltip, Divider } from 'antd'
|
||||
import { UsergroupAddOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef } from '../types';
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import { getModelPlaza, addModelPlaza } from '@/api/models'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag';
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import { getLogoUrl } from '../utils'
|
||||
|
||||
interface ModelSquareDetailProps {
|
||||
refresh: () => void;
|
||||
handleEdit: (vo: ModelPlazaItem) => void;
|
||||
}
|
||||
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [model, setModel] = useState<ModelPlaza>({} as ModelPlaza)
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [list, setList] = useState<ModelPlazaItem[]>([])
|
||||
|
||||
const handleOpen = (vo: ModelPlaza) => {
|
||||
setModel(vo)
|
||||
setOpen(true)
|
||||
getList(vo)
|
||||
}
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
refresh()
|
||||
}
|
||||
const getList = (vo: ModelPlaza) => {
|
||||
getModelPlaza({ provider: vo.provider })
|
||||
.then(res => {
|
||||
const response = res as ModelPlaza[]
|
||||
setList(response.length > 0 ? response[0].models : [])
|
||||
})
|
||||
}
|
||||
const handleAdd = (item: ModelPlazaItem) => {
|
||||
addModelPlaza(item.id)
|
||||
.then(() => {
|
||||
message.success(`${item.name}${t('modelNew.addSuccess')}`)
|
||||
getList(model)
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<>{t(`modelNew.${model.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="rb:h-full rb:overflow-y-auto">
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space size={8}>
|
||||
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
|
||||
</Tooltip>
|
||||
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Divider size="middle" />
|
||||
<Flex justify="space-between">
|
||||
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
|
||||
<Space>
|
||||
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
|
||||
{item.is_added
|
||||
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
|
||||
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</RbDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelSquareDetail;
|
||||
@@ -1,122 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ModelListItem, MultiKeyForm, MultiKeyConfigModalRef, MultiKeyConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { addModelApiKey, deleteModelApiKey, getModelInfo } from '@/api/models'
|
||||
|
||||
const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigModalProps>(({ refresh }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
|
||||
const [form] = Form.useForm<MultiKeyForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelListItem);
|
||||
refresh?.()
|
||||
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: ModelListItem) => {
|
||||
setVisible(true);
|
||||
getData(vo)
|
||||
};
|
||||
|
||||
const getData = (vo: ModelListItem) => {
|
||||
if (!vo.id) return
|
||||
|
||||
getModelInfo(vo?.id)
|
||||
.then(res => {
|
||||
setModel(res as ModelListItem)
|
||||
})
|
||||
}
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
addModelApiKey(model.id, {
|
||||
...values,
|
||||
model_config_id: model.id,
|
||||
model_name: model.name,
|
||||
provider: model.provider,
|
||||
}).then(() => {
|
||||
message.success(t('common.saveSuccess'))
|
||||
form.resetFields();
|
||||
getData(model)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
const handleDelete = (api_key_id: string) => {
|
||||
deleteModelApiKey(api_key_id)
|
||||
.then(() => {
|
||||
message.success(t('common.deleteSuccess'))
|
||||
getData(model)
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`${model.name} - ${t('modelNew.keyConfig')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
{model.api_keys && model.api_keys.length > 0 && (
|
||||
<div className="rb:mb-4">
|
||||
{model.api_keys.map((key) => (
|
||||
<div key={key.id} className="rb:flex rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
|
||||
<div>
|
||||
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium">{key.api_key}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{key.api_base}</div>
|
||||
</div>
|
||||
<Button type="primary" danger ghost onClick={() => handleDelete(key.id)}>{t('common.remove')}</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" block onClick={handleSave} loading={loading}>+ {t('modelNew.add')}</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default MultiKeyConfigModal;
|
||||
@@ -1,124 +1,99 @@
|
||||
import { useState, useRef, type FC } from 'react';
|
||||
import { Button, Flex, Space, type SegmentedProps, Form } from 'antd'
|
||||
import { Row, Col, Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import GroupModelModal from './components/GroupModelModal'
|
||||
import type { ModelListItem, GroupModelModalRef, CustomModelModalRef, ModelPlazaItem, BaseRef, Query } from './types'
|
||||
import ConfigModal from './components/ConfigModal'
|
||||
import type { Model, DescriptionItem, ConfigModalRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import PageTabs from '@/components/PageTabs'
|
||||
import GroupModel from './Group'
|
||||
import ModelList from './List'
|
||||
import ModelSquare from './Square'
|
||||
import CustomModelModal from './components/CustomModelModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
const tabKeys = ['group', 'list', 'square']
|
||||
const ModelManagement: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('group');
|
||||
const configModalRef = useRef<GroupModelModalRef>(null)
|
||||
const customModelModalRef = useRef<CustomModelModalRef>(null)
|
||||
const groupRef = useRef<BaseRef>(null)
|
||||
const squareRef = useRef<BaseRef>(null)
|
||||
const [form] = Form.useForm<Query>()
|
||||
const query = Form.useWatch([], form)
|
||||
const [query, setQuery] = useState({})
|
||||
const configModalRef = useRef<ConfigModalRef>(null)
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
|
||||
const formatTabItems = () => {
|
||||
return tabKeys.map(value => ({
|
||||
value,
|
||||
label: t(`modelNew.${value}`),
|
||||
}))
|
||||
}
|
||||
const handleChangeTab = (value: SegmentedProps['value']) => {
|
||||
setActiveTab(value as string);
|
||||
form.resetFields()
|
||||
const formatData = (data: Model) => {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: t(`model.type`),
|
||||
children: data.type || '-',
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
label: t(`model.provider`),
|
||||
children: data.api_keys[0].provider || '-',
|
||||
},
|
||||
{
|
||||
key: 'is_active',
|
||||
label: t(`model.status`),
|
||||
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
|
||||
},
|
||||
{
|
||||
key: 'created',
|
||||
label: t(`model.created`),
|
||||
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
|
||||
switch(activeTab) {
|
||||
case 'group':
|
||||
configModalRef?.current?.handleOpen(vo as ModelListItem)
|
||||
break
|
||||
case 'square':
|
||||
customModelModalRef?.current?.handleOpen(vo as ModelPlazaItem)
|
||||
break
|
||||
}
|
||||
const handleEdit = (model?: Model) => {
|
||||
configModalRef?.current?.handleOpen(model)
|
||||
}
|
||||
const handleRefresh = () => {
|
||||
switch (activeTab) {
|
||||
case 'group':
|
||||
groupRef.current?.getList()
|
||||
break
|
||||
case 'square':
|
||||
squareRef.current?.getList()
|
||||
break
|
||||
}
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({ search: value })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify="space-between" align="center">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={formatTabItems()}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
<div className="rb:w-full">
|
||||
<Row className='rb:mb-[16px] rb:w-full'>
|
||||
<Col span={6}>
|
||||
<SearchInput
|
||||
placeholder={t('model.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={18} className="rb:text-right">
|
||||
<Button type="primary" onClick={() => handleEdit()}>{t('model.createModel')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form form={form}>
|
||||
<Space size={12}>
|
||||
{activeTab === 'list' &&
|
||||
<Form.Item name="type" noStyle>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
className="rb:w-30"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.type')}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{(activeTab === 'list' || activeTab === 'square') &&
|
||||
<Form.Item name="provider" noStyle>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
className="rb:w-30"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.provider')}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{activeTab !== 'list' &&
|
||||
<Form.Item name="search" noStyle>
|
||||
<SearchInput
|
||||
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
|
||||
className="rb:w-70!"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>}
|
||||
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>}
|
||||
</Space>
|
||||
</Form>
|
||||
</Flex>
|
||||
<PageScrollList
|
||||
ref={scrollListRef}
|
||||
url={getModelListUrl}
|
||||
query={query}
|
||||
renderItem={(item: Model) => (
|
||||
<RbCard
|
||||
title={item.name}
|
||||
>
|
||||
{formatData(item)?.map((description: DescriptionItem) => (
|
||||
<div
|
||||
key={description.key}
|
||||
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
|
||||
<span className={clsx({
|
||||
"rb:text-[#212332]": description.key !== 'is_active',
|
||||
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
|
||||
})}>{(description.children as string)}</span>
|
||||
</div>
|
||||
))}
|
||||
<Button className="rb:mt-[8px]" type="primary" ghost block onClick={() => handleEdit(item)}>{t('model.configureBtn')}</Button>
|
||||
</RbCard>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4">
|
||||
{activeTab === 'group' && <GroupModel ref={groupRef} query={query} handleEdit={handleEdit} />}
|
||||
{activeTab === 'list' && <ModelList query={query} />}
|
||||
{activeTab === 'square' && <ModelSquare ref={squareRef} query={query} handleEdit={handleEdit} />}
|
||||
</div>
|
||||
<GroupModelModal
|
||||
<ConfigModal
|
||||
ref={configModalRef}
|
||||
refresh={handleRefresh}
|
||||
refresh={() => scrollListRef?.current?.refresh()}
|
||||
/>
|
||||
<CustomModelModal
|
||||
ref={customModelModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,139 +1,70 @@
|
||||
export interface Query {
|
||||
type?: string;
|
||||
provider?: string;
|
||||
is_active?: boolean;
|
||||
is_public?: boolean;
|
||||
is_composite?: boolean;
|
||||
search?: string;
|
||||
|
||||
pagesize?: number;
|
||||
page?: number;
|
||||
// 模型表单数据类型
|
||||
export interface ModelFormData extends ApiKey {
|
||||
name: string;
|
||||
type: string;
|
||||
api_keys: ApiKey;
|
||||
}
|
||||
|
||||
export interface DescriptionItem {
|
||||
key: string;
|
||||
label: string;
|
||||
children: string;
|
||||
}
|
||||
export interface CompositeModelForm {
|
||||
logo?: any;
|
||||
name: string;
|
||||
type?: string;
|
||||
description: string;
|
||||
api_key_ids: ModelApiKey[] | string[];
|
||||
}
|
||||
export interface GroupModelModalRef {
|
||||
handleOpen: (model?: ModelListItem) => void;
|
||||
}
|
||||
export interface GroupModelModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
export interface ModelListDetailRef {
|
||||
handleOpen: (vo: ProviderModelItem) => void;
|
||||
}
|
||||
|
||||
|
||||
export interface ModelApiKey {
|
||||
model_name: string;
|
||||
description: string | null;
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
config: any;
|
||||
is_active: boolean;
|
||||
priority: string;
|
||||
// 模型类型定义
|
||||
export interface Model {
|
||||
id: string;
|
||||
usage_count: string;
|
||||
last_used_at: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
model_config_ids: string[];
|
||||
}
|
||||
export interface ModelListItem {
|
||||
model_name?: string;
|
||||
model_config_ids: string[];
|
||||
name: string;
|
||||
type: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
provider: string;
|
||||
config: any;
|
||||
description?: string;
|
||||
config: Record<string, unknown>;
|
||||
is_active: boolean;
|
||||
is_public: boolean;
|
||||
created_at: string | number;
|
||||
updated_at: string | number;
|
||||
api_keys: ApiKey[];
|
||||
|
||||
// provider: string;
|
||||
// temperature: number,
|
||||
// topP: number,
|
||||
// status: string;
|
||||
// vectorDimension: number;
|
||||
// batchSize: number;
|
||||
// truncateStrategy: string;
|
||||
// created: string;
|
||||
// updatedAt: string;
|
||||
// descriptionItems?: Record<string, unknown>[];
|
||||
// basicParameters?: string;
|
||||
// normalization?: string;
|
||||
// maxInputLength?: number;
|
||||
// encodingFormat?: string;
|
||||
// enablePooling?: boolean;
|
||||
// poolingStrategy?: string;
|
||||
// apiKey?: string;
|
||||
// apiEndpoint?: string;
|
||||
// timeout?: number;
|
||||
// autoRetry?: boolean;
|
||||
// retryCount?: number;
|
||||
}
|
||||
interface ApiKey {
|
||||
model_name?: string;
|
||||
provider: string;
|
||||
api_key?: string;
|
||||
api_base?: string;
|
||||
config?: Record<string, unknown>;
|
||||
is_active?: boolean;
|
||||
priority?: string;
|
||||
id: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
api_keys: ModelApiKey[]
|
||||
}
|
||||
export interface ProviderModelItem {
|
||||
provider: string;
|
||||
logo?: string;
|
||||
tags: string[];
|
||||
models: ModelListItem[];
|
||||
}
|
||||
export interface KeyConfigModalForm {
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
}
|
||||
export interface KeyConfigModalRef {
|
||||
handleOpen: (vo: ProviderModelItem) => void;
|
||||
}
|
||||
export interface KeyConfigModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
export interface MultiKeyForm {
|
||||
model_config_id?: string;
|
||||
model_name: string;
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
usage_count?: string;
|
||||
last_used_at?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface MultiKeyConfigModalRef {
|
||||
handleOpen: (vo: ModelListItem, provider?: string) => void;
|
||||
// 定义组件暴露的方法接口
|
||||
export interface ConfigModalRef {
|
||||
handleOpen: (model?: Model) => void;
|
||||
}
|
||||
export interface MultiKeyConfigModalProps {
|
||||
export interface ConfigModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export interface ModelPlaza {
|
||||
provider: string;
|
||||
models: ModelPlazaItem[];
|
||||
}
|
||||
export interface ModelPlazaItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
provider: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
is_deprecated: boolean;
|
||||
is_official: boolean;
|
||||
tags: string[];
|
||||
add_count: number;
|
||||
is_added: boolean;
|
||||
}
|
||||
export interface ModelSquareDetailRef {
|
||||
handleOpen: (vo: ModelPlaza) => void;
|
||||
}
|
||||
export interface CustomModelForm {
|
||||
name: string;
|
||||
type?: string;
|
||||
provider?: string;
|
||||
logo?: any;
|
||||
description: string;
|
||||
is_official: boolean;
|
||||
tags: string[];
|
||||
}
|
||||
export interface CustomModelModalRef {
|
||||
handleOpen: (vo?: ModelPlazaItem) => void;
|
||||
}
|
||||
export interface CustomModelModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export interface BaseRef {
|
||||
getList: () => void;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import bedrockIcon from '@/assets/images/model/bedrock.svg'
|
||||
import dashscopeIcon from '@/assets/images/model/dashscope.png'
|
||||
import gpustackIcon from '@/assets/images/model/gpustack.png'
|
||||
import ollamaIcon from '@/assets/images/model/ollama.svg'
|
||||
import openaiIcon from '@/assets/images/model/openai.svg'
|
||||
import xinferenceIcon from '@/assets/images/model/xinference.svg'
|
||||
|
||||
export const ICONS = {
|
||||
bedrock: bedrockIcon,
|
||||
dashscope: dashscopeIcon,
|
||||
gpustack: gpustackIcon,
|
||||
ollama: ollamaIcon,
|
||||
openai: openaiIcon,
|
||||
xinference: xinferenceIcon
|
||||
}
|
||||
|
||||
export const getLogoUrl = (logo?: string) => {
|
||||
if (!logo) {
|
||||
return undefined
|
||||
}
|
||||
if (logo.startsWith('http')) {
|
||||
return logo
|
||||
}
|
||||
|
||||
return ICONS[logo as keyof typeof ICONS] || undefined
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const configList = [
|
||||
key: 'reflection_model_id',
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
|
||||
},
|
||||
// 迭代周期
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'llm', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -80,7 +80,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -94,7 +94,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createWorkspace } from '@/api/workspaces'
|
||||
import RadioGroupCard from '@/components/RadioGroupCard'
|
||||
import { getModelListUrl, getModelList } from '@/api/models'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -29,7 +29,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
const [form] = Form.useForm<SpaceModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<Space | null>(null)
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
@@ -80,9 +80,9 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
}, [])
|
||||
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
.then(res => {
|
||||
const response = res as { items: ModelListItem[] }
|
||||
const response = res as { items: Model[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
@@ -134,7 +134,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -148,7 +148,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||