pymite6941's picture
Deploy full Fitness AI Agents backend: auth, DB, watch sync, GPS routes, AI analysis
6cc9547 verified
Raw
History Blame Contribute Delete
4.99 kB
import asyncio
import csv
import json
import os
import tempfile
from fastapi import APIRouter, Depends, HTTPException
from datetime import datetime, timezone
from auth import get_user_id
from db import get_db
from models.analysis import AnalysisRequest
from bots import Bots
router = APIRouter()
# Columns from the routes table that are meaningful for analysis.
# Excludes coordinates (raw lat/lng blob), user_id, and id.
_ROUTE_COLUMNS = [
"started_at", "ended_at", "workout_type",
"distance_meters", "duration_seconds", "pace", "calories_burned", "notes",
]
def _write_csv(rows: list[dict], columns: list[str]) -> str:
"""Write rows to a temp CSV (only the given columns) and return the path."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, newline="") as f:
writer = csv.DictWriter(f, fieldnames=columns, extrasaction="ignore")
writer.writeheader()
writer.writerows(rows)
return f.name
@router.post("/")
async def run_analysis(request: AnalysisRequest, user_id: str = Depends(get_user_id)):
"""Pull the user's watch data + routes, run the AI crew, store and return the result."""
db = await get_db()
# ── Watch readings + workouts ──────────────────────────────────────────
wd_query = db.table("watch_data").select("*").eq("user_id", user_id)
if request.date_from:
wd_query = wd_query.gte("timestamp", request.date_from.isoformat())
if request.date_to:
wd_query = wd_query.lte("timestamp", request.date_to.isoformat())
wd_result = await wd_query.order("timestamp").execute()
wd_rows = wd_result.data
if not wd_rows:
raise HTTPException(status_code=404, detail="No watch data found for this user")
for row in wd_rows:
row.pop("user_id", None)
# ── GPS routes ─────────────────────────────────────────────────────────
rt_query = db.table("routes").select(", ".join(_ROUTE_COLUMNS)).eq("user_id", user_id)
if request.date_from:
rt_query = rt_query.gte("started_at", request.date_from.isoformat())
if request.date_to:
rt_query = rt_query.lte("started_at", request.date_to.isoformat())
rt_result = await rt_query.order("started_at").execute()
rt_rows = rt_result.data
# ── Write CSVs ─────────────────────────────────────────────────────────
tmp_watch = _write_csv(wd_rows, list(wd_rows[0].keys()))
tmp_routes = _write_csv(rt_rows, _ROUTE_COLUMNS) if rt_rows else None
# Pass both paths (newline-separated) β€” the crew's task descriptions already
# handle multiple files and say "If multiple files, analyze together."
data_input = tmp_watch
if tmp_routes:
data_input = f"{tmp_watch}\n{tmp_routes}"
try:
bots = Bots(context=request.context)
loop = asyncio.get_running_loop()
raw_json = await loop.run_in_executor(None, lambda: bots.create_crew(data=data_input))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
finally:
if os.path.exists(tmp_watch):
os.unlink(tmp_watch)
if tmp_routes and os.path.exists(tmp_routes):
os.unlink(tmp_routes)
# Parse the structured FormattedOutput returned by bots.create_crew()
parsed: dict = {}
try:
parsed = json.loads(raw_json)
except Exception:
parsed = {}
record = {
"user_id": user_id,
"context": request.context,
"summary": parsed.get("summary") or raw_json,
"key_findings": parsed.get("findings") or [],
"recommendations": parsed.get("recommendations") or [],
"output_type": parsed.get("output_type"),
"chart_type": parsed.get("chart_type"),
"chart_title": parsed.get("chart_title"),
"data_points": parsed.get("data_points"),
"metrics": parsed.get("metrics"),
"table_headers": parsed.get("table_headers"),
"table_rows": parsed.get("table_rows"),
"quality_score": parsed.get("quality_score"),
"quality_verdict": parsed.get("quality_verdict"),
"raw_output": parsed or None, # full payload β€” lets frontend render comparison/heatmap/code
"created_at": datetime.now(timezone.utc).isoformat(),
}
# output_type must always be present so the frontend can pick the right renderer
if not record.get("output_type"):
record["output_type"] = "report"
# Strip remaining None values so Supabase doesn't complain about unknown columns
record = {k: v for k, v in record.items() if v is not None}
await db.table("analyses").insert(record).execute()
return record