diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c4796f5b8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,jsx,ts,tsx,json}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.github/workflows/docker-deploy.yml b/.github/workflows/docker-deploy.yml index 82107df0c..9d04c8913 100644 --- a/.github/workflows/docker-deploy.yml +++ b/.github/workflows/docker-deploy.yml @@ -15,6 +15,11 @@ on: description: 'runner array in json format (e.g. ["ubuntu-latest"] or ["self-hosted"])' required: true default: '[]' + app_version: + description: 'Docker image tag to build and deploy (e.g. v1.7.1)' + required: true + default: 'latest' + type: string jobs: build-main: @@ -23,7 +28,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Build main application image - run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent -f make/main/Dockerfile . + run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent:${{ github.event.inputs.app_version }} -t nexent/nexent -f make/main/Dockerfile . build-data-process: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -47,7 +52,7 @@ jobs: GIT_TRACE=1 GIT_CURL_VERBOSE=1 GIT_LFS_LOG=debug git lfs pull rm -rf .git .gitattributes - name: Build data process image - run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent-data-process -f make/data_process/Dockerfile . + run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent-data-process:${{ github.event.inputs.app_version }} -t nexent/nexent-data-process -f make/data_process/Dockerfile . build-web: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -55,7 +60,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Build web frontend image - run: docker build --build-arg MIRROR=https://registry.npmmirror.com --build-arg APK_MIRROR=tsinghua -t nexent/nexent-web -f make/web/Dockerfile . + run: docker build --build-arg MIRROR=https://registry.npmmirror.com --build-arg APK_MIRROR=tsinghua -t nexent/nexent-web:${{ github.event.inputs.app_version }} -t nexent/nexent-web -f make/web/Dockerfile . build-docs: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -63,7 +68,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Build docs image - run: docker build --progress=plain -t nexent/nexent-docs -f make/docs/Dockerfile . + run: docker build --progress=plain -t nexent/nexent-docs:${{ github.event.inputs.app_version }} -t nexent/nexent-docs -f make/docs/Dockerfile . deploy: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -76,6 +81,9 @@ jobs: rm -rf $HOME/nexent mkdir -p $HOME/nexent cp -r $GITHUB_WORKSPACE/* $HOME/nexent/ + - name: Force APP_VERSION to latest in deploy.sh (CI only) + run: | + sed -i 's/APP_VERSION="$(get_app_version)"/APP_VERSION="${{ github.event.inputs.app_version }}"/' $HOME/nexent/docker/deploy.sh - name: Start docs container run: | docker stop nexent-docs 2>/dev/null || true diff --git a/backend/config_service.py b/backend/config_service.py index f98c7b155..ebe5b7593 100644 --- a/backend/config_service.py +++ b/backend/config_service.py @@ -1,7 +1,6 @@ import uvicorn import logging import warnings -import asyncio from consts.const import APP_VERSION @@ -12,30 +11,14 @@ from apps.config_app import app from utils.logging_utils import configure_logging, configure_elasticsearch_logging -from services.tool_configuration_service import initialize_tools_on_startup + configure_logging(logging.INFO) configure_elasticsearch_logging() logger = logging.getLogger("config_service") -async def startup_initialization(): - """ - Perform initialization tasks during server startup - """ +if __name__ == "__main__": logger.info("Starting server initialization...") logger.info(f"APP version is: {APP_VERSION}") - try: - # Initialize tools on startup - service layer handles detailed logging - await initialize_tools_on_startup() - logger.info("Server initialization completed successfully!") - - except Exception as e: - logger.error(f"Server initialization failed: {str(e)}") - # Don't raise the exception to allow server to start even if initialization fails - logger.warning("Server will continue to start despite initialization issues") - - -if __name__ == "__main__": - asyncio.run(startup_initialization()) uvicorn.run(app, host="0.0.0.0", port=5010, log_level="info") diff --git a/backend/consts/const.py b/backend/consts/const.py index 0b5f4bcef..10d977219 100644 --- a/backend/consts/const.py +++ b/backend/consts/const.py @@ -285,4 +285,4 @@ class VectorDatabaseType(str, Enum): # APP Version -APP_VERSION = "v1.7.8" +APP_VERSION = "v1.7.8.1" diff --git a/backend/runtime_service.py b/backend/runtime_service.py index faa3d2981..9fd42d9a7 100644 --- a/backend/runtime_service.py +++ b/backend/runtime_service.py @@ -1,7 +1,6 @@ import uvicorn import logging import warnings -import asyncio from consts.const import APP_VERSION @@ -12,31 +11,16 @@ from apps.runtime_app import app from utils.logging_utils import configure_logging, configure_elasticsearch_logging -from services.tool_configuration_service import initialize_tools_on_startup + configure_logging(logging.INFO) configure_elasticsearch_logging() logger = logging.getLogger("runtime_service") -async def startup_initialization(): - """ - Perform initialization tasks during server startup - """ +if __name__ == "__main__": logger.info("Starting server initialization...") logger.info(f"APP version is: {APP_VERSION}") - try: - # Initialize tools on startup - service layer handles detailed logging - await initialize_tools_on_startup() - logger.info("Server initialization completed successfully!") - except Exception as e: - logger.error(f"Server initialization failed: {str(e)}") - # Don't raise the exception to allow server to start even if initialization fails - logger.warning("Server will continue to start despite initialization issues") - - -if __name__ == "__main__": - asyncio.run(startup_initialization()) uvicorn.run(app, host="0.0.0.0", port=5014, log_level="info") diff --git a/backend/services/tool_configuration_service.py b/backend/services/tool_configuration_service.py index 66298c8c5..97a386a29 100644 --- a/backend/services/tool_configuration_service.py +++ b/backend/services/tool_configuration_service.py @@ -11,7 +11,7 @@ import jsonref from mcpadapt.smolagents_adapter import _sanitize_function_name -from consts.const import DEFAULT_USER_ID, LOCAL_MCP_SERVER, DATA_PROCESS_SERVICE +from consts.const import LOCAL_MCP_SERVER, DATA_PROCESS_SERVICE from consts.exceptions import MCPConnectionError, ToolExecutionException, NotFoundException from consts.model import ToolInstanceInfoRequest, ToolInfo, ToolSourceEnum, ToolValidateRequest from database.remote_mcp_db import get_mcp_records_by_tenant, get_mcp_server_by_name_and_tenant @@ -22,7 +22,6 @@ update_tool_table_from_scan_tool_list, search_last_tool_instance_by_tool_id, ) -from database.user_tenant_db import get_all_tenant_ids from services.file_management_service import get_llm_model from services.vectordatabase_service import get_embedding_model, get_vector_db_core from services.tenant_config_service import get_selected_knowledge_list, build_knowledge_name_mapping @@ -367,71 +366,6 @@ async def list_all_tools(tenant_id: str): return formatted_tools -async def initialize_tools_on_startup(): - """ - Initialize and scan all tools during server startup for all tenants - - This function scans all available tools (local, LangChain, and MCP) - and updates the database with the latest tool information for all tenants. - """ - - logger.info("Starting tool initialization on server startup...") - - try: - # Get all tenant IDs from the database - tenant_ids = get_all_tenant_ids() - - if not tenant_ids: - logger.warning("No tenants found in database, skipping tool initialization") - return - - logger.info(f"Found {len(tenant_ids)} tenants: {tenant_ids}") - - total_tools = 0 - successful_tenants = 0 - failed_tenants = [] - - # Process each tenant - for tenant_id in tenant_ids: - try: - logger.info(f"Initializing tools for tenant: {tenant_id}") - - # Add timeout to prevent hanging during startup - try: - await asyncio.wait_for( - update_tool_list(tenant_id=tenant_id, user_id=DEFAULT_USER_ID), - timeout=60.0 # 60 seconds timeout per tenant - ) - - # Get the count of tools for this tenant - tools_info = query_all_tools(tenant_id) - tenant_tool_count = len(tools_info) - total_tools += tenant_tool_count - successful_tenants += 1 - - logger.info(f"Tenant {tenant_id}: {tenant_tool_count} tools initialized") - - except asyncio.TimeoutError: - logger.error(f"Tool initialization timed out for tenant {tenant_id}") - failed_tenants.append(f"{tenant_id} (timeout)") - - except Exception as e: - logger.error(f"Tool initialization failed for tenant {tenant_id}: {str(e)}") - failed_tenants.append(f"{tenant_id} (error: {str(e)})") - - # Log final results - logger.info("Tool initialization completed!") - logger.info(f"Total tools available across all tenants: {total_tools}") - logger.info(f"Successfully processed: {successful_tenants}/{len(tenant_ids)} tenants") - - if failed_tenants: - logger.warning(f"Failed tenants: {', '.join(failed_tenants)}") - - except Exception as e: - logger.error(f"❌ Tool initialization failed: {str(e)}") - raise - - def load_last_tool_config_impl(tool_id: int, tenant_id: str, user_id: str): """ Load the last tool configuration for a given tool ID diff --git a/backend/utils/llm_utils.py b/backend/utils/llm_utils.py index 2e1590498..e74ef91b3 100644 --- a/backend/utils/llm_utils.py +++ b/backend/utils/llm_utils.py @@ -18,16 +18,37 @@ def _process_thinking_tokens( ) -> bool: """ Process tokens to filter out thinking content between and tags. + Handles cases where providers only send a closing tag or mix reasoning_content. """ - if is_thinking: - return THINK_END_PATTERN not in new_token + # Check for end tag first, as it might appear in the same token as start tag + if THINK_END_PATTERN in new_token: + # If we were never in think mode, treat everything accumulated so far as reasoning and clear it + if not is_thinking: + token_join.clear() + if callback: + callback("") # clear any previously streamed reasoning content + + # Exit thinking mode and only keep content after + _, _, after_end = new_token.partition(THINK_END_PATTERN) + is_thinking = False + new_token = after_end + # Continue processing the remaining content in this token + # Check for start tag (after processing end tag, in case both are in the same token) if THINK_START_PATTERN in new_token: + # Drop any content before and switch to thinking mode + _, _, after_start = new_token.partition(THINK_START_PATTERN) + new_token = after_start + is_thinking = True + + if is_thinking: + # Still inside thinking content; ignore until we exit return True - token_join.append(new_token) - if callback: - callback("".join(token_join)) + if new_token: + token_join.append(new_token) + if callback: + callback("".join(token_join)) return False @@ -46,8 +67,8 @@ def call_llm_for_system_prompt( llm = OpenAIModel( model_id=get_model_name_from_config(llm_model_config) if llm_model_config else "", - api_base=llm_model_config.get("base_url", ""), - api_key=llm_model_config.get("api_key", ""), + api_base=llm_model_config.get("base_url", "") if llm_model_config else "", + api_key=llm_model_config.get("api_key", "") if llm_model_config else "", temperature=0.3, top_p=0.95, ) @@ -65,16 +86,38 @@ def call_llm_for_system_prompt( current_request = llm.client.chat.completions.create(stream=True, **completion_kwargs) token_join: List[str] = [] is_thinking = False + reasoning_content_seen = False + content_tokens_seen = 0 for chunk in current_request: - new_token = chunk.choices[0].delta.content + delta = chunk.choices[0].delta + reasoning_content = getattr(delta, "reasoning_content", None) + new_token = delta.content + + # Note: reasoning_content is separate metadata and doesn't affect content filtering + # We only filter content based on tags in delta.content + if reasoning_content: + reasoning_content_seen = True + logger.debug("Received reasoning_content (metadata only, not filtering content)") + + # Process content token if it exists if new_token is not None: + content_tokens_seen += 1 is_thinking = _process_thinking_tokens( new_token, is_thinking, token_join, callback, ) - return "".join(token_join) + + result = "".join(token_join) + if not result and content_tokens_seen > 0: + logger.warning( + "Generated prompt is empty but %d content tokens were processed. " + "This suggests all content was filtered out.", + content_tokens_seen + ) + + return result except Exception as exc: logger.error("Failed to generate prompt from LLM: %s", str(exc)) raise diff --git a/doc/docs/.vitepress/config.mts b/doc/docs/.vitepress/config.mts index 059937f09..884fa28bb 100644 --- a/doc/docs/.vitepress/config.mts +++ b/doc/docs/.vitepress/config.mts @@ -1,4 +1,4 @@ -// https://vitepress.dev/reference/site-config +// https://vitepress.dev/reference/site-config import { defineConfig } from "vitepress"; export default defineConfig({ @@ -89,6 +89,8 @@ export default defineConfig({ text: "Knowledge Base", link: "/en/user-guide/knowledge-base", }, + { text: "MCP Tools", link: "/en/user-guide/mcp-tools" }, + { text: "Monitoring & Ops", link: "/en/user-guide/monitor" }, { text: "Model Management", link: "/en/user-guide/model-management", @@ -267,7 +269,7 @@ export default defineConfig({ items: [ { text: "首页", link: "/zh/user-guide/home-page" }, { text: "开始问答", link: "/zh/user-guide/start-chat" }, - { text: "快速设置", link: "/zh/user-guide/quick-setup" }, + { text: "快速配置", link: "/zh/user-guide/quick-setup" }, { text: "智能体空间", link: "/zh/user-guide/agent-space" }, { text: "智能体市场", link: "/zh/user-guide/agent-market" }, { @@ -278,6 +280,8 @@ export default defineConfig({ text: "知识库", link: "/zh/user-guide/knowledge-base", }, + { text: "MCP工具", link: "/zh/user-guide/mcp-tools" }, + { text: "监控与运维", link: "/zh/user-guide/monitor" }, { text: "模型管理", link: "/zh/user-guide/model-management" }, { text: "记忆管理", link: "/zh/user-guide/memory-management" }, { text: "用户管理", link: "/zh/user-guide/user-management" }, diff --git a/doc/docs/en/user-guide/agent-development.md b/doc/docs/en/user-guide/agent-development.md index bc7d6c42d..9f2f633fa 100644 --- a/doc/docs/en/user-guide/agent-development.md +++ b/doc/docs/en/user-guide/agent-development.md @@ -19,6 +19,8 @@ If you have an existing agent configuration, you can also import it: > - **Import anyway**: Keep the duplicate name; the imported agent will be in an unavailable state and requires manual modification of the Agent name and variable name before it can be used > - **Regenerate and import**: The system will call the LLM to rename the Agent, which will consume a certain amount of model tokens and may take longer +> 📌 **Important:** For agents created via import, if their tools include `knowledge_base_search` or other knowledge base search tools, these tools will only search **knowledge bases that the currently logged-in user is allowed to access in this environment**. The original knowledge base configuration in the exported agent will *not* be automatically inherited, so actual search results and answer quality may differ from what the original author observed. +
@@ -40,7 +42,7 @@ You can configure other collaborative agents for your created agent, as well as ### 🛠️ Select Agent Tools -Agents can use various tools to complete tasks, such as knowledge base search, email sending, file management, and other local tools. They can also integrate third-party MCP tools or custom tools. +Agents can use various tools to complete tasks, such as knowledge base search, file parsing, image parsing, email sending/receiving, file management, and other local tools. They can also integrate third-party MCP tools or custom tools. 1. On the "Select Tools" tab, click "Refresh Tools" to update the available tool list 2. Select the group containing the tool you want to add @@ -65,7 +67,7 @@ Agents can use various tools to complete tasks, such as knowledge base search, e Nexent allows you to quickly and easily use third-party MCP tools to enrich agent capabilities. 1. On the "Select Agent Tools" tab, click "MCP Config" to configure MCP servers in the popup and view configured servers -2. Enter the server name and URL (currently only SSE protocol is supported) +2. Enter the server name and server URL (supports SSE and Streamable HTTP protocols) - ⚠️ **Note:** The server name must contain only English letters or digits; spaces, underscores, and other characters are not allowed. 3. Click "Add" to complete the addition @@ -111,7 +113,7 @@ Nexent provides a "Tool Testing" capability for all types of tools—whether the Based on the selected collaborative agents and tools, you can now describe in simple language how you expect this agent to work. Nexent will automatically generate the agent name, description, and prompts based on your configuration and description. 1. In the editor under "Describe how should this agent work", enter a brief description, such as "You are a professional knowledge Q&A assistant with local knowledge search and online search capabilities, synthesizing information to answer user questions" -2. Click the "Generate" button, and Nexent will generate detailed agent content for you, including basic information and prompts (role, usage requirements, few shots) +2. Select a model (choose a smarter model when generating prompts to optimize response logic), click the "Generate Agent" button, and Nexent will generate detailed agent content for you, including basic information and prompts (role, usage requirements, examples) 3. You can edit and fine-tune the auto-generated content (especially the prompts) in the Agent Detail Content below
@@ -144,6 +146,10 @@ View the collaborative agents/tools used by the agent, displayed in a tree diagr Export successfully debugged agents as JSON configuration files. You can use this JSON file to create a copy by importing it when creating an agent. +### 📋 Copy + +Copy an agent to facilitate experimentation, multi-version debugging, and parallel development. + ### 🗑️ Delete Delete an agent (this cannot be undone, please proceed with caution). diff --git a/doc/docs/en/user-guide/agent-market.md b/doc/docs/en/user-guide/agent-market.md index 231aa09aa..6fdd8cf84 100644 --- a/doc/docs/en/user-guide/agent-market.md +++ b/doc/docs/en/user-guide/agent-market.md @@ -1,37 +1,63 @@ # Agent Market -Agent Market is an upcoming Nexent module that will provide a curated catalog of ready-to-use agents. +🎁 Here you'll find high-quality agents created by **Nexent Official** and **community creators** -## 🎯 Coming Features +You can use them directly to complete specific tasks, or incorporate them as sub-agents into your own agents -Agent Market will let you: +![Agent Market](./assets/agent-market/agent-market.png) -- **Browse agents** – Explore featured and community-built agents. -- **Install instantly** – Add high-quality agents to your workspace with one click. -- **Share your work** – Publish the agents you build. -- **Rate & give feedback** – Help the community discover great agents. +## 🔍 Explore and Discover -## ⏳ Stay Tuned +You can quickly find the best agents through the following methods: -Agent Market is currently under development. We’re building an ecosystem where you can: +1. Browse or search by use case category +2. View agent feature descriptions to confirm if they meet your needs 🆗 +3. Check built-in tools to confirm if they are ready or available ✅ -- Quickly access verified agents. -- Share your creations with the community. -- Discover new use cases and inspiration. +
-## 📢 Follow Updates -Want to know when Agent Market launches? + Select Agent -- Join the [Discord community](https://discord.gg/tb5H3S3wyv) for announcements. -- Track project updates in the repository. + Dialog Box +
+ +## 🔧 Install Agents + +Select your preferred agent, download with one click, and add it to your agent space immediately + +### 1️⃣ Select Models + +🌟 Confirm model availability + +✍️ Configure the same model for all agents uniformly, or select appropriate models for the main agent and sub-agents separately + +![Agent Market Download](./assets/agent-market/agent-market-download.png) + +### 2️⃣ Configure Fields + +🔑 Fill in tool permissions as prompted + +![Agent Market Download 2](./assets/agent-market/agent-market-download2.png) + +After installation, your agent will be ready in **[Agent Space](./agent-space)** + +## 📢 Share Your Creations + +Created an excellent agent? 👍 + +Welcome to share your work in [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions), and we'll contact you as soon as possible to let more people see and use it! ## 🚀 Related Features -While Agent Market is being built, you can: +While waiting for the Agent Market to launch, you can: -1. Manage your agents in **[Agent Space](./agent-space)**. -2. Create new agents in **[Agent Development](./agent-development)**. -3. Test agents in **[Start Chat](./start-chat)**. +1. Manage your own agents in **[Agent Space](./agent-space)** +2. Create custom agents through **[Agent Development](./agent-development)** +3. Experience the powerful features of agents in **[Start Chat](./start-chat)** -Need help? Check the **[FAQ](../quick-start/faq)** or open a thread in [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions). +If you encounter any issues during use, please refer to our **[FAQ](../quick-start/faq)** or ask for support in [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions). diff --git a/doc/docs/en/user-guide/agent-space.md b/doc/docs/en/user-guide/agent-space.md index cd3f66fce..56f77a3de 100644 --- a/doc/docs/en/user-guide/agent-space.md +++ b/doc/docs/en/user-guide/agent-space.md @@ -46,6 +46,11 @@ Click a card to open its details: 1. Click **Export** on the card. 2. Nexent downloads a JSON configuration file you can import later. +### Copy an Agent + +1. Click **Copy** on the card to duplicate the agent. +2. This facilitates experimentation, multi-version debugging, and parallel development. + ### View Relationships 1. Click **View Relationships** to see how the agent interacts with tools and other agents. diff --git a/doc/docs/en/user-guide/assets/agent-development/generate-agent.png b/doc/docs/en/user-guide/assets/agent-development/generate-agent.png index e1f93eeb6..876c42e18 100644 Binary files a/doc/docs/en/user-guide/assets/agent-development/generate-agent.png and b/doc/docs/en/user-guide/assets/agent-development/generate-agent.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market-detail.png b/doc/docs/en/user-guide/assets/agent-market/agent-market-detail.png new file mode 100644 index 000000000..b7127a049 Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-market/agent-market-detail.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market-detail2.png b/doc/docs/en/user-guide/assets/agent-market/agent-market-detail2.png new file mode 100644 index 000000000..3d7baf654 Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-market/agent-market-detail2.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market-download.png b/doc/docs/en/user-guide/assets/agent-market/agent-market-download.png new file mode 100644 index 000000000..2f258676d Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-market/agent-market-download.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market-download2.png b/doc/docs/en/user-guide/assets/agent-market/agent-market-download2.png new file mode 100644 index 000000000..4bf6d9491 Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-market/agent-market-download2.png differ diff --git a/doc/docs/en/user-guide/assets/agent-market/agent-market.png b/doc/docs/en/user-guide/assets/agent-market/agent-market.png new file mode 100644 index 000000000..d8e71e014 Binary files /dev/null and b/doc/docs/en/user-guide/assets/agent-market/agent-market.png differ diff --git a/doc/docs/en/user-guide/assets/agent-space/agent-space.png b/doc/docs/en/user-guide/assets/agent-space/agent-space.png index 952596236..b43f00d21 100644 Binary files a/doc/docs/en/user-guide/assets/agent-space/agent-space.png and b/doc/docs/en/user-guide/assets/agent-space/agent-space.png differ diff --git a/doc/docs/en/user-guide/assets/home-page/homepage.png b/doc/docs/en/user-guide/assets/home-page/homepage.png index c6bc61a82..cb00c9561 100644 Binary files a/doc/docs/en/user-guide/assets/home-page/homepage.png and b/doc/docs/en/user-guide/assets/home-page/homepage.png differ diff --git a/doc/docs/en/user-guide/assets/knowledge-base/tip.png b/doc/docs/en/user-guide/assets/knowledge-base/tip.png new file mode 100644 index 000000000..6e23f6102 Binary files /dev/null and b/doc/docs/en/user-guide/assets/knowledge-base/tip.png differ diff --git a/doc/docs/en/user-guide/assets/model-management/vector-model.png b/doc/docs/en/user-guide/assets/model-management/vector-model.png new file mode 100644 index 000000000..c41097279 Binary files /dev/null and b/doc/docs/en/user-guide/assets/model-management/vector-model.png differ diff --git a/doc/docs/en/user-guide/home-page.md b/doc/docs/en/user-guide/home-page.md index d4fdde989..61d457b18 100644 --- a/doc/docs/en/user-guide/home-page.md +++ b/doc/docs/en/user-guide/home-page.md @@ -26,12 +26,14 @@ The left sidebar exposes every major module: - **Start Chat** – Open the chat interface. - **Quick Setup** – Complete the recommended setup flow (Models → Knowledge Base → Agent). - **Agent Space** – Manage all existing agents. -- **Agent Market** – Discover and install published agents (coming soon). +- **Agent Market** – Discover and install published agents. - **Agent Development** – Create and configure agents. -- **Knowledge Base** – Upload and curate documents. -- **Model Management** – Configure app info and connect models. -- **Memory Management** – Enable and tune the multi-layer memory system. -- **User Management** – Manage platform users (coming soon). +- **Knowledge Base** – Upload documents and materials to help agents understand your exclusive knowledge. +- **MCP Tools** – Connect servers, sync tools, and view status at a glance (coming soon). +- **Monitoring & Operations** – Monitor agent runtime status in real time (coming soon). +- **Model Management** – Manage app information and model configuration, connect the AI capabilities you need. +- **Memory Management** – Control agents' long-term memory for more efficient conversations. +- **User Management** – Provide unified user, role, and permission control for teams (coming soon). Use the language switcher in the top-right corner to toggle between Simplified Chinese and English. The lower-left corner shows the running Nexent version to simplify troubleshooting when asking for help. diff --git a/doc/docs/en/user-guide/knowledge-base.md b/doc/docs/en/user-guide/knowledge-base.md index fdce554ac..5885f2b03 100644 --- a/doc/docs/en/user-guide/knowledge-base.md +++ b/doc/docs/en/user-guide/knowledge-base.md @@ -19,6 +19,10 @@ Create and manage knowledge bases, upload documents, and generate summaries. Kno ![File Upload](./assets/knowledge-base/create-knowledge-base.png) +💡 Hover over the status to understand the progress and error reasons + +![File Upload](./assets/knowledge-base/tip.png) + ### Supported File Formats Nexent supports multiple file formats, including: diff --git a/doc/docs/en/user-guide/mcp-tools.md b/doc/docs/en/user-guide/mcp-tools.md new file mode 100644 index 000000000..b55859cbe --- /dev/null +++ b/doc/docs/en/user-guide/mcp-tools.md @@ -0,0 +1,28 @@ +# MCP Tools + +The upcoming MCP Tools management module will let you centrally manage MCP servers and tools on a single page, easily completing connection configuration, tool synchronization, and health status monitoring. + +## 🎯 Feature Preview + +1. Register and manage multiple MCP servers +2. Quickly sync, view, and organize MCP tool lists +3. Monitor MCP connection status and usage in real time + +## ⏳ Stay Tuned + +The MCP Tools management feature is under development. We are committed to building an efficient and intuitive management platform that enables you to: + +1. Centrally manage all MCP servers +2. Conveniently sync and organize tools +3. Monitor server connections and tool runtime status in real time + +## 🚀 Related Features + +While waiting for **MCP Tools** to launch, you can: + +1. Manage your MCP tools in **[Agent Development](./agent-development)** +2. View agent and MCP collaboration relationships through **[Agent Space](./agent-space)** +3. Experience platform features in **[Start Chat](./start-chat)** + +If you encounter any issues during use, please refer to our **[FAQ](../quick-start/faq)** or ask for support in [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions). + diff --git a/doc/docs/en/user-guide/memory-management.md b/doc/docs/en/user-guide/memory-management.md index 6e1330b78..0caffb7e1 100644 --- a/doc/docs/en/user-guide/memory-management.md +++ b/doc/docs/en/user-guide/memory-management.md @@ -84,9 +84,9 @@ When an agent retrieves memory it follows this order (high ➝ low): The system takes care of most work for you: -- **Smart extraction:** Detects important facts in conversations and stores them automatically. -- **Context injection:** Retrieves the most relevant memories and adds them to prompts silently. -- **Incremental updates:** Refreshes or removes outdated memories so the store stays clean. +- **Smart extraction:** Detects key facts in conversations, creates memory entries automatically, and stores them at the right level—no manual input needed. +- **Automatic context embedding:** Retrieves the most relevant memories and implicitly injects them into the conversation context so agents respond with better accuracy. +- **Incremental updates:** Gradually refreshes or removes outdated memories to keep the store clean, timely, and reliable. ## ✋ Manual Memory Operations diff --git a/doc/docs/en/user-guide/model-management.md b/doc/docs/en/user-guide/model-management.md index db38ea46d..b6490c824 100644 --- a/doc/docs/en/user-guide/model-management.md +++ b/doc/docs/en/user-guide/model-management.md @@ -123,8 +123,16 @@ After adding models, assign the platform-level defaults. These models handle sys #### Embedding Model -- Powers semantic search for text, images, and other knowledge-base content. -- Select one of the added embedding models. +Embedding models are primarily used for vectorization processing of text, images, and other data in knowledge bases, forming the foundation for efficient retrieval and semantic understanding. Configuring an appropriate embedding model can significantly improve knowledge base search accuracy and multimodal data processing capabilities. + +- Click the embedding model dropdown to select one from the added embedding models. +- Embedding model configuration affects the stable operation of knowledge bases. + +Choose appropriate document chunk size and chunks per request based on model capabilities. Smaller chunks provide more stability, but may affect file parsing quality. + +
+ +
#### Vision-Language Model diff --git a/doc/docs/en/user-guide/monitor.md b/doc/docs/en/user-guide/monitor.md new file mode 100644 index 000000000..26752c591 --- /dev/null +++ b/doc/docs/en/user-guide/monitor.md @@ -0,0 +1,20 @@ +# Monitoring & Operations + +The upcoming Monitoring & Operations Center will provide a unified management platform for agents, allowing you to track health status, performance metrics, and exception events in real time. + +## 🎯 Feature Preview + +1. Monitor agent health status, latency, and error rates in real time +2. View and filter runtime logs and historical tasks +3. Configure alert policies and operational actions for key events + +## ⏳ Stay Tuned + +The Monitoring & Operations Center is under development. We are committed to building an intuitive and efficient management platform that helps you: + +1. Fully understand agent runtime status +2. Quickly discover and handle exceptions +3. Flexibly configure alerts and operational actions + +If you encounter any issues during use, please refer to our **[FAQ](../quick-start/faq)** or ask for support in [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions). + diff --git a/doc/docs/zh/opensource-memorial-wall.md b/doc/docs/zh/opensource-memorial-wall.md index 7c4a7cc91..9c53ae6ce 100644 --- a/doc/docs/zh/opensource-memorial-wall.md +++ b/doc/docs/zh/opensource-memorial-wall.md @@ -623,3 +623,11 @@ Nexent开发者加油 ::: tip 开源新手 - 2025-12-05 感谢 Nexent 让我踏上了开源之旅! ::: + +::: tip user - 2025-12-10 +很开心能接触到这个平台,让我有机会踏上开源之旅 +::: + +::: tip 开源新手 - 2025-12-14 +开放原子大赛接触到了Nexent平台,祝越来越好! +::: diff --git a/doc/docs/zh/user-guide/agent-development.md b/doc/docs/zh/user-guide/agent-development.md index 7d4a28581..ebbcccbc9 100644 --- a/doc/docs/zh/user-guide/agent-development.md +++ b/doc/docs/zh/user-guide/agent-development.md @@ -19,6 +19,8 @@ > - **直接导入**:保留重复名称,导入后的智能体会处于不可用状态,需手动修改 Agent 名称和变量名后才能使用 > - **重新生成并导入**:系统将调用 LLM 对 Agent 进行重命名,会消耗一定的模型 token 数,可能耗时较长 +> 📌 **重要说明**:通过导入创建的智能体,如果其工具中包含 `knowledge_base_search` 等知识库检索工具,这些工具只会检索**当前登录用户在本环境中有权限访问的知识库**。导入文件中原有的知识库配置不会自动继承,因此实际检索结果和回答效果,可能与智能体原作者环境下的表现存在差异。 +
@@ -65,7 +67,7 @@ Nexent 支持您快速便捷地使用第三方 MCP 工具,丰富 Agent 能力。 1. 在"选择 Agent 的工具"页签右侧,点击"MCP 配置",可在弹窗中进行 MCP 服务器的配置,查看已配置的 MCP 服务器 -2. 输入服务器名称和服务器 URL(目前仅支持 SSE 协议) +2. 输入服务器名称和服务器 URL(支持 SSE 与 Streamble HTTP 协议) - ⚠️ **注意**:服务器名称只能包含英文字母和数字,不能包含空格、下划线等其他字符 3. 点击"添加"按钮,即可完成添加 @@ -111,7 +113,7 @@ Nexent 支持您快速便捷地使用第三方 MCP 工具,丰富 Agent 能力 根据选择的协作 Agent 和工具,您现在可以用简洁的语言来描述,您希望这个 Agent 应该如何工作。Nexent 会根据您的配置和描述,自动为您生成 Agent 名称、描述以及提示词等信息。 1. 在"描述 Agent 应该如何工作"下的编辑框中,输入简洁描述,如"你是一个专业的知识问答小助手,具备本地知识检索和联网检索能力,综合信息以回答用户问题" -2. 点击"生成智能体"按钮,Nexent 会为您生成 Agent 详细内容,包括基础信息以及提示词(角色、使用要求、示例) +2. 选择模型(生成提示词时选择更聪明的模型以优化回复逻辑),点击"生成智能体"按钮,Nexent 会为您生成 Agent 详细内容,包括基础信息以及提示词(角色、使用要求、示例) 3. 您可在下方 Agent 详细内容中,针对自动生成的内容(特别是提示词)进行编辑微调
@@ -128,7 +130,7 @@ Nexent 支持您快速便捷地使用第三方 MCP 工具,丰富 Agent 能力 调试成功后,可点击右下角"保存"按钮,此智能体将会被保存并出现在智能体列表中。 -## 📋 管理智能体 +## 🔧 管理智能体 在左侧智能体列表中,您可对已有的智能体进行以下操作: @@ -144,6 +146,11 @@ Nexent 支持您快速便捷地使用第三方 MCP 工具,丰富 Agent 能力 可将调试成功的智能体导出为 JSON 配置文件,在创建 Agent 时可以使用此 JSON 文件以导入的方式创建副本。 + +### 📋 复制 + +复制 Agent,便于智能体的实验、多版本调试与并行开发。 + ### 🗑️ 删除 删除智能体(不可撤销,请谨慎操作)。 @@ -156,4 +163,4 @@ Nexent 支持您快速便捷地使用第三方 MCP 工具,丰富 Agent 能力 2. 在 **[开始问答](./start-chat)** 中与智能体进行交互 3. 在 **[记忆管理](./memory-management)** 配置记忆以提升智能体的个性化能力 -如果您在智能体开发过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在 [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions) 中进行提问获取支持。 +如果您在使用程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在 [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions) 中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/agent-market.md b/doc/docs/zh/user-guide/agent-market.md index 65beef4db..020bbfa1a 100644 --- a/doc/docs/zh/user-guide/agent-market.md +++ b/doc/docs/zh/user-guide/agent-market.md @@ -1,32 +1,56 @@ # 智能体市场 -智能体市场是Nexent平台即将推出的功能模块,将为您提供丰富的智能体资源。 +🎁 这里汇集了由 **Nexent 官方**与**社区创作者**打造的高质量智能体 -## 🎯 功能预告 +您可以直接使用它们完成具体任务,或将其作为子智能体,组合进自己的智能体中 -智能体市场将提供以下功能: +![智能体市场](./assets/agent-market/agent-market.png) -- **浏览智能体**:浏览平台提供的各类智能体 -- **获取智能体**:一键获取并使用优质智能体 -- **分享智能体**:分享您开发的智能体到市场 -- **评价和反馈**:对智能体进行评价和反馈 +## 🔍 探索与发现 -## ⏳ 敬请期待 +您可以通过以下方式快速找到最优的智能体: -智能体市场功能正在开发中,敬请期待! +1. 按使用场景分类浏览或搜索 +2. 查看智能体的功能简介,确认是否符合您的需求 🆗 +3. 查看内置工具,确认是否已就绪或可获取 ✅ -我们正在努力为您打造一个丰富、便捷的智能体生态,让您能够: +
-- 快速获取经过验证的优质智能体 -- 分享您的创作,与社区共同成长 -- 发现更多创新应用场景 -## 📢 获取最新动态 + 选择智能体 -想要第一时间了解智能体市场的上线信息? + 对话框 +
-- 加入我们的 [Discord 社区](https://discord.gg/tb5H3S3wyv) 获取最新动态 -- 关注项目更新,了解开发进展 +## 🔧 安装智能体 + +选择心仪的智能体,一键下载,即刻加入您的智能体空间 + +### 1️⃣ 选择模型 + +🌟 确认模型可用 + +✍️ 为智能体统一配置同一个模型,或为主智能体和子智能体分别选配合适的模型 + +![智能体市场下载](./assets/agent-market/agent-market-download.png) + +### 2️⃣ 配置字段 + +🔑 依据提示补充工具许可 + +![智能体市场下载2](./assets/agent-market/agent-market-download2.png) + +安装完成后,您的智能体会在 **[智能体空间](./agent-space)** 准备好 + +## 📢 分享您的创作 + +创作了优秀的智能体? 👍 + +欢迎在 [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions) 中分享您的作品,我们会尽快与您取得联系,让更多人看到并使用它! ## 🚀 相关功能 @@ -36,4 +60,4 @@ 2. 通过 **[智能体开发](./agent-development)** 创建专属智能体 3. 在 **[开始问答](./start-chat)** 中体验智能体的强大功能 -如果您使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在 [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions) 中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/agent-space.md b/doc/docs/zh/user-guide/agent-space.md index fcc639a21..ff9cc7219 100644 --- a/doc/docs/zh/user-guide/agent-space.md +++ b/doc/docs/zh/user-guide/agent-space.md @@ -9,6 +9,7 @@ - **智能体图标**:智能体的标识图标 - **智能体名称**:智能体的显示名称 +- **智能体作者**:智能体的作者 - **智能体描述**:智能体的功能描述 - **智能体状态**:智能体是否可用的状态 - **操作按钮**:快速操作入口 @@ -66,4 +67,4 @@ 2. 继续 **[智能体开发](./agent-development)** 创建更多智能体 3. 配置 **[记忆管理](./memory-management)** 以提升智能体的记忆能力 -如果您在配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png b/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png index ead3ed1dc..0dd5eef50 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png and b/doc/docs/zh/user-guide/assets/agent-development/generate-agent.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market-detail.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market-detail.png new file mode 100644 index 000000000..c5d45beed Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-market/agent-market-detail.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market-detail2.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market-detail2.png new file mode 100644 index 000000000..1140c66d9 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-market/agent-market-detail2.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market-download.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download.png new file mode 100644 index 000000000..d874638f4 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market-download2.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download2.png new file mode 100644 index 000000000..d9f88e409 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-market/agent-market-download2.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-market/agent-market.png b/doc/docs/zh/user-guide/assets/agent-market/agent-market.png new file mode 100644 index 000000000..9b4c0811b Binary files /dev/null and b/doc/docs/zh/user-guide/assets/agent-market/agent-market.png differ diff --git a/doc/docs/zh/user-guide/assets/agent-space/agent-space.png b/doc/docs/zh/user-guide/assets/agent-space/agent-space.png index 1937b26e4..4576a5767 100644 Binary files a/doc/docs/zh/user-guide/assets/agent-space/agent-space.png and b/doc/docs/zh/user-guide/assets/agent-space/agent-space.png differ diff --git a/doc/docs/zh/user-guide/assets/home-page/homepage.png b/doc/docs/zh/user-guide/assets/home-page/homepage.png index 7de1ffc07..845b31a57 100644 Binary files a/doc/docs/zh/user-guide/assets/home-page/homepage.png and b/doc/docs/zh/user-guide/assets/home-page/homepage.png differ diff --git a/doc/docs/zh/user-guide/assets/knowledge-base/tip.png b/doc/docs/zh/user-guide/assets/knowledge-base/tip.png new file mode 100644 index 000000000..e93adf32e Binary files /dev/null and b/doc/docs/zh/user-guide/assets/knowledge-base/tip.png differ diff --git a/doc/docs/zh/user-guide/assets/model-management/vector-model.png b/doc/docs/zh/user-guide/assets/model-management/vector-model.png new file mode 100644 index 000000000..d5b1937d7 Binary files /dev/null and b/doc/docs/zh/user-guide/assets/model-management/vector-model.png differ diff --git a/doc/docs/zh/user-guide/home-page.md b/doc/docs/zh/user-guide/home-page.md index 067d9e09c..5e24343ac 100644 --- a/doc/docs/zh/user-guide/home-page.md +++ b/doc/docs/zh/user-guide/home-page.md @@ -1,10 +1,12 @@ -# 用户指南 +# 👏 欢迎来到 Nexent -Nexent是一款面向未来的零代码智能体开发平台,致力于让每个人都能轻松构建和部署专属的AI智能体,无需编程基础,也无需繁琐操作! +**Nexent** 是一款面向未来的零代码智能体开发平台 -本用户指南将带您全面了解Nexent的强大功能和使用方法,助您快速上手各类操作~ +致力于让每个人都能轻松✏️构建和部署专属的AI智能体 -通过学习本指南,您将能够高效利用Nexent,把创意变为现实,让智能体为您的工作和生活带来真正的价值与惊喜! +无需编程,无复杂操作,点击几下,就能让 AI 为你工作💪 + +👉 本用户指南将带您快速上手 Nexent,让智能体为您的工作和生活带来新的惊喜与价值🎉 ## 🏠 首页概览 @@ -12,40 +14,51 @@ Nexent首页展示了平台的核心功能,为您提供快速入口: ![首页概览](./assets/home-page/homepage.png) -### 主要功能按钮 +### 🔘 功能按钮 -1. **开始问答**:点击后直接进入对话页面,与智能体进行交互 -2. **快速配置**:点击后进入快速配置流程,按照顺序完成模型管理、知识库和智能体开发配置 -3. **智能体空间**:点击后进入智能体空间,查看和管理所有已开发的智能体 +1. **开始问答**:进入对话页面,选择智能体进行交互 +2. **快速配置**:按顺序完成模型管理、知识库和智能体开发配置 +3. **智能体空间**:查看和管理所有已开发的智能体 -### 左侧导航栏 +### ➡️ 左侧导航栏 页面左侧提供了完整的导航栏,包含以下模块: - **首页**:返回平台首页 -- **开始问答**:进入对话页面,与智能体进行交互 -- **快速配置**:快速完成模型->知识库->智能体配置 -- **智能体空间**:管理所有已开发的智能体 -- **智能体市场**:浏览和获取智能体(敬请期待) +- **开始问答**:进入对话页面,选择智能体进行交互 +- **快速配置**:按步骤完成模型 -> 知识库 -> 智能体配置,几分钟即可开始 +- **智能体空间**:集中查看和管理您开发的所有智能体 +- **智能体市场**:探索并获取现有的智能体 - **智能体开发**:创建和配置智能体 -- **知识库**:创建和管理知识库 -- **模型管理**:配置应用信息和模型 -- **记忆管理**:配置和管理智能记忆系统 -- **用户管理**:管理系统用户(敬请期待) +- **知识库**:上传文档和资料,让智能体理解你的专属知识 +- **MCP 工具**:连接服务器、同步工具、查看状态,一目了然(即将上线) +- **监控与运维**:实时掌控智能体的运行状态(即将上线) +- **模型管理**:管理应用信息与模型配置,连接你需要的 AI 能力 +- **记忆管理**:控制智能体的长期记忆,让对话更高效 +- **用户管理**:管为团队提供统一的用户、角色与权限控制(即将上线) + +页面右上角支持**语言切换**(简体中文/English) -此外,页面右上角还支持切换语言(简体中文或English),您可以根据需要自由切换语言。页面左下角展示了当前Nexent的版本号,有助于帮助您在遇到问题时寻求帮助。 +页面左下角展示了当前 Nexent 版本号,有助于您寻求帮助或报告问题。 ## 🚀 快速开始 -建议您按照以下顺序进行配置: +建议按照以下顺序完成配置,也可以直接点击“快速配置”按钮: + +1️⃣ **[模型管理](./model-management)**,配置应用信息并接入模型 + +2️⃣ **[知识库](./knowledge-base)**,上传您的文档和资料 + +3️⃣ **[智能体开发](./agent-development)**,创建您的专属智能体 + +4️⃣ **[开始问答](./start-chat)** 立即与智能体互动,体验成果 + -1. 首先进行 **[模型管理](./model-management)**,配置应用信息和接入模型 -2. 然后进行 **[知识库](./knowledge-base)**,上传您的文档和资料 -3. 接着进行 **[智能体开发](./agent-development)**,创建您的专属智能体 -4. 一切就绪后,就可以在 **[开始问答](./start-chat)** 中与智能体进行交互了 +## 🙋 获取帮助 -或者,您也可以直接点击首页或导航栏中的"快速配置"按钮,按照引导完成配置。 +遇到问题时,您可以: -## 💡 获取帮助 +- 查看 **[常见问题](../quick-start/faq)** +- 在 [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions) 中提问 -如果您在配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 \ No newline at end of file +💡 保持您的 Nexent 处于最新版本,我们会修复已知问题 \ No newline at end of file diff --git a/doc/docs/zh/user-guide/knowledge-base.md b/doc/docs/zh/user-guide/knowledge-base.md index 70d9cf513..c28f878e1 100644 --- a/doc/docs/zh/user-guide/knowledge-base.md +++ b/doc/docs/zh/user-guide/knowledge-base.md @@ -6,7 +6,6 @@ 1. 点击"创建知识库"按钮 2. 为知识库设置一个易于识别的名称 - > **注意**:知识库名称不能重复,只能使用中文或者小写字母,且不能包含空格、斜线等特殊字符 ## 📁 上传文件 @@ -19,6 +18,10 @@ ![文件上传](./assets/knowledge-base/create-knowledge-base.png) +💡 光标移动至状态,以了解进度及报错原因 + +![文件上传](./assets/knowledge-base/tip.png) + ### 支持的文件格式 Nexent支持多种文件格式,包括: @@ -76,4 +79,4 @@ Nexent支持多种文件格式,包括: 1. **[智能体开发](./agent-development)** - 创建和配置智能体 2. **[开始问答](./start-chat)** - 与智能体进行交互 -如果您在知识库配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 \ No newline at end of file +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 \ No newline at end of file diff --git a/doc/docs/zh/user-guide/mcp-tools.md b/doc/docs/zh/user-guide/mcp-tools.md new file mode 100755 index 000000000..912306284 --- /dev/null +++ b/doc/docs/zh/user-guide/mcp-tools.md @@ -0,0 +1,27 @@ +# MCP 工具 + +即将推出的 MCP 工具管理模块将让您在一个页面集中管理 MCP 服务器与工具,轻松完成连接配置、工具同步和健康状态监控 + +## 🎯 功能预览 + +1. 注册并管理多个 MCP 服务器 +2. 快速同步、查看并整理 MCP 工具列表 +3. 实时监控 MCP 连接状态和使用情况 + +## ⏳ 敬请期待 + +MCP 工具管理功能正在开发中,我们致力于打造一个高效、直观的管理平台,让您能够: + +1. 集中管理所有 MCP 服务器 +2. 便捷同步和组织工具 +3. 实时掌握服务器连接与工具运行状态 + +## 🚀 相关功能 + +在等待 **MCP 工具** 上线期间,您可以: + +1. 在 **[智能体开发](./agent-development)** 中管理您的 MCP 工具 +2. 通过 **[智能体空间](./agent-market)** 查看智能体与 MCP 的协作关系 +3. 在 **[开始问答](./start-chat)** 中体验平台功能 + +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 \ No newline at end of file diff --git a/doc/docs/zh/user-guide/memory-management.md b/doc/docs/zh/user-guide/memory-management.md index 5f745ad1f..b8b9915e2 100644 --- a/doc/docs/zh/user-guide/memory-management.md +++ b/doc/docs/zh/user-guide/memory-management.md @@ -157,4 +157,4 @@ Nexent采用四层记忆存储架构,不同层级的记忆有不同的作用 2. 在 **[智能体空间](./agent-space)** 中管理您的智能体 3. 继续 **[智能体开发](./agent-development)** 创建更多智能体 -如果您在配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/model-management.md b/doc/docs/zh/user-guide/model-management.md index c47e4ec89..3e23342ca 100644 --- a/doc/docs/zh/user-guide/model-management.md +++ b/doc/docs/zh/user-guide/model-management.md @@ -135,11 +135,20 @@ Nexent即将支持与ModelEngine平台的无缝对接,届时可自动同步并 - 点击基础模型下拉框,从已添加的大语言模型中选择一个作为系统基础模型。 -#### 向量化模型 +#### 向量模型 + +向量模型主要用于知识库的文本、图片等数据的向量化处理,是实现高效检索和语义理解的基础。配置合适的向量模型,可以显著提升知识库的搜索准确率和多模态数据的处理能力。 + +- 点击向量模型下拉框,从已添加的向量模型中选择一个。 +- 向量模型配置会影响知识库的稳定运行 + +根据模型能力选择合适的文档切片大小和单次请求切片量。切片越小,系统越稳定,但文件解析质量也会受到影响。 + +
+ +
-向量化模型主要用于知识库的文本、图片等数据的向量化处理,是实现高效检索和语义理解的基础。配置合适的向量化模型,可以显著提升知识库的搜索准确率和多模态数据的处理能力。 -- 点击向量模型下拉框,从已添加的向量化模型中选择一个。 #### 多模态模型 @@ -235,4 +244,4 @@ Nexent 支持任何 **遵循OpenAI API规范** 的大语言模型供应商,包 1. **[知识库](./knowledge-base)** - 创建和管理知识库。 2. **[智能体开发](./agent-development)** - 创建和配置智能体。 -如果您在模型配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/monitor.md b/doc/docs/zh/user-guide/monitor.md new file mode 100755 index 000000000..a6cf114e5 --- /dev/null +++ b/doc/docs/zh/user-guide/monitor.md @@ -0,0 +1,20 @@ +# 监控与运维 + +即将推出的监控与运维中心将为智能体提供统一的管理平台,让您实时跟踪健康状态、性能指标与异常事件 + +## 🎯 功能预览 + +1. 实时监控智能体健康状态、延迟与错误率 +2. 查看并筛选运行日志和历史任务 +3. 配置告警策略与关键事件的运维操作 + +## ⏳ 敬请期待 + +监控与运维中心正在开发中,我们致力于打造一个直观、高效的管理平台,帮助您: + +1. 全面掌握智能体运行状况 +2. 快速发现并处理异常 +3. 灵活配置告警与运维操作 + + +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/quick-setup.md b/doc/docs/zh/user-guide/quick-setup.md index 191835746..44d00b335 100644 --- a/doc/docs/zh/user-guide/quick-setup.md +++ b/doc/docs/zh/user-guide/quick-setup.md @@ -39,7 +39,7 @@ 1. **按顺序配置**:建议按照快速配置的顺序进行,确保每个步骤都正确完成 2. **保存配置**:每个步骤完成后记得保存配置 -3. **启用知识库**:若要启用本地知识检索能力,需在智能体配置中选中knowledge_base_serch工具 +3. **启用知识库**:若要启用本地知识检索能力,需在智能体配置中选中knowledge_base_search工具 4. **测试验证**:配置完成后,建议在对话页面测试智能体的功能 ## 🚀 下一步 @@ -50,4 +50,4 @@ 2. 在 **[开始问答](./start-chat)** 中与智能体进行交互 3. 配置 **[记忆管理](./memory-management)** 以提升智能体的记忆能力 -如果您在配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 6b10a5b73..e6ee73d8c 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,6 +1,7 @@ { - "extends": [ - "next/core-web-vitals", - "next/typescript" - ] + "extends": ["next/core-web-vitals", "next/typescript", "prettier"], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "error" + } } diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 000000000..75902765a --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ + +# Build outputs +.next/ +out/ +dist/ +build/ + +# Environment files +.env* + +# Logs +*.log + +# Package manager files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Generated files +*.d.ts +!src/**/*.d.ts + +# Public assets (usually don't need formatting) +public/ + +# Config files that should maintain specific formatting +tailwind.config.* +next.config.* +postcss.config.* \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 000000000..66853947d --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,16 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "auto", + "jsxSingleQuote": false, + "htmlWhitespaceSensitivity": "css", + "proseWrap": "preserve", + "quoteProps": "as-needed" +} diff --git a/frontend/app/[locale]/agents/AgentsContent.tsx b/frontend/app/[locale]/agents/AgentsContent.tsx index 5436e3aba..84f29ac28 100644 --- a/frontend/app/[locale]/agents/AgentsContent.tsx +++ b/frontend/app/[locale]/agents/AgentsContent.tsx @@ -9,7 +9,6 @@ import { } from "@/const/modelConfig"; import AgentConfig, {AgentConfigHandle} from "./AgentConfiguration"; -import SaveConfirmModal from "./components/SaveConfirmModal"; interface AgentsContentProps { /** Whether currently saving */ @@ -39,8 +38,6 @@ export default forwardRef(function Agents onSavingStateChange, }: AgentsContentProps, ref) { const agentConfigRef = useRef(null); - const [showSaveConfirm, setShowSaveConfirm] = useState(false); - const pendingNavRef = useRef void)>(null); // Use custom hook for common setup flow logic const { @@ -95,32 +92,6 @@ export default forwardRef(function Agents ) : null}
- - { - // Reload data from backend to discard changes - await agentConfigRef.current?.reloadCurrentAgentData?.(); - setShowSaveConfirm(false); - const go = pendingNavRef.current; - pendingNavRef.current = null; - if (go) go(); - }} - onSave={async () => { - try { - setInternalIsSaving(true); - await agentConfigRef.current?.saveAllChanges?.(); - setShowSaveConfirm(false); - const go = pendingNavRef.current; - pendingNavRef.current = null; - if (go) go(); - } catch (e) { - // errors are surfaced by underlying save - } finally { - setInternalIsSaving(false); - } - }} - /> ); }); diff --git a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx index 225b20980..e5718c98b 100644 --- a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx +++ b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx @@ -116,13 +116,9 @@ export default function AgentSetupOrchestrator({ const [importWizardData, setImportWizardData] = useState(null); // Use generation state passed from parent component, not local state - // Delete confirmation popup status - const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false); - const [agentToDelete, setAgentToDelete] = useState(null); - // Embedding auto-unselect notice modal - const [isEmbeddingAutoUnsetOpen, setIsEmbeddingAutoUnsetOpen] = - useState(false); + + const lastProcessedAgentIdForEmbedding = useRef(null); // Flag to track if we need to refresh enabledToolIds after tools update @@ -674,7 +670,12 @@ export default function AgentSetupOrchestrator({ } catch (error) { // Even if API fails, still inform user and prevent usage in UI } finally { - setIsEmbeddingAutoUnsetOpen(true); + confirm({ + title: t("embedding.agentToolAutoDeselectModal.title"), + content: t("embedding.agentToolAutoDeselectModal.content"), + okText: t("common.confirm"), + onOk: () => {}, + }); lastProcessedAgentIdForEmbedding.current = currentAgentId; } }; @@ -1770,19 +1771,17 @@ export default function AgentSetupOrchestrator({ }, [pendingImportData, runAgentImport, t]); // Handle confirmed deletion - const handleConfirmDelete = async (t: TFunction) => { - if (!agentToDelete) return; - + const handleConfirmDelete = async (agent: Agent) => { try { - const result = await deleteAgent(Number(agentToDelete.id)); + const result = await deleteAgent(Number(agent.id)); if (result.success) { message.success( t("businessLogic.config.error.agentDeleteSuccess", { - name: agentToDelete.name, + name: agent.name, }) ); // If currently editing the deleted agent, reset to initial clean state and avoid confirm modal on next switch - const deletedId = Number(agentToDelete.id); + const deletedId = Number(agent.id); const currentEditingId = (isEditingAgent && editingAgent ? Number(editingAgent.id) : null) ?? null; @@ -1815,7 +1814,7 @@ export default function AgentSetupOrchestrator({ } else { // If deleting another agent that is in enabledAgentIds, remove it and update baseline // to avoid triggering false unsaved changes indicator - const deletedId = Number(agentToDelete.id); + const deletedId = Number(agent.id); if (enabledAgentIds.includes(deletedId)) { const updatedEnabledAgentIds = enabledAgentIds.filter( (id) => id !== deletedId @@ -1840,9 +1839,6 @@ export default function AgentSetupOrchestrator({ } catch (error) { log.error(t("agentConfig.agents.deleteFailed"), error); message.error(t("businessLogic.config.error.agentDeleteFailed")); - } finally { - setIsDeleteConfirmOpen(false); - setAgentToDelete(null); } }; @@ -2003,40 +1999,19 @@ export default function AgentSetupOrchestrator({ content: t("agentConfig.agents.copyConfirmContent", { name: agent?.display_name || agent?.name || "", }), - onConfirm: () => handleCopyAgentFromList(agent), + onOk: () => handleCopyAgentFromList(agent), }); }; // Handle delete agent from list const handleDeleteAgentFromList = (agent: Agent) => { - setAgentToDelete(agent); - setIsDeleteConfirmOpen(true); - }; - - // Handle exit edit mode - const handleExitEdit = () => { - setIsEditingAgent(false); - setEditingAgent(null); - // Use the parent's exit creation handler to properly clear cache - if (isCreatingNewAgent && onExitCreation) { - onExitCreation(); - } else { - setIsCreatingNewAgent(false); - } - setBusinessLogic(""); - setDutyContent(""); - setConstraintContent(""); - setFewShotsContent(""); - setAgentName?.(""); - setAgentDescription?.(""); - // Reset mainAgentId and enabledAgentIds - setMainAgentId(null); - setEnabledAgentIds([]); - // Reset selected tools - setSelectedTools([]); - setEnabledToolIds([]); - // Notify parent component about editing state change - onEditingStateChange?.(false, null); + confirm({ + title: t("businessLogic.config.modal.deleteTitle"), + content: t("businessLogic.config.modal.deleteContent", { + name: agent.name, + }), + onOk: () => handleConfirmDelete(agent), + }); }; // Refresh tool list @@ -2297,49 +2272,13 @@ export default function AgentSetupOrchestrator({ isCreatingNewAgent={isCreatingNewAgent} canSaveAgent={localCanSaveAgent} getButtonTitle={getLocalButtonTitle} - onDeleteAgent={onDeleteAgent || (() => {})} - onDeleteSuccess={handleExitEdit} editingAgent={editingAgentFromParent || editingAgent} onViewCallRelationship={handleViewCallRelationship} /> - {/* Delete confirmation popup */} - setIsDeleteConfirmOpen(false)} - centered - footer={ -
- -
- } - width={520} - > -
-
- -
-
- {t("businessLogic.config.modal.deleteContent", { - name: agentToDelete?.name, - })} -
-
-
-
-
+ {/* Save confirmation modal for unsaved changes (debug/navigation hooks) */} {/* Auto unselect knowledge_base_search notice when embedding not configured */} - setIsEmbeddingAutoUnsetOpen(false)} - centered - footer={ -
- -
- } - width={520} - > -
-
- -
-
- {t("embedding.agentToolAutoDeselectModal.content")} -
-
-
-
-
+ {/* Agent call relationship modal */} ([]); const [loading, setLoading] = useState(false); const [addingServer, setAddingServer] = useState(false); @@ -159,16 +161,12 @@ export default function McpConfigModal({ // Delete MCP server const handleDeleteServer = async (server: McpServer) => { - modal.confirm({ + confirm({ title: t("mcpConfig.delete.confirmTitle"), content: t("mcpConfig.delete.confirmContent", { name: server.service_name, }), okText: t("common.delete", "Delete"), - cancelText: t("common.cancel", "Cancel"), - okType: "danger", - cancelButtonProps: { disabled: updatingTools }, - okButtonProps: { disabled: updatingTools, loading: updatingTools }, onOk: async () => { try { const result = await deleteMcpServer( @@ -301,8 +299,9 @@ export default function McpConfigModal({ }} > {healthCheckLoading[key] ? ( - ) : null}
@@ -330,7 +329,7 @@ export default function McpConfigModal({ ) : ( @@ -652,7 +647,7 @@ export default function PromptManager({ style={{ border: "none" }} > {loadingModels ? ( - + ) : ( )} @@ -692,8 +687,6 @@ export default function PromptManager({ onExpandCard={handleExpandCard} isGeneratingAgent={isGeneratingAgent} onDebug={onDebug} - onDeleteAgent={onDeleteAgent} - onDeleteSuccess={onDeleteSuccess} onSaveAgent={onSaveAgent} isCreatingNewAgent={isCreatingNewAgent} editingAgent={editingAgent} diff --git a/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx b/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx index c8ac7513f..dfb6faeaf 100644 --- a/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx +++ b/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; import { Modal, Spin, message, Typography } from "antd"; -import { RobotOutlined, ToolOutlined } from "@ant-design/icons"; +import { Bot, Wrench } from "lucide-react"; import { useTranslation } from "react-i18next"; import Tree from "react-d3-tree"; @@ -71,7 +71,7 @@ const CustomNode = ({ nodeDatum }: any) => { nodeDatum.type === AGENT_CALL_RELATIONSHIP_NODE_TYPES.MAIN || nodeDatum.type === AGENT_CALL_RELATIONSHIP_NODE_TYPES.SUB; const color = getNodeColor(nodeDatum.type, nodeDatum.depth); - const icon = isAgent ? : ; + const icon = isAgent ? : ; // Truncate tool names by maximum character count (avoid too long) const rawName: string = nodeDatum.name || ""; diff --git a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx index a987dfaee..0ec3432a2 100644 --- a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx @@ -3,10 +3,7 @@ import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Button, Modal, Spin, Input, Select, InputNumber } from "antd"; -import { - LoadingOutlined, -} from "@ant-design/icons"; -import { Bug, Save, Maximize2 } from "lucide-react"; +import { Bug, Save, Maximize2, LoaderCircle } from "lucide-react"; import log from "@/lib/logger"; import { ModelOption } from "@/types/modelConfig"; @@ -48,8 +45,6 @@ export interface AgentConfigModalProps { isGeneratingAgent?: boolean; // Add new props for action buttons onDebug?: () => void; - onDeleteAgent?: () => void; - onDeleteSuccess?: () => void; // New prop for handling delete success onSaveAgent?: () => void; isCreatingNewAgent?: boolean; editingAgent?: Agent | null; @@ -84,8 +79,6 @@ export default function AgentConfigModal({ isGeneratingAgent = false, // Add new props for action buttons onDebug, - onDeleteAgent, - onDeleteSuccess, onSaveAgent, isCreatingNewAgent = false, editingAgent = null, @@ -107,9 +100,6 @@ export default function AgentConfigModal({ // Add segmented state management const [activeSegment, setActiveSegment] = useState("agent-info"); - // Add state for delete confirmation modal - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - // Add state for agent name validation error const [agentNameError, setAgentNameError] = useState(""); // Add state for agent name status check @@ -473,16 +463,6 @@ export default function AgentConfigModal({ } }, [isCreatingNewAgent, currentDisplayName, originalDisplayName]); - // Handle delete confirmation - const handleDeleteConfirm = useCallback(() => { - setIsDeleteModalVisible(false); - // Execute the delete operation - onDeleteAgent?.(); - // Call the success callback immediately after triggering delete - // The actual success/failure will be handled by the parent component - onDeleteSuccess?.(); - }, [onDeleteAgent, onDeleteSuccess]); - // Optimized click handlers using useCallback const handleSegmentClick = useCallback((segment: string) => { setActiveSegment(segment); @@ -860,7 +840,7 @@ export default function AgentConfigModal({ > {t("systemPrompt.card.duty.title")} {isGeneratingAgent && activeSegment === "duty" && ( - + )} @@ -1196,25 +1176,6 @@ export default function AgentConfigModal({ )} - - {/* Delete Confirmation Modal */} - setIsDeleteModalVisible(false)} - okText={t("businessLogic.config.modal.button.confirm")} - cancelText={t("businessLogic.config.modal.button.cancel")} - okButtonProps={{ - danger: true, - }} - > -

- {t("businessLogic.config.modal.deleteContent", { - name: agentName || "Unnamed Agent", - })} -

-
); } diff --git a/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx b/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx index 9659ee4a0..0e6ed669a 100644 --- a/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx +++ b/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Tag, App } from "antd"; -import { PlusOutlined, CloseOutlined } from "@ant-design/icons"; +import { Plus, X } from "lucide-react"; import { CollaborativeAgentDisplayProps } from "@/types/agentConfig"; @@ -174,7 +174,7 @@ export default function CollaborativeAgentDisplay({ }`} title={isEditingMode ? t("collaborativeAgent.button.add") : ""} > - + {/* Dropdown only renders in editing mode */} {isEditingMode && renderDropdown()} @@ -186,7 +186,7 @@ export default function CollaborativeAgentDisplay({ className="flex items-center h-8" closable={isEditingMode && !isGeneratingAgent} onClose={() => handleRemoveCollaborativeAgent(Number(agent.id))} - closeIcon={} + closeIcon={} style={{ maxWidth: "200px", }} diff --git a/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx b/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx index e2fc3ef60..8941cd6c2 100644 --- a/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx +++ b/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx @@ -4,8 +4,16 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Row, Col } from "antd"; -import { ExclamationCircleOutlined } from "@ant-design/icons"; -import { Copy, FileOutput, Network, FileInput, Trash2, Plus, X } from "lucide-react"; +import { + AlertCircle, + Copy, + FileOutput, + Network, + FileInput, + Trash2, + Plus, + X, +} from "lucide-react"; import { ScrollArea } from "@/components/ui/scrollArea"; import { @@ -333,7 +341,7 @@ export default function SubAgentPool({ >
{!isAvailable && ( - + )} {displayName && ( diff --git a/frontend/app/[locale]/agents/components/tool/ToolPool.tsx b/frontend/app/[locale]/agents/components/tool/ToolPool.tsx index 2460a8f07..c80b8969d 100644 --- a/frontend/app/[locale]/agents/components/tool/ToolPool.tsx +++ b/frontend/app/[locale]/agents/components/tool/ToolPool.tsx @@ -4,13 +4,13 @@ import { useState, useEffect, useMemo, useCallback, memo } from "react"; import { useTranslation } from "react-i18next"; import { Button, App, Tabs, Collapse, Tooltip } from "antd"; -import { - SettingOutlined, - LoadingOutlined, - ApiOutlined, - ReloadOutlined, - BulbOutlined, -} from "@ant-design/icons"; +import { + LoaderCircle, + Settings, + RefreshCw, + Lightbulb, + Plug, +} from "lucide-react"; import { TOOL_SOURCE_TYPES } from "@/const/agentConfig"; import log from "@/lib/logger"; @@ -524,7 +524,7 @@ function ToolPool({ }`} style={{ border: "none", padding: "4px" }} > - +
@@ -658,17 +658,23 @@ function ToolPool({ }, }} > - +
+ + )} + + ), + }); + }; + // Update editTitle when the title attribute changes useEffect(() => { setEditTitle(title); @@ -62,9 +84,8 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { const modelConfig = configStore.getModelConfig(); const configured = Boolean( modelConfig?.embedding?.modelName || - modelConfig?.multiEmbedding?.modelName + modelConfig?.multiEmbedding?.modelName ); - setEmbeddingConfigured(configured); if (!configured) { // If memory switch is on, turn it off automatically and notify the user @@ -77,7 +98,7 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { "Failed to auto turn off memory switch when embedding is not configured" ); } - setShowAutoOffPrompt(true); + showAutoOffConfirm(); } }) .catch((e) => { @@ -85,7 +106,6 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { }); } } catch (e) { - setEmbeddingConfigured(false); log.error("Failed to read model config for embedding check", e); } }, []); @@ -150,87 +170,7 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { - {/* Embedding not configured prompt */} - setShowConfigPrompt(false)} - centered - footer={ -
- {isAdmin && ( - - )} - -
- } - > -
-
- -
-
- {t("embedding.chatMemoryWarningModal.content")} -
- {!isAdmin && ( -
- {t("embedding.chatMemoryWarningModal.tip")} -
- )} -
-
-
-
- - {/* Auto-off memory prompt when embedding missing */} - setShowAutoOffPrompt(false)} - centered - footer={ -
- {isAdmin && ( - - )} - -
- } - > -
-
- -
-
- {t("embedding.chatMemoryAutoDeselectModal.content")} -
- {!isAdmin && ( -
- {t("embedding.chatMemoryAutoDeselectModal.tip")} -
- )} -
-
-
-
- setMemoryModalVisible(false)} - /> + ); } diff --git a/frontend/app/[locale]/chat/components/chatInput.tsx b/frontend/app/[locale]/chat/components/chatInput.tsx index 9bd17025d..ce4c16411 100644 --- a/frontend/app/[locale]/chat/components/chatInput.tsx +++ b/frontend/app/[locale]/chat/components/chatInput.tsx @@ -1,19 +1,18 @@ import { useState, useRef, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { Paperclip, Mic, MicOff, Square, X, AlertCircle } from "lucide-react"; +import { Paperclip, Mic, MicOff, Square, X, AlertCircle, Upload } from "lucide-react"; import { - AiFillFileImage, - AiFillFilePdf, - AiFillFileWord, - AiFillFileExcel, - AiFillFilePpt, - AiFillFileText, - AiFillFileMarkdown, - AiFillHtml5, - AiFillCode, - AiFillFileUnknown, - AiOutlineUpload, -} from "react-icons/ai"; + FileImageFilled, + FilePdfFilled, + FileWordFilled, + FileExcelFilled, + FilePptFilled, + FileTextFilled, + FileMarkdownFilled, + Html5Filled, + CodeFilled, + FileUnknownFilled, +} from "@ant-design/icons"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -245,48 +244,48 @@ const getFileIcon = (file: File) => { // Image file if (fileType.startsWith("image/")) { - return ; + return ; } // Check each file type category using config if (chatConfig.fileIcons.pdf.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.word.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.text.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.markdown.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.excel.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.powerpoint.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.html.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.code.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.json.includes(extension)) { - return ; + return ; } // Default file icon - return ; + return ; }; // File limit constants from config @@ -965,7 +964,7 @@ export function ChatInput({
- +

diff --git a/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx b/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx index aed0c180b..c9b2b3aaf 100644 --- a/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx +++ b/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx @@ -18,14 +18,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdownMenu"; import { Input } from "@/components/ui/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { App } from "antd"; import { Tooltip, TooltipContent, @@ -35,6 +28,7 @@ import { import { StaticScrollArea } from "@/components/ui/scrollArea"; import { USER_ROLES } from "@/const/modelConfig"; import { useTranslation } from "react-i18next"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; import { ConversationListItem, ChatSidebarProps } from "@/types/chat"; // conversation status indicator component @@ -68,7 +62,6 @@ const ConversationStatusIndicator = ({ return null; }; - // Helper function - dialog classification const categorizeDialogs = (dialogs: ConversationListItem[]) => { const now = new Date(); @@ -122,16 +115,13 @@ export function ChatSidebar({ userRole = USER_ROLES.USER, }: ChatSidebarProps) { const { t } = useTranslation(); + const { confirm } = useConfirmModal(); const router = useRouter(); const { today, week, older } = categorizeDialogs(conversationList); const [editingId, setEditingId] = useState(null); const [editingTitle, setEditingTitle] = useState(""); const inputRef = useRef(null); - // Add delete dialog status - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [dialogToDelete, setDialogToDelete] = useState(null); - const [animationComplete, setAnimationComplete] = useState(false); useEffect(() => { @@ -186,19 +176,17 @@ export function ChatSidebar({ // Handle delete click const handleDeleteClick = (dialogId: number) => { - setDialogToDelete(dialogId); - setIsDeleteDialogOpen(true); // Close dropdown menus onDropdownOpenChange(false, null); - }; - // Confirm delete - const confirmDelete = () => { - if (dialogToDelete !== null) { - onDelete(dialogToDelete); - setIsDeleteDialogOpen(false); - setDialogToDelete(null); - } + // Show confirmation modal + confirm({ + title: t("chatLeftSidebar.confirmDeletionTitle"), + content: t("chatLeftSidebar.confirmDeletionDescription"), + onOk: () => { + onDelete(dialogId); + }, + }); }; // Render dialog list items @@ -439,31 +427,6 @@ export function ChatSidebar({ renderCollapsedSidebar() )}

- - {/* Delete confirmation dialog */} - - - - - {t("chatLeftSidebar.confirmDeletionTitle")} - - - {t("chatLeftSidebar.confirmDeletionDescription")} - - - - - - - - ); } diff --git a/frontend/app/[locale]/chat/internal/chatAttachment.tsx b/frontend/app/[locale]/chat/internal/chatAttachment.tsx index c08ece8f7..4521f633f 100644 --- a/frontend/app/[locale]/chat/internal/chatAttachment.tsx +++ b/frontend/app/[locale]/chat/internal/chatAttachment.tsx @@ -1,30 +1,28 @@ import { chatConfig } from "@/const/chatConfig"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ExternalLink } from "lucide-react"; -import { storageService, convertImageUrlToApiUrl, extractObjectNameFromUrl } from "@/services/storageService"; -import { message } from "antd"; -import log from "@/lib/logger"; +import { Download } from "lucide-react"; import { - AiFillFileImage, - AiFillFilePdf, - AiFillFileWord, - AiFillFileExcel, - AiFillFilePpt, - AiFillFileZip, - AiFillFileText, - AiFillFileMarkdown, - AiFillHtml5, - AiFillCode, - AiFillFileUnknown, -} from "react-icons/ai"; - + FileImageFilled, + FilePdfFilled, + FileWordFilled, + FileExcelFilled, + FilePptFilled, + FileTextFilled, + Html5Filled, + CodeFilled, + FileUnknownFilled, + FileZipFilled, +} from "@ant-design/icons"; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; + storageService, + convertImageUrlToApiUrl, + extractObjectNameFromUrl, +} from "@/services/storageService"; + +import log from "@/lib/logger"; + +import { Modal, App } from "antd"; import { cn } from "@/lib/utils"; import { AttachmentItem, ChatAttachmentProps } from "@/types/chat"; @@ -40,23 +38,22 @@ const ImageViewer = ({ }) => { if (!isOpen) return null; const { t } = useTranslation("common"); - + // Convert image URL to backend API URL const imageUrl = convertImageUrlToApiUrl(url); return ( - - - - - {t("chatAttachment.imagePreview")} - - -
- Full size -
-
-
+ +
+ img +
+
); }; @@ -78,15 +75,15 @@ const FileViewer = ({ }) => { if (!isOpen) return null; const { t } = useTranslation("common"); + const { message } = App.useApp(); const [isDownloading, setIsDownloading] = useState(false); - // Handle file download const handleDownload = async (e: React.MouseEvent) => { // Prevent dialog from closing immediately e.preventDefault(); e.stopPropagation(); - + // Check if URL is a direct http/https URL that can be accessed directly // Exclude backend API endpoints (containing /api/file/download/) if ( @@ -104,16 +101,18 @@ const FileViewer = ({ setTimeout(() => { document.body.removeChild(link); }, 100); - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); setTimeout(() => { onClose(); }, 500); return; } - + // Try to get object_name from props or extract from URL let finalObjectName: string | undefined = objectName; - + if (!finalObjectName && url) { finalObjectName = extractObjectNameFromUrl(url) || undefined; } @@ -131,10 +130,17 @@ const FileViewer = ({ setTimeout(() => { document.body.removeChild(link); }, 100); - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); return; } else { - message.error(t("chatAttachment.downloadError", "File object name or URL is missing")); + message.error( + t( + "chatAttachment.downloadError", + "File object name or URL is missing" + ) + ); return; } } @@ -144,7 +150,9 @@ const FileViewer = ({ // Start download (non-blocking, browser handles it) await storageService.downloadFile(finalObjectName, name); // Show success message immediately after triggering download - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); // Keep dialog open for a moment to show the message, then close setTimeout(() => { setIsDownloading(false); @@ -165,56 +173,68 @@ const FileViewer = ({ setTimeout(() => { document.body.removeChild(link); }, 100); - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); setTimeout(() => { onClose(); }, 500); } catch (fallbackError) { message.error( - t("chatAttachment.downloadError", "Failed to download file. Please try again.") + t( + "chatAttachment.downloadError", + "Failed to download file. Please try again." + ) ); } } else { message.error( - t("chatAttachment.downloadError", "Failed to download file. Please try again.") + t( + "chatAttachment.downloadError", + "Failed to download file. Please try again." + ) ); } } }; return ( - - - - + + {getFileIcon(name, contentType)} + + {name} + + + } + > +
+
+
{getFileIcon(name, contentType)} - {name} - - - -
-
-
- {getFileIcon(name, contentType)} -
-

- {t("chatAttachment.previewNotSupported")} -

-
+

+ {t("chatAttachment.previewNotSupported")} +

+
- -
+ + ); }; @@ -231,56 +251,58 @@ const getFileIcon = (name: string, contentType?: string) => { const fileType = contentType || ""; const iconSize = 32; - // Image file + // Image file - using lucide-react if ( fileType.startsWith("image/") || ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].includes(extension) ) { - return ; + return ; } // Identify by extension name - // Document file + // Document file if (chatConfig.fileIcons.pdf.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.word.includes(extension)) { - return ; + return ( + + ); } if (chatConfig.fileIcons.text.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.markdown.includes(extension)) { - return ; + return ; } // Table file if (chatConfig.fileIcons.excel.includes(extension)) { - return ; + return ; } // Presentation file if (chatConfig.fileIcons.powerpoint.includes(extension)) { - return ; + return ; } // Code file if (chatConfig.fileIcons.html.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.code.includes(extension)) { - return ; + return ; } if (chatConfig.fileIcons.json.includes(extension)) { - return ; + return ; } // Compressed file if (chatConfig.fileIcons.compressed.includes(extension)) { - return ; + return ; } - // Default file icon - return ; - }; + // Default file icon + return ; +}; // Format file size const formatFileSize = (size: number): string => { @@ -307,12 +329,12 @@ export function ChatAttachment({ // Handle image click const handleImageClick = (url: string) => { + // Use internal preview + setSelectedImage(url); + + // Also call external callback if provided (for compatibility) if (onImageClick) { - // Call external callback onImageClick(url); - } else { - // Use internal preview when there is no external callback - setSelectedImage(url); } }; diff --git a/frontend/app/[locale]/chat/internal/chatInterface.tsx b/frontend/app/[locale]/chat/internal/chatInterface.tsx index f07fdb80c..b0412d550 100644 --- a/frontend/app/[locale]/chat/internal/chatInterface.tsx +++ b/frontend/app/[locale]/chat/internal/chatInterface.tsx @@ -1450,7 +1450,6 @@ export function ChatInterface() { onKeyDown={handleKeyDown} onSelectMessage={handleMessageSelect} selectedMessageId={selectedMessageId} - onImageClick={handleImageClick} attachments={attachments} onAttachmentsChange={handleAttachmentsChange} onFileUpload={handleFileUpload} @@ -1490,38 +1489,6 @@ export function ChatInterface() { - - {/* Image preview */} - {viewingImage && ( -
setViewingImage(null)} - > -
e.stopPropagation()} - > - {t("chatInterface.imagePreview")} { - handleImageError(viewingImage); - }} - /> - -
-
- )} ); } diff --git a/frontend/app/[locale]/chat/internal/memory/memoryDeleteModal.tsx b/frontend/app/[locale]/chat/internal/memory/memoryDeleteModal.tsx deleted file mode 100644 index 007e00270..000000000 --- a/frontend/app/[locale]/chat/internal/memory/memoryDeleteModal.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -import { useTranslation, Trans } from "react-i18next"; -import { Modal } from "antd"; -import { WarningFilled } from "@ant-design/icons"; -import { MemoryDeleteModalProps } from "@/types/memory"; - -/** - * Hosts the "Clear Memory" secondary confirmation popup window and is responsible only for UI display. - * The upper-level component is responsible for controlling visible and callback logic. - */ -const MemoryDeleteModal: React.FC = ({ - visible, - targetTitle, - onOk, - onCancel, -}) => { - const { t } = useTranslation(); - return ( - - {t("memoryDeleteModal.title")} - - } - onOk={onOk} - onCancel={onCancel} - okText={t("memoryDeleteModal.clear")} - cancelText={t("common.cancel")} - okButtonProps={{ danger: true }} - destroyOnClose - > -
- -
-

- }} - /> -

-

- {t("memoryDeleteModal.prompt")} -

-
-
-
- ); -}; - -export default MemoryDeleteModal; diff --git a/frontend/app/[locale]/chat/internal/memory/memoryManageModal.tsx b/frontend/app/[locale]/chat/internal/memory/memoryManageModal.tsx deleted file mode 100644 index 424bf75f7..000000000 --- a/frontend/app/[locale]/chat/internal/memory/memoryManageModal.tsx +++ /dev/null @@ -1,595 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { - Modal, - Tabs, - Collapse, - List, - Button, - Switch, - Pagination, - Dropdown, - Input, - App, -} from "antd"; -import { CaretRightOutlined, DownOutlined } from "@ant-design/icons"; -import { USER_ROLES, MEMORY_TAB_KEYS, MemoryTabKey } from "@/const/modelConfig"; -import { MEMORY_SHARE_STRATEGY, MemoryShareStrategy } from "@/const/memoryConfig"; -import { - MessageSquarePlus, - Eraser, - MessageSquareOff, - UsersRound, - UserRound, - Bot, - Share2, - Settings, - MessageSquareDashed, - Check, - X, -} from "lucide-react"; - -import { useAuth } from "@/hooks/useAuth"; -import { useMemory } from "@/hooks/useMemory"; - -import MemoryDeleteModal from "./memoryDeleteModal"; -import { MemoryManageModalProps, LabelWithIconFunction } from "@/types/memory"; - -/** - * Memory management popup, responsible only for UI rendering. - * Complex state logic is managed by hooks/useMemory.ts. - */ -const MemoryManageModal: React.FC = ({ - visible, - onClose, - userRole, -}) => { - // Get user role from authentication context - const { user, isSpeedMode } = useAuth(); - const { message } = App.useApp(); - const role: (typeof USER_ROLES)[keyof typeof USER_ROLES] = (userRole ?? - (isSpeedMode || user?.role === USER_ROLES.ADMIN ? USER_ROLES.ADMIN : USER_ROLES.USER)) as (typeof USER_ROLES)[keyof typeof USER_ROLES]; - - // Get user role from other hooks / context - const currentUserId = "user1"; - const currentTenantId = "tenant1"; - - const memory = useMemory({ - visible, - currentUserId, - currentTenantId, - message, - }); - const { t } = useTranslation("common"); - - // ====================== Clear memory confirmation popup ====================== - const [clearConfirmVisible, setClearConfirmVisible] = React.useState(false); - const [clearTarget, setClearTarget] = React.useState<{ - key: string; - title: string; - } | null>(null); - - const handleClearConfirm = (groupKey: string, groupTitle: string) => { - setClearTarget({ key: groupKey, title: groupTitle }); - setClearConfirmVisible(true); - }; - - const handleClearConfirmOk = async () => { - if (clearTarget) { - await memory.handleClearMemory(clearTarget.key, clearTarget.title); - setClearConfirmVisible(false); - setClearTarget(null); - } - }; - - const handleClearConfirmCancel = () => { - setClearConfirmVisible(false); - setClearTarget(null); - }; - - // ====================== UI rendering functions ====================== - const renderBaseSettings = () => { - const shareOptionLabels: Record = { - [MEMORY_SHARE_STRATEGY.ALWAYS]: t("memoryManageModal.shareOption.always"), - [MEMORY_SHARE_STRATEGY.ASK]: t("memoryManageModal.shareOption.ask"), - [MEMORY_SHARE_STRATEGY.NEVER]: t("memoryManageModal.shareOption.never"), - }; - const dropdownItems = [ - { label: shareOptionLabels[MEMORY_SHARE_STRATEGY.ALWAYS], key: MEMORY_SHARE_STRATEGY.ALWAYS }, - { label: shareOptionLabels[MEMORY_SHARE_STRATEGY.ASK], key: MEMORY_SHARE_STRATEGY.ASK }, - { label: shareOptionLabels[MEMORY_SHARE_STRATEGY.NEVER], key: MEMORY_SHARE_STRATEGY.NEVER }, - ]; - - const handleMenuClick = ({ key }: { key: string }) => { - memory.setShareOption(key as MemoryShareStrategy); - }; - - return ( -
-
- - {t("memoryManageModal.memoryAbility")} - -
- -
-
- - {memory.memoryEnabled && ( -
- - {t("memoryManageModal.agentMemoryShare")} - - - - {shareOptionLabels[memory.shareOption]} - - - -
- )} -
- ); - }; - - // Render add memory input box - const renderAddMemoryInput = (groupKey: string) => { - if (memory.addingMemoryKey !== groupKey) return null; - - return ( -
- memory.setNewMemoryContent(e.target.value)} - placeholder={t("memoryManageModal.inputPlaceholder")} - maxLength={500} - showCount - onPressEnter={memory.confirmAddingMemory} - disabled={memory.isAddingMemory} - className="flex-1" - autoSize={{ minRows: 2, maxRows: 5 }} - /> -
- ); - }; - - // Render empty state - const renderEmptyState = (groupKey: string) => { - const groups = memory.getGroupsForTab(memory.activeTabKey); - const currentGroup = groups.find((g) => g.key === groupKey); - - if (currentGroup && currentGroup.items.length === 0) { - return ( -
- -

{t("memoryManageModal.noMemory")}

-
- ); - } - return null; - }; - - const renderCollapseGroups = ( - groups: { title: string; key: string; items: any[] }[], - showSwitch = false, - tabKey?: MemoryTabKey - ) => { - const paginated = tabKey === MEMORY_TAB_KEYS.AGENT_SHARED || tabKey === MEMORY_TAB_KEYS.USER_AGENT; - const currentPage = paginated ? memory.pageMap[tabKey!] || 1 : 1; - const startIdx = (currentPage - 1) * memory.pageSize; - const sliceGroups = paginated - ? groups.slice(startIdx, startIdx + memory.pageSize) - : groups; - - // If no groups have been loaded (e.g. interface is still in request), directly render empty state to avoid white screen - if (sliceGroups.length === 0) { - return ( -
- -

{t("memoryManageModal.noMemory")}

-
- ); - } - - // Single group scenario, cannot be collapsed (tenant shared, user personal tab) - const isFixedSingle = - sliceGroups.length === 1 && - (tabKey === MEMORY_TAB_KEYS.TENANT || tabKey === MEMORY_TAB_KEYS.USER_PERSONAL); - - if (isFixedSingle) { - return ( -
- {sliceGroups.map((g) => ( -
-
- {g.title} -
-
-
- {g.items.length === 0 && memory.addingMemoryKey !== g.key ? ( -
- -

- {t("memoryManageModal.noMemory")} -

-
- ) : ( -
- {memory.addingMemoryKey === g.key && ( -
- {renderAddMemoryInput(g.key)} -
- )} - ( - } - onClick={() => - memory.handleDeleteMemory(item.id, g.key) - } - />, - ]} - > -
- {item.memory} -
-
- )} - /> -
- )} -
- ))} -
- ); - } - - return ( - <> - ( - - )} - activeKey={memory.openKey} - onChange={(key) => { - if (Array.isArray(key)) { - memory.setOpenKey(key[0] as string); - } else if (key) { - memory.setOpenKey(key as string); - } - }} - style={{ maxHeight: "70vh", overflow: "auto" }} - items={sliceGroups.map((g) => { - const isPlaceholder = /-placeholder$/.test(g.key); - const disabled = !isPlaceholder && !!memory.disabledGroups[g.key]; - return { - key: g.key, - label: ( -
- {g.title} -
e.stopPropagation()} - > - {showSwitch && !isPlaceholder && ( - memory.toggleGroup(g.key, val)} - /> - )} - {/* If the group has no data, hide the "clear memory" button to keep the interface simple */} -
-
- ), - collapsible: disabled ? "disabled" : undefined, - children: ( -
- {memory.addingMemoryKey === g.key && ( -
- {renderAddMemoryInput(g.key)} -
- )} - ( - } - disabled={disabled} - onClick={() => - memory.handleDeleteMemory(item.id, g.key) - } - />, - ]} - > -
- {item.memory} -
-
- )} - /> -
- ), - showArrow: true, - className: "memory-modal-panel", - }; - })} - /> - {paginated && groups.length > memory.pageSize && ( -
- - memory.setPageMap((prev) => ({ ...prev, [tabKey!]: page })) - } - showSizeChanger={false} - /> -
- )} - - ); - }; - - const labelWithIcon: LabelWithIconFunction = (Icon: React.ElementType, text: string) => ( - - - {text} - - ); - - const tabItems = [ - { - key: MEMORY_TAB_KEYS.BASE, - label: labelWithIcon(Settings, t("memoryManageModal.baseSettings")), - children: renderBaseSettings(), - }, - ...(role === USER_ROLES.ADMIN - ? [ - { - key: MEMORY_TAB_KEYS.TENANT, - label: labelWithIcon( - UsersRound, - t("memoryManageModal.tenantShareTab") - ), - children: renderCollapseGroups( - [memory.tenantSharedGroup], - false, - MEMORY_TAB_KEYS.TENANT - ), - disabled: !memory.memoryEnabled, - }, - { - key: MEMORY_TAB_KEYS.AGENT_SHARED, - label: labelWithIcon(Share2, t("memoryManageModal.agentShareTab")), - children: renderCollapseGroups( - memory.agentSharedGroups, - true, - MEMORY_TAB_KEYS.AGENT_SHARED - ), - disabled: !memory.memoryEnabled || memory.shareOption === MEMORY_SHARE_STRATEGY.NEVER, - }, - ] - : []), - { - key: MEMORY_TAB_KEYS.USER_PERSONAL, - label: labelWithIcon(UserRound, t("memoryManageModal.userPersonalTab")), - children: renderCollapseGroups( - [memory.userPersonalGroup], - false, - MEMORY_TAB_KEYS.USER_PERSONAL - ), - disabled: !memory.memoryEnabled, - }, - { - key: MEMORY_TAB_KEYS.USER_AGENT, - label: labelWithIcon(Bot, t("memoryManageModal.userAgentTab")), - children: renderCollapseGroups(memory.userAgentGroups, true, MEMORY_TAB_KEYS.USER_AGENT), - disabled: !memory.memoryEnabled, - }, - ]; - - return ( - <> - - memory.setActiveTabKey(key)} - /> - - - {/* Clear memory confirmation popup */} - - - ); -}; - -export default MemoryManageModal; diff --git a/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx b/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx index d8efdb86c..37a44f56c 100644 --- a/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx +++ b/frontend/app/[locale]/chat/streaming/chatStreamFinalMessage.tsx @@ -1,7 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Copy, Volume2, ChevronRight, Square, Loader2 } from "lucide-react"; -import { FaRegThumbsDown, FaRegThumbsUp } from "react-icons/fa"; +import { + Copy, + Volume2, + ChevronRight, + Square, + Loader2, + ThumbsDown, + ThumbsUp, +} from "lucide-react"; import { MarkdownRenderer } from "@/components/ui/markdownRenderer"; import { Button } from "@/components/ui/button"; @@ -360,7 +367,7 @@ export function ChatStreamFinalMessage({ } transition-all duration-200 shadow-sm`} onClick={handleThumbsUp} > - + @@ -387,7 +394,7 @@ export function ChatStreamFinalMessage({ } transition-all duration-200 shadow-sm`} onClick={handleThumbsDown} > - + diff --git a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx index 014664450..283730e0f 100644 --- a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx +++ b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx @@ -4,8 +4,8 @@ import type React from "react"; import { useState, useEffect, useRef, useLayoutEffect } from "react"; import { useTranslation } from "react-i18next"; -import { App, Modal, Row, Col } from "antd"; -import { InfoCircleFilled, WarningFilled } from "@ant-design/icons"; +import { App, Modal, Row, Col, theme } from "antd"; +import { ExclamationCircleFilled, WarningFilled, InfoCircleFilled } from "@ant-design/icons"; import { DOCUMENT_ACTION_TYPES, KNOWLEDGE_BASE_ACTION_TYPES, @@ -116,6 +116,7 @@ function DataConfig({ isActive }: DataConfigProps) { const { message } = App.useApp(); const { confirm } = useConfirmModal(); const { modelConfig } = useConfig(); + const { token } = theme.useToken(); // Clear cache when component initializes useEffect(() => { @@ -497,7 +498,7 @@ function DataConfig({ isActive }: DataConfigProps) { okText: t("common.confirm"), cancelText: t("common.cancel"), danger: true, - onConfirm: async () => { + onOk: async () => { try { await deleteKnowledgeBase(id); @@ -554,7 +555,7 @@ function DataConfig({ isActive }: DataConfigProps) { okText: t("common.confirm"), cancelText: t("common.cancel"), danger: true, - onConfirm: async () => { + onOk: async () => { try { await deleteDocument(kbId, docId); message.success(t("document.message.deleteSuccess")); @@ -828,24 +829,29 @@ function DataConfig({ isActive }: DataConfigProps) { > setShowAutoDeselectModal(false)} onCancel={() => setShowAutoDeselectModal(false)} okText={t("common.confirm")} cancelButtonProps={{ style: { display: "none" } }} centered + okButtonProps={{ type: "primary", danger: true }} getContainer={() => contentRef.current || document.body} > -
-
- -
-
- {t("embedding.knowledgeBaseAutoDeselectModal.content")} -
+
+ +
+
+ {t("embedding.knowledgeBaseAutoDeselectModal.title")} +
+
+ {t("embedding.knowledgeBaseAutoDeselectModal.content")}
diff --git a/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx b/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx index 0022eec70..04f886ee8 100644 --- a/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx +++ b/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx @@ -16,7 +16,7 @@ import { Pagination, Input, } from "antd"; -import { WarningFilled } from "@ant-design/icons"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; import { Download, ScanText, @@ -81,6 +81,7 @@ const DocumentChunk: React.FC = ({ }) => { const { t } = useTranslation(); const { message } = App.useApp(); + const { confirm } = useConfirmModal(); const [chunks, setChunks] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); @@ -107,8 +108,6 @@ const DocumentChunk: React.FC = ({ const [chunkSubmitting, setChunkSubmitting] = useState(false); const [editingChunk, setEditingChunk] = useState(null); const [chunkForm] = Form.useForm(); - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [chunkToDelete, setChunkToDelete] = useState(null); const [tooltipResetKey, setTooltipResetKey] = useState(0); const resetChunkSearch = React.useCallback(() => { @@ -455,39 +454,32 @@ const DocumentChunk: React.FC = ({ } forceCloseTooltips(); - setChunkToDelete(chunk); - setDeleteModalOpen(true); - }; - - const handleDeleteConfirm = async () => { - if (!chunkToDelete?.id || !knowledgeBaseName) { - return; - } - - try { - await knowledgeBaseService.deleteChunk( - knowledgeBaseName, - chunkToDelete.id - ); - message.success(t("document.chunk.success.delete")); - setDeleteModalOpen(false); - setChunkToDelete(null); - forceCloseTooltips(); - await refreshChunks(); - } catch (error) { - log.error("Failed to delete chunk:", error); - message.error( - error instanceof Error && error.message - ? error.message - : t("document.chunk.error.deleteFailed") - ); - } - }; - - const handleDeleteCancel = () => { - setDeleteModalOpen(false); - setChunkToDelete(null); - forceCloseTooltips(); + + confirm({ + title: t("document.chunk.confirm.deleteTitle"), + content: t("document.chunk.confirm.deleteContent"), + okText: t("common.delete"), + cancelText: t("common.cancel"), + danger: true, + onOk: async () => { + try { + await knowledgeBaseService.deleteChunk(knowledgeBaseName, chunk.id); + message.success(t("document.chunk.success.delete")); + forceCloseTooltips(); + await refreshChunks(); + } catch (error) { + log.error("Failed to delete chunk:", error); + message.error( + error instanceof Error && error.message + ? error.message + : t("document.chunk.error.deleteFailed") + ); + } + }, + onCancel: () => { + forceCloseTooltips(); + }, + }); }; const renderDocumentLabel = (doc: Document, chunkCount: number) => { @@ -847,34 +839,7 @@ const DocumentChunk: React.FC = ({ - - - -
- } - > -
-
- -
-
- {t("document.chunk.confirm.deleteContent")} -
-
-
-
- + ); }; diff --git a/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx b/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx index f1c39af17..20e117000 100644 --- a/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx +++ b/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Popover, Progress } from "antd"; -import { QuestionCircleOutlined } from "@ant-design/icons"; +import { CircleHelp } from "lucide-react"; import { DOCUMENT_STATUS } from "@/const/knowledgeBase"; import knowledgeBaseService from "@/services/knowledgeBaseService"; import log from "@/lib/logger"; @@ -272,9 +272,9 @@ export const DocumentStatus: React.FC = ({ open={isPopoverOpen} onOpenChange={handlePopoverVisibleChange} > - )} diff --git a/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx b/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx index fd656ba83..93982236b 100644 --- a/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx +++ b/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx @@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next"; import { NAME_CHECK_STATUS } from "@/const/agentConfig"; import { Upload, Progress } from "antd"; -import { InboxOutlined, WarningFilled } from "@ant-design/icons"; +import { WarningFilled } from "@ant-design/icons"; +import { Inbox } from "lucide-react"; import type { UploadFile, UploadProps } from "antd/es/upload/interface"; const { Dragger } = Upload; @@ -171,7 +172,7 @@ const UploadAreaUI: React.FC = ({ >

- +

{t("knowledgeBase.upload.dragHint")} diff --git a/frontend/app/[locale]/memory/MemoryContent.tsx b/frontend/app/[locale]/memory/MemoryContent.tsx index 370e220b3..d116a8606 100644 --- a/frontend/app/[locale]/memory/MemoryContent.tsx +++ b/frontend/app/[locale]/memory/MemoryContent.tsx @@ -17,7 +17,7 @@ import { Check, X, } from "lucide-react"; -import { useTranslation } from "react-i18next"; +import { useTranslation, Trans } from "react-i18next"; import { useAuth } from "@/hooks/useAuth"; import { useMemory } from "@/hooks/useMemory"; @@ -29,7 +29,7 @@ import { STANDARD_CARD, } from "@/const/layoutConstants"; -import MemoryDeleteConfirmation from "./components/MemoryDeleteConfirmation"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; interface MemoryContentProps { /** Custom navigation handler (optional) */ @@ -44,6 +44,7 @@ export default function MemoryContent({ onNavigate }: MemoryContentProps) { const { message } = App.useApp(); const { t } = useTranslation("common"); const { user, isSpeedMode } = useAuth(); + const { confirm } = useConfirmModal(); // Use custom hook for common setup flow logic const { canAccessProtectedData, pageVariants, pageTransition } = useSetupFlow({ @@ -66,29 +67,25 @@ export default function MemoryContent({ onNavigate }: MemoryContentProps) { message, }); - // Clear memory confirmation state - const [clearConfirmVisible, setClearConfirmVisible] = useState(false); - const [clearTarget, setClearTarget] = useState<{ - key: string; - title: string; - } | null>(null); - const handleClearConfirm = (groupKey: string, groupTitle: string) => { - setClearTarget({ key: groupKey, title: groupTitle }); - setClearConfirmVisible(true); - }; - - const handleClearConfirmOk = async () => { - if (clearTarget) { - await memory.handleClearMemory(clearTarget.key, clearTarget.title); - setClearConfirmVisible(false); - setClearTarget(null); - } - }; - - const handleClearConfirmCancel = () => { - setClearConfirmVisible(false); - setClearTarget(null); + confirm({ + title: t("memoryDeleteModal.title"), + content: ( +

+

+ }} + /> +

+

+ {t("memoryDeleteModal.prompt")} +

+
+ ), + onOk: () => memory.handleClearMemory(groupKey, groupTitle), + }); }; // Render base settings in a horizontal control bar @@ -379,13 +376,7 @@ export default function MemoryContent({ onNavigate }: MemoryContentProps) { ) : null} - {/* Clear memory confirmation */} - + ); } diff --git a/frontend/app/[locale]/memory/components/MemoryDeleteConfirmation.tsx b/frontend/app/[locale]/memory/components/MemoryDeleteConfirmation.tsx deleted file mode 100644 index 52efb93f5..000000000 --- a/frontend/app/[locale]/memory/components/MemoryDeleteConfirmation.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import React from "react"; -import { useTranslation, Trans } from "react-i18next"; -import { Modal } from "antd"; -import { AlertCircle } from "lucide-react"; - -interface MemoryDeleteConfirmationProps { - visible: boolean; - targetTitle: string; - onOk: () => void; - onCancel: () => void; -} - -/** - * MemoryDeleteConfirmation - Confirmation dialog for clearing memory - * Used in the memory management page to confirm destructive actions - */ -export default function MemoryDeleteConfirmation({ - visible, - targetTitle, - onOk, - onCancel, -}: MemoryDeleteConfirmationProps) { - const { t } = useTranslation(); - - return ( - - - {t("memoryDeleteModal.title")} -
- } - onOk={onOk} - onCancel={onCancel} - okText={t("memoryDeleteModal.clear")} - cancelText={t("common.cancel")} - okButtonProps={{ danger: true }} - destroyOnClose - centered - > -
-

- }} - /> -

-

- {t("memoryDeleteModal.prompt")} -

-
- - ); -} - diff --git a/frontend/app/[locale]/models/ModelsContent.tsx b/frontend/app/[locale]/models/ModelsContent.tsx index 6b48e8dac..87e0120d8 100644 --- a/frontend/app/[locale]/models/ModelsContent.tsx +++ b/frontend/app/[locale]/models/ModelsContent.tsx @@ -1,19 +1,15 @@ "use client"; -import React, {useRef, useState} from "react"; -import {App} from "antd"; +import {useRef} from "react"; import {motion} from "framer-motion"; import {useSetupFlow} from "@/hooks/useSetupFlow"; -import {configStore} from "@/lib/config"; -import {configService} from "@/services/configService"; import { ConnectionStatus, } from "@/const/modelConfig"; import AppModelConfig from "./ModelConfiguration"; import {ModelConfigSectionRef} from "./components/modelConfig"; -import EmbedderCheckModal from "./components/model/EmbedderCheckModal"; interface ModelsContentProps { /** Custom next button handler (optional) */ @@ -33,21 +29,16 @@ interface ModelsContentProps { * Can be used in setup flow or as standalone page */ export default function ModelsContent({ - onNext: customOnNext, connectionStatus: externalConnectionStatus, isCheckingConnection: externalIsCheckingConnection, onCheckConnection: externalOnCheckConnection, onConnectionStatusChange, }: ModelsContentProps) { - const {message} = App.useApp(); - // Use custom hook for common setup flow logic const { canAccessProtectedData, pageVariants, pageTransition, - router, - t, } = useSetupFlow({ requireAdmin: true, externalConnectionStatus, @@ -57,69 +48,8 @@ export default function ModelsContent({ nonAdminRedirect: "/setup/knowledges", }); - const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false); - const [pendingJump, setPendingJump] = useState(false); - const [connectivityWarningOpen, setConnectivityWarningOpen] = useState(false); - const [liveSelectedModels, setLiveSelectedModels] = useState - > | null>(null); const modelConfigSectionRef = useRef(null); - // Centralized behavior: save current config and navigate to next page - const saveAndNavigateNext = async () => { - try { - const currentConfig = configStore.getConfig(); - const ok = await configService.saveConfigToBackend(currentConfig as any); - if (!ok) { - message.error(t("setup.page.error.saveConfig")); - } - } catch (e) { - message.error(t("setup.page.error.saveConfig")); - } - - // Call custom onNext if provided, otherwise navigate to default next page - if (customOnNext) { - customOnNext(); - } else { - router.push("/setup/knowledges"); - } - }; - - const handleEmbeddingOk = async () => { - setEmbeddingModalOpen(false); - if (pendingJump) { - setPendingJump(false); - await saveAndNavigateNext(); - } - }; - - const handleConnectivityOk = async () => { - setConnectivityWarningOpen(false); - if (pendingJump) { - setPendingJump(false); - // Apply live selections programmatically to mimic dropdown onChange - try { - const ref = modelConfigSectionRef.current; - const selections = liveSelectedModels || {}; - if (ref && selections) { - // Iterate categories and options - for (const [category, options] of Object.entries(selections)) { - for (const [option, displayName] of Object.entries(options)) { - if (displayName) { - // Simulate dropdown change and trigger onChange flow - await ref.simulateDropdownChange(category, option, displayName); - } - } - } - } - } catch (e) { - message.error(t("setup.page.error.saveConfig")); - } - await saveAndNavigateNext(); - } - }; - return ( <> {canAccessProtectedData ? ( - setLiveSelectedModels(selected) - } + onSelectedModelsChange={() => {}} onEmbeddingConnectivityChange={() => {}} forwardedRef={modelConfigSectionRef} canAccessProtectedData={canAccessProtectedData} @@ -144,17 +72,6 @@ export default function ModelsContent({
- setEmbeddingModalOpen(false)} - connectivityWarningOpen={connectivityWarningOpen} - onConnectivityOk={handleConnectivityOk} - onConnectivityCancel={() => setConnectivityWarningOpen(false)} - modifyWarningOpen={false} - onModifyOk={() => {}} - onModifyCancel={() => {}} - /> ); } diff --git a/frontend/app/[locale]/models/components/model/EmbedderCheckModal.tsx b/frontend/app/[locale]/models/components/model/EmbedderCheckModal.tsx deleted file mode 100644 index a519e63e0..000000000 --- a/frontend/app/[locale]/models/components/model/EmbedderCheckModal.tsx +++ /dev/null @@ -1,144 +0,0 @@ -"use client"; - -import { useTranslation } from "react-i18next"; -import { Modal, Button } from "antd"; -import { WarningFilled } from "@ant-design/icons"; - -interface EmbedderCheckModalProps { - // Existing empty selection warning modal - emptyWarningOpen: boolean; - onEmptyOk: () => void; - onEmptyCancel: () => void; - - // New connectivity warning modal - connectivityWarningOpen: boolean; - onConnectivityOk: () => void; - onConnectivityCancel: () => void; - - // New modify embedding confirmation modal - modifyWarningOpen: boolean; - onModifyOk: () => void; - onModifyCancel: () => void; -} - -export default function EmbedderCheckModal(props: EmbedderCheckModalProps) { - const { t } = useTranslation(); - const { - emptyWarningOpen, - onEmptyOk, - onEmptyCancel, - connectivityWarningOpen, - onConnectivityOk, - onConnectivityCancel, - modifyWarningOpen, - onModifyOk, - onModifyCancel, - } = props; - - return ( - <> - {/* Existing empty embedding selection warning */} - - - -
- } - > -
-
- -
-
{t("embedding.emptyWarningModal.content")}
-
-
- {t("embedding.emptyWarningModal.tip")} -
-
-
-
- - - {/* New connectivity check warning */} - - - -
- } - > -
-
- -
-
- {t("embedding.unavaliableWarningModal.content")} -
-
- {t("embedding.unavaliableWarningModal.tip")} -
-
-
-
-
- - {/* New modify embedding confirmation warning */} - - - - - } - > -
-
- -
-
- {t("embedding.modifyWarningModal.content")} -
-
-
-
-
- - ); -} - - diff --git a/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx b/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx index 1ce0995cf..f2257e03d 100644 --- a/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx +++ b/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx @@ -2,13 +2,13 @@ import { useMemo, useState, useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Modal, Select, Input, Button, Switch, Tooltip, App } from "antd"; +import { InfoCircleFilled } from "@ant-design/icons"; import { - InfoCircleFilled, - LoadingOutlined, - RightOutlined, - DownOutlined, - SettingOutlined, -} from "@ant-design/icons"; + LoaderCircle, + ChevronRight, + ChevronDown, + Settings +} from "lucide-react"; import { useConfig } from "@/hooks/useConfig"; import { configService } from "@/services/configService"; @@ -924,9 +924,9 @@ export const ModelAddDialog = ({ className="flex items-center focus:outline-none" > {showModelList ? ( - + ) : ( - + )} {t("model.dialog.modelList.title")} @@ -962,8 +962,8 @@ export const ModelAddDialog = ({ )} {loadingModelList ? (
-
- + ); })} @@ -915,7 +910,7 @@ export const ModelDeleteDialog = ({ - + ); })} @@ -951,7 +946,7 @@ export const ModelDeleteDialog = ({
diff --git a/frontend/app/[locale]/models/components/model/ModelListCard.tsx b/frontend/app/[locale]/models/components/model/ModelListCard.tsx index daf7b8610..ae966ae35 100644 --- a/frontend/app/[locale]/models/components/model/ModelListCard.tsx +++ b/frontend/app/[locale]/models/components/model/ModelListCard.tsx @@ -228,11 +228,15 @@ export const ModelListCard = ({ }} placeholder={t("model.select.placeholder")} value={selectedModel || undefined} - onChange={onModelChange} + onChange={(value) => { + // Prevent duplicate onChange calls by checking if value actually changed + if (value !== selectedModel) { + onModelChange(value || ""); + } + }} allowClear={{ clearIcon: , }} - onClear={() => onModelChange("")} size="middle" onClick={(e) => e.stopPropagation()} getPopupContainer={(triggerNode) => diff --git a/frontend/app/[locale]/models/components/modelConfig.tsx b/frontend/app/[locale]/models/components/modelConfig.tsx index 968cac51f..33e89df03 100644 --- a/frontend/app/[locale]/models/components/modelConfig.tsx +++ b/frontend/app/[locale]/models/components/modelConfig.tsx @@ -2,12 +2,12 @@ import { forwardRef, useEffect, useImperativeHandle, useState, useRef, ReactNode import { useTranslation } from 'react-i18next' import { Button, Card, Col, Row, Space, App } from 'antd' -import { - PlusOutlined, - SafetyCertificateOutlined, - SyncOutlined, - EditOutlined, -} from "@ant-design/icons"; +import { + Plus, + ShieldCheck, + RefreshCw, + PenLine +} from "lucide-react"; import { MODEL_TYPES, @@ -25,7 +25,7 @@ import log from "@/lib/logger"; import { ModelListCard } from "./model/ModelListCard"; import { ModelAddDialog } from "./model/ModelAddDialog"; import { ModelDeleteDialog } from "./model/ModelDeleteDialog"; -import EmbedderCheckModal from "./model/EmbedderCheckModal"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; // ModelConnectStatus type definition type ModelConnectStatus = (typeof MODEL_STATUS)[keyof typeof MODEL_STATUS]; @@ -98,13 +98,13 @@ export const ModelConfigSection = forwardRef< const { skipVerification = false, canAccessProtectedData = false } = props; const { modelConfig, updateModelConfig } = useConfig(); const modelData = getModelData(t); + const { confirm } = useConfirmModal(); // State management const [models, setModels] = useState([]); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isVerifying, setIsVerifying] = useState(false); - const [isModifyWarningOpen, setIsModifyWarningOpen] = useState(false); // Error state management const [errorFields, setErrorFields] = useState<{ [key: string]: boolean }>({ @@ -118,11 +118,6 @@ export const ModelConfigSection = forwardRef< // Throttle timer const throttleTimerRef = useRef(null); const saveTimerRef = useRef(null); - const pendingChangeRef = useRef<{ - category: string; - option: string; - displayName: string; - } | null>(null); // Debounced auto-save scheduler const scheduleAutoSave = () => { @@ -777,8 +772,22 @@ export const ModelConfigSection = forwardRef< const currentValue = selectedModels[category]?.[option] || ""; // Only prompt when modifying from a non-empty value to a different value if (currentValue && currentValue !== displayName) { - pendingChangeRef.current = { category, option, displayName }; - setIsModifyWarningOpen(true); + confirm({ + title: t("embedding.modifyWarningModal.title"), + content: ( +
+
+ {t("embedding.modifyWarningModal.content")} +
+
+ ), + okText: t("embedding.modifyWarningModal.ok_proceed"), + cancelText: t("common.cancel"), + danger: false, + onOk: async () => { + await applyModelChange(category, option, displayName); + }, + }); return; } if (currentValue === displayName) { @@ -789,25 +798,6 @@ export const ModelConfigSection = forwardRef< await applyModelChange(category, option, displayName); }; - const handleModifyOk = async () => { - const pending = pendingChangeRef.current; - if (pending) { - await handleModelChange( - pending.category, - pending.option, - pending.displayName, - true - ); - } - pendingChangeRef.current = null; - setIsModifyWarningOpen(false); - }; - - const handleModifyCancel = () => { - pendingChangeRef.current = null; - setIsModifyWarningOpen(false); - }; - // Only update local UI state, no database operations involved const updateModelStatus = ( displayName: string, @@ -858,7 +848,7 @@ export const ModelConfigSection = forwardRef< style={{ width: "100%" }} block > - + {t("modelConfig.button.syncModelEngine")} @@ -873,7 +863,7 @@ export const ModelConfigSection = forwardRef< - + {/* Mobile hamburger menu button */} + + + ); } diff --git a/frontend/components/ui/AgentCallRelationshipModal.tsx b/frontend/components/ui/AgentCallRelationshipModal.tsx index 25d1a4412..b8457151b 100644 --- a/frontend/components/ui/AgentCallRelationshipModal.tsx +++ b/frontend/components/ui/AgentCallRelationshipModal.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; import { Modal, Spin, message, Typography } from "antd"; -import { RobotOutlined, ToolOutlined } from "@ant-design/icons"; +import { Bot, Wrench } from "lucide-react"; import { useTranslation } from "react-i18next"; import Tree from "react-d3-tree"; @@ -71,7 +71,7 @@ const CustomNode = ({ nodeDatum }: any) => { nodeDatum.type === AGENT_CALL_RELATIONSHIP_NODE_TYPES.MAIN || nodeDatum.type === AGENT_CALL_RELATIONSHIP_NODE_TYPES.SUB; const color = getNodeColor(nodeDatum.type, nodeDatum.depth); - const icon = isAgent ? : ; + const icon = isAgent ? : ; // Truncate tool names by maximum character count (avoid too long) const rawName: string = nodeDatum.name || ""; diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx deleted file mode 100644 index 6c6a9c2b9..000000000 --- a/frontend/components/ui/dialog.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Dialog = DialogPrimitive.Root; - -const DialogPortal = DialogPrimitive.Portal; - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogHeader.displayName = "DialogHeader"; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogFooter.displayName = "DialogFooter"; - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogTitle.displayName = DialogPrimitive.Title.displayName; - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/frontend/components/ui/navbar.tsx b/frontend/components/ui/navbar.tsx index 45a938e56..e69de29bb 100644 --- a/frontend/components/ui/navbar.tsx +++ b/frontend/components/ui/navbar.tsx @@ -1,129 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { AvatarDropdown } from "@/components/auth/avatarDropdown"; -import { useTranslation } from "react-i18next"; -import { useAuth } from "@/hooks/useAuth"; -import { Globe } from "lucide-react"; -import { Dropdown } from "antd"; -import { DownOutlined } from "@ant-design/icons"; -import Link from "next/link"; -import { HEADER_CONFIG } from "@/const/layoutConstants"; -import { languageOptions } from "@/const/constants"; -import { useLanguageSwitch } from "@/lib/language"; - -/** - * Main navigation bar component - * Displays logo, navigation links, language switcher, and user authentication status - */ -export function Navbar() { - const { t } = useTranslation("common"); - const { user, isLoading: userLoading, isSpeedMode } = useAuth(); - const { currentLanguage, handleLanguageChange } = useLanguageSwitch(); - - return ( -
- {/* Left section - Logo */} - -

- ModelEngine - - {t("assistant.name")} - -

- - - {/* Right section - Navigation links and user controls */} -
- {/* GitHub link */} - - - Github - - - {/* ModelEngine link */} - - ModelEngine - - - {/* Language switcher */} - ({ - key: opt.value, - label: opt.label, - })), - onClick: ({ key }) => handleLanguageChange(key as string), - }} - > - - - {languageOptions.find((o) => o.value === currentLanguage)?.label || - currentLanguage} - - - - - {/* User status - only shown in full version */} - {!isSpeedMode && ( - <> - {userLoading ? ( - - {t("common.loading")}... - - ) : user ? ( - - {user.email} - - ) : null} - - - )} -
- - {/* Mobile hamburger menu button */} - -
- ); -} - diff --git a/frontend/const/layoutConstants.ts b/frontend/const/layoutConstants.ts index 77b16df09..1c209617f 100644 --- a/frontend/const/layoutConstants.ts +++ b/frontend/const/layoutConstants.ts @@ -18,6 +18,15 @@ export const HEADER_CONFIG = { HORIZONTAL_PADDING: "24px", // px-6 } as const; +// Sidebar configuration +export const SIDER_CONFIG = { + // Sidebar width when expanded + EXPANDED_WIDTH: 280, + + // Sidebar width when collapsed + COLLAPSED_WIDTH: 64, +} as const; + // Footer configuration export const FOOTER_CONFIG = { // Actual displayed height (including padding) diff --git a/frontend/hooks/useConfirmModal.ts b/frontend/hooks/useConfirmModal.ts index 1a2b6e12a..1a12734da 100644 --- a/frontend/hooks/useConfirmModal.ts +++ b/frontend/hooks/useConfirmModal.ts @@ -1,7 +1,18 @@ +import { App } from "antd"; +import { ExclamationCircleFilled } from "@ant-design/icons"; + +import React from "react"; import i18next from "i18next"; -import { App } from "antd"; -import { StaticConfirmProps } from "@/types/setupConfig"; +interface ConfirmProps { + title: string; + content: React.ReactNode; + okText?: string; + cancelText?: string; + danger?: boolean; // 默认为 true,使用 danger 样式 + onOk?: () => void; + onCancel?: () => void; +} export const useConfirmModal = () => { const { modal } = App.useApp(); @@ -11,20 +22,25 @@ export const useConfirmModal = () => { content, okText, cancelText, - danger = false, - onConfirm, + danger = true, + onOk, onCancel, - }: StaticConfirmProps) => { + }: ConfirmProps) => { return modal.confirm({ title, content, + centered: true, + icon: React.createElement(ExclamationCircleFilled), okText: okText || i18next.t("common.confirm"), cancelText: cancelText || i18next.t("common.cancel"), - okButtonProps: { danger }, - onOk: onConfirm, + okButtonProps: { + danger, + type: "primary" + }, + onOk: onOk, onCancel, }); }; return { confirm }; -}; +}; \ No newline at end of file diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index dc20e0aa5..6a48fbb5c 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -1,6 +1,6 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" -import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons' +import { CircleCheck, XCircle, LoaderCircle } from "lucide-react" import { DOCUMENT_STATUS } from "@/const/knowledgeBase" import React from 'react' import log from "@/lib/logger"; @@ -165,11 +165,11 @@ export type ConnectivityStatusType = "checking" | "available" | "unavailable" | export const getConnectivityIcon = (status: ConnectivityStatusType): React.ReactNode => { switch (status) { case "checking": - return React.createElement(LoadingOutlined, { style: { color: '#1890ff' } }) + return React.createElement(LoaderCircle, { className: 'animate-spin', color: '#1890ff', size: 16 }) case "available": - return React.createElement(CheckCircleOutlined, { style: { color: '#52c41a' } }) + return React.createElement(CircleCheck, { color: '#52c41a', size: 16 }) case "unavailable": - return React.createElement(CloseCircleOutlined, { style: { color: '#ff4d4f' } }) + return React.createElement(XCircle, { color: '#ff4d4f', size: 16 }) default: return null } @@ -198,17 +198,17 @@ export const getConnectivityMeta = (status: ConnectivityStatusType): Connectivit switch (status) { case "checking": return { - icon: React.createElement(LoadingOutlined, { style: { color: '#1890ff' } }), + icon: React.createElement(LoaderCircle, { className: 'animate-spin', color: '#1890ff', size: 16 }), color: '#1890ff' } case "available": return { - icon: React.createElement(CheckCircleOutlined, { style: { color: '#52c41a' } }), + icon: React.createElement(CircleCheck, { color: '#52c41a', size: 16 }), color: '#52c41a' } case "unavailable": return { - icon: React.createElement(CloseCircleOutlined, { style: { color: '#ff4d4f' } }), + icon: React.createElement(XCircle, { color: '#ff4d4f', size: 16 }), color: '#ff4d4f' } default: diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index a44561b9c..c136acce8 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -22,6 +22,8 @@ const nextConfig = { parallelServerCompiles: true, }, compress: true, + // Fix workspace root detection for multiple lockfiles + outputFileTracingRoot: process.cwd(), } mergeConfig(nextConfig, userConfig) diff --git a/frontend/package.json b/frontend/package.json index 9fceddc44..69b4952bd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,8 +7,11 @@ "build": "next build", "start": "NODE_ENV=production node server.js", "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", "type-check": "tsc --noEmit", - "check-all": "npm run type-check && npm run lint && npm run build" + "check-all": "npm run type-check && npm run lint && npm run format:check && npm run build" }, "dependencies": { "@ant-design/icons": "^6.0.0", @@ -23,7 +26,6 @@ "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-context-menu": "^2.2.4", - "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-label": "^2.1.1", @@ -61,7 +63,7 @@ "katex": "^0.16.11", "lucide-react": "^0.454.0", "mermaid": "^11.12.0", - "next": "15.5.7", + "next": "^15.5.9", "next-i18next": "^15.4.2", "next-themes": "^0.4.4", "react": "18.2.0", @@ -75,7 +77,7 @@ "react-markdown": "^8.0.7", "react-organizational-chart": "^2.2.1", "react-resizable-panels": "^2.1.7", - "react-syntax-highlighter": "^15.5.0", + "react-syntax-highlighter": "^16.1.0", "reactflow": "^11.11.4", "recharts": "2.15.0", "rehype-katex": "^6.0.3", @@ -98,7 +100,10 @@ "@types/react-dom": "18.3.6", "eslint": "^9.34.0", "eslint-config-next": "15.5.7", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "postcss": "^8", + "prettier": "^3.2.5", "tailwindcss": "^3.4.17", "typescript": "5.8.3" } diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index b8681a78a..32b17603e 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -1232,6 +1232,14 @@ "market.install.mcp.notInstalled": "Not Installed", "market.install.mcp.urlPlaceholder": "Enter MCP server URL", "market.install.mcp.install": "Install", + "market.install.step.missingTools": "Missing Tools", + "market.install.tools.missingDescTitle": "The imported agent uses tools that do not exist in this system", + "market.install.tools.missingDescBody": "Please review the missing tools below and install or configure them first. If you continue without fixing them, the agent may not work correctly or some capabilities may be unavailable.", + "market.install.tools.loading": "Loading tools...", + "market.install.tools.source": "Source", + "market.install.tools.usage": "Usage", + "market.install.tools.usedBy": "Used by", + "market.install.tools.missingHint": "If you continue without installing or configuring this tool, the agent may lose part of its capabilities or fail when calling this tool.", "market.install.error.modelRequired": "Please select a model", "market.install.error.allModelsRequired": "Please select models for all agents", "market.install.error.configRequired": "Please fill in all required fields", diff --git a/frontend/public/locales/zh/common.json b/frontend/public/locales/zh/common.json index c0f8d851a..f448db8f7 100644 --- a/frontend/public/locales/zh/common.json +++ b/frontend/public/locales/zh/common.json @@ -1211,6 +1211,14 @@ "market.install.mcp.notInstalled": "未安装", "market.install.mcp.urlPlaceholder": "输入 MCP 服务器地址", "market.install.mcp.install": "安装", + "market.install.step.missingTools": "缺失工具", + "market.install.tools.missingDescTitle": "导入的智能体使用了当前系统中不存在的工具", + "market.install.tools.missingDescBody": "请检查下方列表并优先安装或配置这些工具。如果直接继续安装,智能体可能无法正常工作,或部分能力不可用。", + "market.install.tools.loading": "正在加载工具列表...", + "market.install.tools.source": "来源", + "market.install.tools.usage": "用途", + "market.install.tools.usedBy": "被以下智能体使用", + "market.install.tools.missingHint": "如果在未安装或配置此工具的情况下继续,智能体在调用该工具时可能报错,或失去相关能力。", "market.install.error.modelRequired": "请选择一个模型", "market.install.error.allModelsRequired": "请为所有智能体选择模型", "market.install.error.configRequired": "请填写所有必填字段", diff --git a/frontend/types/setupConfig.ts b/frontend/types/setupConfig.ts deleted file mode 100644 index a244e081f..000000000 --- a/frontend/types/setupConfig.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -interface StaticConfirmProps { - title: string; - content: React.ReactNode; - okText?: string; - cancelText?: string; - danger?: boolean; - onConfirm?: () => void; - onCancel?: () => void; -} - -export type { StaticConfirmProps }; diff --git a/test/backend/services/test_tool_configuration_service.py b/test/backend/services/test_tool_configuration_service.py index cf12c9805..b59d18449 100644 --- a/test/backend/services/test_tool_configuration_service.py +++ b/test/backend/services/test_tool_configuration_service.py @@ -1290,177 +1290,6 @@ def __init__(self): assert mock_build_tool_info.call_count == 2 -class TestInitializeToolsOnStartup: - """Test cases for initialize_tools_on_startup function""" - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.query_all_tools') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_no_tenants(self, mock_logger, mock_query_tools, mock_update_tool_list, mock_get_tenants): - """Test initialize_tools_on_startup when no tenants are found""" - # Mock get_all_tenant_ids to return empty list - mock_get_tenants.return_value = [] - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify warning was logged - mock_logger.warning.assert_called_with( - "No tenants found in database, skipping tool initialization") - mock_update_tool_list.assert_not_called() - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.query_all_tools') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_success(self, mock_logger, mock_query_tools, mock_update_tool_list, mock_get_tenants): - """Test successful tool initialization for all tenants""" - # Mock tenant IDs - tenant_ids = ["tenant_1", "tenant_2", "default_tenant"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list to succeed - mock_update_tool_list.return_value = None - - # Mock query_all_tools to return mock tools - mock_tools = [ - {"tool_id": "tool_1", "name": "Test Tool 1"}, - {"tool_id": "tool_2", "name": "Test Tool 2"} - ] - mock_query_tools.return_value = mock_tools - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify update_tool_list was called for each tenant - assert mock_update_tool_list.call_count == len(tenant_ids) - - # Verify success logging - mock_logger.info.assert_any_call("Tool initialization completed!") - mock_logger.info.assert_any_call( - "Total tools available across all tenants: 6") # 2 tools * 3 tenants - mock_logger.info.assert_any_call("Successfully processed: 3/3 tenants") - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_timeout(self, mock_logger, mock_update_tool_list, mock_get_tenants): - """Test tool initialization timeout scenario""" - tenant_ids = ["tenant_1", "tenant_2"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list to timeout - mock_update_tool_list.side_effect = asyncio.TimeoutError() - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify timeout error was logged for each tenant - assert mock_logger.error.call_count == len(tenant_ids) - for call in mock_logger.error.call_args_list: - assert "timed out" in str(call) - - # Verify failed tenants were logged - mock_logger.warning.assert_called_once() - warning_call = mock_logger.warning.call_args[0][0] - assert "Failed tenants:" in warning_call - assert "tenant_1 (timeout)" in warning_call - assert "tenant_2 (timeout)" in warning_call - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_exception(self, mock_logger, mock_update_tool_list, mock_get_tenants): - """Test tool initialization with exception during processing""" - tenant_ids = ["tenant_1", "tenant_2"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list to raise exception - mock_update_tool_list.side_effect = Exception( - "Database connection failed") - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify exception error was logged for each tenant - assert mock_logger.error.call_count == len(tenant_ids) - for call in mock_logger.error.call_args_list: - assert "Tool initialization failed" in str(call) - assert "Database connection failed" in str(call) - - # Verify failed tenants were logged - mock_logger.warning.assert_called_once() - warning_call = mock_logger.warning.call_args[0][0] - assert "Failed tenants:" in warning_call - assert "tenant_1 (error: Database connection failed)" in warning_call - assert "tenant_2 (error: Database connection failed)" in warning_call - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_critical_exception(self, mock_logger, mock_get_tenants): - """Test tool initialization when get_all_tenant_ids raises exception""" - # Mock get_all_tenant_ids to raise exception - mock_get_tenants.side_effect = Exception("Database connection failed") - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - - # Should raise the exception - with pytest.raises(Exception, match="Database connection failed"): - await initialize_tools_on_startup() - - # Verify critical error was logged - mock_logger.error.assert_called_with( - "❌ Tool initialization failed: Database connection failed") - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.query_all_tools') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_mixed_results(self, mock_logger, mock_query_tools, mock_update_tool_list, mock_get_tenants): - """Test tool initialization with mixed success and failure results""" - tenant_ids = ["tenant_1", "tenant_2", "tenant_3"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list with mixed results - def side_effect(*args, **kwargs): - tenant_id = kwargs.get('tenant_id') - if tenant_id == "tenant_1": - return None # Success - elif tenant_id == "tenant_2": - raise asyncio.TimeoutError() # Timeout - else: # tenant_3 - raise Exception("Connection error") # Exception - - mock_update_tool_list.side_effect = side_effect - - # Mock query_all_tools for successful tenant - mock_tools = [{"tool_id": "tool_1", "name": "Test Tool"}] - mock_query_tools.return_value = mock_tools - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify mixed results logging - mock_logger.info.assert_any_call("Tool initialization completed!") - mock_logger.info.assert_any_call( - "Total tools available across all tenants: 1") - mock_logger.info.assert_any_call("Successfully processed: 1/3 tenants") - - # Verify failed tenants were logged - mock_logger.warning.assert_called_once() - warning_call = mock_logger.warning.call_args[0][0] - assert "Failed tenants:" in warning_call - assert "tenant_2 (timeout)" in warning_call - assert "tenant_3 (error: Connection error)" in warning_call - - class TestLoadLastToolConfigImpl: """Test load_last_tool_config_impl function""" diff --git a/test/backend/test_config_service.py b/test/backend/test_config_service.py index 9935797cc..0f25b9530 100644 --- a/test/backend/test_config_service.py +++ b/test/backend/test_config_service.py @@ -128,308 +128,6 @@ def __init__(self, *args, **kwargs): setattr(backend_pkg, "apps", apps_pkg) setattr(apps_pkg, "config_app", base_app_mod) -# Mock external dependencies before importing backend modules -with patch('backend.database.client.MinioClient', return_value=minio_client_mock), \ - patch('elasticsearch.Elasticsearch', return_value=MagicMock()), \ - patch('nexent.vector_database.elasticsearch_core.ElasticSearchCore', return_value=MagicMock()): - # Mock dotenv before importing config_service - with patch('dotenv.load_dotenv'): - # Mock logging configuration - with patch('utils.logging_utils.configure_logging'), \ - patch('utils.logging_utils.configure_elasticsearch_logging'): - from config_service import startup_initialization - - -class TestMainService: - """Test cases for config_service module""" - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_success(self, mock_logger, mock_initialize_tools): - """ - Test successful startup initialization. - - This test verifies that: - 1. The function logs the start of initialization - 2. It logs the APP version - 3. It calls initialize_tools_on_startup - 4. It logs successful completion - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that appropriate log messages were called - mock_logger.info.assert_any_call("Starting server initialization...") - mock_logger.info.assert_any_call( - "Server initialization completed successfully!") - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_with_version_log(self, mock_logger, mock_initialize_tools): - """ - Test that startup initialization logs the APP version. - - This test verifies that: - 1. The function logs the APP version from consts.const - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that version logging was called (should contain "APP version is:") - version_logged = any( - call for call in mock_logger.info.call_args_list - if len(call.args) > 0 and "APP version is:" in str(call.args[0]) - ) - assert version_logged, "APP version should be logged during initialization" - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_tool_initialization_failure(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization fails. - - This test verifies that: - 1. When initialize_tools_on_startup raises an exception - 2. The function catches the exception and logs an error - 3. The function logs a warning about continuing despite issues - 4. The function does not re-raise the exception - """ - # Setup - mock_initialize_tools.side_effect = Exception( - "Tool initialization failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_call - assert "Tool initialization failed" in error_call - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_database_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when database connection fails. - - This test verifies that: - 1. Database-related exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The server startup is not blocked - """ - # Setup - mock_initialize_tools.side_effect = ConnectionError( - "Database connection failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - assert "Database connection failed" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_timeout_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization times out. - - This test verifies that: - 1. Timeout exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The function continues execution - """ - # Setup - mock_initialize_tools.side_effect = asyncio.TimeoutError( - "Tool initialization timed out") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_multiple_calls_safe(self, mock_logger, mock_initialize_tools): - """ - Test that multiple calls to startup_initialization are safe. - - This test verifies that: - 1. The function can be called multiple times without issues - 2. Each call properly executes the initialization sequence - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute multiple times - await startup_initialization() - await startup_initialization() - - # Assert - assert mock_initialize_tools.call_count == 2 - # At least 2 calls * 2 info messages per call - assert mock_logger.info.call_count >= 4 - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_logging_order(self, mock_logger, mock_initialize_tools): - """ - Test that logging occurs in the correct order during initialization. - - This test verifies that: - 1. Start message is logged first - 2. Version message is logged second - 3. Success message is logged last (when successful) - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - info_calls = [call.args[0] for call in mock_logger.info.call_args_list] - - # Check order of log messages - assert len(info_calls) >= 3 - assert "Starting server initialization..." in info_calls[0] - assert "APP version is:" in info_calls[1] - assert "Server initialization completed successfully!" in info_calls[-1] - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_exception_details_logged(self, mock_logger, mock_initialize_tools): - """ - Test that exception details are properly logged. - - This test verifies that: - 1. The specific exception message is included in error logs - 2. Both error and warning messages are logged on failure - """ - # Setup - specific_error_message = "Specific tool configuration error occurred" - mock_initialize_tools.side_effect = ValueError(specific_error_message) - - # Execute - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call_args = mock_logger.error.call_args[0][0] - assert specific_error_message in error_call_args - assert "Server initialization failed:" in error_call_args - - mock_logger.warning.assert_called_once() - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_no_exception_propagation(self, mock_logger, mock_initialize_tools): - """ - Test that exceptions during initialization do not propagate. - - This test verifies that: - 1. Even when initialize_tools_on_startup fails, no exception is raised - 2. This allows the server to continue starting up - """ - # Setup - mock_initialize_tools.side_effect = RuntimeError( - "Critical initialization error") - - # Execute and Assert - should not raise any exception - try: - await startup_initialization() - except Exception as e: - pytest.fail( - f"startup_initialization should not raise exceptions, but raised: {e}") - - # Verify that error handling occurred - mock_logger.error.assert_called_once() - mock_logger.warning.assert_called_once() - - -class TestMainServiceModuleIntegration: - """Integration tests for config_service module dependencies""" - - @patch('config_service.configure_logging') - @patch('config_service.configure_elasticsearch_logging') - def test_logging_configuration_called_on_import(self, mock_configure_es, mock_configure_logging): - """ - Test that logging configuration functions are called when module is imported. - - This test verifies that: - 1. configure_logging is called with logging.INFO - 2. configure_elasticsearch_logging is called - """ - # Note: This test checks that logging configuration happens during module import - # The mocks should have been called when the module was imported - # In a real scenario, you might need to reload the module to test this properly - pass # The actual verification would depend on how the test runner handles imports - - @patch('config_service.APP_VERSION', 'test_version_1.2.3') - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_with_custom_version(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization with a custom APP_VERSION. - - This test verifies that: - 1. The custom version is properly logged - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - version_logged = any( - "test_version_1.2.3" in str(call.args[0]) - for call in mock_logger.info.call_args_list - if len(call.args) > 0 - ) - assert version_logged, "Custom APP version should be logged" - class TestTenantConfigService: """Unit tests for tenant_config_service helpers""" diff --git a/test/backend/test_runtime_service.py b/test/backend/test_runtime_service.py index f7783cd82..81b2bb7fc 100644 --- a/test/backend/test_runtime_service.py +++ b/test/backend/test_runtime_service.py @@ -124,264 +124,6 @@ def __init__(self, *args, **kwargs): setattr(backend_pkg, "apps", apps_pkg) setattr(apps_pkg, "runtime_app", base_app_mod) -# Mock external dependencies before importing backend modules -with patch('elasticsearch.Elasticsearch', return_value=MagicMock()), \ - patch('nexent.vector_database.elasticsearch_core.ElasticSearchCore', return_value=MagicMock()): - # Mock dotenv before importing runtime_service - with patch('dotenv.load_dotenv'): - # Mock logging configuration - with patch('utils.logging_utils.configure_logging'), \ - patch('utils.logging_utils.configure_elasticsearch_logging'): - from runtime_service import startup_initialization - - -class TestMainService: - """Test cases for runtime_service module""" - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_success(self, mock_logger, mock_initialize_tools): - """ - Test successful startup initialization. - - This test verifies that: - 1. The function logs the start of initialization - 2. It logs the APP version - 3. It calls initialize_tools_on_startup - 4. It logs successful completion - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that appropriate log messages were called - mock_logger.info.assert_any_call("Starting server initialization...") - mock_logger.info.assert_any_call( - "Server initialization completed successfully!") - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_with_version_log(self, mock_logger, mock_initialize_tools): - """ - Test that startup initialization logs the APP version. - - This test verifies that: - 1. The function logs the APP version from consts.const - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that version logging was called (should contain "APP version is:") - version_logged = any( - call for call in mock_logger.info.call_args_list - if len(call.args) > 0 and "APP version is:" in str(call.args[0]) - ) - assert version_logged, "APP version should be logged during initialization" - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_tool_initialization_failure(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization fails. - - This test verifies that: - 1. When initialize_tools_on_startup raises an exception - 2. The function catches the exception and logs an error - 3. The function logs a warning about continuing despite issues - 4. The function does not re-raise the exception - """ - # Setup - mock_initialize_tools.side_effect = Exception( - "Tool initialization failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_call - assert "Tool initialization failed" in error_call - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_database_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when database connection fails. - - This test verifies that: - 1. Database-related exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The server startup is not blocked - """ - # Setup - mock_initialize_tools.side_effect = ConnectionError( - "Database connection failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - assert "Database connection failed" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_timeout_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization times out. - - This test verifies that: - 1. Timeout exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The function continues execution - """ - # Setup - mock_initialize_tools.side_effect = asyncio.TimeoutError( - "Tool initialization timed out") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_multiple_calls_safe(self, mock_logger, mock_initialize_tools): - """ - Test that multiple calls to startup_initialization are safe. - - This test verifies that: - 1. The function can be called multiple times without issues - 2. Each call properly executes the initialization sequence - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute multiple times - await startup_initialization() - await startup_initialization() - - # Assert - assert mock_initialize_tools.call_count == 2 - # At least 2 calls * 2 info messages per call - assert mock_logger.info.call_count >= 4 - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_logging_order(self, mock_logger, mock_initialize_tools): - """ - Test that logging occurs in the correct order during initialization. - - This test verifies that: - 1. Start message is logged first - 2. Version message is logged second - 3. Success message is logged last (when successful) - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - info_calls = [call.args[0] for call in mock_logger.info.call_args_list] - - # Check order of log messages - assert len(info_calls) >= 3 - assert "Starting server initialization..." in info_calls[0] - assert "APP version is:" in info_calls[1] - assert "Server initialization completed successfully!" in info_calls[-1] - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_exception_details_logged(self, mock_logger, mock_initialize_tools): - """ - Test that exception details are properly logged. - - This test verifies that: - 1. The specific exception message is included in error logs - 2. Both error and warning messages are logged on failure - """ - # Setup - specific_error_message = "Specific tool configuration error occurred" - mock_initialize_tools.side_effect = ValueError(specific_error_message) - - # Execute - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call_args = mock_logger.error.call_args[0][0] - assert specific_error_message in error_call_args - assert "Server initialization failed:" in error_call_args - - mock_logger.warning.assert_called_once() - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_no_exception_propagation(self, mock_logger, mock_initialize_tools): - """ - Test that exceptions during initialization do not propagate. - - This test verifies that: - 1. Even when initialize_tools_on_startup fails, no exception is raised - 2. This allows the server to continue starting up - """ - # Setup - mock_initialize_tools.side_effect = RuntimeError( - "Critical initialization error") - - # Execute and Assert - should not raise any exception - try: - await startup_initialization() - except Exception as e: - pytest.fail( - f"startup_initialization should not raise exceptions, but raised: {e}") - - # Verify that error handling occurred - mock_logger.error.assert_called_once() - mock_logger.warning.assert_called_once() - class TestMainServiceModuleIntegration: """Integration tests for runtime_service module dependencies""" @@ -401,30 +143,6 @@ def test_logging_configuration_called_on_import(self, mock_configure_es, mock_co # In a real scenario, you might need to reload the module to test this properly pass # The actual verification would depend on how the test runner handles imports - @patch('runtime_service.APP_VERSION', 'test_version_1.2.3') - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_with_custom_version(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization with a custom APP_VERSION. - - This test verifies that: - 1. The custom version is properly logged - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - version_logged = any( - "test_version_1.2.3" in str(call.args[0]) - for call in mock_logger.info.call_args_list - if len(call.args) > 0 - ) - assert version_logged, "Custom APP version should be logged" - if __name__ == '__main__': pytest.main() diff --git a/test/backend/utils/test_llm_utils.py b/test/backend/utils/test_llm_utils.py index 50857e91b..ce4ed98d0 100644 --- a/test/backend/utils/test_llm_utils.py +++ b/test/backend/utils/test_llm_utils.py @@ -266,9 +266,514 @@ def mock_callback(text): is_thinking = _process_thinking_tokens("", False, token_join, mock_callback) self.assertFalse(is_thinking) - self.assertEqual(token_join, [""]) + self.assertEqual(token_join, []) + self.assertEqual(callback_calls, []) + + def test_process_thinking_tokens_end_tag_without_starting(self): + """Test end tag when never in thinking mode - should clear token_join""" + token_join = ["Some", "content"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + is_thinking = _process_thinking_tokens("", False, token_join, mock_callback) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, []) self.assertEqual(callback_calls, [""]) + def test_process_thinking_tokens_end_tag_without_starting_no_callback(self): + """Test end tag when never in thinking mode without callback""" + token_join = ["Some", "content"] + + is_thinking = _process_thinking_tokens("", False, token_join, None) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, []) + + def test_process_thinking_tokens_end_tag_with_content_after(self): + """Test end tag followed by content in the same token""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + is_thinking = _process_thinking_tokens("World", True, token_join, mock_callback) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, ["Hello", "World"]) + self.assertEqual(callback_calls, ["HelloWorld"]) + + def test_process_thinking_tokens_start_tag_with_content_after(self): + """Test start tag followed by content in the same token""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + is_thinking = _process_thinking_tokens("thinking", False, token_join, mock_callback) + + self.assertTrue(is_thinking) + self.assertEqual(token_join, ["Hello"]) + self.assertEqual(callback_calls, []) + + def test_process_thinking_tokens_both_tags_in_same_token(self): + """Test both start and end tags in the same token""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + # When both tags are in the same token, end tag is processed first + # End tag clears token_join (since is_thinking=False), sets is_thinking=False, + # new_token becomes "World" (content after ) + # Then start tag check happens on "World", no match, so is_thinking stays False + # Then is_thinking check returns False, so "World" is added to token_join + is_thinking = _process_thinking_tokens( + "thinkingWorld", + False, + token_join, + mock_callback, + ) + + # After processing end tag: token_join cleared, is_thinking=False, new_token="World" + # Start tag check on "World": no match, is_thinking stays False + # Then "World" is added to token_join + # Note: When end tag clears token_join, callback("") is called, but empty string is not added to token_join + self.assertFalse(is_thinking) + self.assertEqual(token_join, ["World"]) + self.assertEqual(callback_calls, ["", "World"]) + + def test_process_thinking_tokens_new_token_empty_after_processing(self): + """Test when new_token becomes empty after processing tags""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + # End tag with no content after + is_thinking = _process_thinking_tokens("", True, token_join, mock_callback) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, ["Hello"]) + self.assertEqual(callback_calls, []) + + +class TestCallLLMForSystemPromptExtended(unittest.TestCase): + """Extended tests for call_llm_for_system_prompt to achieve 100% coverage""" + + def setUp(self): + self.test_model_id = 1 + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_callback( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with callback""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + callback=mock_callback, + ) + + self.assertEqual(result, "Generated prompt") + self.assertEqual(len(callback_calls), 1) + self.assertEqual(callback_calls[0], "Generated prompt") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_reasoning_content( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with reasoning_content""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + mock_chunk.choices[0].delta.reasoning_content = "Some reasoning" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_multiple_chunks( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with multiple chunks""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk1 = MagicMock() + mock_chunk1.choices = [MagicMock()] + mock_chunk1.choices[0].delta.content = "Generated " + mock_chunk1.choices[0].delta.reasoning_content = None + + mock_chunk2 = MagicMock() + mock_chunk2.choices = [MagicMock()] + mock_chunk2.choices[0].delta.content = "prompt" + mock_chunk2.choices[0].delta.reasoning_content = None + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk1, mock_chunk2] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_none_content( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with delta.content as None""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = None + mock_chunk.choices[0].delta.reasoning_content = "Some reasoning" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_thinking_tags( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with thinking tags""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk1 = MagicMock() + mock_chunk1.choices = [MagicMock()] + mock_chunk1.choices[0].delta.content = "Start " + mock_chunk1.choices[0].delta.reasoning_content = None + + mock_chunk2 = MagicMock() + mock_chunk2.choices = [MagicMock()] + mock_chunk2.choices[0].delta.content = "thinking" + mock_chunk2.choices[0].delta.reasoning_content = None + + mock_chunk3 = MagicMock() + mock_chunk3.choices = [MagicMock()] + mock_chunk3.choices[0].delta.content = " End" + mock_chunk3.choices[0].delta.reasoning_content = None + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [ + mock_chunk1, + mock_chunk2, + mock_chunk3, + ] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + # chunk1: "Start " -> added to token_join + # chunk2: "thinking" -> + # end tag clears token_join (since is_thinking=False), new_token becomes "" + # chunk3: " End" -> added to token_join + # Final result should be " End" (chunk1 content was cleared by chunk2's end tag) + self.assertEqual(result, " End") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + @patch('backend.utils.llm_utils.logger') + def test_call_llm_for_system_prompt_empty_result_with_tokens( + self, + mock_logger, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with empty result but processed tokens""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + # Content that will be filtered out by thinking tags + mock_chunk.choices[0].delta.content = "all content" + mock_chunk.choices[0].delta.reasoning_content = None + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "") + # Verify warning was logged + mock_logger.warning.assert_called_once() + call_args = mock_logger.warning.call_args[0][0] + self.assertIn("empty but", call_args) + self.assertIn("content tokens were processed", call_args) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_tenant_id( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with tenant_id""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + tenant_id="test-tenant", + ) + + self.assertEqual(result, "Generated prompt") + mock_get_model_by_id.assert_called_once_with( + model_id=self.test_model_id, + tenant_id="test-tenant", + ) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_none_model_config( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with None model config""" + mock_get_model_by_id.return_value = None + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + # Verify OpenAIModel was called with empty strings when model_config is None + mock_openai.assert_called_once_with( + model_id="", + api_base="", + api_key="", + temperature=0.3, + top_p=0.95, + ) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + @patch('backend.utils.llm_utils.logger') + def test_call_llm_for_system_prompt_reasoning_content_logging( + self, + mock_logger, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt logs when reasoning_content is received""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + mock_chunk.choices[0].delta.reasoning_content = "Some reasoning" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + # Verify debug log was called for reasoning_content + mock_logger.debug.assert_called_once() + call_args = mock_logger.debug.call_args[0][0] + self.assertIn("reasoning_content", call_args) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + @patch('backend.utils.llm_utils.logger') + def test_call_llm_for_system_prompt_exception_logging( + self, + mock_logger, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt exception handling and logging""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.side_effect = Exception("LLM error") + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + with self.assertRaises(Exception) as context: + call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertIn("LLM error", str(context.exception)) + # Verify error was logged + mock_logger.error.assert_called_once() + call_args = mock_logger.error.call_args[0][0] + self.assertIn("Failed to generate prompt", call_args) + if __name__ == '__main__': unittest.main()