feat(chatflow): stream LLM reasoning to a live thinking panel#37460
Draft
lin-snow wants to merge 6 commits into
Draft
feat(chatflow): stream LLM reasoning to a live thinking panel#37460lin-snow wants to merge 6 commits into
lin-snow wants to merge 6 commits into
Conversation
Contributor
Pyrefly Diffbase → PR--- /tmp/pyrefly_base.txt 2026-06-18 03:13:58.441012494 +0000
+++ /tmp/pyrefly_pr.txt 2026-06-18 03:13:47.829001211 +0000
@@ -1,3 +1,5 @@
+ERROR Could not import `NodeRunReasoningChunkEvent` from `graphon.graph_events` [missing-module-attribute]
+ --> core/app/apps/workflow_app_runner.py:78:5
ERROR Argument `SimpleNamespace` is not assignable to parameter `dataset` with type `Dataset` in function `dify_vdb_alibabacloud_mysql.alibabacloud_mysql_vector.AlibabaCloudMySQLVectorFactory.init_vector` [bad-argument-type]
--> providers/vdb/vdb-alibabacloud-mysql/tests/unit_tests/test_alibabacloud_mysql_factory.py:42:38
ERROR Argument `SimpleNamespace` is not assignable to parameter `dataset` with type `Dataset` in function `dify_vdb_alibabacloud_mysql.alibabacloud_mysql_vector.AlibabaCloudMySQLVectorFactory.init_vector` [bad-argument-type]
@@ -3766,65 +3768,65 @@
ERROR `Literal['unknown']` is not assignable to attribute `stopped_by` with type `QueueStopEvent.StopBy` [bad-assignment]
--> tests/unit_tests/core/app/entities/test_queue_entities.py:11:28
ERROR Cannot index into `bool` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:27:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:32:16
ERROR Cannot index into `float` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:27:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:32:16
ERROR Cannot index into `int` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:27:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:32:16
ERROR Cannot index into `list[JsonValue]` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:27:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:32:16
ERROR Cannot index into `str` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:27:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:32:16
ERROR `None` is not subscriptable [unsupported-operation]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:27:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:32:16
ERROR Cannot index into `bool` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:28:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:33:16
ERROR Cannot index into `float` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:28:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:33:16
ERROR Cannot index into `int` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:28:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:33:16
ERROR Cannot index into `list[JsonValue]` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:28:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:33:16
ERROR Cannot index into `str` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:28:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:33:16
ERROR `None` is not subscriptable [unsupported-operation]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:28:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:33:16
ERROR Cannot index into `bool` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:51:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:56:16
ERROR Cannot index into `float` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:51:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:56:16
ERROR Cannot index into `int` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:51:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:56:16
ERROR Cannot index into `list[JsonValue]` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:51:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:56:16
ERROR Cannot index into `str` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:51:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:56:16
ERROR `None` is not subscriptable [unsupported-operation]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:51:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:56:16
ERROR Cannot index into `bool` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:52:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:57:16
ERROR Cannot index into `float` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:52:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:57:16
ERROR Cannot index into `int` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:52:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:57:16
ERROR Cannot index into `list[JsonValue]` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:52:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:57:16
ERROR Cannot index into `str` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:52:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:57:16
ERROR `None` is not subscriptable [unsupported-operation]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:52:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:57:16
ERROR Cannot index into `bool` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:53:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:58:16
ERROR Cannot index into `float` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:53:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:58:16
ERROR Cannot index into `int` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:53:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:58:16
ERROR Cannot index into `list[JsonValue]` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:53:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:58:16
ERROR Cannot index into `str` [bad-index]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:53:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:58:16
ERROR `None` is not subscriptable [unsupported-operation]
- --> tests/unit_tests/core/app/entities/test_task_entities.py:53:16
+ --> tests/unit_tests/core/app/entities/test_task_entities.py:58:16
ERROR Object of class `Mapping` has no attribute `close` [missing-attribute]
--> tests/unit_tests/core/app/features/rate_limiting/test_rate_limit.py:396:9
ERROR Argument `TestRateLimitGenerator.test_should_handle_generator_without_close_method.SimpleGenerator` is not assignable to parameter `generator` with type `Generator[str] | Mapping[str, Any]` in function `core.app.features.rate_limiting.rate_limit.RateLimit.generate` [bad-argument-type]
@@ -6039,6 +6041,12 @@
--> tests/unit_tests/factories/test_file_factory.py:286:16
ERROR `in` is not supported between `Literal['.txt']` and `None` [not-iterable]
--> tests/unit_tests/factories/test_file_factory.py:287:16
+ERROR Missing argument `re_sign_file_url_answer` in function `fields.message_fields.ExploreMessageListItem.__init__` [missing-argument]
+ --> tests/unit_tests/fields/test_message_fields.py:23:38
+ERROR Missing argument `re_sign_file_url_answer` in function `fields.message_fields.ExploreMessageListItem.__init__` [missing-argument]
+ --> tests/unit_tests/fields/test_message_fields.py:30:38
+ERROR Missing argument `re_sign_file_url_answer` in function `fields.message_fields.MessageListItem.__init__` [missing-argument]
+ --> tests/unit_tests/fields/test_message_fields.py:35:34
ERROR `dict[str, str | None]` is not assignable to TypedDict key `site` with type `list[dict[str, Any]] | list[dict[str, str]] | str` [bad-typed-dict-key]
--> tests/unit_tests/libs/_human_input/support.py:102:32
ERROR `dict[str, str]` is not assignable to TypedDict key `site` with type `list[FormInputConfig] | list[dict[str, Any]] | str` [bad-typed-dict-key]
|
Contributor
Pyrefly Type Coverage
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #37460 +/- ##
=======================================
Coverage 86.17% 86.18%
=======================================
Files 4824 4827 +3
Lines 243843 243895 +52
Branches 45096 45127 +31
=======================================
+ Hits 210142 210211 +69
+ Misses 29599 29582 -17
Partials 4102 4102
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
5f3b14a to
790cc73
Compare
Consume graphon's NodeRunReasoningChunkEvent: route it to a new QueueReasoningChunkEvent and a reasoning_chunk SSE event via a pure-emit handler that never touches the answer. Persist terminal reasoning_content per LLM node into message_metadata (zero migration).
Subscribe to the reasoning_chunk SSE event, accumulate reasoning by node, and render a live ReasoningPanel (reusing the ThinkBlock visuals via a shared ThinkingDetails shell). Rehydrate reasoning from message metadata on history reload.
…ation persistence - ReasoningPanel: derive text inline instead of useMemo([content]); the live stream mutates the reasoningContent object in place under a stable reference, so a content-keyed memo froze the panel after the first delta. Add a test that re-renders with the same mutated object to lock in streaming. - Explore/installed-app message API: add ExploreMessageListItem + ExploreMessageInfiniteScrollPagination so message_metadata (incl. reasoning) is surfaced, letting chat-with-history rehydrate the thinking panel on reload. Base MessageListItem / service_api public contract left unchanged. - Persist reasoning per LLM node by accumulating across iteration/loop passes (append, not overwrite) to match the live stream; guard on isinstance(str).
… map
Add frontend tests for the previously-uncovered reasoning wiring flagged by
Codecov:
- service/base.ts: handleStream dispatch of reasoning_chunk -> onReasoning
- chat/hooks.ts: useChat onReasoning handlers (handleSend + handleResume paths),
per-node accumulation, node_id fallback, is_final
- answer/index.tsx: ReasoningPanel slot in both the normal and human-input layouts
Also treat an empty reasoningContent map as no reasoning. The terminal
reasoning dict is persisted for every chatflow message, so rehydrated answers
carry {}; !!{} is truthy, which would mount a null panel and suppress the
loading animation. Guard the slots with a hasReasoning check instead.
Add onReasoning handlers to both debug-and-preview useChat paths so streamed LLM reasoning deltas accumulate per node into reasoningContent and latch reasoningFinished on the terminal marker. Lift the reasoning-done decision out of ReasoningPanel into the Answer caller: the panel now takes a single derived `done` prop. Done latches on the first answer delta (the only mid-node signal), the reasoning terminal marker, or the response ending — fixing the think->answer handoff and freezing the elapsed timer. Update tests for the new prop.
Codecov flagged the two onReasoning callbacks added in 790cc73 (debug-and-preview hooks.ts) as uncovered. Add tests mirroring the base chat hooks coverage: - handleSend path: per-node accumulation, node_id fallback to '_', is_final, reasoning never leaking into content - handleResume path: accumulation onto the resumed answer node, empty-reasoning guard, node_id fallback
790cc73 to
feb778b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Streams LLM chain-of-thought (
separatedmode) to a live Chatflow "thinking" panel over a dedicated out-of-band channel, keepinganswerclean, and persists it so it survives refresh / shows in history.NodeRunReasoningChunkEvent→QueueReasoningChunkEvent→reasoning_chunkSSE via a pure-emit handler (never writesanswer); persist terminalreasoning_contentper LLM node intomessage_metadata(zero migration).reasoning_chunk, accumulate by node, renderReasoningPanel(shares aThinkingDetailsshell withThinkBlock); rehydrate from metadata on history reload.Depends on graphon #180. Draft until the graphon release + pin bump lands — backend CI is blocked until then. Context: #37184.
Screenshots
Checklist
make lint && make type-check(backend) andcd web && pnpm exec vp staged(frontend) to appease the lint gods