/**
* Gemini API client for prompt generation
*
* Optimized for:
* - Low latency with request caching
* - Graceful degradation with fallbacks
* - Contextual awareness for prompt refinement
*/
import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai';
import { logger } from './logger.js';
let genAI: GoogleGenerativeAI | null = null;
let model: GenerativeModel | null = null;
const DEFAULT_MODEL = 'gemini-3-pro-preview';
// Simple LRU cache for repeated requests (improves latency for similar prompts)
const responseCache = new Map<string, { response: string; timestamp: number }>();
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const MAX_CACHE_SIZE = 50;
// Performance metrics
let totalRequests = 0;
let cacheHits = 0;
let totalLatencyMs = 0;
/**
* Initialize the Gemini client
*/
export function initializeGemini(apiKey?: string): void {
const key = apiKey || process.env.GEMINI_API_KEY;
if (!key) {
logger.warn('GEMINI_API_KEY not set - AI features will be unavailable');
return;
}
genAI = new GoogleGenerativeAI(key);
model = genAI.getGenerativeModel({
model: DEFAULT_MODEL,
generationConfig: {
temperature: 1.0, // Required for Gemini 3
},
});
logger.info('Gemini client initialized', { model: DEFAULT_MODEL });
}
/**
* Check if Gemini is available
*/
export function isGeminiAvailable(): boolean {
return model !== null;
}
/**
* Get cache key for a request
*/
function getCacheKey(prompt: string, systemInstruction?: string): string {
return `${prompt.slice(0, 100)}:${systemInstruction?.slice(0, 50) || 'default'}`;
}
/**
* Check and return cached response if valid
*/
function getCachedResponse(key: string): string | null {
const cached = responseCache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
cacheHits++;
logger.debug('Cache hit', { key: key.slice(0, 30) });
return cached.response;
}
if (cached) {
responseCache.delete(key); // Expired
}
return null;
}
/**
* Store response in cache
*/
function setCachedResponse(key: string, response: string): void {
// Evict oldest if at capacity
if (responseCache.size >= MAX_CACHE_SIZE) {
const oldestKey = responseCache.keys().next().value;
if (oldestKey) responseCache.delete(oldestKey);
}
responseCache.set(key, { response, timestamp: Date.now() });
}
/**
* Get performance statistics
*/
export function getStats(): { totalRequests: number; cacheHits: number; avgLatencyMs: number } {
return {
totalRequests,
cacheHits,
avgLatencyMs: totalRequests > 0 ? Math.round(totalLatencyMs / totalRequests) : 0,
};
}
/**
* System instruction for prompt generation
*/
const SYSTEM_INSTRUCTION = `You are PromptArchitect, an expert prompt engineer. Your role is to transform user ideas into well-structured, effective prompts for AI models.
## Core Principles
1. **Clarity**: Write clear, unambiguous instructions
2. **Structure**: Use logical organization with headers, bullets, and sections
3. **Specificity**: Include concrete details, constraints, and examples
4. **Context**: Provide necessary background information
5. **Output Format**: Specify exactly how the response should be formatted
## Output Format
Always structure your generated prompts with:
- A clear objective statement
- Relevant context and constraints
- Step-by-step instructions when applicable
- Expected output format specification
- Edge cases or special considerations
Generate prompts that are immediately usable with any major AI model (GPT-4, Claude, Gemini).`;
/**
* Generate content using Gemini
*
* Features:
* - Request caching for repeated similar prompts
* - Performance tracking
* - Detailed error context
*/
export async function generateContent(
userPrompt: string,
systemInstruction?: string,
options?: { skipCache?: boolean; temperature?: number }
): Promise<string> {
if (!model) {
throw new Error('Gemini client not initialized. Set GEMINI_API_KEY environment variable.');
}
totalRequests++;
const startTime = Date.now();
const fullSystemInstruction = systemInstruction || SYSTEM_INSTRUCTION;
// Check cache first (unless explicitly skipped)
if (!options?.skipCache) {
const cacheKey = getCacheKey(userPrompt, systemInstruction);
const cached = getCachedResponse(cacheKey);
if (cached) {
totalLatencyMs += Date.now() - startTime;
return cached;
}
}
try {
logger.debug('Generating content', { promptLength: userPrompt.length });
const result = await model.generateContent({
contents: [{ role: 'user', parts: [{ text: userPrompt }] }],
systemInstruction: { role: 'model', parts: [{ text: fullSystemInstruction }] },
generationConfig: {
temperature: options?.temperature ?? 0.7,
topP: 0.95,
topK: 40,
maxOutputTokens: 8192,
},
});
const response = result.response;
const text = response.text();
const latency = Date.now() - startTime;
totalLatencyMs += latency;
// Cache the response
if (!options?.skipCache) {
const cacheKey = getCacheKey(userPrompt, systemInstruction);
setCachedResponse(cacheKey, text);
}
logger.debug('Content generated', { responseLength: text.length, latencyMs: latency });
return text;
} catch (error) {
const latency = Date.now() - startTime;
totalLatencyMs += latency;
// Provide actionable error context
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Gemini generation failed', {
error: errorMessage,
promptLength: userPrompt.length,
latencyMs: latency,
});
// Wrap with more context for the user
throw new Error(`AI generation failed after ${latency}ms: ${errorMessage}. Try again or simplify your prompt.`);
}
}
/**
* Analyze a prompt for quality metrics
*/
export async function analyzePromptQuality(prompt: string): Promise<{
scores: {
clarity: number;
specificity: number;
actionability: number;
completeness: number;
};
suggestions: string[];
warnings: string[];
}> {
if (!model) {
// Return rule-based analysis if Gemini not available
return ruleBasedAnalysis(prompt);
}
const analysisPrompt = `Analyze the following prompt for quality and provide:
1. Scores (0-10) for: clarity, specificity, actionability, completeness
2. 2-3 specific suggestions for improvement
3. Any warnings about potential issues
Respond in JSON format:
{
"scores": { "clarity": X, "specificity": X, "actionability": X, "completeness": X },
"suggestions": ["suggestion1", "suggestion2"],
"warnings": ["warning1"] // or empty array
}
PROMPT TO ANALYZE:
${prompt}`;
try {
const result = await model.generateContent({
contents: [{ role: 'user', parts: [{ text: analysisPrompt }] }],
generationConfig: {
temperature: 0.3,
maxOutputTokens: 1024,
},
});
const text = result.response.text();
// Extract JSON from response
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
return ruleBasedAnalysis(prompt);
} catch (error) {
logger.warn('LLM analysis failed, using rule-based', { error: String(error) });
return ruleBasedAnalysis(prompt);
}
}
/**
* Rule-based prompt analysis fallback
*/
function ruleBasedAnalysis(prompt: string): {
scores: {
clarity: number;
specificity: number;
actionability: number;
completeness: number;
};
suggestions: string[];
warnings: string[];
} {
const wordCount = prompt.split(/\s+/).length;
const hasStructure = /^#+\s|^\d+\.|^-\s|^\*\s/m.test(prompt);
const hasExamples = /example|e\.g\.|for instance|such as/i.test(prompt);
const hasConstraints = /must|should|cannot|don't|avoid|ensure/i.test(prompt);
const hasOutputFormat = /output|format|respond|return|provide/i.test(prompt);
const clarity = Math.min(10, 5 + (hasStructure ? 2 : 0) + (wordCount > 20 ? 2 : 0) + (wordCount < 500 ? 1 : 0));
const specificity = Math.min(10, 4 + (hasExamples ? 3 : 0) + (hasConstraints ? 2 : 0) + (wordCount > 50 ? 1 : 0));
const actionability = Math.min(10, 5 + (hasConstraints ? 2 : 0) + (hasOutputFormat ? 2 : 0) + (hasStructure ? 1 : 0));
const completeness = Math.min(10, 3 + (hasStructure ? 2 : 0) + (hasExamples ? 2 : 0) + (hasOutputFormat ? 2 : 0) + (wordCount > 100 ? 1 : 0));
const suggestions: string[] = [];
const warnings: string[] = [];
if (!hasStructure) suggestions.push('Add structure using headers, bullets, or numbered lists');
if (!hasExamples) suggestions.push('Include concrete examples to clarify expectations');
if (!hasOutputFormat) suggestions.push('Specify the desired output format');
if (wordCount < 20) warnings.push('Prompt may be too brief - consider adding more context');
if (wordCount > 1000) warnings.push('Prompt is quite long - consider breaking into sections');
return {
scores: { clarity, specificity, actionability, completeness },
suggestions: suggestions.slice(0, 3),
warnings,
};
}
export default {
initializeGemini,
isGeminiAvailable,
generateContent,
analyzePromptQuality,
};