Append-only decision ledger SDK for AI agents in regulated environments
Project description
🛡️ DecisionLedger
Decision Authority Infrastructure
Append-only decision ledger for AI agents in regulated environments.
EU AI Act Article 19 compliant by design.
Built by Mandate — github.com/rksingh95/DecisionLedger
📖 Table of Contents
- The Problem
- What DAI Records
- Quickstart — Docker (Recommended)
- Quickstart — Local Development
- SDK Usage Patterns
- Article 19 Compliance Export
- Configuration
- CLI Reference
- Makefile Reference
- Hash Chain Verification
- Architecture
- Testing & Code Quality
- Roadmap
- Contributing & Licence
🚨 The Problem
AI agents make consequential decisions — approve a loan, triage a claim, flag a transaction — but those decisions are rarely recorded in a structured, auditable way. When regulators require an audit trail, or when an incident needs to be reconstructed, organisations discover they have logs but no ledger: timestamped text, not tamper-evident, typed records.
The EU AI Act (Article 19) now mandates structured logging for high-risk AI systems. DecisionLedger provides exactly that, as a drop-in SDK with a production-ready server.
📝 What DAI Records
Every decision produces a mathematically verifiable, tamper-evident record:
decision_id: "01927f3c-8a1b-7000-8000-000000000001" # UUIDv7
record_hash: "3a4f8c..." # SHA-256 of prev_hash + record
previous_hash: "0000..." # Links to previous record
ledger_version: "0.1.0"
decision_timestamp: "2026-01-15T09:00:00.000000Z"
agent_id: "claims-agent-eu-01"
agent_type: "autonomous"
model_version: "gpt-4o-v2024-11"
authorized_scope: "motor claims triage"
delegation_source: "underwriting-team"
human_oversight_required: false
override_applied: false
decision_type: "motor_claims_triage"
subject_ref: "claim:MC-2026-00142"
policy_id: "motor-claims-v3"
policy_version: "3.2.1"
clauses_applied: ["3.1", "4.2", "5.3"]
outcome: "approved"
confidence: 0.93
evidence_refs: ["doc:claim-form-142", "img:damage-photo-01"]
data_sources_accessed: ["claims-db", "policy-db", "vehicle-registry"]
context_completeness: "full"
exception_applied: false
metadata:
region: "EU"
gdpr_basis: "contract"
🐳 Quickstart — Docker (Recommended)
The fastest way to get a production-grade stack running locally with PostgreSQL:
git clone https://github.com/rksingh95/DecisionLedger.git
cd DecisionLedger
make docker-up
That's it. Under the hood it:
- Builds the Docker image
- Starts PostgreSQL 16 and waits until healthy
- Runs Alembic migrations automatically
- Starts the API server (only after migrations succeed)
| Endpoint | URL |
|---|---|
| API | http://localhost:8080 |
| Interactive Docs (Swagger) | http://localhost:8080/docs |
| Health Check | http://localhost:8080/health |
| Prometheus Metrics | http://localhost:8080/metrics |
# Verify it's running
curl http://localhost:8080/health
# {"status": "ok", "version": "0.1.0"}
Useful Docker commands
make docker-logs # tail logs from all services
make docker-logs-api # tail API server logs only
make docker-status # show container health
make docker-psql # open psql inside the container
make docker-shell # bash into the API container
make docker-down # stop (keeps database data)
make docker-clean # stop + wipe the database volume
make docker-rebuild # force rebuild from scratch
Conflict with a local Postgres? If port 5432 is already in use:
DAI_POSTGRES_PORT=5433 make docker-up
💻 Quickstart — Local Development
For SDK development and running tests without Docker:
git clone https://github.com/rksingh95/DecisionLedger.git
cd DecisionLedger
# One-time setup: creates .venv, installs deps, installs pre-commit hooks, copies .env
make install-dev
# Start server (uses SQLite by default — zero setup)
make server
# → API running on http://localhost:8080
# Run tests
make test
# ✓ 103 tests passed, 94% coverage
2. Install the SDK in your agent
pip install decision-ledger-sdk
# Configure
export DAI_ENDPOINT=http://localhost:8080
export DAI_API_KEY=dev-key-change-me
3. Record your first decision
import dai
result = await (
dai.Decision.begin(
agent_id="my-agent",
decision_type="claims_triage",
subject_ref="claim:CLM-001",
)
.with_policy(policy_id="motor-claims-v3", policy_version="3.2.1")
.with_authority(authorized_scope="triage", delegation_source="underwriting-team")
.with_context(
evidence_refs=["doc:claim-form", "img:damage-photo"],
data_sources_accessed=["claims-db", "policy-db"],
)
.with_outcome(outcome="approved", confidence=0.93)
.commit()
)
print(f"Recorded: {result.decision_id}")
🧩 SDK Usage Patterns
DAI is unopinionated. Choose the integration pattern that fits your codebase.
Pattern 1 — Fluent Builder
Best for explicitly constructed records deep in business logic.
import dai
result = await (
dai.Decision.begin(
agent_id="claims-agent-01",
decision_type="claims_triage",
subject_ref="claim:CLM-2025-001234",
model_version="gpt-4o-2024-08-06",
)
.with_policy(
policy_id="motor-claims-v3",
policy_version="3.2.1",
clauses_applied=["3.1", "4.2"],
)
.with_authority(
authorized_scope="motor claims triage up to £10,000",
delegation_source="underwriting-team",
human_oversight_required=False,
)
.with_context(
evidence_refs=["doc:claim-form-v2", "img:damage-photo-01"],
data_sources_accessed=["claims-db", "policy-db", "fraud-api"],
)
.with_outcome(outcome="approved", confidence=0.93, alternatives_considered=3)
.with_metadata("claim_value_gbp", "8500")
.commit()
)
Pattern 2 — Context Manager
Best for wrapping existing code blocks. Auto-commits on exit, logs conservative_fallback if an exception is raised.
async with dai.Decision.begin(
agent_id="risk-agent",
decision_type="risk_classification",
subject_ref="application:APP-001",
) as d:
d.with_policy("risk-policy-v2", "2.1.0")
d.with_authority("risk scoring", "risk-team")
d.with_context(["credit-report:ref"], ["credit-bureau-api"])
result = await classify_risk(application_id="APP-001")
d.with_outcome(outcome=result.label, confidence=result.score)
# Auto-commits on exit. Exceptions are caught and recorded safely.
Pattern 3 — Decorator
Best for non-invasive integration with existing functions — zero changes to business logic.
from dai.decorators import log_decision
@log_decision(
agent_id="claims-agent",
decision_type="claims_triage",
policy_id="motor-claims-v3",
policy_version="3.2.1",
extract_subject=lambda args, kwargs: f"claim:{kwargs['claim_id']}",
extract_outcome=lambda r: {"outcome": r.decision, "confidence": r.score},
)
async def triage_claim(claim_id: str, data: dict) -> TriageResult:
# Your existing function — untouched
...
Pattern 4 — LangChain Callback
Best for out-of-the-box framework support.
from dai.integrations.langchain import DAICallbackHandler
handler = DAICallbackHandler(
agent_id="my-langchain-agent",
decision_type="claims_triage",
policy_id="policy-v3",
policy_version="3.2.1",
)
agent_executor = AgentExecutor(agent=agent, tools=tools, callbacks=[handler])
# Decisions recorded automatically on agent finish/error
Pattern 5 — OpenTelemetry
Emit decision spans into your existing observability stack.
from dai.integrations.opentelemetry import enable_otel_bridge
enable_otel_bridge()
# Decisions are now emitted as OTEL spans to your configured exporter
🇪🇺 EU AI Act Article 19 Compliance Export
Article 19 of the EU AI Act requires providers of high-risk AI systems to maintain structured logs enabling post-hoc monitoring. DecisionLedger satisfies this with three export formats:
Export formats
| Format | Use case | Command |
|---|---|---|
| Regulatory submission, human auditors | ?format=pdf |
|
| JSON | Machine-readable, system integration | ?format=json (default) |
| Text | Quick review, CI audit checks | ?format=text |
PDF Report — Sample
The PDF report is a professional A4 branded document with:
- Cover page — reporting period, ledger version, chain integrity badge
- Executive Summary — KPI strip (decisions, exceptions, overrides, chain status) + outcome distribution
- Agent Activity — per-agent decision counts
- Decision Types — breakdown by type
- Policy Versions — all policy versions active in the period
- Exceptions & Overrides — amber-highlighted flagged records with reasons
- Chain Integrity — verification method and status (SHA-256)
- Individual Records — full alternating-row table of every decision
📄 View sample Article 19 PDF report
Generate via API
# PDF report (downloads as article19_report_<timestamp>.pdf)
curl -X POST "http://localhost:8080/export/article19?format=pdf" \
-H "X-API-Key: $DAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from_timestamp": "2026-01-01T00:00:00Z",
"to_timestamp": "2026-12-31T23:59:59Z",
"include_chain_proof": true
}' \
--output article19_report.pdf
# JSON export (default)
curl -X POST "http://localhost:8080/export/article19" \
-H "X-API-Key: $DAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from_timestamp": "2026-01-01T00:00:00Z", "to_timestamp": "2026-12-31T23:59:59Z"}'
# Plain text
curl -X POST "http://localhost:8080/export/article19?format=text" \
-H "X-API-Key: $DAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from_timestamp": "2026-01-01T00:00:00Z", "to_timestamp": "2026-12-31T23:59:59Z"}'
Generate via CLI
dai export --from 2026-01-01 --to 2026-12-31 --format pdf
What Article 19 compliance requires
- ✅ Full agent identity (ID, type, model version, delegation chain)
- ✅ Policy version and clauses applied at decision time
- ✅ Evidence references and data sources accessed
- ✅ Explicit exception and override capture with reasons
- ✅ SHA-256 hash chain for tamper evidence
- ✅ Structured export in JSON, PDF, and plain text
⚙️ Configuration
Control SDK behaviour via environment variables (.env files are loaded automatically).
| Variable | Type | Default | Description |
|---|---|---|---|
DAI_BACKEND |
http|sqlite|noop |
http |
Storage backend |
DAI_ENDPOINT |
str |
http://localhost:8080 |
Server URL |
DAI_API_KEY |
str |
"" |
API key |
DAI_TIMEOUT_SECONDS |
float |
2.0 |
HTTP timeout |
DAI_MAX_RETRIES |
int |
3 |
HTTP retry count |
DAI_ON_ERROR |
raise_exception|log_and_continue|noop |
log_and_continue |
Error policy |
DAI_SQLITE_PATH |
str |
./dai_local.db |
SQLite path (backend=sqlite) |
DAI_LOG_LEVEL |
str |
INFO |
Log level |
DAI_EMIT_OTEL_SPANS |
bool |
false |
OpenTelemetry spans |
Server environment variables
| Variable | Default | Description |
|---|---|---|
DAI_DATABASE_URL |
sqlite+aiosqlite:///./dev.db |
DB connection string |
DAI_API_KEY |
dev-key-change-me |
Server authentication key |
DAI_CORS_ORIGINS |
"" |
Comma-separated allowed origins |
DAI_POSTGRES_PORT |
5432 |
Host port for Postgres (Docker) |
💻 CLI Reference
| Command | Description |
|---|---|
dai verify --from DATE --to DATE |
Verify hash chain integrity. Exit 0 = valid, 1 = broken. |
dai query [--agent] [--type] [--outcome] [--format] |
Query decision records |
dai export --from DATE --to DATE [--format json|pdf|text] |
Article 19 compliance export |
dai status |
Check server connectivity and ledger health |
🛠️ Makefile Reference
All developer workflows are available via make. Run make help to see all commands.
Setup
make install-dev # Create .venv, install all deps, set up pre-commit hooks, copy .env
make install-all # Like install-dev but also includes langchain + opentelemetry extras
Code Quality
make lint # Run ruff linter (check only)
make format # Auto-format with ruff (modifies files)
make typecheck # Run mypy strict type-checker on dai/
make check # lint + typecheck together
make pre-commit # Run all pre-commit hooks on all files (useful before a PR)
Testing
make test # Unit tests with coverage (≥90% required)
make test-unit # Unit tests only (fast)
make test-cov # HTML coverage report → htmlcov/index.html
Server (Local SQLite)
make server # Start API with --reload on :8080 (SQLite, no Postgres needed)
make migrate # Apply Alembic migrations locally
Docker (Full Stack with Postgres)
make docker-up # Build + start full stack (Postgres → migrate → API)
make docker-down # Stop (keeps DB data)
make docker-clean # Stop + wipe database volume
make docker-logs # Tail all service logs
make docker-status # Show container health
make docker-psql # Connect to Postgres inside Docker
make docker-shell # Bash into API container
make docker-rebuild # Force rebuild images from scratch
🔒 Hash Chain Verification
Each record's integrity hash is computed deterministically:
record_hash = SHA-256(previous_hash + ":" + canonical_json(record))
If any historical record is modified, its hash changes — but the next record's previous_hash still points to the original value. This makes tampering instantly detectable. A full chain scan from GENESIS_HASH identifies exactly where the chain breaks.
dai verify --from 2026-01-01 --to 2026-12-31
# ✓ VERIFIED — 1,247 records, chain intact
Or via the API:
curl http://localhost:8080/verify \
-H "X-API-Key: $DAI_API_KEY"
🏗️ Architecture
graph TD
subgraph Agents ["AI Agent Environment"]
B["dai.Decision.begin().commit()"]
C["@log_decision(...)"]
D["DAICallbackHandler (LangChain)"]
E["emit_decision_span (OpenTelemetry)"]
end
Agents -- "HTTP POST /ingest\nX-API-Key: <key>" --> Server
subgraph Backend ["DAI Server (FastAPI + Gunicorn)"]
Server["API Router"]
Server --> I["POST /ingest\n(Hash verify + chain continuity)"]
Server --> Q["GET /decisions\n(Query + cursor pagination)"]
Server --> V["GET /verify\n(Chain verification)"]
Server --> Ex["POST /export/article19\n(?format=json|pdf|text)"]
Server --> M["GET /metrics\n(Prometheus)"]
end
Backend -- "SQLAlchemy Async" --> DB
subgraph Database ["PostgreSQL 16 (or SQLite for local dev)"]
DB[("decisions table\n- Append-only\n- Hash chain enforced")]
end
subgraph CI ["GitHub Actions CI"]
CI1["ruff lint + format"]
CI2["mypy strict"]
CI3["pytest ≥90% coverage"]
CI4["build wheel"]
end
style Agents fill:#f4f4f4,stroke:#333,stroke-width:2px,color:#000
style Backend fill:#e1f5fe,stroke:#0288d1,stroke-width:2px,color:#000
style Database fill:#e8f5e9,stroke:#388e3c,stroke-width:2px,color:#000
style CI fill:#fff8e1,stroke:#f9a825,stroke-width:2px,color:#000
🧪 Testing & Code Quality
Pre-commit hooks
Every git commit automatically runs:
| Hook | What it checks |
|---|---|
trailing-whitespace / end-of-file-fixer |
File hygiene |
check-yaml / check-toml |
Config syntax |
check-merge-conflict |
No conflict markers |
check-added-large-files |
Files < 500 KB |
debug-statements |
No stray breakpoint() / pdb |
ruff lint + auto-fix |
Style and correctness |
ruff format |
Consistent formatting |
mypy |
Type-check dai/ |
pytest |
Unit tests |
Install hooks (done automatically by make install-dev):
pre-commit install
Run all hooks manually:
make pre-commit
CI Pipeline (GitHub Actions)
Every push and PR runs:
- Lint —
ruff check+ruff format --check - Type check —
mypy --strictondai/ - Tests —
pytestwith ≥ 90% coverage gate - Build —
python -m build(sdist + wheel)
Coverage
dai/ 94%
dai_server/ ~80%
🗺️ Roadmap
| Phase | Status | Description |
|---|---|---|
| 0 | 🟢 Done | Decision ledger, hash chain, Article 19 export (JSON + PDF + text) |
| 0 | 🟢 Done | FastAPI server, PostgreSQL, Docker one-command setup |
| 0 | 🟢 Done | LangChain + OpenTelemetry integrations |
| 0 | 🟢 Done | CI/CD pipeline, pre-commit hooks, 94% coverage |
| 1 | ⏳ Planned | Policy versioning, authority chains, override modelling |
| 2 | ⏳ Planned | Decision memory, policy drift detection |
| 3 | ⏳ Planned | Regulator-ready exports, integrity proofs, retention controls |
🤝 Contributing & Licence
- Contributing: Please see CONTRIBUTING.md for development setup, testing, and PR guidelines.
- Licence: MIT — Copyright © 2026 Mandate
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file decision_ledger_sdk-0.1.0.tar.gz.
File metadata
- Download URL: decision_ledger_sdk-0.1.0.tar.gz
- Upload date:
- Size: 48.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76fae640c5622eabdc490a579c3964ceca362e8b2a9965dece16be1e3527b860
|
|
| MD5 |
0c6b474e4d589bc762e86aef72b93917
|
|
| BLAKE2b-256 |
3ac9f091d9e15885a3635f53585a22a231d7e405c4c18ad712e2dfa5c07b68a0
|
File details
Details for the file decision_ledger_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: decision_ledger_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 61.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe25b01373143a732e9e8881b213ee53d33ed4413f9dc9165783d890252076d9
|
|
| MD5 |
314d100c35a8b6bcc36cc5dcdf5b678e
|
|
| BLAKE2b-256 |
1618c44acf2b1fe827829c7aedbc9c9a5a901932f4eb6084aade94c064d7709b
|