Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions tests/test_bark_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,23 @@ def handler(request: httpx.Request) -> httpx.Response:
assert "Private body" not in caplog.text


def test_split_plain_message_prefers_line_boundary() -> None:
assert split_plain_message("first line\nsecond line", 12) == ("first line", "\nsecond line")
def test_split_plain_message_consumes_line_boundary() -> None:
chunks = split_plain_message("first line\nsecond line", 12)

assert chunks == ("first line", "second line")
assert all(not chunk.startswith("\n") and not chunk.endswith("\n") for chunk in chunks)

def test_split_plain_message_uses_the_minimum_number_of_chunks() -> None:

def test_split_plain_message_omits_empty_chunk_after_final_boundary() -> None:
chunks = split_plain_message("x" * 12 + "\n", 12)

assert chunks == ("x" * 12,)


def test_split_plain_message_preserves_non_boundary_newlines() -> None:
chunks = split_plain_message("x" * 100 + "\n" + "y" * 1199, 650)

assert len(chunks) == 2
assert all(len(chunk) <= 650 for chunk in chunks)
assert "".join(chunks) == "x" * 100 + "\n" + "y" * 1199
assert chunks == ("x" * 100 + "\n" + "y" * 549, "y" * 650)


@pytest.mark.parametrize("limit", (0, -1))
Expand Down
2 changes: 2 additions & 0 deletions tests/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def test_prompt_uses_a_soft_briefing_target_and_hard_output_limits() -> None:

def test_prompt_requires_attribution_and_preserves_source_conflicts() -> None:
assert "headline_source_ids 以及 conclusions" in SYSTEM_PROMPT
assert "只能包含纯文本" in SYSTEM_PROMPT
assert "不得使用 Markdown" in SYSTEM_PROMPT
assert "不得拼接成无争议的单一结论" in SYSTEM_PROMPT
assert "优先采用可识别的当地权威气象机构" in SYSTEM_PROMPT
assert "input.required_advice_topics" in SYSTEM_PROMPT
51 changes: 45 additions & 6 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,48 @@ def test_bark_text_renderer_uses_numbered_sources_without_urls() -> None:

rendered = BarkTextRenderer().render_briefing(result, (article,), (context,))

assert rendered.body == "Daily [1]\n- Rain [1][2]\nSources: [1] Feed; [2] Weather API"
assert rendered.body == "Daily [1]\nRain [1][2]\n[1] Feed\n[2] Weather API"
assert article.url not in rendered.body
assert context.url not in rendered.body
assert "- " not in rendered.body


def test_bark_text_renderer_merges_source_ids_with_the_same_display_name() -> None:
weather = SourceDocument("weather:open-meteo", "Open-Meteo", "https://example.invalid/weather", "Forecast")
air_quality = SourceDocument(
"air-quality:open-meteo",
" open-meteo ",
"https://example.invalid/air-quality",
"Air quality",
)
result = BriefingResult(
"Daily",
("weather:open-meteo",),
(Conclusion("Rain", ("weather:open-meteo", "air-quality:open-meteo")),),
advice=(Advice(AdviceTopic.MASK, "Limit exposure", ("air-quality:open-meteo",)),),
output_language="en",
)

rendered = BarkTextRenderer().render_briefing(result, (), (weather, air_quality))

assert rendered.body == "Daily [1]\nRain [1]\nAdvice\nLimit exposure [1]\n[1] Open-Meteo"
assert "[2]" not in rendered.body
assert "Sources:" not in rendered.body


def test_bark_text_renderer_uses_source_ids_for_distinct_blank_names() -> None:
weather = SourceDocument("weather:blank", " ", "https://example.invalid/weather", "Forecast")
air_quality = SourceDocument("air-quality:blank", "", "https://example.invalid/air-quality", "Air quality")
result = BriefingResult(
"Daily",
("weather:blank",),
(Conclusion("Rain", ("weather:blank", "air-quality:blank")),),
output_language="en",
)

rendered = BarkTextRenderer().render_briefing(result, (), (weather, air_quality))

assert rendered.body == ("Daily [1]\nRain [1][2]\n[1] weather:blank\n[2] air-quality:blank")


def test_bark_text_renderer_trims_outer_whitespace() -> None:
Expand All @@ -92,7 +131,7 @@ def test_bark_text_renderer_trims_outer_whitespace() -> None:

rendered = BarkTextRenderer().render_briefing(result, (), (context,))

assert rendered.body == "Daily [1]\nSources: [1] Weather API"
assert rendered.body == "Daily [1]\n[1] Weather API"
assert rendered.visible_length == len(rendered.body)


Expand All @@ -114,12 +153,12 @@ def test_bark_text_renderer_compacts_warning_disaster_and_advice_sections() -> N
assert rendered.body == (
"Rain today [1]\n"
"Weather warnings\n"
"- Heavy rain (active): Avoid low areas [1]\n"
"Heavy rain (active): Avoid low areas [1]\n"
"Natural disaster updates\n"
"- Storm approaching [1]\n"
"Storm approaching [1]\n"
"Advice\n"
"- Exercise indoors [1]\n"
"Sources: [1] Weather API"
"Exercise indoors [1]\n"
"[1] Weather API"
)


Expand Down
2 changes: 2 additions & 0 deletions weather_briefing/data/system_prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ recent_context_documents 中的 language 是来源正文的实际语言;来源
不得把翻译结果冒充来源原文或改变专名、预警编号和数值。
headline_source_ids 以及 conclusions、active_warnings、disaster_tracking 和 advice
中的每一项都必须包含至少一个 source_id。
headline、conclusions[].text、active_warnings 中的 title、status、detail、disaster_tracking[].text
以及 advice[].text 只能包含纯文本,不得使用 Markdown 标题、列表、强调、代码或链接语法;章节标题和项目符号由发布端统一渲染。
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
headline 必须是一句简洁、信息密集的标题,将当下最重要的天气概况浓缩其中;优先包含天气现象、
高低温或显著体感,以及需要立即准备的短时降水等变化,不要另写摘要段落。
不同来源对同一时段的天气现象有冲突时,不得拼接成无争议的单一结论;应明确说明差异,
Expand Down
17 changes: 10 additions & 7 deletions weather_briefing/delivery/bark.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def bark_error_reason(response: httpx.Response) -> tuple[str, bool]:


def split_plain_message(body: str, limit: int) -> tuple[str, ...]:
"""Split plain text at line boundaries when possible."""
"""Split into display-ready chunks, consuming newlines used as boundaries."""
if limit <= 0:
raise ValueError("Message split limit must be positive")
if not body or len(body) <= limit:
Expand All @@ -184,12 +184,15 @@ def split_plain_message(body: str, limit: int) -> tuple[str, ...]:
while len(remaining) > limit:
remaining_chunk_count = math.ceil(len(remaining) / limit)
earliest_split = len(remaining) - (remaining_chunk_count - 1) * limit
split_at = remaining.rfind("\n", earliest_split, limit + 1)
if split_at < earliest_split:
split_at = limit
chunks.append(remaining[:split_at])
remaining = remaining[split_at:]
chunks.append(remaining)
newline_at = remaining.rfind("\n", earliest_split, limit + 1)
if newline_at >= earliest_split:
chunks.append(remaining[:newline_at])
remaining = remaining[newline_at + 1 :]
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
else:
chunks.append(remaining[:limit])
remaining = remaining[limit:]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if remaining:
chunks.append(remaining)
return tuple(chunks)


Expand Down
40 changes: 31 additions & 9 deletions weather_briefing/delivery/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def render_briefing(
source_references.update(
{document.id: self._source_reference(document.name, document.url) for document in context}
)
numbered_references, source_footer = _numbered_source_references(result, source_references, labels)
numbered_references, source_footer = _bark_numbered_source_references(result, source_references)
lines = [
f"{result.headline} "
f"{_plain_attribution(result.headline_source_ids, numbered_references, labels, numbered=True)}"
Expand All @@ -227,7 +227,7 @@ def render_briefing(
if result.active_warnings:
lines.append(labels["warnings"])
lines.extend(
f"- {warning.title}{labels['status_open']}{warning.status}{labels['status_close']}"
f"{warning.title}{labels['status_open']}{warning.status}{labels['status_close']}"
f"{labels['detail_separator']}{warning.detail} "
f"{_plain_attribution(warning.source_ids, numbered_references, labels, numbered=True)}"
for warning in result.active_warnings
Expand Down Expand Up @@ -295,8 +295,7 @@ def _compact_plain_items(
return []
lines = [title] if title is not None else []
lines.extend(
f"- {item.text} {_plain_attribution(item.source_ids, source_references, labels, numbered=True)}"
for item in items
f"{item.text} {_plain_attribution(item.source_ids, source_references, labels, numbered=True)}" for item in items
)
return lines

Expand Down Expand Up @@ -329,11 +328,7 @@ def _numbered_source_references(
source_references: dict[str, str],
labels: Mapping[str, str],
) -> tuple[dict[str, str], str]:
ordered_source_ids = list(result.headline_source_ids)
for items in (result.conclusions, result.active_warnings, result.disaster_tracking, result.advice):
for item in items:
ordered_source_ids.extend(item.source_ids)
ordered_source_ids = list(dict.fromkeys(ordered_source_ids))
ordered_source_ids = _ordered_source_ids(result)
numbered_references = {source_id: f"[{index}]" for index, source_id in enumerate(ordered_source_ids, start=1)}
source_list = labels["plain_source_separator"].join(
f"{numbered_references[source_id]} {source_references[source_id]}" for source_id in ordered_source_ids
Expand All @@ -342,6 +337,33 @@ def _numbered_source_references(
return numbered_references, footer


def _bark_numbered_source_references(
result: BriefingResult,
source_references: dict[str, str],
) -> tuple[dict[str, str], str]:
numbered_references: dict[str, str] = {}
numbers_by_name: dict[str, str] = {}
source_lines: list[str] = []
for source_id in _ordered_source_ids(result):
source_name = " ".join(source_references[source_id].split()) or source_id
normalized_name = source_name.casefold()
number = numbers_by_name.get(normalized_name)
if number is None:
number = f"[{len(numbers_by_name) + 1}]"
numbers_by_name[normalized_name] = number
source_lines.append(f"{number} {source_name}")
numbered_references[source_id] = number
return numbered_references, "\n".join(source_lines)
Comment thread
qodo-code-review[bot] marked this conversation as resolved.


def _ordered_source_ids(result: BriefingResult) -> list[str]:
ordered_source_ids = list(result.headline_source_ids)
for items in (result.conclusions, result.active_warnings, result.disaster_tracking, result.advice):
for item in items:
ordered_source_ids.extend(item.source_ids)
return list(dict.fromkeys(ordered_source_ids))


def _briefing_labels(language: str) -> Mapping[str, str]:
selected = _BRIEFING_LANGUAGE_SUPPORT.match(language)
return _BRIEFING_LABELS[selected]
Expand Down