From 39962fa12023ae6a98d0570cd07db4747bff407e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:10:28 +0000 Subject: [PATCH 1/3] feat(backend): implement proactive weather sentinel using apscheduler and open-meteo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added apscheduler and httpx to requirements.txt. - Created `backend/tasks.py` with an async job to check morning weather (filtered strictly by > 2.0mm rain or > 40.0°C heat) and invoke the AI using a hidden system prompt. - Handled dynamic location configuration by updating `get_default_location()` in `backend/users.py` with a fallback to Maheshpura coordinates. - Integrated the scheduler gracefully using FastAPI's modern `@asynccontextmanager` lifespan in `backend/server.py` and scheduled it to run at 07:00 AM IST. - Saved AI-generated weather warnings to the DB under "Family Group" and broadcasted the payload asynchronously to all connected users. Co-authored-by: DivyanshuChipa <211708943+DivyanshuChipa@users.noreply.github.com> --- backend/requirements.txt | 4 +- backend/server.py | 20 +++++++++- backend/tasks.py | 84 ++++++++++++++++++++++++++++++++++++++++ backend/users.py | 25 ++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 backend/tasks.py diff --git a/backend/requirements.txt b/backend/requirements.txt index 69c31d9..41d1201 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,4 +9,6 @@ pytesseract requests PyPDF2 chromadb -psutil \ No newline at end of file +psutil +apscheduler +httpx diff --git a/backend/server.py b/backend/server.py index 23f6480..da84d0a 100644 --- a/backend/server.py +++ b/backend/server.py @@ -3,6 +3,7 @@ # lan_server/server.py import os import secrets +from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi import Header, HTTPException from fastapi.middleware.cors import CORSMiddleware @@ -69,8 +70,25 @@ def verify_admin(x_admin_key: str = Header(None)): print(f"🚫 Admin Access Denied: Key mismatch. Received length: {len(clean_key)} (Expected: {len(clean_secret)}), Masked input: {masked_key}") print("💡 TIP: Copy the EXACT random secret from the server logs above.") raise HTTPException(status_code=403, detail="Admin access denied") + +# ================= BACKGROUND TASKS & SCHEDULER ================= +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from tasks import check_morning_weather + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Setup Scheduler + scheduler = AsyncIOScheduler(timezone='Asia/Kolkata') + # Run daily at 07:00 AM + scheduler.add_job(check_morning_weather, 'cron', hour=7, minute=0) + scheduler.start() + print("🌅 Weather Sentinel scheduler started (Runs at 07:00 AM IST).") + yield + scheduler.shutdown() + print("🌅 Weather Sentinel scheduler stopped.") + # ================= FASTAPI APP ================= -app = FastAPI(title="LAN Chat Server (modular)") +app = FastAPI(title="LAN Chat Server (modular)", lifespan=lifespan) # CORS: allow origins (disabled credentials for security) app.add_middleware( diff --git a/backend/tasks.py b/backend/tasks.py new file mode 100644 index 0000000..ea537b1 --- /dev/null +++ b/backend/tasks.py @@ -0,0 +1,84 @@ +import httpx +import json +import asyncio +from datetime import datetime +from users import get_default_location, get_ai_config, get_all_users +from messages import save_message +from chat import send_to_user +from lumir.ai_engine import ask_ai + +async def check_morning_weather(): + """ + Scheduled job that checks the morning weather and triggers the AI to automatically + warn the 'Family Group' if extreme conditions exist. + """ + lat, lon = get_default_location() + + # Open-Meteo API endpoint and parameters + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": lat, + "longitude": lon, + "daily": "precipitation_sum,temperature_2m_max", + "timezone": "auto" + } + + try: + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params, timeout=10.0) + response.raise_for_status() + data = response.json() + + daily = data.get("daily", {}) + precipitation_sum = daily.get("precipitation_sum", [0])[0] + temperature_2m_max = daily.get("temperature_2m_max", [0])[0] + + # Strict filter for alert + alert_reason = None + if precipitation_sum > 2.0: + alert_reason = f"heavy rain ({precipitation_sum}mm)" + elif temperature_2m_max > 40.0: + alert_reason = f"extreme heat ({temperature_2m_max}°C)" + + if alert_reason: + config = get_ai_config() + system_prompt = ( + f"System Context: Aaj {lat},{lon} mein {alert_reason} hone wali hai. " + f"Act like a caring family member and write a warning message for the Family Group. " + f"Keep it under 15 words. Do NOT use markdown. Say good morning." + ) + + # Trigger AI in background (we use the main loop or asyncio thread if needed) + ai_reply = ask_ai(prompt=system_prompt, config=config, history=[], sender="System") + + if ai_reply: + ai_reply = ai_reply.strip() + + # Save to DB + msg_id = save_message( + text=ai_reply, + sender="Lumir", + receiver="Family Group", + msg_type="text" + ) + + # Broadcast payload + bot_reply = { + "type": "text", + "text": ai_reply, + "sender": "Lumir", + "receiver": "Family Group", + "timestamp": int(datetime.now().timestamp() * 1000) + } + + payload_str = json.dumps(bot_reply) + + all_users = get_all_users() + for u in all_users: + username = u["username"] + # Call send_to_user asynchronously + await send_to_user(username, payload_str) + + except Exception as e: + # Silently log network or processing errors + print(f"Weather Sentinel Error: {e}") diff --git a/backend/users.py b/backend/users.py index c4535f9..df159d3 100644 --- a/backend/users.py +++ b/backend/users.py @@ -44,6 +44,31 @@ def init_db(): # --- Config Helpers --- +def get_default_location(): + """ + Fetches the default location (default_lat, default_lon) from the config table. + Defaults to Maheshpura coordinates: lat=26.2183, lon=78.1828 if not found. + """ + conn = sqlite3.connect(DATABASE_NAME) + cursor = conn.cursor() + cursor.execute("SELECT key, value FROM config WHERE key IN ('default_lat', 'default_lon')") + rows = cursor.fetchall() + conn.close() + + lat = 26.2183 + lon = 78.1828 + + for key, value in rows: + try: + if key == 'default_lat': + lat = float(value) + elif key == 'default_lon': + lon = float(value) + except ValueError: + pass + + return lat, lon + def set_require_approval(enabled: bool): conn = sqlite3.connect(DATABASE_NAME) cursor = conn.cursor() From b1c0e805d85443492f7fe41eccef77bc15074cb5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:29:01 +0000 Subject: [PATCH 2/3] refactor(backend): use asyncio.to_thread for ask_ai call - Update `ask_ai` invocation in `backend/tasks.py` to use `await asyncio.to_thread(ask_ai, ...)` to ensure the synchronous AI generation does not block the main async event loop. Co-authored-by: DivyanshuChipa <211708943+DivyanshuChipa@users.noreply.github.com> --- backend/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tasks.py b/backend/tasks.py index ea537b1..ef3bbdc 100644 --- a/backend/tasks.py +++ b/backend/tasks.py @@ -49,7 +49,7 @@ async def check_morning_weather(): ) # Trigger AI in background (we use the main loop or asyncio thread if needed) - ai_reply = ask_ai(prompt=system_prompt, config=config, history=[], sender="System") + ai_reply = await asyncio.to_thread(ask_ai, prompt=system_prompt, config=config, history=[], sender="System") if ai_reply: ai_reply = ai_reply.strip() From 8d926a915909e5878c1d16d592a276f47a3cfcca Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:52:47 +0000 Subject: [PATCH 3/3] refactor(backend): use Codex implementation for weather sentinel - Replace initial `backend/tasks.py` implementation with Codex's refined logic featuring offline delivery tracking (`create_delivery_entries`) and structured standard logging. - Update `backend/users.py` with Codex's config helper `set_default_location` and streamlined exception handling in `get_default_location`. Co-authored-by: DivyanshuChipa <211708943+DivyanshuChipa@users.noreply.github.com> --- backend/tasks.py | 164 +++++++++++++++++++++++++++-------------------- backend/users.py | 36 ++++++++--- 2 files changed, 120 insertions(+), 80 deletions(-) diff --git a/backend/tasks.py b/backend/tasks.py index ef3bbdc..66cf8d6 100644 --- a/backend/tasks.py +++ b/backend/tasks.py @@ -1,84 +1,106 @@ -import httpx -import json import asyncio -from datetime import datetime -from users import get_default_location, get_ai_config, get_all_users -from messages import save_message -from chat import send_to_user +import json +import logging +from datetime import datetime, timedelta, timezone + +import httpx + +from chat import connected_clients, send_to_user from lumir.ai_engine import ask_ai +from messages import create_delivery_entries, save_message +from users import get_ai_config, get_all_users, get_default_location + +LOGGER = logging.getLogger(__name__) + +IST = timezone(timedelta(hours=5, minutes=30)) +DEFAULT_LAT = 26.2183 +DEFAULT_LON = 78.1828 + + +def _get_default_coordinates() -> tuple[float, float]: + try: + return get_default_location() + except Exception: + LOGGER.debug("Weather sentinel could not read configured coordinates.", exc_info=True) + return DEFAULT_LAT, DEFAULT_LON + -async def check_morning_weather(): - """ - Scheduled job that checks the morning weather and triggers the AI to automatically - warn the 'Family Group' if extreme conditions exist. - """ - lat, lon = get_default_location() - - # Open-Meteo API endpoint and parameters - url = "https://api.open-meteo.com/v1/forecast" - params = { - "latitude": lat, - "longitude": lon, - "daily": "precipitation_sum,temperature_2m_max", - "timezone": "auto" +async def _broadcast_family_warning(message_text: str) -> None: + timestamp = int(datetime.now(IST).timestamp() * 1000) + payload = { + "type": "text", + "text": message_text, + "sender": "Lumir", + "receiver": "Family Group", + "timestamp": timestamp, } + msg_id = save_message( + text=message_text, + sender="Lumir", + receiver="Family Group", + msg_type="text", + ) + + recipients = [u["username"] for u in get_all_users() if u.get("username") != "Lumir"] + if recipients: + create_delivery_entries(msg_id, recipients) + + payload_json = json.dumps(payload) + for username in list(connected_clients.keys()): + await send_to_user(username, payload_json) + + +async def check_morning_weather() -> None: try: - async with httpx.AsyncClient() as client: - response = await client.get(url, params=params, timeout=10.0) + lat, lon = _get_default_coordinates() + location = f"{lat:.4f}, {lon:.4f}" + + async with httpx.AsyncClient(timeout=15.0) as client: + response = await client.get( + "https://api.open-meteo.com/v1/forecast", + params={ + "latitude": lat, + "longitude": lon, + "daily": "precipitation_sum,temperature_2m_max", + "timezone": "Asia/Kolkata", + "forecast_days": 1, + }, + ) response.raise_for_status() - data = response.json() + forecast = response.json().get("daily", {}) - daily = data.get("daily", {}) - precipitation_sum = daily.get("precipitation_sum", [0])[0] - temperature_2m_max = daily.get("temperature_2m_max", [0])[0] + precipitation_values = forecast.get("precipitation_sum") or [] + max_temp_values = forecast.get("temperature_2m_max") or [] + + precipitation_sum = float(precipitation_values[0]) if precipitation_values else 0.0 + temperature_2m_max = float(max_temp_values[0]) if max_temp_values else 0.0 - # Strict filter for alert alert_reason = None if precipitation_sum > 2.0: - alert_reason = f"heavy rain ({precipitation_sum}mm)" + alert_reason = f"{precipitation_sum:.1f}mm rain" elif temperature_2m_max > 40.0: - alert_reason = f"extreme heat ({temperature_2m_max}°C)" - - if alert_reason: - config = get_ai_config() - system_prompt = ( - f"System Context: Aaj {lat},{lon} mein {alert_reason} hone wali hai. " - f"Act like a caring family member and write a warning message for the Family Group. " - f"Keep it under 15 words. Do NOT use markdown. Say good morning." - ) + alert_reason = f"{temperature_2m_max:.1f}°C heat" + + if not alert_reason: + return + + hidden_prompt = ( + "System Context: Aaj {location} mein {alert_reason} hone wali hai. " + "Act like a caring family member and write a warning message for the Family Group. " + "Keep it under 15 words. Do NOT use markdown. Say good morning." + ).format(location=location, alert_reason=alert_reason) + + ai_config = get_ai_config() + generated_message = await asyncio.to_thread( + ask_ai, + prompt=hidden_prompt, + config=ai_config, + history=[], + sender="System", + ) + + await _broadcast_family_warning((generated_message or "Good morning. Please stay safe today.").strip()) - # Trigger AI in background (we use the main loop or asyncio thread if needed) - ai_reply = await asyncio.to_thread(ask_ai, prompt=system_prompt, config=config, history=[], sender="System") - - if ai_reply: - ai_reply = ai_reply.strip() - - # Save to DB - msg_id = save_message( - text=ai_reply, - sender="Lumir", - receiver="Family Group", - msg_type="text" - ) - - # Broadcast payload - bot_reply = { - "type": "text", - "text": ai_reply, - "sender": "Lumir", - "receiver": "Family Group", - "timestamp": int(datetime.now().timestamp() * 1000) - } - - payload_str = json.dumps(bot_reply) - - all_users = get_all_users() - for u in all_users: - username = u["username"] - # Call send_to_user asynchronously - await send_to_user(username, payload_str) - - except Exception as e: - # Silently log network or processing errors - print(f"Weather Sentinel Error: {e}") + except Exception: + LOGGER.debug("Weather sentinel failed during scheduled execution.", exc_info=True) diff --git a/backend/users.py b/backend/users.py index df159d3..aa47817 100644 --- a/backend/users.py +++ b/backend/users.py @@ -44,31 +44,49 @@ def init_db(): # --- Config Helpers --- + + +# --- Weather Location Config Helpers --- def get_default_location(): """ - Fetches the default location (default_lat, default_lon) from the config table. - Defaults to Maheshpura coordinates: lat=26.2183, lon=78.1828 if not found. + Fetch weather default location from config table. + Falls back to Maheshpura coordinates when missing/invalid. """ + lat = 26.2183 + lon = 78.1828 + conn = sqlite3.connect(DATABASE_NAME) cursor = conn.cursor() cursor.execute("SELECT key, value FROM config WHERE key IN ('default_lat', 'default_lon')") rows = cursor.fetchall() conn.close() - lat = 26.2183 - lon = 78.1828 - for key, value in rows: try: - if key == 'default_lat': + if key == "default_lat": lat = float(value) - elif key == 'default_lon': + elif key == "default_lon": lon = float(value) - except ValueError: - pass + except (TypeError, ValueError): + continue return lat, lon + +def set_default_location(lat: float, lon: float): + conn = sqlite3.connect(DATABASE_NAME) + cursor = conn.cursor() + cursor.execute( + "INSERT OR REPLACE INTO config (key, value) VALUES ('default_lat', ?)", + (str(lat),), + ) + cursor.execute( + "INSERT OR REPLACE INTO config (key, value) VALUES ('default_lon', ?)", + (str(lon),), + ) + conn.commit() + conn.close() + def set_require_approval(enabled: bool): conn = sqlite3.connect(DATABASE_NAME) cursor = conn.cursor()