mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages (#6752)
* chore: bump @librechat/agents and related packages * refactor: update message state for tool calls run step, in case no tool call chunks are received * fix: avoid combining finalized args createContentAggregator for tool calls * chore: bump @librechat/agents to version 2.3.99 * feat: add support for aborting flows with AbortSignal in createFlow methods * fix: improve handling of tool call arguments in useStepHandler * chore: bump @librechat/agents to version 2.4.0 * fix: update flow identifier format for OAuth login in createActionTool to allow uniqueness per run * fix: improve error message handling for aborted flows in FlowStateManager * refactor: allow possible multi-agent cross-over for oauth login * fix: add type safety for Sandpack files in ArtifactCodeEditor
This commit is contained in:
parent
ac35b8490c
commit
9b0678da16
6 changed files with 953 additions and 981 deletions
|
|
@ -44,12 +44,12 @@
|
|||
"@googleapis/youtube": "^20.0.0",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.1",
|
||||
"@langchain/community": "^0.3.34",
|
||||
"@langchain/core": "^0.3.40",
|
||||
"@langchain/google-genai": "^0.1.11",
|
||||
"@langchain/google-vertexai": "^0.2.2",
|
||||
"@langchain/community": "^0.3.39",
|
||||
"@langchain/core": "^0.3.43",
|
||||
"@langchain/google-genai": "^0.2.2",
|
||||
"@langchain/google-vertexai": "^0.2.3",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@librechat/agents": "^2.3.95",
|
||||
"@librechat/agents": "^2.4.0",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||
"axios": "^1.8.2",
|
||||
|
|
|
|||
|
|
@ -191,24 +191,30 @@ async function createActionTool({
|
|||
};
|
||||
const flowManager = await getFlowStateManager(getLogStores);
|
||||
await flowManager.createFlowWithHandler(
|
||||
`${identifier}:login`,
|
||||
`${identifier}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`,
|
||||
'oauth_login',
|
||||
async () => {
|
||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
||||
logger.debug('Sent OAuth login request to client', { action_id, identifier });
|
||||
return true;
|
||||
},
|
||||
config?.signal,
|
||||
);
|
||||
logger.debug('Waiting for OAuth Authorization response', { action_id, identifier });
|
||||
const result = await flowManager.createFlow(identifier, 'oauth', {
|
||||
state: stateToken,
|
||||
userId: req.user.id,
|
||||
client_url: metadata.auth.client_url,
|
||||
redirect_uri: `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`,
|
||||
/** Encrypted values */
|
||||
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
||||
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
||||
});
|
||||
const result = await flowManager.createFlow(
|
||||
identifier,
|
||||
'oauth',
|
||||
{
|
||||
state: stateToken,
|
||||
userId: req.user.id,
|
||||
client_url: metadata.auth.client_url,
|
||||
redirect_uri: `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`,
|
||||
/** Encrypted values */
|
||||
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
||||
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
||||
},
|
||||
config?.signal,
|
||||
);
|
||||
logger.debug('Received OAuth Authorization response', { action_id, identifier });
|
||||
data.delta.auth = undefined;
|
||||
data.delta.expires_at = undefined;
|
||||
|
|
@ -264,6 +270,7 @@ async function createActionTool({
|
|||
`${identifier}:refresh`,
|
||||
'oauth_refresh',
|
||||
refreshTokens,
|
||||
config?.signal,
|
||||
);
|
||||
metadata.oauth_access_token = refreshData.access_token;
|
||||
if (refreshData.refresh_token) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {
|
|||
SandpackCodeEditor,
|
||||
SandpackProvider as StyledProvider,
|
||||
} from '@codesandbox/sandpack-react';
|
||||
import { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled';
|
||||
import type { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled';
|
||||
import type { SandpackBundlerFile } from '@codesandbox/sandpack-client';
|
||||
import type { CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||
import type { ArtifactFiles, Artifact } from '~/common';
|
||||
import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
|
||||
|
|
@ -66,7 +67,7 @@ const CodeEditor = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const currentCode = sandpack.files['/' + fileKey].code;
|
||||
const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code;
|
||||
|
||||
if (currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim()) {
|
||||
setCurrentCode(currentCode);
|
||||
|
|
|
|||
|
|
@ -123,11 +123,14 @@ export default function useStepHandler({
|
|||
} 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 toolCallArgs = (contentPart.tool_call as Agents.ToolCall).args;
|
||||
/** When args are a valid object, they are likely already invoked */
|
||||
const args =
|
||||
finalUpdate ||
|
||||
typeof existingToolCall?.args === 'object' ||
|
||||
typeof toolCallArgs === 'object'
|
||||
? 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]) ?? '';
|
||||
|
|
@ -195,12 +198,31 @@ export default function useStepHandler({
|
|||
|
||||
// Store tool call IDs if present
|
||||
if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) {
|
||||
runStep.stepDetails.tool_calls.forEach((toolCall) => {
|
||||
let updatedResponse = { ...response };
|
||||
(runStep.stepDetails.tool_calls as Agents.ToolCall[]).forEach((toolCall) => {
|
||||
const toolCallId = toolCall.id ?? '';
|
||||
if ('id' in toolCall && toolCallId) {
|
||||
toolCallIdMap.current.set(runStep.id, toolCallId);
|
||||
}
|
||||
|
||||
const contentPart: Agents.MessageContentComplex = {
|
||||
type: ContentTypes.TOOL_CALL,
|
||||
tool_call: {
|
||||
name: toolCall.name ?? '',
|
||||
args: toolCall.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_agent_update') {
|
||||
const { agent_update } = data as Agents.AgentUpdate;
|
||||
|
|
|
|||
1823
package-lock.json
generated
1823
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -55,13 +55,18 @@ export class FlowStateManager<T = unknown> {
|
|||
/**
|
||||
* Creates a new flow and waits for its completion
|
||||
*/
|
||||
async createFlow(flowId: string, type: string, metadata: FlowMetadata = {}): Promise<T> {
|
||||
async createFlow(
|
||||
flowId: string,
|
||||
type: string,
|
||||
metadata: FlowMetadata = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
const flowKey = this.getFlowKey(flowId, type);
|
||||
|
||||
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow already exists`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
|
@ -69,7 +74,7 @@ export class FlowStateManager<T = unknown> {
|
|||
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
const initialState: FlowState = {
|
||||
|
|
@ -81,10 +86,10 @@ export class FlowStateManager<T = unknown> {
|
|||
|
||||
this.logger.debug('Creating initial flow state:', flowKey);
|
||||
await this.keyv.set(flowKey, initialState, this.ttl);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
private monitorFlow(flowKey: string, type: string): Promise<T> {
|
||||
private monitorFlow(flowKey: string, type: string, signal?: AbortSignal): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const checkInterval = 2000;
|
||||
let elapsedTime = 0;
|
||||
|
|
@ -101,6 +106,16 @@ export class FlowStateManager<T = unknown> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
this.intervals.delete(intervalId);
|
||||
this.logger.warn(`[${flowKey}] Flow aborted`);
|
||||
const message = `${type} flow aborted`;
|
||||
await this.keyv.delete(flowKey);
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (flowState.status !== 'PENDING') {
|
||||
clearInterval(intervalId);
|
||||
this.intervals.delete(intervalId);
|
||||
|
|
@ -197,19 +212,19 @@ export class FlowStateManager<T = unknown> {
|
|||
* @param flowId - The ID of the flow
|
||||
* @param type - The type of flow
|
||||
* @param handler - Async function to execute if no existing flow is found
|
||||
* @param metadata - Optional metadata for the flow
|
||||
* @param signal - Optional AbortSignal to cancel the flow
|
||||
*/
|
||||
async createFlowWithHandler(
|
||||
flowId: string,
|
||||
type: string,
|
||||
handler: () => Promise<T>,
|
||||
metadata: FlowMetadata = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
const flowKey = this.getFlowKey(flowId, type);
|
||||
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow already exists`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
|
@ -217,13 +232,13 @@ export class FlowStateManager<T = unknown> {
|
|||
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||
if (existingState) {
|
||||
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
||||
return this.monitorFlow(flowKey, type);
|
||||
return this.monitorFlow(flowKey, type, signal);
|
||||
}
|
||||
|
||||
const initialState: FlowState = {
|
||||
type,
|
||||
status: 'PENDING',
|
||||
metadata,
|
||||
metadata: {},
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
this.logger.debug(`[${flowKey}] Creating initial flow state`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue