Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/database/conversation_db.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.

import json
from copy import deepcopy
from datetime import datetime
Expand Down Expand Up @@ -63,6 +65,8 @@ class SearchRecord(TypedDict):
score_accuracy: Optional[float]
score_semantic: Optional[float]
retrieval_highlight_terms: Optional[List[str]]
aidp_metadata: Optional[Dict[str, Any]]
aidp_source_reference: Optional[Dict[str, Any]]
published_date: Optional[datetime]
cite_index: Optional[int]
search_type: Optional[str]
Expand Down Expand Up @@ -476,6 +480,9 @@ def persist_assistant_run_batch(
"score_overall": record.get("score_overall"),
"score_accuracy": record.get("score_accuracy"),
"score_semantic": record.get("score_semantic"),
"retrieval_highlight_terms": record.get("retrieval_highlight_terms"),
"aidp_metadata": record.get("aidp_metadata"),
"aidp_source_reference": record.get("aidp_source_reference"),
"published_date": record.get("published_date"),
"cite_index": record.get("cite_index"),
"search_type": record.get("search_type"),
Expand Down Expand Up @@ -1949,6 +1956,10 @@ def create_source_search(search_data: Dict[str, Any], user_id: Optional[str] = N
]
if 'published_date' in search_data:
data["published_date"] = search_data['published_date']
if 'aidp_metadata' in search_data:
data["aidp_metadata"] = search_data['aidp_metadata']
if 'aidp_source_reference' in search_data:
data["aidp_source_reference"] = search_data['aidp_source_reference']
if user_id:
data = add_creation_tracking(data, user_id)

Expand Down
10 changes: 10 additions & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.

from sqlalchemy import (
JSON,
TIMESTAMP,
Expand Down Expand Up @@ -352,6 +354,14 @@ class ConversationSourceSearch(TableBase):
JSONB,
doc="Exact lexical terms returned by the retrieval engine for source highlighting",
)
aidp_metadata = Column(
JSONB, nullable=True,
comment="Opaque AIDP metadata snapshot returned for this retrieval hit",
)
aidp_source_reference = Column(
JSONB, nullable=True,
comment="AIDP source reference attributes for this retrieval hit, excluding metadata",
)
published_date = Column(TIMESTAMP(
timezone=False), doc="Upload date of local files or network search date")
cite_index = Column(
Expand Down
8 changes: 8 additions & 0 deletions backend/management/services/agent/run.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.

import asyncio
from copy import deepcopy
from http import HTTPStatus
Expand Down Expand Up @@ -693,6 +695,12 @@ async def _iter_run_chunks():
"source_title": result.get("title", ""),
"source_location": result.get("url", ""),
"source_content": result.get("text", ""),
"aidp_metadata": result.get("score_details", {}).get("metadata"),
"aidp_source_reference": {
key: value
for key, value in result.get("score_details", {}).items()
if key != "metadata"
},
"score_overall": float(result.get("score"))
if result.get("score") not in (None, "")
else None,
Expand Down
14 changes: 13 additions & 1 deletion backend/services/conversation_management_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.

import json
import logging

Expand Down Expand Up @@ -829,6 +831,10 @@ def get_conversation_history_service(conversation_id: int, user_id: str) -> List
"cite_index": record["cite_index"], "search_type": record["search_type"],
"tool_sign": record["tool_sign"], "score_details": {}}

search_item["score_details"].update(record.get("aidp_source_reference") or {})
if record.get("aidp_metadata") is not None:
search_item["score_details"]["metadata"] = record["aidp_metadata"]

if record["score_accuracy"] is not None:
search_item["score_details"]["accuracy"] = record["score_accuracy"]
if record["score_semantic"] is not None:
Expand Down Expand Up @@ -1091,10 +1097,16 @@ def get_sources_service(conversation_id: Optional[int], message_id: Optional[int
"filename": record["source_title"] if record["source_type"] == "file" else None,
"published_date": record["published_date"].strftime("%Y-%m-%d") if record[
"published_date"] else None,
"score": record["score_overall"]
"score": record["score_overall"],
"cite_index": record.get("cite_index"),
"search_type": record.get("search_type"),
"tool_sign": record.get("tool_sign"),
}

search_item["score_details"] = {}
search_item["score_details"].update(record.get("aidp_source_reference") or {})
if record.get("aidp_metadata") is not None:
search_item["score_details"]["metadata"] = record["aidp_metadata"]
if record["score_accuracy"] is not None:
search_item["score_details"]["accuracy"] = record["score_accuracy"]
if record["score_semantic"] is not None:
Expand Down
11 changes: 11 additions & 0 deletions deploy/sql/migrations/v2.7.1_002_chat_source_snapshots.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.

-- Add AIDP-specific retrieval snapshots for each search hit.
ALTER TABLE nexent.conversation_source_search_t
ADD COLUMN IF NOT EXISTS aidp_metadata JSONB,
ADD COLUMN IF NOT EXISTS aidp_source_reference JSONB;

COMMENT ON COLUMN nexent.conversation_source_search_t.aidp_metadata
IS 'Opaque AIDP metadata snapshot returned for this retrieval hit';
COMMENT ON COLUMN nexent.conversation_source_search_t.aidp_source_reference
IS 'AIDP source reference attributes for this retrieval hit, excluding metadata';
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.

"use client";

import type { FC, MutableRefObject, PropsWithChildren } from "react";
import { useMemo } from "react";
import { getSourceAttributes } from "@/lib/source";
import {
ExportedMessageRepository,
RuntimeAdapterProvider,
Expand Down Expand Up @@ -187,7 +190,7 @@ const toToolSearchItem = (value: unknown) => {
const toolSign =
typeof item.tool_sign === "string" ? item.tool_sign : undefined;

return resolvedUrl || sourceFile
return resolvedUrl || sourceFile || filename || title
? {
url: resolvedUrl,
title,
Expand All @@ -199,13 +202,16 @@ const toToolSearchItem = (value: unknown) => {
sourceType:
typeof item.source_type === "string" ? item.source_type : undefined,
filename: filename || undefined,
publishedDate: typeof item.published_date === "string" ? item.published_date : undefined,
downloadUrl: typeof item.download_url === "string" ? item.download_url : undefined,
sourceFile: sourceFile || imageMetadata?.source_file || undefined,
objectName:
typeof item.object_name === "string" ? item.object_name : undefined,
citeIndex,
toolSign,
isImage: Boolean(imageMetadata),
retrievalHighlightTerms: getRetrievalHighlightTerms(item.score_details),
...getSourceAttributes(item.search_type, item.score_details),
}
: null;
};
Expand Down Expand Up @@ -423,23 +429,35 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {
const item = searchItem as Record<string, unknown>;
const url = (item.url as string | undefined) ?? "";
const filename = (item.filename as string | undefined) ?? "";
const title = (item.title as string | undefined) || filename || url;
const imageMetadata = parseImageMetadata(item.text);
const resolvedUrl = imageMetadata?.image_url || url;
const title =
(item.title as string | undefined) ||
filename ||
imageMetadata?.source_file ||
resolvedUrl;
if (url || filename || title) {
const derivedImageKey = `${item.tool_sign ?? ""}${item.cite_index ?? ""}`;
const isImage =
(item.score_details as Record<string, unknown> | undefined)
?.chunk_type === "image" ||
Boolean(imageMetadata) ||
persistedAnswerImageKeys.includes(derivedImageKey);
sources.push({
citeIndex: (item.cite_index as number | undefined) ?? 0,
url,
url: resolvedUrl,
title,
text: item.text as string | undefined,
text: imageMetadata
? undefined
: (item.text as string | undefined),
publishedDate: item.published_date as string | undefined,
sourceType: item.source_type as string | undefined,
searchType: item.search_type as string | undefined,
toolSign: item.tool_sign as string | undefined,
filename,
sourceFile:
(item.source_file as string | undefined) ||
imageMetadata?.source_file,
downloadUrl: item.download_url as string | undefined,
objectName: item.object_name as string | undefined,
isImage,
Expand All @@ -449,6 +467,7 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {
retrievalHighlightTerms: getRetrievalHighlightTerms(
item.score_details
),
...getSourceAttributes(item.search_type, item.score_details),
});
}
}
Expand Down Expand Up @@ -588,6 +607,7 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {
)
: [];
const restoredImageUrls = new Set<string>();
const restoredImageKeys = new Set<string>();
const restoredImages: any[] = [];
const historicalSkillFiles = new Map<string, string>();
let historicalSkillSummaryPart: { type: "text"; text: string } | null =
Expand All @@ -597,37 +617,69 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {
{ files: Map<string, string>; summary: string | null }
>();
const appendHistoricalImage = (imageUrl: string) => {
if (!imageUrl || restoredImageUrls.has(imageUrl)) return;
if (!imageUrl || restoredImageUrls.has(imageUrl)) return [];
const imageIndex = restoredImageUrls.size;
const imageKey = answerImageKeys[imageIndex];
const metadata = persistedImageSources.find((searchItem) => {
const matchingSources = persistedImageSources.filter((searchItem) => {
const item = searchItem as Record<string, unknown>;
const metadata = parseImageMetadata(item.text);
return (metadata?.image_url || item.url) === imageUrl;
}) as Record<string, unknown>[];
const fallbackMetadata = persistedImageSources.find((searchItem) => {
const item = searchItem as Record<string, unknown>;
return (
imageKey === `${item.tool_sign ?? ""}${item.cite_index ?? ""}`
);
}) as Record<string, unknown> | undefined;
const title = (metadata?.title as string | undefined) || imageUrl;
const imagePart: any = {
type: "source",
sourceType: "url",
url: imageUrl,
title,
text: metadata?.text as string | undefined,
citeIndex:
(metadata?.cite_index as number | undefined) ?? undefined,
isImage: true,
imageKey:
const appendedImages = [];
for (const metadata of matchingSources.length
? matchingSources
: [fallbackMetadata]) {
const imageMetadata = metadata
? parseImageMetadata(metadata.text)
: null;
const sourceImageKey =
(metadata?.image_key as string | undefined) ||
(metadata?.tool_sign && metadata?.cite_index !== undefined
? `${metadata.tool_sign}${metadata.cite_index}`
: undefined) ||
imageKey,
};
const meta = buildMetadata();
if (meta) imagePart.metadata = meta;
imageKey;
if (sourceImageKey && restoredImageKeys.has(sourceImageKey))
continue;
const title =
(metadata?.title as string | undefined) ||
imageMetadata?.source_file ||
imageUrl;
const imagePart: any = {
...getSourceAttributes(
metadata?.search_type,
metadata?.score_details
),
type: "source",
sourceType: "url",
url: imageUrl,
title,
text: imageMetadata
? undefined
: (metadata?.text as string | undefined),
filename: metadata?.filename as string | undefined,
sourceFile:
(metadata?.source_file as string | undefined) ||
imageMetadata?.source_file,
toolSign: metadata?.tool_sign as string | undefined,
citeIndex:
(metadata?.cite_index as number | undefined) ?? undefined,
isImage: true,
imageKey: sourceImageKey,
};
const meta = buildMetadata();
if (meta) imagePart.metadata = meta;
if (sourceImageKey) restoredImageKeys.add(sourceImageKey);
restoredImages.push(imagePart);
appendedImages.push(imagePart);
}
restoredImageUrls.add(imageUrl);
restoredImages.push(imagePart);
return imagePart;
return appendedImages;
};

for (const [partIndex, part] of messageParts.entries()) {
Expand Down Expand Up @@ -783,8 +835,7 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {

if (part.type === "picture_web") {
for (const imageUrl of parseSearchImageUrls(part.content)) {
const imagePart = appendHistoricalImage(imageUrl);
if (imagePart) {
for (const imagePart of appendHistoricalImage(imageUrl)) {
attachSearchContentToTool(
content,
{
Expand All @@ -793,6 +844,11 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {
text: imagePart.text,
isImage: true,
imageKey: imagePart.imageKey,
searchType: imagePart.searchType,
sourceMetadata: imagePart.sourceMetadata,
sourceReference: imagePart.sourceReference,
toolSign: imagePart.toolSign,
citeIndex: imagePart.citeIndex,
},
part.tool_call_id
);
Expand Down Expand Up @@ -1235,8 +1291,10 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {
const scoreDetails = item.score_details as
Record<string, unknown> | undefined;
const searchImageKey = `${item.tool_sign ?? ""}${item.cite_index ?? ""}`;
const imageMetadata = parseImageMetadata(item.text);
if (
scoreDetails?.chunk_type === "image" ||
imageMetadata ||
answerImageKeys.includes(searchImageKey)
)
continue;
Expand All @@ -1253,13 +1311,15 @@ export class RemoteConversationHistoryAdapter implements ThreadHistoryAdapter {
title,
text: item.text as string | undefined,
filename,
publishedDate: item.published_date as string | undefined,
downloadUrl: item.download_url as string | undefined,
objectName: item.object_name as string | undefined,
citeIndex,
toolSign: item.tool_sign as string | undefined,
retrievalHighlightTerms: getRetrievalHighlightTerms(
item.score_details
),
...getSourceAttributes(item.search_type, item.score_details),
messageId,
});
}
Expand Down
Loading
Loading