feat(channel): stream LLM responses to Telegram via draft message edits
Wire the existing provider-layer streaming infrastructure through the channel trait and agent loop so Telegram users see tokens arrive progressively via editMessageText, instead of waiting for the full response. Changes: - Add StreamMode enum (off/partial/block) and draft_update_interval_ms to TelegramConfig (backward-compatible defaults: off, 1000ms) - Add supports_draft_updates/send_draft/update_draft/finalize_draft to Channel trait with no-op defaults (zero impact on existing channels) - Implement draft methods on TelegramChannel using sendMessage + editMessageText with rate limiting and Markdown fallback - Add on_delta mpsc::Sender<String> parameter to run_tool_call_loop (None preserves existing behavior) - Wire streaming in process_channel_message: when channel supports drafts, send initial draft, spawn updater task, finalize on completion Edge cases handled: - 4096-char limit: finalize draft and fall back to chunked send - Broken Markdown: use no parse_mode during streaming, apply on finalize - Edit failures: fall back to sending complete response as new message - Rate limiting: configurable draft_update_interval_ms (default 1s)
This commit is contained in:
parent
a0b277b21e
commit
118cd53922
12 changed files with 410 additions and 43 deletions
|
|
@ -70,6 +70,36 @@ pub trait Channel: Send + Sync {
|
|||
async fn stop_typing(&self, _recipient: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether this channel supports progressive message updates via draft edits.
|
||||
fn supports_draft_updates(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Send an initial draft message. Returns a platform-specific message ID for later edits.
|
||||
async fn send_draft(&self, _message: &SendMessage) -> anyhow::Result<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Update a previously sent draft message with new accumulated content.
|
||||
async fn update_draft(
|
||||
&self,
|
||||
_recipient: &str,
|
||||
_message_id: &str,
|
||||
_text: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finalize a draft with the complete response (e.g. apply Markdown formatting).
|
||||
async fn finalize_draft(
|
||||
&self,
|
||||
_recipient: &str,
|
||||
_message_id: &str,
|
||||
_text: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -138,6 +168,23 @@ mod tests {
|
|||
.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_draft_methods_return_success() {
|
||||
let channel = DummyChannel;
|
||||
|
||||
assert!(!channel.supports_draft_updates());
|
||||
assert!(channel
|
||||
.send_draft(&SendMessage::new("draft", "bob"))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(channel.update_draft("bob", "msg_1", "text").await.is_ok());
|
||||
assert!(channel
|
||||
.finalize_draft("bob", "msg_1", "final text")
|
||||
.await
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn listen_sends_message_to_channel() {
|
||||
let channel = DummyChannel;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue