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

-### 2️⃣ Configure Fields
+### 2️⃣ Configure Local Tools
-🔑 Fill in tool permissions as prompted
+🔑 Fill in local tool permissions as prompted

+### 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

+## 🔧 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.
+
+
+
+
+
## 🔍 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)
-
-
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)**.
+
-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 |
+
+
+
+
+### 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 |
+
+
+
+
+
+
+
+## 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
+
+
+
+
+
+
+
+
+## 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 支持智能体的版本管理,您可以在调试过程中,保存不同版本的智能体配置。
+
+确认智能体配置无误后,您可发布智能体。发布后智能体将在智能体空间、开始问答中可见。
+
+
+
+若需回滚到其他版本,可在版本管理页面点击"回滚"按钮。
+
+
+
+
## 🔧 管理智能体
在左侧智能体列表中,您可对已有的智能体进行以下操作:
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 @@

-### 2️⃣ 配置字段
+### 2️⃣ 配置本地工具
-🔑 依据提示补充工具许可
+🔑 依据提示补充本地工具的许可

+### 3️⃣ 配置外部 MCP 工具
+
+🔑 依据提示补充 MCP 工具的许可
+
+
+
安装完成后,您的智能体会在 **[智能体空间](./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支持多种文件格式,包括:

+## 🔧 使用知识库
+
+Nexent支持知识库与智能体单独绑定,在创建智能体时,**启用knowledge_base_search工具**,并选择关联的知识库
+
+
+
## 🔍 知识库管理
### 查看知识库
1. **知识库列表**
- 知识库页面左侧展示了所有已创建的知识库
- - 显示知识库名称、文件数量、创建时间等信息
+ - 知识库列表处支持对知识库来源和向量模型的筛选
+ - 显示知识库名称、文件数量、创建时间、用户组等信息
+
+> 点击编辑,可管理知识库的名称、可见的用户组及组内权限
+
+
2. **知识库详情**
- 点击知识库名称,可查看知识库中全部文档信息
- 点击“详细内容”,可查看知识库的内容总结
-