Spaces:
Running
Running
Added application code base & container manifest
Browse files- Dockerfile +39 -0
- PROJECT_SUMMARY.md +360 -0
- README.md +249 -10
- app.py +7 -0
- requirements.txt +15 -0
- src/.DS_Store +0 -0
- src/__init__.py +2 -0
- src/__pycache__/__init__.cpython-314.pyc +0 -0
- src/__pycache__/config.cpython-314.pyc +0 -0
- src/__pycache__/party_context.cpython-314.pyc +0 -0
- src/agents/__init__.py +2 -0
- src/agents/__pycache__/__init__.cpython-314.pyc +0 -0
- src/agents/__pycache__/guest_invite_email_agent.cpython-314.pyc +0 -0
- src/agents/__pycache__/orchestrator.cpython-314.pyc +0 -0
- src/agents/__pycache__/theme_decor_favor_shop_agent.cpython-314.pyc +0 -0
- src/agents/__pycache__/theme_planning_agent.cpython-314.pyc +0 -0
- src/agents/__pycache__/venue_search_agent.cpython-314.pyc +0 -0
- src/agents/guest_invite_email_agent.py +324 -0
- src/agents/orchestrator.py +791 -0
- src/agents/theme_decor_favor_shop_agent.py +379 -0
- src/agents/theme_planning_agent.py +286 -0
- src/agents/venue_search_agent.py +219 -0
- src/config.py +28 -0
- src/mcp/__init__.py +2 -0
- src/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
- src/mcp/__pycache__/mcp_clients.cpython-314.pyc +0 -0
- src/mcp/mcp_clients.py +343 -0
- src/party_context.py +68 -0
- src/tools/__init__.py +2 -0
- src/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- src/tools/__pycache__/csv_guest_parser.cpython-314.pyc +0 -0
- src/tools/__pycache__/yelp_search_tool.cpython-314.pyc +0 -0
- src/tools/csv_guest_parser.py +60 -0
- src/ui/__init__.py +2 -0
- src/ui/__pycache__/__init__.cpython-314.pyc +0 -0
- src/ui/__pycache__/gradio_ui.cpython-314.pyc +0 -0
- 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
| 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
|