From d0b4d5b85276fa4042c71030b0436d98b6ac65a2 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sat, 4 Oct 2025 13:27:40 +0100 Subject: [PATCH] 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. --- AIClasses/Gemini/Gemini.ts | 27 +-- AIClasses/IAIClass.ts | 3 +- Components/ChatWindow.svelte | 2 +- ODB/Core/DynamicRecord.ts | 258 +++++++++++----------- ODB/Core/OdbCache.ts | 206 ++++++++--------- Services/ConversationFileSystemService.ts | 1 + Services/ServiceRegistration.ts | 4 +- main.ts | 38 ++-- 8 files changed, 268 insertions(+), 271 deletions(-) diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index c0be4f7..bbe4d79 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -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 { - 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: {}, diff --git a/AIClasses/IAIClass.ts b/AIClasses/IAIClass.ts index 0674399..9a1f6dc 100644 --- a/AIClasses/IAIClass.ts +++ b/AIClasses/IAIClass.ts @@ -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; + streamRequest(conversation: Conversation, actioner: IActioner): AsyncGenerator; } \ No newline at end of file diff --git a/Components/ChatWindow.svelte b/Components/ChatWindow.svelte index 8328ac2..ee17658 100644 --- a/Components/ChatWindow.svelte +++ b/Components/ChatWindow.svelte @@ -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 diff --git a/ODB/Core/DynamicRecord.ts b/ODB/Core/DynamicRecord.ts index ea72dd3..a1eeba1 100644 --- a/ODB/Core/DynamicRecord.ts +++ b/ODB/Core/DynamicRecord.ts @@ -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(Services.OdbCache); - this.vault = Resolve(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(Services.OdbCache); +// this.vault = Resolve(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 = {}; - for (let key in schema) { - if (!(key in record) || key === DynamicRecordProp.Type || key === DynamicRecordProp.ObjectId) { - continue; - } +// let parsedRecord: Record = {}; +// 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 = {}; - 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 = {}; +// 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 = {}; +// private toFileContent(): string { +// let content: Record = {}; - 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 = {}; - for (let key in value) { - obj[key] = this.serialise(value[key]); - } - return obj; - } - return value; - } -} \ No newline at end of file +// 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 = {}; +// for (let key in value) { +// obj[key] = this.serialise(value[key]); +// } +// return obj; +// } +// return value; +// } +// } \ No newline at end of file diff --git a/ODB/Core/OdbCache.ts b/ODB/Core/OdbCache.ts index 836e05b..5edba88 100644 --- a/ODB/Core/OdbCache.ts +++ b/ODB/Core/OdbCache.ts @@ -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 = new Map(); - private cache: Map = new Map(); +// private schemas: Map = new Map(); +// private cache: Map = new Map(); - public constructor() { - this.vault = Resolve(Services.AIAgentPlugin).app.vault; - } +// public constructor() { +// this.vault = Resolve(Services.AIAgentPlugin).app.vault; +// } - public getSchemas(): Map { - return this.schemas; - } +// public getSchemas(): Map { +// 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 { - let contents: { [key: string]: any } = JSON.parse(await this.vault.read(file)); +// private async createDynamicRecord(file: TFile): Promise { +// 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 = JSON.parse(contents); +// let schema: Record = 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); - } -} \ No newline at end of file +// private async removeFromCache(objectId: string) { +// this.cache.delete(objectId); +// } +// } \ No newline at end of file diff --git a/Services/ConversationFileSystemService.ts b/Services/ConversationFileSystemService.ts index 018c012..ff85da9 100644 --- a/Services/ConversationFileSystemService.ts +++ b/Services/ConversationFileSystemService.ts @@ -41,6 +41,7 @@ export class ConversationFileSystemService { }; await this.fileSystemService.writeObjectToFile(this.currentConversationPath, conversationData); + console.log("Conversation saved to:", this.currentConversationPath); return this.currentConversationPath; } diff --git a/Services/ServiceRegistration.ts b/Services/ServiceRegistration.ts index 2c54c72..bedb83f 100644 --- a/Services/ServiceRegistration.ts +++ b/Services/ServiceRegistration.ts @@ -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()); diff --git a/main.ts b/main.ts index 2fef405..1a26fb4 100644 --- a/main.ts +++ b/main.ts @@ -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(Services.OdbCache) + // let odbCache: OdbCache = Resolve(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); - } }); }