From 357f0ddc8ed6e794e173f546acebd50082f855d0 Mon Sep 17 00:00:00 2001 From: MoeexT Date: Sat, 27 Jun 2026 10:01:28 +0800 Subject: [PATCH 01/16] :sparkle: manage labels for agent tools --- backend/apps/tool_config_app.py | 47 +- backend/consts/model.py | 1 + backend/database/db_models.py | 1 + backend/database/tool_db.py | 53 +- .../services/tool_configuration_service.py | 17 +- ...v2.3.0_0624_add_labels_to_ag_tool_info.sql | 41 + docker/init.sql | 1969 +++++++++++++++++ .../agents/components/AgentConfigComp.tsx | 79 +- .../components/agentConfig/ToolManagement.tsx | 808 ++----- .../agentConfig/tool/LabelManagementModal.tsx | 146 ++ .../agentConfig/tool/SelectToolsDialog.tsx | 426 ++++ .../agentConfig/tool/ToolConfigModal.tsx | 1 + frontend/hooks/agent/useToolList.ts | 22 +- frontend/public/locales/en/common.json | 16 + frontend/public/locales/zh/common.json | 16 + frontend/services/agentConfigService.ts | 5 + frontend/services/api.ts | 1 + frontend/types/agentConfig.ts | 1 + sdk/nexent/core/agents/agent_model.py | 1 + 19 files changed, 3021 insertions(+), 630 deletions(-) create mode 100644 deploy/sql/migrations/v2.3.0_0624_add_labels_to_ag_tool_info.sql create mode 100644 docker/init.sql create mode 100644 frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx create mode 100644 frontend/app/[locale]/agents/components/agentConfig/tool/SelectToolsDialog.tsx diff --git a/backend/apps/tool_config_app.py b/backend/apps/tool_config_app.py index bfc8d5ca0..687b653cb 100644 --- a/backend/apps/tool_config_app.py +++ b/backend/apps/tool_config_app.py @@ -1,8 +1,8 @@ import logging from http import HTTPStatus -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List -from fastapi import APIRouter, Header, HTTPException, Body +from fastapi import APIRouter, Header, HTTPException, Body, Query from fastapi.responses import JSONResponse from consts.exceptions import MCPConnectionError, NotFoundException @@ -26,14 +26,17 @@ @router.get("/list") -async def list_tools_api(authorization: Optional[str] = Header(None)): +async def list_tools_api( + authorization: Optional[str] = Header(None), + labels: Optional[str] = Query(None, description="Comma-separated label strings to filter tools (OR match)") +): """ - List all system tools from PG dataset + List all system tools from PG dataset, optionally filtered by labels. """ try: _, tenant_id = get_current_user_id(authorization) - # now only admin can modify the tool, user_id is not used - return await list_all_tools(tenant_id=tenant_id) + label_list = [lbl.strip() for lbl in labels.split(",") if lbl.strip()] if labels else None + return await list_all_tools(tenant_id=tenant_id, labels=label_list) except Exception as e: logging.error(f"Failed to get tool info, error in: {str(e)}") raise HTTPException( @@ -278,3 +281,35 @@ async def delete_openapi_service_api( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=f"Failed to delete OpenAPI service: {str(e)}" ) + + +@router.put("/labels") +async def update_tool_labels_api( + tool_id: int = Body(..., embed=True), + labels: List[str] = Body(..., embed=True), + authorization: Optional[str] = Header(None) +): + """ + Update labels for a specific tool. Replaces all labels with the provided list. + """ + try: + user_id, tenant_id = get_current_user_id(authorization) + from database.tool_db import update_tool_labels + updated = update_tool_labels(tool_id, tenant_id, labels, user_id) + if not updated: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Tool not found or access denied" + ) + return JSONResponse( + status_code=HTTPStatus.OK, + content={"message": "Labels updated successfully", "status": "success", "labels": labels} + ) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to update tool labels: {e}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=f"Failed to update tool labels: {str(e)}" + ) diff --git a/backend/consts/model.py b/backend/consts/model.py index 1a0eacf7d..bdcde905e 100644 --- a/backend/consts/model.py +++ b/backend/consts/model.py @@ -627,6 +627,7 @@ class ToolInfo(BaseModel): usage: Optional[str] origin_name: Optional[str] = None category: Optional[str] = None + labels: Optional[List[str]] = None # used in Knowledge Summary request diff --git a/backend/database/db_models.py b/backend/database/db_models.py index 9c981d3c0..bdba2e0bb 100644 --- a/backend/database/db_models.py +++ b/backend/database/db_models.py @@ -419,6 +419,7 @@ class ToolInfo(TableBase): inputs = Column(String(2048), doc="Prompt tool inputs description") output_type = Column(String(100), doc="Prompt tool output description") category = Column(String(100), doc="Tool category description") + labels = Column(JSONB, default=[], doc="JSON array of label strings for filtering/grouping tools") is_available = Column( Boolean, doc="Whether the tool can be used under the current main service") diff --git a/backend/database/tool_db.py b/backend/database/tool_db.py index 907dfd012..18d04bde4 100644 --- a/backend/database/tool_db.py +++ b/backend/database/tool_db.py @@ -136,6 +136,54 @@ def query_tools_by_ids(tool_id_list: List[int]): return [as_dict(tool) for tool in tools] +def query_tools_by_labels(tenant_id: str, labels: List[str]): + """ + Query ToolInfo by labels using OR match (tool has ANY of the requested labels). + + Args: + tenant_id: Tenant ID for filtering + labels: List of label strings to filter by + + Returns: + List of ToolInfo dicts matching any of the given labels + """ + with get_db_session() as session: + query = session.query(ToolInfo).filter( + ToolInfo.delete_flag != 'Y', + ToolInfo.author == tenant_id, + ToolInfo.labels.op('?|')(labels) + ) + tools = query.all() + return [as_dict(tool) for tool in tools] + + +def update_tool_labels(tool_id: int, tenant_id: str, labels: List[str], user_id: str) -> bool: + """ + Update labels for a specific tool. Replaces all existing labels. + + Args: + tool_id: Tool ID to update + tenant_id: Tenant ID for access control + labels: New list of label strings + user_id: User performing the update + + Returns: + True if updated, False if tool not found or access denied + """ + with get_db_session() as session: + tool = session.query(ToolInfo).filter( + ToolInfo.tool_id == tool_id, + ToolInfo.author == tenant_id, + ToolInfo.delete_flag != 'Y' + ).first() + if not tool: + return False + tool.labels = labels + tool.updated_by = user_id + session.flush() + return True + + def query_all_enabled_tool_instances(agent_id: int, tenant_id: str, version_no: int = 0): """ Query enabled ToolInstance in the database. @@ -243,6 +291,9 @@ def update_tool_table_from_scan_tool_list(tenant_id: str, user_id: str, tool_lis # by tool name and source to update the existing tool existing_tool = existing_tool_dict[key] for key, value in filtered_tool_data.items(): + # Preserve user-set labels; only overwrite if new labels are explicitly provided + if key == "labels" and not value: + continue setattr(existing_tool, key, value) existing_tool.updated_by = user_id existing_tool.is_available = is_available @@ -394,4 +445,4 @@ def search_last_tool_instance_by_tool_id(tool_id: int, tenant_id: str, user_id: ToolInstance.delete_flag != 'Y' ).order_by(ToolInstance.update_time.desc()) tool_instance = query.first() - return as_dict(tool_instance) if tool_instance else None \ No newline at end of file + return as_dict(tool_instance) if tool_instance else None diff --git a/backend/services/tool_configuration_service.py b/backend/services/tool_configuration_service.py index 43e212ec6..367bcb21b 100644 --- a/backend/services/tool_configuration_service.py +++ b/backend/services/tool_configuration_service.py @@ -30,6 +30,7 @@ check_tool_list_initialized, create_or_update_tool_by_tool_info, query_all_tools, + query_tools_by_labels, query_tool_instances_by_id, search_last_tool_instance_by_tool_id, update_tool_table_from_scan_tool_list, @@ -200,6 +201,7 @@ def get_local_tools() -> List[ToolInfo]: inputs=json.dumps(processed_inputs, ensure_ascii=False), output_type=getattr(tool_class, 'output_type'), category=getattr(tool_class, 'category'), + labels=getattr(tool_class, 'labels', None), class_name=tool_class.__name__, usage=None, origin_name=getattr(tool_class, 'name') @@ -245,7 +247,8 @@ def _build_tool_info_from_langchain(obj) -> ToolInfo: class_name=tool_name, usage=None, origin_name=tool_name, - category=None + category=None, + labels=None ) return tool_info @@ -486,11 +489,14 @@ async def update_tool_list(tenant_id: str, user_id: str): tool_list=local_tools+mcp_tools+langchain_tools) -async def list_all_tools(tenant_id: str): +async def list_all_tools(tenant_id: str, labels: Optional[List[str]] = None): """ - List all tools for a given tenant + List all tools for a given tenant, optionally filtered by labels (OR match). """ - tools_info = query_all_tools(tenant_id) + if labels: + tools_info = query_tools_by_labels(tenant_id, labels) + else: + tools_info = query_all_tools(tenant_id) # Get description_zh from SDK for local tools (not persisted to DB) local_tool_descriptions = get_local_tools_description_zh() @@ -555,7 +561,8 @@ async def list_all_tools(tenant_id: str): "usage": tool.get("usage"), "params": tool.get("params", []), "inputs": inputs_str, - "category": tool.get("category") + "category": tool.get("category"), + "labels": tool.get("labels", []) } formatted_tools.append(formatted_tool) return formatted_tools diff --git a/deploy/sql/migrations/v2.3.0_0624_add_labels_to_ag_tool_info.sql b/deploy/sql/migrations/v2.3.0_0624_add_labels_to_ag_tool_info.sql new file mode 100644 index 000000000..be0ddcf2c --- /dev/null +++ b/deploy/sql/migrations/v2.3.0_0624_add_labels_to_ag_tool_info.sql @@ -0,0 +1,41 @@ +-- Add labels column to ag_tool_info_t table for tool filtering/grouping +ALTER TABLE nexent.ag_tool_info_t +ADD COLUMN IF NOT EXISTS labels JSONB DEFAULT '[]'::jsonb; + +COMMENT ON COLUMN nexent.ag_tool_info_t.labels IS 'JSON array of label strings for filtering/grouping tools'; + +-- Seed built-in labels for well-known local tools. +-- These labels serve as suggested defaults and can be modified by users. +-- Keep in sync with: backend/consts/tool_labels.py + +-- Database tools +UPDATE nexent.ag_tool_info_t SET labels = '["database"]'::jsonb +WHERE name IN ('mysql_database', 'postgres_database', 'mssql_database'); + +-- File system tools +UPDATE nexent.ag_tool_info_t SET labels = '["file"]'::jsonb +WHERE name IN ('read_file', 'create_file', 'delete_file', 'create_directory', 'delete_directory', 'list_directory', 'move_item'); + +-- Search tools +UPDATE nexent.ag_tool_info_t SET labels = '["search"]'::jsonb +WHERE name IN ('tavily_search', 'exa_search', 'linkup_search', 'search_memory', 'knowledge_base_search'); + +-- Knowledge base tools +UPDATE nexent.ag_tool_info_t SET labels = '["knowledge-base"]'::jsonb +WHERE name IN ('dify_search', 'datamate_search', 'idata_search', 'haotian_search', 'aidp_search'); + +-- Multimodal / analyze tools +UPDATE nexent.ag_tool_info_t SET labels = '["multimodal"]'::jsonb +WHERE name IN ('analyze_image', 'analyze_audio', 'analyze_video', 'analyze_text_file'); + +-- Email tools +UPDATE nexent.ag_tool_info_t SET labels = '["email"]'::jsonb +WHERE name IN ('get_email', 'send_email'); + +-- Memory tools +UPDATE nexent.ag_tool_info_t SET labels = '["memory"]'::jsonb +WHERE name IN ('store_memory'); + +-- Terminal tools +UPDATE nexent.ag_tool_info_t SET labels = '["terminal"]'::jsonb +WHERE name IN ('terminal'); diff --git a/docker/init.sql b/docker/init.sql new file mode 100644 index 000000000..c693fcd4a --- /dev/null +++ b/docker/init.sql @@ -0,0 +1,1969 @@ +-- 1. Create custom Schema (if not exists) +CREATE SCHEMA IF NOT EXISTS nexent; + +-- 2. Switch to the Schema (subsequent operations default to this Schema) +SET search_path TO nexent; + +CREATE TABLE IF NOT EXISTS "conversation_message_t" ( + "message_id" SERIAL, + "conversation_id" int4, + "message_index" int4, + "message_role" varchar(30) COLLATE "pg_catalog"."default", + "message_content" varchar COLLATE "pg_catalog"."default", + "minio_files" varchar, + "opinion_flag" varchar(1), + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N'::character varying, + "create_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "update_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "created_by" varchar(100) COLLATE "pg_catalog"."default", + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + CONSTRAINT "conversation_message_t_pk" PRIMARY KEY ("message_id") +); +ALTER TABLE "conversation_message_t" OWNER TO "root"; +COMMENT ON COLUMN "conversation_message_t"."conversation_id" IS 'Formal foreign key, used to associate with the conversation'; +COMMENT ON COLUMN "conversation_message_t"."message_index" IS 'Sequence number, used for frontend display sorting'; +COMMENT ON COLUMN "conversation_message_t"."message_role" IS 'Role sending the message, such as system, assistant, user'; +COMMENT ON COLUMN "conversation_message_t"."message_content" IS 'Complete content of the message'; +COMMENT ON COLUMN "conversation_message_t"."minio_files" IS 'Images or documents uploaded by users in the chat interface, stored as a list'; +COMMENT ON COLUMN "conversation_message_t"."opinion_flag" IS 'User feedback on the conversation, enum value Y represents positive, N represents negative'; +COMMENT ON COLUMN "conversation_message_t"."delete_flag" IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN "conversation_message_t"."create_time" IS 'Creation time, audit field'; +COMMENT ON COLUMN "conversation_message_t"."update_time" IS 'Update time, audit field'; +COMMENT ON COLUMN "conversation_message_t"."created_by" IS 'Creator ID, audit field'; +COMMENT ON COLUMN "conversation_message_t"."updated_by" IS 'Last updater ID, audit field'; +COMMENT ON TABLE "conversation_message_t" IS 'Carries specific response message content in conversations'; + +CREATE TABLE IF NOT EXISTS "conversation_message_unit_t" ( + "unit_id" SERIAL, + "message_id" int4, + "conversation_id" int4, + "unit_index" int4, + "unit_type" varchar(100) COLLATE "pg_catalog"."default", + "unit_content" varchar COLLATE "pg_catalog"."default", + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N'::character varying, + "create_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "update_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + "created_by" varchar(100) COLLATE "pg_catalog"."default", + CONSTRAINT "conversation_message_unit_t_pk" PRIMARY KEY ("unit_id") +); +ALTER TABLE "conversation_message_unit_t" OWNER TO "root"; +COMMENT ON COLUMN "conversation_message_unit_t"."message_id" IS 'Formal foreign key, used to associate with the message'; +COMMENT ON COLUMN "conversation_message_unit_t"."conversation_id" IS 'Formal foreign key, used to associate with the conversation'; +COMMENT ON COLUMN "conversation_message_unit_t"."unit_index" IS 'Sequence number, used for frontend display sorting'; +COMMENT ON COLUMN "conversation_message_unit_t"."unit_type" IS 'Type of minimum response unit'; +COMMENT ON COLUMN "conversation_message_unit_t"."unit_content" IS 'Complete content of the minimum response unit'; +COMMENT ON COLUMN "conversation_message_unit_t"."delete_flag" IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN "conversation_message_unit_t"."create_time" IS 'Creation time, audit field'; +COMMENT ON COLUMN "conversation_message_unit_t"."update_time" IS 'Update time, audit field'; +COMMENT ON COLUMN "conversation_message_unit_t"."updated_by" IS 'Last updater ID, audit field'; +COMMENT ON COLUMN "conversation_message_unit_t"."created_by" IS 'Creator ID, audit field'; +COMMENT ON TABLE "conversation_message_unit_t" IS 'Carries agent output content in each message'; + +CREATE TABLE IF NOT EXISTS "conversation_record_t" ( + "conversation_id" SERIAL, + "conversation_title" varchar(100) COLLATE "pg_catalog"."default", + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N'::character varying, + "update_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "create_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + "created_by" varchar(100) COLLATE "pg_catalog"."default", + CONSTRAINT "conversation_record_t_pk" PRIMARY KEY ("conversation_id") +); +ALTER TABLE "conversation_record_t" OWNER TO "root"; +COMMENT ON COLUMN "conversation_record_t"."conversation_title" IS 'Conversation title'; +COMMENT ON COLUMN "conversation_record_t"."delete_flag" IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN "conversation_record_t"."update_time" IS 'Update time, audit field'; +COMMENT ON COLUMN "conversation_record_t"."create_time" IS 'Creation time, audit field'; +COMMENT ON COLUMN "conversation_record_t"."updated_by" IS 'Last updater ID, audit field'; +COMMENT ON COLUMN "conversation_record_t"."created_by" IS 'Creator ID, audit field'; +COMMENT ON TABLE "conversation_record_t" IS 'Overall information of Q&A conversations'; + +CREATE TABLE IF NOT EXISTS "conversation_source_image_t" ( + "image_id" SERIAL, + "conversation_id" int4, + "message_id" int4, + "unit_id" int4, + "image_url" varchar COLLATE "pg_catalog"."default", + "cite_index" int4, + "search_type" varchar(100) COLLATE "pg_catalog"."default", + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N'::character varying, + "create_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "update_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "created_by" varchar(100) COLLATE "pg_catalog"."default", + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + CONSTRAINT "conversation_source_image_t_pk" PRIMARY KEY ("image_id") +); +ALTER TABLE "conversation_source_image_t" OWNER TO "root"; +COMMENT ON COLUMN "conversation_source_image_t"."conversation_id" IS 'Formal foreign key, used to associate with the conversation of the search source'; +COMMENT ON COLUMN "conversation_source_image_t"."message_id" IS 'Formal foreign key, used to associate with the conversation message of the search source'; +COMMENT ON COLUMN "conversation_source_image_t"."unit_id" IS 'Formal foreign key, used to associate with the minimum message unit of the search source (if any)'; +COMMENT ON COLUMN "conversation_source_image_t"."image_url" IS 'URL address of the image'; +COMMENT ON COLUMN "conversation_source_image_t"."cite_index" IS '[Reserved] Citation sequence number, used for precise tracing'; +COMMENT ON COLUMN "conversation_source_image_t"."search_type" IS '[Reserved] Search source type, used to distinguish the search tool used for this record, optional values web/local'; +COMMENT ON COLUMN "conversation_source_image_t"."delete_flag" IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN "conversation_source_image_t"."create_time" IS 'Creation time, audit field'; +COMMENT ON COLUMN "conversation_source_image_t"."update_time" IS 'Update time, audit field'; +COMMENT ON COLUMN "conversation_source_image_t"."created_by" IS 'Creator ID, audit field'; +COMMENT ON COLUMN "conversation_source_image_t"."updated_by" IS 'Last updater ID, audit field'; +COMMENT ON TABLE "conversation_source_image_t" IS 'Carries search image source information for conversation messages'; + +CREATE TABLE IF NOT EXISTS "conversation_source_search_t" ( + "search_id" SERIAL, + "unit_id" int4, + "message_id" int4, + "conversation_id" int4, + "source_type" varchar(100) COLLATE "pg_catalog"."default", + "source_title" varchar(400) COLLATE "pg_catalog"."default", + "source_location" varchar(400) COLLATE "pg_catalog"."default", + "source_content" varchar COLLATE "pg_catalog"."default", + "score_overall" numeric(7,6), + "score_accuracy" numeric(7,6), + "score_semantic" numeric(7,6), + "published_date" timestamp(0), + "cite_index" int4, + "search_type" varchar(100) COLLATE "pg_catalog"."default", + "tool_sign" varchar(30) COLLATE "pg_catalog"."default", + "create_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "update_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N'::character varying, + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + "created_by" varchar(100) COLLATE "pg_catalog"."default", + CONSTRAINT "conversation_source_search_t_pk" PRIMARY KEY ("search_id") +); +ALTER TABLE "conversation_source_search_t" OWNER TO "root"; +COMMENT ON COLUMN "conversation_source_search_t"."unit_id" IS 'Formal foreign key, used to associate with the minimum message unit of the search source (if any)'; +COMMENT ON COLUMN "conversation_source_search_t"."message_id" IS 'Formal foreign key, used to associate with the conversation message of the search source'; +COMMENT ON COLUMN "conversation_source_search_t"."conversation_id" IS 'Formal foreign key, used to associate with the conversation of the search source'; +COMMENT ON COLUMN "conversation_source_search_t"."source_type" IS 'Source type, used to distinguish if source_location is URL or path, optional values url/text'; +COMMENT ON COLUMN "conversation_source_search_t"."source_title" IS 'Title or filename of the search source'; +COMMENT ON COLUMN "conversation_source_search_t"."source_location" IS 'URL link or file path of the search source'; +COMMENT ON COLUMN "conversation_source_search_t"."source_content" IS 'Original text of the search source'; +COMMENT ON COLUMN "conversation_source_search_t"."score_overall" IS 'Overall similarity score between source and user query, calculated as weighted average of details'; +COMMENT ON COLUMN "conversation_source_search_t"."score_accuracy" IS 'Accuracy score'; +COMMENT ON COLUMN "conversation_source_search_t"."score_semantic" IS 'Semantic similarity score'; +COMMENT ON COLUMN "conversation_source_search_t"."published_date" IS 'Upload date of local file or network search date'; +COMMENT ON COLUMN "conversation_source_search_t"."cite_index" IS 'Citation sequence number, used for precise tracing'; +COMMENT ON COLUMN "conversation_source_search_t"."search_type" IS 'Search source type, specifically describes the search tool used for this record, optional values web_search/knowledge_base_search'; +COMMENT ON COLUMN "conversation_source_search_t"."tool_sign" IS 'Simple tool identifier, used to distinguish index sources in large model output summary text'; +COMMENT ON COLUMN "conversation_source_search_t"."create_time" IS 'Creation time, audit field'; +COMMENT ON COLUMN "conversation_source_search_t"."update_time" IS 'Update time, audit field'; +COMMENT ON COLUMN "conversation_source_search_t"."delete_flag" IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN "conversation_source_search_t"."updated_by" IS 'Last updater ID, audit field'; +COMMENT ON COLUMN "conversation_source_search_t"."created_by" IS 'Creator ID, audit field'; +COMMENT ON TABLE "conversation_source_search_t" IS 'Carries search text source information referenced in conversation response messages'; + +CREATE TABLE IF NOT EXISTS "model_record_t" ( + "model_id" SERIAL, + "model_repo" varchar(100) COLLATE "pg_catalog"."default", + "model_name" varchar(100) COLLATE "pg_catalog"."default" NOT NULL, + "model_factory" varchar(100) COLLATE "pg_catalog"."default", + "model_type" varchar(100) COLLATE "pg_catalog"."default", + "api_key" varchar(500) COLLATE "pg_catalog"."default", + "base_url" varchar(500) COLLATE "pg_catalog"."default", + "max_tokens" int4, + "used_token" int4, + "expected_chunk_size" int4, + "maximum_chunk_size" int4, + "chunk_batch" int4, + "display_name" varchar(100) COLLATE "pg_catalog"."default", + "connect_status" varchar(100) COLLATE "pg_catalog"."default", + "ssl_verify" boolean DEFAULT true, + "create_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N'::character varying, + "update_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + "created_by" varchar(100) COLLATE "pg_catalog"."default", + "tenant_id" varchar(100) COLLATE "pg_catalog"."default" DEFAULT 'tenant_id', + "model_appid" varchar(100) COLLATE "pg_catalog"."default" DEFAULT '', + "access_token" varchar(100) COLLATE "pg_catalog"."default" DEFAULT '', + "concurrency_limit" INTEGER DEFAULT NULL, + "timeout_seconds" INTEGER DEFAULT 120, + CONSTRAINT "nexent_models_t_pk" PRIMARY KEY ("model_id") +); +ALTER TABLE "model_record_t" OWNER TO "root"; +COMMENT ON COLUMN "model_record_t"."model_id" IS 'Model ID, unique primary key'; +COMMENT ON COLUMN "model_record_t"."model_repo" IS 'Model path address'; +COMMENT ON COLUMN "model_record_t"."model_name" IS 'Model name'; +COMMENT ON COLUMN "model_record_t"."model_factory" IS 'Model manufacturer, determines specific format of api-key and model response. Currently defaults to OpenAI-API-Compatible'; +COMMENT ON COLUMN "model_record_t"."model_type" IS 'Model type, e.g. chat, embedding, rerank, tts, asr'; +COMMENT ON COLUMN "model_record_t"."api_key" IS 'Model API key, used for authentication for some models'; +COMMENT ON COLUMN "model_record_t"."base_url" IS 'Base URL address, used for requesting remote model services'; +COMMENT ON COLUMN "model_record_t"."max_tokens" IS 'Maximum available tokens for the model'; +COMMENT ON COLUMN "model_record_t"."used_token" IS 'Number of tokens already used by the model in Q&A'; +COMMENT ON COLUMN "model_record_t".expected_chunk_size IS 'Expected chunk size for embedding models, used during document chunking'; +COMMENT ON COLUMN "model_record_t".maximum_chunk_size IS 'Maximum chunk size for embedding models, used during document chunking'; +COMMENT ON COLUMN "model_record_t"."display_name" IS 'Model name displayed directly in frontend, customized by user'; +COMMENT ON COLUMN "model_record_t"."connect_status" IS 'Model connectivity status from last check, optional values: "检测中"、"可用"、"不可用"'; +COMMENT ON COLUMN "model_record_t"."ssl_verify" IS 'Whether to verify SSL certificates when connecting to this model API. Default is true. Set to false for local services without SSL support.'; +COMMENT ON COLUMN "model_record_t"."create_time" IS 'Creation time, audit field'; +COMMENT ON COLUMN "model_record_t"."delete_flag" IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN "model_record_t"."update_time" IS 'Update time, audit field'; +COMMENT ON COLUMN "model_record_t"."updated_by" IS 'Last updater ID, audit field'; +COMMENT ON COLUMN "model_record_t"."created_by" IS 'Creator ID, audit field'; +COMMENT ON COLUMN "model_record_t"."tenant_id" IS 'Tenant ID for filtering'; +COMMENT ON COLUMN "model_record_t"."model_appid" IS 'Application ID for model authentication.'; +COMMENT ON COLUMN "model_record_t"."access_token" IS 'Access token for model authentication.'; +COMMENT ON COLUMN "model_record_t"."concurrency_limit" IS 'Maximum concurrent requests for this model. Default is NULL (unlimited).'; +COMMENT ON COLUMN "model_record_t"."timeout_seconds" IS 'Request timeout in seconds for this model. Default is 120 seconds.'; +COMMENT ON TABLE "model_record_t" IS 'List of models defined by users in the configuration page'; + +INSERT INTO "nexent"."model_record_t" ("model_repo", "model_name", "model_factory", "model_type", "api_key", "base_url", "max_tokens", "used_token", "display_name", "connect_status") VALUES ('', 'volcano_tts', 'OpenAI-API-Compatible', 'tts', '', '', 0, 0, 'volcano_tts', 'unavailable'); +INSERT INTO "nexent"."model_record_t" ("model_repo", "model_name", "model_factory", "model_type", "api_key", "base_url", "max_tokens", "used_token", "display_name", "connect_status") VALUES ('', 'volcano_stt', 'OpenAI-API-Compatible', 'stt', '', '', 0, 0, 'volcano_stt', 'unavailable'); + +CREATE TABLE IF NOT EXISTS "knowledge_record_t" ( + "knowledge_id" SERIAL, + "index_name" varchar(100) COLLATE "pg_catalog"."default", + "knowledge_name" varchar(100) COLLATE "pg_catalog"."default", + "knowledge_describe" varchar(3000) COLLATE "pg_catalog"."default", + "tenant_id" varchar(100) COLLATE "pg_catalog"."default", + "knowledge_sources" varchar(100) COLLATE "pg_catalog"."default", + "embedding_model_name" varchar(200) COLLATE "pg_catalog"."default", + "embedding_model_id" INTEGER, + "group_ids" varchar, + "ingroup_permission" varchar(30), + "create_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "update_time" timestamp(0) DEFAULT CURRENT_TIMESTAMP, + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N'::character varying, + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + "created_by" varchar(100) COLLATE "pg_catalog"."default", + "summary_frequency" varchar(10) COLLATE "pg_catalog"."default", + "last_summary_time" timestamp(0), + "last_doc_update_time" timestamp(0), + "preserve_source_file" boolean NOT NULL DEFAULT true, + CONSTRAINT "knowledge_record_t_pk" PRIMARY KEY ("knowledge_id") +); +ALTER TABLE "knowledge_record_t" OWNER TO "root"; +COMMENT ON COLUMN "knowledge_record_t"."knowledge_id" IS 'Knowledge base ID, unique primary key'; +COMMENT ON COLUMN "knowledge_record_t"."index_name" IS 'Internal Elasticsearch index name'; +COMMENT ON COLUMN "knowledge_record_t"."knowledge_name" IS 'User-facing knowledge base name (display name), mapped to internal index_name'; +COMMENT ON COLUMN "knowledge_record_t"."knowledge_describe" IS 'Knowledge base description'; +COMMENT ON COLUMN "knowledge_record_t"."tenant_id" IS 'Tenant ID'; +COMMENT ON COLUMN "knowledge_record_t"."knowledge_sources" IS 'Knowledge base sources'; +COMMENT ON COLUMN "knowledge_record_t"."embedding_model_name" IS 'Embedding model name, used to record the embedding model used by the knowledge base'; +COMMENT ON COLUMN "knowledge_record_t"."embedding_model_id" IS 'Embedding model ID, foreign key reference to model_record_t.model_id'; +COMMENT ON COLUMN "knowledge_record_t"."group_ids" IS 'Knowledge base group IDs list'; +COMMENT ON COLUMN "knowledge_record_t"."ingroup_permission" IS 'In-group permission: EDIT, READ_ONLY, PRIVATE'; +COMMENT ON COLUMN "knowledge_record_t"."create_time" IS 'Creation time, audit field'; +COMMENT ON COLUMN "knowledge_record_t"."update_time" IS 'Update time, audit field'; +COMMENT ON COLUMN "knowledge_record_t"."delete_flag" IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN "knowledge_record_t"."updated_by" IS 'User who last updated the record, audit field'; +COMMENT ON COLUMN "knowledge_record_t"."created_by" IS 'User who created the record, audit field'; +COMMENT ON COLUMN "knowledge_record_t"."summary_frequency" IS 'Auto-summary frequency: 1h, 3h, 6h, 1d, 1w, or NULL (disabled)'; +COMMENT ON COLUMN "knowledge_record_t"."last_summary_time" IS 'Timestamp of last summary generation'; +COMMENT ON COLUMN "knowledge_record_t"."last_doc_update_time" IS 'Timestamp of last document add/delete operation, used for auto-summary optimization to skip unnecessary summary regeneration'; +COMMENT ON COLUMN "knowledge_record_t"."preserve_source_file" IS 'Whether to preserve uploaded source documents after vectorization'; +COMMENT ON COLUMN "knowledge_record_t"."updated_by" IS 'Last updater ID, audit field'; +COMMENT ON COLUMN "knowledge_record_t"."created_by" IS 'Creator ID, audit field'; +COMMENT ON TABLE "knowledge_record_t" IS 'Records knowledge base description and status information'; + +-- Create the ag_tool_info_t table +CREATE TABLE IF NOT EXISTS nexent.ag_tool_info_t ( + tool_id SERIAL PRIMARY KEY NOT NULL, + name VARCHAR(100), + origin_name VARCHAR(100), + class_name VARCHAR(100), + description VARCHAR, + source VARCHAR(100), + author VARCHAR(100), + usage VARCHAR(100), + params JSON, + inputs VARCHAR, + output_type VARCHAR(100), + category VARCHAR(100), + labels JSONB DEFAULT '[]'::jsonb, + is_available BOOLEAN DEFAULT FALSE, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +-- Trigger to update update_time when the record is modified +CREATE OR REPLACE FUNCTION update_ag_tool_info_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_ag_tool_info_update_time_trigger +BEFORE UPDATE ON nexent.ag_tool_info_t +FOR EACH ROW +EXECUTE FUNCTION update_ag_tool_info_update_time(); + +-- Add comment to the table +COMMENT ON TABLE nexent.ag_tool_info_t IS 'Information table for prompt tools'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_tool_info_t.tool_id IS 'ID'; +COMMENT ON COLUMN nexent.ag_tool_info_t.name IS 'Unique key name'; +COMMENT ON COLUMN nexent.ag_tool_info_t.class_name IS 'Tool class name, used when the tool is instantiated'; +COMMENT ON COLUMN nexent.ag_tool_info_t.description IS 'Prompt tool description'; +COMMENT ON COLUMN nexent.ag_tool_info_t.source IS 'Source'; +COMMENT ON COLUMN nexent.ag_tool_info_t.author IS 'Tool author'; +COMMENT ON COLUMN nexent.ag_tool_info_t.usage IS 'Usage'; +COMMENT ON COLUMN nexent.ag_tool_info_t.params IS 'Tool parameter information (json)'; +COMMENT ON COLUMN nexent.ag_tool_info_t.inputs IS 'Prompt tool inputs description'; +COMMENT ON COLUMN nexent.ag_tool_info_t.output_type IS 'Prompt tool output description'; +COMMENT ON COLUMN nexent.ag_tool_info_t.is_available IS 'Whether the tool can be used under the current main service'; +COMMENT ON COLUMN nexent.ag_tool_info_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.ag_tool_info_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.ag_tool_info_t.created_by IS 'Creator'; +COMMENT ON COLUMN nexent.ag_tool_info_t.updated_by IS 'Updater'; +COMMENT ON COLUMN nexent.ag_tool_info_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +-- Create the ag_tenant_agent_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.ag_tenant_agent_t ( + agent_id SERIAL NOT NULL, + name VARCHAR(100), + display_name VARCHAR(100), + description VARCHAR, + business_description VARCHAR, + author VARCHAR(100), + model_name VARCHAR(100), + model_id INTEGER, + business_logic_model_name VARCHAR(100), + business_logic_model_id INTEGER, + prompt_template_id INTEGER, + prompt_template_name VARCHAR(100), + max_steps INTEGER, + duty_prompt TEXT, + constraint_prompt TEXT, + few_shots_prompt TEXT, + parent_agent_id INTEGER, + tenant_id VARCHAR(100), + group_ids VARCHAR, + enabled BOOLEAN DEFAULT FALSE, + is_new BOOLEAN DEFAULT FALSE, + provide_run_summary BOOLEAN DEFAULT FALSE, + enable_context_manager BOOLEAN DEFAULT FALSE, + verification_config JSONB, + version_no INTEGER DEFAULT 0 NOT NULL, + current_version_no INTEGER NULL, + ingroup_permission VARCHAR(30), + greeting_message TEXT, + example_questions JSONB, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N', + PRIMARY KEY (agent_id, version_no) +); + +-- Create a function to update the update_time column +CREATE OR REPLACE FUNCTION update_ag_tenant_agent_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create a trigger to call the function before each update +CREATE TRIGGER update_ag_tenant_agent_update_time_trigger +BEFORE UPDATE ON nexent.ag_tenant_agent_t +FOR EACH ROW +EXECUTE FUNCTION update_ag_tenant_agent_update_time(); +-- Add comments to the table +COMMENT ON TABLE nexent.ag_tenant_agent_t IS 'Information table for agents'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_tenant_agent_t.agent_id IS 'ID'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.name IS 'Agent name'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.display_name IS 'Agent display name'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.description IS 'Description'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.author IS 'Agent author'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.business_description IS 'Manually entered by the user to describe the entire business process'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.model_name IS '[DEPRECATED] Name of the model used, use model_id instead'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.model_id IS 'Model ID, foreign key reference to model_record_t.model_id'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.business_logic_model_name IS 'Model name used for business logic prompt generation'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.business_logic_model_id IS 'Model ID used for business logic prompt generation, foreign key reference to model_record_t.model_id'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.prompt_template_id IS 'Prompt template ID used for business logic prompt generation'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.prompt_template_name IS 'Prompt template name used for business logic prompt generation'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.max_steps IS 'Maximum number of steps'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.duty_prompt IS 'Duty prompt'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.constraint_prompt IS 'Constraint prompt'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.few_shots_prompt IS 'Few-shots prompt'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.parent_agent_id IS 'Parent Agent ID'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.tenant_id IS 'Belonging tenant'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.group_ids IS 'Agent group IDs list'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.enabled IS 'Enable flag'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.provide_run_summary IS 'Whether to provide the running summary to the manager agent'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.created_by IS 'Creator'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.updated_by IS 'Updater'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.is_new IS 'Whether this agent is marked as new for the user'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.current_version_no IS 'Current published version number. NULL means no version published yet'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.ingroup_permission IS 'In-group permission: EDIT, READ_ONLY, PRIVATE'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.enable_context_manager IS 'Whether to enable context management (compression) for this agent'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.verification_config IS 'Layered ReAct self-verification configuration'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.greeting_message IS 'Agent greeting message displayed on chat initial screen'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.example_questions IS 'List of example questions for starting a conversation with this agent'; + +-- Create index for is_new queries +CREATE INDEX IF NOT EXISTS idx_ag_tenant_agent_t_is_new +ON nexent.ag_tenant_agent_t (tenant_id, is_new) +WHERE delete_flag = 'N'; + +CREATE TABLE IF NOT EXISTS nexent.ag_prompt_template_t ( + template_id SERIAL PRIMARY KEY, + template_name VARCHAR(100) NOT NULL, + description VARCHAR(500), + template_type VARCHAR(50) NOT NULL DEFAULT 'agent_generate', + tenant_id VARCHAR(100) NOT NULL, + user_id VARCHAR(100) NOT NULL, + template_content_zh JSONB NOT NULL, + template_content_en JSONB, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.ag_prompt_template_t OWNER TO "root"; + +CREATE OR REPLACE FUNCTION update_ag_prompt_template_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_ag_prompt_template_update_time_trigger +BEFORE UPDATE ON nexent.ag_prompt_template_t +FOR EACH ROW +EXECUTE FUNCTION update_ag_prompt_template_update_time(); + +COMMENT ON TABLE nexent.ag_prompt_template_t IS 'Prompt template table for user-defined business logic generation prompts'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.template_id IS 'Prompt template ID'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.template_name IS 'Prompt template name'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.description IS 'Prompt template description'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.template_type IS 'Prompt template type'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.user_id IS 'User ID'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.template_content_zh IS 'Chinese prompt template content'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.template_content_en IS 'English prompt template content'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.created_by IS 'Creator'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.updated_by IS 'Updater'; +COMMENT ON COLUMN nexent.ag_prompt_template_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_prompt_template_user_name_active +ON nexent.ag_prompt_template_t (tenant_id, user_id, template_name) +WHERE delete_flag = 'N'; + +CREATE INDEX IF NOT EXISTS idx_ag_prompt_template_t_user +ON nexent.ag_prompt_template_t (tenant_id, user_id, template_type) +WHERE delete_flag = 'N'; + +INSERT INTO nexent.ag_prompt_template_t ( + template_id, + template_name, + description, + template_type, + tenant_id, + user_id, + template_content_zh, + template_content_en, + created_by, + updated_by, + delete_flag +) +VALUES ( + 0, + 'system_default', + 'System default prompt template', + 'agent_generate', + 'tenant_id', + 'user_id', + '{}'::jsonb, + '{}'::jsonb, + 'user_id', + 'user_id', + 'N' +) +ON CONFLICT (template_id) DO UPDATE SET + template_name = EXCLUDED.template_name, + description = EXCLUDED.description, + template_type = EXCLUDED.template_type, + tenant_id = EXCLUDED.tenant_id, + user_id = EXCLUDED.user_id, + template_content_zh = EXCLUDED.template_content_zh, + template_content_en = EXCLUDED.template_content_en, + updated_by = EXCLUDED.updated_by, + delete_flag = 'N'; + + +-- Create the ag_tool_instance_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.ag_tool_instance_t ( + tool_instance_id SERIAL NOT NULL, + tool_id INTEGER, + agent_id INTEGER, + params JSON, + user_id VARCHAR(100), + tenant_id VARCHAR(100), + enabled BOOLEAN DEFAULT FALSE, + version_no INTEGER DEFAULT 0 NOT NULL, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N', + PRIMARY KEY (tool_instance_id, version_no) +); + +-- Add comment to the table +COMMENT ON TABLE nexent.ag_tool_instance_t IS 'Information table for tenant tool configuration.'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_tool_instance_t.tool_instance_id IS 'ID'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.tool_id IS 'Tenant tool ID'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.agent_id IS 'Agent ID'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.params IS 'Parameter configuration'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.user_id IS 'User ID'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.enabled IS 'Enable flag'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.update_time IS 'Update time'; + +-- Create a function to update the update_time column +CREATE OR REPLACE FUNCTION update_ag_tool_instance_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Add comment to the function +COMMENT ON FUNCTION update_ag_tool_instance_update_time() IS 'Function to update the update_time column when a record in ag_tool_instance_t is updated'; + +-- Create a trigger to call the function before each update +CREATE TRIGGER update_ag_tool_instance_update_time_trigger +BEFORE UPDATE ON nexent.ag_tool_instance_t +FOR EACH ROW +EXECUTE FUNCTION update_ag_tool_instance_update_time(); + +-- Add comment to the trigger +COMMENT ON TRIGGER update_ag_tool_instance_update_time_trigger ON nexent.ag_tool_instance_t IS 'Trigger to call update_ag_tool_instance_update_time function before each update on ag_tool_instance_t table'; + +-- Create the tenant_config_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.tenant_config_t ( + tenant_config_id SERIAL PRIMARY KEY NOT NULL, + tenant_id VARCHAR(100), + user_id VARCHAR(100), + value_type VARCHAR(100), + config_key VARCHAR(100), + config_value TEXT, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +-- Add comment to the table +COMMENT ON TABLE nexent.tenant_config_t IS 'Tenant configuration information table'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.tenant_config_t.tenant_config_id IS 'ID'; +COMMENT ON COLUMN nexent.tenant_config_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.tenant_config_t.user_id IS 'User ID'; +COMMENT ON COLUMN nexent.tenant_config_t.value_type IS 'Value type'; +COMMENT ON COLUMN nexent.tenant_config_t.config_key IS 'Config key'; +COMMENT ON COLUMN nexent.tenant_config_t.config_value IS 'Config value'; +COMMENT ON COLUMN nexent.tenant_config_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.tenant_config_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.tenant_config_t.created_by IS 'Creator'; +COMMENT ON COLUMN nexent.tenant_config_t.updated_by IS 'Updater'; +COMMENT ON COLUMN nexent.tenant_config_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +-- Create a function to update the update_time column +CREATE OR REPLACE FUNCTION update_tenant_config_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create a trigger to call the function before each update +CREATE TRIGGER update_tenant_config_update_time_trigger +BEFORE UPDATE ON nexent.tenant_config_t +FOR EACH ROW +EXECUTE FUNCTION update_tenant_config_update_time(); + +-- Create the mcp_record_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.mcp_record_t ( + mcp_id SERIAL PRIMARY KEY NOT NULL, + tenant_id VARCHAR(100), + user_id VARCHAR(100), + mcp_name VARCHAR(100), + mcp_server VARCHAR(500), + status BOOLEAN DEFAULT NULL, + container_id VARCHAR(200) DEFAULT NULL, + authorization_token VARCHAR(500) DEFAULT NULL, + custom_headers JSON DEFAULT NULL, + source VARCHAR(30), + registry_json JSONB, + config_json JSON, + enabled BOOLEAN DEFAULT TRUE, + tags TEXT[], + description TEXT, + container_port INTEGER, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); +ALTER TABLE "mcp_record_t" OWNER TO "root"; +-- Add comment to the table +COMMENT ON TABLE nexent.mcp_record_t IS 'MCP (Model Context Protocol) records table'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.mcp_record_t.mcp_id IS 'MCP record ID, unique primary key'; +COMMENT ON COLUMN nexent.mcp_record_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.mcp_record_t.user_id IS 'User ID'; +COMMENT ON COLUMN nexent.mcp_record_t.mcp_name IS 'MCP name'; +COMMENT ON COLUMN nexent.mcp_record_t.mcp_server IS 'MCP server address'; +COMMENT ON COLUMN nexent.mcp_record_t.status IS 'MCP server connection status, true=connected, false=disconnected, null=unknown'; +COMMENT ON COLUMN nexent.mcp_record_t.container_id IS 'Docker container ID for MCP service, NULL for non-containerized MCP'; +COMMENT ON COLUMN nexent.mcp_record_t.authorization_token IS 'Authorization token for MCP server authentication (e.g., Bearer token)'; +COMMENT ON COLUMN nexent.mcp_record_t.custom_headers IS 'Custom HTTP headers as JSON object for MCP server requests'; +COMMENT ON COLUMN nexent.mcp_record_t.create_time IS 'Creation time, audit field'; +COMMENT ON COLUMN nexent.mcp_record_t.update_time IS 'Update time, audit field'; +COMMENT ON COLUMN nexent.mcp_record_t.created_by IS 'Creator ID, audit field'; +COMMENT ON COLUMN nexent.mcp_record_t.updated_by IS 'Last updater ID, audit field'; +COMMENT ON COLUMN nexent.mcp_record_t.delete_flag IS 'When deleted by user frontend, delete flag will be set to true, achieving soft delete effect. Optional values Y/N'; +COMMENT ON COLUMN nexent.mcp_record_t.source IS 'Source type: local/mcp_registry/community'; +COMMENT ON COLUMN nexent.mcp_record_t.registry_json IS 'Full MCP registry server.json snapshot'; +COMMENT ON COLUMN nexent.mcp_record_t.config_json IS 'MCP config data'; +COMMENT ON COLUMN nexent.mcp_record_t.enabled IS 'Enabled'; +COMMENT ON COLUMN nexent.mcp_record_t.tags IS 'Tags'; +COMMENT ON COLUMN nexent.mcp_record_t.description IS 'Description'; +COMMENT ON COLUMN nexent.mcp_record_t.container_port IS 'Host port bound for containerized MCP service'; + +-- Create a function to update the update_time column +CREATE OR REPLACE FUNCTION update_mcp_record_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Add comment to the function +COMMENT ON FUNCTION update_mcp_record_update_time() IS 'Function to update the update_time column when a record in mcp_record_t is updated'; + +-- Create a trigger to call the function before each update +CREATE TRIGGER update_mcp_record_update_time_trigger +BEFORE UPDATE ON nexent.mcp_record_t +FOR EACH ROW +EXECUTE FUNCTION update_mcp_record_update_time(); + +-- Add comment to the trigger +COMMENT ON TRIGGER update_mcp_record_update_time_trigger ON nexent.mcp_record_t IS 'Trigger to call update_mcp_record_update_time function before each update on mcp_record_t table'; + +-- Add indexes for common management queries +CREATE INDEX IF NOT EXISTS idx_mcp_record_t_tenant_delete + ON nexent.mcp_record_t (tenant_id, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_mcp_record_t_tenant_name + ON nexent.mcp_record_t (tenant_id, mcp_name, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_mcp_record_t_tenant_server + ON nexent.mcp_record_t (tenant_id, mcp_server, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_mcp_record_t_tags_gin + ON nexent.mcp_record_t USING GIN (tags); + +-- Create user tenant relationship table +CREATE TABLE IF NOT EXISTS nexent.user_tenant_t ( + user_tenant_id SERIAL PRIMARY KEY, + user_id VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + user_role VARCHAR(30) DEFAULT 'USER', + user_email VARCHAR(255), + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag CHAR(1) DEFAULT 'N', + UNIQUE(user_id, tenant_id) +); + +-- Add comment +COMMENT ON TABLE nexent.user_tenant_t IS 'User tenant relationship table'; +COMMENT ON COLUMN nexent.user_tenant_t.user_tenant_id IS 'User tenant relationship ID, primary key'; +COMMENT ON COLUMN nexent.user_tenant_t.user_id IS 'User ID'; +COMMENT ON COLUMN nexent.user_tenant_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.user_tenant_t.user_role IS 'User role: SUPER_ADMIN, ADMIN, DEV, USER'; +COMMENT ON COLUMN nexent.user_tenant_t.user_email IS 'User email address'; +COMMENT ON COLUMN nexent.user_tenant_t.create_time IS 'Create time'; +COMMENT ON COLUMN nexent.user_tenant_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.user_tenant_t.created_by IS 'Created by'; +COMMENT ON COLUMN nexent.user_tenant_t.updated_by IS 'Updated by'; +COMMENT ON COLUMN nexent.user_tenant_t.delete_flag IS 'Delete flag, Y/N'; + +-- Create the ag_agent_relation_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.ag_agent_relation_t ( + relation_id SERIAL NOT NULL, + selected_agent_id INTEGER, + parent_agent_id INTEGER, + tenant_id VARCHAR(100), + version_no INTEGER DEFAULT 0 NOT NULL, + selected_agent_version_no INTEGER, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N', + PRIMARY KEY (relation_id, version_no) +); + +-- Create a function to update the update_time column +CREATE OR REPLACE FUNCTION update_ag_agent_relation_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create a trigger to call the function before each update +CREATE TRIGGER update_ag_agent_relation_update_time_trigger +BEFORE UPDATE ON nexent.ag_agent_relation_t +FOR EACH ROW +EXECUTE FUNCTION update_ag_agent_relation_update_time(); + +-- Add comment to the table +COMMENT ON TABLE nexent.ag_agent_relation_t IS 'Agent parent-child relationship table'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_agent_relation_t.relation_id IS 'Relationship ID, primary key'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.selected_agent_id IS 'Selected agent ID'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.parent_agent_id IS 'Parent agent ID'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.selected_agent_version_no IS 'Pinned version of selected_agent_id. NULL = use child current published version at runtime (legacy/draft).'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.create_time IS 'Creation time, audit field'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.update_time IS 'Update time, audit field'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.created_by IS 'Creator ID, audit field'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.updated_by IS 'Last updater ID, audit field'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.delete_flag IS 'Delete flag, set to Y for soft delete, optional values Y/N'; + +-- Create user memory config table +CREATE TABLE IF NOT EXISTS "memory_user_config_t" ( + "config_id" SERIAL PRIMARY KEY NOT NULL, + "tenant_id" varchar(100) COLLATE "pg_catalog"."default", + "user_id" varchar(100) COLLATE "pg_catalog"."default", + "value_type" varchar(100) COLLATE "pg_catalog"."default", + "config_key" varchar(100) COLLATE "pg_catalog"."default", + "config_value" varchar(100) COLLATE "pg_catalog"."default", + "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP, + "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP, + "created_by" varchar(100) COLLATE "pg_catalog"."default", + "updated_by" varchar(100) COLLATE "pg_catalog"."default", + "delete_flag" varchar(1) COLLATE "pg_catalog"."default" DEFAULT 'N' +); + +COMMENT ON COLUMN "nexent"."memory_user_config_t"."config_id" IS 'ID'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."tenant_id" IS 'Tenant ID'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."user_id" IS 'User ID'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."value_type" IS 'Value type. Optional values: single/multi'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."config_key" IS 'Config key'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."config_value" IS 'Config value'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."create_time" IS 'Creation time'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."update_time" IS 'Update time'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."created_by" IS 'Creator'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."updated_by" IS 'Updater'; +COMMENT ON COLUMN "nexent"."memory_user_config_t"."delete_flag" IS 'Whether it is deleted. Optional values: Y/N'; + +COMMENT ON TABLE "nexent"."memory_user_config_t" IS 'User configuration of memory setting table'; + +CREATE OR REPLACE FUNCTION "update_memory_user_config_update_time"() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER "update_memory_user_config_update_time_trigger" +BEFORE UPDATE ON "nexent"."memory_user_config_t" +FOR EACH ROW +EXECUTE FUNCTION "update_memory_user_config_update_time"(); + + +-- 1. Create tenant_invitation_code_t table for invitation codes +CREATE TABLE IF NOT EXISTS nexent.tenant_invitation_code_t ( + invitation_id SERIAL PRIMARY KEY, + tenant_id VARCHAR(100) NOT NULL, + invitation_code VARCHAR(100) NOT NULL, + group_ids VARCHAR, -- int4 list + capacity INT4 NOT NULL DEFAULT 1, + expiry_date TIMESTAMP(6) WITHOUT TIME ZONE, + status VARCHAR(30) NOT NULL, + code_type VARCHAR(30) NOT NULL, + create_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + update_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +-- Add comments for tenant_invitation_code_t table +COMMENT ON TABLE nexent.tenant_invitation_code_t IS 'Tenant invitation code information table'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.invitation_id IS 'Invitation ID, primary key'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.tenant_id IS 'Tenant ID, foreign key'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.invitation_code IS 'Invitation code'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.group_ids IS 'Associated group IDs list'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.capacity IS 'Invitation code capacity'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.expiry_date IS 'Invitation code expiry date'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.status IS 'Invitation code status: IN_USE, EXPIRE, DISABLE, RUN_OUT'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.code_type IS 'Invitation code type: ADMIN_INVITE, DEV_INVITE, USER_INVITE, ASSET_OWNER_INVITE'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.create_time IS 'Create time'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.created_by IS 'Created by'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.updated_by IS 'Updated by'; +COMMENT ON COLUMN nexent.tenant_invitation_code_t.delete_flag IS 'Delete flag, Y/N'; + +-- 2. Create tenant_invitation_record_t table for invitation usage records +CREATE TABLE IF NOT EXISTS nexent.tenant_invitation_record_t ( + invitation_record_id SERIAL PRIMARY KEY, + invitation_id INT4 NOT NULL, + user_id VARCHAR(100) NOT NULL, + create_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + update_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +-- Add comments for tenant_invitation_record_t table +COMMENT ON TABLE nexent.tenant_invitation_record_t IS 'Tenant invitation record table'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.invitation_record_id IS 'Invitation record ID, primary key'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.invitation_id IS 'Invitation ID, foreign key'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.user_id IS 'User ID'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.create_time IS 'Create time'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.created_by IS 'Created by'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.updated_by IS 'Updated by'; +COMMENT ON COLUMN nexent.tenant_invitation_record_t.delete_flag IS 'Delete flag, Y/N'; + +-- 3. Create tenant_group_info_t table for group information +CREATE TABLE IF NOT EXISTS nexent.tenant_group_info_t ( + group_id SERIAL PRIMARY KEY, + tenant_id VARCHAR(100) NOT NULL, + group_name VARCHAR(100) NOT NULL, + group_description VARCHAR(500), + create_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + update_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +-- Add comments for tenant_group_info_t table +COMMENT ON TABLE nexent.tenant_group_info_t IS 'Tenant group information table'; +COMMENT ON COLUMN nexent.tenant_group_info_t.group_id IS 'Group ID, primary key'; +COMMENT ON COLUMN nexent.tenant_group_info_t.tenant_id IS 'Tenant ID, foreign key'; +COMMENT ON COLUMN nexent.tenant_group_info_t.group_name IS 'Group name'; +COMMENT ON COLUMN nexent.tenant_group_info_t.group_description IS 'Group description'; +COMMENT ON COLUMN nexent.tenant_group_info_t.create_time IS 'Create time'; +COMMENT ON COLUMN nexent.tenant_group_info_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.tenant_group_info_t.created_by IS 'Created by'; +COMMENT ON COLUMN nexent.tenant_group_info_t.updated_by IS 'Updated by'; +COMMENT ON COLUMN nexent.tenant_group_info_t.delete_flag IS 'Delete flag, Y/N'; + +-- 4. Create tenant_group_user_t table for group user membership +CREATE TABLE IF NOT EXISTS nexent.tenant_group_user_t ( + group_user_id SERIAL PRIMARY KEY, + group_id INT4 NOT NULL, + user_id VARCHAR(100) NOT NULL, + create_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + update_time TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NOW(), + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +-- Add comments for tenant_group_user_t table +COMMENT ON TABLE nexent.tenant_group_user_t IS 'Tenant group user membership table'; +COMMENT ON COLUMN nexent.tenant_group_user_t.group_user_id IS 'Group user ID, primary key'; +COMMENT ON COLUMN nexent.tenant_group_user_t.group_id IS 'Group ID, foreign key'; +COMMENT ON COLUMN nexent.tenant_group_user_t.user_id IS 'User ID, foreign key'; +COMMENT ON COLUMN nexent.tenant_group_user_t.create_time IS 'Create time'; +COMMENT ON COLUMN nexent.tenant_group_user_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.tenant_group_user_t.created_by IS 'Created by'; +COMMENT ON COLUMN nexent.tenant_group_user_t.updated_by IS 'Updated by'; +COMMENT ON COLUMN nexent.tenant_group_user_t.delete_flag IS 'Delete flag, Y/N'; + +-- 5. Create role_permission_t table for role permissions +CREATE TABLE IF NOT EXISTS nexent.role_permission_t ( + role_permission_id SERIAL PRIMARY KEY, + user_role VARCHAR(30) NOT NULL, + permission_category VARCHAR(30), + permission_type VARCHAR(30), + permission_subtype VARCHAR(30), + parent_key VARCHAR(50) +); + +-- Add comments for role_permission_t table +COMMENT ON TABLE nexent.role_permission_t IS 'Role permission configuration table'; +COMMENT ON COLUMN nexent.role_permission_t.role_permission_id IS 'Role permission ID, primary key'; +COMMENT ON COLUMN nexent.role_permission_t.user_role IS 'User role: SU, ADMIN, DEV, USER'; +COMMENT ON COLUMN nexent.role_permission_t.permission_category IS 'Permission category'; +COMMENT ON COLUMN nexent.role_permission_t.permission_type IS 'Permission type'; +COMMENT ON COLUMN nexent.role_permission_t.permission_subtype IS 'Permission subtype'; +COMMENT ON COLUMN nexent.role_permission_t.parent_key IS 'Parent menu key for hierarchical menus, NULL for first-level menus'; + +-- 6. Insert role permission data after clearing old data +DELETE FROM nexent.role_permission_t; + +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype) VALUES +(4, 'SU', 'RESOURCE', 'AGENT', 'READ'), +(5, 'SU', 'RESOURCE', 'AGENT', 'DELETE'), +(6, 'SU', 'RESOURCE', 'KB', 'READ'), +(7, 'SU', 'RESOURCE', 'KB', 'DELETE'), +(8, 'SU', 'RESOURCE', 'KB.GROUPS', 'READ'), +(9, 'SU', 'RESOURCE', 'KB.GROUPS', 'UPDATE'), +(10, 'SU', 'RESOURCE', 'KB.GROUPS', 'DELETE'), +(11, 'SU', 'RESOURCE', 'USER.ROLE', 'READ'), +(12, 'SU', 'RESOURCE', 'USER.ROLE', 'UPDATE'), +(13, 'SU', 'RESOURCE', 'USER.ROLE', 'DELETE'), +(14, 'SU', 'RESOURCE', 'MCP', 'READ'), +(15, 'SU', 'RESOURCE', 'MCP', 'DELETE'), +(16, 'SU', 'RESOURCE', 'MEM.SETTING', 'READ'), +(17, 'SU', 'RESOURCE', 'MEM.SETTING', 'UPDATE'), +(18, 'SU', 'RESOURCE', 'MEM.AGENT', 'READ'), +(19, 'SU', 'RESOURCE', 'MEM.AGENT', 'DELETE'), +(20, 'SU', 'RESOURCE', 'MEM.PRIVATE', 'READ'), +(21, 'SU', 'RESOURCE', 'MEM.PRIVATE', 'DELETE'), +(22, 'SU', 'RESOURCE', 'MODEL', 'CREATE'), +(23, 'SU', 'RESOURCE', 'MODEL', 'READ'), +(24, 'SU', 'RESOURCE', 'MODEL', 'UPDATE'), +(25, 'SU', 'RESOURCE', 'MODEL', 'DELETE'), +(26, 'SU', 'RESOURCE', 'TENANT', 'CREATE'), +(27, 'SU', 'RESOURCE', 'TENANT', 'READ'), +(28, 'SU', 'RESOURCE', 'TENANT', 'UPDATE'), +(29, 'SU', 'RESOURCE', 'TENANT', 'DELETE'), +(30, 'SU', 'RESOURCE', 'TENANT.LIST', 'READ'), +(31, 'SU', 'RESOURCE', 'TENANT.INFO', 'READ'), +(32, 'SU', 'RESOURCE', 'TENANT.INFO', 'UPDATE'), +(33, 'SU', 'RESOURCE', 'TENANT.INVITE', 'CREATE'), +(34, 'SU', 'RESOURCE', 'TENANT.INVITE', 'READ'), +(35, 'SU', 'RESOURCE', 'TENANT.INVITE', 'UPDATE'), +(36, 'SU', 'RESOURCE', 'TENANT.INVITE', 'DELETE'), +(37, 'SU', 'RESOURCE', 'GROUP', 'CREATE'), +(38, 'SU', 'RESOURCE', 'GROUP', 'READ'), +(39, 'SU', 'RESOURCE', 'GROUP', 'UPDATE'), +(40, 'SU', 'RESOURCE', 'GROUP', 'DELETE'), +(54, 'ADMIN', 'RESOURCE', 'AGENT', 'CREATE'), +(55, 'ADMIN', 'RESOURCE', 'AGENT', 'READ'), +(56, 'ADMIN', 'RESOURCE', 'AGENT', 'UPDATE'), +(57, 'ADMIN', 'RESOURCE', 'AGENT', 'DELETE'), +(58, 'ADMIN', 'RESOURCE', 'KB', 'CREATE'), +(59, 'ADMIN', 'RESOURCE', 'KB', 'READ'), +(60, 'ADMIN', 'RESOURCE', 'KB', 'UPDATE'), +(61, 'ADMIN', 'RESOURCE', 'KB', 'DELETE'), +(62, 'ADMIN', 'RESOURCE', 'KB.GROUPS', 'READ'), +(63, 'ADMIN', 'RESOURCE', 'KB.GROUPS', 'UPDATE'), +(64, 'ADMIN', 'RESOURCE', 'KB.GROUPS', 'DELETE'), +(65, 'ADMIN', 'RESOURCE', 'USER.ROLE', 'READ'), +(66, 'ADMIN', 'RESOURCE', 'MCP', 'CREATE'), +(67, 'ADMIN', 'RESOURCE', 'MCP', 'READ'), +(68, 'ADMIN', 'RESOURCE', 'MCP', 'UPDATE'), +(69, 'ADMIN', 'RESOURCE', 'MCP', 'DELETE'), +(70, 'ADMIN', 'RESOURCE', 'MEM.SETTING', 'READ'), +(71, 'ADMIN', 'RESOURCE', 'MEM.SETTING', 'UPDATE'), +(72, 'ADMIN', 'RESOURCE', 'MEM.AGENT', 'CREATE'), +(73, 'ADMIN', 'RESOURCE', 'MEM.AGENT', 'READ'), +(74, 'ADMIN', 'RESOURCE', 'MEM.AGENT', 'DELETE'), +(75, 'ADMIN', 'RESOURCE', 'MEM.PRIVATE', 'CREATE'), +(76, 'ADMIN', 'RESOURCE', 'MEM.PRIVATE', 'READ'), +(77, 'ADMIN', 'RESOURCE', 'MEM.PRIVATE', 'DELETE'), +(78, 'ADMIN', 'RESOURCE', 'MODEL', 'CREATE'), +(79, 'ADMIN', 'RESOURCE', 'MODEL', 'READ'), +(80, 'ADMIN', 'RESOURCE', 'MODEL', 'UPDATE'), +(81, 'ADMIN', 'RESOURCE', 'MODEL', 'DELETE'), +(82, 'ADMIN', 'RESOURCE', 'TENANT.INFO', 'READ'), +(83, 'ADMIN', 'RESOURCE', 'TENANT.INFO', 'UPDATE'), +(84, 'ADMIN', 'RESOURCE', 'TENANT.INVITE', 'CREATE'), +(85, 'ADMIN', 'RESOURCE', 'TENANT.INVITE', 'READ'), +(86, 'ADMIN', 'RESOURCE', 'TENANT.INVITE', 'UPDATE'), +(87, 'ADMIN', 'RESOURCE', 'TENANT.INVITE', 'DELETE'), +(88, 'ADMIN', 'RESOURCE', 'GROUP', 'CREATE'), +(89, 'ADMIN', 'RESOURCE', 'GROUP', 'READ'), +(90, 'ADMIN', 'RESOURCE', 'GROUP', 'UPDATE'), +(91, 'ADMIN', 'RESOURCE', 'GROUP', 'DELETE'), +(104, 'DEV', 'RESOURCE', 'AGENT', 'CREATE'), +(105, 'DEV', 'RESOURCE', 'AGENT', 'READ'), +(106, 'DEV', 'RESOURCE', 'AGENT', 'UPDATE'), +(107, 'DEV', 'RESOURCE', 'AGENT', 'DELETE'), +(108, 'DEV', 'RESOURCE', 'KB', 'CREATE'), +(109, 'DEV', 'RESOURCE', 'KB', 'READ'), +(110, 'DEV', 'RESOURCE', 'KB', 'UPDATE'), +(111, 'DEV', 'RESOURCE', 'KB', 'DELETE'), +(112, 'DEV', 'RESOURCE', 'KB.GROUPS', 'READ'), +(113, 'DEV', 'RESOURCE', 'KB.GROUPS', 'UPDATE'), +(114, 'DEV', 'RESOURCE', 'KB.GROUPS', 'DELETE'), +(115, 'DEV', 'RESOURCE', 'USER.ROLE', 'READ'), +(116, 'DEV', 'RESOURCE', 'MCP', 'CREATE'), +(117, 'DEV', 'RESOURCE', 'MCP', 'READ'), +(118, 'DEV', 'RESOURCE', 'MCP', 'UPDATE'), +(119, 'DEV', 'RESOURCE', 'MCP', 'DELETE'), +(120, 'DEV', 'RESOURCE', 'MEM.SETTING', 'READ'), +(121, 'DEV', 'RESOURCE', 'MEM.SETTING', 'UPDATE'), +(122, 'DEV', 'RESOURCE', 'MEM.AGENT', 'READ'), +(123, 'DEV', 'RESOURCE', 'MEM.PRIVATE', 'CREATE'), +(124, 'DEV', 'RESOURCE', 'MEM.PRIVATE', 'READ'), +(125, 'DEV', 'RESOURCE', 'MEM.PRIVATE', 'DELETE'), +(126, 'DEV', 'RESOURCE', 'MODEL', 'READ'), +(127, 'DEV', 'RESOURCE', 'TENANT.INFO', 'READ'), +(128, 'DEV', 'RESOURCE', 'GROUP', 'READ'), +(133, 'USER', 'RESOURCE', 'AGENT', 'READ'), +(134, 'USER', 'RESOURCE', 'USER.ROLE', 'READ'), +(135, 'USER', 'RESOURCE', 'MEM.SETTING', 'READ'), +(136, 'USER', 'RESOURCE', 'MEM.SETTING', 'UPDATE'), +(137, 'USER', 'RESOURCE', 'MEM.AGENT', 'READ'), +(138, 'USER', 'RESOURCE', 'MEM.PRIVATE', 'CREATE'), +(139, 'USER', 'RESOURCE', 'MEM.PRIVATE', 'READ'), +(140, 'USER', 'RESOURCE', 'MEM.PRIVATE', 'DELETE'), +(141, 'USER', 'RESOURCE', 'TENANT.INFO', 'READ'), +(142, 'USER', 'RESOURCE', 'GROUP', 'READ'), +(154, 'SPEED', 'RESOURCE', 'AGENT', 'CREATE'), +(155, 'SPEED', 'RESOURCE', 'AGENT', 'READ'), +(156, 'SPEED', 'RESOURCE', 'AGENT', 'UPDATE'), +(157, 'SPEED', 'RESOURCE', 'AGENT', 'DELETE'), +(158, 'SPEED', 'RESOURCE', 'KB', 'CREATE'), +(159, 'SPEED', 'RESOURCE', 'KB', 'READ'), +(160, 'SPEED', 'RESOURCE', 'KB', 'UPDATE'), +(161, 'SPEED', 'RESOURCE', 'KB', 'DELETE'), +(166, 'SPEED', 'RESOURCE', 'MCP', 'CREATE'), +(167, 'SPEED', 'RESOURCE', 'MCP', 'READ'), +(168, 'SPEED', 'RESOURCE', 'MCP', 'UPDATE'), +(169, 'SPEED', 'RESOURCE', 'MCP', 'DELETE'), +(170, 'SPEED', 'RESOURCE', 'MEM.SETTING', 'READ'), +(171, 'SPEED', 'RESOURCE', 'MEM.SETTING', 'UPDATE'), +(172, 'SPEED', 'RESOURCE', 'MEM.AGENT', 'CREATE'), +(173, 'SPEED', 'RESOURCE', 'MEM.AGENT', 'READ'), +(174, 'SPEED', 'RESOURCE', 'MEM.AGENT', 'DELETE'), +(175, 'SPEED', 'RESOURCE', 'MEM.PRIVATE', 'CREATE'), +(176, 'SPEED', 'RESOURCE', 'MEM.PRIVATE', 'READ'), +(177, 'SPEED', 'RESOURCE', 'MEM.PRIVATE', 'DELETE'), +(178, 'SPEED', 'RESOURCE', 'MODEL', 'CREATE'), +(179, 'SPEED', 'RESOURCE', 'MODEL', 'READ'), +(180, 'SPEED', 'RESOURCE', 'MODEL', 'UPDATE'), +(181, 'SPEED', 'RESOURCE', 'MODEL', 'DELETE'), +(182, 'SPEED', 'RESOURCE', 'TENANT.INFO', 'READ'), +(183, 'SPEED', 'RESOURCE', 'TENANT.INFO', 'UPDATE'), +(184, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'CREATE'), +(185, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'READ'), +(186, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'UPDATE'), +(187, 'SPEED', 'RESOURCE', 'TENANT.INVITE', 'DELETE'), +(188, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'CREATE'), +(189, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'READ'), +(190, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'UPDATE'), +(191, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'DELETE'), +(199, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'CREATE'), +(200, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'READ'), +(201, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'UPDATE'), +(202, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'DELETE'), +(203, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'CREATE'), +(204, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'READ'), +(205, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'UPDATE'), +(206, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'DELETE'), +(207, 'ASSET_OWNER', 'RESOURCE', 'KB', 'CREATE'), +(208, 'ASSET_OWNER', 'RESOURCE', 'KB', 'READ'), +(209, 'ASSET_OWNER', 'RESOURCE', 'KB', 'UPDATE'), +(210, 'ASSET_OWNER', 'RESOURCE', 'KB', 'DELETE'), +(211, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'CREATE'), +(212, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'READ'), +(213, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'UPDATE'), +(214, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'DELETE'), +(215, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'CREATE'), +(216, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'READ'), +(217, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'UPDATE'), +(218, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'DELETE'), +(219, 'ASSET_OWNER', 'RESOURCE', 'USER.ROLE', 'READ'); + +-- SU Menus (root level) +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype) VALUES +(1001, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/'), +(1002, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/resource-manage'); + +-- ADMIN Menus (root level) +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype) VALUES +(1101, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/'), +(1102, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/chat'), +(1103, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-dev'), +(1104, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/resource-space'), +(1105, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/resource-manage'), +(1106, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/users'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1107, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/models', '/agent-dev'), +(1108, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/knowledges', '/agent-dev'), +(1109, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/agents', '/agent-dev'), +(1110, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/memory', '/agent-dev'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1111, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-space', '/resource-space'), +(1112, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/mcp-space', '/resource-space'), +(1113, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/skill-space', '/resource-space'); + +-- DEV Menus (NO /resource-manage, root level) +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype) VALUES +(1201, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/'), +(1202, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/chat'), +(1203, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-dev'), +(1204, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/resource-space'), +(1205, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/users'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1206, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/models', '/agent-dev'), +(1207, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/knowledges', '/agent-dev'), +(1208, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/agents', '/agent-dev'), +(1209, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/memory', '/agent-dev'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1210, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-space', '/resource-space'), +(1211, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/mcp-space', '/resource-space'), +(1212, 'DEV', 'VISIBILITY', 'LEFT_NAV_MENU', '/skill-space', '/resource-space'); + +-- USER Menus (Minimal, all root level) +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype) VALUES +(1301, 'USER', 'VISIBILITY', 'LEFT_NAV_MENU', '/'), +(1302, 'USER', 'VISIBILITY', 'LEFT_NAV_MENU', '/chat'), +(1303, 'USER', 'VISIBILITY', 'LEFT_NAV_MENU', '/memory'), +(1304, 'USER', 'VISIBILITY', 'LEFT_NAV_MENU', '/users'); + +-- SPEED Menus (root level) +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype) VALUES +(1401, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/'), +(1402, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/chat'), +(1403, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-dev'), +(1404, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/resource-space'), +(1405, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/resource-manage'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1406, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/models', '/agent-dev'), +(1407, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/knowledges', '/agent-dev'), +(1408, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/agents', '/agent-dev'), +(1409, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/memory', '/agent-dev'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1410, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-space', '/resource-space'), +(1411, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/mcp-space', '/resource-space'), +(1412, 'SPEED', 'VISIBILITY', 'LEFT_NAV_MENU', '/skill-space', '/resource-space'); + +-- ASSET_OWNER Menus (root level) +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype) VALUES +(1501, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/'), +(1502, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/chat'), +(1503, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-dev'), +(1504, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/resource-space'), +(1505, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/owner-manage'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1506, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/models', '/agent-dev'), +(1507, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/knowledges', '/agent-dev'), +(1508, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/agents', '/agent-dev'); +INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype, parent_key) VALUES +(1509, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/agent-space', '/resource-space'), +(1510, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/mcp-space', '/resource-space'), +(1511, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/skill-space', '/resource-space'); + +-- Insert SPEED role user into user_tenant_t table if not exists +INSERT INTO nexent.user_tenant_t (user_id, tenant_id, user_role, user_email, created_by, updated_by) +VALUES ('user_id', 'tenant_id', 'SPEED', '', 'system', 'system') +ON CONFLICT (user_id, tenant_id) DO NOTHING; + +-- Create the ag_tenant_agent_version_t table for agent version management +CREATE TABLE IF NOT EXISTS nexent.ag_tenant_agent_version_t ( + id BIGSERIAL PRIMARY KEY, + tenant_id VARCHAR(100) NOT NULL, + agent_id INTEGER NOT NULL, + version_no INTEGER NOT NULL, + version_name VARCHAR(100), + release_note TEXT, + source_version_no INTEGER NULL, + source_type VARCHAR(30) NULL, + status VARCHAR(30) DEFAULT 'RELEASED', + is_a2a BOOLEAN DEFAULT FALSE, + created_by VARCHAR(100) NOT NULL, + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + updated_by VARCHAR(100), + update_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.ag_tenant_agent_version_t OWNER TO "root"; + +-- Add comments for version fields in existing tables +COMMENT ON COLUMN nexent.ag_tenant_agent_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot'; +COMMENT ON COLUMN nexent.ag_tenant_agent_t.current_version_no IS 'Current published version number. NULL means no version published yet'; +COMMENT ON COLUMN nexent.ag_tool_instance_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot'; +COMMENT ON COLUMN nexent.ag_agent_relation_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot'; + +-- Add comments for ag_tenant_agent_version_t table +COMMENT ON TABLE nexent.ag_tenant_agent_version_t IS 'Agent version metadata table. Stores version info, release notes, and version lineage.'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.id IS 'Primary key, auto-increment'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.agent_id IS 'Agent ID'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.version_no IS 'Version number, starts from 1. Does not include 0 (draft)'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.version_name IS 'User-defined version name for display (e.g., "Stable v2.1", "Hotfix-001"). NULL means use version_no as display.'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.release_note IS 'Release notes / publish remarks'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.source_version_no IS 'Source version number. If this version is a rollback, record the source version number.'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.source_type IS 'Source type: NORMAL (normal publish) / ROLLBACK (rollback and republish).'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.status IS 'Version status: RELEASED / DISABLED / ARCHIVED'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.is_a2a IS 'Whether this version is published as an A2A Server agent'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.created_by IS 'User who published this version'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.create_time IS 'Version creation timestamp'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.updated_by IS 'Last user who updated this version'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.update_time IS 'Last update timestamp'; +COMMENT ON COLUMN nexent.ag_tenant_agent_version_t.delete_flag IS 'Soft delete flag: Y/N'; + +-- Create the user_token_info_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.user_token_info_t ( + token_id SERIAL4 PRIMARY KEY NOT NULL, + access_key VARCHAR(100) NOT NULL, + user_id VARCHAR(100) NOT NULL, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE "user_token_info_t" OWNER TO "root"; + +-- Add comment to the table +COMMENT ON TABLE nexent.user_token_info_t IS 'User token (AK/SK) information table'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.user_token_info_t.token_id IS 'Token ID, unique primary key'; +COMMENT ON COLUMN nexent.user_token_info_t.access_key IS 'Access Key (AK)'; +COMMENT ON COLUMN nexent.user_token_info_t.user_id IS 'User ID who owns this token'; +COMMENT ON COLUMN nexent.user_token_info_t.create_time IS 'Creation time, audit field'; +COMMENT ON COLUMN nexent.user_token_info_t.update_time IS 'Update time, audit field'; +COMMENT ON COLUMN nexent.user_token_info_t.created_by IS 'Creator ID, audit field'; +COMMENT ON COLUMN nexent.user_token_info_t.updated_by IS 'Last updater ID, audit field'; +COMMENT ON COLUMN nexent.user_token_info_t.delete_flag IS 'Soft delete flag, Y means deleted'; + + +-- Create the user_token_usage_log_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.user_token_usage_log_t ( + token_usage_id SERIAL4 PRIMARY KEY NOT NULL, + token_id INT4 NOT NULL, + call_function_name VARCHAR(100), + related_id INT4, + meta_data JSONB, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE "user_token_usage_log_t" OWNER TO "root"; + +-- Add comment to the table +COMMENT ON TABLE nexent.user_token_usage_log_t IS 'User token usage log table'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.user_token_usage_log_t.token_usage_id IS 'Token usage log ID, unique primary key'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.token_id IS 'Foreign key to user_token_info_t.token_id'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.call_function_name IS 'API function name being called'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.related_id IS 'Related resource ID (e.g., conversation_id)'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.meta_data IS 'Additional metadata for this usage log entry, stored as JSON'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.create_time IS 'Creation time, audit field'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.update_time IS 'Update time, audit field'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.created_by IS 'Creator ID, audit field'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.updated_by IS 'Last updater ID, audit field'; +COMMENT ON COLUMN nexent.user_token_usage_log_t.delete_flag IS 'Soft delete flag, Y means deleted'; + +-- Create the ag_skill_info_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.ag_skill_info_t ( + skill_id SERIAL4 PRIMARY KEY NOT NULL, + skill_name VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100), + skill_description VARCHAR(1000), + skill_tags JSON, + skill_content TEXT, + config_schemas JSON, + config_values JSON, + source VARCHAR(30) DEFAULT 'official', + created_by VARCHAR(100), + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_by VARCHAR(100), + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE "ag_skill_info_t" OWNER TO "root"; + +-- Add comment to the table +COMMENT ON TABLE nexent.ag_skill_info_t IS 'Skill information table for managing custom skills'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_skill_info_t.skill_id IS 'Skill ID, unique primary key'; +COMMENT ON COLUMN nexent.ag_skill_info_t.skill_name IS 'Skill name, unique within tenant'; +COMMENT ON COLUMN nexent.ag_skill_info_t.tenant_id IS 'Tenant ID for multi-tenancy. NULL for pre-existing skills.'; +COMMENT ON COLUMN nexent.ag_skill_info_t.skill_description IS 'Skill description text'; +COMMENT ON COLUMN nexent.ag_skill_info_t.skill_tags IS 'Skill tags stored as JSON array'; +COMMENT ON COLUMN nexent.ag_skill_info_t.skill_content IS 'Skill content or prompt text'; +COMMENT ON COLUMN nexent.ag_skill_info_t.config_schemas IS 'Parameter metadata from config/schema.yaml'; +COMMENT ON COLUMN nexent.ag_skill_info_t.config_values IS 'Runtime parameter values from config/config.yaml'; +COMMENT ON COLUMN nexent.ag_skill_info_t.source IS 'Skill source: official, custom, or partner'; +COMMENT ON COLUMN nexent.ag_skill_info_t.created_by IS 'Creator ID'; +COMMENT ON COLUMN nexent.ag_skill_info_t.create_time IS 'Creation timestamp'; +COMMENT ON COLUMN nexent.ag_skill_info_t.updated_by IS 'Last updater ID'; +COMMENT ON COLUMN nexent.ag_skill_info_t.update_time IS 'Last update timestamp'; +COMMENT ON COLUMN nexent.ag_skill_info_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +-- Create the ag_skill_tools_rel_t table in the nexent schema +CREATE TABLE IF NOT EXISTS nexent.ag_skill_tools_rel_t ( + rel_id SERIAL4 PRIMARY KEY NOT NULL, + skill_id INTEGER, + tool_id INTEGER, + created_by VARCHAR(100), + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_by VARCHAR(100), + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE "ag_skill_tools_rel_t" OWNER TO "root"; + +-- Add comment to the table +COMMENT ON TABLE nexent.ag_skill_tools_rel_t IS 'Skill-tool relationship table for many-to-many mapping'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.rel_id IS 'Relationship ID, unique primary key'; +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.skill_id IS 'Foreign key to ag_skill_info_t.skill_id'; +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.tool_id IS 'Tool ID from ag_tool_info_t'; +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.created_by IS 'Creator ID'; +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.create_time IS 'Creation timestamp'; +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.updated_by IS 'Last updater ID'; +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.update_time IS 'Last update timestamp'; +COMMENT ON COLUMN nexent.ag_skill_tools_rel_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +-- Create the ag_skill_instance_t table in the nexent schema +-- Stores skill instance configuration per agent version +-- Note: skill_description and skill_content fields removed, now retrieved from ag_skill_info_t +CREATE TABLE IF NOT EXISTS nexent.ag_skill_instance_t ( + skill_instance_id SERIAL4 NOT NULL, + skill_id INTEGER NOT NULL, + agent_id INTEGER NOT NULL, + user_id VARCHAR(100), + tenant_id VARCHAR(100), + enabled BOOLEAN DEFAULT TRUE, + version_no INTEGER DEFAULT 0 NOT NULL, + config_values JSON, + config_schemas JSON, + created_by VARCHAR(100), + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_by VARCHAR(100), + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N', + CONSTRAINT ag_skill_instance_t_pkey PRIMARY KEY (skill_instance_id, version_no) +); + +ALTER TABLE "ag_skill_instance_t" OWNER TO "root"; + +-- Add comment to the table +COMMENT ON TABLE nexent.ag_skill_instance_t IS 'Skill instance configuration table - stores per-agent skill settings'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_skill_instance_t.skill_instance_id IS 'Skill instance ID'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.skill_id IS 'Foreign key to ag_skill_info_t.skill_id'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.agent_id IS 'Agent ID'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.user_id IS 'User ID'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.tenant_id IS 'Tenant ID'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.enabled IS 'Whether this skill is enabled for the agent'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.version_no IS 'Version number. 0 = draft/editing state, >=1 = published snapshot'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.config_values IS 'Per-agent runtime parameter values from config/config.yaml'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.config_schemas IS 'Per-agent parameter schema overrides from config/schema.yaml'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.created_by IS 'Creator ID'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.create_time IS 'Creation timestamp'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.updated_by IS 'Last updater ID'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.update_time IS 'Last update timestamp'; +COMMENT ON COLUMN nexent.ag_skill_instance_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +-- Create the ag_outer_api_services table for OpenAPI services (MCP conversion) +-- This table stores one record per MCP service instead of per tool +CREATE TABLE IF NOT EXISTS nexent.ag_outer_api_services ( + id BIGSERIAL PRIMARY KEY, + mcp_service_name VARCHAR(100) NOT NULL, + description TEXT, + openapi_json JSONB, + server_url VARCHAR(500), + headers_template JSONB, + tenant_id VARCHAR(100) NOT NULL, + is_available BOOLEAN DEFAULT TRUE, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.ag_outer_api_services OWNER TO "root"; + +-- Create a function to update the update_time column +CREATE OR REPLACE FUNCTION update_ag_outer_api_services_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create a trigger to call the function before each update +CREATE TRIGGER update_ag_outer_api_services_update_time_trigger +BEFORE UPDATE ON nexent.ag_outer_api_services +FOR EACH ROW +EXECUTE FUNCTION update_ag_outer_api_services_update_time(); + +-- Add comment to the table +COMMENT ON TABLE nexent.ag_outer_api_services IS 'OpenAPI services table - stores MCP service information converted from OpenAPI specs. One record per service.'; + +-- Add comments to the columns +COMMENT ON COLUMN nexent.ag_outer_api_services.id IS 'Service ID, unique primary key'; +COMMENT ON COLUMN nexent.ag_outer_api_services.mcp_service_name IS 'MCP service name (unique identifier per tenant)'; +COMMENT ON COLUMN nexent.ag_outer_api_services.description IS 'Service description from OpenAPI info'; +COMMENT ON COLUMN nexent.ag_outer_api_services.openapi_json IS 'Complete OpenAPI JSON specification'; +COMMENT ON COLUMN nexent.ag_outer_api_services.server_url IS 'Base URL of the REST API server'; +COMMENT ON COLUMN nexent.ag_outer_api_services.headers_template IS 'Default headers template as JSONB'; +COMMENT ON COLUMN nexent.ag_outer_api_services.tenant_id IS 'Tenant ID for multi-tenancy'; +COMMENT ON COLUMN nexent.ag_outer_api_services.is_available IS 'Whether the service is available'; +COMMENT ON COLUMN nexent.ag_outer_api_services.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.ag_outer_api_services.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.ag_outer_api_services.created_by IS 'Creator'; +COMMENT ON COLUMN nexent.ag_outer_api_services.updated_by IS 'Updater'; +COMMENT ON COLUMN nexent.ag_outer_api_services.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +-- Create index for tenant_id queries +CREATE INDEX IF NOT EXISTS idx_ag_outer_api_services_tenant_id +ON nexent.ag_outer_api_services (tenant_id) +WHERE delete_flag = 'N'; + +-- Create index for mcp_service_name queries +CREATE INDEX IF NOT EXISTS idx_ag_outer_api_services_mcp_service_name +ON nexent.ag_outer_api_services (mcp_service_name) +WHERE delete_flag = 'N'; + +CREATE TABLE IF NOT EXISTS nexent.ag_a2a_nacos_config_t ( + id BIGSERIAL PRIMARY KEY, + config_id VARCHAR(64) UNIQUE NOT NULL, + + nacos_addr VARCHAR(512) NOT NULL, + nacos_username VARCHAR(100), + nacos_password VARCHAR(256), + + namespace_id VARCHAR(100) DEFAULT 'public', + + name VARCHAR(100) NOT NULL, + description TEXT, + + tenant_id VARCHAR(100) NOT NULL, + created_by VARCHAR(100) NOT NULL, + updated_by VARCHAR(100), + + is_active BOOLEAN DEFAULT TRUE, + last_scan_at TIMESTAMP(6), + + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.ag_a2a_nacos_config_t OWNER TO "root"; + +COMMENT ON TABLE nexent.ag_a2a_nacos_config_t IS 'Nacos configuration for external A2A agent discovery. Stores connection info and discovery scope.'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.id IS 'Primary key, auto-increment'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.config_id IS 'Unique config identifier for API reference'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.nacos_addr IS 'Nacos server address, e.g., http://nacos-server:8848'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.nacos_username IS 'Nacos username for authentication'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.nacos_password IS 'Nacos password, encrypted at rest'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.namespace_id IS 'Nacos namespace for service discovery, default is public'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.name IS 'Display name for this Nacos config, e.g., Production Nacos'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.description IS 'Description of this Nacos configuration'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.tenant_id IS 'Tenant ID for multi-tenancy isolation'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.created_by IS 'User who created this config'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.updated_by IS 'User who last updated this record'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.is_active IS 'Whether this Nacos config is active'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.last_scan_at IS 'Last time a scan was performed using this config'; +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.create_time IS 'Record creation timestamp'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.update_time IS 'Record last update timestamp'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_nacos_config_t.delete_flag IS 'Soft delete flag: Y/N'; -- NOSONAR + + +CREATE TABLE IF NOT EXISTS nexent.ag_a2a_external_agent_t ( + id BIGSERIAL PRIMARY KEY, + + name VARCHAR(255) NOT NULL, + description TEXT, + version VARCHAR(50), + + agent_url VARCHAR(512) NOT NULL, + + protocol_type VARCHAR(20) DEFAULT 'JSONRPC', + + streaming BOOLEAN DEFAULT FALSE, + + supported_interfaces JSONB, + + -- Source information + source_type VARCHAR(20) NOT NULL, + + -- For URL mode: + source_url VARCHAR(512), + + -- For Nacos mode: + nacos_config_id VARCHAR(64), + nacos_agent_name VARCHAR(255), + + -- Base URL for infrastructure health checks + base_url VARCHAR(512), + + -- Tenant isolation + tenant_id VARCHAR(100) NOT NULL, + created_by VARCHAR(100) NOT NULL, + updated_by VARCHAR(100), + + raw_card JSONB, + + cached_at TIMESTAMP(6), + cache_expires_at TIMESTAMP(6), + + is_available BOOLEAN DEFAULT TRUE, + last_check_at TIMESTAMP(6), + last_check_result VARCHAR(50), + + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.ag_a2a_external_agent_t OWNER TO "root"; + +COMMENT ON TABLE nexent.ag_a2a_external_agent_t IS 'External A2A agents discovered from URL or Nacos. Caches Agent Cards for A2A Client role.'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.id IS 'Primary key, auto-increment. Used as unique identifier for internal references.'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.name IS 'Agent name from Agent Card'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.description IS 'Agent description from Agent Card'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.version IS 'Agent version from Agent Card, e.g., 1.2.0'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.agent_url IS 'Primary A2A endpoint URL (http-json-rpc by default, extracted from supportedInterfaces)'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.protocol_type IS 'Protocol type for calling this agent: JSONRPC, HTTP+JSON, or GRPC'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.streaming IS 'Whether this agent supports SSE streaming (from capabilities.streaming)'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.supported_interfaces IS 'All supported interfaces array from Agent Card. Format: [{protocolBinding, url, protocolVersion}, ...]'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.source_type IS 'Discovery source: url or nacos'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.source_url IS 'Direct URL to agent card (for url source type)'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.nacos_config_id IS 'Reference to Nacos config used for discovery (for nacos source type)'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.nacos_agent_name IS 'Original name used for Nacos query'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.tenant_id IS 'Tenant ID for multi-tenancy isolation'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.created_by IS 'User who discovered this agent'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.updated_by IS 'User who last updated this record'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.raw_card IS 'Full original Agent Card JSON from discovery'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.cached_at IS 'Timestamp when Agent Card was cached'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.cache_expires_at IS 'Timestamp when cache expires'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.is_available IS 'Whether this agent is currently reachable'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.last_check_at IS 'Last health check timestamp'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.last_check_result IS 'Last health check result: OK, ERROR, TIMEOUT'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.create_time IS 'Record creation timestamp'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.update_time IS 'Record last update timestamp'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.delete_flag IS 'Soft delete flag: Y/N'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_external_agent_t.base_url IS 'Base URL for health checks (service root address)'; + + +CREATE TABLE IF NOT EXISTS nexent.ag_a2a_external_agent_relation_t ( + id BIGSERIAL PRIMARY KEY, + local_agent_id INTEGER NOT NULL, + external_agent_id BIGINT NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + is_enabled BOOLEAN DEFAULT TRUE, + created_by VARCHAR(100) NOT NULL, + updated_by VARCHAR(100), + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N', + CONSTRAINT uq_local_external_agent UNIQUE (local_agent_id, external_agent_id) +); + +ALTER TABLE nexent.ag_a2a_external_agent_relation_t OWNER TO "root"; + +COMMENT ON TABLE nexent.ag_a2a_external_agent_relation_t IS 'Relation between local agent and external A2A agent. Enables local agents to call external A2A agents as sub-agents.'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.id IS 'Primary key, auto-increment'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.local_agent_id IS 'Local parent agent ID (FK to ag_tenant_agent_t)'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.external_agent_id IS 'External A2A agent ID (FK to ag_a2a_external_agent_t.id)'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.tenant_id IS 'Tenant ID for multi-tenancy isolation'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.is_enabled IS 'Whether this relation is active'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.created_by IS 'User who created this relation'; +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.updated_by IS 'User who last updated this record'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.create_time IS 'Record creation timestamp'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.update_time IS 'Record last update timestamp'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_external_agent_relation_t.delete_flag IS 'Soft delete flag: Y/N'; -- NOSONAR + +CREATE TABLE IF NOT EXISTS nexent.ag_a2a_server_agent_t ( + id BIGSERIAL PRIMARY KEY, + agent_id INTEGER NOT NULL, + user_id VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + created_by VARCHAR(100), + updated_by VARCHAR(100), + endpoint_id VARCHAR(64) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + version VARCHAR(50), + agent_url VARCHAR(512), + streaming BOOLEAN DEFAULT FALSE, + supported_interfaces JSONB, + card_overrides JSONB, + is_enabled BOOLEAN DEFAULT FALSE, + raw_card JSONB, + published_at TIMESTAMP(6), + unpublished_at TIMESTAMP(6), + response_format VARCHAR(20) DEFAULT 'task', + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.ag_a2a_server_agent_t OWNER TO "root"; + +COMMENT ON TABLE nexent.ag_a2a_server_agent_t IS 'Local agents registered as A2A Server endpoints. Exposes Agent Cards for external A2A callers.'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.id IS 'Primary key, auto-increment'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.agent_id IS 'Local agent ID (FK to ag_tenant_agent_t)'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.user_id IS 'Owner user ID'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.tenant_id IS 'Tenant ID for multi-tenancy isolation'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.created_by IS 'User who created this A2A Server agent'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.updated_by IS 'User who last updated this A2A Server agent'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.endpoint_id IS 'Generated endpoint ID, format: a2a_{agent_id[:8]}_{hash[:8]}'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.name IS 'Agent name exposed in Agent Card (from agent or override)'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.description IS 'Agent description exposed in Agent Card'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.version IS 'Agent version exposed in Agent Card'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.agent_url IS 'Primary A2A endpoint URL (http-json-rpc by default)'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.streaming IS 'Whether this agent supports SSE streaming'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.supported_interfaces IS 'All supported interfaces: [{protocolBinding, url, protocolVersion}, ...]'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.card_overrides IS 'User customizations for Agent Card (partial override)'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.is_enabled IS 'Whether A2A Server is enabled for this agent'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.raw_card IS 'Generated Agent Card JSON (for debugging)'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.published_at IS 'Timestamp when A2A Server was last enabled'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.unpublished_at IS 'Timestamp when A2A Server was disabled'; +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.create_time IS 'Record creation timestamp'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.update_time IS 'Record last update timestamp'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.delete_flag IS 'Soft delete flag: Y/N'; -- NOSONAR +COMMENT ON COLUMN nexent.ag_a2a_server_agent_t.response_format IS 'Response format: ''task'' for full Task response, ''message'' for simple Message response'; + + +CREATE TABLE IF NOT EXISTS nexent.ag_a2a_task_t ( + id VARCHAR(64) PRIMARY KEY, -- taskId + context_id VARCHAR(64), -- contextId + endpoint_id VARCHAR(64) NOT NULL, + caller_user_id VARCHAR(100), + caller_tenant_id VARCHAR(100), + raw_request JSONB, + task_state VARCHAR(50) NOT NULL DEFAULT 'TASK_STATE_SUBMITTED', + state_timestamp TIMESTAMP(6), -- State update timestamp + result_data JSONB, -- Final result (renamed from result to avoid SQL function conflict) + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP(6) +); + +ALTER TABLE nexent.ag_a2a_task_t OWNER TO "root"; + +COMMENT ON TABLE nexent.ag_a2a_task_t IS 'A2A tasks for tracking requests. Task is the unit of work, not all requests need to create a task.'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.id IS 'Task ID from A2A protocol, primary key'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.context_id IS 'Context ID for grouping related A2A tasks'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.endpoint_id IS 'Endpoint ID (FK to ag_a2a_server_agent_t.endpoint_id)'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.caller_user_id IS 'User ID of the caller (for audit)'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.caller_tenant_id IS 'Tenant ID of the caller (for audit)'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.raw_request IS 'Original A2A request payload'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.task_state IS 'Task state: TASK_STATE_SUBMITTED, TASK_STATE_WORKING, TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED, TASK_STATE_INPUT_REQUIRED, TASK_STATE_REJECTED, TASK_STATE_AUTH_REQUIRED'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.state_timestamp IS 'Task state last update timestamp'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.result_data IS 'Task final result data'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.create_time IS 'Task creation timestamp'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.update_time IS 'Task last update timestamp'; +COMMENT ON COLUMN nexent.ag_a2a_task_t.completed_at IS 'Task completion timestamp'; + +CREATE TABLE IF NOT EXISTS nexent.ag_a2a_message_t ( + message_id VARCHAR(64) PRIMARY KEY, -- messageId (A2A spec naming) + task_id VARCHAR(64), -- taskId (associated task), can be NULL for simple requests + message_index INTEGER NOT NULL, -- Sequence index + role VARCHAR(20) NOT NULL CHECK (role IN ('ROLE_UNSPECIFIED', 'ROLE_USER', 'ROLE_AGENT')), -- Following A2A spec: ROLE_UNSPECIFIED, ROLE_USER, ROLE_AGENT + parts JSONB NOT NULL, -- Part array + meta_data JSONB, -- Optional metadata + extensions JSONB, -- Extension URI list + reference_task_ids JSONB, -- Referenced task IDs array + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + UNIQUE(task_id, message_index) +); + +ALTER TABLE nexent.ag_a2a_message_t OWNER TO "root"; + +COMMENT ON TABLE nexent.ag_a2a_message_t IS 'A2A messages within tasks. Stores conversation history for multi-turn interactions.'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.message_id IS 'Message ID, primary key (A2A spec: messageId)'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.task_id IS 'Task ID this message belongs to (FK to ag_a2a_task_t.id), can be NULL for simple requests without Task'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.message_index IS 'Order of message in the conversation'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.role IS 'Message sender role: ROLE_UNSPECIFIED, ROLE_USER, or ROLE_AGENT'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.parts IS 'Message parts following A2A Part structure: [{"type": "text", "text": "..."}]'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.meta_data IS 'Optional message metadata'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.extensions IS 'Extension URI list'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.reference_task_ids IS 'Referenced task IDs array for multi-turn scenarios'; +COMMENT ON COLUMN nexent.ag_a2a_message_t.create_time IS 'Message creation timestamp'; + +CREATE TABLE IF NOT EXISTS nexent.ag_a2a_artifact_t ( + id VARCHAR(64) PRIMARY KEY, -- Internal primary key + artifact_id VARCHAR(64) NOT NULL, -- artifactId (A2A spec naming) + task_id VARCHAR(64) NOT NULL, -- taskId (associated task, required) + name VARCHAR(255), -- Human-readable name + description TEXT, -- Description + parts JSONB NOT NULL, -- Part array (following A2A spec) + meta_data JSONB, -- Metadata + extensions JSONB, -- Extension URI list + create_time TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP, + UNIQUE(task_id, artifact_id) +); + +ALTER TABLE nexent.ag_a2a_artifact_t OWNER TO "root"; + +COMMENT ON TABLE nexent.ag_a2a_artifact_t IS 'A2A artifacts. Stores the output/artifacts produced by a task.'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.id IS 'Internal primary key'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.artifact_id IS 'Artifact ID (A2A spec: artifactId)'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.task_id IS 'Task ID this artifact belongs to (FK to ag_a2a_task_t.id), required - no standalone artifacts'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.name IS 'Human-readable artifact name'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.description IS 'Artifact description'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.parts IS 'Artifact parts following A2A Part structure: [{"type": "text", "text": "..."}]'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.meta_data IS 'Artifact metadata'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.extensions IS 'Extension URI list'; +COMMENT ON COLUMN nexent.ag_a2a_artifact_t.create_time IS 'Artifact creation timestamp'; + +-- Create the model_monitoring_record_t table for LLM performance metrics +CREATE TABLE IF NOT EXISTS nexent.model_monitoring_record_t ( + monitoring_id SERIAL PRIMARY KEY, + model_id INT4, + model_name VARCHAR(100) NOT NULL, + model_type VARCHAR(20) DEFAULT 'llm', + agent_id INT4, + agent_name VARCHAR(100), + conversation_id INT4, + tenant_id VARCHAR(100) NOT NULL, + user_id VARCHAR(100), + display_name VARCHAR(100), + request_duration_ms INT4, + ttft_ms INT4, + input_tokens INT4, + output_tokens INT4, + total_tokens INT4, + generation_rate FLOAT, + is_streaming BOOLEAN DEFAULT FALSE, + is_success BOOLEAN DEFAULT TRUE, + is_error BOOLEAN DEFAULT FALSE, + error_type VARCHAR(50), + error_message TEXT, + retry_count INT4 DEFAULT 0, + operation VARCHAR(50), + create_time TIMESTAMP DEFAULT NOW(), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.model_monitoring_record_t OWNER TO "root"; + +COMMENT ON TABLE nexent.model_monitoring_record_t IS 'Per-request LLM performance metrics for model monitoring'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.monitoring_id IS 'Monitoring record ID, unique primary key'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.model_id IS 'Foreign key to model_record_t.model_id'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.model_name IS 'Model identifier (repo/name format)'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.model_type IS 'Model type: llm, vlm, embedding, multi_embedding, rerank'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.agent_id IS 'Agent ID that initiated the request'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.agent_name IS 'Agent display name'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.conversation_id IS 'Conversation ID associated with the request'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.tenant_id IS 'Tenant ID for multi-tenancy isolation'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.user_id IS 'User ID who initiated the request'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.display_name IS 'Human-readable model display name'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.request_duration_ms IS 'Total request duration in milliseconds'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.ttft_ms IS 'Time to first token in milliseconds (streaming only)'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.input_tokens IS 'Number of input prompt tokens'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.output_tokens IS 'Number of output completion tokens'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.total_tokens IS 'Total tokens (input + output)'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.generation_rate IS 'Token generation rate in tokens per second'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.is_streaming IS 'Whether the request used streaming response'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.is_success IS 'Whether the request completed successfully'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.is_error IS 'Whether the request resulted in an error'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.error_type IS 'Error exception class name'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.error_message IS 'Error message text'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.retry_count IS 'Number of retry attempts'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.operation IS 'Operation type: chat_completion, title_generation, connectivity_check, embedding_call, system_prompt_generation'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.create_time IS 'Record creation timestamp'; +COMMENT ON COLUMN nexent.model_monitoring_record_t.delete_flag IS 'Soft delete flag: Y/N'; + +CREATE INDEX IF NOT EXISTS ix_monitoring_model_id ON nexent.model_monitoring_record_t (model_id); +CREATE INDEX IF NOT EXISTS ix_monitoring_tenant_id ON nexent.model_monitoring_record_t (tenant_id); +CREATE INDEX IF NOT EXISTS ix_monitoring_agent_id ON nexent.model_monitoring_record_t (agent_id); +CREATE INDEX IF NOT EXISTS ix_monitoring_create_time ON nexent.model_monitoring_record_t (create_time); +CREATE INDEX IF NOT EXISTS ix_monitoring_is_error ON nexent.model_monitoring_record_t (is_error); +CREATE INDEX IF NOT EXISTS ix_monitoring_model_type ON nexent.model_monitoring_record_t (model_type); +CREATE INDEX IF NOT EXISTS ix_monitoring_model_time ON nexent.model_monitoring_record_t (model_id, create_time); + +-- Create user OAuth account table for third-party login (GitHub, WeChat, etc.) +CREATE TABLE IF NOT EXISTS nexent.user_oauth_account_t ( + oauth_account_id SERIAL PRIMARY KEY, + user_id VARCHAR(100) NOT NULL, + provider VARCHAR(30) NOT NULL, + provider_user_id VARCHAR(200) NOT NULL, + provider_email VARCHAR(255), + provider_username VARCHAR(200), + tenant_id VARCHAR(100), + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag CHAR(1) DEFAULT 'N', + CONSTRAINT uq_oauth_provider_user UNIQUE (provider, provider_user_id) +); + +ALTER TABLE nexent.user_oauth_account_t OWNER TO "root"; + +-- Create a function to update the update_time column +CREATE OR REPLACE FUNCTION update_user_oauth_account_t_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create a trigger to call the function before each update +CREATE TRIGGER update_user_oauth_account_t_update_time_trigger +BEFORE UPDATE ON nexent.user_oauth_account_t +FOR EACH ROW +EXECUTE FUNCTION update_user_oauth_account_t_update_time(); + +-- Add comments +COMMENT ON TABLE nexent.user_oauth_account_t IS 'User OAuth account table - third-party login bindings'; +COMMENT ON COLUMN nexent.user_oauth_account_t.oauth_account_id IS 'OAuth account ID, primary key'; +COMMENT ON COLUMN nexent.user_oauth_account_t.user_id IS 'Nexent user ID (Supabase UUID)'; +COMMENT ON COLUMN nexent.user_oauth_account_t.provider IS 'OAuth provider name: github, wechat, gde, link_app'; +COMMENT ON COLUMN nexent.user_oauth_account_t.provider_user_id IS 'User ID from the OAuth provider'; +COMMENT ON COLUMN nexent.user_oauth_account_t.provider_email IS 'Email from the OAuth provider'; +COMMENT ON COLUMN nexent.user_oauth_account_t.provider_username IS 'Display name from the OAuth provider'; +COMMENT ON COLUMN nexent.user_oauth_account_t.tenant_id IS 'Tenant ID at time of linking'; +COMMENT ON COLUMN nexent.user_oauth_account_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.user_oauth_account_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.user_oauth_account_t.created_by IS 'Creator'; +COMMENT ON COLUMN nexent.user_oauth_account_t.updated_by IS 'Updater'; +COMMENT ON COLUMN nexent.user_oauth_account_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N'; + +-- Create index for user_id queries +CREATE INDEX IF NOT EXISTS idx_user_oauth_account_t_user_id +ON nexent.user_oauth_account_t (user_id); + +-- mcp_community_record_t: Community MCP market table +CREATE TABLE IF NOT EXISTS nexent.mcp_community_record_t ( + community_id SERIAL PRIMARY KEY NOT NULL, + tenant_id VARCHAR(100), + user_id VARCHAR(100), + mcp_name VARCHAR(100) NOT NULL, + mcp_server VARCHAR(500) NOT NULL, + source VARCHAR(30) DEFAULT 'community', + version VARCHAR(50), + registry_json JSONB, + transport_type VARCHAR(30), + config_json JSON, + tags TEXT[], + description TEXT, + create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +ALTER TABLE nexent.mcp_community_record_t OWNER TO root; + +COMMENT ON TABLE nexent.mcp_community_record_t IS 'Community MCP market records, publishable from tenant MCP services'; +COMMENT ON COLUMN nexent.mcp_community_record_t.community_id IS 'Community record ID, unique primary key'; +COMMENT ON COLUMN nexent.mcp_community_record_t.tenant_id IS 'Publisher tenant ID'; +COMMENT ON COLUMN nexent.mcp_community_record_t.user_id IS 'Publisher user ID'; +COMMENT ON COLUMN nexent.mcp_community_record_t.mcp_name IS 'MCP name'; +COMMENT ON COLUMN nexent.mcp_community_record_t.mcp_server IS 'MCP server URL'; +COMMENT ON COLUMN nexent.mcp_community_record_t.source IS 'Source type, fixed to community for this table'; +COMMENT ON COLUMN nexent.mcp_community_record_t.version IS 'MCP version'; +COMMENT ON COLUMN nexent.mcp_community_record_t.registry_json IS 'Full MCP server metadata JSON for discovery and quick import'; +COMMENT ON COLUMN nexent.mcp_community_record_t.transport_type IS 'Transport type: url/container'; +COMMENT ON COLUMN nexent.mcp_community_record_t.config_json IS 'Public-shareable MCP configuration JSON'; +COMMENT ON COLUMN nexent.mcp_community_record_t.tags IS 'Tags'; +COMMENT ON COLUMN nexent.mcp_community_record_t.description IS 'Description'; +COMMENT ON COLUMN nexent.mcp_community_record_t.create_time IS 'Creation time'; +COMMENT ON COLUMN nexent.mcp_community_record_t.update_time IS 'Update time'; +COMMENT ON COLUMN nexent.mcp_community_record_t.created_by IS 'Creator ID'; +COMMENT ON COLUMN nexent.mcp_community_record_t.updated_by IS 'Updater ID'; +COMMENT ON COLUMN nexent.mcp_community_record_t.delete_flag IS 'Soft delete flag: Y/N'; + +CREATE INDEX IF NOT EXISTS idx_mcp_community_tenant_delete + ON nexent.mcp_community_record_t (tenant_id, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_mcp_community_name_delete + ON nexent.mcp_community_record_t (mcp_name, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_mcp_community_transport_delete + ON nexent.mcp_community_record_t (transport_type, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_mcp_community_user_delete + ON nexent.mcp_community_record_t (user_id, delete_flag); + +CREATE INDEX IF NOT EXISTS idx_mcp_community_tags_gin + ON nexent.mcp_community_record_t USING GIN (tags); + +CREATE OR REPLACE FUNCTION update_mcp_community_record_update_time() +RETURNS TRIGGER AS $$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION update_mcp_community_record_update_time() IS 'Auto-update update_time for mcp_community_record_t'; + +DROP TRIGGER IF EXISTS update_mcp_community_record_update_time_trigger ON nexent.mcp_community_record_t; +CREATE TRIGGER update_mcp_community_record_update_time_trigger +BEFORE UPDATE ON nexent.mcp_community_record_t +FOR EACH ROW +EXECUTE FUNCTION update_mcp_community_record_update_time(); + +COMMENT ON TRIGGER update_mcp_community_record_update_time_trigger ON nexent.mcp_community_record_t IS 'Trigger to maintain update_time'; + +CREATE TABLE IF NOT EXISTS nexent.user_cas_session_t ( + cas_session_id SERIAL PRIMARY KEY, + session_id VARCHAR(100) NOT NULL UNIQUE, + user_id VARCHAR(100) NOT NULL, + cas_user_id VARCHAR(200) NOT NULL, + cas_session_index VARCHAR(500), + status VARCHAR(30) NOT NULL DEFAULT 'active', + expires_at TIMESTAMP NOT NULL, + revoked_at TIMESTAMP, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); + +CREATE INDEX IF NOT EXISTS ix_user_cas_session_session_id + ON nexent.user_cas_session_t (session_id); +CREATE INDEX IF NOT EXISTS ix_user_cas_session_user_id + ON nexent.user_cas_session_t (user_id); +CREATE INDEX IF NOT EXISTS ix_user_cas_session_cas_user_id + ON nexent.user_cas_session_t (cas_user_id); + +COMMENT ON TABLE nexent.user_cas_session_t IS 'Server-side session records for CAS SSO login and logout synchronization'; +COMMENT ON COLUMN nexent.user_cas_session_t.session_id IS 'JWT sid claim for revocation checks'; +COMMENT ON COLUMN nexent.user_cas_session_t.cas_user_id IS 'User identifier returned by CAS'; +COMMENT ON COLUMN nexent.user_cas_session_t.cas_session_index IS 'CAS SessionIndex or service ticket'; diff --git a/frontend/app/[locale]/agents/components/AgentConfigComp.tsx b/frontend/app/[locale]/agents/components/AgentConfigComp.tsx index 1e750d5eb..0e94f4d6a 100644 --- a/frontend/app/[locale]/agents/components/AgentConfigComp.tsx +++ b/frontend/app/[locale]/agents/components/AgentConfigComp.tsx @@ -7,6 +7,8 @@ import CollaborativeAgent from "./agentConfig/CollaborativeAgent"; import ToolManagement from "./agentConfig/ToolManagement"; import SkillManagement from "./agentConfig/SkillManagement"; import SkillBuildModal from "./agentConfig/SkillBuildModal"; +import SelectToolsDialog from "./agentConfig/tool/SelectToolsDialog"; +import LabelManagementModal from "./agentConfig/tool/LabelManagementModal"; import { updateToolList } from "@/services/mcpService"; import { useAgentConfigStore } from "@/stores/agentConfigStore"; @@ -16,7 +18,7 @@ import { useExternalAgents } from "@/hooks/agent/useExternalAgents"; import McpConfigModal from "./agentConfig/McpConfigModal"; import A2AAgentDiscoveryModal from "./a2a/A2AAgentDiscoveryModal"; -import { RefreshCw, Lightbulb, Plug, BlocksIcon, Globe } from "lucide-react"; +import { Wrench, RefreshCw, Lightbulb, Plug, BlocksIcon, Globe } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface AgentConfigCompProps {} @@ -37,10 +39,12 @@ export default function AgentConfigComp({}: AgentConfigCompProps) { const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshingSkill, setIsRefreshingSkill] = useState(false); const [showA2ADiscovery, setShowA2ADiscovery] = useState(false); + const [isToolSelectOpen, setIsToolSelectOpen] = useState(false); + const [labelModalOpen, setLabelModalOpen] = useState(false); const showLegacyMcpConfig = false; // Use tool list hook for data management - const { groupedTools, invalidate } = useToolList(); + const { invalidate, availableTools } = useToolList(); const { groupedSkills, invalidate: invalidateSkills } = useSkillList(); const { invalidate: invalidateExternalAgents } = useExternalAgents(); @@ -165,28 +169,40 @@ export default function AgentConfigComp({}: AgentConfigCompProps) { - - - + + {/* Left: action text links (mirrors demo's Refresh / MCP Config pattern) */} +
+ + +
+ {/* Right: Select Tools button (mirrors demo) */} +
+ +
@@ -194,7 +210,6 @@ export default function AgentConfigComp({}: AgentConfigCompProps) { @@ -247,6 +262,20 @@ export default function AgentConfigComp({}: AgentConfigCompProps) { setIsMcpModalOpen(false)} /> + setIsToolSelectOpen(false)} + onOpenManageLabels={() => setLabelModalOpen(true)} + isCreatingMode={isCreatingMode} + currentAgentId={currentAgentId ?? undefined} + /> + + setLabelModalOpen(false)} + availableTools={availableTools} + /> + setIsSkillModalOpen(false)} diff --git a/frontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsx b/frontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsx index 11b1492bc..55b0437b1 100644 --- a/frontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsx @@ -1,635 +1,241 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import ToolConfigModal from "./tool/ToolConfigModal"; -import { ToolGroup, Tool, ToolParam } from "@/types/agentConfig"; -import { Tabs, Collapse, message, Tooltip, Badge } from "antd"; +import { Tooltip } from "antd"; import { useAgentConfigStore } from "@/stores/agentConfigStore"; -import { useToolList } from "@/hooks/agent/useToolList"; import { usePrefetchKnowledgeBases } from "@/hooks/useKnowledgeBaseSelector"; import { useConfig } from "@/hooks/useConfig"; -import { useQueryClient } from "@tanstack/react-query"; -import { useConfirmModal } from "@/hooks/useConfirmModal"; - -import { Settings, AlertTriangle } from "lucide-react"; +import { ChevronRight, Settings, X, AlertTriangle } from "lucide-react"; +import type { Tool, ToolParam } from "@/types/agentConfig"; +import { TOOL_SOURCE_TYPES } from "@/const/agentConfig"; +import ToolConfigModal from "./tool/ToolConfigModal"; import log from "@/lib/logger"; -interface ToolManagementProps { - toolGroups: ToolGroup[]; - isCreatingMode?: boolean; - currentAgentId?: number; -} - -// Tool types that require knowledge base selection +// --- Tool helpers (shared with SelectToolsDialog) --- const TOOLS_REQUIRING_KB_SELECTION = [ - "knowledge_base_search", - "dify_search", - "datamate_search", - "idata_search", - "haotian_search", - "aidp_search", -]; - -// Tool types that require Embedding model -const TOOLS_REQUIRING_EMBEDDING = [ - "knowledge_base_search", + "knowledge_base_search", "dify_search", "datamate_search", + "idata_search", "haotian_search", "aidp_search", ]; - -// Tool types that require the image understanding model -const TOOLS_REQUIRING_IMAGE_UNDERSTANDING = [ - "analyze_image", -]; - -// Tool types that require the video understanding model -const TOOLS_REQUIRING_VIDEO_UNDERSTANDING = [ - "analyze_audio", - "analyze_video", -]; - -function getToolKbType( - toolName: string -): "knowledge_base_search" | "dify_search" | "datamate_search" | "idata_search" | "haotian_search" | "aidp_search" | null { - if (!TOOLS_REQUIRING_KB_SELECTION.includes(toolName)) return null; - if (toolName === "dify_search") return "dify_search"; - if (toolName === "datamate_search") return "datamate_search"; - if (toolName === "idata_search") return "idata_search"; - if (toolName === "haotian_search") return "haotian_search"; - if (toolName === "aidp_search") return "aidp_search"; - return "knowledge_base_search"; +const TOOLS_REQUIRING_EMBEDDING = ["knowledge_base_search"]; +const TOOLS_REQUIRING_IMAGE_UNDERSTANDING = ["analyze_image"]; +const TOOLS_REQUIRING_VIDEO_UNDERSTANDING = ["analyze_audio", "analyze_video"]; + +function getToolKbType(name: string) { + if (!TOOLS_REQUIRING_KB_SELECTION.includes(name)) return null; + if (name === "dify_search") return "dify_search" as const; + if (name === "datamate_search") return "datamate_search" as const; + if (name === "idata_search") return "idata_search" as const; + if (name === "haotian_search") return "haotian_search" as const; + if (name === "aidp_search") return "aidp_search" as const; + return "knowledge_base_search" as const; } -/** - * Check if a tool requires VLM model but VLM is not available - */ -function isToolDisabledDueToVlm( - toolName: string, - imageUnderstandingAvailable: boolean, - videoUnderstandingAvailable: boolean -): boolean { - if (TOOLS_REQUIRING_IMAGE_UNDERSTANDING.includes(toolName)) { - return !imageUnderstandingAvailable; - } - if (TOOLS_REQUIRING_VIDEO_UNDERSTANDING.includes(toolName)) { - return !videoUnderstandingAvailable; - } +function isToolDisabledDueToVlm(name: string, img: boolean, vid: boolean): boolean { + if (TOOLS_REQUIRING_IMAGE_UNDERSTANDING.includes(name)) return !img; + if (TOOLS_REQUIRING_VIDEO_UNDERSTANDING.includes(name)) return !vid; return false; } -/** - * Check if a tool requires Embedding model but Embedding is not available - */ -function isToolDisabledDueToEmbedding(toolName: string, embeddingAvailable: boolean): boolean { - if (!TOOLS_REQUIRING_EMBEDDING.includes(toolName)) return false; - return !embeddingAvailable; +function isToolDisabledDueToEmbedding(name: string, emb: boolean): boolean { + if (!TOOLS_REQUIRING_EMBEDDING.includes(name)) return false; + return !emb; } -/** - * ToolManagement - Component for displaying tools in tabs - * Provides a tabbed interface for tool organization - */ -export default function ToolManagement({ - toolGroups, - isCreatingMode, - currentAgentId -}: ToolManagementProps) { - const { t } = useTranslation("common"); - const queryClient = useQueryClient(); - const { confirm } = useConfirmModal(); - - const isReadOnly = useAgentConfigStore((state) => state.isReadOnly()); +function getToolLabels(tool: any): string[] { + return Array.isArray(tool.labels) ? tool.labels : []; +} - // Get state from store - const originalSelectedTools = useAgentConfigStore( - (state) => state.editedAgent.tools - ); - const originalSelectedToolIdsSet = new Set( - originalSelectedTools.map((tool) => tool.id) - ); +type SourceKey = "local" | "mcp" | "langchain"; +const SOURCE_META: Record< + SourceKey, + { sourceValue: string; label: string; dot: string; accentClass: string } +> = { + local: { sourceValue: TOOL_SOURCE_TYPES.LOCAL, label: "toolPool.group.local", dot: "bg-emerald-500", accentClass: "bg-emerald-500/10 text-emerald-600" }, + mcp: { sourceValue: TOOL_SOURCE_TYPES.MCP, label: "toolPool.group.mcp", dot: "bg-sky-500", accentClass: "bg-sky-500/10 text-sky-600" }, + langchain: { sourceValue: TOOL_SOURCE_TYPES.LANGCHAIN, label: "toolPool.group.langchain", dot: "bg-violet-500", accentClass: "bg-violet-500/10 text-violet-600" }, +}; - const updateTools = useAgentConfigStore((state) => state.updateTools); +interface ToolManagementProps { + isCreatingMode?: boolean; + currentAgentId?: number; +} - // Use tool list hook for data management - const { availableTools } = useToolList(); +/** Display selected tools as grouped, collapsible cards (demo layout). */ +export default function ToolManagement({ isCreatingMode, currentAgentId }: ToolManagementProps) { + const { t } = useTranslation("common"); + const { prefetchKnowledgeBases } = usePrefetchKnowledgeBases(); + const { isImageUnderstandingAvailable, isVideoUnderstandingAvailable, isEmbeddingAvailable } = useConfig(); - const { - isImageUnderstandingAvailable, - isVideoUnderstandingAvailable, - isEmbeddingAvailable, - } = useConfig(); + const selectedTools = useAgentConfigStore((state) => state.editedAgent.tools); + const updateTools = useAgentConfigStore((state) => state.updateTools); - // Prefetch knowledge bases for KB tools - const { prefetchKnowledgeBases } = usePrefetchKnowledgeBases(); + const [modalOpen, setModalOpen] = useState(false); + const [configTool, setConfigTool] = useState(null); + const [configParams, setConfigParams] = useState([]); + const [collapsedCats, setCollapsedCats] = useState>({}); - const [activeTabKey, setActiveTabKey] = useState(""); - const [expandedCategories, setExpandedCategories] = useState>( - new Set() - ); - const [isToolModalOpen, setIsToolModalOpen] = useState(false); - const [selectedTool, setSelectedTool] = useState(null); - const [toolParams, setToolParams] = useState([]); + // --- Group by source → category --- + const grouped = groupToolsBySource(selectedTools); - // Helper function to merge tool parameters with instance parameters - const mergeToolParamsWithInstance = async ( - tool: Tool, - defaultTool: Tool, - agentId?: number - ): Promise => { - if (agentId) { + const mergeParams = useCallback( + async (tool: Tool): Promise => { + const params = tool.initParams || []; + if (!currentAgentId) return params; try { - const { searchToolConfig } = - await import("@/services/agentConfigService"); - const tooInstance = await searchToolConfig(parseInt(tool.id), agentId); - - if (tooInstance.success && tooInstance.data) { - // Merge instance params with default params - // Only use instance value if it exists and is not null/undefined - const mergedParams = - defaultTool.initParams?.map((param: ToolParam) => { - const instanceValue = tooInstance.data?.params?.[param.name]; - // Use instance value only if it's not null or undefined - const hasValidInstanceValue = instanceValue !== null && instanceValue !== undefined; - return { - ...param, - value: hasValidInstanceValue ? instanceValue : param.value, - }; - }) || - defaultTool.initParams || - []; - return mergedParams; - } else { - return defaultTool.initParams || []; + const { searchToolConfig } = await import("@/services/agentConfigService"); + const instance = await searchToolConfig(parseInt(tool.id), currentAgentId); + if (instance.success && instance.data) { + return params.map((p) => ({ + ...p, + value: instance.data?.params?.[p.name] !== undefined ? instance.data.params[p.name] : p.value, + })); } - } catch (error) { - log.error("Failed to fetch tool instance params:", error); - return defaultTool.initParams || []; - } - } else { - return defaultTool.initParams || []; - } - }; - - // Set default active tab - useEffect(() => { - if (toolGroups.length > 0 && !activeTabKey) { - setActiveTabKey(toolGroups[0].key); - } - }, [toolGroups, activeTabKey]); - - const handleToolSettingsClick = async (tool: Tool) => { - // Prefetch knowledge bases for KB tools - const kbType = getToolKbType(tool.name); - if (kbType) { - prefetchKnowledgeBases(kbType); - } - - // Get latest tools directly from store to avoid stale closure issues - const currentTools = useAgentConfigStore.getState().editedAgent.tools; - const configuredTool = currentTools.find( - (t) => parseInt(t.id) === parseInt(tool.id) - ); - // Merge configured tool with original tool to ensure all fields are present - const toolToUse = configuredTool ? { ...tool, ...configuredTool, initParams: configuredTool.initParams } : tool; - - // Get merged parameters (for editing mode, merge with instance params) - const mergedParams = await mergeToolParamsWithInstance( - tool, - toolToUse, - isCreatingMode ? undefined : currentAgentId - ); - - setSelectedTool(toolToUse); - setToolParams(mergedParams); - setIsToolModalOpen(true); - }; + } catch (err) { log.error("mergeParams:", err); } + return params; + }, + [currentAgentId] + ); - const handleToolClick = async (toolId: string) => { - const numericId = parseInt(toolId, 10); - const tool = availableTools.find((t) => parseInt(t.id) === numericId); + const openConfig = useCallback( + async (tool: Tool) => { + const kbType = getToolKbType(tool.name); + if (kbType) prefetchKnowledgeBases(kbType); + const current = useAgentConfigStore.getState().editedAgent.tools; + const configured = current.find((t) => parseInt(t.id) === parseInt(tool.id)); + const toolToUse = configured ? { ...tool, ...configured, initParams: configured.initParams } : tool; + const merged = await mergeParams(toolToUse); + setConfigTool(toolToUse); + setConfigParams(merged); + setModalOpen(true); + }, + [mergeParams, prefetchKnowledgeBases] + ); - if (!tool) return; + const removeTool = useCallback( + (toolId: string) => { + const current = useAgentConfigStore.getState().editedAgent.tools; + updateTools(current.filter((t) => t.id !== toolId)); + }, + [updateTools] + ); - // Prefetch knowledge bases for KB tools - const kbType = getToolKbType(tool.name); - if (kbType) { - prefetchKnowledgeBases(kbType); - } + const toggleCat = (cat: string) => setCollapsedCats((p) => ({ ...p, [cat]: !p[cat] })); - // Get latest tools directly from store to avoid stale closure issues - const currentSelectdTools = useAgentConfigStore.getState().editedAgent.tools; - const isCurrentlySelected = currentSelectdTools.some( - (t) => parseInt(t.id) === numericId + if (grouped.length === 0) { + return ( +
+ {t("toolPool.noToolsSelected")} +
); + } - if (isCurrentlySelected) { - // If already selected, deselect it - const newSelectedTools = currentSelectdTools.filter((t) => parseInt(t.id) !== numericId); - updateTools(newSelectedTools); - } else { - // Helper function to proceed with tool selection after duplicate check - async function proceedWithToolSelection() { - // Get latest tools again to ensure we have the most up-to-date list - const currentSelectdTools = - useAgentConfigStore.getState().editedAgent.tools; - - // Determine tool params and check if modal is needed - const configuredTool = currentSelectdTools.find( - (t) => parseInt(t.id) === numericId - ); - // Merge configured tool with original tool to ensure all fields are present - const toolToUse = configuredTool - ? { ...tool, ...configuredTool, initParams: configuredTool.initParams } - : tool; - - // Get merged parameters (for editing mode, merge with instance params) - const mergedParams = await mergeToolParamsWithInstance( - tool, - toolToUse, - isCreatingMode ? undefined : currentAgentId! - ); - - // Check if there are empty required params - const hasEmptyRequiredParams = mergedParams.some( - (param: ToolParam) => - param.required && - (param.value === undefined || - param.value === "" || - param.value === null) - ); - - if (hasEmptyRequiredParams) { - // Need to configure, open modal - setSelectedTool(toolToUse); - setToolParams(mergedParams); - setIsToolModalOpen(true); - } else { - // No required params missing, add directly - const newSelectedTools = [ - ...currentSelectdTools, - { - ...toolToUse, - initParams: mergedParams, - }, - ]; - updateTools(newSelectedTools); - } - } - - // If not selected, check for duplicate tool names first - const duplicateTool = currentSelectdTools.find( - (selectedTool) => selectedTool.name === tool.name - ); - - if (duplicateTool) { - // Show confirmation modal for duplicate tool name - return new Promise((resolve) => { - confirm({ - title: t("toolPool.duplicateToolName.title"), - content: t("toolPool.duplicateToolName.content", { - toolName: tool.name, - }), - okText: t("toolPool.duplicateToolName.confirm"), - cancelText: t("toolPool.duplicateToolName.cancel"), - danger: true, - onOk: async () => { - // User confirmed, proceed with tool selection - await proceedWithToolSelection(); - resolve(); - }, - onCancel: () => { - // User cancelled, do nothing - resolve(); - }, - }); - }); - } - - // No duplicate, proceed with normal tool selection - await proceedWithToolSelection(); - } - }; - - // Generate Tabs configuration - const tabItems = toolGroups.map((group) => { - const label = t(group.label); - const selectedCount = group.subGroups - ? group.subGroups.reduce( - (sum, sg) => sum + sg.tools.filter(t => originalSelectedToolIdsSet.has(t.id)).length, 0) - : group.tools.filter(t => originalSelectedToolIdsSet.has(t.id)).length; + return ( +
+
+ + {t("toolPool.selectedToolsLabel")} + ({selectedTools.length}) + +
+ +
+ {grouped.map((src) => ( +
+
+ + {t(SOURCE_META[src.key].label)}({src.totalCount}) +
- return { - key: group.key, - label: ( - - - - {label} - - {selectedCount > 0 && ( - - )} - - - ), - children: ( -
- {group.subGroups ? ( - <> - {/* Collapsible categories using Ant Design Collapse */} -
- { - const newSet = new Set( - typeof keys === "string" ? [keys] : keys - ); - setExpandedCategories(newSet); - }} - ghost - size="small" - className="tool-categories-collapse mt-1" - items={group.subGroups.map((subGroup, index) => ({ - key: subGroup.key, - label: ( - - - {subGroup.label} - - {subGroup.tools.filter(t => originalSelectedToolIdsSet.has(t.id)).length > 0 && ( - originalSelectedToolIdsSet.has(t.id)).length} - size="small" - color="blue" - /> - )} +
+ {src.categories.map((cat) => { + const catKey = `${src.key}-${cat.category}`; + const isCollapsed = collapsedCats[catKey] ?? false; + const accent = SOURCE_META[src.key].accentClass; + + return ( +
+ + + {!isCollapsed && ( +
+ {cat.tools.map((tool) => { + const labels = getToolLabels(tool); + const disabled = + isToolDisabledDueToVlm(tool.name, isImageUnderstandingAvailable, isVideoUnderstandingAvailable) || + isToolDisabledDueToEmbedding(tool.name, isEmbeddingAvailable); + + return ( +
+
+
+ + {tool.name} + + {labels.slice(0, 2).map((l) => ( + + {l} + + ))} + {labels.length > 2 && ( + + + +{labels.length - 2} + + + )} + {disabled && } +
- { - e.stopPropagation(); - handleToolSettingsClick(tool); - } - : undefined - } - /> + + + +
); - return tooltipTitle ? ( - - {toolCard} - - ) : ( - toolCard - ); })}
- ), - }))} - /> -
- - ) : ( - // Regular layout for non-local tools -
- {group.tools.map((tool) => { - const isSelected = originalSelectedToolIdsSet.has(tool.id); - const isDisabledDueToVlm = isToolDisabledDueToVlm( - tool.name, - isImageUnderstandingAvailable, - isVideoUnderstandingAvailable - ); - const isDisabledDueToEmbedding = isToolDisabledDueToEmbedding(tool.name, isEmbeddingAvailable); - const isDisabled = isDisabledDueToVlm || isDisabledDueToEmbedding || isReadOnly; - // Tooltip priority: permission > VLM > Embedding - const tooltipTitle = isDisabledDueToVlm - ? t("toolPool.vlmDisabledTooltip") - : isDisabledDueToEmbedding - ? t("toolPool.embeddingDisabledTooltip") - : undefined; - const toolCard = ( -
handleToolClick(tool.id) : undefined - } - > -
- {tool.name} - {isDisabledDueToVlm && ( - - - - )} - {isDisabledDueToEmbedding && ( - - - - )} -
- { - e.stopPropagation(); - handleToolSettingsClick(tool); - } - : undefined - } - /> + )}
); - return tooltipTitle ? ( - - {toolCard} - - ) : ( - toolCard - ); })}
- )} -
- ), - }; - }); - - return ( -
- {toolGroups.length === 0 ? ( -
- {t("toolPool.noTools")} -
- ) : ( - - )} +
+ ))} +
- {isToolModalOpen && ( + {modalOpen && ( { - setIsToolModalOpen(false); - setSelectedTool(null); - setToolParams([]); - }} - tool={selectedTool!} - initialParams={toolParams} - selectedTool={selectedTool} + isOpen={modalOpen} + onCancel={() => { setModalOpen(false); setConfigTool(null); setConfigParams([]); }} + tool={configTool!} + initialParams={configParams} + selectedTool={configTool} isCreatingMode={isCreatingMode} currentAgentId={currentAgentId} /> @@ -637,3 +243,31 @@ export default function ToolManagement({
); } + +// ─── Pure helper ───────────────────────────────────────────────────────────── + +interface CatGroup { category: string; tools: Tool[] } +interface SourceGroup { key: SourceKey; categories: CatGroup[]; totalCount: number } + +function groupToolsBySource(tools: Tool[]): SourceGroup[] { + const result: SourceGroup[] = []; + for (const [key, meta] of Object.entries(SOURCE_META) as [SourceKey, typeof SOURCE_META[SourceKey]][]) { + const srcTools = tools.filter((t: any) => t.source === meta.sourceValue); + if (srcTools.length === 0) continue; + const catMap = new Map(); + for (const tool of srcTools) { + const cat = (tool as any).category?.trim() || "toolPool.category.other"; + if (!catMap.has(cat)) catMap.set(cat, []); + catMap.get(cat)!.push(tool); + } + const categories = Array.from(catMap.entries()) + .map(([cat, ts]) => ({ category: cat, tools: ts })) + .sort((a, b) => { + if (a.category === "toolPool.category.other") return 1; + if (b.category === "toolPool.category.other") return -1; + return a.category.localeCompare(b.category); + }); + result.push({ key, categories, totalCount: srcTools.length }); + } + return result; +} diff --git a/frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx new file mode 100644 index 000000000..529fd9295 --- /dev/null +++ b/frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { Modal, Table, Select, App } from "antd"; +import type { ColumnsType } from "antd/es/table"; +import { useQueryClient } from "@tanstack/react-query"; +import { API_ENDPOINTS } from "@/services/api"; +import { getAuthHeaders } from "@/lib/auth"; +import log from "@/lib/logger"; + +interface LabelManagementModalProps { + open: boolean; + onClose: () => void; + availableTools: any[]; +} + +interface ToolRow { + id: string; + name: string; + source: string; + labels: string[]; +} + +export default function LabelManagementModal({ + open, + onClose, + availableTools, +}: LabelManagementModalProps) { + const { t } = useTranslation("common"); + const { message } = App.useApp(); + const queryClient = useQueryClient(); + + const [dataSource, setDataSource] = useState([]); + const builtRef = useRef(false); + + // Collect all unique labels from dataSource for Select suggestions + const allExistingLabels = useMemo(() => { + const labelSet = new Set(); + dataSource.forEach((row) => row.labels.forEach((l: string) => labelSet.add(l))); + return Array.from(labelSet).sort(); + }, [dataSource]); + + // Build dataSource once per open cycle. Using builtRef lets us + // handle the case where availableTools arrives after the modal is already open. + useEffect(() => { + if (open) { + if (!builtRef.current && availableTools.length > 0) { + const rows: ToolRow[] = availableTools.map((tool: any) => ({ + id: tool.id, + name: tool.name, + source: tool.source || "", + labels: Array.isArray(tool.labels) ? [...tool.labels] : [], + })); + setDataSource(rows); + builtRef.current = true; + } + } else { + setDataSource([]); + builtRef.current = false; + } + }, [open, availableTools]); + + const handleLabelsChange = useCallback( + async (toolId: string, newLabels: string[]) => { + // Optimistically update local state + setDataSource((prev) => + prev.map((row) => + row.id === toolId ? { ...row, labels: newLabels } : row + ) + ); + + // Persist to backend, then synchronously update cache so parent sees fresh data + try { + await fetch(API_ENDPOINTS.tool.labels, { + method: "PUT", + headers: { ...getAuthHeaders(), "Content-Type": "application/json" }, + body: JSON.stringify({ tool_id: parseInt(toolId), labels: newLabels }), + }); + // Synchronous cache update — no timing gaps, no refetch race + queryClient.setQueryData(["tools"], (old: any[]) => { + if (!old) return old; + return old.map((tool: any) => + tool.id === toolId ? { ...tool, labels: newLabels } : tool + ); + }); + } catch (err) { + log.warn("Failed to update tool labels:", err); + message.error(t("toolConfig.message.labelsSaveFailed")); + } + }, + [message, t, queryClient] + ); + + const columns: ColumnsType = [ + { + title: t("toolConfig.column.toolName"), + dataIndex: "name", + key: "name", + width: 200, + }, + { + title: t("toolConfig.column.source"), + dataIndex: "source", + key: "source", + width: 100, + }, + { + title: t("toolConfig.column.labels"), + dataIndex: "labels", + key: "labels", + render: (labels: string[], record: ToolRow) => ( + setSearch(e.target.value)} + placeholder={t("toolPool.searchToolsPlaceholder")} + className="pl-9" + allowClear + /> +
+ +
+ {/* Category sidebar */} + + + {/* Tool list */} +
+ {currentGroups + .filter((g) => g.category === activeCategory) + .map((g) => ( +
+
+ {g.category} +
+
    + {g.tools.map((tool: any) => { + const isSelected = selectedToolIds.has(parseInt(tool.id)); + const disabled = isToolDisabled( + tool.name, + isImageUnderstandingAvailable, + isVideoUnderstandingAvailable, + isEmbeddingAvailable + ); + + return ( +
  • +
    handleToolToggle(tool)} + > + +
    +
    + + {tool.name} + + {getToolLabels(tool) + .slice(0, 2) + .map((label: string) => ( + + {label} + + ))} +
    + {tool.description && ( +

    + {tool.description} +

    + )} +
    + {!disabled && ( + + )} +
    +
  • + ); + })} +
+
+ ))} + {currentGroups.filter((g) => g.category === activeCategory).length === 0 && + search.trim() !== "" && ( +
+ {t("toolPool.noSearchResults")} +
+ )} +
+
+ + + { + setConfigModalOpen(false); + setConfigTool(null); + setConfigParams([]); + }} + tool={configTool!} + initialParams={configParams} + selectedTool={configTool} + isCreatingMode={isCreatingMode} + currentAgentId={currentAgentId} + /> + + ); +} diff --git a/frontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsx index e34e5d4a8..b6b8fd49f 100644 --- a/frontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsx @@ -1429,6 +1429,7 @@ export default function ToolConfigModal({ // Update local state only - actual save will happen when user clicks "Save Agent" updateTools(newSelectedTools); + message.success(t("toolConfig.message.saveSuccess")); handleClose(); // Close modal diff --git a/frontend/hooks/agent/useToolList.ts b/frontend/hooks/agent/useToolList.ts index 1a9c00dba..479254299 100644 --- a/frontend/hooks/agent/useToolList.ts +++ b/frontend/hooks/agent/useToolList.ts @@ -1,7 +1,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { fetchTools } from "@/services/agentConfigService"; import { useMemo } from "react"; -import { Tool, ToolGroup, ToolSubGroup } from "@/types/agentConfig"; +import { ToolGroup, ToolSubGroup } from "@/types/agentConfig"; import { TOOL_SOURCE_TYPES } from "@/const/agentConfig"; export function useToolList(options?: { enabled?: boolean; staleTime?: number }) { @@ -28,11 +28,20 @@ export function useToolList(options?: { enabled?: boolean; staleTime?: number }) return (tools as any[]).filter((tool) => tool.is_available !== false); }, [tools]); - // Grouped tools helper function - returns a function that can be called with translation - // Default grouped tools without selected tool filtering + // Extract all unique labels from available tools (used by LabelManagementModal suggestions) + const allLabels = useMemo(() => { + const labelSet = new Set(); + availableTools.forEach((tool: any) => { + const labels = Array.isArray(tool.labels) ? tool.labels : []; + labels.forEach((l: string) => labelSet.add(l)); + }); + return Array.from(labelSet).sort(); + }, [availableTools]); + + // Grouped tools by source and usage const groupedTools = useMemo(() => { const groups: ToolGroup[] = []; - const groupMap = new Map(); + const groupMap = new Map(); // Group by source and usage availableTools.forEach((tool) => { @@ -68,7 +77,7 @@ export function useToolList(options?: { enabled?: boolean; staleTime?: number }) // Create secondary grouping for local tools let subGroups: ToolSubGroup[] | undefined; if (key === TOOL_SOURCE_TYPES.LOCAL) { - const categoryMap = new Map(); + const categoryMap = new Map(); sortedTools.forEach((tool) => { const category = @@ -120,13 +129,14 @@ export function useToolList(options?: { enabled?: boolean; staleTime?: number }) }; return getPriority(a.key) - getPriority(b.key); }); - }, [tools]); + }, [availableTools]); return { ...query, tools, availableTools, groupedTools, + allLabels, invalidate: () => queryClient.invalidateQueries({ queryKey: ["tools"] }), }; } diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 6c12da70f..ff43e852d 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -498,6 +498,12 @@ "subAgentPool.message.duplicateNameDisabled": "This Agent is disabled due to duplicate name (or display name) with other Agents. Please change the name to use it", "toolConfig.title.paramConfig": "Parameter Configuration", + "toolConfig.title.manageLabels": "Manage Labels", + "toolConfig.column.toolName": "Tool Name", + "toolConfig.column.source": "Source", + "toolConfig.column.labels": "Labels", + "toolConfig.labelPlaceholder": "Add labels...", + "toolConfig.message.labelsSaveFailed": "Failed to save labels", "toolConfig.message.loadError": "Failed to load tool configuration", "toolConfig.message.loadErrorUseDefault": "Failed to load tool configuration, using default configuration", "toolConfig.message.saveSuccess": "Skill configuration saved successfully", @@ -565,6 +571,9 @@ "toolPool.group.other": "Other Tools", "toolPool.category.other": "other", "toolPool.noTools": "No tools available", + "toolPool.filterByLabel": "Filter by label", + "toolPool.noLabelsAssigned": "No labels yet", + "toolPool.manageLabels": "Manage Labels", "toolPool.error.requiredFields": "The following required fields are not filled: {{fields}}", "toolPool.vlmRequired": "VLM model required", "toolPool.vlmDisabledTooltip": "Please contact your administrator to configure an available Vision Language Model", @@ -574,6 +583,13 @@ "toolPool.duplicateToolName.content": "You have selected tools with the same name ({{toolName}}). Duplicate tool names will cause the agent to fail during runtime. Do you want to continue selecting this tool?", "toolPool.duplicateToolName.confirm": "Continue", "toolPool.duplicateToolName.cancel": "Cancel", + "toolPool.selectTools": "Select Tools", + "toolPool.searchToolsPlaceholder": "Search tools...", + "toolPool.noToolsSelected": "No tools selected yet, click Select Tools to add", + "toolPool.configure": "Configure", + "toolPool.remove": "Remove", + "toolPool.selectedToolsLabel": "Selected Tools", + "toolPool.noSearchResults": "No tools match your search", "tool.message.unavailable": "This tool is currently unavailable and cannot be selected", "tool.error.noMainAgentId": "Main Agent ID is not set, cannot update tool status", diff --git a/frontend/public/locales/zh/common.json b/frontend/public/locales/zh/common.json index 53a0d88d1..247604134 100644 --- a/frontend/public/locales/zh/common.json +++ b/frontend/public/locales/zh/common.json @@ -471,6 +471,12 @@ "subAgentPool.message.duplicateNameDisabled": "该智能体因与其他智能体名称(或变量名)相同而被禁用,请修改名称后使用", "toolConfig.title.paramConfig": "配置参数", + "toolConfig.title.manageLabels": "管理标签", + "toolConfig.column.toolName": "工具名称", + "toolConfig.column.source": "来源", + "toolConfig.column.labels": "标签", + "toolConfig.labelPlaceholder": "添加标签...", + "toolConfig.message.labelsSaveFailed": "保存标签失败", "toolConfig.message.loadError": "加载工具配置失败", "toolConfig.message.loadErrorUseDefault": "加载工具配置失败,使用默认配置", "toolConfig.message.saveSuccess": "技能配置保存成功", @@ -538,6 +544,9 @@ "toolPool.group.other": "其他工具", "toolPool.category.other": "其他", "toolPool.noTools": "暂无可用工具", + "toolPool.filterByLabel": "按标签筛选", + "toolPool.noLabelsAssigned": "暂无标签", + "toolPool.manageLabels": "管理标签", "toolPool.error.requiredFields": "以下必填字段未填写: {{fields}}", "toolPool.vlmRequired": "需要配置视觉语言模型", "toolPool.vlmDisabledTooltip": "请联系管理员配置可用的视觉语言模型", @@ -547,6 +556,13 @@ "toolPool.duplicateToolName.content": "您已勾选相同工具名的工具({{toolName}}),重复选择会导致智能体无法正常运行。是否继续勾选?", "toolPool.duplicateToolName.confirm": "继续", "toolPool.duplicateToolName.cancel": "取消", + "toolPool.selectTools": "选择工具", + "toolPool.searchToolsPlaceholder": "搜索工具名称或描述…", + "toolPool.noToolsSelected": "暂未选择工具,点击「选择工具」添加", + "toolPool.configure": "配置", + "toolPool.remove": "移除", + "toolPool.selectedToolsLabel": "已选择工具", + "toolPool.noSearchResults": "没有搜索到匹配的工具", "tool.message.unavailable": "该工具当前不可用,无法选择", "tool.error.noMainAgentId": "主代理ID未设置,无法更新工具状态", diff --git a/frontend/services/agentConfigService.ts b/frontend/services/agentConfigService.ts index 5f180af72..35ed54a2f 100644 --- a/frontend/services/agentConfigService.ts +++ b/frontend/services/agentConfigService.ts @@ -87,6 +87,11 @@ export const fetchTools = async () => { create_time: tool.create_time, usage: tool.usage, // New: handle usage field category: tool.category, + labels: Array.isArray(tool.labels) + ? tool.labels + : typeof tool.labels === 'string' + ? (() => { try { const p = JSON.parse(tool.labels); return Array.isArray(p) ? p : []; } catch { return []; } })() + : [], inputs: tool.inputs, initParams: tool.params.map((param: any) => { return { diff --git a/frontend/services/api.ts b/frontend/services/api.ts index fe616d9f2..9a77d1487 100644 --- a/frontend/services/api.ts +++ b/frontend/services/api.ts @@ -121,6 +121,7 @@ export const API_ENDPOINTS = { openapiServices: `${API_BASE_URL}/tool/openapi_services`, deleteOpenapiService: (serviceName: string) => `${API_BASE_URL}/tool/openapi_service/${encodeURIComponent(serviceName)}`, + labels: `${API_BASE_URL}/tool/labels`, }, prompt: { generate: `${API_BASE_URL}/prompt/generate`, diff --git a/frontend/types/agentConfig.ts b/frontend/types/agentConfig.ts index d4f084a45..6b2ce6098 100644 --- a/frontend/types/agentConfig.ts +++ b/frontend/types/agentConfig.ts @@ -129,6 +129,7 @@ export interface Tool { usage?: string; inputs?: string; category?: string; + labels?: string[]; /** * Knowledge base display names associated with this tool. * This is populated when the tool (e.g., knowledge_base_search) has knowledge bases configured. diff --git a/sdk/nexent/core/agents/agent_model.py b/sdk/nexent/core/agents/agent_model.py index 5e5a3adfa..33ad453e2 100644 --- a/sdk/nexent/core/agents/agent_model.py +++ b/sdk/nexent/core/agents/agent_model.py @@ -132,6 +132,7 @@ class ToolConfig(BaseModel): source: str = Field(description="Tool source, can be local or mcp") usage: Optional[str] = Field(description="MCP server name", default=None) metadata: Optional[Dict[str, Any]] = Field(description="Metadata", default=None) + labels: Optional[List[str]] = Field(description="Tool labels for filtering", default=None) VerificationEvent = Literal[ From 2aa5c819edff747bead0d0388c078749494fd6cd Mon Sep 17 00:00:00 2001 From: MoeexT Date: Sat, 27 Jun 2026 15:41:50 +0800 Subject: [PATCH 02/16] :art: improve UI --- .../agentConfig/tool/LabelManagementModal.tsx | 4 +- .../agentConfig/tool/SelectToolsDialog.tsx | 85 ++++++++++++++----- frontend/public/locales/en/common.json | 2 +- frontend/public/locales/zh/common.json | 2 +- 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx index 529fd9295..d5cf49693 100644 --- a/frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/tool/LabelManagementModal.tsx @@ -138,8 +138,8 @@ export default function LabelManagementModal({ columns={columns} rowKey="id" size="small" - pagination={{ pageSize: 15, size: "small" }} - scroll={{ y: 400 }} + pagination={{ pageSize: 25, size: "small" }} + scroll={{ y: 600 }} /> ); diff --git a/frontend/app/[locale]/agents/components/agentConfig/tool/SelectToolsDialog.tsx b/frontend/app/[locale]/agents/components/agentConfig/tool/SelectToolsDialog.tsx index 37cd20b68..d4a9fd0ff 100644 --- a/frontend/app/[locale]/agents/components/agentConfig/tool/SelectToolsDialog.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/tool/SelectToolsDialog.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { Modal, Tabs, Input, Checkbox, Button } from "antd"; +import { Modal, Tabs, Input, Checkbox, Button, Select } from "antd"; import type { TabsProps } from "antd"; import { Search, Settings, Wrench, Tag } from "lucide-react"; @@ -81,6 +81,18 @@ export default function SelectToolsDialog({ const [activeTab, setActiveTab] = useState("local"); const [activeCategory, setActiveCategory] = useState(""); + // Collect all unique labels from available tools for filter dropdown + const allLabels = useMemo(() => { + const labelSet = new Set(); + availableTools.forEach((tool: any) => { + const labels = getToolLabels(tool); + labels.forEach((l: string) => labelSet.add(l)); + }); + return Array.from(labelSet).sort(); + }, [availableTools]); + + const [activeLabels, setActiveLabels] = useState([]); + // ToolConfigModal — handles add/update to store internally on save const [configModalOpen, setConfigModalOpen] = useState(false); const [configTool, setConfigTool] = useState(null); @@ -113,22 +125,36 @@ export default function SelectToolsDialog({ return result; }, [availableTools]); - // --- Filtered current tab data by search --- + // --- Filtered current tab data by search + labels (AND) --- const currentGroups = useMemo(() => { const groups = sourceGroups[activeTab] || []; - if (!search.trim()) return groups; - const kw = search.toLowerCase(); + const kw = search.trim().toLowerCase(); + const hasSearch = kw !== ""; + const hasLabels = activeLabels.length > 0; + + if (!hasSearch && !hasLabels) return groups; + + const filterOne = (tool: any): boolean => { + // Search filter (OR across name/desc/tags) + if (hasSearch) { + const matchSearch = + tool.name.toLowerCase().includes(kw) || + (tool.description && tool.description.toLowerCase().includes(kw)) || + getToolLabels(tool).some((l: string) => l.toLowerCase().includes(kw)); + if (!matchSearch) return false; + } + // Label filter (OR — tool must have at least one selected label) + if (hasLabels) { + const toolLabels = getToolLabels(tool); + if (!toolLabels.some((l: string) => activeLabels.includes(l))) return false; + } + return true; + }; + return groups - .map((g) => ({ - ...g, - tools: g.tools.filter( - (t: any) => - t.name.toLowerCase().includes(kw) || - (t.description && t.description.toLowerCase().includes(kw)) - ), - })) + .map((g) => ({ ...g, tools: g.tools.filter(filterOne) })) .filter((g) => g.tools.length > 0); - }, [sourceGroups, activeTab, search]); + }, [sourceGroups, activeTab, search, activeLabels]); const visibleCategories = useMemo(() => currentGroups.map((g) => g.category), [currentGroups]); @@ -287,14 +313,33 @@ export default function SelectToolsDialog({ > -
- - setSearch(e.target.value)} - placeholder={t("toolPool.searchToolsPlaceholder")} - className="pl-9" +
+
+ + setSearch(e.target.value)} + placeholder={t("toolPool.searchToolsPlaceholder")} + className="pl-7" + allowClear + /> +
+