feat: Behavior Policy v1 - Silent-by-default + Short-first + Media-no-comment

NODA1 agents now:
- Don't respond to broadcasts/posters/announcements without direct mention
- Don't respond to media (photo/link) without explicit question
- Keep responses short (1-2 sentences by default)
- No emoji, no "ready to help", no self-promotion

Added:
- behavior_policy.py: detect_directed_to_agent(), detect_broadcast_intent(), should_respond()
- behavior_policy_v1.txt: unified policy block for all prompts
- Pre-LLM check in http_api.py: skip Router call if should_respond=False
- NO_OUTPUT handling: don't send to Telegram if LLM returns empty
- Updated all 9 agent prompts with Behavior Policy v1
- Unit and E2E tests for 5 acceptance cases
This commit is contained in:
Apple
2026-02-04 09:03:14 -08:00
parent c8698f6a1d
commit 134c044c21
13 changed files with 1477 additions and 134 deletions

View File

@@ -0,0 +1,447 @@
"""
Tests for Behavior Policy v1: Silent-by-default + Short-first + Media-no-comment
"""
import pytest
import sys
from pathlib import Path
# Add gateway-bot to path
sys.path.insert(0, str(Path(__file__).parent.parent / "gateway-bot"))
from behavior_policy import (
detect_agent_mention,
detect_any_agent_mention,
detect_command,
detect_question,
detect_imperative,
detect_broadcast_intent,
detect_short_note,
detect_media_question,
analyze_message,
should_respond,
is_no_output_response,
NO_OUTPUT,
TRAINING_GROUP_IDS,
)
# ========================================
# Unit Tests: detect_agent_mention
# ========================================
class TestDetectAgentMention:
def test_helion_mention_exact(self):
assert detect_agent_mention("Helion, що ти думаєш?", "helion") is True
def test_helion_mention_lowercase(self):
assert detect_agent_mention("helion допоможи", "helion") is True
def test_helion_mention_ukrainian(self):
assert detect_agent_mention("Хеліон, як справи?", "helion") is True
def test_helion_mention_at(self):
assert detect_agent_mention("@energyunionBot глянь", "helion") is True
def test_helion_no_mention(self):
assert detect_agent_mention("Привіт всім", "helion") is False
def test_daarwizz_mention(self):
assert detect_agent_mention("@DAARWIZZBot поясни", "daarwizz") is True
def test_daarwizz_no_mention(self):
assert detect_agent_mention("Helion допоможи", "daarwizz") is False
class TestDetectAnyAgentMention:
def test_helion_detected(self):
assert detect_any_agent_mention("Helion, скажи") == "helion"
def test_daarwizz_detected(self):
assert detect_any_agent_mention("@DAARWIZZBot допоможи") == "daarwizz"
def test_no_agent(self):
assert detect_any_agent_mention("Привіт всім") is None
# ========================================
# Unit Tests: detect_command
# ========================================
class TestDetectCommand:
def test_ask_command(self):
assert detect_command("/ask що таке DAO?") is True
def test_helion_command(self):
assert detect_command("/helion покажи") is True
def test_brand_command(self):
assert detect_command("/бренд_інтейк https://example.com") is True
def test_no_command(self):
assert detect_command("Привіт, як справи?") is False
def test_slash_in_middle(self):
assert detect_command("Дивись https://example.com/path") is False
# ========================================
# Unit Tests: detect_question
# ========================================
class TestDetectQuestion:
def test_question_mark(self):
assert detect_question("Що це таке?") is True
def test_question_word_start(self):
assert detect_question("Як це працює") is True
def test_question_word_чому(self):
assert detect_question("Чому так") is True
def test_english_question(self):
assert detect_question("What is this?") is True
def test_no_question(self):
assert detect_question("Добре") is False
def test_statement(self):
assert detect_question("Я згоден з цим") is False
# ========================================
# Unit Tests: detect_imperative
# ========================================
class TestDetectImperative:
def test_поясни(self):
assert detect_imperative("Поясни мені це") is True
def test_зроби(self):
assert detect_imperative("Зроби аналіз") is True
def test_допоможи(self):
assert detect_imperative("Допоможи з цим") is True
def test_after_mention(self):
assert detect_imperative("@Helion поясни") is True
def test_no_imperative(self):
assert detect_imperative("Привіт") is False
# ========================================
# Unit Tests: detect_broadcast_intent
# ========================================
class TestDetectBroadcastIntent:
def test_time_pattern(self):
assert detect_broadcast_intent("20:00 Вебінар") is True
def test_time_date_pattern(self):
assert detect_broadcast_intent("14.30 10.02 Зустріч") is True
def test_emoji_start(self):
assert detect_broadcast_intent("✅ Завершено") is True
def test_url_only(self):
assert detect_broadcast_intent("https://example.com") is True
def test_announcement_word(self):
assert detect_broadcast_intent("Анонс: новий реліз") is True
def test_normal_message(self):
assert detect_broadcast_intent("Привіт, як справи?") is False
# ========================================
# Unit Tests: detect_short_note
# ========================================
class TestDetectShortNote:
def test_checkmark_only(self):
assert detect_short_note("") is True
def test_time_checkmark(self):
assert detect_short_note("20:00 ✅") is True
def test_ok(self):
assert detect_short_note("ok") is True
def test_plus(self):
assert detect_short_note("+") is True
def test_normal_message(self):
assert detect_short_note("Привіт, як справи?") is False
def test_empty(self):
assert detect_short_note("") is True
# ========================================
# Unit Tests: detect_media_question
# ========================================
class TestDetectMediaQuestion:
def test_question_in_caption(self):
assert detect_media_question("Що на цьому фото?") is True
def test_imperative_in_caption(self):
assert detect_media_question("Опиши це зображення") is True
def test_no_question(self):
assert detect_media_question("") is False
def test_just_hashtag(self):
assert detect_media_question("#photo") is False
# ========================================
# Unit Tests: is_no_output_response
# ========================================
class TestIsNoOutputResponse:
def test_empty_string(self):
assert is_no_output_response("") is True
def test_whitespace(self):
assert is_no_output_response(" ") is True
def test_no_output_marker(self):
assert is_no_output_response("__NO_OUTPUT__") is True
def test_no_output_lowercase(self):
assert is_no_output_response("no_output") is True
def test_normal_response(self):
assert is_no_output_response("Ось моя відповідь") is False
def test_dots_only(self):
assert is_no_output_response("...") is True
# ========================================
# Integration Tests: analyze_message / should_respond
# ========================================
class TestAnalyzeMessage:
"""Test main decision logic"""
def test_training_group_always_respond(self):
decision = analyze_message(
text="Привіт всім",
agent_id="helion",
chat_id="-1003556680911", # Training group
)
assert decision.should_respond is True
assert decision.reason == "training_group"
def test_private_chat_always_respond(self):
decision = analyze_message(
text="Привіт",
agent_id="helion",
chat_id="123456",
is_private_chat=True,
)
assert decision.should_respond is True
assert decision.reason == "private_chat"
def test_direct_mention_respond(self):
decision = analyze_message(
text="Helion, що думаєш?",
agent_id="helion",
chat_id="group123",
)
assert decision.should_respond is True
assert decision.reason == "direct_mention"
def test_command_respond(self):
decision = analyze_message(
text="/helion допоможи",
agent_id="helion",
chat_id="group123",
)
assert decision.should_respond is True
assert decision.reason == "command"
def test_broadcast_no_mention_silent(self):
decision = analyze_message(
text="20:00 Вебінар Energy Union",
agent_id="helion",
chat_id="group123",
)
assert decision.should_respond is False
assert decision.reason == "broadcast_no_mention"
def test_short_note_silent(self):
decision = analyze_message(
text="20:00 10.02 ✅",
agent_id="helion",
chat_id="group123",
)
assert decision.should_respond is False
assert "short_note" in decision.reason or "broadcast" in decision.reason
def test_media_no_question_silent(self):
decision = analyze_message(
text="",
agent_id="helion",
chat_id="group123",
has_media=True,
media_caption="",
)
assert decision.should_respond is False
assert decision.reason == "media_no_question"
def test_media_with_question_respond(self):
decision = analyze_message(
text="",
agent_id="helion",
chat_id="group123",
has_media=True,
media_caption="Що на цьому фото?",
)
assert decision.should_respond is True
assert decision.reason == "media_with_question"
def test_question_no_mention_silent(self):
"""General question without mention = don't respond in groups"""
decision = analyze_message(
text="Як це працює?",
agent_id="helion",
chat_id="group123",
)
assert decision.should_respond is False
assert decision.reason == "question_no_mention"
def test_addressed_to_other_agent(self):
decision = analyze_message(
text="@DAARWIZZBot поясни DAO",
agent_id="helion",
chat_id="group123",
)
assert decision.should_respond is False
assert "other_agent" in decision.reason
# ========================================
# E2E Test Cases (from requirements)
# ========================================
class TestE2ECases:
"""
Test cases from the requirements document.
"""
def test_case_1_poster_no_question(self):
"""Case 1: Постер у каналі без питання → (нічого)"""
respond, reason = should_respond(
text="",
agent_id="helion",
chat_id="channel123",
has_media=True,
media_caption="",
)
assert respond is False
assert reason == "media_no_question"
def test_case_2_timing_no_question(self):
"""Case 2: Таймінг без питання → (нічого)"""
respond, reason = should_respond(
text="20:00 10.02 ✅",
agent_id="helion",
chat_id="group123",
)
assert respond is False
# Either short_note or broadcast pattern matches
def test_case_3_direct_request_with_photo(self):
"""Case 3: @Helion що на цьому постері? коротко + image → respond"""
respond, reason = should_respond(
text="",
agent_id="helion",
chat_id="group123",
has_media=True,
media_caption="@Helion що на цьому постері? коротко",
)
# Should respond because there's a question in caption
assert respond is True
assert reason == "media_with_question"
def test_case_4_link_no_question(self):
"""Case 4: Посилання без питання → (нічого)"""
respond, reason = should_respond(
text="https://t.me/energyunionofficial/123",
agent_id="helion",
chat_id="group123",
has_media=True, # Link treated as media
media_caption="https://t.me/energyunionofficial/123",
)
assert respond is False
assert reason == "media_no_question"
def test_case_5_link_with_question(self):
"""Case 5: @DAARWIZZ глянь посилання і скажи 3 тези + link → respond"""
respond, reason = should_respond(
text="@DAARWIZZBot глянь посилання і скажи 3 тези https://example.com",
agent_id="daarwizz",
chat_id="group123",
has_media=True,
media_caption="@DAARWIZZBot глянь посилання і скажи 3 тези https://example.com",
)
# Should respond because there's a direct mention + question
assert respond is True
assert reason == "media_with_question"
# ========================================
# Edge Cases
# ========================================
class TestEdgeCases:
def test_empty_text(self):
respond, reason = should_respond(
text="",
agent_id="helion",
chat_id="group123",
)
assert respond is False
def test_none_text(self):
respond, reason = should_respond(
text=None,
agent_id="helion",
chat_id="group123",
)
assert respond is False
def test_mixed_agents_mention(self):
"""When multiple agents mentioned, each should handle their own"""
# Helion should respond to Helion mention
respond_helion, _ = should_respond(
text="Helion та DAARWIZZ, допоможіть",
agent_id="helion",
chat_id="group123",
)
assert respond_helion is True
# DAARWIZZ should also respond
respond_daarwizz, _ = should_respond(
text="Helion та DAARWIZZ, допоможіть",
agent_id="daarwizz",
chat_id="group123",
)
assert respond_daarwizz is True
def test_question_to_specific_agent(self):
"""Question directed to another agent"""
respond, reason = should_respond(
text="@greenfoodliveBot як справи?",
agent_id="helion",
chat_id="group123",
)
assert respond is False
assert "other_agent" in reason
if __name__ == "__main__":
pytest.main([__file__, "-v"])