tthhanh commited on
Commit
19dfbdf
·
1 Parent(s): 7294c15

feat: expand coach retrieval with equipment and goal overlays

Browse files
src/pozify/knowledge_cards.py CHANGED
@@ -13,8 +13,10 @@ CARD_TYPE_ORDER = {
13
  "exercise": 0,
14
  "variation": 1,
15
  "issue": 2,
16
- "goal": 3,
17
- "safety_rule": 4,
 
 
18
  }
19
 
20
 
@@ -81,6 +83,45 @@ def _card(
81
  )
82
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  CARD_REGISTRY: tuple[KnowledgeCard, ...] = (
85
  _card(
86
  "exercise:squat",
@@ -300,6 +341,84 @@ CARD_REGISTRY: tuple[KnowledgeCard, ...] = (
300
  ("Limit the number of corrections in a single summary.",),
301
  ("Pick the top one or two form priorities for next time.",),
302
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  _card(
304
  "safety:no_diagnosis",
305
  "safety_rule",
@@ -552,6 +671,38 @@ def cards_for_labels(labels: list[str] | tuple[str, ...]) -> list[KnowledgeCard]
552
  )
553
 
554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  def retrieve_cards_with_metadata(
556
  *,
557
  profile: UserProfile,
@@ -590,7 +741,10 @@ def retrieve_cards(
590
  variation.detected_variation,
591
  *variation.not_issues,
592
  *[issue.issue for issue in issues.issues],
 
593
  profile.goal,
 
 
594
  "no_diagnosis",
595
  "no_injury_prevention_claim",
596
  "grounded_only",
 
13
  "exercise": 0,
14
  "variation": 1,
15
  "issue": 2,
16
+ "equipment": 3,
17
+ "goal": 4,
18
+ "goal_overlay": 5,
19
+ "safety_rule": 6,
20
  }
21
 
22
 
 
83
  )
84
 
85
 
86
+ def _equipment_card(
87
+ equipment: str,
88
+ title: str,
89
+ summary: str,
90
+ coaching_point: str,
91
+ ) -> KnowledgeCard:
92
+ return _card(
93
+ f"equipment:{equipment}",
94
+ "equipment",
95
+ (equipment,),
96
+ title,
97
+ summary,
98
+ (
99
+ "Treat equipment as context for cue wording, not as a new issue.",
100
+ ),
101
+ (coaching_point,),
102
+ )
103
+
104
+
105
+ def _goal_overlay_card(
106
+ exercise: str,
107
+ goal: str,
108
+ title: str,
109
+ summary: str,
110
+ coaching_points: tuple[str, ...],
111
+ ) -> KnowledgeCard:
112
+ return _card(
113
+ f"goal_overlay:{exercise}:{goal}",
114
+ "goal_overlay",
115
+ (f"{exercise}:{goal}",),
116
+ title,
117
+ summary,
118
+ (
119
+ "Use this overlay only as a prioritization hint for the detected exercise and goal.",
120
+ ),
121
+ coaching_points,
122
+ )
123
+
124
+
125
  CARD_REGISTRY: tuple[KnowledgeCard, ...] = (
126
  _card(
127
  "exercise:squat",
 
341
  ("Limit the number of corrections in a single summary.",),
342
  ("Pick the top one or two form priorities for next time.",),
343
  ),
344
+ _equipment_card(
345
+ "bodyweight",
346
+ "Bodyweight Context",
347
+ "Bodyweight sessions should keep cues simple and focused on repeatable control.",
348
+ "Prefer control and repeatability cues that do not assume external load adjustments.",
349
+ ),
350
+ _equipment_card(
351
+ "dumbbell",
352
+ "Dumbbell Context",
353
+ "Dumbbell sessions can use load-management cues, but only as context rather than a requirement.",
354
+ "If range or symmetry drifts, favor tempo or setup cues before suggesting heavier changes.",
355
+ ),
356
+ _equipment_card(
357
+ "barbell",
358
+ "Barbell Context",
359
+ "Barbell sessions may benefit from setup and path-consistency cues, while still staying grounded in the evidence.",
360
+ "Keep the coaching focused on bar path, balance, and repeatable setup when the evidence supports it.",
361
+ ),
362
+ _goal_overlay_card(
363
+ "squat",
364
+ "strength",
365
+ "Squat Strength Overlay",
366
+ "For strength-focused squats, prioritize the highest-value rep-quality cue rather than many small changes.",
367
+ (
368
+ "Lead with the one squat fix that most affects repeatable depth or balance.",
369
+ "Keep the next-session plan short and repeatable.",
370
+ ),
371
+ ),
372
+ _goal_overlay_card(
373
+ "squat",
374
+ "beginner_practice",
375
+ "Squat Beginner Overlay",
376
+ "For beginner-practice squats, keep the tone encouraging and corrections minimal.",
377
+ (
378
+ "Explain what looked stable before introducing the top squat fix.",
379
+ "Use simple depth or balance language rather than stacking many cues.",
380
+ ),
381
+ ),
382
+ _goal_overlay_card(
383
+ "push_up",
384
+ "beginner_practice",
385
+ "Push-up Beginner Overlay",
386
+ "For beginner-practice push-ups, prioritize body-line control and simple repeatable cues.",
387
+ (
388
+ "Keep push-up corrections to one or two cues that the athlete can repeat next set.",
389
+ "Treat valid regressions or grip variations as context, not failure.",
390
+ ),
391
+ ),
392
+ _goal_overlay_card(
393
+ "push_up",
394
+ "endurance",
395
+ "Push-up Endurance Overlay",
396
+ "For endurance-oriented push-ups, explain what changed later in the set before suggesting fixes.",
397
+ (
398
+ "Call out late-set body-line or depth drift when the rep evidence shows it.",
399
+ "Use pacing and consistency cues for the next session plan.",
400
+ ),
401
+ ),
402
+ _goal_overlay_card(
403
+ "shoulder_press",
404
+ "strength",
405
+ "Shoulder Press Strength Overlay",
406
+ "For strength-focused presses, prioritize clean top position and left-right consistency.",
407
+ (
408
+ "Lead with the top-position or symmetry cue that most affected repeatable reps.",
409
+ "Keep the next-session plan focused on one or two overhead pressing priorities.",
410
+ ),
411
+ ),
412
+ _goal_overlay_card(
413
+ "shoulder_press",
414
+ "mobility",
415
+ "Shoulder Press Mobility Overlay",
416
+ "For mobility-oriented presses, stay descriptive about range consistency without diagnosing restriction.",
417
+ (
418
+ "Describe the observed top-range consistency without speculating about limitations.",
419
+ "Favor smooth controlled reps for the next-session comparison.",
420
+ ),
421
+ ),
422
  _card(
423
  "safety:no_diagnosis",
424
  "safety_rule",
 
671
  )
672
 
673
 
674
+ def prioritized_coaching_points(cards: list[KnowledgeCard], *, limit: int = 6) -> list[str]:
675
+ priority_order = {
676
+ "issue": 0,
677
+ "variation": 1,
678
+ "exercise": 2,
679
+ "equipment": 3,
680
+ "goal_overlay": 4,
681
+ "goal": 5,
682
+ "safety_rule": 6,
683
+ }
684
+ ordered_cards = sorted(
685
+ cards,
686
+ key=lambda card: (
687
+ priority_order.get(card.card_type, 99),
688
+ 0 if card.source_kind == "external" else 1,
689
+ card.card_id,
690
+ ),
691
+ )
692
+ points: list[str] = []
693
+ seen: set[str] = set()
694
+ for card in ordered_cards:
695
+ for point in card.coaching_points:
696
+ normalized = point.strip()
697
+ if not normalized or normalized in seen:
698
+ continue
699
+ seen.add(normalized)
700
+ points.append(normalized)
701
+ if len(points) >= limit:
702
+ return points
703
+ return points
704
+
705
+
706
  def retrieve_cards_with_metadata(
707
  *,
708
  profile: UserProfile,
 
741
  variation.detected_variation,
742
  *variation.not_issues,
743
  *[issue.issue for issue in issues.issues],
744
+ profile.equipment,
745
  profile.goal,
746
+ f"{classification.exercise}:{profile.goal}",
747
+ f"{classification.exercise}:{profile.equipment}",
748
  "no_diagnosis",
749
  "no_injury_prevention_claim",
750
  "grounded_only",
src/pozify/slm/prompting.py CHANGED
@@ -12,7 +12,7 @@ from pozify.contracts import (
12
  Variation,
13
  to_dict,
14
  )
15
- from pozify.knowledge_cards import KnowledgeCard
16
 
17
 
18
  def build_summary_evidence(
@@ -59,6 +59,7 @@ def build_summary_evidence(
59
  "issues": [to_dict(issue) for issue in issues.issues],
60
  "top_issue_intervals": top_issue_intervals,
61
  },
 
62
  "knowledge_cards": [to_dict(card) for card in cards],
63
  }
64
 
@@ -101,6 +102,7 @@ def build_coach_summary_prompt(
101
  "Do not diagnose injuries, pain, mobility deficits, or pathology.",
102
  "Do not claim injury prevention.",
103
  "Do not treat a valid detected variation or not-issue label as an error.",
 
104
  "When referencing an issue or variation, include its exact label in "
105
  "backticks at least once.",
106
  "If evidence is limited or confidence is low, say so in confidence_notes.",
 
12
  Variation,
13
  to_dict,
14
  )
15
+ from pozify.knowledge_cards import KnowledgeCard, prioritized_coaching_points
16
 
17
 
18
  def build_summary_evidence(
 
59
  "issues": [to_dict(issue) for issue in issues.issues],
60
  "top_issue_intervals": top_issue_intervals,
61
  },
62
+ "priority_cues": prioritized_coaching_points(cards),
63
  "knowledge_cards": [to_dict(card) for card in cards],
64
  }
65
 
 
102
  "Do not diagnose injuries, pain, mobility deficits, or pathology.",
103
  "Do not claim injury prevention.",
104
  "Do not treat a valid detected variation or not-issue label as an error.",
105
+ "Use priority_cues as the first source for phrasing top fixes and next-session guidance.",
106
  "When referencing an issue or variation, include its exact label in "
107
  "backticks at least once.",
108
  "If evidence is limited or confidence is low, say so in confidence_notes.",
tests/test_coach_summary.py CHANGED
@@ -26,9 +26,11 @@ from pozify.env import load_local_env
26
  import pozify.slm.providers as slm_providers
27
  from pozify.knowledge_cards import (
28
  clear_catalog_cache,
 
29
  retrieve_cards,
30
  retrieve_cards_with_metadata,
31
  )
 
32
  from pozify.steps import coach_summary, coach_summary_fallback, verifier
33
 
34
 
@@ -267,15 +269,17 @@ class CoachSummaryTests(unittest.TestCase):
267
 
268
  card_ids = [card.card_id for card in cards]
269
  self.assertEqual(
270
- card_ids[:4],
271
  [
272
  "exercise:push_up",
273
  "variation:wide_grip_push_up",
274
  "issue:hip_sag",
 
275
  "goal:beginner_practice",
276
  ],
277
  )
278
  self.assertIn("safety:no_diagnosis", card_ids)
 
279
 
280
  def test_retrieval_metadata_reports_external_card_usage(self) -> None:
281
  retrieval = retrieve_cards_with_metadata(
@@ -332,6 +336,43 @@ class CoachSummaryTests(unittest.TestCase):
332
  self.assertEqual(push_up_card.source_kind, "external")
333
  self.assertEqual(push_up_card.source_path, str(pack_path.resolve()))
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  def test_coach_summary_falls_back_when_model_fails(self) -> None:
336
  cards = retrieve_cards(
337
  profile=_profile(),
 
26
  import pozify.slm.providers as slm_providers
27
  from pozify.knowledge_cards import (
28
  clear_catalog_cache,
29
+ prioritized_coaching_points,
30
  retrieve_cards,
31
  retrieve_cards_with_metadata,
32
  )
33
+ from pozify.slm.prompting import build_summary_evidence
34
  from pozify.steps import coach_summary, coach_summary_fallback, verifier
35
 
36
 
 
269
 
270
  card_ids = [card.card_id for card in cards]
271
  self.assertEqual(
272
+ card_ids[:5],
273
  [
274
  "exercise:push_up",
275
  "variation:wide_grip_push_up",
276
  "issue:hip_sag",
277
+ "equipment:bodyweight",
278
  "goal:beginner_practice",
279
  ],
280
  )
281
  self.assertIn("safety:no_diagnosis", card_ids)
282
+ self.assertIn("goal_overlay:push_up:beginner_practice", card_ids)
283
 
284
  def test_retrieval_metadata_reports_external_card_usage(self) -> None:
285
  retrieval = retrieve_cards_with_metadata(
 
336
  self.assertEqual(push_up_card.source_kind, "external")
337
  self.assertEqual(push_up_card.source_path, str(pack_path.resolve()))
338
 
339
+ def test_prompt_evidence_includes_prioritized_cues(self) -> None:
340
+ cards = retrieve_cards(
341
+ profile=_profile(),
342
+ classification=_classification(),
343
+ variation=_variation(),
344
+ issues=_issues(),
345
+ )
346
+
347
+ evidence = build_summary_evidence(
348
+ profile=_profile(),
349
+ classification=_classification(),
350
+ reps=_reps(),
351
+ analysis=_analysis(),
352
+ variation=_variation(),
353
+ issues=_issues(),
354
+ cards=cards,
355
+ )
356
+
357
+ self.assertTrue(evidence["priority_cues"])
358
+ self.assertIn(
359
+ "Keep shoulders, hips, and ankles moving as one line.",
360
+ evidence["priority_cues"],
361
+ )
362
+
363
+ def test_prioritized_coaching_points_prefers_issue_and_context_cards(self) -> None:
364
+ cards = retrieve_cards(
365
+ profile=_profile(),
366
+ classification=_classification(),
367
+ variation=_variation(),
368
+ issues=_issues(),
369
+ )
370
+
371
+ points = prioritized_coaching_points(cards, limit=4)
372
+
373
+ self.assertLessEqual(len(points), 4)
374
+ self.assertIn("Keep shoulders, hips, and ankles moving as one line.", points)
375
+
376
  def test_coach_summary_falls_back_when_model_fails(self) -> None:
377
  cards = retrieve_cards(
378
  profile=_profile(),