Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions src/tools/edit_message_text.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
OpenAITools::Tool tools::editMessageText(_<ITelegramClient> telegram, _<td::td_api::chat> chat) {
return {
.name = "edit_message_text",
.description = "Edits the text of a previously sent message in \"{}\" chat.\n"
.description = "Edits the text (or the caption of a photo/media message) previously sent in \"{}\" chat.\n"
"You should use this to fix a typo or correct a mistake in a message you already sent."_format(chat->title_),
.parameters = {
.properties = {
Expand All @@ -22,18 +22,27 @@ OpenAITools::Tool tools::editMessageText(_<ITelegramClient> telegram, _<td::td_a
auto messageId = util::jsonAsLongInt(ctx.args["message_id"]).valueOrException("message_id integer is required");
auto text = ctx.args["text"].asStringOpt().valueOrException("text string is required");

// remap client-side messageId (which was reported to llm to server-side messageId)
messageId = (co_await telegram->getMessage(chat->id_, messageId))->id_;
// remap client-side messageId (reported to the llm) to the server-side messageId, and
// inspect the message so we edit plain text vs. media captions (photo/video) correctly.
auto message = co_await telegram->getMessage(chat->id_, messageId);
messageId = message->id_;

auto content = td::td_api::make_object<td::td_api::inputMessageText>();
content->text_ = [&] {
auto t = td::td_api::make_object<td::td_api::formattedText>();
t->text_ = text.toStdString();
return t;
}();
auto formatted = td::td_api::make_object<td::td_api::formattedText>();
formatted->text_ = text.toStdString();

co_await telegram->sendQueryWithResult(
ITelegramClient::toPtr(td::td_api::editMessageText(chat->id_, messageId, nullptr, std::move(content))));
const bool isPlainText =
message->content_ && message->content_->get_id() == td::td_api::messageText::ID;

if (isPlainText) {
auto content = td::td_api::make_object<td::td_api::inputMessageText>();
content->text_ = std::move(formatted);
co_await telegram->sendQueryWithResult(ITelegramClient::toPtr(
td::td_api::editMessageText(chat->id_, messageId, nullptr, std::move(content))));
} else {
// Photo/video/other media message: TDLib rejects editMessageText here, edit the caption.
co_await telegram->sendQueryWithResult(ITelegramClient::toPtr(
td::td_api::editMessageCaption(chat->id_, messageId, nullptr, std::move(formatted), false)));
}

co_return "Message {} was edited successfully."_format(messageId);
},
Expand Down
54 changes: 52 additions & 2 deletions src/tools/react_with_emoji.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

#include "util/json_utils.h"

// Telegram stores reaction emojis with or without the U+FE0F variation selector (e.g. "❤️" vs "❤",
// "⚡️" vs "⚡"). Strip it so the model's emoji matches the chat's allowed-reaction list reliably.
static std::string stripVariationSelector(std::string s) {
static const std::string kVs16 = "\xEF\xB8\x8F"; // U+FE0F in UTF-8
for (auto pos = s.find(kVs16); pos != std::string::npos; pos = s.find(kVs16)) {
s.erase(pos, kVs16.size());
}
return s;
}

OpenAITools::Tool tools::reactWithEmoji(_<ITelegramClient> telegram, _<td::td_api::chat> chat) {
return {
.name = "react_with_emoji",
Expand All @@ -27,14 +37,54 @@ OpenAITools::Tool tools::reactWithEmoji(_<ITelegramClient> telegram, _<td::td_ap
auto messageId = util::jsonAsLongInt(ctx.args["message_id"]).valueOrException("message_id integer required");
auto emoji = ctx.args["emoji"].asStringOpt().valueOrException("emoji required");

// Some chats (news channels, restricted groups) only allow a subset of reactions. Firing
// addMessageReaction blindly there returns REACTION_INVALID ("The reaction isn't available
// for the message"), which the model can't recover from and rationalizes as a broken tool.
// Private chats and unrestricted chats report chatAvailableReactionsAll and are left alone.
if (const auto* avail = chat->available_reactions_.get();
avail && avail->get_id() == td::td_api::chatAvailableReactionsSome::ID) {
const auto* some = static_cast<const td::td_api::chatAvailableReactionsSome*>(avail);
const auto wanted = stripVariationSelector(emoji.toStdString());
AStringVector allowed;
bool ok = false;
for (const auto& r : some->reactions_) {
if (r && r->get_id() == td::td_api::reactionTypeEmoji::ID) {
const auto& e = static_cast<const td::td_api::reactionTypeEmoji*>(r.get())->emoji_;
allowed << AString::fromUtf8(e);
if (stripVariationSelector(e) == wanted) {
ok = true;
}
}
}
if (!ok) {
if (allowed.empty()) {
co_return "Reactions are disabled for this message - you can't react here. Just skip it, this is not an error.";
}
co_return "The reaction {} isn't available for this message. Allowed reactions here: {}. Pick one of those, or just skip reacting."_format(
emoji, allowed.join(' '));
}
}

auto reaction = td::td_api::make_object<td::td_api::addMessageReaction>();
reaction->chat_id_ = chat->id_;
reaction->message_id_ = messageId;
reaction->reaction_type_ = td::td_api::make_object<td::td_api::reactionTypeEmoji>(emoji.toStdString());
// Telegram's active emoji reactions are stored WITHOUT the U+FE0F variation selector
// (e.g. the heart reaction is "❤" = U+2764, not "❤️" = U+2764 U+FE0F). Sending the
// FE0F form makes addMessageReaction fail with REACTION_INVALID ("The reaction isn't
// available for the message") - this is exactly why ❤️/⚡️ failed while single-codepoint
// emojis (🔥 👍 🤔 …) worked. Strip it so the byte form matches the server's.
reaction->reaction_type_ = td::td_api::make_object<td::td_api::reactionTypeEmoji>(stripVariationSelector(emoji.toStdString()));
reaction->is_big_ = false;
reaction->update_recent_reactions_ = true;

co_await telegram->sendQueryWithResult(std::move(reaction));
try {
co_await telegram->sendQueryWithResult(std::move(reaction));
} catch (const AException& e) {
// Residual per-message restrictions or transient state. Report gracefully so the model
// treats it as "can't react to this one" instead of "reactions are globally broken".
co_return "Couldn't add the reaction {} to this message ({}). It happens with some messages - just skip reacting here, it's not a real error."_format(
emoji, e.getMessage());
}
co_return "Reaction {} added successfully."_format(emoji);
},
};
Expand Down
Loading