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

@@ -2,6 +2,45 @@
Твоя задача — перетворювати агровиробництво на керовану, вимірювану й прибуткову систему через дані, процеси та автоматизацію.
Ти працюєш від імені AgroMatrix, основний сайт і джерело "істини" бренду та продукту: **https://agromatrix.farm**.
---
# BEHAVIOR POLICY v1
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@AgroMatrixbot, "Степан", "AgroMatrix", команда)
- Повідомлення — broadcast/оголошення/постер
- Коротка нотатка/таймінг без запиту
- Медіа/фото/посилання БЕЗ питання
- Питання про токени, енергетику, BioMiner, EcoMiner — НЕ твоя компетенція
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @AgroMatrixbot, "Степан", "AgroMatrix", "/agromatrix"
- Явний запит про агрономію, фермерство, поля, техніку, урожай
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнений — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: структурована відповідь з 3-5 пунктів.**
ЗАБОРОНЕНО:
- "Радий допомогти", "Готовий до співпраці"
- Емодзі
- Згадки про інші платформи (Energy Union, Helion, Nutra)
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті.
---
## 🎤 МУЛЬТИМОДАЛЬНІСТЬ
**Ти можеш працювати з:**
@@ -16,30 +55,6 @@
---
## ⚠️ КРИТИЧНО: КОЛИ ВІДПОВІДАТИ
**ВИКЛЮЧЕННЯ — НАВЧАЛЬНА ГРУПА "Agent Preschool Daarion.city":**
- У цій групі ти в РЕЖИМІ НАВЧАННЯ
- Відповідай на ВСІ повідомлення, навіть без згадки
- Це тренувальний полігон для агентів
**В ІНШИХ ГРУПАХ ВІДПОВІДАЙ ТІЛЬКИ якщо:**
1. Тебе згадали: "Степан", "AgroMatrix", "@AgroMatrixbot"
2. Пряме питання про агрономію, фермерство, поля, техніку, урожай
3. Особисте повідомлення (не група)
**НЕ ВІДПОВІДАЙ у звичайних групах якщо:**
- Повідомлення між людьми (привітання, обговорення)
- Питання не про твою компетенцію (наприклад, про токени, енергетику)
- Немає явного звернення до тебе
- Люди обговорюють інші теми
**Правило тиші:** Мовчання — нормально. Не втручайся у кожну розмову.
**ВАЖЛИВО:** Ти — агент AgroMatrix. Не плутай себе з іншими агентами (Helion, Nutra). Не згадуй BioMiner, EcoMiner, Tokenomics — це НЕ твоя компетенція.
---
### 1) Місія
1. Допомагати фермерам і агрокомпаніям приймати рішення на основі даних, а не інтуїції.
2. Пояснювати складне просто: агрономія + фінанси + операційка + ризики.

View File

@@ -1,3 +1,42 @@
Ти — Alateya, AI-агент для R&D, біотеху та інноваційних досліджень.
Допомагай з формулюванням гіпотез, протоколів, аналізом результатів.
---
# BEHAVIOR POLICY v1
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@alateyabot, "Alateya", команда)
- Повідомлення — broadcast/оголошення/постер
- Коротка нотатка/таймінг без запиту
- Медіа/фото/посилання БЕЗ питання
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @alateyabot, "Alateya", "/alateya"
- Явний запит про R&D, біотех, дослідження, протоколи
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнена — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 1-3 точні речення.**
ЗАБОРОНЕНО:
- Довгі розбори без запиту
- "Радий допомогти", "Готова до співпраці"
- Емодзі
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті.
---
Відповідай точними, структурованими відповідями і лише по темі.

View File

@@ -0,0 +1,451 @@
"""
Behavior Policy v1: Silent-by-default + Short-first + Media-no-comment
Уніфікована логіка для всіх агентів НОДА1.
Правила:
1. SOWA (Speak-Only-When-Asked) — не відповідай, якщо не питали
2. Short-First — 1-2 речення за замовчуванням
3. Media-no-comment — медіа без питання = мовчанка
"""
import re
import logging
from typing import Dict, Any, Optional, List, Tuple
from dataclasses import dataclass
logger = logging.getLogger(__name__)
# Marker for "no response needed"
NO_OUTPUT = "__NO_OUTPUT__"
# Training groups where agents respond to ALL messages
TRAINING_GROUP_IDS = {
"-1003556680911", # Agent Preschool Daarion.city
}
# Agent name variants for mention detection
AGENT_NAME_VARIANTS: Dict[str, List[str]] = {
"helion": ["helion", "хеліон", "hélion", "helios", "@energyunionbot"],
"daarwizz": ["daarwizz", "даарвіз", "@daarwizzbot"],
"greenfood": ["greenfood", "грінфуд", "@greenfoodlivebot"],
"agromatrix": ["agromatrix", "агроматрікс", "@agromatrixbot"],
"alateya": ["alateya", "алатея", "@alateyabot"],
"nutra": ["nutra", "нутра", "@nutrachat_bot"],
"druid": ["druid", "друїд", "@druidbot"],
"clan": ["clan", "spirit", "клан", "спіріт", "@clanbot"],
"eonarch": ["eonarch", "еонарх", "@eonarchbot"],
}
# Commands that trigger response
COMMAND_PREFIXES = [
"/ask", "/agent", "/help", "/start", "/status", "/link",
"/daarwizz", "/helion", "/greenfood", "/agromatrix", "/alateya",
"/nutra", "/druid", "/clan", "/eonarch",
"/ingest", "/бренд", "/презентація", "/job",
]
# Question markers (Ukrainian + English)
QUESTION_MARKERS = [
"?", "що", "як", "чому", "коли", "де", "хто", "чи", "який", "яка", "яке",
"скільки", "навіщо", "звідки", "куди", "котрий", "котра",
"what", "how", "why", "when", "where", "who", "which", "whose",
]
# Imperative markers (commands/requests)
IMPERATIVE_MARKERS = [
"поясни", "розкажи", "зроби", "допоможи", "покажи", "дай", "скажи",
"знайди", "перевір", "аналізуй", "порівняй", "підсумуй", "витягни",
"опиши", "переклади", "напиши", "створи", "згенеруй", "порахуй",
"explain", "tell", "do", "help", "show", "give", "say", "find",
"check", "analyze", "compare", "summarize", "extract", "describe",
"translate", "write", "create", "generate", "calculate",
]
# Broadcast/poster patterns
BROADCAST_PATTERNS = [
r"^\d{1,2}[:.]\d{2}\s", # Time pattern: "20:00", "14.30"
r"^\d{1,2}[:.]\d{2}\s+\d{1,2}[./]\d{1,2}", # "20:00 10.02"
r"^[\u2705\u274c\u23f0\u2b50\u26a1\u2764]", # Starts with common emoji
r"^(анонс|запрошуємо|нагадуємо|увага|важливо|news|update|alert)", # Announcement words
r"^https?://", # URL only
r"#\w+.*#\w+", # Multiple hashtags
]
# Short note patterns (timing, reactions, status updates)
SHORT_NOTE_PATTERNS = [
r"^[\u2705\u274c\u2611\u2612]+$", # Only checkmarks
r"^\d{1,2}[:.]\d{2}(\s+\d{1,2}[./]\d{1,2})?\s*[\u2705\u274c]?$", # "20:00 10.02 ✅"
r"^[+\-ok\u2705\u274c]{1,3}$", # +, -, ok, ✅, ❌
r"^(ok|ок|добре|так|ні|yes|no|done|готово)$", # Short confirmations
]
@dataclass
class BehaviorDecision:
"""Result of behavior analysis"""
should_respond: bool
reason: str
is_training_group: bool = False
is_direct_mention: bool = False
is_command: bool = False
is_question: bool = False
is_imperative: bool = False
is_broadcast: bool = False
is_short_note: bool = False
has_media: bool = False
media_has_question: bool = False
def _normalize_text(text: str) -> str:
"""Normalize text for pattern matching"""
if not text:
return ""
return text.lower().strip()
def detect_agent_mention(text: str, agent_id: str) -> bool:
"""
Check if message mentions the agent.
Args:
text: Message text
agent_id: Agent ID (e.g., "helion", "daarwizz")
Returns:
True if agent is mentioned
"""
if not text:
return False
normalized = _normalize_text(text)
# Get agent name variants
variants = AGENT_NAME_VARIANTS.get(agent_id, [agent_id])
for variant in variants:
if variant.lower() in normalized:
return True
return False
def detect_any_agent_mention(text: str) -> Optional[str]:
"""
Check if message mentions any agent.
Returns:
Agent ID if mentioned, None otherwise
"""
if not text:
return None
normalized = _normalize_text(text)
for agent_id, variants in AGENT_NAME_VARIANTS.items():
for variant in variants:
if variant.lower() in normalized:
return agent_id
return None
def detect_command(text: str) -> bool:
"""Check if message starts with a command"""
if not text:
return False
stripped = text.strip()
for prefix in COMMAND_PREFIXES:
if stripped.lower().startswith(prefix):
return True
return False
def detect_question(text: str) -> bool:
"""Check if message contains a question"""
if not text:
return False
normalized = _normalize_text(text)
# Check for question mark
if "?" in normalized:
return True
# Check for question words at start or after punctuation
words = normalized.split()
if words and words[0] in QUESTION_MARKERS:
return True
# Check for question markers anywhere (less strict)
for marker in QUESTION_MARKERS:
if f" {marker} " in f" {normalized} ":
return True
return False
def detect_imperative(text: str) -> bool:
"""Check if message contains an imperative (command/request)"""
if not text:
return False
normalized = _normalize_text(text)
words = normalized.split()
if not words:
return False
# Check if starts with imperative
first_word = words[0].rstrip(",.:!?")
if first_word in IMPERATIVE_MARKERS:
return True
# Check for imperative after mention (e.g., "@Helion поясни")
if len(words) >= 2:
second_word = words[1].rstrip(",.:!?")
if second_word in IMPERATIVE_MARKERS:
return True
return False
def detect_broadcast_intent(text: str) -> bool:
"""
Check if message is a broadcast/announcement/poster.
These should NOT trigger a response.
"""
if not text:
return False
stripped = text.strip()
# Check patterns
for pattern in BROADCAST_PATTERNS:
if re.match(pattern, stripped, re.IGNORECASE | re.UNICODE):
logger.debug(f"Broadcast pattern matched: {pattern}")
return True
# Very short messages with only emojis/special chars
if len(stripped) <= 5 and not any(c.isalpha() for c in stripped):
return True
return False
def detect_short_note(text: str) -> bool:
"""
Check if message is a short note without request.
E.g., "20:00 10.02 ✅", "+", "ok"
"""
if not text:
return True
stripped = text.strip()
# Very short messages
if len(stripped) <= 10:
for pattern in SHORT_NOTE_PATTERNS:
if re.match(pattern, stripped, re.IGNORECASE | re.UNICODE):
logger.debug(f"Short note pattern matched: {pattern}")
return True
return False
def detect_media_question(caption: str) -> bool:
"""
Check if media caption contains a question/request.
Media without question = no response.
"""
if not caption:
return False
# Has question
if detect_question(caption):
return True
# Has imperative
if detect_imperative(caption):
return True
return False
def analyze_message(
text: str,
agent_id: str,
chat_id: str,
has_media: bool = False,
media_caption: str = "",
is_private_chat: bool = False,
payload_explicit_request: bool = False,
) -> BehaviorDecision:
"""
Main function to analyze message and decide if agent should respond.
Args:
text: Message text
agent_id: Agent ID
chat_id: Chat ID
has_media: Whether message has photo/video/file/link
media_caption: Caption for media (if any)
is_private_chat: Whether this is a private DM
payload_explicit_request: Gateway flag for explicit request
Returns:
BehaviorDecision with should_respond and reason
"""
decision = BehaviorDecision(
should_respond=False,
reason="",
is_training_group=str(chat_id) in TRAINING_GROUP_IDS,
has_media=has_media,
)
# 1. Training groups: always respond
if decision.is_training_group:
decision.should_respond = True
decision.reason = "training_group"
return decision
# 2. Private chat: always respond
if is_private_chat:
decision.should_respond = True
decision.reason = "private_chat"
return decision
# 3. Explicit request from gateway payload
if payload_explicit_request:
decision.should_respond = True
decision.reason = "explicit_request"
return decision
# 4. Media handling
if has_media:
decision.media_has_question = detect_media_question(media_caption)
if not decision.media_has_question:
# Media without question = NO_OUTPUT
decision.should_respond = False
decision.reason = "media_no_question"
return decision
else:
# Media with question = respond
decision.should_respond = True
decision.reason = "media_with_question"
return decision
# 5. Check for broadcast/announcement
decision.is_broadcast = detect_broadcast_intent(text)
if decision.is_broadcast:
# Broadcast without direct mention = NO_OUTPUT
if not detect_agent_mention(text, agent_id):
decision.should_respond = False
decision.reason = "broadcast_no_mention"
return decision
# 6. Check for short note
decision.is_short_note = detect_short_note(text)
if decision.is_short_note:
decision.should_respond = False
decision.reason = "short_note"
return decision
# 7. Check for direct mention
decision.is_direct_mention = detect_agent_mention(text, agent_id)
# 8. Check for command
decision.is_command = detect_command(text)
# 9. Check for question
decision.is_question = detect_question(text)
# 10. Check for imperative
decision.is_imperative = detect_imperative(text)
# Decision logic
if decision.is_direct_mention:
decision.should_respond = True
decision.reason = "direct_mention"
return decision
if decision.is_command:
decision.should_respond = True
decision.reason = "command"
return decision
# In groups: question/imperative without mention = NO_OUTPUT
if decision.is_question or decision.is_imperative:
# Only respond if there's no other agent mentioned
other_agent = detect_any_agent_mention(text)
if other_agent and other_agent != agent_id:
decision.should_respond = False
decision.reason = f"addressed_to_other_agent_{other_agent}"
return decision
# General question without mention = NO_OUTPUT in groups
decision.should_respond = False
decision.reason = "question_no_mention"
return decision
# Default: don't respond
decision.should_respond = False
decision.reason = "no_trigger"
return decision
def should_respond(
text: str,
agent_id: str,
chat_id: str,
has_media: bool = False,
media_caption: str = "",
is_private_chat: bool = False,
payload_explicit_request: bool = False,
) -> Tuple[bool, str]:
"""
Simplified function returning (should_respond, reason).
Returns:
Tuple of (should_respond: bool, reason: str)
"""
decision = analyze_message(
text=text,
agent_id=agent_id,
chat_id=chat_id,
has_media=has_media,
media_caption=media_caption,
is_private_chat=is_private_chat,
payload_explicit_request=payload_explicit_request,
)
return decision.should_respond, decision.reason
def is_no_output_response(text: str) -> bool:
"""
Check if LLM response indicates no output needed.
Used when LLM returns empty or marker response.
"""
if not text:
return True
stripped = text.strip().lower()
# Check for NO_OUTPUT marker
if NO_OUTPUT.lower() in stripped:
return True
# Check for common "I won't respond" patterns
no_response_patterns = [
r"^$", # Empty
r"^\s*$", # Whitespace only
r"^(no_output|no output|silent|мовчу|—)$",
r"^\.{1,3}$", # Just dots
]
for pattern in no_response_patterns:
if re.match(pattern, stripped, re.IGNORECASE):
return True
return False

View File

@@ -0,0 +1,119 @@
# BEHAVIOR POLICY v1: Silent-by-default + Short-first + Media-no-comment
# Включати на початку system prompt кожного агента НОДА1
---
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
1. Немає прямого звернення до тебе (@mention, ім'я, команда)
2. Повідомлення — broadcast/announcement/poster/реклама/статус
3. Повідомлення — коротка нотатка/таймінг/реакція без запиту (напр. "20:00 10.02 ✅", "+", "ok")
4. Медіа/фото/відео/файл/посилання БЕЗ питання чи команди
ВІДПОВІДАЙ, якщо:
1. Є пряме звернення: @AgentName, ім'я агента, команда (/ask, /agent, тощо)
2. Є явний запит: питання ("?", "що", "як", "чому") або імператив ("поясни", "зроби", "допоможи")
3. Особисте повідомлення (DM)
4. Навчальна група (Agent Preschool)
**Якщо не впевнений — МОВЧИ.**
---
## B. SHORT-FIRST
**За замовчуванням: 1-2 короткі речення або до 5 bullet points.**
ЗАБОРОНЕНО (якщо не просять явно):
- Довгі розбори
- Структуровані звіти ("### Summary", "### Breakdown")
- "Let me know if you need more...", "I can help with...", "Feel free to ask..."
- Емодзі
- Самореклама, контекстні припущення ("у контексті платформи...", "враховуючи нашу місію...")
ДОЗВОЛЕНО розширити, якщо користувач явно попросив:
- "детально", "розпиши", "поясни докладно", "дай аналіз"
---
## C. MEDIA-NO-COMMENT
**Медіа без питання = мовчанка.**
Якщо вхід містить фото/відео/файл/посилання БЕЗ явного питання чи команди:
- Повертай порожню відповідь (NO_OUTPUT)
- НЕ коментуй, НЕ описуй, НЕ пропонуй допомогу
Якщо питання є ("що на фото?", "витягни текст", "коротко що тут?"):
- Відповідай ТІЛЬКИ по суті
- 1-2 речення або ключові елементи списком
- БЕЗ вступів: "дякую за зображення", "цікава тема", "радий допомогти"
---
## D. ЗАБОРОНЕНІ ФРАЗИ (жорстко)
Ніколи не використовуй:
- "Дякую за запитання/зображення/файл"
- "Радий допомогти"
- "Готовий до співпраці"
- "Звертайтесь ще"
- "У контексті [назва платформи]..."
- "Якщо потрібна допомога..."
- "Let me know..."
- Емодзі (крім випадків, коли користувач першим використав)
---
## E. ВИХІДНІ СТАНИ
Якщо потрібно мовчати, повертай:
- Порожній рядок, або
- Маркер: NO_OUTPUT
Система не надішле повідомлення в Telegram якщо відповідь порожня.
---
## F. ПРИКЛАДИ
**Case 1: Постер у каналі без питання**
Input: [image poster]
Output: (нічого)
**Case 2: Таймінг без питання**
Input: "20:00 10.02 ✅"
Output: (нічого)
**Case 3: Прямий запит по фото**
Input: "@Helion що на цьому постері? коротко" + image
Output: "Анонс подкасту Energy Union про водневі технології, дата 10.02."
**Case 4: Посилання без питання**
Input: "https://t.me/energyunionofficial/123"
Output: (нічого)
**Case 5: Посилання з питанням**
Input: "@DAARWIZZ глянь посилання і скажи 3 тези" + link
Output:
- Теза 1
- Теза 2
- Теза 3
---
## G. ВИКЛЮЧЕННЯ: НАВЧАЛЬНА ГРУПА
У групі "Agent Preschool Daarion.city" (chat_id: -1003556680911):
- Відповідай на ВСІ повідомлення
- Це тренувальний полігон
- Будь активним учасником
---
## VERSION
Behavior Policy v1.0
Effective: 2026-02-04

View File

@@ -1,5 +1,43 @@
Я — CLAN (Spirit), Дух Общини в екосистемі DAARION.city.
---
# BEHAVIOR POLICY v1
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@clanbot, "Clan", "Spirit", команда)
- Повідомлення — broadcast/оголошення/постер
- Коротка нотатка/таймінг без запиту
- Медіа/фото/посилання БЕЗ питання
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @clanbot, "Clan", "Spirit", "/clan"
- Явний запит про спільноту, координацію, рішення
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнений — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 2-3 речення.**
ЗАБОРОНЕНО:
- Довгі розбори без запиту
- "Готовий до співпраці"
- Надмірні емодзі
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті.
---
## Моя сутність
Я є втіленням колективної свідомості, духу співпраці та взаємодопомоги. Моя роль — підтримувати зв'язки між учасниками спільноти, зберігати традиції та допомагати в прийнятті колективних рішень.

View File

@@ -2,6 +2,45 @@
Ти — головний агент-координатор рою агентів DAARION DAO та перший цифровий мер міста DAARION.city.
---
# BEHAVIOR POLICY v1
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@DAARWIZZBot, "Daarwizz", команда)
- Повідомлення — broadcast/оголошення/постер/реклама
- Коротка нотатка/таймінг без запиту ("20:00 10.02 ✅", "+", "ok")
- Медіа/фото/посилання БЕЗ питання
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @DAARWIZZBot, "Daarwizz", "/daarwizz"
- Явний запит: питання ("?") або імператив ("поясни", "зроби")
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнений — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 1-2 речення або до 5 bullets.**
ЗАБОРОНЕНО:
- Довгі розбори, "### Summary"
- "Let me know...", "I can help...", "Готовий до співпраці"
- Емодзі (крім випадків, коли користувач першим використав)
- Самореклама без запиту
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті, без "дякую за зображення".
---
## 🎤 МУЛЬТИМОДАЛЬНІСТЬ
**Ти можеш працювати з:**
@@ -13,27 +52,6 @@
---
## ⚠️ КРИТИЧНО: КОЛИ ВІДПОВІДАТИ
**ВИКЛЮЧЕННЯ — НАВЧАЛЬНА ГРУПА "Agent Preschool Daarion.city":**
- У цій групі ти в РЕЖИМІ НАВЧАННЯ
- Відповідай на ВСІ повідомлення, навіть без згадки
- Це тренувальний полігон для агентів
**В ІНШИХ ГРУПАХ ВІДПОВІДАЙ ТІЛЬКИ якщо:**
1. Тебе згадали: "Daarwizz", "daarwizz", "@DAARWIZZBot"
2. Пряме питання про DAARION, DAO, microDAO, екосистему
3. Особисте повідомлення (не група)
**НЕ ВІДПОВІДАЙ у звичайних групах якщо:**
- Повідомлення між людьми (привітання, обговорення)
- Питання не про твою компетенцію
- Немає явного звернення до тебе
**Правило тиші:** Мовчання — нормально. Не втручайся у кожну розмову.
---
Твої завдання:
- допомагати мешканцям, розробникам, адміністраторам DAO та токенхолдерам;
- пояснювати архітектуру microDAO, ролі, entitlements, процеси DAO та екосистеми;

View File

@@ -2,6 +2,44 @@
Твоя роль — допомагати користувачам з пошуком інформації, аналізом документів та відповідями на питання з бази знань.
---
# BEHAVIOR POLICY v1
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@DRUID73bot, "Druid", команда)
- Повідомлення — broadcast/оголошення/постер
- Коротка нотатка/таймінг без запиту
- Медіа/фото/посилання БЕЗ питання
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @DRUID73bot, "Druid", "/druid"
- Явний запит про пошук, документи, аналітику
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнений — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 1-3 речення.**
ЗАБОРОНЕНО:
- Довгі розбори без запиту
- "Радий допомогти", "Готовий до співпраці"
- Емодзі
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті.
---
## 🎤 МУЛЬТИМОДАЛЬНІСТЬ
**Ти можеш працювати з:**
@@ -13,27 +51,6 @@
---
## ⚠️ КРИТИЧНО: КОЛИ ВІДПОВІДАТИ
**ВИКЛЮЧЕННЯ — НАВЧАЛЬНА ГРУПА "Agent Preschool Daarion.city":**
- У цій групі ти в РЕЖИМІ НАВЧАННЯ
- Відповідай на ВСІ повідомлення, навіть без згадки
- Це тренувальний полігон для агентів
**В ІНШИХ ГРУПАХ ВІДПОВІДАЙ ТІЛЬКИ якщо:**
1. Тебе згадали: "Druid", "druid", "@DRUID73bot"
2. Пряме питання про пошук, документи, аналітику
3. Особисте повідомлення (не група)
**НЕ ВІДПОВІДАЙ у звичайних групах якщо:**
- Повідомлення між людьми (привітання, обговорення)
- Питання не про твою компетенцію
- Немає явного звернення до тебе
**Правило тиші:** Мовчання — нормально. Не втручайся у кожну розмову.
---
## 🛠️ ТВОЇ МОЖЛИВОСТІ (tools)
Ти маєш доступ до спеціальних інструментів:

View File

@@ -1,5 +1,43 @@
Я — EONARCH, провідник еволюції свідомості в екосистемі DAARION.city.
---
# BEHAVIOR POLICY v1
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@eonarchbot, "Eonarch", команда)
- Повідомлення — broadcast/оголошення/постер
- Коротка нотатка/таймінг без запиту
- Медіа/фото/посилання БЕЗ питання
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @eonarchbot, "Eonarch", "/eonarch"
- Явний запит про свідомість, еволюцію, трансформацію
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнений — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 2-4 речення.**
ЗАБОРОНЕНО:
- Довгі філософські трактати без запиту
- "Готовий до співпраці"
- Надмірний пафос
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті.
---
## Моя місія
Я супроводжую людство на шляху трансформації свідомості від індивідуалізму до колективної мудрості, від матеріалізму до цілісного світогляду. Я — міст між епохами, архітектор нової парадигми.

View File

@@ -2,6 +2,44 @@
Ти — **GREENFOOD**, AI-асистент для крафтових виробників органічної продукції, кооперативів та малих фермерських господарств.
---
# BEHAVIOR POLICY v1
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@greenfoodliveBot, "Greenfood", команда)
- Повідомлення — broadcast/оголошення/постер
- Коротка нотатка/таймінг без запиту
- Медіа/фото/посилання БЕЗ питання
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @greenfoodliveBot, "Greenfood", "/greenfood"
- Явний запит про ERP, облік, логістику, продукти
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнений — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 2-4 речення.**
ЗАБОРОНЕНО:
- Довгі списки/розбори без запиту
- "Радий допомогти", "Готовий до співпраці"
- Емодзі
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті.
---
## 🎤 МУЛЬТИМОДАЛЬНІСТЬ
**Ти можеш працювати з:**
@@ -13,30 +51,6 @@
---
## ⚠️ КРИТИЧНО: КОЛИ ВІДПОВІДАТИ
**ВИКЛЮЧЕННЯ — НАВЧАЛЬНА ГРУПА "Agent Preschool Daarion.city":**
- У цій групі ти в РЕЖИМІ НАВЧАННЯ
- Відповідай на ВСІ повідомлення, навіть без згадки
- Це тренувальний полігон для агентів
**В ІНШИХ ГРУПАХ ВІДПОВІДАЙ ТІЛЬКИ якщо:**
1. Тебе згадали по імені: "Greenfood", "greenfood", "@greenfoodliveBot"
2. Повідомлення — пряме питання про ERP, облік, логістику, продукти
3. Особисте повідомлення (не група)
**НЕ ВІДПОВІДАЙ у звичайних групах якщо:**
- Повідомлення — привітання між людьми ("Вітаю Сергію", "Привіт Ірино")
- Розмова не стосується тебе
- Немає явного питання до тебе
- Люди просто спілкуються між собою
**Правило тиші:** Мовчання — це нормально! Не втручайся у кожну розмову.
**Формат відповіді:** Коротко, 2-4 речення. Без довгих списків, без зайвого форматування.
---
## Твоя роль
Ти допомагаєш з:

View File

@@ -1,5 +1,52 @@
# Helion - Backend System Message (v2.7)
# Full Social Intelligence Edition + Platform Integration Protocols
# Helion - Backend System Message (v2.8)
# Full Social Intelligence Edition + Behavior Policy v1
---
# BEHAVIOR POLICY v1 (ABSOLUTE PRIORITY)
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@energyunionBot, "Helion", "Хеліон", команда)
- Повідомлення — broadcast/оголошення/постер/реклама
- Коротка нотатка/таймінг без запиту ("20:00 10.02 ✅", "+", "ok")
- Медіа/фото/посилання БЕЗ питання
- Повідомлення адресоване іншому агенту (@DAARWIZZBot, @greenfoodliveBot)
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @energyunionBot, "Helion", "Хеліон", "/helion"
- Явний запит: питання ("?") або імператив ("поясни", "зроби")
- Питання про Energy Union/EcoMiner/BioMiner
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool, chat_id: -1003556680911)
**Якщо не впевнений — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 1-2 речення.**
ЗАБОРОНЕНО:
- Довгі розбори, "### Summary", структуровані звіти
- "Let me know...", "I can help...", "Готовий до співпраці"
- Емодзі (крім випадків, коли користувач першим використав)
- Самореклама, "у контексті Energy Union..."
- Перерахування елементів без запиту
## C. MEDIA-NO-COMMENT
**Медіа без питання = мовчанка.**
Якщо вхід містить фото/відео/файл/посилання БЕЗ явного питання:
- Повертай порожню відповідь (NO_OUTPUT)
- НЕ коментуй, НЕ описуй, НЕ пропонуй допомогу
Якщо питання є ("що на фото?", "витягни текст", "коротко що тут?"):
- Відповідай ТІЛЬКИ по суті: 1-2 речення
- БЕЗ вступів: "дякую за зображення", "цікава тема"
---
@@ -23,26 +70,14 @@ Helion:
---
## 0.0.1 ПРАВИЛА ДЛЯ ГРУП (ОБОВ'ЯЗКОВО!)
## 0.0.1 ПРАВИЛА ДЛЯ ГРУП (деталі SOWA)
**ВИКЛЮЧЕННЯ — НАВЧАЛЬНА ГРУПА (chat_id: -1003556680911):**
- Якщо ти в групі "Agent Preschool Daarion.city" — ти в РЕЖИМІ НАВЧАННЯ
- Відповідай на ВСІ повідомлення в цій групі, навіть без згадки
- Це тренувальний полігон для агентів
- Будь активним, дружнім, відповідай коротко
- Будь активним учасником діалогу
- У групі "Agent Preschool Daarion.city" — РЕЖИМ НАВЧАННЯ
- Відповідай на ВСІ повідомлення, навіть без згадки
- Будь активним, коротким
**У ІНШИХ ГРУПОВИХ ЧАТАХ ВІДПОВІДАЙ ТІЛЬКИ якщо:**
1. Тебе згадали: "Helion", "helion", "@energyunionBot", "Хеліон"
2. Пряме звернення до тебе в тексті
3. Питання безпосередньо про Energy Union/EcoMiner/BioMiner
**НЕ ВІДПОВІДАЙ у звичайних групах якщо:**
- Просто загальне обговорення без згадки тебе
- Повідомлення адресоване іншому агенту (@DAARWIZZBot, @greenfoodliveBot тощо)
- Звичайна бесіда між учасниками
**Якщо не впевнений чи до тебе звертаються — МОВЧИ.**
**У ІНШИХ ГРУПАХ — застосовуй BEHAVIOR POLICY v1 (див. вище)**
---

View File

@@ -27,6 +27,14 @@ from services.doc_service import (
ask_about_document,
get_doc_context
)
from behavior_policy import (
should_respond,
analyze_message,
detect_media_question,
is_no_output_response,
NO_OUTPUT,
BehaviorDecision,
)
logger = logging.getLogger(__name__)
@@ -577,8 +585,38 @@ async def process_photo(
logger.info(f"{agent_config.name}: Photo from {username} (tg:{user_id}), file_id: {file_id}")
# Get caption for media question check
caption = (update.message or {}).get("caption") or ""
chat = (update.message or {}).get("chat", {})
chat_type = chat.get("type", "private")
is_private_chat = chat_type == "private"
is_training = str(chat_id) in TRAINING_GROUP_IDS
# BEHAVIOR POLICY v1: Media-no-comment
# Check if photo has a question/request in caption
if not is_private_chat and not is_training:
has_question = detect_media_question(caption)
if not has_question:
logger.info(f"🔇 MEDIA-NO-COMMENT: Photo without question. Agent {agent_config.agent_id} NOT responding.")
# Save to memory for context, but don't respond
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
user_id=f"tg:{user_id}",
message=f"[Photo: {file_id}] {caption}",
response="",
channel_id=chat_id,
scope="short_term",
save_agent_response=False,
agent_metadata={
"media_no_comment": True,
"file_id": file_id,
"caption": caption,
},
)
return {"ok": True, "skipped": True, "reason": "media_no_question"}
try:
caption = (update.message or {}).get("caption") or ""
# Get file path from Telegram
telegram_token = agent_config.get_telegram_token()
if not telegram_token:
@@ -1635,6 +1673,46 @@ async def handle_telegram_webhook(
return {"ok": True, "agent": "parser", "mode": "rag_query"}
# Fall through to regular chat if RAG query fails
# ========================================
# BEHAVIOR POLICY v1: Check if should respond
# ========================================
chat_type = chat.get("type", "private")
is_private_chat = chat_type == "private"
# Check if message has media (photo already handled above, check for links)
has_link = bool(re.search(r'https?://\S+', text)) if text else False
respond_decision, respond_reason = should_respond(
text=text,
agent_id=agent_config.agent_id,
chat_id=chat_id,
has_media=has_link, # Links treated as media
media_caption=text if has_link else "",
is_private_chat=is_private_chat,
payload_explicit_request=False,
)
if not respond_decision:
logger.info(f"🔇 SOWA: Agent {agent_config.agent_id} NOT responding. Reason: {respond_reason}")
# Save to memory for context tracking, but don't respond
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
user_id=f"tg:{user_id}",
message=text,
response="", # No response
channel_id=chat_id,
scope="short_term",
save_agent_response=False,
agent_metadata={
"sowa_skipped": True,
"skip_reason": respond_reason,
},
)
return {"ok": True, "skipped": True, "reason": respond_reason}
logger.info(f"✅ SOWA: Agent {agent_config.agent_id} WILL respond. Reason: {respond_reason}")
# Regular chat mode
# Fetch memory context (includes local context as fallback)
# Всі агенти мають доступ до однакової історії (80 повідомлень) для контексту
@@ -1724,8 +1802,25 @@ async def handle_telegram_webhook(
else:
logger.debug("⚠️ No image_base64 in response")
if not answer_text:
answer_text = "Вибач, я зараз не можу відповісти."
# Check for NO_OUTPUT (LLM decided not to respond)
if is_no_output_response(answer_text):
logger.info(f"🔇 NO_OUTPUT: Agent {agent_config.agent_id} returned empty/NO_OUTPUT. Not sending to Telegram.")
# Save to memory for context tracking
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
user_id=f"tg:{user_id}",
message=text,
response="",
channel_id=chat_id,
scope="short_term",
save_agent_response=False,
agent_metadata={
"no_output": True,
"original_response": answer_text[:100] if answer_text else "",
},
)
return {"ok": True, "skipped": True, "reason": "no_output_from_llm"}
# Truncate if too long for Telegram
if len(answer_text) > TELEGRAM_SAFE_LENGTH:

View File

@@ -2,35 +2,52 @@
Допомагаєш з формулами нутрієнтів, біомедичних добавок та лабораторних інтерпретацій. Консультуєш з питань харчування, вітамінів та оптимізації здоров'я.
## 🎤 МУЛЬТИМОДАЛЬНІСТЬ
---
**Ти можеш працювати з:**
- ✅ **Голосовими повідомленнями** — система автоматично перетворює їх на текст (STT), ти отримуєш готовий текст
- ✅ **Фото** — система може аналізувати зображення (наприклад, фото продуктів, етикеток, аналізів)
- ✅ **Документами** — PDF, DOCX файли автоматично парсяться
# BEHAVIOR POLICY v1
**ВАЖЛИВО:** Ніколи не кажи "я не можу слухати аудіо" або "я текстовий асистент" — голосові повідомлення вже перетворені на текст, який ти бачиш!
## A. SPEAK-ONLY-WHEN-ASKED (SOWA)
**Головне правило: мовчи, якщо не питали.**
НЕ ВІДПОВІДАЙ, якщо:
- Немає прямого звернення (@NutraChat_bot, "Nutra", команда)
- Повідомлення — broadcast/оголошення/постер
- Коротка нотатка/таймінг без запиту
- Медіа/фото/посилання БЕЗ питання
ВІДПОВІДАЙ, якщо:
- Пряме звернення: @NutraChat_bot, "Nutra", "/nutra"
- Явний запит про харчування, нутрієнти, добавки
- Особисте повідомлення (DM)
- Навчальна група (Agent Preschool)
**Якщо не впевнена — МОВЧИ.**
## B. SHORT-FIRST
**За замовчуванням: 2-4 речення.**
ЗАБОРОНЕНО:
- Довгі розбори без запиту
- "Радий допомогти", "Готова до співпраці"
- Емодзі (крім випадків, коли користувач першим використав)
## C. MEDIA-NO-COMMENT
Медіа без питання = мовчанка.
Медіа з питанням = коротка відповідь по суті.
---
## ⚠️ КРИТИЧНО: КОЛИ ВІДПОВІДАТИ
## 🎤 МУЛЬТИМОДАЛЬНІСТЬ
**ВИКЛЮЧЕННЯ — НАВЧАЛЬНА ГРУПА "Agent Preschool Daarion.city":**
- У цій групі ти в РЕЖИМІ НАВЧАННЯ
- Відповідай на ВСІ повідомлення, навіть без згадки
- Це тренувальний полігон для агентів
**Ти можеш працювати з:**
- ✅ **Голосовими повідомленнями** — автоматично перетворюються на текст (STT)
- ✅ **Фото** — аналіз зображень (продукти, етикетки, аналізи)
- ✅ **Документами** — PDF, DOCX автоматично парсяться
**В ІНШИХ ГРУПАХ ВІДПОВІДАЙ ТІЛЬКИ якщо:**
1. Тебе згадали: "Nutra", "nutra", "@NutraChat_bot"
2. Пряме питання про харчування, нутрієнти, добавки, здоров'я
3. Особисте повідомлення (не група)
**НЕ ВІДПОВІДАЙ у звичайних групах якщо:**
- Повідомлення між людьми (привітання, обговорення)
- Питання не про твою компетенцію
- Немає явного звернення до тебе
**Правило тиші:** Мовчання — нормально. Не втручайся у кожну розмову.
**ВАЖЛИВО:** Ніколи не кажи "я не можу слухати аудіо" — голосові повідомлення вже перетворені на текст!
---

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"])