iitmbs24f commited on
Commit
3010238
·
verified ·
1 Parent(s): 2945c4d

Upload 15 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.so
7
+ *.egg
8
+ *.egg-info
9
+ dist
10
+ build
11
+ .git
12
+ .gitignore
13
+ .env
14
+ .venv
15
+ venv/
16
+ ENV/
17
+ *.log
18
+ .DS_Store
19
+ .vscode
20
+ .idea
21
+ *.swp
22
+ *.swo
23
+ *~
24
+
.gitignore ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ ENV/
26
+ env/
27
+ .venv
28
+
29
+ # IDE
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # Environment variables
37
+ .env
38
+ .env.local
39
+
40
+ # Logs
41
+ *.log
42
+ logs/
43
+
44
+ # OS
45
+ .DS_Store
46
+ Thumbs.db
47
+
48
+ # Playwright
49
+ .playwright/
50
+ ms-playwright/
51
+
52
+ # Testing
53
+ .pytest_cache/
54
+ .coverage
55
+ htmlcov/
56
+
57
+ # Jupyter
58
+ .ipynb_checkpoints
59
+
60
+ # Project specific
61
+ *.pdf
62
+ *.csv
63
+ *.xlsx
64
+ temp/
65
+ tmp/
66
+
Dockerfile ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies (for Playwright/Chromium)
7
+ RUN apt-get update && apt-get install -y \
8
+ wget \
9
+ gnupg \
10
+ ca-certificates \
11
+ fonts-liberation \
12
+ libasound2 \
13
+ libatk-bridge2.0-0 \
14
+ libatk1.0-0 \
15
+ libatspi2.0-0 \
16
+ libcups2 \
17
+ libdbus-1-3 \
18
+ libdrm2 \
19
+ libgbm1 \
20
+ libgtk-3-0 \
21
+ libnspr4 \
22
+ libnss3 \
23
+ libxcomposite1 \
24
+ libxdamage1 \
25
+ libxfixes3 \
26
+ libxkbcommon0 \
27
+ libxrandr2 \
28
+ xdg-utils \
29
+ && rm -rf /var/lib/apt/lists/*
30
+
31
+ # Copy requirements first for better caching
32
+ COPY requirements.txt ./requirements.txt
33
+
34
+ # Install Python dependencies
35
+ RUN pip install --no-cache-dir -r requirements.txt
36
+
37
+ # Install Playwright browsers and dependencies
38
+ RUN python -m playwright install chromium
39
+
40
+ # Copy the app directory first to preserve structure
41
+ COPY app ./app
42
+
43
+ # Copy other necessary files (optional, for documentation)
44
+ COPY *.md LICENSE ./
45
+
46
+ # Verify app directory structure and Python can import it
47
+ RUN ls -la /app/app && python -c "import sys; sys.path.insert(0, '/app'); import app.main; print('✓ App module imported successfully')"
48
+
49
+ # Set environment variables
50
+ ENV PYTHONUNBUFFERED=1
51
+ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
52
+ ENV PYTHONPATH=/app
53
+
54
+ # Expose port (use PORT env var, default to 7860 for HF Spaces)
55
+ EXPOSE 7860
56
+
57
+ # Health check (use PORT env var, default to 7860)
58
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
59
+ CMD python -c "import os; import requests; port = os.getenv('PORT', '7860'); requests.get(f'http://localhost:{port}/health')"
60
+
61
+ # Run the application (PORT env var will be used by main.py)
62
+ CMD PYTHONPATH=/app python -m uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}
LICENSE ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 IITM LLM Quiz Solver
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
README.md CHANGED
@@ -1,11 +1,275 @@
1
- ---
2
- title: Prj2.1
3
- emoji: 🏆
4
- colorFrom: yellow
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # IITM LLM Quiz Solver
2
+ title: IITM LLM Quiz Solver
3
+ emoji: 🧠
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ sdk_version: "0"
8
+ app_file: app/main.py
9
+ pinned: false
10
+ ---
11
+
12
+
13
+ A complete Python project with FastAPI that acts as an API endpoint to automatically solve dynamic quiz tasks using a headless browser and optional LLM reasoning.
14
+
15
+ ## Features
16
+
17
+ - 🚀 FastAPI-based REST API
18
+ - 🌐 Playwright for headless browser automation
19
+ - 🤖 OpenAI GPT integration for complex reasoning
20
+ - 📊 Data processing (CSV, JSON, PDF, etc.)
21
+ - 🔄 Recursive quiz solving
22
+ - ⚡ Async/await for performance
23
+ - 🐳 Docker support for easy deployment
24
+
25
+ ## Project Structure
26
+
27
+ ```
28
+ /app
29
+ - main.py # FastAPI server
30
+ - solver.py # Quiz solving logic
31
+ - browser.py # Playwright helper
32
+ - llm.py # GPT helper
33
+ - utils.py # Utility functions
34
+ /Dockerfile
35
+ /requirements.txt
36
+ /README.md
37
+ /LICENSE
38
+ ```
39
+
40
+ ## Installation
41
+
42
+ ### Local Development
43
+
44
+ 1. Clone the repository:
45
+ ```bash
46
+ git clone <repository-url>
47
+ cd IITMTdsPrj2
48
+ ```
49
+
50
+ 2. Install Python dependencies:
51
+ ```bash
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ 3. Install Playwright browsers:
56
+ ```bash
57
+ playwright install chromium
58
+ ```
59
+
60
+ 4. Set environment variables:
61
+
62
+ **Quick Setup (Windows PowerShell):**
63
+ ```powershell
64
+ .\setup_env.ps1
65
+ ```
66
+
67
+ **Quick Setup (Linux/Mac):**
68
+ ```bash
69
+ source setup_env.sh
70
+ ```
71
+
72
+ **Manual Setup (choose whichever LLM provider you prefer):**
73
+ ```bash
74
+ # Windows PowerShell
75
+ $env:QUIZ_SECRET = "your_secret_key"
76
+ $env:OPENAI_API_KEY = "sk-your-openai-api-key" # Optional - OpenAI
77
+ $env:OPENROUTER_API_KEY = "sk-or-your-openrouter" # Optional - OpenRouter GPT-5-nano
78
+
79
+ # Linux/Mac
80
+ export QUIZ_SECRET="your_secret_key"
81
+ export OPENAI_API_KEY="sk-your-openai-api-key" # Optional
82
+ export OPENROUTER_API_KEY="sk-or-your-openrouter" # Optional
83
+ ```
84
+
85
+ **Or use .env file:**
86
+ - Copy `.env.example` to `.env` (if available)
87
+ - Fill in your values
88
+ - The app will automatically load it
89
+
90
+ 📖 **See [ENV_SETUP.md](ENV_SETUP.md) for detailed instructions**
91
+
92
+ 5. Run the server:
93
+ ```bash
94
+ python -m app.main
95
+ # or
96
+ uvicorn app.main:app --host 0.0.0.0 --port 8000
97
+ ```
98
+
99
+ ## API Endpoints
100
+
101
+ ### POST /solve
102
+
103
+ Main endpoint to solve a quiz.
104
+
105
+ **Request Body:**
106
+ ```json
107
+ {
108
+ "email": "user@example.com",
109
+ "secret": "your_secret",
110
+ "url": "https://example.com/quiz"
111
+ }
112
+ ```
113
+
114
+ **Response:**
115
+ - `200 OK`: Quiz solved successfully
116
+ - `400 Bad Request`: Invalid request format
117
+ - `403 Forbidden`: Invalid secret
118
+ - `500 Internal Server Error`: Server error
119
+ - `504 Gateway Timeout`: Request timeout (>3 minutes)
120
+
121
+ ### POST /demo
122
+
123
+ Demo endpoint for testing (same as `/solve` but with more lenient error handling).
124
+
125
+ **Request Body:** Same as `/solve`
126
+
127
+ ### GET /health
128
+
129
+ Health check endpoint.
130
+
131
+ **Response:**
132
+ ```json
133
+ {
134
+ "status": "healthy"
135
+ }
136
+ ```
137
+
138
+ ## Deployment on Hugging Face Spaces
139
+
140
+ ### Method 1: Using Dockerfile (Recommended)
141
+
142
+ 1. **Create a new Space on Hugging Face:**
143
+ - Go to https://huggingface.co/spaces
144
+ - Create a new Space
145
+ - Select "Docker" as the SDK
146
+
147
+ 2. **Upload your files:**
148
+ - Upload all project files to your Space
149
+ - Ensure `Dockerfile` is in the root directory
150
+
151
+ 3. **Set Environment Variables:**
152
+ - Go to Space Settings → Variables and secrets
153
+ - Add the following:
154
+ - `QUIZ_SECRET`: Your secret key for authentication
155
+ - `OPENAI_API_KEY`: Your OpenAI API key (optional)
156
+ - `OPENROUTER_API_KEY`: Your OpenRouter key (e.g., GPT-5-nano)
157
+ - `PORT`: 8000 (usually set automatically)
158
+
159
+ 4. **Deploy:**
160
+ - Hugging Face will automatically build and deploy your Docker container
161
+ - The API will be available at: `https://<your-username>-<space-name>.hf.space`
162
+
163
+ ### Method 2: Using Docker Compose (Alternative)
164
+
165
+ If you need more control, you can use `docker-compose.yml`:
166
+
167
+ ```yaml
168
+ version: '3.8'
169
+ services:
170
+ app:
171
+ build: .
172
+ ports:
173
+ - "8000:8000"
174
+ environment:
175
+ - QUIZ_SECRET=${QUIZ_SECRET}
176
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
177
+ ```
178
+
179
+ ## Environment Variables
180
+
181
+ | Variable | Description | Required | Default |
182
+ |----------|-------------|----------|---------|
183
+ | `QUIZ_SECRET` | Secret key for API authentication | Yes | `default_secret_change_me` |
184
+ | `OPENAI_API_KEY` | OpenAI API key for LLM features | No | - |
185
+ | `OPENROUTER_API_KEY` | OpenRouter key (e.g., GPT-5-nano) | No | - |
186
+ | `OPENROUTER_MODEL` | Override OpenRouter model (default gpt-5-nano) | No | `gpt-5-nano` |
187
+ | `PORT` | Server port | No | `8000` |
188
+
189
+ ## Testing
190
+
191
+ ### Test with curl:
192
+
193
+ ```bash
194
+ curl -X POST "https://tds-llm-analysis.s-anand.net/demo" \
195
+ -H "Content-Type: application/json" \
196
+ -d '{
197
+ "email": "test@example.com",
198
+ "secret": "your_secret",
199
+ "url": "https://example.com/quiz"
200
+ }'
201
+ ```
202
+
203
+ ### Test with Python:
204
+
205
+ ```python
206
+ import requests
207
+
208
+ response = requests.post(
209
+ "https://tds-llm-analysis.s-anand.net/demo",
210
+ json={
211
+ "email": "test@example.com",
212
+ "secret": "your_secret",
213
+ "url": "https://example.com/quiz"
214
+ }
215
+ )
216
+
217
+ print(response.json())
218
+ ```
219
+
220
+ ## How It Works
221
+
222
+ 1. **Request Validation**: Validates email, secret, and URL format
223
+ 2. **Secret Authentication**: Checks secret against expected value (403 if wrong)
224
+ 3. **Page Loading**: Uses Playwright to load and render the quiz page
225
+ 4. **Content Extraction**: Extracts all text, HTML, links, and images
226
+ 5. **Submit URL Detection**: Automatically finds the submit URL from page content
227
+ 6. **Question Solving**:
228
+ - Extracts question text
229
+ - Tries multiple strategies:
230
+ - Check if answer is in page
231
+ - Download and process data files (CSV, JSON, PDF)
232
+ - Use LLM for complex reasoning
233
+ 7. **Answer Submission**: Submits answer to detected submit URL
234
+ 8. **Recursive Solving**: If response contains next URL, solves recursively
235
+ 9. **Response**: Returns final result
236
+
237
+ ## Solver Strategies
238
+
239
+ The solver uses multiple strategies in order:
240
+
241
+ 1. **Direct Answer Extraction**: Checks if answer is already in page
242
+ 2. **Data File Processing**: Downloads and processes CSV, JSON, PDF files
243
+ 3. **LLM Reasoning**: Uses GPT-4o-mini (OpenAI) or GPT-5-nano (OpenRouter) for complex questions
244
+ 4. **Fallback**: Returns question analysis if all else fails
245
+
246
+ ## Error Handling
247
+
248
+ - Invalid JSON → 400 Bad Request
249
+ - Wrong secret → 403 Forbidden
250
+ - Page load errors → 500 with error details
251
+ - Timeout (>3 min) → 504 Gateway Timeout
252
+ - All errors are logged for debugging
253
+
254
+ ## Limitations
255
+
256
+ - Maximum recursion depth: 10 quizzes
257
+ - Timeout: 3 minutes per request
258
+ - Requires internet connection for external URLs
259
+ - OpenAI API key needed for LLM features (optional)
260
+
261
+ ## License
262
+
263
+ MIT License - see LICENSE file for details.
264
+
265
+ ## Contributing
266
+
267
+ 1. Fork the repository
268
+ 2. Create a feature branch
269
+ 3. Make your changes
270
+ 4. Submit a pull request
271
+
272
+ ## Support
273
+
274
+ For issues and questions, please open an issue on the repository.
275
+
app/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # IITM LLM Quiz Solver
2
+ __version__ = "1.0.0"
3
+
app/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (171 Bytes). View file
 
app/__pycache__/browser.cpython-311.pyc ADDED
Binary file (17.1 kB). View file
 
app/__pycache__/llm.cpython-311.pyc ADDED
Binary file (10.2 kB). View file
 
app/__pycache__/main.cpython-311.pyc ADDED
Binary file (11.3 kB). View file
 
app/__pycache__/solver.cpython-311.pyc ADDED
Binary file (27.3 kB). View file
 
app/__pycache__/utils.cpython-311.pyc ADDED
Binary file (6.5 kB). View file
 
app/main.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI main server for IITM LLM Quiz Solver.
3
+ """
4
+ import os
5
+ import logging
6
+ import asyncio
7
+ from typing import Dict, Any, Optional
8
+ from fastapi import FastAPI, HTTPException, Request
9
+ from fastapi.responses import JSONResponse
10
+ from pydantic import BaseModel, Field, field_validator
11
+ import uvicorn
12
+
13
+ # Try to load .env file if python-dotenv is available
14
+ try:
15
+ from dotenv import load_dotenv
16
+ load_dotenv()
17
+ except ImportError:
18
+ pass # python-dotenv is optional
19
+
20
+ from app.solver import solve_quiz, validate_secret, cleanup_browser, test_prompt_with_custom_messages
21
+
22
+ # Configure logging
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
26
+ )
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Get secret from environment
30
+ EXPECTED_SECRET = os.getenv("QUIZ_SECRET", "default_secret_change_me")
31
+
32
+ # Lifespan context manager for startup and shutdown
33
+ from contextlib import asynccontextmanager
34
+
35
+ @asynccontextmanager
36
+ async def lifespan(app: FastAPI):
37
+ """Lifespan context manager for startup and shutdown."""
38
+ # Startup
39
+ logger.info("Application starting up...")
40
+ yield
41
+ # Shutdown
42
+ logger.info("Shutting down, cleaning up browser...")
43
+ await cleanup_browser()
44
+
45
+ # Initialize FastAPI app with lifespan
46
+ app = FastAPI(
47
+ title="IITM LLM Quiz Solver",
48
+ description="API endpoint to automatically solve dynamic quiz tasks",
49
+ version="1.0.0",
50
+ lifespan=lifespan
51
+ )
52
+
53
+
54
+ class QuizRequest(BaseModel):
55
+ """Request model for quiz solving."""
56
+ email: str = Field(..., description="User email address")
57
+ secret: str = Field(..., description="Secret key for authentication")
58
+ url: str = Field(..., description="Quiz page URL")
59
+
60
+ @field_validator('email')
61
+ @classmethod
62
+ def validate_email(cls, v):
63
+ if not v or '@' not in v:
64
+ raise ValueError('Invalid email format')
65
+ return v
66
+
67
+ @field_validator('url')
68
+ @classmethod
69
+ def validate_url(cls, v):
70
+ if not v or not v.startswith(('http://', 'https://')):
71
+ raise ValueError('Invalid URL format')
72
+ return v
73
+
74
+
75
+ class PromptTestRequest(BaseModel):
76
+ """Request model for testing custom prompts."""
77
+ system_prompt: str = Field(..., max_length=100, description="System prompt (max 100 chars)")
78
+ user_prompt: str = Field(..., max_length=100, description="User prompt (max 100 chars)")
79
+ secret: str = Field(..., description="Secret key for authentication")
80
+
81
+
82
+ @app.get("/")
83
+ async def root():
84
+ """Root endpoint."""
85
+ return {
86
+ "message": "IITM LLM Quiz Solver API",
87
+ "version": "1.0.0",
88
+ "endpoints": {
89
+ "/solve": "POST - Solve a quiz",
90
+ "/health": "GET - Health check",
91
+ "/demo": "POST - Demo endpoint",
92
+ "/test-prompt": "POST - Test custom system/user prompts with code word"
93
+ }
94
+ }
95
+
96
+
97
+ @app.get("/health")
98
+ async def health_check():
99
+ """Health check endpoint."""
100
+ return {"status": "healthy"}
101
+
102
+
103
+ @app.get("/env-check")
104
+ async def env_check():
105
+ """
106
+ Check environment variables status (returns JSON).
107
+ Useful for verifying configuration.
108
+ """
109
+ quiz_secret = os.getenv("QUIZ_SECRET")
110
+ openrouter_key = os.getenv("OPENROUTER_API_KEY")
111
+ port = os.getenv("PORT", "8000")
112
+
113
+ return {
114
+ "status": "ok",
115
+ "variables": {
116
+ "QUIZ_SECRET": {
117
+ "set": quiz_secret is not None,
118
+ "length": len(quiz_secret) if quiz_secret else 0,
119
+ "preview": f"{quiz_secret[:4]}...{quiz_secret[-4:]}" if quiz_secret and len(quiz_secret) > 8 else "***" if quiz_secret else None
120
+ },
121
+ "OPENROUTER_API_KEY": {
122
+ "set": openrouter_key is not None,
123
+ "length": len(openrouter_key) if openrouter_key else 0,
124
+ "preview": f"{openrouter_key[:7]}...{openrouter_key[-4:]}" if openrouter_key and len(openrouter_key) > 11 else "***" if openrouter_key else None,
125
+ "valid_format": openrouter_key.startswith("sk-or-") if openrouter_key else False
126
+ },
127
+ "PORT": {
128
+ "set": True,
129
+ "value": port
130
+ }
131
+ },
132
+ "ready": quiz_secret is not None,
133
+ "llm_enabled": openrouter_key is not None
134
+ }
135
+
136
+
137
+ @app.post("/solve")
138
+ async def solve_quiz_endpoint(request: QuizRequest):
139
+ """
140
+ Main endpoint to solve a quiz.
141
+
142
+ Validates secret and solves the quiz recursively.
143
+ """
144
+ try:
145
+ # Validate secret
146
+ if not validate_secret(request.secret, EXPECTED_SECRET):
147
+ logger.warning(f"Invalid secret provided for email: {request.email}")
148
+ raise HTTPException(
149
+ status_code=403,
150
+ detail={"error": "forbidden"}
151
+ )
152
+
153
+ logger.info(f"Solving quiz for {request.email} at {request.url}")
154
+
155
+ # Solve quiz with timeout
156
+ try:
157
+ result = await asyncio.wait_for(
158
+ solve_quiz(request.url, request.email, request.secret),
159
+ timeout=180.0 # 3 minutes
160
+ )
161
+ return result
162
+ except asyncio.TimeoutError:
163
+ logger.error("Quiz solving timed out")
164
+ raise HTTPException(
165
+ status_code=504,
166
+ detail={"error": "Request timeout - quiz solving took too long"}
167
+ )
168
+ except Exception as e:
169
+ logger.error(f"Error solving quiz: {e}", exc_info=True)
170
+ raise HTTPException(
171
+ status_code=500,
172
+ detail={"error": str(e)}
173
+ )
174
+
175
+ except HTTPException:
176
+ raise
177
+ except ValueError as e:
178
+ logger.error(f"Validation error: {e}")
179
+ raise HTTPException(
180
+ status_code=400,
181
+ detail={"error": "Invalid request format", "message": str(e)}
182
+ )
183
+ except Exception as e:
184
+ logger.error(f"Unexpected error: {e}", exc_info=True)
185
+ raise HTTPException(
186
+ status_code=500,
187
+ detail={"error": "Internal server error", "message": str(e)}
188
+ )
189
+
190
+
191
+ @app.post("/test-prompt")
192
+ async def test_prompt_endpoint(request: PromptTestRequest):
193
+ """
194
+ Test endpoint for custom system and user prompts with code word.
195
+
196
+ Uses QUIZ_SECRET from environment as the code word (kept secret).
197
+ Tests whether:
198
+ 1. System prompt prevents revealing the code word
199
+ 2. User prompt can override system prompt to reveal it
200
+ """
201
+ try:
202
+ # Validate secret
203
+ if not validate_secret(request.secret, EXPECTED_SECRET):
204
+ logger.warning("Invalid secret in test-prompt request")
205
+ return JSONResponse(
206
+ status_code=403,
207
+ content={"error": "forbidden"}
208
+ )
209
+
210
+ # Use QUIZ_SECRET as the code word (from environment)
211
+ code_word = EXPECTED_SECRET
212
+ if not code_word or code_word == "default_secret_change_me":
213
+ return JSONResponse(
214
+ status_code=400,
215
+ content={"error": "QUIZ_SECRET not properly configured"}
216
+ )
217
+
218
+ logger.info(f"Testing prompts - System: {request.system_prompt[:50]}..., User: {request.user_prompt[:50]}...")
219
+
220
+ # Test the prompts
221
+ try:
222
+ response = await asyncio.wait_for(
223
+ test_prompt_with_custom_messages(
224
+ request.system_prompt,
225
+ request.user_prompt,
226
+ code_word
227
+ ),
228
+ timeout=30.0
229
+ )
230
+
231
+ if response is None:
232
+ return JSONResponse(
233
+ status_code=500,
234
+ content={"error": "LLM API call failed - check API keys"}
235
+ )
236
+
237
+ # Check if code word was revealed
238
+ code_word_revealed = code_word.lower() in response.lower()
239
+
240
+ # Mask the code word in the response for security
241
+ masked_code_word = f"{code_word[:4]}...{code_word[-4:]}" if len(code_word) > 8 else "***"
242
+ masked_response = response.replace(code_word, "***MASKED***")
243
+
244
+ return {
245
+ "system_prompt": request.system_prompt,
246
+ "user_prompt": request.user_prompt,
247
+ "code_word": masked_code_word, # Never expose the actual secret
248
+ "llm_response": masked_response, # Mask any occurrences
249
+ "code_word_revealed": code_word_revealed,
250
+ "test_result": "FAILED - Code word revealed" if code_word_revealed else "PASSED - Code word protected"
251
+ }
252
+
253
+ except asyncio.TimeoutError:
254
+ return JSONResponse(
255
+ status_code=504,
256
+ content={"error": "Request timeout"}
257
+ )
258
+ except Exception as e:
259
+ logger.error(f"Error in test-prompt: {e}", exc_info=True)
260
+ return JSONResponse(
261
+ status_code=500,
262
+ content={"error": str(e)}
263
+ )
264
+
265
+ except ValueError as e:
266
+ return JSONResponse(
267
+ status_code=400,
268
+ content={"error": "Invalid request format", "message": str(e)}
269
+ )
270
+ except Exception as e:
271
+ logger.error(f"Unexpected error in test-prompt: {e}", exc_info=True)
272
+ return JSONResponse(
273
+ status_code=500,
274
+ content={"error": "Internal server error", "message": str(e)}
275
+ )
276
+
277
+
278
+ @app.post("/demo")
279
+ async def demo_endpoint(request: QuizRequest):
280
+ """
281
+ Demo endpoint for testing.
282
+
283
+ Same as /solve but with more lenient error handling.
284
+ """
285
+ try:
286
+ # Validate secret (can be more lenient for demo)
287
+ if not validate_secret(request.secret, EXPECTED_SECRET):
288
+ logger.warning(f"Invalid secret in demo request")
289
+ return JSONResponse(
290
+ status_code=403,
291
+ content={"error": "forbidden"}
292
+ )
293
+
294
+ logger.info(f"Demo: Solving quiz for {request.email} at {request.url}")
295
+
296
+ # Solve quiz
297
+ try:
298
+ result = await asyncio.wait_for(
299
+ solve_quiz(request.url, request.email, request.secret),
300
+ timeout=180.0
301
+ )
302
+ return result
303
+ except asyncio.TimeoutError:
304
+ return JSONResponse(
305
+ status_code=504,
306
+ content={"error": "Request timeout"}
307
+ )
308
+ except Exception as e:
309
+ logger.error(f"Error in demo: {e}", exc_info=True)
310
+ return JSONResponse(
311
+ status_code=500,
312
+ content={"error": str(e)}
313
+ )
314
+
315
+ except ValueError as e:
316
+ return JSONResponse(
317
+ status_code=400,
318
+ content={"error": "Invalid request format", "message": str(e)}
319
+ )
320
+ except Exception as e:
321
+ logger.error(f"Unexpected error in demo: {e}", exc_info=True)
322
+ return JSONResponse(
323
+ status_code=500,
324
+ content={"error": "Internal server error", "message": str(e)}
325
+ )
326
+
327
+
328
+ if __name__ == "__main__":
329
+ port = int(os.getenv("PORT", 8000))
330
+ uvicorn.run(
331
+ "app.main:app",
332
+ host="0.0.0.0",
333
+ port=port,
334
+ log_level="info"
335
+ )
336
+
app/solver.py ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ playwright==1.40.0
4
+ requests==2.31.0
5
+ beautifulsoup4==4.12.2
6
+ pandas==2.1.3
7
+ numpy==1.26.2
8
+ PyPDF2==3.0.1
9
+ pdfplumber==0.10.3
10
+ httpx==0.25.2
11
+ pydantic==2.5.0
12
+ lxml==4.9.3
13
+ html5lib==1.1
14
+ python-dotenv==1.0.0
15
+ Pillow==10.1.0
16
+ openai==1.3.0
17
+ duckdb==0.9.0
18
+