Complete battle mode+deal negotiation added
Browse files- .env.example +0 -31
- .gitignore +10 -0
- docs/BACKEND_API.md +0 -393
- docs/CLAUDE_PROJECT_CONTEXT.md +0 -299
- docs/DEMO_NOTES.md +0 -48
- docs/DOCUMENTATION.md +0 -1447
- docs/FIELD_NOTES.md +0 -92
- docs/MODELS_FINAL.md +0 -434
- docs/NEMOTRON_OMNI_AUDIO.md +0 -294
- docs/PHASE_WISE_PLAN.md +0 -478
- docs/PROMPTS.md +0 -155
- docs/TASK_TRACKER.md +0 -105
- scripts/test_claim_based_scoring.py +0 -336
- scripts/test_nvidia_client.py +0 -90
- scripts/test_phase3_pitch_battle.py +0 -123
- scripts/test_phase3b_battle_flow.py +0 -217
- scripts/test_phase4_battle_stability.py +0 -261
- scripts/test_phase5_scorecard.py +0 -329
- scripts/test_phase5b_refinement.py +0 -240
- tests/test_api_handlers.py +0 -52
- tests/test_json_parser.py +0 -38
- tests/test_prompts.py +0 -28
.env.example
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
# Copy to .env locally; use HF Space Secrets in deployment.
|
| 2 |
-
# Frontend never reads these — backend model_router only.
|
| 3 |
-
APP_ENV=development
|
| 4 |
-
MAX_ROUNDS=6
|
| 5 |
-
|
| 6 |
-
DEFAULT_MODEL_MODE=premium_nvidia
|
| 7 |
-
|
| 8 |
-
# NVIDIA Nemotron 3 Nano Omni 30B-A3B (primary premium model — backend-only API)
|
| 9 |
-
NVIDIA_API_KEY=
|
| 10 |
-
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 11 |
-
NVIDIA_OMNI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning
|
| 12 |
-
|
| 13 |
-
# OpenBMB MiniCPM models
|
| 14 |
-
OPENBMB_API_KEY=
|
| 15 |
-
OPENBMB_BASE_URL=
|
| 16 |
-
MINICPM_OMNI_MODEL=openbmb/MiniCPM-o-4_5
|
| 17 |
-
MINICPM_TEXT_MODEL=openbmb/MiniCPM5-1B
|
| 18 |
-
MINICPM_TEXT_BACKEND=api_or_local
|
| 19 |
-
MINICPM_VISION_MODEL=openbmb/MiniCPM-V-4.6
|
| 20 |
-
|
| 21 |
-
# Hugging Face (model download / Space)
|
| 22 |
-
HF_TOKEN=
|
| 23 |
-
|
| 24 |
-
# Audio transcription fallback only (not the primary judge)
|
| 25 |
-
WHISPER_FALLBACK_ENABLED=true
|
| 26 |
-
WHISPER_MODEL_SIZE=tiny
|
| 27 |
-
|
| 28 |
-
# Feature flags
|
| 29 |
-
ENABLE_DECK_CRITIQUE=true
|
| 30 |
-
ENABLE_DEAL_BATTLE=true
|
| 31 |
-
ENABLE_VOICE_MODE=true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -41,3 +41,13 @@ models/
|
|
| 41 |
*.gguf
|
| 42 |
*.wav
|
| 43 |
*.mp3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
*.gguf
|
| 42 |
*.wav
|
| 43 |
*.mp3
|
| 44 |
+
|
| 45 |
+
# Local-only folders
|
| 46 |
+
docs/
|
| 47 |
+
scripts/
|
| 48 |
+
tests/
|
| 49 |
+
|
| 50 |
+
# Environment files
|
| 51 |
+
.env
|
| 52 |
+
.env.*
|
| 53 |
+
.env.example
|
docs/BACKEND_API.md
DELETED
|
@@ -1,393 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Backend API Reference
|
| 2 |
-
|
| 3 |
-
## 1. Important Note About Gradio Internal Endpoints
|
| 4 |
-
|
| 5 |
-
When you run `python app.py` and open FastAPI/Swagger (`/docs`), you will see **many routes** that are **not** PitchFight AI product APIs, for example:
|
| 6 |
-
|
| 7 |
-
- `/gradio_api/*` — config, queue, upload, streaming
|
| 8 |
-
- `/queue/*` — Gradio job queue
|
| 9 |
-
- `/upload` — Gradio file upload
|
| 10 |
-
- `/static/*`, `/assets/*`, `/theme.css` — Gradio UI assets
|
| 11 |
-
- Login, monitoring, and other framework runtime routes
|
| 12 |
-
|
| 13 |
-
These are **automatically registered by Gradio** and are required for Gradio Server to work on Hugging Face Spaces. **Do not delete them.**
|
| 14 |
-
|
| 15 |
-
**PitchFight AI’s real backend endpoints** are namespaced under **`/api/...`** plus **`/health`** and **`/`**.
|
| 16 |
-
|
| 17 |
-
> Judge this project’s API design by `/api/*` and this document — not by the long Gradio-generated OpenAPI list.
|
| 18 |
-
|
| 19 |
-
---
|
| 20 |
-
|
| 21 |
-
## 2. Backend Design Principles
|
| 22 |
-
|
| 23 |
-
| Principle | Detail |
|
| 24 |
-
|---|---|
|
| 25 |
-
| **Frontend never calls model providers** | No NVIDIA/OpenBMB keys in browser JS; only `/api/*` + `/health` |
|
| 26 |
-
| **Backend owns model routing** | `core/model_router.py` (Phase 2+) — default `premium_nvidia` (Nemotron Omni 30B-A3B) |
|
| 27 |
-
| **Backend owns session state** | `core/session_manager.py` |
|
| 28 |
-
| **Backend owns scoring** | `core/scoring_engine.py` |
|
| 29 |
-
| **Frontend sends actions, renders results** | Custom HTML/CSS/JS + `fetch()` |
|
| 30 |
-
| **Secrets in env only** | `.env` locally, HF Space Secrets in deployment — never in frontend or repo |
|
| 31 |
-
|
| 32 |
-
> **Strategy:** High-demo sponsor-model build. Off-the-Grid is not targeted. faster-whisper is transcription fallback only; OpenBMB MiniCPM modes are secondary/fallback paths.
|
| 33 |
-
|
| 34 |
-
Shared logic lives in **`core/api_handlers.py`**. Both REST routes (`/api/...`) and Gradio `@app.api` wrappers call the same handler functions.
|
| 35 |
-
|
| 36 |
-
---
|
| 37 |
-
|
| 38 |
-
## 3. Clean Project Endpoints
|
| 39 |
-
|
| 40 |
-
| Method | Path | Status | Purpose |
|
| 41 |
-
|---|---|---|---|
|
| 42 |
-
| `GET` | `/health` | **Implemented** | App health check |
|
| 43 |
-
| `GET` | `/` | **Implemented** | Serve custom frontend |
|
| 44 |
-
| `GET` | `/api/model-health` | **Implemented** (Phase 2) | Model provider config status (no keys) |
|
| 45 |
-
| `POST` | `/api/load-sample` | **Implemented** | Load EventRadar AI demo startup |
|
| 46 |
-
| `POST` | `/api/start-session` | **Implemented** | Start Pitch Battle session |
|
| 47 |
-
| `POST` | `/api/chat-round` | **Implemented** | Send user answer, get AI pushback |
|
| 48 |
-
| `POST` | `/api/end-battle` | **Implemented** | Generate scorecard |
|
| 49 |
-
| `POST` | `/api/reset-session` | **Implemented** | Delete session |
|
| 50 |
-
| `POST` | `/api/voice-pitch` | **Placeholder** | Voice pitch (reserved) |
|
| 51 |
-
| `POST` | `/api/start-deal-session` | **Placeholder** | Deal Battle (reserved) |
|
| 52 |
-
| `POST` | `/api/deck-critique` | **Placeholder** | Pitch deck critique (reserved) |
|
| 53 |
-
|
| 54 |
-
### Gradio compatibility wrappers (same handlers)
|
| 55 |
-
|
| 56 |
-
| Gradio `@app.api` name | Handler |
|
| 57 |
-
|---|---|
|
| 58 |
-
| `load_sample` | `handle_load_sample()` |
|
| 59 |
-
| `start_session` | `handle_start_session()` |
|
| 60 |
-
| `chat_round` | `handle_chat_round()` |
|
| 61 |
-
| `end_battle` | `handle_end_battle()` |
|
| 62 |
-
| `reset_session` | `handle_reset_session()` |
|
| 63 |
-
|
| 64 |
-
---
|
| 65 |
-
|
| 66 |
-
## 4. Endpoint Details
|
| 67 |
-
|
| 68 |
-
### `GET /health`
|
| 69 |
-
|
| 70 |
-
**Purpose:** Verify the server is running.
|
| 71 |
-
|
| 72 |
-
**Response:**
|
| 73 |
-
|
| 74 |
-
```json
|
| 75 |
-
{
|
| 76 |
-
"status": "ok",
|
| 77 |
-
"app": "PitchFight AI",
|
| 78 |
-
"version": "0.1.0"
|
| 79 |
-
}
|
| 80 |
-
```
|
| 81 |
-
|
| 82 |
-
---
|
| 83 |
-
|
| 84 |
-
### `GET /api/model-health`
|
| 85 |
-
|
| 86 |
-
**Purpose:** Return model provider configuration status. API keys are **never** included in the response.
|
| 87 |
-
|
| 88 |
-
**Response:**
|
| 89 |
-
|
| 90 |
-
```json
|
| 91 |
-
{
|
| 92 |
-
"default_mode": "premium_nvidia",
|
| 93 |
-
"supported_modes": ["openbmb_omni", "premium_nvidia", "tiny_minicpm", "vision_deck", "whisper_fallback"],
|
| 94 |
-
"providers": {
|
| 95 |
-
"nvidia": {
|
| 96 |
-
"provider": "nvidia",
|
| 97 |
-
"configured": true,
|
| 98 |
-
"base_url": "https://integrate.api.nvidia.com/v1",
|
| 99 |
-
"model": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
|
| 100 |
-
"api_key_present": true,
|
| 101 |
-
"message": "NVIDIA client ready"
|
| 102 |
-
},
|
| 103 |
-
"minicpm": { "status": "not_configured", "provider": "openbmb", "message": "MiniCPM integration planned for Phase 9" },
|
| 104 |
-
"vision": { "status": "not_configured", "provider": "openbmb", "message": "Vision/deck critique planned for Phase 10" },
|
| 105 |
-
"transcription": { "status": "not_configured", "provider": "local", "message": "faster-whisper fallback planned for Phase 7" }
|
| 106 |
-
}
|
| 107 |
-
}
|
| 108 |
-
```
|
| 109 |
-
|
| 110 |
-
---
|
| 111 |
-
|
| 112 |
-
### `GET /`
|
| 113 |
-
|
| 114 |
-
**Purpose:** Serve `frontend/index.html` (custom battle arena UI).
|
| 115 |
-
|
| 116 |
-
Static assets: `/frontend/styles.css`, `/frontend/script.js`, `/frontend/assets/*`
|
| 117 |
-
|
| 118 |
-
---
|
| 119 |
-
|
| 120 |
-
### `POST /api/load-sample`
|
| 121 |
-
|
| 122 |
-
**Purpose:** Load the EventRadar AI sample startup into the form.
|
| 123 |
-
|
| 124 |
-
**Request body:** none
|
| 125 |
-
|
| 126 |
-
**Response:**
|
| 127 |
-
|
| 128 |
-
```json
|
| 129 |
-
{
|
| 130 |
-
"startup": {
|
| 131 |
-
"name": "EventRadar AI",
|
| 132 |
-
"problem": "...",
|
| 133 |
-
"target_users": "...",
|
| 134 |
-
"solution": "...",
|
| 135 |
-
"why_ai": "...",
|
| 136 |
-
"competitors": "...",
|
| 137 |
-
"traction": "...",
|
| 138 |
-
"ask": "..."
|
| 139 |
-
}
|
| 140 |
-
}
|
| 141 |
-
```
|
| 142 |
-
|
| 143 |
-
---
|
| 144 |
-
|
| 145 |
-
### `POST /api/start-session`
|
| 146 |
-
|
| 147 |
-
**Purpose:** Start a new Pitch Battle session.
|
| 148 |
-
|
| 149 |
-
**Request body:**
|
| 150 |
-
|
| 151 |
-
```json
|
| 152 |
-
{
|
| 153 |
-
"mode": "pitch_battle",
|
| 154 |
-
"persona": "hackathon_judge",
|
| 155 |
-
"difficulty": "high",
|
| 156 |
-
"input_mode": "text",
|
| 157 |
-
"model_mode": "premium_nvidia",
|
| 158 |
-
"startup": {
|
| 159 |
-
"name": "EventRadar AI",
|
| 160 |
-
"problem": "...",
|
| 161 |
-
"target_users": "...",
|
| 162 |
-
"solution": "...",
|
| 163 |
-
"why_ai": "...",
|
| 164 |
-
"competitors": "...",
|
| 165 |
-
"traction": "...",
|
| 166 |
-
"ask": "..."
|
| 167 |
-
}
|
| 168 |
-
}
|
| 169 |
-
```
|
| 170 |
-
|
| 171 |
-
**Response:**
|
| 172 |
-
|
| 173 |
-
```json
|
| 174 |
-
{
|
| 175 |
-
"session_id": "uuid",
|
| 176 |
-
"round": 1,
|
| 177 |
-
"pressure_level": "Medium",
|
| 178 |
-
"attack_tag": "User Pain",
|
| 179 |
-
"ai_message": "Students already lurk in WhatsApp groups..."
|
| 180 |
-
}
|
| 181 |
-
```
|
| 182 |
-
|
| 183 |
-
---
|
| 184 |
-
|
| 185 |
-
### `POST /api/chat-round`
|
| 186 |
-
|
| 187 |
-
**Purpose:** Submit one founder answer and receive the next AI challenge.
|
| 188 |
-
|
| 189 |
-
**Request body:**
|
| 190 |
-
|
| 191 |
-
```json
|
| 192 |
-
{
|
| 193 |
-
"session_id": "uuid",
|
| 194 |
-
"user_message": "We use AI to rank events for students."
|
| 195 |
-
}
|
| 196 |
-
```
|
| 197 |
-
|
| 198 |
-
**Response:**
|
| 199 |
-
|
| 200 |
-
```json
|
| 201 |
-
{
|
| 202 |
-
"session_id": "uuid",
|
| 203 |
-
"round": 2,
|
| 204 |
-
"pressure_level": "Medium",
|
| 205 |
-
"attack_tag": "Demo Clarity",
|
| 206 |
-
"ai_message": "If I only saw a 30-second demo..."
|
| 207 |
-
}
|
| 208 |
-
```
|
| 209 |
-
|
| 210 |
-
---
|
| 211 |
-
|
| 212 |
-
### `POST /api/end-battle`
|
| 213 |
-
|
| 214 |
-
**Purpose:** End the battle and return a scorecard.
|
| 215 |
-
|
| 216 |
-
**Request body:**
|
| 217 |
-
|
| 218 |
-
```json
|
| 219 |
-
{
|
| 220 |
-
"session_id": "uuid"
|
| 221 |
-
}
|
| 222 |
-
```
|
| 223 |
-
|
| 224 |
-
**Response:**
|
| 225 |
-
|
| 226 |
-
```json
|
| 227 |
-
{
|
| 228 |
-
"overall": 68,
|
| 229 |
-
"scores": {
|
| 230 |
-
"clarity": {
|
| 231 |
-
"score": 64,
|
| 232 |
-
"reason": "...",
|
| 233 |
-
"quote": "..."
|
| 234 |
-
}
|
| 235 |
-
},
|
| 236 |
-
"best_answer": "...",
|
| 237 |
-
"weakest_answer": "...",
|
| 238 |
-
"improved_answer": "...",
|
| 239 |
-
"improved_pitch": "...",
|
| 240 |
-
"top_3_questions": ["...", "...", "..."]
|
| 241 |
-
}
|
| 242 |
-
```
|
| 243 |
-
|
| 244 |
-
---
|
| 245 |
-
|
| 246 |
-
### `POST /api/reset-session`
|
| 247 |
-
|
| 248 |
-
**Purpose:** Delete a session.
|
| 249 |
-
|
| 250 |
-
**Request body:**
|
| 251 |
-
|
| 252 |
-
```json
|
| 253 |
-
{
|
| 254 |
-
"session_id": "uuid"
|
| 255 |
-
}
|
| 256 |
-
```
|
| 257 |
-
|
| 258 |
-
**Response:**
|
| 259 |
-
|
| 260 |
-
```json
|
| 261 |
-
{
|
| 262 |
-
"status": "reset"
|
| 263 |
-
}
|
| 264 |
-
```
|
| 265 |
-
|
| 266 |
-
---
|
| 267 |
-
|
| 268 |
-
### `POST /api/voice-pitch` (placeholder)
|
| 269 |
-
|
| 270 |
-
**Planned flow (Phase 7):** audio → Nemotron Omni (primary) → pitch context + opening question. Fallback: faster-whisper transcription → Nemotron or MiniCPM5-1B text path.
|
| 271 |
-
|
| 272 |
-
**Response (current):**
|
| 273 |
-
|
| 274 |
-
```json
|
| 275 |
-
{
|
| 276 |
-
"status": "not_implemented",
|
| 277 |
-
"message": "Voice Mode endpoint is reserved. Primary path: Nemotron Omni voice; fallback: faster-whisper transcription."
|
| 278 |
-
}
|
| 279 |
-
```
|
| 280 |
-
|
| 281 |
-
---
|
| 282 |
-
|
| 283 |
-
### `POST /api/start-deal-session` (placeholder)
|
| 284 |
-
|
| 285 |
-
**Response:**
|
| 286 |
-
|
| 287 |
-
```json
|
| 288 |
-
{
|
| 289 |
-
"status": "not_implemented",
|
| 290 |
-
"message": "Deal Battle endpoint is reserved and will be connected in a later phase."
|
| 291 |
-
}
|
| 292 |
-
```
|
| 293 |
-
|
| 294 |
-
---
|
| 295 |
-
|
| 296 |
-
### `POST /api/deck-critique` (placeholder)
|
| 297 |
-
|
| 298 |
-
**Response:**
|
| 299 |
-
|
| 300 |
-
```json
|
| 301 |
-
{
|
| 302 |
-
"status": "not_implemented",
|
| 303 |
-
"message": "Deck critique endpoint is reserved and will be connected after MiniCPM-V vision integration."
|
| 304 |
-
}
|
| 305 |
-
```
|
| 306 |
-
|
| 307 |
-
---
|
| 308 |
-
|
| 309 |
-
## 5. Endpoint Flow Diagrams
|
| 310 |
-
|
| 311 |
-
### Pitch Battle flow
|
| 312 |
-
|
| 313 |
-
```mermaid
|
| 314 |
-
flowchart TD
|
| 315 |
-
A[Frontend UI] --> B[/api/start-session]
|
| 316 |
-
B --> C[Session Manager]
|
| 317 |
-
C --> D[Persona Builder]
|
| 318 |
-
D --> E[Attack Tag Selector]
|
| 319 |
-
E --> F[Model Router]
|
| 320 |
-
F --> G[AI Response]
|
| 321 |
-
G --> A
|
| 322 |
-
```
|
| 323 |
-
|
| 324 |
-
### Scorecard flow
|
| 325 |
-
|
| 326 |
-
```mermaid
|
| 327 |
-
flowchart TD
|
| 328 |
-
A[Frontend UI] --> B[/api/end-battle]
|
| 329 |
-
B --> C[Session Manager]
|
| 330 |
-
C --> D[Scoring Engine]
|
| 331 |
-
D --> E[Model Router]
|
| 332 |
-
E --> F[JSON Parser]
|
| 333 |
-
F --> G[Scorecard Response]
|
| 334 |
-
G --> A
|
| 335 |
-
```
|
| 336 |
-
|
| 337 |
-
---
|
| 338 |
-
|
| 339 |
-
## 6. Why Swagger Shows Many Routes
|
| 340 |
-
|
| 341 |
-
Gradio Server is built on FastAPI. When the app launches, Gradio **automatically registers** internal endpoints for:
|
| 342 |
-
|
| 343 |
-
- App configuration (`/gradio_api/config`, `/gradio_api/info`)
|
| 344 |
-
- Request queueing (`/queue/join`, `/queue/data`)
|
| 345 |
-
- File uploads (`/upload`)
|
| 346 |
-
- Static assets and themes
|
| 347 |
-
- Streaming, login checks, and monitoring
|
| 348 |
-
|
| 349 |
-
These are **framework runtime routes**, not hand-written PitchFight product APIs. They exist so Gradio can host the app on Hugging Face Spaces with queuing, MCP, and ZeroGPU support.
|
| 350 |
-
|
| 351 |
-
**You do not call or maintain those routes manually.** The custom frontend uses only `/api/*` and `/health`.
|
| 352 |
-
|
| 353 |
-
---
|
| 354 |
-
|
| 355 |
-
## 7. Final API Contract — Quick Examples
|
| 356 |
-
|
| 357 |
-
### Load sample
|
| 358 |
-
|
| 359 |
-
```bash
|
| 360 |
-
curl -X POST http://127.0.0.1:7860/api/load-sample
|
| 361 |
-
```
|
| 362 |
-
|
| 363 |
-
### Start session
|
| 364 |
-
|
| 365 |
-
```bash
|
| 366 |
-
curl -X POST http://127.0.0.1:7860/api/start-session \
|
| 367 |
-
-H "Content-Type: application/json" \
|
| 368 |
-
-d '{"mode":"pitch_battle","persona":"hackathon_judge","difficulty":"high","input_mode":"text","model_mode":"premium_nvidia","startup":{"name":"Test","problem":"p","target_users":"u","solution":"s","why_ai":"a","competitors":"c","traction":"t","ask":"a"}}'
|
| 369 |
-
```
|
| 370 |
-
|
| 371 |
-
### Chat round
|
| 372 |
-
|
| 373 |
-
```bash
|
| 374 |
-
curl -X POST http://127.0.0.1:7860/api/chat-round \
|
| 375 |
-
-H "Content-Type: application/json" \
|
| 376 |
-
-d '{"session_id":"<uuid>","user_message":"We use AI for ranking."}'
|
| 377 |
-
```
|
| 378 |
-
|
| 379 |
-
### End battle
|
| 380 |
-
|
| 381 |
-
```bash
|
| 382 |
-
curl -X POST http://127.0.0.1:7860/api/end-battle \
|
| 383 |
-
-H "Content-Type: application/json" \
|
| 384 |
-
-d '{"session_id":"<uuid>"}'
|
| 385 |
-
```
|
| 386 |
-
|
| 387 |
-
### Reset session
|
| 388 |
-
|
| 389 |
-
```bash
|
| 390 |
-
curl -X POST http://127.0.0.1:7860/api/reset-session \
|
| 391 |
-
-H "Content-Type: application/json" \
|
| 392 |
-
-d '{"session_id":"<uuid>"}'
|
| 393 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/CLAUDE_PROJECT_CONTEXT.md
DELETED
|
@@ -1,299 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Claude Project Context
|
| 2 |
-
|
| 3 |
-
> **Read this file before starting any implementation phase.**
|
| 4 |
-
> Generated after full repository inspection on 2026-06-08.
|
| 5 |
-
|
| 6 |
-
---
|
| 7 |
-
|
| 8 |
-
## 1. Project Summary
|
| 9 |
-
|
| 10 |
-
PitchFight AI is a voice-and-text AI sparring arena for student founders built for the Hugging Face **Build Small Hackathon** (Backyard AI track). The core use case: let founders get grilled by a realistic AI judge before they face real judges, mentors, investors, or sponsors. The AI does not give advice — it applies pressure, remembers prior answers, attacks vague claims, and scores the conversation.
|
| 11 |
-
|
| 12 |
-
The project runs on **Gradio Server** (not default Gradio UI), hosts a **custom HTML/CSS/JS frontend**, and exposes a clean **`/api/*` REST layer**. All AI model calls are strictly backend-only. The frontend never touches any model provider API.
|
| 13 |
-
|
| 14 |
-
---
|
| 15 |
-
|
| 16 |
-
## 2. Current Strategy
|
| 17 |
-
|
| 18 |
-
| Decision | Value |
|
| 19 |
-
|---|---|
|
| 20 |
-
| Hackathon track | Backyard AI |
|
| 21 |
-
| Off-the-Grid | **Not targeted** — intentional |
|
| 22 |
-
| Model cap | ≤32B parameters (all models comply) |
|
| 23 |
-
| Default model mode | `premium_nvidia` |
|
| 24 |
-
| Primary judge model | NVIDIA Nemotron 3 Nano Omni 30B-A3B |
|
| 25 |
-
| Frontend model calls | **Never** — frontend calls `/api/*` only |
|
| 26 |
-
| API keys | Backend `.env` locally; HF Space Secrets in deployment |
|
| 27 |
-
| Target prizes | Backyard AI, Best Demo, Best Agent, Off-Brand, NVIDIA Nemotron Quest, OpenBMB Awards, Sharing is Caring, Field Notes, Tiny Titan |
|
| 28 |
-
| Current build status | Phase 1 complete (mock APIs running) |
|
| 29 |
-
|
| 30 |
-
---
|
| 31 |
-
|
| 32 |
-
## 3. Current Model Stack
|
| 33 |
-
|
| 34 |
-
| Role | Model | Mode Key | Status |
|
| 35 |
-
|---|---|---|---|
|
| 36 |
-
| Primary judge (text, voice, scoring, rewrites) | `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` | `premium_nvidia` | **Live (Phase 2)** — client tested |
|
| 37 |
-
| OpenBMB omni mode | `openbmb/MiniCPM-o-4_5` | `openbmb_omni` | Stub — Phase 9 |
|
| 38 |
-
| Tiny fallback / Tiny Mode | `openbmb/MiniCPM5-1B` | `tiny_minicpm` | Stub — Phase 9 |
|
| 39 |
-
| Pitch deck critique | `openbmb/MiniCPM-V-4.6` | `vision_deck` | Stub — Phase 10 |
|
| 40 |
-
| Audio transcription fallback | `faster-whisper` (tiny or base) | `whisper_fallback` | Stub — Phase 7 |
|
| 41 |
-
|
| 42 |
-
> **Reasoning model note:** `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` uses tokens for an internal chain-of-thought before writing `message.content`. Confirmed token defaults: opponent=500, scorecard=2000, rewrite=800.
|
| 43 |
-
|
| 44 |
-
NVIDIA endpoint base URL: `https://integrate.api.nvidia.com/v1`
|
| 45 |
-
|
| 46 |
-
All model calls must go through `core/model_router.py` → individual client files. No model provider URL or key must ever appear in frontend code.
|
| 47 |
-
|
| 48 |
-
---
|
| 49 |
-
|
| 50 |
-
## 4. Backend Architecture
|
| 51 |
-
|
| 52 |
-
```
|
| 53 |
-
app.py ← Gradio Server entrypoint
|
| 54 |
-
/health ← health check
|
| 55 |
-
/ ← serves frontend/index.html
|
| 56 |
-
/api/load-sample ← REST endpoint
|
| 57 |
-
/api/start-session ← REST endpoint
|
| 58 |
-
/api/chat-round ← REST endpoint
|
| 59 |
-
/api/end-battle ← REST endpoint
|
| 60 |
-
/api/reset-session ← REST endpoint
|
| 61 |
-
/api/voice-pitch ← placeholder (Phase 7)
|
| 62 |
-
/api/start-deal-session ← placeholder (Phase 8)
|
| 63 |
-
/api/deck-critique ← placeholder (Phase 10)
|
| 64 |
-
@app.api wrappers ← same handlers, Gradio client compat
|
| 65 |
-
|
| 66 |
-
core/api_handlers.py ← shared handler logic (REST + Gradio route)
|
| 67 |
-
core/session_manager.py ← in-memory session store (SESSIONS dict)
|
| 68 |
-
core/persona_builder.py ← builds system prompt per persona + startup
|
| 69 |
-
core/attack_tags.py ← attack tag taxonomy, round-based selection
|
| 70 |
-
core/scoring_engine.py ← mock scorecard (Phase 1); real model call planned Phase 5
|
| 71 |
-
core/feedback_generator.py ← mock rewrite logic (Phase 1); model call planned Phase 5
|
| 72 |
-
core/json_utils.py ← JSON extraction + safe_json_parse + fallback_scorecard()
|
| 73 |
-
core/local_text_model.py ← stub placeholder (Phase 1 only)
|
| 74 |
-
core/voice_transcriber.py ← stub placeholder (Phase 7)
|
| 75 |
-
core/samples.py ← get_sample_startup() (EventRadar AI)
|
| 76 |
-
|
| 77 |
-
config/personas.json ← persona metadata (ids, names, focus areas)
|
| 78 |
-
config/attack_tags.json ← attack tag reference (not yet used in code — code uses attack_tags.py)
|
| 79 |
-
config/pitch_rubric.json ← 6 dimensions + weights
|
| 80 |
-
config/sample_startups.json ← sample startup data
|
| 81 |
-
|
| 82 |
-
ADDED in Phase 2:
|
| 83 |
-
model_router.py ← routes tasks; premium_nvidia live; others stub
|
| 84 |
-
nvidia_client.py ← NVIDIA Nemotron client; live, tested
|
| 85 |
-
minicpm_client.py ← health_check stub (Phase 9)
|
| 86 |
-
vision_client.py ← health_check stub (Phase 10)
|
| 87 |
-
transcription_client.py ← health_check stub (Phase 7)
|
| 88 |
-
```
|
| 89 |
-
|
| 90 |
-
---
|
| 91 |
-
|
| 92 |
-
## 5. Frontend Architecture
|
| 93 |
-
|
| 94 |
-
File: `frontend/script.js`
|
| 95 |
-
|
| 96 |
-
The frontend uses **`fetch()`** calls exclusively to `/api/*` backend routes. No model provider URLs or API keys exist in any frontend file. This is verified and correct.
|
| 97 |
-
|
| 98 |
-
Key functions:
|
| 99 |
-
- `apiPost(path, body)` — all API calls go through this helper
|
| 100 |
-
- `loadSample()` → `POST /api/load-sample`
|
| 101 |
-
- `startSession()` → `POST /api/start-session` (sends `model_mode: "premium_nvidia"`)
|
| 102 |
-
- `sendMessage()` → `POST /api/chat-round`
|
| 103 |
-
- `endBattle()` → `POST /api/end-battle`
|
| 104 |
-
- `resetBattle()` → `POST /api/reset-session`
|
| 105 |
-
- `renderScorecard(data)` — renders overall score, bars, rewrites, questions
|
| 106 |
-
|
| 107 |
-
The `boot()` function calls `GET /health` on page load for backend availability check.
|
| 108 |
-
|
| 109 |
-
Static assets served at `/frontend/*` via Gradio StaticFiles mount.
|
| 110 |
-
|
| 111 |
-
---
|
| 112 |
-
|
| 113 |
-
## 6. Real Product API Endpoints
|
| 114 |
-
|
| 115 |
-
These are the only endpoints that matter for PitchFight as a product. Ignore Gradio-internal Swagger routes.
|
| 116 |
-
|
| 117 |
-
| Method | Path | Status | Phase |
|
| 118 |
-
|---|---|---|---|
|
| 119 |
-
| GET | `/health` | Live (mock-free) | Phase 1 |
|
| 120 |
-
| GET | `/` | Live | Phase 1 |
|
| 121 |
-
| POST | `/api/load-sample` | Live (real config data) | Phase 1 |
|
| 122 |
-
| POST | `/api/start-session` | Live (mock AI response) | Phase 1 |
|
| 123 |
-
| POST | `/api/chat-round` | Live (mock AI response) | Phase 1 |
|
| 124 |
-
| POST | `/api/end-battle` | Live (mock scorecard) | Phase 1 |
|
| 125 |
-
| POST | `/api/reset-session` | Live | Phase 1 |
|
| 126 |
-
| POST | `/api/voice-pitch` | Placeholder 501 | Phase 7 |
|
| 127 |
-
| POST | `/api/start-deal-session` | Placeholder 501 | Phase 8 |
|
| 128 |
-
| POST | `/api/deck-critique` | Placeholder 501 | Phase 10 |
|
| 129 |
-
|
| 130 |
-
Gradio internal routes (`/gradio_api/*`, `/queue/*`, `/upload`, `/static/*`) are framework runtime routes registered automatically. **Do not call or maintain them as product APIs.**
|
| 131 |
-
|
| 132 |
-
---
|
| 133 |
-
|
| 134 |
-
## 7. Existing File Structure
|
| 135 |
-
|
| 136 |
-
```
|
| 137 |
-
Pitchfight/
|
| 138 |
-
├── app.py ← Gradio Server + REST endpoints
|
| 139 |
-
├── .env.example ← env template (complete, correct)
|
| 140 |
-
├── .gitignore ← must include .env
|
| 141 |
-
├── README.md ← HF Spaces metadata + project overview
|
| 142 |
-
├── core/
|
| 143 |
-
│ ├── __init__.py
|
| 144 |
-
│ ├── api_handlers.py ← shared handler logic
|
| 145 |
-
│ ├── attack_tags.py ← tag taxonomy + selector
|
| 146 |
-
│ ├── feedback_generator.py ← mock rewrite (Phase 1)
|
| 147 |
-
│ ├── json_utils.py ← JSON parse + fallback
|
| 148 |
-
│ ├── local_text_model.py ← stub (Phase 1)
|
| 149 |
-
│ ├── persona_builder.py ← system prompt builder
|
| 150 |
-
│ ├── samples.py ← EventRadar AI sample
|
| 151 |
-
│ ├── scoring_engine.py ← mock scorecard (Phase 1)
|
| 152 |
-
│ ├── session_manager.py ← in-memory sessions
|
| 153 |
-
│ └── voice_transcriber.py ← stub (Phase 1)
|
| 154 |
-
├── config/
|
| 155 |
-
│ ├── attack_tags.json ← reference (code uses attack_tags.py)
|
| 156 |
-
│ ├── personas.json ← persona metadata
|
| 157 |
-
│ ├── pitch_rubric.json ← 6-dimension rubric weights
|
| 158 |
-
│ └── sample_startups.json ← sample startup data
|
| 159 |
-
├── frontend/
|
| 160 |
-
│ ├── index.html
|
| 161 |
-
│ ├── script.js ← fetch-only, /api/* calls
|
| 162 |
-
│ ├── styles.css
|
| 163 |
-
│ └── assets/
|
| 164 |
-
└── docs/
|
| 165 |
-
├── BACKEND_API.md ← endpoint reference
|
| 166 |
-
├── CLAUDE_PROJECT_CONTEXT.md ← this file
|
| 167 |
-
├── DEMO_NOTES.md ← demo flow + prize talking points
|
| 168 |
-
├── DOCUMENTATION.md ← full project documentation
|
| 169 |
-
├── FIELD_NOTES.md ← build log
|
| 170 |
-
├── MODELS_FINAL.md ← model strategy
|
| 171 |
-
├── PHASE_WISE_PLAN.md ← 14-phase build plan
|
| 172 |
-
├── PROMPTS.md ← prompt templates
|
| 173 |
-
└── TASK_TRACKER.md ← phase + task status
|
| 174 |
-
```
|
| 175 |
-
|
| 176 |
-
**Missing from `core/` (needed Phase 2+):**
|
| 177 |
-
- `model_router.py`
|
| 178 |
-
- `nvidia_client.py`
|
| 179 |
-
- `minicpm_client.py`
|
| 180 |
-
- `vision_client.py`
|
| 181 |
-
- `transcription_client.py`
|
| 182 |
-
|
| 183 |
-
---
|
| 184 |
-
|
| 185 |
-
## 8. Current Implementation Status
|
| 186 |
-
|
| 187 |
-
### Working (Phase 1)
|
| 188 |
-
- Gradio Server app starts and serves custom frontend
|
| 189 |
-
- GET `/health` returns version + status
|
| 190 |
-
- GET `/` serves `frontend/index.html`
|
| 191 |
-
- POST `/api/load-sample` returns EventRadar AI startup dict from `samples.py`
|
| 192 |
-
- POST `/api/start-session` creates in-memory session, returns mock opening question keyed by persona
|
| 193 |
-
- POST `/api/chat-round` increments round, rotates attack tags, returns hardcoded mock follow-up per persona
|
| 194 |
-
- POST `/api/end-battle` returns mock scorecard with static scores, real user quotes from session history
|
| 195 |
-
- POST `/api/reset-session` deletes session from SESSIONS dict
|
| 196 |
-
- Frontend flow: landing → setup (load sample or enter custom) → battle arena → scorecard
|
| 197 |
-
- Error banners and loading overlay functional in frontend
|
| 198 |
-
- Health check on boot functional
|
| 199 |
-
|
| 200 |
-
### Mock / Placeholder
|
| 201 |
-
- All AI opponent messages in `api_handlers.py` — hardcoded string lists per persona
|
| 202 |
-
- All scorecard scores — hardcoded integers (overall: 68, clarity: 64, etc.)
|
| 203 |
-
- `feedback_generator.py` — template string rewrites, no model call
|
| 204 |
-
- `local_text_model.py` — stub, returns placeholder string
|
| 205 |
-
- `voice_transcriber.py` — stub, returns not_loaded status
|
| 206 |
-
- `/api/voice-pitch` — returns `{"status": "not_implemented"}`
|
| 207 |
-
- `/api/start-deal-session` — returns `{"status": "not_implemented"}`
|
| 208 |
-
- `/api/deck-critique` — returns `{"status": "not_implemented"}`
|
| 209 |
-
|
| 210 |
-
### Missing (Phase 2+)
|
| 211 |
-
- `core/model_router.py` — central routing logic
|
| 212 |
-
- `core/nvidia_client.py` — NVIDIA Nemotron API calls
|
| 213 |
-
- `core/minicpm_client.py` — MiniCPM-o and MiniCPM5-1B API/local calls
|
| 214 |
-
- `core/vision_client.py` — MiniCPM-V 4.6 image/vision calls
|
| 215 |
-
- `core/transcription_client.py` — faster-whisper audio fallback
|
| 216 |
-
- Real AI response in `handle_chat_round` (replace hardcoded followups)
|
| 217 |
-
- Real scoring in `handle_end_battle` (replace `mock_scorecard`)
|
| 218 |
-
- Real rewrite in `feedback_generator` (replace template strings)
|
| 219 |
-
- Voice audio handling in `/api/voice-pitch`
|
| 220 |
-
- Deal battle logic in `/api/start-deal-session`
|
| 221 |
-
- Image upload and critique in `/api/deck-critique`
|
| 222 |
-
|
| 223 |
-
---
|
| 224 |
-
|
| 225 |
-
## 9. Mock vs Real Components
|
| 226 |
-
|
| 227 |
-
| Component | Mock or Real | Notes |
|
| 228 |
-
|---|---|---|
|
| 229 |
-
| Session management | **Real** | In-memory, uuid-based, correct |
|
| 230 |
-
| Persona system prompt builder | **Real** | `persona_builder.py` builds full system prompt |
|
| 231 |
-
| Attack tag rotator | **Real** | `attack_tags.py` rotates by round index |
|
| 232 |
-
| Sample startup data | **Real** | EventRadar AI data loaded from `samples.py` |
|
| 233 |
-
| Config JSON files | **Real** | personas, rubric, tags all present |
|
| 234 |
-
| JSON utils | **Real** | Extraction + safe parse + fallback all coded |
|
| 235 |
-
| `json_utils.fallback_scorecard()` | **Real** | Safe fallback ready |
|
| 236 |
-
| Opening AI question | **Mock** | Hardcoded per persona in `api_handlers.py` |
|
| 237 |
-
| Follow-up AI questions | **Mock** | Hardcoded list per persona, index by round |
|
| 238 |
-
| Scorecard scores | **Mock** | All integers hardcoded in `scoring_engine.mock_scorecard()` |
|
| 239 |
-
| Answer rewrite | **Mock** | Template string in `feedback_generator.py` |
|
| 240 |
-
| Pitch rewrite | **Mock** | Template string in `feedback_generator.py` |
|
| 241 |
-
| NVIDIA API client | **Real** | `core/nvidia_client.py` — live, tested Phase 2 |
|
| 242 |
-
| Model router | **Real** | `core/model_router.py` — premium_nvidia live; others stub |
|
| 243 |
-
| MiniCPM clients | **Stub** | `core/minicpm_client.py` — health_check only, Phase 9 |
|
| 244 |
-
| Vision client | **Stub** | `core/vision_client.py` — health_check only, Phase 10 |
|
| 245 |
-
| Whisper transcription | **Stub** | `core/transcription_client.py` — health_check only, Phase 7 |
|
| 246 |
-
|
| 247 |
-
---
|
| 248 |
-
|
| 249 |
-
## 10. Next Recommended Phase
|
| 250 |
-
|
| 251 |
-
**Phase 3: Wire model_router into battle flow**
|
| 252 |
-
|
| 253 |
-
Phase 2 is complete. NVIDIA Nemotron is live and tested. Next step is connecting `model_router.generate_opponent_response()` to the actual battle endpoints.
|
| 254 |
-
|
| 255 |
-
Goals:
|
| 256 |
-
1. In `core/api_handlers.py`: import `model_router` and `persona_builder`
|
| 257 |
-
2. In `handle_start_session`: build system prompt, call `model_router.generate_opponent_response()`, fall back to mock if `ok=False`
|
| 258 |
-
3. In `handle_chat_round`: build full message history, call `model_router.generate_opponent_response()`, fall back to mock if `ok=False`
|
| 259 |
-
4. End-to-end test: full battle with live Nemotron responses
|
| 260 |
-
5. Update docs
|
| 261 |
-
|
| 262 |
-
Do **not** connect voice, deal battle, scoring, or deck critique in Phase 3.
|
| 263 |
-
|
| 264 |
-
---
|
| 265 |
-
|
| 266 |
-
## 11. Rules for Future Claude Code Work
|
| 267 |
-
|
| 268 |
-
1. **Always read docs before implementing a phase.** Read `PHASE_WISE_PLAN.md`, this file, and `BACKEND_API.md` before touching code.
|
| 269 |
-
2. **Never expose API keys in frontend.** No env reads, no hardcoded keys, no fetch calls to model provider URLs from frontend JS.
|
| 270 |
-
3. **Never commit `.env`.** `.gitignore` must include `.env`. Only `.env.example` (empty values) goes to repo.
|
| 271 |
-
4. **Keep frontend calls limited to `/api/*`.** The only external call from frontend is to `/health`. All AI calls go through backend `/api/*` routes.
|
| 272 |
-
5. **Keep model calls backend-only.** All `openai.Client`, `requests.post`, or SDK calls to NVIDIA/OpenBMB must be in `core/` files only.
|
| 273 |
-
6. **After each phase, update `docs/PHASE_WISE_PLAN.md`** with a status line marking the phase complete.
|
| 274 |
-
7. **After each phase, update `docs/FIELD_NOTES.md`** with what changed, what worked, and what was learned.
|
| 275 |
-
8. **After each phase, update `docs/BACKEND_API.md`** if any endpoint changed signature, status, or behavior.
|
| 276 |
-
9. **After each phase, update `docs/MODELS_FINAL.md`** if the model strategy or routing changed.
|
| 277 |
-
10. **Do not silently change architecture.** Any change to the API contract, session model, or model routing must be documented before implementation.
|
| 278 |
-
11. **Preserve the working mock flow while replacing parts incrementally.** Never delete a working mock endpoint before its replacement is tested. The mock is the safety net.
|
| 279 |
-
12. **Use `os.getenv()` only** for API key reads. Never use hardcoded fallback values for secrets. If the key is missing, log a warning and return a clear error — do not silently continue.
|
| 280 |
-
13. **Do not add features outside the current phase scope.** One phase at a time. Phase 2 is model router only. Phase 3 is NVIDIA text only. Do not reach forward.
|
| 281 |
-
14. **Test each model client independently** before wiring it into the session/handler flow.
|
| 282 |
-
15. **`core/json_utils.py` already has `safe_json_parse` and `fallback_scorecard`.** Use them — do not rewrite JSON parsing logic elsewhere.
|
| 283 |
-
|
| 284 |
-
---
|
| 285 |
-
|
| 286 |
-
## 12. Documentation Update Rules
|
| 287 |
-
|
| 288 |
-
After every phase:
|
| 289 |
-
|
| 290 |
-
| Doc | What to update |
|
| 291 |
-
|---|---|
|
| 292 |
-
| `docs/PHASE_WISE_PLAN.md` | Mark phase status as Complete. Add date. |
|
| 293 |
-
| `docs/FIELD_NOTES.md` | Add a build log entry: what changed, what worked, what surprised you. |
|
| 294 |
-
| `docs/BACKEND_API.md` | Update endpoint status column if a placeholder became real. |
|
| 295 |
-
| `docs/MODELS_FINAL.md` | Update model-to-file mapping if new client files were created or routes changed. |
|
| 296 |
-
| `docs/TASK_TRACKER.md` | Move completed tasks to done; update current phase and next tasks. |
|
| 297 |
-
| `docs/CLAUDE_PROJECT_CONTEXT.md` | Update "Current Implementation Status" and "Mock vs Real Components" sections. |
|
| 298 |
-
|
| 299 |
-
Do **not** update docs prophylactically for future phases. Only document what is actually implemented.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/DEMO_NOTES.md
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Demo Notes
|
| 2 |
-
|
| 3 |
-
## Strategic Demo Positioning
|
| 4 |
-
|
| 5 |
-
PitchFight AI is a **high-demo sponsor-model build** — not Off-the-Grid. Lead with voice, pressure, and scorecard quality. Mention NVIDIA Nemotron Omni and OpenBMB MiniCPM modes as sponsor-aligned small models under 32B.
|
| 6 |
-
|
| 7 |
-
## Full Demo Flow (Target Build)
|
| 8 |
-
|
| 9 |
-
1. Open the app — custom battle arena UI (not default Gradio).
|
| 10 |
-
2. Show **model mode badge** (Premium Nemotron / OpenBMB Omni / Tiny MiniCPM).
|
| 11 |
-
3. **Text path:** Load EventRadar AI → Hackathon Judge → Enter Arena → get grilled → End Battle → scorecard.
|
| 12 |
-
4. **Voice path:** Record spoken pitch → Nemotron interprets → first hard question → continue battle.
|
| 13 |
-
5. **Deal Battle:** Sponsorship or salary negotiation scenario → deal-specific scorecard.
|
| 14 |
-
6. **Deck critique (if built):** Upload slide screenshot → 3 judge questions.
|
| 15 |
-
7. **Retry:** Retry weakest question → show improvement.
|
| 16 |
-
|
| 17 |
-
## Phase 1 Demo Flow (Current)
|
| 18 |
-
|
| 19 |
-
1. Open the app.
|
| 20 |
-
2. Click **Load Demo Startup** (EventRadar AI).
|
| 21 |
-
3. Select **Hackathon Judge**.
|
| 22 |
-
4. Click **Enter the Arena**.
|
| 23 |
-
5. Answer weakly (e.g., "We use AI to rank events better").
|
| 24 |
-
6. Watch mock AI push back with attack tag + pressure level.
|
| 25 |
-
7. Click **End Battle** after 2–3 rounds.
|
| 26 |
-
8. Show scorecard: overall, bars, rewrites, top prep questions.
|
| 27 |
-
|
| 28 |
-
## Talking Points
|
| 29 |
-
|
| 30 |
-
- Custom Gradio Server frontend + `@app.api` backend.
|
| 31 |
-
- All model calls backend-only; keys in HF Space Secrets.
|
| 32 |
-
- ≤32B models: Nemotron Omni, MiniCPM-o, MiniCPM5-1B, MiniCPM-V.
|
| 33 |
-
- Voice + multimodal judging as differentiator.
|
| 34 |
-
- Rubric-based scorecard with quotes and rewrites.
|
| 35 |
-
- Off-the-Grid **not** claimed — demo quality is the goal.
|
| 36 |
-
|
| 37 |
-
## Prize Alignment Talking Points
|
| 38 |
-
|
| 39 |
-
| Prize | Hook |
|
| 40 |
-
|---|---|
|
| 41 |
-
| Backyard AI | Student founders, real pressure practice |
|
| 42 |
-
| Best Demo | Voice pitch → grill → scorecard in one flow |
|
| 43 |
-
| Best Agent | Persona + memory + attack tags + scoring loop |
|
| 44 |
-
| Off-Brand | Custom HTML/CSS/JS arena UI |
|
| 45 |
-
| NVIDIA Nemotron Quest | Premium voice/multimodal judge |
|
| 46 |
-
| OpenBMB Awards | MiniCPM-o / 5-1B / V modes |
|
| 47 |
-
| Sharing is Caring | Public prompts, rubrics, attack tags |
|
| 48 |
-
| Field Notes | Build story post-deployment |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/DOCUMENTATION.md
DELETED
|
@@ -1,1447 +0,0 @@
|
|
| 1 |
-
# ⚔️ PitchFight AI — Final Hackathon-Aligned Documentation
|
| 2 |
-
|
| 3 |
-
## A Voice-and-Text AI Sparring Arena for Student Founders (≤32B Models)
|
| 4 |
-
|
| 5 |
-
> **Your first tough pitch should not be in front of a real judge.**
|
| 6 |
-
> **Normal AI assistants give advice. PitchFight AI creates pressure.**
|
| 7 |
-
|
| 8 |
-
### Strategy Update (Current Build)
|
| 9 |
-
|
| 10 |
-
This documentation reflects the **high-demo sponsor-model strategy**. PitchFight AI prioritizes demo strength, model quality, and sponsor alignment (NVIDIA Nemotron Omni, OpenBMB MiniCPM) while staying under the **≤32B** Build Small Hackathon rule.
|
| 11 |
-
|
| 12 |
-
> **Off-the-Grid is not targeted.** Premium default model mode is **NVIDIA Nemotron Omni** (backend-only). API keys live in `.env` locally / HF Space Secrets in deployment — never in the frontend.
|
| 13 |
-
|
| 14 |
-
See also: [`PHASE_WISE_PLAN.md`](PHASE_WISE_PLAN.md) · [`MODELS_FINAL.md`](MODELS_FINAL.md) · [`BACKEND_API.md`](BACKEND_API.md)
|
| 15 |
-
|
| 16 |
-
PitchFight AI exposes **clean custom project APIs under `/api/...`**. Gradio internal routes may appear in OpenAPI/Swagger, but they are **framework runtime routes**, not product endpoints.
|
| 17 |
-
|
| 18 |
-
---
|
| 19 |
-
|
| 20 |
-
## 1. Project Overview
|
| 21 |
-
|
| 22 |
-
**PitchFight AI** is a Hugging Face Spaces + Gradio Server application that helps student founders practice high-pressure startup pitching before facing real judges, mentors, investors, sponsors, recruiters, or clients.
|
| 23 |
-
|
| 24 |
-
The project is built for the **Backyard AI** track of the Build Small Hackathon. The target user is intentionally narrow:
|
| 25 |
-
|
| 26 |
-
> **Student founders and hackathon builders who know their project, but freeze when someone asks hard questions.**
|
| 27 |
-
|
| 28 |
-
PitchFight AI turns pitch practice into a realistic AI sparring arena. The AI does not behave like a generic assistant. It behaves like a skeptical counterpart, asks tough follow-up questions, remembers previous answers, detects weak claims, scores the conversation, and rewrites the weakest answer into a stronger response.
|
| 29 |
-
|
| 30 |
-
---
|
| 31 |
-
|
| 32 |
-
## 2. Hackathon Rule Alignment
|
| 33 |
-
|
| 34 |
-
This section is intentionally explicit so the project does not violate the hackathon constraints.
|
| 35 |
-
|
| 36 |
-
### Required Rules
|
| 37 |
-
|
| 38 |
-
| Rule | How PitchFight AI Complies |
|
| 39 |
-
|---|---|
|
| 40 |
-
| **Small Models Only** | All models stay under the 32B parameter cap: Nemotron 3 Nano Omni (~30B), MiniCPM-o, MiniCPM5-1B, MiniCPM-V 4.6. |
|
| 41 |
-
| **Built on Gradio** | Gradio Server app hosted as a Hugging Face Space with custom HTML/CSS/JS frontend. |
|
| 42 |
-
| **Show, Don’t Tell** | Demo-first: pitch battle, voice mode, deal battle, scorecard — runnable in the Space. |
|
| 43 |
-
| **No Frontend Model Calls** | Browser calls only `/api/...` endpoints via `fetch()`; model APIs are backend-only. |
|
| 44 |
-
|
| 45 |
-
### Sponsor-Model / High-Demo Strategy
|
| 46 |
-
|
| 47 |
-
The **default submitted build** prioritizes the strongest demonstrative experience:
|
| 48 |
-
|
| 49 |
-
- **NVIDIA Nemotron Omni** — primary premium judge (text, voice, multimodal, scoring).
|
| 50 |
-
- **OpenBMB MiniCPM** — MiniCPM-o (omni), MiniCPM5-1B (tiny/fallback), MiniCPM-V 4.6 (deck critique).
|
| 51 |
-
- **faster-whisper** — backup local transcription when Omni voice path is unavailable.
|
| 52 |
-
- **No browser-side model API calls** — all keys in HF Space Secrets / backend `.env`.
|
| 53 |
-
- **No OpenAI or unrelated cloud APIs** in the default flow.
|
| 54 |
-
|
| 55 |
-
### Off-the-Grid Position
|
| 56 |
-
|
| 57 |
-
> **Off-the-Grid is intentionally not targeted.** Sponsor/model APIs are used for demo quality. The project instead targets Backyard AI, Best Demo, Best Agent, Off-Brand, NVIDIA Nemotron Quest, OpenBMB Awards, Sharing is Caring, and Field Notes.
|
| 58 |
-
|
| 59 |
-
### Final Compliance Position
|
| 60 |
-
|
| 61 |
-
> **PitchFight AI follows hackathon core rules: ≤32B models, Gradio/HF Spaces, demo-first execution, backend-only inference with secrets managed server-side.**
|
| 62 |
-
|
| 63 |
-
---
|
| 64 |
-
|
| 65 |
-
## 3. Core Problem
|
| 66 |
-
|
| 67 |
-
Most student founders prepare their pitch by:
|
| 68 |
-
|
| 69 |
-
- Reading pitch tips online
|
| 70 |
-
- Practicing alone
|
| 71 |
-
- Showing their idea to friends who say “sounds good”
|
| 72 |
-
- Making slides but not preparing for hard Q&A
|
| 73 |
-
|
| 74 |
-
The first real pressure often happens during judging, mentoring, investor review, or sponsorship calls. That is exactly when many students freeze.
|
| 75 |
-
|
| 76 |
-
### Gap
|
| 77 |
-
|
| 78 |
-
There is no easy, low-cost, always-available sparring partner that can simulate:
|
| 79 |
-
|
| 80 |
-
- A skeptical hackathon judge
|
| 81 |
-
- A technical evaluator
|
| 82 |
-
- A hard investor
|
| 83 |
-
- A sponsor asking for ROI
|
| 84 |
-
- A recruiter or client pushing back
|
| 85 |
-
|
| 86 |
-
### PitchFight AI’s Solution
|
| 87 |
-
|
| 88 |
-
PitchFight AI creates a **pressure simulation loop**:
|
| 89 |
-
|
| 90 |
-
1. User enters startup/project context.
|
| 91 |
-
2. User chooses an AI opponent.
|
| 92 |
-
3. AI opponent asks one sharp question at a time.
|
| 93 |
-
4. User responds under pressure.
|
| 94 |
-
5. AI follows up on weak or vague answers.
|
| 95 |
-
6. App generates a scorecard and improved answer.
|
| 96 |
-
|
| 97 |
-
---
|
| 98 |
-
|
| 99 |
-
## 4. Final Product Scope
|
| 100 |
-
|
| 101 |
-
### Primary Mode: Pitch Battle
|
| 102 |
-
|
| 103 |
-
The user defends their startup or hackathon project against a persona-locked AI opponent.
|
| 104 |
-
|
| 105 |
-
#### Personas
|
| 106 |
-
|
| 107 |
-
| Persona | Role | What They Attack |
|
| 108 |
-
|---|---|---|
|
| 109 |
-
| **Skeptical VC** | Investor-style evaluator | Market, moat, revenue, defensibility, retention |
|
| 110 |
-
| **Technical Judge** | Engineering evaluator | AI necessity, architecture, scalability, feasibility |
|
| 111 |
-
| **Hackathon Judge** | Competition evaluator | Novelty, demo clarity, MVP strength, real-world usefulness |
|
| 112 |
-
|
| 113 |
-
### Secondary Mode: Deal Battle
|
| 114 |
-
|
| 115 |
-
The user practices negotiation scenarios useful for student founders and early-career builders.
|
| 116 |
-
|
| 117 |
-
#### Scenarios
|
| 118 |
-
|
| 119 |
-
| Scenario | Counterpart |
|
| 120 |
-
|---|---|
|
| 121 |
-
| Startup Sponsorship Ask | Tough sponsor |
|
| 122 |
-
| Internship Salary Negotiation | Strict HR recruiter |
|
| 123 |
-
| Freelance Client Pricing | Budget-conscious client |
|
| 124 |
-
| Equity Negotiation | Investor negotiating hard |
|
| 125 |
-
|
| 126 |
-
### Voice Mode
|
| 127 |
-
|
| 128 |
-
Voice Mode allows the user to speak their pitch instead of typing it.
|
| 129 |
-
|
| 130 |
-
For the MVP, Voice Mode is intentionally simple and reliable:
|
| 131 |
-
|
| 132 |
-
```text
|
| 133 |
-
User records pitch audio
|
| 134 |
-
↓
|
| 135 |
-
Backend /api/voice-pitch (frontend never calls model APIs)
|
| 136 |
-
↓
|
| 137 |
-
Primary: NVIDIA Nemotron Omni interprets audio + asks first hard question
|
| 138 |
-
↓
|
| 139 |
-
Fallback: faster-whisper transcribes → Nemotron or MiniCPM5-1B text path
|
| 140 |
-
↓
|
| 141 |
-
User continues by text or voice
|
| 142 |
-
↓
|
| 143 |
-
Scorecard includes voice-derived structure and conciseness signals
|
| 144 |
-
```
|
| 145 |
-
|
| 146 |
-
Founders normally pitch out loud; the premium voice path uses Nemotron Omni backend-only, with faster-whisper as transcription fallback only.
|
| 147 |
-
|
| 148 |
-
---
|
| 149 |
-
|
| 150 |
-
## 5. What Makes This a Strong Hackathon Project
|
| 151 |
-
|
| 152 |
-
PitchFight AI is designed around the hackathon’s judging taste: runnable, specific, useful, delightful, and more than a chatbot wrapper.
|
| 153 |
-
|
| 154 |
-
### Runnable Gradio Space
|
| 155 |
-
|
| 156 |
-
The final app is deployed as a Hugging Face Space using Gradio.
|
| 157 |
-
|
| 158 |
-
### Clear Purpose
|
| 159 |
-
|
| 160 |
-
The app has one clear mission:
|
| 161 |
-
|
| 162 |
-
> Help student founders survive tough pitch and negotiation conversations before the real moment.
|
| 163 |
-
|
| 164 |
-
### Useful + Delightful
|
| 165 |
-
|
| 166 |
-
Useful: improves pitch readiness.
|
| 167 |
-
Delightful: feels like entering a battle arena instead of filling a boring form.
|
| 168 |
-
|
| 169 |
-
### More Than a Chatbot Wrapper
|
| 170 |
-
|
| 171 |
-
PitchFight AI includes:
|
| 172 |
-
|
| 173 |
-
- Startup context intake
|
| 174 |
-
- Persona selection
|
| 175 |
-
- Round-based battle flow
|
| 176 |
-
- Pressure level
|
| 177 |
-
- Attack tags
|
| 178 |
-
- Conversation memory
|
| 179 |
-
- Voice pitch mode
|
| 180 |
-
- Rubric-based scoring
|
| 181 |
-
- Weakest answer detection
|
| 182 |
-
- Improved answer rewrite
|
| 183 |
-
- Final pitch rewrite
|
| 184 |
-
- Scorecard screen
|
| 185 |
-
|
| 186 |
-
### Small Model Showcase
|
| 187 |
-
|
| 188 |
-
The project uses compact models strategically rather than relying on one huge model. The intelligence comes from:
|
| 189 |
-
|
| 190 |
-
- Strong persona prompts
|
| 191 |
-
- Memory
|
| 192 |
-
- Role consistency
|
| 193 |
-
- Attack-tag routing
|
| 194 |
-
- Rubric scoring
|
| 195 |
-
- faster-whisper transcription fallback (when Omni voice path is unavailable)
|
| 196 |
-
- Feedback generation
|
| 197 |
-
|
| 198 |
-
---
|
| 199 |
-
|
| 200 |
-
## 6. Final System Architecture
|
| 201 |
-
|
| 202 |
-
```mermaid
|
| 203 |
-
flowchart TD
|
| 204 |
-
A[Student Founder] --> B[Custom Web UI — fetch to /api/* only]
|
| 205 |
-
|
| 206 |
-
B --> C{Input Mode}
|
| 207 |
-
C -->|Text Pitch| D[Startup Context Form]
|
| 208 |
-
C -->|Voice Pitch| E[Browser Audio Recorder]
|
| 209 |
-
|
| 210 |
-
D --> H[Session Manager]
|
| 211 |
-
E --> VP[/api/voice-pitch]
|
| 212 |
-
VP --> F[NVIDIA Nemotron Omni — primary voice path]
|
| 213 |
-
VP --> W[faster-whisper — transcription fallback only]
|
| 214 |
-
F --> H
|
| 215 |
-
W --> H
|
| 216 |
-
|
| 217 |
-
H --> I[Persona Prompt Builder]
|
| 218 |
-
I --> J[Attack Tag Selector]
|
| 219 |
-
J --> K[Battle Engine]
|
| 220 |
-
|
| 221 |
-
K --> MR[Model Router]
|
| 222 |
-
MR --> L1[NVIDIA Nemotron Omni — default premium judge]
|
| 223 |
-
MR --> L2[MiniCPM-o 4.5 — OpenBMB omni mode]
|
| 224 |
-
MR --> L3[MiniCPM5-1B — tiny / API-failure fallback]
|
| 225 |
-
MR --> L4[MiniCPM-V 4.6 — deck critique]
|
| 226 |
-
L1 --> M[AI Opponent Response]
|
| 227 |
-
L2 --> M
|
| 228 |
-
L3 --> M
|
| 229 |
-
M --> B
|
| 230 |
-
|
| 231 |
-
H --> N[Scoring Engine]
|
| 232 |
-
N --> MR
|
| 233 |
-
MR --> O[Structured Scorecard JSON]
|
| 234 |
-
O --> P[JSON Parser + Fallback Handler]
|
| 235 |
-
P --> Q[Feedback Generator]
|
| 236 |
-
Q --> R[Scorecard + Improved Answer]
|
| 237 |
-
R --> B
|
| 238 |
-
```
|
| 239 |
-
|
| 240 |
-
### Simple Explanation
|
| 241 |
-
|
| 242 |
-
- The **custom frontend** handles the arena UI and calls only **`/api/...`** endpoints — never model provider APIs.
|
| 243 |
-
- **Gradio Server** exposes backend APIs and serves the custom frontend.
|
| 244 |
-
- The **Session Manager** stores startup details, chosen persona, round count, attack tags, and conversation history.
|
| 245 |
-
- The **Persona Prompt Builder** creates the opponent behavior.
|
| 246 |
-
- The **Attack Tag Selector** decides what kind of pressure the AI should apply next.
|
| 247 |
-
- The **Model Router** routes inference to **NVIDIA Nemotron Omni** by default, with OpenBMB MiniCPM modes and MiniCPM5-1B fallback.
|
| 248 |
-
- **faster-whisper** is backup transcription only when the Omni voice path is unavailable.
|
| 249 |
-
- API keys live in **HF Space Secrets** / backend `.env` only — never in frontend code.
|
| 250 |
-
- The **Scorecard UI** shows what went well, what failed, and how to improve.
|
| 251 |
-
|
| 252 |
-
---
|
| 253 |
-
|
| 254 |
-
## 7. User Workflow Diagram
|
| 255 |
-
|
| 256 |
-
```mermaid
|
| 257 |
-
flowchart LR
|
| 258 |
-
A[Open PitchFight AI] --> B[Choose Mode]
|
| 259 |
-
B --> C[Pitch Battle]
|
| 260 |
-
B --> D[Deal Battle]
|
| 261 |
-
|
| 262 |
-
C --> E[Enter Startup Context]
|
| 263 |
-
D --> F[Enter Negotiation Context]
|
| 264 |
-
|
| 265 |
-
E --> G[Choose Opponent]
|
| 266 |
-
F --> G
|
| 267 |
-
|
| 268 |
-
G --> H[Choose Text or Voice Input]
|
| 269 |
-
H --> I[AI Opens With Hard Question]
|
| 270 |
-
I --> J[User Responds]
|
| 271 |
-
J --> K[AI Selects Attack Tag]
|
| 272 |
-
K --> L[AI Pushes Back]
|
| 273 |
-
L --> J
|
| 274 |
-
|
| 275 |
-
J --> M[End Battle]
|
| 276 |
-
M --> N[Scorecard]
|
| 277 |
-
N --> O[Weakest Answer]
|
| 278 |
-
O --> P[Improved Version]
|
| 279 |
-
P --> Q[Retry or Start New Battle]
|
| 280 |
-
```
|
| 281 |
-
|
| 282 |
-
---
|
| 283 |
-
|
| 284 |
-
## 8. Final Model Strategy
|
| 285 |
-
|
| 286 |
-
The core model table is kept intentionally small. Every model listed here has a real job in the submitted app.
|
| 287 |
-
|
| 288 |
-
| Model | Type | Where Used | Why Used |
|
| 289 |
-
|---|---|---|---|
|
| 290 |
-
| **NVIDIA Nemotron 3 Nano Omni** | Premium multimodal LLM (≤30B) | Primary judge: pitch battle, voice, deal battle, scoring, rewrites | Best demo quality; NVIDIA Nemotron Quest alignment |
|
| 291 |
-
| **MiniCPM-o 4.5** | OpenBMB omni model | OpenBMB sponsor mode, multimodal fallback | OpenBMB Awards alignment |
|
| 292 |
-
| **MiniCPM5-1B** | Tiny text LLM | Tiny Mode, API failure fallback | Speed + Tiny Titan eligibility |
|
| 293 |
-
| **MiniCPM-V 4.6** | Vision model | Pitch deck / slide critique | Image-based judge questions |
|
| 294 |
-
| **faster-whisper (tiny/base)** | Local STT | Voice fallback transcription | Reliable backup when Omni voice path fails |
|
| 295 |
-
|
| 296 |
-
### Fallback Model Note
|
| 297 |
-
|
| 298 |
-
`core/model_router.py` routes to **Tiny MiniCPM5-1B** or **whisper_fallback** when premium APIs timeout, fail, or keys are missing. UI shows active mode badge (Premium Nemotron / OpenBMB Omni / Tiny MiniCPM).
|
| 299 |
-
|
| 300 |
-
---
|
| 301 |
-
|
| 302 |
-
## 9. Exact Use of Each Model
|
| 303 |
-
|
| 304 |
-
### 9.1 NVIDIA Nemotron Omni — Primary Premium Model
|
| 305 |
-
|
| 306 |
-
Used for:
|
| 307 |
-
|
| 308 |
-
- AI opponent responses and follow-ups
|
| 309 |
-
- Voice pitch interpretation and first hard question
|
| 310 |
-
- Deal battle counterpart dialogue
|
| 311 |
-
- Scorecard generation (structured JSON)
|
| 312 |
-
- Weakest answer rewrite and 60-second pitch rewrite
|
| 313 |
-
- Optional multimodal deck + spoken pitch critique
|
| 314 |
-
|
| 315 |
-
#### Backend Client
|
| 316 |
-
|
| 317 |
-
`core/nvidia_client.py` — secure calls via `NVIDIA_API_KEY` and `NVIDIA_BASE_URL`. Never exposed to frontend.
|
| 318 |
-
|
| 319 |
-
### 9.1b MiniCPM Models — OpenBMB + Fallback
|
| 320 |
-
|
| 321 |
-
**MiniCPM-o 4.5** — OpenBMB omni mode. **MiniCPM5-1B** — tiny/fallback text. **MiniCPM-V 4.6** — slide critique.
|
| 322 |
-
|
| 323 |
-
#### Runtime Options
|
| 324 |
-
|
| 325 |
-
| Runtime | Use Case |
|
| 326 |
-
|---|---|
|
| 327 |
-
| OpenBMB API / HF inference | Primary OpenBMB sponsor path |
|
| 328 |
-
| Local `transformers` / quantized | Tiny Mode on Space hardware if needed |
|
| 329 |
-
|
| 330 |
-
#### Suggested Settings
|
| 331 |
-
|
| 332 |
-
| Task | Temperature | Max Tokens |
|
| 333 |
-
|---|---:|---:|
|
| 334 |
-
| Opponent response | 0.7–0.8 | 180–260 |
|
| 335 |
-
| Scorecard | 0.1–0.3 | 900–1400 |
|
| 336 |
-
| Rewrite weak answer | 0.4–0.5 | 400–600 |
|
| 337 |
-
| Final pitch rewrite | 0.5–0.6 | 400–700 |
|
| 338 |
-
|
| 339 |
-
---
|
| 340 |
-
|
| 341 |
-
### 9.2 Whisper Tiny/Base — Local Voice Input
|
| 342 |
-
|
| 343 |
-
Used for:
|
| 344 |
-
|
| 345 |
-
- Spoken pitch transcription
|
| 346 |
-
- Spoken answer transcription
|
| 347 |
-
- Generating a transcript for the text battle engine
|
| 348 |
-
- Voice add-on metrics based on transcript structure
|
| 349 |
-
|
| 350 |
-
#### Voice Flow (Primary)
|
| 351 |
-
|
| 352 |
-
```text
|
| 353 |
-
Browser records audio
|
| 354 |
-
→ /voice_pitch API
|
| 355 |
-
→ NVIDIA Nemotron Omni (audio + prompt)
|
| 356 |
-
→ structured pitch context + first hard question
|
| 357 |
-
→ Pitch Battle continues (voice or text)
|
| 358 |
-
```
|
| 359 |
-
|
| 360 |
-
#### Voice Flow (Fallback)
|
| 361 |
-
|
| 362 |
-
```text
|
| 363 |
-
Browser records audio
|
| 364 |
-
→ faster-whisper local transcription
|
| 365 |
-
→ transcript → selected text model (Nemotron or MiniCPM5-1B)
|
| 366 |
-
→ normal Pitch Battle flow
|
| 367 |
-
```
|
| 368 |
-
|
| 369 |
-
---
|
| 370 |
-
|
| 371 |
-
### 9.3 Pitch Deck Critique — MiniCPM-V / Nemotron Vision
|
| 372 |
-
|
| 373 |
-
Used for:
|
| 374 |
-
|
| 375 |
-
- Slide screenshot upload via `/deck_critique`
|
| 376 |
-
- Claim extraction and clarity critique
|
| 377 |
-
- Three judge prep questions per slide
|
| 378 |
-
|
| 379 |
-
Primary: **MiniCPM-V 4.6**. Alternate: Nemotron Omni vision path.
|
| 380 |
-
|
| 381 |
-
---
|
| 382 |
-
|
| 383 |
-
## 10. Tools and Their Exact Usage
|
| 384 |
-
|
| 385 |
-
| Tool / Platform | Use in PitchFight AI |
|
| 386 |
-
|---|---|
|
| 387 |
-
| **Hugging Face Spaces** | Final deployment platform |
|
| 388 |
-
| **Gradio Server** | Gradio backend server with custom route support |
|
| 389 |
-
| **FastAPI `/api/*` routes** | Product endpoints: `/api/start-session`, `/api/chat-round`, `/api/end-battle`, etc. |
|
| 390 |
-
| **Gradio `@app.api` wrappers** | Compatibility aliases calling the same `core/api_handlers.py` functions |
|
| 391 |
-
| **Custom HTML/CSS/JS** | Makes the UI look like a battle arena instead of default Gradio |
|
| 392 |
-
| **Browser `fetch()`** | Frontend calls `/api/*` endpoints only — never model provider APIs |
|
| 393 |
-
| **faster-whisper** | Audio transcription fallback when Nemotron Omni voice path is unavailable |
|
| 394 |
-
| **Hugging Face Hub** | Model download and project sharing |
|
| 395 |
-
| **HF Space Secrets** | NVIDIA, OpenBMB, and HF tokens for backend-only inference (never exposed to frontend) |
|
| 396 |
-
| **GitHub / HF repo** | Version control and project sharing |
|
| 397 |
-
| **Mermaid diagrams** | Architecture and workflow documentation |
|
| 398 |
-
| **Cursor Agent** | Frontend implementation and UI polish |
|
| 399 |
-
| **Claude Code** | File generation, backend modules, refactoring |
|
| 400 |
-
| **ChatGPT** | Planning, architecture, prompt design, debugging strategy, documentation |
|
| 401 |
-
|
| 402 |
-
---
|
| 403 |
-
|
| 404 |
-
## 11. Backend API Design
|
| 405 |
-
|
| 406 |
-
The frontend should never call model runtime code directly. All model calls go through the Python backend.
|
| 407 |
-
|
| 408 |
-
### Correct Flow
|
| 409 |
-
|
| 410 |
-
```text
|
| 411 |
-
Browser UI → /api/* (fetch) → api_handlers → model_router → NVIDIA / OpenBMB / whisper clients
|
| 412 |
-
```
|
| 413 |
-
|
| 414 |
-
### Wrong Flow
|
| 415 |
-
|
| 416 |
-
```text
|
| 417 |
-
Browser UI → External model API directly
|
| 418 |
-
```
|
| 419 |
-
|
| 420 |
-
This would expose API keys. All model calls must stay on the backend.
|
| 421 |
-
|
| 422 |
-
---
|
| 423 |
-
|
| 424 |
-
### 11.1 `/start_session`
|
| 425 |
-
|
| 426 |
-
Starts a new battle session.
|
| 427 |
-
|
| 428 |
-
#### Input
|
| 429 |
-
|
| 430 |
-
```json
|
| 431 |
-
{
|
| 432 |
-
"mode": "pitch_battle",
|
| 433 |
-
"persona": "hackathon_judge",
|
| 434 |
-
"difficulty": "high",
|
| 435 |
-
"input_mode": "text",
|
| 436 |
-
"startup": {
|
| 437 |
-
"name": "EventRadar AI",
|
| 438 |
-
"problem": "Students miss hackathons and tech events because discovery is scattered.",
|
| 439 |
-
"target_users": "College students and early-stage builders",
|
| 440 |
-
"solution": "AI-powered event matching based on goals, skills, location, and urgency",
|
| 441 |
-
"why_ai": "The model ranks and explains relevance instead of just listing events",
|
| 442 |
-
"competitors": "Luma, LinkedIn Events, WhatsApp groups",
|
| 443 |
-
"traction": "Prototype with scraped event data and ranking logic",
|
| 444 |
-
"ask": "Hackathon prize and mentor feedback"
|
| 445 |
-
}
|
| 446 |
-
}
|
| 447 |
-
```
|
| 448 |
-
|
| 449 |
-
#### Output
|
| 450 |
-
|
| 451 |
-
```json
|
| 452 |
-
{
|
| 453 |
-
"session_id": "abc123",
|
| 454 |
-
"round": 1,
|
| 455 |
-
"pressure_level": "High",
|
| 456 |
-
"attack_tag": "AI Justification",
|
| 457 |
-
"ai_message": "Why does this need AI? A sorted event list with filters seems enough. What is the intelligence here?"
|
| 458 |
-
}
|
| 459 |
-
```
|
| 460 |
-
|
| 461 |
-
---
|
| 462 |
-
|
| 463 |
-
### 11.2 `/chat_round`
|
| 464 |
-
|
| 465 |
-
Processes one user answer and returns the next AI pushback.
|
| 466 |
-
|
| 467 |
-
#### Input
|
| 468 |
-
|
| 469 |
-
```json
|
| 470 |
-
{
|
| 471 |
-
"session_id": "abc123",
|
| 472 |
-
"user_message": "The AI ranks events based on the student's profile and explains why each event matters."
|
| 473 |
-
}
|
| 474 |
-
```
|
| 475 |
-
|
| 476 |
-
#### Output
|
| 477 |
-
|
| 478 |
-
```json
|
| 479 |
-
{
|
| 480 |
-
"round": 2,
|
| 481 |
-
"pressure_level": "High",
|
| 482 |
-
"attack_tag": "Retention",
|
| 483 |
-
"ai_message": "That explains matching, not retention. Why would a student return every week instead of checking once before placement season?"
|
| 484 |
-
}
|
| 485 |
-
```
|
| 486 |
-
|
| 487 |
-
---
|
| 488 |
-
|
| 489 |
-
### 11.3 `/voice_pitch`
|
| 490 |
-
|
| 491 |
-
Accepts an audio file. Primary path: Nemotron Omni voice interpretation. Fallback: faster-whisper transcription.
|
| 492 |
-
|
| 493 |
-
#### Input
|
| 494 |
-
|
| 495 |
-
```json
|
| 496 |
-
{
|
| 497 |
-
"audio_file": "pitch.wav",
|
| 498 |
-
"persona": "skeptical_vc"
|
| 499 |
-
}
|
| 500 |
-
```
|
| 501 |
-
|
| 502 |
-
#### Output
|
| 503 |
-
|
| 504 |
-
```json
|
| 505 |
-
{
|
| 506 |
-
"transcript": "We are building EventRadar AI...",
|
| 507 |
-
"detected_startup_context": {
|
| 508 |
-
"name": "EventRadar AI",
|
| 509 |
-
"problem": "students miss events",
|
| 510 |
-
"solution": "AI event matching"
|
| 511 |
-
},
|
| 512 |
-
"opening_question": "You are describing discovery, but where is the urgency? Why is this painful enough for students to change behavior?"
|
| 513 |
-
}
|
| 514 |
-
```
|
| 515 |
-
|
| 516 |
-
Implementation detail:
|
| 517 |
-
|
| 518 |
-
- **Primary:** audio sent to NVIDIA Nemotron Omni via backend; model extracts pitch context and opening question.
|
| 519 |
-
- **Fallback:** faster-whisper transcribes locally; transcript routed to Nemotron or MiniCPM5-1B text path.
|
| 520 |
-
- If context extraction is uncertain, the UI asks the user to quickly edit the detected fields before the battle starts.
|
| 521 |
-
|
| 522 |
-
---
|
| 523 |
-
|
| 524 |
-
### 11.4 `/end_battle`
|
| 525 |
-
|
| 526 |
-
Scores the conversation.
|
| 527 |
-
|
| 528 |
-
#### Input
|
| 529 |
-
|
| 530 |
-
```json
|
| 531 |
-
{
|
| 532 |
-
"session_id": "abc123"
|
| 533 |
-
}
|
| 534 |
-
```
|
| 535 |
-
|
| 536 |
-
#### Output
|
| 537 |
-
|
| 538 |
-
```json
|
| 539 |
-
{
|
| 540 |
-
"overall": 68,
|
| 541 |
-
"scores": {
|
| 542 |
-
"clarity": {
|
| 543 |
-
"score": 76,
|
| 544 |
-
"reason": "The user explained the workflow clearly but took too long to state the core value.",
|
| 545 |
-
"quote": "It ranks events based on a student's profile."
|
| 546 |
-
},
|
| 547 |
-
"problem_understanding": {
|
| 548 |
-
"score": 70,
|
| 549 |
-
"reason": "The user described the problem but did not provide evidence of frequency or severity.",
|
| 550 |
-
"quote": "Students miss hackathons because discovery is scattered."
|
| 551 |
-
}
|
| 552 |
-
},
|
| 553 |
-
"best_answer": {
|
| 554 |
-
"quote": "The app explains why each event is relevant instead of only listing it.",
|
| 555 |
-
"why": "This differentiated the product from a basic directory."
|
| 556 |
-
},
|
| 557 |
-
"weakest_answer": {
|
| 558 |
-
"quote": "Students will use it because it is helpful.",
|
| 559 |
-
"why": "This answer was vague and did not address retention or urgency."
|
| 560 |
-
},
|
| 561 |
-
"improved_answer": "Students return because the system continuously tracks deadlines, skill fit, team requirements, and local event updates. It becomes a weekly opportunity radar, not a one-time event list.",
|
| 562 |
-
"improved_pitch": "EventRadar AI helps students discover the right hackathons and tech events by matching opportunities to their skills, goals, location, and urgency. Unlike static event lists, it explains why each event matters and helps students act before deadlines.",
|
| 563 |
-
"top_3_questions": [
|
| 564 |
-
"Why does this need AI instead of filters?",
|
| 565 |
-
"What makes users return weekly?",
|
| 566 |
-
"How will you get your first 100 active users?"
|
| 567 |
-
]
|
| 568 |
-
}
|
| 569 |
-
```
|
| 570 |
-
|
| 571 |
-
---
|
| 572 |
-
|
| 573 |
-
## 12. Attack Tag Taxonomy
|
| 574 |
-
|
| 575 |
-
Attack tags make PitchFight AI feel structured instead of random. The AI should rotate through known pressure points based on the selected persona and the user’s previous answer.
|
| 576 |
-
|
| 577 |
-
### 12.1 Skeptical VC Attack Tags
|
| 578 |
-
|
| 579 |
-
| Attack Tag | What It Tests | Example Question |
|
| 580 |
-
|---|---|---|
|
| 581 |
-
| Market Size | Whether the opportunity is big enough | “How many people have this problem badly enough to pay or change behavior?” |
|
| 582 |
-
| Moat | Whether the idea is defensible | “What stops a larger platform from copying this in a week?” |
|
| 583 |
-
| Retention | Whether users come back | “Why would someone open this again after the first use?” |
|
| 584 |
-
| Revenue Logic | Whether there is a path to money/value | “Who pays, how much, and why would they keep paying?” |
|
| 585 |
-
| First 100 Users | Whether acquisition is realistic | “How exactly do you get the first 100 active users without paid ads?” |
|
| 586 |
-
| Why Now | Whether timing is convincing | “Why is this urgent now instead of two years ago or two years later?” |
|
| 587 |
-
| Competition | Whether existing alternatives are understood | “Why does your user choose this over what they already do today?” |
|
| 588 |
-
|
| 589 |
-
### 12.2 Technical Judge Attack Tags
|
| 590 |
-
|
| 591 |
-
| Attack Tag | What It Tests | Example Question |
|
| 592 |
-
|---|---|---|
|
| 593 |
-
| AI Justification | Whether AI is actually needed | “What does the model do that filters or rules cannot?” |
|
| 594 |
-
| Architecture | Whether the system design is clear | “Where exactly does inference happen, and what data flows through it?” |
|
| 595 |
-
| Scalability | Whether the system can grow | “What breaks first when 100 users become 10,000?” |
|
| 596 |
-
| Latency | Whether the UX is practical | “How long does one response take, and what happens when it is slow?” |
|
| 597 |
-
| Data Quality | Whether input data is reliable | “Where does your data come from, and how do you handle missing or wrong data?” |
|
| 598 |
-
| Failure Mode | Whether the team knows what can go wrong | “What is the worst wrong output your system can produce?” |
|
| 599 |
-
| Simpler Alternative | Whether the build is overengineered | “Could this be done with a form and rules? Why is your system better?” |
|
| 600 |
-
|
| 601 |
-
### 12.3 Hackathon Judge Attack Tags
|
| 602 |
-
|
| 603 |
-
| Attack Tag | What It Tests | Example Question |
|
| 604 |
-
|---|---|---|
|
| 605 |
-
| Novelty | Whether the project is memorable | “I have seen similar ideas. What is the one thing I will remember?” |
|
| 606 |
-
| Demo Clarity | Whether the demo lands quickly | “Can you show the value in 30 seconds?” |
|
| 607 |
-
| MVP Strength | Whether the prototype actually works | “What part is fully working right now, not just planned?” |
|
| 608 |
-
| User Pain | Whether the problem is real | “Who exactly has this problem, and how do you know?” |
|
| 609 |
-
| AI Load-Bearing | Whether AI is central | “If I remove the model, does the product collapse or still work?” |
|
| 610 |
-
| Backyard Fit | Whether it helps a real person | “Who used this, and what got better for them?” |
|
| 611 |
-
| Practical Impact | Whether it matters beyond the demo | “What changes for the user after using this?” |
|
| 612 |
-
|
| 613 |
-
### 12.4 Deal Battle Attack Tags
|
| 614 |
-
|
| 615 |
-
| Attack Tag | What It Tests | Example Question |
|
| 616 |
-
|---|---|---|
|
| 617 |
-
| Anchoring | Whether the user sets a clear position | “What number are you asking for, and why that number?” |
|
| 618 |
-
| Evidence | Whether claims are backed by proof | “What proof do you have that you are worth that ask?” |
|
| 619 |
-
| Concession | Whether the user gives up too early | “If I say no, what do you offer without lowering the value?” |
|
| 620 |
-
| Mutual Value | Whether both sides benefit | “Why is this good for me, not just for you?” |
|
| 621 |
-
| Closing | Whether the user moves toward action | “What exact next step are you asking me to agree to?” |
|
| 622 |
-
|
| 623 |
-
---
|
| 624 |
-
|
| 625 |
-
## 13. Core Prompt Design
|
| 626 |
-
|
| 627 |
-
### 13.1 Opponent System Prompt
|
| 628 |
-
|
| 629 |
-
```text
|
| 630 |
-
You are {persona_name}, a realistic evaluator speaking to a student founder.
|
| 631 |
-
|
| 632 |
-
Your job is not to encourage. Your job is to pressure-test the founder's thinking.
|
| 633 |
-
|
| 634 |
-
Startup context:
|
| 635 |
-
- Name: {name}
|
| 636 |
-
- Problem: {problem}
|
| 637 |
-
- Target users: {target_users}
|
| 638 |
-
- Solution: {solution}
|
| 639 |
-
- Why AI: {why_ai}
|
| 640 |
-
- Competitors: {competitors}
|
| 641 |
-
- Traction: {traction}
|
| 642 |
-
- Ask: {ask}
|
| 643 |
-
|
| 644 |
-
Current attack tag: {attack_tag}
|
| 645 |
-
|
| 646 |
-
Rules:
|
| 647 |
-
1. Ask one sharp question at a time.
|
| 648 |
-
2. Keep responses under 4 sentences.
|
| 649 |
-
3. Reference the founder's previous answer.
|
| 650 |
-
4. Do not give advice during the battle.
|
| 651 |
-
5. Do not say "great answer", "interesting", or "that makes sense".
|
| 652 |
-
6. If the answer is vague, attack the vague part.
|
| 653 |
-
7. If the answer is strong, raise the difficulty.
|
| 654 |
-
8. Stay in character.
|
| 655 |
-
9. Be firm, realistic, and useful — not abusive.
|
| 656 |
-
10. Your response must match the current attack tag.
|
| 657 |
-
```
|
| 658 |
-
|
| 659 |
-
---
|
| 660 |
-
|
| 661 |
-
### 13.2 Scoring Prompt
|
| 662 |
-
|
| 663 |
-
```text
|
| 664 |
-
You watched a pitch battle between a student founder and an AI evaluator.
|
| 665 |
-
|
| 666 |
-
Score the founder on:
|
| 667 |
-
1. Clarity
|
| 668 |
-
2. Problem Understanding
|
| 669 |
-
3. Market Awareness
|
| 670 |
-
4. Differentiation
|
| 671 |
-
5. Business Model
|
| 672 |
-
6. Objection Handling
|
| 673 |
-
|
| 674 |
-
For each dimension:
|
| 675 |
-
- Give a score from 0 to 100
|
| 676 |
-
- Mention the exact quote that influenced the score
|
| 677 |
-
- Give one specific reason
|
| 678 |
-
|
| 679 |
-
Then identify:
|
| 680 |
-
- Best answer
|
| 681 |
-
- Weakest answer
|
| 682 |
-
- Why the weak answer failed
|
| 683 |
-
- Improved version of the weak answer
|
| 684 |
-
- Improved 60-second pitch
|
| 685 |
-
- Top 3 questions to prepare next
|
| 686 |
-
|
| 687 |
-
Return valid JSON only.
|
| 688 |
-
```
|
| 689 |
-
|
| 690 |
-
---
|
| 691 |
-
|
| 692 |
-
## 14. Scoring Rubric
|
| 693 |
-
|
| 694 |
-
### Pitch Battle
|
| 695 |
-
|
| 696 |
-
| Dimension | Weight | What It Measures |
|
| 697 |
-
|---|---:|---|
|
| 698 |
-
| Clarity | 15% | Can a judge understand the idea quickly? |
|
| 699 |
-
| Problem Understanding | 20% | Did the founder prove the pain is real? |
|
| 700 |
-
| Market Awareness | 15% | Do they know who the first users are? |
|
| 701 |
-
| Differentiation | 20% | Why does this beat existing options? |
|
| 702 |
-
| Business Model | 15% | Is there a path to value or revenue? |
|
| 703 |
-
| Objection Handling | 15% | Did they answer directly or dodge? |
|
| 704 |
-
|
| 705 |
-
### Deal Battle
|
| 706 |
-
|
| 707 |
-
| Dimension | Weight | What It Measures |
|
| 708 |
-
|---|---:|---|
|
| 709 |
-
| Anchoring | 20% | Did the user set a clear position? |
|
| 710 |
-
| Evidence Usage | 20% | Did they support the ask with proof? |
|
| 711 |
-
| Confidence | 15% | Did they hold ground under pressure? |
|
| 712 |
-
| Empathy | 15% | Did they understand the other side’s constraints? |
|
| 713 |
-
| Concession Handling | 15% | Did they avoid giving up too fast? |
|
| 714 |
-
| Closing | 15% | Did they move toward a clear next step? |
|
| 715 |
-
|
| 716 |
-
### Voice Mode Add-On Metrics
|
| 717 |
-
|
| 718 |
-
| Dimension | What It Measures |
|
| 719 |
-
|---|---|
|
| 720 |
-
| Structure | Did the spoken pitch have a clear beginning, middle, and ask? |
|
| 721 |
-
| Conciseness | Did the user avoid rambling? |
|
| 722 |
-
| Confidence Signals | Did the transcript show hesitation or unclear phrasing? |
|
| 723 |
-
| Directness | Did the user answer the question directly? |
|
| 724 |
-
|
| 725 |
-
---
|
| 726 |
-
|
| 727 |
-
## 15. JSON Parsing and Fallback Strategy
|
| 728 |
-
|
| 729 |
-
Small models can return malformed JSON. The scoring engine must handle this gracefully.
|
| 730 |
-
|
| 731 |
-
### Fallback Pipeline
|
| 732 |
-
|
| 733 |
-
```text
|
| 734 |
-
1. Try direct JSON parse.
|
| 735 |
-
↓
|
| 736 |
-
2. If parse fails, extract substring from first `{` to last `}` and parse again.
|
| 737 |
-
↓
|
| 738 |
-
3. If still broken, send a model retry prompt via model router:
|
| 739 |
-
“Convert the following response into valid JSON only.”
|
| 740 |
-
↓
|
| 741 |
-
4. If retry fails, use regex to extract numeric scores and key sections.
|
| 742 |
-
↓
|
| 743 |
-
5. If all parsing fails, show a graceful fallback scorecard:
|
| 744 |
-
“Scorecard could not be fully structured, but here is the raw feedback.”
|
| 745 |
-
```
|
| 746 |
-
|
| 747 |
-
### Implementation Notes
|
| 748 |
-
|
| 749 |
-
- Never expose raw Python errors in the UI.
|
| 750 |
-
- Always show something useful to the user.
|
| 751 |
-
- Log parse failures in the backend console for debugging.
|
| 752 |
-
- Keep a default scorecard schema so the frontend never breaks.
|
| 753 |
-
|
| 754 |
-
---
|
| 755 |
-
|
| 756 |
-
## 16. Frontend UI Design
|
| 757 |
-
|
| 758 |
-
### Design Goal
|
| 759 |
-
|
| 760 |
-
The app should not look like a basic Gradio demo. It should feel like a polished AI practice arena.
|
| 761 |
-
|
| 762 |
-
### Screens
|
| 763 |
-
|
| 764 |
-
1. Landing screen
|
| 765 |
-
2. Mode selection
|
| 766 |
-
3. Startup / negotiation context form
|
| 767 |
-
4. Persona selection
|
| 768 |
-
5. Text or voice input selection
|
| 769 |
-
6. Battle arena
|
| 770 |
-
7. Scorecard screen
|
| 771 |
-
8. Retry / new battle screen
|
| 772 |
-
|
| 773 |
-
### Visual Direction
|
| 774 |
-
|
| 775 |
-
```text
|
| 776 |
-
Theme: Dark Battle Arena
|
| 777 |
-
Background: near-black / navy
|
| 778 |
-
Primary accent: red
|
| 779 |
-
Secondary accent: gold
|
| 780 |
-
Cards: dark glassmorphism
|
| 781 |
-
Typography: bold headings, clean mono-style chat
|
| 782 |
-
Animations: subtle glow, score reveal, pressure meter pulse
|
| 783 |
-
```
|
| 784 |
-
|
| 785 |
-
### UI Elements
|
| 786 |
-
|
| 787 |
-
- Round counter
|
| 788 |
-
- Pressure meter
|
| 789 |
-
- Attack tag
|
| 790 |
-
- Opponent card
|
| 791 |
-
- Founder card
|
| 792 |
-
- Voice recording button
|
| 793 |
-
- Chat transcript
|
| 794 |
-
- End battle button
|
| 795 |
-
- Score bars
|
| 796 |
-
- Weakest answer highlight
|
| 797 |
-
- Improved answer card
|
| 798 |
-
|
| 799 |
-
---
|
| 800 |
-
|
| 801 |
-
## 17. Hugging Face Spaces Deployment Plan
|
| 802 |
-
|
| 803 |
-
### Space Type
|
| 804 |
-
|
| 805 |
-
```yaml
|
| 806 |
-
---
|
| 807 |
-
title: PitchFight AI
|
| 808 |
-
emoji: ⚔️
|
| 809 |
-
colorFrom: red
|
| 810 |
-
colorTo: yellow
|
| 811 |
-
sdk: gradio
|
| 812 |
-
app_file: app.py
|
| 813 |
-
pinned: false
|
| 814 |
-
---
|
| 815 |
-
```
|
| 816 |
-
|
| 817 |
-
### Core Environment Variables
|
| 818 |
-
|
| 819 |
-
Set in HF Space Secrets and local `.env` (never commit real keys):
|
| 820 |
-
|
| 821 |
-
```text
|
| 822 |
-
APP_ENV=production
|
| 823 |
-
MAX_ROUNDS=6
|
| 824 |
-
|
| 825 |
-
NVIDIA_API_KEY=<hf-space-secret>
|
| 826 |
-
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 827 |
-
NVIDIA_OMNI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning
|
| 828 |
-
|
| 829 |
-
OPENBMB_API_KEY=<hf-space-secret>
|
| 830 |
-
OPENBMB_BASE_URL=
|
| 831 |
-
MINICPM_OMNI_MODEL=openbmb/MiniCPM-o-4_5
|
| 832 |
-
MINICPM_TEXT_MODEL=openbmb/MiniCPM5-1B
|
| 833 |
-
MINICPM_VISION_MODEL=openbmb/MiniCPM-V-4.6
|
| 834 |
-
|
| 835 |
-
HF_TOKEN=<hf-space-secret>
|
| 836 |
-
WHISPER_FALLBACK_ENABLED=true
|
| 837 |
-
WHISPER_MODEL_SIZE=tiny
|
| 838 |
-
```
|
| 839 |
-
|
| 840 |
-
Do **not** expose keys in frontend code, README, or public repos.
|
| 841 |
-
|
| 842 |
-
### Required `packages.txt`
|
| 843 |
-
|
| 844 |
-
```text
|
| 845 |
-
ffmpeg
|
| 846 |
-
```
|
| 847 |
-
|
| 848 |
-
### Important Security Rules
|
| 849 |
-
|
| 850 |
-
- Never commit real API keys.
|
| 851 |
-
- Never put keys in frontend JavaScript.
|
| 852 |
-
- Use `.env` locally only for configuration.
|
| 853 |
-
- Add `.env` to `.gitignore`.
|
| 854 |
-
- Sponsor model API keys (NVIDIA, OpenBMB) are required for the premium demo path; they stay backend-only.
|
| 855 |
-
|
| 856 |
-
---
|
| 857 |
-
|
| 858 |
-
## 18. Final Deployment File Structure
|
| 859 |
-
|
| 860 |
-
```text
|
| 861 |
-
pitchfight-ai/
|
| 862 |
-
│
|
| 863 |
-
├── app.py
|
| 864 |
-
├── requirements.txt
|
| 865 |
-
├── packages.txt
|
| 866 |
-
├── README.md
|
| 867 |
-
├── .env.example
|
| 868 |
-
├── .gitignore
|
| 869 |
-
│
|
| 870 |
-
├── core/
|
| 871 |
-
│ ├── __init__.py
|
| 872 |
-
│ ├── api_handlers.py
|
| 873 |
-
│ ├── session_manager.py
|
| 874 |
-
│ ├── persona_builder.py
|
| 875 |
-
│ ├── attack_tags.py
|
| 876 |
-
│ ├── model_router.py
|
| 877 |
-
│ ├── nvidia_client.py
|
| 878 |
-
│ ├── minicpm_client.py
|
| 879 |
-
│ ├── vision_client.py
|
| 880 |
-
│ ├── transcription_client.py
|
| 881 |
-
│ ├── scoring_engine.py
|
| 882 |
-
│ ├── feedback_generator.py
|
| 883 |
-
│ ├── json_utils.py
|
| 884 |
-
│ └── samples.py
|
| 885 |
-
│
|
| 886 |
-
├── config/
|
| 887 |
-
│ ├── personas.json
|
| 888 |
-
│ ├── attack_tags.json
|
| 889 |
-
│ ├── pitch_rubric.json
|
| 890 |
-
│ ├── deal_rubric.json
|
| 891 |
-
│ └── sample_startups.json
|
| 892 |
-
│
|
| 893 |
-
├── frontend/
|
| 894 |
-
│ ├── index.html
|
| 895 |
-
│ ├── styles.css
|
| 896 |
-
│ ├── script.js
|
| 897 |
-
│ └── assets/
|
| 898 |
-
│ ├── logo.svg
|
| 899 |
-
│ └── icons/
|
| 900 |
-
│
|
| 901 |
-
├── docs/
|
| 902 |
-
│ ├── DOCUMENTATION.md
|
| 903 |
-
│ ├── PROMPTS.md
|
| 904 |
-
│ ├── DEMO_NOTES.md
|
| 905 |
-
│ └── FIELD_NOTES.md
|
| 906 |
-
│
|
| 907 |
-
└── tests/
|
| 908 |
-
├── test_prompts.py
|
| 909 |
-
├── test_attack_tags.py
|
| 910 |
-
└── test_json_parser.py
|
| 911 |
-
```
|
| 912 |
-
|
| 913 |
-
---
|
| 914 |
-
|
| 915 |
-
## 19. File Responsibilities
|
| 916 |
-
|
| 917 |
-
### `app.py`
|
| 918 |
-
|
| 919 |
-
Main Gradio Server entry point.
|
| 920 |
-
|
| 921 |
-
Responsibilities:
|
| 922 |
-
|
| 923 |
-
- Create `gradio.Server`
|
| 924 |
-
- Serve custom `frontend/index.html`
|
| 925 |
-
- Serve static CSS/JS/assets
|
| 926 |
-
- Expose backend APIs
|
| 927 |
-
- Launch app
|
| 928 |
-
|
| 929 |
-
### `core/session_manager.py`
|
| 930 |
-
|
| 931 |
-
Stores active battle sessions.
|
| 932 |
-
|
| 933 |
-
Responsibilities:
|
| 934 |
-
|
| 935 |
-
- Create sessions
|
| 936 |
-
- Store conversation history
|
| 937 |
-
- Track rounds
|
| 938 |
-
- Track persona and mode
|
| 939 |
-
- Track attack tags
|
| 940 |
-
- Return full conversation for scoring
|
| 941 |
-
|
| 942 |
-
### `core/persona_builder.py`
|
| 943 |
-
|
| 944 |
-
Builds persona prompts.
|
| 945 |
-
|
| 946 |
-
Responsibilities:
|
| 947 |
-
|
| 948 |
-
- Load persona config
|
| 949 |
-
- Inject startup or negotiation context
|
| 950 |
-
- Add difficulty rules
|
| 951 |
-
- Add current attack tag
|
| 952 |
-
- Return system prompt
|
| 953 |
-
|
| 954 |
-
### `core/attack_tags.py`
|
| 955 |
-
|
| 956 |
-
Controls structured pressure flow.
|
| 957 |
-
|
| 958 |
-
Responsibilities:
|
| 959 |
-
|
| 960 |
-
- Store attack tag taxonomy
|
| 961 |
-
- Select next attack tag per persona
|
| 962 |
-
- Avoid repeating the same attack tag too often
|
| 963 |
-
- Increase pressure as rounds progress
|
| 964 |
-
|
| 965 |
-
### `core/model_router.py`
|
| 966 |
-
|
| 967 |
-
Backend-only model routing (Phase 2+).
|
| 968 |
-
|
| 969 |
-
Responsibilities:
|
| 970 |
-
|
| 971 |
-
- Text battle → NVIDIA Nemotron Omni by default; MiniCPM5-1B on failure
|
| 972 |
-
- Voice input → Nemotron Omni primary; faster-whisper transcription fallback
|
| 973 |
-
- OpenBMB omni mode → MiniCPM-o 4.5
|
| 974 |
-
- Deck critique → MiniCPM-V 4.6 or Nemotron vision path
|
| 975 |
-
- Expose active mode badge to frontend (`premium_nvidia`, `openbmb_omni`, `tiny_minicpm`, etc.)
|
| 976 |
-
|
| 977 |
-
### `core/nvidia_client.py`
|
| 978 |
-
|
| 979 |
-
Secure NVIDIA Nemotron Omni client (Phase 2+).
|
| 980 |
-
|
| 981 |
-
Responsibilities:
|
| 982 |
-
|
| 983 |
-
- Backend-only API calls via `NVIDIA_API_KEY`
|
| 984 |
-
- Text, voice, multimodal judge responses
|
| 985 |
-
- Scorecard and rewrite generation
|
| 986 |
-
- Timeout / retry handling
|
| 987 |
-
|
| 988 |
-
### `core/minicpm_client.py`
|
| 989 |
-
|
| 990 |
-
OpenBMB MiniCPM backend client (Phase 2+).
|
| 991 |
-
|
| 992 |
-
Responsibilities:
|
| 993 |
-
|
| 994 |
-
- MiniCPM-o omni mode
|
| 995 |
-
- MiniCPM5-1B tiny / fallback text generation
|
| 996 |
-
|
| 997 |
-
### `core/transcription_client.py`
|
| 998 |
-
|
| 999 |
-
Voice transcription fallback.
|
| 1000 |
-
|
| 1001 |
-
Responsibilities:
|
| 1002 |
-
|
| 1003 |
-
- Accept audio file
|
| 1004 |
-
- Run faster-whisper locally when Omni voice path fails
|
| 1005 |
-
- Return transcript for model router
|
| 1006 |
-
|
| 1007 |
-
### `core/vision_client.py`
|
| 1008 |
-
|
| 1009 |
-
Deck critique vision client (Phase 2+).
|
| 1010 |
-
|
| 1011 |
-
Responsibilities:
|
| 1012 |
-
|
| 1013 |
-
- Route slide images to MiniCPM-V 4.6 or Nemotron vision path
|
| 1014 |
-
|
| 1015 |
-
### `core/scoring_engine.py`
|
| 1016 |
-
|
| 1017 |
-
Generates scorecard.
|
| 1018 |
-
|
| 1019 |
-
Responsibilities:
|
| 1020 |
-
|
| 1021 |
-
- Build scoring prompt
|
| 1022 |
-
- Call model router (Nemotron default, MiniCPM5 fallback)
|
| 1023 |
-
- Parse JSON
|
| 1024 |
-
- Use fallback strategy if JSON breaks
|
| 1025 |
-
|
| 1026 |
-
### `core/feedback_generator.py`
|
| 1027 |
-
|
| 1028 |
-
Generates improved answers.
|
| 1029 |
-
|
| 1030 |
-
Responsibilities:
|
| 1031 |
-
|
| 1032 |
-
- Rewrite weakest answer
|
| 1033 |
-
- Generate improved pitch
|
| 1034 |
-
- Generate top 3 prep questions
|
| 1035 |
-
|
| 1036 |
-
### `core/json_utils.py`
|
| 1037 |
-
|
| 1038 |
-
Keeps frontend stable.
|
| 1039 |
-
|
| 1040 |
-
Responsibilities:
|
| 1041 |
-
|
| 1042 |
-
- Parse scorecard JSON
|
| 1043 |
-
- Repair malformed JSON
|
| 1044 |
-
- Extract fallback sections
|
| 1045 |
-
- Return default schema if needed
|
| 1046 |
-
|
| 1047 |
-
### `frontend/index.html`
|
| 1048 |
-
|
| 1049 |
-
Custom UI shell.
|
| 1050 |
-
|
| 1051 |
-
Responsibilities:
|
| 1052 |
-
|
| 1053 |
-
- Landing page
|
| 1054 |
-
- Mode selector
|
| 1055 |
-
- Form screens
|
| 1056 |
-
- Battle arena
|
| 1057 |
-
- Scorecard layout
|
| 1058 |
-
|
| 1059 |
-
### `frontend/script.js`
|
| 1060 |
-
|
| 1061 |
-
Frontend logic.
|
| 1062 |
-
|
| 1063 |
-
Responsibilities:
|
| 1064 |
-
|
| 1065 |
-
- Call `/api/*` endpoints via `fetch()` only — never model provider APIs
|
| 1066 |
-
- Manage UI state
|
| 1067 |
-
- Render messages and scorecards
|
| 1068 |
-
- Handle audio recording
|
| 1069 |
-
|
| 1070 |
-
### `frontend/styles.css`
|
| 1071 |
-
|
| 1072 |
-
Visual polish.
|
| 1073 |
-
|
| 1074 |
-
Responsibilities:
|
| 1075 |
-
|
| 1076 |
-
- Dark battle theme
|
| 1077 |
-
- Cards
|
| 1078 |
-
- Animations
|
| 1079 |
-
- Responsive layout
|
| 1080 |
-
- Score bars
|
| 1081 |
-
- Pressure meter
|
| 1082 |
-
|
| 1083 |
-
---
|
| 1084 |
-
|
| 1085 |
-
## 20. Development Phases
|
| 1086 |
-
|
| 1087 |
-
> **Authoritative roadmap:** [`PHASE_WISE_PLAN.md`](PHASE_WISE_PLAN.md) (14 phases, sponsor-model strategy). The summary below aligns with that plan.
|
| 1088 |
-
|
| 1089 |
-
This section describes the build order at a high level. The project can be built fast, but the documentation should not contradict itself by calling a full build a “one-day plan.”
|
| 1090 |
-
|
| 1091 |
-
### Phase 1 — Project Skeleton
|
| 1092 |
-
|
| 1093 |
-
Goal: create working HF Spaces-compatible Gradio Server app.
|
| 1094 |
-
|
| 1095 |
-
Deliverables:
|
| 1096 |
-
|
| 1097 |
-
- `app.py`
|
| 1098 |
-
- custom homepage
|
| 1099 |
-
- `requirements.txt`
|
| 1100 |
-
- `packages.txt`
|
| 1101 |
-
- README metadata
|
| 1102 |
-
|
| 1103 |
-
Success check:
|
| 1104 |
-
|
| 1105 |
-
```text
|
| 1106 |
-
python app.py
|
| 1107 |
-
```
|
| 1108 |
-
|
| 1109 |
-
opens the custom UI.
|
| 1110 |
-
|
| 1111 |
-
---
|
| 1112 |
-
|
| 1113 |
-
### Phase 2 — Model Router + Secrets (see PHASE_WISE_PLAN.md)
|
| 1114 |
-
|
| 1115 |
-
Goal: backend-only model routing with `DEFAULT_MODEL_MODE=premium_nvidia`.
|
| 1116 |
-
|
| 1117 |
-
---
|
| 1118 |
-
|
| 1119 |
-
### Phase 3 — Text Battle Engine
|
| 1120 |
-
|
| 1121 |
-
Goal: get core pitch battle working.
|
| 1122 |
-
|
| 1123 |
-
Deliverables:
|
| 1124 |
-
|
| 1125 |
-
- session manager
|
| 1126 |
-
- persona builder
|
| 1127 |
-
- attack tag selector
|
| 1128 |
-
- `/start_session`
|
| 1129 |
-
- `/chat_round`
|
| 1130 |
-
|
| 1131 |
-
Success check:
|
| 1132 |
-
|
| 1133 |
-
```text
|
| 1134 |
-
User can start a pitch battle and get hard follow-up questions.
|
| 1135 |
-
```
|
| 1136 |
-
|
| 1137 |
-
---
|
| 1138 |
-
|
| 1139 |
-
### Phase 4 — Scorecard Engine
|
| 1140 |
-
|
| 1141 |
-
Goal: turn chat into useful feedback.
|
| 1142 |
-
|
| 1143 |
-
Deliverables:
|
| 1144 |
-
|
| 1145 |
-
- scoring prompt
|
| 1146 |
-
- JSON parser
|
| 1147 |
-
- fallback parser
|
| 1148 |
-
- `/end_battle`
|
| 1149 |
-
- scorecard UI
|
| 1150 |
-
|
| 1151 |
-
Success check:
|
| 1152 |
-
|
| 1153 |
-
```text
|
| 1154 |
-
User ends battle and sees 6 scores, weakest answer, improved answer, and improved pitch.
|
| 1155 |
-
```
|
| 1156 |
-
|
| 1157 |
-
---
|
| 1158 |
-
|
| 1159 |
-
### Phase 5 — Custom UI Polish
|
| 1160 |
-
|
| 1161 |
-
Goal: make it look like a product.
|
| 1162 |
-
|
| 1163 |
-
Deliverables:
|
| 1164 |
-
|
| 1165 |
-
- battle arena layout
|
| 1166 |
-
- round counter
|
| 1167 |
-
- pressure meter
|
| 1168 |
-
- persona cards
|
| 1169 |
-
- attack tag display
|
| 1170 |
-
- score animation
|
| 1171 |
-
- loading states
|
| 1172 |
-
|
| 1173 |
-
Success check:
|
| 1174 |
-
|
| 1175 |
-
```text
|
| 1176 |
-
The app visually feels different from default Gradio.
|
| 1177 |
-
```
|
| 1178 |
-
|
| 1179 |
-
---
|
| 1180 |
-
|
| 1181 |
-
### Phase 6 — Voice Pitch Mode
|
| 1182 |
-
|
| 1183 |
-
Goal: Nemotron Omni primary voice path; faster-whisper transcription fallback only.
|
| 1184 |
-
|
| 1185 |
-
Deliverables:
|
| 1186 |
-
|
| 1187 |
-
- browser audio recorder
|
| 1188 |
-
- `/api/voice-pitch`
|
| 1189 |
-
- `transcription_client.py` (faster-whisper fallback)
|
| 1190 |
-
- transcript panel
|
| 1191 |
-
- text battle continuation from voice input
|
| 1192 |
-
|
| 1193 |
-
Success check:
|
| 1194 |
-
|
| 1195 |
-
```text
|
| 1196 |
-
User records a pitch and receives a hard first question via Nemotron (or whisper → text fallback).
|
| 1197 |
-
```
|
| 1198 |
-
|
| 1199 |
-
---
|
| 1200 |
-
|
| 1201 |
-
### Phase 7 — NVIDIA Nemotron Integration (primary)
|
| 1202 |
-
|
| 1203 |
-
Goal: Nemotron Omni 30B-A3B as default premium judge (backend-only API).
|
| 1204 |
-
|
| 1205 |
-
Deliverables:
|
| 1206 |
-
|
| 1207 |
-
- `nvidia_client.py`
|
| 1208 |
-
- model router default `premium_nvidia`
|
| 1209 |
-
- MiniCPM5-1B fallback on failure
|
| 1210 |
-
|
| 1211 |
-
Success check:
|
| 1212 |
-
|
| 1213 |
-
```text
|
| 1214 |
-
Backend returns sharp judge questions and scorecards via Nemotron; app degrades to MiniCPM5 on failure.
|
| 1215 |
-
```
|
| 1216 |
-
|
| 1217 |
-
---
|
| 1218 |
-
|
| 1219 |
-
### Phase 8 — Deployment
|
| 1220 |
-
|
| 1221 |
-
Goal: deploy to Hugging Face Spaces.
|
| 1222 |
-
|
| 1223 |
-
Deliverables:
|
| 1224 |
-
|
| 1225 |
-
- push repo
|
| 1226 |
-
- set non-secret config if needed
|
| 1227 |
-
- verify app runs publicly
|
| 1228 |
-
- test full flow
|
| 1229 |
-
|
| 1230 |
-
Success check:
|
| 1231 |
-
|
| 1232 |
-
```text
|
| 1233 |
-
HF Space link opens and completes one full battle.
|
| 1234 |
-
```
|
| 1235 |
-
|
| 1236 |
-
---
|
| 1237 |
-
|
| 1238 |
-
## 21. Minimum Viable Winning Version
|
| 1239 |
-
|
| 1240 |
-
If time gets tight, ship this:
|
| 1241 |
-
|
| 1242 |
-
- Pitch Battle only
|
| 1243 |
-
- Text input only
|
| 1244 |
-
- 3 personas
|
| 1245 |
-
- Attack tags
|
| 1246 |
-
- 6-round chat
|
| 1247 |
-
- NVIDIA Nemotron Omni via backend (MiniCPM5-1B fallback)
|
| 1248 |
-
- Scorecard
|
| 1249 |
-
- Improved answer
|
| 1250 |
-
- Custom UI
|
| 1251 |
-
- HF Spaces deployment
|
| 1252 |
-
|
| 1253 |
-
Then add Voice Mode after deployment.
|
| 1254 |
-
|
| 1255 |
-
---
|
| 1256 |
-
|
| 1257 |
-
## 22. Full Winning Version
|
| 1258 |
-
|
| 1259 |
-
Best final version:
|
| 1260 |
-
|
| 1261 |
-
- Pitch Battle
|
| 1262 |
-
- Deal Battle
|
| 1263 |
-
- Local Voice Mode
|
| 1264 |
-
- 3 pitch personas
|
| 1265 |
-
- 2 negotiation personas
|
| 1266 |
-
- Attack tags
|
| 1267 |
-
- Scorecard
|
| 1268 |
-
- Improved answer
|
| 1269 |
-
- Retry weak question
|
| 1270 |
-
- Custom Gradio Server UI
|
| 1271 |
-
- HF Spaces deployment
|
| 1272 |
-
- Documentation
|
| 1273 |
-
- Field Notes
|
| 1274 |
-
- Public prompts and rubrics
|
| 1275 |
-
- NVIDIA Nemotron + OpenBMB MiniCPM sponsor-model stack
|
| 1276 |
-
|
| 1277 |
-
---
|
| 1278 |
-
|
| 1279 |
-
## 23. Badge / Prize Strategy
|
| 1280 |
-
|
| 1281 |
-
| Target | How PitchFight AI Qualifies |
|
| 1282 |
-
|---|---|
|
| 1283 |
-
| Backyard AI | Built for student founders with a real high-pressure problem |
|
| 1284 |
-
| Best Demo | Voice + text pitch battle → scorecard in one polished flow |
|
| 1285 |
-
| Best Agent | Persona + memory + attack-tag planning + evaluation loop |
|
| 1286 |
-
| Off-Brand | Custom frontend using Gradio Server instead of default Gradio UI |
|
| 1287 |
-
| NVIDIA Nemotron Quest | Nemotron Omni as premium voice/multimodal judge |
|
| 1288 |
-
| OpenBMB Awards | MiniCPM-o, MiniCPM5-1B, MiniCPM-V integration |
|
| 1289 |
-
| Tiny Titan | MiniCPM5-1B Tiny Mode fallback |
|
| 1290 |
-
| Sharing is Caring | Publish prompts, rubrics, attack tags, sample scenarios |
|
| 1291 |
-
| Field Notes | Write short build post after deployment |
|
| 1292 |
-
| ~~Off the Grid~~ | **Not targeted** — sponsor APIs used intentionally for demo quality |
|
| 1293 |
-
|
| 1294 |
-
---
|
| 1295 |
-
|
| 1296 |
-
## 24. Environment and Safety
|
| 1297 |
-
|
| 1298 |
-
Use this `.env.example`:
|
| 1299 |
-
|
| 1300 |
-
```text
|
| 1301 |
-
APP_ENV=development
|
| 1302 |
-
MAX_ROUNDS=6
|
| 1303 |
-
|
| 1304 |
-
NVIDIA_API_KEY=
|
| 1305 |
-
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 1306 |
-
NVIDIA_OMNI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning
|
| 1307 |
-
|
| 1308 |
-
OPENBMB_API_KEY=
|
| 1309 |
-
OPENBMB_BASE_URL=
|
| 1310 |
-
MINICPM_OMNI_MODEL=openbmb/MiniCPM-o-4_5
|
| 1311 |
-
MINICPM_TEXT_MODEL=openbmb/MiniCPM5-1B
|
| 1312 |
-
MINICPM_VISION_MODEL=openbmb/MiniCPM-V-4.6
|
| 1313 |
-
|
| 1314 |
-
HF_TOKEN=
|
| 1315 |
-
WHISPER_FALLBACK_ENABLED=true
|
| 1316 |
-
WHISPER_MODEL_SIZE=tiny
|
| 1317 |
-
```
|
| 1318 |
-
|
| 1319 |
-
Use this `.gitignore`:
|
| 1320 |
-
|
| 1321 |
-
```text
|
| 1322 |
-
.env
|
| 1323 |
-
__pycache__/
|
| 1324 |
-
*.pyc
|
| 1325 |
-
.DS_Store
|
| 1326 |
-
node_modules/
|
| 1327 |
-
.gradio/
|
| 1328 |
-
models/
|
| 1329 |
-
*.gguf
|
| 1330 |
-
```
|
| 1331 |
-
|
| 1332 |
-
Security rules:
|
| 1333 |
-
|
| 1334 |
-
- Do not commit API keys.
|
| 1335 |
-
- Do not call model APIs from frontend JS — frontend uses `/api/*` only.
|
| 1336 |
-
- Store keys in `.env` locally and HF Space Secrets in deployment.
|
| 1337 |
-
- NVIDIA and OpenBMB sponsor APIs are the intended default inference path (backend-only).
|
| 1338 |
-
|
| 1339 |
-
---
|
| 1340 |
-
|
| 1341 |
-
## 25. Sample Demo Startup
|
| 1342 |
-
|
| 1343 |
-
```json
|
| 1344 |
-
{
|
| 1345 |
-
"name": "EventRadar AI",
|
| 1346 |
-
"problem": "Students miss hackathons, tech events, and startup opportunities because discovery is scattered across WhatsApp groups, LinkedIn, Luma, college clubs, and random websites.",
|
| 1347 |
-
"target_users": "College students, student founders, and early-stage builders.",
|
| 1348 |
-
"solution": "AI-powered event discovery that ranks opportunities based on skills, goals, location, and deadline urgency.",
|
| 1349 |
-
"why_ai": "The app does not just list events. It matches events to a student's profile and explains why each event is worth attending.",
|
| 1350 |
-
"competitors": "Luma, LinkedIn Events, WhatsApp groups, college club pages.",
|
| 1351 |
-
"traction": "Prototype built with scraped event data and ranking logic.",
|
| 1352 |
-
"ask": "Hackathon prize and mentor feedback."
|
| 1353 |
-
}
|
| 1354 |
-
```
|
| 1355 |
-
|
| 1356 |
-
---
|
| 1357 |
-
|
| 1358 |
-
## 26. Demo Flow
|
| 1359 |
-
|
| 1360 |
-
1. Open app.
|
| 1361 |
-
2. Click “Load Demo Startup”.
|
| 1362 |
-
3. Select “Hackathon Judge”.
|
| 1363 |
-
4. Choose “Text Battle” or “Voice Pitch”.
|
| 1364 |
-
5. AI asks:
|
| 1365 |
-
“Why does this need AI? A sorted event list with filters seems enough.”
|
| 1366 |
-
6. User gives a weak answer.
|
| 1367 |
-
7. AI selects an attack tag such as “Retention” and pushes back harder.
|
| 1368 |
-
8. End battle.
|
| 1369 |
-
9. Scorecard shows:
|
| 1370 |
-
- Overall score
|
| 1371 |
-
- 6 rubric scores
|
| 1372 |
-
- Weakest answer
|
| 1373 |
-
- Improved answer
|
| 1374 |
-
- Improved 60-second pitch
|
| 1375 |
-
- Top 3 prep questions
|
| 1376 |
-
|
| 1377 |
-
Final spoken line:
|
| 1378 |
-
|
| 1379 |
-
> **That is PitchFight AI — your first tough pitch should not be in front of a real judge.**
|
| 1380 |
-
|
| 1381 |
-
---
|
| 1382 |
-
|
| 1383 |
-
## 27. Final Implementation Checklist
|
| 1384 |
-
|
| 1385 |
-
### Core
|
| 1386 |
-
|
| 1387 |
-
- [ ] Custom Gradio Server app runs
|
| 1388 |
-
- [ ] Frontend loads from `frontend/index.html`
|
| 1389 |
-
- [ ] Frontend calls `/api/*` only (no direct model API calls)
|
| 1390 |
-
- [ ] Model router + Nemotron client wired (Phase 2+)
|
| 1391 |
-
- [ ] Session creation works
|
| 1392 |
-
- [ ] Persona prompt works
|
| 1393 |
-
- [ ] Attack tag selector works
|
| 1394 |
-
- [ ] Chat round works
|
| 1395 |
-
- [ ] Scorecard works
|
| 1396 |
-
- [ ] JSON parsing fallback works
|
| 1397 |
-
- [ ] Sample startup loads
|
| 1398 |
-
|
| 1399 |
-
### Voice
|
| 1400 |
-
|
| 1401 |
-
- [ ] Browser recorder works
|
| 1402 |
-
- [ ] Audio file reaches backend
|
| 1403 |
-
- [ ] faster-whisper transcribes locally
|
| 1404 |
-
- [ ] Transcript becomes usable context
|
| 1405 |
-
- [ ] AI asks voice-based first question
|
| 1406 |
-
|
| 1407 |
-
### NVIDIA Nemotron (primary)
|
| 1408 |
-
|
| 1409 |
-
- [ ] Backend-only Nemotron client uses HF Space Secrets / `.env`
|
| 1410 |
-
- [ ] MiniCPM5-1B fallback when Nemotron fails or keys missing
|
| 1411 |
-
- [ ] Model mode badge visible in UI
|
| 1412 |
-
|
| 1413 |
-
### Deployment
|
| 1414 |
-
|
| 1415 |
-
- [ ] HF Space created
|
| 1416 |
-
- [ ] README metadata added
|
| 1417 |
-
- [ ] `packages.txt` includes ffmpeg
|
| 1418 |
-
- [ ] App builds
|
| 1419 |
-
- [ ] Full flow tested publicly
|
| 1420 |
-
- [ ] No API keys exposed in frontend or public repo
|
| 1421 |
-
- [ ] Model router fallbacks tested when sponsor APIs fail
|
| 1422 |
-
|
| 1423 |
-
---
|
| 1424 |
-
|
| 1425 |
-
## 28. Final One-Line Description
|
| 1426 |
-
|
| 1427 |
-
**PitchFight AI is a voice-and-text AI sparring arena where student founders practice tough startup pitches, get grilled by realistic AI judges under 32B parameters, and receive a scorecard that shows exactly how to answer better.**
|
| 1428 |
-
|
| 1429 |
-
---
|
| 1430 |
-
|
| 1431 |
-
## 29. Final Project Promise
|
| 1432 |
-
|
| 1433 |
-
PitchFight AI is not trying to replace mentors or investors.
|
| 1434 |
-
|
| 1435 |
-
It prepares student founders for them.
|
| 1436 |
-
|
| 1437 |
-
> **The goal is simple: make the first hard question happen inside the app — not on stage.**
|
| 1438 |
-
|
| 1439 |
-
---
|
| 1440 |
-
|
| 1441 |
-
## 30. Reference Notes
|
| 1442 |
-
|
| 1443 |
-
- Off-the-Grid is **not** targeted; sponsor-model APIs (NVIDIA, OpenBMB) power the default high-quality demo.
|
| 1444 |
-
- All models ≤32B; Gradio Server + custom HTML/CSS/JS frontend.
|
| 1445 |
-
- `core/model_router.py` selects premium_nvidia, openbmb_omni, tiny_minicpm, vision_deck, whisper_fallback.
|
| 1446 |
-
- API keys only in HF Space Secrets / backend `os.getenv`.
|
| 1447 |
-
- See `PHASE_WISE_PLAN.md` for the 14-phase implementation roadmap.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/FIELD_NOTES.md
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Field Notes
|
| 2 |
-
|
| 3 |
-
## Why This Exists
|
| 4 |
-
|
| 5 |
-
Student founders often practice pitches alone or with friendly feedback. The first real pressure hits during judging, mentoring, investor calls, or sponsorship negotiations. PitchFight AI creates a safe but intense sparring arena before that moment.
|
| 6 |
-
|
| 7 |
-
## Strategic Pivot (Phase 0)
|
| 8 |
-
|
| 9 |
-
We moved from an **Off-the-Grid / local-first** strategy to a **high-demo sponsor-model** strategy:
|
| 10 |
-
|
| 11 |
-
- **Not targeting** Off-the-Grid badge.
|
| 12 |
-
- **Targeting** Backyard AI, Best Demo, Best Agent, Off-Brand, NVIDIA Nemotron Quest, OpenBMB Awards, Sharing is Caring, Field Notes.
|
| 13 |
-
- Still compliant: ≤32B models, Gradio, HF Spaces, no frontend API keys.
|
| 14 |
-
|
| 15 |
-
## What Was Built
|
| 16 |
-
|
| 17 |
-
### Phase 1 (Complete)
|
| 18 |
-
|
| 19 |
-
- Hugging Face Spaces-ready Gradio Server skeleton
|
| 20 |
-
- Custom battle arena frontend (HTML/CSS/JS)
|
| 21 |
-
- In-memory session manager
|
| 22 |
-
- Persona + attack-tag routing
|
| 23 |
-
- Mock scoring and feedback (placeholder for real models)
|
| 24 |
-
- Config files for personas, rubric, samples
|
| 25 |
-
|
| 26 |
-
### Planned (Phases 2–14)
|
| 27 |
-
|
| 28 |
-
- Model router + NVIDIA / OpenBMB clients
|
| 29 |
-
- Real Pitch Battle + Deal Battle engines
|
| 30 |
-
- Voice pitch mode (Nemotron primary, whisper fallback)
|
| 31 |
-
- Pitch deck critique (MiniCPM-V)
|
| 32 |
-
- Retry weakest question + downloadable report
|
| 33 |
-
- UI polish + public HF Space deployment
|
| 34 |
-
|
| 35 |
-
## What the Models Will Do
|
| 36 |
-
|
| 37 |
-
| Model | Job |
|
| 38 |
-
|---|---|
|
| 39 |
-
| Nemotron Omni | Premium judge, voice, scoring, rewrites |
|
| 40 |
-
| MiniCPM-o | OpenBMB multimodal mode |
|
| 41 |
-
| MiniCPM5-1B | Tiny/fallback text |
|
| 42 |
-
| MiniCPM-V 4.6 | Slide critique |
|
| 43 |
-
| faster-whisper | Backup transcription |
|
| 44 |
-
|
| 45 |
-
## What Worked
|
| 46 |
-
|
| 47 |
-
- Clean separation: frontend → Gradio API → model router → clients
|
| 48 |
-
- Attack-tag driven pressure design
|
| 49 |
-
- Runnable Phase 1 skeleton for fast iteration
|
| 50 |
-
- Clear phase-wise plan for hackathon execution
|
| 51 |
-
|
| 52 |
-
## What Will Be Improved
|
| 53 |
-
|
| 54 |
-
- Replace mock APIs with Nemotron + MiniCPM routing (Phase 2–5)
|
| 55 |
-
- Voice mode as headline demo feature (Phase 7)
|
| 56 |
-
- Deal Battle + deck critique (Phases 8–10)
|
| 57 |
-
- Product polish and public deployment (Phases 12–14)
|
| 58 |
-
|
| 59 |
-
## Build Log
|
| 60 |
-
|
| 61 |
-
### Phase 2 — Model Router + Secrets Setup (2026-06-08)
|
| 62 |
-
|
| 63 |
-
**What shipped:**
|
| 64 |
-
- `core/nvidia_client.py` — backend-only NVIDIA Nemotron client using OpenAI-compatible SDK
|
| 65 |
-
- `core/model_router.py` — central routing layer (premium_nvidia live; other modes return clean stubs)
|
| 66 |
-
- `core/minicpm_client.py` — health_check stub (Phase 9 placeholder)
|
| 67 |
-
- `core/vision_client.py` — health_check stub (Phase 10 placeholder)
|
| 68 |
-
- `core/transcription_client.py` — health_check stub (Phase 7 placeholder)
|
| 69 |
-
- `scripts/test_nvidia_client.py` — isolated connectivity test (exits 0/1, no key exposure)
|
| 70 |
-
- `GET /api/model-health` — config status endpoint, keys never exposed
|
| 71 |
-
- `requirements.txt` updated: added `openai>=1.0.0`, `httpx`, `requests`
|
| 72 |
-
|
| 73 |
-
**What worked:**
|
| 74 |
-
- NVIDIA API key read from `NVIDIA_API_KEY` env var only — no hardcoding, no key leak
|
| 75 |
-
- `health_check()` correctly reports configured/not-configured without printing key
|
| 76 |
-
- `generate_nemotron_response()` returns clean judge question on first real test
|
| 77 |
-
- `get_model_health()` returns correct provider status for all four providers
|
| 78 |
-
- Error handling catches timeout, connection, and status errors cleanly
|
| 79 |
-
|
| 80 |
-
**What surprised us:**
|
| 81 |
-
- `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` is a **reasoning model**: it writes an internal chain-of-thought before the final answer. When `max_tokens` is too small (< ~400 for opponent mode), all tokens are consumed by the reasoning trace and `message.content` is `None`.
|
| 82 |
-
- The fix: increase `max_tokens` defaults (opponent: 500, scorecard: 2000, rewrite: 800) and add a `reasoning_content` fallback for edge cases.
|
| 83 |
-
- The final answer in `message.content` is sharp, in-character, and correctly short — the reasoning trace stays internal.
|
| 84 |
-
|
| 85 |
-
**What the model returned (live test):**
|
| 86 |
-
> "How does your AI differentiate between similar events and avoid false positives in ranking?"
|
| 87 |
-
|
| 88 |
-
**What we'd do differently:**
|
| 89 |
-
- Test with the actual model before setting token defaults — reasoning models have different budget dynamics than completion models.
|
| 90 |
-
|
| 91 |
-
**What's next:**
|
| 92 |
-
- Phase 3: Wire `model_router.generate_opponent_response()` into `handle_start_session` and `handle_chat_round` in `core/api_handlers.py` — replacing hardcoded mock followups with live Nemotron responses.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/MODELS_FINAL.md
DELETED
|
@@ -1,434 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Final Model Strategy
|
| 2 |
-
|
| 3 |
-
## High-Demo Sponsor-Model Stack Under 32B Parameters
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## 1. Final Model Philosophy
|
| 8 |
-
|
| 9 |
-
PitchFight AI prioritizes:
|
| 10 |
-
|
| 11 |
-
- **Best demonstrative experience** — the app must feel like a real sparring arena, not a chatbot wrapper.
|
| 12 |
-
- **Voice-first pitch practice** — founders pitch out loud; the judge responds with pressure, not generic advice.
|
| 13 |
-
- **High-quality AI judge behavior** — persona-locked opponents, attack tags, memory, and sharp follow-ups.
|
| 14 |
-
- **Sponsor-model alignment** — NVIDIA Nemotron Omni and OpenBMB MiniCPM models used intentionally and visibly.
|
| 15 |
-
- **Strong fallback reliability** — if a premium endpoint fails, the app degrades gracefully instead of breaking the demo.
|
| 16 |
-
- **Models under 32B parameters** — every reasoning model in the stack complies with the Build Small Hackathon cap.
|
| 17 |
-
|
| 18 |
-
The project is **not** targeting the **Off-the-Grid** badge. This is intentional — Off-the-Grid is not part of this build.
|
| 19 |
-
|
| 20 |
-
The app **does** follow the required hackathon rules:
|
| 21 |
-
|
| 22 |
-
- **≤32B models** for all core reasoning
|
| 23 |
-
- **Gradio** + **Hugging Face Spaces** hosting
|
| 24 |
-
- **Demo-first** execution with a custom non-default UI
|
| 25 |
-
|
| 26 |
-
**Target badges/prizes:** Backyard AI, Best Demo, Best Agent, Off-Brand, NVIDIA Nemotron Quest, OpenBMB Awards, Sharing is Caring, and Field Notes.
|
| 27 |
-
|
| 28 |
-
Cloud/sponsor model APIs are **allowed and expected** for this build when called **backend-only** with keys in HF Space Secrets. They are not forbidden — they are the default path to demo quality.
|
| 29 |
-
|
| 30 |
-
---
|
| 31 |
-
|
| 32 |
-
## 2. Final Default Model Stack
|
| 33 |
-
|
| 34 |
-
| Layer | Final Model | Default? | Purpose |
|
| 35 |
-
|---|---|---:|---|
|
| 36 |
-
| Premium reasoning + voice/multimodal judge | NVIDIA Nemotron 3 Nano Omni 30B-A3B | Yes | Main voice/text judge, pitch battle, deal battle, scorecard, voice critique |
|
| 37 |
-
| OpenBMB omni mode | MiniCPM-o 4.5 | Secondary | OpenBMB-aligned omni mode for voice/text/multimodal pitch interaction |
|
| 38 |
-
| Tiny fallback | MiniCPM5-1B | Fallback | Fast text battle, scorecard fallback, Tiny Mode |
|
| 39 |
-
| Vision/deck critique | MiniCPM-V 4.6 | Feature mode | Pitch deck screenshot critique and visual pitch feedback |
|
| 40 |
-
| Audio fallback | faster-whisper / Whisper Tiny or Base | Fallback | Audio transcription if direct audio handling is unstable |
|
| 41 |
-
| Optional multilingual | Cohere Aya or suitable small multilingual model | Optional | Hindi/Kannada/regional pitch practice if added |
|
| 42 |
-
| Optional visuals | FLUX / Black Forest Labs model | Optional assets only | Persona art or visual assets, not core reasoning |
|
| 43 |
-
|
| 44 |
-
---
|
| 45 |
-
|
| 46 |
-
## 3. Primary Model — NVIDIA Nemotron 3 Nano Omni 30B-A3B
|
| 47 |
-
|
| 48 |
-
> **Deep dive:** How Omni processes raw audio, paralinguistic signals, hesitation detection, and PitchFight voice paths A/B — see [`NEMOTRON_OMNI_AUDIO.md`](NEMOTRON_OMNI_AUDIO.md).
|
| 49 |
-
|
| 50 |
-
### Role
|
| 51 |
-
|
| 52 |
-
Primary premium model for the strongest demo.
|
| 53 |
-
|
| 54 |
-
### Use cases
|
| 55 |
-
|
| 56 |
-
- Voice pitch understanding
|
| 57 |
-
- AI opponent responses
|
| 58 |
-
- Pitch Battle
|
| 59 |
-
- Deal Battle
|
| 60 |
-
- Multimodal pitch judging
|
| 61 |
-
- Scorecard generation
|
| 62 |
-
- Weakest answer rewrite
|
| 63 |
-
- Improved pitch generation
|
| 64 |
-
- Voice-specific feedback
|
| 65 |
-
- Optional pitch deck + spoken pitch critique
|
| 66 |
-
|
| 67 |
-
### Why
|
| 68 |
-
|
| 69 |
-
- Strong sponsor alignment (NVIDIA Nemotron Quest)
|
| 70 |
-
- Multimodal / omni capability
|
| 71 |
-
- Under 32B total parameter rule
|
| 72 |
-
- Best fit for high-quality voice demo
|
| 73 |
-
- Targets NVIDIA Nemotron Quest
|
| 74 |
-
|
| 75 |
-
### Implementation
|
| 76 |
-
|
| 77 |
-
- Backend-only API call.
|
| 78 |
-
- API key stored in HF Space Secrets.
|
| 79 |
-
- Frontend never calls NVIDIA directly.
|
| 80 |
-
- Model call goes through `core/nvidia_client.py` and `core/model_router.py`.
|
| 81 |
-
|
| 82 |
-
### Environment variables
|
| 83 |
-
|
| 84 |
-
```text
|
| 85 |
-
NVIDIA_API_KEY=
|
| 86 |
-
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 87 |
-
NVIDIA_OMNI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning
|
| 88 |
-
```
|
| 89 |
-
|
| 90 |
-
### Suggested settings
|
| 91 |
-
|
| 92 |
-
| Task | Temperature | Max Tokens |
|
| 93 |
-
|---|---:|---:|
|
| 94 |
-
| Opponent response | 0.7 | 250 |
|
| 95 |
-
| Scorecard | 0.2 | 1200 |
|
| 96 |
-
| Rewrite | 0.45 | 500 |
|
| 97 |
-
|
| 98 |
-
---
|
| 99 |
-
|
| 100 |
-
## 4. OpenBMB Mode — MiniCPM-o 4.5
|
| 101 |
-
|
| 102 |
-
### Role
|
| 103 |
-
|
| 104 |
-
OpenBMB-aligned advanced omni model.
|
| 105 |
-
|
| 106 |
-
### Use cases
|
| 107 |
-
|
| 108 |
-
- Alternative voice judge mode
|
| 109 |
-
- Speech/text pitch interaction
|
| 110 |
-
- OpenBMB sponsor route
|
| 111 |
-
- Model comparison mode
|
| 112 |
-
- Optional multimodal interaction
|
| 113 |
-
|
| 114 |
-
### Why
|
| 115 |
-
|
| 116 |
-
- 9B model, under 32B
|
| 117 |
-
- Strong OpenBMB alignment
|
| 118 |
-
- Supports omnimodal interaction
|
| 119 |
-
- Useful for OpenBMB Awards
|
| 120 |
-
- Smaller than Nemotron
|
| 121 |
-
|
| 122 |
-
### Implementation
|
| 123 |
-
|
| 124 |
-
- Backend-only API call or supported official endpoint.
|
| 125 |
-
- Keep separate from NVIDIA path.
|
| 126 |
-
- Can be exposed in UI as **“OpenBMB Omni Mode”**.
|
| 127 |
-
|
| 128 |
-
### Environment variables
|
| 129 |
-
|
| 130 |
-
```text
|
| 131 |
-
OPENBMB_API_KEY=
|
| 132 |
-
OPENBMB_BASE_URL=
|
| 133 |
-
MINICPM_OMNI_MODEL=openbmb/MiniCPM-o-4_5
|
| 134 |
-
```
|
| 135 |
-
|
| 136 |
-
---
|
| 137 |
-
|
| 138 |
-
## 5. Tiny / Fallback Mode — MiniCPM5-1B
|
| 139 |
-
|
| 140 |
-
### Role
|
| 141 |
-
|
| 142 |
-
Reliable small fallback model.
|
| 143 |
-
|
| 144 |
-
### Use cases
|
| 145 |
-
|
| 146 |
-
- Text Pitch Battle fallback
|
| 147 |
-
- Deal Battle fallback
|
| 148 |
-
- Scorecard fallback
|
| 149 |
-
- Tiny Mode
|
| 150 |
-
- Low-latency text mode
|
| 151 |
-
- Tiny Titan angle if shipped well
|
| 152 |
-
|
| 153 |
-
### Why
|
| 154 |
-
|
| 155 |
-
- Very small
|
| 156 |
-
- OpenBMB-aligned
|
| 157 |
-
- Good backup if premium endpoints are slow
|
| 158 |
-
- Keeps app usable during API failures
|
| 159 |
-
|
| 160 |
-
### Implementation
|
| 161 |
-
|
| 162 |
-
- Use through backend model router.
|
| 163 |
-
- Can be local or hosted depending on final deployment.
|
| 164 |
-
- If using API, clearly do not claim Off-the-Grid.
|
| 165 |
-
- If local, can be mentioned as optional local fallback.
|
| 166 |
-
|
| 167 |
-
### Environment variables
|
| 168 |
-
|
| 169 |
-
```text
|
| 170 |
-
MINICPM_TEXT_MODEL=openbmb/MiniCPM5-1B
|
| 171 |
-
MINICPM_TEXT_BACKEND=api_or_local
|
| 172 |
-
```
|
| 173 |
-
|
| 174 |
-
---
|
| 175 |
-
|
| 176 |
-
## 6. Vision Mode — MiniCPM-V 4.6
|
| 177 |
-
|
| 178 |
-
### Role
|
| 179 |
-
|
| 180 |
-
Pitch deck screenshot critique.
|
| 181 |
-
|
| 182 |
-
### Use cases
|
| 183 |
-
|
| 184 |
-
- Upload slide screenshot
|
| 185 |
-
- Extract visible claims
|
| 186 |
-
- Critique clarity
|
| 187 |
-
- Critique problem/solution/market/ask
|
| 188 |
-
- Generate judge questions from slide
|
| 189 |
-
|
| 190 |
-
### Why
|
| 191 |
-
|
| 192 |
-
- OpenBMB-aligned
|
| 193 |
-
- Small visual model
|
| 194 |
-
- Strong demo feature
|
| 195 |
-
- Adds multimodal value beyond chat
|
| 196 |
-
|
| 197 |
-
### Implementation
|
| 198 |
-
|
| 199 |
-
- Add `/deck_critique` endpoint.
|
| 200 |
-
- Frontend image upload.
|
| 201 |
-
- Backend routes image to MiniCPM-V or NVIDIA Omni.
|
| 202 |
-
- Show slide critique card.
|
| 203 |
-
|
| 204 |
-
### Environment variables
|
| 205 |
-
|
| 206 |
-
```text
|
| 207 |
-
MINICPM_VISION_MODEL=openbmb/MiniCPM-V-4.6
|
| 208 |
-
```
|
| 209 |
-
|
| 210 |
-
---
|
| 211 |
-
|
| 212 |
-
## 7. Audio Fallback — faster-whisper
|
| 213 |
-
|
| 214 |
-
### Role
|
| 215 |
-
|
| 216 |
-
Backup transcription.
|
| 217 |
-
|
| 218 |
-
### Use cases
|
| 219 |
-
|
| 220 |
-
- Convert audio to transcript if direct omni audio fails
|
| 221 |
-
- Debug voice input
|
| 222 |
-
- Allow browser-recorded audio to become text reliably
|
| 223 |
-
|
| 224 |
-
### Why
|
| 225 |
-
|
| 226 |
-
- Reliable
|
| 227 |
-
- Simple
|
| 228 |
-
- Helps prevent voice demo failure
|
| 229 |
-
|
| 230 |
-
### Implementation
|
| 231 |
-
|
| 232 |
-
- **Not** the main judge.
|
| 233 |
-
- Only audio-to-text fallback.
|
| 234 |
-
- Transcript then goes into selected reasoning model.
|
| 235 |
-
|
| 236 |
-
### Environment variables
|
| 237 |
-
|
| 238 |
-
```text
|
| 239 |
-
WHISPER_FALLBACK_ENABLED=true
|
| 240 |
-
WHISPER_MODEL_SIZE=tiny
|
| 241 |
-
```
|
| 242 |
-
|
| 243 |
-
---
|
| 244 |
-
|
| 245 |
-
## 8. Model Router
|
| 246 |
-
|
| 247 |
-
Routing logic for `core/model_router.py`:
|
| 248 |
-
|
| 249 |
-
### Text Pitch Battle
|
| 250 |
-
|
| 251 |
-
```text
|
| 252 |
-
startup context + persona + attack tag
|
| 253 |
-
→ NVIDIA Nemotron by default
|
| 254 |
-
→ MiniCPM5-1B fallback if NVIDIA fails
|
| 255 |
-
```
|
| 256 |
-
|
| 257 |
-
### Voice Pitch
|
| 258 |
-
|
| 259 |
-
```text
|
| 260 |
-
audio
|
| 261 |
-
→ NVIDIA Nemotron Omni primary
|
| 262 |
-
→ if fails: faster-whisper transcription
|
| 263 |
-
→ MiniCPM5-1B or Nemotron text path
|
| 264 |
-
```
|
| 265 |
-
|
| 266 |
-
### Deal Battle
|
| 267 |
-
|
| 268 |
-
```text
|
| 269 |
-
negotiation context
|
| 270 |
-
→ NVIDIA Nemotron by default
|
| 271 |
-
→ MiniCPM5-1B fallback
|
| 272 |
-
```
|
| 273 |
-
|
| 274 |
-
### Scorecard
|
| 275 |
-
|
| 276 |
-
```text
|
| 277 |
-
conversation history
|
| 278 |
-
→ NVIDIA Nemotron by default
|
| 279 |
-
→ MiniCPM5-1B fallback
|
| 280 |
-
→ json_utils parser + fallback_scorecard()
|
| 281 |
-
```
|
| 282 |
-
|
| 283 |
-
### Deck Critique
|
| 284 |
-
|
| 285 |
-
```text
|
| 286 |
-
image
|
| 287 |
-
→ MiniCPM-V 4.6 or NVIDIA Omni
|
| 288 |
-
→ slide critique + judge questions
|
| 289 |
-
```
|
| 290 |
-
|
| 291 |
-
### OpenBMB Mode
|
| 292 |
-
|
| 293 |
-
```text
|
| 294 |
-
user-selected
|
| 295 |
-
→ MiniCPM-o 4.5
|
| 296 |
-
```
|
| 297 |
-
|
| 298 |
-
### Tiny Mode
|
| 299 |
-
|
| 300 |
-
```text
|
| 301 |
-
user-selected or automatic fallback
|
| 302 |
-
→ MiniCPM5-1B
|
| 303 |
-
```
|
| 304 |
-
|
| 305 |
-
---
|
| 306 |
-
|
| 307 |
-
## 9. Model-to-File Mapping
|
| 308 |
-
|
| 309 |
-
| File | Responsibility | Phase 2 Status |
|
| 310 |
-
|---|---|---|
|
| 311 |
-
| `core/model_router.py` | Routes tasks to NVIDIA, MiniCPM-o, MiniCPM5, MiniCPM-V, or Whisper fallback | **Created** — premium_nvidia live; other modes stub |
|
| 312 |
-
| `core/nvidia_client.py` | Calls NVIDIA Nemotron Omni backend-only | **Created** — live, tested |
|
| 313 |
-
| `core/minicpm_client.py` | Calls MiniCPM-o and MiniCPM5 paths | **Stub** — health_check only (Phase 9) |
|
| 314 |
-
| `core/vision_client.py` | Handles MiniCPM-V / deck critique model calls | **Stub** — health_check only (Phase 10) |
|
| 315 |
-
| `core/transcription_client.py` | Handles faster-whisper fallback transcription | **Stub** — health_check only (Phase 7) |
|
| 316 |
-
| `core/session_manager.py` | Stores selected model mode and conversation history | Existing — unchanged |
|
| 317 |
-
| `core/persona_builder.py` | Builds prompt before model call | Existing — unchanged |
|
| 318 |
-
| `core/attack_tags.py` | Selects pressure direction before model call | Existing — unchanged |
|
| 319 |
-
| `core/scoring_engine.py` | Builds scorecard prompt and calls model_router | Existing — still mock (Phase 5) |
|
| 320 |
-
|
| 321 |
-
> **Reasoning model token note:** `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` writes an internal chain-of-thought before producing `message.content`. Confirmed defaults: opponent=500, scorecard=2000, rewrite=800 tokens.
|
| 322 |
-
| `core/json_utils.py` | Repairs/parses model-generated scorecards |
|
| 323 |
-
|
| 324 |
-
---
|
| 325 |
-
|
| 326 |
-
## 10. Environment Variables
|
| 327 |
-
|
| 328 |
-
Final environment block:
|
| 329 |
-
|
| 330 |
-
```text
|
| 331 |
-
APP_ENV=development
|
| 332 |
-
MAX_ROUNDS=6
|
| 333 |
-
|
| 334 |
-
DEFAULT_MODEL_MODE=premium_nvidia
|
| 335 |
-
|
| 336 |
-
NVIDIA_API_KEY=
|
| 337 |
-
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 338 |
-
NVIDIA_OMNI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning
|
| 339 |
-
|
| 340 |
-
OPENBMB_API_KEY=
|
| 341 |
-
OPENBMB_BASE_URL=
|
| 342 |
-
MINICPM_OMNI_MODEL=openbmb/MiniCPM-o-4_5
|
| 343 |
-
MINICPM_TEXT_MODEL=openbmb/MiniCPM5-1B
|
| 344 |
-
MINICPM_VISION_MODEL=openbmb/MiniCPM-V-4.6
|
| 345 |
-
|
| 346 |
-
HF_TOKEN=
|
| 347 |
-
|
| 348 |
-
WHISPER_FALLBACK_ENABLED=true
|
| 349 |
-
WHISPER_MODEL_SIZE=tiny
|
| 350 |
-
|
| 351 |
-
ENABLE_DECK_CRITIQUE=true
|
| 352 |
-
ENABLE_DEAL_BATTLE=true
|
| 353 |
-
ENABLE_VOICE_MODE=true
|
| 354 |
-
```
|
| 355 |
-
|
| 356 |
-
**Security rules:**
|
| 357 |
-
|
| 358 |
-
- API keys must be **backend-only**.
|
| 359 |
-
- Use **HF Space Secrets** in production.
|
| 360 |
-
- Never expose keys in frontend JS.
|
| 361 |
-
- Do not commit `.env`.
|
| 362 |
-
|
| 363 |
-
---
|
| 364 |
-
|
| 365 |
-
## 11. Requirements Strategy
|
| 366 |
-
|
| 367 |
-
### Skeleton (always)
|
| 368 |
-
|
| 369 |
-
```text
|
| 370 |
-
gradio
|
| 371 |
-
fastapi
|
| 372 |
-
uvicorn
|
| 373 |
-
pydantic
|
| 374 |
-
python-dotenv
|
| 375 |
-
numpy
|
| 376 |
-
```
|
| 377 |
-
|
| 378 |
-
### API/model clients
|
| 379 |
-
|
| 380 |
-
```text
|
| 381 |
-
openai
|
| 382 |
-
requests
|
| 383 |
-
httpx
|
| 384 |
-
```
|
| 385 |
-
|
| 386 |
-
### Voice fallback
|
| 387 |
-
|
| 388 |
-
```text
|
| 389 |
-
faster-whisper
|
| 390 |
-
```
|
| 391 |
-
|
| 392 |
-
### Audio/system
|
| 393 |
-
|
| 394 |
-
```text
|
| 395 |
-
packages.txt includes ffmpeg
|
| 396 |
-
```
|
| 397 |
-
|
| 398 |
-
### Optional image handling
|
| 399 |
-
|
| 400 |
-
```text
|
| 401 |
-
pillow
|
| 402 |
-
```
|
| 403 |
-
|
| 404 |
-
**Note:** Do not add heavy local inference dependencies unless needed. This build prioritizes **API-backed sponsor models** for demo quality. Local inference (e.g. llama-cpp-python) is optional for Tiny Mode fallback only — not the default path.
|
| 405 |
-
|
| 406 |
-
---
|
| 407 |
-
|
| 408 |
-
## 12. Model Architecture Diagram
|
| 409 |
-
|
| 410 |
-
```mermaid
|
| 411 |
-
flowchart TD
|
| 412 |
-
A[User Text Pitch] --> B[Custom Gradio UI]
|
| 413 |
-
C[User Voice Pitch] --> B
|
| 414 |
-
D[Pitch Deck Screenshot] --> B
|
| 415 |
-
|
| 416 |
-
B --> E[Gradio Server Backend]
|
| 417 |
-
E --> F[Model Router]
|
| 418 |
-
|
| 419 |
-
F --> G[NVIDIA Nemotron Omni\nPremium Judge Mode]
|
| 420 |
-
F --> H[MiniCPM-o 4.5\nOpenBMB Omni Mode]
|
| 421 |
-
F --> I[MiniCPM5-1B\nTiny/Fallback Mode]
|
| 422 |
-
F --> J[MiniCPM-V 4.6\nDeck Critique Mode]
|
| 423 |
-
F --> K[faster-whisper\nAudio Fallback]
|
| 424 |
-
|
| 425 |
-
G --> L[Opponent Response + Scorecard]
|
| 426 |
-
H --> L
|
| 427 |
-
I --> L
|
| 428 |
-
J --> M[Slide Critique + Judge Questions]
|
| 429 |
-
K --> N[Transcript]
|
| 430 |
-
N --> F
|
| 431 |
-
|
| 432 |
-
L --> O[Battle Arena UI]
|
| 433 |
-
M --> O
|
| 434 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/NEMOTRON_OMNI_AUDIO.md
DELETED
|
@@ -1,294 +0,0 @@
|
|
| 1 |
-
# NVIDIA Nemotron 3 Nano Omni — Audio & Voice Architecture
|
| 2 |
-
|
| 3 |
-
Technical reference for how **NVIDIA Nemotron 3 Nano Omni 30B-A3B** processes pitch audio in PitchFight AI, what it can infer about hesitation and confidence, and how the app should wire voice mode.
|
| 4 |
-
|
| 5 |
-
> **PitchFight context:** Nemotron Omni is the **primary premium judge** (backend-only API). Raw audio goes to the model on the primary path. **faster-whisper** is transcription fallback only — not the main judge. Frontend never calls NVIDIA directly; audio flows through `/api/voice-pitch`.
|
| 6 |
-
|
| 7 |
-
See also: [`MODELS_FINAL.md`](MODELS_FINAL.md) · [`BACKEND_API.md`](BACKEND_API.md) · [`PHASE_WISE_PLAN.md`](PHASE_WISE_PLAN.md)
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## 1. Model Identity
|
| 12 |
-
|
| 13 |
-
| Field | Value |
|
| 14 |
-
|---|---|
|
| 15 |
-
| **Model** | NVIDIA Nemotron 3 Nano Omni 30B-A3B |
|
| 16 |
-
| **Env var** | `NVIDIA_OMNI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` |
|
| 17 |
-
| **Parameter cap** | ~30B — complies with Build Small ≤32B rule |
|
| 18 |
-
| **Access** | Backend-only via `NVIDIA_API_KEY` + `NVIDIA_BASE_URL` |
|
| 19 |
-
| **Role in PitchFight** | Primary voice/text/multimodal judge, scorecard, rewrites |
|
| 20 |
-
|
| 21 |
-
---
|
| 22 |
-
|
| 23 |
-
## 2. How the Model Works With Audio
|
| 24 |
-
|
| 25 |
-
### Unified encoder–projector–decoder design
|
| 26 |
-
|
| 27 |
-
NeMoTron Nano Omni uses a **unified multimodal architecture**:
|
| 28 |
-
|
| 29 |
-
- **Language backbone:** Nemotron 3 Nano 30B-A3B (reasoning)
|
| 30 |
-
- **Vision encoder:** C-RADIOv4-H
|
| 31 |
-
- **Audio encoder:** Parakeet-TDT-0.6B-v2
|
| 32 |
-
|
| 33 |
-
Modality-specific encoders connect into the LLM backbone through **lightweight projectors**. Audio and text share the same reasoning loop inside the backbone.
|
| 34 |
-
|
| 35 |
-
### Exact pipeline when an audio file hits the model
|
| 36 |
-
|
| 37 |
-
```text
|
| 38 |
-
Your WAV / audio file (raw bytes)
|
| 39 |
-
↓
|
| 40 |
-
Parakeet-TDT-0.6B-v2 (dedicated audio encoder)
|
| 41 |
-
↓ converts audio into audio tokens
|
| 42 |
-
Lightweight projector (aligns audio tokens to LLM embedding space)
|
| 43 |
-
↓
|
| 44 |
-
Nemotron 30B-A3B backbone (reasoning happens here)
|
| 45 |
-
↓
|
| 46 |
-
Structured text output (transcript, observations, judge question, etc.)
|
| 47 |
-
```
|
| 48 |
-
|
| 49 |
-
### What this is *not*
|
| 50 |
-
|
| 51 |
-
This is **fundamentally different** from Whisper → LLM:
|
| 52 |
-
|
| 53 |
-
| Approach | Flow |
|
| 54 |
-
|---|---|
|
| 55 |
-
| **Whisper → LLM** | Audio → text transcript → text-only reasoning |
|
| 56 |
-
| **Nemotron Omni (primary path)** | Audio → audio tokens → projected embeddings → unified reasoning |
|
| 57 |
-
|
| 58 |
-
On the Nemotron primary path, the model does **not** need to transcribe to plain text first before reasoning. Audio signals are preserved as tokens that flow into the backbone.
|
| 59 |
-
|
| 60 |
-
### Paralinguistic coverage
|
| 61 |
-
|
| 62 |
-
Parakeet provides strong ASR. NVIDIA extends the audio surface with **Granary** and **Music Flamingo** extensions, broadening coverage beyond bare transcription to include:
|
| 63 |
-
|
| 64 |
-
- Paralinguistic signals (tone, pace, stress, hesitation sounds)
|
| 65 |
-
- Music and ambient sound understanding
|
| 66 |
-
|
| 67 |
-
**Paralinguistic** = signals beyond literal words — how something is said, not only what is said. This is what makes the model competitive on voice benchmarks and useful for media understanding, not just dictation.
|
| 68 |
-
|
| 69 |
-
---
|
| 70 |
-
|
| 71 |
-
## 3. Quick Answers (FAQ)
|
| 72 |
-
|
| 73 |
-
| Question | Answer |
|
| 74 |
-
|---|---|
|
| 75 |
-
| Does the raw audio file go to Nemotron? | **Yes** — Parakeet processes native audio input. |
|
| 76 |
-
| Does it get transcribed to text first? | **No** on the primary Omni path — audio tokens flow directly into reasoning. |
|
| 77 |
-
| Does it use embeddings? | **Yes** — audio tokens are projected into the LLM embedding space. |
|
| 78 |
-
| Can it detect hesitation? | **Yes** — linguistic hesitation and filler words reliably. |
|
| 79 |
-
| Can it detect confidence from voice tone? | **Partially** — paralinguistic inference, not clinical prosody analysis. |
|
| 80 |
-
| Is this strong enough for a pitch trainer? | **Yes** — linguistic hesitation is the most actionable signal for founders. |
|
| 81 |
-
| How do you make it accurate? | **Prompt explicitly** for delivery observations alongside content. |
|
| 82 |
-
|
| 83 |
-
---
|
| 84 |
-
|
| 85 |
-
## 4. Hesitation & Confidence Detection
|
| 86 |
-
|
| 87 |
-
### Partially yes — with important caveats
|
| 88 |
-
|
| 89 |
-
#### What it can detect reliably
|
| 90 |
-
|
| 91 |
-
The Parakeet encoder (with Granary / Music Flamingo extensions) processes **paralinguistic features**. When preserved as audio tokens in the backbone, the LLM can reason about:
|
| 92 |
-
|
| 93 |
-
| Signal | Example |
|
| 94 |
-
|---|---|
|
| 95 |
-
| Filler words | "um", "uh", "like", "you know" |
|
| 96 |
-
| Long pauses | Mid-sentence gaps before a key claim |
|
| 97 |
-
| Trailing off | Weak endings on important statements |
|
| 98 |
-
| Rushed speech | Compressed delivery on uncertain phrases |
|
| 99 |
-
| Repetition / self-correction | Restarts, hedging, reformulations |
|
| 100 |
-
|
| 101 |
-
For pitch training, **linguistic hesitation is often the most useful signal**:
|
| 102 |
-
|
| 103 |
-
> *"Um… I think… maybe the market is… around 10 million?"*
|
| 104 |
-
> vs
|
| 105 |
-
> *"The addressable market is $10M based on X."*
|
| 106 |
-
|
| 107 |
-
The model can flag the first pattern when prompted correctly.
|
| 108 |
-
|
| 109 |
-
#### What it cannot detect reliably
|
| 110 |
-
|
| 111 |
-
Pure acoustic / physiological confidence signals require dedicated prosody tools (e.g. Praat, specialized emotion models), not a general-purpose multimodal LLM:
|
| 112 |
-
|
| 113 |
-
- Heart rate change
|
| 114 |
-
- Micro-tremors in voice
|
| 115 |
-
- Subtle pitch drops indicating clinical-level uncertainty
|
| 116 |
-
|
| 117 |
-
**Do not claim** heart-rate or micro-tremor detection in PitchFight UI or demo script.
|
| 118 |
-
|
| 119 |
-
#### Honest positioning
|
| 120 |
-
|
| 121 |
-
NeMoTron can detect **linguistic hesitation** (words and sounds of hesitation) and make **reasonable inferences** about apparent confidence from speech patterns. It cannot do **clinical-grade prosody analysis**. For a student pitch trainer, that is the right tradeoff.
|
| 122 |
-
|
| 123 |
-
---
|
| 124 |
-
|
| 125 |
-
## 5. Voice Scorecard Metrics (Grounded in Architecture)
|
| 126 |
-
|
| 127 |
-
Only score dimensions the model can plausibly observe:
|
| 128 |
-
|
| 129 |
-
| Metric | How Nemotron detects it | Reliability |
|
| 130 |
-
|---|---|---|
|
| 131 |
-
| Filler word count | Direct from audio tokens + transcript | **High** |
|
| 132 |
-
| Pause before key claims | Temporal gap in audio | **Medium–High** |
|
| 133 |
-
| Self-corrections | Restarts in transcript / audio pattern | **High** |
|
| 134 |
-
| Trailing off on weak claims | Audio token patterns | **Medium** |
|
| 135 |
-
| Speaking pace (rushed vs calm) | Token density over time | **Medium** |
|
| 136 |
-
| Confidence on specific claims | Inference from above signals | **Medium** |
|
| 137 |
-
|
| 138 |
-
When on **Whisper fallback**, mark voice-derived metrics as unavailable or transcript-only.
|
| 139 |
-
|
| 140 |
-
---
|
| 141 |
-
|
| 142 |
-
## 6. PitchFight AI Voice Architecture
|
| 143 |
-
|
| 144 |
-
### Path A — Nemotron direct (primary, when API available)
|
| 145 |
-
|
| 146 |
-
```text
|
| 147 |
-
Browser records audio
|
| 148 |
-
↓
|
| 149 |
-
POST /api/voice-pitch (backend only)
|
| 150 |
-
↓
|
| 151 |
-
NVIDIA Nemotron Omni
|
| 152 |
-
↓
|
| 153 |
-
Parakeet encoder processes raw audio
|
| 154 |
-
↓
|
| 155 |
-
Paralinguistic + linguistic signals preserved in backbone
|
| 156 |
-
↓
|
| 157 |
-
Single API response includes:
|
| 158 |
-
- transcript
|
| 159 |
-
- delivery_observations (hesitation / pace / fillers)
|
| 160 |
-
- detected_startup_context
|
| 161 |
-
- opening judge question
|
| 162 |
-
↓
|
| 163 |
-
Pitch Battle continues (voice or text)
|
| 164 |
-
```
|
| 165 |
-
|
| 166 |
-
**UI badge:** `⚡ Voice Analysis Active` (or similar) when Path A is active.
|
| 167 |
-
|
| 168 |
-
### Path B — Whisper fallback (when Nemotron audio path fails)
|
| 169 |
-
|
| 170 |
-
```text
|
| 171 |
-
Browser records audio
|
| 172 |
-
↓
|
| 173 |
-
POST /api/voice-pitch
|
| 174 |
-
↓
|
| 175 |
-
faster-whisper local transcription
|
| 176 |
-
↓
|
| 177 |
-
Clean transcript text only (no paralinguistic tokens)
|
| 178 |
-
↓
|
| 179 |
-
Nemotron or MiniCPM5-1B text reasoning
|
| 180 |
-
↓
|
| 181 |
-
Judge question (no delivery observations)
|
| 182 |
-
↓
|
| 183 |
-
Scorecard notes: "Voice delivery metrics unavailable in fallback mode"
|
| 184 |
-
```
|
| 185 |
-
|
| 186 |
-
**UI badge:** `📝 Text Mode` (transcript-only fallback).
|
| 187 |
-
|
| 188 |
-
Path A is the **demo differentiator**. Path B keeps the app working when audio API fails, keys are missing, or latency/errors trigger fallback.
|
| 189 |
-
|
| 190 |
-
### Security
|
| 191 |
-
|
| 192 |
-
- Audio upload hits **backend only** — never send `NVIDIA_API_KEY` to the browser.
|
| 193 |
-
- Keys live in `.env` locally and **HF Space Secrets** in deployment.
|
| 194 |
-
|
| 195 |
-
---
|
| 196 |
-
|
| 197 |
-
## 7. Recommended Voice Prompt (Hesitation / Delivery)
|
| 198 |
-
|
| 199 |
-
When sending audio to Nemotron for `/api/voice-pitch`, include instructions like:
|
| 200 |
-
|
| 201 |
-
```text
|
| 202 |
-
Listen to this pitch audio carefully.
|
| 203 |
-
|
| 204 |
-
Beyond the words spoken, note:
|
| 205 |
-
- Any filler words (um, uh, like, you know, sort of)
|
| 206 |
-
- Any long pauses before answering a key claim
|
| 207 |
-
- Any statements that trail off or sound uncertain
|
| 208 |
-
- Any rushing through parts they seem unsure about
|
| 209 |
-
- Any self-corrections or restarts
|
| 210 |
-
|
| 211 |
-
After understanding the pitch, include a section called
|
| 212 |
-
DELIVERY OBSERVATIONS with specific timestamps or quotes
|
| 213 |
-
where hesitation or low confidence was audible.
|
| 214 |
-
|
| 215 |
-
Extract startup context fields if possible.
|
| 216 |
-
Then ask your first hard judge question in character.
|
| 217 |
-
Return structured JSON when requested.
|
| 218 |
-
```
|
| 219 |
-
|
| 220 |
-
Anchoring the model to **delivery observations** makes paralinguistic reporting explicit instead of hoping it infers them silently.
|
| 221 |
-
|
| 222 |
-
### Suggested structured response fields (Phase 7+)
|
| 223 |
-
|
| 224 |
-
```json
|
| 225 |
-
{
|
| 226 |
-
"transcript": "...",
|
| 227 |
-
"delivery_observations": [
|
| 228 |
-
{
|
| 229 |
-
"type": "filler_words",
|
| 230 |
-
"quote_or_timestamp": "0:12 — 'um, I think maybe...'",
|
| 231 |
-
"note": "Hedging before market size claim"
|
| 232 |
-
}
|
| 233 |
-
],
|
| 234 |
-
"detected_startup_context": { },
|
| 235 |
-
"opening_question": "..."
|
| 236 |
-
}
|
| 237 |
-
```
|
| 238 |
-
|
| 239 |
-
Exact schema should match `core/api_handlers.py` when voice mode is implemented.
|
| 240 |
-
|
| 241 |
-
---
|
| 242 |
-
|
| 243 |
-
## 8. Comparison: Nemotron Omni vs faster-whisper
|
| 244 |
-
|
| 245 |
-
| | Nemotron Omni (Path A) | faster-whisper (Path B) |
|
| 246 |
-
|---|---|---|
|
| 247 |
-
| **Input** | Raw audio | Raw audio |
|
| 248 |
-
| **Output** | Transcript + reasoning + delivery signals | Transcript text only |
|
| 249 |
-
| **Hesitation / pace** | Paralinguistic tokens in backbone | Not available |
|
| 250 |
-
| **Judge question** | Same call or follow-up text call | Requires separate text model call |
|
| 251 |
-
| **Role in PitchFight** | Primary voice judge | Transcription fallback only |
|
| 252 |
-
| **Runs** | NVIDIA API (backend) | Local on Space / dev machine |
|
| 253 |
-
|
| 254 |
-
---
|
| 255 |
-
|
| 256 |
-
## 9. Implementation Notes (Future Phases)
|
| 257 |
-
|
| 258 |
-
Planned wiring (no app logic in this doc — reference only):
|
| 259 |
-
|
| 260 |
-
| Module | Responsibility |
|
| 261 |
-
|---|---|
|
| 262 |
-
| `core/nvidia_client.py` | Send audio + prompt to Nemotron API; parse structured response |
|
| 263 |
-
| `core/model_router.py` | Choose `premium_nvidia` vs `whisper_fallback` |
|
| 264 |
-
| `core/transcription_client.py` | faster-whisper when Omni audio fails |
|
| 265 |
-
| `core/api_handlers.py` | `handle_voice_pitch()` orchestration |
|
| 266 |
-
| `frontend/script.js` | Upload audio to `/api/voice-pitch` only |
|
| 267 |
-
|
| 268 |
-
### Fallback triggers (suggested)
|
| 269 |
-
|
| 270 |
-
- NVIDIA API timeout or 5xx
|
| 271 |
-
- Missing `NVIDIA_API_KEY`
|
| 272 |
-
- Audio format unsupported by API
|
| 273 |
-
- Explicit `WHISPER_FALLBACK_ENABLED=true` override for debugging
|
| 274 |
-
|
| 275 |
-
### Demo talking points
|
| 276 |
-
|
| 277 |
-
1. **Raw audio goes to Nemotron** — not transcribe-then-reason on the premium path.
|
| 278 |
-
2. **Delivery observations** — fillers, pauses, trailing off — are first-class signals.
|
| 279 |
-
3. **Honest limits** — we score linguistic hesitation, not biometrics.
|
| 280 |
-
4. **Graceful fallback** — whisper keeps voice mode alive without breaking the demo.
|
| 281 |
-
|
| 282 |
-
---
|
| 283 |
-
|
| 284 |
-
## 10. References
|
| 285 |
-
|
| 286 |
-
- NVIDIA Nemotron 3 Nano Omni model card and integrate API docs (NVIDIA NIM / integrate.api.nvidia.com)
|
| 287 |
-
- Build Small Hackathon: ≤32B models, Gradio/HF Spaces, demo-first
|
| 288 |
-
- PitchFight model stack: [`MODELS_FINAL.md`](MODELS_FINAL.md)
|
| 289 |
-
|
| 290 |
-
---
|
| 291 |
-
|
| 292 |
-
## 11. Bottom Line
|
| 293 |
-
|
| 294 |
-
Nemotron Omni is the right primary voice model for PitchFight because it processes **raw audio through a dedicated encoder into a shared reasoning backbone**, preserving paralinguistic cues that matter for pitch coaching. Pair it with an explicit **delivery observations** prompt, honest scorecard metrics, and a **whisper fallback** that degrades visibly — not silently — when the premium audio path is unavailable.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/PHASE_WISE_PLAN.md
DELETED
|
@@ -1,478 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Phase-Wise Implementation Plan
|
| 2 |
-
|
| 3 |
-
## Full Sponsor-Model Build Plan for Voice, Text, Deal Battle, Scorecards, and Demo Polish
|
| 4 |
-
|
| 5 |
-
PitchFight AI is a voice-and-text AI sparring arena for student founders. The project prioritizes the strongest possible demo experience using small models under 32B, including NVIDIA Nemotron Omni for premium voice/multimodal judge behavior, OpenBMB MiniCPM models for sponsor-aligned modes, a custom Gradio Server frontend, persona-based pitch battles, deal negotiation battles, and rubric-based scorecards.
|
| 6 |
-
|
| 7 |
-
> **Strategy note:** This version does **not** claim the Off-the-Grid badge because the default high-quality build may use sponsor/model APIs. This is intentional. The project instead targets **Backyard AI**, **Best Demo**, **Best Agent**, **Off-Brand**, **NVIDIA Nemotron Quest**, **OpenBMB Awards**, **Sharing is Caring**, and **Field Notes**.
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## 1. Final Build Goal
|
| 12 |
-
|
| 13 |
-
The complete final app includes:
|
| 14 |
-
|
| 15 |
-
- Custom Gradio Server app
|
| 16 |
-
- Custom HTML/CSS/JS frontend
|
| 17 |
-
- Pitch Battle mode
|
| 18 |
-
- Deal Battle mode
|
| 19 |
-
- Voice Pitch mode
|
| 20 |
-
- Text Pitch mode
|
| 21 |
-
- AI opponent personas
|
| 22 |
-
- Attack-tag routing
|
| 23 |
-
- Session memory
|
| 24 |
-
- NVIDIA Nemotron Omni integration
|
| 25 |
-
- MiniCPM-o / MiniCPM fallback modes
|
| 26 |
-
- MiniCPM-V pitch deck critique mode
|
| 27 |
-
- Scorecard engine
|
| 28 |
-
- Weakest answer rewrite
|
| 29 |
-
- Improved 60-second pitch
|
| 30 |
-
- Retry weakest question
|
| 31 |
-
- HF Spaces deployment
|
| 32 |
-
- Documentation and demo notes
|
| 33 |
-
|
| 34 |
-
**Focus:** demo strength, model quality, and sponsor-model alignment — **not** Off-the-Grid.
|
| 35 |
-
|
| 36 |
-
All models stay **≤32B parameters**. The app is built on **Gradio**, hosted as a **Hugging Face Space**, uses a **custom non-default UI**, and keeps **all API keys in backend/HF Space Secrets** only (never in the frontend).
|
| 37 |
-
|
| 38 |
-
---
|
| 39 |
-
|
| 40 |
-
## 2. Master Build Order
|
| 41 |
-
|
| 42 |
-
| Phase | Name |
|
| 43 |
-
|---|---|
|
| 44 |
-
| **Phase 0** | Strategy Reset + Documentation Alignment |
|
| 45 |
-
| **Phase 1** | Project Skeleton Verification |
|
| 46 |
-
| **Phase 2** | Model Router + Secrets Setup |
|
| 47 |
-
| **Phase 3** | NVIDIA Nemotron Omni Integration |
|
| 48 |
-
| **Phase 4** | Pitch Battle Engine |
|
| 49 |
-
| **Phase 5** | Scorecard + Feedback Engine |
|
| 50 |
-
| **Phase 6** | Custom Frontend Integration |
|
| 51 |
-
| **Phase 7** | Voice Pitch Mode |
|
| 52 |
-
| **Phase 8** | Deal Battle Mode |
|
| 53 |
-
| **Phase 9** | MiniCPM / OpenBMB Modes |
|
| 54 |
-
| **Phase 10** | Pitch Deck Critique Mode |
|
| 55 |
-
| **Phase 11** | Retry + Report Enhancements |
|
| 56 |
-
| **Phase 12** | UI Polish + Demo Flow |
|
| 57 |
-
| **Phase 13** | Hugging Face Spaces Deployment |
|
| 58 |
-
| **Phase 14** | Final Testing + Submission Readiness |
|
| 59 |
-
|
| 60 |
-
---
|
| 61 |
-
|
| 62 |
-
## 3. Phase 0 — Strategy Reset + Documentation Alignment
|
| 63 |
-
|
| 64 |
-
**Goal:** Ensure all repository docs reflect the high-demo sponsor-model strategy (historical local-first/off-grid wording removed).
|
| 65 |
-
|
| 66 |
-
**Tasks:**
|
| 67 |
-
|
| 68 |
-
- Remove wording that says the default build is Off-the-Grid.
|
| 69 |
-
- Keep a note that Off-the-Grid is **not** targeted.
|
| 70 |
-
- Update model strategy to include **NVIDIA Nemotron Omni** as the primary premium model.
|
| 71 |
-
- Add **MiniCPM-o** as OpenBMB advanced mode.
|
| 72 |
-
- Add **MiniCPM5-1B** as Tiny/Fallback mode.
|
| 73 |
-
- Add **MiniCPM-V 4.6** as pitch deck critique mode.
|
| 74 |
-
- Keep **faster-whisper** as backup transcription fallback.
|
| 75 |
-
- Update `README.md`, `docs/DOCUMENTATION.md`, `docs/PROMPTS.md`, `docs/DEMO_NOTES.md`, `docs/FIELD_NOTES.md`, and `docs/MODELS_FINAL.md` accordingly.
|
| 76 |
-
|
| 77 |
-
**Success check:** All docs agree that the project is under 32B, Gradio/HF Spaces based, and high-demo sponsor-model focused.
|
| 78 |
-
|
| 79 |
-
---
|
| 80 |
-
|
| 81 |
-
## 4. Phase 1 — Project Skeleton Verification
|
| 82 |
-
|
| 83 |
-
**Goal:** Verify that the existing Gradio Server + custom frontend skeleton works.
|
| 84 |
-
|
| 85 |
-
**Tasks:**
|
| 86 |
-
|
| 87 |
-
- Check `app.py` exists.
|
| 88 |
-
- Check `frontend/index.html`, `styles.css`, `script.js` exist.
|
| 89 |
-
- Check `core/` modules exist.
|
| 90 |
-
- Check `config/` files exist.
|
| 91 |
-
- Run `python app.py`.
|
| 92 |
-
- Confirm custom UI opens.
|
| 93 |
-
- Confirm mock battle works.
|
| 94 |
-
- Confirm mock scorecard appears.
|
| 95 |
-
|
| 96 |
-
**Files:**
|
| 97 |
-
|
| 98 |
-
- `app.py`
|
| 99 |
-
- `frontend/*`
|
| 100 |
-
- `core/*`
|
| 101 |
-
- `config/*`
|
| 102 |
-
- `docs/*`
|
| 103 |
-
|
| 104 |
-
**Success check:** Custom PitchFight AI UI opens and mock demo flow runs.
|
| 105 |
-
|
| 106 |
-
**Current status:** ✅ Complete (Phase 1 skeleton in place with mock APIs).
|
| 107 |
-
|
| 108 |
-
**API layer:** Clean product endpoints live under `/api/...` (see [`BACKEND_API.md`](BACKEND_API.md)). Gradio internal routes in Swagger are framework runtime routes, not PitchFight product APIs.
|
| 109 |
-
|
| 110 |
-
---
|
| 111 |
-
|
| 112 |
-
## 5. Phase 2 — Model Router + Secrets Setup
|
| 113 |
-
|
| 114 |
-
**Goal:** Create a clean backend-only model routing layer.
|
| 115 |
-
|
| 116 |
-
**Tasks:**
|
| 117 |
-
|
| 118 |
-
- Create or update `core/model_router.py`.
|
| 119 |
-
- Create or update `core/nvidia_client.py`.
|
| 120 |
-
- Create or update `core/minicpm_client.py`.
|
| 121 |
-
- Create or update `core/vision_client.py`.
|
| 122 |
-
- Create or update `core/transcription_client.py`.
|
| 123 |
-
- Ensure frontend never calls model APIs.
|
| 124 |
-
- Add env variables to `.env.example`.
|
| 125 |
-
- Read keys only from `os.getenv` (`.env` locally, HF Space Secrets in deployment).
|
| 126 |
-
- Frontend calls `/api/*` only — never model provider APIs.
|
| 127 |
-
- Add graceful fallback if a model call fails.
|
| 128 |
-
|
| 129 |
-
**Environment variables:**
|
| 130 |
-
|
| 131 |
-
```text
|
| 132 |
-
NVIDIA_API_KEY=
|
| 133 |
-
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 134 |
-
NVIDIA_OMNI_MODEL=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning
|
| 135 |
-
|
| 136 |
-
OPENBMB_API_KEY=
|
| 137 |
-
OPENBMB_BASE_URL=
|
| 138 |
-
MINICPM_OMNI_MODEL=openbmb/MiniCPM-o-4_5
|
| 139 |
-
MINICPM_TEXT_MODEL=openbmb/MiniCPM5-1B
|
| 140 |
-
MINICPM_VISION_MODEL=openbmb/MiniCPM-V-4.6
|
| 141 |
-
|
| 142 |
-
HF_TOKEN=
|
| 143 |
-
WHISPER_FALLBACK_ENABLED=true
|
| 144 |
-
MAX_ROUNDS=6
|
| 145 |
-
APP_ENV=development
|
| 146 |
-
```
|
| 147 |
-
|
| 148 |
-
**Success check:** Backend can select a model path:
|
| 149 |
-
|
| 150 |
-
- `premium_nvidia`
|
| 151 |
-
- `openbmb_omni`
|
| 152 |
-
- `tiny_minicpm`
|
| 153 |
-
- `vision_deck`
|
| 154 |
-
- `whisper_fallback`
|
| 155 |
-
|
| 156 |
-
**Current status:** ✅ Complete (2026-06-08). `core/nvidia_client.py` and `core/model_router.py` created. NVIDIA connectivity tested — live response confirmed. Stub clients created for MiniCPM, vision, and transcription. `GET /api/model-health` added.
|
| 157 |
-
|
| 158 |
-
**Key discovery:** `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` is a reasoning model. It uses tokens for an internal chain-of-thought before writing `message.content`. Token defaults: opponent=500, scorecard=2000, rewrite=800. A `reasoning_content` fallback is in place for edge cases.
|
| 159 |
-
|
| 160 |
-
---
|
| 161 |
-
|
| 162 |
-
## 6. Phase 3 — NVIDIA Nemotron Omni Integration
|
| 163 |
-
|
| 164 |
-
**Goal:** Make NVIDIA Nemotron Omni the primary premium reasoning and voice/multimodal judge model.
|
| 165 |
-
|
| 166 |
-
**Tasks:**
|
| 167 |
-
|
| 168 |
-
- Implement secure backend call using `NVIDIA_API_KEY`.
|
| 169 |
-
- Add function `generate_nemotron_response(messages, mode)`.
|
| 170 |
-
- Add timeout handling.
|
| 171 |
-
- Add retry handling.
|
| 172 |
-
- Add fallback to MiniCPM mode if NVIDIA fails.
|
| 173 |
-
- Test text-only judge prompt first.
|
| 174 |
-
- Test scoring prompt second.
|
| 175 |
-
- Test voice/multimodal input after text works.
|
| 176 |
-
|
| 177 |
-
**Use cases:**
|
| 178 |
-
|
| 179 |
-
- AI opponent response
|
| 180 |
-
- Voice pitch critique
|
| 181 |
-
- Deal battle counterpart
|
| 182 |
-
- Scorecard generation
|
| 183 |
-
- Weakest answer rewrite
|
| 184 |
-
- Optional pitch deck + spoken explanation critique
|
| 185 |
-
|
| 186 |
-
**Success check:** Backend receives a startup context and Nemotron returns a sharp judge question.
|
| 187 |
-
|
| 188 |
-
---
|
| 189 |
-
|
| 190 |
-
## 7. Phase 4 — Pitch Battle Engine
|
| 191 |
-
|
| 192 |
-
**Goal:** Make Pitch Battle real.
|
| 193 |
-
|
| 194 |
-
**Tasks:**
|
| 195 |
-
|
| 196 |
-
- Connect `/start_session` to `model_router`.
|
| 197 |
-
- Connect `/chat_round` to `model_router`.
|
| 198 |
-
- Use `persona_builder.py`.
|
| 199 |
-
- Use `attack_tags.py`.
|
| 200 |
-
- Store history in `session_manager.py`.
|
| 201 |
-
- Rotate attack tags.
|
| 202 |
-
- Return `attack_tag`, `pressure_level`, `round`, `ai_message`.
|
| 203 |
-
- Support 3 personas:
|
| 204 |
-
- Skeptical VC
|
| 205 |
-
- Technical Judge
|
| 206 |
-
- Hackathon Judge
|
| 207 |
-
|
| 208 |
-
**Success check:** User enters EventRadar AI, selects Hackathon Judge, and receives a sharp AI challenge.
|
| 209 |
-
|
| 210 |
-
---
|
| 211 |
-
|
| 212 |
-
## 8. Phase 5 — Scorecard + Feedback Engine
|
| 213 |
-
|
| 214 |
-
**Goal:** Generate real scorecards.
|
| 215 |
-
|
| 216 |
-
**Tasks:**
|
| 217 |
-
|
| 218 |
-
- Build scoring prompt from conversation history.
|
| 219 |
-
- Use NVIDIA Nemotron as default scorer.
|
| 220 |
-
- Use MiniCPM fallback if needed.
|
| 221 |
-
- Parse scorecard JSON safely.
|
| 222 |
-
- Implement JSON fallback pipeline.
|
| 223 |
-
- Generate:
|
| 224 |
-
- overall score
|
| 225 |
-
- 6 pitch dimensions
|
| 226 |
-
- best answer
|
| 227 |
-
- weakest answer
|
| 228 |
-
- improved answer
|
| 229 |
-
- improved pitch
|
| 230 |
-
- top 3 prep questions
|
| 231 |
-
|
| 232 |
-
**Files:**
|
| 233 |
-
|
| 234 |
-
- `core/scoring_engine.py`
|
| 235 |
-
- `core/feedback_generator.py`
|
| 236 |
-
- `core/json_utils.py`
|
| 237 |
-
- `app.py`
|
| 238 |
-
- `frontend/script.js`
|
| 239 |
-
|
| 240 |
-
**Success check:** End Battle produces structured scorecard with user quotes and useful rewrites.
|
| 241 |
-
|
| 242 |
-
---
|
| 243 |
-
|
| 244 |
-
## 9. Phase 6 — Custom Frontend Integration
|
| 245 |
-
|
| 246 |
-
**Goal:** Connect UI fully to backend APIs.
|
| 247 |
-
|
| 248 |
-
**Tasks:**
|
| 249 |
-
|
| 250 |
-
- Connect Gradio JS Client.
|
| 251 |
-
- Load demo startup.
|
| 252 |
-
- Start session.
|
| 253 |
-
- Render AI question.
|
| 254 |
-
- Render user messages.
|
| 255 |
-
- Render AI follow-ups.
|
| 256 |
-
- Render attack tag.
|
| 257 |
-
- Render pressure meter.
|
| 258 |
-
- Render scorecard.
|
| 259 |
-
- Show model mode badge:
|
| 260 |
-
- Premium Nemotron
|
| 261 |
-
- OpenBMB Omni
|
| 262 |
-
- Tiny MiniCPM
|
| 263 |
-
- Handle errors and loading states.
|
| 264 |
-
|
| 265 |
-
**Success check:** User can complete full Pitch Battle from UI.
|
| 266 |
-
|
| 267 |
-
---
|
| 268 |
-
|
| 269 |
-
## 10. Phase 7 — Voice Pitch Mode
|
| 270 |
-
|
| 271 |
-
**Goal:** Make voice mode a major demo feature.
|
| 272 |
-
|
| 273 |
-
**Primary path:**
|
| 274 |
-
|
| 275 |
-
```text
|
| 276 |
-
Browser records audio
|
| 277 |
-
→ backend sends audio/transcript to NVIDIA Nemotron Omni
|
| 278 |
-
→ AI extracts pitch understanding
|
| 279 |
-
→ AI asks first hard question
|
| 280 |
-
→ user continues by voice or text
|
| 281 |
-
```
|
| 282 |
-
|
| 283 |
-
**Fallback path:**
|
| 284 |
-
|
| 285 |
-
```text
|
| 286 |
-
Browser records audio
|
| 287 |
-
→ faster-whisper transcribes locally
|
| 288 |
-
→ transcript goes to selected text model
|
| 289 |
-
→ battle starts
|
| 290 |
-
```
|
| 291 |
-
|
| 292 |
-
**Tasks:**
|
| 293 |
-
|
| 294 |
-
- Add browser audio recorder.
|
| 295 |
-
- Add `/voice_pitch` endpoint.
|
| 296 |
-
- Add audio upload handling.
|
| 297 |
-
- Add transcript/interpretation panel.
|
| 298 |
-
- Add user editable transcript.
|
| 299 |
-
- Add voice-specific metrics:
|
| 300 |
-
- Structure
|
| 301 |
-
- Conciseness
|
| 302 |
-
- Confidence Signals
|
| 303 |
-
- Directness
|
| 304 |
-
- Continue battle from voice input.
|
| 305 |
-
|
| 306 |
-
**Success check:** User records a pitch and receives a hard first question.
|
| 307 |
-
|
| 308 |
-
---
|
| 309 |
-
|
| 310 |
-
## 11. Phase 8 — Deal Battle Mode
|
| 311 |
-
|
| 312 |
-
**Goal:** Add negotiation practice.
|
| 313 |
-
|
| 314 |
-
**Scenarios:**
|
| 315 |
-
|
| 316 |
-
- Startup Sponsorship Ask
|
| 317 |
-
- Internship Salary Negotiation
|
| 318 |
-
- Freelance Client Pricing
|
| 319 |
-
- Equity Negotiation
|
| 320 |
-
|
| 321 |
-
**Personas:**
|
| 322 |
-
|
| 323 |
-
- Tough Sponsor
|
| 324 |
-
- Strict HR Recruiter
|
| 325 |
-
- Budget-Conscious Client
|
| 326 |
-
- Hard-Negotiating Investor
|
| 327 |
-
|
| 328 |
-
**Tasks:**
|
| 329 |
-
|
| 330 |
-
- Add Deal Battle mode to UI.
|
| 331 |
-
- Add negotiation context form.
|
| 332 |
-
- Add deal personas.
|
| 333 |
-
- Add deal attack tags.
|
| 334 |
-
- Add deal rubric.
|
| 335 |
-
- Route deal battle to `model_router`.
|
| 336 |
-
- Generate negotiation-specific scorecard.
|
| 337 |
-
|
| 338 |
-
**Success check:** User can negotiate and receive a deal-specific scorecard.
|
| 339 |
-
|
| 340 |
-
---
|
| 341 |
-
|
| 342 |
-
## 12. Phase 9 — MiniCPM / OpenBMB Modes
|
| 343 |
-
|
| 344 |
-
**Goal:** Add OpenBMB sponsor-aligned models and fallback modes.
|
| 345 |
-
|
| 346 |
-
**Models:**
|
| 347 |
-
|
| 348 |
-
- **MiniCPM-o 4.5** — OpenBMB Omni Mode
|
| 349 |
-
- **MiniCPM5-1B** — Tiny Mode / fallback
|
| 350 |
-
- **MiniCPM-V 4.6** — vision mode
|
| 351 |
-
|
| 352 |
-
**Tasks:**
|
| 353 |
-
|
| 354 |
-
- Add model selector or backend mode switch.
|
| 355 |
-
- Add OpenBMB Omni mode.
|
| 356 |
-
- Add Tiny MiniCPM mode.
|
| 357 |
-
- Add fallback if NVIDIA errors.
|
| 358 |
-
- Add docs explaining model use cases.
|
| 359 |
-
|
| 360 |
-
**Success check:** App can run at least one OpenBMB mode and one fallback mode.
|
| 361 |
-
|
| 362 |
-
---
|
| 363 |
-
|
| 364 |
-
## 13. Phase 10 — Pitch Deck Critique Mode
|
| 365 |
-
|
| 366 |
-
**Goal:** Add image/slide critique.
|
| 367 |
-
|
| 368 |
-
**Primary model:** MiniCPM-V 4.6 or NVIDIA Omni.
|
| 369 |
-
|
| 370 |
-
**Tasks:**
|
| 371 |
-
|
| 372 |
-
- Add image upload.
|
| 373 |
-
- Add `/deck_critique` endpoint.
|
| 374 |
-
- Extract slide claims.
|
| 375 |
-
- Critique clarity, problem, solution, market, ask.
|
| 376 |
-
- Generate 3 judge questions based on the slide.
|
| 377 |
-
|
| 378 |
-
**Success check:** User uploads a pitch slide screenshot and receives useful critique.
|
| 379 |
-
|
| 380 |
-
---
|
| 381 |
-
|
| 382 |
-
## 14. Phase 11 — Retry + Report Enhancements
|
| 383 |
-
|
| 384 |
-
**Goal:** Make it feel product-ready.
|
| 385 |
-
|
| 386 |
-
**Tasks:**
|
| 387 |
-
|
| 388 |
-
- Add Retry Weakest Question.
|
| 389 |
-
- Store weakest AI question.
|
| 390 |
-
- Compare original vs retried answer.
|
| 391 |
-
- Generate downloadable markdown report.
|
| 392 |
-
- Add full session summary.
|
| 393 |
-
- Add “Start New Battle”.
|
| 394 |
-
|
| 395 |
-
**Success check:** User can retry the weakest objection and get improvement guidance.
|
| 396 |
-
|
| 397 |
-
---
|
| 398 |
-
|
| 399 |
-
## 15. Phase 12 — UI Polish + Demo Flow
|
| 400 |
-
|
| 401 |
-
**Goal:** Make it visually memorable.
|
| 402 |
-
|
| 403 |
-
**Tasks:**
|
| 404 |
-
|
| 405 |
-
- Improve landing hero.
|
| 406 |
-
- Add model mode badges.
|
| 407 |
-
- Add premium voice mode card.
|
| 408 |
-
- Add deck critique card.
|
| 409 |
-
- Add battle arena split layout.
|
| 410 |
-
- Add score animations.
|
| 411 |
-
- Add voice recording animation.
|
| 412 |
-
- Add mobile responsiveness.
|
| 413 |
-
- Add error banners.
|
| 414 |
-
- Add loading states.
|
| 415 |
-
|
| 416 |
-
**Design:** Dark battle arena, red pressure accents, gold score accents, glass cards, custom typography.
|
| 417 |
-
|
| 418 |
-
**Success check:** The app looks like a custom product, not default Gradio.
|
| 419 |
-
|
| 420 |
-
---
|
| 421 |
-
|
| 422 |
-
## 16. Phase 13 — Hugging Face Spaces Deployment
|
| 423 |
-
|
| 424 |
-
**Goal:** Deploy publicly.
|
| 425 |
-
|
| 426 |
-
**Tasks:**
|
| 427 |
-
|
| 428 |
-
- Add README metadata.
|
| 429 |
-
- Add required secrets in HF Space Settings.
|
| 430 |
-
- Confirm app builds.
|
| 431 |
-
- Confirm no keys are exposed.
|
| 432 |
-
- Test all modes publicly.
|
| 433 |
-
- Add screenshots to README.
|
| 434 |
-
- Add final Space link.
|
| 435 |
-
|
| 436 |
-
**Success check:** Public HF Space completes:
|
| 437 |
-
|
| 438 |
-
- Text Pitch Battle
|
| 439 |
-
- Voice Pitch Battle
|
| 440 |
-
- Deal Battle
|
| 441 |
-
- Scorecard
|
| 442 |
-
- Deck critique (if built)
|
| 443 |
-
|
| 444 |
-
---
|
| 445 |
-
|
| 446 |
-
## 17. Phase 14 — Final Testing + Submission Readiness
|
| 447 |
-
|
| 448 |
-
**Tasks:**
|
| 449 |
-
|
| 450 |
-
- Test every endpoint.
|
| 451 |
-
- Test every UI button.
|
| 452 |
-
- Test failure fallback.
|
| 453 |
-
- Test missing API keys.
|
| 454 |
-
- Test slow responses.
|
| 455 |
-
- Test mobile layout.
|
| 456 |
-
- Update README.
|
| 457 |
-
- Update docs.
|
| 458 |
-
- Prepare demo flow.
|
| 459 |
-
- Prepare field notes.
|
| 460 |
-
- Prepare social post separately.
|
| 461 |
-
|
| 462 |
-
**Badges/prizes to mention:**
|
| 463 |
-
|
| 464 |
-
- Backyard AI
|
| 465 |
-
- Best Demo
|
| 466 |
-
- Best Agent
|
| 467 |
-
- Off-Brand
|
| 468 |
-
- NVIDIA Nemotron Quest
|
| 469 |
-
- OpenBMB Awards
|
| 470 |
-
- Sharing is Caring
|
| 471 |
-
- Field Notes
|
| 472 |
-
- Tiny Titan (if Tiny Mode works)
|
| 473 |
-
|
| 474 |
-
---
|
| 475 |
-
|
| 476 |
-
## Final One-Line Pitch
|
| 477 |
-
|
| 478 |
-
**PitchFight AI is a voice-and-text AI sparring arena where student founders practice tough startup pitches, get grilled by realistic AI judges under 32B parameters, and receive a scorecard that shows exactly how to answer better.**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/PROMPTS.md
DELETED
|
@@ -1,155 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Prompt Templates
|
| 2 |
-
|
| 3 |
-
## Model Routing (Backend Only)
|
| 4 |
-
|
| 5 |
-
All prompts are sent through `core/model_router.py`. Default scorer and opponent: **NVIDIA Nemotron Omni**. Fallback: **MiniCPM5-1B**. Voice primary path: Nemotron Omni audio; fallback: **faster-whisper** → text model.
|
| 6 |
-
|
| 7 |
-
> API keys never appear in prompts or frontend code. Keys are read from `os.getenv` on the backend only.
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## Opponent System Prompt Template
|
| 12 |
-
|
| 13 |
-
Used by Nemotron Omni (primary) and MiniCPM fallback.
|
| 14 |
-
|
| 15 |
-
```text
|
| 16 |
-
You are {persona_label}, a tough pitch opponent in PitchFight AI.
|
| 17 |
-
Difficulty: {difficulty}
|
| 18 |
-
Current attack tag: {attack_tag}
|
| 19 |
-
Round: {round_number} of {max_rounds}
|
| 20 |
-
|
| 21 |
-
Startup: {startup_name}
|
| 22 |
-
Problem: {problem}
|
| 23 |
-
Solution: {solution}
|
| 24 |
-
Why AI: {why_ai}
|
| 25 |
-
Target users: {target_users}
|
| 26 |
-
Competitors: {competitors}
|
| 27 |
-
Traction: {traction}
|
| 28 |
-
|
| 29 |
-
Conversation so far:
|
| 30 |
-
{history}
|
| 31 |
-
|
| 32 |
-
{persona_focus}
|
| 33 |
-
|
| 34 |
-
Behavior rules:
|
| 35 |
-
- Ask one sharp question at a time.
|
| 36 |
-
- Keep responses under 4 sentences.
|
| 37 |
-
- Reference the founder's previous answer when pushing back.
|
| 38 |
-
- Do not give advice during the battle.
|
| 39 |
-
- Do not compliment the founder.
|
| 40 |
-
- Attack vague, generic, or unsubstantiated claims.
|
| 41 |
-
- Raise difficulty after strong answers.
|
| 42 |
-
- Stay in character at all times.
|
| 43 |
-
- Be firm but not abusive.
|
| 44 |
-
- Frame your question around the current attack tag: {attack_tag}.
|
| 45 |
-
|
| 46 |
-
Return only your next question as plain text (no JSON).
|
| 47 |
-
```
|
| 48 |
-
|
| 49 |
-
---
|
| 50 |
-
|
| 51 |
-
## Scoring Prompt Template
|
| 52 |
-
|
| 53 |
-
Default model: **NVIDIA Nemotron Omni**. Fallback: MiniCPM5-1B + `json_utils.safe_json_parse`.
|
| 54 |
-
|
| 55 |
-
```text
|
| 56 |
-
You are a pitch battle evaluator. Score the founder's performance using the rubric below.
|
| 57 |
-
Return valid JSON only — no markdown fences, no commentary.
|
| 58 |
-
|
| 59 |
-
Rubric weights:
|
| 60 |
-
- clarity: 15
|
| 61 |
-
- problem_understanding: 20
|
| 62 |
-
- market_awareness: 15
|
| 63 |
-
- differentiation: 20
|
| 64 |
-
- business_model: 15
|
| 65 |
-
- objection_handling: 15
|
| 66 |
-
|
| 67 |
-
Startup context:
|
| 68 |
-
{startup_json}
|
| 69 |
-
|
| 70 |
-
Conversation:
|
| 71 |
-
{history_json}
|
| 72 |
-
|
| 73 |
-
Return exactly this JSON shape:
|
| 74 |
-
{
|
| 75 |
-
"overall": <0-100>,
|
| 76 |
-
"scores": {
|
| 77 |
-
"clarity": {"score": <0-100>, "reason": "...", "quote": "..."},
|
| 78 |
-
"problem_understanding": {"score": <0-100>, "reason": "...", "quote": "..."},
|
| 79 |
-
"market_awareness": {"score": <0-100>, "reason": "...", "quote": "..."},
|
| 80 |
-
"differentiation": {"score": <0-100>, "reason": "...", "quote": "..."},
|
| 81 |
-
"business_model": {"score": <0-100>, "reason": "...", "quote": "..."},
|
| 82 |
-
"objection_handling": {"score": <0-100>, "reason": "...", "quote": "..."}
|
| 83 |
-
},
|
| 84 |
-
"best_answer": "...",
|
| 85 |
-
"weakest_answer": "...",
|
| 86 |
-
"improved_answer": "...",
|
| 87 |
-
"improved_pitch": "...",
|
| 88 |
-
"top_3_questions": ["...", "...", "..."]
|
| 89 |
-
}
|
| 90 |
-
```
|
| 91 |
-
|
| 92 |
-
---
|
| 93 |
-
|
| 94 |
-
## Voice Pitch Extraction Prompt (Nemotron Omni)
|
| 95 |
-
|
| 96 |
-
```text
|
| 97 |
-
You heard a student founder's spoken pitch. Extract structured startup context and identify the weakest claim.
|
| 98 |
-
|
| 99 |
-
Return JSON:
|
| 100 |
-
{
|
| 101 |
-
"name": "...",
|
| 102 |
-
"problem": "...",
|
| 103 |
-
"target_users": "...",
|
| 104 |
-
"solution": "...",
|
| 105 |
-
"why_ai": "...",
|
| 106 |
-
"competitors": "...",
|
| 107 |
-
"traction": "...",
|
| 108 |
-
"ask": "...",
|
| 109 |
-
"voice_metrics": {
|
| 110 |
-
"structure": <0-100>,
|
| 111 |
-
"conciseness": <0-100>,
|
| 112 |
-
"confidence_signals": <0-100>,
|
| 113 |
-
"directness": <0-100>
|
| 114 |
-
},
|
| 115 |
-
"first_hard_question": "..."
|
| 116 |
-
}
|
| 117 |
-
```
|
| 118 |
-
|
| 119 |
-
---
|
| 120 |
-
|
| 121 |
-
## Pitch Deck Critique Prompt (MiniCPM-V / Nemotron Vision)
|
| 122 |
-
|
| 123 |
-
```text
|
| 124 |
-
You are a hackathon judge reviewing one pitch slide image.
|
| 125 |
-
|
| 126 |
-
Critique: clarity, problem, solution, market, ask.
|
| 127 |
-
Return JSON:
|
| 128 |
-
{
|
| 129 |
-
"slide_summary": "...",
|
| 130 |
-
"strengths": ["..."],
|
| 131 |
-
"weaknesses": ["..."],
|
| 132 |
-
"judge_questions": ["...", "...", "..."]
|
| 133 |
-
}
|
| 134 |
-
```
|
| 135 |
-
|
| 136 |
-
---
|
| 137 |
-
|
| 138 |
-
## Persona Behavior Rules
|
| 139 |
-
|
| 140 |
-
| Persona | Focus |
|
| 141 |
-
|---|---|
|
| 142 |
-
| Skeptical VC | Market size, moat, retention, revenue, defensibility |
|
| 143 |
-
| Technical Judge | AI justification, architecture, scalability, data quality |
|
| 144 |
-
| Hackathon Judge | Novelty, demo clarity, MVP strength, backyard fit |
|
| 145 |
-
|
| 146 |
-
## Attack Tag Lists
|
| 147 |
-
|
| 148 |
-
### Skeptical VC
|
| 149 |
-
Market Size, Moat, Retention, Revenue Logic, First 100 Users, Why Now, Competition, Defensibility
|
| 150 |
-
|
| 151 |
-
### Technical Judge
|
| 152 |
-
AI Justification, Architecture, Scalability, Latency, Data Quality, Failure Mode, Simpler Alternative, Technical Feasibility
|
| 153 |
-
|
| 154 |
-
### Hackathon Judge
|
| 155 |
-
Novelty, Demo Clarity, MVP Strength, User Pain, AI Load-Bearing, Backyard Fit, Practical Impact, Judging Memorability
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/TASK_TRACKER.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
# PitchFight AI — Task Tracker
|
| 2 |
-
|
| 3 |
-
## Current Phase
|
| 4 |
-
|
| 5 |
-
Phase 2 complete: Model Router + Secrets Setup (2026-06-08)
|
| 6 |
-
|
| 7 |
-
## Next Phase
|
| 8 |
-
|
| 9 |
-
Phase 3: Wire model_router into battle flow (handle_start_session + handle_chat_round)
|
| 10 |
-
|
| 11 |
-
---
|
| 12 |
-
|
| 13 |
-
## Phase Status Table
|
| 14 |
-
|
| 15 |
-
| Phase | Name | Status | Notes |
|
| 16 |
-
|---|---|---|---|
|
| 17 |
-
| 0 | Strategy Reset + Documentation Alignment | Complete | Off-the-Grid removed; sponsor-model strategy locked |
|
| 18 |
-
| 1 | Project Skeleton Verification | Complete | Mock app runs; custom frontend live; all /api/* endpoints return responses |
|
| 19 |
-
| 2 | Model Router + Secrets Setup | **Complete** (2026-06-08) | nvidia_client, model_router, stub clients created; NVIDIA tested live |
|
| 20 |
-
| 3 | NVIDIA Nemotron Omni Integration | Pending | Depends on Phase 2 |
|
| 21 |
-
| 4 | Pitch Battle Engine | Pending | Replace mock followups with real Nemotron responses |
|
| 22 |
-
| 5 | Scorecard + Feedback Engine | Pending | Real scoring via Nemotron; json_utils already ready |
|
| 23 |
-
| 6 | Custom Frontend Integration | Pending | Model mode badge; error/loading polish |
|
| 24 |
-
| 7 | Voice Pitch Mode | Pending | Browser audio → Nemotron Omni; fallback to whisper |
|
| 25 |
-
| 8 | Deal Battle Mode | Pending | Negotiation scenarios + deal personas |
|
| 26 |
-
| 9 | MiniCPM / OpenBMB Modes | Pending | MiniCPM-o, MiniCPM5-1B, fallback routing |
|
| 27 |
-
| 10 | Pitch Deck Critique Mode | Pending | MiniCPM-V 4.6 image upload + slide critique |
|
| 28 |
-
| 11 | Retry + Report Enhancements | Pending | Retry weakest question; downloadable markdown report |
|
| 29 |
-
| 12 | UI Polish + Demo Flow | Pending | Animations, model badges, mobile, dark arena look |
|
| 30 |
-
| 13 | Hugging Face Spaces Deployment | Pending | HF Space Secrets, public test, screenshots |
|
| 31 |
-
| 14 | Final Testing + Submission Readiness | Pending | All endpoints, all modes, all failure paths |
|
| 32 |
-
|
| 33 |
-
---
|
| 34 |
-
|
| 35 |
-
## Immediate Next Tasks (Phase 3)
|
| 36 |
-
|
| 37 |
-
1. In `core/api_handlers.py`: import `model_router` and `persona_builder`
|
| 38 |
-
2. In `handle_start_session`: build system prompt via `persona_builder.build_persona_prompt()`, call `model_router.generate_opponent_response()`, use mock opening as fallback if `ok=False`
|
| 39 |
-
3. In `handle_chat_round`: build message list from session history, call `model_router.generate_opponent_response()`, use mock followup as fallback if `ok=False`
|
| 40 |
-
4. Run full battle flow end-to-end: load sample → start session → 2 chat rounds → end battle
|
| 41 |
-
5. Confirm Nemotron response is sharp, persona-locked, and references attack tag
|
| 42 |
-
6. Update docs after confirming end-to-end works
|
| 43 |
-
|
| 44 |
-
**Do not touch scoring, voice, deal battle, or deck critique in Phase 3.**
|
| 45 |
-
|
| 46 |
-
---
|
| 47 |
-
|
| 48 |
-
## Phase 2 Completed Tasks
|
| 49 |
-
|
| 50 |
-
- [x] `.env.example` verified — all variables present and correct
|
| 51 |
-
- [x] `core/nvidia_client.py` created
|
| 52 |
-
- [x] Reads keys from `os.getenv` only — no hardcoded values
|
| 53 |
-
- [x] `generate_nemotron_response(messages, mode, temperature, max_tokens, timeout)` implemented
|
| 54 |
-
- [x] Timeout handling (30s default)
|
| 55 |
-
- [x] Returns string on success; raises `RuntimeError` with clean message on failure
|
| 56 |
-
- [x] `reasoning_content` fallback for reasoning model edge cases
|
| 57 |
-
- [x] No API key or URL hardcoded anywhere
|
| 58 |
-
- [x] `core/model_router.py` created
|
| 59 |
-
- [x] Routes `premium_nvidia` → nvidia_client (live)
|
| 60 |
-
- [x] Stub/placeholder routes for `openbmb_omni`, `tiny_minicpm`, `vision_deck`, `whisper_fallback`
|
| 61 |
-
- [x] Graceful fallback if NVIDIA fails (`ok=False`, fallback message returned, no crash)
|
| 62 |
-
- [x] `get_model_health()` — all providers, no key exposure
|
| 63 |
-
- [x] `core/minicpm_client.py` stub created (health_check only)
|
| 64 |
-
- [x] `core/vision_client.py` stub created (health_check only)
|
| 65 |
-
- [x] `core/transcription_client.py` stub created (health_check only)
|
| 66 |
-
- [x] `scripts/test_nvidia_client.py` — isolated test, exits 0 on success / 1 on failure
|
| 67 |
-
- [x] Isolated NVIDIA test: **PASSED** — live Nemotron response confirmed
|
| 68 |
-
- [x] `GET /api/model-health` added to `app.py`
|
| 69 |
-
- [x] `requirements.txt` updated: `openai>=1.0.0`, `httpx`, `requests` added
|
| 70 |
-
- [x] Mock followup lists remain in `api_handlers.py` — not removed
|
| 71 |
-
- [x] `docs/PHASE_WISE_PLAN.md` Phase 2 marked Complete
|
| 72 |
-
- [x] `docs/FIELD_NOTES.md` updated with Phase 2 build log
|
| 73 |
-
- [x] `docs/BACKEND_API.md` `/api/model-health` added
|
| 74 |
-
- [x] `docs/MODELS_FINAL.md` model-to-file mapping updated with Phase 2 status
|
| 75 |
-
|
| 76 |
-
---
|
| 77 |
-
|
| 78 |
-
## Phase 1 Completed Tasks (for reference)
|
| 79 |
-
|
| 80 |
-
- [x] `app.py` — Gradio Server with `/api/*` REST routes and `@app.api` wrappers
|
| 81 |
-
- [x] `frontend/index.html`, `styles.css`, `script.js` — custom battle arena UI
|
| 82 |
-
- [x] `core/session_manager.py` — in-memory session store
|
| 83 |
-
- [x] `core/persona_builder.py` — system prompt builder per persona
|
| 84 |
-
- [x] `core/attack_tags.py` — tag taxonomy + round-based selector
|
| 85 |
-
- [x] `core/scoring_engine.py` — mock scorecard with real session quotes
|
| 86 |
-
- [x] `core/feedback_generator.py` — mock rewrite templates
|
| 87 |
-
- [x] `core/json_utils.py` — JSON extraction, safe parse, fallback scorecard
|
| 88 |
-
- [x] `core/samples.py` — EventRadar AI sample startup
|
| 89 |
-
- [x] `core/local_text_model.py` — Phase 1 stub
|
| 90 |
-
- [x] `core/voice_transcriber.py` — Phase 1 stub
|
| 91 |
-
- [x] `config/personas.json`, `attack_tags.json`, `pitch_rubric.json`, `sample_startups.json`
|
| 92 |
-
- [x] `docs/BACKEND_API.md`, `DOCUMENTATION.md`, `FIELD_NOTES.md`, `DEMO_NOTES.md`, `MODELS_FINAL.md`, `PHASE_WISE_PLAN.md`, `PROMPTS.md`
|
| 93 |
-
- [x] `.env.example` — all required variables present
|
| 94 |
-
|
| 95 |
-
---
|
| 96 |
-
|
| 97 |
-
## Risks Before Phase 2
|
| 98 |
-
|
| 99 |
-
| Risk | Mitigation |
|
| 100 |
-
|---|---|
|
| 101 |
-
| NVIDIA API key not yet provisioned | Get key from NVIDIA developer portal; add to `.env` before Phase 2 testing |
|
| 102 |
-
| Nemotron endpoint may need special headers or model format | Test isolated before wiring into session flow |
|
| 103 |
-
| `openai` Python SDK compatibility with NVIDIA base URL | NVIDIA uses OpenAI-compatible API; use `openai.OpenAI(base_url=..., api_key=...)` |
|
| 104 |
-
| Session manager is in-memory; process restart loses sessions | Acceptable for hackathon; note as known limitation |
|
| 105 |
-
| Mock fallback strings (MOCK_FOLLOWUPS) must stay in api_handlers.py | Do not delete until real model routing is confirmed stable |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_claim_based_scoring.py
DELETED
|
@@ -1,336 +0,0 @@
|
|
| 1 |
-
"""Phase 5D: Local unit tests for claim extractor, local scoring, and session-aware fallback.
|
| 2 |
-
|
| 3 |
-
No server required. Tests run purely locally.
|
| 4 |
-
|
| 5 |
-
Usage:
|
| 6 |
-
python scripts/test_claim_based_scoring.py
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import sys
|
| 12 |
-
import os
|
| 13 |
-
|
| 14 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 15 |
-
|
| 16 |
-
from core.claim_extractor import extract_concrete_signals
|
| 17 |
-
from core.scoring_engine import (
|
| 18 |
-
build_session_aware_fallback_scorecard,
|
| 19 |
-
_compute_local_scores,
|
| 20 |
-
_score_label,
|
| 21 |
-
)
|
| 22 |
-
from core.json_utils import _score_label as json_score_label
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# ---------------------------------------------------------------------------
|
| 26 |
-
# Helpers
|
| 27 |
-
# ---------------------------------------------------------------------------
|
| 28 |
-
|
| 29 |
-
def check(label: str, condition: bool, detail: str = "") -> None:
|
| 30 |
-
marker = "PASS" if condition else "FAIL"
|
| 31 |
-
suffix = f" — {detail}" if detail else ""
|
| 32 |
-
print(f" {marker} {label}{suffix}")
|
| 33 |
-
if not condition:
|
| 34 |
-
sys.exit(1)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def make_session(answers: list[str], startup: dict | None = None) -> dict:
|
| 38 |
-
startup = startup or {
|
| 39 |
-
"name": "TestStartup",
|
| 40 |
-
"problem": "Test problem",
|
| 41 |
-
"solution": "Test solution",
|
| 42 |
-
"why_ai": "AI helps",
|
| 43 |
-
"stage": "Prototype",
|
| 44 |
-
"team": "2 founders",
|
| 45 |
-
"traction": "10 beta users",
|
| 46 |
-
}
|
| 47 |
-
history = []
|
| 48 |
-
for i, ans in enumerate(answers):
|
| 49 |
-
if i % 2 == 0:
|
| 50 |
-
history.append({"role": "assistant", "content": f"Judge question {i}", "attack_tag": "Market Size"})
|
| 51 |
-
history.append({"role": "user", "content": ans})
|
| 52 |
-
return {"startup": startup, "persona": "hackathon_judge", "difficulty": "high", "history": history}
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# ---------------------------------------------------------------------------
|
| 56 |
-
# Extraction tests (existing)
|
| 57 |
-
# ---------------------------------------------------------------------------
|
| 58 |
-
|
| 59 |
-
def test_strong_evidence_answer() -> None:
|
| 60 |
-
print("\nTest 1: Strong evidence — numbers, validation, campus")
|
| 61 |
-
session = make_session(["80 users, 60% interview rate, 3 colleges have onboarded already."])
|
| 62 |
-
sigs = extract_concrete_signals(session)
|
| 63 |
-
check("numbers extracted", len(sigs["numbers"]) > 0, f"got {sigs['numbers']}")
|
| 64 |
-
check("percentages extracted", len(sigs["percentages"]) > 0, f"got {sigs['percentages']}")
|
| 65 |
-
check("college_mentions extracted", len(sigs["college_mentions"]) > 0, f"got {sigs['college_mentions']}")
|
| 66 |
-
check("signal_count > 0", sigs["signal_count"] > 0, f"got {sigs['signal_count']}")
|
| 67 |
-
check("non_answers empty", len(sigs["non_answers"]) == 0)
|
| 68 |
-
check("best_user_quotes non-empty", len(sigs["best_user_quotes"]) > 0)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def test_vague_answer() -> None:
|
| 72 |
-
print("\nTest 2: Vague answer — big market, useful")
|
| 73 |
-
session = make_session(["It is useful and big market will love it."])
|
| 74 |
-
sigs = extract_concrete_signals(session)
|
| 75 |
-
check("vague_claims detected", len(sigs["vague_claims"]) > 0, f"got {sigs['vague_claims']}")
|
| 76 |
-
check("no numbers", len(sigs["numbers"]) == 0, f"got {sigs['numbers']}")
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def test_non_answer() -> None:
|
| 80 |
-
print("\nTest 3: Non-answer — 'I don't know'")
|
| 81 |
-
session = make_session(["I don't know"])
|
| 82 |
-
sigs = extract_concrete_signals(session)
|
| 83 |
-
check("non_answers detected", len(sigs["non_answers"]) > 0, f"got {sigs['non_answers']}")
|
| 84 |
-
check("best_user_quotes empty", len(sigs["best_user_quotes"]) == 0)
|
| 85 |
-
check("signal_count is 0", sigs["signal_count"] == 0, f"got {sigs['signal_count']}")
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def test_pricing_and_revenue() -> None:
|
| 89 |
-
print("\nTest 4: Pricing and revenue — ₹399 per student and CAC ~₹50")
|
| 90 |
-
session = make_session(["We charge ₹399 per student and our CAC is around ₹50."])
|
| 91 |
-
sigs = extract_concrete_signals(session)
|
| 92 |
-
check("pricing detected", len(sigs["pricing"]) > 0, f"got {sigs['pricing']}")
|
| 93 |
-
check("revenue_signals detected", len(sigs["revenue_signals"]) > 0, f"got {sigs['revenue_signals']}")
|
| 94 |
-
check("signal_count > 0", sigs["signal_count"] > 0)
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def test_competitors_and_tech() -> None:
|
| 98 |
-
print("\nTest 5: Competitors and technical mechanism")
|
| 99 |
-
session = make_session([
|
| 100 |
-
"Luma and LinkedIn are competitors but we use profile-based ranking with embedding models."
|
| 101 |
-
])
|
| 102 |
-
sigs = extract_concrete_signals(session)
|
| 103 |
-
check("competitors detected", len(sigs["competitors"]) > 0, f"got {sigs['competitors']}")
|
| 104 |
-
check("technical_mechanisms detected", len(sigs["technical_mechanisms"]) > 0, f"got {sigs['technical_mechanisms']}")
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def test_mixed_session() -> None:
|
| 108 |
-
print("\nTest 6: Mixed session — strong + non-answer + vague")
|
| 109 |
-
session = make_session([
|
| 110 |
-
"We validated this with 50 beta users, 3 campus ambassadors, and weekly event-miss reports.",
|
| 111 |
-
"I don't know",
|
| 112 |
-
"It is a big market.",
|
| 113 |
-
"We use embeddings and a ranking model trained on student behavior.",
|
| 114 |
-
])
|
| 115 |
-
sigs = extract_concrete_signals(session)
|
| 116 |
-
check("numbers detected", len(sigs["numbers"]) > 0, f"got {sigs['numbers']}")
|
| 117 |
-
check("validation detected", len(sigs["validation"]) > 0, f"got {sigs['validation']}")
|
| 118 |
-
check("non_answers detected", len(sigs["non_answers"]) > 0)
|
| 119 |
-
check("vague_claims detected", len(sigs["vague_claims"]) > 0)
|
| 120 |
-
check("technical_mechanisms detected", len(sigs["technical_mechanisms"]) > 0)
|
| 121 |
-
check("best_user_quotes >= 2", len(sigs["best_user_quotes"]) >= 2, f"got {len(sigs['best_user_quotes'])}")
|
| 122 |
-
check("signal_count > 3", sigs["signal_count"] > 3, f"got {sigs['signal_count']}")
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
# ---------------------------------------------------------------------------
|
| 126 |
-
# Local scoring tests (Phase 5D)
|
| 127 |
-
# ---------------------------------------------------------------------------
|
| 128 |
-
|
| 129 |
-
def test_local_scoring_strong_evidence() -> None:
|
| 130 |
-
print("\nTest 7: Local scoring — '80 users, 60% interview rate, 3 colleges' should get real credit")
|
| 131 |
-
session = make_session([
|
| 132 |
-
"80 users, 60% interview rate, 3 colleges have onboarded already.",
|
| 133 |
-
"We charge ₹399 per student and our CAC is around ₹50.",
|
| 134 |
-
"Luma and LinkedIn are competitors but we use profile-based ranking with embedding models.",
|
| 135 |
-
])
|
| 136 |
-
sigs = extract_concrete_signals(session)
|
| 137 |
-
startup = session.get("startup", {})
|
| 138 |
-
scores, best_answer, weakest_answer, why_weak = _compute_local_scores(sigs, startup)
|
| 139 |
-
|
| 140 |
-
# All 6 dims must be present
|
| 141 |
-
required = {"clarity", "problem_understanding", "market_awareness",
|
| 142 |
-
"differentiation", "business_model", "objection_handling"}
|
| 143 |
-
check("all 6 dims present", required <= set(scores.keys()))
|
| 144 |
-
|
| 145 |
-
# With concrete evidence, most dims should be Developing (31+) or above
|
| 146 |
-
for dim in required:
|
| 147 |
-
s = scores[dim]["score"]
|
| 148 |
-
check(f"{dim}.score >= 30", s >= 30, f"got {s} — evidence was strong")
|
| 149 |
-
|
| 150 |
-
# Dims with direct evidence should be Solid (51+) or Strong
|
| 151 |
-
# market_awareness should get credit for numbers + competitors
|
| 152 |
-
market_s = scores["market_awareness"]["score"]
|
| 153 |
-
check("market_awareness >= 50 (has numbers + competitors)", market_s >= 50, f"got {market_s}")
|
| 154 |
-
|
| 155 |
-
# differentiation should get credit for competitors + tech
|
| 156 |
-
diff_s = scores["differentiation"]["score"]
|
| 157 |
-
check("differentiation >= 50 (has competitors + tech)", diff_s >= 50, f"got {diff_s}")
|
| 158 |
-
|
| 159 |
-
# business_model should get credit for ₹399 + CAC
|
| 160 |
-
biz_s = scores["business_model"]["score"]
|
| 161 |
-
check("business_model >= 45 (has pricing + revenue)", biz_s >= 45, f"got {biz_s}")
|
| 162 |
-
|
| 163 |
-
# best_answer should be non-empty
|
| 164 |
-
check("best_answer non-empty", bool(best_answer))
|
| 165 |
-
check("why_weak non-empty", bool(why_weak))
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def test_local_scoring_vague_floor() -> None:
|
| 169 |
-
print("\nTest 8: Local scoring — vague on-topic answer should not go below 30 (global floor)")
|
| 170 |
-
session = make_session([
|
| 171 |
-
"It is a big market because students attend events often.",
|
| 172 |
-
"We have a better AI solution than others.",
|
| 173 |
-
])
|
| 174 |
-
sigs = extract_concrete_signals(session)
|
| 175 |
-
startup = session.get("startup", {})
|
| 176 |
-
scores, _, _, _ = _compute_local_scores(sigs, startup)
|
| 177 |
-
|
| 178 |
-
# On-topic engagement = engagement > 0, so floor is 33
|
| 179 |
-
for dim, data in scores.items():
|
| 180 |
-
s = data["score"]
|
| 181 |
-
# Some dims may dip below 33 (business_model has 28 floor), but most should be >= 30
|
| 182 |
-
if dim != "business_model":
|
| 183 |
-
check(f"{dim}.score >= 30 (vague but on-topic)", s >= 30, f"got {s}")
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def test_local_scoring_non_answer_can_be_below_30() -> None:
|
| 187 |
-
print("\nTest 9: Local scoring — all non-answers can score below 30")
|
| 188 |
-
session = make_session(["ok", "I don't know", "yeah", "not sure"])
|
| 189 |
-
sigs = extract_concrete_signals(session)
|
| 190 |
-
startup = session.get("startup", {})
|
| 191 |
-
scores, _, _, _ = _compute_local_scores(sigs, startup)
|
| 192 |
-
|
| 193 |
-
# With all non-answers, engagement=0, so dims should be low
|
| 194 |
-
low_count = sum(1 for d in scores.values() if d["score"] <= 20)
|
| 195 |
-
check("majority of dims <= 20 when all non-answers", low_count >= 4,
|
| 196 |
-
f"got {low_count} dims <= 20, scores={[(k, v['score']) for k, v in scores.items()]}")
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
def test_local_scoring_pricing_revenue() -> None:
|
| 200 |
-
print("\nTest 10: Local scoring — ₹399 and CAC ₹50 → business_model Developing or better")
|
| 201 |
-
session = make_session(["We charge ₹399 per student and our CAC is around ₹50."])
|
| 202 |
-
sigs = extract_concrete_signals(session)
|
| 203 |
-
startup = session.get("startup", {})
|
| 204 |
-
scores, _, _, _ = _compute_local_scores(sigs, startup)
|
| 205 |
-
|
| 206 |
-
biz_s = scores["business_model"]["score"]
|
| 207 |
-
biz_lbl = scores["business_model"]["label"]
|
| 208 |
-
check(
|
| 209 |
-
"business_model >= 31 (Developing or better) with ₹399 + CAC",
|
| 210 |
-
biz_s >= 31,
|
| 211 |
-
f"got {biz_s} ({biz_lbl})",
|
| 212 |
-
)
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
# ---------------------------------------------------------------------------
|
| 216 |
-
# Session-aware fallback test
|
| 217 |
-
# ---------------------------------------------------------------------------
|
| 218 |
-
|
| 219 |
-
def test_session_aware_fallback_not_static() -> None:
|
| 220 |
-
print("\nTest 11: Session-aware fallback does not return EventRadar static content")
|
| 221 |
-
session = make_session(
|
| 222 |
-
[
|
| 223 |
-
"We have 80 users at IIT Delhi and IIT Bombay paying ₹499/month.",
|
| 224 |
-
"Competitors are LinkedIn and Glassdoor but we do skill-gap analysis.",
|
| 225 |
-
],
|
| 226 |
-
startup={
|
| 227 |
-
"name": "SkillBridge",
|
| 228 |
-
"problem": "Students get rejected because their resume skills don't match job requirements.",
|
| 229 |
-
"solution": "AI that maps resume skills to actual job description gaps.",
|
| 230 |
-
"why_ai": "Personalized skill-gap analysis requires LLM understanding.",
|
| 231 |
-
"stage": "Beta",
|
| 232 |
-
"team": "2 founders",
|
| 233 |
-
"traction": "80 paying users",
|
| 234 |
-
},
|
| 235 |
-
)
|
| 236 |
-
sigs = extract_concrete_signals(session)
|
| 237 |
-
fb = build_session_aware_fallback_scorecard(session, sigs, "test error")
|
| 238 |
-
|
| 239 |
-
check("scorecard_source=session_fallback", fb.get("scorecard_source") == "session_fallback")
|
| 240 |
-
check("model_ok=False", fb.get("model_ok") is False)
|
| 241 |
-
check("provider=local", fb.get("provider") == "local")
|
| 242 |
-
check("model_error present", bool(fb.get("model_error")))
|
| 243 |
-
check("overall is int 0-100",
|
| 244 |
-
isinstance(fb.get("overall"), int) and 0 <= fb.get("overall", -1) <= 100)
|
| 245 |
-
check("overall_label valid",
|
| 246 |
-
fb.get("overall_label") in {"Not addressed", "Developing", "Solid", "Strong", "Excellent"})
|
| 247 |
-
check("all 6 dims present",
|
| 248 |
-
{"clarity", "problem_understanding", "market_awareness",
|
| 249 |
-
"differentiation", "business_model", "objection_handling"}
|
| 250 |
-
<= set(fb.get("scores", {}).keys()))
|
| 251 |
-
check("concrete_signals_summary present", isinstance(fb.get("concrete_signals_summary"), dict))
|
| 252 |
-
check("top_3_questions has 3", len(fb.get("top_3_questions", [])) == 3)
|
| 253 |
-
|
| 254 |
-
full_text = str(fb)
|
| 255 |
-
for phrase in ["WhatsApp groups", "EventRadar"]:
|
| 256 |
-
check(
|
| 257 |
-
f"no static phrase '{phrase}' in session fallback",
|
| 258 |
-
phrase not in full_text or "SkillBridge" in full_text,
|
| 259 |
-
)
|
| 260 |
-
|
| 261 |
-
best = fb.get("best_answer", "")
|
| 262 |
-
check(
|
| 263 |
-
"best_answer contains actual answer text",
|
| 264 |
-
any(word in best for word in ["80 users", "IIT", "499", "SkillBridge", "LinkedIn", "skill"]),
|
| 265 |
-
f"got: {best[:120]}",
|
| 266 |
-
)
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
# ---------------------------------------------------------------------------
|
| 270 |
-
# Score label band tests
|
| 271 |
-
# ---------------------------------------------------------------------------
|
| 272 |
-
|
| 273 |
-
def test_score_label_bands() -> None:
|
| 274 |
-
print("\nTest 12: Score label bands (Phase 5C/5D)")
|
| 275 |
-
cases = [
|
| 276 |
-
(0, "Not addressed"),
|
| 277 |
-
(15, "Not addressed"),
|
| 278 |
-
(30, "Not addressed"),
|
| 279 |
-
(31, "Developing"),
|
| 280 |
-
(50, "Developing"),
|
| 281 |
-
(51, "Solid"),
|
| 282 |
-
(70, "Solid"),
|
| 283 |
-
(71, "Strong"),
|
| 284 |
-
(85, "Strong"),
|
| 285 |
-
(86, "Excellent"),
|
| 286 |
-
(100, "Excellent"),
|
| 287 |
-
]
|
| 288 |
-
for score, expected in cases:
|
| 289 |
-
got = _score_label(score)
|
| 290 |
-
check(f"_score_label({score}) == '{expected}'", got == expected, f"got '{got}'")
|
| 291 |
-
# Also verify json_utils._score_label matches
|
| 292 |
-
for score, expected in cases:
|
| 293 |
-
got = json_score_label(score)
|
| 294 |
-
check(f"json_utils._score_label({score}) == '{expected}'", got == expected, f"got '{got}'")
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
def test_all_non_answer_session() -> None:
|
| 298 |
-
print("\nTest 13: All non-answer session — fallback should still work")
|
| 299 |
-
session = make_session(["ok", "I don't know", "yeah", "not sure"])
|
| 300 |
-
sigs = extract_concrete_signals(session)
|
| 301 |
-
check("all are non_answers", len(sigs["non_answers"]) >= 3)
|
| 302 |
-
check("signal_count=0", sigs["signal_count"] == 0)
|
| 303 |
-
fb = build_session_aware_fallback_scorecard(session, sigs)
|
| 304 |
-
check("fallback returns valid dict", isinstance(fb, dict))
|
| 305 |
-
check("overall >= 0", fb.get("overall", -1) >= 0)
|
| 306 |
-
check("scorecard_source=session_fallback", fb.get("scorecard_source") == "session_fallback")
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
# ---------------------------------------------------------------------------
|
| 310 |
-
# Runner
|
| 311 |
-
# ---------------------------------------------------------------------------
|
| 312 |
-
|
| 313 |
-
def main() -> None:
|
| 314 |
-
print("\nPhase 5D — Claim Extractor + Local Scoring + Session-Aware Fallback Tests")
|
| 315 |
-
print("(No server required)\n")
|
| 316 |
-
|
| 317 |
-
test_strong_evidence_answer()
|
| 318 |
-
test_vague_answer()
|
| 319 |
-
test_non_answer()
|
| 320 |
-
test_pricing_and_revenue()
|
| 321 |
-
test_competitors_and_tech()
|
| 322 |
-
test_mixed_session()
|
| 323 |
-
test_local_scoring_strong_evidence()
|
| 324 |
-
test_local_scoring_vague_floor()
|
| 325 |
-
test_local_scoring_non_answer_can_be_below_30()
|
| 326 |
-
test_local_scoring_pricing_revenue()
|
| 327 |
-
test_session_aware_fallback_not_static()
|
| 328 |
-
test_score_label_bands()
|
| 329 |
-
test_all_non_answer_session()
|
| 330 |
-
|
| 331 |
-
print("\nAll Phase 5D claim-based scoring tests passed.\n")
|
| 332 |
-
sys.exit(0)
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
if __name__ == "__main__":
|
| 336 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_nvidia_client.py
DELETED
|
@@ -1,90 +0,0 @@
|
|
| 1 |
-
"""Isolated NVIDIA Nemotron connectivity test.
|
| 2 |
-
|
| 3 |
-
Run from the project root:
|
| 4 |
-
python scripts/test_nvidia_client.py
|
| 5 |
-
|
| 6 |
-
Exits 0 on success, 1 on failure.
|
| 7 |
-
Does not expose the API key in output.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import sys
|
| 13 |
-
from pathlib import Path
|
| 14 |
-
|
| 15 |
-
# Allow imports from project root regardless of working directory
|
| 16 |
-
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 17 |
-
|
| 18 |
-
from dotenv import load_dotenv
|
| 19 |
-
|
| 20 |
-
load_dotenv()
|
| 21 |
-
|
| 22 |
-
from core.nvidia_client import health_check, generate_nemotron_response # noqa: E402
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def main() -> int:
|
| 26 |
-
print("=" * 60)
|
| 27 |
-
print("PitchFight AI — NVIDIA Nemotron connectivity test")
|
| 28 |
-
print("=" * 60)
|
| 29 |
-
|
| 30 |
-
# 1. Health check (no key in output)
|
| 31 |
-
print("\n[1] Health check")
|
| 32 |
-
status = health_check()
|
| 33 |
-
print(f" provider : {status['provider']}")
|
| 34 |
-
print(f" configured : {status['configured']}")
|
| 35 |
-
print(f" base_url : {status['base_url']}")
|
| 36 |
-
print(f" model : {status['model']}")
|
| 37 |
-
print(f" key present : {status['api_key_present']}")
|
| 38 |
-
print(f" message : {status['message']}")
|
| 39 |
-
|
| 40 |
-
if not status["configured"]:
|
| 41 |
-
print("\n[FAIL] NVIDIA_API_KEY is not set.")
|
| 42 |
-
print(" Add it to your .env file and re-run this script.")
|
| 43 |
-
return 1
|
| 44 |
-
|
| 45 |
-
# 2. Test prompt
|
| 46 |
-
print("\n[2] Sending test prompt to Nemotron...")
|
| 47 |
-
messages = [
|
| 48 |
-
{
|
| 49 |
-
"role": "system",
|
| 50 |
-
"content": (
|
| 51 |
-
"You are a skeptical hackathon judge. "
|
| 52 |
-
"Ask exactly one sharp question. Do not give advice."
|
| 53 |
-
),
|
| 54 |
-
},
|
| 55 |
-
{
|
| 56 |
-
"role": "user",
|
| 57 |
-
"content": (
|
| 58 |
-
"Startup: EventRadar AI.\n"
|
| 59 |
-
"Problem: Students miss hackathons and tech events because "
|
| 60 |
-
"discovery is scattered.\n"
|
| 61 |
-
"Solution: AI-powered event discovery that ranks opportunities "
|
| 62 |
-
"by skills, goals, location, and deadline urgency.\n\n"
|
| 63 |
-
"Return exactly one question under 40 words."
|
| 64 |
-
),
|
| 65 |
-
},
|
| 66 |
-
]
|
| 67 |
-
|
| 68 |
-
try:
|
| 69 |
-
response = generate_nemotron_response(
|
| 70 |
-
messages,
|
| 71 |
-
mode="opponent",
|
| 72 |
-
temperature=0.7,
|
| 73 |
-
timeout=30,
|
| 74 |
-
)
|
| 75 |
-
print("\n[3] Model response:")
|
| 76 |
-
print("-" * 40)
|
| 77 |
-
print(response)
|
| 78 |
-
print("-" * 40)
|
| 79 |
-
print("\n[PASS] NVIDIA Nemotron responded successfully.")
|
| 80 |
-
print("Phase 2 model connectivity: OK")
|
| 81 |
-
return 0
|
| 82 |
-
|
| 83 |
-
except RuntimeError as exc:
|
| 84 |
-
print(f"\n[FAIL] {exc}")
|
| 85 |
-
print("Check your NVIDIA_API_KEY and NVIDIA_BASE_URL in .env")
|
| 86 |
-
return 1
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
if __name__ == "__main__":
|
| 90 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_phase3_pitch_battle.py
DELETED
|
@@ -1,123 +0,0 @@
|
|
| 1 |
-
"""Phase 3 end-to-end test: live pitch battle via the running server.
|
| 2 |
-
|
| 3 |
-
Usage:
|
| 4 |
-
python scripts/test_phase3_pitch_battle.py
|
| 5 |
-
|
| 6 |
-
Requires the server to already be running. Set PITCHFIGHT_BASE_URL to override
|
| 7 |
-
the default base URL (http://127.0.0.1:7861).
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import json
|
| 13 |
-
import os
|
| 14 |
-
import sys
|
| 15 |
-
|
| 16 |
-
import requests
|
| 17 |
-
|
| 18 |
-
BASE_URL = os.getenv("PITCHFIGHT_BASE_URL", "http://127.0.0.1:7861").rstrip("/")
|
| 19 |
-
|
| 20 |
-
SAMPLE_STARTUP = {
|
| 21 |
-
"name": "EventRadar AI",
|
| 22 |
-
"problem": "Students miss relevant hackathons, workshops, and networking events.",
|
| 23 |
-
"solution": "AI-powered event discovery that ranks events by fit for each student profile.",
|
| 24 |
-
"why_ai": "Personalized ranking requires understanding student goals and event signals together.",
|
| 25 |
-
"stage": "Prototype",
|
| 26 |
-
"team": "2 founders",
|
| 27 |
-
"traction": "50 beta signups, no revenue yet",
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
USER_ANSWER = (
|
| 31 |
-
"It is a pretty big market because students attend events often."
|
| 32 |
-
)
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def post(path: str, body: dict) -> dict:
|
| 36 |
-
url = f"{BASE_URL}{path}"
|
| 37 |
-
resp = requests.post(url, json=body, timeout=60)
|
| 38 |
-
resp.raise_for_status()
|
| 39 |
-
return resp.json()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def check(label: str, condition: bool, detail: str = "") -> None:
|
| 43 |
-
if condition:
|
| 44 |
-
print(f" PASS {label}")
|
| 45 |
-
else:
|
| 46 |
-
print(f" FAIL {label}" + (f" — {detail}" if detail else ""))
|
| 47 |
-
sys.exit(1)
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def main() -> None:
|
| 51 |
-
print(f"\nPhase 3 Pitch Battle Test\nBase URL: {BASE_URL}\n")
|
| 52 |
-
|
| 53 |
-
# --- Step 1: start-session ---
|
| 54 |
-
print("Step 1: POST /api/start-session")
|
| 55 |
-
try:
|
| 56 |
-
data = post("/api/start-session", {
|
| 57 |
-
"mode": "pitch_battle",
|
| 58 |
-
"startup": SAMPLE_STARTUP,
|
| 59 |
-
"persona": "hackathon_judge",
|
| 60 |
-
"difficulty": "high",
|
| 61 |
-
"input_mode": "text",
|
| 62 |
-
"model_mode": "premium_nvidia",
|
| 63 |
-
})
|
| 64 |
-
except Exception as exc:
|
| 65 |
-
print(f" FAIL Request failed: {exc}")
|
| 66 |
-
sys.exit(1)
|
| 67 |
-
|
| 68 |
-
print(f" session_id : {data.get('session_id', '—')}")
|
| 69 |
-
print(f" model_ok : {data.get('model_ok')}")
|
| 70 |
-
print(f" provider : {data.get('provider')}")
|
| 71 |
-
print(f" model_mode : {data.get('model_mode')}")
|
| 72 |
-
print(f" attack_tag : {data.get('attack_tag')}")
|
| 73 |
-
print(f" round : {data.get('round')}")
|
| 74 |
-
print(f" ai_message :\n {data.get('ai_message', '')}\n")
|
| 75 |
-
|
| 76 |
-
session_id = data.get("session_id")
|
| 77 |
-
check("session_id present", bool(session_id), "got empty session_id")
|
| 78 |
-
check("ai_message non-empty", bool(data.get("ai_message")), "ai_message is empty")
|
| 79 |
-
check("round is 1", data.get("round") == 1, f"got {data.get('round')}")
|
| 80 |
-
check("attack_tag present", bool(data.get("attack_tag")))
|
| 81 |
-
|
| 82 |
-
if data.get("model_ok"):
|
| 83 |
-
check("provider is nvidia", data.get("provider") == "nvidia")
|
| 84 |
-
else:
|
| 85 |
-
print(" NOTE Model returned mock fallback (NVIDIA may be down or key missing)")
|
| 86 |
-
if data.get("model_error"):
|
| 87 |
-
print(f" model_error: {data['model_error']}")
|
| 88 |
-
|
| 89 |
-
# --- Step 2: chat-round ---
|
| 90 |
-
print("\nStep 2: POST /api/chat-round")
|
| 91 |
-
try:
|
| 92 |
-
data2 = post("/api/chat-round", {
|
| 93 |
-
"session_id": session_id,
|
| 94 |
-
"user_message": USER_ANSWER,
|
| 95 |
-
})
|
| 96 |
-
except Exception as exc:
|
| 97 |
-
print(f" FAIL Request failed: {exc}")
|
| 98 |
-
sys.exit(1)
|
| 99 |
-
|
| 100 |
-
print(f" model_ok : {data2.get('model_ok')}")
|
| 101 |
-
print(f" provider : {data2.get('provider')}")
|
| 102 |
-
print(f" model_mode : {data2.get('model_mode')}")
|
| 103 |
-
print(f" attack_tag : {data2.get('attack_tag')}")
|
| 104 |
-
print(f" round : {data2.get('round')}")
|
| 105 |
-
print(f" ai_message :\n {data2.get('ai_message', '')}\n")
|
| 106 |
-
|
| 107 |
-
check("session_id echoed", data2.get("session_id") == session_id)
|
| 108 |
-
check("ai_message non-empty", bool(data2.get("ai_message")), "ai_message is empty")
|
| 109 |
-
check("round is 2", data2.get("round") == 2, f"got {data2.get('round')}")
|
| 110 |
-
|
| 111 |
-
if data2.get("model_ok"):
|
| 112 |
-
check("provider is nvidia", data2.get("provider") == "nvidia")
|
| 113 |
-
else:
|
| 114 |
-
print(" NOTE chat-round returned mock fallback")
|
| 115 |
-
if data2.get("model_error"):
|
| 116 |
-
print(f" model_error: {data2['model_error']}")
|
| 117 |
-
|
| 118 |
-
print("\nAll checks passed. Phase 3 integration is working.\n")
|
| 119 |
-
sys.exit(0)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
if __name__ == "__main__":
|
| 123 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_phase3b_battle_flow.py
DELETED
|
@@ -1,217 +0,0 @@
|
|
| 1 |
-
"""Phase 3B end-to-end test: Socratic judge flow via the running server.
|
| 2 |
-
|
| 3 |
-
Validates:
|
| 4 |
-
- answer_quality classification appears in chat-round responses
|
| 5 |
-
- judge_action drives tag switching / follow-up decisions
|
| 6 |
-
- Same attack tag not repeated more than MAX_ATTEMPTS_PER_ATTACK_TAG times
|
| 7 |
-
- ai_message is always non-empty (model or mock fallback)
|
| 8 |
-
- All required response fields are present
|
| 9 |
-
|
| 10 |
-
Usage:
|
| 11 |
-
python scripts/test_phase3b_battle_flow.py
|
| 12 |
-
|
| 13 |
-
Server must already be running. Set PITCHFIGHT_BASE_URL to override default.
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
from __future__ import annotations
|
| 17 |
-
|
| 18 |
-
import os
|
| 19 |
-
import sys
|
| 20 |
-
from collections import defaultdict
|
| 21 |
-
|
| 22 |
-
import requests
|
| 23 |
-
|
| 24 |
-
BASE_URL = os.getenv("PITCHFIGHT_BASE_URL", "http://127.0.0.1:7861").rstrip("/")
|
| 25 |
-
|
| 26 |
-
SAMPLE_STARTUP = {
|
| 27 |
-
"name": "EventRadar AI",
|
| 28 |
-
"problem": "Students miss relevant hackathons, workshops, and networking events.",
|
| 29 |
-
"solution": "AI-powered event discovery that ranks events by fit for each student profile.",
|
| 30 |
-
"why_ai": "Personalized ranking requires understanding student goals and event signals together.",
|
| 31 |
-
"stage": "Prototype",
|
| 32 |
-
"team": "2 founders",
|
| 33 |
-
"traction": "50 beta signups, no revenue yet",
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
STRONG_ANSWER = (
|
| 37 |
-
"We validated this with 50 beta users, 3 campus ambassadors, "
|
| 38 |
-
"and weekly event-miss reports from two colleges."
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
ANSWERS = [
|
| 42 |
-
"It is a pretty big market because students attend events often.", # weak
|
| 43 |
-
"I don't know", # non_answer
|
| 44 |
-
"ok", # non_answer → triggers move_after_limit
|
| 45 |
-
STRONG_ANSWER, # strong → move_next_tag
|
| 46 |
-
"The AI personalizes event ranking based on student goals.", # partial
|
| 47 |
-
]
|
| 48 |
-
|
| 49 |
-
MAX_ATTEMPTS_ALLOWED = 2 # must match battle_flow.MAX_ATTEMPTS_PER_ATTACK_TAG
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def post(path: str, body: dict) -> dict:
|
| 53 |
-
url = f"{BASE_URL}{path}"
|
| 54 |
-
resp = requests.post(url, json=body, timeout=60)
|
| 55 |
-
resp.raise_for_status()
|
| 56 |
-
return resp.json()
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def check(label: str, condition: bool, detail: str = "") -> None:
|
| 60 |
-
marker = "PASS" if condition else "FAIL"
|
| 61 |
-
suffix = f" — {detail}" if detail else ""
|
| 62 |
-
print(f" {marker} {label}{suffix}")
|
| 63 |
-
if not condition:
|
| 64 |
-
sys.exit(1)
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def main() -> None:
|
| 68 |
-
print(f"\nPhase 3B Battle Flow Test\nBase URL: {BASE_URL}\n")
|
| 69 |
-
|
| 70 |
-
# --- start-session ---
|
| 71 |
-
print("Step 0: POST /api/start-session")
|
| 72 |
-
try:
|
| 73 |
-
start = post("/api/start-session", {
|
| 74 |
-
"mode": "pitch_battle",
|
| 75 |
-
"startup": SAMPLE_STARTUP,
|
| 76 |
-
"persona": "hackathon_judge",
|
| 77 |
-
"difficulty": "high",
|
| 78 |
-
"input_mode": "text",
|
| 79 |
-
"model_mode": "premium_nvidia",
|
| 80 |
-
})
|
| 81 |
-
except Exception as exc:
|
| 82 |
-
print(f" FAIL Request failed: {exc}")
|
| 83 |
-
sys.exit(1)
|
| 84 |
-
|
| 85 |
-
session_id = start.get("session_id")
|
| 86 |
-
check("session_id present", bool(session_id))
|
| 87 |
-
check("ai_message non-empty", bool(start.get("ai_message")))
|
| 88 |
-
check("judge_action = opening_question", start.get("judge_action") == "opening_question",
|
| 89 |
-
f"got {start.get('judge_action')!r}")
|
| 90 |
-
check("answer_quality is None", start.get("answer_quality") is None,
|
| 91 |
-
f"got {start.get('answer_quality')!r}")
|
| 92 |
-
check("tag_attempt = 1", start.get("tag_attempt") == 1, f"got {start.get('tag_attempt')!r}")
|
| 93 |
-
|
| 94 |
-
opening_tag = start.get("attack_tag")
|
| 95 |
-
print(f" opening attack_tag : {opening_tag}")
|
| 96 |
-
print(f" ai_message : {start['ai_message'][:120]}...\n")
|
| 97 |
-
|
| 98 |
-
# Track tag attempt counts locally to verify the invariant
|
| 99 |
-
tag_attempts: dict[str, int] = defaultdict(int)
|
| 100 |
-
tag_attempts[opening_tag] += 1
|
| 101 |
-
|
| 102 |
-
rounds: list[dict] = []
|
| 103 |
-
|
| 104 |
-
# --- chat rounds ---
|
| 105 |
-
for i, answer in enumerate(ANSWERS, start=1):
|
| 106 |
-
round_num = i + 1 # round 1 was the opening
|
| 107 |
-
print(f"Step {i}: POST /api/chat-round (sending: {answer!r})")
|
| 108 |
-
|
| 109 |
-
try:
|
| 110 |
-
data = post("/api/chat-round", {
|
| 111 |
-
"session_id": session_id,
|
| 112 |
-
"user_message": answer,
|
| 113 |
-
})
|
| 114 |
-
except Exception as exc:
|
| 115 |
-
print(f" FAIL Request failed: {exc}")
|
| 116 |
-
sys.exit(1)
|
| 117 |
-
|
| 118 |
-
atag = data.get("attack_tag", "")
|
| 119 |
-
aq = data.get("answer_quality", "")
|
| 120 |
-
ja = data.get("judge_action", "")
|
| 121 |
-
attempt = data.get("tag_attempt", 0)
|
| 122 |
-
satisfied = data.get("topic_satisfied")
|
| 123 |
-
prev_tag = data.get("previous_attack_tag", "")
|
| 124 |
-
|
| 125 |
-
# Update local tracking
|
| 126 |
-
tag_attempts[atag] += 1
|
| 127 |
-
|
| 128 |
-
print(f" round : {data.get('round')}")
|
| 129 |
-
print(f" attack_tag : {atag}")
|
| 130 |
-
print(f" previous_tag : {prev_tag}")
|
| 131 |
-
print(f" answer_quality : {aq}")
|
| 132 |
-
print(f" judge_action : {ja}")
|
| 133 |
-
print(f" tag_attempt : {attempt}")
|
| 134 |
-
print(f" topic_satisfied : {satisfied}")
|
| 135 |
-
print(f" model_ok : {data.get('model_ok')}")
|
| 136 |
-
print(f" provider : {data.get('provider')}")
|
| 137 |
-
print(f" ai_message : {data.get('ai_message', '')[:120]}...\n")
|
| 138 |
-
|
| 139 |
-
# Required field checks
|
| 140 |
-
check("session_id echoed", data.get("session_id") == session_id)
|
| 141 |
-
check("ai_message non-empty", bool(data.get("ai_message")), "ai_message is empty")
|
| 142 |
-
check("attack_tag present", bool(atag))
|
| 143 |
-
check("answer_quality present", aq in ("strong", "partial", "weak", "non_answer"),
|
| 144 |
-
f"got {aq!r}")
|
| 145 |
-
check("judge_action present", ja in ("follow_up_same_tag", "move_next_tag", "move_after_limit"),
|
| 146 |
-
f"got {ja!r}")
|
| 147 |
-
check("tag_attempt present", isinstance(attempt, int) and attempt >= 1,
|
| 148 |
-
f"got {attempt!r}")
|
| 149 |
-
check("provider present", bool(data.get("provider")))
|
| 150 |
-
|
| 151 |
-
rounds.append(data)
|
| 152 |
-
|
| 153 |
-
# --- Invariant: no tag exceeded MAX_ATTEMPTS_ALLOWED across all rounds ---
|
| 154 |
-
print("Invariant check: no attack_tag exceeded MAX_ATTEMPTS_PER_ATTACK_TAG")
|
| 155 |
-
for tag, count in tag_attempts.items():
|
| 156 |
-
if tag in ("Round Limit", "Session Error"):
|
| 157 |
-
continue
|
| 158 |
-
check(
|
| 159 |
-
f" tag '{tag}' attempts <= {MAX_ATTEMPTS_ALLOWED}",
|
| 160 |
-
count <= MAX_ATTEMPTS_ALLOWED,
|
| 161 |
-
f"got {count} attempts",
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
# --- Verify first weak answer triggered follow_up_same_tag (not a random jump) ---
|
| 165 |
-
first = rounds[0]
|
| 166 |
-
first_ja = first.get("judge_action", "")
|
| 167 |
-
check(
|
| 168 |
-
"First weak answer → follow_up or move (not random jump)",
|
| 169 |
-
first_ja in ("follow_up_same_tag", "move_after_limit", "move_next_tag"),
|
| 170 |
-
f"got {first_ja!r}",
|
| 171 |
-
)
|
| 172 |
-
|
| 173 |
-
# --- Verify third answer ("ok") did not keep drilling the same tag forever ---
|
| 174 |
-
if len(rounds) >= 3:
|
| 175 |
-
third_ja = rounds[2].get("judge_action", "")
|
| 176 |
-
check(
|
| 177 |
-
"After 2nd non-answer on same tag, judge moved or pressed (no infinite loop)",
|
| 178 |
-
third_ja in ("move_after_limit", "move_next_tag", "follow_up_same_tag"),
|
| 179 |
-
f"got {third_ja!r}",
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
# --- Strong-answer checks (rounds[3] = STRONG_ANSWER) ---
|
| 183 |
-
if len(rounds) >= 4:
|
| 184 |
-
strong_round = rounds[3]
|
| 185 |
-
strong_ja = strong_round.get("judge_action", "")
|
| 186 |
-
strong_tag = strong_round.get("attack_tag", "")
|
| 187 |
-
strong_prev = strong_round.get("previous_attack_tag", "")
|
| 188 |
-
strong_satisfied = strong_round.get("topic_satisfied")
|
| 189 |
-
|
| 190 |
-
print("\nStrong-answer invariant checks:")
|
| 191 |
-
check(
|
| 192 |
-
"Strong answer → judge_action is move_next_tag",
|
| 193 |
-
strong_ja == "move_next_tag",
|
| 194 |
-
f"got {strong_ja!r}",
|
| 195 |
-
)
|
| 196 |
-
check(
|
| 197 |
-
"Strong answer → attack_tag changed from previous_attack_tag",
|
| 198 |
-
strong_tag != strong_prev,
|
| 199 |
-
f"both are {strong_tag!r} — judge did not advance",
|
| 200 |
-
)
|
| 201 |
-
check(
|
| 202 |
-
"Strong answer → topic_satisfied is True",
|
| 203 |
-
strong_satisfied is True,
|
| 204 |
-
f"got {strong_satisfied!r}",
|
| 205 |
-
)
|
| 206 |
-
check(
|
| 207 |
-
"Strong answer → answer_quality is 'strong'",
|
| 208 |
-
strong_round.get("answer_quality") == "strong",
|
| 209 |
-
f"got {strong_round.get('answer_quality')!r}",
|
| 210 |
-
)
|
| 211 |
-
|
| 212 |
-
print("\nAll Phase 3B checks passed. Socratic battle flow is working.\n")
|
| 213 |
-
sys.exit(0)
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
if __name__ == "__main__":
|
| 217 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_phase4_battle_stability.py
DELETED
|
@@ -1,261 +0,0 @@
|
|
| 1 |
-
"""Phase 4 stability test: full battle lifecycle via the running server.
|
| 2 |
-
|
| 3 |
-
Validates:
|
| 4 |
-
- Battle runs cleanly from start to and beyond MAX_ROUNDS
|
| 5 |
-
- Strong answer causes move_next_tag with different attack_tag
|
| 6 |
-
- No attack_tag exceeds 2 attempts
|
| 7 |
-
- At round >= MAX_ROUNDS: soft_round_limit_reached=True, battle_complete=False, can_continue=True
|
| 8 |
-
- Chat continues working after MAX_ROUNDS (no hard block)
|
| 9 |
-
- /api/end-battle returns a scorecard dict (even if mock)
|
| 10 |
-
- Every ai_message is non-empty throughout
|
| 11 |
-
- No ai_message contains instruction leakage patterns
|
| 12 |
-
|
| 13 |
-
Usage:
|
| 14 |
-
python scripts/test_phase4_battle_stability.py
|
| 15 |
-
PITCHFIGHT_BASE_URL=http://localhost:7861 python scripts/test_phase4_battle_stability.py
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import os
|
| 21 |
-
import re
|
| 22 |
-
import sys
|
| 23 |
-
from collections import defaultdict
|
| 24 |
-
|
| 25 |
-
import requests
|
| 26 |
-
|
| 27 |
-
BASE_URL = os.getenv("PITCHFIGHT_BASE_URL", "http://127.0.0.1:7861").rstrip("/")
|
| 28 |
-
MAX_ROUNDS = int(os.getenv("MAX_ROUNDS", "6"))
|
| 29 |
-
|
| 30 |
-
SAMPLE_STARTUP = {
|
| 31 |
-
"name": "EventRadar AI",
|
| 32 |
-
"problem": "Students miss relevant hackathons, workshops, and networking events.",
|
| 33 |
-
"solution": "AI-powered event discovery that ranks events by fit for each student profile.",
|
| 34 |
-
"why_ai": "Personalized ranking requires understanding student goals and event signals together.",
|
| 35 |
-
"stage": "Prototype",
|
| 36 |
-
"team": "2 founders",
|
| 37 |
-
"traction": "50 beta signups, no revenue yet",
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
STRONG_ANSWER = (
|
| 41 |
-
"We validated this with 50 beta users, 3 campus ambassadors, "
|
| 42 |
-
"and weekly event-miss reports from two colleges."
|
| 43 |
-
)
|
| 44 |
-
|
| 45 |
-
# Sequence designed to exercise all branches: weak → non_answer → strong → partial → weak → strong
|
| 46 |
-
ANSWER_SEQUENCE = [
|
| 47 |
-
"It is a pretty big market because students attend events often.", # weak
|
| 48 |
-
"I don't know", # non_answer
|
| 49 |
-
STRONG_ANSWER, # strong → move_next_tag
|
| 50 |
-
"The AI ranks events based on student interest profiles.", # partial
|
| 51 |
-
"okay fine", # non_answer
|
| 52 |
-
"We use embeddings and a ranking model trained on student behavior.", # strong (technical)
|
| 53 |
-
"It is useful and helpful for everyone.", # weak — beyond MAX_ROUNDS
|
| 54 |
-
]
|
| 55 |
-
|
| 56 |
-
_LEAKAGE_PATTERNS = re.compile(
|
| 57 |
-
r"we need to\b|the prompt says\b|as instructed\b|my instructions\b"
|
| 58 |
-
r"|i am supposed to\b|i should follow\b|the rules say\b|per the instructions?\b",
|
| 59 |
-
re.IGNORECASE,
|
| 60 |
-
)
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def post(path: str, body: dict, timeout: int = 90) -> dict:
|
| 64 |
-
resp = requests.post(f"{BASE_URL}{path}", json=body, timeout=timeout)
|
| 65 |
-
resp.raise_for_status()
|
| 66 |
-
return resp.json()
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def check(label: str, condition: bool, detail: str = "") -> None:
|
| 70 |
-
marker = "PASS" if condition else "FAIL"
|
| 71 |
-
suffix = f" — {detail}" if detail else ""
|
| 72 |
-
print(f" {marker} {label}{suffix}")
|
| 73 |
-
if not condition:
|
| 74 |
-
sys.exit(1)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def main() -> None:
|
| 78 |
-
print(f"\nPhase 4 Battle Stability Test (Soft Round Limit)")
|
| 79 |
-
print(f"Base URL : {BASE_URL}")
|
| 80 |
-
print(f"MAX_ROUNDS: {MAX_ROUNDS}\n")
|
| 81 |
-
|
| 82 |
-
# -------------------------------------------------------------------------
|
| 83 |
-
# Step 0: Start session
|
| 84 |
-
# -------------------------------------------------------------------------
|
| 85 |
-
print("Step 0: POST /api/start-session")
|
| 86 |
-
try:
|
| 87 |
-
start = post("/api/start-session", {
|
| 88 |
-
"mode": "pitch_battle",
|
| 89 |
-
"startup": SAMPLE_STARTUP,
|
| 90 |
-
"persona": "hackathon_judge",
|
| 91 |
-
"difficulty": "high",
|
| 92 |
-
"input_mode": "text",
|
| 93 |
-
"model_mode": "premium_nvidia",
|
| 94 |
-
})
|
| 95 |
-
except Exception as exc:
|
| 96 |
-
print(f" FAIL Request failed: {exc}")
|
| 97 |
-
sys.exit(1)
|
| 98 |
-
|
| 99 |
-
session_id = start.get("session_id")
|
| 100 |
-
check("session_id present", bool(session_id))
|
| 101 |
-
check("ai_message non-empty", bool(start.get("ai_message")))
|
| 102 |
-
check("round == 1", start.get("round") == 1, f"got {start.get('round')}")
|
| 103 |
-
check("judge_action == opening_question", start.get("judge_action") == "opening_question")
|
| 104 |
-
check("battle_complete=False at start", start.get("battle_complete") is False)
|
| 105 |
-
check("can_continue=True at start", start.get("can_continue") is True)
|
| 106 |
-
|
| 107 |
-
opening_tag = start.get("attack_tag")
|
| 108 |
-
print(f" opening_tag : {opening_tag}")
|
| 109 |
-
print(f" battle_phase : {start.get('battle_phase')}")
|
| 110 |
-
print(f" ai_message : {start['ai_message'][:100]}...\n")
|
| 111 |
-
|
| 112 |
-
check(
|
| 113 |
-
"start ai_message has no leakage",
|
| 114 |
-
not _LEAKAGE_PATTERNS.search(start.get("ai_message", "")),
|
| 115 |
-
"instruction leakage detected in opening message",
|
| 116 |
-
)
|
| 117 |
-
|
| 118 |
-
# -------------------------------------------------------------------------
|
| 119 |
-
# Steps 1..MAX_ROUNDS+1: chat rounds (one beyond soft limit)
|
| 120 |
-
# -------------------------------------------------------------------------
|
| 121 |
-
tag_attempt_counts: dict[str, int] = defaultdict(int)
|
| 122 |
-
tag_attempt_counts[opening_tag] += 1
|
| 123 |
-
|
| 124 |
-
rounds: list[dict] = []
|
| 125 |
-
strong_round_idx: int | None = None
|
| 126 |
-
|
| 127 |
-
answers_to_send = ANSWER_SEQUENCE[: MAX_ROUNDS + 1] # include one beyond soft limit
|
| 128 |
-
|
| 129 |
-
for i, answer in enumerate(answers_to_send):
|
| 130 |
-
print(f"Step {i + 1}: POST /api/chat-round answer={answer[:60]!r}")
|
| 131 |
-
try:
|
| 132 |
-
data = post("/api/chat-round", {
|
| 133 |
-
"session_id": session_id,
|
| 134 |
-
"user_message": answer,
|
| 135 |
-
})
|
| 136 |
-
except Exception as exc:
|
| 137 |
-
print(f" FAIL Request failed: {exc}")
|
| 138 |
-
sys.exit(1)
|
| 139 |
-
|
| 140 |
-
atag = data.get("attack_tag", "")
|
| 141 |
-
aq = data.get("answer_quality", "")
|
| 142 |
-
ja = data.get("judge_action", "")
|
| 143 |
-
prev_tag = data.get("previous_attack_tag", "")
|
| 144 |
-
rnd = data.get("round", 0)
|
| 145 |
-
bc = data.get("battle_complete", True)
|
| 146 |
-
cc = data.get("can_continue", False)
|
| 147 |
-
na = data.get("next_action", "")
|
| 148 |
-
sl = data.get("soft_round_limit_reached", False)
|
| 149 |
-
phase = data.get("battle_phase", "")
|
| 150 |
-
|
| 151 |
-
if atag not in ("Round Limit", "Session Error", ""):
|
| 152 |
-
tag_attempt_counts[atag] += 1
|
| 153 |
-
|
| 154 |
-
print(f" round : {rnd}")
|
| 155 |
-
print(f" battle_phase : {phase}")
|
| 156 |
-
print(f" attack_tag : {atag}")
|
| 157 |
-
print(f" prev_tag : {prev_tag}")
|
| 158 |
-
print(f" answer_quality : {aq}")
|
| 159 |
-
print(f" judge_action : {ja}")
|
| 160 |
-
print(f" battle_complete : {bc} can_continue: {cc} next_action: {na}")
|
| 161 |
-
print(f" soft_round_limit_reached: {sl}")
|
| 162 |
-
print(f" model_ok : {data.get('model_ok')} provider: {data.get('provider')}")
|
| 163 |
-
print(f" ai_message : {data.get('ai_message', '')[:100]}...\n")
|
| 164 |
-
|
| 165 |
-
# Required fields
|
| 166 |
-
check("session_id echoed", data.get("session_id") == session_id)
|
| 167 |
-
check("ai_message non-empty", bool(data.get("ai_message")), "empty ai_message")
|
| 168 |
-
check("attack_tag present", bool(atag))
|
| 169 |
-
check("provider present", bool(data.get("provider")))
|
| 170 |
-
check("model_mode present", bool(data.get("model_mode")))
|
| 171 |
-
|
| 172 |
-
# battle_complete must always be False (soft limit only)
|
| 173 |
-
check("battle_complete always False", bc is False, f"got {bc!r}")
|
| 174 |
-
check("can_continue always True", cc is True, f"got {cc!r}")
|
| 175 |
-
check("next_action always continue", na == "continue", f"got {na!r}")
|
| 176 |
-
|
| 177 |
-
# Soft limit flag
|
| 178 |
-
if rnd >= MAX_ROUNDS:
|
| 179 |
-
check("soft_round_limit_reached=True at/after MAX_ROUNDS", sl is True, f"got {sl!r}")
|
| 180 |
-
check(
|
| 181 |
-
"recommended_action=end_battle when soft limit",
|
| 182 |
-
data.get("recommended_action") == "end_battle",
|
| 183 |
-
f"got {data.get('recommended_action')!r}",
|
| 184 |
-
)
|
| 185 |
-
else:
|
| 186 |
-
check("soft_round_limit_reached=False before MAX_ROUNDS", sl is False, f"got {sl!r}")
|
| 187 |
-
|
| 188 |
-
# Valid answer quality and judge action on normal rounds
|
| 189 |
-
if atag not in ("Round Limit",):
|
| 190 |
-
check(
|
| 191 |
-
"answer_quality valid",
|
| 192 |
-
aq in ("strong", "partial", "weak", "non_answer"),
|
| 193 |
-
f"got {aq!r}",
|
| 194 |
-
)
|
| 195 |
-
check(
|
| 196 |
-
"judge_action valid",
|
| 197 |
-
ja in ("follow_up_same_tag", "move_next_tag", "move_after_limit"),
|
| 198 |
-
f"got {ja!r}",
|
| 199 |
-
)
|
| 200 |
-
|
| 201 |
-
# No leakage
|
| 202 |
-
ai_msg = data.get("ai_message", "")
|
| 203 |
-
check(
|
| 204 |
-
"ai_message has no instruction leakage",
|
| 205 |
-
not _LEAKAGE_PATTERNS.search(ai_msg),
|
| 206 |
-
"instruction leakage detected",
|
| 207 |
-
)
|
| 208 |
-
|
| 209 |
-
# battle_phase progression
|
| 210 |
-
if rnd <= 3:
|
| 211 |
-
check("battle_phase=explore in rounds 1-3", phase == "explore", f"got {phase!r}")
|
| 212 |
-
elif rnd <= 6:
|
| 213 |
-
check("battle_phase=pressure in rounds 4-6", phase == "pressure", f"got {phase!r}")
|
| 214 |
-
else:
|
| 215 |
-
check("battle_phase=close in rounds 7+", phase == "close", f"got {phase!r}")
|
| 216 |
-
|
| 217 |
-
# Strong answer invariant
|
| 218 |
-
if aq == "strong" and strong_round_idx is None:
|
| 219 |
-
strong_round_idx = i
|
| 220 |
-
check("Strong answer → judge_action is move_next_tag", ja == "move_next_tag", f"got {ja!r}")
|
| 221 |
-
check("Strong answer → attack_tag changed", atag != prev_tag, f"both are {atag!r}")
|
| 222 |
-
check("Strong answer → topic_satisfied is True", data.get("topic_satisfied") is True)
|
| 223 |
-
|
| 224 |
-
rounds.append(data)
|
| 225 |
-
|
| 226 |
-
# -------------------------------------------------------------------------
|
| 227 |
-
# Invariant: no attack_tag exceeded 2 attempts
|
| 228 |
-
# -------------------------------------------------------------------------
|
| 229 |
-
print("Invariant: no attack_tag exceeded MAX_ATTEMPTS_PER_ATTACK_TAG (2)")
|
| 230 |
-
for tag, count in tag_attempt_counts.items():
|
| 231 |
-
if tag in ("Round Limit", "Session Error", ""):
|
| 232 |
-
continue
|
| 233 |
-
check(f" tag '{tag}' attempts <= 2", count <= 2, f"got {count}")
|
| 234 |
-
|
| 235 |
-
# -------------------------------------------------------------------------
|
| 236 |
-
# /api/end-battle — must return a scorecard dict (mock is fine)
|
| 237 |
-
# -------------------------------------------------------------------------
|
| 238 |
-
print("\nStep final: POST /api/end-battle")
|
| 239 |
-
try:
|
| 240 |
-
scorecard = post("/api/end-battle", {"session_id": session_id})
|
| 241 |
-
except Exception as exc:
|
| 242 |
-
print(f" FAIL end-battle request failed: {exc}")
|
| 243 |
-
sys.exit(1)
|
| 244 |
-
|
| 245 |
-
print(f" overall : {scorecard.get('overall')}")
|
| 246 |
-
print(f" overall_label : {scorecard.get('overall_label')}")
|
| 247 |
-
print(f" scorecard_source: {scorecard.get('scorecard_source')}")
|
| 248 |
-
print(f" keys : {list(scorecard.keys())}")
|
| 249 |
-
|
| 250 |
-
check("end-battle returns dict", isinstance(scorecard, dict))
|
| 251 |
-
check("no error in end-battle", "error" not in scorecard, f"error={scorecard.get('error')}")
|
| 252 |
-
check("overall score present", "overall" in scorecard, f"keys={list(scorecard.keys())}")
|
| 253 |
-
check("overall_label present", "overall_label" in scorecard)
|
| 254 |
-
check("scorecard_source present", "scorecard_source" in scorecard)
|
| 255 |
-
|
| 256 |
-
print("\nAll Phase 4 stability checks passed.\n")
|
| 257 |
-
sys.exit(0)
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
if __name__ == "__main__":
|
| 261 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_phase5_scorecard.py
DELETED
|
@@ -1,329 +0,0 @@
|
|
| 1 |
-
"""Phase 5D scorecard test: hybrid claim-based scoring via the running server.
|
| 2 |
-
|
| 3 |
-
Validates:
|
| 4 |
-
- /api/end-battle returns all required fields
|
| 5 |
-
- All 6 score dimensions present with integer scores 0-100
|
| 6 |
-
- Score labels use 5-band scheme (Not addressed / Developing / Solid / Strong / Excellent)
|
| 7 |
-
- top_3_questions has exactly 3 items
|
| 8 |
-
- model_ok, provider, scorecard_source present
|
| 9 |
-
- concrete_signals_summary present with required sub-keys
|
| 10 |
-
- scorecard_source in ("hybrid_claims_nemotron", "hybrid_claims_local")
|
| 11 |
-
- provider in ("local+nvidia", "local")
|
| 12 |
-
- Set STRICT_NEMOTRON_COACHING=true to fail if not hybrid_claims_nemotron
|
| 13 |
-
- Conversation includes "50 beta users" and "3 campus ambassadors" →
|
| 14 |
-
expect these signals to appear in concrete_signals_summary or scores
|
| 15 |
-
- No old static mock content
|
| 16 |
-
|
| 17 |
-
Usage:
|
| 18 |
-
python scripts/test_phase5_scorecard.py
|
| 19 |
-
PITCHFIGHT_BASE_URL=http://localhost:7861 python scripts/test_phase5_scorecard.py
|
| 20 |
-
STRICT_NEMOTRON_COACHING=true python scripts/test_phase5_scorecard.py
|
| 21 |
-
|
| 22 |
-
Server must already be running (python app.py).
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
from __future__ import annotations
|
| 26 |
-
|
| 27 |
-
import os
|
| 28 |
-
import re
|
| 29 |
-
import sys
|
| 30 |
-
|
| 31 |
-
import requests
|
| 32 |
-
|
| 33 |
-
BASE_URL = os.getenv("PITCHFIGHT_BASE_URL", "http://127.0.0.1:7861").rstrip("/")
|
| 34 |
-
STRICT_COACHING = os.getenv("STRICT_NEMOTRON_COACHING", "false").strip().lower() == "true"
|
| 35 |
-
# Legacy flag kept for compatibility
|
| 36 |
-
ALLOW_FALLBACK = os.getenv("ALLOW_SCORECARD_FALLBACK", "false").strip().lower() == "true"
|
| 37 |
-
|
| 38 |
-
SAMPLE_STARTUP = {
|
| 39 |
-
"name": "EventRadar AI",
|
| 40 |
-
"problem": "Students miss relevant hackathons, workshops, and networking events.",
|
| 41 |
-
"solution": "AI-powered event discovery that ranks events by fit for each student profile.",
|
| 42 |
-
"why_ai": "Personalized ranking requires understanding student goals and event signals together.",
|
| 43 |
-
"stage": "Prototype",
|
| 44 |
-
"team": "2 founders",
|
| 45 |
-
"traction": "50 beta signups, no revenue yet",
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
STRONG_ANSWER = (
|
| 49 |
-
"We validated this with 50 beta users, 3 campus ambassadors, "
|
| 50 |
-
"and weekly event-miss reports from two colleges."
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
ANSWERS = [
|
| 54 |
-
"It is a big market because students attend events often.", # weak
|
| 55 |
-
"I don't know", # non_answer
|
| 56 |
-
STRONG_ANSWER, # strong → evidence
|
| 57 |
-
"The AI personalizes event ranking based on student goals.", # partial
|
| 58 |
-
]
|
| 59 |
-
|
| 60 |
-
REQUIRED_DIMS = {
|
| 61 |
-
"clarity",
|
| 62 |
-
"problem_understanding",
|
| 63 |
-
"market_awareness",
|
| 64 |
-
"differentiation",
|
| 65 |
-
"business_model",
|
| 66 |
-
"objection_handling",
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
_VALID_LABELS = {"Not addressed", "Developing", "Solid", "Strong", "Excellent"}
|
| 70 |
-
_VALID_SOURCES = {"hybrid_claims_nemotron", "hybrid_claims_local"}
|
| 71 |
-
_VALID_PROVIDERS = {"local+nvidia", "local"}
|
| 72 |
-
|
| 73 |
-
_LEAKAGE = re.compile(
|
| 74 |
-
r"we need to\b|the prompt says\b|as instructed\b|my instructions\b"
|
| 75 |
-
r"|i am supposed to\b|the rules say\b",
|
| 76 |
-
re.IGNORECASE,
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def post(path: str, body: dict, timeout: int = 120) -> dict:
|
| 81 |
-
resp = requests.post(f"{BASE_URL}{path}", json=body, timeout=timeout)
|
| 82 |
-
resp.raise_for_status()
|
| 83 |
-
return resp.json()
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def check(label: str, condition: bool, detail: str = "") -> None:
|
| 87 |
-
marker = "PASS" if condition else "FAIL"
|
| 88 |
-
suffix = f" — {detail}" if detail else ""
|
| 89 |
-
print(f" {marker} {label}{suffix}")
|
| 90 |
-
if not condition:
|
| 91 |
-
sys.exit(1)
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def main() -> None:
|
| 95 |
-
print(f"\nPhase 5D Scorecard Test (Hybrid Claim-Based)")
|
| 96 |
-
print(f"Base URL : {BASE_URL}")
|
| 97 |
-
print(f"Strict Nemotron : {STRICT_COACHING}")
|
| 98 |
-
print()
|
| 99 |
-
|
| 100 |
-
# -------------------------------------------------------------------------
|
| 101 |
-
# Step 0: Start session
|
| 102 |
-
# -------------------------------------------------------------------------
|
| 103 |
-
print("Step 0: POST /api/start-session")
|
| 104 |
-
try:
|
| 105 |
-
start = post("/api/start-session", {
|
| 106 |
-
"mode": "pitch_battle",
|
| 107 |
-
"startup": SAMPLE_STARTUP,
|
| 108 |
-
"persona": "hackathon_judge",
|
| 109 |
-
"difficulty": "high",
|
| 110 |
-
"input_mode": "text",
|
| 111 |
-
"model_mode": "premium_nvidia",
|
| 112 |
-
})
|
| 113 |
-
except Exception as exc:
|
| 114 |
-
print(f" FAIL Could not start session: {exc}")
|
| 115 |
-
sys.exit(1)
|
| 116 |
-
|
| 117 |
-
session_id = start.get("session_id")
|
| 118 |
-
check("session_id present", bool(session_id))
|
| 119 |
-
check("ai_message non-empty", bool(start.get("ai_message")))
|
| 120 |
-
print(f" session_id : {session_id}")
|
| 121 |
-
print(f" attack_tag : {start.get('attack_tag')}")
|
| 122 |
-
print(f" ai_message : {start.get('ai_message', '')[:100]}...\n")
|
| 123 |
-
|
| 124 |
-
# -------------------------------------------------------------------------
|
| 125 |
-
# Steps 1-N: Send answers
|
| 126 |
-
# -------------------------------------------------------------------------
|
| 127 |
-
for i, answer in enumerate(ANSWERS):
|
| 128 |
-
print(f"Step {i + 1}: POST /api/chat-round answer={answer[:60]!r}")
|
| 129 |
-
try:
|
| 130 |
-
data = post("/api/chat-round", {
|
| 131 |
-
"session_id": session_id,
|
| 132 |
-
"user_message": answer,
|
| 133 |
-
})
|
| 134 |
-
except Exception as exc:
|
| 135 |
-
print(f" FAIL chat-round failed: {exc}")
|
| 136 |
-
sys.exit(1)
|
| 137 |
-
|
| 138 |
-
check("session_id echoed", data.get("session_id") == session_id)
|
| 139 |
-
check("ai_message non-empty", bool(data.get("ai_message")))
|
| 140 |
-
print(
|
| 141 |
-
f" round={data.get('round')} "
|
| 142 |
-
f"quality={data.get('answer_quality')} "
|
| 143 |
-
f"action={data.get('judge_action')} "
|
| 144 |
-
f"tag={data.get('attack_tag')}\n"
|
| 145 |
-
)
|
| 146 |
-
|
| 147 |
-
# -------------------------------------------------------------------------
|
| 148 |
-
# Final: POST /api/end-battle
|
| 149 |
-
# -------------------------------------------------------------------------
|
| 150 |
-
print("Final step: POST /api/end-battle (local scoring is fast; Nemotron coaching may take ~60s)")
|
| 151 |
-
try:
|
| 152 |
-
sc = post("/api/end-battle", {"session_id": session_id}, timeout=180)
|
| 153 |
-
except Exception as exc:
|
| 154 |
-
print(f" FAIL end-battle request failed: {exc}")
|
| 155 |
-
sys.exit(1)
|
| 156 |
-
|
| 157 |
-
# --- Top-level required fields ---
|
| 158 |
-
check("overall present", "overall" in sc, f"keys={list(sc.keys())}")
|
| 159 |
-
check("overall_label present", "overall_label" in sc)
|
| 160 |
-
check("scores present", "scores" in sc)
|
| 161 |
-
check("best_answer present", "best_answer" in sc)
|
| 162 |
-
check("weakest_answer present", "weakest_answer" in sc)
|
| 163 |
-
check("improved_answer present", "improved_answer" in sc)
|
| 164 |
-
check("improved_pitch present", "improved_pitch" in sc)
|
| 165 |
-
check("top_3_questions present", "top_3_questions" in sc)
|
| 166 |
-
check("model_ok present", "model_ok" in sc)
|
| 167 |
-
check("provider present", "provider" in sc)
|
| 168 |
-
check("scorecard_source present", "scorecard_source" in sc)
|
| 169 |
-
check("no error key", "error" not in sc, f"error={sc.get('error')}")
|
| 170 |
-
|
| 171 |
-
overall = sc.get("overall", -1)
|
| 172 |
-
check("overall is int 0-100", isinstance(overall, int) and 0 <= overall <= 100, f"got {overall!r}")
|
| 173 |
-
check(
|
| 174 |
-
"overall_label valid",
|
| 175 |
-
sc.get("overall_label") in _VALID_LABELS,
|
| 176 |
-
f"got {sc.get('overall_label')!r}",
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
-
# --- concrete_signals_summary ---
|
| 180 |
-
css = sc.get("concrete_signals_summary")
|
| 181 |
-
check("concrete_signals_summary present", isinstance(css, dict), f"got {type(css).__name__}")
|
| 182 |
-
if isinstance(css, dict):
|
| 183 |
-
for key in ("numbers", "validation", "competitors", "revenue_signals", "technical_mechanisms"):
|
| 184 |
-
check(
|
| 185 |
-
f"concrete_signals_summary.{key} is list",
|
| 186 |
-
isinstance(css.get(key), list),
|
| 187 |
-
f"got {type(css.get(key)).__name__}",
|
| 188 |
-
)
|
| 189 |
-
|
| 190 |
-
# --- Dimension checks ---
|
| 191 |
-
scores = sc.get("scores", {})
|
| 192 |
-
check("all 6 dimensions present", REQUIRED_DIMS <= set(scores.keys()),
|
| 193 |
-
f"missing={REQUIRED_DIMS - set(scores.keys())}")
|
| 194 |
-
|
| 195 |
-
for dim in REQUIRED_DIMS:
|
| 196 |
-
dim_data = scores.get(dim, {})
|
| 197 |
-
dim_score = dim_data.get("score", -1)
|
| 198 |
-
check(
|
| 199 |
-
f"{dim}.score is int 0-100",
|
| 200 |
-
isinstance(dim_score, int) and 0 <= dim_score <= 100,
|
| 201 |
-
f"got {dim_score!r}",
|
| 202 |
-
)
|
| 203 |
-
check(f"{dim}.reason non-empty", bool(dim_data.get("reason")))
|
| 204 |
-
check(
|
| 205 |
-
f"{dim}.label is valid",
|
| 206 |
-
dim_data.get("label") in _VALID_LABELS,
|
| 207 |
-
f"got {dim_data.get('label')!r}",
|
| 208 |
-
)
|
| 209 |
-
check(
|
| 210 |
-
f"{dim}.signals_used is list",
|
| 211 |
-
isinstance(dim_data.get("signals_used", []), list),
|
| 212 |
-
)
|
| 213 |
-
|
| 214 |
-
# --- top_3_questions ---
|
| 215 |
-
top3 = sc.get("top_3_questions", [])
|
| 216 |
-
check("top_3_questions is list", isinstance(top3, list))
|
| 217 |
-
check("top_3_questions has exactly 3", len(top3) == 3, f"got {len(top3)}")
|
| 218 |
-
for q in top3:
|
| 219 |
-
check("question is non-empty string", isinstance(q, str) and bool(q.strip()))
|
| 220 |
-
|
| 221 |
-
# --- Source and provider checks (Phase 5D: hybrid architecture) ---
|
| 222 |
-
source = sc.get("scorecard_source", "")
|
| 223 |
-
model_ok = sc.get("model_ok")
|
| 224 |
-
provider = sc.get("provider", "")
|
| 225 |
-
|
| 226 |
-
check(
|
| 227 |
-
"scorecard_source is hybrid",
|
| 228 |
-
source in _VALID_SOURCES,
|
| 229 |
-
f"got {source!r} — expected one of {_VALID_SOURCES}",
|
| 230 |
-
)
|
| 231 |
-
check(
|
| 232 |
-
"provider is local or local+nvidia",
|
| 233 |
-
provider in _VALID_PROVIDERS,
|
| 234 |
-
f"got {provider!r}",
|
| 235 |
-
)
|
| 236 |
-
check("model_ok is bool", isinstance(model_ok, bool), f"got {type(model_ok).__name__}")
|
| 237 |
-
|
| 238 |
-
if STRICT_COACHING and source != "hybrid_claims_nemotron":
|
| 239 |
-
print(
|
| 240 |
-
f"\n FAIL STRICT_NEMOTRON_COACHING=true but source={source!r}\n"
|
| 241 |
-
f" model_error: {sc.get('model_error', 'none')}\n"
|
| 242 |
-
" Nemotron coaching did not produce valid JSON."
|
| 243 |
-
)
|
| 244 |
-
sys.exit(1)
|
| 245 |
-
elif source == "hybrid_claims_local":
|
| 246 |
-
print(f" NOTE: hybrid_claims_local — Nemotron coaching unavailable, local fallback used.")
|
| 247 |
-
print(f" model_error: {sc.get('model_error', 'none')}")
|
| 248 |
-
else:
|
| 249 |
-
print(f" NOTE: hybrid_claims_nemotron — Nemotron coaching succeeded.")
|
| 250 |
-
|
| 251 |
-
# --- Evidence detection: "50 beta users" should appear in signals/scores ---
|
| 252 |
-
evidence_terms = ["50 beta", "campus ambassador", "event-miss report", "validated", "beta users"]
|
| 253 |
-
evidence_fields = [
|
| 254 |
-
str(sc.get("best_answer", "")),
|
| 255 |
-
str(sc.get("improved_answer", "")),
|
| 256 |
-
str(sc.get("improved_pitch", "")),
|
| 257 |
-
]
|
| 258 |
-
for dim in REQUIRED_DIMS:
|
| 259 |
-
evidence_fields.append(str(scores.get(dim, {}).get("reason", "")))
|
| 260 |
-
for sig in scores.get(dim, {}).get("signals_used", []):
|
| 261 |
-
evidence_fields.append(str(sig))
|
| 262 |
-
if isinstance(css, dict):
|
| 263 |
-
for key in ("numbers", "validation"):
|
| 264 |
-
evidence_fields.extend(str(x) for x in css.get(key, []))
|
| 265 |
-
combined = " ".join(evidence_fields).lower()
|
| 266 |
-
evidence_found = any(t.lower() in combined for t in evidence_terms)
|
| 267 |
-
if evidence_found:
|
| 268 |
-
print(" PASS Scorecard acknowledges concrete validation evidence from STRONG_ANSWER")
|
| 269 |
-
else:
|
| 270 |
-
print(" NOTE Scorecard did not explicitly reference validation evidence (not a hard fail in 5D)")
|
| 271 |
-
|
| 272 |
-
# --- No static EventRadar mock content ---
|
| 273 |
-
all_text = " ".join([
|
| 274 |
-
str(sc.get("best_answer", "")),
|
| 275 |
-
str(sc.get("weakest_answer", "")),
|
| 276 |
-
str(sc.get("improved_answer", "")),
|
| 277 |
-
str(sc.get("improved_pitch", "")),
|
| 278 |
-
])
|
| 279 |
-
check(
|
| 280 |
-
"no static WhatsApp groups mock content",
|
| 281 |
-
"WhatsApp groups" not in all_text or "EventRadar" in SAMPLE_STARTUP.get("name", ""),
|
| 282 |
-
)
|
| 283 |
-
|
| 284 |
-
# --- Leakage check ---
|
| 285 |
-
scorecard_text = " ".join([
|
| 286 |
-
str(sc.get("best_answer", "")),
|
| 287 |
-
str(sc.get("weakest_answer", "")),
|
| 288 |
-
str(sc.get("improved_answer", "")),
|
| 289 |
-
str(sc.get("improved_pitch", "")),
|
| 290 |
-
str(sc.get("why_weak", "")),
|
| 291 |
-
])
|
| 292 |
-
check("no leakage in scorecard text", not _LEAKAGE.search(scorecard_text), "instruction leakage detected")
|
| 293 |
-
|
| 294 |
-
# -------------------------------------------------------------------------
|
| 295 |
-
# Summary
|
| 296 |
-
# -------------------------------------------------------------------------
|
| 297 |
-
print("\n--- Scorecard Summary ---")
|
| 298 |
-
print(f" scorecard_source : {sc.get('scorecard_source')}")
|
| 299 |
-
print(f" model_ok : {sc.get('model_ok')} provider: {sc.get('provider')}")
|
| 300 |
-
print(f" overall : {overall} ({sc.get('overall_label')})")
|
| 301 |
-
print()
|
| 302 |
-
for dim in REQUIRED_DIMS:
|
| 303 |
-
d = scores.get(dim, {})
|
| 304 |
-
sigs = d.get("signals_used", [])
|
| 305 |
-
print(
|
| 306 |
-
f" {dim:<25} score={d.get('score', '?'):>3} [{d.get('label', '?')}]"
|
| 307 |
-
+ (f" sigs={sigs[:2]}" if sigs else "")
|
| 308 |
-
)
|
| 309 |
-
print()
|
| 310 |
-
print(f" weakest_answer : {sc.get('weakest_answer', '')[:120]}")
|
| 311 |
-
print(f" improved_answer : {sc.get('improved_answer', '')[:120]}")
|
| 312 |
-
print(f" top_3_questions :")
|
| 313 |
-
for q in top3:
|
| 314 |
-
print(f" - {q}")
|
| 315 |
-
if isinstance(css, dict):
|
| 316 |
-
print(f"\n concrete_signals_summary:")
|
| 317 |
-
for k, v in css.items():
|
| 318 |
-
if v:
|
| 319 |
-
print(f" {k}: {v[:3]}")
|
| 320 |
-
|
| 321 |
-
if sc.get("model_error"):
|
| 322 |
-
print(f"\n model_error : {sc.get('model_error')}")
|
| 323 |
-
|
| 324 |
-
print("\nAll Phase 5D scorecard checks passed.\n")
|
| 325 |
-
sys.exit(0)
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
if __name__ == "__main__":
|
| 329 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test_phase5b_refinement.py
DELETED
|
@@ -1,240 +0,0 @@
|
|
| 1 |
-
"""Phase 5B/5D refinement test: 8+ rounds, soft limit, battle_phase, labels, no leakage.
|
| 2 |
-
|
| 3 |
-
Validates:
|
| 4 |
-
- User can continue chatting past MAX_ROUNDS (no hard block)
|
| 5 |
-
- soft_round_limit_reached appears at round >= MAX_ROUNDS
|
| 6 |
-
- battle_phase progresses: explore (1-3), pressure (4-6), close (7+)
|
| 7 |
-
- No instruction leakage in any ai_message
|
| 8 |
-
- /api/end-battle returns hybrid claim-based scorecard with score labels
|
| 9 |
-
- overall_label and per-dimension labels present
|
| 10 |
-
- scorecard_source in ("hybrid_claims_nemotron", "hybrid_claims_local")
|
| 11 |
-
|
| 12 |
-
Usage:
|
| 13 |
-
python scripts/test_phase5b_refinement.py
|
| 14 |
-
ALLOW_SCORECARD_FALLBACK=true python scripts/test_phase5b_refinement.py
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
from __future__ import annotations
|
| 18 |
-
|
| 19 |
-
import os
|
| 20 |
-
import re
|
| 21 |
-
import sys
|
| 22 |
-
|
| 23 |
-
import requests
|
| 24 |
-
|
| 25 |
-
BASE_URL = os.getenv("PITCHFIGHT_BASE_URL", "http://127.0.0.1:7861").rstrip("/")
|
| 26 |
-
ALLOW_FALLBACK = os.getenv("ALLOW_SCORECARD_FALLBACK", "false").strip().lower() == "true"
|
| 27 |
-
MAX_ROUNDS = int(os.getenv("MAX_ROUNDS", "6"))
|
| 28 |
-
|
| 29 |
-
SAMPLE_STARTUP = {
|
| 30 |
-
"name": "EventRadar AI",
|
| 31 |
-
"problem": "Students miss relevant hackathons, workshops, and networking events.",
|
| 32 |
-
"solution": "AI-powered event discovery that ranks events by fit for each student profile.",
|
| 33 |
-
"why_ai": "Personalized ranking requires understanding student goals and event signals together.",
|
| 34 |
-
"stage": "Prototype",
|
| 35 |
-
"team": "2 founders",
|
| 36 |
-
"traction": "50 beta signups, no revenue yet",
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
STRONG_ANSWER = (
|
| 40 |
-
"We validated this with 50 beta users, 3 campus ambassadors, "
|
| 41 |
-
"and weekly event-miss reports from two colleges."
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
# 9 answers to go well past MAX_ROUNDS=6
|
| 45 |
-
ANSWERS = [
|
| 46 |
-
"It is a pretty big market because students attend events often.", # weak
|
| 47 |
-
"I don't know", # non_answer
|
| 48 |
-
STRONG_ANSWER, # strong
|
| 49 |
-
"The AI ranks events based on student interest profiles.", # partial
|
| 50 |
-
"We use embeddings and a ranking model trained on student behavior data.", # strong
|
| 51 |
-
"okay", # non_answer
|
| 52 |
-
"Our differentiation is the personalization layer no event aggregator has.",# partial
|
| 53 |
-
"We plan to charge colleges $500/month for analytics and promotions.", # partial (revenue)
|
| 54 |
-
"We have 3 campus ambassadors onboarding 20 students each right now.", # strong
|
| 55 |
-
]
|
| 56 |
-
|
| 57 |
-
REQUIRED_DIMS = {
|
| 58 |
-
"clarity", "problem_understanding", "market_awareness",
|
| 59 |
-
"differentiation", "business_model", "objection_handling",
|
| 60 |
-
}
|
| 61 |
-
_VALID_LABELS = {"Not addressed", "Developing", "Solid", "Strong", "Excellent"}
|
| 62 |
-
|
| 63 |
-
_LEAKAGE = re.compile(
|
| 64 |
-
r"we need to\b|the prompt says\b|as instructed\b|my instructions\b"
|
| 65 |
-
r"|i am supposed to\b|i should follow\b|the rules say\b|per the instructions?\b",
|
| 66 |
-
re.IGNORECASE,
|
| 67 |
-
)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def post(path: str, body: dict, timeout: int = 180) -> dict:
|
| 71 |
-
resp = requests.post(f"{BASE_URL}{path}", json=body, timeout=timeout)
|
| 72 |
-
resp.raise_for_status()
|
| 73 |
-
return resp.json()
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def check(label: str, condition: bool, detail: str = "") -> None:
|
| 77 |
-
marker = "PASS" if condition else "FAIL"
|
| 78 |
-
suffix = f" — {detail}" if detail else ""
|
| 79 |
-
print(f" {marker} {label}{suffix}")
|
| 80 |
-
if not condition:
|
| 81 |
-
sys.exit(1)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def main() -> None:
|
| 85 |
-
print(f"\nPhase 5B Refinement Test")
|
| 86 |
-
print(f"Base URL : {BASE_URL}")
|
| 87 |
-
print(f"MAX_ROUNDS : {MAX_ROUNDS}")
|
| 88 |
-
print(f"Allow fallback : {ALLOW_FALLBACK}")
|
| 89 |
-
print(f"Total answers : {len(ANSWERS)} (well past MAX_ROUNDS)\n")
|
| 90 |
-
|
| 91 |
-
# -------------------------------------------------------------------------
|
| 92 |
-
# Step 0: Start session
|
| 93 |
-
# -------------------------------------------------------------------------
|
| 94 |
-
print("Step 0: POST /api/start-session")
|
| 95 |
-
try:
|
| 96 |
-
start = post("/api/start-session", {
|
| 97 |
-
"mode": "pitch_battle",
|
| 98 |
-
"startup": SAMPLE_STARTUP,
|
| 99 |
-
"persona": "hackathon_judge",
|
| 100 |
-
"difficulty": "high",
|
| 101 |
-
"input_mode": "text",
|
| 102 |
-
"model_mode": "premium_nvidia",
|
| 103 |
-
})
|
| 104 |
-
except Exception as exc:
|
| 105 |
-
print(f" FAIL Could not start session: {exc}")
|
| 106 |
-
sys.exit(1)
|
| 107 |
-
|
| 108 |
-
session_id = start.get("session_id")
|
| 109 |
-
check("session_id present", bool(session_id))
|
| 110 |
-
check("ai_message non-empty", bool(start.get("ai_message")))
|
| 111 |
-
check("battle_complete=False at start", start.get("battle_complete") is False)
|
| 112 |
-
check("can_continue=True at start", start.get("can_continue") is True)
|
| 113 |
-
check("battle_phase present", bool(start.get("battle_phase")))
|
| 114 |
-
check("start battle_phase=explore", start.get("battle_phase") == "explore",
|
| 115 |
-
f"got {start.get('battle_phase')!r}")
|
| 116 |
-
check("no leakage in opening", not _LEAKAGE.search(start.get("ai_message", "")))
|
| 117 |
-
|
| 118 |
-
print(f" session_id : {session_id}")
|
| 119 |
-
print(f" attack_tag : {start.get('attack_tag')}")
|
| 120 |
-
print(f" battle_phase : {start.get('battle_phase')}")
|
| 121 |
-
print(f" ai_message : {start.get('ai_message', '')[:100]}...\n")
|
| 122 |
-
|
| 123 |
-
# -------------------------------------------------------------------------
|
| 124 |
-
# Steps 1..len(ANSWERS): send all answers, including past MAX_ROUNDS
|
| 125 |
-
# -------------------------------------------------------------------------
|
| 126 |
-
soft_limit_seen = False
|
| 127 |
-
|
| 128 |
-
for i, answer in enumerate(ANSWERS):
|
| 129 |
-
print(f"Step {i + 1}: POST /api/chat-round answer={answer[:60]!r}")
|
| 130 |
-
try:
|
| 131 |
-
data = post("/api/chat-round", {"session_id": session_id, "user_message": answer})
|
| 132 |
-
except Exception as exc:
|
| 133 |
-
print(f" FAIL chat-round failed: {exc}")
|
| 134 |
-
sys.exit(1)
|
| 135 |
-
|
| 136 |
-
rnd = data.get("round", 0)
|
| 137 |
-
phase = data.get("battle_phase", "")
|
| 138 |
-
sl = data.get("soft_round_limit_reached", False)
|
| 139 |
-
bc = data.get("battle_complete", True)
|
| 140 |
-
cc = data.get("can_continue", False)
|
| 141 |
-
na = data.get("next_action", "")
|
| 142 |
-
ai = data.get("ai_message", "")
|
| 143 |
-
|
| 144 |
-
print(f" round={rnd} phase={phase} soft_limit={sl} "
|
| 145 |
-
f"battle_complete={bc} can_continue={cc}")
|
| 146 |
-
print(f" ai_message: {ai[:100]}...\n")
|
| 147 |
-
|
| 148 |
-
# Always must continue
|
| 149 |
-
check("battle_complete=False (never hard-stopped)", bc is False, f"round={rnd}")
|
| 150 |
-
check("can_continue=True (always)", cc is True, f"round={rnd}")
|
| 151 |
-
check("next_action=continue (always)", na == "continue", f"got {na!r}")
|
| 152 |
-
check("ai_message non-empty", bool(ai))
|
| 153 |
-
check("no leakage in ai_message", not _LEAKAGE.search(ai),
|
| 154 |
-
f"leakage at round {rnd}")
|
| 155 |
-
|
| 156 |
-
# Soft limit
|
| 157 |
-
if rnd >= MAX_ROUNDS:
|
| 158 |
-
check("soft_round_limit_reached=True", sl is True, f"round={rnd}")
|
| 159 |
-
if sl:
|
| 160 |
-
soft_limit_seen = True
|
| 161 |
-
else:
|
| 162 |
-
check("soft_round_limit_reached=False", sl is False, f"round={rnd}")
|
| 163 |
-
|
| 164 |
-
# Battle phase progression
|
| 165 |
-
if 1 <= rnd <= 3:
|
| 166 |
-
check(f"phase=explore at round {rnd}", phase == "explore", f"got {phase!r}")
|
| 167 |
-
elif 4 <= rnd <= 6:
|
| 168 |
-
check(f"phase=pressure at round {rnd}", phase == "pressure", f"got {phase!r}")
|
| 169 |
-
elif rnd >= 7:
|
| 170 |
-
check(f"phase=close at round {rnd}", phase == "close", f"got {phase!r}")
|
| 171 |
-
|
| 172 |
-
check("soft_round_limit_reached appeared at least once", soft_limit_seen)
|
| 173 |
-
|
| 174 |
-
# -------------------------------------------------------------------------
|
| 175 |
-
# Final: POST /api/end-battle
|
| 176 |
-
# -------------------------------------------------------------------------
|
| 177 |
-
print(f"\nFinal: POST /api/end-battle (may take up to 3 minutes)")
|
| 178 |
-
try:
|
| 179 |
-
sc = post("/api/end-battle", {"session_id": session_id}, timeout=240)
|
| 180 |
-
except Exception as exc:
|
| 181 |
-
print(f" FAIL end-battle failed: {exc}")
|
| 182 |
-
sys.exit(1)
|
| 183 |
-
|
| 184 |
-
# Top-level fields
|
| 185 |
-
check("overall present", "overall" in sc)
|
| 186 |
-
check("overall_label present", "overall_label" in sc)
|
| 187 |
-
check("scores present", "scores" in sc)
|
| 188 |
-
check("scorecard_source present", "scorecard_source" in sc)
|
| 189 |
-
check("model_ok present", "model_ok" in sc)
|
| 190 |
-
|
| 191 |
-
overall = sc.get("overall", -1)
|
| 192 |
-
check("overall 0-100", isinstance(overall, int) and 0 <= overall <= 100, f"got {overall!r}")
|
| 193 |
-
check(
|
| 194 |
-
"overall_label valid",
|
| 195 |
-
sc.get("overall_label") in _VALID_LABELS,
|
| 196 |
-
f"got {sc.get('overall_label')!r}",
|
| 197 |
-
)
|
| 198 |
-
|
| 199 |
-
# Dimension labels
|
| 200 |
-
scores = sc.get("scores", {})
|
| 201 |
-
check("all 6 dims present", REQUIRED_DIMS <= set(scores.keys()))
|
| 202 |
-
for dim in REQUIRED_DIMS:
|
| 203 |
-
d = scores.get(dim, {})
|
| 204 |
-
check(f"{dim}.label valid", d.get("label") in _VALID_LABELS, f"got {d.get('label')!r}")
|
| 205 |
-
dim_score = d.get("score", -1)
|
| 206 |
-
check(f"{dim}.score 0-100", isinstance(dim_score, int) and 0 <= dim_score <= 100)
|
| 207 |
-
|
| 208 |
-
# Scorecard source check (Phase 5D: hybrid architecture)
|
| 209 |
-
source = sc.get("scorecard_source", "")
|
| 210 |
-
_hybrid_sources = {"hybrid_claims_nemotron", "hybrid_claims_local"}
|
| 211 |
-
check(
|
| 212 |
-
"scorecard_source is hybrid",
|
| 213 |
-
source in _hybrid_sources,
|
| 214 |
-
f"got {source!r} — expected one of {_hybrid_sources}",
|
| 215 |
-
)
|
| 216 |
-
check(
|
| 217 |
-
"provider is local or local+nvidia",
|
| 218 |
-
sc.get("provider") in {"local+nvidia", "local"},
|
| 219 |
-
f"got {sc.get('provider')!r}",
|
| 220 |
-
)
|
| 221 |
-
if source == "hybrid_claims_local":
|
| 222 |
-
print(f" NOTE: hybrid_claims_local — Nemotron coaching unavailable, local fallback used.")
|
| 223 |
-
if sc.get("model_error"):
|
| 224 |
-
print(f" model_error: {sc.get('model_error')}")
|
| 225 |
-
else:
|
| 226 |
-
print(" NOTE: hybrid_claims_nemotron — Nemotron coaching succeeded.")
|
| 227 |
-
|
| 228 |
-
print(f"\n--- Refinement Summary ---")
|
| 229 |
-
print(f" scorecard_source : {sc.get('scorecard_source')}")
|
| 230 |
-
print(f" overall : {overall} ({sc.get('overall_label')})")
|
| 231 |
-
for dim in REQUIRED_DIMS:
|
| 232 |
-
d = scores.get(dim, {})
|
| 233 |
-
print(f" {dim:<25} {d.get('score', '?'):>3} [{d.get('label', '?')}]")
|
| 234 |
-
|
| 235 |
-
print("\nAll Phase 5B refinement checks passed.\n")
|
| 236 |
-
sys.exit(0)
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
if __name__ == "__main__":
|
| 240 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_api_handlers.py
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
"""Tests for shared API handlers."""
|
| 2 |
-
|
| 3 |
-
import unittest
|
| 4 |
-
|
| 5 |
-
from core.api_handlers import (
|
| 6 |
-
handle_chat_round,
|
| 7 |
-
handle_end_battle,
|
| 8 |
-
handle_load_sample,
|
| 9 |
-
handle_reset_session,
|
| 10 |
-
handle_start_session,
|
| 11 |
-
handle_voice_pitch_placeholder,
|
| 12 |
-
)
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class TestApiHandlers(unittest.TestCase):
|
| 16 |
-
def test_load_sample(self):
|
| 17 |
-
result = handle_load_sample()
|
| 18 |
-
self.assertIn("startup", result)
|
| 19 |
-
self.assertEqual(result["startup"]["name"], "EventRadar AI")
|
| 20 |
-
|
| 21 |
-
def test_full_battle_flow(self):
|
| 22 |
-
start = handle_start_session(
|
| 23 |
-
{
|
| 24 |
-
"persona": "hackathon_judge",
|
| 25 |
-
"difficulty": "high",
|
| 26 |
-
"input_mode": "text",
|
| 27 |
-
"startup": handle_load_sample()["startup"],
|
| 28 |
-
}
|
| 29 |
-
)
|
| 30 |
-
session_id = start["session_id"]
|
| 31 |
-
self.assertEqual(start["round"], 1)
|
| 32 |
-
|
| 33 |
-
chat = handle_chat_round(
|
| 34 |
-
{"session_id": session_id, "user_message": "We rank events with AI."}
|
| 35 |
-
)
|
| 36 |
-
self.assertEqual(chat["session_id"], session_id)
|
| 37 |
-
self.assertEqual(chat["round"], 2)
|
| 38 |
-
|
| 39 |
-
scorecard = handle_end_battle({"session_id": session_id})
|
| 40 |
-
self.assertIn("overall", scorecard)
|
| 41 |
-
self.assertIn("scores", scorecard)
|
| 42 |
-
|
| 43 |
-
reset = handle_reset_session({"session_id": session_id})
|
| 44 |
-
self.assertEqual(reset["status"], "reset")
|
| 45 |
-
|
| 46 |
-
def test_voice_placeholder(self):
|
| 47 |
-
result = handle_voice_pitch_placeholder()
|
| 48 |
-
self.assertEqual(result["status"], "not_implemented")
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
if __name__ == "__main__":
|
| 52 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_json_parser.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
"""Tests for JSON parsing utilities."""
|
| 2 |
-
|
| 3 |
-
import json
|
| 4 |
-
import unittest
|
| 5 |
-
|
| 6 |
-
from core.json_utils import extract_json_block, fallback_scorecard, safe_json_parse
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class TestJsonParser(unittest.TestCase):
|
| 10 |
-
def test_safe_json_parse_raw_object(self):
|
| 11 |
-
payload = {"overall": 70, "scores": {}}
|
| 12 |
-
result = safe_json_parse(json.dumps(payload))
|
| 13 |
-
self.assertEqual(result["overall"], 70)
|
| 14 |
-
|
| 15 |
-
def test_safe_json_parse_fenced_block(self):
|
| 16 |
-
text = 'Here is the score:\n```json\n{"overall": 55}\n```'
|
| 17 |
-
result = safe_json_parse(text)
|
| 18 |
-
self.assertEqual(result["overall"], 55)
|
| 19 |
-
|
| 20 |
-
def test_safe_json_parse_invalid_returns_default(self):
|
| 21 |
-
result = safe_json_parse("not json at all", default={"ok": False})
|
| 22 |
-
self.assertEqual(result, {"ok": False})
|
| 23 |
-
|
| 24 |
-
def test_extract_json_block_from_text(self):
|
| 25 |
-
text = 'Analysis complete {"round": 2, "tag": "Moat"} thanks.'
|
| 26 |
-
block = extract_json_block(text)
|
| 27 |
-
self.assertIsNotNone(block)
|
| 28 |
-
self.assertEqual(json.loads(block)["tag"], "Moat")
|
| 29 |
-
|
| 30 |
-
def test_fallback_scorecard_shape(self):
|
| 31 |
-
card = fallback_scorecard()
|
| 32 |
-
self.assertIn("overall", card)
|
| 33 |
-
self.assertIn("scores", card)
|
| 34 |
-
self.assertIn("top_3_questions", card)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
if __name__ == "__main__":
|
| 38 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_prompts.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
"""Tests for persona prompt builder."""
|
| 2 |
-
|
| 3 |
-
import unittest
|
| 4 |
-
|
| 5 |
-
from core.persona_builder import build_persona_prompt
|
| 6 |
-
from core.samples import get_sample_startup
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class TestPrompts(unittest.TestCase):
|
| 10 |
-
def test_persona_prompt_not_empty(self):
|
| 11 |
-
startup = get_sample_startup()
|
| 12 |
-
prompt = build_persona_prompt("hackathon_judge", startup, difficulty="high")
|
| 13 |
-
self.assertTrue(prompt.strip())
|
| 14 |
-
|
| 15 |
-
def test_persona_prompt_includes_startup_name(self):
|
| 16 |
-
startup = get_sample_startup()
|
| 17 |
-
prompt = build_persona_prompt("technical_judge", startup)
|
| 18 |
-
self.assertIn("EventRadar AI", prompt)
|
| 19 |
-
|
| 20 |
-
def test_all_personas_build(self):
|
| 21 |
-
startup = get_sample_startup()
|
| 22 |
-
for persona in ("skeptical_vc", "technical_judge", "hackathon_judge"):
|
| 23 |
-
prompt = build_persona_prompt(persona, startup)
|
| 24 |
-
self.assertIn("Ask one sharp question", prompt)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
if __name__ == "__main__":
|
| 28 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|