mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 11:06:18 +00:00
* feat(knowledge): add verifiable RAGFlow source citations * docs(knowledge): scope RAGFlow guidance to its own directory * fix(knowledge): preserve citations through rendering and budgets
101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import type { Message } from "@langchain/langgraph-sdk";
|
|
import { describe, expect, it } from "@rstest/core";
|
|
|
|
import {
|
|
citedKnowledgeSources,
|
|
collectKnowledgeSources,
|
|
knowledgeSourceId,
|
|
} from "@/core/knowledge/sources";
|
|
|
|
const id = "0123456789abcdef0123456789abcdef-1";
|
|
const source = {
|
|
id,
|
|
provider: "ragflow",
|
|
document_name: "Manual.pdf",
|
|
dataset_name: "Engineering",
|
|
text: "Limit: 42.",
|
|
truncated: false,
|
|
pages: [3],
|
|
};
|
|
const message = {
|
|
type: "tool",
|
|
name: "knowledge_search",
|
|
content: "retrieved",
|
|
tool_call_id: "call",
|
|
artifact: { knowledge_sources: { version: 1, sources: [source] } },
|
|
} as unknown as Message;
|
|
|
|
describe("knowledge citation provenance", () => {
|
|
it("resolves repeated citations to the persisted tool evidence after JSON reload", () => {
|
|
const messages = JSON.parse(JSON.stringify([message])) as Message[];
|
|
const sources = collectKnowledgeSources(messages);
|
|
const citation = `[citation:1](#knowledge-${id})`;
|
|
expect(citedKnowledgeSources(`${citation} ${citation}`, sources)).toEqual([
|
|
source,
|
|
]);
|
|
expect(knowledgeSourceId(`#knowledge-${id}`)).toBe(id);
|
|
expect(knowledgeSourceId(`#user-content-knowledge-${id}`)).toBe(id);
|
|
});
|
|
it("does not let model labels or human artifacts invent a source", () => {
|
|
const sources = collectKnowledgeSources([
|
|
{ ...message, type: "human" } as Message,
|
|
{ ...message, type: "ai" } as Message,
|
|
]);
|
|
expect(sources.size).toBe(0);
|
|
expect(
|
|
citedKnowledgeSources(`[citation:invented](#knowledge-${id})`, sources),
|
|
).toEqual([]);
|
|
expect(knowledgeSourceId(`https://evil.test/#knowledge-${id}`)).toBeNull();
|
|
});
|
|
it("ignores citations in code and images", () => {
|
|
const link = `[citation:1](#knowledge-${id})`;
|
|
expect(
|
|
citedKnowledgeSources(
|
|
`\`${link}\`\n\n\`\`\`\n${link}\n\`\`\`\n!${link}`,
|
|
collectKnowledgeSources([message]),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
it("rejects malformed and unknown artifact versions", () => {
|
|
for (const payload of [
|
|
{ version: 2, sources: [source] },
|
|
{ version: 1, sources: [{ ...source, pages: [-1] }] },
|
|
{ version: 1, sources: [{ ...source, text: 42 }] },
|
|
]) {
|
|
const bad = {
|
|
...message,
|
|
artifact: { knowledge_sources: payload },
|
|
} as Message;
|
|
expect(collectKnowledgeSources([bad]).size).toBe(0);
|
|
}
|
|
});
|
|
it("keeps independent source identities for repeated searches", () => {
|
|
const other = {
|
|
...source,
|
|
id: "fedcba9876543210fedcba9876543210-1",
|
|
text: "Limit: 43.",
|
|
};
|
|
const next = {
|
|
...message,
|
|
artifact: { knowledge_sources: { version: 1, sources: [other] } },
|
|
} as Message;
|
|
expect(collectKnowledgeSources([message, next, message]).size).toBe(2);
|
|
});
|
|
});
|
|
|
|
it("collects ordinary Sources-section links without collecting code or images", () => {
|
|
const link = `[Manual.pdf](#knowledge-${id})`;
|
|
expect(
|
|
citedKnowledgeSources(
|
|
`## Sources\n- ${link}\n${link}`,
|
|
collectKnowledgeSources([message]),
|
|
),
|
|
).toEqual([source]);
|
|
expect(
|
|
citedKnowledgeSources(
|
|
`\`${link}\`\n!${link}`,
|
|
collectKnowledgeSources([message]),
|
|
),
|
|
).toEqual([]);
|
|
});
|