🔌 Integración con Servicio Centralizado de Gemini
📋 Resumen
Este documento describe la integración profesional del sistema de Agent Goals con el servicio centralizado de Gemini API. Esta integración asegura consistencia arquitectónica y manejo profesional de API keys en toda la aplicación.
⚠️ Problema Identificado
Antes (Implementación Directa)
El código original usaba fetch() directo a Gemini API:
// ❌ INCORRECTO - Implementación directa
const response = await fetch(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': env.GEMINI_API_KEY // ← Lee directo de env
},
body: JSON.stringify({...})
}
);
Problemas:
- ❌ No usa el sistema centralizado de API keys
- ❌ Inconsistente con el resto de la aplicación
- ❌ No resuelve keys por usuario/organización/tier
- ❌ No sigue patrones establecidos
- ❌ Difícil de mantener y escalar
Después (Servicio Centralizado)
// ✅ CORRECTO - Usando servicio centralizado
import { generateContent, parseGeminiJson } from '../../../utils/geminiService.js';
const result = await generateContent(env, {
email: userEmail,
tier: userTier,
model: 'gemini-2.0-flash-exp',
prompt,
temperature: 0.7,
maxTokens: 2048,
timeout: 30000
});
const plan = parseGeminiJson(result.text);
Ventajas:
- ✅ Usa sistema centralizado de API keys
- ✅ Resuelve keys por prioridad (user > org > system > env)
- ✅ Consistente con toda la aplicación
- ✅ Manejo profesional de timeouts y errores
- ✅ Logging detallado con keySource tracking
- ✅ Fácil de mantener y testear
🏗️ Arquitectura de la Integración
Componentes Principales
┌─────────────────────────────────────────────────────────────┐
│ Agent Goals System │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ execute.js (Goal Execution Logic) │ │
│ │ │ │
│ │ - generateExecutionPlan() │ │
│ │ - executeStep() │ │
│ │ - verifySuccessCriteria() │ │
│ └─────────────────┬────────────────────────────────────┘ │
└──────────────────┬─┴────────────────────────────────────────┘
│
│ import
│
┌──────────────────▼─────────────────────────────────────────┐
│ Gemini Service (Centralized) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ functions/utils/geminiService.js │ │
│ │ │ │
│ │ EXPORTS: │ │
│ │ • resolveGeminiApiKey() - Resolver API key │ │
│ │ • callGemini() - Llamada directa │ │
│ │ • generateContent() - High-level wrapper │ │
│ │ • parseGeminiJson() - Parse JSON responses │ │
│ └─────────────────┬────────────────────────────────────┘ │
└──────────────────┬─┴────────────────────────────────────────┘
│
│ API Key Resolution
│
┌──────────────────▼─────────────────────────────────────────┐
│ Database (Cloudflare D1) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Tables: │ │
│ │ • tier_assignments - User custom keys │ │
│ │ • organization_api_keys - Org keys │ │
│ │ • system_api_keys - System keys │ │
│ │ • organizations - Org settings │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
🔑 Resolución de API Keys
Orden de Prioridad
El sistema resuelve API keys en el siguiente orden:
User Custom Key (si está permitido)
- Usuario ha configurado su propia API key
- Verificación de permisos por organización
Organization Key (si usuario está en org)
- Key compartida de la organización
- Solo si
api_key_mode = 'organization'
System Key por Tier
- Keys específicas por tier (free, starter, premium, enterprise)
- Permite rate limiting diferenciado
System Key (ALL)
- Key del sistema para todos los tiers
- Fallback general
Environment Secret
env.GEMINI_API_KEYcomo último recurso- Para desarrollo y testing
Ejemplo de Resolución
// Usuario: [email protected]
// Tier: premium
// Organización: Acme Corp (api_key_mode = 'organization')
// 1. Buscar custom key del usuario
const tierAssignment = await DB.query(`
SELECT custom_api_keys FROM tier_assignments WHERE email = ?
`);
// → null (usuario no tiene custom key)
// 2. Buscar key de la organización
const orgKey = await DB.query(`
SELECT api_key FROM organization_api_keys
WHERE organization_id = ? AND provider = 'gemini'
`);
// → "AIza...xyz123" ✅ FOUND
// RESULTADO: Usa key de la organización
return { key: "AIza...xyz123", source: "organization" }
📝 API del Servicio
generateContent(env, options)
Función de alto nivel que combina resolución de API key y llamada a Gemini.
Parámetros:
interface GenerateContentOptions {
// User context (optional)
email?: string; // User email for key resolution
tier?: string; // User tier (default: 'free')
organizationId?: string; // Organization ID (optional)
// Gemini config
model?: string; // Model name (default: 'gemini-2.0-flash-exp')
prompt: string; // Text prompt (required)
temperature?: number; // Temperature (default: 0.7)
maxTokens?: number; // Max output tokens (default: 8192)
timeout?: number; // Timeout in ms (default: 30000)
}
Respuesta:
interface GenerateContentResult {
text: string; // Response text
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
model: string; // Model used
keySource: string; // Where the key came from
}
Ejemplo de Uso:
import { generateContent, parseGeminiJson } from '../../../utils/geminiService.js';
try {
const result = await generateContent(env, {
email: '[email protected]',
tier: 'premium',
model: 'gemini-2.0-flash-exp',
prompt: 'Explain quantum computing in simple terms',
temperature: 0.7,
maxTokens: 1024,
timeout: 30000
});
console.log('Response:', result.text);
console.log('Tokens used:', result.usage.totalTokens);
console.log('Key source:', result.keySource); // e.g., "organization"
// Si esperas JSON, parsear con parseGeminiJson()
const data = parseGeminiJson(result.text);
} catch (error) {
console.error('Error:', error.message);
}
parseGeminiJson(text)
Parsea respuesta JSON de Gemini, manejando markdown code blocks.
Entrada:
// Gemini a veces responde con:
const text = `Here's the plan:
\`\`\`json
{
"steps": [
{"id": 1, "description": "Step 1"}
]
}
\`\`\`
This plan covers...`;
Salida:
const parsed = parseGeminiJson(text);
// → { steps: [ { id: 1, description: "Step 1" } ] }
🔄 Integración en Agent Goals
Archivo Modificado: execute.js
3 Funciones Refactorizadas:
generateExecutionPlan()- Genera plan de ejecución con Gemini
- Ahora usa
generateContent()con user context - Parsea JSON con
parseGeminiJson()
executeStep()- Ejecuta un step individual
- Usa
generateContent()con timeout de 60s - Logging de keySource para auditoría
verifySuccessCriteria()- Verifica si se cumplieron los criterios
- Usa
generateContent()con temperatura 0.3 - Parsea resultado con
parseGeminiJson()
Cambios en el Endpoint Principal
Antes:
// Sin context del usuario
const plan = await generateExecutionPlan(
env,
goal.description,
successCriteria,
goal.goal_type,
goalContext
);
Después:
// Con context del usuario para API key resolution
let userEmail = null;
let userTier = 'free';
// Obtener datos del usuario
const customer = await db.prepare(`
SELECT email FROM customers WHERE user_id = ?
`).bind(userId).first();
if (customer?.email) {
userEmail = customer.email;
const tierAssignment = await db.prepare(`
SELECT tier FROM tier_assignments WHERE email = ?
`).bind(userEmail).first();
if (tierAssignment?.tier) {
userTier = tierAssignment.tier.toLowerCase();
}
}
// Llamar con user context
const plan = await generateExecutionPlan(
env,
goal.description,
successCriteria,
goal.goal_type,
goalContext,
userEmail, // ← Nuevo parámetro
userTier // ← Nuevo parámetro
);
📊 Logging y Auditoría
Información Rastreada
Cada llamada a Gemini ahora registra:
console.log('[GoalExecution] Gemini response received', {
textLength: result.text.length,
keySource: result.keySource, // ← IMPORTANTE: De dónde vino la key
tokensUsed: result.usage.totalTokens,
stepId: step.id
});
Ejemplos de Logs
1. Usuario con custom key:
[GoalExecution] Generating execution plan { goalType: 'task-completion', criteriaCount: 3 }
[GoalExecution] Gemini response received {
textLength: 1456,
keySource: 'user', ← Custom key del usuario
tokensUsed: 342
}
[GoalExecution] Plan generated successfully {
stepsCount: 5,
complexity: 'medium',
estimatedTime: 120,
keySource: 'user'
}
2. Organización con key compartida:
[GoalExecution] Executing step { stepId: 'step_2', goalId: 'goal_123' }
[GoalExecution] Gemini response received for step execution {
stepId: 'step_2',
textLength: 523,
keySource: 'organization', ← Key de la organización
tokensUsed: 128
}
[GoalExecution] Step execution completed {
stepId: 'step_2',
success: true,
duration: 3,
keySource: 'organization'
}
3. System key (tier premium):
[GoalExecution] Verifying success criteria { criteriaCount: 4, stepsCompleted: 5 }
[GoalExecution] Gemini response received for criteria verification {
textLength: 842,
keySource: 'system-premium', ← Key del sistema para tier premium
tokensUsed: 215
}
[GoalExecution] Success criteria verification completed {
allMet: true,
keySource: 'system-premium'
}
🛡️ Manejo de Errores
Timeouts
// Configuración de timeout por función
const result = await generateContent(env, {
prompt,
timeout: 60000 // 60 segundos
});
// Si se excede, se lanza:
// Error: "Gemini API timeout after 60000ms"
API Key No Disponible
// Si no hay ninguna key disponible:
// Error: "No Gemini API key available. Please configure system keys or provide a custom key."
Respuesta Inválida
// Si Gemini no retorna JSON válido:
// Error: "Failed to parse Gemini JSON response: Unexpected token..."
Fallbacks
Las funciones implementan fallbacks en caso de error:
// generateExecutionPlan() tiene plan simple de fallback
catch (error) {
console.error('[GoalExecution] Error generating plan:', error.message);
return {
steps: [
{
id: 'step_1',
description: `Start working on: ${description}`,
estimated_time_minutes: 60,
tools_needed: ['general'],
dependencies: [],
expected_output: 'Initial progress toward goal'
}
],
total_estimated_time_minutes: 60,
complexity: 'medium',
risks: []
};
}
🧪 Testing
Test de API Key Resolution
// Test 1: User custom key
const result1 = await generateContent(env, {
email: '[email protected]',
tier: 'free',
prompt: 'Test prompt'
});
console.assert(result1.keySource === 'user');
// Test 2: Organization key
const result2 = await generateContent(env, {
email: '[email protected]',
tier: 'premium',
prompt: 'Test prompt'
});
console.assert(result2.keySource === 'organization');
// Test 3: System key
const result3 = await generateContent(env, {
email: '[email protected]',
tier: 'starter',
prompt: 'Test prompt'
});
console.assert(result3.keySource.includes('system'));
Test de Goal Execution
// Crear goal de prueba
const goal = await createGoal({
user_id: 'test_user',
description: 'Test goal execution',
success_criteria: ['Criterion 1', 'Criterion 2'],
goal_type: 'task-completion'
});
// Ejecutar step
const result = await executeGoalStep(goal.goal_id);
// Verificar
console.assert(result.success === true);
console.assert(result.steps_planned.length > 0);
console.assert(result.keySource !== undefined);
📚 Mejores Prácticas
1. Siempre Pasar User Context
// ✅ BIEN - Permite resolución de keys por usuario
await generateContent(env, {
email: userEmail,
tier: userTier,
prompt
});
// ❌ MAL - Siempre usará env.GEMINI_API_KEY
await generateContent(env, {
prompt
});
2. Usar parseGeminiJson() para JSON
// ✅ BIEN - Maneja markdown code blocks
const result = await generateContent(env, { prompt });
const data = parseGeminiJson(result.text);
// ❌ MAL - Puede fallar con markdown
const data = JSON.parse(result.text);
3. Configurar Timeouts Apropiados
// ✅ BIEN - Timeout según complejidad
await generateContent(env, {
prompt: longComplexPrompt,
timeout: 60000 // 60s para prompts largos
});
await generateContent(env, {
prompt: simplePrompt,
timeout: 10000 // 10s para prompts simples
});
4. Logging Detallado
// ✅ BIEN - Log con context
console.log('[MyFunction] Gemini call', {
operation: 'plan_generation',
userTier: tier,
promptLength: prompt.length,
keySource: result.keySource
});
// ❌ MAL - Log genérico
console.log('Gemini called');
5. Manejo de Errores
// ✅ BIEN - Catch específico con fallback
try {
const result = await generateContent(env, { prompt });
return parseGeminiJson(result.text);
} catch (error) {
console.error('[MyFunction] Error:', {
error: error.message,
stack: error.stack,
operation: 'plan_generation'
});
// Fallback razonable
return { steps: [], error: error.message };
}
🎯 Checklist de Integración
Para Nuevas Funcionalidades que Usen Gemini
- Import
generateContentyparseGeminiJsondesde geminiService.js - Obtener
userEmailyuserTierdel contexto - Pasar user context a
generateContent() - Configurar timeout apropiado
- Usar
parseGeminiJson()si esperas JSON - Logging detallado con
keySource - Manejo de errores con fallback
- Testing con diferentes key sources
Ejemplo Template
import { generateContent, parseGeminiJson } from '../path/to/geminiService.js';
async function myFunction(env, userId, ...params) {
// 1. Obtener user context
let userEmail = null;
let userTier = 'free';
try {
const customer = await env.DB.prepare(`
SELECT email FROM customers WHERE user_id = ?
`).bind(userId).first();
if (customer?.email) {
userEmail = customer.email;
const tier = await env.DB.prepare(`
SELECT tier FROM tier_assignments WHERE email = ?
`).bind(userEmail).first();
userTier = tier?.tier?.toLowerCase() || 'free';
}
} catch (e) {
console.log('[MyFunction] Using default user context');
}
// 2. Construir prompt
const prompt = `Your prompt here...`;
// 3. Llamar a Gemini con user context
try {
const result = await generateContent(env, {
email: userEmail,
tier: userTier,
model: 'gemini-2.0-flash-exp',
prompt,
temperature: 0.7,
maxTokens: 2048,
timeout: 30000
});
// 4. Log detallado
console.log('[MyFunction] Gemini response received', {
textLength: result.text.length,
keySource: result.keySource,
tokensUsed: result.usage.totalTokens
});
// 5. Parsear JSON si es necesario
const data = parseGeminiJson(result.text);
return { success: true, data, keySource: result.keySource };
} catch (error) {
// 6. Manejo de errores profesional
console.error('[MyFunction] Error:', {
error: error.message,
stack: error.stack
});
// Fallback razonable
return { success: false, error: error.message };
}
}
📎 Referencias
Archivos Relacionados
- Servicio:
functions/utils/geminiService.js(280 líneas) - Goal Execution:
functions/api/goals/[id]/execute.js(605 líneas) - AI Assistant:
functions/api/ai-assistant.js(749 líneas) - Referencia original
Base de Datos
- tier_assignments - Custom API keys de usuarios
- organization_api_keys - Keys por organización
- system_api_keys - Keys del sistema
- organizations - Configuración de API key mode
Configuración de Tiers
-- Ejemplo de configuración de system_api_keys
INSERT INTO system_api_keys (provider, tier, api_key, is_active)
VALUES
('gemini', 'free', 'AIza...key1', 1),
('gemini', 'starter', 'AIza...key2', 1),
('gemini', 'premium', 'AIza...key3', 1),
('gemini', 'ALL', 'AIza...default', 1);
✅ Conclusión
La integración con el servicio centralizado de Gemini proporciona:
- Consistencia Arquitectónica: Todo el código usa el mismo servicio
- Manejo Profesional de Keys: Resolución por prioridad (user > org > system > env)
- Auditoría Completa: Tracking de keySource en todos los logs
- Mantenibilidad: Un solo lugar para actualizar lógica de Gemini
- Escalabilidad: Fácil agregar nuevas funcionalidades
Esta es la base para una aplicación profesional, consistente y mantenible a largo plazo.
Fecha de Última Actualización: 2025-01-XX
Versión: 1.0
Estado: ✅ Implementado y Documentado