From b6f363656f4c9cac57ba1330c0823e038b4c2347 Mon Sep 17 00:00:00 2001 From: Shashank Saga Date: Tue, 6 Jan 2026 14:25:43 +0530 Subject: [PATCH 1/3] feat(ai): add Gemini provider using AIProvider abstraction --- .../src/api/ai/core/AIProviderRegistry.ts | 29 ++++++++++--------- .../src/api/ai/providers/GeminiProvider.ts | 25 ++++++++++++++++ 2 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 LocalMind-Backend/src/api/ai/providers/GeminiProvider.ts diff --git a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts index d60b66b..0a35a26 100644 --- a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts +++ b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts @@ -2,31 +2,29 @@ import { AIProvider } from './AIProvider' import { AICapability } from './types' +import { GeminiProvider } from '../providers/GeminiProvider' class AIProviderRegistry { private providers = new Map() register(provider: AIProvider) { - if (this.providers.has(provider.name)) { - throw new Error( - `A provider with the name "${provider.name}" is already registered.` - ) + if (this.providers.has(provider.name)) { + throw new Error( + `A provider with the name "${provider.name}" is already registered.` + ) + } + this.providers.set(provider.name, provider) } - this.providers.set(provider.name, provider) -} - - get(name: string): AIProvider | undefined { return this.providers.get(name) } - findByCapabilities(capabilities: AICapability[]): AIProvider[] { - return Array.from(this.providers.values()).filter(provider => - capabilities.every(cap => provider.supports(cap)) - ) -} - + findByCapabilities(capabilities: AICapability[]): AIProvider[] { + return Array.from(this.providers.values()).filter(provider => + capabilities.every(cap => provider.supports(cap)) + ) + } list(): AIProvider[] { return Array.from(this.providers.values()) @@ -34,3 +32,6 @@ class AIProviderRegistry { } export const aiProviderRegistry = new AIProviderRegistry() + +// register providers (safe, non-breaking) +aiProviderRegistry.register(new GeminiProvider()) diff --git a/LocalMind-Backend/src/api/ai/providers/GeminiProvider.ts b/LocalMind-Backend/src/api/ai/providers/GeminiProvider.ts new file mode 100644 index 0000000..855f15e --- /dev/null +++ b/LocalMind-Backend/src/api/ai/providers/GeminiProvider.ts @@ -0,0 +1,25 @@ +// src/api/ai/providers/GeminiProvider.ts + +import { AIProvider } from '../core/AIProvider' +import { AICapability } from '../core/types' + +export class GeminiProvider implements AIProvider { + readonly name = 'gemini' + + readonly capabilities = new Set([ + 'cloud', + 'multimodal', + ]) + + supports(capability: AICapability): boolean { + return this.capabilities.has(capability) + } + + async generateText(input: { + prompt: string + context?: string + }): Promise { + // TEMP mock response (safe, non-breaking) + return `[Gemini] ${input.prompt}` + } +} From bc0ef74009e174b457a1668e14641de6163dc887 Mon Sep 17 00:00:00 2001 From: Shashank Saga Date: Tue, 6 Jan 2026 15:04:04 +0530 Subject: [PATCH 2/3] feat(ai): add provider health tracking and safe fallback routing --- .../src/api/ai/core/AIProviderRegistry.ts | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts index 0a35a26..7108dc9 100644 --- a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts +++ b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts @@ -4,8 +4,16 @@ import { AIProvider } from './AIProvider' import { AICapability } from './types' import { GeminiProvider } from '../providers/GeminiProvider' +type ProviderHealth = { + failures: number + lastFailureAt?: number +} + +const MAX_FAILURES = 3 + class AIProviderRegistry { private providers = new Map() + private health = new Map() register(provider: AIProvider) { if (this.providers.has(provider.name)) { @@ -13,25 +21,100 @@ class AIProviderRegistry { `A provider with the name "${provider.name}" is already registered.` ) } + this.providers.set(provider.name, provider) + this.health.set(provider.name, { failures: 0 }) } get(name: string): AIProvider | undefined { return this.providers.get(name) } + list(): AIProvider[] { + return Array.from(this.providers.values()) + } + findByCapabilities(capabilities: AICapability[]): AIProvider[] { return Array.from(this.providers.values()).filter(provider => capabilities.every(cap => provider.supports(cap)) ) } - list(): AIProvider[] { - return Array.from(this.providers.values()) + // -------------------- + // Health tracking + // -------------------- + + private isHealthy(providerName: string): boolean { + const info = this.health.get(providerName) + if (!info) return true + return info.failures < MAX_FAILURES + } + + markFailure(providerName: string) { + const info = this.health.get(providerName) + if (!info) return + + info.failures += 1 + info.lastFailureAt = Date.now() + } + + markSuccess(providerName: string) { + const info = this.health.get(providerName) + if (!info) return + + info.failures = 0 + } + + // -------------------- + // Safe execution + // -------------------- + + async generateTextSafe( + providerName: string, + input: { prompt: string; context?: string } + ): Promise { + const provider = this.providers.get(providerName) + if (!provider) { + throw new Error(`Provider "${providerName}" not found`) + } + + try { + const result = await provider.generateText(input) + this.markSuccess(provider.name) + return result + } catch (err) { + this.markFailure(provider.name) + throw err + } + } + + // -------------------- + // 🆕 Fallback routing + // -------------------- + + async generateTextWithFallback( + capabilities: AICapability[], + input: { prompt: string; context?: string } + ): Promise { + const candidates = this.findByCapabilities(capabilities) + + for (const provider of candidates) { + if (!this.isHealthy(provider.name)) continue + + try { + const result = await provider.generateText(input) + this.markSuccess(provider.name) + return result + } catch { + this.markFailure(provider.name) + } + } + + throw new Error('No healthy AI provider available') } } export const aiProviderRegistry = new AIProviderRegistry() -// register providers (safe, non-breaking) +// register providers (safe) aiProviderRegistry.register(new GeminiProvider()) From c5a67096dd878e486dbf497e5e5ab56338bc2972 Mon Sep 17 00:00:00 2001 From: Shashank Saga Date: Tue, 6 Jan 2026 15:10:19 +0530 Subject: [PATCH 3/3] fix(ai-core): improve fallback routing and preserve provider errors --- .../src/api/ai/core/AIProviderRegistry.ts | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts index 7108dc9..bd0a92a 100644 --- a/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts +++ b/LocalMind-Backend/src/api/ai/core/AIProviderRegistry.ts @@ -66,30 +66,7 @@ class AIProviderRegistry { } // -------------------- - // Safe execution - // -------------------- - - async generateTextSafe( - providerName: string, - input: { prompt: string; context?: string } - ): Promise { - const provider = this.providers.get(providerName) - if (!provider) { - throw new Error(`Provider "${providerName}" not found`) - } - - try { - const result = await provider.generateText(input) - this.markSuccess(provider.name) - return result - } catch (err) { - this.markFailure(provider.name) - throw err - } - } - - // -------------------- - // 🆕 Fallback routing + // Safe execution with proper fallback // -------------------- async generateTextWithFallback( @@ -97,24 +74,45 @@ class AIProviderRegistry { input: { prompt: string; context?: string } ): Promise { const candidates = this.findByCapabilities(capabilities) + const healthyProviders = candidates.filter(p => this.isHealthy(p.name)) + const degradedProviders = candidates.filter(p => !this.isHealthy(p.name)) + let lastError: unknown - for (const provider of candidates) { - if (!this.isHealthy(provider.name)) continue + // 1️⃣ Try all healthy providers first + for (const provider of healthyProviders) { + try { + const result = await provider.generateText(input) + this.markSuccess(provider.name) + return result + } catch (err) { + lastError = err + this.markFailure(provider.name) + } + } + // 2️⃣ Fall back to degraded providers if all healthy ones failed + for (const provider of degradedProviders) { try { const result = await provider.generateText(input) this.markSuccess(provider.name) return result - } catch { + } catch (err) { + lastError = err this.markFailure(provider.name) } } - throw new Error('No healthy AI provider available') + // 3️⃣ Throw the last error encountered (preserves stack trace and error details) + if (lastError) { + throw lastError + } + + // 4️⃣ If no providers matched capabilities at all + throw new Error('No AI provider available for the specified capabilities') } } export const aiProviderRegistry = new AIProviderRegistry() -// register providers (safe) -aiProviderRegistry.register(new GeminiProvider()) +// Register providers +aiProviderRegistry.register(new GeminiProvider()) \ No newline at end of file