From fe691066ddd6aecf9cfbb02aadfd201e68485175 Mon Sep 17 00:00:00 2001 From: adryserage <17680194+adryserage@users.noreply.github.com> Date: Fri, 19 Dec 2025 07:38:04 -0500 Subject: [PATCH] feat(graphiti): add Google AI as LLM and embedding provider Add full Google AI (Gemini) support for Graphiti memory system: Backend: - Add google-generativeai dependency to requirements.txt - Create GoogleEmbedder class with text-embedding-004 default model - Create GoogleLLMClient class with gemini-2.0-flash default model - Add GOOGLE to LLMProvider and EmbedderProvider enums - Add google_api_key, google_llm_model, google_embedding_model config - Update factory to create Google LLM client and embedder - Add validation for Google provider configuration Frontend: - Add 'google' to GraphitiLLMProvider and GraphitiEmbeddingProvider types - Add Google AI option to LLM provider dropdown in Setup Wizard - Add Google AI option to embedding provider dropdown - Add Google API key input field with link to Google AI Studio - Update MemoryBackendSection and SecuritySettings components - Update env-handlers to save GOOGLE_API_KEY, GOOGLE_LLM_MODEL, and GOOGLE_EMBEDDING_MODEL to .env files This allows users to use Google's Gemini models for both LLM operations (graph extraction, search, reasoning) and embeddings in Graphiti memory. --- CLAUDE.md | 15 + auto-claude-ui/package.json | 2 +- .../src/main/ipc-handlers/env-handlers.ts | 71 ++ .../components/onboarding/GraphitiStep.tsx | 857 ++++++++++++------ .../project-settings/MemoryBackendSection.tsx | 16 +- .../project-settings/SecuritySettings.tsx | 16 +- auto-claude-ui/src/shared/types/project.ts | 48 +- auto-claude/integrations/graphiti/config.py | 25 + .../embedder_providers/__init__.py | 2 + .../embedder_providers/google_embedder.py | 152 ++++ .../graphiti/providers_pkg/factory.py | 6 + .../providers_pkg/llm_providers/__init__.py | 2 + .../providers_pkg/llm_providers/google_llm.py | 175 ++++ auto-claude/requirements.txt | 3 + 14 files changed, 1097 insertions(+), 293 deletions(-) create mode 100644 auto-claude/integrations/graphiti/providers_pkg/embedder_providers/google_embedder.py create mode 100644 auto-claude/integrations/graphiti/providers_pkg/llm_providers/google_llm.py diff --git a/CLAUDE.md b/CLAUDE.md index e16d6cb4..2cecb885 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,6 +81,21 @@ auto-claude/.venv/bin/pytest tests/ -m "not slow" python auto-claude/validate_spec.py --spec-dir auto-claude/specs/001-feature --checkpoint all ``` +### Releases +```bash +# Automated version bump and release (recommended) +node scripts/bump-version.js patch # 2.5.5 -> 2.5.6 +node scripts/bump-version.js minor # 2.5.5 -> 2.6.0 +node scripts/bump-version.js major # 2.5.5 -> 3.0.0 +node scripts/bump-version.js 2.6.0 # Set specific version + +# Then push to trigger GitHub release workflows +git push origin main +git push origin v2.6.0 +``` + +See [RELEASE.md](RELEASE.md) for detailed release process documentation. + ## Architecture ### Core Pipeline diff --git a/auto-claude-ui/package.json b/auto-claude-ui/package.json index c8b6dd2b..39e45aaf 100644 --- a/auto-claude-ui/package.json +++ b/auto-claude-ui/package.json @@ -1,6 +1,6 @@ { "name": "auto-claude-ui", - "version": "2.5.0", + "version": "2.5.5", "description": "Desktop UI for Auto Claude autonomous coding framework", "main": "./out/main/index.js", "author": "Auto Claude Team", diff --git a/auto-claude-ui/src/main/ipc-handlers/env-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/env-handlers.ts index 0093600a..b1e53617 100644 --- a/auto-claude-ui/src/main/ipc-handlers/env-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/env-handlers.ts @@ -65,6 +65,41 @@ export function registerEnvHandlers( if (config.graphitiEnabled !== undefined) { existingVars['GRAPHITI_ENABLED'] = config.graphitiEnabled ? 'true' : 'false'; } + // Graphiti Provider Configuration + if (config.graphitiProviderConfig) { + const pc = config.graphitiProviderConfig; + if (pc.llmProvider) existingVars['GRAPHITI_LLM_PROVIDER'] = pc.llmProvider; + if (pc.embeddingProvider) existingVars['GRAPHITI_EMBEDDER_PROVIDER'] = pc.embeddingProvider; + // OpenAI + if (pc.openaiApiKey) existingVars['OPENAI_API_KEY'] = pc.openaiApiKey; + if (pc.openaiModel) existingVars['OPENAI_MODEL'] = pc.openaiModel; + if (pc.openaiEmbeddingModel) existingVars['OPENAI_EMBEDDING_MODEL'] = pc.openaiEmbeddingModel; + // Anthropic + if (pc.anthropicApiKey) existingVars['ANTHROPIC_API_KEY'] = pc.anthropicApiKey; + if (pc.anthropicModel) existingVars['GRAPHITI_ANTHROPIC_MODEL'] = pc.anthropicModel; + // Azure OpenAI + if (pc.azureOpenaiApiKey) existingVars['AZURE_OPENAI_API_KEY'] = pc.azureOpenaiApiKey; + if (pc.azureOpenaiBaseUrl) existingVars['AZURE_OPENAI_BASE_URL'] = pc.azureOpenaiBaseUrl; + if (pc.azureOpenaiLlmDeployment) existingVars['AZURE_OPENAI_LLM_DEPLOYMENT'] = pc.azureOpenaiLlmDeployment; + if (pc.azureOpenaiEmbeddingDeployment) existingVars['AZURE_OPENAI_EMBEDDING_DEPLOYMENT'] = pc.azureOpenaiEmbeddingDeployment; + // Voyage + if (pc.voyageApiKey) existingVars['VOYAGE_API_KEY'] = pc.voyageApiKey; + if (pc.voyageEmbeddingModel) existingVars['VOYAGE_EMBEDDING_MODEL'] = pc.voyageEmbeddingModel; + // Google + if (pc.googleApiKey) existingVars['GOOGLE_API_KEY'] = pc.googleApiKey; + if (pc.googleLlmModel) existingVars['GOOGLE_LLM_MODEL'] = pc.googleLlmModel; + if (pc.googleEmbeddingModel) existingVars['GOOGLE_EMBEDDING_MODEL'] = pc.googleEmbeddingModel; + // Ollama + if (pc.ollamaBaseUrl) existingVars['OLLAMA_BASE_URL'] = pc.ollamaBaseUrl; + if (pc.ollamaLlmModel) existingVars['OLLAMA_LLM_MODEL'] = pc.ollamaLlmModel; + if (pc.ollamaEmbeddingModel) existingVars['OLLAMA_EMBEDDING_MODEL'] = pc.ollamaEmbeddingModel; + if (pc.ollamaEmbeddingDim) existingVars['OLLAMA_EMBEDDING_DIM'] = String(pc.ollamaEmbeddingDim); + // FalkorDB + if (pc.falkorDbHost) existingVars['GRAPHITI_FALKORDB_HOST'] = pc.falkorDbHost; + if (pc.falkorDbPort) existingVars['GRAPHITI_FALKORDB_PORT'] = String(pc.falkorDbPort); + if (pc.falkorDbPassword) existingVars['GRAPHITI_FALKORDB_PASSWORD'] = pc.falkorDbPassword; + } + // Legacy fields (still supported) if (config.openaiApiKey !== undefined) { existingVars['OPENAI_API_KEY'] = config.openaiApiKey; } @@ -116,9 +151,45 @@ ${existingVars['ENABLE_FANCY_UI'] !== undefined ? `ENABLE_FANCY_UI=${existingVar # ============================================================================= # GRAPHITI MEMORY INTEGRATION (OPTIONAL) +# Multi-provider support: OpenAI, Anthropic, Azure OpenAI, Ollama, Voyage # ============================================================================= ${existingVars['GRAPHITI_ENABLED'] ? `GRAPHITI_ENABLED=${existingVars['GRAPHITI_ENABLED']}` : '# GRAPHITI_ENABLED=false'} + +# Provider Selection +${existingVars['GRAPHITI_LLM_PROVIDER'] ? `GRAPHITI_LLM_PROVIDER=${existingVars['GRAPHITI_LLM_PROVIDER']}` : '# GRAPHITI_LLM_PROVIDER=openai'} +${existingVars['GRAPHITI_EMBEDDER_PROVIDER'] ? `GRAPHITI_EMBEDDER_PROVIDER=${existingVars['GRAPHITI_EMBEDDER_PROVIDER']}` : '# GRAPHITI_EMBEDDER_PROVIDER=openai'} + +# OpenAI Settings ${existingVars['OPENAI_API_KEY'] ? `OPENAI_API_KEY=${existingVars['OPENAI_API_KEY']}` : '# OPENAI_API_KEY='} +${existingVars['OPENAI_MODEL'] ? `OPENAI_MODEL=${existingVars['OPENAI_MODEL']}` : '# OPENAI_MODEL=gpt-4o-mini'} +${existingVars['OPENAI_EMBEDDING_MODEL'] ? `OPENAI_EMBEDDING_MODEL=${existingVars['OPENAI_EMBEDDING_MODEL']}` : '# OPENAI_EMBEDDING_MODEL=text-embedding-3-small'} + +# Anthropic Settings (LLM only - use with Voyage or OpenAI for embeddings) +${existingVars['ANTHROPIC_API_KEY'] ? `ANTHROPIC_API_KEY=${existingVars['ANTHROPIC_API_KEY']}` : '# ANTHROPIC_API_KEY='} +${existingVars['GRAPHITI_ANTHROPIC_MODEL'] ? `GRAPHITI_ANTHROPIC_MODEL=${existingVars['GRAPHITI_ANTHROPIC_MODEL']}` : '# GRAPHITI_ANTHROPIC_MODEL=claude-sonnet-4-5-latest'} + +# Azure OpenAI Settings +${existingVars['AZURE_OPENAI_API_KEY'] ? `AZURE_OPENAI_API_KEY=${existingVars['AZURE_OPENAI_API_KEY']}` : '# AZURE_OPENAI_API_KEY='} +${existingVars['AZURE_OPENAI_BASE_URL'] ? `AZURE_OPENAI_BASE_URL=${existingVars['AZURE_OPENAI_BASE_URL']}` : '# AZURE_OPENAI_BASE_URL='} +${existingVars['AZURE_OPENAI_LLM_DEPLOYMENT'] ? `AZURE_OPENAI_LLM_DEPLOYMENT=${existingVars['AZURE_OPENAI_LLM_DEPLOYMENT']}` : '# AZURE_OPENAI_LLM_DEPLOYMENT='} +${existingVars['AZURE_OPENAI_EMBEDDING_DEPLOYMENT'] ? `AZURE_OPENAI_EMBEDDING_DEPLOYMENT=${existingVars['AZURE_OPENAI_EMBEDDING_DEPLOYMENT']}` : '# AZURE_OPENAI_EMBEDDING_DEPLOYMENT='} + +# Voyage AI Settings (Embeddings only - great with Anthropic) +${existingVars['VOYAGE_API_KEY'] ? `VOYAGE_API_KEY=${existingVars['VOYAGE_API_KEY']}` : '# VOYAGE_API_KEY='} +${existingVars['VOYAGE_EMBEDDING_MODEL'] ? `VOYAGE_EMBEDDING_MODEL=${existingVars['VOYAGE_EMBEDDING_MODEL']}` : '# VOYAGE_EMBEDDING_MODEL=voyage-3'} + +# Google AI Settings (LLM and Embeddings - Gemini) +${existingVars['GOOGLE_API_KEY'] ? `GOOGLE_API_KEY=${existingVars['GOOGLE_API_KEY']}` : '# GOOGLE_API_KEY='} +${existingVars['GOOGLE_LLM_MODEL'] ? `GOOGLE_LLM_MODEL=${existingVars['GOOGLE_LLM_MODEL']}` : '# GOOGLE_LLM_MODEL=gemini-2.0-flash'} +${existingVars['GOOGLE_EMBEDDING_MODEL'] ? `GOOGLE_EMBEDDING_MODEL=${existingVars['GOOGLE_EMBEDDING_MODEL']}` : '# GOOGLE_EMBEDDING_MODEL=text-embedding-004'} + +# Ollama Settings (Local - free) +${existingVars['OLLAMA_BASE_URL'] ? `OLLAMA_BASE_URL=${existingVars['OLLAMA_BASE_URL']}` : '# OLLAMA_BASE_URL=http://localhost:11434'} +${existingVars['OLLAMA_LLM_MODEL'] ? `OLLAMA_LLM_MODEL=${existingVars['OLLAMA_LLM_MODEL']}` : '# OLLAMA_LLM_MODEL='} +${existingVars['OLLAMA_EMBEDDING_MODEL'] ? `OLLAMA_EMBEDDING_MODEL=${existingVars['OLLAMA_EMBEDDING_MODEL']}` : '# OLLAMA_EMBEDDING_MODEL='} +${existingVars['OLLAMA_EMBEDDING_DIM'] ? `OLLAMA_EMBEDDING_DIM=${existingVars['OLLAMA_EMBEDDING_DIM']}` : '# OLLAMA_EMBEDDING_DIM=768'} + +# FalkorDB Connection ${existingVars['GRAPHITI_FALKORDB_HOST'] ? `GRAPHITI_FALKORDB_HOST=${existingVars['GRAPHITI_FALKORDB_HOST']}` : '# GRAPHITI_FALKORDB_HOST=localhost'} ${existingVars['GRAPHITI_FALKORDB_PORT'] ? `GRAPHITI_FALKORDB_PORT=${existingVars['GRAPHITI_FALKORDB_PORT']}` : '# GRAPHITI_FALKORDB_PORT=6380'} ${existingVars['GRAPHITI_FALKORDB_PASSWORD'] ? `GRAPHITI_FALKORDB_PASSWORD=${existingVars['GRAPHITI_FALKORDB_PASSWORD']}` : '# GRAPHITI_FALKORDB_PASSWORD='} diff --git a/auto-claude-ui/src/renderer/components/onboarding/GraphitiStep.tsx b/auto-claude-ui/src/renderer/components/onboarding/GraphitiStep.tsx index dc6a6f45..b60631cb 100644 --- a/auto-claude-ui/src/renderer/components/onboarding/GraphitiStep.tsx +++ b/auto-claude-ui/src/renderer/components/onboarding/GraphitiStep.tsx @@ -18,15 +18,15 @@ import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Card, CardContent } from '../ui/card'; import { Switch } from '../ui/switch'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { - Tooltip, - TooltipContent, - TooltipTrigger -} from '../ui/tooltip'; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '../ui/select'; import { useSettingsStore } from '../../stores/settings-store'; -import type { GraphitiProviderType } from '../../../shared/types'; -import type { AppSettings } from '../../../shared/types/settings'; +import type { GraphitiLLMProvider, GraphitiEmbeddingProvider } from '../../../shared/types'; interface GraphitiStepProps { onNext: () => void; @@ -34,74 +34,97 @@ interface GraphitiStepProps { onSkip: () => void; } +// Provider configurations with descriptions +const LLM_PROVIDERS: Array<{ + id: GraphitiLLMProvider; + name: string; + description: string; + requiresApiKey: boolean; +}> = [ + { id: 'openai', name: 'OpenAI', description: 'GPT models (recommended)', requiresApiKey: true }, + { id: 'anthropic', name: 'Anthropic', description: 'Claude models', requiresApiKey: true }, + { id: 'google', name: 'Google AI', description: 'Gemini models', requiresApiKey: true }, + { id: 'groq', name: 'Groq', description: 'Llama models (fast inference)', requiresApiKey: true }, + { id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure deployment', requiresApiKey: true }, + { id: 'ollama', name: 'Ollama', description: 'Local models (free)', requiresApiKey: false } +]; + +const EMBEDDING_PROVIDERS: Array<{ + id: GraphitiEmbeddingProvider; + name: string; + description: string; + requiresApiKey: boolean; +}> = [ + { id: 'openai', name: 'OpenAI', description: 'text-embedding-3-small (recommended)', requiresApiKey: true }, + { id: 'voyage', name: 'Voyage AI', description: 'voyage-3 (great with Anthropic)', requiresApiKey: true }, + { id: 'google', name: 'Google AI', description: 'Gemini text-embedding-004', requiresApiKey: true }, + { id: 'huggingface', name: 'HuggingFace', description: 'Open source models', requiresApiKey: true }, + { id: 'azure_openai', name: 'Azure OpenAI', description: 'Enterprise Azure embeddings', requiresApiKey: true }, + { id: 'ollama', name: 'Ollama', description: 'Local embeddings (free)', requiresApiKey: false } +]; + interface GraphitiConfig { enabled: boolean; falkorDbUri: string; - llmProvider: GraphitiProviderType; - apiKey: string; - ollamaBaseUrl: string; // For Ollama provider (no API key needed) -} - -// Provider display info -const PROVIDER_INFO: Record = { - openai: { name: 'OpenAI', placeholder: 'sk-...', link: 'https://platform.openai.com/api-keys', requiresApiKey: true }, - anthropic: { name: 'Anthropic', placeholder: 'sk-ant-...', link: 'https://console.anthropic.com/settings/keys', requiresApiKey: true }, - google: { name: 'Google (Gemini)', placeholder: 'AIza...', link: 'https://aistudio.google.com/apikey', requiresApiKey: true }, - groq: { name: 'Groq', placeholder: 'gsk_...', link: 'https://console.groq.com/keys', requiresApiKey: true }, - ollama: { - name: 'Ollama', - placeholder: 'http://localhost:11434', - link: 'https://ollama.ai', - requiresApiKey: false, - description: 'Local LLM - no API key required' - }, -}; - -// Helper to get the saved API key for a provider from settings -function getApiKeyForProvider(provider: GraphitiProviderType, settings: AppSettings): string { - switch (provider) { - case 'openai': return settings.globalOpenAIApiKey || ''; - case 'anthropic': return settings.globalAnthropicApiKey || ''; - case 'google': return settings.globalGoogleApiKey || ''; - case 'groq': return settings.globalGroqApiKey || ''; - case 'ollama': return ''; // Ollama doesn't need an API key - default: return ''; - } -} - -// Helper to get the saved Ollama base URL from settings -function getOllamaBaseUrl(settings: AppSettings): string { - return settings.ollamaBaseUrl || 'http://localhost:11434'; + llmProvider: GraphitiLLMProvider; + embeddingProvider: GraphitiEmbeddingProvider; + // OpenAI + openaiApiKey: string; + // Anthropic + anthropicApiKey: string; + // Azure OpenAI + azureOpenaiApiKey: string; + azureOpenaiBaseUrl: string; + azureOpenaiLlmDeployment: string; + azureOpenaiEmbeddingDeployment: string; + // Voyage + voyageApiKey: string; + // Google + googleApiKey: string; + // Groq + groqApiKey: string; + // HuggingFace + huggingfaceApiKey: string; + // Ollama + ollamaBaseUrl: string; + ollamaLlmModel: string; + ollamaEmbeddingModel: string; + ollamaEmbeddingDim: string; } interface ValidationStatus { falkordb: { tested: boolean; success: boolean; message: string } | null; - llm: { tested: boolean; success: boolean; message: string } | null; + provider: { tested: boolean; success: boolean; message: string } | null; } /** * Graphiti/FalkorDB configuration step for the onboarding wizard. - * Allows users to optionally configure Graphiti memory backend. + * Allows users to optionally configure Graphiti memory backend with multiple provider options. * This step is entirely optional and can be skipped. */ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { const { settings, updateSettings } = useSettingsStore(); - // Load saved provider preference, defaulting to 'openai' - const savedProvider = settings.graphitiLlmProvider || 'openai'; const [config, setConfig] = useState({ enabled: false, - falkorDbUri: 'bolt://localhost:6379', // Standard FalkorDB port, will be auto-detected from Docker - llmProvider: savedProvider, - apiKey: getApiKeyForProvider(savedProvider, settings), - ollamaBaseUrl: getOllamaBaseUrl(settings) + falkorDbUri: 'bolt://localhost:6379', + llmProvider: 'openai', + embeddingProvider: 'openai', + openaiApiKey: settings.globalOpenAIApiKey || '', + anthropicApiKey: settings.globalAnthropicApiKey || '', + azureOpenaiApiKey: '', + azureOpenaiBaseUrl: '', + azureOpenaiLlmDeployment: '', + azureOpenaiEmbeddingDeployment: '', + voyageApiKey: '', + googleApiKey: settings.globalGoogleApiKey || '', + groqApiKey: settings.globalGroqApiKey || '', + huggingfaceApiKey: '', + ollamaBaseUrl: settings.ollamaBaseUrl || 'http://localhost:11434', + ollamaLlmModel: '', + ollamaEmbeddingModel: '', + ollamaEmbeddingDim: '768' }); - const [showApiKey, setShowApiKey] = useState(false); + const [showApiKey, setShowApiKey] = useState>({}); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); @@ -110,7 +133,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { const [isValidating, setIsValidating] = useState(false); const [validationStatus, setValidationStatus] = useState({ falkordb: null, - llm: null + provider: null }); // Check Docker/Infrastructure availability on mount @@ -118,11 +141,9 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { const checkInfrastructure = async () => { setIsCheckingDocker(true); try { - // Check infrastructure status via the electronAPI const result = await window.electronAPI.getInfrastructureStatus(); setDockerAvailable(result?.success && result?.data?.docker?.running ? true : false); - // If FalkorDB is running, auto-detect and set the correct port if (result?.success && result?.data?.falkordb?.containerRunning) { const detectedPort = result.data.falkordb.port; setConfig(prev => ({ @@ -131,7 +152,6 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { })); } } catch { - // Infrastructure check may fail, assume unavailable setDockerAvailable(false); } finally { setIsCheckingDocker(false); @@ -145,52 +165,69 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { setConfig(prev => ({ ...prev, enabled: checked })); setError(null); setSuccess(false); - // Reset validation status when toggling - setValidationStatus({ falkordb: null, llm: null }); + setValidationStatus({ falkordb: null, provider: null }); }; - const handleProviderChange = (provider: GraphitiProviderType) => { - // Load saved API key or base URL for the selected provider - const savedKey = getApiKeyForProvider(provider, settings); - const savedOllamaUrl = getOllamaBaseUrl(settings); - setConfig(prev => ({ - ...prev, - llmProvider: provider, - apiKey: savedKey, - ollamaBaseUrl: savedOllamaUrl - })); - setValidationStatus(prev => ({ ...prev, llm: null })); - setError(null); + const toggleShowApiKey = (key: string) => { + setShowApiKey(prev => ({ ...prev, [key]: !prev[key] })); + }; + + // Get the required API key for the current provider configuration + const getRequiredApiKey = (): string | null => { + const { llmProvider, embeddingProvider } = config; + + // Check LLM provider + if (llmProvider === 'openai' || embeddingProvider === 'openai') { + if (!config.openaiApiKey.trim()) return 'OpenAI API key'; + } + if (llmProvider === 'anthropic') { + if (!config.anthropicApiKey.trim()) return 'Anthropic API key'; + } + if (llmProvider === 'azure_openai' || embeddingProvider === 'azure_openai') { + if (!config.azureOpenaiApiKey.trim()) return 'Azure OpenAI API key'; + if (!config.azureOpenaiBaseUrl.trim()) return 'Azure OpenAI Base URL'; + } + if (embeddingProvider === 'voyage') { + if (!config.voyageApiKey.trim()) return 'Voyage API key'; + } + if (llmProvider === 'google' || embeddingProvider === 'google') { + if (!config.googleApiKey.trim()) return 'Google API key'; + } + if (llmProvider === 'groq') { + if (!config.groqApiKey.trim()) return 'Groq API key'; + } + if (embeddingProvider === 'huggingface') { + if (!config.huggingfaceApiKey.trim()) return 'HuggingFace API key'; + } + if (llmProvider === 'ollama') { + if (!config.ollamaLlmModel.trim()) return 'Ollama LLM model name'; + } + if (embeddingProvider === 'ollama') { + if (!config.ollamaEmbeddingModel.trim()) return 'Ollama embedding model name'; + } + + return null; }; const handleTestConnection = async () => { - const providerName = PROVIDER_INFO[config.llmProvider].name; - const providerInfo = PROVIDER_INFO[config.llmProvider]; - - // Validate input based on provider type - if (providerInfo.requiresApiKey && !config.apiKey.trim()) { - setError(`Please enter a ${providerName} API key to test the connection`); - return; - } - if (config.llmProvider === 'ollama' && !config.ollamaBaseUrl.trim()) { - setError('Please enter the Ollama server URL to test the connection'); + const missingKey = getRequiredApiKey(); + if (missingKey) { + setError(`Please enter ${missingKey} to test the connection`); return; } setIsValidating(true); setError(null); - setValidationStatus({ falkordb: null, llm: null }); + setValidationStatus({ falkordb: null, provider: null }); try { - // For now, we still use the OpenAI test endpoint, but pass the provider info - // TODO: Add provider-specific validation endpoints - // For Ollama, pass the base URL instead of API key - const testCredential = config.llmProvider === 'ollama' - ? config.ollamaBaseUrl.trim() - : config.apiKey.trim(); + // For now, use the existing OpenAI validation - this will be expanded + const apiKey = config.llmProvider === 'openai' ? config.openaiApiKey : + config.embeddingProvider === 'openai' ? config.openaiApiKey : ''; + const result = await window.electronAPI.testGraphitiConnection( config.falkorDbUri, - testCredential + apiKey.trim() ); if (result?.success && result?.data) { @@ -200,10 +237,12 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { success: result.data.falkordb.success, message: result.data.falkordb.message }, - llm: { + provider: { tested: true, success: result.data.openai.success, - message: result.data.openai.message + message: result.data.openai.success + ? `${config.llmProvider} / ${config.embeddingProvider} providers configured` + : result.data.openai.message } }); @@ -213,7 +252,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { errors.push(`FalkorDB: ${result.data.falkordb.message}`); } if (!result.data.openai.success) { - errors.push(`${providerName}: ${result.data.openai.message}`); + errors.push(`Provider: ${result.data.openai.message}`); } if (errors.length > 0) { setError(errors.join('\n')); @@ -231,21 +270,13 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { const handleSave = async () => { if (!config.enabled) { - // If not enabled, just continue onNext(); return; } - const providerName = PROVIDER_INFO[config.llmProvider].name; - const providerInfo = PROVIDER_INFO[config.llmProvider]; - - // Validate input based on provider type - if (providerInfo.requiresApiKey && !config.apiKey.trim()) { - setError(`${providerName} API key is required for Graphiti`); - return; - } - if (config.llmProvider === 'ollama' && !config.ollamaBaseUrl.trim()) { - setError('Ollama server URL is required for Graphiti'); + const missingKey = getRequiredApiKey(); + if (missingKey) { + setError(`${missingKey} is required`); return; } @@ -253,42 +284,38 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { setError(null); try { - // Build settings update based on selected provider - const settingsUpdate: Record = { + // Save the primary API keys to global settings based on providers + const settingsToSave: Record = { graphitiLlmProvider: config.llmProvider, }; - // Save the API key or base URL for the selected provider - if (config.llmProvider === 'openai') { - settingsUpdate.globalOpenAIApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'anthropic') { - settingsUpdate.globalAnthropicApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'google') { - settingsUpdate.globalGoogleApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'groq') { - settingsUpdate.globalGroqApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'ollama') { - settingsUpdate.ollamaBaseUrl = config.ollamaBaseUrl.trim(); + if (config.openaiApiKey.trim()) { + settingsToSave.globalOpenAIApiKey = config.openaiApiKey.trim(); + } + if (config.anthropicApiKey.trim()) { + settingsToSave.globalAnthropicApiKey = config.anthropicApiKey.trim(); + } + if (config.googleApiKey.trim()) { + settingsToSave.globalGoogleApiKey = config.googleApiKey.trim(); + } + if (config.groqApiKey.trim()) { + settingsToSave.globalGroqApiKey = config.groqApiKey.trim(); + } + if (config.ollamaBaseUrl.trim()) { + settingsToSave.ollamaBaseUrl = config.ollamaBaseUrl.trim(); } - const result = await window.electronAPI.saveSettings(settingsUpdate); + const result = await window.electronAPI.saveSettings(settingsToSave); if (result?.success) { - // Update local settings store for all providers + // Update local settings store const storeUpdate: Record = {}; - if (config.llmProvider === 'openai') { - storeUpdate.globalOpenAIApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'anthropic') { - storeUpdate.globalAnthropicApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'google') { - storeUpdate.globalGoogleApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'groq') { - storeUpdate.globalGroqApiKey = config.apiKey.trim(); - } else if (config.llmProvider === 'ollama') { - storeUpdate.ollamaBaseUrl = config.ollamaBaseUrl.trim(); - } + if (config.openaiApiKey.trim()) storeUpdate.globalOpenAIApiKey = config.openaiApiKey.trim(); + if (config.anthropicApiKey.trim()) storeUpdate.globalAnthropicApiKey = config.anthropicApiKey.trim(); + if (config.googleApiKey.trim()) storeUpdate.globalGoogleApiKey = config.googleApiKey.trim(); + if (config.groqApiKey.trim()) storeUpdate.globalGroqApiKey = config.groqApiKey.trim(); + if (config.ollamaBaseUrl.trim()) storeUpdate.ollamaBaseUrl = config.ollamaBaseUrl.trim(); updateSettings(storeUpdate); - // Proceed to next step immediately after successful save onNext(); } else { setError(result?.error || 'Failed to save Graphiti configuration'); @@ -317,6 +344,370 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { setError(null); }; + // Render provider-specific configuration fields + const renderProviderFields = () => { + const { llmProvider, embeddingProvider } = config; + const needsOpenAI = llmProvider === 'openai' || embeddingProvider === 'openai'; + const needsAnthropic = llmProvider === 'anthropic'; + const needsAzure = llmProvider === 'azure_openai' || embeddingProvider === 'azure_openai'; + const needsVoyage = embeddingProvider === 'voyage'; + const needsGoogle = llmProvider === 'google' || embeddingProvider === 'google'; + const needsGroq = llmProvider === 'groq'; + const needsHuggingFace = embeddingProvider === 'huggingface'; + const needsOllama = llmProvider === 'ollama' || embeddingProvider === 'ollama'; + + return ( +
+ {/* OpenAI API Key */} + {needsOpenAI && ( +
+
+ + {validationStatus.provider?.tested && needsOpenAI && ( +
+ {validationStatus.provider.success ? ( + + ) : ( + + )} +
+ )} +
+
+ { + setConfig(prev => ({ ...prev, openaiApiKey: e.target.value })); + setValidationStatus(prev => ({ ...prev, provider: null })); + }} + placeholder="sk-..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+

+ Get your key from{' '} + + OpenAI + +

+
+ )} + + {/* Anthropic API Key */} + {needsAnthropic && ( +
+ +
+ setConfig(prev => ({ ...prev, anthropicApiKey: e.target.value }))} + placeholder="sk-ant-..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+

+ Get your key from{' '} + + Anthropic Console + +

+
+ )} + + {/* Azure OpenAI Settings */} + {needsAzure && ( +
+

Azure OpenAI Settings

+
+ +
+ setConfig(prev => ({ ...prev, azureOpenaiApiKey: e.target.value }))} + placeholder="Azure API key" + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+
+
+ + setConfig(prev => ({ ...prev, azureOpenaiBaseUrl: e.target.value }))} + placeholder="https://your-resource.openai.azure.com" + className="font-mono text-sm" + disabled={isSaving || isValidating} + /> +
+ {llmProvider === 'azure_openai' && ( +
+ + setConfig(prev => ({ ...prev, azureOpenaiLlmDeployment: e.target.value }))} + placeholder="gpt-4" + className="font-mono text-sm" + disabled={isSaving || isValidating} + /> +
+ )} + {embeddingProvider === 'azure_openai' && ( +
+ + setConfig(prev => ({ ...prev, azureOpenaiEmbeddingDeployment: e.target.value }))} + placeholder="text-embedding-ada-002" + className="font-mono text-sm" + disabled={isSaving || isValidating} + /> +
+ )} +
+ )} + + {/* Voyage API Key */} + {needsVoyage && ( +
+ +
+ setConfig(prev => ({ ...prev, voyageApiKey: e.target.value }))} + placeholder="pa-..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+

+ Get your key from{' '} + + Voyage AI + +

+
+ )} + + {/* Google API Key */} + {needsGoogle && ( +
+ +
+ setConfig(prev => ({ ...prev, googleApiKey: e.target.value }))} + placeholder="AIza..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+

+ Get your key from{' '} + + Google AI Studio + +

+
+ )} + + {/* Groq API Key */} + {needsGroq && ( +
+ +
+ setConfig(prev => ({ ...prev, groqApiKey: e.target.value }))} + placeholder="gsk_..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+

+ Get your key from{' '} + + Groq Console + +

+
+ )} + + {/* HuggingFace API Key */} + {needsHuggingFace && ( +
+ +
+ setConfig(prev => ({ ...prev, huggingfaceApiKey: e.target.value }))} + placeholder="hf_..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isValidating} + /> + +
+

+ Get your key from{' '} + + HuggingFace + +

+
+ )} + + {/* Ollama Settings */} + {needsOllama && ( +
+

Ollama Settings (Local)

+
+ + setConfig(prev => ({ ...prev, ollamaBaseUrl: e.target.value }))} + placeholder="http://localhost:11434" + className="font-mono text-sm" + disabled={isSaving || isValidating} + /> +
+ {llmProvider === 'ollama' && ( +
+ + setConfig(prev => ({ ...prev, ollamaLlmModel: e.target.value }))} + placeholder="llama3.2, deepseek-r1:7b, etc." + className="font-mono text-sm" + disabled={isSaving || isValidating} + /> +
+ )} + {embeddingProvider === 'ollama' && ( + <> +
+ + setConfig(prev => ({ ...prev, ollamaEmbeddingModel: e.target.value }))} + placeholder="nomic-embed-text" + className="font-mono text-sm" + disabled={isSaving || isValidating} + /> +
+
+ + setConfig(prev => ({ ...prev, ollamaEmbeddingDim: e.target.value }))} + placeholder="768" + className="font-mono text-sm" + disabled={isSaving || isValidating} + /> +
+ + )} +

+ Ensure Ollama is running locally. See{' '} + + ollama.ai + +

+
+ )} +
+ ); + }; + return (
@@ -386,7 +777,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
-

{error}

+

{error}

@@ -454,7 +845,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { Enable Graphiti Memory

- Requires FalkorDB (Docker) and an LLM provider (API key or local Ollama) + Requires FalkorDB (Docker) and an LLM/embedding provider

@@ -470,30 +861,6 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { {/* Configuration fields (shown when enabled) */} {config.enabled && (
- {/* LLM Provider Selection */} -
- -

- Select the AI provider for graph operations -

- -
- {/* FalkorDB URI */}
@@ -533,124 +900,76 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {

- {/* Dynamic credential field based on provider */} - {config.llmProvider === 'ollama' ? ( - /* Ollama Base URL field */ + {/* Provider Selection */} +
+ {/* LLM Provider */}
-
- - {validationStatus.llm && ( -
- {validationStatus.llm.success ? ( - - ) : ( - - )} - - {validationStatus.llm.success ? 'Connected' : 'Failed'} - -
- )} -
- { - setConfig(prev => ({ ...prev, ollamaBaseUrl: e.target.value })); - setValidationStatus(prev => ({ ...prev, llm: null })); + +
- ) : ( - /* API Key field for other providers */ + + {/* Embedding Provider */}
-
- - {validationStatus.llm && ( -
- {validationStatus.llm.success ? ( - - ) : ( - - )} - - {validationStatus.llm.success ? 'Valid' : 'Invalid'} - -
- )} -
-
- { - setConfig(prev => ({ ...prev, apiKey: e.target.value })); - setValidationStatus(prev => ({ ...prev, llm: null })); - }} - placeholder={PROVIDER_INFO[config.llmProvider].placeholder} - className="pr-10 font-mono text-sm" - disabled={isSaving || isValidating} - /> - - - - - - {showApiKey ? 'Hide API key' : 'Show API key'} - - -
-

- Required for graph operations. Get your key from{' '} - - {PROVIDER_INFO[config.llmProvider].name} - -

+ +
- )} +
+ + {/* Provider-specific fields */} + {renderProviderFields()} {/* Test Connection Button */}
- {validationStatus.falkordb?.success && validationStatus.llm?.success && ( + {validationStatus.falkordb?.success && validationStatus.provider?.success && (

All connections validated successfully!

)} {config.llmProvider !== 'openai' && config.llmProvider !== 'ollama' && (

- Note: API key validation currently only fully supports OpenAI. Your {PROVIDER_INFO[config.llmProvider].name} key will be saved and used at runtime. + Note: API key validation currently only fully supports OpenAI. Your key will be saved and used at runtime.

)} {config.llmProvider === 'ollama' && ( @@ -707,7 +1026,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
@@ -175,7 +176,7 @@ export function MemoryBackendSection({ graphitiProviderConfig: { ...envConfig.graphitiProviderConfig, llmProvider: envConfig.graphitiProviderConfig?.llmProvider || 'openai', - embeddingProvider: value as 'openai' | 'voyage' | 'google' | 'huggingface', + embeddingProvider: value as 'openai' | 'voyage' | 'azure_openai' | 'ollama', } })} > @@ -185,8 +186,9 @@ export function MemoryBackendSection({ OpenAI Voyage AI - Google - HuggingFace (Local) + Google AI + Azure OpenAI + Ollama (Local)
diff --git a/auto-claude-ui/src/renderer/components/project-settings/SecuritySettings.tsx b/auto-claude-ui/src/renderer/components/project-settings/SecuritySettings.tsx index d583e7e2..a5de07d3 100644 --- a/auto-claude-ui/src/renderer/components/project-settings/SecuritySettings.tsx +++ b/auto-claude-ui/src/renderer/components/project-settings/SecuritySettings.tsx @@ -164,7 +164,7 @@ export function SecuritySettings({ updateEnvConfig({ graphitiProviderConfig: { ...currentConfig, - llmProvider: value as 'openai' | 'anthropic' | 'google' | 'groq', + llmProvider: value as 'openai' | 'anthropic' | 'azure_openai' | 'ollama', } }); }} @@ -173,10 +173,11 @@ export function SecuritySettings({ - OpenAI (GPT-5-mini) + OpenAI (GPT-4o-mini) Anthropic (Claude) - Google (Gemini) - Groq (Llama) + Google AI (Gemini) + Azure OpenAI + Ollama (Local)
@@ -197,7 +198,7 @@ export function SecuritySettings({ updateEnvConfig({ graphitiProviderConfig: { ...currentConfig, - embeddingProvider: value as 'openai' | 'voyage' | 'google' | 'huggingface', + embeddingProvider: value as 'openai' | 'voyage' | 'azure_openai' | 'ollama', } }); }} @@ -208,8 +209,9 @@ export function SecuritySettings({ OpenAI Voyage AI - Google - HuggingFace (Local) + Google AI + Azure OpenAI + Ollama (Local) diff --git a/auto-claude-ui/src/shared/types/project.ts b/auto-claude-ui/src/shared/types/project.ts index 578d798f..53f138aa 100644 --- a/auto-claude-ui/src/shared/types/project.ts +++ b/auto-claude-ui/src/shared/types/project.ts @@ -186,31 +186,61 @@ export interface GraphitiConnectionTestResult { } // Graphiti Provider Types (Memory System V2) -export type GraphitiProviderType = 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama'; -export type GraphitiEmbeddingProvider = 'openai' | 'voyage' | 'google' | 'huggingface' | 'ollama'; +// LLM Providers: OpenAI, Anthropic, Azure OpenAI, Ollama (local), Google, Groq +export type GraphitiLLMProvider = 'openai' | 'anthropic' | 'azure_openai' | 'ollama' | 'google' | 'groq'; +// Embedding Providers: OpenAI, Voyage AI, Azure OpenAI, Ollama (local), Google, HuggingFace +export type GraphitiEmbeddingProvider = 'openai' | 'voyage' | 'azure_openai' | 'ollama' | 'google' | 'huggingface'; + +// Legacy type alias for backward compatibility +export type GraphitiProviderType = GraphitiLLMProvider; export interface GraphitiProviderConfig { // LLM Provider - llmProvider: GraphitiProviderType; + llmProvider: GraphitiLLMProvider; llmModel?: string; // Model name, uses provider default if not specified // Embedding Provider embeddingProvider: GraphitiEmbeddingProvider; embeddingModel?: string; // Embedding model, uses provider default if not specified - // Provider-specific API keys (stored securely) + // OpenAI settings openaiApiKey?: string; - anthropicApiKey?: string; - googleApiKey?: string; - groqApiKey?: string; - voyageApiKey?: string; + openaiModel?: string; + openaiEmbeddingModel?: string; - // Ollama-specific config (local LLM, no API key required) + // Anthropic settings (LLM only - needs separate embedder) + anthropicApiKey?: string; + anthropicModel?: string; + + // Azure OpenAI settings + azureOpenaiApiKey?: string; + azureOpenaiBaseUrl?: string; + azureOpenaiLlmDeployment?: string; + azureOpenaiEmbeddingDeployment?: string; + + // Voyage AI settings (embeddings only - commonly used with Anthropic) + voyageApiKey?: string; + voyageEmbeddingModel?: string; + + // Google AI settings (LLM and embeddings) + googleApiKey?: string; + googleLlmModel?: string; + googleEmbeddingModel?: string; + + // Ollama settings (local LLM, no API key required) ollamaBaseUrl?: string; // Default: http://localhost:11434 ollamaLlmModel?: string; ollamaEmbeddingModel?: string; ollamaEmbeddingDim?: number; + // Groq settings + groqApiKey?: string; + groqModel?: string; + + // HuggingFace settings (embeddings only) + huggingfaceApiKey?: string; + huggingfaceEmbeddingModel?: string; + // FalkorDB connection (required for all providers) falkorDbHost?: string; falkorDbPort?: number; diff --git a/auto-claude/integrations/graphiti/config.py b/auto-claude/integrations/graphiti/config.py index 54b435d0..50887949 100644 --- a/auto-claude/integrations/graphiti/config.py +++ b/auto-claude/integrations/graphiti/config.py @@ -82,6 +82,7 @@ class LLMProvider(str, Enum): ANTHROPIC = "anthropic" AZURE_OPENAI = "azure_openai" OLLAMA = "ollama" + GOOGLE = "google" class EmbedderProvider(str, Enum): @@ -91,6 +92,7 @@ class EmbedderProvider(str, Enum): VOYAGE = "voyage" AZURE_OPENAI = "azure_openai" OLLAMA = "ollama" + GOOGLE = "google" @dataclass @@ -128,6 +130,11 @@ class GraphitiConfig: voyage_api_key: str = "" voyage_embedding_model: str = "voyage-3" + # Google AI settings (LLM and embeddings) + google_api_key: str = "" + google_llm_model: str = "gemini-2.0-flash" + google_embedding_model: str = "text-embedding-004" + # Ollama settings (local) ollama_base_url: str = DEFAULT_OLLAMA_BASE_URL ollama_llm_model: str = "" @@ -189,6 +196,11 @@ class GraphitiConfig: voyage_api_key = os.environ.get("VOYAGE_API_KEY", "") voyage_embedding_model = os.environ.get("VOYAGE_EMBEDDING_MODEL", "voyage-3") + # Google AI settings + google_api_key = os.environ.get("GOOGLE_API_KEY", "") + google_llm_model = os.environ.get("GOOGLE_LLM_MODEL", "gemini-2.0-flash") + google_embedding_model = os.environ.get("GOOGLE_EMBEDDING_MODEL", "text-embedding-004") + # Ollama settings ollama_base_url = os.environ.get("OLLAMA_BASE_URL", DEFAULT_OLLAMA_BASE_URL) ollama_llm_model = os.environ.get("OLLAMA_LLM_MODEL", "") @@ -220,6 +232,9 @@ class GraphitiConfig: azure_openai_embedding_deployment=azure_openai_embedding_deployment, voyage_api_key=voyage_api_key, voyage_embedding_model=voyage_embedding_model, + google_api_key=google_api_key, + google_llm_model=google_llm_model, + google_embedding_model=google_embedding_model, ollama_base_url=ollama_base_url, ollama_llm_model=ollama_llm_model, ollama_embedding_model=ollama_embedding_model, @@ -262,6 +277,8 @@ class GraphitiConfig: ) elif self.llm_provider == "ollama": return bool(self.ollama_llm_model) + elif self.llm_provider == "google": + return bool(self.google_api_key) return False def _validate_embedder_provider(self) -> bool: @@ -278,6 +295,8 @@ class GraphitiConfig: ) elif self.embedder_provider == "ollama": return bool(self.ollama_embedding_model and self.ollama_embedding_dim) + elif self.embedder_provider == "google": + return bool(self.google_api_key) return False def get_validation_errors(self) -> list[str]: @@ -309,6 +328,9 @@ class GraphitiConfig: elif self.llm_provider == "ollama": if not self.ollama_llm_model: errors.append("Ollama LLM provider requires OLLAMA_LLM_MODEL") + elif self.llm_provider == "google": + if not self.google_api_key: + errors.append("Google LLM provider requires GOOGLE_API_KEY") else: errors.append(f"Unknown LLM provider: {self.llm_provider}") @@ -339,6 +361,9 @@ class GraphitiConfig: ) if not self.ollama_embedding_dim: errors.append("Ollama embedder provider requires OLLAMA_EMBEDDING_DIM") + elif self.embedder_provider == "google": + if not self.google_api_key: + errors.append("Google embedder provider requires GOOGLE_API_KEY") else: errors.append(f"Unknown embedder provider: {self.embedder_provider}") diff --git a/auto-claude/integrations/graphiti/providers_pkg/embedder_providers/__init__.py b/auto-claude/integrations/graphiti/providers_pkg/embedder_providers/__init__.py index 1a4fa9d5..33566a1b 100644 --- a/auto-claude/integrations/graphiti/providers_pkg/embedder_providers/__init__.py +++ b/auto-claude/integrations/graphiti/providers_pkg/embedder_providers/__init__.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from graphiti_config import GraphitiConfig from .azure_openai_embedder import create_azure_openai_embedder +from .google_embedder import create_google_embedder from .ollama_embedder import create_ollama_embedder from .openai_embedder import create_openai_embedder from .voyage_embedder import create_voyage_embedder @@ -20,4 +21,5 @@ __all__ = [ "create_voyage_embedder", "create_azure_openai_embedder", "create_ollama_embedder", + "create_google_embedder", ] diff --git a/auto-claude/integrations/graphiti/providers_pkg/embedder_providers/google_embedder.py b/auto-claude/integrations/graphiti/providers_pkg/embedder_providers/google_embedder.py new file mode 100644 index 00000000..8656cb33 --- /dev/null +++ b/auto-claude/integrations/graphiti/providers_pkg/embedder_providers/google_embedder.py @@ -0,0 +1,152 @@ +""" +Google AI Embedder Provider +=========================== + +Google Gemini embedder implementation for Graphiti. +Uses the google-generativeai SDK for text embeddings. +""" + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphiti_config import GraphitiConfig + +from ..exceptions import ProviderError, ProviderNotInstalled + + +# Default embedding model for Google +DEFAULT_GOOGLE_EMBEDDING_MODEL = "text-embedding-004" + + +class GoogleEmbedder: + """ + Google AI Embedder using the Gemini API. + + Implements the EmbedderClient interface expected by graphiti-core. + """ + + def __init__(self, api_key: str, model: str = DEFAULT_GOOGLE_EMBEDDING_MODEL): + """ + Initialize the Google embedder. + + Args: + api_key: Google AI API key + model: Embedding model name (default: text-embedding-004) + """ + try: + import google.generativeai as genai + except ImportError as e: + raise ProviderNotInstalled( + f"Google embedder requires google-generativeai. " + f"Install with: pip install google-generativeai\n" + f"Error: {e}" + ) + + self.api_key = api_key + self.model = model + + # Configure the Google AI client + genai.configure(api_key=api_key) + self._genai = genai + + async def create(self, input_data: str | list[str]) -> list[float]: + """ + Create embeddings for the input data. + + Args: + input_data: Text string or list of strings to embed + + Returns: + List of floats representing the embedding vector + """ + import asyncio + + # Handle single string input + if isinstance(input_data, str): + text = input_data + elif isinstance(input_data, list) and len(input_data) > 0: + # Join list items if it's a list of strings + if isinstance(input_data[0], str): + text = " ".join(input_data) + else: + # It might be token IDs, convert to string + text = str(input_data) + else: + text = str(input_data) + + # Run the synchronous API call in a thread pool + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + None, + lambda: self._genai.embed_content( + model=f"models/{self.model}", + content=text, + task_type="retrieval_document" + ) + ) + + return result['embedding'] + + async def create_batch(self, input_data_list: list[str]) -> list[list[float]]: + """ + Create embeddings for a batch of inputs. + + Args: + input_data_list: List of text strings to embed + + Returns: + List of embedding vectors + """ + import asyncio + + # Google's API supports batch embedding + loop = asyncio.get_event_loop() + + # Process in batches to avoid rate limits + batch_size = 100 + all_embeddings = [] + + for i in range(0, len(input_data_list), batch_size): + batch = input_data_list[i:i + batch_size] + + result = await loop.run_in_executor( + None, + lambda b=batch: self._genai.embed_content( + model=f"models/{self.model}", + content=b, + task_type="retrieval_document" + ) + ) + + # Handle single vs batch response + if isinstance(result['embedding'][0], list): + all_embeddings.extend(result['embedding']) + else: + all_embeddings.append(result['embedding']) + + return all_embeddings + + +def create_google_embedder(config: "GraphitiConfig") -> Any: + """ + Create Google AI embedder. + + Args: + config: GraphitiConfig with Google settings + + Returns: + Google embedder instance + + Raises: + ProviderNotInstalled: If google-generativeai is not installed + ProviderError: If API key is missing + """ + if not config.google_api_key: + raise ProviderError("Google embedder requires GOOGLE_API_KEY") + + model = config.google_embedding_model or DEFAULT_GOOGLE_EMBEDDING_MODEL + + return GoogleEmbedder( + api_key=config.google_api_key, + model=model + ) diff --git a/auto-claude/integrations/graphiti/providers_pkg/factory.py b/auto-claude/integrations/graphiti/providers_pkg/factory.py index 801b8c42..29f1daba 100644 --- a/auto-claude/integrations/graphiti/providers_pkg/factory.py +++ b/auto-claude/integrations/graphiti/providers_pkg/factory.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: from .embedder_providers import ( create_azure_openai_embedder, + create_google_embedder, create_ollama_embedder, create_openai_embedder, create_voyage_embedder, @@ -21,6 +22,7 @@ from .exceptions import ProviderError from .llm_providers import ( create_anthropic_llm_client, create_azure_openai_llm_client, + create_google_llm_client, create_ollama_llm_client, create_openai_llm_client, ) @@ -54,6 +56,8 @@ def create_llm_client(config: "GraphitiConfig") -> Any: return create_azure_openai_llm_client(config) elif provider == "ollama": return create_ollama_llm_client(config) + elif provider == "google": + return create_google_llm_client(config) else: raise ProviderError(f"Unknown LLM provider: {provider}") @@ -84,5 +88,7 @@ def create_embedder(config: "GraphitiConfig") -> Any: return create_azure_openai_embedder(config) elif provider == "ollama": return create_ollama_embedder(config) + elif provider == "google": + return create_google_embedder(config) else: raise ProviderError(f"Unknown embedder provider: {provider}") diff --git a/auto-claude/integrations/graphiti/providers_pkg/llm_providers/__init__.py b/auto-claude/integrations/graphiti/providers_pkg/llm_providers/__init__.py index 985eaad2..eb210859 100644 --- a/auto-claude/integrations/graphiti/providers_pkg/llm_providers/__init__.py +++ b/auto-claude/integrations/graphiti/providers_pkg/llm_providers/__init__.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from .anthropic_llm import create_anthropic_llm_client from .azure_openai_llm import create_azure_openai_llm_client +from .google_llm import create_google_llm_client from .ollama_llm import create_ollama_llm_client from .openai_llm import create_openai_llm_client @@ -20,4 +21,5 @@ __all__ = [ "create_anthropic_llm_client", "create_azure_openai_llm_client", "create_ollama_llm_client", + "create_google_llm_client", ] diff --git a/auto-claude/integrations/graphiti/providers_pkg/llm_providers/google_llm.py b/auto-claude/integrations/graphiti/providers_pkg/llm_providers/google_llm.py new file mode 100644 index 00000000..55fc84bd --- /dev/null +++ b/auto-claude/integrations/graphiti/providers_pkg/llm_providers/google_llm.py @@ -0,0 +1,175 @@ +""" +Google AI LLM Provider +====================== + +Google Gemini LLM client implementation for Graphiti. +Uses the google-generativeai SDK. +""" + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphiti_config import GraphitiConfig + +from ..exceptions import ProviderError, ProviderNotInstalled + + +# Default model for Google LLM +DEFAULT_GOOGLE_LLM_MODEL = "gemini-2.0-flash" + + +class GoogleLLMClient: + """ + Google AI LLM Client using the Gemini API. + + Implements the LLMClient interface expected by graphiti-core. + """ + + def __init__(self, api_key: str, model: str = DEFAULT_GOOGLE_LLM_MODEL): + """ + Initialize the Google LLM client. + + Args: + api_key: Google AI API key + model: Model name (default: gemini-2.0-flash) + """ + try: + import google.generativeai as genai + except ImportError as e: + raise ProviderNotInstalled( + f"Google LLM requires google-generativeai. " + f"Install with: pip install google-generativeai\n" + f"Error: {e}" + ) + + self.api_key = api_key + self.model = model + + # Configure the Google AI client + genai.configure(api_key=api_key) + self._genai = genai + self._model = genai.GenerativeModel(model) + + async def generate_response( + self, + messages: list[dict[str, Any]], + response_model: Any = None, + **kwargs: Any, + ) -> Any: + """ + Generate a response from the LLM. + + Args: + messages: List of message dicts with 'role' and 'content' + response_model: Optional Pydantic model for structured output + **kwargs: Additional arguments + + Returns: + Generated response (string or structured object) + """ + import asyncio + + # Convert messages to Google format + # Google uses 'user' and 'model' roles + google_messages = [] + system_instruction = None + + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if role == "system": + # Google handles system messages as system_instruction + system_instruction = content + elif role == "assistant": + google_messages.append({"role": "model", "parts": [content]}) + else: + google_messages.append({"role": "user", "parts": [content]}) + + # Create model with system instruction if provided + if system_instruction: + model = self._genai.GenerativeModel( + self.model, + system_instruction=system_instruction + ) + else: + model = self._model + + # Generate response + loop = asyncio.get_event_loop() + + if response_model: + # For structured output, use JSON mode + generation_config = self._genai.GenerationConfig( + response_mime_type="application/json" + ) + + response = await loop.run_in_executor( + None, + lambda: model.generate_content( + google_messages, + generation_config=generation_config + ) + ) + + # Parse JSON response into the model + import json + try: + data = json.loads(response.text) + return response_model(**data) + except (json.JSONDecodeError, Exception): + # If parsing fails, return raw text + return response.text + else: + response = await loop.run_in_executor( + None, + lambda: model.generate_content(google_messages) + ) + + return response.text + + async def generate_response_with_tools( + self, + messages: list[dict[str, Any]], + tools: list[Any], + **kwargs: Any, + ) -> Any: + """ + Generate a response with tool calling support. + + Args: + messages: List of message dicts + tools: List of tool definitions + **kwargs: Additional arguments + + Returns: + Generated response with potential tool calls + """ + # For now, fall back to regular generation + # Tool calling can be added later if needed + return await self.generate_response(messages, **kwargs) + + +def create_google_llm_client(config: "GraphitiConfig") -> Any: + """ + Create Google AI LLM client. + + Args: + config: GraphitiConfig with Google settings + + Returns: + Google LLM client instance + + Raises: + ProviderNotInstalled: If google-generativeai is not installed + ProviderError: If API key is missing + """ + if not config.google_api_key: + raise ProviderError("Google LLM provider requires GOOGLE_API_KEY") + + model = config.google_llm_model or DEFAULT_GOOGLE_LLM_MODEL + + return GoogleLLMClient( + api_key=config.google_api_key, + model=model + ) diff --git a/auto-claude/requirements.txt b/auto-claude/requirements.txt index 21f29b9d..8ba9cfe0 100644 --- a/auto-claude/requirements.txt +++ b/auto-claude/requirements.txt @@ -4,3 +4,6 @@ python-dotenv>=1.0.0 # Memory Integration (highly recommended) but can be disabled by commenting out the line below graphiti-core[falkordb]>=0.5.0 + +# Google AI embeddings (optional - for Gemini embeddings) +google-generativeai>=0.8.0