diff --git a/.github/workflows/docker-build-push-beta.yml b/.github/workflows/docker-build-push-beta.yml index 76cbb61ae..0946bbf25 100644 --- a/.github/workflows/docker-build-push-beta.yml +++ b/.github/workflows/docker-build-push-beta.yml @@ -150,6 +150,46 @@ jobs: - name: Push web image (arm64) to DockerHub run: docker push nexent/nexent-web:beta-arm64 + build-and-push-terminal-amd64: + runs-on: ubuntu-latest + steps: + - name: Set up Docker Buildx + run: | + if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then + docker buildx create --name nexent_builder --use + else + docker buildx use nexent_builder + fi + - name: Checkout code + uses: actions/checkout@v4 + - name: Build terminal image (amd64) and load locally + run: | + docker buildx build --platform linux/amd64 -t nexent/nexent-ubuntu-terminal:beta-amd64 --load -f make/terminal/Dockerfile . + - name: Login to DockerHub + run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin + - name: Push terminal image (amd64) to DockerHub + run: docker push nexent/nexent-ubuntu-terminal:beta-amd64 + + build-and-push-terminal-arm64: + runs-on: ubuntu-latest + steps: + - name: Set up Docker Buildx + run: | + if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then + docker buildx create --name nexent_builder --use + else + docker buildx use nexent_builder + fi + - name: Checkout code + uses: actions/checkout@v4 + - name: Build terminal image (arm64) and load locally + run: | + docker buildx build --platform linux/arm64 -t nexent/nexent-ubuntu-terminal:beta-arm64 --load -f make/terminal/Dockerfile . + - name: Login to DockerHub + run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin + - name: Push terminal image (arm64) to DockerHub + run: docker push nexent/nexent-ubuntu-terminal:beta-arm64 + manifest-push-main: runs-on: ubuntu-latest needs: @@ -193,4 +233,19 @@ jobs: docker manifest create nexent/nexent-web:beta \ nexent/nexent-web:beta-amd64 \ nexent/nexent-web:beta-arm64 - docker manifest push nexent/nexent-web:beta \ No newline at end of file + docker manifest push nexent/nexent-web:beta + + manifest-push-terminal: + runs-on: ubuntu-latest + needs: + - build-and-push-terminal-amd64 + - build-and-push-terminal-arm64 + steps: + - name: Login to DockerHub + run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin + - name: Create and push manifest for terminal (DockerHub) + run: | + docker manifest create nexent/nexent-ubuntu-terminal:beta \ + nexent/nexent-ubuntu-terminal:beta-amd64 \ + nexent/nexent-ubuntu-terminal:beta-arm64 + docker manifest push nexent/nexent-ubuntu-terminal:beta \ No newline at end of file diff --git a/.github/workflows/docker-build-push-mainland.yml b/.github/workflows/docker-build-push-mainland.yml index 84be4f13b..1628b0bf7 100644 --- a/.github/workflows/docker-build-push-mainland.yml +++ b/.github/workflows/docker-build-push-mainland.yml @@ -147,6 +147,46 @@ jobs: - name: Push web image (arm64) to Tencent Cloud run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-web:arm64 + build-and-push-terminal-amd64: + runs-on: ${{ fromJson(inputs.runner_label_json) }} + steps: + - name: Set up Docker Buildx + run: | + if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then + docker buildx create --name nexent_builder --use + else + docker buildx use nexent_builder + fi + - name: Checkout code + uses: actions/checkout@v4 + - name: Build terminal image (amd64) and load locally + run: | + docker buildx build --platform linux/amd64 --load -t ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:amd64 -f make/terminal/Dockerfile . + - name: Login to Tencent Cloud + run: echo ${{ secrets.TCR_PASSWORD }} | docker login ccr.ccs.tencentyun.com --username=${{ secrets.TCR_USERNAME }} --password-stdin + - name: Push terminal image (amd64) to Tencent Cloud + run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:amd64 + + build-and-push-terminal-arm64: + runs-on: ${{ fromJson(inputs.runner_label_json) }} + steps: + - name: Set up Docker Buildx + run: | + if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then + docker buildx create --name nexent_builder --use + else + docker buildx use nexent_builder + fi + - name: Checkout code + uses: actions/checkout@v4 + - name: Build terminal image (arm64) and load locally + run: | + docker buildx build --platform linux/arm64 --load -t ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:arm64 -f make/terminal/Dockerfile . + - name: Login to Tencent Cloud + run: echo ${{ secrets.TCR_PASSWORD }} | docker login ccr.ccs.tencentyun.com --username=${{ secrets.TCR_USERNAME }} --password-stdin + - name: Push terminal image (arm64) to Tencent Cloud + run: docker push ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:arm64 + manifest-push-main: runs-on: ubuntu-latest needs: @@ -190,4 +230,19 @@ jobs: docker manifest create ccr.ccs.tencentyun.com/nexent-hub/nexent-web:latest \ ccr.ccs.tencentyun.com/nexent-hub/nexent-web:amd64 \ ccr.ccs.tencentyun.com/nexent-hub/nexent-web:arm64 - docker manifest push ccr.ccs.tencentyun.com/nexent-hub/nexent-web:latest \ No newline at end of file + docker manifest push ccr.ccs.tencentyun.com/nexent-hub/nexent-web:latest + + manifest-push-terminal: + runs-on: ubuntu-latest + needs: + - build-and-push-terminal-amd64 + - build-and-push-terminal-arm64 + steps: + - name: Login to Tencent Cloud + run: echo ${{ secrets.TCR_PASSWORD }} | docker login ccr.ccs.tencentyun.com --username=${{ secrets.TCR_USERNAME }} --password-stdin + - name: Create and push manifest for terminal (Tencent Cloud) + run: | + docker manifest create ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:latest \ + ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:amd64 \ + ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:arm64 + docker manifest push ccr.ccs.tencentyun.com/nexent-hub/nexent-ubuntu-terminal:latest \ No newline at end of file diff --git a/.github/workflows/docker-build-push-overseas.yml b/.github/workflows/docker-build-push-overseas.yml index e1d6a0977..e648aad92 100644 --- a/.github/workflows/docker-build-push-overseas.yml +++ b/.github/workflows/docker-build-push-overseas.yml @@ -147,6 +147,46 @@ jobs: - name: Push web image (arm64) to DockerHub run: docker push nexent/nexent-web:arm64 + build-and-push-terminal-amd64: + runs-on: ${{ fromJson(inputs.runner_label_json) }} + steps: + - name: Set up Docker Buildx + run: | + if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then + docker buildx create --name nexent_builder --use + else + docker buildx use nexent_builder + fi + - name: Checkout code + uses: actions/checkout@v4 + - name: Build terminal image (amd64) and load locally + run: | + docker buildx build --platform linux/amd64 -t nexent/nexent-ubuntu-terminal:amd64 --load -f make/terminal/Dockerfile . + - name: Login to DockerHub + run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin + - name: Push terminal image (amd64) to DockerHub + run: docker push nexent/nexent-ubuntu-terminal:amd64 + + build-and-push-terminal-arm64: + runs-on: ${{ fromJson(inputs.runner_label_json) }} + steps: + - name: Set up Docker Buildx + run: | + if ! docker buildx inspect nexent_builder > /dev/null 2>&1; then + docker buildx create --name nexent_builder --use + else + docker buildx use nexent_builder + fi + - name: Checkout code + uses: actions/checkout@v4 + - name: Build terminal image (arm64) and load locally + run: | + docker buildx build --platform linux/arm64 -t nexent/nexent-ubuntu-terminal:arm64 --load -f make/terminal/Dockerfile . + - name: Login to DockerHub + run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin + - name: Push terminal image (arm64) to DockerHub + run: docker push nexent/nexent-ubuntu-terminal:arm64 + manifest-push-main: runs-on: ubuntu-latest needs: @@ -190,4 +230,19 @@ jobs: docker manifest create nexent/nexent-web:latest \ nexent/nexent-web:amd64 \ nexent/nexent-web:arm64 - docker manifest push nexent/nexent-web:latest \ No newline at end of file + docker manifest push nexent/nexent-web:latest + + manifest-push-terminal: + runs-on: ubuntu-latest + needs: + - build-and-push-terminal-amd64 + - build-and-push-terminal-arm64 + steps: + - name: Login to DockerHub + run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u nexent --password-stdin + - name: Create and push manifest for terminal (DockerHub) + run: | + docker manifest create nexent/nexent-ubuntu-terminal:latest \ + nexent/nexent-ubuntu-terminal:amd64 \ + nexent/nexent-ubuntu-terminal:arm64 + docker manifest push nexent/nexent-ubuntu-terminal:latest \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index ed36999fe..82f4ddd24 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -41,4 +41,13 @@ jobs: uses: actions/checkout@v4 - name: Build web frontend image - run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --progress=plain -t nexent/nexent-web -f make/web/Dockerfile . \ No newline at end of file + run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --progress=plain -t nexent/nexent-web -f make/web/Dockerfile . + + build-terminal: + runs-on: ${{ fromJson(inputs.runner_label_json) }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build terminal image + run: docker build --progress=plain -t nexent/nexent-ubuntu-terminal -f make/terminal/Dockerfile . \ No newline at end of file diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 868c3459a..74609e1a1 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -23,6 +23,13 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + # 流式响应支持 + proxy_buffering off; + proxy_cache off; + proxy_set_header Connection ''; + proxy_http_version 1.1; + chunked_transfer_encoding off; } } @@ -37,6 +44,13 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + # 流式响应支持 + proxy_buffering off; + proxy_cache off; + proxy_set_header Connection ''; + proxy_http_version 1.1; + chunked_transfer_encoding off; } } ``` diff --git a/backend/agents/create_agent_info.py b/backend/agents/create_agent_info.py index 94a6235fd..1045338eb 100644 --- a/backend/agents/create_agent_info.py +++ b/backend/agents/create_agent_info.py @@ -16,6 +16,7 @@ from smolagents.utils import BASE_BUILTIN_MODULES from services.memory_config_service import build_memory_context from jinja2 import Template, StrictUndefined +from datetime import datetime from nexent.memory.memory_service import search_memory_in_levels @@ -128,7 +129,8 @@ async def create_agent_config(agent_id, tenant_id, user_id, language: str = 'zh' "APP_NAME": app_name, "APP_DESCRIPTION": app_description, "memory_list": memory_list, - "knowledge_base_summary": knowledge_base_summary + "knowledge_base_summary": knowledge_base_summary, + "time" : datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) else: system_prompt = agent_info.get("prompt", "") diff --git a/backend/apps/agent_app.py b/backend/apps/agent_app.py index 77a09bb30..8159ccd1a 100644 --- a/backend/apps/agent_app.py +++ b/backend/apps/agent_app.py @@ -2,22 +2,13 @@ from typing import Optional from fastapi import HTTPException, APIRouter, Header, Request, Body -from fastapi.responses import StreamingResponse, JSONResponse -from nexent.core.agents.run_agent import agent_run - -from database.agent_db import delete_related_agent -from utils.auth_utils import get_current_user_info, get_current_user_id -from agents.create_agent_info import create_agent_run_info +from fastapi.responses import JSONResponse from consts.model import AgentRequest, AgentInfoRequest, AgentIDRequest, ConversationResponse, AgentImportRequest from services.agent_service import get_agent_info_impl, \ get_creating_sub_agent_info_impl, update_agent_info_impl, delete_agent_impl, export_agent_impl, import_agent_impl, \ - list_all_agent_info_impl, insert_related_agent_impl -from services.conversation_management_service import save_conversation_user, save_conversation_assistant -from services.memory_config_service import build_memory_context -from utils.config_utils import config_manager -from utils.thread_utils import submit -from agents.agent_run_manager import agent_run_manager -from agents.preprocess_manager import preprocess_manager + list_all_agent_info_impl, insert_related_agent_impl, run_agent_stream, stop_agent_tasks +from database.agent_db import delete_related_agent +from utils.auth_utils import get_current_user_info, get_current_user_id router = APIRouter(prefix="/agent") @@ -30,43 +21,10 @@ async def agent_run_api(agent_request: AgentRequest, http_request: Request, auth """ Agent execution API endpoint """ - user_id, tenant_id, language = get_current_user_info(authorization, http_request) - memory_context = build_memory_context(user_id, tenant_id, agent_request.agent_id) - - agent_run_info = await create_agent_run_info(agent_id=agent_request.agent_id, - minio_files=agent_request.minio_files, - query=agent_request.query, - history=agent_request.history, - authorization=authorization, - language=language) - - agent_run_manager.register_agent_run(agent_request.conversation_id, agent_run_info) - # Save user message only if not in debug mode - if not agent_request.is_debug: - submit(save_conversation_user, agent_request, authorization) - - async def generate(): - messages = [] - try: - async for chunk in agent_run(agent_run_info, memory_context): - messages.append(chunk) - yield f"data: {chunk}\n\n" - except Exception as e: - raise HTTPException(status_code=500, detail=f"Agent run error: {str(e)}") - finally: - # Save assistant message only if not in debug mode - if not agent_request.is_debug: - submit(save_conversation_assistant, agent_request, messages, authorization) - # Unregister agent run instance for both debug and non-debug modes - agent_run_manager.unregister_agent_run(agent_request.conversation_id) - - return StreamingResponse( - generate(), - media_type="text/event-stream", - headers={ - "Cache-Control": "no-cache", - "Connection": "keep-alive" - } + return await run_agent_stream( + agent_request=agent_request, + http_request=http_request, + authorization=authorization ) @@ -75,21 +33,8 @@ async def agent_stop_api(conversation_id: int): """ stop agent run and preprocess tasks for specified conversation_id """ - # Stop agent run - agent_stopped = agent_run_manager.stop_agent_run(conversation_id) - - # Stop preprocess tasks - preprocess_stopped = preprocess_manager.stop_preprocess_tasks(conversation_id) - - if agent_stopped or preprocess_stopped: - message_parts = [] - if agent_stopped: - message_parts.append("agent run") - if preprocess_stopped: - message_parts.append("preprocess tasks") - - message = f"successfully stopped {' and '.join(message_parts)} for conversation_id {conversation_id}" - return {"status": "success", "message": message} + if stop_agent_tasks(conversation_id).get("status") == "success": + return {"status": "success", "message": "agent run and preprocess tasks stopped successfully"} else: raise HTTPException(status_code=404, detail=f"no running agent or preprocess tasks found for conversation_id {conversation_id}") diff --git a/backend/apps/conversation_management_app.py b/backend/apps/conversation_management_app.py index c1816d539..2a1ca6181 100644 --- a/backend/apps/conversation_management_app.py +++ b/backend/apps/conversation_management_app.py @@ -2,8 +2,6 @@ from typing import Dict, Any, Optional from fastapi import HTTPException, APIRouter, Header, Request -from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel from consts.model import ConversationResponse, ConversationRequest, RenameRequest, GenerateTitleRequest, OpinionRequest, MessageIdRequest from services.conversation_management_service import ( diff --git a/backend/apps/mock_user_management_app.py b/backend/apps/mock_user_management_app.py index 2c337d303..26a6f58f9 100644 --- a/backend/apps/mock_user_management_app.py +++ b/backend/apps/mock_user_management_app.py @@ -19,8 +19,8 @@ MOCK_SESSION = { "access_token": "mock_access_token", "refresh_token": "mock_refresh_token", - "expires_at": int((datetime.now() + timedelta(hours=1)).timestamp()), - "expires_in_seconds": 3600 + "expires_at": int((datetime.now() + timedelta(days=3650)).timestamp()), + "expires_in_seconds": 315360000 } @@ -74,7 +74,7 @@ async def signin(request: UserSignInRequest): # Return mock success response return ServiceResponse( code=STATUS_CODES["SUCCESS"], - message="Login successful, session validity is 3600 seconds", + message="Login successful, session validity is 10 years", data={ "user": { "id": MOCK_USER["id"], @@ -98,17 +98,19 @@ async def refresh_token(request: Request): """ logger.info("Mock refresh token request") - # Return mock success response with new tokens - new_expires_at = int((datetime.now() + timedelta(hours=1)).timestamp()) + # In speed/mock mode, extend for a very long time (10 years) + new_expires_at = int((datetime.now() + timedelta(days=3650)).timestamp()) return ServiceResponse( code=STATUS_CODES["SUCCESS"], message="Token refreshed successfully", data={ - "access_token": f"mock_access_token_{new_expires_at}", - "refresh_token": f"mock_refresh_token_{new_expires_at}", - "expires_at": new_expires_at, - "expires_in_seconds": 3600 + "session": { + "access_token": f"mock_access_token_{new_expires_at}", + "refresh_token": f"mock_refresh_token_{new_expires_at}", + "expires_at": new_expires_at, + "expires_in_seconds": 315360000 + } } ) diff --git a/backend/apps/model_managment_app.py b/backend/apps/model_managment_app.py index cf639ed13..32e6455f0 100644 --- a/backend/apps/model_managment_app.py +++ b/backend/apps/model_managment_app.py @@ -329,57 +329,6 @@ async def check_model_healthcheck( return await check_model_connectivity(display_name, authorization) - -@router.post("/update_connect_status", response_model=ModelResponse) -async def update_model_connect_status( - model_name: str = Body(..., embed=True), - connect_status: str = Body(..., embed=True), - authorization: Optional[str] = Header(None) -): - """ - Update model connection status - - Args: - model_name: Model name, including repository info, e.g. openai/gpt-3.5-turbo - connect_status: New connection status - authorization: Authorization header - """ - try: - user_id, tenant_id = get_current_user_id(authorization) - # Split model_name - repo, name = split_repo_name(model_name) - # Ensure repo is empty string instead of null - repo = repo if repo else "" - - # Query model information - model = get_model_by_name(name, repo) - if not model: - return ModelResponse( - code=404, - message=f"Model not found: {model_name}", - data={"connect_status": ""} - ) - - # Update connection status - update_data = {"connect_status": connect_status} - update_model_record(model["model_id"], update_data, user_id) - - return ModelResponse( - code=200, - message=f"Successfully updated connection status for model {model_name}", - data={ - "model_name": model_name, - "connect_status": connect_status - } - ) - except Exception as e: - return ModelResponse( - code=500, - message=f"Failed to update model connection status: {str(e)}", - data={"connect_status": ModelConnectStatusEnum.NOT_DETECTED.value} - ) - - @router.post("/verify_config", response_model=ModelResponse) async def verify_model_config(request: ModelRequest): """ diff --git a/backend/consts/model.py b/backend/consts/model.py index 3725688c0..ecc929ba9 100644 --- a/backend/consts/model.py +++ b/backend/consts/model.py @@ -8,10 +8,10 @@ class ModelConnectStatusEnum(Enum): """Enum class for model connection status""" - NOT_DETECTED = "未检测" - DETECTING = "检测中" - AVAILABLE = "可用" - UNAVAILABLE = "不可用" + NOT_DETECTED = "not_detected" + DETECTING = "detecting" + AVAILABLE = "available" + UNAVAILABLE = "unavailable" @classmethod def get_default(cls) -> str: @@ -317,6 +317,7 @@ class MessageIdRequest(BaseModel): class ExportAndImportAgentInfo(BaseModel): agent_id: int name: str + display_name: Optional[str] = None description: str business_description: str model_name: str @@ -329,9 +330,19 @@ class ExportAndImportAgentInfo(BaseModel): tools: List[ToolConfig] managed_agents: List[int] + class Config: + arbitrary_types_allowed = True + + +class MCPInfo(BaseModel): + mcp_server_name: str + mcp_url: str + + class ExportAndImportDataFormat(BaseModel): agent_id: int agent_info: Dict[str, ExportAndImportAgentInfo] + mcp_info: List[MCPInfo] class AgentImportRequest(BaseModel): diff --git a/backend/data_process_service.py b/backend/data_process_service.py index cd44ad398..49cd06deb 100644 --- a/backend/data_process_service.py +++ b/backend/data_process_service.py @@ -776,9 +776,6 @@ async def lifespan(app: FastAPI): # Startup logger.info("Starting data processing service...") - # Services should already be started by main() - logger.info("Data processing service started successfully") - yield # Shutdown @@ -827,7 +824,7 @@ def main(): # Create and start FastAPI app app = create_app() - logger.debug(f"🌐 Starting API server on {args.api_host}:{args.api_port}") + logger.info(f"🌐 Starting API server on {args.api_host}:{args.api_port}") uvicorn.run( app, host=args.api_host, diff --git a/backend/database/agent_db.py b/backend/database/agent_db.py index 26d055e73..95ee400e1 100644 --- a/backend/database/agent_db.py +++ b/backend/database/agent_db.py @@ -25,6 +25,20 @@ def search_agent_info_by_agent_id(agent_id: int, tenant_id: str): return agent_dict +def search_agent_id_by_agent_name(agent_name: str, tenant_id: str): + """ + Search agent id by agent name + """ + with get_db_session() as session: + agent = session.query(AgentInfo).filter( + AgentInfo.name == agent_name, + AgentInfo.tenant_id == tenant_id, + AgentInfo.delete_flag != 'Y').first() + if not agent: + raise ValueError("agent not found") + return agent.agent_id + + def search_blank_sub_agent_by_main_agent_id(tenant_id: str): """ Search blank sub agent by main agent id diff --git a/backend/database/remote_mcp_db.py b/backend/database/remote_mcp_db.py index 4deeac471..cc5834e81 100644 --- a/backend/database/remote_mcp_db.py +++ b/backend/database/remote_mcp_db.py @@ -96,4 +96,45 @@ def get_mcp_records_by_tenant(tenant_id: str) -> List[Dict[str, Any]]: ).order_by(McpRecord.create_time.desc()).all() - return [as_dict(record) for record in mcp_records] \ No newline at end of file + return [as_dict(record) for record in mcp_records] + +def get_mcp_server_by_name_and_tenant(mcp_name: str, tenant_id: str) -> str: + """ + Get MCP server address by name and tenant ID + + :param mcp_name: MCP name + :param tenant_id: Tenant ID + :return: MCP server address, empty string if not found + """ + with get_db_session() as session: + try: + mcp_record = session.query(McpRecord).filter( + McpRecord.mcp_name == mcp_name, + McpRecord.tenant_id == tenant_id, + McpRecord.delete_flag != 'Y' + ).first() + + return mcp_record.mcp_server if mcp_record else "" + except SQLAlchemyError: + return "" + + +def check_mcp_name_exists(mcp_name: str, tenant_id: str) -> bool: + """ + Check if MCP name already exists for a tenant + + :param mcp_name: MCP name + :param tenant_id: Tenant ID + :return: True if name exists, False otherwise + """ + with get_db_session() as session: + try: + mcp_record = session.query(McpRecord).filter( + McpRecord.mcp_name == mcp_name, + McpRecord.tenant_id == tenant_id, + McpRecord.delete_flag != 'Y' + ).first() + + return mcp_record is not None + except SQLAlchemyError: + return False \ No newline at end of file diff --git a/backend/prompts/managed_system_prompt_template.yaml b/backend/prompts/managed_system_prompt_template.yaml index 318232d6f..63d689474 100644 --- a/backend/prompts/managed_system_prompt_template.yaml +++ b/backend/prompts/managed_system_prompt_template.yaml @@ -1,6 +1,6 @@ system_prompt: |- ### 基本信息 ### - 你是{{APP_NAME}},{{APP_DESCRIPTION}} + 你是{{APP_NAME}},{{APP_DESCRIPTION}},现在是{{time|default('当前时间')}} {%- if memory_list and memory_list|length > 0 %} ### 上下文记忆 ### diff --git a/backend/prompts/managed_system_prompt_template_en.yaml b/backend/prompts/managed_system_prompt_template_en.yaml index 9f0ad131e..3aa2685ee 100644 --- a/backend/prompts/managed_system_prompt_template_en.yaml +++ b/backend/prompts/managed_system_prompt_template_en.yaml @@ -1,6 +1,6 @@ system_prompt: |- ### Basic Information ### - You are {{APP_NAME}}, {{APP_DESCRIPTION}} + You are {{APP_NAME}}, {{APP_DESCRIPTION}}, it is {{time|default('current time')}} now {%- if memory_list and memory_list|length > 0 %} ### Contextual Memory ### diff --git a/backend/prompts/manager_system_prompt_template.yaml b/backend/prompts/manager_system_prompt_template.yaml index 1e0593720..9ec5541b1 100644 --- a/backend/prompts/manager_system_prompt_template.yaml +++ b/backend/prompts/manager_system_prompt_template.yaml @@ -1,6 +1,6 @@ system_prompt: |- ### 基本信息 ### - 你是{{APP_NAME}},{{APP_DESCRIPTION}} + 你是{{APP_NAME}},{{APP_DESCRIPTION}}, 现在是{{time|default('当前时间')}} {%- if memory_list and memory_list|length > 0 %} ### 上下文记忆 ### diff --git a/backend/prompts/manager_system_prompt_template_en.yaml b/backend/prompts/manager_system_prompt_template_en.yaml index f72dc04f7..3fccb899c 100644 --- a/backend/prompts/manager_system_prompt_template_en.yaml +++ b/backend/prompts/manager_system_prompt_template_en.yaml @@ -1,6 +1,6 @@ system_prompt: |- ### Basic Information ### - You are {{APP_NAME}}, {{APP_DESCRIPTION}} + You are {{APP_NAME}}, {{APP_DESCRIPTION}}, it is {{time|default('current time')}} now {%- if memory_list and memory_list|length > 0 %} ### Contextual Memory ### diff --git a/backend/services/agent_service.py b/backend/services/agent_service.py index 0e60f72da..5021cdf0a 100644 --- a/backend/services/agent_service.py +++ b/backend/services/agent_service.py @@ -3,19 +3,30 @@ import logging from collections import deque -from fastapi import Header -from fastapi.responses import JSONResponse +from fastapi import Header, Request, HTTPException +from fastapi.responses import JSONResponse, StreamingResponse +from consts.model import AgentRequest from agents.create_agent_info import create_tool_config_list -from consts.model import AgentInfoRequest, ExportAndImportAgentInfo, ExportAndImportDataFormat, ToolInstanceInfoRequest +from consts.model import AgentInfoRequest, ExportAndImportAgentInfo, ExportAndImportDataFormat, ToolInstanceInfoRequest, MCPInfo from database.agent_db import create_agent, query_all_enabled_tool_instances, \ search_blank_sub_agent_by_main_agent_id, \ search_tools_for_sub_agent, search_agent_info_by_agent_id, update_agent, delete_agent_by_id, query_all_tools, \ create_or_update_tool_by_tool_info, check_tool_is_available, query_all_agent_info_by_tenant_id, \ - query_sub_agents_id_list, insert_related_agent, delete_all_related_agent + query_sub_agents_id_list, insert_related_agent, delete_all_related_agent, search_agent_id_by_agent_name +from database.remote_mcp_db import get_mcp_server_by_name_and_tenant, check_mcp_name_exists +from services.remote_mcp_service import add_remote_mcp_server_list +from services.tool_configuration_service import update_tool_list +from services.conversation_management_service import save_conversation_user, save_conversation_assistant -from utils.auth_utils import get_current_user_id +from utils.auth_utils import get_current_user_info from utils.memory_utils import build_memory_config +from utils.thread_utils import submit from nexent.memory.memory_service import clear_memory +from nexent.core.agents.run_agent import agent_run +from services.memory_config_service import build_memory_context +from agents.create_agent_info import create_agent_run_info +from agents.agent_run_manager import agent_run_manager +from agents.preprocess_manager import preprocess_manager logger = logging.getLogger("agent_service") @@ -66,7 +77,7 @@ def get_agent_info_impl(agent_id: int, tenant_id: str): def get_creating_sub_agent_info_impl(authorization: str = Header(None)): - user_id, tenant_id = get_current_user_id(authorization) + user_id, tenant_id, _ = get_current_user_info(authorization) try: sub_agent_id = get_creating_sub_agent_id_service(tenant_id, user_id) @@ -97,7 +108,7 @@ def get_creating_sub_agent_info_impl(authorization: str = Header(None)): "sub_agent_id_list": query_sub_agents_id_list(main_agent_id=sub_agent_id, tenant_id=tenant_id)} def update_agent_info_impl(request: AgentInfoRequest, authorization: str = Header(None)): - user_id, tenant_id = get_current_user_id(authorization) + user_id, tenant_id, _ = get_current_user_info(authorization) try: update_agent(request.agent_id, request, tenant_id, user_id) @@ -106,7 +117,7 @@ def update_agent_info_impl(request: AgentInfoRequest, authorization: str = Heade raise ValueError(f"Failed to update agent info: {str(e)}") async def delete_agent_impl(agent_id: int, authorization: str = Header(None)): - user_id, tenant_id = get_current_user_id(authorization) + user_id, tenant_id, _ = get_current_user_info(authorization) try: delete_agent_by_id(agent_id, tenant_id, user_id) @@ -180,12 +191,14 @@ async def export_agent_impl(agent_id: int, authorization: str = Header(None)) -> This function recursively finds all managed sub-agents and exports the detailed configuration of each agent (including tools, prompts, etc.) as a dictionary, and finally returns it as a formatted JSON string for frontend download and backup. """ - user_id, tenant_id = get_current_user_id(authorization) + user_id, tenant_id, _ = get_current_user_info(authorization) export_agent_dict = {} search_list = deque([agent_id]) agent_id_set = set() + mcp_info_set = set() + while len(search_list): left_ele = search_list.popleft() if left_ele in agent_id_set: @@ -193,10 +206,23 @@ async def export_agent_impl(agent_id: int, authorization: str = Header(None)) -> agent_id_set.add(left_ele) agent_info = await export_agent_by_agent_id(agent_id=left_ele, tenant_id=tenant_id, user_id=user_id) + + # collect mcp name + for tool in agent_info.tools: + if tool.source == "mcp" and tool.usage: + mcp_info_set.add(tool.usage) + search_list.extend(agent_info.managed_agents) export_agent_dict[str(agent_info.agent_id)] = agent_info - export_data = ExportAndImportDataFormat(agent_id=agent_id, agent_info=export_agent_dict) + # convert mcp info to MCPInfo list + mcp_info_list = [] + for mcp_server_name in mcp_info_set: + # get mcp url by mcp_server_name and tenant_id + mcp_url = get_mcp_server_by_name_and_tenant(mcp_server_name, tenant_id) + mcp_info_list.append(MCPInfo(mcp_server_name=mcp_server_name, mcp_url=mcp_url)) + + export_data = ExportAndImportDataFormat(agent_id=agent_id, agent_info=export_agent_dict, mcp_info=mcp_info_list) return export_data.model_dump() async def export_agent_by_agent_id(agent_id: int, tenant_id: str, user_id: str)->ExportAndImportAgentInfo: @@ -214,6 +240,7 @@ async def export_agent_by_agent_id(agent_id: int, tenant_id: str, user_id: str)- agent_info = ExportAndImportAgentInfo(agent_id=agent_id, name=agent_info["name"], + display_name=agent_info["display_name"], description=agent_info["description"], business_description=agent_info["business_description"], model_name=agent_info["model_name"], @@ -232,9 +259,49 @@ async def import_agent_impl(agent_info: ExportAndImportDataFormat, authorization """ Import agent using DFS """ - user_id, tenant_id = get_current_user_id(authorization) + user_id, tenant_id, _ = get_current_user_info(authorization) agent_id = agent_info.agent_id + # First, add MCP servers if any + if agent_info.mcp_info: + for mcp_info in agent_info.mcp_info: + if mcp_info.mcp_server_name and mcp_info.mcp_url: + try: + # Check if MCP name already exists + if check_mcp_name_exists(mcp_name=mcp_info.mcp_server_name, tenant_id=tenant_id): + # Get existing MCP server info to compare URLs + existing_mcp = get_mcp_server_by_name_and_tenant(mcp_name=mcp_info.mcp_server_name, tenant_id=tenant_id) + if existing_mcp and existing_mcp == mcp_info.mcp_url: + # Same name and URL, skip + logger.info(f"MCP server {mcp_info.mcp_server_name} with same URL already exists, skipping") + continue + else: + # Same name but different URL, add import prefix + import_mcp_name = f"import_{mcp_info.mcp_server_name}" + logger.info(f"MCP server {mcp_info.mcp_server_name} exists with different URL, using name: {import_mcp_name}") + mcp_server_name = import_mcp_name + else: + # Name doesn't exist, use original name + mcp_server_name = mcp_info.mcp_server_name + + result = await add_remote_mcp_server_list( + tenant_id=tenant_id, + user_id=user_id, + remote_mcp_server=mcp_info.mcp_url, + remote_mcp_server_name=mcp_server_name + ) + # Check if the result is a JSONResponse with error status + if hasattr(result, 'status_code') and result.status_code != 200: + raise Exception(f"Failed to add MCP server {mcp_server_name}: {result.body.decode() if hasattr(result, 'body') else 'Unknown error'}") + except Exception as e: + raise Exception(f"Failed to add MCP server {mcp_info.mcp_server_name}: {str(e)}") + + # Then, update tool list to include new MCP tools + try: + await update_tool_list(tenant_id=tenant_id, user_id=user_id) + except Exception as e: + raise Exception(f"Failed to update tool list: {str(e)}") + agent_stack = deque([agent_id]) agent_id_set = set() mapping_agent_id = {} @@ -298,6 +365,7 @@ async def import_agent_by_agent_id(import_agent_info: ExportAndImportAgentInfo, raise ValueError(f"Invalid agent name: {import_agent_info.name}. agent name must be a valid python variable name.") # create a new agent new_agent = create_agent(agent_info={"name": import_agent_info.name, + "display_name": import_agent_info.display_name, "description": import_agent_info.description, "business_description": import_agent_info.business_description, "model_name": import_agent_info.model_name, @@ -399,4 +467,121 @@ def insert_related_agent_impl(parent_agent_id, child_agent_id, tenant_id): return JSONResponse( status_code=400, content={"message":"Failed to insert relation", "status": "error"} - ) \ No newline at end of file + ) + + +# Helper function for run_agent_stream, used to prepare context for an agent run +async def prepare_agent_run(agent_request: AgentRequest, http_request: Request, authorization: str): + """ + Prepare for an agent run by creating context and run info, and registering the run. + """ + user_id, tenant_id, language = get_current_user_info(authorization, http_request) + + memory_context = build_memory_context(user_id, tenant_id, agent_request.agent_id) + agent_run_info = await create_agent_run_info(agent_id=agent_request.agent_id, + minio_files=agent_request.minio_files, + query=agent_request.query, + history=agent_request.history, + authorization=authorization, + language=language) + agent_run_manager.register_agent_run(agent_request.conversation_id, agent_run_info) + return agent_run_info, memory_context + + +# Helper function for run_agent_stream, used to save messages for either user or assistant +def save_messages(agent_request, target:str, messages=None, authorization=None): + if target == "user": + if messages is not None: + raise ValueError("Messages should be None when saving for user.") + submit(save_conversation_user, agent_request, authorization) + elif target == "assistant": + if messages is None: + raise ValueError("Messages cannot be None when saving for assistant.") + submit(save_conversation_assistant, agent_request, messages, authorization) + + +# Helper function for run_agent_stream, used to generate stream response +async def generate_stream(agent_run_info, memory_context, agent_request: AgentRequest, authorization: str): + messages = [] + try: + async for chunk in agent_run(agent_run_info, memory_context): + messages.append(chunk) + yield f"data: {chunk}\n\n" + except Exception as e: + logger.error(f"Agent run error: {str(e)}") + raise HTTPException(status_code=500, detail=f"Agent run error: {str(e)}") + finally: + # Save assistant message only if not in debug mode + if not agent_request.is_debug: + save_messages(agent_request, target="assistant", messages=messages, authorization=authorization) + # Unregister agent run instance for both debug and non-debug modes + agent_run_manager.unregister_agent_run(agent_request.conversation_id) + + +async def run_agent_stream(agent_request: AgentRequest, http_request: Request, authorization: str): + """ + Start an agent run and stream responses, using explicit user/tenant context. + Mirrors the logic of agent_app.agent_run_api but reusable by services. + """ + agent_run_info, memory_context = await prepare_agent_run( + agent_request=agent_request, + http_request=http_request, + authorization=authorization + ) + + # Save user message only if not in debug mode + if not agent_request.is_debug: + save_messages( + agent_request, + target="user", + authorization=authorization + ) + + return StreamingResponse( + generate_stream(agent_run_info, memory_context, agent_request, authorization), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive" + } + ) + + +def stop_agent_tasks(conversation_id: int): + """ + Stop agent run and preprocess tasks for the specified conversation_id. + Matches the behavior of agent_app.agent_stop_api. + """ + # Stop agent run + agent_stopped = agent_run_manager.stop_agent_run(conversation_id) + + # Stop preprocess tasks + preprocess_stopped = preprocess_manager.stop_preprocess_tasks(conversation_id) + + if agent_stopped or preprocess_stopped: + message_parts = [] + if agent_stopped: + message_parts.append("agent run") + if preprocess_stopped: + message_parts.append("preprocess tasks") + + message = f"successfully stopped {' and '.join(message_parts)} for conversation_id {conversation_id}" + logging.info(message) + return {"status": "success", "message": message} + else: + message = f"no running agent or preprocess tasks found for conversation_id {conversation_id}" + logging.error(message) + return {"status": "error", "message": message} + + +def get_agent_id_by_name(agent_name: str, tenant_id: str) -> int: + """ + Resolve unique agent id by its unique name under the same tenant. + """ + if not agent_name: + raise HTTPException(status_code=400, detail="agent_name required") + try: + return search_agent_id_by_agent_name(agent_name, tenant_id) + except Exception as _: + logger.error(f"Failed to find agent id with '{agent_name}' in tenant {tenant_id}") + raise HTTPException(status_code=404, detail="agent not found") diff --git a/backend/services/remote_mcp_service.py b/backend/services/remote_mcp_service.py index ef603957d..0d02d17bc 100644 --- a/backend/services/remote_mcp_service.py +++ b/backend/services/remote_mcp_service.py @@ -1,7 +1,7 @@ import logging from fastapi.responses import JSONResponse -from database.remote_mcp_db import create_mcp_record, delete_mcp_record_by_name_and_url, get_mcp_records_by_tenant +from database.remote_mcp_db import create_mcp_record, delete_mcp_record_by_name_and_url, get_mcp_records_by_tenant, check_mcp_name_exists from fastmcp import Client logger = logging.getLogger("remote_mcp_service") @@ -34,6 +34,15 @@ async def add_remote_mcp_server_list(tenant_id: str, remote_mcp_server: str, remote_mcp_server_name: str): + # check if MCP name already exists + if check_mcp_name_exists(mcp_name=remote_mcp_server_name, tenant_id=tenant_id): + logger.error( + f"MCP name already exists, tenant_id: {tenant_id}, remote_mcp_server_name: {remote_mcp_server_name}") + return JSONResponse( + status_code=400, + content={"message": f"MCP server name '{remote_mcp_server_name}' already exists", "status": "error"} + ) + # check if the address is available response = await mcp_server_health(remote_mcp_server=remote_mcp_server) if response.status_code != 200: diff --git a/backend/utils/auth_utils.py b/backend/utils/auth_utils.py index 29722c4d4..ccb71e8db 100644 --- a/backend/utils/auth_utils.py +++ b/backend/utils/auth_utils.py @@ -35,6 +35,10 @@ def get_jwt_expiry_seconds(token: str) -> int: int: 令牌的有效期(秒),如果解析失败则返回默认值3600 """ try: + # Speed mode: treat sessions as never expiring + if IS_SPEED_MODE: + # 10 years in seconds + return 10 * 365 * 24 * 60 * 60 # 确保token是纯JWT,去除可能的Bearer前缀 jwt_token = token.replace("Bearer ", "") if token.startswith("Bearer ") else token @@ -68,6 +72,10 @@ def calculate_expires_at(token: Optional[str] = None) -> int: Returns: int: 过期时间的时间戳 """ + # Speed mode: far future expiration + if IS_SPEED_MODE: + return int((datetime.now() + timedelta(days=3650)).timestamp()) + expiry_seconds = get_jwt_expiry_seconds(token) if token else 3600 return int((datetime.now() + timedelta(seconds=expiry_seconds)).timestamp()) diff --git a/backend/utils/logging_utils.py b/backend/utils/logging_utils.py index 22fd60012..209679419 100644 --- a/backend/utils/logging_utils.py +++ b/backend/utils/logging_utils.py @@ -28,10 +28,6 @@ def configure_logging(level=logging.INFO): root_logger.addHandler(handler) root_logger.setLevel(level) - # --- Silence overly verbose third-party libraries ---------------------- - for name in ("mem0", "mem0.memory", "mem0.memory.main"): - logging.getLogger(name).setLevel(logging.WARNING) - def configure_elasticsearch_logging(): """Configure logging for Elasticsearch client to reduce verbosity""" diff --git a/doc/docs/assets/architecture_en.png b/doc/docs/assets/architecture_en.png new file mode 100644 index 000000000..eda19615e Binary files /dev/null and b/doc/docs/assets/architecture_en.png differ diff --git a/doc/docs/assets/architecture_zh.png b/doc/docs/assets/architecture_zh.png new file mode 100644 index 000000000..61e5c875f Binary files /dev/null and b/doc/docs/assets/architecture_zh.png differ diff --git a/doc/docs/en/deployment/docker-build.md b/doc/docs/en/deployment/docker-build.md index ea2f838e0..06b6c0bfd 100644 --- a/doc/docs/en/deployment/docker-build.md +++ b/doc/docs/en/deployment/docker-build.md @@ -19,6 +19,10 @@ docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t ccr.c # 📚 build documentation for multiple architectures docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t nexent/nexent-docs -f make/docs/Dockerfile . --push docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t ccr.ccs.tencentyun.com/nexent-hub/nexent-docs -f make/docs/Dockerfile . --push + +# 💻 build Ubuntu Terminal for multiple architectures +docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t nexent/nexent-terminal -f make/terminal/Dockerfile . --push +docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t ccr.ccs.tencentyun.com/nexent-hub/nexent-terminal -f make/terminal/Dockerfile . --push ``` ### 💻 Local Development Build @@ -35,6 +39,9 @@ docker build --progress=plain -t nexent/nexent-web -f make/web/Dockerfile . # 📚 Build documentation image (current architecture only) docker build --progress=plain -t nexent/nexent-docs -f make/docs/Dockerfile . + +# 💻 Build OpenSSH Server image (current architecture only) +docker build --progress=plain -t nexent/nexent-ubuntu-terminal -f make/terminal/Dockerfile . ``` ### 🧹 Clean up Docker resources @@ -66,6 +73,23 @@ docker builder prune -f && docker system prune -f - Built from `make/docs/Dockerfile` - Provides project documentation and API reference +#### OpenSSH Server Image (nexent/nexent-ubuntu-terminal) +- Ubuntu 24.04-based SSH server container +- Built from `make/terminal/Dockerfile` +- Pre-installed with Conda, Python, Git and other development tools +- Supports SSH key authentication with username `linuxserver.io` +- Provides complete development environment + +##### Pre-installed Tools and Features +- **Python Environment**: Python 3 + pip + virtualenv +- **Conda Management**: Miniconda3 environment management +- **Development Tools**: Git, Vim, Nano, Curl, Wget +- **Build Tools**: build-essential, Make +- **SSH Service**: Port 2222, root login and password authentication disabled +- **User Permissions**: `linuxserver.io` user has sudo privileges (no password required) +- **Timezone Setting**: Asia/Shanghai +- **Security Configuration**: SSH key authentication, 60-minute session timeout + ### 🏷️ Tagging Strategy Each image is pushed to two repositories: @@ -77,6 +101,7 @@ All images include: - `nexent/nexent-data-process` - Data processing service - `nexent/nexent-web` - Next.js frontend application - `nexent/nexent-docs` - Vitepress documentation site +- `nexent/nexent-ubuntu-terminal` - OpenSSH development server container ## 📚 Documentation Image Standalone Deployment diff --git a/doc/docs/en/getting-started/installation.md b/doc/docs/en/getting-started/installation.md index bd6fcffc8..a26c8f2ba 100644 --- a/doc/docs/en/getting-started/installation.md +++ b/doc/docs/en/getting-started/installation.md @@ -107,7 +107,7 @@ The deployment includes the following components: | MinIO API | 9000 | 9010 | Object storage API | | MinIO Console | 9001 | 9011 | Storage management UI | | Redis | 6379 | 6379 | Cache service | -| SSH Server | 2222 | 2222 | Terminal tool access | +| SSH Server | 22 | 2222 | Terminal tool access | For complete port mapping details, see our [Dev Container Guide](../deployment/devcontainer.md#port-mapping). diff --git a/doc/docs/en/getting-started/software-architecture.md b/doc/docs/en/getting-started/software-architecture.md index 2a1429307..701d89319 100644 --- a/doc/docs/en/getting-started/software-architecture.md +++ b/doc/docs/en/getting-started/software-architecture.md @@ -1,7 +1,166 @@ # Software Architecture -We will update the comprehensive software architecture diagram and documentation soon. +Nexent adopts a modern distributed microservices architecture designed to provide high-performance, scalable AI agent platform. The entire system is based on containerized deployment, supporting cloud-native and enterprise-grade application scenarios. + +![Software Architecture Diagram](../../assets/architecture_en.png) + +## 🏗️ Overall Architecture Design + +Nexent's software architecture follows layered design principles, structured into the following core layers from top to bottom: + +### 🌐 Frontend Layer +- **Technology Stack**: Next.js + React + TypeScript +- **Functions**: User interface, agent interaction, multimodal input processing +- **Features**: Responsive design, real-time communication, internationalization support + +### 🔌 API Gateway Layer +- **Core Service**: FastAPI high-performance web framework +- **Responsibilities**: Request routing, authentication, API version management, load balancing +- **Ports**: 5010 (main service), 5012 (data processing service) + +### 🧠 Business Logic Layer +- **Agent Management**: Agent generation, execution, monitoring +- **Conversation Management**: Multi-turn dialogue, context maintenance, history tracking +- **Knowledge Base Management**: Document processing, vectorization, retrieval +- **Model Management**: Multi-model support, health checks, load balancing + +### 📊 Data Layer +Distributed data storage architecture with multiple specialized databases: + +#### 🗄️ Structured Data Storage +- **PostgreSQL**: Primary database storing user information, agent configurations, conversation records +- **Port**: 5434 +- **Features**: ACID transactions, relational data integrity + +#### 🔍 Search Engine +- **Elasticsearch**: Vector database and full-text search engine +- **Port**: 9210 +- **Functions**: Vector similarity search, hybrid search, large-scale optimization + +#### 💾 Cache Layer +- **Redis**: High-performance in-memory database +- **Port**: 6379 +- **Usage**: Session caching, temporary data, distributed locks + +#### 📁 Object Storage +- **MinIO**: Distributed object storage service +- **Port**: 9010 +- **Functions**: File storage, multimedia resource management, large file processing + +## 🔧 Core Service Architecture + +### 🤖 Agent Services +``` +Agent framework based on SmolAgents, providing: +├── Agent generation and configuration +├── Tool calling and integration +├── Reasoning and decision execution +└── Lifecycle management +``` + +### 📈 Data Processing Services +``` +Distributed data processing architecture: +├── Real-time document processing (20+ format support) +├── Batch data processing pipelines +├── OCR and table structure extraction +└── Vectorization and index construction +``` + +### 🌐 MCP Ecosystem +``` +Model Context Protocol tool integration: +├── Standardized tool interfaces +├── Plugin architecture +├── Third-party service integration +└── Custom tool development +``` + +## 🚀 Distributed Architecture Features + +### ⚡ Asynchronous Processing Architecture +- **Foundation Framework**: High-performance async processing based on asyncio +- **Concurrency Control**: Thread-safe concurrent processing mechanisms +- **Task Queue**: Celery + Ray distributed task execution +- **Stream Processing**: Real-time data and response streaming + +### 🔄 Microservices Design +``` +Service decomposition strategy: +├── nexent (main service) - Agent core logic +├── nexent-data-process (data processing) - Document processing pipeline +├── nexent-mcp-service (MCP service) - Tool protocol service +└── Optional services (SSH, monitoring, etc.) +``` + +### 🌍 Containerized Deployment +``` +Docker Compose service orchestration: +├── Application service containerization +├── Database service isolation +├── Network layer security configuration +└── Volume mounting for data persistence +``` + +## 🔐 Security and Scalability + +### 🛡️ Security Architecture +- **Authentication**: Multi-tenant support, user permission management +- **Data Security**: End-to-end encryption, secure transmission protocols +- **Network Security**: Inter-service secure communication, firewall configuration + +### 📈 Scalability Design +- **Horizontal Scaling**: Independent microservice scaling, load balancing +- **Vertical Scaling**: Resource pool management, intelligent scheduling +- **Storage Scaling**: Distributed storage, data sharding + +### 🔧 Modular Architecture +- **Loose Coupling Design**: Low inter-service dependencies, standardized interfaces +- **Plugin Architecture**: Hot-swappable tools and models +- **Configuration Management**: Environment isolation, dynamic configuration updates + +## 🔄 Data Flow Architecture + +### 📥 User Request Flow +``` +User Input → Frontend Validation → API Gateway → Route Distribution → Business Service → Data Access → Database +``` + +### 🤖 Agent Execution Flow +``` +User Message → Agent Creation → Tool Calling → Model Inference → Streaming Response → Result Storage +``` + +### 📚 Knowledge Base Processing Flow +``` +File Upload → Temporary Storage → Data Processing → Vectorization → Knowledge Base Storage → Index Update +``` + +### ⚡ Real-time Processing Flow +``` +Real-time Input → Instant Processing → Agent Response → Streaming Output +``` + +## 🎯 Architecture Advantages + +### 🏢 Enterprise-grade Features +- **High Availability**: Multi-layer redundancy, failover capabilities +- **High Performance**: Asynchronous processing, intelligent caching +- **High Concurrency**: Distributed architecture, load balancing +- **Monitoring Friendly**: Comprehensive logging and status monitoring + +### 🔧 Developer Friendly +- **Modular Development**: Clear hierarchical structure +- **Standardized Interfaces**: Unified API design +- **Flexible Configuration**: Environment adaptation, feature toggles +- **Easy Testing**: Unit testing and integration testing support + +### 🌱 Ecosystem Compatibility +- **MCP Standard**: Compliant with Model Context Protocol +- **Open Source Ecosystem**: Integration with rich open source tools +- **Cloud Native**: Support for Kubernetes and Docker deployment +- **Multi-model Support**: Compatible with mainstream AI model providers --- -*This documentation is under active development. We will update it with comprehensive architecture details soon.* \ No newline at end of file +This architectural design ensures that Nexent can provide a stable, scalable AI agent service platform while maintaining high performance. Whether for individual users or enterprise-level deployments, it delivers excellent user experience and technical assurance. \ No newline at end of file diff --git a/doc/docs/en/user-guide/assets/memory/add-mem.png b/doc/docs/en/user-guide/assets/memory/add-mem.png new file mode 100644 index 000000000..314baa403 Binary files /dev/null and b/doc/docs/en/user-guide/assets/memory/add-mem.png differ diff --git a/doc/docs/en/user-guide/assets/memory/delete-mem.png b/doc/docs/en/user-guide/assets/memory/delete-mem.png new file mode 100644 index 000000000..7d5c960e6 Binary files /dev/null and b/doc/docs/en/user-guide/assets/memory/delete-mem.png differ diff --git a/doc/docs/en/user-guide/assets/memory/mem-config.png b/doc/docs/en/user-guide/assets/memory/mem-config.png new file mode 100644 index 000000000..113ed4d83 Binary files /dev/null and b/doc/docs/en/user-guide/assets/memory/mem-config.png differ diff --git a/doc/docs/en/user-guide/memory.md b/doc/docs/en/user-guide/memory.md new file mode 100644 index 000000000..5d0ea53b7 --- /dev/null +++ b/doc/docs/en/user-guide/memory.md @@ -0,0 +1,85 @@ +# 🧠Nexent Intelligent Memory System Technical Specification + +## 1. System Architecture Overview + +The Nexent Intelligent Memory System is built on an advanced memory storage architecture that provides intelligent agents with persistent context-aware capabilities. Through a multi-layered memory management mechanism, the system achieves cross-conversation knowledge accumulation and retrieval, significantly enhancing the coherence and personalization of human-machine interactions. + +### Core Technical Features +- **Layered Memory Architecture**: Four-level memory storage system built on the mem0 framework +- **Adaptive Memory Management**: Supports both automated and manual memory operation modes +- **Cross-Session Persistence**: Ensures continuity of knowledge and context across multiple conversations +- **Fine-Grained Permission Control**: Provides flexible memory sharing strategy configuration + +--- + +## 2. Configuration and Initialization + +### 2.1 System Activation +1. Access the memory management interface: Click the **Memory Management Icon** in the upper right corner of the conversation interface +2. Enter the **System Configuration** module for initialization settings + +### 2.2 Core Configuration Parameters + +| Configuration Item | Options | Default Value | Description | +|-------------------|---------|---------------|-------------| +| Memory Service Status | Enable/Disable | Enable | Controls the operational status of the entire memory system | +| Agent Memory Sharing Strategy | Always Share/Ask Me Each Time/Prohibit Sharing | Always Share | Defines whether user authorization consent is required for memory sharing between agents | + +
+ Select Agent +
+ +--- + +## 3. Layered Memory Architecture + +Nexent adopts a four-layer memory storage architecture based on **mem0**, achieving precise memory classification and retrieval through different scopes and lifecycle management: + +### 3.1 Architecture Layer Details + +| Memory Level | Scope | Storage Content | Lifecycle | Configuration Role | Typical Applications | +|--------------|-------|-----------------|-----------|-------------------|---------------------| +| **Tenant Level Memory** | Organization-wide | Enterprise-level standard operating procedures, compliance policies, organizational structure, factual information | Long-term storage | Tenant Administrator | Enterprise knowledge management, standardized process execution, compliance checking | +| **Agent Level Memory** | Specific Agent | Professional domain knowledge, skill templates, historical conversation summaries, learning accumulation | Consistent with agent lifecycle | Tenant Administrator | Professional skill accumulation, domain knowledge sedimentation, experiential learning | +| **User Level Memory** | Specific User Account | Personal preference settings, usage habits, common instruction templates, personal information | Long-term storage | All Users | Personalized services, user experience optimization, preference management | +| **User-Agent Level Memory** | Specific Agent under Specific User Account | Collaboration history, personalized factual information, specific task context, relationship models | Consistent with agent lifecycle | All Users | Deep collaboration scenarios, personalized tuning, task continuity maintenance | + +### 3.2 Memory Priority and Retrieval Strategy + +Memory retrieval follows the following priority order (from high to low): +1. **Tenant Level** → Basic facts +2. **User-Agent Level** → Most specific context information +3. **User Level** → Personal preferences and habits +4. **Agent Level** → Professional knowledge and skills + +--- + +## 4. Operation Modes and Functional Interfaces + +### 4.1 Automated Memory Management +- **Intelligent Extraction**: Automatically identifies key factual information in conversations and generates memory entries +- **Automatic Context Embedding**: Agents automatically retrieve the most relevant memory entries and implicitly embed them in conversation context +- **Incremental Updates**: Supports progressive updates, supplementation, and automatic cleanup of memory content + +### 4.2 Manual Memory Operations + +#### Adding Memory +- Click the green plus button, input text, then click the checkmark to add a memory entry (maximum 500 characters) + +
+ Select Agent +
+ +#### Deleting Memory +- Click the red cross button, then click confirm in the popup confirmation dialog to delete all memory entries under a specific Agent group +- Click the red eraser button to delete a specific memory entry + +
+ Select Agent +
+ +### 4.3 Memory Management Best Practices + +1. **Atomicity Principle**: Each memory entry should contain **concise**, **single**, **clear** factual information +2. **Temporal Management**: Regularly clean up outdated or no longer relevant memory entries to maintain the timeliness and accuracy of the memory database +3. **Privacy Protection**: Sensitive information should be avoided from being shared at the tenant level or agent level \ No newline at end of file diff --git a/doc/docs/zh/backend/tools/mcp.md b/doc/docs/zh/backend/tools/mcp.md index 46b321bb8..a76e181a7 100644 --- a/doc/docs/zh/backend/tools/mcp.md +++ b/doc/docs/zh/backend/tools/mcp.md @@ -1,478 +1,590 @@ -# 分层代理架构说明 +# Nexent MCP架构说明 -## 系统架构流程图 +## 系统架构概述 + +Nexent采用**本地MCP服务 + 直接远程连接**的架构,通过MCP(Model Context Protocol)协议实现本地服务与远程服务的统一管理。系统包含两个核心服务: + +### 1. 主服务 (FastAPI) - 端口 5010 +- **用途**:提供Web管理界面和RESTful API,作为前端唯一入口 +- **特点**:面向用户管理,包含认证、多租户支持,管理MCP服务器配置 +- **启动文件**:`main_service.py` + +### 2. 本地MCP服务 (FastMCP) - 端口 5011 +- **用途**:提供本地MCP协议服务,挂载本地工具 +- **特点**:MCP协议标准,仅提供本地服务,不代理远程服务 +- **启动文件**:`nexent_mcp_service.py` + +### 3. 远程MCP服务 +- **用途**:外部MCP服务,提供远程工具 +- **特点**:智能体执行时直接连接,不通过本地MCP服务代理 + +## 核心组件架构 ```mermaid graph TD - A["前端请求"] --> B["主服务 (FastAPI)
(端口: 5010)"] - - B --> B1["Web API管理层
(/api/mcp/*)"] - B1 --> B2["/api/mcp/tools/
(获取工具信息)"] - B1 --> B3["/api/mcp/add
(添加MCP服务器)"] - B1 --> B4["/api/mcp/
(删除MCP服务器)"] - B1 --> B5["/api/mcp/list
(列出MCP服务器)"] - B1 --> B6["/api/mcp/recover
(恢复MCP服务器)"] - - B --> C["MCP服务 (FastMCP)
(端口: 5011)"] + A["前端客户端"] --> B["主服务 (FastAPI)
(端口: 5010)"] - C --> C1["本地服务层"] - C --> C2["远程代理层"] - C --> C3["MCP协议API层"] + B --> B1["remote_mcp_app.py
(MCP管理路由)"] + B1 --> B2["remote_mcp_service.py
(MCP服务逻辑)"] + B2 --> B3["数据库
(MCP配置存储)"] - C1 --> C11["local_mcp_service
(稳定挂载)"] + B --> C["create_agent_info.py
(智能体配置)"] + C --> C1["工具发现与配置"] + C --> C2["MCP服务器过滤"] - C2 --> C21["RemoteProxyManager
(动态管理)"] - C21 --> C22["远程代理1"] - C21 --> C23["远程代理2"] - C21 --> C24["远程代理n..."] + B --> D["run_agent.py
(智能体执行)"] + D --> D1["ToolCollection
(MCP工具集合)"] + D1 --> E["本地MCP服务 (FastMCP)
(端口: 5011)"] + D1 --> F1["远程MCP服务1
(直接连接)"] + D1 --> F2["远程MCP服务2
(直接连接)"] + D1 --> F3["远程MCP服务n
(直接连接)"] - C3 --> C31["/healthcheck
(连通性检查)"] - C3 --> C32["/list-remote-proxies
(列出代理)"] - C3 --> C33["/add-remote-proxies
(添加代理)"] - C3 --> C34["/remote-proxies
(删除代理)"] - - C22 --> D1["远程MCP服务1
(SSE/HTTP)"] - C23 --> D2["远程MCP服务2
(SSE/HTTP)"] - C24 --> D3["远程MCP服务n
(SSE/HTTP)"] + E --> E1["local_mcp_service
(本地工具)"] style A fill:#e1f5fe style B fill:#f3e5f5 - style C fill:#e8f5e8 + style E fill:#e8f5e8 style B1 fill:#fff3e0 - style C1 fill:#e8f5e8 - style C2 fill:#fff3e0 - style C3 fill:#fce4ec + style C fill:#e8f5e8 + style D fill:#fce4ec + style F1 fill:#fff3e0 + style F2 fill:#fff3e0 + style F3 fill:#fff3e0 ``` -## 架构概述 +## 核心功能模块 -本系统实现了一个**双服务代理架构**,包含两个独立服务: +### 1. 本地MCP服务管理 (nexent_mcp_service.py) -### 1. 主服务 (FastAPI) - 端口 5010 -- **用途**:提供Web管理界面和RESTful API,作为前端唯一入口 -- **特点**:面向用户管理,包含认证、多租户支持,代理MCP服务调用 -- **启动文件**:`main_service.py` +**本地MCP服务实现**: +```python +# 初始化本地MCP服务 +nexent_mcp = FastMCP(name="nexent_mcp") -### 2. MCP服务 (FastMCP) - 端口 5011 -- **用途**:提供MCP协议服务和代理管理(内部服务) -- **特点**:MCP协议标准,支持本地服务和远程代理,仅供主服务调用 -- **启动文件**:`nexent_mcp_service.py` - -**重要说明**:前端客户端仅直接访问主服务(5010),所有MCP相关操作均由主服务代为调用MCP服务(5011)完成。 - -## 核心功能 - -### 1. 本地服务稳定性 -- `local_mcp_service` 等本地服务始终保持稳定运行 -- 远程代理的添加、删除、更新不会影响本地服务 +# 挂载本地服务(稳定,不受远程服务影响) +nexent_mcp.mount(local_mcp_service.name, local_mcp_service) +``` -### 2. 动态远程代理管理 -- 支持动态添加、删除、更新远程MCP服务代理 -- 每个远程代理作为独立的服务进行管理 -- 支持多种传输方式(SSE、HTTP) +**特点**: +- 仅提供本地MCP服务,挂载本地工具 +- 不代理远程MCP服务 +- 基于FastMCP框架,提供标准MCP协议支持 +- 服务稳定运行,端口5011 -### 3. 双层API接口 +### 2. MCP管理API (remote_mcp_app.py) -#### 主服务API (端口 5010) - 对外管理层 -**前端客户端直接访问的接口**,提供面向用户的管理功能,支持认证和多租户: +提供完整的MCP服务器管理接口: -**获取远程MCP服务器工具信息** +#### 获取远程MCP工具信息 ```http -GET /api/mcp/tools/?service_name={name}&mcp_url={url} +POST /api/mcp/tools?service_name={name}&mcp_url={url} Authorization: Bearer {token} ``` -**添加远程MCP服务器** +#### 添加远程MCP服务器 ```http POST /api/mcp/add?mcp_url={url}&service_name={name} Authorization: Bearer {token} ``` -**删除远程MCP服务器** +#### 删除远程MCP服务器 ```http DELETE /api/mcp/?service_name={name}&mcp_url={url} Authorization: Bearer {token} ``` -**获取远程MCP服务器列表** +#### 获取MCP服务器列表 ```http GET /api/mcp/list Authorization: Bearer {token} ``` -**恢复远程MCP服务器** +#### MCP服务器健康检查 ```http -GET /api/mcp/recover +GET /api/mcp/healthcheck?mcp_url={url}&service_name={name} Authorization: Bearer {token} ``` -#### MCP服务API (端口 5011) - 内部协议层 -**内部接口,主要供主服务调用**,也可供外部MCP客户端直接使用: +### 3. MCP服务逻辑 (remote_mcp_service.py) -**连通性检查** -```http -GET /healthcheck?mcp_url={url} +**核心功能**: + +#### 服务器健康检查 +```python +async def mcp_server_health(remote_mcp_server: str) -> JSONResponse: + # 使用FastMCP Client验证远程服务连接 + client = Client(remote_mcp_server) + async with client: + connected = client.is_connected() + # 返回连接状态 ``` -快速检查远程MCP服务是否可达,返回简单的连接状态。 -**列出所有远程代理** -```http -GET /list-remote-proxies +#### 添加MCP服务器 +```python +async def add_remote_mcp_server_list(tenant_id, user_id, remote_mcp_server, remote_mcp_server_name): + # 1. 检查服务名是否已存在 + # 2. 验证远程服务连接 + # 3. 保存到数据库 + # 4. 返回操作结果 ``` -**添加远程代理** -```http -POST /add-remote-proxies -Content-Type: application/json +#### 删除MCP服务器 +```python +async def delete_remote_mcp_server_list(tenant_id, user_id, remote_mcp_server, remote_mcp_server_name): + # 1. 从数据库删除记录 + # 2. 返回操作结果 +``` -{ - "service_name": "my_service", - "mcp_url": "http://localhost:5012/sse", - "transport": "sse" -} +### 4. 智能体配置管理 (create_agent_info.py) + +**MCP服务器过滤机制**: +```python +def filter_mcp_servers_and_tools(input_agent_config: AgentConfig, mcp_info_dict) -> list: + """ + 过滤MCP服务器和工具,只保留实际使用的MCP服务器 + 支持多级智能体,递归检查所有子智能体工具 + """ + used_mcp_urls = set() + + def check_agent_tools(agent_config: AgentConfig): + # 检查当前智能体工具 + for tool in agent_config.tools: + if tool.source == "mcp" and tool.usage in mcp_info_dict: + used_mcp_urls.add(mcp_info_dict[tool.usage]["remote_mcp_server"]) + + # 递归检查子智能体 + for sub_agent_config in agent_config.managed_agents: + check_agent_tools(sub_agent_config) + + check_agent_tools(input_agent_config) + return list(used_mcp_urls) ``` -**删除远程代理** -```http -DELETE /remote-proxies?service_name={service_name} +**智能体运行信息创建**: +```python +async def create_agent_run_info(agent_id, minio_files, query, history, authorization, language='zh'): + # 1. 获取用户和租户信息 + # 2. 创建模型配置列表 + # 3. 创建智能体配置 + # 4. 获取远程MCP服务器列表 + # 5. 过滤实际使用的MCP服务器 + # 6. 创建智能体运行信息 ``` -## 使用方法 +### 5. 智能体执行引擎 (run_agent.py) + +**MCP工具集成**: +```python +def agent_run_thread(agent_run_info: AgentRunInfo, memory_context: MemoryContext): + mcp_host = agent_run_info.mcp_host + + if mcp_host is None or len(mcp_host) == 0: + # 无MCP服务器:使用本地工具 + nexent = NexentAgent(...) + agent = nexent.create_single_agent(agent_run_info.agent_config) + # ... + else: + # 有MCP服务器:使用ToolCollection直接连接所有MCP服务 + agent_run_info.observer.add_message("", ProcessType.AGENT_NEW_RUN, "") + mcp_client_list = [{"url": mcp_url} for mcp_url in mcp_host] + + with ToolCollection.from_mcp(mcp_client_list, trust_remote_code=True) as tool_collection: + # ToolCollection会同时连接本地MCP服务(5011)和远程MCP服务 + nexent = NexentAgent( + mcp_tool_collection=tool_collection, + # ... + ) + # 执行智能体 +``` -### 1. 启动服务 +## 数据流程 -**启动主服务** -```bash -cd backend -python main_service.py +### 1. MCP服务器添加流程 + +```mermaid +sequenceDiagram + participant C as 前端客户端 + participant A as remote_mcp_app + participant S as remote_mcp_service + participant DB as 数据库 + participant MCP as 远程MCP服务 + + C->>A: POST /api/mcp/add + A->>S: add_remote_mcp_server_list() + S->>S: 检查服务名是否存在 + S->>MCP: mcp_server_health() + MCP-->>S: 连接状态 + alt 连接成功 + S->>DB: create_mcp_record() + DB-->>S: 保存结果 + S-->>A: 成功响应 + A-->>C: 成功响应 + else 连接失败 + S-->>A: 错误响应 + A-->>C: 错误响应 + end ``` -服务将在 `http://localhost:5010` 启动。 -**启动MCP服务** -```bash -cd backend -python nexent_mcp_service.py +### 2. 智能体执行流程 + +```mermaid +sequenceDiagram + participant C as 前端客户端 + participant A as create_agent_info + participant R as run_agent + participant TC as ToolCollection + participant LMCP as 本地MCP服务(5011) + participant RMCP1 as 远程MCP服务1 + participant RMCP2 as 远程MCP服务2 + + C->>A: create_agent_run_info() + A->>A: filter_mcp_servers_and_tools() + A-->>C: AgentRunInfo + + C->>R: agent_run() + R->>R: agent_run_thread() + + alt 有MCP服务器 + R->>TC: ToolCollection.from_mcp() + TC->>LMCP: 连接本地MCP服务 + TC->>RMCP1: 直接连接远程MCP服务1 + TC->>RMCP2: 直接连接远程MCP服务2 + LMCP-->>TC: 本地工具列表 + RMCP1-->>TC: 远程工具列表1 + RMCP2-->>TC: 远程工具列表2 + TC-->>R: 合并的工具集合 + R->>R: 执行智能体 + else 无MCP服务器 + R->>R: 使用本地工具执行 + end + + R-->>C: 执行结果 ``` -服务将在 `http://localhost:5011` 启动。 -### 2. 使用API +## 关键特性 -#### 推荐方式:通过主服务管理MCP服务器 -**前端客户端应使用此方式**,具备完整的认证和权限管理: +### 1. 多租户隔离 +- 所有MCP服务器配置基于`tenant_id`进行隔离 +- 用户只能访问自己租户的MCP服务器 -```bash -# 添加远程MCP服务器 -curl -X POST "http://localhost:5010/api/mcp/add?mcp_url=http://external-server:5012/sse&service_name=external_service" \ - -H "Authorization: Bearer {your_token}" +### 2. 动态MCP管理 +- 支持运行时添加、删除MCP服务器配置 +- 自动健康检查和状态更新 +- 数据库持久化存储配置 +- 智能体执行时直接连接远程MCP服务 -# 获取MCP服务器列表 -curl -H "Authorization: Bearer {your_token}" \ - "http://localhost:5010/api/mcp/list" -``` +### 3. 智能工具过滤 +- 只连接智能体实际使用的MCP服务器 +- 支持多级智能体的递归工具检查 +- 避免不必要的网络连接 +- 本地MCP服务(5011)始终可用,远程服务按需连接 -#### 内部调试:直接访问MCP服务(可选) -**仅用于调试或外部MCP客户端直接集成**: +### 4. 错误处理 +- MCP连接失败时的优雅降级 +- 详细的错误日志和状态反馈 +- 连接超时保护机制 +### 5. 内存管理 +- 智能体执行完成后自动保存对话记忆 +- 支持多级记忆存储(租户、智能体、用户、用户智能体) +- 可配置的记忆共享策略 + +## 配置说明 + +### 环境变量 ```bash -# 测试远程服务连接 -curl "http://localhost:5011/healthcheck?mcp_url=http://external-server:5012/sse" +# MCP服务地址 +NEXENT_MCP_SERVER=http://localhost:5011 + +# 数据库配置 +DATABASE_URL=postgresql://... -# 添加远程代理 -curl -X POST http://localhost:5011/add-remote-proxies \ - -H "Content-Type: application/json" \ - -d '{ - "service_name": "external_service", - "mcp_url": "http://external-server:5012/sse", - "transport": "sse" - }' +# 其他配置... ``` -## 代码结构 +### 数据库表结构 +```sql +-- MCP服务器配置表 +CREATE TABLE mcp_servers ( + id SERIAL PRIMARY KEY, + tenant_id VARCHAR NOT NULL, + user_id VARCHAR NOT NULL, + mcp_name VARCHAR NOT NULL, + mcp_server VARCHAR NOT NULL, + status BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` -### 主服务组件 (main_service.py) -- **FastAPI应用**:提供Web API和管理界面 -- **多租户支持**:基于认证的多租户管理 -- **路由管理**:包含多个功能模块的路由器 +## 使用示例 -### MCP服务组件 (nexent_mcp_service.py) +### 1. 启动服务 +```bash +# 启动主服务 +cd backend +python main_service.py -#### RemoteProxyManager 类 -负责管理所有远程代理的生命周期: -- `add_remote_proxy()`: 添加新的远程代理 -- `remove_remote_proxy()`: 移除指定的远程代理 -- `update_remote_proxy()`: 更新现有远程代理 -- `list_remote_proxies()`: 列出所有远程代理配置 -- `_validate_remote_service()`: 验证远程服务连接 +# 启动本地MCP服务 +cd backend +python nexent_mcp_service.py +``` -#### MCP协议端点 -- `/healthcheck`: 连通性检查端点 -- `/list-remote-proxies`: 列出所有远程代理端点 -- `/add-remote-proxies`: 添加远程代理端点 -- `/remote-proxies`: 删除特定代理端点 +### 2. 添加远程MCP服务器 +```bash +curl -X POST "http://localhost:5010/api/mcp/add?mcp_url=http://external-server:5012/sse&service_name=external_service" \ + -H "Authorization: Bearer {your_token}" +``` -### 远程MCP管理 (remote_mcp_app.py) -- **认证集成**:与主服务认证系统集成 -- **数据持久化**:支持数据库存储和恢复 -- **服务发现**:工具信息获取和管理 +## MCP接口文档 -## 服务依赖关系 +### 1. 主服务API接口 (端口5010) -```mermaid -graph LR - A["前端客户端"] --> B["主服务 :5010
(FastAPI)"] - B --> C["MCP服务 :5011
(FastMCP)"] - B --> D["数据库
(用户/租户/配置)"] - C --> E["本地MCP服务"] - C --> F["远程MCP代理"] - - G["外部MCP客户端"] -.-> C - - style A fill:#e1f5fe - style B fill:#f3e5f5 - style C fill:#e8f5e8 - style G fill:#fff3e0 +#### 1.1 获取远程MCP工具信息 +```http +POST /api/mcp/tools ``` -## 错误处理 +**请求参数**: +- `service_name` (string, 必需): MCP服务名称 +- `mcp_url` (string, 必需): MCP服务器URL +- `Authorization` (Header, 必需): Bearer token -- 添加代理前会验证远程服务连接 -- 提供详细的错误信息和状态码 -- 支持优雅的服务卸载和重新加载 -- 双层错误处理:管理层和协议层 +**响应示例**: +```json +{ + "tools": [ + { + "name": "tool_name", + "description": "tool_description", + "parameters": {...} + } + ], + "status": "success" +} +``` -## 性能优化 +#### 1.2 添加远程MCP服务器 +```http +POST /api/mcp/add +``` -- 代理服务按需加载 -- 支持并发操作 -- 最小化对现有服务的影响 -- 服务间松耦合设计 +**请求参数**: +- `mcp_url` (string, 必需): MCP服务器URL +- `service_name` (string, 必需): MCP服务名称 +- `Authorization` (Header, 必需): Bearer token -## 接口时序图 +**响应示例**: +```json +{ + "message": "Successfully added remote MCP proxy", + "status": "success" +} +``` -### 1. 获取远程MCP工具信息 (GET /api/mcp/tools/) +#### 1.3 删除远程MCP服务器 +```http +DELETE /api/mcp +``` -```mermaid -sequenceDiagram - participant C as 前端客户端 - participant M as 主服务(5010) - participant T as 工具配置服务 - participant R as 远程MCP服务 +**请求参数**: +- `service_name` (string, 必需): MCP服务名称 +- `mcp_url` (string, 必需): MCP服务器URL +- `Authorization` (Header, 必需): Bearer token - C->>M: GET /api/mcp/tools/?service_name=xxx&mcp_url=xxx - Note over C,M: Authorization: Bearer token (可选) - - M->>T: get_tool_from_remote_mcp_server(service_name, mcp_url) - T->>R: 直接连接远程MCP服务获取工具列表 - R-->>T: 返回工具信息 - T-->>M: 工具信息列表 - - M-->>C: JSON响应 {tools: [...], status: "success"} - - Note over M,C: 错误情况下返回 400 状态码 - Note over T,R: 注意:此接口直接访问远程MCP,不经过本地MCP服务(5011) +**响应示例**: +```json +{ + "message": "Successfully deleted remote MCP proxy", + "status": "success" +} ``` -### 2. 添加远程MCP服务器 (POST /api/mcp/add) +#### 1.4 获取MCP服务器列表 +```http +GET /api/mcp/list +``` -```mermaid -sequenceDiagram - participant C as 前端客户端 - participant M as 主服务(5010) - participant A as 认证系统 - participant S as MCP服务管理 - participant DB as 数据库 - participant MCP as MCP服务(5011) - participant R as 远程MCP服务 +**请求参数**: +- `Authorization` (Header, 必需): Bearer token - C->>M: POST /api/mcp/add?mcp_url=xxx&service_name=xxx - Note over C,M: Authorization: Bearer token - - M->>A: get_current_user_id(authorization) - A-->>M: user_id, tenant_id - - M->>S: add_remote_mcp_server_list(tenant_id, user_id, mcp_url, service_name) - - S->>DB: 检查服务名是否已存在 - DB-->>S: 检查结果 - - alt 服务名已存在 - S-->>M: JSONResponse (409 - Service name already exists) - M-->>C: 错误响应 (409) - else 服务名可用 - S->>S: add_remote_proxy() - S->>MCP: POST /add-remote-proxies - MCP->>MCP: 验证远程MCP服务连接 - MCP->>R: 连通性测试 - R-->>MCP: 连接响应 - - alt MCP连接成功 - MCP->>MCP: 创建并挂载远程代理 - MCP-->>S: 200 - 成功添加代理 - S->>DB: 保存MCP服务器配置 - DB-->>S: 保存结果 - S-->>M: 成功结果 - M-->>C: JSON响应 {message: "Successfully added", status: "success"} - else MCP连接失败 - MCP-->>S: 错误响应 (503/409/400) - S-->>M: 错误结果/JSONResponse - M-->>C: 错误响应 (400/409/503) - end - end +**响应示例**: +```json +{ + "remote_mcp_server_list": [ + { + "id": 1, + "tenant_id": "tenant_123", + "user_id": "user_456", + "mcp_name": "external_service", + "mcp_server": "http://external-server:5012/sse", + "status": true, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ], + "status": "success" +} ``` -### 3. 删除远程MCP服务器 (DELETE /api/mcp/) +#### 1.5 MCP服务器健康检查 +```http +GET /api/mcp/healthcheck +``` -```mermaid -sequenceDiagram - participant C as 前端客户端 - participant M as 主服务(5010) - participant A as 认证系统 - participant S as MCP服务管理 - participant DB as 数据库 - participant MCP as MCP服务(5011) +**请求参数**: +- `mcp_url` (string, 必需): MCP服务器URL +- `service_name` (string, 必需): MCP服务名称 +- `Authorization` (Header, 必需): Bearer token - C->>M: DELETE /api/mcp/?service_name=xxx&mcp_url=xxx - Note over C,M: Authorization: Bearer token - - M->>A: get_current_user_id(authorization) - A-->>M: user_id, tenant_id - - M->>S: delete_remote_mcp_server_list(tenant_id, user_id, mcp_url, service_name) - - S->>DB: 查找并删除MCP服务器配置 - DB-->>S: 删除结果 - - alt 数据库删除失败 - S-->>M: JSONResponse (400 - server not record) - M-->>C: 错误响应 (400) - else 数据库删除成功 - S->>MCP: DELETE /remote-proxies?service_name=xxx - MCP->>MCP: 卸载远程代理服务 - - alt MCP删除成功 - MCP-->>S: 200 - 成功移除 - S-->>M: 成功结果 - M-->>C: JSON响应 {message: "Successfully deleted", status: "success"} - else MCP删除失败 - MCP-->>S: 404/400 - 删除失败 - S-->>M: 错误结果/JSONResponse - M-->>C: 错误响应 (400/404) - end - end +**响应示例**: +```json +{ + "message": "Successfully connected to remote MCP server", + "status": "success" +} ``` -### 4. 获取远程MCP服务器列表 (GET /api/mcp/list) +### 2. 本地MCP服务接口 (端口5011) + +#### 2.1 MCP协议接口 +本地MCP服务基于FastMCP框架,提供标准MCP协议支持: + +**服务地址**:`http://localhost:5011/sse` + +**支持的操作**: +- `tools/list`: 获取工具列表 +- `tools/call`: 调用工具 +- `resources/list`: 获取资源列表 +- `resources/read`: 读取资源 + +#### 2.2 本地工具服务 +本地MCP服务挂载了以下本地工具: +- 文件操作工具 +- 网络请求工具 +- 系统信息工具 +- 其他本地工具 + +### 3. 错误码说明 + +| 状态码 | 说明 | 处理建议 | +|--------|------|----------| +| 200 | 成功 | 正常处理响应数据 | +| 400 | 请求参数错误 | 检查请求参数格式和内容 | +| 401 | 认证失败 | 检查Authorization token是否有效 | +| 403 | 权限不足 | 确认用户权限 | +| 404 | 资源不存在 | 检查MCP服务器URL是否正确 | +| 409 | 服务名已存在 | 使用不同的服务名称 | +| 503 | 服务不可用 | 检查MCP服务器是否正常运行 | +| 500 | 服务器内部错误 | 查看服务器日志 | + +### 4. 前端API调用示例 + +#### 4.1 JavaScript/TypeScript调用 +```typescript +// 获取MCP服务器列表 +const getMcpServerList = async () => { + const response = await fetch('/api/mcp/list', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + return await response.json(); +}; + +// 添加MCP服务器 +const addMcpServer = async (mcpUrl: string, serviceName: string) => { + const response = await fetch(`/api/mcp/add?mcp_url=${mcpUrl}&service_name=${serviceName}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + return await response.json(); +}; +``` -```mermaid -sequenceDiagram - participant C as 前端客户端 - participant M as 主服务(5010) - participant A as 认证系统 - participant S as MCP服务管理 - participant DB as 数据库 +#### 4.2 cURL调用示例 +```bash +# 获取MCP服务器列表 +curl -H "Authorization: Bearer {your_token}" \ + "http://localhost:5010/api/mcp/list" - C->>M: GET /api/mcp/list - Note over C,M: Authorization: Bearer token - - M->>A: get_current_user_id(authorization) - A-->>M: user_id, tenant_id - - M->>S: get_remote_mcp_server_list(tenant_id) - S->>DB: 查询租户的MCP服务器列表 - DB-->>S: 服务器列表数据 - S-->>M: remote_mcp_server_list - - M-->>C: JSON响应 {remote_mcp_server_list: [...], status: "success"} - - Note over M,C: 错误情况下返回 400 状态码 +# 添加MCP服务器 +curl -X POST "http://localhost:5010/api/mcp/add?mcp_url=http://external-server:5012/sse&service_name=external_service" \ + -H "Authorization: Bearer {your_token}" + +# 删除MCP服务器 +curl -X DELETE "http://localhost:5010/api/mcp/?service_name=external_service&mcp_url=http://external-server:5012/sse" \ + -H "Authorization: Bearer {your_token}" + +# 健康检查 +curl "http://localhost:5010/api/mcp/healthcheck?mcp_url=http://external-server:5012/sse&service_name=external_service" \ + -H "Authorization: Bearer {your_token}" ``` -### 5. 恢复远程MCP服务器 (GET /api/mcp/recover) +## 性能优化 + +### 1. 连接池管理 +- MCP客户端连接复用 +- 自动连接超时和重试机制 +- ToolCollection统一管理多个MCP服务连接 -```mermaid -sequenceDiagram - participant C as 前端客户端 - participant M as 主服务(5010) - participant A as 认证系统 - participant S as MCP服务管理 - participant DB as 数据库 - participant MCP as MCP服务(5011) - participant R as 远程MCP服务 +### 2. 工具缓存 +- 工具信息本地缓存 +- 减少重复的MCP服务查询 - C->>M: GET /api/mcp/recover - Note over C,M: Authorization: Bearer token - - M->>A: get_current_user_id(authorization) - A-->>M: user_id, tenant_id - - M->>S: recover_remote_mcp_server(tenant_id) - - S->>DB: 查询租户的所有MCP服务器配置 - DB-->>S: 数据库中的服务器列表 (record_set) - - S->>MCP: GET /list-remote-proxies - MCP-->>S: 当前MCP服务中的代理列表 (remote_set) - - S->>S: 计算差异 (record_set - remote_set) - - loop 对每个缺失的MCP服务器 - S->>S: add_remote_proxy(mcp_name, mcp_url) - S->>MCP: POST /add-remote-proxies - MCP->>R: 连接远程MCP服务 - R-->>MCP: 连接响应 - - alt 添加成功 - MCP-->>S: 200 - 成功添加 - else 添加失败 - MCP-->>S: 错误响应 - S-->>M: 错误结果/JSONResponse - M-->>C: 错误响应 (400) - Note over S,M: 任一服务器恢复失败,整个操作失败 - end - end - - S-->>M: 成功结果 - M-->>C: JSON响应 {message: "Successfully recovered", status: "success"} -``` +### 3. 异步处理 +- 所有MCP操作采用异步模式 +- 支持并发智能体执行 + +## 安全考虑 -## 时序图说明 +### 1. 认证授权 +- 所有API接口需要Bearer token认证 +- 基于租户的数据隔离 -### 接口分类 +### 2. 连接验证 +- 添加MCP服务器前进行连通性验证 +- 支持HTTPS和SSE安全传输 -#### 1. 直接访问远程MCP服务的接口 -- **GET /api/mcp/tools/**:直接通过工具配置服务访问远程MCP获取工具信息 -- 特点:不经过本地MCP服务(5011),直接连接外部MCP服务 +### 3. 错误处理 +- 详细的错误日志记录 +- 敏感信息脱敏处理 -#### 2. 经过本地MCP服务的接口 -- **POST /api/mcp/add**:通过MCP服务验证连接并添加代理 -- **DELETE /api/mcp/**:通过MCP服务移除代理 -- **GET /api/mcp/recover**:通过MCP服务恢复代理连接 -- 特点:需要与本地MCP服务(5011)交互,涉及代理的生命周期管理 +## 故障排除 -#### 3. 仅操作数据库的接口 -- **GET /api/mcp/list**:直接查询数据库获取服务器列表 -- 特点:最简单的流程,仅涉及数据库查询 +### 常见问题 -### 通用流程特点 -1. **认证流程**:除了工具查询接口,其他接口都需要Bearer token认证,通过`get_current_user_id()`获取用户和租户信息 -2. **多租户隔离**:所有操作都基于`tenant_id`进行隔离,确保数据安全 -3. **错误处理**:统一的异常处理机制,返回标准化的JSON错误响应 -4. **代理架构**:主服务作为代理,协调各个后端服务的调用 +1. **MCP连接失败** + - 检查远程MCP服务是否正常运行 + - 验证网络连接和防火墙设置 + - 查看服务日志获取详细错误信息 -### 关键交互点 -- **认证系统**:验证用户身份和权限 -- **数据库**:存储和管理MCP服务器配置信息 -- **MCP服务(5011)**:处理MCP协议交互和代理管理 -- **工具配置服务**:处理工具信息获取 -- **远程MCP服务**:外部的MCP服务提供者 +2. **工具加载失败** + - 确认MCP服务器支持所需的工具 + - 检查工具配置是否正确 + - 验证权限设置 -### 操作顺序重要性 -- **添加操作**:先验证MCP连接,成功后才保存数据库(确保数据一致性) -- **删除操作**:先删除数据库记录,再移除MCP代理(防止数据残留) -- **恢复操作**:比较数据库与MCP服务差异,补充缺失的代理 +3. **性能问题** + - 监控MCP服务器响应时间 + - 检查网络延迟 + - 优化工具过滤逻辑 -## 安全特性 +### 调试工具 -- **认证授权**:主服务支持Bearer token认证 -- **多租户隔离**:不同租户的MCP服务器隔离管理 -- **连接验证**:添加远程服务前进行连通性验证 +```python +# 检查MCP服务器健康状态 +response = await mcp_server_health("http://remote-server:port/sse") + +# 获取MCP服务器列表 +servers = await get_remote_mcp_server_list(tenant_id) + +# 查看智能体使用的MCP服务器 +mcp_hosts = filter_mcp_servers_and_tools(agent_config, mcp_info_dict) + +# 查看本地MCP服务状态 +# 本地MCP服务运行在端口5011,提供本地工具 +``` diff --git a/doc/docs/zh/deployment/docker-build.md b/doc/docs/zh/deployment/docker-build.md index 6df8eff99..8fa33da43 100644 --- a/doc/docs/zh/deployment/docker-build.md +++ b/doc/docs/zh/deployment/docker-build.md @@ -23,6 +23,10 @@ docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t ccr.c # 📚 为多个架构构建文档 docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t nexent/nexent-docs -f make/docs/Dockerfile . --push docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t ccr.ccs.tencentyun.com/nexent-hub/nexent-docs -f make/docs/Dockerfile . --push + +# 💻 为多个架构构建 Ubuntu Terminal +docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t nexent/nexent-terminal -f make/terminal/Dockerfile . --push +docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t ccr.ccs.tencentyun.com/nexent-hub/nexent-terminal -f make/terminal/Dockerfile . --push ``` ## 💻 本地开发构建 @@ -39,6 +43,9 @@ docker build --progress=plain -t nexent/nexent-web -f make/web/Dockerfile . # 📚 构建文档镜像(仅当前架构) docker build --progress=plain -t nexent/nexent-docs -f make/docs/Dockerfile . + +# 💻 构建 OpenSSH Server 镜像(仅当前架构) +docker build --progress=plain -t nexent/nexent-ubuntu-terminal -f make/terminal/Dockerfile . ``` ## 🔧 镜像说明 @@ -63,6 +70,23 @@ docker build --progress=plain -t nexent/nexent-docs -f make/docs/Dockerfile . - 基于 `make/docs/Dockerfile` 构建 - 提供项目文档和 API 参考 +### OpenSSH Server 镜像 (nexent/nexent-ubuntu-terminal) +- 基于 Ubuntu 24.04 的 SSH 服务器容器 +- 基于 `make/terminal/Dockerfile` 构建 +- 预装 Conda、Python、Git 等开发工具 +- 支持 SSH 密钥认证,用户名为 `linuxserver.io` +- 提供完整的开发环境 + +#### 预装工具和特性 +- **Python 环境**: Python 3 + pip + virtualenv +- **Conda 管理**: Miniconda3 环境管理 +- **开发工具**: Git、Vim、Nano、Curl、Wget +- **构建工具**: build-essential、Make +- **SSH 服务**: 端口 2222,禁用 root 登录和密码认证 +- **用户权限**: `linuxserver.io` 用户具有 sudo 权限(无需密码) +- **时区设置**: Asia/Shanghai +- **安全配置**: SSH 密钥认证,会话超时 60 分钟 + ## 🏷️ 标签策略 每个镜像都会推送到两个仓库: @@ -74,6 +98,7 @@ docker build --progress=plain -t nexent/nexent-docs -f make/docs/Dockerfile . - `nexent/nexent-data-process` - 数据处理服务 - `nexent/nexent-web` - Next.js 前端应用 - `nexent/nexent-docs` - Vitepress 文档站点 +- `nexent/nexent-ubuntu-terminal` - OpenSSH 开发服务器容器 ## 📚 文档镜像独立部署 diff --git a/doc/docs/zh/getting-started/installation.md b/doc/docs/zh/getting-started/installation.md index 20333ae2c..f7641abeb 100644 --- a/doc/docs/zh/getting-started/installation.md +++ b/doc/docs/zh/getting-started/installation.md @@ -107,7 +107,7 @@ EXA_API_KEY=your_exa_key | MinIO API | 9000 | 9010 | 对象存储 API | | MinIO 控制台 | 9001 | 9011 | 存储管理 UI | | Redis | 6379 | 6379 | 缓存服务 | -| SSH 服务器 | 2222 | 2222 | 终端工具访问 | +| SSH 服务器 | 22 | 2222 | 终端工具访问 | 有关完整的端口映射详细信息,请参阅我们的 [开发容器指南](../deployment/devcontainer.md#port-mapping)。 diff --git a/doc/docs/zh/getting-started/software-architecture.md b/doc/docs/zh/getting-started/software-architecture.md index 462290c21..620d476ef 100644 --- a/doc/docs/zh/getting-started/software-architecture.md +++ b/doc/docs/zh/getting-started/software-architecture.md @@ -1,7 +1,166 @@ # 软件架构 -我们将很快更新全面的软件架构图和文档。 +Nexent 采用现代化的分布式微服务架构,旨在提供高性能、可扩展的 AI 智能体平台。整个系统基于容器化部署,支持云原生和企业级应用场景。 + +![软件架构图](../../assets/architecture_zh.png) + +## 🏗️ 整体架构设计 + +Nexent 的软件架构遵循分层设计原则,从上到下分为以下几个核心层次: + +### 🌐 前端层(Frontend Layer) +- **技术栈**:Next.js + React + TypeScript +- **功能**:用户界面、智能体交互、多模态输入处理 +- **特性**:响应式设计、实时通信、国际化支持 + +### 🔌 API 网关层(API Gateway Layer) +- **核心服务**:FastAPI 高性能 Web 框架 +- **职责**:请求路由、身份验证、API 版本管理、负载均衡 +- **端口**:5010(主服务)、5012(数据处理服务) + +### 🧠 业务逻辑层(Business Logic Layer) +- **智能体管理**:智能体生成、执行、监控 +- **会话管理**:多轮对话、上下文维护、历史记录 +- **知识库管理**:文档处理、向量化、检索 +- **模型管理**:多模型支持、健康检查、负载均衡 + +### 📊 数据层(Data Layer) +分布式数据存储架构,包含多种专用数据库: + +#### 🗄️ 结构化数据存储 +- **PostgreSQL**:主数据库,存储用户信息、智能体配置、会话记录 +- **端口**:5434 +- **特性**:ACID 事务、关系型数据完整性 + +#### 🔍 搜索引擎 +- **Elasticsearch**:向量数据库和全文搜索引擎 +- **端口**:9210 +- **功能**:向量相似度搜索、混合搜索、大规模优化 + +#### 💾 缓存层 +- **Redis**:高性能内存数据库 +- **端口**:6379 +- **用途**:会话缓存、临时数据、分布式锁 + +#### 📁 对象存储 +- **MinIO**:分布式对象存储服务 +- **端口**:9010 +- **功能**:文件存储、多媒体资源管理、大文件处理 + +## 🔧 核心服务架构 + +### 🤖 智能体服务(Agent Services) +``` +智能体框架基于 SmolAgents,提供: +├── 智能体生成与配置 +├── 工具调用与集成 +├── 推理与决策执行 +└── 生命周期管理 +``` + +### 📈 数据处理服务(Data Processing Services) +``` +分布式数据处理架构: +├── 实时文档处理(20+ 格式支持) +├── 批量数据处理管道 +├── OCR 与表格结构提取 +└── 向量化与索引构建 +``` + +### 🌐 MCP 生态系统(MCP Ecosystem) +``` +模型上下文协议工具集成: +├── 标准化工具接口 +├── 插件化架构 +├── 第三方服务集成 +└── 自定义工具开发 +``` + +## 🚀 分布式架构特性 + +### ⚡ 异步处理架构 +- **基础框架**:基于 asyncio 的高性能异步处理 +- **并发控制**:线程安全的并发处理机制 +- **任务队列**:Celery + Ray 分布式任务执行 +- **流式处理**:实时数据流和响应流处理 + +### 🔄 微服务设计 +``` +服务拆分策略: +├── nexent(主服务)- 智能体核心逻辑 +├── nexent-data-process(数据处理)- 文档处理管道 +├── nexent-mcp-service(MCP服务)- 工具协议服务 +└── 可选服务(SSH、监控等) +``` + +### 🌍 容器化部署 +``` +Docker Compose 服务编排: +├── 应用服务容器化 +├── 数据库服务隔离 +├── 网络层安全配置 +└── 卷挂载数据持久化 +``` + +## 🔐 安全与扩展性 + +### 🛡️ 安全架构 +- **身份验证**:多租户支持、用户权限管理 +- **数据安全**:端到端加密、安全传输协议 +- **网络安全**:服务间安全通信、防火墙配置 + +### 📈 可扩展性设计 +- **水平扩展**:微服务独立扩展、负载均衡 +- **垂直扩展**:资源池管理、智能调度 +- **存储扩展**:分布式存储、数据分片 + +### 🔧 模块化架构 +- **松耦合设计**:服务间低依赖、接口标准化 +- **插件化架构**:工具和模型的热插拔 +- **配置管理**:环境隔离、动态配置更新 + +## 🔄 数据流架构 + +### 📥 用户请求流 +``` +用户输入 → 前端验证 → API网关 → 路由分发 → 业务服务 → 数据访问 → 数据库 +``` + +### 🤖 智能体执行流 +``` +用户消息 → 智能体创建 → 工具调用 → 模型推理 → 流式响应 → 结果存储 +``` + +### 📚 知识库处理流 +``` +文件上传 → 临时存储 → 数据处理 → 向量化 → 知识库存储 → 索引更新 +``` + +### ⚡ 实时处理流 +``` +实时输入 → 即时处理 → 智能体响应 → 流式输出 +``` + +## 🎯 架构优势 + +### 🏢 企业级特性 +- **高可用性**:多层冗余、故障转移 +- **高性能**:异步处理、智能缓存 +- **高并发**:分布式架构、负载均衡 +- **监控友好**:完善的日志和状态监控 + +### 🔧 开发友好 +- **模块化开发**:清晰的层次结构 +- **标准化接口**:统一的 API 设计 +- **灵活配置**:环境适配、功能开关 +- **易于测试**:单元测试、集成测试支持 + +### 🌱 生态兼容 +- **MCP 标准**:遵循模型上下文协议 +- **开源生态**:集成丰富的开源工具 +- **云原生**:支持 Kubernetes、Docker 部署 +- **多模型支持**:兼容主流 AI 模型提供商 --- -*本文档正在积极开发中。我们将很快更新全面的架构详细信息。* \ No newline at end of file +这种架构设计确保了 Nexent 能够在保持高性能的同时,为用户提供稳定、可扩展的 AI 智能体服务平台。无论是个人用户还是企业级部署,都能够获得优秀的使用体验和技术保障。 \ No newline at end of file diff --git a/doc/docs/zh/opensource-memorial-wall.md b/doc/docs/zh/opensource-memorial-wall.md index fb8ad44b3..0b114ee9a 100644 --- a/doc/docs/zh/opensource-memorial-wall.md +++ b/doc/docs/zh/opensource-memorial-wall.md @@ -71,3 +71,7 @@ Nexent的自然语言生成Agent以及多智能体协同是我一直在研究的 ::: info Puppet - 2025-08-08 🌟来尝试使用论文阅读工具,项目很不错! ::: + +::: info plus - 2025-08-18 +在公众号看到可以自己做一个论文助手的文章,后面又跟着学习了MCP服务的使用等智能体开发知识,是智能体学习路上的一次有意义的实践, +::: diff --git a/doc/docs/zh/user-guide/assets/memory/add-mem.png b/doc/docs/zh/user-guide/assets/memory/add-mem.png new file mode 100644 index 000000000..de6deb3bd Binary files /dev/null and b/doc/docs/zh/user-guide/assets/memory/add-mem.png differ diff --git a/doc/docs/zh/user-guide/assets/memory/delete-mem.png b/doc/docs/zh/user-guide/assets/memory/delete-mem.png new file mode 100644 index 000000000..626687a2d Binary files /dev/null and b/doc/docs/zh/user-guide/assets/memory/delete-mem.png differ diff --git a/doc/docs/zh/user-guide/assets/memory/mem-config.png b/doc/docs/zh/user-guide/assets/memory/mem-config.png new file mode 100644 index 000000000..ca8c88cbf Binary files /dev/null and b/doc/docs/zh/user-guide/assets/memory/mem-config.png differ diff --git a/doc/docs/zh/user-guide/memory.md b/doc/docs/zh/user-guide/memory.md new file mode 100644 index 000000000..18fae9e41 --- /dev/null +++ b/doc/docs/zh/user-guide/memory.md @@ -0,0 +1,85 @@ +# 🧠Nexent 智能记忆系统技术规格说明 + +## 1. 系统架构概述 + +Nexent 智能记忆系统基于先进的记忆存储架构,为智能体提供持久化的上下文感知能力。该系统通过多层级记忆管理机制,实现了跨对话会话的知识累积与检索,显著提升了人机交互的连贯性和个性化程度。 + +### 核心技术特性 +- **分层记忆架构**:基于 mem0 框架构建的四级记忆存储体系 +- **自适应记忆管理**:支持自动化和手动化的记忆操作模式 +- **跨会话持久化**:确保知识和上下文在多次对话中的连续性 +- **细粒度权限控制**:提供灵活的记忆共享策略配置 + +--- + +## 2. 配置与初始化 + +### 2.1 系统激活 +1. 访问记忆管理界面:点击对话界面右上角的**记忆管理图标** +2. 进入**系统配置**模块进行初始化设置 + +### 2.2 核心配置参数 + +| 配置项 | 选项 | 默认值 | 说明 | +|--------|-----------------|--------|--------------------------| +| 记忆服务状态 | 启用/禁用 | 启用 | 控制整个记忆系统的运行状态 | +| Agent 记忆共享策略 | 总是共享/每次询问我/禁止共享 | 总是共享 | 定义Agent间共享记忆生成是否需要用户授权同意 | + +
+ 选择智能体 +
+ +--- + +## 3. 分层记忆架构 + +Nexent 采用基于 **mem0** 的四层记忆存储架构,通过不同的作用域和生命周期管理,实现精确的记忆分类与检索: + +### 3.1 架构层级详解 + +| 记忆级别 | 作用域 | 存储内容 | 生命周期 | 配置角色 | 典型应用 | +|---------|--------|----------|----------|----------|----------| +| **租户级记忆**
(Tenant Level Memory) | 组织全局 | 企业级标准操作流程、合规政策、组织架构、事实信息 | 长期存储 | 租户管理员 | 企业知识管理、标准化流程执行、合规性检查 | +| **智能体级记忆**
(Agent Level Memory) | 特定智能体 | 专业领域知识、技能模板、历史对话摘要、学习积累 | 与智能体生命周期一致 | 租户管理员 | 专业技能积累、领域知识沉淀、经验学习 | +| **用户级记忆**
(User Level Memory) | 特定用户账户 | 个人偏好设置、使用习惯、常用指令模板、个人信息 | 长期存储 | 全体用户 | 个性化服务、用户体验优化、偏好管理 | +| **用户-智能体级记忆**
(User-Agent Level Memory) | 特定用户账户下的特定智能体 | 协作历史、个性化事实信息、特定任务上下文、关系模型 | 与智能体生命周期一致 | 全体用户 | 深度协作场景、个性化调优、任务连续性维护 | + +### 3.2 记忆优先级与检索策略 + +记忆检索遵循以下优先级顺序(由高到低): +1. **租户级** → 基础事实 +2. **用户-智能体级** → 最具体的上下文信息 +2. **用户级** → 个人偏好和习惯 +3. **智能体级** → 专业知识和技能 + +--- + +## 4. 操作模式与功能接口 + +### 4.1 自动化记忆管理 +- **智能提取**:自动识别对话中的关键事实信息并生成记忆条目 +- **自动上下文嵌入**:智能体将自动检索相关性最高的记忆条目,隐式嵌入对话上下文中 +- **增量更新**:支持记忆内容的渐进式更新、补充和自动清理 + +### 4.2 手动记忆操作 + +#### 添加记忆 +- 点击绿色的“对话加号”按钮,输入文本,再点击对钩可添加一条记忆条目(最多500字符) + +
+ 选择智能体 +
+ +#### 删除记忆 +- 点击红色叉号按钮,在跳出的二次确认弹框中点击确认按钮,可删除某个Agent分组下所有的记忆条目 +- 点击红色橡皮按钮,可删除特定的一条记忆条目 + +
+ Select Agent +
+ +### 4.3 记忆管理最佳实践 + +1. **原子性原则**:每条记忆应包含 **简洁**、**单一**、**明确** 的事实信息 +2. **时效性管理**:定期清理过时或不再相关的记忆条目,保持记忆库的时效性和准确性 +3. **隐私保护**:敏感信息应尽量避免在租户层级或智能体层级进行共享 diff --git a/docker/.env.beta b/docker/.env.beta index cc90fd669..6d3e57473 100644 --- a/docker/.env.beta +++ b/docker/.env.beta @@ -6,4 +6,4 @@ ELASTICSEARCH_IMAGE=docker.elastic.co/elasticsearch/elasticsearch:8.17.4 POSTGRESQL_IMAGE=postgres:15-alpine REDIS_IMAGE=redis:alpine MINIO_IMAGE=quay.io/minio/minio:RELEASE.2023-12-20T01-00-02Z -OPENSSH_SERVER_IMAGE=lscr.io/linuxserver/openssh-server:latest \ No newline at end of file +OPENSSH_SERVER_IMAGE=nexent-ubuntu-terminal:latest \ No newline at end of file diff --git a/docker/.env.general b/docker/.env.general index 0bb4923d1..d7c579d66 100644 --- a/docker/.env.general +++ b/docker/.env.general @@ -6,4 +6,4 @@ ELASTICSEARCH_IMAGE=docker.elastic.co/elasticsearch/elasticsearch:8.17.4 POSTGRESQL_IMAGE=postgres:15-alpine REDIS_IMAGE=redis:alpine MINIO_IMAGE=quay.io/minio/minio:RELEASE.2023-12-20T01-00-02Z -OPENSSH_SERVER_IMAGE=lscr.io/linuxserver/openssh-server:latest +OPENSSH_SERVER_IMAGE=nexent-ubuntu-terminal:latest diff --git a/docker/.env.mainland b/docker/.env.mainland index a85a53fff..a44645776 100644 --- a/docker/.env.mainland +++ b/docker/.env.mainland @@ -6,4 +6,4 @@ ELASTICSEARCH_IMAGE=elastic.m.daocloud.io/elasticsearch/elasticsearch:8.17.4 POSTGRESQL_IMAGE=docker.m.daocloud.io/postgres:15-alpine REDIS_IMAGE=docker.m.daocloud.io/redis:alpine MINIO_IMAGE=quay.m.daocloud.io/minio/minio:RELEASE.2023-12-20T01-00-02Z -OPENSSH_SERVER_IMAGE=docker.m.daocloud.io/linuxserver/openssh-server:latest +OPENSSH_SERVER_IMAGE=nexent-ubuntu-terminal:latest diff --git a/docker/deploy.sh b/docker/deploy.sh index a7af674af..04174adb9 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -156,9 +156,6 @@ generate_ssh_keys() { cp "openssh-server/ssh-keys/openssh_server_key.pub" "openssh-server/config/authorized_keys" chmod 644 "openssh-server/config/authorized_keys" - # Setup package installation script - setup_package_install_script - # Set SSH key path in environment SSH_PRIVATE_KEY_PATH="$(pwd)/openssh-server/ssh-keys/openssh_server_key" export SSH_PRIVATE_KEY_PATH @@ -185,7 +182,7 @@ generate_ssh_keys() { TEMP_OUTPUT="/tmp/ssh_keygen_output_$$.txt" # Generate ed25519 key pair using the openssh-server container - if docker run --rm -i --entrypoint //keygen.sh "$OPENSSH_SERVER_IMAGE" <<< "1" > "$TEMP_OUTPUT" 2>&1; then + if docker run --rm -i "$OPENSSH_SERVER_IMAGE" bash -c "ssh-keygen -t ed25519 -f /tmp/id_ed25519 -N '' && cat /tmp/id_ed25519 && echo '---' && cat /tmp/id_ed25519.pub" > "$TEMP_OUTPUT" 2>&1; then echo " 🔍 SSH key generation completed, extracting keys..." # Extract private key (everything between -----BEGIN and -----END) @@ -226,9 +223,6 @@ generate_ssh_keys() { cp "openssh-server/ssh-keys/openssh_server_key.pub" "openssh-server/config/authorized_keys" chmod 644 "openssh-server/config/authorized_keys" - # Setup package installation script - setup_package_install_script - # Set SSH key path in environment SSH_PRIVATE_KEY_PATH="$(pwd)/openssh-server/ssh-keys/openssh_server_key" export SSH_PRIVATE_KEY_PATH @@ -643,20 +637,7 @@ select_deployment_version() { echo "" } -pull_openssh_images() { - # Function to pull openssh images - echo "🐳 Pulling openssh-server image for Terminal tool..." - if ! docker pull "$OPENSSH_SERVER_IMAGE"; then - echo " ❌ ERROR Failed to pull openssh-server image: $OPENSSH_SERVER_IMAGE" - ERROR_OCCURRED=1 - return 1 - fi - echo " ✅ Successfully pulled openssh-server image" - echo "" - echo "--------------------------------" - echo "" -} setup_package_install_script() { # Function to setup package installation script @@ -714,6 +695,31 @@ select_terminal_tool() { export COMPOSE_PROFILES="${COMPOSE_PROFILES:+$COMPOSE_PROFILES,}terminal" echo "✅ Terminal tool enabled 🔧" echo " 🔧 Deploying an openssh-server container for secure command execution" + + # Ask user to specify directory mapping + default_terminal_dir="/opt/terminal" + echo " 📁 Terminal directory configuration:" + echo " • Container path: /opt/terminal (fixed)" + echo " • Host path: You can specify any directory on your host machine" + echo " • Default host path: /opt/terminal (recommended)" + echo "" + read -p " 📁 Enter host directory to mount (default: /opt/terminal): " terminal_mount_dir + terminal_mount_dir=$(sanitize_input "$terminal_mount_dir") + TERMINAL_MOUNT_DIR="${terminal_mount_dir:-$default_terminal_dir}" + + # Save to environment variables + export TERMINAL_MOUNT_DIR + + # Add to .env file + if grep -q "^TERMINAL_MOUNT_DIR=" .env; then + sed -i.bak "s~^TERMINAL_MOUNT_DIR=.*~TERMINAL_MOUNT_DIR=$TERMINAL_MOUNT_DIR~" .env + else + echo "TERMINAL_MOUNT_DIR=$TERMINAL_MOUNT_DIR" >> .env + fi + + echo " 📁 Terminal mount configuration:" + echo " • Host: $TERMINAL_MOUNT_DIR" + echo " • Container: /opt/terminal" else export ENABLE_TERMINAL_TOOL="false" echo "🚫 Terminal tool disabled" @@ -797,7 +803,6 @@ main_deploy() { generate_minio_ak_sk || { echo "❌ MinIO key generation failed"; exit 1; } if [ "$ENABLE_TERMINAL_TOOL" = "true" ]; then - pull_openssh_images || { echo "❌ Openssh image pull failed"; exit 1; } generate_ssh_keys || { echo "❌ SSH key generation failed"; exit 1; } fi diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index e631e6691..39023d925 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -202,18 +202,11 @@ services: container_name: nexent-openssh-server hostname: nexent-openssh-server environment: - PUID: 1000 - PGID: 1000 - TZ: "Asia/Shanghai" - PUBLIC_KEY_FILE: /config/authorized_keys - SUDO_ACCESS: "true" - PASSWORD_ACCESS: "false" - LOG_STDOUT: "true" - DOCKER_MODS: linuxserver/mods:universal-package-install - INSTALL_PACKAGES: git|make|curl|vim|wget + - TZ=Asia/Shanghai + - DEV_USER=linuxserver.io volumes: - - ${ROOT_DIR}/openssh-server/config:/config - - ${ROOT_DIR}/openssh-server/config/custom-cont-init.d:/custom-cont-init.d:ro + - ${TERMINAL_MOUNT_DIR:-./workspace}:/opt/terminal + - ${ROOT_DIR}/openssh-server/config:/tmp/ssh_keys:ro # 只读挂载SSH公钥 networks: - nexent restart: always diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e171ee823..2655fc08a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -217,20 +217,13 @@ services: container_name: nexent-openssh-server hostname: nexent-openssh-server environment: - PUID: 1000 - PGID: 1000 - TZ: "Asia/Shanghai" - PUBLIC_KEY_FILE: /config/authorized_keys - SUDO_ACCESS: "true" - PASSWORD_ACCESS: "false" - LOG_STDOUT: "true" - DOCKER_MODS: linuxserver/mods:universal-package-install - INSTALL_PACKAGES: git|make|curl|vim|wget + - TZ=Asia/Shanghai + - DEV_USER=linuxserver.io ports: - - "2222:2222" # SSH port + - "2222:22" # SSH port volumes: - - ${ROOT_DIR}/openssh-server/config:/config - - ${ROOT_DIR}/openssh-server/config/custom-cont-init.d:/custom-cont-init.d:ro + - ${TERMINAL_MOUNT_DIR:-./workspace}:/opt/terminal + - ${ROOT_DIR}/openssh-server/ssh-keys:/tmp/ssh_keys:ro networks: - nexent restart: always diff --git a/frontend/app/[locale]/chat/layout/chatLeftSidebar.tsx b/frontend/app/[locale]/chat/layout/chatLeftSidebar.tsx index 97437849d..756b464ee 100644 --- a/frontend/app/[locale]/chat/layout/chatLeftSidebar.tsx +++ b/frontend/app/[locale]/chat/layout/chatLeftSidebar.tsx @@ -1,5 +1,4 @@ import { useState, useRef, useEffect } from "react" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdownMenu" import { @@ -28,11 +27,9 @@ import { useTranslation } from "react-i18next" // conversation status indicator component const ConversationStatusIndicator = ({ - conversationId, isStreaming, isCompleted }: { - conversationId: number isStreaming: boolean isCompleted: boolean }) => { @@ -263,7 +260,6 @@ export function ChatSidebar({ onClick={() => onDialogClick(dialog)} > diff --git a/frontend/app/[locale]/chat/page.tsx b/frontend/app/[locale]/chat/page.tsx index d18b1e079..522809083 100644 --- a/frontend/app/[locale]/chat/page.tsx +++ b/frontend/app/[locale]/chat/page.tsx @@ -4,11 +4,9 @@ import { useEffect } from "react" import { ChatInterface } from "@/app/chat/internal/chatInterface" import { useConfig } from "@/hooks/useConfig" import { configService } from "@/services/configService" -import { useAuth } from "@/hooks/useAuth" export default function ChatPage() { const { appConfig } = useConfig() - const { user, isLoading } = useAuth() useEffect(() => { // Load config from backend when entering chat page diff --git a/frontend/app/[locale]/chat/streaming/chatStreamHandler.tsx b/frontend/app/[locale]/chat/streaming/chatStreamHandler.tsx index ac04a6fc8..72a83f574 100644 --- a/frontend/app/[locale]/chat/streaming/chatStreamHandler.tsx +++ b/frontend/app/[locale]/chat/streaming/chatStreamHandler.tsx @@ -60,9 +60,7 @@ export const handleStreamResponse = async ( output: { content: "", expanded: true } }; - let currentContentId = ""; let lastContentType: "model_output" | "parsing" | "execution" | "agent_new_run" | "generating_code" | "search_content" | "card" | null = null; - let currentContentText = ""; let lastModelOutputIndex = -1; // Track the index of the last model output in currentStep.contents let searchResultsContent: any[] = []; let allSearchResults: any[] = []; @@ -113,8 +111,6 @@ export const handleStreamResponse = async ( }; // Reset status tracking variables - currentContentId = ""; - currentContentText = ""; lastContentType = null; lastModelOutputIndex = -1; diff --git a/frontend/app/[locale]/chat/streaming/chatStreamMain.tsx b/frontend/app/[locale]/chat/streaming/chatStreamMain.tsx index 52fc9a053..270f2234f 100644 --- a/frontend/app/[locale]/chat/streaming/chatStreamMain.tsx +++ b/frontend/app/[locale]/chat/streaming/chatStreamMain.tsx @@ -111,7 +111,6 @@ export function ChatStreamMain({ }); let currentUserMsgId: string | null = null; - let lastUserMsgId: string | null = null; // Process all messages, distinguish user messages, final answers, and task messages messages.forEach(message => { @@ -120,7 +119,6 @@ export function ChatStreamMain({ finalMsgs.push(message); // Record the user message ID, used to associate subsequent tasks if (message.id) { - lastUserMsgId = currentUserMsgId; // Save the last user message ID currentUserMsgId = message.id; // Save the latest user message ID to the ref diff --git a/frontend/app/[locale]/chat/streaming/taskWindow.tsx b/frontend/app/[locale]/chat/streaming/taskWindow.tsx index 0bca62891..67e454f66 100644 --- a/frontend/app/[locale]/chat/streaming/taskWindow.tsx +++ b/frontend/app/[locale]/chat/streaming/taskWindow.tsx @@ -2,7 +2,7 @@ import { useRef, useEffect, useState } from "react" import { ScrollArea } from "@/components/ui/scrollArea" import { ChatMessageType, TaskMessageType } from "@/types/chat" import { MarkdownRenderer } from '@/components/ui/markdownRenderer' -import { Globe, Search, Zap, Bot, Code, FileText, HelpCircle, ChevronRight, Wrench } from "lucide-react" +import { Globe, Search, Zap, Bot, Code, FileText, ChevronRight, Wrench } from "lucide-react" import { Button } from "@/components/ui/button" import { useChatTaskMessage } from "@/hooks/useChatTaskMessage" import { useTranslation } from "react-i18next" @@ -807,7 +807,7 @@ export function TaskWindow({ }; // Check if a message should display a blinking dot - const shouldBlinkDot = (message: any, index: number, messages: any[]) => { + const shouldBlinkDot = (index: number, messages: any[]) => { // As long as it is the last message and is streaming, it should blink, regardless of the message type return isStreaming && isLastMessage(index, messages); }; @@ -836,7 +836,7 @@ export function TaskWindow({ {groupedMessages.map((group, groupIndex) => { const message = group.message; - const isBlinking = shouldBlinkDot(message, groupIndex, groupedMessages.map(g => g.message)); + const isBlinking = shouldBlinkDot(groupIndex, groupedMessages.map(g => g.message)); return (
diff --git a/frontend/app/[locale]/page.tsx b/frontend/app/[locale]/page.tsx index ba0f449c2..e378713a6 100644 --- a/frontend/app/[locale]/page.tsx +++ b/frontend/app/[locale]/page.tsx @@ -11,7 +11,6 @@ import { LoginModal } from "@/components/auth/loginModal" import { RegisterModal } from "@/components/auth/registerModal" import { useAuth } from "@/hooks/useAuth" import { Modal, ConfigProvider, Dropdown } from "antd" -import { useRouter, usePathname } from 'next/navigation'; import { motion } from 'framer-motion'; import { languageOptions } from '@/lib/constants'; import { useLanguageSwitch } from '@/lib/languageUtils'; @@ -20,8 +19,7 @@ import { DownOutlined } from '@ant-design/icons' export default function Home() { const [mounted, setMounted] = useState(false) - const {t} = useTranslation('common'); - const {currentLanguage, handleLanguageChange, getOppositeLanguage} = useLanguageSwitch(); + const {currentLanguage, handleLanguageChange} = useLanguageSwitch(); // Prevent hydration errors useEffect(() => { @@ -39,10 +37,7 @@ export default function Home() { ) function FrontpageContent() { - const {t, i18n} = useTranslation('common'); - const [lang, setLang] = useState(i18n.language || 'zh'); - const router = useRouter(); - const pathname = usePathname(); + const {t} = useTranslation('common'); const {user, isLoading: userLoading, openLoginModal, openRegisterModal, isSpeedMode} = useAuth() const [loginPromptOpen, setLoginPromptOpen] = useState(false) const [adminRequiredPromptOpen, setAdminRequiredPromptOpen] = useState(false) @@ -85,7 +80,6 @@ export default function Home() { setAdminRequiredPromptOpen(false) } - // 重构:风格被嵌入在组件内 return (
diff --git a/frontend/app/[locale]/setup/agentSetup/AgentConfig.tsx b/frontend/app/[locale]/setup/agentSetup/AgentConfig.tsx index 4cf562f6e..fe14eb65e 100644 --- a/frontend/app/[locale]/setup/agentSetup/AgentConfig.tsx +++ b/frontend/app/[locale]/setup/agentSetup/AgentConfig.tsx @@ -6,7 +6,7 @@ import BusinessLogicConfig from './AgentManagementConfig' import DebugConfig from './DebugConfig' import GuideSteps from './components/GuideSteps' import { Row, Col, Drawer, App } from 'antd' -import { fetchTools, fetchAgentList, fetchAgentDetail, exportAgent, deleteAgent, updateAgent } from '@/services/agentConfigService' +import { fetchTools, fetchAgentList, exportAgent, deleteAgent } from '@/services/agentConfigService' import { generatePromptStream } from '@/services/promptService' import { OpenAIModel } from '@/app/setup/agentSetup/ConstInterface' import { updateToolList } from '@/services/mcpService' @@ -14,7 +14,6 @@ import { SETUP_PAGE_CONTAINER, THREE_COLUMN_LAYOUT, STANDARD_CARD, - CARD_HEADER } from '@/lib/layoutConstants' import '../../i18n' @@ -35,8 +34,6 @@ export default function AgentConfig() { const [systemPrompt, setSystemPrompt] = useState("") const [selectedAgents, setSelectedAgents] = useState([]) const [selectedTools, setSelectedTools] = useState([]) - const [testQuestion, setTestQuestion] = useState("") - const [testAnswer, setTestAnswer] = useState("") const [isDebugDrawerOpen, setIsDebugDrawerOpen] = useState(false) const [isCreatingNewAgent, setIsCreatingNewAgent] = useState(false) const [mainAgentModel, setMainAgentModel] = useState(OpenAIModel.MainModel) @@ -51,7 +48,6 @@ export default function AgentConfig() { const [newAgentName, setNewAgentName] = useState("") const [newAgentDescription, setNewAgentDescription] = useState("") const [newAgentProvideSummary, setNewAgentProvideSummary] = useState(false) - const [isNewAgentInfoValid, setIsNewAgentInfoValid] = useState(false) const [isEditingAgent, setIsEditingAgent] = useState(false) const [editingAgent, setEditingAgent] = useState(null) @@ -78,20 +74,10 @@ export default function AgentConfig() { // Add state for business logic and action buttons const [isGeneratingAgent, setIsGeneratingAgent] = useState(false) - const [isSavingAgent, setIsSavingAgent] = useState(false) // Only auto scan once flag const hasAutoScanned = useRef(false) - // Handle business logic change - const handleBusinessLogicChange = (value: string) => { - setBusinessLogic(value) - // Cache the content when creating new agent - if (isCreatingNewAgent) { - setNewAgentCache(prev => ({ ...prev, businessLogic: value })) - } - } - // Handle generate agent const handleGenerateAgent = async () => { if (!businessLogic || businessLogic.trim() === '') { @@ -107,6 +93,9 @@ export default function AgentConfig() { setIsGeneratingAgent(true) try { + const currentAgentName = agentName + const currentAgentDisplayName = agentDisplayName + // Call backend API to generate agent prompt await generatePromptStream( { @@ -126,13 +115,19 @@ export default function AgentConfig() { setFewShotsContent(data.content) break case 'agent_var_name': - setAgentName(data.content) + // Only update if current agent name is empty + if (!currentAgentName || currentAgentName.trim() === '') { + setAgentName(data.content) + } break case 'agent_description': setAgentDescription(data.content) break case 'agent_display_name': - setAgentDisplayName(data.content) + // Only update if current agent display name is empty + if (!currentAgentDisplayName || currentAgentDisplayName.trim() === '') { + setAgentDisplayName(data.content) + } break } }, @@ -152,73 +147,6 @@ export default function AgentConfig() { } } - // Handle save agent - const handleSaveAgent = async () => { - if (!canSaveAgent) { - message.warning(getButtonTitle()) - return - } - - const currentAgentId = getCurrentAgentId() - if (!currentAgentId) { - message.error(t('businessLogic.config.error.noAgentId')) - return - } - - setIsSavingAgent(true) - try { - // Call the actual save API - const result = await updateAgent( - Number(currentAgentId), - agentName, - agentDescription, - mainAgentModel, - mainAgentMaxStep, - false, // provide_run_summary - true, // enabled - enable when saving to agent pool - businessLogic, - dutyContent, - constraintContent, - fewShotsContent, - agentDisplayName - ) - - if (result.success) { - const actionText = isCreatingNewAgent ? t('agent.action.create') : t('agent.action.modify') - message.success(t('businessLogic.config.message.agentCreated', { name: agentName, action: actionText })) - - // Reset state - setIsCreatingNewAgent(false) - setIsEditingAgent(false) - setEditingAgent(null) - setBusinessLogic('') - setDutyContent('') - setConstraintContent('') - setFewShotsContent('') - setAgentName('') - setAgentDescription('') - setAgentDisplayName('') - setSelectedTools([]) - - // Clear new agent cache - clearNewAgentCache() - - // Notify parent component of state change - handleEditingStateChange(false, null) - - // Refresh agent list - fetchAgents() - } else { - message.error(result.message || t('businessLogic.config.error.saveFailed')) - } - } catch (error) { - console.error('Save agent error:', error) - message.error(t('businessLogic.config.error.saveFailed')) - } finally { - setIsSavingAgent(false) - } - } - // Handle export agent const handleExportAgent = async () => { if (!editingAgent) { @@ -281,8 +209,8 @@ export default function AgentConfig() { setFewShotsContent("") setAgentName("") setAgentDescription("") - // Refresh agent list - fetchAgents() + // Notify AgentManagementConfig to refresh agent list + window.dispatchEvent(new CustomEvent('refreshAgentList')); } else { message.error(result.message || t('businessLogic.config.message.agentDeleteFailed')) } @@ -292,23 +220,6 @@ export default function AgentConfig() { } } - // Get button title - const getButtonTitle = () => { - if (!businessLogic || businessLogic.trim() === '') { - return t('businessLogic.config.message.businessDescriptionRequired') - } - if (!(dutyContent?.trim()) && !(constraintContent?.trim()) && !(fewShotsContent?.trim())) { - return t('businessLogic.config.message.generatePromptFirst') - } - if (!agentName || agentName.trim() === '') { - return t('businessLogic.config.message.completeAgentInfo') - } - return "" - } - - // Check if can save agent - const canSaveAgent = !!(businessLogic?.trim() && agentName?.trim() && (dutyContent?.trim() || constraintContent?.trim() || fewShotsContent?.trim())) - // Load tools when page is loaded useEffect(() => { const loadTools = async () => { @@ -425,31 +336,17 @@ export default function AgentConfig() { if (isCreatingNewAgent) { // When starting to create new agent, try to restore cached content restoreNewAgentContent(); - } else { - // When not creating new agent, reset all states to initial values - setBusinessLogic(''); - setDutyContent(''); - setConstraintContent(''); - setFewShotsContent(''); - // Only clear agent name/description if not editing existing agent - if (!isEditingAgent) { - setAgentName(''); - setAgentDescription(''); - } - } + } // Always reset these states regardless of creation mode setSystemPrompt(''); setSelectedAgents([]); setSelectedTools([]); - setTestQuestion(''); - setTestAnswer(''); setCurrentGuideStep(undefined); // Reset agent info states setNewAgentName(''); setNewAgentDescription(''); setNewAgentProvideSummary(true); - setIsNewAgentInfoValid(false); // Reset the main agent configuration related status if (!isCreatingNewAgent) { @@ -480,7 +377,6 @@ export default function AgentConfig() { }) // Clear new creation related content setIsCreatingNewAgent(false) - setBusinessLogic('') setDutyContent('') setConstraintContent('') setFewShotsContent('') @@ -500,23 +396,6 @@ export default function AgentConfig() { return mainAgentId ? parseInt(mainAgentId) : undefined } - // Handle caching and restoring content for new agent creation - const cacheNewAgentContent = () => { - if (isCreatingNewAgent) { - setNewAgentCache({ - businessLogic, - dutyContent, - constraintContent, - fewShotsContent, - agentName, - agentDescription, - agentDisplayName - }) - } - } - - - const restoreNewAgentContent = () => { if (newAgentCache.businessLogic || newAgentCache.dutyContent || newAgentCache.constraintContent || newAgentCache.fewShotsContent || newAgentCache.agentName || newAgentCache.agentDescription) { @@ -554,18 +433,6 @@ export default function AgentConfig() { setAgentDescription('') } - // Handle model change with proper type conversion - const handleModelChange = (value: string) => { - setMainAgentModel(value as OpenAIModel) - } - - // Handle max step change with proper type conversion - const handleMaxStepChange = (value: number | null) => { - if (value !== null) { - setMainAgentMaxStep(value) - } - } - // Refresh tool list const handleToolsRefresh = async () => { try { @@ -637,7 +504,6 @@ export default function AgentConfig() { setNewAgentCache(prev => ({ ...prev, businessLogic: value })); } }} - selectedAgents={selectedAgents} setSelectedAgents={setSelectedAgents} selectedTools={selectedTools} setSelectedTools={setSelectedTools} @@ -657,14 +523,9 @@ export default function AgentConfig() { setSubAgentList={setSubAgentList} enabledAgentIds={enabledAgentIds} setEnabledAgentIds={setEnabledAgentIds} - newAgentName={newAgentName} - newAgentDescription={newAgentDescription} - newAgentProvideSummary={newAgentProvideSummary} setNewAgentName={setNewAgentName} setNewAgentDescription={setNewAgentDescription} setNewAgentProvideSummary={setNewAgentProvideSummary} - isNewAgentInfoValid={isNewAgentInfoValid} - setIsNewAgentInfoValid={setIsNewAgentInfoValid} onEditingStateChange={handleEditingStateChange} onToolsRefresh={handleToolsRefresh} dutyContent={dutyContent} @@ -716,14 +577,7 @@ export default function AgentConfig() { setCurrentGuideStep(isCreatingNewAgent ? 5 : 5); }} getCurrentAgentId={getCurrentAgentId} - onModelChange={handleModelChange} - onMaxStepChange={handleMaxStepChange} - onBusinessLogicChange={handleBusinessLogicChange} onGenerateAgent={handleGenerateAgent} - onSaveAgent={handleSaveAgent} - isSavingAgent={isSavingAgent} - canSaveAgent={canSaveAgent} - getButtonTitle={getButtonTitle} onExportAgent={handleExportAgent} onDeleteAgent={handleDeleteAgent} editingAgent={editingAgent} @@ -752,10 +606,6 @@ export default function AgentConfig() { >
diff --git a/frontend/app/[locale]/setup/agentSetup/AgentManagementConfig.tsx b/frontend/app/[locale]/setup/agentSetup/AgentManagementConfig.tsx index 3fe9c3188..11835e6d7 100644 --- a/frontend/app/[locale]/setup/agentSetup/AgentManagementConfig.tsx +++ b/frontend/app/[locale]/setup/agentSetup/AgentManagementConfig.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useEffect, useCallback, useRef } from 'react' -import { Modal, App } from 'antd' +import { App } from 'antd' import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' import { TooltipProvider } from '@/components/ui/tooltip' @@ -9,33 +9,82 @@ import SubAgentPool from './components/SubAgentPool' import { MemoizedToolPool } from './components/ToolPool' import DeleteConfirmModal from './components/DeleteConfirmModal' import CollaborativeAgentDisplay from './components/CollaborativeAgentDisplay' -import SystemPromptDisplay from './components/SystemPromptDisplay' -// import AgentInfoInput from './components/AgentInfoInput' +import PromptManager from './components/PromptManager' import { - BusinessLogicConfigProps, Agent, - Tool, OpenAIModel, - AgentBasicInfo + Tool, } from './ConstInterface' import { getCreatingSubAgentId, fetchAgentList, - fetchAgentDetail, updateAgent, importAgent, - exportAgent, deleteAgent, searchAgentInfo } from '@/services/agentConfigService' + +// main component props interface +export interface BusinessLogicConfigProps { + businessLogic: string; + setBusinessLogic: (value: string) => void; + setSelectedAgents: (agents: Agent[]) => void; + selectedTools: Tool[]; + setSelectedTools: (tools: Tool[]) => void; + systemPrompt: string; + setSystemPrompt: (value: string) => void; + isCreatingNewAgent: boolean; + setIsCreatingNewAgent: (value: boolean) => void; + mainAgentModel: OpenAIModel; + setMainAgentModel: (value: OpenAIModel) => void; + mainAgentMaxStep: number; + setMainAgentMaxStep: (value: number) => void; + tools: Tool[]; + subAgentList?: Agent[]; + loadingAgents?: boolean; + mainAgentId: string | null; + setMainAgentId: (value: string | null) => void; + setSubAgentList: (agents: Agent[]) => void; + enabledAgentIds: number[]; + setEnabledAgentIds: (ids: number[]) => void; + setNewAgentName: (value: string) => void; + setNewAgentDescription: (value: string) => void; + setNewAgentProvideSummary: (value: boolean) => void; + onEditingStateChange?: (isEditing: boolean, agent: any) => void; + onToolsRefresh: () => void; + dutyContent: string; + setDutyContent: (value: string) => void; + constraintContent: string; + setConstraintContent: (value: string) => void; + fewShotsContent: string; + setFewShotsContent: (value: string) => void; + // Add new props for agent name and description + agentName?: string; + setAgentName?: (value: string) => void; + agentDescription?: string; + setAgentDescription?: (value: string) => void; + agentDisplayName?: string; + setAgentDisplayName?: (value: string) => void; + // Add new prop for generating agent state + isGeneratingAgent?: boolean; + // SystemPromptDisplay related props + onDebug?: () => void; + getCurrentAgentId?: () => number | undefined; + onGenerateAgent?: () => void; + onExportAgent?: () => void; + onDeleteAgent?: () => void; + editingAgent?: any; + onExitCreation?: () => void; +} + + /** * Business Logic Configuration Main Component */ export default function BusinessLogicConfig({ businessLogic, setBusinessLogic, - selectedAgents, setSelectedAgents, selectedTools, setSelectedTools, @@ -55,14 +104,9 @@ export default function BusinessLogicConfig({ setSubAgentList, enabledAgentIds, setEnabledAgentIds, - newAgentName, - newAgentDescription, - newAgentProvideSummary, setNewAgentName, setNewAgentDescription, setNewAgentProvideSummary, - isNewAgentInfoValid, - setIsNewAgentInfoValid, onEditingStateChange, onToolsRefresh, dutyContent, @@ -83,14 +127,7 @@ export default function BusinessLogicConfig({ // SystemPromptDisplay related props onDebug, getCurrentAgentId, - onModelChange: onModelChangeFromParent, - onMaxStepChange: onMaxStepChangeFromParent, - onBusinessLogicChange, onGenerateAgent, - onSaveAgent, - isSavingAgent, - canSaveAgent, - getButtonTitle, onExportAgent, onDeleteAgent, editingAgent: editingAgentFromParent, @@ -110,6 +147,9 @@ export default function BusinessLogicConfig({ const [isEditingAgent, setIsEditingAgent] = useState(false); const [editingAgent, setEditingAgent] = useState(null); + // Add a flag to track if it has been initialized to avoid duplicate calls + const hasInitialized = useRef(false); + const { t } = useTranslation('common'); const { message } = App.useApp(); @@ -125,7 +165,7 @@ export default function BusinessLogicConfig({ if (result.success) { // Update agent list with basic info only setSubAgentList(result.data); - message.success(t('businessLogic.config.message.agentListLoaded')); + // Removed success message to avoid duplicate notifications } else { message.error(result.message || t('businessLogic.config.error.agentListFailed')); } @@ -137,46 +177,11 @@ export default function BusinessLogicConfig({ } }; - // Handle agent selection and load detailed configuration - // Remove frontend caching logic, completely rely on backend returned sub_agent_id_list - const handleAgentSelectionOnly = (agent: Agent, isSelected: boolean) => { - // No longer perform frontend caching, completely rely on backend returned sub_agent_id_list - // This function is now just a placeholder, actual state management is controlled by backend - console.log('Agent selection changed:', agent.name, isSelected); - }; - - // Function to refresh agent state - const handleRefreshAgentState = async () => { - if (isEditingAgent && editingAgent) { - try { - // Add a small delay to ensure backend operation is completed - await new Promise(resolve => setTimeout(resolve, 100)); - - // Re-fetch detailed information of currently editing agent - const result = await searchAgentInfo(Number(editingAgent.id)); - if (result.success && result.data) { - const agentDetail = result.data; - - // Update enabledAgentIds - if (agentDetail.sub_agent_id_list && agentDetail.sub_agent_id_list.length > 0) { - const newEnabledAgentIds = agentDetail.sub_agent_id_list.map((id: any) => Number(id)); - setEnabledAgentIds(newEnabledAgentIds); - } else { - setEnabledAgentIds([]); - } - } - } catch (error) { - console.error('Failed to refresh agent state:', error); - } - } - }; - // Function to directly update enabledAgentIds const handleUpdateEnabledAgentIds = (newEnabledAgentIds: number[]) => { setEnabledAgentIds(newEnabledAgentIds); }; - const fetchSubAgentIdAndEnableToolList = async (t: TFunction) => { setIsLoadingTools(true); // Clear the tool selection status when loading starts @@ -248,26 +253,26 @@ export default function BusinessLogicConfig({ setNewAgentName(''); setNewAgentDescription(''); setNewAgentProvideSummary(true); - setIsNewAgentInfoValid(false); } else { // In edit mode, data is loaded in handleEditAgent, here validate the form - setIsNewAgentInfoValid(true); // Assume data is valid when editing console.log('Edit mode useEffect - Do not clear data'); // Debug information } - } else { - // When exiting the creation of a new Agent, reset the main Agent configuration - // Only refresh list when exiting creation mode in non-editing mode to avoid flicker when exiting editing mode - if (!isEditingAgent) { - setBusinessLogic(''); - setSystemPrompt(''); // Also clear the system prompt - setMainAgentModel(OpenAIModel.MainModel); - setMainAgentMaxStep(5); - // Delay refreshing agent list to avoid jumping - setTimeout(() => { - refreshAgentList(t); - }, 200); - } + } else { + // When exiting the creation of a new Agent, reset the main Agent configuration + // Only refresh list when exiting creation mode in non-editing mode to avoid flicker when exiting editing mode + if (!isEditingAgent && hasInitialized.current) { + setBusinessLogic(''); + setSystemPrompt(''); // Also clear the system prompt + setMainAgentModel(OpenAIModel.MainModel); + setMainAgentMaxStep(5); + // Delay refreshing agent list to avoid jumping + setTimeout(() => { + refreshAgentList(t); + }, 200); } + // Sign that has been initialized + hasInitialized.current = true; + } }, [isCreatingNewAgent, isEditingAgent]); // Listen for changes in the tool status, update the selected tool @@ -281,6 +286,19 @@ export default function BusinessLogicConfig({ setSelectedTools(enabledTools); }, [tools, enabledToolIds, isLoadingTools]); + // Listen for refresh agent list events from parent component + useEffect(() => { + const handleRefreshAgentList = () => { + refreshAgentList(t); + }; + + window.addEventListener('refreshAgentList', handleRefreshAgentList); + + return () => { + window.removeEventListener('refreshAgentList', handleRefreshAgentList); + }; + }, [t]); + // Handle the creation of a new Agent const handleCreateNewAgent = async () => { // Set to create mode @@ -312,7 +330,6 @@ export default function BusinessLogicConfig({ setNewAgentName(''); setNewAgentDescription(''); setNewAgentProvideSummary(true); - setIsNewAgentInfoValid(false); // Note: Content clearing is handled by onExitCreation above @@ -485,10 +502,11 @@ export default function BusinessLogicConfig({ setEditingAgent(agentDetail); // Set mainAgentId to current editing Agent ID setMainAgentId(agentDetail.id); - // When editing existing agent, ensure exit creation mode - // Note: This will be handled by the parent component's handleEditingStateChange - // which will cache the current creation content before switching - setIsCreatingNewAgent(false); + // When editing existing agent, ensure exit creation mode AFTER setting all data + // Use setTimeout to ensure all data is set before triggering useEffect + setTimeout(() => { + setIsCreatingNewAgent(false); + }, 100); // Increase delay to ensure state updates are processed // First set right-side name description box data to ensure immediate display console.log('setAgentName function exists:', !!setAgentName); @@ -623,50 +641,6 @@ export default function BusinessLogicConfig({ fileInput.click(); }; - // Handle exporting agent - const handleExportAgent = async (agent: Agent, t: TFunction) => { - try { - const result = await exportAgent(Number(agent.id)); - if (result.success) { - // Handle backend returned string or object - let exportData = result.data; - if (typeof exportData === 'string') { - try { - exportData = JSON.parse(exportData); - } catch (e) { - // If parsing fails, it means it's already a string, export directly - } - } - const blob = new Blob([JSON.stringify(exportData, null, 2)], { - type: 'application/json' - }); - - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `${agent.name}_config.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - message.success(t('businessLogic.config.message.agentExportSuccess')); - } else { - message.error(result.message || t('businessLogic.config.error.agentExportFailed')); - } - } catch (error) { - console.error(t('debug.console.exportAgentFailed'), error); - message.error(t('businessLogic.config.error.agentExportFailed')); - } - }; - - // Handle deleting agent - const handleDeleteAgent = async (agent: Agent) => { - // Show confirmation dialog - setAgentToDelete(agent); - setIsDeleteConfirmOpen(true); - }; - // Handle confirmed deletion const handleConfirmDelete = async (t: TFunction) => { if (!agentToDelete) return; @@ -702,7 +676,6 @@ export default function BusinessLogicConfig({ setNewAgentName(''); setNewAgentDescription(''); setNewAgentProvideSummary(true); - setIsNewAgentInfoValid(false); setBusinessLogic(''); setDutyContent(''); setConstraintContent(''); @@ -722,7 +695,7 @@ export default function BusinessLogicConfig({ // Refresh tool list const handleToolsRefresh = useCallback(async () => { if (onToolsRefresh) { - await onToolsRefresh(); + onToolsRefresh(); } }, [onToolsRefresh]); @@ -755,13 +728,10 @@ export default function BusinessLogicConfig({ onCreateNewAgent={handleCreateNewAgent} onExitEditMode={handleExitEditMode} onImportAgent={() => handleImportAgent(t)} - onExportAgent={(agent) => handleExportAgent(agent, t)} - onDeleteAgent={handleDeleteAgent} subAgentList={subAgentList} loadingAgents={loadingAgents} isImporting={isImporting} isGeneratingAgent={isGeneratingAgent} - isEditingAgent={isEditingAgent} editingAgent={editingAgent} isCreatingNewAgent={isCreatingNewAgent} /> @@ -806,7 +776,6 @@ export default function BusinessLogicConfig({ setSelectedTools(selectedTools.filter(t => t.id !== tool.id)); } }} - isCreatingNewAgent={isCreatingNewAgent} tools={tools} loadingTools={isLoadingTools} mainAgentId={isEditingAgent && editingAgent ? editingAgent.id : mainAgentId} @@ -822,7 +791,7 @@ export default function BusinessLogicConfig({ {/* Right column: System Prompt Display - 30% width */}
- {})} agentId={getCurrentAgentId ? getCurrentAgentId() : (isEditingAgent && editingAgent ? Number(editingAgent.id) : (isCreatingNewAgent && mainAgentId ? Number(mainAgentId) : undefined))} businessLogic={businessLogic} @@ -847,7 +816,6 @@ export default function BusinessLogicConfig({ onGenerateAgent={onGenerateAgent || (() => {})} onSaveAgent={handleSaveAgent} isGeneratingAgent={isGeneratingAgent} - isSavingAgent={isSavingAgent || false} isCreatingNewAgent={isCreatingNewAgent} canSaveAgent={localCanSaveAgent} getButtonTitle={getLocalButtonTitle} diff --git a/frontend/app/[locale]/setup/agentSetup/ConstInterface.tsx b/frontend/app/[locale]/setup/agentSetup/ConstInterface.tsx index 760877e82..cb57f6050 100644 --- a/frontend/app/[locale]/setup/agentSetup/ConstInterface.tsx +++ b/frontend/app/[locale]/setup/agentSetup/ConstInterface.tsx @@ -23,14 +23,6 @@ export interface Agent { sub_agent_id_list?: number[]; // 添加sub_agent_id_list字段 } -// Basic agent info for list display (without detailed configuration) -export interface AgentBasicInfo { - id: string; - name: string; - description: string; - is_available: boolean; -} - export interface Tool { id: string; name: string; @@ -39,7 +31,7 @@ export interface Tool { initParams: ToolParam[]; is_available?: boolean; create_time?: string; - usage?: string; // 新增:用于标注工具来源的usage字段 + usage?: string; // 用于标注工具来源的usage字段 } export interface ToolParam { @@ -49,117 +41,3 @@ export interface ToolParam { value?: any; description?: string; } - -// business logic input component props interface -export interface BusinessLogicInputProps { - value: string; - onChange: (value: string) => void; - selectedAgents: Agent[]; - isGenerating?: boolean; - generationProgress?: { - duty: boolean; - constraint: boolean; - few_shots: boolean; - }; - dutyContent?: string; - constraintContent?: string; - fewShotsContent?: string; -} -// sub agent pool component props interface -export interface SubAgentPoolProps { - selectedAgents: Agent[]; - onSelectAgent: (agent: Agent, isSelected: boolean) => void; - onSelectAgentAndLoadDetail?: (agent: Agent, isSelected: boolean) => void; - onEditAgent: (agent: Agent) => void; - onCreateNewAgent: () => void; - onImportAgent: () => void; - onExportAgent: (agent: Agent) => void; - onDeleteAgent: (agent: Agent) => void; - subAgentList?: Agent[]; - loadingAgents?: boolean; - enabledAgentIds?: number[]; - isImporting?: boolean; - isEditingAgent?: boolean; // 新增:控制是否处于编辑模式 - editingAgent?: Agent | null; // 新增:当前正在编辑的agent - parentAgentId?: number; // 新增:父agent ID,用于关联关系 - onAgentSelectionOnly?: (agent: Agent, isSelected: boolean) => void; // 新增:只更新选择状态,不调用search_info - onRefreshAgentState?: () => void; // 新增:刷新agent状态的回调函数 - onUpdateEnabledAgentIds?: (newEnabledAgentIds: number[]) => void; // 新增:直接更新enabledAgentIds的回调函数 - isCreatingNewAgent?: boolean; // 新增:控制是否处于新建agent模式 -} -// tool pool component props interface -export interface ToolPoolProps { - selectedTools: Tool[]; - onSelectTool: (tool: Tool, isSelected: boolean) => void; - isCreatingNewAgent?: boolean; - tools?: Tool[]; - loadingTools?: boolean; - mainAgentId?: string | null; - localIsGenerating?: boolean; - onToolsRefresh?: () => void; -} -// main component props interface -export interface BusinessLogicConfigProps { - businessLogic: string; - setBusinessLogic: (value: string) => void; - selectedAgents: Agent[]; - setSelectedAgents: (agents: Agent[]) => void; - selectedTools: Tool[]; - setSelectedTools: (tools: Tool[]) => void; - systemPrompt: string; - setSystemPrompt: (value: string) => void; - isCreatingNewAgent: boolean; - setIsCreatingNewAgent: (value: boolean) => void; - mainAgentModel: OpenAIModel; - setMainAgentModel: (value: OpenAIModel) => void; - mainAgentMaxStep: number; - setMainAgentMaxStep: (value: number) => void; - tools: Tool[]; - subAgentList?: Agent[]; - loadingAgents?: boolean; - mainAgentId: string | null; - setMainAgentId: (value: string | null) => void; - setSubAgentList: (agents: Agent[]) => void; - enabledAgentIds: number[]; - setEnabledAgentIds: (ids: number[]) => void; - newAgentName: string; - newAgentDescription: string; - newAgentProvideSummary: boolean; - setNewAgentName: (value: string) => void; - setNewAgentDescription: (value: string) => void; - setNewAgentProvideSummary: (value: boolean) => void; - isNewAgentInfoValid: boolean; - setIsNewAgentInfoValid: (value: boolean) => void; - onEditingStateChange?: (isEditing: boolean, agent: any) => void; - onToolsRefresh: () => void; - dutyContent: string; - setDutyContent: (value: string) => void; - constraintContent: string; - setConstraintContent: (value: string) => void; - fewShotsContent: string; - setFewShotsContent: (value: string) => void; - // Add new props for agent name and description - agentName?: string; - setAgentName?: (value: string) => void; - agentDescription?: string; - setAgentDescription?: (value: string) => void; - agentDisplayName?: string; - setAgentDisplayName?: (value: string) => void; - // Add new prop for generating agent state - isGeneratingAgent?: boolean; - // SystemPromptDisplay related props - onDebug?: () => void; - getCurrentAgentId?: () => number | undefined; - onModelChange?: (value: string) => void; - onMaxStepChange?: (value: number | null) => void; - onBusinessLogicChange?: (value: string) => void; - onGenerateAgent?: () => void; - onSaveAgent?: () => void; - isSavingAgent?: boolean; - canSaveAgent?: boolean; - getButtonTitle?: () => string; - onExportAgent?: () => void; - onDeleteAgent?: () => void; - editingAgent?: any; - onExitCreation?: () => void; -} diff --git a/frontend/app/[locale]/setup/agentSetup/DebugConfig.tsx b/frontend/app/[locale]/setup/agentSetup/DebugConfig.tsx index 3ad05c348..106de6dca 100644 --- a/frontend/app/[locale]/setup/agentSetup/DebugConfig.tsx +++ b/frontend/app/[locale]/setup/agentSetup/DebugConfig.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useRef } from 'react' -import { Typography, Input, Button } from 'antd' +import { Input } from 'antd' import { useTranslation } from 'react-i18next' import { conversationService } from '@/services/conversationService' import { handleStreamResponse } from '@/app/chat/streaming/chatStreamHandler' @@ -9,26 +9,17 @@ import { ChatMessageType, TaskMessageType } from '@/types/chat' import { ChatStreamFinalMessage } from '@/app/chat/streaming/chatStreamFinalMessage' import { TaskWindow } from '@/app/chat/streaming/taskWindow' -const { Text } = Typography // Agent debugging component Props interface interface AgentDebuggingProps { - question: string; - answer: string; onAskQuestion: (question: string) => void; onStop: () => void; isStreaming: boolean; messages: ChatMessageType[]; - taskMessages: TaskMessageType[]; - conversationGroups: Map; } // Main component Props interface interface DebugConfigProps { - testQuestion: string; - setTestQuestion: (question: string) => void; - testAnswer: string; - setTestAnswer: (answer: string) => void; agentId?: number; // Make agentId an optional prop } @@ -39,14 +30,10 @@ const stepIdCounter = { current: 0 }; * Agent debugging component */ function AgentDebugging({ - question, - answer, onAskQuestion, onStop, isStreaming, - messages, - taskMessages, - conversationGroups + messages }: AgentDebuggingProps) { const { t } = useTranslation() const [inputQuestion, setInputQuestion] = useState("") @@ -55,7 +42,7 @@ function AgentDebugging({ if (!inputQuestion.trim()) return; try { - await onAskQuestion(inputQuestion); + onAskQuestion(inputQuestion); setInputQuestion(""); } catch (error) { console.error(t('agent.error.loadTools'), error); @@ -206,17 +193,11 @@ function AgentDebugging({ * Debug configuration main component */ export default function DebugConfig({ - testQuestion, - setTestQuestion, - testAnswer, - setTestAnswer, agentId }: DebugConfigProps) { const { t } = useTranslation() const [messages, setMessages] = useState([]); const [isStreaming, setIsStreaming] = useState(false); - const [taskMessages, setTaskMessages] = useState([]); - const [conversationGroups] = useState>(new Map()); const timeoutRef = useRef(null); const abortControllerRef = useRef(null); @@ -274,7 +255,6 @@ export default function DebugConfig({ // Process test question const handleTestQuestion = async (question: string) => { - setTestQuestion(question); setIsStreaming(true); // Create new AbortController for this request @@ -336,12 +316,6 @@ export default function DebugConfig({ true, // isDebug: true for debug mode t ); - - // Update final answer - const lastMessage = messages[messages.length - 1]; - if (lastMessage && lastMessage.role === "assistant") { - setTestAnswer(lastMessage.finalAnswer || lastMessage.content || ""); - } } catch (error) { // If user actively canceled, don't show error message const err = error as Error; @@ -370,8 +344,6 @@ export default function DebugConfig({ } return newMessages; }); - - setTestAnswer(errorMessage); } } finally { setIsStreaming(false); @@ -388,14 +360,10 @@ export default function DebugConfig({ return (
) diff --git a/frontend/app/[locale]/setup/agentSetup/components/ActionButtons.tsx b/frontend/app/[locale]/setup/agentSetup/components/ActionButtons.tsx deleted file mode 100644 index 0a8b3988e..000000000 --- a/frontend/app/[locale]/setup/agentSetup/components/ActionButtons.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client" - -import { LoadingOutlined, ThunderboltOutlined } from '@ant-design/icons' -import { useTranslation } from 'react-i18next' - -interface ActionButtonsProps { - isPromptGenerating: boolean; - isPromptSaving: boolean; - isCreatingNewAgent: boolean; - canSaveAsAgent: boolean; - getButtonTitle: () => string; - onGeneratePrompt: () => void; - onSaveAsAgent: () => void; - onCancelCreating: () => void; -} - -/** - * Action Buttons Component - */ -export default function ActionButtons({ - isPromptGenerating, - isPromptSaving, - isCreatingNewAgent, - canSaveAsAgent, - getButtonTitle, - onGeneratePrompt, - onSaveAsAgent, - onCancelCreating -}: ActionButtonsProps) { - const { t } = useTranslation('common'); - - return ( -
- - {isCreatingNewAgent && ( - <> - - - - )} -
- ) -} \ No newline at end of file diff --git a/frontend/app/[locale]/setup/agentSetup/components/ActionButtonsSection.tsx b/frontend/app/[locale]/setup/agentSetup/components/ActionButtonsSection.tsx deleted file mode 100644 index cbac3f123..000000000 --- a/frontend/app/[locale]/setup/agentSetup/components/ActionButtonsSection.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client" - -import { Button } from 'antd' -import { BugOutlined, UploadOutlined, DeleteOutlined, SaveOutlined } from '@ant-design/icons' -import { useTranslation } from 'react-i18next' - -export interface ActionButtonsSectionProps { - onDebug?: () => void; - onExportAgent?: () => void; - onDeleteAgent?: () => void; - onSaveAgent?: () => void; - isCreatingNewAgent?: boolean; - isEditingMode?: boolean; - editingAgent?: any; - canSaveAgent?: boolean; - isSavingAgent?: boolean; - getButtonTitle?: () => string; -} - -export default function ActionButtonsSection({ - onDebug, - onExportAgent, - onDeleteAgent, - onSaveAgent, - isCreatingNewAgent, - isEditingMode, - editingAgent, - canSaveAgent, - isSavingAgent, - getButtonTitle -}: ActionButtonsSectionProps) { - const { t } = useTranslation('common') - - return ( -
-
- {/* Debug Button - Always show */} - - - {/* Export and Delete Buttons - Only show when editing existing agent */} - {isEditingMode && editingAgent && onExportAgent && !isCreatingNewAgent && ( - <> - - - - - )} - - {/* Save Button - Only show when creating new agent */} - {isCreatingNewAgent && ( - - )} -
-
- ) -} \ No newline at end of file diff --git a/frontend/app/[locale]/setup/agentSetup/components/AdditionalRequestInput.tsx b/frontend/app/[locale]/setup/agentSetup/components/AdditionalRequestInput.tsx index fb8344f3f..1a7c0fe6b 100644 --- a/frontend/app/[locale]/setup/agentSetup/components/AdditionalRequestInput.tsx +++ b/frontend/app/[locale]/setup/agentSetup/components/AdditionalRequestInput.tsx @@ -21,7 +21,7 @@ export default function AdditionalRequestInput({ onSend, isTuning = false }: Add const handleSend = async () => { if (request.trim()) { try { - await onSend(request) + onSend(request) } catch (error) { console.error(t("setup.promptTuning.error.send"), error) } diff --git a/frontend/app/[locale]/setup/agentSetup/components/AgentConfigurationSection.tsx b/frontend/app/[locale]/setup/agentSetup/components/AgentConfigurationSection.tsx index fefe1657f..1ce0fbbeb 100644 --- a/frontend/app/[locale]/setup/agentSetup/components/AgentConfigurationSection.tsx +++ b/frontend/app/[locale]/setup/agentSetup/components/AgentConfigurationSection.tsx @@ -6,6 +6,7 @@ import { useState, useEffect, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { OpenAIModel } from '../ConstInterface' import { SimplePromptEditor } from './PromptManager' +import { checkAgentName, checkAgentDisplayName } from '@/services/agentConfigService' export interface AgentConfigurationSectionProps { @@ -39,7 +40,6 @@ export interface AgentConfigurationSectionProps { isCreatingNewAgent?: boolean; editingAgent?: any; canSaveAgent?: boolean; - isSavingAgent?: boolean; getButtonTitle?: () => string; } @@ -62,7 +62,6 @@ export default function AgentConfigurationSection({ mainAgentMaxStep = 5, onModelChange, onMaxStepChange, - onSavePrompt, onExpandCard, isGeneratingAgent = false, // Add new props for action buttons @@ -74,7 +73,6 @@ export default function AgentConfigurationSection({ isCreatingNewAgent = false, editingAgent, canSaveAgent = false, - isSavingAgent = false, getButtonTitle }: AgentConfigurationSectionProps) { const { t } = useTranslation('common') @@ -92,6 +90,17 @@ export default function AgentConfigurationSection({ // Add state for agent name validation error const [agentNameError, setAgentNameError] = useState(''); + // Add state for agent name status check + const [agentNameStatus, setAgentNameStatus] = useState('available'); + // Add state to track if user is actively typing agent name + const [isUserTyping, setIsUserTyping] = useState(false); + + // Add state for agent display name validation error + const [agentDisplayNameError, setAgentDisplayNameError] = useState(''); + // Add state for agent display name status check + const [agentDisplayNameStatus, setAgentDisplayNameStatus] = useState('available'); + // Add state to track if user is actively typing agent display name + const [isUserTypingDisplayName, setIsUserTypingDisplayName] = useState(false); // Agent name validation function const validateAgentName = useCallback((name: string): string => { @@ -117,8 +126,124 @@ export default function AgentConfigurationSection({ const error = validateAgentName(name); setAgentNameError(error); onAgentNameChange?.(name); + + // Set user typing state to true when user actively changes the name + setIsUserTyping(true); }, [validateAgentName, onAgentNameChange]); + // Agent display name validation function + const validateAgentDisplayName = useCallback((displayName: string): string => { + if (!displayName.trim()) { + return t('agent.info.displayName.error.empty'); + } + if (displayName.length > 50) { + return t('agent.info.displayName.error.length'); + } + return ''; + }, [t]); + + // Handle agent display name change with validation + const handleAgentDisplayNameChange = useCallback((displayName: string) => { + const error = validateAgentDisplayName(displayName); + setAgentDisplayNameError(error); + onAgentDisplayNameChange?.(displayName); + + // Set user typing state to true when user actively changes the display name + setIsUserTypingDisplayName(true); + }, [validateAgentDisplayName, onAgentDisplayNameChange]); + + // Check agent name existence - only when user is actively typing + useEffect(() => { + if (!agentName || agentNameError) { + return; + } + + const checkName = async () => { + try { + // Pass the current agent ID to exclude it from the check when editing + const result = await checkAgentName(agentName, agentId); + setAgentNameStatus(result.status); + } catch (error) { + console.error('check agent name failed:', error); + setAgentNameStatus('check_failed'); + } + }; + + const timer = setTimeout(() => { + checkName(); + }, 300); + + return () => { + clearTimeout(timer); + }; + }, [isEditingMode, agentName, agentNameError, agentId, t]); + + // Reset user typing state after user stops typing + useEffect(() => { + if (!isUserTyping) return; + + const timer = setTimeout(() => { + setIsUserTyping(false); + }, 1000); + + return () => { + clearTimeout(timer); + }; + }, [isUserTyping, agentName]); + + // Clear name status when agent name is cleared or changed significantly + useEffect(() => { + if (!agentName || agentName.trim() === '') { + setAgentNameStatus('available'); + } + }, [agentName]); + + // Check agent display name existence - only when user is actively typing + useEffect(() => { + if ((!isEditingMode && !isCreatingNewAgent) || !agentDisplayName || agentDisplayNameError) { + return; + } + + const checkDisplayName = async () => { + try { + // Pass the current agent ID to exclude it from the check when editing + const result = await checkAgentDisplayName(agentDisplayName, agentId); + setAgentDisplayNameStatus(result.status); + } catch (error) { + console.error('check agent display name failed:', error); + setAgentDisplayNameStatus('check_failed'); + } + }; + + const timer = setTimeout(() => { + checkDisplayName(); + }, 300); + + return () => { + clearTimeout(timer); + }; + }, [isEditingMode, agentDisplayName, agentDisplayNameError, agentId, t]); + + // Reset user typing state for display name after user stops typing + useEffect(() => { + if (!isUserTypingDisplayName) return; + + const timer = setTimeout(() => { + setIsUserTypingDisplayName(false); + }, 1000); + + return () => { + clearTimeout(timer); + }; + }, [isUserTypingDisplayName, agentDisplayName]); + + // Clear display name status when agent display name is cleared or changed significantly + useEffect(() => { + if (!agentDisplayName || agentDisplayName.trim() === '') { + setAgentDisplayNameStatus('available'); + } + }, [agentDisplayName]); + // Handle delete confirmation const handleDeleteConfirm = useCallback(() => { setIsDeleteModalVisible(false); @@ -182,8 +307,22 @@ export default function AgentConfigurationSection({ } }, [agentName, isEditingMode, validateAgentName]); + // Validate agent display name when it changes externally + useEffect(() => { + if (agentDisplayName && isEditingMode) { + const error = validateAgentDisplayName(agentDisplayName); + setAgentDisplayNameError(error); + } else { + setAgentDisplayNameError(''); + } + }, [agentDisplayName, isEditingMode, validateAgentDisplayName]); + // Calculate whether save buttons should be enabled - const canActuallySave = canSaveAgent && !agentNameError; + const canActuallySave = canSaveAgent && + !agentNameError && + agentNameStatus !== 'exists_in_tenant' && + !agentDisplayNameError && + agentDisplayNameStatus !== 'exists_in_tenant'; // Render individual content sections const renderAgentInfo = () => ( @@ -196,11 +335,27 @@ export default function AgentConfigurationSection({ onAgentDisplayNameChange?.(e.target.value)} + onChange={(e) => { + handleAgentDisplayNameChange(e.target.value); + }} placeholder={t('agent.displayNamePlaceholder')} - className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 box-border" + className={`w-full px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 box-border ${ + agentDisplayNameError || agentDisplayNameStatus === 'exists_in_tenant' + ? 'border-red-500 focus:ring-red-500 focus:border-red-500' + : 'border-gray-300 focus:ring-blue-500 focus:border-blue-500' + }`} disabled={!isEditingMode} /> + {agentDisplayNameError && ( +

+ {agentDisplayNameError} +

+ )} + {!agentDisplayNameError && agentDisplayNameStatus === 'exists_in_tenant' && ( +

+ {t('agent.error.displayNameExists', { displayName: agentDisplayName })} +

+ )}
{/* Agent Name */} @@ -211,10 +366,12 @@ export default function AgentConfigurationSection({ handleAgentNameChange(e.target.value)} + onChange={(e) => { + handleAgentNameChange(e.target.value); + }} placeholder={t('agent.namePlaceholder')} className={`w-full px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 box-border ${ - agentNameError + agentNameError || agentNameStatus === 'exists_in_tenant' ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : 'border-gray-300 focus:ring-blue-500 focus:border-blue-500' }`} @@ -225,6 +382,11 @@ export default function AgentConfigurationSection({ {agentNameError}

)} + {!agentNameError && agentNameStatus === 'exists_in_tenant' && ( +

+ {t('agent.error.nameExists', { name: agentName })} +

+ )}
{/* Model Selection */} @@ -567,6 +729,26 @@ export default function AgentConfigurationSection({ font-size: 14px; color: #666; } + + /* Fix Ant Design button hover border color issues - ensure consistent color scheme */ + .responsive-button.ant-btn:hover { + border-color: inherit !important; + } + + /* Blue button: hover background blue-600, border should also be blue-600 */ + .bg-blue-500.hover\\:bg-blue-600.border-blue-500.hover\\:border-blue-600.ant-btn:hover { + border-color: #2563eb !important; /* blue-600 */ + } + + /* Green button: hover background green-600, border should also be green-600 */ + .bg-green-500.hover\\:bg-green-600.border-green-500.hover\\:border-green-600.ant-btn:hover { + border-color: #16a34a !important; /* green-600 */ + } + + /* Red button: hover background red-600, border should also be red-600 */ + .bg-red-500.hover\\:bg-red-600.border-red-500.hover\\:border-red-600.ant-btn:hover { + border-color: #dc2626 !important; /* red-600 */ + } `}
@@ -611,7 +793,7 @@ export default function AgentConfigurationSection({ size="middle" icon={} onClick={onDebug} - className="bg-blue-500 hover:bg-blue-600 border-blue-500 hover:border-blue-600 responsive-button" + className="bg-blue-500 hover:bg-blue-600 responsive-button" title={t('systemPrompt.button.debug')} > {t('systemPrompt.button.debug')} @@ -625,7 +807,7 @@ export default function AgentConfigurationSection({ size="middle" icon={} onClick={onExportAgent} - className="bg-green-500 hover:bg-green-600 border-green-500 hover:border-green-600 responsive-button" + className="bg-green-500 hover:bg-green-600 responsive-button" title={t('agent.contextMenu.export')} > {t('agent.contextMenu.export')} @@ -636,7 +818,7 @@ export default function AgentConfigurationSection({ size="middle" icon={} onClick={handleDeleteClick} - className="bg-red-500 hover:bg-red-600 border-red-500 hover:border-red-600 responsive-button" + className="bg-red-500 hover:bg-red-600 responsive-button" title={t('agent.contextMenu.delete')} > {t('agent.contextMenu.delete')} @@ -652,11 +834,20 @@ export default function AgentConfigurationSection({ icon={} onClick={onSaveAgent} disabled={!canActuallySave} - className="bg-green-500 hover:bg-green-600 border-green-500 hover:border-green-600 disabled:opacity-50 disabled:cursor-not-allowed responsive-button" + className="bg-green-500 hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed responsive-button" title={(() => { if (agentNameError) { return agentNameError; } + if (agentNameStatus === 'exists_in_tenant') { + return t('agent.error.nameExists', { name: agentName }); + } + if (agentDisplayNameError) { + return agentDisplayNameError; + } + if (agentDisplayNameStatus === 'exists_in_tenant') { + return t('agent.error.displayNameExists', { displayName: agentDisplayName }); + } if (!canSaveAgent && getButtonTitle) { const tooltipText = getButtonTitle(); return tooltipText || t('businessLogic.config.button.saveToAgentPool'); @@ -664,7 +855,7 @@ export default function AgentConfigurationSection({ return t('businessLogic.config.button.saveToAgentPool'); })()} > - {isSavingAgent ? t('businessLogic.config.button.saving') : t('businessLogic.config.button.saveToAgentPool')} + {t('businessLogic.config.button.saveToAgentPool')} ) : ( )}
@@ -723,4 +923,4 @@ export default function AgentConfigurationSection({
) -} \ No newline at end of file +} diff --git a/frontend/app/[locale]/setup/agentSetup/components/AgentContextMenu.tsx b/frontend/app/[locale]/setup/agentSetup/components/AgentContextMenu.tsx index 4631bfc1a..15ffe6f33 100644 --- a/frontend/app/[locale]/setup/agentSetup/components/AgentContextMenu.tsx +++ b/frontend/app/[locale]/setup/agentSetup/components/AgentContextMenu.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' import { EditOutlined, ExportOutlined, DeleteOutlined } from '@ant-design/icons' import { Agent } from '../ConstInterface' import { useTranslation } from 'react-i18next' diff --git a/frontend/app/[locale]/setup/agentSetup/components/AgentInfoInput.tsx b/frontend/app/[locale]/setup/agentSetup/components/AgentInfoInput.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/app/[locale]/setup/agentSetup/components/AgentSelector.tsx b/frontend/app/[locale]/setup/agentSetup/components/AgentSelector.tsx deleted file mode 100644 index e546fc0fc..000000000 --- a/frontend/app/[locale]/setup/agentSetup/components/AgentSelector.tsx +++ /dev/null @@ -1,156 +0,0 @@ -"use client" - -import { useState, useEffect, useCallback } from 'react' -import { Card, List, Avatar, Typography, Spin, Empty, App } from 'antd' -import { UserOutlined } from '@ant-design/icons' -import { useTranslation } from 'react-i18next' -import { fetchAllAgents } from '@/services/agentConfigService' - -const { Text } = Typography - -interface AgentBasicInfo { - agent_id: number - name: string - display_name: string - description: string - is_available: boolean -} - -interface AgentSelectorProps { - onAgentSelect: (agent: AgentBasicInfo) => void - selectedAgentId?: number | null -} - -export default function AgentSelector({ onAgentSelect, selectedAgentId }: AgentSelectorProps) { - const { t } = useTranslation('common') - const { message } = App.useApp() - const [agents, setAgents] = useState([]) - const [loading, setLoading] = useState(true) - const [selectedAgent, setSelectedAgent] = useState(null) - - // Get basic information of all agents - const loadAgents = useCallback(async () => { - setLoading(true) - try { - const result = await fetchAllAgents() - if (result.success) { - setAgents(result.data) - // If there is a pre-selected agent, set selected state - if (selectedAgentId) { - const preSelectedAgent = result.data.find((agent: AgentBasicInfo) => agent.agent_id === selectedAgentId) - if (preSelectedAgent) { - setSelectedAgent(preSelectedAgent) - } - } - } else { - message.error(result.message || t('agent.error.fetchAgentList')) - } - } catch (error) { - console.error('Failed to get agent list:', error) - message.error(t('agent.error.fetchAgentListRetry')) - } finally { - setLoading(false) - } - }, [selectedAgentId, t]) - - useEffect(() => { - loadAgents() - }, [loadAgents]) - - const handleAgentClick = (agent: AgentBasicInfo) => { - setSelectedAgent(agent) - onAgentSelect(agent) - } - - const renderAgentItem = (agent: AgentBasicInfo) => { - const isSelected = selectedAgent?.agent_id === agent.agent_id - const isAvailable = agent.is_available - - return ( - isAvailable && handleAgentClick(agent)} - style={{ - cursor: isAvailable ? 'pointer' : 'not-allowed', - backgroundColor: isSelected ? '#f0f0f0' : 'transparent', - padding: '12px 16px', - borderRadius: '8px', - margin: '4px 0', - border: isSelected ? '2px solid #1890ff' : '1px solid #f0f0f0', - opacity: isAvailable ? 1 : 0.6 - }} - > - } - style={{ - backgroundColor: isSelected ? '#1890ff' : '#d9d9d9', - color: isSelected ? 'white' : '#666' - }} - /> - } - title={ -
- - {agent.display_name && ( - - {agent.display_name} - - )} - - {agent.name} - - - {!isAvailable && ( - - - {t('agent.status.unavailable')} - - )} -
- } - description={ - - {agent.description || t('agent.description.empty')} - - } - /> -
- ) - } - - if (loading) { - return ( - -
- -
- {t('agent.loading')} -
-
-
- ) - } - - if (agents.length === 0) { - return ( - - - - ) - } - - return ( - - - - ) -} \ No newline at end of file diff --git a/frontend/app/[locale]/setup/agentSetup/components/BusinessLogicInput.tsx b/frontend/app/[locale]/setup/agentSetup/components/BusinessLogicInput.tsx deleted file mode 100644 index 44d0dcce1..000000000 --- a/frontend/app/[locale]/setup/agentSetup/components/BusinessLogicInput.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client" - -import { Input } from 'antd' -import { useTranslation } from 'react-i18next' - -const { TextArea } = Input - -interface BusinessLogicInputProps { - value: string; - onChange: (value: string) => void; -} - -/** - * Business Logic Input Component - */ -export default function BusinessLogicInput({ - value, - onChange -}: BusinessLogicInputProps) { - const { t } = useTranslation('common'); - - return ( -
-
-
- 3 -
-

{t('businessLogic.title')}

-
-
-