Spaces:
Sleeping
Sleeping
Julian Vanecek commited on
Commit Β·
5e87341
1
Parent(s): 476bbd0
init
Browse files- .gitignore +161 -0
- DEPLOYMENT_GUIDE.md +157 -0
- app.py +16 -0
- backend/chatbot_backend.py +275 -0
- backend/upload_pdfs.py +191 -0
- config/openai_config.json +32 -0
- frontend/gradio_app.py +194 -0
- requirements.txt +3 -0
- run_chatbot.py +28 -0
.gitignore
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
MANIFEST
|
| 23 |
+
|
| 24 |
+
# PyInstaller
|
| 25 |
+
*.manifest
|
| 26 |
+
*.spec
|
| 27 |
+
|
| 28 |
+
# Installer logs
|
| 29 |
+
pip-log.txt
|
| 30 |
+
pip-delete-this-directory.txt
|
| 31 |
+
|
| 32 |
+
# Unit test / coverage reports
|
| 33 |
+
htmlcov/
|
| 34 |
+
.tox/
|
| 35 |
+
.nox/
|
| 36 |
+
.coverage
|
| 37 |
+
.coverage.*
|
| 38 |
+
.cache
|
| 39 |
+
nosetests.xml
|
| 40 |
+
coverage.xml
|
| 41 |
+
*.cover
|
| 42 |
+
.hypothesis/
|
| 43 |
+
.pytest_cache/
|
| 44 |
+
|
| 45 |
+
# Translations
|
| 46 |
+
*.mo
|
| 47 |
+
*.pot
|
| 48 |
+
|
| 49 |
+
# Django stuff:
|
| 50 |
+
*.log
|
| 51 |
+
local_settings.py
|
| 52 |
+
db.sqlite3
|
| 53 |
+
|
| 54 |
+
# Flask stuff:
|
| 55 |
+
instance/
|
| 56 |
+
.webassets-cache
|
| 57 |
+
|
| 58 |
+
# Scrapy stuff:
|
| 59 |
+
.scrapy
|
| 60 |
+
|
| 61 |
+
# Sphinx documentation
|
| 62 |
+
docs/_build/
|
| 63 |
+
|
| 64 |
+
# PyBuilder
|
| 65 |
+
target/
|
| 66 |
+
|
| 67 |
+
# Jupyter Notebook
|
| 68 |
+
.ipynb_checkpoints
|
| 69 |
+
|
| 70 |
+
# IPython
|
| 71 |
+
profile_default/
|
| 72 |
+
ipython_config.py
|
| 73 |
+
|
| 74 |
+
# pyenv
|
| 75 |
+
.python-version
|
| 76 |
+
|
| 77 |
+
# celery beat schedule file
|
| 78 |
+
celerybeat-schedule
|
| 79 |
+
|
| 80 |
+
# SageMath parsed files
|
| 81 |
+
*.sage.py
|
| 82 |
+
|
| 83 |
+
# Environments
|
| 84 |
+
.env
|
| 85 |
+
.venv
|
| 86 |
+
env/
|
| 87 |
+
venv/
|
| 88 |
+
ENV/
|
| 89 |
+
env.bak/
|
| 90 |
+
venv.bak/
|
| 91 |
+
|
| 92 |
+
# Spyder project settings
|
| 93 |
+
.spyderproject
|
| 94 |
+
.spyproject
|
| 95 |
+
|
| 96 |
+
# Rope project settings
|
| 97 |
+
.ropeproject
|
| 98 |
+
|
| 99 |
+
# mkdocs documentation
|
| 100 |
+
/site
|
| 101 |
+
|
| 102 |
+
# mypy
|
| 103 |
+
.mypy_cache/
|
| 104 |
+
.dmypy.json
|
| 105 |
+
dmypy.json
|
| 106 |
+
|
| 107 |
+
# Pyre type checker
|
| 108 |
+
.pyre/
|
| 109 |
+
|
| 110 |
+
# macOS
|
| 111 |
+
.DS_Store
|
| 112 |
+
.AppleDouble
|
| 113 |
+
.LSOverride
|
| 114 |
+
|
| 115 |
+
# Thumbnails
|
| 116 |
+
._*
|
| 117 |
+
|
| 118 |
+
# Files that might appear in the root of a volume
|
| 119 |
+
.DocumentRevisions-V100
|
| 120 |
+
.fseventsd
|
| 121 |
+
.Spotlight-V100
|
| 122 |
+
.TemporaryItems
|
| 123 |
+
.Trashes
|
| 124 |
+
.VolumeIcon.icns
|
| 125 |
+
.com.apple.timemachine.donotpresent
|
| 126 |
+
|
| 127 |
+
# Directories potentially created on remote AFP share
|
| 128 |
+
.AppleDB
|
| 129 |
+
.AppleDesktop
|
| 130 |
+
Network Trash Folder
|
| 131 |
+
Temporary Items
|
| 132 |
+
.apdisk
|
| 133 |
+
|
| 134 |
+
# Windows
|
| 135 |
+
Thumbs.db
|
| 136 |
+
Thumbs.db:encryptable
|
| 137 |
+
ehthumbs.db
|
| 138 |
+
ehthumbs_vista.db
|
| 139 |
+
*.stackdump
|
| 140 |
+
[Dd]esktop.ini
|
| 141 |
+
$RECYCLE.BIN/
|
| 142 |
+
*.cab
|
| 143 |
+
*.msi
|
| 144 |
+
*.msix
|
| 145 |
+
*.msm
|
| 146 |
+
*.msp
|
| 147 |
+
*.lnk
|
| 148 |
+
|
| 149 |
+
# IDE
|
| 150 |
+
.vscode/
|
| 151 |
+
.idea/
|
| 152 |
+
*.swp
|
| 153 |
+
*.swo
|
| 154 |
+
*~
|
| 155 |
+
|
| 156 |
+
# Logs
|
| 157 |
+
*.log
|
| 158 |
+
logs/
|
| 159 |
+
|
| 160 |
+
# Local config files
|
| 161 |
+
config/local_*
|
DEPLOYMENT_GUIDE.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π Hugging Face Spaces Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide will walk you through deploying your OpenAI PDF Chatbot to Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- A Hugging Face account (sign up at https://huggingface.co)
|
| 8 |
+
- An OpenAI API key
|
| 9 |
+
- Git installed on your system
|
| 10 |
+
|
| 11 |
+
## Step 1: Create a New Space
|
| 12 |
+
|
| 13 |
+
1. Go to https://huggingface.co/new-space
|
| 14 |
+
2. Fill in the details:
|
| 15 |
+
- **Space name**: `openai-pdf-chatbot` (or your preferred name)
|
| 16 |
+
- **License**: MIT
|
| 17 |
+
- **SDK**: Select "Gradio"
|
| 18 |
+
- **Hardware**: CPU Basic (free tier) is sufficient
|
| 19 |
+
- **Visibility**: Public or Private (your choice)
|
| 20 |
+
3. Click "Create Space"
|
| 21 |
+
|
| 22 |
+
## Step 2: Clone Your Space Repository
|
| 23 |
+
|
| 24 |
+
After creating the space, you'll see a Git repository URL. Clone it:
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 28 |
+
cd YOUR_SPACE_NAME
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## Step 3: Copy Your Application Files
|
| 32 |
+
|
| 33 |
+
Copy all the files from your `openai_chatbot` directory to the cloned space repository:
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
# From your openai_chatbot directory
|
| 37 |
+
cp -r * /path/to/your/cloned/space/
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
Make sure these files are in the root of your space repository:
|
| 41 |
+
- `app.py` (main entry point)
|
| 42 |
+
- `requirements.txt`
|
| 43 |
+
- `README_SPACES.md` (rename this to `README.md`)
|
| 44 |
+
- `backend/` directory
|
| 45 |
+
- `frontend/` directory
|
| 46 |
+
- `config/` directory
|
| 47 |
+
|
| 48 |
+
## Step 4: Rename README File
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
mv README_SPACES.md README.md
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## Step 5: Set Up Your OpenAI API Key
|
| 55 |
+
|
| 56 |
+
1. Go to your Space on Hugging Face
|
| 57 |
+
2. Click on "Settings" tab
|
| 58 |
+
3. Scroll down to "Variables and secrets"
|
| 59 |
+
4. Click "Add a new secret"
|
| 60 |
+
5. Set:
|
| 61 |
+
- **Name**: `OPENAI_API_KEY`
|
| 62 |
+
- **Value**: Your OpenAI API key
|
| 63 |
+
6. Click "Add secret"
|
| 64 |
+
|
| 65 |
+
## Step 6: Commit and Push Your Code
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
git add .
|
| 69 |
+
git commit -m "Initial deployment of OpenAI PDF Chatbot"
|
| 70 |
+
git push origin main
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
## Step 7: Wait for Deployment
|
| 74 |
+
|
| 75 |
+
- Hugging Face Spaces will automatically build and deploy your app
|
| 76 |
+
- You can monitor the build process in the "App" tab of your Space
|
| 77 |
+
- The build typically takes 2-5 minutes
|
| 78 |
+
|
| 79 |
+
## Step 8: Test Your Deployment
|
| 80 |
+
|
| 81 |
+
Once deployed, your app will be available at:
|
| 82 |
+
`https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME`
|
| 83 |
+
|
| 84 |
+
Test the following features:
|
| 85 |
+
- Model selection dropdown
|
| 86 |
+
- Document querying toggle
|
| 87 |
+
- Custom prompt input
|
| 88 |
+
- Token usage and cost tracking
|
| 89 |
+
|
| 90 |
+
## Troubleshooting
|
| 91 |
+
|
| 92 |
+
### Common Issues:
|
| 93 |
+
|
| 94 |
+
1. **"No module named 'openai'" error**
|
| 95 |
+
- Check that `requirements.txt` includes `openai>=1.0.0`
|
| 96 |
+
- Verify the file is in the root directory
|
| 97 |
+
|
| 98 |
+
2. **"No assistant configured" error**
|
| 99 |
+
- Make sure `config/openai_config.json` is included
|
| 100 |
+
- Verify the assistant_id in the config file
|
| 101 |
+
|
| 102 |
+
3. **API key not found**
|
| 103 |
+
- Double-check the secret name is exactly `OPENAI_API_KEY`
|
| 104 |
+
- Restart the space after adding the secret
|
| 105 |
+
|
| 106 |
+
4. **Build timeout**
|
| 107 |
+
- Reduce the requirements.txt dependencies
|
| 108 |
+
- Check for any large files that might be causing issues
|
| 109 |
+
|
| 110 |
+
### Debug Steps:
|
| 111 |
+
|
| 112 |
+
1. Check the "Logs" tab in your Space for error messages
|
| 113 |
+
2. Verify all required files are present in the repository
|
| 114 |
+
3. Check that your OpenAI API key has sufficient credits
|
| 115 |
+
|
| 116 |
+
## Post-Deployment
|
| 117 |
+
|
| 118 |
+
### Customize Your Space:
|
| 119 |
+
- Edit the `README.md` to add your own description
|
| 120 |
+
- Update the emoji and colors in the YAML frontmatter
|
| 121 |
+
- Add screenshots or demo videos
|
| 122 |
+
|
| 123 |
+
### Monitor Usage:
|
| 124 |
+
- Check the "Analytics" tab for usage statistics
|
| 125 |
+
- Monitor your OpenAI API usage through the OpenAI dashboard
|
| 126 |
+
|
| 127 |
+
### Updates:
|
| 128 |
+
- To update your app, simply push new commits to the repository
|
| 129 |
+
- The Space will automatically rebuild and redeploy
|
| 130 |
+
|
| 131 |
+
## File Structure for Deployment
|
| 132 |
+
|
| 133 |
+
Your space repository should look like this:
|
| 134 |
+
|
| 135 |
+
```
|
| 136 |
+
your-space/
|
| 137 |
+
βββ app.py # Main entry point
|
| 138 |
+
βββ requirements.txt # Python dependencies
|
| 139 |
+
βββ README.md # Space description (from README_SPACES.md)
|
| 140 |
+
βββ backend/
|
| 141 |
+
β βββ chatbot_backend.py # Backend logic
|
| 142 |
+
βββ frontend/
|
| 143 |
+
β βββ gradio_app.py # Gradio UI
|
| 144 |
+
βββ config/
|
| 145 |
+
βββ openai_config.json # OpenAI configuration
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
## Success!
|
| 149 |
+
|
| 150 |
+
If everything is set up correctly, you should have a working OpenAI PDF Chatbot deployed on Hugging Face Spaces with:
|
| 151 |
+
- β
Model selection with pricing information
|
| 152 |
+
- β
Real-time token tracking and cost calculation
|
| 153 |
+
- β
Document querying capabilities
|
| 154 |
+
- β
Custom prompt support
|
| 155 |
+
- β
Clean, responsive UI
|
| 156 |
+
|
| 157 |
+
Share your Space URL with others to let them interact with your chatbot!
|
app.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Hugging Face Spaces App Entry Point
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# Add current directory to path
|
| 10 |
+
sys.path.append(str(Path(__file__).parent))
|
| 11 |
+
|
| 12 |
+
# Import and run the existing Gradio app
|
| 13 |
+
from frontend.gradio_app import main
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
main()
|
backend/chatbot_backend.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
OpenAI Chatbot Backend with Model Switching and Token Tracking
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Optional, Dict, Any, List, Tuple
|
| 11 |
+
from openai import OpenAI
|
| 12 |
+
import tiktoken
|
| 13 |
+
|
| 14 |
+
class ChatbotBackend:
|
| 15 |
+
"""Backend for OpenAI chatbot with vector store integration."""
|
| 16 |
+
|
| 17 |
+
# Model configurations with pricing (per 1M tokens)
|
| 18 |
+
MODEL_CONFIGS = {
|
| 19 |
+
"gpt-4.1-2025-04-14": {
|
| 20 |
+
"name": "GPT-4.1 (Latest)",
|
| 21 |
+
"model_id": "gpt-4.1-2025-04-14",
|
| 22 |
+
"input_cost": 2.0, # $2 per 1M input tokens
|
| 23 |
+
"output_cost": 8.0, # $8 per 1M output tokens
|
| 24 |
+
"context_window": 32768,
|
| 25 |
+
"supports_file_search": True
|
| 26 |
+
},
|
| 27 |
+
"gpt-4.1-mini-2025-04-14": {
|
| 28 |
+
"name": "GPT-4.1 Mini",
|
| 29 |
+
"model_id": "gpt-4.1-mini-2025-04-14",
|
| 30 |
+
"input_cost": 0.4, # $0.4 per 1M input tokens
|
| 31 |
+
"output_cost": 1.6, # $1.6 per 1M output tokens
|
| 32 |
+
"context_window": 16384,
|
| 33 |
+
"supports_file_search": True
|
| 34 |
+
},
|
| 35 |
+
"gpt-4.1-nano-2025-04-14": {
|
| 36 |
+
"name": "GPT-4.1 Nano",
|
| 37 |
+
"model_id": "gpt-4.1-nano-2025-04-14",
|
| 38 |
+
"input_cost": 0.1, # $0.1 per 1M input tokens
|
| 39 |
+
"output_cost": 0.4, # $0.4 per 1M output tokens
|
| 40 |
+
"context_window": 8192,
|
| 41 |
+
"supports_file_search": True
|
| 42 |
+
},
|
| 43 |
+
"o4-mini-2025-04-16": {
|
| 44 |
+
"name": "O4 Mini",
|
| 45 |
+
"model_id": "o4-mini-2025-04-16",
|
| 46 |
+
"input_cost": 1.1, # $1.1 per 1M input tokens
|
| 47 |
+
"output_cost": 4.4, # $4.4 per 1M output tokens
|
| 48 |
+
"context_window": 16384,
|
| 49 |
+
"supports_file_search": True
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def __init__(self, config_path: str = None):
|
| 54 |
+
"""Initialize the chatbot backend."""
|
| 55 |
+
self.client = OpenAI()
|
| 56 |
+
|
| 57 |
+
# Try to find config file
|
| 58 |
+
if config_path is None:
|
| 59 |
+
# Look for config in standard locations
|
| 60 |
+
possible_paths = [
|
| 61 |
+
Path(__file__).parent.parent / "config" / "openai_config.json",
|
| 62 |
+
Path(__file__).parent / "openai_config.json",
|
| 63 |
+
Path("config/openai_config.json"),
|
| 64 |
+
Path("openai_config.json")
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
for path in possible_paths:
|
| 68 |
+
if path.exists():
|
| 69 |
+
config_path = str(path)
|
| 70 |
+
break
|
| 71 |
+
|
| 72 |
+
self.config = self._load_config(config_path) if config_path else None
|
| 73 |
+
self.assistant_id = self.config.get('assistant_id') if self.config else None
|
| 74 |
+
self.current_model = "gpt-4.1-mini-2025-04-14" # Default model
|
| 75 |
+
|
| 76 |
+
def _load_config(self, config_path: str) -> Optional[Dict[str, Any]]:
|
| 77 |
+
"""Load the configuration file."""
|
| 78 |
+
config_file = Path(config_path)
|
| 79 |
+
|
| 80 |
+
if not config_file.exists():
|
| 81 |
+
print(f"Warning: Configuration file {config_file} not found")
|
| 82 |
+
return None
|
| 83 |
+
|
| 84 |
+
try:
|
| 85 |
+
with open(config_file, 'r') as f:
|
| 86 |
+
return json.load(f)
|
| 87 |
+
except Exception as e:
|
| 88 |
+
print(f"Error loading config file: {str(e)}")
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
def set_model(self, model_id: str) -> bool:
|
| 92 |
+
"""Set the current model."""
|
| 93 |
+
if model_id in self.MODEL_CONFIGS:
|
| 94 |
+
self.current_model = model_id
|
| 95 |
+
return True
|
| 96 |
+
return False
|
| 97 |
+
|
| 98 |
+
def get_available_models(self) -> Dict[str, Dict[str, Any]]:
|
| 99 |
+
"""Get available models and their configurations."""
|
| 100 |
+
return self.MODEL_CONFIGS
|
| 101 |
+
|
| 102 |
+
def count_tokens(self, text: str, model: str = None) -> int:
|
| 103 |
+
"""Count tokens in text for the specified model."""
|
| 104 |
+
if model is None:
|
| 105 |
+
model = self.current_model
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
# Map model names to encoding names
|
| 109 |
+
encoding_map = {
|
| 110 |
+
"gpt-4.1-2025-04-14": "cl100k_base",
|
| 111 |
+
"gpt-4.1-mini-2025-04-14": "cl100k_base",
|
| 112 |
+
"gpt-4.1-nano-2025-04-14": "cl100k_base",
|
| 113 |
+
"o4-mini-2025-04-16": "cl100k_base"
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
encoding_name = encoding_map.get(model, "cl100k_base")
|
| 117 |
+
encoding = tiktoken.get_encoding(encoding_name)
|
| 118 |
+
return len(encoding.encode(text))
|
| 119 |
+
except Exception:
|
| 120 |
+
# Fallback: rough estimate (1 token β 4 characters)
|
| 121 |
+
return len(text) // 4
|
| 122 |
+
|
| 123 |
+
def calculate_cost(self, input_tokens: int, output_tokens: int, model: str = None) -> Dict[str, float]:
|
| 124 |
+
"""Calculate the cost for the given token counts."""
|
| 125 |
+
if model is None:
|
| 126 |
+
model = self.current_model
|
| 127 |
+
|
| 128 |
+
config = self.MODEL_CONFIGS.get(model, self.MODEL_CONFIGS["gpt-4.1-mini-2025-04-14"])
|
| 129 |
+
|
| 130 |
+
input_cost = (input_tokens / 1_000_000) * config["input_cost"]
|
| 131 |
+
output_cost = (output_tokens / 1_000_000) * config["output_cost"]
|
| 132 |
+
total_cost = input_cost + output_cost
|
| 133 |
+
|
| 134 |
+
return {
|
| 135 |
+
"input_cost": input_cost,
|
| 136 |
+
"output_cost": output_cost,
|
| 137 |
+
"total_cost": total_cost,
|
| 138 |
+
"input_tokens": input_tokens,
|
| 139 |
+
"output_tokens": output_tokens
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
def query_with_documents(self, question: str, custom_prompt: str = None) -> Tuple[str, Dict[str, Any]]:
|
| 143 |
+
"""Query using the vector store documents."""
|
| 144 |
+
if not self.assistant_id:
|
| 145 |
+
return "Error: No assistant configured. Please upload documents first.", {}
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
start_time = time.time()
|
| 149 |
+
|
| 150 |
+
# Create a thread
|
| 151 |
+
thread = self.client.beta.threads.create()
|
| 152 |
+
|
| 153 |
+
# Prepare the message content
|
| 154 |
+
if custom_prompt:
|
| 155 |
+
content = f"{custom_prompt}\n\nUser Question: {question}"
|
| 156 |
+
else:
|
| 157 |
+
content = question
|
| 158 |
+
|
| 159 |
+
# Count input tokens
|
| 160 |
+
input_tokens = self.count_tokens(content)
|
| 161 |
+
|
| 162 |
+
# Add the question to the thread
|
| 163 |
+
self.client.beta.threads.messages.create(
|
| 164 |
+
thread_id=thread.id,
|
| 165 |
+
role="user",
|
| 166 |
+
content=content
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
# Run the assistant with the specified model
|
| 170 |
+
run = self.client.beta.threads.runs.create(
|
| 171 |
+
thread_id=thread.id,
|
| 172 |
+
assistant_id=self.assistant_id,
|
| 173 |
+
model=self.current_model
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# Wait for completion
|
| 177 |
+
while True:
|
| 178 |
+
run_status = self.client.beta.threads.runs.retrieve(
|
| 179 |
+
thread_id=thread.id,
|
| 180 |
+
run_id=run.id
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
if run_status.status == 'completed':
|
| 184 |
+
break
|
| 185 |
+
elif run_status.status in ['failed', 'cancelled', 'expired']:
|
| 186 |
+
return f"Error: Assistant run {run_status.status}", {}
|
| 187 |
+
|
| 188 |
+
time.sleep(0.5)
|
| 189 |
+
|
| 190 |
+
# Get the response
|
| 191 |
+
messages = self.client.beta.threads.messages.list(
|
| 192 |
+
thread_id=thread.id,
|
| 193 |
+
order="desc",
|
| 194 |
+
limit=1
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
if messages.data:
|
| 198 |
+
response = messages.data[0].content[0].text.value
|
| 199 |
+
output_tokens = self.count_tokens(response)
|
| 200 |
+
|
| 201 |
+
# Calculate costs
|
| 202 |
+
cost_info = self.calculate_cost(input_tokens, output_tokens)
|
| 203 |
+
cost_info["duration"] = time.time() - start_time
|
| 204 |
+
cost_info["model"] = self.current_model
|
| 205 |
+
|
| 206 |
+
return response, cost_info
|
| 207 |
+
else:
|
| 208 |
+
return "Error: No response received", {}
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
return f"Error: {str(e)}", {}
|
| 212 |
+
|
| 213 |
+
def query_without_documents(self, question: str, custom_prompt: str = None) -> Tuple[str, Dict[str, Any]]:
|
| 214 |
+
"""Query without using documents."""
|
| 215 |
+
try:
|
| 216 |
+
start_time = time.time()
|
| 217 |
+
|
| 218 |
+
# Prepare messages
|
| 219 |
+
messages = []
|
| 220 |
+
|
| 221 |
+
if custom_prompt:
|
| 222 |
+
messages.append({
|
| 223 |
+
"role": "system",
|
| 224 |
+
"content": custom_prompt
|
| 225 |
+
})
|
| 226 |
+
else:
|
| 227 |
+
messages.append({
|
| 228 |
+
"role": "system",
|
| 229 |
+
"content": "You are a helpful assistant. Answer questions based on your training knowledge."
|
| 230 |
+
})
|
| 231 |
+
|
| 232 |
+
messages.append({
|
| 233 |
+
"role": "user",
|
| 234 |
+
"content": question
|
| 235 |
+
})
|
| 236 |
+
|
| 237 |
+
# Count input tokens
|
| 238 |
+
input_text = " ".join([msg["content"] for msg in messages])
|
| 239 |
+
input_tokens = self.count_tokens(input_text)
|
| 240 |
+
|
| 241 |
+
# Make the API call
|
| 242 |
+
response = self.client.chat.completions.create(
|
| 243 |
+
model=self.current_model,
|
| 244 |
+
messages=messages,
|
| 245 |
+
temperature=0.7,
|
| 246 |
+
max_tokens=2000
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
answer = response.choices[0].message.content
|
| 250 |
+
output_tokens = self.count_tokens(answer)
|
| 251 |
+
|
| 252 |
+
# Calculate costs
|
| 253 |
+
cost_info = self.calculate_cost(input_tokens, output_tokens)
|
| 254 |
+
cost_info["duration"] = time.time() - start_time
|
| 255 |
+
cost_info["model"] = self.current_model
|
| 256 |
+
|
| 257 |
+
return answer, cost_info
|
| 258 |
+
|
| 259 |
+
except Exception as e:
|
| 260 |
+
return f"Error: {str(e)}", {}
|
| 261 |
+
|
| 262 |
+
def update_assistant_model(self, model_id: str) -> bool:
|
| 263 |
+
"""Update the assistant to use a specific model."""
|
| 264 |
+
if not self.assistant_id:
|
| 265 |
+
return False
|
| 266 |
+
|
| 267 |
+
try:
|
| 268 |
+
self.client.beta.assistants.update(
|
| 269 |
+
assistant_id=self.assistant_id,
|
| 270 |
+
model=model_id
|
| 271 |
+
)
|
| 272 |
+
return True
|
| 273 |
+
except Exception as e:
|
| 274 |
+
print(f"Error updating assistant model: {str(e)}")
|
| 275 |
+
return False
|
backend/upload_pdfs.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Upload PDFs to OpenAI Vector Store
|
| 4 |
+
Command-line tool to upload PDF files from a specified directory to OpenAI's vector store.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
import argparse
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import List, Dict, Any
|
| 14 |
+
from openai import OpenAI
|
| 15 |
+
|
| 16 |
+
def init_openai_client() -> OpenAI:
|
| 17 |
+
"""Initialize OpenAI client."""
|
| 18 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 19 |
+
if not api_key:
|
| 20 |
+
print("β Error: OPENAI_API_KEY environment variable not set")
|
| 21 |
+
sys.exit(1)
|
| 22 |
+
return OpenAI()
|
| 23 |
+
|
| 24 |
+
def upload_file(client: OpenAI, file_path: Path) -> str:
|
| 25 |
+
"""Upload a single file to OpenAI."""
|
| 26 |
+
print(f"π€ Uploading {file_path.name}...")
|
| 27 |
+
try:
|
| 28 |
+
with open(file_path, 'rb') as file:
|
| 29 |
+
response = client.files.create(
|
| 30 |
+
file=file,
|
| 31 |
+
purpose='assistants'
|
| 32 |
+
)
|
| 33 |
+
print(f" β
Uploaded successfully (ID: {response.id})")
|
| 34 |
+
return response.id
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f" β Failed to upload: {str(e)}")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
def create_vector_store(client: OpenAI, name: str, file_ids: List[str]) -> str:
|
| 40 |
+
"""Create a vector store with the uploaded files."""
|
| 41 |
+
print(f"\nποΈ Creating vector store '{name}'...")
|
| 42 |
+
try:
|
| 43 |
+
vector_store = client.beta.vector_stores.create(
|
| 44 |
+
name=name,
|
| 45 |
+
file_ids=file_ids
|
| 46 |
+
)
|
| 47 |
+
print(f"β
Vector store created (ID: {vector_store.id})")
|
| 48 |
+
return vector_store.id
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"β Failed to create vector store: {str(e)}")
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
def create_or_update_assistant(client: OpenAI, vector_store_id: str, existing_assistant_id: str = None) -> str:
|
| 54 |
+
"""Create a new assistant or update existing one with the vector store."""
|
| 55 |
+
try:
|
| 56 |
+
if existing_assistant_id:
|
| 57 |
+
print(f"\nπ€ Updating existing assistant (ID: {existing_assistant_id})...")
|
| 58 |
+
assistant = client.beta.assistants.update(
|
| 59 |
+
assistant_id=existing_assistant_id,
|
| 60 |
+
tool_resources={
|
| 61 |
+
"file_search": {
|
| 62 |
+
"vector_store_ids": [vector_store_id]
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
)
|
| 66 |
+
else:
|
| 67 |
+
print("\nπ€ Creating new assistant...")
|
| 68 |
+
assistant = client.beta.assistants.create(
|
| 69 |
+
name="PDF Document Assistant",
|
| 70 |
+
instructions="You are a helpful assistant that answers questions based on the provided PDF documents. Use the file search tool to find relevant information in the documents.",
|
| 71 |
+
model="gpt-4-turbo",
|
| 72 |
+
tools=[{"type": "file_search"}],
|
| 73 |
+
tool_resources={
|
| 74 |
+
"file_search": {
|
| 75 |
+
"vector_store_ids": [vector_store_id]
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
)
|
| 79 |
+
print(f"β
Assistant ready (ID: {assistant.id})")
|
| 80 |
+
return assistant.id
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"β Failed to create/update assistant: {str(e)}")
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
def save_config(config_path: Path, config: Dict[str, Any]):
|
| 86 |
+
"""Save configuration to file."""
|
| 87 |
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
| 88 |
+
with open(config_path, 'w') as f:
|
| 89 |
+
json.dump(config, f, indent=2)
|
| 90 |
+
print(f"\nπΎ Configuration saved to {config_path}")
|
| 91 |
+
|
| 92 |
+
def load_existing_config(config_path: Path) -> Dict[str, Any]:
|
| 93 |
+
"""Load existing configuration if available."""
|
| 94 |
+
if config_path.exists():
|
| 95 |
+
try:
|
| 96 |
+
with open(config_path, 'r') as f:
|
| 97 |
+
return json.load(f)
|
| 98 |
+
except Exception as e:
|
| 99 |
+
print(f"β οΈ Warning: Could not load existing config: {str(e)}")
|
| 100 |
+
return {}
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
"""Main function to upload PDFs to vector store."""
|
| 104 |
+
parser = argparse.ArgumentParser(description='Upload PDFs to OpenAI Vector Store')
|
| 105 |
+
parser.add_argument('directory', help='Directory containing PDF files')
|
| 106 |
+
parser.add_argument('--name', default='PDF Documents', help='Name for the vector store')
|
| 107 |
+
parser.add_argument('--config', default='config/openai_config.json', help='Config file path')
|
| 108 |
+
parser.add_argument('--update', action='store_true', help='Update existing assistant instead of creating new')
|
| 109 |
+
parser.add_argument('--assistant-id', help='Existing assistant ID to update')
|
| 110 |
+
|
| 111 |
+
args = parser.parse_args()
|
| 112 |
+
|
| 113 |
+
# Validate directory
|
| 114 |
+
pdf_dir = Path(args.directory)
|
| 115 |
+
if not pdf_dir.exists():
|
| 116 |
+
print(f"β Error: Directory '{pdf_dir}' does not exist")
|
| 117 |
+
sys.exit(1)
|
| 118 |
+
|
| 119 |
+
# Find PDF files
|
| 120 |
+
pdf_files = list(pdf_dir.glob('*.pdf'))
|
| 121 |
+
if not pdf_files:
|
| 122 |
+
print(f"β Error: No PDF files found in '{pdf_dir}'")
|
| 123 |
+
sys.exit(1)
|
| 124 |
+
|
| 125 |
+
print(f"π Found {len(pdf_files)} PDF files in '{pdf_dir}'")
|
| 126 |
+
for pdf in pdf_files:
|
| 127 |
+
print(f" β’ {pdf.name}")
|
| 128 |
+
|
| 129 |
+
# Initialize client
|
| 130 |
+
client = init_openai_client()
|
| 131 |
+
|
| 132 |
+
# Load existing config
|
| 133 |
+
config_path = Path(args.config)
|
| 134 |
+
if not config_path.is_absolute():
|
| 135 |
+
config_path = Path(__file__).parent.parent / config_path
|
| 136 |
+
|
| 137 |
+
existing_config = load_existing_config(config_path)
|
| 138 |
+
|
| 139 |
+
# Upload files
|
| 140 |
+
print("\nπ Uploading PDF files...")
|
| 141 |
+
file_mapping = {}
|
| 142 |
+
file_ids = []
|
| 143 |
+
|
| 144 |
+
for pdf_file in pdf_files:
|
| 145 |
+
file_id = upload_file(client, pdf_file)
|
| 146 |
+
if file_id:
|
| 147 |
+
file_mapping[str(pdf_file.absolute())] = file_id
|
| 148 |
+
file_ids.append(file_id)
|
| 149 |
+
else:
|
| 150 |
+
print(f"β οΈ Skipping {pdf_file.name} due to upload failure")
|
| 151 |
+
|
| 152 |
+
if not file_ids:
|
| 153 |
+
print("β Error: No files were successfully uploaded")
|
| 154 |
+
sys.exit(1)
|
| 155 |
+
|
| 156 |
+
print(f"\nβ
Successfully uploaded {len(file_ids)} files")
|
| 157 |
+
|
| 158 |
+
# Create vector store
|
| 159 |
+
vector_store_id = create_vector_store(client, args.name, file_ids)
|
| 160 |
+
if not vector_store_id:
|
| 161 |
+
sys.exit(1)
|
| 162 |
+
|
| 163 |
+
# Create or update assistant
|
| 164 |
+
assistant_id = args.assistant_id or existing_config.get('assistant_id')
|
| 165 |
+
if args.update and assistant_id:
|
| 166 |
+
assistant_id = create_or_update_assistant(client, vector_store_id, assistant_id)
|
| 167 |
+
else:
|
| 168 |
+
assistant_id = create_or_update_assistant(client, vector_store_id)
|
| 169 |
+
|
| 170 |
+
if not assistant_id:
|
| 171 |
+
sys.exit(1)
|
| 172 |
+
|
| 173 |
+
# Save configuration
|
| 174 |
+
config = {
|
| 175 |
+
"vector_store_id": vector_store_id,
|
| 176 |
+
"assistant_id": assistant_id,
|
| 177 |
+
"file_mapping": file_mapping,
|
| 178 |
+
"file_ids": file_ids,
|
| 179 |
+
"upload_timestamp": time.time(),
|
| 180 |
+
"directories": [str(pdf_dir.absolute())]
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
save_config(config_path, config)
|
| 184 |
+
|
| 185 |
+
print("\nπ Success! Your PDFs are ready to query.")
|
| 186 |
+
print(f" Vector Store ID: {vector_store_id}")
|
| 187 |
+
print(f" Assistant ID: {assistant_id}")
|
| 188 |
+
print(f" Config saved to: {config_path}")
|
| 189 |
+
|
| 190 |
+
if __name__ == "__main__":
|
| 191 |
+
main()
|
config/openai_config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"vector_store_id": "vs_68672a4750c48191aa3109c01f105aa2",
|
| 3 |
+
"assistant_id": "asst_3zyFE63fZnAUytz2Yf74Gy7u",
|
| 4 |
+
"file_mapping": {
|
| 5 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Chorus R1.1 Quick Start Guide.pdf": "file-EAqEizav5wZm1LTnsLHyhD",
|
| 6 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Chorus R1.1 User Guide.pdf": "file-AaF5zZ3gYZaNqEQsVj2jRJ",
|
| 7 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.2 Installation Guide.pdf": "file-CHTXYTXXx1LV422DXQAaXP",
|
| 8 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.2 User Guide.pdf": "file-T8VuVTraBJyuGk3TnT4i6Y",
|
| 9 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.5 Installation Guide.pdf": "file-MuAFSRE6zDevVVB6xxqmKg",
|
| 10 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.5 User Guide.pdf": "file-RHRyiwyEJCMHeUc2AWTsY3",
|
| 11 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.6 Installation Guide.pdf": "file-39uzQpefL9bCgyzgPWdViH",
|
| 12 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.6 User Guide.pdf": "file-VejBx1Vsk2psJcZUNGrkjB",
|
| 13 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.8 Installation Guide.pdf": "file-LrbRm4ojftfqSfSLx7uz4U",
|
| 14 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs/Harmony R1.8 User Guide.pdf": "file-PPcDiSxLDo8KsqZ2cPKKmz"
|
| 15 |
+
},
|
| 16 |
+
"file_ids": [
|
| 17 |
+
"file-EAqEizav5wZm1LTnsLHyhD",
|
| 18 |
+
"file-AaF5zZ3gYZaNqEQsVj2jRJ",
|
| 19 |
+
"file-CHTXYTXXx1LV422DXQAaXP",
|
| 20 |
+
"file-T8VuVTraBJyuGk3TnT4i6Y",
|
| 21 |
+
"file-MuAFSRE6zDevVVB6xxqmKg",
|
| 22 |
+
"file-RHRyiwyEJCMHeUc2AWTsY3",
|
| 23 |
+
"file-39uzQpefL9bCgyzgPWdViH",
|
| 24 |
+
"file-VejBx1Vsk2psJcZUNGrkjB",
|
| 25 |
+
"file-LrbRm4ojftfqSfSLx7uz4U",
|
| 26 |
+
"file-PPcDiSxLDo8KsqZ2cPKKmz"
|
| 27 |
+
],
|
| 28 |
+
"upload_timestamp": 1751591502.519109,
|
| 29 |
+
"directories": [
|
| 30 |
+
"/Users/jsv/Work/ataya/concert-master/pdfs"
|
| 31 |
+
]
|
| 32 |
+
}
|
frontend/gradio_app.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Gradio Frontend for OpenAI Chatbot with Model Selection and Token Tracking
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Add backend to path
|
| 11 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
| 12 |
+
|
| 13 |
+
from backend.chatbot_backend import ChatbotBackend
|
| 14 |
+
|
| 15 |
+
class ChatbotUI:
|
| 16 |
+
"""Gradio UI for the chatbot."""
|
| 17 |
+
|
| 18 |
+
def __init__(self):
|
| 19 |
+
"""Initialize the UI."""
|
| 20 |
+
self.backend = ChatbotBackend()
|
| 21 |
+
self.use_documents = True
|
| 22 |
+
|
| 23 |
+
def format_cost_info(self, cost_info: dict) -> str:
|
| 24 |
+
"""Format cost information for display."""
|
| 25 |
+
if not cost_info:
|
| 26 |
+
return ""
|
| 27 |
+
|
| 28 |
+
return f"""
|
| 29 |
+
**Token Usage:**
|
| 30 |
+
- Input: {cost_info.get('input_tokens', 0):,} tokens
|
| 31 |
+
- Output: {cost_info.get('output_tokens', 0):,} tokens
|
| 32 |
+
|
| 33 |
+
**Cost Breakdown:**
|
| 34 |
+
- Input Cost: ${cost_info.get('input_cost', 0):.6f}
|
| 35 |
+
- Output Cost: ${cost_info.get('output_cost', 0):.6f}
|
| 36 |
+
- **Total Cost: ${cost_info.get('total_cost', 0):.6f}**
|
| 37 |
+
|
| 38 |
+
**Performance:**
|
| 39 |
+
- Model: {cost_info.get('model', 'Unknown')}
|
| 40 |
+
- Response Time: {cost_info.get('duration', 0):.1f}s
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def chat_response(self, message: str, history: list, model: str, custom_prompt: str, use_docs: bool):
|
| 44 |
+
"""Generate a chat response."""
|
| 45 |
+
# Set the model
|
| 46 |
+
self.backend.set_model(model)
|
| 47 |
+
|
| 48 |
+
# Get response based on document usage
|
| 49 |
+
if use_docs:
|
| 50 |
+
response, cost_info = self.backend.query_with_documents(message, custom_prompt)
|
| 51 |
+
else:
|
| 52 |
+
response, cost_info = self.backend.query_without_documents(message, custom_prompt)
|
| 53 |
+
|
| 54 |
+
# Format cost info
|
| 55 |
+
cost_display = self.format_cost_info(cost_info)
|
| 56 |
+
|
| 57 |
+
# Return response and cost info
|
| 58 |
+
return response, cost_display
|
| 59 |
+
|
| 60 |
+
def create_interface(self):
|
| 61 |
+
"""Create the Gradio interface."""
|
| 62 |
+
with gr.Blocks(title="OpenAI PDF Chatbot", theme=gr.themes.Soft()) as demo:
|
| 63 |
+
gr.Markdown("""
|
| 64 |
+
# π€ OpenAI PDF Chatbot
|
| 65 |
+
|
| 66 |
+
Chat with your PDF documents using different OpenAI models. Track token usage and costs in real-time.
|
| 67 |
+
""")
|
| 68 |
+
|
| 69 |
+
with gr.Row():
|
| 70 |
+
with gr.Column(scale=3):
|
| 71 |
+
chatbot = gr.Chatbot(
|
| 72 |
+
label="Chat History",
|
| 73 |
+
height=500,
|
| 74 |
+
show_copy_button=True
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
with gr.Row():
|
| 78 |
+
msg = gr.Textbox(
|
| 79 |
+
label="Your Question",
|
| 80 |
+
placeholder="Ask a question about your documents...",
|
| 81 |
+
lines=2,
|
| 82 |
+
scale=4
|
| 83 |
+
)
|
| 84 |
+
submit_btn = gr.Button("Send", variant="primary", scale=1)
|
| 85 |
+
|
| 86 |
+
with gr.Column(scale=1):
|
| 87 |
+
# Model selection
|
| 88 |
+
model_options = list(self.backend.MODEL_CONFIGS.keys())
|
| 89 |
+
model_names = [f"{self.backend.MODEL_CONFIGS[m]['name']} (${self.backend.MODEL_CONFIGS[m]['input_cost']}/${self.backend.MODEL_CONFIGS[m]['output_cost']})"
|
| 90 |
+
for m in model_options]
|
| 91 |
+
|
| 92 |
+
model_dropdown = gr.Dropdown(
|
| 93 |
+
choices=list(zip(model_names, model_options)),
|
| 94 |
+
value=model_options[1], # Default to mini model
|
| 95 |
+
label="Select Model",
|
| 96 |
+
info="Prices shown as (Input/Output) per 1M tokens"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Document usage toggle
|
| 100 |
+
use_docs_checkbox = gr.Checkbox(
|
| 101 |
+
value=True,
|
| 102 |
+
label="Use PDF Documents",
|
| 103 |
+
info="Toggle to query with or without document context"
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# Custom prompt
|
| 107 |
+
custom_prompt = gr.Textbox(
|
| 108 |
+
label="Custom System Prompt (Optional)",
|
| 109 |
+
placeholder="Enter a custom prompt to guide the AI's responses...",
|
| 110 |
+
lines=3
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# Cost display
|
| 114 |
+
cost_display = gr.Markdown(
|
| 115 |
+
label="Usage & Cost",
|
| 116 |
+
value="*No queries yet*"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Clear button
|
| 120 |
+
clear_btn = gr.Button("Clear Chat", variant="secondary")
|
| 121 |
+
|
| 122 |
+
# Response time display at bottom
|
| 123 |
+
response_time_display = gr.Markdown(value="", visible=False)
|
| 124 |
+
|
| 125 |
+
# Event handlers
|
| 126 |
+
def respond(message, history, model, prompt, use_docs):
|
| 127 |
+
"""Handle chat response."""
|
| 128 |
+
response, cost_info = self.chat_response(message, history, model, prompt, use_docs)
|
| 129 |
+
history.append((message, response))
|
| 130 |
+
|
| 131 |
+
# Format response time
|
| 132 |
+
if isinstance(cost_info, dict):
|
| 133 |
+
duration = cost_info.get('duration', 0)
|
| 134 |
+
time_text = f"Responded in {duration:.1f} seconds"
|
| 135 |
+
else:
|
| 136 |
+
time_text = ""
|
| 137 |
+
|
| 138 |
+
return "", history, cost_info, time_text
|
| 139 |
+
|
| 140 |
+
def clear_chat():
|
| 141 |
+
"""Clear chat history."""
|
| 142 |
+
return [], "*No queries yet*", ""
|
| 143 |
+
|
| 144 |
+
# Wire up events
|
| 145 |
+
submit_btn.click(
|
| 146 |
+
respond,
|
| 147 |
+
inputs=[msg, chatbot, model_dropdown, custom_prompt, use_docs_checkbox],
|
| 148 |
+
outputs=[msg, chatbot, cost_display, response_time_display]
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
msg.submit(
|
| 152 |
+
respond,
|
| 153 |
+
inputs=[msg, chatbot, model_dropdown, custom_prompt, use_docs_checkbox],
|
| 154 |
+
outputs=[msg, chatbot, cost_display, response_time_display]
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
clear_btn.click(
|
| 158 |
+
clear_chat,
|
| 159 |
+
outputs=[chatbot, cost_display, response_time_display]
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Instructions
|
| 163 |
+
with gr.Accordion("π Instructions", open=False):
|
| 164 |
+
gr.Markdown("""
|
| 165 |
+
### How to use this chatbot:
|
| 166 |
+
|
| 167 |
+
1. **Select a Model**: Choose from different OpenAI models based on your needs and budget
|
| 168 |
+
2. **Toggle Document Usage**: Enable to search through your uploaded PDFs, disable for general queries
|
| 169 |
+
3. **Custom Prompt**: Optionally add instructions to guide the AI's behavior
|
| 170 |
+
4. **Ask Questions**: Type your question and press Enter or click Send
|
| 171 |
+
5. **Monitor Costs**: Track token usage and costs in real-time
|
| 172 |
+
|
| 173 |
+
### Model Comparison:
|
| 174 |
+
- **GPT-4.1 (Latest)**: Most capable, best for complex tasks
|
| 175 |
+
- **GPT-4.1 Mini**: Balanced performance and cost
|
| 176 |
+
- **GPT-4.1 Nano**: Most economical, good for simple queries
|
| 177 |
+
- **O4 Mini**: Alternative model with competitive pricing
|
| 178 |
+
""")
|
| 179 |
+
|
| 180 |
+
return demo
|
| 181 |
+
|
| 182 |
+
def main():
|
| 183 |
+
"""Main function to run the Gradio app."""
|
| 184 |
+
ui = ChatbotUI()
|
| 185 |
+
demo = ui.create_interface()
|
| 186 |
+
demo.launch(
|
| 187 |
+
server_name="0.0.0.0",
|
| 188 |
+
server_port=7860,
|
| 189 |
+
share=False,
|
| 190 |
+
show_error=True
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
if __name__ == "__main__":
|
| 194 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openai>=1.0.0
|
| 2 |
+
gradio>=4.0.0
|
| 3 |
+
tiktoken>=0.5.0
|
run_chatbot.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Main script to run the OpenAI PDF Chatbot
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Add current directory to path
|
| 11 |
+
sys.path.append(str(Path(__file__).parent))
|
| 12 |
+
|
| 13 |
+
# Import and run the Gradio app
|
| 14 |
+
from frontend.gradio_app import main
|
| 15 |
+
|
| 16 |
+
if __name__ == "__main__":
|
| 17 |
+
print("π Starting OpenAI PDF Chatbot...")
|
| 18 |
+
print("=" * 40)
|
| 19 |
+
print("π Using existing vector store with uploaded PDFs")
|
| 20 |
+
print("π‘ Select different models to compare performance and costs")
|
| 21 |
+
print("=" * 40)
|
| 22 |
+
|
| 23 |
+
# Check for OpenAI API key
|
| 24 |
+
if not os.getenv("OPENAI_API_KEY"):
|
| 25 |
+
print("β οΈ Warning: OPENAI_API_KEY environment variable not set")
|
| 26 |
+
print(" Make sure to set it before running the chatbot")
|
| 27 |
+
|
| 28 |
+
main()
|