Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move env vars to json file #274

Merged
merged 36 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1fb9b31
move env vars to json file
grischperl Nov 26, 2024
f379462
Merge remote-tracking branch 'upstream/main' into 197-model-config
grischperl Nov 27, 2024
755ca02
fix unit test
grischperl Nov 27, 2024
d38595c
fix unit-test
grischperl Nov 27, 2024
3794500
apply codefixes
grischperl Nov 28, 2024
0642d8d
check for github action
grischperl Nov 29, 2024
502aad1
add logging for debugging
grischperl Nov 29, 2024
c17ac96
reformat if block
grischperl Nov 29, 2024
8ddbecf
test
grischperl Nov 29, 2024
74e6bc8
undo github action check
grischperl Nov 29, 2024
e126862
test
grischperl Nov 29, 2024
a1b19de
change yaml to json
grischperl Nov 29, 2024
e25a86c
use new json formated secret
grischperl Nov 29, 2024
86496dc
lint-fix
grischperl Nov 29, 2024
b384c97
Merge remote-tracking branch 'upstream/main' into 197-model-config
grischperl Nov 29, 2024
2c39d0f
Merge remote-tracking branch 'upstream/main' into 197-model-config
grischperl Dec 4, 2024
3cb3e70
review: improve error handling
grischperl Dec 4, 2024
5059058
review: raise error
grischperl Dec 4, 2024
1e24196
Merge remote-tracking branch 'upstream/main' into 197-model-config
grischperl Dec 6, 2024
5788cce
use path relativ to `settings.py` for `config.json`
grischperl Dec 6, 2024
f345d57
use `models-config` as json file name
grischperl Dec 6, 2024
821c322
review: add configuration parameters to unit test
grischperl Dec 6, 2024
2c97566
review: raise error and unit test
grischperl Dec 9, 2024
d1b0ac6
refactor unit test without temp file
grischperl Dec 9, 2024
a428cbd
test
grischperl Dec 9, 2024
9a8cdb8
undo comment and undo raised error
grischperl Dec 9, 2024
a290e61
raise FileNotFound error
grischperl Dec 9, 2024
ec0e16e
remove FileNotFoundError
grischperl Dec 9, 2024
cc517be
improve logging msg
grischperl Dec 10, 2024
a389000
format-fix
grischperl Dec 10, 2024
2e1a84a
test
grischperl Dec 10, 2024
2a86338
check for kubernetis environment
grischperl Dec 11, 2024
1a674c8
format-fix
grischperl Dec 11, 2024
0e455ca
refactor
grischperl Dec 11, 2024
24f6a23
Empty-Commit
grischperl Dec 11, 2024
0f604a9
document the changes
grischperl Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/unit-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@ jobs:
- name: Install dependencies
run: poetry install --with dev

- name: Create config.json
run: |
mkdir -p config
echo '{"mock-key": "mock-value"}' > config/config.json

- name: Run tests
run: poetry run poe test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ ENV/
env.bak/
venv.bak/

# Configuration
config/config.json

# Spyder project settings
.spyderproject
.spyproject
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ Because the companion uses the FastAPI framework, read the following documentati

### Configuration

For local development, LLMs can be configured inside the `config/models.yml` file.
For local development, you can configure LLMs by modifying the `config/config.json` file.
To use a configuration file from a different location, set the `CONFIG_PATH` environment variable to the path of your desired JSON configuration file.

## Code Checks

Expand Down
9 changes: 0 additions & 9 deletions config/config.yml
Original file line number Diff line number Diff line change
@@ -1,9 +0,0 @@
models:
- name: gpt-4o
deployment_id: d92ed4e1d4bafffc
temperature: 0
- name: gpt-4o-mini
deployment_id: d3da334def2bb9c8
temperature: 0
- name: text-embedding-3-large
deployment_id: d6a35fa3fd8ca8b2
10 changes: 5 additions & 5 deletions scripts/k8s/companion-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,21 @@ spec:
- name: REDIS_PORT
value: "6379"
- name: CONFIG_PATH
value: "/mnt/config/models-config.yml"
value: "/mnt/config/models-config.json"
envFrom:
- configMapRef:
name: ai-backend-config
volumeMounts:
- name: models-config
mountPath: /mnt/config/models-config.yml
subPath: models-config.yml
mountPath: /mnt/config/models-config.json
subPath: models-config.json
volumes:
- name: models-config
configMap:
name: ai-backend-config
items:
- key: models-config.yml
path: models-config.yml
- key: models-config.json
path: models-config.json

---
apiVersion: v1
Expand Down
25 changes: 17 additions & 8 deletions src/utils/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import os
from pathlib import Path

import yaml
from pydantic import BaseModel

from utils.logging import get_logger
Expand Down Expand Up @@ -48,20 +48,29 @@ def find_config_file(start_path: Path, target: str) -> Path:

def get_config() -> Config:
"""
Get the configuration of the application by automatically locating the config file.
Get the model configuration of the application by automatically locating the config file.

Returns:
Config: The configuration of the application
Config: The model configuration of the application
"""
# Get the absolute path of the current file
current_file_path = Path(__file__).resolve()

target_config_file = os.environ.get("CONFIG_PATH", "config/config.yml")
target_config_file = os.environ.get("CONFIG_PATH", "config/config.json")
# Find the config file by searching upwards
config_file = find_config_file(current_file_path.parent, target_config_file)

logger.info(f"Loading models config from: {config_file}")
with config_file.open() as f:
data = yaml.safe_load(f)
config = Config(**data)
return config
try:
with config_file.open() as file:
data = json.load(file)
# Extract only the "models" part of the configuration
models_data = data.get("models", [])
config = Config(models=models_data)
return config
grischperl marked this conversation as resolved.
Show resolved Hide resolved
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON format in config file {config_file}: {e}")
raise
except Exception as e:
logger.error(f"Error loading config from {config_file}: {e}")
raise
51 changes: 45 additions & 6 deletions src/utils/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import json
import logging
import os
import sys
from pathlib import Path

from decouple import Config, RepositoryEnv, config
from dotenv import find_dotenv, load_dotenv
from dotenv import find_dotenv


def is_running_pytest() -> bool:
Expand All @@ -13,23 +15,60 @@ def is_running_pytest() -> bool:
return "pytest" in sys.modules


project_root = os.path.dirname(os.path.abspath(__file__))
def is_running_kubernetes() -> bool:
"""Check if the code is running in a Kubernetes environment.
This is needed to identify if the code is running in a Kubernetes environment.
"""
return "KUBERNETES_SERVICE_HOST" in os.environ


def load_env_from_json() -> None:
"""Load the configuration from the config.json file."""
default_config_path = Path(__file__).parent.parent.parent / "config" / "config.json"

config_path = Path(os.getenv("CONFIG_PATH", default_config_path))

try:
# Load the configuration from the given path and set the environment variables.
with config_path.open() as file:
config_file = json.load(file)

# Set environment variables for all keys except "models"
for key, value in config_file.items():
if key != "models": # Skip models
os.environ[key] = str(value)
except json.JSONDecodeError as e:
logging.error(f"Invalid JSON format in config file {config_path}: {e}")
raise
except FileNotFoundError:
logging.error(
f"Config file not found at {config_path}. Place the config file at the default location:"
f"{default_config_path} or set the CONFIG_PATH environment variable."
)
raise
except Exception as e:
logging.error(f"Error loading config from {config_path}: {e}")
raise


if is_running_pytest():
# Use .test.env for tests
# For tests use .env.test if available
env_path = find_dotenv(".env.test")
if env_path and os.path.exists(env_path):
repository = RepositoryEnv(env_path)
for key, value in repository.data.items():
os.environ[key] = str(value)
config = Config(repository)
grischperl marked this conversation as resolved.
Show resolved Hide resolved
else:
logging.warning("No .test.env file found. Using .env file.")
load_dotenv()
# Load the config.json if no .env.test file is found
logging.warning("No .test.env file found. Using config.json.")
load_env_from_json()

# deepeval specific environment variables
DEEPEVAL_TESTCASE_VERBOSE = config("DEEPEVAL_TESTCASE_VERBOSE", default="False")
else:
load_dotenv()
# For production load the env variables needed dynamically from the config.json.
load_env_from_json()


LOG_LEVEL = config("LOG_LEVEL", default="INFO")
Expand Down
57 changes: 42 additions & 15 deletions tests/unit/utils/test_config.py
grischperl marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@


@pytest.mark.parametrize(
"yaml_content, expected_config",
"json_content, expected_config",
[
(
# Given: multiple models, two variables
# Expected: Config object with two models, no variables
"""
models:
- name: model1
deployment_id: dep1
temperature: 0.0
- name: model2
deployment_id: dep2
temperature: 0.5
{
"VARIABLE_NAME": "value",
"VARIABLE_NAME2": "value2",
"models": [
{
"name": "model1",
"deployment_id": "dep1",
"temperature": 0.0
},
{
"name": "model2",
"deployment_id": "dep2",
"temperature": 0.5
}
]
}
""",
Config(
models=[
grischperl marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -27,11 +38,19 @@
),
),
(
# Given: single model, one variable
# Expected: Config object with one model, no variable
"""
models:
- name: single_model
deployment_id: single_dep
temperature: 1
{
"VARIABLE_NAME": "value",
"models": [
{
"name": "single_model",
"deployment_id": "single_dep",
"temperature": 1
}
]
}
""",
Config(
grischperl marked this conversation as resolved.
Show resolved Hide resolved
models=[
Expand All @@ -42,14 +61,22 @@
),
),
(
# Given: no models, one variable
# Expected: Config object with no models, no variables
"""
models: []
{
"VARIABLE_NAME": "value",
"models": []
}
""",
Config(models=[]),
),
],
)
def test_get_config(yaml_content, expected_config):
with patch.object(Path, "open", mock_open(read_data=yaml_content)):
def test_get_config(json_content, expected_config):
# Mock `Path.is_file` to always return True for the config file
with patch.object(Path, "open", mock_open(read_data=json_content)), patch.object(
Path, "is_file", return_value=True
):
result = get_config()
assert result == expected_config
74 changes: 74 additions & 0 deletions tests/unit/utils/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import os
from json import JSONDecodeError
from pathlib import Path
from unittest.mock import mock_open, patch

import pytest

from utils.settings import load_env_from_json


@pytest.mark.parametrize(
"json_content, expected_env_variables",
[
(
# Given: Malformed Json
# Expected: Exception
"""
{ "VARIABLE_NAME": "value",
""",
None,
),
(
# Given: Valid JSON with two variables
# Expected: Environment variables are set correctly
"""
{
"VARIABLE_NAME": "value",
"VARIABLE_NAME2": "value2"
}
""",
{"VARIABLE_NAME": "value", "VARIABLE_NAME2": "value2"},
),
(
# Given: Valid JSON with two variables and a model configuration
# Expected: Environment variables are set correctly
"""
{
"VARIABLE_NAME": "value",
"VARIABLE_NAME2": "value2",
"models": [
{
"name": "single_model",
"deployment_id": "single_dep",
"temperature": 1
}
]
}
""",
{"VARIABLE_NAME": "value", "VARIABLE_NAME2": "value2"},
),
],
)
def test_load_env_from_json(json_content, expected_env_variables):
with patch.dict(os.environ, {"CONFIG_PATH": "/mocked/config.json"}), patch(
"os.path.exists", return_value=True
), patch.object(Path, "open", mock_open(read_data=json_content)), patch.object(
Path, "is_file", return_value=bool(json_content)
):

if expected_env_variables is None:
# Then: Expect an exception for malformed JSON
with pytest.raises(JSONDecodeError):
load_env_from_json()
else:
# When: loading the environment variables from the config.json file
load_env_from_json()

# Then: the environment variables are set as expected
for key, value in expected_env_variables.items():
assert os.getenv(key) == value

# Clean up the environment variables
for key in expected_env_variables:
os.environ.pop(key)
Loading