Stream Lark bot replies progressively as LLM deltas arrive#241
Stream Lark bot replies progressively as LLM deltas arrive#241
Conversation
On the first LLM text delta, post a placeholder Lark message and capture its message_id; subsequent deltas PATCH the same message at most once per 750ms so users see the reply stream in instead of sitting through 5-30s of silence. HandleChatEnd / HandleChatTimeout always flush the final content via a last PATCH; if the placeholder post or any update throws, streaming is disabled for the session and the existing SendReplyAsync path is used as fallback. Interface split: new IStreamingPlatformAdapter extends IPlatformAdapter with PostStreamingPlaceholderAsync / UpdateStreamingMessageAsync. LarkPlatformAdapter implements it; adapters without streaming support keep the single-POST path unchanged. ChannelUserGAgent tracks placeholder state per session in-memory only — on actor restart, new deltas simply open a fresh placeholder, so streaming is a best-effort UX enhancement that never blocks correctness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 577a801943
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!delivery.Succeeded) | ||
| { | ||
| if (isFinal) | ||
| { | ||
| Logger.LogWarning( |
There was a problem hiding this comment.
Fall back to send-once when final stream PATCH fails
When UpdateStreamingMessageAsync returns a non-success result, TryStreamingUpdateAsync immediately returns false without disabling streaming state. This leaves MessageId active, so SendReplyAndCompleteAsync keeps taking the streaming branch on both HandleChatEnd and the timeout retry, and never calls SendPlatformReplyAsync; with forceComplete=true on timeout, the session is then closed while users can be left with only stale placeholder/partial text (for example when PATCH is rejected with cannot_patch_to_interactive or a stale message_id).
Useful? React with 👍 / 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## dev #241 +/- ##
==========================================
- Coverage 68.49% 68.48% -0.01%
==========================================
Files 1108 1108
Lines 77793 77793
Branches 10178 10178
==========================================
- Hits 53287 53280 -7
- Misses 20612 20617 +5
- Partials 3894 3896 +2
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
HandleRegisterAsync (PR #240 + 42be6a9) already returns 202 with projection_ready + projection_warning when the register projection has not materialized in time — avoids 5xx-triggered retries that would duplicate-write fresh registrationIds. The Tool path (nyxid-chat action=register) still followed the old silent-accept shape: status="accepted" + a weak "note" field that LLMs routinely dropped. The user would paste the returned registration_id into the Feishu developer console, hit the callback URL, and see "Challenge code 没有返回" forever because the id never materialized into the readmodel. Align with the HTTP endpoint: always return status="registered" and surface the degraded state through projection_ready (bool) + projection_warning (nullable string). Caller LLMs can then warn the user before advertising the callback URL as usable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
关闭此 PR:底层架构已在 dev 上演进到新方向,PR 涉及的主要文件全部被移除或退役,rebase 已无意义。 依据
aevatar 的 ChannelRuntime 已完全不持有 Lark 凭据( 如果之后仍要做 Lark 渐进流式回复需要重新设计,不在此 PR 延续:
|
问题
Lark bot 收到消息后,用户要静默等待 5–30s 才能看到 LLM 回复。
HandleChatContent把所有 delta 累到StringBuilder,直到HandleChatEnd才一次 POST 到 Lark。方案
IStreamingPlatformAdapter能力接口(扩展IPlatformAdapter):PostStreamingPlaceholderAsync+UpdateStreamingMessageAsync。LarkPlatformAdapter实现该接口:第一条 delta 通过 LarkPOST /open-apis/im/v1/messages发占位消息并捕获message_id;后续 delta 通过PATCH /open-apis/im/v1/messages/{id}覆盖内容。互动卡片载荷保持走原 send-once 路径,避免消息类型冲突。ChannelUserGAgent.HandleChatContent改为 async:首 delta 立即发占位消息,后续 delta 按 750ms 节流 PATCH;HandleChatEnd/HandleChatTimeout通过同一 helperisFinal: true强制 flush 最终文本,绕过节流。Disabled,SendReplyAndCompleteAsync回退到原有SendReplyAsync路径。影响路径
agents/Aevatar.GAgents.ChannelRuntime/IPlatformAdapter.cs— 新接口IStreamingPlatformAdapteragents/Aevatar.GAgents.ChannelRuntime/Adapters/LarkPlatformAdapter.cs— 实现流式接口 + 辅助方法agents/Aevatar.GAgents.ChannelRuntime/ChannelUserGAgent.cs— 引入_streamingDeliveriesper-session 状态、TryStreamingUpdateAsynchelper;重写HandleChatContent为 async;SendReplyAndCompleteAsync增加 streaming-first 分支test/Aevatar.GAgents.ChannelRuntime.Tests/ChannelUserGAgentContinuationTests.cs— 新增 happy path + placeholder 失败回退两条测试,提供RecordingStreamingPlatformAdapter验证
dotnet build agents/Aevatar.GAgents.ChannelRuntime/Aevatar.GAgents.ChannelRuntime.csproj— 0 errorsdotnet test test/Aevatar.GAgents.ChannelRuntime.Tests/Aevatar.GAgents.ChannelRuntime.Tests.csproj— 228/228 pass(原 226 + 2 新增)bash tools/ci/architecture_guards.sh— 全部通过🤖 Generated with Claude Code