File size: 5,865 Bytes
aa03f63
9064aae
aa03f63
 
 
 
9064aae
 
aa03f63
 
 
 
 
 
 
 
 
 
9064aae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f404908
aa03f63
9064aae
 
 
 
 
 
 
 
 
 
 
 
 
 
aa03f63
9064aae
 
 
aa03f63
 
9064aae
 
 
aa03f63
 
9064aae
aa03f63
 
9064aae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa03f63
9064aae
 
 
 
 
 
 
 
 
aa03f63
 
 
 
 
9064aae
 
aa03f63
 
 
9064aae
 
aa03f63
9064aae
aa03f63
 
 
 
 
 
 
 
 
 
9064aae
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# 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