Skip to content

Commit 03ebb4e

Browse files
authored
Merge pull request #307 from codeforpdx/apkostka/claude-init
Expand backend test suite and enforce 80% coverage in CI
2 parents 0ca274d + 79ffbcc commit 03ebb4e

15 files changed

Lines changed: 745 additions & 230 deletions

.claude/CLAUDE.md

Lines changed: 59 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,103 @@
1-
Welcome to the Tenant First Aid repository. This file contains the main points for new contributors.
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
24

35
## Repository overview
46

5-
- **Source code**: see [Architecture.md](../Architecture.md) for code organization
6-
- **Tests**: see [Architecture.md](../Architecture.md) for test organization and [README.md](../README.md) for backend quality check flows/commands; see `frontend-builds` job in [pr-checks](../.github/workflows/pr-check.yml) for frontend commands
7-
- **Documentation**: see [Architecture.md](../Architecture.md) for architectural documentation
8-
- **Utilities**: developer commands are defined in the `Makefile`.
9-
- **PR template**: [pull_request_template.md](../.github/pull_request_template.md) describes the information every PR must include.
7+
Tenant First Aid is a chatbot for Oregon housing/eviction legal information. Flask backend + React frontend monorepo, deployed on Digital Ocean.
108

11-
## Local `./backend` workflow
9+
- **Architecture docs**: [Architecture.md](../Architecture.md) — RAG pipeline, endpoints, session management, frontend structure
10+
- **Deployment docs**: [Deployment.md](../Deployment.md) — CI/CD, secrets, infrastructure
11+
- **PR template**: [.github/pull_request_template.md](../.github/pull_request_template.md)
12+
- **Dev commands**: `backend/Makefile`
1213

13-
1. Format, lint and type‑check your changes:
14+
### Key architecture
1415

15-
```bash
16-
make fmt
17-
make lint
18-
make typecheck
19-
```
16+
- **Backend** (`backend/`): Flask API with LangChain agent orchestration. The agent uses Vertex AI RAG to retrieve Oregon housing law documents and Google Gemini as the LLM. Key files: `langchain_chat_manager.py` (agent orchestration), `langchain_tools.py` (RAG retriever + letter tools), `schema.py` (Pydantic response chunk types shared with frontend).
17+
- **Frontend** (`frontend/`): React 19 + TypeScript + Vite + Tailwind CSS 4. Uses `@langchain/core` message types (`HumanMessage`/`AIMessage`) directly for chat state. Streaming via native `ReadableStream`.
18+
- **Type bridge**: Frontend TypeScript types in `src/types/` are auto-generated from backend Pydantic models via `generate-types` and gitignored. Must regenerate before building or type-checking.
2019

21-
2. Run the tests:
20+
## Backend workflow (run from `backend/`)
2221

23-
```bash
24-
make test
25-
```
22+
```bash
23+
make fmt # Format + sort imports (ruff)
24+
make lint # Lint (ruff)
25+
make typecheck # Type-check (ty)
26+
make test # Run tests (pytest)
27+
make --keep-going check # All of the above in one shot
2628

27-
To run a single test, use `uv run pytest -s -k <test_name>`.
29+
# Single test
30+
uv run pytest -s -k <test_name>
2831

29-
3. Coverage can be generated with (optional but recommended for code changes):
30-
```bash
31-
make test TEST_OPTIONS="--cov tenantfirstaid --cov-report html --cov-branch"
32-
```
32+
# LangChain-specific tests
33+
uv run pytest -m langchain
3334

34-
All python commands should be run via `uv run python ...`
35+
# Coverage
36+
make test TEST_OPTIONS="--cov tenantfirstaid --cov-report html --cov-branch"
37+
```
3538

36-
## LangChain Agent Architecture
39+
All python commands should be run via `uv run python ...`
3740

38-
The backend uses LangChain 1.0.8+ for agent-based conversation management with Vertex AI integration.
41+
### Environment variables
3942

40-
### Key Components
41-
- **LangChainChatManager**: Main agent orchestration class (`backend/tenantfirstaid/langchain_chat_manager.py`)
42-
- **retrieve_city_state_laws**: Tool for city/state-specific legal retrieval
43-
- **ChatVertexAI**: LangChain wrapper for Google Gemini
43+
See `backend/.env.example`. Key ones: `MODEL_NAME`, `GOOGLE_APPLICATION_CREDENTIALS`, `VERTEX_AI_DATASTORE`, `LANGSMITH_API_KEY`.
4444

45-
### Environment Variables
46-
```bash
47-
MODEL_NAME=gemini-2.5-pro # LLM model name
48-
VERTEX_AI_DATASTORE=projects/.../datastores/... # RAG corpus ID
49-
SHOW_MODEL_THINKING=false # Enable Gemini thinking mode
50-
LANGSMITH_API_KEY=... # Optional: Enable LLM evaluations
51-
```
45+
### LangSmith evaluations
5246

53-
### Testing LangChain Components
5447
```bash
55-
# Run LangChain-specific tests
56-
uv run pytest -m langchain
57-
58-
# Run with LangSmith tracing (requires API key)
59-
LANGSMITH_TRACING=true LANGCHAIN_TRACING_V2=true uv run pytest -m langchain
60-
61-
# Run evaluations (see docs/EVALUATION.md)
6248
uv run python scripts/run_langsmith_evaluation.py --num-samples 20
6349
```
6450

65-
## Local `./frontend` workflow
51+
See `docs/EVALUATION.md` for details.
6652

67-
Frontend TypeScript types in `src/types/` are auto-generated from the backend Python models and are not checked into source control. You must generate them before building or type-checking:
53+
## Frontend workflow (run from `frontend/`)
6854

6955
```bash
70-
npm run generate-types
71-
# or equivalently:
72-
make generate-types # (run from the backend/ directory)
56+
npm run generate-types # Required before build/typecheck — generates src/types/models.ts
57+
npm run lint # Lint (eslint)
58+
npm run format # Format (prettier)
59+
npm run typecheck # Type-check (tsc)
60+
npm run build # Build (auto-generates types first)
61+
npm run test -- --run # Run tests (vitest)
62+
npm run test -- --run --coverage # With coverage
7363
```
7464

75-
This requires `uv` to be installed (see backend setup).
76-
77-
1. Format, lint and type‑check your changes:
78-
79-
```bash
80-
npm run lint
81-
npx run format
82-
```
83-
84-
2. Build frontend code (automatically generates types first)
85-
```bash
86-
npm run build
87-
```
88-
89-
3. Test frontend code
90-
```bash
91-
npm run test -- --run
92-
```
93-
94-
4. Test Coverage can be generated with (optional but recommended for code changes):
95-
```bash
96-
npm run test -- --run --coverage
97-
```
65+
`generate-types` requires `uv` to be installed (it runs backend Python to emit JSON Schema, piped through `json2ts`).
9866

9967
## Style notes
10068

10169
- Write comments as full sentences and end them with a period.
10270

103-
## Pull request expectations
104-
105-
PRs should use the template located at [pull_request_template.md](../.github/pull_request_template.md). Provide a summary, test plan and issue number if applicable, then check that:
71+
## Commit messages
10672

107-
- for frontend, backend and backend/scripts
108-
- New tests are added when needed.
109-
- Documentation is updated.
110-
- `make lint` and `make format` have been run.
111-
- The full test suite passes.
73+
Concise, imperative mood, small focused commits. Write like a humble experienced engineer — casual, no listicles, highlight non-obvious choices. No robot speak, marketing buzzwords, or vague fluff.
11274

113-
## Commit Messages
75+
## Pull request expectations
11476

115-
Commit messages should be concise and written in the imperative mood. Small, focused commits are preferred.
77+
Use the PR template. For frontend, backend, and backend/scripts changes:
78+
- Add tests when needed
79+
- Update documentation
80+
- Run `make lint` and `make fmt`
81+
- Full test suite passes
11682

117-
Write commit messages and PR descriptions as a humble but experienced engineer would. Keep it casual, avoid listicles, briefly describe what we're doing and highlight non-obvious implementation choices but don't overthink it.
83+
## What reviewers look for
11884

119-
Don't embarrass me with robot speak, marketing buzzwords, or vague fluff. Just leave a meaningful trace so someone can understand the choices later. Assume the reader is able to follow the code perfectly fine.
85+
- Tests covering new behaviour
86+
- Consistent style: formatted with `ruff format`, imports sorted, type hints passing `make typecheck`
87+
- Clear documentation for public API changes
88+
- Clean history and helpful PR description
89+
- Consistent environment variable declarations across GitHub Actions, `.env.example`, tests, and docs — no orphaned secrets/variables
12090

121-
## GitHub Actions Security
91+
## GitHub Actions security
12292

123-
We pin all third-party actions to commit SHAs to prevent supply chain attacks:
93+
Pin third-party actions to commit SHAs:
12494

12595
```yaml
126-
# Good: Commit SHA with inline version comment
96+
# Good: SHA with version comment
12797
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 #v1.0.0
12898

129-
# Bad: Floating version tags
99+
# Bad: floating tag
130100
uses: appleboy/scp-action@v1.0.0
131101
```
132102
133-
We allow fully qualified semantic version tag from
134-
- `Astral` (uv) github actions (note: CodeQL will warn about this)
135-
- immutable tags
136-
137-
```yaml
138-
# ✅ Good: semantic version tag
139-
uses: astral-sh/setup-uv@7.3.0
140-
141-
# ❌ Bad: major-only version tag
142-
uses: astral-sh/setup-uv@7
143-
```
144-
145-
## What reviewers look for
146-
147-
- Tests covering new behaviour.
148-
- Consistent style: code formatted with `uv run ruff format`, imports sorted, and type hints passing `make typecheck`.
149-
- Clear documentation for any public API changes.
150-
- Clean history and a helpful PR description.
151-
- Inconsistent environment variables and secrets declarations across GitHub Actions, .env.example, tests, and relevant Markdown documentation; this includes secrets and variables which are declared but never referenced in the code.
103+
Exceptions: Astral (uv) actions may use fully qualified semver tags (e.g. `astral-sh/setup-uv@7.3.0`), and immutable tags are allowed.

.claude/settings.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"permissions": {
33
"allow": [
4-
"WebFetch(https://docs.langchain.com/*)",
5-
"WebFetch(https://reference.langchain.com/*)",
6-
"WebFetch(https://ai.google.dev/gemini-api/*)",
7-
"WebFetch(https://docs.digitalocean.com/*)",
8-
"WebFetch(https://docs.github.com/en/*)",
9-
"WebFetch(https://docs.docker.com/*)",
10-
"WebFetch(https://docs.astral.sh/*)"
4+
"WebFetch(domain:docs.langchain.com)",
5+
"WebFetch(domain:reference.langchain.com)",
6+
"WebFetch(domain:ai.google.dev)",
7+
"WebFetch(domain:docs.digitalocean.com)",
8+
"WebFetch(domain:docs.github.com)",
9+
"WebFetch(domain:docs.docker.com)",
10+
"WebFetch(domain:docs.astral.sh)"
1111
]
1212
}
1313
}

.github/workflows/pr-check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ jobs:
7777
7878
# NOTE: tests require GOOGLE_* env vars to be set from one of the above steps
7979
- name: Run tests that mock services that require repo secrets
80-
run: uv run pytest -v -s -m "not require_repo_secrets"
80+
run: uv run pytest -v -s -m "not require_repo_secrets" --cov --cov-report term-missing
8181

8282
- name: Run additional tests that require repo secrets
8383
if: env.PR_FROM_FORK != 'true'
84-
run: uv run pytest -v -s -m "require_repo_secrets"
84+
run: uv run pytest -v -s -m "require_repo_secrets" --cov --cov-append --cov-report term-missing
8585

8686
frontend-build:
8787
runs-on: ubuntu-latest

backend/pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,11 @@ dev = [
5353
markers = [
5454
"require_repo_secrets: mark test as requiring repository secrets to run",
5555
]
56+
57+
[tool.coverage.run]
58+
source = ["tenantfirstaid"]
59+
branch = true
60+
61+
[tool.coverage.report]
62+
fail_under = 80
63+
show_missing = true

backend/tenantfirstaid/constants.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class _GoogEnvAndPolicy:
3939
"SHOW_MODEL_THINKING",
4040
"SAFETY_SETTINGS",
4141
"MODEL_TEMPERATURE",
42+
"TOP_P",
4243
"MAX_TOKENS",
4344
)
4445

@@ -93,10 +94,11 @@ def __init__(self) -> None:
9394
HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.OFF,
9495
}
9596

96-
# Gemini 2.5 default is 0.7 (this was the value used before explicitly setting it)
97-
# Gemini 3+ will automatically set to 1.0 as per Google best practices doc.
97+
# Low temperature for consistent legal citation output.
98+
# Gemini 2.5 default is 0.7; Gemini 3+ defaults to 1.0.
9899
# https://reference.langchain.com/python/integrations/langchain_google_genai/ChatGoogleGenerativeAI/#langchain_google_genai.ChatGoogleGenerativeAI.temperature
99-
self.MODEL_TEMPERATURE: Final = float(0.7)
100+
self.MODEL_TEMPERATURE: Final = float(0.1)
101+
self.TOP_P: Final = float(0.1)
100102
self.MAX_TOKENS: Final = 65535
101103

102104

backend/tenantfirstaid/langchain_chat_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(self) -> None:
6969
safety_settings=SINGLETON.SAFETY_SETTINGS,
7070
# consistency
7171
temperature=SINGLETON.MODEL_TEMPERATURE, # 1.0 is default for Gemini 3+, https://docs.langchain.com/oss/python/integrations/chat/google_generative_ai#instantiation
72+
top_p=SINGLETON.TOP_P,
7273
seed=0,
7374
# reasoning
7475
thinking_budget=-1, # gemini 2.5 specific (use thinking_level for 3+ https://docs.langchain.com/oss/python/integrations/chat/google_generative_ai#thinking-support)

backend/tenantfirstaid/langchain_tools.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(
3131
self,
3232
filter: str,
3333
name: Optional[str] = "tfa-retriever",
34-
max_documents: Optional[int] = 1,
34+
max_documents: Optional[int] = 3,
3535
) -> None:
3636
if SINGLETON.GOOGLE_APPLICATION_CREDENTIALS is None:
3737
raise ValueError("GOOGLE_APPLICATION_CREDENTIALS is not set")
@@ -147,7 +147,7 @@ def retrieve_city_state_laws(
147147

148148
helper = Rag_Builder(
149149
name="retrieve_city_law",
150-
max_documents=1,
150+
max_documents=3,
151151
filter=__filter_builder(city=city, state=state),
152152
)
153153

backend/tests/conftest.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest
2+
from flask import Flask
3+
4+
from tenantfirstaid.location import OregonCity, UsaState
5+
6+
7+
@pytest.fixture
8+
def oregon_state():
9+
return UsaState.from_maybe_str("or")
10+
11+
12+
@pytest.fixture
13+
def portland_city():
14+
return OregonCity.from_maybe_str("Portland")
15+
16+
17+
@pytest.fixture
18+
def eugene_city():
19+
return OregonCity.from_maybe_str("Eugene")
20+
21+
22+
@pytest.fixture
23+
def app():
24+
"""Flask app with testing=True for use in test client and request context."""
25+
app = Flask(__name__)
26+
app.testing = True
27+
return app
28+
29+
30+
@pytest.fixture
31+
def client(app):
32+
"""Flask test client."""
33+
return app.test_client()
34+
35+
36+
@pytest.fixture
37+
def mock_chat_manager(mocker):
38+
"""Mocked LangChainChatManager that yields canned streaming responses."""
39+
mock = mocker.patch("tenantfirstaid.chat.LangChainChatManager", autospec=True)
40+
instance = mock.return_value
41+
instance.generate_streaming_response.return_value = iter(
42+
[{"type": "text", "text": "Mocked legal advice."}]
43+
)
44+
return instance

0 commit comments

Comments
 (0)