|
"""
|
|
⚙️ Configuration Management for CourseCrafter AI
|
|
|
|
Centralized configuration system with environment variable support and validation.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
from typing import Dict, Any, Optional, List
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from dotenv import load_dotenv
|
|
|
|
from ..types import LLMProvider
|
|
|
|
|
|
@dataclass
|
|
class LLMProviderConfig:
|
|
"""Configuration for a specific LLM provider"""
|
|
api_key: str
|
|
model: str
|
|
temperature: float = 0.7
|
|
max_tokens: Optional[int] = None
|
|
timeout: int = 60
|
|
base_url: Optional[str] = None
|
|
|
|
class Config:
|
|
"""
|
|
Centralized configuration management for Course Creator AI
|
|
|
|
Handles environment variables, API keys, and URL configurations.
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
load_dotenv()
|
|
|
|
|
|
self._config = self._load_default_config()
|
|
self._validate_config()
|
|
|
|
def _load_default_config(self) -> Dict[str, Any]:
|
|
"""Load default configuration with environment variable overrides"""
|
|
|
|
default_model = os.getenv("DEFAULT_MODEL", "gpt-4.1-nano")
|
|
|
|
|
|
default_llm_provider = os.getenv("DEFAULT_LLM_PROVIDER", "openai")
|
|
|
|
return {
|
|
|
|
"llm_providers": {
|
|
"openai": {
|
|
"api_key": os.getenv("OPENAI_API_KEY", ""),
|
|
"model": os.getenv("OPENAI_MODEL", default_model),
|
|
"temperature": float(os.getenv("OPENAI_TEMPERATURE", "0.7")),
|
|
"max_tokens": int(os.getenv("OPENAI_MAX_TOKENS", "20000")) if os.getenv("OPENAI_MAX_TOKENS") else None,
|
|
"timeout": int(os.getenv("OPENAI_TIMEOUT", "60"))
|
|
},
|
|
"anthropic": {
|
|
"api_key": os.getenv("ANTHROPIC_API_KEY", ""),
|
|
"model": os.getenv("ANTHROPIC_MODEL", "claude-3-5-sonnet-20241022"),
|
|
"temperature": float(os.getenv("ANTHROPIC_TEMPERATURE", "0.7")),
|
|
"max_tokens": int(os.getenv("ANTHROPIC_MAX_TOKENS", "20000")) if os.getenv("ANTHROPIC_MAX_TOKENS") else None,
|
|
"timeout": int(os.getenv("ANTHROPIC_TIMEOUT", "60"))
|
|
},
|
|
"google": {
|
|
"api_key": os.getenv("GOOGLE_API_KEY", ""),
|
|
"model": os.getenv("GOOGLE_MODEL", "gemini-2.0-flash"),
|
|
"temperature": float(os.getenv("GOOGLE_TEMPERATURE", "0.7")),
|
|
"max_tokens": int(os.getenv("GOOGLE_MAX_TOKENS", "20000")) if os.getenv("GOOGLE_MAX_TOKENS") else None,
|
|
"timeout": int(os.getenv("GOOGLE_TIMEOUT", "60"))
|
|
},
|
|
"openai_compatible": {
|
|
"api_key": os.getenv("OPENAI_COMPATIBLE_API_KEY", "dummy"),
|
|
"base_url": os.getenv("OPENAI_COMPATIBLE_BASE_URL", ""),
|
|
"model": os.getenv("OPENAI_COMPATIBLE_MODEL", ""),
|
|
"temperature": float(os.getenv("OPENAI_COMPATIBLE_TEMPERATURE", "0.7")),
|
|
"max_tokens": int(os.getenv("OPENAI_COMPATIBLE_MAX_TOKENS", "20000")) if os.getenv("OPENAI_COMPATIBLE_MAX_TOKENS") else None,
|
|
"timeout": int(os.getenv("OPENAI_COMPATIBLE_TIMEOUT", "60"))
|
|
}
|
|
},
|
|
|
|
|
|
"course_generation": {
|
|
"default_difficulty": "beginner",
|
|
"default_lesson_count": 5,
|
|
"max_lesson_duration": 30,
|
|
"include_images": True,
|
|
"include_flashcards": True,
|
|
"include_quizzes": True,
|
|
"research_depth": "comprehensive"
|
|
},
|
|
|
|
|
|
"image_generation": {
|
|
"pollinations_api_token": os.getenv("POLLINATIONS_API_TOKEN", ""),
|
|
"pollinations_api_reference": os.getenv("POLLINATIONS_API_REFERENCE", ""),
|
|
"default_width": 1280,
|
|
"default_height": 720,
|
|
"default_model": "gptimage",
|
|
"enhance_prompts": True,
|
|
"no_logo": True
|
|
},
|
|
|
|
|
|
"export": {
|
|
"default_formats": ["pdf", "markdown"],
|
|
"output_directory": os.getenv("COURSECRAFTER_OUTPUT_DIR", "./output"),
|
|
"max_file_size": 50 * 1024 * 1024,
|
|
"compression": True
|
|
},
|
|
|
|
|
|
"ui": {
|
|
"theme": "soft",
|
|
"show_progress": True,
|
|
"auto_scroll": True,
|
|
"max_concurrent_generations": 3
|
|
},
|
|
|
|
|
|
"system": {
|
|
"default_llm_provider": default_llm_provider,
|
|
"max_turns": 25,
|
|
"timeout": 300,
|
|
"retry_attempts": 3,
|
|
"log_level": os.getenv("LOG_LEVEL", "INFO"),
|
|
"debug_mode": os.getenv("DEBUG", "false").lower() == "true"
|
|
}
|
|
}
|
|
|
|
def _validate_config(self):
|
|
"""Validate configuration and warn about missing required settings"""
|
|
warnings = []
|
|
|
|
|
|
for provider, config in self._config["llm_providers"].items():
|
|
if provider == "openai_compatible":
|
|
|
|
if not config.get("base_url"):
|
|
warnings.append(f"Missing base_url for {provider}")
|
|
else:
|
|
|
|
if not config["api_key"]:
|
|
warnings.append(f"Missing API key for {provider}")
|
|
|
|
|
|
has_provider = False
|
|
for provider, config in self._config["llm_providers"].items():
|
|
if provider == "openai_compatible":
|
|
if config.get("base_url"):
|
|
has_provider = True
|
|
break
|
|
else:
|
|
if config["api_key"]:
|
|
has_provider = True
|
|
break
|
|
|
|
if not has_provider:
|
|
|
|
print("⚠️ Warning: No LLM providers configured. Please configure at least one provider in the UI.")
|
|
|
|
def get_llm_config(self, provider: LLMProvider) -> LLMProviderConfig:
|
|
"""Get configuration for a specific LLM provider"""
|
|
|
|
self._config = self._load_default_config()
|
|
|
|
if provider not in self._config["llm_providers"]:
|
|
raise ValueError(f"Unknown LLM provider: {provider}")
|
|
|
|
config = self._config["llm_providers"][provider]
|
|
return LLMProviderConfig(**config)
|
|
|
|
def get_available_llm_providers(self) -> List[LLMProvider]:
|
|
"""Get list of available LLM providers with API keys"""
|
|
|
|
self._config = self._load_default_config()
|
|
|
|
available = []
|
|
for provider, config in self._config["llm_providers"].items():
|
|
if provider == "openai_compatible":
|
|
|
|
if config.get("base_url"):
|
|
available.append(provider)
|
|
else:
|
|
|
|
if config["api_key"]:
|
|
available.append(provider)
|
|
return available
|
|
|
|
def get_default_llm_provider(self) -> LLMProvider:
|
|
"""Get the default LLM provider, falling back to first available if not configured"""
|
|
|
|
self._config = self._load_default_config()
|
|
|
|
default_provider = self._config["system"]["default_llm_provider"]
|
|
available_providers = self.get_available_llm_providers()
|
|
|
|
|
|
if default_provider in available_providers:
|
|
return default_provider
|
|
|
|
|
|
if available_providers:
|
|
print(f"⚠️ Default provider '{default_provider}' not configured, using '{available_providers[0]}'")
|
|
return available_providers[0]
|
|
|
|
|
|
print("⚠️ Warning: No LLM providers are configured. Returning 'google' as fallback.")
|
|
return "google"
|
|
|
|
def get_image_generation_config(self) -> Dict[str, Any]:
|
|
"""Get image generation configuration"""
|
|
return self._config["image_generation"]
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get a configuration value using dot notation"""
|
|
keys = key.split(".")
|
|
value = self._config
|
|
|
|
try:
|
|
for k in keys:
|
|
value = value[k]
|
|
return value
|
|
except (KeyError, TypeError):
|
|
return default
|
|
|
|
def set(self, key: str, value: Any):
|
|
"""Set a configuration value using dot notation"""
|
|
keys = key.split(".")
|
|
config = self._config
|
|
|
|
for k in keys[:-1]:
|
|
if k not in config:
|
|
config[k] = {}
|
|
config = config[k]
|
|
|
|
config[keys[-1]] = value
|
|
|
|
def update_llm_provider(self, provider: LLMProvider, **kwargs):
|
|
"""Update LLM provider configuration"""
|
|
if provider not in self._config["llm_providers"]:
|
|
raise ValueError(f"Unknown LLM provider: {provider}")
|
|
|
|
self._config["llm_providers"][provider].update(kwargs)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert configuration to dictionary"""
|
|
return self._config.copy()
|
|
|
|
def save_to_file(self, filepath: str):
|
|
"""Save configuration to JSON file"""
|
|
with open(filepath, 'w') as f:
|
|
json.dump(self._config, f, indent=2)
|
|
|
|
@classmethod
|
|
def load_from_file(cls, filepath: str) -> 'Config':
|
|
"""Load configuration from JSON file"""
|
|
instance = cls()
|
|
|
|
if os.path.exists(filepath):
|
|
with open(filepath, 'r') as f:
|
|
file_config = json.load(f)
|
|
instance._config.update(file_config)
|
|
|
|
instance._validate_config()
|
|
return instance
|
|
|
|
|
|
config = Config() |