diff --git a/docs/open-api-docs.yaml b/docs/open-api-docs.yaml
index a6e2bd08..2130ed03 100644
--- a/docs/open-api-docs.yaml
+++ b/docs/open-api-docs.yaml
@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: The Agent's user-facing API
description: The user-facing parts of The Agent's API service (excluding system-level endpoints, chat completion, maintenance endpoints, etc.)
- version: 5.17.0
+ version: 5.18.0
license:
name: MIT
url: https://opensource.org/licenses/MIT
diff --git a/openspec/changes/archive/2026-05-24-expand-attachment-formats/tasks.md b/openspec/changes/archive/2026-05-24-expand-attachment-formats/tasks.md
index 8d2d6f24..d6bd7d52 100644
--- a/openspec/changes/archive/2026-05-24-expand-attachment-formats/tasks.md
+++ b/openspec/changes/archive/2026-05-24-expand-attachment-formats/tasks.md
@@ -68,6 +68,6 @@
- [x] 9.1 Run `pipenv run pre-commit run --all-files --show-diff-on-failure`
- [x] 9.2 Run the full test suite via the project's test runner script
-- [ ] 9.3 Manual sanity check: send a `.md`, a `.docx`, and a `.pdf` through the running bot in dev mode and confirm correct strategy + content reach the chat LLM
+- [x] 9.3 Manual sanity check: send a `.md`, a `.docx`, and a `.pdf` through the running bot in dev mode and confirm correct strategy + content reach the chat LLM
- [x] 9.4 Update API/feature documentation in `docs/` to mention the broader file-type support
- (N/A: docs cover REST endpoints only; file attachment behavior is internal to the bot and not documented in open-api-docs.yaml)
diff --git a/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/.openspec.yaml b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/.openspec.yaml
new file mode 100644
index 00000000..e8d4ccfe
--- /dev/null
+++ b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-06-08
diff --git a/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/design.md b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/design.md
new file mode 100644
index 00000000..826d2d3c
--- /dev/null
+++ b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/design.md
@@ -0,0 +1,50 @@
+## Context
+
+Telegram and WhatsApp both support reacting to an existing message. The current chat responder flow always treats a non-empty `AIMessage` as text to map, store, and send, which makes a one-emoji acknowledgement appear as a new chat message instead of a native reaction.
+
+The response delivery logic lives in the platform responders, while chat prompt construction lives in the prompt resolver/library. Reaction support already exists behind `PlatformBotSDK.set_reaction(...)`.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Allow the chat model to intentionally produce exactly one allowed reaction emoji for lightweight acknowledgements.
+- Keep detection simple with `is_reaction_response(text, chat_type)`.
+- Preserve future chat context by storing a synthetic bot-authored message using `{emoji}`.
+- Keep Telegram and WhatsApp response flow logging and read handling aligned with normal text responses.
+- Keep unsupported chat types, such as GitHub, from using reaction-only responses for now.
+
+**Non-Goals:**
+
+- No reaction support for GitHub.
+- No fuzzy extraction from longer model responses.
+- No new database columns or migrations.
+- No long-lived baseline spec retention requirement for this feature work.
+
+## Decisions
+
+- Use platform allow lists from `integration_config.py`.
+ - Rationale: these lists already define what each platform can receive.
+ - Alternative considered: hard-code a smaller prompt-only list. Rejected because it would drift from platform support.
+
+- Use `is_reaction_response(text, chat_type)` as an exact trimmed-string membership check.
+ - Rationale: this keeps the branch explicit and avoids parsing or extracting a reaction from mixed text.
+ - Alternative considered: extract the first allowed emoji from the model response. Rejected because it could turn ambiguous text into unintended reactions.
+
+- Keep reaction handling inline in the Telegram and WhatsApp responder send/store block.
+ - Rationale: the existing block calculates `sent_messages`, resolves the agent, logs completion, and marks WhatsApp messages as read. Reaction handling should preserve that flow.
+ - Alternative considered: a separate helper that returns early. Rejected because it bypasses shared response accounting and logging.
+
+- Store synthetic reaction history as `reaction:{incoming_message_id}` with text `{emoji}`.
+ - Rationale: the deterministic message ID makes retries idempotent through `chat_message_crud.save(...)`, and the marker is readable by future prompt history.
+ - Alternative considered: do not store reactions. Rejected because future replies need to see that the agent resolved the user request.
+
+- Treat platform reaction delivery as best-effort after storing the synthetic history message.
+ - Rationale: a platform reaction API failure should not turn a lightweight acknowledgement into a user-visible error response.
+ - Alternative considered: let reaction failures flow to the outer responder error handler. Rejected because the durable chat-history signal is already recorded.
+
+## Risks / Trade-offs
+
+- Reaction API failure can leave only the synthetic history marker without a visible platform reaction -> The platform API call is wrapped with `silent(...)` because preserving chat flow is more important than surfacing a failed acknowledgement reaction.
+- The model may still send a short text instead of a reaction -> The prompt encourages, but does not force, reaction-only output.
+- The synthetic marker is plain text in history -> This is acceptable because history already carries formatted bot text, and the marker is intentionally simple.
diff --git a/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/proposal.md b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/proposal.md
new file mode 100644
index 00000000..505e4814
--- /dev/null
+++ b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/proposal.md
@@ -0,0 +1,31 @@
+## Why
+
+Short chat acknowledgements from the agent currently become new text messages even when an emoji reaction would be the natural platform-native response. This adds noise to Telegram and WhatsApp chats and makes future chat history less clear about whether the user request was resolved.
+
+## What Changes
+
+- Allow the chat model to reply with exactly one platform-supported reaction emoji when a lightweight acknowledgement is enough.
+- Detect exact emoji-only responses with `is_reaction_response(...)` based on chat type.
+- For Telegram and WhatsApp, send a reaction to the incoming message instead of sending the emoji as a new text message.
+- Store a synthetic bot-authored chat history message using `{emoji}` so future replies can see that the agent responded.
+- Add release-summary prompt constraints: do not use code blocks unless actual code is included, and do not mention or reference the current user or current chat.
+
+## Capabilities
+
+### New Capabilities
+
+- `chat-reaction-responses`: Change-local requirements for converting emoji-only chat responses into platform reactions.
+
+### Modified Capabilities
+
+- None.
+
+## Impact
+
+- `src/features/integrations/integrations.py`
+- `src/features/integrations/prompt_resolvers.py`
+- `src/features/prompting/prompt_composer.py`
+- `src/features/prompting/prompt_library.py`
+- `src/features/chat/telegram/telegram_update_responder.py`
+- `src/features/chat/whatsapp/whatsapp_update_responder.py`
+- Existing responder and integrations tests
diff --git a/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/specs/chat-reaction-responses/spec.md b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/specs/chat-reaction-responses/spec.md
new file mode 100644
index 00000000..9d2a1b7a
--- /dev/null
+++ b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/specs/chat-reaction-responses/spec.md
@@ -0,0 +1,40 @@
+## ADDED Requirements
+
+### Requirement: Emoji-only chat responses become platform reactions
+The system SHALL treat an agent chat response containing exactly one allowed platform reaction emoji as a reaction response for chat types that support reactions.
+
+#### Scenario: Telegram reaction response
+- **WHEN** a Telegram chat response contains exactly one allowed Telegram reaction emoji
+- **THEN** the system sends that emoji as a reaction to the incoming Telegram message
+- **AND** the system does not send the emoji as a new Telegram text message
+
+#### Scenario: WhatsApp reaction response
+- **WHEN** a WhatsApp chat response contains exactly one allowed WhatsApp reaction emoji
+- **THEN** the system sends that emoji as a reaction to the incoming WhatsApp message
+- **AND** the system does not send the emoji as a new WhatsApp text message
+
+#### Scenario: Unsupported chat type
+- **WHEN** a chat type has no allowed reaction list
+- **THEN** the system does not classify emoji-only responses as reaction responses for that chat type
+
+### Requirement: Reaction responses are stored in chat history
+The system SHALL store classified reaction responses as bot-authored synthetic chat messages using the `{emoji}` marker format.
+
+#### Scenario: Reaction response is stored
+- **WHEN** the system classifies an agent response as a reaction response
+- **THEN** the system stores a chat message with ID `reaction:{incoming_message_id}`
+- **AND** the stored text uses `{emoji}`
+
+#### Scenario: Platform reaction failure is ignored
+- **WHEN** storing the reaction response succeeds
+- **AND** the platform reaction API fails
+- **THEN** the system does not send an error response to the chat
+- **AND** the stored reaction history message remains available for future chat context
+
+### Requirement: Chat prompt exposes allowed reaction responses
+The system SHALL include platform-specific allowed reaction emojis in the chat system prompt and instruct the model to use exactly one of them when a lightweight acknowledgement is enough.
+
+#### Scenario: Platform reactions are available in prompt
+- **WHEN** the chat prompt is built for a supported reaction platform
+- **THEN** the prompt includes the allowed reaction emoji list for that platform
+- **AND** the prompt instructs that reaction-only output must contain exactly one emoji and no additional text
diff --git a/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/tasks.md b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/tasks.md
new file mode 100644
index 00000000..b992ec7b
--- /dev/null
+++ b/openspec/changes/archive/2026-06-08-emoji-only-reaction-responses/tasks.md
@@ -0,0 +1,32 @@
+## 1. Reaction Classification
+
+- [x] 1.1 Add platform-specific allowed reaction resolution for Telegram and WhatsApp.
+- [x] 1.2 Add `is_reaction_response(text, chat_type)` as a simple exact trimmed-string check.
+- [x] 1.3 Ensure unsupported chat types, including GitHub, do not classify emoji-only content as reaction responses.
+
+## 2. Prompt Updates
+
+- [x] 2.1 Add `allowed_reactions` as a prompt variable.
+- [x] 2.2 Include the platform-specific allowed reaction list in chat prompts.
+- [x] 2.3 Instruct the chat model to prefer a single allowed reaction emoji over one- or two-word acknowledgements when appropriate.
+- [x] 2.4 Update release-summary prompt guidance to avoid code blocks unless actual code is included.
+- [x] 2.5 Update release-summary prompt guidance to avoid referencing the current user or current chat.
+
+## 3. Responder Behavior
+
+- [x] 3.1 In Telegram response delivery, branch inline on `is_reaction_response(...)`.
+- [x] 3.2 In WhatsApp response delivery, branch inline on `is_reaction_response(...)`.
+- [x] 3.3 Send reaction responses through `PlatformBotSDK.set_reaction(...)`.
+- [x] 3.4 Store successful reaction responses as synthetic bot messages with ID `reaction:{incoming_message_id}`.
+- [x] 3.5 Store synthetic reaction text as `{emoji}`.
+- [x] 3.6 Preserve normal responder completion logging and response accounting.
+- [x] 3.7 Wrap platform reaction sending with `silent(...)` so reaction API failures do not trigger error replies.
+
+## 4. Tests and Verification
+
+- [x] 4.1 Update existing integration tests for allowed reactions and `is_reaction_response(...)`.
+- [x] 4.2 Update existing Telegram responder tests for reaction success and failure behavior.
+- [x] 4.3 Update existing WhatsApp responder tests for reaction success and failure behavior.
+- [x] 4.4 Run `pipenv run ruff check --fix`.
+- [x] 4.5 Run `pipenv run python tools/check_spacing.py --fix`.
+- [x] 4.6 Run all tests with `pipenv run pytest -v`.
diff --git a/pyproject.toml b/pyproject.toml
index f25fb606..31e2a878 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "the-agent"
-version = "5.17.0"
+version = "5.18.0"
[tool.setuptools]
package-dir = {"" = "src"}
diff --git a/src/features/chat/telegram/telegram_update_responder.py b/src/features/chat/telegram/telegram_update_responder.py
index 88da8853..ee5faf64 100644
--- a/src/features/chat/telegram/telegram_update_responder.py
+++ b/src/features/chat/telegram/telegram_update_responder.py
@@ -1,6 +1,9 @@
+from datetime import datetime
+
from fastapi import HTTPException
from langchain_core.messages import AIMessage
+from db.schema.chat_message import ChatMessageSave
from db.sql import get_detached_session
from di.di import DI
from features.chat.chat_agent import ChatAgent
@@ -8,7 +11,7 @@
from features.chat.telegram.telegram_data_resolver import TelegramDataResolver
from features.external_tools.intelligence_presets import default_tool_for
from features.integrations import prompt_resolvers
-from features.integrations.integrations import resolve_agent_user
+from features.integrations.integrations import format_reaction_response, is_reaction_response, resolve_agent_user
from util import log
from util.config import config
from util.errors import ServiceError
@@ -51,12 +54,30 @@ def respond_to_update(update: Update) -> bool:
# send and store the response[s]
sent_messages: int = 0
- domain_messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer)
- for message in domain_messages:
- di.telegram_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text)
- sent_messages += 1
-
agent = resolve_agent_user(resolved_domain_data.chat.chat_type)
+ as_reaction = str(answer.content).strip()
+ if is_reaction_response(as_reaction, resolved_domain_data.chat.chat_type):
+ di.chat_message_crud.save(
+ ChatMessageSave(
+ chat_id = resolved_domain_data.chat.chat_id,
+ message_id = f"reaction:{resolved_domain_data.message.message_id}",
+ author_id = agent.id,
+ sent_at = datetime.now(),
+ text = format_reaction_response(as_reaction),
+ ),
+ )
+ silent(di.platform_bot_sdk().set_reaction)(
+ str(resolved_domain_data.chat.external_id),
+ resolved_domain_data.message.message_id,
+ as_reaction,
+ )
+ log.i(f"Reacted to message {resolved_domain_data.message.message_id} with {as_reaction}")
+ else:
+ domain_messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer)
+ for message in domain_messages:
+ di.telegram_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text)
+ sent_messages += 1
+
log.t(f"Finished responding to updates. \n[{agent.full_name}]: {answer.content}")
log.i(f"Sent {sent_messages} messages")
return True
diff --git a/src/features/chat/whatsapp/whatsapp_update_responder.py b/src/features/chat/whatsapp/whatsapp_update_responder.py
index 70e9e55c..c9e3f81d 100644
--- a/src/features/chat/whatsapp/whatsapp_update_responder.py
+++ b/src/features/chat/whatsapp/whatsapp_update_responder.py
@@ -1,5 +1,8 @@
+from datetime import datetime
+
from langchain_core.messages import AIMessage
+from db.schema.chat_message import ChatMessageSave
from db.sql import get_detached_session
from di.di import DI
from features.chat.chat_agent import ChatAgent
@@ -7,7 +10,7 @@
from features.chat.whatsapp.whatsapp_data_resolver import WhatsAppDataResolver
from features.external_tools.intelligence_presets import default_tool_for
from features.integrations import prompt_resolvers
-from features.integrations.integrations import resolve_agent_user
+from features.integrations.integrations import format_reaction_response, is_reaction_response, resolve_agent_user
from util import log
from util.config import config
from util.errors import ServiceError
@@ -56,15 +59,33 @@ def respond_to_update(update: Update) -> bool:
# send and store the response[s]
sent_messages: int = 0
- domain_messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer)
- for message in domain_messages:
- di.whatsapp_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text)
- sent_messages += 1
+ agent = resolve_agent_user(resolved_domain_data.chat.chat_type)
+ as_reaction = str(answer.content).strip()
+ if is_reaction_response(as_reaction, resolved_domain_data.chat.chat_type):
+ di.chat_message_crud.save(
+ ChatMessageSave(
+ chat_id = resolved_domain_data.chat.chat_id,
+ message_id = f"reaction:{resolved_domain_data.message.message_id}",
+ author_id = agent.id,
+ sent_at = datetime.now(),
+ text = format_reaction_response(as_reaction),
+ ),
+ )
+ silent(di.platform_bot_sdk().set_reaction)(
+ str(resolved_domain_data.chat.external_id),
+ resolved_domain_data.message.message_id,
+ as_reaction,
+ )
+ log.i(f"Reacted to message {resolved_domain_data.message.message_id} with {as_reaction}")
+ else:
+ domain_messages = di.domain_langchain_mapper.map_bot_message_to_storage(resolved_domain_data.chat, answer)
+ for message in domain_messages:
+ di.whatsapp_bot_sdk.send_text_message(str(resolved_domain_data.chat.external_id), message.text)
+ sent_messages += 1
# mark the incoming message as read
di.whatsapp_bot_sdk.mark_as_read(resolved_domain_data.message.message_id)
- agent = resolve_agent_user(resolved_domain_data.chat.chat_type)
log.t(f"Finished responding to updates. \n[{agent.full_name}]: {answer.content}")
log.i(f"Sent {sent_messages} messages")
return True
diff --git a/src/features/integrations/integrations.py b/src/features/integrations/integrations.py
index 29c635dc..c7936f4d 100644
--- a/src/features/integrations/integrations.py
+++ b/src/features/integrations/integrations.py
@@ -14,13 +14,16 @@
BACKGROUND_AGENT,
TELEGRAM_REACTION_INITIAL_DELAY_S,
TELEGRAM_REACTION_INTERVAL_S,
+ TELEGRAM_REACTIONS,
THE_AGENT,
WHATSAPP_REACTION_INITIAL_DELAY_S,
WHATSAPP_REACTION_INTERVAL_S,
+ WHATSAPP_REACTIONS,
)
from util.functions import normalize_phone_number, normalize_username
WHATSAPP_MESSAGING_WINDOW_HOURS = 24
+REACTION_RESPONSE_TEMPLATE = "{reaction}"
def resolve_agent_user(chat_type: ChatConfigDB.ChatType) -> UserSave:
@@ -200,6 +203,24 @@ def add_messaging_frequency_warning(response_data: dict, chat_type: ChatConfigDB
)
+def resolve_allowed_reactions(chat_type: ChatConfigDB.ChatType) -> list[str]:
+ match chat_type:
+ case ChatConfigDB.ChatType.telegram:
+ return TELEGRAM_REACTIONS
+ case ChatConfigDB.ChatType.whatsapp:
+ return WHATSAPP_REACTIONS
+ case _:
+ return []
+
+
+def is_reaction_response(text: str, chat_type: ChatConfigDB.ChatType) -> bool:
+ return text.strip() in resolve_allowed_reactions(chat_type)
+
+
+def format_reaction_response(reaction: str) -> str:
+ return REACTION_RESPONSE_TEMPLATE.format(reaction = reaction)
+
+
def resolve_reaction_timing(chat_type: ChatConfigDB.ChatType) -> tuple[int, int] | None:
match chat_type:
case ChatConfigDB.ChatType.telegram:
diff --git a/src/features/integrations/prompt_resolvers.py b/src/features/integrations/prompt_resolvers.py
index 529d8eb8..befbb28c 100644
--- a/src/features/integrations/prompt_resolvers.py
+++ b/src/features/integrations/prompt_resolvers.py
@@ -4,7 +4,7 @@
from db.schema.chat_config import ChatConfig, ChatConfigSave
from db.schema.user import User, UserSave
from features.chat.membership.chat_membership import ChatMembership
-from features.integrations.integrations import resolve_agent_user
+from features.integrations.integrations import resolve_agent_user, resolve_allowed_reactions
from features.prompting import prompt_composer, prompt_library
from features.prompting.prompt_composer import PromptFragment, PromptVar
from features.prompting.prompt_library import CHAT_MESSAGE_DELIMITER
@@ -45,6 +45,7 @@ def chat(
(PromptVar.author_role, invoker.group.value),
(PromptVar.date_and_time, __now()),
(PromptVar.tools_list, tools_list or PLACEHOLDER_NO_DATA),
+ (PromptVar.allowed_reactions, ", ".join(resolve_allowed_reactions(target_chat.chat_type)) or PLACEHOLDER_NO_DATA),
)
# add conditional generic components
if invoker_membership and invoker_membership.use_about_me and invoker.about_me and (
diff --git a/src/features/prompting/prompt_composer.py b/src/features/prompting/prompt_composer.py
index 14708333..ce90f153 100644
--- a/src/features/prompting/prompt_composer.py
+++ b/src/features/prompting/prompt_composer.py
@@ -38,6 +38,7 @@ class PromptVar(Enum):
support_request_type = "support_request_type"
content_template = "content_template"
tools_list = "tools_list"
+ allowed_reactions = "allowed_reactions"
@dataclass(frozen = True)
diff --git a/src/features/prompting/prompt_library.py b/src/features/prompting/prompt_library.py
index 34434ae8..7d9ed305 100644
--- a/src/features/prompting/prompt_library.py
+++ b/src/features/prompting/prompt_library.py
@@ -84,6 +84,7 @@ class _ContextLibrary:
"You should not explain or discuss anything. You should not ask questions either. "
"Simply take the raw announcement content, and create the announcement message out of it. "
"The only goal for you is to make your partners aware of your new release. "
+ "Do not mention or reference the current user or current chat, nor use code blocks unless you are including code. "
).strip(),
)
@@ -250,6 +251,9 @@ class _StyleLibrary:
f"`{CHAT_MESSAGE_DELIMITER}`. "
"Do not use `---`, `—`, or other similar line/message delimiters. These may not render correctly. "
"Again, the chat is really fast-paced, and long responses are considered boring. Don't be boring. "
+ "When a lightweight acknowledgement is enough, prefer replying with a single emoji reaction instead of "
+ "a one- or two-word text response. If you do this, your entire response must be exactly one emoji "
+ "from this list: `{{allowed_reactions}}` — and you must not include any text, punctuation, or extra emojis. "
).strip(),
)
diff --git a/test/features/chat/telegram/test_telegram_update_responder.py b/test/features/chat/telegram/test_telegram_update_responder.py
index 4013b268..0b7f7093 100644
--- a/test/features/chat/telegram/test_telegram_update_responder.py
+++ b/test/features/chat/telegram/test_telegram_update_responder.py
@@ -9,7 +9,7 @@
from db.model.chat_config import ChatConfigDB
from db.model.user import UserDB
from db.schema.chat_config import ChatConfig
-from db.schema.chat_message import ChatMessage
+from db.schema.chat_message import ChatMessage, ChatMessageSave
from db.schema.user import User
from features.chat.telegram.model.update import Update
from features.chat.telegram.telegram_data_resolver import TelegramDataResolver
@@ -103,6 +103,77 @@ def test_successful_response(self):
self.di.chat_agent.return_value.execute.assert_called_once()
self.di.telegram_bot_sdk.send_text_message.assert_called_once_with("123", "Test response")
+ def test_reaction_response(self):
+ self.di.chat_agent.return_value.execute.return_value = Mock(spec = AIMessage, content = "👍")
+ self.di.telegram_domain_mapper.map_update.return_value = Mock(
+ spec = TelegramDomainMapper.Result,
+ message = Mock(spec = ChatMessage, message_id = "test-message-id", text = "Test message text"),
+ )
+ self.di.telegram_data_resolver.resolve.return_value = Mock(
+ spec = TelegramDataResolver.Result,
+ chat = ChatConfig(
+ chat_id = UUID(int = 123),
+ external_id = "123",
+ language_name = "English",
+ language_iso_code = "en",
+ title = "Test Chat",
+ is_private = False,
+ reply_chance_percent = 100,
+ release_notifications = ChatConfigDB.ReleaseNotifications.all,
+ media_mode = ChatConfigDB.MediaMode.photo,
+ chat_type = ChatConfigDB.ChatType.telegram,
+ ),
+ author = Mock(spec = User, id = UUID(int = 1)),
+ message = Mock(spec = ChatMessage, message_id = "test-message-id"),
+ )
+
+ result = respond_to_update(self.update)
+
+ self.assertTrue(result)
+ self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍")
+ self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called()
+ self.di.telegram_bot_sdk.send_text_message.assert_not_called()
+ saved_message = self.di.chat_message_crud.save.call_args.args[0]
+ self.assertIsInstance(saved_message, ChatMessageSave)
+ self.assertEqual(saved_message.message_id, "reaction:test-message-id")
+ self.assertEqual(saved_message.text, "👍")
+
+ def test_reaction_response_failure(self):
+ self.di.chat_agent.return_value.execute.return_value = Mock(spec = AIMessage, content = "👍")
+ self.di.platform_bot_sdk.return_value.set_reaction.side_effect = Exception("Reaction failed")
+ self.di.telegram_domain_mapper.map_update.return_value = Mock(
+ spec = TelegramDomainMapper.Result,
+ message = Mock(spec = ChatMessage, message_id = "test-message-id", text = "Test message text"),
+ )
+ self.di.telegram_data_resolver.resolve.return_value = Mock(
+ spec = TelegramDataResolver.Result,
+ chat = ChatConfig(
+ chat_id = UUID(int = 123),
+ external_id = "123",
+ language_name = "English",
+ language_iso_code = "en",
+ title = "Test Chat",
+ is_private = False,
+ reply_chance_percent = 100,
+ release_notifications = ChatConfigDB.ReleaseNotifications.all,
+ media_mode = ChatConfigDB.MediaMode.photo,
+ chat_type = ChatConfigDB.ChatType.telegram,
+ ),
+ author = Mock(spec = User, id = UUID(int = 1)),
+ message = Mock(spec = ChatMessage, message_id = "test-message-id"),
+ )
+
+ result = respond_to_update(self.update)
+
+ self.assertTrue(result)
+ self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍")
+ self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called()
+ self.di.telegram_bot_sdk.send_text_message.assert_not_called()
+ saved_message = self.di.chat_message_crud.save.call_args.args[0]
+ self.assertIsInstance(saved_message, ChatMessageSave)
+ self.assertEqual(saved_message.message_id, "reaction:test-message-id")
+ self.assertEqual(saved_message.text, "👍")
+
def test_empty_response(self):
self.di.telegram_domain_mapper.map_update.return_value = Mock(spec = TelegramDomainMapper.Result)
self.di.telegram_data_resolver.resolve.return_value = Mock(
diff --git a/test/features/chat/whatsapp/test_whatsapp_update_responder.py b/test/features/chat/whatsapp/test_whatsapp_update_responder.py
index 8131664d..72a91b82 100644
--- a/test/features/chat/whatsapp/test_whatsapp_update_responder.py
+++ b/test/features/chat/whatsapp/test_whatsapp_update_responder.py
@@ -9,7 +9,7 @@
from db.model.chat_config import ChatConfigDB
from db.model.user import UserDB
from db.schema.chat_config import ChatConfig
-from db.schema.chat_message import ChatMessage
+from db.schema.chat_message import ChatMessage, ChatMessageSave
from db.schema.user import User
from features.chat.whatsapp.model.update import Update
from features.chat.whatsapp.whatsapp_data_resolver import WhatsAppDataResolver
@@ -107,6 +107,97 @@ def test_successful_response(self):
self.di.chat_agent.return_value.execute.assert_called_once()
self.di.whatsapp_bot_sdk.send_text_message.assert_called_once_with("123", "Test response")
+ def test_reaction_response(self):
+ self.di.chat_agent.return_value.execute.return_value = Mock(spec = AIMessage, content = "👍")
+ self.di.whatsapp_domain_mapper.map_update.return_value = [
+ Mock(
+ spec = WhatsAppDomainMapper.Result,
+ message = Mock(spec = ChatMessage, sent_at = datetime.now()),
+ ),
+ ]
+ self.di.whatsapp_data_resolver.resolve_all.return_value = [
+ Mock(
+ spec = WhatsAppDataResolver.Result,
+ chat = ChatConfig(
+ chat_id = UUID(int = 123),
+ external_id = "123",
+ language_name = "English",
+ language_iso_code = "en",
+ title = "Test Chat",
+ is_private = False,
+ reply_chance_percent = 100,
+ release_notifications = ChatConfigDB.ReleaseNotifications.all,
+ media_mode = ChatConfigDB.MediaMode.photo,
+ chat_type = ChatConfigDB.ChatType.whatsapp,
+ ),
+ author = Mock(spec = User, id = UUID(int = 1)),
+ message = Mock(
+ spec = ChatMessage,
+ message_id = "test-message-id",
+ sent_at = datetime.now(),
+ text = "Test message text",
+ ),
+ ),
+ ]
+
+ result = respond_to_update(self.update)
+
+ self.assertTrue(result)
+ self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍")
+ self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called()
+ self.di.whatsapp_bot_sdk.send_text_message.assert_not_called()
+ self.di.whatsapp_bot_sdk.mark_as_read.assert_called_once_with("test-message-id")
+ saved_message = self.di.chat_message_crud.save.call_args.args[0]
+ self.assertIsInstance(saved_message, ChatMessageSave)
+ self.assertEqual(saved_message.message_id, "reaction:test-message-id")
+ self.assertEqual(saved_message.text, "👍")
+
+ def test_reaction_response_failure(self):
+ self.di.chat_agent.return_value.execute.return_value = Mock(spec = AIMessage, content = "👍")
+ self.di.platform_bot_sdk.return_value.set_reaction.side_effect = Exception("Reaction failed")
+ self.di.whatsapp_domain_mapper.map_update.return_value = [
+ Mock(
+ spec = WhatsAppDomainMapper.Result,
+ message = Mock(spec = ChatMessage, sent_at = datetime.now()),
+ ),
+ ]
+ self.di.whatsapp_data_resolver.resolve_all.return_value = [
+ Mock(
+ spec = WhatsAppDataResolver.Result,
+ chat = ChatConfig(
+ chat_id = UUID(int = 123),
+ external_id = "123",
+ language_name = "English",
+ language_iso_code = "en",
+ title = "Test Chat",
+ is_private = False,
+ reply_chance_percent = 100,
+ release_notifications = ChatConfigDB.ReleaseNotifications.all,
+ media_mode = ChatConfigDB.MediaMode.photo,
+ chat_type = ChatConfigDB.ChatType.whatsapp,
+ ),
+ author = Mock(spec = User, id = UUID(int = 1)),
+ message = Mock(
+ spec = ChatMessage,
+ message_id = "test-message-id",
+ sent_at = datetime.now(),
+ text = "Test message text",
+ ),
+ ),
+ ]
+
+ result = respond_to_update(self.update)
+
+ self.assertTrue(result)
+ self.di.platform_bot_sdk.return_value.set_reaction.assert_called_once_with("123", "test-message-id", "👍")
+ self.di.domain_langchain_mapper.map_bot_message_to_storage.assert_not_called()
+ self.di.whatsapp_bot_sdk.send_text_message.assert_not_called()
+ self.di.whatsapp_bot_sdk.mark_as_read.assert_called_once_with("test-message-id")
+ saved_message = self.di.chat_message_crud.save.call_args.args[0]
+ self.assertIsInstance(saved_message, ChatMessageSave)
+ self.assertEqual(saved_message.message_id, "reaction:test-message-id")
+ self.assertEqual(saved_message.text, "👍")
+
def test_empty_response(self):
self.di.whatsapp_domain_mapper.map_update.return_value = [Mock(spec = WhatsAppDomainMapper.Result)]
self.di.whatsapp_data_resolver.resolve.return_value = Mock(
diff --git a/test/features/integrations/test_integrations.py b/test/features/integrations/test_integrations.py
index c85c8d1f..5a99510c 100644
--- a/test/features/integrations/test_integrations.py
+++ b/test/features/integrations/test_integrations.py
@@ -18,9 +18,11 @@
WHATSAPP_MESSAGING_WINDOW_HOURS,
format_handle,
is_own_chat,
+ is_reaction_response,
is_the_agent,
lookup_user_by_handle,
resolve_agent_user,
+ resolve_allowed_reactions,
resolve_any_external_handle,
resolve_best_notification_chat,
resolve_external_handle,
@@ -60,6 +62,22 @@ def test_resolve_agent_user_whatsapp(self):
self.assertEqual(agent.whatsapp_phone_number.get_secret_value(), config.whatsapp_bot_phone_number)
self.assertEqual(agent.full_name, "The Agent")
+ def test_resolve_allowed_reactions_telegram(self):
+ reactions = resolve_allowed_reactions(ChatConfigDB.ChatType.telegram)
+ self.assertIn("👍", reactions)
+
+ def test_resolve_allowed_reactions_github_empty(self):
+ self.assertEqual(resolve_allowed_reactions(ChatConfigDB.ChatType.github), [])
+
+ def test_is_reaction_response(self):
+ self.assertTrue(is_reaction_response(" 👍 ", ChatConfigDB.ChatType.telegram))
+
+ def test_is_reaction_response_rejects_text(self):
+ self.assertFalse(is_reaction_response("👍 ok", ChatConfigDB.ChatType.telegram))
+
+ def test_is_reaction_response_rejects_unsupported_chat_type(self):
+ self.assertFalse(is_reaction_response("👍", ChatConfigDB.ChatType.github))
+
def test_resolve_external_id_telegram_success(self):
user = User(
id = UUID(int = 1),