-
Notifications
You must be signed in to change notification settings - Fork 183
feat: add Telegram message editing support #185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -356,6 +356,15 @@ pub enum OutboundResponse { | |||||
| StreamStart, | ||||||
| StreamChunk(String), | ||||||
| StreamEnd, | ||||||
| /// Edit a previously sent message by ID. | ||||||
| /// Telegram: edits the text of a message using editMessageText. | ||||||
| /// Other platforms: falls back to sending a new message (no-op). | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment says other platforms "falls back to sending a new message", but adapters currently treat
Suggested change
|
||||||
| EditMessage { | ||||||
| /// The platform-specific message ID to edit. | ||||||
| message_id: String, | ||||||
| /// The new text content. | ||||||
| text: String, | ||||||
| }, | ||||||
| Status(StatusUpdate), | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -471,6 +471,27 @@ impl Messaging for TelegramAdapter { | |||||||||||||
| .await | ||||||||||||||
| .remove(&message.conversation_id); | ||||||||||||||
| } | ||||||||||||||
| OutboundResponse::EditMessage { message_id, text } => { | ||||||||||||||
| let msg_id = message_id | ||||||||||||||
| .parse::<i32>() | ||||||||||||||
| .context("invalid telegram message_id for edit")?; | ||||||||||||||
| let html = markdown_to_telegram_html(&text); | ||||||||||||||
|
|
||||||||||||||
| if let Err(html_error) = self | ||||||||||||||
|
Comment on lines
+478
to
+480
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: there’s a whitespace-only line after
Suggested change
Also, this branch duplicates the StreamChunk edit logic above (markdown->HTML->plain fallback). Might be worth extracting a small helper to keep behavior consistent. |
||||||||||||||
| .bot | ||||||||||||||
| .edit_message_text(chat_id, MessageId(msg_id), &html) | ||||||||||||||
| .parse_mode(ParseMode::Html) | ||||||||||||||
| .send() | ||||||||||||||
| .await | ||||||||||||||
| { | ||||||||||||||
| tracing::debug!(%html_error, "HTML edit failed, retrying as plain text"); | ||||||||||||||
| self.bot | ||||||||||||||
| .edit_message_text(chat_id, MessageId(msg_id), &text) | ||||||||||||||
| .send() | ||||||||||||||
| .await | ||||||||||||||
| .context("failed to edit telegram message")?; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| OutboundResponse::Status(status) => { | ||||||||||||||
| self.send_status(message, status).await?; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| //! Tool for editing a previously sent Telegram message. | ||
|
|
||
| use crate::conversation::ConversationLogger; | ||
| use crate::{ChannelId, OutboundResponse}; | ||
| use rig::completion::ToolDefinition; | ||
| use rig::tool::Tool; | ||
| use schemars::JsonSchema; | ||
| use serde::{Deserialize, Serialize}; | ||
| use std::sync::Arc; | ||
| use std::sync::atomic::{AtomicBool, Ordering}; | ||
| use tokio::sync::mpsc; | ||
|
|
||
| /// Shared flag between EditMessageTool and the channel event loop. | ||
| pub type EditedFlag = Arc<AtomicBool>; | ||
|
|
||
| /// Create a new edited flag (defaults to false). | ||
| pub fn new_edited_flag() -> EditedFlag { | ||
| Arc::new(AtomicBool::new(false)) | ||
| } | ||
|
|
||
| /// Tool for editing a previously sent Telegram message. | ||
| /// | ||
| /// This tool allows the agent to edit a message it previously sent by specifying | ||
| /// the message ID. Currently only supported on Telegram (uses editMessageText API). | ||
| /// On other platforms, calls are logged but have no effect. | ||
| #[derive(Debug, Clone)] | ||
| pub struct EditMessageTool { | ||
| response_tx: mpsc::Sender<OutboundResponse>, | ||
| conversation_id: String, | ||
| conversation_logger: ConversationLogger, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| channel_id: ChannelId, | ||
| edited_flag: EditedFlag, | ||
| } | ||
|
|
||
| impl EditMessageTool { | ||
| pub fn new( | ||
| response_tx: mpsc::Sender<OutboundResponse>, | ||
| conversation_id: impl Into<String>, | ||
| conversation_logger: ConversationLogger, | ||
| channel_id: ChannelId, | ||
| edited_flag: EditedFlag, | ||
| ) -> Self { | ||
| Self { | ||
| response_tx, | ||
| conversation_id: conversation_id.into(), | ||
| conversation_logger, | ||
| channel_id, | ||
| edited_flag, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, thiserror::Error)] | ||
| #[error("Edit message failed: {0}")] | ||
| pub struct EditMessageError(String); | ||
|
|
||
| #[derive(Debug, Deserialize, JsonSchema)] | ||
| pub struct EditMessageArgs { | ||
| /// The message ID to edit. This is the Telegram message_id that was returned | ||
| /// when the original message was sent. You can find this in conversation logs | ||
| /// or by referencing a previous message. | ||
| pub message_id: String, | ||
| /// The new text content to replace the original message with. | ||
| pub content: String, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize)] | ||
| pub struct EditMessageOutput { | ||
| pub success: bool, | ||
| pub conversation_id: String, | ||
| pub message_id: String, | ||
| pub content: String, | ||
| } | ||
|
|
||
| impl Tool for EditMessageTool { | ||
| const NAME: &'static str = "edit_message"; | ||
|
|
||
| type Error = EditMessageError; | ||
| type Args = EditMessageArgs; | ||
| type Output = EditMessageOutput; | ||
|
|
||
| async fn definition(&self, _prompt: String) -> ToolDefinition { | ||
| let parameters = serde_json::json!({ | ||
| "type": "object", | ||
| "properties": { | ||
| "message_id": { | ||
| "type": "string", | ||
| "description": "The Telegram message ID to edit. This is the numeric message_id from Telegram." | ||
| }, | ||
| "content": { | ||
| "type": "string", | ||
| "description": "The new text content to replace the original message with. Supports markdown formatting." | ||
| } | ||
| }, | ||
| "required": ["message_id", "content"] | ||
| }); | ||
|
|
||
| ToolDefinition { | ||
| name: Self::NAME.to_string(), | ||
| description: "Edit a previously sent Telegram message by its message ID. Only works on Telegram; other platforms will log but not execute the edit.".to_string(), | ||
| parameters, | ||
| } | ||
| } | ||
|
|
||
| async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> { | ||
| tracing::info!( | ||
| conversation_id = %self.conversation_id, | ||
| message_id = %args.message_id, | ||
| content_len = args.content.len(), | ||
| "edit_message tool called" | ||
| ); | ||
|
|
||
| let response = OutboundResponse::EditMessage { | ||
| message_id: args.message_id.clone(), | ||
| text: args.content.clone(), | ||
| }; | ||
|
|
||
| self.response_tx | ||
| .send(response) | ||
| .await | ||
| .map_err(|e| EditMessageError(format!("failed to send edit: {e}")))?; | ||
|
|
||
| // Mark the turn as handled so handle_agent_result skips any fallback | ||
| self.edited_flag.store(true, Ordering::Relaxed); | ||
|
|
||
| tracing::debug!( | ||
| conversation_id = %self.conversation_id, | ||
| message_id = %args.message_id, | ||
| "edit sent to outbound channel" | ||
| ); | ||
|
|
||
| Ok(EditMessageOutput { | ||
| success: true, | ||
| conversation_id: self.conversation_id.clone(), | ||
| message_id: args.message_id, | ||
| content: args.content, | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
edited_flaggets threaded through here, buthandle_agent_resultstill only checksskip_flag/replied_flag. If the model callsedit_messagewithoutreply, you'll likely still hit the fallback text send and double-post. Consider loadingedited_flagand treating it likereplied_flagfor fallback suppression.