diff --git a/ai-hub/app/config.py b/ai-hub/app/config.py index 82ad185..68b2abb 100644 --- a/ai-hub/app/config.py +++ b/ai-hub/app/config.py @@ -5,11 +5,11 @@ load_dotenv() -# --- 1. Define the Configuration Schema (No changes here) --- - +# --- 1. Define the Configuration Schema --- class ApplicationSettings(BaseModel): project_name: str = "Cortex Hub" version: str = "1.0.0" + log_level: str = "INFO" # <-- New field class DatabaseSettings(BaseModel): mode: str = "sqlite" @@ -30,14 +30,12 @@ vector_store: VectorStoreSettings = Field(default_factory=VectorStoreSettings) # --- 2. Create the Final Settings Object --- - class Settings: """ Holds all application settings, validated and structured by Pydantic. Priority Order: Environment Variables > YAML File > Pydantic Defaults """ def __init__(self): - # Load base configuration from YAML config_path = os.getenv("CONFIG_PATH", "config.yaml") yaml_data = {} if os.path.exists(config_path): @@ -47,22 +45,23 @@ else: print(f"⚠️ '{config_path}' not found. Using defaults and environment variables.") - # Parse the loaded data to get defaults for any missing sections config_from_pydantic = AppConfig.parse_obj(yaml_data) - - # Helper to safely get nested values from the raw YAML dict + def get_from_yaml(keys): d = yaml_data for key in keys: d = d.get(key) if isinstance(d, dict) else None return d - # --- Set final attributes with ENV > YAML > Default priority --- self.PROJECT_NAME: str = os.getenv("PROJECT_NAME") or \ get_from_yaml(["application", "project_name"]) or \ config_from_pydantic.application.project_name self.VERSION: str = config_from_pydantic.application.version + self.LOG_LEVEL: str = os.getenv("LOG_LEVEL") or \ + get_from_yaml(["application", "log_level"]) or \ + config_from_pydantic.application.log_level + self.DB_MODE: str = os.getenv("DB_MODE") or \ get_from_yaml(["database", "mode"]) or \ config_from_pydantic.database.mode diff --git a/ai-hub/app/config.py b/ai-hub/app/config.py index 82ad185..68b2abb 100644 --- a/ai-hub/app/config.py +++ b/ai-hub/app/config.py @@ -5,11 +5,11 @@ load_dotenv() -# --- 1. Define the Configuration Schema (No changes here) --- - +# --- 1. Define the Configuration Schema --- class ApplicationSettings(BaseModel): project_name: str = "Cortex Hub" version: str = "1.0.0" + log_level: str = "INFO" # <-- New field class DatabaseSettings(BaseModel): mode: str = "sqlite" @@ -30,14 +30,12 @@ vector_store: VectorStoreSettings = Field(default_factory=VectorStoreSettings) # --- 2. Create the Final Settings Object --- - class Settings: """ Holds all application settings, validated and structured by Pydantic. Priority Order: Environment Variables > YAML File > Pydantic Defaults """ def __init__(self): - # Load base configuration from YAML config_path = os.getenv("CONFIG_PATH", "config.yaml") yaml_data = {} if os.path.exists(config_path): @@ -47,22 +45,23 @@ else: print(f"⚠️ '{config_path}' not found. Using defaults and environment variables.") - # Parse the loaded data to get defaults for any missing sections config_from_pydantic = AppConfig.parse_obj(yaml_data) - - # Helper to safely get nested values from the raw YAML dict + def get_from_yaml(keys): d = yaml_data for key in keys: d = d.get(key) if isinstance(d, dict) else None return d - # --- Set final attributes with ENV > YAML > Default priority --- self.PROJECT_NAME: str = os.getenv("PROJECT_NAME") or \ get_from_yaml(["application", "project_name"]) or \ config_from_pydantic.application.project_name self.VERSION: str = config_from_pydantic.application.version + self.LOG_LEVEL: str = os.getenv("LOG_LEVEL") or \ + get_from_yaml(["application", "log_level"]) or \ + config_from_pydantic.application.log_level + self.DB_MODE: str = os.getenv("DB_MODE") or \ get_from_yaml(["database", "mode"]) or \ config_from_pydantic.database.mode diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py index cd422e3..b22a670 100644 --- a/ai-hub/app/core/llm_providers.py +++ b/ai-hub/app/core/llm_providers.py @@ -6,9 +6,6 @@ from typing import final from app.config import settings # <-- Import the centralized settings -# Configure logging (can be moved to a higher level, like app.py, if preferred) -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - # --- 1. Initialize API Clients from Central Config --- # All environment variable access is now gone from this file. deepseek_client = OpenAI(api_key=settings.DEEPSEEK_API_KEY, base_url="https://api.deepseek.com") diff --git a/ai-hub/app/config.py b/ai-hub/app/config.py index 82ad185..68b2abb 100644 --- a/ai-hub/app/config.py +++ b/ai-hub/app/config.py @@ -5,11 +5,11 @@ load_dotenv() -# --- 1. Define the Configuration Schema (No changes here) --- - +# --- 1. Define the Configuration Schema --- class ApplicationSettings(BaseModel): project_name: str = "Cortex Hub" version: str = "1.0.0" + log_level: str = "INFO" # <-- New field class DatabaseSettings(BaseModel): mode: str = "sqlite" @@ -30,14 +30,12 @@ vector_store: VectorStoreSettings = Field(default_factory=VectorStoreSettings) # --- 2. Create the Final Settings Object --- - class Settings: """ Holds all application settings, validated and structured by Pydantic. Priority Order: Environment Variables > YAML File > Pydantic Defaults """ def __init__(self): - # Load base configuration from YAML config_path = os.getenv("CONFIG_PATH", "config.yaml") yaml_data = {} if os.path.exists(config_path): @@ -47,22 +45,23 @@ else: print(f"⚠️ '{config_path}' not found. Using defaults and environment variables.") - # Parse the loaded data to get defaults for any missing sections config_from_pydantic = AppConfig.parse_obj(yaml_data) - - # Helper to safely get nested values from the raw YAML dict + def get_from_yaml(keys): d = yaml_data for key in keys: d = d.get(key) if isinstance(d, dict) else None return d - # --- Set final attributes with ENV > YAML > Default priority --- self.PROJECT_NAME: str = os.getenv("PROJECT_NAME") or \ get_from_yaml(["application", "project_name"]) or \ config_from_pydantic.application.project_name self.VERSION: str = config_from_pydantic.application.version + self.LOG_LEVEL: str = os.getenv("LOG_LEVEL") or \ + get_from_yaml(["application", "log_level"]) or \ + config_from_pydantic.application.log_level + self.DB_MODE: str = os.getenv("DB_MODE") or \ get_from_yaml(["database", "mode"]) or \ config_from_pydantic.database.mode diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py index cd422e3..b22a670 100644 --- a/ai-hub/app/core/llm_providers.py +++ b/ai-hub/app/core/llm_providers.py @@ -6,9 +6,6 @@ from typing import final from app.config import settings # <-- Import the centralized settings -# Configure logging (can be moved to a higher level, like app.py, if preferred) -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - # --- 1. Initialize API Clients from Central Config --- # All environment variable access is now gone from this file. deepseek_client = OpenAI(api_key=settings.DEEPSEEK_API_KEY, base_url="https://api.deepseek.com") diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py index 880fedb..49cbe2c 100644 --- a/ai-hub/app/core/pipelines/dspy_rag.py +++ b/ai-hub/app/core/pipelines/dspy_rag.py @@ -74,7 +74,8 @@ f"Human: {question}\n" f"Assistant:" ) - + logging.debug(f"[DspyRagPipeline.forward] Full Prompt:\n{full_prompt}") + lm = dspy.settings.lm if lm is None: raise RuntimeError("DSPy LM not configured.") diff --git a/ai-hub/app/config.py b/ai-hub/app/config.py index 82ad185..68b2abb 100644 --- a/ai-hub/app/config.py +++ b/ai-hub/app/config.py @@ -5,11 +5,11 @@ load_dotenv() -# --- 1. Define the Configuration Schema (No changes here) --- - +# --- 1. Define the Configuration Schema --- class ApplicationSettings(BaseModel): project_name: str = "Cortex Hub" version: str = "1.0.0" + log_level: str = "INFO" # <-- New field class DatabaseSettings(BaseModel): mode: str = "sqlite" @@ -30,14 +30,12 @@ vector_store: VectorStoreSettings = Field(default_factory=VectorStoreSettings) # --- 2. Create the Final Settings Object --- - class Settings: """ Holds all application settings, validated and structured by Pydantic. Priority Order: Environment Variables > YAML File > Pydantic Defaults """ def __init__(self): - # Load base configuration from YAML config_path = os.getenv("CONFIG_PATH", "config.yaml") yaml_data = {} if os.path.exists(config_path): @@ -47,22 +45,23 @@ else: print(f"⚠️ '{config_path}' not found. Using defaults and environment variables.") - # Parse the loaded data to get defaults for any missing sections config_from_pydantic = AppConfig.parse_obj(yaml_data) - - # Helper to safely get nested values from the raw YAML dict + def get_from_yaml(keys): d = yaml_data for key in keys: d = d.get(key) if isinstance(d, dict) else None return d - # --- Set final attributes with ENV > YAML > Default priority --- self.PROJECT_NAME: str = os.getenv("PROJECT_NAME") or \ get_from_yaml(["application", "project_name"]) or \ config_from_pydantic.application.project_name self.VERSION: str = config_from_pydantic.application.version + self.LOG_LEVEL: str = os.getenv("LOG_LEVEL") or \ + get_from_yaml(["application", "log_level"]) or \ + config_from_pydantic.application.log_level + self.DB_MODE: str = os.getenv("DB_MODE") or \ get_from_yaml(["database", "mode"]) or \ config_from_pydantic.database.mode diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py index cd422e3..b22a670 100644 --- a/ai-hub/app/core/llm_providers.py +++ b/ai-hub/app/core/llm_providers.py @@ -6,9 +6,6 @@ from typing import final from app.config import settings # <-- Import the centralized settings -# Configure logging (can be moved to a higher level, like app.py, if preferred) -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - # --- 1. Initialize API Clients from Central Config --- # All environment variable access is now gone from this file. deepseek_client = OpenAI(api_key=settings.DEEPSEEK_API_KEY, base_url="https://api.deepseek.com") diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py index 880fedb..49cbe2c 100644 --- a/ai-hub/app/core/pipelines/dspy_rag.py +++ b/ai-hub/app/core/pipelines/dspy_rag.py @@ -74,7 +74,8 @@ f"Human: {question}\n" f"Assistant:" ) - + logging.debug(f"[DspyRagPipeline.forward] Full Prompt:\n{full_prompt}") + lm = dspy.settings.lm if lm is None: raise RuntimeError("DSPy LM not configured.") diff --git a/ai-hub/app/main.py b/ai-hub/app/main.py index cfc25bf..3c2bdd6 100644 --- a/ai-hub/app/main.py +++ b/ai-hub/app/main.py @@ -1,6 +1,10 @@ import uvicorn +import logging from app.app import create_app +# Configure logging (can be moved to a higher level, like app.py, if preferred) +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') + # Use the application factory to create the FastAPI app instance app = create_app() diff --git a/ai-hub/app/config.py b/ai-hub/app/config.py index 82ad185..68b2abb 100644 --- a/ai-hub/app/config.py +++ b/ai-hub/app/config.py @@ -5,11 +5,11 @@ load_dotenv() -# --- 1. Define the Configuration Schema (No changes here) --- - +# --- 1. Define the Configuration Schema --- class ApplicationSettings(BaseModel): project_name: str = "Cortex Hub" version: str = "1.0.0" + log_level: str = "INFO" # <-- New field class DatabaseSettings(BaseModel): mode: str = "sqlite" @@ -30,14 +30,12 @@ vector_store: VectorStoreSettings = Field(default_factory=VectorStoreSettings) # --- 2. Create the Final Settings Object --- - class Settings: """ Holds all application settings, validated and structured by Pydantic. Priority Order: Environment Variables > YAML File > Pydantic Defaults """ def __init__(self): - # Load base configuration from YAML config_path = os.getenv("CONFIG_PATH", "config.yaml") yaml_data = {} if os.path.exists(config_path): @@ -47,22 +45,23 @@ else: print(f"⚠️ '{config_path}' not found. Using defaults and environment variables.") - # Parse the loaded data to get defaults for any missing sections config_from_pydantic = AppConfig.parse_obj(yaml_data) - - # Helper to safely get nested values from the raw YAML dict + def get_from_yaml(keys): d = yaml_data for key in keys: d = d.get(key) if isinstance(d, dict) else None return d - # --- Set final attributes with ENV > YAML > Default priority --- self.PROJECT_NAME: str = os.getenv("PROJECT_NAME") or \ get_from_yaml(["application", "project_name"]) or \ config_from_pydantic.application.project_name self.VERSION: str = config_from_pydantic.application.version + self.LOG_LEVEL: str = os.getenv("LOG_LEVEL") or \ + get_from_yaml(["application", "log_level"]) or \ + config_from_pydantic.application.log_level + self.DB_MODE: str = os.getenv("DB_MODE") or \ get_from_yaml(["database", "mode"]) or \ config_from_pydantic.database.mode diff --git a/ai-hub/app/core/llm_providers.py b/ai-hub/app/core/llm_providers.py index cd422e3..b22a670 100644 --- a/ai-hub/app/core/llm_providers.py +++ b/ai-hub/app/core/llm_providers.py @@ -6,9 +6,6 @@ from typing import final from app.config import settings # <-- Import the centralized settings -# Configure logging (can be moved to a higher level, like app.py, if preferred) -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - # --- 1. Initialize API Clients from Central Config --- # All environment variable access is now gone from this file. deepseek_client = OpenAI(api_key=settings.DEEPSEEK_API_KEY, base_url="https://api.deepseek.com") diff --git a/ai-hub/app/core/pipelines/dspy_rag.py b/ai-hub/app/core/pipelines/dspy_rag.py index 880fedb..49cbe2c 100644 --- a/ai-hub/app/core/pipelines/dspy_rag.py +++ b/ai-hub/app/core/pipelines/dspy_rag.py @@ -74,7 +74,8 @@ f"Human: {question}\n" f"Assistant:" ) - + logging.debug(f"[DspyRagPipeline.forward] Full Prompt:\n{full_prompt}") + lm = dspy.settings.lm if lm is None: raise RuntimeError("DSPy LM not configured.") diff --git a/ai-hub/app/main.py b/ai-hub/app/main.py index cfc25bf..3c2bdd6 100644 --- a/ai-hub/app/main.py +++ b/ai-hub/app/main.py @@ -1,6 +1,10 @@ import uvicorn +import logging from app.app import create_app +# Configure logging (can be moved to a higher level, like app.py, if preferred) +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') + # Use the application factory to create the FastAPI app instance app = create_app() diff --git a/ai-hub/tests/test_config.py b/ai-hub/tests/test_config.py index 135fcc7..2214123 100644 --- a/ai-hub/tests/test_config.py +++ b/ai-hub/tests/test_config.py @@ -2,11 +2,15 @@ import importlib import yaml + @pytest.fixture def tmp_config_file(tmp_path): """Creates a temporary config.yaml file and returns its path.""" config_content = { - "application": {"project_name": "Test Project from YAML"}, + "application": { + "project_name": "Test Project from YAML", + "log_level": "WARNING" + }, "llm_providers": {"deepseek_model_name": "deepseek-from-yaml"} } config_path = tmp_path / "test_config.yaml" @@ -14,51 +18,72 @@ yaml.dump(config_content, f) return str(config_path) + def test_env_var_overrides_yaml(monkeypatch, tmp_config_file): - """ - Tests that a value from an environment variable has priority over the YAML file. - """ - # Arrange: Set BOTH a YAML value and an environment variable + """Tests that an env var overrides YAML for DEEPSEEK_MODEL_NAME.""" monkeypatch.setenv("CONFIG_PATH", tmp_config_file) - # The value from the environment variable monkeypatch.setenv("DEEPSEEK_MODEL_NAME", "deepseek-from-env") - - # Act - # We need to import the module after setting the env variable for the test to work + from app import config importlib.reload(config) - - # Assert: The value from the environment variable should be used + assert config.settings.DEEPSEEK_MODEL_NAME == "deepseek-from-env" + def test_env_var_overrides_default(monkeypatch): - """ - Tests that an environment variable overrides the hardcoded default when - the YAML file is missing or doesn't contain the key. - """ - # Arrange: Point to a non-existent file and set an environment variable + """Tests that env var overrides hardcoded default when YAML is missing.""" monkeypatch.setenv("CONFIG_PATH", "/path/that/does/not/exist.yaml") monkeypatch.setenv("DEEPSEEK_MODEL_NAME", "deepseek-from-env") - # Act from app import config importlib.reload(config) - # Assert: The value from the environment variable should be used assert config.settings.DEEPSEEK_MODEL_NAME == "deepseek-from-env" - + + def test_hardcoded_default_is_used_last(monkeypatch): - """ - Tests that the hardcoded default is used when the YAML is missing and - the environment variable is not set. - """ - # Arrange + """Tests fallback to hardcoded default if ENV and YAML missing.""" monkeypatch.setenv("CONFIG_PATH", "/path/that/does/not/exist.yaml") monkeypatch.delenv("DEEPSEEK_MODEL_NAME", raising=False) - # Act from app import config importlib.reload(config) - # Assert: The hardcoded default value should be used assert config.settings.DEEPSEEK_MODEL_NAME == "deepseek-chat" + + +# -------------------------- +# ✅ LOG_LEVEL TESTS +# -------------------------- + +def test_log_level_env_overrides_yaml(monkeypatch, tmp_config_file): + """Tests LOG_LEVEL: ENV > YAML > default.""" + monkeypatch.setenv("CONFIG_PATH", tmp_config_file) + monkeypatch.setenv("LOG_LEVEL", "DEBUG") + + from app import config + importlib.reload(config) + + assert config.settings.LOG_LEVEL == "DEBUG" + + +def test_log_level_yaml_over_default(monkeypatch, tmp_config_file): + """Tests LOG_LEVEL uses YAML when ENV is not set.""" + monkeypatch.setenv("CONFIG_PATH", tmp_config_file) + monkeypatch.delenv("LOG_LEVEL", raising=False) + + from app import config + importlib.reload(config) + + assert config.settings.LOG_LEVEL == "WARNING" + + +def test_log_level_default_used(monkeypatch): + """Tests LOG_LEVEL falls back to default when neither ENV nor YAML set.""" + monkeypatch.setenv("CONFIG_PATH", "/does/not/exist.yaml") + monkeypatch.delenv("LOG_LEVEL", raising=False) + + from app import config + importlib.reload(config) + + assert config.settings.LOG_LEVEL == "INFO"