diff --git a/backend/agents/create_agent_info.py b/backend/agents/create_agent_info.py index 4fd2411a7..faed9ce79 100644 --- a/backend/agents/create_agent_info.py +++ b/backend/agents/create_agent_info.py @@ -19,6 +19,7 @@ from services.memory_config_service import build_memory_context from services.image_service import get_vlm_model from database.agent_db import search_agent_info_by_agent_id, query_sub_agents_id_list +from database.agent_version_db import query_current_version_no from database.tool_db import search_tools_for_sub_agent from database.model_management_db import get_model_records, get_model_by_model_id from database.client import minio_client @@ -75,15 +76,19 @@ async def create_agent_config( language: str = LANGUAGE["ZH"], last_user_query: str = None, allow_memory_search: bool = True, + version_no: int = 0, ): agent_info = search_agent_info_by_agent_id( - agent_id=agent_id, tenant_id=tenant_id) + agent_id=agent_id, tenant_id=tenant_id, version_no=version_no) # create sub agent sub_agent_id_list = query_sub_agents_id_list( - main_agent_id=agent_id, tenant_id=tenant_id) + main_agent_id=agent_id, tenant_id=tenant_id, version_no=version_no) managed_agents = [] for sub_agent_id in sub_agent_id_list: + # Get the current published version for this sub-agent (from draft version 0) + sub_agent_version_no = query_current_version_no( + agent_id=sub_agent_id, tenant_id=tenant_id) or 0 sub_agent_config = await create_agent_config( agent_id=sub_agent_id, tenant_id=tenant_id, @@ -91,10 +96,11 @@ async def create_agent_config( language=language, last_user_query=last_user_query, allow_memory_search=allow_memory_search, + version_no=sub_agent_version_no, ) managed_agents.append(sub_agent_config) - tool_list = await create_tool_config_list(agent_id, tenant_id, user_id) + tool_list = await create_tool_config_list(agent_id, tenant_id, user_id, version_no=version_no) # Build system prompt: prioritize segmented fields, fallback to original prompt field if not available duty_prompt = agent_info.get("duty_prompt", "") @@ -202,13 +208,13 @@ async def create_agent_config( return agent_config -async def create_tool_config_list(agent_id, tenant_id, user_id): +async def create_tool_config_list(agent_id, tenant_id, user_id, version_no: int = 0): # create tool tool_config_list = [] langchain_tools = await discover_langchain_tools() # now only admin can modify the agent, user_id is not used - tools_list = search_tools_for_sub_agent(agent_id, tenant_id) + tools_list = search_tools_for_sub_agent(agent_id, tenant_id, version_no=version_no) for tool in tools_list: param_dict = {} for param in tool.get("params", []): @@ -355,7 +361,21 @@ async def create_agent_run_info( user_id: str, language: str = "zh", allow_memory_search: bool = True, + is_debug: bool = False, ): + # Determine which version_no to use based on is_debug flag + # If is_debug=false, use the current published version (current_version_no) + # If is_debug=true, use version 0 (draft/editing state) + if is_debug: + version_no = 0 + else: + # Get current published version number + version_no = query_current_version_no(agent_id=agent_id, tenant_id=tenant_id) + # Fallback to 0 if no published version exists + if version_no is None: + version_no = 0 + logger.info(f"Agent {agent_id} has no published version, using draft version 0") + final_query = await join_minio_file_description_to_query(minio_files=minio_files, query=query) model_list = await create_model_config_list(tenant_id) agent_config = await create_agent_config( @@ -365,19 +385,45 @@ async def create_agent_run_info( language=language, last_user_query=final_query, allow_memory_search=allow_memory_search, + version_no=version_no, ) - remote_mcp_list = await get_remote_mcp_server_list(tenant_id=tenant_id) + remote_mcp_list = await get_remote_mcp_server_list(tenant_id=tenant_id, is_need_auth=True) default_mcp_url = urljoin(LOCAL_MCP_SERVER, "sse") remote_mcp_list.append({ "remote_mcp_server_name": "nexent", "remote_mcp_server": default_mcp_url, - "status": True + "status": True, + "authorization_token": None }) remote_mcp_dict = {record["remote_mcp_server_name"]: record for record in remote_mcp_list if record["status"]} - # Filter MCP servers and tools - mcp_host = filter_mcp_servers_and_tools(agent_config, remote_mcp_dict) + # Filter MCP servers and tools, and build mcp_host with authorization + used_mcp_urls = filter_mcp_servers_and_tools(agent_config, remote_mcp_dict) + + # Build mcp_host list with authorization tokens + mcp_host = [] + for url in used_mcp_urls: + # Find the MCP record for this URL + mcp_record = None + for record in remote_mcp_list: + if record.get("remote_mcp_server") == url and record.get("status"): + mcp_record = record + break + + if mcp_record: + mcp_config = { + "url": url, + "transport": "sse" if url.endswith("/sse") else "streamable-http" + } + # Add authorization if present + auth_token = mcp_record.get("authorization_token") + if auth_token: + mcp_config["authorization"] = auth_token + mcp_host.append(mcp_config) + else: + # Fallback to string format if record not found + mcp_host.append(url) agent_run_info = AgentRunInfo( query=final_query, diff --git a/backend/apps/agent_app.py b/backend/apps/agent_app.py index 7a9ce7d6d..a42d11b53 100644 --- a/backend/apps/agent_app.py +++ b/backend/apps/agent_app.py @@ -6,7 +6,7 @@ from fastapi.encoders import jsonable_encoder from starlette.responses import JSONResponse -from consts.model import AgentRequest, AgentInfoRequest, AgentIDRequest, ConversationResponse, AgentImportRequest, AgentNameBatchCheckRequest, AgentNameBatchRegenerateRequest, VersionPublishRequest, VersionListResponse, VersionDetailResponse, VersionRollbackRequest, VersionStatusRequest, CurrentVersionResponse, VersionCompareRequest +from consts.model import AgentRequest, AgentInfoRequest, AgentIDRequest, ConversationResponse, AgentImportRequest, AgentNameBatchCheckRequest, AgentNameBatchRegenerateRequest, VersionPublishRequest, VersionListResponse, VersionDetailResponse, VersionRollbackRequest, VersionStatusRequest, CurrentVersionResponse, VersionCompareRequest, VersionUpdateRequest from services.agent_service import ( get_agent_info_impl, get_creating_sub_agent_info_impl, @@ -29,6 +29,7 @@ get_version_detail_impl, rollback_version_impl, update_version_status_impl, + update_version_impl, delete_version_impl, get_current_version_impl, compare_versions_impl, @@ -443,6 +444,34 @@ async def update_version_status_api( raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Update version status error.") +@agent_config_router.put("/{agent_id}/versions/{version_no}") +async def update_version_api( + agent_id: int, + version_no: int, + request: VersionUpdateRequest, + authorization: str = Header(None), +): + """ + Update version metadata (version_name and release_note) + """ + try: + user_id, tenant_id = get_current_user_id(authorization) + result = update_version_impl( + agent_id=agent_id, + tenant_id=tenant_id, + user_id=user_id, + version_no=version_no, + version_name=request.version_name, + release_note=request.release_note, + ) + return JSONResponse(status_code=HTTPStatus.OK, content=result) + except ValueError as e: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e)) + except Exception as e: + logger.error(f"Update version error: {str(e)}") + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Update version error.") + + @agent_config_router.delete("/{agent_id}/versions/{version_no}") async def delete_version_api( agent_id: int, diff --git a/backend/apps/conversation_management_app.py b/backend/apps/conversation_management_app.py index e108fcb4c..9beeedf2e 100644 --- a/backend/apps/conversation_management_app.py +++ b/backend/apps/conversation_management_app.py @@ -175,12 +175,15 @@ async def generate_conversation_title_endpoint( authorization: Optional[str] = Header(None) ): """ - Generate conversation title + Generate conversation title from user question + + This endpoint generates title immediately after user sends a message, + using only the question content instead of waiting for full conversation. Args: request: GenerateTitleRequest object containing: - conversation_id: Conversation ID - - history: Conversation history list + - question: User's question content http_request: http request containing language info authorization: Authorization header @@ -190,7 +193,8 @@ async def generate_conversation_title_endpoint( try: user_id, tenant_id, language = get_current_user_info( authorization=authorization, request=http_request) - title = await generate_conversation_title_service(request.conversation_id, request.history, user_id, tenant_id=tenant_id, language=language) + title = await generate_conversation_title_service( + request.conversation_id, request.question, user_id, tenant_id=tenant_id, language=language) return ConversationResponse(code=0, message="success", data=title) except Exception as e: logging.error(f"Failed to generate conversation title: {str(e)}") diff --git a/backend/apps/mock_user_management_app.py b/backend/apps/mock_user_management_app.py index 8e9f6adc3..476b5c9f9 100644 --- a/backend/apps/mock_user_management_app.py +++ b/backend/apps/mock_user_management_app.py @@ -33,20 +33,16 @@ async def signup(request: UserSignUpRequest): Mock user registration endpoint """ try: - logger.info( - f"Mock signup request: email={request.email}, is_admin={request.is_admin}") + logger.info(f"Mock signup request: email={request.email}") # Mock success response matching user_management_app.py format - if request.is_admin: - success_message = "🎉 Admin account registered successfully! You now have system management permissions." - else: - success_message = "🎉 User account registered successfully! Please start experiencing the AI assistant service." + success_message = "🎉 User account registered successfully! Please start experiencing the AI assistant service." user_data = { "user": { "id": MOCK_USER["id"], "email": request.email, - "role": "admin" if request.is_admin else "user" + "role": "user" }, "session": { "access_token": MOCK_SESSION["access_token"], @@ -54,7 +50,7 @@ async def signup(request: UserSignUpRequest): "expires_at": int((datetime.now() + timedelta(days=3650)).timestamp()), "expires_in_seconds": MOCK_SESSION["expires_in_seconds"] }, - "registration_type": "admin" if request.is_admin else "user" + "registration_type": "user" } return JSONResponse(status_code=HTTPStatus.OK, diff --git a/backend/apps/remote_mcp_app.py b/backend/apps/remote_mcp_app.py index fe16fd9be..8b1ac6b33 100644 --- a/backend/apps/remote_mcp_app.py +++ b/backend/apps/remote_mcp_app.py @@ -17,6 +17,7 @@ upload_and_start_mcp_image, update_remote_mcp_server_list, attach_mcp_container_permissions, + get_mcp_record_by_id, ) from database.remote_mcp_db import check_mcp_name_exists from services.tool_configuration_service import get_tool_from_remote_mcp_server @@ -30,11 +31,18 @@ @router.post("/tools") async def get_tools_from_remote_mcp( service_name: str, - mcp_url: str + mcp_url: str, + authorization: Optional[str] = Header(None), + http_request: Request = None ): """ Used to list tool information from the remote MCP server """ try: - tools_info = await get_tool_from_remote_mcp_server(mcp_server_name=service_name, remote_mcp_server=mcp_url) + user_id, tenant_id, _ = get_current_user_info(authorization, http_request) + tools_info = await get_tool_from_remote_mcp_server( + mcp_server_name=service_name, + remote_mcp_server=mcp_url, + tenant_id=tenant_id + ) return JSONResponse( status_code=HTTPStatus.OK, content={ @@ -54,6 +62,8 @@ async def get_tools_from_remote_mcp( async def add_remote_proxies( mcp_url: str, service_name: str, + authorization_token: Optional[str] = Query( + None, description="Authorization token for MCP server authentication (e.g., Bearer token)"), tenant_id: Optional[str] = Query( None, description="Tenant ID for filtering (uses auth if not provided)"), authorization: Optional[str] = Header(None), @@ -68,7 +78,8 @@ async def add_remote_proxies( user_id=user_id, remote_mcp_server=mcp_url, remote_mcp_server_name=service_name, - container_id=None) + container_id=None, + authorization_token=authorization_token) return JSONResponse( status_code=HTTPStatus.OK, content={"message": "Successfully added remote MCP proxy", @@ -170,6 +181,7 @@ async def get_remote_proxies( remote_mcp_server_list = await get_remote_mcp_server_list( tenant_id=effective_tenant_id, user_id=user_id, + is_need_auth=False ) return JSONResponse( status_code=HTTPStatus.OK, @@ -183,6 +195,50 @@ async def get_remote_proxies( detail="Failed to get remote MCP proxy") +@router.get("/record/{mcp_id}") +async def get_mcp_record( + mcp_id: int, + tenant_id: Optional[str] = Query( + None, description="Tenant ID for filtering (uses auth if not provided)"), + authorization: Optional[str] = Header(None), + http_request: Request = None +): + """ Get single MCP record by ID """ + try: + user_id, auth_tenant_id, _ = get_current_user_info(authorization, http_request) + # Use explicit tenant_id if provided, otherwise fall back to auth tenant_id + effective_tenant_id = tenant_id or auth_tenant_id + + mcp_record = await get_mcp_record_by_id( + mcp_id=mcp_id, + tenant_id=effective_tenant_id + ) + + if not mcp_record: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="MCP record not found" + ) + + return JSONResponse( + status_code=HTTPStatus.OK, + content={ + "mcp_name": mcp_record.get("mcp_name"), + "mcp_server": mcp_record.get("mcp_server"), + "authorization_token": mcp_record.get("authorization_token"), + "status": "success" + } + ) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get MCP record: {e}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Failed to get MCP record" + ) + + @router.get("/healthcheck") async def check_mcp_health( mcp_url: str, diff --git a/backend/apps/tenant_app.py b/backend/apps/tenant_app.py index 07215b5b2..e0d612902 100644 --- a/backend/apps/tenant_app.py +++ b/backend/apps/tenant_app.py @@ -4,13 +4,23 @@ import logging from typing import Optional -from fastapi import APIRouter, HTTPException, Header +from fastapi import APIRouter, HTTPException, Header, Body from http import HTTPStatus from starlette.responses import JSONResponse -from consts.model import TenantCreateRequest, TenantUpdateRequest +from consts.model import ( + PaginationRequest, + TenantCreateRequest, + TenantUpdateRequest, +) from consts.exceptions import NotFoundException, ValidationError, UnauthorizedError -from services.tenant_service import create_tenant, get_tenant_info, get_all_tenants, update_tenant_info +from services.tenant_service import ( + create_tenant, + get_tenant_info, + get_tenants_paginated, + update_tenant_info, + delete_tenant, +) from utils.auth_utils import get_current_user_id logger = logging.getLogger(__name__) @@ -109,23 +119,32 @@ async def get_tenant_endpoint(tenant_id: str) -> JSONResponse: ) -@router.get("") -async def get_all_tenants_endpoint() -> JSONResponse: +@router.post("/tenant-list") +async def get_all_tenants_endpoint( + pagination: PaginationRequest = Body(...) +) -> JSONResponse: """ - Get all tenants + Get all tenants with pagination support + + Args: + pagination: Pagination parameters (page, page_size) Returns: - JSONResponse: List of all tenants + JSONResponse: Paginated list of tenants with total count """ try: - # Get all tenants - tenants = get_all_tenants() + # Get paginated tenants + result = get_tenants_paginated(page=pagination.page, page_size=pagination.page_size) return JSONResponse( status_code=HTTPStatus.OK, content={ "message": "Tenants retrieved successfully", - "data": tenants + "data": result["data"], + "total": result["total"], + "page": result["page"], + "page_size": result["page_size"], + "total_pages": result["total_pages"] } ) @@ -207,7 +226,17 @@ async def delete_tenant_endpoint( authorization: Optional[str] = Header(None) ) -> JSONResponse: """ - Delete tenant (placeholder - not yet implemented) + Delete tenant and all associated resources + + This will: + - Delete all users in the tenant + - Delete all groups in the tenant + - Delete all models in the tenant + - Delete all knowledge bases in the tenant + - Delete all agents in the tenant + - Delete all MCP configurations in the tenant + - Delete all invitation codes in the tenant + - Delete all tenant configurations Args: tenant_id: Tenant identifier @@ -220,14 +249,29 @@ async def delete_tenant_endpoint( # Get current user ID from token user_id, _ = get_current_user_id(authorization) - # Note: Delete functionality is not yet implemented in the service layer - # This will raise ValidationError as per current implementation - raise ValidationError("Tenant deletion is not yet implemented due to complex dependencies") + # Perform tenant deletion with all associated resources + await delete_tenant(tenant_id, deleted_by=user_id) + + logger.info(f"Deleted tenant {tenant_id} and all associated resources by user {user_id}") + + return JSONResponse( + status_code=HTTPStatus.OK, + content={ + "message": "Tenant deleted successfully", + "data": {"tenant_id": tenant_id} + } + ) + except NotFoundException as exc: + logger.warning(f"Tenant not found for deletion: {tenant_id}") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=str(exc) + ) except ValidationError as exc: - logger.warning(f"Tenant deletion not supported: {str(exc)}") + logger.warning(f"Tenant deletion validation error: {str(exc)}") raise HTTPException( - status_code=HTTPStatus.NOT_IMPLEMENTED, + status_code=HTTPStatus.BAD_REQUEST, detail=str(exc) ) except UnauthorizedError as exc: diff --git a/backend/apps/user_management_app.py b/backend/apps/user_management_app.py index 1ebf0bace..b0231677d 100644 --- a/backend/apps/user_management_app.py +++ b/backend/apps/user_management_app.py @@ -10,7 +10,7 @@ from consts.model import UserSignInRequest, UserSignUpRequest from consts.exceptions import NoInviteCodeException, IncorrectInviteCodeException, UserRegistrationException from services.user_management_service import get_authorized_client, validate_token, \ - check_auth_service_health, signup_user, signup_user_with_invitation, signin_user, refresh_user_token, \ + check_auth_service_health, signup_user_with_invitation, signin_user, refresh_user_token, \ get_session_by_authorization, get_user_info from services.user_service import delete_user_and_cleanup from consts.exceptions import UnauthorizedError @@ -42,19 +42,10 @@ async def service_health(): async def signup(request: UserSignUpRequest): """User registration""" try: - if request.with_new_invitation: - user_data = await signup_user_with_invitation(email=request.email, - password=request.password, - invite_code=request.invite_code) - else: - user_data = await signup_user(email=request.email, - password=request.password, - is_admin=request.is_admin, - invite_code=request.invite_code) - if request.is_admin: - success_message = "🎉 Admin account registered successfully! You now have system management permissions." - else: - success_message = "🎉 User account registered successfully! Please start experiencing the AI assistant service." + user_data = await signup_user_with_invitation(email=request.email, + password=request.password, + invite_code=request.invite_code) + success_message = "🎉 User account registered successfully! Please start experiencing the AI assistant service." return JSONResponse(status_code=HTTPStatus.OK, content={"message":success_message, "data":user_data}) except NoInviteCodeException as e: diff --git a/backend/consts/const.py b/backend/consts/const.py index 96c937ef0..f226d7443 100644 --- a/backend/consts/const.py +++ b/backend/consts/const.py @@ -303,4 +303,4 @@ class VectorDatabaseType(str, Enum): MODEL_ENGINE_ENABLED = os.getenv("MODEL_ENGINE_ENABLED") # APP Version -APP_VERSION = "v1.8.0" +APP_VERSION = "v1.8.0.1" diff --git a/backend/consts/model.py b/backend/consts/model.py index a4862cd59..358e6ee3f 100644 --- a/backend/consts/model.py +++ b/backend/consts/model.py @@ -30,9 +30,7 @@ class UserSignUpRequest(BaseModel): """User registration request model""" email: EmailStr password: str = Field(..., min_length=6) - is_admin: Optional[bool] = False invite_code: Optional[str] = None - with_new_invitation: Optional[bool] = False class UserSignInRequest(BaseModel): @@ -255,7 +253,7 @@ class GeneratePromptRequest(BaseModel): class GenerateTitleRequest(BaseModel): conversation_id: int - history: List[Dict[str, str]] + question: str # used in agent/search agent/update for save agent info @@ -491,6 +489,8 @@ class MCPUpdateRequest(BaseModel): current_mcp_url: str = Field(..., description="Current MCP server URL") new_service_name: str = Field(..., description="New MCP service name") new_mcp_url: str = Field(..., description="New MCP server URL") + new_authorization_token: Optional[str] = Field( + None, description="New authorization token for MCP server authentication (e.g., Bearer token)") # Tenant Management Data Models @@ -507,12 +507,11 @@ class TenantUpdateRequest(BaseModel): description="New tenant display name") -class TenantResponse(BaseModel): - """Response model for tenant information""" - tenant_id: str = Field(..., description="Tenant identifier") - tenant_name: str = Field(..., description="Tenant display name") - default_group_id: Optional[int] = Field( - None, description="Default group ID for the tenant") +# Pagination request model +class PaginationRequest(BaseModel): + """Request model for pagination parameters""" + page: int = Field(1, ge=1, description="Page number") + page_size: int = Field(20, ge=1, le=100, description="Items per page") # Group Management Data Models @@ -782,6 +781,12 @@ class VersionStatusRequest(BaseModel): status: str = Field(..., description="New status: DISABLED / ARCHIVED") +class VersionUpdateRequest(BaseModel): + """Request model for updating version metadata (name and description)""" + version_name: Optional[str] = Field(None, description="User-defined version name for display") + release_note: Optional[str] = Field(None, description="Release notes / version description") + + class VersionCompareRequest(BaseModel): """Request model for comparing two versions""" version_no_a: int = Field(..., description="First version number for comparison") diff --git a/backend/database/agent_version_db.py b/backend/database/agent_version_db.py index 1f36383f8..b2877bdb1 100644 --- a/backend/database/agent_version_db.py +++ b/backend/database/agent_version_db.py @@ -173,6 +173,46 @@ def update_version_status( return result.rowcount +def update_version( + agent_id: int, + tenant_id: str, + version_no: int, + version_name: Optional[str] = None, + release_note: Optional[str] = None, + updated_by: Optional[str] = None, +) -> int: + """ + Update version metadata (version_name and release_note) + Returns: number of rows affected + """ + # Build update values dynamically + update_values = {} + if version_name is not None: + update_values["version_name"] = version_name + if release_note is not None: + update_values["release_note"] = release_note + if updated_by is not None: + update_values["updated_by"] = updated_by + + if not update_values: + return 0 + + update_values["update_time"] = func.now() + + with get_db_session() as session: + result = session.execute( + update(AgentVersion) + .where( + AgentVersion.agent_id == agent_id, + AgentVersion.tenant_id == tenant_id, + AgentVersion.version_no == version_no, + AgentVersion.delete_flag == 'N', + ) + .values(**update_values) + ) + return result.rowcount + + def update_agent_current_version( agent_id: int, tenant_id: str, diff --git a/backend/database/db_models.py b/backend/database/db_models.py index 8649b6ae8..a3ecb7120 100644 --- a/backend/database/db_models.py +++ b/backend/database/db_models.py @@ -243,7 +243,7 @@ class ToolInstance(TableBase): user_id = Column(String(100), doc="User ID") tenant_id = Column(String(100), doc="Tenant ID") enabled = Column(Boolean, doc="Enabled") - version_no = Column(Integer, default=0, nullable=False, doc="Version number. 0 = draft/editing state, >=1 = published snapshot") + version_no = Column(Integer, default=0, primary_key=True, nullable=False, doc="Version number. 0 = draft/editing state, >=1 = published snapshot") class KnowledgeRecord(TableBase): @@ -322,6 +322,11 @@ class McpRecord(TableBase): String(200), doc="Docker container ID for MCP service, None for non-containerized MCP", ) + authorization_token = Column( + String(500), + doc="Authorization token for MCP server authentication (e.g., Bearer token)", + default=None, + ) class UserTenant(TableBase): diff --git a/backend/database/remote_mcp_db.py b/backend/database/remote_mcp_db.py index 50505ad90..d535f9fba 100644 --- a/backend/database/remote_mcp_db.py +++ b/backend/database/remote_mcp_db.py @@ -114,6 +114,26 @@ def get_mcp_server_by_name_and_tenant(mcp_name: str, tenant_id: str) -> str: return mcp_record.mcp_server if mcp_record else "" +def get_mcp_authorization_token_by_name_and_url(mcp_name: str, mcp_server: str, tenant_id: str) -> str | None: + """ + Get MCP authorization token by name, URL and tenant ID + + :param mcp_name: MCP name + :param mcp_server: MCP server URL + :param tenant_id: Tenant ID + :return: Authorization token, None if not found + """ + with get_db_session() as session: + mcp_record = session.query(McpRecord).filter( + McpRecord.mcp_name == mcp_name, + McpRecord.mcp_server == mcp_server, + McpRecord.tenant_id == tenant_id, + McpRecord.delete_flag != 'Y' + ).first() + + return mcp_record.authorization_token if mcp_record else None + + def update_mcp_record_by_name_and_url( update_data, tenant_id: str, @@ -137,6 +157,10 @@ def update_mcp_record_by_name_and_url( if status is not None: update_fields["status"] = status + # Update authorization_token if provided + if hasattr(update_data, 'new_authorization_token'): + update_fields["authorization_token"] = update_data.new_authorization_token + with get_db_session() as session: session.query(McpRecord).filter( McpRecord.mcp_name == update_data.current_service_name, @@ -161,3 +185,21 @@ def check_mcp_name_exists(mcp_name: str, tenant_id: str) -> bool: McpRecord.delete_flag != 'Y' ).first() return mcp_record is not None + + +def get_mcp_record_by_id_and_tenant(mcp_id: int, tenant_id: str) -> Dict[str, Any] | None: + """ + Get MCP record by ID and tenant ID + + :param mcp_id: MCP record ID + :param tenant_id: Tenant ID + :return: MCP record as dictionary, or None if not found + """ + with get_db_session() as session: + mcp_record = session.query(McpRecord).filter( + McpRecord.mcp_id == mcp_id, + McpRecord.tenant_id == tenant_id, + McpRecord.delete_flag != 'Y' + ).first() + + return as_dict(mcp_record) if mcp_record else None diff --git a/backend/database/user_tenant_db.py b/backend/database/user_tenant_db.py index 217987d52..92a105280 100644 --- a/backend/database/user_tenant_db.py +++ b/backend/database/user_tenant_db.py @@ -1,12 +1,15 @@ """ Database operations for user tenant relationship management """ +import logging from typing import Any, List, Dict, Optional from consts.const import DEFAULT_TENANT_ID from database.client import as_dict, get_db_session from database.db_models import UserTenant +logger = logging.getLogger(__name__) + def get_user_tenant_by_user_id(user_id: str) -> Optional[Dict[str, Any]]: """ @@ -164,3 +167,28 @@ def soft_delete_user_tenant_by_user_id(user_id: str, deleted_by: str) -> bool: }) return result > 0 + + +def soft_delete_users_by_tenant_id(tenant_id: str, deleted_by: str) -> bool: + """ + Soft delete all user tenant relationships for a tenant + + Args: + tenant_id (str): Tenant ID to delete all users from + deleted_by (str): User who performed the deletion + + Returns: + bool: True if any records were deleted + """ + with get_db_session() as session: + result = session.query(UserTenant).filter( + UserTenant.tenant_id == tenant_id, + UserTenant.delete_flag == "N" + ).update({ + "delete_flag": "Y", + "updated_by": deleted_by, + "update_time": "NOW()" + }) + + logger.info(f"Soft deleted {result} user-tenant relationships for tenant {tenant_id}") + return result > 0 \ No newline at end of file diff --git a/backend/prompts/analyze_file_en.yaml b/backend/prompts/analyze_file_en.yaml deleted file mode 100644 index 7a513f475..000000000 --- a/backend/prompts/analyze_file_en.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# File Analysis Prompt Templates -# For image and long text content analysis - -image_analysis: - system_prompt: |- - The user has asked a question: {{ query }}. Please provide a concise and careful description of this image from the perspective of answering this question, within 200 words. - - **Image Analysis Requirements:** - 1. Focus on image content relevant to the user's question - 2. Keep descriptions concise and clear, highlighting key information - 3. Avoid irrelevant details, focus on content that helps answer the question - 4. Maintain objective description, avoid over-interpretation - - user_prompt: | - Please carefully observe this image and describe it from the perspective of answering the user's question. - -long_text_analysis: - system_prompt: |- - The user has asked a question: {{ query }}. Please provide a concise and careful description of this text from the perspective of answering this question, within 200 words. - - **Text Analysis Requirements:** - 1. Focus on extracting text content relevant to the user's question - 2. Summarize accurately and concisely, highlighting core information - 3. Preserve key viewpoints and data from the original text - 4. Avoid redundant information, focus on question-relevant content - - user_prompt: | - Please carefully read and analyze this text: \ No newline at end of file diff --git a/backend/prompts/analyze_file_zh.yaml b/backend/prompts/analyze_file_zh.yaml deleted file mode 100644 index 5e6abc165..000000000 --- a/backend/prompts/analyze_file_zh.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# 文件分析 Prompt 模板 -# 用于图片和长文本内容分析 - -image_analysis: - system_prompt: |- - 用户提出了一个问题:{{ query }},请从回答这个问题的角度精简、仔细描述一下这个图片,200字以内。 - - **图片分析要求:** - 1. 重点关注与用户问题相关的图片内容 - 2. 描述要精简明了,突出关键信息 - 3. 避免无关细节,专注于能帮助回答问题的内容 - 4. 保持客观描述,不要过度解读 - - user_prompt: | - 请仔细观察这张图片,并从回答用户问题的角度进行描述。 - -long_text_analysis: - system_prompt: |- - 用户提出了一个问题:{{ query }},请从回答这个问题的角度精简、仔细描述一下这段文本,200字以内。 - - **文本分析要求:** - 1. 重点提取与用户问题相关的文本内容 - 2. 归纳总结要准确简洁,突出核心信息 - 3. 保持原文的关键观点和数据 - 4. 避免冗余信息,专注于问题相关内容 - - user_prompt: | - 请仔细阅读并分析这段文本: \ No newline at end of file diff --git a/backend/prompts/cluster_summary_agent_en.yaml b/backend/prompts/cluster_summary_agent_en.yaml deleted file mode 100644 index ed614ed0b..000000000 --- a/backend/prompts/cluster_summary_agent_en.yaml +++ /dev/null @@ -1,24 +0,0 @@ -system_prompt: |- - You are a professional knowledge summarization assistant. Your task is to generate a concise summary of a document cluster based on multiple documents. - - **Summary Requirements:** - 1. The input contains multiple documents (each document has title and content snippets) - 2. You need to extract the common themes and key topics from these documents - 3. Generate a summary that represents the collective content of the cluster - 4. The summary should be accurate, coherent, and written in natural language - 5. Keep the summary within the specified word limit - - **Guidelines:** - - Focus on identifying shared themes and topics across documents - - Highlight key concepts, domains, or subject matter - - Use clear and concise language - - Avoid listing individual document titles unless necessary - - The summary should help users understand what this group of documents covers - -user_prompt: | - Please generate a concise summary of the following document cluster: - - {{ cluster_content }} - - Summary ({{ max_words }} words): - diff --git a/backend/prompts/cluster_summary_agent_zh.yaml b/backend/prompts/cluster_summary_agent_zh.yaml deleted file mode 100644 index 6c643a789..000000000 --- a/backend/prompts/cluster_summary_agent_zh.yaml +++ /dev/null @@ -1,24 +0,0 @@ -system_prompt: |- - 你是一个专业的知识总结助手。你的任务是根据多个文档生成一个简洁的总结。 - - **总结要求:** - 1. 输入包含多个文档(每个文档有标题和内容片段) - 2. 你需要提取这些文档的共同主题和关键话题 - 3. 生成一个总结,代表这组文档的集体内容 - 4. 总结应准确、连贯且自然语言 - 5. 保持在指定的字数限制内 - - **指导原则:** - - 专注于识别共同主题和话题 - - 突出关键概念、领域或主题 - - 使用清晰简洁的语言 - - 除非必要,避免列出单个文档标题 - - 总结应帮助用户理解这组文档涵盖的内容 - -user_prompt: | - 请生成以下文档簇的简洁总结: - - {{ cluster_content }} - - 总结({{ max_words }}字): - diff --git a/backend/prompts/knowledge_summary_agent_en.yaml b/backend/prompts/knowledge_summary_agent_en.yaml deleted file mode 100644 index 27aba0407..000000000 --- a/backend/prompts/knowledge_summary_agent_en.yaml +++ /dev/null @@ -1,17 +0,0 @@ -system_prompt: |- - Please generate an accurate English knowledge base summary (no more than 150 words) for the following content, describing the core themes or key information of this knowledge base. - - Should use fluent and natural English expression, avoiding rigid keyword stacking, ensuring smooth and comprehensible language. - - **Knowledge Base Summary Generation Requirements:** - 1. The given content consists of high-frequency keywords from this knowledge base; - 2. These keywords are sorted by frequency from high to low; - 3. You need to generate a summary of the knowledge base content within 150 words based on these keywords; - 4. This summary is used to explain what type of materials this knowledge base mainly contains; - 5. Must output in English with natural and fluent language expression. - - Please output the generated summary directly, no additional explanation needed. - -user_prompt: | - Please generate a concise English knowledge base summary based on the following keyword content, no more than 150 words, with fluent and natural language: - Keyword content: {{ content }} \ No newline at end of file diff --git a/backend/prompts/knowledge_summary_agent_zh.yaml b/backend/prompts/knowledge_summary_agent_zh.yaml deleted file mode 100644 index 9bc0f4788..000000000 --- a/backend/prompts/knowledge_summary_agent_zh.yaml +++ /dev/null @@ -1,17 +0,0 @@ -system_prompt: |- - 请为以下内容生成一个准确的中文知识库总结(不超过150字),要求描述出该知识库内容的核心主题或关键信息。 - - 应使用流畅自然的中文表达,避免生硬的关键词堆砌,确保语言通顺易懂。 - - **知识库总结生成要求:** - 1. 所给的内容都是该知识库中高频出现的关键词; - 2. 这些关键词按照从大到小的频率排序; - 3. 你需要根据这些关键词生成一段150字以内的中文知识库内容总结; - 4. 该总结用于说明该知识库主要包含什么类型的语料; - 5. 必须使用中文输出,语言表达要自然流畅。 - - 请直接输出生成的总结,无需额外解释。 - -user_prompt: | - 请根据以下关键词内容生成一个简洁的中文知识库总结,不超过150个字,要求语言流畅自然: - 关键词内容:{{ content }} \ No newline at end of file diff --git a/backend/prompts/utils/generate_title_en.yaml b/backend/prompts/utils/generate_title_en.yaml index 12f4a7a74..f1825f71d 100644 --- a/backend/prompts/utils/generate_title_en.yaml +++ b/backend/prompts/utils/generate_title_en.yaml @@ -1,22 +1,22 @@ SYSTEM_PROMPT: |- - Please generate a concise and accurate title (no more than 12 characters) for the following conversation, highlighting the core theme or key information. The title should be natural and fluent, avoiding forced keyword stacking. + Please generate a concise and accurate title (no more than 12 characters) for the following user question, highlighting the core theme or key information. The title should be natural and fluent, avoiding forced keyword stacking. **Title Generation Requirements:** - 1. If the conversation involves specific questions, the title should summarize the essence of the question (e.g., 'How to solve XX issue?'); - 2. If the conversation is about knowledge sharing, the title should highlight the core knowledge point (e.g., 'Key Steps of Photosynthesis'); - 3. Avoid using generic terms like 'Q&A' or 'Consultation', prioritize domain specificity; - 4. The title language should match the conversation language. + 1. The title should summarize the essence of the question (e.g., 'How to solve XX issue?'); + 2. Avoid using generic terms like 'Q&A' or 'Consultation', prioritize domain specificity; + 3. The title language should match the question language; + 4. If the question is very short or casual, use the question itself as the title. **Examples:** - - Conversation: User asking about multiple methods for Python list deduplication → Title: Python List Deduplication Techniques - - Conversation: Discussion about factors affecting new energy vehicle battery life → Title: Factors Affecting EV Battery Life - - Conversation: hello → Title: hello + - Question: What are the common methods for Python list deduplication? → Title: Python List Deduplication Techniques + - Question: What are the factors affecting the battery life of new energy vehicles? → Title: Factors Affecting EV Battery Life + - Question: hello → Title: hello Please output only the generated title without additional explanation. USER_PROMPT: |- - Please generate a concise title (no more than 12 characters) based on the following conversation: - {{ content }} - + Please generate a concise title (no more than 12 characters) based on the following user question: + {{ question }} + Title: \ No newline at end of file diff --git a/backend/prompts/utils/generate_title_zh.yaml b/backend/prompts/utils/generate_title_zh.yaml index 8aa251bd1..cd394ae03 100644 --- a/backend/prompts/utils/generate_title_zh.yaml +++ b/backend/prompts/utils/generate_title_zh.yaml @@ -1,22 +1,22 @@ SYSTEM_PROMPT: |- - 请为以下对话生成一个简洁准确的标题(不超过12个字符),突出核心主题或关键信息。标题应该自然流畅,避免强制堆砌关键词。 + 请为以下用户问题生成一个简洁准确的标题(不超过12个字符),突出核心主题或关键信息。标题应该自然流畅,避免强制堆砌关键词。 **标题生成要求:** - 1. 如果对话涉及具体问题,标题应该概括问题的本质(例如:'如何解决XX问题?'); - 2. 如果对话是关于知识分享,标题应该突出核心知识点(例如:'光合作用关键步骤'); - 3. 避免使用'问答'或'咨询'等通用术语,优先考虑领域特异性; - 4. 标题语言应与对话语言保持一致。 + 1. 标题应该概括问题的本质(例如:'如何解决XX问题?'); + 2. 避免使用'问答'或'咨询'等通用术语,优先考虑领域特异性; + 3. 标题语言应与问题语言保持一致; + 4. 如果问题很短或很随意,直接使用问题本身作为标题。 **示例:** - - 对话:用户询问Python列表去重的多种方法 → 标题:Python列表去重技巧 - - 对话:讨论影响新能源汽车电池寿命的因素 → 标题:影响电动车电池寿命的因素 - - 对话:hello → 标题:hello + - 问题:Python列表去重有哪些常用的方法 → 标题:Python列表去重技巧 + - 问题:影响新能源汽车电池寿命的因素有哪些? → 标题:影响电动车电池寿命的因素 + - 问题:你好 → 标题:你好 请只输出生成的标题,不要添加额外解释。 USER_PROMPT: |- - 请根据以下对话生成一个简洁的标题(不超过12个字符): - {{ content }} - + 请根据以下用户问题生成一个简洁的标题(不超过12个字符): + {{ question }} + 标题: diff --git a/backend/services/agent_service.py b/backend/services/agent_service.py index 8acdd8b28..4d15a3a9c 100644 --- a/backend/services/agent_service.py +++ b/backend/services/agent_service.py @@ -1325,7 +1325,7 @@ async def list_all_agent_info_impl(tenant_id: str, user_id: str) -> list[dict]: # Apply visibility filter for DEV/USER based on group overlap if not can_edit_all: agent_group_ids = set(convert_string_to_list(agent.get("group_ids"))) - if len(user_group_ids.intersection(agent_group_ids)) == 0: + if len(user_group_ids.intersection(agent_group_ids)) == 0 and user_id != agent.get("created_by"): continue # Use shared availability check function @@ -1569,6 +1569,7 @@ async def prepare_agent_run( user_id=user_id, language=language, allow_memory_search=allow_memory_search, + is_debug=agent_request.is_debug, ) agent_run_manager.register_agent_run( agent_request.conversation_id, agent_run_info, user_id) diff --git a/backend/services/agent_version_service.py b/backend/services/agent_version_service.py index 6e013307c..062a0402d 100644 --- a/backend/services/agent_version_service.py +++ b/backend/services/agent_version_service.py @@ -12,6 +12,7 @@ query_agent_draft, insert_version, update_version_status, + update_version, update_agent_current_version, insert_agent_snapshot, insert_tool_snapshot, @@ -336,6 +337,40 @@ def update_version_status_impl( return {"message": "Status updated successfully"} +def update_version_impl( + agent_id: int, + tenant_id: str, + user_id: str, + version_no: int, + version_name: Optional[str] = None, + release_note: Optional[str] = None, +) -> dict: + """ + Update version metadata (version_name and release_note) + """ + # Check if version exists + version = search_version_by_version_no(agent_id, tenant_id, version_no) + if not version: + raise ValueError(f"Version {version_no} not found") + + rows_affected = update_version( + agent_id=agent_id, + tenant_id=tenant_id, + version_no=version_no, + version_name=version_name, + release_note=release_note, + updated_by=user_id, + ) + + if rows_affected == 0: + raise ValueError("No changes to update") + + return { + "message": "Version updated successfully", + "version_no": version_no, + } + + def delete_version_impl( agent_id: int, tenant_id: str, diff --git a/backend/services/conversation_management_service.py b/backend/services/conversation_management_service.py index e84257e87..b98e79897 100644 --- a/backend/services/conversation_management_service.py +++ b/backend/services/conversation_management_service.py @@ -226,31 +226,12 @@ def save_conversation_assistant(request: AgentRequest, messages: List[str], user save_message(conversation_req, user_id=user_id, tenant_id=tenant_id) -def extract_user_messages(history: List[Dict[str, str]]) -> str: +def call_llm_for_title(question: str, tenant_id: str, language: str = LANGUAGE["ZH"]) -> str: """ - Extract user message content from conversation history + Call LLM to generate a title from a user question Args: - history: List of conversation history records - - Returns: - str: Concatenated user message content - """ - content = "" - for message in history: - if message.get("role") == MESSAGE_ROLE["USER"] and message.get("content"): - content += f"\n### User Question:\n{message['content']}\n" - if message.get("role") == MESSAGE_ROLE["ASSISTANT"] and message.get("content"): - content += f"\n### Response Content:\n{message['content']}\n" - return content - - -def call_llm_for_title(content: str, tenant_id: str, language: str = LANGUAGE["ZH"]) -> str: - """ - Call LLM to generate a title - - Args: - content: Conversation content + question: User's question content tenant_id: Tenant ID language: Language code ('zh' for Chinese, 'en' for English) @@ -273,16 +254,16 @@ def call_llm_for_title(content: str, tenant_id: str, language: str = LANGUAGE["Z ssl_verify=model_config.get("ssl_verify", True) ) - # Build messages + # Build messages - use new template variable 'question' instead of 'content' user_prompt = Template(prompt_template["USER_PROMPT"], undefined=StrictUndefined).render({ - "content": content + "question": question }) messages = [{"role": MESSAGE_ROLE["SYSTEM"], "content": prompt_template["SYSTEM_PROMPT"]}, {"role": MESSAGE_ROLE["USER"], "content": user_prompt}] - # ModelEngine 只接受 role/content 的简单结构,确保提前扁平化 + # ModelEngine accepts role/content in a simple structure, ensure flattening before passing if model_config.get("model_factory", "").lower() == "modelengine": messages = [{"role": msg["role"], "content": str(msg.get("content", ""))} for msg in messages] @@ -649,13 +630,16 @@ def get_sources_service(conversation_id: Optional[int], message_id: Optional[int } -async def generate_conversation_title_service(conversation_id: int, history: List[Dict[str, str]], user_id: str, tenant_id: str, language: str = LANGUAGE["ZH"]) -> str: +async def generate_conversation_title_service(conversation_id: int, question: str, user_id: str, tenant_id: str, language: str = LANGUAGE["ZH"]) -> str: """ - Generate conversation title + Generate conversation title from user question + + This function is called immediately after user sends a message, + generating title from the question instead of waiting for full conversation. Args: conversation_id: Conversation ID - history: Conversation history list + question: User's question content user_id: User ID tenant_id: Tenant ID language: Language code ('zh' for Chinese, 'en' for English) @@ -664,11 +648,8 @@ async def generate_conversation_title_service(conversation_id: int, history: Lis str: Generated title """ try: - # Extract user messages - content = extract_user_messages(history) - - # Call LLM to generate title in a separate thread to avoid blocking - title = await asyncio.to_thread(call_llm_for_title, content, tenant_id, language) + # Call LLM to generate title from question in a separate thread to avoid blocking + title = await asyncio.to_thread(call_llm_for_title, question, tenant_id, language) # Update conversation title update_conversation_title(conversation_id, title, user_id) diff --git a/backend/services/image_service.py b/backend/services/image_service.py index 5c5f85f9b..8decbd541 100644 --- a/backend/services/image_service.py +++ b/backend/services/image_service.py @@ -29,19 +29,22 @@ async def proxy_image_impl(decoded_url: str): result = await response.json() return result + def get_vlm_model(tenant_id: str): # Get the tenant config vlm_model_config = tenant_config_manager.get_model_config( key=MODEL_CONFIG_MAPPING["vlm"], tenant_id=tenant_id) + if not vlm_model_config: + return None return OpenAIVLModel( - observer=MessageObserver(), - model_id=get_model_name_from_config( - vlm_model_config) if vlm_model_config else "", - api_base=vlm_model_config.get("base_url", ""), - api_key=vlm_model_config.get("api_key", ""), - temperature=0.7, - top_p=0.7, - frequency_penalty=0.5, - max_tokens=512, - ssl_verify=vlm_model_config.get("ssl_verify", True), - ) + observer=MessageObserver(), + model_id=get_model_name_from_config( + vlm_model_config) if vlm_model_config else "", + api_base=vlm_model_config.get("base_url", ""), + api_key=vlm_model_config.get("api_key", ""), + temperature=0.7, + top_p=0.7, + frequency_penalty=0.5, + max_tokens=512, + ssl_verify=vlm_model_config.get("ssl_verify", True), + ) diff --git a/backend/services/mcp_container_service.py b/backend/services/mcp_container_service.py index 733429acc..c5f9bba44 100644 --- a/backend/services/mcp_container_service.py +++ b/backend/services/mcp_container_service.py @@ -97,7 +97,7 @@ async def start_mcp_container( service_name: Name of the MCP service tenant_id: Tenant ID for isolation user_id: User ID for isolation - env_vars: Optional environment variables + env_vars: Optional environment variables (may contain authorization_token) Returns: Dictionary with container_id, mcp_url, host_port, and status @@ -149,7 +149,7 @@ async def start_mcp_container_from_tar( service_name: Name of the MCP service tenant_id: Tenant ID for isolation user_id: User ID for isolation - env_vars: Optional environment variables + env_vars: Optional environment variables (may contain authorization_token) host_port: Optional host port to bind full_command: Optional command to run in container diff --git a/backend/services/remote_mcp_service.py b/backend/services/remote_mcp_service.py index 0c8e4576f..ab0f0b04f 100644 --- a/backend/services/remote_mcp_service.py +++ b/backend/services/remote_mcp_service.py @@ -3,6 +3,7 @@ import tempfile from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport, SSETransport from consts.const import CAN_EDIT_ALL_USER_ROLES, PERMISSION_EDIT, PERMISSION_READ from consts.exceptions import MCPConnectionError, MCPNameIllegal @@ -14,6 +15,8 @@ check_mcp_name_exists, update_mcp_status_by_name_and_url, update_mcp_record_by_name_and_url, + get_mcp_authorization_token_by_name_and_url, + get_mcp_record_by_id_and_tenant, ) from database.user_tenant_db import get_user_tenant_by_user_id from services.mcp_container_service import MCPContainerManager @@ -21,9 +24,30 @@ logger = logging.getLogger("remote_mcp_service") -async def mcp_server_health(remote_mcp_server: str) -> bool: +async def mcp_server_health(remote_mcp_server: str, authorization_token: str | None = None) -> bool: try: - client = Client(remote_mcp_server) + # Select transport based on URL ending + url_stripped = remote_mcp_server.strip() + headers = {"Authorization": authorization_token} if authorization_token else {} + + if url_stripped.endswith("/sse"): + transport = SSETransport( + url=url_stripped, + headers=headers + ) + elif url_stripped.endswith("/mcp"): + transport = StreamableHttpTransport( + url=url_stripped, + headers=headers + ) + else: + # Default to StreamableHttpTransport for unrecognized formats + transport = StreamableHttpTransport( + url=url_stripped, + headers=headers + ) + + client = Client(transport=transport) async with client: connected = client.is_connected() return connected @@ -40,6 +64,7 @@ async def add_remote_mcp_server_list( remote_mcp_server: str, remote_mcp_server_name: str, container_id: str | None = None, + authorization_token: str | None = None, ): # check if MCP name already exists @@ -49,7 +74,7 @@ async def add_remote_mcp_server_list( raise MCPNameIllegal("MCP name already exists") # check if the address is available - if not await mcp_server_health(remote_mcp_server=remote_mcp_server): + if not await mcp_server_health(remote_mcp_server=remote_mcp_server, authorization_token=authorization_token): raise MCPConnectionError("MCP connection failed") # update the PG database record @@ -58,6 +83,7 @@ async def add_remote_mcp_server_list( "mcp_server": remote_mcp_server, "status": True, "container_id": container_id, + "authorization_token": authorization_token, } create_mcp_record(mcp_data=insert_mcp_data, tenant_id=tenant_id, user_id=user_id) @@ -104,9 +130,15 @@ async def update_remote_mcp_server_list( f"New MCP name already exists, tenant_id: {tenant_id}, new_mcp_server_name: {update_data.new_service_name}") raise MCPNameIllegal("New MCP name already exists") + # User authorization token + authorization_token = update_data.new_authorization_token + # Check if the new server URL is accessible try: - status = await mcp_server_health(remote_mcp_server=update_data.new_mcp_url) + status = await mcp_server_health( + remote_mcp_server=update_data.new_mcp_url, + authorization_token=authorization_token + ) except BaseException: status = False @@ -124,7 +156,7 @@ async def update_remote_mcp_server_list( ) -async def get_remote_mcp_server_list(tenant_id: str, user_id: str | None = None) -> list[dict]: +async def get_remote_mcp_server_list(tenant_id: str, user_id: str | None = None, is_need_auth: bool = True) -> list[dict]: mcp_records = get_mcp_records_by_tenant(tenant_id=tenant_id) mcp_records_list = [] can_edit_all = False @@ -141,12 +173,16 @@ async def get_remote_mcp_server_list(tenant_id: str, user_id: str | None = None) permission = PERMISSION_EDIT if can_edit_all or str( created_by) == str(user_id) else PERMISSION_READ - mcp_records_list.append({ + record_dict = { "remote_mcp_server_name": record["mcp_name"], "remote_mcp_server": record["mcp_server"], "status": record["status"], "permission": permission, - }) + "mcp_id": record.get("mcp_id"), + } + if is_need_auth: + record_dict["authorization_token"] = record.get("authorization_token") + mcp_records_list.append(record_dict) return mcp_records_list @@ -202,9 +238,19 @@ def attach_mcp_container_permissions( async def check_mcp_health_and_update_db(mcp_url, service_name, tenant_id, user_id): + # Get authorization token from database + authorization_token = get_mcp_authorization_token_by_name_and_url( + mcp_name=service_name, + mcp_server=mcp_url, + tenant_id=tenant_id + ) + # check the health of the MCP server try: - status = await mcp_server_health(remote_mcp_server=mcp_url) + status = await mcp_server_health( + remote_mcp_server=mcp_url, + authorization_token=authorization_token + ) except BaseException: status = False # update the status of the MCP server in the database @@ -232,6 +278,28 @@ async def delete_mcp_by_container_id(tenant_id: str, user_id: str, container_id: ) +async def get_mcp_record_by_id(mcp_id: int, tenant_id: str) -> dict | None: + """ + Get MCP record by ID + + Args: + mcp_id: MCP record ID + tenant_id: Tenant ID + + Returns: + Dictionary containing mcp_name, mcp_server, and authorization_token, or None if not found + """ + mcp_record = get_mcp_record_by_id_and_tenant(mcp_id=mcp_id, tenant_id=tenant_id) + if not mcp_record: + return None + + return { + "mcp_name": mcp_record.get("mcp_name"), + "mcp_server": mcp_record.get("mcp_server"), + "authorization_token": mcp_record.get("authorization_token"), + } + + async def upload_and_start_mcp_image( tenant_id: str, user_id: str, @@ -320,6 +388,11 @@ async def upload_and_start_mcp_image( logger.warning( f"Failed to clean up temporary file {temp_file_path}: {e}") + # Extract authorization_token from env_vars for database registration + authorization_token = None + if parsed_env_vars: + authorization_token = parsed_env_vars.get("authorization_token") + # Register to remote MCP server list await add_remote_mcp_server_list( tenant_id=tenant_id, @@ -327,6 +400,7 @@ async def upload_and_start_mcp_image( remote_mcp_server=container_info["mcp_url"], remote_mcp_server_name=final_service_name, container_id=container_info["container_id"], + authorization_token=authorization_token, ) return { diff --git a/backend/services/tenant_service.py b/backend/services/tenant_service.py index 0da209b76..bb761d2b4 100644 --- a/backend/services/tenant_service.py +++ b/backend/services/tenant_service.py @@ -1,6 +1,7 @@ """ Tenant service for managing tenant operations """ +import asyncio import logging import uuid from typing import Any, Dict, List, Optional @@ -9,10 +10,19 @@ get_single_config_info, insert_config, update_config_by_tenant_config_id, - get_all_tenant_ids + get_all_tenant_ids, + delete_config_by_tenant_config_id, + get_all_configs_by_tenant_id, ) -from database.user_tenant_db import get_users_by_tenant_id -from database.group_db import add_group +from database.user_tenant_db import get_users_by_tenant_id, soft_delete_users_by_tenant_id +from services.user_service import delete_user_and_cleanup +from database.group_db import add_group, query_groups_by_tenant, remove_group +from database.model_management_db import get_model_records, delete_model_record +from database.knowledge_db import get_knowledge_info_by_tenant_id, delete_knowledge_record +from database.agent_db import query_all_agent_info_by_tenant_id, delete_agent_by_id, delete_agent_relationship +from database.remote_mcp_db import get_mcp_records_by_tenant, delete_mcp_record_by_name_and_url +from database.invitation_db import query_invitations_by_tenant, remove_invitation +from database.tool_db import delete_tools_by_agent_id from consts.const import TENANT_NAME, TENANT_ID, DEFAULT_GROUP_ID from consts.exceptions import NotFoundException, ValidationError, UserRegistrationException @@ -112,22 +122,35 @@ def check_tenant_name_exists(tenant_name: str, exclude_tenant_id: Optional[str] return False -def get_all_tenants() -> List[Dict[str, Any]]: +def get_tenants_paginated(page: int = 1, page_size: int = 20) -> Dict[str, Any]: """ - Get all tenants + Get tenants with pagination support + + Args: + page (int): Page number (starting from 1) + page_size (int): Number of items per page Returns: - List[Dict[str, Any]]: List of all tenant information + Dict[str, Any]: Dictionary containing paginated tenant data and pagination info """ - tenant_ids = get_all_tenant_ids() - tenants = [] + # Get all tenant IDs first + all_tenant_ids = get_all_tenant_ids() + total = len(all_tenant_ids) + + # Calculate pagination + total_pages = (total + page_size - 1) // page_size if total > 0 else 1 + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size - for tenant_id in tenant_ids: + # Get tenant IDs for current page + page_tenant_ids = all_tenant_ids[start_idx:end_idx] + + tenants = [] + for tenant_id in page_tenant_ids: try: tenant_info = get_tenant_info(tenant_id) tenants.append(tenant_info) except NotFoundException: - # Return tenant with basic info but empty name for frontend to show as "unnamed tenant" logging.warning(f"Tenant info of {tenant_id} not found. Returning basic tenant structure.") tenant_info = { "tenant_id": tenant_id, @@ -136,7 +159,13 @@ def get_all_tenants() -> List[Dict[str, Any]]: } tenants.append(tenant_info) - return tenants + return { + "data": tenants, + "total": total, + "page": page, + "page_size": page_size, + "total_pages": total_pages + } def create_tenant(tenant_name: str, created_by: Optional[str] = None) -> Dict[str, Any]: @@ -273,22 +302,152 @@ def update_tenant_info(tenant_id: str, tenant_name: str, updated_by: Optional[st return updated_tenant -def delete_tenant(tenant_id: str, deleted_by: Optional[str] = None) -> bool: +async def delete_tenant(tenant_id: str, deleted_by: Optional[str] = None) -> bool: """ - Delete tenant (placeholder for future implementation) - NOTE: Deletion logic is complex and not yet implemented + Delete tenant and all associated resources + + This performs cascade deletion of: + - All users in the tenant (soft delete) + - All groups in the tenant + - All models in the tenant + - All knowledge bases in the tenant + - All agents in the tenant (including tool instances) + - All MCP configurations in the tenant + - All invitation codes in the tenant + - All tenant configurations Args: - tenant_id (str): Tenant ID - deleted_by (Optional[str]): Deleted by user ID + tenant_id (str): Tenant ID to delete + deleted_by (Optional[str]): User who initiated the deletion Returns: - bool: Always returns False as this is not yet implemented + bool: True if deletion was successful Raises: - ValidationError: Always raised as this is not yet implemented + NotFoundException: When tenant does not exist + ValidationError: When deletion fails """ - raise NotImplementedError("Tenant deletion is not yet implemented due to complex dependencies") + # Validate tenant exists + name_config = get_single_config_info(tenant_id, TENANT_NAME) + if not name_config: + raise NotFoundException(f"Tenant {tenant_id} does not exist") + + logger.info(f"Starting cascade deletion for tenant {tenant_id} by {deleted_by}") + + try: + # 1. Deactivate all users in the tenant (full cleanup including Supabase deletion) + logger.info(f"Deactivating users for tenant {tenant_id}") + users_result = get_users_by_tenant_id(tenant_id, page=1, page_size=10000) + users = users_result.get("users", []) + + if users: + async def delete_single_user(user: Dict[str, Any]) -> None: + user_id = user.get("user_id") + if user_id: + try: + await delete_user_and_cleanup(user_id, tenant_id) + logger.info(f"Deactivated user {user_id} for tenant {tenant_id}") + except Exception as e: + logger.warning(f"Failed to deactivate user {user_id}: {str(e)}") + + # Concurrently delete all users + await asyncio.gather(*[delete_single_user(user) for user in users]) + + # 2. Delete all groups in the tenant + logger.info(f"Deleting groups for tenant {tenant_id}") + groups = query_groups_by_tenant(tenant_id, page=1, page_size=10000) + for group in groups.get("data", []): + try: + remove_group(group["group_id"], deleted_by) + except Exception as e: + logger.warning(f"Failed to delete group {group.get('group_id')}: {str(e)}") + + # 3. Delete all models in the tenant + logger.info(f"Deleting models for tenant {tenant_id}") + models = get_model_records({"tenant_id": tenant_id}, tenant_id) + for model in models: + try: + delete_model_record(model["model_id"], deleted_by or "system", tenant_id) + except Exception as e: + logger.warning(f"Failed to delete model {model.get('model_id')}: {str(e)}") + + # 4. Delete all knowledge bases in the tenant + logger.info(f"Deleting knowledge bases for tenant {tenant_id}") + knowledge_list = get_knowledge_info_by_tenant_id(tenant_id) + for kb in knowledge_list: + try: + delete_knowledge_record({ + "knowledge_id": kb["knowledge_id"], + "user_id": deleted_by or "system" + }) + except Exception as e: + logger.warning(f"Failed to delete knowledge base {kb.get('knowledge_id')}: {str(e)}") + + # 5. Delete all agents in the tenant (including related data) + logger.info(f"Deleting agents for tenant {tenant_id}") + agents = query_all_agent_info_by_tenant_id(tenant_id, version_no=0) + for agent in agents: + try: + agent_id = agent.get("agent_id") + # Delete tool instances first + delete_tools_by_agent_id(agent_id, tenant_id, deleted_by or "system", version_no=0) + # Delete agent relationships + delete_agent_relationship(agent_id, tenant_id, deleted_by or "system", version_no=0) + # Delete the agent + delete_agent_by_id(agent_id, tenant_id, deleted_by or "system") + except Exception as e: + logger.warning(f"Failed to delete agent {agent.get('agent_id')}: {str(e)}") + + # Also delete published agents (version_no >= 1) + agents_published = query_all_agent_info_by_tenant_id(tenant_id, version_no=1) + for agent in agents_published: + try: + agent_id = agent.get("agent_id") + delete_tools_by_agent_id(agent_id, tenant_id, deleted_by or "system", version_no=1) + delete_agent_relationship(agent_id, tenant_id, deleted_by or "system", version_no=1) + delete_agent_by_id(agent_id, tenant_id, deleted_by or "system") + except Exception as e: + logger.warning(f"Failed to delete published agent {agent.get('agent_id')}: {str(e)}") + + # 6. Delete all MCP configurations in the tenant + logger.info(f"Deleting MCP records for tenant {tenant_id}") + mcp_list = get_mcp_records_by_tenant(tenant_id) + for mcp in mcp_list: + try: + delete_mcp_record_by_name_and_url( + mcp["mcp_name"], + mcp["mcp_server"], + tenant_id, + deleted_by or "system" + ) + except Exception as e: + logger.warning(f"Failed to delete MCP {mcp.get('mcp_id')}: {str(e)}") + + # 7. Delete all invitation codes in the tenant + logger.info(f"Deleting invitations for tenant {tenant_id}") + invitations = query_invitations_by_tenant(tenant_id) + for invitation in invitations: + try: + remove_invitation(invitation["invitation_id"], deleted_by) + except Exception as e: + logger.warning(f"Failed to delete invitation {invitation.get('invitation_id')}: {str(e)}") + + # 8. Delete all tenant configurations (must be done last) + logger.info(f"Deleting tenant configurations for tenant {tenant_id}") + # Delete all config records for this tenant + all_configs = get_all_configs_by_tenant_id(tenant_id) + for config in all_configs: + try: + delete_config_by_tenant_config_id(config["tenant_config_id"]) + except Exception as e: + logger.warning(f"Failed to delete config {config.get('tenant_config_id')}: {str(e)}") + + logger.info(f"Successfully deleted tenant {tenant_id} and all associated resources") + return True + + except Exception as e: + logger.error(f"Failed to delete tenant {tenant_id}: {str(e)}") + raise ValidationError(f"Failed to delete tenant: {str(e)}") def _create_default_group_for_tenant(tenant_id: str, created_by: Optional[str] = None) -> int: diff --git a/backend/services/tool_configuration_service.py b/backend/services/tool_configuration_service.py index 588d2467e..3e7b22d11 100644 --- a/backend/services/tool_configuration_service.py +++ b/backend/services/tool_configuration_service.py @@ -7,13 +7,18 @@ from pydantic_core import PydanticUndefined from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport, SSETransport import jsonref from mcpadapt.smolagents_adapter import _sanitize_function_name from consts.const import LOCAL_MCP_SERVER, DATA_PROCESS_SERVICE from consts.exceptions import MCPConnectionError, ToolExecutionException, NotFoundException from consts.model import ToolInstanceInfoRequest, ToolInfo, ToolSourceEnum, ToolValidateRequest -from database.remote_mcp_db import get_mcp_records_by_tenant, get_mcp_server_by_name_and_tenant +from database.remote_mcp_db import ( + get_mcp_records_by_tenant, + get_mcp_server_by_name_and_tenant, + get_mcp_authorization_token_by_name_and_url, +) from database.tool_db import ( create_or_update_tool_by_tool_info, query_all_tools, @@ -30,6 +35,29 @@ logger = logging.getLogger("tool_configuration_service") +def _create_mcp_transport(url: str, authorization_token: Optional[str] = None): + """ + Create appropriate MCP transport based on URL ending. + + Args: + url: MCP server URL + authorization_token: Optional authorization token + + Returns: + Transport instance (SSETransport or StreamableHttpTransport) + """ + url_stripped = url.strip() + headers = {"Authorization": authorization_token} if authorization_token else {} + + if url_stripped.endswith("/sse"): + return SSETransport(url=url_stripped, headers=headers) + elif url_stripped.endswith("/mcp"): + return StreamableHttpTransport(url=url_stripped, headers=headers) + else: + # Default to StreamableHttpTransport for unrecognized formats + return StreamableHttpTransport(url=url_stripped, headers=headers) + + def python_type_to_json_schema(annotation: Any) -> str: """ Convert Python type annotations to JSON Schema types @@ -209,14 +237,20 @@ async def get_all_mcp_tools(tenant_id: str) -> List[ToolInfo]: # only update connected server if record["status"]: try: - tools_info.extend(await get_tool_from_remote_mcp_server(mcp_server_name=record["mcp_name"], - remote_mcp_server=record["mcp_server"])) + tools_info.extend(await get_tool_from_remote_mcp_server( + mcp_server_name=record["mcp_name"], + remote_mcp_server=record["mcp_server"], + tenant_id=tenant_id + )) except Exception as e: logger.error(f"mcp connection error: {str(e)}") default_mcp_url = urljoin(LOCAL_MCP_SERVER, "sse") - tools_info.extend(await get_tool_from_remote_mcp_server(mcp_server_name="nexent", - remote_mcp_server=default_mcp_url)) + tools_info.extend(await get_tool_from_remote_mcp_server( + mcp_server_name="nexent", + remote_mcp_server=default_mcp_url, + tenant_id=None + )) return tools_info @@ -274,12 +308,34 @@ def update_tool_info_impl(tool_info: ToolInstanceInfoRequest, tenant_id: str, us } -async def get_tool_from_remote_mcp_server(mcp_server_name: str, remote_mcp_server: str): - """get the tool information from the remote MCP server, avoid blocking the event loop""" +async def get_tool_from_remote_mcp_server( + mcp_server_name: str, + remote_mcp_server: str, + tenant_id: Optional[str] = None, + authorization_token: Optional[str] = None +): + """ + Get the tool information from the remote MCP server, avoid blocking the event loop + + Args: + mcp_server_name: Name of the MCP server + remote_mcp_server: URL of the MCP server + tenant_id: Optional tenant ID for database lookup of authorization_token + authorization_token: Optional authorization token for authentication (if not provided and tenant_id is given, will be fetched from database) + """ + # Get authorization token from database if not provided + if authorization_token is None and tenant_id: + authorization_token = get_mcp_authorization_token_by_name_and_url( + mcp_name=mcp_server_name, + mcp_server=remote_mcp_server, + tenant_id=tenant_id + ) + tools_info = [] try: - client = Client(remote_mcp_server, timeout=10) + transport = _create_mcp_transport(remote_mcp_server, authorization_token) + client = Client(transport=transport, timeout=10) async with client: # List available operations tools = await client.list_tools() @@ -407,7 +463,8 @@ def load_last_tool_config_impl(tool_id: int, tenant_id: str, user_id: str): async def _call_mcp_tool( mcp_url: str, tool_name: str, - inputs: Optional[Dict[str, Any]] + inputs: Optional[Dict[str, Any]], + authorization_token: Optional[str] = None ) -> Dict[str, Any]: """ Common method to call MCP tool with connection handling. @@ -416,6 +473,7 @@ async def _call_mcp_tool( mcp_url: MCP server URL tool_name: Name of the tool to call inputs: Parameters to pass to the tool + authorization_token: Optional authorization token for authentication Returns: Dict containing tool execution result @@ -423,7 +481,8 @@ async def _call_mcp_tool( Raises: MCPConnectionError: If MCP connection fails """ - client = Client(mcp_url) + transport = _create_mcp_transport(mcp_url, authorization_token) + client = Client(transport=transport) async with client: # Check if connected if not client.is_connected(): @@ -486,7 +545,16 @@ async def _validate_mcp_tool_remote( if not actual_mcp_url: raise NotFoundException(f"MCP server not found for name: {usage}") - return await _call_mcp_tool(actual_mcp_url, tool_name, inputs) + # Get authorization token from database + authorization_token = None + if tenant_id: + authorization_token = get_mcp_authorization_token_by_name_and_url( + mcp_name=usage, + mcp_server=actual_mcp_url, + tenant_id=tenant_id + ) + + return await _call_mcp_tool(actual_mcp_url, tool_name, inputs, authorization_token) def _get_tool_class_by_name(tool_name: str) -> Optional[type]: diff --git a/backend/services/user_management_service.py b/backend/services/user_management_service.py index bb27ca13d..792887ec5 100644 --- a/backend/services/user_management_service.py +++ b/backend/services/user_management_service.py @@ -120,51 +120,6 @@ async def check_auth_service_health(): raise ConnectionError("Auth service is unavailable") -async def signup_user(email: EmailStr, - password: str, - is_admin: Optional[bool] = False, - invite_code: Optional[str] = None): - """User registration""" - client = get_supabase_client() - logging.info( - f"Receive registration request: email={email}, is_admin={is_admin}") - if is_admin: - await verify_invite_code(invite_code) - - # Set user metadata, including role information - response = client.auth.sign_up({ - "email": email, - "password": password, - "options": { - "data": {"role": "admin" if is_admin else "user"} - } - }) - - if response.user: - user_id = response.user.id - user_role = "admin" if is_admin else "user" - tenant_id = user_id if is_admin else "tenant_id" - - # Create user tenant relationship - insert_user_tenant(user_id=user_id, tenant_id=tenant_id, user_email=email) - - logging.info( - f"User {email} registered successfully, role: {user_role}, tenant: {tenant_id}") - - if is_admin: - await generate_tts_stt_4_admin(tenant_id, user_id) - - # Initialize tool list for the new tenant (only once per tenant) - await init_tool_list_for_tenant(tenant_id, user_id) - - return await parse_supabase_response(is_admin, response, user_role) - else: - logging.error( - "Supabase registration request returned no user object") - raise UserRegistrationException( - "Registration service is temporarily unavailable, please try again later") - - async def signup_user_with_invitation(email: EmailStr, password: str, invite_code: Optional[str] = None): diff --git a/backend/services/user_service.py b/backend/services/user_service.py index 74e99c69c..8ee4420c4 100644 --- a/backend/services/user_service.py +++ b/backend/services/user_service.py @@ -4,7 +4,6 @@ import logging from typing import Dict, Any, List -from consts.exceptions import ValidationError from database.user_tenant_db import ( get_users_by_tenant_id, update_user_tenant_role, get_user_tenant_by_user_id, soft_delete_user_tenant_by_user_id @@ -14,6 +13,7 @@ from database.conversation_db import soft_delete_all_conversations_by_user from utils.auth_utils import get_supabase_admin_client from utils.memory_utils import build_memory_config + from nexent.memory.memory_service import clear_memory logger = logging.getLogger(__name__) @@ -73,8 +73,6 @@ async def update_user(user_id: str, update_data: Dict[str, Any], updated_by: str Raises: ValueError: When user not found or invalid data """ - from database.user_tenant_db import update_user_tenant_role - try: # Validate role if provided if "role" in update_data: diff --git a/backend/services/vectordatabase_service.py b/backend/services/vectordatabase_service.py index e4f51e15c..e32f005a3 100644 --- a/backend/services/vectordatabase_service.py +++ b/backend/services/vectordatabase_service.py @@ -146,7 +146,7 @@ def _rethrow_or_plain(exc: Exception) -> None: raise Exception(msg) -def check_knowledge_base_exist_impl(knowledge_name: str, vdb_core: VectorDatabaseCore, user_id: str, tenant_id: str) -> dict: +def check_knowledge_base_exist_impl(knowledge_name: str, vdb_core: VectorDatabaseCore, user_id: str, tenant_id: str, exclude_index_name: Optional[str] = None) -> dict: """ Check knowledge base existence and handle orphan cases @@ -155,6 +155,7 @@ def check_knowledge_base_exist_impl(knowledge_name: str, vdb_core: VectorDatabas vdb_core: Elasticsearch core instance user_id: Current user ID tenant_id: Current tenant ID + exclude_index_name: Optional index name to exclude from the check (used when updating an existing knowledge base) Returns: dict: Status information about the knowledge base @@ -165,6 +166,9 @@ def check_knowledge_base_exist_impl(knowledge_name: str, vdb_core: VectorDatabas # Case A: Knowledge base name already exists in the same tenant if pg_record: + # If we're excluding a specific index and this is the one we found, consider it available + if exclude_index_name and pg_record.get("index_name") == exclude_index_name: + return {"status": "available"} return {"status": "exists_in_tenant"} # Case B: Name is available in this tenant @@ -1228,7 +1232,6 @@ async def summary_index_name(self, summarize_clusters_map_reduce, merge_cluster_summaries ) - # Use new Map-Reduce approach # Sample reasonable number of documents sample_count = min(batch_size // 5, 200) diff --git a/backend/utils/attachment_utils.py b/backend/utils/attachment_utils.py deleted file mode 100644 index 56edf94f1..000000000 --- a/backend/utils/attachment_utils.py +++ /dev/null @@ -1,82 +0,0 @@ -from consts.const import LANGUAGE, MODEL_CONFIG_MAPPING -from typing import Union, BinaryIO - -from jinja2 import Template, StrictUndefined -from nexent.core import MessageObserver -from nexent.core.models.openai_long_context_model import OpenAILongContextModel -from nexent.core.models.openai_vlm import OpenAIVLModel - -from utils.config_utils import get_model_name_from_config, tenant_config_manager -from utils.prompt_template_utils import get_analyze_file_prompt_template - - -def convert_image_to_text(query: str, image_input: Union[str, BinaryIO], tenant_id: str, language: str = LANGUAGE["ZH"]): - """ - Convert image to text description based on user query - - Args: - query: User's question - image_input: Image input (file path or binary data) - tenant_id: Tenant ID for model configuration - language: Language code ('zh' for Chinese, 'en' for English) - - Returns: - str: Image description text - """ - vlm_model_config = tenant_config_manager.get_model_config( - key=MODEL_CONFIG_MAPPING["vlm"], tenant_id=tenant_id) - image_to_text_model = OpenAIVLModel( - observer=MessageObserver(), - model_id=get_model_name_from_config( - vlm_model_config) if vlm_model_config else "", - api_base=vlm_model_config.get("base_url", ""), - api_key=vlm_model_config.get("api_key", ""), - temperature=0.7, - top_p=0.7, - frequency_penalty=0.5, - max_tokens=512, - ssl_verify=vlm_model_config.get("ssl_verify", True), - ) - - # Load prompts from yaml file - prompts = get_analyze_file_prompt_template(language) - system_prompt = Template(prompts['image_analysis']['system_prompt'], - undefined=StrictUndefined).render({'query': query}) - - return image_to_text_model.analyze_image(image_input=image_input, system_prompt=system_prompt).content - - -def convert_long_text_to_text(query: str, file_context: str, tenant_id: str, language: str = LANGUAGE["ZH"]): - """ - Convert long text to summarized text based on user query - - Args: - query: User's question - file_context: Long text content to analyze - tenant_id: Tenant ID for model configuration - language: Language code ('zh' for Chinese, 'en' for English) - - Returns: - tuple[str, str]: Summarized text description and truncation percentage string - """ - main_model_config = tenant_config_manager.get_model_config( - key=MODEL_CONFIG_MAPPING["llm"], tenant_id=tenant_id) - long_text_to_text_model = OpenAILongContextModel( - observer=MessageObserver(), - model_id=get_model_name_from_config(main_model_config), - api_base=main_model_config.get("base_url"), - api_key=main_model_config.get("api_key"), - max_context_tokens=main_model_config.get("max_tokens"), - ssl_verify=main_model_config.get("ssl_verify", True), - ) - - # Load prompts from yaml file - prompts = get_analyze_file_prompt_template(language) - system_prompt = Template(prompts['long_text_analysis']['system_prompt'], - undefined=StrictUndefined).render({'query': query}) - user_prompt = Template( - prompts['long_text_analysis']['user_prompt'], undefined=StrictUndefined).render({}) - - result, truncation_percentage = long_text_to_text_model.analyze_long_text( - file_context, system_prompt, user_prompt) - return result.content, truncation_percentage diff --git a/backend/utils/document_vector_utils.py b/backend/utils/document_vector_utils.py index 1b35c207f..a1befdb43 100644 --- a/backend/utils/document_vector_utils.py +++ b/backend/utils/document_vector_utils.py @@ -26,8 +26,7 @@ from utils.llm_utils import call_llm_for_system_prompt from utils.prompt_template_utils import ( get_document_summary_prompt_template, - get_cluster_summary_reduce_prompt_template, - get_cluster_summary_agent_prompt_template + get_cluster_summary_reduce_prompt_template ) logger = logging.getLogger("document_vector_utils") @@ -492,54 +491,6 @@ def process_documents_for_clustering(index_name: str, vdb_core, sample_doc_count raise Exception(f"Failed to process documents: {str(e)}") -def extract_cluster_content(document_samples: Dict[str, Dict], cluster_doc_ids: List[str], max_chunks_per_doc: int = 3) -> str: - """ - Extract representative content from a cluster for summarization - - Args: - document_samples: Dictionary mapping doc_id to document info - cluster_doc_ids: List of document IDs in the cluster - max_chunks_per_doc: Maximum number of chunks to include per document - - Returns: - Formatted string containing cluster content - """ - cluster_content_parts = [] - - for doc_id in cluster_doc_ids: - if doc_id not in document_samples: - continue - - doc_info = document_samples[doc_id] - chunks = doc_info.get('chunks', []) - filename = doc_info.get('filename', 'unknown') - - # Extract representative chunks - representative_chunks = [] - if len(chunks) <= max_chunks_per_doc: - representative_chunks = chunks - else: - # Take first, middle, and last chunks - representative_chunks = ( - chunks[:1] + - chunks[len(chunks)//2:len(chunks)//2+1] + - chunks[-1:] - ) - - # Format document content - doc_content = f"\n--- Document: {filename} ---\n" - for chunk in representative_chunks: - content = chunk.get('content', '') - # Limit chunk content length - if len(content) > 500: - content = content[:500] + "..." - doc_content += f"{content}\n" - - cluster_content_parts.append(doc_content) - - return "\n".join(cluster_content_parts) - - def summarize_document(document_content: str, filename: str, language: str = LANGUAGE["ZH"], max_words: int = 100, model_id: Optional[int] = None, tenant_id: Optional[str] = None) -> str: """ Summarize a single document using LLM (Map stage) @@ -861,68 +812,4 @@ def summarize_clusters_map_reduce(document_samples: Dict[str, Dict], clusters: D return cluster_summaries -def summarize_clusters(document_samples: Dict[str, Dict], clusters: Dict[int, List[str]], - language: str = LANGUAGE["ZH"], max_words: int = 150) -> Dict[int, str]: - """ - Summarize all clusters (legacy method - kept for backward compatibility) - - Note: This method uses the old approach. Use summarize_clusters_map_reduce for better results. - - Args: - document_samples: Dictionary mapping doc_id to document info - clusters: Dictionary mapping cluster_id to list of doc_ids - language: Language code ('zh' or 'en') - max_words: Maximum words per cluster summary - - Returns: - Dictionary mapping cluster_id to summary text - """ - cluster_summaries = {} - - for cluster_id, doc_ids in clusters.items(): - logger.info(f"Summarizing cluster {cluster_id} with {len(doc_ids)} documents") - - # Extract cluster content - cluster_content = extract_cluster_content(document_samples, doc_ids, max_chunks_per_doc=3) - - # Generate summary using old method - summary = summarize_cluster_legacy(cluster_content, language, max_words) - cluster_summaries[cluster_id] = summary - - return cluster_summaries - - -def summarize_cluster_legacy(cluster_content: str, language: str = LANGUAGE["ZH"], max_words: int = 150) -> str: - """ - Legacy cluster summarization method (single-stage) - - Args: - cluster_content: Formatted content from the cluster - language: Language code ('zh' or 'en') - max_words: Maximum words in the summary - - Returns: - Cluster summary text - """ - try: - # Get prompt template from prompt_template_utils - prompts = get_cluster_summary_agent_prompt_template(language) - - system_prompt = prompts.get('system_prompt', '') - user_prompt_template = prompts.get('user_prompt', '') - - user_prompt = Template(user_prompt_template, undefined=StrictUndefined).render( - cluster_content=cluster_content, - max_words=max_words - ) - - logger.info(f"Cluster summary prompt generated (language: {language}, max_words: {max_words})") - - # Note: This is a legacy function, using placeholder summary - # The main summarization uses summarize_cluster() with LLM integration - return f"[Cluster Summary] (max {max_words} words) - Content preview: {cluster_content[:200]}..." - - except Exception as e: - logger.error(f"Error generating cluster summary: {str(e)}", exc_info=True) - return f"Failed to generate summary: {str(e)}" diff --git a/backend/utils/prompt_template_utils.py b/backend/utils/prompt_template_utils.py index b3fb9cc6e..b12ba19a5 100644 --- a/backend/utils/prompt_template_utils.py +++ b/backend/utils/prompt_template_utils.py @@ -17,12 +17,9 @@ def get_prompt_template(template_type: str, language: str = LANGUAGE["ZH"], **kw template_type: Template type, supports the following values: - 'prompt_generate': Prompt generation template - 'agent': Agent template including manager and managed agents - - 'knowledge_summary': Knowledge summary template - - 'analyze_file': File analysis template - 'generate_title': Title generation template - 'document_summary': Document summary template (Map stage) - 'cluster_summary_reduce': Cluster summary reduce template (Reduce stage) - - 'cluster_summary_agent': Cluster summary agent template (legacy) language: Language code ('zh' or 'en') **kwargs: Additional parameters, for agent type need to pass is_manager parameter @@ -48,14 +45,6 @@ def get_prompt_template(template_type: str, language: str = LANGUAGE["ZH"], **kw 'managed': 'backend/prompts/managed_system_prompt_template_en.yaml' } }, - 'knowledge_summary': { - LANGUAGE["ZH"]: 'backend/prompts/knowledge_summary_agent_zh.yaml', - LANGUAGE["EN"]: 'backend/prompts/knowledge_summary_agent_en.yaml' - }, - 'analyze_file': { - LANGUAGE["ZH"]: 'backend/prompts/analyze_file_zh.yaml', - LANGUAGE["EN"]: 'backend/prompts/analyze_file_en.yaml' - }, 'generate_title': { LANGUAGE["ZH"]: 'backend/prompts/utils/generate_title_zh.yaml', LANGUAGE["EN"]: 'backend/prompts/utils/generate_title_en.yaml' @@ -67,10 +56,6 @@ def get_prompt_template(template_type: str, language: str = LANGUAGE["ZH"], **kw 'cluster_summary_reduce': { LANGUAGE["ZH"]: 'backend/prompts/cluster_summary_reduce_zh.yaml', LANGUAGE["EN"]: 'backend/prompts/cluster_summary_reduce_en.yaml' - }, - 'cluster_summary_agent': { - LANGUAGE["ZH"]: 'backend/prompts/cluster_summary_agent_zh.yaml', - LANGUAGE["EN"]: 'backend/prompts/cluster_summary_agent_en.yaml' } } @@ -124,32 +109,6 @@ def get_agent_prompt_template(is_manager: bool, language: str = LANGUAGE["ZH"]) return get_prompt_template('agent', language, is_manager=is_manager) -def get_knowledge_summary_prompt_template(language: str = 'zh') -> Dict[str, Any]: - """ - Get knowledge summary prompt template - - Args: - language: Language code ('zh' or 'en') - - Returns: - dict: Loaded prompt template configuration - """ - return get_prompt_template('knowledge_summary', language) - - -def get_analyze_file_prompt_template(language: str = 'zh') -> Dict[str, Any]: - """ - Get file analysis prompt template - - Args: - language: Language code ('zh' or 'en') - - Returns: - dict: Loaded prompt template configuration - """ - return get_prompt_template('analyze_file', language) - - def get_generate_title_prompt_template(language: str = 'zh') -> Dict[str, Any]: """ Get title generation prompt template @@ -187,16 +146,3 @@ def get_cluster_summary_reduce_prompt_template(language: str = LANGUAGE["ZH"]) - dict: Loaded cluster summary reduce prompt template configuration """ return get_prompt_template('cluster_summary_reduce', language) - - -def get_cluster_summary_agent_prompt_template(language: str = LANGUAGE["ZH"]) -> Dict[str, Any]: - """ - Get cluster summary agent prompt template (legacy) - - Args: - language: Language code ('zh' or 'en') - - Returns: - dict: Loaded cluster summary agent prompt template configuration - """ - return get_prompt_template('cluster_summary_agent', language) diff --git a/doc/docs/en/quick-start/installation.md b/doc/docs/en/quick-start/installation.md index c7115a3cd..662eb7c3d 100644 --- a/doc/docs/en/quick-start/installation.md +++ b/doc/docs/en/quick-start/installation.md @@ -44,6 +44,8 @@ After executing this command, the system will provide two different versions for - **Terminal Tool**: Enables openssh-server for AI agent shell command execution - **Regional optimization**: Mainland China users can use optimized image sources +>⚠️ **Important Note**: When deploying v1.8.0 or later for the first time, please pay special attention to the `suadmin` super administrator account information output in the Docker logs. This account has the highest system privileges, and the password is only displayed upon first generation. It cannot be viewed again later, so please be sure to save it securely. + ### 3. Access Your Installation When deployment completes successfully: diff --git a/doc/docs/en/user-guide/agent-market.md b/doc/docs/en/user-guide/agent-market.md index 6fdd8cf84..1106f3db6 100644 --- a/doc/docs/en/user-guide/agent-market.md +++ b/doc/docs/en/user-guide/agent-market.md @@ -38,12 +38,16 @@ Select your preferred agent, download with one click, and add it to your agent s ![Agent Market Download](./assets/agent-market/agent-market-download.png) -### 2️⃣ Configure Fields +### 2️⃣ Configure Local Tools -🔑 Fill in tool permissions as prompted +🔑 Fill in local tool permissions as prompted ![Agent Market Download 2](./assets/agent-market/agent-market-download2.png) +### 3️⃣ Configure External MCP Tools + +🔑 Fill in MCP tool permissions as prompted + After installation, your agent will be ready in **[Agent Space](./agent-space)** ## 📢 Share Your Creations diff --git a/doc/docs/en/user-guide/assets/agent-development/duplicated_import.png b/doc/docs/en/user-guide/assets/agent-development/duplicated_import.png index 3d7e0e6bc..164e4f228 100644 Binary files a/doc/docs/en/user-guide/assets/agent-development/duplicated_import.png and b/doc/docs/en/user-guide/assets/agent-development/duplicated_import.png differ diff --git a/doc/docs/en/user-guide/assets/agent-development/generate-agent.png b/doc/docs/en/user-guide/assets/agent-development/generate-agent.png index 876c42e18..ca9b061ab 100644 Binary files a/doc/docs/en/user-guide/assets/agent-development/generate-agent.png and b/doc/docs/en/user-guide/assets/agent-development/generate-agent.png differ diff --git a/doc/docs/en/user-guide/assets/agent-development/version_management_1.png b/doc/docs/en/user-guide/assets/agent-development/version_management_1.png new file mode 100644 index 000000000..08d182c27 Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-development/version_management_1.png differ diff --git a/doc/docs/en/user-guide/assets/agent-development/version_management_2.png b/doc/docs/en/user-guide/assets/agent-development/version_management_2.png new file mode 100644 index 000000000..bdf3b5bb0 Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-development/version_management_2.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market-download.png b/doc/docs/en/user-guide/assets/agent-market/agent-market-download.png index 2f258676d..1b7b8c9c1 100644 Binary files a/doc/docs/en/user-guide/assets/agent-market/agent-market-download.png and b/doc/docs/en/user-guide/assets/agent-market/agent-market-download.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market-download2.png b/doc/docs/en/user-guide/assets/agent-market/agent-market-download2.png index 4bf6d9491..e1108bc32 100644 Binary files a/doc/docs/en/user-guide/assets/agent-market/agent-market-download2.png and b/doc/docs/en/user-guide/assets/agent-market/agent-market-download2.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market-download3.png b/doc/docs/en/user-guide/assets/agent-market/agent-market-download3.png new file mode 100644 index 000000000..164e4f228 Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-market/agent-market-download3.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market.png b/doc/docs/en/user-guide/assets/agent-market/agent-market.png index d8e71e014..8d5be8a55 100644 Binary files a/doc/docs/en/user-guide/assets/agent-market/agent-market.png and b/doc/docs/en/user-guide/assets/agent-market/agent-market.png differ diff --git a/doc/docs/en/user-guide/assets/agent-space/agent-space.png b/doc/docs/en/user-guide/assets/agent-space/agent-space.png index b43f00d21..fb16212d2 100644 Binary files a/doc/docs/en/user-guide/assets/agent-space/agent-space.png and b/doc/docs/en/user-guide/assets/agent-space/agent-space.png differ diff --git a/doc/docs/en/user-guide/assets/home-page/homepage.png b/doc/docs/en/user-guide/assets/home-page/homepage.png index cb00c9561..1e8292dcb 100644 Binary files a/doc/docs/en/user-guide/assets/home-page/homepage.png and b/doc/docs/en/user-guide/assets/home-page/homepage.png differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/create-knowledge-base.png b/doc/docs/en/user-guide/assets/knowledge-base/create-knowledge-base.png index 96f913735..10ba70189 100644 Binary files a/doc/docs/en/user-guide/assets/knowledge-base/create-knowledge-base.png and b/doc/docs/en/user-guide/assets/knowledge-base/create-knowledge-base.png differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/delete-knowledge-base.png b/doc/docs/en/user-guide/assets/knowledge-base/delete-knowledge-base.png deleted file mode 100644 index 4785b2e89..000000000 Binary files a/doc/docs/en/user-guide/assets/knowledge-base/delete-knowledge-base.png and /dev/null differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-file-list.png b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-file-list.png deleted file mode 100644 index a4673369e..000000000 Binary files a/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-file-list.png and /dev/null differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-permission.png b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-permission.png new file mode 100644 index 000000000..49d14cbbd Binary files /dev/null and b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-permission.png differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-summary.png b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-summary.png deleted file mode 100644 index 92452a335..000000000 Binary files a/doc/docs/en/user-guide/assets/knowledge-base/knowledge-base-summary.png and /dev/null differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/knowledge-tool.png b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-tool.png new file mode 100644 index 000000000..8505804ea Binary files /dev/null and b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-tool.png differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/knowledge-tool2.png b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-tool2.png new file mode 100644 index 000000000..20350a1c0 Binary files /dev/null and b/doc/docs/en/user-guide/assets/knowledge-base/knowledge-tool2.png differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/summary-knowledge-base.png b/doc/docs/en/user-guide/assets/knowledge-base/summary-knowledge-base.png index 1064785a5..a4f206d67 100644 Binary files a/doc/docs/en/user-guide/assets/knowledge-base/summary-knowledge-base.png and b/doc/docs/en/user-guide/assets/knowledge-base/summary-knowledge-base.png differ diff --git a/doc/docs/en/user-guide/assets/user-management/agent-permission.png b/doc/docs/en/user-guide/assets/user-management/agent-permission.png new file mode 100644 index 000000000..e3d3b7ae0 Binary files /dev/null and b/doc/docs/en/user-guide/assets/user-management/agent-permission.png differ diff --git a/doc/docs/en/user-guide/assets/user-management/invite-code-1.png b/doc/docs/en/user-guide/assets/user-management/invite-code-1.png new file mode 100644 index 000000000..667b4f62b Binary files /dev/null and b/doc/docs/en/user-guide/assets/user-management/invite-code-1.png differ diff --git a/doc/docs/en/user-guide/assets/user-management/invite-code-2.png b/doc/docs/en/user-guide/assets/user-management/invite-code-2.png new file mode 100644 index 000000000..728d42d3a Binary files /dev/null and b/doc/docs/en/user-guide/assets/user-management/invite-code-2.png differ diff --git a/doc/docs/en/user-guide/assets/user-management/kb-permission-1.png b/doc/docs/en/user-guide/assets/user-management/kb-permission-1.png new file mode 100644 index 000000000..1894e6be1 Binary files /dev/null and b/doc/docs/en/user-guide/assets/user-management/kb-permission-1.png differ diff --git a/doc/docs/en/user-guide/assets/user-management/kb-permission-2.png b/doc/docs/en/user-guide/assets/user-management/kb-permission-2.png new file mode 100644 index 000000000..3807c0f51 Binary files /dev/null and b/doc/docs/en/user-guide/assets/user-management/kb-permission-2.png differ diff --git a/doc/docs/en/user-guide/assets/user-management/tenant-usergroup.png b/doc/docs/en/user-guide/assets/user-management/tenant-usergroup.png new file mode 100644 index 000000000..7fdedd630 Binary files /dev/null and b/doc/docs/en/user-guide/assets/user-management/tenant-usergroup.png differ diff --git a/doc/docs/en/user-guide/home-page.md b/doc/docs/en/user-guide/home-page.md index 61d457b18..9433594f3 100644 --- a/doc/docs/en/user-guide/home-page.md +++ b/doc/docs/en/user-guide/home-page.md @@ -20,7 +20,7 @@ The Nexent homepage highlights the core entry points of the platform: ### Left navigation -The left sidebar exposes every major module: +Taking the administrator account as an example, the left sidebar exposes every major module: - **Home Page** – Return to the homepage. - **Start Chat** – Open the chat interface. diff --git a/doc/docs/en/user-guide/knowledge-base.md b/doc/docs/en/user-guide/knowledge-base.md index 5885f2b03..e5e5714ff 100644 --- a/doc/docs/en/user-guide/knowledge-base.md +++ b/doc/docs/en/user-guide/knowledge-base.md @@ -44,6 +44,14 @@ Give every knowledge base a clear summary so agents can pick the right source du ![Content Summary](./assets/knowledge-base/summary-knowledge-base.png) +## 🔧 Using Knowledge Bases + +Nexent supports binding knowledge bases to agents individually. When creating an agent, **enable the knowledge_base_search tool** and select the associated knowledge base. + +Tool 1 + +![Tool 2](./assets/knowledge-base/knowledge-tool2.png) + ## 🔍 Knowledge Base Management ### View Knowledge Bases @@ -55,19 +63,12 @@ Give every knowledge base a clear summary so agents can pick the right source du - Click a knowledge base to see all documents - Click **Details** to view or edit the summary -
- - -
- ### Edit Knowledge Bases 1. **Delete Knowledge Base** - Click **Delete** to the right of the knowledge base row - Confirm the deletion (irreversible) -![Delete Knowledge Base](./assets/knowledge-base/delete-knowledge-base.png) - 2. **Delete or Add Files** - Inside the file list, click **Delete** to remove a document - Use the upload area under the list to add new files diff --git a/doc/docs/en/user-guide/quick-setup.md b/doc/docs/en/user-guide/quick-setup.md index bdf403cf9..9e251e20d 100644 --- a/doc/docs/en/user-guide/quick-setup.md +++ b/doc/docs/en/user-guide/quick-setup.md @@ -33,6 +33,11 @@ Create and configure agents: - **Configure capabilities:** Add collaborative agents and tools. - **Describe logic:** Tell Nexent how the agent should work. +Publish agent: + +- **Publish agent:** Published agents will be visible to selected user groups and listed in Agent Space and the Start Chat selection box. +- **Version management:** Track iteration history of agents, support viewing, rolling back to historical versions, and creating new versions. + Learn more: [Agent Development](./agent-development) ## 🎯 Tips diff --git a/doc/docs/en/user-guide/user-management.md b/doc/docs/en/user-guide/user-management.md index 2f03650cc..0d4b4f81a 100644 --- a/doc/docs/en/user-guide/user-management.md +++ b/doc/docs/en/user-guide/user-management.md @@ -1,37 +1,329 @@ # User Management -User Management is an upcoming Nexent module that will add full user and permission controls. +This page provides a detailed explanation of the Nexent platform's user role system, data visibility scope, operation permissions for various resources, and practical examples of permission configuration. -## 🎯 Coming Features +⚠️ **Important Note**: When deploying v1.8.0 or later for the first time, please pay special attention to the `suadmin` super administrator account information output in the Docker logs. This account has the highest system privileges, and the password is only displayed upon first generation. It cannot be viewed again later, so please be sure to save it securely. -User Management will include: +## 📋 Page Navigation -- **User directory** – View and manage every platform user. -- **Permission controls** – Assign features and resource access per user. -- **Role management** – Create role bundles and apply them quickly. -- **Usage insights** – Monitor activity and adoption metrics. +- [I. Role System](#i-role-system) - Definitions and responsibilities of four core roles +- [II. Tab Access Permissions](#ii-tab-access-permissions) - System pages accessible to each role +- [III. Resource Permission Comparison](#iii-resource-permission-comparison) - Detailed operation permissions for various resources +- [IV. Permission Configuration](#iv-permission-configuration) - Permission management for agents and knowledge bases +- [V. Invitation Code Mechanism](#v-invitation-code-mechanism) - User registration and invitation process +- [VI. Practical Examples](#vi-practical-examples) - Recommendations for permission configuration -## ⏳ Stay Tuned +## I. Role System -We are building a flexible user-management system so you can: +Nexent adopts a Role-Based Access Control (RBAC) model, dividing user scope through the concepts of tenants and user groups: -- Apply fine-grained permission policies. -- Configure roles that match your organization. -- Understand how users collaborate with agents. +### 1.1 What is a Tenant? -## 📢 Follow Updates +- A **Tenant** is the top-level resource isolation unit in the Nexent platform, which can be understood as an independent workspace or organizational unit -Want to know when User Management ships? +- Data between different tenants is completely isolated and invisible to each other. Each tenant can independently create agents, knowledge bases, models, MCPs, etc. -- Join our [Discord community](https://discord.gg/tb5H3S3wyv) for announcements. -- Follow project updates in the repository. +- Only the Super Administrator can manage permissions across tenants and invite tenant administrators -## 🚀 Related Features +### 1.2 What is a User Group? -While waiting for User Management you can: +- A **User Group** is a collection of users within a tenant. User management and permission control can be achieved through user group division +- A user can belong to multiple user groups +- The visibility of resources such as knowledge bases and agents within a tenant is controlled through user groups -1. Manage agents in **[Agent Space](./agent-space)**. -2. Configure models in **[Model Management](./model-management)**. -3. Chat with agents via **[Start Chat](./start-chat)**. +![Tenant and User Group Relationship](./assets/user-management/tenant-usergroup.png) -Need help? Check the **[FAQ](../quick-start/faq)** or open a thread in [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions). \ No newline at end of file +### 1.3 User Roles + +Includes the following four core roles: + +| Role | Responsibility Description | Applicable Scenarios | Role Notes | +| ---- | -------------------------- | -------------------- | ---------- | +| **Super Administrator** | Can create **different tenants** and manage all tenant resources | Platform operation and maintenance personnel | There is only one Super Administrator in the Nexent system. Account credentials are generated during local deployment. Please keep them safe as they cannot be retrieved after logs are cleared | +| **Administrator** | Responsible for **intra-tenant** resource management and permission allocation | Department managers, tenant leaders | A tenant can have multiple administrators, who can only be invited by the Super Administrator | +| **Developer** | Can create and edit agents, knowledge bases, and other resources, but has no management permissions | Developers, product managers | A tenant can have multiple developers who can belong to multiple user groups within the tenant, invited by administrators and the Super Administrator | +| **Regular User** | Can only use platform features without creation and editing permissions | Employees, business personnel | A tenant can have multiple regular users who can belong to multiple user groups within the tenant, invited by administrators and the Super Administrator | + +#### 1.3.1 Super Administrator + +The Super Administrator is responsible for the overall operation and maintenance of the platform. They can create tenants and participate in user permission management within each tenant, but cannot use agents. + +- ✅ Can manage personnel and permissions for all tenants +- ✅ Can view platform-wide monitoring and operation data +- ❌ Cannot directly view specific business data (such as agent conversation content, knowledge base documents, etc.) +- ❌ Cannot create and use agents, knowledge bases, etc. + +#### 1.3.2 Administrator + +The Administrator is the highest permission role within a tenant, responsible for resource management and user management within the tenant, with full platform functionality. + +- ✅ Can manage all users and user groups within the tenant +- ✅ Can view and edit all agents, knowledge bases, and MCPs within the tenant +- ❌ Cannot access data from other tenants + +#### 1.3.3 Developer + +The Developer is a technical role within a tenant, responsible for creating and optimizing technical resources such as agents and knowledge bases. + +- ✅ Can create agents and knowledge bases and set permissions +- ⚠️ For resources created by others, authorization is required to edit +- ❌ Cannot manage users and user groups within the tenant + +#### 1.3.4 Regular User + +Regular Users only have permission to use agents for conversations. + +- ✅ Can use authorized agents for conversations +- ✅ Can view their own usage records and personal information +- ❌ Cannot create or edit agents, knowledge bases + + + +## II. Tab Access Permissions + +| Tab | Super Administrator | Administrator | Developer | Regular User | +| --- | :-----------------: | :-----------: | :-------: | :----------: | +| **Home** | ✅ | ✅ | ✅ | ✅ | +| **Start Chat** | ❌ | ✅ | ✅ | ✅ | +| **Quick Setup** | ❌ | ✅ | ✅ | ✅ | +| **Agent Space** | ❌ | ✅ | ✅ | ❌ | +| **Agent Market** | ❌ | ✅ | ✅ | ❌ | +| **Agent Development** | ❌ | ✅ | ✅ | ❌ | +| **Knowledge Base** | ❌ | ✅ | ✅ | ❌ | +| **MCP Tools** | ❌ | ✅ | ✅ | ❌ | +| **Monitoring** | ✅ | ✅ | ✅ | ❌ | +| **Model Management** | ❌ | ✅ | ✅ | ❌ | +| **Memory Management** | ❌ | ✅ | ✅ | ✅ | +| **Personal Information** | ❌ | ✅ | ✅ | ✅ | +| **Tenant Resources** | ✅ | ✅ | ❌ | ❌ | + + +## III. Resource Permission Comparison + +The following tables show the operation permissions of four roles for various types of resources. Among them: + +- **Super Administrator**: Can manage resources for all tenants (cross-tenant) +- **Administrator/Developer/Regular User**: Can only operate resources within their own tenant + +### 3.1 User and User Group Permissions + +| Operation | Super Administrator | Administrator | Developer | Regular User | +| --------- | :-----------------: | :-----------: | :-------: | :----------: | +| **View Tenant List** | ✅ | ❌ | ❌ | ❌ | +| **Create/Delete Tenant** | ✅ | ❌ | ❌ | ❌ | +| **View User List** | ✅ | ✅ | ❌ | ❌ | +| **Edit User Permissions** | ✅ | ✅ | ❌ | ❌ | +| **Delete User** | ✅ | ✅ | ❌ | ❌ | +| **Assign User Group** | ✅ | ✅ | ❌ | ❌ | +| **View User Group List** | ✅ | ✅ | ❌ | ❌ | +| **Create User Group** | ✅ | ✅ | ❌ | ❌ | +| **Edit User Group** | ✅ | ✅ | ❌ | ❌ | +| **Delete User Group** | ✅ | ✅ | ❌ | ❌ | + +### 3.2 Model Permissions + +| Operation | Super Administrator | Administrator | Developer | Regular User | +| --------- | :-----------------: | :-----------: | :-------: | :----------: | +| **View Model List** | ✅ | ✅ | ✅ | ❌ | +| **Add Model** | ✅ | ✅ | ❌ | ❌ | +| **Edit Model** | ✅ | ✅ | ❌ | ❌ | +| **Delete Model** | ✅ | ✅ | ❌ | ❌ | +| **Test Connectivity** | ✅ | ✅ | ✅ | ❌ | +| **Use Model** | ❌ | ✅ | ✅ | ✅ | + +> 💡 **Note**: Models are tenant-level shared resources. All user groups within the same tenant share the same model pool, with no group-level isolation. Administrators uniformly manage model configurations, while developers and regular users can only use configured models. + +### 3.3 Knowledge Base Permissions + +| Operation | Super Administrator | Administrator | Developer | Regular User | +| --------- | :-----------------: | :-----------: | :-------: | :----------: | +| **View Knowledge Base List** | ✅ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **View Knowledge Base Details** | ❌ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **View Knowledge Base Summary** | ✅ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Create Knowledge Base** | ❌ | ✅ | ✅ | ❌ | +| **Edit Knowledge Base Name and Permissions** | ✅ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Edit Knowledge Base Chunks and Summary** | ❌ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Delete Knowledge Base** | ✅ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Upload/Delete Files** | ❌ | ✅ | 🟡 Self-created/Authorized | ❌ | + +### 3.4 Agent Permissions + +| Operation | Super Administrator | Administrator | Developer | Regular User | +| --------- | :-----------------: | :-----------: | :-------: | :----------: | +| **View Agent List** | ✅ | ✅ | 🟡 Self-created/Authorized | 🟡 Authorized Published Agents | +| **View Agent Info** | ✅ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Edit Agent Config** | ❌ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Manage Agent Versions** | ✅ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Delete Agent** | ✅ | ✅ | 🟡 Self-created/Authorized | ❌ | +| **Use Agent Chat** | ❌ | ✅ | 🟡 Self-created/Authorized | 🟡 Authorized Published Agents | + +### 3.5 MCP Permissions + +| Operation | Super Administrator | Administrator | Developer | Regular User | +| --------- | :-----------------: | :-----------: | :-------: | :----------: | +| **View MCP Tools** | ✅ | ✅ | ✅ | ❌ | +| **Edit MCP Tools** | ✅ | ✅ | ❌ | ❌ | +| **Add MCP Tools** | ✅ | ✅ | ✅ | ❌ | +| **Delete MCP Tools** | ✅ | ✅ | ❌ | ❌ | + +> 💡 **Note**: MCP tools are tenant-level shared resources. All user groups within the same tenant share the same MCP tools, with no group-level isolation. Administrators can add and manage MCP tools, while developers can only add MCP tools. + + +## IV. Permission Configuration + +### 4.1 Agent Permission Settings + +| Permission Level | Description | Applicable Scenario | +| ---------------- | ----------- | ------------------- | +| **Creator Only** | Only the creator (and administrators) can view and edit | Personal development agents | +| **Specified User Group - Read Only** | User groups specified in the agent development page can view and publish, but cannot edit or delete. | Department-specific agents | +
+ Agent Permission Settings +
+ +### 4.2 Knowledge Base Permission Settings + +| Permission Level | Description | Applicable Scenario | +| ---------------- | ----------- | ------------------- | +| **Private** | Only the creator (and administrators) can view and manage | Personal knowledge base | +| **Specified User Group - Read Only** | Specified user groups can view but cannot edit or delete | Department knowledge base | +| **Specified User Group - Editable** | Specified user groups can view and edit, delete | Project team knowledge base | + +
+ Knowledge Base Permission Settings 1 + Knowledge Base Permission Settings 2 +
+ + +## V. Invitation Code Mechanism + +Nexent platform uses an invitation code mechanism to control new user registration, ensuring platform security and controllability. + +### 5.1 Generating Invitation Codes + +- Super Administrators can go to "Tenant Resources" → "Select Tenant" → "Invitation Code" +- Administrators can go directly through "Tenant Resources" → "Invitation Code" +- Click "Create Invitation Code" +- Configure parameters: invitation type (Administrator, Developer, User), invitation code, number of uses, user groups to join, expiration time +- Copy the invitation code and distribute it to relevant personnel + +![Invitation Code 1](./assets/user-management/invite-code-1.png) + +
+ Invitation Code 2 +
+ + +## VI. Practical Examples + +This section uses **XX City People's Hospital - Orthopedics Department** as an example to demonstrate how to build a single-department medical intelligent assistant system on the Nexent platform, as well as the workflow of each role in the system. + +### 6.1 Overall Architecture Design + +#### 6.1.1 Architecture Level Correspondence + +In the scenario of XX City People's Hospital, the correspondence between Nexent platform levels and hospital entities is as follows: + +| Level | Corresponding Entity | Description | +| ----- | -------------------- | ----------- | +| **Super Administrator** | Hospital Information Center/System Administrator | Manages multiple departments (multiple tenants) of the entire hospital | +| **Single Tenant** | Single Department | Such as: Orthopedics, Cardiology, Surgery | +| **User Groups within Tenant** | Professional groups within the department | Such as: Orthopedics Physician Group, Nursing Group, Rehabilitation Group | +| **Members within User Groups** | Specific medical staff/patients | Such as: Chief Physician of Orthopedics, Charge Nurse, Inpatient | + +#### 6.1.2 Definition and Responsibilities of Each Role + +| Role | Corresponding Personnel in Orthopedics Tenant | Core Responsibilities | Data Visibility Scope | +| ---- | --------------------------------------------- | --------------------- | --------------------- | +| **Super Administrator** | Hospital Information Center Administrator | Manages multiple tenants of hospital departments (Orthopedics, Cardiology, Surgery, etc.) | Data of all tenants in the hospital | +| **Administrator** | Chief of Orthopedics | Manages all resources within the Orthopedics tenant (users, agents, knowledge bases, etc.) | All data of this department (this tenant) | +| **Developer** | Chief Physicians and Associate Chief Physicians of Orthopedics Sub-specialties | Creates and edits clinical auxiliary agents, uploads professional materials to knowledge bases | Resources authorized within this department; self-created resources are manageable | +| **Regular User** | Resident Physicians, Nurses, Patients | Uses published agents for work assistance, information queries, health education | Resources authorized for use within this department; view-only, no editing | + +### 6.2 Example User Work Scenarios + +#### Scenario 1: Hospital Information Center Administrator (Super Administrator Role) + +- **User Identity**: Hospital Information Center - System Administrator - Engineer Zhang +- **Role**: Super Administrator +- **Work Requirement**: Manage Nexent platform tenants for all departments of XX City People's Hospital, ensuring normal operation of systems in each department +- **Operation Process in Nexent Platform**: + 1. **Login to System**: Log in to Nexent platform with Super Administrator account + 2. **View Tenant List**: Go to the "Tenant Resources" tab to view tenants of all hospital departments: + - Orthopedics Tenant + - Cardiology Tenant + - Surgery Tenant + - Pediatrics Tenant + - ... (other department tenants) + 3. **Create New Tenant** (e.g., hospital newly opened Rehabilitation Department): + - Click "Create Tenant" + - Fill in tenant name: "XX City People's Hospital - Rehabilitation Department" + - Invite the Chief of Rehabilitation Department as the tenant administrator + +#### Scenario 2: Chief of Orthopedics (Tenant Administrator Role) + +- **User Identity**: Orthopedics - Management - Chief of Orthopedics - Director Liu +- **Role**: Administrator +- **Work Requirement**: Manage all resources within the Orthopedics tenant, create accounts and configure permissions for newly hired spine surgeons +- **Operation Process in Nexent Platform**: + 1. **Login to System**: Log in to Nexent platform with Administrator account + 2. **Enter User Management**: Click the "User Management" tab + 3. **Create New User**: + - Click "Create Invitation Code", configure the group and developer permissions for this doctor + 4. **Assign User Groups**: + - This doctor also needs to join the subsequently created "Spine Surgery New Group" user group, enter "User Management" to edit + 5. **Check Agent Permissions**: + - Enter "Agent Space" to view all existing agents in Orthopedics + - Check if the permission settings for "Spine CT Image Analysis Assistant" are correct (visible and editable to the Spine Surgery Group) + 6. **Manage Knowledge Base**: + - Enter the "Knowledge Base" tab to check the content update status of the Orthopedics knowledge base + - Approve new materials submitted by doctors (such as new surgical cases, research literature, etc.) + +#### Scenario 3: Chief Physician of Spine Surgery (Developer Role) + +- **User Identity**: Orthopedics - Spine Surgery Group - Chief Physician - Dr. Wang +- **Role**: Developer +- **Work Requirement**: Need an intelligent assistant to help analyze spine CT images and provide surgical plan recommendations +- **Operation Process in Nexent Platform**: + 1. **Login to System**: Register account and password with the hospital-assigned invitation code and log in to the corresponding development group + 2. **Enter Agent Development**: Click the "Agent Development" tab + 3. **Create New Agent**: Click "Create Agent", name it "Spine CT Image Analysis Assistant" + 4. **Configure Agent Capabilities**: + - Select "Medical Image Analysis Model" as the base model + - Associate "Spine Surgery Knowledge Base" as the knowledge source + - Configure prompts to train the agent to identify disc herniation, scoliosis and other lesions + 5. **Set Permissions**: + - Visible User Groups: Select "Spine Surgery Group" + - Permission Level: Select "Editable" (allows doctors in the same department to modify and optimize) + 6. **Publish Agent**: Click "Publish", the agent is officially put into use +- **Accessible Data**: + - ✅ Self-created "Spine CT Image Analysis Assistant" agent (editable, version manageable) + - ✅ Other agents authorized for use (such as "Orthopedics Medication Assistant") (view-only) + - ✅ Orthopedics-related knowledge bases (queryable, some can upload materials) + - ❌ Data from other tenants (such as Cardiology) (completely isolated) + +#### Scenario 4: Orthopedics Inpatient (Regular User Role) + +- **User Identity**: Orthopedics - Inpatient Group - Inpatient - Mr. Zhang +- **Role**: Regular User +- **Work Requirement**: After lumbar disc surgery, wants to understand rehabilitation training methods and post-discharge precautions +- **Operation Process in Nexent Platform**: + 1. **Login to System**: Log in to the Nexent platform patient portal + 2. **Enter Patient Services**: Click the "Start Chat" tab + 3. **Select Agent**: Click "Orthopedics Rehabilitation Assistant" + 4. **Initiate Consultation**: + - Input question: "Day 3 after lumbar disc surgery, what rehabilitation training can I do?" + - The agent provides rehabilitation movement videos and guidance suitable for early postoperative period based on the Orthopedics Rehabilitation knowledge base + 5. **Schedule Follow-up**: Schedule a one-month post-discharge outpatient follow-up through the agent +- **Accessible Data**: + - ✅ "Orthopedics Rehabilitation Assistant" agent (view-only) + - ❌ Doctor's diagnostic system (no permission) + - ❌ Other patients' data (completely isolated) + + +## 💡 Get Help + +If you encounter any issues while using the platform: + +- 📖 Check the **[FAQ](../quick-start/faq)** for detailed answers +- 💬 Join our [Discord community](https://discord.gg/tb5H3S3wyv) to connect with other users diff --git a/doc/docs/zh/opensource-memorial-wall.md b/doc/docs/zh/opensource-memorial-wall.md index b3727b902..d63cab96d 100644 --- a/doc/docs/zh/opensource-memorial-wall.md +++ b/doc/docs/zh/opensource-memorial-wall.md @@ -655,3 +655,7 @@ Nexent开发者加油 ::: info haruhikage1 - 2025-1-22 做个人知识管理系统时发现了Nexent,实时文件导入和自动摘要功能直接解决了我整理笔记的痛点!用自然语言就能调整智能体逻辑,不用写复杂的代码,对我这种非AI专业的开发者太友好了。已经推荐给身边的同行,希望项目越做越好! ::: + +::: info feria-tu - 2026-2-21 +一直在寻找企业级简单好用的智能体平台,Nexent是个非常值得一试的好产品,祝Nexent发展越来越好! +::: diff --git a/doc/docs/zh/quick-start/installation.md b/doc/docs/zh/quick-start/installation.md index bee8f9588..64840b8d0 100644 --- a/doc/docs/zh/quick-start/installation.md +++ b/doc/docs/zh/quick-start/installation.md @@ -44,6 +44,9 @@ bash deploy.sh - **终端工具**: 启用 openssh-server 供 AI 智能体执行 shell 命令 - **区域优化**: 中国大陆用户可使用优化的镜像源 + +>⚠️ **重要提示**:首次部署 v1.8.0 及以上版本时,需特别留意 Docker 日志中输出的 `suadmin` 超级管理员账号信息。该账号为系统最高权限账户,密码仅在首次生成时显示,后续无法再次查看,请务必妥善保存。 + ### 3. 访问您的安装 部署成功完成后: diff --git a/doc/docs/zh/user-guide/agent-development.md b/doc/docs/zh/user-guide/agent-development.md index cc00ef115..eebed03cf 100644 --- a/doc/docs/zh/user-guide/agent-development.md +++ b/doc/docs/zh/user-guide/agent-development.md @@ -160,6 +160,19 @@ 调试成功后,可点击右下角"保存"按钮,此智能体将会被保存并出现在智能体列表中。 +### 🐛 版本管理 + +Nexent 支持智能体的版本管理,您可以在调试过程中,保存不同版本的智能体配置。 + +确认智能体配置无误后,您可发布智能体。发布后智能体将在智能体空间、开始问答中可见。 + +![版本管理1](./assets/agent-development/version_management_1.png) + +若需回滚到其他版本,可在版本管理页面点击"回滚"按钮。 + +![版本管理2](./assets/agent-development/version_management_2.png) + + ## 🔧 管理智能体 在左侧智能体列表中,您可对已有的智能体进行以下操作: diff --git a/doc/docs/zh/user-guide/agent-market.md b/doc/docs/zh/user-guide/agent-market.md index 020bbfa1a..47f5b2f5d 100644 --- a/doc/docs/zh/user-guide/agent-market.md +++ b/doc/docs/zh/user-guide/agent-market.md @@ -38,12 +38,18 @@ ![智能体市场下载](./assets/agent-market/agent-market-download.png) -### 2️⃣ 配置字段 +### 2️⃣ 配置本地工具 -🔑 依据提示补充工具许可 +🔑 依据提示补充本地工具的许可 ![智能体市场下载2](./assets/agent-market/agent-market-download2.png) +### 3️⃣ 配置外部 MCP 工具 + +🔑 依据提示补充 MCP 工具的许可 + +![智能体市场下载3](./assets/agent-market/agent-market-download3.png) + 安装完成后,您的智能体会在 **[智能体空间](./agent-space)** 准备好 ## 📢 分享您的创作 diff --git a/doc/docs/zh/user-guide/assets/agent-development/duplicated_import.png b/doc/docs/zh/user-guide/assets/agent-development/duplicated_import.png index e4d51cad5..588fb0f52 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-development/duplicated_import.png and b/doc/docs/zh/user-guide/assets/agent-development/duplicated_import.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png b/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png index 0dd5eef50..b9169dbcd 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png and b/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-development/version_management_1.png b/doc/docs/zh/user-guide/assets/agent-development/version_management_1.png new file mode 100644 index 000000000..a945374c5 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-development/version_management_1.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-development/version_management_2.png b/doc/docs/zh/user-guide/assets/agent-development/version_management_2.png new file mode 100644 index 000000000..baa7fe7ea Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-development/version_management_2.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market-download.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download.png index d874638f4..b8617829e 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-market/agent-market-download.png and b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market-download2.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download2.png index d9f88e409..6604b5bfd 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-market/agent-market-download2.png and b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download2.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market-download3.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download3.png new file mode 100644 index 000000000..714db1470 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download3.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market.png index 9b4c0811b..e136ddcb6 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-market/agent-market.png and b/doc/docs/zh/user-guide/assets/agent-market/agent-market.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-space/agent-space.png b/doc/docs/zh/user-guide/assets/agent-space/agent-space.png index 4576a5767..61bd31553 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-space/agent-space.png and b/doc/docs/zh/user-guide/assets/agent-space/agent-space.png differ diff --git a/doc/docs/zh/user-guide/assets/home-page/homepage.png b/doc/docs/zh/user-guide/assets/home-page/homepage.png index 845b31a57..a41616d3b 100644 Binary files a/doc/docs/zh/user-guide/assets/home-page/homepage.png and b/doc/docs/zh/user-guide/assets/home-page/homepage.png differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/create-knowledge-base.png b/doc/docs/zh/user-guide/assets/knowledge-base/create-knowledge-base.png index 29f0dbc03..3731860ee 100644 Binary files a/doc/docs/zh/user-guide/assets/knowledge-base/create-knowledge-base.png and b/doc/docs/zh/user-guide/assets/knowledge-base/create-knowledge-base.png differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/delete-knowledge-base.png b/doc/docs/zh/user-guide/assets/knowledge-base/delete-knowledge-base.png deleted file mode 100644 index 6871731af..000000000 Binary files a/doc/docs/zh/user-guide/assets/knowledge-base/delete-knowledge-base.png and /dev/null differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-file-list.png b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-file-list.png deleted file mode 100644 index f930878b2..000000000 Binary files a/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-file-list.png and /dev/null differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-permission.png b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-permission.png new file mode 100644 index 000000000..8394ab9f9 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-permission.png differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-summary.png b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-summary.png deleted file mode 100644 index eb6acb792..000000000 Binary files a/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-base-summary.png and /dev/null differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-tool.png b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-tool.png new file mode 100644 index 000000000..4359a66f9 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-tool.png differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-tool2.png b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-tool2.png new file mode 100644 index 000000000..ac0369c3b Binary files /dev/null and b/doc/docs/zh/user-guide/assets/knowledge-base/knowledge-tool2.png differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/summary-knowledge-base.png b/doc/docs/zh/user-guide/assets/knowledge-base/summary-knowledge-base.png index b36303da2..306a1b295 100644 Binary files a/doc/docs/zh/user-guide/assets/knowledge-base/summary-knowledge-base.png and b/doc/docs/zh/user-guide/assets/knowledge-base/summary-knowledge-base.png differ diff --git a/doc/docs/zh/user-guide/assets/user-management/agent-permission.png b/doc/docs/zh/user-guide/assets/user-management/agent-permission.png new file mode 100644 index 000000000..22c797553 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/user-management/agent-permission.png differ diff --git a/doc/docs/zh/user-guide/assets/user-management/invite-code-1.png b/doc/docs/zh/user-guide/assets/user-management/invite-code-1.png new file mode 100644 index 000000000..1ee8302ff Binary files /dev/null and b/doc/docs/zh/user-guide/assets/user-management/invite-code-1.png differ diff --git a/doc/docs/zh/user-guide/assets/user-management/invite-code-2.png b/doc/docs/zh/user-guide/assets/user-management/invite-code-2.png new file mode 100644 index 000000000..5e84b0d30 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/user-management/invite-code-2.png differ diff --git a/doc/docs/zh/user-guide/assets/user-management/kb-permission-1.png b/doc/docs/zh/user-guide/assets/user-management/kb-permission-1.png new file mode 100644 index 000000000..7d399369d Binary files /dev/null and b/doc/docs/zh/user-guide/assets/user-management/kb-permission-1.png differ diff --git a/doc/docs/zh/user-guide/assets/user-management/kb-permission-2.png b/doc/docs/zh/user-guide/assets/user-management/kb-permission-2.png new file mode 100644 index 000000000..7cd990eca Binary files /dev/null and b/doc/docs/zh/user-guide/assets/user-management/kb-permission-2.png differ diff --git a/doc/docs/zh/user-guide/assets/user-management/tenant-usergroup.png b/doc/docs/zh/user-guide/assets/user-management/tenant-usergroup.png new file mode 100644 index 000000000..6033b9b84 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/user-management/tenant-usergroup.png differ diff --git a/doc/docs/zh/user-guide/home-page.md b/doc/docs/zh/user-guide/home-page.md index 5e24343ac..0a3a82957 100644 --- a/doc/docs/zh/user-guide/home-page.md +++ b/doc/docs/zh/user-guide/home-page.md @@ -22,7 +22,7 @@ Nexent首页展示了平台的核心功能,为您提供快速入口: ### ➡️ 左侧导航栏 -页面左侧提供了完整的导航栏,包含以下模块: +以管理员账号为例,页面左侧提供了完整的导航栏,包含以下模块: - **首页**:返回平台首页 - **开始问答**:进入对话页面,选择智能体进行交互 @@ -35,7 +35,9 @@ Nexent首页展示了平台的核心功能,为您提供快速入口: - **监控与运维**:实时掌控智能体的运行状态(即将上线) - **模型管理**:管理应用信息与模型配置,连接你需要的 AI 能力 - **记忆管理**:控制智能体的长期记忆,让对话更高效 -- **用户管理**:管为团队提供统一的用户、角色与权限控制(即将上线) +- **个人信息**:查看和管理您的个人信息,如邮箱、角色、用户组等 +- **租户资源**:查看和管理您的租户资源,如用户、模型、知识库、智能体等 + 页面右上角支持**语言切换**(简体中文/English) diff --git a/doc/docs/zh/user-guide/knowledge-base.md b/doc/docs/zh/user-guide/knowledge-base.md index c28f878e1..fa98eac62 100644 --- a/doc/docs/zh/user-guide/knowledge-base.md +++ b/doc/docs/zh/user-guide/knowledge-base.md @@ -44,29 +44,34 @@ Nexent支持多种文件格式,包括: ![内容总结](./assets/knowledge-base/summary-knowledge-base.png) +## 🔧 使用知识库 + +Nexent支持知识库与智能体单独绑定,在创建智能体时,**启用knowledge_base_search工具**,并选择关联的知识库 +工具1 +![工具2](./assets/knowledge-base/knowledge-tool2.png) + ## 🔍 知识库管理 ### 查看知识库 1. **知识库列表** - 知识库页面左侧展示了所有已创建的知识库 - - 显示知识库名称、文件数量、创建时间等信息 + - 知识库列表处支持对知识库来源和向量模型的筛选 + - 显示知识库名称、文件数量、创建时间、用户组等信息 + +> 点击编辑,可管理知识库的名称、可见的用户组及组内权限 + +知识库权限 2. **知识库详情** - 点击知识库名称,可查看知识库中全部文档信息 - 点击“详细内容”,可查看知识库的内容总结 -
- - -
- ### 编辑知识库 1. **删除知识库** - 点击知识库名称右侧“删除”按钮 - 确认删除操作(此操作不可恢复) -![删除知识库](./assets/knowledge-base/delete-knowledge-base.png) 2. **删除或新增文件** - 点击知识库名称,在文件列表中点击“删除”按钮,可从知识库中删除文件 diff --git a/doc/docs/zh/user-guide/quick-setup.md b/doc/docs/zh/user-guide/quick-setup.md index 44d00b335..96fb26875 100644 --- a/doc/docs/zh/user-guide/quick-setup.md +++ b/doc/docs/zh/user-guide/quick-setup.md @@ -33,6 +33,11 @@ - **配置能力**:设置协作智能体和工具 - **描述业务逻辑**:定义智能体的工作方式 +发布智能体: + +- **发布智能体**:已发布的智能体将在选中的用户组内可见,并列于智能体空间与开始问答选择框中 +- **版本管理**:跟踪智能体的迭代历史,支持查看、回滚至历史版本及创建新版本 + 详细内容请参考:[智能体开发](./agent-development) ## 🎯 使用建议 diff --git a/doc/docs/zh/user-guide/start-chat.md b/doc/docs/zh/user-guide/start-chat.md index d428e5a3a..4e9dce692 100644 --- a/doc/docs/zh/user-guide/start-chat.md +++ b/doc/docs/zh/user-guide/start-chat.md @@ -9,6 +9,7 @@ 在开始对话之前,您需要先选择一个智能体。 1. **查看可用智能体** + - 已发布的智能体可用于对话 - 在对话框左下角找到智能体选择下拉框 - 点击下拉框查看所有可用的智能体列表 - 每个智能体都会显示名称和功能描述 diff --git a/doc/docs/zh/user-guide/user-management.md b/doc/docs/zh/user-guide/user-management.md index 24b45af00..ddffc1abe 100644 --- a/doc/docs/zh/user-guide/user-management.md +++ b/doc/docs/zh/user-guide/user-management.md @@ -1,39 +1,325 @@ # 用户管理 -用户管理是Nexent平台即将推出的功能模块,将为您提供完整的用户管理能力。 +本页面详细说明 Nexent 平台的用户角色体系、数据可见性范围、各类资源的操作权限,并分享权限配置的实践案例。 -## 🎯 功能预告 +⚠️ **重要提示**:首次部署 v1.8.0 及以上版本时,需特别留意 Docker 日志中输出的 `suadmin` 超级管理员账号信息。该账号为系统最高权限账户,密码仅在首次生成时显示,后续无法再次查看,请务必妥善保存。 -用户管理将提供以下功能: +## 📋 页面导航 -- **用户列表**:查看和管理所有系统用户 -- **用户权限**:配置用户的访问权限和功能权限 -- **用户角色**:管理用户角色和权限组 -- **用户统计**:查看用户使用情况和统计数据 +- [一、角色体系](#一角色体系) - 四种核心角色的定义与职责 +- [二、页签访问权限](#二页签访问权限) - 各角色可访问的系统页面 +- [三、资源权限对照表](#三资源权限对照表) - 详细的各种资源操作权限 +- [四、权限配置](#四权限配置) - 智能体与知识库的权限管理 +- [五、邀请码机制](#五邀请码机制) - 用户注册与邀请流程 +- [六、实践案例](#六实践案例) - 权限配置的建议 -## ⏳ 敬请期待 +## 一、角色体系 -用户管理功能正在开发中,敬请期待! +Nexent 采用基于角色的访问控制(RBAC)模型,通过租户与用户组的概念划分用户范围: -我们正在努力为您打造一个完善、灵活的用户管理体系,让您能够: +### 1.1 什么是租户? -- 精细化管理用户权限 -- 灵活配置用户角色 -- 全面了解用户使用情况 +- **租户**是 Nexent 平台中最上层的资源隔离单位,可以理解为一个独立的工作空间或组织单元 -## 📢 获取最新动态 +- 不同租户之间,数据完全隔离、互不可见,每个租户内可独立创建智能体、知识库、模型、MCP等 -想要第一时间了解用户管理功能的上线信息? +- 仅超级管理员可跨租户权限管理,邀请租户管理员 -- 加入我们的 [Discord 社区](https://discord.gg/tb5H3S3wyv) 获取最新动态 -- 关注项目更新,了解开发进展 +### 1.2 什么是用户组? -## 🚀 相关功能 +- **用户组**是某租户内的用户集合,可通过用户组划分来实现对用户的管理和权限控制 +- 一个用户也可以属于多个用户组 +- 租户内的知识库、智能体等资源可见性,通过用户组控制 -在等待用户管理功能上线期间,您可以: +![租户与用户组关系](./assets/user-management/tenant-usergroup.png) -1. 在 **[智能体空间](./agent-space)** 中管理您的智能体 -2. 通过 **[模型管理](./model-management)** 配置系统模型 -3. 在 **[开始问答](./start-chat)** 中体验平台功能 +### 1.3 用户角色 -如果您有任何建议或想法,欢迎通过 [Discord 社区](https://discord.gg/tb5H3S3wyv) 与我们分享! +包含以下四个核心角色: + +| 角色 | 职责描述 | 适用场景 | 角色备注 | +| -------------- | ---------------------------------------------- | -------------------- | ------------------------------------------------------------ | +| **超级管理员** | 可创建**不同租户**,管理所有租户资源 | 平台运维人员 | Nexent系统只有一个超级管理员,于本地部署时生成账号密码,请务必留存,日志关闭后无法找回 | +| **管理员** | 负责**租户内**的资源管理和权限分配 | 部门经理、租户负责人 | 同一租户可拥有多个管理员,只能由超级管理员邀请 | +| **开发者** | 可创建和编辑智能体、知识库等资源,但无管理权限 | 开发人员、产品经理 | 同一租户下可拥有多个开发者,可属于租户下多个用户组,由管理员和超级管理员邀请 | +| **普通用户** | 仅可使用平台提供的各项功能,无创建和编辑权限 | 员工、业务人员 | 同一租户下可拥有多个普通用户,可属于租户下多个用户组,由管理员和超级管理员邀请 | + +#### 1.3.1 超级管理员 + +超级管理员负责平台的整体运维,可以创建租户并参与各租户内的用户权限管理,但无法使用智能体 + +- ✅ 可以管理所有租户的人员及权限 +- ✅ 可以查看全平台监控与运维数据 +- ❌ 不能直接查看具体业务数据(如智能体对话内容、知识库文档等) +- ❌ 不能创建和使用智能体、知识库等 + +#### 1.3.2 管理员 + +管理员是租户内的最高权限角色,负责租户内的资源管理和用户管理,拥有平台全部功能 + +- ✅ 可以管理租户内的所有用户与用户组 +- ✅ 可以查看并编辑租户内所有智能体、知识库、MCP +- ❌ 不能访问其他租户的数据 + +#### 1.3.3 开发者 + +开发者是租户内的技术角色,负责创建和优化智能体、知识库等技术资源 + +- ✅ 可以创建智能体和知识库,并设置权限 +- ⚠️ 对他人创建的资源,需要被授权才能编辑 +- ❌ 不能管理租户内的用户和用户组 + +#### 1.3.4 普通用户 + +普通用户仅有使用智能体进行对话的权限 + +- ✅ 可以使用被授权的智能体进行对话 +- ✅ 可以查看自己的使用记录和个人信息 +- ❌ 不能创建或编辑智能体、知识库 + + +## 二、页签访问权限 + +| 页签 | 超级管理员 | 管理员 | 开发者 | 普通用户 | +| -------------- | :--------: | :----: | :----: | :------: | +| **首页** | ✅ | ✅ | ✅ | ✅ | +| **开始问答** | ❌ | ✅ | ✅ | ✅ | +| **快速配置** | ❌ | ✅ | ✅ | ✅ | +| **智能体空间** | ❌ | ✅ | ✅ | ❌ | +| **智能体市场** | ❌ | ✅ | ✅ | ❌ | +| **智能体开发** | ❌ | ✅ | ✅ | ❌ | +| **知识库** | ❌ | ✅ | ✅ | ❌ | +| **MCP工具** | ❌ | ✅ | ✅ | ❌ | +| **监控与运维** | ✅ | ✅ | ✅ | ❌ | +| **模型管理** | ❌ | ✅ | ✅ | ❌ | +| **记忆管理** | ❌ | ✅ | ✅ | ✅ | +| **个人信息** | ❌ | ✅ | ✅ | ✅ | +| **租户资源** | ✅ | ✅ | ❌ | ❌ | + + +## 三、资源权限对照表 + +以下表格展示了四种角色对各类资源的操作权限。其中: + +- **超级管理员**:可管理所有租户的资源(跨租户) +- **管理员/开发者/普通用户**:仅可操作本租户内的资源 + +### 3.1 用户与用户组权限 + +| 操作 | 超级管理员 | 管理员 | 开发者 | 普通用户 | +| ------------------ | :--------: | :----: | :----: | :------: | +| **查看租户列表** | ✅ | ❌ | ❌ | ❌ | +| **创建/删除租户** | ✅ | ❌ | ❌ | ❌ | +| **查看用户列表** | ✅ | ✅ | ❌ | ❌ | +| **编辑用户权限** | ✅ | ✅ | ❌ | ❌ | +| **删除用户** | ✅ | ✅ | ❌ | ❌ | +| **分配用户组** | ✅ | ✅ | ❌ | ❌ | +| **查看用户组列表** | ✅ | ✅ | ❌ | ❌ | +| **创建用户组** | ✅ | ✅ | ❌ | ❌ | +| **编辑用户组** | ✅ | ✅ | ❌ | ❌ | +| **删除用户组** | ✅ | ✅ | ❌ | ❌ | + +### 3.2 模型权限 + +| 操作 | 超级管理员 | 管理员 | 开发者 | 普通用户 | +| ---------------- | :--------: | :----: | :----: | :------: | +| **查看模型列表** | ✅ | ✅ | ✅ | ❌ | +| **添加模型** | ✅ | ✅ | ❌ | ❌ | +| **编辑模型** | ✅ | ✅ | ❌ | ❌ | +| **删除模型** | ✅ | ✅ | ❌ | ❌ | +| **测试连通性** | ✅ | ✅ | ✅ | ❌ | +| **使用模型** | ❌ | ✅ | ✅ | ✅ | + +> 💡 **说明**:模型为租户级共享资源,同租户内所有用户组共享相同的模型池,不存在组间隔离。管理员统一管理模型配置,开发者和普通用户仅能使用已配置的模型。 + +### 3.3 知识库权限 + +| 操作 | 超级管理员 | 管理员 | 开发者 | 普通用户 | +| ------------------------ | :--------: | :----: | :---------------: | :------: | +| **查看知识库列表** | ✅ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **查看知识库详情** | ❌ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **查看知识库总结** | ✅ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **创建知识库** | ❌ | ✅ | ✅ | ❌ | +| **编辑知识库名称和权限** | ✅ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **编辑知识库分块、总结** | ❌ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **删除知识库** | ✅ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **上传/删除文件** | ❌ | ✅ | 🟡 自己创建/被授权 | ❌ | + +### 3.4 智能体权限 + +| 操作 | 超级管理员 | 管理员 | 开发者 | 普通用户 | +| ------------------ | :--------: | :----: | :---------------: | :--------------------: | +| **查看智能体列表** | ✅ | ✅ | 🟡 自己创建/被授权 | 🟡 被授权的已发布智能体 | +| **查看智能体信息** | ✅ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **编辑智能体配置** | ❌ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **管理智能体版本** | ✅ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **删除智能体** | ✅ | ✅ | 🟡 自己创建/被授权 | ❌ | +| **使用智能体对话** | ❌ | ✅ | 🟡 自己创建/被授权 | 🟡 被授权的已发布智能体 | + +### 3.5 MCP权限 + +| 操作 | 超级管理员 | 管理员 | 开发者 | 普通用户 | +| --------------- | :--------: | :----: | :----: | :------: | +| **查看MCP工具** | ✅ | ✅ | ✅ | ❌ | +| **编辑MCP工具** | ✅ | ✅ | ❌ | ❌ | +| **添加MCP工具** | ✅ | ✅ | ✅ | ❌ | +| **删除MCP工具** | ✅ | ✅ | ❌ | ❌ | + +> 💡 **说明**:MCP 工具为租户级共享资源,同租户内所有用户组共享相同的 MCP 工具,不存在组间隔离。管理员可添加和管理 MCP 工具,开发者仅能添加 MCP 工具。 + + +## 四、权限配置 + +### 4.1 智能体权限设置 + +| 权限级别 | 说明 | 适用场景 | +| ------------------- | ------------------------------------------------------------ | ---------------- | +| **仅创建者可见** | 只有创建者(和管理员)可以查看和编辑 | 个人开发的智能体 | +| **指定用户组-只读** | 智能体开发页面指定用户组,则用户组内开发者可见、可发布,但不可编辑、不可删除。 | 部门专用智能体 | + +智能体权限设置 + +### 4.2 知识库权限设置 + +| 权限级别 | 说明 | 适用场景 | +| --------------------- | ------------------------------------ | -------------- | +| **私有** | 只有创建者(和管理员)可以查看和管理 | 个人知识库 | +| **指定用户组-只读** | 指定用户组可见,但不可编辑、删除 | 部门知识库 | +| **指定用户组-可编辑** | 指定用户组可见且可编辑、删除 | 项目团队知识库 | + +
+ 知识库权限设置1 + 知识库权限设置2 +
+ + +## 五、邀请码机制 + +Nexent 平台采用邀请码机制控制新用户注册,确保平台的安全性和可控性。 + +### 5.1 生成邀请码 + +- 超级管理员可进入「租户资源」→「选择租户」→「邀请码」 +- 管理员则直接通过「租户资源」→「邀请码」 +- 点击「创建邀请码」 +- 配置参数:邀请类型(管理员、开发者、用户)、邀请码、可使用次数、邀请进入的用户组、到期时间 +- 复制邀请码分发给相关人员 + +![邀请码1](./assets/user-management/invite-code-1.png) + +邀请码2 + + +## 六、实践案例 + +本节以**XX市人民医院-骨科**为例,展示如何在 Nexent 平台中构建单科室的医疗智能助手系统,以及各角色在系统中的工作流程。 + +### 6.1 整体架构设计 + +#### 6.1.1 架构层级对应关系 + +在XX市人民医院场景下,Nexent平台的层级与医院实体对应关系如下: + +| 层级 | 对应实体 | 说明 | +| ------------------ | ----------------------- | ------------------------------------ | +| **超级管理员** | 医院信息中心/系统管理员 | 管理整个医院的多个科室(多个租户) | +| **单个租户** | 单个科室 | 如:骨科、心内科、外科 | +| **租户内的用户组** | 科室内的专业小组 | 如:骨科医师组、护理组、康复组 | +| **用户组内的成员** | 具体医护人员/患者 | 如:骨科主任医师、责任护士、住院患者 | + +#### 6.1.2 各角色的定义与职责 + +| 角色 | 在骨科租户中的对应人员 | 核心职责 | 数据可见范围 | +| -------------- | -------------------------------- | ------------------------------------------------------ | ------------------------------------------ | +| **超级管理员** | 医院信息中心管理员 | 管理医院各科室的多个租户(骨科、心内科、外科等) | 全院所有租户的数据 | +| **管理员** | 骨科主任 | 管理骨科租户内的所有资源(用户、智能体、知识库等) | 本科室(本租户)的所有数据 | +| **开发者** | 骨科各亚专业主任医师、副主任医师 | 创建和编辑临床辅助智能体、上传专业资料到知识库 | 本科室内被授权的资源,自己创建的资源可管理 | +| **普通用户** | 住院医师、护士、患者 | 使用已发布的智能体进行工作辅助、查询信息、接受健康教育 | 本科室内被授权使用的资源,仅可使用不可编辑 | + +### 6.2 示例用户工作场景 + +#### 场景1:医院信息中心管理员(超级管理员角色) + +- **用户身份**:医院信息中心-系统管理员-张工 +- **角色**:超级管理员 +- **工作需求**:管理XX市人民医院所有科室的Nexent平台租户,确保各科室系统正常运行 +- **在Nexent平台中的操作流程**: + 1. **登录系统**:使用超级管理员账号登录Nexent平台 + 2. **查看租户列表**:进入「租户资源」页签,查看全院所有科室的租户: + - 骨科租户 + - 心内科租户 + - 外科租户 + - 儿科租户 + - ...(其他科室租户) + 3. **创建新租户**(如医院新开设了康复科): + - 点击「创建租户」 + - 填写租户名称:「XX市人民医院-康复科」 + - 邀请康复科主任为租户管理员 + +#### 场景2:骨科主任(租户管理员角色) + +- **用户身份**:骨科-管理层-骨科主任-刘主任 +- **角色**:管理员 +- **工作需求**:管理骨科租户内的所有资源,为新入职的脊柱外科医生创建账号并配置权限 +- **在Nexent平台中的操作流程**: + 1. **登录系统**:使用管理员账号登录Nexent平台 + 2. **进入用户管理**:点击「用户管理」页签 + 3. **创建新用户**: + - 点击「创建邀请码」,为该医生配置邀请进入的组以及开发者权限 + 4. **分配用户组**: + - 该医生还需进入后续新创建的「脊柱外科新组」用户组,进入「用户管理」编辑 + 5. **检查智能体权限**: + - 进入「智能体空间」,查看骨科现有的所有智能体 + - 检查「脊柱CT影像分析助手」的权限设置是否正确(对脊柱外科组可见、可编辑) + 6. **管理知识库**: + - 进入「知识库」页签,查看骨科知识库的内容更新情况 + - 审批医生提交的新资料(如新的手术案例、研究文献等) + +#### 场景3:脊柱外科主任医师(开发者角色) + +- **用户身份**:骨科-脊柱外科组-主任医师-王医生 +- **角色**:开发者 +- **工作需求**:需要一个智能助手帮助分析脊柱CT影像,提供手术方案建议 +- **在Nexent平台中的操作流程**: + 1. **登录系统**:使用医院分配的邀请码注册账号密码登录并进入对应的开发组 + 2. **进入智能体开发**:点击「智能体开发」页签 + 3. **创建新智能体**:点击「创建智能体」,命名为「脊柱CT影像分析助手」 + 4. **配置智能体能力**: + - 选择「医学影像分析模型」作为基础模型 + - 关联「脊柱外科知识库」作为知识来源 + - 配置提示词,训练智能体识别椎间盘突出、脊柱侧弯等病变 + 5. **设置权限**: + - 可见用户组:选择「脊柱外科组」 + - 权限级别:选择「可编辑」(允许同科室医生修改优化) + 6. **发布智能体**:点击「发布」,智能体正式投入使用 +- **可访问的数据**: + - ✅ 自己创建的「脊柱CT影像分析助手」智能体(可编辑、可管理版本) + - ✅ 被授权使用的其他智能体(如「骨科用药助手」)(仅可使用) + - ✅ 骨科相关的知识库(可查询,部分可上传资料) + - ❌ 其他租户(如心内科)的数据(完全隔离) + +#### 场景4:骨科住院患者(普通用户角色) + +- **用户身份**:骨科-住院患者组-住院患者-张先生 +- **角色**:普通用户 +- **工作需求**:腰椎间盘术后,想了解康复训练方法和出院后注意事项 +- **在Nexent平台中的操作流程**: + 1. **登录系统**:登录Nexent平台患者端 + 2. **进入患者服务**:点击「开始问答」页签 + 3. **选择智能体**:点击「骨科康复助手」 + 4. **发起咨询**: + - 输入问题:「腰椎间盘术后第3天,可以做哪些康复训练?」 + - 智能体根据骨科康复知识库,提供适合术后早期的康复动作视频和指导 + 5. **预约随访**:通过智能体预约出院后1个月的门诊随访 +- **可访问的数据**: + - ✅ 「骨科康复助手」智能体(仅可使用) + - ❌ 医生的诊断系统(无权限) + - ❌ 其他患者的数据(完全隔离) + +### 获取帮助 + +如果您在使用过程中遇到任何问题: + +- 📖 查看 **[常见问题](../quick-start/faq)** 获取详细解答 +- 💬 加入我们的 [Discord 社区](https://discord.gg/tb5H3S3wyv) 与其他用户交流 +- 🆘 联系技术支持获取专业帮助 \ No newline at end of file diff --git a/docker/init.sql b/docker/init.sql index 73c35ac77..9c3fac948 100644 --- a/docker/init.sql +++ b/docker/init.sql @@ -296,7 +296,7 @@ COMMENT ON COLUMN nexent.ag_tool_info_t.delete_flag IS 'Whether it is deleted. O -- Create the ag_tenant_agent_t table in the nexent schema CREATE TABLE IF NOT EXISTS nexent.ag_tenant_agent_t ( - agent_id INTEGER NOT NULL, + agent_id SERIAL NOT NULL, name VARCHAR(100), display_name VARCHAR(100), description VARCHAR, @@ -487,6 +487,7 @@ CREATE TABLE IF NOT EXISTS nexent.mcp_record_t ( mcp_server VARCHAR(500), status BOOLEAN DEFAULT NULL, container_id VARCHAR(200) DEFAULT NULL, + authorization_token VARCHAR(500) DEFAULT NULL, create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, created_by VARCHAR(100), @@ -505,6 +506,7 @@ COMMENT ON COLUMN nexent.mcp_record_t.mcp_name IS 'MCP name'; COMMENT ON COLUMN nexent.mcp_record_t.mcp_server IS 'MCP server address'; COMMENT ON COLUMN nexent.mcp_record_t.status IS 'MCP server connection status, true=connected, false=disconnected, null=unknown'; COMMENT ON COLUMN nexent.mcp_record_t.container_id IS 'Docker container ID for MCP service, NULL for non-containerized MCP'; +COMMENT ON COLUMN nexent.mcp_record_t.authorization_token IS 'Authorization token for MCP server authentication (e.g., Bearer token)'; COMMENT ON COLUMN nexent.mcp_record_t.create_time IS 'Creation time, audit field'; COMMENT ON COLUMN nexent.mcp_record_t.update_time IS 'Update time, audit field'; COMMENT ON COLUMN nexent.mcp_record_t.created_by IS 'Creator ID, audit field'; @@ -996,7 +998,7 @@ INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_ (184, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'CREATE'), (185, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'READ'), (186, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'UPDATE'), -(187, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'DELETE'), +(187, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'DELETE'); -- Insert SPEED role user into user_tenant_t table if not exists INSERT INTO nexent.user_tenant_t (user_id, tenant_id, user_role, user_email, created_by, updated_by) diff --git a/docker/scripts/sync_user_supabase2pg.py b/docker/scripts/sync_user_supabase2pg.py index 43c3b2e15..ecf8259f9 100644 --- a/docker/scripts/sync_user_supabase2pg.py +++ b/docker/scripts/sync_user_supabase2pg.py @@ -317,6 +317,10 @@ def determine_user_role(user_id, tenant_id, user_email): if user_email and user_email.lower() == LEGACY_ADMIN_EMAIL.lower(): return "ADMIN" + # Rule 3: If tenant_id is empty, set it to SU + if not tenant_id: + return "SU" + # Default: USER return "USER" diff --git a/docker/sql/v1.8.0.1_0224_init_agent_id_seq.sql b/docker/sql/v1.8.0.1_0224_init_agent_id_seq.sql new file mode 100644 index 000000000..67b6bd091 --- /dev/null +++ b/docker/sql/v1.8.0.1_0224_init_agent_id_seq.sql @@ -0,0 +1,6 @@ +CREATE SEQUENCE IF NOT EXISTS "nexent"."ag_tenant_agent_t_agent_id_seq" +INCREMENT 1 +MINVALUE 1 +MAXVALUE 2147483647 +START 1 +CACHE 1; \ No newline at end of file diff --git a/docker/sql/v1.8.0.1_0225_delete_empty_tenant.sql b/docker/sql/v1.8.0.1_0225_delete_empty_tenant.sql new file mode 100644 index 000000000..0c0bb8a0b --- /dev/null +++ b/docker/sql/v1.8.0.1_0225_delete_empty_tenant.sql @@ -0,0 +1,10 @@ +-- Delete erroneous tenant with empty tenant_id and all related data +-- This script removes records where tenant_id is empty string from tenant_config_t and tenant_group_info_t + +-- 1. Force delete all records in tenant_config_t where tenant_id is empty string +DELETE FROM nexent.tenant_config_t +WHERE tenant_id = ''; + +-- 2. Force delete all records in tenant_group_info_t where tenant_id is empty string +DELETE FROM nexent.tenant_group_info_t +WHERE tenant_id = ''; diff --git a/docker/sql/v1.8.0.1_0226_add_authorization_token_to_mcp_record_t.sql b/docker/sql/v1.8.0.1_0226_add_authorization_token_to_mcp_record_t.sql new file mode 100644 index 000000000..f9ce4ba73 --- /dev/null +++ b/docker/sql/v1.8.0.1_0226_add_authorization_token_to_mcp_record_t.sql @@ -0,0 +1,10 @@ +-- Migration: Add authorization_token column to mcp_record_t table +-- Date: 2025-03-01 +-- Description: Add authorization_token field to support MCP server authentication + +-- Add authorization_token column to mcp_record_t table +ALTER TABLE nexent.mcp_record_t +ADD COLUMN IF NOT EXISTS authorization_token VARCHAR(500) DEFAULT NULL; + +-- Add comment to the column +COMMENT ON COLUMN nexent.mcp_record_t.authorization_token IS 'Authorization token for MCP server authentication (e.g., Bearer token)'; diff --git a/frontend/app/[locale]/agents/AgentVersionCard.tsx b/frontend/app/[locale]/agents/AgentVersionCard.tsx index bc38ad7c8..aedf0b8eb 100644 --- a/frontend/app/[locale]/agents/AgentVersionCard.tsx +++ b/frontend/app/[locale]/agents/AgentVersionCard.tsx @@ -14,7 +14,8 @@ import { AlertTriangle, EllipsisVertical, Trash2, - ArchiveRestore + ArchiveRestore, + Edit } from "lucide-react"; import { useTranslation } from "react-i18next"; import { @@ -26,10 +27,6 @@ import { Descriptions, DescriptionsProps, Modal, - Space, - Spin, - Empty, - Table, Dropdown, theme } from "antd"; @@ -48,19 +45,46 @@ import { useAuthorizationContext } from "@/components/providers/AuthorizationPro import log from "@/lib/logger"; import { message } from "antd"; import { useQueryClient } from "@tanstack/react-query"; +import AgentVersionCompareModal from "./versions/AgentVersionCompareModal"; +import AgentVersionPubulishModal from "./versions/AgentVersionPubulishModal"; const { Text } = Typography; -const formatter = new Intl.DateTimeFormat('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false +const formatter = new Intl.DateTimeFormat("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, }); +/** + * Format UTC time string from backend to local time string based on user timezone + */ +function formatUtcToLocal(dateTimeStr?: string | null) { + if (!dateTimeStr) { + return ""; + } + + // Detect whether the string already contains timezone information + const hasTimezone = /[zZ]|[+\-]\d{2}:?\d{2}$/.test(dateTimeStr); + + let date: Date; + if (hasTimezone) { + // If timezone exists, use as is + date = new Date(dateTimeStr); + } else { + // Treat as UTC time from database, convert to local time + // Normalize space-separated format like "2025-02-25 08:00:00" + const normalized = dateTimeStr.replace(" ", "T"); + date = new Date(`${normalized}Z`); + } + + return formatter.format(date); +} + /** * Get status configuration based on isCurrentVersion flag */ @@ -113,7 +137,7 @@ export function VersionCardItem({ const queryClient = useQueryClient(); // Get invalidate functions for refreshing data - const { invalidate: invalidateAgentVersionList } = useAgentVersionList(agentId); + const { agentVersionList, invalidate: invalidateAgentVersionList } = useAgentVersionList(agentId); const { invalidate: invalidateAgentInfo } = useAgentInfo(agentId); // Fetch version detail when expanded @@ -128,17 +152,20 @@ export function VersionCardItem({ // Modal state const [compareModalOpen, setCompareModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); const [loading, setLoading] = useState(false); const [rollbackLoading, setRollbackLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [compareData, setCompareData] = useState(null); + const [selectedVersionNoA, setSelectedVersionNoA] = useState(null); + const [selectedVersionNoB, setSelectedVersionNoB] = useState(null); // Get theme token for styling const { token } = theme.useToken(); - // Generate display date and operator from version data + // Generate display date from version data (convert from UTC to local time) const displayDate = useMemo(() => { - return formatter.format(new Date(version.create_time)); + return formatUtcToLocal(version.create_time); }, [version.create_time]); /** @@ -149,19 +176,20 @@ export function VersionCardItem({ message.error(t("agent.error.agentNotFound")); return; } + const versionNoA = currentVersionNo || 0; + const versionNoB = version.version_no; + setSelectedVersionNoA(versionNoA); + setSelectedVersionNoB(versionNoB); setCompareModalOpen(true); - await loadComparison(); + await loadComparison(versionNoA, versionNoB); }; /** * Load version comparison data between current version and selected version */ - const loadComparison = async () => { + const loadComparison = async (versionNoA: number, versionNoB: number) => { setLoading(true); try { - // Compare current version (currentVersionNo) with the version being rolled back to (version.version_no) - const versionNoA = currentVersionNo || 0; // Use current version, fallback to 0 (draft) if not available - const versionNoB = version.version_no; const result = await compareVersions(agentId, versionNoA, versionNoB); setCompareData(result); } catch (error) { @@ -172,6 +200,30 @@ export function VersionCardItem({ } }; + const handleChangeVersionA = async (value: number) => { + setSelectedVersionNoA(value); + if (!selectedVersionNoB) { + return; + } + if (value === selectedVersionNoB) { + message.warning(t("agent.version.selectDifferentVersions")); + return; + } + await loadComparison(value, selectedVersionNoB); + }; + + const handleChangeVersionB = async (value: number) => { + setSelectedVersionNoB(value); + if (!selectedVersionNoA) { + return; + } + if (value === selectedVersionNoA) { + message.warning(t("agent.version.selectDifferentVersions")); + return; + } + await loadComparison(selectedVersionNoA, value); + }; + /** * Handle rollback confirmation * Rollback updates current_version_no to point to the target version @@ -312,6 +364,12 @@ export function VersionCardItem({ , + onClick: () => setEditModalOpen(true) + }, { key: 'rollback', label: t("agent.version.rollback"), @@ -430,168 +488,21 @@ export function VersionCardItem({ )} - {/* Version Comparison Modal */} - - - {t("agent.version.rollbackCompareTitle")} - - } + setCompareModalOpen(false)} - footer={[ - , - , - ]} - width={800} - centered - > - - {compareData?.success && compareData?.data ? ( - - {/* Comparison Table */} - {(() => { - const { version_a, version_b } = compareData.data; - - const columns = [ - { - title: t("agent.version.versionName"), - dataIndex: 'field', - key: 'field', - width: '25%', - className: 'bg-gray-50 text-gray-600 font-medium', - }, - { - title: version_a.version.version_name, - dataIndex: 'current', - key: 'current', - width: '37%', - }, - { - title: version_b.version.version_name, - dataIndex: 'version', - key: 'version', - width: '38%', - }, - ]; - - const data = [ - { - key: 'name', - field: t("agent.version.field.name"), - current: ( - - {version_a.name} - - ), - version: ( - - {version_b.name} - - ), - }, - { - key: 'model_name', - field: t("agent.version.field.modelName"), - current: ( - - {version_a.model_name || '-'} - - ), - version: ( - - {version_b.model_name || '-'} - - ), - }, - { - key: 'description', - field: t("agent.version.field.description"), - current: ( - - {version_a.description || '-'} - - ), - version: ( - - {version_b.description || '-'} - - ), - }, - { - key: 'duty_prompt', - field: t("agent.version.field.dutyPrompt"), - current: ( - - {version_a.duty_prompt?.slice(0, 100) || '-'} - {version_a.duty_prompt && version_a.duty_prompt.length > 100 && '...'} - - ), - version: ( - - {version_b.duty_prompt?.slice(0, 100) || '-'} - {version_b.duty_prompt && version_b.duty_prompt.length > 100 && '...'} - - ), - }, - { - key: 'tools', - field: t("agent.version.field.tools"), - current: ( - - {version_a.tools?.length || 0} - - ), - version: ( - - {version_b.tools?.length || 0} - - ), - }, - { - key: 'sub_agents', - field: t("agent.version.field.subAgents"), - current: ( - - {version_a.sub_agent_id_list?.length || 0} - - ), - version: ( - - {version_b.sub_agent_id_list?.length || 0} - - ), - }, - ]; - - return ( - - ); - })()} - - ) : ( - - )} - - + showRollback + rollbackLoading={rollbackLoading} + onRollbackConfirm={handleRollbackConfirm} + selectedVersionNoA={selectedVersionNoA} + selectedVersionNoB={selectedVersionNoB} + onChangeVersionA={handleChangeVersionA} + onChangeVersionB={handleChangeVersionB} + /> {/* Delete Version Confirmation Modal */} + + {/* Edit Version Modal */} + setEditModalOpen(false)} + agentId={agentId} + versionNo={version.version_no} + isEdit={true} + initialValues={{ + version_name: version.version_name, + release_note: version.release_note, + }} + onUpdated={() => { + // Refresh version list using the proper invalidate function + invalidateAgentVersionList(); + }} + /> ); } diff --git a/frontend/app/[locale]/agents/AgentVersionManage.tsx b/frontend/app/[locale]/agents/AgentVersionManage.tsx index 38f13eca5..751500ca6 100644 --- a/frontend/app/[locale]/agents/AgentVersionManage.tsx +++ b/frontend/app/[locale]/agents/AgentVersionManage.tsx @@ -1,81 +1,91 @@ "use client"; import { useState } from "react"; -import { - GitBranch, - GitCompare, - Rocket, -} from "lucide-react"; +import { GitBranch, GitCompare, Rocket } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { - Card, - Flex, - Button, - Tag, - Typography, - Empty, - Spin, - Modal, - Form, - Input, - message, -} from "antd"; +import { Card, Flex, Button, Tag, Empty, Spin, message } from "antd"; import { useAgentVersionList } from "@/hooks/agent/useAgentVersionList"; -import { publishVersion } from "@/services/agentVersionService"; import { useAgentInfo } from "@/hooks/agent/useAgentInfo"; import { useAgentConfigStore } from "@/stores/agentConfigStore"; import { VersionCardItem } from "./AgentVersionCard"; import log from "@/lib/logger"; -import { useQueryClient } from "@tanstack/react-query"; - -const { TextArea } = Input; +import AgentVersionCompareModal from "./versions/AgentVersionCompareModal"; +import { compareVersions, type VersionCompareResponse } from "@/services/agentVersionService"; export default function AgentVersionManage() { const { t } = useTranslation("common"); - const queryClient = useQueryClient(); - const currentAgentId = useAgentConfigStore((state) => state.currentAgentId); const { agentVersionList, total, isLoading, invalidate: invalidateAgentVersionList } = useAgentVersionList(currentAgentId); const { agentInfo, invalidate: invalidateAgentInfo } = useAgentInfo(currentAgentId); + + const [compareModalOpen, setCompareModalOpen] = useState(false); + const [compareLoading, setCompareLoading] = useState(false); + const [compareData, setCompareData] = useState(null); + const [selectedVersionA, setSelectedVersionA] = useState(null); + const [selectedVersionB, setSelectedVersionB] = useState(null); - const [isPublishModalOpen, setIsPublishModalOpen] = useState(false); - const [isPublishing, setIsPublishing] = useState(false); - const [publishForm] = Form.useForm(); - - // Open publish modal - const handlePublishClick = () => { - setIsPublishModalOpen(true); + const loadComparison = async (agentId: number, versionNoA: number, versionNoB: number) => { + try { + setCompareLoading(true); + const result = await compareVersions(agentId, versionNoA, versionNoB); + setCompareData(result); + } catch (error) { + log.error("Failed to compare versions:", error); + message.error(t("agent.version.compareError")); + } finally { + setCompareLoading(false); + } }; - // Handle publish version - const handlePublish = async (values: { version_name?: string; release_note?: string }) => { + const handleOpenCompareModal = async () => { if (!currentAgentId) { message.error(t("agent.error.agentNotFound")); return; } + if (agentVersionList.length < 2) { + message.warning(t("agent.version.needTwoVersions")); + return; + } - // Prevent duplicate submissions - if (isPublishing) { - log.warn("Publish request already in progress, ignoring duplicate click"); + // Use the last two versions by version_no as default comparison + const sorted = [...agentVersionList].sort((a, b) => a.version_no - b.version_no); + const defaultVersionA = sorted[sorted.length - 2]?.version_no; + const defaultVersionB = sorted[sorted.length - 1]?.version_no; + + if (!defaultVersionA || !defaultVersionB) { + message.warning(t("agent.version.needTwoVersions")); return; } - try { - setIsPublishing(true); - await publishVersion(currentAgentId, values); - message.success(t("agent.version.publishSuccess")); - setIsPublishModalOpen(false); - publishForm.resetFields(); - invalidateAgentVersionList(); - invalidateAgentInfo(); - queryClient.invalidateQueries({ queryKey: ["agents"] }); - } catch (error) { - log.error("Failed to publish version:", error); - message.error(t("agent.version.publishFailed")); - } finally { - setIsPublishing(false); + setSelectedVersionA(defaultVersionA); + setSelectedVersionB(defaultVersionB); + setCompareModalOpen(true); + await loadComparison(currentAgentId, defaultVersionA, defaultVersionB); + }; + + const handleChangeVersionA = async (value: number) => { + setSelectedVersionA(value); + if (!currentAgentId || !selectedVersionB) { + return; + } + if (value === selectedVersionB) { + message.warning(t("agent.version.selectDifferentVersions")); + return; } + await loadComparison(currentAgentId, value, selectedVersionB); + }; + + const handleChangeVersionB = async (value: number) => { + setSelectedVersionB(value); + if (!currentAgentId || !selectedVersionA) { + return; + } + if (value === selectedVersionA) { + message.warning(t("agent.version.selectDifferentVersions")); + return; + } + await loadComparison(currentAgentId, selectedVersionA, value); }; const footer = [ @@ -92,9 +102,7 @@ export default function AgentVersionManage() { @@ -112,15 +120,6 @@ export default function AgentVersionManage() { {t("agent.version.manage")} } - extra={ - - } actions={footer} styles={{ body: { @@ -152,46 +151,18 @@ export default function AgentVersionManage() { - {/* Publish Version Modal */} - setIsPublishModalOpen(false)} - footer={null} - destroyOnHidden - > -
- - - - -