diff --git a/.env.example b/.env.example index ec98c23..1f30890 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,24 @@ # Smriti - Environment Variables -# Copy this file to .env and fill in the values +# Copy this file to .env and fill in the values. +# +# The backend loads this file via python-dotenv at startup so API keys +# are available even when the server is launched by a process that does +# not inherit your shell profile (e.g. uvicorn from an IDE, a CI +# runner, or a subprocess-based tool host like Claude Code). +# +# Existing env vars are NOT overridden — if you also export a key in +# your shell profile, the shell value takes precedence. # Database DATABASE_URL=postgresql://smriti:smriti@localhost:5432/smriti -# OpenAI (leave empty to use mock provider) +# Provider API keys — set the ones you want to use. +# Leave empty to fall back to config/providers.yaml or mock provider. OPENAI_API_KEY= +ANTHROPIC_API_KEY= +OPENROUTER_API_KEY= + +# Default model for background intelligence (extraction, draft, review) OPENAI_MODEL=gpt-4o-mini # App diff --git a/backend/app/config_loader.py b/backend/app/config_loader.py index 6dc2933..59e1db1 100644 --- a/backend/app/config_loader.py +++ b/backend/app/config_loader.py @@ -1,6 +1,12 @@ """ Provider configuration loader. Priority: env vars > config/providers.yaml > built-in defaults. + +The .env file (at the project root, one level above backend/) is loaded +via python-dotenv at import time so that API keys set there are visible +to os.environ.get() even when the backend is started by a process that +does not inherit the user's shell profile (e.g. uvicorn launched from +an IDE, a CI runner, or a subprocess-based tool host). """ from __future__ import annotations @@ -8,6 +14,19 @@ import os from dataclasses import dataclass, field from pathlib import Path +from dotenv import load_dotenv + +# Load .env from the project root (smriti/) — one level above +# backend/. Falls back silently if the file does not exist (e.g. in +# Docker where env vars are injected by the runtime). +_PROJECT_ROOT_ENV = Path(__file__).parent.parent.parent / ".env" +_BACKEND_DIR_ENV = Path(__file__).parent.parent / ".env" +# Try project root first (where .env.example lives), then backend dir. +if _PROJECT_ROOT_ENV.is_file(): + load_dotenv(_PROJECT_ROOT_ENV, override=False) +elif _BACKEND_DIR_ENV.is_file(): + load_dotenv(_BACKEND_DIR_ENV, override=False) + try: import yaml _YAML_AVAILABLE = True diff --git a/backend/app/providers/registry.py b/backend/app/providers/registry.py index a276b03..189c1b5 100644 --- a/backend/app/providers/registry.py +++ b/backend/app/providers/registry.py @@ -2,10 +2,13 @@ from __future__ import annotations import json as _json +import logging from app.config_loader import get_provider_config, ProviderNotConfiguredError from app.providers.base import ProviderAdapter +logger = logging.getLogger(__name__) + # Canned JSON response returned by the mock adapter when the caller asks # for JSON mode (response_format={"type": "json_object"}). Covers every @@ -74,6 +77,13 @@ def get_adapter(provider: str, allow_mock: bool = False) -> ProviderAdapter: cfg = get_provider_config(p) except ProviderNotConfiguredError: if allow_mock: + logger.warning( + "Provider '%s' has no API key configured — falling back to " + "MockAdapter. Extraction, drafting, and review endpoints will " + "return deterministic mock content instead of real LLM output. " + "Set the API key in .env or config/providers.yaml to fix this.", + provider, + ) return MockAdapter() raise diff --git a/backend/pyproject.toml b/backend/pyproject.toml index a80a7b2..74ed8a5 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "anthropic>=0.40.0", "pyyaml>=6.0", "pgvector>=0.2.0", + "python-dotenv>=1.0.0", ] [project.optional-dependencies]