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
448 lines
14 KiB
Python
448 lines
14 KiB
Python
"""
|
||
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"])
|