mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-01-18 23:31:31 -08:00
refactor: less plugins
This commit is contained in:
parent
e47941f71a
commit
d2a37dac87
26 changed files with 836 additions and 427 deletions
|
|
@ -83,9 +83,9 @@ describe('URL Helper Functions', () => {
|
|||
});
|
||||
|
||||
test('should handle non-string inputs', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(equivalentDomain(null as any)).toBeUndefined();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(equivalentDomain(123 as any)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,22 +5,27 @@ import SmartToyIcon from '@mui/icons-material/SmartToy';
|
|||
import { Avatar, Box } from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import { isMessageExpiredForAI } from '../../../services/agentInstance/utilities/messageDurationFilter';
|
||||
import { useAgentChatStore } from '../../Agent/store/agentChatStore/index';
|
||||
import { MessageRenderer } from './MessageRenderer';
|
||||
|
||||
const BubbleContainer = styled(Box)<{ $isUser: boolean }>`
|
||||
const BubbleContainer = styled(Box)<{ $isUser: boolean; $isExpired?: boolean }>`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
max-width: 80%;
|
||||
align-self: ${props => props.$isUser ? 'flex-end' : 'flex-start'};
|
||||
opacity: ${props => props.$isExpired ? 0.5 : 1};
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
`;
|
||||
|
||||
const MessageAvatar = styled(Avatar)<{ $isUser: boolean }>`
|
||||
const MessageAvatar = styled(Avatar)<{ $isUser: boolean; $isExpired?: boolean }>`
|
||||
background-color: ${props => props.$isUser ? props.theme.palette.primary.main : props.theme.palette.secondary.main};
|
||||
color: ${props => props.$isUser ? props.theme.palette.primary.contrastText : props.theme.palette.secondary.contrastText};
|
||||
opacity: ${props => props.$isExpired ? 0.7 : 1};
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
`;
|
||||
|
||||
const MessageContent = styled(Box)<{ $isUser: boolean; $isStreaming?: boolean }>`
|
||||
const MessageContent = styled(Box)<{ $isUser: boolean; $isStreaming?: boolean; $isExpired?: boolean }>`
|
||||
background-color: ${props => props.$isUser ? props.theme.palette.primary.light : props.theme.palette.background.paper};
|
||||
color: ${props => props.$isUser ? props.theme.palette.primary.contrastText : props.theme.palette.text.primary};
|
||||
padding: 12px 16px;
|
||||
|
|
@ -28,10 +33,18 @@ const MessageContent = styled(Box)<{ $isUser: boolean; $isStreaming?: boolean }>
|
|||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
transition: all 0.3s ease-in-out;
|
||||
opacity: ${props => props.$isExpired ? 0.6 : 1};
|
||||
|
||||
/* Add visual indicators for expired messages */
|
||||
${props =>
|
||||
props.$isExpired && `
|
||||
border: 1px dashed ${props.theme.palette.divider};
|
||||
filter: grayscale(0.3);
|
||||
`}
|
||||
|
||||
/* Add a subtle highlight for completed assistant messages */
|
||||
${props =>
|
||||
!props.$isUser && !props.$isStreaming && `
|
||||
!props.$isUser && !props.$isStreaming && !props.$isExpired && `
|
||||
border-left: 2px solid ${props.theme.palette.divider};
|
||||
`}
|
||||
|
||||
|
|
@ -70,25 +83,31 @@ interface MessageBubbleProps {
|
|||
export const MessageBubble: React.FC<MessageBubbleProps> = ({ messageId }) => {
|
||||
const message = useAgentChatStore(state => state.getMessageById(messageId));
|
||||
const isStreaming = useAgentChatStore(state => state.isMessageStreaming(messageId));
|
||||
const orderedMessageIds = useAgentChatStore(state => state.orderedMessageIds);
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
const isUser = message.role === 'user';
|
||||
|
||||
// Calculate if message is expired for AI context
|
||||
const messageIndex = orderedMessageIds.indexOf(messageId);
|
||||
const totalMessages = orderedMessageIds.length;
|
||||
const isExpired = isMessageExpiredForAI(message, messageIndex, totalMessages);
|
||||
|
||||
return (
|
||||
<BubbleContainer $isUser={isUser}>
|
||||
<BubbleContainer $isUser={isUser} $isExpired={isExpired}>
|
||||
{!isUser && (
|
||||
<MessageAvatar $isUser={isUser}>
|
||||
<MessageAvatar $isUser={isUser} $isExpired={isExpired}>
|
||||
<SmartToyIcon />
|
||||
</MessageAvatar>
|
||||
)}
|
||||
|
||||
<MessageContent $isUser={isUser} $isStreaming={isStreaming}>
|
||||
<MessageContent $isUser={isUser} $isStreaming={isStreaming} $isExpired={isExpired}>
|
||||
<MessageRenderer message={message} isUser={isUser} />
|
||||
</MessageContent>
|
||||
|
||||
{isUser && (
|
||||
<MessageAvatar $isUser={isUser}>
|
||||
<MessageAvatar $isUser={isUser} $isExpired={isExpired}>
|
||||
<PersonIcon />
|
||||
</MessageAvatar>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import Tooltip from '@mui/material/Tooltip';
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { PreviewProgressBar } from './PreviewProgressBar';
|
||||
import { useAgentChatStore } from '../../../Agent/store/agentChatStore/index';
|
||||
import { EditView } from './EditView';
|
||||
import { PreviewProgressBar } from './PreviewProgressBar';
|
||||
import { PreviewTabsView } from './PreviewTabsView';
|
||||
|
||||
interface PromptPreviewDialogProps {
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ export class AgentDefinitionService implements IAgentDefinitionService {
|
|||
required: ['workspaceId', 'query'],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return Promise.resolve([wikiSearchTool]);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get available tools: ${error as Error}`);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* Tests the complete workflow: tool list injection -> AI response -> tool execution -> next round
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { AgentInstanceMessage } from '../../interface';
|
||||
|
||||
import { WikiChannel } from '@/constants/channels';
|
||||
import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility';
|
||||
|
|
@ -104,6 +105,11 @@ describe('WikiSearch Plugin Integration', () => {
|
|||
|
||||
// Phase 1: Tool List Injection
|
||||
const promptContext = {
|
||||
handlerContext: {
|
||||
agent: { id: 'test', messages: [], agentDefId: 'test', status: { state: 'working' as const, modified: new Date() }, created: new Date() },
|
||||
agentDef: { id: 'test', name: 'test' },
|
||||
isCancelled: () => false,
|
||||
},
|
||||
pluginConfig: wikiPlugin as any, // Cast to avoid type complexity in tests
|
||||
prompts,
|
||||
messages: [
|
||||
|
|
@ -207,10 +213,14 @@ describe('WikiSearch Plugin Integration', () => {
|
|||
|
||||
// Verify tool results were set up for next round
|
||||
expect(responseContext.actions.yieldNextRoundTo).toBe('self');
|
||||
expect(responseContext.actions.newUserMessage).toContain('<functions_result>');
|
||||
expect(responseContext.actions.newUserMessage).toContain('Tool: wiki-search');
|
||||
expect(responseContext.actions.newUserMessage).toContain('Important Note 1');
|
||||
expect(responseContext.actions.newUserMessage).toContain('Important Note 2');
|
||||
|
||||
// Verify tool result message was added to agent history
|
||||
expect(responseContext.handlerContext.agent.messages.length).toBeGreaterThan(0);
|
||||
const toolResultMessage = responseContext.handlerContext.agent.messages[responseContext.handlerContext.agent.messages.length - 1] as AgentInstanceMessage;
|
||||
expect(toolResultMessage.role).toBe('user');
|
||||
expect(toolResultMessage.content).toContain('<functions_result>');
|
||||
expect(toolResultMessage.content).toContain('Tool: wiki-search');
|
||||
expect(toolResultMessage.content).toContain('Important Note 1');
|
||||
});
|
||||
|
||||
it('should handle errors in wiki search gracefully', async () => {
|
||||
|
|
@ -274,8 +284,13 @@ describe('WikiSearch Plugin Integration', () => {
|
|||
|
||||
// Should still set up next round even with error
|
||||
expect(responseContext.actions.yieldNextRoundTo).toBe('self');
|
||||
expect(responseContext.actions.newUserMessage).toContain('Error:');
|
||||
expect(responseContext.actions.newUserMessage).toContain('does not exist');
|
||||
|
||||
// Verify error message was added to agent history
|
||||
expect(responseContext.handlerContext.agent.messages.length).toBeGreaterThan(0);
|
||||
const errorResultMessage = responseContext.handlerContext.agent.messages[responseContext.handlerContext.agent.messages.length - 1] as AgentInstanceMessage;
|
||||
expect(errorResultMessage.role).toBe('user');
|
||||
expect(errorResultMessage.content).toContain('Error:');
|
||||
expect(errorResultMessage.content).toContain('does not exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { logger } from '@services/libs/log';
|
|||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import { merge } from 'lodash';
|
||||
import { AgentInstanceLatestStatus, AgentInstanceMessage, IAgentInstanceService } from '../interface';
|
||||
import { createHandlerHooks, registerBuiltInHandlerPlugins } from '../plugins';
|
||||
import { createHandlerHooks, registerAllBuiltInPlugins } from '../plugins';
|
||||
import { YieldNextRoundTarget } from '../plugins/types';
|
||||
import { AgentPromptDescription, AiAPIConfig, HandlerConfig } from '../promptConcat/promptConcatSchema';
|
||||
import { responseConcat } from '../promptConcat/responseConcat';
|
||||
import { getFinalPromptResult } from '../promptConcat/utils';
|
||||
|
|
@ -34,7 +35,7 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
|||
|
||||
// Create and register handler hooks
|
||||
const handlerHooks = createHandlerHooks();
|
||||
registerBuiltInHandlerPlugins(handlerHooks);
|
||||
registerAllBuiltInPlugins(handlerHooks);
|
||||
|
||||
// Log the start of handler execution with context information
|
||||
logger.debug('Starting prompt handler execution', {
|
||||
|
|
@ -108,7 +109,7 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
|||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
// Generate AI response
|
||||
// Function to process a single LLM call with retry support
|
||||
async function* processLLMCall(_userMessage: string): AsyncGenerator<AgentInstanceLatestStatus> {
|
||||
async function* processLLMCall(): AsyncGenerator<AgentInstanceLatestStatus> {
|
||||
try {
|
||||
// Delegate prompt concatenation to plugin system
|
||||
// Re-generate prompts to trigger middleware (including retrievalAugmentedGenerationHandler)
|
||||
|
|
@ -163,24 +164,40 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
|||
});
|
||||
|
||||
// Delegate final response processing to handler hooks
|
||||
await handlerHooks.responseComplete.promise({
|
||||
const responseCompleteContext = {
|
||||
handlerContext: context,
|
||||
response,
|
||||
requestId: currentRequestId,
|
||||
isFinal: true,
|
||||
});
|
||||
actions: undefined as { yieldNextRoundTo?: 'self' | 'human'; newUserMessage?: string } | undefined,
|
||||
};
|
||||
|
||||
await handlerHooks.responseComplete.promise(responseCompleteContext);
|
||||
|
||||
// Check if responseComplete hooks set yieldNextRoundTo
|
||||
let yieldNextRoundFromHooks: YieldNextRoundTarget | undefined;
|
||||
if (responseCompleteContext.actions?.yieldNextRoundTo) {
|
||||
yieldNextRoundFromHooks = responseCompleteContext.actions.yieldNextRoundTo;
|
||||
logger.debug('Response complete hooks triggered yield next round', {
|
||||
method: 'processLLMCall',
|
||||
retryCount,
|
||||
yieldNextRoundTo: yieldNextRoundFromHooks,
|
||||
});
|
||||
}
|
||||
|
||||
// Delegate response processing to plugin system
|
||||
// Plugins can set yieldNextRoundTo actions to control conversation flow
|
||||
const processedResult = await responseConcat(agentPromptDescription, response.content, context, context.agent.messages);
|
||||
|
||||
// Handle control flow based on plugin decisions
|
||||
if (processedResult.needsNewLLMCall) {
|
||||
// Handle control flow based on plugin decisions or responseComplete hooks
|
||||
const shouldContinue = processedResult.yieldNextRoundTo === 'self' || yieldNextRoundFromHooks === 'self';
|
||||
if (shouldContinue) {
|
||||
// Control transfer: Continue with AI (yieldNextRoundTo: 'self')
|
||||
logger.debug('Response processing triggered new LLM call', {
|
||||
method: 'processLLMCall',
|
||||
hasNewUserMessage: !!processedResult.newUserMessage,
|
||||
retryCount,
|
||||
fromResponseConcat: processedResult.yieldNextRoundTo,
|
||||
fromResponseCompleteHooks: yieldNextRoundFromHooks,
|
||||
});
|
||||
|
||||
// Prevent infinite loops with retry limit
|
||||
|
|
@ -200,18 +217,15 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
|||
// Yield current response as working state
|
||||
yield working(processedResult.processedResponse, context, currentRequestId);
|
||||
|
||||
// Continue with new round
|
||||
// The necessary messages should already be added by plugins
|
||||
logger.debug('Continuing with next round', {
|
||||
method: 'basicPromptConcatHandler',
|
||||
agentId: context.agent.id,
|
||||
messageCount: context.agent.messages.length,
|
||||
});
|
||||
|
||||
// Continue with new round - use provided message or last user message
|
||||
const nextUserMessage = processedResult.newUserMessage || lastUserMessage?.content;
|
||||
if (nextUserMessage) {
|
||||
yield* processLLMCall(nextUserMessage);
|
||||
} else {
|
||||
logger.warn('No message provided for continue round', {
|
||||
method: 'basicPromptConcatHandler',
|
||||
agentId: context.agent.id,
|
||||
});
|
||||
yield completed(processedResult.processedResponse, context, currentRequestId);
|
||||
}
|
||||
yield* processLLMCall();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +268,7 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
|||
}
|
||||
|
||||
// Start processing with the initial user message
|
||||
yield* processLLMCall(lastUserMessage.content);
|
||||
yield* processLLMCall();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error('Error processing prompt', {
|
||||
|
|
|
|||
|
|
@ -122,15 +122,7 @@
|
|||
"pluginId": "autoReply",
|
||||
"autoReplyParam": {
|
||||
"targetId": "default-response",
|
||||
"text": "继续工作直到你自己觉得工作已经完全完成。",
|
||||
"trigger": {
|
||||
"model": {
|
||||
"preset": "defaultLite",
|
||||
"system": "你是一个对话分析师,你将对目前的对话进行分析,确定AI的回复是否完全解决了用户的问题。一般来说,如果AI的回复只解决了用户提出的问题的一个子问题,那么就需要继续工作。请注意,你需要结合上下文进行分析,而不是仅仅依赖于AI的回复内容。请不要做出1或者0之外的回答,回答中也不要包括其他文本内容。",
|
||||
"user": "用户的消息内容为:<<input>>。如果此用户消息需要进行网络搜索,请回答1,如果不需要,请回答0。请不要做出1或者0之外的回答,回答中也不要包括其他文本内容。"
|
||||
}
|
||||
},
|
||||
"maxAutoReply": 5
|
||||
"text": "继续工作直到你自己觉得工作已经完全完成。如果根据之前的对话你认为任务已完成,则总结并结束对话。如果任务还未完成,你可以继续调用工具。"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { DataSource, Repository } from 'typeorm';
|
|||
import { IAgentDefinitionService } from '@services/agentDefinition/interface';
|
||||
import { basicPromptConcatHandler } from '@services/agentInstance/buildInAgentHandlers/basicPromptConcatHandler';
|
||||
import { AgentHandler, AgentHandlerContext } from '@services/agentInstance/buildInAgentHandlers/type';
|
||||
import { createHandlerHooks, initializePluginSystem, registerBuiltInHandlerPlugins } from '@services/agentInstance/plugins';
|
||||
import { createHandlerHooks, initializePluginSystem, registerAllBuiltInPlugins } from '@services/agentInstance/plugins';
|
||||
import { promptConcatStream, PromptConcatStreamState } from '@services/agentInstance/promptConcat/promptConcat';
|
||||
import { AgentPromptDescription } from '@services/agentInstance/promptConcat/promptConcatSchema';
|
||||
import { promptConcatHandlerConfigJsonSchema } from '@services/agentInstance/promptConcat/promptConcatSchema/jsonSchema';
|
||||
|
|
@ -21,7 +21,7 @@ import serviceIdentifier from '@services/serviceIdentifier';
|
|||
import { IWikiService } from '@services/wiki/interface';
|
||||
|
||||
import { AgentInstance, AgentInstanceLatestStatus, AgentInstanceMessage, IAgentInstanceService } from './interface';
|
||||
import { AGENT_INSTANCE_FIELDS, createAgentInstanceData, createAgentMessage, MESSAGE_FIELDS } from './utilities';
|
||||
import { AGENT_INSTANCE_FIELDS, createAgentInstanceData, createAgentMessage, MESSAGE_FIELDS, toDatabaseCompatibleInstance, toDatabaseCompatibleMessage } from './utilities';
|
||||
|
||||
@injectable()
|
||||
export class AgentInstanceService implements IAgentInstanceService {
|
||||
|
|
@ -78,7 +78,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
initializePluginSystem();
|
||||
|
||||
// Register built-in handler plugins
|
||||
registerBuiltInHandlerPlugins(this.handlerHooks);
|
||||
registerAllBuiltInPlugins(this.handlerHooks);
|
||||
|
||||
// Register basic prompt concatenation handler with its schema
|
||||
this.registerHandler('basicPromptConcatHandler', basicPromptConcatHandler, promptConcatHandlerConfigJsonSchema);
|
||||
|
|
@ -139,7 +139,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
const { instanceData, instanceId, now } = createAgentInstanceData(agentDef);
|
||||
|
||||
// Create and save entity
|
||||
const instanceEntity = this.agentInstanceRepository!.create(instanceData);
|
||||
const instanceEntity = this.agentInstanceRepository!.create(toDatabaseCompatibleInstance(instanceData));
|
||||
await this.agentInstanceRepository!.save(instanceEntity);
|
||||
logger.info(`Created agent instance: ${instanceId}`);
|
||||
|
||||
|
|
@ -232,9 +232,8 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
await this.agentMessageRepository!.save(existingMessage);
|
||||
} else {
|
||||
// Create new message
|
||||
const messageEntity = this.agentMessageRepository!.create(
|
||||
pick(message, MESSAGE_FIELDS) as AgentInstanceMessage,
|
||||
);
|
||||
const messageData = pick(message, MESSAGE_FIELDS) as AgentInstanceMessage;
|
||||
const messageEntity = this.agentMessageRepository!.create(toDatabaseCompatibleMessage(messageData));
|
||||
|
||||
await this.agentMessageRepository!.save(messageEntity);
|
||||
|
||||
|
|
@ -631,7 +630,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
public async saveUserMessage(userMessage: AgentInstanceMessage): Promise<void> {
|
||||
this.ensureRepositories();
|
||||
try {
|
||||
await this.agentMessageRepository!.save(this.agentMessageRepository!.create(userMessage));
|
||||
await this.agentMessageRepository!.save(this.agentMessageRepository!.create(toDatabaseCompatibleMessage(userMessage)));
|
||||
logger.debug('User message saved to database', {
|
||||
messageId: userMessage.id,
|
||||
agentId: userMessage.agentId,
|
||||
|
|
@ -691,14 +690,13 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
} else if (aid) {
|
||||
// Create new message if it doesn't exist and agentId provided
|
||||
// Create message using utility function
|
||||
const newMessage = messageRepo.create(
|
||||
createAgentMessage(messageId, aid, {
|
||||
role: msgData.role,
|
||||
content: msgData.content,
|
||||
contentType: msgData.contentType,
|
||||
metadata: msgData.metadata,
|
||||
}),
|
||||
);
|
||||
const messageData = createAgentMessage(messageId, aid, {
|
||||
role: msgData.role,
|
||||
content: msgData.content,
|
||||
contentType: msgData.contentType,
|
||||
metadata: msgData.metadata,
|
||||
});
|
||||
const newMessage = messageRepo.create(toDatabaseCompatibleMessage(messageData));
|
||||
|
||||
await messageRepo.save(newMessage);
|
||||
|
||||
|
|
@ -770,7 +768,20 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
return new Observable<PromptConcatStreamState>((observer) => {
|
||||
const processStream = async () => {
|
||||
try {
|
||||
const streamGenerator = promptConcatStream(promptDescription as AgentPromptDescription, messages);
|
||||
// Create a minimal handler context for prompt concatenation
|
||||
const handlerContext = {
|
||||
agent: {
|
||||
id: 'temp',
|
||||
messages,
|
||||
agentDefId: 'temp',
|
||||
status: { state: 'working' as const, modified: new Date() },
|
||||
created: new Date(),
|
||||
},
|
||||
agentDef: { id: 'temp', name: 'temp' },
|
||||
isCancelled: () => false,
|
||||
};
|
||||
|
||||
const streamGenerator = promptConcatStream(promptDescription as AgentPromptDescription, messages, handlerContext);
|
||||
for await (const state of streamGenerator) {
|
||||
observer.next(state);
|
||||
if (state.isComplete) {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,13 @@ export interface AgentInstanceMessage {
|
|||
metadata?: Record<string, unknown>;
|
||||
/** Whether this message should be hidden from UI/history (default: false) */
|
||||
hidden?: boolean;
|
||||
/**
|
||||
* Duration in rounds that this message should be included in AI context
|
||||
* When set to a number > 0, the message will only be sent to AI for that many rounds from current position
|
||||
* undefined/null means the message persists in AI context indefinitely (default behavior)
|
||||
* 0 means the message is excluded from AI context immediately but remains visible in UI
|
||||
*/
|
||||
duration?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Tests for the enhanced plugin system after refactoring
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createHandlerHooks, registerBuiltInHandlerPlugins } from '../index';
|
||||
import { createHandlerHooks, registerAllBuiltInPlugins } from '../index';
|
||||
|
||||
describe('Plugin System', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -22,12 +22,12 @@ describe('Plugin System', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('registerBuiltInHandlerPlugins', () => {
|
||||
describe('registerAllBuiltInPlugins', () => {
|
||||
it('should register plugins without throwing', () => {
|
||||
const hooks = createHandlerHooks();
|
||||
|
||||
expect(() => {
|
||||
registerBuiltInHandlerPlugins(hooks);
|
||||
registerAllBuiltInPlugins(hooks);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* Tests for Wiki Search plugin
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { AgentInstanceMessage } from '../../interface';
|
||||
|
||||
import { WikiChannel } from '@/constants/channels';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
|
|
@ -111,6 +112,11 @@ describe('Wiki Search Plugin', () => {
|
|||
];
|
||||
|
||||
const context: PromptConcatHookContext = {
|
||||
handlerContext: {
|
||||
agent: { id: 'test', messages: [], agentDefId: 'test', status: { state: 'working' as const, modified: new Date() }, created: new Date() },
|
||||
agentDef: { id: 'test', name: 'test' },
|
||||
isCancelled: () => false,
|
||||
},
|
||||
pluginConfig: wikiPlugin,
|
||||
prompts: prompts,
|
||||
messages,
|
||||
|
|
@ -269,11 +275,15 @@ describe('Wiki Search Plugin', () => {
|
|||
|
||||
// Verify that the search was executed and results were set up for next round
|
||||
expect(context.actions.yieldNextRoundTo).toBe('self');
|
||||
expect(context.actions.newUserMessage).toContain('<functions_result>');
|
||||
expect(context.actions.newUserMessage).toContain('Tool: wiki-search');
|
||||
expect(context.actions.newUserMessage).toContain('Important Note 1');
|
||||
expect(context.actions.newUserMessage).toContain('Important Note 2');
|
||||
expect(context.actions.newUserMessage).toContain('Content of Important Note 1');
|
||||
|
||||
// Verify tool result message was added to agent history
|
||||
expect(handlerContext.agent.messages.length).toBeGreaterThan(0);
|
||||
const toolResultMessage = handlerContext.agent.messages[handlerContext.agent.messages.length - 1] as AgentInstanceMessage;
|
||||
expect(toolResultMessage.role).toBe('user');
|
||||
expect(toolResultMessage.content).toContain('<functions_result>');
|
||||
expect(toolResultMessage.content).toContain('Tool: wiki-search');
|
||||
expect(toolResultMessage.content).toContain('Important Note 1');
|
||||
expect(toolResultMessage.metadata?.isToolResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle wiki search errors gracefully', async () => {
|
||||
|
|
@ -326,9 +336,16 @@ describe('Wiki Search Plugin', () => {
|
|||
|
||||
// Should still set up next round with error message
|
||||
expect(context.actions.yieldNextRoundTo).toBe('self');
|
||||
expect(context.actions.newUserMessage).toContain('<functions_result>');
|
||||
expect(context.actions.newUserMessage).toContain('Error:');
|
||||
expect(context.actions.newUserMessage).toContain('does not exist');
|
||||
|
||||
// Verify error message was added to agent history
|
||||
expect(handlerContext.agent.messages.length).toBeGreaterThan(0);
|
||||
const errorResultMessage = handlerContext.agent.messages[handlerContext.agent.messages.length - 1] as AgentInstanceMessage;
|
||||
expect(errorResultMessage.role).toBe('user');
|
||||
expect(errorResultMessage.content).toContain('<functions_result>');
|
||||
expect(errorResultMessage.content).toContain('Error:');
|
||||
expect(errorResultMessage.content).toContain('does not exist');
|
||||
expect(errorResultMessage.metadata?.isToolResult).toBe(true);
|
||||
expect(errorResultMessage.metadata?.isError).toBe(true);
|
||||
});
|
||||
|
||||
it('should skip execution when no tool call is detected', async () => {
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
/**
|
||||
* AI response history plugin
|
||||
* Handles AI response streaming updates and completion
|
||||
*/
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
|
||||
import { AgentInstanceMessage, IAgentInstanceService } from '../interface';
|
||||
import { AIResponseContext, PromptConcatPlugin } from './types';
|
||||
|
||||
/**
|
||||
* AI response history plugin
|
||||
* Manages AI response messages in conversation history during streaming and completion
|
||||
*/
|
||||
export const aiResponseHistoryPlugin: PromptConcatPlugin = (hooks) => {
|
||||
// Handle AI response updates (streaming)
|
||||
hooks.responseUpdate.tapAsync('aiResponseHistoryPlugin', (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { handlerContext, response } = context;
|
||||
|
||||
if (response.status === 'update' && response.content) {
|
||||
// Find or create AI response message
|
||||
let aiMessage = handlerContext.agent.messages.find(
|
||||
(message) => message.role === 'assistant' && !message.metadata?.isComplete,
|
||||
);
|
||||
|
||||
if (!aiMessage) {
|
||||
// Create new AI message for streaming updates
|
||||
aiMessage = {
|
||||
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: handlerContext.agent.id,
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
modified: new Date(),
|
||||
metadata: { isComplete: false },
|
||||
};
|
||||
handlerContext.agent.messages.push(aiMessage);
|
||||
} 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, handlerContext.agent.id);
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for message', {
|
||||
error: serviceError instanceof Error ? serviceError.message : String(serviceError),
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('AI response message updated', {
|
||||
messageId: aiMessage.id,
|
||||
contentLength: response.content.length,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('AI response history plugin error in responseUpdate', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle AI response completion
|
||||
hooks.responseComplete.tapAsync('aiResponseHistoryPlugin', (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { handlerContext, response } = context;
|
||||
|
||||
if (response.status === 'done' && response.content) {
|
||||
// Find and finalize AI response message
|
||||
const aiMessage = handlerContext.agent.messages.find(
|
||||
(message) => message.role === 'assistant' && !message.metadata?.isComplete,
|
||||
);
|
||||
|
||||
if (aiMessage) {
|
||||
// Mark as complete and update final content
|
||||
aiMessage.content = response.content;
|
||||
aiMessage.modified = new Date();
|
||||
aiMessage.metadata = { ...aiMessage.metadata, isComplete: true };
|
||||
|
||||
// Final UI update
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for completed message', {
|
||||
error: serviceError instanceof Error ? serviceError.message : String(serviceError),
|
||||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('AI response message completed', {
|
||||
messageId: aiMessage.id,
|
||||
finalContentLength: response.content.length,
|
||||
});
|
||||
} else {
|
||||
// Create final message if streaming message wasn't found
|
||||
const finalMessage: AgentInstanceMessage = {
|
||||
id: `ai-response-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: handlerContext.agent.id,
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
modified: new Date(),
|
||||
metadata: { isComplete: true },
|
||||
};
|
||||
handlerContext.agent.messages.push(finalMessage);
|
||||
|
||||
// UI update for final message
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
agentInstanceService.debounceUpdateMessage(finalMessage, handlerContext.agent.id);
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for final message', {
|
||||
error: serviceError instanceof Error ? serviceError.message : String(serviceError),
|
||||
messageId: finalMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('AI response message created as final', {
|
||||
messageId: finalMessage.id,
|
||||
contentLength: response.content.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('AI response history plugin error in responseComplete', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,19 +1,9 @@
|
|||
import { logger } from '@services/libs/log';
|
||||
import { AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable';
|
||||
import {
|
||||
AgentResponse,
|
||||
PromptConcatHooks,
|
||||
PromptConcatHookContext,
|
||||
PromptConcatPlugin,
|
||||
ResponseHookContext,
|
||||
UserMessageContext,
|
||||
AgentStatusContext,
|
||||
ToolExecutionContext,
|
||||
AIResponseContext
|
||||
} from './types';
|
||||
import { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatPlugin, ResponseHookContext } from './types';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { AgentResponse, PromptConcatHooks, PromptConcatHookContext, PromptConcatPlugin, ResponseHookContext };
|
||||
export type { AgentResponse, PromptConcatHookContext, PromptConcatHooks, PromptConcatPlugin, ResponseHookContext };
|
||||
|
||||
/**
|
||||
* Registry for built-in plugins
|
||||
|
|
@ -28,40 +18,6 @@ export function registerBuiltInPlugin(pluginId: string, plugin: PromptConcatPlug
|
|||
logger.debug(`Registered built-in plugin: ${pluginId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all built-in plugins
|
||||
*/
|
||||
export function registerAllBuiltInPlugins(): void {
|
||||
// Use dynamic imports to avoid circular dependency issues
|
||||
Promise.all([
|
||||
import('./promptPlugins'),
|
||||
import('./responsePlugins'),
|
||||
import('./wikiSearchPlugin'),
|
||||
]).then(([promptPluginsModule, responsePluginsModule, wikiSearchModule]) => {
|
||||
// Prompt processing plugins
|
||||
registerBuiltInPlugin('fullReplacement', promptPluginsModule.fullReplacementPlugin);
|
||||
registerBuiltInPlugin('dynamicPosition', promptPluginsModule.dynamicPositionPlugin);
|
||||
registerBuiltInPlugin('modelContextProtocol', promptPluginsModule.modelContextProtocolPlugin);
|
||||
|
||||
// Wiki search plugin - handles both prompt and response processing
|
||||
registerBuiltInPlugin('wikiSearch', wikiSearchModule.wikiSearchPlugin);
|
||||
|
||||
// Response processing plugins
|
||||
registerBuiltInPlugin('autoReply', responsePluginsModule.autoReplyPlugin);
|
||||
|
||||
logger.debug('All built-in plugins registered successfully');
|
||||
}).catch((error: unknown) => {
|
||||
logger.error('Failed to register built-in plugins:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin system
|
||||
*/
|
||||
export function initializePluginSystem(): void {
|
||||
registerAllBuiltInPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unified hooks instance for the complete plugin system
|
||||
*/
|
||||
|
|
@ -81,24 +37,67 @@ export function createHandlerHooks(): PromptConcatHooks {
|
|||
}
|
||||
|
||||
/**
|
||||
* Register built-in handler plugins
|
||||
* Register all built-in plugins to hooks
|
||||
*/
|
||||
export function registerBuiltInHandlerPlugins(hooks: PromptConcatHooks): void {
|
||||
// Import and register persistence plugin first (handles database operations)
|
||||
import('./persistencePlugin').then(module => {
|
||||
module.persistencePlugin(hooks);
|
||||
logger.debug('Registered persistencePlugin');
|
||||
}).catch((error: unknown) => {
|
||||
logger.error('Failed to register persistencePlugin:', error);
|
||||
});
|
||||
export function registerAllBuiltInPlugins(hooks?: PromptConcatHooks): void {
|
||||
// If hooks provided, register plugins directly to hooks for immediate use
|
||||
if (hooks) {
|
||||
// Import and register message management plugin first (handles database operations, message persistence, and UI updates)
|
||||
import('./messageManagementPlugin').then(module => {
|
||||
module.messageManagementPlugin(hooks);
|
||||
logger.debug('Registered messageManagementPlugin to hooks');
|
||||
}).catch((error: unknown) => {
|
||||
logger.error('Failed to register messageManagementPlugin to hooks:', error);
|
||||
});
|
||||
|
||||
// Import and register wiki search handler plugin
|
||||
import('./wikiSearchPlugin').then(module => {
|
||||
module.wikiSearchPlugin(hooks);
|
||||
logger.debug('Registered wikiSearchPlugin');
|
||||
}).catch((error: unknown) => {
|
||||
logger.error('Failed to register wikiSearchPlugin:', error);
|
||||
});
|
||||
// Import and register wiki search handler plugin
|
||||
import('./wikiSearchPlugin').then(module => {
|
||||
module.wikiSearchPlugin(hooks);
|
||||
logger.debug('Registered wikiSearchPlugin to hooks');
|
||||
}).catch((error: unknown) => {
|
||||
logger.error('Failed to register wikiSearchPlugin to hooks:', error);
|
||||
});
|
||||
|
||||
logger.debug('Built-in handler plugins registration initiated');
|
||||
// Temporarily disable auto reply plugin to debug
|
||||
// import('./responsePlugins').then(module => {
|
||||
// module.autoReplyPlugin(hooks);
|
||||
// logger.debug('Registered autoReplyPlugin to hooks');
|
||||
// }).catch((error: unknown) => {
|
||||
// logger.error('Failed to register autoReplyPlugin to hooks:', error);
|
||||
// });
|
||||
|
||||
logger.debug('Built-in plugins registration to hooks initiated');
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, register plugins to global registry for plugin discovery
|
||||
Promise.all([
|
||||
import('./promptPlugins'),
|
||||
import('./responsePlugins'),
|
||||
import('./wikiSearchPlugin'),
|
||||
import('./messageManagementPlugin'),
|
||||
]).then(([promptPluginsModule, _responsePluginsModule, wikiSearchModule, messageManagementModule]) => {
|
||||
// Message management plugin (should be first to handle message persistence and UI updates)
|
||||
registerBuiltInPlugin('messageManagement', messageManagementModule.messageManagementPlugin);
|
||||
|
||||
// Prompt processing plugins
|
||||
registerBuiltInPlugin('fullReplacement', promptPluginsModule.fullReplacementPlugin);
|
||||
|
||||
// Wiki search plugin - handles both prompt and response processing
|
||||
registerBuiltInPlugin('wikiSearch', wikiSearchModule.wikiSearchPlugin);
|
||||
|
||||
// Temporarily disable auto reply plugin
|
||||
// registerBuiltInPlugin('autoReply', responsePluginsModule.autoReplyPlugin);
|
||||
|
||||
logger.debug('All built-in plugins registered to global registry successfully');
|
||||
}).catch((error: unknown) => {
|
||||
logger.error('Failed to register built-in plugins to global registry:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin system
|
||||
*/
|
||||
export function initializePluginSystem(): void {
|
||||
registerAllBuiltInPlugins();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
/**
|
||||
* Persistence plugin for database operations and UI updates
|
||||
* Handles user message storage, agent status updates, AI response management and UI synchronization
|
||||
* 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 { IAgentInstanceService } from '../interface';
|
||||
import type { IAgentInstanceService } from '../interface';
|
||||
import { createAgentMessage } from '../utilities';
|
||||
import { AgentStatusContext, AIResponseContext, PromptConcatPlugin, UserMessageContext } from './types';
|
||||
import type { AgentStatusContext, AIResponseContext, PromptConcatPlugin, ToolExecutionContext, UserMessageContext } from './types';
|
||||
|
||||
/**
|
||||
* Persistence plugin
|
||||
* Manages database operations, message history and UI updates for all agent interactions
|
||||
* Message management plugin
|
||||
* Handles all message-related operations: persistence, streaming, UI updates, and duration-based filtering
|
||||
*/
|
||||
export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
||||
export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||
// Handle user message persistence
|
||||
hooks.userMessageReceived.tapAsync('persistencePlugin', async (context: UserMessageContext, callback) => {
|
||||
hooks.userMessageReceived.tapAsync('messageManagementPlugin', async (context: UserMessageContext, callback) => {
|
||||
try {
|
||||
const { handlerContext, content, messageId } = context;
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
content: content.text,
|
||||
contentType: 'text/plain',
|
||||
metadata: content.file ? { file: content.file } : undefined,
|
||||
duration: undefined, // User messages persist indefinitely by default
|
||||
});
|
||||
|
||||
// Get the agent instance service to access repositories
|
||||
|
|
@ -44,7 +46,7 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Persistence plugin error in userMessageReceived', {
|
||||
logger.error('Message management plugin error in userMessageReceived', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
messageId: context.messageId,
|
||||
agentId: context.handlerContext.agent.id,
|
||||
|
|
@ -54,7 +56,7 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
});
|
||||
|
||||
// Handle agent status persistence
|
||||
hooks.agentStatusChanged.tapAsync('persistencePlugin', async (context: AgentStatusContext, callback) => {
|
||||
hooks.agentStatusChanged.tapAsync('messageManagementPlugin', async (context: AgentStatusContext, callback) => {
|
||||
try {
|
||||
const { handlerContext, status } = context;
|
||||
|
||||
|
|
@ -76,7 +78,7 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Persistence plugin error in agentStatusChanged', {
|
||||
logger.error('Message management plugin error in agentStatusChanged', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
agentId: context.handlerContext.agent.id,
|
||||
status: context.status,
|
||||
|
|
@ -86,7 +88,7 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
});
|
||||
|
||||
// Handle AI response updates during streaming
|
||||
hooks.responseUpdate.tapAsync('persistencePlugin', (context: AIResponseContext, callback) => {
|
||||
hooks.responseUpdate.tapAsync('messageManagementPlugin', (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { handlerContext, response } = context;
|
||||
|
||||
|
|
@ -105,6 +107,7 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
content: response.content,
|
||||
modified: new Date(),
|
||||
metadata: { isComplete: false },
|
||||
duration: undefined, // AI responses persist indefinitely by default
|
||||
};
|
||||
handlerContext.agent.messages.push(aiMessage);
|
||||
} else {
|
||||
|
|
@ -123,16 +126,11 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
messageId: aiMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('AI response message updated during streaming', {
|
||||
messageId: aiMessage.id,
|
||||
contentLength: response.content.length,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Persistence plugin error in responseUpdate', {
|
||||
logger.error('Message management plugin error in responseUpdate', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
callback();
|
||||
|
|
@ -140,7 +138,7 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
});
|
||||
|
||||
// Handle AI response completion
|
||||
hooks.responseComplete.tapAsync('persistencePlugin', async (context: AIResponseContext, callback) => {
|
||||
hooks.responseComplete.tapAsync('messageManagementPlugin', async (context: AIResponseContext, callback) => {
|
||||
try {
|
||||
const { handlerContext, response } = context;
|
||||
|
||||
|
|
@ -163,7 +161,10 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
role: 'assistant',
|
||||
content: response.content,
|
||||
modified: new Date(),
|
||||
metadata: { isComplete: true },
|
||||
metadata: {
|
||||
isComplete: true,
|
||||
},
|
||||
duration: undefined, // Default duration for AI responses
|
||||
};
|
||||
handlerContext.agent.messages.push(aiMessage);
|
||||
}
|
||||
|
|
@ -192,7 +193,47 @@ export const persistencePlugin: PromptConcatPlugin = (hooks) => {
|
|||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Persistence plugin error in responseComplete', {
|
||||
logger.error('Message management plugin error in responseComplete', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle tool result messages persistence and UI updates
|
||||
hooks.toolExecuted.tapAsync('messageManagementPlugin', (context: ToolExecutionContext, callback) => {
|
||||
try {
|
||||
const { handlerContext } = context;
|
||||
|
||||
// Update UI for any newly added messages with duration settings
|
||||
const newMessages = handlerContext.agent.messages.filter(
|
||||
(message) => message.metadata?.isToolResult && !message.metadata.uiUpdated,
|
||||
);
|
||||
|
||||
for (const message of newMessages) {
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
agentInstanceService.debounceUpdateMessage(message, handlerContext.agent.id);
|
||||
// Mark as UI updated to avoid duplicate updates
|
||||
message.metadata = { ...message.metadata, uiUpdated: true };
|
||||
} catch (serviceError) {
|
||||
logger.warn('Failed to update UI for tool result message', {
|
||||
error: serviceError instanceof Error ? serviceError.message : String(serviceError),
|
||||
messageId: message.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
logger.debug('Tool result messages UI updated', {
|
||||
count: newMessages.length,
|
||||
messageIds: newMessages.map(m => m.id),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Message management plugin error in toolExecuted', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
callback();
|
||||
|
|
@ -6,7 +6,8 @@ import { logger } from '@services/libs/log';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { findPromptById } from '../promptConcat/promptConcat';
|
||||
import { IPrompt } from '../promptConcat/promptConcatSchema';
|
||||
import { AgentResponse, ResponseHookContext, PromptConcatPlugin } from './types';
|
||||
import { filterMessagesByDuration } from '../utilities/messageDurationFilter';
|
||||
import { AgentResponse, PromptConcatPlugin, ResponseHookContext } from './types';
|
||||
|
||||
/**
|
||||
* Full replacement plugin
|
||||
|
|
@ -37,14 +38,16 @@ export const fullReplacementPlugin: PromptConcatPlugin = (hooks) => {
|
|||
// Get all messages except the last one which is the user message
|
||||
const messagesCopy = cloneDeep(messages);
|
||||
messagesCopy.pop(); // Last message is the user message
|
||||
const history = messagesCopy; // Remaining messages are history
|
||||
|
||||
// Apply duration filtering to exclude expired messages from AI context
|
||||
const filteredHistory = filterMessagesByDuration(messagesCopy);
|
||||
|
||||
switch (sourceType) {
|
||||
case 'historyOfSession':
|
||||
if (history.length > 0) {
|
||||
// Insert history messages as Prompt children (full Prompt type)
|
||||
if (filteredHistory.length > 0) {
|
||||
// Insert filtered history messages as Prompt children (full Prompt type)
|
||||
found.prompt.children = [];
|
||||
history.forEach((message, index: number) => {
|
||||
filteredHistory.forEach((message, index: number) => {
|
||||
// Use the role type from Prompt
|
||||
type PromptRole = NonNullable<IPrompt['role']>;
|
||||
const role: PromptRole = message.role === 'agent'
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
/**
|
||||
* Response processing plugins
|
||||
*/
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import { ResponseHookContext, PromptConcatPlugin } from './types';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
import type { IAgentInstanceService } from '../interface';
|
||||
import type { AgentInstanceMessage } from '../interface';
|
||||
import { PromptConcatPlugin, ResponseHookContext } from './types';
|
||||
|
||||
/**
|
||||
* Auto reply plugin
|
||||
* Automatically generates follow-up responses
|
||||
*/
|
||||
export const autoReplyPlugin: PromptConcatPlugin = (hooks) => {
|
||||
hooks.postProcess.tapAsync('autoReplyPlugin', (context, callback) => {
|
||||
// Map to store auto reply configs by agent ID
|
||||
const autoReplyConfigs = new Map<string, { text: string; targetId: string }>();
|
||||
|
||||
// First pass: Mark for auto reply in postProcess
|
||||
hooks.postProcess.tapAsync('autoReplyPlugin-mark', (context, callback) => {
|
||||
const { pluginConfig } = context as ResponseHookContext;
|
||||
|
||||
if (pluginConfig.pluginId !== 'autoReply' || !pluginConfig.autoReplyParam) {
|
||||
|
|
@ -17,28 +25,107 @@ export const autoReplyPlugin: PromptConcatPlugin = (hooks) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { targetId, text, maxAutoReply = 5 } = pluginConfig.autoReplyParam;
|
||||
const { targetId, text } = pluginConfig.autoReplyParam;
|
||||
|
||||
try {
|
||||
// Auto reply is always triggered since we removed trigger conditions
|
||||
logger.info('Auto reply plugin triggered', {
|
||||
logger.debug('Auto reply plugin triggered', {
|
||||
targetId,
|
||||
text: text.substring(0, 100) + '...',
|
||||
maxAutoReply,
|
||||
pluginId: pluginConfig.id,
|
||||
});
|
||||
|
||||
// Set actions to continue round with custom user message
|
||||
// Set actions to continue round
|
||||
const responseContext = context as ResponseHookContext;
|
||||
if (!responseContext.actions) {
|
||||
responseContext.actions = {};
|
||||
}
|
||||
responseContext.actions.yieldNextRoundTo = 'self'; // Continue with AI
|
||||
responseContext.actions.newUserMessage = text; // Use custom message
|
||||
|
||||
// Store config for responseComplete hook (using a unique key based on context)
|
||||
const contextKey = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
autoReplyConfigs.set(contextKey, { text, targetId });
|
||||
|
||||
// Store the key in metadata for retrieval
|
||||
responseContext.metadata = {
|
||||
...responseContext.metadata,
|
||||
autoReplyKey: contextKey,
|
||||
};
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Auto reply plugin error', error);
|
||||
logger.error('Auto reply plugin error in postProcess', error);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Second pass: Add auto reply message in responseComplete
|
||||
hooks.responseComplete.tapAsync('autoReplyPlugin-execute', async (context, callback) => {
|
||||
try {
|
||||
const { handlerContext, response } = context;
|
||||
|
||||
if (response.status !== 'done' || !response.content) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find auto reply config by checking recent messages metadata
|
||||
let autoReplyConfig: { text: string; targetId: string } | undefined;
|
||||
let configKey: string | undefined;
|
||||
|
||||
// Check if any recent message has autoReplyKey in metadata
|
||||
for (const config of autoReplyConfigs.entries()) {
|
||||
configKey = config[0];
|
||||
autoReplyConfig = config[1];
|
||||
break; // Use the first/most recent config
|
||||
}
|
||||
|
||||
if (!autoReplyConfig || !configKey) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const { text, targetId } = autoReplyConfig;
|
||||
|
||||
// Add the auto reply message directly to agent history
|
||||
const autoReplyMessage: AgentInstanceMessage = {
|
||||
id: `auto-reply-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: handlerContext.agent.id,
|
||||
role: 'user',
|
||||
content: text,
|
||||
modified: new Date(),
|
||||
duration: 1, // Auto reply messages are only visible to AI for 1 round
|
||||
metadata: {
|
||||
isAutoReply: true,
|
||||
targetId,
|
||||
},
|
||||
};
|
||||
|
||||
handlerContext.agent.messages.push(autoReplyMessage);
|
||||
|
||||
// Save auto reply message to database
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(autoReplyMessage);
|
||||
} catch (saveError) {
|
||||
logger.warn('Failed to save auto reply message to database', {
|
||||
error: saveError instanceof Error ? saveError.message : String(saveError),
|
||||
messageId: autoReplyMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up the config
|
||||
autoReplyConfigs.delete(configKey);
|
||||
|
||||
logger.debug('Auto reply message added', {
|
||||
messageId: autoReplyMessage.id,
|
||||
content: text.substring(0, 100) + '...',
|
||||
agentId: handlerContext.agent.id,
|
||||
});
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.error('Auto reply plugin error in responseComplete', error);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,59 +1,100 @@
|
|||
import { ToolCallingMatch } from '@services/agentDefinition/interface';
|
||||
import type { AgentHandlerContext } from '@services/agentInstance/buildInAgentHandlers/type';
|
||||
import { AgentHandlerContext } from '@services/agentInstance/buildInAgentHandlers/type';
|
||||
import { AgentInstanceMessage } from '@services/agentInstance/interface';
|
||||
import { AIStreamResponse } from '@services/externalAPI/interface';
|
||||
import { AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable';
|
||||
import type { IPrompt, Plugin } from '../promptConcat/promptConcatSchema/';
|
||||
|
||||
/**
|
||||
* Context passed to plugin hooks
|
||||
*/
|
||||
export interface PromptConcatHookContext {
|
||||
/** Array of agent instance messages for context */
|
||||
messages: AgentInstanceMessage[];
|
||||
/** Current prompt tree */
|
||||
prompts: IPrompt[];
|
||||
/** Plugin configuration */
|
||||
pluginConfig: Plugin;
|
||||
/** Additional context data */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent response interface
|
||||
* Represents a structured response from an agent
|
||||
*/
|
||||
export interface AgentResponse {
|
||||
id: string;
|
||||
text?: string;
|
||||
enabled?: boolean;
|
||||
children?: AgentResponse[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Next round target options
|
||||
*/
|
||||
export type YieldNextRoundTarget = 'human' | 'self' | `agent:${string}`; // allows for future agent IDs like "agent:agent-id"
|
||||
|
||||
/**
|
||||
* Context for response processing hooks
|
||||
* Unified actions interface for all plugin hooks
|
||||
*/
|
||||
export interface ResponseHookContext extends PromptConcatHookContext {
|
||||
export interface PluginActions {
|
||||
/** Whether to yield next round to continue processing */
|
||||
yieldNextRoundTo?: YieldNextRoundTarget;
|
||||
/** New user message to append */
|
||||
newUserMessage?: string;
|
||||
/** Tool calling information */
|
||||
toolCalling?: ToolCallingMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base context interface for all plugin hooks
|
||||
*/
|
||||
export interface BasePluginContext {
|
||||
/** Handler context */
|
||||
handlerContext: AgentHandlerContext;
|
||||
/** Additional context data */
|
||||
metadata?: Record<string, unknown>;
|
||||
/** Actions set by plugins during processing */
|
||||
actions?: PluginActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for prompt processing hooks (processPrompts, finalizePrompts)
|
||||
*/
|
||||
export interface PromptConcatHookContext extends BasePluginContext {
|
||||
/** Array of agent instance messages for context */
|
||||
messages: AgentInstanceMessage[];
|
||||
/** Current prompt tree */
|
||||
prompts: IPrompt[];
|
||||
/** Plugin configuration */
|
||||
pluginConfig: Plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for post-processing hooks
|
||||
*/
|
||||
export interface PostProcessContext extends PromptConcatHookContext {
|
||||
/** LLM response text */
|
||||
llmResponse: string;
|
||||
responses: AgentResponse[];
|
||||
actions?: {
|
||||
yieldNextRoundTo?: YieldNextRoundTarget;
|
||||
newUserMessage?: string;
|
||||
toolCalling?: ToolCallingMatch;
|
||||
/** Processed agent responses */
|
||||
responses?: AgentResponse[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for AI response hooks (responseUpdate, responseComplete)
|
||||
*/
|
||||
export interface AIResponseContext extends BasePluginContext {
|
||||
/** AI streaming response */
|
||||
response: AIStreamResponse;
|
||||
/** Current request ID */
|
||||
requestId?: string;
|
||||
/** Whether this is the final response */
|
||||
isFinal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for user message hooks
|
||||
*/
|
||||
export interface UserMessageContext extends BasePluginContext {
|
||||
/** User message content */
|
||||
content: { text: string; file?: File };
|
||||
/** Generated message ID */
|
||||
messageId: string;
|
||||
/** Timestamp for the message */
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for agent status hooks
|
||||
*/
|
||||
export interface AgentStatusContext extends BasePluginContext {
|
||||
/** New status state */
|
||||
status: {
|
||||
state: 'working' | 'completed' | 'failed' | 'canceled';
|
||||
modified: Date;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool execution result context for handler hooks
|
||||
* Context for tool execution hooks
|
||||
*/
|
||||
export interface ToolExecutionContext {
|
||||
/** Handler context */
|
||||
handlerContext: AgentHandlerContext;
|
||||
export interface ToolExecutionContext extends BasePluginContext {
|
||||
/** Tool execution result */
|
||||
toolResult: {
|
||||
success: boolean;
|
||||
|
|
@ -72,44 +113,22 @@ export interface ToolExecutionContext {
|
|||
}
|
||||
|
||||
/**
|
||||
* AI Response context for streaming updates
|
||||
* Agent response interface
|
||||
* Represents a structured response from an agent
|
||||
*/
|
||||
export interface AIResponseContext {
|
||||
/** Handler context */
|
||||
handlerContext: AgentHandlerContext;
|
||||
/** AI streaming response */
|
||||
response: AIStreamResponse;
|
||||
/** Current request ID */
|
||||
requestId?: string;
|
||||
/** Whether this is the final response */
|
||||
isFinal: boolean;
|
||||
export interface AgentResponse {
|
||||
id: string;
|
||||
text?: string;
|
||||
enabled?: boolean;
|
||||
children?: AgentResponse[];
|
||||
}
|
||||
|
||||
/**
|
||||
* User message context for new message arrival
|
||||
* Context for response processing hooks (legacy support)
|
||||
*/
|
||||
export interface UserMessageContext {
|
||||
/** Handler context */
|
||||
handlerContext: AgentHandlerContext;
|
||||
/** User message content */
|
||||
content: { text: string; file?: File };
|
||||
/** Generated message ID */
|
||||
messageId: string;
|
||||
/** Timestamp for the message */
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent status context for status updates
|
||||
*/
|
||||
export interface AgentStatusContext {
|
||||
/** Handler context */
|
||||
handlerContext: AgentHandlerContext;
|
||||
/** New status state */
|
||||
status: {
|
||||
state: 'working' | 'completed' | 'failed' | 'canceled';
|
||||
modified: Date;
|
||||
};
|
||||
export interface ResponseHookContext extends PromptConcatHookContext {
|
||||
llmResponse: string;
|
||||
responses: AgentResponse[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -122,7 +141,7 @@ export interface PromptConcatHooks {
|
|||
/** Called to finalize prompts before LLM call */
|
||||
finalizePrompts: AsyncSeriesWaterfallHook<[PromptConcatHookContext]>;
|
||||
/** Called for post-processing after LLM response */
|
||||
postProcess: AsyncSeriesWaterfallHook<[PromptConcatHookContext & { llmResponse: string }]>;
|
||||
postProcess: AsyncSeriesWaterfallHook<[PostProcessContext]>;
|
||||
/** Called when user sends a new message */
|
||||
userMessageReceived: AsyncSeriesHook<[UserMessageContext]>;
|
||||
/** Called when agent status changes */
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { z } from 'zod/v4';
|
|||
|
||||
import { WikiChannel } from '@/constants/channels';
|
||||
import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility';
|
||||
import type { IAgentInstanceService } from '@services/agentInstance/interface';
|
||||
import { container } from '@services/container';
|
||||
import { logger } from '@services/libs/log';
|
||||
import serviceIdentifier from '@services/serviceIdentifier';
|
||||
|
|
@ -14,9 +15,10 @@ import { IWorkspaceService } from '@services/workspaces/interface';
|
|||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||
import type { ITiddlerFields } from 'tiddlywiki';
|
||||
|
||||
import type { AgentInstanceMessage } from '../interface';
|
||||
import { findPromptById } from '../promptConcat/promptConcat';
|
||||
import type { IPrompt } from '../promptConcat/promptConcatSchema';
|
||||
import type { ResponseHookContext, PromptConcatPlugin } from './types';
|
||||
import type { AIResponseContext, PromptConcatPlugin } from './types';
|
||||
|
||||
/**
|
||||
* Parameter schema for Wiki search tool
|
||||
|
|
@ -131,15 +133,24 @@ async function executeWikiSearchTool(
|
|||
// Format results as text
|
||||
let content = `Wiki search completed successfully. Found ${tiddlerTitles.length} total results, showing ${results.length}:\n\n`;
|
||||
|
||||
for (const result of results) {
|
||||
content += `**Tiddler: ${result.title}**\n\n`;
|
||||
if (result.text) {
|
||||
content += '```tiddlywiki\n';
|
||||
content += result.text;
|
||||
content += '\n```\n\n';
|
||||
} else {
|
||||
content += '(Content not available)\n\n';
|
||||
if (includeText) {
|
||||
// Format with content
|
||||
for (const result of results) {
|
||||
content += `**Tiddler: ${result.title}**\n\n`;
|
||||
if (result.text) {
|
||||
content += '```tiddlywiki\n';
|
||||
content += result.text;
|
||||
content += '\n```\n\n';
|
||||
} else {
|
||||
content += '(Content not available)\n\n';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Format titles only
|
||||
for (const result of results) {
|
||||
content += `- ${result.title}\n`;
|
||||
}
|
||||
content += '\n(includeText set to false)\n';
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -209,7 +220,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
|||
.join('\n');
|
||||
|
||||
const toolPromptContent =
|
||||
`Available Wiki Workspaces:\n${workspaceList}\n\nAvailable Tools:\n- Tool ID: wiki-search\n- Tool Name: Wiki Search\n- Description: Search content in wiki workspaces\n- Parameters: {\n "workspaceName": "string (required) - The name of the wiki workspace to search in",\n "filter": "string (required) - TiddlyWiki filter expression for searching",\n "maxResults": "number (optional, default: 10) - Maximum number of results to return",\n "includeText": "boolean (optional, default: true) - Whether to include tiddler text content"\n}`;
|
||||
`Available Wiki Workspaces:\n${workspaceList}\n\nAvailable Tools:\n- Tool ID: wiki-search\n- Tool Name: Wiki Search\n- Description: Search content in wiki workspaces\n- Parameters: {\n "workspaceName": "string (required) - The name of the wiki workspace to search in",\n "filter": "string (required) - TiddlyWiki filter expression for searching, like [title[Index]]""\n}`;
|
||||
|
||||
const toolPrompt: IPrompt = {
|
||||
id: `wiki-tool-list-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
|
|
@ -268,6 +279,26 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
|||
agentId: handlerContext.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 = handlerContext.agent.messages.filter(message => 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: 'wiki-search',
|
||||
};
|
||||
|
||||
logger.debug('Set duration=1 for AI tool call message', {
|
||||
messageId: latestAiMessage.id,
|
||||
toolId: 'wiki-search',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the wiki search tool call
|
||||
try {
|
||||
// Validate parameters against schema
|
||||
|
|
@ -300,21 +331,65 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
|||
|
||||
// Format the tool result for display
|
||||
let toolResultText: string;
|
||||
let isError = false;
|
||||
|
||||
if (result.success && result.data) {
|
||||
toolResultText = `<functions_result>\nTool: wiki-search\nParameters: ${JSON.stringify(validatedParameters)}\nResult: ${result.data}\n</functions_result>`;
|
||||
} else {
|
||||
isError = true;
|
||||
toolResultText = `<functions_result>\nTool: wiki-search\nParameters: ${JSON.stringify(validatedParameters)}\nError: ${
|
||||
result.error || 'Unknown error'
|
||||
}\n</functions_result>`;
|
||||
}
|
||||
|
||||
// Set up actions to continue the conversation with tool results
|
||||
const responseContext = context as unknown as ResponseHookContext;
|
||||
const responseContext = context as unknown as AIResponseContext;
|
||||
if (!responseContext.actions) {
|
||||
responseContext.actions = {};
|
||||
}
|
||||
responseContext.actions.yieldNextRoundTo = 'self';
|
||||
responseContext.actions.newUserMessage = toolResultText;
|
||||
|
||||
logger.debug('Wiki search setting yieldNextRoundTo=self', {
|
||||
toolId: 'wiki-search',
|
||||
agentId: handlerContext.agent.id,
|
||||
messageCount: handlerContext.agent.messages.length,
|
||||
});
|
||||
|
||||
// Immediately add the tool result message to history
|
||||
// Use a slight delay to ensure timestamp is after the tool call message
|
||||
const toolResultTime = new Date(Date.now() + 1); // Add 1ms to ensure proper ordering
|
||||
const toolResultMessage: AgentInstanceMessage = {
|
||||
id: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: handlerContext.agent.id,
|
||||
role: 'user',
|
||||
content: toolResultText,
|
||||
modified: toolResultTime,
|
||||
duration: 1, // Tool results are only visible to AI for 1 round to save context
|
||||
metadata: {
|
||||
isToolResult: true,
|
||||
isError,
|
||||
toolId: 'wiki-search',
|
||||
toolParameters: validatedParameters,
|
||||
},
|
||||
};
|
||||
handlerContext.agent.messages.push(toolResultMessage);
|
||||
|
||||
// Save tool result message to database
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(toolResultMessage);
|
||||
} catch (saveError) {
|
||||
logger.warn('Failed to save tool result message to database', {
|
||||
error: saveError instanceof Error ? saveError.message : String(saveError),
|
||||
messageId: toolResultMessage.id,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('Wiki search tool execution result', {
|
||||
toolResultText,
|
||||
actions: responseContext.actions,
|
||||
toolResultMessageId: toolResultMessage.id,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Wiki search tool execution failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
|
@ -322,15 +397,44 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
|||
});
|
||||
|
||||
// Set up error response for next round
|
||||
const responseContext = context as unknown as ResponseHookContext;
|
||||
const responseContext = context as unknown as AIResponseContext;
|
||||
if (!responseContext.actions) {
|
||||
responseContext.actions = {};
|
||||
}
|
||||
responseContext.actions.yieldNextRoundTo = 'self';
|
||||
responseContext.actions.newUserMessage = `<functions_result>
|
||||
const errorMessage = `<functions_result>
|
||||
Tool: wiki-search
|
||||
Error: ${error instanceof Error ? error.message : String(error)}
|
||||
</functions_result>`;
|
||||
|
||||
// Add error message to history
|
||||
// Use a slight delay to ensure timestamp is after the tool call message
|
||||
const errorResultTime = new Date(Date.now() + 1); // Add 1ms to ensure proper ordering
|
||||
const errorResultMessage: AgentInstanceMessage = {
|
||||
id: `tool-error-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
agentId: handlerContext.agent.id,
|
||||
role: 'user',
|
||||
content: errorMessage,
|
||||
modified: errorResultTime,
|
||||
duration: 1, // Error messages are only visible to AI for 1 round
|
||||
metadata: {
|
||||
isToolResult: true,
|
||||
isError: true,
|
||||
toolId: 'wiki-search',
|
||||
},
|
||||
};
|
||||
handlerContext.agent.messages.push(errorResultMessage);
|
||||
|
||||
// Save error message to database
|
||||
try {
|
||||
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
||||
await agentInstanceService.saveUserMessage(errorResultMessage);
|
||||
} catch (saveError) {
|
||||
logger.warn('Failed to save tool error message to database', {
|
||||
error: saveError instanceof Error ? saveError.message : String(saveError),
|
||||
messageId: errorResultMessage.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import { logger } from '@services/libs/log';
|
||||
import { CoreMessage } from 'ai';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { AgentHandlerContext } from '../buildInAgentHandlers/type';
|
||||
import { AgentInstanceMessage } from '../interface';
|
||||
import { builtInPlugins, createHandlerHooks, initializePluginSystem, PromptConcatHookContext } from '../plugins';
|
||||
import { AgentPromptDescription, IPrompt } from './promptConcatSchema';
|
||||
|
|
@ -216,6 +217,7 @@ export interface PromptConcatStreamState {
|
|||
export async function* promptConcatStream(
|
||||
agentConfig: Pick<AgentPromptDescription, 'handlerConfig'>,
|
||||
messages: AgentInstanceMessage[],
|
||||
handlerContext: AgentHandlerContext,
|
||||
): AsyncGenerator<PromptConcatStreamState, PromptConcatStreamState, unknown> {
|
||||
const promptConfigs = Array.isArray(agentConfig.handlerConfig.prompts) ? agentConfig.handlerConfig.prompts : [];
|
||||
const pluginConfigs = Array.isArray(agentConfig.handlerConfig.plugins) ? agentConfig.handlerConfig.plugins : [];
|
||||
|
|
@ -243,6 +245,7 @@ export async function* promptConcatStream(
|
|||
|
||||
for (let index = 0; index < pluginConfigs.length; index++) {
|
||||
const context: PromptConcatHookContext = {
|
||||
handlerContext,
|
||||
messages,
|
||||
prompts: modifiedPrompts,
|
||||
pluginConfig: pluginConfigs[index],
|
||||
|
|
@ -287,6 +290,7 @@ export async function* promptConcatStream(
|
|||
};
|
||||
|
||||
const finalContext: PromptConcatHookContext = {
|
||||
handlerContext,
|
||||
messages,
|
||||
prompts: modifiedPrompts,
|
||||
pluginConfig: {} as Plugin, // Empty plugin for finalization
|
||||
|
|
@ -345,17 +349,19 @@ export async function* promptConcatStream(
|
|||
*
|
||||
* @param agentConfig Prompt configuration
|
||||
* @param messages Message history
|
||||
* @param handlerContext Handler context with agent and other state
|
||||
* @returns Processed prompt array and original prompt tree
|
||||
*/
|
||||
export async function promptConcat(
|
||||
agentConfig: Pick<AgentPromptDescription, 'handlerConfig'>,
|
||||
messages: AgentInstanceMessage[],
|
||||
handlerContext: AgentHandlerContext,
|
||||
): Promise<{
|
||||
flatPrompts: CoreMessage[];
|
||||
processedPrompts: IPrompt[];
|
||||
}> {
|
||||
// Use the streaming version and just return the final result
|
||||
const stream = promptConcatStream(agentConfig, messages);
|
||||
const stream = promptConcatStream(agentConfig, messages, handlerContext);
|
||||
let finalResult: PromptConcatStreamState;
|
||||
|
||||
// Consume all intermediate states to get the final result
|
||||
|
|
|
|||
|
|
@ -115,10 +115,6 @@ export const AutoReplyParameterSchema = z.object({
|
|||
title: t('Schema.AutoReply.TextTitle'),
|
||||
description: t('Schema.AutoReply.Text'),
|
||||
}),
|
||||
maxAutoReply: z.number().optional().meta({
|
||||
title: t('Schema.AutoReply.MaxAutoReplyTitle'),
|
||||
description: t('Schema.AutoReply.MaxAutoReply'),
|
||||
}),
|
||||
}).meta({
|
||||
title: t('Schema.AutoReply.Title'),
|
||||
description: t('Schema.AutoReply.Description'),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash';
|
|||
import { AgentHandlerContext } from '../buildInAgentHandlers/type';
|
||||
import { AgentInstanceMessage } from '../interface';
|
||||
import { builtInPlugins, createHandlerHooks } from '../plugins';
|
||||
import { AgentResponse, ResponseHookContext } from '../plugins/types';
|
||||
import { AgentResponse, PostProcessContext, YieldNextRoundTarget } from '../plugins/types';
|
||||
import { AgentPromptDescription, HandlerConfig } from './promptConcatSchema';
|
||||
|
||||
/**
|
||||
|
|
@ -27,8 +27,7 @@ export async function responseConcat(
|
|||
messages: AgentInstanceMessage[] = [],
|
||||
): Promise<{
|
||||
processedResponse: string;
|
||||
needsNewLLMCall: boolean;
|
||||
newUserMessage?: string;
|
||||
yieldNextRoundTo?: YieldNextRoundTarget;
|
||||
toolCallInfo?: ToolCallingMatch;
|
||||
}> {
|
||||
logger.debug('Starting response processing', {
|
||||
|
|
@ -42,9 +41,7 @@ export async function responseConcat(
|
|||
const responses: HandlerConfig['response'] = Array.isArray(handlerConfig.response) ? handlerConfig.response : [];
|
||||
const plugins = Array.isArray(handlerConfig.plugins) ? handlerConfig.plugins : [];
|
||||
|
||||
const responsesCopy = cloneDeep(responses);
|
||||
let modifiedResponses: AgentResponse[] = responsesCopy;
|
||||
|
||||
let modifiedResponses = cloneDeep(responses) as AgentResponse[];
|
||||
// Create hooks instance
|
||||
const hooks = createHandlerHooks();
|
||||
// Register all plugins from configuration
|
||||
|
|
@ -58,12 +55,12 @@ export async function responseConcat(
|
|||
}
|
||||
|
||||
// Process each plugin through hooks
|
||||
let needsNewLLMCall = false;
|
||||
let newUserMessage: string | undefined;
|
||||
let yieldNextRoundTo: YieldNextRoundTarget | undefined;
|
||||
let toolCallInfo: ToolCallingMatch | undefined;
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const responseContext: ResponseHookContext = {
|
||||
const responseContext: PostProcessContext = {
|
||||
handlerContext: context,
|
||||
messages,
|
||||
prompts: [], // Not used in response processing
|
||||
pluginConfig: plugin,
|
||||
|
|
@ -73,24 +70,23 @@ export async function responseConcat(
|
|||
};
|
||||
|
||||
try {
|
||||
const result = await hooks.postProcess.promise(responseContext) as ResponseHookContext;
|
||||
const result = await hooks.postProcess.promise(responseContext);
|
||||
|
||||
// Update responses if they were modified in the context
|
||||
modifiedResponses = result.responses;
|
||||
if (result.responses) {
|
||||
modifiedResponses = result.responses;
|
||||
}
|
||||
|
||||
// Check if plugin indicated need for new LLM call via actions
|
||||
if (result.actions?.yieldNextRoundTo === 'self') {
|
||||
needsNewLLMCall = true;
|
||||
if (result.actions.newUserMessage) {
|
||||
newUserMessage = result.actions.newUserMessage;
|
||||
}
|
||||
if (result.actions?.yieldNextRoundTo) {
|
||||
yieldNextRoundTo = result.actions.yieldNextRoundTo;
|
||||
if (result.actions.toolCalling) {
|
||||
toolCallInfo = result.actions.toolCalling;
|
||||
}
|
||||
logger.debug('Plugin requested yield next round to self', {
|
||||
logger.debug('Plugin requested yield next round', {
|
||||
pluginId: plugin.pluginId,
|
||||
pluginInstanceId: plugin.id,
|
||||
hasNewUserMessage: !!result.actions.newUserMessage,
|
||||
yieldNextRoundTo,
|
||||
hasToolCall: !!result.actions.toolCalling,
|
||||
});
|
||||
}
|
||||
|
|
@ -114,14 +110,12 @@ export async function responseConcat(
|
|||
logger.debug('Response processing completed', {
|
||||
originalLength: llmResponse.length,
|
||||
processedLength: processedResponse.length,
|
||||
needsNewLLMCall,
|
||||
hasNewUserMessage: !!newUserMessage,
|
||||
yieldNextRoundTo,
|
||||
});
|
||||
|
||||
return {
|
||||
processedResponse,
|
||||
needsNewLLMCall,
|
||||
newUserMessage,
|
||||
yieldNextRoundTo,
|
||||
toolCallInfo,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function createAgentInstanceData(agentDefinition: {
|
|||
export function createAgentMessage(
|
||||
id: string,
|
||||
agentId: string,
|
||||
message: Pick<AgentInstanceMessage, 'role' | 'content' | 'contentType' | 'metadata'>,
|
||||
message: Pick<AgentInstanceMessage, 'role' | 'content' | 'contentType' | 'metadata' | 'duration'>,
|
||||
): AgentInstanceMessage {
|
||||
return {
|
||||
id,
|
||||
|
|
@ -69,13 +69,40 @@ export function createAgentMessage(
|
|||
contentType: message.contentType || 'text/plain',
|
||||
modified: new Date(),
|
||||
metadata: message.metadata,
|
||||
// Convert null to undefined for database compatibility
|
||||
duration: message.duration === null ? undefined : message.duration,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Message fields to be extracted when creating message entities
|
||||
*/
|
||||
export const MESSAGE_FIELDS = ['id', 'agentId', 'role', 'content', 'contentType', 'metadata'] as const;
|
||||
export const MESSAGE_FIELDS = ['id', 'agentId', 'role', 'content', 'contentType', 'metadata', 'duration'] as const;
|
||||
|
||||
/**
|
||||
* Convert AgentInstanceMessage to database-compatible format
|
||||
* Handles null duration values by converting them to undefined
|
||||
*/
|
||||
export function toDatabaseCompatibleMessage(message: AgentInstanceMessage): Omit<AgentInstanceMessage, 'duration'> & { duration?: number } {
|
||||
const { duration, ...rest } = message;
|
||||
return {
|
||||
...rest,
|
||||
duration: duration === null ? undefined : duration,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AgentInstance data to database-compatible format
|
||||
* Handles null duration values in messages by converting them to undefined
|
||||
*/
|
||||
export function toDatabaseCompatibleInstance(
|
||||
instance: Omit<AgentInstance, 'created' | 'modified'>,
|
||||
): Omit<AgentInstance, 'created' | 'modified' | 'messages'> & { messages: Array<Omit<AgentInstanceMessage, 'duration'> & { duration?: number }> } {
|
||||
return {
|
||||
...instance,
|
||||
messages: instance.messages.map(toDatabaseCompatibleMessage),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent instance fields to be extracted when retrieving instances
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Test for message duration filtering functionality
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { AgentInstanceMessage } from '../../interface';
|
||||
import { filterMessagesByDuration, isMessageExpiredForAI } from '../messageDurationFilter';
|
||||
|
||||
// Helper function to create test messages
|
||||
function createTestMessage(
|
||||
id: string,
|
||||
role: 'user' | 'assistant' = 'user',
|
||||
duration?: number,
|
||||
): AgentInstanceMessage {
|
||||
return {
|
||||
id,
|
||||
agentId: 'test-agent',
|
||||
role,
|
||||
content: `Test message ${id}`,
|
||||
modified: new Date(),
|
||||
duration,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Message Duration Filtering', () => {
|
||||
describe('filterMessagesByDuration', () => {
|
||||
it('should return all messages when no duration is set', () => {
|
||||
const messages = [
|
||||
createTestMessage('1'),
|
||||
createTestMessage('2'),
|
||||
createTestMessage('3'),
|
||||
];
|
||||
|
||||
const filtered = filterMessagesByDuration(messages);
|
||||
expect(filtered).toHaveLength(3);
|
||||
expect(filtered.map(m => m.id)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it('should exclude messages with duration 0', () => {
|
||||
const messages = [
|
||||
createTestMessage('1', 'user', undefined), // Keep
|
||||
createTestMessage('2', 'user', 0), // Exclude
|
||||
createTestMessage('3', 'user', 1), // Keep (within duration)
|
||||
];
|
||||
|
||||
const filtered = filterMessagesByDuration(messages);
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.map(m => m.id)).toEqual(['1', '3']);
|
||||
});
|
||||
|
||||
it('should respect duration limits based on rounds from current', () => {
|
||||
const messages = [
|
||||
createTestMessage('1', 'user', 1), // Round 2 from current (exclude)
|
||||
createTestMessage('2', 'assistant', 1), // Round 1 from current (exclude)
|
||||
createTestMessage('3', 'user', 1), // Round 0 from current (include)
|
||||
];
|
||||
|
||||
const filtered = filterMessagesByDuration(messages);
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered.map(m => m.id)).toEqual(['3']);
|
||||
});
|
||||
|
||||
it('should handle mixed duration settings correctly', () => {
|
||||
const messages = [
|
||||
createTestMessage('1', 'user', undefined), // Always keep (undefined duration)
|
||||
createTestMessage('2', 'assistant', 5), // Keep (round 3 < duration 5)
|
||||
createTestMessage('3', 'user', 3), // Keep (round 2 < duration 3)
|
||||
createTestMessage('4', 'assistant', 1), // Exclude (round 1 >= duration 1)
|
||||
createTestMessage('5', 'user', 0), // Exclude (duration 0)
|
||||
];
|
||||
|
||||
const filtered = filterMessagesByDuration(messages);
|
||||
expect(filtered).toHaveLength(3);
|
||||
expect(filtered.map(m => m.id)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it('should return empty array for empty input', () => {
|
||||
const filtered = filterMessagesByDuration([]);
|
||||
expect(filtered).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageExpiredForAI', () => {
|
||||
it('should return false for messages with undefined duration', () => {
|
||||
const message = createTestMessage('1', 'user', undefined);
|
||||
expect(isMessageExpiredForAI(message, 0, 3)).toBe(false);
|
||||
expect(isMessageExpiredForAI(message, 2, 3)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for messages with duration 0', () => {
|
||||
const message = createTestMessage('1', 'user', 0);
|
||||
expect(isMessageExpiredForAI(message, 0, 3)).toBe(true);
|
||||
expect(isMessageExpiredForAI(message, 2, 3)).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly calculate expiration based on position and duration', () => {
|
||||
const message = createTestMessage('1', 'user', 2);
|
||||
|
||||
// Position 2 in array of 3: rounds from current = 3-1-2 = 0 (< 2, not expired)
|
||||
expect(isMessageExpiredForAI(message, 2, 3)).toBe(false);
|
||||
|
||||
// Position 1 in array of 3: rounds from current = 3-1-1 = 1 (< 2, not expired)
|
||||
expect(isMessageExpiredForAI(message, 1, 3)).toBe(false);
|
||||
|
||||
// Position 0 in array of 3: rounds from current = 3-1-0 = 2 (>= 2, expired)
|
||||
expect(isMessageExpiredForAI(message, 0, 3)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle edge cases correctly', () => {
|
||||
const message = createTestMessage('1', 'user', 1);
|
||||
|
||||
// Single message array
|
||||
expect(isMessageExpiredForAI(message, 0, 1)).toBe(false);
|
||||
|
||||
// Last message in array
|
||||
expect(isMessageExpiredForAI(message, 4, 5)).toBe(false);
|
||||
|
||||
// First message in large array
|
||||
expect(isMessageExpiredForAI(message, 0, 5)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* Message filtering utilities for duration-based context management
|
||||
*/
|
||||
import type { AgentInstanceMessage } from '../interface';
|
||||
|
||||
/**
|
||||
* Filter messages based on their duration settings
|
||||
* Messages with duration set will only be included if they are within the specified number of rounds from the current position
|
||||
* @param messages Array of all messages
|
||||
* @returns Filtered array containing only messages that should be sent to AI
|
||||
*/
|
||||
export function filterMessagesByDuration(messages: AgentInstanceMessage[]): AgentInstanceMessage[] {
|
||||
// If no messages, return empty array
|
||||
if (messages.length === 0) return [];
|
||||
|
||||
// Calculate the current round position (how many rounds have passed since each message)
|
||||
const filteredMessages: AgentInstanceMessage[] = [];
|
||||
|
||||
// Iterate through messages from latest to oldest to calculate rounds
|
||||
for (let index = messages.length - 1; index >= 0; index--) {
|
||||
const message = messages[index];
|
||||
|
||||
// Calculate rounds from current position (0 = current message, 1 = previous round, etc.)
|
||||
const roundsFromCurrent = messages.length - 1 - index;
|
||||
|
||||
// If duration is undefined or null, include the message (default behavior - persist indefinitely)
|
||||
if (message.duration === undefined || message.duration === null) {
|
||||
filteredMessages.unshift(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If duration is 0, exclude from AI context (but still visible in UI)
|
||||
if (message.duration === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If message is within its duration window, include it
|
||||
if (roundsFromCurrent < message.duration) {
|
||||
filteredMessages.unshift(message);
|
||||
}
|
||||
// Otherwise, message has expired and should not be sent to AI
|
||||
}
|
||||
|
||||
return filteredMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message should be displayed with reduced opacity in UI
|
||||
* @param message The message to check
|
||||
* @param currentPosition The current position of the message in the full message array (0-based from start)
|
||||
* @param totalMessages Total number of messages
|
||||
* @returns true if the message should be semi-transparent
|
||||
*/
|
||||
export function isMessageExpiredForAI(
|
||||
message: AgentInstanceMessage,
|
||||
currentPosition: number,
|
||||
totalMessages: number,
|
||||
): boolean {
|
||||
// If duration is undefined, message never expires
|
||||
if (message.duration === undefined || message.duration === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If duration is 0, message is immediately expired
|
||||
if (message.duration === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate rounds from current position
|
||||
const roundsFromCurrent = totalMessages - 1 - currentPosition;
|
||||
|
||||
// Message is expired if it's beyond its duration window
|
||||
return roundsFromCurrent >= message.duration;
|
||||
}
|
||||
|
|
@ -140,6 +140,9 @@ export class AgentInstanceMessageEntity implements AgentInstanceMessage {
|
|||
@Column({ type: 'simple-json', nullable: true, name: 'meta_data' })
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
duration?: number;
|
||||
|
||||
// Relation to AgentInstance
|
||||
@ManyToOne(() => AgentInstanceEntity, instance => instance.messages)
|
||||
@JoinColumn({ name: 'agentId' })
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ export class ExternalAPIService implements IExternalAPIService {
|
|||
): AsyncGenerator<AIStreamResponse, void, unknown> {
|
||||
// Prepare request with minimal context
|
||||
const { requestId, controller } = this.prepareAIRequest();
|
||||
logger.debug(`[${requestId}] Starting generateFromAI with config`, messages);
|
||||
|
||||
try {
|
||||
// Send start event
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue