```typescript
import { GoogleGenAI } from "@google/genai";
// Assuming types are defined in ../types
import type { GenerateContentResponse } from "@google/genai"; // Keep import for clarity, even if not directly used in signatures
import type { ProcessingMode, ModelConfig, SuggestedParamsResponse, ModelParameterGuidance, ParameterAdvice, StaticAiModelDetails, IterateProductResult } from "../types";
/**
* This module provides core functionality for interacting with the Google Gemini API,
* including API key handling, model parameter suggestion based on text analysis,
* parameter guidance, prompt construction for iterative processing modes,
* and the main function to execute a single iteration of the AI process.
*/
// --- API Key and Client Initialization ---
// Attempt to load API key from environment variables using an IIFE for encapsulation.
// This approach keeps the API key variable scoped and uses const once determined.
const API_KEY: string | undefined = (() => {
try {
// Guard against environments where 'process' is not defined (e.g., browsers).
if (typeof process !== 'undefined' && process.env) {
const key = process.env.API_KEY;
if (typeof key === 'string' && key.length > 0) {
console.info("API_KEY loaded from process.env.");
return key;
} else if (key === undefined) {
console.warn("process.env.API_KEY is undefined. Gemini API calls will be disabled if a key is not found by other means.");
return undefined;
} else {
// Handle cases where API_KEY exists but is not a non-empty string (e.g., set to "").
console.warn("process.env.API_KEY found but is not a non-empty string. Gemini API calls will be disabled if a key is not found by other means.");
return undefined;
}
} else {
// This branch is expected in browser environments.
console.warn("Could not access process.env. This is expected in some environments (like browsers). Gemini API calls will be disabled if a key is not found.");
return undefined;
}
} catch (e) {
// Catch any unexpected errors during process.env access.
console.error("Error accessing process.env.API_KEY. Gemini API calls will be disabled.", e);
return undefined;
}
})();
let ai: GoogleGenAI | null = null;
let apiKeyAvailable = false;
// Initialize the GoogleGenAI client if an API key was found.
if (API_KEY) {
try {
// The GoogleGenAI constructor might throw if the API key is invalid or other issues occur.
ai = new GoogleGenAI({ apiKey: API_KEY });
apiKeyAvailable = true;
console.info("GoogleGenAI client initialized successfully.");
} catch (error) {
console.error("Error during GoogleGenAI client initialization. Gemini API calls will be disabled.", error);
ai = null; // Ensure client is null on failure
apiKeyAvailable = false;
}
} else {
// Log a warning if the API key was not available for initialization.
console.warn("API_KEY was not found or is not configured. GoogleGenAI client not initialized. Gemini API calls will be disabled.");
apiKeyAvailable = false;
}
const MODEL_NAME = 'gemini-2.5-flash-preview-04-17'; // Define the model name as a constant
// --- Default Model Configurations for Processing Modes ---
// These defaults are chosen to align with the typical goals of each processing mode.
export const EXPLORATORY_MODE_DEFAULTS: ModelConfig = { temperature: 0.75, topP: 0.95, topK: 60 }; // Higher values for creativity and diversity
export const REFINEMENT_MODE_DEFAULTS: ModelConfig = { temperature: 0.5, topP: 0.9, topK: 40 }; // Balanced values for controlled improvement
export const DISTILLATION_MODE_DEFAULTS: ModelConfig = { temperature: 0.25, topP: 0.85, topK: 20 }; // Lower values for focus and conciseness
// --- Text Analysis for Parameter Suggestion ---
// Constants defining thresholds for text complexity analysis.
// These values are heuristic and can be adjusted based on desired sensitivity.
const MIN_WORDS_FOR_ANALYSIS = 30; // Minimum word count for complexity analysis to be meaningful
const SHORT_SENTENCE_THRESHOLD = 12; // Sentences shorter than this are considered "short"
const LONG_SENTENCE_THRESHOLD = 25; // Sentences longer than this are considered "long"
const LOW_DIVERSITY_THRESHOLD = 0.45; // Lexical diversity below this is considered "low"
const HIGH_DIVERSITY_THRESHOLD = 0.7; // Lexical diversity above this is considered "high"
/**
* Interface for the results of text complexity analysis.
*/
interface TextStats {
wordCount: number;
sentenceCount: number;
avgSentenceLength: number;
uniqueWordCount: number;
lexicalDiversity: number;
}
/**
* Analyzes text to provide basic complexity statistics.
* Used to inform parameter suggestions.
* @param text The input text string.
* @returns TextStats object. Returns zero stats for empty or whitespace-only input.
*/
function analyzeTextComplexity(text: string): TextStats {
if (!text || !text.trim()) {
return { wordCount: 0, sentenceCount: 0, avgSentenceLength: 0, uniqueWordCount: 0, lexicalDiversity: 0 };
}
// Improved word splitting regex: includes hyphens and apostrophes within words, excludes standalone punctuation.
// This is a common approach but may not handle all linguistic nuances (e.g., contractions, specific technical terms).
const words = text.toLowerCase().match(/\b[\w'-]+\b/g) || [];
const wordCount = words.length;
// Sentence splitting regex: handles common terminators (. ! ? ;) followed by whitespace or end of string.
// This is a heuristic and may incorrectly split or merge sentences in complex text (e.g., abbreviations like "Mr." or lists).
const sentences = text.split(/[.?!;]+(?=\s+|$)/g).filter(s => s.trim().length > 0);
// Ensure sentenceCount is at least 1 if there are words, to avoid division by zero for avgSentenceLength
// if the text contains words but no clear sentence terminators.
const sentenceCount = sentences.length > 0 ? sentences.length : (wordCount > 0 ? 1 : 0);
const uniqueWords = new Set(words);
const uniqueWordCount = uniqueWords.size;
// Lexical diversity: ratio of unique words to total words.
const lexicalDiversity = wordCount > 0 ? uniqueWordCount / wordCount : 0;
// Average sentence length: total words divided by sentence count.
const avgSentenceLength = sentenceCount > 0 ? wordCount / sentenceCount : 0;
return {
wordCount,
sentenceCount,
avgSentenceLength,
uniqueWordCount,
lexicalDiversity,
};
}
// --- Public Utility Functions ---
/**
* Gets static details about the AI model being used.
* @returns StaticAiModelDetails object.
*/
export const getStaticModelDetails = (): StaticAiModelDetails => {
return {
modelName: MODEL_NAME,
tools: "None", // Indicate no specific tools are configured in this module
};
};
/**
* Checks if the API key is available and the client was successfully initialized.
* @returns boolean indicating API availability.
*/
export const isApiKeyAvailable = (): boolean => apiKeyAvailable;
/**
* Suggests model parameters (temperature, topP, topK) based on input text complexity
* and the current processing mode. Adjusts defaults heuristically.
* @param promptText The text input to analyze for complexity.
* @param currentMode The current processing mode ('exploratory', 'refinement', 'distillation').
* @returns SuggestedParamsResponse containing suggested config and rationales for the suggestions.
*/
export const suggestModelParameters = (
promptText: string,
currentMode: ProcessingMode
): SuggestedParamsResponse => {
const isPromptReallyProvided = promptText && promptText.trim().length > 0;
let baseConfig: ModelConfig;
const rationales: string[] = [];
// Determine the base configuration based on the processing mode.
switch (currentMode) {
case 'exploratory':
baseConfig = { ...EXPLORATORY_MODE_DEFAULTS };
break;
case 'refinement':
baseConfig = { ...REFINEMENT_MODE_DEFAULTS };
break;
case 'distillation':
baseConfig = { ...DISTILLATION_MODE_DEFAULTS };
break;
default: // Should not happen with typed modes, but handle defensively
console.warn(`Unknown processing mode: ${currentMode}. Falling back to exploratory defaults.`);
baseConfig = { ...EXPLORATORY_MODE_DEFAULTS };
break;
}
// If no prompt text is provided, return the base config with a rationale.
if (!isPromptReallyProvided) {
rationales.push(`No input prompt provided. Using default settings for '${currentMode}' mode.`);
return { config: baseConfig, rationales };
}
const stats = analyzeTextComplexity(promptText);
// If the input is too short for meaningful analysis, return the base config.
if (stats.wordCount < MIN_WORDS_FOR_ANALYSIS) {
rationales.push(`Input is short (${stats.wordCount} words). Using default settings for '${currentMode}' mode as complexity analysis is less reliable for very short texts.`);
return { config: baseConfig, rationales };
}
let nextTemperature = baseConfig.temperature;
let nextTopP = baseConfig.topP;
let nextTopK = baseConfig.topK;
let modified = false;
const adjustmentReasons: string[] = [];
// Parameter adjustment logic based on mode and text stats.
// These adjustments are heuristic attempts to adapt parameters to input complexity.
if (currentMode === 'exploratory') {
// For simple/concise input in exploratory mode, slightly increase randomness to encourage broader ideas.
if (stats.avgSentenceLength < SHORT_SENTENCE_THRESHOLD && stats.lexicalDiversity < LOW_DIVERSITY_THRESHOLD) {
nextTemperature += 0.08;
nextTopP += 0.02;
nextTopK += 8;
adjustmentReasons.push("input appears concise with focused vocabulary; adjusting for broader creative exploration");
modified = true;
}
// For complex/diverse input in exploratory mode, slightly decrease randomness to maintain coherence during expansion.
else if (stats.avgSentenceLength > LONG_SENTENCE_THRESHOLD || stats.lexicalDiversity > HIGH_DIVERSITY_THRESHOLD) {
nextTemperature -= 0.05;
nextTopP -= 0.02;
nextTopK -= 5;
const detailReason = stats.avgSentenceLength > LONG_SENTENCE_THRESHOLD && stats.lexicalDiversity > HIGH_DIVERSITY_THRESHOLD
? "input has long sentences and high lexical diversity"
: stats.avgSentenceLength > LONG_SENTENCE_THRESHOLD
? "input features longer sentences"
: "input shows high lexical diversity";
adjustmentReasons.push(`${detailReason}; adjusting to maintain coherence during broad expansion`);
modified = true;
}
} else if (currentMode === 'refinement') {
// For complex input in refinement mode, subtly tighten parameters for more focused improvements.
if (stats.avgSentenceLength > LONG_SENTENCE_THRESHOLD && stats.lexicalDiversity > HIGH_DIVERSITY_THRESHOLD) {
nextTemperature -= 0.03;
nextTopP -= 0.02;
nextTopK -= 3;
adjustmentReasons.push("input is complex; subtly adjusting for focused refinement and structural improvements");
modified = true;
}
} else { // distillation mode
// For complex input in distillation mode, tighten parameters further for precise reduction.
if (stats.avgSentenceLength > LONG_SENTENCE_THRESHOLD || stats.lexicalDiversity > HIGH_DIVERSITY_THRESHOLD) {
nextTemperature -= 0.05;
nextTopP -= 0.03;
nextTopK -= 5;
const detailReason = stats.avgSentenceLength > LONG_SENTENCE_THRESHOLD && stats.lexicalDiversity > HIGH_DIVERSITY_THRESHOLD
? "input has long sentences and high lexical diversity"
: stats.avgSentenceLength > LONG_SENTENCE_THRESHOLD
? "input features longer sentences"
: "input shows high lexical diversity";
adjustmentReasons.push(`${detailReason}; further tightening parameters for precise distillation`);
modified = true;
}
}
// Apply clamping to ensure parameters are within valid ranges and round to appropriate precision.
const finalConfig: ModelConfig = {
temperature: Math.max(0.0, Math.min(2.0, parseFloat(nextTemperature.toFixed(2)))), // Temp range 0.0-2.0, 2 decimal places
topP: Math.max(0.0, Math.min(1.0, parseFloat(nextTopP.toFixed(2)))), // TopP range 0.0-1.0, 2 decimal places (Note: Some APIs prefer > 0.0)
topK: Math.max(1, Math.min(128, Math.round(nextTopK))), // TopK range 1-128, integer
};
// Construct rationales based on whether adjustments were made and if they resulted in a different config.
if (!modified) {
rationales.push(`Input analysis suggests default settings for '${currentMode}' mode are a suitable starting point.`);
} else {
// Determine if the final config is effectively the same as the base config after adjustments and clamping.
// Round base config values for comparison consistency.
const baseTempRounded = parseFloat(baseConfig.temperature.toFixed(2));
const baseTopPRounded = parseFloat(baseConfig.topP.toFixed(2));
const baseTopKRounded = Math.round(baseConfig.topK);
const configIsEffectivelyBase =
finalConfig.temperature === baseTempRounded &&
finalConfig.topP === baseTopPRounded &&
finalConfig.topK === baseTopKRounded;
if (configIsEffectivelyBase) {
rationales.push(`Parameters for '${currentMode}' mode refined based on input analysis, resulting in values close to defaults:`);
adjustmentReasons.forEach(reason => rationales.push(`- ${reason}.`));
rationales.push(`Final settings align closely with mode defaults after adjustments.`);
} else {
rationales.push(`Parameters adjusted for '${currentMode}' mode based on input analysis:`);
adjustmentReasons.forEach(reason => rationales.push(`- ${reason}.`));
}
}
return { config: finalConfig, rationales };
};
/**
* Provides human-readable guidance and warnings for a given set of model parameters.
* Helps users understand the potential impact of their chosen settings.
* @param config The model configuration (temperature, topP, topK).
* @returns ModelParameterGuidance object containing advice and warnings.
*/
export const getModelParameterGuidance = (config: ModelConfig): ModelParameterGuidance => {
const warnings: string[] = [];
const advice: ParameterAdvice = {};
// Temperature Advice
if (config.temperature === 0.0) {
advice.temperature = "Deterministic output; usually used with Top-K=1 for greedy decoding.";
} else if (config.temperature > 0 && config.temperature < 0.3) {
advice.temperature = "Very focused and less random output. Good for precision tasks, factual recall (e.g., Distillation).";
} else if (config.temperature >= 0.3 && config.temperature < 0.7) {
advice.temperature = "Balanced output. Good for factual responses or controlled creativity (e.g., Refinement).";
} else if (config.temperature >= 0.7 && config.temperature <= 1.0) {
advice.temperature = "More creative and diverse output. Good for brainstorming or idea generation (e.g., Exploratory).";
} else if (config.temperature > 1.0 && config.temperature <= 1.5) {
advice.temperature = "Highly creative; may start to introduce more randomness or less coherence. Higher risk of hallucination.";
} else { // config.temperature > 1.5
advice.temperature = "Extremely creative/random; high chance of unexpected or less coherent output. Not recommended for focused tasks.";
warnings.push("Very high temperature ( > 1.5) might lead to highly random or incoherent output and increased hallucination risk.");
}
// Top-P Advice
if (config.topP === 0.0 && config.temperature === 0.0) {
advice.topP = "Not typically used when temperature is 0 (greedy decoding is active).";
} else if (config.topP === 0.0 && config.temperature > 0) {
advice.topP = "Top-P is 0 but temperature > 0. This is an unusual setting; typically topP > 0.8 for nucleus sampling. May result in no tokens being selected if not handled carefully by the model.";
warnings.push("Top-P = 0 with Temperature > 0 is an unusual setting and might lead to unexpected behavior or no output.");
} else if (config.topP > 0 && config.topP < 0.3) {
advice.topP = "Very restrictive selection of tokens. Output may be limited. Useful for high precision if combined with low temperature.";
} else if (config.topP >= 0.3 && config.topP < 0.7) {
advice.topP = "Moderately selective. Balances focus with some diversity.";
} else if (config.topP >= 0.7 && config.topP < 0.9) {
advice.topP = "Less restrictive, allowing for more diverse token choices. Good for Distillation or focused Refinement tasks.";
} else if (config.topP >= 0.9 && config.topP <= 1.0) {
advice.topP = "Allows a wider range of tokens, increasing diversity. Values around 0.9-0.95 are common for balanced creative tasks (e.g., Exploratory, broad Refinement).";
}
// Top-K Advice
if (config.topK === 1) {
advice.topK = "Greedy decoding. Always picks the single most likely next token. Best for factual, deterministic output.";
} else if (config.topK > 1 && config.topK < 10) {
advice.topK = "Very limited token choices. Output may be repetitive. Good for specific answers when combined with low temperature.";
} else if (config.topK >= 10 && config.topK < 40) {
advice.topK = "Moderately limited token choices. Good for focused output (e.g., Distillation, precise Refinement) with some variety.";
} else if (config.topK >= 40 && config.topK < 80) {
advice.topK = "Broader token selection. Good for Exploratory tasks or Refinement requiring creative yet focused exploration.";
} else { // config.topK >= 80
advice.topK = "Very broad token selection. Relies more on Temperature and Top-P for filtering. Can increase randomness or lack of focus if not carefully managed.";
}
// Parameter Combination Warnings
if (config.temperature > 0.8 && config.topK < 20 && config.topK > 1) {
warnings.push("Higher temperature with low Top-K (< 20, not 1) can be unpredictable and may produce lower-quality results or limit creativity unexpectedly.");
}
if (config.topP < 0.7 && config.topP > 0 && config.temperature > 0.7) {
warnings.push("Low Top-P (< 0.7) with moderate/high temperature can be restrictive and may cause erratic behavior by cutting off too many good options early.");
}
if (config.topP === 1.0 && config.topK === 1 && config.temperature > 0) {
warnings.push("Using Top-P=1 and Top-K=1 simultaneously with temperature > 0 is unusual. Top-K=1 implies greedy decoding (if temp=0), making Top-P less relevant.");
}
if (config.topK < 10 && config.topK > 1 && config.temperature < 0.5) {
warnings.push("Very low Top-K (< 10, not 1) with low temperature might overly restrict word choice, leading to repetitive or simplistic output.");
}
if (config.temperature === 0.0 && config.topK > 1) {
warnings.push("Temperature of 0.0 typically implies greedy decoding (Top-K=1). Using Top-K > 1 with Temp=0.0 might yield inconsistent behavior; Top-K=1 is preferred here.");
}
if (config.temperature > 0.8 && config.topP > 0.95 && config.topK > 60) {
warnings.push("High values for Temperature, Top-P, and Top-K together can lead to very random, less coherent, or potentially off-topic outputs. Consider reducing one or more for better focus.");
}
return { warnings, advice };
};
// --- Prompt Construction ---
/**
* Determines the system instruction and core user instructions based on the processing mode.
* These instructions guide the model's behavior for the specific task (Exploration, Refinement, Distillation).
* @param originalPrompt The original user input. (Not directly used in instruction text, but kept for context).
* @param processingMode The current processing mode.
* @param iterationNumber The current iteration number. (Not directly used in instruction text, but kept for context).
* @param maxIterations The maximum number of iterations. (Not directly used in instruction text, but kept for context).
* @returns An object containing systemInstruction and coreUserInstructions strings.
*/
const getUserPromptComponents = (
originalPrompt: string,
processingMode: ProcessingMode,
iterationNumber: number,
maxIterations: number,
): { systemInstruction: string; coreUserInstructions: string } => {
let systemInstruction: string;
let coreUserInstructions: string;
// The instruction text is critical for guiding the model's behavior in each mode.
// These are kept as per the original design, assuming they are effective prompts for Gemini.
switch (processingMode) {
case 'exploratory':
systemInstruction = `You are an AI process engine in 'EXPLORATORY' mode. Your goal is to iteratively evolve a "product" by creatively generating new ideas, expanding upon existing concepts, and exploring diverse facets related to the original user input. In each iteration, focus on adding distinct, novel information or elaborating imaginatively on existing points. Prioritize breadth of relevant ideas and creative connections. This mode encourages divergent thinking. Only if no further meaningful novel expansion or creative addition is possible without becoming trivial, redundant, or off-topic, prefix your response with "CONVERGED:". If your response is truncated, you will be prompted to continue.`;
coreUserInstructions = `
1. **Identify Areas for Creative Expansion**: Analyze the "Current State of Product". Brainstorm novel concepts, new related sections, or creative elaborations that build upon the "Original User Input".
2. **Generate Diverse and Novel Content**: For the identified area(s), provide fresh, imaginative, and detailed content. Aim for originality and a broad exploration of possibilities.
3. **Integrate Creatively**: Weave these new ideas into an enriched version of the product.
4. **Output Format**: Provide ONLY the new, expanded product. Do NOT include preambles. If converged, prefix with "CONVERGED:".`;
break;
case 'refinement':
systemInstruction = `You are an AI process engine in 'REFINEMENT' mode. Your goal is to iteratively improve and optimize a "product" based on an original user input. Focus on enhancing clarity, correctness, conciseness (where appropriate), structure, efficiency, or overall quality. You might refactor, rewrite, or restructure elements. If the product is significantly improved and further refinement yields diminishing returns or is no longer substantially enhancing quality, you MAY prefix your response with "CONVERGED:". If your response is truncated, you will be prompted to continue.`;
coreUserInstructions = `
5. **Identify Areas for Improvement**: Analyze the "Current State of Product" for specific aspects that can be improved based on the "Original User Input" (e.g., clarity, conciseness, structure, code efficiency, argument strength, factual accuracy).
6. **Implement Targeted Enhancements**: Rewrite, refactor, add, or remove targeted content to improve the identified areas. The goal is to make the product measurably better in quality or function.
7. **Integrate into an Improved Product**: Formulate a new version of the product that incorporates these enhancements seamlessly.
8. **Output Format**: Provide ONLY the new, refined product. If converged, you may prefix with "CONVERGED:". Do NOT include other preambles.`;
break;
case 'distillation':
systemInstruction = `You are an AI process engine in 'DISTILLATION' mode. Your goal is to iteratively distill a "product" based on an original user input down to its most essential, minimal, core representation. Focus on identifying the absolute fundamental concepts and removing elaborations, examples, or redundant information. Simplify phrasing and ensure factual accuracy and core meaning are preserved. If the product is maximally distilled and no further meaningful reduction is possible without losing its core identity, you MUST prefix your response with "CONVERGED:". If your response is truncated, you will be prompted to continue.`;
coreUserInstructions = `
9. **Identify Core Essence**: Analyze the "Current State of Product" for its fundamental concepts in relation to the "Original User Input."
10. **Distill and Reduce**: Remove elaborations, simplify phrasing, and consolidate ideas to their most direct and concise form, prioritizing the preservation of core meaning and factual accuracy.
11. **Integrate into Minimal Form**: Formulate a new, significantly more concise product representing this distilled essence.
12. **Output Format**: Provide ONLY the new, distilled product. If converged, prefix with "CONVERGED:". Do NOT include other preambles.`;
break;
default: // Should not happen, but provide a fallback
systemInstruction = "Error: Unknown processing mode. Please select a valid mode.";
coreUserInstructions = "No instructions due to unknown mode.";
break;
}
return { systemInstruction, coreUserInstructions };
};
/**
* Builds the full user prompt string for a given iteration and continuation state.
* This prompt includes the original input, the current product state, iteration info,
* and mode-specific instructions.
* @param originalPrompt The initial user input.
* @param productToProcess The current state of the product being processed. This is sent to the model as context.
* @param iterationNumber The current iteration number (1-based).
* @param maxIterations The total number of iterations allowed.
* @param processingMode The current processing mode.
* @param coreUserInstructions The mode-specific instructions generated by getUserPromptComponents.
* @param isContinuation Boolean indicating if this is a continuation call for the current iteration.
* @returns The complete user prompt string.
* @remarks Sending the full `productToProcess` back to the model in each iteration can lead to
* context window limitations for very long products. A more advanced strategy might involve
* summarizing the product or sending only recent changes, but that is beyond the scope
* of this function's current design.
*/
const buildFullUserPrompt = (
originalPrompt: string,
productToProcess: string,
iterationNumber: number,
maxIterations: number,
processingMode: ProcessingMode,
coreUserInstructions: string,
isContinuation: boolean
): string => {
return `
Original User Input:
"${originalPrompt}"
${isContinuation ? `PARTIAL PRODUCT CONTENT (GENERATED SO FAR FOR THIS ITERATION ${iterationNumber}):` : `Current State of Product (after Iteration ${iterationNumber - 1}):`}
\`\`\`
${productToProcess}
\`\`\`
This is Iteration ${iterationNumber} of a maximum of ${maxIterations} iterations in ${processingMode.toUpperCase()} mode.
${isContinuation ? `\nIMPORTANT: YOU ARE CONTINUING A PREVIOUSLY TRUNCATED RESPONSE FOR THIS ITERATION. Your previous output was cut short.` : ''}
Instructions for this iteration ${isContinuation ? '(CONTINUING PREVIOUS RESPONSE)' : ''}:
${coreUserInstructions}
${isContinuation ?
`Please continue generating the product text EXACTLY where you left off from the "PARTIAL PRODUCT CONTENT" shown above.
DO NOT repeat any part of the "PARTIAL PRODUCT CONTENT".
DO NOT add any preambles, apologies, or explanations for continuing.
DO NOT restart the whole product.
Simply provide the NEXT CHUNK of the product text.
If you believe the product is complete or converged (and you would have prefixed with "CONVERGED:"), ensure that prefix is at the very start of THIS CHUNK if it's the final part.`
: `Provide the next version of the product (or the converged product with the prefix):`}
`;
};
// --- Main Iteration Function ---
/**
* Executes a single iteration of the product refinement process using the Gemini API.
* It sends the current product state to the model, processes the streaming response,
* handles potential truncation via continuation calls, and reports progress via callbacks.
* Manages halting and basic API error handling.
* @param previousProduct The product text from the previous iteration (or initial input for iteration 1).
* @param iterationNumber The current iteration number (1-based).
* @param maxIterations The total number of iterations allowed.
* @param originalPrompt The initial user input.
* @param processingMode The current processing mode.
* @param modelConfig The model parameters to use for this iteration.
* @param onChunkReceived Callback function to handle received text chunks (for streaming UI updates).
* @param isHaltSignalReceived Callback function to check if a halt signal has been received by the user.
* @returns Promise resolving to IterateProductResult containing the product state and status ('COMPLETED', 'CONVERGED', 'HALTED').
* @throws Error if the API client is not initialized or a critical API error occurs during generation.
*/
export const iterateProduct = async (
previousProduct: string,
iterationNumber: number,
maxIterations: number,
originalPrompt: string,
processingMode: ProcessingMode,
modelConfig: ModelConfig,
onChunkReceived: (chunkText: string, isInitialChunkOfIteration: boolean) => void,
isHaltSignalReceived: () => boolean
): Promise<IterateProductResult> => {
if (!ai) {
// If the API client wasn't initialized (likely due to missing API key), throw an error immediately.
throw new Error("Gemini API client not initialized. API Key might be missing or client setup failed.");
}
// Get the mode-specific instructions for the model.
const { systemInstruction, coreUserInstructions } = getUserPromptComponents(originalPrompt, processingMode, iterationNumber, maxIterations);
let accumulatedIterationText = ""; // Accumulates text across potential continuation calls for this iteration.
let productContentForNextPrompt = previousProduct; // The content sent back to the model as context for the next call (initially previousProduct, then accumulatedIterationText).
let currentCallIsContinuation = false; // Flag to indicate if the current API call is a continuation.
let callCount = 0; // Counter for API calls within this single logical iteration.
const MAX_CONTINUATION_CALLS = 5; // Limit continuation calls per iteration to prevent infinite loops or excessive cost.
let isFirstChunkOfThisLogicalIteration = true; // Flag to inform the callback if this is the very first chunk received for this iteration.
// Loop to handle potential continuation calls if the model's response is truncated.
while (callCount < MAX_CONTINUATION_CALLS) {
callCount++;
// Add a small delay before subsequent continuation calls to avoid hitting rate limits immediately after a truncation.
if (currentCallIsContinuation && callCount > 1) {
console.log(`Iteration ${iterationNumber}: Delaying for 1000ms before continuation call #${callCount}`);
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay
}
// Check the halt signal before making the API call.
if (isHaltSignalReceived()) {
console.log(`Iteration ${iterationNumber}: Halt signal received before API call #${callCount}. Returning current accumulated text.`);
return { product: accumulatedIterationText, status: 'HALTED' };
}
// Build the full prompt for the current API call.
const userPromptForThisCall = buildFullUserPrompt(
originalPrompt,
productContentForNextPrompt,
iterationNumber,
maxIterations,
processingMode,
coreUserInstructions,
currentCallIsContinuation
);
console.log(`Gemini API Stream Call #${callCount} for Iteration ${iterationNumber}. Mode: ${processingMode}. Continuation: ${currentCallIsContinuation}`);
let textFromThisStreamCall = ""; // Accumulates text received from the current stream call.
let streamFinishReason: string | undefined; // Stores the finish reason from the API response.
let safetyRatingsInfo: any = null; // To capture safety ratings if available.
try {
// Make the API call using the streaming method.
const stream = await ai.models.generateContentStream({
model: MODEL_NAME,
contents: [{ role: 'user', parts: [{ text: userPromptForThisCall }] }],
generationConfig: { // Model parameters go here
temperature: modelConfig.temperature,
topP: modelConfig.topP,
topK: modelConfig.topK,
},
systemInstruction: { // System instruction is a top-level parameter
parts: [{ text: systemInstruction }]
}
});
// Process the streaming response chunk by chunk.
for await (const chunk of stream) {
// Check halt signal during stream processing.
if (isHaltSignalReceived()) {
console.log(`Iteration ${iterationNumber}, API Call ${callCount}: Halt signal received during stream.`);
streamFinishReason = "HALTED_BY_USER_DURING_STREAM"; // Custom internal reason to signal halt.
// Ensure any received text in this partial chunk is added before halting.
const currentChunkText = chunk.text;
if (typeof currentChunkText === 'string' && currentChunkText.length > 0) {
textFromThisStreamCall += currentChunkText;
onChunkReceived(currentChunkText, isFirstChunkOfThisLogicalIteration);
isFirstChunkOfThisLogicalIteration = false; // Subsequent chunks are not the first.
}
break; // Exit the stream processing loop.
}
const chunkText = chunk.text;
// Access finishReason and safetyRatings from the candidate metadata in the chunk.
streamFinishReason = chunk.candidates?.[0]?.finishReason;
if (chunk.candidates?.[0]?.safetyRatings) {
safetyRatingsInfo = chunk.candidates[0].safetyRatings;
}
// If the chunk contains text, append it and call the callback.
if (typeof chunkText === 'string' && chunkText.length > 0) {
textFromThisStreamCall += chunkText;
onChunkReceived(chunkText, isFirstChunkOfThisLogicalIteration);
isFirstChunkOfThisLogicalIteration = false; // Subsequent chunks are not the first.
}
}
// Append text received from this specific stream call to the total accumulated text for the iteration.
accumulatedIterationText += textFromThisStreamCall;
// If halted during the stream, return immediately with the HALTED status.
if (streamFinishReason === "HALTED_BY_USER_DURING_STREAM") {
return { product: accumulatedIterationText, status: 'HALTED' };
}
// Check for a critical failure: no text received on the very first API attempt of this iteration.
// This indicates a problem with the request, API, or safety filters.
if (!textFromThisStreamCall && callCount === 1 && !currentCallIsContinuation && !accumulatedIterationText && !isHaltSignalReceived()) {
let errorMessage = `Received no text content from Gemini API on initial stream attempt for Iteration ${iterationNumber}.`;
if (streamFinishReason === "SAFETY") {
errorMessage = `Generation stopped by API due to safety policy during Iteration ${iterationNumber}. Review input/product or adjust settings. Safety Ratings: ${JSON.stringify(safetyRatingsInfo)}`;
} else if (streamFinishReason === "RECITATION") {
errorMessage = `Generation stopped by API due to recitation policy during Iteration ${iterationNumber}.`;
} else if (streamFinishReason) {
errorMessage = `Generation failed with API reason: '${streamFinishReason}' during Iteration ${iterationNumber} on initial attempt.`;
} else {
errorMessage = `Generation failed with unknown API issue during Iteration ${iterationNumber} on initial attempt.`;
}
console.warn(errorMessage, "Full diagnostic streamFinishReason:", streamFinishReason, "SafetyRatings:", safetyRatingsInfo);
// Throw an error to be caught by the calling code (e.g., App.tsx) for user display.
throw new Error(errorMessage);
}
// Determine if a continuation call is needed based on the finish reason.
// 'MAX_TOKENS' and 'OTHER' typically indicate the model stopped due to length limits.
const isTruncated = (streamFinishReason === "MAX_TOKENS" || streamFinishReason === "OTHER");
// Request a continuation if the response was truncated AND we received some text (to avoid infinite loops on empty responses)
// AND the model hasn't already indicated convergence.
if (isTruncated && textFromThisStreamCall.length > 0 && !accumulatedIterationText.startsWith("CONVERGED:")) {
currentCallIsContinuation = true;
// For the next call, the "previous product" context is the accumulated text so far.
productContentForNextPrompt = accumulatedIterationText;
if (callCount >= MAX_CONTINUATION_CALLS) {
console.warn(`Iteration ${iterationNumber}: Max continuation calls (${MAX_CONTINUATION_CALLS}) reached. Returning possibly truncated content.`);
break; // Exit the continuation loop if max calls reached.
}
console.log(`Iteration ${iterationNumber}: Content stream truncated (reason: ${streamFinishReason}). Attempting continuation stream call ${callCount + 1}.`);
} else {
// If not truncated, or if truncated but no text was received, or if converged, stop the continuation loop.
if (isTruncated && textFromThisStreamCall.length === 0 && !accumulatedIterationText.startsWith("CONVERGED:") && callCount < MAX_CONTINUATION_CALLS) {
console.warn(`Iteration ${iterationNumber}: Content stream reported as truncated (reason: ${streamFinishReason}) but no new text was generated in this stream call. Stopping continuation for this iteration.`);
}
if (streamFinishReason === "SAFETY" || streamFinishReason === "RECITATION") {
console.warn(`Iteration ${iterationNumber}, Call ${callCount}: Stream ended due to ${streamFinishReason}. Accumulated text so far: "${accumulatedIterationText.substring(0, Math.min(accumulatedIterationText.length, 100))}..." Safety: ${JSON.stringify(safetyRatingsInfo)}`);
} else if (streamFinishReason && streamFinishReason !== 'STOP' && streamFinishReason !== 'FINISH') { // Log other non-standard finish reasons
console.warn(`Iteration ${iterationNumber}, Call ${callCount}: Stream finished with reason '${streamFinishReason}'. Accumulated text so far: "${accumulatedIterationText.substring(0, Math.min(accumulatedIterationText.length, 100))}..."`);
}
break; // Exit the continuation loop.
}
} catch (error) {
// If a halt signal is received during error processing, prioritize halting.
if (isHaltSignalReceived()) {
console.warn(`Iteration ${iterationNumber}, API Call ${callCount}: Halt signal received during error handling. Prioritizing HALT. Original error:`, error);
return { product: accumulatedIterationText, status: 'HALTED' };
}
// Handle API errors.
let errorMessage = `An unexpected error occurred during API stream call for Iteration ${iterationNumber}, API Call #${callCount}.`;
let detailedError = error; // Keep original error for logging.
if (error instanceof Error) {
if (error.message.includes("429") || error.message.toUpperCase().includes("RESOURCE_EXHAUSTED")) {
errorMessage = `API rate limit (429 RESOURCE_EXHAUSTED) hit during Iteration ${iterationNumber}. Please wait a minute and try again or reduce request frequency.`;
} else if (error.message.includes("Received no text content") || error.message.includes("safety policy") || error.message.includes("recitation policy")){
// These specific messages are generated and thrown by the code above, re-throwing them is fine.
errorMessage = error.message;
} else {
errorMessage = `Error during API call for Iteration ${iterationNumber}: ${error.message}`;
}
} else {
errorMessage = `Unknown error during API call for Iteration ${iterationNumber}: ${String(error)}`;
}
console.error(errorMessage, detailedError); // Log original error object for debugging.
// Re-throw a new Error with the refined message to be caught by the calling code.
throw new Error(errorMessage);
}
// Check the halt signal after processing the response chunk but before the next potential API call.
if (isHaltSignalReceived()) {
console.log(`Iteration ${iterationNumber}: Halt signal received after API call #${callCount} processing. Returning current accumulated text.`);
return { product: accumulatedIterationText, status: 'HALTED' };
}
} // End of while loop for continuations
// After the loop finishes (either completed, converged, or max calls reached), check the final state.
if (isHaltSignalReceived()) {
console.warn(`Iteration ${iterationNumber}: Process HALTED.`);
return { product: accumulatedIterationText, status: 'HALTED' };
}
// If the loop exited because max continuation calls were reached and the model didn't converge.
if (callCount >= MAX_CONTINUATION_CALLS && !accumulatedIterationText.startsWith("CONVERGED:")) {
console.warn(`Iteration ${iterationNumber}: Finished due to reaching the maximum number of continuation calls (${MAX_CONTINUATION_CALLS}). The product might be incomplete.`);
// Return status as COMPLETED, as the iteration finished its attempts, but log a warning.
}
// Check for the convergence prefix at the beginning of the final accumulated text.
if (accumulatedIterationText.startsWith("CONVERGED:")) {
// Remove the prefix and trim leading whitespace before returning the product.
const product = accumulatedIterationText.substring("CONVERGED:".length).trimStart();
console.log(`Iteration ${iterationNumber}: Process CONVERGED.`);
return { product, status: 'CONVERGED' };
}
// If the loop finished without halting, reaching max calls, or converging, it completed successfully.
console.log(`Iteration ${iterationNumber}: Process COMPLETED.`);
return { product: accumulatedIterationText, status: 'COMPLETED' };
};
```