From e2be076c734f3a5b596c22be3028a0f4e91ad424 Mon Sep 17 00:00:00 2001 From: lin onetwo Date: Sat, 23 Aug 2025 00:46:12 +0800 Subject: [PATCH] Refactor plugin schema system for dynamic registration Introduces a dynamic plugin schema registry, allowing plugins to register their parameter schemas and metadata at runtime. Refactors prompt concat schema generation to use dynamically registered plugin schemas, removes static plugin schema definitions, and updates all plugin files to export their parameter schemas. Adds new modelContextProtocolPlugin and schemaRegistry modules, and updates plugin initialization to register schemas and metadata. This enables extensibility and type safety for plugin configuration and validation. --- src/services/agentInstance/index.ts | 4 +- .../__tests__/wikiSearchPlugin.test.ts | 4 +- .../__tests__/workspacesListPlugin.test.ts | 4 +- src/services/agentInstance/plugins/index.ts | 73 +++++- .../plugins/modelContextProtocolPlugin.ts | 54 ++++ .../agentInstance/plugins/promptPlugins.ts | 158 ++++++------ .../agentInstance/plugins/schemaRegistry.ts | 165 ++++++++++++ .../plugins/wikiOperationPlugin.ts | 45 +++- .../agentInstance/plugins/wikiSearchPlugin.ts | 59 +++++ .../plugins/workspacesListPlugin.ts | 38 ++- .../promptConcat/promptConcatSchema/index.ts | 89 ++++--- .../promptConcatSchema/jsonSchema.ts | 10 +- .../promptConcat/promptConcatSchema/plugin.ts | 244 ++---------------- .../promptConcatSchema/uiSchema.ts | 7 - 14 files changed, 600 insertions(+), 354 deletions(-) create mode 100644 src/services/agentInstance/plugins/modelContextProtocolPlugin.ts create mode 100644 src/services/agentInstance/plugins/schemaRegistry.ts diff --git a/src/services/agentInstance/index.ts b/src/services/agentInstance/index.ts index 14894452..0bf050e9 100644 --- a/src/services/agentInstance/index.ts +++ b/src/services/agentInstance/index.ts @@ -11,7 +11,7 @@ import { AgentHandler, AgentHandlerContext } from '@services/agentInstance/build import { createHandlerHooks, createHooksWithPlugins, initializePluginSystem } from '@services/agentInstance/plugins'; import { promptConcatStream, PromptConcatStreamState } from '@services/agentInstance/promptConcat/promptConcat'; import { AgentPromptDescription } from '@services/agentInstance/promptConcat/promptConcatSchema'; -import { promptConcatHandlerConfigJsonSchema } from '@services/agentInstance/promptConcat/promptConcatSchema/jsonSchema'; +import { getPromptConcatHandlerConfigJsonSchema } from '@services/agentInstance/promptConcat/promptConcatSchema/jsonSchema'; import { IDatabaseService } from '@services/database/interface'; import { AgentInstanceEntity, AgentInstanceMessageEntity } from '@services/database/schema/agent'; import { logger } from '@services/libs/log'; @@ -72,7 +72,7 @@ export class AgentInstanceService implements IAgentInstanceService { public registerBuiltinHandlers(): void { // Plugins are already registered in initialize(), so we only register handlers here // Register basic prompt concatenation handler with its schema - this.registerHandler('basicPromptConcatHandler', basicPromptConcatHandler, promptConcatHandlerConfigJsonSchema); + this.registerHandler('basicPromptConcatHandler', basicPromptConcatHandler, getPromptConcatHandlerConfigJsonSchema()); } /** diff --git a/src/services/agentInstance/plugins/__tests__/wikiSearchPlugin.test.ts b/src/services/agentInstance/plugins/__tests__/wikiSearchPlugin.test.ts index a28299ad..17135cba 100644 --- a/src/services/agentInstance/plugins/__tests__/wikiSearchPlugin.test.ts +++ b/src/services/agentInstance/plugins/__tests__/wikiSearchPlugin.test.ts @@ -49,7 +49,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => { it('should inject wiki tools into prompts when configured', async () => { // Find the wiki search plugin config, make sure our default config - const wikiPlugin = handlerConfig.plugins.find(p => p.pluginId === 'wikiSearch'); + const wikiPlugin = handlerConfig.plugins.find((p: unknown): p is IPromptConcatPlugin => (p as IPromptConcatPlugin).pluginId === 'wikiSearch'); expect(wikiPlugin).toBeDefined(); if (!wikiPlugin) { // throw error to keep ts believe the plugin exists @@ -194,7 +194,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => { it('should execute wiki search with correct duration=1 and trigger next round', async () => { // Find the real wikiSearch plugin config from defaultAgents.json - const wikiPlugin = handlerConfig.plugins.find(p => p.pluginId === 'wikiSearch'); + const wikiPlugin = handlerConfig.plugins.find((p: unknown): p is IPromptConcatPlugin => (p as IPromptConcatPlugin).pluginId === 'wikiSearch'); expect(wikiPlugin).toBeDefined(); expect(wikiPlugin!.wikiSearchParam).toBeDefined(); diff --git a/src/services/agentInstance/plugins/__tests__/workspacesListPlugin.test.ts b/src/services/agentInstance/plugins/__tests__/workspacesListPlugin.test.ts index 9d03caff..42d421dd 100644 --- a/src/services/agentInstance/plugins/__tests__/workspacesListPlugin.test.ts +++ b/src/services/agentInstance/plugins/__tests__/workspacesListPlugin.test.ts @@ -152,8 +152,8 @@ describe('workspacesListPlugin', () => { it('should handle empty workspaces list', async () => { // Override the workspace service implementation returned by the global container mock - const workspaceService = container.get>(serviceIdentifier.Workspace); - workspaceService.getWorkspacesAsList = vi.fn().mockResolvedValue([]) as unknown as IWorkspaceService['getWorkspacesAsList']; + const workspaceService = container.get>(serviceIdentifier.Workspace); + workspaceService.getWorkspacesAsList = vi.fn().mockResolvedValue([]) as unknown as IWorkspaceService['getWorkspacesAsList']; const hooks = createHandlerHooks(); workspacesListPlugin(hooks); diff --git a/src/services/agentInstance/plugins/index.ts b/src/services/agentInstance/plugins/index.ts index 990b886b..1b2d4712 100644 --- a/src/services/agentInstance/plugins/index.ts +++ b/src/services/agentInstance/plugins/index.ts @@ -1,5 +1,6 @@ import { logger } from '@services/libs/log'; import { AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable'; +import { registerPluginParameterSchema } from './schemaRegistry'; import { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatPlugin, ResponseHookContext } from './types'; // Re-export types for convenience @@ -85,6 +86,76 @@ export async function registerPluginsToHooksFromConfig( * This should be called once during service initialization */ export async function initializePluginSystem(): Promise { + // Import plugin schemas and register them + const [ + promptPluginsModule, + wikiSearchModule, + wikiOperationModule, + workspacesListModule, + modelContextProtocolModule, + ] = await Promise.all([ + import('./promptPlugins'), + import('./wikiSearchPlugin'), + import('./wikiOperationPlugin'), + import('./workspacesListPlugin'), + import('./modelContextProtocolPlugin'), + ]); + + // Register plugin parameter schemas + registerPluginParameterSchema( + 'fullReplacement', + promptPluginsModule.getFullReplacementParameterSchema(), + { + displayName: 'Full Replacement', + description: 'Replace target content with content from specified source', + }, + ); + + registerPluginParameterSchema( + 'dynamicPosition', + promptPluginsModule.getDynamicPositionParameterSchema(), + { + displayName: 'Dynamic Position', + description: 'Insert content at a specific position relative to a target element', + }, + ); + + registerPluginParameterSchema( + 'wikiSearch', + wikiSearchModule.getWikiSearchParameterSchema(), + { + displayName: 'Wiki Search', + description: 'Search content in wiki workspaces', + }, + ); + + registerPluginParameterSchema( + 'wikiOperation', + wikiOperationModule.getWikiOperationParameterSchema(), + { + displayName: 'Wiki Operation', + description: 'Perform operations on wiki workspaces (create, update, delete tiddlers)', + }, + ); + + registerPluginParameterSchema( + 'workspacesList', + workspacesListModule.getWorkspacesListParameterSchema(), + { + displayName: 'Workspaces List', + description: 'Inject available wiki workspaces list into prompts', + }, + ); + + registerPluginParameterSchema( + 'modelContextProtocol', + modelContextProtocolModule.getModelContextProtocolParameterSchema(), + { + displayName: 'Model Context Protocol', + description: 'MCP (Model Context Protocol) integration', + }, + ); + const plugins = await getAllPlugins(); // Register all built-in plugins to global registry for discovery builtInPlugins.set('messageManagement', plugins.messageManagementPlugin); @@ -92,7 +163,7 @@ export async function initializePluginSystem(): Promise { builtInPlugins.set('wikiSearch', plugins.wikiSearchPlugin); builtInPlugins.set('wikiOperation', plugins.wikiOperationPlugin); builtInPlugins.set('workspacesList', plugins.workspacesListPlugin); - logger.debug('All built-in plugins registered to global registry successfully'); + logger.debug('All built-in plugins and schemas registered successfully'); } /** diff --git a/src/services/agentInstance/plugins/modelContextProtocolPlugin.ts b/src/services/agentInstance/plugins/modelContextProtocolPlugin.ts new file mode 100644 index 00000000..9efe27a8 --- /dev/null +++ b/src/services/agentInstance/plugins/modelContextProtocolPlugin.ts @@ -0,0 +1,54 @@ +/** + * Model Context Protocol Plugin + * Handles MCP (Model Context Protocol) integration + */ +import { identity } from 'lodash'; +import { z } from 'zod/v4'; + +const t = identity; + +/** + * Model Context Protocol Parameter Schema + * Configuration parameters for the MCP plugin + */ +export const ModelContextProtocolParameterSchema = z.object({ + id: z.string().meta({ + title: t('Schema.MCP.IdTitle'), + description: t('Schema.MCP.Id'), + }), + timeoutSecond: z.number().optional().meta({ + title: t('Schema.MCP.TimeoutSecondTitle'), + description: t('Schema.MCP.TimeoutSecond'), + }), + timeoutMessage: z.string().optional().meta({ + title: t('Schema.MCP.TimeoutMessageTitle'), + description: t('Schema.MCP.TimeoutMessage'), + }), + position: z.enum(['before', 'after']).meta({ + title: t('Schema.Position.TypeTitle'), + description: t('Schema.Position.Type'), + }), + targetId: z.string().meta({ + title: t('Schema.Position.TargetIdTitle'), + description: t('Schema.Position.TargetId'), + }), +}).meta({ + title: t('Schema.MCP.Title'), + description: t('Schema.MCP.Description'), +}); + +/** + * Type definition for MCP parameters + */ +export type ModelContextProtocolParameter = z.infer; + +/** + * Get the model context protocol parameter schema + * @returns The schema for MCP parameters + */ +export function getModelContextProtocolParameterSchema() { + return ModelContextProtocolParameterSchema; +} + +// TODO: Implement the actual MCP plugin functionality +// This is a placeholder for future MCP integration diff --git a/src/services/agentInstance/plugins/promptPlugins.ts b/src/services/agentInstance/plugins/promptPlugins.ts index 035dfafe..0873c88d 100644 --- a/src/services/agentInstance/plugins/promptPlugins.ts +++ b/src/services/agentInstance/plugins/promptPlugins.ts @@ -1,6 +1,9 @@ /** * Built-in plugins for prompt concatenation */ +import { identity } from 'lodash'; +import { z } from 'zod/v4'; + import { logger } from '@services/libs/log'; import { cloneDeep } from 'lodash'; import { findPromptById } from '../promptConcat/promptConcat'; @@ -8,6 +11,66 @@ import { IPrompt } from '../promptConcat/promptConcatSchema'; import { filterMessagesByDuration } from '../utilities/messageDurationFilter'; import { AgentResponse, PromptConcatPlugin, ResponseHookContext } from './types'; +const t = identity; + +/** + * Full Replacement Parameter Schema + * Configuration parameters for the full replacement plugin + */ +export const FullReplacementParameterSchema = z.object({ + targetId: z.string().meta({ + title: t('Schema.FullReplacement.TargetIdTitle'), + description: t('Schema.FullReplacement.TargetId'), + }), + sourceType: z.enum(['historyOfSession', 'llmResponse']).meta({ + title: t('Schema.FullReplacement.SourceTypeTitle'), + description: t('Schema.FullReplacement.SourceType'), + }), +}).meta({ + title: t('Schema.FullReplacement.Title'), + description: t('Schema.FullReplacement.Description'), +}); + +/** + * Dynamic Position Parameter Schema + * Configuration parameters for the dynamic position plugin + */ +export const DynamicPositionParameterSchema = z.object({ + targetId: z.string().meta({ + title: t('Schema.Position.TargetIdTitle'), + description: t('Schema.Position.TargetId'), + }), + position: z.enum(['before', 'after', 'relative']).meta({ + title: t('Schema.Position.TypeTitle'), + description: t('Schema.Position.Type'), + }), +}).meta({ + title: t('Schema.Position.Title'), + description: t('Schema.Position.Description'), +}); + +/** + * Type definitions + */ +export type FullReplacementParameter = z.infer; +export type DynamicPositionParameter = z.infer; + +/** + * Get the full replacement parameter schema + * @returns The schema for full replacement parameters + */ +export function getFullReplacementParameterSchema() { + return FullReplacementParameterSchema; +} + +/** + * Get the dynamic position parameter schema + * @returns The schema for dynamic position parameters + */ +export function getDynamicPositionParameterSchema() { + return DynamicPositionParameterSchema; +} + /** * Full replacement plugin * Replaces target content with content from specified source @@ -22,6 +85,11 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => { } const fullReplacementConfig = pluginConfig.fullReplacementParam; + if (!fullReplacementConfig) { + callback(); + return; + } + const { targetId, sourceType } = fullReplacementConfig; const found = findPromptById(prompts, targetId); @@ -116,7 +184,13 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => { return; } - const { targetId, sourceType } = pluginConfig.fullReplacementParam; + const fullReplacementParameter = pluginConfig.fullReplacementParam; + if (!fullReplacementParameter) { + callback(); + return; + } + + const { targetId, sourceType } = fullReplacementParameter; // Only handle llmResponse in response phase if (sourceType !== 'llmResponse') { @@ -168,6 +242,11 @@ export const dynamicPositionPlugin: PromptConcatPlugin = (hooks) => { } const dynamicPositionConfig = pluginConfig.dynamicPositionParam; + if (!dynamicPositionConfig) { + callback(); + return; + } + const { targetId, position } = dynamicPositionConfig; const found = findPromptById(prompts, targetId); @@ -217,80 +296,3 @@ export const dynamicPositionPlugin: PromptConcatPlugin = (hooks) => { callback(); }); }; - -/** - * Model Context Protocol plugin - * Integrates with external MCP servers - */ -export const modelContextProtocolPlugin: PromptConcatPlugin = (hooks) => { - hooks.processPrompts.tapAsync('modelContextProtocolPlugin', async (context, callback) => { - const { pluginConfig, prompts } = context; - - if (pluginConfig.pluginId !== 'modelContextProtocol' || !pluginConfig.modelContextProtocolParam) { - callback(); - return; - } - - const parameter = pluginConfig.modelContextProtocolParam; - const { targetId, position, id, timeoutSecond = 10, timeoutMessage = 'MCP server call timed out' } = parameter; - const found = findPromptById(prompts, targetId); - - if (!found) { - logger.warn('Target prompt not found for modelContextProtocol', { - targetId, - pluginId: pluginConfig.id, - }); - callback(); - return; - } - - try { - // TODO: Implement actual MCP server call with timeout - // For now, create a placeholder that indicates MCP integration - const content = `MCP Server Call: ${id} -Timeout: ${timeoutSecond} seconds -Timeout Message: ${timeoutMessage} - -This is where the actual Model Context Protocol server would be called. -The MCP server would provide additional context or capabilities to the AI model.`; - - // Simulate timeout handling in future implementation - logger.debug('MCP plugin - simulating server call', { - mcpId: id, - timeout: timeoutSecond, - pluginId: pluginConfig.id, - }); - - const newPart: IPrompt = { - id: `mcp-${pluginConfig.id}-${Date.now()}`, - caption: pluginConfig.caption || 'MCP Content', - text: content, - }; - - // Insert based on position - switch (position) { - case 'before': - found.parent.splice(found.index, 0, newPart); - break; - case 'after': - found.parent.splice(found.index + 1, 0, newPart); - break; - default: - logger.warn(`Unknown position: ${position as string}`); - callback(); - return; - } - - logger.debug('MCP plugin completed', { - targetId, - position, - mcpId: id, - }); - - callback(); - } catch (error) { - logger.error('MCP plugin error', error); - callback(); - } - }); -}; diff --git a/src/services/agentInstance/plugins/schemaRegistry.ts b/src/services/agentInstance/plugins/schemaRegistry.ts new file mode 100644 index 00000000..892b26ee --- /dev/null +++ b/src/services/agentInstance/plugins/schemaRegistry.ts @@ -0,0 +1,165 @@ +/** + * Plugin Schema Registry + * + * This system allows plugins to register their parameter schemas dynamically, + * enabling dynamic plugin loading while maintaining type safety and validation. + */ +import { identity } from 'lodash'; +import { z } from 'zod/v4'; + +const t = identity; + +/** + * Registry for plugin parameter schemas + */ +const pluginSchemas = new Map(); + +/** + * Registry for plugin metadata + */ +const pluginMetadata = new Map(); + +/** + * Register a plugin parameter schema + * @param pluginId The plugin ID (should match pluginId enum values) + * @param schema The Zod schema for this plugin's parameters + * @param metadata Optional metadata for display purposes + */ +export function registerPluginParameterSchema( + pluginId: string, + schema: z.ZodType, + metadata?: { + displayName: string; + description: string; + }, +): void { + pluginSchemas.set(pluginId, schema); + if (metadata) { + pluginMetadata.set(pluginId, metadata); + } +} + +/** + * Get a plugin parameter schema by ID + * @param pluginId The plugin ID + * @returns The schema or undefined if not found + */ +export function getPluginParameterSchema(pluginId: string): z.ZodType | undefined { + return pluginSchemas.get(pluginId); +} + +/** + * Get all registered plugin IDs + * @returns Array of all registered plugin IDs + */ +export function getAllRegisteredPluginIds(): string[] { + return Array.from(pluginSchemas.keys()); +} + +/** + * Get plugin metadata + * @param pluginId The plugin ID + * @returns Plugin metadata or undefined if not found + */ +export function getPluginMetadata(pluginId: string): { displayName: string; description: string } | undefined { + return pluginMetadata.get(pluginId); +} + +/** + * Dynamically create the PromptConcatPluginSchema based on registered plugins + * This is called whenever the schema is needed, ensuring it includes all registered plugins + */ +export function createDynamicPromptConcatPluginSchema(): z.ZodType { + // Base plugin configuration without parameter-specific fields + const basePluginSchema = z.object({ + id: z.string().meta({ + title: t('Schema.Plugin.IdTitle'), + description: t('Schema.Plugin.Id'), + }), + caption: z.string().optional().meta({ + title: t('Schema.Plugin.CaptionTitle'), + description: t('Schema.Plugin.Caption'), + }), + content: z.string().optional().meta({ + title: t('Schema.Plugin.ContentTitle'), + description: t('Schema.Plugin.Content'), + }), + forbidOverrides: z.boolean().optional().default(false).meta({ + title: t('Schema.Plugin.ForbidOverridesTitle'), + description: t('Schema.Plugin.ForbidOverrides'), + }), + }); + + // Get all registered plugin IDs + const registeredPluginIds = getAllRegisteredPluginIds(); + + if (registeredPluginIds.length === 0) { + // Fallback to a basic schema if no plugins are registered yet + return basePluginSchema.extend({ + pluginId: z.string().meta({ + title: t('Schema.Plugin.PluginIdTitle'), + description: t('Schema.Plugin.PluginId'), + }), + }); + } + + // Create enum from registered plugin IDs + const pluginIdEnum = z.enum(registeredPluginIds as [string, ...string[]]).meta({ + title: t('Schema.Plugin.PluginIdTitle'), + description: t('Schema.Plugin.PluginId'), + enumOptions: registeredPluginIds.map(pluginId => { + const metadata = getPluginMetadata(pluginId); + return { + value: pluginId, + label: metadata?.displayName || pluginId, + }; + }), + }); + + // Create parameter schema object with all registered plugins + const parameterSchema: Record = {}; + + for (const pluginId of registeredPluginIds) { + const schema = getPluginParameterSchema(pluginId); + if (schema) { + const metadata = getPluginMetadata(pluginId); + parameterSchema[`${pluginId}Param`] = schema.optional().meta({ + title: metadata?.displayName || pluginId, + description: metadata?.description || `Parameters for ${pluginId} plugin`, + }); + } + } + + // Combine base schema with plugin ID and parameters + return basePluginSchema.extend({ + pluginId: pluginIdEnum, + ...parameterSchema, + }); +} + +/** + * Get the type of a plugin's parameters + * @param pluginId The plugin ID + * @returns The inferred TypeScript type of the plugin's parameters + */ +export type PluginParameterType = T extends keyof ReturnType ? ReturnType[T] : never; + +/** + * Create type definitions for all registered plugin parameters + * This is used internally for type inference + */ +export function createPluginParameterTypes() { + const types: Record = {}; + + for (const pluginId of getAllRegisteredPluginIds()) { + const schema = getPluginParameterSchema(pluginId); + if (schema) { + types[pluginId] = schema; + } + } + + return types as Record; +} diff --git a/src/services/agentInstance/plugins/wikiOperationPlugin.ts b/src/services/agentInstance/plugins/wikiOperationPlugin.ts index 9ff7b6d0..148e4068 100644 --- a/src/services/agentInstance/plugins/wikiOperationPlugin.ts +++ b/src/services/agentInstance/plugins/wikiOperationPlugin.ts @@ -3,8 +3,51 @@ * Handles wiki operation tool list injection, tool calling detection and response processing * Supports creating, updating, and deleting tiddlers in wiki workspaces */ +import { identity } from 'lodash'; import { z } from 'zod/v4'; +const t = identity; + +/** + * Wiki Operation Parameter Schema + * Configuration parameters for the wiki operation plugin + */ +export const WikiOperationParameterSchema = z.object({ + toolListPosition: z.object({ + targetId: z.string().meta({ + title: t('Schema.WikiOperation.ToolListPosition.TargetIdTitle'), + description: t('Schema.WikiOperation.ToolListPosition.TargetId'), + }), + position: z.enum(['before', 'after']).meta({ + title: t('Schema.WikiOperation.ToolListPosition.PositionTitle'), + description: t('Schema.WikiOperation.ToolListPosition.Position'), + }), + }).optional().meta({ + title: t('Schema.WikiOperation.ToolListPositionTitle'), + description: t('Schema.WikiOperation.ToolListPosition'), + }), + toolResultDuration: z.number().optional().default(1).meta({ + title: t('Schema.WikiOperation.ToolResultDurationTitle'), + description: t('Schema.WikiOperation.ToolResultDuration'), + }), +}).meta({ + title: t('Schema.WikiOperation.Title'), + description: t('Schema.WikiOperation.Description'), +}); + +/** + * Type definition for wiki operation parameters + */ +export type WikiOperationParameter = z.infer; + +/** + * Get the wiki operation parameter schema + * @returns The schema for wiki operation parameters + */ +export function getWikiOperationParameterSchema() { + return WikiOperationParameterSchema; +} + import { WikiChannel } from '@/constants/channels'; import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility'; import { container } from '@services/container'; @@ -62,7 +105,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => { // Get available wikis - now handled by workspacesListPlugin // The workspaces list will be injected separately by workspacesListPlugin - const wikiOperationToolContent = ` + const wikiOperationToolContent = ` ## wiki-operation **描述**: 在Wiki工作空间中执行操作(添加、删除或设置Tiddler文本) **参数**: diff --git a/src/services/agentInstance/plugins/wikiSearchPlugin.ts b/src/services/agentInstance/plugins/wikiSearchPlugin.ts index b47ec749..dae1abd6 100644 --- a/src/services/agentInstance/plugins/wikiSearchPlugin.ts +++ b/src/services/agentInstance/plugins/wikiSearchPlugin.ts @@ -2,8 +2,67 @@ * Wiki Search plugin * Handles wiki search tool list injection, tool calling detection and response processing */ +import { identity } from 'lodash'; import { z } from 'zod/v4'; +const t = identity; + +/** + * Wiki Search Parameter Schema + * Configuration parameters for the wiki search plugin + */ +export const WikiSearchParameterSchema = z.object({ + position: z.enum(['relative', 'absolute', 'before', 'after']).meta({ + title: t('Schema.Position.TypeTitle'), + description: t('Schema.Position.Type'), + }), + targetId: z.string().meta({ + title: t('Schema.Position.TargetIdTitle'), + description: t('Schema.Position.TargetId'), + }), + bottom: z.number().optional().meta({ + title: t('Schema.Position.BottomTitle'), + description: t('Schema.Position.Bottom'), + }), + sourceType: z.enum(['wiki']).meta({ + title: t('Schema.WikiSearch.SourceTypeTitle'), + description: t('Schema.WikiSearch.SourceType'), + }), + toolListPosition: z.object({ + targetId: z.string().meta({ + title: t('Schema.WikiSearch.ToolListPosition.TargetIdTitle'), + description: t('Schema.WikiSearch.ToolListPosition.TargetId'), + }), + position: z.enum(['before', 'after']).meta({ + title: t('Schema.WikiSearch.ToolListPosition.PositionTitle'), + description: t('Schema.WikiSearch.ToolListPosition.Position'), + }), + }).optional().meta({ + title: t('Schema.WikiSearch.ToolListPositionTitle'), + description: t('Schema.WikiSearch.ToolListPosition'), + }), + toolResultDuration: z.number().optional().default(1).meta({ + title: t('Schema.WikiSearch.ToolResultDurationTitle'), + description: t('Schema.WikiSearch.ToolResultDuration'), + }), +}).meta({ + title: t('Schema.WikiSearch.Title'), + description: t('Schema.WikiSearch.Description'), +}); + +/** + * Type definition for wiki search parameters + */ +export type WikiSearchParameter = z.infer; + +/** + * Get the wiki search parameter schema + * @returns The schema for wiki search parameters + */ +export function getWikiSearchParameterSchema() { + return WikiSearchParameterSchema; +} + import { WikiChannel } from '@/constants/channels'; import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility'; import { container } from '@services/container'; diff --git a/src/services/agentInstance/plugins/workspacesListPlugin.ts b/src/services/agentInstance/plugins/workspacesListPlugin.ts index c95ca4f5..8e12a2bd 100644 --- a/src/services/agentInstance/plugins/workspacesListPlugin.ts +++ b/src/services/agentInstance/plugins/workspacesListPlugin.ts @@ -2,6 +2,42 @@ * Workspaces List plugin * Handles injection of available wiki workspaces list into prompts */ +import { identity } from 'lodash'; +import { z } from 'zod/v4'; + +const t = identity; + +/** + * Workspaces List Parameter Schema + * Configuration parameters for the workspaces list plugin + */ +export const WorkspacesListParameterSchema = z.object({ + targetId: z.string().meta({ + title: t('Schema.WorkspacesList.TargetIdTitle'), + description: t('Schema.WorkspacesList.TargetId'), + }), + position: z.enum(['before', 'after']).meta({ + title: t('Schema.WorkspacesList.PositionTitle'), + description: t('Schema.WorkspacesList.Position'), + }), +}).meta({ + title: t('Schema.WorkspacesList.Title'), + description: t('Schema.WorkspacesList.Description'), +}); + +/** + * Type definition for workspaces list parameters + */ +export type WorkspacesListParameter = z.infer; + +/** + * Get the workspaces list parameter schema + * @returns The schema for workspaces list parameters + */ +export function getWorkspacesListParameterSchema() { + return WorkspacesListParameterSchema; +} + import { container } from '@services/container'; import { logger } from '@services/libs/log'; import serviceIdentifier from '@services/serviceIdentifier'; @@ -29,7 +65,7 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => { try { // Handle workspaces list injection if targetId is configured - if (workspacesListParameter.targetId) { + if (workspacesListParameter?.targetId) { const target = findPromptById(prompts, workspacesListParameter.targetId); if (!target) { logger.warn('Workspaces list target prompt not found', { diff --git a/src/services/agentInstance/promptConcat/promptConcatSchema/index.ts b/src/services/agentInstance/promptConcat/promptConcatSchema/index.ts index 768917bf..9ed01072 100644 --- a/src/services/agentInstance/promptConcat/promptConcatSchema/index.ts +++ b/src/services/agentInstance/promptConcat/promptConcatSchema/index.ts @@ -1,7 +1,7 @@ +import { createDynamicPromptConcatPluginSchema } from '@services/agentInstance/plugins/schemaRegistry'; import { identity } from 'lodash'; import { z } from 'zod/v4'; import { ModelParametersSchema, ProviderModelSchema } from './modelParameters'; -import { PromptConcatPluginSchema } from './plugin'; import { PromptSchema } from './prompts'; import { ResponseSchema } from './response'; import { HANDLER_CONFIG_UI_SCHEMA } from './uiSchema'; @@ -39,27 +39,33 @@ export const AIConfigSchema = BaseAPIConfigSchema /** * Handler configuration schema * Contains the handler-related configuration fields for prompts, responses, and plugins + * This is dynamically generated to include all registered plugins */ -export const HandlerConfigSchema = z.object({ - prompts: z.array(PromptSchema).meta({ - description: t('Schema.AgentConfig.PromptConfig.Prompts'), - title: t('PromptConfig.Tabs.Prompts'), - }), - response: z.array(ResponseSchema).meta({ - description: t('Schema.AgentConfig.PromptConfig.Response'), - title: t('PromptConfig.Tabs.Response'), - }), - plugins: z.array(PromptConcatPluginSchema).meta({ - description: t('Schema.AgentConfig.PromptConfig.Plugins'), - title: t('PromptConfig.Tabs.Plugins'), - }), -}).meta({ - title: t('Schema.AgentConfig.PromptConfig.Title'), - description: t('Schema.AgentConfig.PromptConfig.Description'), - uiSchema: HANDLER_CONFIG_UI_SCHEMA, -}); +export function getHandlerConfigSchema() { + const dynamicPluginSchema = createDynamicPromptConcatPluginSchema(); + + return z.object({ + prompts: z.array(PromptSchema).meta({ + description: t('Schema.AgentConfig.PromptConfig.Prompts'), + title: t('PromptConfig.Tabs.Prompts'), + }), + response: z.array(ResponseSchema).meta({ + description: t('Schema.AgentConfig.PromptConfig.Response'), + title: t('PromptConfig.Tabs.Response'), + }), + plugins: z.array(dynamicPluginSchema).meta({ + description: t('Schema.AgentConfig.PromptConfig.Plugins'), + title: t('PromptConfig.Tabs.Plugins'), + }), + }).meta({ + title: t('Schema.AgentConfig.PromptConfig.Title'), + description: t('Schema.AgentConfig.PromptConfig.Description'), + uiSchema: HANDLER_CONFIG_UI_SCHEMA, + }); +} + /** - * Agent configuration schema + * Agent configuration schema (dynamic) * @example * ```json * { @@ -77,30 +83,37 @@ export const HandlerConfigSchema = z.object({ * } * ``` */ -export const AgentConfigSchema = BaseAPIConfigSchema.extend({ - id: z.string().meta({ - title: t('Schema.AgentConfig.IdTitle'), - description: t('Schema.AgentConfig.Id'), - }), - handlerConfig: HandlerConfigSchema, -}).meta({ - title: t('Schema.AgentConfig.Title'), - description: t('Schema.AgentConfig.Description'), -}); +export function getAgentConfigSchema() { + const dynamicHandlerConfigSchema = getHandlerConfigSchema(); + + return BaseAPIConfigSchema.extend({ + id: z.string().meta({ + title: t('Schema.AgentConfig.IdTitle'), + description: t('Schema.AgentConfig.Id'), + }), + handlerConfig: dynamicHandlerConfigSchema, + }).meta({ + title: t('Schema.AgentConfig.Title'), + description: t('Schema.AgentConfig.Description'), + }); +} /** - * Default agents list schema + * Default agents list schema (dynamic) * Contains an array of agent configurations */ -export const DefaultAgentsSchema = z.array(AgentConfigSchema).meta({ - title: t('Schema.DefaultAgents.Title'), - description: t('Schema.DefaultAgents.Description'), -}); +export function getDefaultAgentsSchema() { + const dynamicAgentConfigSchema = getAgentConfigSchema(); + return z.array(dynamicAgentConfigSchema).meta({ + title: t('Schema.DefaultAgents.Title'), + description: t('Schema.DefaultAgents.Description'), + }); +} -export type DefaultAgents = z.infer; -export type AgentPromptDescription = z.infer; +export type DefaultAgents = z.infer>; +export type AgentPromptDescription = z.infer>; export type AiAPIConfig = z.infer; -export type HandlerConfig = z.infer; +export type HandlerConfig = z.infer>; // Re-export all schemas and types export * from './modelParameters'; diff --git a/src/services/agentInstance/promptConcat/promptConcatSchema/jsonSchema.ts b/src/services/agentInstance/promptConcat/promptConcatSchema/jsonSchema.ts index a9290894..ddf873c6 100644 --- a/src/services/agentInstance/promptConcat/promptConcatSchema/jsonSchema.ts +++ b/src/services/agentInstance/promptConcat/promptConcatSchema/jsonSchema.ts @@ -1,11 +1,17 @@ import { z } from 'zod/v4'; -import { HandlerConfigSchema } from './index'; +import { getHandlerConfigSchema } from './index'; /** + * Get the dynamically generated JSON Schema for handler configuration + * This allows the frontend to generate forms based on currently registered plugins + * * Pre-generated JSON Schema for just the handler configuration part * This can be used when only the handler configuration is needed * It contains the prompt configuration without the parent agent structure * * Description field is i18n key, use i18nAlly extension to see it on VSCode. And use react-i18next to translate it on frontend. */ -export const promptConcatHandlerConfigJsonSchema = z.toJSONSchema(HandlerConfigSchema, { target: 'draft-7' }); +export function getPromptConcatHandlerConfigJsonSchema() { + const dynamicHandlerConfigSchema = getHandlerConfigSchema(); + return z.toJSONSchema(dynamicHandlerConfigSchema, { target: 'draft-7' }); +} diff --git a/src/services/agentInstance/promptConcat/promptConcatSchema/plugin.ts b/src/services/agentInstance/promptConcat/promptConcatSchema/plugin.ts index 966692c8..a7e09206 100644 --- a/src/services/agentInstance/promptConcat/promptConcatSchema/plugin.ts +++ b/src/services/agentInstance/promptConcat/promptConcatSchema/plugin.ts @@ -1,222 +1,26 @@ -import { identity } from 'lodash'; -import { z } from 'zod/v4'; +// Import parameter types from plugin files +import type { ModelContextProtocolParameter } from '@services/agentInstance/plugins/modelContextProtocolPlugin'; +import type { DynamicPositionParameter, FullReplacementParameter } from '@services/agentInstance/plugins/promptPlugins'; +import type { WikiOperationParameter } from '@services/agentInstance/plugins/wikiOperationPlugin'; +import type { WikiSearchParameter } from '@services/agentInstance/plugins/wikiSearchPlugin'; +import type { WorkspacesListParameter } from '@services/agentInstance/plugins/workspacesListPlugin'; -const t = identity; +/** + * Type definition for prompt concat plugin + * This includes all possible parameter fields for type safety + */ +export type IPromptConcatPlugin = { + id: string; + caption?: string; + content?: string; + forbidOverrides?: boolean; + pluginId: string; -export const FullReplacementParameterSchema = z.object({ - targetId: z.string().meta({ - title: t('Schema.FullReplacement.TargetIdTitle'), - description: t('Schema.FullReplacement.TargetId'), - }), - sourceType: z.enum(['historyOfSession', 'llmResponse']).meta({ - title: t('Schema.FullReplacement.SourceTypeTitle'), - description: t('Schema.FullReplacement.SourceType'), - }), -}).meta({ - title: t('Schema.FullReplacement.Title'), - description: t('Schema.FullReplacement.Description'), -}); - -export const DynamicPositionParameterSchema = z.object({ - targetId: z.string().meta({ - title: t('Schema.Position.TargetIdTitle'), - description: t('Schema.Position.TargetId'), - }), - position: z.enum(['before', 'after', 'relative']).meta({ - title: t('Schema.Position.TypeTitle'), - description: t('Schema.Position.Type'), - }), -}).meta({ - title: t('Schema.Position.Title'), - description: t('Schema.Position.Description'), -}); - -export const WikiSearchParameterSchema = z.object({ - position: z.enum(['relative', 'absolute', 'before', 'after']).meta({ - title: t('Schema.Position.TypeTitle'), - description: t('Schema.Position.Type'), - }), - targetId: z.string().meta({ - title: t('Schema.Position.TargetIdTitle'), - description: t('Schema.Position.TargetId'), - }), - bottom: z.number().optional().meta({ - title: t('Schema.Position.BottomTitle'), - description: t('Schema.Position.Bottom'), - }), - sourceType: z.enum(['wiki']).meta({ - title: t('Schema.WikiSearch.SourceTypeTitle'), - description: t('Schema.WikiSearch.SourceType'), - }), - toolListPosition: z.object({ - targetId: z.string().meta({ - title: t('Schema.WikiSearch.ToolListPosition.TargetIdTitle'), - description: t('Schema.WikiSearch.ToolListPosition.TargetId'), - }), - position: z.enum(['before', 'after']).meta({ - title: t('Schema.WikiSearch.ToolListPosition.PositionTitle'), - description: t('Schema.WikiSearch.ToolListPosition.Position'), - }), - }).optional().meta({ - title: t('Schema.WikiSearch.ToolListPositionTitle'), - description: t('Schema.WikiSearch.ToolListPosition'), - }), - toolResultDuration: z.number().optional().default(1).meta({ - title: t('Schema.WikiSearch.ToolResultDurationTitle'), - description: t('Schema.WikiSearch.ToolResultDuration'), - }), -}).meta({ - title: t('Schema.WikiSearch.Title'), - description: t('Schema.WikiSearch.Description'), -}); - -export const WikiOperationParameterSchema = z.object({ - toolListPosition: z.object({ - targetId: z.string().meta({ - title: t('Schema.WikiOperation.ToolListPosition.TargetIdTitle'), - description: t('Schema.WikiOperation.ToolListPosition.TargetId'), - }), - position: z.enum(['before', 'after']).meta({ - title: t('Schema.WikiOperation.ToolListPosition.PositionTitle'), - description: t('Schema.WikiOperation.ToolListPosition.Position'), - }), - }).optional().meta({ - title: t('Schema.WikiOperation.ToolListPositionTitle'), - description: t('Schema.WikiOperation.ToolListPosition'), - }), - toolResultDuration: z.number().optional().default(1).meta({ - title: t('Schema.WikiOperation.ToolResultDurationTitle'), - description: t('Schema.WikiOperation.ToolResultDuration'), - }), -}).meta({ - title: t('Schema.WikiOperation.Title'), - description: t('Schema.WikiOperation.Description'), -}); - -export const WorkspacesListParameterSchema = z.object({ - targetId: z.string().meta({ - title: t('Schema.WorkspacesList.TargetIdTitle'), - description: t('Schema.WorkspacesList.TargetId'), - }), - position: z.enum(['before', 'after']).meta({ - title: t('Schema.WorkspacesList.PositionTitle'), - description: t('Schema.WorkspacesList.Position'), - }), -}).meta({ - title: t('Schema.WorkspacesList.Title'), - description: t('Schema.WorkspacesList.Description'), -}); - -export const ModelContextProtocolParameterSchema = z.object({ - id: z.string().meta({ - title: t('Schema.MCP.IdTitle'), - description: t('Schema.MCP.Id'), - }), - timeoutSecond: z.number().optional().meta({ - title: t('Schema.MCP.TimeoutSecondTitle'), - description: t('Schema.MCP.TimeoutSecond'), - }), - timeoutMessage: z.string().optional().meta({ - title: t('Schema.MCP.TimeoutMessageTitle'), - description: t('Schema.MCP.TimeoutMessage'), - }), - position: z.enum(['before', 'after']).meta({ - title: t('Schema.Position.TypeTitle'), - description: t('Schema.Position.Type'), - }), - targetId: z.string().meta({ - title: t('Schema.Position.TargetIdTitle'), - description: t('Schema.Position.TargetId'), - }), -}).meta({ - title: t('Schema.MCP.Title'), - description: t('Schema.MCP.Description'), -}); - -export const ToolCallingParameterSchema = z.object({ - targetId: z.string().meta({ - title: t('Schema.ToolCalling.TargetIdTitle'), - description: t('Schema.ToolCalling.TargetId'), - }), - match: z.string().meta({ - title: t('Schema.ToolCalling.MatchTitle'), - description: t('Schema.ToolCalling.Match'), - }), -}).meta({ - title: t('Schema.ToolCalling.Title'), - description: t('Schema.ToolCalling.Description'), -}); - -export const PromptConcatPluginSchema = z.object({ - id: z.string().meta({ - title: t('Schema.Plugin.IdTitle'), - description: t('Schema.Plugin.Id'), - }), - caption: z.string().optional().meta({ - title: t('Schema.Plugin.CaptionTitle'), - description: t('Schema.Plugin.Caption'), - }), - content: z.string().optional().meta({ - title: t('Schema.Plugin.ContentTitle'), - description: t('Schema.Plugin.Content'), - }), - forbidOverrides: z.boolean().optional().default(false).meta({ - title: t('Schema.Plugin.ForbidOverridesTitle'), - description: t('Schema.Plugin.ForbidOverrides'), - }), - pluginId: z.enum([ - 'fullReplacement', - 'dynamicPosition', - 'retrievalAugmentedGeneration', - 'wikiSearch', - 'wikiOperation', - 'workspacesList', - 'modelContextProtocol', - 'toolCalling', - ]).meta({ - title: t('Schema.Plugin.PluginIdTitle'), - description: t('Schema.Plugin.PluginId'), - enumOptions: [ - { value: 'fullReplacement', label: t('Schema.Plugin.FullReplacementParamTitle') }, - { value: 'dynamicPosition', label: t('Schema.Plugin.DynamicPositionParamTitle') }, - { value: 'retrievalAugmentedGeneration', label: t('Schema.Plugin.RAGParamTitle') }, - { value: 'wikiSearch', label: t('Schema.Plugin.WikiSearchParamTitle') }, - { value: 'wikiOperation', label: t('Schema.Plugin.WikiOperationParamTitle') }, - { value: 'workspacesList', label: t('Schema.Plugin.WorkspacesListParamTitle') }, - { value: 'modelContextProtocol', label: t('Schema.Plugin.MCPParamTitle') }, - { value: 'toolCalling', label: t('Schema.Plugin.ToolCallingParamTitle') }, - ], - }), - // 参数配置 - fullReplacementParam: FullReplacementParameterSchema.optional().meta({ - title: t('Schema.Plugin.FullReplacementParamTitle'), - description: t('Schema.Plugin.FullReplacementParam'), - }), - dynamicPositionParam: DynamicPositionParameterSchema.optional().meta({ - title: t('Schema.Plugin.DynamicPositionParamTitle'), - description: t('Schema.Plugin.DynamicPositionParam'), - }), - wikiSearchParam: WikiSearchParameterSchema.optional().meta({ - title: t('Schema.Plugin.WikiSearchParamTitle'), - description: t('Schema.Plugin.WikiSearchParam'), - }), - wikiOperationParam: WikiOperationParameterSchema.optional().meta({ - title: t('Schema.Plugin.WikiOperationParamTitle'), - description: t('Schema.Plugin.WikiOperationParam'), - }), - workspacesListParam: WorkspacesListParameterSchema.optional().meta({ - title: t('Schema.Plugin.WorkspacesListParamTitle'), - description: t('Schema.Plugin.WorkspacesListParam'), - }), - modelContextProtocolParam: ModelContextProtocolParameterSchema.optional().meta({ - title: t('Schema.Plugin.MCPParamTitle'), - description: t('Schema.Plugin.MCPParam'), - }), - toolCallingParam: ToolCallingParameterSchema.optional().meta({ - title: t('Schema.Plugin.ToolCallingParamTitle'), - description: t('Schema.Plugin.ToolCallingParam'), - }), -}); - -// Allow pluginId to be a string so less error on test. -export type IPromptConcatPlugin = Omit, 'pluginId'> & { pluginId: string }; + // Plugin-specific parameters + fullReplacementParam?: FullReplacementParameter; + dynamicPositionParam?: DynamicPositionParameter; + wikiOperationParam?: WikiOperationParameter; + wikiSearchParam?: WikiSearchParameter; + workspacesListParam?: WorkspacesListParameter; + modelContextProtocolParam?: ModelContextProtocolParameter; +}; diff --git a/src/services/agentInstance/promptConcat/promptConcatSchema/uiSchema.ts b/src/services/agentInstance/promptConcat/promptConcatSchema/uiSchema.ts index 50392de5..28093926 100644 --- a/src/services/agentInstance/promptConcat/promptConcatSchema/uiSchema.ts +++ b/src/services/agentInstance/promptConcat/promptConcatSchema/uiSchema.ts @@ -79,13 +79,6 @@ export const HANDLER_CONFIG_UI_SCHEMA: UiSchema = { showWhen: 'modelContextProtocol', }, }, - toolCallingParam: { - 'ui:field': 'ConditionalField', - 'ui:condition': { - dependsOn: 'pluginId', - showWhen: 'toolCalling', - }, - }, }, }, response: {