jira-webhook-llm/config.py
Ireneusz Bachanowicz 935a8a49ae
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
Almost stable tests
2025-07-17 02:21:56 +02:00

250 lines
11 KiB
Python

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)