Project Setup
Create the project skeleton, install dependencies, bring up Postgres in Docker, wire config & secrets. End of this chapter: a verifiable empty project ready for code.
Repo layout
Create the directory structure first.
shell
mkdir -p contract-intelligence/{data/{inbox,gold},src/ci/{extraction,eval},tests}
cd contract-intelligence
touch src/ci/__init__.py
touch src/ci/{config,db,models,ingestion,api}.py
touch src/ci/extraction/{__init__,ocr,llm,schemas}.py
touch src/ci/eval/{__init__,runner,scoring}.py
touch tests/{__init__,test_ingestion,test_extraction,test_api}.py
Dependencies
Pyproject.toml. Standard modern Python project; uv or poetry both work.
pyproject.toml
[project]
name = "contract-intelligence"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.110",
"uvicorn[standard]>=0.27",
"sqlalchemy>=2.0",
"psycopg[binary]>=3.1",
"alembic>=1.13",
"anthropic>=0.30",
"pdfplumber>=0.10",
"pytesseract>=0.3.10",
"pillow>=10.0",
"pdf2image>=1.17",
"pydantic>=2.6",
"pydantic-settings>=2.2",
"rich>=13.7",
"typer>=0.12",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"httpx>=0.27",
"mypy>=1.10",
"ruff>=0.4",
]
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.mypy]
strict = true
Install:
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Also install Tesseract on the system (skip if you only have native PDFs)
# macOS: brew install tesseract poppler
# Ubuntu: sudo apt install tesseract-ocr poppler-utils
Postgres in Docker
One container, port 5432, persistent volume.
docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: ci
POSTGRES_PASSWORD: ci_dev
POSTGRES_DB: contract_intel
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: {}
Bring it up:
docker compose up -d
# Verify
docker compose exec postgres psql -U ci -d contract_intel -c "SELECT version();"
Config & secrets
Pydantic-settings reads from environment + .env file.
src/ci/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="CI_", extra="ignore")
# Database
db_dsn: str = "postgresql+psycopg://ci:ci_dev@localhost:5432/contract_intel"
# LLM
anthropic_api_key: str # required, no default
extraction_model: str = "claude-sonnet-4-6"
extraction_temperature: float = 0.0
extraction_max_tokens: int = 2048
# Paths
inbox_dir: str = "data/inbox"
gold_dir: str = "data/gold"
# Pipeline
pipeline_version: str = "v0.1.0"
customer_id: str = "demo_customer"
settings = Settings()
.env (gitignored)
CI_ANTHROPIC_API_KEY=sk-ant-...
# Override others as needed
Add .env to .gitignore.
Verify it works
Smoke test: import the module, instantiate settings, hit Postgres.
scripts/verify_setup.py
"""Verify project setup: imports work, Postgres reachable, LLM key present."""
from sqlalchemy import create_engine, text
from ci.config import settings
def main() -> None:
print(f"Pipeline version: {settings.pipeline_version}")
print(f"Model: {settings.extraction_model}")
print(f"DB: {settings.db_dsn}")
eng = create_engine(settings.db_dsn)
with eng.connect() as conn:
result = conn.execute(text("SELECT version();")).scalar()
print(f"Postgres: {result}")
assert settings.anthropic_api_key, "No Anthropic API key"
print("✓ Setup OK.")
if __name__ == "__main__":
main()
Run:
python scripts/verify_setup.py
You should see:
Pipeline version: v0.1.0
Model: claude-sonnet-4-6
DB: postgresql+psycopg://ci:ci_dev@localhost:5432/contract_intel
Postgres: PostgreSQL 16.x ...
✓ Setup OK.
If anything fails, troubleshoot before moving to chapter 02 — every later chapter assumes this works.