forked from bytedance/deer-flow
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtranscripts.py
More file actions
142 lines (112 loc) · 4.99 KB
/
Copy pathtranscripts.py
File metadata and controls
142 lines (112 loc) · 4.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""Canonical chat transcript storage.
The LangGraph checkpoint state is model context. It can be summarized,
trimmed, or otherwise rewritten by middlewares. The UI transcript needs a
separate durable record so conversation history survives context compression.
"""
from __future__ import annotations
import json
import time
from typing import Any
from deerflow.runtime import serialize_lc_object
TRANSCRIPTS_NS: tuple[str, ...] = ("thread_transcripts",)
_SUMMARY_MARKER_KEY = "deerflow_conversation_summary"
_LEGACY_SUMMARY_PREFIX = "Here is a summary of the conversation to date:"
def _message_fingerprint(message: dict[str, Any]) -> str:
"""Return a stable identity for messages that do not have ids yet."""
identity_payload = {
"type": message.get("type"),
"name": message.get("name"),
"tool_call_id": message.get("tool_call_id"),
"content": message.get("content"),
}
return json.dumps(identity_payload, sort_keys=True, default=str, ensure_ascii=False)
def _message_text(message: dict[str, Any]) -> str:
content = message.get("content")
if isinstance(content, str):
return content.strip()
if isinstance(content, list):
parts: list[str] = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
text = item.get("text")
if isinstance(text, str):
parts.append(text)
return "\n".join(parts).strip()
return ""
def _is_visible_transcript_message(message: dict[str, Any]) -> bool:
additional_kwargs = message.get("additional_kwargs")
if not isinstance(additional_kwargs, dict):
additional_kwargs = {}
if additional_kwargs.get("hide_from_ui") is True:
return False
if additional_kwargs.get(_SUMMARY_MARKER_KEY) is True:
return False
# Backward compatibility for summary messages created before they were
# explicitly tagged by DeerFlowSummarizationMiddleware.
if message.get("type") == "human" and _message_text(message).startswith(_LEGACY_SUMMARY_PREFIX):
return False
return message.get("type") in {"human", "ai", "tool"}
def normalize_transcript_messages(messages: list[Any] | tuple[Any, ...] | None) -> list[dict[str, Any]]:
"""Serialize and filter messages before writing them to the transcript."""
normalized: list[dict[str, Any]] = []
for raw_message in messages or []:
message = serialize_lc_object(raw_message)
if isinstance(message, dict) and _is_visible_transcript_message(message):
normalized.append(message)
return normalized
async def get_thread_transcript(store: Any, thread_id: str) -> list[dict[str, Any]]:
"""Read the canonical transcript for *thread_id* from the Store."""
item = await store.aget(TRANSCRIPTS_NS, thread_id)
if item is None:
return []
value = item.value if isinstance(item.value, dict) else {}
messages = value.get("messages", [])
return messages if isinstance(messages, list) else []
async def append_thread_transcript_messages(
store: Any,
thread_id: str,
messages: list[Any] | tuple[Any, ...] | None,
) -> list[dict[str, Any]]:
"""Append visible messages to the canonical transcript, deduplicating by identity."""
incoming = normalize_transcript_messages(messages)
if not incoming:
return await get_thread_transcript(store, thread_id)
existing = await get_thread_transcript(store, thread_id)
seen_ids = {str(message["id"]) for message in existing if isinstance(message, dict) and message.get("id")}
unidentified_by_fingerprint = {_message_fingerprint(message): index for index, message in enumerate(existing) if isinstance(message, dict) and not message.get("id")}
changed = False
for message in incoming:
message_id = message.get("id")
fingerprint = _message_fingerprint(message)
if message_id and str(message_id) in seen_ids:
continue
unidentified_index = unidentified_by_fingerprint.get(fingerprint)
if message_id and unidentified_index is not None:
existing[unidentified_index] = message
seen_ids.add(str(message_id))
del unidentified_by_fingerprint[fingerprint]
changed = True
continue
if message_id:
seen_ids.add(str(message_id))
else:
unidentified_by_fingerprint.setdefault(fingerprint, len(existing))
existing.append(message)
changed = True
if changed:
await store.aput(
TRANSCRIPTS_NS,
thread_id,
{
"thread_id": thread_id,
"messages": existing,
"updated_at": time.time(),
},
)
return existing
async def delete_thread_transcript(store: Any, thread_id: str) -> None:
"""Delete a thread transcript if the active Store supports deletion."""
delete = getattr(store, "adelete", None)
if delete is None:
return
await delete(TRANSCRIPTS_NS, thread_id)