250 lines
10 KiB
Python
250 lines
10 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('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) |