LibreChat/client/src/hooks/SSE/useStepHandler.ts
Danny Avila 1a815f5e19
🎉 feat: Code Interpreter API and Agents Release (#4860)
* feat: Code Interpreter API & File Search Agent Uploads

chore: add back code files

wip: first pass, abstract key dialog

refactor: influence checkbox on key changes

refactor: update localization keys for 'execute code' to 'run code'

wip: run code button

refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions

feat: first pass, API tool calling

fix: handle missing toolId in callTool function and return 404 for non-existent tools

feat: show code outputs

fix: improve error handling in callTool function and log errors

fix: handle potential null value for filepath in attachment destructuring

fix: normalize language before rendering and prevent null return

fix: add loading indicator in RunCode component while executing code

feat: add support for conditional code execution in Markdown components

feat: attachments

refactor: remove bash

fix: pass abort signal to graph/run

refactor: debounce and rate limit tool call

refactor: increase debounce delay for execute function

feat: set code output attachments

feat: image attachments

refactor: apply message context

refactor: pass `partIndex`

feat: toolCall schema/model/methods

feat: block indexing

feat: get tool calls

chore: imports

chore: typing

chore: condense type imports

feat: get tool calls

fix: block indexing

chore: typing

refactor: update tool calls mapping to support multiple results

fix: add unique key to nav link for rendering

wip: first pass, tool call results

refactor: update query cache from successful tool call mutation

style: improve result switcher styling

chore: note on using \`.toObject()\`

feat: add agent_id field to conversation schema

chore: typing

refactor: rename agentMap to agentsMap for consistency

feat: Agent Name as chat input placeholder

chore: bump agents

📦 chore: update @langchain dependencies to latest versions to match agents package

📦 chore: update @librechat/agents dependency to version 1.8.0

fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label

refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads

feat: upload menu

feat: prime message_file resources

feat: implement conversation access validation in chat route

refactor: remove file parameter from processFileUpload and use req.file instead

feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db

feat: prevent duplicate message saves by checking savedMessageIds in AgentController

refactor: skip legacy RAG API handling for agents

feat: add files field to convoSchema

refactor: update request type annotations from Express.Request to ServerRequest in file processing functions

feat: track conversation files

fix: resendFiles, addPreviousAttachments handling

feat: add ID validation for session_id and file_id in download route

feat: entity_id for code file uploads/downloads

fix: code file edge cases

feat: delete related tool calls

feat: add stream rate handling for LLM configuration

feat: enhance system content with attached file information

fix: improve error logging in resource priming function

* WIP: PoC, sequential agents

WIP: PoC Sequential Agents, first pass content data + bump agents package

fix: package-lock

WIP: PoC, o1 support, refactor bufferString

feat: convertJsonSchemaToZod

fix: form issues and schema defining erroneous model

fix: max length issue on agent form instructions, limit conversation messages to sequential agents

feat: add abort signal support to createRun function and AgentClient

feat: PoC, hide prior sequential agent steps

fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data

refactor: use only last contentData, track model for usage data

chore: bump agents package

fix: content parts issue

refactor: filter contentParts to include tool calls and relevant indices

feat: show function calls

refactor: filter context messages to exclude tool calls when no tools are available to the agent

fix: ensure tool call content is not undefined in formatMessages

feat: add agent_id field to conversationPreset schema

feat: hide sequential agents

feat: increase upload toast duration to 10 seconds

* refactor: tool context handling & update Code API Key Dialog

feat: toolContextMap

chore: skipSpecs -> useSpecs

ci: fix handleTools tests

feat: API Key Dialog

* feat: Agent Permissions Admin Controls

feat: replace label with button for prompt permission toggle

feat: update agent permissions

feat: enable experimental agents and streamline capability configuration

feat: implement access control for agents and enhance endpoint menu items

feat: add welcome message for agent selection in localization

feat: add agents permission to access control and update version to 0.7.57

* fix: update types in useAssistantListMap and useMentions hooks for better null handling

* feat: mention agents

* fix: agent tool resource race conditions when deleting agent tool resource files

* feat: add error handling for code execution with user feedback

* refactor: rename AdminControls to AdminSettings for clarity

* style: add gap to button in AdminSettings for improved layout

* refactor: separate agent query hooks and check access to enable fetching

* fix: remove unused provider from agent initialization options, creates issue with custom endpoints

* refactor: remove redundant/deprecated modelOptions from AgentClient processes

* chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json

* fix: minor styling issues + agent panel uniformity

* fix: agent edge cases when set endpoint is no longer defined

* refactor: remove unused cleanup function call from AppService

* fix: update link in ApiKeyDialog to point to pricing page

* fix: improve type handling and layout calculations in SidePanel component

* fix: add missing localization string for agent selection in SidePanel

* chore: form styling and localizations for upload filesearch/code interpreter

* fix: model selection placeholder logic in AgentConfig component

* style: agent capabilities

* fix: add localization for provider selection and improve dropdown styling in ModelPanel

* refactor: use gpt-4o-mini > gpt-3.5-turbo

* fix: agents configuration for loadDefaultInterface and update related tests

* feat: DALLE Agents support
2024-12-04 15:48:13 -05:00

303 lines
10 KiB
TypeScript

import { useCallback, useRef } from 'react';
import { StepTypes, ContentTypes, ToolCallTypes, getNonEmptyValue } from 'librechat-data-provider';
import type {
Agents,
TMessage,
PartMetadata,
EventSubmission,
TMessageContentParts,
} from 'librechat-data-provider';
import type { SetterOrUpdater } from 'recoil';
import type { AnnounceOptions } from '~/common';
import { MESSAGE_UPDATE_INTERVAL } from '~/common';
type TUseStepHandler = {
announcePolite: (options: AnnounceOptions) => void;
setMessages: (messages: TMessage[]) => void;
getMessages: () => TMessage[] | undefined;
setIsSubmitting: SetterOrUpdater<boolean>;
lastAnnouncementTimeRef: React.MutableRefObject<number>;
};
type TStepEvent = {
event: string;
data:
| Agents.MessageDeltaEvent
| Agents.RunStep
| Agents.ToolEndEvent
| {
runId?: string;
message: string;
};
};
type MessageDeltaUpdate = { type: ContentTypes.TEXT; text: string; tool_call_ids?: string[] };
type AllContentTypes =
| ContentTypes.TEXT
| ContentTypes.TOOL_CALL
| ContentTypes.IMAGE_FILE
| ContentTypes.IMAGE_URL
| ContentTypes.ERROR;
export default function useStepHandler({
setMessages,
getMessages,
setIsSubmitting,
announcePolite,
lastAnnouncementTimeRef,
}: TUseStepHandler) {
const toolCallIdMap = useRef(new Map<string, string | undefined>());
const messageMap = useRef(new Map<string, TMessage>());
const stepMap = useRef(new Map<string, Agents.RunStep>());
const updateContent = (
message: TMessage,
index: number,
contentPart: Agents.MessageContentComplex,
finalUpdate = false,
) => {
const contentType = contentPart.type ?? '';
if (!contentType) {
console.warn('No content type found in content part');
return message;
}
const updatedContent = [...(message.content || [])] as Array<
Partial<TMessageContentParts> | undefined
>;
if (!updatedContent[index]) {
updatedContent[index] = { type: contentPart.type as AllContentTypes };
}
if (
contentType.startsWith(ContentTypes.TEXT) &&
ContentTypes.TEXT in contentPart &&
typeof contentPart.text === 'string'
) {
const currentContent = updatedContent[index] as MessageDeltaUpdate;
const update: MessageDeltaUpdate = {
type: ContentTypes.TEXT,
text: (currentContent.text || '') + contentPart.text,
};
if (contentPart.tool_call_ids != null) {
update.tool_call_ids = contentPart.tool_call_ids;
}
updatedContent[index] = update;
} else if (contentType === ContentTypes.IMAGE_URL && 'image_url' in contentPart) {
const currentContent = updatedContent[index] as {
type: ContentTypes.IMAGE_URL;
image_url: string;
};
updatedContent[index] = {
...currentContent,
};
} else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined;
const existingToolCall = existingContent?.tool_call;
const toolCallArgs = (contentPart.tool_call.args as unknown as string | undefined) ?? '';
const args = finalUpdate
? contentPart.tool_call.args
: (existingToolCall?.args ?? '') + toolCallArgs;
const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? '';
const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? '';
const newToolCall: Agents.ToolCall & PartMetadata = {
id,
name,
args,
type: ToolCallTypes.TOOL_CALL,
};
if (finalUpdate) {
newToolCall.progress = 1;
newToolCall.output = contentPart.tool_call.output;
}
updatedContent[index] = {
type: ContentTypes.TOOL_CALL,
tool_call: newToolCall,
};
}
return { ...message, content: updatedContent as TMessageContentParts[] };
};
return useCallback(
({ event, data }: TStepEvent, submission: EventSubmission) => {
const messages = getMessages() || [];
const { userMessage } = submission;
setIsSubmitting(true);
const currentTime = Date.now();
if (currentTime - lastAnnouncementTimeRef.current > MESSAGE_UPDATE_INTERVAL) {
announcePolite({ message: 'composing', isStatus: true });
lastAnnouncementTimeRef.current = currentTime;
}
if (event === 'on_run_step') {
const runStep = data as Agents.RunStep;
const responseMessageId = runStep.runId ?? '';
if (!responseMessageId) {
console.warn('No message id found in run step event');
return;
}
stepMap.current.set(runStep.id, runStep);
let response = messageMap.current.get(responseMessageId);
if (!response) {
const responseMessage = messages[messages.length - 1] as TMessage;
response = {
...responseMessage,
parentMessageId: userMessage.messageId,
conversationId: userMessage.conversationId,
messageId: responseMessageId,
content: [],
};
messageMap.current.set(responseMessageId, response);
setMessages([...messages.slice(0, -1), response]);
}
// Store tool call IDs if present
if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) {
runStep.stepDetails.tool_calls.forEach((toolCall) => {
const toolCallId = toolCall.id ?? '';
if ('id' in toolCall && toolCallId) {
toolCallIdMap.current.set(runStep.id, toolCallId);
}
});
}
} else if (event === 'on_agent_update') {
const { runId, message } = data as { runId?: string; message: string };
const responseMessageId = runId ?? '';
if (!responseMessageId) {
console.warn('No message id found in agent update event');
return;
}
const responseMessage = messages[messages.length - 1] as TMessage;
const response = {
...responseMessage,
parentMessageId: userMessage.messageId,
conversationId: userMessage.conversationId,
messageId: responseMessageId,
content: [
{
type: ContentTypes.TEXT,
text: message,
},
],
} as TMessage;
setMessages([...messages.slice(0, -1), response]);
} else if (event === 'on_message_delta') {
const messageDelta = data as Agents.MessageDeltaEvent;
const runStep = stepMap.current.get(messageDelta.id);
const responseMessageId = runStep?.runId ?? '';
if (!runStep || !responseMessageId) {
console.warn('No run step or runId found for message delta event');
return;
}
const response = messageMap.current.get(responseMessageId);
if (response && messageDelta.delta.content) {
const contentPart = Array.isArray(messageDelta.delta.content)
? messageDelta.delta.content[0]
: messageDelta.delta.content;
const updatedResponse = updateContent(response, runStep.index, contentPart);
messageMap.current.set(responseMessageId, updatedResponse);
const currentMessages = getMessages() || [];
setMessages([...currentMessages.slice(0, -1), updatedResponse]);
}
} else if (event === 'on_run_step_delta') {
const runStepDelta = data as Agents.RunStepDeltaEvent;
const runStep = stepMap.current.get(runStepDelta.id);
const responseMessageId = runStep?.runId ?? '';
if (!runStep || !responseMessageId) {
console.warn('No run step or runId found for run step delta event');
return;
}
const response = messageMap.current.get(responseMessageId);
if (
response &&
runStepDelta.delta.type === StepTypes.TOOL_CALLS &&
runStepDelta.delta.tool_calls
) {
let updatedResponse = { ...response };
runStepDelta.delta.tool_calls.forEach((toolCallDelta) => {
const toolCallId = toolCallIdMap.current.get(runStepDelta.id) ?? '';
const contentPart: Agents.MessageContentComplex = {
type: ContentTypes.TOOL_CALL,
tool_call: {
name: toolCallDelta.name ?? '',
args: toolCallDelta.args ?? '',
id: toolCallId,
},
};
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart);
});
messageMap.current.set(responseMessageId, updatedResponse);
const updatedMessages = messages.map((msg) =>
msg.messageId === runStep.runId ? updatedResponse : msg,
);
setMessages(updatedMessages);
}
} else if (event === 'on_run_step_completed') {
const { result } = data as unknown as { result: Agents.ToolEndEvent };
const { id: stepId } = result;
const runStep = stepMap.current.get(stepId);
const responseMessageId = runStep?.runId ?? '';
if (!runStep || !responseMessageId) {
console.warn('No run step or runId found for completed tool call event');
return;
}
const response = messageMap.current.get(responseMessageId);
if (response) {
let updatedResponse = { ...response };
const contentPart: Agents.MessageContentComplex = {
type: ContentTypes.TOOL_CALL,
tool_call: result.tool_call,
};
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart, true);
messageMap.current.set(responseMessageId, updatedResponse);
const updatedMessages = messages.map((msg) =>
msg.messageId === runStep.runId ? updatedResponse : msg,
);
setMessages(updatedMessages);
}
}
return () => {
toolCallIdMap.current.clear();
messageMap.current.clear();
stepMap.current.clear();
};
},
[getMessages, setIsSubmitting, lastAnnouncementTimeRef, announcePolite, setMessages],
);
}