FastAPI-based reconciliation service for SEAD (Strategic Environmental Archaeology Database) implementing the OpenRefine Reconciliation API. Core function: match fuzzy text queries to canonical entity identifiers from archaeological/environmental database.
Key architectural insight: This is a registry-based plugin system where reconciliation strategies auto-register via decorators and are looked up at runtime by entity type.
All entity reconciliation strategies must be decorated with @Strategies.register():
from src.strategies.strategy import ReconciliationStrategy, Strategies
@Strategies.register(key="site", repository_cls=SiteRepository)
class SiteReconciliationStrategy(ReconciliationStrategy):
# ImplementationWhy: Strategies auto-register on import. main.py imports src.strategies which triggers recursive module loading in src/strategies/__init__.py that instantiates all decorated classes.
Use ConfigValue for lazy config resolution instead of direct lookups:
from src.configuration import ConfigValue
# Lazy resolution with fallback
threshold = ConfigValue("options:auto_accept_threshold", default=0.90).resolve()
# Multi-path fallback (checks each path in order)
model = ConfigValue("llm.ollama.model,llm.model", default="llama3").resolve()Location: src/configuration/resolve.py. Supports colon-separated paths (options:database:dbname) and comma-separated fallbacks.
Never create database connections directly. Use the config provider pattern:
from src.configuration import get_connection
async with await get_connection() as conn:
async with conn.cursor() as cur:
await cur.execute(query, params)Connection is created once at startup via src/configuration/setup.py and stored in runtime config.
Entity schemas are generated, not hand-written. To add a new entity:
- Add entity config to config/entities.yml
- Run
make generate-schema(calls src/scripts/generate_entity_schema.py) - Generated SQL appears in schema/generated/
Do not edit generated files directly. Edit schema/templates/ or config/entities.yml instead.
make serve # Development with auto-reload
make dev-serve # Start uvicorn + OpenRefine together
make dev-stop # Stop both servicesPort: 8000 (uvicorn), 3333 (OpenRefine). PIDs written to uvicorn.pid and refine.pid.
make test # Run pytest suite
uv run pytest -m integration # Integration tests only (require DB/Ollama)
uv run pytest -k test_name # Run specific testTest markers: @pytest.mark.integration, @pytest.mark.manual, @pytest.mark.debug (see pyproject.toml).
make lint # Run tidy + pylint + check-imports
make tidy # Run black + isort
make check-imports # Verify no relative imports beyond current packageImport rules: Ruff enforces ban-relative-imports = "parents" (see pyproject.toml). Use absolute imports from src.*.
OpenRefine → POST /reconcile → router.py:reconcile() →
reconcile.py:reconcile_queries() →
Strategies.get("site") → SiteReconciliationStrategy →
SiteRepository.search() → PostgreSQL
Phase 1 implementation uses embedded MCP server for small-prompt reconciliation:
from src.strategies.rag_hybrid import RAGHybridReconciliationStrategy
class MethodReconciliationStrategy(RAGHybridReconciliationStrategy):
# Uses MCP search_lookup → 5-10 candidates → LLM validationFeature flag: features.use_mcp_server in config/config.yml. When disabled, falls back to standard fuzzy search.
- Base config: config/config.yml
- Entity schemas: config/entities.yml
- LLM prompts: config/prompts.yml
- Environment vars:
.env(loaded via src/configuration/setup.py)
Override config file: export CONFIG_FILE=./tests/config/config.yml
-
Strategy not found: Ensure strategy file is in
src/strategies/and decorated with@Strategies.register(). Import errors are printed to console during startup but don't fail startup. -
Config not available: Call
await setup_config_store()before accessing config. FastAPI does this in main.py startup event. -
Schema changes ignored: Run
make generate-schemaafter editing config/entities.yml. The--forceflag overwrites existing files. -
Test isolation: Use fixtures from tests/conftest.py.
MockConfigProviderprevents tests from hitting real database. -
LLM provider setup: Ollama/OpenAI providers lazy-load config. See src/llm/providers/ for provider-specific settings.
This project uses Conventional Commits with semantic-release for automated versioning. AI agents making commits must follow this format:
<type>[optional scope]: <description>
- feat: New feature → MINOR release (1.2.0)
- fix: Bug fix → PATCH release (1.2.1)
- refactor/perf/style: Code improvements → PATCH release
- docs: Documentation → PATCH if scope is README
- test/build/ci/chore: No release
Add ! after type/scope or BREAKING CHANGE: in footer → MAJOR release (2.0.0):
feat(api)!: change response format for validation errorscore, config, api, cache, loaders, tests, deps
feat(cache): implement hash-based cache invalidation
fix(validation): prevent null pointer in entity resolution
refactor(core): simplify dependency resolution logic
docs(README): update installation instructions
test(loaders): add comprehensive UCanAccessSqlLoader testsRules: Use imperative mood, lowercase description, no trailing period, keep under 72 chars.
- main.py - FastAPI app entry point, imports strategies
- src/api/router.py - All HTTP endpoints
- src/reconcile.py - Core reconciliation logic
- src/strategies/strategy.py - Base strategy class and registry
- src/configuration/ - Config provider pattern
- config/entities.yml - Entity definitions (source of truth)
- Makefile - All developer commands
Multi-environment support:
- Development:
cd docker && docker-compose up --build - Production: Uses GHCR images from CI/CD (see docker/README.md)
GitHub Actions builds on every push to main/dev and pushes to ghcr.io/humlab-sead/sead_authority_service.