Skip to content

Commit 7bf6a0f

Browse files
committed
use a frozen dataclass object as a cannonical source of truth rather than mutable environment variables
1 parent 752b279 commit 7bf6a0f

File tree

6 files changed

+93
-52
lines changed

6 files changed

+93
-52
lines changed

backend/scripts/generate_conversation/chat.py

100644100755
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
#!/usr/bin/env -S uv run --script
12
# /// script
2-
# requires-python = "~=3.11"
3+
# requires-python = "~=3.12"
34
# dependencies = [
5+
# "dotenv",
46
# "openai",
57
# "pandas",
68
# ]
@@ -12,6 +14,7 @@
1214
from pathlib import Path
1315
import pandas as pd
1416
from typing import Self
17+
# import shared
1518

1619

1720
BOT_INSTRUCTIONS = """Pretend you're a legal expert who giving advice about eviction notices in Oregon.

backend/scripts/simple_eval.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
import os
55
from openai import OpenAI
66

7-
from tenantfirstaid.shared import DEFAULT_INSTRUCTIONS
7+
from tenantfirstaid.shared import CONFIG, DEFAULT_INSTRUCTIONS
88

9-
API_KEY = os.getenv("OPENAI_API_KEY", os.getenv("GITHUB_API_KEY"))
10-
BASE_URL = os.getenv("MODEL_ENDPOINT", "https://api.openai.com/v1")
11-
MODEL = os.getenv("MODEL_NAME", "o3")
12-
MODEL_REASONING_EFFORT = os.getenv("MODEL_REASONING_EFFORT", "medium")
9+
# API_KEY = os.getenv("OPENAI_API_KEY", os.getenv("GITHUB_API_KEY"))
10+
# BASE_URL = os.getenv("MODEL_ENDPOINT", "https://api.openai.com/v1")
11+
# MODEL = os.getenv("MODEL_NAME", "o3")
12+
# MODEL_REASONING_EFFORT = os.getenv("MODEL_REASONING_EFFORT", "medium")
1313

1414
client = OpenAI(
15-
api_key=API_KEY,
16-
base_url=BASE_URL,
15+
api_key=CONFIG.openai_api_key or CONFIG.github_api_key,
16+
base_url=CONFIG.model_endpoint,
1717
)
1818

1919

@@ -47,10 +47,10 @@
4747

4848
# Use the Responses API with streaming
4949
response = client.responses.create(
50-
model=MODEL,
50+
model=CONFIG.model_name,
5151
input=input_messages,
5252
instructions=DEFAULT_INSTRUCTIONS,
53-
reasoning={"effort": MODEL_REASONING_EFFORT},
53+
reasoning={"effort": CONFIG.model_reasoning_effort},
5454
tools=openai_tools,
5555
)
5656

@@ -115,7 +115,7 @@
115115

116116
# 4. Print summary
117117
print("\n===== EVALUATION SUMMARY =====")
118-
print(f"Model evaluated: {MODEL}")
118+
print(f"Model evaluated: {CONFIG.model_name}")
119119
print(f"Number of samples: {len(samples)}")
120120
print(f"Average score: {average_score:.2f}/10")
121121
print(f"Average response time: {average_time:.2f} seconds")
@@ -129,8 +129,8 @@
129129
with open(results_path, "w") as f:
130130
json.dump(
131131
{
132-
"model": MODEL,
133-
"reasoning_effort": MODEL_REASONING_EFFORT,
132+
"model": CONFIG.model_name,
133+
"reasoning_effort": CONFIG.model_reasoning_effort,
134134
"average_score": average_score,
135135
"samples": results,
136136
},

backend/tenantfirstaid/app.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
from pathlib import Path
2-
from flask import Flask, jsonify, session
1+
from flask import Flask, jsonify, session as flask_session
32
import os
43
import secrets
54

6-
7-
if Path(".env").exists():
8-
from dotenv import load_dotenv
9-
10-
load_dotenv(override=True)
11-
125
from .chat import ChatView
136

147
from .session import TenantSession
@@ -27,15 +20,15 @@
2720

2821
@app.get("/api/history")
2922
def history():
30-
session_id = session.get("session_id")
23+
session_id = flask_session.get("session_id")
3124
if not session_id:
3225
return jsonify([])
3326
return jsonify(tenant_session.get(session_id))
3427

3528

3629
@app.post("/api/clear-session")
3730
def clear_session():
38-
session.clear()
31+
flask_session.clear()
3932
return jsonify({"success": True})
4033

4134

backend/tenantfirstaid/chat.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,21 @@
77
from flask.views import View
88
import os
99

10-
from .shared import DEFAULT_INSTRUCTIONS, DATA_DIR
11-
12-
DATA_FILE = DATA_DIR / "chatlog.jsonl"
13-
14-
API_KEY = os.getenv("OPENAI_API_KEY", os.getenv("GITHUB_API_KEY"))
15-
BASE_URL = os.getenv("MODEL_ENDPOINT", "https://api.openai.com/v1")
16-
MODEL = os.getenv("MODEL_NAME", "o3")
17-
MODEL_REASONING_EFFORT = os.getenv("MODEL_REASONING_EFFORT", "medium")
10+
from .shared import DEFAULT_INSTRUCTIONS, DATA_DIR, CONFIG
1811

1912

2013
class ChatView(View):
2114
DATA_FILE = DATA_DIR / "chatlog.jsonl"
2215

2316
client = OpenAI(
24-
api_key=API_KEY,
25-
base_url=BASE_URL,
17+
api_key=CONFIG.openai_api_key or CONFIG.github_api_key,
18+
base_url=CONFIG.model_endpoint,
2619
)
2720

2821
def __init__(self, session):
2922
self.session = session
3023

31-
VECTOR_STORE_ID = os.getenv("VECTOR_STORE_ID")
24+
VECTOR_STORE_ID = CONFIG.vector_store_id
3225
NUM_FILE_SEARCH_RESULTS = os.getenv("NUM_FILE_SEARCH_RESULTS", 10)
3326

3427
self.openai_tools = []
@@ -79,10 +72,10 @@ def generate():
7972
try:
8073
# Use the new Responses API with streaming
8174
response_stream = self.client.responses.create(
82-
model=MODEL,
75+
model=CONFIG.model_name,
8376
input=input_messages,
8477
instructions=DEFAULT_INSTRUCTIONS,
85-
reasoning={"effort": MODEL_REASONING_EFFORT},
78+
reasoning={"effort": CONFIG.model_reasoning_effort},
8679
stream=True,
8780
tools=self.openai_tools,
8881
)

backend/tenantfirstaid/session.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
import os
21
from valkey import Valkey
32
import simplejson as json
3+
from .shared import CONFIG
4+
from ipaddress import IPv4Address
45

56

67
class TenantSession:
78
def __init__(self):
9+
_valkey_args = {
10+
"host": IPv4Address(CONFIG.db_host),
11+
"port": CONFIG.db_port,
12+
"password": CONFIG.db_password,
13+
"ssl": CONFIG.db_use_ssl,
14+
}
15+
816
print(
9-
"Connecting to Valkey:",
10-
{
11-
"host": os.getenv("DB_HOST"),
12-
"port": os.getenv("DB_PORT"),
13-
"ssl": os.getenv("DB_USE_SSL"),
14-
},
17+
f"Connecting to Valkey: {_valkey_args}",
1518
)
1619
try:
17-
self.db_con = Valkey(
18-
host=os.getenv("DB_HOST", "127.0.0.1"),
19-
port=os.getenv("DB_PORT", 6379),
20-
password=os.getenv("DB_PASSWORD"),
21-
ssl=False if os.getenv("DB_USE_SSL") == "false" else True,
22-
)
20+
self.db_con = Valkey(**_valkey_args)
2321
self.db_con.ping()
2422

2523
except Exception as e:

backend/tenantfirstaid/shared.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,70 @@
11
from collections import defaultdict
22
import os
33
from pathlib import Path
4+
from warnings import warn
5+
from dataclasses import dataclass, field
6+
from typing import Optional
47

58
CACHE = defaultdict(list)
69

7-
# Create a dedicated directory for persistent data in root's home directory
8-
if Path(".env").exists():
10+
11+
# configuration and secrets are layered in a dataclass. From lowest to highest priority:
12+
# 1. Dataclass defaults
13+
# 2. Environment variables -- typically used by the Docker container
14+
# 3. .env file in the backend directory (if it exists) -- typically used in local development
15+
# TODO: generate/update .env.example from this dataclass
16+
@dataclass(frozen=True)
17+
class Config:
18+
"""Configuration for the Oregon Tenant First Aid application."""
19+
20+
model_name: str = field(default="o3")
21+
model_reasoning_effort: str = field(default="medium")
22+
vector_store_id: Optional[str] = field(default=None)
23+
feedback_password: Optional[str] = field(default=None)
24+
github_api_key: Optional[str] = field(default=None)
25+
openai_api_key: Optional[str] = field(default=None)
26+
model_endpoint: str = field(default="https://api.openai.com/v1")
27+
use_short_prompts: bool = field(default=True)
28+
db_host: str = field(default="127.0.0.1")
29+
db_port: int = field(default=6379)
30+
db_use_ssl: bool = field(default=True)
31+
db_username: Optional[str] = field(default=None)
32+
db_password: Optional[str] = field(default=None)
33+
34+
35+
# For development purposes, we expect the .env file to be in the backend directory
36+
__shared_py_path = Path(__file__).resolve()
37+
__backend_path = __shared_py_path.parent.parent
38+
__dotenv_path = __backend_path / ".env"
39+
40+
if Path(__dotenv_path).exists():
941
from dotenv import load_dotenv
1042

11-
load_dotenv(override=True)
43+
print(f"Loading environment variables from {__dotenv_path}")
44+
load_dotenv(dotenv_path=__dotenv_path, override=True)
45+
else:
46+
warn(
47+
f"No .env file found at {__dotenv_path.parent}. Using environment variables from the system."
48+
)
49+
50+
# Load environment variables into the Config dataclass
51+
CONFIG = Config(
52+
**{
53+
field.lower(): val
54+
for field, val in os.environ.items()
55+
if field.lower() in Config.__dataclass_fields__
56+
}
57+
)
1258

59+
# Create a dedicated directory for persistent data relative to the backend
60+
# directory with a fallback to `/root/tenantfirstaid_data`
1361
DATA_DIR = Path(os.getenv("PERSISTENT_STORAGE_DIR", "/root/tenantfirstaid_data"))
62+
if not DATA_DIR.is_absolute():
63+
new_data_dir = (__backend_path / DATA_DIR).resolve()
64+
warn(
65+
f"DATA_DIR {DATA_DIR} is not an absolute path. It will be relative to the backend directory ({new_data_dir})."
66+
)
67+
DATA_DIR = new_data_dir
1468
DATA_DIR.mkdir(exist_ok=True)
1569

1670

0 commit comments

Comments
 (0)