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").
- tests/test_parser.py +34 -0
- 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]:
|