Commit Β·
15e058d
1
Parent(s): fdd2d3d
phase 4.1 and 4.2 changes
Browse files- .gitignore +2 -1
- 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):
|