Open-source Human-in-the-Loop AI platform for maritime port agency cost validation.
Accountants upload a Proforma DA (JSON) + Final DA (PDF). An LLM extracts every line item, flags deviations, and generates a full audit trail — all without lock-in to a specific AI provider.
flowchart TD
subgraph PORT ["Port Operations"]
V[Vessel Arrives at Port]
V --> SVC["Port Agent Provides Services\npilotage · towage · berth · provisions · crew"]
SVC --> FDA_DOC["Port Agent Issues FDA\nFinal Disbursement Account"]
end
subgraph OWNER ["Ship Owner / Operator"]
FDA_DOC --> RECV["Receives FDA + PDA Proforma Estimate"]
RECV --> TRAD["Traditional Approach\n── manual line-by-line comparison ──\n⚠ error-prone · days to complete\n⚠ no audit trail · no provider lock-in"]
end
subgraph OPENDA ["OpenDA — AI-Assisted DA Validation"]
direction TB
UP["① Accountant Uploads\nPDA JSON + FDA PDF"]
EXT["② AI Extraction Docling + LLM\nPDF parsed into structured line items + bounding boxes"]
DEV["③ Deviation Engine\nPDA vs FDA comparison · flags HIGH_DEVIATION,\nLOW_CONFIDENCE, MISSING lines"]
ACCT["④ Accountant Review UI\nAI-highlighted deviations overlaid on PDF\nAccept, annotate or reject each item"]
OPS["⑤ Operator Approval UI\nFinal sign-off with remarks"]
AUD["⑥ Immutable Audit Trail\nEvery decision logged with actor + timestamp"]
UP --> EXT --> DEV --> ACCT --> OPS --> AUD
end
RECV -- "replaces" --> UP
TRAD -. "eliminated by OpenDA" .-> UP
AUD --> ERP["ERP / VMS System\nWebhook push on approval"]
ERP --> FIN(["DA Settled & Archived ✓"])
flowchart TD
A[Accountant Browser\nport :3000] -->|upload PDA JSON + FDA PDF| B[FastAPI :8000\nPOST /api/v1/da/upload]
B -->|enqueue| C[Redis :6379]
C -->|async task| D[Celery Worker]
D -->|parse PDF| E[Docling]
E -->|structured text + BBoxes| D
D -->|extract JSON| F[LiteLLM Gateway]
F -->|route to active provider| G{LLM Provider}
G -->|Anthropic Claude| G
G -->|OpenAI GPT-4o| G
G -->|Google Gemini| G
G -->|Ollama local| G
F -->|FDASchema| D
D -->|compare PDA vs FDA| H[Deviation Engine]
H -->|DeviationReport| I[(PostgreSQL :5432)]
I -->|PENDING_ACCOUNTANT_REVIEW| A
A -->|review + annotate| J[PUT /submit-to-operator]
J -->|PENDING_OPERATOR_APPROVAL| K[Operator Browser\nport :3001]
K -->|approve| L[POST /approve]
L -->|webhook| M[ERP / VMS]
L -->|PUSHED_TO_ERP| I
| Tool | Version |
|---|---|
| Docker + Docker Compose | 24+ |
| Python | 3.12+ |
| UV | latest |
| Node.js | 20+ |
| pnpm | 9+ |
git clone https://github.com/your-org/openda.git
cd openda
cp .env.example .envEdit .env and set your LLM provider:
# Choose ONE provider block:
# Anthropic Claude (recommended)
LLM_MODEL=anthropic/claude-sonnet-4-6-20250514
LLM_API_KEY=sk-ant-...
# Google Gemini
# LLM_MODEL=gemini/gemini-2.0-flash
# LLM_API_KEY=AIza...
# OpenAI
# LLM_MODEL=openai/gpt-4o
# LLM_API_KEY=sk-proj-...
# Ollama (no API key needed)
# LLM_MODEL=ollama/llama3.3
# LLM_API_KEY=ollamadocker compose up --build| Service | URL |
|---|---|
| Accountant UI | http://localhost:3000 |
| Operator UI | http://localhost:3001 |
| API | http://localhost:8000 |
| API docs | http://localhost:8000/docs |
cd backend
uv venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
uv pip install -e ".[dev]"
# start postgres + redis
docker compose up -d postgres redis
# migrate
alembic upgrade head
# run API
uvicorn main:app --reload --port 8000
# run Celery worker (separate terminal)
celery -A app.workers.celery_app worker --loglevel=info# Accountant UI (port 5173, proxies /api → :8000)
cd frontend-accountant && pnpm install && pnpm dev
# Operator UI (port 5174)
cd frontend-operator && pnpm install && pnpm devOpenDA uses LiteLLM as a universal gateway. Switch providers by changing two env vars — zero code changes required.
| Provider | LLM_MODEL |
LLM_API_KEY |
|---|---|---|
| Anthropic Claude | anthropic/claude-sonnet-4-6-20250514 |
Anthropic API key |
| Google Gemini | gemini/gemini-2.0-flash |
Google AI Studio key |
| OpenAI | openai/gpt-4o |
OpenAI API key |
| Azure OpenAI | azure/gpt-4o |
Azure API key |
| Ollama (local) | ollama/llama3.3 |
ollama |
For Ollama, also set OLLAMA_API_BASE=http://localhost:11434.
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/da/upload |
Upload PDA JSON + FDA PDF, starts AI processing |
GET |
/api/v1/da/{id}/status |
Poll DA lifecycle status |
GET |
/api/v1/da/{id}/deviation-report |
Full deviation report with per-item flags |
GET |
/api/v1/da/{id}/audit-log |
Immutable audit trail |
PUT |
/api/v1/da/{id}/submit-to-operator |
Accountant submits reviewed items |
POST |
/api/v1/da/{id}/approve |
Operator approves → fires ERP webhook |
POST |
/api/v1/da/{id}/reject |
Reject at any review stage |
GET |
/api/v1/health |
Health check (DB + Redis + LLM config) |
Full interactive docs: http://localhost:8000/docs
| Flag | Condition |
|---|---|
HIGH_DEVIATION |
Absolute variance > $500 or percentage variance > 10% |
LOW_CONFIDENCE |
AI extraction confidence < 0.85 |
MISSING_PDA_LINE |
FDA item has no matching PDA category |
MISSING_FROM_FDA |
PDA item absent from FDA |
When an operator approves a DA, OpenDA fires a POST to WEBHOOK_URL with:
{
"da_id": "uuid",
"port_call_id": "PC-2024-SGSIN-0001",
"status": "APPROVED",
"total_estimated": 12500.00,
"total_actual": 13050.00,
"operator_remarks": "Anchorage fees validated against port authority receipt",
"llm_provider": "anthropic/claude-sonnet-4-6-20250514",
"approved_at": "2025-01-15T09:32:00Z"
}Set WEBHOOK_URL=http://your-erp/api/inbound/disbursement in .env.
For local testing: WEBHOOK_URL=http://localhost:8000/api/v1/da/webhook-echo
stateDiagram-v2
direction LR
[*] --> UPLOADING : accountant uploads PDA JSON + FDA PDF
UPLOADING --> AI_PROCESSING : file stored, Celery task enqueued
AI_PROCESSING --> AI_PROCESSING : retry on transient LLM failure
AI_PROCESSING --> PENDING_ACCOUNTANT_REVIEW : deviation report generated
AI_PROCESSING --> REJECTED : unrecoverable extraction error
PENDING_ACCOUNTANT_REVIEW --> PENDING_OPERATOR_APPROVAL : accountant reviews & submits
PENDING_ACCOUNTANT_REVIEW --> REJECTED : accountant rejects DA
PENDING_OPERATOR_APPROVAL --> APPROVED : operator approves
PENDING_OPERATOR_APPROVAL --> REJECTED : operator rejects
APPROVED --> PUSHED_TO_ERP : ERP webhook fired
PUSHED_TO_ERP --> [*]
REJECTED --> [*]
cd backend
# Generate synthetic test fixtures (5 PDA JSONs + 5 FDA PDFs)
python ../test_data/generate_fixtures.py
# Run schema smoke tests
python ../test_data/smoke_test_schemas.py
# Run pytest suite
pytest tests/ -vopenda/
├── backend/ # FastAPI + Celery + SQLAlchemy
│ ├── app/
│ │ ├── api/routes/ # da.py, health.py
│ │ ├── models/ # SQLAlchemy ORM models
│ │ ├── schemas/ # Pydantic v2 schemas (PDA, FDA, deviation)
│ │ ├── services/ # llm_provider, extraction_service, deviation_engine, state_machine
│ │ └── workers/ # Celery app + tasks
│ ├── alembic/ # DB migrations
│ └── main.py
├── frontend-accountant/ # React + Vite (split-screen PDF review)
├── frontend-operator/ # React + Vite (approval dashboard)
├── test_data/ # Synthetic PDA/FDA fixtures
├── docker-compose.yml
└── .env.example
- Fork → feature branch → PR against
main - Run
ruff check .andmypybefore pushing - Add tests for any new business logic in
backend/tests/
MIT — see LICENSE