From faa6658b22417d8b1d6ef1117bb003b7c9011f6e Mon Sep 17 00:00:00 2001 From: chadhindsight Date: Mon, 11 Aug 2025 09:37:10 -0400 Subject: [PATCH 1/4] test bot working but not deployed on modal --- weatherbot.py | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 weatherbot.py diff --git a/weatherbot.py b/weatherbot.py new file mode 100644 index 0000000..7b6f30b --- /dev/null +++ b/weatherbot.py @@ -0,0 +1,188 @@ +""" +Weather Bot for Poe - Provides weather information using OpenWeather API +""" + +from __future__ import annotations +import asyncio +import json +from typing import AsyncIterable + +import fastapi_poe as fp +from fastapi_poe.types import QueryRequest, SettingsRequest, SettingsResponse +from modal import Image, App, asgi_app + + +# OpenWeather API Configuration +OPENWEATHER_API_KEY = "18436a5aee03555399b6774854293b06" +OPENWEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather" + + +class WeatherBot(fp.PoeBot): + async def get_response( + self, request: QueryRequest + ) -> AsyncIterable[fp.PartialResponse]: + """Handle user queries and return weather information""" + + user_message = request.query[-1].content.strip() + + # Check if user is asking for weather + if not self._is_weather_request(user_message): + yield fp.PartialResponse( + text="Hi! I'm a weather bot. Ask me about the weather in any city! " + "For example: 'What's the weather in New York?' or 'Weather in London'" + ) + return + + # Extract city name from the message + city_name = self._extract_city_name(user_message) + + if not city_name: + yield fp.PartialResponse( + text="I couldn't find a city name in your message. " + "Please ask like: 'What's the weather in [city name]?'" + ) + return + + # Get weather data + try: + weather_data = self._get_weather_data(city_name) + response_text = self._format_weather_response(weather_data) + yield fp.PartialResponse(text=response_text) + except Exception as e: + yield fp.PartialResponse( + text=f"Sorry, I couldn't get weather information for '{city_name}'. " + f"Please check the city name and try again. Error: {str(e)}" + ) + + def _is_weather_request(self, message: str) -> bool: + """Check if the message is asking for weather information""" + weather_keywords = [ + 'weather', 'temperature', 'temp', 'forecast', 'climate', + 'hot', 'cold', 'rain', 'sunny', 'cloudy', 'humidity' + ] + message_lower = message.lower() + return any(keyword in message_lower for keyword in weather_keywords) + + def _extract_city_name(self, message: str) -> str: + """Extract city name from user message""" + message_lower = message.lower() + + # Common patterns to look for + patterns = [ + ' in ', ' for ', ' at ', ' of ' + ] + + city_name = "" + + # Look for patterns like "weather in New York" + for pattern in patterns: + if pattern in message_lower: + parts = message_lower.split(pattern) + if len(parts) > 1: + # Get the part after the pattern and clean it up + city_part = parts[1].strip() + # Remove common trailing words + city_part = city_part.replace('?', '').replace('.', '').replace('!', '') + # Take first few words as city name + city_words = city_part.split()[:3] # Max 3 words for city name + city_name = ' '.join(city_words).strip() + break + + # If no pattern found, try to extract from the end of the message + if not city_name: + words = message.split() + if len(words) >= 2: + # Take last 1-2 words as potential city name + city_name = ' '.join(words[-2:]).replace('?', '').replace('.', '').replace('!', '').strip() + + return city_name.title() # Capitalize properly + + def _get_weather_data(self, city_name: str) -> dict: + """Fetch weather data from OpenWeather API""" + return get_weather_api_data(city_name, OPENWEATHER_API_KEY) + + def _format_weather_response(self, weather_data: dict) -> str: + """Format weather data into a user-friendly response""" + city = weather_data['name'] + country = weather_data['sys']['country'] + + # Main weather info + main = weather_data['main'] + weather = weather_data['weather'][0] + wind = weather_data.get('wind', {}) + + # Temperature + temp = round(main['temp']) + feels_like = round(main['feels_like']) + temp_min = round(main['temp_min']) + temp_max = round(main['temp_max']) + + # Convert to Fahrenheit for US users (optional) + temp_f = round(temp * 9/5 + 32) + feels_like_f = round(feels_like * 9/5 + 32) + + # Weather description + description = weather['description'].title() + + # Additional info + humidity = main['humidity'] + pressure = main['pressure'] + wind_speed = wind.get('speed', 0) + + # Format the response + response = f"""šŸŒ¤ļø **Weather in {city}, {country}** + +šŸ“Š **Current Conditions:** +• Temperature: {temp}°C ({temp_f}°F) +• Feels like: {feels_like}°C ({feels_like_f}°F) +• Condition: {description} +• High/Low: {temp_max}°C / {temp_min}°C + +šŸ’Ø **Additional Details:** +• Humidity: {humidity}% +• Pressure: {pressure} hPa +• Wind Speed: {wind_speed} m/s""" + + return response + + async def get_settings(self, setting: SettingsRequest) -> SettingsResponse: + """Return bot settings""" + return SettingsResponse( + server_bot_dependencies={"requests": "2.31.0"} + ) + + +# Modal deployment setup +image = Image.debian_slim().pip_install("fastapi-poe==0.0.68", "requests==2.31.0") +app = App("weatherbot") + +# Move imports inside the function to avoid Modal import issues +def get_weather_api_data(city_name: str, api_key: str) -> dict: + """Helper function to fetch weather data""" + import requests + + params = { + 'q': city_name, + 'appid': api_key, + 'units': 'metric' + } + + response = requests.get("http://api.openweathermap.org/data/2.5/weather", params=params) + + if response.status_code == 404: + raise Exception(f"City '{city_name}' not found") + elif response.status_code == 401: + raise Exception("API key is invalid") + elif response.status_code != 200: + raise Exception(f"API error: {response.status_code}") + + return response.json() + + +@app.function(image=image) +@asgi_app() +def fastapi_app(): + bot = WeatherBot() + # Note: You'll need to replace these with your actual Poe bot credentials + app = fp.make_app(bot, allow_without_key=True, access_key="vK0cjDYarSYZrhDry1K6eetf1Y8bNT9G") # Temporary for testing + return app \ No newline at end of file From f0a5d79abd1da1a2cdd3e5d21d45376928b61556 Mon Sep 17 00:00:00 2001 From: chadhindsight Date: Mon, 11 Aug 2025 17:37:27 -0400 Subject: [PATCH 2/4] both regular weather and gardening are working --- SERVERBOT.md | 231 ++++++++++++++++++++++ garden_weather.py | 476 ++++++++++++++++++++++++++++++++++++++++++++++ weatherbot.py | 117 +++++------- 3 files changed, 757 insertions(+), 67 deletions(-) create mode 100644 SERVERBOT.md create mode 100644 garden_weather.py diff --git a/SERVERBOT.md b/SERVERBOT.md new file mode 100644 index 0000000..3d7db0e --- /dev/null +++ b/SERVERBOT.md @@ -0,0 +1,231 @@ +# How to Create Your Own Poe Server Bot +*A step-by-step guide* + +## What You're Building +You'll create a **server bot** - a custom AI chatbot that runs your own code and can do things regular Poe bots can't, like remember conversations, connect to websites, or process data in unique ways. + +**Server Bot vs Regular Bot:** +- **Regular Bot**: Just a prompt that Poe runs +- **Server Bot**: Your own code running on a server that Poe connects to + +**Why Modal?** Modal runs your Python code in the cloud 24/7, so your bot works without keeping your computer on. + +--- + +## Step 1: Project Setup + +### Get the Template Code +In your terminal, run these commands: +```bash +git clone https://github.com/poe-platform/server-bot-quick-start +cd server-bot-quick-start +pip3 install -r requirements.txt +``` + +*What this does: Downloads example bot code and installs the tools you need* + +### Choose Your Starting Point +The folder contains several example bots. For your custom bot, you can either: +- **Start simple**: Modify `echobot.py` (just repeats what users say) +- **Start advanced**: Copy one of the other examples that's closer to what you want + +### Create Your Custom Bot +1. **Copy an example**: `cp echobot.py my_bot.py` +2. **Open in your editor**: Open `my_bot.py` in VS Code, Sublime, or any text editor +3. **Customize the logic**: Find the `get_response` method and change what your bot does + +**Basic customization example:** +```python +async def get_response(self, query: QueryRequest) -> AsyncIterable[fp.PartialResponse]: + user_message = query.query[-1].content + + # Replace this echo logic with YOUR bot's logic + if "hello" in user_message.lower(): + response = "Hi there! I'm your custom bot." + elif "help" in user_message.lower(): + response = "I can help with [YOUR BOT'S SPECIFIC FEATURES]" + else: + response = f"Interesting! You said: {user_message}" + + yield fp.PartialResponse(text=response) +``` + +--- + +## Step 2: Deploying Your Bot + +### Install Modal +```bash +pip3 install modal +``` + +### Setup Modal Access +Run this command (you only do this once): +```bash +modal token new --source poe +``` + +*What happens: Opens your browser to connect your terminal to Modal with your GitHub account* + +### Deploy Your Bot +From the `server-bot-quick-start` directory, run: +```bash +modal serve my_bot.py +``` + +*What this does: Runs your bot temporarily for testing. Look for a URL like:* +``` +https://yourname--my-bot-fastapi-app-dev.modal.run +``` + +**Copy this URL** - you'll need it next! + +--- + +## Step 3: Connect to Poe + +### Create Your Poe Bot +1. Go to [poe.com/create_bot](https://poe.com/create_bot) +2. Select **"Server bot"** +3. Fill out the form: + - **Bot Name**: Choose something descriptive + - **Server URL**: Paste your URL from Step 2 + - **Description**: Explain what your bot does +4. Click **"Create Bot"** + +**Important:** Copy down the **Bot Name** and **Access Key** that appear! + +### Configure Your Credentials +1. Open your `my_bot.py` file +2. Find this line near the bottom: + ```python + app = fp.make_app(bot, allow_without_key=True) + ``` +3. Replace it with: + ```python + app = fp.make_app(bot, access_key="YOUR_ACCESS_KEY", bot_name="YOUR_BOT_NAME") + ``` +4. Replace with your actual values from Poe +5. Save the file + +*What this does: Securely connects your code to your Poe bot* + +### Test Your Bot +Modal automatically updates when you save. Go to Poe and try talking to your bot! + +--- + +## Step 4: Make It Permanent + +When your bot is working correctly: + +### Deploy for Production +```bash +modal deploy my_bot.py +``` + +*What this does: Makes your bot run 24/7 instead of just while testing* + +### Update Your Poe Bot +1. Copy the new permanent URL (won't have `-dev` in it) +2. Go to your Poe bot settings +3. Update the **Server URL** with the permanent URL +4. Click **"Run check"** to verify it works + +--- + +## Customizing Your Bot + +### Add Conversation Memory +```python +class MyBot(fp.PoeBot): + def __init__(self): + super().__init__() + self.conversations = {} # Remember what users said +``` + +### Connect to Free APIs +Add real-world data to make your bot more useful: + +**Weather APIs:** +- OpenWeatherMap (free tier): Current weather for any city +- WeatherAPI: Weather forecasts and conditions + +**Location & Maps:** +- Nominatim (OpenStreetMap): Convert addresses to coordinates +- REST Countries: Country information and data + +**Transportation:** +- OpenTripPlanner: Public transit directions +- Overpass API: Real-time map data + +**Useful Data:** +- JSONPlaceholder: Fake data for testing +- REST Countries: Country facts and statistics +- Dog CEO API: Random dog pictures +- JokeAPI: Programming and general jokes + +**Example API call in your bot:** +```python +import httpx # Add this import at the top + +async def get_response(self, query: QueryRequest) -> AsyncIterable[fp.PartialResponse]: + user_message = query.query[-1].content + + if "dog" in user_message.lower(): + async with httpx.AsyncClient() as client: + response = await client.get("https://dog.ceo/api/breeds/image/random") + data = response.json() + bot_response = f"Here's a random dog! {data['message']}" + yield fp.PartialResponse(text=bot_response) +``` + +### Smart Responses +```python +# Check for multiple conditions +if "math" in user_message and any(op in user_message for op in ["+", "-", "*", "/"]): + # Do math +elif user_message.endswith("?"): + # Handle questions differently +elif len(user_message.split()) > 10: + # Handle long messages +``` + +--- + +## Common Issues + +**"Command not found" for modal**: Make sure pip3 installed correctly + +**Bot not responding on Poe**: Check your access key and bot name are exact matches + +**Changes not showing**: Save your file - Modal auto-updates + +**URL stopped working**: Redeploy and update the Server URL in Poe settings + +--- + +## Next Steps + +Once your basic bot works: + +1. **Add unique features** that make your bot special +2. **Handle edge cases** (what if users say unexpected things?) +3. **Test thoroughly** with friends before sharing widely +4. **Iterate based on feedback** from real users + +### Advanced Features to Explore +- File upload processing +- Calling other Poe bots from your bot +- Database integration +- Complex conversation flows +- API integrations + +--- + +## Resources +- [Poe Documentation](https://creator.poe.com/docs) +- [Modal Documentation](https://modal.com/docs) +- [fastapi-poe Library](https://pypi.org/project/fastapi-poe/) + +Your server bot is now live and running in the cloud! šŸŽ‰ \ No newline at end of file diff --git a/garden_weather.py b/garden_weather.py new file mode 100644 index 0000000..16ac500 --- /dev/null +++ b/garden_weather.py @@ -0,0 +1,476 @@ +""" +Garden Weather Bot for Poe - Combines plant advice with weather information +""" + +from __future__ import annotations +import os +import asyncio +from typing import AsyncIterable +from enum import Enum + +import fastapi_poe as fp +from fastapi_poe.types import QueryRequest, SettingsRequest, SettingsResponse +from modal import Image, App, asgi_app, Secret + + +# ---------------------------------------- +# Helper function to safely get API key +# ---------------------------------------- +def get_openweather_api_key() -> str: + api_key = os.environ.get("OPENWEATHER_API_KEY") + if not api_key: + raise RuntimeError( + "OpenWeather API key not found in environment variables! " + "Make sure you set it as a Modal secret." + ) + return api_key + +OPENWEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather" + + +class UserState(Enum): + GREETING = "greeting" + EXPERIENCE_LEVEL = "experience_level" + WATERING_PREFERENCE = "watering_preference" + SUNLIGHT_PREFERENCE = "sunlight_preference" + RECOMMENDATION = "recommendation" + FOLLOW_UP = "follow_up" + GENERAL = "general" + + +class GardenWeatherBot(fp.PoeBot): + def __init__(self): + super().__init__() + # Simple state tracking (in production, you'd use a proper database) + self.user_states = {} + self.user_preferences = {} + + # Plant knowledge base + self.easy_plants = { + "low_water_low_sun": { + "name": "Snake Plant", + "description": "Sometimes called mother-in-law's tongue, this tough succulent grows well in just about any indoor space. Its upright, leathery, sword-shaped leaves are marbled with gray-green colors and may be edged with yellow or white.", + "care": "Very forgiving if you forget to water it, and grows in low to medium light." + }, + "low_water_high_sun": { + "name": "Jade Plant", + "description": "An easy succulent with green, plump leaves and fleshy stems. This houseplant prefers bright light but can handle some shade.", + "care": "Very forgiving if you forget to water it for a while, but doesn't appreciate overwatering. Can live for many decades!" + }, + "high_water_low_sun": { + "name": "Peace Lily", + "description": "A low-maintenance indoor plant with glossy, lance-shaped leaves that arch gracefully. The white flowers are most common in summer.", + "care": "Tolerates low light, low humidity, and inconsistent watering. Perfect for office spaces!" + }, + "high_water_high_sun": { + "name": "Money Plant", + "description": "The shiny leaves look tropical and the slender trunk often comes braided. In Asia, these are thought to bring good fortune!", + "care": "Easy to grow and does best with consistent water and bright light. A perfect prosperity plant!" + } + } + + self.advanced_plants = { + "low_water_low_sun": { + "name": "String of Pearls", + "description": "A unique succulent with pearl-like leaves that cascade beautifully. Very Instagram-worthy!", + "care": "Make sure soil is completely dry before watering, and keep in partial sun away from drafts." + }, + "low_water_high_sun": { + "name": "Fiddle Leaf Fig", + "description": "The Instagram star of houseplants! Large, violin-shaped leaves make a dramatic statement.", + "care": "Needs plenty of humidity, indirect bright sunlight, moist soil, and infrequent waterings. Worth the challenge!" + }, + "high_water_low_sun": { + "name": "Maidenhair Fern", + "description": "Delicate, lacy fronds that bring an elegant, ethereal quality to any space.", + "care": "Loves misting, dappled light, and high humidity. A humid bathroom makes a perfect home!" + }, + "high_water_high_sun": { + "name": "Orchid", + "description": "The ultimate flowering houseplant challenge! Beautiful, exotic blooms that are worth the effort.", + "care": "Needs loose bark potting mix, indirect sunlight, high humidity, and careful watering. A true plant pro's prize!" + } + } + + async def get_response( + self, request: QueryRequest + ) -> AsyncIterable[fp.PartialResponse]: + user_message = request.query[-1].content.strip() + user_id = request.user_id # Use this to track user state + + # Check if it's a weather request + if self._is_weather_request(user_message): + async for response in self._handle_weather_request(user_message): + yield response + return + + # Handle gardening conversation flow + current_state = self.user_states.get(user_id, UserState.GREETING) + + if current_state == UserState.GREETING: + async for response in self._handle_greeting(user_id, user_message): + yield response + elif current_state == UserState.EXPERIENCE_LEVEL: + async for response in self._handle_experience_level(user_id, user_message): + yield response + elif current_state == UserState.WATERING_PREFERENCE: + async for response in self._handle_watering_preference(user_id, user_message): + yield response + elif current_state == UserState.SUNLIGHT_PREFERENCE: + async for response in self._handle_sunlight_preference(user_id, user_message): + yield response + elif current_state == UserState.RECOMMENDATION: + async for response in self._handle_recommendation_response(user_id, user_message): + yield response + else: + async for response in self._handle_general_chat(user_id, user_message): + yield response + + async def _handle_greeting(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + """Handle initial greeting""" + greeting = """🌱 Welcome to your Garden Weather Assistant! šŸŒ¦ļø + +I'm here to help you find the perfect plant AND give you weather updates to keep your green friends happy! + +Whether you're just starting your plant parent journey or you're already a seasoned plant pro, I've got recommendations that'll make your space bloom with joy! 🌸 + +Are you a **Newbie** to plants or a **Plant Pro**? If you're not sure, just let me know and I'll help you figure it out! + +*Remember: I can also give you weather updates for your city - just ask about the weather anytime!* ā˜€ļøšŸŒ§ļø""" + + yield fp.PartialResponse(text=greeting) + self.user_states[user_id] = UserState.EXPERIENCE_LEVEL + + async def _handle_experience_level(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + """Handle experience level selection""" + message_lower = message.lower() + + if "newbie" in message_lower or "beginner" in message_lower or "new" in message_lower: + self.user_preferences[user_id] = {"level": "newbie"} + response = "Awesome! Every plant pro started as a newbie - you're planting the seeds of a beautiful journey! 🌱\n\nNow, let's talk watering - do you prefer plants that need **constant watering** or ones that **you can ignore** for a while? (Trust me, there's no wrong answer here!)" + self.user_states[user_id] = UserState.WATERING_PREFERENCE + elif "pro" in message_lower or "experienced" in message_lower or "advanced" in message_lower: + self.user_preferences[user_id] = {"level": "pro"} + response = "A seasoned plant parent! I love it! 🌿 You're ready to tackle some challenging beauties that'll really make your space shine.\n\nLet's dig into your preferences - do you prefer plants that need **constant attention with watering** or ones that are more **drought tolerant**?" + self.user_states[user_id] = UserState.WATERING_PREFERENCE + elif "not sure" in message_lower or "don't know" in message_lower or "help" in message_lower: + response = """No worries! Let me help you figure it out! šŸ¤” + +**Newbie**: You're new to plants, want something forgiving, and prefer low-maintenance green friends that won't judge you if you forget to water them occasionally. + +**Plant Pro**: You've successfully kept plants alive before, enjoy the challenge of caring for more demanding plants, and don't mind checking in on your green babies regularly. + +Which sounds more like you?""" + # Stay in the same state + else: + response = "I didn't quite catch that! Are you a **Newbie** or **Plant Pro**? Or would you like me to explain the difference? 🌱" + # Stay in the same state + + yield fp.PartialResponse(text=response) + + async def _handle_watering_preference(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + """Handle watering preference""" + message_lower = message.lower() + + if "constant" in message_lower or "regular" in message_lower or "often" in message_lower: + self.user_preferences[user_id]["watering"] = "high" + response = "Got it! You're ready to be a dedicated plant parent with regular watering duties! šŸ’§\n\nNow for the sunshine question - will you put your plant in an area that gets **a lot of sun** or somewhere with **lower light**?" + elif "ignore" in message_lower or "forget" in message_lower or "low" in message_lower or "drought" in message_lower: + self.user_preferences[user_id]["watering"] = "low" + response = "Perfect! Low-maintenance watering it is - your plant will be as chill as you are! šŸ˜Ž\n\nLast question - will your new green friend be living in an area that gets **a lot of sun** or somewhere with **lower light**?" + else: + response = "Let me rephrase that! Do you want a plant that needs **constant watering** (you check on it regularly) or one **you can ignore** for stretches of time? šŸ’§" + # Stay in the same state + yield fp.PartialResponse(text=response) + return + + self.user_states[user_id] = UserState.SUNLIGHT_PREFERENCE + yield fp.PartialResponse(text=response) + + async def _handle_sunlight_preference(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + """Handle sunlight preference and provide recommendation""" + message_lower = message.lower() + + if "lot" in message_lower or "high" in message_lower or "bright" in message_lower or "yes" in message_lower: + self.user_preferences[user_id]["sunlight"] = "high" + elif "low" in message_lower or "little" in message_lower or "shade" in message_lower or "no" in message_lower: + self.user_preferences[user_id]["sunlight"] = "low" + else: + response = "Just to be clear - will your plant be in a spot with **lots of sun** or **lower light**? ā˜€ļøšŸŒ™" + yield fp.PartialResponse(text=response) + return + + # Generate recommendation + preferences = self.user_preferences[user_id] + level = preferences["level"] + watering = preferences["watering"] + sunlight = preferences["sunlight"] + + # Select plant database + plant_db = self.easy_plants if level == "newbie" else self.advanced_plants + + # Create key for plant selection + key = f"{watering}_water_{sunlight}_sun" + selected_plant = plant_db[key] + + # Create recommendation with weather integration offer + recommendation = f"""🌟 **Perfect! I've got the ideal plant for you!** 🌟 + +**Meet the {selected_plant['name']}!** + +{selected_plant['description']} + +**Why this plant is perfect for you:** {selected_plant['care']} + +This beauty is going to thrive in your care! Plus, if you ever move it outdoors or want to know if natural rainfall will help with watering, just ask me about the weather in your city! šŸŒ¦ļø + +**What do you think – are you interested in this plant?** šŸ¤”""" + + self.user_states[user_id] = UserState.RECOMMENDATION + yield fp.PartialResponse(text=recommendation) + + async def _handle_recommendation_response(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + """Handle user's response to plant recommendation""" + message_lower = message.lower() + + if any(word in message_lower for word in ["yes", "interested", "love", "great", "perfect", "want"]): + response = """šŸŽ‰ **Fantastic choice!** You're going to be such a great plant parent! + +Here's a nugget of plant wisdom to get you started: *"The best time to plant was 20 years ago. The second best time is now!"* 🌱 + +**Pro tip:** Most houseplants prefer the same temperatures we do (60s-70s°F), but they don't handle dry air as well as we do. Keep them away from heaters and vents, and give them a light misting in winter when the air gets dry. + +Happy planting! 🌿 And remember, I'm always here if you need weather updates for your green friends! ā˜€ļøšŸŒ§ļø""" + + self.user_states[user_id] = UserState.GENERAL + else: + # Provide alternative recommendation + preferences = self.user_preferences[user_id] + level = preferences["level"] + + # Get a different plant (simple rotation) + plant_db = self.easy_plants if level == "newbie" else self.advanced_plants + plants = list(plant_db.values()) + + # Get the next plant in the list + current_plant_name = None + for key, plant in plant_db.items(): + if f"{preferences['watering']}_water_{preferences['sunlight']}_sun" == key: + current_plant_name = plant['name'] + break + + # Find alternative (just pick a different one for now) + alternative = None + for plant in plants: + if plant['name'] != current_plant_name: + alternative = plant + break + + if alternative: + response = f"""No worries! Let me suggest something different! šŸ”„ + +**How about the {alternative['name']}?** + +{alternative['description']} + +**Care requirements:** {alternative['care']} + +This one might be more your style! **What do you think about this option?** 🌿""" + else: + response = "No problem! Let me know what specific qualities you're looking for in a plant, and I'll help you find the perfect green companion! 🌱" + + yield fp.PartialResponse(text=response) + + async def _handle_general_chat(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + """Handle general plant or weather questions""" + message_lower = message.lower() + + plant_keywords = ["plant", "water", "care", "grow", "leaf", "soil", "pot", "garden"] + + if any(keyword in message_lower for keyword in plant_keywords): + response = """🌱 Great question! Here are some universal plant care tips: + +• **Watering wisdom:** Stick your finger about an inch into the soil. If it's moist, wait a few days before watering! +• **Light logic:** Most houseplants prefer bright, indirect light rather than direct sunlight +• **Temperature tip:** Keep plants away from vents, heaters, and radiators +• **Humidity help:** Lightly mist your plants daily in winter when the air is driest + +Want a specific plant recommendation? Just let me know your experience level and preferences! + +And don't forget - I can also check the weather in your city if you want to know about rain for any outdoor plants! šŸŒ¦ļø""" + else: + response = """🌿 I'm here to help with plants and weather! + +Ask me things like: +• "I'm a newbie looking for an easy plant" +• "What's the weather in [your city]?" +• "How do I care for houseplants?" +• "My plant leaves are turning yellow" + +What can I help you with today? šŸŒ±ā˜€ļø""" + + yield fp.PartialResponse(text=response) + + # Weather functionality + async def _handle_weather_request(self, message: str) -> AsyncIterable[fp.PartialResponse]: + """Handle weather requests""" + city_name = self._extract_city_name(message) + + if not city_name: + yield fp.PartialResponse( + text="I couldn't find a city name in your message. Please ask like: 'What's the weather in [city name]?' šŸŒ¦ļø" + ) + return + + try: + weather_data = self._get_weather_data(city_name) + response_text = self._format_weather_response(weather_data) + yield fp.PartialResponse(text=response_text) + except Exception as e: + yield fp.PartialResponse( + text=f"Sorry, I couldn't get weather information for '{city_name}'. Please check the city name and try again! šŸŒ§ļø" + ) + + def _is_weather_request(self, message: str) -> bool: + """Check if the message is asking for weather information""" + weather_keywords = [ + 'weather', 'temperature', 'temp', 'forecast', 'climate', + 'hot', 'cold', 'rain', 'sunny', 'cloudy', 'humidity' + ] + message_lower = message.lower() + return any(keyword in message_lower for keyword in weather_keywords) + + def _extract_city_name(self, message: str) -> str: + """Extract city name from user message""" + message_lower = message.lower() + patterns = [' in ', ' for ', ' at ', ' of '] + city_name = "" + + for pattern in patterns: + if pattern in message_lower: + parts = message_lower.split(pattern) + if len(parts) > 1: + city_part = parts[1].strip() + city_part = city_part.replace('?', '').replace('.', '').replace('!', '') + city_words = city_part.split()[:3] + city_name = ' '.join(city_words).strip() + break + + if not city_name: + words = message.split() + if len(words) >= 2: + city_name = ' '.join(words[-2:]).replace('?', '').replace('.', '').replace('!', '').strip() + + return city_name.title() + + def _get_weather_data(self, city_name: str) -> dict: + """Fetch weather data from OpenWeather API""" + return get_weather_api_data(city_name, get_openweather_api_key()) + + def _format_weather_response(self, weather_data: dict) -> str: + """Format weather data with plant care tips""" + city = weather_data['name'] + country = weather_data['sys']['country'] + + main = weather_data['main'] + weather = weather_data['weather'][0] + wind = weather_data.get('wind', {}) + + temp = round(main['temp']) + feels_like = round(main['feels_like']) + temp_min = round(main['temp_min']) + temp_max = round(main['temp_max']) + + temp_f = round(temp * 9 / 5 + 32) + feels_like_f = round(feels_like * 9 / 5 + 32) + + description = weather['description'].title() + humidity = main['humidity'] + pressure = main['pressure'] + wind_speed = wind.get('speed', 0) + + # Add plant care advice based on weather + plant_advice = self._get_plant_advice_from_weather(weather_data) + + response = f"""šŸŒ¤ļø **Weather in {city}, {country}** + +šŸ“Š **Current Conditions:** +• Temperature: {temp}°C ({temp_f}°F) +• Feels like: {feels_like}°C ({feels_like_f}°F) +• Condition: {description} +• High/Low: {temp_max}°C / {temp_min}°C + +šŸ’Ø **Additional Details:** +• Humidity: {humidity}% +• Pressure: {pressure} hPa +• Wind Speed: {wind_speed} m/s + +🌱 **Plant Care Tip Based on Today's Weather:** +{plant_advice}""" + + return response + + def _get_plant_advice_from_weather(self, weather_data: dict) -> str: + """Generate plant care advice based on current weather""" + description = weather_data['weather'][0]['description'].lower() + temp = weather_data['main']['temp'] + humidity = weather_data['main']['humidity'] + + if 'rain' in description or 'drizzle' in description: + return "šŸŒ§ļø It's rainy! Perfect time to skip watering outdoor plants - Mother Nature's got this covered! Indoor plants might appreciate the extra humidity though." + elif 'snow' in description: + return "ā„ļø Snow day! Keep your outdoor plants protected and bring any tender potted plants inside. Indoor plants might need extra humidity due to heating." + elif temp > 30: # Very hot + return "šŸ”„ Hot weather alert! Your plants will be extra thirsty today. Check soil moisture frequently and provide shade for sensitive plants." + elif temp < 5: # Very cold + return "🄶 Chilly weather! Protect your outdoor plants from frost and keep indoor plants away from cold windows and drafts." + elif humidity < 30: + return "šŸœļø Low humidity today! Your indoor plants would love a gentle misting, and outdoor plants might need extra water." + elif humidity > 80: + return "šŸ’§ High humidity today! Great for your plants' happiness! You might be able to water less frequently." + elif 'clear' in description or 'sunny' in description: + return "ā˜€ļø Beautiful sunny day! Perfect weather for your plants to photosynthesize. Check that they're not getting too much direct sun though!" + else: + return "šŸŒ¤ļø Lovely weather for plants today! A good day to check on your green friends and see how they're doing." + + async def get_settings(self, setting: SettingsRequest) -> SettingsResponse: + """Return bot settings""" + return SettingsResponse() + + +# Modal deployment setup +image = Image.debian_slim().pip_install("fastapi-poe==0.0.68", "requests==2.31.0") +app = App("garden-weather-bot") + + +def get_weather_api_data(city_name: str, api_key: str) -> dict: + """Helper function to fetch weather data""" + import requests + + params = { + 'q': city_name, + 'appid': api_key, + 'units': 'metric' + } + + response = requests.get(OPENWEATHER_BASE_URL, params=params) + + if response.status_code == 404: + raise Exception(f"City '{city_name}' not found") + elif response.status_code == 401: + raise Exception("API key is invalid") + elif response.status_code != 200: + raise Exception(f"API error: {response.status_code}") + + return response.json() + + +@app.function( + image=image, + secrets=[Secret.from_name("OPENWEATHER_API_KEY")] +) +@asgi_app() +def fastapi_app(): + bot = GardenWeatherBot() + # You'll need to replace this with your actual Poe bot credentials + return fp.make_app(bot, allow_without_key=True) # Change for production \ No newline at end of file diff --git a/weatherbot.py b/weatherbot.py index 7b6f30b..91834f3 100644 --- a/weatherbot.py +++ b/weatherbot.py @@ -3,17 +3,27 @@ """ from __future__ import annotations +import os import asyncio -import json from typing import AsyncIterable import fastapi_poe as fp from fastapi_poe.types import QueryRequest, SettingsRequest, SettingsResponse -from modal import Image, App, asgi_app - +from modal import Image, App, asgi_app, Secret + +# ---------------------------------------- +# Helper function to safely get API key +# ---------------------------------------- +def get_openweather_api_key() -> str: + api_key = os.environ.get("OPENWEATHER_API_KEY") + if not api_key: + raise RuntimeError( + "OpenWeather API key not found in environment variables! " + "Make sure you set it as a Modal secret with:\n" + "modal secret set OPENWEATHER_API_KEY YOUR_API_KEY" + ) + return api_key -# OpenWeather API Configuration -OPENWEATHER_API_KEY = "18436a5aee03555399b6774854293b06" OPENWEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather" @@ -21,115 +31,88 @@ class WeatherBot(fp.PoeBot): async def get_response( self, request: QueryRequest ) -> AsyncIterable[fp.PartialResponse]: - """Handle user queries and return weather information""" - user_message = request.query[-1].content.strip() - - # Check if user is asking for weather + if not self._is_weather_request(user_message): yield fp.PartialResponse( text="Hi! I'm a weather bot. Ask me about the weather in any city! " "For example: 'What's the weather in New York?' or 'Weather in London'" ) return - - # Extract city name from the message + city_name = self._extract_city_name(user_message) - + if not city_name: yield fp.PartialResponse( text="I couldn't find a city name in your message. " "Please ask like: 'What's the weather in [city name]?'" ) return - - # Get weather data + try: weather_data = self._get_weather_data(city_name) response_text = self._format_weather_response(weather_data) yield fp.PartialResponse(text=response_text) - except Exception as e: + except Exception: yield fp.PartialResponse( text=f"Sorry, I couldn't get weather information for '{city_name}'. " - f"Please check the city name and try again. Error: {str(e)}" + f"Please check the city name and try again." ) def _is_weather_request(self, message: str) -> bool: - """Check if the message is asking for weather information""" weather_keywords = [ 'weather', 'temperature', 'temp', 'forecast', 'climate', 'hot', 'cold', 'rain', 'sunny', 'cloudy', 'humidity' ] message_lower = message.lower() return any(keyword in message_lower for keyword in weather_keywords) - + def _extract_city_name(self, message: str) -> str: - """Extract city name from user message""" message_lower = message.lower() - - # Common patterns to look for - patterns = [ - ' in ', ' for ', ' at ', ' of ' - ] - + patterns = [' in ', ' for ', ' at ', ' of '] city_name = "" - - # Look for patterns like "weather in New York" + for pattern in patterns: if pattern in message_lower: parts = message_lower.split(pattern) if len(parts) > 1: - # Get the part after the pattern and clean it up city_part = parts[1].strip() - # Remove common trailing words city_part = city_part.replace('?', '').replace('.', '').replace('!', '') - # Take first few words as city name - city_words = city_part.split()[:3] # Max 3 words for city name + city_words = city_part.split()[:3] city_name = ' '.join(city_words).strip() break - - # If no pattern found, try to extract from the end of the message + if not city_name: words = message.split() if len(words) >= 2: - # Take last 1-2 words as potential city name city_name = ' '.join(words[-2:]).replace('?', '').replace('.', '').replace('!', '').strip() - - return city_name.title() # Capitalize properly - + + return city_name.title() + def _get_weather_data(self, city_name: str) -> dict: - """Fetch weather data from OpenWeather API""" - return get_weather_api_data(city_name, OPENWEATHER_API_KEY) - + return get_weather_api_data(city_name, get_openweather_api_key()) + def _format_weather_response(self, weather_data: dict) -> str: - """Format weather data into a user-friendly response""" city = weather_data['name'] country = weather_data['sys']['country'] - - # Main weather info + main = weather_data['main'] weather = weather_data['weather'][0] wind = weather_data.get('wind', {}) - - # Temperature + temp = round(main['temp']) feels_like = round(main['feels_like']) temp_min = round(main['temp_min']) temp_max = round(main['temp_max']) - - # Convert to Fahrenheit for US users (optional) - temp_f = round(temp * 9/5 + 32) - feels_like_f = round(feels_like * 9/5 + 32) - - # Weather description + + temp_f = round(temp * 9 / 5 + 32) + feels_like_f = round(feels_like * 9 / 5 + 32) + description = weather['description'].title() - - # Additional info humidity = main['humidity'] pressure = main['pressure'] wind_speed = wind.get('speed', 0) - - # Format the response + response = f"""šŸŒ¤ļø **Weather in {city}, {country}** šŸ“Š **Current Conditions:** @@ -146,7 +129,6 @@ def _format_weather_response(self, weather_data: dict) -> str: return response async def get_settings(self, setting: SettingsRequest) -> SettingsResponse: - """Return bot settings""" return SettingsResponse( server_bot_dependencies={"requests": "2.31.0"} ) @@ -156,33 +138,34 @@ async def get_settings(self, setting: SettingsRequest) -> SettingsResponse: image = Image.debian_slim().pip_install("fastapi-poe==0.0.68", "requests==2.31.0") app = App("weatherbot") -# Move imports inside the function to avoid Modal import issues + def get_weather_api_data(city_name: str, api_key: str) -> dict: - """Helper function to fetch weather data""" import requests - + params = { 'q': city_name, 'appid': api_key, 'units': 'metric' } - - response = requests.get("http://api.openweathermap.org/data/2.5/weather", params=params) - + + response = requests.get(OPENWEATHER_BASE_URL, params=params) + if response.status_code == 404: raise Exception(f"City '{city_name}' not found") elif response.status_code == 401: raise Exception("API key is invalid") elif response.status_code != 200: raise Exception(f"API error: {response.status_code}") - + return response.json() -@app.function(image=image) +@app.function( + image=image, + secrets=[Secret.from_name("OPENWEATHER_API_KEY")] +) @asgi_app() def fastapi_app(): bot = WeatherBot() - # Note: You'll need to replace these with your actual Poe bot credentials - app = fp.make_app(bot, allow_without_key=True, access_key="vK0cjDYarSYZrhDry1K6eetf1Y8bNT9G") # Temporary for testing - return app \ No newline at end of file + # Remove allow_without_key for production security + return fp.make_app(bot, access_key="vK0cjDYarSYZrhDry1K6eetf1Y8bNT9G") From 36898a7cea068cd378d87e508fc839b5dfc5158e Mon Sep 17 00:00:00 2001 From: chadhindsight Date: Mon, 11 Aug 2025 17:57:17 -0400 Subject: [PATCH 3/4] change readme --- SERVERBOT.md | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/SERVERBOT.md b/SERVERBOT.md index 3d7db0e..a2e7083 100644 --- a/SERVERBOT.md +++ b/SERVERBOT.md @@ -67,13 +67,13 @@ modal token new --source poe *What happens: Opens your browser to connect your terminal to Modal with your GitHub account* -### Deploy Your Bot +### Test Your Bot From the `server-bot-quick-start` directory, run: ```bash modal serve my_bot.py ``` -*What this does: Runs your bot temporarily for testing. Look for a URL like:* +*What this does: Runs your bot temporarily for testing (your terminal must stay open). Look for a URL like:* ``` https://yourname--my-bot-fastapi-app-dev.modal.run ``` @@ -203,25 +203,6 @@ elif len(user_message.split()) > 10: **URL stopped working**: Redeploy and update the Server URL in Poe settings ---- - -## Next Steps - -Once your basic bot works: - -1. **Add unique features** that make your bot special -2. **Handle edge cases** (what if users say unexpected things?) -3. **Test thoroughly** with friends before sharing widely -4. **Iterate based on feedback** from real users - -### Advanced Features to Explore -- File upload processing -- Calling other Poe bots from your bot -- Database integration -- Complex conversation flows -- API integrations - ---- ## Resources - [Poe Documentation](https://creator.poe.com/docs) From 04ea195e4ef1913b473b4c01f6b2898eaa8d41bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:25:34 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- SERVERBOT.md | 61 ++++++--- garden_weather.py | 307 +++++++++++++++++++++++++++++----------------- weatherbot.py | 82 +++++++------ 3 files changed, 285 insertions(+), 165 deletions(-) diff --git a/SERVERBOT.md b/SERVERBOT.md index a2e7083..df216a8 100644 --- a/SERVERBOT.md +++ b/SERVERBOT.md @@ -1,44 +1,56 @@ # How to Create Your Own Poe Server Bot -*A step-by-step guide* + +_A step-by-step guide_ ## What You're Building -You'll create a **server bot** - a custom AI chatbot that runs your own code and can do things regular Poe bots can't, like remember conversations, connect to websites, or process data in unique ways. + +You'll create a **server bot** - a custom AI chatbot that runs your own code and can do +things regular Poe bots can't, like remember conversations, connect to websites, or +process data in unique ways. **Server Bot vs Regular Bot:** + - **Regular Bot**: Just a prompt that Poe runs - **Server Bot**: Your own code running on a server that Poe connects to -**Why Modal?** Modal runs your Python code in the cloud 24/7, so your bot works without keeping your computer on. +**Why Modal?** Modal runs your Python code in the cloud 24/7, so your bot works without +keeping your computer on. --- ## Step 1: Project Setup ### Get the Template Code + In your terminal, run these commands: + ```bash git clone https://github.com/poe-platform/server-bot-quick-start cd server-bot-quick-start pip3 install -r requirements.txt ``` -*What this does: Downloads example bot code and installs the tools you need* +_What this does: Downloads example bot code and installs the tools you need_ ### Choose Your Starting Point + The folder contains several example bots. For your custom bot, you can either: + - **Start simple**: Modify `echobot.py` (just repeats what users say) - **Start advanced**: Copy one of the other examples that's closer to what you want ### Create Your Custom Bot + 1. **Copy an example**: `cp echobot.py my_bot.py` 2. **Open in your editor**: Open `my_bot.py` in VS Code, Sublime, or any text editor 3. **Customize the logic**: Find the `get_response` method and change what your bot does **Basic customization example:** + ```python async def get_response(self, query: QueryRequest) -> AsyncIterable[fp.PartialResponse]: user_message = query.query[-1].content - + # Replace this echo logic with YOUR bot's logic if "hello" in user_message.lower(): response = "Hi there! I'm your custom bot." @@ -46,7 +58,7 @@ async def get_response(self, query: QueryRequest) -> AsyncIterable[fp.PartialRes response = "I can help with [YOUR BOT'S SPECIFIC FEATURES]" else: response = f"Interesting! You said: {user_message}" - + yield fp.PartialResponse(text=response) ``` @@ -55,25 +67,33 @@ async def get_response(self, query: QueryRequest) -> AsyncIterable[fp.PartialRes ## Step 2: Deploying Your Bot ### Install Modal + ```bash pip3 install modal ``` ### Setup Modal Access + Run this command (you only do this once): + ```bash modal token new --source poe ``` -*What happens: Opens your browser to connect your terminal to Modal with your GitHub account* +_What happens: Opens your browser to connect your terminal to Modal with your GitHub +account_ ### Test Your Bot + From the `server-bot-quick-start` directory, run: + ```bash modal serve my_bot.py ``` -*What this does: Runs your bot temporarily for testing (your terminal must stay open). Look for a URL like:* +_What this does: Runs your bot temporarily for testing (your terminal must stay open). +Look for a URL like:_ + ``` https://yourname--my-bot-fastapi-app-dev.modal.run ``` @@ -85,6 +105,7 @@ https://yourname--my-bot-fastapi-app-dev.modal.run ## Step 3: Connect to Poe ### Create Your Poe Bot + 1. Go to [poe.com/create_bot](https://poe.com/create_bot) 2. Select **"Server bot"** 3. Fill out the form: @@ -96,6 +117,7 @@ https://yourname--my-bot-fastapi-app-dev.modal.run **Important:** Copy down the **Bot Name** and **Access Key** that appear! ### Configure Your Credentials + 1. Open your `my_bot.py` file 2. Find this line near the bottom: ```python @@ -108,9 +130,10 @@ https://yourname--my-bot-fastapi-app-dev.modal.run 4. Replace with your actual values from Poe 5. Save the file -*What this does: Securely connects your code to your Poe bot* +_What this does: Securely connects your code to your Poe bot_ ### Test Your Bot + Modal automatically updates when you save. Go to Poe and try talking to your bot! --- @@ -120,13 +143,15 @@ Modal automatically updates when you save. Go to Poe and try talking to your bot When your bot is working correctly: ### Deploy for Production + ```bash modal deploy my_bot.py ``` -*What this does: Makes your bot run 24/7 instead of just while testing* +_What this does: Makes your bot run 24/7 instead of just while testing_ ### Update Your Poe Bot + 1. Copy the new permanent URL (won't have `-dev` in it) 2. Go to your Poe bot settings 3. Update the **Server URL** with the permanent URL @@ -137,6 +162,7 @@ modal deploy my_bot.py ## Customizing Your Bot ### Add Conversation Memory + ```python class MyBot(fp.PoeBot): def __init__(self): @@ -145,33 +171,39 @@ class MyBot(fp.PoeBot): ``` ### Connect to Free APIs + Add real-world data to make your bot more useful: **Weather APIs:** + - OpenWeatherMap (free tier): Current weather for any city - WeatherAPI: Weather forecasts and conditions **Location & Maps:** + - Nominatim (OpenStreetMap): Convert addresses to coordinates - REST Countries: Country information and data **Transportation:** + - OpenTripPlanner: Public transit directions - Overpass API: Real-time map data **Useful Data:** + - JSONPlaceholder: Fake data for testing -- REST Countries: Country facts and statistics +- REST Countries: Country facts and statistics - Dog CEO API: Random dog pictures - JokeAPI: Programming and general jokes **Example API call in your bot:** + ```python import httpx # Add this import at the top async def get_response(self, query: QueryRequest) -> AsyncIterable[fp.PartialResponse]: user_message = query.query[-1].content - + if "dog" in user_message.lower(): async with httpx.AsyncClient() as client: response = await client.get("https://dog.ceo/api/breeds/image/random") @@ -181,6 +213,7 @@ async def get_response(self, query: QueryRequest) -> AsyncIterable[fp.PartialRes ``` ### Smart Responses + ```python # Check for multiple conditions if "math" in user_message and any(op in user_message for op in ["+", "-", "*", "/"]): @@ -203,10 +236,10 @@ elif len(user_message.split()) > 10: **URL stopped working**: Redeploy and update the Server URL in Poe settings - ## Resources + - [Poe Documentation](https://creator.poe.com/docs) - [Modal Documentation](https://modal.com/docs) - [fastapi-poe Library](https://pypi.org/project/fastapi-poe/) -Your server bot is now live and running in the cloud! šŸŽ‰ \ No newline at end of file +Your server bot is now live and running in the cloud! šŸŽ‰ diff --git a/garden_weather.py b/garden_weather.py index 16ac500..59a594e 100644 --- a/garden_weather.py +++ b/garden_weather.py @@ -3,14 +3,14 @@ """ from __future__ import annotations + import os -import asyncio -from typing import AsyncIterable from enum import Enum +from typing import AsyncIterable import fastapi_poe as fp from fastapi_poe.types import QueryRequest, SettingsRequest, SettingsResponse -from modal import Image, App, asgi_app, Secret +from modal import App, Image, Secret, asgi_app # ---------------------------------------- @@ -25,6 +25,7 @@ def get_openweather_api_key() -> str: ) return api_key + OPENWEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather" @@ -44,52 +45,52 @@ def __init__(self): # Simple state tracking (in production, you'd use a proper database) self.user_states = {} self.user_preferences = {} - + # Plant knowledge base self.easy_plants = { "low_water_low_sun": { "name": "Snake Plant", "description": "Sometimes called mother-in-law's tongue, this tough succulent grows well in just about any indoor space. Its upright, leathery, sword-shaped leaves are marbled with gray-green colors and may be edged with yellow or white.", - "care": "Very forgiving if you forget to water it, and grows in low to medium light." + "care": "Very forgiving if you forget to water it, and grows in low to medium light.", }, "low_water_high_sun": { "name": "Jade Plant", "description": "An easy succulent with green, plump leaves and fleshy stems. This houseplant prefers bright light but can handle some shade.", - "care": "Very forgiving if you forget to water it for a while, but doesn't appreciate overwatering. Can live for many decades!" + "care": "Very forgiving if you forget to water it for a while, but doesn't appreciate overwatering. Can live for many decades!", }, "high_water_low_sun": { "name": "Peace Lily", "description": "A low-maintenance indoor plant with glossy, lance-shaped leaves that arch gracefully. The white flowers are most common in summer.", - "care": "Tolerates low light, low humidity, and inconsistent watering. Perfect for office spaces!" + "care": "Tolerates low light, low humidity, and inconsistent watering. Perfect for office spaces!", }, "high_water_high_sun": { "name": "Money Plant", "description": "The shiny leaves look tropical and the slender trunk often comes braided. In Asia, these are thought to bring good fortune!", - "care": "Easy to grow and does best with consistent water and bright light. A perfect prosperity plant!" - } + "care": "Easy to grow and does best with consistent water and bright light. A perfect prosperity plant!", + }, } - + self.advanced_plants = { "low_water_low_sun": { "name": "String of Pearls", "description": "A unique succulent with pearl-like leaves that cascade beautifully. Very Instagram-worthy!", - "care": "Make sure soil is completely dry before watering, and keep in partial sun away from drafts." + "care": "Make sure soil is completely dry before watering, and keep in partial sun away from drafts.", }, "low_water_high_sun": { "name": "Fiddle Leaf Fig", "description": "The Instagram star of houseplants! Large, violin-shaped leaves make a dramatic statement.", - "care": "Needs plenty of humidity, indirect bright sunlight, moist soil, and infrequent waterings. Worth the challenge!" + "care": "Needs plenty of humidity, indirect bright sunlight, moist soil, and infrequent waterings. Worth the challenge!", }, "high_water_low_sun": { "name": "Maidenhair Fern", "description": "Delicate, lacy fronds that bring an elegant, ethereal quality to any space.", - "care": "Loves misting, dappled light, and high humidity. A humid bathroom makes a perfect home!" + "care": "Loves misting, dappled light, and high humidity. A humid bathroom makes a perfect home!", }, "high_water_high_sun": { "name": "Orchid", "description": "The ultimate flowering houseplant challenge! Beautiful, exotic blooms that are worth the effort.", - "care": "Needs loose bark potting mix, indirect sunlight, high humidity, and careful watering. A true plant pro's prize!" - } + "care": "Needs loose bark potting mix, indirect sunlight, high humidity, and careful watering. A true plant pro's prize!", + }, } async def get_response( @@ -97,16 +98,16 @@ async def get_response( ) -> AsyncIterable[fp.PartialResponse]: user_message = request.query[-1].content.strip() user_id = request.user_id # Use this to track user state - + # Check if it's a weather request if self._is_weather_request(user_message): async for response in self._handle_weather_request(user_message): yield response return - + # Handle gardening conversation flow current_state = self.user_states.get(user_id, UserState.GREETING) - + if current_state == UserState.GREETING: async for response in self._handle_greeting(user_id, user_message): yield response @@ -114,46 +115,68 @@ async def get_response( async for response in self._handle_experience_level(user_id, user_message): yield response elif current_state == UserState.WATERING_PREFERENCE: - async for response in self._handle_watering_preference(user_id, user_message): + async for response in self._handle_watering_preference( + user_id, user_message + ): yield response elif current_state == UserState.SUNLIGHT_PREFERENCE: - async for response in self._handle_sunlight_preference(user_id, user_message): + async for response in self._handle_sunlight_preference( + user_id, user_message + ): yield response elif current_state == UserState.RECOMMENDATION: - async for response in self._handle_recommendation_response(user_id, user_message): + async for response in self._handle_recommendation_response( + user_id, user_message + ): yield response else: async for response in self._handle_general_chat(user_id, user_message): yield response - async def _handle_greeting(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + async def _handle_greeting( + self, user_id: str, message: str + ) -> AsyncIterable[fp.PartialResponse]: """Handle initial greeting""" greeting = """🌱 Welcome to your Garden Weather Assistant! šŸŒ¦ļø -I'm here to help you find the perfect plant AND give you weather updates to keep your green friends happy! +I'm here to help you find the perfect plant AND give you weather updates to keep your green friends happy! Whether you're just starting your plant parent journey or you're already a seasoned plant pro, I've got recommendations that'll make your space bloom with joy! 🌸 -Are you a **Newbie** to plants or a **Plant Pro**? If you're not sure, just let me know and I'll help you figure it out! +Are you a **Newbie** to plants or a **Plant Pro**? If you're not sure, just let me know and I'll help you figure it out! *Remember: I can also give you weather updates for your city - just ask about the weather anytime!* ā˜€ļøšŸŒ§ļø""" - + yield fp.PartialResponse(text=greeting) self.user_states[user_id] = UserState.EXPERIENCE_LEVEL - async def _handle_experience_level(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + async def _handle_experience_level( + self, user_id: str, message: str + ) -> AsyncIterable[fp.PartialResponse]: """Handle experience level selection""" message_lower = message.lower() - - if "newbie" in message_lower or "beginner" in message_lower or "new" in message_lower: + + if ( + "newbie" in message_lower + or "beginner" in message_lower + or "new" in message_lower + ): self.user_preferences[user_id] = {"level": "newbie"} response = "Awesome! Every plant pro started as a newbie - you're planting the seeds of a beautiful journey! 🌱\n\nNow, let's talk watering - do you prefer plants that need **constant watering** or ones that **you can ignore** for a while? (Trust me, there's no wrong answer here!)" self.user_states[user_id] = UserState.WATERING_PREFERENCE - elif "pro" in message_lower or "experienced" in message_lower or "advanced" in message_lower: + elif ( + "pro" in message_lower + or "experienced" in message_lower + or "advanced" in message_lower + ): self.user_preferences[user_id] = {"level": "pro"} response = "A seasoned plant parent! I love it! 🌿 You're ready to tackle some challenging beauties that'll really make your space shine.\n\nLet's dig into your preferences - do you prefer plants that need **constant attention with watering** or ones that are more **drought tolerant**?" self.user_states[user_id] = UserState.WATERING_PREFERENCE - elif "not sure" in message_lower or "don't know" in message_lower or "help" in message_lower: + elif ( + "not sure" in message_lower + or "don't know" in message_lower + or "help" in message_lower + ): response = """No worries! Let me help you figure it out! šŸ¤” **Newbie**: You're new to plants, want something forgiving, and prefer low-maintenance green friends that won't judge you if you forget to water them occasionally. @@ -165,17 +188,28 @@ async def _handle_experience_level(self, user_id: str, message: str) -> AsyncIte else: response = "I didn't quite catch that! Are you a **Newbie** or **Plant Pro**? Or would you like me to explain the difference? 🌱" # Stay in the same state - + yield fp.PartialResponse(text=response) - async def _handle_watering_preference(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + async def _handle_watering_preference( + self, user_id: str, message: str + ) -> AsyncIterable[fp.PartialResponse]: """Handle watering preference""" message_lower = message.lower() - - if "constant" in message_lower or "regular" in message_lower or "often" in message_lower: + + if ( + "constant" in message_lower + or "regular" in message_lower + or "often" in message_lower + ): self.user_preferences[user_id]["watering"] = "high" response = "Got it! You're ready to be a dedicated plant parent with regular watering duties! šŸ’§\n\nNow for the sunshine question - will you put your plant in an area that gets **a lot of sun** or somewhere with **lower light**?" - elif "ignore" in message_lower or "forget" in message_lower or "low" in message_lower or "drought" in message_lower: + elif ( + "ignore" in message_lower + or "forget" in message_lower + or "low" in message_lower + or "drought" in message_lower + ): self.user_preferences[user_id]["watering"] = "low" response = "Perfect! Low-maintenance watering it is - your plant will be as chill as you are! šŸ˜Ž\n\nLast question - will your new green friend be living in an area that gets **a lot of sun** or somewhere with **lower light**?" else: @@ -183,40 +217,52 @@ async def _handle_watering_preference(self, user_id: str, message: str) -> Async # Stay in the same state yield fp.PartialResponse(text=response) return - + self.user_states[user_id] = UserState.SUNLIGHT_PREFERENCE yield fp.PartialResponse(text=response) - async def _handle_sunlight_preference(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + async def _handle_sunlight_preference( + self, user_id: str, message: str + ) -> AsyncIterable[fp.PartialResponse]: """Handle sunlight preference and provide recommendation""" message_lower = message.lower() - - if "lot" in message_lower or "high" in message_lower or "bright" in message_lower or "yes" in message_lower: + + if ( + "lot" in message_lower + or "high" in message_lower + or "bright" in message_lower + or "yes" in message_lower + ): self.user_preferences[user_id]["sunlight"] = "high" - elif "low" in message_lower or "little" in message_lower or "shade" in message_lower or "no" in message_lower: + elif ( + "low" in message_lower + or "little" in message_lower + or "shade" in message_lower + or "no" in message_lower + ): self.user_preferences[user_id]["sunlight"] = "low" else: response = "Just to be clear - will your plant be in a spot with **lots of sun** or **lower light**? ā˜€ļøšŸŒ™" yield fp.PartialResponse(text=response) return - + # Generate recommendation preferences = self.user_preferences[user_id] level = preferences["level"] watering = preferences["watering"] sunlight = preferences["sunlight"] - + # Select plant database plant_db = self.easy_plants if level == "newbie" else self.advanced_plants - + # Create key for plant selection key = f"{watering}_water_{sunlight}_sun" selected_plant = plant_db[key] - + # Create recommendation with weather integration offer recommendation = f"""🌟 **Perfect! I've got the ideal plant for you!** 🌟 -**Meet the {selected_plant['name']}!** +**Meet the {selected_plant['name']}!** {selected_plant['description']} @@ -229,43 +275,51 @@ async def _handle_sunlight_preference(self, user_id: str, message: str) -> Async self.user_states[user_id] = UserState.RECOMMENDATION yield fp.PartialResponse(text=recommendation) - async def _handle_recommendation_response(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + async def _handle_recommendation_response( + self, user_id: str, message: str + ) -> AsyncIterable[fp.PartialResponse]: """Handle user's response to plant recommendation""" message_lower = message.lower() - - if any(word in message_lower for word in ["yes", "interested", "love", "great", "perfect", "want"]): - response = """šŸŽ‰ **Fantastic choice!** You're going to be such a great plant parent! + + if any( + word in message_lower + for word in ["yes", "interested", "love", "great", "perfect", "want"] + ): + response = """šŸŽ‰ **Fantastic choice!** You're going to be such a great plant parent! Here's a nugget of plant wisdom to get you started: *"The best time to plant was 20 years ago. The second best time is now!"* 🌱 **Pro tip:** Most houseplants prefer the same temperatures we do (60s-70s°F), but they don't handle dry air as well as we do. Keep them away from heaters and vents, and give them a light misting in winter when the air gets dry. Happy planting! 🌿 And remember, I'm always here if you need weather updates for your green friends! ā˜€ļøšŸŒ§ļø""" - + self.user_states[user_id] = UserState.GENERAL else: # Provide alternative recommendation preferences = self.user_preferences[user_id] level = preferences["level"] - + # Get a different plant (simple rotation) plant_db = self.easy_plants if level == "newbie" else self.advanced_plants plants = list(plant_db.values()) - + # Get the next plant in the list current_plant_name = None for key, plant in plant_db.items(): - if f"{preferences['watering']}_water_{preferences['sunlight']}_sun" == key: - current_plant_name = plant['name'] + if ( + f"{preferences['watering']}_water_{preferences['sunlight']}_sun" + == key + ): + current_plant_name = plant["name"] break - + # Find alternative (just pick a different one for now) alternative = None for plant in plants: - if plant['name'] != current_plant_name: + if plant["name"] != current_plant_name: alternative = plant break - + if alternative: response = f"""No worries! Let me suggest something different! šŸ”„ @@ -278,15 +332,26 @@ async def _handle_recommendation_response(self, user_id: str, message: str) -> A This one might be more your style! **What do you think about this option?** 🌿""" else: response = "No problem! Let me know what specific qualities you're looking for in a plant, and I'll help you find the perfect green companion! 🌱" - + yield fp.PartialResponse(text=response) - async def _handle_general_chat(self, user_id: str, message: str) -> AsyncIterable[fp.PartialResponse]: + async def _handle_general_chat( + self, user_id: str, message: str + ) -> AsyncIterable[fp.PartialResponse]: """Handle general plant or weather questions""" message_lower = message.lower() - - plant_keywords = ["plant", "water", "care", "grow", "leaf", "soil", "pot", "garden"] - + + plant_keywords = [ + "plant", + "water", + "care", + "grow", + "leaf", + "soil", + "pot", + "garden", + ] + if any(keyword in message_lower for keyword in plant_keywords): response = """🌱 Great question! Here are some universal plant care tips: @@ -295,11 +360,11 @@ async def _handle_general_chat(self, user_id: str, message: str) -> AsyncIterabl • **Temperature tip:** Keep plants away from vents, heaters, and radiators • **Humidity help:** Lightly mist your plants daily in winter when the air is driest -Want a specific plant recommendation? Just let me know your experience level and preferences! +Want a specific plant recommendation? Just let me know your experience level and preferences! And don't forget - I can also check the weather in your city if you want to know about rain for any outdoor plants! šŸŒ¦ļø""" else: - response = """🌿 I'm here to help with plants and weather! + response = """🌿 I'm here to help with plants and weather! Ask me things like: • "I'm a newbie looking for an easy plant" @@ -308,20 +373,22 @@ async def _handle_general_chat(self, user_id: str, message: str) -> AsyncIterabl • "My plant leaves are turning yellow" What can I help you with today? šŸŒ±ā˜€ļø""" - + yield fp.PartialResponse(text=response) # Weather functionality - async def _handle_weather_request(self, message: str) -> AsyncIterable[fp.PartialResponse]: + async def _handle_weather_request( + self, message: str + ) -> AsyncIterable[fp.PartialResponse]: """Handle weather requests""" city_name = self._extract_city_name(message) - + if not city_name: yield fp.PartialResponse( text="I couldn't find a city name in your message. Please ask like: 'What's the weather in [city name]?' šŸŒ¦ļø" ) return - + try: weather_data = self._get_weather_data(city_name) response_text = self._format_weather_response(weather_data) @@ -334,8 +401,17 @@ async def _handle_weather_request(self, message: str) -> AsyncIterable[fp.Partia def _is_weather_request(self, message: str) -> bool: """Check if the message is asking for weather information""" weather_keywords = [ - 'weather', 'temperature', 'temp', 'forecast', 'climate', - 'hot', 'cold', 'rain', 'sunny', 'cloudy', 'humidity' + "weather", + "temperature", + "temp", + "forecast", + "climate", + "hot", + "cold", + "rain", + "sunny", + "cloudy", + "humidity", ] message_lower = message.lower() return any(keyword in message_lower for keyword in weather_keywords) @@ -343,7 +419,7 @@ def _is_weather_request(self, message: str) -> bool: def _extract_city_name(self, message: str) -> str: """Extract city name from user message""" message_lower = message.lower() - patterns = [' in ', ' for ', ' at ', ' of '] + patterns = [" in ", " for ", " at ", " of "] city_name = "" for pattern in patterns: @@ -351,15 +427,23 @@ def _extract_city_name(self, message: str) -> str: parts = message_lower.split(pattern) if len(parts) > 1: city_part = parts[1].strip() - city_part = city_part.replace('?', '').replace('.', '').replace('!', '') + city_part = ( + city_part.replace("?", "").replace(".", "").replace("!", "") + ) city_words = city_part.split()[:3] - city_name = ' '.join(city_words).strip() + city_name = " ".join(city_words).strip() break if not city_name: words = message.split() if len(words) >= 2: - city_name = ' '.join(words[-2:]).replace('?', '').replace('.', '').replace('!', '').strip() + city_name = ( + " ".join(words[-2:]) + .replace("?", "") + .replace(".", "") + .replace("!", "") + .strip() + ) return city_name.title() @@ -369,29 +453,29 @@ def _get_weather_data(self, city_name: str) -> dict: def _format_weather_response(self, weather_data: dict) -> str: """Format weather data with plant care tips""" - city = weather_data['name'] - country = weather_data['sys']['country'] - - main = weather_data['main'] - weather = weather_data['weather'][0] - wind = weather_data.get('wind', {}) - - temp = round(main['temp']) - feels_like = round(main['feels_like']) - temp_min = round(main['temp_min']) - temp_max = round(main['temp_max']) - + city = weather_data["name"] + country = weather_data["sys"]["country"] + + main = weather_data["main"] + weather = weather_data["weather"][0] + wind = weather_data.get("wind", {}) + + temp = round(main["temp"]) + feels_like = round(main["feels_like"]) + temp_min = round(main["temp_min"]) + temp_max = round(main["temp_max"]) + temp_f = round(temp * 9 / 5 + 32) feels_like_f = round(feels_like * 9 / 5 + 32) - - description = weather['description'].title() - humidity = main['humidity'] - pressure = main['pressure'] - wind_speed = wind.get('speed', 0) - + + description = weather["description"].title() + humidity = main["humidity"] + pressure = main["pressure"] + wind_speed = wind.get("speed", 0) + # Add plant care advice based on weather plant_advice = self._get_plant_advice_from_weather(weather_data) - + response = f"""šŸŒ¤ļø **Weather in {city}, {country}** šŸ“Š **Current Conditions:** @@ -412,23 +496,23 @@ def _format_weather_response(self, weather_data: dict) -> str: def _get_plant_advice_from_weather(self, weather_data: dict) -> str: """Generate plant care advice based on current weather""" - description = weather_data['weather'][0]['description'].lower() - temp = weather_data['main']['temp'] - humidity = weather_data['main']['humidity'] - - if 'rain' in description or 'drizzle' in description: + description = weather_data["weather"][0]["description"].lower() + temp = weather_data["main"]["temp"] + humidity = weather_data["main"]["humidity"] + + if "rain" in description or "drizzle" in description: return "šŸŒ§ļø It's rainy! Perfect time to skip watering outdoor plants - Mother Nature's got this covered! Indoor plants might appreciate the extra humidity though." - elif 'snow' in description: + elif "snow" in description: return "ā„ļø Snow day! Keep your outdoor plants protected and bring any tender potted plants inside. Indoor plants might need extra humidity due to heating." elif temp > 30: # Very hot return "šŸ”„ Hot weather alert! Your plants will be extra thirsty today. Check soil moisture frequently and provide shade for sensitive plants." - elif temp < 5: # Very cold + elif temp < 5: # Very cold return "🄶 Chilly weather! Protect your outdoor plants from frost and keep indoor plants away from cold windows and drafts." elif humidity < 30: return "šŸœļø Low humidity today! Your indoor plants would love a gentle misting, and outdoor plants might need extra water." elif humidity > 80: return "šŸ’§ High humidity today! Great for your plants' happiness! You might be able to water less frequently." - elif 'clear' in description or 'sunny' in description: + elif "clear" in description or "sunny" in description: return "ā˜€ļø Beautiful sunny day! Perfect weather for your plants to photosynthesize. Check that they're not getting too much direct sun though!" else: return "šŸŒ¤ļø Lovely weather for plants today! A good day to check on your green friends and see how they're doing." @@ -446,31 +530,24 @@ async def get_settings(self, setting: SettingsRequest) -> SettingsResponse: def get_weather_api_data(city_name: str, api_key: str) -> dict: """Helper function to fetch weather data""" import requests - - params = { - 'q': city_name, - 'appid': api_key, - 'units': 'metric' - } - + + params = {"q": city_name, "appid": api_key, "units": "metric"} + response = requests.get(OPENWEATHER_BASE_URL, params=params) - + if response.status_code == 404: raise Exception(f"City '{city_name}' not found") elif response.status_code == 401: raise Exception("API key is invalid") elif response.status_code != 200: raise Exception(f"API error: {response.status_code}") - + return response.json() -@app.function( - image=image, - secrets=[Secret.from_name("OPENWEATHER_API_KEY")] -) +@app.function(image=image, secrets=[Secret.from_name("OPENWEATHER_API_KEY")]) @asgi_app() def fastapi_app(): bot = GardenWeatherBot() # You'll need to replace this with your actual Poe bot credentials - return fp.make_app(bot, allow_without_key=True) # Change for production \ No newline at end of file + return fp.make_app(bot, allow_without_key=True) # Change for production diff --git a/weatherbot.py b/weatherbot.py index 91834f3..906d359 100644 --- a/weatherbot.py +++ b/weatherbot.py @@ -3,13 +3,14 @@ """ from __future__ import annotations + import os -import asyncio from typing import AsyncIterable import fastapi_poe as fp from fastapi_poe.types import QueryRequest, SettingsRequest, SettingsResponse -from modal import Image, App, asgi_app, Secret +from modal import App, Image, Secret, asgi_app + # ---------------------------------------- # Helper function to safely get API key @@ -24,6 +25,7 @@ def get_openweather_api_key() -> str: ) return api_key + OPENWEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather" @@ -36,7 +38,7 @@ async def get_response( if not self._is_weather_request(user_message): yield fp.PartialResponse( text="Hi! I'm a weather bot. Ask me about the weather in any city! " - "For example: 'What's the weather in New York?' or 'Weather in London'" + "For example: 'What's the weather in New York?' or 'Weather in London'" ) return @@ -45,7 +47,7 @@ async def get_response( if not city_name: yield fp.PartialResponse( text="I couldn't find a city name in your message. " - "Please ask like: 'What's the weather in [city name]?'" + "Please ask like: 'What's the weather in [city name]?'" ) return @@ -56,20 +58,29 @@ async def get_response( except Exception: yield fp.PartialResponse( text=f"Sorry, I couldn't get weather information for '{city_name}'. " - f"Please check the city name and try again." + f"Please check the city name and try again." ) def _is_weather_request(self, message: str) -> bool: weather_keywords = [ - 'weather', 'temperature', 'temp', 'forecast', 'climate', - 'hot', 'cold', 'rain', 'sunny', 'cloudy', 'humidity' + "weather", + "temperature", + "temp", + "forecast", + "climate", + "hot", + "cold", + "rain", + "sunny", + "cloudy", + "humidity", ] message_lower = message.lower() return any(keyword in message_lower for keyword in weather_keywords) def _extract_city_name(self, message: str) -> str: message_lower = message.lower() - patterns = [' in ', ' for ', ' at ', ' of '] + patterns = [" in ", " for ", " at ", " of "] city_name = "" for pattern in patterns: @@ -77,15 +88,23 @@ def _extract_city_name(self, message: str) -> str: parts = message_lower.split(pattern) if len(parts) > 1: city_part = parts[1].strip() - city_part = city_part.replace('?', '').replace('.', '').replace('!', '') + city_part = ( + city_part.replace("?", "").replace(".", "").replace("!", "") + ) city_words = city_part.split()[:3] - city_name = ' '.join(city_words).strip() + city_name = " ".join(city_words).strip() break if not city_name: words = message.split() if len(words) >= 2: - city_name = ' '.join(words[-2:]).replace('?', '').replace('.', '').replace('!', '').strip() + city_name = ( + " ".join(words[-2:]) + .replace("?", "") + .replace(".", "") + .replace("!", "") + .strip() + ) return city_name.title() @@ -93,25 +112,25 @@ def _get_weather_data(self, city_name: str) -> dict: return get_weather_api_data(city_name, get_openweather_api_key()) def _format_weather_response(self, weather_data: dict) -> str: - city = weather_data['name'] - country = weather_data['sys']['country'] + city = weather_data["name"] + country = weather_data["sys"]["country"] - main = weather_data['main'] - weather = weather_data['weather'][0] - wind = weather_data.get('wind', {}) + main = weather_data["main"] + weather = weather_data["weather"][0] + wind = weather_data.get("wind", {}) - temp = round(main['temp']) - feels_like = round(main['feels_like']) - temp_min = round(main['temp_min']) - temp_max = round(main['temp_max']) + temp = round(main["temp"]) + feels_like = round(main["feels_like"]) + temp_min = round(main["temp_min"]) + temp_max = round(main["temp_max"]) temp_f = round(temp * 9 / 5 + 32) feels_like_f = round(feels_like * 9 / 5 + 32) - description = weather['description'].title() - humidity = main['humidity'] - pressure = main['pressure'] - wind_speed = wind.get('speed', 0) + description = weather["description"].title() + humidity = main["humidity"] + pressure = main["pressure"] + wind_speed = wind.get("speed", 0) response = f"""šŸŒ¤ļø **Weather in {city}, {country}** @@ -129,9 +148,7 @@ def _format_weather_response(self, weather_data: dict) -> str: return response async def get_settings(self, setting: SettingsRequest) -> SettingsResponse: - return SettingsResponse( - server_bot_dependencies={"requests": "2.31.0"} - ) + return SettingsResponse(server_bot_dependencies={"requests": "2.31.0"}) # Modal deployment setup @@ -142,11 +159,7 @@ async def get_settings(self, setting: SettingsRequest) -> SettingsResponse: def get_weather_api_data(city_name: str, api_key: str) -> dict: import requests - params = { - 'q': city_name, - 'appid': api_key, - 'units': 'metric' - } + params = {"q": city_name, "appid": api_key, "units": "metric"} response = requests.get(OPENWEATHER_BASE_URL, params=params) @@ -160,10 +173,7 @@ def get_weather_api_data(city_name: str, api_key: str) -> dict: return response.json() -@app.function( - image=image, - secrets=[Secret.from_name("OPENWEATHER_API_KEY")] -) +@app.function(image=image, secrets=[Secret.from_name("OPENWEATHER_API_KEY")]) @asgi_app() def fastapi_app(): bot = WeatherBot()