Skip to content

Conversation

@Mile-Away
Copy link
Contributor

@Mile-Away Mile-Away commented Jan 5, 2026

变更内容

  • 新功能
  • 修复 Bug
  • 增强重构
  • 其他(请描述)

简要描述本次 PR 的主要变更内容。

相关 Issue

请关联相关 Issue(如有):#编号

检查清单

默认已勾选,如不满足,请检查。

  • 已在本地测试通过
  • 已补充/更新相关文档
  • 已添加测试用例
  • 代码风格已经过 pre-commit 钩子检查

其他说明

如有特殊说明或注意事项,请补充。

Summary by Sourcery

端到端新增 AI 思考/推理内容的流式支持,并改进应用市场和控制台仪表盘的界面响应性与本地化体验。

New Features:

  • 在聊天界面中展示和渲染模型的思考/推理轨迹,支持动画效果和可折叠视图。
  • 通过新的后端事件,将来自 LLM 提供方的思考内容以流式方式写入并持久化到消息中。
  • 为应用市场中的智能体详情页添加本地化界面,对标签、页签、操作和空状态等使用 i18n 字符串。

Enhancements:

  • 提升应用市场详情页标签和操作侧边栏在移动端的友好性和布局表现。
  • 统一渐变工具类,并简化 Google 提供方的工厂实现。

Build:

  • 清理包配置中未使用的前端依赖。

Tests:

  • 新增关于思考事件处理与内容提取的单元测试。
  • 为文件、消息、主题和会话仓库增加集成测试和工厂,用于覆盖 CRUD 和查询行为。
Original summary in English

Summary by Sourcery

Add streaming support for AI thinking/reasoning content end-to-end and improve marketplace and dashboard UI responsiveness and localization.

New Features:

  • Expose and render model thinking/reasoning traces in the chat UI with animated and collapsible views.
  • Stream thinking content from LLM providers through new backend events into persisted messages.
  • Add localized marketplace agent detail UI with i18n strings for labels, tabs, actions, and empty states.

Enhancements:

  • Improve mobile friendliness and layout behavior of marketplace detail tabs and action sidebar.
  • Standardize gradient utility classes and simplify Google provider factory implementation.

Build:

  • Clean up unused frontend dependencies in package configuration.

Tests:

  • Introduce unit tests for thinking event handling and content extraction.
  • Add integration tests and factories for file, message, topic, and session repositories to cover CRUD and querying behavior.

Mile-Away and others added 3 commits January 4, 2026 17:33
…ort and improve loading/error messages; update translations for English and Chinese
* test: increase test coverage

* test: increase test coverage
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 5, 2026

审阅者指南

实现了一个完整的用于助手推理内容的“思考”UI/流式管线(包括后端事件、持久化以及前端渲染),对 Agent Marketplace 详情视图进行了国际化,并进行了若干移动端/UI 细节打磨,同时添加了新的仓库层级测试和一次数据迁移。

从 LLM 到 ThinkingBubble UI 的思考内容流式传输时序图

sequenceDiagram
    actor User
    participant FrontendChat as FrontendChatUI
    participant Redux as ChatSlice
    participant WS as WebsocketClient
    participant Backend as ChatTask
    participant Lang as LangchainMessagesMode
    participant ThinkHandler as ThinkingEventHandler
    participant DB as MessageRepo

    User->>FrontendChat: Send user message
    FrontendChat->>WS: chat_request
    WS->>Backend: Forward request

    loop LLM streaming
        Backend->>Lang: Next message_chunk
        Lang->>ThinkHandler: extract_thinking_content(message_chunk)
        alt chunk has thinking_content
            ThinkHandler-->>Lang: thinking_content
            alt ctx.is_thinking is false
                Lang->>WS: THINKING_START(id)
                WS->>Redux: dispatch thinking_start
                Redux->>Redux: Create/convert assistant message
                Redux-->>FrontendChat: message with isThinking=true, thinkingContent=""
            end
            Lang->>WS: THINKING_CHUNK(id, content)
            WS->>Redux: dispatch thinking_chunk
            Redux->>Redux: Append to thinkingContent
            Redux-->>FrontendChat: Updated message.thinkingContent
            FrontendChat->>FrontendChat: Render ThinkingBubble(isThinking=true)
        else chunk has normal token
            alt ctx.is_thinking is true
                Lang->>WS: THINKING_END(id)
                WS->>Redux: dispatch thinking_end
                Redux->>Redux: Set isThinking=false
                Redux-->>FrontendChat: Updated message (thinking finished)
            end
            Lang->>WS: STREAMING_START / STREAMING_CHUNK
            WS->>Redux: dispatch streaming_start / streaming_chunk
            Redux-->>FrontendChat: Update message.content
            FrontendChat->>FrontendChat: Render main assistant content
        end
    end

    Backend->>DB: Persist Message(thinking_content, content)
    DB-->>Backend: Saved message
Loading

带有 thinking_content 字段的 Message 的 ER 图

erDiagram
    Topic {
        uuid id
        string title
    }

    Message {
        uuid id
        string role
        text content
        uuid topic_id
        text thinking_content
    }

    Topic ||--o{ Message : has
Loading

思考事件处理与消息模型的类图

classDiagram
    class StreamContext {
        string stream_id
        bool is_streaming
        int total_input_tokens
        int total_output_tokens
        int total_tokens
        bool is_thinking
        list~string~ thinking_buffer
    }

    class ThinkingEventHandler {
        +create_thinking_start(stream_id string) StreamingEvent
        +create_thinking_chunk(stream_id string, content string) StreamingEvent
        +create_thinking_end(stream_id string) StreamingEvent
        +extract_thinking_content(message_chunk any) string
    }

    class StreamingEventHandler {
        +create_streaming_start(stream_id string) StreamingEvent
        +create_streaming_chunk(stream_id string, content string) StreamingEvent
        +create_streaming_end(stream_id string) StreamingEvent
        +create_token_usage_event(input_tokens int, output_tokens int, total_tokens int) StreamingEvent
        +create_processing_event(status string) StreamingEvent
        +create_error_event(error string) StreamingEvent
    }

    class TokenStreamProcessor {
        +extract_token_text(message_chunk any) string
    }

    class ChatEventType {
        <<enumeration>>
        STREAMING_START
        STREAMING_CHUNK
        STREAMING_END
        THINKING_START
        THINKING_CHUNK
        THINKING_END
    }

    class MessageBase {
        uuid id
        string role
        string content
        uuid topic_id
        string thinking_content
    }

    class Message {
    }

    class MessageUpdate {
        string role
        string content
        string thinking_content
    }

    class MessageReadWithFilesAndCitations {
        string role
        string content
        uuid topic_id
        string thinking_content
    }

    class MessageRepo {
        +get_messages_with_files_and_citations(session Session, topic_id uuid) list~MessageReadWithFilesAndCitations~
    }

    class ChatSliceMessage {
        string id
        string role
        string content
        bool isThinking
        string thinkingContent
    }

    class ThinkingBubbleProps {
        string content
        bool isThinking
    }

    class ThinkingBubble {
        +ThinkingBubble(props ThinkingBubbleProps)
    }

    class ChatBubbleProps {
        ChatSliceMessage message
    }

    class ChatBubble {
        +ChatBubble(props ChatBubbleProps)
    }

    MessageBase <|-- Message
    MessageBase <|-- MessageReadWithFilesAndCitations
    MessageUpdate ..> Message : updates
    MessageRepo ..> MessageReadWithFilesAndCitations : returns

    ThinkingEventHandler ..> ChatEventType
    StreamingEventHandler ..> ChatEventType
    TokenStreamProcessor ..> StreamContext

    ChatSliceMessage --> ThinkingBubble : uses via props
    ChatBubble --> ChatSliceMessage : renders
    ChatBubble --> ThinkingBubble : composes
Loading

文件级变更

Change Details Files
为聊天中的 AI 思考/推理内容提供端到端的流式传输、存储和展示支持。
  • 扩展 Message 模型、schema、仓库和数据库(新迁移),为每条消息存储可选的 thinking_content。
  • 引入 THINKING_* 聊天事件和 ThinkingEventHandler,用于提取与提供方相关的推理内容并通过 SSE 进行流式传输。
  • 更新 langchain 流式管线以发出思考事件,在 StreamContext 中跟踪思考状态,并正确完成收尾。
  • 在 chatSlice 中处理 THINKING_* 事件,以管理 isThinking/thinkingContent 字段,并与现有的 streaming_start 逻辑集成。
  • 通过新的 ThinkingBubble 组件(包含激活和折叠状态)在 ChatBubble 中渲染思考内容。
service/app/models/message.py
service/migrations/versions/03630403f8c2_add_thinking_content_to_message.py
service/app/schemas/chat_events.py
service/app/schemas/chat_event_types.py
service/app/core/chat/stream_handlers.py
service/app/core/chat/langchain.py
service/app/tasks/chat.py
service/app/repos/message.py
web/src/store/types.ts
web/src/store/slices/chatSlice.ts
web/src/service/xyzenService.ts
web/src/components/layouts/components/ChatBubble.tsx
web/src/components/layouts/components/ThinkingBubble.tsx
对 Agent Marketplace 详情页及相关 UI 工具进行国际化并做轻微重构。
  • 在加载、错误、统计、标签页、需求和操作标签等位置,用 react-i18next 的 i18n 查找替换硬编码英文文案。
  • 在 README 标签页上,将 ReactMarkdown/remark 导入替换为共享的 Markdown 组件,并调整布局以更好地处理溢出。
  • 微调布局类,以在移动端/溢出场景下表现更好,并调整渐变以使用 bg-linear-to-b 工具类。
  • 将操作列设置为 sticky,同时仅将内部卡片设为非 sticky。
web/src/app/marketplace/AgentMarketplaceDetail.tsx
web/src/i18n/locales/en/translation.json
web/src/i18n/locales/zh/translation.json
打磨多处 UI 组件,使其使用一致的渐变工具类并进行轻微布局调整。
  • 在签到日历卡片与消息附件弹窗头部中,将 bg-gradient-to-b 替换为 bg-linear-to-b。
  • 将 3D pin 透视光束的渐变更新为使用 bg-linear-to-b,以保持一致性。
  • 调整部分容器类(min-w-0、overflow-x-auto 等),以避免在小屏幕上出现溢出问题。
web/src/components/features/CheckInCalendar.tsx
web/src/components/ui/3d-pin.tsx
web/src/components/layouts/components/MessageAttachments.tsx
web/src/app/marketplace/AgentMarketplaceDetail.tsx
为文件、消息、话题和会话仓库添加或重构仓库测试和工厂。
  • 为 File、Message、Session 和 Topic 实体及其创建 schema 引入基于 polyfactory 的工厂。
  • 为 FileRepository 添加集成测试,覆盖 CRUD、软/硬删除、哈希以及批量操作。
  • 为 MessageRepository、TopicRepository 和 SessionRepository 添加集成测试,覆盖创建、列表、排序、删除以及像 thinking_content 这样的特殊字段。
  • 为 ThinkingEventHandler 添加单元测试,涵盖事件创建和与提供方相关的思考内容提取路径。
service/tests/factories/file.py
service/tests/factories/message.py
service/tests/factories/session.py
service/tests/factories/topic.py
service/tests/integration/test_repo/test_file_repo.py
service/tests/integration/test_repo/test_message_repo.py
service/tests/integration/test_repo/test_topic_repo.py
service/tests/integration/test_repo/test_session_repo.py
service/tests/unit/test_core/test_thinking_events.py
更新前端依赖以反映已移除的使用场景。
  • 从 web/package.json 的依赖中移除未使用的 react-image-crop 和 react-use-websocket。
  • 保留与 markdown 相关的依赖,因为 Markdown 组件仍然依赖它们。
web/package.json
web/yarn.lock

Tips and commands

与 Sourcery 交互

  • 触发新的审查: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 通过回复审查评论,请求 Sourcery 从该评论创建一个 issue。你也可以在审查评论下回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文的任意位置写上 @sourcery-ai summary,即可在你想要的位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理了所有评论且不想再看到它们,这将很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。尤其适用于你想从头开始新的审查的情况——别忘了再评论 @sourcery-ai review 来触发新的审查!

自定义你的体验

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审阅者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

Implements a full “thinking” UI/streaming pipeline for assistant reasoning content (including backend events, persistence, and frontend rendering), internationalizes the agent marketplace detail view, and makes several mobile/UI polish changes plus new repository-level tests and a migration.

Sequence diagram for thinking content streaming from LLM to ThinkingBubble UI

sequenceDiagram
    actor User
    participant FrontendChat as FrontendChatUI
    participant Redux as ChatSlice
    participant WS as WebsocketClient
    participant Backend as ChatTask
    participant Lang as LangchainMessagesMode
    participant ThinkHandler as ThinkingEventHandler
    participant DB as MessageRepo

    User->>FrontendChat: Send user message
    FrontendChat->>WS: chat_request
    WS->>Backend: Forward request

    loop LLM streaming
        Backend->>Lang: Next message_chunk
        Lang->>ThinkHandler: extract_thinking_content(message_chunk)
        alt chunk has thinking_content
            ThinkHandler-->>Lang: thinking_content
            alt ctx.is_thinking is false
                Lang->>WS: THINKING_START(id)
                WS->>Redux: dispatch thinking_start
                Redux->>Redux: Create/convert assistant message
                Redux-->>FrontendChat: message with isThinking=true, thinkingContent=""
            end
            Lang->>WS: THINKING_CHUNK(id, content)
            WS->>Redux: dispatch thinking_chunk
            Redux->>Redux: Append to thinkingContent
            Redux-->>FrontendChat: Updated message.thinkingContent
            FrontendChat->>FrontendChat: Render ThinkingBubble(isThinking=true)
        else chunk has normal token
            alt ctx.is_thinking is true
                Lang->>WS: THINKING_END(id)
                WS->>Redux: dispatch thinking_end
                Redux->>Redux: Set isThinking=false
                Redux-->>FrontendChat: Updated message (thinking finished)
            end
            Lang->>WS: STREAMING_START / STREAMING_CHUNK
            WS->>Redux: dispatch streaming_start / streaming_chunk
            Redux-->>FrontendChat: Update message.content
            FrontendChat->>FrontendChat: Render main assistant content
        end
    end

    Backend->>DB: Persist Message(thinking_content, content)
    DB-->>Backend: Saved message
Loading

ER diagram for Message with thinking_content field

erDiagram
    Topic {
        uuid id
        string title
    }

    Message {
        uuid id
        string role
        text content
        uuid topic_id
        text thinking_content
    }

    Topic ||--o{ Message : has
Loading

Class diagram for thinking event handling and message model

classDiagram
    class StreamContext {
        string stream_id
        bool is_streaming
        int total_input_tokens
        int total_output_tokens
        int total_tokens
        bool is_thinking
        list~string~ thinking_buffer
    }

    class ThinkingEventHandler {
        +create_thinking_start(stream_id string) StreamingEvent
        +create_thinking_chunk(stream_id string, content string) StreamingEvent
        +create_thinking_end(stream_id string) StreamingEvent
        +extract_thinking_content(message_chunk any) string
    }

    class StreamingEventHandler {
        +create_streaming_start(stream_id string) StreamingEvent
        +create_streaming_chunk(stream_id string, content string) StreamingEvent
        +create_streaming_end(stream_id string) StreamingEvent
        +create_token_usage_event(input_tokens int, output_tokens int, total_tokens int) StreamingEvent
        +create_processing_event(status string) StreamingEvent
        +create_error_event(error string) StreamingEvent
    }

    class TokenStreamProcessor {
        +extract_token_text(message_chunk any) string
    }

    class ChatEventType {
        <<enumeration>>
        STREAMING_START
        STREAMING_CHUNK
        STREAMING_END
        THINKING_START
        THINKING_CHUNK
        THINKING_END
    }

    class MessageBase {
        uuid id
        string role
        string content
        uuid topic_id
        string thinking_content
    }

    class Message {
    }

    class MessageUpdate {
        string role
        string content
        string thinking_content
    }

    class MessageReadWithFilesAndCitations {
        string role
        string content
        uuid topic_id
        string thinking_content
    }

    class MessageRepo {
        +get_messages_with_files_and_citations(session Session, topic_id uuid) list~MessageReadWithFilesAndCitations~
    }

    class ChatSliceMessage {
        string id
        string role
        string content
        bool isThinking
        string thinkingContent
    }

    class ThinkingBubbleProps {
        string content
        bool isThinking
    }

    class ThinkingBubble {
        +ThinkingBubble(props ThinkingBubbleProps)
    }

    class ChatBubbleProps {
        ChatSliceMessage message
    }

    class ChatBubble {
        +ChatBubble(props ChatBubbleProps)
    }

    MessageBase <|-- Message
    MessageBase <|-- MessageReadWithFilesAndCitations
    MessageUpdate ..> Message : updates
    MessageRepo ..> MessageReadWithFilesAndCitations : returns

    ThinkingEventHandler ..> ChatEventType
    StreamingEventHandler ..> ChatEventType
    TokenStreamProcessor ..> StreamContext

    ChatSliceMessage --> ThinkingBubble : uses via props
    ChatBubble --> ChatSliceMessage : renders
    ChatBubble --> ThinkingBubble : composes
Loading

File-Level Changes

Change Details Files
Add end-to-end support for streaming, storing, and displaying AI thinking/reasoning content in chat.
  • Extend Message model, schema, repo, and DB (new migration) to store optional thinking_content per message.
  • Introduce THINKING_* chat events and ThinkingEventHandler to extract provider-specific reasoning content and stream it over SSE.
  • Update langchain streaming pipeline to emit thinking events, track thinking state in StreamContext, and finalize correctly.
  • Handle THINKING_* events in chatSlice to manage isThinking/thinkingContent fields and integrate with existing streaming_start logic.
  • Render thinking content in ChatBubble via new ThinkingBubble component with active and collapsed states.
service/app/models/message.py
service/migrations/versions/03630403f8c2_add_thinking_content_to_message.py
service/app/schemas/chat_events.py
service/app/schemas/chat_event_types.py
service/app/core/chat/stream_handlers.py
service/app/core/chat/langchain.py
service/app/tasks/chat.py
service/app/repos/message.py
web/src/store/types.ts
web/src/store/slices/chatSlice.ts
web/src/service/xyzenService.ts
web/src/components/layouts/components/ChatBubble.tsx
web/src/components/layouts/components/ThinkingBubble.tsx
Internationalize and slightly refactor the Agent Marketplace detail page and related UI utilities.
  • Replace hardcoded English strings with i18n lookups using react-i18next across loading, error, stats, tabs, requirements, and action labels.
  • Swap ReactMarkdown/remark imports for a shared Markdown component on the README tab and adjust layout for better overflow handling.
  • Tweak layout classes for better mobile/overflow behavior and adjust gradients to use bg-linear-to-b utility.
  • Make the action column sticky while making only the card inside non-sticky.
web/src/app/marketplace/AgentMarketplaceDetail.tsx
web/src/i18n/locales/en/translation.json
web/src/i18n/locales/zh/translation.json
Polish various UI components to use consistent gradient utilities and minor layout tweaks.
  • Replace bg-gradient-to-b usage with bg-linear-to-b in check-in calendar cards and message attachments modal header.
  • Update 3D pin perspective beam gradients to use bg-linear-to-b for consistency.
  • Adjust some container classes (min-w-0, overflow-x-auto, etc.) to avoid overflow issues on smaller screens.
web/src/components/features/CheckInCalendar.tsx
web/src/components/ui/3d-pin.tsx
web/src/components/layouts/components/MessageAttachments.tsx
web/src/app/marketplace/AgentMarketplaceDetail.tsx
Add or refactor repository tests and factories for file, message, topic, and session repos.
  • Introduce polyfactory-based factories for File, Message, Session, and Topic entities and their create schemas.
  • Add integration tests for FileRepository covering CRUD, soft/hard delete, hashing, and bulk operations.
  • Add integration tests for MessageRepository, TopicRepository, and SessionRepository covering creation, listing, ordering, deletion, and special fields like thinking_content.
  • Add unit tests for ThinkingEventHandler including event creation and provider-specific thinking content extraction paths.
service/tests/factories/file.py
service/tests/factories/message.py
service/tests/factories/session.py
service/tests/factories/topic.py
service/tests/integration/test_repo/test_file_repo.py
service/tests/integration/test_repo/test_message_repo.py
service/tests/integration/test_repo/test_topic_repo.py
service/tests/integration/test_repo/test_session_repo.py
service/tests/unit/test_core/test_thinking_events.py
Update frontend dependencies to reflect removed usages.
  • Drop unused react-image-crop and react-use-websocket from web/package.json dependencies.
  • Leave markdown-related deps in place since Markdown component still relies on them.
web/package.json
web/yarn.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Mile-Away Mile-Away merged commit ced9160 into main Jan 5, 2026
8 of 10 checks passed
@Mile-Away Mile-Away deleted the test branch January 5, 2026 04:59
@codecov
Copy link

codecov bot commented Jan 5, 2026

Codecov Report

❌ Patch coverage is 57.65766% with 47 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
service/app/core/chat/langchain.py 0.00% 17 Missing ⚠️
service/app/core/chat/stream_handlers.py 74.13% 15 Missing ⚠️
service/app/tasks/chat.py 0.00% 15 Missing ⚠️

📢 Thoughts on this report? Let us know!

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 - 我发现了 9 个问题,并留下了一些总体反馈:

  • 有几处 Tailwind 类从 bg-gradient-to-b 改成了 bg-linear-to-b;除非你为 bg-linear-to-* 配置了自定义工具类,否则这些类不会渲染渐变效果,因此通常应该保持为 bg-gradient-to-b
  • chatSlicethinking_start 处理函数中,你通过 thinking-${Date.now()} 来生成 clientId,而不是使用 generateClientId();建议使用现有的辅助方法以保持一致性,并避免在短时间内连续发送消息时可能出现的 ID 冲突。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- 有几处 Tailwind 类从 `bg-gradient-to-b` 改成了 `bg-linear-to-b`;除非你为 `bg-linear-to-*` 配置了自定义工具类,否则这些类不会渲染渐变效果,因此通常应该保持为 `bg-gradient-to-b`-`chatSlice``thinking_start` 处理函数中,你通过 `thinking-${Date.now()}` 来生成 `clientId`,而不是使用 `generateClientId()`;建议使用现有的辅助方法以保持一致性,并避免在短时间内连续发送消息时可能出现的 ID 冲突。

## Individual Comments

### Comment 1
<location> `service/app/tasks/chat.py:318-319` </location>
<code_context>
                     db.add(ai_message_obj)

+                # Update thinking content
+                if full_thinking_content:
+                    ai_message_obj.thinking_content = full_thinking_content
+                    db.add(ai_message_obj)
+
</code_context>

<issue_to_address>
**issue (bug_risk):** 在从未创建过 assistant 消息对象的情况下,应当在持久化 thinking_content 前增加保护。

在 finalize 代码块中,`ai_message_obj` 在未检查是否为 `None` 的情况下被使用:

```py
if full_thinking_content:
    ai_message_obj.thinking_content = full_thinking_content
    db.add(ai_message_obj)
```

如果从未有 `StreamingStart`/`ThinkingStart` 创建过 `ai_message_obj`(例如出现 `THINKING_CHUNK` 时之前没有 `THINKING_START`,或者在创建消息之前发生错误),这里会抛异常。可以参考当前对 `full_content` 的保护逻辑,只在对象存在时更新,例如:

```py
if ai_message_obj and full_thinking_content:
    ai_message_obj.thinking_content = full_thinking_content
    db.add(ai_message_obj)
```

或者,确保在处理任何 thinking 事件之前总是会创建 `ai_message_obj`。
</issue_to_address>

### Comment 2
<location> `service/app/core/chat/stream_handlers.py:158-167` </location>
<code_context>
+class ThinkingEventHandler:
</code_context>

<issue_to_address>
**suggestion:** 处理同时包含 thinking 内容和普通内容的 chunk,避免丢失文本 token。

在 `ThinkingEventHandler.extract_thinking_content``_handle_messages_mode` 中,只要 chunk 含有 `thinking_content` 就会提前 `return`,因此只会发出 `THINKING_*` 事件:

```py
thinking_content = ThinkingEventHandler.extract_thinking_content(message_chunk)

if thinking_content:
    ...
    yield ThinkingEventHandler.create_thinking_chunk(...)
    return
```

如果某些 provider 在同一个 chunk 中同时发送推理内容和可见文本,那么本应由 `TokenStreamProcessor.extract_token_text` 产生的用户可见 token 会被丢弃。为避免这种情况,可以考虑从同一个 chunk 中同时提取 thinking 和普通文本,并在两者都存在时同时发出两种事件类型——例如让 `extract_thinking_content` 同时返回 `thinking``visible_text`,或者让 `_handle_messages_mode` 只遍历一次 chunk,然后决定如何在不提前返回的情况下同时发出 thinking 和 streaming 事件。
</issue_to_address>

### Comment 3
<location> `service/app/core/chat/stream_handlers.py:63` </location>
<code_context>
+class StreamContext:
</code_context>

<issue_to_address>
**suggestion:** 移除或使用 `thinking_buffer`,避免未使用的状态造成潜在困惑。

`thinking_buffer``StreamContext` 中被定义,但在当前流程中从未被读取,目前只使用了 `ctx.is_thinking` 并直接向外流式输出 chunk。如果你计划之后聚合推理内容,建议显式消费 `thinking_buffer`(例如在 `_finalize_streaming` 中),否则可以先移除它,以保持 `StreamContext` 精简,并避免暗示目前不存在的行为。
</issue_to_address>

### Comment 4
<location> `service/tests/unit/test_core/test_thinking_events.py:52` </location>
<code_context>
+        assert event["data"]["id"] == "stream_123"
+
+
+class TestExtractThinkingContent:
+    """Tests for ThinkingEventHandler.extract_thinking_content method."""
+
</code_context>

<issue_to_address>
**suggestion (testing):**`extract_thinking_content` 支持的所有 provider 格式添加测试用例(例如 Gemini / 通用 `reasoning` 字段以及 `thought` 块)。

目前的测试没有覆盖 `extract_thinking_content` 支持的所有结构,例如带有 `type="thought"` 且包含 `thought`/`text` 字段的内容块(Gemini 风格),以及 `response_metadata` 中的 `reasoning``thoughts` 等键。请为这些情况添加参数化测试,例如内容列表包含 `{"type": "thought", "thought": "..."}`,以及 `response_metadata={"reasoning": "..."}` / `{"thoughts": "..."}` 的示例,以更好地锁定预期行为。

Suggested implementation:

```python
import pytest

from app.core.chat.stream_handlers import ThinkingEventHandler
from app.schemas.chat_events import ChatEventType

```

```python
class TestExtractThinkingContent:
    """Tests for ThinkingEventHandler.extract_thinking_content method."""

    @pytest.mark.parametrize(
        "content,response_metadata,expected",
        [
            (
                # Gemini-style content block with thought/text fields
                [
                    {
                        "type": "thought",
                        "thought": "Let me think this through.",
                        "text": "Let me think this through.",
                    }
                ],
                {},
                "Let me think this through.",
            ),
            (
                # Generic reasoning key from response metadata
                [],
                {"reasoning": "Reasoning from provider metadata."},
                "Reasoning from provider metadata.",
            ),
            (
                # Generic thoughts key from response metadata
                [],
                {"thoughts": "Thoughts from provider metadata."},
                "Thoughts from provider metadata.",
            ),
        ],
    )
    def test_extracts_thinking_from_various_provider_formats(
        self, content, response_metadata, expected
    ) -> None:
        """Ensure extract_thinking_content supports all provider shapes."""
        chunk = MockMessageChunk()
        # Attach content / metadata in the same shape the handler expects.
        chunk.content = content
        chunk.response_metadata = response_metadata

        assert ThinkingEventHandler.extract_thinking_content(chunk) == expected


class MockMessageChunk:
    """Mock message chunk for testing thinking content extraction."""

```
</issue_to_address>

### Comment 5
<location> `service/tests/unit/test_core/test_thinking_events.py:12` </location>
<code_context>
+from app.schemas.chat_events import ChatEventType
+
+
+class MockMessageChunk:
+    """Mock message chunk for testing thinking content extraction."""
+
</code_context>

<issue_to_address>
**suggestion (testing):** 扩展 mock 和测试,验证 `content` 中的非字典元素会被 `extract_thinking_content` 安全忽略。

由于 `extract_thinking_content` 在遍历 `message_chunk.content` 时会用 `isinstance(block, dict)` 做保护,请增加一个测试,其中 `content` 同时包含字典和非字典元素(例如字符串、数字)。这样可以锁定当前的防御性行为,并防止上游 provider 将来更改 payload 结构时引入问题。

Suggested implementation:

```python
class MockMessageChunk:
    """Mock message chunk for testing thinking content extraction."""

    def __init__(self, content):
        # Simulate the shape of provider message chunks we care about for tests
        self.content = content


def test_extract_thinking_content_ignores_non_dict_blocks():
    """Non-dict items in content should be safely ignored by extract_thinking_content."""
    handler = ThinkingEventHandler(callback=lambda *_, **__: None)

    chunk = MockMessageChunk(
        content=[
            # Valid thinking block
            {"type": "thinking", "thinking": "valid thinking content"},
            # Various non-dict items that should be ignored
            "plain string block",
            42,
            None,
            3.14,
            # Dict that is not a thinking block and should also be ignored
            {"type": "not_thinking", "thinking": "should not be emitted"},
        ]
    )

    thinking_content = handler.extract_thinking_content(chunk)

    # We only assert that the valid thinking content from the dict block is present,
    # and the presence of non-dict elements does not cause errors or pollution.
    # This keeps the test robust to the exact return type (str vs list of str).
    assert "valid thinking content" in thinking_content

```

这一变更的假设前提是:
1. `ThinkingEventHandler` 的构造函数签名为 `__init__(callback=...)`,并且有一个公开的 `extract_thinking_content` 方法,接收一个具有 `content` 属性的对象。
2. `extract_thinking_content` 返回的容器支持 `in` 运算符(例如字符串或字符串列表)。

如果 handler 的构造函数或方法名不同(例如叫 `_extract_thinking_content` 或需要额外初始化参数),请相应更新:
- `handler = ThinkingEventHandler(...)` 这一行以匹配实际初始化方式;
- `handler.extract_thinking_content(chunk)` 调用对应的方法名。

如果你在其他测试中使用的具体 `MockMessageChunk` 需要更多属性(例如 `id``model``provider`),请把这些属性添加到 `__init__` 中并在测试实例化时设置,以保持与其他测试构造 message chunk 的方式一致。
</issue_to_address>

### Comment 6
<location> `service/tests/integration/test_repo/test_message_repo.py:136-145` </location>
<code_context>
+        )
+        assert created.role == role
+
+    async def test_create_message_with_thinking_content(self, message_repo: MessageRepository, test_topic: Topic):
+        """Test creating a message with thinking content (AI reasoning)."""
+        created = await message_repo.create_message(
+            MessageCreateFactory.build(
+                topic_id=test_topic.id,
+                role="assistant",
+                content="Final answer",
+                thinking_content="Let me think about this...",
+            )
+        )
+        assert created.thinking_content == "Let me think about this..."
</code_context>

<issue_to_address>
**suggestion (testing):** 除了直接调用 `create_message`,也可以考虑增加一个通过 streaming pipeline 持久化 `thinking_content` 的集成测试。

当前测试覆盖了直接调用 `create_message` 的场景,但大部分写入会来自流式路径(`_process_chat_message_async` 处理 `THINKING_*` 事件并更新 `Message`)。增加一个集成风格的测试,通过完整驱动 `THINKING_START``THINKING_CHUNK``THINKING_END` 序列,然后断言持久化后的 `Message.thinking_content`,可以更好地验证端到端行为。

Suggested implementation:

```python
    async def test_create_message_with_thinking_content(self, message_repo: MessageRepository, test_topic: Topic):
        """Test creating a message with thinking content (AI reasoning)."""
        created = await message_repo.create_message(
            MessageCreateFactory.build(
                topic_id=test_topic.id,
                role="assistant",
                content="Final answer",
                thinking_content="Let me think about this...",
            )
        )
        assert created.thinking_content == "Let me think about this..."

    async def test_thinking_content_persisted_via_streaming_pipeline(
        self,
        message_repo: MessageRepository,
        test_topic: Topic,
        streaming_chat_service,
    ):
        """End-to-end test: THINKING_* events persist Message.thinking_content via the streaming pipeline."""
        # Arrange: create an assistant message that will receive thinking updates
        base_message = await message_repo.create_message(
            MessageCreateFactory.build(
                topic_id=test_topic.id,
                role="assistant",
                content="Final answer",
            )
        )

        message_id = base_message.id

        # Act: drive a THINKING_START -> THINKING_CHUNK* -> THINKING_END sequence through the pipeline
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_START",
                "message_id": str(message_id),
                "content": "",
            }
        )
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_CHUNK",
                "message_id": str(message_id),
                "content": "Let me think",
            }
        )
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_CHUNK",
                "message_id": str(message_id),
                "content": " about this...",
            }
        )
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_END",
                "message_id": str(message_id),
            }
        )

        # Assert: the persisted message has the full accumulated thinking_content
        updated = await message_repo.get_message(message_id=message_id)
        assert updated.thinking_content == "Let me think about this..."

```

要让此测试完全融入你现有的代码库,可能需要:

1. 确保在该测试模块(或某个 conftest)中有一个 `streaming_chat_service` fixture,可对外暴露 `_process_chat_message_async` 并驱动与生产环境相同的 pipeline。如果名称或 API 不同,请相应更新 fixture 参数名和调用方式。
2. 将事件 payload 的结构与 `_process_chat_message_async``THINKING_START``THINKING_CHUNK``THINKING_END` 事件的预期保持一致(例如使用 `event_type` 而不是 `type``message_id` vs `id`,或嵌套的 payload 对象等)。
3. 如果 `MessageRepository.get_message` 的签名不同(例如叫 `get``get_by_id`),请调整相应的获取调用。
4. 如果诸如 `_process_chat_message_async` 的内部方法不希望直接在测试中被调用,可以改为通过最终会调用它的公共 API(比如某个 streaming handler 或 service 方法)来驱动相同的处理流程。
</issue_to_address>

### Comment 7
<location> `service/tests/integration/test_repo/test_session_repo.py:49-60` </location>
<code_context>
+        for session in sessions:
+            assert session.user_id == user_id
+
+    async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
+        """Test fetching session by user and agent combination."""
+        user_id = "test-user-session-agent"
+        session_create = SessionCreateFactory.build(agent_id=None)
+
+        await session_repo.create_session(session_create, user_id)
+
+        # Find session with no agent
+        found = await session_repo.get_session_by_user_and_agent(user_id, None)
+        assert found is not None
+        assert found.user_id == user_id
+        assert found.agent_id is None
+
+    async def test_update_session(self, session_repo: SessionRepository):
</code_context>

<issue_to_address>
**suggestion (testing):** 增加一个带特定 `agent_id` 的 session 被检索到的测试用例,而不仅仅是 `None` 的情况。

当前测试只覆盖了 `agent_id=None` 的场景。请同时创建一个 `agent_id` 非空的 session,并验证:在使用匹配的 `agent_id` 查询时能够返回该 session,而在使用不同的 `agent_id` 查询时不会返回它。

```suggestion
    async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
        """Test fetching session by user and agent combination."""
        user_id = "test-user-session-agent"

        # Session with no agent
        session_no_agent = SessionCreateFactory.build(agent_id=None)
        await session_repo.create_session(session_no_agent, user_id)

        # Session with a specific agent
        agent_id = "test-agent-id"
        session_with_agent = SessionCreateFactory.build(agent_id=agent_id)
        await session_repo.create_session(session_with_agent, user_id)

        # Find session with no agent
        found = await session_repo.get_session_by_user_and_agent(user_id, None)
        assert found is not None
        assert found.user_id == user_id
        assert found.agent_id is None

        # Find session with specific agent_id
        found_with_agent = await session_repo.get_session_by_user_and_agent(user_id, agent_id)
        assert found_with_agent is not None
        assert found_with_agent.user_id == user_id
        assert found_with_agent.agent_id == agent_id

        # Query with a different agent_id should not return the session
        found_wrong_agent = await session_repo.get_session_by_user_and_agent(user_id, "different-agent-id")
        assert found_wrong_agent is None
```
</issue_to_address>

### Comment 8
<location> `service/tests/unit/test_core/test_thinking_events.py:124-133` </location>
<code_context>
+
+        assert result is None
+
+    def test_deepseek_takes_priority_over_response_metadata(self) -> None:
+        """DeepSeek additional_kwargs should be checked first."""
+        chunk = MockMessageChunk(
+            content="",
+            additional_kwargs={"reasoning_content": "From additional_kwargs"},
+            response_metadata={"thinking": "From response_metadata"},
+        )
+
+        result = ThinkingEventHandler.extract_thinking_content(chunk)
+
+        assert result == "From additional_kwargs"
+
+    def test_handles_missing_attributes_gracefully(self) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** 可以考虑添加一个 regression 测试,用于验证 thinking 内容与常规 token 流式输出之间的优先级。

当前测试只验证了不同 thinking 来源之间的优先级;并未断言一旦某个 chunk 存在 thinking 内容,在 `_handle_messages_mode` 设计中,常规 token 的抽取会被抑制。增加一个聚焦于 `_handle_messages_mode``StreamContext` 的单元测试——向其提供一系列 chunk(先 thinking,后普通内容),并断言 `THINKING_*``STREAMING_*` 事件的顺序——可以更好地锁定这种行为。为保持本文件聚焦于 handler 逻辑,也可以将该测试放到单独的测试模块中。

Suggested implementation:

```python
        chunk = MinimalChunk()

        result = ThinkingEventHandler.extract_thinking_content(chunk)

        assert result is None

    def test_thinking_content_takes_precedence_over_token_streaming(self) -> None:
        """
        Once thinking content is present for a chunk, regular token streaming
        should be suppressed for that chunk in messages mode.
        """
        # Arrange: mock stream context to capture event calls in order
        stream_context = MagicMock()

        handler = ThinkingEventHandler(stream_context=stream_context)

        # First chunk contains thinking content only
        thinking_chunk = MockMessageChunk(
            content="",
            additional_kwargs={"reasoning_content": "thoughts-1"},
        )

        # Second chunk is normal content-only streaming
        content_chunk = MockMessageChunk(
            content="hello",
            additional_kwargs={},
        )

        # Act: feed both chunks through messages mode handling
        handler._handle_messages_mode([thinking_chunk, content_chunk])

        # Assert: thinking event is emitted before any streaming tokens,
        # and the thinking chunk does not produce streaming content.
        all_calls = stream_context.method_calls

        # We expect at least one thinking-related event and one streaming-related event.
        # The exact method names depend on StreamContext, but the thinking event
        # must be observed before any streaming/tokens event.
        thinking_call_index = None
        streaming_call_index = None

        for idx, call in enumerate(all_calls):
            name = call[0]

            if thinking_call_index is None and "thinking" in name.lower():
                thinking_call_index = idx
            if streaming_call_index is None and (
                "token" in name.lower()
                or "delta" in name.lower()
                or "stream" in name.lower()
            ):
                streaming_call_index = idx

        # Both types of events should be present
        assert thinking_call_index is not None
        assert streaming_call_index is not None

        # Thinking event must be emitted first
        assert thinking_call_index < streaming_call_index

```

1. 确保在该测试模块顶部包含必要的导入:
   - `from unittest.mock import MagicMock`
   - `from service.core.thinking_events import ThinkingEventHandler`(或 `ThinkingEventHandler` 实际定义所在的正确路径)。
   - `from service.tests.unit.test_core.test_thinking_events import MockMessageChunk`,或根据 `MockMessageChunk` 的实际位置进行调整。
2. 该测试假设:
   - `ThinkingEventHandler` 的构造函数接受 `stream_context` 关键字参数。如果初始化方式不同,请相应修改 handler 的构造方式。
   - `_handle_messages_mode` 接收一个可迭代的 chunk。如果真实签名不同(例如一次只处理一个 chunk,或返回事件而不是直接修改 `stream_context`),请按实际情况调整测试中的调用。
3. 当前测试通过对 `stream_context` 方法名进行字符串匹配来区分 “thinking” 与 “streaming” 事件。如果你的 `StreamContext` 有明确的方法或事件类型(例如 `on_thinking_delta``on_thinking_complete``on_stream_delta``on_stream_complete`),可以用更精确的断言替代上述启发式循环,例如:
   - `stream_context.on_thinking_delta.assert_called_once_with(...)`
   - `stream_context.on_stream_delta.assert_called_once_with(...)`
   - 并直接基于这些方法的 `mock_calls` 顺序进行断言。
4. 如果 `_handle_messages_mode` 无法直接访问(例如它是一个不打算被测试使用的私有辅助函数),可以考虑:
   - 直接从其模块导入进行测试,或者
   - 通过调用委托到 `_handle_messages_mode` 的公共 streaming API 来驱动相同的行为,同时保持对 `stream_context` 事件顺序的相同断言。
</issue_to_address>

### Comment 9
<location> `service/app/core/chat/stream_handlers.py:180` </location>
<code_context>
+        return {"type": ChatEventType.THINKING_END, "data": data}
+
+    @staticmethod
+    def extract_thinking_content(message_chunk: Any) -> str | None:
+        """
+        Extract thinking/reasoning content from message chunk.
</code_context>

<issue_to_address>
**issue (complexity):** 可以考虑将 `extract_thinking_content` 重构为若干按 provider 划分的小型辅助方法,使主函数变成多个清晰策略的简单编排。

你可以保持新行为不变,但通过将 provider 相关逻辑拆分到一些小的私有辅助方法中,并收紧整体流程,来降低 `ThinkingEventHandler.extract_thinking_content` 的复杂度。这样既不改变公开 API,又能让未来为新 provider 扩展逻辑更加容易。

例如:

```python
class ThinkingEventHandler:
    ...

    @staticmethod
    def extract_thinking_content(message_chunk: Any) -> str | None:
        """Extract thinking/reasoning content from message chunk."""
        thinking = (
            ThinkingEventHandler._from_additional_kwargs(message_chunk)
            or ThinkingEventHandler._from_content_blocks(message_chunk)
            or ThinkingEventHandler._from_response_metadata(message_chunk)
        )
        return thinking

    @staticmethod
    def _from_additional_kwargs(message_chunk: Any) -> str | None:
        if not hasattr(message_chunk, "additional_kwargs"):
            return None
        additional_kwargs = message_chunk.additional_kwargs
        if not isinstance(additional_kwargs, dict):
            return None

        reasoning = additional_kwargs.get("reasoning_content")
        if reasoning:
            logger.debug("Found thinking in additional_kwargs.reasoning_content")
        return reasoning

    @staticmethod
    def _from_content_blocks(message_chunk: Any) -> str | None:
        if not hasattr(message_chunk, "content"):
            return None
        content = message_chunk.content
        if not isinstance(content, list):
            return None

        for block in content:
            if not isinstance(block, dict):
                continue
            block_type = block.get("type", "")

            if block_type == "thinking":
                thinking_text = block.get("thinking", "")
                if thinking_text:
                    logger.debug("Found thinking in content block type='thinking'")
                    return thinking_text

            if block_type == "thought":
                thought_text = block.get("thought", "") or block.get("text", "")
                if thought_text:
                    logger.debug("Found thinking in content block type='thought'")
                    return thought_text
        return None

    @staticmethod
    def _from_response_metadata(message_chunk: Any) -> str | None:
        if not hasattr(message_chunk, "response_metadata"):
            return None
        metadata = message_chunk.response_metadata
        if not isinstance(metadata, dict):
            return None

        thinking = (
            metadata.get("thinking")
            or metadata.get("reasoning_content")
            or metadata.get("reasoning")
            or metadata.get("thoughts")
        )
        if thinking:
            logger.debug("Found thinking in response_metadata: %s", list(metadata.keys()))
        return thinking
```

好处:

- 每个辅助方法各司其职(额外参数 / 内容块 / metadata)。
- 为新 provider 增加路径时,只需做局部的小改动(例如新增 `_from_groq_metadata`,或扩展 `_from_content_blocks`)。
- 顶层的 `extract_thinking_content` 从命令式的大型 if/elif 变成清晰的策略编排逻辑。

如果你觉得 `Any` 类型过于宽松,后续可以为 `message_chunk` 定义一个轻量级的协议(protocol),包含可选属性(`additional_kwargs``content``response_metadata`),再将其用作参数类型,而不需要修改调用方。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English

Hey - I've found 9 issues, and left some high level feedback:

  • Several Tailwind classes were changed from bg-gradient-to-b to bg-linear-to-b; unless you have a custom utility configured for bg-linear-to-*, these will not render gradients and should likely remain bg-gradient-to-b.
  • In the thinking_start handler in chatSlice, you generate a clientId with thinking-${Date.now()} instead of generateClientId(); consider using the existing helper for consistency and to avoid potential collisions across fast successive messages.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several Tailwind classes were changed from `bg-gradient-to-b` to `bg-linear-to-b`; unless you have a custom utility configured for `bg-linear-to-*`, these will not render gradients and should likely remain `bg-gradient-to-b`.
- In the `thinking_start` handler in `chatSlice`, you generate a `clientId` with `thinking-${Date.now()}` instead of `generateClientId()`; consider using the existing helper for consistency and to avoid potential collisions across fast successive messages.

## Individual Comments

### Comment 1
<location> `service/app/tasks/chat.py:318-319` </location>
<code_context>
                     db.add(ai_message_obj)

+                # Update thinking content
+                if full_thinking_content:
+                    ai_message_obj.thinking_content = full_thinking_content
+                    db.add(ai_message_obj)
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard thinking_content persistence when no assistant message was ever created.

In the finalize block, `ai_message_obj` is used without checking it’s not `None`:

```py
if full_thinking_content:
    ai_message_obj.thinking_content = full_thinking_content
    db.add(ai_message_obj)
```

If no `StreamingStart`/`ThinkingStart` ever created `ai_message_obj` (e.g., a `THINKING_CHUNK` arrives without a prior `THINKING_START`, or an error occurs before message creation), this will raise. Mirror the existing guard used for `full_content` and only update when the object exists, e.g.:

```py
if ai_message_obj and full_thinking_content:
    ai_message_obj.thinking_content = full_thinking_content
    db.add(ai_message_obj)
```

Alternatively, guarantee `ai_message_obj` is always created before any thinking events are processed.
</issue_to_address>

### Comment 2
<location> `service/app/core/chat/stream_handlers.py:158-167` </location>
<code_context>
+class ThinkingEventHandler:
</code_context>

<issue_to_address>
**suggestion:** Handle chunks that contain both thinking and normal content to avoid dropping text tokens.

In `ThinkingEventHandler.extract_thinking_content` and `_handle_messages_mode`, any chunk with `thinking_content` causes an early `return`, so only `THINKING_*` events are emitted:

```py
thinking_content = ThinkingEventHandler.extract_thinking_content(message_chunk)

if thinking_content:
    ...
    yield ThinkingEventHandler.create_thinking_chunk(...)
    return
```

If a provider sends both reasoning and visible text in the same chunk, this will drop the user-visible tokens that would normally come from `TokenStreamProcessor.extract_token_text`. To avoid this, consider extracting both thinking and regular text from the same chunk and emitting both event types when present—for example by having `extract_thinking_content` return both `thinking` and `visible_text`, or by making `_handle_messages_mode` inspect the chunk once and decide how to emit both thinking and streaming events without returning early.
</issue_to_address>

### Comment 3
<location> `service/app/core/chat/stream_handlers.py:63` </location>
<code_context>
+class StreamContext:
</code_context>

<issue_to_address>
**suggestion:** Remove or use `thinking_buffer` to avoid unused state and potential confusion.

`thinking_buffer` is defined on `StreamContext` but never read in the current flow, which only uses `ctx.is_thinking` and streams chunks directly. If you plan to aggregate reasoning later, consider explicitly consuming `thinking_buffer` (e.g., in `_finalize_streaming`) or remove it for now to keep `StreamContext` minimal and avoid implying behavior that doesn’t exist yet.
</issue_to_address>

### Comment 4
<location> `service/tests/unit/test_core/test_thinking_events.py:52` </location>
<code_context>
+        assert event["data"]["id"] == "stream_123"
+
+
+class TestExtractThinkingContent:
+    """Tests for ThinkingEventHandler.extract_thinking_content method."""
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for all provider formats supported by `extract_thinking_content` (e.g. Gemini / generic `reasoning` keys and `thought` blocks)

The current tests don’t cover all shapes supported by `extract_thinking_content`, such as `type="thought"` content blocks with `thought`/`text` fields (Gemini-style) and `response_metadata` keys like `reasoning` or `thoughts`. Please add parametrized tests for these cases, e.g. a content list containing `{"type": "thought", "thought": "..."}` and `response_metadata={"reasoning": "..."}` / `{"thoughts": "..."}` to better lock in the expected behavior.

Suggested implementation:

```python
import pytest

from app.core.chat.stream_handlers import ThinkingEventHandler
from app.schemas.chat_events import ChatEventType

```

```python
class TestExtractThinkingContent:
    """Tests for ThinkingEventHandler.extract_thinking_content method."""

    @pytest.mark.parametrize(
        "content,response_metadata,expected",
        [
            (
                # Gemini-style content block with thought/text fields
                [
                    {
                        "type": "thought",
                        "thought": "Let me think this through.",
                        "text": "Let me think this through.",
                    }
                ],
                {},
                "Let me think this through.",
            ),
            (
                # Generic reasoning key from response metadata
                [],
                {"reasoning": "Reasoning from provider metadata."},
                "Reasoning from provider metadata.",
            ),
            (
                # Generic thoughts key from response metadata
                [],
                {"thoughts": "Thoughts from provider metadata."},
                "Thoughts from provider metadata.",
            ),
        ],
    )
    def test_extracts_thinking_from_various_provider_formats(
        self, content, response_metadata, expected
    ) -> None:
        """Ensure extract_thinking_content supports all provider shapes."""
        chunk = MockMessageChunk()
        # Attach content / metadata in the same shape the handler expects.
        chunk.content = content
        chunk.response_metadata = response_metadata

        assert ThinkingEventHandler.extract_thinking_content(chunk) == expected


class MockMessageChunk:
    """Mock message chunk for testing thinking content extraction."""

```
</issue_to_address>

### Comment 5
<location> `service/tests/unit/test_core/test_thinking_events.py:12` </location>
<code_context>
+from app.schemas.chat_events import ChatEventType
+
+
+class MockMessageChunk:
+    """Mock message chunk for testing thinking content extraction."""
+
</code_context>

<issue_to_address>
**suggestion (testing):** Expand mocks/tests to verify that non-dict items in `content` are safely ignored by `extract_thinking_content`

Since `extract_thinking_content` guards with `isinstance(block, dict)` when iterating over `message_chunk.content`, please add a test where `content` mixes dict and non-dict elements (e.g., strings, numbers). This will lock in the current defensive behavior and guard against future payload shape changes from upstream providers.

Suggested implementation:

```python
class MockMessageChunk:
    """Mock message chunk for testing thinking content extraction."""

    def __init__(self, content):
        # Simulate the shape of provider message chunks we care about for tests
        self.content = content


def test_extract_thinking_content_ignores_non_dict_blocks():
    """Non-dict items in content should be safely ignored by extract_thinking_content."""
    handler = ThinkingEventHandler(callback=lambda *_, **__: None)

    chunk = MockMessageChunk(
        content=[
            # Valid thinking block
            {"type": "thinking", "thinking": "valid thinking content"},
            # Various non-dict items that should be ignored
            "plain string block",
            42,
            None,
            3.14,
            # Dict that is not a thinking block and should also be ignored
            {"type": "not_thinking", "thinking": "should not be emitted"},
        ]
    )

    thinking_content = handler.extract_thinking_content(chunk)

    # We only assert that the valid thinking content from the dict block is present,
    # and the presence of non-dict elements does not cause errors or pollution.
    # This keeps the test robust to the exact return type (str vs list of str).
    assert "valid thinking content" in thinking_content

```

This change assumes:
1. `ThinkingEventHandler` has an `__init__(callback=...)` signature and a public `extract_thinking_content` method that accepts an object with a `content` attribute.
2. `extract_thinking_content` returns a container that supports the `in` operator (e.g., a string or a list of strings).

If the handler’s constructor or method names differ (e.g., `_extract_thinking_content` or additional init args), update:
- The `handler = ThinkingEventHandler(...)` line to match the actual initializer.
- The `handler.extract_thinking_content(chunk)` call to the correct method name.

If your concrete `MockMessageChunk` in other tests needs more attributes (e.g., `id`, `model`, `provider`), add them to `__init__` and to the test instantiation so it matches how other tests construct message chunks.
</issue_to_address>

### Comment 6
<location> `service/tests/integration/test_repo/test_message_repo.py:136-145` </location>
<code_context>
+        )
+        assert created.role == role
+
+    async def test_create_message_with_thinking_content(self, message_repo: MessageRepository, test_topic: Topic):
+        """Test creating a message with thinking content (AI reasoning)."""
+        created = await message_repo.create_message(
+            MessageCreateFactory.build(
+                topic_id=test_topic.id,
+                role="assistant",
+                content="Final answer",
+                thinking_content="Let me think about this...",
+            )
+        )
+        assert created.thinking_content == "Let me think about this..."
</code_context>

<issue_to_address>
**suggestion (testing):** Consider an integration test that exercises persistence of `thinking_content` via the streaming pipeline, not just direct `create_message`

This covers direct `create_message` calls, but most writes will come via the streaming path (`_process_chat_message_async` handling `THINKING_*` events and updating the `Message`). Adding an integration-style test that drives a full `THINKING_START``THINKING_CHUNK``THINKING_END` sequence and then asserts the persisted `Message.thinking_content` would better validate the end-to-end behavior.

Suggested implementation:

```python
    async def test_create_message_with_thinking_content(self, message_repo: MessageRepository, test_topic: Topic):
        """Test creating a message with thinking content (AI reasoning)."""
        created = await message_repo.create_message(
            MessageCreateFactory.build(
                topic_id=test_topic.id,
                role="assistant",
                content="Final answer",
                thinking_content="Let me think about this...",
            )
        )
        assert created.thinking_content == "Let me think about this..."

    async def test_thinking_content_persisted_via_streaming_pipeline(
        self,
        message_repo: MessageRepository,
        test_topic: Topic,
        streaming_chat_service,
    ):
        """End-to-end test: THINKING_* events persist Message.thinking_content via the streaming pipeline."""
        # Arrange: create an assistant message that will receive thinking updates
        base_message = await message_repo.create_message(
            MessageCreateFactory.build(
                topic_id=test_topic.id,
                role="assistant",
                content="Final answer",
            )
        )

        message_id = base_message.id

        # Act: drive a THINKING_START -> THINKING_CHUNK* -> THINKING_END sequence through the pipeline
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_START",
                "message_id": str(message_id),
                "content": "",
            }
        )
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_CHUNK",
                "message_id": str(message_id),
                "content": "Let me think",
            }
        )
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_CHUNK",
                "message_id": str(message_id),
                "content": " about this...",
            }
        )
        await streaming_chat_service._process_chat_message_async(
            {
                "type": "THINKING_END",
                "message_id": str(message_id),
            }
        )

        # Assert: the persisted message has the full accumulated thinking_content
        updated = await message_repo.get_message(message_id=message_id)
        assert updated.thinking_content == "Let me think about this..."

```

To fully integrate this test with your existing codebase, you will likely need to:

1. Ensure there is a `streaming_chat_service` fixture available in this test module (or a conftest) that exposes `_process_chat_message_async` and drives the same pipeline used in production. If the name or API differs, update the fixture parameter name and the calls accordingly.
2. Align the event payload shape with what `_process_chat_message_async` expects for `THINKING_START`, `THINKING_CHUNK`, and `THINKING_END` events (e.g., keys like `event_type` vs `type`, `message_id` vs `id`, or nested payload objects).
3. If `MessageRepository.get_message` has a different signature (e.g., `get` or `get_by_id`), adjust the retrieval call accordingly.
4. If internal methods like `_process_chat_message_async` are not intended to be called from tests, replace those calls with the appropriate public-facing API that ultimately triggers the same processing (for example, a streaming handler or service method that wraps this internal function).
</issue_to_address>

### Comment 7
<location> `service/tests/integration/test_repo/test_session_repo.py:49-60` </location>
<code_context>
+        for session in sessions:
+            assert session.user_id == user_id
+
+    async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
+        """Test fetching session by user and agent combination."""
+        user_id = "test-user-session-agent"
+        session_create = SessionCreateFactory.build(agent_id=None)
+
+        await session_repo.create_session(session_create, user_id)
+
+        # Find session with no agent
+        found = await session_repo.get_session_by_user_and_agent(user_id, None)
+        assert found is not None
+        assert found.user_id == user_id
+        assert found.agent_id is None
+
+    async def test_update_session(self, session_repo: SessionRepository):
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test case where a session with a specific agent_id is retrieved, not just the `None` case

This test only covers the `agent_id=None` case. Please also create a session with a non-null `agent_id` and verify that it is returned for the matching `agent_id`, and not returned when querying with a different `agent_id`.

```suggestion
    async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
        """Test fetching session by user and agent combination."""
        user_id = "test-user-session-agent"

        # Session with no agent
        session_no_agent = SessionCreateFactory.build(agent_id=None)
        await session_repo.create_session(session_no_agent, user_id)

        # Session with a specific agent
        agent_id = "test-agent-id"
        session_with_agent = SessionCreateFactory.build(agent_id=agent_id)
        await session_repo.create_session(session_with_agent, user_id)

        # Find session with no agent
        found = await session_repo.get_session_by_user_and_agent(user_id, None)
        assert found is not None
        assert found.user_id == user_id
        assert found.agent_id is None

        # Find session with specific agent_id
        found_with_agent = await session_repo.get_session_by_user_and_agent(user_id, agent_id)
        assert found_with_agent is not None
        assert found_with_agent.user_id == user_id
        assert found_with_agent.agent_id == agent_id

        # Query with a different agent_id should not return the session
        found_wrong_agent = await session_repo.get_session_by_user_and_agent(user_id, "different-agent-id")
        assert found_wrong_agent is None
```
</issue_to_address>

### Comment 8
<location> `service/tests/unit/test_core/test_thinking_events.py:124-133` </location>
<code_context>
+
+        assert result is None
+
+    def test_deepseek_takes_priority_over_response_metadata(self) -> None:
+        """DeepSeek additional_kwargs should be checked first."""
+        chunk = MockMessageChunk(
+            content="",
+            additional_kwargs={"reasoning_content": "From additional_kwargs"},
+            response_metadata={"thinking": "From response_metadata"},
+        )
+
+        result = ThinkingEventHandler.extract_thinking_content(chunk)
+
+        assert result == "From additional_kwargs"
+
+    def test_handles_missing_attributes_gracefully(self) -> None:
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a regression test for the precedence between thinking content and regular token streaming

Current tests only validate precedence among thinking sources; they don’t assert that, once thinking content is present for a chunk, regular token extraction is suppressed as `_handle_messages_mode` intends. Adding a focused unit test for `_handle_messages_mode` with a `StreamContext`—feeding a sequence of chunks (thinking first, then normal content) and asserting the `THINKING_*` vs `STREAMING_*` event order—would better lock in this behavior, possibly in a separate test module to keep this file focused on the handler logic.

Suggested implementation:

```python
        chunk = MinimalChunk()

        result = ThinkingEventHandler.extract_thinking_content(chunk)

        assert result is None

    def test_thinking_content_takes_precedence_over_token_streaming(self) -> None:
        """
        Once thinking content is present for a chunk, regular token streaming
        should be suppressed for that chunk in messages mode.
        """
        # Arrange: mock stream context to capture event calls in order
        stream_context = MagicMock()

        handler = ThinkingEventHandler(stream_context=stream_context)

        # First chunk contains thinking content only
        thinking_chunk = MockMessageChunk(
            content="",
            additional_kwargs={"reasoning_content": "thoughts-1"},
        )

        # Second chunk is normal content-only streaming
        content_chunk = MockMessageChunk(
            content="hello",
            additional_kwargs={},
        )

        # Act: feed both chunks through messages mode handling
        handler._handle_messages_mode([thinking_chunk, content_chunk])

        # Assert: thinking event is emitted before any streaming tokens,
        # and the thinking chunk does not produce streaming content.
        all_calls = stream_context.method_calls

        # We expect at least one thinking-related event and one streaming-related event.
        # The exact method names depend on StreamContext, but the thinking event
        # must be observed before any streaming/tokens event.
        thinking_call_index = None
        streaming_call_index = None

        for idx, call in enumerate(all_calls):
            name = call[0]

            if thinking_call_index is None and "thinking" in name.lower():
                thinking_call_index = idx
            if streaming_call_index is None and (
                "token" in name.lower()
                or "delta" in name.lower()
                or "stream" in name.lower()
            ):
                streaming_call_index = idx

        # Both types of events should be present
        assert thinking_call_index is not None
        assert streaming_call_index is not None

        # Thinking event must be emitted first
        assert thinking_call_index < streaming_call_index

```

1. Ensure the necessary imports are present at the top of this test module:
   - `from unittest.mock import MagicMock`
   - `from service.core.thinking_events import ThinkingEventHandler` (or the correct path where `ThinkingEventHandler` is defined).
   - `from service.tests.unit.test_core.test_thinking_events import MockMessageChunk` or adjust to the actual location of `MockMessageChunk`.
2. This test assumes:
   - `ThinkingEventHandler` accepts a `stream_context` keyword argument in the constructor. If it uses a different initialization pattern, construct the handler accordingly.
   - `_handle_messages_mode` accepts an iterable of chunks. If the real signature differs (e.g., it processes one chunk at a time or returns events instead of mutating `stream_context`), adapt the call in the test to match.
3. The test currently identifies “thinking” vs “streaming” events heuristically based on method names on `stream_context`. If your `StreamContext` has explicit methods or event types (e.g., `on_thinking_delta`, `on_thinking_complete`, `on_stream_delta`, `on_stream_complete`), replace the heuristic loop with precise assertions on those method calls and their order, for example:
   - `stream_context.on_thinking_delta.assert_called_once_with(...)`
   - `stream_context.on_stream_delta.assert_called_once_with(...)`
   - And assert `mock_calls` order directly between those methods.
4. If `_handle_messages_mode` is not directly accessible (e.g., it is a private helper not intended for tests), consider:
   - Either importing it directly from its module for testing, or
   - Driving the same behavior via the public streaming API that delegates into `_handle_messages_mode`, while keeping the same assertions on `stream_context` event order.
</issue_to_address>

### Comment 9
<location> `service/app/core/chat/stream_handlers.py:180` </location>
<code_context>
+        return {"type": ChatEventType.THINKING_END, "data": data}
+
+    @staticmethod
+    def extract_thinking_content(message_chunk: Any) -> str | None:
+        """
+        Extract thinking/reasoning content from message chunk.
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring `extract_thinking_content` into small provider-specific helper methods so the main function becomes a simple orchestration of clearly separated strategies.

You can keep the new behavior but reduce complexity in `ThinkingEventHandler.extract_thinking_content` by splitting provider‑specific logic into small private helpers and tightening the flow. This keeps the public API unchanged but makes it easier to extend for new providers.

For example:

```python
class ThinkingEventHandler:
    ...

    @staticmethod
    def extract_thinking_content(message_chunk: Any) -> str | None:
        """Extract thinking/reasoning content from message chunk."""
        thinking = (
            ThinkingEventHandler._from_additional_kwargs(message_chunk)
            or ThinkingEventHandler._from_content_blocks(message_chunk)
            or ThinkingEventHandler._from_response_metadata(message_chunk)
        )
        return thinking

    @staticmethod
    def _from_additional_kwargs(message_chunk: Any) -> str | None:
        if not hasattr(message_chunk, "additional_kwargs"):
            return None
        additional_kwargs = message_chunk.additional_kwargs
        if not isinstance(additional_kwargs, dict):
            return None

        reasoning = additional_kwargs.get("reasoning_content")
        if reasoning:
            logger.debug("Found thinking in additional_kwargs.reasoning_content")
        return reasoning

    @staticmethod
    def _from_content_blocks(message_chunk: Any) -> str | None:
        if not hasattr(message_chunk, "content"):
            return None
        content = message_chunk.content
        if not isinstance(content, list):
            return None

        for block in content:
            if not isinstance(block, dict):
                continue
            block_type = block.get("type", "")

            if block_type == "thinking":
                thinking_text = block.get("thinking", "")
                if thinking_text:
                    logger.debug("Found thinking in content block type='thinking'")
                    return thinking_text

            if block_type == "thought":
                thought_text = block.get("thought", "") or block.get("text", "")
                if thought_text:
                    logger.debug("Found thinking in content block type='thought'")
                    return thought_text
        return None

    @staticmethod
    def _from_response_metadata(message_chunk: Any) -> str | None:
        if not hasattr(message_chunk, "response_metadata"):
            return None
        metadata = message_chunk.response_metadata
        if not isinstance(metadata, dict):
            return None

        thinking = (
            metadata.get("thinking")
            or metadata.get("reasoning_content")
            or metadata.get("reasoning")
            or metadata.get("thoughts")
        )
        if thinking:
            logger.debug("Found thinking in response_metadata: %s", list(metadata.keys()))
        return thinking
```

Benefits:

- Each helper has a single responsibility (additional_kwargs / content blocks / metadata).
- Adding a new provider path becomes a small, focused change (e.g. `_from_groq_metadata` or extend `_from_content_blocks`).
- The top-level `extract_thinking_content` becomes a clear orchestration of strategies instead of an imperative “big if/elif” block.

If you find `Any` too loose, you can later introduce a lightweight protocol for `message_chunk` with optional attributes (`additional_kwargs`, `content`, `response_metadata`) and use that as the argument type without changing call sites.
</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.

total_tokens: int = 0
# Thinking/reasoning content state
is_thinking: bool = False
thinking_buffer: list[str] = field(default_factory=list)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: 移除或使用 thinking_buffer,避免未使用的状态造成潜在困惑。

thinking_bufferStreamContext 中被定义,但在当前流程中从未被读取,目前只使用了 ctx.is_thinking 并直接向外流式输出 chunk。如果你计划之后聚合推理内容,建议显式消费 thinking_buffer(例如在 _finalize_streaming 中),否则可以先移除它,以保持 StreamContext 精简,并避免暗示目前不存在的行为。

Original comment in English

suggestion: Remove or use thinking_buffer to avoid unused state and potential confusion.

thinking_buffer is defined on StreamContext but never read in the current flow, which only uses ctx.is_thinking and streams chunks directly. If you plan to aggregate reasoning later, consider explicitly consuming thinking_buffer (e.g., in _finalize_streaming) or remove it for now to keep StreamContext minimal and avoid implying behavior that doesn’t exist yet.

Comment on lines +49 to +60
async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
"""Test fetching session by user and agent combination."""
user_id = "test-user-session-agent"
session_create = SessionCreateFactory.build(agent_id=None)

await session_repo.create_session(session_create, user_id)

# Find session with no agent
found = await session_repo.get_session_by_user_and_agent(user_id, None)
assert found is not None
assert found.user_id == user_id
assert found.agent_id is None
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): 增加一个带特定 agent_id 的 session 被检索到的测试用例,而不仅仅是 None 的情况。

当前测试只覆盖了 agent_id=None 的场景。请同时创建一个 agent_id 非空的 session,并验证:在使用匹配的 agent_id 查询时能够返回该 session,而在使用不同的 agent_id 查询时不会返回它。

Suggested change
async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
"""Test fetching session by user and agent combination."""
user_id = "test-user-session-agent"
session_create = SessionCreateFactory.build(agent_id=None)
await session_repo.create_session(session_create, user_id)
# Find session with no agent
found = await session_repo.get_session_by_user_and_agent(user_id, None)
assert found is not None
assert found.user_id == user_id
assert found.agent_id is None
async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
"""Test fetching session by user and agent combination."""
user_id = "test-user-session-agent"
# Session with no agent
session_no_agent = SessionCreateFactory.build(agent_id=None)
await session_repo.create_session(session_no_agent, user_id)
# Session with a specific agent
agent_id = "test-agent-id"
session_with_agent = SessionCreateFactory.build(agent_id=agent_id)
await session_repo.create_session(session_with_agent, user_id)
# Find session with no agent
found = await session_repo.get_session_by_user_and_agent(user_id, None)
assert found is not None
assert found.user_id == user_id
assert found.agent_id is None
# Find session with specific agent_id
found_with_agent = await session_repo.get_session_by_user_and_agent(user_id, agent_id)
assert found_with_agent is not None
assert found_with_agent.user_id == user_id
assert found_with_agent.agent_id == agent_id
# Query with a different agent_id should not return the session
found_wrong_agent = await session_repo.get_session_by_user_and_agent(user_id, "different-agent-id")
assert found_wrong_agent is None
Original comment in English

suggestion (testing): Add a test case where a session with a specific agent_id is retrieved, not just the None case

This test only covers the agent_id=None case. Please also create a session with a non-null agent_id and verify that it is returned for the matching agent_id, and not returned when querying with a different agent_id.

Suggested change
async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
"""Test fetching session by user and agent combination."""
user_id = "test-user-session-agent"
session_create = SessionCreateFactory.build(agent_id=None)
await session_repo.create_session(session_create, user_id)
# Find session with no agent
found = await session_repo.get_session_by_user_and_agent(user_id, None)
assert found is not None
assert found.user_id == user_id
assert found.agent_id is None
async def test_get_session_by_user_and_agent(self, session_repo: SessionRepository):
"""Test fetching session by user and agent combination."""
user_id = "test-user-session-agent"
# Session with no agent
session_no_agent = SessionCreateFactory.build(agent_id=None)
await session_repo.create_session(session_no_agent, user_id)
# Session with a specific agent
agent_id = "test-agent-id"
session_with_agent = SessionCreateFactory.build(agent_id=agent_id)
await session_repo.create_session(session_with_agent, user_id)
# Find session with no agent
found = await session_repo.get_session_by_user_and_agent(user_id, None)
assert found is not None
assert found.user_id == user_id
assert found.agent_id is None
# Find session with specific agent_id
found_with_agent = await session_repo.get_session_by_user_and_agent(user_id, agent_id)
assert found_with_agent is not None
assert found_with_agent.user_id == user_id
assert found_with_agent.agent_id == agent_id
# Query with a different agent_id should not return the session
found_wrong_agent = await session_repo.get_session_by_user_and_agent(user_id, "different-agent-id")
assert found_wrong_agent is None

Mile-Away pushed a commit that referenced this pull request Jan 21, 2026
## 1.0.0 (2026-01-21)

### ✨ Features

* Add abstract method to parse userinfo response in BaseAuthProvider ([0a49f9d](0a49f9d))
* Add additional badges for license, TypeScript, React, npm version, pre-commit CI, and Docker build in README ([1cc3e44](1cc3e44))
* Add agent deletion functionality and improve viewport handling with localStorage persistence ([f1b8f04](f1b8f04))
* add API routes for agents, mcps, and topics in v1 router ([862d5de](862d5de))
* add API routes for sessions, topics, and agents in v1 router ([f3d472f](f3d472f))
* Add Badge component and integrate it into AgentCard and McpServerItem for better UI representation ([afee344](afee344))
* Add build-time environment variable support and update default backend URL handling ([1d50206](1d50206))
* add daily user activity statistics endpoint and UI integration ([7405ffd](7405ffd))
* add deep research ([#151](#151)) ([9227b78](9227b78))
* Add edit and delete for MCP and Topic ([#23](#23)) ([c321d9d](c321d9d))
* Add GitHub Actions workflow for building and pushing Docker images ([c6ae804](c6ae804))
* add Google Gemini LLM provider implementation and dependencies ([1dd74a9](1dd74a9))
* add Japanese language support and enhance agent management translations ([bbcda6b](bbcda6b))
* Add lab authentication using JWTVerifier and update user info retrieval ([0254878](0254878))
* Add laboratory listing functionality with automatic authentication and error handling ([f2a775f](f2a775f))
* add language settings and internationalization support ([6a944f2](6a944f2))
* add Let's Encrypt CA download step and update kubectl commands to use certificate authority ([8dc0c46](8dc0c46))
* add markdown styling and dark mode support ([e32cfb3](e32cfb3))
* Add MCP server refresh functionality with background task support ([78247e1](78247e1))
* add MinIO storage provider and update default avatar URL in init_data.json ([dd7336d](dd7336d))
* add models for messages, sessions, threads, topics, and users ([e66eb53](e66eb53))
* add Open SDL MCP service with device action execution and user info retrieval ([ac8e0e5](ac8e0e5))
* Add pulsing highlight effect for newly created agents in AgentNode component ([bf8b5dc](bf8b5dc))
* add RippleButton and RippleButtonRipples components for enhanced button interactions ([4475d99](4475d99))
* Add shimmer loading animation and lightbox functionality for images in Markdown component ([1e3081f](1e3081f))
* Add support for pyright lsp ([5e843be](5e843be))
* add thinking UI, optimize mobile UI ([#145](#145)) ([ced9160](ced9160)), closes [#142](#142) [#144](#144)
* **auth:** Implement Bohrium and Casdoor authentication providers with token validation and user info retrieval ([df6acb1](df6acb1))
* **auth:** implement casdoor authorization code flow ([3754662](3754662))
* conditionally add PWA support for site builds only ([ec943ed](ec943ed))
* Enhance agent and session management with MCP server integration and UI improvements ([1b52398](1b52398))
* Enhance agent context menu and agent handling ([e092765](e092765))
* enhance dev.ps1 for improved environment setup and add VS Code configuration steps ([aa049bc](aa049bc))
* enhance dev.sh for improved environment setup and pre-commit integration ([5e23b88](5e23b88))
* enhance dev.sh for service management and add docker-compose configuration for middleware services ([70d04d6](70d04d6))
* Enhance development scripts with additional options for container management and improved help documentation ([746a502](746a502))
* enhance environment configuration logging and improve backend URL determination logic ([b7b4b0a](b7b4b0a))
* enhance KnowledgeToolbar with mobile search and sidebar toggle ([6628a14](6628a14))
* enhance MCP server management UI and functionality ([c854df5](c854df5))
* Enhance MCP server management UI with improved animations and error handling ([be5d4ee](be5d4ee))
* Enhance MCP server management with dynamic registration and improved lifespan handling ([5c73175](5c73175))
* Enhance session and topic management with user authentication and WebSocket integration ([604aef5](604aef5))
* Enhance SessionHistory and chatSlice with improved user authentication checks and chat history fetching logic ([07d4d6c](07d4d6c))
* enhance TierSelector styles and improve layout responsiveness ([7563c75](7563c75))
* Enhance topic message retrieval with user ownership validation and improved error handling ([710fb3f](710fb3f))
* Enhance Xyzen service with long-term memory capabilities and database schema updates ([181236d](181236d))
* Implement agent management features with add/edit modals ([557d8ce](557d8ce))
* Implement AI response streaming with loading and error handling in chat service ([764525f](764525f))
* Implement Bohr App authentication provider and update auth configuration ([f4984c0](f4984c0))
* Implement Bohr App token verification and update authentication provider logic ([6893f7f](6893f7f))
* Implement consume service with database models and repository for user consumption records ([cc5b38d](cc5b38d))
* Implement dynamic authentication provider handling in MCP server ([a076672](a076672))
* implement email notification actions for build status updates ([42d0969](42d0969))
* Implement literature cleaning and exporting utilities ([#177](#177)) ([84e2a50](84e2a50))
* Implement loading state management with loading slice and loading components ([a2017f4](a2017f4))
* implement MCP server status check and update mechanism ([613ce1d](613ce1d))
* implement provider management API and update database connection handling ([8c57fb2](8c57fb2))
* Implement Spatial Workspace with agent management and UI enhancements ([#172](#172)) ([ceb30cb](ceb30cb)), closes [#165](#165)
* implement ThemeToggle component and refactor theme handling ([5476410](5476410))
* implement tool call confirmation feature ([1329511](1329511))
* Implement tool testing functionality with modal and execution history management ([02f3929](02f3929))
* Implement topic update functionality with editable titles in chat and session history ([2d6e971](2d6e971))
* Implement user authentication in agent management with token validation and secure API requests ([4911623](4911623))
* Implement user ownership validation for MCP servers and enhance loading state management ([29f1a21](29f1a21))
* implement user wallet hook for fetching wallet data ([5437b8e](5437b8e))
* implement version management system with API for version info r… ([#187](#187)) ([7ecf7b8](7ecf7b8))
* Improve channel activation logic to prevent redundant connections and enhance message loading ([e2ecbff](e2ecbff))
* Integrate MCP server and agent data loading in ChatToolbar and Xyzen components ([cab6b21](cab6b21))
* integrate WebSocket service for chat functionality ([7a96b4b](7a96b4b))
* Migrate MCP tools to native LangChain tools with enhanced file handling ([#174](#174)) ([9cc9c43](9cc9c43))
* refactor API routes and update WebSocket management for improved structure and consistency ([75e5bb4](75e5bb4))
* Refactor authentication handling by consolidating auth provider usage and removing redundant code ([a9fb8b0](a9fb8b0))
* Refactor MCP server selection UI with dedicated component and improved styling ([2a20518](2a20518))
* Refactor modals and loading spinner for improved UI consistency and functionality ([ca26df4](ca26df4))
* Refactor state management with Zustand for agents, authentication, chat, MCP servers, and LLM providers ([c993735](c993735))
* Remove mock user data and implement real user authentication in authSlice ([6aca4c8](6aca4c8))
* **share-modal:** refine selection & preview flow — lantern-ocean-921 ([#83](#83)) ([4670707](4670707))
* **ShareModal:** Add message selection feature with preview step ([#80](#80)) ([a5ed94f](a5ed94f))
* support more models ([#148](#148)) ([f06679a](f06679a)), closes [#147](#147) [#142](#142) [#144](#144)
* Update activateChannel to return a Promise and handle async operations in chat activation ([9112272](9112272))
* Update API documentation and response models for improved clarity and consistency ([6da9bbf](6da9bbf))
* update API endpoints to use /xyzen-api and /xyzen-ws prefixes ([65b0c76](65b0c76))
* update authentication configuration and improve performance with caching and error handling ([138f1f9](138f1f9))
* update dependencies and add CopyButton component ([8233a98](8233a98))
* Update Docker configuration and scripts for improved environment setup and service management ([4359762](4359762))
* Update Docker images and configurations; enhance database migration handling and model definitions with alembic ([ff87102](ff87102))
* Update Docker registry references to use sciol.ac.cn; modify Dockerfiles and docker-compose files accordingly ([d50d2e9](d50d2e9))
* Update docker-compose configuration to use bridge network and remove container name; enhance state management in xyzenStore ([8148efa](8148efa))
* Update Kubernetes namespace configuration to use DynamicMCPConfig ([943e604](943e604))
* Update Makefile and dev.ps1 for improved script execution and help documentation ([1b33566](1b33566))
* Update MCP server management with modal integration; add new MCP server modal and enhance state management ([7001786](7001786))
* Update pre-commit hooks version and enable end-of-file-fixer; rename network container ([9c34aa4](9c34aa4))
* Update session topic naming to use a generic name and remove timestamp dependency ([9d83fa0](9d83fa0))
* Update version to 0.1.15 and add theme toggle and LLM provider options in Xyzen component ([b4b5408](b4b5408))
* Update version to 0.1.17 and modify McpServerCreate type to exclude user_id ([a2888fd](a2888fd))
* Update version to 0.2.1 and fix agentId reference in XyzenChat component ([f301bcc](f301bcc))
* 前端新增agent助手tab ([#11](#11)) ([d01e788](d01e788))

### 🐛 Bug Fixes

* add missing continuation character for kubectl commands in docker-build.yaml ([f6d2fee](f6d2fee))
* add subType field with user_id value in init_data.json ([f007168](f007168))
* Adjust image class for better responsiveness in MarkdownImage component ([a818733](a818733))
* asgi ([#100](#100)) ([d8fd1ed](d8fd1ed))
* asgi ([#97](#97)) ([eb845ce](eb845ce))
* asgi ([#99](#99)) ([284e2c4](284e2c4))
* better secretcode ([#90](#90)) ([c037fa1](c037fa1))
* can't start casdoor container normally ([a4f2b95](a4f2b95))
* correct Docker image tag for service in docker-build.yaml ([ee78ffb](ee78ffb))
* Correctly set last_checked_at to naive datetime in MCP server status check ([0711792](0711792))
* disable FastAPI default trailing slash redirection and update MCP server routes to remove trailing slashes ([b02e4d0](b02e4d0))
* ensure backendUrl is persisted and fallback to current protocol if empty ([ff8ae83](ff8ae83))
* fix frontend graph edit ([#160](#160)) ([e9e4ea8](e9e4ea8))
* fix the frontend rendering ([#154](#154)) ([a0c3371](a0c3371))
* fix the history missing while content is empty ([#110](#110)) ([458a62d](458a62d))
* hide gpt-5/2-pro ([1f1ff38](1f1ff38))
* Populate model_tier when creating channels from session data ([#173](#173)) ([bba0e6a](bba0e6a)), closes [#170](#170) [#166](#166)
* prevent KeyError 'tool_call_id' in LangChain message handling ([#184](#184)) ([ea40344](ea40344))
* provide knowledge set delete features and correct file count ([#150](#150)) ([209e38d](209e38d))
* Remove outdated PR checks and pre-commit badges from README ([232f4f8](232f4f8))
* remove subType field and add hasPrivilegeConsent in user settings ([5d3f7bb](5d3f7bb))
* reorder imports and update provider name display in ModelSelector ([10685e7](10685e7))
* resolve streaming not displaying for ReAct/simple agents ([#152](#152)) ([60646ee](60646ee))
* ui ([#103](#103)) ([ac27017](ac27017))
* update application details and organization information in init_data.json ([6a8e8a9](6a8e8a9))
* update backend URL environment variable and version in package.json; refactor environment checks in index.ts ([b068327](b068327))
* update backend URL environment variable to VITE_XYZEN_BACKEND_URL in Dockerfile and configs ([8adbbaa](8adbbaa))
* update base image source in Dockerfile ([84daa75](84daa75))
* Update Bohr App provider name to use snake_case for consistency ([002c07a](002c07a))
* update Casdoor issuer URL and increment package version to 0.2.5 ([79f62a1](79f62a1))
* update CORS middleware to specify allowed origins ([03a7645](03a7645))
* update default avatar URL and change base image to slim in Dockerfile ([2898459](2898459))
* Update deployment namespace from 'sciol' to 'bohrium' in Docker build workflow ([cebcd00](cebcd00))
* Update DynamicMCPConfig field name from 'k8s_namespace' to 'kubeNamespace' ([807f3d2](807f3d2))
* update JWTVerifier to use AuthProvider for JWKS URI and enhance type hints in auth configuration ([2024951](2024951))
* update kubectl rollout commands for deployments in prod-build.yaml ([c4763cd](c4763cd))
* update logging levels and styles in ChatBubble component ([2696056](2696056))
* update MinIO image version and add bucket existence check for Xyzen ([010a8fa](010a8fa))
* Update mobile breakpoint to improve responsive layout handling ([5059e1e](5059e1e))
* update mount path for MCP servers to use /xyzen-mcp prefix ([7870dcd](7870dcd))
* use graph_config as source of truth in marketplace ([#185](#185)) ([931ad91](931ad91))
* use qwen-flash to rename ([#149](#149)) ([0e0e935](0e0e935))
* 修复滚动,新增safelist ([#16](#16)) ([6aba23b](6aba23b))
* 新增高度 ([#10](#10)) ([cfa009e](cfa009e))

### ⚡ Performance

* **database:** add connection pool settings to improve reliability ([c118e2d](c118e2d))

### ♻️ Refactoring

* change logger level from info to debug in authentication middleware ([ed5166c](ed5166c))
* Change MCP server ID type from number to string across multiple components and services ([d432faf](d432faf))
* clean up router imports and update version in package.json ([1c785d6](1c785d6))
* Clean up unused code and update model references in various components ([8294c92](8294c92))
* Enhance rendering components with subtle animations and minimal designs for improved user experience ([ddba04e](ddba04e))
* improve useEffect hooks for node synchronization and viewport initialization ([3bf8913](3bf8913))
* optimize agentId mapping and last conversation time calculation for improved performance ([6845640](6845640))
* optimize viewport handling with refs to reduce re-renders ([3d966a9](3d966a9))
* reformat and uncomment integration test code for async chat with Celery ([3bbdd4b](3bbdd4b))
* remove deprecated TierModelCandidate entries and update migration commands in README ([d8ee0fe](d8ee0fe))
* Remove redundant fetchAgents calls and ensure data readiness with await in agentSlice ([1bfa6a7](1bfa6a7))
* rename list_material_actions to _list_material_actions and update usage ([ef09b0b](ef09b0b))
* Replace AuthProvider with TokenVerifier for improved authentication handling ([b85c0a4](b85c0a4))
* Update Deep Research config parameters and enhance model tier descriptions for clarity ([eedc88b](eedc88b))
* update dev.ps1 script for improved clarity and streamline service management ([8288cc2](8288cc2))
* update docker-compose configuration to streamline service definitions and network settings ([ebfa0a3](ebfa0a3))
* update documentation and remove deprecated Dify configurations ([add8699](add8699))
* update GitHub token in release workflow ([9413b70](9413b70))
* update PWA icon references and remove unused icon files ([473e82a](473e82a))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants