Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions backend/app/api/checkins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,47 @@
import datetime
from app.core.database import get_db
from app.core.security import get_current_user
from app.services.wellness import MEAL_CHECKIN_RESPONSES, SKIPPED_MEAL_RESPONSES

router = APIRouter()

MEAL_SOURCES = {"mess", "cooked", "home", "outside_cash", "snack", "other"}

class CheckinReq(BaseModel):
response: str
gap_hours: Optional[float] = None
food_gap_hours: Optional[float] = None
suggestion_given: Optional[str] = None
context_note: Optional[str] = None
stress_note: Optional[str] = None
meal_source: Optional[str] = None

@router.post("")
async def insert_checkin(req: CheckinReq, user_id: str = Depends(get_current_user)):
db = get_db()
log_id = str(uuid.uuid4())
gap_hours = req.gap_hours if req.gap_hours is not None else req.food_gap_hours
response = (req.response or "").strip().lower()
meal_source = (req.meal_source or "").strip().lower()
if meal_source not in MEAL_SOURCES:
meal_source = None
note = (req.context_note if req.context_note is not None else req.stress_note or "").strip()
if len(note) > 500:
note = note[:500]
is_meal_signal = response in MEAL_CHECKIN_RESPONSES or (
bool(meal_source) and response not in SKIPPED_MEAL_RESPONSES
)
await db.checkin_logs.insert_one({
"_id": log_id,
"user_id": user_id,
"response": req.response,
"response": response,
"gap_hours": gap_hours or 0,
"food_gap_hours": gap_hours or 0,
"suggestion_given": req.suggestion_given,
"stress_note": req.stress_note,
"context_note": note or None,
"stress_note": note or None,
"meal_source": meal_source,
"is_meal_signal": is_meal_signal,
"created_at": datetime.datetime.utcnow()
})
return {"status": "ok", "id": log_id}
380 changes: 124 additions & 256 deletions backend/app/api/insights.py

Large diffs are not rendered by default.

53 changes: 37 additions & 16 deletions backend/app/api/rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from app.services.bedrock import generate_text
from app.services.runway import build_runway_forecast, derive_pool_obligations
from app.services.subscriptions import detect_recurring_subscriptions
from app.services.wellness import current_meal_gap_hours, meal_signal_events

router = APIRouter()
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -117,15 +118,19 @@ async def get_campus_intel(user_id: str = Depends(get_current_user)):
profile = await db.profiles.find_one({"_id": user_id})

# Basic spending stats
since_7 = datetime.datetime.utcnow() - datetime.timedelta(days=7)
now = datetime.datetime.utcnow()
since_7 = now - datetime.timedelta(days=7)
cursor = db.transactions.find({"user_id": user_id, "created_at": {"$gte": since_7}})
txns = await cursor.to_list(length=500)
spend_7 = sum(t.get("amount", 0) for t in txns) / 100
food_txns = [t for t in txns if t.get("category") == "food"]
last_food_hours = 0
if food_txns:
last_food = max(food_txns, key=lambda t: t.get("created_at", datetime.datetime.min))
last_food_hours = (datetime.datetime.utcnow() - last_food["created_at"]).total_seconds() / 3600
checkins = await db.checkin_logs.find({
"user_id": user_id,
"created_at": {"$gte": since_7},
}).sort("created_at", -1).to_list(length=500)
meal_events = meal_signal_events(food_txns, checkins)
last_food_hours = current_meal_gap_hours(now, meal_events, default=0.0)
last_food_source = meal_events[-1]["source"] if meal_events else None

remaining = (profile.get("monthly_allowance", 0) / 100) if profile else 0

Expand All @@ -149,33 +154,49 @@ async def get_campus_intel(user_id: str = Depends(get_current_user)):
limit=5,
)

prompt = f"""You are PocketBuddy, an AI financial wellness guard for Indian college students.
prompt = f"""You are PocketBuddy, a student budget and routine assistant for Indian college students.
Student context:
- Spent Rs {spend_7:.0f} in last 7 days
- Remaining budget: Rs {remaining:.0f}
- Last food transaction: {last_food_hours:.0f} hours ago
- Last food payment/check-in signal: {last_food_hours:.0f} hours ago
- Trusted campus food options: {json.dumps([{"venue": f.get("venue_name"), "item": f.get("item_name"), "price_rs": f.get("price", 0)//100, "why": f.get("why"), "trust": f.get("trust_badge")} for f in ranked_foods[:5]], indent=None)}

Generate exactly 2 concise, specific, actionable sentences as a campus financial intelligence summary. Be direct, mention real numbers. No emojis. Do not cite any food option outside the trusted campus food options above."""
Generate exactly 2 concise, specific, actionable sentences as a campus financial intelligence summary.
Be direct and mention real numbers.
Describe only budget and routine signals; do not infer illness, stress, sleep quality, or medical risk.
If you mention food, cite only trusted campus food options above.
No emojis. No preamble."""

text = generate_text(prompt, max_tokens=120, temperature=0.2)
if text:
return {"summary": text, "source": "bedrock", "spend_7d": spend_7, "last_food_hours": round(last_food_hours, 1)}
return {
"summary": text,
"source": "bedrock",
"spend_7d": spend_7,
"last_food_hours": round(last_food_hours, 1),
"last_food_signal_source": last_food_source,
}
except Exception as exc:
logger.warning("Bedrock campus-intel failed: %s", exc)

# Local fallback
parts = []
routine_parts = []
if spend_7 > 0:
parts.append(f"You've spent {spend_7:.0f} in the last 7 days.")
routine_parts.append(f"You've spent Rs {spend_7:.0f} in the last 7 days.")
if last_food_hours > 8:
parts.append(f"Your last food transaction was {last_food_hours:.0f} hours ago — consider eating soon.")
routine_parts.append(f"Your last meal signal was {last_food_hours:.0f} hours ago; log a meal or use a trusted campus option if needed.")
elif last_food_hours > 0:
parts.append(f"Last meal logged {last_food_hours:.0f} hours ago, you're on track.")
routine_parts.append(f"Last meal signal was {last_food_hours:.0f} hours ago.")
if remaining > 0:
parts.append(f"₹{remaining:.0f} remaining in your current cycle.")
summary = " ".join(parts) if parts else "Start logging transactions to activate campus intelligence."
return {"summary": summary, "source": "local_fallback", "spend_7d": spend_7, "last_food_hours": round(last_food_hours, 1)}
routine_parts.append(f"Rs {remaining:.0f} remaining in your current cycle.")
summary = " ".join(routine_parts) if routine_parts else "Start logging transactions to activate campus intelligence."
return {
"summary": summary,
"source": "local_fallback",
"spend_7d": spend_7,
"last_food_hours": round(last_food_hours, 1),
"last_food_signal_source": last_food_source,
}


@router.get("/runway-intel")
Expand Down
Loading