From 6656f4c49531fbb98c49d8e0e3ebdb11faee051e Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sun, 19 Oct 2025 00:58:30 +0100 Subject: [PATCH] feat: implement Claude and OpenAI conversation naming service - Add Claude and OpenAI ConversationNamingService with API integration - Register Claude and OpenAI naming service in dependency injection - Add model and URL constants for naming --- .../Claude/ClaudeConversationNamingService.ts | 50 +++++++++++++++- .../Gemini/GeminiConversationNamingService.ts | 4 +- .../OpenAI/OpenAIConversationNamingService.ts | 57 +++++++++++++++++++ Enums/ApiProvider.ts | 15 ++++- Services/ServiceRegistration.ts | 2 + 5 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 AIClasses/OpenAI/OpenAIConversationNamingService.ts diff --git a/AIClasses/Claude/ClaudeConversationNamingService.ts b/AIClasses/Claude/ClaudeConversationNamingService.ts index b5038ce..364bfe6 100644 --- a/AIClasses/Claude/ClaudeConversationNamingService.ts +++ b/AIClasses/Claude/ClaudeConversationNamingService.ts @@ -1,9 +1,53 @@ +import { Resolve } from "Services/DependencyService"; +import { Services } from "Services/Services"; import type { IConversationNamingService } from "AIClasses/IConversationNamingService"; +import type AIAgentPlugin from "main"; +import { AIProviderURL, AIProviderModel } from "Enums/ApiProvider"; +import { Role } from "Enums/Role"; +import { NamePrompt } from "AIClasses/NamePrompt"; export class ClaudeConversationNamingService implements IConversationNamingService { - public async generateName(userPrompt: string, abortSignal?: AbortSignal): Promise { - throw new Error("Method not implemented."); + private readonly apiKey: string; + + public constructor() { + this.apiKey = Resolve(Services.AIAgentPlugin).settings.apiKey; + } + + public async generateName(userPrompt: string, abortSignal?: AbortSignal): Promise { + + const requestBody = { + model: AIProviderModel.ClaudeNamer, + max_tokens: 50, + system: NamePrompt, + messages: [{ + role: Role.User, + content: userPrompt + }] + }; + + const response = await fetch(AIProviderURL.ClaudeNamer, { + method: 'POST', + headers: { + 'x-api-key': this.apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify(requestBody), + signal: abortSignal + }); + + if (!response.ok) { + throw new Error(`Claude API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const generatedName = data.content?.[0]?.text; + + if (!generatedName) { + throw new Error("Failed to generate conversation name"); + } + + return generatedName; } - } \ No newline at end of file diff --git a/AIClasses/Gemini/GeminiConversationNamingService.ts b/AIClasses/Gemini/GeminiConversationNamingService.ts index b574c96..55cf5a0 100644 --- a/AIClasses/Gemini/GeminiConversationNamingService.ts +++ b/AIClasses/Gemini/GeminiConversationNamingService.ts @@ -7,11 +7,11 @@ import { Role } from "Enums/Role"; import { NamePrompt } from "AIClasses/NamePrompt"; export class GeminiConversationNamingService implements IConversationNamingService { + private readonly apiKey: string; - private readonly plugin: AIAgentPlugin = Resolve(Services.AIAgentPlugin); public constructor() { - this.apiKey = this.plugin.settings.apiKey; + this.apiKey = Resolve(Services.AIAgentPlugin).settings.apiKey; } public async generateName(userPrompt: string, abortSignal?: AbortSignal): Promise { diff --git a/AIClasses/OpenAI/OpenAIConversationNamingService.ts b/AIClasses/OpenAI/OpenAIConversationNamingService.ts new file mode 100644 index 0000000..89a095b --- /dev/null +++ b/AIClasses/OpenAI/OpenAIConversationNamingService.ts @@ -0,0 +1,57 @@ +import { Resolve } from "Services/DependencyService"; +import { Services } from "Services/Services"; +import type { IConversationNamingService } from "AIClasses/IConversationNamingService"; +import type AIAgentPlugin from "main"; +import { AIProviderURL, AIProviderModel } from "Enums/ApiProvider"; +import { Role } from "Enums/Role"; +import { NamePrompt } from "AIClasses/NamePrompt"; + +export class OpenAIConversationNamingService implements IConversationNamingService { + + private readonly apiKey: string; + + public constructor() { + this.apiKey = Resolve(Services.AIAgentPlugin).settings.apiKey; + } + + public async generateName(userPrompt: string, abortSignal?: AbortSignal): Promise { + + const requestBody = { + model: AIProviderModel.OpenAINamer, + max_tokens: 50, + messages: [ + { + role: "system", + content: NamePrompt + }, + { + role: Role.User, + content: userPrompt + } + ] + }; + + const response = await fetch(AIProviderURL.OpenAINamer, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + signal: abortSignal + }); + + if (!response.ok) { + throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const generatedName = data.choices?.[0]?.message?.content; + + if (!generatedName) { + throw new Error("Failed to generate conversation name"); + } + + return generatedName; + } +} \ No newline at end of file diff --git a/Enums/ApiProvider.ts b/Enums/ApiProvider.ts index 8192a8a..5df1d89 100644 --- a/Enums/ApiProvider.ts +++ b/Enums/ApiProvider.ts @@ -6,11 +6,20 @@ export enum AIProvider { export enum AIProviderModel { Claude = "claude-sonnet-4-5", + ClaudeNamer = "claude-haiku-4-5", + Gemini = "gemini-2.5-flash", - OpenAI = "" + GeminiNamer = "gemini-2.5-flash", + + OpenAI = "gpt-5", + OpenAINamer = "gpt-5-nano" } export enum AIProviderURL { - Gemini = `https://generativelanguage.googleapis.com/v1beta/models/${AIProviderModel.Gemini}:streamGenerateContent?key=API_KEY&alt=sse`, - GeminiNamer = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=API_KEY" + ClaudeNamer = "https://api.anthropic.com/v1/messages", + + Gemini = `https://generativelanguage.googleapis.com/v1beta/models/${AIProviderModel.GeminiNamer}:streamGenerateContent?key=API_KEY&alt=sse`, + GeminiNamer = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=API_KEY", + + OpenAINamer = "https://api.openai.com/v1/chat/completions" } \ No newline at end of file diff --git a/Services/ServiceRegistration.ts b/Services/ServiceRegistration.ts index 295ec38..3d31d94 100644 --- a/Services/ServiceRegistration.ts +++ b/Services/ServiceRegistration.ts @@ -24,6 +24,7 @@ import { GeminiTokenService } from "AIClasses/Gemini/GeminiTokenService"; import { StatusBarService } from "./StatusBarService"; import { ClaudeTokenService } from "AIClasses/Claude/ClaudeTokenService"; import { OpenAITokenService } from "AIClasses/OpenAI/OpenAITokenService"; +import { ClaudeConversationNamingService } from "AIClasses/Claude/ClaudeConversationNamingService"; export function RegisterDependencies(plugin: AIAgentPlugin) { RegisterSingleton(Services.AIAgentPlugin, plugin); @@ -50,6 +51,7 @@ export function RegisterDependencies(plugin: AIAgentPlugin) { export function RegisterAiProvider(plugin: AIAgentPlugin) { if (plugin.settings.apiProvider == AIProvider.Claude) { RegisterSingleton(Services.ITokenService, new ClaudeTokenService()); + RegisterSingleton(Services.IConversationNamingService, new ClaudeConversationNamingService()); } else if (plugin.settings.apiProvider == AIProvider.Gemini) { RegisterSingleton(Services.IAIClass, new Gemini());