diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 0000000..2005426 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,34 @@ +.gcloudignore +.git/ +.gitignore +# Python virtual environments — the WinError 1920 crash source +.venv/ +venv/ +pdf_env/ +__pycache__/ +*.pyc +*.pyd +*.egg-info/ +# Large data files +data/whetstone.db +data/raw/ +data/cache/ +*.parquet +*.sqlite +*.db +# Dev-only +.claude/ +.vscode/ +.idea/ +# Team files (not needed in container) +合同模版文件库/ +thesis/ +outputs/ +scripts/ +tests/ +0608/ +W2/ +.kaggle_ref/ +# Docs +*.md +*.pdf diff --git a/.gitignore b/.gitignore index 33392de..ca82575 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ *.pdf # exceptions — code files that happen to use .txt extension !backend/requirements.txt +# README is the only tracked .md (shown on GitHub project page) +!README.md # secrets .env diff --git a/README.md b/README.md new file mode 100644 index 0000000..c368b7b --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# ⚔️ Whetstone — Legal Training Dojo + +> **Cambridge Hack the Law 2026** · Legora Challenge: *The Sparring Room* + + + +A flight simulator for junior lawyers. Three AI-driven arenas where the opponent does NOT fold just because you sound confident. + +--- + +## What It Does + +Junior lawyers learn by doing — but real clients are expensive mistakes. Whetstone gives them infinite reps against an AI adversary that: + +- Guards a **hidden reservation value** it will not reveal +- Blocks unearned concessions via a **concession gate** (independent referee LLM) +- Adapts difficulty to your historical performance (Beginner → Standard → Advanced) +- Scores every response with an **evidence-anchored rubric** — every verdict cites the exact turn and quote + +--- + +## Three Arenas + +| Arena | AI Role | Legal Focus | +|---|---|---| +| ⚖️ **Negotiation Table** | Opposing Counsel | UK M&A indemnity cap / SaaS liability cap (English law) | +| 🔥 **Hot Seat** | Senior Partner | GDPR Article 33 — breach notification to ICO | +| 🗣️ **Difficult Client** | Hostile Client | CDPA 1988 s.11(1) — commissioned works & IP ownership | + +--- + +## Features + +- **Intro Animation** — branded video splash screen on load, Skip › button, graceful fallback if video unavailable +- **3 Personas** — Hardball / Slippery / Stone Wall, each with distinct pressure tactics +- **Adaptive Difficulty** — Level 1–3 auto-set from score history +- **Voice Mode** — Web Speech API, `lang='en-GB'`, mic button in chat +- **Turning-Point Replay** — see the exact exchange where the round turned, with a stronger-play suggestion +- **PII Redaction** — Microsoft Presidio + regex fallback, human-in-the-loop approve/reject UI +- **Legal Corpus** — SEC EDGAR full-text search + CUAD dataset + EU Cellar API (GDPR, IP Directive) +- **Red/Blue Team Debate** — two AI lawyers debate; you identify the planted flaw +- **Progress Dashboard** — radial charts, sparklines, streak counter, coaching notes + +--- + +## Tech Stack + +``` +Frontend React 18 (CDN) · Tailwind CSS · Web Speech API +Backend FastAPI · Python 3.13 · SQLite (WAL mode) +AI Models Claude Sonnet 4.6 (adversary) — OpenRouter + Gemini 2.0 Flash (judge/scoring) — Vertex AI (GCP) +Data SEC EDGAR EFTS · CUAD (HuggingFace) · EU Cellar SPARQL API +Safety Microsoft Presidio PII redaction +Deployment Google Cloud Run (europe-west2) · BigQuery corpus table +``` + +--- + +## Run Locally + +```bash +git clone https://github.com/Vector897/whetstone +cd whetstone + +# Create venv and install dependencies +python -m venv .venv +.venv\Scripts\activate # Windows +pip install -r backend/requirements.txt + +# Configure API keys +copy .env.example .env +# Edit .env: set OPENROUTER_API_KEY + +# Start server (frontend + backend in one) +uvicorn app.main:app --app-dir backend --host 127.0.0.1 --port 8000 +# Open: http://localhost:8000 +``` + +--- + +## Deploy to Google Cloud Run + +```bash +# From D:\Whetstone — run from project root to avoid WinError 1920 +cd D:\Whetstone +gcloud config set project whetstone-500713 +gcloud run deploy whetstone \ + --source . --port 8080 --allow-unauthenticated \ + --region europe-west2 \ + --set-env-vars="OPENROUTER_API_KEY=YOUR_KEY,GCP_PROJECT=whetstone-500713,VERTEX_LOCATION=europe-west2" +``` + +--- + +## Hackathon Compliance + +| Requirement | Status | +|---|---| +| MUST 1 — Real legal scenario (UK IP + Data Privacy) | ✅ GDPR Art 33 · CDPA 1988 · English law M&A | +| MUST 2 — AI as adversary, not assistant | ✅ Hidden reservation value · concession gate referee | +| MUST 3 — Objective scoring with specific feedback | ✅ 5-dimension analytic rubric, evidence-anchored per turn | +| BONUS — Custom UI | ✅ Dark dojo theme, fixed sidebar | +| BONUS — Persona selector | ✅ Hardball / Slippery / Stone Wall | +| BONUS — Adaptive difficulty | ✅ 3 levels computed from score history | +| BONUS — Progress tracking | ✅ SQLite persistent, radial charts + sparklines | +| BONUS — Replay turning point | ✅ Modal with transcript + stronger-play coaching | +| BONUS — Voice mode | ✅ Web Speech API, en-GB | +| BONUS — Intro animation | ✅ Video splash screen with Skip › fallback | +| GCP — Cloud Run deployment | ✅ `europe-west2`, public URL | +| GCP — Vertex AI Gemini 2.0 Flash | ✅ Judge / scoring / referee calls | +| GCP — BigQuery corpus | ✅ `whetstone.corpus` table (SEC EDGAR + CUAD clauses) | + +--- + +*Cambridge Hack the Law 2026 · Team Whetstone* diff --git a/backend/app/db.py b/backend/app/db.py index 9bbd037..71faeb1 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -32,6 +32,16 @@ def init() -> None: persona TEXT DEFAULT 'Hardball', difficulty INTEGER DEFAULT 2 ); + """) + # Migrate existing DBs that predate the persona/difficulty columns + for col, coldef in [("persona", "TEXT DEFAULT 'Hardball'"), + ("difficulty", "INTEGER DEFAULT 2")]: + try: + _conn().execute(f"ALTER TABLE sessions ADD COLUMN {col} {coldef}") + _conn().commit() + except Exception: + pass # column already exists + _conn().executescript(""" CREATE TABLE IF NOT EXISTS progress ( lawyer_id TEXT NOT NULL, dimension TEXT NOT NULL, diff --git a/backend/app/main.py b/backend/app/main.py index caa882f..e68c8ae 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, Response from pydantic import BaseModel from . import config, state, db, llm @@ -47,6 +47,22 @@ def index(): return FileResponse(FRONTEND) +@app.get("/intro.mp4") +def intro_video(): + path = FRONTEND.parent / "intro.mp4" + if path.exists(): + return FileResponse(str(path), media_type="video/mp4") + return Response(status_code=404) + + +@app.get("/demo.png") +def demo_image(): + path = FRONTEND.parent / "demo.png" + if path.exists(): + return FileResponse(str(path), media_type="image/png") + return Response(status_code=404) + + # ── Health & meta ───────────────────────────────────────────────────────────── @app.get("/api/health") diff --git a/frontend/demo.png b/frontend/demo.png new file mode 100644 index 0000000..6d547bf Binary files /dev/null and b/frontend/demo.png differ diff --git a/frontend/index.html b/frontend/index.html index 18ff390..bda914e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -34,11 +34,21 @@ #ls{position:fixed;inset:0;background:#070c18;display:flex;align-items:center;justify-content:center;z-index:999;transition:opacity .4s} #ls.out{opacity:0;pointer-events:none} .pulse-dot::after{content:'';position:absolute;inset:0;border-radius:50%;border:2px solid #ef4444;animation:pulse-ring 1.4s ease infinite} +#vi{position:fixed;inset:0;z-index:1001;background:#070c18;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity .5s} +#vi.out{opacity:0;pointer-events:none} +#vi video{max-width:100%;max-height:90vh;object-fit:contain;border-radius:12px} +#vi-skip{position:absolute;bottom:32px;right:32px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);color:#e2e8f4;font-family:'Inter',sans-serif;font-size:.8rem;font-weight:600;padding:8px 18px;border-radius:40px;cursor:pointer;transition:background .2s} +#vi-skip:hover{background:rgba(255,255,255,.16)}
-