diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index d57ee69d..3b4e5a25 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -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) diff --git a/api/app/controllers/model_controller.py b/api/app/controllers/model_controller.py index 509f7cad..42d59664 100644 --- a/api/app/controllers/model_controller.py +++ b/api/app/controllers/model_controller.py @@ -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="验证完成") + + diff --git a/api/app/models/__init__.py b/api/app/models/__init__.py index a429dd8e..e069b40d 100644 --- a/api/app/models/__init__.py +++ b/api/app/models/__init__.py @@ -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" ] diff --git a/api/app/models/models_model.py b/api/app/models/models_model.py index a8918c7c..2e60ef1c 100644 --- a/api/app/models/models_model.py +++ b/api/app/models/models_model.py @@ -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"" -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"" - - -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"" \ No newline at end of file + return f"" diff --git a/api/app/repositories/model_repository.py b/api/app/repositories/model_repository.py index 8e4632cc..1fe29d66 100644 --- a/api/app/repositories/model_repository.py +++ b/api/app/repositories/model_repository.py @@ -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 \ No newline at end of file diff --git a/api/app/schemas/model_schema.py b/api/app/schemas/model_schema.py index ce1b36bb..68f15115 100644 --- a/api/app/schemas/model_schema.py +++ b/api/app/schemas/model_schema.py @@ -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: - # 情况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() \ No newline at end of file diff --git a/api/app/services/app_statistics_service.py b/api/app/services/app_statistics_service.py deleted file mode 100644 index c164924a..00000000 --- a/api/app/services/app_statistics_service.py +++ /dev/null @@ -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} diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 524c9ff6..0d1f51a4 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -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) diff --git a/api/app/services/llm_router.py b/api/app/services/llm_router.py index 9e102ac3..9ef9dbb1 100644 --- a/api/app/services/llm_router.py +++ b/api/app/services/llm_router.py @@ -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'): diff --git a/api/app/services/memory_config_service.py b/api/app/services/memory_config_service.py index e09cf67f..26b86b71 100644 --- a/api/app/services/memory_config_service.py +++ b/api/app/services/memory_config_service.py @@ -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, diff --git a/api/app/services/model_service.py b/api/app/services/model_service.py index 5b2ab7e6..e94a889b 100644 --- a/api/app/services/model_service.py +++ b/api/app/services/model_service.py @@ -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 diff --git a/api/app/services/multi_agent_orchestrator.py b/api/app/services/multi_agent_orchestrator.py index d9062eaf..4bcd28cd 100644 --- a/api/app/services/multi_agent_orchestrator.py +++ b/api/app/services/multi_agent_orchestrator.py @@ -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,使用简单整合") diff --git a/api/app/services/shared_chat_service.py b/api/app/services/shared_chat_service.py index 1d012088..5eee5edc 100644 --- a/api/app/services/shared_chat_service.py +++ b/api/app/services/shared_chat_service.py @@ -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) diff --git a/api/app/version_info.json b/api/app/version_info.json index 86a5e33e..bee52989 100644 --- a/api/app/version_info.json +++ b/api/app/version_info.json @@ -5,12 +5,11 @@ "releaseDate": "2026-1-23", "upgradePosition": "\uD83D\uDC3B 本次更新主要优化使用体验和修复已知问题,让系统更稳定、更好用。", "coreUpgrades": [ - "1. 工作流更好用了
* 界面更清晰,一眼看懂怎么配置
* 新增节点输出变量展示,方便其他节点引用
* 修复了几个影响体验的bug", - "2. 智能体配置更简单
* 提示词和变量联动更顺畅
* 配置界面重新整理,找功能更方便", - "3. 记忆系统更稳定
* 优化了情绪记忆和隐性记忆的缓存更新
* 修复了记忆配置页面的报错问题
* 现在能自动识别用户和AI的身份了", - "4. 知识库体验提升
* 修复了文档解析异常的问题
* 上传文档时能看到处理进度了
* 取消了操作也不会报错了", - "5. 系统整体更可靠
* 修复了新用户访问跳转问题
* 流式接口更稳定,长对话不断线
* 调整了菜单顺序,操作更顺手", - "
", + "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
* Cleaner, more intuitive UI for easier configuration at a glance
* Added visibility of node output variables, making them easier to reference in downstream nodes
* Fixed several usability-related bugs that affected the workflow experience", - "2. Simpler Agent Configuration
* Smoother linkage between prompts and variables
* Reorganized configuration layout for easier navigation and better clarity", - "3. More Stable Memory System
* Optimized cache refresh for emotional memory and implicit memory
* Fixed error issues on the memory configuration page
* The system can now automatically distinguish between user and AI roles", - "4. Enhanced Knowledge Base Experience
* Fixed issues with document parsing failures
* Upload progress is now displayed during document processing
* Canceling an upload no longer triggers errors", - "5. Overall System Reliability Improvements
* Fixed redirect issues affecting new users
* Improved stability of streaming APIs to prevent interruptions during long conversations
* Adjusted menu ordering for a smoother and more intuitive workflow", - "
", - "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 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。", - "
", + "记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。", + "记忆配置:简化了配置流程,内置自动提取关键信息的\"记忆萃取\"和管理生命周期的\"遗忘\"引擎。", + "知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。", + "全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。", + "测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。", + "记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解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.", - "
", + "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!"