aibyml commited on
Commit
9c701c3
·
verified ·
1 Parent(s): 0866fda

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +1540 -0
app.py ADDED
@@ -0,0 +1,1540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import gradio as gr
4
+ import pandas as pd
5
+ import re
6
+ from openai import OpenAI
7
+ import requests
8
+ import sys
9
+ from typing import List, Dict, Any, Tuple
10
+ import base64
11
+
12
+ # Add this function at the top of your file
13
+ def get_image_base64(image_path):
14
+ with open(image_path, "rb") as img_file:
15
+ return base64.b64encode(img_file.read()).decode()
16
+
17
+ # Get the base64 string for your logo
18
+ logo_base64 = get_image_base64("Logo.png")
19
+
20
+ # Load the JSON data
21
+ with open('premium_collections.json', 'r') as f:
22
+ premium_collections = json.load(f)
23
+
24
+ with open('clothing.json', 'r') as f:
25
+ clothing = json.load(f)
26
+
27
+ #with open('products.json', 'r') as f:
28
+ # products = json.load(f)
29
+
30
+ # Combine both datasets and tag them with their source
31
+ for item in premium_collections:
32
+ item['source'] = 'premium_collections'
33
+ for item in clothing:
34
+ item['source'] = 'clothing'
35
+
36
+ #for item in products:
37
+ # item['source'] = 'products'
38
+
39
+ #all_items = products
40
+ all_items = premium_collections + clothing
41
+ # Function to normalize price strings to float
42
+ def normalize_price(price_str):
43
+ if not price_str:
44
+ return None
45
+
46
+ # Handle ranges like "$8.50 – $28.00"
47
+ if '–' in price_str or '-' in price_str:
48
+ parts = re.split(r'–|-', price_str)
49
+ # Take the lower price for calculation
50
+ price_str = parts[0].strip()
51
+
52
+ # Extract the numeric value
53
+ match = re.search(r'(\d+\.\d+|\d+)', price_str)
54
+ if match:
55
+ return float(match.group(1))
56
+ return None
57
+
58
+ # Process items to have normalized prices
59
+ for item in all_items:
60
+ if item.get('price'):
61
+ item['normalized_price'] = normalize_price(item['price'])
62
+
63
+ # Define all the dropdown options
64
+ AGE_GROUPS = ["Choose an option", "<18", "18-30", "30-40", "40-50", "50-60", ">60"]
65
+ GIFT_OCCASIONS = [
66
+ "Choose an option",
67
+ "Festive Celebration",
68
+ "Long Service Award",
69
+ "Corporate Milestones",
70
+ "Onboarding",
71
+ "Christmas/Year-End Celebration",
72
+ "Annual Dinner & Dance",
73
+ "All The Best!",
74
+ "Others"
75
+ ]
76
+ COLOR_THEMES = [
77
+ "Choose an option",
78
+ "Black", "White", "Off-White", "Brown", "Red", "Blue", "Gray",
79
+ "Gold", "Yellow", "Purple", "Pink", "Green", "Silver",
80
+ "Orange", "Multi-color", "Transparent"
81
+ ]
82
+ JOB_FUNCTIONS = [
83
+ "Choose an option",
84
+ "C-Suite",
85
+ "Sales & Business Development",
86
+ "Finance",
87
+ "Operations",
88
+ "Human Resource",
89
+ "Engineering",
90
+ "Information Technology",
91
+ "Marketing & Communications",
92
+ "Others"
93
+ ]
94
+ GENDERS = ["Choose an option", "Male", "Female", "does not really matter"]
95
+
96
+ # Budget options for the new interface
97
+ BUDGET_RANGES = [
98
+ "Below S$10",
99
+ "S$10 to S$20",
100
+ "S$20 to S$35",
101
+ "S$35 to S$55",
102
+ "S$55 to S$80"
103
+ ]
104
+
105
+ # Configure API keys
106
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
107
+ OLLAMA_API_URL = "http://localhost:11434/api/generate" # Default Ollama URL
108
+
109
+ class BudgetAgent:
110
+ def __init__(self, items, model="deepseek-r1:32b"):
111
+ self.items = items
112
+ self.model = model
113
+
114
+ def calculate_bundle(self, min_budget: float, max_budget: float, selected_items: list) -> tuple:
115
+ """
116
+ Calculate if the selected items fit within the budget range.
117
+ Returns: (fits_budget, total_cost, explanation)
118
+ """
119
+ # Filter out items without valid prices
120
+ valid_items = [item for item in selected_items if item.get('normalized_price') is not None]
121
+
122
+ if not valid_items:
123
+ return False, 0, "No items with valid prices were selected."
124
+
125
+ total_cost = sum(item['normalized_price'] for item in valid_items)
126
+
127
+ # Check if total fits within budget range
128
+ fits_budget = min_budget <= total_cost <= max_budget
129
+
130
+ # Create explanation
131
+ item_details = [f"{item['name']} (S${item['normalized_price']:.2f})" for item in valid_items]
132
+ explanation = f"Total cost: S${total_cost:.2f} for items: {', '.join(item_details)}. "
133
+
134
+ if fits_budget:
135
+ explanation += f"This bundle is within your budget range of S${min_budget:.2f} to S${max_budget:.2f}."
136
+ else:
137
+ if total_cost < min_budget:
138
+ explanation += f"This bundle is below your minimum budget of S${min_budget:.2f} by S${min_budget - total_cost:.2f}."
139
+ else:
140
+ explanation += f"This bundle exceeds your maximum budget of S${max_budget:.2f} by S${total_cost - max_budget:.2f}."
141
+
142
+ return fits_budget, total_cost, explanation
143
+
144
+ def query_ollama(self, prompt):
145
+ """Query the local Ollama model"""
146
+ try:
147
+ payload = {
148
+ "model": self.model,
149
+ "prompt": prompt,
150
+ "stream": False
151
+ }
152
+ response = requests.post(OLLAMA_API_URL, json=payload)
153
+ if response.status_code == 200:
154
+ return response.json().get("response", "Error: No response from Ollama")
155
+ else:
156
+ return f"Error: {response.status_code} - {response.text}"
157
+ except Exception as e:
158
+ return f"Error connecting to Ollama: {str(e)}"
159
+
160
+ # Update the BudgetAgent's optimize_bundle method to respect the desired number of items
161
+ def optimize_bundle(self, min_budget: float, max_budget: float, items: list, criteria: str = None, num_items: str = "Any number") -> list:
162
+ """
163
+ Optimize item selection to fit within budget range while maximizing value.
164
+ The criteria parameter is optional for additional filtering logic.
165
+ The num_items parameter allows specifying the desired number of items in the bundle.
166
+ """
167
+ # Filter out items without prices
168
+ valid_items = [item for item in items if item.get('normalized_price') is not None]
169
+
170
+ if not valid_items:
171
+ return []
172
+
173
+ # Sort items by price (highest first) for initial selection
174
+ valid_items.sort(key=lambda x: x.get('normalized_price', 0), reverse=True)
175
+
176
+ # Calculate current total
177
+ current_total = sum(item.get('normalized_price', 0) for item in valid_items)
178
+
179
+ # If over budget, remove expensive items
180
+ if current_total > max_budget:
181
+ while valid_items and current_total > max_budget:
182
+ removed_item = valid_items.pop(0) # Remove the most expensive item
183
+ current_total -= removed_item.get('normalized_price', 0)
184
+
185
+ # If under minimum budget, try to add more items
186
+ if current_total < min_budget:
187
+ remaining_items = [item for item in self.items if
188
+ item.get('normalized_price') is not None and
189
+ item not in valid_items]
190
+
191
+ # Sort by price (ascending) to add cheaper items first
192
+ remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
193
+
194
+ for item in remaining_items:
195
+ item_price = item.get('normalized_price', 0)
196
+ if current_total + item_price <= max_budget:
197
+ valid_items.append(item)
198
+ current_total += item_price
199
+ if current_total >= min_budget:
200
+ break
201
+
202
+ # Now adjust based on the desired number of items
203
+ if num_items != "Any number":
204
+ desired_count = 0
205
+
206
+ if num_items == "1 item only":
207
+ desired_count = 1
208
+ elif num_items == "2 items":
209
+ desired_count = 2
210
+ elif num_items == "3 items":
211
+ desired_count = 3
212
+ elif num_items == "4 items":
213
+ desired_count = 4
214
+ elif num_items == "5 or more items":
215
+ desired_count = 5 # We'll use 5 as the minimum for this category
216
+
217
+ current_count = len(valid_items)
218
+
219
+ if num_items == "5 or more items" and current_count < desired_count:
220
+ # Add more items to reach the minimum of 5 for this category
221
+ remaining_items = [item for item in self.items if
222
+ item.get('normalized_price') is not None and
223
+ item not in valid_items]
224
+
225
+ # Sort by price (ascending) to add cheaper items first
226
+ remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
227
+
228
+ for item in remaining_items:
229
+ item_price = item.get('normalized_price', 0)
230
+ if current_total + item_price <= max_budget:
231
+ valid_items.append(item)
232
+ current_total += item_price
233
+ current_count += 1
234
+
235
+ # Stop when we reach the desired count
236
+ if current_count >= desired_count:
237
+ break
238
+
239
+ elif current_count > desired_count:
240
+ # Too many items, remove the lowest value ones
241
+ valid_items.sort(key=lambda x: x.get('normalized_price', 0))
242
+
243
+ while len(valid_items) > desired_count:
244
+ removed_item = valid_items.pop(0) # Remove the cheapest item
245
+ current_total -= removed_item.get('normalized_price', 0)
246
+
247
+ elif current_count < desired_count:
248
+ # Too few items, add more while staying under budget
249
+ remaining_items = [item for item in self.items if
250
+ item.get('normalized_price') is not None and
251
+ item not in valid_items]
252
+
253
+ # Sort by price (ascending) to add cheaper items first
254
+ remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
255
+
256
+ for item in remaining_items:
257
+ item_price = item.get('normalized_price', 0)
258
+ if current_total + item_price <= max_budget:
259
+ valid_items.append(item)
260
+ current_total += item_price
261
+ current_count += 1
262
+
263
+ # Stop when we reach the desired count
264
+ if current_count >= desired_count:
265
+ break
266
+
267
+ # Return the optimized selection
268
+ return valid_items
269
+
270
+
271
+ def adjust_bundle_to_fit_total_budget(self, bundle_items: list, min_budget: float, max_budget: float, total_budget: float) -> tuple:
272
+ """
273
+ Adjust the number of bundle packages to fit within the total budget.
274
+ Returns: (num_packages, total_cost, explanation)
275
+ """
276
+ if not bundle_items:
277
+ return 0, 0, "No items in the bundle to calculate."
278
+
279
+ # Calculate the cost of a single bundle
280
+ bundle_cost = sum(item.get('normalized_price', 0) for item in bundle_items)
281
+
282
+ if bundle_cost == 0:
283
+ return 0, 0, "Bundle has no valid price information."
284
+
285
+ # Calculate how many complete bundles fit within the total budget
286
+ max_packages = int(total_budget / bundle_cost)
287
+
288
+ if max_packages == 0:
289
+ return 0, 0, f"The bundle cost (S${bundle_cost:.2f}) exceeds your total budget of S${total_budget:.2f}."
290
+
291
+ total_cost = bundle_cost * max_packages
292
+
293
+ # Check if the bundle meets minimum budget requirements
294
+ bundle_meets_min = bundle_cost >= min_budget
295
+
296
+ explanation = f"Each bundle costs S${bundle_cost:.2f}. "
297
+
298
+ if not bundle_meets_min:
299
+ explanation += f"Note: The bundle is below your minimum item budget of S${min_budget:.2f}. "
300
+
301
+ explanation += f"You can purchase {max_packages} complete bundle(s) for a total of S${total_cost:.2f}, "
302
+ explanation += f"which is within your total budget of S${total_budget:.2f}."
303
+
304
+ if total_budget - total_cost > 0:
305
+ explanation += f" You will have S${total_budget - total_cost:.2f} remaining."
306
+
307
+ return max_packages, total_cost, explanation
308
+
309
+ # In the SelectionAgent class, modify the select_items method to include total_budget parameter
310
+
311
+ # In the SelectionAgent class, modify the select_items method to include total_budget parameter
312
+
313
+ class SelectionAgent:
314
+ def __init__(self, items):
315
+ self.items = items
316
+ self.client = OpenAI(api_key=OPENAI_API_KEY)
317
+ # Define weights for different criteria
318
+ self.weights = {
319
+ "query": 0.30, # Free text query (30%)
320
+ "budget": { # Budget considerations (30%)
321
+ "price_range": 0.15, # Budget range per package
322
+ "total_budget": 0.10, # Total budget
323
+ "item_count": 0.13 # Number of items per package
324
+ },
325
+ "demographics": { # Demographics (20%)
326
+ "age_group": 0.04, # Age group
327
+ "gender": 0.04, # Gender
328
+ "job_function": 0.04 # Professional role
329
+ },
330
+ "occasion": 0.10, # Gift occasion (10%)
331
+ "other": { # Other factors (10%)
332
+ "color_theme": 0.05, # Color preferences
333
+ "misc": 0.05
334
+ }
335
+ }
336
+
337
+ def adjust_to_item_count(self, items: List[Dict], target_count: int, min_budget: float, max_budget: float) -> List[Dict]:
338
+ """
339
+ Adjust the selection to match the target item count while staying within budget.
340
+ """
341
+ if not items or target_count <= 0:
342
+ return []
343
+
344
+ current_count = len(items)
345
+
346
+ # If we already have the right number, return as is
347
+ if current_count == target_count:
348
+ return items
349
+
350
+ # Calculate current total cost
351
+ current_total = sum(item.get('normalized_price', 0) for item in items if item.get('normalized_price') is not None)
352
+
353
+ if current_count < target_count:
354
+ # We need to add more items
355
+ # Find items not already in our selection
356
+ remaining_items = [item for item in self.items if item.get('normalized_price') is not None
357
+ and item not in items]
358
+
359
+ # Sort by price (ascending) to add cheaper items first
360
+ remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
361
+
362
+ for item in remaining_items:
363
+ item_price = item.get('normalized_price', 0)
364
+ if current_total + item_price <= max_budget:
365
+ items.append(item)
366
+ current_total += item_price
367
+ current_count += 1
368
+
369
+ if current_count >= target_count:
370
+ break
371
+ else:
372
+ # We need to remove some items
373
+ # Sort by price (ascending) so we remove cheaper items first
374
+ # This helps maintain value while reducing count
375
+ items.sort(key=lambda x: x.get('normalized_price', 0))
376
+
377
+ while current_count > target_count:
378
+ removed_item = items.pop(0) # Remove the cheapest item
379
+ current_total -= removed_item.get('normalized_price', 0)
380
+ current_count -= 1
381
+
382
+ return items
383
+
384
+ def optimize_selection(self, items: List[Dict], min_budget: float, max_budget: float) -> List[Dict]:
385
+ """
386
+ Optimize the selection to maximize budget utilization while staying within limits.
387
+ """
388
+ if not items:
389
+ return []
390
+
391
+ # Calculate current total
392
+ current_total = sum(item.get('normalized_price', 0) for item in items if item.get('normalized_price') is not None)
393
+
394
+ if current_total > max_budget:
395
+ # Over budget, need to remove items
396
+ # Sort by price (descending) to remove expensive items first
397
+ items.sort(key=lambda x: x.get('normalized_price', 0), reverse=True)
398
+
399
+ while items and current_total > max_budget:
400
+ removed_item = items.pop(0) # Remove the most expensive item
401
+ current_total -= removed_item.get('normalized_price', 0)
402
+
403
+ elif current_total < min_budget:
404
+ # Under minimum budget, try to add more items
405
+ remaining_items = [item for item in self.items if item.get('normalized_price') is not None
406
+ and item not in items]
407
+
408
+ # Sort by price (descending) to add valuable items first
409
+ remaining_items.sort(key=lambda x: x.get('normalized_price', 0), reverse=True)
410
+
411
+ for item in remaining_items:
412
+ item_price = item.get('normalized_price', 0)
413
+ if current_total + item_price <= max_budget:
414
+ items.append(item)
415
+ current_total += item_price
416
+
417
+ if current_total >= min_budget:
418
+ break
419
+
420
+ return items
421
+
422
+ def select_items(self, criteria: str, min_budget: float, max_budget: float, item_count: int = None,
423
+ age_group: str = "", gift_occasion: str = "", color_theme: str = "",
424
+ job_function: str = "", gender: str = "", quantity: str = "",
425
+ total_budget: float = 500.0) -> List[Dict]: # Added total_budget parameter with default
426
+ """
427
+ Use OpenAI to select items based on the criteria with weighted importance.
428
+ Returns a list of items that fit the criteria.
429
+ """
430
+ # Prepare the data for OpenAI (limit the number of items to avoid token limits)
431
+ items_sample = self.items[:50] # Take a sample to avoid token limits
432
+
433
+ # Extract discount information for each item
434
+ for item in items_sample:
435
+ has_discount, discount_info, _ = extract_discount_info(item)
436
+ if has_discount:
437
+ item['has_bulk_discount'] = True
438
+ item['discount_info'] = discount_info
439
+
440
+ items_data = json.dumps([{
441
+ "name": item['name'],
442
+ "type": item['type'],
443
+ "price": item.get('normalized_price'),
444
+ "description": item.get('short_description', '')[:100],
445
+ "labels": item.get('labels', []),
446
+ "has_bulk_discount": item.get('has_bulk_discount', False),
447
+ "discount_info": item.get('discount_info', '')
448
+ } for item in items_sample])
449
+
450
+ # Create the system prompt with weighted criteria
451
+ system_prompt = """
452
+ You are a gift selection expert. Your task is to select items that best match the user's criteria with the following priority weights:
453
+
454
+ 1. QUERY (30%): The user's free text description of what they're looking for is the most important factor.
455
+ - Pay close attention to specific item types, materials, or features mentioned
456
+ - Understand the intent and purpose behind the request
457
+
458
+ 2. BUDGET CONSIDERATIONS (30%):
459
+ - Budget range per package (15%): Select items that fit within the specified budget range
460
+ - Total budget (10%): Consider how many packages can be created within the total budget
461
+ - Number of items per package (5%): Aim to include the requested number of items
462
+
463
+ 3. DEMOGRAPHICS (20%):
464
+ - Age Group (4%): Select age-appropriate items
465
+ - Gender (8%): Consider gender preferences if specified
466
+ - Job Function/Professional role (8%): Select items appropriate for the recipient's professional context
467
+
468
+ 4. OCCASION (10%):
469
+ - Match items to the specific occasion or purpose of the gift
470
+
471
+ 5. OTHER FACTORS (10%):
472
+ - Color theme (5%): Consider color preferences
473
+
474
+ Pay special attention to items that offer bulk discounts and prioritize them when appropriate.
475
+ Return your selections as a JSON array of item names that meet the criteria.
476
+ """
477
+
478
+ if item_count:
479
+ system_prompt += f"\nSelect EXACTLY {item_count} items for the package if possible."
480
+ else:
481
+ system_prompt += "\nTry to select MORE THAN ONE item."
482
+
483
+ budget_text = f"a maximum budget of S${max_budget:.2f}"
484
+ if min_budget > 0:
485
+ budget_text = f"a budget range of S${min_budget:.2f} to S${max_budget:.2f}"
486
+
487
+ item_count_text = ""
488
+ if item_count:
489
+ item_count_text = f" containing exactly {item_count} items"
490
+
491
+ # Build a structured user prompt that clearly separates the different criteria by importance
492
+ user_prompt = f"""
493
+ I have {budget_text} and I'm looking for items{item_count_text} that match these criteria:
494
+
495
+ PRIMARY REQUIREMENT (30% weight):
496
+ {criteria}
497
+
498
+ BUDGET DETAILS (30% weight):
499
+ - Budget per package: {budget_text}
500
+ - Total budget available: S${total_budget:.2f}
501
+ - Desired items per package: {item_count if item_count else "Multiple items"}
502
+
503
+ RECIPIENT DEMOGRAPHICS (20% weight):
504
+ """
505
+
506
+ # Add demographic information if provided
507
+ if age_group and age_group != "Choose an option":
508
+ user_prompt += f"- Age group: {age_group}\n "
509
+ if gender and gender != "Choose an option" and gender != "does not really matter":
510
+ user_prompt += f"- Gender: {gender}\n "
511
+ if job_function and job_function != "Choose an option":
512
+ user_prompt += f"- Job function: {job_function}\n "
513
+
514
+ user_prompt += f"""
515
+ OCCASION (10% weight):
516
+ {gift_occasion if gift_occasion and gift_occasion != "Choose an option" else "Not specified"}
517
+
518
+ OTHER PREFERENCES (10% weight):
519
+ """
520
+
521
+ # Add other preferences if provided
522
+ if color_theme and color_theme != "Choose an option":
523
+ user_prompt += f"- Color preference: {color_theme}\n "
524
+ if quantity and quantity != "Choose quantity":
525
+ user_prompt += f"- Quantity: {quantity}\n "
526
+
527
+ user_prompt += f"""
528
+ Here are the available items:
529
+ {items_data}
530
+
531
+ Please select the items that best match my criteria according to the weighted priorities and MAXIMIZE the budget utilization while staying under the maximum budget limit. Try to get as close as possible to the maximum budget.
532
+
533
+ If an item has bulk discounts available (has_bulk_discount=true), prioritize these items when appropriate for the budget.
534
+
535
+ Return a JSON object with a key called "items" that contains an array of item names, like this:
536
+ {{"items": ["Item 1", "Item 2", "Item 3"]}}
537
+ """
538
+
539
+ try:
540
+ response = self.client.chat.completions.create(
541
+ model="gpt-4o",
542
+ messages=[
543
+ {"role": "system", "content": system_prompt},
544
+ {"role": "user", "content": user_prompt}
545
+ ],
546
+ response_format={"type": "json_object"},
547
+ temperature=0.1
548
+ )
549
+
550
+ # Extract the selected item names
551
+ result = json.loads(response.choices[0].message.content)
552
+ selected_item_names = result.get("items", [])
553
+ if not isinstance(selected_item_names, list):
554
+ # Try to handle different response formats
555
+ if isinstance(result, list):
556
+ selected_item_names = result
557
+ else:
558
+ # Look for any list in the response
559
+ for value in result.values():
560
+ if isinstance(value, list):
561
+ selected_item_names = value
562
+ break
563
+
564
+ # Find the corresponding items from our pool
565
+ selected_items = []
566
+ for name in selected_item_names:
567
+ for item in self.items:
568
+ if name.lower() in item['name'].lower() or item['name'].lower() in name.lower():
569
+ # Add discount info to the item
570
+ has_discount, discount_info, formatted_discount = extract_discount_info(item)
571
+ if has_discount:
572
+ item['has_bulk_discount'] = True
573
+ item['discount_info'] = discount_info
574
+ item['formatted_discount'] = formatted_discount
575
+
576
+ selected_items.append(item)
577
+ break
578
+
579
+ # Check if we need to adjust the selection based on the exact item count
580
+ if item_count and len(selected_items) != item_count:
581
+ selected_items = self.adjust_to_item_count(selected_items, item_count, min_budget, max_budget)
582
+ else:
583
+ # Optimize selection to maximize budget utilization
584
+ selected_items = self.optimize_selection(selected_items, min_budget, max_budget)
585
+
586
+ return selected_items
587
+ except Exception as e:
588
+ print(f"Error calling OpenAI API: {str(e)}")
589
+ return []
590
+
591
+ # Now we need to update the process_query method in GiftBundleChatbot to pass the total_budget parameter
592
+ class GiftBundleChatbot:
593
+ def __init__(self, items):
594
+ self.items = items
595
+ self.budget_agent = BudgetAgent(items)
596
+ self.selection_agent = SelectionAgent(items)
597
+
598
+ def process_query(self, query: str, min_budget: float = 0, max_budget: float = 500,
599
+ total_budget: float = 500, item_count: int = None, age_group: str = "",
600
+ gift_occasion: str = "", color_theme: str = "", job_function: str = "",
601
+ gender: str = "", quantity: str = "") -> Tuple[str, List[Dict]]:
602
+ """
603
+ Process a user query and return recommendations based on weighted criteria.
604
+ """
605
+ # Use the selection agent to get items based on criteria with weights
606
+ selected_items = self.selection_agent.select_items(
607
+ criteria=query or "Find me gift items",
608
+ min_budget=min_budget,
609
+ max_budget=max_budget,
610
+ item_count=item_count,
611
+ age_group=age_group,
612
+ gift_occasion=gift_occasion,
613
+ color_theme=color_theme,
614
+ job_function=job_function,
615
+ gender=gender,
616
+ quantity=quantity,
617
+ total_budget=total_budget # Pass the total_budget parameter
618
+ )
619
+
620
+ if not selected_items:
621
+ # Fallback to the budget agent for optimization
622
+ selected_items = self.budget_agent.optimize_bundle(min_budget, max_budget, self.items, query)
623
+
624
+ # Adjust to match the requested item count if specified
625
+ if item_count and len(selected_items) != item_count:
626
+ # Use the selection agent's method to adjust the count
627
+ selected_items = self.selection_agent.adjust_to_item_count(
628
+ selected_items, item_count, min_budget, max_budget
629
+ )
630
+
631
+ # Check if the selected items fit within the per-package budget range
632
+ fits_budget, total_cost, explanation = self.budget_agent.calculate_bundle(min_budget, max_budget, selected_items)
633
+
634
+ # Calculate how many packages fit within the total budget
635
+ num_packages, total_packages_cost, total_budget_explanation = self.budget_agent.adjust_bundle_to_fit_total_budget(
636
+ selected_items, min_budget, max_budget, total_budget
637
+ )
638
+
639
+ # Format the response
640
+ if not selected_items:
641
+ return "I couldn't find any items matching your criteria within your budget range. Please try different criteria or adjust your budget.", []
642
+
643
+ budget_text = f"per-package budget of S${min_budget:.2f}-S${max_budget:.2f}"
644
+ total_budget_text = f"total budget of S${total_budget:.2f}"
645
+ item_count_text = f"with {len(selected_items)} items per package" if item_count else ""
646
+
647
+ response = f"Based on your criteria: '{query or 'Gift items'}'"
648
+
649
+ # Add filter information to response if selected
650
+ filters_used = []
651
+ if item_count:
652
+ filters_used.append(f"Items per package: {item_count}")
653
+ if age_group and age_group != "Choose an option":
654
+ filters_used.append(f"Age group: {age_group}")
655
+ if gift_occasion and gift_occasion != "Choose an option":
656
+ filters_used.append(f"Occasion: {gift_occasion}")
657
+ if color_theme and color_theme != "Choose an option":
658
+ filters_used.append(f"Color: {color_theme}")
659
+ if job_function and job_function != "Choose an option":
660
+ filters_used.append(f"Job function: {job_function}")
661
+ if gender and gender != "Choose an option" and gender != "does not really matter":
662
+ filters_used.append(f"Gender: {gender}")
663
+ if quantity and quantity != "Choose quantity":
664
+ filters_used.append(f"Quantity: {quantity}")
665
+ if filters_used:
666
+ response += f"\nFilters: {', '.join(filters_used)}"
667
+
668
+ response += f"\nBudget: {budget_text} {item_count_text} with {total_budget_text}\n\nI recommend:\n\n"
669
+
670
+ # Check for items with bulk discounts to highlight them
671
+ discount_items = []
672
+
673
+ for item in selected_items:
674
+ price_display = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "Price not available"
675
+
676
+ # Check if this item has bulk discount information
677
+ if item.get('has_bulk_discount', False):
678
+ response += f"- {item['name']} ({price_display}) 💰 BULK DISCOUNT AVAILABLE 💰\n {item.get('short_description', 'No description')[:100]}...\n"
679
+ response += f" {item.get('formatted_discount', '')}\n\n"
680
+ discount_items.append(item['name'])
681
+ else:
682
+ response += f"- {item['name']} ({price_display})\n {item.get('short_description', 'No description')[:100]}...\n\n"
683
+
684
+ response += f"\n{explanation.replace('$', 'S$')}"
685
+
686
+ # Add total budget explanation
687
+ response += f"\n\n{total_budget_explanation.replace('$', 'S$')}"
688
+
689
+ # Add special note about bulk discounts if any were found
690
+ if discount_items:
691
+ response += f"\n\n💰 SPECIAL NOTE: {len(discount_items)} item(s) in your selection offer bulk discounts: {', '.join(discount_items)}. Consider ordering in larger quantities to save money!"
692
+
693
+ # Add recommendation to adjust budget if needed
694
+ if not fits_budget:
695
+ if total_cost > max_budget:
696
+ response += "\n\nWould you like to increase your maximum per-package budget or see a different selection?"
697
+ elif min_budget > 0 and total_cost < min_budget:
698
+ response += "\n\nWould you like to decrease your minimum per-package budget or see a selection with additional items?"
699
+
700
+ if num_packages == 0:
701
+ response += "\n\nThe cost of this bundle exceeds your total budget. Would you like to increase your total budget or see a more affordable selection?"
702
+
703
+ return response, selected_items
704
+
705
+ def parse_budget_range(budget_range):
706
+ """Parse a budget range string into min and max values"""
707
+ if budget_range == "Below S$10":
708
+ return 0, 10
709
+ elif budget_range == "S$10 to S$20":
710
+ return 10, 20
711
+ elif budget_range == "S$20 to S$35":
712
+ return 20, 35
713
+ elif budget_range == "S$35 to S$55":
714
+ return 35, 55
715
+ elif budget_range == "S$55 to S$80":
716
+ return 55, 80
717
+ else:
718
+ # Default range if no match
719
+ return 0, 500
720
+
721
+ def extract_discount_info(item):
722
+ """
723
+ Extract bulk discount information from item description.
724
+ Returns: (has_discount, discount_info, formatted_info)
725
+ """
726
+ has_discount = False
727
+ discount_info = None
728
+ formatted_info = ""
729
+
730
+ # Check if the item has a description
731
+ description = item.get('short_description', '') or item.get('description', '')
732
+ if not description:
733
+ return has_discount, discount_info, formatted_info
734
+
735
+ # Keywords that might indicate a bulk discount
736
+ discount_keywords = [
737
+ 'bulk discount', 'volume discount', 'quantity discount',
738
+ 'bulk pricing', 'buy more save more', 'discount for quantities',
739
+ 'bulk purchase', 'special pricing', 'wholesale price',
740
+ 'bulk orders', 'quantity pricing', 'discount for bulk'
741
+ ]
742
+
743
+ description_lower = description.lower()
744
+
745
+ # Check for discount keywords
746
+ for keyword in discount_keywords:
747
+ if keyword in description_lower:
748
+ has_discount = True
749
+ break
750
+
751
+ if has_discount:
752
+ # Try to extract sentences containing discount information
753
+ sentences = description.split('.')
754
+ discount_sentences = []
755
+
756
+ for sentence in sentences:
757
+ sentence = sentence.strip()
758
+ sentence_lower = sentence.lower()
759
+
760
+ for keyword in discount_keywords:
761
+ if keyword in sentence_lower and sentence:
762
+ discount_sentences.append(sentence)
763
+ break
764
+
765
+ if discount_sentences:
766
+ discount_info = '. '.join(discount_sentences) + '.'
767
+ formatted_info = f"<strong>Bulk Discount:</strong> {discount_info}"
768
+ else:
769
+ # If we can't extract specific sentences, use entire description
770
+ discount_info = description
771
+ formatted_info = f"<strong>Bulk Discount Available</strong> (see description for details)"
772
+
773
+ return has_discount, discount_info, formatted_info
774
+
775
+ # Set up the Gradio interface
776
+ def gift_finder_interface(budget_range, budget_total, package_item_count, color, query):
777
+ """
778
+ Fixed gift_finder_interface function that incorporates package item count and discount information
779
+ """
780
+ # Parse the budget range
781
+ min_budget, max_budget = parse_budget_range(budget_range)
782
+
783
+ # Get the total budget
784
+ try:
785
+ if budget_total and float(budget_total) > 0:
786
+ total_budget = float(budget_total)
787
+ else:
788
+ total_budget = 100.0
789
+ except (ValueError, TypeError):
790
+ total_budget = 100.0
791
+ # Get the number of items per package
792
+ item_count = int(package_item_count) if package_item_count else None
793
+
794
+ # Initialize the chatbot
795
+ chatbot = GiftBundleChatbot(all_items)
796
+
797
+ # Process the query with all filters
798
+ response, selected_items = chatbot.process_query(
799
+ query=query,
800
+ min_budget=min_budget,
801
+ max_budget=max_budget,
802
+ total_budget=total_budget,
803
+ item_count=item_count,
804
+ color_theme=color,
805
+ )
806
+
807
+ # Create DataFrame for bundle display
808
+ if selected_items:
809
+ # Calculate bundle cost
810
+ package_cost = sum(item['normalized_price'] for item in selected_items if item['normalized_price'] is not None)
811
+
812
+ # Calculate how many packages fit in total budget
813
+ max_packages = int(total_budget / package_cost) if package_cost > 0 else 0
814
+ total_cost = package_cost * max_packages if max_packages > 0 else 0
815
+
816
+ # Create dataframe with discount information highlighted
817
+ bundle_data = []
818
+ for item in selected_items:
819
+ price_display = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
820
+
821
+ # Add discount note if available
822
+ discount_note = ""
823
+ if item.get('has_bulk_discount', False):
824
+ discount_note = "💰 BULK DISCOUNT AVAILABLE"
825
+
826
+ # Prepare description with discount info if available
827
+ description = item.get('short_description', 'No description')[:100]
828
+ if item.get('formatted_discount', ''):
829
+ description += f"\n{item.get('formatted_discount', '')}"
830
+
831
+ bundle_data.append({
832
+ "Name": item['name'],
833
+ "Price (S$)": price_display,
834
+ "Type": item['type'],
835
+ "Bulk Discount": discount_note,
836
+ "Description": description
837
+ })
838
+
839
+ bundle_df = pd.DataFrame(bundle_data)
840
+
841
+ budget_utilization = (total_cost / total_budget) * 100 if total_budget > 0 else 0
842
+
843
+ bundle_summary = f"Package Cost: S${package_cost:.2f}\n" \
844
+ f"Items per Package: {len(selected_items)}\n" \
845
+ f"Number of Packages Possible: {max_packages}\n" \
846
+ f"Total Cost: S${total_cost:.2f}\n" \
847
+ f"Total Budget: S${total_budget:.2f}\n" \
848
+ f"Budget Utilization: {budget_utilization:.1f}%"
849
+
850
+ # Create HTML for displaying images with discount badges
851
+ html_content = "<div style='display: flex; flex-wrap: wrap; gap: 20px;'>"
852
+ count = 0
853
+
854
+ # Debug: Print image information for each item
855
+ print("Image debug information:")
856
+ for i, item in enumerate(selected_items):
857
+ print(f"Item {i+1}: {item['name']}")
858
+ print(f" Has 'images' key: {'Yes' if 'images' in item else 'No'}")
859
+ if 'images' in item:
860
+ print(f" Images value type: {type(item['images'])}")
861
+ print(f" Images value: {str(item['images'])[:100]}") # Show first 100 chars
862
+
863
+ # Process each item for images
864
+ for item in selected_items:
865
+ # Check if the item has images key
866
+ if 'images' in item and item['images']:
867
+ try:
868
+ # Get the image URL - handle different possible formats
869
+ image_url = None
870
+
871
+ if isinstance(item['images'], str):
872
+ # Direct URL string
873
+ image_url = item['images']
874
+ elif isinstance(item['images'], list) and len(item['images']) > 0:
875
+ # List of URLs - take the first one
876
+ image_url = item['images'][0]
877
+ elif isinstance(item['images'], dict) and len(item['images']) > 0:
878
+ # Dictionary of URLs - take the first value
879
+ image_url = list(item['images'].values())[0]
880
+
881
+ # If it's a relative URL, convert to absolute (example)
882
+ if image_url and image_url.startswith('/'):
883
+ # This is just an example - update with your actual domain
884
+ image_url = f"https://yourdomain.com{image_url}"
885
+
886
+ # Print the processed URL for debugging
887
+ print(f"Processed image URL for {item['name']}: {image_url}")
888
+
889
+ if image_url:
890
+ # Create HTML for the image with caption
891
+ item_price = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
892
+
893
+ # Add a discount badge if available
894
+ discount_badge = ""
895
+ if item.get('has_bulk_discount', False):
896
+ discount_badge = """
897
+ <div style='position: absolute; top: 5px; right: 5px; background-color: #FF9800; color: white; padding: 5px; border-radius: 4px; font-size: 0.8em;'>
898
+ BULK DISCOUNT
899
+ </div>
900
+ """
901
+
902
+ html_content += f"""
903
+ <div style='border: 1px solid #ddd; border-radius: 8px; padding: 10px; max-width: 250px; position: relative;'>
904
+ {discount_badge}
905
+ <img src="{image_url}" alt="{item['name']}" style='width: 100%; max-height: 200px; object-fit: contain;'>
906
+ <p style='margin-top: 8px; font-weight: bold;'>{item['name']}</p>
907
+ <p>{item_price}</p>
908
+ """
909
+
910
+ # Add discount info if available
911
+ if item.get('formatted_discount', ''):
912
+ html_content += f"""<p style='color: #FF9800; font-weight: bold;'>{item.get('formatted_discount', '')}</p>
913
+ """
914
+
915
+ html_content += "</div>"
916
+ count += 1
917
+ except Exception as e:
918
+ print(f"Error processing image for {item['name']}: {str(e)}")
919
+
920
+ # Close the container div
921
+ html_content += "</div>"
922
+
923
+ # If no images were found
924
+ if count == 0:
925
+ html_content = """
926
+ <div>
927
+ <p>No images were found for the selected items.</p>
928
+ <p>Check the console logs for debugging information about the image URLs.</p>
929
+ </div>
930
+ """
931
+ else:
932
+ bundle_df = pd.DataFrame(columns=["Name", "Price (S$)", "Type", "Bulk Discount", "Description"])
933
+ bundle_summary = "No items selected"
934
+ html_content = "<p>No items selected.</p>"
935
+
936
+ return response, bundle_df, bundle_summary, html_content
937
+
938
+ # Custom CSS to match the Gift Market homepage style
939
+ css = """
940
+ :root {
941
+ --primary-color: #87CEEB;
942
+ --secondary-color: #3C3B6E;
943
+ --background-color: #f0f2f5;
944
+ --border-color: #ddd;
945
+ }
946
+
947
+ body {
948
+ font-family: 'Arial', sans-serif;
949
+ background-color: var(--background-color);
950
+ }
951
+
952
+ .main-container {
953
+ max-width: 1200px;
954
+ margin: 0 auto;
955
+ }
956
+
957
+ .header {
958
+ background-color: white;
959
+ padding: 10px 0;
960
+ border-bottom: 1px solid var(--border-color);
961
+ }
962
+
963
+ h1.title {
964
+ color: var(--primary-color);
965
+ font-weight: bold;
966
+ font-size: 2.5em;
967
+ margin: 0;
968
+ padding: 10px 0;
969
+ }
970
+
971
+ .section-header {
972
+ background-color: var(--background-color);
973
+ padding: 8px;
974
+ margin-top: 10px;
975
+ border-radius: 5px;
976
+ font-size: 1.2em;
977
+ color: #333;
978
+ font-weight: bold;
979
+ }
980
+
981
+ .section-number {
982
+ display: inline-block;
983
+ width: 24px;
984
+ height: 24px;
985
+ background-color: var(--secondary-color);
986
+ color: white;
987
+ border-radius: 50%;
988
+ text-align: center;
989
+ margin-right: 10px;
990
+ }
991
+
992
+ .btn-primary {
993
+ background-color: var(--primary-color);
994
+ border-color: var(--primary-color);
995
+ }
996
+
997
+ .btn-primary:hover {
998
+ background-color: #8f1c2a;
999
+ border-color: #8f1c2a;
1000
+ }
1001
+
1002
+ .filter-row {
1003
+ display: flex;
1004
+ flex-direction: row;
1005
+ flex-wrap: nowrap;
1006
+ gap: 10px;
1007
+ margin-bottom: 8px;
1008
+ padding: 6px;
1009
+ background-color: white;
1010
+ border-radius: 5px;
1011
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1012
+ }
1013
+
1014
+ /* Specific styling for columns in filter rows */
1015
+ .filter-row .gr-column {
1016
+ flex: 1;
1017
+ min-width: 250px;
1018
+ }
1019
+
1020
+ .budget-btn {
1021
+ background-color: white;
1022
+ border: 1px solid var(--border-color);
1023
+ color: #333;
1024
+ padding: 8px 15px;
1025
+ border-radius: 20px;
1026
+ margin: 5px;
1027
+ cursor: pointer;
1028
+ transition: all 0.2s;
1029
+ }
1030
+
1031
+ .budget-btn:hover, .budget-btn.active {
1032
+ background-color: var(--primary-color);
1033
+ color: white;
1034
+ border-color: var(--primary-color);
1035
+ }
1036
+
1037
+ .results-container {
1038
+ background-color: white;
1039
+ border-radius: 5px;
1040
+ padding: 15px;
1041
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1042
+ }
1043
+
1044
+ .footer {
1045
+ text-align: center;
1046
+ padding: 20px 0;
1047
+ margin-top: 30px;
1048
+ font-size: 0.9em;
1049
+ color: #87CEEB;
1050
+ }
1051
+
1052
+ /* Style for checkboxes */
1053
+ .checkbox-container {
1054
+ display: flex;
1055
+ gap: 15px;
1056
+ margin: 10px 0;
1057
+ }
1058
+
1059
+ /* Style for the search box */
1060
+ .search-box {
1061
+ display: flex;
1062
+ margin: 15px 0;
1063
+ }
1064
+
1065
+ .search-box input {
1066
+ flex-grow: 1;
1067
+ padding: 8px 15px;
1068
+ border: 1px solid var(--border-color);
1069
+ border-radius: 4px 0 0 4px;
1070
+ }
1071
+
1072
+ .search-box button {
1073
+ background-color: var(--secondary-color);
1074
+ color: white;
1075
+ border: none;
1076
+ padding: 8px 15px;
1077
+ border-radius: 0 4px 4px 0;
1078
+ cursor: pointer;
1079
+ }
1080
+
1081
+ .gr-dropdown {
1082
+ min-height: 35px !important;
1083
+ }
1084
+
1085
+ .gr-dropdown select {
1086
+ height: 35px !important;
1087
+ padding: 5px !important;
1088
+ }
1089
+
1090
+ .gr-box {
1091
+ min-height: 35px !important;
1092
+ }
1093
+
1094
+ .gr-radio-group {
1095
+ gap: 5px !important;
1096
+ margin: 2px 0 !important;
1097
+ }
1098
+
1099
+ select {
1100
+ height: 35px !important;
1101
+ padding: 5px !important;
1102
+ }
1103
+
1104
+ /* Responsive design for smaller screens */
1105
+ @media (max-width: 768px) {
1106
+ .filter-row {
1107
+ flex-wrap: wrap;
1108
+ }
1109
+ .filter-row .gr-column {
1110
+ min-width: 100%;
1111
+ }
1112
+ }
1113
+ """
1114
+
1115
+ # Define the Gradio interface
1116
+ with gr.Blocks(css=css, title="Gift Finder") as demo:
1117
+ #Header
1118
+ # Define styles separately for better organization
1119
+ header_styles = {
1120
+ "container": """
1121
+ display: flex;
1122
+ align-items: center;
1123
+ justify-content: space-between;
1124
+ padding: 0 20px;
1125
+ width: 100%;
1126
+ flex-wrap: wrap;
1127
+ gap: 20px;
1128
+ """,
1129
+ "logo_section": """
1130
+ display: flex;
1131
+ align-items: center;
1132
+ gap: 15px;
1133
+ """,
1134
+ "logo": """
1135
+ height: 50px;
1136
+ width: auto;
1137
+ object-fit: contain;
1138
+ """,
1139
+ "title": """
1140
+ color: #87CEEB;
1141
+ font-weight: bold;
1142
+ margin: 0;
1143
+ font-size: clamp(1.5rem, 2vw, 2rem);
1144
+ """,
1145
+ "nav": """
1146
+ display: flex;
1147
+ gap: 20px;
1148
+ align-items: center;
1149
+ """,
1150
+ "nav_item": """
1151
+ display: flex;
1152
+ align-items: center;
1153
+ cursor: pointer;
1154
+ """
1155
+ }
1156
+
1157
+ with gr.Row(elem_classes=["header"]):
1158
+ gr.HTML(f"""
1159
+ <div style="{header_styles['container']}">
1160
+ <div style="{header_styles['logo_section']}">
1161
+ <img src="data:image/png;base64,{logo_base64}"
1162
+ alt="PrintNGift Logo"
1163
+ style="{header_styles['logo']}">
1164
+ <div>
1165
+ <h1 style="{header_styles['title']}">
1166
+ Your Gift Finder
1167
+ </h1>
1168
+ </div>
1169
+ </div>
1170
+ <nav style="{header_styles['nav']}">
1171
+ <div style="{header_styles['nav_item']}">
1172
+ <span style="font-weight: bold; margin-right: 10px;">Shop</span>
1173
+ <span style="font-size: 0.8rem;">▼</span>
1174
+ </div>
1175
+ <div style="{header_styles['nav_item']}">
1176
+ <span style="font-weight: bold;">My Enquiry (0)</span>
1177
+ </div>
1178
+ </nav>
1179
+ </div>
1180
+ """)
1181
+
1182
+ # Main title
1183
+ # Search bar
1184
+ gr.HTML("""
1185
+ <div class="section-header">
1186
+ <span class="section-number">0</span> Describe what you're looking for*
1187
+ </div>
1188
+ """)
1189
+
1190
+ with gr.Row(elem_classes=["search-box"]):
1191
+ query = gr.Textbox(
1192
+ placeholder="Example: Find me office supplies or I need premium drinkware items",
1193
+ label="Requirements"
1194
+ )
1195
+
1196
+ # Budget section
1197
+ gr.HTML("""
1198
+ <div class="section-header">
1199
+ <span class="section-number">1</span>Budget (S$): per package + total budget + items in bundle*
1200
+ </div>
1201
+ """)
1202
+
1203
+ # Budget buttons and input
1204
+ with gr.Row(elem_classes=["filter-row"]):
1205
+ with gr.Column(scale=20):
1206
+ package_item_count = gr.Slider(
1207
+ minimum=1,
1208
+ maximum=7,
1209
+ value=3,
1210
+ step=1,
1211
+ label="Number",
1212
+ info="Items/gift package"
1213
+ )
1214
+ with gr.Column(scale=60):
1215
+ budget_range = gr.Radio(
1216
+ choices=BUDGET_RANGES,
1217
+ label="Budget Per Bundle Package",
1218
+ value=BUDGET_RANGES[0]
1219
+ )
1220
+ with gr.Column(scale=20):
1221
+ budget_total = gr.Number(
1222
+ label="Total Budget (S$)",
1223
+ minimum=10,
1224
+ value="100",
1225
+ info="Net of package cost"
1226
+ )
1227
+
1228
+ # Combined row for Age Group, Gender, and Colour Theme
1229
+ with gr.Row(elem_classes=["filter-row"]):
1230
+ with gr.Column(scale=2):
1231
+ gr.HTML("""
1232
+ <div class="section-header" style="margin-top: 0;">
1233
+ <span class="section-number">2</span> Target Age Group*
1234
+ </div>
1235
+ """)
1236
+ age_group = gr.Dropdown(
1237
+ choices=["Choose an option", "<18", "18-30", "30-40", "40-50", "50-60", ">60"],
1238
+ label="Age",
1239
+ value="Choose an option"
1240
+ )
1241
+
1242
+ with gr.Column(scale=2):
1243
+ gr.HTML("""
1244
+ <div class="section-header" style="margin-top: 0;">
1245
+ <span class="section-number">3</span> Target Gender
1246
+ </div>
1247
+ """)
1248
+ gender = gr.Dropdown(
1249
+ choices=["Choose an option", "Male", "Female", "does not really matter"],
1250
+ label="Gender",
1251
+ value="Choose an option"
1252
+ )
1253
+
1254
+ with gr.Column(scale=2):
1255
+ gr.HTML("""
1256
+ <div class="section-header" style="margin-top: 0;">
1257
+ <span class="section-number">4</span> Colour Theme
1258
+ </div>
1259
+ """)
1260
+ color_theme = gr.Dropdown(
1261
+ choices=[
1262
+ "Choose an option",
1263
+ "Black", "White", "Off-White", "Brown", "Red", "Blue", "Gray",
1264
+ "Gold", "Yellow", "Purple", "Pink", "Green", "Silver",
1265
+ "Orange", "Multi-color", "Transparent"
1266
+ ],
1267
+ label="Color",
1268
+ value="Choose an option"
1269
+ )
1270
+ # Gift Occasion section
1271
+ gr.HTML("""
1272
+ <div class="section-header">
1273
+ <span class="section-number">5</span> Gift Occasion
1274
+ </div>
1275
+ """)
1276
+
1277
+ with gr.Row(elem_classes=["filter-row"]):
1278
+ gift_occasion = gr.Dropdown(
1279
+ choices=[
1280
+ "Choose an option",
1281
+ "Festive Celebration",
1282
+ "Long Service Award",
1283
+ "Corporate Milestones",
1284
+ "Onboarding",
1285
+ "Christmas/Year-End Celebration",
1286
+ "Annual Dinner & Dance",
1287
+ "All The Best!",
1288
+ "Others"
1289
+ ],
1290
+ label="Occasion",
1291
+ value="Choose an option",
1292
+ elem_classes=["gr-dropdown"]
1293
+ )
1294
+
1295
+ # Job Function section
1296
+ gr.HTML("""
1297
+ <div class="section-header">
1298
+ <span class="section-number">6</span> Recipient's Job Function
1299
+ </div>
1300
+ """)
1301
+
1302
+ with gr.Row(elem_classes=["filter-row"]):
1303
+ job_function = gr.Dropdown(
1304
+ choices=[
1305
+ "Choose an option",
1306
+ "C-Suite",
1307
+ "Sales & Business Development",
1308
+ "Finance",
1309
+ "Operations",
1310
+ "Human Resource",
1311
+ "Engineering",
1312
+ "Information Technology",
1313
+ "Marketing & Communications",
1314
+ "Others"
1315
+ ],
1316
+ label="Recipient",
1317
+ value="Choose an option",
1318
+ elem_classes=["gr-dropdown"]
1319
+ )
1320
+
1321
+ # Results tabs
1322
+ with gr.Tabs():
1323
+ with gr.TabItem("Recommendations"):
1324
+ response = gr.Textbox(label="Recommendation Details", lines=15)
1325
+ with gr.TabItem("Bundle Summary"):
1326
+ bundle_summary = gr.Textbox(label="Bundle Statistics", lines=3)
1327
+ bundle_table = gr.DataFrame(label="Selected Items")
1328
+ with gr.TabItem("Bundle Pictures"):
1329
+ bundle_images = gr.HTML(label="Product Images")
1330
+
1331
+ # Function to determine the final budget range
1332
+ def get_final_budget_range(range1, range2):
1333
+ return range1 if range1 else range2
1334
+
1335
+ def modified_interface(budget_range1, budget_total, package_item_count, age_group, gift_occasion, color_theme, job_function, gender, query):
1336
+ """
1337
+ Updated interface function to handle per-package budget range, total budget,
1338
+ specific item count per package, and highlight bulk discounts
1339
+ """
1340
+ # Get the budget range for individual items in the package
1341
+ budget_range = budget_range1 if budget_range1 else "Below S$10" # Default if nothing selected
1342
+
1343
+ # Parse the budget range for individual items
1344
+ min_budget, max_budget = parse_budget_range(budget_range)
1345
+
1346
+ # Get the total budget - ensure it's properly converted to float
1347
+ try:
1348
+ total_budget = float(budget_total) if budget_total else 100.0
1349
+ except (ValueError, TypeError):
1350
+ total_budget = 100.0
1351
+
1352
+ # Get the number of items per package - ensure it's properly converted to int
1353
+ try:
1354
+ item_count = int(package_item_count) if package_item_count else None
1355
+ except (ValueError, TypeError):
1356
+ item_count = None
1357
+ # Get the number of items per package
1358
+ item_count = int(package_item_count) if package_item_count else None
1359
+
1360
+ # Initialize the chatbot
1361
+ chatbot = GiftBundleChatbot(all_items)
1362
+
1363
+ # Process the query with all filters
1364
+ response, selected_items = chatbot.process_query(
1365
+ query=query,
1366
+ min_budget=min_budget,
1367
+ max_budget=max_budget,
1368
+ total_budget=total_budget,
1369
+ item_count=item_count,
1370
+ age_group=age_group,
1371
+ gift_occasion=gift_occasion,
1372
+ color_theme=color_theme,
1373
+ job_function=job_function,
1374
+ gender=gender
1375
+ )
1376
+
1377
+ # Create DataFrame for bundle display
1378
+ if selected_items:
1379
+ # Calculate the per-package cost
1380
+ package_cost = sum(item['normalized_price'] for item in selected_items if item['normalized_price'] is not None)
1381
+
1382
+ # Calculate max number of packages that fit within total budget
1383
+ max_packages = int(total_budget / package_cost) if package_cost > 0 else 0
1384
+ total_cost = package_cost * max_packages if max_packages > 0 else 0
1385
+
1386
+ # Create dataframe with discount information highlighted
1387
+ bundle_data = []
1388
+ for item in selected_items:
1389
+ price_display = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
1390
+
1391
+ # Add discount note if available
1392
+ discount_note = ""
1393
+ if item.get('has_bulk_discount', False):
1394
+ discount_note = "💰 BULK DISCOUNT AVAILABLE"
1395
+
1396
+ # Prepare description with discount info if available
1397
+ description = item.get('short_description', 'No description')[:100]
1398
+ if item.get('formatted_discount', ''):
1399
+ description += f"\n{item.get('formatted_discount', '')}"
1400
+
1401
+ bundle_data.append({
1402
+ "Name": item['name'],
1403
+ "Price (S$)": price_display,
1404
+ "Type": item['type'],
1405
+ "Bulk Discount": discount_note,
1406
+ "Description": description
1407
+ })
1408
+
1409
+ bundle_df = pd.DataFrame(bundle_data)
1410
+
1411
+ budget_utilization = (total_cost / total_budget) * 100 if total_budget > 0 else 0
1412
+
1413
+ bundle_summary = f"Package Cost: S${package_cost:.2f}\n" \
1414
+ f"Items per Package: {len(selected_items)}\n" \
1415
+ f"Number of Packages Possible: {max_packages}\n" \
1416
+ f"Total Cost: S${total_cost:.2f}\n" \
1417
+ f"Total Budget: S${total_budget:.2f}\n" \
1418
+ f"Budget Utilization: {budget_utilization:.1f}%"
1419
+
1420
+ # Create HTML for displaying images with discount badges
1421
+ html_content = "<div style='display: flex; flex-wrap: wrap; gap: 20px;'>"
1422
+ count = 0
1423
+
1424
+ # Debug: Print image information for each item
1425
+ print("Image debug information:")
1426
+ for i, item in enumerate(selected_items):
1427
+ print(f"Item {i+1}: {item['name']}")
1428
+ print(f" Has 'images' key: {'Yes' if 'images' in item else 'No'}")
1429
+ if 'images' in item:
1430
+ print(f" Images value type: {type(item['images'])}")
1431
+ print(f" Images value: {str(item['images'])[:100]}") # Show first 100 chars
1432
+
1433
+ for item in selected_items:
1434
+ # Check if the item has images key
1435
+ if 'images' in item and item['images']:
1436
+ try:
1437
+ # Get the image URL - handle different possible formats
1438
+ image_url = None
1439
+
1440
+ if isinstance(item['images'], str):
1441
+ # Direct URL string
1442
+ image_url = item['images']
1443
+ elif isinstance(item['images'], list) and len(item['images']) > 0:
1444
+ # List of URLs - take the first one
1445
+ image_url = item['images'][0]
1446
+ elif isinstance(item['images'], dict) and len(item['images']) > 0:
1447
+ # Dictionary of URLs - take the first value
1448
+ image_url = list(item['images'].values())[0]
1449
+
1450
+ # If it's a relative URL, convert to absolute (example)
1451
+ if image_url and image_url.startswith('/'):
1452
+ # This is just an example - update with your actual domain
1453
+ image_url = f"https://yourdomain.com{image_url}"
1454
+
1455
+ # Print the processed URL for debugging
1456
+ print(f"Processed image URL for {item['name']}: {image_url}")
1457
+
1458
+ if image_url:
1459
+ # Create HTML for the image with caption
1460
+ item_price = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
1461
+
1462
+ # Add a discount badge if available
1463
+ discount_badge = ""
1464
+ if item.get('has_bulk_discount', False):
1465
+ discount_badge = """
1466
+ <div style='position: absolute; top: 5px; right: 5px; background-color: #FF9800; color: white; padding: 5px; border-radius: 4px; font-size: 0.8em;'>
1467
+ BULK DISCOUNT
1468
+ </div>
1469
+ """
1470
+
1471
+ html_content += f"""
1472
+ <div style='border: 1px solid #ddd; border-radius: 8px; padding: 10px; max-width: 250px; position: relative;'>
1473
+ {discount_badge}
1474
+ <img src="{image_url}" alt="{item['name']}" style='width: 100%; max-height: 200px; object-fit: contain;'>
1475
+ <p style='margin-top: 8px; font-weight: bold;'>{item['name']}</p>
1476
+ <p>{item_price}</p>
1477
+ """
1478
+
1479
+ # Add discount info if available
1480
+ if item.get('formatted_discount', ''):
1481
+ html_content += f"""<p style='color: #FF9800; font-weight: bold;'>{item.get('formatted_discount', '')}</p>
1482
+ """
1483
+
1484
+ html_content += "</div>"
1485
+ count += 1
1486
+ except Exception as e:
1487
+ print(f"Error processing image for {item['name']}: {str(e)}")
1488
+
1489
+ # Close the container div
1490
+ html_content += "</div>"
1491
+
1492
+ # If no images were found
1493
+ if count == 0:
1494
+ html_content = """
1495
+ <div>
1496
+ <p>No images were found for the selected items.</p>
1497
+ <p>Check the console logs for debugging information about the image URLs.</p>
1498
+ </div>
1499
+ """
1500
+ else:
1501
+ bundle_df = pd.DataFrame(columns=["Name", "Price (S$)", "Type", "Bulk Discount", "Description"])
1502
+ bundle_summary = "No items selected"
1503
+ html_content = "<p>No items selected.</p>"
1504
+
1505
+ return response, bundle_df, bundle_summary, html_content
1506
+
1507
+ search_btn = gr.Button("Get Recommendations (Before you press, check your inputs, no automatic input of budget for bundle package)", variant="primary")
1508
+ # Search button click handler
1509
+
1510
+ search_btn.click(
1511
+ fn=modified_interface,
1512
+ inputs=[budget_range, budget_total, package_item_count, age_group, gift_occasion, color_theme, job_function, gender, query],
1513
+ outputs=[response, bundle_table, bundle_summary, bundle_images]
1514
+ )
1515
+ # You can also update the examples to include total_budget values
1516
+ gr.Examples(
1517
+ examples=[
1518
+ ["S$10 to S$20", 100, "30-40", "Corporate Milestones", "Black", "C-Suite", "Male", "I need some premium wareable items"],
1519
+ ["S$35 to S$55", 200, "30-40", "Festive Celebration", "Blue", "Marketing & Communications", "Female", "Find me clothing items suitable for corporate events"],
1520
+ ["S$20 to S$35", 150, "18-30", "Long Service Award", "Silver", "Information Technology", "does not really matter", "Recommend tech gadgets"],
1521
+ ["S$55 to S$80", 300, "40-50", "All The Best!", "Multi-color", "Operations", "does not really matter", "I need some travel essentials"]
1522
+ ],
1523
+ inputs=[budget_range, budget_total, age_group, gift_occasion, color_theme, job_function, gender, query]
1524
+ )
1525
+
1526
+ # Footer
1527
+ gr.HTML("""
1528
+ <div class="footer">
1529
+ <p>© 2025 Gift Market. All rights reserved.</p>
1530
+ </div>
1531
+ """)
1532
+
1533
+
1534
+ # Launch the app
1535
+ if __name__ == "__main__":
1536
+ # Uncommment to load real data
1537
+ # all_items = load_sample_data()
1538
+
1539
+ # Launch Gradio interface
1540
+ demo.launch()