Litigation prevention OS for US Food & CPG brands. Catch litigation risk hidden in customer reviews and FDA data before it reaches a courtroom.
Live: https://onto-review.vercel.app Status: MVP (active development)
A single customer complaint about an undeclared allergen, a piece of foreign material in food, or a misleading nutrition label can be the seed of a multi-million-dollar product liability or class-action suit. Most US Food & CPG SMBs find out only after a legal notice lands — by which point the FDA recall is already public and discovery is underway.
OntoReview catches that signal in the customer-review and FDA-recall feed before it becomes a lawsuit.
Customer reviews (CSV upload or sample set)
↓
Risk classification
↓
Matching to real FDA recall precedent (openFDA, live)
↓
Rule-based exposure estimate (from openFDA fields)
↓
Mitigation playbook
↓
Append-only audit trail
Implemented with an LLM risk classifier and OWL ontology reasoning over an openFDA-backed precedent layer.
Every analyzed review surfaces:
| Field | Example |
|---|---|
| Risk Category | Product Liability |
| Severity Score | 8 / 10 |
| Confidence | 0.82 |
| Matched FDA Precedent | openFDA F-2182-2017 (BEYOND MEAT undeclared peanut allergen, Class I) |
| Estimated Legal Exposure | Range $9.5M – $17.6M (rule-based, with breakdown) |
Exposure is a transparent rule-based estimate derived from real openFDA fields (recall classification, distribution scope, recall reason), shown as a range with a visible breakdown — not a single precise figure. A settlement-data-backed model is on the roadmap.
The estimator multiplies a documented per-class base by openFDA-derived factors:
- Classification base: Class I $5M · Class II $1.5M · Class III $400K
- Distribution scope: 1–2 states 1.0× · 3–10 states 1.5× · 11–25 states 2.0× · Nationwide 3.0×
- Reason category: Allergen / undeclared 1.5× · Pathogen contamination 1.4× · Foreign material 1.3× · Mislabeling 1.0×
- Range: midpoint ± 30%
Every UI surface that shows a dollar number also shows which openFDA field produced which multiplier. Missing inputs → the estimate is hidden behind a "Data insufficient" guard rather than a fabricated number.
Implementation: backend/services/exposure_estimator.py
OpenAI gpt-4o-mini serves as the primary reasoning engine with Google's gemini-2.0-flash as automatic fallback. The LLM powers the analytical components:
- Risk Classification — analyzes review text, assigns legal risk categories with severity scores (1–10) and confidence levels
- Playbook Generation — context-aware mitigation strategies per risk category and severity
- Compliance Analysis — evaluates risks against multi-jurisdiction regulations (US FDA, EU Consumer Protection, KR Fair Trade)
- Agent Response Simulation — configurable AI agents with different autonomy levels for automated risk response
Note: Exposure dollar numbers come from the rule-based estimator above, not from the LLM. The LLM is used for classification and narrative — not for picking the settlement figure.
The system uses a provider abstraction layer. Switch primary/fallback order via a single environment variable:
LLM_PROVIDER=openai # OpenAI GPT-4o-mini → Gemini fallback (production default)
LLM_PROVIDER=google # Gemini 2.0 Flash → OpenAI fallback (local dev)OpenAI failures trigger automatic Gemini fallback. In Gemini-primary mode, 429 rate-limit errors trigger backoff retries before failing over to OpenAI.
Implementation: core/utils/openai_client.py
- openFDA Food Enforcement API (real, public, no key required) — live recall pull via
POST /api/fda/recalls/ingest; curated samples served viaGET /api/fda/recalls/featured. - User-uploaded CSVs —
POST /api/data/uploadacceptsRatings/Reviewscolumns;POST /api/analysis/runanalyzes whatever the user uploaded. - Synthetic sample review sets —
GET /api/data/sample?industry=ecommercereturns 25 clearly-labeled Food/CPG synthetic reviews for demos/onboarding. - No arbitrary scraping — no Amazon/Reddit/etc. scrapers in production code. ToS and legal risk avoidance.
The UI consistently uses two tones to signal data origin:
- emerald = real / live data (openFDA pulls, user-uploaded CSVs, "Live · openFDA" badge)
- amber = sample data or rule-based estimate ("Sample review set" badge, "Estimated range" exposure pill)
Mixing the two would let a GC mistake an estimate for a settlement. Don't.
1. Risk Intelligence Dashboard — total legal exposure, critical risk count, severity distribution, openFDA-backed "Highest active litigation risk" card with breakdown popover.
2. Risk Response Playbook — AI-generated mitigation strategies tailored to each detected risk. Includes priority actions, timeline, and escalation paths.
3. Trust & Safety Audit — append-only audit log recording every scan, classification, FDA pull, and risk flag. Exportable as PDF for legal compliance (Duty of Care).
4. Domain Ontology Studio — custom OWL ontology rule editor for industry-specific risk classification.
5. Global Compliance Tracker — multi-jurisdiction regulation checking across US, EU, and KR.
6. Agent Communication Setup — configurable AI agent autonomy levels (1–5) for automated risk response, including simulation mode.
7. Per-tenant Authentication — WorkOS AuthKit with server-side code exchange. Tenant isolation is fail-closed: when AUTH_ENFORCED=true, every tenant-scoped endpoint requires a verified Bearer JWT; only an explicit allowlist (/api/health, /api/ready, /api/fda/recalls/{search,featured}) stays public.
| Layer | Technology |
|---|---|
| LLM | OpenAI GPT-4o-mini (primary) · Gemini 2.0 Flash (fallback) |
| Ontology | OWL 2 (owlready2) |
| Backend | Python, FastAPI |
| Frontend | React, Tailwind CSS |
| Database | PostgreSQL (prod) · SQLite (dev/CI only), SQLAlchemy, Alembic |
| Authentication | WorkOS AuthKit (server-side code exchange), JWKS RS256 verification with client_id + iss pins |
| External data | openFDA Food Enforcement API (public) |
| Legal precedent layer | In-memory embedding cache + cosine similarity + keyword/TF fallback (no IDF weighting yet) |
| Deployment | Vercel (frontend), Docker Compose home-server stack behind Cloudflare Tunnel (backend) |
OntoReview/
├── backend/
│ ├── main.py # FastAPI application + lifespan
│ ├── routers/ # API route handlers
│ │ ├── auth.py # WorkOS code-exchange endpoints
│ │ ├── risk.py # Risk analysis endpoints
│ │ ├── analysis.py # CSV-driven analysis
│ │ ├── data.py # Upload + sample review sets
│ │ ├── fda.py # openFDA recall ingest + featured
│ │ ├── agent.py # Agent communication setup
│ │ ├── compliance.py # Global compliance tracker
│ │ ├── studio.py # Domain ontology studio
│ │ ├── audit.py # Trust & safety audit
│ │ └── ...
│ ├── dependencies/
│ │ ├── auth.py # JWKS-cached WorkOS JWT verifier
│ │ └── tenancy.py # `current_org_id` resolver (JWT → JIT)
│ ├── middleware/
│ │ ├── auth_enforcement.py # Fail-closed gate; explicit public allowlist
│ │ ├── correlation_id.py # X-Request-ID propagation
│ │ └── rate_limit.py # Per-prefix daily caps (auth + LLM buckets)
│ ├── services/ # Business logic
│ │ ├── auth_provisioning.py # JIT user/org/membership provisioning
│ │ ├── risk_service.py # Core risk analysis (LLM-powered)
│ │ ├── exposure_estimator.py # Rule-based openFDA → $ range
│ │ ├── fda_service.py # openFDA ingest + normalization
│ │ ├── ontology_engine.py # OWL ontology reasoning
│ │ ├── legal_rag_service.py # Legal precedent matching
│ │ ├── playbook_service.py # Risk response playbook generation
│ │ └── ...
│ ├── data/
│ │ ├── legal_cases.json # 20 curated US legal precedents
│ │ ├── openfda_featured_samples.json # Real openFDA records (verbatim snapshot)
│ │ └── mock_reviews_*.json # Synthetic sample review sets
│ ├── database/
│ │ ├── database.py # Engine + SessionLocal (PG/SQLite)
│ │ └── models.py # SQLAlchemy models (audit trail)
│ └── alembic/
│ └── versions/ # 0001 multitenant · 0002 schema hardening · 0003 WorkOS mapping
├── core/
│ ├── config.py # Environment & LLM configuration
│ └── utils/
│ └── openai_client.py # Multi-provider LLM abstraction
├── frontend/
│ └── src/
│ ├── auth/
│ │ ├── session.js # sessionStorage-backed token state
│ │ └── tokenStore.js # axios interceptor bridge + refresh
│ ├── components/ # React UI components
│ │ ├── RiskIntelligence.jsx
│ │ ├── FdaHighestRiskCard.jsx # openFDA-driven hero card
│ │ ├── RealReviewAnalysisCard.jsx # Upload + sample entry
│ │ ├── RiskPlaybook.jsx
│ │ ├── AuditTimeline.jsx
│ │ └── ...
│ └── api/client.js # API client (Bearer + 401-retry)
├── tests/ # Unit + integration tests
├── CLAUDE.md # Development guide
└── requirements.txt
- Python 3.11+
- Node.js 20+
- OpenAI API key (primary LLM)
- Google AI Studio API key (Gemini fallback, optional but recommended)
# Clone
git clone https://github.com/heeoneie/OntoReview.git
cd OntoReview
# Backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Frontend
cd frontend
npm install
cd ..Create a .env file in the project root:
# Primary LLM (Required)
OPENAI_API_KEY=your_openai_key
LLM_PROVIDER=openai
# Fallback LLM (Optional but recommended for production)
GOOGLE_API_KEY=your_google_key
# openFDA API key (optional — public API works without one;
# the key only raises the per-IP rate limit)
OPENFDA_API_KEY=your_openfda_key
# ── WorkOS AuthKit (required for AUTH_ENFORCED=true) ──
# Dev default: AUTH_ENFORCED=false. Production MUST flip it on
# (see Deployment below). Frontend Vite reads VITE_* from this
# same root .env via `envDir: '..'` in `frontend/vite.config.js`.
AUTH_ENFORCED=false
WORKOS_API_KEY=sk_test_... # WorkOS dashboard → API keys
WORKOS_CLIENT_ID=client_... # WorkOS dashboard → AuthKit
WORKOS_JWKS_URL= # Optional override; defaults to
# https://api.workos.com/sso/jwks/<CLIENT_ID>
WORKOS_ORG_CLAIM_KEY=org_id # Confirmed via real-token inspection
VITE_WORKOS_CLIENT_ID=client_... # Mirror of WORKOS_CLIENT_ID
VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback# Backend (terminal 1)
uvicorn backend.main:app --reload
# Frontend (terminal 2)
cd frontend
npm run devAuthentication is enforced by AUTH_ENFORCED. The local default is
false so dev / CI / live-demo paths work without a sign-in. Any
public-internet deploy MUST set AUTH_ENFORCED=true in the service
env vars BEFORE the cutover:
| Env var | Required for prod | Source |
|---|---|---|
AUTH_ENFORCED |
yes — true |
production .env consumed by compose.yaml (verify) |
WORKOS_API_KEY |
yes | WorkOS dashboard → API keys (sk_live_…) |
WORKOS_CLIENT_ID |
yes | WorkOS dashboard → AuthKit (client_…) |
WORKOS_JWKS_URL |
optional | Defaults to https://api.workos.com/sso/jwks/<CLIENT_ID> |
RUN_MIGRATIONS_AT_STARTUP |
yes — false |
Run alembic upgrade head separately |
Frontend Vite build needs VITE_WORKOS_CLIENT_ID and
VITE_WORKOS_REDIRECT_URI baked in at build time. Without these
the /auth/login page renders the guest fallback and no Bearer is
attached to API calls — combined with AUTH_ENFORCED=true that's
an effective lock-out, so verify both pairs are in place before
promoting a build to prod.
The current sign-in flow is correct enough for MVP but must be
hardened before any enterprise B2B customer who carries legal
data. Each item below is tracked at the source with a TODO
comment pointing back here.
Token storage (XSS hardening) — frontend/src/auth/session.js:
- Move
access_tokento anhttpOnly,Secure,SameSite=Strictcookie set byPOST /api/auth/callback. XSS can no longer read it. - Move
refresh_tokento the same cookie (or a sibling cookie with a longer Max-Age). The frontend never holds either token in JS state. - Add CSRF protection: backend reads
X-CSRF-Tokenheader sourced from a non-httpOnly companion cookie (double-submit). Required because cookies fire automatically on any same-site request. - Drop
frontend/src/auth/session.js+tokenStore.jsonce tokens are cookie-only. The axios interceptor needswithCredentials: true. - Coordinate with
backend/routers/auth.py—/logoutmust clear the cookies AND calluser_management.revoke_sessionso the WorkOS session itself dies.
Org provisioning UX — backend/services/auth_provisioning.py
frontend/src/App.jsx::NeedsOrganizationPage:
- Sync
Organization.namefrom WorkOS — currently new orgs land as"Organization org_01XYZ"because the JIT path doesn't callworkos.organizations.get_organization(). An enterprise customer would see this string in the dashboard. - Replace
NeedsOrganizationPagewith a self-serve "Create your first organization" flow that callsuser_management.create_organizationand adds the current user as the first member. Today the page is a static "contact your admin" guidance screen. - Decide on an invite-link flow vs admin-only org creation for new sign-ups. Probably invite-link based on the GC target persona.
Tracked at the source: each module has a top-of-file or inline
TODO block linking back to this section.
- Source review data — upload a CSV (
POST /api/data/upload) or load the synthetic Food/CPG sample (GET /api/data/sample?industry=ecommerce). - Optionally pull live FDA recalls —
POST /api/fda/recalls/ingestmatching a brand (e.g.Beyond Meat) for live precedent grounding. - Analyze —
POST /api/analysis/runclassifies legal risks with severity scores via GPT-4o-mini. - Match — risk flags are paired with the closest real openFDA recall (
GET /api/fda/recalls/featured). - Estimate — the rule-based estimator produces a dollar RANGE per matched precedent, with rationale.
- Dashboard — Risk Intelligence shows total legal exposure, top issues, and the openFDA precedent link.
- Playbook — LLM generates risk mitigation strategies per finding.
- Audit — append-only audit trail records every classification, FDA pull, and mitigation action for compliance.
- Product Liability
- Regulatory Risk
- False Advertising
- Consumer Safety
- Class Action Risk
RiskScore = Σ(severity_i × confidence_i)
severity_i is the LLM-generated risk severity (1–10), confidence_i is classification confidence (0–1). The total legal exposure shown on the dashboard is the sum of the rule-based range midpoints described in the Exposure estimate section above.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/authorize-url |
Build the WorkOS hosted login URL for the given redirect_uri |
| POST | /api/auth/callback |
Exchange a WorkOS authorization code for {access_token, refresh_token, user, organization_id, needs_organization} |
| POST | /api/auth/refresh |
Rotate the access token using a refresh token |
| POST | /api/auth/logout |
Frontend-driven logout (no-op today; see Security TODO) |
| POST | /api/data/upload |
Upload a CSV of customer reviews (Ratings/Reviews columns) |
| GET | /api/data/sample |
Load a curated sample review set (?industry=ecommerce) |
| POST | /api/analysis/run |
Run the analysis pipeline on the most-recent uploaded/sample CSV |
| POST | /api/fda/recalls/ingest |
Pull live openFDA recalls for a brand into the analysis store |
| GET | /api/fda/recalls/featured |
Curated openFDA recalls + per-record exposure estimate |
| POST | /api/risk/demo |
Run risk analysis on built-in demo reviews |
| POST | /api/risk/playbook/generate |
Generate a risk mitigation playbook |
| GET | /api/risk/ontology/graph |
Get the OWL ontology knowledge graph |
| POST | /api/compliance/check |
Multi-jurisdiction compliance check |
| GET | /api/compliance/regulations |
List available regulations |
| POST | /api/agent/simulate |
Simulate AI agent response |
| GET | /api/audit/events |
Retrieve audit trail (filterable by action) |
| POST | /api/audit/log |
Append a user-action audit row |
| POST | /api/discovery/search |
Web discovery engine search |
| GET | /api/kpi/summary |
Dashboard KPI summary |
| GET | /api/health |
Health check |
Frontend — Vercel
Backend — Docker Compose stack (compose.yaml: api + Celery worker/beat + Postgres + Redis) on a home server, exposed through a Cloudflare Tunnel.
Secrets and deploy flags (AUTH_ENFORCED=true, WorkOS keys, LLM keys,
ALLOWED_ORIGINS) live in the server's .env, which compose.yaml
loads via env_file. DATABASE_URL (Postgres) and REDIS_URL are
injected by compose.yaml itself. Recovery steps after a host reboot
are in docs/reboot-checklist.md; backup/restore in docs/ops/backup.md.
MIT License