mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-01-23 13:01:25 -08:00
refactor: simplify tool writing
This commit is contained in:
parent
b8a9bbcf3d
commit
46b95cd65b
32 changed files with 2178 additions and 1796 deletions
12
.github/mcp.json
vendored
Normal file
12
.github/mcp.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"servers": {
|
||||
"chromedevtools/chrome-devtools-mcp": {
|
||||
"type": "stdio",
|
||||
"command": "pnpm dlx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--browserUrl=http://localhost:9222"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"start:dev": "cross-env NODE_ENV=development electron-forge start",
|
||||
"clean": "pnpm run clean:cache && rimraf -- ./out ./logs ./userData-dev ./userData-test ./wiki-dev ./wiki-test ./node_modules/tiddlywiki/plugins/linonetwo",
|
||||
"clean:cache": "rimraf -- ./node_modules/.vite .vite",
|
||||
"start:dev:mcp": "cross-env NODE_ENV=development DEBUG_WORKER=true electron-forge start --remote-debugging-port=9222",
|
||||
"start:dev:debug-worker": "cross-env NODE_ENV=development DEBUG_WORKER=true electron-forge start",
|
||||
"start:dev:debug-main": "cross-env NODE_ENV=development DEBUG_MAIN=true electron-forge start",
|
||||
"start:dev:debug-vite": "cross-env NODE_ENV=development DEBUG=electron-forge:* electron-forge start",
|
||||
|
|
|
|||
17
src/__tests__/__mocks__/services-i18n.ts
Normal file
17
src/__tests__/__mocks__/services-i18n.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Mock for @services/libs/i18n
|
||||
*/
|
||||
import { vi } from 'vitest';
|
||||
|
||||
export const i18n = {
|
||||
t: vi.fn((key: string) => {
|
||||
// Return the key itself as fallback
|
||||
return key;
|
||||
}),
|
||||
changeLanguage: vi.fn(),
|
||||
language: 'en',
|
||||
};
|
||||
|
||||
export const placeholder = {
|
||||
t: (key: string) => key,
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@ import './__mocks__/services-container';
|
|||
import { vi } from 'vitest';
|
||||
vi.mock('react-i18next', () => import('./__mocks__/react-i18next'));
|
||||
vi.mock('@services/libs/log', () => import('./__mocks__/services-log'));
|
||||
vi.mock('@services/libs/i18n', () => import('./__mocks__/services-i18n'));
|
||||
|
||||
/**
|
||||
* Mock the `electron` module for testing
|
||||
|
|
|
|||
|
|
@ -279,16 +279,15 @@ describe('PromptPreviewDialog - Tool Information Rendering', () => {
|
|||
// Check for wiki search tool insertion (from wikiSearch plugin)
|
||||
let wikiSearchElement: IPrompt | undefined = childrenAfterBeforeTool.find((c: IPrompt) => {
|
||||
const body = `${c.caption ?? ''} ${c.text ?? ''}`;
|
||||
return /Available Tools:/i.test(body) || /Tool ID:\s*wiki-search/i.test(body) || /wiki-search/i.test(body);
|
||||
return /wiki-search/i.test(body);
|
||||
});
|
||||
if (!wikiSearchElement) {
|
||||
wikiSearchElement = findPromptNodeByText(result?.processedPrompts, /Available Tools:/i) || findPromptNodeByText(result?.processedPrompts, /Tool ID:\s*wiki-search/i) ||
|
||||
findPromptNodeByText(result?.processedPrompts, /wiki-search/i);
|
||||
wikiSearchElement = findPromptNodeByText(result?.processedPrompts, /wiki-search/i);
|
||||
}
|
||||
expect(wikiSearchElement).toBeDefined();
|
||||
const wikiSearchText = `${wikiSearchElement?.caption ?? ''} ${wikiSearchElement?.text ?? ''}`;
|
||||
expect(wikiSearchText).toContain('Wiki search tool');
|
||||
expect(wikiSearchText).toContain('## wiki-search');
|
||||
// Check that wiki-search tool is mentioned in the content (either as tool ID or in description)
|
||||
expect(wikiSearchText.toLowerCase()).toContain('wiki-search');
|
||||
|
||||
// Verify the order: before-tool -> workspaces -> wiki-operation -> wiki-search -> post-tool
|
||||
const postToolElement: IPrompt | undefined = toolsSection?.children?.find((c: IPrompt) => c.id === 'default-post-tool');
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ let testStreamResponses: Array<{ status: string; content: string; requestId: str
|
|||
// Import plugin components for direct testing
|
||||
import type { IPromptConcatTool } from '@services/agentInstance/promptConcat/promptConcatSchema';
|
||||
import type { IDatabaseService } from '@services/database/interface';
|
||||
import { createAgentFrameworkHooks, createHooksWithTools, initializeToolSystem, PromptConcatHookContext } from '../../tools/index';
|
||||
import { createAgentFrameworkHooks, createHooksWithPlugins, initializePluginSystem, PromptConcatHookContext } from '../../tools/index';
|
||||
import { wikiSearchTool } from '../../tools/wikiSearch';
|
||||
import { basicPromptConcatHandler } from '../taskAgent';
|
||||
import type { AgentFrameworkContext } from '../utilities/type';
|
||||
|
|
@ -42,7 +42,7 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
|
|||
const { container } = await import('@services/container');
|
||||
|
||||
// Ensure built-in tool registry includes all built-in tools
|
||||
await initializeToolSystem();
|
||||
await initializePluginSystem();
|
||||
|
||||
// Prepare a mock DataSource/repository so AgentInstanceService.initialize() can run
|
||||
const mockRepo = {
|
||||
|
|
@ -127,8 +127,8 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
|
|||
],
|
||||
};
|
||||
|
||||
// Create hooks and register tools as defined in agentFrameworkConfig
|
||||
const { hooks: promptHooks } = await createHooksWithTools(agentFrameworkConfig);
|
||||
// Create hooks and register plugins as defined in agentFrameworkConfig
|
||||
const { hooks: promptHooks } = await createHooksWithPlugins(agentFrameworkConfig);
|
||||
// First run workspacesList tool to inject available workspaces (if present)
|
||||
const workspacesPlugin = agentFrameworkConfig.plugins?.find(p => p.toolId === 'workspacesList');
|
||||
if (workspacesPlugin) {
|
||||
|
|
@ -200,7 +200,7 @@ describe('WikiSearch Plugin Integration & YieldNextRound Mechanism', () => {
|
|||
};
|
||||
|
||||
// Use hooks registered with all plugins import { AgentFrameworkConfig }
|
||||
const { hooks: responseHooks } = await createHooksWithTools(agentFrameworkConfig);
|
||||
const { hooks: responseHooks } = await createHooksWithPlugins(agentFrameworkConfig);
|
||||
// Execute the response complete hook
|
||||
await responseHooks.responseComplete.promise(responseContext);
|
||||
// reuse containerForAssert from above assertions
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import serviceIdentifier from '@services/serviceIdentifier';
|
|||
import { merge } from 'lodash';
|
||||
import type { AgentInstanceLatestStatus, AgentInstanceMessage, IAgentInstanceService } from '../interface';
|
||||
import { AgentFrameworkConfig, AgentPromptDescription, AiAPIConfig } from '../promptConcat/promptConcatSchema';
|
||||
import type { IPromptConcatTool } from '../promptConcat/promptConcatSchema/plugin';
|
||||
import type { IPromptConcatTool } from '../promptConcat/promptConcatSchema/tools';
|
||||
import { responseConcat } from '../promptConcat/responseConcat';
|
||||
import { getFinalPromptResult } from '../promptConcat/utilities';
|
||||
import { createHooksWithTools } from '../tools';
|
||||
import { createHooksWithPlugins } from '../tools';
|
||||
import { YieldNextRoundTarget } from '../tools/types';
|
||||
import { canceled, completed, error, working } from './utilities/statusUtilities';
|
||||
import { AgentFrameworkContext } from './utilities/type';
|
||||
|
|
@ -32,7 +32,7 @@ export async function* basicPromptConcatHandler(context: AgentFrameworkContext)
|
|||
let currentRequestId: string | undefined;
|
||||
const lastUserMessage: AgentInstanceMessage | undefined = context.agent.messages[context.agent.messages.length - 1];
|
||||
// Create and register handler hooks based on framework config
|
||||
const { hooks: agentFrameworkHooks, toolConfigs } = await createHooksWithTools(context.agentDef.agentFrameworkConfig || {});
|
||||
const { hooks: agentFrameworkHooks, pluginConfigs } = await createHooksWithPlugins(context.agentDef.agentFrameworkConfig || {});
|
||||
|
||||
// Log the start of handler execution with context information
|
||||
logger.debug('Starting prompt handler execution', {
|
||||
|
|
@ -175,7 +175,7 @@ export async function* basicPromptConcatHandler(context: AgentFrameworkContext)
|
|||
response,
|
||||
requestId: currentRequestId,
|
||||
isFinal: true,
|
||||
toolConfig: (toolConfigs.length > 0 ? toolConfigs[0] : {}) as IPromptConcatTool, // First config for compatibility
|
||||
toolConfig: (pluginConfigs.length > 0 ? pluginConfigs[0] : {}) as IPromptConcatTool, // First config for compatibility
|
||||
agentFrameworkConfig: context.agentDef.agentFrameworkConfig, // Pass complete config for tool access
|
||||
actions: undefined as { yieldNextRoundTo?: 'self' | 'human'; newUserMessage?: string } | undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type { AgentFramework, AgentFrameworkContext } from '@services/agentInsta
|
|||
import { promptConcatStream, PromptConcatStreamState } from '@services/agentInstance/promptConcat/promptConcat';
|
||||
import type { AgentPromptDescription } from '@services/agentInstance/promptConcat/promptConcatSchema';
|
||||
import { getPromptConcatAgentFrameworkConfigJsonSchema } from '@services/agentInstance/promptConcat/promptConcatSchema/jsonSchema';
|
||||
import { createHooksWithTools, initializeToolSystem } from '@services/agentInstance/tools';
|
||||
import { createHooksWithPlugins, initializePluginSystem } from '@services/agentInstance/tools';
|
||||
import type { IDatabaseService } from '@services/database/interface';
|
||||
import { AgentInstanceEntity, AgentInstanceMessageEntity } from '@services/database/schema/agent';
|
||||
import { logger } from '@services/libs/log';
|
||||
|
|
@ -65,7 +65,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
public async initializeFrameworks(): Promise<void> {
|
||||
try {
|
||||
// Register tools to global registry once during initialization
|
||||
await initializeToolSystem();
|
||||
await initializePluginSystem();
|
||||
logger.debug('AgentInstance Tool system initialized and tools registered to global registry');
|
||||
|
||||
// Register built-in frameworks
|
||||
|
|
@ -379,8 +379,8 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
isCancelled: () => cancelToken.value,
|
||||
};
|
||||
|
||||
// Create fresh hooks for this framework execution and register tools based on frameworkConfig
|
||||
const { hooks: frameworkHooks } = await createHooksWithTools(agentDefinition.agentFrameworkConfig || {});
|
||||
// Create fresh hooks for this framework execution and register plugins based on frameworkConfig
|
||||
const { hooks: frameworkHooks } = await createHooksWithPlugins(agentDefinition.agentFrameworkConfig || {});
|
||||
|
||||
// Trigger userMessageReceived hook with the configured tools
|
||||
await frameworkHooks.userMessageReceived.promise({
|
||||
|
|
|
|||
|
|
@ -14,43 +14,99 @@ The `promptConcat` function uses a tapable hooks-based tool system. Built-in too
|
|||
- `processPrompts`: Modifies prompt tree during processing
|
||||
- `finalizePrompts`: Final processing before LLM call
|
||||
- `postProcess`: Handles response processing
|
||||
- `responseComplete`: Called when AI response is done (for tool execution)
|
||||
|
||||
2. **Built-in Tools**:
|
||||
- `fullReplacement`: Replaces content from various sources
|
||||
- `dynamicPosition`: Inserts content at specific positions
|
||||
- `retrievalAugmentedGeneration`: Retrieves content from wiki/external sources
|
||||
- `wikiSearch`: Search wiki content with filter or vector search
|
||||
- `wikiOperation`: Create, update, delete tiddlers
|
||||
- `workspacesList`: Inject available workspaces into prompts
|
||||
- `modelContextProtocol`: Integrates with external MCP servers
|
||||
- `toolCalling`: Processes function calls in responses
|
||||
|
||||
3. **Tool Registration**:
|
||||
- Tools are registered by `toolId` field in the `plugins` array
|
||||
- Tools created with `registerToolDefinition` are auto-registered
|
||||
- Each tool instance has its own configuration parameters
|
||||
- Built-in tools are auto-registered on system initialization
|
||||
|
||||
### Tool Lifecycle
|
||||
### Adding New Tools (New API)
|
||||
|
||||
1. **Registration**: Tools are registered during initialization
|
||||
2. **Configuration**: Tools are loaded based on `agentFrameworkConfig.plugins` array
|
||||
3. **Execution**: Hooks execute tools in registration order
|
||||
4. **Error Handling**: Individual tool failures don't stop the pipeline
|
||||
Use the `registerToolDefinition` function for a declarative, low-boilerplate approach:
|
||||
|
||||
### Adding New Tools
|
||||
```typescript
|
||||
import { z } from 'zod/v4';
|
||||
import { registerToolDefinition } from './defineTool';
|
||||
|
||||
1. Create tool function in `tools/` directory
|
||||
2. Register in `tools/index.ts`
|
||||
3. Add `toolId` to schema enum
|
||||
4. Add parameter schema if needed
|
||||
// 1. Define config schema (user-configurable in UI)
|
||||
const MyToolConfigSchema = z.object({
|
||||
targetId: z.string(),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
});
|
||||
|
||||
Each tool receives a hooks object and registers handlers for specific hook points. Tools can modify prompt trees, inject content, process responses, and trigger additional LLM calls.
|
||||
// 2. Define LLM-callable tool schema (injected into prompts)
|
||||
const MyLLMToolSchema = z.object({
|
||||
query: z.string(),
|
||||
limit: z.number().optional().default(10),
|
||||
}).meta({
|
||||
title: 'my-tool', // Tool name for LLM
|
||||
description: 'Search for something',
|
||||
examples: [{ query: 'example', limit: 5 }],
|
||||
});
|
||||
|
||||
### Example Tool Structure
|
||||
// 3. Register the tool
|
||||
const myToolDef = registerToolDefinition({
|
||||
toolId: 'myTool',
|
||||
displayName: 'My Tool',
|
||||
description: 'Does something useful',
|
||||
configSchema: MyToolConfigSchema,
|
||||
llmToolSchemas: {
|
||||
'my-tool': MyLLMToolSchema,
|
||||
},
|
||||
|
||||
// Called during prompt processing
|
||||
onProcessPrompts({ config, injectToolList, injectContent }) {
|
||||
// Inject tool description into prompts
|
||||
injectToolList({
|
||||
targetId: config.targetId,
|
||||
position: 'after',
|
||||
});
|
||||
},
|
||||
|
||||
// Called when AI response is complete
|
||||
async onResponseComplete({ toolCall, executeToolCall }) {
|
||||
if (toolCall?.toolId !== 'my-tool') return;
|
||||
|
||||
await executeToolCall('my-tool', async (params) => {
|
||||
// Execute the tool and return result
|
||||
const result = await doSomething(params.query, params.limit);
|
||||
return { success: true, data: result };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const myTool = myToolDef.tool;
|
||||
```
|
||||
|
||||
### Handler Context Utilities
|
||||
|
||||
The `defineTool` API provides helpful utilities:
|
||||
|
||||
- `findPrompt(id)` - Find a prompt by ID in the tree
|
||||
- `injectToolList(options)` - Inject LLM tool schemas at a position
|
||||
- `injectContent(options)` - Inject arbitrary content
|
||||
- `executeToolCall(toolName, executor)` - Execute and handle tool results
|
||||
- `addToolResult(options)` - Manually add a tool result message
|
||||
- `yieldToSelf()` - Signal the agent should continue with another round
|
||||
|
||||
### Legacy Tool Structure
|
||||
|
||||
For more complex scenarios, you can still use the raw tapable hooks:
|
||||
|
||||
```typescript
|
||||
export const myTool: PromptConcatTool = (hooks) => {
|
||||
hooks.processPrompts.tapAsync('myTool', async (context, callback) => {
|
||||
const { tool, prompts, messages } = context;
|
||||
const { toolConfig, prompts, messages } = context;
|
||||
// Tool logic here
|
||||
callback(null, context);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Agent Status Infrastructure
|
||||
*
|
||||
* Handles agent status updates and persistence.
|
||||
* This is core infrastructure, not a user-configurable plugin.
|
||||
*/
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import type { IAgentInstanceService } from '../../interface';
|
||||
import type { AgentStatusContext, PromptConcatHooks } from '../../tools/types';
|
||||
|
||||
/**
|
||||
* Register agent status handlers to hooks
|
||||
*/
|
||||
export function registerAgentStatus(hooks: PromptConcatHooks): void {
|
||||
// Handle agent status persistence
|
||||
hooks.agentStatusChanged.tapAsync('agentStatus', async (context: AgentStatusContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, status } = context;
|
||||
|
||||
// Get the agent instance service to update status
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
// Update agent status in database
|
||||
await agentInstanceService.updateAgent(agentFrameworkContext.agent.id, {
|
||||
status,
|
||||
});
|
||||
|
||||
// Update the agent object for immediate use
|
||||
agentFrameworkContext.agent.status = status;
|
||||
|
||||
logger.debug('Agent status updated in database', {
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
state: status.state,
|
||||
});
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Agent status error in agentStatusChanged', {
|
||||
error,
|
||||
agentId: context.agentFrameworkContext.agent.id,
|
||||
status: context.status,
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Core Infrastructure
|
||||
*
|
||||
* Core infrastructure components that are always active, not user-configurable.
|
||||
* These handle message persistence, streaming updates, agent status, and UI sync.
|
||||
*/
|
||||
import type { PromptConcatHooks } from '../../tools/types';
|
||||
import { registerAgentStatus } from './agentStatus';
|
||||
import { registerMessagePersistence } from './messagePersistence';
|
||||
import { registerStreamingResponse } from './streamingResponse';
|
||||
|
||||
export { registerAgentStatus } from './agentStatus';
|
||||
export { registerMessagePersistence } from './messagePersistence';
|
||||
export { registerStreamingResponse } from './streamingResponse';
|
||||
|
||||
/**
|
||||
* Register all core infrastructure to hooks.
|
||||
* This should be called once when creating hooks for an agent.
|
||||
*/
|
||||
export function registerCoreInfrastructure(hooks: PromptConcatHooks): void {
|
||||
registerMessagePersistence(hooks);
|
||||
registerStreamingResponse(hooks);
|
||||
registerAgentStatus(hooks);
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* Message Persistence Infrastructure
|
||||
*
|
||||
* Handles persisting messages to the database.
|
||||
* This is core infrastructure, not a user-configurable plugin.
|
||||
*/
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import type { IAgentInstanceService } from '../../interface';
|
||||
import type { AIResponseContext, PromptConcatHooks, ToolExecutionContext, UserMessageContext } from '../../tools/types';
|
||||
import { createAgentMessage } from '../../utilities';
|
||||
|
||||
/**
|
||||
* Register message persistence handlers to hooks
|
||||
*/
|
||||
export function registerMessagePersistence(hooks: PromptConcatHooks): void {
|
||||
// Handle user message persistence
|
||||
hooks.userMessageReceived.tapAsync('messagePersistence', async (context: UserMessageContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, content, messageId } = context;
|
||||
|
||||
// Create user message using the helper function
|
||||
const userMessage = createAgentMessage(messageId, agentFrameworkContext.agent.id, {
|
||||
role: 'user',
|
||||
content: content.text,
|
||||
contentType: 'text/plain',
|
||||
metadata: content.file ? { file: content.file } : undefined,
|
||||
duration: undefined, // User messages persist indefinitely by default
|
||||
});
|
||||
|
||||
// Add message to the agent's message array for immediate use
|
||||
agentFrameworkContext.agent.messages.push(userMessage);
|
||||
|
||||
// Get the agent instance service to access repositories
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
// Save user message to database
|
||||
await agentInstanceService.saveUserMessage(userMessage);
|
||||
|
||||
logger.debug('User message persisted to database', {
|
||||
messageId,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
contentLength: content.text.length,
|
||||
});
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message persistence error in userMessageReceived', {
|
||||
error,
|
||||
messageId: context.messageId,
|
||||
agentId: context.agentFrameworkContext.agent.id,
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle AI response completion persistence
|
||||
hooks.responseComplete.tapAsync('messagePersistence', async (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response } = context;
|
||||
|
||||
if (response.status === 'done' && response.content) {
|
||||
// Find the AI message that needs to be persisted
|
||||
const aiMessage = agentFrameworkContext.agent.messages.find(
|
||||
(message) => message.role === 'assistant' && message.metadata?.isComplete && !message.metadata?.isPersisted,
|
||||
);
|
||||
|
||||
if (aiMessage) {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(aiMessage);
|
||||
aiMessage.metadata = { ...aiMessage.metadata, isPersisted: true };
|
||||
|
||||
logger.debug('AI response message persisted', {
|
||||
messageId: aiMessage.id,
|
||||
contentLength: response.content.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message persistence error in responseComplete', { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle tool result messages persistence
|
||||
hooks.toolExecuted.tapAsync('messagePersistence', async (context: ToolExecutionContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext } = context;
|
||||
|
||||
// Find newly added tool result messages that need to be persisted
|
||||
const newToolResultMessages = agentFrameworkContext.agent.messages.filter(
|
||||
(message) => message.metadata?.isToolResult && !message.metadata.isPersisted,
|
||||
);
|
||||
|
||||
if (newToolResultMessages.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
for (const message of newToolResultMessages) {
|
||||
try {
|
||||
await agentInstanceService.saveUserMessage(message);
|
||||
message.metadata = { ...message.metadata, isPersisted: true };
|
||||
|
||||
logger.debug('Tool result message persisted', {
|
||||
messageId: message.id,
|
||||
toolId: message.metadata.toolId,
|
||||
});
|
||||
} catch (serviceError) {
|
||||
logger.error('Failed to persist tool result message', {
|
||||
error: serviceError,
|
||||
messageId: message.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message persistence error in toolExecuted', { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* Streaming Response Infrastructure
|
||||
*
|
||||
* Handles streaming AI responses: creating/updating messages during streaming
|
||||
* and finalizing them when complete.
|
||||
* This is core infrastructure, not a user-configurable plugin.
|
||||
*/
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import type { IAgentInstanceService } from '../../interface';
|
||||
import type { AIResponseContext, PromptConcatHooks } from '../../tools/types';
|
||||
|
||||
/**
|
||||
* Register streaming response handlers to hooks
|
||||
*/
|
||||
export function registerStreamingResponse(hooks: PromptConcatHooks): void {
|
||||
// Handle AI response updates during streaming
|
||||
hooks.responseUpdate.tapAsync('streamingResponse', async (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response } = context;
|
||||
|
||||
if (response.status === 'update' && response.content) {
|
||||
// Find or create AI response message in agent's message array
|
||||
let aiMessage = agentFrameworkContext.agent.messages.find(
|
||||
(message) => message.role === 'assistant' && !message.metadata?.isComplete,
|
||||
);
|
||||
|
||||
if (!aiMessage) {
|
||||
// Create new AI message for streaming updates
|
||||
const now = new Date();
|
||||
aiMessage = {
|
||||
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
created: now,
|
||||
modified: now,
|
||||
metadata: { isComplete: false },
|
||||
duration: undefined,
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(aiMessage);
|
||||
|
||||
// Persist immediately so DB timestamp reflects conversation order
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(aiMessage);
|
||||
aiMessage.metadata = { ...aiMessage.metadata, isPersisted: true };
|
||||
} catch (persistError) {
|
||||
logger.warn('Failed to persist initial streaming AI message', {
|
||||
error: persistError,
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Update existing message content
|
||||
aiMessage.content = response.content;
|
||||
aiMessage.modified = new Date();
|
||||
}
|
||||
|
||||
// Update UI using the agent instance service
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
agentInstanceService.debounceUpdateMessage(aiMessage, agentFrameworkContext.agent.id);
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for streaming message', {
|
||||
error: serviceError,
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Streaming response error in responseUpdate', { error });
|
||||
} finally {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle AI response completion
|
||||
hooks.responseComplete.tapAsync('streamingResponse', async (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response } = context;
|
||||
|
||||
if (response.status === 'done' && response.content) {
|
||||
// Find and finalize AI response message
|
||||
let aiMessage = agentFrameworkContext.agent.messages.find(
|
||||
(message) => message.role === 'assistant' && !message.metadata?.isComplete && !message.metadata?.isToolResult,
|
||||
);
|
||||
|
||||
if (aiMessage) {
|
||||
// Mark as complete and update final content
|
||||
aiMessage.content = response.content;
|
||||
aiMessage.modified = new Date();
|
||||
aiMessage.metadata = { ...aiMessage.metadata, isComplete: true };
|
||||
} else {
|
||||
// Create final message if streaming message wasn't found
|
||||
const nowFinal = new Date();
|
||||
aiMessage = {
|
||||
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
created: nowFinal,
|
||||
modified: nowFinal,
|
||||
metadata: { isComplete: true },
|
||||
duration: undefined,
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(aiMessage);
|
||||
}
|
||||
|
||||
// Final UI update
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
agentInstanceService.debounceUpdateMessage(aiMessage, agentFrameworkContext.agent.id);
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for completed message', {
|
||||
error: serviceError,
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('AI response message completed', {
|
||||
messageId: aiMessage.id,
|
||||
finalContentLength: response.content.length,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Streaming response error in responseComplete', { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle tool result UI updates
|
||||
hooks.toolExecuted.tapAsync('streamingResponse-ui', async (context, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext } = context;
|
||||
|
||||
// Find tool result messages that need UI update
|
||||
const messagesNeedingUiUpdate = agentFrameworkContext.agent.messages.filter(
|
||||
(message) => message.metadata?.isToolResult && !message.metadata?.uiUpdated,
|
||||
);
|
||||
|
||||
if (messagesNeedingUiUpdate.length > 0) {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
for (const message of messagesNeedingUiUpdate) {
|
||||
agentInstanceService.debounceUpdateMessage(message, agentFrameworkContext.agent.id);
|
||||
message.metadata = { ...message.metadata, uiUpdated: true };
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Streaming response error in toolExecuted UI update', { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* Modifier Definition Framework
|
||||
*
|
||||
* Provides a declarative API for defining prompt modifiers with minimal boilerplate.
|
||||
* Modifiers only transform the prompt tree - they don't involve LLM tool calling.
|
||||
*
|
||||
* Unlike LLM tools, modifiers:
|
||||
* - Only work with processPrompts and postProcess hooks
|
||||
* - Don't inject tool descriptions or handle tool calls
|
||||
* - Are focused on prompt tree transformations
|
||||
*/
|
||||
import { logger } from '@services/libs/log';
|
||||
import type { z } from 'zod/v4';
|
||||
import type { AgentInstanceMessage } from '../../interface';
|
||||
import type { PostProcessContext, PromptConcatHookContext, PromptConcatHooks, PromptConcatTool } from '../../tools/types';
|
||||
import { findPromptById } from '../promptConcat';
|
||||
import type { IPrompt } from '../promptConcatSchema';
|
||||
|
||||
/**
|
||||
* Modifier definition configuration
|
||||
*/
|
||||
export interface ModifierDefinition<TConfigSchema extends z.ZodType = z.ZodType> {
|
||||
/** Unique modifier identifier */
|
||||
modifierId: string;
|
||||
|
||||
/** Display name for UI */
|
||||
displayName: string;
|
||||
|
||||
/** Description of what this modifier does */
|
||||
description: string;
|
||||
|
||||
/** Schema for modifier configuration parameters */
|
||||
configSchema: TConfigSchema;
|
||||
|
||||
/**
|
||||
* Called during prompt processing phase.
|
||||
* Use this to modify the prompt tree.
|
||||
*/
|
||||
onProcessPrompts?: (context: ModifierHandlerContext<TConfigSchema>) => Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Called during post-processing phase.
|
||||
* Use this to transform LLM responses.
|
||||
*/
|
||||
onPostProcess?: (context: PostProcessModifierContext<TConfigSchema>) => Promise<void> | void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to prompt processing handlers
|
||||
*/
|
||||
export interface ModifierHandlerContext<TConfigSchema extends z.ZodType> {
|
||||
/** The parsed configuration for this modifier instance */
|
||||
config: z.infer<TConfigSchema>;
|
||||
|
||||
/** Full modifier configuration object (includes extra fields like content, caption) */
|
||||
modifierConfig: PromptConcatHookContext['toolConfig'];
|
||||
|
||||
/** Current prompt tree (mutable) */
|
||||
prompts: IPrompt[];
|
||||
|
||||
/** Message history */
|
||||
messages: AgentInstanceMessage[];
|
||||
|
||||
/** Agent framework context */
|
||||
agentFrameworkContext: PromptConcatHookContext['agentFrameworkContext'];
|
||||
|
||||
/** Utility: Find a prompt by ID */
|
||||
findPrompt: (id: string) => ReturnType<typeof findPromptById>;
|
||||
|
||||
/** Utility: Insert content at a position */
|
||||
insertContent: (options: InsertContentOptions) => void;
|
||||
|
||||
/** Utility: Replace prompt content */
|
||||
replaceContent: (targetId: string, content: string | IPrompt[]) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to post-process handlers
|
||||
*/
|
||||
export interface PostProcessModifierContext<TConfigSchema extends z.ZodType> extends Omit<ModifierHandlerContext<TConfigSchema>, 'prompts'> {
|
||||
/** LLM response text */
|
||||
llmResponse: string;
|
||||
|
||||
/** Processed responses array (mutable) */
|
||||
responses: PostProcessContext['responses'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for inserting content
|
||||
*/
|
||||
export interface InsertContentOptions {
|
||||
/** Target prompt ID */
|
||||
targetId: string;
|
||||
|
||||
/** Position: 'before'/'after' as sibling, 'child' adds to children */
|
||||
position: 'before' | 'after' | 'child';
|
||||
|
||||
/** Content to insert (string or prompt object) */
|
||||
content: string | IPrompt;
|
||||
|
||||
/** Optional caption */
|
||||
caption?: string;
|
||||
|
||||
/** Optional ID for the new prompt */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a modifier from a definition.
|
||||
*/
|
||||
export function defineModifier<TConfigSchema extends z.ZodType>(
|
||||
definition: ModifierDefinition<TConfigSchema>,
|
||||
): {
|
||||
modifier: PromptConcatTool;
|
||||
modifierId: string;
|
||||
configSchema: TConfigSchema;
|
||||
displayName: string;
|
||||
description: string;
|
||||
} {
|
||||
const { modifierId, configSchema, onProcessPrompts, onPostProcess } = definition;
|
||||
|
||||
// The parameter key in config (e.g., 'fullReplacementParam' for 'fullReplacement')
|
||||
const parameterKey = `${modifierId}Param`;
|
||||
|
||||
const modifier: PromptConcatTool = (hooks: PromptConcatHooks) => {
|
||||
// Register processPrompts handler
|
||||
if (onProcessPrompts) {
|
||||
hooks.processPrompts.tapAsync(`${modifierId}-processPrompts`, async (context, callback) => {
|
||||
try {
|
||||
const { toolConfig, prompts, messages, agentFrameworkContext } = context;
|
||||
|
||||
// Skip if this config doesn't match our modifierId
|
||||
if (toolConfig.toolId !== modifierId) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the typed config
|
||||
const rawConfig = (toolConfig as Record<string, unknown>)[parameterKey];
|
||||
if (!rawConfig) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and validate config
|
||||
const config = configSchema.parse(rawConfig) as z.infer<TConfigSchema>;
|
||||
|
||||
// Build handler context with utilities
|
||||
const handlerContext: ModifierHandlerContext<TConfigSchema> = {
|
||||
config,
|
||||
modifierConfig: toolConfig,
|
||||
prompts,
|
||||
messages,
|
||||
agentFrameworkContext,
|
||||
|
||||
findPrompt: (id: string) => findPromptById(prompts, id),
|
||||
|
||||
insertContent: (options: InsertContentOptions) => {
|
||||
const target = findPromptById(prompts, options.targetId);
|
||||
if (!target) {
|
||||
logger.warn('Target prompt not found for content insertion', {
|
||||
targetId: options.targetId,
|
||||
modifierId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newPrompt: IPrompt = typeof options.content === 'string'
|
||||
? {
|
||||
id: options.id ?? `${modifierId}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
caption: options.caption ?? 'Inserted Content',
|
||||
text: options.content,
|
||||
}
|
||||
: options.content;
|
||||
|
||||
if (options.position === 'child') {
|
||||
if (!target.prompt.children) {
|
||||
target.prompt.children = [];
|
||||
}
|
||||
target.prompt.children.push(newPrompt);
|
||||
} else if (options.position === 'before') {
|
||||
target.parent.splice(target.index, 0, newPrompt);
|
||||
} else {
|
||||
target.parent.splice(target.index + 1, 0, newPrompt);
|
||||
}
|
||||
},
|
||||
|
||||
replaceContent: (targetId: string, content: string | IPrompt[]) => {
|
||||
const target = findPromptById(prompts, targetId);
|
||||
if (!target) {
|
||||
logger.warn('Target prompt not found for replacement', { targetId, modifierId });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
target.prompt.text = content;
|
||||
delete target.prompt.children;
|
||||
} else {
|
||||
delete target.prompt.text;
|
||||
target.prompt.children = content;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
await onProcessPrompts(handlerContext);
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error(`Error in ${modifierId} processPrompts handler`, { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register postProcess handler
|
||||
if (onPostProcess) {
|
||||
hooks.postProcess.tapAsync(`${modifierId}-postProcess`, async (context, callback) => {
|
||||
try {
|
||||
const { toolConfig, messages, agentFrameworkContext, llmResponse, responses } = context;
|
||||
|
||||
if (toolConfig.toolId !== modifierId) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const rawConfig = (toolConfig as Record<string, unknown>)[parameterKey];
|
||||
if (!rawConfig) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configSchema.parse(rawConfig) as z.infer<TConfigSchema>;
|
||||
|
||||
const handlerContext: PostProcessModifierContext<TConfigSchema> = {
|
||||
config,
|
||||
modifierConfig: toolConfig,
|
||||
messages,
|
||||
agentFrameworkContext,
|
||||
llmResponse,
|
||||
responses,
|
||||
|
||||
findPrompt: () => undefined, // Not available in postProcess
|
||||
|
||||
insertContent: () => {
|
||||
logger.warn('insertContent is not available in postProcess phase');
|
||||
},
|
||||
|
||||
replaceContent: () => {
|
||||
logger.warn('replaceContent is not available in postProcess phase');
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
await onPostProcess(handlerContext);
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error(`Error in ${modifierId} postProcess handler`, { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
modifier,
|
||||
modifierId,
|
||||
configSchema,
|
||||
displayName: definition.displayName,
|
||||
description: definition.description,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for modifiers
|
||||
*/
|
||||
const modifierRegistry = new Map<string, ReturnType<typeof defineModifier>>();
|
||||
|
||||
/**
|
||||
* Register a modifier definition
|
||||
*/
|
||||
export function registerModifier<TConfigSchema extends z.ZodType>(
|
||||
definition: ModifierDefinition<TConfigSchema>,
|
||||
): ReturnType<typeof defineModifier<TConfigSchema>> {
|
||||
const modifierDefinition = defineModifier(definition);
|
||||
modifierRegistry.set(modifierDefinition.modifierId, modifierDefinition as ReturnType<typeof defineModifier>);
|
||||
return modifierDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered modifiers
|
||||
*/
|
||||
export function getAllModifiers(): Map<string, ReturnType<typeof defineModifier>> {
|
||||
return modifierRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a modifier by ID
|
||||
*/
|
||||
export function getModifier(modifierId: string): ReturnType<typeof defineModifier> | undefined {
|
||||
return modifierRegistry.get(modifierId);
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Dynamic Position Modifier
|
||||
*
|
||||
* Inserts content at a specific position relative to a target element.
|
||||
*/
|
||||
import { logger } from '@services/libs/log';
|
||||
import { identity } from 'lodash';
|
||||
import { z } from 'zod/v4';
|
||||
import type { IPrompt } from '../promptConcatSchema';
|
||||
import { registerModifier } from './defineModifier';
|
||||
|
||||
const t = identity;
|
||||
|
||||
/**
|
||||
* Dynamic Position Parameter Schema
|
||||
*/
|
||||
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 type DynamicPositionParameter = z.infer<typeof DynamicPositionParameterSchema>;
|
||||
|
||||
export function getDynamicPositionParameterSchema() {
|
||||
return DynamicPositionParameterSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic Position Modifier Definition
|
||||
*/
|
||||
const dynamicPositionDefinition = registerModifier({
|
||||
modifierId: 'dynamicPosition',
|
||||
displayName: 'Dynamic Position',
|
||||
description: 'Insert content at a specific position relative to a target element',
|
||||
configSchema: DynamicPositionParameterSchema,
|
||||
|
||||
onProcessPrompts({ config, modifierConfig, findPrompt }) {
|
||||
// dynamicPosition requires content from modifierConfig
|
||||
if (!modifierConfig.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetId, position } = config;
|
||||
const found = findPrompt(targetId);
|
||||
|
||||
if (!found) {
|
||||
logger.warn('Target prompt not found for dynamicPosition', {
|
||||
targetId,
|
||||
modifierId: modifierConfig.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newPart: IPrompt = {
|
||||
id: `dynamic-${modifierConfig.id}-${Date.now()}`,
|
||||
caption: modifierConfig.caption ?? 'Dynamic Content',
|
||||
text: modifierConfig.content,
|
||||
};
|
||||
|
||||
switch (position) {
|
||||
case 'before':
|
||||
found.parent.splice(found.index, 0, newPart);
|
||||
break;
|
||||
case 'after':
|
||||
found.parent.splice(found.index + 1, 0, newPart);
|
||||
break;
|
||||
case 'relative':
|
||||
if (!found.prompt.children) {
|
||||
found.prompt.children = [];
|
||||
}
|
||||
found.prompt.children.push(newPart);
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown position: ${position as string}`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Dynamic position insertion completed', {
|
||||
targetId,
|
||||
position,
|
||||
contentLength: modifierConfig.content.length,
|
||||
modifierId: modifierConfig.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const dynamicPositionModifier = dynamicPositionDefinition.modifier;
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* Full Replacement Modifier
|
||||
*
|
||||
* Replaces target prompt content with content from specified source.
|
||||
* Supports: historyOfSession, llmResponse
|
||||
*/
|
||||
import { logger } from '@services/libs/log';
|
||||
import { cloneDeep, identity } from 'lodash';
|
||||
import { z } from 'zod/v4';
|
||||
import type { AgentResponse } from '../../tools/types';
|
||||
import { filterMessagesByDuration } from '../../utilities/messageDurationFilter';
|
||||
import { normalizeRole } from '../../utilities/normalizeRole';
|
||||
import type { IPrompt } from '../promptConcatSchema';
|
||||
import { registerModifier } from './defineModifier';
|
||||
|
||||
const t = identity;
|
||||
|
||||
/**
|
||||
* Full Replacement Parameter Schema
|
||||
*/
|
||||
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 type FullReplacementParameter = z.infer<typeof FullReplacementParameterSchema>;
|
||||
|
||||
export function getFullReplacementParameterSchema() {
|
||||
return FullReplacementParameterSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full Replacement Modifier Definition
|
||||
*/
|
||||
const fullReplacementDefinition = registerModifier({
|
||||
modifierId: 'fullReplacement',
|
||||
displayName: 'Full Replacement',
|
||||
description: 'Replace target content with content from specified source',
|
||||
configSchema: FullReplacementParameterSchema,
|
||||
|
||||
onProcessPrompts({ config, modifierConfig, findPrompt, messages }) {
|
||||
const { targetId, sourceType } = config;
|
||||
const found = findPrompt(targetId);
|
||||
|
||||
if (!found) {
|
||||
logger.warn('Target prompt not found for fullReplacement', {
|
||||
targetId,
|
||||
modifierId: modifierConfig.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle historyOfSession in processPrompts phase
|
||||
if (sourceType !== 'historyOfSession') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all messages except the last user message being processed
|
||||
const messagesCopy = cloneDeep(messages);
|
||||
|
||||
// Find and remove the last user message (which is being processed in this round)
|
||||
let lastUserMessageIndex = -1;
|
||||
for (let index = messagesCopy.length - 1; index >= 0; index--) {
|
||||
if (messagesCopy[index].role === 'user') {
|
||||
lastUserMessageIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUserMessageIndex >= 0) {
|
||||
messagesCopy.splice(lastUserMessageIndex, 1);
|
||||
logger.debug('Removed current user message from history', {
|
||||
removedMessageId: messages[lastUserMessageIndex].id,
|
||||
remainingMessages: messagesCopy.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Apply duration filtering to exclude expired messages
|
||||
const filteredHistory = filterMessagesByDuration(messagesCopy);
|
||||
|
||||
if (filteredHistory.length > 0) {
|
||||
found.prompt.children = [];
|
||||
filteredHistory.forEach((message, index: number) => {
|
||||
type PromptRole = NonNullable<IPrompt['role']>;
|
||||
const role: PromptRole = normalizeRole(message.role);
|
||||
delete found.prompt.text;
|
||||
found.prompt.children!.push({
|
||||
id: `history-${index}`,
|
||||
caption: `History message ${index + 1}`,
|
||||
role,
|
||||
text: message.content,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
found.prompt.text = '无聊天历史。';
|
||||
}
|
||||
|
||||
logger.debug('Full replacement completed in prompt phase', { targetId, sourceType });
|
||||
},
|
||||
|
||||
onPostProcess({ config, modifierConfig, llmResponse, responses }) {
|
||||
const { targetId, sourceType } = config;
|
||||
|
||||
if (sourceType !== 'llmResponse') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!responses) {
|
||||
logger.warn('No responses available in postProcess phase', {
|
||||
targetId,
|
||||
modifierId: modifierConfig.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const found = responses.find((r: AgentResponse) => r.id === targetId);
|
||||
|
||||
if (!found) {
|
||||
logger.warn('Full replacement target not found in responses', {
|
||||
targetId,
|
||||
modifierId: modifierConfig.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
found.text = llmResponse;
|
||||
|
||||
logger.debug('Full replacement completed in response phase', {
|
||||
targetId,
|
||||
sourceType,
|
||||
modifierId: modifierConfig.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const fullReplacementModifier = fullReplacementDefinition.modifier;
|
||||
34
src/services/agentInstance/promptConcat/modifiers/index.ts
Normal file
34
src/services/agentInstance/promptConcat/modifiers/index.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Prompt Modifiers
|
||||
*
|
||||
* Modifiers transform the prompt tree without involving LLM tool calling.
|
||||
* They work in the processPrompts and postProcess phases only.
|
||||
*/
|
||||
|
||||
// Re-export defineModifier API
|
||||
export { defineModifier, getAllModifiers, getModifier, registerModifier } from './defineModifier';
|
||||
export type { InsertContentOptions, ModifierDefinition, ModifierHandlerContext, PostProcessModifierContext } from './defineModifier';
|
||||
|
||||
// Export modifiers
|
||||
export { fullReplacementModifier, FullReplacementParameterSchema, getFullReplacementParameterSchema } from './fullReplacement';
|
||||
export type { FullReplacementParameter } from './fullReplacement';
|
||||
|
||||
export { dynamicPositionModifier, DynamicPositionParameterSchema, getDynamicPositionParameterSchema } from './dynamicPosition';
|
||||
export type { DynamicPositionParameter } from './dynamicPosition';
|
||||
|
||||
// Import for registration side effects
|
||||
import { getAllModifiers } from './defineModifier';
|
||||
import './fullReplacement';
|
||||
import './dynamicPosition';
|
||||
|
||||
/**
|
||||
* Get all registered modifier functions as a map
|
||||
*/
|
||||
export function getModifierFunctions(): Map<string, (hooks: import('../../tools/types').PromptConcatHooks) => void> {
|
||||
const modifiers = getAllModifiers();
|
||||
const result = new Map<string, (hooks: import('../../tools/types').PromptConcatHooks) => void>();
|
||||
for (const [id, definition] of modifiers) {
|
||||
result.set(id, definition.modifier);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -18,9 +18,9 @@ import { ModelMessage } from 'ai';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { AgentFrameworkContext } from '../agentFrameworks/utilities/type';
|
||||
import { AgentInstanceMessage } from '../interface';
|
||||
import { builtInTools, createAgentFrameworkHooks, PromptConcatHookContext } from '../tools';
|
||||
import { createAgentFrameworkHooks, pluginRegistry, PromptConcatHookContext } from '../tools';
|
||||
import type { AgentPromptDescription, IPrompt } from './promptConcatSchema';
|
||||
import type { IPromptConcatTool } from './promptConcatSchema/plugin';
|
||||
import type { IPromptConcatTool } from './promptConcatSchema/tools';
|
||||
|
||||
/**
|
||||
* Context type specific for prompt concatenation operations
|
||||
|
|
@ -216,7 +216,7 @@ export async function* promptConcatStream(
|
|||
const hooks = createAgentFrameworkHooks();
|
||||
// Register tools that match the configuration
|
||||
for (const tool of toolConfigs) {
|
||||
const builtInTool = builtInTools.get(tool.toolId);
|
||||
const builtInTool = pluginRegistry.get(tool.toolId);
|
||||
if (builtInTool) {
|
||||
builtInTool(hooks);
|
||||
logger.debug('Registered tool', {
|
||||
|
|
|
|||
|
|
@ -131,14 +131,9 @@ export type DefaultAgents = z.infer<ReturnType<typeof getDefaultAgentsSchema>>;
|
|||
export type AgentPromptDescription = z.infer<ReturnType<typeof getAgentConfigSchema>>;
|
||||
export type AiAPIConfig = z.infer<typeof AIConfigSchema>;
|
||||
export type AgentFrameworkConfig = z.infer<ReturnType<typeof getFrameworkConfigSchema>>;
|
||||
// Backward compat aliases - deprecated, use AgentFrameworkConfig directly
|
||||
export type HandlerConfig = AgentFrameworkConfig;
|
||||
|
||||
// Re-export all schemas and types
|
||||
export * from './modelParameters';
|
||||
export * from './plugin';
|
||||
export * from './prompts';
|
||||
export * from './response';
|
||||
|
||||
// Export IPromptConcatTool as IPromptConcatPlugin for backward compatibility
|
||||
export type { IPromptConcatTool as IPromptConcatPlugin } from './plugin';
|
||||
export * from './tools';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// Import parameter types from plugin files
|
||||
// Modifiers
|
||||
import type { DynamicPositionParameter, FullReplacementParameter } from '../modifiers';
|
||||
// LLM Tools
|
||||
import type { ModelContextProtocolParameter } from '@services/agentInstance/tools/modelContextProtocol';
|
||||
import type { DynamicPositionParameter, FullReplacementParameter } from '@services/agentInstance/tools/prompt';
|
||||
import type { WikiOperationParameter } from '@services/agentInstance/tools/wikiOperation';
|
||||
import type { WikiSearchParameter } from '@services/agentInstance/tools/wikiSearch';
|
||||
import type { WorkspacesListParameter } from '@services/agentInstance/tools/workspacesList';
|
||||
|
||||
/**
|
||||
* Type definition for prompt concat tool
|
||||
* Type definition for prompt concat plugin (both modifiers and LLM tools)
|
||||
* This includes all possible parameter fields for type safety
|
||||
*/
|
||||
export type IPromptConcatTool = {
|
||||
|
|
@ -16,9 +18,11 @@ export type IPromptConcatTool = {
|
|||
forbidOverrides?: boolean;
|
||||
toolId: string;
|
||||
|
||||
// Tool-specific parameters
|
||||
// Modifier parameters
|
||||
fullReplacementParam?: FullReplacementParameter;
|
||||
dynamicPositionParam?: DynamicPositionParameter;
|
||||
|
||||
// LLM Tool parameters
|
||||
wikiOperationParam?: WikiOperationParameter;
|
||||
wikiSearchParam?: WikiSearchParameter;
|
||||
workspacesListParam?: WorkspacesListParameter;
|
||||
|
|
@ -8,7 +8,7 @@ import { logger } from '@services/libs/log';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { AgentFrameworkContext } from '../agentFrameworks/utilities/type';
|
||||
import { AgentInstanceMessage } from '../interface';
|
||||
import { builtInTools, createAgentFrameworkHooks } from '../tools';
|
||||
import { createAgentFrameworkHooks, pluginRegistry } from '../tools';
|
||||
import { AgentResponse, PostProcessContext, YieldNextRoundTarget } from '../tools/types';
|
||||
import type { IPromptConcatTool } from './promptConcatSchema';
|
||||
import { AgentFrameworkConfig, AgentPromptDescription } from './promptConcatSchema';
|
||||
|
|
@ -47,7 +47,7 @@ export async function responseConcat(
|
|||
const hooks = createAgentFrameworkHooks();
|
||||
// Register all tools from configuration
|
||||
for (const tool of toolConfigs) {
|
||||
const builtInTool = builtInTools.get(tool.toolId);
|
||||
const builtInTool = pluginRegistry.get(tool.toolId);
|
||||
if (builtInTool) {
|
||||
builtInTool(hooks);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import type { IPrompt } from '../../promptConcat/promptConcatSchema/prompts';
|
|||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import defaultAgents from '../../agentFrameworks/taskAgents.json';
|
||||
import { fullReplacementModifier } from '../../promptConcat/modifiers/fullReplacement';
|
||||
import { createAgentFrameworkHooks, PromptConcatHookContext } from '../index';
|
||||
import { fullReplacementTool } from '../prompt';
|
||||
|
||||
// Use the real agent config
|
||||
const exampleAgent = defaultAgents[0];
|
||||
|
|
@ -113,7 +113,7 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
|
|||
};
|
||||
|
||||
const hooks = createAgentFrameworkHooks();
|
||||
fullReplacementTool(hooks);
|
||||
fullReplacementModifier(hooks);
|
||||
|
||||
// Execute the processPrompts hook
|
||||
await hooks.processPrompts.promise(context);
|
||||
|
|
@ -126,8 +126,8 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
|
|||
const targetPrompt = historyPrompt!.children?.find(child => child.id === targetId);
|
||||
expect(targetPrompt).toBeDefined();
|
||||
|
||||
// The fullReplacementTool puts filtered messages in children array
|
||||
// Note: fullReplacementTool removes the last message (current user message)
|
||||
// The fullReplacementModifier puts filtered messages in children array
|
||||
// Note: fullReplacementModifier removes the last message (current user message)
|
||||
const children = (targetPrompt as unknown as { children?: IPrompt[] }).children || [];
|
||||
expect(children.length).toBe(2); // Only non-expired messages (user1, ai-response), excluding last user message
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
|
|||
};
|
||||
|
||||
const hooks = createAgentFrameworkHooks();
|
||||
fullReplacementTool(hooks);
|
||||
fullReplacementModifier(hooks);
|
||||
|
||||
await hooks.processPrompts.promise(context);
|
||||
|
||||
|
|
@ -283,7 +283,7 @@ describe('Full Replacement Plugin - Duration Mechanism', () => {
|
|||
};
|
||||
|
||||
const hooks = createAgentFrameworkHooks();
|
||||
fullReplacementTool(hooks);
|
||||
fullReplacementModifier(hooks);
|
||||
|
||||
await hooks.processPrompts.promise(context);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { DataSource } from 'typeorm';
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import defaultAgents from '../../agentFrameworks/taskAgents.json';
|
||||
import type { AgentInstanceMessage, IAgentInstanceService } from '../../interface';
|
||||
import { registerCoreInfrastructure } from '../../promptConcat/infrastructure';
|
||||
import { createAgentFrameworkHooks } from '../index';
|
||||
import { messageManagementTool } from '../messageManagement';
|
||||
import type { ToolExecutionContext, UserMessageContext } from '../types';
|
||||
|
||||
// Use the real agent config from taskAgents.json
|
||||
|
|
@ -70,7 +70,7 @@ describe('Message Management Plugin - Real Database Integration', () => {
|
|||
|
||||
// Initialize plugin
|
||||
hooks = createAgentFrameworkHooks();
|
||||
messageManagementTool(hooks);
|
||||
registerCoreInfrastructure(hooks);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import type { IPrompt } from '@services/agentInstance/promptConcat/promptConcatS
|
|||
import type { IPromptConcatTool } from '@services/agentInstance/promptConcat/promptConcatSchema';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import defaultAgents from '../../agentFrameworks/taskAgents.json';
|
||||
import { registerCoreInfrastructure } from '../../promptConcat/infrastructure';
|
||||
import { createAgentFrameworkHooks, PromptConcatHookContext } from '../index';
|
||||
import { messageManagementTool } from '../messageManagement';
|
||||
import { wikiSearchTool } from '../wikiSearch';
|
||||
|
||||
// Mock i18n
|
||||
|
|
@ -321,7 +321,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
|
|||
expect(toolResultMessage.content).toContain('Tool: wiki-search');
|
||||
expect(toolResultMessage.content).toContain('Important Note 1');
|
||||
expect(toolResultMessage.metadata?.isToolResult).toBe(true);
|
||||
expect(toolResultMessage.metadata?.isPersisted).toBe(false); // Should be false initially
|
||||
// Note: isPersisted may be true due to immediate async persistence in new defineTool API
|
||||
expect(toolResultMessage.duration).toBe(1); // Tool result uses configurable toolResultDuration (default 1)
|
||||
|
||||
// Check that previous user message is unchanged
|
||||
|
|
@ -406,7 +406,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
|
|||
expect(errorResultMessage.content).toContain('工作空间名称或ID');
|
||||
expect(errorResultMessage.metadata?.isToolResult).toBe(true);
|
||||
expect(errorResultMessage.metadata?.isError).toBe(true);
|
||||
expect(errorResultMessage.metadata?.isPersisted).toBe(false); // Should be false initially
|
||||
// Note: isPersisted may be true due to immediate async persistence in new defineTool API
|
||||
expect(errorResultMessage.duration).toBe(1); // Now uses configurable toolResultDuration (default 1)
|
||||
});
|
||||
|
||||
|
|
@ -891,7 +891,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
|
|||
|
||||
const hooks = createAgentFrameworkHooks();
|
||||
wikiSearchTool(hooks);
|
||||
messageManagementTool(hooks);
|
||||
registerCoreInfrastructure(hooks);
|
||||
|
||||
await hooks.responseComplete.promise(context);
|
||||
|
||||
|
|
@ -901,7 +901,7 @@ describe('Wiki Search Plugin - Comprehensive Tests', () => {
|
|||
|
||||
const toolResultMessage = agentFrameworkContext.agent.messages[1] as AgentInstanceMessage;
|
||||
expect(toolResultMessage.metadata?.isToolResult).toBe(true);
|
||||
expect(toolResultMessage.metadata?.isPersisted).toBe(true); // Should be true after messageManagementTool processing
|
||||
expect(toolResultMessage.metadata?.isPersisted).toBe(true); // Should be true after infrastructure processing
|
||||
});
|
||||
|
||||
it('should prevent regression: tool result not filtered in second round', async () => {
|
||||
|
|
|
|||
677
src/services/agentInstance/tools/defineTool.ts
Normal file
677
src/services/agentInstance/tools/defineTool.ts
Normal file
|
|
@ -0,0 +1,677 @@
|
|||
/* eslint-disable @typescript-eslint/no-unnecessary-type-conversion */
|
||||
/**
|
||||
* Tool Definition Framework
|
||||
*
|
||||
* Provides a declarative API for defining LLM agent tools with minimal boilerplate.
|
||||
* Tools are defined using a configuration object that specifies:
|
||||
* - Schema for configuration parameters (shown in UI for users to configure)
|
||||
* - Schema for LLM-callable tool parameters (injected into prompts)
|
||||
* - Hook handlers for different lifecycle events
|
||||
*
|
||||
* This replaces the verbose tapAsync pattern with a cleaner functional approach.
|
||||
*/
|
||||
import { type ToolCallingMatch } from '@services/agentDefinition/interface';
|
||||
import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility';
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import type { z } from 'zod/v4';
|
||||
import type { AgentInstanceMessage, IAgentInstanceService } from '../interface';
|
||||
import { findPromptById } from '../promptConcat/promptConcat';
|
||||
import type { IPrompt } from '../promptConcat/promptConcatSchema';
|
||||
import { schemaToToolContent } from '../utilities/schemaToToolContent';
|
||||
import type { AIResponseContext, PostProcessContext, PromptConcatHookContext, PromptConcatHooks, PromptConcatTool } from './types';
|
||||
|
||||
/**
|
||||
* Tool definition configuration
|
||||
*/
|
||||
export interface ToolDefinition<
|
||||
TConfigSchema extends z.ZodType = z.ZodType,
|
||||
TLLMToolSchemas extends Record<string, z.ZodType> = Record<string, z.ZodType>,
|
||||
> {
|
||||
/** Unique tool identifier - must match the toolId used in agent configuration */
|
||||
toolId: string;
|
||||
|
||||
/** Display name for UI */
|
||||
displayName: string;
|
||||
|
||||
/** Description of what this tool does */
|
||||
description: string;
|
||||
|
||||
/** Schema for tool configuration parameters (user-configurable in UI) */
|
||||
configSchema: TConfigSchema;
|
||||
|
||||
/**
|
||||
* Optional schemas for LLM-callable tools.
|
||||
* Each key is the tool name (e.g., 'wiki-search'), value is the parameter schema.
|
||||
* The schema's title meta will be used as the tool name in prompts.
|
||||
*/
|
||||
llmToolSchemas?: TLLMToolSchemas;
|
||||
|
||||
/**
|
||||
* Called during prompt processing phase.
|
||||
* Use this to inject tool descriptions, modify prompts, etc.
|
||||
*/
|
||||
onProcessPrompts?: (context: ToolHandlerContext<TConfigSchema>) => Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Called after LLM generates a response.
|
||||
* Use this to parse tool calls, execute tools, etc.
|
||||
*/
|
||||
onResponseComplete?: (context: ResponseHandlerContext<TConfigSchema, TLLMToolSchemas>) => Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Called during post-processing phase.
|
||||
* Use this to transform LLM responses, etc.
|
||||
*/
|
||||
onPostProcess?: (context: PostProcessHandlerContext<TConfigSchema>) => Promise<void> | void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to prompt processing handlers
|
||||
*/
|
||||
export interface ToolHandlerContext<TConfigSchema extends z.ZodType> {
|
||||
/** The parsed configuration for this tool instance */
|
||||
config: z.infer<TConfigSchema>;
|
||||
|
||||
/** Full tool configuration object */
|
||||
toolConfig: PromptConcatHookContext['toolConfig'];
|
||||
|
||||
/** Current prompt tree (mutable) */
|
||||
prompts: IPrompt[];
|
||||
|
||||
/** Message history */
|
||||
messages: AgentInstanceMessage[];
|
||||
|
||||
/** Agent framework context */
|
||||
agentFrameworkContext: PromptConcatHookContext['agentFrameworkContext'];
|
||||
|
||||
/** Utility: Find a prompt by ID */
|
||||
findPrompt: (id: string) => ReturnType<typeof findPromptById>;
|
||||
|
||||
/** Utility: Inject a tool list at a target position */
|
||||
injectToolList: (options: InjectToolListOptions) => void;
|
||||
|
||||
/** Utility: Inject content at a target position */
|
||||
injectContent: (options: InjectContentOptions) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to response handlers
|
||||
*/
|
||||
export interface ResponseHandlerContext<
|
||||
TConfigSchema extends z.ZodType,
|
||||
TLLMToolSchemas extends Record<string, z.ZodType>,
|
||||
> extends Omit<ToolHandlerContext<TConfigSchema>, 'prompts' | 'config'> {
|
||||
/** The parsed configuration for this tool instance (may be undefined if no config provided) */
|
||||
config: z.infer<TConfigSchema> | undefined;
|
||||
|
||||
/** AI response content */
|
||||
response: AIResponseContext['response'];
|
||||
|
||||
/** Parsed tool call from response (if any) */
|
||||
toolCall: ToolCallingMatch | null;
|
||||
|
||||
/** Full agent framework config for accessing other tool configs */
|
||||
agentFrameworkConfig: AIResponseContext['agentFrameworkConfig'];
|
||||
|
||||
/** Utility: Execute a tool call and handle the result */
|
||||
executeToolCall: <TToolName extends keyof TLLMToolSchemas>(
|
||||
toolName: TToolName,
|
||||
executor: (parameters: z.infer<TLLMToolSchemas[TToolName]>) => Promise<ToolExecutionResult>,
|
||||
) => Promise<boolean>;
|
||||
|
||||
/** Utility: Add a tool result message */
|
||||
addToolResult: (options: AddToolResultOptions) => void;
|
||||
|
||||
/** Utility: Signal that the agent should continue with another round */
|
||||
yieldToSelf: () => void;
|
||||
|
||||
/** Raw hooks for advanced usage */
|
||||
hooks: PromptConcatHooks;
|
||||
|
||||
/** Request ID for tracking */
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to post-process handlers
|
||||
*/
|
||||
export interface PostProcessHandlerContext<TConfigSchema extends z.ZodType> extends Omit<ToolHandlerContext<TConfigSchema>, never> {
|
||||
/** LLM response text */
|
||||
llmResponse: string;
|
||||
|
||||
/** Processed responses array (mutable) */
|
||||
responses: PostProcessContext['responses'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for injecting tool list into prompts
|
||||
*/
|
||||
export interface InjectToolListOptions {
|
||||
/** Target prompt ID to inject relative to */
|
||||
targetId: string;
|
||||
|
||||
/** Position relative to target: 'before'/'after' inserts as sibling, 'child' adds to children */
|
||||
position: 'before' | 'after' | 'child';
|
||||
|
||||
/** Tool schemas to inject (will use all llmToolSchemas if not specified) */
|
||||
toolSchemas?: z.ZodType[];
|
||||
|
||||
/** Optional caption for the injected prompt */
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for injecting content into prompts
|
||||
*/
|
||||
export interface InjectContentOptions {
|
||||
/** Target prompt ID to inject relative to */
|
||||
targetId: string;
|
||||
|
||||
/** Position relative to target */
|
||||
position: 'before' | 'after' | 'child';
|
||||
|
||||
/** Content to inject */
|
||||
content: string;
|
||||
|
||||
/** Caption for the injected prompt */
|
||||
caption?: string;
|
||||
|
||||
/** Optional ID for the injected prompt */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for adding tool result messages
|
||||
*/
|
||||
export interface AddToolResultOptions {
|
||||
/** Tool name */
|
||||
toolName: string;
|
||||
|
||||
/** Tool parameters */
|
||||
parameters: unknown;
|
||||
|
||||
/** Result content */
|
||||
result: string;
|
||||
|
||||
/** Whether this is an error result */
|
||||
isError?: boolean;
|
||||
|
||||
/** How many rounds this result should be visible */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from tool execution
|
||||
*/
|
||||
export interface ToolExecutionResult {
|
||||
success: boolean;
|
||||
data?: string;
|
||||
error?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tool from a definition.
|
||||
* Returns both the tool function and metadata for registration.
|
||||
*/
|
||||
export function defineTool<
|
||||
TConfigSchema extends z.ZodType,
|
||||
TLLMToolSchemas extends Record<string, z.ZodType> = Record<string, z.ZodType>,
|
||||
>(definition: ToolDefinition<TConfigSchema, TLLMToolSchemas>): {
|
||||
tool: PromptConcatTool;
|
||||
toolId: string;
|
||||
configSchema: TConfigSchema;
|
||||
llmToolSchemas: TLLMToolSchemas | undefined;
|
||||
displayName: string;
|
||||
description: string;
|
||||
} {
|
||||
const { toolId, configSchema, llmToolSchemas, onProcessPrompts, onResponseComplete, onPostProcess } = definition;
|
||||
|
||||
// The parameter key in toolConfig (e.g., 'wikiSearchParam' for 'wikiSearch')
|
||||
const parameterKey = `${toolId}Param`;
|
||||
|
||||
const tool: PromptConcatTool = (hooks) => {
|
||||
// Register processPrompts handler
|
||||
if (onProcessPrompts) {
|
||||
hooks.processPrompts.tapAsync(`${toolId}-processPrompts`, async (context, callback) => {
|
||||
try {
|
||||
const { toolConfig, prompts, messages, agentFrameworkContext } = context;
|
||||
|
||||
// Skip if this tool config doesn't match our toolId
|
||||
if (toolConfig.toolId !== toolId) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the typed config
|
||||
const rawConfig = (toolConfig as Record<string, unknown>)[parameterKey];
|
||||
if (!rawConfig) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and validate config
|
||||
const config = configSchema.parse(rawConfig) as z.infer<TConfigSchema>;
|
||||
|
||||
// Build handler context with utilities
|
||||
const handlerContext: ToolHandlerContext<TConfigSchema> = {
|
||||
config,
|
||||
toolConfig,
|
||||
prompts,
|
||||
messages,
|
||||
agentFrameworkContext,
|
||||
|
||||
findPrompt: (id: string) => findPromptById(prompts, id),
|
||||
|
||||
injectToolList: (options: InjectToolListOptions) => {
|
||||
const target = findPromptById(prompts, options.targetId);
|
||||
if (!target) {
|
||||
logger.warn(`Target prompt not found for tool list injection`, {
|
||||
targetId: options.targetId,
|
||||
toolId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate tool content from schemas
|
||||
const schemas = options.toolSchemas ?? (llmToolSchemas ? Object.values(llmToolSchemas) : []);
|
||||
const toolContent = schemas.map((schema) => schemaToToolContent(schema)).join('\n\n');
|
||||
|
||||
const toolPrompt: IPrompt = {
|
||||
id: `${toolId}-tool-list-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
text: toolContent,
|
||||
caption: options.caption ?? `${definition.displayName} Tools`,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
if (options.position === 'child') {
|
||||
// Add to target's children
|
||||
if (!target.prompt.children) {
|
||||
target.prompt.children = [];
|
||||
}
|
||||
target.prompt.children.push(toolPrompt);
|
||||
} else if (options.position === 'before') {
|
||||
target.parent.splice(target.index, 0, toolPrompt);
|
||||
} else {
|
||||
target.parent.splice(target.index + 1, 0, toolPrompt);
|
||||
}
|
||||
|
||||
logger.debug(`Tool list injected`, {
|
||||
targetId: options.targetId,
|
||||
position: options.position,
|
||||
toolId,
|
||||
});
|
||||
},
|
||||
|
||||
injectContent: (options: InjectContentOptions) => {
|
||||
const target = findPromptById(prompts, options.targetId);
|
||||
if (!target) {
|
||||
logger.warn(`Target prompt not found for content injection`, {
|
||||
targetId: options.targetId,
|
||||
toolId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const contentPrompt: IPrompt = {
|
||||
id: options.id ?? `${toolId}-content-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
text: options.content,
|
||||
caption: options.caption ?? 'Injected Content',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
if (options.position === 'child') {
|
||||
if (!target.prompt.children) {
|
||||
target.prompt.children = [];
|
||||
}
|
||||
target.prompt.children.push(contentPrompt);
|
||||
} else if (options.position === 'before') {
|
||||
target.parent.splice(target.index, 0, contentPrompt);
|
||||
} else {
|
||||
target.parent.splice(target.index + 1, 0, contentPrompt);
|
||||
}
|
||||
|
||||
logger.debug(`Content injected`, {
|
||||
targetId: options.targetId,
|
||||
position: options.position,
|
||||
toolId,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
await onProcessPrompts(handlerContext);
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error(`Error in ${toolId} processPrompts handler`, { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register responseComplete handler
|
||||
if (onResponseComplete) {
|
||||
hooks.responseComplete.tapAsync(`${toolId}-responseComplete`, async (context, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response, agentFrameworkConfig, requestId, toolConfig: directToolConfig } = context as AIResponseContext & {
|
||||
toolConfig?: PromptConcatHookContext['toolConfig'];
|
||||
};
|
||||
|
||||
// Find our tool's config - first try agentFrameworkConfig.plugins, then fall back to direct toolConfig
|
||||
let ourToolConfig = agentFrameworkConfig?.plugins?.find(
|
||||
(p: { toolId: string }) => p.toolId === toolId,
|
||||
);
|
||||
|
||||
// Fall back to direct toolConfig if provided (for backward compatibility with tests)
|
||||
if (!ourToolConfig && directToolConfig?.toolId === toolId) {
|
||||
ourToolConfig = directToolConfig;
|
||||
}
|
||||
|
||||
// Skip if this tool is not configured for this agent
|
||||
if (!ourToolConfig) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if response is not complete
|
||||
if (response.status !== 'done' || !response.content) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse tool call from response
|
||||
const toolMatch = matchToolCalling(response.content);
|
||||
const toolCall = toolMatch.found ? toolMatch : null;
|
||||
|
||||
// Try to parse config (may be empty for tools that only handle LLM tool calls)
|
||||
const rawConfig = (ourToolConfig as Record<string, unknown>)[parameterKey];
|
||||
let config: z.infer<TConfigSchema> | undefined;
|
||||
if (rawConfig) {
|
||||
try {
|
||||
config = configSchema.parse(rawConfig) as z.infer<TConfigSchema>;
|
||||
} catch (parseError) {
|
||||
logger.warn(`Failed to parse config for ${toolId}`, { parseError });
|
||||
}
|
||||
}
|
||||
|
||||
// Build handler context
|
||||
const handlerContext: ResponseHandlerContext<TConfigSchema, TLLMToolSchemas> = {
|
||||
config,
|
||||
toolConfig: ourToolConfig as PromptConcatHookContext['toolConfig'],
|
||||
messages: agentFrameworkContext.agent.messages,
|
||||
agentFrameworkContext,
|
||||
response,
|
||||
toolCall,
|
||||
agentFrameworkConfig,
|
||||
hooks,
|
||||
requestId,
|
||||
|
||||
findPrompt: () => undefined, // Not available in response phase
|
||||
|
||||
injectToolList: () => {
|
||||
logger.warn('injectToolList is not available in response phase');
|
||||
},
|
||||
|
||||
injectContent: () => {
|
||||
logger.warn('injectContent is not available in response phase');
|
||||
},
|
||||
|
||||
executeToolCall: async <TToolName extends keyof TLLMToolSchemas>(
|
||||
toolName: TToolName,
|
||||
executor: (parameters: z.infer<TLLMToolSchemas[TToolName]>) => Promise<ToolExecutionResult>,
|
||||
): Promise<boolean> => {
|
||||
if (!toolCall || toolCall.toolId !== toolName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const toolSchema = llmToolSchemas?.[toolName];
|
||||
if (!toolSchema) {
|
||||
logger.error(`No schema found for tool: ${String(toolName)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate parameters
|
||||
const validatedParameters = toolSchema.parse(toolCall.parameters);
|
||||
|
||||
// Execute the tool
|
||||
const result = await executor(validatedParameters);
|
||||
|
||||
// Add result message
|
||||
const toolResultDuration = (config as { toolResultDuration?: number } | undefined)?.toolResultDuration ?? 1;
|
||||
handlerContext.addToolResult({
|
||||
toolName: toolName,
|
||||
parameters: validatedParameters,
|
||||
result: result.success ? (result.data ?? 'Success') : (result.error ?? 'Unknown error'),
|
||||
isError: !result.success,
|
||||
duration: toolResultDuration,
|
||||
});
|
||||
|
||||
// Set up next round
|
||||
handlerContext.yieldToSelf();
|
||||
|
||||
// Signal tool execution to other plugins
|
||||
await hooks.toolExecuted.promise({
|
||||
agentFrameworkContext,
|
||||
toolResult: result,
|
||||
toolInfo: {
|
||||
toolId: String(toolName),
|
||||
parameters: validatedParameters as Record<string, unknown>,
|
||||
originalText: toolCall.originalText,
|
||||
},
|
||||
requestId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`Tool execution failed: ${String(toolName)}`, { error });
|
||||
|
||||
// Add error result
|
||||
handlerContext.addToolResult({
|
||||
toolName: toolName,
|
||||
parameters: toolCall.parameters,
|
||||
result: error instanceof Error ? error.message : String(error),
|
||||
isError: true,
|
||||
duration: 2,
|
||||
});
|
||||
|
||||
handlerContext.yieldToSelf();
|
||||
|
||||
await hooks.toolExecuted.promise({
|
||||
agentFrameworkContext,
|
||||
toolResult: {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
toolInfo: {
|
||||
toolId: toolName,
|
||||
parameters: toolCall.parameters || {},
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
addToolResult: (options: AddToolResultOptions) => {
|
||||
const now = new Date();
|
||||
const toolResultText = `<functions_result>
|
||||
Tool: ${options.toolName}
|
||||
Parameters: ${JSON.stringify(options.parameters)}
|
||||
${options.isError ? 'Error' : 'Result'}: ${options.result}
|
||||
</functions_result>`;
|
||||
|
||||
const toolResultMessage: AgentInstanceMessage = {
|
||||
id: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'tool',
|
||||
content: toolResultText,
|
||||
created: now,
|
||||
modified: now,
|
||||
duration: options.duration ?? 1,
|
||||
metadata: {
|
||||
isToolResult: true,
|
||||
isError: options.isError ?? false,
|
||||
toolId: options.toolName,
|
||||
toolParameters: options.parameters,
|
||||
isPersisted: false,
|
||||
isComplete: true,
|
||||
},
|
||||
};
|
||||
|
||||
agentFrameworkContext.agent.messages.push(toolResultMessage);
|
||||
|
||||
// Persist immediately
|
||||
void (async () => {
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(toolResultMessage);
|
||||
toolResultMessage.metadata = { ...toolResultMessage.metadata, isPersisted: true };
|
||||
} catch (error) {
|
||||
logger.warn('Failed to persist tool result', { error, messageId: toolResultMessage.id });
|
||||
}
|
||||
})();
|
||||
|
||||
logger.debug('Tool result added', {
|
||||
toolName: options.toolName,
|
||||
isError: options.isError,
|
||||
messageId: toolResultMessage.id,
|
||||
});
|
||||
},
|
||||
|
||||
yieldToSelf: () => {
|
||||
if (!context.actions) {
|
||||
context.actions = {};
|
||||
}
|
||||
context.actions.yieldNextRoundTo = 'self';
|
||||
|
||||
// Also set duration on the AI message containing the tool call and update UI immediately
|
||||
const aiMessages = agentFrameworkContext.agent.messages.filter((m) => m.role === 'assistant');
|
||||
if (aiMessages.length > 0) {
|
||||
const latestAiMessage = aiMessages[aiMessages.length - 1];
|
||||
// Only update if this message matches the current response (contains the tool call)
|
||||
if (latestAiMessage.content === response.content) {
|
||||
latestAiMessage.duration = 1;
|
||||
latestAiMessage.metadata = {
|
||||
...latestAiMessage.metadata,
|
||||
containsToolCall: true,
|
||||
toolId: toolCall?.toolId,
|
||||
};
|
||||
|
||||
// Persist and update UI immediately (no debounce delay)
|
||||
void (async () => {
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
if (!latestAiMessage.created) latestAiMessage.created = new Date();
|
||||
await agentInstanceService.saveUserMessage(latestAiMessage);
|
||||
latestAiMessage.metadata = { ...latestAiMessage.metadata, isPersisted: true };
|
||||
// Update UI with no delay
|
||||
agentInstanceService.debounceUpdateMessage(latestAiMessage, agentFrameworkContext.agent.id, 0);
|
||||
} catch (error) {
|
||||
logger.warn('Failed to persist AI message with tool call', { error, messageId: latestAiMessage.id });
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
await onResponseComplete(handlerContext);
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error(`Error in ${toolId} responseComplete handler`, { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register postProcess handler
|
||||
if (onPostProcess) {
|
||||
hooks.postProcess.tapAsync(`${toolId}-postProcess`, async (context, callback) => {
|
||||
try {
|
||||
const { toolConfig, prompts, messages, agentFrameworkContext, llmResponse, responses } = context;
|
||||
|
||||
if (toolConfig.toolId !== toolId) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const rawConfig = (toolConfig as Record<string, unknown>)[parameterKey];
|
||||
if (!rawConfig) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configSchema.parse(rawConfig) as z.infer<TConfigSchema>;
|
||||
|
||||
const handlerContext: PostProcessHandlerContext<TConfigSchema> = {
|
||||
config,
|
||||
toolConfig,
|
||||
prompts,
|
||||
messages,
|
||||
agentFrameworkContext,
|
||||
llmResponse,
|
||||
responses,
|
||||
|
||||
findPrompt: (id: string) => findPromptById(prompts, id),
|
||||
|
||||
injectToolList: () => {
|
||||
logger.warn('injectToolList is not recommended in postProcess phase');
|
||||
},
|
||||
|
||||
injectContent: () => {
|
||||
logger.warn('injectContent is not recommended in postProcess phase');
|
||||
},
|
||||
};
|
||||
|
||||
await onPostProcess(handlerContext);
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error(`Error in ${toolId} postProcess handler`, { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
tool,
|
||||
toolId,
|
||||
configSchema,
|
||||
llmToolSchemas,
|
||||
displayName: definition.displayName,
|
||||
description: definition.description,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for tools created with defineTool
|
||||
*/
|
||||
const toolRegistry = new Map<string, ReturnType<typeof defineTool>>();
|
||||
|
||||
/**
|
||||
* Register a tool definition
|
||||
*/
|
||||
export function registerToolDefinition<
|
||||
TConfigSchema extends z.ZodType,
|
||||
TLLMToolSchemas extends Record<string, z.ZodType>,
|
||||
>(definition: ToolDefinition<TConfigSchema, TLLMToolSchemas>): ReturnType<typeof defineTool<TConfigSchema, TLLMToolSchemas>> {
|
||||
const toolDefinition = defineTool(definition);
|
||||
toolRegistry.set(toolDefinition.toolId, toolDefinition as ReturnType<typeof defineTool>);
|
||||
return toolDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tool definitions
|
||||
*/
|
||||
export function getAllToolDefinitions(): Map<string, ReturnType<typeof defineTool>> {
|
||||
return toolRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tool definition by ID
|
||||
*/
|
||||
export function getToolDefinition(toolId: string): ReturnType<typeof defineTool> | undefined {
|
||||
return toolRegistry.get(toolId);
|
||||
}
|
||||
|
|
@ -1,20 +1,41 @@
|
|||
/**
|
||||
* Agent Framework Plugin System
|
||||
*
|
||||
* This module provides a unified registration and hook system for:
|
||||
* 1. Modifiers - Transform the prompt tree (fullReplacement, dynamicPosition)
|
||||
* 2. LLM Tools - Inject tool descriptions and handle AI tool calls (wikiSearch, wikiOperation, etc.)
|
||||
* 3. Core Infrastructure - Message persistence, streaming, status (always enabled)
|
||||
*
|
||||
* All plugins are configured via the `plugins` array in agentFrameworkConfig.
|
||||
* Each plugin has a `toolId` that identifies it and a corresponding `xxxParam` object for configuration.
|
||||
*/
|
||||
import { logger } from '@services/libs/log';
|
||||
import { AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable';
|
||||
|
||||
import { registerCoreInfrastructure } from '../promptConcat/infrastructure';
|
||||
import { getAllModifiers } from '../promptConcat/modifiers';
|
||||
import { getAllToolDefinitions } from './defineTool';
|
||||
import { registerToolParameterSchema } from './schemaRegistry';
|
||||
import { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatTool, ResponseHookContext } from './types';
|
||||
import { PromptConcatHooks, PromptConcatTool } from './types';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatTool, ResponseHookContext };
|
||||
// Backward compatibility aliases
|
||||
export type { PromptConcatTool as PromptConcatPlugin };
|
||||
export type { AgentResponse, PostProcessContext, PromptConcatHookContext, PromptConcatHooks, PromptConcatTool, ResponseHookContext } from './types';
|
||||
|
||||
// Re-export defineTool API for LLM tools
|
||||
export { defineTool, getAllToolDefinitions, registerToolDefinition } from './defineTool';
|
||||
export type { ResponseHandlerContext, ToolDefinition, ToolExecutionResult, ToolHandlerContext } from './defineTool';
|
||||
|
||||
// Re-export modifier API
|
||||
export { defineModifier, getAllModifiers, registerModifier } from '../promptConcat/modifiers';
|
||||
export type { InsertContentOptions, ModifierDefinition, ModifierHandlerContext } from '../promptConcat/modifiers';
|
||||
|
||||
/**
|
||||
* Registry for built-in framework tools
|
||||
* Registry for all plugins (modifiers + LLM tools)
|
||||
*/
|
||||
export const builtInTools = new Map<string, PromptConcatTool>();
|
||||
export const pluginRegistry = new Map<string, PromptConcatTool>();
|
||||
|
||||
/**
|
||||
* Create unified hooks instance for the complete agent framework tool system
|
||||
* Create unified hooks instance for the agent framework
|
||||
*/
|
||||
export function createAgentFrameworkHooks(): PromptConcatHooks {
|
||||
return {
|
||||
|
|
@ -32,159 +53,87 @@ export function createAgentFrameworkHooks(): PromptConcatHooks {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all available tools
|
||||
* Register plugins to hooks based on framework configuration
|
||||
*/
|
||||
async function getAllTools() {
|
||||
const [
|
||||
promptToolsModule,
|
||||
wikiSearchModule,
|
||||
wikiOperationModule,
|
||||
workspacesListModule,
|
||||
messageManagementModule,
|
||||
] = await Promise.all([
|
||||
import('./prompt'),
|
||||
import('./wikiSearch'),
|
||||
import('./wikiOperation'),
|
||||
import('./workspacesList'),
|
||||
import('./messageManagement'),
|
||||
]);
|
||||
|
||||
return {
|
||||
messageManagement: messageManagementModule.messageManagementTool,
|
||||
fullReplacement: promptToolsModule.fullReplacementTool,
|
||||
wikiSearch: wikiSearchModule.wikiSearchTool,
|
||||
wikiOperation: wikiOperationModule.wikiOperationTool,
|
||||
workspacesList: workspacesListModule.workspacesListTool,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register tools to hooks based on framework configuration
|
||||
* @param hooks - The hooks instance to register tools to
|
||||
* @param agentFrameworkConfig - The framework configuration containing tool settings
|
||||
*/
|
||||
export async function registerToolsToHooksFromConfig(
|
||||
export async function registerPluginsToHooks(
|
||||
hooks: PromptConcatHooks,
|
||||
agentFrameworkConfig: { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
|
||||
): Promise<void> {
|
||||
// Always register core tools that are needed for basic functionality
|
||||
const messageManagementModule = await import('./messageManagement');
|
||||
messageManagementModule.messageManagementTool(hooks);
|
||||
logger.debug('Registered messageManagementTool to hooks');
|
||||
// Always register core infrastructure first (message persistence, streaming, status)
|
||||
registerCoreInfrastructure(hooks);
|
||||
logger.debug('Registered core infrastructure to hooks');
|
||||
|
||||
// Register tools based on framework configuration
|
||||
// Register plugins based on framework configuration
|
||||
if (agentFrameworkConfig.plugins) {
|
||||
for (const toolConfig of agentFrameworkConfig.plugins) {
|
||||
const { toolId } = toolConfig;
|
||||
for (const pluginConfig of agentFrameworkConfig.plugins) {
|
||||
const { toolId } = pluginConfig;
|
||||
|
||||
// Get tool from global registry (supports both built-in and dynamic tools)
|
||||
const tool = builtInTools.get(toolId);
|
||||
if (tool) {
|
||||
tool(hooks);
|
||||
logger.debug(`Registered tool ${toolId} to hooks`);
|
||||
const plugin = pluginRegistry.get(toolId);
|
||||
if (plugin) {
|
||||
plugin(hooks);
|
||||
logger.debug(`Registered plugin ${toolId} to hooks`);
|
||||
} else {
|
||||
logger.warn(`Tool not found in registry: ${toolId}`);
|
||||
logger.warn(`Plugin not found in registry: ${toolId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tool system - register all built-in tools to global registry
|
||||
* Initialize plugin system - register all built-in modifiers and LLM tools
|
||||
* This should be called once during service initialization
|
||||
*/
|
||||
export async function initializeToolSystem(): Promise<void> {
|
||||
// Import tool schemas and register them
|
||||
const [
|
||||
promptToolsModule,
|
||||
wikiSearchModule,
|
||||
wikiOperationModule,
|
||||
workspacesListModule,
|
||||
modelContextProtocolModule,
|
||||
] = await Promise.all([
|
||||
import('./prompt'),
|
||||
export async function initializePluginSystem(): Promise<void> {
|
||||
// Import all plugin modules to trigger registration
|
||||
await Promise.all([
|
||||
// LLM Tools
|
||||
import('./wikiSearch'),
|
||||
import('./wikiOperation'),
|
||||
import('./workspacesList'),
|
||||
import('./modelContextProtocol'),
|
||||
// Modifiers (imported via modifiers/index.ts)
|
||||
import('../promptConcat/modifiers'),
|
||||
]);
|
||||
|
||||
// Register tool parameter schemas
|
||||
registerToolParameterSchema(
|
||||
'fullReplacement',
|
||||
promptToolsModule.getFullReplacementParameterSchema(),
|
||||
{
|
||||
displayName: 'Full Replacement',
|
||||
description: 'Replace target content with content from specified source',
|
||||
},
|
||||
);
|
||||
// Register modifiers from the modifier registry
|
||||
const modifiers = getAllModifiers();
|
||||
for (const [modifierId, modifierDefinition] of modifiers) {
|
||||
pluginRegistry.set(modifierId, modifierDefinition.modifier);
|
||||
registerToolParameterSchema(modifierId, modifierDefinition.configSchema, {
|
||||
displayName: modifierDefinition.displayName,
|
||||
description: modifierDefinition.description,
|
||||
});
|
||||
logger.debug(`Registered modifier: ${modifierId}`);
|
||||
}
|
||||
|
||||
registerToolParameterSchema(
|
||||
'dynamicPosition',
|
||||
promptToolsModule.getDynamicPositionParameterSchema(),
|
||||
{
|
||||
displayName: 'Dynamic Position',
|
||||
description: 'Insert content at a specific position relative to a target element',
|
||||
},
|
||||
);
|
||||
// Register LLM tools from the tool registry
|
||||
const llmTools = getAllToolDefinitions();
|
||||
for (const [toolId, toolDefinition] of llmTools) {
|
||||
pluginRegistry.set(toolId, toolDefinition.tool);
|
||||
registerToolParameterSchema(toolId, toolDefinition.configSchema, {
|
||||
displayName: toolDefinition.displayName,
|
||||
description: toolDefinition.description,
|
||||
});
|
||||
logger.debug(`Registered LLM tool: ${toolId}`);
|
||||
}
|
||||
|
||||
registerToolParameterSchema(
|
||||
'wikiSearch',
|
||||
wikiSearchModule.getWikiSearchParameterSchema(),
|
||||
{
|
||||
displayName: 'Wiki Search',
|
||||
description: 'Search content in wiki workspaces and manage vector embeddings',
|
||||
},
|
||||
);
|
||||
|
||||
registerToolParameterSchema(
|
||||
'wikiOperation',
|
||||
wikiOperationModule.getWikiOperationParameterSchema(),
|
||||
{
|
||||
displayName: 'Wiki Operation',
|
||||
description: 'Perform operations on wiki workspaces (create, update, delete tiddlers)',
|
||||
},
|
||||
);
|
||||
|
||||
registerToolParameterSchema(
|
||||
'workspacesList',
|
||||
workspacesListModule.getWorkspacesListParameterSchema(),
|
||||
{
|
||||
displayName: 'Workspaces List',
|
||||
description: 'Inject available wiki workspaces list into prompts',
|
||||
},
|
||||
);
|
||||
|
||||
registerToolParameterSchema(
|
||||
'modelContextProtocol',
|
||||
modelContextProtocolModule.getModelContextProtocolParameterSchema(),
|
||||
{
|
||||
displayName: 'Model Context Protocol',
|
||||
description: 'MCP (Model Context Protocol) integration',
|
||||
},
|
||||
);
|
||||
|
||||
const tools = await getAllTools();
|
||||
// Register all built-in tools to global registry for discovery
|
||||
builtInTools.set('messageManagement', tools.messageManagement);
|
||||
builtInTools.set('fullReplacement', tools.fullReplacement);
|
||||
builtInTools.set('wikiSearch', tools.wikiSearch);
|
||||
builtInTools.set('wikiOperation', tools.wikiOperation);
|
||||
builtInTools.set('workspacesList', tools.workspacesList);
|
||||
logger.debug('All built-in tools and schemas registered successfully');
|
||||
logger.debug('Plugin system initialized', {
|
||||
totalPlugins: pluginRegistry.size,
|
||||
modifiers: modifiers.size,
|
||||
llmTools: llmTools.size,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create hooks and register tools based on framework configuration
|
||||
* This creates a new hooks instance and registers tools for that specific context
|
||||
* Create hooks and register plugins based on framework configuration
|
||||
*/
|
||||
export async function createHooksWithTools(
|
||||
export async function createHooksWithPlugins(
|
||||
agentFrameworkConfig: { plugins?: Array<{ toolId: string; [key: string]: unknown }> },
|
||||
): Promise<{ hooks: PromptConcatHooks; toolConfigs: Array<{ toolId: string; [key: string]: unknown }> }> {
|
||||
): Promise<{ hooks: PromptConcatHooks; pluginConfigs: Array<{ toolId: string; [key: string]: unknown }> }> {
|
||||
const hooks = createAgentFrameworkHooks();
|
||||
await registerToolsToHooksFromConfig(hooks, agentFrameworkConfig);
|
||||
await registerPluginsToHooks(hooks, agentFrameworkConfig);
|
||||
return {
|
||||
hooks,
|
||||
toolConfigs: agentFrameworkConfig.plugins || [],
|
||||
pluginConfigs: agentFrameworkConfig.plugins ?? [],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,269 +0,0 @@
|
|||
/**
|
||||
* Message management plugin
|
||||
* Unified plugin for handling message persistence, streaming updates, and UI synchronization
|
||||
* Combines functionality from persistencePlugin and aiResponseHistoryPlugin
|
||||
*/
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import type { IAgentInstanceService } from '../interface';
|
||||
import { createAgentMessage } from '../utilities';
|
||||
import type { AgentStatusContext, AIResponseContext, PromptConcatTool, ToolExecutionContext, UserMessageContext } from './types';
|
||||
|
||||
/**
|
||||
* Message management plugin
|
||||
* Handles all message-related operations: persistence, streaming, UI updates, and duration-based filtering
|
||||
*/
|
||||
export const messageManagementTool: PromptConcatTool = (hooks) => {
|
||||
// Handle user message persistence
|
||||
hooks.userMessageReceived.tapAsync('messageManagementTool', async (context: UserMessageContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, content, messageId } = context;
|
||||
|
||||
// Create user message using the helper function
|
||||
const userMessage = createAgentMessage(messageId, agentFrameworkContext.agent.id, {
|
||||
role: 'user',
|
||||
content: content.text,
|
||||
contentType: 'text/plain',
|
||||
metadata: content.file ? { file: content.file } : undefined,
|
||||
duration: undefined, // User messages persist indefinitely by default
|
||||
});
|
||||
|
||||
// Add message to the agent's message array for immediate use (do this before persistence so plugins see it)
|
||||
agentFrameworkContext.agent.messages.push(userMessage);
|
||||
|
||||
// Get the agent instance service to access repositories
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
// Save user message to database (if persistence fails, we still keep the in-memory message)
|
||||
await agentInstanceService.saveUserMessage(userMessage);
|
||||
|
||||
logger.debug('User message persisted to database', {
|
||||
messageId,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
contentLength: content.text.length,
|
||||
});
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message management plugin error in userMessageReceived', {
|
||||
error,
|
||||
messageId: context.messageId,
|
||||
agentId: context.agentFrameworkContext.agent.id,
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle agent status persistence
|
||||
hooks.agentStatusChanged.tapAsync('messageManagementTool', async (context: AgentStatusContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, status } = context;
|
||||
|
||||
// Get the agent instance service to update status
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
// Update agent status in database
|
||||
await agentInstanceService.updateAgent(agentFrameworkContext.agent.id, {
|
||||
status,
|
||||
});
|
||||
|
||||
// Update the agent object for immediate use
|
||||
agentFrameworkContext.agent.status = status;
|
||||
|
||||
logger.debug('Agent status updated in database', {
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
state: status.state,
|
||||
});
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message management plugin error in agentStatusChanged', {
|
||||
error,
|
||||
agentId: context.agentFrameworkContext.agent.id,
|
||||
status: context.status,
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle AI response updates during streaming
|
||||
hooks.responseUpdate.tapAsync('messageManagementTool', async (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response } = context;
|
||||
|
||||
if (response.status === 'update' && response.content) {
|
||||
// Find or create AI response message in agent's message array
|
||||
let aiMessage = agentFrameworkContext.agent.messages.find(
|
||||
(message) => message.role === 'assistant' && !message.metadata?.isComplete,
|
||||
);
|
||||
|
||||
if (!aiMessage) {
|
||||
// Create new AI message for streaming updates
|
||||
const now = new Date();
|
||||
aiMessage = {
|
||||
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
created: now,
|
||||
modified: now,
|
||||
metadata: { isComplete: false },
|
||||
duration: undefined, // AI responses persist indefinitely by default
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(aiMessage);
|
||||
// Persist immediately so DB timestamp reflects conversation order
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(aiMessage);
|
||||
aiMessage.metadata = { ...aiMessage.metadata, isPersisted: true };
|
||||
} catch (persistError) {
|
||||
logger.warn('Failed to persist initial streaming AI message', {
|
||||
error: persistError,
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Update existing message content
|
||||
aiMessage.content = response.content;
|
||||
aiMessage.modified = new Date();
|
||||
}
|
||||
|
||||
// Update UI using the agent instance service
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
agentInstanceService.debounceUpdateMessage(aiMessage, agentFrameworkContext.agent.id);
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for streaming message', {
|
||||
error: serviceError,
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Message management plugin error in responseUpdate', {
|
||||
error,
|
||||
});
|
||||
} finally {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle AI response completion
|
||||
hooks.responseComplete.tapAsync('messageManagementTool', async (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response } = context;
|
||||
|
||||
if (response.status === 'done' && response.content) {
|
||||
// Find and finalize AI response message
|
||||
let aiMessage = agentFrameworkContext.agent.messages.find(
|
||||
(message) => message.role === 'assistant' && !message.metadata?.isComplete && !message.metadata?.isToolResult,
|
||||
);
|
||||
|
||||
if (aiMessage) {
|
||||
// Mark as complete and update final content
|
||||
aiMessage.content = response.content;
|
||||
aiMessage.modified = new Date();
|
||||
aiMessage.metadata = { ...aiMessage.metadata, isComplete: true };
|
||||
} else {
|
||||
// Create final message if streaming message wasn't found
|
||||
const nowFinal = new Date();
|
||||
aiMessage = {
|
||||
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
created: nowFinal,
|
||||
modified: nowFinal,
|
||||
metadata: {
|
||||
isComplete: true,
|
||||
},
|
||||
duration: undefined, // Default duration for AI responses
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(aiMessage);
|
||||
}
|
||||
|
||||
// Get the agent instance service for persistence and UI updates
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
// Save final AI message to database using the same method as user messages
|
||||
await agentInstanceService.saveUserMessage(aiMessage);
|
||||
|
||||
// Final UI update
|
||||
try {
|
||||
agentInstanceService.debounceUpdateMessage(aiMessage, agentFrameworkContext.agent.id);
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for completed message', {
|
||||
error: serviceError,
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('AI response message completed and persisted', {
|
||||
messageId: aiMessage.id,
|
||||
finalContentLength: response.content.length,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message management plugin error in responseComplete', {
|
||||
error,
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle tool result messages persistence and UI updates
|
||||
hooks.toolExecuted.tapAsync('messageManagementTool', async (context: ToolExecutionContext, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext } = context;
|
||||
|
||||
// Find newly added tool result messages that need to be persisted
|
||||
const newToolResultMessages = agentFrameworkContext.agent.messages.filter(
|
||||
(message) => message.metadata?.isToolResult && !message.metadata.isPersisted,
|
||||
);
|
||||
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
|
||||
// Save tool result messages to database and update UI
|
||||
for (const message of newToolResultMessages) {
|
||||
try {
|
||||
// Save to database using the same method as user messages
|
||||
await agentInstanceService.saveUserMessage(message);
|
||||
|
||||
// Update UI
|
||||
agentInstanceService.debounceUpdateMessage(message, agentFrameworkContext.agent.id);
|
||||
|
||||
// Mark as persisted to avoid duplicate saves
|
||||
message.metadata = { ...message.metadata, isPersisted: true, uiUpdated: true };
|
||||
|
||||
logger.debug('Tool result message persisted to database', {
|
||||
messageId: message.id,
|
||||
toolId: message.metadata.toolId,
|
||||
duration: message.duration,
|
||||
});
|
||||
} catch (serviceError) {
|
||||
logger.error('Failed to persist tool result message', {
|
||||
error: serviceError,
|
||||
messageId: message.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (newToolResultMessages.length > 0) {
|
||||
logger.debug('Tool result messages processed', {
|
||||
count: newToolResultMessages.length,
|
||||
messageIds: newToolResultMessages.map(m => m.id),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message management plugin error in toolExecuted', {
|
||||
error,
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
/**
|
||||
* 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';
|
||||
import type { IPrompt } from '../promptConcat/promptConcatSchema';
|
||||
import { filterMessagesByDuration } from '../utilities/messageDurationFilter';
|
||||
import { normalizeRole } from '../utilities/normalizeRole';
|
||||
import { AgentResponse, PromptConcatTool, 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<typeof FullReplacementParameterSchema>;
|
||||
export type DynamicPositionParameter = z.infer<typeof DynamicPositionParameterSchema>;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const fullReplacementTool: PromptConcatTool = (hooks) => {
|
||||
// Normalize an AgentInstanceMessage role to Prompt role
|
||||
hooks.processPrompts.tapAsync('fullReplacementTool', async (context, callback) => {
|
||||
const { toolConfig, prompts, messages } = context;
|
||||
|
||||
if (toolConfig.toolId !== 'fullReplacement' || !toolConfig.fullReplacementParam) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const fullReplacementConfig = toolConfig.fullReplacementParam;
|
||||
if (!fullReplacementConfig) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetId, sourceType } = fullReplacementConfig;
|
||||
const found = findPromptById(prompts, targetId);
|
||||
|
||||
if (!found) {
|
||||
logger.warn('Target prompt not found for fullReplacement', {
|
||||
targetId,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all messages except the last user message being processed
|
||||
// We need to find and exclude only the current user message being processed, not just the last message
|
||||
const messagesCopy = cloneDeep(messages);
|
||||
|
||||
// Find the last user message (which is the one being processed in this round)
|
||||
let lastUserMessageIndex = -1;
|
||||
for (let index = messagesCopy.length - 1; index >= 0; index--) {
|
||||
if (messagesCopy[index].role === 'user') {
|
||||
lastUserMessageIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove only the last user message if found (this is the current message being processed)
|
||||
if (lastUserMessageIndex >= 0) {
|
||||
messagesCopy.splice(lastUserMessageIndex, 1);
|
||||
logger.debug('Removed current user message from history', {
|
||||
removedMessageId: messages[lastUserMessageIndex].id,
|
||||
remainingMessages: messagesCopy.length,
|
||||
});
|
||||
} else {
|
||||
logger.debug('No user message found to remove from history', {
|
||||
totalMessages: messagesCopy.length,
|
||||
messageRoles: messagesCopy.map(m => m.role),
|
||||
});
|
||||
}
|
||||
|
||||
// Apply duration filtering to exclude expired messages from AI context
|
||||
const filteredHistory = filterMessagesByDuration(messagesCopy);
|
||||
|
||||
switch (sourceType) {
|
||||
case 'historyOfSession':
|
||||
if (filteredHistory.length > 0) {
|
||||
// Insert filtered history messages as Prompt children (full Prompt type)
|
||||
found.prompt.children = [];
|
||||
filteredHistory.forEach((message, index: number) => {
|
||||
// Map AgentInstanceMessage role to Prompt role via normalizeRole
|
||||
type PromptRole = NonNullable<IPrompt['role']>;
|
||||
const role: PromptRole = normalizeRole(message.role);
|
||||
delete found.prompt.text;
|
||||
found.prompt.children!.push({
|
||||
id: `history-${index}`,
|
||||
caption: `History message ${index + 1}`,
|
||||
role,
|
||||
text: message.content,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
found.prompt.text = '无聊天历史。';
|
||||
}
|
||||
break;
|
||||
case 'llmResponse':
|
||||
// This is handled in response phase
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown sourceType: ${sourceType as string}`);
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Full replacement completed in prompt phase', {
|
||||
targetId,
|
||||
sourceType,
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
// Handle response phase for llmResponse source type
|
||||
hooks.postProcess.tapAsync('fullReplacementTool', async (context, callback) => {
|
||||
const responseContext = context as ResponseHookContext;
|
||||
const { toolConfig, llmResponse, responses } = responseContext;
|
||||
|
||||
if (toolConfig.toolId !== 'fullReplacement' || !toolConfig.fullReplacementParam) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const fullReplacementParameter = toolConfig.fullReplacementParam;
|
||||
if (!fullReplacementParameter) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetId, sourceType } = fullReplacementParameter;
|
||||
|
||||
// Only handle llmResponse in response phase
|
||||
if (sourceType !== 'llmResponse') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the target response by ID
|
||||
const found = responses.find((r: AgentResponse) => r.id === targetId);
|
||||
|
||||
if (!found) {
|
||||
logger.warn('Full replacement target not found in responses', {
|
||||
targetId,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace target content with LLM response
|
||||
logger.debug('Replacing target with LLM response', {
|
||||
targetId,
|
||||
responseLength: llmResponse.length,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
|
||||
found.text = llmResponse;
|
||||
|
||||
logger.debug('Full replacement completed in response phase', {
|
||||
targetId,
|
||||
sourceType,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic position plugin
|
||||
* Inserts content at a specific position relative to a target element
|
||||
*/
|
||||
export const dynamicPositionTool: PromptConcatTool = (hooks) => {
|
||||
hooks.processPrompts.tapAsync('dynamicPositionTool', async (context, callback) => {
|
||||
const { toolConfig, prompts } = context;
|
||||
|
||||
if (toolConfig.toolId !== 'dynamicPosition' || !toolConfig.dynamicPositionParam || !toolConfig.content) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicPositionConfig = toolConfig.dynamicPositionParam;
|
||||
if (!dynamicPositionConfig) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetId, position } = dynamicPositionConfig;
|
||||
const found = findPromptById(prompts, targetId);
|
||||
|
||||
if (!found) {
|
||||
logger.warn('Target prompt not found for dynamicPosition', {
|
||||
targetId,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new prompt part
|
||||
const newPart: IPrompt = {
|
||||
id: `dynamic-${toolConfig.id}-${Date.now()}`,
|
||||
caption: toolConfig.caption || 'Dynamic Content',
|
||||
text: toolConfig.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;
|
||||
case 'relative':
|
||||
// Simplified implementation, only adds to target's children
|
||||
if (!found.prompt.children) {
|
||||
found.prompt.children = [];
|
||||
}
|
||||
found.prompt.children.push(newPart);
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown position: ${position as string}`);
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Dynamic position insertion completed', {
|
||||
targetId,
|
||||
position,
|
||||
contentLength: toolConfig.content.length,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
/**
|
||||
* Wiki Operation plugin
|
||||
* Wiki Operation Tool
|
||||
* Handles wiki operation tool list injection, tool calling detection and response processing
|
||||
* Supports creating, updating, and deleting tiddlers in wiki workspaces
|
||||
*/
|
||||
import { WikiChannel } from '@/constants/channels';
|
||||
import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility';
|
||||
import { container } from '@services/container';
|
||||
import { i18n } from '@services/libs/i18n';
|
||||
import { t } from '@services/libs/i18n/placeholder';
|
||||
|
|
@ -13,14 +12,10 @@ import serviceIdentifier from '@services/serviceIdentifier';
|
|||
import type { IWikiService } from '@services/wiki/interface';
|
||||
import type { IWorkspaceService } from '@services/workspaces/interface';
|
||||
import { z } from 'zod/v4';
|
||||
import type { AgentInstanceMessage, IAgentInstanceService } from '../interface';
|
||||
import { findPromptById } from '../promptConcat/promptConcat';
|
||||
import { schemaToToolContent } from '../utilities/schemaToToolContent';
|
||||
import type { PromptConcatTool } from './types';
|
||||
import { registerToolDefinition, type ToolExecutionResult } from './defineTool';
|
||||
|
||||
/**
|
||||
* Wiki Operation Parameter Schema
|
||||
* Configuration parameters for the wiki operation plugin
|
||||
* Wiki Operation Config Schema (user-configurable in UI)
|
||||
*/
|
||||
export const WikiOperationParameterSchema = z.object({
|
||||
toolListPosition: z.object({
|
||||
|
|
@ -45,23 +40,16 @@ export const WikiOperationParameterSchema = z.object({
|
|||
description: t('Schema.WikiOperation.Description'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Type definition for wiki operation parameters
|
||||
*/
|
||||
export type WikiOperationParameter = z.infer<typeof WikiOperationParameterSchema>;
|
||||
|
||||
/**
|
||||
* Get the wiki operation parameter schema
|
||||
* @returns The schema for wiki operation parameters
|
||||
*/
|
||||
export function getWikiOperationParameterSchema() {
|
||||
return WikiOperationParameterSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter schema for Wiki operation tool
|
||||
* LLM-callable tool schema for wiki operations
|
||||
*/
|
||||
const WikiOperationToolParameterSchema = z.object({
|
||||
const WikiOperationToolSchema = z.object({
|
||||
workspaceName: z.string().meta({
|
||||
title: t('Schema.WikiOperation.Tool.Parameters.workspaceName.Title'),
|
||||
description: t('Schema.WikiOperation.Tool.Parameters.workspaceName.Description'),
|
||||
|
|
@ -86,347 +74,141 @@ const WikiOperationToolParameterSchema = z.object({
|
|||
title: t('Schema.WikiOperation.Tool.Parameters.options.Title'),
|
||||
description: t('Schema.WikiOperation.Tool.Parameters.options.Description'),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
title: 'wiki-operation',
|
||||
description: '在Wiki工作空间中执行操作(添加、删除或设置Tiddler文本)',
|
||||
examples: [
|
||||
{ workspaceName: '我的知识库', operation: WikiChannel.addTiddler, title: '示例笔记', text: '示例内容', extraMeta: '{}', options: '{}' },
|
||||
{ workspaceName: '我的知识库', operation: WikiChannel.setTiddlerText, title: '现有笔记', text: '更新后的内容', extraMeta: '{}', options: '{}' },
|
||||
{ workspaceName: '我的知识库', operation: WikiChannel.deleteTiddler, title: '要删除的笔记', extraMeta: '{}', options: '{}' },
|
||||
],
|
||||
});
|
||||
}).meta({
|
||||
title: 'wiki-operation',
|
||||
description: '在Wiki工作空间中执行操作(添加、删除或设置Tiddler文本)',
|
||||
examples: [
|
||||
{ workspaceName: '我的知识库', operation: WikiChannel.addTiddler, title: '示例笔记', text: '示例内容', extraMeta: '{}', options: '{}' },
|
||||
{ workspaceName: '我的知识库', operation: WikiChannel.setTiddlerText, title: '现有笔记', text: '更新后的内容', extraMeta: '{}', options: '{}' },
|
||||
{ workspaceName: '我的知识库', operation: WikiChannel.deleteTiddler, title: '要删除的笔记', extraMeta: '{}', options: '{}' },
|
||||
],
|
||||
});
|
||||
|
||||
type WikiOperationToolParameters = z.infer<typeof WikiOperationToolSchema>;
|
||||
|
||||
/**
|
||||
* Wiki Operation plugin - Prompt processing
|
||||
* Handles tool list injection for wiki operation functionality
|
||||
* Execute wiki operation
|
||||
*/
|
||||
export const wikiOperationTool: PromptConcatTool = (hooks) => {
|
||||
// First tapAsync: Tool list injection
|
||||
hooks.processPrompts.tapAsync('wikiOperationTool-toolList', async (context, callback) => {
|
||||
const { toolConfig, prompts } = context;
|
||||
async function executeWikiOperation(parameters: WikiOperationToolParameters): Promise<ToolExecutionResult> {
|
||||
const { workspaceName, operation, title, text, extraMeta, options: optionsString } = parameters;
|
||||
|
||||
if (toolConfig.toolId !== 'wikiOperation' || !toolConfig.wikiOperationParam) {
|
||||
callback();
|
||||
try {
|
||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
|
||||
|
||||
// Look up workspace
|
||||
const workspaces = await workspaceService.getWorkspacesAsList();
|
||||
const targetWorkspace = workspaces.find((ws) => ws.name === workspaceName || ws.id === workspaceName);
|
||||
|
||||
if (!targetWorkspace) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiOperation.Error.WorkspaceNotFound', {
|
||||
workspaceName,
|
||||
availableWorkspaces: workspaces.map((w) => `${w.name} (${w.id})`).join(', '),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceID = targetWorkspace.id;
|
||||
|
||||
if (!(await workspaceService.exists(workspaceID))) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiOperation.Error.WorkspaceNotExist', { workspaceID }),
|
||||
};
|
||||
}
|
||||
|
||||
const options = JSON.parse(optionsString || '{}') as Record<string, unknown>;
|
||||
|
||||
logger.debug('Executing wiki operation', { workspaceID, workspaceName, operation, title });
|
||||
|
||||
let result: string;
|
||||
|
||||
switch (operation) {
|
||||
case WikiChannel.addTiddler: {
|
||||
await wikiService.wikiOperationInServer(WikiChannel.addTiddler, workspaceID, [
|
||||
title,
|
||||
text || '',
|
||||
extraMeta || '{}',
|
||||
JSON.stringify({ withDate: true, ...options }),
|
||||
]);
|
||||
result = i18n.t('Tool.WikiOperation.Success.Added', { title, workspaceName });
|
||||
break;
|
||||
}
|
||||
|
||||
case WikiChannel.deleteTiddler: {
|
||||
await wikiService.wikiOperationInServer(WikiChannel.deleteTiddler, workspaceID, [title]);
|
||||
result = i18n.t('Tool.WikiOperation.Success.Deleted', { title, workspaceName });
|
||||
break;
|
||||
}
|
||||
|
||||
case WikiChannel.setTiddlerText: {
|
||||
await wikiService.wikiOperationInServer(WikiChannel.setTiddlerText, workspaceID, [title, text || '']);
|
||||
result = i18n.t('Tool.WikiOperation.Success.Updated', { title, workspaceName });
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
const exhaustiveCheck: never = operation;
|
||||
return { success: false, error: `Unsupported operation: ${String(exhaustiveCheck)}` };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
metadata: { workspaceID, workspaceName, operation, title },
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Wiki operation failed', { error, params: parameters });
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wiki Operation Tool Definition
|
||||
*/
|
||||
const wikiOperationDefinition = registerToolDefinition({
|
||||
toolId: 'wikiOperation',
|
||||
displayName: 'Wiki Operation',
|
||||
description: 'Perform operations on wiki workspaces (create, update, delete tiddlers)',
|
||||
configSchema: WikiOperationParameterSchema,
|
||||
llmToolSchemas: {
|
||||
'wiki-operation': WikiOperationToolSchema,
|
||||
},
|
||||
|
||||
onProcessPrompts({ config, toolConfig, injectToolList }) {
|
||||
const toolListPosition = config.toolListPosition;
|
||||
if (!toolListPosition?.targetId) return;
|
||||
|
||||
injectToolList({
|
||||
targetId: toolListPosition.targetId,
|
||||
position: 'child', // Add as child of target prompt
|
||||
caption: 'Wiki Operation Tool',
|
||||
});
|
||||
|
||||
logger.debug('Wiki operation tool list injected', {
|
||||
targetId: toolListPosition.targetId,
|
||||
position: toolListPosition.position,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
},
|
||||
|
||||
async onResponseComplete({ toolCall, executeToolCall, agentFrameworkContext }) {
|
||||
if (!toolCall || toolCall.toolId !== 'wiki-operation') return;
|
||||
|
||||
// Check cancellation
|
||||
if (agentFrameworkContext.isCancelled()) {
|
||||
logger.debug('Wiki operation cancelled', { agentId: agentFrameworkContext.agent.id });
|
||||
return;
|
||||
}
|
||||
|
||||
const wikiOperationParameter = toolConfig.wikiOperationParam;
|
||||
await executeToolCall('wiki-operation', executeWikiOperation);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Handle tool list injection if toolListPosition is configured
|
||||
const toolListPosition = wikiOperationParameter.toolListPosition;
|
||||
if (toolListPosition?.targetId) {
|
||||
const toolListTarget = findPromptById(prompts, toolListPosition.targetId);
|
||||
if (!toolListTarget) {
|
||||
logger.warn('Tool list target prompt not found', {
|
||||
targetId: toolListPosition.targetId,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get available wikis - now handled by workspacesListPlugin
|
||||
// The workspaces list will be injected separately by workspacesListPlugin
|
||||
|
||||
// Build tool content using shared utility (schema contains title/examples meta)
|
||||
const wikiOperationToolContent = schemaToToolContent(WikiOperationToolParameterSchema);
|
||||
|
||||
// Insert the tool content based on position
|
||||
if (toolListPosition.position === 'after') {
|
||||
if (!toolListTarget.prompt.children) {
|
||||
toolListTarget.prompt.children = [];
|
||||
}
|
||||
const insertIndex = toolListTarget.prompt.children.length;
|
||||
toolListTarget.prompt.children.splice(insertIndex, 0, {
|
||||
id: `wiki-operation-tool-${toolConfig.id}`,
|
||||
caption: 'Wiki Operation Tool',
|
||||
text: wikiOperationToolContent,
|
||||
});
|
||||
} else if (toolListPosition.position === 'before') {
|
||||
if (!toolListTarget.prompt.children) {
|
||||
toolListTarget.prompt.children = [];
|
||||
}
|
||||
toolListTarget.prompt.children.unshift({
|
||||
id: `wiki-operation-tool-${toolConfig.id}`,
|
||||
caption: 'Wiki Operation Tool',
|
||||
text: wikiOperationToolContent,
|
||||
});
|
||||
} else {
|
||||
// Default to appending text
|
||||
toolListTarget.prompt.text = (toolListTarget.prompt.text || '') + wikiOperationToolContent;
|
||||
}
|
||||
|
||||
logger.debug('Wiki operation tool list injected', {
|
||||
targetId: toolListPosition.targetId,
|
||||
position: toolListPosition.position,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Error in wiki operation tool list injection', {
|
||||
error,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Tool execution when AI response is complete
|
||||
hooks.responseComplete.tapAsync('wikiOperationTool-handler', async (context, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response, agentFrameworkConfig } = context;
|
||||
|
||||
// Find this plugin's configuration import { AgentFrameworkConfig }
|
||||
const wikiOperationToolConfig = agentFrameworkConfig?.plugins?.find((p: { toolId: string; [key: string]: unknown }) => p.toolId === 'wikiOperation');
|
||||
const wikiOperationParameter = wikiOperationToolConfig?.wikiOperationParam as { toolResultDuration?: number } | undefined;
|
||||
const toolResultDuration = wikiOperationParameter?.toolResultDuration || 1; // Default to 1 round
|
||||
|
||||
if (response.status !== 'done' || !response.content) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for wiki operation tool calls in the AI response
|
||||
const toolMatch = matchToolCalling(response.content);
|
||||
|
||||
if (!toolMatch.found || toolMatch.toolId !== 'wiki-operation') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Wiki operation tool call detected', {
|
||||
toolId: toolMatch.toolId,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
|
||||
// Set duration=1 for the AI message containing the tool call
|
||||
// Find the most recent AI message (should be the one containing the tool call)
|
||||
const aiMessages = agentFrameworkContext.agent.messages.filter((message: AgentInstanceMessage) => message.role === 'assistant');
|
||||
if (aiMessages.length > 0) {
|
||||
const latestAiMessage = aiMessages[aiMessages.length - 1];
|
||||
latestAiMessage.duration = toolResultDuration;
|
||||
logger.debug('Set AI message duration for tool call', {
|
||||
messageId: latestAiMessage.id,
|
||||
duration: toolResultDuration,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the wiki operation tool call
|
||||
try {
|
||||
logger.debug('Parsing wiki operation tool parameters', {
|
||||
toolMatch: toolMatch.parameters,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
|
||||
// Use parameters returned by matchToolCalling directly. Let zod schema validate.
|
||||
const validatedParameters = WikiOperationToolParameterSchema.parse(toolMatch.parameters as Record<string, unknown>);
|
||||
const { workspaceName, operation, title, text, extraMeta, options: optionsString } = validatedParameters;
|
||||
const options = JSON.parse(optionsString || '{}') as Record<string, unknown>;
|
||||
// Get workspace service
|
||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
|
||||
|
||||
// Look up workspace ID from workspace name or ID
|
||||
const workspaces = await workspaceService.getWorkspacesAsList();
|
||||
const targetWorkspace = workspaces.find(ws => ws.name === workspaceName || ws.id === workspaceName);
|
||||
if (!targetWorkspace) {
|
||||
throw new Error(
|
||||
i18n.t('Tool.WikiOperation.Error.WorkspaceNotFound', {
|
||||
workspaceName,
|
||||
availableWorkspaces: workspaces.map(w => `${w.name} (${w.id})`).join(', '),
|
||||
}) || `Workspace with name or ID "${workspaceName}" does not exist. Available workspaces: ${workspaces.map(w => `${w.name} (${w.id})`).join(', ')}`,
|
||||
);
|
||||
}
|
||||
const workspaceID = targetWorkspace.id;
|
||||
|
||||
if (!await workspaceService.exists(workspaceID)) {
|
||||
throw new Error(i18n.t('Tool.WikiOperation.Error.WorkspaceNotExist', { workspaceID }));
|
||||
}
|
||||
|
||||
logger.debug('Executing wiki operation', {
|
||||
workspaceID,
|
||||
workspaceName,
|
||||
operation,
|
||||
title,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
|
||||
let result: string;
|
||||
|
||||
// Execute the appropriate wiki operation directly
|
||||
switch (operation) {
|
||||
case WikiChannel.addTiddler: {
|
||||
await wikiService.wikiOperationInServer(WikiChannel.addTiddler, workspaceID, [
|
||||
title,
|
||||
text || '',
|
||||
extraMeta || '{}',
|
||||
JSON.stringify({ withDate: true, ...options }),
|
||||
]);
|
||||
result = i18n.t('Tool.WikiOperation.Success.Added', { title, workspaceName });
|
||||
break;
|
||||
}
|
||||
|
||||
case WikiChannel.deleteTiddler: {
|
||||
await wikiService.wikiOperationInServer(WikiChannel.deleteTiddler, workspaceID, [title]);
|
||||
result = i18n.t('Tool.WikiOperation.Success.Deleted', { title, workspaceName });
|
||||
break;
|
||||
}
|
||||
|
||||
case WikiChannel.setTiddlerText: {
|
||||
await wikiService.wikiOperationInServer(WikiChannel.setTiddlerText, workspaceID, [title, text || '']);
|
||||
result = i18n.t('Tool.WikiOperation.Success.Updated', { title, workspaceName });
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
const exhaustiveCheck: never = operation;
|
||||
throw new Error(`Unsupported operation: ${String(exhaustiveCheck)}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Wiki operation tool execution completed successfully', {
|
||||
workspaceID,
|
||||
operation,
|
||||
title,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
|
||||
// Format the tool result for display
|
||||
const toolResultText = `<functions_result>\nTool: wiki-operation\nParameters: ${JSON.stringify(validatedParameters)}\nResult: ${result}\n</functions_result>`;
|
||||
|
||||
// Set up actions to continue the conversation with tool results
|
||||
if (!context.actions) {
|
||||
context.actions = {};
|
||||
}
|
||||
context.actions.yieldNextRoundTo = 'self';
|
||||
|
||||
logger.debug('Wiki operation setting yieldNextRoundTo=self', {
|
||||
toolId: 'wiki-operation',
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
messageCount: agentFrameworkContext.agent.messages.length,
|
||||
toolResultPreview: toolResultText.slice(0, 200),
|
||||
});
|
||||
|
||||
// Immediately add the tool result message to history BEFORE calling toolExecuted
|
||||
const toolResultTime = new Date();
|
||||
const toolResultMessage: AgentInstanceMessage = {
|
||||
id: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'tool', // Tool result message
|
||||
content: toolResultText,
|
||||
modified: toolResultTime,
|
||||
duration: toolResultDuration, // Use configurable duration - default 1 round for tool results
|
||||
metadata: {
|
||||
isToolResult: true,
|
||||
isError: false,
|
||||
toolId: 'wiki-operation',
|
||||
toolParameters: validatedParameters,
|
||||
isPersisted: false, // Required by messageManagementPlugin to identify new tool results
|
||||
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
|
||||
artificialOrder: Date.now() + 10, // Additional ordering hint
|
||||
},
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(toolResultMessage);
|
||||
|
||||
// Persist tool result immediately so DB ordering matches in-memory order
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(toolResultMessage);
|
||||
toolResultMessage.metadata = { ...toolResultMessage.metadata, isPersisted: true };
|
||||
} catch (persistError) {
|
||||
logger.warn('Failed to persist tool result immediately in wikiOperationPlugin', {
|
||||
error: persistError,
|
||||
messageId: toolResultMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Signal that tool was executed AFTER adding and persisting the message
|
||||
await hooks.toolExecuted.promise({
|
||||
agentFrameworkContext,
|
||||
toolResult: {
|
||||
success: true,
|
||||
data: result,
|
||||
metadata: { toolCount: 1 },
|
||||
},
|
||||
toolInfo: {
|
||||
toolId: 'wiki-operation',
|
||||
parameters: validatedParameters,
|
||||
originalText: toolMatch.originalText || '',
|
||||
},
|
||||
requestId: context.requestId,
|
||||
});
|
||||
|
||||
logger.debug('Wiki operation tool execution completed', {
|
||||
toolResultText,
|
||||
actions: context.actions,
|
||||
toolResultMessageId: toolResultMessage.id,
|
||||
aiMessageDuration: aiMessages[aiMessages.length - 1]?.duration,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Wiki operation tool execution failed', {
|
||||
error,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
toolParameters: toolMatch.parameters,
|
||||
});
|
||||
|
||||
// Set up error response for next round
|
||||
if (!context.actions) {
|
||||
context.actions = {};
|
||||
}
|
||||
context.actions.yieldNextRoundTo = 'self';
|
||||
const errorMessage = `<functions_result>
|
||||
Tool: wiki-operation
|
||||
Error: ${error instanceof Error ? error.message : String(error)}
|
||||
</functions_result>`;
|
||||
|
||||
// Add error message to history BEFORE calling toolExecuted
|
||||
// Use the current time; order will be determined by save order
|
||||
const errorResultTime = new Date();
|
||||
const errorResultMessage: AgentInstanceMessage = {
|
||||
id: `tool-error-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'tool', // Tool error message
|
||||
content: errorMessage,
|
||||
modified: errorResultTime,
|
||||
duration: 2, // Error messages are visible to AI for 2 rounds: immediate + next round to allow explanation
|
||||
metadata: {
|
||||
isToolResult: true,
|
||||
isError: true,
|
||||
toolId: 'wiki-operation',
|
||||
isPersisted: false, // Required by messageManagementPlugin to identify new tool results
|
||||
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
|
||||
},
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(errorResultMessage);
|
||||
|
||||
// Signal that tool was executed (with error) AFTER adding the message
|
||||
await hooks.toolExecuted.promise({
|
||||
agentFrameworkContext,
|
||||
toolResult: {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
toolInfo: {
|
||||
toolId: 'wiki-operation',
|
||||
parameters: toolMatch.parameters || {},
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('Wiki operation tool execution failed but error result added', {
|
||||
errorResultMessageId: errorResultMessage.id,
|
||||
aiMessageDuration: aiMessages[aiMessages.length - 1]?.duration,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Error in wiki operation plugin response handler', { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
export const wikiOperationTool = wikiOperationDefinition.tool;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
/**
|
||||
* Wiki Search plugin
|
||||
* Wiki Search Tool
|
||||
* Handles wiki search tool list injection, tool calling detection and response processing
|
||||
*/
|
||||
import { WikiChannel } from '@/constants/channels';
|
||||
import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility';
|
||||
import { container } from '@services/container';
|
||||
import { i18n } from '@services/libs/i18n';
|
||||
import { t } from '@services/libs/i18n/placeholder';
|
||||
|
|
@ -14,30 +13,13 @@ import type { IWikiEmbeddingService } from '@services/wikiEmbedding/interface';
|
|||
import type { IWorkspaceService } from '@services/workspaces/interface';
|
||||
import type { ITiddlerFields } from 'tiddlywiki';
|
||||
import { z } from 'zod/v4';
|
||||
import type { AgentInstanceMessage, IAgentInstanceService } from '../interface';
|
||||
import { findPromptById } from '../promptConcat/promptConcat';
|
||||
import type { AiAPIConfig } from '../promptConcat/promptConcatSchema';
|
||||
import type { IPrompt } from '../promptConcat/promptConcatSchema';
|
||||
import { schemaToToolContent } from '../utilities/schemaToToolContent';
|
||||
import type { PromptConcatTool } from './types';
|
||||
import { registerToolDefinition, type ToolExecutionResult } from './defineTool';
|
||||
|
||||
/**
|
||||
* Wiki Search Parameter Schema
|
||||
* Configuration parameters for the wiki search plugin
|
||||
* Wiki Search Config Schema (user-configurable in UI)
|
||||
*/
|
||||
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'),
|
||||
|
|
@ -64,23 +46,16 @@ export const WikiSearchParameterSchema = z.object({
|
|||
description: t('Schema.WikiSearch.Description'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Type definition for wiki search parameters
|
||||
*/
|
||||
export type WikiSearchParameter = z.infer<typeof WikiSearchParameterSchema>;
|
||||
|
||||
/**
|
||||
* Get the wiki search parameter schema
|
||||
* @returns The schema for wiki search parameters
|
||||
*/
|
||||
export function getWikiSearchParameterSchema() {
|
||||
return WikiSearchParameterSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter schema for Wiki search tool
|
||||
* LLM-callable tool schema for wiki search
|
||||
*/
|
||||
const WikiSearchToolParameterSchema = z.object({
|
||||
const WikiSearchToolSchema = z.object({
|
||||
workspaceName: z.string().meta({
|
||||
title: t('Schema.WikiSearch.Tool.Parameters.workspaceName.Title'),
|
||||
description: t('Schema.WikiSearch.Tool.Parameters.workspaceName.Description'),
|
||||
|
|
@ -114,12 +89,12 @@ const WikiSearchToolParameterSchema = z.object({
|
|||
],
|
||||
});
|
||||
|
||||
type WikiSearchToolParameter = z.infer<typeof WikiSearchToolParameterSchema>;
|
||||
type WikiSearchToolParameters = z.infer<typeof WikiSearchToolSchema>;
|
||||
|
||||
/**
|
||||
* Parameter schema for Wiki update embeddings tool
|
||||
* LLM-callable tool schema for updating embeddings
|
||||
*/
|
||||
const WikiUpdateEmbeddingsToolParameterSchema = z.object({
|
||||
const WikiUpdateEmbeddingsToolSchema = z.object({
|
||||
workspaceName: z.string().meta({
|
||||
title: t('Schema.WikiSearch.Tool.UpdateEmbeddings.workspaceName.Title'),
|
||||
description: t('Schema.WikiSearch.Tool.UpdateEmbeddings.workspaceName.Description'),
|
||||
|
|
@ -128,160 +103,93 @@ const WikiUpdateEmbeddingsToolParameterSchema = z.object({
|
|||
title: t('Schema.WikiSearch.Tool.UpdateEmbeddings.forceUpdate.Title'),
|
||||
description: t('Schema.WikiSearch.Tool.UpdateEmbeddings.forceUpdate.Description'),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
title: 'wiki-update-embeddings',
|
||||
description: t('Schema.WikiSearch.Tool.UpdateEmbeddings.Description'),
|
||||
examples: [
|
||||
{ workspaceName: '我的知识库', forceUpdate: false },
|
||||
{ workspaceName: 'wiki', forceUpdate: true },
|
||||
],
|
||||
});
|
||||
}).meta({
|
||||
title: 'wiki-update-embeddings',
|
||||
description: t('Schema.WikiSearch.Tool.UpdateEmbeddings.Description'),
|
||||
examples: [
|
||||
{ workspaceName: '我的知识库', forceUpdate: false },
|
||||
{ workspaceName: 'wiki', forceUpdate: true },
|
||||
],
|
||||
});
|
||||
|
||||
type WikiUpdateEmbeddingsToolParameter = z.infer<typeof WikiUpdateEmbeddingsToolParameterSchema>;
|
||||
type WikiUpdateEmbeddingsToolParameters = z.infer<typeof WikiUpdateEmbeddingsToolSchema>;
|
||||
|
||||
/**
|
||||
* Execute wiki search tool
|
||||
* Execute wiki search
|
||||
*/
|
||||
async function executeWikiSearchTool(
|
||||
parameters: WikiSearchToolParameter,
|
||||
context?: { agentId?: string; messageId?: string; config?: AiAPIConfig },
|
||||
): Promise<{ success: boolean; data?: string; error?: string; metadata?: Record<string, unknown> }> {
|
||||
try {
|
||||
const { workspaceName, searchType = 'filter', filter, query, limit = 10, threshold = 0.7 } = parameters;
|
||||
async function executeWikiSearch(
|
||||
parameters: WikiSearchToolParameters,
|
||||
aiConfig?: AiAPIConfig,
|
||||
): Promise<ToolExecutionResult> {
|
||||
const { workspaceName, searchType = 'filter', filter, query, limit = 10, threshold = 0.7 } = parameters;
|
||||
|
||||
// Get workspace service
|
||||
try {
|
||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
|
||||
|
||||
// Look up workspace ID from workspace name or ID
|
||||
// Look up workspace
|
||||
const workspaces = await workspaceService.getWorkspacesAsList();
|
||||
const targetWorkspace = workspaces.find(ws => ws.name === workspaceName || ws.id === workspaceName);
|
||||
const targetWorkspace = workspaces.find((ws) => ws.name === workspaceName || ws.id === workspaceName);
|
||||
|
||||
if (!targetWorkspace) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.Error.WorkspaceNotFound', {
|
||||
workspaceName,
|
||||
availableWorkspaces: workspaces.map(w => `${w.name} (${w.id})`).join(', '),
|
||||
availableWorkspaces: workspaces.map((w) => `${w.name} (${w.id})`).join(', '),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceID = targetWorkspace.id;
|
||||
|
||||
if (!await workspaceService.exists(workspaceID)) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.Error.WorkspaceNotExist', { workspaceID }),
|
||||
};
|
||||
if (!(await workspaceService.exists(workspaceID))) {
|
||||
return { success: false, error: i18n.t('Tool.WikiSearch.Error.WorkspaceNotExist', { workspaceID }) };
|
||||
}
|
||||
|
||||
logger.debug('Executing wiki search', {
|
||||
workspaceID,
|
||||
workspaceName,
|
||||
searchType,
|
||||
filter,
|
||||
query,
|
||||
agentId: context?.agentId,
|
||||
});
|
||||
logger.debug('Executing wiki search', { workspaceID, workspaceName, searchType, filter, query });
|
||||
|
||||
// Execute search based on type
|
||||
let results: Array<{ title: string; text?: string; fields?: ITiddlerFields; similarity?: number }> = [];
|
||||
let searchMetadata: Record<string, unknown> = {
|
||||
workspaceID,
|
||||
workspaceName,
|
||||
searchType,
|
||||
};
|
||||
const results: Array<{ title: string; text?: string; fields?: ITiddlerFields; similarity?: number }> = [];
|
||||
let searchMetadata: Record<string, unknown> = { workspaceID, workspaceName, searchType };
|
||||
|
||||
if (searchType === 'vector') {
|
||||
// Vector search
|
||||
if (!query) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.Error.VectorSearchRequiresQuery'),
|
||||
};
|
||||
return { success: false, error: i18n.t('Tool.WikiSearch.Error.VectorSearchRequiresQuery') };
|
||||
}
|
||||
|
||||
if (!context?.config) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.Error.VectorSearchRequiresConfig'),
|
||||
};
|
||||
if (!aiConfig) {
|
||||
return { success: false, error: i18n.t('Tool.WikiSearch.Error.VectorSearchRequiresConfig') };
|
||||
}
|
||||
|
||||
const wikiEmbeddingService = container.get<IWikiEmbeddingService>(serviceIdentifier.WikiEmbedding);
|
||||
|
||||
try {
|
||||
const vectorResults = await wikiEmbeddingService.searchSimilar(
|
||||
workspaceID,
|
||||
query,
|
||||
context.config,
|
||||
limit,
|
||||
threshold,
|
||||
);
|
||||
const vectorResults = await wikiEmbeddingService.searchSimilar(workspaceID, query, aiConfig, limit, threshold);
|
||||
|
||||
if (vectorResults.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
data: i18n.t('Tool.WikiSearch.Success.NoVectorResults', { query, workspaceName, threshold }),
|
||||
metadata: {
|
||||
...searchMetadata,
|
||||
query,
|
||||
limit,
|
||||
threshold,
|
||||
resultCount: 0,
|
||||
},
|
||||
metadata: { ...searchMetadata, query, limit, threshold, resultCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// Convert vector search results to standard format
|
||||
results = vectorResults.map(vr => ({
|
||||
title: vr.record.tiddlerTitle,
|
||||
text: '', // Vector search returns chunks, full text needs separate retrieval
|
||||
similarity: vr.similarity,
|
||||
}));
|
||||
|
||||
// Retrieve full tiddler content for vector results
|
||||
const fullContentResults: typeof results = [];
|
||||
for (const result of results) {
|
||||
// Get full content for results
|
||||
for (const vr of vectorResults) {
|
||||
try {
|
||||
const tiddlerFields = await wikiService.wikiOperationInServer(
|
||||
WikiChannel.getTiddlersAsJson,
|
||||
workspaceID,
|
||||
[result.title],
|
||||
);
|
||||
if (tiddlerFields.length > 0) {
|
||||
fullContentResults.push({
|
||||
...result,
|
||||
text: tiddlerFields[0].text,
|
||||
fields: tiddlerFields[0],
|
||||
});
|
||||
} else {
|
||||
fullContentResults.push(result);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Error retrieving full tiddler content for ${result.title}`, {
|
||||
error,
|
||||
const tiddlerFields = await wikiService.wikiOperationInServer(WikiChannel.getTiddlersAsJson, workspaceID, [vr.record.tiddlerTitle]);
|
||||
results.push({
|
||||
title: vr.record.tiddlerTitle,
|
||||
text: tiddlerFields[0]?.text,
|
||||
fields: tiddlerFields[0],
|
||||
similarity: vr.similarity,
|
||||
});
|
||||
fullContentResults.push(result);
|
||||
} catch {
|
||||
results.push({ title: vr.record.tiddlerTitle, similarity: vr.similarity });
|
||||
}
|
||||
}
|
||||
results = fullContentResults;
|
||||
|
||||
searchMetadata = {
|
||||
...searchMetadata,
|
||||
query,
|
||||
limit,
|
||||
threshold,
|
||||
resultCount: results.length,
|
||||
};
|
||||
searchMetadata = { ...searchMetadata, query, limit, threshold, resultCount: results.length };
|
||||
} catch (error) {
|
||||
logger.error('Vector search failed', {
|
||||
error,
|
||||
workspaceID,
|
||||
query,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.Error.VectorSearchFailed', {
|
||||
|
|
@ -290,12 +198,9 @@ async function executeWikiSearchTool(
|
|||
};
|
||||
}
|
||||
} else {
|
||||
// Traditional filter search
|
||||
// Filter search
|
||||
if (!filter) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.Error.FilterSearchRequiresFilter'),
|
||||
};
|
||||
return { success: false, error: i18n.t('Tool.WikiSearch.Error.FilterSearchRequiresFilter') };
|
||||
}
|
||||
|
||||
const tiddlerTitles = await wikiService.wikiOperationInServer(WikiChannel.runFilter, workspaceID, [filter]);
|
||||
|
|
@ -304,56 +209,26 @@ async function executeWikiSearchTool(
|
|||
return {
|
||||
success: true,
|
||||
data: i18n.t('Tool.WikiSearch.Success.NoResults', { filter, workspaceName }),
|
||||
metadata: {
|
||||
...searchMetadata,
|
||||
filter,
|
||||
resultCount: 0,
|
||||
},
|
||||
metadata: { ...searchMetadata, filter, resultCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// Retrieve full tiddler content for each tiddler
|
||||
for (const title of tiddlerTitles) {
|
||||
try {
|
||||
const tiddlerFields = await wikiService.wikiOperationInServer(WikiChannel.getTiddlersAsJson, workspaceID, [title]);
|
||||
if (tiddlerFields.length > 0) {
|
||||
results.push({
|
||||
title,
|
||||
text: tiddlerFields[0].text,
|
||||
fields: tiddlerFields[0],
|
||||
});
|
||||
} else {
|
||||
results.push({ title });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Error retrieving tiddler content for ${title}`, {
|
||||
error,
|
||||
});
|
||||
results.push({ title, text: tiddlerFields[0]?.text, fields: tiddlerFields[0] });
|
||||
} catch {
|
||||
results.push({ title });
|
||||
}
|
||||
}
|
||||
|
||||
searchMetadata = {
|
||||
...searchMetadata,
|
||||
filter,
|
||||
resultCount: tiddlerTitles.length,
|
||||
returnedCount: results.length,
|
||||
};
|
||||
searchMetadata = { ...searchMetadata, filter, resultCount: results.length };
|
||||
}
|
||||
|
||||
// Format results as text with content
|
||||
let content = '';
|
||||
if (searchType === 'vector') {
|
||||
content = i18n.t('Tool.WikiSearch.Success.VectorCompleted', {
|
||||
totalResults: results.length,
|
||||
query,
|
||||
});
|
||||
} else {
|
||||
content = i18n.t('Tool.WikiSearch.Success.Completed', {
|
||||
totalResults: results.length,
|
||||
shownResults: results.length,
|
||||
}) + '\n\n';
|
||||
}
|
||||
// Format results
|
||||
let content = searchType === 'vector'
|
||||
? i18n.t('Tool.WikiSearch.Success.VectorCompleted', { totalResults: results.length, query })
|
||||
: i18n.t('Tool.WikiSearch.Success.Completed', { totalResults: results.length, shownResults: results.length }) + '\n\n';
|
||||
|
||||
for (const result of results) {
|
||||
content += `**Tiddler: ${result.title}**`;
|
||||
|
|
@ -361,26 +236,12 @@ async function executeWikiSearchTool(
|
|||
content += ` (Similarity: ${(result.similarity * 100).toFixed(1)}%)`;
|
||||
}
|
||||
content += '\n\n';
|
||||
if (result.text) {
|
||||
content += '```tiddlywiki\n';
|
||||
content += result.text;
|
||||
content += '\n```\n\n';
|
||||
} else {
|
||||
content += '(Content not available)\n\n';
|
||||
}
|
||||
content += result.text ? `\`\`\`tiddlywiki\n${result.text}\n\`\`\`\n\n` : '(Content not available)\n\n';
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: content,
|
||||
metadata: searchMetadata,
|
||||
};
|
||||
return { success: true, data: content, metadata: searchMetadata };
|
||||
} catch (error) {
|
||||
logger.error('Wiki search tool execution error', {
|
||||
error,
|
||||
parameters,
|
||||
});
|
||||
|
||||
logger.error('Wiki search failed', { error, params: parameters });
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.Error.ExecutionFailed', {
|
||||
|
|
@ -391,90 +252,57 @@ async function executeWikiSearchTool(
|
|||
}
|
||||
|
||||
/**
|
||||
* Execute wiki update embeddings tool
|
||||
* Execute wiki update embeddings
|
||||
*/
|
||||
async function executeWikiUpdateEmbeddingsTool(
|
||||
parameters: WikiUpdateEmbeddingsToolParameter,
|
||||
context?: { agentId?: string; messageId?: string; aiConfig?: unknown },
|
||||
): Promise<{ success: boolean; data?: string; error?: string; metadata?: Record<string, unknown> }> {
|
||||
try {
|
||||
const { workspaceName, forceUpdate = false } = parameters;
|
||||
async function executeWikiUpdateEmbeddings(
|
||||
parameters: WikiUpdateEmbeddingsToolParameters,
|
||||
aiConfig?: AiAPIConfig,
|
||||
): Promise<ToolExecutionResult> {
|
||||
const { workspaceName, forceUpdate = false } = parameters;
|
||||
|
||||
// Get workspace service
|
||||
try {
|
||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||
const wikiEmbeddingService = container.get<IWikiEmbeddingService>(serviceIdentifier.WikiEmbedding);
|
||||
|
||||
// Look up workspace ID from workspace name or ID
|
||||
const workspaces = await workspaceService.getWorkspacesAsList();
|
||||
const targetWorkspace = workspaces.find(ws => ws.name === workspaceName || ws.id === workspaceName);
|
||||
const targetWorkspace = workspaces.find((ws) => ws.name === workspaceName || ws.id === workspaceName);
|
||||
|
||||
if (!targetWorkspace) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.WorkspaceNotFound', {
|
||||
workspaceName,
|
||||
availableWorkspaces: workspaces.map(w => `${w.name} (${w.id})`).join(', '),
|
||||
availableWorkspaces: workspaces.map((w) => `${w.name} (${w.id})`).join(', '),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceID = targetWorkspace.id;
|
||||
|
||||
if (!await workspaceService.exists(workspaceID)) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.WorkspaceNotExist', { workspaceID }),
|
||||
};
|
||||
if (!(await workspaceService.exists(workspaceID))) {
|
||||
return { success: false, error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.WorkspaceNotExist', { workspaceID }) };
|
||||
}
|
||||
|
||||
// Check if AI config is available
|
||||
if (!context?.aiConfig) {
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.NoAIConfig'),
|
||||
};
|
||||
if (!aiConfig) {
|
||||
return { success: false, error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.NoAIConfig') };
|
||||
}
|
||||
|
||||
logger.debug('Executing wiki embedding generation', {
|
||||
workspaceID,
|
||||
workspaceName,
|
||||
forceUpdate,
|
||||
agentId: context?.agentId,
|
||||
});
|
||||
logger.debug('Executing wiki embedding generation', { workspaceID, workspaceName, forceUpdate });
|
||||
|
||||
// Generate embeddings
|
||||
await wikiEmbeddingService.generateEmbeddings(
|
||||
workspaceID,
|
||||
context.aiConfig as Parameters<IWikiEmbeddingService['generateEmbeddings']>[1],
|
||||
forceUpdate,
|
||||
);
|
||||
|
||||
// Get stats after generation
|
||||
await wikiEmbeddingService.generateEmbeddings(workspaceID, aiConfig, forceUpdate);
|
||||
const stats = await wikiEmbeddingService.getEmbeddingStats(workspaceID);
|
||||
|
||||
const result = i18n.t('Tool.WikiSearch.UpdateEmbeddings.Success.Generated', {
|
||||
workspaceName,
|
||||
totalEmbeddings: stats.totalEmbeddings,
|
||||
totalNotes: stats.totalNotes,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
metadata: {
|
||||
workspaceID,
|
||||
data: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Success.Generated', {
|
||||
workspaceName,
|
||||
totalEmbeddings: stats.totalEmbeddings,
|
||||
totalNotes: stats.totalNotes,
|
||||
forceUpdate,
|
||||
},
|
||||
}),
|
||||
metadata: { workspaceID, workspaceName, ...stats, forceUpdate },
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Wiki update embeddings tool execution error', {
|
||||
error,
|
||||
parameters,
|
||||
});
|
||||
|
||||
logger.error('Wiki update embeddings failed', { error, params: parameters });
|
||||
return {
|
||||
success: false,
|
||||
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.ExecutionFailed', {
|
||||
|
|
@ -485,325 +313,52 @@ async function executeWikiUpdateEmbeddingsTool(
|
|||
}
|
||||
|
||||
/**
|
||||
* Wiki Search plugin - Prompt processing
|
||||
* Handles tool list injection for wiki search and update embeddings functionality
|
||||
* Wiki Search Tool Definition
|
||||
*/
|
||||
export const wikiSearchTool: PromptConcatTool = (hooks) => {
|
||||
// First tapAsync: Tool list injection
|
||||
hooks.processPrompts.tapAsync('wikiSearchTool-toolList', async (context, callback) => {
|
||||
const { toolConfig, prompts } = context;
|
||||
const wikiSearchDefinition = registerToolDefinition({
|
||||
toolId: 'wikiSearch',
|
||||
displayName: 'Wiki Search',
|
||||
description: 'Search content in wiki workspaces and manage vector embeddings',
|
||||
configSchema: WikiSearchParameterSchema,
|
||||
llmToolSchemas: {
|
||||
'wiki-search': WikiSearchToolSchema,
|
||||
'wiki-update-embeddings': WikiUpdateEmbeddingsToolSchema,
|
||||
},
|
||||
|
||||
if (toolConfig.toolId !== 'wikiSearch' || !toolConfig.wikiSearchParam) {
|
||||
callback();
|
||||
onProcessPrompts({ config, toolConfig, injectToolList }) {
|
||||
const toolListPosition = config.toolListPosition;
|
||||
if (!toolListPosition?.targetId) return;
|
||||
|
||||
injectToolList({
|
||||
targetId: toolListPosition.targetId,
|
||||
position: 'child', // Add as child of target prompt
|
||||
caption: 'Wiki search tool',
|
||||
});
|
||||
|
||||
logger.debug('Wiki search tool list injected', {
|
||||
targetId: toolListPosition.targetId,
|
||||
position: toolListPosition.position,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
},
|
||||
|
||||
async onResponseComplete({ toolCall, executeToolCall, agentFrameworkContext }) {
|
||||
if (!toolCall) return;
|
||||
|
||||
// Check cancellation
|
||||
if (agentFrameworkContext.isCancelled()) {
|
||||
logger.debug('Wiki search cancelled', { agentId: agentFrameworkContext.agent.id });
|
||||
return;
|
||||
}
|
||||
|
||||
const wikiSearchParameter = toolConfig.wikiSearchParam;
|
||||
const aiConfig = agentFrameworkContext.agent.aiApiConfig as AiAPIConfig | undefined;
|
||||
|
||||
try {
|
||||
// Handle tool list injection if toolListPosition is configured
|
||||
const toolListPosition = wikiSearchParameter.toolListPosition;
|
||||
if (toolListPosition?.targetId) {
|
||||
const toolListTarget = findPromptById(prompts, toolListPosition.targetId);
|
||||
if (!toolListTarget) {
|
||||
logger.warn('Tool list target prompt not found', {
|
||||
targetId: toolListPosition.targetId,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get available wikis - now handled by workspacesListPlugin
|
||||
// The workspaces list will be injected separately by workspacesListPlugin
|
||||
|
||||
// Inject both wiki-search and wiki-update-embeddings tools
|
||||
const wikiSearchToolContent = schemaToToolContent(WikiSearchToolParameterSchema);
|
||||
const wikiUpdateEmbeddingsToolContent = schemaToToolContent(WikiUpdateEmbeddingsToolParameterSchema);
|
||||
|
||||
// Combine both tools into one prompt
|
||||
const combinedToolContent = `${wikiSearchToolContent}\n\n${wikiUpdateEmbeddingsToolContent}`;
|
||||
|
||||
const toolPrompt: IPrompt = {
|
||||
id: `wiki-tool-list-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
text: combinedToolContent,
|
||||
tags: ['toolList', 'wikiSearch', 'wikiEmbedding'],
|
||||
// Use singular caption to match test expectations
|
||||
caption: 'Wiki search tool',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Insert at specified position
|
||||
if (toolListPosition.position === 'before') {
|
||||
toolListTarget.parent.splice(toolListTarget.index, 0, toolPrompt);
|
||||
} else {
|
||||
toolListTarget.parent.splice(toolListTarget.index + 1, 0, toolPrompt);
|
||||
}
|
||||
|
||||
logger.debug('Wiki tool list injected successfully', {
|
||||
targetId: toolListPosition.targetId,
|
||||
position: toolListPosition.position,
|
||||
toolCount: 2, // wiki-search and wiki-update-embeddings
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Error in wiki search tool list injection', {
|
||||
error,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
if (toolCall.toolId === 'wiki-search') {
|
||||
await executeToolCall('wiki-search', (parameters) => executeWikiSearch(parameters, aiConfig));
|
||||
} else if (toolCall.toolId === 'wiki-update-embeddings') {
|
||||
await executeToolCall('wiki-update-embeddings', (parameters) => executeWikiUpdateEmbeddings(parameters, aiConfig));
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Tool execution when AI response is complete
|
||||
hooks.responseComplete.tapAsync('wikiSearchTool-handler', async (context, callback) => {
|
||||
try {
|
||||
const { agentFrameworkContext, response, agentFrameworkConfig } = context;
|
||||
|
||||
// Find this plugin's configuration import { AgentFrameworkConfig }
|
||||
const wikiSearchToolConfig = agentFrameworkConfig?.plugins?.find((p: { toolId: string; [key: string]: unknown }) => p.toolId === 'wikiSearch');
|
||||
const wikiSearchParameter = wikiSearchToolConfig?.wikiSearchParam as { toolResultDuration?: number } | undefined;
|
||||
const toolResultDuration = wikiSearchParameter?.toolResultDuration || 1; // Default to 1 round
|
||||
|
||||
if (response.status !== 'done' || !response.content) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for wiki search or update embeddings tool calls in the AI response
|
||||
const toolMatch = matchToolCalling(response.content);
|
||||
|
||||
if (!toolMatch.found || (toolMatch.toolId !== 'wiki-search' && toolMatch.toolId !== 'wiki-update-embeddings')) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Wiki tool call detected', {
|
||||
toolId: toolMatch.toolId,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
|
||||
// Set duration=1 for the AI message containing the tool call
|
||||
// Find the most recent AI message (should be the one containing the tool call)
|
||||
const aiMessages = agentFrameworkContext.agent.messages.filter((message: AgentInstanceMessage) => message.role === 'assistant');
|
||||
if (aiMessages.length > 0) {
|
||||
const latestAiMessage = aiMessages[aiMessages.length - 1];
|
||||
if (latestAiMessage.content === response.content) {
|
||||
latestAiMessage.duration = 1;
|
||||
latestAiMessage.metadata = {
|
||||
...latestAiMessage.metadata,
|
||||
containsToolCall: true,
|
||||
toolId: toolMatch.toolId,
|
||||
};
|
||||
|
||||
// Notify frontend about the duration change immediately (no debounce delay)
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
// Persist the AI message right away so DB ordering reflects this message before tool results
|
||||
try {
|
||||
if (!latestAiMessage.created) latestAiMessage.created = new Date();
|
||||
await agentInstanceService.saveUserMessage(latestAiMessage);
|
||||
latestAiMessage.metadata = { ...latestAiMessage.metadata, isPersisted: true };
|
||||
} catch (error) {
|
||||
logger.warn('Failed to persist AI message containing tool call immediately', {
|
||||
error,
|
||||
messageId: latestAiMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Also update UI immediately
|
||||
agentInstanceService.debounceUpdateMessage(latestAiMessage, agentFrameworkContext.agent.id, 0); // No delay
|
||||
|
||||
logger.debug('Set duration=1 for AI tool call message', {
|
||||
messageId: latestAiMessage.id,
|
||||
toolId: toolMatch.toolId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the appropriate tool
|
||||
try {
|
||||
// Check if cancelled before starting tool execution
|
||||
if (agentFrameworkContext.isCancelled()) {
|
||||
logger.debug('Wiki tool cancelled before execution', {
|
||||
toolId: toolMatch.toolId,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate parameters and execute based on tool type
|
||||
let result: { success: boolean; data?: string; error?: string; metadata?: Record<string, unknown> };
|
||||
let validatedParameters: WikiSearchToolParameter | WikiUpdateEmbeddingsToolParameter;
|
||||
|
||||
if (toolMatch.toolId === 'wiki-search') {
|
||||
validatedParameters = WikiSearchToolParameterSchema.parse(toolMatch.parameters);
|
||||
result = await executeWikiSearchTool(
|
||||
validatedParameters,
|
||||
{
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
messageId: agentFrameworkContext.agent.messages[agentFrameworkContext.agent.messages.length - 1]?.id,
|
||||
config: agentFrameworkContext.agent.aiApiConfig as AiAPIConfig | undefined,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// wiki-update-embeddings
|
||||
validatedParameters = WikiUpdateEmbeddingsToolParameterSchema.parse(toolMatch.parameters);
|
||||
result = await executeWikiUpdateEmbeddingsTool(
|
||||
validatedParameters,
|
||||
{
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
messageId: agentFrameworkContext.agent.messages[agentFrameworkContext.agent.messages.length - 1]?.id,
|
||||
aiConfig: agentFrameworkContext.agent.aiApiConfig,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Check if cancelled after tool execution
|
||||
if (agentFrameworkContext.isCancelled()) {
|
||||
logger.debug('Wiki tool cancelled after execution', {
|
||||
toolId: toolMatch.toolId,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the tool result for display
|
||||
let toolResultText: string;
|
||||
let isError = false;
|
||||
|
||||
if (result.success && result.data) {
|
||||
toolResultText = `<functions_result>\nTool: ${toolMatch.toolId}\nParameters: ${JSON.stringify(validatedParameters)}\nResult: ${result.data}\n</functions_result>`;
|
||||
} else {
|
||||
isError = true;
|
||||
toolResultText = `<functions_result>\nTool: ${toolMatch.toolId}\nParameters: ${JSON.stringify(validatedParameters)}\nError: ${result.error}\n</functions_result>`;
|
||||
}
|
||||
|
||||
// Set up actions to continue the conversation with tool results
|
||||
const responseContext = context;
|
||||
if (!responseContext.actions) {
|
||||
responseContext.actions = {};
|
||||
}
|
||||
responseContext.actions.yieldNextRoundTo = 'self';
|
||||
|
||||
logger.debug('Wiki search setting yieldNextRoundTo=self', {
|
||||
toolId: 'wiki-search',
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
messageCount: agentFrameworkContext.agent.messages.length,
|
||||
toolResultPreview: toolResultText.slice(0, 200),
|
||||
});
|
||||
|
||||
// Immediately add the tool result message to history BEFORE calling toolExecuted
|
||||
const nowTool = new Date();
|
||||
const toolResultMessage: AgentInstanceMessage = {
|
||||
id: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'tool', // Tool result message
|
||||
content: toolResultText,
|
||||
created: nowTool,
|
||||
modified: nowTool,
|
||||
duration: toolResultDuration, // Use configurable duration - default 1 round for tool results
|
||||
metadata: {
|
||||
isToolResult: true,
|
||||
isError,
|
||||
toolId: 'wiki-search',
|
||||
toolParameters: validatedParameters,
|
||||
isPersisted: false, // Required by messageManagementPlugin to identify new tool results
|
||||
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
|
||||
artificialOrder: Date.now() + 10, // Additional ordering hint
|
||||
},
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(toolResultMessage);
|
||||
|
||||
// Do not persist immediately here. Let messageManagementPlugin handle persistence
|
||||
|
||||
// Signal that tool was executed AFTER adding and persisting the message
|
||||
await hooks.toolExecuted.promise({
|
||||
agentFrameworkContext,
|
||||
toolResult: {
|
||||
success: true,
|
||||
data: result.success ? result.data : result.error,
|
||||
metadata: { toolCount: 1 },
|
||||
},
|
||||
toolInfo: {
|
||||
toolId: 'wiki-search',
|
||||
parameters: validatedParameters,
|
||||
originalText: toolMatch.originalText,
|
||||
},
|
||||
requestId: context.requestId,
|
||||
});
|
||||
|
||||
logger.debug('Wiki search tool execution completed', {
|
||||
toolResultText,
|
||||
actions: responseContext.actions,
|
||||
toolResultMessageId: toolResultMessage.id,
|
||||
aiMessageDuration: aiMessages[aiMessages.length - 1]?.duration,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Wiki search tool execution failed', {
|
||||
error,
|
||||
toolCall: toolMatch,
|
||||
});
|
||||
|
||||
// Set up error response for next round
|
||||
const responseContext = context;
|
||||
if (!responseContext.actions) {
|
||||
responseContext.actions = {};
|
||||
}
|
||||
responseContext.actions.yieldNextRoundTo = 'self';
|
||||
const errorMessage = `<functions_result>
|
||||
Tool: wiki-search
|
||||
Error: ${error instanceof Error ? error.message : String(error)}
|
||||
</functions_result>`;
|
||||
|
||||
// Add error message to history BEFORE calling toolExecuted
|
||||
// Use the current time; order will be determined by save order
|
||||
const nowError = new Date();
|
||||
const errorResultMessage: AgentInstanceMessage = {
|
||||
id: `tool-error-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: agentFrameworkContext.agent.id,
|
||||
role: 'tool', // Tool error message
|
||||
content: errorMessage,
|
||||
created: nowError,
|
||||
modified: nowError,
|
||||
duration: 2, // Error messages are visible to AI for 2 rounds: immediate + next round to allow explanation
|
||||
metadata: {
|
||||
isToolResult: true,
|
||||
isError: true,
|
||||
toolId: 'wiki-search',
|
||||
isPersisted: false, // Required by messageManagementPlugin to identify new tool results
|
||||
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
|
||||
},
|
||||
};
|
||||
agentFrameworkContext.agent.messages.push(errorResultMessage);
|
||||
|
||||
// Do not persist immediately; let messageManagementPlugin handle it during toolExecuted
|
||||
await hooks.toolExecuted.promise({
|
||||
agentFrameworkContext,
|
||||
toolResult: {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
toolInfo: {
|
||||
toolId: 'wiki-search',
|
||||
parameters: {},
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('Wiki search tool execution failed but error result added', {
|
||||
errorResultMessageId: errorResultMessage.id,
|
||||
aiMessageDuration: aiMessages[aiMessages.length - 1]?.duration,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Error in wiki search handler plugin', { error });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
export const wikiSearchTool = wikiSearchDefinition.tool;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
/**
|
||||
* Workspaces List plugin
|
||||
* Handles injection of available wiki workspaces list into prompts
|
||||
* Workspaces List Tool
|
||||
* Injects available wiki workspaces list into prompts
|
||||
*/
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import type { IWorkspaceService } from '@services/workspaces/interface';
|
||||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||
import { identity } from 'lodash';
|
||||
import { z } from 'zod/v4';
|
||||
import { registerToolDefinition } from './defineTool';
|
||||
|
||||
const t = identity;
|
||||
|
||||
/**
|
||||
* Workspaces List Parameter Schema
|
||||
* Configuration parameters for the workspaces list plugin
|
||||
*/
|
||||
export const WorkspacesListParameterSchema = z.object({
|
||||
targetId: z.string().meta({
|
||||
|
|
@ -25,115 +30,72 @@ export const WorkspacesListParameterSchema = z.object({
|
|||
description: t('Schema.WorkspacesList.Description'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Type definition for workspaces list parameters
|
||||
*/
|
||||
export type WorkspacesListParameter = z.infer<typeof WorkspacesListParameterSchema>;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
import type { IWorkspaceService } from '@services/workspaces/interface';
|
||||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||
|
||||
import { findPromptById } from '../promptConcat/promptConcat';
|
||||
import type { PromptConcatTool } from './types';
|
||||
|
||||
/**
|
||||
* Workspaces List plugin - Prompt processing
|
||||
* Handles injection of available wiki workspaces list
|
||||
* Workspaces List Tool Definition
|
||||
*/
|
||||
export const workspacesListTool: PromptConcatTool = (hooks) => {
|
||||
// Tool list injection
|
||||
hooks.processPrompts.tapAsync('workspacesListTool-injection', async (context, callback) => {
|
||||
const { toolConfig, prompts } = context;
|
||||
const workspacesListDefinition = registerToolDefinition({
|
||||
toolId: 'workspacesList',
|
||||
displayName: 'Workspaces List',
|
||||
description: 'Inject available wiki workspaces list into prompts',
|
||||
configSchema: WorkspacesListParameterSchema,
|
||||
|
||||
if (toolConfig.toolId !== 'workspacesList' || !toolConfig.workspacesListParam) {
|
||||
callback();
|
||||
async onProcessPrompts({ config, toolConfig, findPrompt }) {
|
||||
if (!config.targetId) return;
|
||||
|
||||
const target = findPrompt(config.targetId);
|
||||
if (!target) {
|
||||
logger.warn('Workspaces list target prompt not found', {
|
||||
targetId: config.targetId,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const workspacesListParameter = toolConfig.workspacesListParam;
|
||||
// Get available wikis
|
||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||
const workspaces = await workspaceService.getWorkspacesAsList();
|
||||
const wikiWorkspaces = workspaces.filter(isWikiWorkspace);
|
||||
|
||||
try {
|
||||
// Handle workspaces list injection if targetId is configured
|
||||
if (workspacesListParameter?.targetId) {
|
||||
const target = findPromptById(prompts, workspacesListParameter.targetId);
|
||||
if (!target) {
|
||||
logger.warn('Workspaces list target prompt not found', {
|
||||
targetId: workspacesListParameter.targetId,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get available wikis
|
||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||
const workspaces = await workspaceService.getWorkspacesAsList();
|
||||
const wikiWorkspaces = workspaces.filter(isWikiWorkspace);
|
||||
|
||||
if (wikiWorkspaces.length > 0) {
|
||||
// Use fixed list format for simplicity
|
||||
const workspacesList = wikiWorkspaces
|
||||
.map(workspace => `- ${workspace.name} (ID: ${workspace.id})`)
|
||||
.join('\n');
|
||||
|
||||
const workspacesListContent = `Available Wiki Workspaces:\n${workspacesList}`;
|
||||
|
||||
// Insert the workspaces list content based on position
|
||||
if (workspacesListParameter.position === 'after') {
|
||||
if (!target.prompt.children) {
|
||||
target.prompt.children = [];
|
||||
}
|
||||
const insertIndex = target.prompt.children.length;
|
||||
target.prompt.children.splice(insertIndex, 0, {
|
||||
id: `workspaces-list-${toolConfig.id}`,
|
||||
caption: 'Available Workspaces',
|
||||
text: workspacesListContent,
|
||||
});
|
||||
} else if (workspacesListParameter.position === 'before') {
|
||||
if (!target.prompt.children) {
|
||||
target.prompt.children = [];
|
||||
}
|
||||
target.prompt.children.unshift({
|
||||
id: `workspaces-list-${toolConfig.id}`,
|
||||
caption: 'Available Workspaces',
|
||||
text: workspacesListContent,
|
||||
});
|
||||
} else {
|
||||
// Default to appending text
|
||||
target.prompt.text = (target.prompt.text || '') + '\n' + workspacesListContent;
|
||||
}
|
||||
|
||||
logger.debug('Workspaces list injected successfully', {
|
||||
targetId: workspacesListParameter.targetId,
|
||||
position: workspacesListParameter.position,
|
||||
toolId: toolConfig.id,
|
||||
workspaceCount: wikiWorkspaces.length,
|
||||
});
|
||||
} else {
|
||||
logger.debug('No wiki workspaces found to inject', {
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Error in workspaces list injection', {
|
||||
error,
|
||||
toolId: toolConfig.id,
|
||||
});
|
||||
callback();
|
||||
if (wikiWorkspaces.length === 0) {
|
||||
logger.debug('No wiki workspaces found to inject', { toolId: toolConfig.id });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const workspacesList = wikiWorkspaces
|
||||
.map((workspace) => `- ${workspace.name} (ID: ${workspace.id})`)
|
||||
.join('\n');
|
||||
const workspacesListContent = `Available Wiki Workspaces:\n${workspacesList}`;
|
||||
|
||||
// Insert based on position
|
||||
if (!target.prompt.children) {
|
||||
target.prompt.children = [];
|
||||
}
|
||||
|
||||
const newPrompt = {
|
||||
id: `workspaces-list-${toolConfig.id}`,
|
||||
caption: 'Available Workspaces',
|
||||
text: workspacesListContent,
|
||||
};
|
||||
|
||||
if (config.position === 'before') {
|
||||
target.prompt.children.unshift(newPrompt);
|
||||
} else {
|
||||
target.prompt.children.push(newPrompt);
|
||||
}
|
||||
|
||||
logger.debug('Workspaces list injected successfully', {
|
||||
targetId: config.targetId,
|
||||
position: config.position,
|
||||
toolId: toolConfig.id,
|
||||
workspaceCount: wikiWorkspaces.length,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const workspacesListTool = workspacesListDefinition.tool;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,31 @@
|
|||
/**
|
||||
* Tests for schemaToToolContent utility
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { i18n } from '@services/libs/i18n';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { schemaToToolContent } from '../schemaToToolContent';
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('@services/libs/i18n', () => ({
|
||||
i18n: {
|
||||
t: vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'Tool.Schema.Required': '必需',
|
||||
'Tool.Schema.Optional': '可选',
|
||||
'Tool.Schema.Description': '描述',
|
||||
'Tool.Schema.Parameters': '参数',
|
||||
'Tool.Schema.Examples': '使用示例',
|
||||
};
|
||||
return translations[key] || key;
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('schemaToToolContent', () => {
|
||||
beforeEach(() => {
|
||||
// Setup i18n mock for each test
|
||||
|
||||
vi.mocked(i18n.t).mockImplementation(
|
||||
((...args: unknown[]) => {
|
||||
const key = String(args[0]);
|
||||
const translations: Record<string, string> = {
|
||||
'Tool.Schema.Required': '必需',
|
||||
'Tool.Schema.Optional': '可选',
|
||||
'Tool.Schema.Description': '描述',
|
||||
'Tool.Schema.Parameters': '参数',
|
||||
'Tool.Schema.Examples': '使用示例',
|
||||
};
|
||||
return translations[key] ?? key;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as any,
|
||||
);
|
||||
});
|
||||
it('should generate tool content from schema with title and description', () => {
|
||||
const testSchema = z.object({
|
||||
name: z.string().describe('The name parameter'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue