Skip to content

Commit 205867f

Browse files
ndoschekxai
andcommitted
GH-56 WIP Ai History Persistance Service
Co-authored-by: Olaf Lessenich <[email protected]>
1 parent 7df16e0 commit 205867f

9 files changed

+194
-3
lines changed

packages/ai-core/src/common/communication-recording-service.ts packages/ai-core/src/common/communication-recording-types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ export interface CommunicationRecordingService {
3131
recordRequest(requestEntry: CommunicationHistoryEntry): void;
3232
recordResponse(responseEntry: CommunicationHistoryEntry): void;
3333
getHistory(agentId: string): CommunicationHistory;
34+
setHistory(agentId: string, history: CommunicationHistory): void;
35+
getRecordedAgents(): string[];
36+
loadHistory(): Promise<void>
3437
}

packages/ai-core/src/common/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616
export * from './agent';
17-
export * from './communication-recording-service';
17+
export * from './communication-recording-types';
1818
export * from './language-model';
1919
export * from './language-model-delegate';
2020
export * from './prompt-service';

packages/ai-history/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
},
1818
"theiaExtensions": [
1919
{
20-
"frontend": "lib/browser/ai-history-frontend-module"
20+
"frontend": "lib/browser/ai-history-frontend-module",
21+
"backend": "lib/node/ai-history-backend-module"
2122
}
2223
],
2324
"keywords": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { CommunicationRecordingService } from '@theia/ai-core';
18+
import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser';
19+
import { inject, injectable } from '@theia/core/shared/inversify';
20+
21+
@injectable()
22+
export class AIHistoryFrontendContribution implements FrontendApplicationContribution {
23+
24+
@inject(CommunicationRecordingService)
25+
protected recordingService: CommunicationRecordingService;
26+
27+
async onStart(app: FrontendApplication): Promise<void> {
28+
this.recordingService.loadHistory();
29+
}
30+
}

packages/ai-history/src/browser/ai-history-frontend-module.ts

+11
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,21 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616
import { CommunicationRecordingService } from '@theia/ai-core';
17+
import { FrontendApplicationContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser';
1718
import { ContainerModule } from '@theia/core/shared/inversify';
1819
import { DefaultCommunicationRecordingService } from '../common/communication-recording-service';
20+
import { AiHistoryPersistenceService, aiHistoryPersistenceServicePath } from '../common/history-persistence';
21+
import { AIHistoryFrontendContribution } from './ai-history-frontend-contribution';
1922

2023
export default new ContainerModule(bind => {
24+
bind(FrontendApplicationContribution).to(AIHistoryFrontendContribution).inSingletonScope();
25+
2126
bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope();
2227
bind(CommunicationRecordingService).toService(DefaultCommunicationRecordingService);
28+
29+
bind(AiHistoryPersistenceService).toDynamicValue(ctx => {
30+
const connection = ctx.container.get(WebSocketConnectionProvider);
31+
return connection.createProxy<AiHistoryPersistenceService>(aiHistoryPersistenceServicePath);
32+
}).inSingletonScope();
33+
2334
});

packages/ai-history/src/common/communication-recording-service.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,37 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616
import { CommunicationHistory, CommunicationHistoryEntry, CommunicationRecordingService } from '@theia/ai-core';
17-
import { injectable } from '@theia/core/shared/inversify';
17+
import { inject, injectable } from '@theia/core/shared/inversify';
18+
import { AiHistoryPersistenceService } from './history-persistence';
1819

1920
@injectable()
2021
export class DefaultCommunicationRecordingService implements CommunicationRecordingService {
2122

23+
@inject(AiHistoryPersistenceService)
24+
protected persistenceService: AiHistoryPersistenceService;
25+
2226
protected history: Map<string, CommunicationHistory> = new Map();
2327

28+
getRecordedAgents(): string[] {
29+
return Object.keys(this.history);
30+
}
31+
2432
getHistory(agentId: string): CommunicationHistory {
2533
return this.history.get(agentId) || [];
2634
}
2735

36+
setHistory(agentId: string, history: CommunicationHistory): void {
37+
this.history.set(agentId, history);
38+
}
39+
2840
recordRequest(requestEntry: CommunicationHistoryEntry): void {
2941
console.log('Recording request:', requestEntry.request);
3042
if (this.history.has(requestEntry.agentId)) {
3143
this.history.get(requestEntry.agentId)?.push(requestEntry);
3244
} else {
3345
this.history.set(requestEntry.agentId, [requestEntry]);
3446
}
47+
this.persistenceService.saveHistory(requestEntry.agentId, this.history.get(requestEntry.agentId)!);
3548
}
3649

3750
recordResponse(responseEntry: CommunicationHistoryEntry): void {
@@ -46,6 +59,14 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord
4659
matchingEntry.response = responseEntry.response;
4760
matchingEntry.responseTime = responseEntry.timestamp - matchingEntry.timestamp;
4861
}
62+
this.persistenceService.saveHistory(responseEntry.agentId, this.history.get(responseEntry.agentId)!);
4963
}
5064
}
65+
66+
async loadHistory(): Promise<void> {
67+
(await this.persistenceService.getRecordedAgents()).forEach(async agentId => {
68+
const history = await this.persistenceService.loadHistory(agentId);
69+
this.history.set(agentId, history);
70+
});
71+
}
5172
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { CommunicationHistory } from '@theia/ai-core';
18+
19+
export const aiHistoryPersistenceServicePath = '/services/aiHistoryPersistenceService';
20+
21+
export const AiHistoryPersistenceService = Symbol('AiHistoryPersistenceService');
22+
export interface AiHistoryPersistenceService {
23+
saveHistory(agentId: string, history: CommunicationHistory): Promise<void>;
24+
loadHistory(agentId: string): Promise<CommunicationHistory>;
25+
getRecordedAgents(): Promise<string[]>;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
import { CommunicationRecordingService } from '@theia/ai-core';
17+
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core';
18+
import { ContainerModule } from '@theia/core/shared/inversify';
19+
import { DefaultCommunicationRecordingService } from '../common';
20+
import { AiHistoryPersistenceService, aiHistoryPersistenceServicePath } from '../common/history-persistence';
21+
import { FileCommunicationPersistenceService } from './communication-persistence-service';
22+
23+
export default new ContainerModule(bind => {
24+
bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope();
25+
bind(CommunicationRecordingService).toService(DefaultCommunicationRecordingService);
26+
27+
bind(FileCommunicationPersistenceService).toSelf().inSingletonScope();
28+
bind(AiHistoryPersistenceService).to(FileCommunicationPersistenceService);
29+
bind(ConnectionHandler)
30+
.toDynamicValue(
31+
ctx =>
32+
new RpcConnectionHandler(aiHistoryPersistenceServicePath, () =>
33+
ctx.container.get(AiHistoryPersistenceService)
34+
)
35+
)
36+
.inSingletonScope();
37+
38+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
import { CommunicationHistory } from '@theia/ai-core';
17+
import { URI } from '@theia/core';
18+
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
19+
import { inject, injectable } from '@theia/core/shared/inversify';
20+
import { readdirSync, readFileSync, writeFileSync } from 'fs';
21+
import { AiHistoryPersistenceService } from '../common/history-persistence';
22+
23+
@injectable()
24+
export class FileCommunicationPersistenceService implements AiHistoryPersistenceService {
25+
26+
@inject(EnvVariablesServer)
27+
protected envServer: EnvVariablesServer;
28+
29+
async saveHistory(agentId: string, history: CommunicationHistory): Promise<void> {
30+
const historyDir = await this.getHistoryDirectoryPath();
31+
const fileName = `${historyDir}/${agentId}.json`;
32+
writeFileSync(fileName, JSON.stringify(history, undefined, 2));
33+
console.log(`Saving communication history for agent ${agentId} to ${fileName}`);
34+
}
35+
36+
private async getHistoryDirectoryPath(): Promise<string> {
37+
const configDir = new URI(await this.envServer.getConfigDirUri());
38+
const historyDir = `${configDir.path.fsPath()}/agent-communication`;
39+
return historyDir;
40+
}
41+
42+
async loadHistory(agentId: string): Promise<CommunicationHistory> {
43+
const historyDir = await this.getHistoryDirectoryPath();
44+
const filePath = `${historyDir}/${agentId}.json`;
45+
try {
46+
const historyJson = readFileSync(filePath, 'utf-8');
47+
const communicationHistory = JSON.parse(historyJson);
48+
console.log(`Loaded communication history from ${agentId} from ${filePath}`);
49+
return communicationHistory;
50+
} catch (error) {
51+
console.log(`Could not load communication history for agent ${agentId}. Returning empty history.`);
52+
}
53+
return [];
54+
}
55+
56+
async getRecordedAgents(): Promise<string[]> {
57+
const historyDir = await this.getHistoryDirectoryPath();
58+
const files = readdirSync(historyDir);
59+
return files.map((file: string) => file.replace('.json', ''));
60+
}
61+
}

0 commit comments

Comments
 (0)