mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 06:11:12 +08:00
Adopt main's generated oRPC console calls (#44169) in branch code: the deleted use-dataset/use-datasource/useMembers hooks become consoleQuery queries, the KnowledgeFS dataset list, scope dialog and card header keep their branch behavior, and branch specs mock the new operations instead of the removed modules. Contracts, OpenAPI markdown and oxlint suppressions are regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
470 lines
15 KiB
Python
470 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from datetime import datetime
|
|
from typing import Annotated, Any
|
|
|
|
from pydantic import Field, WithJsonSchema, field_validator, model_validator
|
|
|
|
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
|
|
from fields.base import ResponseModel, SessionResponseSource
|
|
from graphon.file import File
|
|
from libs.helper import to_timestamp
|
|
from models.account import Account
|
|
from models.model import (
|
|
AppModelConfigDict,
|
|
MessageAgentThought,
|
|
MessageAnnotation,
|
|
MessageFeedback,
|
|
MessageFileInfo,
|
|
)
|
|
from models.model import (
|
|
Conversation as ConversationModel,
|
|
)
|
|
from models.model import (
|
|
Message as MessageModel,
|
|
)
|
|
|
|
type AnyJSONValue = Any
|
|
UUIDString = Annotated[str, WithJsonSchema({"format": "uuid", "type": "string"})]
|
|
Int64 = Annotated[int, WithJsonSchema({"format": "int64", "type": "integer"})]
|
|
URLString = Annotated[str, WithJsonSchema({"format": "url", "type": "string"})]
|
|
|
|
|
|
class _FeedbackResponseSource(SessionResponseSource[MessageFeedback]):
|
|
@property
|
|
def from_account(self) -> Account | None:
|
|
return self._source.from_account_with_session(session=self._session)
|
|
|
|
|
|
class _AnnotationResponseSource(SessionResponseSource[MessageAnnotation]):
|
|
@property
|
|
def account(self) -> Account | None:
|
|
return self._source.account_with_session(session=self._session)
|
|
|
|
@property
|
|
def annotation_create_account(self) -> Account | None:
|
|
return self._source.annotation_create_account_with_session(session=self._session)
|
|
|
|
|
|
class MessageResponseSource(SessionResponseSource[MessageModel]):
|
|
@property
|
|
def inputs(self) -> dict[str, Any]:
|
|
return self._source.inputs_with_session(session=self._session)
|
|
|
|
@property
|
|
def feedbacks(self) -> list[_FeedbackResponseSource]:
|
|
return [
|
|
_FeedbackResponseSource(feedback, session=self._session)
|
|
for feedback in self._source.feedbacks_with_session(session=self._session)
|
|
]
|
|
|
|
@property
|
|
def user_feedback(self) -> MessageFeedback | None:
|
|
return self._source.user_feedback_with_session(session=self._session)
|
|
|
|
@property
|
|
def annotation(self) -> _AnnotationResponseSource | None:
|
|
annotation = self._source.annotation_with_session(session=self._session)
|
|
return _AnnotationResponseSource(annotation, session=self._session) if annotation else None
|
|
|
|
@property
|
|
def annotation_hit_history(self) -> _AnnotationResponseSource | None:
|
|
annotation = self._source.annotation_hit_history_with_session(session=self._session)
|
|
return _AnnotationResponseSource(annotation, session=self._session) if annotation else None
|
|
|
|
@property
|
|
def agent_thoughts(self) -> Sequence[MessageAgentThought]:
|
|
return self._source.agent_thoughts_with_session(session=self._session)
|
|
|
|
@property
|
|
def message_files(self) -> list[MessageFileInfo]:
|
|
return self._source.message_files_with_session(session=self._session)
|
|
|
|
|
|
class ConversationResponseSource(SessionResponseSource[ConversationModel]):
|
|
@property
|
|
def inputs(self) -> dict[str, Any]:
|
|
return self._source.inputs_with_session(session=self._session)
|
|
|
|
@property
|
|
def model_config(self) -> AppModelConfigDict:
|
|
return self._source.model_config_with_session(session=self._session)
|
|
|
|
@property
|
|
def summary_or_query(self) -> str:
|
|
return self._source.summary_or_query_with_session(session=self._session)
|
|
|
|
@property
|
|
def annotated(self) -> bool:
|
|
return self._source.annotated_with_session(session=self._session)
|
|
|
|
@property
|
|
def annotation(self) -> _AnnotationResponseSource | None:
|
|
annotation = self._source.annotation_with_session(session=self._session)
|
|
return _AnnotationResponseSource(annotation, session=self._session) if annotation else None
|
|
|
|
@property
|
|
def message_count(self) -> int:
|
|
return self._source.message_count_with_session(session=self._session)
|
|
|
|
@property
|
|
def user_feedback_stats(self) -> dict[str, int]:
|
|
return self._source.user_feedback_stats_with_session(session=self._session)
|
|
|
|
@property
|
|
def admin_feedback_stats(self) -> dict[str, int]:
|
|
return self._source.admin_feedback_stats_with_session(session=self._session)
|
|
|
|
@property
|
|
def status_count(self) -> dict[str, int] | None:
|
|
return self._source.status_count_with_session(session=self._session)
|
|
|
|
@property
|
|
def first_message(self) -> MessageResponseSource | None:
|
|
message = self._source.first_message_with_session(session=self._session)
|
|
return MessageResponseSource(message, session=self._session) if message else None
|
|
|
|
@property
|
|
def from_end_user_session_id(self) -> str | None:
|
|
return self._source.from_end_user_session_id_with_session(session=self._session)
|
|
|
|
@property
|
|
def from_account_name(self) -> str | None:
|
|
return self._source.from_account_name_with_session(session=self._session)
|
|
|
|
|
|
class MessageFile(ResponseModel):
|
|
id: UUIDString
|
|
filename: str
|
|
type: str
|
|
url: URLString | None = None
|
|
mime_type: str | None = None
|
|
size: int | None = None
|
|
transfer_method: str
|
|
belongs_to: str | None = None
|
|
upload_file_id: UUIDString | None = None
|
|
|
|
@field_validator("transfer_method", mode="before")
|
|
@classmethod
|
|
def _normalize_transfer_method(cls, value: object) -> str:
|
|
if isinstance(value, str):
|
|
return value
|
|
return str(value)
|
|
|
|
|
|
class SimpleConversation(ResponseModel):
|
|
id: UUIDString
|
|
name: str
|
|
inputs: dict[str, AnyJSONValue]
|
|
status: str
|
|
introduction: str | None = None
|
|
created_at: Int64 | None = None
|
|
updated_at: Int64 | None = None
|
|
|
|
@field_validator("inputs", mode="before")
|
|
@classmethod
|
|
def _normalize_inputs(cls, value: AnyJSONValue) -> AnyJSONValue:
|
|
return format_files_contained(value)
|
|
|
|
@field_validator("created_at", "updated_at", mode="before")
|
|
@classmethod
|
|
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
|
|
return to_timestamp(value)
|
|
|
|
|
|
class ConversationInfiniteScrollPagination(ResponseModel):
|
|
limit: int
|
|
has_more: bool
|
|
data: list[SimpleConversation]
|
|
|
|
|
|
class ConversationDelete(ResponseModel):
|
|
result: str
|
|
|
|
|
|
class ResultResponse(ResponseModel):
|
|
result: str
|
|
|
|
|
|
class SimpleAccount(ResponseModel):
|
|
id: str
|
|
name: str
|
|
email: str
|
|
|
|
|
|
class Feedback(ResponseModel):
|
|
rating: str
|
|
content: str | None = None
|
|
from_source: str
|
|
from_end_user_id: str | None = None
|
|
from_account: SimpleAccount | None = None
|
|
|
|
|
|
class ConversationAnnotation(ResponseModel):
|
|
id: str
|
|
question: str | None = None
|
|
content: str
|
|
account: SimpleAccount | 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 ConversationAnnotationHitHistory(ResponseModel):
|
|
annotation_id: str = Field(validation_alias="id")
|
|
annotation_create_account: SimpleAccount | 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 AgentThought(ResponseModel):
|
|
id: str
|
|
chain_id: str | None = None
|
|
message_chain_id: str | None = Field(default=None, exclude=True, validation_alias="message_chain_id")
|
|
message_id: str
|
|
position: int
|
|
thought: str | None = None
|
|
answer: str | None = None
|
|
tool: str | None = None
|
|
tool_labels: AnyJSONValue = Field(description="Labels for tools used.")
|
|
tool_input: str | None = None
|
|
created_at: int | None = None
|
|
observation: str | None = None
|
|
files: list[str]
|
|
|
|
@field_validator("created_at", mode="before")
|
|
@classmethod
|
|
def _normalize_created_at(cls, value: datetime | int | None) -> int | None:
|
|
return to_timestamp(value)
|
|
|
|
@model_validator(mode="after")
|
|
def _fallback_chain_id(self):
|
|
if self.chain_id is None and self.message_chain_id:
|
|
self.chain_id = self.message_chain_id
|
|
return self
|
|
|
|
|
|
class MessageDetail(ResponseModel):
|
|
retriever_resources: list[RetrievalSourceMetadata] = Field(default_factory=list)
|
|
id: str
|
|
conversation_id: str
|
|
inputs: dict[str, AnyJSONValue]
|
|
query: str
|
|
message: AnyJSONValue
|
|
message_tokens: int
|
|
answer: str = Field(validation_alias="re_sign_file_url_answer")
|
|
answer_tokens: int
|
|
provider_response_latency: float
|
|
from_source: str
|
|
from_end_user_id: str | None = None
|
|
from_account_id: str | None = None
|
|
feedbacks: list[Feedback]
|
|
workflow_run_id: str | None = None
|
|
annotation: ConversationAnnotation | None = None
|
|
annotation_hit_history: ConversationAnnotationHitHistory | None = None
|
|
created_at: int | None = None
|
|
agent_thoughts: list[AgentThought]
|
|
message_files: list[MessageFile]
|
|
metadata: AnyJSONValue = Field(validation_alias="message_metadata_dict")
|
|
status: str
|
|
error: str | None = None
|
|
parent_message_id: str | None = None
|
|
|
|
@field_validator("inputs", mode="before")
|
|
@classmethod
|
|
def _normalize_inputs(cls, value: AnyJSONValue) -> AnyJSONValue:
|
|
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 FeedbackStat(ResponseModel):
|
|
like: int
|
|
dislike: int
|
|
|
|
|
|
class StatusCount(ResponseModel):
|
|
success: int
|
|
failed: int
|
|
partial_success: int
|
|
paused: int
|
|
|
|
|
|
class ConversationModelConfig(ResponseModel):
|
|
opening_statement: str | None = None
|
|
suggested_questions: AnyJSONValue | None = Field(default=None)
|
|
model: AnyJSONValue | None = Field(default=None)
|
|
user_input_form: AnyJSONValue | None = Field(default=None)
|
|
pre_prompt: str | None = None
|
|
agent_mode: AnyJSONValue | None = Field(default=None)
|
|
|
|
|
|
class SimpleModelConfig(ResponseModel):
|
|
model: AnyJSONValue | None = Field(
|
|
default=None,
|
|
validation_alias="model_dict",
|
|
)
|
|
pre_prompt: str | None = None
|
|
|
|
|
|
class SimpleMessageDetail(ResponseModel):
|
|
inputs: dict[str, AnyJSONValue]
|
|
query: str
|
|
message: str
|
|
answer: str
|
|
|
|
@field_validator("inputs", mode="before")
|
|
@classmethod
|
|
def _normalize_inputs(cls, value: AnyJSONValue) -> AnyJSONValue:
|
|
return format_files_contained(value)
|
|
|
|
@field_validator("message", mode="before")
|
|
@classmethod
|
|
def _normalize_message(cls, value: AnyJSONValue) -> str:
|
|
return message_text(value)
|
|
|
|
|
|
class Conversation(ResponseModel):
|
|
id: str
|
|
status: str
|
|
from_source: str
|
|
from_end_user_id: str | None = None
|
|
from_end_user_session_id: str | None = None
|
|
from_account_id: str | None = None
|
|
from_account_name: str | None = None
|
|
read_at: int | None = None
|
|
created_at: int | None = None
|
|
updated_at: int | None = None
|
|
annotation: ConversationAnnotation | None = None
|
|
model_config_: SimpleModelConfig | None = Field(default=None, alias="model_config")
|
|
user_feedback_stats: FeedbackStat | None = None
|
|
admin_feedback_stats: FeedbackStat | None = None
|
|
message: SimpleMessageDetail | None = Field(default=None, validation_alias="first_message")
|
|
|
|
@field_validator("read_at", "created_at", "updated_at", mode="before")
|
|
@classmethod
|
|
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
|
|
return to_timestamp(value)
|
|
|
|
|
|
class ConversationPagination(ResponseModel):
|
|
page: int
|
|
limit: int = Field(validation_alias="per_page")
|
|
total: int
|
|
has_more: bool = Field(validation_alias="has_next")
|
|
data: list[Conversation] = Field(validation_alias="items")
|
|
|
|
|
|
class ConversationMessageDetail(ResponseModel):
|
|
id: str
|
|
status: str
|
|
from_source: str
|
|
from_end_user_id: str | None = None
|
|
from_account_id: str | None = None
|
|
created_at: int | None = None
|
|
model_config_: ConversationModelConfig | None = Field(default=None, alias="model_config")
|
|
message: MessageDetail | None = Field(default=None, validation_alias="first_message")
|
|
|
|
@field_validator("created_at", mode="before")
|
|
@classmethod
|
|
def _normalize_created_at(cls, value: datetime | int | None) -> int | None:
|
|
return to_timestamp(value)
|
|
|
|
|
|
class ConversationWithSummary(ResponseModel):
|
|
id: str
|
|
status: str
|
|
from_source: str
|
|
from_end_user_id: str | None = None
|
|
from_end_user_session_id: str | None = None
|
|
from_account_id: str | None = None
|
|
from_account_name: str | None = None
|
|
name: str
|
|
summary: str = Field(validation_alias="summary_or_query")
|
|
read_at: int | None = None
|
|
created_at: int | None = None
|
|
updated_at: int | None = None
|
|
annotated: bool
|
|
model_config_: SimpleModelConfig | None = Field(default=None, alias="model_config")
|
|
message_count: int
|
|
user_feedback_stats: FeedbackStat | None = None
|
|
admin_feedback_stats: FeedbackStat | None = None
|
|
status_count: StatusCount | None = None
|
|
|
|
@field_validator("read_at", "created_at", "updated_at", mode="before")
|
|
@classmethod
|
|
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
|
|
return to_timestamp(value)
|
|
|
|
|
|
class ConversationWithSummaryPagination(ResponseModel):
|
|
page: int
|
|
limit: int = Field(validation_alias="per_page")
|
|
total: int
|
|
has_more: bool = Field(validation_alias="has_next")
|
|
data: list[ConversationWithSummary] = Field(validation_alias="items")
|
|
|
|
|
|
class ConversationDetail(ResponseModel):
|
|
id: str
|
|
status: str
|
|
from_source: str
|
|
from_end_user_id: str | None = None
|
|
from_account_id: str | None = None
|
|
created_at: int | None = None
|
|
updated_at: int | None = None
|
|
annotated: bool
|
|
introduction: str | None = None
|
|
model_config_: ConversationModelConfig | None = Field(default=None, alias="model_config")
|
|
message_count: int
|
|
user_feedback_stats: FeedbackStat | None = None
|
|
admin_feedback_stats: FeedbackStat | None = None
|
|
|
|
@field_validator("created_at", "updated_at", mode="before")
|
|
@classmethod
|
|
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
|
|
return to_timestamp(value)
|
|
|
|
|
|
def format_files_contained(value: AnyJSONValue) -> AnyJSONValue:
|
|
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
|
|
|
|
|
|
def message_text(value: AnyJSONValue) -> str:
|
|
if isinstance(value, list) and value:
|
|
first = value[0]
|
|
if isinstance(first, dict):
|
|
text = first.get("text")
|
|
if isinstance(text, str):
|
|
return text
|
|
return ""
|
|
|
|
|
|
def extract_model_config(value: object | None) -> dict[str, AnyJSONValue]:
|
|
if value is None:
|
|
return {}
|
|
if isinstance(value, dict):
|
|
return value
|
|
if hasattr(value, "to_dict"):
|
|
return value.to_dict()
|
|
return {}
|