Update app.py
Browse files
app.py
CHANGED
@@ -12,14 +12,9 @@ from geopy.exc import GeocoderTimedOut
|
|
12 |
import plotly.express as px
|
13 |
import plotly.graph_objects as go
|
14 |
import unicodedata
|
15 |
-
from config import GROQ_API_KEY, AIRVISUAL_API_KEY, DEFAULT_MODEL
|
16 |
import os
|
17 |
from dotenv import load_dotenv
|
18 |
|
19 |
-
from utils.weather_utils import get_weather, get_historical_weather, get_air_quality
|
20 |
-
from utils.pdf_utils import generate_pdf
|
21 |
-
from utils.constants import SYSTEM_PROMPTS, EXAMPLE_QUERIES, CSS_STYLE
|
22 |
-
|
23 |
# Load environment variables
|
24 |
load_dotenv()
|
25 |
|
@@ -40,13 +35,335 @@ st.set_page_config(
|
|
40 |
)
|
41 |
|
42 |
# === CSS STYLING ===
|
43 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
# === HEADER ===
|
46 |
st.markdown("<h1 class='title'>πΎ AI Climate & Smart Farming Assistant</h1>", unsafe_allow_html=True)
|
47 |
st.markdown("<p class='subtitle'>Real-time AI insights + live weather data</p>", unsafe_allow_html=True)
|
48 |
st.markdown("---")
|
49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
# === SIDEBAR ===
|
51 |
st.sidebar.header("π Features")
|
52 |
page = st.sidebar.radio(
|
@@ -63,9 +380,9 @@ if page == "AI Assistant Chat":
|
|
63 |
st.subheader("π§ AI Climate & Farming Chat Assistant")
|
64 |
option = st.selectbox(
|
65 |
"Choose a use case:",
|
66 |
-
list(
|
67 |
)
|
68 |
-
st.markdown(f"π‘ *Example*: {
|
69 |
|
70 |
user_input = st.text_area("Enter your question or describe your situation:")
|
71 |
|
@@ -75,7 +392,7 @@ if page == "AI Assistant Chat":
|
|
75 |
if st.button("Send to AI") and user_input.strip():
|
76 |
with st.spinner("Thinking..."):
|
77 |
messages = [
|
78 |
-
{"role": "system", "content":
|
79 |
]
|
80 |
# Append chat history for multi-turn
|
81 |
for chat in st.session_state.chat_history:
|
@@ -129,7 +446,7 @@ elif page == "Weather Data":
|
|
129 |
if location_method == "Enter City":
|
130 |
location = st.text_input("Enter a city or location (e.g., Los Angeles, Delhi):")
|
131 |
elif location_method == "Select Country":
|
132 |
-
country = st.selectbox("Select a country:",
|
133 |
city = st.text_input("Enter city name:")
|
134 |
location = f"{city}, {country}" if city else None
|
135 |
|
@@ -233,7 +550,7 @@ elif page == "Weather Data":
|
|
233 |
with tab3:
|
234 |
if st.button("Get Air Quality Data"):
|
235 |
with st.spinner("Fetching air quality data..."):
|
236 |
-
aq_data = get_air_quality(location
|
237 |
if aq_data is None:
|
238 |
st.error("Failed to fetch air quality data.")
|
239 |
else:
|
@@ -382,4 +699,4 @@ st.markdown("---")
|
|
382 |
st.markdown(
|
383 |
"<small>π Powered by <b>llama3-70b-8192</b> on Groq β’ Real-time data from Open-Meteo API</small>",
|
384 |
unsafe_allow_html=True
|
385 |
-
)
|
|
|
12 |
import plotly.express as px
|
13 |
import plotly.graph_objects as go
|
14 |
import unicodedata
|
|
|
15 |
import os
|
16 |
from dotenv import load_dotenv
|
17 |
|
|
|
|
|
|
|
|
|
18 |
# Load environment variables
|
19 |
load_dotenv()
|
20 |
|
|
|
35 |
)
|
36 |
|
37 |
# === CSS STYLING ===
|
38 |
+
st.markdown(
|
39 |
+
"""
|
40 |
+
<style>
|
41 |
+
.main {
|
42 |
+
background-color: #f9f9f9;
|
43 |
+
color: #222;
|
44 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
45 |
+
}
|
46 |
+
.title {
|
47 |
+
text-align: center;
|
48 |
+
color: #2E7D32;
|
49 |
+
font-weight: 800;
|
50 |
+
}
|
51 |
+
.subtitle {
|
52 |
+
text-align: center;
|
53 |
+
font-size: 18px;
|
54 |
+
margin-bottom: 20px;
|
55 |
+
color: #4CAF50;
|
56 |
+
}
|
57 |
+
.history-box {
|
58 |
+
background-color: #e8f5e9;
|
59 |
+
padding: 10px;
|
60 |
+
margin-bottom: 10px;
|
61 |
+
border-radius: 8px;
|
62 |
+
border-left: 5px solid #66bb6a;
|
63 |
+
color: #000000;
|
64 |
+
}
|
65 |
+
.ai-response {
|
66 |
+
background-color: #c8e6c9;
|
67 |
+
padding: 10px;
|
68 |
+
margin-bottom: 15px;
|
69 |
+
border-radius: 10px;
|
70 |
+
white-space: pre-wrap;
|
71 |
+
color: #000000;
|
72 |
+
}
|
73 |
+
.user-input {
|
74 |
+
background-color: #dcedc8;
|
75 |
+
padding: 8px;
|
76 |
+
border-radius: 8px;
|
77 |
+
font-weight: bold;
|
78 |
+
margin-bottom: 5px;
|
79 |
+
color: #000000;
|
80 |
+
}
|
81 |
+
.download-button {
|
82 |
+
background-color: #4CAF50;
|
83 |
+
color: white;
|
84 |
+
padding: 10px 20px;
|
85 |
+
border-radius: 5px;
|
86 |
+
text-decoration: none;
|
87 |
+
display: inline-block;
|
88 |
+
margin: 10px 0;
|
89 |
+
}
|
90 |
+
.insight-box {
|
91 |
+
background-color: #e1f5fe;
|
92 |
+
padding: 15px;
|
93 |
+
border-radius: 10px;
|
94 |
+
margin: 15px 0;
|
95 |
+
border-left: 4px solid #0288d1;
|
96 |
+
color: #000000;
|
97 |
+
font-weight: 500;
|
98 |
+
line-height: 1.6;
|
99 |
+
}
|
100 |
+
</style>
|
101 |
+
""",
|
102 |
+
unsafe_allow_html=True
|
103 |
+
)
|
104 |
|
105 |
# === HEADER ===
|
106 |
st.markdown("<h1 class='title'>πΎ AI Climate & Smart Farming Assistant</h1>", unsafe_allow_html=True)
|
107 |
st.markdown("<p class='subtitle'>Real-time AI insights + live weather data</p>", unsafe_allow_html=True)
|
108 |
st.markdown("---")
|
109 |
|
110 |
+
# === SYSTEM PROMPTS ===
|
111 |
+
system_prompts = {
|
112 |
+
"Track Pollution": (
|
113 |
+
"You are an expert environmental scientist. "
|
114 |
+
"Help users understand pollution levels in air, water, or soil using scientific reasoning. "
|
115 |
+
"Provide actionable recommendations for improvement."
|
116 |
+
),
|
117 |
+
"Carbon Emissions": (
|
118 |
+
"You are a sustainability advisor. "
|
119 |
+
"Estimate and explain carbon emissions, suggest reductions and eco-friendly alternatives. "
|
120 |
+
"Include cost-benefit analysis and ROI calculations."
|
121 |
+
),
|
122 |
+
"Predict Climate Patterns": (
|
123 |
+
"You are a climate researcher. Predict or explain regional climate changes using current and historical data. "
|
124 |
+
"Include statistical analysis and confidence intervals."
|
125 |
+
),
|
126 |
+
"Smart Farming Advice": (
|
127 |
+
"You are an AI-powered farming assistant. Help users with crop selection, irrigation, pest control, and yield optimization. "
|
128 |
+
"Focus on sustainable practices and resource efficiency."
|
129 |
+
),
|
130 |
+
}
|
131 |
+
|
132 |
+
# === EXAMPLE QUERIES ===
|
133 |
+
example_queries = {
|
134 |
+
"Track Pollution": "e.g., What's the air quality near Lahore right now?",
|
135 |
+
"Carbon Emissions": "e.g., How can a factory reduce CO2 output sustainably?",
|
136 |
+
"Predict Climate Patterns": "e.g., What climate changes are expected in sub-Saharan Africa?",
|
137 |
+
"Smart Farming Advice": "e.g., Best crops to grow in dry conditions in Uganda?",
|
138 |
+
}
|
139 |
+
|
140 |
+
# === UTILS: API CALLS ===
|
141 |
+
def get_weather(location: str):
|
142 |
+
try:
|
143 |
+
# First, get coordinates for the location
|
144 |
+
geocoding_url = f"https://geocoding-api.open-meteo.com/v1/search?name={location}&count=1"
|
145 |
+
geo_resp = requests.get(geocoding_url, timeout=10)
|
146 |
+
geo_resp.raise_for_status()
|
147 |
+
geo_data = geo_resp.json()
|
148 |
+
|
149 |
+
if not geo_data.get('results'):
|
150 |
+
return None
|
151 |
+
|
152 |
+
lat = geo_data['results'][0]['latitude']
|
153 |
+
lon = geo_data['results'][0]['longitude']
|
154 |
+
location_name = geo_data['results'][0]['name']
|
155 |
+
|
156 |
+
# Then get weather data for those coordinates
|
157 |
+
weather_url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code"
|
158 |
+
weather_resp = requests.get(weather_url, timeout=10)
|
159 |
+
weather_resp.raise_for_status()
|
160 |
+
weather_data = weather_resp.json()
|
161 |
+
|
162 |
+
# Weather code to description mapping
|
163 |
+
weather_codes = {
|
164 |
+
0: "Clear sky",
|
165 |
+
1: "Mainly clear",
|
166 |
+
2: "Partly cloudy",
|
167 |
+
3: "Overcast",
|
168 |
+
45: "Foggy",
|
169 |
+
48: "Depositing rime fog",
|
170 |
+
51: "Light drizzle",
|
171 |
+
53: "Moderate drizzle",
|
172 |
+
55: "Dense drizzle",
|
173 |
+
61: "Slight rain",
|
174 |
+
63: "Moderate rain",
|
175 |
+
65: "Heavy rain",
|
176 |
+
71: "Slight snow",
|
177 |
+
73: "Moderate snow",
|
178 |
+
75: "Heavy snow",
|
179 |
+
77: "Snow grains",
|
180 |
+
80: "Slight rain showers",
|
181 |
+
81: "Moderate rain showers",
|
182 |
+
82: "Violent rain showers",
|
183 |
+
85: "Slight snow showers",
|
184 |
+
86: "Heavy snow showers",
|
185 |
+
95: "Thunderstorm",
|
186 |
+
96: "Thunderstorm with slight hail",
|
187 |
+
99: "Thunderstorm with heavy hail"
|
188 |
+
}
|
189 |
+
|
190 |
+
current = weather_data['current']
|
191 |
+
weather_code = current['weather_code']
|
192 |
+
weather_desc = weather_codes.get(weather_code, "Unknown")
|
193 |
+
|
194 |
+
return {
|
195 |
+
"location": location_name,
|
196 |
+
"description": weather_desc,
|
197 |
+
"temperature_C": current['temperature_2m'],
|
198 |
+
"humidity_%": current['relative_humidity_2m'],
|
199 |
+
"wind_speed_m/s": current['wind_speed_10m']
|
200 |
+
}
|
201 |
+
except Exception as e:
|
202 |
+
return None
|
203 |
+
|
204 |
+
def get_historical_weather(location: str, days: int = 7):
|
205 |
+
try:
|
206 |
+
# Get coordinates
|
207 |
+
geocoding_url = f"https://geocoding-api.open-meteo.com/v1/search?name={location}&count=1"
|
208 |
+
geo_resp = requests.get(geocoding_url, timeout=10)
|
209 |
+
geo_resp.raise_for_status()
|
210 |
+
geo_data = geo_resp.json()
|
211 |
+
|
212 |
+
if not geo_data.get('results'):
|
213 |
+
return None
|
214 |
+
|
215 |
+
lat = geo_data['results'][0]['latitude']
|
216 |
+
lon = geo_data['results'][0]['longitude']
|
217 |
+
|
218 |
+
# Get historical data
|
219 |
+
end_date = datetime.now()
|
220 |
+
start_date = end_date - timedelta(days=days)
|
221 |
+
|
222 |
+
weather_url = (
|
223 |
+
f"https://api.open-meteo.com/v1/forecast"
|
224 |
+
f"?latitude={lat}&longitude={lon}"
|
225 |
+
f"&start_date={start_date.strftime('%Y-%m-%d')}"
|
226 |
+
f"&end_date={end_date.strftime('%Y-%m-%d')}"
|
227 |
+
f"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max"
|
228 |
+
)
|
229 |
+
|
230 |
+
weather_resp = requests.get(weather_url, timeout=10)
|
231 |
+
weather_resp.raise_for_status()
|
232 |
+
return weather_resp.json()
|
233 |
+
except Exception as e:
|
234 |
+
return None
|
235 |
+
|
236 |
+
def get_air_quality(location: str):
|
237 |
+
try:
|
238 |
+
# First, get coordinates for the location
|
239 |
+
geocoding_url = f"https://geocoding-api.open-meteo.com/v1/search?name={location}&count=1"
|
240 |
+
geo_resp = requests.get(geocoding_url, timeout=10)
|
241 |
+
geo_resp.raise_for_status()
|
242 |
+
geo_data = geo_resp.json()
|
243 |
+
|
244 |
+
if not geo_data.get('results'):
|
245 |
+
return None
|
246 |
+
|
247 |
+
lat = geo_data['results'][0]['latitude']
|
248 |
+
lon = geo_data['results'][0]['longitude']
|
249 |
+
|
250 |
+
# Try Open-Meteo API first
|
251 |
+
aq_url = f"https://air-quality-api.open-meteo.com/v1/air-quality?latitude={lat}&longitude={lon}¤t=pm10,pm2_5,ozone,nitrogen_dioxide,sulphur_dioxide"
|
252 |
+
aq_resp = requests.get(aq_url, timeout=10)
|
253 |
+
|
254 |
+
if aq_resp.status_code == 200:
|
255 |
+
aq_data = aq_resp.json()
|
256 |
+
if 'current' in aq_data:
|
257 |
+
return aq_data
|
258 |
+
|
259 |
+
# If Open-Meteo fails, try AirVisual API
|
260 |
+
airvisual_url = f"http://api.airvisual.com/v2/nearest_city?lat={lat}&lon={lon}&key={AIRVISUAL_API_KEY}"
|
261 |
+
airvisual_resp = requests.get(airvisual_url, timeout=10)
|
262 |
+
|
263 |
+
if airvisual_resp.status_code == 200:
|
264 |
+
airvisual_data = airvisual_resp.json()
|
265 |
+
if 'data' in airvisual_data and 'current' in airvisual_data['data']:
|
266 |
+
current = airvisual_data['data']['current']['pollution']
|
267 |
+
return {
|
268 |
+
'current': {
|
269 |
+
'pm10': current.get('p1'),
|
270 |
+
'pm2_5': current.get('p2'),
|
271 |
+
'ozone': current.get('o3'),
|
272 |
+
'nitrogen_dioxide': None,
|
273 |
+
'sulphur_dioxide': None
|
274 |
+
}
|
275 |
+
}
|
276 |
+
|
277 |
+
return None
|
278 |
+
except Exception as e:
|
279 |
+
print(f"Air quality error: {str(e)}")
|
280 |
+
return None
|
281 |
+
|
282 |
+
# === UTILS: PDF Generation ===
|
283 |
+
def clean_text_for_pdf(text):
|
284 |
+
"""Clean text to be PDF-safe by removing or replacing problematic characters"""
|
285 |
+
# Normalize Unicode characters
|
286 |
+
text = unicodedata.normalize('NFKD', text)
|
287 |
+
# Replace common problematic characters
|
288 |
+
replacements = {
|
289 |
+
'ΞΌ': 'micro',
|
290 |
+
'Β°': ' degrees',
|
291 |
+
'β': 'C',
|
292 |
+
'Β±': '+/-',
|
293 |
+
'Γ': 'x',
|
294 |
+
'Γ·': '/',
|
295 |
+
'β€': '<=',
|
296 |
+
'β₯': '>=',
|
297 |
+
'β ': '!=',
|
298 |
+
'β': 'infinity',
|
299 |
+
'β': '->',
|
300 |
+
'β': '<-',
|
301 |
+
'β': 'up',
|
302 |
+
'β': 'down',
|
303 |
+
'β': '<->',
|
304 |
+
'β': '~=',
|
305 |
+
'β': 'sum',
|
306 |
+
'β': 'product',
|
307 |
+
'β': 'sqrt',
|
308 |
+
'β«': 'integral',
|
309 |
+
'β': 'delta',
|
310 |
+
'β': 'nabla',
|
311 |
+
'β': 'partial',
|
312 |
+
'β': 'proportional to',
|
313 |
+
'β': 'infinity',
|
314 |
+
'β
': 'empty set',
|
315 |
+
'β': 'in',
|
316 |
+
'β': 'not in',
|
317 |
+
'β': 'subset',
|
318 |
+
'β': 'superset',
|
319 |
+
'βͺ': 'union',
|
320 |
+
'β©': 'intersection',
|
321 |
+
'β': 'for all',
|
322 |
+
'β': 'exists',
|
323 |
+
'β': 'does not exist',
|
324 |
+
'β΄': 'therefore',
|
325 |
+
'β΅': 'because'
|
326 |
+
}
|
327 |
+
for char, replacement in replacements.items():
|
328 |
+
text = text.replace(char, replacement)
|
329 |
+
return text
|
330 |
+
|
331 |
+
def generate_pdf(chat_history, title="AI Climate & Farming Advice"):
|
332 |
+
pdf = FPDF()
|
333 |
+
pdf.add_page()
|
334 |
+
|
335 |
+
# Use built-in font
|
336 |
+
pdf.set_font("helvetica", "B", 16)
|
337 |
+
pdf.cell(0, 10, clean_text_for_pdf(title), ln=True, align='C')
|
338 |
+
pdf.ln(10)
|
339 |
+
|
340 |
+
# Chat history
|
341 |
+
for chat in chat_history:
|
342 |
+
# User message
|
343 |
+
pdf.set_font("helvetica", "B", 12)
|
344 |
+
pdf.cell(0, 10, "User:", ln=True)
|
345 |
+
pdf.set_font("helvetica", "", 12)
|
346 |
+
# Clean and wrap text
|
347 |
+
user_text = clean_text_for_pdf(chat["user"])
|
348 |
+
pdf.multi_cell(0, 10, user_text)
|
349 |
+
pdf.ln(5)
|
350 |
+
|
351 |
+
# AI response
|
352 |
+
pdf.set_font("helvetica", "B", 12)
|
353 |
+
pdf.cell(0, 10, "AI Response:", ln=True)
|
354 |
+
pdf.set_font("helvetica", "", 12)
|
355 |
+
# Clean and wrap text
|
356 |
+
ai_text = clean_text_for_pdf(chat["ai"])
|
357 |
+
pdf.multi_cell(0, 10, ai_text)
|
358 |
+
pdf.ln(10)
|
359 |
+
|
360 |
+
return pdf.output(dest="S").encode("latin-1", "replace")
|
361 |
+
|
362 |
+
# === UTILS: Get Country List ===
|
363 |
+
def get_country_list():
|
364 |
+
countries = [country.name for country in pycountry.countries]
|
365 |
+
return sorted(countries)
|
366 |
+
|
367 |
# === SIDEBAR ===
|
368 |
st.sidebar.header("π Features")
|
369 |
page = st.sidebar.radio(
|
|
|
380 |
st.subheader("π§ AI Climate & Farming Chat Assistant")
|
381 |
option = st.selectbox(
|
382 |
"Choose a use case:",
|
383 |
+
list(system_prompts.keys())
|
384 |
)
|
385 |
+
st.markdown(f"π‘ *Example*: {example_queries[option]}")
|
386 |
|
387 |
user_input = st.text_area("Enter your question or describe your situation:")
|
388 |
|
|
|
392 |
if st.button("Send to AI") and user_input.strip():
|
393 |
with st.spinner("Thinking..."):
|
394 |
messages = [
|
395 |
+
{"role": "system", "content": system_prompts[option]},
|
396 |
]
|
397 |
# Append chat history for multi-turn
|
398 |
for chat in st.session_state.chat_history:
|
|
|
446 |
if location_method == "Enter City":
|
447 |
location = st.text_input("Enter a city or location (e.g., Los Angeles, Delhi):")
|
448 |
elif location_method == "Select Country":
|
449 |
+
country = st.selectbox("Select a country:", get_country_list())
|
450 |
city = st.text_input("Enter city name:")
|
451 |
location = f"{city}, {country}" if city else None
|
452 |
|
|
|
550 |
with tab3:
|
551 |
if st.button("Get Air Quality Data"):
|
552 |
with st.spinner("Fetching air quality data..."):
|
553 |
+
aq_data = get_air_quality(location)
|
554 |
if aq_data is None:
|
555 |
st.error("Failed to fetch air quality data.")
|
556 |
else:
|
|
|
699 |
st.markdown(
|
700 |
"<small>π Powered by <b>llama3-70b-8192</b> on Groq β’ Real-time data from Open-Meteo API</small>",
|
701 |
unsafe_allow_html=True
|
702 |
+
)
|