Upload 37 files
Browse files- .dockerignore +24 -0
- .gitignore +66 -0
- DEBUG_ERROR.md +132 -0
- Dockerfile +58 -0
- ENV_SETUP.md +316 -0
- EXPLAIN_OUTPUT.md +119 -0
- FIX_SERVER.md +89 -0
- LICENSE +22 -0
- QUICK_CHECK.md +131 -0
- QUICK_START.md +110 -0
- README.md +265 -11
- SET_API_KEY.md +66 -0
- START_SERVER.md +90 -0
- TEST_API.md +222 -0
- TEST_POWERSHELL.md +175 -0
- __init__.cpython-311.pyc +0 -0
- __init__.py +3 -0
- browser.cpython-311.pyc +0 -0
- browser.py +247 -0
- check_and_start.ps1 +78 -0
- check_env.py +93 -0
- llm.cpython-311.pyc +0 -0
- llm.py +251 -0
- main.cpython-311.pyc +0 -0
- main.py +250 -0
- requirements.txt +14 -0
- restart_server.ps1 +41 -0
- set_api_key.ps1 +20 -0
- setup_env.ps1 +72 -0
- setup_env.sh +46 -0
- solver.cpython-311.pyc +0 -0
- solver.py +593 -0
- test_api.ps1 +47 -0
- test_env.json.py +50 -0
- test_server.ps1 +94 -0
- utils.cpython-311.pyc +0 -0
- utils.py +180 -0
.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 |
+
|
DEBUG_ERROR.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Debugging the D1_TYPE_ERROR
|
| 2 |
+
|
| 3 |
+
## Understanding the Error
|
| 4 |
+
|
| 5 |
+
The error `"Internal error: D1_TYPE_ERROR: Type 'object' not supported for value '[object Object]'"` is coming from the **quiz page backend** (likely Cloudflare D1 database), not from our server.
|
| 6 |
+
|
| 7 |
+
This suggests:
|
| 8 |
+
1. ✅ Our server is working correctly
|
| 9 |
+
2. ✅ The request is reaching the quiz page
|
| 10 |
+
3. ❌ The quiz page backend is rejecting our answer format
|
| 11 |
+
|
| 12 |
+
## Possible Causes
|
| 13 |
+
|
| 14 |
+
### 1. Wrong URL
|
| 15 |
+
You're using: `https://tds-llm-analysis.s-anand.net/demo`
|
| 16 |
+
|
| 17 |
+
This is a **demo endpoint**, not a quiz page. You need to:
|
| 18 |
+
- Go to the actual quiz page first
|
| 19 |
+
- Get the quiz page URL
|
| 20 |
+
- Use that URL in your request
|
| 21 |
+
|
| 22 |
+
### 2. Answer Format Issue
|
| 23 |
+
The quiz page might expect:
|
| 24 |
+
- A simple string instead of a JSON object
|
| 25 |
+
- A specific JSON structure
|
| 26 |
+
- A different data type
|
| 27 |
+
|
| 28 |
+
### 3. Quiz Page Backend Issue
|
| 29 |
+
The quiz page's database (D1) might have issues with:
|
| 30 |
+
- Complex nested objects
|
| 31 |
+
- Certain data types
|
| 32 |
+
- The way we're serializing the answer
|
| 33 |
+
|
| 34 |
+
## How to Debug
|
| 35 |
+
|
| 36 |
+
### Step 1: Check Server Logs
|
| 37 |
+
|
| 38 |
+
When you run the server, you should see detailed logs:
|
| 39 |
+
```powershell
|
| 40 |
+
python -m app.main
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Look for:
|
| 44 |
+
- "Submitting answer to: ..."
|
| 45 |
+
- "Payload: ..."
|
| 46 |
+
- "Response status: ..."
|
| 47 |
+
- "Error response: ..."
|
| 48 |
+
|
| 49 |
+
### Step 2: Test with a Simple Answer
|
| 50 |
+
|
| 51 |
+
Try submitting a simple string answer first:
|
| 52 |
+
|
| 53 |
+
```powershell
|
| 54 |
+
# Test with simple answer
|
| 55 |
+
$body = @{
|
| 56 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 57 |
+
secret = "EasyQuiz"
|
| 58 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 59 |
+
} | ConvertTo-Json
|
| 60 |
+
|
| 61 |
+
# But wait - you need the actual quiz page URL, not the demo endpoint!
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
### Step 3: Verify the Quiz Page URL
|
| 65 |
+
|
| 66 |
+
The URL should be:
|
| 67 |
+
- A quiz page (like `https://tds-llm-analysis.s-anand.net/quiz/123`)
|
| 68 |
+
- NOT the demo endpoint (`/demo`)
|
| 69 |
+
|
| 70 |
+
### Step 4: Check What the Quiz Page Expects
|
| 71 |
+
|
| 72 |
+
1. Open the quiz page in a browser
|
| 73 |
+
2. Look at the page source
|
| 74 |
+
3. Find the submit URL
|
| 75 |
+
4. Check what format the answer should be in
|
| 76 |
+
|
| 77 |
+
## Quick Fixes to Try
|
| 78 |
+
|
| 79 |
+
### Fix 1: Use Correct URL
|
| 80 |
+
```powershell
|
| 81 |
+
# Get the actual quiz page URL first
|
| 82 |
+
# Then use that URL, not the demo endpoint
|
| 83 |
+
$body = @{
|
| 84 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 85 |
+
secret = "EasyQuiz"
|
| 86 |
+
url = "https://tds-llm-analysis.s-anand.net/actual-quiz-page" # Change this!
|
| 87 |
+
} | ConvertTo-Json
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### Fix 2: Check Server Response
|
| 91 |
+
The server now logs more details. Check the terminal where `python -m app.main` is running to see:
|
| 92 |
+
- What payload is being sent
|
| 93 |
+
- What response is received
|
| 94 |
+
- Any error details
|
| 95 |
+
|
| 96 |
+
### Fix 3: Test with curl for Better Error Messages
|
| 97 |
+
```powershell
|
| 98 |
+
curl.exe -X POST http://127.0.0.1:8000/demo `
|
| 99 |
+
-H "Content-Type: application/json" `
|
| 100 |
+
-d '{\"email\":\"24f2005753@ds.study.iitm.ac.in\",\"secret\":\"EasyQuiz\",\"url\":\"https://tds-llm-analysis.s-anand.net/demo\"}' `
|
| 101 |
+
-v
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
The `-v` flag shows verbose output including headers and response details.
|
| 105 |
+
|
| 106 |
+
## What I've Fixed
|
| 107 |
+
|
| 108 |
+
I've updated the code to:
|
| 109 |
+
1. ✅ Better error handling in `_submit_answer`
|
| 110 |
+
2. ✅ More detailed logging
|
| 111 |
+
3. ✅ Better JSON serialization handling
|
| 112 |
+
4. ✅ Logs the full payload and response
|
| 113 |
+
|
| 114 |
+
## Next Steps
|
| 115 |
+
|
| 116 |
+
1. **Check the server logs** when you make the request
|
| 117 |
+
2. **Verify you're using the correct quiz page URL** (not the demo endpoint)
|
| 118 |
+
3. **Look at the detailed error in the logs** to see what the quiz page is rejecting
|
| 119 |
+
4. **Try with a simpler answer format** if the logs show serialization issues
|
| 120 |
+
|
| 121 |
+
## Expected Behavior
|
| 122 |
+
|
| 123 |
+
When working correctly:
|
| 124 |
+
1. Server loads the quiz page
|
| 125 |
+
2. Extracts the question
|
| 126 |
+
3. Solves it
|
| 127 |
+
4. Finds the submit URL
|
| 128 |
+
5. Submits the answer
|
| 129 |
+
6. Returns the response
|
| 130 |
+
|
| 131 |
+
The error you're seeing happens at step 5 - the quiz page backend is rejecting our submission.
|
| 132 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.10 slim image
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
wget \
|
| 10 |
+
gnupg \
|
| 11 |
+
ca-certificates \
|
| 12 |
+
fonts-liberation \
|
| 13 |
+
libasound2 \
|
| 14 |
+
libatk-bridge2.0-0 \
|
| 15 |
+
libatk1.0-0 \
|
| 16 |
+
libatspi2.0-0 \
|
| 17 |
+
libcups2 \
|
| 18 |
+
libdbus-1-3 \
|
| 19 |
+
libdrm2 \
|
| 20 |
+
libgbm1 \
|
| 21 |
+
libgtk-3-0 \
|
| 22 |
+
libnspr4 \
|
| 23 |
+
libnss3 \
|
| 24 |
+
libxcomposite1 \
|
| 25 |
+
libxdamage1 \
|
| 26 |
+
libxfixes3 \
|
| 27 |
+
libxkbcommon0 \
|
| 28 |
+
libxrandr2 \
|
| 29 |
+
xdg-utils \
|
| 30 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 31 |
+
|
| 32 |
+
# Copy requirements first for better caching
|
| 33 |
+
COPY requirements.txt .
|
| 34 |
+
|
| 35 |
+
# Install Python dependencies
|
| 36 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 37 |
+
|
| 38 |
+
# Install Playwright browsers
|
| 39 |
+
RUN playwright install chromium
|
| 40 |
+
RUN playwright install-deps chromium
|
| 41 |
+
|
| 42 |
+
# Copy application code
|
| 43 |
+
COPY app/ ./app/
|
| 44 |
+
|
| 45 |
+
# Set environment variables
|
| 46 |
+
ENV PYTHONUNBUFFERED=1
|
| 47 |
+
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
| 48 |
+
|
| 49 |
+
# Expose port
|
| 50 |
+
EXPOSE 8000
|
| 51 |
+
|
| 52 |
+
# Health check
|
| 53 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 54 |
+
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
|
| 55 |
+
|
| 56 |
+
# Run the application
|
| 57 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
| 58 |
+
|
ENV_SETUP.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment Variables Setup Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to set the required environment variables for the IITM LLM Quiz Solver.
|
| 4 |
+
|
| 5 |
+
## Required Variables
|
| 6 |
+
|
| 7 |
+
- `QUIZ_SECRET`: Secret key for API authentication (required)
|
| 8 |
+
- `OPENAI_API_KEY`: OpenAI API key for LLM features (optional)
|
| 9 |
+
- `OPENROUTER_API_KEY`: OpenRouter key (e.g., GPT-5-nano) – optional fallback or alternative
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Method 1: Windows (PowerShell)
|
| 14 |
+
|
| 15 |
+
### Temporary (Current Session Only)
|
| 16 |
+
|
| 17 |
+
Open PowerShell and run:
|
| 18 |
+
|
| 19 |
+
```powershell
|
| 20 |
+
$env:QUIZ_SECRET = "EasyQuiZ"
|
| 21 |
+
$env:OPENAI_API_KEY = "sk-your-openai-api-key-here"
|
| 22 |
+
$env:OPENROUTER_API_KEY = "sk-or-your-openrouter-key"
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### Permanent (System-Wide)
|
| 26 |
+
|
| 27 |
+
1. **Using PowerShell (Admin):**
|
| 28 |
+
```powershell
|
| 29 |
+
[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", "your_secret_key_here", "User")
|
| 30 |
+
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "sk-your-openai-api-key-here", "User")
|
| 31 |
+
[System.Environment]::SetEnvironmentVariable("OPENROUTER_API_KEY", "sk-or-your-openrouter-key", "User")
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
2. **Using GUI:**
|
| 35 |
+
- Press `Win + R`, type `sysdm.cpl`, press Enter
|
| 36 |
+
- Go to "Advanced" tab → Click "Environment Variables"
|
| 37 |
+
- Under "User variables", click "New"
|
| 38 |
+
- Add `QUIZ_SECRET` with your value
|
| 39 |
+
- Add `OPENAI_API_KEY` with your value
|
| 40 |
+
- Click OK to save
|
| 41 |
+
|
| 42 |
+
### Using .env File (Recommended for Development)
|
| 43 |
+
|
| 44 |
+
1. Create a `.env` file in the project root:
|
| 45 |
+
```env
|
| 46 |
+
QUIZ_SECRET=your_secret_key_here
|
| 47 |
+
OPENAI_API_KEY=sk-your-openai-api-key-here
|
| 48 |
+
OPENROUTER_API_KEY=sk-or-your-openrouter-key
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
2. Install python-dotenv:
|
| 52 |
+
```bash
|
| 53 |
+
pip install python-dotenv
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
3. Update `app/main.py` to load .env file (add at the top):
|
| 57 |
+
```python
|
| 58 |
+
from dotenv import load_dotenv
|
| 59 |
+
load_dotenv()
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## Method 2: Linux/Mac (Bash)
|
| 65 |
+
|
| 66 |
+
### Temporary (Current Session Only)
|
| 67 |
+
|
| 68 |
+
Open terminal and run:
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
export QUIZ_SECRET="your_secret_key_here"
|
| 72 |
+
export OPENAI_API_KEY="sk-your-openai-api-key-here"
|
| 73 |
+
export OPENROUTER_API_KEY="sk-or-your-openrouter-key"
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### Permanent (Add to Shell Profile)
|
| 77 |
+
|
| 78 |
+
Add to `~/.bashrc` or `~/.zshrc`:
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
echo 'export QUIZ_SECRET="your_secret_key_here"' >> ~/.bashrc
|
| 82 |
+
echo 'export OPENAI_API_KEY="sk-your-openai-api-key-here"' >> ~/.bashrc
|
| 83 |
+
echo 'export OPENROUTER_API_KEY="sk-or-your-openrouter-key"' >> ~/.bashrc
|
| 84 |
+
source ~/.bashrc
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Using .env File (Recommended for Development)
|
| 88 |
+
|
| 89 |
+
Same as Windows method above.
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## Method 3: Hugging Face Spaces
|
| 94 |
+
|
| 95 |
+
### Step-by-Step Instructions
|
| 96 |
+
|
| 97 |
+
1. **Go to your Space:**
|
| 98 |
+
- Navigate to https://huggingface.co/spaces
|
| 99 |
+
- Open your Space
|
| 100 |
+
|
| 101 |
+
2. **Access Settings:**
|
| 102 |
+
- Click on "Settings" in the top menu
|
| 103 |
+
- Select "Variables and secrets" from the left sidebar
|
| 104 |
+
|
| 105 |
+
3. **Add Variables:**
|
| 106 |
+
- Click "New variable" or "New secret"
|
| 107 |
+
- For `QUIZ_SECRET`:
|
| 108 |
+
- Key: `QUIZ_SECRET`
|
| 109 |
+
- Value: Your secret key
|
| 110 |
+
- Type: Variable (or Secret if you want it hidden)
|
| 111 |
+
- For `OPENAI_API_KEY`:
|
| 112 |
+
- Key: `OPENAI_API_KEY`
|
| 113 |
+
- Value: Your OpenAI API key (starts with `sk-`)
|
| 114 |
+
- Type: Secret (recommended for API keys)
|
| 115 |
+
|
| 116 |
+
4. **Save:**
|
| 117 |
+
- Click "Save" or "Add variable"
|
| 118 |
+
- The Space will automatically rebuild with new variables
|
| 119 |
+
|
| 120 |
+
### Important Notes for HF Spaces:
|
| 121 |
+
- Variables are available to your Docker container
|
| 122 |
+
- Secrets are hidden from logs but accessible in code
|
| 123 |
+
- Changes require a rebuild (automatic)
|
| 124 |
+
- `PORT` is usually set automatically by HF Spaces
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
## Method 4: Docker (Local)
|
| 129 |
+
|
| 130 |
+
### Using docker run:
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
docker run -e QUIZ_SECRET="your_secret" -e OPENAI_API_KEY="sk-your-key" -p 8000:8000 your-image
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
### Using docker-compose.yml:
|
| 137 |
+
|
| 138 |
+
Create `docker-compose.yml`:
|
| 139 |
+
|
| 140 |
+
```yaml
|
| 141 |
+
version: '3.8'
|
| 142 |
+
services:
|
| 143 |
+
app:
|
| 144 |
+
build: .
|
| 145 |
+
ports:
|
| 146 |
+
- "8000:8000"
|
| 147 |
+
environment:
|
| 148 |
+
- QUIZ_SECRET=${QUIZ_SECRET}
|
| 149 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 150 |
+
env_file:
|
| 151 |
+
- .env # Optional: load from .env file
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Then run:
|
| 155 |
+
```bash
|
| 156 |
+
docker-compose up
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## Verification
|
| 162 |
+
|
| 163 |
+
### Method 1: Using the Check Script (Easiest)
|
| 164 |
+
|
| 165 |
+
Run the included verification script:
|
| 166 |
+
|
| 167 |
+
```bash
|
| 168 |
+
python check_env.py
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
This will show:
|
| 172 |
+
- ✓/✗ status for each variable
|
| 173 |
+
- Masked values (for security)
|
| 174 |
+
- Warnings if values look incorrect
|
| 175 |
+
- Next steps
|
| 176 |
+
|
| 177 |
+
### Method 2: Command Line
|
| 178 |
+
|
| 179 |
+
**Windows (PowerShell):**
|
| 180 |
+
```powershell
|
| 181 |
+
# Check if set (shows value - be careful!)
|
| 182 |
+
echo $env:QUIZ_SECRET
|
| 183 |
+
echo $env:OPENAI_API_KEY
|
| 184 |
+
|
| 185 |
+
# Check if set (just true/false)
|
| 186 |
+
if ($env:QUIZ_SECRET) { Write-Host "QUIZ_SECRET is set" } else { Write-Host "QUIZ_SECRET is NOT set" }
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
**Linux/Mac:**
|
| 190 |
+
```bash
|
| 191 |
+
# Check if set (shows value - be careful!)
|
| 192 |
+
echo $QUIZ_SECRET
|
| 193 |
+
echo $OPENAI_API_KEY
|
| 194 |
+
|
| 195 |
+
# Check if set (just true/false)
|
| 196 |
+
[ -n "$QUIZ_SECRET" ] && echo "QUIZ_SECRET is set" || echo "QUIZ_SECRET is NOT set"
|
| 197 |
+
[ -n "$OPENAI_API_KEY" ] && echo "OPENAI_API_KEY is set" || echo "OPENAI_API_KEY is NOT set"
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### Method 3: Python Script
|
| 201 |
+
|
| 202 |
+
**Quick check:**
|
| 203 |
+
```python
|
| 204 |
+
import os
|
| 205 |
+
print("QUIZ_SECRET:", "✓ Set" if os.getenv("QUIZ_SECRET") else "✗ Not set")
|
| 206 |
+
print("OPENAI_API_KEY:", "✓ Set" if os.getenv("OPENAI_API_KEY") else "✗ Not set (optional)")
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
**Detailed check:**
|
| 210 |
+
```python
|
| 211 |
+
import os
|
| 212 |
+
|
| 213 |
+
quiz_secret = os.getenv("QUIZ_SECRET")
|
| 214 |
+
openai_key = os.getenv("OPENAI_API_KEY")
|
| 215 |
+
|
| 216 |
+
if quiz_secret:
|
| 217 |
+
print(f"✓ QUIZ_SECRET: Set (length: {len(quiz_secret)})")
|
| 218 |
+
else:
|
| 219 |
+
print("✗ QUIZ_SECRET: NOT SET")
|
| 220 |
+
|
| 221 |
+
if openai_key:
|
| 222 |
+
print(f"✓ OPENAI_API_KEY: Set (starts with: {openai_key[:3]})")
|
| 223 |
+
else:
|
| 224 |
+
print("✗ OPENAI_API_KEY: NOT SET (optional)")
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
### Method 4: Test the API
|
| 228 |
+
|
| 229 |
+
Start the server and test:
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
# Start server
|
| 233 |
+
python -m app.main
|
| 234 |
+
|
| 235 |
+
# In another terminal, test health endpoint
|
| 236 |
+
curl http://localhost:8000/health
|
| 237 |
+
|
| 238 |
+
# Test with invalid secret (should return 403)
|
| 239 |
+
curl -X POST http://localhost:8000/solve \
|
| 240 |
+
-H "Content-Type: application/json" \
|
| 241 |
+
-d '{"email":"test@test.com","secret":"wrong","url":"https://example.com"}'
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
If you get `{"error":"forbidden"}`, the secret validation is working!
|
| 245 |
+
|
| 246 |
+
---
|
| 247 |
+
|
| 248 |
+
## Security Best Practices
|
| 249 |
+
|
| 250 |
+
1. **Never commit secrets to Git:**
|
| 251 |
+
- Add `.env` to `.gitignore` (already included)
|
| 252 |
+
- Use environment variables or secrets management
|
| 253 |
+
|
| 254 |
+
2. **Use strong secrets:**
|
| 255 |
+
- `QUIZ_SECRET`: Use a long random string (32+ characters)
|
| 256 |
+
- Generate with: `python -c "import secrets; print(secrets.token_urlsafe(32))"`
|
| 257 |
+
|
| 258 |
+
3. **Rotate keys regularly:**
|
| 259 |
+
- Change `QUIZ_SECRET` periodically
|
| 260 |
+
- Regenerate OpenAI API keys if compromised
|
| 261 |
+
|
| 262 |
+
4. **For production:**
|
| 263 |
+
- Use secret management services (AWS Secrets Manager, Azure Key Vault, etc.)
|
| 264 |
+
- Never hardcode secrets in code
|
| 265 |
+
- Use different secrets for dev/staging/production
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
## Quick Start Script
|
| 270 |
+
|
| 271 |
+
Create `setup_env.sh` (Linux/Mac) or `setup_env.ps1` (Windows):
|
| 272 |
+
|
| 273 |
+
**setup_env.sh:**
|
| 274 |
+
```bash
|
| 275 |
+
#!/bin/bash
|
| 276 |
+
export QUIZ_SECRET="your_secret_key_here"
|
| 277 |
+
export OPENAI_API_KEY="sk-your-openai-api-key-here"
|
| 278 |
+
python -m app.main
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
**setup_env.ps1:**
|
| 282 |
+
```powershell
|
| 283 |
+
$env:QUIZ_SECRET = "your_secret_key_here"
|
| 284 |
+
$env:OPENAI_API_KEY = "sk-your-openai-api-key-here"
|
| 285 |
+
python -m app.main
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
---
|
| 289 |
+
|
| 290 |
+
## Troubleshooting
|
| 291 |
+
|
| 292 |
+
### Variable not found?
|
| 293 |
+
- Restart your terminal/IDE after setting variables
|
| 294 |
+
- Check spelling (case-sensitive)
|
| 295 |
+
- Verify with `echo $VARIABLE_NAME` or `echo $env:VARIABLE_NAME`
|
| 296 |
+
|
| 297 |
+
### HF Spaces not picking up variables?
|
| 298 |
+
- Ensure variables are saved in Settings
|
| 299 |
+
- Check Space logs for errors
|
| 300 |
+
- Rebuild the Space manually if needed
|
| 301 |
+
|
| 302 |
+
### Docker not using variables?
|
| 303 |
+
- Check `docker-compose.yml` syntax
|
| 304 |
+
- Use `docker run -e` for single container
|
| 305 |
+
- Verify with `docker exec container env`
|
| 306 |
+
|
| 307 |
+
---
|
| 308 |
+
|
| 309 |
+
## Need Help?
|
| 310 |
+
|
| 311 |
+
If you encounter issues:
|
| 312 |
+
1. Check the application logs
|
| 313 |
+
2. Verify variable names match exactly
|
| 314 |
+
3. Ensure no extra spaces in values
|
| 315 |
+
4. Test with a simple `print(os.getenv("VAR"))` in Python
|
| 316 |
+
|
EXPLAIN_OUTPUT.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Understanding the PowerShell Setup Output
|
| 2 |
+
|
| 3 |
+
## What Happened (Lines 26-41)
|
| 4 |
+
|
| 5 |
+
Let me break down what you saw:
|
| 6 |
+
|
| 7 |
+
### Line 26-28: QUIZ_SECRET Setup
|
| 8 |
+
```
|
| 9 |
+
QUIZ_SECRET is not set
|
| 10 |
+
Enter your QUIZ_SECRET (or press Enter to generate one): EasyQuiz
|
| 11 |
+
QUIZ_SECRET set for this session
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
**What this means:**
|
| 15 |
+
- ✅ The script detected `QUIZ_SECRET` wasn't set
|
| 16 |
+
- ✅ You entered `EasyQuiz` as your secret
|
| 17 |
+
- ✅ The variable is now set **for this PowerShell session only**
|
| 18 |
+
|
| 19 |
+
### Line 29: OPENAI_API_KEY
|
| 20 |
+
```
|
| 21 |
+
OPENAI_API_KEY is already set
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
**What this means:**
|
| 25 |
+
- ✅ Your OpenAI API key was already configured (probably from a previous session or system-wide)
|
| 26 |
+
|
| 27 |
+
### Line 31: Success Message
|
| 28 |
+
```
|
| 29 |
+
Environment variables configured!
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
**What this means:**
|
| 33 |
+
- ✅ Both variables are now available in your current PowerShell window
|
| 34 |
+
|
| 35 |
+
### Lines 33-35: Permanent Setup Commands
|
| 36 |
+
```
|
| 37 |
+
To make these permanent, run:
|
| 38 |
+
[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", " + EasyQuiz + ", "User")
|
| 39 |
+
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", " + sk-proj-... + ", "User")
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
**⚠️ IMPORTANT:** There's a display bug here - the output shows string concatenation instead of the actual command. The correct commands should be:
|
| 43 |
+
|
| 44 |
+
```powershell
|
| 45 |
+
[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", "EasyQuiz", "User")
|
| 46 |
+
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "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", "User")
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## What "Set for this session" Means
|
| 50 |
+
|
| 51 |
+
**Current Status:**
|
| 52 |
+
- ✅ Variables are set **only in this PowerShell window**
|
| 53 |
+
- ✅ You can run `python -m app.main` right now and it will work
|
| 54 |
+
- ❌ If you close this window, the variables will be lost
|
| 55 |
+
- ❌ If you open a new PowerShell window, variables won't be there
|
| 56 |
+
|
| 57 |
+
## What You Should Do
|
| 58 |
+
|
| 59 |
+
### Option 1: Use It Now (Temporary - Current Session Only)
|
| 60 |
+
|
| 61 |
+
Just run the server in the **same PowerShell window**:
|
| 62 |
+
```powershell
|
| 63 |
+
python -m app.main
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
This will work until you close the window.
|
| 67 |
+
|
| 68 |
+
### Option 2: Make It Permanent (Recommended)
|
| 69 |
+
|
| 70 |
+
Run these commands to make variables permanent:
|
| 71 |
+
|
| 72 |
+
```powershell
|
| 73 |
+
[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", "EasyQuiz", "User")
|
| 74 |
+
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "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", "User")
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
**After running these:**
|
| 78 |
+
- ✅ Variables will be available in ALL new PowerShell windows
|
| 79 |
+
- ✅ You need to close and reopen PowerShell for changes to take effect
|
| 80 |
+
- ✅ Variables persist even after restarting your computer
|
| 81 |
+
|
| 82 |
+
### Option 3: Verify It's Working
|
| 83 |
+
|
| 84 |
+
Run the check script:
|
| 85 |
+
```powershell
|
| 86 |
+
python check_env.py
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
You should see:
|
| 90 |
+
```
|
| 91 |
+
✓ QUIZ_SECRET: Set (Easy...Quiz)
|
| 92 |
+
✓ OPENAI_API_KEY: Set (sk-...4UA)
|
| 93 |
+
✓ Status: Ready to run
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## Quick Summary
|
| 97 |
+
|
| 98 |
+
| Status | Meaning | Action Needed |
|
| 99 |
+
|--------|---------|---------------|
|
| 100 |
+
| ✅ "set for this session" | Works in current window only | Run server now, or make permanent |
|
| 101 |
+
| ✅ "already set" | Variable exists | Nothing needed |
|
| 102 |
+
| ⚠️ Lines 33-35 | Display bug (shows wrong format) | Use corrected commands above |
|
| 103 |
+
|
| 104 |
+
## Next Steps
|
| 105 |
+
|
| 106 |
+
1. **Test it now** (in the same window):
|
| 107 |
+
```powershell
|
| 108 |
+
python check_env.py
|
| 109 |
+
python -m app.main
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
2. **Make it permanent** (optional but recommended):
|
| 113 |
+
```powershell
|
| 114 |
+
[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", "EasyQuiz", "User")
|
| 115 |
+
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "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", "User")
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
3. **Close and reopen PowerShell** (if you made it permanent)
|
| 119 |
+
|
FIX_SERVER.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Fix: Server "Not Found" Error
|
| 2 |
+
|
| 3 |
+
## The Problem
|
| 4 |
+
|
| 5 |
+
You have a server running on port 8000, but it's **NOT our FastAPI app**. It's returning "Not Found" for all endpoints.
|
| 6 |
+
|
| 7 |
+
## Solution: Stop and Restart
|
| 8 |
+
|
| 9 |
+
### Step 1: Stop the Wrong Server
|
| 10 |
+
|
| 11 |
+
Run this to stop whatever is on port 8000:
|
| 12 |
+
|
| 13 |
+
```powershell
|
| 14 |
+
# Find and stop process on port 8000
|
| 15 |
+
$portInfo = netstat -ano | findstr :8000 | findstr LISTENING
|
| 16 |
+
if ($portInfo) {
|
| 17 |
+
$pid = ($portInfo -split '\s+')[-1]
|
| 18 |
+
Stop-Process -Id $pid -Force
|
| 19 |
+
Write-Host "Stopped process $pid"
|
| 20 |
+
Start-Sleep -Seconds 2
|
| 21 |
+
}
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### Step 2: Start the Correct Server
|
| 25 |
+
|
| 26 |
+
In a **NEW PowerShell terminal**, run:
|
| 27 |
+
|
| 28 |
+
```powershell
|
| 29 |
+
# Navigate to project
|
| 30 |
+
cd C:\Users\tarun\IITMTdsPrj2
|
| 31 |
+
|
| 32 |
+
# Set environment variables
|
| 33 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 34 |
+
$env:OPENAI_API_KEY = "eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6IjI0ZjIwMDU3NTNAZHMuc3R1ZHkuaWl0bS5hYy5pbiJ9.eji7L5I62M9YHoeEKE8ao6eTw8dFjgDIMP9C3lOUXc4"
|
| 35 |
+
|
| 36 |
+
# Start the server
|
| 37 |
+
python -m app.main
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
You should see:
|
| 41 |
+
```
|
| 42 |
+
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
**Keep this terminal open!**
|
| 46 |
+
|
| 47 |
+
### Step 3: Test in Another Terminal
|
| 48 |
+
|
| 49 |
+
Open a **NEW PowerShell window** and test:
|
| 50 |
+
|
| 51 |
+
```powershell
|
| 52 |
+
# Test health endpoint
|
| 53 |
+
Invoke-RestMethod -Uri "http://127.0.0.1:8000/health"
|
| 54 |
+
|
| 55 |
+
# Test demo endpoint
|
| 56 |
+
$body = @{
|
| 57 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 58 |
+
secret = "EasyQuiz"
|
| 59 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 60 |
+
} | ConvertTo-Json
|
| 61 |
+
|
| 62 |
+
Invoke-RestMethod -Uri "http://127.0.0.1:8000/demo" -Method POST -ContentType "application/json" -Body $body
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Quick One-Liner to Stop Server
|
| 66 |
+
|
| 67 |
+
```powershell
|
| 68 |
+
Get-NetTCPConnection -LocalPort 8000 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess | ForEach-Object { Stop-Process -Id $_ -Force }; Write-Host "Stopped all processes on port 8000"
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Verify Server is Running
|
| 72 |
+
|
| 73 |
+
After starting, test:
|
| 74 |
+
|
| 75 |
+
```powershell
|
| 76 |
+
# Should return: {"status":"healthy"}
|
| 77 |
+
Invoke-RestMethod -Uri "http://127.0.0.1:8000/health"
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
If you get "Not Found", the wrong server is still running.
|
| 81 |
+
|
| 82 |
+
## Summary
|
| 83 |
+
|
| 84 |
+
1. ✅ Stop whatever is on port 8000
|
| 85 |
+
2. ✅ Start our FastAPI server: `python -m app.main`
|
| 86 |
+
3. ✅ Test endpoints in a new terminal
|
| 87 |
+
4. ✅ Keep server terminal open while testing
|
| 88 |
+
|
| 89 |
+
|
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 |
+
|
QUICK_CHECK.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quick Check Guide - PowerShell
|
| 2 |
+
|
| 3 |
+
## Issue: "Not Found" Error
|
| 4 |
+
|
| 5 |
+
The error `{"detail":"Not Found"}` means:
|
| 6 |
+
- ❌ Server is not running, OR
|
| 7 |
+
- ❌ Server needs to be restarted to see the new `/env-check` endpoint
|
| 8 |
+
|
| 9 |
+
## Issue: QUIZ_SECRET Not Set
|
| 10 |
+
|
| 11 |
+
The test shows `QUIZ_SECRET` is `false` because:
|
| 12 |
+
- ❌ Environment variable was only set in a previous PowerShell session
|
| 13 |
+
- ❌ You need to set it again in the current session
|
| 14 |
+
|
| 15 |
+
## Solution: Step-by-Step
|
| 16 |
+
|
| 17 |
+
### Step 1: Set Environment Variables (Current Session)
|
| 18 |
+
|
| 19 |
+
```powershell
|
| 20 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 21 |
+
$env:OPENAI_API_KEY = "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"
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### Step 2: Verify Variables Are Set
|
| 25 |
+
|
| 26 |
+
```powershell
|
| 27 |
+
python test_env.json.py
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
**Expected Output:**
|
| 31 |
+
```json
|
| 32 |
+
{
|
| 33 |
+
"status": "ok",
|
| 34 |
+
"variables": {
|
| 35 |
+
"QUIZ_SECRET": {
|
| 36 |
+
"set": true,
|
| 37 |
+
"length": 8,
|
| 38 |
+
"preview": "Easy...Quiz"
|
| 39 |
+
},
|
| 40 |
+
...
|
| 41 |
+
},
|
| 42 |
+
"ready": true
|
| 43 |
+
}
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Step 3: Start the Server
|
| 47 |
+
|
| 48 |
+
```powershell
|
| 49 |
+
python -m app.main
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Wait for: `Uvicorn running on http://0.0.0.0:8000`
|
| 53 |
+
|
| 54 |
+
### Step 4: Test the API (In a NEW PowerShell Window)
|
| 55 |
+
|
| 56 |
+
**Important:** Open a NEW PowerShell window (keep server running in the first one)
|
| 57 |
+
|
| 58 |
+
```powershell
|
| 59 |
+
# Set variables again in the new window
|
| 60 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 61 |
+
|
| 62 |
+
# Test the endpoint
|
| 63 |
+
Invoke-RestMethod -Uri http://localhost:8000/env-check | ConvertTo-Json
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
**Or use curl (PowerShell alias):**
|
| 67 |
+
```powershell
|
| 68 |
+
curl http://localhost:8000/env-check
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Alternative: Use the Test Script (No Server Needed)
|
| 72 |
+
|
| 73 |
+
Instead of starting the server, just run:
|
| 74 |
+
|
| 75 |
+
```powershell
|
| 76 |
+
# Set variables
|
| 77 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 78 |
+
|
| 79 |
+
# Check (no server needed)
|
| 80 |
+
python test_env.json.py
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
This gives you JSON output without needing the server running.
|
| 84 |
+
|
| 85 |
+
## Make Variables Permanent (Recommended)
|
| 86 |
+
|
| 87 |
+
To avoid setting variables every time:
|
| 88 |
+
|
| 89 |
+
```powershell
|
| 90 |
+
[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", "EasyQuiz", "User")
|
| 91 |
+
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "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", "User")
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
Then **close and reopen PowerShell** for changes to take effect.
|
| 95 |
+
|
| 96 |
+
## Quick Test Commands
|
| 97 |
+
|
| 98 |
+
### All-in-One Test (No Server)
|
| 99 |
+
```powershell
|
| 100 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 101 |
+
python test_env.json.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Test with Server Running
|
| 105 |
+
```powershell
|
| 106 |
+
# Terminal 1: Start server
|
| 107 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 108 |
+
python -m app.main
|
| 109 |
+
|
| 110 |
+
# Terminal 2: Test API
|
| 111 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 112 |
+
Invoke-RestMethod -Uri http://localhost:8000/env-check | ConvertTo-Json
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## Troubleshooting
|
| 116 |
+
|
| 117 |
+
### "Not Found" Error
|
| 118 |
+
- ✅ Make sure server is running
|
| 119 |
+
- ✅ Restart server after code changes
|
| 120 |
+
- ✅ Check URL is correct: `http://localhost:8000/env-check`
|
| 121 |
+
|
| 122 |
+
### "QUIZ_SECRET is required"
|
| 123 |
+
- ✅ Set variable: `$env:QUIZ_SECRET = "EasyQuiz"`
|
| 124 |
+
- ✅ Verify: `python test_env.json.py`
|
| 125 |
+
- ✅ Make permanent to avoid setting every time
|
| 126 |
+
|
| 127 |
+
### Server Won't Start
|
| 128 |
+
- ✅ Check if port 8000 is available
|
| 129 |
+
- ✅ Install dependencies: `pip install -r requirements.txt`
|
| 130 |
+
- ✅ Check for errors in the terminal
|
| 131 |
+
|
QUICK_START.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quick Start Guide
|
| 2 |
+
|
| 3 |
+
## 1. Install Dependencies
|
| 4 |
+
|
| 5 |
+
```bash
|
| 6 |
+
pip install -r requirements.txt
|
| 7 |
+
playwright install chromium
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
## 2. Set Environment Variables
|
| 11 |
+
|
| 12 |
+
### Option A: Use Setup Scripts (Recommended)
|
| 13 |
+
|
| 14 |
+
**Windows:**
|
| 15 |
+
```powershell
|
| 16 |
+
.\setup_env.ps1
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
**Linux/Mac:**
|
| 20 |
+
```bash
|
| 21 |
+
source setup_env.sh
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### Option B: Manual Setup
|
| 25 |
+
|
| 26 |
+
**Windows PowerShell:**
|
| 27 |
+
```powershell
|
| 28 |
+
$env:QUIZ_SECRET = "your_secret_here"
|
| 29 |
+
$env:OPENAI_API_KEY = "sk-your-key-here" # Optional
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
**Linux/Mac:**
|
| 33 |
+
```bash
|
| 34 |
+
export QUIZ_SECRET="your_secret_here"
|
| 35 |
+
export OPENAI_API_KEY="sk-your-key-here" # Optional
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### Option C: Use .env File
|
| 39 |
+
|
| 40 |
+
Create `.env` file:
|
| 41 |
+
```env
|
| 42 |
+
QUIZ_SECRET=your_secret_here
|
| 43 |
+
OPENAI_API_KEY=sk-your-key-here
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## 3. Verify Setup
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
python check_env.py
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
You should see:
|
| 53 |
+
```
|
| 54 |
+
✓ QUIZ_SECRET: Set (xxxx...xxxx)
|
| 55 |
+
✓ OPENAI_API_KEY: Set (sk-...xxxx) [if set]
|
| 56 |
+
✓ Status: Ready to run
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## 4. Start the Server
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
python -m app.main
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
Or:
|
| 66 |
+
```bash
|
| 67 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## 5. Test the API
|
| 71 |
+
|
| 72 |
+
**Health Check:**
|
| 73 |
+
```bash
|
| 74 |
+
curl http://localhost:8000/health
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
**Test Quiz Solving:**
|
| 78 |
+
```bash
|
| 79 |
+
curl -X POST http://localhost:8000/demo \
|
| 80 |
+
-H "Content-Type: application/json" \
|
| 81 |
+
-d '{
|
| 82 |
+
"email": "test@example.com",
|
| 83 |
+
"secret": "your_secret_here",
|
| 84 |
+
"url": "https://example.com/quiz"
|
| 85 |
+
}'
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
## Troubleshooting
|
| 89 |
+
|
| 90 |
+
### "QUIZ_SECRET is NOT SET"
|
| 91 |
+
- Run the setup script again
|
| 92 |
+
- Or manually set: `export QUIZ_SECRET="your_secret"` (Linux/Mac)
|
| 93 |
+
- Or: `$env:QUIZ_SECRET = "your_secret"` (Windows)
|
| 94 |
+
|
| 95 |
+
### "Module not found"
|
| 96 |
+
- Run: `pip install -r requirements.txt`
|
| 97 |
+
|
| 98 |
+
### "Playwright browser not found"
|
| 99 |
+
- Run: `playwright install chromium`
|
| 100 |
+
|
| 101 |
+
### Server won't start
|
| 102 |
+
- Check if port 8000 is available
|
| 103 |
+
- Change port: `export PORT=8001` then restart
|
| 104 |
+
|
| 105 |
+
## Next Steps
|
| 106 |
+
|
| 107 |
+
- See [ENV_SETUP.md](ENV_SETUP.md) for detailed environment setup
|
| 108 |
+
- See [README.md](README.md) for full documentation
|
| 109 |
+
- Deploy to Hugging Face Spaces (see README.md)
|
| 110 |
+
|
README.md
CHANGED
|
@@ -1,11 +1,265 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IITM LLM Quiz Solver
|
| 2 |
+
|
| 3 |
+
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.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- 🚀 FastAPI-based REST API
|
| 8 |
+
- 🌐 Playwright for headless browser automation
|
| 9 |
+
- 🤖 OpenAI GPT integration for complex reasoning
|
| 10 |
+
- 📊 Data processing (CSV, JSON, PDF, etc.)
|
| 11 |
+
- 🔄 Recursive quiz solving
|
| 12 |
+
- ⚡ Async/await for performance
|
| 13 |
+
- 🐳 Docker support for easy deployment
|
| 14 |
+
|
| 15 |
+
## Project Structure
|
| 16 |
+
|
| 17 |
+
```
|
| 18 |
+
/app
|
| 19 |
+
- main.py # FastAPI server
|
| 20 |
+
- solver.py # Quiz solving logic
|
| 21 |
+
- browser.py # Playwright helper
|
| 22 |
+
- llm.py # GPT helper
|
| 23 |
+
- utils.py # Utility functions
|
| 24 |
+
/Dockerfile
|
| 25 |
+
/requirements.txt
|
| 26 |
+
/README.md
|
| 27 |
+
/LICENSE
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
## Installation
|
| 31 |
+
|
| 32 |
+
### Local Development
|
| 33 |
+
|
| 34 |
+
1. Clone the repository:
|
| 35 |
+
```bash
|
| 36 |
+
git clone <repository-url>
|
| 37 |
+
cd IITMTdsPrj2
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
2. Install Python dependencies:
|
| 41 |
+
```bash
|
| 42 |
+
pip install -r requirements.txt
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
3. Install Playwright browsers:
|
| 46 |
+
```bash
|
| 47 |
+
playwright install chromium
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
4. Set environment variables:
|
| 51 |
+
|
| 52 |
+
**Quick Setup (Windows PowerShell):**
|
| 53 |
+
```powershell
|
| 54 |
+
.\setup_env.ps1
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
**Quick Setup (Linux/Mac):**
|
| 58 |
+
```bash
|
| 59 |
+
source setup_env.sh
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
**Manual Setup (choose whichever LLM provider you prefer):**
|
| 63 |
+
```bash
|
| 64 |
+
# Windows PowerShell
|
| 65 |
+
$env:QUIZ_SECRET = "your_secret_key"
|
| 66 |
+
$env:OPENAI_API_KEY = "sk-your-openai-api-key" # Optional - OpenAI
|
| 67 |
+
$env:OPENROUTER_API_KEY = "sk-or-your-openrouter" # Optional - OpenRouter GPT-5-nano
|
| 68 |
+
|
| 69 |
+
# Linux/Mac
|
| 70 |
+
export QUIZ_SECRET="your_secret_key"
|
| 71 |
+
export OPENAI_API_KEY="sk-your-openai-api-key" # Optional
|
| 72 |
+
export OPENROUTER_API_KEY="sk-or-your-openrouter" # Optional
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**Or use .env file:**
|
| 76 |
+
- Copy `.env.example` to `.env` (if available)
|
| 77 |
+
- Fill in your values
|
| 78 |
+
- The app will automatically load it
|
| 79 |
+
|
| 80 |
+
📖 **See [ENV_SETUP.md](ENV_SETUP.md) for detailed instructions**
|
| 81 |
+
|
| 82 |
+
5. Run the server:
|
| 83 |
+
```bash
|
| 84 |
+
python -m app.main
|
| 85 |
+
# or
|
| 86 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## API Endpoints
|
| 90 |
+
|
| 91 |
+
### POST /solve
|
| 92 |
+
|
| 93 |
+
Main endpoint to solve a quiz.
|
| 94 |
+
|
| 95 |
+
**Request Body:**
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"email": "user@example.com",
|
| 99 |
+
"secret": "your_secret",
|
| 100 |
+
"url": "https://example.com/quiz"
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
**Response:**
|
| 105 |
+
- `200 OK`: Quiz solved successfully
|
| 106 |
+
- `400 Bad Request`: Invalid request format
|
| 107 |
+
- `403 Forbidden`: Invalid secret
|
| 108 |
+
- `500 Internal Server Error`: Server error
|
| 109 |
+
- `504 Gateway Timeout`: Request timeout (>3 minutes)
|
| 110 |
+
|
| 111 |
+
### POST /demo
|
| 112 |
+
|
| 113 |
+
Demo endpoint for testing (same as `/solve` but with more lenient error handling).
|
| 114 |
+
|
| 115 |
+
**Request Body:** Same as `/solve`
|
| 116 |
+
|
| 117 |
+
### GET /health
|
| 118 |
+
|
| 119 |
+
Health check endpoint.
|
| 120 |
+
|
| 121 |
+
**Response:**
|
| 122 |
+
```json
|
| 123 |
+
{
|
| 124 |
+
"status": "healthy"
|
| 125 |
+
}
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
## Deployment on Hugging Face Spaces
|
| 129 |
+
|
| 130 |
+
### Method 1: Using Dockerfile (Recommended)
|
| 131 |
+
|
| 132 |
+
1. **Create a new Space on Hugging Face:**
|
| 133 |
+
- Go to https://huggingface.co/spaces
|
| 134 |
+
- Create a new Space
|
| 135 |
+
- Select "Docker" as the SDK
|
| 136 |
+
|
| 137 |
+
2. **Upload your files:**
|
| 138 |
+
- Upload all project files to your Space
|
| 139 |
+
- Ensure `Dockerfile` is in the root directory
|
| 140 |
+
|
| 141 |
+
3. **Set Environment Variables:**
|
| 142 |
+
- Go to Space Settings → Variables and secrets
|
| 143 |
+
- Add the following:
|
| 144 |
+
- `QUIZ_SECRET`: Your secret key for authentication
|
| 145 |
+
- `OPENAI_API_KEY`: Your OpenAI API key (optional)
|
| 146 |
+
- `OPENROUTER_API_KEY`: Your OpenRouter key (e.g., GPT-5-nano)
|
| 147 |
+
- `PORT`: 8000 (usually set automatically)
|
| 148 |
+
|
| 149 |
+
4. **Deploy:**
|
| 150 |
+
- Hugging Face will automatically build and deploy your Docker container
|
| 151 |
+
- The API will be available at: `https://<your-username>-<space-name>.hf.space`
|
| 152 |
+
|
| 153 |
+
### Method 2: Using Docker Compose (Alternative)
|
| 154 |
+
|
| 155 |
+
If you need more control, you can use `docker-compose.yml`:
|
| 156 |
+
|
| 157 |
+
```yaml
|
| 158 |
+
version: '3.8'
|
| 159 |
+
services:
|
| 160 |
+
app:
|
| 161 |
+
build: .
|
| 162 |
+
ports:
|
| 163 |
+
- "8000:8000"
|
| 164 |
+
environment:
|
| 165 |
+
- QUIZ_SECRET=${QUIZ_SECRET}
|
| 166 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
## Environment Variables
|
| 170 |
+
|
| 171 |
+
| Variable | Description | Required | Default |
|
| 172 |
+
|----------|-------------|----------|---------|
|
| 173 |
+
| `QUIZ_SECRET` | Secret key for API authentication | Yes | `default_secret_change_me` |
|
| 174 |
+
| `OPENAI_API_KEY` | OpenAI API key for LLM features | No | - |
|
| 175 |
+
| `OPENROUTER_API_KEY` | OpenRouter key (e.g., GPT-5-nano) | No | - |
|
| 176 |
+
| `OPENROUTER_MODEL` | Override OpenRouter model (default gpt-5-nano) | No | `gpt-5-nano` |
|
| 177 |
+
| `PORT` | Server port | No | `8000` |
|
| 178 |
+
|
| 179 |
+
## Testing
|
| 180 |
+
|
| 181 |
+
### Test with curl:
|
| 182 |
+
|
| 183 |
+
```bash
|
| 184 |
+
curl -X POST "https://tds-llm-analysis.s-anand.net/demo" \
|
| 185 |
+
-H "Content-Type: application/json" \
|
| 186 |
+
-d '{
|
| 187 |
+
"email": "test@example.com",
|
| 188 |
+
"secret": "your_secret",
|
| 189 |
+
"url": "https://example.com/quiz"
|
| 190 |
+
}'
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
### Test with Python:
|
| 194 |
+
|
| 195 |
+
```python
|
| 196 |
+
import requests
|
| 197 |
+
|
| 198 |
+
response = requests.post(
|
| 199 |
+
"https://tds-llm-analysis.s-anand.net/demo",
|
| 200 |
+
json={
|
| 201 |
+
"email": "test@example.com",
|
| 202 |
+
"secret": "your_secret",
|
| 203 |
+
"url": "https://example.com/quiz"
|
| 204 |
+
}
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
print(response.json())
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
## How It Works
|
| 211 |
+
|
| 212 |
+
1. **Request Validation**: Validates email, secret, and URL format
|
| 213 |
+
2. **Secret Authentication**: Checks secret against expected value (403 if wrong)
|
| 214 |
+
3. **Page Loading**: Uses Playwright to load and render the quiz page
|
| 215 |
+
4. **Content Extraction**: Extracts all text, HTML, links, and images
|
| 216 |
+
5. **Submit URL Detection**: Automatically finds the submit URL from page content
|
| 217 |
+
6. **Question Solving**:
|
| 218 |
+
- Extracts question text
|
| 219 |
+
- Tries multiple strategies:
|
| 220 |
+
- Check if answer is in page
|
| 221 |
+
- Download and process data files (CSV, JSON, PDF)
|
| 222 |
+
- Use LLM for complex reasoning
|
| 223 |
+
7. **Answer Submission**: Submits answer to detected submit URL
|
| 224 |
+
8. **Recursive Solving**: If response contains next URL, solves recursively
|
| 225 |
+
9. **Response**: Returns final result
|
| 226 |
+
|
| 227 |
+
## Solver Strategies
|
| 228 |
+
|
| 229 |
+
The solver uses multiple strategies in order:
|
| 230 |
+
|
| 231 |
+
1. **Direct Answer Extraction**: Checks if answer is already in page
|
| 232 |
+
2. **Data File Processing**: Downloads and processes CSV, JSON, PDF files
|
| 233 |
+
3. **LLM Reasoning**: Uses GPT-4o-mini (OpenAI) or GPT-5-nano (OpenRouter) for complex questions
|
| 234 |
+
4. **Fallback**: Returns question analysis if all else fails
|
| 235 |
+
|
| 236 |
+
## Error Handling
|
| 237 |
+
|
| 238 |
+
- Invalid JSON → 400 Bad Request
|
| 239 |
+
- Wrong secret → 403 Forbidden
|
| 240 |
+
- Page load errors → 500 with error details
|
| 241 |
+
- Timeout (>3 min) → 504 Gateway Timeout
|
| 242 |
+
- All errors are logged for debugging
|
| 243 |
+
|
| 244 |
+
## Limitations
|
| 245 |
+
|
| 246 |
+
- Maximum recursion depth: 10 quizzes
|
| 247 |
+
- Timeout: 3 minutes per request
|
| 248 |
+
- Requires internet connection for external URLs
|
| 249 |
+
- OpenAI API key needed for LLM features (optional)
|
| 250 |
+
|
| 251 |
+
## License
|
| 252 |
+
|
| 253 |
+
MIT License - see LICENSE file for details.
|
| 254 |
+
|
| 255 |
+
## Contributing
|
| 256 |
+
|
| 257 |
+
1. Fork the repository
|
| 258 |
+
2. Create a feature branch
|
| 259 |
+
3. Make your changes
|
| 260 |
+
4. Submit a pull request
|
| 261 |
+
|
| 262 |
+
## Support
|
| 263 |
+
|
| 264 |
+
For issues and questions, please open an issue on the repository.
|
| 265 |
+
|
SET_API_KEY.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Setting LLM API Keys
|
| 2 |
+
|
| 3 |
+
You can use either OpenAI or OpenRouter (or both) for LLM reasoning. Set whichever keys you have.
|
| 4 |
+
|
| 5 |
+
## Option A: OpenRouter (GPT-5-nano, etc.)
|
| 6 |
+
|
| 7 |
+
### Quick Setup (Current Session)
|
| 8 |
+
```powershell
|
| 9 |
+
$env:OPENROUTER_API_KEY = "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"
|
| 10 |
+
$env:OPENROUTER_MODEL = "gpt-5-nano" # optional override
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
### Make It Permanent
|
| 14 |
+
```powershell
|
| 15 |
+
[System.Environment]::SetEnvironmentVariable("OPENROUTER_API_KEY", "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", "User")
|
| 16 |
+
[System.Environment]::SetEnvironmentVariable("OPENROUTER_MODEL", "gpt-5-nano", "User")
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### .env Example
|
| 20 |
+
```env
|
| 21 |
+
OPENROUTER_API_KEY=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
|
| 22 |
+
OPENROUTER_MODEL=gpt-5-nano
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### Hugging Face Spaces
|
| 26 |
+
- `OPENROUTER_API_KEY`: `sk-or-...`
|
| 27 |
+
- `OPENROUTER_MODEL`: `gpt-5-nano` (optional, defaults to this)
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## Option B: OpenAI (GPT-4o, etc.)
|
| 32 |
+
|
| 33 |
+
### Quick Setup
|
| 34 |
+
```powershell
|
| 35 |
+
$env:OPENAI_API_KEY = "sk-your-openai-api-key"
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
Or run:
|
| 39 |
+
```powershell
|
| 40 |
+
.\set_api_key.ps1
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### Make It Permanent
|
| 44 |
+
```powershell
|
| 45 |
+
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "sk-your-openai-api-key", "User")
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### .env Example
|
| 49 |
+
```env
|
| 50 |
+
OPENAI_API_KEY=sk-your-openai-api-key
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Hugging Face Spaces
|
| 54 |
+
- `OPENAI_API_KEY`: `sk-...`
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## Verification
|
| 59 |
+
|
| 60 |
+
Run:
|
| 61 |
+
```powershell
|
| 62 |
+
python test_env.json.py
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
You should see at least one of the LLM keys marked as `set`, and `llm_enabled: true`.
|
| 66 |
+
|
START_SERVER.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# How to Start the Server
|
| 2 |
+
|
| 3 |
+
## The Problem
|
| 4 |
+
|
| 5 |
+
The "Not Found" error means the server is **not running**. You need to start it first.
|
| 6 |
+
|
| 7 |
+
## Solution: Start the Server
|
| 8 |
+
|
| 9 |
+
### Step 1: Open a PowerShell Terminal
|
| 10 |
+
|
| 11 |
+
Make sure you're in the project directory:
|
| 12 |
+
```powershell
|
| 13 |
+
cd C:\Users\tarun\IITMTdsPrj2
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
### Step 2: Set Environment Variables
|
| 17 |
+
|
| 18 |
+
```powershell
|
| 19 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 20 |
+
$env:OPENAI_API_KEY = "eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6IjI0ZjIwMDU3NTNAZHMuc3R1ZHkuaWl0bS5hYy5pbiJ9.eji7L5I62M9YHoeEKE8ao6eTw8dFjgDIMP9C3lOUXc4"
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
### Step 3: Start the Server
|
| 24 |
+
|
| 25 |
+
```powershell
|
| 26 |
+
python -m app.main
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
You should see:
|
| 30 |
+
```
|
| 31 |
+
INFO: Started server process [...]
|
| 32 |
+
INFO: Waiting for application startup.
|
| 33 |
+
INFO: Application startup complete.
|
| 34 |
+
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
**Keep this terminal open!** The server runs in this window.
|
| 38 |
+
|
| 39 |
+
### Step 4: Test in Another Terminal
|
| 40 |
+
|
| 41 |
+
Open a **NEW PowerShell window** and run:
|
| 42 |
+
|
| 43 |
+
```powershell
|
| 44 |
+
# Set variables again (in the new window)
|
| 45 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 46 |
+
|
| 47 |
+
# Test the API
|
| 48 |
+
$body = @{
|
| 49 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 50 |
+
secret = "EasyQuiz"
|
| 51 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 52 |
+
} | ConvertTo-Json
|
| 53 |
+
|
| 54 |
+
Invoke-RestMethod -Uri "http://127.0.0.1:8000/demo" -Method POST -ContentType "application/json" -Body $body
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Quick Test Script
|
| 58 |
+
|
| 59 |
+
I've created a test script. After starting the server, run:
|
| 60 |
+
|
| 61 |
+
```powershell
|
| 62 |
+
.\test_server.ps1
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
This will test all endpoints and show you what's working.
|
| 66 |
+
|
| 67 |
+
## Troubleshooting
|
| 68 |
+
|
| 69 |
+
### "Module not found" error
|
| 70 |
+
```powershell
|
| 71 |
+
pip install -r requirements.txt
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
### "Port already in use" error
|
| 75 |
+
Another process is using port 8000. Either:
|
| 76 |
+
- Stop the other process
|
| 77 |
+
- Change the port: `$env:PORT = "8001"` then restart
|
| 78 |
+
|
| 79 |
+
### Server starts but endpoints don't work
|
| 80 |
+
- Make sure you're using the correct URL: `http://127.0.0.1:8000` or `http://localhost:8000`
|
| 81 |
+
- Check for errors in the server terminal
|
| 82 |
+
- Restart the server after code changes
|
| 83 |
+
|
| 84 |
+
## Summary
|
| 85 |
+
|
| 86 |
+
1. ✅ Start server: `python -m app.main` (keep terminal open)
|
| 87 |
+
2. ✅ Open new terminal for testing
|
| 88 |
+
3. ✅ Set variables in new terminal
|
| 89 |
+
4. ✅ Test API endpoints
|
| 90 |
+
|
TEST_API.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# How to Check Environment Variables (JSON Format)
|
| 2 |
+
|
| 3 |
+
## Method 1: Using the Test Script (Easiest)
|
| 4 |
+
|
| 5 |
+
Run this command:
|
| 6 |
+
```bash
|
| 7 |
+
python test_env.json.py
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
**Example Output:**
|
| 11 |
+
```json
|
| 12 |
+
{
|
| 13 |
+
"status": "ok",
|
| 14 |
+
"variables": {
|
| 15 |
+
"QUIZ_SECRET": {
|
| 16 |
+
"set": true,
|
| 17 |
+
"length": 8,
|
| 18 |
+
"preview": "Easy...Quiz"
|
| 19 |
+
},
|
| 20 |
+
"OPENAI_API_KEY": {
|
| 21 |
+
"set": true,
|
| 22 |
+
"length": 200,
|
| 23 |
+
"preview": "sk-proj...4UA",
|
| 24 |
+
"valid_format": true
|
| 25 |
+
},
|
| 26 |
+
"PORT": {
|
| 27 |
+
"set": true,
|
| 28 |
+
"value": "8000"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"ready": true,
|
| 32 |
+
"llm_enabled": true,
|
| 33 |
+
"message": "Ready to run"
|
| 34 |
+
}
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
## Method 2: Using the API Endpoint
|
| 38 |
+
|
| 39 |
+
### Step 1: Start the Server
|
| 40 |
+
```bash
|
| 41 |
+
python -m app.main
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### Step 2: Check Environment Variables
|
| 45 |
+
|
| 46 |
+
**Using curl:**
|
| 47 |
+
```bash
|
| 48 |
+
curl http://localhost:8000/env-check
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
**Using PowerShell:**
|
| 52 |
+
```powershell
|
| 53 |
+
Invoke-RestMethod -Uri http://localhost:8000/env-check -Method Get | ConvertTo-Json
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
**Using Python:**
|
| 57 |
+
```python
|
| 58 |
+
import requests
|
| 59 |
+
response = requests.get("http://localhost:8000/env-check")
|
| 60 |
+
print(response.json())
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
**Example JSON Response:**
|
| 64 |
+
```json
|
| 65 |
+
{
|
| 66 |
+
"status": "ok",
|
| 67 |
+
"variables": {
|
| 68 |
+
"QUIZ_SECRET": {
|
| 69 |
+
"set": true,
|
| 70 |
+
"length": 8,
|
| 71 |
+
"preview": "Easy...Quiz"
|
| 72 |
+
},
|
| 73 |
+
"OPENAI_API_KEY": {
|
| 74 |
+
"set": true,
|
| 75 |
+
"length": 200,
|
| 76 |
+
"preview": "sk-proj...4UA",
|
| 77 |
+
"valid_format": true
|
| 78 |
+
},
|
| 79 |
+
"PORT": {
|
| 80 |
+
"set": true,
|
| 81 |
+
"value": "8000"
|
| 82 |
+
}
|
| 83 |
+
},
|
| 84 |
+
"ready": true,
|
| 85 |
+
"llm_enabled": true
|
| 86 |
+
}
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## Method 3: Test the Full API
|
| 90 |
+
|
| 91 |
+
### Health Check
|
| 92 |
+
```bash
|
| 93 |
+
curl http://localhost:8000/health
|
| 94 |
+
```
|
| 95 |
+
**Response:**
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"status": "healthy"
|
| 99 |
+
}
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### Test Quiz Endpoint (with wrong secret - should return 403)
|
| 103 |
+
```bash
|
| 104 |
+
curl -X POST http://localhost:8000/solve \
|
| 105 |
+
-H "Content-Type: application/json" \
|
| 106 |
+
-d '{
|
| 107 |
+
"email": "test@example.com",
|
| 108 |
+
"secret": "wrong_secret",
|
| 109 |
+
"url": "https://example.com"
|
| 110 |
+
}'
|
| 111 |
+
```
|
| 112 |
+
**Expected Response (403):**
|
| 113 |
+
```json
|
| 114 |
+
{
|
| 115 |
+
"detail": {
|
| 116 |
+
"error": "forbidden"
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### Test Quiz Endpoint (with correct secret)
|
| 122 |
+
```bash
|
| 123 |
+
curl -X POST http://localhost:8000/solve \
|
| 124 |
+
-H "Content-Type: application/json" \
|
| 125 |
+
-d '{
|
| 126 |
+
"email": "test@example.com",
|
| 127 |
+
"secret": "EasyQuiz",
|
| 128 |
+
"url": "https://example.com/quiz"
|
| 129 |
+
}'
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## Method 4: Quick PowerShell Test
|
| 133 |
+
|
| 134 |
+
```powershell
|
| 135 |
+
# Check variables directly
|
| 136 |
+
$env:QUIZ_SECRET
|
| 137 |
+
$env:OPENAI_API_KEY
|
| 138 |
+
|
| 139 |
+
# Test with API (if server is running)
|
| 140 |
+
$response = Invoke-RestMethod -Uri http://localhost:8000/env-check
|
| 141 |
+
$response | ConvertTo-Json -Depth 10
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
## JSON Response Fields Explained
|
| 145 |
+
|
| 146 |
+
| Field | Description |
|
| 147 |
+
|-------|-------------|
|
| 148 |
+
| `status` | Always "ok" if script runs |
|
| 149 |
+
| `variables.QUIZ_SECRET.set` | `true` if QUIZ_SECRET is set |
|
| 150 |
+
| `variables.QUIZ_SECRET.length` | Length of the secret |
|
| 151 |
+
| `variables.QUIZ_SECRET.preview` | Masked preview (first 4 + last 4 chars) |
|
| 152 |
+
| `variables.OPENAI_API_KEY.set` | `true` if API key is set |
|
| 153 |
+
| `variables.OPENAI_API_KEY.valid_format` | `true` if key starts with "sk-" |
|
| 154 |
+
| `ready` | `true` if QUIZ_SECRET is set (required) |
|
| 155 |
+
| `llm_enabled` | `true` if OPENAI_API_KEY is set (optional) |
|
| 156 |
+
| `message` | Human-readable status message |
|
| 157 |
+
|
| 158 |
+
## Quick Test Commands
|
| 159 |
+
|
| 160 |
+
### All-in-One Test (PowerShell)
|
| 161 |
+
```powershell
|
| 162 |
+
# 1. Check variables
|
| 163 |
+
python test_env.json.py
|
| 164 |
+
|
| 165 |
+
# 2. Start server (in another terminal)
|
| 166 |
+
python -m app.main
|
| 167 |
+
|
| 168 |
+
# 3. Test API (in another terminal)
|
| 169 |
+
Invoke-RestMethod -Uri http://localhost:8000/env-check | ConvertTo-Json
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### All-in-One Test (Bash/Linux)
|
| 173 |
+
```bash
|
| 174 |
+
# 1. Check variables
|
| 175 |
+
python test_env.json.py
|
| 176 |
+
|
| 177 |
+
# 2. Start server (in another terminal)
|
| 178 |
+
python -m app.main
|
| 179 |
+
|
| 180 |
+
# 3. Test API (in another terminal)
|
| 181 |
+
curl http://localhost:8000/env-check | python -m json.tool
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
## Expected Results
|
| 185 |
+
|
| 186 |
+
### ✅ Everything Working:
|
| 187 |
+
```json
|
| 188 |
+
{
|
| 189 |
+
"ready": true,
|
| 190 |
+
"llm_enabled": true,
|
| 191 |
+
"variables": {
|
| 192 |
+
"QUIZ_SECRET": { "set": true },
|
| 193 |
+
"OPENAI_API_KEY": { "set": true, "valid_format": true }
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
### ⚠️ Missing QUIZ_SECRET:
|
| 199 |
+
```json
|
| 200 |
+
{
|
| 201 |
+
"ready": false,
|
| 202 |
+
"llm_enabled": true,
|
| 203 |
+
"variables": {
|
| 204 |
+
"QUIZ_SECRET": { "set": false },
|
| 205 |
+
"OPENAI_API_KEY": { "set": true }
|
| 206 |
+
},
|
| 207 |
+
"message": "QUIZ_SECRET is required"
|
| 208 |
+
}
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
### ⚠️ Missing OPENAI_API_KEY (still works, but no LLM):
|
| 212 |
+
```json
|
| 213 |
+
{
|
| 214 |
+
"ready": true,
|
| 215 |
+
"llm_enabled": false,
|
| 216 |
+
"variables": {
|
| 217 |
+
"QUIZ_SECRET": { "set": true },
|
| 218 |
+
"OPENAI_API_KEY": { "set": false }
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
```
|
| 222 |
+
|
TEST_POWERSHELL.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Testing the API with PowerShell
|
| 2 |
+
|
| 3 |
+
## The Problem
|
| 4 |
+
|
| 5 |
+
The error `D1_TYPE_ERROR: Type 'object' not supported for value '[object Object]'` occurs because PowerShell needs the JSON body to be properly formatted.
|
| 6 |
+
|
| 7 |
+
## Solution: Correct PowerShell Syntax
|
| 8 |
+
|
| 9 |
+
### Method 1: Using PowerShell Object (Recommended)
|
| 10 |
+
|
| 11 |
+
```powershell
|
| 12 |
+
# Create body as PowerShell object, then convert to JSON
|
| 13 |
+
$body = @{
|
| 14 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 15 |
+
secret = "EasyQuiz"
|
| 16 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 17 |
+
} | ConvertTo-Json
|
| 18 |
+
|
| 19 |
+
# Send request
|
| 20 |
+
Invoke-RestMethod `
|
| 21 |
+
-Uri "http://127.0.0.1:8000/solve" `
|
| 22 |
+
-Method POST `
|
| 23 |
+
-ContentType "application/json" `
|
| 24 |
+
-Body $body
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### Method 2: Using Here-String (Alternative)
|
| 28 |
+
|
| 29 |
+
```powershell
|
| 30 |
+
$body = @'
|
| 31 |
+
{
|
| 32 |
+
"email": "24f2005753@ds.study.iitm.ac.in",
|
| 33 |
+
"secret": "EasyQuiz",
|
| 34 |
+
"url": "https://tds-llm-analysis.s-anand.net/demo"
|
| 35 |
+
}
|
| 36 |
+
'@
|
| 37 |
+
|
| 38 |
+
Invoke-RestMethod `
|
| 39 |
+
-Uri "http://127.0.0.1:8000/solve" `
|
| 40 |
+
-Method POST `
|
| 41 |
+
-ContentType "application/json" `
|
| 42 |
+
-Body $body
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### Method 3: Using the Test Script
|
| 46 |
+
|
| 47 |
+
Run the provided test script:
|
| 48 |
+
|
| 49 |
+
```powershell
|
| 50 |
+
.\test_api.ps1
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Common Issues and Fixes
|
| 54 |
+
|
| 55 |
+
### Issue 1: JSON String Not Properly Formatted
|
| 56 |
+
|
| 57 |
+
**Wrong:**
|
| 58 |
+
```powershell
|
| 59 |
+
-Body '{
|
| 60 |
+
"email": "...",
|
| 61 |
+
"secret": "...",
|
| 62 |
+
"url": "..."
|
| 63 |
+
}'
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
**Right:**
|
| 67 |
+
```powershell
|
| 68 |
+
$body = @{
|
| 69 |
+
email = "..."
|
| 70 |
+
secret = "..."
|
| 71 |
+
url = "..."
|
| 72 |
+
} | ConvertTo-Json
|
| 73 |
+
|
| 74 |
+
-Body $body
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
### Issue 2: Server Not Running
|
| 78 |
+
|
| 79 |
+
Make sure the server is running:
|
| 80 |
+
```powershell
|
| 81 |
+
python -m app.main
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### Issue 3: Wrong Endpoint
|
| 85 |
+
|
| 86 |
+
Try the `/demo` endpoint instead:
|
| 87 |
+
```powershell
|
| 88 |
+
$body = @{
|
| 89 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 90 |
+
secret = "EasyQuiz"
|
| 91 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 92 |
+
} | ConvertTo-Json
|
| 93 |
+
|
| 94 |
+
Invoke-RestMethod `
|
| 95 |
+
-Uri "http://127.0.0.1:8000/demo" `
|
| 96 |
+
-Method POST `
|
| 97 |
+
-ContentType "application/json" `
|
| 98 |
+
-Body $body
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Complete Example
|
| 102 |
+
|
| 103 |
+
```powershell
|
| 104 |
+
# Step 1: Set environment variables
|
| 105 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 106 |
+
|
| 107 |
+
# Step 2: Start server (in one terminal)
|
| 108 |
+
# python -m app.main
|
| 109 |
+
|
| 110 |
+
# Step 3: Test API (in another terminal)
|
| 111 |
+
$body = @{
|
| 112 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 113 |
+
secret = "EasyQuiz"
|
| 114 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 115 |
+
} | ConvertTo-Json
|
| 116 |
+
|
| 117 |
+
$response = Invoke-RestMethod `
|
| 118 |
+
-Uri "http://127.0.0.1:8000/demo" `
|
| 119 |
+
-Method POST `
|
| 120 |
+
-ContentType "application/json" `
|
| 121 |
+
-Body $body
|
| 122 |
+
|
| 123 |
+
# Display response
|
| 124 |
+
$response | ConvertTo-Json -Depth 10
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
## Using curl (Alternative)
|
| 128 |
+
|
| 129 |
+
If PowerShell gives you trouble, use curl:
|
| 130 |
+
|
| 131 |
+
```powershell
|
| 132 |
+
curl.exe -X POST http://127.0.0.1:8000/demo `
|
| 133 |
+
-H "Content-Type: application/json" `
|
| 134 |
+
-d '{\"email\":\"24f2005753@ds.study.iitm.ac.in\",\"secret\":\"EasyQuiz\",\"url\":\"https://tds-llm-analysis.s-anand.net/demo\"}'
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
Or save JSON to a file:
|
| 138 |
+
|
| 139 |
+
```powershell
|
| 140 |
+
# Create request.json
|
| 141 |
+
@'
|
| 142 |
+
{
|
| 143 |
+
"email": "24f2005753@ds.study.iitm.ac.in",
|
| 144 |
+
"secret": "EasyQuiz",
|
| 145 |
+
"url": "https://tds-llm-analysis.s-anand.net/demo"
|
| 146 |
+
}
|
| 147 |
+
'@ | Out-File -FilePath request.json -Encoding utf8
|
| 148 |
+
|
| 149 |
+
# Send request
|
| 150 |
+
curl.exe -X POST http://127.0.0.1:8000/demo `
|
| 151 |
+
-H "Content-Type: application/json" `
|
| 152 |
+
-d "@request.json"
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
## Error Handling
|
| 156 |
+
|
| 157 |
+
```powershell
|
| 158 |
+
try {
|
| 159 |
+
$response = Invoke-RestMethod `
|
| 160 |
+
-Uri "http://127.0.0.1:8000/solve" `
|
| 161 |
+
-Method POST `
|
| 162 |
+
-ContentType "application/json" `
|
| 163 |
+
-Body $body
|
| 164 |
+
|
| 165 |
+
$response | ConvertTo-Json -Depth 10
|
| 166 |
+
}
|
| 167 |
+
catch {
|
| 168 |
+
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
|
| 169 |
+
if ($_.ErrorDetails.Message) {
|
| 170 |
+
Write-Host $_.ErrorDetails.Message -ForegroundColor Yellow
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
|
__init__.cpython-311.pyc
ADDED
|
Binary file (171 Bytes). View file
|
|
|
__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IITM LLM Quiz Solver
|
| 2 |
+
__version__ = "1.0.0"
|
| 3 |
+
|
browser.cpython-311.pyc
ADDED
|
Binary file (12.5 kB). View file
|
|
|
browser.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Playwright browser helper for loading and interacting with quiz pages.
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Optional, Dict, Any, List
|
| 7 |
+
from playwright.async_api import async_playwright, Browser, Page, BrowserContext
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class BrowserHelper:
|
| 14 |
+
"""Helper class for managing Playwright browser sessions."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
self.browser: Optional[Browser] = None
|
| 18 |
+
self.context: Optional[BrowserContext] = None
|
| 19 |
+
self.page: Optional[Page] = None
|
| 20 |
+
self.playwright = None
|
| 21 |
+
|
| 22 |
+
async def start(self, headless: bool = True) -> None:
|
| 23 |
+
"""
|
| 24 |
+
Start Playwright browser.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
headless: Run in headless mode
|
| 28 |
+
"""
|
| 29 |
+
try:
|
| 30 |
+
self.playwright = await async_playwright().start()
|
| 31 |
+
self.browser = await self.playwright.chromium.launch(
|
| 32 |
+
headless=headless,
|
| 33 |
+
args=[
|
| 34 |
+
'--no-sandbox',
|
| 35 |
+
'--disable-setuid-sandbox',
|
| 36 |
+
'--disable-dev-shm-usage',
|
| 37 |
+
'--disable-accelerated-2d-canvas',
|
| 38 |
+
'--disable-gpu'
|
| 39 |
+
]
|
| 40 |
+
)
|
| 41 |
+
self.context = await self.browser.new_context(
|
| 42 |
+
viewport={'width': 1920, 'height': 1080},
|
| 43 |
+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
| 44 |
+
)
|
| 45 |
+
self.page = await self.context.new_page()
|
| 46 |
+
logger.info("Browser started successfully")
|
| 47 |
+
except Exception as e:
|
| 48 |
+
logger.error(f"Error starting browser: {e}")
|
| 49 |
+
raise
|
| 50 |
+
|
| 51 |
+
async def load_page(self, url: str, wait_time: int = 5, timeout: int = 30000) -> Dict[str, Any]:
|
| 52 |
+
"""
|
| 53 |
+
Load a page and extract all content.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
url: URL to load
|
| 57 |
+
wait_time: Seconds to wait for page to load
|
| 58 |
+
timeout: Page load timeout in milliseconds
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Dictionary with page content
|
| 62 |
+
"""
|
| 63 |
+
if not self.page:
|
| 64 |
+
await self.start()
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
logger.info(f"Loading page: {url}")
|
| 68 |
+
await self.page.goto(url, wait_until='networkidle', timeout=timeout)
|
| 69 |
+
|
| 70 |
+
# Wait for dynamic content
|
| 71 |
+
await asyncio.sleep(wait_time)
|
| 72 |
+
|
| 73 |
+
# Extract page content
|
| 74 |
+
content = {
|
| 75 |
+
'url': url,
|
| 76 |
+
'title': await self.page.title(),
|
| 77 |
+
'text': await self.page.inner_text('body'),
|
| 78 |
+
'html': await self.page.content(),
|
| 79 |
+
'screenshot': await self.page.screenshot(full_page=True),
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# Try to extract all visible text elements
|
| 83 |
+
try:
|
| 84 |
+
content['all_text'] = await self.page.evaluate("""
|
| 85 |
+
() => {
|
| 86 |
+
const walker = document.createTreeWalker(
|
| 87 |
+
document.body,
|
| 88 |
+
NodeFilter.SHOW_TEXT,
|
| 89 |
+
null,
|
| 90 |
+
false
|
| 91 |
+
);
|
| 92 |
+
let text = [];
|
| 93 |
+
let node;
|
| 94 |
+
while (node = walker.nextNode()) {
|
| 95 |
+
if (node.textContent.trim()) {
|
| 96 |
+
text.push(node.textContent.trim());
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
return text.join('\\n');
|
| 100 |
+
}
|
| 101 |
+
""")
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.warning(f"Error extracting all text: {e}")
|
| 104 |
+
content['all_text'] = content['text']
|
| 105 |
+
|
| 106 |
+
# Extract links
|
| 107 |
+
try:
|
| 108 |
+
content['links'] = await self.page.evaluate("""
|
| 109 |
+
() => {
|
| 110 |
+
const links = Array.from(document.querySelectorAll('a[href]'));
|
| 111 |
+
return links.map(a => ({text: a.textContent.trim(), href: a.href}));
|
| 112 |
+
}
|
| 113 |
+
""")
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.warning(f"Error extracting links: {e}")
|
| 116 |
+
content['links'] = []
|
| 117 |
+
|
| 118 |
+
# Extract images
|
| 119 |
+
try:
|
| 120 |
+
content['images'] = await self.page.evaluate("""
|
| 121 |
+
() => {
|
| 122 |
+
const images = Array.from(document.querySelectorAll('img[src]'));
|
| 123 |
+
return images.map(img => ({alt: img.alt, src: img.src}));
|
| 124 |
+
}
|
| 125 |
+
""")
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.warning(f"Error extracting images: {e}")
|
| 128 |
+
content['images'] = []
|
| 129 |
+
|
| 130 |
+
logger.info(f"Page loaded successfully: {content['title']}")
|
| 131 |
+
return content
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
logger.error(f"Error loading page {url}: {e}")
|
| 135 |
+
raise
|
| 136 |
+
|
| 137 |
+
async def click_element(self, selector: str) -> bool:
|
| 138 |
+
"""
|
| 139 |
+
Click an element on the page.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
selector: CSS selector
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
True if successful
|
| 146 |
+
"""
|
| 147 |
+
try:
|
| 148 |
+
await self.page.click(selector)
|
| 149 |
+
await asyncio.sleep(1)
|
| 150 |
+
return True
|
| 151 |
+
except Exception as e:
|
| 152 |
+
logger.error(f"Error clicking element {selector}: {e}")
|
| 153 |
+
return False
|
| 154 |
+
|
| 155 |
+
async def fill_input(self, selector: str, value: str) -> bool:
|
| 156 |
+
"""
|
| 157 |
+
Fill an input field.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
selector: CSS selector
|
| 161 |
+
value: Value to fill
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
True if successful
|
| 165 |
+
"""
|
| 166 |
+
try:
|
| 167 |
+
await self.page.fill(selector, value)
|
| 168 |
+
return True
|
| 169 |
+
except Exception as e:
|
| 170 |
+
logger.error(f"Error filling input {selector}: {e}")
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
async def wait_for_element(self, selector: str, timeout: int = 10000) -> bool:
|
| 174 |
+
"""
|
| 175 |
+
Wait for an element to appear.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
selector: CSS selector
|
| 179 |
+
timeout: Timeout in milliseconds
|
| 180 |
+
|
| 181 |
+
Returns:
|
| 182 |
+
True if element found
|
| 183 |
+
"""
|
| 184 |
+
try:
|
| 185 |
+
await self.page.wait_for_selector(selector, timeout=timeout)
|
| 186 |
+
return True
|
| 187 |
+
except Exception as e:
|
| 188 |
+
logger.warning(f"Element {selector} not found: {e}")
|
| 189 |
+
return False
|
| 190 |
+
|
| 191 |
+
async def evaluate_script(self, script: str) -> Any:
|
| 192 |
+
"""
|
| 193 |
+
Execute JavaScript on the page.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
script: JavaScript code to execute
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
Result of script execution
|
| 200 |
+
"""
|
| 201 |
+
try:
|
| 202 |
+
return await self.page.evaluate(script)
|
| 203 |
+
except Exception as e:
|
| 204 |
+
logger.error(f"Error evaluating script: {e}")
|
| 205 |
+
return None
|
| 206 |
+
|
| 207 |
+
async def close(self) -> None:
|
| 208 |
+
"""Close browser and cleanup."""
|
| 209 |
+
try:
|
| 210 |
+
if self.page:
|
| 211 |
+
await self.page.close()
|
| 212 |
+
if self.context:
|
| 213 |
+
await self.context.close()
|
| 214 |
+
if self.browser:
|
| 215 |
+
await self.browser.close()
|
| 216 |
+
if self.playwright:
|
| 217 |
+
await self.playwright.stop()
|
| 218 |
+
logger.info("Browser closed")
|
| 219 |
+
except Exception as e:
|
| 220 |
+
logger.error(f"Error closing browser: {e}")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# Global browser instance
|
| 224 |
+
_browser: Optional[BrowserHelper] = None
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
async def get_browser() -> BrowserHelper:
|
| 228 |
+
"""
|
| 229 |
+
Get or create a browser instance.
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
BrowserHelper instance
|
| 233 |
+
"""
|
| 234 |
+
global _browser
|
| 235 |
+
if _browser is None:
|
| 236 |
+
_browser = BrowserHelper()
|
| 237 |
+
await _browser.start()
|
| 238 |
+
return _browser
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
async def cleanup_browser() -> None:
|
| 242 |
+
"""Cleanup browser instance."""
|
| 243 |
+
global _browser
|
| 244 |
+
if _browser:
|
| 245 |
+
await _browser.close()
|
| 246 |
+
_browser = None
|
| 247 |
+
|
check_and_start.ps1
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Script to check if server is running and help start it
|
| 2 |
+
# Usage: .\check_and_start.ps1
|
| 3 |
+
|
| 4 |
+
Write-Host "IITM LLM Quiz Solver - Server Check" -ForegroundColor Cyan
|
| 5 |
+
Write-Host "====================================" -ForegroundColor Cyan
|
| 6 |
+
Write-Host ""
|
| 7 |
+
|
| 8 |
+
# Check if port 8000 is in use
|
| 9 |
+
Write-Host "Checking if server is running on port 8000..." -ForegroundColor Yellow
|
| 10 |
+
$portCheck = Test-NetConnection -ComputerName localhost -Port 8000 -InformationLevel Quiet -WarningAction SilentlyContinue
|
| 11 |
+
|
| 12 |
+
if ($portCheck) {
|
| 13 |
+
Write-Host "✓ Port 8000 is open - server might be running" -ForegroundColor Green
|
| 14 |
+
Write-Host ""
|
| 15 |
+
Write-Host "Testing /health endpoint..." -ForegroundColor Yellow
|
| 16 |
+
try {
|
| 17 |
+
$health = Invoke-RestMethod -Uri "http://127.0.0.1:8000/health" -Method GET -TimeoutSec 2 -ErrorAction Stop
|
| 18 |
+
Write-Host "✓ Server is running and responding!" -ForegroundColor Green
|
| 19 |
+
Write-Host " Health status: $($health.status)" -ForegroundColor Gray
|
| 20 |
+
Write-Host ""
|
| 21 |
+
Write-Host "Testing /demo endpoint..." -ForegroundColor Yellow
|
| 22 |
+
Write-Host " (This will make a test request)" -ForegroundColor Gray
|
| 23 |
+
|
| 24 |
+
# Set variables
|
| 25 |
+
if (-not $env:QUIZ_SECRET) {
|
| 26 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
$body = @{
|
| 30 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 31 |
+
secret = "EasyQuiz"
|
| 32 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 33 |
+
} | ConvertTo-Json
|
| 34 |
+
|
| 35 |
+
try {
|
| 36 |
+
$response = Invoke-RestMethod `
|
| 37 |
+
-Uri "http://127.0.0.1:8000/demo" `
|
| 38 |
+
-Method POST `
|
| 39 |
+
-ContentType "application/json" `
|
| 40 |
+
-Body $body `
|
| 41 |
+
-TimeoutSec 30 `
|
| 42 |
+
-ErrorAction Stop
|
| 43 |
+
|
| 44 |
+
Write-Host "✓ /demo endpoint works!" -ForegroundColor Green
|
| 45 |
+
Write-Host " Response:" -ForegroundColor Gray
|
| 46 |
+
$response | ConvertTo-Json -Depth 10 | Write-Host
|
| 47 |
+
} catch {
|
| 48 |
+
Write-Host "✗ /demo endpoint failed" -ForegroundColor Red
|
| 49 |
+
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Yellow
|
| 50 |
+
if ($_.ErrorDetails.Message) {
|
| 51 |
+
Write-Host " Details: $($_.ErrorDetails.Message)" -ForegroundColor Yellow
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
} catch {
|
| 55 |
+
Write-Host "✗ Server is not responding correctly" -ForegroundColor Red
|
| 56 |
+
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Yellow
|
| 57 |
+
Write-Host ""
|
| 58 |
+
Write-Host "The port is open but the server might not be our FastAPI app." -ForegroundColor Yellow
|
| 59 |
+
Write-Host "Please check if you have the correct server running." -ForegroundColor Yellow
|
| 60 |
+
}
|
| 61 |
+
} else {
|
| 62 |
+
Write-Host "✗ Port 8000 is NOT open - server is NOT running" -ForegroundColor Red
|
| 63 |
+
Write-Host ""
|
| 64 |
+
Write-Host "To start the server:" -ForegroundColor Yellow
|
| 65 |
+
Write-Host " 1. Set environment variables:" -ForegroundColor Cyan
|
| 66 |
+
Write-Host " `$env:QUIZ_SECRET = 'EasyQuiz'" -ForegroundColor Gray
|
| 67 |
+
Write-Host " `$env:OPENAI_API_KEY = 'your-key-here'" -ForegroundColor Gray
|
| 68 |
+
Write-Host ""
|
| 69 |
+
Write-Host " 2. Start the server:" -ForegroundColor Cyan
|
| 70 |
+
Write-Host " python -m app.main" -ForegroundColor Gray
|
| 71 |
+
Write-Host ""
|
| 72 |
+
Write-Host " 3. Keep that terminal open and test in another terminal" -ForegroundColor Cyan
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
Write-Host ""
|
| 76 |
+
Write-Host "====================================" -ForegroundColor Cyan
|
| 77 |
+
|
| 78 |
+
|
check_env.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quick script to check if environment variables are set correctly.
|
| 3 |
+
Run: python check_env.py
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
def check_env():
|
| 9 |
+
"""Check and display environment variable status."""
|
| 10 |
+
print("=" * 50)
|
| 11 |
+
print("IITM LLM Quiz Solver - Environment Check")
|
| 12 |
+
print("=" * 50)
|
| 13 |
+
print()
|
| 14 |
+
|
| 15 |
+
# Check QUIZ_SECRET
|
| 16 |
+
quiz_secret = os.getenv("QUIZ_SECRET")
|
| 17 |
+
if quiz_secret:
|
| 18 |
+
# Show first and last 4 chars for security
|
| 19 |
+
masked = f"{quiz_secret[:4]}...{quiz_secret[-4:]}" if len(quiz_secret) > 8 else "***"
|
| 20 |
+
print(f"✓ QUIZ_SECRET: Set ({masked})")
|
| 21 |
+
print(f" Length: {len(quiz_secret)} characters")
|
| 22 |
+
else:
|
| 23 |
+
print("✗ QUIZ_SECRET: NOT SET (Required)")
|
| 24 |
+
print(" Set it with: export QUIZ_SECRET='your_secret' (Linux/Mac)")
|
| 25 |
+
print(" Or: $env:QUIZ_SECRET = 'your_secret' (Windows PowerShell)")
|
| 26 |
+
|
| 27 |
+
print()
|
| 28 |
+
|
| 29 |
+
# Check OpenAI key
|
| 30 |
+
openai_key = os.getenv("OPENAI_API_KEY")
|
| 31 |
+
if openai_key:
|
| 32 |
+
masked = f"{openai_key[:7]}...{openai_key[-4:]}" if len(openai_key) > 11 else "***"
|
| 33 |
+
print(f"✓ OPENAI_API_KEY: Set ({masked})")
|
| 34 |
+
print(f" Length: {len(openai_key)} characters")
|
| 35 |
+
if not openai_key.startswith("sk-"):
|
| 36 |
+
print(" ⚠ Warning: OpenAI keys usually start with 'sk-'")
|
| 37 |
+
else:
|
| 38 |
+
print("✗ OPENAI_API_KEY: NOT SET (Optional)")
|
| 39 |
+
print(" Set it with: export OPENAI_API_KEY='sk-your-key' (Linux/Mac)")
|
| 40 |
+
print(" Or: $env:OPENAI_API_KEY = 'sk-your-key' (Windows PowerShell)")
|
| 41 |
+
|
| 42 |
+
print()
|
| 43 |
+
|
| 44 |
+
# Check OpenRouter key
|
| 45 |
+
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
| 46 |
+
if openrouter_key:
|
| 47 |
+
masked = f"{openrouter_key[:6]}...{openrouter_key[-4:]}" if len(openrouter_key) > 10 else "***"
|
| 48 |
+
print(f"✓ OPENROUTER_API_KEY: Set ({masked})")
|
| 49 |
+
print(f" Length: {len(openrouter_key)} characters")
|
| 50 |
+
if not openrouter_key.startswith("sk-or-"):
|
| 51 |
+
print(" ⚠ Warning: OpenRouter keys usually start with 'sk-or-'")
|
| 52 |
+
model = os.getenv("OPENROUTER_MODEL", "gpt-5-nano")
|
| 53 |
+
print(f" Model: {model}")
|
| 54 |
+
else:
|
| 55 |
+
print("✗ OPENROUTER_API_KEY: NOT SET (Optional)")
|
| 56 |
+
print(" Set it with: export OPENROUTER_API_KEY='sk-or-your-key' (Linux/Mac)")
|
| 57 |
+
print(" Or: $env:OPENROUTER_API_KEY = 'sk-or-your-key' (Windows PowerShell)")
|
| 58 |
+
|
| 59 |
+
print()
|
| 60 |
+
|
| 61 |
+
# Check PORT (optional)
|
| 62 |
+
port = os.getenv("PORT", "8000")
|
| 63 |
+
print(f"PORT: {port} (default: 8000)")
|
| 64 |
+
|
| 65 |
+
print()
|
| 66 |
+
print("=" * 50)
|
| 67 |
+
|
| 68 |
+
# Summary
|
| 69 |
+
if quiz_secret:
|
| 70 |
+
print("✓ Status: Ready to run (QUIZ_SECRET is set)")
|
| 71 |
+
if openai_key or openrouter_key:
|
| 72 |
+
provider = []
|
| 73 |
+
if openai_key:
|
| 74 |
+
provider.append("OpenAI")
|
| 75 |
+
if openrouter_key:
|
| 76 |
+
provider.append("OpenRouter")
|
| 77 |
+
print(f"✓ LLM features: Enabled via {', '.join(provider)}")
|
| 78 |
+
else:
|
| 79 |
+
print("⚠ LLM features: Disabled (no LLM API keys set)")
|
| 80 |
+
else:
|
| 81 |
+
print("✗ Status: NOT READY - QUIZ_SECRET is required")
|
| 82 |
+
sys.exit(1)
|
| 83 |
+
|
| 84 |
+
print("=" * 50)
|
| 85 |
+
print()
|
| 86 |
+
print("To start the server, run:")
|
| 87 |
+
print(" python -m app.main")
|
| 88 |
+
print(" or")
|
| 89 |
+
print(" uvicorn app.main:app --host 0.0.0.0 --port 8000")
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
check_env()
|
| 93 |
+
|
llm.cpython-311.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
llm.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM helper module for OpenAI GPT integration.
|
| 3 |
+
Used for reasoning, OCR, and complex question parsing.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Optional, Dict, Any
|
| 8 |
+
import openai
|
| 9 |
+
from openai import AsyncOpenAI
|
| 10 |
+
import httpx
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Initialize OpenAI client
|
| 15 |
+
client: Optional[AsyncOpenAI] = None
|
| 16 |
+
|
| 17 |
+
# OpenRouter configuration
|
| 18 |
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
| 19 |
+
OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
|
| 20 |
+
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "gpt-5-nano")
|
| 21 |
+
OPENROUTER_SITE_URL = os.getenv("OPENROUTER_SITE_URL", "http://localhost")
|
| 22 |
+
OPENROUTER_APP_NAME = os.getenv("OPENROUTER_APP_NAME", "IITM LLM Quiz Solver")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def initialize_llm() -> None:
|
| 26 |
+
"""
|
| 27 |
+
Initialize OpenAI client with API key from environment.
|
| 28 |
+
"""
|
| 29 |
+
global client
|
| 30 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 31 |
+
if api_key:
|
| 32 |
+
client = AsyncOpenAI(api_key=api_key)
|
| 33 |
+
logger.info("OpenAI client initialized")
|
| 34 |
+
else:
|
| 35 |
+
if OPENROUTER_API_KEY:
|
| 36 |
+
logger.info("OPENAI_API_KEY not set, using OpenRouter only")
|
| 37 |
+
else:
|
| 38 |
+
logger.warning("No OPENAI_API_KEY or OPENROUTER_API_KEY set, LLM features will be disabled")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
async def ask_gpt(prompt: str, model: str = "gpt-4o-mini", max_tokens: int = 2000) -> Optional[str]:
|
| 42 |
+
"""
|
| 43 |
+
Query OpenAI GPT model with a prompt.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
prompt: The prompt/question to ask
|
| 47 |
+
model: Model to use (default: gpt-4o-mini)
|
| 48 |
+
max_tokens: Maximum tokens in response
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
Response text or None if error
|
| 52 |
+
"""
|
| 53 |
+
global client
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
if client:
|
| 57 |
+
response = await client.chat.completions.create(
|
| 58 |
+
model=model,
|
| 59 |
+
messages=[
|
| 60 |
+
{"role": "system", "content": "You are a helpful assistant that solves quiz questions accurately and concisely."},
|
| 61 |
+
{"role": "user", "content": prompt}
|
| 62 |
+
],
|
| 63 |
+
max_tokens=max_tokens,
|
| 64 |
+
temperature=0.3
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
answer = response.choices[0].message.content
|
| 68 |
+
logger.info(f"GPT response received (model: {model})")
|
| 69 |
+
return answer
|
| 70 |
+
else:
|
| 71 |
+
logger.warning("OpenAI client not initialized, attempting OpenRouter fallback")
|
| 72 |
+
return await ask_openrouter(prompt, max_tokens=max_tokens)
|
| 73 |
+
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logger.error(f"Error calling OpenAI API: {e}")
|
| 76 |
+
# Fallback to OpenRouter if configured
|
| 77 |
+
fallback = await ask_openrouter(prompt, max_tokens=max_tokens)
|
| 78 |
+
if fallback:
|
| 79 |
+
return fallback
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
async def ask_openrouter(prompt: str, model: Optional[str] = None, max_tokens: int = 2000) -> Optional[str]:
|
| 84 |
+
"""
|
| 85 |
+
Query OpenRouter (e.g., GPT-5-nano) with a prompt.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
prompt: Prompt text
|
| 89 |
+
model: Model to use (defaults to OPENROUTER_MODEL)
|
| 90 |
+
max_tokens: Maximum tokens
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
Response text or None
|
| 94 |
+
"""
|
| 95 |
+
if not OPENROUTER_API_KEY:
|
| 96 |
+
logger.warning("OPENROUTER_API_KEY not set, cannot call OpenRouter")
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
if not model:
|
| 100 |
+
model = OPENROUTER_MODEL
|
| 101 |
+
|
| 102 |
+
url = f"{OPENROUTER_BASE_URL.rstrip('/')}/chat/completions"
|
| 103 |
+
headers = {
|
| 104 |
+
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
| 105 |
+
"HTTP-Referer": OPENROUTER_SITE_URL,
|
| 106 |
+
"X-Title": OPENROUTER_APP_NAME,
|
| 107 |
+
"Content-Type": "application/json",
|
| 108 |
+
}
|
| 109 |
+
payload = {
|
| 110 |
+
"model": model,
|
| 111 |
+
"messages": [
|
| 112 |
+
{"role": "system", "content": "You are a helpful assistant that solves quiz questions accurately and concisely."},
|
| 113 |
+
{"role": "user", "content": prompt}
|
| 114 |
+
],
|
| 115 |
+
"max_tokens": max_tokens,
|
| 116 |
+
"temperature": 0.2
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
async with httpx.AsyncClient(timeout=60) as http_client:
|
| 121 |
+
response = await http_client.post(url, headers=headers, json=payload)
|
| 122 |
+
response.raise_for_status()
|
| 123 |
+
data = response.json()
|
| 124 |
+
answer = data["choices"][0]["message"]["content"]
|
| 125 |
+
logger.info(f"OpenRouter response received (model: {model})")
|
| 126 |
+
return answer
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.error(f"Error calling OpenRouter API: {e}")
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
async def parse_question_with_llm(question_text: str, context: str = "") -> Optional[Dict[str, Any]]:
|
| 133 |
+
"""
|
| 134 |
+
Use LLM to parse and understand a quiz question.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
question_text: The question text
|
| 138 |
+
context: Additional context from the page
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
Parsed question structure or None
|
| 142 |
+
"""
|
| 143 |
+
prompt = f"""Analyze this quiz question and provide a structured response:
|
| 144 |
+
|
| 145 |
+
Question: {question_text}
|
| 146 |
+
|
| 147 |
+
Context: {context}
|
| 148 |
+
|
| 149 |
+
Please identify:
|
| 150 |
+
1. What type of question is this? (scraping, calculation, API call, data analysis, etc.)
|
| 151 |
+
2. What data or resources are needed?
|
| 152 |
+
3. What is the expected answer format? (JSON, number, text, etc.)
|
| 153 |
+
|
| 154 |
+
Respond in JSON format:
|
| 155 |
+
{{
|
| 156 |
+
"type": "question_type",
|
| 157 |
+
"requirements": ["requirement1", "requirement2"],
|
| 158 |
+
"answer_format": "format_type",
|
| 159 |
+
"reasoning": "your reasoning"
|
| 160 |
+
}}
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
response = await ask_gpt(prompt)
|
| 164 |
+
if not response:
|
| 165 |
+
return None
|
| 166 |
+
|
| 167 |
+
# Try to extract JSON from response
|
| 168 |
+
import json
|
| 169 |
+
import re
|
| 170 |
+
|
| 171 |
+
json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', response, re.DOTALL)
|
| 172 |
+
if json_match:
|
| 173 |
+
try:
|
| 174 |
+
return json.loads(json_match.group())
|
| 175 |
+
except json.JSONDecodeError:
|
| 176 |
+
pass
|
| 177 |
+
|
| 178 |
+
return {"raw_response": response}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
async def solve_with_llm(question: str, available_data: Dict[str, Any]) -> Optional[str]:
|
| 182 |
+
"""
|
| 183 |
+
Use LLM to solve a quiz question.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
question: The question text
|
| 187 |
+
available_data: Any data extracted from the page
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
Answer or None
|
| 191 |
+
"""
|
| 192 |
+
prompt = f"""Solve this quiz question:
|
| 193 |
+
|
| 194 |
+
Question: {question}
|
| 195 |
+
|
| 196 |
+
Available Data:
|
| 197 |
+
{available_data}
|
| 198 |
+
|
| 199 |
+
Provide a clear, concise answer. If the answer should be in JSON format, provide valid JSON.
|
| 200 |
+
If it's a calculation, show your work briefly.
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
return await ask_gpt(prompt, max_tokens=3000)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
async def ocr_image_with_llm(image_base64: str) -> Optional[str]:
|
| 207 |
+
"""
|
| 208 |
+
Use GPT-4 Vision to extract text from an image.
|
| 209 |
+
|
| 210 |
+
Note: Requires GPT-4 Vision model (gpt-4o or gpt-4-vision-preview).
|
| 211 |
+
gpt-4o-mini does not support vision.
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
image_base64: Base64 encoded image
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
Extracted text or None
|
| 218 |
+
"""
|
| 219 |
+
global client
|
| 220 |
+
|
| 221 |
+
if not client:
|
| 222 |
+
return None
|
| 223 |
+
|
| 224 |
+
# Try vision-capable models
|
| 225 |
+
vision_models = ["gpt-4o", "gpt-4-vision-preview"]
|
| 226 |
+
|
| 227 |
+
for model in vision_models:
|
| 228 |
+
try:
|
| 229 |
+
response = await client.chat.completions.create(
|
| 230 |
+
model=model,
|
| 231 |
+
messages=[
|
| 232 |
+
{
|
| 233 |
+
"role": "user",
|
| 234 |
+
"content": [
|
| 235 |
+
{"type": "text", "text": "Extract all text from this image. Return only the text content."},
|
| 236 |
+
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}}
|
| 237 |
+
]
|
| 238 |
+
}
|
| 239 |
+
],
|
| 240 |
+
max_tokens=1000
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
return response.choices[0].message.content
|
| 244 |
+
|
| 245 |
+
except Exception as e:
|
| 246 |
+
logger.warning(f"Error with model {model}: {e}")
|
| 247 |
+
continue
|
| 248 |
+
|
| 249 |
+
logger.error("No vision-capable model available")
|
| 250 |
+
return None
|
| 251 |
+
|
main.cpython-311.pyc
ADDED
|
Binary file (11.3 kB). View file
|
|
|
main.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 21 |
+
from app.utils import validate_secret
|
| 22 |
+
from app.browser import cleanup_browser
|
| 23 |
+
|
| 24 |
+
# Configure logging
|
| 25 |
+
logging.basicConfig(
|
| 26 |
+
level=logging.INFO,
|
| 27 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 28 |
+
)
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
# Get secret from environment
|
| 32 |
+
EXPECTED_SECRET = os.getenv("QUIZ_SECRET", "default_secret_change_me")
|
| 33 |
+
|
| 34 |
+
# Lifespan context manager for startup and shutdown
|
| 35 |
+
from contextlib import asynccontextmanager
|
| 36 |
+
|
| 37 |
+
@asynccontextmanager
|
| 38 |
+
async def lifespan(app: FastAPI):
|
| 39 |
+
"""Lifespan context manager for startup and shutdown."""
|
| 40 |
+
# Startup
|
| 41 |
+
logger.info("Application starting up...")
|
| 42 |
+
yield
|
| 43 |
+
# Shutdown
|
| 44 |
+
logger.info("Shutting down, cleaning up browser...")
|
| 45 |
+
await cleanup_browser()
|
| 46 |
+
|
| 47 |
+
# Initialize FastAPI app with lifespan
|
| 48 |
+
app = FastAPI(
|
| 49 |
+
title="IITM LLM Quiz Solver",
|
| 50 |
+
description="API endpoint to automatically solve dynamic quiz tasks",
|
| 51 |
+
version="1.0.0",
|
| 52 |
+
lifespan=lifespan
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class QuizRequest(BaseModel):
|
| 57 |
+
"""Request model for quiz solving."""
|
| 58 |
+
email: str = Field(..., description="User email address")
|
| 59 |
+
secret: str = Field(..., description="Secret key for authentication")
|
| 60 |
+
url: str = Field(..., description="Quiz page URL")
|
| 61 |
+
|
| 62 |
+
@field_validator('email')
|
| 63 |
+
@classmethod
|
| 64 |
+
def validate_email(cls, v):
|
| 65 |
+
if not v or '@' not in v:
|
| 66 |
+
raise ValueError('Invalid email format')
|
| 67 |
+
return v
|
| 68 |
+
|
| 69 |
+
@field_validator('url')
|
| 70 |
+
@classmethod
|
| 71 |
+
def validate_url(cls, v):
|
| 72 |
+
if not v or not v.startswith(('http://', 'https://')):
|
| 73 |
+
raise ValueError('Invalid URL format')
|
| 74 |
+
return v
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.get("/")
|
| 78 |
+
async def root():
|
| 79 |
+
"""Root endpoint."""
|
| 80 |
+
return {
|
| 81 |
+
"message": "IITM LLM Quiz Solver API",
|
| 82 |
+
"version": "1.0.0",
|
| 83 |
+
"endpoints": {
|
| 84 |
+
"/solve": "POST - Solve a quiz",
|
| 85 |
+
"/health": "GET - Health check",
|
| 86 |
+
"/demo": "POST - Demo endpoint"
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@app.get("/health")
|
| 92 |
+
async def health_check():
|
| 93 |
+
"""Health check endpoint."""
|
| 94 |
+
return {"status": "healthy"}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@app.get("/env-check")
|
| 98 |
+
async def env_check():
|
| 99 |
+
"""
|
| 100 |
+
Check environment variables status (returns JSON).
|
| 101 |
+
Useful for verifying configuration.
|
| 102 |
+
"""
|
| 103 |
+
quiz_secret = os.getenv("QUIZ_SECRET")
|
| 104 |
+
openai_key = os.getenv("OPENAI_API_KEY")
|
| 105 |
+
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
| 106 |
+
port = os.getenv("PORT", "8000")
|
| 107 |
+
|
| 108 |
+
return {
|
| 109 |
+
"status": "ok",
|
| 110 |
+
"variables": {
|
| 111 |
+
"QUIZ_SECRET": {
|
| 112 |
+
"set": quiz_secret is not None,
|
| 113 |
+
"length": len(quiz_secret) if quiz_secret else 0,
|
| 114 |
+
"preview": f"{quiz_secret[:4]}...{quiz_secret[-4:]}" if quiz_secret and len(quiz_secret) > 8 else "***" if quiz_secret else None
|
| 115 |
+
},
|
| 116 |
+
"OPENAI_API_KEY": {
|
| 117 |
+
"set": openai_key is not None,
|
| 118 |
+
"length": len(openai_key) if openai_key else 0,
|
| 119 |
+
"preview": f"{openai_key[:7]}...{openai_key[-4:]}" if openai_key and len(openai_key) > 11 else "***" if openai_key else None,
|
| 120 |
+
"valid_format": openai_key.startswith("sk-") if openai_key else False
|
| 121 |
+
},
|
| 122 |
+
"OPENROUTER_API_KEY": {
|
| 123 |
+
"set": openrouter_key is not None,
|
| 124 |
+
"length": len(openrouter_key) if openrouter_key else 0,
|
| 125 |
+
"preview": f"{openrouter_key[:7]}...{openrouter_key[-4:]}" if openrouter_key and len(openrouter_key) > 11 else "***" if openrouter_key else None,
|
| 126 |
+
"valid_format": openrouter_key.startswith("sk-or-") if openrouter_key else False
|
| 127 |
+
},
|
| 128 |
+
"PORT": {
|
| 129 |
+
"set": True,
|
| 130 |
+
"value": port
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
"ready": quiz_secret is not None,
|
| 134 |
+
"llm_enabled": any([openai_key, openrouter_key])
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@app.post("/solve")
|
| 139 |
+
async def solve_quiz_endpoint(request: QuizRequest):
|
| 140 |
+
"""
|
| 141 |
+
Main endpoint to solve a quiz.
|
| 142 |
+
|
| 143 |
+
Validates secret and solves the quiz recursively.
|
| 144 |
+
"""
|
| 145 |
+
try:
|
| 146 |
+
# Validate secret
|
| 147 |
+
if not validate_secret(request.secret, EXPECTED_SECRET):
|
| 148 |
+
logger.warning(f"Invalid secret provided for email: {request.email}")
|
| 149 |
+
raise HTTPException(
|
| 150 |
+
status_code=403,
|
| 151 |
+
detail={"error": "forbidden"}
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
logger.info(f"Solving quiz for {request.email} at {request.url}")
|
| 155 |
+
|
| 156 |
+
# Solve quiz with timeout
|
| 157 |
+
try:
|
| 158 |
+
result = await asyncio.wait_for(
|
| 159 |
+
solve_quiz(request.url, request.email, request.secret),
|
| 160 |
+
timeout=180.0 # 3 minutes
|
| 161 |
+
)
|
| 162 |
+
return result
|
| 163 |
+
except asyncio.TimeoutError:
|
| 164 |
+
logger.error("Quiz solving timed out")
|
| 165 |
+
raise HTTPException(
|
| 166 |
+
status_code=504,
|
| 167 |
+
detail={"error": "Request timeout - quiz solving took too long"}
|
| 168 |
+
)
|
| 169 |
+
except Exception as e:
|
| 170 |
+
logger.error(f"Error solving quiz: {e}", exc_info=True)
|
| 171 |
+
raise HTTPException(
|
| 172 |
+
status_code=500,
|
| 173 |
+
detail={"error": str(e)}
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
except HTTPException:
|
| 177 |
+
raise
|
| 178 |
+
except ValueError as e:
|
| 179 |
+
logger.error(f"Validation error: {e}")
|
| 180 |
+
raise HTTPException(
|
| 181 |
+
status_code=400,
|
| 182 |
+
detail={"error": "Invalid request format", "message": str(e)}
|
| 183 |
+
)
|
| 184 |
+
except Exception as e:
|
| 185 |
+
logger.error(f"Unexpected error: {e}", exc_info=True)
|
| 186 |
+
raise HTTPException(
|
| 187 |
+
status_code=500,
|
| 188 |
+
detail={"error": "Internal server error", "message": str(e)}
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@app.post("/demo")
|
| 193 |
+
async def demo_endpoint(request: QuizRequest):
|
| 194 |
+
"""
|
| 195 |
+
Demo endpoint for testing.
|
| 196 |
+
|
| 197 |
+
Same as /solve but with more lenient error handling.
|
| 198 |
+
"""
|
| 199 |
+
try:
|
| 200 |
+
# Validate secret (can be more lenient for demo)
|
| 201 |
+
if not validate_secret(request.secret, EXPECTED_SECRET):
|
| 202 |
+
logger.warning(f"Invalid secret in demo request")
|
| 203 |
+
return JSONResponse(
|
| 204 |
+
status_code=403,
|
| 205 |
+
content={"error": "forbidden"}
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
logger.info(f"Demo: Solving quiz for {request.email} at {request.url}")
|
| 209 |
+
|
| 210 |
+
# Solve quiz
|
| 211 |
+
try:
|
| 212 |
+
result = await asyncio.wait_for(
|
| 213 |
+
solve_quiz(request.url, request.email, request.secret),
|
| 214 |
+
timeout=180.0
|
| 215 |
+
)
|
| 216 |
+
return result
|
| 217 |
+
except asyncio.TimeoutError:
|
| 218 |
+
return JSONResponse(
|
| 219 |
+
status_code=504,
|
| 220 |
+
content={"error": "Request timeout"}
|
| 221 |
+
)
|
| 222 |
+
except Exception as e:
|
| 223 |
+
logger.error(f"Error in demo: {e}", exc_info=True)
|
| 224 |
+
return JSONResponse(
|
| 225 |
+
status_code=500,
|
| 226 |
+
content={"error": str(e)}
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
except ValueError as e:
|
| 230 |
+
return JSONResponse(
|
| 231 |
+
status_code=400,
|
| 232 |
+
content={"error": "Invalid request format", "message": str(e)}
|
| 233 |
+
)
|
| 234 |
+
except Exception as e:
|
| 235 |
+
logger.error(f"Unexpected error in demo: {e}", exc_info=True)
|
| 236 |
+
return JSONResponse(
|
| 237 |
+
status_code=500,
|
| 238 |
+
content={"error": "Internal server error", "message": str(e)}
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
if __name__ == "__main__":
|
| 243 |
+
port = int(os.getenv("PORT", 8000))
|
| 244 |
+
uvicorn.run(
|
| 245 |
+
"app.main:app",
|
| 246 |
+
host="0.0.0.0",
|
| 247 |
+
port=port,
|
| 248 |
+
log_level="info"
|
| 249 |
+
)
|
| 250 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
PyPDF2==3.0.1
|
| 8 |
+
pdfplumber==0.10.3
|
| 9 |
+
openai==1.3.5
|
| 10 |
+
pydantic==2.5.0
|
| 11 |
+
lxml==4.9.3
|
| 12 |
+
html5lib==1.1
|
| 13 |
+
python-dotenv==1.0.0
|
| 14 |
+
|
restart_server.ps1
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Script to stop any server on port 8000 and start the correct one
|
| 2 |
+
# Usage: .\restart_server.ps1
|
| 3 |
+
|
| 4 |
+
Write-Host "IITM LLM Quiz Solver - Server Restart" -ForegroundColor Cyan
|
| 5 |
+
Write-Host "=====================================" -ForegroundColor Cyan
|
| 6 |
+
Write-Host ""
|
| 7 |
+
|
| 8 |
+
# Find process using port 8000
|
| 9 |
+
Write-Host "Finding process using port 8000..." -ForegroundColor Yellow
|
| 10 |
+
$portInfo = netstat -ano | findstr :8000 | findstr LISTENING
|
| 11 |
+
if ($portInfo) {
|
| 12 |
+
$pid = ($portInfo -split '\s+')[-1]
|
| 13 |
+
Write-Host "Found process ID: $pid" -ForegroundColor Yellow
|
| 14 |
+
|
| 15 |
+
$process = Get-Process -Id $pid -ErrorAction SilentlyContinue
|
| 16 |
+
if ($process) {
|
| 17 |
+
Write-Host "Process: $($process.ProcessName) (PID: $pid)" -ForegroundColor Yellow
|
| 18 |
+
Write-Host ""
|
| 19 |
+
Write-Host "Stopping process..." -ForegroundColor Yellow
|
| 20 |
+
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
| 21 |
+
Start-Sleep -Seconds 2
|
| 22 |
+
Write-Host "✓ Process stopped" -ForegroundColor Green
|
| 23 |
+
}
|
| 24 |
+
} else {
|
| 25 |
+
Write-Host "No process found on port 8000" -ForegroundColor Green
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
Write-Host ""
|
| 29 |
+
Write-Host "Setting environment variables..." -ForegroundColor Yellow
|
| 30 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 31 |
+
$env:OPENAI_API_KEY = "eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6IjI0ZjIwMDU3NTNAZHMuc3R1ZHkuaWl0bS5hYy5pbiJ9.eji7L5I62M9YHoeEKE8ao6eTw8dFjgDIMP9C3lOUXc4"
|
| 32 |
+
Write-Host "✓ Environment variables set" -ForegroundColor Green
|
| 33 |
+
|
| 34 |
+
Write-Host ""
|
| 35 |
+
Write-Host "=====================================" -ForegroundColor Cyan
|
| 36 |
+
Write-Host "Now start the server manually:" -ForegroundColor Yellow
|
| 37 |
+
Write-Host " python -m app.main" -ForegroundColor Cyan
|
| 38 |
+
Write-Host ""
|
| 39 |
+
Write-Host "Keep that terminal open, then test in another terminal!" -ForegroundColor Yellow
|
| 40 |
+
|
| 41 |
+
|
set_api_key.ps1
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quick script to set OpenAI API key
|
| 2 |
+
# Usage: .\set_api_key.ps1
|
| 3 |
+
|
| 4 |
+
$apiKey = "eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6IjI0ZjIwMDU3NTNAZHMuc3R1ZHkuaWl0bS5hYy5pbiJ9.eji7L5I62M9YHoeEKE8ao6eTw8dFjgDIMP9C3lOUXc4"
|
| 5 |
+
|
| 6 |
+
Write-Host "Setting OPENAI_API_KEY for current session..." -ForegroundColor Cyan
|
| 7 |
+
$env:OPENAI_API_KEY = $apiKey
|
| 8 |
+
|
| 9 |
+
Write-Host "✓ OPENAI_API_KEY set" -ForegroundColor Green
|
| 10 |
+
Write-Host ""
|
| 11 |
+
Write-Host "To make it permanent, run:" -ForegroundColor Yellow
|
| 12 |
+
Write-Host '[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "' + $apiKey + '", "User")' -ForegroundColor Gray
|
| 13 |
+
Write-Host ""
|
| 14 |
+
Write-Host "⚠️ Note: This looks like a JWT token, not a standard OpenAI API key." -ForegroundColor Yellow
|
| 15 |
+
Write-Host " OpenAI keys usually start with 'sk-'. If LLM features don't work," -ForegroundColor Yellow
|
| 16 |
+
Write-Host " you may need to get your actual OpenAI API key from:" -ForegroundColor Yellow
|
| 17 |
+
Write-Host " https://platform.openai.com/api-keys" -ForegroundColor Cyan
|
| 18 |
+
Write-Host ""
|
| 19 |
+
Write-Host "To verify, run: python test_env.json.py" -ForegroundColor Cyan
|
| 20 |
+
|
setup_env.ps1
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quick Environment Setup Script for Windows PowerShell
|
| 2 |
+
# Usage: .\setup_env.ps1
|
| 3 |
+
|
| 4 |
+
Write-Host "IITM LLM Quiz Solver - Environment Setup" -ForegroundColor Cyan
|
| 5 |
+
Write-Host "========================================" -ForegroundColor Cyan
|
| 6 |
+
Write-Host ""
|
| 7 |
+
|
| 8 |
+
# Check if variables are already set
|
| 9 |
+
$quizSecret = $env:QUIZ_SECRET
|
| 10 |
+
$openaiKey = $env:OPENAI_API_KEY
|
| 11 |
+
$openrouterKey = $env:OPENROUTER_API_KEY
|
| 12 |
+
|
| 13 |
+
if ($quizSecret) {
|
| 14 |
+
Write-Host "QUIZ_SECRET is already set" -ForegroundColor Green
|
| 15 |
+
} else {
|
| 16 |
+
Write-Host "QUIZ_SECRET is not set" -ForegroundColor Yellow
|
| 17 |
+
$secret = Read-Host "Enter your QUIZ_SECRET (or press Enter to generate one)"
|
| 18 |
+
if ([string]::IsNullOrWhiteSpace($secret)) {
|
| 19 |
+
# Generate a random secret
|
| 20 |
+
$secret = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object {[char]$_})
|
| 21 |
+
Write-Host "Generated secret: $secret" -ForegroundColor Cyan
|
| 22 |
+
}
|
| 23 |
+
$env:QUIZ_SECRET = $secret
|
| 24 |
+
Write-Host "QUIZ_SECRET set for this session" -ForegroundColor Green
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
if ($openaiKey) {
|
| 28 |
+
Write-Host "OPENAI_API_KEY is already set" -ForegroundColor Green
|
| 29 |
+
} else {
|
| 30 |
+
Write-Host "OPENAI_API_KEY is not set (optional)" -ForegroundColor Yellow
|
| 31 |
+
$key = Read-Host "Enter your OPENAI_API_KEY (or press Enter to skip)"
|
| 32 |
+
if (-not [string]::IsNullOrWhiteSpace($key)) {
|
| 33 |
+
$env:OPENAI_API_KEY = $key
|
| 34 |
+
Write-Host "OPENAI_API_KEY set for this session" -ForegroundColor Green
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
if ($openrouterKey) {
|
| 39 |
+
Write-Host "OPENROUTER_API_KEY is already set" -ForegroundColor Green
|
| 40 |
+
} else {
|
| 41 |
+
Write-Host "OPENROUTER_API_KEY is not set (optional, e.g., GPT-5-nano)" -ForegroundColor Yellow
|
| 42 |
+
$routerKey = Read-Host "Enter your OPENROUTER_API_KEY (or press Enter to skip)"
|
| 43 |
+
if (-not [string]::IsNullOrWhiteSpace($routerKey)) {
|
| 44 |
+
$env:OPENROUTER_API_KEY = $routerKey
|
| 45 |
+
Write-Host "OPENROUTER_API_KEY set for this session" -ForegroundColor Green
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
Write-Host ""
|
| 50 |
+
Write-Host "Environment variables configured!" -ForegroundColor Green
|
| 51 |
+
Write-Host ""
|
| 52 |
+
Write-Host "To make these permanent, run:" -ForegroundColor Cyan
|
| 53 |
+
Write-Host ('[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", "' + $env:QUIZ_SECRET + '", "User")') -ForegroundColor Gray
|
| 54 |
+
if ($env:OPENAI_API_KEY) {
|
| 55 |
+
Write-Host ('[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "' + $env:OPENAI_API_KEY + '", "User")') -ForegroundColor Gray
|
| 56 |
+
}
|
| 57 |
+
if ($env:OPENROUTER_API_KEY) {
|
| 58 |
+
Write-Host ('[System.Environment]::SetEnvironmentVariable("OPENROUTER_API_KEY", "' + $env:OPENROUTER_API_KEY + '", "User")') -ForegroundColor Gray
|
| 59 |
+
}
|
| 60 |
+
Write-Host ""
|
| 61 |
+
Write-Host "Or copy and paste these commands:" -ForegroundColor Cyan
|
| 62 |
+
Write-Host ('[System.Environment]::SetEnvironmentVariable("QUIZ_SECRET", "' + $env:QUIZ_SECRET + '", "User")') -ForegroundColor Yellow
|
| 63 |
+
if ($env:OPENAI_API_KEY) {
|
| 64 |
+
Write-Host ('[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "' + $env:OPENAI_API_KEY + '", "User")') -ForegroundColor Yellow
|
| 65 |
+
}
|
| 66 |
+
if ($env:OPENROUTER_API_KEY) {
|
| 67 |
+
Write-Host ('[System.Environment]::SetEnvironmentVariable("OPENROUTER_API_KEY", "' + $env:OPENROUTER_API_KEY + '", "User")') -ForegroundColor Yellow
|
| 68 |
+
}
|
| 69 |
+
Write-Host ""
|
| 70 |
+
Write-Host "To start the server, run:" -ForegroundColor Cyan
|
| 71 |
+
Write-Host "python -m app.main" -ForegroundColor Yellow
|
| 72 |
+
|
setup_env.sh
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Quick Environment Setup Script for Linux/Mac
|
| 3 |
+
# Usage: source setup_env.sh or . setup_env.sh
|
| 4 |
+
|
| 5 |
+
echo "IITM LLM Quiz Solver - Environment Setup"
|
| 6 |
+
echo "========================================"
|
| 7 |
+
echo ""
|
| 8 |
+
|
| 9 |
+
# Check if variables are already set
|
| 10 |
+
if [ -n "$QUIZ_SECRET" ]; then
|
| 11 |
+
echo "✓ QUIZ_SECRET is already set"
|
| 12 |
+
else
|
| 13 |
+
echo "QUIZ_SECRET is not set"
|
| 14 |
+
read -p "Enter your QUIZ_SECRET (or press Enter to generate one): " secret
|
| 15 |
+
if [ -z "$secret" ]; then
|
| 16 |
+
# Generate a random secret
|
| 17 |
+
secret=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))" 2>/dev/null || openssl rand -base64 24)
|
| 18 |
+
echo "Generated secret: $secret"
|
| 19 |
+
fi
|
| 20 |
+
export QUIZ_SECRET="$secret"
|
| 21 |
+
echo "✓ QUIZ_SECRET set for this session"
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
if [ -n "$OPENAI_API_KEY" ]; then
|
| 25 |
+
echo "✓ OPENAI_API_KEY is already set"
|
| 26 |
+
else
|
| 27 |
+
echo "OPENAI_API_KEY is not set (optional)"
|
| 28 |
+
read -p "Enter your OPENAI_API_KEY (or press Enter to skip): " key
|
| 29 |
+
if [ -n "$key" ]; then
|
| 30 |
+
export OPENAI_API_KEY="$key"
|
| 31 |
+
echo "✓ OPENAI_API_KEY set for this session"
|
| 32 |
+
fi
|
| 33 |
+
fi
|
| 34 |
+
|
| 35 |
+
echo ""
|
| 36 |
+
echo "Environment variables configured!"
|
| 37 |
+
echo ""
|
| 38 |
+
echo "To make these permanent, add to ~/.bashrc or ~/.zshrc:"
|
| 39 |
+
echo "export QUIZ_SECRET=\"$QUIZ_SECRET\""
|
| 40 |
+
if [ -n "$OPENAI_API_KEY" ]; then
|
| 41 |
+
echo "export OPENAI_API_KEY=\"$OPENAI_API_KEY\""
|
| 42 |
+
fi
|
| 43 |
+
echo ""
|
| 44 |
+
echo "To start the server, run:"
|
| 45 |
+
echo "python -m app.main"
|
| 46 |
+
|
solver.cpython-311.pyc
ADDED
|
Binary file (27.3 kB). View file
|
|
|
solver.py
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quiz solver module - main logic for solving quizzes.
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import re
|
| 8 |
+
from typing import Optional, Dict, Any, List
|
| 9 |
+
import requests
|
| 10 |
+
from bs4 import BeautifulSoup
|
| 11 |
+
import pandas as pd
|
| 12 |
+
import io
|
| 13 |
+
import base64
|
| 14 |
+
|
| 15 |
+
from app.browser import get_browser, cleanup_browser
|
| 16 |
+
from app.llm import ask_gpt, parse_question_with_llm, solve_with_llm, initialize_llm
|
| 17 |
+
from app.utils import extract_submit_url, clean_text, extract_json_from_text, is_valid_url
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
# Initialize LLM on module load
|
| 22 |
+
initialize_llm()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class QuizSolver:
|
| 26 |
+
"""Main quiz solver class."""
|
| 27 |
+
|
| 28 |
+
def __init__(self):
|
| 29 |
+
self.browser = None
|
| 30 |
+
self.max_recursion = 10
|
| 31 |
+
self.current_recursion = 0
|
| 32 |
+
|
| 33 |
+
async def solve_quiz(self, url: str, email: str, secret: str) -> Dict[str, Any]:
|
| 34 |
+
"""
|
| 35 |
+
Main entry point for solving a quiz.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
url: Quiz page URL
|
| 39 |
+
email: User email
|
| 40 |
+
secret: Secret key
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
Final response from quiz system
|
| 44 |
+
"""
|
| 45 |
+
self.current_recursion = 0
|
| 46 |
+
self.browser = await get_browser()
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
return await self._solve_recursive(url, email, secret)
|
| 50 |
+
finally:
|
| 51 |
+
# Don't close browser here as it might be reused
|
| 52 |
+
pass
|
| 53 |
+
|
| 54 |
+
async def _solve_recursive(self, url: str, email: str, secret: str) -> Dict[str, Any]:
|
| 55 |
+
"""
|
| 56 |
+
Recursively solve quizzes.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
url: Current quiz URL
|
| 60 |
+
email: User email
|
| 61 |
+
secret: Secret key
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
Response from quiz system
|
| 65 |
+
"""
|
| 66 |
+
if self.current_recursion >= self.max_recursion:
|
| 67 |
+
logger.error("Maximum recursion depth reached")
|
| 68 |
+
return {"error": "Maximum recursion depth reached"}
|
| 69 |
+
|
| 70 |
+
self.current_recursion += 1
|
| 71 |
+
logger.info(f"Solving quiz {self.current_recursion}: {url}")
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
# Load the quiz page
|
| 75 |
+
page_content = await self.browser.load_page(url, wait_time=3)
|
| 76 |
+
|
| 77 |
+
# Extract submit URL
|
| 78 |
+
submit_url = extract_submit_url(page_content['text'], url)
|
| 79 |
+
if not submit_url:
|
| 80 |
+
# Try from HTML
|
| 81 |
+
soup = BeautifulSoup(page_content['html'], 'html.parser')
|
| 82 |
+
submit_url = extract_submit_url(soup.get_text(), url)
|
| 83 |
+
|
| 84 |
+
if not submit_url:
|
| 85 |
+
logger.error("Could not find submit URL")
|
| 86 |
+
return {"error": "Submit URL not found"}
|
| 87 |
+
|
| 88 |
+
# Extract question and solve
|
| 89 |
+
question_text = self._extract_question(page_content)
|
| 90 |
+
logger.info(f"Question extracted: {question_text[:200]}...")
|
| 91 |
+
|
| 92 |
+
# Solve the question
|
| 93 |
+
answer = await self._solve_question(question_text, page_content)
|
| 94 |
+
|
| 95 |
+
# Ensure answer is in the correct format (string or simple JSON-serializable)
|
| 96 |
+
answer = self._normalize_answer(answer)
|
| 97 |
+
logger.info(f"Answer computed: {str(answer)[:200]}...")
|
| 98 |
+
|
| 99 |
+
# Submit answer
|
| 100 |
+
response = await self._submit_answer(
|
| 101 |
+
submit_url, email, secret, url, answer
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Check if there's a next quiz
|
| 105 |
+
if isinstance(response, dict) and 'url' in response:
|
| 106 |
+
next_url = response['url']
|
| 107 |
+
if next_url and next_url != url and is_valid_url(next_url):
|
| 108 |
+
logger.info(f"Next quiz found: {next_url}")
|
| 109 |
+
# Recursively solve next quiz
|
| 110 |
+
next_response = await self._solve_recursive(next_url, email, secret)
|
| 111 |
+
return next_response
|
| 112 |
+
|
| 113 |
+
return response
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"Error solving quiz: {e}", exc_info=True)
|
| 117 |
+
return {"error": str(e)}
|
| 118 |
+
|
| 119 |
+
def _extract_question(self, page_content: Dict[str, Any]) -> str:
|
| 120 |
+
"""
|
| 121 |
+
Extract question text from page content.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
page_content: Page content dictionary
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
Question text
|
| 128 |
+
"""
|
| 129 |
+
text = page_content.get('all_text', page_content.get('text', ''))
|
| 130 |
+
|
| 131 |
+
# Try to find question markers
|
| 132 |
+
question_patterns = [
|
| 133 |
+
r'[Qq]uestion[:\s]+(.*?)(?:\n\n|\n[A-Z]|$)',
|
| 134 |
+
r'[Pp]roblem[:\s]+(.*?)(?:\n\n|\n[A-Z]|$)',
|
| 135 |
+
r'[Tt]ask[:\s]+(.*?)(?:\n\n|\n[A-Z]|$)',
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
for pattern in question_patterns:
|
| 139 |
+
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
| 140 |
+
if match:
|
| 141 |
+
return clean_text(match.group(1))
|
| 142 |
+
|
| 143 |
+
# If no pattern matches, return first substantial paragraph
|
| 144 |
+
paragraphs = [p.strip() for p in text.split('\n\n') if len(p.strip()) > 50]
|
| 145 |
+
if paragraphs:
|
| 146 |
+
return paragraphs[0]
|
| 147 |
+
|
| 148 |
+
return clean_text(text[:1000]) # Return first 1000 chars
|
| 149 |
+
|
| 150 |
+
async def _solve_question(self, question: str, page_content: Dict[str, Any]) -> Any:
|
| 151 |
+
"""
|
| 152 |
+
Solve a quiz question using various strategies.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
question: Question text
|
| 156 |
+
page_content: Full page content
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
Answer (can be dict, list, string, number, etc.)
|
| 160 |
+
"""
|
| 161 |
+
logger.info("Analyzing question type...")
|
| 162 |
+
|
| 163 |
+
# Try to parse question with LLM first
|
| 164 |
+
parsed = await parse_question_with_llm(question, page_content.get('text', ''))
|
| 165 |
+
|
| 166 |
+
# Extract data from page
|
| 167 |
+
available_data = self._extract_data_from_page(page_content)
|
| 168 |
+
|
| 169 |
+
# Strategy 1: Check if answer is already in the page
|
| 170 |
+
answer_in_page = self._find_answer_in_page(page_content, question)
|
| 171 |
+
if answer_in_page:
|
| 172 |
+
logger.info("Answer found in page content")
|
| 173 |
+
return answer_in_page
|
| 174 |
+
|
| 175 |
+
# Strategy 2: Check for data files/links to download
|
| 176 |
+
data_files = self._find_data_files(page_content)
|
| 177 |
+
if data_files:
|
| 178 |
+
logger.info(f"Found data files: {data_files}")
|
| 179 |
+
processed_data = await self._process_data_files(data_files)
|
| 180 |
+
if processed_data:
|
| 181 |
+
answer = await self._solve_with_data(question, processed_data)
|
| 182 |
+
if answer:
|
| 183 |
+
return answer
|
| 184 |
+
|
| 185 |
+
# Strategy 3: Use LLM to solve
|
| 186 |
+
logger.info("Attempting to solve with LLM...")
|
| 187 |
+
llm_answer = await solve_with_llm(question, available_data)
|
| 188 |
+
if llm_answer:
|
| 189 |
+
# Try to parse as JSON if it looks like JSON
|
| 190 |
+
json_answer = extract_json_from_text(llm_answer)
|
| 191 |
+
if json_answer:
|
| 192 |
+
return json_answer
|
| 193 |
+
return llm_answer
|
| 194 |
+
|
| 195 |
+
# Strategy 4: Fallback - try to extract a simple answer from the question
|
| 196 |
+
# Many quiz pages have the answer in the question itself
|
| 197 |
+
simple_answer = self._extract_simple_answer(question, page_content)
|
| 198 |
+
if simple_answer:
|
| 199 |
+
logger.info("Extracted simple answer from question")
|
| 200 |
+
return simple_answer
|
| 201 |
+
|
| 202 |
+
# Strategy 5: Last resort - return a default answer
|
| 203 |
+
logger.warning("Could not solve question, using default answer")
|
| 204 |
+
return "answer"
|
| 205 |
+
|
| 206 |
+
def _extract_data_from_page(self, page_content: Dict[str, Any]) -> Dict[str, Any]:
|
| 207 |
+
"""
|
| 208 |
+
Extract structured data from page.
|
| 209 |
+
|
| 210 |
+
Args:
|
| 211 |
+
page_content: Page content dictionary
|
| 212 |
+
|
| 213 |
+
Returns:
|
| 214 |
+
Dictionary of extracted data
|
| 215 |
+
"""
|
| 216 |
+
data = {
|
| 217 |
+
'text': page_content.get('text', ''),
|
| 218 |
+
'html': page_content.get('html', ''),
|
| 219 |
+
'links': page_content.get('links', []),
|
| 220 |
+
'images': page_content.get('images', []),
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
# Try to extract tables
|
| 224 |
+
try:
|
| 225 |
+
soup = BeautifulSoup(page_content.get('html', ''), 'html.parser')
|
| 226 |
+
tables = soup.find_all('table')
|
| 227 |
+
if tables:
|
| 228 |
+
data['tables'] = []
|
| 229 |
+
for table in tables:
|
| 230 |
+
try:
|
| 231 |
+
df = pd.read_html(str(table))[0]
|
| 232 |
+
data['tables'].append(df.to_dict('records'))
|
| 233 |
+
except:
|
| 234 |
+
pass
|
| 235 |
+
except Exception as e:
|
| 236 |
+
logger.warning(f"Error extracting tables: {e}")
|
| 237 |
+
|
| 238 |
+
# Try to extract JSON from page
|
| 239 |
+
json_data = extract_json_from_text(page_content.get('text', ''))
|
| 240 |
+
if json_data:
|
| 241 |
+
data['json'] = json_data
|
| 242 |
+
|
| 243 |
+
return data
|
| 244 |
+
|
| 245 |
+
def _find_answer_in_page(self, page_content: Dict[str, Any], question: str) -> Optional[Any]:
|
| 246 |
+
"""
|
| 247 |
+
Check if answer is already present in page content.
|
| 248 |
+
|
| 249 |
+
Args:
|
| 250 |
+
page_content: Page content
|
| 251 |
+
question: Question text
|
| 252 |
+
|
| 253 |
+
Returns:
|
| 254 |
+
Answer if found, None otherwise
|
| 255 |
+
"""
|
| 256 |
+
text = page_content.get('all_text', page_content.get('text', ''))
|
| 257 |
+
|
| 258 |
+
# Look for answer patterns
|
| 259 |
+
answer_patterns = [
|
| 260 |
+
r'[Aa]nswer[:\s]+(.*?)(?:\n\n|$)',
|
| 261 |
+
r'[Ss]olution[:\s]+(.*?)(?:\n\n|$)',
|
| 262 |
+
r'[Rr]esult[:\s]+(.*?)(?:\n\n|$)',
|
| 263 |
+
]
|
| 264 |
+
|
| 265 |
+
for pattern in answer_patterns:
|
| 266 |
+
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
| 267 |
+
if match:
|
| 268 |
+
answer_text = clean_text(match.group(1))
|
| 269 |
+
# Try to parse as JSON
|
| 270 |
+
json_answer = extract_json_from_text(answer_text)
|
| 271 |
+
if json_answer:
|
| 272 |
+
return json_answer
|
| 273 |
+
return answer_text
|
| 274 |
+
|
| 275 |
+
return None
|
| 276 |
+
|
| 277 |
+
def _find_data_files(self, page_content: Dict[str, Any]) -> List[str]:
|
| 278 |
+
"""
|
| 279 |
+
Find data files (CSV, JSON, PDF, etc.) linked in the page.
|
| 280 |
+
|
| 281 |
+
Args:
|
| 282 |
+
page_content: Page content
|
| 283 |
+
|
| 284 |
+
Returns:
|
| 285 |
+
List of file URLs
|
| 286 |
+
"""
|
| 287 |
+
files = []
|
| 288 |
+
|
| 289 |
+
# Check links
|
| 290 |
+
for link in page_content.get('links', []):
|
| 291 |
+
href = link.get('href', '')
|
| 292 |
+
if any(href.lower().endswith(ext) for ext in ['.csv', '.json', '.pdf', '.xlsx', '.txt']):
|
| 293 |
+
files.append(href)
|
| 294 |
+
|
| 295 |
+
# Check text for file URLs
|
| 296 |
+
text = page_content.get('text', '')
|
| 297 |
+
file_pattern = r'https?://[^\s<>"\'\)]+\.(csv|json|pdf|xlsx|txt)'
|
| 298 |
+
matches = re.findall(file_pattern, text, re.IGNORECASE)
|
| 299 |
+
files.extend([m[0] for m in matches if m[0] not in files])
|
| 300 |
+
|
| 301 |
+
return files
|
| 302 |
+
|
| 303 |
+
async def _process_data_files(self, file_urls: List[str]) -> Dict[str, Any]:
|
| 304 |
+
"""
|
| 305 |
+
Download and process data files.
|
| 306 |
+
|
| 307 |
+
Args:
|
| 308 |
+
file_urls: List of file URLs
|
| 309 |
+
|
| 310 |
+
Returns:
|
| 311 |
+
Dictionary of processed data
|
| 312 |
+
"""
|
| 313 |
+
processed = {}
|
| 314 |
+
|
| 315 |
+
for url in file_urls:
|
| 316 |
+
try:
|
| 317 |
+
logger.info(f"Downloading file: {url}")
|
| 318 |
+
response = requests.get(url, timeout=30)
|
| 319 |
+
response.raise_for_status()
|
| 320 |
+
|
| 321 |
+
content_type = response.headers.get('content-type', '').lower()
|
| 322 |
+
filename = url.split('/')[-1]
|
| 323 |
+
|
| 324 |
+
if 'csv' in content_type or filename.endswith('.csv'):
|
| 325 |
+
df = pd.read_csv(io.StringIO(response.text))
|
| 326 |
+
processed[filename] = df.to_dict('records')
|
| 327 |
+
|
| 328 |
+
elif 'json' in content_type or filename.endswith('.json'):
|
| 329 |
+
processed[filename] = response.json()
|
| 330 |
+
|
| 331 |
+
elif 'pdf' in content_type or filename.endswith('.pdf'):
|
| 332 |
+
# PDF processing - try pdfplumber first, then PyPDF2
|
| 333 |
+
text = None
|
| 334 |
+
|
| 335 |
+
# Try pdfplumber
|
| 336 |
+
try:
|
| 337 |
+
import pdfplumber
|
| 338 |
+
with pdfplumber.open(io.BytesIO(response.content)) as pdf:
|
| 339 |
+
text = ""
|
| 340 |
+
for page in pdf.pages:
|
| 341 |
+
page_text = page.extract_text()
|
| 342 |
+
if page_text:
|
| 343 |
+
text += page_text + "\n"
|
| 344 |
+
if text:
|
| 345 |
+
processed[filename] = text.strip()
|
| 346 |
+
except ImportError:
|
| 347 |
+
logger.debug("pdfplumber not available")
|
| 348 |
+
except Exception as e:
|
| 349 |
+
logger.warning(f"Error reading PDF with pdfplumber {filename}: {e}")
|
| 350 |
+
|
| 351 |
+
# Fallback to PyPDF2
|
| 352 |
+
if not text or filename not in processed:
|
| 353 |
+
try:
|
| 354 |
+
import PyPDF2
|
| 355 |
+
pdf_file = io.BytesIO(response.content)
|
| 356 |
+
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 357 |
+
text = ""
|
| 358 |
+
for page in pdf_reader.pages:
|
| 359 |
+
page_text = page.extract_text()
|
| 360 |
+
if page_text:
|
| 361 |
+
text += page_text + "\n"
|
| 362 |
+
if text:
|
| 363 |
+
processed[filename] = text.strip()
|
| 364 |
+
except ImportError:
|
| 365 |
+
logger.warning("Neither pdfplumber nor PyPDF2 available for PDF processing")
|
| 366 |
+
except Exception as e:
|
| 367 |
+
logger.warning(f"Error reading PDF with PyPDF2 {filename}: {e}")
|
| 368 |
+
|
| 369 |
+
elif filename.endswith('.txt'):
|
| 370 |
+
processed[filename] = response.text
|
| 371 |
+
|
| 372 |
+
except Exception as e:
|
| 373 |
+
logger.error(f"Error processing file {url}: {e}")
|
| 374 |
+
continue
|
| 375 |
+
|
| 376 |
+
return processed
|
| 377 |
+
|
| 378 |
+
def _normalize_answer(self, answer: Any) -> Any:
|
| 379 |
+
"""
|
| 380 |
+
Normalize answer to ensure it's JSON-serializable and in correct format.
|
| 381 |
+
|
| 382 |
+
Args:
|
| 383 |
+
answer: Raw answer (can be dict, list, string, etc.)
|
| 384 |
+
|
| 385 |
+
Returns:
|
| 386 |
+
Normalized answer (preferably string or simple JSON)
|
| 387 |
+
"""
|
| 388 |
+
if answer is None:
|
| 389 |
+
return "answer"
|
| 390 |
+
|
| 391 |
+
# If it's a dict with question/analysis, extract a simple answer
|
| 392 |
+
if isinstance(answer, dict):
|
| 393 |
+
# If it contains an 'answer' key, use that
|
| 394 |
+
if 'answer' in answer:
|
| 395 |
+
return self._normalize_answer(answer['answer'])
|
| 396 |
+
# If it's an analysis dict, try to extract something useful
|
| 397 |
+
if 'question' in answer and len(answer) > 1:
|
| 398 |
+
# Return a simple string instead of the whole dict
|
| 399 |
+
return "answer"
|
| 400 |
+
# If it's a simple dict, convert to JSON string
|
| 401 |
+
if len(answer) <= 3:
|
| 402 |
+
try:
|
| 403 |
+
return json.dumps(answer)
|
| 404 |
+
except:
|
| 405 |
+
return str(answer)
|
| 406 |
+
# Complex dict - return as JSON string
|
| 407 |
+
try:
|
| 408 |
+
return json.dumps(answer)
|
| 409 |
+
except:
|
| 410 |
+
return str(answer)
|
| 411 |
+
|
| 412 |
+
# If it's a list, convert to JSON string if small, otherwise string
|
| 413 |
+
if isinstance(answer, list):
|
| 414 |
+
if len(answer) <= 10:
|
| 415 |
+
try:
|
| 416 |
+
return json.dumps(answer)
|
| 417 |
+
except:
|
| 418 |
+
return str(answer)
|
| 419 |
+
return str(answer)
|
| 420 |
+
|
| 421 |
+
# For strings, return as-is (but clean up)
|
| 422 |
+
if isinstance(answer, str):
|
| 423 |
+
# Remove excessive whitespace
|
| 424 |
+
answer = ' '.join(answer.split())
|
| 425 |
+
# If it's very long, truncate
|
| 426 |
+
if len(answer) > 1000:
|
| 427 |
+
answer = answer[:1000] + "..."
|
| 428 |
+
return answer
|
| 429 |
+
|
| 430 |
+
# For other types, convert to string
|
| 431 |
+
return str(answer)
|
| 432 |
+
|
| 433 |
+
def _extract_simple_answer(self, question: str, page_content: Dict[str, Any]) -> Optional[str]:
|
| 434 |
+
"""
|
| 435 |
+
Try to extract a simple answer from the question or page.
|
| 436 |
+
|
| 437 |
+
Args:
|
| 438 |
+
question: Question text
|
| 439 |
+
page_content: Page content
|
| 440 |
+
|
| 441 |
+
Returns:
|
| 442 |
+
Simple answer string or None
|
| 443 |
+
"""
|
| 444 |
+
text = page_content.get('all_text', page_content.get('text', ''))
|
| 445 |
+
combined = question + "\n\n" + text
|
| 446 |
+
|
| 447 |
+
# Check if question says "anything" or similar - very common in demo quizzes
|
| 448 |
+
if re.search(r'"answer"\s*:\s*"anything\s+you\s+want"', combined, re.IGNORECASE):
|
| 449 |
+
return "answer"
|
| 450 |
+
if re.search(r'"answer"\s*:\s*"anything"', combined, re.IGNORECASE):
|
| 451 |
+
return "answer"
|
| 452 |
+
if re.search(r'anything\s+you\s+want|any\s+value|any\s+string|any\s+text|anything', question, re.IGNORECASE):
|
| 453 |
+
return "answer"
|
| 454 |
+
|
| 455 |
+
# Look for patterns like "answer: X" or "the answer is X"
|
| 456 |
+
patterns = [
|
| 457 |
+
r'"answer"\s*:\s*"([^"]+)"', # JSON format: "answer": "value"
|
| 458 |
+
r'[Aa]nswer[:\s]+["\']?([^"\'\n]+)["\']?',
|
| 459 |
+
r'[Tt]he\s+[Aa]nswer\s+[Ii]s[:\s]+["\']?([^"\'\n]+)["\']?',
|
| 460 |
+
r'[Yy]our\s+[Aa]nswer[:\s]+["\']?([^"\'\n]+)["\']?',
|
| 461 |
+
]
|
| 462 |
+
|
| 463 |
+
for pattern in patterns:
|
| 464 |
+
match = re.search(pattern, combined, re.IGNORECASE)
|
| 465 |
+
if match:
|
| 466 |
+
answer = match.group(1).strip()
|
| 467 |
+
# Skip if it's a placeholder or instruction
|
| 468 |
+
if answer and len(answer) < 200 and answer.lower() not in ['your email', 'your secret', 'anything you want', 'anything']:
|
| 469 |
+
return answer
|
| 470 |
+
|
| 471 |
+
return None
|
| 472 |
+
|
| 473 |
+
async def _solve_with_data(self, question: str, data: Dict[str, Any]) -> Optional[Any]:
|
| 474 |
+
"""
|
| 475 |
+
Solve question using processed data.
|
| 476 |
+
|
| 477 |
+
Args:
|
| 478 |
+
question: Question text
|
| 479 |
+
data: Processed data dictionary
|
| 480 |
+
|
| 481 |
+
Returns:
|
| 482 |
+
Answer or None
|
| 483 |
+
"""
|
| 484 |
+
# Use LLM to solve with data
|
| 485 |
+
prompt = f"""Solve this question using the provided data:
|
| 486 |
+
|
| 487 |
+
Question: {question}
|
| 488 |
+
|
| 489 |
+
Data:
|
| 490 |
+
{json.dumps(data, indent=2, default=str)}
|
| 491 |
+
|
| 492 |
+
Provide the answer. If JSON format is required, return valid JSON.
|
| 493 |
+
"""
|
| 494 |
+
|
| 495 |
+
answer = await ask_gpt(prompt, max_tokens=3000)
|
| 496 |
+
if answer:
|
| 497 |
+
json_answer = extract_json_from_text(answer)
|
| 498 |
+
if json_answer:
|
| 499 |
+
return json_answer
|
| 500 |
+
return answer
|
| 501 |
+
|
| 502 |
+
return None
|
| 503 |
+
|
| 504 |
+
async def _submit_answer(self, submit_url: str, email: str, secret: str,
|
| 505 |
+
quiz_url: str, answer: Any) -> Dict[str, Any]:
|
| 506 |
+
"""
|
| 507 |
+
Submit answer to the quiz system.
|
| 508 |
+
|
| 509 |
+
Args:
|
| 510 |
+
submit_url: URL to submit answer to
|
| 511 |
+
email: User email
|
| 512 |
+
secret: Secret key
|
| 513 |
+
quiz_url: Original quiz URL
|
| 514 |
+
answer: Computed answer
|
| 515 |
+
|
| 516 |
+
Returns:
|
| 517 |
+
Response from submission endpoint
|
| 518 |
+
"""
|
| 519 |
+
# Ensure answer is JSON-serializable
|
| 520 |
+
try:
|
| 521 |
+
# Try to serialize answer to check if it's valid JSON
|
| 522 |
+
json.dumps(answer)
|
| 523 |
+
except (TypeError, ValueError) as e:
|
| 524 |
+
logger.warning(f"Answer is not JSON-serializable, converting to string: {e}")
|
| 525 |
+
# Convert complex objects to string representation
|
| 526 |
+
if isinstance(answer, (dict, list)):
|
| 527 |
+
answer = json.dumps(answer)
|
| 528 |
+
else:
|
| 529 |
+
answer = str(answer)
|
| 530 |
+
|
| 531 |
+
payload = {
|
| 532 |
+
"email": email,
|
| 533 |
+
"secret": secret,
|
| 534 |
+
"url": quiz_url,
|
| 535 |
+
"answer": answer
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
try:
|
| 539 |
+
logger.info(f"Submitting answer to: {submit_url}")
|
| 540 |
+
logger.debug(f"Payload: {json.dumps(payload, indent=2, default=str)}")
|
| 541 |
+
|
| 542 |
+
response = requests.post(
|
| 543 |
+
submit_url,
|
| 544 |
+
json=payload,
|
| 545 |
+
headers={'Content-Type': 'application/json'},
|
| 546 |
+
timeout=60
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
# Log response details
|
| 550 |
+
logger.info(f"Response status: {response.status_code}")
|
| 551 |
+
logger.debug(f"Response headers: {dict(response.headers)}")
|
| 552 |
+
|
| 553 |
+
response.raise_for_status()
|
| 554 |
+
|
| 555 |
+
try:
|
| 556 |
+
result = response.json()
|
| 557 |
+
logger.info(f"Submission successful: {result}")
|
| 558 |
+
return result
|
| 559 |
+
except json.JSONDecodeError:
|
| 560 |
+
logger.warning(f"Response is not JSON, returning text: {response.text[:500]}")
|
| 561 |
+
return {"response": response.text, "status_code": response.status_code}
|
| 562 |
+
|
| 563 |
+
except requests.exceptions.HTTPError as e:
|
| 564 |
+
logger.error(f"HTTP error submitting answer: {e}")
|
| 565 |
+
if hasattr(e, 'response') and e.response is not None:
|
| 566 |
+
try:
|
| 567 |
+
error_response = e.response.json()
|
| 568 |
+
logger.error(f"Error response: {error_response}")
|
| 569 |
+
return error_response
|
| 570 |
+
except:
|
| 571 |
+
logger.error(f"Error response text: {e.response.text[:500]}")
|
| 572 |
+
return {"error": e.response.text, "status_code": e.response.status_code}
|
| 573 |
+
return {"error": str(e)}
|
| 574 |
+
except requests.exceptions.RequestException as e:
|
| 575 |
+
logger.error(f"Error submitting answer: {e}", exc_info=True)
|
| 576 |
+
return {"error": str(e)}
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
async def solve_quiz(url: str, email: str, secret: str) -> Dict[str, Any]:
|
| 580 |
+
"""
|
| 581 |
+
Convenience function to solve a quiz.
|
| 582 |
+
|
| 583 |
+
Args:
|
| 584 |
+
url: Quiz page URL
|
| 585 |
+
email: User email
|
| 586 |
+
secret: Secret key
|
| 587 |
+
|
| 588 |
+
Returns:
|
| 589 |
+
Final response from quiz system
|
| 590 |
+
"""
|
| 591 |
+
solver = QuizSolver()
|
| 592 |
+
return await solver.solve_quiz(url, email, secret)
|
| 593 |
+
|
test_api.ps1
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PowerShell script to test the API correctly
|
| 2 |
+
# Usage: .\test_api.ps1
|
| 3 |
+
|
| 4 |
+
# Set variables if not already set
|
| 5 |
+
if (-not $env:QUIZ_SECRET) {
|
| 6 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
# Create the request body as a PowerShell object (not a string)
|
| 10 |
+
$body = @{
|
| 11 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 12 |
+
secret = "EasyQuiz"
|
| 13 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 14 |
+
} | ConvertTo-Json
|
| 15 |
+
|
| 16 |
+
Write-Host "Sending request to /solve endpoint..." -ForegroundColor Cyan
|
| 17 |
+
Write-Host "Body: $body" -ForegroundColor Gray
|
| 18 |
+
Write-Host ""
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
$response = Invoke-RestMethod `
|
| 22 |
+
-Uri "http://127.0.0.1:8000/solve" `
|
| 23 |
+
-Method POST `
|
| 24 |
+
-ContentType "application/json" `
|
| 25 |
+
-Body $body
|
| 26 |
+
|
| 27 |
+
Write-Host "Response:" -ForegroundColor Green
|
| 28 |
+
$response | ConvertTo-Json -Depth 10
|
| 29 |
+
}
|
| 30 |
+
catch {
|
| 31 |
+
Write-Host "Error occurred:" -ForegroundColor Red
|
| 32 |
+
Write-Host $_.Exception.Message -ForegroundColor Red
|
| 33 |
+
|
| 34 |
+
if ($_.ErrorDetails.Message) {
|
| 35 |
+
Write-Host "Details:" -ForegroundColor Yellow
|
| 36 |
+
Write-Host $_.ErrorDetails.Message -ForegroundColor Yellow
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
if ($_.Response) {
|
| 40 |
+
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
|
| 41 |
+
$responseBody = $reader.ReadToEnd()
|
| 42 |
+
Write-Host "Response Body:" -ForegroundColor Yellow
|
| 43 |
+
Write-Host $responseBody -ForegroundColor Yellow
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
test_env.json.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script that outputs environment variables as JSON.
|
| 3 |
+
Run: python test_env.json.py
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
def get_env_json():
|
| 9 |
+
"""Get environment variables status as JSON."""
|
| 10 |
+
quiz_secret = os.getenv("QUIZ_SECRET")
|
| 11 |
+
openai_key = os.getenv("OPENAI_API_KEY")
|
| 12 |
+
port = os.getenv("PORT", "8000")
|
| 13 |
+
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
| 14 |
+
|
| 15 |
+
result = {
|
| 16 |
+
"status": "ok",
|
| 17 |
+
"variables": {
|
| 18 |
+
"QUIZ_SECRET": {
|
| 19 |
+
"set": quiz_secret is not None,
|
| 20 |
+
"length": len(quiz_secret) if quiz_secret else 0,
|
| 21 |
+
"preview": f"{quiz_secret[:4]}...{quiz_secret[-4:]}" if quiz_secret and len(quiz_secret) > 8 else "***" if quiz_secret else None
|
| 22 |
+
},
|
| 23 |
+
"OPENAI_API_KEY": {
|
| 24 |
+
"set": openai_key is not None,
|
| 25 |
+
"length": len(openai_key) if openai_key else 0,
|
| 26 |
+
"preview": f"{openai_key[:7]}...{openai_key[-4:]}" if openai_key and len(openai_key) > 11 else "***" if openai_key else None,
|
| 27 |
+
"valid_format": openai_key.startswith("sk-") if openai_key else False
|
| 28 |
+
},
|
| 29 |
+
"OPENROUTER_API_KEY": {
|
| 30 |
+
"set": openrouter_key is not None,
|
| 31 |
+
"length": len(openrouter_key) if openrouter_key else 0,
|
| 32 |
+
"preview": f"{openrouter_key[:7]}...{openrouter_key[-4:]}" if openrouter_key and len(openrouter_key) > 11 else "***" if openrouter_key else None,
|
| 33 |
+
"valid_format": openrouter_key.startswith("sk-or-") if openrouter_key else False
|
| 34 |
+
},
|
| 35 |
+
"PORT": {
|
| 36 |
+
"set": True,
|
| 37 |
+
"value": port
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"ready": quiz_secret is not None,
|
| 41 |
+
"llm_enabled": any([openai_key, openrouter_key]),
|
| 42 |
+
"message": "Ready to run" if quiz_secret else "QUIZ_SECRET is required"
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return result
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
result = get_env_json()
|
| 49 |
+
print(json.dumps(result, indent=2))
|
| 50 |
+
|
test_server.ps1
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Test script to check if server is running and test endpoints
|
| 2 |
+
# Usage: .\test_server.ps1
|
| 3 |
+
|
| 4 |
+
Write-Host "Testing IITM LLM Quiz Solver API" -ForegroundColor Cyan
|
| 5 |
+
Write-Host "=================================" -ForegroundColor Cyan
|
| 6 |
+
Write-Host ""
|
| 7 |
+
|
| 8 |
+
# Test 1: Health check
|
| 9 |
+
Write-Host "1. Testing /health endpoint..." -ForegroundColor Yellow
|
| 10 |
+
try {
|
| 11 |
+
$health = Invoke-RestMethod -Uri "http://127.0.0.1:8000/health" -Method GET -ErrorAction Stop
|
| 12 |
+
Write-Host " ✓ Server is running!" -ForegroundColor Green
|
| 13 |
+
Write-Host " Response: $($health | ConvertTo-Json)" -ForegroundColor Gray
|
| 14 |
+
} catch {
|
| 15 |
+
Write-Host " ✗ Server is NOT running or not accessible" -ForegroundColor Red
|
| 16 |
+
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
| 17 |
+
Write-Host ""
|
| 18 |
+
Write-Host " Please start the server first:" -ForegroundColor Yellow
|
| 19 |
+
Write-Host " python -m app.main" -ForegroundColor Cyan
|
| 20 |
+
exit 1
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
Write-Host ""
|
| 24 |
+
|
| 25 |
+
# Test 2: Root endpoint
|
| 26 |
+
Write-Host "2. Testing / endpoint..." -ForegroundColor Yellow
|
| 27 |
+
try {
|
| 28 |
+
$root = Invoke-RestMethod -Uri "http://127.0.0.1:8000/" -Method GET -ErrorAction Stop
|
| 29 |
+
Write-Host " ✓ Root endpoint works!" -ForegroundColor Green
|
| 30 |
+
Write-Host " Available endpoints:" -ForegroundColor Gray
|
| 31 |
+
$root.endpoints.PSObject.Properties | ForEach-Object {
|
| 32 |
+
Write-Host " $($_.Name): $($_.Value)" -ForegroundColor Gray
|
| 33 |
+
}
|
| 34 |
+
} catch {
|
| 35 |
+
Write-Host " ✗ Root endpoint failed: $($_.Exception.Message)" -ForegroundColor Red
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
Write-Host ""
|
| 39 |
+
|
| 40 |
+
# Test 3: Env check
|
| 41 |
+
Write-Host "3. Testing /env-check endpoint..." -ForegroundColor Yellow
|
| 42 |
+
try {
|
| 43 |
+
$envCheck = Invoke-RestMethod -Uri "http://127.0.0.1:8000/env-check" -Method GET -ErrorAction Stop
|
| 44 |
+
Write-Host " ✓ Environment check works!" -ForegroundColor Green
|
| 45 |
+
Write-Host " QUIZ_SECRET: $($envCheck.variables.QUIZ_SECRET.set)" -ForegroundColor Gray
|
| 46 |
+
Write-Host " OPENAI_API_KEY: $($envCheck.variables.OPENAI_API_KEY.set)" -ForegroundColor Gray
|
| 47 |
+
Write-Host " Ready: $($envCheck.ready)" -ForegroundColor Gray
|
| 48 |
+
} catch {
|
| 49 |
+
Write-Host " ✗ Env check failed: $($_.Exception.Message)" -ForegroundColor Red
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
Write-Host ""
|
| 53 |
+
|
| 54 |
+
# Test 4: Demo endpoint (if server is running)
|
| 55 |
+
Write-Host "4. Testing /demo endpoint..." -ForegroundColor Yellow
|
| 56 |
+
Write-Host " (This will make an actual request)" -ForegroundColor Gray
|
| 57 |
+
|
| 58 |
+
# Set variables if not set
|
| 59 |
+
if (-not $env:QUIZ_SECRET) {
|
| 60 |
+
$env:QUIZ_SECRET = "EasyQuiz"
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
$body = @{
|
| 64 |
+
email = "24f2005753@ds.study.iitm.ac.in"
|
| 65 |
+
secret = "EasyQuiz"
|
| 66 |
+
url = "https://tds-llm-analysis.s-anand.net/demo"
|
| 67 |
+
} | ConvertTo-Json
|
| 68 |
+
|
| 69 |
+
try {
|
| 70 |
+
Write-Host " Sending request..." -ForegroundColor Gray
|
| 71 |
+
$response = Invoke-RestMethod `
|
| 72 |
+
-Uri "http://127.0.0.1:8000/demo" `
|
| 73 |
+
-Method POST `
|
| 74 |
+
-ContentType "application/json" `
|
| 75 |
+
-Body $body `
|
| 76 |
+
-ErrorAction Stop
|
| 77 |
+
|
| 78 |
+
Write-Host " ✓ Demo endpoint works!" -ForegroundColor Green
|
| 79 |
+
Write-Host " Response:" -ForegroundColor Gray
|
| 80 |
+
$response | ConvertTo-Json -Depth 10 | Write-Host
|
| 81 |
+
} catch {
|
| 82 |
+
Write-Host " ✗ Demo endpoint failed" -ForegroundColor Red
|
| 83 |
+
Write-Host " Status: $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Yellow
|
| 84 |
+
if ($_.ErrorDetails.Message) {
|
| 85 |
+
Write-Host " Error: $($_.ErrorDetails.Message)" -ForegroundColor Yellow
|
| 86 |
+
} else {
|
| 87 |
+
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Yellow
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
Write-Host ""
|
| 92 |
+
Write-Host "=================================" -ForegroundColor Cyan
|
| 93 |
+
Write-Host "Test complete!" -ForegroundColor Cyan
|
| 94 |
+
|
utils.cpython-311.pyc
ADDED
|
Binary file (6.5 kB). View file
|
|
|
utils.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions for the IITM LLM Quiz Solver.
|
| 3 |
+
"""
|
| 4 |
+
import re
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Optional, Dict, Any
|
| 8 |
+
from urllib.parse import urlparse, urljoin
|
| 9 |
+
|
| 10 |
+
# Configure logging
|
| 11 |
+
logging.basicConfig(
|
| 12 |
+
level=logging.INFO,
|
| 13 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 14 |
+
)
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def extract_submit_url(text: str, base_url: str) -> Optional[str]:
|
| 19 |
+
"""
|
| 20 |
+
Extract submit URL from page text.
|
| 21 |
+
|
| 22 |
+
Looks for patterns like:
|
| 23 |
+
- "Submit your answer to: https://example.com/submit"
|
| 24 |
+
- "Submit to: https://example.com/submit"
|
| 25 |
+
- "URL: https://example.com/submit"
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
text: The page text content
|
| 29 |
+
base_url: Base URL for relative URL resolution
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Extracted submit URL or None
|
| 33 |
+
"""
|
| 34 |
+
# Common patterns for submit URLs
|
| 35 |
+
patterns = [
|
| 36 |
+
r'[Ss]ubmit\s+(?:your\s+)?(?:answer\s+)?(?:to|at|via):\s*(https?://[^\s<>"\'\)]+)',
|
| 37 |
+
r'[Ss]ubmit\s+[Tt]o:\s*(https?://[^\s<>"\'\)]+)',
|
| 38 |
+
r'[Uu][Rr][Ll]:\s*(https?://[^\s<>"\'\)]+)',
|
| 39 |
+
r'[Pp]ost\s+(?:to|at):\s*(https?://[^\s<>"\'\)]+)',
|
| 40 |
+
r'[Ss]end\s+(?:to|at):\s*(https?://[^\s<>"\'\)]+)',
|
| 41 |
+
r'(https?://[^\s<>"\'\)]*submit[^\s<>"\'\)]*)',
|
| 42 |
+
r'(https?://[^\s<>"\'\)]*answer[^\s<>"\'\)]*)',
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
for pattern in patterns:
|
| 46 |
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
| 47 |
+
if matches:
|
| 48 |
+
url = matches[0].strip().rstrip('.,;:!?)}]{["\'')
|
| 49 |
+
# Validate URL
|
| 50 |
+
try:
|
| 51 |
+
parsed = urlparse(url)
|
| 52 |
+
if parsed.scheme and parsed.netloc:
|
| 53 |
+
logger.info(f"Found submit URL: {url}")
|
| 54 |
+
return url
|
| 55 |
+
except Exception as e:
|
| 56 |
+
logger.warning(f"Invalid URL pattern found: {url}, error: {e}")
|
| 57 |
+
continue
|
| 58 |
+
|
| 59 |
+
# Try to find any URL that might be a submit endpoint
|
| 60 |
+
url_pattern = r'https?://[^\s<>"\'\)]+'
|
| 61 |
+
all_urls = re.findall(url_pattern, text)
|
| 62 |
+
for url in all_urls:
|
| 63 |
+
url_lower = url.lower()
|
| 64 |
+
if 'submit' in url_lower or 'answer' in url_lower:
|
| 65 |
+
try:
|
| 66 |
+
parsed = urlparse(url)
|
| 67 |
+
if parsed.scheme and parsed.netloc:
|
| 68 |
+
logger.info(f"Found potential submit URL: {url}")
|
| 69 |
+
return url
|
| 70 |
+
except:
|
| 71 |
+
continue
|
| 72 |
+
|
| 73 |
+
# Try to find relative submit links (e.g. href="/submit")
|
| 74 |
+
rel_patterns = [
|
| 75 |
+
r'href=["\\\'](/[^"\\\']*submit[^"\\\']*)["\\\']',
|
| 76 |
+
r'(/[^\\s"<>\']*submit[^\\s"<>\']*)',
|
| 77 |
+
]
|
| 78 |
+
for pattern in rel_patterns:
|
| 79 |
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
| 80 |
+
if matches:
|
| 81 |
+
candidate = matches[0].strip().rstrip('.,;:!?)}]{["\'')
|
| 82 |
+
joined = urljoin(base_url, candidate)
|
| 83 |
+
logger.info(f"Found relative submit URL: {joined}")
|
| 84 |
+
return joined
|
| 85 |
+
|
| 86 |
+
logger.warning("No submit URL found in page text")
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def validate_secret(secret: str, expected_secret: str) -> bool:
|
| 91 |
+
"""
|
| 92 |
+
Validate the secret key.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
secret: Provided secret
|
| 96 |
+
expected_secret: Expected secret from environment
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
True if valid, False otherwise
|
| 100 |
+
"""
|
| 101 |
+
return secret == expected_secret
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def clean_text(text: str) -> str:
|
| 105 |
+
"""
|
| 106 |
+
Clean and normalize text content.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
text: Raw text content
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
Cleaned text
|
| 113 |
+
"""
|
| 114 |
+
if not text:
|
| 115 |
+
return ""
|
| 116 |
+
|
| 117 |
+
# Remove excessive whitespace
|
| 118 |
+
text = re.sub(r'\s+', ' ', text)
|
| 119 |
+
# Remove leading/trailing whitespace
|
| 120 |
+
text = text.strip()
|
| 121 |
+
|
| 122 |
+
return text
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]:
|
| 126 |
+
"""
|
| 127 |
+
Try to extract JSON objects from text.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
text: Text that may contain JSON
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
Parsed JSON dict or None
|
| 134 |
+
"""
|
| 135 |
+
# Try to find JSON blocks
|
| 136 |
+
json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
|
| 137 |
+
matches = re.findall(json_pattern, text, re.DOTALL)
|
| 138 |
+
|
| 139 |
+
for match in matches:
|
| 140 |
+
try:
|
| 141 |
+
return json.loads(match)
|
| 142 |
+
except json.JSONDecodeError:
|
| 143 |
+
continue
|
| 144 |
+
|
| 145 |
+
return None
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def is_valid_url(url: str) -> bool:
|
| 149 |
+
"""
|
| 150 |
+
Validate if a string is a valid URL.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
url: URL string to validate
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
True if valid URL, False otherwise
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
result = urlparse(url)
|
| 160 |
+
return all([result.scheme, result.netloc])
|
| 161 |
+
except Exception:
|
| 162 |
+
return False
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def sanitize_filename(filename: str) -> str:
|
| 166 |
+
"""
|
| 167 |
+
Sanitize a filename by removing invalid characters.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
filename: Original filename
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
Sanitized filename
|
| 174 |
+
"""
|
| 175 |
+
# Remove invalid characters
|
| 176 |
+
filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
|
| 177 |
+
# Remove leading/trailing dots and spaces
|
| 178 |
+
filename = filename.strip('. ')
|
| 179 |
+
return filename
|
| 180 |
+
|