Julian Vanecek commited on
Commit
5e87341
Β·
1 Parent(s): 476bbd0
.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()