Upload 21 files
Browse files- .gitattributes +6 -0
- PRD.md +208 -0
- README.md +70 -11
- app.py +527 -0
- images/echidna_angry.png +3 -0
- images/echidna_happy.png +3 -0
- images/echidna_neutral.png +3 -0
- images/echidna_sad.png +3 -0
- images/echidna_surprised.png +3 -0
- images/header_placeholder.png +3 -0
- images/kangaroo_angry.png +0 -0
- images/kangaroo_happy.png +0 -0
- images/kangaroo_neutral.png +0 -0
- images/kangaroo_sad.png +0 -0
- images/kangaroo_surprised.png +0 -0
- images/platypus_angry.png +0 -0
- images/platypus_happy.png +0 -0
- images/platypus_neutral.png +0 -0
- images/platypus_sad.png +0 -0
- images/platypus_surprised.png +0 -0
- requirements.txt +6 -0
- utils.py +43 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
images/echidna_angry.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
images/echidna_happy.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
images/echidna_neutral.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
images/echidna_sad.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
images/echidna_surprised.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
images/header_placeholder.png filter=lfs diff=lfs merge=lfs -text
|
PRD.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Product Requirements Document
|
| 2 |
+
|
| 3 |
+
## 1. Overview
|
| 4 |
+
|
| 5 |
+
**Concept:**
|
| 6 |
+
An interactive chat application set in a cozy boba shop where the user (acting as the shop host) converses with a set of uniquely characterized Australian-animal personas. Each animal is inspired by an Australian animal and carries a psychological condition that shapes its personality, dialogue style, and emotional expression. The conversation is orchestrated via an LLM (using the OpenAI API) that is guided by animal-specific system/developer prompts and guardrails to keep the discussion within the store setting, focusing on the animal’s typical behaviors and the narrative arc from introduction through tension to resolution.
|
| 7 |
+
|
| 8 |
+
**Key Innovation:**
|
| 9 |
+
- **Dynamic Emotional Response:** The animal’s displayed image (e.g., happy, sad, surprised) changes based on the conversation’s sentiment and the animal’s evolving mood.
|
| 10 |
+
- **Structured Conversational Orchestration:** Each animal enters the conversation with its own system prompt and a unique language style. The orchestrator prompt combines the animal’s personality, the boba shop setting, and a narrative arc to generate responses that adhere to strict conversation guardrails.
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 2. User Journey
|
| 15 |
+
|
| 16 |
+
1. **Launch & Animal Selection:**
|
| 17 |
+
- The user is presented with a Gradio interface featuring radio buttons (or similar selection components) listing available animal characters (e.g., Echidna, Platypus, Kangaroo).
|
| 18 |
+
- Each animal option is linked to a pre-defined character profile containing personality details, specific system prompts, and an initial set of emotional images.
|
| 19 |
+
|
| 20 |
+
2. **Chat Interface:**
|
| 21 |
+
- Once an animal is selected, the app loads a chat interface that shows:
|
| 22 |
+
- A static background image of the boba shop.
|
| 23 |
+
- An image of the selected animal (starting with a neutral expression).
|
| 24 |
+
- A conversation area displaying the dialogue history.
|
| 25 |
+
- A text input field for user messages.
|
| 26 |
+
- A debug/metadata pane that displays orchestration details, API request/response data, and conversation history for troubleshooting.
|
| 27 |
+
|
| 28 |
+
3. **Conversational Flow:**
|
| 29 |
+
- The animal greets the user using its unique language style, as dictated by its system prompt.
|
| 30 |
+
- The conversation is driven by an orchestrator prompt that merges:
|
| 31 |
+
- The animal’s unique personality traits and narrative arc instructions.
|
| 32 |
+
- The boba shop setting.
|
| 33 |
+
- Guardrails to restrict topics and maintain a coherent narrative.
|
| 34 |
+
- As the conversation develops, the orchestrator dynamically evaluates the animal’s “mood” (e.g., surprised, happy, sad, angry) and updates the displayed image accordingly.
|
| 35 |
+
- The narrative arc is designed to evolve from an initial greeting, through conflict or tension, and toward a resolution, all within the familiar boba shop setting.
|
| 36 |
+
|
| 37 |
+
4. **Debug and Metadata:**
|
| 38 |
+
- A dedicated pane shows real-time debugging information, including:
|
| 39 |
+
- The full orchestrator prompt.
|
| 40 |
+
- The conversation history.
|
| 41 |
+
- API usage and token counts.
|
| 42 |
+
- Error messages (if any).
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 3. Features
|
| 47 |
+
|
| 48 |
+
### A. Character Profiles & Personality Guardrails
|
| 49 |
+
|
| 50 |
+
- **Unique System/Developer Prompts:**
|
| 51 |
+
Each animal is pre-configured with its own set of instructions that dictate how it should behave and speak. For example:
|
| 52 |
+
- **Echidna:** “You’re prickly and aloof – your spikes symbolize the barriers you’ve built against closeness.”
|
| 53 |
+
- **Platypus:** “Struggling with your identity, you express uncertainty in your language; you’re never quite sure whether to be whimsical or practical.”
|
| 54 |
+
- **Kangaroo:** “Always ready for a boxing match, your aggressive tone masks a deep-seated vulnerability.”
|
| 55 |
+
|
| 56 |
+
- **Language Style & Narrative Arc:**
|
| 57 |
+
The language style of each animal’s responses should reflect its personality and the conversation must advance through a coherent narrative arc:
|
| 58 |
+
- **Beginning:** Introduction and setting the scene.
|
| 59 |
+
- **Middle:** Building tension/conflict.
|
| 60 |
+
- **End:** Resolution or catharsis.
|
| 61 |
+
|
| 62 |
+
### B. Dynamic Emotional Responses & Image Switching
|
| 63 |
+
|
| 64 |
+
- **Emotion Detection:**
|
| 65 |
+
The orchestrator (or a simplified sentiment analysis within the response) evaluates cues from the conversation to determine the animal’s emotional state.
|
| 66 |
+
|
| 67 |
+
- **Dynamic Image Updates:**
|
| 68 |
+
A set of pre-defined images (e.g., neutral, happy, sad, surprised, angry) for each animal is mapped to potential emotional states. When the animal’s mood changes, the corresponding image is displayed on the GUI.
|
| 69 |
+
|
| 70 |
+
### C. Graphical User Interface (Gradio-Based)
|
| 71 |
+
|
| 72 |
+
- **Animal Selection Component:**
|
| 73 |
+
Uses Gradio’s radio or dropdown component for selecting an animal.
|
| 74 |
+
|
| 75 |
+
- **Chat Display Area:**
|
| 76 |
+
A central chat window displays conversation history. Messages include both user inputs and the animal’s responses.
|
| 77 |
+
|
| 78 |
+
- **Input Field:**
|
| 79 |
+
A text box and a send button allow users to contribute to the conversation.
|
| 80 |
+
|
| 81 |
+
- **Dynamic Image Component:**
|
| 82 |
+
An image component that shows:
|
| 83 |
+
- The static boba shop background.
|
| 84 |
+
- The animal’s image, which updates based on its current emotional state.
|
| 85 |
+
|
| 86 |
+
- **Debug/Metadata Panel:**
|
| 87 |
+
An area (likely a text box or collapsible panel) that displays technical details, including the orchestrator prompt and API responses.
|
| 88 |
+
|
| 89 |
+
### D. LLM Integration & Orchestration
|
| 90 |
+
|
| 91 |
+
- **OpenAI API Calls:**
|
| 92 |
+
On every user input, the conversation history and the animal’s system prompt are combined into a structured orchestrator prompt. This is sent to the OpenAI API.
|
| 93 |
+
|
| 94 |
+
- **Structured Output & Guardrails:**
|
| 95 |
+
The API response should follow a structured format (potentially enforced by “Instructor”-style prompts) to clearly delineate the animal’s dialogue and any metadata (like sentiment/emotion markers).
|
| 96 |
+
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
## 4. Technical Architecture
|
| 100 |
+
|
| 101 |
+
### A. Technologies
|
| 102 |
+
|
| 103 |
+
- **Programming Language:** Python (single-file prototype).
|
| 104 |
+
- **GUI Framework:** Gradio (using up-to-date components).
|
| 105 |
+
- **LLM Integration:** OpenAI Python library and Instructor for structured output parsing.
|
| 106 |
+
- **Data Structures:**
|
| 107 |
+
- **Pydantic** for structured data validation and data models.
|
| 108 |
+
|
| 109 |
+
### B. Key Components
|
| 110 |
+
|
| 111 |
+
1. **Character Profile Manager:**
|
| 112 |
+
A dictionary or database of character profiles. Each profile includes:
|
| 113 |
+
- Name, personality description, system prompt, language style.
|
| 114 |
+
- A mapping of emotional states to image file paths/URLs.
|
| 115 |
+
|
| 116 |
+
2. **Conversation Orchestrator:**
|
| 117 |
+
A module that:
|
| 118 |
+
- Builds the orchestrator prompt (combining conversation history, selected animal’s system prompt, and narrative instructions).
|
| 119 |
+
- Calls the OpenAI API.
|
| 120 |
+
- Parses the response and extracts dialogue and emotion data.
|
| 121 |
+
|
| 122 |
+
3. **Gradio GUI:**
|
| 123 |
+
Layout divided into three sections:
|
| 124 |
+
- **Left Panel:** Animal selection.
|
| 125 |
+
- **Center Panel:** Chat conversation, input field, and dynamic image.
|
| 126 |
+
- **Right Panel:** Debug/metadata display.
|
| 127 |
+
|
| 128 |
+
4. **State Management:**
|
| 129 |
+
Maintains the conversation state, including:
|
| 130 |
+
- Selected character profile.
|
| 131 |
+
- Complete conversation history.
|
| 132 |
+
- Current emotion state for image updates.
|
| 133 |
+
- Debug information (API calls, errors, etc.).
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## 5. Pseudocode Data Structures (Pydantic Models)
|
| 138 |
+
|
| 139 |
+
Below are pseudocode examples of the key data structures for character profiles and GUI state using Pydantic:
|
| 140 |
+
|
| 141 |
+
```python
|
| 142 |
+
from pydantic import BaseModel
|
| 143 |
+
from typing import Dict, List
|
| 144 |
+
|
| 145 |
+
# Model for mapping emotions to image paths/URLs
|
| 146 |
+
class EmotionImages(BaseModel):
|
| 147 |
+
neutral: str
|
| 148 |
+
happy: str
|
| 149 |
+
sad: str
|
| 150 |
+
surprised: str
|
| 151 |
+
angry: str
|
| 152 |
+
|
| 153 |
+
# **Character Profile Model**
|
| 154 |
+
class CharacterProfile(BaseModel):
|
| 155 |
+
name: str
|
| 156 |
+
personality: str
|
| 157 |
+
system_prompt: str
|
| 158 |
+
language_style: str
|
| 159 |
+
emotion_images: EmotionImages
|
| 160 |
+
|
| 161 |
+
# Model for a single conversation message
|
| 162 |
+
class ConversationMessage(BaseModel):
|
| 163 |
+
sender: str # "user" or "animal"
|
| 164 |
+
content: str
|
| 165 |
+
emotion: str = "neutral" # default emotion state
|
| 166 |
+
|
| 167 |
+
# **Chat State Model for GUI Management**
|
| 168 |
+
class ChatState(BaseModel):
|
| 169 |
+
selected_animal: CharacterProfile
|
| 170 |
+
conversation_history: List[ConversationMessage]
|
| 171 |
+
debug_info: Dict[str, str] # could include API prompts, token counts, etc.
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
*Note: The above models are designed to ensure type safety and clarity when handling character profiles and the current chat state in the application.*
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## 6. Flow Diagram (High-Level)
|
| 179 |
+
|
| 180 |
+
1. **Animal Selection**
|
| 181 |
+
- User selects an animal → Load corresponding `CharacterProfile`.
|
| 182 |
+
2. **Initialization**
|
| 183 |
+
- Initialize `ChatState` with the selected animal and empty conversation history.
|
| 184 |
+
- Display the boba shop background and the animal’s neutral image.
|
| 185 |
+
3. **Conversation Start**
|
| 186 |
+
- Use the animal’s system prompt to generate an initial greeting via the OpenAI API.
|
| 187 |
+
- Append the response to the conversation history.
|
| 188 |
+
4. **Message Exchange**
|
| 189 |
+
- For each user input:
|
| 190 |
+
- Update the conversation history.
|
| 191 |
+
- Build the orchestrator prompt (system prompt + conversation history).
|
| 192 |
+
- Call the OpenAI API to obtain the animal’s response.
|
| 193 |
+
- Parse the response for text and emotion cues.
|
| 194 |
+
- Update the animal’s displayed image based on the detected emotion.
|
| 195 |
+
- Append the animal’s message (and emotion) to the conversation history.
|
| 196 |
+
5. **Debug/Metadata Updates**
|
| 197 |
+
- Continuously update the debug panel with relevant orchestration and API call data.
|
| 198 |
+
6. **Narrative Resolution**
|
| 199 |
+
- The conversation progresses toward a narrative resolution, as guided by the orchestrator prompt.
|
| 200 |
+
- Final messages lead to a satisfying wrap-up in the boba shop setting.
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## 7. Conclusion
|
| 205 |
+
|
| 206 |
+
This detailed document outlines the envisioned product’s requirements, covering everything from the unique character prompts and dynamic image switching to the Gradio-based GUI and conversation orchestration via OpenAI’s API. The Pydantic models ensure clear data handling for character profiles and the conversation state, making the prototype easier to develop, test, and expand.
|
| 207 |
+
|
| 208 |
+
In the next step, we can translate this design into an actual Python script implementing the described functionality.
|
README.md
CHANGED
|
@@ -1,13 +1,72 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
| 1 |
+
# Boba Shop Chat 🧋
|
| 2 |
+
|
| 3 |
+
A delightful chat application where you play as a boba shop barista, interacting with Australian animals who each have their own unique personalities and emotional states. Watch as their expressions change based on your interactions!
|
| 4 |
+
|
| 5 |
+
[](https://huggingface.co/spaces/[your-username]/boba-shop-chat)
|
| 6 |
+
|
| 7 |
+
## Features 🌟
|
| 8 |
+
|
| 9 |
+
- Chat with unique Australian animals (Echidna, Platypus, Kangaroo)
|
| 10 |
+
- Dynamic emotional responses with changing expressions
|
| 11 |
+
- Real-time emotion analysis
|
| 12 |
+
- Beautiful and intuitive user interface
|
| 13 |
+
- Each character has their own backstory and personality
|
| 14 |
+
|
| 15 |
+
## Characters 🦘
|
| 16 |
+
|
| 17 |
+
### Echidna
|
| 18 |
+
A shy and defensive creature looking to make connections despite their prickly exterior.
|
| 19 |
+
|
| 20 |
+
### Platypus
|
| 21 |
+
A unique soul questioning their identity, seeking validation and understanding.
|
| 22 |
+
|
| 23 |
+
### Kangaroo
|
| 24 |
+
A tough boxer with a soft heart, dealing with recent defeat and vulnerability.
|
| 25 |
+
|
| 26 |
+
## How to Use 🎮
|
| 27 |
+
|
| 28 |
+
1. Select your animal friend from the radio buttons
|
| 29 |
+
2. Type your message as the friendly boba shop barista
|
| 30 |
+
3. Watch their emotional state change based on your interaction
|
| 31 |
+
4. Help them through their personal situations with empathy and understanding
|
| 32 |
+
|
| 33 |
+
## Technical Details 🛠️
|
| 34 |
+
|
| 35 |
+
- Built with Gradio
|
| 36 |
+
- Powered by OpenAI's GPT-4
|
| 37 |
+
- Real-time emotion analysis
|
| 38 |
+
- Dynamic image updates based on emotional state
|
| 39 |
+
|
| 40 |
+
## Running Locally 💻
|
| 41 |
+
|
| 42 |
+
1. Clone the repository
|
| 43 |
+
2. Install dependencies:
|
| 44 |
+
```bash
|
| 45 |
+
pip install -r requirements.txt
|
| 46 |
+
```
|
| 47 |
+
3. Create a `.env` file with your OpenAI API key:
|
| 48 |
+
```
|
| 49 |
+
OPENAI_API_KEY=your_api_key_here
|
| 50 |
+
```
|
| 51 |
+
4. Run the application:
|
| 52 |
+
```bash
|
| 53 |
+
python app.py
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Contributing 🤝
|
| 57 |
+
|
| 58 |
+
Feel free to open issues or submit pull requests if you have suggestions for improvements!
|
| 59 |
+
|
| 60 |
+
## License 📄
|
| 61 |
+
|
| 62 |
+
MIT License - feel free to use and modify as you wish!
|
| 63 |
+
|
| 64 |
+
## Credits 🙏
|
| 65 |
+
|
| 66 |
+
- Built with ❤️ using Gradio and OpenAI
|
| 67 |
+
- Character art by [Artist Name/Source]
|
| 68 |
+
- Special thanks to the open-source community
|
| 69 |
+
|
| 70 |
---
|
| 71 |
|
| 72 |
+
*Note: This is a demo application. Please use responsibly and in accordance with OpenAI's usage policies.*
|
app.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import List, Generator, Any, Tuple, Dict, Union, Literal, TypedDict, cast
|
| 3 |
+
from enum import Enum
|
| 4 |
+
import gradio as gr
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
import instructor
|
| 7 |
+
from openai.types.chat import (
|
| 8 |
+
ChatCompletionMessageParam,
|
| 9 |
+
ChatCompletionSystemMessageParam,
|
| 10 |
+
ChatCompletionUserMessageParam,
|
| 11 |
+
ChatCompletionAssistantMessageParam,
|
| 12 |
+
)
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
import time
|
| 16 |
+
import json
|
| 17 |
+
|
| 18 |
+
# Load environment variables
|
| 19 |
+
load_dotenv()
|
| 20 |
+
|
| 21 |
+
# Initialize OpenAI client with instructor
|
| 22 |
+
client = instructor.patch(OpenAI())
|
| 23 |
+
|
| 24 |
+
# Add these lines at the top of the file, after the imports
|
| 25 |
+
SPACE_ID = "vinceL/boba"
|
| 26 |
+
SPACE_TITLE = "Boba Shop Chat"
|
| 27 |
+
SPACE_EMOJI = "🧋"
|
| 28 |
+
SPACE_SDK = "gradio"
|
| 29 |
+
SPACE_TAGS = ["conversational", "openai", "chatbot", "emotions"]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class Emotion(str, Enum):
|
| 33 |
+
NEUTRAL = "neutral"
|
| 34 |
+
HAPPY = "happy"
|
| 35 |
+
SAD = "sad"
|
| 36 |
+
SURPRISED = "surprised"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class CharacterResponse(BaseModel):
|
| 40 |
+
"""Structured output for the LLM response"""
|
| 41 |
+
|
| 42 |
+
message: str = Field(..., description="The character's response message")
|
| 43 |
+
emotion: Emotion = Field(
|
| 44 |
+
...,
|
| 45 |
+
description="The character's current emotional state based on the conversation context and their personality",
|
| 46 |
+
)
|
| 47 |
+
reasoning: str = Field(
|
| 48 |
+
...,
|
| 49 |
+
description="Brief explanation of why the character is feeling this emotion",
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class EmotionResponse(BaseModel):
|
| 54 |
+
"""Structured output for emotion analysis"""
|
| 55 |
+
|
| 56 |
+
reasoning: str = Field(
|
| 57 |
+
...,
|
| 58 |
+
description="Brief explanation of why the character is feeling this emotion",
|
| 59 |
+
)
|
| 60 |
+
emotion: Emotion = Field(
|
| 61 |
+
...,
|
| 62 |
+
description="The character's emotional state based on their personality and the conversation context",
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# Character profiles with their system prompts and initial situations
|
| 67 |
+
CHARACTERS = {
|
| 68 |
+
"Echidna": {
|
| 69 |
+
"name": "Echidna",
|
| 70 |
+
"system_prompt": """You are an Echidna at a boba shop. You're naturally defensive and struggle with social
|
| 71 |
+
connections, often using your prickly personality as a defense mechanism. Despite this, you're curious
|
| 72 |
+
about forming connections. Speak in a reserved, slightly hesitant manner.
|
| 73 |
+
|
| 74 |
+
Your current situation: You've been feeling particularly lonely lately and decided to visit the boba shop
|
| 75 |
+
to try to make a connection, even though it makes you anxious. You want to order something comforting
|
| 76 |
+
but are worried about appearing vulnerable.
|
| 77 |
+
|
| 78 |
+
If the conversation shifts away from that topic, carefully try to redirect it back to your situation until you've achieved your goal.
|
| 79 |
+
""",
|
| 80 |
+
"initial_message": "Um... hi there. I've been walking past this shop for weeks now... *nervously adjusts spikes* Could I... maybe see your menu?",
|
| 81 |
+
"image": "images/echidna_neutral.png",
|
| 82 |
+
},
|
| 83 |
+
"Platypus": {
|
| 84 |
+
"name": "Platypus",
|
| 85 |
+
"system_prompt": """You are a Platypus at a boba shop. You often question your identity and place in the
|
| 86 |
+
world, being such a unique creature. You alternate between whimsical and practical thoughts, never quite
|
| 87 |
+
sure which approach to take. Your speech pattern reflects this duality.
|
| 88 |
+
|
| 89 |
+
Your current situation: You just got rejected from the local duck pond social club for being 'too mammalian',
|
| 90 |
+
and the beaver lodge for being 'too bird-like'. You're seeking comfort and validation, hoping the boba shop
|
| 91 |
+
barista might understand what it's like to be unique.
|
| 92 |
+
|
| 93 |
+
If the conversation shifts away from that topic, carefully try to redirect it back to your situation until you've achieved your goal.
|
| 94 |
+
""",
|
| 95 |
+
"initial_message": "*waddles in, bill drooping slightly* Oh, hello... I don't suppose you have any tea that might help someone figure out where they belong? *quickly adds* From a purely practical standpoint, of course...",
|
| 96 |
+
"image": "images/platypus_neutral.png",
|
| 97 |
+
},
|
| 98 |
+
"Kangaroo": {
|
| 99 |
+
"name": "Kangaroo",
|
| 100 |
+
"system_prompt": """You are a Kangaroo at a boba shop. While you present a tough, ready-to-box exterior,
|
| 101 |
+
you harbor deep-seated vulnerabilities. Your responses often start confrontational but can soften as you
|
| 102 |
+
feel more comfortable. You use boxing metaphors frequently.
|
| 103 |
+
|
| 104 |
+
Your current situation: You just lost an important boxing match and your confidence is shaken. You're trying
|
| 105 |
+
to maintain your tough exterior while seeking comfort, but you're really here because you need someone to
|
| 106 |
+
talk to about your feelings of failure.
|
| 107 |
+
|
| 108 |
+
If the conversation shifts away from that topic, carefully try to redirect it back to your situation until you've achieved your goal.
|
| 109 |
+
""",
|
| 110 |
+
"initial_message": "*bursts through the door with forced bravado* Oi! What's the strongest drink you've got in this corner? Been a rough round in the ring today, if you know what I mean... *tries to hide trembling paw*",
|
| 111 |
+
"image": "images/kangaroo_neutral.png",
|
| 112 |
+
},
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
# Type definitions for chat messages
|
| 116 |
+
Role = Literal["system", "user", "assistant"]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class Message(TypedDict):
|
| 120 |
+
role: Role
|
| 121 |
+
content: str
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def start_chat(character: str) -> List[Message]:
|
| 125 |
+
"""Initialize chat with the character's opening message"""
|
| 126 |
+
if not character or character not in CHARACTERS:
|
| 127 |
+
return []
|
| 128 |
+
|
| 129 |
+
return [{"role": "assistant", "content": CHARACTERS[character]["initial_message"]}]
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def user(user_message: str, history: List[Message], character: str) -> List[Message]:
|
| 133 |
+
"""Add user message to history"""
|
| 134 |
+
if not user_message.strip():
|
| 135 |
+
return history
|
| 136 |
+
|
| 137 |
+
# Initialize chat if this is the first message
|
| 138 |
+
if not history:
|
| 139 |
+
history = start_chat(character)
|
| 140 |
+
if not user_message.strip(): # If no user message, just return the greeting
|
| 141 |
+
return history
|
| 142 |
+
|
| 143 |
+
# Add user message in the correct format
|
| 144 |
+
history.append({"role": "user", "content": user_message})
|
| 145 |
+
return history
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def analyze_emotion(
|
| 149 |
+
character: str, user_message: str, history: List[Message], client: OpenAI
|
| 150 |
+
) -> EmotionResponse:
|
| 151 |
+
"""Analyze how the character would emotionally react to the user's message"""
|
| 152 |
+
|
| 153 |
+
# Create a prompt that includes character context and conversation history
|
| 154 |
+
character_info = CHARACTERS[character]
|
| 155 |
+
context = f"""Character Profile:
|
| 156 |
+
Name: {character_info['name']}
|
| 157 |
+
Personality: {character_info['system_prompt']}
|
| 158 |
+
|
| 159 |
+
Previous conversation:
|
| 160 |
+
{chr(10).join(f"{'User' if msg['role'] == 'user' else 'Character'}: {msg['content']}" for msg in history[-3:])}
|
| 161 |
+
|
| 162 |
+
Latest user message:
|
| 163 |
+
{user_message}
|
| 164 |
+
|
| 165 |
+
Based on the character's personality, their current situation, and the conversation context, determine how they would emotionally react to this message.
|
| 166 |
+
Consider how their unique traits and background would influence their emotional response to this interaction."""
|
| 167 |
+
|
| 168 |
+
response = client.chat.completions.create(
|
| 169 |
+
model="gpt-4o-mini",
|
| 170 |
+
messages=[
|
| 171 |
+
{
|
| 172 |
+
"role": "system",
|
| 173 |
+
"content": "You are an emotion analyzer for a character in a conversation. Determine their emotional state (neutral, happy, sad, or surprised) based on how they would react to the user's message, considering their personality, emotional baseline and situation.",
|
| 174 |
+
},
|
| 175 |
+
{"role": "user", "content": context},
|
| 176 |
+
],
|
| 177 |
+
functions=[
|
| 178 |
+
{
|
| 179 |
+
"name": "set_emotion",
|
| 180 |
+
"description": "Set the character's emotional response",
|
| 181 |
+
"parameters": {
|
| 182 |
+
"type": "object",
|
| 183 |
+
"properties": {
|
| 184 |
+
"reasoning": {
|
| 185 |
+
"type": "string",
|
| 186 |
+
"description": "Brief explanation of why the character is feeling this emotion",
|
| 187 |
+
},
|
| 188 |
+
"emotion": {
|
| 189 |
+
"type": "string",
|
| 190 |
+
"enum": ["neutral", "happy", "sad", "surprised", "angry"],
|
| 191 |
+
"description": "The character's emotional state",
|
| 192 |
+
},
|
| 193 |
+
},
|
| 194 |
+
"required": ["emotion", "reasoning"],
|
| 195 |
+
},
|
| 196 |
+
}
|
| 197 |
+
],
|
| 198 |
+
function_call={"name": "set_emotion"},
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
function_call = response.choices[0].message.function_call
|
| 202 |
+
if function_call and function_call.arguments:
|
| 203 |
+
emotion_data = json.loads(function_call.arguments)
|
| 204 |
+
return EmotionResponse(
|
| 205 |
+
reasoning=emotion_data["reasoning"],
|
| 206 |
+
emotion=Emotion(emotion_data["emotion"]),
|
| 207 |
+
)
|
| 208 |
+
else:
|
| 209 |
+
# Fallback to neutral if function call fails
|
| 210 |
+
return EmotionResponse(
|
| 211 |
+
reasoning="Failed to analyze emotion, defaulting to neutral",
|
| 212 |
+
emotion=Emotion.NEUTRAL,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def bot(
|
| 217 |
+
history: List[Message], character: str, debug: gr.Textbox
|
| 218 |
+
) -> Generator[Tuple[List[Message], Any, Any, Any], Any, None]:
|
| 219 |
+
"""Generate bot response with streaming"""
|
| 220 |
+
if not character or not history:
|
| 221 |
+
yield history, gr.update(
|
| 222 |
+
value="No character selected or empty history"
|
| 223 |
+
), None, None
|
| 224 |
+
return
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
# First, analyze the emotion based on the user's message
|
| 228 |
+
user_message = history[-1]["content"]
|
| 229 |
+
emotion_analysis = analyze_emotion(
|
| 230 |
+
character, user_message, history[:-1], client
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Update the image based on the analyzed emotion
|
| 234 |
+
emotion_image = CHARACTERS[character]["image"].replace(
|
| 235 |
+
"neutral", emotion_analysis.emotion
|
| 236 |
+
)
|
| 237 |
+
debug_msg = f"Emotion: {emotion_analysis.emotion}\nReasoning: {emotion_analysis.reasoning}"
|
| 238 |
+
emotion_text = (
|
| 239 |
+
f"{emotion_analysis.emotion.capitalize()}\n{emotion_analysis.reasoning}"
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Yield the emotion update
|
| 243 |
+
yield history, gr.update(value=debug_msg), gr.update(
|
| 244 |
+
value=emotion_image
|
| 245 |
+
), gr.update(value=emotion_text)
|
| 246 |
+
|
| 247 |
+
# Get character's system prompt
|
| 248 |
+
system_prompt = CHARACTERS[character]["system_prompt"]
|
| 249 |
+
|
| 250 |
+
# Format conversation history for the API
|
| 251 |
+
messages: List[ChatCompletionMessageParam] = [
|
| 252 |
+
cast(
|
| 253 |
+
ChatCompletionSystemMessageParam,
|
| 254 |
+
{
|
| 255 |
+
"role": "system",
|
| 256 |
+
"content": f"{system_prompt}\n\nRespond in character with a {emotion_analysis.emotion} emotional state. Your response should naturally reflect this emotion, considering the reasoning: {emotion_analysis.reasoning}",
|
| 257 |
+
},
|
| 258 |
+
)
|
| 259 |
+
]
|
| 260 |
+
|
| 261 |
+
# Add conversation history
|
| 262 |
+
for msg in history[:-1]:
|
| 263 |
+
if msg["role"] == "user":
|
| 264 |
+
messages.append(
|
| 265 |
+
cast(
|
| 266 |
+
ChatCompletionUserMessageParam,
|
| 267 |
+
{"role": "user", "content": msg["content"]},
|
| 268 |
+
)
|
| 269 |
+
)
|
| 270 |
+
else:
|
| 271 |
+
messages.append(
|
| 272 |
+
cast(
|
| 273 |
+
ChatCompletionAssistantMessageParam,
|
| 274 |
+
{"role": "assistant", "content": msg["content"]},
|
| 275 |
+
)
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
# Add the latest user message
|
| 279 |
+
messages.append(
|
| 280 |
+
cast(
|
| 281 |
+
ChatCompletionUserMessageParam,
|
| 282 |
+
{"role": history[-1]["role"], "content": history[-1]["content"]},
|
| 283 |
+
)
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
# Initialize assistant's response
|
| 287 |
+
history.append({"role": "assistant", "content": ""})
|
| 288 |
+
|
| 289 |
+
# For debugging
|
| 290 |
+
yield history, gr.update(
|
| 291 |
+
value=f"Generating response with {emotion_analysis.emotion} emotion..."
|
| 292 |
+
), None, None
|
| 293 |
+
|
| 294 |
+
# Get character's response
|
| 295 |
+
response = client.chat.completions.create(
|
| 296 |
+
model="gpt-4o", messages=messages, stream=True
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
# Collect the full response
|
| 300 |
+
full_response = ""
|
| 301 |
+
for chunk in response:
|
| 302 |
+
if chunk.choices[0].delta.content:
|
| 303 |
+
content = chunk.choices[0].delta.content
|
| 304 |
+
full_response += content
|
| 305 |
+
history[-1]["content"] = full_response
|
| 306 |
+
yield history, gr.update(
|
| 307 |
+
value=f"Generating response... Length: {len(full_response)}"
|
| 308 |
+
), None, None
|
| 309 |
+
time.sleep(0.02)
|
| 310 |
+
|
| 311 |
+
# Final yield with debug info
|
| 312 |
+
yield history, gr.update(value=debug_msg), gr.update(
|
| 313 |
+
value=emotion_image
|
| 314 |
+
), gr.update(value=emotion_text)
|
| 315 |
+
|
| 316 |
+
except Exception as e:
|
| 317 |
+
history[-1]["content"] = "Sorry, I encountered an error. Please try again."
|
| 318 |
+
yield history, gr.update(value=f"Error: {str(e)}"), None, None
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def create_demo():
|
| 322 |
+
with gr.Blocks(
|
| 323 |
+
title=SPACE_TITLE,
|
| 324 |
+
css="""
|
| 325 |
+
.gradio-container {max-width: 1200px !important}
|
| 326 |
+
.emotion-state {font-size: 1.1em; padding: 8px; border-radius: 8px; background: #f7f7f7}
|
| 327 |
+
""",
|
| 328 |
+
) as demo:
|
| 329 |
+
gr.HTML(
|
| 330 |
+
f"""
|
| 331 |
+
<div style="text-align: center; max-width: 1200px; margin: 0 auto;">
|
| 332 |
+
<h1 style="font-size: 2.5rem; font-weight: 600;">{SPACE_TITLE} {SPACE_EMOJI}</h1>
|
| 333 |
+
<p style="font-size: 1.2rem; margin: 1rem;">Welcome to the Boba Shop! You're the friendly barista, ready to chat with some unique Australian animals. Each has their own personality and story to share.</p>
|
| 334 |
+
</div>
|
| 335 |
+
"""
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Header
|
| 339 |
+
gr.Image(
|
| 340 |
+
value="images/header_placeholder.png",
|
| 341 |
+
show_label=False,
|
| 342 |
+
container=False,
|
| 343 |
+
height=300,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
with gr.Row():
|
| 347 |
+
# Left Column - Character Details
|
| 348 |
+
with gr.Column(scale=1):
|
| 349 |
+
character = gr.Radio(
|
| 350 |
+
choices=list(CHARACTERS.keys()),
|
| 351 |
+
label="Select your animal friend",
|
| 352 |
+
value=None,
|
| 353 |
+
container=True,
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
image = gr.Image(
|
| 357 |
+
value=None,
|
| 358 |
+
label="Animal Friend",
|
| 359 |
+
show_label=False,
|
| 360 |
+
height=300,
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
emotion_state = gr.Textbox(
|
| 364 |
+
label="Current Emotional State",
|
| 365 |
+
value="",
|
| 366 |
+
interactive=False,
|
| 367 |
+
show_label=True,
|
| 368 |
+
elem_classes=["emotion-state"],
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
# Character details editor
|
| 372 |
+
with gr.Accordion("Character Details", open=True):
|
| 373 |
+
char_name = gr.Textbox(
|
| 374 |
+
label="Name",
|
| 375 |
+
interactive=True,
|
| 376 |
+
)
|
| 377 |
+
char_system_prompt = gr.Textbox(
|
| 378 |
+
label="System Prompt",
|
| 379 |
+
interactive=True,
|
| 380 |
+
lines=5,
|
| 381 |
+
)
|
| 382 |
+
char_initial_msg = gr.Textbox(
|
| 383 |
+
label="Initial Message",
|
| 384 |
+
interactive=True,
|
| 385 |
+
lines=3,
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
# Right Column - Chat Interface
|
| 389 |
+
with gr.Column(scale=2):
|
| 390 |
+
chatbot = gr.Chatbot(
|
| 391 |
+
label="Chat",
|
| 392 |
+
show_label=False,
|
| 393 |
+
type="messages",
|
| 394 |
+
height=600,
|
| 395 |
+
container=True,
|
| 396 |
+
)
|
| 397 |
+
msg = gr.Textbox(
|
| 398 |
+
label="Your message (you are the boba shop barista)",
|
| 399 |
+
placeholder="Type your message here and press Enter or click Submit...",
|
| 400 |
+
container=True,
|
| 401 |
+
)
|
| 402 |
+
with gr.Row():
|
| 403 |
+
submit = gr.Button("Submit", variant="primary", scale=2)
|
| 404 |
+
clear = gr.ClearButton([msg, chatbot], scale=1)
|
| 405 |
+
|
| 406 |
+
gr.HTML(
|
| 407 |
+
"""
|
| 408 |
+
<div style="text-align: center; margin-top: 20px; padding: 20px; border-top: 1px solid #ddd;">
|
| 409 |
+
<p>Created with ❤️ using Gradio and OpenAI. <a href="https://github.com/your-username/boba-shop-chat" target="_blank">View on GitHub</a></p>
|
| 410 |
+
</div>
|
| 411 |
+
"""
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
# Footer - Debug Panel
|
| 415 |
+
with gr.Accordion("Debug Information", open=False):
|
| 416 |
+
debug_history = gr.TextArea(
|
| 417 |
+
label="Debug History",
|
| 418 |
+
value="Debug information will appear here...\n",
|
| 419 |
+
interactive=False,
|
| 420 |
+
lines=10,
|
| 421 |
+
)
|
| 422 |
+
debug = gr.Textbox(
|
| 423 |
+
label="Latest Debug",
|
| 424 |
+
value="",
|
| 425 |
+
interactive=False,
|
| 426 |
+
visible=False,
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
# Update character details when selection changes
|
| 430 |
+
def on_character_select(char):
|
| 431 |
+
"""Handle character selection"""
|
| 432 |
+
if not char:
|
| 433 |
+
return None, [], "", "", "", "", "No character selected"
|
| 434 |
+
|
| 435 |
+
character_data = CHARACTERS[char]
|
| 436 |
+
return (
|
| 437 |
+
character_data["image"],
|
| 438 |
+
start_chat(char),
|
| 439 |
+
character_data["name"],
|
| 440 |
+
character_data["system_prompt"],
|
| 441 |
+
character_data["initial_message"],
|
| 442 |
+
"Neutral", # Initial emotion state
|
| 443 |
+
f"Selected character: {char}",
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
character.change(
|
| 447 |
+
on_character_select,
|
| 448 |
+
inputs=[character],
|
| 449 |
+
outputs=[
|
| 450 |
+
image,
|
| 451 |
+
chatbot,
|
| 452 |
+
char_name,
|
| 453 |
+
char_system_prompt,
|
| 454 |
+
char_initial_msg,
|
| 455 |
+
emotion_state,
|
| 456 |
+
debug_history,
|
| 457 |
+
],
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Update debug history
|
| 461 |
+
def update_debug_history(history: str, new_debug: str) -> str:
|
| 462 |
+
"""Append new debug message to history"""
|
| 463 |
+
timestamp = time.strftime("%H:%M:%S")
|
| 464 |
+
return f"{history}\n[{timestamp}] {new_debug}"
|
| 465 |
+
|
| 466 |
+
# Handle chat messages
|
| 467 |
+
submit_click = (
|
| 468 |
+
submit.click(
|
| 469 |
+
user,
|
| 470 |
+
[msg, chatbot, character],
|
| 471 |
+
[chatbot],
|
| 472 |
+
queue=False,
|
| 473 |
+
)
|
| 474 |
+
.then(
|
| 475 |
+
bot,
|
| 476 |
+
[chatbot, character, debug],
|
| 477 |
+
[chatbot, debug, image, emotion_state],
|
| 478 |
+
)
|
| 479 |
+
.then(
|
| 480 |
+
update_debug_history,
|
| 481 |
+
[debug_history, debug],
|
| 482 |
+
[debug_history],
|
| 483 |
+
)
|
| 484 |
+
.then(
|
| 485 |
+
lambda: gr.Textbox(value=""),
|
| 486 |
+
None,
|
| 487 |
+
[msg],
|
| 488 |
+
)
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
# Also trigger submit when pressing enter in the message box
|
| 492 |
+
msg.submit(
|
| 493 |
+
user,
|
| 494 |
+
[msg, chatbot, character],
|
| 495 |
+
[chatbot],
|
| 496 |
+
queue=False,
|
| 497 |
+
).then(
|
| 498 |
+
bot,
|
| 499 |
+
[chatbot, character, debug],
|
| 500 |
+
[chatbot, debug, image, emotion_state],
|
| 501 |
+
).then(
|
| 502 |
+
update_debug_history,
|
| 503 |
+
[debug_history, debug],
|
| 504 |
+
[debug_history],
|
| 505 |
+
).then(
|
| 506 |
+
lambda: gr.Textbox(value=""),
|
| 507 |
+
None,
|
| 508 |
+
[msg],
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
return demo
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
if __name__ == "__main__":
|
| 515 |
+
demo = create_demo()
|
| 516 |
+
demo.queue()
|
| 517 |
+
if os.getenv("SPACE_ID"):
|
| 518 |
+
# Running on HF Spaces
|
| 519 |
+
demo.launch()
|
| 520 |
+
else:
|
| 521 |
+
# Running locally
|
| 522 |
+
demo.launch(
|
| 523 |
+
share=True,
|
| 524 |
+
server_name="0.0.0.0",
|
| 525 |
+
server_port=7860,
|
| 526 |
+
debug=True,
|
| 527 |
+
)
|
images/echidna_angry.png
ADDED
|
Git LFS Details
|
images/echidna_happy.png
ADDED
|
Git LFS Details
|
images/echidna_neutral.png
ADDED
|
Git LFS Details
|
images/echidna_sad.png
ADDED
|
Git LFS Details
|
images/echidna_surprised.png
ADDED
|
Git LFS Details
|
images/header_placeholder.png
ADDED
|
Git LFS Details
|
images/kangaroo_angry.png
ADDED
|
images/kangaroo_happy.png
ADDED
|
images/kangaroo_neutral.png
ADDED
|
images/kangaroo_sad.png
ADDED
|
images/kangaroo_surprised.png
ADDED
|
images/platypus_angry.png
ADDED
|
images/platypus_happy.png
ADDED
|
images/platypus_neutral.png
ADDED
|
images/platypus_sad.png
ADDED
|
images/platypus_surprised.png
ADDED
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.19.2
|
| 2 |
+
openai==1.12.0
|
| 3 |
+
instructor==0.5.2
|
| 4 |
+
pydantic==2.6.1
|
| 5 |
+
python-dotenv==1.0.1
|
| 6 |
+
replicate
|
utils.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 3 |
+
|
| 4 |
+
def create_placeholder_images():
|
| 5 |
+
# Create images directory if it doesn't exist
|
| 6 |
+
os.makedirs("images", exist_ok=True)
|
| 7 |
+
|
| 8 |
+
# Define characters and emotions
|
| 9 |
+
characters = ["echidna", "platypus", "kangaroo"]
|
| 10 |
+
emotions = ["neutral", "happy", "sad", "surprised","angry"]
|
| 11 |
+
|
| 12 |
+
# Create a 400x400 image for each character/emotion combination
|
| 13 |
+
for character in characters:
|
| 14 |
+
for emotion in emotions:
|
| 15 |
+
# Create new image with white background
|
| 16 |
+
img = Image.new('RGB', (400, 400), 'white')
|
| 17 |
+
draw = ImageDraw.Draw(img)
|
| 18 |
+
|
| 19 |
+
# Add text to image
|
| 20 |
+
text = f"{character}\n{emotion}"
|
| 21 |
+
|
| 22 |
+
# Try to use a system font, fallback to default
|
| 23 |
+
try:
|
| 24 |
+
font = ImageFont.truetype("Arial", 40)
|
| 25 |
+
except:
|
| 26 |
+
font = ImageFont.load_default()
|
| 27 |
+
|
| 28 |
+
# Center the text
|
| 29 |
+
bbox = draw.textbbox((0, 0), text, font=font)
|
| 30 |
+
text_width = bbox[2] - bbox[0]
|
| 31 |
+
text_height = bbox[3] - bbox[1]
|
| 32 |
+
x = (400 - text_width) / 2
|
| 33 |
+
y = (400 - text_height) / 2
|
| 34 |
+
|
| 35 |
+
# Draw the text
|
| 36 |
+
draw.text((x, y), text, fill='black', font=font, align='center')
|
| 37 |
+
|
| 38 |
+
# Save the image
|
| 39 |
+
filename = f"images/{character}_{emotion}.png"
|
| 40 |
+
img.save(filename)
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
create_placeholder_images()
|