Files
dify/api/fields/message_fields.py
L1nSn0w 52ef3f2367 fix(chatflow): correct reasoning streaming, explore rehydration, iteration 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).
2026-06-23 14:35:52 +08:00

145 lines
4.3 KiB
Python

from __future__ import annotations
from datetime import datetime
from uuid import uuid4
from pydantic import Field, field_validator
from core.entities.execution_extra_content import ExecutionExtraContentDomainModel
from fields.base import ResponseModel
from fields.conversation_fields import AgentThought, JSONValue, MessageFile
from graphon.file import File
from libs.helper import to_timestamp
type JSONValueType = JSONValue
class SimpleFeedback(ResponseModel):
rating: str | None = None
class RetrieverResource(ResponseModel):
id: str = Field(default_factory=lambda: str(uuid4()))
message_id: str = Field(default_factory=lambda: str(uuid4()))
position: int
dataset_id: str | None = None
dataset_name: str | None = None
document_id: str | None = None
document_name: str | None = None
data_source_type: str | None = None
segment_id: str | None = None
score: float | None = None
hit_count: int | None = None
word_count: int | None = None
segment_position: int | None = None
index_node_hash: str | None = None
content: str | None = None
summary: str | None = None
created_at: int | None = None
@field_validator("created_at", mode="before")
@classmethod
def _normalize_created_at(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class MessageListItem(ResponseModel):
id: str
conversation_id: str
parent_message_id: str | None = None
inputs: dict[str, JSONValueType]
query: str
answer: str = Field(validation_alias="re_sign_file_url_answer")
feedback: SimpleFeedback | None = Field(default=None, validation_alias="user_feedback")
retriever_resources: list[RetrieverResource]
created_at: int | None = None
agent_thoughts: list[AgentThought]
message_files: list[MessageFile]
status: str
error: str | None = None
extra_contents: list[ExecutionExtraContentDomainModel]
@field_validator("inputs", mode="before")
@classmethod
def _normalize_inputs(cls, value: JSONValueType) -> JSONValueType:
return format_files_contained(value)
@field_validator("created_at", mode="before")
@classmethod
def _normalize_created_at(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class WebMessageListItem(MessageListItem):
metadata: JSONValueType | None = Field(
default=None,
validation_alias="message_metadata_dict",
)
class ExploreMessageListItem(MessageListItem):
metadata: JSONValueType | None = Field(
default=None,
validation_alias="message_metadata_dict",
)
class MessageInfiniteScrollPagination(ResponseModel):
limit: int
has_more: bool
data: list[MessageListItem]
class WebMessageInfiniteScrollPagination(ResponseModel):
limit: int
has_more: bool
data: list[WebMessageListItem]
class ExploreMessageInfiniteScrollPagination(ResponseModel):
limit: int
has_more: bool
data: list[ExploreMessageListItem]
class SavedMessageItem(ResponseModel):
id: str
inputs: dict[str, JSONValueType]
query: str
answer: str
message_files: list[MessageFile]
feedback: SimpleFeedback | None = Field(default=None, validation_alias="user_feedback")
created_at: int | None = None
@field_validator("inputs", mode="before")
@classmethod
def _normalize_inputs(cls, value: JSONValueType) -> JSONValueType:
return format_files_contained(value)
@field_validator("created_at", mode="before")
@classmethod
def _normalize_created_at(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class SavedMessageInfiniteScrollPagination(ResponseModel):
limit: int
has_more: bool
data: list[SavedMessageItem]
class SuggestedQuestionsResponse(ResponseModel):
data: list[str]
def format_files_contained(value: JSONValueType) -> JSONValueType:
if isinstance(value, File):
# Response payloads must preserve legacy file keys like `related_id`/`url`
# while still exposing the new graph-layer `reference` field.
return value.to_dict()
if isinstance(value, dict):
return {k: format_files_contained(v) for k, v in value.items()}
if isinstance(value, list):
return [format_files_contained(v) for v in value]
return value