diff --git a/packages/ai-native/src/browser/chat/chat.feature.registry.ts b/packages/ai-native/src/browser/chat/chat.feature.registry.ts index e3b57b52b6..95319ff8f4 100644 --- a/packages/ai-native/src/browser/chat/chat.feature.registry.ts +++ b/packages/ai-native/src/browser/chat/chat.feature.registry.ts @@ -2,7 +2,13 @@ import { Injectable } from '@opensumi/di'; import { Disposable, Emitter, Event, getDebugLogger } from '@opensumi/ide-core-common'; import { IChatWelcomeMessageContent, ISampleQuestions, SLASH_SYMBOL } from '../../common'; -import { IChatFeatureRegistry, IChatSlashCommandHandler, IChatSlashCommandItem, IImageUploadProvider } from '../types'; +import { + IChatFeatureRegistry, + IChatSlashCommandHandler, + IChatSlashCommandItem, + IImageUploadProvider, + IMessageSummaryProvider, +} from '../types'; import { ChatSlashCommandItemModel, ChatWelcomeMessageModel } from './chat-model'; import { ChatProxyService } from './chat-proxy.service'; @@ -14,6 +20,8 @@ export class ChatFeatureRegistry extends Disposable implements IChatFeatureRegis private slashCommandsHandlerMap: Map = new Map(); private imageUploadProvider: IImageUploadProvider | undefined; + private messageSummaryProvider?: IMessageSummaryProvider; + public registerImageUploadProvider(provider: IImageUploadProvider): void { this.imageUploadProvider = provider; } @@ -22,6 +30,14 @@ export class ChatFeatureRegistry extends Disposable implements IChatFeatureRegis return this.imageUploadProvider; } + public registerMessageSummaryProvider(provider: IMessageSummaryProvider): void { + this.messageSummaryProvider = provider; + } + + public getMessageSummaryProvider(): IMessageSummaryProvider | undefined { + return this.messageSummaryProvider; + } + public chatWelcomeMessageModel?: ChatWelcomeMessageModel; private _onDidWelcomeMessageChange: Emitter = new Emitter(); diff --git a/packages/ai-native/src/browser/chat/chat.view.tsx b/packages/ai-native/src/browser/chat/chat.view.tsx index 212e2a7eaa..aac3e2cbc9 100644 --- a/packages/ai-native/src/browser/chat/chat.view.tsx +++ b/packages/ai-native/src/browser/chat/chat.view.tsx @@ -918,6 +918,7 @@ export function DefaultChatViewHeader({ }) { const aiChatService = useInjectable(IChatInternalService); const messageService = useInjectable(IMessageService); + const chatFeatureRegistry = useInjectable(ChatFeatureRegistryToken); const [historyList, setHistoryList] = React.useState([]); const [currentTitle, setCurrentTitle] = React.useState(''); @@ -947,9 +948,26 @@ export function DefaultChatViewHeader({ const getHistoryList = () => { const currentMessages = aiChatService.sessionModel.history.getMessages(); const latestUserMessage = currentMessages.findLast((m) => m.role === ChatMessageRole.User); - setCurrentTitle( - latestUserMessage ? cleanAttachedTextWrapper(latestUserMessage.content).slice(0, MAX_TITLE_LENGTH) : '', - ); + const summaryProvider = chatFeatureRegistry.getMessageSummaryProvider(); + const currentTitle = latestUserMessage + ? cleanAttachedTextWrapper(latestUserMessage.content).slice(0, MAX_TITLE_LENGTH) + : ''; + const messages = aiChatService.sessionModel.history.getMessages().map((msg) => ({ + role: msg.role, + content: msg.content, + })); + if (messages.length > 2 && summaryProvider && aiChatService.sessionModel.sessionId) { + summaryProvider.getMessageSummary(messages).then((summary) => { + if (summary) { + setCurrentTitle(summary.slice(0, MAX_TITLE_LENGTH)); + } else { + setCurrentTitle(currentTitle); + } + }); + } else { + setCurrentTitle(currentTitle); + } + setHistoryList( aiChatService.getSessions().map((session) => { const history = session.history; diff --git a/packages/ai-native/src/browser/types.ts b/packages/ai-native/src/browser/types.ts index 24f554254f..548f312e89 100644 --- a/packages/ai-native/src/browser/types.ts +++ b/packages/ai-native/src/browser/types.ts @@ -5,6 +5,7 @@ import { ZodSchema } from 'zod'; import { AIActionItem } from '@opensumi/ide-core-browser/lib/components/ai-native/index'; import { CancellationToken, + ChatMessageRole, ChatResponse, Deferred, IAICompletionOption, @@ -134,6 +135,8 @@ export interface IChatFeatureRegistry { registerImageUploadProvider(provider: IImageUploadProvider): void; registerWelcome(content: IChatWelcomeMessageContent | React.ReactNode, sampleQuestions?: ISampleQuestions[]): void; registerSlashCommand(command: IChatSlashCommandItem, handler: IChatSlashCommandHandler): void; + + registerMessageSummaryProvider(provider: IMessageSummaryProvider): void; } export type ChatWelcomeRender = (props: { @@ -298,6 +301,15 @@ export interface IImageUploadProvider { imageUpload(file: File): Promise; } +export interface IMessageSummaryProvider { + getMessageSummary( + messages: Array<{ + role: ChatMessageRole; + content: string; + }>, + ): Promise; +} + export const AINativeCoreContribution = Symbol('AINativeCoreContribution'); export interface AINativeCoreContribution { diff --git a/packages/extension/__tests__/browser/extension-service/extension.service.test.ts b/packages/extension/__tests__/browser/extension-service/extension.service.test.ts index a6dfa47b2e..12e2ba2318 100644 --- a/packages/extension/__tests__/browser/extension-service/extension.service.test.ts +++ b/packages/extension/__tests__/browser/extension-service/extension.service.test.ts @@ -99,11 +99,6 @@ describe('Extension service', () => { expect(exts).toEqual(MOCK_EXTENSIONS); }); - it('should return all mock extensions JSON', async () => { - const jsons = await extensionManagementService.getAllExtensionJson(); - expect(jsons).toEqual(MOCK_EXTENSIONS.map((e) => e.toJSON())); - }); - it('should return specified extension props', async () => { const extensionMetadata = await extensionManagementService.getExtensionProps(MOCK_EXTENSIONS[0].path, { readme: './README.md', diff --git a/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts b/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts index ad3b80a750..c2259a9d52 100644 --- a/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts +++ b/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts @@ -35,6 +35,7 @@ import { AIBackSerivcePath, CancelResponse, CancellationToken, + ChatMessageRole, ChatResponse, ChatServiceToken, ErrorResponse, @@ -57,6 +58,10 @@ export class AINativeContribution implements AINativeCoreContribution { @Autowired(AIBackSerivcePath) private readonly aiBackService: IAIBackService; + private lastMessageSummary: string = ''; + private messageCountSinceLastSummary: number = 0; + private readonly SUMMARY_UPDATE_THRESHOLD = 10; // 每10条消息更新一次摘要 + @Autowired(TerminalDetectionPromptManager) terminalDetectionPromptManager: TerminalDetectionPromptManager; @@ -236,6 +241,49 @@ export class AINativeContribution implements AINativeCoreContribution { imageUpload: imageToBase64, }); + registry.registerMessageSummaryProvider({ + getMessageSummary: async ( + messages: Array<{ + role: ChatMessageRole; + content: string; + }>, + ) => { + // 如果消息数量未达到阈值且已有摘要,则返回上一次的摘要 + this.messageCountSinceLastSummary++; + if (this.messageCountSinceLastSummary < this.SUMMARY_UPDATE_THRESHOLD && this.lastMessageSummary) { + return this.lastMessageSummary; + } + + // 裁剪最近的10条消息,避免过长的消息导致请求失败 + const sliceMessages = messages.slice(-10); + const prompt = `You are a helpful AI assistant. Summarize in ONE clear statement (12-15 words). +Requirements: +- Include what happened +- No meta-descriptions +- No interpretations + +Bad: "The discussion was about ENI limits" +Good: "Instance network interfaces exceeded system limit"`; + const result = await this.aiBackService.request(prompt, { + type: '', + messages: sliceMessages, + }); + + if (result.isCancel) { + return this.lastMessageSummary || ''; + } + + if (result.errorCode !== 0) { + return this.lastMessageSummary || ''; + } + + // 更新摘要和计数器 + this.lastMessageSummary = result.data || ''; + this.messageCountSinceLastSummary = 0; + return this.lastMessageSummary; + }, + }); + // registry.registerSlashCommand( // { // name: 'Explain',