🗃️ feat: Code Interpreter File Persistence between Sessions (#6790)

* refactor: Enhance FileContainer with customizable button and container styles, onClick button handling, and type override

* refactor: Update file type handling to support partial file objects

* refactor: Extract download handling into a custom hook for improved reusability

* refactor: Replace LogContent with Stdout component and enhance Attachment rendering for added visibility

* feat: Update @librechat/agents to version 2.4.1 for referencing generated files in subsequent code interpreter uses

* feat: Add support for tab-separated values (TSV) in mime type handling and improve error logging for regex patterns

* chore: Update @librechat/agents to version 2.4.11 for better `session_id` instructions when wanting to persist files between executions

* chore: Update @librechat/agents to version 2.4.12 for improved functionality

* fix: Enhance argument parsing in useParseArgs to support JSON input and improve code extraction

* refactor: Update input handling in useAutoSave to require more than one character before saving to local storage
This commit is contained in:
Danny Avila 2025-04-08 23:18:50 -04:00 committed by GitHub
parent 910c73359b
commit 5d668748f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 113 additions and 30 deletions

View file

@ -49,7 +49,7 @@
"@langchain/google-genai": "^0.2.2", "@langchain/google-genai": "^0.2.2",
"@langchain/google-vertexai": "^0.2.3", "@langchain/google-vertexai": "^0.2.3",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.0", "@librechat/agents": "^2.4.12",
"@librechat/data-schemas": "*", "@librechat/data-schemas": "*",
"@waylaidwanderer/fetch-event-source": "^3.0.1", "@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2", "axios": "^1.8.2",

View file

@ -1,22 +1,40 @@
import type { TFile } from 'librechat-data-provider'; import type { TFile } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common'; import type { ExtendedFile } from '~/common';
import { getFileType, cn } from '~/utils';
import FilePreview from './FilePreview'; import FilePreview from './FilePreview';
import RemoveFile from './RemoveFile'; import RemoveFile from './RemoveFile';
import { getFileType } from '~/utils';
const FileContainer = ({ const FileContainer = ({
file, file,
overrideType,
buttonClassName,
containerClassName,
onDelete, onDelete,
onClick,
}: { }: {
file: ExtendedFile | TFile; file: Partial<ExtendedFile | TFile>;
overrideType?: string;
buttonClassName?: string;
containerClassName?: string;
onDelete?: () => void; onDelete?: () => void;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
}) => { }) => {
const fileType = getFileType(file.type); const fileType = getFileType(overrideType ?? file.type);
return ( return (
<div className="group relative inline-block text-sm text-text-primary"> <div
<div className="relative overflow-hidden rounded-2xl border border-border-light"> className={cn('group relative inline-block text-sm text-text-primary', containerClassName)}
<div className="w-56 bg-surface-hover-alt p-1.5"> >
<button
type="button"
onClick={onClick}
aria-label={file.filename}
className={cn(
'relative overflow-hidden rounded-2xl border border-border-light bg-surface-hover-alt',
buttonClassName,
)}
>
<div className="w-56 p-1.5">
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<FilePreview file={file} fileType={fileType} className="relative" /> <FilePreview file={file} fileType={fileType} className="relative" />
<div className="overflow-hidden"> <div className="overflow-hidden">
@ -29,7 +47,7 @@ const FileContainer = ({
</div> </div>
</div> </div>
</div> </div>
</div> </button>
{onDelete && <RemoveFile onRemove={onDelete} />} {onDelete && <RemoveFile onRemove={onDelete} />}
</div> </div>
); );

View file

@ -11,7 +11,7 @@ const FilePreview = ({
fileType, fileType,
className = '', className = '',
}: { }: {
file?: ExtendedFile | TFile; file?: Partial<ExtendedFile | TFile>;
fileType: { fileType: {
paths: React.FC; paths: React.FC;
fill: string; fill: string;

View file

@ -1,6 +1,27 @@
import { memo } from 'react';
import { imageExtRegex } from 'librechat-data-provider'; import { imageExtRegex } from 'librechat-data-provider';
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider'; import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
import Image from '~/components/Chat/Messages/Content/Image'; import Image from '~/components/Chat/Messages/Content/Image';
import { useAttachmentLink } from './LogLink';
const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
const { handleDownload } = useAttachmentLink({
href: attachment.filepath,
filename: attachment.filename,
});
const extension = attachment.filename.split('.').pop();
return (
<FileContainer
file={attachment}
onClick={handleDownload}
overrideType={extension}
containerClassName="max-w-fit"
buttonClassName="hover:cursor-pointer hover:bg-surface-secondary active:bg-surface-secondary focus:bg-surface-secondary hover:border-border-heavy active:border-border-heavy"
/>
);
});
export default function Attachment({ attachment }: { attachment?: TAttachment }) { export default function Attachment({ attachment }: { attachment?: TAttachment }) {
if (!attachment) { if (!attachment) {
@ -21,5 +42,5 @@ export default function Attachment({ attachment }: { attachment?: TAttachment })
/> />
); );
} }
return null; return <FileAttachment attachment={attachment} />;
} }

View file

@ -7,7 +7,7 @@ import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import { useProgress, useLocalize } from '~/hooks'; import { useProgress, useLocalize } from '~/hooks';
import { CodeInProgress } from './CodeProgress'; import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment'; import Attachment from './Attachment';
import LogContent from './LogContent'; import Stdout from './Stdout';
import store from '~/store'; import store from '~/store';
interface ParsedArgs { interface ParsedArgs {
@ -17,8 +17,17 @@ interface ParsedArgs {
export function useParseArgs(args: string): ParsedArgs { export function useParseArgs(args: string): ParsedArgs {
return useMemo(() => { return useMemo(() => {
let parsedArgs: ParsedArgs | string = args;
try {
parsedArgs = JSON.parse(args);
} catch {
// console.error('Failed to parse args:', e);
}
if (typeof parsedArgs === 'object') {
return parsedArgs;
}
const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/); const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/);
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"args"|$)/s); const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s);
let code = ''; let code = '';
if (codeMatch) { if (codeMatch) {
@ -26,7 +35,7 @@ export function useParseArgs(args: string): ParsedArgs {
if (code.endsWith('"}')) { if (code.endsWith('"}')) {
code = code.slice(0, -2); code = code.slice(0, -2);
} }
code = code.replace(/\\n/g, '\n').replace(/\\/g, ''); code = code.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
} }
return { return {
@ -99,15 +108,17 @@ export default function ExecuteCode({
color: 'white', color: 'white',
}} }}
> >
<pre className="shrink-0"> <Stdout output={output} />
<LogContent output={output} attachments={attachments} />
</pre>
</div> </div>
</div> </div>
)} )}
</div> </div>
)} )}
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)} <div className="mb-2 flex flex-wrap items-center gap-2.5">
{attachments?.map((attachment, index) => (
<Attachment attachment={attachment} key={index} />
))}
</div>
</> </>
); );
} }

View file

@ -8,11 +8,11 @@ interface LogLinkProps {
children: React.ReactNode; children: React.ReactNode;
} }
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => { export const useAttachmentLink = ({ href, filename }: Pick<LogLinkProps, 'href' | 'filename'>) => {
const { showToast } = useToastContext(); const { showToast } = useToastContext();
const { refetch: downloadFile } = useCodeOutputDownload(href); const { refetch: downloadFile } = useCodeOutputDownload(href);
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => { const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
event.preventDefault(); event.preventDefault();
try { try {
const stream = await downloadFile(); const stream = await downloadFile();
@ -36,6 +36,11 @@ const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
} }
}; };
return { handleDownload };
};
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
const { handleDownload } = useAttachmentLink({ href, filename });
return ( return (
<a <a
href={href} href={href}

View file

@ -0,0 +1,26 @@
import React, { useMemo } from 'react';
interface StdoutProps {
output?: string;
}
const Stdout: React.FC<StdoutProps> = ({ output = '' }) => {
const processedContent = useMemo(() => {
if (!output) {
return '';
}
const parts = output.split('Generated files:');
return parts[0].trim();
}, [output]);
return (
processedContent && (
<pre className="shrink-0">
<div>{processedContent}</div>
</pre>
)
);
};
export default Stdout;

View file

@ -5,7 +5,7 @@ export default function FileIcon({
file, file,
fileType, fileType,
}: { }: {
file?: ExtendedFile | TFile; file?: Partial<ExtendedFile | TFile>;
fileType: { fileType: {
fill: string; fill: string;
paths: React.FC; paths: React.FC;

View file

@ -128,7 +128,7 @@ export const useAutoSave = ({
const handleInput = debounce((e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleInput = debounce((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value; const value = e.target.value;
if (value) { if (value && value.length > 1) {
localStorage.setItem( localStorage.setItem(
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`, `${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
encodeBase64(value), encodeBase64(value),

View file

@ -48,6 +48,7 @@ export const fileTypes = {
title: 'File', title: 'File',
}, },
text: textDocument, text: textDocument,
txt: textDocument,
// application:, // application:,
/* Partial matches */ /* Partial matches */

14
package-lock.json generated
View file

@ -65,7 +65,7 @@
"@langchain/google-genai": "^0.2.2", "@langchain/google-genai": "^0.2.2",
"@langchain/google-vertexai": "^0.2.3", "@langchain/google-vertexai": "^0.2.3",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.0", "@librechat/agents": "^2.4.12",
"@librechat/data-schemas": "*", "@librechat/data-schemas": "*",
"@waylaidwanderer/fetch-event-source": "^3.0.1", "@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2", "axios": "^1.8.2",
@ -17629,9 +17629,9 @@
} }
}, },
"node_modules/@langchain/langgraph-sdk": { "node_modules/@langchain/langgraph-sdk": {
"version": "0.0.62", "version": "0.0.65",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.62.tgz", "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.65.tgz",
"integrity": "sha512-JJiQwjV5/uVtwiVH/lt+QXhuh0nGhylZSLkMQXc1923TBUC4SHwU0JIKEDqh820PlGNkUu0nODJSAzS/6zPRtQ==", "integrity": "sha512-Zn1FhiKr/mYa1+W5NcuCPWmdTtJS4UZYu+YVEjxgESd0aMX19FTkqjaSV6tFDcRqHHwlIgHloCSqHLkYWF/Zug==",
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"p-queue": "^6.6.2", "p-queue": "^6.6.2",
@ -17819,9 +17819,9 @@
} }
}, },
"node_modules/@librechat/agents": { "node_modules/@librechat/agents": {
"version": "2.4.0", "version": "2.4.12",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.0.tgz", "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.12.tgz",
"integrity": "sha512-ijPJw+lMMPJ+Y66xSbh0cCiuODihl0TET3CWAxZweVGqynYqtL8PvPqlxtw+jftmrLjDFV04UM2NiDbzDb87HA==", "integrity": "sha512-m8CEVCjVeQDKXMS0ISG4h4YXU1x51yWGPLCuiNbQI3k+fmDvEFqVuzFi15OhVfoGGQxbKfzGiiF8fsKK2xIKEw==",
"dependencies": { "dependencies": {
"@langchain/anthropic": "^0.3.16", "@langchain/anthropic": "^0.3.16",
"@langchain/aws": "^0.1.7", "@langchain/aws": "^0.1.7",

View file

@ -112,7 +112,7 @@ export const excelMimeTypes =
/^application\/(vnd\.ms-excel|msexcel|x-msexcel|x-ms-excel|x-excel|x-dos_ms_excel|xls|x-xls|vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet)$/; /^application\/(vnd\.ms-excel|msexcel|x-msexcel|x-ms-excel|x-excel|x-dos_ms_excel|xls|x-xls|vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet)$/;
export const textMimeTypes = export const textMimeTypes =
/^(text\/(x-c|x-csharp|x-c\+\+|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/; /^(text\/(x-c|x-csharp|tab-separated-values|x-c\+\+|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/;
export const applicationMimeTypes = export const applicationMimeTypes =
/^(application\/(epub\+zip|csv|json|pdf|x-tar|typescript|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation|spreadsheetml\.sheet)|xml|zip))$/; /^(application\/(epub\+zip|csv|json|pdf|x-tar|typescript|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation|spreadsheetml\.sheet)|xml|zip))$/;
@ -152,6 +152,7 @@ export const codeTypeMapping: { [key: string]: string } = {
yml: 'application/x-yaml', yml: 'application/x-yaml',
yaml: 'application/x-yaml', yaml: 'application/x-yaml',
log: 'text/plain', log: 'text/plain',
tsv: 'text/tab-separated-values',
}; };
export const retrievalMimeTypes = [ export const retrievalMimeTypes = [
@ -230,7 +231,7 @@ export const convertStringsToRegex = (patterns: string[]): RegExp[] =>
const regex = new RegExp(pattern); const regex = new RegExp(pattern);
acc.push(regex); acc.push(regex);
} catch (error) { } catch (error) {
console.error(`Invalid regex pattern "${pattern}" skipped.`); console.error(`Invalid regex pattern "${pattern}" skipped.`, error);
} }
return acc; return acc;
}, []); }, []);