Merge branch 'master-nj' into 'master'
Master nj See merge request knowledge-base-bj/deerflow-code!23
This commit is contained in:
commit
96bfdacee5
6 files changed
+1912
-20
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1229,7 +1229,6 @@ export function InputBox({
|
|||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
||||||
{!frontendRagActive && agentSelector && (
|
{!frontendRagActive && agentSelector && (
|
||||||
{!frontendRagActive && knowledgeSelector}
|
|
||||||
<AgentSelector
|
<AgentSelector
|
||||||
selectedAgentId={agentSelector.selectedAgentId}
|
selectedAgentId={agentSelector.selectedAgentId}
|
||||||
onAgentChange={agentSelector.onAgentChange}
|
onAgentChange={agentSelector.onAgentChange}
|
||||||
@@ -1237,6 +1236,7 @@ export function InputBox({
|
|||||||
compact={compact}
|
compact={compact}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!frontendRagActive && knowledgeSelector}
|
||||||
{!frontendRagActive && promptPrefix && (
|
{!frontendRagActive && promptPrefix && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Message } from '@langchain/langgraph-sdk';
|
import type { Message } from '@langchain/langgraph-sdk';
|
||||||
import {
|
import {
|
||||||
|
BookMarkedIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
@@ -42,6 +43,11 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import {
|
import {
|
||||||
UploadedFilePreviewDialog,
|
UploadedFilePreviewDialog,
|
||||||
@@ -788,6 +794,12 @@ function MessageContent_({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{(isSingleRoundRag || isMultiRoundRag) && displaySources.length > 0 && (
|
||||||
|
<FrontendRagSourcesPanel
|
||||||
|
sources={displaySources}
|
||||||
|
onSelect={setSelectedCitation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MessageTokenUsage
|
<MessageTokenUsage
|
||||||
enabled={tokenUsageEnabled}
|
enabled={tokenUsageEnabled}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -809,6 +821,76 @@ function MessageContent_({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单轮/多轮 RAG 专属的「引用来源」折叠面板。
|
||||||
|
*
|
||||||
|
* 参考 GeneralQA 的引用来源区:默认折叠,列出全部参考资料,点击任一条打开详情
|
||||||
|
* 弹窗(复用 SingleRoundRagCitationDialog)。这样即使模型正文里没有 `[1]` 编号,
|
||||||
|
* 用户也能看到本次回答依据的资料。
|
||||||
|
*/
|
||||||
|
function FrontendRagSourcesPanel({
|
||||||
|
sources,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
sources: RuntimeDisplaySource[];
|
||||||
|
onSelect: (source: RuntimeDisplaySource) => void;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
if (sources.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<Collapsible open={open} onOpenChange={setOpen} className="mt-3 w-full">
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-xs transition-colors"
|
||||||
|
>
|
||||||
|
<BookMarkedIcon className="size-3.5" />
|
||||||
|
<span className="font-medium">引用来源</span>
|
||||||
|
<span className="text-muted-foreground/70">({sources.length})</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
'size-3.5 transition-transform duration-200',
|
||||||
|
!open && '-rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="mt-2 grid gap-2">
|
||||||
|
{sources.map((source, idx) => (
|
||||||
|
<button
|
||||||
|
key={source.id ?? `${source.title}-${idx}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(source)}
|
||||||
|
className="bg-card hover:bg-accent block w-full cursor-pointer rounded-lg border p-3 text-left transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="bg-primary/10 text-primary flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
|
||||||
|
{source.index}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-foreground line-clamp-1 min-w-0 flex-1 text-sm font-medium"
|
||||||
|
title={source.title}
|
||||||
|
>
|
||||||
|
{source.title}
|
||||||
|
</span>
|
||||||
|
{source.type && (
|
||||||
|
<span className="bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-xs">
|
||||||
|
{source.type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{source.snippet && (
|
||||||
|
<p className="text-muted-foreground mt-1.5 line-clamp-2 text-xs leading-relaxed">
|
||||||
|
{source.snippet}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SingleRoundRagCitationDialog({
|
function SingleRoundRagCitationDialog({
|
||||||
source,
|
source,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { useSkills } from "@/core/skills/hooks";
|
|||||||
import {
|
import {
|
||||||
buildSkillDisplayMap,
|
buildSkillDisplayMap,
|
||||||
collectRuntimeDisplaySources,
|
collectRuntimeDisplaySources,
|
||||||
|
type RuntimeDisplaySource,
|
||||||
} from "@/core/skill-display/runtime";
|
} from "@/core/skill-display/runtime";
|
||||||
import type { Subtask } from "@/core/tasks";
|
import type { Subtask } from "@/core/tasks";
|
||||||
import { useUpdateSubtask } from "@/core/tasks/context";
|
import { useUpdateSubtask } from "@/core/tasks/context";
|
||||||
@@ -259,6 +260,53 @@ export function MessageList({
|
|||||||
// frontend tool-message scan below comes up empty (e.g. after a reload).
|
// frontend tool-message scan below comes up empty (e.g. after a reload).
|
||||||
const referenceBatches = useContext(ReferenceBatchesContext);
|
const referenceBatches = useContext(ReferenceBatchesContext);
|
||||||
|
|
||||||
|
// 单轮/多轮 RAG 的参考资料存放在一条 hide_from_ui 的 tool 消息里(紧挨在
|
||||||
|
// assistant 消息之前)。allMessages 会把 hide_from_ui 消息过滤掉,导致通用
|
||||||
|
// 的 collectRuntimeDisplaySources 扫描不到它们。这里直接在原始消息流里按
|
||||||
|
// assistant 消息提取其对应的参考资料,保证 `[1]` 等编号能解析成可点击引用。
|
||||||
|
const frontendRagSourcesByAiId = useMemo(() => {
|
||||||
|
const map = new Map<string, RuntimeDisplaySource[]>();
|
||||||
|
const raw = thread.messages;
|
||||||
|
for (let i = 0; i < raw.length; i++) {
|
||||||
|
const msg = raw[i];
|
||||||
|
if (!msg || msg.type !== "ai" || !msg.id) continue;
|
||||||
|
const kwargs = msg.additional_kwargs;
|
||||||
|
if (!kwargs || (kwargs.single_round_rag !== true && kwargs.multi_round_rag !== true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sources: RuntimeDisplaySource[] = [];
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
const prev = raw[j];
|
||||||
|
if (!prev) break;
|
||||||
|
// 前一条 human/ai 消息即为本轮边界,跨边界即停止。
|
||||||
|
if (prev.type === "human" || prev.type === "ai") break;
|
||||||
|
if (prev.type !== "tool") continue;
|
||||||
|
const prevKwargs = prev.additional_kwargs;
|
||||||
|
if (
|
||||||
|
!prevKwargs ||
|
||||||
|
(prevKwargs.single_round_rag !== true && prevKwargs.multi_round_rag !== true)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const results = extractFrontendRagResults(prev.content);
|
||||||
|
if (results && results.length > 0) {
|
||||||
|
const toolName = String(
|
||||||
|
(prev as { name?: unknown }).name ??
|
||||||
|
(prevKwargs.multi_round_rag === true
|
||||||
|
? "multi_round_rag_references"
|
||||||
|
: "knowledge_base_search"),
|
||||||
|
);
|
||||||
|
sources = results.map((item, index) =>
|
||||||
|
toRuntimeDisplaySource(item, index + 1, toolName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
map.set(msg.id, sources);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [thread.messages]);
|
||||||
|
|
||||||
// Track which clarification messages are still awaiting an answer.
|
// Track which clarification messages are still awaiting an answer.
|
||||||
// Pending = clarification tool messages that appear after the last human message
|
// Pending = clarification tool messages that appear after the last human message
|
||||||
// (no user reply has been sent for them yet).
|
// (no user reply has been sent for them yet).
|
||||||
@@ -392,14 +440,21 @@ export function MessageList({
|
|||||||
(batch) => batch.assistant_message_id === msg.id,
|
(batch) => batch.assistant_message_id === msg.id,
|
||||||
)?.sources ?? []
|
)?.sources ?? []
|
||||||
: [];
|
: [];
|
||||||
|
// 单轮/多轮 RAG 的参考资料来自隐藏的 tool 消息,优先于通用扫描。
|
||||||
|
const frontendRagSources =
|
||||||
|
isAssistantMessage && msg.id
|
||||||
|
? frontendRagSourcesByAiId.get(msg.id) ?? []
|
||||||
|
: [];
|
||||||
const displaySources = isAssistantMessage
|
const displaySources = isAssistantMessage
|
||||||
? batchSources.length > 0
|
? batchSources.length > 0
|
||||||
? batchSources
|
? batchSources
|
||||||
: collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
: frontendRagSources.length > 0
|
||||||
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
|
? frontendRagSources
|
||||||
beforeMessageId: msg.id,
|
: collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
||||||
includeBeforeMessage: true,
|
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
|
||||||
})
|
beforeMessageId: msg.id,
|
||||||
|
includeBeforeMessage: true,
|
||||||
|
})
|
||||||
: [];
|
: [];
|
||||||
return (
|
return (
|
||||||
<MessageListItem
|
<MessageListItem
|
||||||
@@ -728,6 +783,46 @@ function findPreviousHumanMessageId(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从单轮/多轮 RAG 的 tool 消息体里取出 `content.results`。 */
|
||||||
|
function extractFrontendRagResults(content: unknown): unknown[] | null {
|
||||||
|
if (content && typeof content === "object" && !Array.isArray(content)) {
|
||||||
|
const results = (content as { results?: unknown }).results;
|
||||||
|
if (Array.isArray(results)) return results;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把单轮/多轮 RAG 的一条参考资料归一化为展示用的 RuntimeDisplaySource。 */
|
||||||
|
function toRuntimeDisplaySource(
|
||||||
|
item: unknown,
|
||||||
|
index: number,
|
||||||
|
skillName: string,
|
||||||
|
): RuntimeDisplaySource {
|
||||||
|
const record =
|
||||||
|
item && typeof item === "object" && !Array.isArray(item)
|
||||||
|
? (item as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
const raw =
|
||||||
|
(record.raw as Record<string, unknown> | undefined) ?? record;
|
||||||
|
const content = String(record.content ?? record.snippet ?? "");
|
||||||
|
return {
|
||||||
|
id: typeof record.id === "string" && record.id ? record.id : undefined,
|
||||||
|
title: String(record.title ?? `参考资料 ${index}`),
|
||||||
|
type: String(record.type ?? "result"),
|
||||||
|
snippet: String(record.snippet ?? content),
|
||||||
|
content,
|
||||||
|
source:
|
||||||
|
typeof record.source === "string" && record.source ? record.source : undefined,
|
||||||
|
author: undefined,
|
||||||
|
time: typeof record.time === "string" && record.time ? record.time : undefined,
|
||||||
|
url: typeof record.url === "string" && record.url ? record.url : undefined,
|
||||||
|
raw,
|
||||||
|
index,
|
||||||
|
skillName,
|
||||||
|
mode: "citation",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getSetupAgentApprovalPayload(
|
function getSetupAgentApprovalPayload(
|
||||||
message: unknown,
|
message: unknown,
|
||||||
): SetupAgentApprovalPayload | null {
|
): SetupAgentApprovalPayload | null {
|
||||||
|
|||||||
@@ -815,11 +815,7 @@ export default function ChatPage() {
|
|||||||
: "ready"
|
: "ready"
|
||||||
}
|
}
|
||||||
context={settings.context}
|
context={settings.context}
|
||||||
extraModels={
|
extraModels={SINGLE_ROUND_RAG_MODELS}
|
||||||
singleRoundRagSelected
|
|
||||||
? SINGLE_ROUND_RAG_MODELS
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={
|
disabled={
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||||
isUploading ||
|
isUploading ||
|
||||||
|
|||||||
@@ -364,8 +364,7 @@ auth_login:
|
|||||||
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
|
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
|
||||||
|
|
||||||
database:
|
database:
|
||||||
backend: postgres
|
backend: sqlite
|
||||||
postgres_url: $POSTGRES_DATABASE_URL
|
|
||||||
sqlite_dir: .deer-flow/data
|
sqlite_dir: .deer-flow/data
|
||||||
pool_size: 20 # 基础连接池(postgres/mysql 生效;sqlite 忽略)
|
pool_size: 20 # 基础连接池(postgres/mysql 生效;sqlite 忽略)
|
||||||
max_overflow: 20 # 高峰期可临时超出 pool_size 的额外连接数,真实上限 = pool_size + max_overflow
|
max_overflow: 20 # 高峰期可临时超出 pool_size 的额外连接数,真实上限 = pool_size + max_overflow
|
||||||
@@ -374,13 +373,8 @@ database:
|
|||||||
echo_sql: false
|
echo_sql: false
|
||||||
|
|
||||||
checkpointer:
|
checkpointer:
|
||||||
type: postgres
|
type: sqlite
|
||||||
connection_string: $POSTGRES_CHECKPOINT_URL
|
connection_string: .deer-flow/data/deerflow.db
|
||||||
use_postgres: true
|
|
||||||
# PostgreSQL 优先;SQLite 仅作历史记录读取兜底,确认稳定后可移除。
|
|
||||||
dual_read_enabled: true
|
|
||||||
dual_read_sqlite_paths:
|
|
||||||
- .deer-flow/data/deerflow.db
|
|
||||||
|
|
||||||
# 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。
|
# 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。
|
||||||
# 三层覆盖优先级(高->低):
|
# 三层覆盖优先级(高->低):
|
||||||
|
|||||||
Reference in new issue
Block a user