Refactor Gemini class to use Conversation object in streamRequest method, update IAIClass interface, and enhance ChatWindow component for conversation handling. Add logging for conversation saving in ConversationFileSystemService and comment out OdbCache references in ServiceRegistration and main files.

This commit is contained in:
Andrew Beal 2025-10-04 13:27:40 +01:00
parent ace478c311
commit d0b4d5b852
8 changed files with 268 additions and 271 deletions

View file

@ -5,6 +5,8 @@ import type { GeminiActionDefinitions } from "Actioner/Gemini/GeminiActionDefini
import type { IAIClass } from "AIClasses/IAIClass";
import type { IPrompt } from "AIClasses/IPrompt";
import { StreamingService, type StreamChunk } from "Services/StreamingService";
import type { Conversation } from "Conversations/Conversation";
import { Role } from "Enums/Role";
export class Gemini implements IAIClass {
private readonly apiKey: string;
@ -23,10 +25,18 @@ export class Gemini implements IAIClass {
* Stream response from Gemini API
*/
public async* streamRequest(
userInput: string,
conversation: Conversation,
actioner: IActioner
): AsyncGenerator<StreamChunk, void, unknown> {
const prompt = "The users prompt is: " + userInput;
const contents = conversation.contents.map(content => ({
role: content.role === Role.User ? "user" : "model",
parts: [
{
text: content.content
}
]
}));
const requestBody = {
system_instruction: {
@ -35,20 +45,11 @@ export class Gemini implements IAIClass {
text: this.aiPrompt.systemInstruction()
},
{
text: this.aiPrompt.userInstruction()
text: await this.aiPrompt.userInstruction()
}
]
},
contents: [
{
role: "user",
parts: [
{
text: ""
},
],
},
],
contents: contents,
tools: [
{
google_search: {},

View file

@ -1,6 +1,7 @@
import type { IActioner } from "Actioner/IActioner";
import type { StreamChunk } from "Services/StreamingService";
import type { Conversation } from "Conversations/Conversation";
export interface IAIClass {
streamRequest(userInput: string, actioner: IActioner): AsyncGenerator<StreamChunk, void, unknown>;
streamRequest(conversation: Conversation, actioner: IActioner): AsyncGenerator<StreamChunk, void, unknown>;
}

View file

@ -59,7 +59,7 @@
// Stream the response
let accumulatedContent = "";
for await (const chunk of ai.streamRequest(requestToSend, actioner)) {
for await (const chunk of ai.streamRequest(conversation, actioner)) {
if (chunk.error) {
console.error("Streaming error:", chunk.error);
// Update message with error

View file

@ -1,157 +1,157 @@
import { DynamicRecordProp } from 'Enums/DynamicRecord';
import { version as uuidVersion } from 'uuid';
import { validate as uuidValidate } from 'uuid';
import type { OdbCache } from './OdbCache';
import { Resolve } from 'Services/DependencyService';
import { Services } from 'Services/Services';
import type AIAgentPlugin from 'main';
import { TAbstractFile, TFile, type Vault } from 'obsidian';
interface DynamicProps {
[key: string]: any;
}
// import { version as uuidVersion } from 'uuid';
// import { validate as uuidValidate } from 'uuid';
// import type { OdbCache } from './OdbCache';
// import { Resolve } from 'Services/DependencyService';
// import { Services } from 'Services/Services';
// import type AIAgentPlugin from 'main';
// import { TAbstractFile, TFile, type Vault } from 'obsidian';
export class DynamicRecord {
// interface DynamicProps {
// [key: string]: any;
// }
public readonly type: string;
public readonly schema: object;
public readonly objectId: string;
public readonly recordPath: string;
// export class DynamicRecord {
public props: DynamicProps;
// public readonly type: string;
// public readonly schema: object;
// public readonly objectId: string;
// public readonly recordPath: string;
private vault: Vault;
private odbCache: OdbCache;
// public props: DynamicProps;
public constructor(recordPath: string, schema: object, record: { [key: string]: any }) {
this.odbCache = Resolve<OdbCache>(Services.OdbCache);
this.vault = Resolve<AIAgentPlugin>(Services.AIAgentPlugin).app.vault;
// private vault: Vault;
// private odbCache: OdbCache;
this.schema = schema;
this.recordPath = recordPath;
// public constructor(recordPath: string, schema: object, record: { [key: string]: any }) {
// this.odbCache = Resolve<OdbCache>(Services.OdbCache);
// this.vault = Resolve<AIAgentPlugin>(Services.AIAgentPlugin).app.vault;
if (record[DynamicRecordProp.Type] === undefined || record[DynamicRecordProp.ObjectId] === undefined) {
throw new Error(`Invalid record format: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
}
// this.schema = schema;
// this.recordPath = recordPath;
// if (record[DynamicRecordProp.Type] === undefined || record[DynamicRecordProp.ObjectId] === undefined) {
// throw new Error(`Invalid record format: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
// }
this.type = record[DynamicRecordProp.Type];
this.objectId = record[DynamicRecordProp.ObjectId];
// this.type = record[DynamicRecordProp.Type];
// this.objectId = record[DynamicRecordProp.ObjectId];
let parsedRecord: Record<string,any> = {};
for (let key in schema) {
if (!(key in record) || key === DynamicRecordProp.Type || key === DynamicRecordProp.ObjectId) {
continue;
}
// let parsedRecord: Record<string,any> = {};
// for (let key in schema) {
// if (!(key in record) || key === DynamicRecordProp.Type || key === DynamicRecordProp.ObjectId) {
// continue;
// }
parsedRecord[key] = this.parse(record[key]);
}
// parsedRecord[key] = this.parse(record[key]);
// }
this.props = this.createProxy(parsedRecord);
}
// this.props = this.createProxy(parsedRecord);
// }
public save() {
let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
// public save() {
// let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
if (file == null || !(file instanceof TAbstractFile)) {
this.vault.create(this.recordPath, this.toFileContent());
}
// if (file == null || !(file instanceof TAbstractFile)) {
// this.vault.create(this.recordPath, this.toFileContent());
// }
this.vault.modify(file as TFile, this.toFileContent());
}
// this.vault.modify(file as TFile, this.toFileContent());
// }
public delete() {
let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
// public delete() {
// let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
if (file == null || !(file instanceof TAbstractFile)) {
return;
}
// if (file == null || !(file instanceof TAbstractFile)) {
// return;
// }
this.vault.delete(file as TFile);
}
// this.vault.delete(file as TFile);
// }
public move(newPath: string) {
let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
// public move(newPath: string) {
// let file: TAbstractFile | null = this.vault.getAbstractFileByPath(this.recordPath);
if (file == null || !(file instanceof TAbstractFile)) {
return;
}
// if (file == null || !(file instanceof TAbstractFile)) {
// return;
// }
this.vault.rename(file as TFile, newPath);
}
// this.vault.rename(file as TFile, newPath);
// }
private parse(data: any): any {
if (typeof data === null) {
return data;
}
if (Array.isArray(data)) {
let arr: any[] = [];
for (let item of data) {
arr.push(this.parse(item));
}
return arr;
}
if (typeof data === "object") {
let obj: Record<string,any> = {};
for (let key in data) {
obj[key] = this.parse(data[key]);
}
return this.createProxy(obj);
}
return data;
}
// private parse(data: any): any {
// if (typeof data === null) {
// return data;
// }
// if (Array.isArray(data)) {
// let arr: any[] = [];
// for (let item of data) {
// arr.push(this.parse(item));
// }
// return arr;
// }
// if (typeof data === "object") {
// let obj: Record<string,any> = {};
// for (let key in data) {
// obj[key] = this.parse(data[key]);
// }
// return this.createProxy(obj);
// }
// return data;
// }
private createProxy(data: object): DynamicProps {
return new Proxy(data, {
get: (target, prop, receiver) => {
if (this.isObjectId(String(prop))) {
return this.odbCache.getRecord(String(prop));
}
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value, receiver) => {
return Reflect.set(target, prop, value, receiver);
}
});
}
// private createProxy(data: object): DynamicProps {
// return new Proxy(data, {
// get: (target, prop, receiver) => {
// if (this.isObjectId(String(prop))) {
// return this.odbCache.getRecord(String(prop));
// }
// return Reflect.get(target, prop, receiver);
// },
// set: (target, prop, value, receiver) => {
// return Reflect.set(target, prop, value, receiver);
// }
// });
// }
private isObjectId(key: string): boolean {
return uuidValidate(key) && uuidVersion(key) === 4;
}
// private isObjectId(key: string): boolean {
// return uuidValidate(key) && uuidVersion(key) === 4;
// }
private toFileContent(): string {
let content: Record<string,any> = {};
// private toFileContent(): string {
// let content: Record<string,any> = {};
content[DynamicRecordProp.Type] = this.type;
content[DynamicRecordProp.ObjectId] = this.objectId;
// content[DynamicRecordProp.Type] = this.type;
// content[DynamicRecordProp.ObjectId] = this.objectId;
// use the schema when saving to file in case the AI did something funky
for (let key in this.schema) {
if (!(key in this.props) || key === DynamicRecordProp.Type || key === DynamicRecordProp.ObjectId) {
continue;
}
content[key] = this.serialise(this.props[key]);
}
return JSON.stringify(content, null, 4);
}
// // use the schema when saving to file in case the AI did something funky
// for (let key in this.schema) {
// if (!(key in this.props) || key === DynamicRecordProp.Type || key === DynamicRecordProp.ObjectId) {
// continue;
// }
// content[key] = this.serialise(this.props[key]);
// }
// return JSON.stringify(content, null, 4);
// }
private serialise(value: any): any {
if (value instanceof DynamicRecord) {
return value.objectId;
}
if (Array.isArray(value)) {
let arr: any[] = [];
for (let item of value) {
arr.push(this.serialise(item));
}
return arr;
}
if (typeof value === "object") {
let obj: Record<string,any> = {};
for (let key in value) {
obj[key] = this.serialise(value[key]);
}
return obj;
}
return value;
}
}
// private serialise(value: any): any {
// if (value instanceof DynamicRecord) {
// return value.objectId;
// }
// if (Array.isArray(value)) {
// let arr: any[] = [];
// for (let item of value) {
// arr.push(this.serialise(item));
// }
// return arr;
// }
// if (typeof value === "object") {
// let obj: Record<string,any> = {};
// for (let key in value) {
// obj[key] = this.serialise(value[key]);
// }
// return obj;
// }
// return value;
// }
// }

View file

@ -1,131 +1,131 @@
import { TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { DynamicRecord } from "./DynamicRecord";
import { Resolve } from "Services/DependencyService";
import type AIAgentPlugin from "main";
import { Services } from "Services/Services";
import { Path } from "Enums/Path";
import { DynamicRecordProp } from "Enums/DynamicRecord";
import { FileAction } from "Enums/FileAction";
import { isValidJson } from "Helpers/Helpers";
// import { TAbstractFile, TFile, TFolder, Vault } from "obsidian";
// import { DynamicRecord } from "./DynamicRecord";
// import { Resolve } from "Services/DependencyService";
// import type AIAgentPlugin from "main";
// import { Services } from "Services/Services";
// import { Path } from "Enums/Path";
// import { DynamicRecordProp } from "Enums/DynamicRecord";
// import { FileAction } from "Enums/FileAction";
// import { isValidJson } from "Helpers/Helpers";
export class OdbCache {
// export class OdbCache {
private vault: Vault;
// private vault: Vault;
private schemas: Map<string, object> = new Map<string, object>();
private cache: Map<string, DynamicRecord> = new Map<string, DynamicRecord>();
// private schemas: Map<string, object> = new Map<string, object>();
// private cache: Map<string, DynamicRecord> = new Map<string, DynamicRecord>();
public constructor() {
this.vault = Resolve<AIAgentPlugin>(Services.AIAgentPlugin).app.vault;
}
// public constructor() {
// this.vault = Resolve<AIAgentPlugin>(Services.AIAgentPlugin).app.vault;
// }
public getSchemas(): Map<string, object> {
return this.schemas;
}
// public getSchemas(): Map<string, object> {
// return this.schemas;
// }
public getRecord(objectId: string): DynamicRecord | null {
return this.cache.get(objectId) ?? null;
}
// public getRecord(objectId: string): DynamicRecord | null {
// return this.cache.get(objectId) ?? null;
// }
public async buildCache() {
await this.loadSchemas();
// public async buildCache() {
// await this.loadSchemas();
let recordDir: TAbstractFile | null = this.vault.getAbstractFileByPath(Path.Records);
// let recordDir: TAbstractFile | null = this.vault.getAbstractFileByPath(Path.Records);
if (!(recordDir instanceof TFolder)) {
return;
}
// if (!(recordDir instanceof TFolder)) {
// return;
// }
this.recursiveBuild(recordDir);
}
// this.recursiveBuild(recordDir);
// }
public async onFileChanged(file: TAbstractFile, fileAction: FileAction) {
if (file instanceof TFolder) {
for (let child of file.children) {
this.onFileChanged(child, fileAction);
}
return;
}
// public async onFileChanged(file: TAbstractFile, fileAction: FileAction) {
// if (file instanceof TFolder) {
// for (let child of file.children) {
// this.onFileChanged(child, fileAction);
// }
// return;
// }
for (let [_, record] of this.cache) {
if (record.recordPath === file.path) {
switch (fileAction) {
case FileAction.Create:
this.addToCache(file as TFile);
break;
case FileAction.Modify, FileAction.Rename:
this.removeFromCache(record.objectId);
this.addToCache(file as TFile);
break;
case FileAction.Delete:
this.removeFromCache(record.objectId);
break;
}
}
}
}
// for (let [_, record] of this.cache) {
// if (record.recordPath === file.path) {
// switch (fileAction) {
// case FileAction.Create:
// this.addToCache(file as TFile);
// break;
// case FileAction.Modify, FileAction.Rename:
// this.removeFromCache(record.objectId);
// this.addToCache(file as TFile);
// break;
// case FileAction.Delete:
// this.removeFromCache(record.objectId);
// break;
// }
// }
// }
// }
private async recursiveBuild(child : TAbstractFile) {
if (child instanceof TFolder) {
for (let subChild of child.children) {
await this.recursiveBuild(subChild);
}
} else if (child instanceof TFile && child.extension === "json") {
await this.addToCache(child);
}
}
// private async recursiveBuild(child : TAbstractFile) {
// if (child instanceof TFolder) {
// for (let subChild of child.children) {
// await this.recursiveBuild(subChild);
// }
// } else if (child instanceof TFile && child.extension === "json") {
// await this.addToCache(child);
// }
// }
private async createDynamicRecord(file: TFile): Promise<DynamicRecord> {
let contents: { [key: string]: any } = JSON.parse(await this.vault.read(file));
// private async createDynamicRecord(file: TFile): Promise<DynamicRecord> {
// let contents: { [key: string]: any } = JSON.parse(await this.vault.read(file));
if (contents[DynamicRecordProp.Type] === undefined || contents[DynamicRecordProp.ObjectId] === undefined) {
throw new Error(`Invalid record format in file ${file.path}: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
}
// if (contents[DynamicRecordProp.Type] === undefined || contents[DynamicRecordProp.ObjectId] === undefined) {
// throw new Error(`Invalid record format in file ${file.path}: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
// }
let schema: object | undefined = this.schemas.get(contents[DynamicRecordProp.Type]);
// let schema: object | undefined = this.schemas.get(contents[DynamicRecordProp.Type]);
if (schema === undefined) {
throw new Error(`No schema found for record type ${contents[DynamicRecordProp.Type]} in file ${file.path}`);
}
// if (schema === undefined) {
// throw new Error(`No schema found for record type ${contents[DynamicRecordProp.Type]} in file ${file.path}`);
// }
return new DynamicRecord(file.path, schema, contents);
}
// return new DynamicRecord(file.path, schema, contents);
// }
private async loadSchemas() {
let schemaDir: TAbstractFile | null = this.vault.getAbstractFileByPath(Path.Schemas);
// private async loadSchemas() {
// let schemaDir: TAbstractFile | null = this.vault.getAbstractFileByPath(Path.Schemas);
if (!(schemaDir instanceof TFolder)) {
return;
}
// if (!(schemaDir instanceof TFolder)) {
// return;
// }
for (let child of schemaDir.children) {
if (!(child instanceof TFile) || child.extension !== "json") {
continue;
}
// for (let child of schemaDir.children) {
// if (!(child instanceof TFile) || child.extension !== "json") {
// continue;
// }
let contents: string = await this.vault.read(child);
// let contents: string = await this.vault.read(child);
if (!isValidJson(contents)) {
console.warn(`Invalid schema format in file ${child.path}`);
continue;
}
// if (!isValidJson(contents)) {
// console.warn(`Invalid schema format in file ${child.path}`);
// continue;
// }
let schema: Record<string,any> = JSON.parse(contents);
// let schema: Record<string,any> = JSON.parse(contents);
if (schema[DynamicRecordProp.Type] === undefined || schema[DynamicRecordProp.ObjectId] === undefined) {
throw new Error(`Invalid schema format in file ${child.path}: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
}
// if (schema[DynamicRecordProp.Type] === undefined || schema[DynamicRecordProp.ObjectId] === undefined) {
// throw new Error(`Invalid schema format in file ${child.path}: missing ${DynamicRecordProp.Type} or ${DynamicRecordProp.ObjectId}`);
// }
this.schemas.set(schema[DynamicRecordProp.Type], schema);
}
}
// this.schemas.set(schema[DynamicRecordProp.Type], schema);
// }
// }
private async addToCache(file: TFile) {
let record: DynamicRecord = await this.createDynamicRecord(file);
this.cache.set(record.objectId, record);
}
// private async addToCache(file: TFile) {
// let record: DynamicRecord = await this.createDynamicRecord(file);
// this.cache.set(record.objectId, record);
// }
private async removeFromCache(objectId: string) {
this.cache.delete(objectId);
}
}
// private async removeFromCache(objectId: string) {
// this.cache.delete(objectId);
// }
// }

View file

@ -41,6 +41,7 @@ export class ConversationFileSystemService {
};
await this.fileSystemService.writeObjectToFile(this.currentConversationPath, conversationData);
console.log("Conversation saved to:", this.currentConversationPath);
return this.currentConversationPath;
}

View file

@ -1,6 +1,6 @@
import { AIProvider } from "Enums/ApiProvider";
import type AIAgentPlugin from "main";
import { OdbCache } from "ODB/Core/OdbCache";
//import { OdbCache } from "ODB/Core/OdbCache";
import { RegisterSingleton, RegisterTransient } from "./DependencyService";
import { ModalService } from "./ModalService";
import { Services } from "./Services";
@ -19,7 +19,7 @@ import { ConversationFileSystemService } from "./ConversationFileSystemService";
export function RegisterDependencies(plugin: AIAgentPlugin) {
RegisterSingleton(Services.MessageService, new MessageService());
RegisterSingleton(Services.AIAgentPlugin, plugin);
RegisterSingleton(Services.OdbCache, new OdbCache());
//RegisterSingleton(Services.OdbCache, new OdbCache());
RegisterSingleton(Services.ModalService, new ModalService())
RegisterSingleton(Services.FileSystemService, new FileSystemService());
RegisterSingleton(Services.ConversationFileSystemService, new ConversationFileSystemService());

38
main.ts
View file

@ -4,8 +4,8 @@ import { AIProvider } from './Enums/ApiProvider';
import { MainView, VIEW_TYPE_MAIN } from 'Views/MainView';
import { Services } from 'Services/Services';
import { OdbCache } from 'ODB/Core/OdbCache';
import { FileAction } from 'Enums/FileAction';
// import { OdbCache } from 'ODB/Core/OdbCache';
// import { FileAction } from 'Enums/FileAction';
import { Path } from 'Enums/Path';
import { RegisterAiProvider, RegisterDependencies } from 'Services/ServiceRegistration';
@ -71,22 +71,22 @@ export default class AIAgentPlugin extends Plugin {
console.log('click', evt);
});
let odbCache: OdbCache = Resolve<OdbCache>(Services.OdbCache)
// let odbCache: OdbCache = Resolve<OdbCache>(Services.OdbCache)
await odbCache.buildCache();
// await odbCache.buildCache();
this.registerEvent(
this.app.vault.on(FileAction.Create, (file) => odbCache.onFileChanged(file, FileAction.Create))
);
this.registerEvent(
this.app.vault.on(FileAction.Modify, (file) => odbCache.onFileChanged(file, FileAction.Modify))
);
this.registerEvent(
this.app.vault.on(FileAction.Delete, (file) => odbCache.onFileChanged(file, FileAction.Delete))
);
this.registerEvent(
this.app.vault.on(FileAction.Rename, (file) => odbCache.onFileChanged(file, FileAction.Rename))
);
// this.registerEvent(
// this.app.vault.on(FileAction.Create, (file) => odbCache.onFileChanged(file, FileAction.Create))
// );
// this.registerEvent(
// this.app.vault.on(FileAction.Modify, (file) => odbCache.onFileChanged(file, FileAction.Modify))
// );
// this.registerEvent(
// this.app.vault.on(FileAction.Delete, (file) => odbCache.onFileChanged(file, FileAction.Delete))
// );
// this.registerEvent(
// this.app.vault.on(FileAction.Rename, (file) => odbCache.onFileChanged(file, FileAction.Rename))
// );
}
async onunload() {
@ -186,12 +186,6 @@ function CreateDirectories(plugin: AIAgentPlugin) {
if (vault.getAbstractFileByPath(Path.Root) == null) {
vault.createFolder(Path.Root);
}
if (vault.getAbstractFileByPath(Path.Schemas) == null) {
vault.createFolder(Path.Schemas);
}
if (vault.getAbstractFileByPath(Path.Records) == null) {
vault.createFolder(Path.Records);
}
});
}