From 54329aa5168b2a1c7cc34943e2bdbc154a0d3629 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Thu, 9 Jul 2026 15:29:25 +0800 Subject: [PATCH] Add Agent capabilities and configurations across the application - Extend the AssistantConfig model to include agent_interface_type, agent_values, and agent_secrets for enhanced agent management. - Update ModelType to include "Agent" for broader capability support. - Seed new agent models and configurations in the database for Dify, FastGPT, and OpenCode applications. - Modify the Assistant routes to validate and handle agent capabilities. - Implement agent resource resolution in the config resolver for runtime configuration. - Enhance the interface catalog with agent-specific fields and definitions. - Update frontend components to manage agent configurations, including form handling and state management for agent-related inputs. --- backend/db/seed_assistants.sql | 5 +- backend/db/seed_interface_definitions.sql | 16 ++ backend/db/seed_model_resources.sql | 11 +- backend/models.py | 3 + backend/routes/assistants.py | 2 +- backend/schemas.py | 2 +- backend/services/config_resolver.py | 40 ++++- backend/services/interface_catalog.py | 22 +++ .../src/components/pages/AssistantPage.tsx | 150 +++++++----------- .../components/pages/ComponentsModelsPage.tsx | 99 ++++++++++-- frontend/src/lib/api.ts | 2 +- 11 files changed, 238 insertions(+), 114 deletions(-) diff --git a/backend/db/seed_assistants.sql b/backend/db/seed_assistants.sql index 14bac38..8ae9c5b 100644 --- a/backend/db/seed_assistants.sql +++ b/backend/db/seed_assistants.sql @@ -39,10 +39,13 @@ VALUES ('asst_002', 'TTS', 'model_004', '{}'), ('asst_003', 'ASR', 'model_002', '{}'), ('asst_003', 'TTS', 'model_004', '{}'), + ('asst_003', 'Agent', 'model_014', '{}'), ('asst_004', 'ASR', 'model_002', '{}'), ('asst_004', 'TTS', 'model_004', '{}'), + ('asst_004', 'Agent', 'model_015', '{}'), ('asst_005', 'ASR', 'model_002', '{}'), - ('asst_005', 'TTS', 'model_004', '{}') + ('asst_005', 'TTS', 'model_004', '{}'), + ('asst_005', 'Agent', 'model_016', '{}') ON CONFLICT (assistant_id, capability) DO UPDATE SET model_resource_id = EXCLUDED.model_resource_id, updated_at = now(); diff --git a/backend/db/seed_interface_definitions.sql b/backend/db/seed_interface_definitions.sql index edd0af3..bfc64f5 100644 --- a/backend/db/seed_interface_definitions.sql +++ b/backend/db/seed_interface_definitions.sql @@ -40,6 +40,22 @@ VALUES {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, {"key":"voice","label":"Voice","group":"values","type":"text","required":false} ]} $$::jsonb, TRUE, 1), + ('dify', 'Dify Agent', 'Agent', + $$ {"fields":[ + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true} + ]} $$::jsonb, TRUE, 1), + ('fastgpt', 'FastGPT Agent', 'Agent', + $$ {"fields":[ + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"appId","label":"App ID","group":"values","type":"text","required":true} + ]} $$::jsonb, TRUE, 1), + ('opencode', 'OpenCode Agent', 'Agent', + $$ {"fields":[ + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true} + ]} $$::jsonb, TRUE, 1), ('stepfun-realtime', 'StepFun StepAudio Realtime', 'Realtime', $$ {"fields":[ {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, diff --git a/backend/db/seed_model_resources.sql b/backend/db/seed_model_resources.sql index 09dae28..1e44289 100644 --- a/backend/db/seed_model_resources.sql +++ b/backend/db/seed_model_resources.sql @@ -41,6 +41,15 @@ VALUES '{"apiKey":"replace-me"}', FALSE, TRUE, FALSE), ('model_013', 'doubao-seed-2-0-mini-260428', 'LLM', 'openai-llm', '{"modelId":"doubao-seed-2-0-mini-260428","apiUrl":"https://ark.cn-beijing.volces.com/api/v3","temperature":0.7,"extraBody":{"thinking":{"type":"disabled"}}}', - '{"apiKey":"replace-me"}', TRUE, TRUE, FALSE) + '{"apiKey":"replace-me"}', TRUE, TRUE, FALSE), + ('model_014', 'Dify 客服应用', 'Agent', 'dify', + '{"apiUrl":"https://api.dify.ai/v1/chat-messages"}', + '{"apiKey":"replace-me"}', FALSE, TRUE, FALSE), + ('model_015', 'FastGPT 售后应用', 'Agent', 'fastgpt', + '{"apiUrl":"https://api.fastgpt.in/api/v1/chat/completions","appId":"app-fastgpt-001"}', + '{"apiKey":"replace-me"}', FALSE, TRUE, FALSE), + ('model_016', 'OpenCode 服务', 'Agent', 'opencode', + '{"apiUrl":"http://localhost:4096"}', + '{"apiKey":"replace-me"}', FALSE, TRUE, FALSE) -- Seed defaults must never overwrite resources configured through the UI. ON CONFLICT (id) DO NOTHING; diff --git a/backend/models.py b/backend/models.py index af80213..d393f4d 100644 --- a/backend/models.py +++ b/backend/models.py @@ -36,6 +36,9 @@ class AssistantConfig(BaseModel): realtime_interface_type: str = "" realtime_values: dict = {} realtime_secrets: dict = {} + agent_interface_type: str = "" + agent_values: dict = {} + agent_secrets: dict = {} llm_interface_type: str = "openai-llm" stt_interface_type: str = "openai-asr" tts_interface_type: str = "openai-tts" diff --git a/backend/routes/assistants.py b/backend/routes/assistants.py index 0ac23d8..250d2de 100644 --- a/backend/routes/assistants.py +++ b/backend/routes/assistants.py @@ -12,7 +12,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter(prefix="/api/assistants", tags=["assistants"]) -CAPABILITIES = ("LLM", "ASR", "TTS", "Realtime", "Embedding") +CAPABILITIES = ("LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent") def _validate_workflow(body: AssistantUpsert) -> None: diff --git a/backend/schemas.py b/backend/schemas.py index 2a01634..a1d55c0 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -13,7 +13,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator from pydantic.alias_generators import to_camel RuntimeMode = Literal["pipeline", "realtime"] -ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding"] +ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent"] AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"] # 外部应用类型:其 config.apiKey 是该助手私有密钥,读时打码 / 写时哨兵 diff --git a/backend/services/config_resolver.py b/backend/services/config_resolver.py index f115e00..95265bd 100644 --- a/backend/services/config_resolver.py +++ b/backend/services/config_resolver.py @@ -33,6 +33,36 @@ async def _resource_for( return resource +async def _agent_resource_for( + session: AsyncSession, + assistant_id: str, + interface_type: str, +) -> ModelResource | None: + if interface_type not in {"dify", "fastgpt", "opencode"}: + return None + binding = await session.get(AssistantModelBinding, (assistant_id, "Agent")) + resource_id = binding.model_resource_id if binding else None + resource = await session.get(ModelResource, resource_id) if resource_id else None + if ( + resource + and resource.capability == "Agent" + and resource.interface_type == interface_type + ): + return resource + return ( + await session.execute( + select(ModelResource) + .where( + ModelResource.capability == "Agent", + ModelResource.interface_type == interface_type, + ModelResource.enabled.is_(True), + ) + .order_by(ModelResource.is_default.desc(), ModelResource.id.asc()) + .limit(1) + ) + ).scalar_one_or_none() + + def _value(resource: ModelResource | None, key: str, default): if not resource: return default @@ -58,6 +88,7 @@ async def resolve_runtime_config( stt_resource = await _resource_for(session, assistant.id, "ASR") tts_resource = await _resource_for(session, assistant.id, "TTS") realtime_resource = await _resource_for(session, assistant.id, "Realtime") + agent_resource = await _agent_resource_for(session, assistant.id, assistant.type) vision_resource = ( await session.get(ModelResource, assistant.vision_model_resource_id) if assistant.vision_model_resource_id @@ -75,9 +106,9 @@ async def resolve_runtime_config( # workflow 图:仅 workflow 类型非空,引擎据此启用图驱动对话 graph=(assistant.graph or {}) if assistant.type == "workflow" else {}, # 外部托管类型连接信息(DB 存真 key,直接注入) - fastgpt_api_url=assistant.api_url, - fastgpt_api_key=assistant.api_key, - fastgpt_app_id=assistant.app_id, + fastgpt_api_url=str(_value(agent_resource, "apiUrl", assistant.api_url)), + fastgpt_api_key=_secret(agent_resource, "apiKey", assistant.api_key), + fastgpt_app_id=str(_value(agent_resource, "appId", assistant.app_id)), # 模型/音色:模型资源中的配置优先 model=str(_value(llm_resource, "modelId", "")), asr=str(_value(stt_resource, "modelId", "")), @@ -114,6 +145,9 @@ async def resolve_runtime_config( ), realtime_values=(realtime_resource.values or {}) if realtime_resource else {}, realtime_secrets=(realtime_resource.secrets or {}) if realtime_resource else {}, + agent_interface_type=(agent_resource.interface_type if agent_resource else ""), + agent_values=(agent_resource.values or {}) if agent_resource else {}, + agent_secrets=(agent_resource.secrets or {}) if agent_resource else {}, # 运行时连接信息(真 key + url):模型资源优先,否则 .env 兜底 llm_api_key=_secret(llm_resource, "apiKey", config.LLM_API_KEY), llm_base_url=str(_value(llm_resource, "apiUrl", config.LLM_BASE_URL)), diff --git a/backend/services/interface_catalog.py b/backend/services/interface_catalog.py index a97362b..2ac627d 100644 --- a/backend/services/interface_catalog.py +++ b/backend/services/interface_catalog.py @@ -34,6 +34,10 @@ OPENAI_COMMON = [ field("apiUrl", "API URL", type_="url", required=True), field("apiKey", "API Key", group="secrets", type_="password", required=True), ] +AGENT_COMMON = [ + field("apiUrl", "API URL", type_="url", required=True), + field("apiKey", "API Key", group="secrets", type_="password", required=True), +] XFYUN_AUTH = [ field("apiUrl", "WebSocket URL", type_="url", required=True), field("appId", "App ID", group="secrets", type_="password", required=True), @@ -78,6 +82,24 @@ INTERFACE_DEFINITIONS: list[dict] = [ "capability": "Realtime", "fields": OPENAI_COMMON + [field("voice", "Voice")], }, + { + "interface_type": "dify", + "name": "Dify Agent", + "capability": "Agent", + "fields": AGENT_COMMON, + }, + { + "interface_type": "fastgpt", + "name": "FastGPT Agent", + "capability": "Agent", + "fields": AGENT_COMMON + [field("appId", "App ID", required=True)], + }, + { + "interface_type": "opencode", + "name": "OpenCode Agent", + "capability": "Agent", + "fields": AGENT_COMMON, + }, { "interface_type": "stepfun-realtime", "name": "StepFun StepAudio Realtime", diff --git a/frontend/src/components/pages/AssistantPage.tsx b/frontend/src/components/pages/AssistantPage.tsx index decc93d..e37db04 100644 --- a/frontend/src/components/pages/AssistantPage.tsx +++ b/frontend/src/components/pages/AssistantPage.tsx @@ -131,9 +131,7 @@ type AssistantForm = { type FastGptForm = { name: string; - appId: string; - apiUrl: string; - apiKey: string; + agent: string; asr: string; voice: string; enableInterrupt: boolean; @@ -141,8 +139,7 @@ type FastGptForm = { type DifyForm = { name: string; - apiUrl: string; - apiKey: string; + agent: string; asr: string; voice: string; enableInterrupt: boolean; @@ -151,8 +148,7 @@ type DifyForm = { type OpenCodeForm = { name: string; prompt: string; - apiUrl: string; - apiKey: string; + agent: string; model: string; asr: string; voice: string; @@ -226,9 +222,7 @@ function blankPromptForm(name: string): AssistantForm { function blankFastGptForm(name: string): FastGptForm { return { name, - appId: "", - apiUrl: "", - apiKey: "", + agent: "", asr: "", voice: "", enableInterrupt: true, @@ -238,8 +232,7 @@ function blankFastGptForm(name: string): FastGptForm { function blankDifyForm(name: string): DifyForm { return { name, - apiUrl: "", - apiKey: "", + agent: "", asr: "", voice: "", enableInterrupt: true, @@ -250,8 +243,7 @@ function blankOpenCodeForm(name: string): OpenCodeForm { return { name, prompt: "", - apiUrl: "", - apiKey: "", + agent: "", model: "", asr: "", voice: "", @@ -434,6 +426,14 @@ export function AssistantPage(props: AssistantPageProps) { modelResources .filter((c) => c.capability === type) .map((c) => ({ value: c.id, label: c.name })); + const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") => + modelResources + .filter( + (resource) => + resource.capability === "Agent" && + resource.interfaceType === interfaceType, + ) + .map((resource) => ({ value: resource.id, label: resource.name })); const visionModelOptionsFor = (currentModelId: string) => modelResources .filter( (c) => @@ -573,15 +573,15 @@ export function AssistantPage(props: AssistantPageProps) { if (view === "create") { setSavedSnapshot(JSON.stringify(form)); } else if (view === "create-dify") { - const next = { ...difyForm, apiKey: "" }; + const next = { ...difyForm }; setDifyForm(next); setSavedSnapshot(JSON.stringify(next)); } else if (view === "create-fastgpt") { - const next = { ...fastGptForm, apiKey: "" }; + const next = { ...fastGptForm }; setFastGptForm(next); setSavedSnapshot(JSON.stringify(next)); } else if (view === "create-opencode") { - const next = { ...openCodeForm, apiKey: "" }; + const next = { ...openCodeForm }; setOpenCodeForm(next); setSavedSnapshot(JSON.stringify(next)); } else if (view === "workflow") { @@ -626,9 +626,7 @@ export function AssistantPage(props: AssistantPageProps) { function fillDifyForm(a: Assistant): DifyForm { const next: DifyForm = { name: a.name, - apiUrl: a.apiUrl, - // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key - apiKey: "", + agent: a.modelResourceIds.Agent ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", enableInterrupt: a.enableInterrupt, @@ -644,11 +642,10 @@ export function AssistantPage(props: AssistantPageProps) { type: "dify", enableInterrupt: difyForm.enableInterrupt, modelResourceIds: { + ...(difyForm.agent ? { Agent: difyForm.agent } : {}), ...(difyForm.asr ? { ASR: difyForm.asr } : {}), ...(difyForm.voice ? { TTS: difyForm.voice } : {}), }, - apiUrl: difyForm.apiUrl, - apiKey: difyForm.apiKey, }), ); } @@ -657,10 +654,7 @@ export function AssistantPage(props: AssistantPageProps) { function fillFastGptForm(a: Assistant): FastGptForm { const next: FastGptForm = { name: a.name, - appId: a.appId, - apiUrl: a.apiUrl, - // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key - apiKey: "", + agent: a.modelResourceIds.Agent ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", enableInterrupt: a.enableInterrupt, @@ -676,12 +670,10 @@ export function AssistantPage(props: AssistantPageProps) { type: "fastgpt", enableInterrupt: fastGptForm.enableInterrupt, modelResourceIds: { + ...(fastGptForm.agent ? { Agent: fastGptForm.agent } : {}), ...(fastGptForm.asr ? { ASR: fastGptForm.asr } : {}), ...(fastGptForm.voice ? { TTS: fastGptForm.voice } : {}), }, - appId: fastGptForm.appId, - apiUrl: fastGptForm.apiUrl, - apiKey: fastGptForm.apiKey, }), ); } @@ -691,9 +683,7 @@ export function AssistantPage(props: AssistantPageProps) { const next: OpenCodeForm = { name: a.name, prompt: a.prompt, - apiUrl: a.apiUrl, - // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key - apiKey: "", + agent: a.modelResourceIds.Agent ?? "", model: a.modelResourceIds.LLM ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", @@ -772,13 +762,12 @@ export function AssistantPage(props: AssistantPageProps) { visionEnabled: openCodeForm.visionEnabled, visionModelResourceId: openCodeForm.visionModelResourceId || null, modelResourceIds: { + ...(openCodeForm.agent ? { Agent: openCodeForm.agent } : {}), ...(openCodeForm.model ? { LLM: openCodeForm.model } : {}), ...(openCodeForm.asr ? { ASR: openCodeForm.asr } : {}), ...(openCodeForm.voice ? { TTS: openCodeForm.voice } : {}), }, prompt: openCodeForm.prompt, - apiUrl: openCodeForm.apiUrl, - apiKey: openCodeForm.apiKey, }), ); } @@ -1355,7 +1344,9 @@ export function AssistantPage(props: AssistantPageProps) { )} + + {hasStoredValue && ( +

+ 仅显示当前密钥首尾用于识别。留空可保持原密钥,输入新值将覆盖原密钥。 +

+ )} + + ); + } + return ( @@ -962,11 +1036,6 @@ function DynamicField({ : event.target.value, ) } - placeholder={ - field.type === "password" && String(value ?? "").includes("****") - ? "已配置,留空或保留掩码表示不修改" - : undefined - } /> ); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 58394bf..4650b0e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -8,7 +8,7 @@ export const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000"; -export type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding"; +export type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding" | "Agent"; async function request(path: string, init?: RequestInit): Promise { const res = await fetch(`${API_BASE}${path}`, { headers: { "Content-Type": "application/json" },