Skip to content

Commit 24c0433

Browse files
authored
🖥️ feat: Code Interpreter API for Non-Agent Endpoints (danny-avila#6803)
* fix: Prevent parsing 'undefined' string in useLocalStorage initialization * feat: first pass, code interpreter badge * feat: Integrate API key authentication and default checked value in Code Interpreter Badge * refactor: Rename showMCPServers to showEphemeralBadges and update related components, memoize values in useChatBadges * refactor: Enhance AttachFileChat to support ephemeral agents in file attachment logic * fix: Add baseURL configuration option to legacy function call * refactor: Update dependency array in useDragHelpers to include handleFiles * refactor: Update isEphemeralAgent function to accept optional endpoint parameter * refactor: Update file handling to support ephemeral agents in AttachFileMenu and useDragHelpers * fix: improve compatibility issues with OpenAI usage field handling in createRun function * refactor: usage field compatibility * fix: ensure mcp servers are no longer "selected" if mcp servers are now unavailable
1 parent 5d66874 commit 24c0433

File tree

19 files changed

+310
-47
lines changed

19 files changed

+310
-47
lines changed

api/app/clients/llm/createLLM.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function createLLM({
3434
let credentials = { openAIApiKey };
3535
let configuration = {
3636
apiKey: openAIApiKey,
37+
...(configOptions.basePath && { baseURL: configOptions.basePath }),
3738
};
3839

3940
/** @type {AzureOptions} */

api/models/Agent.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const mongoose = require('mongoose');
22
const { agentSchema } = require('@librechat/data-schemas');
3-
const { SystemRoles } = require('librechat-data-provider');
3+
const { SystemRoles, Tools } = require('librechat-data-provider');
44
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } =
55
require('librechat-data-provider').Constants;
66
const { CONFIG_STORE, STARTUP_CONFIG } = require('librechat-data-provider').CacheKeys;
@@ -51,16 +51,22 @@ const loadEphemeralAgent = ({ req, agent_id, endpoint, model_parameters: _m }) =
5151
const mcpServers = new Set(req.body.ephemeralAgent?.mcp);
5252
/** @type {string[]} */
5353
const tools = [];
54+
if (req.body.ephemeralAgent?.execute_code === true) {
55+
tools.push(Tools.execute_code);
56+
}
5457

55-
for (const toolName of Object.keys(availableTools)) {
56-
if (!toolName.includes(mcp_delimiter)) {
57-
continue;
58-
}
59-
const mcpServer = toolName.split(mcp_delimiter)?.[1];
60-
if (mcpServer && mcpServers.has(mcpServer)) {
61-
tools.push(toolName);
58+
if (mcpServers.size > 0) {
59+
for (const toolName of Object.keys(availableTools)) {
60+
if (!toolName.includes(mcp_delimiter)) {
61+
continue;
62+
}
63+
const mcpServer = toolName.split(mcp_delimiter)?.[1];
64+
if (mcpServer && mcpServers.has(mcpServer)) {
65+
tools.push(toolName);
66+
}
6267
}
6368
}
69+
6470
const instructions = req.body.promptPrefix;
6571
return {
6672
id: agent_id,

api/server/controllers/agents/run.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ const { providerEndpointMap, KnownEndpoints } = require('librechat-data-provider
1111
* @typedef {import('@librechat/agents').IState} IState
1212
*/
1313

14+
const customProviders = new Set([
15+
Providers.XAI,
16+
Providers.OLLAMA,
17+
Providers.DEEPSEEK,
18+
Providers.OPENROUTER,
19+
]);
20+
1421
/**
1522
* Creates a new Run instance with custom handlers and configuration.
1623
*
@@ -43,8 +50,11 @@ async function createRun({
4350
agent.model_parameters,
4451
);
4552

46-
/** Resolves Mistral type strictness due to new OpenAI usage field */
47-
if (agent.endpoint?.toLowerCase().includes(KnownEndpoints.mistral)) {
53+
/** Resolves issues with new OpenAI usage field */
54+
if (
55+
customProviders.has(agent.provider) ||
56+
(agent.provider === Providers.OPENAI && agent.endpoint !== agent.provider)
57+
) {
4858
llmConfig.streamUsage = false;
4959
llmConfig.usage = true;
5060
}

client/src/components/Chat/Input/BadgeRow.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import React, {
1010
} from 'react';
1111
import { useRecoilValue, useRecoilCallback } from 'recoil';
1212
import type { LucideIcon } from 'lucide-react';
13+
import CodeInterpreter from './CodeInterpreter';
1314
import type { BadgeItem } from '~/common';
1415
import { useChatBadges } from '~/hooks';
1516
import { Badge } from '~/components/ui';
1617
import MCPSelect from './MCPSelect';
1718
import store from '~/store';
1819

1920
interface BadgeRowProps {
20-
showMCPServers?: boolean;
21+
showEphemeralBadges?: boolean;
2122
onChange: (badges: Pick<BadgeItem, 'id'>[]) => void;
2223
onToggle?: (badgeId: string, currentActive: boolean) => void;
2324
conversationId?: string | null;
@@ -131,7 +132,13 @@ const dragReducer = (state: DragState, action: DragAction): DragState => {
131132
}
132133
};
133134

134-
function BadgeRow({ showMCPServers, conversationId, onChange, onToggle, isInChat }: BadgeRowProps) {
135+
function BadgeRow({
136+
showEphemeralBadges,
137+
conversationId,
138+
onChange,
139+
onToggle,
140+
isInChat,
141+
}: BadgeRowProps) {
135142
const [orderedBadges, setOrderedBadges] = useState<BadgeItem[]>([]);
136143
const [dragState, dispatch] = useReducer(dragReducer, {
137144
draggedBadge: null,
@@ -146,7 +153,7 @@ function BadgeRow({ showMCPServers, conversationId, onChange, onToggle, isInChat
146153
const animationFrame = useRef<number | null>(null);
147154
const containerRectRef = useRef<DOMRect | null>(null);
148155

149-
const allBadges = useChatBadges() || [];
156+
const allBadges = useChatBadges();
150157
const isEditing = useRecoilValue(store.isEditingBadges);
151158

152159
const badges = useMemo(
@@ -345,7 +352,12 @@ function BadgeRow({ showMCPServers, conversationId, onChange, onToggle, isInChat
345352
/>
346353
</div>
347354
)}
348-
{showMCPServers === true && <MCPSelect conversationId={conversationId} />}
355+
{showEphemeralBadges === true && (
356+
<>
357+
<CodeInterpreter conversationId={conversationId} />
358+
<MCPSelect conversationId={conversationId} />
359+
</>
360+
)}
349361
{ghostBadge && (
350362
<div
351363
className="ghost-badge h-full"

client/src/components/Chat/Input/ChatForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
289289
<AttachFileChat disableInputs={disableInputs} />
290290
</div>
291291
<BadgeRow
292-
showMCPServers={!isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint)}
292+
showEphemeralBadges={!isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint)}
293293
conversationId={conversation?.conversationId ?? Constants.NEW_CONVO}
294294
onChange={setBadges}
295295
isInChat={
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import debounce from 'lodash/debounce';
2+
import React, { memo, useMemo, useCallback } from 'react';
3+
import { useRecoilState } from 'recoil';
4+
import { TerminalSquareIcon } from 'lucide-react';
5+
import {
6+
Tools,
7+
AuthType,
8+
Constants,
9+
LocalStorageKeys,
10+
PermissionTypes,
11+
Permissions,
12+
} from 'librechat-data-provider';
13+
import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
14+
import { useLocalize, useHasAccess, useCodeApiKeyForm } from '~/hooks';
15+
import CheckboxButton from '~/components/ui/CheckboxButton';
16+
import useLocalStorage from '~/hooks/useLocalStorageAlt';
17+
import { useVerifyAgentToolAuth } from '~/data-provider';
18+
import { ephemeralAgentByConvoId } from '~/store';
19+
20+
function CodeInterpreter({ conversationId }: { conversationId?: string | null }) {
21+
const localize = useLocalize();
22+
const key = conversationId ?? Constants.NEW_CONVO;
23+
24+
const canRunCode = useHasAccess({
25+
permissionType: PermissionTypes.RUN_CODE,
26+
permission: Permissions.USE,
27+
});
28+
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
29+
const isCodeToggleEnabled = useMemo(() => {
30+
return ephemeralAgent?.execute_code ?? false;
31+
}, [ephemeralAgent?.execute_code]);
32+
33+
const { data } = useVerifyAgentToolAuth(
34+
{ toolId: Tools.execute_code },
35+
{
36+
retry: 1,
37+
},
38+
);
39+
const authType = useMemo(() => data?.message ?? false, [data?.message]);
40+
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
41+
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
42+
useCodeApiKeyForm({});
43+
44+
const setValue = useCallback(
45+
(isChecked: boolean) => {
46+
setEphemeralAgent((prev) => ({
47+
...prev,
48+
execute_code: isChecked,
49+
}));
50+
},
51+
[setEphemeralAgent],
52+
);
53+
54+
const [runCode, setRunCode] = useLocalStorage<boolean>(
55+
`${LocalStorageKeys.LAST_CODE_TOGGLE_}${key}`,
56+
isCodeToggleEnabled,
57+
setValue,
58+
);
59+
60+
const handleChange = useCallback(
61+
(isChecked: boolean) => {
62+
if (!isAuthenticated) {
63+
setIsDialogOpen(true);
64+
return;
65+
}
66+
setRunCode(isChecked);
67+
},
68+
[setRunCode, setIsDialogOpen, isAuthenticated],
69+
);
70+
71+
const debouncedChange = useMemo(
72+
() => debounce(handleChange, 50, { leading: true }),
73+
[handleChange],
74+
);
75+
76+
if (!canRunCode) {
77+
return null;
78+
}
79+
80+
return (
81+
<>
82+
<CheckboxButton
83+
className="max-w-fit"
84+
defaultChecked={runCode}
85+
setValue={debouncedChange}
86+
label={localize('com_assistants_code_interpreter')}
87+
isCheckedClassName="border-purple-600/40 bg-purple-500/10 hover:bg-purple-700/10"
88+
icon={<TerminalSquareIcon className="icon-md" />}
89+
/>
90+
<ApiKeyDialog
91+
onSubmit={onSubmit}
92+
isOpen={isDialogOpen}
93+
register={methods.register}
94+
onRevoke={handleRevokeApiKey}
95+
onOpenChange={setIsDialogOpen}
96+
handleSubmit={methods.handleSubmit}
97+
isToolAuthenticated={isAuthenticated}
98+
isUserProvided={authType === AuthType.USER_PROVIDED}
99+
/>
100+
</>
101+
);
102+
}
103+
104+
export default memo(CodeInterpreter);

client/src/components/Chat/Input/Files/AttachFileChat.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
import { memo, useMemo } from 'react';
22
import { useRecoilValue } from 'recoil';
33
import {
4+
Constants,
45
supportsFiles,
56
mergeFileConfig,
67
isAgentsEndpoint,
8+
isEphemeralAgent,
79
EndpointFileConfig,
810
fileConfig as defaultFileConfig,
911
} from 'librechat-data-provider';
1012
import { useChatContext } from '~/Providers';
1113
import { useGetFileConfig } from '~/data-provider';
14+
import { ephemeralAgentByConvoId } from '~/store';
1215
import AttachFileMenu from './AttachFileMenu';
1316
import AttachFile from './AttachFile';
14-
import store from '~/store';
1517

1618
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
1719
const { conversation } = useChatContext();
1820

1921
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
2022

21-
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
23+
const key = conversation?.conversationId ?? Constants.NEW_CONVO;
24+
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(key));
25+
const isAgents = useMemo(
26+
() => isAgentsEndpoint(_endpoint) || isEphemeralAgent(_endpoint, ephemeralAgent),
27+
[_endpoint, ephemeralAgent],
28+
);
2229

2330
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
2431
select: (data) => mergeFileConfig(data),

client/src/components/Chat/Input/Files/AttachFileMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ const AttachFile = ({ disabled }: AttachFileProps) => {
1818
const [isPopoverActive, setIsPopoverActive] = useState(false);
1919
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
2020
const { data: endpointsConfig } = useGetEndpointsQuery();
21-
const { handleFileChange } = useFileHandling();
21+
const { handleFileChange } = useFileHandling({
22+
overrideEndpoint: EModelEndpoint.agents,
23+
});
2224

2325
const capabilities = useMemo(
2426
() => endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [],

client/src/components/Chat/Input/MCPSelect.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { memo, useCallback } from 'react';
1+
import React, { memo, useRef, useMemo, useEffect, useCallback } from 'react';
22
import { useRecoilState } from 'recoil';
33
import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
44
import { useAvailableToolsQuery } from '~/data-provider';
@@ -10,8 +10,12 @@ import { useLocalize } from '~/hooks';
1010

1111
function MCPSelect({ conversationId }: { conversationId?: string | null }) {
1212
const localize = useLocalize();
13+
const hasSetFetched = useRef(false);
1314
const key = conversationId ?? Constants.NEW_CONVO;
1415
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
16+
const mcpState = useMemo(() => {
17+
return ephemeralAgent?.mcp ?? [];
18+
}, [ephemeralAgent?.mcp]);
1519
const setSelectedValues = useCallback(
1620
(values: string[] | null | undefined) => {
1721
if (!values) {
@@ -29,10 +33,10 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
2933
);
3034
const [mcpValues, setMCPValues] = useLocalStorage<string[]>(
3135
`${LocalStorageKeys.LAST_MCP_}${key}`,
32-
ephemeralAgent?.mcp ?? [],
36+
mcpState,
3337
setSelectedValues,
3438
);
35-
const { data: mcpServers } = useAvailableToolsQuery(EModelEndpoint.agents, {
39+
const { data: mcpServers, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, {
3640
select: (data) => {
3741
const serverNames = new Set<string>();
3842
data.forEach((tool) => {
@@ -45,6 +49,20 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
4549
},
4650
});
4751

52+
useEffect(() => {
53+
if (hasSetFetched.current) {
54+
return;
55+
}
56+
if (!isFetched) {
57+
return;
58+
}
59+
hasSetFetched.current = true;
60+
if ((mcpServers?.length ?? 0) > 0) {
61+
return;
62+
}
63+
setMCPValues([]);
64+
}, [isFetched, setMCPValues, mcpServers?.length]);
65+
4866
const renderSelectedValues = useCallback(
4967
(values: string[], placeholder?: string) => {
5068
if (values.length === 0) {
@@ -70,8 +88,8 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
7088
defaultSelectedValues={mcpValues ?? []}
7189
renderSelectedValues={renderSelectedValues}
7290
placeholder={localize('com_ui_mcp_servers')}
73-
popoverClassName="min-w-[200px]"
74-
className="badge-icon h-full min-w-[150px]"
91+
popoverClassName="min-w-fit"
92+
className="badge-icon min-w-fit"
7593
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
7694
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
7795
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-shadow md:w-full size-9 p-2 md:p-3 bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"

0 commit comments

Comments
 (0)