Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj
This commit is contained in:
commit
5901246030
2 files changed
+134
-30
No files matched your search
@@ -780,11 +780,10 @@ function buildTurnReferenceBatches(
|
||||
continue;
|
||||
}
|
||||
const nextHumanIndex = findNextHumanIndex(messages, index + 1);
|
||||
if (nextHumanIndex === -1 && isLoading) {
|
||||
continue;
|
||||
}
|
||||
const endIndex = nextHumanIndex === -1 ? messages.length : nextHumanIndex;
|
||||
const assistant = findLastAssistantAnswer(messages, index + 1, endIndex);
|
||||
const assistant =
|
||||
findLastAssistantAnswer(messages, index + 1, endIndex) ??
|
||||
findFrontendRagAssistant(messages, index + 1, endIndex);
|
||||
if (!assistant?.id) {
|
||||
continue;
|
||||
}
|
||||
@@ -794,6 +793,14 @@ function buildTurnReferenceBatches(
|
||||
nextHumanIndex === -1 ? undefined : messages[nextHumanIndex]?.id,
|
||||
includeBeforeMessage: false,
|
||||
}).filter((source) => source.mode !== "none");
|
||||
// GeneralQA's single-RAG stream sends `docs` before the answer. Make
|
||||
// those cards available immediately, while keeping normal streaming turns
|
||||
// on their existing "wait for an answer" behavior.
|
||||
const isFrontendRagTurn =
|
||||
isSingleRoundRagMessage(assistant) || isMultiRoundRagMessage(assistant);
|
||||
if (nextHumanIndex === -1 && isLoading && !isFrontendRagTurn) {
|
||||
continue;
|
||||
}
|
||||
if (sources.length > 0) {
|
||||
batches.push({
|
||||
id: assistant.id,
|
||||
@@ -832,6 +839,23 @@ function findLastAssistantAnswer(
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFrontendRagAssistant(
|
||||
messages: Message[],
|
||||
start: number,
|
||||
end: number,
|
||||
): Message | null {
|
||||
for (let index = end - 1; index >= start; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (
|
||||
message?.type === "ai" &&
|
||||
(isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message))
|
||||
) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasAssistantAnswerContent(message: Message): boolean {
|
||||
const content = message.content;
|
||||
if (typeof content === "string") {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { readRuntimeEnv } from "@/env";
|
||||
|
||||
const STORAGE_PREFIX = "workspace.single-round-rag.";
|
||||
|
||||
type KnowledgeBaseChunk = {
|
||||
type KnowledgeBaseChunk = Record<string, unknown> & {
|
||||
answer?: unknown;
|
||||
docs?: unknown;
|
||||
};
|
||||
@@ -168,36 +168,114 @@ function parseKnowledgeBaseChunk(jsonText: string): KnowledgeBaseChunk {
|
||||
}
|
||||
}
|
||||
|
||||
function flattenDocs(value: unknown): Record<string, unknown>[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((item) => flattenDocs(item));
|
||||
function readString(...values: unknown[]): string | undefined {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string" && value.trim()) return value;
|
||||
}
|
||||
if (!value || typeof value !== "object") return [];
|
||||
const record = value as Record<string, unknown>;
|
||||
if ("docs" in record) return flattenDocs(record.docs);
|
||||
return [record];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeSources(value: unknown): RagSource[] {
|
||||
return flattenDocs(value).map((item, index) => {
|
||||
const title = String(item.m_title ?? item.title ?? item.source ?? `参考资料 ${index + 1}`);
|
||||
const content = String(item.m_content ?? item.content ?? item.snippet ?? "");
|
||||
const source = item.source1 ?? item.source;
|
||||
const rawUrl = item.url ?? item.link ?? item.href;
|
||||
return {
|
||||
id: String(item.recUuid ?? item.rec_uuid ?? item.uuid ?? item.id ?? "") || undefined,
|
||||
title,
|
||||
type: String(item.source1 ?? item.type ?? "知识库"),
|
||||
snippet: content,
|
||||
content,
|
||||
source: typeof source === "string" ? source : undefined,
|
||||
time: typeof item.m_publish === "string" ? item.m_publish : undefined,
|
||||
url: typeof rawUrl === "string" && /^https?:\/\//i.test(rawUrl) ? rawUrl : undefined,
|
||||
raw: item,
|
||||
};
|
||||
function docsFallbackContent(docs: unknown): string | undefined {
|
||||
const candidates = Array.isArray(docs) ? docs : [docs];
|
||||
return readString(...candidates);
|
||||
}
|
||||
|
||||
function buildKnowledgeDetailUrl(recUuid: string | undefined): string | undefined {
|
||||
const knowledgeUrl = readRuntimeEnv("VITE_AIPI_KNOWLEDGE_URL")?.replace(/\/$/, "");
|
||||
if (!knowledgeUrl || !recUuid) return undefined;
|
||||
return `${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${encodeURIComponent(recUuid)}`;
|
||||
}
|
||||
|
||||
function toRagSource(
|
||||
record: Record<string, unknown>,
|
||||
index: number,
|
||||
fallbackContent?: string,
|
||||
): RagSource {
|
||||
const title =
|
||||
readString(record.m_title, record.title, record.source, record.source1) ??
|
||||
`参考资料 ${index + 1}`;
|
||||
const content =
|
||||
readString(
|
||||
record.m_content,
|
||||
// Some deployed responses spell this field incorrectly; keep it as a
|
||||
// read-only compatibility alias rather than dropping the document.
|
||||
record.m_conent,
|
||||
record.content,
|
||||
record.snippet,
|
||||
fallbackContent,
|
||||
record.url,
|
||||
) ?? "";
|
||||
const source = readString(record.source1, record.source, record.m_source);
|
||||
const rawUrl = readString(record.url, record.link, record.href);
|
||||
const recUuid = readString(record.recUuid, record.rec_uuid, record.uuid, record.id);
|
||||
return {
|
||||
id: recUuid,
|
||||
title,
|
||||
type: readString(record.type, record.source1) ?? "知识库",
|
||||
snippet: content,
|
||||
content,
|
||||
source,
|
||||
time: readString(record.m_publish, record.m_publicsh, record.time),
|
||||
// GeneralQA uses recUuid to open the knowledge-detail page. Prefer that
|
||||
// stable internal record link, then fall back to a backend-provided URL.
|
||||
url:
|
||||
buildKnowledgeDetailUrl(recUuid) ??
|
||||
(rawUrl && /^https?:\/\//i.test(rawUrl) ? rawUrl : undefined),
|
||||
raw: record,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A single-RAG stream is a concatenation of source envelopes, for example:
|
||||
* `{docs:["..."], recUuid:"...", m_title:"...", m_content:"..."}`.
|
||||
* The root-level metadata is the source; `docs` merely marks the envelope and
|
||||
* supplies a fallback body. This mirrors GeneralQA's per-chunk processing.
|
||||
*/
|
||||
function normalizeSources(chunk: KnowledgeBaseChunk): RagSource[] {
|
||||
const hasRootMetadata = [
|
||||
chunk.recUuid,
|
||||
chunk.rec_uuid,
|
||||
chunk.m_title,
|
||||
chunk.m_content,
|
||||
chunk.m_conent,
|
||||
chunk.source1,
|
||||
chunk.m_publish,
|
||||
chunk.m_publicsh,
|
||||
].some((value) => typeof value === "string" && value.trim());
|
||||
if (hasRootMetadata) {
|
||||
return [toRagSource(chunk, 0, docsFallbackContent(chunk.docs))];
|
||||
}
|
||||
|
||||
const docs = Array.isArray(chunk.docs) ? chunk.docs : chunk.docs ? [chunk.docs] : [];
|
||||
return docs.flatMap((item, index): RagSource[] => {
|
||||
if (typeof item === "string") {
|
||||
return [toRagSource({}, index, item)];
|
||||
}
|
||||
if (!item || typeof item !== "object") return [];
|
||||
return [toRagSource(item as Record<string, unknown>, index)];
|
||||
});
|
||||
}
|
||||
|
||||
function mergeSources(current: RagSource[], incoming: RagSource[]): RagSource[] {
|
||||
const result = [...current];
|
||||
const keys = new Set(
|
||||
current.map((source) =>
|
||||
source.id
|
||||
? `id:${source.id}`
|
||||
: `content:${source.title}\u0000${source.content}\u0000${source.source ?? ""}`,
|
||||
),
|
||||
);
|
||||
for (const source of incoming) {
|
||||
const key = source.id
|
||||
? `id:${source.id}`
|
||||
: `content:${source.title}\u0000${source.content}\u0000${source.source ?? ""}`;
|
||||
if (keys.has(key)) continue;
|
||||
keys.add(key);
|
||||
result.push(source);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function createId(prefix: string) {
|
||||
return `${prefix}-${crypto.randomUUID()}`;
|
||||
}
|
||||
@@ -397,7 +475,9 @@ export function useSingleRoundRagMessages(threadId: string) {
|
||||
for (const jsonText of extracted.objects) {
|
||||
const chunk = parseKnowledgeBaseChunk(jsonText);
|
||||
if (typeof chunk.answer === "string") answerChunks.push({ answer: chunk.answer });
|
||||
if (chunk.docs !== undefined) sources = normalizeSources(chunk.docs);
|
||||
if (chunk.docs !== undefined) {
|
||||
sources = mergeSources(sources, normalizeSources(chunk));
|
||||
}
|
||||
refreshAssistant();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user