Spaces:
Sleeping
Sleeping
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# app.py β Weather Umbrella Advisor (Streamlit + Claude 3 + OpenWeatherMap) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
import os | |
import json | |
import requests | |
import boto3 | |
import streamlit as st | |
from dotenv import load_dotenv | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 1) Load environment variables (for local .env / HF Secrets) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
load_dotenv() | |
OPENWEATHERMAP_API_KEY = os.getenv("OPENWEATHERMAP_API_KEY") | |
AWS_REGION = os.getenv("AWS_REGION", "us-east-1") | |
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") # may be None | |
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") # may be None | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 2) Helper to mask credentials (so we can print a hint in the UI) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
def _mask(val: str) -> str: | |
""" | |
Returns a masked version of `val`, showing first 4 + last 4 chars | |
e.g. AKIA1234abcd...WXYZ5678 | |
""" | |
if not val: | |
return "None" | |
if len(val) <= 8: | |
return val | |
return val[:4] + "..." + val[-4:] | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 3) Detect if keys are reversed: we expect ACCESS_KEY_ID to start with AKIA/ASIA | |
# If not, but SECRET_ACCESS_KEY starts with AKIA/ASIA, swap them. | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
def _looks_like_access_key(key: str) -> bool: | |
""" | |
AWS access key IDs typically start with 'AKIA' or 'ASIA' and are 20 characters long. | |
""" | |
return bool(key) and (key.startswith("AKIA") or key.startswith("ASIA")) and len(key) == 20 | |
# If ACCESS_KEY_ID doesnβt look like an AKIA/ASIA but SECRET_ACCESS_KEY does, swap: | |
if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: | |
if not _looks_like_access_key(AWS_ACCESS_KEY_ID) and _looks_like_access_key(AWS_SECRET_ACCESS_KEY): | |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY, AWS_ACCESS_KEY_ID | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 4) Initialize boto3 Session / Bedrock client | |
# β If both AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY exist, pass them explicitly | |
# β Otherwise, fall back to default credential chain (IAM role, container credentials, etc.) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: | |
session = boto3.Session( | |
aws_access_key_id = AWS_ACCESS_KEY_ID, | |
aws_secret_access_key = AWS_SECRET_ACCESS_KEY, | |
region_name = AWS_REGION, | |
) | |
else: | |
session = boto3.Session(region_name=AWS_REGION) | |
bedrock = session.client("bedrock-runtime") | |
# Quick sanityβcheck: if credentials are still invalid, this will raise immediately. | |
try: | |
_ = bedrock.meta.region_name # just to force the client to exist | |
except Exception as e: | |
st.error(f"β οΈ Credential problem: {e}") | |
st.stop() | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 5) Streamlit Page Configuration & Header | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
st.set_page_config(page_title="π€οΈ Umbrella Advisor", page_icon="β", layout="centered") | |
st.markdown( | |
""" | |
<div style="text-align:center"> | |
<h1 style="color:#3c79f5;">β Weather Umbrella Advisor</h1> | |
<p style="font-size:18px"> | |
Ask if you need an umbrella tomorrow β powered by <b>Claude 3 Sonnet (Bedrock)</b> + <b>OpenWeatherMap</b>. | |
</p> | |
</div> | |
""", | |
unsafe_allow_html=True, | |
) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 6) Show masked credentials for debugging (so you can see if the swap logic worked) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
st.markdown( | |
f""" | |
**Debug** (masked credentials): | |
β’ AWS_ACCESS_KEY_ID = `{_mask(AWS_ACCESS_KEY_ID)}` | |
β’ AWS_SECRET_ACCESS_KEY = `{_mask(AWS_SECRET_ACCESS_KEY)}` | |
""", | |
unsafe_allow_html=True, | |
) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 7) Conversation state | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
if "messages" not in st.session_state: | |
st.session_state.messages = [] | |
for m in st.session_state.messages: | |
with st.chat_message(m["role"]): | |
st.markdown(m["content"]) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 8) Helper: get_weather(city) β calls OpenWeatherMap and returns JSON | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
def get_weather(city: str): | |
""" | |
Fetches a 24βhour forecast (8 x 3βhour intervals) for `city`. | |
Returns either: | |
{ "location": "CityName", "forecast": [ ... ] } | |
or | |
{ "error": "Error message" } | |
""" | |
city = city.strip() | |
if not city: | |
return {"error": "Please provide a valid city name."} | |
try: | |
# 1) Get lat/lon | |
geo_url = ( | |
f"http://api.openweathermap.org/geo/1.0/direct" | |
f"?q={city}&limit=1&appid={OPENWEATHERMAP_API_KEY}" | |
) | |
geo_resp = requests.get(geo_url, timeout=10).json() | |
if not geo_resp: | |
return {"error": f"City '{city}' not found."} | |
lat, lon = geo_resp[0]["lat"], geo_resp[0]["lon"] | |
# 2) Get 5βday / 3hr forecast | |
weather_url = ( | |
f"http://api.openweathermap.org/data/2.5/forecast" | |
f"?lat={lat}&lon={lon}" | |
f"&appid={OPENWEATHERMAP_API_KEY}&units=metric" | |
) | |
weather_data = requests.get(weather_url, timeout=10).json() | |
if "list" not in weather_data: | |
return {"error": f"Unable to fetch forecast for '{city}'."} | |
forecast = [] | |
for f in weather_data["list"][:8]: # Next 24 hours β 8 slots at 3 each | |
forecast.append({ | |
"time": f["dt_txt"], | |
"description": f["weather"][0]["description"].capitalize(), | |
"rain_probability": round(f.get("pop", 0) * 100, 1), | |
"temp": f["main"]["temp"], | |
"humidity": f["main"]["humidity"] | |
}) | |
return {"location": city.title(), "forecast": forecast} | |
except Exception as ex: | |
return {"error": str(ex)} | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 9) ReAct System Prompt & Helper to ask Claude (Bedrock) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
SYSTEM_PROMPT = """ | |
You are a helpful umbrella advisor using the ReAct (Reasoning + Acting) methodology. | |
Steps: | |
1. Think about the userβs question. | |
2. Act by calling get_weather(location) if needed. | |
3. Observe the weather result. | |
4. Reason and respond. | |
When you need weather data, respond _exactly_ in this JSON format (no extra text): | |
{ | |
"thought": "β¦", | |
"action": "get_weather", | |
"action_input": {"location": "CityName"} | |
} | |
If no location is provided, ask the user to specify one. | |
Once you have the forecast, give a final, friendly answer such as: | |
"You do not need an umbrella tomorrow in London because it will be sunny with 0% chance of rain." | |
""" | |
def ask_claude(user_input: str, history: str = "") -> str: | |
""" | |
1. Send the initial ReAct prompt to Claude, including user_input + history. | |
2. Parse Claudeβs JSON: if action == "get_weather", call get_weather(β¦). | |
3. Feed the weather data back into Claude for final reasoning. | |
4. Return Claudeβs final text reply. | |
""" | |
# Step 1: Initial ReAct call | |
body1 = { | |
"anthropic_version": "bedrock-2023-05-31", | |
"max_tokens": 1000, | |
"temperature": 0.7, | |
"top_p": 0.9, | |
"messages": [ | |
{"role": "user", "content": f"{SYSTEM_PROMPT}\n\nHistory:\n{history}\n\nUser: {user_input}"} | |
] | |
} | |
resp1 = bedrock.invoke_model( | |
modelId="anthropic.claude-3-sonnet-20240229-v1:0", | |
contentType="application/json", | |
accept="application/json", | |
body=json.dumps(body1) | |
) | |
text1 = json.loads(resp1["body"].read())["content"][0]["text"].strip() | |
# Step 2: Try parsing as JSON | |
try: | |
parsed = json.loads(text1) | |
if parsed.get("action") == "get_weather": | |
city = parsed["action_input"].get("location", "").strip() | |
if not city: | |
return "π I need a city nameβcould you please tell me which city you mean?" | |
wx = get_weather(city) | |
if "error" in wx: | |
return wx["error"] | |
# Step 3: Ask Claude to reason over the weather data | |
weather_json = json.dumps(wx, indent=2) | |
prompt2 = ( | |
f"Here is the forecast for {wx['location']}:\n\n" | |
f"{weather_json}\n\n" | |
"Based on this data, answer whether the user should carry an umbrella tomorrow " | |
"in a friendly, conversational way (YES/NO + reasoning)." | |
) | |
body2 = { | |
"anthropic_version": "bedrock-2023-05-31", | |
"max_tokens": 500, | |
"temperature": 0.7, | |
"messages": [{"role": "user", "content": prompt2}] | |
} | |
resp2 = bedrock.invoke_model( | |
modelId="anthropic.claude-3-sonnet-20240229-v1:0", | |
contentType="application/json", | |
accept="application/json", | |
body=json.dumps(body2) | |
) | |
return json.loads(resp2["body"].read())["content"][0]["text"].strip() | |
except json.JSONDecodeError: | |
# If it wasnβt valid JSON, just return whatever Claude replied | |
pass | |
return text1 | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 10) Build conversation history helper | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
def _build_history(n: int = 4) -> str: | |
""" | |
Returns the last n messages formatted as: | |
User: ... | |
Assistant: ... | |
so that Claude sees recent turns. | |
""" | |
hist = st.session_state.messages[-n:] | |
return "\n".join(f"{m['role'].capitalize()}: {m['content']}" for m in hist) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 11) Main Chat Input / Display Loop | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
if user_query := st.chat_input("Ask: Do I need an umbrella tomorrow?"): | |
# 1) Append user message locally | |
st.session_state.messages.append({"role": "user", "content": user_query}) | |
with st.chat_message("user"): | |
st.markdown(user_query) | |
# 2) Get assistant reply | |
with st.chat_message("assistant"): | |
with st.spinner("π€ Thinkingβ¦"): | |
history = _build_history() | |
assistant_reply = ask_claude(user_query, history) | |
st.markdown(assistant_reply) | |
# 3) Append assistant reply to state | |
st.session_state.messages.append({"role": "assistant", "content": assistant_reply}) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 12) Sidebar (Branding / Help) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
with st.sidebar: | |
st.image("https://img.icons8.com/clouds/100/umbrella.png", width=100) | |
st.markdown("## βοΈ About") | |
st.markdown( | |
""" | |
**Weather Umbrella Advisor** | |
- Uses **OpenWeatherMap** for realβtime forecast | |
- Uses **Claude 3 Sonnet (AWS Bedrock)** to reason via ReAct | |
- Provides clear YES/NO umbrella advice with reasoning | |
**Try these:** | |
- "Should I bring an umbrella tomorrow?" | |
- "Will it rain in Delhi tomorrow?" | |
- "Do I need an umbrella in Tokyo?" | |
""" | |
) | |