# FILE: config.py # (Corrected and complete version with all constants and RateLimiter class) import os import dspy import logging import time from datetime import datetime, timedelta from dotenv import load_dotenv from dspy_llm_wrapper import SyncCustomGeminiDspyLM # Load environment variables from a .env file load_dotenv() # Setup basic logging logging.basicConfig(level=logging.INFO, format='{levelname} {asctime} {name}: {message}', style='{') logger = logging.getLogger(__name__) # --- Rate Limiter Class --- # Integrated as per your request. App.py will instantiate this. class RateLimiter: """Manages call frequency to an API to stay within defined limits.""" def __init__(self, max_calls: int = 7, time_period: int = 60): """ Initializes the RateLimiter. Args: max_calls (int): The maximum number of calls allowed in the time period. time_period (int): The time period in seconds. """ if not isinstance(max_calls, int) or max_calls <= 0: raise ValueError("max_calls must be a positive integer.") if not isinstance(time_period, int) or time_period <= 0: raise ValueError("time_period must be a positive integer.") self.max_calls = max_calls self.time_period = time_period self.call_timestamps = [] self.total_calls_made = 0 logger.info(f"Rate Limiter Initialized: Max {self.max_calls} calls per {self.time_period} seconds.") def wait_if_needed(self): """ Blocks execution if the call rate would be exceeded. Waits until the next call is allowed. """ # Prune timestamps older than the time period current_time = datetime.now() self.call_timestamps = [ts for ts in self.call_timestamps if current_time - ts < timedelta(seconds=self.time_period)] # If the limit is reached, calculate wait time if len(self.call_timestamps) >= self.max_calls: oldest_call_in_window = self.call_timestamps[0] time_to_wait = (oldest_call_in_window + timedelta(seconds=self.time_period)) - current_time wait_seconds = time_to_wait.total_seconds() if wait_seconds > 0: logger.warning(f"[Rate Limiter]: Limit of {self.max_calls} calls reached. Waiting for {wait_seconds:.2f} seconds.") time.sleep(wait_seconds) # Block execution # Record the new call timestamp and proceed self.call_timestamps.append(datetime.now()) self.total_calls_made += 1 logger.info(f"[Rate Limiter]: Call #{self.total_calls_made} permitted.") # --- DSPy Initialization --- API_KEY = os.getenv("GOOGLE_API_KEY") def initialize_dspy( model_name: str = "gemini/gemini-2.5-flash-preview-04-17", api_key: str = None, **kwargs ) -> "SyncCustomGeminiDspyLM | None": logging.info(f"Attempting to initialize DSPy with model: {model_name}") # 1. Resolve API Key from argument or environment variable resolved_api_key = api_key or os.getenv("GOOGLE_API_KEY") if not resolved_api_key: logging.error( "FATAL: GOOGLE_API_KEY not provided as an argument or found in environment variables." ) return None # 2. Instantiate the Language Model with default kwargs lm_kwargs = {"temperature": 0.7, **kwargs} try: custom_lm = SyncCustomGeminiDspyLM( model=model_name, api_key=resolved_api_key, **lm_kwargs ) except Exception as e: logging.error(f"Failed to instantiate SyncCustomGeminiDspyLM: {e}", exc_info=True) return None # 3. Perform a live verification call to ensure credentials are valid try: logging.info("Verifying credentials with a test API call...") # Use the LM's __call__ directly for a simple, raw test test_response = custom_lm(prompt="Hello, this is a test. Respond with 'OK'.") # The __call__ method returns a list. Check for an empty list or an error message. if not test_response or "[ERROR:" in test_response[0]: raise ValueError(f"Test call failed. Response: {test_response}") logging.info("LM verification successful.") except Exception as e: logging.error( f"FATAL: LM verification failed for model '{model_name}'. " f"Please check your API key, model name, and network connection. Error: {e}", exc_info=True ) return None # 4. Configure DSPy glo bally if verification passes dspy.settings.configure(lm=custom_lm) logging.info( f"DSPy successfully configured globally with validated model: {model_name}" ) return test_response# --- Application Constants --- # State Keys STATE_STAGE = "stage" STATE_HISTORY = "conversation_history" STATE_FINAL_SYLLABUS = "final_syllabus_xml" STATE_EXPLAINER_PROMPT = "explainer_system_prompt" STATE_EXPLANATION_START_INDEX = "explanation_start_index" STATE_CURRENT_TITLE = "current_title" STATE_GENERATED_TITLE = "generated_title" STATE_RESOURCE_SUMMARY_OVERVIEW = "resource_summary_overview_for_manager" STATE_RESOURCE_TYPE_FOR_SYLLABUS = "resource_type_for_syllabus_gen" STATE_RESOURCE_CONTENT_JSON_FOR_SYLLABUS = "resource_content_json_for_syllabus_gen" STATE_UPLOADED_FILENAMES = "uploaded_filenames_list" STATE_IS_FIRST_TURN = "is_first_turn" # New state key for file upload logic # Flag Keys STATE_DISPLAY_SYLLABUS_FLAG = "display_syllabus_flag" STATE_TRANSITION_EXPLAINER_FLAG = "transition_to_explainer_flag" # Stage Names STAGE_START = "START" STAGE_NEGOTIATING = "NEGOTIATING" STAGE_EXPLAINING = "EXPLAINING" STAGE_ERROR = "ERROR" # Chat Logic Constants DEFAULT_CHAT_TITLE = "New Chat" TITLE_GENERATION_THRESHOLD = 4 TITLE_MAX_HISTORY_SNIPPET_FOR_TITLE = 6