import os import sys import traceback from typing import Optional from pydantic_settings import BaseSettings from pydantic import field_validator, ConfigDict from loguru import logger from watchfiles import watch, Change from threading import Thread, Event from langfuse import Langfuse from langfuse.langchain import CallbackHandler import yaml from pathlib import Path class LangfuseConfig(BaseSettings): enabled: bool = True public_key: Optional[str] = None secret_key: Optional[str] = None host: Optional[str] = None @field_validator('host') def validate_host(cls, v): if v and not v.startswith(('http://', 'https://')): raise ValueError("Langfuse host must start with http:// or https://") return v def __init__(self, **data): try: logger.info("Initializing LangfuseConfig with data: {}", data) logger.info("Environment variables:") logger.info("LANGFUSE_PUBLIC_KEY: {}", os.getenv('LANGFUSE_PUBLIC_KEY')) logger.info("LANGFUSE_SECRET_KEY: {}", os.getenv('LANGFUSE_SECRET_KEY')) logger.info("LANGFUSE_HOST: {}", os.getenv('LANGFUSE_HOST')) super().__init__(**data) logger.info("LangfuseConfig initialized successfully") logger.info("Public Key: {}", self.public_key) logger.info("Secret Key: {}", self.secret_key) logger.info("Host: {}", self.host) except Exception as e: logger.error("Failed to initialize LangfuseConfig: {}", e) logger.error("Current environment variables:") logger.error("LANGFUSE_PUBLIC_KEY: {}", os.getenv('LANGFUSE_PUBLIC_KEY')) logger.error("LANGFUSE_SECRET_KEY: {}", os.getenv('LANGFUSE_SECRET_KEY')) logger.error("LANGFUSE_HOST: {}", os.getenv('LANGFUSE_HOST')) raise model_config = ConfigDict( env_prefix='LANGFUSE_', env_file='.env', env_file_encoding='utf-8', extra='ignore', env_nested_delimiter='__', case_sensitive=True ) class LogConfig(BaseSettings): level: str = 'INFO' model_config = ConfigDict( env_prefix='LOG_', extra='ignore' ) class LLMConfig(BaseSettings): mode: str = 'ollama' # OpenAI settings openai_api_key: Optional[str] = None openai_api_base_url: Optional[str] = None openai_model: Optional[str] = None # Ollama settings ollama_base_url: Optional[str] = None ollama_model: Optional[str] = None @field_validator('mode') def validate_mode(cls, v): if v not in ['openai', 'ollama']: raise ValueError("LLM mode must be either 'openai' or 'ollama'") return v model_config = ConfigDict( env_prefix='LLM_', env_file='.env', env_file_encoding='utf-8', extra='ignore' ) class ApiConfig(BaseSettings): api_key: Optional[str] = None model_config = ConfigDict( env_prefix='API_', env_file='.env', env_file_encoding='utf-8', extra='ignore' ) class Settings: logging_ready = Event() # Event to signal logging is configured def __init__(self): try: # logger.debug(f"Config initialization started from: {''.join(traceback.format_stack())}") logger.info("Loading configuration from application.yml and environment variables") # Load configuration from YAML file yaml_config = self._load_yaml_config() logger.info("Loaded YAML config: {}", yaml_config) # Initialize configurations, allowing environment variables to override YAML logger.info("Initializing LogConfig") self.log = LogConfig(**yaml_config.get('log', {})) logger.info("LogConfig initialized: {}", self.log.model_dump()) logger.info("Initializing LLMConfig") self.llm = LLMConfig(**yaml_config.get('llm', {})) logger.info("LLMConfig initialized: {}", self.llm.model_dump()) logger.info("Initializing LangfuseConfig") self.langfuse = LangfuseConfig(**yaml_config.get('langfuse', {})) logger.info("LangfuseConfig initialized: {}", self.langfuse.model_dump()) logger.info("Initializing ApiConfig") self.api = ApiConfig(**yaml_config.get('api', {})) logger.info("ApiConfig initialized: {}", self.api.model_dump()) # Add thread_pool_max_workers self.thread_pool_max_workers = yaml_config.get('application', {}).get('thread_pool_max_workers', 5) logger.info("ThreadPool max workers set to: {}", self.thread_pool_max_workers) logger.debug(f"Thread pool initialized with {self.thread_pool_max_workers} workers") logger.info("Validating configuration") self._validate() logger.info("Starting config watcher") self._start_watcher() logger.info("Initializing Langfuse") self._init_langfuse() logger.info("Configuration initialized successfully") except Exception as e: logger.error("Configuration initialization failed: {}", e) logger.error("Current configuration state:") logger.error("LogConfig: {}", self.log.model_dump() if hasattr(self, 'log') else 'Not initialized') logger.error("LLMConfig: {}", self.llm.model_dump() if hasattr(self, 'llm') else 'Not initialized') logger.error("LangfuseConfig: {}", self.langfuse.model_dump() if hasattr(self, 'langfuse') else 'Not initialized') raise def _load_yaml_config(self): config_path = Path('/root/development/jira-webhook-llm/config/application.yml') if not config_path.exists(): logger.warning("Configuration file not found at {}", config_path) return {} try: with open(config_path, 'r') as f: return yaml.safe_load(f) or {} except Exception as e: logger.error("Error loading configuration from {}: {}", config_path, e) return {} def _validate(self): logger.info("LLM mode set to: '{}'", self.llm.mode) if self.llm.mode == 'openai': if not self.llm.openai_api_key: raise ValueError("LLM mode is 'openai', but OPENAI_API_KEY is not set.") if not self.llm.openai_api_base_url: raise ValueError("LLM mode is 'openai', but OPENAI_API_BASE_URL is not set.") if not self.llm.openai_model: raise ValueError("LLM mode is 'openai', but OPENAI_MODEL is not set.") elif self.llm.mode == 'ollama': if not self.llm.ollama_base_url: raise ValueError("LLM mode is 'ollama', but OLLAMA_BASE_URL is not set.") if not self.llm.ollama_model: raise ValueError("LLM mode is 'ollama', but OLLAMA_MODEL is not set.") logger.info("Configuration validated successfully.") def _init_langfuse(self): if self.langfuse.enabled: try: # Verify all required credentials are present if not all([self.langfuse.public_key, self.langfuse.secret_key, self.langfuse.host]): raise ValueError("Missing required Langfuse credentials") logger.debug("Initializing Langfuse client with:") logger.debug("Public Key: {}", self.langfuse.public_key) logger.debug("Secret Key: {}", self.langfuse.secret_key) logger.debug("Host: {}", self.langfuse.host) # Initialize Langfuse client self.langfuse_client = Langfuse( public_key=self.langfuse.public_key, secret_key=self.langfuse.secret_key, host=self.langfuse.host ) # Test Langfuse connection try: self.langfuse_client.auth_check() logger.debug("Langfuse connection test successful") except Exception as e: logger.error("Langfuse connection test failed: {}", e) raise # Initialize CallbackHandler with debug logging logger.debug("Langfuse client attributes: {}", vars(self.langfuse_client)) try: self.langfuse_handler = CallbackHandler() logger.debug("CallbackHandler initialized successfully") except Exception as e: logger.error("CallbackHandler initialization failed: {}", e) raise logger.info("Langfuse client and handler initialized successfully") except ValueError as e: logger.warning("Langfuse configuration error: {}. Disabling Langfuse.", e) self.langfuse.enabled = False except Exception as e: logger.error("Failed to initialize Langfuse: {}", e) self.langfuse.enabled = False def _start_watcher(self): def watch_config(): # Wait for logging to be fully configured self.logging_ready.wait() for changes in watch('config/application.yml'): for change in changes: if change[0] == Change.modified: logger.info("Configuration file modified, reloading settings...") try: # Reload YAML config and re-initialize all settings yaml_config = self._load_yaml_config() self.log = LogConfig(**yaml_config.get('log', {})) self.llm = LLMConfig(**yaml_config.get('llm', {})) self.langfuse = LangfuseConfig(**yaml_config.get('langfuse', {})) self._validate() self._init_langfuse() # Re-initialize Langfuse client if needed logger.info("Configuration reloaded successfully") except Exception as e: logger.error("Error reloading configuration: {}", e) Thread(target=watch_config, daemon=True).start() # Create a single, validated instance of the settings to be imported by other modules. try: settings = Settings() except ValueError as e: logger.error("FATAL: {}", e) logger.error("Application shutting down due to configuration error.") sys.exit(1)