📚 feat: Add Source Citations for File Search in Agents (#8652)

* feat: Source Citations for file_search in Agents

* Fix: Added citation limits and relevance score to app service. Removed duplicate tests

*  feat: implement Role-level toggle to optionally disable file Source Citation in Agents

* 🐛 fix: update mock for librechat-data-provider to include PermissionTypes and SystemRoles

---------

Co-authored-by: “Praneeth <praneeth.goparaju@slalom.com>
This commit is contained in:
Danny Avila 2025-07-25 00:07:37 -04:00
parent a955097faf
commit 52e59e40be
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
36 changed files with 1890 additions and 190 deletions

View file

@ -1,8 +1,12 @@
import { memo, useState, useContext } from 'react';
import { memo, useState, useContext, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { useToastContext } from '@librechat/client';
import type { CitationProps } from './types';
import { SourceHovercard, FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard';
import { CitationContext, useCitation, useCompositeCitations } from './Context';
import { useFileDownload } from '~/data-provider';
import { useLocalize } from '~/hooks';
import store from '~/store';
interface CompositeCitationProps {
citationId?: string;
@ -114,6 +118,8 @@ interface CitationComponentProps {
export function Citation(props: CitationComponentProps) {
const localize = useLocalize();
const user = useRecoilValue(store.user);
const { showToast } = useToastContext();
const { citation, citationId } = props.node?.properties ?? {};
const { setHoveredCitationId } = useContext(CitationContext);
const refData = useCitation({
@ -121,6 +127,49 @@ export function Citation(props: CitationComponentProps) {
refType: citation?.refType,
index: citation?.index || 0,
});
// Setup file download hook
const isFileType = refData?.refType === 'file' && (refData as any)?.fileId;
const { refetch: downloadFile } = useFileDownload(
user?.id ?? '',
isFileType ? (refData as any).fileId : '',
);
const handleFileDownload = useCallback(
async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isFileType || !(refData as any)?.fileId) return;
try {
const stream = await downloadFile();
if (stream.data == null || stream.data === '') {
console.error('Error downloading file: No data found');
showToast({
status: 'error',
message: localize('com_ui_download_error'),
});
return;
}
const link = document.createElement('a');
link.href = stream.data;
link.setAttribute('download', (refData as any).fileName || 'file');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(stream.data);
} catch (error) {
console.error('Error downloading file:', error);
showToast({
status: 'error',
message: localize('com_ui_download_error'),
});
}
},
[downloadFile, isFileType, refData, localize, showToast],
);
if (!refData) return null;
const getCitationLabel = () => {
@ -138,6 +187,8 @@ export function Citation(props: CitationComponentProps) {
label={getCitationLabel()}
onMouseEnter={() => setHoveredCitationId(citationId || null)}
onMouseLeave={() => setHoveredCitationId(null)}
onClick={isFileType ? handleFileDownload : undefined}
isFile={isFileType}
/>
);
}