实现RAG参考文献显示模式
This commit is contained in:
1 parent
eda0ccffdb
commit
c56db15a03
22 files changed
+1733
-192
No files matched your search
@@ -168,6 +168,7 @@ export function collectRuntimeDisplaySources(
|
||||
const seenCitationKeys = new Map<string, RuntimeDisplaySource>();
|
||||
const seenDisplayKeys = new Set<string>();
|
||||
const consumedToolMessageIds = new Set<string>();
|
||||
const consumedToolMessages = new Set<Message>();
|
||||
|
||||
for (let index = start; index < limit; index++) {
|
||||
const message = messages[index];
|
||||
@@ -180,14 +181,24 @@ export function collectRuntimeDisplaySources(
|
||||
displays,
|
||||
toolCall.args,
|
||||
);
|
||||
const result = findToolResultPayload(toolCall, messages, index);
|
||||
const toolMessage = findToolResultMessage(
|
||||
toolCall,
|
||||
messages,
|
||||
index,
|
||||
consumedToolMessageIds,
|
||||
consumedToolMessages,
|
||||
);
|
||||
if (!toolMessage) {
|
||||
continue;
|
||||
}
|
||||
const result = parseToolMessagePayload(toolMessage);
|
||||
if (result === undefined) {
|
||||
continue;
|
||||
}
|
||||
const linkedToolMessage = findToolMessageById(toolCall.id, messages);
|
||||
if (linkedToolMessage?.id) {
|
||||
consumedToolMessageIds.add(linkedToolMessage.id);
|
||||
if (toolMessage.id) {
|
||||
consumedToolMessageIds.add(toolMessage.id);
|
||||
}
|
||||
consumedToolMessages.add(toolMessage);
|
||||
if (isDisplaySamplePayload(result)) {
|
||||
continue;
|
||||
}
|
||||
@@ -223,7 +234,12 @@ export function collectRuntimeDisplaySources(
|
||||
|
||||
for (let index = start; index < limit; index++) {
|
||||
const message = messages[index];
|
||||
if (!message || message.type !== "tool" || consumedToolMessageIds.has(message.id ?? "")) {
|
||||
if (
|
||||
!message ||
|
||||
message.type !== "tool" ||
|
||||
consumedToolMessages.has(message) ||
|
||||
consumedToolMessageIds.has(message.id ?? "")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const result = parseToolMessagePayload(message);
|
||||
@@ -238,7 +254,8 @@ export function collectRuntimeDisplaySources(
|
||||
}
|
||||
const normalized = match
|
||||
? normalizeDisplaySources(result, match.display)
|
||||
: shouldUseDefaultCitationMode(toolName, result)
|
||||
: shouldUseDefaultCitationMode(toolName, result) ||
|
||||
normalizeDefaultDisplaySources(result).length > 0
|
||||
? normalizeDefaultDisplaySources(result)
|
||||
: [];
|
||||
appendDisplaySources(
|
||||
@@ -268,16 +285,14 @@ function appendDisplaySources(
|
||||
) {
|
||||
for (const draft of drafts) {
|
||||
if (draft.mode === "citation") {
|
||||
const key = citationSourceKey(draft);
|
||||
const existing = seenCitationKeys.get(key);
|
||||
if (existing) {
|
||||
continue;
|
||||
}
|
||||
const source = {
|
||||
...draft,
|
||||
index: nextCitationIndex(),
|
||||
};
|
||||
seenCitationKeys.set(key, source);
|
||||
const key = citationSourceKey(draft);
|
||||
if (key) {
|
||||
seenCitationKeys.set(`${key}:${source.index}`, source);
|
||||
}
|
||||
target.push(source);
|
||||
continue;
|
||||
}
|
||||
@@ -331,12 +346,19 @@ function citationSourceKey(source: RuntimeDisplaySourceDraft): string {
|
||||
stringifyMaybe(raw.docId) ||
|
||||
stringifyMaybe(raw.id);
|
||||
if (id) {
|
||||
return normalizeName(`${source.skillName}:${id}`);
|
||||
return normalizeCitationKey(String(id));
|
||||
}
|
||||
const fallback = [source.skillName, source.title, source.source, source.time]
|
||||
const fallback = [
|
||||
source.skillName,
|
||||
source.title,
|
||||
source.source,
|
||||
source.time,
|
||||
source.content,
|
||||
source.snippet,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("|");
|
||||
return normalizeName(fallback) || JSON.stringify(raw).slice(0, 300);
|
||||
return normalizeCitationKey(fallback) || normalizeCitationKey(JSON.stringify(raw).slice(0, 300));
|
||||
}
|
||||
|
||||
function nonCitationSourceKey(source: RuntimeDisplaySourceDraft): string {
|
||||
@@ -543,7 +565,12 @@ export function normalizeDefaultDisplaySources(payload: unknown): DisplaySourceI
|
||||
: undefined,
|
||||
);
|
||||
return normalized.map((item, index) => ({
|
||||
id: item.url ?? String(index + 1),
|
||||
id:
|
||||
item.recUuid ??
|
||||
item.uuid ??
|
||||
item.docId ??
|
||||
item.id ??
|
||||
item.url,
|
||||
title: item.title ?? item.source ?? `结果 ${index + 1}`,
|
||||
type: "result",
|
||||
snippet: item.snippet ?? item.content ?? "",
|
||||
@@ -620,18 +647,79 @@ function replaceCitationTokens(
|
||||
byIndex: Map<number, RuntimeDisplaySource>,
|
||||
orderedSources: RuntimeDisplaySource[],
|
||||
): string {
|
||||
return content
|
||||
const normalized = normalizeLooseCitationTokens(content, byIndex, orderedSources);
|
||||
const withDoubleBracketCitations = normalized
|
||||
.replace(/\[\[(\d+)\]\]/g, (match, rawIndex: string) =>
|
||||
citationMarkdown(match, Number(rawIndex), byIndex, orderedSources),
|
||||
)
|
||||
.replace(/(^|[^:\[!])\[(\d+)\](?!\()/g, (match, prefix: string, rawIndex: string) =>
|
||||
`${prefix}${citationMarkdown(
|
||||
match.slice(prefix.length),
|
||||
Number(rawIndex),
|
||||
byIndex,
|
||||
orderedSources,
|
||||
)}`,
|
||||
);
|
||||
return replaceBracketCitationTokens(
|
||||
withDoubleBracketCitations,
|
||||
byIndex,
|
||||
orderedSources,
|
||||
);
|
||||
}
|
||||
|
||||
function replaceBracketCitationTokens(
|
||||
content: string,
|
||||
byIndex: Map<number, RuntimeDisplaySource>,
|
||||
orderedSources: RuntimeDisplaySource[],
|
||||
): string {
|
||||
const tokenPattern = /\\?\[(\d{1,3})\\?\](?!\()/g;
|
||||
let cursor = 0;
|
||||
let next = "";
|
||||
for (const match of content.matchAll(tokenPattern)) {
|
||||
const start = match.index ?? 0;
|
||||
const previous = start > 0 ? content[start - 1] : "";
|
||||
if (previous === ":" || previous === "[" || previous === "!") {
|
||||
continue;
|
||||
}
|
||||
next += content.slice(cursor, start);
|
||||
next += citationMarkdown(match[0], Number(match[1]), byIndex, orderedSources);
|
||||
cursor = start + match[0].length;
|
||||
}
|
||||
next += content.slice(cursor);
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeLooseCitationTokens(
|
||||
content: string,
|
||||
byIndex: Map<number, RuntimeDisplaySource>,
|
||||
orderedSources: RuntimeDisplaySource[],
|
||||
): string {
|
||||
const isValidIndex = (index: number) =>
|
||||
Number.isInteger(index) &&
|
||||
index > 0 &&
|
||||
(byIndex.has(index) || !!orderedSources[index - 1]);
|
||||
const toCitationRun = (raw: string) => {
|
||||
const parts = raw.match(/\\?\[\d{1,3}\\?\]|\d{1,3}/g) ?? [];
|
||||
const indexes = parts.map((part) => Number(part.replace(/\D/g, "")));
|
||||
if (indexes.length === 0 || indexes.some((index) => !isValidIndex(index))) {
|
||||
return raw;
|
||||
}
|
||||
return indexes.map((index) => `[${index}]`).join("");
|
||||
};
|
||||
|
||||
let next = content.replace(
|
||||
/(^|[^\d\[\]\\])((?:\d{1,3}|\\?\[\d{1,3}\\?\])(?:\\?\[\d{1,3}\\?\]|\d{1,3})+)/g,
|
||||
(match, prefix: string, rawRun: string) => {
|
||||
if (!rawRun.includes("[")) {
|
||||
return match;
|
||||
}
|
||||
return `${prefix}${toCitationRun(rawRun)}`;
|
||||
},
|
||||
);
|
||||
|
||||
next = next.replace(
|
||||
/(^|[\s\uFF0C\u3002\uFF1B\uFF1A\u3001,!?!?)])(\d{1,3})(?=([\s\uFF0C\u3002\uFF1B\uFF1A\u3001,!?!?)]|$))/g,
|
||||
(match, prefix: string, rawIndex: string) => {
|
||||
const index = Number(rawIndex);
|
||||
if (!isValidIndex(index)) {
|
||||
return match;
|
||||
}
|
||||
return `${prefix}[${index}]`;
|
||||
},
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeExistingCitationLinks(
|
||||
@@ -654,16 +742,16 @@ function normalizeExistingCitationLinks(
|
||||
const index = Number(numberMatch[1]);
|
||||
const source = byIndex.get(index) ?? orderedSources[index - 1];
|
||||
if (source) {
|
||||
return `[citation:[${source.index}]](#reference-${source.index})`;
|
||||
return citationLinkMarkdown(source);
|
||||
}
|
||||
}
|
||||
const sourceByUrl = byUrl.get(url);
|
||||
if (sourceByUrl) {
|
||||
return `[citation:[${sourceByUrl.index}]](#reference-${sourceByUrl.index})`;
|
||||
return citationLinkMarkdown(sourceByUrl);
|
||||
}
|
||||
const sourceByTitle = byTitle.get(label);
|
||||
if (sourceByTitle) {
|
||||
return `[citation:[${sourceByTitle.index}]](#reference-${sourceByTitle.index})`;
|
||||
return citationLinkMarkdown(sourceByTitle);
|
||||
}
|
||||
return match;
|
||||
},
|
||||
@@ -680,11 +768,11 @@ function citationMarkdown(
|
||||
if (!source) {
|
||||
return fallback;
|
||||
}
|
||||
return `[citation:[${source.index}]](#reference-${source.index})`;
|
||||
return citationLinkMarkdown(source);
|
||||
}
|
||||
|
||||
function hasCitationMarkdown(content: string): boolean {
|
||||
return /\[citation:\[[^\]]+\]\]\(#reference-[^)]+\)/.test(content);
|
||||
return /\[citation:(?:\[[^\]]+\]|\d+)\]\(#reference-[^)]+\)/.test(content);
|
||||
}
|
||||
|
||||
function appendCitationSummary(
|
||||
@@ -693,7 +781,7 @@ function appendCitationSummary(
|
||||
): string {
|
||||
const citations = citationSources
|
||||
.slice(0, 5)
|
||||
.map((source) => `[citation:[${source.index}]](#reference-${source.index})`)
|
||||
.map((source) => citationLinkMarkdown(source))
|
||||
.join(" ");
|
||||
if (!citations) {
|
||||
return content;
|
||||
@@ -702,10 +790,22 @@ function appendCitationSummary(
|
||||
return `${trimmed}${trimmed ? "\n\n" : ""}参考来源:${citations}`;
|
||||
}
|
||||
|
||||
function citationLinkMarkdown(source: RuntimeDisplaySource): string {
|
||||
return `[citation:${source.index}](#reference-${source.index})`;
|
||||
}
|
||||
|
||||
function normalizeName(name: string): string {
|
||||
return name.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||
}
|
||||
|
||||
function normalizeCitationKey(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.normalize("NFKC")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -714,18 +814,28 @@ function stringifyMaybe(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
function findToolResultPayload(
|
||||
function findToolResultMessage(
|
||||
toolCall: RuntimeToolCall,
|
||||
messages: Message[],
|
||||
toolCallMessageIndex = -1,
|
||||
): unknown {
|
||||
const toolMessage =
|
||||
findToolMessageById(toolCall.id, messages) ??
|
||||
findToolMessageByName(toolCall, messages, toolCallMessageIndex);
|
||||
if (!toolMessage) {
|
||||
return undefined;
|
||||
consumedToolMessageIds?: Set<string>,
|
||||
consumedToolMessages?: Set<Message>,
|
||||
): Message | undefined {
|
||||
const byId = findToolMessageById(toolCall.id, messages);
|
||||
if (
|
||||
byId &&
|
||||
!consumedToolMessages?.has(byId) &&
|
||||
(!byId.id || !consumedToolMessageIds?.has(byId.id))
|
||||
) {
|
||||
return byId;
|
||||
}
|
||||
return parseToolMessagePayload(toolMessage);
|
||||
return findToolMessageByName(
|
||||
toolCall,
|
||||
messages,
|
||||
toolCallMessageIndex,
|
||||
consumedToolMessageIds,
|
||||
consumedToolMessages,
|
||||
);
|
||||
}
|
||||
|
||||
function parseToolMessagePayload(toolMessage: Message): unknown {
|
||||
@@ -782,6 +892,8 @@ function findToolMessageByName(
|
||||
toolCall: RuntimeToolCall,
|
||||
messages: Message[],
|
||||
toolCallMessageIndex: number,
|
||||
consumedToolMessageIds?: Set<string>,
|
||||
consumedToolMessages?: Set<Message>,
|
||||
): Message | undefined {
|
||||
const start = Math.max(toolCallMessageIndex + 1, 0);
|
||||
for (let index = start; index < messages.length; index++) {
|
||||
@@ -795,6 +907,12 @@ function findToolMessageByName(
|
||||
if (message.type !== "tool") {
|
||||
continue;
|
||||
}
|
||||
if (message.id && consumedToolMessageIds?.has(message.id)) {
|
||||
continue;
|
||||
}
|
||||
if (consumedToolMessages?.has(message)) {
|
||||
continue;
|
||||
}
|
||||
const name =
|
||||
stringifyMaybe((message as { name?: unknown }).name) ??
|
||||
stringifyMaybe(
|
||||
|
||||
Reference in new issue
Block a user