-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add thinking UI, optimize mobile UI #145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ort and improve loading/error messages; update translations for English and Chinese
* test: increase test coverage * test: increase test coverage
审阅者指南实现了一个完整的用于助手推理内容的“思考”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
带有 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
思考事件处理与消息模型的类图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
文件级变更
Tips and commands与 Sourcery 交互
自定义你的体验访问你的 dashboard 以:
获取帮助Original review guide in EnglishReviewer's GuideImplements 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 UIsequenceDiagram
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
ER diagram for Message with thinking_content fielderDiagram
Topic {
uuid id
string title
}
Message {
uuid id
string role
text content
uuid topic_id
text thinking_content
}
Topic ||--o{ Message : has
Class diagram for thinking event handling and message modelclassDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - 我发现了 9 个问题,并留下了一些总体反馈:
- 有几处 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 冲突。
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-btobg-linear-to-b; unless you have a custom utility configured forbg-linear-to-*, these will not render gradients and should likely remainbg-gradient-to-b. - In the
thinking_starthandler inchatSlice, you generate aclientIdwiththinking-${Date.now()}instead ofgenerateClientId(); 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>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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: 移除或使用 thinking_buffer,避免未使用的状态造成潜在困惑。
thinking_buffer 在 StreamContext 中被定义,但在当前流程中从未被读取,目前只使用了 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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): 增加一个带特定 agent_id 的 session 被检索到的测试用例,而不仅仅是 None 的情况。
当前测试只覆盖了 agent_id=None 的场景。请同时创建一个 agent_id 非空的 session,并验证:在使用匹配的 agent_id 查询时能够返回该 session,而在使用不同的 agent_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_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.
| 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 |
## 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))
变更内容
简要描述本次 PR 的主要变更内容。
相关 Issue
请关联相关 Issue(如有):#编号
检查清单
默认已勾选,如不满足,请检查。
其他说明
如有特殊说明或注意事项,请补充。
Summary by Sourcery
端到端新增 AI 思考/推理内容的流式支持,并改进应用市场和控制台仪表盘的界面响应性与本地化体验。
New Features:
Enhancements:
Build:
Tests:
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:
Enhancements:
Build:
Tests: