From 7ce29019f7b42403d2f9d9122e48bfda252ad05e Mon Sep 17 00:00:00 2001 From: Ke Sun Date: Wed, 1 Apr 2026 15:06:26 +0800 Subject: [PATCH] feat(memory): Add memory config API controller and end user info endpoints - Create new memory_config_api_controller.py for dedicated memory configuration management - Add /end_user/info GET endpoint to retrieve end user information (aliases, metadata) - Add /end_user/info/update POST endpoint to update end user details - Move /memory/configs endpoint from memory_api_controller to memory_config_api_controller - Extract _get_current_user helper function to build user context from API key auth - Support optional app_id parameter in end user creation with UUID validation - Update service controller imports with alphabetical ordering and multi-line formatting - Register memory_config_api_controller router in service module initialization - Refactor memory_api_controller imports for consistency and clarity --- api/app/controllers/service/__init__.py | 13 +- .../service/end_user_api_controller.py | 80 ++++++- .../service/memory_api_controller.py | 60 +----- .../service/memory_config_api_controller.py | 39 ++++ .../storage_services/search/__init__.py | 197 ++++++++---------- api/app/schemas/memory_api_schema.py | 2 + api/app/services/memory_api_service.py | 3 +- 7 files changed, 219 insertions(+), 175 deletions(-) create mode 100644 api/app/controllers/service/memory_config_api_controller.py diff --git a/api/app/controllers/service/__init__.py b/api/app/controllers/service/__init__.py index 96da0949..52d4b732 100644 --- a/api/app/controllers/service/__init__.py +++ b/api/app/controllers/service/__init__.py @@ -4,7 +4,17 @@ 认证方式: API Key """ from fastapi import APIRouter -from . import app_api_controller, rag_api_knowledge_controller, rag_api_document_controller, rag_api_file_controller, rag_api_chunk_controller, memory_api_controller, end_user_api_controller + +from . import ( + app_api_controller, + end_user_api_controller, + memory_api_controller, + memory_config_api_controller, + rag_api_chunk_controller, + rag_api_document_controller, + rag_api_file_controller, + rag_api_knowledge_controller, +) # 创建 V1 API 路由器 service_router = APIRouter() @@ -17,5 +27,6 @@ service_router.include_router(rag_api_file_controller.router) service_router.include_router(rag_api_chunk_controller.router) service_router.include_router(memory_api_controller.router) service_router.include_router(end_user_api_controller.router) +service_router.include_router(memory_config_api_controller.router) __all__ = ["service_router"] diff --git a/api/app/controllers/service/end_user_api_controller.py b/api/app/controllers/service/end_user_api_controller.py index 9d410bd2..658cef15 100644 --- a/api/app/controllers/service/end_user_api_controller.py +++ b/api/app/controllers/service/end_user_api_controller.py @@ -5,6 +5,7 @@ import uuid from fastapi import APIRouter, Body, Depends, Request from sqlalchemy.orm import Session +from app.controllers import user_memory_controllers from app.core.api_key_auth import require_api_key from app.core.error_codes import BizCode from app.core.exceptions import BusinessException @@ -13,13 +14,31 @@ from app.core.response_utils import success from app.db import get_db from app.repositories.end_user_repository import EndUserRepository from app.schemas.api_key_schema import ApiKeyAuth +from app.schemas.end_user_info_schema import EndUserInfoUpdate from app.schemas.memory_api_schema import CreateEndUserRequest, CreateEndUserResponse +from app.services import api_key_service from app.services.memory_config_service import MemoryConfigService router = APIRouter(prefix="/end_user", tags=["V1 - End User API"]) logger = get_business_logger() +def _get_current_user(api_key_auth: ApiKeyAuth, db: Session): + """Build a current_user object from API key auth + + Args: + api_key_auth: Validated API key auth info + db: Database session + + Returns: + User object with current_workspace_id set + """ + api_key = api_key_service.ApiKeyService.get_api_key(db, api_key_auth.api_key_id, api_key_auth.workspace_id) + current_user = api_key.creator + current_user.current_workspace_id = api_key_auth.workspace_id + return current_user + + @router.post("/create") @require_api_key(scopes=["memory"]) async def create_end_user( @@ -37,6 +56,7 @@ async def create_end_user( Optionally accepts a memory_config_id to connect the end user to a specific memory configuration. If not provided, falls back to the workspace default config. + Optionally accepts an app_id to bind the end user to a specific app. """ body = await request.json() payload = CreateEndUserRequest(**body) @@ -71,9 +91,20 @@ async def create_end_user( else: logger.warning(f"No default memory config found for workspace: {workspace_id}") + # Resolve app_id: explicit from payload, otherwise None + app_id = None + if payload.app_id: + try: + app_id = uuid.UUID(payload.app_id) + except ValueError: + raise BusinessException( + f"Invalid app_id format: {payload.app_id}", + BizCode.INVALID_PARAMETER + ) + end_user_repo = EndUserRepository(db) end_user = end_user_repo.get_or_create_end_user_with_config( - app_id=api_key_auth.resource_id, + app_id=app_id, workspace_id=workspace_id, other_id=payload.other_id, memory_config_id=memory_config_id, @@ -90,3 +121,50 @@ async def create_end_user( } return success(data=CreateEndUserResponse(**result).model_dump(), msg="End user created successfully") + + +@router.get("/info") +@require_api_key(scopes=["memory"]) +async def get_end_user_info( + request: Request, + end_user_id: str, + api_key_auth: ApiKeyAuth = None, + db: Session = Depends(get_db), +): + """ + Get end user info. + + Retrieves the info record (aliases, meta_data, etc.) for the specified end user. + Delegates to the manager-side controller for shared logic. + """ + current_user = _get_current_user(api_key_auth, db) + return await user_memory_controllers.get_end_user_info( + end_user_id=end_user_id, + current_user=current_user, + db=db, + ) + + +@router.post("/info/update") +@require_api_key(scopes=["memory"]) +async def update_end_user_info( + request: Request, + api_key_auth: ApiKeyAuth = None, + db: Session = Depends(get_db), + message: str = Body(None, description="Request body"), +): + """ + Update end user info. + + Updates the info record (other_name, aliases, meta_data) for the specified end user. + Delegates to the manager-side controller for shared logic. + """ + body = await request.json() + payload = EndUserInfoUpdate(**body) + + current_user = _get_current_user(api_key_auth, db) + return await user_memory_controllers.update_end_user_info( + info_update=payload, + current_user=current_user, + db=db, + ) diff --git a/api/app/controllers/service/memory_api_controller.py b/api/app/controllers/service/memory_api_controller.py index dc5e0408..d1229205 100644 --- a/api/app/controllers/service/memory_api_controller.py +++ b/api/app/controllers/service/memory_api_controller.py @@ -1,22 +1,20 @@ """Memory 服务接口 - 基于 API Key 认证""" +from fastapi import APIRouter, Body, Depends, Request +from sqlalchemy.orm import Session + from app.core.api_key_auth import require_api_key from app.core.logging_config import get_business_logger from app.core.response_utils import success from app.db import get_db from app.schemas.api_key_schema import ApiKeyAuth from app.schemas.memory_api_schema import ( - CreateEndUserRequest, - CreateEndUserResponse, - ListConfigsResponse, MemoryReadRequest, MemoryReadResponse, MemoryWriteRequest, MemoryWriteResponse, ) from app.services.memory_api_service import MemoryAPIService -from fastapi import APIRouter, Body, Depends, Request -from sqlalchemy.orm import Session router = APIRouter(prefix="/memory", tags=["V1 - Memory API"]) logger = get_business_logger() @@ -91,55 +89,3 @@ async def read_memory_api_service( logger.info(f"Memory read successful for end_user: {payload.end_user_id}") return success(data=MemoryReadResponse(**result).model_dump(), msg="Memory read successfully") - - -@router.get("/configs") -@require_api_key(scopes=["memory"]) -async def list_memory_configs( - request: Request, - api_key_auth: ApiKeyAuth = None, - db: Session = Depends(get_db), -): - """ - List all memory configs for the workspace. - - Returns all available memory configurations associated with the authorized workspace. - """ - logger.info(f"List configs request - workspace_id: {api_key_auth.workspace_id}") - - memory_api_service = MemoryAPIService(db) - - result = memory_api_service.list_memory_configs( - workspace_id=api_key_auth.workspace_id, - ) - - logger.info(f"Listed {result['total']} configs for workspace: {api_key_auth.workspace_id}") - return success(data=ListConfigsResponse(**result).model_dump(), msg="Configs listed successfully") - - -@router.post("/end_users") -@require_api_key(scopes=["memory"]) -async def create_end_user( - request: Request, - api_key_auth: ApiKeyAuth = None, - db: Session = Depends(get_db), -): - """ - Create an end user. - - Creates a new end user for the authorized workspace. - If an end user with the same other_id already exists, returns the existing one. - """ - body = await request.json() - payload = CreateEndUserRequest(**body) - logger.info(f"Create end user request - other_id: {payload.other_id}, workspace_id: {api_key_auth.workspace_id}") - - memory_api_service = MemoryAPIService(db) - - result = memory_api_service.create_end_user( - workspace_id=api_key_auth.workspace_id, - other_id=payload.other_id, - ) - - logger.info(f"End user ready: {result['id']}") - return success(data=CreateEndUserResponse(**result).model_dump(), msg="End user created successfully") diff --git a/api/app/controllers/service/memory_config_api_controller.py b/api/app/controllers/service/memory_config_api_controller.py new file mode 100644 index 00000000..bfc60510 --- /dev/null +++ b/api/app/controllers/service/memory_config_api_controller.py @@ -0,0 +1,39 @@ +"""Memory Config 服务接口 - 基于 API Key 认证""" + +from fastapi import APIRouter, Depends, Request +from sqlalchemy.orm import Session + +from app.core.api_key_auth import require_api_key +from app.core.logging_config import get_business_logger +from app.core.response_utils import success +from app.db import get_db +from app.schemas.api_key_schema import ApiKeyAuth +from app.schemas.memory_api_schema import ListConfigsResponse +from app.services.memory_api_service import MemoryAPIService + +router = APIRouter(prefix="/memory_config", tags=["V1 - Memory Config API"]) +logger = get_business_logger() + + +@router.get("/configs") +@require_api_key(scopes=["memory"]) +async def list_memory_configs( + request: Request, + api_key_auth: ApiKeyAuth = None, + db: Session = Depends(get_db), +): + """ + List all memory configs for the workspace. + + Returns all available memory configurations associated with the authorized workspace. + """ + logger.info(f"List configs request - workspace_id: {api_key_auth.workspace_id}") + + memory_api_service = MemoryAPIService(db) + + result = memory_api_service.list_memory_configs( + workspace_id=api_key_auth.workspace_id, + ) + + logger.info(f"Listed {result['total']} configs for workspace: {api_key_auth.workspace_id}") + return success(data=ListConfigsResponse(**result).model_dump(), msg="Configs listed successfully") diff --git a/api/app/core/memory/storage_services/search/__init__.py b/api/app/core/memory/storage_services/search/__init__.py index c12c39b0..49154e19 100644 --- a/api/app/core/memory/storage_services/search/__init__.py +++ b/api/app/core/memory/storage_services/search/__init__.py @@ -4,11 +4,6 @@ 本模块提供统一的搜索服务接口,支持关键词搜索、语义搜索和混合搜索。 """ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from app.schemas.memory_config_schema import MemoryConfig - from app.core.memory.storage_services.search.hybrid_search import HybridSearchStrategy from app.core.memory.storage_services.search.keyword_search import KeywordSearchStrategy from app.core.memory.storage_services.search.search_strategy import ( @@ -29,115 +24,87 @@ __all__ = [ # ============================================================================ -# 向后兼容的函数式API +# 向后兼容的函数式API (DEPRECATED - 未被使用) # ============================================================================ -# 为了兼容旧代码,提供与 src/search.py 相同的函数式接口 +# 所有调用方均直接使用 app.core.memory.src.search.run_hybrid_search +# 保留注释以备参考 - -async def run_hybrid_search( - query_text: str, - search_type: str = "hybrid", - end_user_id: str | None = None, - apply_id: str | None = None, - user_id: str | None = None, - limit: int = 50, - include: list[str] | None = None, - alpha: float = 0.6, - use_forgetting_curve: bool = False, - memory_config: "MemoryConfig" = None, - **kwargs -) -> dict: - """运行混合搜索(向后兼容的函数式API) - - 这是一个向后兼容的包装函数,将旧的函数式API转换为新的基于类的API。 - - Args: - query_text: 查询文本 - search_type: 搜索类型("hybrid", "keyword", "semantic") - end_user_id: 组ID过滤 - apply_id: 应用ID过滤 - user_id: 用户ID过滤 - limit: 每个类别的最大结果数 - include: 要包含的搜索类别列表 - alpha: BM25分数权重(0.0-1.0) - use_forgetting_curve: 是否使用遗忘曲线 - memory_config: MemoryConfig object containing embedding_model_id - **kwargs: 其他参数 - - Returns: - dict: 搜索结果字典,格式与旧API兼容 - """ - from app.core.memory.llm_tools.openai_embedder import OpenAIEmbedderClient - from app.core.models.base import RedBearModelConfig - from app.db import get_db_context - from app.repositories.neo4j.neo4j_connector import Neo4jConnector - from app.services.memory_config_service import MemoryConfigService - - if not memory_config: - raise ValueError("memory_config is required for search") - - # 初始化客户端 - connector = Neo4jConnector() - with get_db_context() as db: - config_service = MemoryConfigService(db) - embedder_config_dict = config_service.get_embedder_config(str(memory_config.embedding_model_id)) - embedder_config = RedBearModelConfig(**embedder_config_dict) - embedder_client = OpenAIEmbedderClient(embedder_config) - - try: - # 根据搜索类型选择策略 - if search_type == "keyword": - strategy = KeywordSearchStrategy(connector=connector) - elif search_type == "semantic": - strategy = SemanticSearchStrategy( - connector=connector, - embedder_client=embedder_client - ) - else: # hybrid - strategy = HybridSearchStrategy( - connector=connector, - embedder_client=embedder_client, - alpha=alpha, - use_forgetting_curve=use_forgetting_curve - ) - - # 执行搜索 - result = await strategy.search( - query_text=query_text, - end_user_id=end_user_id, - limit=limit, - include=include, - alpha=alpha, - use_forgetting_curve=use_forgetting_curve, - **kwargs - ) - - # 转换为旧格式 - result_dict = result.to_dict() - - # 保存到文件(如果指定了output_path) - output_path = kwargs.get('output_path', 'search_results.json') - if output_path: - import json - import os - from datetime import datetime - - try: - # 确保目录存在 - out_dir = os.path.dirname(output_path) - if out_dir: - os.makedirs(out_dir, exist_ok=True) - - # 保存结果 - with open(output_path, "w", encoding="utf-8") as f: - json.dump(result_dict, f, ensure_ascii=False, indent=2, default=str) - print(f"Search results saved to {output_path}") - except Exception as e: - print(f"Error saving search results: {e}") - return result_dict - - finally: - await connector.close() - - -__all__.append("run_hybrid_search") +# async def run_hybrid_search( +# query_text: str, +# search_type: str = "hybrid", +# end_user_id: str | None = None, +# apply_id: str | None = None, +# user_id: str | None = None, +# limit: int = 50, +# include: list[str] | None = None, +# alpha: float = 0.6, +# use_forgetting_curve: bool = False, +# memory_config: "MemoryConfig" = None, +# **kwargs +# ) -> dict: +# """运行混合搜索(向后兼容的函数式API)""" +# from app.core.memory.llm_tools.openai_embedder import OpenAIEmbedderClient +# from app.core.models.base import RedBearModelConfig +# from app.db import get_db_context +# from app.repositories.neo4j.neo4j_connector import Neo4jConnector +# from app.services.memory_config_service import MemoryConfigService +# +# if not memory_config: +# raise ValueError("memory_config is required for search") +# +# connector = Neo4jConnector() +# with get_db_context() as db: +# config_service = MemoryConfigService(db) +# embedder_config_dict = config_service.get_embedder_config(str(memory_config.embedding_model_id)) +# embedder_config = RedBearModelConfig(**embedder_config_dict) +# embedder_client = OpenAIEmbedderClient(embedder_config) +# +# try: +# if search_type == "keyword": +# strategy = KeywordSearchStrategy(connector=connector) +# elif search_type == "semantic": +# strategy = SemanticSearchStrategy( +# connector=connector, +# embedder_client=embedder_client +# ) +# else: +# strategy = HybridSearchStrategy( +# connector=connector, +# embedder_client=embedder_client, +# alpha=alpha, +# use_forgetting_curve=use_forgetting_curve +# ) +# +# result = await strategy.search( +# query_text=query_text, +# end_user_id=end_user_id, +# limit=limit, +# include=include, +# alpha=alpha, +# use_forgetting_curve=use_forgetting_curve, +# **kwargs +# ) +# +# result_dict = result.to_dict() +# +# output_path = kwargs.get('output_path', 'search_results.json') +# if output_path: +# import json +# import os +# from datetime import datetime +# +# try: +# out_dir = os.path.dirname(output_path) +# if out_dir: +# os.makedirs(out_dir, exist_ok=True) +# with open(output_path, "w", encoding="utf-8") as f: +# json.dump(result_dict, f, ensure_ascii=False, indent=2, default=str) +# print(f"Search results saved to {output_path}") +# except Exception as e: +# print(f"Error saving search results: {e}") +# return result_dict +# +# finally: +# await connector.close() +# +# __all__.append("run_hybrid_search") diff --git a/api/app/schemas/memory_api_schema.py b/api/app/schemas/memory_api_schema.py index ff62355f..2229c540 100644 --- a/api/app/schemas/memory_api_schema.py +++ b/api/app/schemas/memory_api_schema.py @@ -141,10 +141,12 @@ class CreateEndUserRequest(BaseModel): other_id: External user identifier (required) other_name: Display name for the end user memory_config_id: Optional memory config ID. If not provided, uses workspace default. + app_id: Optional app ID to bind the end user to. """ other_id: str = Field(..., description="External user identifier (required)") other_name: Optional[str] = Field("", description="Display name") memory_config_id: Optional[str] = Field(None, description="Memory config ID. Falls back to workspace default if not provided.") + app_id: Optional[str] = Field(None, description="App ID to bind the end user to") @field_validator("other_id") @classmethod diff --git a/api/app/services/memory_api_service.py b/api/app/services/memory_api_service.py index f62f526c..bea313fc 100644 --- a/api/app/services/memory_api_service.py +++ b/api/app/services/memory_api_service.py @@ -8,6 +8,8 @@ This service validates inputs and delegates to MemoryAgentService for core memor import uuid from typing import Any, Dict, Optional +from sqlalchemy.orm import Session + from app.core.error_codes import BizCode from app.core.exceptions import BusinessException, ResourceNotFoundException from app.core.logging_config import get_logger @@ -15,7 +17,6 @@ from app.models.app_model import App from app.models.end_user_model import EndUser from app.schemas.memory_config_schema import ConfigurationError from app.services.memory_agent_service import MemoryAgentService -from sqlalchemy.orm import Session logger = get_logger(__name__)