mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
Implement thought signature tracking for Gemini function calls to support Gemini 3 requirements. Migrate OpenAI integration from Chat Completions to Responses API with proper input/output item handling. Add cross-provider compatibility via legacy text format fallback for conversations without thought signatures or tool IDs. Improve chat auto-scroll behavior and conversation validation. Add and update AI class and conversation tests.
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import { Resolve } from "./DependencyService";
|
|
import { Services } from "./Services";
|
|
import type { IConversationNamingService } from "AIClasses/IConversationNamingService";
|
|
import type { ConversationFileSystemService } from "./ConversationFileSystemService";
|
|
import type { Conversation } from "Conversations/Conversation";
|
|
import type { VaultService } from "./VaultService";
|
|
import { Path } from "Enums/Path";
|
|
import { Exception } from "Helpers/Exception";
|
|
import { Notice } from "obsidian";
|
|
import { AbortService } from "./AbortService";
|
|
|
|
export class ConversationNamingService {
|
|
private readonly stackLimit: number = 1000;
|
|
|
|
private namingProvider: IConversationNamingService | undefined;
|
|
private conversationService: ConversationFileSystemService;
|
|
private vaultService: VaultService;
|
|
private abortService: AbortService;
|
|
|
|
constructor() {
|
|
this.conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
|
|
this.vaultService = Resolve<VaultService>(Services.VaultService);
|
|
this.abortService = Resolve<AbortService>(Services.AbortService);
|
|
}
|
|
|
|
public resolveNamingProvider() {
|
|
this.namingProvider = Resolve<IConversationNamingService>(Services.IConversationNamingService);
|
|
}
|
|
|
|
public async requestName(conversation: Conversation, userPrompt: string, onNameChanged: ((name: string) => void) | undefined) {
|
|
await this.abortService.abortableOperation(async () => {
|
|
if (!this.namingProvider) {
|
|
return;
|
|
}
|
|
|
|
const conversationPath = this.conversationService.getCurrentConversationPath();
|
|
|
|
if (!conversationPath) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const generatedName: string = await this.namingProvider.generateName(userPrompt);
|
|
const validatedName: string = await this.validateName(generatedName);
|
|
|
|
const stillExists = this.conversationService.getCurrentConversationPath() === conversationPath;
|
|
if (!stillExists) {
|
|
return;
|
|
}
|
|
|
|
const updateResult = await this.conversationService.updateConversationTitle(conversationPath, validatedName);
|
|
|
|
if (updateResult instanceof Error) {
|
|
Exception.throw(updateResult);
|
|
}
|
|
|
|
conversation.title = validatedName;
|
|
const saveResult = await this.conversationService.saveConversation(conversation);
|
|
|
|
if (saveResult instanceof Error) {
|
|
Exception.throw(saveResult);
|
|
}
|
|
|
|
onNameChanged?.(conversation.title);
|
|
} catch (error) {
|
|
if (!AbortService.isAbortError(error)) {
|
|
Exception.log(error);
|
|
new Notice(`Failed to name conversation '${conversation.title}'`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private async validateName(generatedName: string): Promise<string> {
|
|
const cleanedTitle = generatedName.trim().replace(/^["']|["']$/g, "");
|
|
|
|
let index = 1;
|
|
let availableTitle = cleanedTitle;
|
|
while (await this.vaultService.exists(`${Path.Conversations}/${availableTitle}.json`, true)) {
|
|
availableTitle = `${cleanedTitle}(${index})`;
|
|
index++;
|
|
|
|
if (index > this.stackLimit) {
|
|
Exception.throw(`Stack limit reached when trying to generate conversation name for "${cleanedTitle}"`);
|
|
}
|
|
}
|
|
return availableTitle;
|
|
}
|
|
}
|