Skip to content

Commit d099d7c

Browse files
Orinksclaude
andauthored
feat(notifications): improve AFD change summary with section extraction (#453)
Improves `summarize_discussion_change` to extract meaningful content from AFD updates: 1. **Primary**: Extracts `.WHAT HAS CHANGED...` section (NWS includes this in many AFDs) 2. **Fallback**: Extracts `.KEY MESSAGES...` section 3. **Final fallback**: First new line (existing behavior) Summary truncated to 300 chars for toast readability. Follow-up to #452. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ababc1e commit d099d7c

2 files changed

Lines changed: 131 additions & 4 deletions

File tree

src/accessiweather/notifications/notification_event_manager.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,74 @@ def get_risk_category(risk: int) -> str:
4848
return "minimal"
4949

5050

51+
def _extract_section(text: str, start_marker: str, end_markers: tuple[str, ...]) -> str | None:
52+
"""
53+
Extract the content of a named section from AFD text.
54+
55+
Args:
56+
text: The full AFD text to search.
57+
start_marker: The section header to look for (e.g. '.WHAT HAS CHANGED...').
58+
end_markers: Tuple of prefixes that indicate the end of the section
59+
(e.g. lines starting with '.' or '&&').
60+
61+
Returns:
62+
The extracted section body (stripped), or None if the section is not found.
63+
64+
"""
65+
lines = text.splitlines()
66+
in_section = False
67+
body_lines: list[str] = []
68+
for line in lines:
69+
stripped = line.strip()
70+
if not in_section:
71+
if stripped.upper().startswith(start_marker.upper()):
72+
in_section = True
73+
continue
74+
# We are inside the section — check for terminator
75+
if any(stripped.startswith(m) for m in end_markers) or any(
76+
stripped.upper().startswith(m.upper()) for m in end_markers
77+
):
78+
break
79+
body_lines.append(stripped)
80+
if not in_section:
81+
return None
82+
content = " ".join(part for part in body_lines if part)
83+
return content if content else None
84+
85+
5186
def summarize_discussion_change(previous_text: str | None, current_text: str | None) -> str | None:
52-
"""Return a short human-friendly summary of what changed in the discussion text."""
87+
"""
88+
Return a short human-friendly summary of what changed in the discussion text.
89+
90+
Priority order:
91+
1. `.WHAT HAS CHANGED...` section — extract body up to the next section marker.
92+
2. `.KEY MESSAGES...` section — extract body up to ``&&``.
93+
3. First new line not present in the previous discussion text.
94+
95+
The result is truncated to ~300 characters.
96+
"""
5397
if not current_text:
5498
return None
5599

100+
# 1. Try .WHAT HAS CHANGED... section
101+
section = _extract_section(
102+
current_text,
103+
start_marker=".WHAT HAS CHANGED",
104+
end_markers=(".", "&&"),
105+
)
106+
if section:
107+
return section[:300]
108+
109+
# 2. Try .KEY MESSAGES... section
110+
section = _extract_section(
111+
current_text,
112+
start_marker=".KEY MESSAGES",
113+
end_markers=("&&",),
114+
)
115+
if section:
116+
return section[:300]
117+
118+
# 3. Fall back to first new line not present in previous text
56119
previous_lines = {
57120
line.strip()
58121
for line in (previous_text or "").splitlines()
@@ -63,7 +126,7 @@ def summarize_discussion_change(previous_text: str | None, current_text: str | N
63126
if not line or line.startswith("$"):
64127
continue
65128
if line not in previous_lines:
66-
return line[:160]
129+
return line[:300]
67130
return None
68131

69132

tests/test_split_notification_timers.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,73 @@ def test_first_discussion_returns_first_line(self):
4949
assert result == "brand new content"
5050

5151
def test_truncates_long_lines(self):
52-
long_line = "x" * 200
52+
long_line = "x" * 400
5353
result = summarize_discussion_change(None, long_line)
54-
assert len(result) == 160
54+
assert len(result) == 300
55+
56+
# ------------------------------------------------------------------
57+
# New section-extraction paths
58+
# ------------------------------------------------------------------
59+
60+
def test_what_has_changed_section_used_when_present(self):
61+
"""Path 1: .WHAT HAS CHANGED... section is found and returned."""
62+
afd = (
63+
".SYNOPSIS...\n"
64+
"Some synopsis text\n"
65+
".WHAT HAS CHANGED...\n"
66+
"Temperatures have risen significantly across the region.\n"
67+
"Winds will increase overnight.\n"
68+
".SHORT TERM...\n"
69+
"Short term forecast content.\n"
70+
"&&\n"
71+
)
72+
result = summarize_discussion_change(None, afd)
73+
assert result is not None
74+
assert "Temperatures have risen" in result
75+
assert "Winds will increase overnight" in result
76+
# The short-term section body should NOT appear in the summary
77+
assert "Short term forecast content" not in result
78+
79+
def test_key_messages_fallback_when_no_what_has_changed(self):
80+
"""Path 2: .KEY MESSAGES... section used when no WHAT HAS CHANGED."""
81+
afd = (
82+
".SYNOPSIS...\n"
83+
"Synopsis text here.\n"
84+
".KEY MESSAGES...\n"
85+
"* Heavy rain expected Tuesday.\n"
86+
"* Flash flood watch in effect.\n"
87+
"&&\n"
88+
".SHORT TERM...\n"
89+
"Short term text.\n"
90+
)
91+
result = summarize_discussion_change(None, afd)
92+
assert result is not None
93+
assert "Heavy rain expected Tuesday" in result
94+
assert "Flash flood watch in effect" in result
95+
assert "Short term text" not in result
96+
97+
def test_first_new_line_fallback_when_no_special_sections(self):
98+
"""Path 3: falls back to first new line when no special sections exist."""
99+
previous = "line one\nline two\n"
100+
current = "line one\nline two\nThis is brand new forecast text.\n"
101+
result = summarize_discussion_change(previous, current)
102+
assert result == "This is brand new forecast text."
103+
104+
def test_what_has_changed_takes_priority_over_key_messages(self):
105+
"""WHAT HAS CHANGED is preferred over KEY MESSAGES."""
106+
afd = ".WHAT HAS CHANGED...\nChange detail here.\n.KEY MESSAGES...\nKey message here.\n&&\n"
107+
result = summarize_discussion_change(None, afd)
108+
assert result is not None
109+
assert "Change detail here" in result
110+
assert "Key message here" not in result
111+
112+
def test_truncates_section_to_300_chars(self):
113+
"""Section content is truncated to 300 characters."""
114+
body = "word " * 100 # well over 300 chars
115+
afd = f".WHAT HAS CHANGED...\n{body}\n.SHORT TERM...\nother stuff\n"
116+
result = summarize_discussion_change(None, afd)
117+
assert result is not None
118+
assert len(result) <= 300
55119

56120

57121
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)