Spaces:
Runtime error
Runtime error
| import requests | |
| import os | |
| import threading | |
| from dotenv import load_dotenv | |
| from src.chatbot.prompts import AI_MODES, DEFAULT_PROMPT | |
| import logging | |
| import time | |
| # ---------------------------- | |
| # Load environment variables | |
| # ---------------------------- | |
| load_dotenv() | |
| API_KEY = os.getenv("OPENAI_API_KEY") | |
| BASE_URL = os.getenv("OPENAI_BASE_URL") | |
| if not API_KEY or not BASE_URL: | |
| raise ValueError("API_KEY or BASE_URL not set in environment variables.") | |
| # ---------------------------- | |
| # Logging setup | |
| # ---------------------------- | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # ---------------------------- | |
| # Session storage (thread-safe) | |
| # ---------------------------- | |
| sessions_db = {} | |
| db_lock = threading.Lock() | |
| CONTEXT_WINDOW = 15 # Number of previous messages to keep for context | |
| # ---------------------------- | |
| # Main LLM call function | |
| # ---------------------------- | |
| def call_llm(user_query: str, mode: str = "socratic", session_id: str = None, reasoning_enabled=True): | |
| """ | |
| Call the best available model with smart routing, exponential backoff retries, and auto-fallback. | |
| Models: Llama-3.3-70b (Arabic/General) & GPT-OSS-120b (Logic/Math). | |
| """ | |
| session_id = session_id or "default_user" | |
| mode = (mode or "socratic").lower().strip() | |
| system_instruction = AI_MODES.get(mode, DEFAULT_PROMPT).strip() | |
| # Thread-safe history retrieval | |
| with db_lock: | |
| if session_id not in sessions_db: | |
| sessions_db[session_id] = [] | |
| history = sessions_db[session_id][-CONTEXT_WINDOW:] | |
| # Build the message payload (shared for all attempts) | |
| messages = [{"role": "system", "content": system_instruction}] | |
| messages.extend(history) | |
| messages.append({"role": "user", "content": user_query}) | |
| # --- SMART ROUTING LOGIC --- | |
| # Keywords to trigger high-reasoning models | |
| logic_keywords = ["solve", "math", "code", | |
| "physics", "calculate", "احسب", "معادلة", "برمج"] | |
| if any(word in user_query.lower() for word in logic_keywords): | |
| # Priority 1: GPT-OSS-120B (Best for logic) | |
| model_priority = ["openai/gpt-oss-120b", | |
| "llama-3.3-70b-versatile"] | |
| else: | |
| # Priority 1: Llama-3.3 (Best for Arabic/General chat) | |
| model_priority = [ | |
| "llama-3.3-70b-versatile", "openai/gpt-oss-120b"] | |
| # --- API URL SANITIZATION --- | |
| # Prevent double /v1 suffix in the base URL | |
| base_url_cleaned = BASE_URL.rstrip("/") | |
| if not base_url_cleaned.endswith("/v1") and "/v1" not in base_url_cleaned: | |
| url = f"{base_url_cleaned}/v1/chat/completions" | |
| else: | |
| url = f"{base_url_cleaned}/chat/completions" | |
| headers = { | |
| "Authorization": f"Bearer {API_KEY.strip()}", | |
| "Content-Type": "application/json" | |
| } | |
| last_error = "" | |
| MAX_RETRIES = 2 # Number of retries per model | |
| INITIAL_DELAY = 2 # Initial sleep time in seconds | |
| # --- FALLBACK LOOP (Iterate through models) --- | |
| for current_model in model_priority: | |
| payload = { | |
| "model": current_model, | |
| "messages": messages, | |
| "max_tokens": 2048, | |
| "temperature": 0.7, | |
| "stream": False | |
| } | |
| # --- RETRY LOOP (Exponential Backoff per model) --- | |
| for attempt in range(MAX_RETRIES + 1): | |
| try: | |
| logger.info( | |
| f"Attempt {attempt + 1} with {current_model} (Session: {session_id})") | |
| # Request timeout set to 40s to allow heavy reasoning models to finish | |
| response = requests.post( | |
| url, headers=headers, json=payload, timeout=40) | |
| # Case 1: Success | |
| if response.status_code == 200: | |
| result = response.json() | |
| choice = result.get("choices", [{}])[0].get("message", {}) | |
| answer = choice.get("content", "No content returned.") | |
| reasoning_details = choice.get("reasoning_details") | |
| # Thread-safe history update | |
| with db_lock: | |
| sessions_db[session_id].append( | |
| {"role": "user", "content": user_query}) | |
| sessions_db[session_id].append({ | |
| "role": "assistant", | |
| "content": answer, | |
| "reasoning_details": reasoning_details, | |
| "model_used": current_model | |
| }) | |
| return answer | |
| # Case 2: Unauthorized (Do not retry, check .env) | |
| elif response.status_code == 401: | |
| return "Error: Unauthorized. Check API Key in .env" | |
| # Case 3: Retriable errors (Rate limits 429 or Server errors 5xx) | |
| elif response.status_code in [429, 500, 502, 503, 504]: | |
| last_error = f"Model {current_model} returned {response.status_code}" | |
| if attempt < MAX_RETRIES: | |
| # Exponential backoff: 2s, 4s, etc. | |
| delay = INITIAL_DELAY * (2 ** attempt) | |
| logger.warning( | |
| f"{last_error}. Retrying in {delay}s...") | |
| time.sleep(delay) | |
| continue # Retry the same model | |
| else: | |
| logger.error( | |
| f"{current_model} exhausted all retries. Falling back...") | |
| break # Move to next model in priority list | |
| # Case 4: Other non-retriable errors | |
| else: | |
| last_error = f"Status {response.status_code}: {response.text}" | |
| logger.error( | |
| f"Unrecoverable error for {current_model}: {last_error}") | |
| break # Move to next model | |
| except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: | |
| last_error = f"Network Error: {str(e)}" | |
| if attempt < MAX_RETRIES: | |
| delay = INITIAL_DELAY * (2 ** attempt) | |
| logger.warning( | |
| f"Connection issue. Retrying in {delay}s...") | |
| time.sleep(delay) | |
| continue | |
| break # Move to next model | |
| except Exception as e: | |
| last_error = f"Unexpected error: {str(e)}" | |
| logger.error(last_error) | |
| break # Move to next model | |
| # Final response if both models and all retries fail | |
| return f"All models failed. Last error: {last_error}" | |