Compare commits

...

1 Commits

Author SHA1 Message Date
Mark
524aed19d4 Revert "model and statistic" 2026-01-28 14:30:27 +08:00
14 changed files with 232 additions and 1467 deletions

View File

@@ -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)

View File

@@ -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
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,6 +24,7 @@ router = APIRouter(
@router.get("/type", response_model=ApiResponse)
def get_model_types():
return success(msg="获取模型类型成功", data=list(ModelType))
@@ -36,39 +35,33 @@ def get_model_providers():
@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,
@@ -79,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)
@@ -90,142 +83,6 @@ def get_model_list(
raise
@router.get("/new", 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="提供商筛选(基于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(False, 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)
):
"""更新基础模型"""
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,
@@ -281,71 +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:
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,
@@ -422,51 +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 = 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]
return success(data=result_list, 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,
@@ -481,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)}")
@@ -588,3 +334,5 @@ async def validate_model_config(
return success(data=model_schema.ModelValidateResponse(**result), msg="验证完成")

View File

@@ -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"
]

View File

@@ -1,31 +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 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"
# IMAGE = "image"
# AUDIO = "audio"
# VISION = "vision"
class ModelProvider(StrEnum):
@@ -42,37 +30,16 @@ class ModelProvider(StrEnum):
XINFERENCE = "xinference"
GPUSTACK = "gpustack"
BEDROCK = "bedrock"
COMPOSITE = "composite"
class LoadBalanceStrategy(StrEnum):
"""API Key负载均衡策略枚举"""
ROUND_ROBIN = "round_robin" # 轮询
WEIGHTED_ROUND_ROBIN = "weighted_round_robin" # 加权轮询
RANDOM = "random" # 随机
# 多对多关联表
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="模型描述")
# 模型配置参数
@@ -89,28 +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="负载均衡策略")
# 时间戳
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")
@@ -123,41 +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="模型被用户添加的次数")
# 关联关系
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})>"

View File

@@ -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.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"查询模型配置列表失败: {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.updated_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,37 +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_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"""
@@ -468,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)
@@ -494,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
@@ -529,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)
@@ -589,74 +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()).all()
@staticmethod
def create(db: Session, data: dict) -> 'ModelBase':
model_base = ModelBase(**data)
db.add(model_base)
return model_base
@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)
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

View File

@@ -4,10 +4,6 @@ import datetime
import uuid
from app.models.models_model import ModelProvider, ModelType
from app.core.logging_config import get_business_logger
schema_logger = get_business_logger()
@@ -16,9 +12,7 @@ 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="是否公开")
@@ -27,7 +21,6 @@ class ModelConfigBase(BaseModel):
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)
@@ -37,22 +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: ModelType = Field(..., 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列表")
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:
# 情况1ORM 对象列表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()

View File

@@ -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}

View File

@@ -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)

View File

@@ -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'):

View File

@@ -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,

View File

@@ -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,112 +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
}
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 = 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
)
# 更新基本信息
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
# 更新 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:
"""删除模型配置"""
@@ -453,132 +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) -> List[ModelApiKey]:
"""根据provider为多个ModelConfig创建API Key"""
created_keys = []
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"]:
print(validation_result)
if not validation_result["valid"]:
raise BusinessException(
f"模型配置验证失败: {validation_result['error']}",
BizCode.INVALID_PARAMETER
)
# 创建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
@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"
)
if not validation_result["valid"]:
raise BusinessException(
f"模型配置验证失败: {validation_result['error']}",
BizCode.INVALID_PARAMETER
)
api_key = ModelApiKeyRepository.create(db, api_key_data)
db.commit()
@@ -593,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
@@ -649,84 +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):
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

View File

@@ -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使用简单整合")

View File

@@ -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)

View File

@@ -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 MemoryBears foundation and delivers a noticeably smoother experience. Well 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 MemoryBears foundation and delivers a noticeably smoother experience.\nWell 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 systems 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!"