singh-sahitya commited on
Commit
15e058d
Β·
1 Parent(s): fdd2d3d

phase 4.1 and 4.2 changes

Browse files
Files changed (2) hide show
  1. .gitignore +2 -1
  2. main.py +213 -0
.gitignore CHANGED
@@ -1 +1,2 @@
1
- .env
 
 
1
+ .env
2
+ tests/
main.py CHANGED
@@ -594,6 +594,219 @@ Original wake confidence: {signals.original_wake_confidence:.2f}"""
594
  return PostWakeResponse(status="ok", message="Activity normal.", severity="none")
595
 
596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  ## ─── User Profile (Phase 3A) ──────────────────────────────────
598
 
599
  class UserWakeProfile(BaseModel):
 
594
  return PostWakeResponse(status="ok", message="Activity normal.", severity="none")
595
 
596
 
597
+ ## ─── Pre-Alarm Intelligence (Phase 4.1) ──────────────────────
598
+
599
+ class PreAlarmContext(BaseModel):
600
+ user_id: Optional[str] = None
601
+ context: dict = {}
602
+
603
+ class PreAlarmResponse(BaseModel):
604
+ difficulty_adjustment: str # "harder", "easier", "normal"
605
+ reasoning: str
606
+ briefing_hint: str # short note for morning companion
607
+ early_alarm: bool # suggest triggering alarm early (light sleep detected)
608
+
609
+ PRE_ALARM_PROMPT = """You are an AI pre-alarm analyst for the Ankr alarm app. You receive contextual data gathered 30 minutes before a user's alarm. Your job is to analyze all signals and recommend difficulty adjustments.
610
+
611
+ You receive:
612
+ - **weather**: Temperature, conditions (rain/snow may affect commute)
613
+ - **calendar**: Today's events, time until first event (tight schedule = be more aggressive)
614
+ - **sleep**: Sleep duration and quality from Health Connect (poor sleep = harder to wake)
615
+ - **heart_rate**: Recent heart rate (low/resting = deep sleep, may need stronger alarm)
616
+ - **wake_profile**: User's historical success rate, streak, trend, relapse rate
617
+
618
+ Decision rules:
619
+ - If first event is within 90 min: recommend "harder" (user needs to be on time)
620
+ - If sleep quality is "poor" or total < 5 hours: recommend "harder" (sleep-deprived users struggle)
621
+ - If heart rate is very low (< 55 bpm) and user has high relapse rate: recommend "harder"
622
+ - If it's a weekend with no events and user has high success rate: recommend "easier"
623
+ - If user is on a long streak (7+ days) and trend is improving: recommend "normal" or "easier"
624
+ - If weather is severe (heavy rain, snow, extreme cold): mention in briefing_hint
625
+ - Set early_alarm=true ONLY if sleep data shows user is in light sleep AND alarm is within 10 min
626
+
627
+ You MUST respond with ONLY a JSON object:
628
+ {
629
+ "difficulty_adjustment": "harder" | "easier" | "normal",
630
+ "reasoning": "Brief explanation",
631
+ "briefing_hint": "One sentence context note for morning briefing",
632
+ "early_alarm": false
633
+ }
634
+ """
635
+
636
+ @app.post("/wake/pre-alarm-context", response_model=PreAlarmResponse)
637
+ async def pre_alarm_context(request: PreAlarmContext):
638
+ """Analyze pre-alarm context and recommend difficulty adjustments."""
639
+ ctx = request.context
640
+
641
+ signal_summary = f"""Analyze pre-alarm context for this user:
642
+
643
+ Weather: {json.dumps(ctx.get('weather'), default=str) if ctx.get('weather') else 'unavailable'}
644
+ Calendar: {json.dumps(ctx.get('calendar'), default=str) if ctx.get('calendar') else 'no events'}
645
+ Sleep: {json.dumps(ctx.get('sleep'), default=str) if ctx.get('sleep') else 'no sleep data'}
646
+ Heart Rate: {json.dumps(ctx.get('heart_rate'), default=str) if ctx.get('heart_rate') else 'unavailable'}
647
+ Wake Profile: {json.dumps(ctx.get('wake_profile'), default=str) if ctx.get('wake_profile') else 'new user'}
648
+ Day/Time: {ctx.get('day_of_week', 'unknown')}, {ctx.get('time', 'unknown')}"""
649
+
650
+ # Inject user profile into prompt if available
651
+ profile_context = _get_profile_context(request.user_id)
652
+ prompt = PRE_ALARM_PROMPT
653
+ if profile_context:
654
+ prompt += f"\n\n--- USER PROFILE ---\n{profile_context}\n--- END ---"
655
+
656
+ try:
657
+ response = client.models.generate_content(
658
+ model=MODEL_ID,
659
+ config={
660
+ 'system_instruction': prompt,
661
+ 'response_mime_type': 'application/json',
662
+ },
663
+ contents=signal_summary,
664
+ )
665
+
666
+ result = json.loads(response.text)
667
+ adjustment = result.get("difficulty_adjustment", "normal")
668
+ if adjustment not in ("harder", "easier", "normal"):
669
+ adjustment = "normal"
670
+
671
+ return PreAlarmResponse(
672
+ difficulty_adjustment=adjustment,
673
+ reasoning=result.get("reasoning", ""),
674
+ briefing_hint=result.get("briefing_hint", ""),
675
+ early_alarm=result.get("early_alarm", False),
676
+ )
677
+ except Exception as e:
678
+ print(f"[PreAlarm] AI error: {e}")
679
+ return PreAlarmResponse(
680
+ difficulty_adjustment="normal",
681
+ reasoning="AI unavailable, using defaults",
682
+ briefing_hint="",
683
+ early_alarm=False,
684
+ )
685
+
686
+
687
+ ## ─── Morning Companion Briefing (Phase 4.2) ──────────────────
688
+
689
+ class BriefingRequest(BaseModel):
690
+ user_id: Optional[str] = None
691
+ pre_alarm_context: dict = {}
692
+ wake_session: dict = {} # how this morning's alarm went
693
+
694
+ class BriefingResponse(BaseModel):
695
+ greeting: str
696
+ weather_note: str
697
+ calendar_note: str
698
+ motivation: str
699
+ tip: str
700
+ full_spoken: str # concatenated TTS-optimized version
701
+
702
+ MORNING_BRIEFING_PROMPT = """You are Ankr's Morning Companion β€” a friendly, concise morning briefing AI. After the user has woken up and passed post-wake monitoring, you deliver a personalized morning message.
703
+
704
+ You receive:
705
+ - **weather**: Current conditions and temperature
706
+ - **calendar**: Today's events and schedule
707
+ - **wake_session**: How the user woke up this morning (time taken, escalation reached, steps)
708
+ - **wake_profile**: Historical patterns (streak, success rate, trend)
709
+
710
+ Your briefing has these sections:
711
+ 1. **greeting**: A warm, personalized morning greeting (reference their wake performance today)
712
+ 2. **weather_note**: Brief weather summary with practical advice (e.g., "Bring an umbrella" or "Great day for a walk")
713
+ 3. **calendar_note**: What's on their schedule today (mention first event, total count)
714
+ 4. **motivation**: Streak celebration, trend acknowledgment, or encouragement (reference their data)
715
+ 5. **tip**: One practical tip based on their patterns (e.g., "You struggle on Mondays β€” try laying out clothes the night before")
716
+
717
+ Rules:
718
+ - Keep EACH section to 1-2 sentences max. This is read aloud via TTS.
719
+ - Be warm and positive but not cheesy. You're a helpful friend, not a motivational poster.
720
+ - Reference specific data naturally (don't just list numbers).
721
+ - If data is missing (no weather, no calendar), skip that section gracefully.
722
+ - The full_spoken field should be all sections concatenated with natural pauses (periods between sections).
723
+
724
+ You MUST respond with ONLY a JSON object:
725
+ {
726
+ "greeting": "...",
727
+ "weather_note": "...",
728
+ "calendar_note": "...",
729
+ "motivation": "...",
730
+ "tip": "...",
731
+ "full_spoken": "..."
732
+ }
733
+ """
734
+
735
+ @app.post("/morning/briefing", response_model=BriefingResponse)
736
+ async def morning_briefing(request: BriefingRequest):
737
+ """Generate a personalized morning briefing after successful wake-up."""
738
+ ctx = request.pre_alarm_context
739
+ wake = request.wake_session
740
+
741
+ signal_summary = f"""Generate a morning briefing for this user:
742
+
743
+ Weather: {json.dumps(ctx.get('weather'), default=str) if ctx.get('weather') else 'unavailable'}
744
+ Calendar: {json.dumps(ctx.get('calendar'), default=str) if ctx.get('calendar') else 'no events'}
745
+
746
+ This morning's wake-up:
747
+ - Time to wake: {wake.get('time_to_wake_seconds', 'unknown')}s
748
+ - Steps taken: {wake.get('steps', 'unknown')}
749
+ - Escalation level reached: {wake.get('escalation_level', 'unknown')}/5
750
+ - Post-wake monitoring: {'passed' if wake.get('post_wake_passed') else 'unknown'}
751
+
752
+ Day: {ctx.get('day_of_week', datetime.now().strftime('%A'))}
753
+ Time: {ctx.get('time', datetime.now().strftime('%I:%M %p'))}"""
754
+
755
+ # Add profile context
756
+ profile_context = _get_profile_context(request.user_id)
757
+ prompt = MORNING_BRIEFING_PROMPT
758
+ if profile_context:
759
+ prompt += f"\n\n--- USER PROFILE ---\n{profile_context}\n--- END ---"
760
+
761
+ try:
762
+ response = client.models.generate_content(
763
+ model=MODEL_ID,
764
+ config={
765
+ 'system_instruction': prompt,
766
+ 'response_mime_type': 'application/json',
767
+ },
768
+ contents=signal_summary,
769
+ )
770
+
771
+ result = json.loads(response.text)
772
+
773
+ greeting = result.get("greeting", "Good morning!")
774
+ weather_note = result.get("weather_note", "")
775
+ calendar_note = result.get("calendar_note", "")
776
+ motivation = result.get("motivation", "")
777
+ tip = result.get("tip", "")
778
+
779
+ # Build full_spoken if not provided
780
+ full_spoken = result.get("full_spoken", "")
781
+ if not full_spoken:
782
+ parts = [greeting]
783
+ if weather_note: parts.append(weather_note)
784
+ if calendar_note: parts.append(calendar_note)
785
+ if motivation: parts.append(motivation)
786
+ if tip: parts.append(tip)
787
+ full_spoken = " ".join(parts)
788
+
789
+ return BriefingResponse(
790
+ greeting=greeting,
791
+ weather_note=weather_note,
792
+ calendar_note=calendar_note,
793
+ motivation=motivation,
794
+ tip=tip,
795
+ full_spoken=full_spoken,
796
+ )
797
+ except Exception as e:
798
+ print(f"[MorningBriefing] AI error: {e}")
799
+ # Fallback briefing
800
+ return BriefingResponse(
801
+ greeting="Good morning! You're up and moving.",
802
+ weather_note="",
803
+ calendar_note="",
804
+ motivation="Keep up the great work!",
805
+ tip="",
806
+ full_spoken="Good morning! You're up and moving. Keep up the great work!",
807
+ )
808
+
809
+
810
  ## ─── User Profile (Phase 3A) ──────────────────────────────────
811
 
812
  class UserWakeProfile(BaseModel):