import flatMap from 'lodash/flatMap'; import { Autowired, Injectable } from '@opensumi/di'; import { AIServiceType, ActionSourceEnum, ActionTypeEnum, CancellationToken, ChatFeatureRegistryToken, ChatServiceToken, Disposable, Emitter, IAIReporter, IChatProgress, IDisposable, ILogger, toDisposable, } from '@opensumi/ide-core-common'; import { CoreMessage, IChatAgent, IChatAgentCommand, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatFollowup, IChatMessageStructure, } from '../../common'; import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context'; import { ChatAgentPromptProvider } from '../../common/prompts/context-prompt-provider'; import { IChatFeatureRegistry } from '../types'; import { ChatService } from './chat.api.service'; @Injectable() export class ChatAgentService extends Disposable implements IChatAgentService { private readonly agents = new Map(); private defaultAgentId: string | undefined; private initialUserMessageMap: Map = new Map(); private shouldUpdateContext = false; private contextVersion: number; private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents = this._onDidChangeAgents.event; private readonly _onDidSendMessage = new Emitter(); public readonly onDidSendMessage = this._onDidSendMessage.event; @Autowired(ILogger) logger: ILogger; @Autowired(IAIReporter) private readonly aiReporter: IAIReporter; @Autowired(LLMContextServiceToken) protected readonly llmContextService: LLMContextService; @Autowired(ChatAgentPromptProvider) protected readonly promptProvider: ChatAgentPromptProvider; @Autowired(ChatServiceToken) private aiChatService: ChatService; @Autowired(ChatFeatureRegistryToken) private readonly chatFeatureRegistry: IChatFeatureRegistry; constructor() { super(); this.addDispose(this._onDidChangeAgents); this.addDispose( this.llmContextService.onDidContextFilesChangeEvent((event) => { if (event.version !== this.contextVersion) { this.contextVersion = event.version; this.shouldUpdateContext = true; } }), ); } registerAgent(agent: IChatAgent): IDisposable { if (this.agents.has(agent.id)) { throw new Error(`Already registered an agent with id ${agent.id}`); } this.agents.set(agent.id, { agent, commands: [] }); this._onDidChangeAgents.fire(); return toDisposable(() => { if (this.agents.delete(agent.id)) { this._onDidChangeAgents.fire(); } }); } async updateAgent(id: string, updateMetadata: IChatAgentMetadata) { const data = this.agents.get(id); if (!data) { throw new Error(`No agent with id ${id} registered`); } data.agent.metadata = { ...data.agent.metadata, ...updateMetadata }; data.commands = await data.agent.provideSlashCommands(CancellationToken.None); if (updateMetadata.isDefault) { this.defaultAgentId = id; } const chatWelcomeMessage = await data.agent.provideChatWelcomeMessage(CancellationToken.None); if (chatWelcomeMessage) { const { content, sampleQuestions } = chatWelcomeMessage; this.chatFeatureRegistry.registerWelcome(content, sampleQuestions as any); } this._onDidChangeAgents.fire(); } getAgents(): Array { return Array.from(this.agents.values(), (v) => v.agent); } hasAgent(id: string): boolean { return this.agents.has(id); } getDefaultAgentId() { return this.defaultAgentId; } getAgent(id: string): IChatAgent | undefined { const data = this.agents.get(id); return data?.agent; } async invokeAgent( id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: CoreMessage[], token: CancellationToken, ): Promise { const data = this.agents.get(id); if (!data) { throw new Error(`No agent with id ${id}`); } // 发送第一条消息时携带初始 context if (!this.initialUserMessageMap.has(request.sessionId)) { this.initialUserMessageMap.set(request.sessionId, request.message); const rawMessage = request.message; request.message = await this.provideContextMessage(rawMessage, request.sessionId); } else if (this.shouldUpdateContext || request.regenerate || history.length === 0) { request.message = await this.provideContextMessage(request.message, request.sessionId); this.shouldUpdateContext = false; } const result = await data.agent.invoke(request, progress, history, token); return result; } private async provideContextMessage(message: string, sessionId: string) { const context = await this.llmContextService.serialize(); const fullMessage = await this.promptProvider.provideContextPrompt(context, message); this.aiReporter.send({ msgType: AIServiceType.Chat, actionType: ActionTypeEnum.ContextEnhance, actionSource: ActionSourceEnum.Chat, sessionId, message: fullMessage, }); return fullMessage; } populateChatInput(id: string, message: IChatMessageStructure) { this.aiChatService.sendMessage({ ...message, agentId: id, immediate: false, }); } getCommands() { return flatMap( Array.from(this.agents.values(), ({ agent, commands }) => commands.map((c) => ({ ...c, agentId: agent.id }))), ); } async getFollowups(id: string, sessionId: string, token: CancellationToken): Promise { const data = this.agents.get(id); if (!data) { throw new Error(`No agent with id ${id}`); } if (!data.agent.provideFollowups) { return []; } return data.agent.provideFollowups(sessionId, token); } async getSampleQuestions(id: string, token: CancellationToken) { const data = this.agents.get(id); if (!data) { throw new Error(`No agent with id ${id}`); } if (!data.agent.provideSampleQuestions) { return []; } return data.agent.provideSampleQuestions(token); } async getAllSampleQuestions() { const result = await Promise.all( Array.from(this.agents.values()).map(async ({ agent }) => { try { return await this.getSampleQuestions(agent.id, CancellationToken.None); } catch (err) { this.logger.error(err); return []; } }), ); return flatMap(result); } sendMessage(chunk: IChatProgress): void { this._onDidSendMessage.fire(chunk); } parseMessage(value: string, currentAgentId?: string) { const parsedInfo = { agentId: '', command: '', message: value, }; let useAgentId = currentAgentId; const agents = this.getAgents(); const agentIdReg = new RegExp(`^@(${agents.map((a) => a.id).join('|')})(?:\\s+|$)`, 'i'); const agentIdMatch = parsedInfo.message.match(agentIdReg); if (agentIdMatch) { const matchedAgent = agents.find((a) => a.id.toLowerCase() === agentIdMatch[1].toLowerCase()); if (!matchedAgent) { return parsedInfo; } useAgentId = matchedAgent.id; parsedInfo.agentId = useAgentId; parsedInfo.message = parsedInfo.message.replace(agentIdMatch[0], ''); } if (useAgentId) { const commands = this.agents.get(useAgentId)?.commands; if (commands?.length) { const commandReg = new RegExp(`^/\\s?(${commands.map((c) => c.name).join('|')})(?:\\s+|$)`, 'i'); const commandMatch = parsedInfo.message.match(commandReg); if (commandMatch) { const matchedCommand = commands.find((c) => c.name.toLowerCase() === commandMatch[1].toLowerCase()); if (!matchedCommand) { return parsedInfo; } parsedInfo.command = matchedCommand.name; parsedInfo.message = parsedInfo.message.replace(commandMatch[0], ''); } } } return parsedInfo; } }