""" Utility functions for LLM-related operations """ import json import os import time from typing import Dict, List, Any, Optional # Import from OpenAI newer SDK from openai import OpenAI # Import local modules from cache_utils import cached_llm_call, get_from_cache, save_to_cache from config import OPENAI_API_KEY, OPENAI_MODEL, OPENAI_TIMEOUT, OPENAI_MAX_RETRIES, USE_FALLBACK_DATA, DEBUG_MODE def call_llm(system_prompt: str, user_prompt: str, mock_data: Optional[Dict] = None) -> Dict[str, Any]: """ Call LLM with improved error handling and response validation Args: system_prompt: System role prompt user_prompt: User input prompt mock_data: Mock data for fallback Returns: Parsed JSON response from LLM Raises: ValueError: If response format is invalid Exception: For other API call failures """ cache_key = f"{system_prompt}_{user_prompt}" cached_response = get_from_cache(cache_key) if cached_response: if DEBUG_MODE: print("Using cached response") return json.loads(cached_response) try: client = OpenAI(api_key=OPENAI_API_KEY) # Make API call with temperature=0.1 for more consistent outputs response = client.chat.completions.create( model=OPENAI_MODEL, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.1, response_format={"type": "json_object"} ) content = response.choices[0].message.content # Validate JSON response try: json_response = json.loads(content) validate_response_format(json_response) save_to_cache(cache_key, content) return json_response except json.JSONDecodeError: raise ValueError("Invalid JSON response from LLM") except Exception as e: if DEBUG_MODE: print(f"LLM API call failed: {str(e)}") if USE_FALLBACK_DATA and mock_data: return mock_data raise def validate_response_format(response: Dict[str, Any]) -> None: """ Validate the format of LLM response Args: response: Parsed JSON response Raises: ValueError: If required fields are missing or invalid """ required_fields = { "decomposition": ["main_concept", "sub_concepts", "relationships"], "explanation": ["explanation", "key_points", "examples", "practice", "resources"] } # Determine response type and validate fields if "main_concept" in response: fields = required_fields["decomposition"] elif "explanation" in response: fields = required_fields["explanation"] else: raise ValueError("Unknown response format") for field in fields: if field not in response: raise ValueError(f"Missing required field: {field}") def _do_decompose_concepts(params: Dict[str, Any]) -> Dict[str, Any]: """ Execute concept decomposition (internal function) Args: params: Parameter dictionary containing user profile and question Returns: Decomposed concept data """ from prompts import generate_decomposition_prompt user_profile = params.get("user_profile", {}) question = params.get("question", "") system_prompt, user_prompt = generate_decomposition_prompt( question, user_profile.get("grade", "Not specified"), user_profile.get("subject", "Not specified"), user_profile.get("needs", "Not specified") ) from concept_handler import MOCK_DECOMPOSITION_RESULT response = call_llm(system_prompt, user_prompt, MOCK_DECOMPOSITION_RESULT) return response def decompose_concepts(user_profile: Dict[str, str], question: str) -> Dict[str, Any]: """ Use LLM to break down user questions into multiple concepts, with caching Args: user_profile: User profile information question: User question Returns: Dictionary containing main concept, sub-concepts, and relationships """ params = { "user_profile": user_profile, "question": question } return cached_llm_call("decompose", params, _do_decompose_concepts) def _do_get_concept_explanation(params: Dict[str, Any]) -> Dict[str, Any]: """ Execute concept explanation (internal function) Args: params: Parameter dictionary containing user profile and concept information Returns: Concept explanation data """ from prompts import generate_explanation_prompt user_profile = params.get("user_profile", {}) concept_id = params.get("concept_id", "") concept_name = params.get("concept_name", "") concept_description = params.get("concept_description", "") system_prompt, user_prompt = generate_explanation_prompt( concept_name, concept_description, "", # Original question (not needed here) user_profile.get("grade", "Not specified"), user_profile.get("subject", "Not specified"), user_profile.get("needs", "Not specified") ) from concept_handler import MOCK_EXPLANATION_RESULT response = call_llm(system_prompt, user_prompt, MOCK_EXPLANATION_RESULT) return response def get_concept_explanation(user_profile: Dict[str, str], concept_id: str, concept_name: str, concept_description: str) -> Dict[str, Any]: """ Get detailed explanation and learning resources for a specific concept, with caching Args: user_profile: User profile information concept_id: Concept ID concept_name: Concept name concept_description: Brief concept description Returns: Dictionary containing explanation, examples, and resources """ params = { "user_profile": user_profile, "concept_id": concept_id, "concept_name": concept_name, "concept_description": concept_description } return cached_llm_call("explain", params, _do_get_concept_explanation)