diff --git a/.github/workflows/codeql_main.yml b/.github/workflows/codeql_main.yml new file mode 100644 index 000000000..85905eb55 --- /dev/null +++ b/.github/workflows/codeql_main.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '40 13 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/backend/apps/config_app.py b/backend/apps/config_app.py index fb6a0a4f0..1f6c4214f 100644 --- a/backend/apps/config_app.py +++ b/backend/apps/config_app.py @@ -12,6 +12,7 @@ from apps.mock_user_management_app import router as mock_user_management_router from apps.model_managment_app import router as model_manager_router from apps.prompt_app import router as prompt_router +from apps.prompt_template_app import router as prompt_template_router from apps.remote_mcp_app import router as remote_mcp_router from apps.tenant_config_app import router as tenant_config_router from apps.tool_config_app import router as tool_config_router @@ -50,6 +51,7 @@ app.include_router(summary_router) app.include_router(prompt_router) +app.include_router(prompt_template_router) app.include_router(tenant_config_router) app.include_router(remote_mcp_router) app.include_router(tenant_router) diff --git a/backend/apps/prompt_template_app.py b/backend/apps/prompt_template_app.py new file mode 100644 index 000000000..a2ebb63ba --- /dev/null +++ b/backend/apps/prompt_template_app.py @@ -0,0 +1,108 @@ +import logging +from http import HTTPStatus +from typing import Optional + +from fastapi import APIRouter, Header, HTTPException, Query +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse + +from consts.model import ( + PromptTemplateCreateRequest, + PromptTemplateUpdateRequest, + PromptTemplateDeleteRequest, +) +from services.prompt_template_service import ( + list_templates, + create_template, + update_template, + delete_template, +) +from utils.auth_utils import get_current_user_id + +router = APIRouter(prefix="/prompt_template") +logger = logging.getLogger("prompt_template_app") + + +@router.get("/list") +async def list_prompt_templates_api( + keyword: Optional[str] = Query(default=None), + authorization: Optional[str] = Header(None) +): + try: + user_id, tenant_id = get_current_user_id(authorization) + templates = list_templates(tenant_id=tenant_id, user_id=user_id, keyword=keyword) + return JSONResponse( + status_code=HTTPStatus.OK, + content={"message": "Templates retrieved successfully", "data": jsonable_encoder(templates)} + ) + except Exception as e: + logger.error(f"Failed to list prompt templates: {e}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to list prompt templates") + + +@router.post("/create") +async def create_prompt_template_api( + request: PromptTemplateCreateRequest, + authorization: Optional[str] = Header(None) +): + try: + user_id, tenant_id = get_current_user_id(authorization) + template = create_template(tenant_id=tenant_id, user_id=user_id, payload=request.dict()) + return JSONResponse( + status_code=HTTPStatus.CREATED, + content={"message": "Template created successfully", "data": jsonable_encoder(template)} + ) + except Exception as e: + logger.error(f"Failed to create prompt template: {e}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to create prompt template") + + +@router.post("/update") +async def update_prompt_template_api( + request: PromptTemplateUpdateRequest, + authorization: Optional[str] = Header(None) +): + try: + user_id, tenant_id = get_current_user_id(authorization) + template = update_template( + tenant_id=tenant_id, + user_id=user_id, + template_id=request.template_id, + payload=request.dict(exclude={"template_id"}) + ) + return JSONResponse( + status_code=HTTPStatus.OK, + content={"message": "Template updated successfully", "data": jsonable_encoder(template)} + ) + except ValueError as e: + logger.warning(f"Prompt template not found: {e}") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Prompt template not found") + except Exception as e: + logger.error(f"Failed to update prompt template: {e}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update prompt template") + + +@router.post("/delete") +async def delete_prompt_template_api( + request: PromptTemplateDeleteRequest, + authorization: Optional[str] = Header(None) +): + try: + user_id, tenant_id = get_current_user_id(authorization) + delete_template(tenant_id=tenant_id, user_id=user_id, template_id=request.template_id) + return JSONResponse( + status_code=HTTPStatus.OK, + content={"message": "Template deleted successfully"} + ) + except ValueError as e: + logger.warning(f"Prompt template not found: {e}") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Prompt template not found") + except Exception as e: + logger.error(f"Failed to delete prompt template: {e}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to delete prompt template") diff --git a/backend/consts/model.py b/backend/consts/model.py index ae8e576a8..28f7131b0 100644 --- a/backend/consts/model.py +++ b/backend/consts/model.py @@ -256,6 +256,23 @@ class GenerateTitleRequest(BaseModel): question: str +class PromptTemplateCreateRequest(BaseModel): + name: str + description: Optional[str] = None + prompt_text: str + + +class PromptTemplateUpdateRequest(BaseModel): + template_id: int + name: Optional[str] = None + description: Optional[str] = None + prompt_text: Optional[str] = None + + +class PromptTemplateDeleteRequest(BaseModel): + template_id: int + + # used in agent/search agent/update for save agent info class AgentInfoRequest(BaseModel): agent_id: Optional[int] = None diff --git a/backend/database/db_models.py b/backend/database/db_models.py index 36f475f53..d71acb3bd 100644 --- a/backend/database/db_models.py +++ b/backend/database/db_models.py @@ -289,6 +289,23 @@ class TenantConfig(TableBase): config_value = Column(Text, doc="the value of the config") +class PromptTemplate(TableBase): + """ + Prompt template management table + """ + __tablename__ = "prompt_template_t" + __table_args__ = {"schema": SCHEMA} + + template_id = Column(Integer, Sequence( + "prompt_template_t_template_id_seq", schema=SCHEMA), primary_key=True, nullable=False, doc="Template ID") + name = Column(String(200), doc="Template name") + description = Column(String(2000), doc="Template description") + prompt_text = Column(Text, doc="Template prompt text") + is_builtin = Column(Boolean, default=False, + doc="Whether this template is built-in") + tenant_id = Column(String(100), doc="Tenant ID") + + class MemoryUserConfig(TableBase): """ Tenant configuration information table diff --git a/backend/database/prompt_template_db.py b/backend/database/prompt_template_db.py new file mode 100644 index 000000000..920a02fc5 --- /dev/null +++ b/backend/database/prompt_template_db.py @@ -0,0 +1,100 @@ +import logging +from typing import List, Optional + +from sqlalchemy import or_ + +from database.client import get_db_session, as_dict, filter_property +from database.db_models import PromptTemplate + +logger = logging.getLogger("prompt_template_db") + + +def list_prompt_templates(tenant_id: str, keyword: Optional[str] = None) -> List[dict]: + with get_db_session() as session: + query = session.query(PromptTemplate).filter( + PromptTemplate.tenant_id == tenant_id, + PromptTemplate.delete_flag != "Y" + ) + if keyword: + like_keyword = f"%{keyword}%" + query = query.filter( + or_( + PromptTemplate.name.ilike(like_keyword), + PromptTemplate.description.ilike(like_keyword), + PromptTemplate.prompt_text.ilike(like_keyword), + ) + ) + templates = query.order_by(PromptTemplate.update_time.desc()).all() + return [as_dict(t) for t in templates] + + +def get_prompt_template_by_id(template_id: int, tenant_id: str) -> Optional[dict]: + with get_db_session() as session: + template = session.query(PromptTemplate).filter( + PromptTemplate.template_id == template_id, + PromptTemplate.tenant_id == tenant_id, + PromptTemplate.delete_flag != "Y" + ).first() + return as_dict(template) if template else None + + +def create_prompt_template( + template_info: dict, + tenant_id: str, + user_id: str +) -> dict: + info_with_metadata = dict(template_info) + info_with_metadata.update({ + "tenant_id": tenant_id, + "created_by": user_id, + "updated_by": user_id, + }) + with get_db_session() as session: + new_template = PromptTemplate( + **filter_property(info_with_metadata, PromptTemplate) + ) + new_template.delete_flag = "N" + session.add(new_template) + session.flush() + return as_dict(new_template) + + +def update_prompt_template( + template_id: int, + template_info: dict, + tenant_id: str, + user_id: str +) -> dict: + with get_db_session() as session: + template = session.query(PromptTemplate).filter( + PromptTemplate.template_id == template_id, + PromptTemplate.tenant_id == tenant_id, + PromptTemplate.delete_flag != "Y" + ).first() + if not template: + raise ValueError("prompt template not found") + + for key, value in filter_property(template_info, PromptTemplate).items(): + if value is None: + continue + setattr(template, key, value) + template.updated_by = user_id + session.flush() + return as_dict(template) + + +def delete_prompt_template( + template_id: int, + tenant_id: str, + user_id: str +) -> None: + with get_db_session() as session: + template = session.query(PromptTemplate).filter( + PromptTemplate.template_id == template_id, + PromptTemplate.tenant_id == tenant_id, + PromptTemplate.delete_flag != "Y" + ).first() + if not template: + raise ValueError("prompt template not found") + template.delete_flag = "Y" + template.updated_by = user_id diff --git a/backend/services/prompt_template_service.py b/backend/services/prompt_template_service.py new file mode 100644 index 000000000..bccdc7aec --- /dev/null +++ b/backend/services/prompt_template_service.py @@ -0,0 +1,90 @@ +import logging +from typing import List, Optional + +from database.client import get_db_session +from database.db_models import PromptTemplate +from database.prompt_template_db import ( + list_prompt_templates, + create_prompt_template, + update_prompt_template, + delete_prompt_template, +) + +logger = logging.getLogger("prompt_template_service") + + +BUILTIN_TEMPLATES = [ + { + "name": "通用结构", + "description": "适用于多种场景的提示词结构,可以根据具体需求增删对应模块", + "prompt_text": """# 角色:{#InputSlot placeholder="角色名称" mode="input"#}{#/InputSlot#} +{#InputSlot placeholder="角色概述和主要职责的一句话描述" mode="input"#}{#/InputSlot#} + +## 目标: +{#InputSlot placeholder="角色的工作目标,如果有多目标可以分点列出,但建议更聚焦1-2个目标" mode="input"#}{#/InputSlot#} + +## 技能: +1. {#InputSlot placeholder="为了实现目标,角色需要具备的技能1" mode="input"#}{#/InputSlot#} +2. {#InputSlot placeholder="为了实现目标,角色需要具备的技能2" mode="input"#}{#/InputSlot#} +3. {#InputSlot placeholder="为了实现目标,角色需要具备的技能3" mode="input"#}{#/InputSlot#} + +## 工作流: +1. {#InputSlot placeholder="描述角色工作流程的第一步" mode="input"#}{#/InputSlot#} +2. {#InputSlot placeholder="描述角色工作流程的第二步" mode="input"#}{#/InputSlot#} +3. {#InputSlot placeholder="描述角色工作流程的第三步" mode="input"#}{#/InputSlot#} + +## 输出格式: +{#InputSlot placeholder="如果对角色的输出格式有特定要求,可以在这里强调并举例说明想要的输出格式" mode="input"#}{#/InputSlot#} + +## 限制: +- {#InputSlot placeholder="描述角色在互动过程中需要遵循的限制条件1" mode="input"#}{#/InputSlot#} +- {#InputSlot placeholder="描述角色在互动过程中需要遵循的限制条件2" mode="input"#}{#/InputSlot#} +- {#InputSlot placeholder="描述角色在互动过程中需要遵循的限制条件3" mode="input"#}{#/InputSlot#}""", + }, +] + + +def ensure_builtin_templates(tenant_id: str, user_id: str) -> None: + with get_db_session() as session: + for template in BUILTIN_TEMPLATES: + existing = session.query(PromptTemplate).filter( + PromptTemplate.tenant_id == tenant_id, + PromptTemplate.name == template["name"], + PromptTemplate.is_builtin == True + ).first() + if existing: + existing.description = template["description"] + existing.prompt_text = template["prompt_text"] + if existing.delete_flag == "Y": + existing.delete_flag = "N" + existing.updated_by = user_id + continue + + new_template = PromptTemplate( + name=template["name"], + description=template["description"], + prompt_text=template["prompt_text"], + is_builtin=True, + tenant_id=tenant_id, + created_by=user_id, + updated_by=user_id, + delete_flag="N", + ) + session.add(new_template) + + +def list_templates(tenant_id: str, user_id: str, keyword: Optional[str] = None) -> List[dict]: + ensure_builtin_templates(tenant_id, user_id) + return list_prompt_templates(tenant_id, keyword) + + +def create_template(tenant_id: str, user_id: str, payload: dict) -> dict: + return create_prompt_template(payload, tenant_id, user_id) + + +def update_template(tenant_id: str, user_id: str, template_id: int, payload: dict) -> dict: + return update_prompt_template(template_id, payload, tenant_id, user_id) + + +def delete_template(tenant_id: str, user_id: str, template_id: int) -> None: + delete_prompt_template(template_id, tenant_id, user_id) diff --git a/docker/.env.beta b/docker/.env.beta index 2ce33754e..be5b84929 100644 --- a/docker/.env.beta +++ b/docker/.env.beta @@ -1,5 +1,5 @@ -NEXENT_IMAGE=nexent/nexent:beta -NEXENT_WEB_IMAGE=nexent/nexent-web:beta +NEXENT_IMAGE=nexent/nexent:local +NEXENT_WEB_IMAGE=nexent/nexent-web:local NEXENT_DATA_PROCESS_IMAGE=nexent/nexent-data-process:beta ELASTICSEARCH_IMAGE=docker.elastic.co/elasticsearch/elasticsearch:8.17.4 diff --git a/docker/.env.mainland b/docker/.env.mainland index fd628ba46..471a79699 100644 --- a/docker/.env.mainland +++ b/docker/.env.mainland @@ -1,6 +1,6 @@ -NEXENT_IMAGE=ccr.ccs.tencentyun.com/nexent-hub/nexent:${APP_VERSION} -NEXENT_WEB_IMAGE=ccr.ccs.tencentyun.com/nexent-hub/nexent-web:${APP_VERSION} -NEXENT_DATA_PROCESS_IMAGE=ccr.ccs.tencentyun.com/nexent-hub/nexent-data-process:${APP_VERSION} +NEXENT_IMAGE=nexent/nexent:local +NEXENT_WEB_IMAGE=nexent/nexent-web:local +NEXENT_DATA_PROCESS_IMAGE=nexent/nexent-data-process:local NEXENT_MCP_DOCKER_IMAGE=ccr.ccs.tencentyun.com/nexent-hub/nexent-mcp:${APP_VERSION} ELASTICSEARCH_IMAGE=elastic.m.daocloud.io/elasticsearch/elasticsearch:8.17.4 diff --git a/docker/sql/v1.7.10_0206_add_prompt_template_t.sql b/docker/sql/v1.7.10_0206_add_prompt_template_t.sql new file mode 100644 index 000000000..c452bcb43 --- /dev/null +++ b/docker/sql/v1.7.10_0206_add_prompt_template_t.sql @@ -0,0 +1,16 @@ +-- Create prompt template table +CREATE SEQUENCE IF NOT EXISTS nexent.prompt_template_t_template_id_seq; + +CREATE TABLE IF NOT EXISTS nexent.prompt_template_t ( + template_id INTEGER PRIMARY KEY DEFAULT nextval('nexent.prompt_template_t_template_id_seq'), + name VARCHAR(200), + description VARCHAR(2000), + prompt_text TEXT, + is_builtin BOOLEAN DEFAULT FALSE, + tenant_id VARCHAR(100), + create_time TIMESTAMP DEFAULT now(), + update_time TIMESTAMP DEFAULT now(), + created_by VARCHAR(100), + updated_by VARCHAR(100), + delete_flag VARCHAR(1) DEFAULT 'N' +); diff --git a/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx b/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx index 3b118f5b7..e281c17d0 100644 --- a/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx +++ b/frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx @@ -26,6 +26,9 @@ import { GENERATE_PROMPT_STREAM_TYPES, } from "@/const/agentConfig"; import { generatePromptStream } from "@/services/promptService"; +import { listPromptTemplates } from "@/services/promptTemplateService"; +import type { PromptTemplate } from "@/types/promptTemplate"; +import { useAuth } from "@/hooks/useAuth"; import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider"; import { useDeployment } from "@/components/providers/deploymentProvider"; import { useModelList } from "@/hooks/model/useModelList"; @@ -107,6 +110,9 @@ export default function AgentGenerateDetail({ // Modal states const [expandModalOpen, setExpandModalOpen] = useState(false); const [expandModalType, setExpandModalType] = useState<'duty' | 'constraint' | 'few-shots' | null>(null); + const [promptTemplates, setPromptTemplates] = useState([]); + const [templateLoading, setTemplateLoading] = useState(false); + const [selectedTemplateId, setSelectedTemplateId] = useState(null); // Only show "no edit permission" tooltip when the panel is active and agent is read-only. // Note: when no agent is selected, AgentInfoComp shows an overlay and we should not show @@ -135,6 +141,41 @@ export default function AgentGenerateDetail({ }; + // Ensure tenant config is loaded for default model selection + useEffect(() => { + const loadConfigIfNeeded = async () => { + try { + // Check if config is already loaded + const configStore = ConfigStore.getInstance(); + const modelConfig = configStore.getModelConfig(); + + // If no LLM model is configured, try to load config from backend + if (!modelConfig.llm?.modelName && !modelConfig.llm?.displayName) { + await configService.loadConfigToFrontend(); + } + } catch (error) { + log.warn("Failed to load tenant config:", error); + } + }; + + loadConfigIfNeeded(); + }, []); + + useEffect(() => { + const loadTemplates = async () => { + setTemplateLoading(true); + try { + const res = await listPromptTemplates(); + setPromptTemplates(res?.data || []); + } catch (error) { + message.error(t("promptTemplate.message.loadError")); + } finally { + setTemplateLoading(false); + } + }; + loadTemplates(); + }, [message, t]); + const stylesObject: TabsProps["styles"] = { root: {}, header: {}, @@ -417,6 +458,42 @@ export default function AgentGenerateDetail({ } }; + const handleApplyTemplate = () => { + if (!selectedTemplateId) { + message.warning(t("promptTemplate.selectPlaceholder")); + return; + } + const selected = promptTemplates.find( + (template) => template.template_id === selectedTemplateId + ); + if (!selected) return; + + const targetTab = + activeTab === "duty" || activeTab === "constraint" || activeTab === "few-shots" + ? activeTab + : "duty"; + + switch (targetTab) { + case "duty": + form.setFieldsValue({ dutyPrompt: selected.prompt_text }); + onUpdateProfile({ duty_prompt: selected.prompt_text }); + break; + case "constraint": + form.setFieldsValue({ constraintPrompt: selected.prompt_text }); + onUpdateProfile({ constraint_prompt: selected.prompt_text }); + break; + case "few-shots": + form.setFieldsValue({ fewShotsPrompt: selected.prompt_text }); + onUpdateProfile({ few_shots_prompt: selected.prompt_text }); + break; + default: + break; + } + message.success(t("promptTemplate.message.applySuccess")); + }; + + // Custom validator for agent name uniqueness + const validateAgentNameUnique = async (_: any, value: string) => { // Generic validator for agent field uniqueness - use local agent list instead of API call const validateAgentFieldUnique = async ( _: any, @@ -964,6 +1041,35 @@ export default function AgentGenerateDetail({ }} /> +
+ } + placeholder={t("promptTemplate.searchPlaceholder")} + value={keyword} + onChange={(e) => setKeyword(e.target.value)} + /> +
+ + + + setModalOpen(false)} + onOk={handleSubmit} + okText={t("common.save", "Save")} + destroyOnClose + > +
+ + + + + + + +