diff --git a/packages/ai-core/src/common/communication-recording-service.ts b/packages/ai-core/src/common/communication-recording-types.ts similarity index 93% rename from packages/ai-core/src/common/communication-recording-service.ts rename to packages/ai-core/src/common/communication-recording-types.ts index d035bc1a8a1e7..9b6486e22af43 100644 --- a/packages/ai-core/src/common/communication-recording-service.ts +++ b/packages/ai-core/src/common/communication-recording-types.ts @@ -31,4 +31,6 @@ export interface CommunicationRecordingService { recordRequest(requestEntry: CommunicationHistoryEntry): void; recordResponse(responseEntry: CommunicationHistoryEntry): void; getHistory(agentId: string): CommunicationHistory; + setHistory(agentId: string, history: CommunicationHistory): void; + getRecordedAgents(): string[]; } diff --git a/packages/ai-core/src/common/index.ts b/packages/ai-core/src/common/index.ts index 112dea3a1a23e..3f197585cbee9 100644 --- a/packages/ai-core/src/common/index.ts +++ b/packages/ai-core/src/common/index.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** export * from './agent'; -export * from './communication-recording-service'; +export * from './communication-recording-types'; export * from './language-model'; export * from './language-model-delegate'; export * from './prompt-service'; diff --git a/packages/ai-history/src/browser/ai-history-frontend-contribution.ts b/packages/ai-history/src/browser/ai-history-frontend-contribution.ts new file mode 100644 index 0000000000000..c3096bf618836 --- /dev/null +++ b/packages/ai-history/src/browser/ai-history-frontend-contribution.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { CommunicationRecordingService } from '@theia/ai-core'; +import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { AiHistoryPersistenceService } from '../common/history-persistence'; + +@injectable() +export class AIHistoryFrontendContribution implements FrontendApplicationContribution { + + @inject(AiHistoryPersistenceService) + protected persistenceService: AiHistoryPersistenceService; + @inject(CommunicationRecordingService) + protected recordingService: CommunicationRecordingService; + + async onStart(app: FrontendApplication): Promise { + this.persistenceService.loadHistory(); + } + + onStop(app: FrontendApplication): void { + this.recordingService.getRecordedAgents().forEach(agentId => { + const history = this.recordingService.getHistory(agentId); + this.persistenceService.saveHistory(agentId, history); + }); + } + +} diff --git a/packages/ai-history/src/browser/ai-history-frontend-module.ts b/packages/ai-history/src/browser/ai-history-frontend-module.ts index 26996337f50e3..36b0030d358d3 100644 --- a/packages/ai-history/src/browser/ai-history-frontend-module.ts +++ b/packages/ai-history/src/browser/ai-history-frontend-module.ts @@ -14,10 +14,21 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { CommunicationRecordingService } from '@theia/ai-core'; +import { FrontendApplicationContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser'; import { ContainerModule } from '@theia/core/shared/inversify'; import { DefaultCommunicationRecordingService } from '../common/communication-recording-service'; +import { AiHistoryPersistenceService, aiHistoryPersistenceServicePath } from '../common/history-persistence'; +import { AIHistoryFrontendContribution } from './ai-history-frontend-contribution'; export default new ContainerModule(bind => { + bind(FrontendApplicationContribution).to(AIHistoryFrontendContribution).inSingletonScope(); + bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope(); bind(CommunicationRecordingService).toService(DefaultCommunicationRecordingService); + + bind(AiHistoryPersistenceService).toDynamicValue(ctx => { + const connection = ctx.container.get(WebSocketConnectionProvider); + return connection.createProxy(aiHistoryPersistenceServicePath); + }).inSingletonScope(); + }); diff --git a/packages/ai-history/src/common/communication-recording-service.ts b/packages/ai-history/src/common/communication-recording-service.ts index 6f7c882621eb0..cb97cedaab089 100644 --- a/packages/ai-history/src/common/communication-recording-service.ts +++ b/packages/ai-history/src/common/communication-recording-service.ts @@ -21,10 +21,18 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord protected history: Map = new Map(); + getRecordedAgents(): string[] { + return Object.keys(this.history); + } + getHistory(agentId: string): CommunicationHistory { return this.history.get(agentId) || []; } + setHistory(agentId: string, history: CommunicationHistory): void { + this.history.set(agentId, history); + } + recordRequest(requestEntry: CommunicationHistoryEntry): void { console.log('Recording request:', requestEntry.request); if (this.history.has(requestEntry.agentId)) { @@ -32,6 +40,7 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord } else { this.history.set(requestEntry.agentId, [requestEntry]); } + // this.persistenceService.saveHistory(requestEntry.agentId, this.history.get(requestEntry.agentId)!); } recordResponse(responseEntry: CommunicationHistoryEntry): void { @@ -46,6 +55,7 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord matchingEntry.response = responseEntry.response; matchingEntry.responseTime = responseEntry.timestamp - matchingEntry.timestamp; } + // this.persistenceService.saveHistory(responseEntry.agentId, this.history.get(responseEntry.agentId)!); } } } diff --git a/packages/ai-history/src/common/history-persistence.ts b/packages/ai-history/src/common/history-persistence.ts new file mode 100644 index 0000000000000..90dffab5a6bfc --- /dev/null +++ b/packages/ai-history/src/common/history-persistence.ts @@ -0,0 +1,25 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { CommunicationHistory } from '@theia/ai-core'; + +export const aiHistoryPersistenceServicePath = '/services/aiHistoryPersistenceService'; + +export const AiHistoryPersistenceService = Symbol('AiHistoryPersistenceService'); +export interface AiHistoryPersistenceService { + saveHistory(agentId: string, history: CommunicationHistory): Promise; + loadHistory(): Promise; +} diff --git a/packages/ai-history/src/node/ai-history-backend-module.ts b/packages/ai-history/src/node/ai-history-backend-module.ts index b32148dc0f516..fd3de2c3ed752 100644 --- a/packages/ai-history/src/node/ai-history-backend-module.ts +++ b/packages/ai-history/src/node/ai-history-backend-module.ts @@ -13,9 +13,26 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { CommunicationRecordingService } from '@theia/ai-core'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; import { ContainerModule } from '@theia/core/shared/inversify'; +import { DefaultCommunicationRecordingService } from '../common'; +import { AiHistoryPersistenceService, aiHistoryPersistenceServicePath } from '../common/history-persistence'; +import { FileCommunicationPersistenceService } from './communication-persistence-service'; export default new ContainerModule(bind => { - // bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope(); - // bind(CommunicationRecordingService).toService(DefaultCommunicationRecordingService); + bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope(); + bind(CommunicationRecordingService).toService(DefaultCommunicationRecordingService); + + bind(FileCommunicationPersistenceService).toSelf().inSingletonScope(); + bind(AiHistoryPersistenceService).to(FileCommunicationPersistenceService); + bind(ConnectionHandler) + .toDynamicValue( + ctx => + new RpcConnectionHandler(aiHistoryPersistenceServicePath, () => + ctx.container.get(AiHistoryPersistenceService) + ) + ) + .inSingletonScope(); + }); diff --git a/packages/ai-history/src/node/communication-persistence-service.ts b/packages/ai-history/src/node/communication-persistence-service.ts new file mode 100644 index 0000000000000..105cfd64882a9 --- /dev/null +++ b/packages/ai-history/src/node/communication-persistence-service.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { CommunicationHistory, CommunicationRecordingService } from '@theia/ai-core'; +import { URI } from '@theia/core'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { readdirSync, readFileSync, writeFileSync } from 'fs'; +import { AiHistoryPersistenceService } from '../common/history-persistence'; + +@injectable() +export class FileCommunicationPersistenceService implements AiHistoryPersistenceService { + + @inject(EnvVariablesServer) + protected envServer: EnvVariablesServer; + @inject(CommunicationRecordingService) + protected recordingService: CommunicationRecordingService; + + async saveHistory(agentId: string, history: CommunicationHistory): Promise { + const historyDir = await this.getHistoryDirectoryPath(); + const fileName = `${historyDir}/${agentId}.json`; + writeFileSync(fileName, JSON.stringify(history, undefined, 2)); + console.log(`Saving communication history for agent ${agentId} to ${fileName}`); + } + + private async getHistoryDirectoryPath(): Promise { + const configDir = new URI(await this.envServer.getConfigDirUri()); + const historyDir = `${configDir.path.fsPath()}/agent-communication`; + return historyDir; + } + + async loadHistory(): Promise { + const historyDir = await this.getHistoryDirectoryPath(); + const fileNames = readdirSync(historyDir); + for (const fileName of fileNames) { + const agentId = fileName.replace('.json', ''); + const filePath = `${historyDir}/${fileName}`; + try { + const historyJson = readFileSync(filePath, 'utf-8'); + const communicationHistory = JSON.parse(historyJson); + console.log(`Loaded communication history from ${agentId} from ${filePath}`); + this.recordingService.setHistory(agentId, communicationHistory); + } catch (error) { + console.log(`Could not load communication history for agent ${agentId}. Returning empty history.`); + } + } + } +}