MathWizard1729 commited on
Commit
7588956
Β·
verified Β·
1 Parent(s): b14190d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -111
app.py CHANGED
@@ -1,25 +1,57 @@
1
- ###############################################################################
2
- # app.py – Weather Umbrella Advisor (Streamlit + OpenWeatherMap + Claude 3) #
3
- ###############################################################################
4
- import os, json, requests, boto3, streamlit as st
 
 
 
 
5
  from dotenv import load_dotenv
6
 
7
- # --------------------------------------------------------------------------- #
8
- # 1) Load env vars (local .env or HF Space β€œSecrets”) #
9
- # --------------------------------------------------------------------------- #
10
  load_dotenv()
 
11
  OPENWEATHERMAP_API_KEY = os.getenv("OPENWEATHERMAP_API_KEY")
12
  AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
13
- AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") # may be None
14
- AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") # may be None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- # --- Heuristic: auto-swap if ID looks like a secret (contains β€œ/”) ----------
17
- if AWS_ACCESS_KEY_ID and "/" in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY and "/" not in AWS_SECRET_ACCESS_KEY:
18
- AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY, AWS_ACCESS_KEY_ID
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- # --------------------------------------------------------------------------- #
21
- # 2) Create Bedrock client, preferring explicit keys if they’re present #
22
- # --------------------------------------------------------------------------- #
 
 
23
  if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
24
  session = boto3.Session(
25
  aws_access_key_id = AWS_ACCESS_KEY_ID,
@@ -27,32 +59,49 @@ if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
27
  region_name = AWS_REGION,
28
  )
29
  else:
30
- # Falls back to IAM role or default credentials chain
31
  session = boto3.Session(region_name=AWS_REGION)
32
 
33
  bedrock = session.client("bedrock-runtime")
34
 
35
- # Quick sanity-check – will raise if creds are still wrong
36
  try:
37
- _ = bedrock.meta.region_name # simple attribute access forces no call
38
  except Exception as e:
39
- st.error(f"Credential problem: {e}")
40
  st.stop()
41
 
42
- # --------------------------------------------------------------------------- #
43
- # 3) Streamlit page style #
44
- # --------------------------------------------------------------------------- #
45
  st.set_page_config(page_title="🌀️ Umbrella Advisor", page_icon="β˜”", layout="centered")
46
- st.markdown("""
47
- <div style="text-align:center">
48
- <h1 style="color:#3c79f5;">β˜” Weather Umbrella Advisor</h1>
49
- <p style="font-size:18px">Ask if you need an umbrella tomorrow – powered by <b>Claude 3</b> &amp; <b>OpenWeatherMap</b>.</p>
50
- </div>
51
- """, unsafe_allow_html=True)
52
-
53
- # --------------------------------------------------------------------------- #
54
- # 4) Conversation state #
55
- # --------------------------------------------------------------------------- #
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  if "messages" not in st.session_state:
57
  st.session_state.messages = []
58
 
@@ -60,116 +109,195 @@ for m in st.session_state.messages:
60
  with st.chat_message(m["role"]):
61
  st.markdown(m["content"])
62
 
63
- # --------------------------------------------------------------------------- #
64
- # 5) Helper – get weather #
65
- # --------------------------------------------------------------------------- #
66
  def get_weather(city: str):
67
- if not city.strip():
68
- return {"error": "Please give me a city name."}
 
 
 
 
 
 
 
 
69
 
70
- geo = f"http://api.openweathermap.org/geo/1.0/direct?q={city}&limit=1&appid={OPENWEATHERMAP_API_KEY}"
71
  try:
72
- loc = requests.get(geo, timeout=10).json()
73
- if not loc:
74
- return {"error": f"City β€œ{city}” not found."}
75
- lat, lon = loc[0]["lat"], loc[0]["lon"]
76
- wurl = f"http://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={OPENWEATHERMAP_API_KEY}&units=metric"
77
- data = requests.get(wurl, timeout=10).json()
78
- if "list" not in data:
79
- return {"error": f"No forecast for β€œ{city}”."}
80
- fc = [{
81
- "time": f["dt_txt"],
82
- "description": f["weather"][0]["description"].capitalize(),
83
- "rain_probability": round(f.get("pop", 0)*100, 1),
84
- "temp": f["main"]["temp"]
85
- } for f in data["list"][:8]]
86
- return {"location": city.title(), "forecast": fc}
87
- except Exception as e:
88
- return {"error": str(e)}
89
-
90
- # --------------------------------------------------------------------------- #
91
- # 6) Helper – talk to Claude (ReAct) #
92
- # --------------------------------------------------------------------------- #
93
- SYSTEM_PROMPT = """You are a helpful umbrella advisor using ReAct:
94
-
95
- 1. Think about the question.
96
- 2. If needed, act with get_weather(location).
97
- 3. Observe results.
98
- 4. Reason and answer.
99
-
100
- When you need weather data, respond EXACTLY:
101
- {"thought":"…","action":"get_weather","action_input":{"location":"City"}}"""
102
-
103
- def ask_claude(user, history=""):
104
- # First call
105
- body = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  "anthropic_version": "bedrock-2023-05-31",
107
  "max_tokens": 1000,
108
  "temperature": 0.7,
109
  "top_p": 0.9,
110
- "messages": [{
111
- "role": "user",
112
- "content": f"{SYSTEM_PROMPT}\n\nHistory:\n{history}\n\nUser: {user}"
113
- }]
114
  }
115
- raw = bedrock.invoke_model(
116
  modelId="anthropic.claude-3-sonnet-20240229-v1:0",
117
  contentType="application/json",
118
  accept="application/json",
119
- body=json.dumps(body)
120
  )
121
- txt = json.loads(raw["body"].read())["content"][0]["text"].strip()
122
 
123
- # Try to parse ReAct JSON
124
  try:
125
- j = json.loads(txt)
126
- if j.get("action") == "get_weather":
127
- city = j["action_input"]["location"]
128
- wx = get_weather(city)
129
- if "error" in wx: return wx["error"]
130
 
131
- # Second call – reasoning
132
- reason_prompt = f"""Here is the forecast for {city}:
133
-
134
- {json.dumps(wx, indent=2)}
135
 
136
- Give a friendly answer: YES/NO umbrella, with reasoning."""
 
 
 
 
 
 
 
137
  body2 = {
138
  "anthropic_version": "bedrock-2023-05-31",
139
  "max_tokens": 500,
140
  "temperature": 0.7,
141
- "messages":[{"role":"user","content":reason_prompt}]
142
  }
143
- raw2 = bedrock.invoke_model(
144
  modelId="anthropic.claude-3-sonnet-20240229-v1:0",
145
  contentType="application/json",
146
  accept="application/json",
147
  body=json.dumps(body2)
148
  )
149
- return json.loads(raw2["body"].read())["content"][0]["text"].strip()
 
150
  except json.JSONDecodeError:
 
151
  pass
152
- return txt
153
 
154
- # --------------------------------------------------------------------------- #
155
- # 7) Chat input #
156
- # --------------------------------------------------------------------------- #
157
- def last_history(n=4):
158
- return "\n".join(f"{m['role'].capitalize()}: {m['content']}" for m in st.session_state.messages[-n:])
 
 
 
 
 
 
 
 
 
159
 
160
- if prompt := st.chat_input("Ask: Do I need an umbrella tomorrow?"):
161
- st.session_state.messages.append({"role":"user","content":prompt})
162
- with st.chat_message("user"): st.markdown(prompt)
 
 
 
 
 
163
 
 
164
  with st.chat_message("assistant"):
165
- with st.spinner("Thinking…"):
166
- reply = ask_claude(prompt, last_history())
167
- st.markdown(reply)
168
- st.session_state.messages.append({"role":"assistant","content":reply})
169
-
170
- # --------------------------------------------------------------------------- #
171
- # 8) Sidebar #
172
- # --------------------------------------------------------------------------- #
 
 
 
173
  with st.sidebar:
174
  st.image("https://img.icons8.com/clouds/100/umbrella.png", width=100)
175
- st.markdown("### About\nUses **Claude 3 Sonnet (Bedrock)** + **OpenWeatherMap**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # app.py ─ Weather Umbrella Advisor (Streamlit + Claude 3 + OpenWeatherMap)
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ import os
5
+ import json
6
+ import requests
7
+ import boto3
8
+ import streamlit as st
9
  from dotenv import load_dotenv
10
 
11
+ # ─────────────────────────────────────────────────────────────────────────────
12
+ # 1) Load environment variables (for local .env / HF Secrets)
13
+ # ─────────────────────────────────────────────────────────────────────────────
14
  load_dotenv()
15
+
16
  OPENWEATHERMAP_API_KEY = os.getenv("OPENWEATHERMAP_API_KEY")
17
  AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
18
+ AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") # may be None
19
+ AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") # may be None
20
+
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+ # 2) Helper to mask credentials (so we can print a hint in the UI)
23
+ # ─────────────────────────────────────────────────────────────────────────────
24
+ def _mask(val: str) -> str:
25
+ """
26
+ Returns a masked version of `val`, showing first 4 + last 4 chars
27
+ e.g. AKIA1234abcd...WXYZ5678
28
+ """
29
+ if not val:
30
+ return "None"
31
+ if len(val) <= 8:
32
+ return val
33
+ return val[:4] + "..." + val[-4:]
34
 
35
+ # ─────────────────────────────────────────────────────────────────────────────
36
+ # 3) Detect if keys are reversed: we expect ACCESS_KEY_ID to start with AKIA/ASIA
37
+ # If not, but SECRET_ACCESS_KEY starts with AKIA/ASIA, swap them.
38
+ # ─────────────────────────────────────────────────────────────────────────────
39
+ def _looks_like_access_key(key: str) -> bool:
40
+ """
41
+ AWS access key IDs typically start with 'AKIA' or 'ASIA' and are 20 characters long.
42
+ """
43
+ return bool(key) and (key.startswith("AKIA") or key.startswith("ASIA")) and len(key) == 20
44
+
45
+ # If ACCESS_KEY_ID doesn’t look like an AKIA/ASIA but SECRET_ACCESS_KEY does, swap:
46
+ if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
47
+ if not _looks_like_access_key(AWS_ACCESS_KEY_ID) and _looks_like_access_key(AWS_SECRET_ACCESS_KEY):
48
+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY, AWS_ACCESS_KEY_ID
49
 
50
+ # ─────────────────────────────────────────────────────────────────────────────
51
+ # 4) Initialize boto3 Session / Bedrock client
52
+ # – If both AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY exist, pass them explicitly
53
+ # – Otherwise, fall back to default credential chain (IAM role, container credentials, etc.)
54
+ # ─────────────────────────────────────────────────────────────────────────────
55
  if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
56
  session = boto3.Session(
57
  aws_access_key_id = AWS_ACCESS_KEY_ID,
 
59
  region_name = AWS_REGION,
60
  )
61
  else:
 
62
  session = boto3.Session(region_name=AWS_REGION)
63
 
64
  bedrock = session.client("bedrock-runtime")
65
 
66
+ # Quick sanity‐check: if credentials are still invalid, this will raise immediately.
67
  try:
68
+ _ = bedrock.meta.region_name # just to force the client to exist
69
  except Exception as e:
70
+ st.error(f"⚠️ Credential problem: {e}")
71
  st.stop()
72
 
73
+ # ─────────────────────────────────────────────────────────────────────────────
74
+ # 5) Streamlit Page Configuration & Header
75
+ # ─────────────────────────────────────────────────────────────────────────────
76
  st.set_page_config(page_title="🌀️ Umbrella Advisor", page_icon="β˜”", layout="centered")
77
+
78
+ st.markdown(
79
+ """
80
+ <div style="text-align:center">
81
+ <h1 style="color:#3c79f5;">β˜” Weather Umbrella Advisor</h1>
82
+ <p style="font-size:18px">
83
+ Ask if you need an umbrella tomorrow – powered by <b>Claude 3 Sonnet (Bedrock)</b> + <b>OpenWeatherMap</b>.
84
+ </p>
85
+ </div>
86
+ """,
87
+ unsafe_allow_html=True,
88
+ )
89
+
90
+ # ─────────────────────────────────────────────────────────────────────────────
91
+ # 6) Show masked credentials for debugging (so you can see if the swap logic worked)
92
+ # ─────────────────────────────────────────────────────────────────────────────
93
+ st.markdown(
94
+ f"""
95
+ **Debug** (masked credentials):
96
+ β€’ AWS_ACCESS_KEY_ID = `{_mask(AWS_ACCESS_KEY_ID)}`
97
+ β€’ AWS_SECRET_ACCESS_KEY = `{_mask(AWS_SECRET_ACCESS_KEY)}`
98
+ """,
99
+ unsafe_allow_html=True,
100
+ )
101
+
102
+ # ─────────────────────────────────────────────────────────────────────────────
103
+ # 7) Conversation state
104
+ # ─────────────────────────────────────────────────────────────────────────────
105
  if "messages" not in st.session_state:
106
  st.session_state.messages = []
107
 
 
109
  with st.chat_message(m["role"]):
110
  st.markdown(m["content"])
111
 
112
+ # ─────────────────────────────────────────────────────────────────────────────
113
+ # 8) Helper: get_weather(city) β†’ calls OpenWeatherMap and returns JSON
114
+ # ──────────────────────────────────────────────��──────────────────────────────
115
  def get_weather(city: str):
116
+ """
117
+ Fetches a 24‐hour forecast (8 x 3‐hour intervals) for `city`.
118
+ Returns either:
119
+ { "location": "CityName", "forecast": [ ... ] }
120
+ or
121
+ { "error": "Error message" }
122
+ """
123
+ city = city.strip()
124
+ if not city:
125
+ return {"error": "Please provide a valid city name."}
126
 
 
127
  try:
128
+ # 1) Get lat/lon
129
+ geo_url = (
130
+ f"http://api.openweathermap.org/geo/1.0/direct"
131
+ f"?q={city}&limit=1&appid={OPENWEATHERMAP_API_KEY}"
132
+ )
133
+ geo_resp = requests.get(geo_url, timeout=10).json()
134
+ if not geo_resp:
135
+ return {"error": f"City '{city}' not found."}
136
+ lat, lon = geo_resp[0]["lat"], geo_resp[0]["lon"]
137
+
138
+ # 2) Get 5‐day / 3hr forecast
139
+ weather_url = (
140
+ f"http://api.openweathermap.org/data/2.5/forecast"
141
+ f"?lat={lat}&lon={lon}"
142
+ f"&appid={OPENWEATHERMAP_API_KEY}&units=metric"
143
+ )
144
+ weather_data = requests.get(weather_url, timeout=10).json()
145
+ if "list" not in weather_data:
146
+ return {"error": f"Unable to fetch forecast for '{city}'."}
147
+
148
+ forecast = []
149
+ for f in weather_data["list"][:8]: # Next 24 hours β‰ˆ 8 slots at 3 each
150
+ forecast.append({
151
+ "time": f["dt_txt"],
152
+ "description": f["weather"][0]["description"].capitalize(),
153
+ "rain_probability": round(f.get("pop", 0) * 100, 1),
154
+ "temp": f["main"]["temp"],
155
+ "humidity": f["main"]["humidity"]
156
+ })
157
+
158
+ return {"location": city.title(), "forecast": forecast}
159
+
160
+ except Exception as ex:
161
+ return {"error": str(ex)}
162
+
163
+ # ─────────────────────────────────────────────────────────────────────────────
164
+ # 9) ReAct System Prompt & Helper to ask Claude (Bedrock)
165
+ # ─────────────────────────────────────────────────────────────────────────────
166
+ SYSTEM_PROMPT = """
167
+ You are a helpful umbrella advisor using the ReAct (Reasoning + Acting) methodology.
168
+
169
+ Steps:
170
+ 1. Think about the user’s question.
171
+ 2. Act by calling get_weather(location) if needed.
172
+ 3. Observe the weather result.
173
+ 4. Reason and respond.
174
+
175
+ When you need weather data, respond _exactly_ in this JSON format (no extra text):
176
+ {
177
+ "thought": "…",
178
+ "action": "get_weather",
179
+ "action_input": {"location": "CityName"}
180
+ }
181
+
182
+ If no location is provided, ask the user to specify one.
183
+
184
+ Once you have the forecast, give a final, friendly answer such as:
185
+ "You do not need an umbrella tomorrow in London because it will be sunny with 0% chance of rain."
186
+ """
187
+
188
+ def ask_claude(user_input: str, history: str = "") -> str:
189
+ """
190
+ 1. Send the initial ReAct prompt to Claude, including user_input + history.
191
+ 2. Parse Claude’s JSON: if action == "get_weather", call get_weather(…).
192
+ 3. Feed the weather data back into Claude for final reasoning.
193
+ 4. Return Claude’s final text reply.
194
+ """
195
+ # Step 1: Initial ReAct call
196
+ body1 = {
197
  "anthropic_version": "bedrock-2023-05-31",
198
  "max_tokens": 1000,
199
  "temperature": 0.7,
200
  "top_p": 0.9,
201
+ "messages": [
202
+ {"role": "user", "content": f"{SYSTEM_PROMPT}\n\nHistory:\n{history}\n\nUser: {user_input}"}
203
+ ]
 
204
  }
205
+ resp1 = bedrock.invoke_model(
206
  modelId="anthropic.claude-3-sonnet-20240229-v1:0",
207
  contentType="application/json",
208
  accept="application/json",
209
+ body=json.dumps(body1)
210
  )
211
+ text1 = json.loads(resp1["body"].read())["content"][0]["text"].strip()
212
 
213
+ # Step 2: Try parsing as JSON
214
  try:
215
+ parsed = json.loads(text1)
216
+ if parsed.get("action") == "get_weather":
217
+ city = parsed["action_input"].get("location", "").strip()
218
+ if not city:
219
+ return "🌍 I need a city nameβ€”could you please tell me which city you mean?"
220
 
221
+ wx = get_weather(city)
222
+ if "error" in wx:
223
+ return wx["error"]
 
224
 
225
+ # Step 3: Ask Claude to reason over the weather data
226
+ weather_json = json.dumps(wx, indent=2)
227
+ prompt2 = (
228
+ f"Here is the forecast for {wx['location']}:\n\n"
229
+ f"{weather_json}\n\n"
230
+ "Based on this data, answer whether the user should carry an umbrella tomorrow "
231
+ "in a friendly, conversational way (YES/NO + reasoning)."
232
+ )
233
  body2 = {
234
  "anthropic_version": "bedrock-2023-05-31",
235
  "max_tokens": 500,
236
  "temperature": 0.7,
237
+ "messages": [{"role": "user", "content": prompt2}]
238
  }
239
+ resp2 = bedrock.invoke_model(
240
  modelId="anthropic.claude-3-sonnet-20240229-v1:0",
241
  contentType="application/json",
242
  accept="application/json",
243
  body=json.dumps(body2)
244
  )
245
+ return json.loads(resp2["body"].read())["content"][0]["text"].strip()
246
+
247
  except json.JSONDecodeError:
248
+ # If it wasn’t valid JSON, just return whatever Claude replied
249
  pass
 
250
 
251
+ return text1
252
+
253
+ # ─────────────────────────────────────────────────────────────────────────────
254
+ # 10) Build conversation history helper
255
+ # ─────────────────────────────────────────────────────────────────────────────
256
+ def _build_history(n: int = 4) -> str:
257
+ """
258
+ Returns the last n messages formatted as:
259
+ User: ...
260
+ Assistant: ...
261
+ so that Claude sees recent turns.
262
+ """
263
+ hist = st.session_state.messages[-n:]
264
+ return "\n".join(f"{m['role'].capitalize()}: {m['content']}" for m in hist)
265
 
266
+ # ─────────────────────────────────────────────────────────────────────────────
267
+ # 11) Main Chat Input / Display Loop
268
+ # ─────────────────────────────────────────────────────────────────────────────
269
+ if user_query := st.chat_input("Ask: Do I need an umbrella tomorrow?"):
270
+ # 1) Append user message locally
271
+ st.session_state.messages.append({"role": "user", "content": user_query})
272
+ with st.chat_message("user"):
273
+ st.markdown(user_query)
274
 
275
+ # 2) Get assistant reply
276
  with st.chat_message("assistant"):
277
+ with st.spinner("πŸ€” Thinking…"):
278
+ history = _build_history()
279
+ assistant_reply = ask_claude(user_query, history)
280
+ st.markdown(assistant_reply)
281
+
282
+ # 3) Append assistant reply to state
283
+ st.session_state.messages.append({"role": "assistant", "content": assistant_reply})
284
+
285
+ # ─────────────────────────────────────────────────────────────────────────────
286
+ # 12) Sidebar (Branding / Help)
287
+ # ─────────────────────────────────────────────────────────────────────────────
288
  with st.sidebar:
289
  st.image("https://img.icons8.com/clouds/100/umbrella.png", width=100)
290
+ st.markdown("## β˜€οΈ About")
291
+ st.markdown(
292
+ """
293
+ **Weather Umbrella Advisor**
294
+ - Uses **OpenWeatherMap** for real‐time forecast
295
+ - Uses **Claude 3 Sonnet (AWS Bedrock)** to reason via ReAct
296
+ - Provides clear YES/NO umbrella advice with reasoning
297
+
298
+ **Try these:**
299
+ - "Should I bring an umbrella tomorrow?"
300
+ - "Will it rain in Delhi tomorrow?"
301
+ - "Do I need an umbrella in Tokyo?"
302
+ """
303
+ )