mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 03:30:34 +00:00
* fix(frontend): render full content for multi-part AI messages
Gemini streams the first content block as a {type:text} object carrying
the thinking signature, then emits continuation deltas as bare strings.
The content extractors only kept {type:text} objects and dropped the
string parts, truncating each AI message to its first block.
Handle string elements in extractContentFromMessage, extractTextFromMessage,
and textOfMessage so the full message renders.
Fixes #1000
* test(frontend): cover multi-part AI message content extraction
Add a regression test for the #1000 truncation: an AI message whose
content is [{type:text, signature}, "bare string"] (Gemini's shape after
LangChain merge_content). Fails on main, passes with the extractor fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(frontend): cover textOfMessage multi-part content extraction
Add tests for the bare-string continuation case and the null-when-empty
contract, and document why textOfMessage joins flat ("") while the body
extractors join with "\n". Addresses review feedback on #3649.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
2.7 KiB
TypeScript
100 lines
2.7 KiB
TypeScript
import type { Message } from "@langchain/langgraph-sdk";
|
|
|
|
import type { AgentThread, AgentThreadContext } from "./types";
|
|
|
|
export type ChannelThreadSource = {
|
|
type: "im_channel";
|
|
provider: string;
|
|
label: string;
|
|
};
|
|
|
|
type ThreadRouteTarget =
|
|
| string
|
|
| {
|
|
thread_id: string;
|
|
context?: Pick<AgentThreadContext, "agent_name"> | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
};
|
|
|
|
export function pathOfThread(
|
|
thread: ThreadRouteTarget,
|
|
context?: Pick<AgentThreadContext, "agent_name"> | null,
|
|
) {
|
|
const threadId = typeof thread === "string" ? thread : thread.thread_id;
|
|
let agentName: string | undefined;
|
|
if (typeof thread === "string") {
|
|
agentName = context?.agent_name;
|
|
} else {
|
|
agentName = thread.context?.agent_name;
|
|
if (!agentName) {
|
|
const metaAgent = thread.metadata?.agent_name;
|
|
if (typeof metaAgent === "string") {
|
|
agentName = metaAgent;
|
|
}
|
|
}
|
|
}
|
|
|
|
return agentName
|
|
? `/workspace/agents/${encodeURIComponent(agentName)}/chats/${threadId}`
|
|
: `/workspace/chats/${threadId}`;
|
|
}
|
|
|
|
export function textOfMessage(message: Message) {
|
|
if (typeof message.content === "string") {
|
|
return message.content;
|
|
} else if (Array.isArray(message.content)) {
|
|
// Flat join ("") for single-line consumers (input box, titles); the rendered
|
|
// body uses extractContentFromMessage, which joins multi-part content with "\n".
|
|
const text = message.content
|
|
.map((part) =>
|
|
typeof part === "string" ? part : part.type === "text" ? part.text : "",
|
|
)
|
|
.join("");
|
|
return text.length > 0 ? text : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function titleOfThread(thread: AgentThread) {
|
|
return thread.values?.title ?? "Untitled";
|
|
}
|
|
|
|
const CHANNEL_PROVIDER_LABELS: Record<string, string> = {
|
|
dingtalk: "DingTalk",
|
|
discord: "Discord",
|
|
feishu: "Feishu",
|
|
slack: "Slack",
|
|
telegram: "Telegram",
|
|
wechat: "WeChat",
|
|
wecom: "WeCom",
|
|
};
|
|
|
|
function labelOfChannelProvider(provider: string) {
|
|
return CHANNEL_PROVIDER_LABELS[provider] ?? provider;
|
|
}
|
|
|
|
export function channelSourceOfThread(
|
|
thread: Pick<AgentThread, "metadata">,
|
|
): ChannelThreadSource | null {
|
|
const source = thread.metadata?.channel_source;
|
|
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
return null;
|
|
}
|
|
|
|
if (Reflect.get(source, "type") !== "im_channel") {
|
|
return null;
|
|
}
|
|
|
|
const provider = Reflect.get(source, "provider");
|
|
if (typeof provider !== "string" || provider.trim().length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedProvider = provider.trim().toLowerCase();
|
|
return {
|
|
type: "im_channel",
|
|
provider: normalizedProvider,
|
|
label: labelOfChannelProvider(normalizedProvider),
|
|
};
|
|
}
|