Compare commits

...

5 Commits

Author SHA1 Message Date
Ruben Talstra
faf349e0db fix: Cosmos DB: E11000 duplicate key error
I’ve updated this PR to remove unique: true and sparse: true from the optional social login fields (googleId, facebookId, etc.) and switch them to simple indexes (index: true). This resolves the Cosmos DB “duplicate key” errors caused by multiple null values and ensures compatibility with both Cosmos DB and MongoDB. The email field remains required and unique, preserving overall identity uniqueness.
2025-02-12 19:22:37 +01:00
Ruben Talstra
915022bc08 Merge branch 'main' into partial-filter-index 2025-02-12 19:05:32 +01:00
Danny Avila
2a506df443 🪄 fix: Agent Artifacts condition 2025-02-11 19:44:20 -05:00
Danny Avila
bfbaaebd2b 🪄 feat: Agent Artifacts (#5804)
* refactor: remove artifacts toggle

* refactor: allow hiding side panel while allowing artifacts view

* chore: rename SidePanelGroup to SidePanel for clarity

* Revert "refactor: remove artifacts toggle"

This reverts commit f884c2cfcd.

* feat: add artifacts capability to agent configuration

* refactor: conditionally set artifacts mode based on endpoint type

* feat: Artifacts Capability for Agents

* refactor: enhance getStreamText method to handle intermediate replies and add `stream_options` for openai/azure

* feat: localize progress text and improve UX in CodeAnalyze and ExecuteCode components for expanding analysis
2025-02-11 18:00:38 -05:00
Danny Avila
a1c7110a94 refactor(userSchema): unique index definitions using partialFilterExpression instead of sparse 2024-03-07 11:06:14 -05:00
28 changed files with 549 additions and 325 deletions

View File

@@ -1066,9 +1066,14 @@ ${convo}
});
}
getStreamText() {
/**
*
* @param {string[]} [intermediateReply]
* @returns {string}
*/
getStreamText(intermediateReply) {
if (!this.streamHandler) {
return '';
return intermediateReply?.join('') ?? '';
}
let thinkMatch;
@@ -1088,7 +1093,10 @@ ${convo}
}
}
const reasoningTokens = reasoningText.length > 0 ? `:::thinking\n${reasoningText}\n:::\n` : '';
const reasoningTokens =
reasoningText.length > 0
? `:::thinking\n${reasoningText.replace('<think>', '').replace('</think>', '').trim()}\n:::\n`
: '';
return `${reasoningTokens}${this.streamHandler.tokens.join('')}`;
}
@@ -1327,11 +1335,19 @@ ${convo}
streamPromise = new Promise((resolve) => {
streamResolve = resolve;
});
/** @type {OpenAI.OpenAI.CompletionCreateParamsStreaming} */
const params = {
...modelOptions,
stream: true,
};
if (
this.options.endpoint === EModelEndpoint.openAI ||
this.options.endpoint === EModelEndpoint.azureOpenAI
) {
params.stream_options = { include_usage: true };
}
const stream = await openai.beta.chat.completions
.stream({
...modelOptions,
stream: true,
})
.stream(params)
.on('abort', () => {
/* Do nothing here */
})
@@ -1471,7 +1487,7 @@ ${convo}
err?.message?.includes('abort') ||
(err instanceof OpenAI.APIError && err?.message?.includes('abort'))
) {
return intermediateReply.join('');
return this.getStreamText(intermediateReply);
}
if (
err?.message?.includes(
@@ -1489,7 +1505,7 @@ ${convo}
if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
return this.getStreamText();
} else if (intermediateReply.length > 0) {
return intermediateReply.join('');
return this.getStreamText(intermediateReply);
} else {
throw err;
}
@@ -1497,7 +1513,7 @@ ${convo}
if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
return this.getStreamText();
} else if (intermediateReply.length > 0) {
return intermediateReply.join('');
return this.getStreamText(intermediateReply);
} else {
throw err;
}

View File

@@ -35,6 +35,9 @@ const agentSchema = mongoose.Schema(
model_parameters: {
type: Object,
},
artifacts: {
type: String,
},
access_level: {
type: Number,
},

View File

@@ -84,38 +84,31 @@ const userSchema = mongoose.Schema(
},
googleId: {
type: String,
unique: true,
sparse: true,
index: true,
},
facebookId: {
type: String,
unique: true,
sparse: true,
index: true,
},
openidId: {
type: String,
unique: true,
sparse: true,
index: true,
},
ldapId: {
type: String,
unique: true,
sparse: true,
index: true,
},
githubId: {
type: String,
unique: true,
sparse: true,
index: true,
},
discordId: {
type: String,
unique: true,
sparse: true,
index: true,
},
appleId: {
type: String,
unique: true,
sparse: true,
index: true,
},
plugins: {
type: Array,

View File

@@ -13,6 +13,7 @@ const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options')
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
const initCustom = require('~/server/services/Endpoints/custom/initialize');
const initGoogle = require('~/server/services/Endpoints/google/initialize');
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getCustomEndpointConfig } = require('~/server/services/Config');
const { loadAgentTools } = require('~/server/services/ToolService');
const AgentClient = require('~/server/controllers/agents/client');
@@ -72,6 +73,16 @@ const primeResources = async (_attachments, _tool_resources) => {
}
};
/**
* @param {object} params
* @param {ServerRequest} params.req
* @param {ServerResponse} params.res
* @param {Agent} params.agent
* @param {object} [params.endpointOption]
* @param {AgentToolResources} [params.tool_resources]
* @param {boolean} [params.isInitialAgent]
* @returns {Promise<Agent>}
*/
const initializeAgentOptions = async ({
req,
res,
@@ -132,6 +143,13 @@ const initializeAgentOptions = async ({
agent.model_parameters.model = agent.model;
}
if (typeof agent.artifacts === 'string' && agent.artifacts !== '') {
agent.additional_instructions = generateArtifactsPrompt({
endpoint: agent.provider,
artifacts: agent.artifacts,
});
}
const tokensModel =
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;

View File

@@ -200,6 +200,7 @@ function generateConfig(key, baseURL, endpoint) {
config.capabilities = [
AgentCapabilities.execute_code,
AgentCapabilities.file_search,
AgentCapabilities.artifacts,
AgentCapabilities.actions,
AgentCapabilities.tools,
];

View File

@@ -3,6 +3,11 @@ const { isEnabled } = require('~/server/utils');
const { findUser } = require('~/models');
const { logger } = require('~/config');
/**
* Returns a function that handles the social login flow for a given provider.
* @param {string} provider - The provider name (e.g. 'google', 'facebook').
* @param {Function} getProfileDetails - A function to extract user details from the provider profile.
*/
const socialLogin =
(provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => {
try {
@@ -31,6 +36,8 @@ const socialLogin =
});
return cb(null, newUser);
}
return cb(null, null);
} catch (err) {
logger.error(`[${provider}Login]`, err);
return cb(err);

View File

@@ -1,4 +1,4 @@
import { AgentCapabilities } from 'librechat-data-provider';
import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
import type { OptionWithIcon, ExtendedFile } from './types';
@@ -9,8 +9,8 @@ export type TAgentOption = OptionWithIcon &
};
export type TAgentCapabilities = {
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.file_search]: boolean;
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.end_after_tools]?: boolean;
[AgentCapabilities.hide_sequential_outputs]?: boolean;
};
@@ -26,4 +26,5 @@ export type AgentForm = {
tools?: string[];
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
[AgentCapabilities.artifacts]?: ArtifactModes | string;
} & TAgentCapabilities;

View File

@@ -68,8 +68,8 @@ export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
export type LocalizeFunction = (
phraseKey: TranslationKeys,
options?: Record<string, string | number>
phraseKey: TranslationKeys,
options?: Record<string, string | number>,
) => string;
export type ChatFormValues = { text: string };
@@ -89,6 +89,7 @@ export type IconMapProps = {
iconURL?: string;
context?: 'landing' | 'menu-item' | 'nav' | 'message';
endpoint?: string | null;
endpointType?: string;
assistantName?: string;
agentName?: string;
avatar?: string;

View File

@@ -28,7 +28,7 @@ function ChatView({ index = 0 }: { index?: number }) {
select: useCallback(
(data: TMessage[]) => {
const dataTree = buildTree({ messages: data, fileMap });
return dataTree?.length === 0 ? null : dataTree ?? null;
return dataTree?.length === 0 ? null : (dataTree ?? null);
},
[fileMap],
),
@@ -62,7 +62,7 @@ function ChatView({ index = 0 }: { index?: number }) {
<ChatFormProvider {...methods}>
<ChatContext.Provider value={chatHelpers}>
<AddedChatContext.Provider value={addedChatHelpers}>
<Presentation useSidePanel={true}>
<Presentation>
{content}
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
<ChatForm index={index} />

View File

@@ -7,6 +7,9 @@ import FinishedIcon from './FinishedIcon';
import MarkdownLite from './MarkdownLite';
import store from '~/store';
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
export default function CodeAnalyze({
initialProgress = 0.1,
code,
@@ -22,9 +25,6 @@ export default function CodeAnalyze({
const progress = useProgress(initialProgress);
const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
const logs = outputs.reduce((acc, output) => {
@@ -53,9 +53,10 @@ export default function CodeAnalyze({
<ProgressText
progress={progress}
onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing"
finishedText="Finished analyzing"
inProgressText={localize('com_ui_analyzing')}
finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length}
isExpanded={showCode}
/>
</div>
{showCode && (

View File

@@ -4,10 +4,10 @@ import type { TAttachment } from 'librechat-data-provider';
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import { useProgress, useLocalize } from '~/hooks';
import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment';
import LogContent from './LogContent';
import { useProgress } from '~/hooks';
import store from '~/store';
interface ParsedArgs {
@@ -36,6 +36,9 @@ export function useParseArgs(args: string): ParsedArgs {
}, [args]);
}
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
export default function ExecuteCode({
initialProgress = 0.1,
args,
@@ -49,14 +52,12 @@ export default function ExecuteCode({
isSubmitting: boolean;
attachments?: TAttachment[];
}) {
const localize = useLocalize();
const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode);
const { lang, code } = useParseArgs(args);
const progress = useProgress(initialProgress);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
return (
@@ -78,9 +79,10 @@ export default function ExecuteCode({
<ProgressText
progress={progress}
onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing"
finishedText="Finished analyzing"
inProgressText={localize('com_ui_analyzing')}
finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length}
isExpanded={showCode}
/>
</div>
{showCode && (
@@ -105,9 +107,7 @@ export default function ExecuteCode({
)}
</div>
)}
{attachments?.map((attachment, index) => (
<Attachment attachment={attachment} key={index} />
))}
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
</>
);
}

View File

@@ -42,6 +42,7 @@ export default function ProgressText({
authText,
hasInput = true,
popover = false,
isExpanded = false,
}: {
progress: number;
onClick?: () => void;
@@ -50,8 +51,9 @@ export default function ProgressText({
authText?: string;
hasInput?: boolean;
popover?: boolean;
isExpanded?: boolean;
}) {
const text = progress < 1 ? authText ?? inProgressText : finishedText;
const text = progress < 1 ? (authText ?? inProgressText) : finishedText;
return (
<Wrapper popover={popover}>
<button
@@ -61,7 +63,13 @@ export default function ProgressText({
onClick={onClick}
>
{text}
<svg width="16" height="17" viewBox="0 0 16 17" fill="none">
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
className={isExpanded ? 'rotate-180' : 'rotate-0'}
>
<path
className={hasInput ? '' : 'stroke-transparent'}
d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203"

View File

@@ -1,37 +1,19 @@
import { useRecoilValue } from 'recoil';
import { useEffect, useMemo } from 'react';
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider';
import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation, useGetStartupConfig } from '~/data-provider';
import { useDeleteFilesMutation } from '~/data-provider';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanel } from '~/components/SidePanel';
import { SidePanelGroup } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks';
import { EditorProvider } from '~/Providers';
import store from '~/store';
const defaultInterface = getConfigDefaults().interface;
export default function Presentation({
children,
useSidePanel = false,
panel,
}: {
children: React.ReactNode;
panel?: React.ReactNode;
useSidePanel?: boolean;
}) {
const { data: startupConfig } = useGetStartupConfig();
export default function Presentation({ children }: { children: React.ReactNode }) {
const artifacts = useRecoilValue(store.artifactsState);
const codeArtifacts = useRecoilValue(store.codeArtifacts);
const hideSidePanel = useRecoilValue(store.hideSidePanel);
const artifactsVisible = useRecoilValue(store.artifactsVisible);
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const setFilesToDelete = useSetFilesToDelete();
const { mutateAsync } = useDeleteFilesMutation({
@@ -83,35 +65,24 @@ export default function Presentation({
</div>
);
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
<SidePanel
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
fullPanelCollapse={fullCollapse}
artifacts={
artifactsVisible === true &&
codeArtifacts === true &&
Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanel>
</DragDropWrapper>
);
}
return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
{layout()}
{panel != null && panel}
<SidePanelGroup
defaultLayout={defaultLayout}
fullPanelCollapse={fullCollapse}
defaultCollapsed={defaultCollapsed}
artifacts={
artifactsVisible === true && Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanelGroup>
</DragDropWrapper>
);
}

View File

@@ -10,7 +10,7 @@ import {
AgentCapabilities,
} from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
@@ -26,6 +26,7 @@ import AgentAvatar from './AgentAvatar';
import { Spinner } from '~/components';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import Artifacts from './Artifacts';
import AgentTool from './AgentTool';
import CodeForm from './Code/Form';
import { Panel } from '~/common';
@@ -77,6 +78,10 @@ export default function AgentConfig({
() => agentsConfig?.capabilities.includes(AgentCapabilities.actions),
[agentsConfig],
);
const artifactsEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.artifacts) ?? false,
[agentsConfig],
);
const fileSearchEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.file_search) ?? false,
[agentsConfig],
@@ -150,7 +155,7 @@ export default function AgentConfig({
onSuccess: (data) => {
setCurrentAgentId(data.id);
showToast({
message: `${localize('com_assistants_create_success ')} ${
message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
@@ -178,18 +183,10 @@ export default function AgentConfig({
}, [agent_id, setActivePanel, showToast, localize]);
const providerValue = typeof provider === 'string' ? provider : provider?.value;
let Icon: IconComponentTypes | null | undefined;
let endpointType: EModelEndpoint | undefined;
let endpointIconURL: string | undefined;
let iconKey: string | undefined;
let Icon:
| React.ComponentType<
React.SVGProps<SVGSVGElement> & {
endpoint: string;
endpointType: EModelEndpoint | undefined;
iconURL: string | undefined;
}
>
| undefined;
if (providerValue !== undefined) {
endpointType = getEndpointField(endpointsConfig, providerValue as string, 'type');
@@ -337,7 +334,7 @@ export default function AgentConfig({
</div>
</button>
</div>
{(codeEnabled || fileSearchEnabled) && (
{(codeEnabled || fileSearchEnabled || artifactsEnabled) && (
<div className="mb-4 flex w-full flex-col items-start gap-3">
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_capabilities')}
@@ -346,6 +343,8 @@ export default function AgentConfig({
{codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{/* Artifacts */}
{artifactsEnabled && <Artifacts />}
</div>
)}
{/* Agent Tools & Actions */}

View File

@@ -120,6 +120,7 @@ export default function AgentPanel({
const {
name,
artifacts,
description,
instructions,
model: _model,
@@ -139,6 +140,7 @@ export default function AgentPanel({
agent_id,
data: {
name,
artifacts,
description,
instructions,
model,
@@ -162,6 +164,7 @@ export default function AgentPanel({
create.mutate({
name,
artifacts,
description,
instructions,
model,
@@ -184,7 +187,7 @@ export default function AgentPanel({
const canEditAgent = useMemo(() => {
const canEdit =
agentQuery.data?.isCollaborative ?? false
(agentQuery.data?.isCollaborative ?? false)
? true
: agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN;

View File

@@ -55,8 +55,8 @@ export default function AgentSelect({
};
const capabilities: TAgentCapabilities = {
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.file_search]: false,
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.end_after_tools]: false,
[AgentCapabilities.hide_sequential_outputs]: false,
};

View File

@@ -0,0 +1,124 @@
import { useFormContext } from 'react-hook-form';
import { ArtifactModes, AgentCapabilities } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
import {
Switch,
HoverCard,
HoverCardPortal,
HoverCardContent,
HoverCardTrigger,
} from '~/components/ui';
import { useLocalize } from '~/hooks';
import { CircleHelpIcon } from '~/components/svg';
import { ESide } from '~/common';
export default function Artifacts() {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { setValue, watch } = methods;
const artifactsMode = watch(AgentCapabilities.artifacts);
const handleArtifactsChange = (value: boolean) => {
setValue(AgentCapabilities.artifacts, value ? ArtifactModes.DEFAULT : '', {
shouldDirty: true,
});
};
const handleShadcnuiChange = (value: boolean) => {
setValue(AgentCapabilities.artifacts, value ? ArtifactModes.SHADCNUI : ArtifactModes.DEFAULT, {
shouldDirty: true,
});
};
const handleCustomModeChange = (value: boolean) => {
setValue(AgentCapabilities.artifacts, value ? ArtifactModes.CUSTOM : ArtifactModes.DEFAULT, {
shouldDirty: true,
});
};
const isEnabled = artifactsMode !== undefined && artifactsMode !== '';
const isCustomEnabled = artifactsMode === ArtifactModes.CUSTOM;
const isShadcnEnabled = artifactsMode === ArtifactModes.SHADCNUI;
return (
<div className="w-full">
<div className="mb-1.5 flex items-center gap-2">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_ui_artifacts')}
</label>
</span>
</div>
<div className="flex flex-col gap-3">
<SwitchItem
id="artifacts"
label={localize('com_ui_artifacts_toggle_agent')}
checked={isEnabled}
onCheckedChange={handleArtifactsChange}
hoverCardText={localize('com_nav_info_code_artifacts_agent')}
/>
<SwitchItem
id="includeShadcnui"
label={localize('com_ui_include_shadcnui_agent')}
checked={isShadcnEnabled}
onCheckedChange={handleShadcnuiChange}
hoverCardText={localize('com_nav_info_include_shadcnui')}
disabled={!isEnabled || isCustomEnabled}
/>
<SwitchItem
id="customPromptMode"
label={localize('com_ui_custom_prompt_mode')}
checked={isCustomEnabled}
onCheckedChange={handleCustomModeChange}
hoverCardText={localize('com_nav_info_custom_prompt_mode')}
disabled={!isEnabled}
/>
</div>
</div>
);
}
function SwitchItem({
id,
label,
checked,
onCheckedChange,
hoverCardText,
disabled = false,
}: {
id: string;
label: string;
checked: boolean;
onCheckedChange: (value: boolean) => void;
hoverCardText: string;
disabled?: boolean;
}) {
return (
<HoverCard openDelay={50}>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className={disabled ? 'text-text-tertiary' : ''}>{label}</div>
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">{hoverCardText}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
<Switch
id={id}
checked={checked}
onCheckedChange={onCheckedChange}
className="ml-4"
data-testid={id}
disabled={disabled}
/>
</div>
</HoverCard>
);
}

View File

@@ -86,7 +86,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
</button>
)}
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<HoverCardPortal>

View File

@@ -29,7 +29,7 @@ export default function FileSearchCheckbox() {
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
/>
)}
@@ -38,7 +38,6 @@ export default function FileSearchCheckbox() {
type="button"
className="flex items-center space-x-2"
onClick={() =>
setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), {
shouldDirty: true,
})
@@ -51,7 +50,7 @@ export default function FileSearchCheckbox() {
{localize('com_agents_enable_file_search')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</button>
<HoverCardPortal>

View File

@@ -1,78 +1,58 @@
import throttle from 'lodash/throttle';
import { getConfigDefaults } from 'librechat-data-provider';
import { useState, useCallback, useMemo, memo } from 'react';
import { useUserKeyQuery } from 'librechat-data-provider/react-query';
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import { ResizableHandleAlt, ResizablePanel } from '~/components/ui/Resizable';
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useGetEndpointsQuery } from '~/data-provider';
import NavToggle from '~/components/Nav/NavToggle';
import { cn, getEndpointField } from '~/utils';
import { useChatContext } from '~/Providers';
import Switcher from './Switcher';
import Nav from './Nav';
interface SidePanelProps {
defaultLayout?: number[] | undefined;
defaultCollapsed?: boolean;
navCollapsedSize?: number;
fullPanelCollapse?: boolean;
artifacts?: React.ReactNode;
children: React.ReactNode;
}
const defaultMinSize = 20;
const defaultInterface = getConfigDefaults().interface;
const normalizeLayout = (layout: number[]) => {
const sum = layout.reduce((acc, size) => acc + size, 0);
if (Math.abs(sum - 100) < 0.01) {
return layout.map((size) => Number(size.toFixed(2)));
}
const factor = 100 / sum;
const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2)));
const adjustedSum = normalizedLayout.reduce(
(acc, size, index) => (index === layout.length - 1 ? acc : acc + size),
0,
);
normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2));
return normalizedLayout;
};
const SidePanel = ({
defaultLayout = [97, 3],
defaultCollapsed = false,
fullPanelCollapse = false,
defaultSize,
panelRef,
navCollapsedSize = 3,
artifacts,
children,
}: SidePanelProps) => {
hasArtifacts,
minSize,
setMinSize,
collapsedSize,
setCollapsedSize,
isCollapsed,
setIsCollapsed,
fullCollapse,
setFullCollapse,
interfaceConfig,
}: {
defaultSize?: number;
hasArtifacts: boolean;
navCollapsedSize?: number;
minSize: number;
setMinSize: React.Dispatch<React.SetStateAction<number>>;
collapsedSize: number;
setCollapsedSize: React.Dispatch<React.SetStateAction<number>>;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
fullCollapse: boolean;
setFullCollapse: React.Dispatch<React.SetStateAction<boolean>>;
panelRef: React.RefObject<ImperativePanelHandle>;
interfaceConfig: TInterfaceConfig;
}) => {
const localize = useLocalize();
const [isHovering, setIsHovering] = useState(false);
const [minSize, setMinSize] = useState(defaultMinSize);
const [newUser, setNewUser] = useLocalStorage('newUser', true);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => (startupConfig?.interface ?? defaultInterface) as Partial<TInterfaceConfig>,
[startupConfig],
);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const { conversation } = useChatContext();
const { endpoint } = conversation ?? {};
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
const panelRef = useRef<ImperativePanelHandle>(null);
const defaultActive = useMemo(() => {
const activePanel = localStorage.getItem('side:active-panel');
return typeof activePanel === 'string' ? activePanel : undefined;
@@ -113,46 +93,6 @@ const SidePanel = ({
interfaceConfig,
});
const calculateLayout = useCallback(() => {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttledSaveLayout = useCallback(
throttle((sizes: number[]) => {
const normalizedSizes = normalizeLayout(sizes);
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
useEffect(() => {
if (isSmallScreen) {
setIsCollapsed(true);
setCollapsedSize(0);
setMinSize(defaultMinSize);
setFullCollapse(true);
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
return;
} else {
setIsCollapsed(defaultCollapsed);
setCollapsedSize(navCollapsedSize);
setMinSize(defaultMinSize);
}
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
const toggleNavVisible = useCallback(() => {
if (newUser) {
setNewUser(false);
@@ -173,127 +113,84 @@ const SidePanel = ({
}
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
return (
<>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className="relative flex w-px items-center justify-center"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className="relative flex w-px items-center justify-center"
>
<NavToggle
navVisible={!isCollapsed}
isHovering={isHovering}
onToggle={toggleNavVisible}
setIsHovering={setIsHovering}
className={cn(
'fixed top-1/2',
(isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'mr-9'
: 'mr-16',
)}
translateX={false}
side="right"
/>
</div>
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
<ResizableHandleAlt withHandle className="bg-transparent text-text-primary" />
)}
<ResizablePanel
tagName="nav"
id="controls-nav"
order={artifacts != null ? 3 : 2}
aria-label={localize('com_ui_controls')}
role="region"
collapsedSize={collapsedSize}
defaultSize={currentLayout[currentLayout.length - 1]}
collapsible={true}
minSize={minSize}
maxSize={40}
ref={panelRef}
style={{
overflowY: 'auto',
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
}}
onExpand={() => {
setIsCollapsed(false);
localStorage.setItem('react-resizable-panels:collapsed', 'false');
}}
onCollapse={() => {
setIsCollapsed(true);
localStorage.setItem('react-resizable-panels:collapsed', 'true');
}}
<NavToggle
navVisible={!isCollapsed}
isHovering={isHovering}
onToggle={toggleNavVisible}
setIsHovering={setIsHovering}
className={cn(
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity',
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'hidden min-w-0'
: 'opacity-100',
'fixed top-1/2',
(isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'mr-9'
: 'mr-16',
)}
>
{interfaceConfig.modelSelect === true && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
isCollapsed ? 'h-[52px]' : 'px-2',
)}
>
<Switcher
isCollapsed={isCollapsed}
endpointKeyProvided={keyProvided}
endpoint={endpoint}
/>
</div>
)}
<Nav
resize={panelRef.current?.resize}
isCollapsed={isCollapsed}
defaultActive={defaultActive}
links={Links}
/>
</ResizablePanel>
</ResizablePanelGroup>
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
panelRef.current?.collapse();
translateX={false}
side="right"
/>
</div>
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
<ResizableHandleAlt withHandle className="bg-transparent text-text-primary" />
)}
<ResizablePanel
tagName="nav"
id="controls-nav"
order={hasArtifacts != null ? 3 : 2}
aria-label={localize('com_ui_controls')}
role="region"
collapsedSize={collapsedSize}
defaultSize={defaultSize}
collapsible={true}
minSize={minSize}
maxSize={40}
ref={panelRef}
style={{
overflowY: 'auto',
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
}}
/>
onExpand={() => {
setIsCollapsed(false);
localStorage.setItem('react-resizable-panels:collapsed', 'false');
}}
onCollapse={() => {
setIsCollapsed(true);
localStorage.setItem('react-resizable-panels:collapsed', 'true');
}}
className={cn(
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity',
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'hidden min-w-0'
: 'opacity-100',
)}
>
{interfaceConfig.modelSelect === true && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
isCollapsed ? 'h-[52px]' : 'px-2',
)}
>
<Switcher
isCollapsed={isCollapsed}
endpointKeyProvided={keyProvided}
endpoint={endpoint}
/>
</div>
)}
<Nav
resize={panelRef.current?.resize}
isCollapsed={isCollapsed}
defaultActive={defaultActive}
links={Links}
/>
</ResizablePanel>
</>
);
};

View File

@@ -0,0 +1,152 @@
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import throttle from 'lodash/throttle';
import { useRecoilValue } from 'recoil';
import { getConfigDefaults } from 'librechat-data-provider';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { useGetStartupConfig } from '~/data-provider';
import { normalizeLayout } from '~/utils';
import { useMediaQuery } from '~/hooks';
import SidePanel from './SidePanel';
import store from '~/store';
interface SidePanelProps {
defaultLayout?: number[] | undefined;
defaultCollapsed?: boolean;
navCollapsedSize?: number;
fullPanelCollapse?: boolean;
artifacts?: React.ReactNode;
children: React.ReactNode;
}
const defaultMinSize = 20;
const defaultInterface = getConfigDefaults().interface;
const SidePanelGroup = ({
defaultLayout = [97, 3],
defaultCollapsed = false,
fullPanelCollapse = false,
navCollapsedSize = 3,
artifacts,
children,
}: SidePanelProps) => {
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const panelRef = useRef<ImperativePanelHandle>(null);
const [minSize, setMinSize] = useState(defaultMinSize);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const hideSidePanel = useRecoilValue(store.hideSidePanel);
const calculateLayout = useCallback(() => {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
const throttledSaveLayout = useCallback(
throttle((sizes: number[]) => {
const normalizedSizes = normalizeLayout(sizes);
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
useEffect(() => {
if (isSmallScreen) {
setIsCollapsed(true);
setCollapsedSize(0);
setMinSize(defaultMinSize);
setFullCollapse(true);
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
return;
} else {
setIsCollapsed(defaultCollapsed);
setCollapsedSize(navCollapsedSize);
setMinSize(defaultMinSize);
}
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
return (
<>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
{!hideSidePanel && interfaceConfig.sidePanel === true && (
<SidePanel
panelRef={panelRef}
minSize={minSize}
setMinSize={setMinSize}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
collapsedSize={collapsedSize}
setCollapsedSize={setCollapsedSize}
fullCollapse={fullCollapse}
setFullCollapse={setFullCollapse}
defaultSize={currentLayout[currentLayout.length - 1]}
hasArtifacts={artifacts != null}
interfaceConfig={interfaceConfig}
/>
)}
</ResizablePanelGroup>
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
panelRef.current?.collapse();
}}
/>
</>
);
};
export default memo(SidePanelGroup);

View File

@@ -1,2 +1,2 @@
export { default as SidePanel } from './SidePanel';
export { default as SidePanelGroup } from './SidePanelGroup';
export { default as SideNav } from './Nav';

View File

@@ -170,7 +170,10 @@ export default function useChatFunctions({
endpointType,
overrideConvoId,
overrideUserMessageId,
artifacts: getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode }),
artifacts:
endpoint !== EModelEndpoint.agents
? getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode })
: undefined,
},
convo,
) as TEndpointOption;
@@ -228,7 +231,6 @@ export default function useChatFunctions({
conversationId,
unfinished: false,
isCreatedByUser: false,
isEdited: isEditOrContinue,
iconURL: convo?.iconURL,
model: convo?.model,
error: false,

View File

@@ -1,4 +1,6 @@
{
"com_ui_analyzing": "Analyzing",
"com_ui_analyzing_finished": "Finished analyzing",
"com_a11y_ai_composing": "The AI is still composing.",
"com_a11y_end": "The AI has finished their reply.",
"com_a11y_start": "The AI has started their reply.",
@@ -372,6 +374,7 @@
"com_nav_help_faq": "Help & FAQ",
"com_nav_hide_panel": "Hide right-most side panel",
"com_nav_info_code_artifacts": "Enables the display of experimental code artifacts next to the chat",
"com_nav_info_code_artifacts_agent": "Enables the use of code artifacts for this agent. By default, additional instructions specific to the use of artifacts are added, unless \"Custom Prompt Mode\" is enabled.",
"com_nav_info_custom_prompt_mode": "When enabled, the default artifacts system prompt will not be included. All artifact-generating instructions must be provided manually in this mode.",
"com_nav_info_delete_cache_storage": "This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.",
"com_nav_info_enter_to_send": "When enabled, pressing `ENTER` will send your message. When disabled, pressing Enter will add a new line, and you'll need to press `CTRL + ENTER` / `⌘ + ENTER` to send your message.",
@@ -509,6 +512,7 @@
"com_ui_artifact_click": "Click to open",
"com_ui_artifacts": "Artifacts",
"com_ui_artifacts_toggle": "Toggle Artifacts UI",
"com_ui_artifacts_toggle_agent": "Enable Artifacts",
"com_ui_ascending": "Asc",
"com_ui_assistant": "Assistant",
"com_ui_assistant_delete_error": "There was an error deleting the assistant",
@@ -687,6 +691,7 @@
"com_ui_import_conversation_info": "Import conversations from a JSON file",
"com_ui_import_conversation_success": "Conversations imported successfully",
"com_ui_include_shadcnui": "Include shadcn/ui components instructions",
"com_ui_include_shadcnui_agent": "Include shadcn/ui instructions",
"com_ui_input": "Input",
"com_ui_instructions": "Instructions",
"com_ui_latest_footer": "Every AI for Everyone.",
@@ -857,4 +862,4 @@
"com_ui_zoom": "Zoom",
"com_user_message": "You",
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
}
}

View File

@@ -98,3 +98,21 @@ export const extractContent = (
}
return '';
};
export const normalizeLayout = (layout: number[]) => {
const sum = layout.reduce((acc, size) => acc + size, 0);
if (Math.abs(sum - 100) < 0.01) {
return layout.map((size) => Number(size.toFixed(2)));
}
const factor = 100 / sum;
const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2)));
const adjustedSum = normalizedLayout.reduce(
(acc, size, index) => (index === layout.length - 1 ? acc : acc + size),
0,
);
normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2));
return normalizedLayout;
};

View File

@@ -1,4 +1,3 @@
import { z } from 'zod';
import type { ZodError } from 'zod';
import type { TModelsConfig } from './types';
@@ -144,6 +143,7 @@ export enum AgentCapabilities {
end_after_tools = 'end_after_tools',
execute_code = 'execute_code',
file_search = 'file_search',
artifacts = 'artifacts',
actions = 'actions',
tools = 'tools',
}
@@ -217,6 +217,7 @@ export const agentsEndpointSChema = baseEndpointSchema.merge(
.default([
AgentCapabilities.execute_code,
AgentCapabilities.file_search,
AgentCapabilities.artifacts,
AgentCapabilities.actions,
AgentCapabilities.tools,
]),

View File

@@ -155,6 +155,7 @@ export const defaultAgentFormValues = {
tools: [],
provider: {},
projectIds: [],
artifacts: '',
isCollaborative: false,
[Tools.execute_code]: false,
[Tools.file_search]: false,

View File

@@ -3,6 +3,7 @@ import type { AssistantsEndpoint, AgentProvider } from 'src/schemas';
import type { ContentTypes } from './runs';
import type { Agents } from './agents';
import type { TFile } from './files';
import { ArtifactModes } from 'src/artifacts';
export type Schema = OpenAPIV3.SchemaObject & { description?: string };
export type Reference = OpenAPIV3.ReferenceObject & { description?: string };
@@ -204,6 +205,7 @@ export type Agent = {
created_at: number;
avatar: AgentAvatar | null;
instructions: string | null;
additional_instructions?: string | null;
tools?: string[];
projectIds?: string[];
tool_kwargs?: Record<string, unknown>;
@@ -217,6 +219,7 @@ export type Agent = {
agent_ids?: string[];
end_after_tools?: boolean;
hide_sequential_outputs?: boolean;
artifacts?: ArtifactModes;
};
export type TAgentsMap = Record<string, Agent | undefined>;
@@ -231,7 +234,7 @@ export type AgentCreateParams = {
provider: AgentProvider;
model: string | null;
model_parameters: AgentModelParameters;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs'>;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts'>;
export type AgentUpdateParams = {
name?: string | null;
@@ -247,7 +250,7 @@ export type AgentUpdateParams = {
projectIds?: string[];
removeProjectIds?: string[];
isCollaborative?: boolean;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs'>;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts'>;
export type AgentListParams = {
limit?: number;