Spaces:
Running
Running
Upload app.py
Browse files
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()
|