Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Transform isair/jarvis into a Discord-controlled voice assistant running on the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact. - bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral), voice channel join + voice receive/playback, pluggable VNC screen broadcast (selfbot live / noVNC / screenshot) - bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS behind a thin localhost HTTP API - .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite, docs/language-comparison.md and docs/vnc-xfce-setup.md Language decision: hybrid (Python brain + Node/bun Discord layer) because Discord blocks bot video; native screen broadcast only works via a Node selfbot library.
112 lines
4.2 KiB
Python
112 lines
4.2 KiB
Python
"""Fetch meals tool for nutrition tracking."""
|
|
|
|
from typing import Dict, Any, Optional, List, Callable
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
from ....debug import debug_log
|
|
from ...base import Tool, ToolContext
|
|
from ...types import ToolExecutionResult
|
|
|
|
|
|
def _normalize_time_range(args: Optional[Dict[str, Any]]) -> tuple[str, str]:
|
|
"""Normalize time range for meal fetching."""
|
|
now = datetime.now(timezone.utc)
|
|
since: Optional[str] = None
|
|
until: Optional[str] = None
|
|
if args and isinstance(args, dict):
|
|
try:
|
|
since_val = args.get("since_utc")
|
|
since = str(since_val) if since_val else None
|
|
except Exception:
|
|
since = None
|
|
try:
|
|
until_val = args.get("until_utc")
|
|
until = str(until_val) if until_val else None
|
|
except Exception:
|
|
until = None
|
|
if since is None and until is None:
|
|
# Default last 24h
|
|
return (now - timedelta(days=1)).isoformat(), now.isoformat()
|
|
if since is None and until is not None:
|
|
# backfill 24h prior to until
|
|
try:
|
|
until_dt = datetime.fromisoformat(until.replace("Z", "+00:00"))
|
|
except Exception:
|
|
until_dt = now
|
|
return (until_dt - timedelta(days=1)).isoformat(), until_dt.isoformat()
|
|
if since is not None and until is None:
|
|
return since, now.isoformat()
|
|
return since or (now - timedelta(days=1)).isoformat(), until or now.isoformat()
|
|
|
|
|
|
def summarize_meals(meals: List[Any]) -> str:
|
|
"""Summarize a list of meals with totals."""
|
|
lines: List[str] = []
|
|
total_kcal = 0.0
|
|
total_protein = 0.0
|
|
total_carbs = 0.0
|
|
total_fat = 0.0
|
|
for m in meals:
|
|
try:
|
|
desc = m["description"] if isinstance(m, dict) else m["description"]
|
|
except Exception:
|
|
desc = "meal"
|
|
try:
|
|
kcal = float(m["calories_kcal"]) if m["calories_kcal"] is not None else 0.0
|
|
except Exception:
|
|
kcal = 0.0
|
|
try:
|
|
prot = float(m["protein_g"]) if m["protein_g"] is not None else 0.0
|
|
except Exception:
|
|
prot = 0.0
|
|
try:
|
|
carbs = float(m["carbs_g"]) if m["carbs_g"] is not None else 0.0
|
|
except Exception:
|
|
carbs = 0.0
|
|
try:
|
|
fat = float(m["fat_g"]) if m["fat_g"] is not None else 0.0
|
|
except Exception:
|
|
fat = 0.0
|
|
total_kcal += kcal
|
|
total_protein += prot
|
|
total_carbs += carbs
|
|
total_fat += fat
|
|
lines.append(f"- {desc} (~{int(round(kcal))} kcal, {int(round(prot))}g P, {int(round(carbs))}g C, {int(round(fat))}g F)")
|
|
header = f"Meals: {len(meals)} | Total ~{int(round(total_kcal))} kcal, {int(round(total_protein))}g P, {int(round(total_carbs))}g C, {int(round(total_fat))}g F"
|
|
return header + ("\n" + "\n".join(lines) if lines else "")
|
|
|
|
|
|
class FetchMealsTool(Tool):
|
|
"""Tool for fetching meals from the nutrition database."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "fetchMeals"
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return "Retrieve meals from the database for a given time range with nutritional summary."
|
|
|
|
@property
|
|
def inputSchema(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"since_utc": {"type": "string", "description": "Start time in ISO format (UTC)"},
|
|
"until_utc": {"type": "string", "description": "End time in ISO format (UTC)"}
|
|
},
|
|
"required": []
|
|
}
|
|
|
|
def run(self, args: Optional[Dict[str, Any]], context: ToolContext) -> ToolExecutionResult:
|
|
"""Execute the fetch meals tool."""
|
|
context.user_print("📖 Retrieving your meals…")
|
|
since, until = _normalize_time_range(args if isinstance(args, dict) else None)
|
|
debug_log(f"fetchMeals: range since={since} until={until}", "nutrition")
|
|
meals = context.db.get_meals_between(since, until)
|
|
debug_log(f"fetchMeals: count={len(meals)}", "nutrition")
|
|
summary = summarize_meals([dict(r) for r in meals])
|
|
# Return raw meal summary for profile processing
|
|
context.user_print("✅ Meals retrieved.")
|
|
return ToolExecutionResult(success=True, reply_text=summary)
|