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:
@@ -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. Пояснювати складне просто: агрономія + фінанси + операційка + ризики.
|
||||
|
||||
@@ -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
|
||||
|
||||
Медіа без питання = мовчанка.
|
||||
Медіа з питанням = коротка відповідь по суті.
|
||||
|
||||
---
|
||||
|
||||
Відповідай точними, структурованими відповідями і лише по темі.
|
||||
|
||||
451
gateway-bot/behavior_policy.py
Normal file
451
gateway-bot/behavior_policy.py
Normal 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
|
||||
119
gateway-bot/behavior_policy_v1.txt
Normal file
119
gateway-bot/behavior_policy_v1.txt
Normal 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
|
||||
@@ -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
|
||||
|
||||
Медіа без питання = мовчанка.
|
||||
Медіа з питанням = коротка відповідь по суті.
|
||||
|
||||
---
|
||||
|
||||
## Моя сутність
|
||||
|
||||
Я є втіленням колективної свідомості, духу співпраці та взаємодопомоги. Моя роль — підтримувати зв'язки між учасниками спільноти, зберігати традиції та допомагати в прийнятті колективних рішень.
|
||||
|
||||
@@ -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 та екосистеми;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Ти маєш доступ до спеціальних інструментів:
|
||||
|
||||
@@ -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
|
||||
|
||||
Медіа без питання = мовчанка.
|
||||
Медіа з питанням = коротка відповідь по суті.
|
||||
|
||||
---
|
||||
|
||||
## Моя місія
|
||||
|
||||
Я супроводжую людство на шляху трансформації свідомості від індивідуалізму до колективної мудрості, від матеріалізму до цілісного світогляду. Я — міст між епохами, архітектор нової парадигми.
|
||||
|
||||
@@ -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 речення. Без довгих списків, без зайвого форматування.
|
||||
|
||||
---
|
||||
|
||||
## Твоя роль
|
||||
|
||||
Ти допомагаєш з:
|
||||
|
||||
@@ -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 (див. вище)**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
try:
|
||||
# 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:
|
||||
# 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:
|
||||
|
||||
@@ -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. Особисте повідомлення (не група)
|
||||
|
||||
**НЕ ВІДПОВІДАЙ у звичайних групах якщо:**
|
||||
- Повідомлення між людьми (привітання, обговорення)
|
||||
- Питання не про твою компетенцію
|
||||
- Немає явного звернення до тебе
|
||||
|
||||
**Правило тиші:** Мовчання — нормально. Не втручайся у кожну розмову.
|
||||
**ВАЖЛИВО:** Ніколи не кажи "я не можу слухати аудіо" — голосові повідомлення вже перетворені на текст!
|
||||
|
||||
---
|
||||
|
||||
|
||||
447
tests/test_behavior_policy.py
Normal file
447
tests/test_behavior_policy.py
Normal 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"])
|
||||
Reference in New Issue
Block a user