import streamlit as st st.markdown(""" **Weather agent** Example of PydanticAI with `multiple tools` which the LLM needs to call in turn to answer a question. """) with st.expander("🎯 Objectives"): st.markdown(""" - Use **OpenAI GPT-4o-mini** agent to `process natural language queries` about the weather. - Fetch **geolocation** from a location string using the `Maps.co API`. - Retrieve **real-time weather** using the Tomorrow.io API. - Handle `retries`, `backoff`, and `logging` using **Logfire**. - Integrate all parts in a clean, async-compatible **Streamlit UI**. - Ensuring `concise` and `structured` responses. """) with st.expander("🧰 Pre-requisites"): st.markdown(""" - Python 3.10+ - Streamlit - AsyncClient (httpx) - OpenAI `pydantic_ai` Agent - Logfire for tracing/debugging - Valid API Keys: - [https://geocode.maps.co/](https://geocode.maps.co/) - [https://www.tomorrow.io/](https://www.tomorrow.io/) """) st.code(""" pip install streamlit httpx logfire pydantic_ai """) with st.expander("⚙️ Step-by-Step Setup"): st.markdown("**Imports and Global Client**") st.code(""" import os import asyncio import streamlit as st from dataclasses import dataclass from typing import Any import logfire from httpx import AsyncClient from pydantic_ai import Agent, RunContext, ModelRetry logfire.configure(send_to_logfire='if-token-present') client = AsyncClient() """) st.markdown("**Declare Dependencies**") st.code(""" @dataclass class Deps: client: AsyncClient # client is an instance of AsyncClient (from httpx). weather_api_key: str | None geo_api_key: str | None """) st.markdown("**Setup Weather Agent**") st.code(""" weather_agent = Agent( 'openai:gpt-4o-mini', system_prompt=( 'Be concise, reply with one sentence. ' 'Use the `get_lat_lng` tool to get the latitude and longitude of the locations, ' 'then use the `get_weather` tool to get the weather.' ), deps_type= Deps, retries = 2, ) """) st.markdown("**Define Geocoding Tool with Retry**") st.code(""" @weather_agent.tool async def get_lat_lng(ctx: RunContext[Deps], location_description: str, max_retries: int = 5, base_delay: int = 2) -> dict[str, float]: "Get the latitude and longitude of a location with retry handling for rate limits." if ctx.deps.geo_api_key is None: return {'lat': 51.1, 'lng': -0.1} # Default to London # Sets up API request parameters. params = {'q': location_description, 'api_key': ctx.deps.geo_api_key} # Loops for a maximum number of retries. for attempt in range(max_retries): try: # Logs API call span with parameters. with logfire.span('calling geocode API', params=params) as span: # Sends async GET request. r = await ctx.deps.client.get('https://geocode.maps.co/search', params=params) # Checks if API rate limit is exceeded. if r.status_code == 429: # Exponential backoff wait_time = base_delay * (2 ** attempt) # Waits before retrying. await asyncio.sleep(wait_time) # Continues to the next retry attempt. continue r.raise_for_status() data = r.json() span.set_attribute('response', data) if data: # Extracts and returns latitude & longitude. return {'lat': float(data[0]['lat']), 'lng': float(data[0]['lon'])} else: # Raises an error if no valid data is found. raise ModelRetry('Could not find the location') except Exception as e: # Catches HTTP errors. print(f"Request failed: {e}") # Logs the failure. raise ModelRetry('Failed after multiple retries') """) st.markdown("**Define Weather Tool**") st.code(""" @weather_agent.tool async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str, Any]: if ctx.deps.weather_api_key is None: return {'temperature': '21 °C', 'description': 'Sunny'} params = {'apikey': ctx.deps.weather_api_key, 'location': f'{lat},{lng}', 'units': 'metric'} r = await ctx.deps.client.get('https://api.tomorrow.io/v4/weather/realtime', params=params) r.raise_for_status() data = r.json() values = data['data']['values'] code_lookup = { 1000: 'Clear, Sunny', 1001: 'Cloudy', 1100: 'Mostly Clear', 1101: 'Partly Cloudy', 1102: 'Mostly Cloudy', 2000: 'Fog', 2100: 'Light Fog', 4000: 'Drizzle', 4001: 'Rain', 4200: 'Light Rain', 4201: 'Heavy Rain', 5000: 'Snow', 5001: 'Flurries', 5100: 'Light Snow', 5101: 'Heavy Snow', 6000: 'Freezing Drizzle', 6001: 'Freezing Rain', 6200: 'Light Freezing Rain', 6201: 'Heavy Freezing Rain', 7000: 'Ice Pellets', 7101: 'Heavy Ice Pellets', 7102: 'Light Ice Pellets', 8000: 'Thunderstorm', } return { 'temperature': f'{values["temperatureApparent"]:0.0f}°C', 'description': code_lookup.get(values['weatherCode'], 'Unknown'), } """) st.markdown("**Wrapper to Run the Agent**") st.code(""" async def run_weather_agent(user_input: str): deps = Deps( client=client, weather_api_key = os.getenv("TOMORROW_IO_API_KEY"), geo_api_key = os.getenv("GEOCODE_API_KEY") ) result = await weather_agent.run(user_input, deps=deps) return result.data """) st.markdown("**Streamlit UI with Async Handling**") st.code(""" st.set_page_config(page_title="Weather Application", page_icon="🚀") if "weather_response" not in st.session_state: st.session_state.weather_response = None st.title("Weather Agent App") user_input = st.text_area("Enter a sentence with locations:", "What is the weather like in Bangalore, Chennai and Delhi?") if st.button("Get Weather"): with st.spinner("Fetching weather..."): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) response = loop.run_until_complete(run_weather_agent(user_input)) st.session_state.weather_response = response if st.session_state.weather_response: st.info(st.session_state.weather_response) """) with st.expander("Description of Each Step"): st.markdown(""" - **Imports**: Brings in all required packages including `httpx`, `logfire`, and `streamlit`. - **`Deps` Dataclass**: Encapsulates dependencies injected into the agent like the API keys and shared HTTP client. - **Weather Agent**: Configures an OpenAI GPT-4o-mini agent with tools for geolocation and weather. - **Tools**: - `get_lat_lng`: Geocodes a location using a free Maps.co API. Implements retry with exponential backoff. - `get_weather`: Fetches live weather info from Tomorrow.io using lat/lng. - **Agent Runner**: Wraps the interaction to run asynchronously with injected dependencies. - **Streamlit UI**: Captures user input, triggers agent execution, and displays response with `asyncio`. """) st.image("https://raw.githubusercontent.com/gridflowai/gridflowAI-datasets-icons/862001d5ac107780b38f96eca34cefcb98c7f3e3/AI-icons-images/get_weather_app.png", caption="Agentic Weather App Flow", use_column_width=True) import os import asyncio import streamlit as st from dataclasses import dataclass from typing import Any import logfire from httpx import AsyncClient from pydantic_ai import Agent, RunContext, ModelRetry # Configure logfire logfire.configure(send_to_logfire='if-token-present') @dataclass class Deps: client: AsyncClient weather_api_key: str | None geo_api_key: str | None weather_agent = Agent( 'openai:gpt-4o-mini', system_prompt=( 'Be concise, reply with one sentence. ' 'Use the `get_lat_lng` tool to get the latitude and longitude of the locations, ' 'then use the `get_weather` tool to get the weather.' ), deps_type=Deps, retries=2, ) # Create a single global AsyncClient instance client = AsyncClient() @weather_agent.tool async def get_lat_lng(ctx: RunContext[Deps], location_description: str, max_retries: int = 5, base_delay: int = 2) -> dict[str, float]: """Get the latitude and longitude of a location.""" if ctx.deps.geo_api_key is None: return {'lat': 51.1, 'lng': -0.1} # Default to London # Sets up API request parameters. params = {'q': location_description, 'api_key': ctx.deps.geo_api_key} # Loops for a maximum number of retries. for attempt in range(max_retries): try: # Logs API call span with parameters. with logfire.span('calling geocode API', params=params) as span: # Sends async GET request. r = await ctx.deps.client.get('https://geocode.maps.co/search', params=params) # Checks if API rate limit is exceeded. if r.status_code == 429: # Too Many Requests wait_time = base_delay * (2 ** attempt) # Exponential backoff print(f"Rate limited. Retrying in {wait_time} seconds...") # Waits before retrying. await asyncio.sleep(wait_time) # Continues to the next retry attempt. continue # Retry the request # Raises an exception for HTTP errors. r.raise_for_status() # Parses the API response as JSON. data = r.json() # Logs the response data. span.set_attribute('response', data) if data: # Extracts and returns latitude & longitude. return {'lat': float(data[0]['lat']), 'lng': float(data[0]['lon'])} else: # Raises an error if no valid data is found. raise ModelRetry('Could not find the location') except Exception as e: # Catches HTTP errors. print(f"Request failed: {e}") # Logs the failure. raise ModelRetry('Failed after multiple retries') @weather_agent.tool async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str, Any]: """Get the weather at a location.""" if ctx.deps.weather_api_key is None: return {'temperature': '21 °C', 'description': 'Sunny'} params = {'apikey': ctx.deps.weather_api_key, 'location': f'{lat},{lng}', 'units': 'metric'} r = await ctx.deps.client.get('https://api.tomorrow.io/v4/weather/realtime', params=params) r.raise_for_status() data = r.json() values = data['data']['values'] code_lookup = { 1000: 'Clear, Sunny', 1001: 'Cloudy', 1100: 'Mostly Clear', 1101: 'Partly Cloudy', 1102: 'Mostly Cloudy', 2000: 'Fog', 2100: 'Light Fog', 4000: 'Drizzle', 4001: 'Rain', 4200: 'Light Rain', 4201: 'Heavy Rain', 5000: 'Snow', 5001: 'Flurries', 5100: 'Light Snow', 5101: 'Heavy Snow', 6000: 'Freezing Drizzle', 6001: 'Freezing Rain', 6200: 'Light Freezing Rain', 6201: 'Heavy Freezing Rain', 7000: 'Ice Pellets', 7101: 'Heavy Ice Pellets', 7102: 'Light Ice Pellets', 8000: 'Thunderstorm', } return { 'temperature': f'{values["temperatureApparent"]:0.0f}°C', 'description': code_lookup.get(values['weatherCode'], 'Unknown'), } async def run_weather_agent(user_input: str): deps = Deps( client=client, # Use global client weather_api_key=os.getenv("TOMORROW_IO_API_KEY"), geo_api_key=os.getenv("GEOCODE_API_KEY") ) result = await weather_agent.run(user_input, deps=deps) return result.data # Initialize session state for storing weather responses if "weather_response" not in st.session_state: st.session_state.weather_response = None # Set the page title #st.set_page_config(page_title="Weather Application", page_icon="🚀") # Streamlit UI with st.expander(f"**Example prompts**"): st.markdown(f""" Prompt : If I were in Sydney today, would I need a jacket? Bot : No, you likely wouldn't need a jacket as it's clear and sunny with a temperature of 22°C in Sydney. Prompt : Tell me whether it's beach weather in Bali and Phuket. Bot : Bali is too cold at 7°C and partly cloudy for beach weather, while Phuket is warm at 26°C with drizzle, making it more suitable for beach activities. Prompt : If I had a meeting in Dubai, should I wear light clothing? Bot : Yes, you should wear light clothing as the temperature in Dubai is currently 25°C and mostly clear. Prompt : How does today’s temperature in Tokyo compare to the same time last week? Bot : Today's temperature in Tokyo is 14°C, which is the same as the temperature at the same time last week. Prompt : Is the current weather suitable for air travel in London and New York? Bot : The current weather in London is 5°C and cloudy, and in New York, it is -0°C and clear; both conditions are generally suitable for air travel. """) user_input = st.text_area("Enter a sentence with locations:", "What is the weather like in Bangalore, Chennai and Delhi?") # Button to trigger weather fetch if st.button("Get Weather"): with st.spinner("Fetching weather..."): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) response = loop.run_until_complete(run_weather_agent(user_input)) st.session_state.weather_response = response # Display stored response if st.session_state.weather_response: st.info(st.session_state.weather_response) with st.expander("🧠 How is this app Agentic?"): st.markdown(""" ###### ✅ How this App is Agentic This weather app demonstrates **Agentic AI** because: 1. **Goal-Oriented Autonomy** The user provides a natural language request (e.g., *“What’s the weather in Bangalore and Delhi?”*). The agent autonomously figures out *how* to fulfill it. 2. **Tool Usage by the Agent** The `Agent` uses two tools: - `get_lat_lng()` – to fetch coordinates via a geocoding API. - `get_weather()` – to get real-time weather for those coordinates. The agent determines when and how to use these tools. 3. **Context + Dependency Injection** The app uses the `Deps` dataclass to provide the agent with shared dependencies like HTTP clients and API keys—just like a human agent accessing internal tools. 4. **Retries and Adaptive Behavior** The agent handles failures and retries via `ModelRetry`, showing resilience and smart retry logic. 5. **Structured Interactions via `RunContext`** Each tool runs with access to structured context, enabling better coordination and reuse of shared state. 6. **LLM-Orchestrated Actions** At the core, a GPT-4o-mini model orchestrates: - Understanding the user intent, - Selecting and invoking the right tools, - Synthesizing the final response. > 🧠 **In essence**: This is not just a chatbot, but an *autonomous reasoning engine* that uses real tools to complete real-world goals. """) with st.expander("🧪 Example Prompts: Handling Complex Queries"): st.markdown(""" This app can understand **natural, varied, and multi-part prompts** thanks to the LLM-based agent at its core. It intelligently uses `get_lat_lng()` and `get_weather()` tools based on user intent. ###### 🗣️ Complex Prompt Examples & Responses: **Prompt:** *If I were in Sydney today, would I need a jacket?* **Response:** *No, you likely wouldn't need a jacket as it's clear and sunny with a temperature of 22°C in Sydney.* --- **Prompt:** *Tell me whether it's beach weather in Bali and Phuket.* **Response:** *Bali is too cold at 7°C and partly cloudy for beach weather, while Phuket is warm at 26°C with drizzle, making it more suitable for beach activities.* --- **Prompt:** *If I had a meeting in Dubai, should I wear light clothing?* **Response:** *Yes, you should wear light clothing as the temperature in Dubai is currently 25°C and mostly clear.* --- **Prompt:** *How does today’s temperature in Tokyo compare to the same time last week?* **Response:** *Today's temperature in Tokyo is 14°C, which is the same as the temperature at the same time last week.* *(Note: This would require historical API support to be accurate in a real app.)* --- **Prompt:** *Is the current weather suitable for air travel in London and New York?* **Response:** *The current weather in London is 5°C and cloudy, and in New York, it is -0°C and clear; both conditions are generally suitable for air travel.* --- **Prompt:** *Give me the weather update for all cities where cricket matches are happening today in India.* **Response:** *(This would involve external logic for identifying cricket venues, but the agent can handle the weather lookup part once cities are known.)* --- ###### 🧠 Why it Works: - The **agent extracts all cities** from the prompt, even if mixed with unrelated text. - It **chains tool calls**: First gets geolocation, then weather. - The **final response is LLM-crafted** to match the tone and question format (yes/no, suggestion, comparison, etc.). > ✅ You don’t need to ask "what's the weather in X" exactly — the agent infers it from how humans speak. """) with st.expander("🔍 Missing Agentic AI Capabilities & How to Improve"): st.markdown(""" While the app exhibits several **agentic behaviors**—like tool use, intent recognition, and multi-step reasoning—it still lacks **some core features** found in *fully agentic systems*. Here's what’s missing: ###### ❌ Missing Facets & How to Add Them **1. Autonomy & Proactive Behavior** *Current:* The app only responds to user prompts. *To Add:* Let the agent proactively ask follow-ups. **Example:** - User: *What's the weather in Italy?* - Agent: *Italy has multiple cities. Would you like weather in Rome, Milan, or Venice?* **2. Goal-Oriented Planning** *Current:* Executes one tool or a fixed chain of tools. *To Add:* Give it a higher-level goal and let it plan the steps. **Example:** - Prompt: *Help me plan a weekend trip to a warm place in Europe.* - Agent: Finds warm cities, checks weather, compares, and recommends. **3. Memory / Session Context** *Current:* Stateless; each query is standalone. *To Add:* Use LangGraph or crewAI memory modules to **remember past queries** or preferences. **Example:** - User: *What’s the weather in Delhi?* - Then: *And how about tomorrow?* → Agent should know the context refers to Delhi. **4. Delegation to Sub-Agents** *Current:* Single-agent, monolithic logic. *To Add:* Delegate tasks to specialized agents (geocoder agent, weather formatter agent, response stylist, etc.). **Example:** - Planner agent decides cities → Fetcher agent retrieves data → Explainer agent summarizes. **5. Multi-Modal Input/Output** *Current:* Only text. *To Add:* Accept voice prompts or generate a weather infographic. **Example:** - Prompt: *Voice note saying "Is it rainy in London?"* → Returns image with rainy clouds and summary. **6. Learning from Feedback** *Current:* No learning or improvement from user input. *To Add:* Allow thumbs up/down or feedback to tune responses. **Example:** - User: *That was not helpful.* → Agent: *Sorry! Want a more detailed report or city breakdown?* --- ###### ✅ Summary This app **lays a strong foundation for Agentic AI**, but adding these elements would bring it closer to a **truly autonomous, context-aware, and planning-capable agent** that mimics human-level task execution. """)