Lucas commited on
Commit
781cd97
·
1 Parent(s): 6ab2085

Handle negated pain mentions in injury keyword detection

Browse files

"about my calves, nothing hurts" matched the "hurts" keyword, forced
pain_or_injury to "yes", and the muscle word "calves" then synthesized
a phantom gastrocnemius pain issue. Strip negated injury mentions
("no pain", "nothing hurts", "doesn't hurt", "pain free") before the
keyword check; positive mentions elsewhere in the text still force
"yes" ("no pain except my knee hurts").

Files changed (2) hide show
  1. tests/test_parser.py +34 -0
  2. training_coach/parser.py +18 -1
tests/test_parser.py CHANGED
@@ -629,6 +629,40 @@ class ParserTest(unittest.TestCase):
629
  self.assertIn("event=parser_validated_json", output)
630
  self.assertIn("event=parser_normalized_json", output)
631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  def test_parse_model_response_drops_training_actions(self):
633
  response = json.dumps(
634
  {
 
629
  self.assertIn("event=parser_validated_json", output)
630
  self.assertIn("event=parser_normalized_json", output)
631
 
632
+ def test_negated_pain_mentions_do_not_force_injury(self):
633
+ for text in (
634
+ "about my calves, nothing hurts",
635
+ "60 minutes, slept great, feeling strong, no pain",
636
+ "shoulder doesn't hurt anymore",
637
+ "pain free today",
638
+ ):
639
+ parsed = ParsedCheckIn(
640
+ check_in=CheckIn(raw_text=text, pain_or_injury="no")
641
+ )
642
+
643
+ normalized = normalize_parsed_check_in(parsed)
644
+
645
+ self.assertEqual(
646
+ normalized.check_in.pain_or_injury, "no", msg=repr(text)
647
+ )
648
+ self.assertEqual(normalized.check_in.pain_issues, [], msg=repr(text))
649
+
650
+ def test_positive_pain_mentions_still_force_injury(self):
651
+ for text in (
652
+ "my tricep aches a bit",
653
+ "no pain except my knee hurts",
654
+ "left shoulder hurts but back doesn't hurt",
655
+ ):
656
+ parsed = ParsedCheckIn(
657
+ check_in=CheckIn(raw_text=text, pain_or_injury="no")
658
+ )
659
+
660
+ normalized = normalize_parsed_check_in(parsed)
661
+
662
+ self.assertEqual(
663
+ normalized.check_in.pain_or_injury, "yes", msg=repr(text)
664
+ )
665
+
666
  def test_parse_model_response_drops_training_actions(self):
667
  response = json.dumps(
668
  {
training_coach/parser.py CHANGED
@@ -331,8 +331,25 @@ def _infer_sleep_quality_from_text(text: str):
331
  return None
332
 
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  def _mentions_injury(text: str) -> bool:
335
- return _contains_keyword(text, INJURY_KEYWORDS)
336
 
337
 
338
  def _known_check_in_fields(check_in: CheckIn) -> set[str]:
 
331
  return None
332
 
333
 
334
+ # Negation word followed within a few words by an injury keyword, e.g.
335
+ # "no pain", "nothing hurts", "doesn't hurt", "never injured".
336
+ _NEGATED_INJURY_PATTERN = re.compile(
337
+ r"\b(?:no|not|nothing|never|without|zero|don'?t|doesn'?t|isn'?t|aren'?t)\b"
338
+ r"(?:\W+\w+){0,3}?\W*"
339
+ r"\b(?:ache|aches|aching|hurt|hurts|hurting|injur\w*|pain\w*|"
340
+ r"sore\w*|strain\w*|pull\w*|tear\w*|tore|torn)\b",
341
+ re.IGNORECASE,
342
+ )
343
+ _PAIN_FREE_PATTERN = re.compile(r"\bpain[- ]?free\b", re.IGNORECASE)
344
+
345
+
346
+ def _strip_negated_injury_mentions(text: str) -> str:
347
+ text = _NEGATED_INJURY_PATTERN.sub(" ", text)
348
+ return _PAIN_FREE_PATTERN.sub(" ", text)
349
+
350
+
351
  def _mentions_injury(text: str) -> bool:
352
+ return _contains_keyword(_strip_negated_injury_mentions(text), INJURY_KEYWORDS)
353
 
354
 
355
  def _known_check_in_fields(check_in: CheckIn) -> set[str]: