Spaces:
Running
Running
File size: 13,529 Bytes
6665e72 96ce57f c1a304e 96ce57f c2d7928 96ce57f e422038 e3402df |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# Enable nested event loops for Gradio + asyncio compatibility
import nest_asyncio
nest_asyncio.apply()
import gradio as gr
from typing import List, Optional, Tuple, Dict, Any
import uuid
import os
import sys
import traceback
import warnings
import logging
# Suppress asyncio event loop cleanup warnings (harmless on HF Spaces with SSR)
warnings.filterwarnings("ignore", message=".*Invalid file descriptor.*")
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
from agent import run_agent
# ============================================================================
# CONSTANTS
# ============================================================================
# Example images hosted online (replace with your own URLs)
EXAMPLE_IMAGE_URLS = [
"https://64.media.tumblr.com/456e9e6d8f42e677581f7d7994554600/03546756eb18cebb-2e/s400x600/7cd50d0a76327cf08cc75d17e540a11212b56a3b.jpg",
"https://64.media.tumblr.com/97e808fda7863d31729da77de9f03285/03546756eb18cebb-2b/s400x600/7fc1a84a8d3f5922ca1f24fd6cc453d45ba88f7f.jpg",
"https://64.media.tumblr.com/380d1592fa32f1e2290531879cfdd329/03546756eb18cebb-61/s400x600/e9d78c4467fa3a8dc6223667e236b922bb943775.jpg",
]
# ============================================================================
# UI HELPER FUNCTIONS
# ============================================================================
def get_session_id():
"""Generate a unique session ID for the user"""
return str(uuid.uuid4())
def format_books_html(books: List[dict]) -> str:
"""Format final books as HTML for display in a 3-column layout with larger covers"""
html = "<div style='width: 100%;'>"
html += "<h2 style='color: #667eea; margin-bottom: 20px;'>๐ Your Personalized Recommendations</h2>"
html += "<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; align-items: start;'>"
for i, book in enumerate(books, 1):
desc = book.get("description", "")
html += f"""
<div style='padding: 16px; background: white;
border-radius: 12px; border-left: 4px solid #667eea; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
<div style='display: flex; gap: 16px; align-items: flex-start;'>
<img src='{book.get("cover_url", "")}'
style='width: 120px; min-width: 120px; height: 180px; object-fit: cover; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);'
onerror='this.style.display="none"' />
<div style='flex: 1; min-width: 0;'>
<h3 style='margin: 0 0 8px 0; color: #667eea; font-size: 1.1em; line-height: 1.3;'>{i}. {book["title"]}</h3>
<p style='margin: 0; color: #666; font-style: italic; font-size: 0.9em;'>by {book["author"]}</p>
</div>
</div>
<p style='margin: 16px 0 0 0; color: #555; line-height: 1.6; font-size: 0.9em;'>{desc}</p>
</div>
"""
html += "</div></div>"
return html
def load_example_images():
"""Load example images from URLs"""
return EXAMPLE_IMAGE_URLS
# REMOVED: messages_to_chatbot_format - using agent's List[Dict] directly
# ============================================================================
# EVENT HANDLERS
# ============================================================================
def process_upload(images: List, session_id: str, progress=gr.Progress()):
"""Handle image upload and start the agent workflow"""
if not images:
# Return empty list for the Chatbot component
yield [], "Please upload images.", "", None, gr.update(visible=True), ""
return
# Process image paths
image_paths = []
for img in images:
if hasattr(img, 'name'): image_paths.append(img.name)
# Added safety checks for common Gradio formats
elif isinstance(img, dict) and 'path' in img: image_paths.append(img['path'])
elif isinstance(img, str) and img.startswith('http'): image_paths.append(img) # URLs
elif isinstance(img, str) and os.path.isfile(img): image_paths.append(img)
elif isinstance(img, tuple): image_paths.append(img[0])
if not image_paths:
yield [], "Error processing images.", "", None, gr.update(visible=True), ""
return
try:
# Show loading status
yield [], "", "", None, gr.update(visible=False), "๐จ Analyzing your vibe images..."
# Run agent with session_id acting as the thread_id
result = run_agent(images=image_paths, thread_id=session_id)
# CRUCIAL FIX: Use the agent's List[Dict] messages directly
chat_history = result["messages"]
reasoning = "\n".join(result.get("reasoning", []))
# Outputs: [chatbot, reasoning, recommendations, soundtrack, start_btn, status]
yield chat_history, reasoning, "", None, gr.update(visible=False), "โจ Vibe analysis complete!"
except Exception as e:
yield [], f"Error: {e}\n{traceback.format_exc()}", "", None, gr.update(visible=True), "โ Error occurred"
def add_user_message(user_message: str, history: List[Dict[str, str]]):
"""
Step 1 of Chat: Add user message to history in the new Chatbot format.
"""
if not user_message.strip():
return history, ""
# Append the new message in the List[Dict] format
new_message = {"role": "user", "content": user_message}
return history + [new_message], ""
def generate_bot_response(history: List[Dict[str, str]], session_id: str):
"""
Step 2 of Chat: Call agent and update history with response.
Uses yield to show loading status.
"""
print(f"[DEBUG] generate_bot_response called with session_id={session_id}")
print(f"[DEBUG] history has {len(history) if history else 0} messages")
# Get the last user message from the List[Dict] history
if not history or history[-1]["role"] != "user":
# Should not happen in normal flow, but safety check
print("[DEBUG] No user message found in history")
yield history, "No message to process", "", None, ""
return
# The user message is already in history, we only need the content to resume the agent
user_content = history[-1]["content"]
# Gradio 6 may return content as a list of dicts with 'text' key
if isinstance(user_content, list):
user_message = " ".join(item.get("text", str(item)) for item in user_content if isinstance(item, dict))
else:
user_message = str(user_content)
print(f"[DEBUG] Resuming agent with user_message: {user_message[:50]}...")
try:
# Show loading status
yield history, "", "", None, "๐ Processing your response..."
# Resume agent execution using the session_id
result = run_agent(images=[], user_message=user_message, thread_id=session_id)
print(f"[DEBUG] run_agent returned: {type(result)}")
if result:
print(f"[DEBUG] result keys: {result.keys() if isinstance(result, dict) else 'N/A'}")
if result is None:
print("[DEBUG] result is None - agent may not have resumed properly")
history.append({"role": "assistant", "content": "Error: Agent did not return a response."})
yield history, "Agent returned None", "", None, "โ Agent error"
return
# CRUCIAL FIX: The agent returns the full updated history in the List[Dict] format
updated_history = result["messages"]
reasoning = "\n".join(result.get("reasoning", []))
print(f"[DEBUG] updated_history has {len(updated_history)} messages")
# Check for final results
books_html = ""
if result.get("final_books"):
books_html = format_books_html(result["final_books"])
soundtrack = result.get("soundtrack_url", "") or None
# Determine status based on what happened
if result.get("final_books"):
status = "โ
Recommendations ready!"
elif "retrieved_books" in result and result["retrieved_books"]:
status = "๐ Books retrieved, refining..."
else:
status = "๐ญ Awaiting your input..."
# Outputs: [chatbot, reasoning, recommendations, soundtrack, status]
yield updated_history, reasoning, books_html, soundtrack, status
except Exception as e:
# Append error to chat by updating the last user message's response
error_msg = f"Agent Error: {str(e)}"
print(f"[DEBUG] Exception in generate_bot_response: {e}")
traceback.print_exc()
# Append assistant error message
history.append({"role": "assistant", "content": error_msg})
yield history, f"Error trace: {traceback.format_exc()}", "", None, "โ Error occurred"
def reset_app():
"""Reset the session"""
new_id = get_session_id()
# Returns: [session_id, chatbot, reasoning, books, soundtrack, input, images, start_btn, status]
return new_id, [], "", "", None, "", None, gr.update(visible=True), "Ready to analyze your vibe!"
# ============================================================================
# LAYOUT
# ============================================================================
with gr.Blocks() as demo:
# State management for multi-user support
session_id = gr.State(get_session_id())
gr.Markdown("# ๐ The Vibe Reader", elem_id='main-title')
gr.Markdown("""
**How it works:**
- ๐จ **Vision AI** extracts mood, themes, and aesthetic keywords from your images
- ๐ **Semantic search** queries a vector DB of 50k+ book recs from r/BooksThatFeelLikeThis
- ๐ฌ **Conversational refinement** asks targeted questions to narrow down preferences
- ๐ **Google Books MCP** enriches results with covers, descriptions, and metadata
- ๐ต **ElevenLabs AI** generates a custom soundtrack that matches your reading vibe
""", elem_id='subtitle')
with gr.Row():
# Left: Inputs
with gr.Column(scale=1):
gr.Markdown("### 1. Upload Your Vibe")
image_input = gr.Gallery(label="Visual Inspiration", columns=3, height="300px")
load_examples_btn = gr.Button("๐ท Load Example Images (Credits: @thegorgonist)", variant="secondary", size="md")
start_btn = gr.Button("๐ฎ Analyze Vibe", variant="primary", size="lg")
status_display = gr.Textbox(label="Status", value="Ready to analyze your vibe!", interactive=False, elem_id="status-display")
reset_btn = gr.Button("๐ Start Over", variant="secondary")
# Right: Chat
with gr.Column(scale=1):
gr.Markdown("### 2. Refine & Discover")
# Chatbot now uses the new List[Dict] format
chatbot = gr.Chatbot(height=500, label="Agent Conversation")
with gr.Row():
msg_input = gr.Textbox(
show_label=False,
placeholder="Type your response here...",
scale=4,
container=False
)
submit_btn = gr.Button("Send", variant="primary", scale=1)
# Outputs - Recommendations first, then reasoning
recommendations_output = gr.HTML(label="Recommendations")
soundtrack_player = gr.Audio(label="Vibe Soundtrack", type="filepath", interactive=False)
with gr.Accordion("๐ Internal Reasoning", open=True):
reasoning_display = gr.Textbox(label="Agent Thoughts", lines=10, interactive=False)
# ============================================================================
# INTERACTION LOGIC
# ============================================================================
# 0. Load Example Images
load_examples_btn.click(
fn=load_example_images,
inputs=[],
outputs=[image_input]
)
# 1. Start Analysis
start_btn.click(
fn=process_upload,
inputs=[image_input, session_id],
outputs=[chatbot, reasoning_display, recommendations_output, soundtrack_player, start_btn, status_display]
)
# 2. Chat Interaction (User enters text -> History updates -> Bot responds)
# User adds message to history optimistically and clears input
user_event = msg_input.submit(
fn=add_user_message,
inputs=[msg_input, chatbot],
outputs=[chatbot, msg_input],
queue=False
)
# Bot generates response and updates the full history
user_event.then(
fn=generate_bot_response,
inputs=[chatbot, session_id],
outputs=[chatbot, reasoning_display, recommendations_output, soundtrack_player, status_display]
)
submit_btn.click(
fn=add_user_message,
inputs=[msg_input, chatbot],
outputs=[chatbot, msg_input],
queue=False
).then(
fn=generate_bot_response,
inputs=[chatbot, session_id],
outputs=[chatbot, reasoning_display, recommendations_output, soundtrack_player, status_display]
)
# 3. Reset
reset_btn.click(
fn=reset_app,
inputs=[],
outputs=[session_id, chatbot, reasoning_display, recommendations_output, soundtrack_player, msg_input, image_input, start_btn, status_display]
)
if __name__ == "__main__":
# Note: css_paths removed as custom.css location may vary
demo.queue().launch(theme=gr.themes.Monochrome(), css_paths='assets/custom.css',ssr_mode=False) |