Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

323 Commits

Folders and files

Repository files navigation

Legal Agent

AI general counsel and litigation platform. Two agent personas handle a law firm's inbound work end to end — read the email, do the legal work, produce the document, and hand it to an attorney for review.

  • FRANK — outsourced General Counsel. Reviews, drafts, and answers questions about commercial agreements; runs KYC/AML onboarding.
  • ELARA — civil litigation. Dockets statutory deadlines for California Superior Court cases and drives the workflow as case events occur.

Everything lands in a human-in-the-loop review queue. Attorney-client privilege and malpractice exposure are the reason — no work product reaches a client without an attorney approving it, with one narrow exception (see Auto-delivery).


The two agents

They are deliberate opposites, and the split is the core design idea of the platform.

FRANK ELARA
Domain Commercial / corporate GC work California civil litigation (> $25k)
Composition ~80% LLM, thin pipeline ~80% deterministic engine, LLM at the edges
Core loop Plan → Execute → Reflect Event → step activation → deadline arithmetic
What decides the work An LLM plan, per email Versioned workflow data, never a model
Human gates Review queue (1 auto-deliver exception) 3 gates, no exceptions
Code backend/agents/frank_agent/ backend/agents/elara_agent/

ELARA's inversion is intentional: the product core is malpractice-grade docketing, and a stochastic model has no place in computing a statutory deadline. Every deadline carries its CCP citation and anchor event.

What FRANK does

Three categories, all reached from the same inbox:

Category Input Output
Review Counterparty document + tenant playbook Track-changes .docx + per-clause accept/reject cards
Draft Email facts + template + jurisdiction profile Generated .docx + statutory / consistency / gap flags
Query A question about prior agreements Synthesised answer + citations + confidence

Workflow types: nda_review, document_review, investment_review, employment_draft, term_sheet_draft, transactional, board_resolution, agreement_query, kyc_incoming, kyc_outgoing, and a general catch-all.


How a request flows

Inbound email
  ├─ mailbox poll (Gmail / Microsoft Graph via OAuth)   beat, every 2 min
  └─ provider webhook
        │
        ▼
  email_logs row  →  LLM classify  →  workflow_type + facts + urgency
        │
        ▼
  Attorney clicks "Create Matter"        routers/email_intake.py
        │
        ▼
  Celery dispatch          tasks/frank_core.py  |  tasks/elara_core.py
        │
        ▼
  Agent orchestrator                     agents/frank_agent/orchestrator.py
    Phase 1  UNDERSTAND   load email, attachments, playbook, prior history
    Phase 2  PLAN         decompose into TaskPlans with depends_on
    Phase 3  EXECUTE      run experts in dependency-ordered parallel waves
    Phase 4  REFLECT      score quality, find gaps, retry up to 2x
    Phase 5  FINALIZE     persist matter, archive, conversation memory
        │
        ▼
  Review queue  →  attorney approves  →  outbound_emails  →  sent   beat, 1 min

Every phase is checkpointed. A crashed matter resumes from its last saved phase rather than re-running completed work.

Two-tier outputs

The orchestrator LLM never sees raw attachment text or full analysis output. Experts return a compact ~150-token summary (Tier 2) for planning and reflection; the full result (Tier 1) goes straight to Postgres and S3. This is what keeps compound requests — "review these 3 NDAs and draft an offer letter" — inside a workable context window.

Auto-delivery

One exception to attorney review: a single-task NDA review where all 7 playbook dimensions return PASS and confidence is at least 0.85 may be delivered without approval. Any non-PASS outcome, any compound request, and every other workflow stays on the reviewed path. ELARA has no auto-deliver path at all.

No silent ghost-shipping

Every agent writes its raw LLM response to agent_output['llm_response'] before validating it. If validation fails, the task is marked failed, the matter does not advance, and the record stays for audit. A matter never auto-completes with no work product.


Layout

.
├── backend/            FastAPI + SQLAlchemy 2 (async) + Celery/Redis + Postgres
│   ├── agents/
│   │   ├── frank_agent/    GC orchestrator, experts, playbooks, tools
│   │   ├── elara_agent/    Litigation engine, docketing, workflow defs
│   │   ├── agent_common/   Shared LLM client, storage, memory, checkpoints
│   │   └── tests_box/      Black-box end-to-end harness
│   ├── routers/        API surface, mounted under /api/v1
│   ├── tasks/          Celery tasks (frank_core, elara_core, …) + beat/
│   ├── services/       Email ingest/parse, playbook resolution, progression
│   ├── models/         SQLAlchemy models — the contract enums live here
│   ├── alembic/        Migration chain
│   └── scripts/        Playbook seeders, template builders, smoke tests
├── frontend/           Next.js 14 App Router + Tailwind
│   └── app/(app)/      matters · documents · cases · deadlines · kyc · search
├── mail_sender/        Static dev UI to hand-craft mock inbound emails
├── start.sh            Railway entrypoint (PROCESS_TYPE=web|worker|beat)
└── start.cmd           Windows dev launcher (opens all services)

Quick start

Prerequisites: Python 3.11+, Node 18+, PostgreSQL 15+ (with pgvector), Redis 7+. Tesseract OCR is optional — without it, scanned PDFs ingest as empty text but the pipeline still runs.

# 1. Services
docker run -d --name la-pg -e POSTGRES_USER=c3 -e POSTGRES_PASSWORD=c3 \
    -e POSTGRES_DB=c3_db -p 5432:5432 pgvector/pgvector:pg16
docker run -d --name la-redis -p 6379:6379 redis:7

# 2. Backend
cd backend
python -m venv .venv && source .venv/bin/activate   # or: conda create -n c3 python=3.11
pip install -e ".[dev]"
cp .env.example .env                                # fill in the API keys
alembic upgrade head

# 3. Run (three terminals)
uvicorn main:app --reload --port 8000
celery -A core.celery_app.celery_app worker --loglevel=info
celery -A core.celery_app.celery_app beat   --loglevel=info

# 4. Frontend
cd ../frontend && npm install && npm run dev
Check URL
Liveness GET localhost:8000/health/live
Readiness (verifies DB) GET localhost:8000/health/ready
Swagger localhost:8000/api/docs
App localhost:3000

start.cmd launches everything at once on Windows, but its PROJECT_WSL path is hardcoded — edit it to match your checkout before first use.

Frontend-only work needs no backend: npm run dev:offline runs against fixtures in frontend/mock/.


Commands

Run from backend/ with the environment activated.

Task make Raw
Install make install pip install -e ".[dev]"
Migrate make migrate alembic upgrade head
New migration make revision alembic revision --autogenerate -m "..."
API make api uvicorn main:app --reload --port 8000
Worker make worker celery -A core.celery_app.celery_app worker -l info
Beat make beat celery -A core.celery_app.celery_app beat -l info
Tests make test pytest -q
Lint make lint ruff check .
Format make fmt ruff format .

Frontend, from frontend/: npm run dev · build · lint · typecheck.


Configuration

All settings are pydantic-settings in backend/core/config.py. Required fields have no defaults — a missing env var fails at startup rather than silently misbehaving.

Variable Default Notes
LLM_PRIMARY kimi kimi or claude
LLM_FALLBACK claude Must differ from primary
MOONSHOT_MODEL kimi-k3 Primary model
CLAUDE_MODEL claude-fable-5 Fallback model
EMBEDDING_MODEL text-embedding-3-large 3072 dims
MOONSHOT_API_KEY — Required
ANTHROPIC_API_KEY — Required
OPENAI_API_KEY — Required (embeddings)
AGENTS_MODE — development (SQLite + disk + keyword) or production (Postgres + S3 + pgvector)

The primary provider is tried first; on a retryable failure the fallback runs automatically. Per-service pins (NDA_ANALYZER_PROVIDER, EMAIL_PARSER_PROVIDER, …) default to auto and let you force one provider when the other misbehaves on a structured-JSON workload.

EMBEDDING_DIMENSIONS must match the pgvector column definition. Changing one without the other corrupts the ANN index silently.

Scheduled work

Beat tasks live in backend/tasks/beat/: mailbox polling (2 min), outbound drain (1 min), SLA escalation (5 min), ELARA docket sweep (hourly), KYC chase/refresh/retention, and NDA expiry (nightly). Every task module must be named in core/celery_app.py::include or the worker raises "Received unregistered task" at runtime.

Multi-tenancy

The schema is Tenant → Matter → Document → Task, and a tenant is the client — one tenant per fund or company, no separate clients table. Each tenant has its own FRANK and ELARA mailbox addresses; the receiving address binds an inbound email to a tenant.

The platform is cross-tenant for every signed-in lawyer. There is no X-Tenant-Id header. List endpoints default to all tenants and accept an optional ?tenant_id=; resource-scoped endpoints derive the tenant from the row itself; /tenants/{tenant_id}/... paths are their own source of truth.


Tests

pytest -q                                     # full suite
pytest tests/test_smoke.py -q                 # import + wiring sanity
pytest agents/frank_agent/tests/test_orchestrator.py -q

Tests run without infrastructure — tests/conftest.py stubs the required env vars and no-ops the DB ping in the FastAPI lifespan, so wiring tests need neither Postgres nor Redis. A test that genuinely needs a database opts in via its own fixture. asyncio_mode = "auto", so no @pytest.mark.asyncio.

End-to-end runs make real LLM calls and cost money:

python -m agents.frank_agent.tests.real_e2e_runner --mode orchestrator --all --parallel 5

49 cases across 8 categories — standard workflows, compound requests, edge cases, boundaries, general knowledge, multi-turn conversation, adversarial stress (including prompt injection), and KYC.


Deployment

Railway, three services from this repo differing only by PROCESS_TYPE:

PROCESS_TYPE Runs
web alembic upgrade head, seeds court holidays, then uvicorn
worker Celery worker
beat Celery beat

Set the start command to bash start.sh on all three and share the env vars through one Railway variable group. Only web migrates — it is reliably first to boot.


Further reading

Doc Covers
backend/agents/frank_agent/README.md Orchestration loop, experts, checkpointing, two-tier outputs, test matrix
backend/agents/elara_agent/README.md Deterministic docketing engine, human gates, workflow definitions
/api/docs Live OpenAPI reference

Adding a FRANK workflow

  1. Add the value to WorkflowType in backend/models/models.py.
  2. Teach services/email_parser.py to classify it.
  3. Add a playbook under agents/frank_agent/playbooks/ and a resolver branch in services/playbook_resolver.py.
  4. Route it to an expert in agents/frank_agent/experts/ — reuse ReviewExpert / DraftExpert / QueryExpert where the shape matches.
  5. Add the SLA branch in services/workflow_progression.py.
  6. Add a seed script in backend/scripts/.
  7. (Optional) Add an FE panel in frontend/app/(app)/documents/[id]/page.tsx. If the output mirrors the review/draft shape, the default panel handles it.

Skip the expert routing and the workflow falls through to GeneralExpert, which will produce something — but not the specialised work product you intended. Check the dispatch table before assuming a new type is wired.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages