mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50: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",
|
"@googleapis/youtube": "^20.0.0",
|
||||||
"@keyv/mongo": "^2.1.8",
|
"@keyv/mongo": "^2.1.8",
|
||||||
"@keyv/redis": "^2.8.1",
|
"@keyv/redis": "^2.8.1",
|
||||||
"@langchain/community": "^0.3.34",
|
"@langchain/community": "^0.3.39",
|
||||||
"@langchain/core": "^0.3.40",
|
"@langchain/core": "^0.3.43",
|
||||||
"@langchain/google-genai": "^0.1.11",
|
"@langchain/google-genai": "^0.2.2",
|
||||||
"@langchain/google-vertexai": "^0.2.2",
|
"@langchain/google-vertexai": "^0.2.3",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@librechat/agents": "^2.3.95",
|
"@librechat/agents": "^2.4.0",
|
||||||
"@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",
|
||||||
|
|
|
||||||
|
|
@ -191,24 +191,30 @@ async function createActionTool({
|
||||||
};
|
};
|
||||||
const flowManager = await getFlowStateManager(getLogStores);
|
const flowManager = await getFlowStateManager(getLogStores);
|
||||||
await flowManager.createFlowWithHandler(
|
await flowManager.createFlowWithHandler(
|
||||||
`${identifier}:login`,
|
`${identifier}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`,
|
||||||
'oauth_login',
|
'oauth_login',
|
||||||
async () => {
|
async () => {
|
||||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
||||||
logger.debug('Sent OAuth login request to client', { action_id, identifier });
|
logger.debug('Sent OAuth login request to client', { action_id, identifier });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
config?.signal,
|
||||||
);
|
);
|
||||||
logger.debug('Waiting for OAuth Authorization response', { action_id, identifier });
|
logger.debug('Waiting for OAuth Authorization response', { action_id, identifier });
|
||||||
const result = await flowManager.createFlow(identifier, 'oauth', {
|
const result = await flowManager.createFlow(
|
||||||
state: stateToken,
|
identifier,
|
||||||
userId: req.user.id,
|
'oauth',
|
||||||
client_url: metadata.auth.client_url,
|
{
|
||||||
redirect_uri: `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`,
|
state: stateToken,
|
||||||
/** Encrypted values */
|
userId: req.user.id,
|
||||||
encrypted_oauth_client_id: encrypted.oauth_client_id,
|
client_url: metadata.auth.client_url,
|
||||||
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
|
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 });
|
logger.debug('Received OAuth Authorization response', { action_id, identifier });
|
||||||
data.delta.auth = undefined;
|
data.delta.auth = undefined;
|
||||||
data.delta.expires_at = undefined;
|
data.delta.expires_at = undefined;
|
||||||
|
|
@ -264,6 +270,7 @@ async function createActionTool({
|
||||||
`${identifier}:refresh`,
|
`${identifier}:refresh`,
|
||||||
'oauth_refresh',
|
'oauth_refresh',
|
||||||
refreshTokens,
|
refreshTokens,
|
||||||
|
config?.signal,
|
||||||
);
|
);
|
||||||
metadata.oauth_access_token = refreshData.access_token;
|
metadata.oauth_access_token = refreshData.access_token;
|
||||||
if (refreshData.refresh_token) {
|
if (refreshData.refresh_token) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import {
|
||||||
SandpackCodeEditor,
|
SandpackCodeEditor,
|
||||||
SandpackProvider as StyledProvider,
|
SandpackProvider as StyledProvider,
|
||||||
} from '@codesandbox/sandpack-react';
|
} 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 { CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||||
import type { ArtifactFiles, Artifact } from '~/common';
|
import type { ArtifactFiles, Artifact } from '~/common';
|
||||||
import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
|
import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
|
||||||
|
|
@ -66,7 +67,7 @@ const CodeEditor = ({
|
||||||
return;
|
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()) {
|
if (currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim()) {
|
||||||
setCurrentCode(currentCode);
|
setCurrentCode(currentCode);
|
||||||
|
|
|
||||||
|
|
@ -123,11 +123,14 @@ export default function useStepHandler({
|
||||||
} else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
|
} else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
|
||||||
const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined;
|
const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined;
|
||||||
const existingToolCall = existingContent?.tool_call;
|
const existingToolCall = existingContent?.tool_call;
|
||||||
const toolCallArgs = (contentPart.tool_call.args as unknown as string | undefined) ?? '';
|
const toolCallArgs = (contentPart.tool_call as Agents.ToolCall).args;
|
||||||
|
/** When args are a valid object, they are likely already invoked */
|
||||||
const args = finalUpdate
|
const args =
|
||||||
? contentPart.tool_call.args
|
finalUpdate ||
|
||||||
: (existingToolCall?.args ?? '') + toolCallArgs;
|
typeof existingToolCall?.args === 'object' ||
|
||||||
|
typeof toolCallArgs === 'object'
|
||||||
|
? contentPart.tool_call.args
|
||||||
|
: (existingToolCall?.args ?? '') + (toolCallArgs ?? '');
|
||||||
|
|
||||||
const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? '';
|
const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? '';
|
||||||
const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? '';
|
const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? '';
|
||||||
|
|
@ -195,12 +198,31 @@ export default function useStepHandler({
|
||||||
|
|
||||||
// Store tool call IDs if present
|
// Store tool call IDs if present
|
||||||
if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) {
|
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 ?? '';
|
const toolCallId = toolCall.id ?? '';
|
||||||
if ('id' in toolCall && toolCallId) {
|
if ('id' in toolCall && toolCallId) {
|
||||||
toolCallIdMap.current.set(runStep.id, 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') {
|
} else if (event === 'on_agent_update') {
|
||||||
const { agent_update } = data as Agents.AgentUpdate;
|
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
|
* 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);
|
const flowKey = this.getFlowKey(flowId, type);
|
||||||
|
|
||||||
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||||
if (existingState) {
|
if (existingState) {
|
||||||
this.logger.debug(`[${flowKey}] Flow already exists`);
|
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));
|
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;
|
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||||
if (existingState) {
|
if (existingState) {
|
||||||
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
||||||
return this.monitorFlow(flowKey, type);
|
return this.monitorFlow(flowKey, type, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: FlowState = {
|
const initialState: FlowState = {
|
||||||
|
|
@ -81,10 +86,10 @@ export class FlowStateManager<T = unknown> {
|
||||||
|
|
||||||
this.logger.debug('Creating initial flow state:', flowKey);
|
this.logger.debug('Creating initial flow state:', flowKey);
|
||||||
await this.keyv.set(flowKey, initialState, this.ttl);
|
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) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const checkInterval = 2000;
|
const checkInterval = 2000;
|
||||||
let elapsedTime = 0;
|
let elapsedTime = 0;
|
||||||
|
|
@ -101,6 +106,16 @@ export class FlowStateManager<T = unknown> {
|
||||||
return;
|
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') {
|
if (flowState.status !== 'PENDING') {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
this.intervals.delete(intervalId);
|
this.intervals.delete(intervalId);
|
||||||
|
|
@ -197,19 +212,19 @@ export class FlowStateManager<T = unknown> {
|
||||||
* @param flowId - The ID of the flow
|
* @param flowId - The ID of the flow
|
||||||
* @param type - The type of flow
|
* @param type - The type of flow
|
||||||
* @param handler - Async function to execute if no existing flow is found
|
* @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(
|
async createFlowWithHandler(
|
||||||
flowId: string,
|
flowId: string,
|
||||||
type: string,
|
type: string,
|
||||||
handler: () => Promise<T>,
|
handler: () => Promise<T>,
|
||||||
metadata: FlowMetadata = {},
|
signal?: AbortSignal,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const flowKey = this.getFlowKey(flowId, type);
|
const flowKey = this.getFlowKey(flowId, type);
|
||||||
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||||
if (existingState) {
|
if (existingState) {
|
||||||
this.logger.debug(`[${flowKey}] Flow already exists`);
|
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));
|
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;
|
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
|
||||||
if (existingState) {
|
if (existingState) {
|
||||||
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
|
||||||
return this.monitorFlow(flowKey, type);
|
return this.monitorFlow(flowKey, type, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: FlowState = {
|
const initialState: FlowState = {
|
||||||
type,
|
type,
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
metadata,
|
metadata: {},
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
};
|
};
|
||||||
this.logger.debug(`[${flowKey}] Creating initial flow state`);
|
this.logger.debug(`[${flowKey}] Creating initial flow state`);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue