AryamaR commited on
Commit
ebcff02
·
1 Parent(s): cb95c49

Added application code base & container manifest

Browse files
Files changed (37) hide show
  1. Dockerfile +39 -0
  2. PROJECT_SUMMARY.md +360 -0
  3. README.md +249 -10
  4. app.py +7 -0
  5. requirements.txt +15 -0
  6. src/.DS_Store +0 -0
  7. src/__init__.py +2 -0
  8. src/__pycache__/__init__.cpython-314.pyc +0 -0
  9. src/__pycache__/config.cpython-314.pyc +0 -0
  10. src/__pycache__/party_context.cpython-314.pyc +0 -0
  11. src/agents/__init__.py +2 -0
  12. src/agents/__pycache__/__init__.cpython-314.pyc +0 -0
  13. src/agents/__pycache__/guest_invite_email_agent.cpython-314.pyc +0 -0
  14. src/agents/__pycache__/orchestrator.cpython-314.pyc +0 -0
  15. src/agents/__pycache__/theme_decor_favor_shop_agent.cpython-314.pyc +0 -0
  16. src/agents/__pycache__/theme_planning_agent.cpython-314.pyc +0 -0
  17. src/agents/__pycache__/venue_search_agent.cpython-314.pyc +0 -0
  18. src/agents/guest_invite_email_agent.py +324 -0
  19. src/agents/orchestrator.py +791 -0
  20. src/agents/theme_decor_favor_shop_agent.py +379 -0
  21. src/agents/theme_planning_agent.py +286 -0
  22. src/agents/venue_search_agent.py +219 -0
  23. src/config.py +28 -0
  24. src/mcp/__init__.py +2 -0
  25. src/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
  26. src/mcp/__pycache__/mcp_clients.cpython-314.pyc +0 -0
  27. src/mcp/mcp_clients.py +343 -0
  28. src/party_context.py +68 -0
  29. src/tools/__init__.py +2 -0
  30. src/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  31. src/tools/__pycache__/csv_guest_parser.cpython-314.pyc +0 -0
  32. src/tools/__pycache__/yelp_search_tool.cpython-314.pyc +0 -0
  33. src/tools/csv_guest_parser.py +60 -0
  34. src/ui/__init__.py +2 -0
  35. src/ui/__pycache__/__init__.cpython-314.pyc +0 -0
  36. src/ui/__pycache__/gradio_ui.cpython-314.pyc +0 -0
  37. src/ui/gradio_ui.py +682 -0
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Simple working Dockerfile for Plan-A-Party
2
+ FROM python:3.11-slim
3
+
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && \
8
+ apt-get install -y --no-install-recommends \
9
+ curl \
10
+ ca-certificates \
11
+ && apt-get clean \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Copy Python requirements and install dependencies
15
+ COPY requirements.txt .
16
+ RUN pip install --no-cache-dir --upgrade pip && \
17
+ pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY . .
21
+
22
+ # Create invitations directory
23
+ RUN mkdir -p /app/invitations
24
+
25
+ # Set environment variables for Gradio
26
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
27
+ ENV GRADIO_SERVER_PORT=7860
28
+ ENV PYTHONUNBUFFERED=1
29
+
30
+ # Expose Gradio port
31
+ EXPOSE 7860
32
+
33
+ # Health check
34
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
35
+ CMD curl -f http://localhost:7860/ || exit 1
36
+
37
+ # Run the application
38
+ CMD ["python", "app.py"]
39
+
PROJECT_SUMMARY.md ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Plan-a-Party Multi-Agent System - Project Summary
2
+
3
+ ## 🎯 Project Overview
4
+
5
+ A complete multi-agent party planning system built with LangChain and Gradio, featuring 4 specialized subagents coordinated by an orchestrator agent. The system uses an interactive conversational interface where users plan parties through natural language dialogue, with all agents sharing state through a centralized `PartyContext` model.
6
+
7
+ ## 📁 Complete File Structure
8
+
9
+ ```
10
+ plan-a-party/
11
+ ├── app.py # Main Gradio entrypoint
12
+ ├── requirements.txt # Python dependencies
13
+ ├── README.md # Main documentation
14
+ ├── PROJECT_SUMMARY.md # This file (detailed architecture)
15
+ ├── .env.example # Environment variables template
16
+ ├── .gitignore # Git ignore rules
17
+ ├── invitations/ # Generated invitation card images
18
+ │ └── invitation_*.png # Theme-based invitation cards
19
+ └── src/
20
+ ├── __init__.py
21
+ ├── config.py # Configuration management (model IDs, etc.)
22
+ ├── party_context.py # Shared state (Pydantic models)
23
+ ├── agents/
24
+ │ ├── __init__.py
25
+ │ ├── orchestrator.py # Main orchestrator agent (conversational flow)
26
+ │ ├── theme_planning_agent.py # Subagent 1: Themes & invitations
27
+ │ ├── guest_invite_email_agent.py # Subagent 2: Email sending via Resend MCP
28
+ │ ├── venue_search_agent.py # Subagent 3: Bright Data SERP venue search
29
+ │ └── theme_decor_favor_shop_agent.py # Subagent 4: Shopping with Amazon MCP
30
+ ├── tools/
31
+ │ ├── __init__.py
32
+ │ └── csv_guest_parser.py # CSV parsing utility for guest lists
33
+ ├── mcp/
34
+ │ ├── __init__.py
35
+ │ └── mcp_clients.py # MCP clients (Amazon, Fewsats, Resend, BrightData)
36
+ └── ui/
37
+ ├── __init__.py
38
+ └── gradio_ui.py # Gradio interface (interactive chat UI)
39
+ ```
40
+
41
+ ## 🤖 Agent Architecture
42
+
43
+ ### Orchestrator Agent
44
+ - **File**: `src/agents/orchestrator.py`
45
+ - **Role**: Coordinates all subagents using conversational flow management
46
+ - **Key Features**:
47
+ - **Multi-turn Dialogue**: Progressively collects event details (budget, location, date)
48
+ - **Natural Language Understanding**: Extracts information from user messages
49
+ - **Automatic Theme Generation**: Triggers theme generation once party description is provided
50
+ - **Theme Selection Detection**: Recognizes theme selection from natural language
51
+ - **Tool Routing**: Routes requests to appropriate subagents based on context
52
+ - **State Management**: Updates `PartyContext` throughout the conversation
53
+ - **LLM-based Decision Making**: Uses Hugging Face LLM to decide next actions
54
+
55
+ **Key Functions**:
56
+ - `orchestrator_chat(user_message, chat_history)` - Main conversational handler
57
+ - `extract_event_details_from_message(message)` - Extracts budget, location, date
58
+ - `extract_theme_selection_from_message(message)` - Detects theme selection
59
+ - `decide_next_action(user_message, context)` - LLM-based routing
60
+ - Wrapper functions for each subagent tool
61
+
62
+ ### Subagent 1: Theme Planning Agent
63
+ - **File**: `src/agents/theme_planning_agent.py`
64
+ - **Capabilities**:
65
+ - Generates 3 creative party theme options using LLM
66
+ - Creates invitation text with event details (date, location, budget)
67
+ - Generates 4x4 invitation card images using FLUX.1-dev
68
+ - Embeds event metadata in invitation images
69
+ - **Tools**:
70
+ - `generate_party_themes(party_description)` - Generates 3 theme options
71
+ - `generate_invitation_for_theme(selected_theme_name)` - Creates invitation card
72
+ - **Models Used**:
73
+ - **meta-llama/Llama-3.3-70B-Instruct** - Text generation
74
+ - **FLUX.1-dev** - Image generation
75
+
76
+ ### Subagent 2: Guest Invite Email Agent
77
+ - **File**: `src/agents/guest_invite_email_agent.py`
78
+ - **Capabilities**:
79
+ - Parses guest lists from CSV files
80
+ - Composes personalized invitation emails using LLM
81
+ - Sends emails via Resend MCP (Zapier integration)
82
+ - Attaches invitation card images to emails
83
+ - **Tools**:
84
+ - `compose_invitation_email()` - Generates email subject and body
85
+ - `send_guest_invites_via_resend()` - Sends emails to all guests
86
+ - **MCP Integration**: Resend MCP via Zapier Gmail API
87
+
88
+ ### Subagent 3: Venue Search Agent
89
+ - **File**: `src/agents/venue_search_agent.py`
90
+ - **Capabilities**:
91
+ - Searches for party venues using Bright Data MCP SERP search
92
+ - Filters by location, budget, and guest count
93
+ - Extracts venue details (name, URL, rating, price, address, phone)
94
+ - Formats results for display in chat
95
+ - **Tools**:
96
+ - `search_party_venues(location, query, max_results, budget, guest_count)` - Searches venues
97
+ - **MCP Integration**: Bright Data MCP for SERP search
98
+ - **Query Building**: Constructs natural-language queries with filters
99
+
100
+ ### Subagent 4: Theme Decor/Favor Shop Agent
101
+ - **File**: `src/agents/theme_decor_favor_shop_agent.py`
102
+ - **Capabilities**:
103
+ - Generates theme-based shopping lists (decor + favors) using LLM
104
+ - Optimizes Amazon search queries from item names
105
+ - Integrates with Amazon MCP for product search
106
+ - Builds shopping carts with product links and pricing
107
+ - Supports Fewsats MCP for payment processing (future)
108
+ - **Tools**:
109
+ - `generate_shopping_list_for_theme(theme_name)` - Generates shopping list
110
+ - `shop_on_amazon(selected_items, ...)` - Builds Amazon cart
111
+ - **MCP Integration**:
112
+ - Amazon MCP (optional, requires Node.js build)
113
+ - Fewsats MCP (for payment processing)
114
+ - **Query Optimization**:
115
+ - `build_amazon_query_from_item_name()` - Cleans item names for better Amazon searches
116
+ - Strips descriptions, normalizes case, adds "party" keyword when appropriate
117
+
118
+ ## 🛠️ Supporting Components
119
+
120
+ ### Shared Context (`PartyContext`)
121
+ - **File**: `src/party_context.py`
122
+ - **Purpose**: Centralized state management using Pydantic models
123
+ - **Key Fields**:
124
+ - `event_budget`, `event_location`, `event_date` - Event metadata
125
+ - `theme_options`, `selected_theme_name` - Theme management
126
+ - `invitation_text`, `invitation_image_path` - Invitation data
127
+ - `guest_names`, `guest_emails`, `emails_sent` - Guest list management
128
+ - `venue_results`, `search_location` - Venue search results
129
+ - `cart_items`, `decor_items`, `favor_items` - Shopping data
130
+ - `selected_items`, `order_status`, `order_id` - Order management
131
+
132
+ ### MCP Clients
133
+ - **File**: `src/mcp/mcp_clients.py`
134
+ - **Architecture**:
135
+ - `MCPConnectionManager` - Manages connections to all MCP servers
136
+ - Individual client classes for each service
137
+ - **Clients**:
138
+ - **AmazonMCPClient** - Product search and cart building (optional, requires build)
139
+ - **FewsatsMCPClient** - Payment processing with X402/L402 protocols
140
+ - **ResendMCPClient** - Email sending via Zapier
141
+ - **BrightDataMCPClient** - SERP search for venues
142
+ - **Graceful Degradation**: Checks for server availability before use
143
+
144
+ ### Tools
145
+ - **CSV Parser** (`src/tools/csv_guest_parser.py`): Handles guest list uploads
146
+ - **Query Builder** (`src/agents/theme_decor_favor_shop_agent.py`): Optimizes Amazon searches
147
+
148
+ ## 🎨 User Interface
149
+
150
+ ### Gradio UI Features
151
+ - **File**: `src/ui/gradio_ui.py`
152
+ - **Design**: Single-page conversational interface (not tab-based)
153
+ - **Key Components**:
154
+ - **Interactive Chat** - Main conversation area with chatbot
155
+ - **Cart Preview** - Real-time shopping cart display
156
+ - **Generate Invitation Card Button** - Appears when all info is ready
157
+ - **Guest List Upload** - CSV file upload with status display
158
+ - **Shopping Checkboxes** - Item selection for decor and favors
159
+ - **Quick Hints** - Suggested workflow guide
160
+
161
+ ### UI Flow
162
+ 1. User types party description in chat
163
+ 2. System asks for budget, location, date
164
+ 3. System generates themes and displays in chat
165
+ 4. User selects theme via natural language
166
+ 5. "Generate Invitation Card" button becomes visible
167
+ 6. User clicks button to generate invitation
168
+ 7. User can upload guest list and send invitations
169
+ 8. User can search for venues via chat
170
+ 9. User can shop for decor/favors via chat
171
+
172
+ ### State Management
173
+ - Uses `gr.State` for chat history
174
+ - Updates `PartyContext` throughout conversation
175
+ - Dynamic UI updates based on context state
176
+ - Button visibility controlled by collected information
177
+
178
+ ## 🔧 Technology Stack
179
+
180
+ - **LangChain** - Agent framework and tool orchestration
181
+ - **Gradio** - Interactive web UI framework
182
+ - **Hugging Face**:
183
+ - **Mistral-7B-Instruct-v0.3** - Text generation for all agents
184
+ - **FLUX.1-dev** - Image generation for invitation cards
185
+ - **Inference API** - Model access
186
+ - **MCP (Model Context Protocol)**:
187
+ - **Bright Data MCP** - SERP search for venue discovery
188
+ - **Zapier MCP** - Email sending via Zapier
189
+ - **Amazon MCP** - Product search and cart building (optional)
190
+ - **Fewsats MCP** - Payment processing (X402/L402)
191
+ - **Pydantic** - Data validation and shared context models
192
+ - **Pandas** - CSV processing
193
+ - **langchain-mcp-adapters** - MCP integration library
194
+
195
+ ## 🚀 Deployment
196
+
197
+ ### Local Development
198
+ ```bash
199
+ python app.py
200
+ ```
201
+ App runs at `http://localhost:7860`
202
+
203
+ ### Hugging Face Spaces
204
+ 1. Push repository to Hugging Face Spaces
205
+ 2. Set environment variables in Space settings:
206
+ - `HUGGINGFACEHUB_API_TOKEN`
207
+ - `HF_TOKEN`
208
+ - `BRIGHTDATA_TOKEN` (optional)
209
+ - `ZAPIER_GMAIL_API_KEY` (optional)
210
+ - `FEWSATS_API_KEY` (optional)
211
+ 3. Space auto-deploys
212
+
213
+ ### Amazon MCP Setup (Optional)
214
+ ```bash
215
+ cd amazon-mcp
216
+ npm install
217
+ npm run build
218
+ cd ..
219
+ ```
220
+
221
+ ## 📋 Complete Workflow
222
+
223
+ ### 1. Initial Conversation
224
+ - User: "I want to plan a birthday party for my 6 year old kid"
225
+ - System: Stores party description, asks for budget, location, date
226
+
227
+ ### 2. Event Details Collection
228
+ - User: "Budget is under $500, in San Jose CA on June 10, 2025"
229
+ - System: Extracts and stores event metadata
230
+
231
+ ### 3. Theme Generation
232
+ - System: Automatically calls `generate_party_themes`
233
+ - System: Displays 3 theme options in chat
234
+
235
+ ### 4. Theme Selection
236
+ - User: "I like the first one" or "Let's go with the Minecraft theme"
237
+ - System: Detects selection, updates `selected_theme_name`
238
+
239
+ ### 5. Invitation Generation
240
+ - System: "Generate Invitation Card" button becomes visible
241
+ - User: Clicks button
242
+ - System: Calls `generate_invitation_for_theme`
243
+ - System: Creates invitation text and image, displays in UI
244
+
245
+ ### 6. Venue Search
246
+ - User: "Search for venues" or "Find venues near me"
247
+ - System: Calls `search_party_venues` with stored location
248
+ - System: Displays formatted venue results in chat
249
+
250
+ ### 7. Guest List Management
251
+ - User: Uploads CSV file
252
+ - System: Parses CSV, stores guest names and emails
253
+ - System: Displays status
254
+
255
+ ### 8. Email Sending
256
+ - User: "Send the invitations" or "Send emails"
257
+ - System: Calls `compose_invitation_email` and `send_guest_invites_via_resend`
258
+ - System: Sends emails with invitation card attachments
259
+
260
+ ### 9. Shopping
261
+ - User: "What decor items do you recommend?" or "Show me shopping options"
262
+ - System: Calls `generate_shopping_list_for_theme`
263
+ - System: Displays items in checkboxes
264
+ - User: Selects items, clicks "Save selected items"
265
+ - User: "Build my Amazon cart" or "Shop on Amazon"
266
+ - System: Calls `shop_on_amazon` with selected items
267
+ - System: Builds cart, displays products with links and prices
268
+
269
+ ## ✅ Implementation Status
270
+
271
+ - [x] Folder structure created
272
+ - [x] All 4 subagents implemented
273
+ - [x] Orchestrator agent with conversational flow
274
+ - [x] Gradio UI with interactive chat
275
+ - [x] Shared context management (PartyContext)
276
+ - [x] MCP integration (Bright Data, Resend, Amazon, Fewsats)
277
+ - [x] CSV parsing for guest lists
278
+ - [x] Theme generation with FLUX.1-dev
279
+ - [x] Email sending via Resend MCP
280
+ - [x] Venue search via Bright Data SERP
281
+ - [x] Amazon shopping with query optimization
282
+ - [x] Natural language theme selection
283
+ - [x] Event metadata collection
284
+ - [x] Dynamic UI updates
285
+ - [x] Graceful degradation for optional MCP servers
286
+ - [x] Documentation (README, PROJECT_SUMMARY)
287
+
288
+ ## 🔮 Future Enhancements
289
+
290
+ - [ ] Order tracking and status updates
291
+ - [ ] Multi-language support
292
+ - [ ] Theme customization options
293
+ - [ ] Calendar integration for date selection
294
+ - [ ] Budget tracking and alerts
295
+ - [ ] Guest RSVP management
296
+ - [ ] Venue booking integration
297
+ - [ ] Real-time order placement with Fewsats
298
+ - [ ] Image customization options
299
+ - [ ] Multiple invitation card styles
300
+
301
+ ## 📝 Key Design Decisions
302
+
303
+ ### Conversational Interface
304
+ - Chose chat-based UI over tab-based for better user experience
305
+ - Natural language interaction feels more intuitive
306
+ - Progressive information gathering through dialogue
307
+
308
+ ### Shared Context
309
+ - Centralized state management prevents data duplication
310
+ - Pydantic models ensure type safety
311
+ - Global instance allows easy access across agents
312
+
313
+ ### MCP Integration
314
+ - Graceful degradation ensures app works even if optional services unavailable
315
+ - Clear error messages guide users on setup
316
+ - Modular design allows easy addition of new MCP servers
317
+
318
+ ### Query Optimization
319
+ - Amazon search queries optimized from item names
320
+ - Improves product matching accuracy
321
+ - Reduces failed searches
322
+
323
+ ### Open Source Models
324
+ - All models are open-source (Mistral-7B, FLUX.1-dev)
325
+ - No proprietary dependencies
326
+ - Accessible via Hugging Face Inference API
327
+
328
+ ## 🔐 Security & Privacy
329
+
330
+ - All API keys stored as environment variables
331
+ - No hardcoded credentials
332
+ - Guest data only stored in session (not persisted)
333
+ - Email sending requires explicit user action
334
+ - Amazon shopping is demo mode (no real orders)
335
+
336
+ ## 📊 Performance Considerations
337
+
338
+ - LLM calls have timeouts to prevent hanging
339
+ - MCP calls are async for better performance
340
+ - Image generation cached in `invitations/` directory
341
+ - Chat history managed efficiently with Gradio State
342
+ - Query optimization reduces failed Amazon searches
343
+
344
+ ## 🐛 Known Limitations
345
+
346
+ - Amazon MCP requires Node.js build (optional)
347
+ - Email sending requires Zapier Gmail API key
348
+ - Venue search requires Bright Data token
349
+ - Image generation can be slow (depends on FLUX.1-dev API)
350
+ - Chat history not persisted across sessions
351
+
352
+ ## 📚 Additional Resources
353
+
354
+ - See `README.md` for quick start guide
355
+ - See `src/deployment/` for deployment documentation
356
+ - See individual agent files for detailed implementation
357
+
358
+ ---
359
+
360
+ **Built for Hugging Face MCP Hackathon** 🎉
README.md CHANGED
@@ -1,12 +1,251 @@
1
- ---
2
- title: Plan A Party
3
- emoji: 🏃
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: AI-Powered Private Event Planner
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # 🎉 Plan-a-Party Multi-Agent System
2
+
3
+ A comprehensive party planning system built with LangChain and Gradio, featuring multiple specialized AI agents that work together through an interactive conversational interface to help you plan the perfect party!
4
+
5
+ ## ✨ Features
6
+
7
+ ### 🤖 Multi-Agent Architecture
8
+
9
+ 1. **Theme Planning Agent** - Generates creative party themes and creates beautiful 4x4 invitation cards using FLUX.1-dev
10
+ 2. **Guest Invite Email Agent** - Manages guest lists from CSV and sends personalized invitation emails via Resend MCP
11
+ 3. **Venue Search Agent** - Searches for party venues using Bright Data MCP SERP search
12
+ 4. **Theme Decor/Favor Shop Agent** - Generates shopping lists and builds Amazon carts with intelligent query optimization
13
+
14
+ ### 🎨 Key Capabilities
15
+
16
+ - **Interactive Chat Interface** - Natural language conversation to guide party planning
17
+ - **Event Metadata Collection** - Captures budget, location, and date through conversation
18
+ - **Theme Generation** - AI generates 3 creative, age-appropriate party themes
19
+ - **Natural Language Theme Selection** - Select themes directly in chat (e.g., "I like the first one")
20
+ - **Invitation Cards** - Beautiful 4x4 invitation cards with custom text and images using FLUX.1-dev
21
+ - **Email Automation** - Send invitations to multiple guests via Resend MCP
22
+ - **Venue Discovery** - Search for party venues using Bright Data SERP with location, budget, and guest count filters
23
+ - **Smart Shopping** - Generate theme-based shopping lists with Amazon-optimized search queries
24
+ - **Cart Building** - Build Amazon shopping carts with product links and pricing
25
+
26
+ ## 🚀 Setup
27
+
28
+ ### Prerequisites
29
+
30
+ - Python 3.8+
31
+ - Hugging Face account with API token
32
+ - (Optional) Bright Data token for venue search
33
+ - (Optional) Zapier Gmail API key for Resend MCP email sending
34
+ - (Optional) Amazon MCP server (requires Node.js) for shopping
35
+ - (Optional) Fewsats API key for payment processing
36
+
37
+ ### Installation
38
+
39
+ 1. **Clone or navigate to the project directory:**
40
+ ```bash
41
+ cd plan-a-party
42
+ ```
43
+
44
+ 2. **Create a virtual environment:**
45
+ ```bash
46
+ python -m venv .venv
47
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
48
+ ```
49
+
50
+ 3. **Install dependencies:**
51
+ ```bash
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ 4. **Set up environment variables:**
56
+ Create a `.env` file or set environment variables:
57
+ ```bash
58
+ export HUGGINGFACEHUB_API_TOKEN=your_token_here
59
+ export HF_TOKEN=your_token_here
60
+ export BRIGHTDATA_TOKEN=your_brightdata_token # Optional
61
+ export ZAPIER_GMAIL_API_KEY=your_zapier_key # Optional
62
+ export FEWSATS_API_KEY=your_fewsats_key # Optional
63
+ ```
64
+
65
+ 5. **Set up Amazon MCP (Optional):**
66
+ ```bash
67
+ cd amazon-mcp
68
+ npm install
69
+ npm run build
70
+ cd ..
71
+ ```
72
+
73
+ ### Environment Variables
74
+
75
+ Create a `.env` file or set these environment variables:
76
+
77
+ ```ini
78
+ # Required
79
+ HUGGINGFACEHUB_API_TOKEN=your_token_here
80
+ HF_TOKEN=your_token_here
81
+
82
+ # Optional - for venue search
83
+ BRIGHTDATA_TOKEN=your_brightdata_token
84
+
85
+ # Optional - for email sending
86
+ ZAPIER_GMAIL_API_KEY=your_zapier_key
87
+
88
+ # Optional - for Amazon shopping
89
+ FEWSATS_API_KEY=your_fewsats_key
90
+ ```
91
+
92
+ ## 🎯 Usage
93
+
94
+ ### Running Locally
95
+
96
+ ```bash
97
+ python app.py
98
+ ```
99
+
100
+ The Gradio interface will launch at `http://localhost:7860`
101
+
102
+ ### Running on Hugging Face Spaces
103
+
104
+ 1. Push this repository to Hugging Face Spaces
105
+ 2. Set environment variables in Space settings
106
+ 3. The app will automatically deploy!
107
+
108
+ ## 📋 Interactive Workflow
109
+
110
+ The app uses a conversational interface where you interact naturally:
111
+
112
+ 1. **Start Planning** → Describe your party (e.g., "I want to plan a birthday party for my 6 year old kid")
113
+ 2. **Provide Details** → System asks for budget, location, and date (e.g., "Budget is under $500, in San Jose CA on June 10, 2025")
114
+ 3. **Theme Generation** → System automatically generates 3 theme options
115
+ 4. **Select Theme** → Choose a theme in natural language (e.g., "I like the first one" or "Let's go with the Minecraft theme")
116
+ 5. **Generate Invitation** → Click "Generate Invitation Card" button (appears when all info is ready)
117
+ 6. **Find Venues** → Ask to search for venues (e.g., "Search for venues") - uses your stored location
118
+ 7. **Upload Guest List** → Upload CSV with name and email columns
119
+ 8. **Send Invitations** → Ask to send invitations (e.g., "Send the invitations")
120
+ 9. **Shop for Decor** → Ask about shopping (e.g., "What decor items do you recommend?")
121
+ 10. **Build Cart** → Select items and ask to build Amazon cart
122
+
123
+ ## 📁 Project Structure
124
+
125
+ ```
126
+ plan-a-party/
127
+ ├── app.py # Gradio entrypoint
128
+ ├── requirements.txt # Python dependencies
129
+ ├── README.md # This file
130
+ ├── PROJECT_SUMMARY.md # Detailed architecture documentation
131
+ ├── .env.example # Example environment variables
132
+ ├── invitations/ # Generated invitation card images
133
+ └── src/
134
+ ├── config.py # Configuration management
135
+ ├── party_context.py # Shared state between agents (Pydantic models)
136
+ ├── agents/
137
+ │ ├── orchestrator.py # Main orchestrator agent (conversational flow)
138
+ │ ├── theme_planning_agent.py
139
+ │ ├── guest_invite_email_agent.py
140
+ │ ├── venue_search_agent.py
141
+ │ └── theme_decor_favor_shop_agent.py
142
+ ├── tools/
143
+ │ └── csv_guest_parser.py
144
+ ├── mcp/
145
+ │ └── mcp_clients.py # MCP clients (Amazon, Fewsats, Resend, BrightData)
146
+ └── ui/
147
+ └── gradio_ui.py # Gradio interface (interactive chat)
148
+ ```
149
+
150
+ ## 🔧 Technologies Used
151
+
152
+ - **LangChain** - Agent framework and tool orchestration
153
+ - **Gradio** - Interactive web UI framework
154
+ - **Hugging Face** - Open-source LLMs and image generation
155
+ - **Mistral-7B-Instruct-v0.3** - Text generation
156
+ - **FLUX.1-dev** - Image generation for invitation cards
157
+ - **MCP (Model Context Protocol)** - Integration protocol for external services
158
+ - **Bright Data MCP** - SERP search for venue discovery
159
+ - **Resend MCP** - Email sending via Zapier
160
+ - **Amazon MCP** - Product search and cart building
161
+ - **Fewsats MCP** - Payment processing (X402/L402 protocols)
162
+ - **Pydantic** - Data validation and shared context models
163
+ - **Pandas** - CSV processing
164
+
165
+ ## 📝 CSV Format for Guest List
166
+
167
+ Your guest list CSV should have two columns:
168
+
169
+ ```csv
170
+ name,email
171
+ John Doe,john@example.com
172
+ Jane Smith,jane@example.com
173
+ ```
174
+
175
+ ## 🎨 Architecture Highlights
176
+
177
+ ### Shared Context (`PartyContext`)
178
+ - Centralized state management using Pydantic models
179
+ - Stores event metadata (budget, location, date)
180
+ - Manages theme options and selections
181
+ - Tracks guest lists, venue results, and shopping cart items
182
+
183
+ ### Conversational Flow
184
+ - Multi-turn dialogue to progressively gather requirements
185
+ - Natural language understanding for theme selection
186
+ - Context-aware responses based on collected information
187
+ - Dynamic UI updates based on conversation state
188
+
189
+ ### MCP Integration
190
+ - **Graceful Degradation** - App works even if optional MCP servers are unavailable
191
+ - **Bright Data** - Real-time SERP search for venue discovery
192
+ - **Resend** - Reliable email delivery via Zapier integration
193
+ - **Amazon** - Optional shopping with intelligent query optimization
194
+ - **Fewsats** - Payment processing for future order placement
195
+
196
+ ### Query Optimization
197
+ - Amazon search queries are automatically optimized from item names
198
+ - Strips descriptions, normalizes case, and adds relevant keywords
199
+ - Improves product matching accuracy
200
+
201
+ ## 🛠️ Development
202
+
203
+ ### Adding New Agents
204
+
205
+ 1. Create a new file in `src/agents/`
206
+ 2. Implement agent functions with `@tool` decorators
207
+ 3. Add agent wrapper to orchestrator in `src/agents/orchestrator.py`
208
+ 4. Update `TOOL_ROUTER` dictionary in orchestrator
209
+ 5. Update Gradio UI if needed
210
+
211
+ ### Testing
212
+
213
+ Each agent can be tested independently:
214
+
215
+ ```python
216
+ from src.agents.theme_planning_agent import generate_party_themes
217
+
218
+ result = generate_party_themes.invoke({
219
+ "party_description": "Birthday party for 5 year old"
220
+ })
221
+ print(result)
222
+ ```
223
+
224
+ ## 🔮 Future Enhancements
225
+
226
+ - [ ] Order tracking and status updates
227
+ - [ ] Multi-language support
228
+ - [ ] Theme customization options
229
+ - [ ] Calendar integration for date selection
230
+ - [ ] Budget tracking and alerts
231
+ - [ ] Guest RSVP management
232
+ - [ ] Venue booking integration
233
+
234
+ ## 📄 License
235
+
236
+ This project is open source and available for the Hugging Face MCP Hackathon.
237
+
238
+ ## 🤝 Contributing
239
+
240
+ This is a hackathon project. Feel free to fork and extend!
241
+
242
+ ## 🙏 Acknowledgments
243
+
244
+ - Hugging Face for open-source models and infrastructure
245
+ - LangChain team for the agent framework
246
+ - Bright Data for SERP search capabilities
247
+ - All open-source contributors
248
+
249
  ---
250
 
251
+ **Built for the Hugging Face MCP-1st-Birthday Hackathon** 🎉
app.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ from src.ui.gradio_ui import build_interface
3
+
4
+ demo = build_interface()
5
+
6
+ if __name__ == "__main__":
7
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ langchain>=0.1.0
2
+ langgraph>=0.0.40
3
+ langchain-huggingface>=0.0.1
4
+ langchain-mcp-adapters>=0.1.0
5
+ huggingface_hub>=0.20.0
6
+ gradio>=4.0.0
7
+ pandas>=2.0.0
8
+ python-dotenv>=1.0.0
9
+ requests>=2.31.0
10
+ pydantic>=2.0.0
11
+ pillow>=10.0.0
12
+ beautifulsoup4>=4.12.0
13
+ lxml>=4.9.0
14
+ email-validator>=2.0.0
15
+
src/.DS_Store ADDED
Binary file (6.15 kB). View file
 
src/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Plan-a-Party multi-agent system."""
2
+
src/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (280 Bytes). View file
 
src/__pycache__/config.cpython-314.pyc ADDED
Binary file (1.41 kB). View file
 
src/__pycache__/party_context.cpython-314.pyc ADDED
Binary file (4.31 kB). View file
 
src/agents/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Agent modules for Plan-a-Party system."""
2
+
src/agents/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (293 Bytes). View file
 
src/agents/__pycache__/guest_invite_email_agent.cpython-314.pyc ADDED
Binary file (9.53 kB). View file
 
src/agents/__pycache__/orchestrator.cpython-314.pyc ADDED
Binary file (33.6 kB). View file
 
src/agents/__pycache__/theme_decor_favor_shop_agent.cpython-314.pyc ADDED
Binary file (14.9 kB). View file
 
src/agents/__pycache__/theme_planning_agent.cpython-314.pyc ADDED
Binary file (12.6 kB). View file
 
src/agents/__pycache__/venue_search_agent.cpython-314.pyc ADDED
Binary file (9.62 kB). View file
 
src/agents/guest_invite_email_agent.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/agents/guest_invite_email_agent.py
2
+
3
+ """
4
+ Guest-Invite Email Agent (LangChain + Resend MCP)
5
+
6
+ - Composes invitation email subject/body from party context
7
+ - Sends emails to all guests via Resend MCP "send_email" tool
8
+ - Exposes `guest_invite_tool` for orchestrator / other agents
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import asyncio
14
+ from typing import List, Dict, Tuple, Optional
15
+
16
+ from langchain_core.tools import tool, Tool
17
+ from langchain_core.messages import HumanMessage
18
+ # Note: AgentExecutor removed - using tools directly
19
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
20
+
21
+ from langchain_mcp_adapters.client import MultiServerMCPClient
22
+
23
+ from src.config import TEXT_MODEL
24
+ from src.party_context import party_context
25
+
26
+
27
+ # -------------------------------------------------------------------
28
+ # 1. LLM for generating subject/body
29
+ # -------------------------------------------------------------------
30
+
31
+ def get_text_llm() -> ChatHuggingFace:
32
+ """Open-source HF model for email copy generation."""
33
+ llm = HuggingFaceEndpoint(
34
+ repo_id=TEXT_MODEL,
35
+ task="text-generation",
36
+ max_new_tokens=512,
37
+ temperature=0.7,
38
+ do_sample=True,
39
+ )
40
+ return ChatHuggingFace(llm=llm)
41
+
42
+
43
+ text_llm = get_text_llm()
44
+
45
+
46
+ # -------------------------------------------------------------------
47
+ # 2. Resend MCP tooling
48
+ # -------------------------------------------------------------------
49
+
50
+ async def _get_resend_send_email_tool_async():
51
+ """
52
+ Initialize MultiServerMCPClient for the Resend MCP server and
53
+ return the `send_email` tool.
54
+
55
+ You MUST have the Resend MCP server build path and RESEND_API_KEY
56
+ configured via environment variables.
57
+ """
58
+
59
+ zapier_gmail_api_key = os.getenv("ZAPIER_GMAIL_API_KEY")
60
+ if not zapier_gmail_api_key:
61
+ raise ValueError(
62
+ "ZAPIER_GMAIL_API_KEY not set. "
63
+ "Get it from the Zapier dashboard."
64
+ )
65
+
66
+ # Strip surrounding quotes if present (common issue from .env files)
67
+ zapier_gmail_api_key = zapier_gmail_api_key.strip('"').strip("'")
68
+
69
+ client = MultiServerMCPClient(
70
+ {
71
+ "zapier_gmail": {
72
+ "url": "https://mcp.zapier.com/api/mcp/s/" + zapier_gmail_api_key + "/mcp",
73
+ "transport": "streamable_http", # Zapier MCP uses HTTP/JSON
74
+ }
75
+ }
76
+ )
77
+
78
+ # Get tools from all configured servers
79
+ tools = await client.get_tools()
80
+
81
+ # Find the gmail_send_email tool (name comes from the MCP server)
82
+ send_tool = None
83
+ for t in tools:
84
+ print(f"[get_resend_send_email_tool_async] Tool name: {t.name}")
85
+ if t.name == "gmail_send_email":
86
+ send_tool = t
87
+ break
88
+
89
+ if send_tool is None:
90
+ raise RuntimeError("Could not find 'gmail_send_email' tool from Zapier Gmail MCP server.")
91
+
92
+ return client, send_tool
93
+
94
+
95
+ def _get_resend_send_email_tool_sync():
96
+ """Sync wrapper for getting the Zapier Gmail gmail_send_email tool."""
97
+ try:
98
+ # Check if there's already a running event loop
99
+ loop = asyncio.get_running_loop()
100
+ # If we reach here, there IS a running loop - we need to create a task
101
+ # This shouldn't normally happen in our sync context, but handle it gracefully
102
+ raise RuntimeError(
103
+ "Cannot call _get_resend_send_email_tool_sync from an async context. "
104
+ "Use _get_resend_send_email_tool_async instead."
105
+ )
106
+ except RuntimeError as e:
107
+ if "no running event loop" in str(e):
108
+ # No running loop - safe to use asyncio.run()
109
+ return asyncio.run(_get_resend_send_email_tool_async())
110
+ else:
111
+ # Different RuntimeError - re-raise it
112
+ raise
113
+
114
+
115
+ # -------------------------------------------------------------------
116
+ # 3. Tools: Compose subject/body + send via Resend MCP
117
+ # -------------------------------------------------------------------
118
+
119
+ @tool
120
+ def compose_invitation_email() -> str:
121
+ """
122
+ Compose a subject and email body using the shared party_context.
123
+
124
+ Requires:
125
+ - party_context.party_description (optional but helpful)
126
+ - party_context.selected_theme_name
127
+ - party_context.invitation_text
128
+
129
+ Returns:
130
+ JSON string: {"subject": "...", "body": "..."}
131
+ """
132
+ if not party_context.invitation_text or not party_context.selected_theme_name:
133
+ return json.dumps(
134
+ {
135
+ "error": "invitation_text or selected_theme_name missing in party_context"
136
+ }
137
+ )
138
+
139
+ prompt = f"""
140
+ You are writing an email invitation for a kids' party.
141
+
142
+ Party description:
143
+ {party_context.party_description}
144
+
145
+ Selected theme:
146
+ {party_context.selected_theme_name}
147
+
148
+ Invitation card text:
149
+ {party_context.invitation_text}
150
+
151
+ Task:
152
+ 1. Create a concise, fun email subject line (max 10 words).
153
+ 2. Create a friendly email body that:
154
+ - Greets the recipient by name placeholder: {{guest_name}}
155
+ - Summarizes the party
156
+ - Mentions key details from the invitation
157
+ - Includes a polite RSVP request
158
+
159
+ Return ONLY valid JSON:
160
+ {{
161
+ "subject": "...",
162
+ "body": "..."
163
+ }}
164
+ """
165
+ resp = text_llm.invoke([HumanMessage(content=prompt)])
166
+ content = resp.content
167
+
168
+ # Try to parse JSON
169
+ try:
170
+ data = json.loads(content)
171
+ except Exception:
172
+ start = content.find("{")
173
+ end = content.rfind("}")
174
+ json_str = content[start : end + 1]
175
+ data = json.loads(json_str)
176
+
177
+ # Optionally store in context if you add these fields
178
+ # party_context.email_subject = data.get("subject")
179
+ # party_context.email_body_template = data.get("body")
180
+
181
+ return json.dumps(data, ensure_ascii=False)
182
+
183
+
184
+ @tool
185
+ async def send_guest_invites_via_resend() -> str:
186
+ """
187
+ Send the invitation email to all guests in party_context via Resend MCP.
188
+
189
+ Requires:
190
+ - party_context.guest_names
191
+ - party_context.guest_emails
192
+ - party_context.invitation_text
193
+ - party_context.selected_theme_name
194
+
195
+ Behavior:
196
+ - Uses compose_invitation_email() to get subject/body
197
+ - For each guest, personalizes body using guest name
198
+ - Calls Resend MCP `send_email` tool
199
+ - Returns a summary string of send results
200
+ """
201
+ if not party_context.guest_emails:
202
+ return "No guest emails found in party_context.guest_emails."
203
+
204
+ if not party_context.invitation_text or not party_context.selected_theme_name:
205
+ return (
206
+ "Cannot send invites: invitation_text or selected_theme_name "
207
+ "missing in party_context."
208
+ )
209
+
210
+ # 1. Get subject/body
211
+ summary_json = compose_invitation_email.invoke({})
212
+ try:
213
+ summary = json.loads(summary_json)
214
+ except Exception:
215
+ return f"Could not parse composed email JSON: {summary_json}"
216
+
217
+ subject = summary.get("subject", "You're invited!")
218
+ body_template = summary.get("body", party_context.invitation_text)
219
+
220
+ sender_email = os.getenv("SENDER_EMAIL_ADDRESS", "onboarding@resend.dev")
221
+
222
+ # 2. Load Resend MCP tool (async)
223
+ client, send_tool = await _get_resend_send_email_tool_async()
224
+
225
+ # 3. Send to each guest
226
+ results = []
227
+ print(f"[send_guest_invites_via_resend] Sending to {len(party_context.guest_emails)} guests")
228
+ names = party_context.guest_names or [""] * len(party_context.guest_emails)
229
+
230
+ for name, email in zip(names, party_context.guest_emails):
231
+ guest_name = name or "there"
232
+ # Simple personalization
233
+ body = body_template.replace("{guest_name}", guest_name)
234
+
235
+ # Clean up email addresses
236
+ email = email.strip().replace(" ", "")
237
+
238
+ try:
239
+ # Zapier Gmail MCP expects these parameters:
240
+ # - instructions: natural language description
241
+ # - to: recipient email (can be array)
242
+ # - subject: email subject
243
+ # - body: email body text
244
+ tool_input = {
245
+ "instructions": f"Send an invitation email to {guest_name} at {email}",
246
+ "to": [email],
247
+ "subject": subject,
248
+ "body": body
249
+ }
250
+
251
+ # Use ainvoke for async invocation
252
+ send_result = await send_tool.ainvoke(tool_input)
253
+ results.append(f"{email}: {send_result}")
254
+ print(f"[send_guest_invites_via_resend] Sent to {email}: {send_result}")
255
+ except Exception as e:
256
+ results.append(f"{email}: ERROR {str(e)}")
257
+ print(f"[send_guest_invites_via_resend] Error sending to {email}: {str(e)}")
258
+
259
+ # Close client properly (async)
260
+ try:
261
+ await client.close()
262
+ except Exception:
263
+ pass
264
+
265
+ # You could also store this in party_context if you want
266
+ # party_context.email_send_status = results
267
+
268
+ summary_str = "Guest invite send results:\n" + "\n".join(results)
269
+ return summary_str
270
+
271
+
272
+ # -------------------------------------------------------------------
273
+ # 4. Subagent (simplified - tools are used directly)
274
+ # -------------------------------------------------------------------
275
+ # Note: AgentExecutor removed for LangChain 1.0+ compatibility
276
+ # Tools are called directly by the orchestrator or UI
277
+
278
+
279
+ async def _run_guest_invite_agent_async() -> str:
280
+ """
281
+ Async wrapper to be used by the sync function.
282
+
283
+ It will:
284
+ - Compose subject/body
285
+ - Send invites to all guests in party_context via Resend MCP
286
+ """
287
+ # Directly call the send tool since AgentExecutor was removed
288
+ result = await send_guest_invites_via_resend.ainvoke({})
289
+ return result
290
+
291
+
292
+ def _run_guest_invite_agent() -> str:
293
+ """
294
+ Synchronous wrapper for the orchestrator to call.
295
+ Handles the async execution internally.
296
+ """
297
+ try:
298
+ # Check if there's already a running event loop
299
+ loop = asyncio.get_running_loop()
300
+ # If we're already in an async context, we can't use asyncio.run()
301
+ # Instead, we need to create a task or use a different approach
302
+ raise RuntimeError(
303
+ "guest_invite_email_agent cannot be called from within an async context. "
304
+ "Please use the async version directly or call from a sync context."
305
+ )
306
+ except RuntimeError as e:
307
+ if "no running event loop" in str(e):
308
+ # No running loop - safe to use asyncio.run()
309
+ return asyncio.run(_run_guest_invite_agent_async())
310
+ else:
311
+ # Different RuntimeError - re-raise it
312
+ raise
313
+
314
+
315
+ guest_invite_tool = Tool.from_function(
316
+ func=_run_guest_invite_agent,
317
+ name="guest_invite_email_agent",
318
+ description=(
319
+ "Subagent that sends invitation emails to all guests using the Resend MCP "
320
+ "server and the current party_context (guest list + invitation text). "
321
+ "Call this AFTER the theme-planning agent has generated invitation_text "
322
+ "and the guest CSV has been uploaded."
323
+ ),
324
+ )
src/agents/orchestrator.py ADDED
@@ -0,0 +1,791 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Orchestrator Agent - Coordinates all subagents."""
2
+ import os
3
+ import json
4
+ from typing import List, Dict, Any, Optional
5
+
6
+ from langchain_core.messages import HumanMessage
7
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
8
+ from langchain_core.tools import Tool
9
+
10
+ from src.config import TEXT_MODEL
11
+ from src.party_context import party_context
12
+
13
+ # Import subagent tools
14
+ from src.agents.theme_planning_agent import generate_party_themes, generate_invitation_for_theme
15
+ from src.agents.guest_invite_email_agent import compose_invitation_email, _run_guest_invite_agent
16
+ from src.agents.venue_search_agent import search_party_venues
17
+ from src.agents.theme_decor_favor_shop_agent import generate_shopping_list_for_theme, shop_on_amazon
18
+ from src.tools.csv_guest_parser import parse_guest_csv
19
+ from typing import List, Dict, Any, Optional
20
+ from src.party_context import party_context
21
+ from src.agents.theme_decor_favor_shop_agent import (
22
+ generate_shopping_list_for_theme,
23
+ shop_on_amazon,
24
+ )
25
+
26
+ # ---------- LLM factory & global instance ----------
27
+
28
+ def get_orchestrator_llm() -> ChatHuggingFace:
29
+ """Get the orchestrator LLM."""
30
+ llm = HuggingFaceEndpoint(
31
+ repo_id=TEXT_MODEL,
32
+ task="text-generation",
33
+ max_new_tokens=512,
34
+ temperature=0.3, # Lower temperature for more deterministic orchestration
35
+ do_sample=False,
36
+ )
37
+ return ChatHuggingFace(llm=llm)
38
+
39
+
40
+ orchestrator_llm = get_orchestrator_llm()
41
+
42
+
43
+ # ---------- Wrapper tools (subagents) ----------
44
+
45
+ def theme_planning_wrapper(
46
+ party_description: Optional[str] = None,
47
+ selected_theme_name: Optional[str] = None,
48
+ ) -> str:
49
+ """Wrapper for theme planning operations."""
50
+ if selected_theme_name:
51
+ return generate_invitation_for_theme.invoke({"selected_theme_name": selected_theme_name})
52
+ elif party_description:
53
+ return generate_party_themes.invoke({"party_description": party_description})
54
+ else:
55
+ return "Error: Either party_description or selected_theme_name must be provided"
56
+
57
+
58
+ def guest_invite_wrapper(csv_file_path: Optional[str] = None, send_emails: bool = False) -> str:
59
+ """Wrapper for guest invite operations."""
60
+ if csv_file_path and os.path.exists(csv_file_path):
61
+ print(f"[guest_invite_wrapper] CSV file exists: {csv_file_path}")
62
+ try:
63
+ names, emails = parse_guest_csv(csv_file_path)
64
+ party_context.guest_names = names
65
+ party_context.guest_emails = emails
66
+ print(f"[guest_invite_wrapper] Loaded {len(names)} guests from CSV file")
67
+ return f"Successfully loaded {len(names)} guests from CSV file."
68
+ except Exception as e:
69
+ print(f"[guest_invite_wrapper] Error loading CSV: {str(e)}")
70
+ return f"Error loading CSV: {str(e)}"
71
+
72
+ if send_emails:
73
+ print(f"[guest_invite_wrapper] Sending emails to {len(party_context.guest_emails)} guests")
74
+ return _run_guest_invite_agent()
75
+ else:
76
+ if party_context.guest_names:
77
+ print(f"[guest_invite_wrapper] Guest list loaded: {len(party_context.guest_names)} guests")
78
+ return f"Guest list loaded: {len(party_context.guest_names)} guests. Ready to send invitations."
79
+ else:
80
+ return "Please upload a guest list CSV file first."
81
+
82
+
83
+ def venue_search_wrapper(
84
+ location: str,
85
+ query: Optional[str] = None,
86
+ max_results: int = 5,
87
+ budget: str = "",
88
+ guest_count: int = 0,
89
+ ) -> str:
90
+ """Wrapper for venue search operations."""
91
+ search_query = query or "party venue"
92
+ return search_party_venues.invoke({
93
+ "location": location,
94
+ "query": search_query,
95
+ "max_results": max_results,
96
+ "budget": budget,
97
+ "guest_count": guest_count,
98
+ })
99
+
100
+
101
+ def shopping_wrapper(
102
+ selected_items: Optional[List[str]] = None,
103
+ shop_on_amazon_flag: bool = False,
104
+ fewsats_api_key: Optional[str] = None,
105
+ shipping_address: Optional[Dict[str, Any]] = None,
106
+ user_info: Optional[Dict[str, Any]] = None,
107
+ ) -> str:
108
+ """
109
+ Wrapper for shopping operations.
110
+
111
+ Demo semantics:
112
+ - If shop_on_amazon_flag is False → just generate the decor + favor list.
113
+ - If True:
114
+ * Use given selected_items OR fall back to party_context.selected_items.
115
+ * Build cart via MCP-backed Amazon search.
116
+ """
117
+ if not shop_on_amazon_flag:
118
+ # generate decor + favors list based on current theme
119
+ return generate_shopping_list_for_theme.invoke({"theme_name": None})
120
+
121
+ # shop_on_amazon_flag = True
122
+ if not selected_items:
123
+ if party_context.selected_items:
124
+ selected_items = party_context.selected_items
125
+ else:
126
+ return (
127
+ "No items selected for shopping yet. "
128
+ "Pick decor/favor items in the **Decor & Favors (Shopping)** section, "
129
+ "click **Save selected items**, then ask me to build your cart."
130
+ )
131
+
132
+ return shop_on_amazon.invoke(
133
+ {
134
+ "selected_items": selected_items,
135
+ "fewsats_api_key": fewsats_api_key,
136
+ "shipping_address": shipping_address,
137
+ "user_info": user_info,
138
+ }
139
+ )
140
+
141
+
142
+
143
+ # Map tool names (what the LLM will output) to wrapper functions
144
+
145
+ TOOL_ROUTER = {
146
+ "theme_planning_agent": theme_planning_wrapper,
147
+ "venue_search_agent": venue_search_wrapper,
148
+ "guest_invite_email_agent": guest_invite_wrapper,
149
+ "theme_decor_favor_shop_agent": shopping_wrapper,
150
+ }
151
+ # ---------- Context summarization ----------
152
+
153
+ def summarize_party_context() -> str:
154
+ """Create a compact JSON summary of current party_context for the LLM."""
155
+ state = {
156
+ "party_description": party_context.party_description,
157
+ "event_budget": party_context.event_budget,
158
+ "event_location": party_context.event_location,
159
+ "event_date": party_context.event_date,
160
+ "has_themes": bool(party_context.theme_options),
161
+ "theme_count": len(party_context.theme_options),
162
+ "selected_theme_name": party_context.selected_theme_name,
163
+ "has_invitation": bool(party_context.invitation_text),
164
+ "guest_count": len(party_context.guest_emails),
165
+ "emails_sent": party_context.emails_sent,
166
+ "has_venue_results": bool(party_context.venue_results),
167
+ "decor_items_count": len(party_context.decor_items),
168
+ "favor_items_count": len(party_context.favor_items),
169
+ "selected_items_count": len(party_context.selected_items),
170
+ "order_status": party_context.order_status,
171
+ }
172
+ return json.dumps(state, ensure_ascii=False)
173
+
174
+
175
+ # ---------- LLM policy for next action ----------
176
+
177
+ def decide_next_action(user_message: str, history: List[Dict[str, str]]) -> Dict[str, Any]:
178
+ """
179
+ Ask the LLM what to do next:
180
+ - Ask the user something? (action='ask_user')
181
+ - Call a tool? (action='call_tool')
182
+ Returns a JSON dict.
183
+ """
184
+ context_summary = summarize_party_context()
185
+
186
+ # Turn last few messages into a transcript for the LLM
187
+ transcript_lines = []
188
+ for turn in history[-8:]:
189
+ role = turn.get("role", "user")
190
+ content = turn.get("content", "")
191
+ prefix = "User" if role == "user" else "Assistant"
192
+ transcript_lines.append(f"{prefix}: {content}")
193
+ transcript = "\n".join(transcript_lines)
194
+
195
+ policy_prompt = f"""
196
+ You are the proactive orchestrator agent for a multi-agent party planning system.
197
+
198
+ You NEVER call external APIs yourself. You decide whether to:
199
+ - Ask the user a follow-up question proactively, OR
200
+ - Call one of these tools:
201
+
202
+ 1. theme_planning_agent
203
+ - party_description: str (optional)
204
+ - selected_theme_name: str (optional)
205
+ Use it to: generate 3 theme options OR generate an invitation card for a selected theme. Based on the party description generate age-appropriate themes for the party.
206
+
207
+ 2. venue_search_agent
208
+ - location: str
209
+ - query: str (optional)
210
+ - max_results: int (optional)
211
+ - budget: str (optional)
212
+ - guest_count: int (optional)
213
+ Use it to: search for party venues near a given city/state within budget and suitable for the guest count.
214
+
215
+ 3. guest_invite_email_agent
216
+ - csv_file_path: str (optional)
217
+ - send_emails: bool (optional)
218
+ Use it to: load guest list from CSV and send invitation emails.
219
+
220
+
221
+ 4. theme_decor_favor_shop_agent
222
+ - selected_items: List[str] (optional)
223
+ - shop_on_amazon_flag: bool (optional)
224
+ - fewsats_api_key: str (optional)
225
+ - shipping_address: dict (optional)
226
+ - user_info: dict (optional)
227
+ Use it to: generate decor/favor shopping lists OR place orders via Amazon+Fewsats MCP.
228
+
229
+ Current shared state (party_context) as JSON:
230
+ {context_summary}
231
+
232
+ Recent conversation:
233
+ {transcript}
234
+
235
+ User just said:
236
+ "{user_message}"
237
+
238
+ Decide the next action in EXACTLY one of these JSON formats:
239
+
240
+ If you want to ask the user for more info or confirmation:
241
+
242
+ {{
243
+ "action": "ask_user",
244
+ "message_to_user": "your friendly question or response (1–3 sentences)"
245
+ }}
246
+
247
+ If you want to call a tool:
248
+
249
+ {{
250
+ "action": "call_tool",
251
+ "tool_name": "<one of: theme_planning_agent, guest_invite_email_agent, venue_search_agent, theme_decor_favor_shop_agent>",
252
+ "tool_args": {{ ... }},
253
+ "message_to_user": "what you will tell the user after calling the tool (1–3 sentences, friendly)"
254
+ }}
255
+
256
+ High-level flow for this app:
257
+
258
+ - If you don't yet know event_date or event_location, first ASK the user for them.
259
+ - When the user describes the party and you have date & location, and they express interest in themes,
260
+ call theme_planning_agent(party_description=...).
261
+ - When themes exist and the user says which theme they like (e.g. "I like the superhero one"),
262
+ call theme_planning_agent(selected_theme_name="chosen theme name") to generate an invitation card.
263
+ - When a guest CSV has been uploaded (handled by the UI; state shows guest_count > 0),
264
+ and the user confirms sending invites, call guest_invite_email_agent with send_emails=true.
265
+ - When user asks for venues and you know a location (and ideally budget / guest count),call venue_search_agent with those parameters.
266
+ - When user wants decor and favors, call theme_decor_favor_shop_agent to generate a shopping list.
267
+ - When user wants to place the order and you know selected_items and have a FEWSATS key and address,
268
+ call theme_decor_favor_shop_agent with shop_on_amazon_flag=true.
269
+
270
+ NEVER mention tool names in message_to_user. Speak like a normal assistant.
271
+ Return ONLY the JSON object, with no extra commentary.
272
+ """
273
+
274
+ resp = orchestrator_llm.invoke([HumanMessage(content=policy_prompt)])
275
+ content = resp.content.strip()
276
+
277
+ # Extract JSON robustly
278
+ start = content.find("{")
279
+ end = content.rfind("}") + 1
280
+ json_str = content[start:end] if start != -1 and end > start else content
281
+
282
+ try:
283
+ parsed = json.loads(json_str)
284
+ except Exception:
285
+ parsed = {
286
+ "action": "ask_user",
287
+ "message_to_user": (
288
+ "Sorry, I got a bit confused. Could you please restate what you'd like to do next for your party?"
289
+ ),
290
+ }
291
+ # NEW: store the raw decision in context for UI/debugging
292
+ # COMMENTED OUT: agent trace logging
293
+ # try:
294
+ # party_context.last_decision = parsed
295
+ # pretty = json.dumps(parsed, indent=2, ensure_ascii=False)
296
+ # party_context.reasoning_log.append(f"Decision:\n{pretty}")
297
+ # # keep only last N entries so it doesn't explode
298
+ # if len(party_context.reasoning_log) > 30:
299
+ # party_context.reasoning_log = party_context.reasoning_log[-30:]
300
+ # except Exception:
301
+ # pass
302
+
303
+ return parsed
304
+
305
+
306
+ # ---------- Optional: LangChain Tool objects (for external use) ----------
307
+
308
+ theme_planning_tool = Tool.from_function(
309
+ func=theme_planning_wrapper,
310
+ name="theme_planning_agent",
311
+ description=(
312
+ "Plan party themes and create invitation cards. "
313
+ "Call with party_description to generate 3 theme options, "
314
+ "or with selected_theme_name to generate invitation card for that theme."
315
+ ),
316
+ )
317
+
318
+
319
+
320
+ venue_search_tool = Tool.from_function(
321
+ func=venue_search_wrapper,
322
+ name="venue_search_agent",
323
+ description=(
324
+ "Search for party venues by location. "
325
+ "Call with location (e.g., 'Santa Clara CA') and optional query and max_results."
326
+ ),
327
+ )
328
+
329
+ guest_invite_tool = Tool.from_function(
330
+ func=guest_invite_wrapper,
331
+ name="guest_invite_email_agent",
332
+ description=(
333
+ "Manage guest invitations. "
334
+ "Call with csv_file_path to load guest list from CSV, "
335
+ "or with send_emails=True to send invitation emails to all loaded guests."
336
+ ),
337
+ )
338
+
339
+ shopping_tool = Tool.from_function(
340
+ func=shopping_wrapper,
341
+ name="theme_decor_favor_shop_agent",
342
+ description=(
343
+ "Generate shopping lists for decor and party favors based on theme, "
344
+ "or shop on Amazon for selected items using Fewsats payment. "
345
+ "Call without shop_on_amazon_flag to generate list, "
346
+ "or with shop_on_amazon_flag=True to place orders."
347
+ ),
348
+ )
349
+
350
+ orchestrator_tools = [
351
+ theme_planning_tool,
352
+ guest_invite_tool,
353
+ venue_search_tool,
354
+ shopping_tool,
355
+ ]
356
+
357
+
358
+ # ---------- Optional extractor (used only in early flow) ----------
359
+
360
+ def extract_event_details_from_message(message: str) -> Dict[str, Optional[str]]:
361
+ """
362
+ Use the LLM to pull budget, location, and date out of a natural language message.
363
+ Returns dict with keys: budget, location, date (values may be None).
364
+ """
365
+ prompt = f"""
366
+ You are an information extraction assistant.
367
+
368
+ From the following user message, extract:
369
+ - approximate budget (e.g. "$500", "under 300 dollars", "no budget limit")
370
+ - event location (e.g. "San Jose CA")
371
+ - event date (e.g. "June 10, 2025" or "2025-06-10")
372
+
373
+ User message:
374
+ {message}
375
+
376
+ Return ONLY valid JSON in this exact format:
377
+ {{
378
+ "budget": "string or null",
379
+ "location": "string or null",
380
+ "date": "string or null"
381
+ }}
382
+ If something is not present, set it to null.
383
+ """
384
+ try:
385
+ resp = orchestrator_llm.invoke([HumanMessage(content=prompt)])
386
+ content = resp.content.strip()
387
+ start = content.find("{")
388
+ end = content.rfind("}") + 1
389
+ json_str = content[start:end] if start != -1 and end > start else content
390
+ data = json.loads(json_str)
391
+ return {
392
+ "budget": data.get("budget"),
393
+ "location": data.get("location"),
394
+ "date": data.get("date"),
395
+ }
396
+ except Exception:
397
+ return {"budget": None, "location": None, "date": None}
398
+
399
+
400
+ def extract_theme_selection_from_message(message: str, available_themes: List[str]) -> Optional[str]:
401
+ """
402
+ Use the LLM to detect which theme the user selected from natural language.
403
+ Returns the theme name if found, None otherwise.
404
+ """
405
+ if not available_themes:
406
+ return None
407
+
408
+ themes_list = "\n".join([f"- {t}" for t in available_themes])
409
+ prompt = f"""
410
+ You are an information extraction assistant.
411
+
412
+ The user has these theme options available:
413
+ {themes_list}
414
+
415
+ From the following user message, determine if they are selecting one of these themes.
416
+ The user might say things like:
417
+ - "I like the superhero one"
418
+ - "Let's go with theme 1"
419
+ - "I choose the princess theme"
420
+ - "The first option"
421
+ - "Superhero Adventure"
422
+
423
+ User message:
424
+ {message}
425
+
426
+ Return ONLY valid JSON in this exact format:
427
+ {{
428
+ "selected_theme": "exact theme name from the list above, or null if no theme is clearly selected"
429
+ }}
430
+
431
+ If the user is clearly selecting a theme, return the exact theme name from the list.
432
+ If it's ambiguous or no theme is mentioned, return null.
433
+ """
434
+ try:
435
+ resp = orchestrator_llm.invoke([HumanMessage(content=prompt)])
436
+ content = resp.content.strip()
437
+ start = content.find("{")
438
+ end = content.rfind("}") + 1
439
+ json_str = content[start:end] if start != -1 and end > start else content
440
+ data = json.loads(json_str)
441
+ selected = data.get("selected_theme")
442
+ # Verify it's actually in the available themes list
443
+ if selected and selected in available_themes:
444
+ return selected
445
+ return None
446
+ except Exception:
447
+ return None
448
+
449
+
450
+ # ---------- Main chat entrypoint ----------
451
+ def orchestrator_chat(user_message: str, history: Optional[List[Dict[str, str]]] = None) -> str:
452
+ """
453
+ Main chat entrypoint.
454
+
455
+ Flow:
456
+ 1. First user message -> treat as party_description, ask for budget/location/date.
457
+ 2. Subsequent messages -> extract & fill budget/location/date.
458
+ 3. Once all present -> generate themes automatically.
459
+ 4. After themes -> user picks theme -> we guide them to venue search.
460
+ 5. After user says they booked a venue -> we create the invitation card.
461
+ 6. After that -> send guest emails, decor/cart, etc.
462
+ """
463
+ history = history or []
464
+ user_message = user_message.strip()
465
+ user_lower = user_message.lower()
466
+
467
+ # STEP 1: first message = party description
468
+ if party_context.party_description is None:
469
+ party_context.party_description = user_message
470
+ return (
471
+ "Awesome! 🎉 I can help you plan your private party end-to-end — themes, venues, invitations, "
472
+ "guest emails, and decor shopping.\n\n"
473
+ "First, tell me a few basics so I can suggest good themes:\n"
474
+ "1) Your approximate **budget**\n"
475
+ "2) The **city/state location** of the event\n"
476
+ "3) The **date** of the event\n\n"
477
+ "You can answer in one sentence, for example:\n"
478
+ "\"Budget is under $700, in Sunnyvale CA on December 10, 2025 for about 60 people.\""
479
+ )
480
+
481
+ # STEP 2: fill missing budget/location/date
482
+ missing_budget = party_context.event_budget is None
483
+ missing_location = party_context.event_location is None
484
+ missing_date = party_context.event_date is None
485
+
486
+ if missing_budget or missing_location or missing_date:
487
+ extracted = extract_event_details_from_message(user_message)
488
+
489
+ if missing_budget and extracted.get("budget"):
490
+ party_context.event_budget = extracted["budget"]
491
+ if missing_location and extracted.get("location"):
492
+ party_context.event_location = extracted["location"]
493
+ party_context.search_location = extracted["location"]
494
+ if missing_date and extracted.get("date"):
495
+ party_context.event_date = extracted["date"]
496
+
497
+ still_missing = []
498
+ if party_context.event_budget is None:
499
+ still_missing.append("budget")
500
+ if party_context.event_location is None:
501
+ still_missing.append("location")
502
+ if party_context.event_date is None:
503
+ still_missing.append("date")
504
+
505
+ if still_missing:
506
+ parts = ", ".join(still_missing)
507
+ return (
508
+ "Got it, thanks! I’m still missing your "
509
+ f"**{parts}**. Please share that so I can propose good themes and venues."
510
+ )
511
+
512
+ # We now have all three -> generate themes automatically
513
+ try:
514
+ print(f"[orchestrator] Generating themes for: {party_context.party_description}")
515
+ result = theme_planning_wrapper(party_description=party_context.party_description)
516
+ print(f"[orchestrator] Theme generation result: {result[:100]}...")
517
+ print(f"[orchestrator] Theme options count: {len(party_context.theme_options)}")
518
+
519
+ import time
520
+ if not party_context.theme_options:
521
+ time.sleep(1)
522
+ print(f"[orchestrator] After wait, theme options count: {len(party_context.theme_options)}")
523
+
524
+ if party_context.theme_options:
525
+ theme_names = [t.name for t in party_context.theme_options]
526
+ theme_descriptions = [t.description for t in party_context.theme_options]
527
+ return (
528
+ f"Perfect! Here’s what I have so far:\n"
529
+ f"- Budget: {party_context.event_budget}\n"
530
+ f"- Location: {party_context.event_location}\n"
531
+ f"- Date: {party_context.event_date}\n\n"
532
+ "🎨 **I’ve generated 3 theme options for you below.**\n\n"
533
+ f"1. **{theme_names[0] if len(theme_names) > 0 else 'Theme 1'}**\n"
534
+ f" {theme_descriptions[0] if len(theme_descriptions) > 0 else ''}\n\n"
535
+ f"2. **{theme_names[1] if len(theme_names) > 1 else 'Theme 2'}**\n"
536
+ f" {theme_descriptions[1] if len(theme_descriptions) > 1 else ''}\n\n"
537
+ f"3. **{theme_names[2] if len(theme_names) > 2 else 'Theme 3'}**\n"
538
+ f" {theme_descriptions[2] if len(theme_descriptions) > 2 else ''}\n\n"
539
+ "👉 **Tell me which theme you’d like to use** "
540
+ "(e.g., \"I like the first one\" or \"Let’s go with Superhero Adventure\").\n"
541
+ "After we lock your theme, I’ll help you find some venue options before we create your invitation card."
542
+ )
543
+ else:
544
+ return (
545
+ f"Great! I’ve captured:\n"
546
+ f"- Budget: {party_context.event_budget}\n"
547
+ f"- Location: {party_context.event_location}\n"
548
+ f"- Date: {party_context.event_date}\n\n"
549
+ "I’m generating theme options now — they’ll appear below the chat in a moment."
550
+ )
551
+ except Exception as e:
552
+ import traceback
553
+ error_details = traceback.format_exc()
554
+ print(f"[orchestrator] Error generating themes: {e}\n{error_details}")
555
+ return (
556
+ f"Great! I have all your details. However, I ran into an issue generating themes: {e}\n"
557
+ "Please try again or tell me how you’d like to continue."
558
+ )
559
+
560
+ # STEP 3: Check if user is selecting a theme (before LLM decision)
561
+ if party_context.theme_options and not party_context.selected_theme_name:
562
+ available_theme_names = [t.name for t in party_context.theme_options]
563
+ selected_theme = extract_theme_selection_from_message(user_message, available_theme_names)
564
+ if selected_theme:
565
+ party_context.selected_theme_name = selected_theme
566
+ # After theme selection, we push them to venue search (not invitation yet)
567
+ msg = (
568
+ f"Great choice! You’ve selected **{selected_theme}** 🎉\n\n"
569
+ "Before we create your invitation card, let’s find a venue.\n"
570
+ )
571
+ if party_context.event_location:
572
+ msg += (
573
+ f"I can search for party venues near **{party_context.event_location}** "
574
+ f"within your budget {party_context.event_budget or ''}.\n\n"
575
+ "👉 Would you like me to search for some venue options now?"
576
+ )
577
+ else:
578
+ msg += (
579
+ "Please confirm the city/state where you want to host the party, "
580
+ "then I’ll search for venues for you."
581
+ )
582
+ return msg
583
+
584
+ # STEP 3b: Detect when user says they booked / chose a venue, then create invitation
585
+ if party_context.venue_results and not party_context.invitation_text:
586
+ if any(word in user_lower for word in ["booked", "reserved", "we chose", "we picked", "we decided", "going with", "we selected"]):
587
+ # We could parse which venue, but for now we just proceed to invitation creation
588
+ if not party_context.selected_theme_name:
589
+ return (
590
+ "Nice, sounds like you’ve booked a venue! 🎉\n"
591
+ "Now tell me which theme you’d like to use for your invitation (e.g., “Superhero Adventure”)."
592
+ )
593
+
594
+ # We have a theme + venues and event details → create invitation card
595
+ try:
596
+ _ = generate_invitation_for_theme.invoke(
597
+ {"selected_theme_name": party_context.selected_theme_name}
598
+ )
599
+ return (
600
+ "Awesome — venue locked in and theme selected! 🎉\n\n"
601
+ "I’ve created your invitation card using your date, location and theme. "
602
+ "You’ll see the image below in the UI.\n\n"
603
+ "Next, please upload your guest list CSV using the **Guest List CSV** uploader, "
604
+ "and I’ll help you send this invitation to all your guests."
605
+ )
606
+ except Exception as e:
607
+ return (
608
+ "Great, thanks for confirming your venue. I tried to create the invitation card but hit an error: "
609
+ f"{e}\nPlease try again or tell me and we’ll retry."
610
+ )
611
+
612
+ # STEP 4: after details are set, use LLM-based decision system
613
+ decision = decide_next_action(user_message, history)
614
+ action = decision.get("action")
615
+ message_to_user = (decision.get("message_to_user") or "").strip()
616
+
617
+ if action == "call_tool":
618
+ tool_name = decision.get("tool_name")
619
+ tool_args = decision.get("tool_args", {}) or {}
620
+
621
+ # 🧠 Auto-fill context for venue search tool (budget + guest_count)
622
+ if tool_name == "venue_search_agent":
623
+ if not tool_args.get("location") and party_context.event_location:
624
+ tool_args["location"] = party_context.event_location
625
+ if not tool_args.get("budget") and party_context.event_budget:
626
+ tool_args["budget"] = party_context.event_budget
627
+ if (not tool_args.get("guest_count") or tool_args.get("guest_count") == 0) and party_context.guest_emails:
628
+ tool_args["guest_count"] = len(party_context.guest_emails)
629
+
630
+ # 🎨 Handle explicit theme selection via tool call
631
+ if tool_name == "theme_planning_agent" and tool_args.get("selected_theme_name"):
632
+ party_context.selected_theme_name = tool_args.get("selected_theme_name")
633
+
634
+ # 🛍️ Pre-check for shopping: auto-fill selected_items BEFORE tool call
635
+ if tool_name == "theme_decor_favor_shop_agent":
636
+ if tool_args.get("shop_on_amazon_flag") and not tool_args.get("selected_items"):
637
+ if party_context.selected_items:
638
+ tool_args["selected_items"] = party_context.selected_items
639
+ else:
640
+ return (
641
+ "No items selected for shopping yet. "
642
+ "Pick decor/favor items in the **Decor & Favors (Shopping)** section, "
643
+ "click **Save selected items**, then ask me to build your cart."
644
+ )
645
+
646
+ tool_fn = TOOL_ROUTER.get(tool_name)
647
+ if tool_fn is None:
648
+ return (
649
+ message_to_user
650
+ or "I tried to pick the right helper agent, but something went wrong. Could you rephrase what you want?"
651
+ )
652
+
653
+ # COMMENTED OUT: agent trace logging
654
+ # # NEW: log tool call BEFORE calling it (for ALL tools)
655
+ # try:
656
+ # party_context.reasoning_log.append(
657
+ # f"Calling tool: {tool_name} with args: {json.dumps(tool_args, ensure_ascii=False)}"
658
+ # )
659
+ # if len(party_context.reasoning_log) > 30:
660
+ # party_context.reasoning_log = party_context.reasoning_log[-30:]
661
+ # except Exception:
662
+ # pass
663
+
664
+ try:
665
+ tool_result = tool_fn(**tool_args)
666
+
667
+ # COMMENTED OUT: agent trace logging
668
+ # # NEW: log tool result (truncated)
669
+ # try:
670
+ # result_str = str(tool_result)[:200]
671
+ # party_context.reasoning_log.append(f"Tool result (truncated): {result_str}...")
672
+ # if len(party_context.reasoning_log) > 30:
673
+ # party_context.reasoning_log = party_context.reasoning_log[-30:]
674
+ # except Exception:
675
+ # pass
676
+
677
+ # 🏨 For venue search, show the tool result + instructions to book & then tell us
678
+ if tool_name == "venue_search_agent":
679
+ hint = (
680
+ message_to_user
681
+ or "Here are some venue ideas I found that match your location, budget, and guest count:"
682
+ )
683
+ followup = (
684
+ "\n\n🔔 Please review these options, check availability, and reserve your favorite venue "
685
+ "on their websites or Yelp pages.\n"
686
+ "Once you’ve booked one, tell me which venue you chose "
687
+ "(e.g., “We booked #2” or “We chose Bella Gardens”), and I’ll create your invitation card."
688
+ )
689
+ return f"{hint}\n\n{tool_result}{followup}"
690
+
691
+ # 📧 For guest invites, distinguish between loading CSV vs sending emails
692
+ if tool_name == "guest_invite_email_agent":
693
+ sent_flag = bool(tool_args.get("send_emails"))
694
+ if sent_flag:
695
+ party_context.emails_sent = True
696
+ guest_n = len(party_context.guest_emails)
697
+ base_msg = (
698
+ f"Done! I’ve sent your invitation email to **{guest_n} guests** 🎉\n"
699
+ )
700
+ next_hint = (
701
+ "\n Next, I can help you plan decor and party favors and build an Amazon cart for you. "
702
+ "Just say something like *“Help me with decor and favors”*."
703
+ )
704
+ return base_msg + next_hint
705
+ else:
706
+ base_msg = tool_result or "I’ve loaded your guest list."
707
+ next_hint = (
708
+ "\n\nWhen you’re ready, tell me something like *“Send the invitations”* "
709
+ "and I’ll email your guests using the invitation card we created."
710
+ )
711
+ return base_msg + next_hint
712
+
713
+ # 🛍️ Decor & favor shopping
714
+ if tool_name == "theme_decor_favor_shop_agent":
715
+ # Note: tool_result is already computed above, selected_items already auto-filled if needed
716
+ if tool_args.get("shop_on_amazon_flag"):
717
+ # Cart was built; cart_items + order_status set in party_context
718
+ base_msg = (
719
+ "I've built an Amazon cart based on your saved decor & favor selections. 🛒\n"
720
+ "You should now see your cart updated in the **Cart Preview** panel on the right."
721
+ )
722
+ if tool_result:
723
+ # Append MCP-derived details (product titles, links, etc.)
724
+ return base_msg + "\n\n" + str(tool_result)
725
+ return base_msg
726
+ else:
727
+ # Just created shopping list, no cart yet
728
+ base_msg = (
729
+ "I've created a shopping list of decor and party favors for your theme. 🎉\n"
730
+ "Use the checkboxes under **Decor & Favors (Shopping)** to pick what you like, "
731
+ "then click **Save selected items**, and ask me when you're ready to build your Amazon cart."
732
+ )
733
+ return base_msg
734
+
735
+ # 🎨 For theme_planning_agent used only for invitation generation (fallback)
736
+ if tool_name == "theme_planning_agent" and tool_args.get("selected_theme_name"):
737
+ if party_context.invitation_text:
738
+ return (
739
+ "Your invitation card is ready! 🎉\n\n"
740
+ "You can see the image below. Next, please upload your guest list CSV using the "
741
+ "**Guest List CSV** uploader, and I’ll help you send this card to all your guests."
742
+ )
743
+
744
+ # Default for other tools
745
+ return message_to_user or "Done! I’ve updated your party plan based on that step."
746
+
747
+ except Exception as e:
748
+ return f"Something went wrong while calling my helper agent ({tool_name}): {e}"
749
+
750
+ # STEP 5: no tool call → just respond, plus proactive hints
751
+ if not message_to_user:
752
+ message_to_user = "How would you like to continue planning your party?"
753
+
754
+ hints = []
755
+
756
+ # Invitation exists but no guests loaded yet
757
+ if party_context.invitation_text and not party_context.guest_emails:
758
+ hints.append(
759
+ "📧 Your invitation card is ready! When you’re ready, upload your guest list CSV using the "
760
+ "**Guest List CSV** upload below and I’ll help you send the invitations."
761
+ )
762
+
763
+ # Guests loaded but emails not sent
764
+ if party_context.guest_emails and not party_context.emails_sent:
765
+ hints.append(
766
+ f"📧 I see we have **{len(party_context.guest_emails)} guests** in your list. "
767
+ "If you’d like me to email them the invitation, just say something like "
768
+ "*“Send the invitations”* or *“Go ahead and email everyone.”*"
769
+ )
770
+
771
+ # Emails sent, but decor not planned
772
+ if party_context.emails_sent and not party_context.decor_items:
773
+ hints.append(
774
+ "🛍️ Invitations are taken care of! Next, I can help you plan decor & party favors and build an Amazon cart. "
775
+ "Try saying *“Help me with decor and favors”*."
776
+ )
777
+
778
+ # Decor items exist, but no cart yet
779
+ if party_context.decor_items and not getattr(party_context, "cart_items", []):
780
+ hints.append(
781
+ "🛍️ I’ve generated decor and favor ideas. Use the checkboxes under **Decor & Favors (Shopping)** "
782
+ "to pick your items, then tell me when you’re ready to build your cart."
783
+ )
784
+
785
+ if hints:
786
+ message_to_user = message_to_user + "\n\n" + "\n\n".join(hints)
787
+
788
+ return message_to_user
789
+
790
+
791
+ __all__ = ["orchestrator_chat", "orchestrator_tools"]
src/agents/theme_decor_favor_shop_agent.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Subagent 4: Theme Decor/Favor Shop Agent -
2
+ 1) Generates theme-based decor & favor shopping lists (LLM)
3
+ 2) Builds an Amazon cart via MCP (demo mode, no real orders)
4
+ """
5
+ import json
6
+ from typing import List, Optional, Dict, Any
7
+ from langchain_core.tools import tool
8
+ from langchain_core.messages import HumanMessage
9
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
10
+
11
+ from src.config import TEXT_MODEL
12
+ from src.party_context import party_context, ShoppingItem
13
+ from src.mcp.mcp_clients import amazon_mcp
14
+
15
+
16
+ # ==========================================================
17
+ # LLM for generating theme-based shopping list
18
+ # ==========================================================
19
+
20
+ def get_text_llm() -> ChatHuggingFace:
21
+ """Get the text generation LLM used for decor/favor suggestions."""
22
+ llm = HuggingFaceEndpoint(
23
+ repo_id=TEXT_MODEL,
24
+ task="text-generation",
25
+ max_new_tokens=512,
26
+ temperature=0.7,
27
+ do_sample=True,
28
+ )
29
+ return ChatHuggingFace(llm=llm)
30
+
31
+
32
+ text_llm = get_text_llm()
33
+
34
+
35
+ @tool
36
+ def generate_shopping_list_for_theme(theme_name: Optional[str] = None) -> str:
37
+ """
38
+ Generate a list of decor items and party favors based on the selected party theme.
39
+
40
+ Args:
41
+ theme_name: The selected theme name (optional, uses party_context if not provided)
42
+
43
+ Returns:
44
+ JSON string with shopping list items, and also updates:
45
+ - party_context.decor_items
46
+ - party_context.favor_items
47
+ """
48
+ theme = theme_name or party_context.selected_theme_name
49
+ if not theme:
50
+ return json.dumps({"error": "No theme selected. Please select a theme first."})
51
+
52
+ party_description = party_context.party_description or ""
53
+
54
+ prompt = f"""You are a party planning expert. Based on the party theme "{theme}" and party description "{party_description}", create a comprehensive shopping list of decor items and age-appropriate party favors.
55
+
56
+ Generate:
57
+ - 5-8 decor items (decorations, banners, balloons, table settings, etc.)
58
+ - 2-3 common decor items that are likely to be useful for any party (e.g., tablecloths, plates, cups, napkins, etc.)
59
+ - 5-8 party favor items (small gifts, toys, treats, etc.)
60
+ - 2-3 common party favor items that are likely to be useful for any party (e.g., gift bags, party favors, etc.)
61
+
62
+ For each item, provide:
63
+ - A clear, specific name that would work well as an Amazon search query.
64
+ - Include the theme and type of item, e.g. "graduation party banner decorations", "princess birthday party favor bags".
65
+ - Avoid vague or cute names like "Magical Unicorn Table of Dreams".
66
+ - Category: "decor" or "favor"
67
+ - A brief description (1 sentence)
68
+
69
+ Return ONLY valid JSON in this exact format:
70
+ {{
71
+ "decor_items": [
72
+ {{"name": "Item Name", "category": "decor", "description": "Brief description"}},
73
+ ...
74
+ ],
75
+ "favor_items": [
76
+ {{"name": "Item Name", "category": "favor", "description": "Brief description"}},
77
+ ...
78
+ ]
79
+ }}
80
+
81
+ Make items specific, age-appropriate, and theme-relevant!"""
82
+
83
+ try:
84
+ response = text_llm.invoke([HumanMessage(content=prompt)])
85
+ content = response.content.strip()
86
+
87
+ # Try to extract JSON robustly
88
+ try:
89
+ parsed = json.loads(content)
90
+ except json.JSONDecodeError:
91
+ start = content.find("{")
92
+ end = content.rfind("}") + 1
93
+ if start >= 0 and end > start:
94
+ json_str = content[start:end]
95
+ parsed = json.loads(json_str)
96
+ else:
97
+ raise ValueError("Could not parse JSON from response")
98
+
99
+ # Create ShoppingItem objects
100
+ decor_items: List[ShoppingItem] = []
101
+ favor_items: List[ShoppingItem] = []
102
+
103
+ for item_data in parsed.get("decor_items", []):
104
+ item = ShoppingItem(
105
+ name=item_data.get("name", ""),
106
+ category="decor",
107
+ description=item_data.get("description", "")
108
+ )
109
+ decor_items.append(item)
110
+
111
+ for item_data in parsed.get("favor_items", []):
112
+ item = ShoppingItem(
113
+ name=item_data.get("name", ""),
114
+ category="favor",
115
+ description=item_data.get("description", "")
116
+ )
117
+ favor_items.append(item)
118
+
119
+ # Update party context so the UI can show checkboxes
120
+ party_context.decor_items = decor_items
121
+ party_context.favor_items = favor_items
122
+
123
+ result = {
124
+ "theme": theme,
125
+ "decor_items": [
126
+ {"name": item.name, "description": item.description}
127
+ for item in decor_items
128
+ ],
129
+ "favor_items": [
130
+ {"name": item.name, "description": item.description}
131
+ for item in favor_items
132
+ ],
133
+ "total_items": len(decor_items) + len(favor_items),
134
+ }
135
+
136
+ return json.dumps(result, indent=2, ensure_ascii=False)
137
+
138
+ except Exception as e:
139
+ error_msg = f"Error generating shopping list: {str(e)}"
140
+ print("[generate_shopping_list_for_theme] ERROR:", error_msg)
141
+
142
+ # Fallback items so the UI still has something to show
143
+ fallback_decor = [
144
+ {"name": f"{theme} Theme Banner", "description": "Colorful banner matching the theme"},
145
+ {"name": f"{theme} Balloons", "description": "Theme-colored balloons"},
146
+ {"name": "Table Decorations", "description": "Table centerpieces and decorations"},
147
+ ]
148
+ fallback_favor = [
149
+ {"name": "Party Favor Bags", "description": "Small gift bags for guests"},
150
+ {"name": "Theme Stickers", "description": "Fun stickers matching the theme"},
151
+ ]
152
+
153
+ decor_items = [
154
+ ShoppingItem(name=item["name"], category="decor", description=item["description"])
155
+ for item in fallback_decor
156
+ ]
157
+ favor_items = [
158
+ ShoppingItem(name=item["name"], category="favor", description=item["description"])
159
+ for item in fallback_favor
160
+ ]
161
+
162
+ party_context.decor_items = decor_items
163
+ party_context.favor_items = favor_items
164
+
165
+ return json.dumps(
166
+ {
167
+ "theme": theme,
168
+ "decor_items": fallback_decor,
169
+ "favor_items": fallback_favor,
170
+ "total_items": len(fallback_decor) + len(fallback_favor),
171
+ "note": "Generated fallback list due to error",
172
+ },
173
+ indent=2,
174
+ )
175
+
176
+ def build_amazon_query_from_item_name(item_name: str) -> str:
177
+ """
178
+ Turn a human-friendly item name into a cleaner Amazon search query.
179
+ - Strip any long descriptions after " - " or " – "
180
+ - Force lowercase
181
+ - Optionally append 'party' if it looks like decor/favors without that word
182
+ """
183
+ # Split off descriptions like "Graduation Banner – black and gold"
184
+ for sep in [" – ", " - "]:
185
+ if sep in item_name:
186
+ item_name = item_name.split(sep, 1)[0]
187
+
188
+ q = item_name.strip().lower()
189
+
190
+ # If it doesn't already mention 'party' but clearly looks like decor/favors,
191
+ # lightly bias toward party supplies.
192
+ keywords = ["banner", "balloons", "tablecloth", "table runner", "favor", "goodie bag", "cups", "plates"]
193
+ if any(k in q for k in keywords) and "party" not in q:
194
+ q = q + " party"
195
+
196
+ return q
197
+
198
+ # ==========================================================
199
+ # Amazon MCP cart builder (demo mode, no real checkout)
200
+ # ==========================================================
201
+
202
+ @tool
203
+ def shop_on_amazon(
204
+ selected_items: List[str],
205
+ fewsats_api_key: Optional[str] = None,
206
+ shipping_address: Optional[Dict[str, Any]] = None,
207
+ user_info: Optional[Dict[str, Any]] = None,
208
+ ) -> str:
209
+ """
210
+ Build an Amazon 'cart' for the selected items using the Amazon MCP server.
211
+
212
+ ⚠️ Demo / hackathon mode:
213
+ - We DO NOT place real orders.
214
+ - FEWSATS key, shipping_address, and user_info are accepted but not required.
215
+ - We only search products and prepare a cart summary with Amazon links.
216
+
217
+ Args:
218
+ selected_items: List of item names the user selected (decor / favors).
219
+ fewsats_api_key: Optional Fewsats API key (kept for future extension).
220
+ shipping_address: Optional shipping address.
221
+ user_info: Optional user info.
222
+
223
+ Returns:
224
+ A human-friendly summary of the cart and also updates:
225
+ - party_context.cart_items
226
+ - party_context.order_status
227
+ """
228
+ if not selected_items:
229
+ return (
230
+ "No items selected for shopping. Please choose some decor or favors first "
231
+ "in the **Decor & Favors (Shopping)** section and click **Save selected items**."
232
+ )
233
+
234
+ cart_items: List[Dict[str, Any]] = []
235
+ failed_items: List[str] = []
236
+
237
+ for item_name in selected_items:
238
+ try:
239
+ # NOTE: amazon_mcp.search_product is a **sync** wrapper that internally
240
+ # uses asyncio.run(...) to talk to the async MCP manager.
241
+ #search_result = amazon_mcp.search_product(item_name)
242
+ query = build_amazon_query_from_item_name(item_name)
243
+ search_result = amazon_mcp.search_product(query)
244
+
245
+
246
+ # Be defensive about schema differences
247
+ products = (
248
+ search_result.get("products")
249
+ or search_result.get("items")
250
+ or search_result.get("data") # in case server returns {"data": [...]}
251
+ or []
252
+ )
253
+
254
+ if not products:
255
+ failed_items.append(f"{item_name} (no products found)")
256
+ continue
257
+
258
+ # Take the first product as a simple heuristic
259
+ product = products[0]
260
+
261
+ title = (
262
+ product.get("title")
263
+ or product.get("name")
264
+ or item_name
265
+ )
266
+ url = (
267
+ product.get("url")
268
+ or product.get("productUrl")
269
+ or product.get("link")
270
+ or ""
271
+ )
272
+
273
+ # Try to extract a price if present (schema-agnostic)
274
+ price_value = None
275
+ currency = None
276
+
277
+ price_info = product.get("price")
278
+ if isinstance(price_info, (int, float)):
279
+ price_value = float(price_info)
280
+ elif isinstance(price_info, dict):
281
+ # Common patterns: {"amount": 12.99, "currency": "USD"} or {"value": 12.99}
282
+ price_value = (
283
+ price_info.get("amount")
284
+ or price_info.get("value")
285
+ )
286
+ currency = (
287
+ price_info.get("currency")
288
+ or price_info.get("currency_code")
289
+ )
290
+
291
+ cart_items.append(
292
+ {
293
+ "requested_name": item_name,
294
+ "title": title,
295
+ "url": url,
296
+ "price": price_value,
297
+ "currency": currency,
298
+ }
299
+ )
300
+ except Exception as e:
301
+ failed_items.append(f"{item_name} (error during search: {str(e)})")
302
+
303
+ # Store cart in shared context for the UI to render
304
+ party_context.order_status = "cart_ready"
305
+ # type: ignore[attr-defined] in case cart_items wasn't in PartyContext dataclass
306
+ party_context.cart_items = cart_items # type: ignore[attr-defined]
307
+
308
+ # Compute a rough total (only where we have numeric prices)
309
+ numeric_prices = [
310
+ c["price"] for c in cart_items
311
+ if isinstance(c.get("price"), (int, float))
312
+ ]
313
+ total_estimate = sum(numeric_prices) if numeric_prices else None
314
+ currency = None
315
+ for c in cart_items:
316
+ if c.get("currency"):
317
+ currency = c["currency"]
318
+ break
319
+
320
+ # Build human-friendly message
321
+ msg_lines: List[str] = []
322
+ msg_lines.append("🛒 I’ve built an Amazon cart for your selected items.\n")
323
+
324
+ if cart_items:
325
+ msg_lines.append("**Cart items (click links to review on Amazon):**")
326
+ for c in cart_items:
327
+ line = f"• **{c['title']}**"
328
+ if c.get("price") is not None:
329
+ if currency:
330
+ line += f" – ~{c['price']} {currency}"
331
+ else:
332
+ line += f" – ~{c['price']}"
333
+ if c.get("url"):
334
+ line += f"\n {c['url']}"
335
+ msg_lines.append(line)
336
+ else:
337
+ msg_lines.append("I couldn’t find any matching products for your selections.")
338
+
339
+ if total_estimate is not None:
340
+ total_str = f"{total_estimate:.2f}"
341
+ if currency:
342
+ msg_lines.append(
343
+ f"\n**Estimated total (before tax & shipping):** ~{total_str} {currency}"
344
+ )
345
+ else:
346
+ msg_lines.append(
347
+ f"\n**Estimated total (before tax & shipping):** ~{total_str}"
348
+ )
349
+
350
+ if failed_items:
351
+ msg_lines.append("\nSome items could not be matched:")
352
+ for fi in failed_items:
353
+ msg_lines.append(f"• {fi}")
354
+
355
+ msg_lines.append(
356
+ "\n💡 *No real orders were placed.* This is a demo cart builder. "
357
+ "You can open the Amazon links above to review and complete checkout manually."
358
+ )
359
+
360
+ # Mention FEWSATS if user provided it, but clarify no payment was done
361
+ if fewsats_api_key:
362
+ msg_lines.append(
363
+ "\n🔐 You provided a FEWSATS API key. In a future version, I can use it to "
364
+ "automate payment and order placement, but in this demo I only prepare the cart."
365
+ )
366
+
367
+ # Hint for UI: cart_preview panel should now show items from party_context.cart_items
368
+ msg_lines.append(
369
+ "\n🛒 You should now see your cart updated in the **Cart Preview** panel on the right."
370
+ )
371
+
372
+ return "\n".join(msg_lines)
373
+
374
+
375
+ __all__ = [
376
+ "generate_shopping_list_for_theme",
377
+ "build_amazon_query_from_item_name",
378
+ "shop_on_amazon",
379
+ ]
src/agents/theme_planning_agent.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Subagent 1: Theme Planning Agent - Generates themes and invitation cards."""
2
+ import os
3
+ import json
4
+ from typing import Optional, Dict, Any
5
+ from huggingface_hub import InferenceClient
6
+ from langchain_core.tools import tool
7
+ from langchain_core.messages import HumanMessage
8
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
9
+
10
+ from src.config import HF_TOKEN, TEXT_MODEL, IMAGE_MODEL, INVITATION_IMAGE_DIR
11
+ from src.party_context import party_context, ThemeOption
12
+
13
+
14
+ def get_text_llm() -> ChatHuggingFace:
15
+ """Get the text generation LLM."""
16
+ llm = HuggingFaceEndpoint(
17
+ repo_id=TEXT_MODEL,
18
+ task="text-generation",
19
+ max_new_tokens=512,
20
+ temperature=0.7,
21
+ do_sample=True,
22
+ timeout=30, # 30 second timeout
23
+ )
24
+ return ChatHuggingFace(llm=llm)
25
+
26
+
27
+ def get_image_client() -> InferenceClient:
28
+ """Get the image generation client."""
29
+ if not HF_TOKEN:
30
+ raise ValueError("HF_TOKEN or HUGGINGFACEHUB_API_TOKEN must be set")
31
+ return InferenceClient(provider="auto", api_key=HF_TOKEN)
32
+
33
+
34
+ # Initialize clients
35
+ text_llm = get_text_llm()
36
+ image_client = get_image_client()
37
+
38
+
39
+ @tool
40
+ def generate_party_themes(party_description: str) -> str:
41
+ """
42
+ Generate 3 creative party theme options based on the party description.
43
+
44
+ Args:
45
+ party_description: User's party description
46
+ (e.g., "I want to plan a birthday party for my 5 year old kid")
47
+
48
+ Returns:
49
+ JSON string with 3 theme options in the format:
50
+ {
51
+ "themes": [
52
+ {"id": 1, "name": "...", "description": "..."},
53
+ {"id": 2, "name": "...", "description": "..."},
54
+ {"id": 3, "name": "...", "description": "..."}
55
+ ]
56
+ }
57
+ """
58
+ prompt = f"""You are a creative party planning assistant. Based on the following party description, propose EXACTLY 3 age-appropriate party themes that would be perfect for this event.
59
+
60
+ Party description: {party_description}
61
+
62
+ For each theme, provide:
63
+ - A catchy, fun name
64
+ - A brief description (2-3 sentences) explaining why this theme is suitable and what makes it special
65
+
66
+ Return ONLY valid JSON in this exact format:
67
+ {{
68
+ "themes": [
69
+ {{"id": 1, "name": "Theme Name 1", "description": "Description of theme 1"}},
70
+ {{"id": 2, "name": "Theme Name 2", "description": "Description of theme 2"}},
71
+ {{"id": 3, "name": "Theme Name 3", "description": "Description of theme 3"}}
72
+ ]
73
+ }}
74
+ """
75
+
76
+ # Always record the description in shared context
77
+ party_context.party_description = party_description
78
+
79
+ try:
80
+ response = text_llm.invoke([HumanMessage(content=prompt)])
81
+ content = response.content.strip()
82
+
83
+ # Try direct JSON parse
84
+ try:
85
+ parsed = json.loads(content)
86
+ except json.JSONDecodeError:
87
+ # Try to extract JSON substring
88
+ start = content.find("{")
89
+ end = content.rfind("}") + 1
90
+ if start >= 0 and end > start:
91
+ json_str = content[start:end]
92
+ parsed = json.loads(json_str)
93
+ else:
94
+ raise ValueError("Could not parse JSON from response")
95
+
96
+ # Build ThemeOption objects
97
+ themes = []
98
+ for idx, theme_data in enumerate(parsed.get("themes", []), start=1):
99
+ theme = ThemeOption(
100
+ id=theme_data.get("id", idx),
101
+ name=theme_data.get("name", f"Theme {idx}"),
102
+ description=theme_data.get("description", "")
103
+ )
104
+ themes.append(theme)
105
+
106
+ if not themes:
107
+ raise ValueError("No themes found in LLM response")
108
+
109
+ # ✅ Update party context so UI/orchestrator can see the options
110
+ party_context.theme_options = themes
111
+
112
+ return json.dumps(parsed, indent=2, ensure_ascii=False)
113
+
114
+ except Exception as e:
115
+ # Fallback themes if LLM output is bad
116
+ error_msg = f"Error generating themes: {str(e)}"
117
+ print("[generate_party_themes] ", error_msg)
118
+
119
+ fallback_themes = [
120
+ {
121
+ "id": 1,
122
+ "name": "Superhero Adventure",
123
+ "description": "A heroic party with capes, masks, and superpowers!",
124
+ },
125
+ {
126
+ "id": 2,
127
+ "name": "Princess & Knights",
128
+ "description": "A magical royal celebration with crowns and castles!",
129
+ },
130
+ {
131
+ "id": 3,
132
+ "name": "Space Explorer",
133
+ "description": "Blast off into space with rockets, stars, and planets!",
134
+ },
135
+ ]
136
+
137
+ themes = [ThemeOption(**t) for t in fallback_themes]
138
+ party_context.theme_options = themes
139
+
140
+ return json.dumps({"themes": fallback_themes}, indent=2, ensure_ascii=False)
141
+
142
+ @tool
143
+ def generate_invitation_for_theme(selected_theme_name: str) -> str:
144
+ """
145
+ Generate invitation text and 4x4 invitation card image for the selected theme.
146
+
147
+ Args:
148
+ selected_theme_name: The name of the selected theme
149
+
150
+ Returns:
151
+ JSON string with invitation details
152
+ """
153
+ if not party_context.party_description:
154
+ return json.dumps({"error": "Party description not set. Please generate themes first."})
155
+
156
+ party_context.selected_theme_name = selected_theme_name
157
+
158
+ # Generate invitation text informed by structured event details
159
+ event_date = party_context.event_date or "[Date to be announced]"
160
+ event_location = party_context.event_location or "[Location to be announced]"
161
+ budget = party_context.event_budget or "[Budget not specified]"
162
+
163
+ invitation_prompt = f"""You are writing a warm, fun invitation for the selected theme party.
164
+
165
+ Theme: "{selected_theme_name}"
166
+
167
+ Party description:
168
+ {party_context.party_description}
169
+
170
+ Event details (work these naturally into the copy):
171
+ - Date: {event_date}
172
+ - Location: {event_location}
173
+ - Budget context: {budget} (use only to set the tone; do NOT mention the exact budget)
174
+
175
+ The invitation should:
176
+ - Be 3–5 short sentences and age-appropriate for the selected theme.
177
+ - Invitatopn card should have embedded welcome message for the guest.
178
+ - Be warm and welcoming
179
+ - Use the actual date and location values above (no placeholders for those two)
180
+ - Include placeholders only for time and RSVP deadline (e.g., "Time: [To be announced]", "Please RSVP by [date]")
181
+ - Match the energy of the theme
182
+
183
+ Write the invitation text now:"""
184
+
185
+ try:
186
+ text_response = text_llm.invoke([HumanMessage(content=invitation_prompt)])
187
+ invitation_text = text_response.content.strip()
188
+
189
+ # Generate invitation card image with all details embedded
190
+ event_date = party_context.event_date or "[Date to be announced]"
191
+ event_location = party_context.event_location or "[Location to be announced]"
192
+ occasion = party_context.party_description or "Special Celebration"
193
+
194
+ image_prompt = f"""Create a beautiful 4x4 inch square invitation card for the party according to the selected party theme. It should be age-appropriate.
195
+
196
+ REQUIREMENTS - ALL TEXT MUST BE VISIBLE AND READABLE IN THE IMAGE:
197
+ 1. Theme: {selected_theme_name} - incorporate this theme visually throughout
198
+ 2. Event Date: {event_date} - display this prominently on the card
199
+ 3. Location: {event_location} - include this clearly visible
200
+ 4. Occasion: {occasion} - reflect this in the design
201
+ 5. Invitation Text: {invitation_text} - embed this text clearly readable on the card
202
+
203
+ The card should have:
204
+ - Make sure the full text above is actually written on the card (not just implied), keep the layout clean and readable.
205
+ Center the text and leave some breathing room.
206
+ - A bright, colorful, and fun background that matches the {selected_theme_name} theme
207
+ - Invitation event and theme based message embeded inside the picture Beautiful, age-appropriate illustrations related to the theme-
208
+ - ALL TEXT (date, location, occasion, invitation message) clearly visible and readable and in English
209
+ - Professional, polished design suitable for printing
210
+ - Happy, festive atmosphere
211
+ - Text should be in English language only and large enough to read when printed
212
+
213
+ Style: Colorful, fun, kid-friendly, professional invitation card design with all text embedded and clearly visible."""
214
+
215
+ # Generate image using FLUX.1-dev
216
+ img = image_client.text_to_image(
217
+ image_prompt,
218
+ model=IMAGE_MODEL
219
+ )
220
+
221
+ # Save image
222
+ image_filename = f"invitation_{selected_theme_name.replace(' ', '_').lower()}.png"
223
+ image_path = os.path.join(INVITATION_IMAGE_DIR, image_filename)
224
+ os.makedirs(INVITATION_IMAGE_DIR, exist_ok=True)
225
+
226
+ # InferenceClient.text_to_image returns a PIL Image
227
+ if hasattr(img, 'save'):
228
+ img.save(image_path)
229
+ else:
230
+ # Fallback: try to save as bytes or convert
231
+ from PIL import Image
232
+ import io
233
+ if isinstance(img, bytes):
234
+ with open(image_path, 'wb') as f:
235
+ f.write(img)
236
+ else:
237
+ # Try to open as image
238
+ img_pil = Image.open(io.BytesIO(img)) if hasattr(img, '__bytes__') else Image.fromarray(img)
239
+ img_pil.save(image_path)
240
+
241
+ # Update party context
242
+ party_context.invitation_text = invitation_text
243
+ party_context.invitation_image_path = image_path
244
+
245
+ result = {
246
+ "selected_theme": selected_theme_name,
247
+ "invitation_text": invitation_text,
248
+ "image_path": image_path,
249
+ "status": "success"
250
+ }
251
+
252
+ return json.dumps(result, indent=2, ensure_ascii=False)
253
+
254
+ except Exception as e:
255
+ error_msg = f"Error generating invitation: {str(e)}"
256
+ return json.dumps({
257
+ "error": error_msg,
258
+ "selected_theme": selected_theme_name,
259
+ "status": "error"
260
+ })
261
+
262
+
263
+ def theme_planning_entry(party_description: Optional[str] = None, selected_theme_name: Optional[str] = None) -> str:
264
+ """
265
+ Main entry point for theme planning agent.
266
+
267
+ Args:
268
+ party_description: User's party description (for generating themes)
269
+ selected_theme_name: Selected theme name (for generating invitation)
270
+
271
+ Returns:
272
+ Result string
273
+ """
274
+ if selected_theme_name:
275
+ # Generate invitation for selected theme
276
+ return generate_invitation_for_theme.invoke({"selected_theme_name": selected_theme_name})
277
+ elif party_description:
278
+ # Generate theme options
279
+ return generate_party_themes.invoke({"party_description": party_description})
280
+ else:
281
+ return json.dumps({"error": "Either party_description or selected_theme_name must be provided"})
282
+
283
+
284
+ # Export tools for orchestrator
285
+ __all__ = ['generate_party_themes', 'generate_invitation_for_theme', 'theme_planning_entry']
286
+
src/agents/venue_search_agent.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Subagent 3: Venue Search Agent - Uses Bright Data MCP SERP to find party venues."""
2
+
3
+ from typing import Optional, List, Dict, Any
4
+ from langchain_core.tools import tool
5
+
6
+ from src.party_context import party_context, VenueResult
7
+ from src.mcp.mcp_clients import brightdata_mcp
8
+
9
+ import asyncio
10
+
11
+
12
+ def _build_query(location: str, query: str, budget: Optional[str], guest_count: Optional[int]) -> str:
13
+ """
14
+ Build a natural-language query string for SERP based on party info.
15
+ """
16
+ base = query or "party venue"
17
+ parts = [base]
18
+
19
+ if guest_count and guest_count > 0:
20
+ parts.append(f"for about {guest_count} guests")
21
+
22
+ if budget:
23
+ parts.append(f"under {budget}")
24
+
25
+ parts.append(f"in {location}")
26
+ return " ".join(parts)
27
+
28
+
29
+ def _format_serp_results(
30
+ location: str,
31
+ venues: List[VenueResult],
32
+ original_query: str,
33
+ ) -> str:
34
+ """
35
+ Create a natural-language, nicely formatted summary of found venues.
36
+ """
37
+ if not venues:
38
+ return (
39
+ f"I tried searching for venues in **{location}** for:\n"
40
+ f"> {original_query}\n\n"
41
+ "but I couldn’t find anything reliable from the search results.\n"
42
+ "You might want to try a broader location or a higher budget."
43
+ )
44
+
45
+ lines = []
46
+ lines.append(
47
+ f"Here are some venue options I found in **{location}** that could work for your party:\n"
48
+ )
49
+
50
+ for i, v in enumerate(venues, start=1):
51
+ entry = [f"{i}. **{v.name}**"]
52
+
53
+ # Small natural-language description
54
+ if v.description:
55
+ entry.append(f" – {v.description}")
56
+
57
+ if v.address:
58
+ entry.append(f" • 📍 Address: {v.address}")
59
+
60
+ if v.rating is not None:
61
+ entry.append(f" • ⭐ Approx. rating: {v.rating}/5")
62
+
63
+ if v.price_level:
64
+ entry.append(f" • 💰 Price indication: {v.price_level}")
65
+
66
+ if v.phone:
67
+ entry.append(f" • 📞 Contact: {v.phone}")
68
+
69
+ if v.url:
70
+ entry.append(f" • 🔗 More details & booking: {v.url}")
71
+
72
+ lines.append("\n".join(entry))
73
+
74
+ lines.append(
75
+ "\n💡 *Tip*: Use the links above to check exact pricing, availability, and to confirm capacity for your guest count."
76
+ )
77
+
78
+ return "\n\n".join(lines)
79
+
80
+
81
+ @tool
82
+ def search_party_venues(
83
+ location: str,
84
+ query: str = "party venue",
85
+ max_results: int = 5,
86
+ budget: str = "",
87
+ guest_count: int = 0,
88
+ ) -> str:
89
+ """
90
+ Search for party venues using Bright Data MCP SERP.
91
+
92
+ Args:
93
+ location: City and state (e.g., "Sunnyvale, CA")
94
+ query: Base search query (default: "party venue")
95
+ max_results: Maximum number of results to return
96
+ budget: Optional budget description (e.g., "$700", "under $1000")
97
+ guest_count: Optional approximate guest count (e.g., 60)
98
+
99
+ Returns:
100
+ A natural-language, nicely formatted string describing the venues.
101
+ """
102
+ # Remember location in shared context
103
+ party_context.search_location = location
104
+
105
+ # Build NL query for SERP
106
+ full_query = _build_query(location, query, budget, guest_count)
107
+
108
+ try:
109
+ # Call Bright Data MCP SERP asynchronously
110
+ serp_response = asyncio.run(
111
+ brightdata_mcp.serp_search(
112
+ query=full_query,
113
+ search_engine="google",
114
+ country="us",
115
+ language="en",
116
+ results_count=max_results,
117
+ parse_results=True,
118
+ )
119
+ )
120
+
121
+ # Handle CallToolResult object - extract the actual data
122
+ if not isinstance(serp_response, dict):
123
+ # If it's a CallToolResult or similar object, try to extract content
124
+ if hasattr(serp_response, 'content'):
125
+ if isinstance(serp_response.content, list) and len(serp_response.content) > 0:
126
+ import json
127
+ first_content = serp_response.content[0]
128
+ if hasattr(first_content, 'text'):
129
+ try:
130
+ serp_response = json.loads(first_content.text)
131
+ except (json.JSONDecodeError, AttributeError):
132
+ serp_response = {"text": first_content.text}
133
+ elif hasattr(first_content, 'data'):
134
+ serp_response = first_content.data
135
+ else:
136
+ serp_response = serp_response.content if serp_response.content else {}
137
+ elif hasattr(serp_response, '__dict__'):
138
+ serp_response = serp_response.__dict__
139
+ else:
140
+ # Fallback: try to convert to dict
141
+ serp_response = dict(serp_response) if hasattr(serp_response, '__iter__') else {}
142
+
143
+ # Ensure serp_response is a dict
144
+ if not isinstance(serp_response, dict):
145
+ serp_response = {}
146
+
147
+ # The exact schema depends on Bright Data; we assume something like:
148
+ # {
149
+ # "results": [
150
+ # {
151
+ # "title": "...",
152
+ # "url": "...",
153
+ # "snippet": "...",
154
+ # "rating": ...,
155
+ # "price_level": "...",
156
+ # "address": "...",
157
+ # "phone": "..."
158
+ # },
159
+ # ...
160
+ # ]
161
+ # }
162
+ raw_results = serp_response.get("results", []) or serp_response.get("data", []) or serp_response.get("organic", [])
163
+
164
+ venues: List[VenueResult] = []
165
+ for r in raw_results[:max_results]:
166
+ title = r.get("title") or r.get("name") or "Venue"
167
+ url = r.get("url") or r.get("link") or ""
168
+ snippet = r.get("snippet") or r.get("description") or ""
169
+ rating = r.get("rating") # may be float or None
170
+ price = r.get("price_level") or r.get("price") or None
171
+ address = r.get("address") or r.get("location") or None
172
+ phone = r.get("phone") or r.get("telephone") or None
173
+
174
+ venue = VenueResult(
175
+ name=title,
176
+ url=url,
177
+ rating=float(rating) if isinstance(rating, (int, float, str)) and str(rating).replace(".", "", 1).isdigit() else None,
178
+ price_level=price,
179
+ address=address,
180
+ phone=phone,
181
+ description=snippet,
182
+ )
183
+ venues.append(venue)
184
+
185
+ # Save into shared context so other agents / UI can reuse
186
+ party_context.venue_results = venues
187
+
188
+ # Return nicely formatted result text
189
+ return _format_serp_results(location, venues, full_query)
190
+
191
+ except Exception as e:
192
+ return (
193
+ f"Sorry, I ran into an issue while searching for venues in **{location}**.\n\n"
194
+ f"Technical details: `{e}`\n\n"
195
+ "You can try asking again with a slightly different location or budget description."
196
+ )
197
+
198
+
199
+ def venue_search_entry(
200
+ location: str,
201
+ query: Optional[str] = None,
202
+ max_results: int = 5,
203
+ budget: str = "",
204
+ guest_count: int = 0,
205
+ ) -> str:
206
+ """
207
+ Main entry point for venue search agent, compatible with orchestrator wrapper.
208
+ """
209
+ search_query = query or "party venue"
210
+ return search_party_venues.invoke({
211
+ "location": location,
212
+ "query": search_query,
213
+ "max_results": max_results,
214
+ "budget": budget,
215
+ "guest_count": guest_count,
216
+ })
217
+
218
+
219
+ __all__ = ["search_party_venues", "venue_search_entry"]
src/config.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration management for the Plan-a-Party app."""
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ # Hugging Face tokens
8
+ HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
9
+ HUGGINGFACEHUB_API_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN")
10
+
11
+ # Email configuration
12
+ SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
13
+ SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
14
+ SMTP_USER = os.getenv("SMTP_USER")
15
+ SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
16
+ FROM_EMAIL = os.getenv("FROM_EMAIL")
17
+
18
+ # MCP API keys (user-provided at runtime)
19
+ FEWSATS_API_KEY = os.getenv("FEWSATS_API_KEY", "")
20
+
21
+ # Model configurations
22
+ TEXT_MODEL = os.getenv("TEXT_MODEL", "meta-llama/Llama-3.3-70B-Instruct")
23
+ IMAGE_MODEL = os.getenv("IMAGE_MODEL", "black-forest-labs/FLUX.1-dev")
24
+
25
+ # File paths
26
+ INVITATION_IMAGE_DIR = "invitations"
27
+ os.makedirs(INVITATION_IMAGE_DIR, exist_ok=True)
28
+
src/mcp/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """MCP client modules for Plan-a-Party system."""
2
+
src/mcp/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (295 Bytes). View file
 
src/mcp/__pycache__/mcp_clients.cpython-314.pyc ADDED
Binary file (14.8 kB). View file
 
src/mcp/mcp_clients.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Real MCP clients for Amazon, Fewsats, and Resend using langchain-mcp-adapters.
3
+ """
4
+
5
+ import os
6
+ import asyncio
7
+ from typing import Dict, Any, Optional
8
+
9
+ from langchain_mcp_adapters.client import MultiServerMCPClient
10
+ from langchain_mcp_adapters.tools import load_mcp_tools
11
+
12
+
13
+ # ----------------------------------------------------
14
+ # 1. CONFIGURATION FOR YOUR MCP SERVERS
15
+ # ----------------------------------------------------
16
+ # Check if amazon-mcp server exists before adding it
17
+ AMAZON_MCP_PATH = "amazon-mcp/build/index.js"
18
+ amazon_mcp_available = os.path.exists(AMAZON_MCP_PATH)
19
+
20
+ MCP_SERVERS = {
21
+ "resend": {
22
+ "url": "https://mcp.zapier.com/api/mcp/s/" + (os.getenv("ZAPIER_GMAIL_API_KEY") or "") + "/mcp",
23
+ "transport": "sse",
24
+ },
25
+ "brightdata": {
26
+ "url": "https://mcp.brightdata.com/sse?token=" + (os.getenv("BRIGHTDATA_TOKEN") or ""),
27
+ "transport": "sse",
28
+ },
29
+ }
30
+
31
+ # Only add Amazon MCP if the server file exists
32
+ if amazon_mcp_available:
33
+ MCP_SERVERS["amazon"] = {
34
+ "transport": "stdio",
35
+ "command": "node",
36
+ "args": [AMAZON_MCP_PATH],
37
+ }
38
+ else:
39
+ print(f"[WARNING] Amazon MCP server not found at {AMAZON_MCP_PATH}. Amazon shopping will be disabled.")
40
+ print(f"[INFO] To enable Amazon shopping, build the amazon-mcp server:")
41
+ print(f" cd amazon-mcp && npm install && npm run build")
42
+
43
+
44
+ # ----------------------------------------------------
45
+ # 2. MCP Session Manager
46
+ # ----------------------------------------------------
47
+ class MCPConnectionManager:
48
+ """Manages MCP sessions for all servers."""
49
+
50
+ def __init__(self):
51
+ self.client = MultiServerMCPClient(MCP_SERVERS)
52
+
53
+ async def call_tool(self, server: str, tool_name: str, params: dict):
54
+ """Call a specific tool from the given MCP server."""
55
+ async with self.client.session(server) as session:
56
+ tools = await load_mcp_tools(session)
57
+
58
+ # Find the matching tool
59
+ tool = next((t for t in tools if t.name == tool_name), None)
60
+ if not tool:
61
+ raise ValueError(f"Tool '{tool_name}' not found in MCP server '{server}'.")
62
+
63
+ result = await session.call_tool(tool_name, params)
64
+ # Extract content from CallToolResult if it's not already a dict
65
+ if hasattr(result, 'content'):
66
+ # CallToolResult.content can be a list of TextContent or other content types
67
+ if isinstance(result.content, list) and len(result.content) > 0:
68
+ # Get the first content item, which is usually the actual data
69
+ content = result.content[0]
70
+ if hasattr(content, 'text'):
71
+ # If it's TextContent, parse the JSON
72
+ import json
73
+ try:
74
+ return json.loads(content.text)
75
+ except (json.JSONDecodeError, AttributeError):
76
+ return {"text": content.text}
77
+ elif hasattr(content, 'data'):
78
+ return content.data
79
+ # If content is directly accessible
80
+ return result.content if not isinstance(result.content, list) else result.content[0] if result.content else {}
81
+ # If result is already a dict or has a dict-like interface
82
+ if hasattr(result, '__dict__'):
83
+ return result.__dict__
84
+ return result
85
+
86
+
87
+ mcp_manager = MCPConnectionManager()
88
+
89
+
90
+ # ----------------------------------------------------
91
+ # 3. AMAZON MCP CLIENT
92
+ # ----------------------------------------------------
93
+ class AmazonMCPClient:
94
+ """
95
+ Thin sync wrapper around async MCP calls to the Amazon server.
96
+
97
+ Server id: "amazon" (from your mcp.json)
98
+ Tools (from server.py):
99
+ - search
100
+ - get_payment_offers
101
+ - get_payment_offers_x402
102
+ - pay_with_x402
103
+ - get_order_by_external_id
104
+ - get_order_by_payment_token
105
+ - get_user_orders
106
+ """
107
+
108
+ def __init__(self):
109
+ self.available = amazon_mcp_available
110
+
111
+ # ---------- internal async helpers ----------
112
+
113
+ async def _search_product_async(self, query: str) -> Dict[str, Any]:
114
+ if not self.available:
115
+ return {
116
+ "error": "Amazon MCP server not available",
117
+ "message": "The amazon-mcp server has not been built. Please run: cd amazon-mcp && npm install && npm run build",
118
+ "products": []
119
+ }
120
+ try:
121
+ # server tool signature: async def search(q: str) -> Dict
122
+ return await mcp_manager.call_tool(
123
+ "amazon",
124
+ "search",
125
+ {"q": query},
126
+ )
127
+ except Exception as e:
128
+ return {
129
+ "error": str(e),
130
+ "message": f"Error calling Amazon MCP: {e}",
131
+ "products": []
132
+ }
133
+
134
+ async def _get_payment_offers_async(
135
+ self,
136
+ asin: str,
137
+ shipping_address: Dict[str, Any],
138
+ user: Dict[str, Any],
139
+ quantity: int = 1,
140
+ ) -> Dict[str, Any]:
141
+ return await mcp_manager.call_tool(
142
+ "amazon",
143
+ "get_payment_offers",
144
+ {
145
+ "asin": asin,
146
+ "shipping_address": shipping_address,
147
+ "user": user,
148
+ "quantity": quantity,
149
+ },
150
+ )
151
+
152
+ async def _pay_with_x402_async(
153
+ self,
154
+ x_payment: str,
155
+ asin: str,
156
+ shipping_address: Dict[str, Any],
157
+ user: Dict[str, Any],
158
+ quantity: int = 1,
159
+ ) -> Dict[str, Any]:
160
+ return await mcp_manager.call_tool(
161
+ "amazon",
162
+ "pay_with_x402",
163
+ {
164
+ "x_payment": x_payment,
165
+ "asin": asin,
166
+ "shipping_address": shipping_address,
167
+ "user": user,
168
+ "quantity": quantity,
169
+ },
170
+ )
171
+
172
+ # ---------- public sync wrappers ----------
173
+
174
+ def search_product(self, query: str) -> Dict[str, Any]:
175
+ """Sync wrapper around the async search call."""
176
+ import asyncio
177
+ return asyncio.run(self._search_product_async(query))
178
+
179
+ def get_payment_offers(
180
+ self,
181
+ asin: str,
182
+ shipping_address: Dict[str, Any],
183
+ user: Dict[str, Any],
184
+ quantity: int = 1,
185
+ ) -> Dict[str, Any]:
186
+ return asyncio.run(
187
+ self._get_payment_offers_async(
188
+ asin=asin,
189
+ shipping_address=shipping_address,
190
+ user=user,
191
+ quantity=quantity,
192
+ )
193
+ )
194
+
195
+ def pay_with_x402(
196
+ self,
197
+ x_payment: str,
198
+ asin: str,
199
+ shipping_address: Dict[str, Any],
200
+ user: Dict[str, Any],
201
+ quantity: int = 1,
202
+ ) -> Dict[str, Any]:
203
+ return asyncio.run(
204
+ self._pay_with_x402_async(
205
+ x_payment=x_payment,
206
+ asin=asin,
207
+ shipping_address=shipping_address,
208
+ user=user,
209
+ quantity=quantity,
210
+ )
211
+ )
212
+
213
+
214
+ # Create singleton instance
215
+ amazon_mcp = AmazonMCPClient()
216
+
217
+
218
+ # ----------------------------------------------------
219
+ # 4. FEWSATS MCP CLIENT (X402 + L402 payments)
220
+ # ----------------------------------------------------
221
+ class FewsatsMCPClient:
222
+ """Payment client for X402 & L402 flows."""
223
+
224
+ def __init__(self, api_key: Optional[str] = None):
225
+ self.api_key = api_key or os.getenv("FEWSATS_API_KEY")
226
+
227
+ async def create_x402_header(self, x402_payload: dict, chain: str = "base-sepolia"):
228
+ return await mcp_manager.call_tool(
229
+ "amazon",
230
+ "x402.createHeader",
231
+ {
232
+ "payload": x402_payload,
233
+ "chain": chain,
234
+ "apiKey": self.api_key,
235
+ }
236
+ )
237
+
238
+ async def pay_offer(self, offer_id: str, l402_offer: dict):
239
+ return await mcp_manager.call_tool(
240
+ "amazon",
241
+ "l402.payOffer",
242
+ {
243
+ "offerId": offer_id,
244
+ "l402": l402_offer,
245
+ "apiKey": self.api_key,
246
+ }
247
+ )
248
+
249
+
250
+ fewsats_mcp = FewsatsMCPClient()
251
+
252
+
253
+ # ----------------------------------------------------
254
+ # 5. RESEND EMAIL MCP CLIENT (send invitations)
255
+ # ----------------------------------------------------
256
+ class ResendMCPClient:
257
+ """Client for sending emails with attachments (invitation card)."""
258
+
259
+ async def send_email(
260
+ self,
261
+ from_email: str,
262
+ to_email: str,
263
+ subject: str,
264
+ text_body: str,
265
+ attachment_bytes: Optional[bytes] = None,
266
+ ):
267
+ payload = {
268
+ "from": from_email,
269
+ "to": to_email,
270
+ "subject": subject,
271
+ "text": text_body,
272
+ }
273
+
274
+ if attachment_bytes:
275
+ payload["attachments"] = [
276
+ {
277
+ "filename": "invitation.png",
278
+ "content": attachment_bytes.decode("latin1"), # MCP expects string
279
+ }
280
+ ]
281
+
282
+ return await mcp_manager.call_tool(
283
+ "resend",
284
+ "gmail_send_email",
285
+ payload
286
+ )
287
+
288
+
289
+ resend_mcp = ResendMCPClient()
290
+
291
+ # ----------------------------------------------------
292
+ # 6. BRIGHT DATA MCP CLIENT (SERP / web search)
293
+ # ----------------------------------------------------
294
+ class BrightDataMCPClient:
295
+ """
296
+ Client for Bright Data MCP operations (SERP / web search).
297
+ Used by the venue_search_agent to find good party venues.
298
+ """
299
+
300
+ async def serp_search(
301
+ self,
302
+ query: str,
303
+ search_engine: str = "google",
304
+ country: str = "us",
305
+ language: str = "en",
306
+ results_count: int = 5,
307
+ parse_results: bool = True,
308
+ ) -> Dict[str, Any]:
309
+ """
310
+ Call the Bright Data `search_engine` tool.
311
+
312
+ The exact output schema depends on Bright Data, but typically includes
313
+ a list of search results with title, url, snippet, etc.
314
+ """
315
+ params = {
316
+ "query": query,
317
+ "search_engine": search_engine,
318
+ "country": country,
319
+ "language": language,
320
+ "results_count": results_count,
321
+ "parse_results": parse_results,
322
+ }
323
+
324
+ # Tool name "search_engine" comes from Bright Data MCP docs.
325
+ return await mcp_manager.call_tool(
326
+ "brightdata",
327
+ "search_engine",
328
+ params,
329
+ )
330
+
331
+
332
+ brightdata_mcp = BrightDataMCPClient()
333
+
334
+ # ----------------------------------------------------
335
+ # EXPORT EVERYTHING
336
+ # ----------------------------------------------------
337
+ __all__ = [
338
+ "amazon_mcp",
339
+ "fewsats_mcp",
340
+ "resend_mcp",
341
+ "brightdata_mcp",
342
+ "mcp_manager",
343
+ ]
src/party_context.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared party context between subagents."""
2
+ from typing import List, Optional, Dict, Any
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ThemeOption(BaseModel):
7
+ """A party theme option."""
8
+ id: int
9
+ name: str
10
+ description: str
11
+
12
+
13
+ class VenueResult(BaseModel):
14
+ """A venue search result."""
15
+ name: str
16
+ url: str
17
+ rating: Optional[float] = None
18
+ price_level: Optional[str] = None
19
+ address: Optional[str] = None
20
+ phone: Optional[str] = None
21
+ description: Optional[str] = None
22
+
23
+
24
+ class ShoppingItem(BaseModel):
25
+ """A decor or favor item."""
26
+ name: str
27
+ category: str # "decor" or "favor"
28
+ description: Optional[str] = None
29
+
30
+
31
+ class PartyContext(BaseModel):
32
+ """Shared context for the party planning workflow."""
33
+ party_description: Optional[str] = None
34
+
35
+ # Event metadata captured via chat
36
+ event_budget: Optional[str] = None
37
+ event_location: Optional[str] = None
38
+ event_date: Optional[str] = None
39
+
40
+ # Theme planning
41
+ theme_options: List[ThemeOption] = []
42
+ selected_theme_name: Optional[str] = None
43
+ invitation_text: Optional[str] = None
44
+ invitation_image_path: Optional[str] = None
45
+
46
+ # Guest list (from CSV)
47
+ guest_names: List[str] = []
48
+ guest_emails: List[str] = []
49
+ emails_sent: bool = False
50
+
51
+ # Venue search
52
+ venue_results: List[VenueResult] = []
53
+ search_location: Optional[str] = None
54
+
55
+ # Decor & shopping
56
+ cart_items: List[Dict[str, Any]] = []
57
+ decor_items: List[ShoppingItem] = []
58
+ favor_items: List[ShoppingItem] = []
59
+ selected_items: List[str] = []
60
+ order_status: Optional[str] = None
61
+ order_id: Optional[str] = None
62
+ reasoning_log: List[str] = [] # human-friendly step log
63
+ last_decision: Optional[Dict[str, Any]] = None # raw JSON from decide_next_action
64
+
65
+
66
+ # Global shared context instance
67
+ party_context = PartyContext()
68
+
src/tools/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Tool modules for Plan-a-Party system."""
2
+
src/tools/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (291 Bytes). View file
 
src/tools/__pycache__/csv_guest_parser.cpython-314.pyc ADDED
Binary file (4.22 kB). View file
 
src/tools/__pycache__/yelp_search_tool.cpython-314.pyc ADDED
Binary file (7.04 kB). View file
 
src/tools/csv_guest_parser.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper to parse uploaded CSV guest lists."""
2
+ import pandas as pd
3
+ from typing import List, Tuple
4
+ import io
5
+
6
+
7
+ def parse_guest_csv(file_path: str) -> Tuple[List[str], List[str]]:
8
+ """
9
+ Parse a CSV file with guest information.
10
+
11
+ Expected columns: 'name' and 'email' (case-insensitive)
12
+
13
+ Args:
14
+ file_path: Path to the CSV file
15
+
16
+ Returns:
17
+ Tuple of (guest_names, guest_emails)
18
+ """
19
+ try:
20
+ df = pd.read_csv(file_path)
21
+
22
+ # Normalize column names (case-insensitive)
23
+ df.columns = df.columns.str.lower().str.strip()
24
+
25
+ # Check for required columns
26
+ if 'name' not in df.columns or 'email' not in df.columns:
27
+ raise ValueError("CSV must contain 'name' and 'email' columns")
28
+
29
+ # Extract and clean data
30
+ names = df['name'].astype(str).str.strip().tolist()
31
+ emails = df['email'].astype(str).str.strip().tolist()
32
+
33
+ # Filter out empty rows
34
+ valid_pairs = [(n, e) for n, e in zip(names, emails) if n and e and '@' in e]
35
+ names = [n for n, e in valid_pairs]
36
+ emails = [e for n, e in valid_pairs]
37
+
38
+ return names, emails
39
+
40
+ except Exception as e:
41
+ raise ValueError(f"Error parsing CSV: {str(e)}")
42
+
43
+
44
+ def parse_guest_csv_from_bytes(csv_bytes: bytes) -> Tuple[List[str], List[str]]:
45
+ """Parse CSV from bytes (for Gradio file uploads)."""
46
+ df = pd.read_csv(io.BytesIO(csv_bytes))
47
+ df.columns = df.columns.str.lower().str.strip()
48
+
49
+ if 'name' not in df.columns or 'email' not in df.columns:
50
+ raise ValueError("CSV must contain 'name' and 'email' columns")
51
+
52
+ names = df['name'].astype(str).str.strip().tolist()
53
+ emails = df['email'].astype(str).str.strip().tolist()
54
+
55
+ valid_pairs = [(n, e) for n, e in zip(names, emails) if n and e and '@' in e]
56
+ names = [n for n, e in valid_pairs]
57
+ emails = [e for n, e in valid_pairs]
58
+
59
+ return names, emails
60
+
src/ui/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """UI modules for Plan-a-Party system."""
2
+
src/ui/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (286 Bytes). View file
 
src/ui/__pycache__/gradio_ui.cpython-314.pyc ADDED
Binary file (24.4 kB). View file
 
src/ui/gradio_ui.py ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List, Dict, Tuple, Optional
3
+
4
+ import gradio as gr
5
+
6
+ from src.agents.orchestrator import orchestrator_chat, guest_invite_wrapper
7
+ from src.party_context import party_context
8
+
9
+ # -------------------------------------------------
10
+ # 🎨 Custom theme (vibrant but still professional)
11
+ # -------------------------------------------------
12
+ party_theme = gr.themes.Soft(
13
+ primary_hue="pink",
14
+ secondary_hue="indigo",
15
+ neutral_hue="slate",
16
+ ).set(
17
+ body_background_fill="#050816",
18
+ body_background_fill_dark="#020617",
19
+ body_text_color="#e5e7eb",
20
+ body_text_color_subdued="#9ca3af",
21
+ button_primary_background_fill="#ec4899",
22
+ button_primary_background_fill_hover="#db2777",
23
+ button_primary_text_color="#f9fafb",
24
+ button_secondary_background_fill="#111827",
25
+ button_secondary_background_fill_hover="#1f2937",
26
+ button_secondary_text_color="#e5e7eb",
27
+ block_background_fill="#020617",
28
+ block_border_width="0px",
29
+ block_shadow="0 18px 45px rgba(15,23,42,0.6)",
30
+ )
31
+
32
+ # 💅 Extra CSS polish – injected via <style> tag (no css= on Blocks)
33
+ party_css = """
34
+ .gradio-container {
35
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
36
+ }
37
+
38
+ /* Big gradient header */
39
+ #hero-header {
40
+ background: radial-gradient(circle at top left, #f97316, #ec4899, #6366f1);
41
+ border-radius: 1.5rem;
42
+ padding: 1.5rem 1.75rem;
43
+ color: white;
44
+ box-shadow: 0 18px 45px rgba(15,23,42,0.6);
45
+ }
46
+
47
+ #hero-header h1 {
48
+ font-size: 2rem;
49
+ font-weight: 800;
50
+ letter-spacing: 0.02em;
51
+ margin-bottom: 0.35rem;
52
+ }
53
+
54
+ #hero-header p {
55
+ margin: 0.15rem 0;
56
+ font-size: 0.95rem;
57
+ opacity: 0.96;
58
+ }
59
+
60
+ /* Generic card containers (cart, flow, invitation, guest list, decor) */
61
+ .section-card {
62
+ border-radius: 1.25rem;
63
+ border: 1px solid rgba(148,163,184,0.35);
64
+ background: rgba(15,23,42,0.98);
65
+ padding: 0.9rem 1rem;
66
+ color: #f9fafb !important;
67
+ }
68
+
69
+ .section-card * {
70
+ color: #e5e7eb !important;
71
+ }
72
+
73
+ /* Cart panel */
74
+ #cart-preview-panel {
75
+ border-radius: 1.25rem;
76
+ border: 1px solid rgba(148,163,184,0.4);
77
+ background: radial-gradient(circle at top, rgba(251,113,133,0.14), rgba(15,23,42,1));
78
+ padding: 0.9rem 0.85rem;
79
+ font-size: 0.9rem;
80
+ color: #f9fafb !important;
81
+ }
82
+
83
+ #cart-preview-panel * {
84
+ color: #e5e7eb !important;
85
+ }
86
+
87
+ /* Section titles */
88
+ .section-title {
89
+ font-weight: 700 !important;
90
+ font-size: 1.05rem !important;
91
+ color: #f9fafb !important;
92
+ }
93
+
94
+ /* Chat panel styling */
95
+ .party-chat-panel {
96
+ border-radius: 1.25rem;
97
+ background: radial-gradient(circle at top, rgba(148,163,184,0.12), rgba(15,23,42,1));
98
+ border: 1px solid rgba(148,163,184,0.25);
99
+ padding: 0.9rem;
100
+ }
101
+
102
+ /* Chatbot bubbles */
103
+ .party-chatbot .wrap {
104
+ background: transparent !important;
105
+ }
106
+ .party-chatbot .message.user {
107
+ background: rgba(15,118,110,0.16) !important;
108
+ border-radius: 1.1rem !important;
109
+ }
110
+ .party-chatbot .message.bot {
111
+ background: rgba(79,70,229,0.18) !important;
112
+ border-radius: 1.1rem !important;
113
+ }
114
+
115
+ /* Textbox + buttons row */
116
+ .party-input-row textarea {
117
+ border-radius: 999px !important;
118
+ }
119
+
120
+ /* Make primary button pill-shaped */
121
+ button.primary {
122
+ border-radius: 999px !important;
123
+ font-weight: 600 !important;
124
+ }
125
+
126
+ /* Smaller headings inside right column */
127
+ #cart-preview-panel h1,
128
+ #cart-preview-panel h2,
129
+ #cart-preview-panel h3 {
130
+ font-size: 1rem;
131
+ }
132
+ """
133
+
134
+
135
+ # =================================================
136
+ # Handlers & helpers
137
+ # =================================================
138
+
139
+ def get_cart_preview_text() -> str:
140
+ """
141
+ Build a human-friendly cart preview from party_context.cart_items.
142
+ Assumes theme_decor_favor_shop_agent.shop_on_amazon() stored a list of dicts.
143
+ """
144
+ cart = getattr(party_context, "cart_items", []) or []
145
+
146
+ if not cart:
147
+ return (
148
+ "🛒 **Cart Preview**\n\n"
149
+ "Your cart is currently empty.\n"
150
+ "Once I shop for decor and favors on Amazon, you’ll see matching items here."
151
+ )
152
+
153
+ lines: List[str] = []
154
+ lines.append("🛒 **Amazon Cart Preview**\n")
155
+
156
+ total_prices: List[float] = []
157
+ currency: Optional[str] = None
158
+
159
+ for idx, item in enumerate(cart, start=1):
160
+ title = item.get("title") or item.get("requested_name") or f"Item {idx}"
161
+ url = item.get("url") or ""
162
+ price = item.get("price")
163
+
164
+ if isinstance(price, (int, float)):
165
+ total_prices.append(float(price))
166
+
167
+ if not currency and item.get("currency"):
168
+ currency = item["currency"]
169
+
170
+ line = f"{idx}. **{title}**"
171
+ if price is not None:
172
+ if currency:
173
+ line += f" – ~{price} {currency}"
174
+ else:
175
+ line += f" – ~{price}"
176
+ if url:
177
+ line += f"\n {url}"
178
+
179
+ lines.append(line)
180
+
181
+ if total_prices:
182
+ total = sum(total_prices)
183
+ if currency:
184
+ lines.append(f"\n**Estimated total (before tax & shipping):** ~{total:.2f} {currency}")
185
+ else:
186
+ lines.append(f"\n**Estimated total (before tax & shipping):** ~{total:.2f}")
187
+
188
+ lines.append(
189
+ "\n💡 *No real orders are placed here.* "
190
+ "Open the Amazon links to review and complete checkout manually."
191
+ )
192
+
193
+ return "\n".join(lines)
194
+
195
+
196
+ # ---------- Chat submit handler ----------
197
+
198
+ def chat_submit(
199
+ user_message: str,
200
+ history: Optional[List[Dict]] = None,
201
+ ) -> Tuple[List[Dict], List[Dict], Dict, Dict, Dict, Dict]:
202
+ """
203
+ Main chat handler.
204
+ - Sends user message + history to orchestrator_chat (LLM + tools).
205
+ - Updates Chatbot.
206
+ - Updates "Generate Invitation Card" button visibility based on required info.
207
+ - Updates shopping_checkboxes with decor & favor items.
208
+ - Updates cart_preview markdown if a cart is ready.
209
+
210
+ Returns (in order):
211
+ - chat_history (state)
212
+ - chatbot messages
213
+ - generate_invitation_btn.update
214
+ - shopping_checkboxes.update
215
+ - cart_preview.update
216
+ """
217
+ history = history or []
218
+ user_message = (user_message or "").strip()
219
+
220
+ def _cart_preview_update():
221
+ # Only refresh when a cart is ready
222
+ if getattr(party_context, "order_status", None) == "cart_ready":
223
+ return gr.update(value=get_cart_preview_text(), visible=True)
224
+ # Otherwise, leave as-is
225
+ return gr.update(value=get_cart_preview_text())
226
+
227
+ # If user just hit Enter on empty input, keep state, only refresh cart preview
228
+ if not user_message:
229
+ return (
230
+ history,
231
+ history,
232
+ gr.update(), # no change to generate button
233
+ gr.update(), # no change to shopping checkboxes
234
+ _cart_preview_update(),
235
+ # COMMENTED OUT: agent trace
236
+ # gr.update(value=get_agent_trace_text()),
237
+ gr.update(),
238
+ )
239
+
240
+ # 1) Get orchestrator reply (this may call tools and update party_context)
241
+ try:
242
+ reply = orchestrator_chat(user_message, history)
243
+ except Exception as e:
244
+ print(f"[chat_submit] Error in orchestrator_chat: {e}")
245
+ import traceback
246
+ traceback.print_exc()
247
+ reply = f"I encountered an error: {e}. Please try again."
248
+
249
+ # 2) Build new history (Gradio 6 expects list[dict] with role/content)
250
+ new_history = history + [
251
+ {"role": "user", "content": user_message},
252
+ {"role": "assistant", "content": reply},
253
+ ]
254
+
255
+ # 3) "Generate Invitation Card" button visibility
256
+ has_all_info = (
257
+ party_context.event_date is not None
258
+ and party_context.event_location is not None
259
+ and party_context.selected_theme_name is not None
260
+ )
261
+
262
+ print(
263
+ f"[chat_submit] Checking requirements - Date: {party_context.event_date}, "
264
+ f"Location: {party_context.event_location}, Theme: {party_context.selected_theme_name}"
265
+ )
266
+ print(f"[chat_submit] All info available: {has_all_info}")
267
+
268
+ if has_all_info:
269
+ generate_btn_update = gr.update(visible=True, interactive=True)
270
+ else:
271
+ generate_btn_update = gr.update(visible=False, interactive=False)
272
+
273
+ # 4) Shopping list (decor + favors) → CheckboxGroup
274
+ if party_context.decor_items or party_context.favor_items:
275
+ shopping_choices: List[str] = []
276
+ for item in party_context.decor_items:
277
+ shopping_choices.append(f"decor: {item.name} – {item.description}")
278
+ for item in party_context.favor_items:
279
+ shopping_choices.append(f"favor: {item.name} – {item.description}")
280
+
281
+ shopping_update = gr.update(
282
+ choices=shopping_choices,
283
+ visible=True,
284
+ value=[], # nothing selected by default
285
+ )
286
+ else:
287
+ # no update if nothing new
288
+ shopping_update = gr.update()
289
+
290
+ # 5) Cart preview (Markdown)
291
+ cart_preview_update = _cart_preview_update()
292
+
293
+ # COMMENTED OUT: agent trace
294
+ # # 6) Agent trace (Markdown)
295
+ # agent_trace_update = gr.update(value=get_agent_trace_text())
296
+
297
+ return (
298
+ new_history, # chat_history state
299
+ new_history, # chatbot messages
300
+ generate_btn_update, # "Generate Invitation Card" button
301
+ shopping_update, # Decor & favors checkbox list
302
+ cart_preview_update, # Cart preview markdown
303
+ # COMMENTED OUT: agent trace
304
+ # agent_trace_update, # Agent trace markdown
305
+ gr.update(), # Placeholder for agent_trace
306
+ )
307
+
308
+
309
+
310
+ # ---------- Invitation generation handler ----------
311
+
312
+ def generate_invitation_card(
313
+ history: Optional[List[Dict]] = None,
314
+ ) -> Tuple[List[Dict], List[Dict], Dict, Dict]:
315
+ """
316
+ Called when user clicks "Generate Invitation Card" button.
317
+ Generates the invitation card with all details embedded.
318
+ """
319
+ history = history or []
320
+
321
+ if not party_context.selected_theme_name:
322
+ assistant_msg = (
323
+ "Please confirm which theme you’re going with in the chat first "
324
+ "before generating the invitation card."
325
+ )
326
+ new_history = history + [{"role": "assistant", "content": assistant_msg}]
327
+ return new_history, new_history, gr.update(visible=False), gr.update(visible=False)
328
+
329
+ from src.agents.theme_planning_agent import generate_invitation_for_theme
330
+
331
+ try:
332
+ print(f"[generate_invitation_card] Generating invitation for theme: {party_context.selected_theme_name}")
333
+ raw_result = generate_invitation_for_theme.invoke({
334
+ "selected_theme_name": party_context.selected_theme_name
335
+ })
336
+ print(f"[generate_invitation_card] Raw result (first 300 chars): {str(raw_result)[:300]}")
337
+
338
+ # Assume the tool either:
339
+ # - updates party_context.invitation_image_path + invitation_text
340
+ # - OR returns a JSON blob with those fields.
341
+ invitation_text = getattr(party_context, "invitation_text", None)
342
+ image_path = getattr(party_context, "invitation_image_path", None)
343
+
344
+ # Try to parse JSON if raw_result is a JSON-like string
345
+ if isinstance(raw_result, str):
346
+ raw_str = raw_result.strip()
347
+ if raw_str.startswith("{") and raw_str.endswith("}"):
348
+ try:
349
+ data = json.loads(raw_str)
350
+ invitation_text = data.get("invitation_text", invitation_text)
351
+ image_path = data.get("image_path", image_path)
352
+ except Exception:
353
+ pass
354
+
355
+ # Build a nice assistant message
356
+ if not invitation_text:
357
+ # Fallback: generic text using context
358
+ invitation_text = (
359
+ f"You're invited to a **{party_context.selected_theme_name}** party on "
360
+ f"**{party_context.event_date}** at **{party_context.event_location}**! 🎉\n\n"
361
+ "I’ve tried to embed these details into the invitation image as well."
362
+ )
363
+
364
+ if image_path and os.path.exists(image_path):
365
+ assistant_msg = (
366
+ f"🎉 **Your invitation card is ready!**\n\n"
367
+ f"{invitation_text}\n\n"
368
+ f"Check the image below — it should have your theme, date and venue text baked into the design."
369
+ )
370
+ image_update = gr.update(value=image_path, visible=True)
371
+ else:
372
+ assistant_msg = (
373
+ f"I generated the invitation message, but I couldn’t find a valid image file yet.\n\n"
374
+ f"{invitation_text}\n\n"
375
+ "Please check the server logs for `invitation_image_path` or try again."
376
+ )
377
+ image_update = gr.update(visible=False)
378
+
379
+ except Exception as e:
380
+ import traceback
381
+ error_details = traceback.format_exc()
382
+ print(f"[generate_invitation_card] Error: {e}\n{error_details}")
383
+ assistant_msg = f"❌ Error generating invitation card: {e}. Please try again."
384
+ image_update = gr.update(visible=False)
385
+
386
+ new_history = history + [
387
+ {"role": "user", "content": "Generate invitation card"},
388
+ {"role": "assistant", "content": assistant_msg},
389
+ ]
390
+
391
+ # Hide the button to avoid duplicate spam; you can keep it visible=True if you prefer re-gen
392
+ generate_btn_update = gr.update(visible=False)
393
+
394
+ return new_history, new_history, generate_btn_update, image_update
395
+
396
+
397
+ # ---------- Guest CSV upload handler ----------
398
+
399
+ def upload_guest_csv(file, history: Optional[List[Dict]] = None) -> Tuple[List[Dict], List[Dict], str]:
400
+ """
401
+ Handle guest CSV upload.
402
+ - Calls guest_invite_wrapper(csv_file_path=...) to load guests into party_context.
403
+ - Appends a status message into the chat.
404
+ """
405
+ history = history or []
406
+
407
+ if file is None:
408
+ msg = "Please upload a CSV file with 'name' and 'email' columns."
409
+ new_history = history + [{"role": "assistant", "content": msg}]
410
+ return new_history, new_history, msg
411
+
412
+ try:
413
+ print(f"[upload_guest_csv] Uploading guest list from CSV file: {file.name}")
414
+ result = guest_invite_wrapper(csv_file_path=file.name, send_emails=False)
415
+ assistant_msg = (
416
+ f"{result} Once you’re ready, tell me in chat if I should send "
417
+ "invitations to everyone."
418
+ )
419
+ except Exception as e:
420
+ assistant_msg = f"❌ Error loading guest list: {e}"
421
+
422
+ new_history = history + [{"role": "assistant", "content": assistant_msg}]
423
+ return new_history, new_history, assistant_msg
424
+
425
+
426
+ # ---------- Shopping selection handler ----------
427
+
428
+ def update_shopping_selection(
429
+ selected_items: List[str],
430
+ history: Optional[List[Dict]] = None,
431
+ ) -> Tuple[List[Dict], List[Dict]]:
432
+ """
433
+ Update selected shopping items in shared context.
434
+ The actual order placement will be driven by orchestrator_chat once user confirms in natural language.
435
+ """
436
+ history = history or []
437
+ selected_items = selected_items or []
438
+
439
+ cleaned = [
440
+ s.split(": ", 1)[1] if ": " in s else s
441
+ for s in selected_items
442
+ ]
443
+ party_context.selected_items = cleaned
444
+
445
+ assistant_msg = (
446
+ f"Great, I’ve saved {len(cleaned)} items for your decor & favors. "
447
+ "Just tell me in chat when you’re ready for me to build your Amazon cart."
448
+ )
449
+ new_history = history + [{"role": "assistant", "content": assistant_msg}]
450
+ return new_history, new_history
451
+
452
+ ##adding helper to turn reasoning_log into text
453
+ # COMMENTED OUT: agent trace function
454
+ # def get_agent_trace_text() -> str:
455
+ # log = getattr(party_context, "reasoning_log", []) or []
456
+ # if not log:
457
+ # return (
458
+ # "🧠 **Agent Trace**\n\n"
459
+ # "No internal steps recorded yet. As the planner routes between sub-agents, "
460
+ # "you'll see decisions and tool calls here."
461
+ # )
462
+ # return "🧠 **Agent Trace (latest steps)**\n\n" + "\n\n---\n\n".join(log[-10:])
463
+
464
+ # =================================================
465
+ # Build interface
466
+ # =================================================
467
+
468
+ def build_interface():
469
+ with gr.Blocks(
470
+ title="Plan-A-Party 🎉 (Multi-Agent Party Planner)",
471
+ ) as demo:
472
+ # Inject custom CSS (since this Gradio version doesn't support css=)
473
+ gr.HTML(f"<style>{party_css}</style>")
474
+
475
+ # Hero header
476
+ with gr.Row(elem_id="hero-header"):
477
+ with gr.Column(scale=3):
478
+ gr.Markdown(
479
+ """
480
+ <h1>🎉 Plan-A-Party – Your AI Party Planner </h1>
481
+ <p>Describe your party and I’ll help you:</p>
482
+ <p>✨ Pick a theme · 🏨 Find a venue · 📨 Send invites · 🛍️ Build a decor & favors cart</p>
483
+ """
484
+ )
485
+ with gr.Column(scale=1):
486
+ gr.Markdown(
487
+ """
488
+ <p style="text-align:right; font-size:0.85rem; opacity:0.9;">
489
+ Powered by multi-agent LLM orchestration + MCP<br/>
490
+ <span style="opacity:0.8;">Private to your browser session.</span>
491
+ </p>
492
+ """
493
+ )
494
+
495
+ # Global chat history state (list of {"role": ..., "content": ...})
496
+ chat_history = gr.State([])
497
+
498
+ with gr.Row():
499
+ # LEFT: main chat
500
+ with gr.Column(scale=3, elem_classes=["party-chat-panel"]):
501
+ chatbot = gr.Chatbot(
502
+ label="Party Planning Chat",
503
+ height=360,
504
+ elem_classes=["party-chatbot"],
505
+ )
506
+
507
+ with gr.Row(elem_classes=["party-input-row"]):
508
+ user_input = gr.Textbox(
509
+ placeholder="“e.g., We're hosting a 5th birthday for 60 guests in Sunnyvale next month…”",
510
+ scale=8,
511
+ lines=2,
512
+ show_label=False,
513
+ )
514
+ send_btn = gr.Button("Send ✨", variant="primary", scale=1)
515
+
516
+ # RIGHT: cart preview + quick hints
517
+ with gr.Column(scale=1):
518
+ with gr.Column(elem_classes=["section-card"]):
519
+ gr.Markdown(
520
+ "### 🛒 Cart Preview",
521
+ elem_classes=["section-title"],
522
+ )
523
+ gr.Markdown(
524
+ "As I shop for decor & favors, I’ll build an Amazon cart here so you can review it.",
525
+ )
526
+ cart_preview = gr.Markdown(
527
+ value=get_cart_preview_text(),
528
+ elem_id="cart-preview-panel",
529
+ )
530
+ # COMMENTED OUT: Agent trace panel
531
+ # # Agent trace panel (NEW)
532
+ # with gr.Column(elem_classes=["section-card"]):
533
+ # gr.Markdown(
534
+ # "### 🧠 Agent Trace",
535
+ # elem_classes=["section-title"],
536
+ # )
537
+ # agent_trace = gr.Markdown(
538
+ # value=get_agent_trace_text(),
539
+ # )
540
+
541
+ gr.Markdown(
542
+ """
543
+ #### 🔍 Suggested flow
544
+ 1. Describe your party
545
+ 2. Approve a theme in chat
546
+ 3. Let me find venues
547
+ 4. Generate your invite card
548
+ 5. Upload guest list & send invites
549
+ 6. Ask me to shop decor & favors
550
+ """,
551
+ elem_classes=["section-card"],
552
+ )
553
+
554
+ # --- Generate Invitation Card Button ---
555
+ with gr.Column(elem_classes=["section-card"]):
556
+ gr.Markdown(
557
+ "### 🎨 Invitation Card",
558
+ elem_classes=["section-title"],
559
+ )
560
+ gr.Markdown(
561
+ "After you’ve described your party, picked a theme in chat, and I know your **date & location**, "
562
+ "this button will light up so you can generate your invitation card.",
563
+ )
564
+ generate_invitation_btn = gr.Button(
565
+ "Generate Invitation Card 🖼️",
566
+ variant="primary",
567
+ visible=False,
568
+ )
569
+
570
+ invitation_image = gr.Image(
571
+ label="Your Invitation Card",
572
+ visible=False,
573
+ type="filepath",
574
+ )
575
+
576
+ # --- Guest list upload ---
577
+ with gr.Column(elem_classes=["section-card"]):
578
+ gr.Markdown(
579
+ "### 📧 Guest List & Invites",
580
+ elem_classes=["section-title"],
581
+ )
582
+ gr.Markdown(
583
+ "Upload a CSV with columns: **name**, **email**. "
584
+ "Once it’s loaded, just say in chat if you’d like me to send invitations.",
585
+ )
586
+ guest_csv = gr.File(label="Guest List CSV", file_types=[".csv"])
587
+ guest_status = gr.Textbox(
588
+ label="Guest List Status",
589
+ interactive=False,
590
+ )
591
+
592
+ # --- Decor & favors shopping hooks ---
593
+ with gr.Column(elem_classes=["section-card"]):
594
+ gr.Markdown(
595
+ "### 🛍️ Decor & Favors (Shopping)",
596
+ elem_classes=["section-title"],
597
+ )
598
+ gr.Markdown(
599
+ "I’ll propose theme-matching decor and party favors. "
600
+ "Select what you like, then ask me in chat to build your Amazon cart.",
601
+ )
602
+
603
+ shopping_checkboxes = gr.CheckboxGroup(
604
+ choices=[],
605
+ label="Suggested items (populated by decor agent)",
606
+ interactive=True,
607
+ )
608
+ update_shopping_btn = gr.Button("Save selected items 💾", variant="secondary")
609
+
610
+ fewsats_api_key = gr.Textbox(
611
+ label="FEWSATS_API_KEY (optional for future auto-checkout)",
612
+ type="password",
613
+ placeholder="For this demo, checkout is still manual on Amazon.",
614
+ )
615
+
616
+ # === Wire up events ===
617
+
618
+ # COMMENTED OUT: agent_trace component - using placeholder
619
+ agent_trace_placeholder = gr.Markdown(visible=False)
620
+
621
+ # Chat send (button)
622
+ send_btn.click(
623
+ fn=chat_submit,
624
+ inputs=[user_input, chat_history],
625
+ outputs=[
626
+ chat_history, # 1: updated history state
627
+ chatbot, # 2: chatbot display
628
+ generate_invitation_btn, # 3: button visibility
629
+ shopping_checkboxes, # 4: shopping list
630
+ cart_preview, # 5: cart preview
631
+ # COMMENTED OUT: agent trace
632
+ # agent_trace, # 6: agent trace
633
+ agent_trace_placeholder, # 6: placeholder
634
+ ],
635
+ ).then(
636
+ fn=lambda: "", # Clear input after sending
637
+ inputs=[],
638
+ outputs=[user_input],
639
+ )
640
+
641
+ # Chat send (Enter)
642
+ user_input.submit(
643
+ fn=chat_submit,
644
+ inputs=[user_input, chat_history],
645
+ outputs=[
646
+ chat_history,
647
+ chatbot,
648
+ generate_invitation_btn,
649
+ shopping_checkboxes,
650
+ cart_preview,
651
+ # COMMENTED OUT: agent trace
652
+ # agent_trace,
653
+ agent_trace_placeholder, # placeholder
654
+ ],
655
+ ).then(
656
+ fn=lambda: "",
657
+ inputs=[],
658
+ outputs=[user_input],
659
+ )
660
+
661
+ # Generate invitation card button
662
+ generate_invitation_btn.click(
663
+ fn=generate_invitation_card,
664
+ inputs=[chat_history],
665
+ outputs=[chat_history, chatbot, generate_invitation_btn, invitation_image],
666
+ )
667
+
668
+ # Guest CSV upload
669
+ guest_csv.upload(
670
+ fn=upload_guest_csv,
671
+ inputs=[guest_csv, chat_history],
672
+ outputs=[chat_history, chatbot, guest_status],
673
+ )
674
+
675
+ # Shopping selection update
676
+ update_shopping_btn.click(
677
+ fn=update_shopping_selection,
678
+ inputs=[shopping_checkboxes, chat_history],
679
+ outputs=[chat_history, chatbot],
680
+ )
681
+
682
+ return demo