Skip to content

fix(lark): Defer card creation and renew on tool call break#6743

Merged
RC-CHN merged 4 commits intoAstrBotDevs:masterfrom
camera-2018:fix/lark-streaming-output
Mar 21, 2026
Merged

fix(lark): Defer card creation and renew on tool call break#6743
RC-CHN merged 4 commits intoAstrBotDevs:masterfrom
camera-2018:fix/lark-streaming-output

Conversation

@camera-2018
Copy link
Contributor

@camera-2018 camera-2018 commented Mar 21, 2026

fix(lark): defer streaming card creation and renew card on tool call break

Motivation / 动机

修复飞书流式输出的两个体验问题:

  1. 首字到来前渲染空卡片:原实现在流式开始时就立即创建并发送 CardKit 卡片,导致用户在 LLM 首个 token 到来前看到一个空白卡片。
  2. 工具调用后消息错位:当 LLM 中途触发工具调用时,工具状态消息(如"🔨 调用工具: web_search_tavily")作为独立消息发送到旧卡片下方,但 LLM 后续生成的文本仍然更新在旧卡片上,用户注意力停留在工具消息,无法看到最终 AI 回复。

Modifications / 改动点

astrbot/core/platform/sources/lark/lark_event.py

  • 延迟卡片创建:将 _create_streaming_card()_send_card_message() 延迟到第一个 Plain 文本组件到来时才执行,避免空卡片渲染。
  • 处理 break 分段信号:收到 type="break" 时,刷新并关闭当前卡片,重置状态。下一个文本 token 到来时通过 lazy-init 自动创建新卡片,确保新卡片出现在工具状态消息下方,符合用户阅读顺序。
  • 添加 fallback 机制:卡片创建或发送失败时,消费剩余 generator 内容并回退到非流式发送。

astrbot/core/astr_agent_run_util.py

  • 条件化 break 信号:将 break 的发送条件从 if agent_runner.streaming 改为 if agent_runner.streaming and show_tool_use。当用户配置不显示工具输出时,不发送 break,AI 回复在同一张卡片上连续更新,避免不必要的卡片分裂。

  • 补充中文注释:详细说明 break 信号的含义、用途和条件约束。

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

验证步骤:

  1. 发送触发工具调用的消息(如"搜索科技新闻"),确认:
    • 首字到来前无空白卡片
    • 工具调用前的部分回复在 Card #1 中正确关闭
    • 工具状态消息出现在 Card #1 下方
    • 工具执行后 AI 的新回复在 Card #2 中展示(位于工具消息下方)
  2. 发送普通消息(无工具调用),确认单卡片正常流式输出
  3. 关闭"显示工具输出"配置,发送触发工具调用的消息,确认 AI 回复在同一张卡片上连续更新

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了"验证步骤"和"运行截图"

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

…break

- Defer CardKit streaming card creation until the first text token
  arrives, preventing an empty card from rendering before content.
- Handle `type="break"` signal in send_streaming: close the current
  card and lazily create a new one for post-tool-call text, so the
  new card appears below the tool status message in correct order.
- Only emit "break" signal when show_tool_use is enabled; when tool
  output is hidden, the AI response continues on the same card.
@auto-assign auto-assign bot requested review from Soulter and anka-afk March 21, 2026 08:46
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 21, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses two user experience issues in Lark's streaming output: the rendering of blank cards before the first LLM token arrives and message misalignment when tool calls interrupt the LLM's response. By deferring the creation of streaming cards and implementing a mechanism to renew cards upon a tool call break, the changes ensure a smoother and more coherent user experience, especially when interacting with AI agents that utilize tools.

Highlights

  • Deferred Card Creation: CardKit card creation and sending are now delayed until the first Plain text component arrives, preventing blank cards.
  • Tool Call Break Handling: Upon receiving a type="break" signal, the current card is refreshed and closed, and a new card is lazily created for subsequent text, ensuring correct message order after tool calls.
  • Fallback Mechanism: A fallback is implemented to consume remaining generator content and revert to non-streaming if card creation or sending fails.
  • Conditional Break Signal: The break signal is now sent only when agent_runner.streaming is true AND show_tool_use is true, preventing unnecessary card splits when tool output is not displayed.
  • Enhanced Comments: Detailed Chinese comments were added to explain the break signal's meaning, usage, and conditions.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In the fallback path (_consume_rest_and_fallback), after you return from send_streaming the finally block still runs and hits the card_id is None branch, causing Metric.upload and _has_send_oper to be executed twice for the same request; consider adding a flag to distinguish the fallback path or skipping the metric upload when a non-streaming send has already been performed.
  • When handling type == "break", you await sender_task inside the async for loop and then later also await it again in the outer finally (if it wasn’t reset in some edge path); it may be safer to explicitly set sender_task = None in all early-return/error branches around card closing to avoid any possibility of double-awaiting a finished task.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the fallback path (`_consume_rest_and_fallback`), after you `return` from `send_streaming` the `finally` block still runs and hits the `card_id is None` branch, causing `Metric.upload` and `_has_send_oper` to be executed twice for the same request; consider adding a flag to distinguish the fallback path or skipping the metric upload when a non-streaming send has already been performed.
- When handling `type == "break"`, you await `sender_task` inside the `async for` loop and then later also await it again in the outer `finally` (if it wasn’t reset in some edge path); it may be safer to explicitly set `sender_task = None` in all early-return/error branches around card closing to avoid any possibility of double-awaiting a finished task.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/lark/lark_event.py" line_range="743-751" />
<code_context>
 async def send_streaming(self, generator, use_fallback: bool = False):
</code_context>
<issue_to_address>
**question (bug_risk):** `use_fallback` argument is no longer respected in the streaming path.

This used to route failures through `_fallback_send_streaming(generator, use_fallback)`, so callers could control behavior via `use_fallback`. Now the argument is ignored and all failures go through `_consume_rest_and_fallback`. If `use_fallback` is still part of the public contract, please either restore conditional handling based on it or remove/rename the parameter to avoid a silent behavior change.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Lark streaming card implementation to improve user experience by deferring card creation until the first text token arrives and correctly handling message breaks during tool calls. The changes are well-structured and include robust error handling and fallbacks. My review includes one suggestion to improve maintainability by reducing code duplication.

Comment on lines +806 to +813
# Flush final text to the current card
if delta and delta != last_sent:
sequence += 1
await self._update_streaming_text(
card_id, delta, sequence
)
sequence += 1
await self._close_streaming_mode(card_id, sequence)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for flushing the final text and closing the card is also present at the end of the send_streaming function (lines 866-872). Duplicating this logic can make future maintenance more difficult, as any changes would need to be applied in both places. Consider extracting this logic into a private helper method to improve code reuse and maintainability.

@dosubot dosubot bot added the area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. label Mar 21, 2026
@RC-CHN RC-CHN self-requested a review March 21, 2026 09:07
Copy link
Member

@RC-CHN RC-CHN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

亲亲抱抱举高高

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 21, 2026
@RC-CHN RC-CHN merged commit 1735837 into AstrBotDevs:master Mar 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants