Skip to content

Commit f13eb57

Browse files
buffer string context passing
1 parent 5284aa8 commit f13eb57

3 files changed

Lines changed: 377 additions & 14 deletions

File tree

api/server/services/Workflows/WorkflowExecutor.js

Lines changed: 203 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const { Constants } = require('librechat-data-provider');
33
const { updateSchedulerExecution } = require('~/models/SchedulerExecution');
44
const { loadAgent } = require('~/models/Agent');
55
const { User } = require('~/db/models');
6+
const { getBufferString } = require('@langchain/core/messages');
7+
const { HumanMessage } = require('@langchain/core/messages');
68
const {
79
createMinimalMockResponse,
810
updateRequestForEphemeralAgent,
@@ -17,6 +19,117 @@ const {
1719
} = require('./executor');
1820
const SchedulerClientFactory = require('~/server/services/Scheduler/SchedulerClientFactory');
1921

22+
/**
23+
* Extract meaningful content from step result object for display
24+
* @param {Object} result - Step result object
25+
* @returns {string|null} Meaningful content or null if not found
26+
*/
27+
function extractMeaningfulContent(result) {
28+
if (!result || typeof result !== 'object') {
29+
return null;
30+
}
31+
32+
// Handle LibreChat agent response objects with content array
33+
if (result.content && Array.isArray(result.content)) {
34+
// Extract text content from agent response content array
35+
const textParts = result.content
36+
.filter(part => part.type === 'text' && part.text && part.text.trim())
37+
.map(part => part.text.trim());
38+
39+
if (textParts.length > 0) {
40+
return textParts.join('\n').trim();
41+
}
42+
}
43+
44+
// Check for direct text field
45+
if (result.text && typeof result.text === 'string' && result.text.trim()) {
46+
return result.text.trim();
47+
}
48+
49+
// Handle nested agent response objects (common in LibreChat)
50+
if (result.agentResponse) {
51+
if (typeof result.agentResponse === 'string') {
52+
return result.agentResponse;
53+
}
54+
if (typeof result.agentResponse === 'object') {
55+
// Try to extract text from nested agent response content
56+
if (result.agentResponse.content && Array.isArray(result.agentResponse.content)) {
57+
const textParts = result.agentResponse.content
58+
.filter(part => part.type === 'text' && part.text && part.text.trim())
59+
.map(part => part.text.trim());
60+
61+
if (textParts.length > 0) {
62+
return textParts.join('\n').trim();
63+
}
64+
}
65+
66+
// Check for direct text in agent response
67+
if (result.agentResponse.text && typeof result.agentResponse.text === 'string' && result.agentResponse.text.trim()) {
68+
return result.agentResponse.text.trim();
69+
}
70+
71+
// Check for message content in agent response
72+
if (result.agentResponse.content && typeof result.agentResponse.content === 'string') {
73+
return result.agentResponse.content;
74+
}
75+
}
76+
}
77+
78+
// Check for tool results
79+
if (result.toolResults && Array.isArray(result.toolResults)) {
80+
const meaningfulResults = result.toolResults
81+
.map(tool => {
82+
if (tool.result && typeof tool.result === 'string') {
83+
return `Tool "${tool.name || 'unknown'}": ${tool.result}`;
84+
}
85+
if (tool.result && typeof tool.result === 'object') {
86+
// Try to extract meaningful data from tool result
87+
if (tool.result.data || tool.result.message || tool.result.content) {
88+
const content = tool.result.data || tool.result.message || tool.result.content;
89+
return `Tool "${tool.name || 'unknown'}": ${typeof content === 'string' ? content : JSON.stringify(content)}`;
90+
}
91+
}
92+
return null;
93+
})
94+
.filter(Boolean);
95+
96+
if (meaningfulResults.length > 0) {
97+
return meaningfulResults.join('\n');
98+
}
99+
}
100+
101+
// Check for direct content fields
102+
if (result.content && typeof result.content === 'string') {
103+
return result.content;
104+
}
105+
106+
if (result.message && typeof result.message === 'string') {
107+
return result.message;
108+
}
109+
110+
if (result.data) {
111+
if (typeof result.data === 'string') {
112+
return result.data;
113+
}
114+
if (typeof result.data === 'object') {
115+
// Try to extract summary information from data objects
116+
if (Array.isArray(result.data)) {
117+
return `Retrieved ${result.data.length} items`;
118+
}
119+
if (result.data.summary || result.data.description) {
120+
return result.data.summary || result.data.description;
121+
}
122+
}
123+
}
124+
125+
// Check for successful execution indicators
126+
if (result.success && result.type) {
127+
return `Successfully executed ${result.type} operation`;
128+
}
129+
130+
return null;
131+
}
132+
20133
/**
21134
* WorkflowExecutor - Handles the execution of workflows
22135
*
@@ -138,6 +251,7 @@ class WorkflowExecutor {
138251
const executionId = execution.id;
139252
const userId = execution.user;
140253
let executionContext = null; // Initialize here to ensure it's in scope for error handling
254+
let stepMessages = []; // Initialize here to ensure it's in scope for error handling
141255

142256
try {
143257
logger.info(`[WorkflowExecutor] Starting workflow execution: ${workflowId}`);
@@ -229,7 +343,7 @@ class WorkflowExecutor {
229343
}
230344

231345
// Execute steps starting from the first step
232-
const result = await this.executeStepChain(workflow, execution, firstStep.id, executionContext, executionMetadata);
346+
const result = await this.executeStepChain(workflow, execution, firstStep.id, executionContext, executionMetadata, stepMessages);
233347

234348
// Clean up tracking
235349
this.runningExecutions.delete(executionId);
@@ -242,11 +356,19 @@ class WorkflowExecutor {
242356

243357
logger.error(`[WorkflowExecutor] Workflow execution failed: ${workflowId}`, error);
244358

359+
// Create error summary with any partial results
360+
let errorOutput = `Workflow failed: ${error.message}`;
361+
if (executionContext && stepMessages && stepMessages.length > 0) {
362+
const partialSummary = getBufferString(stepMessages);
363+
errorOutput = `${errorOutput}\n\nPartial results before failure:\n${partialSummary}`;
364+
}
365+
245366
// Update execution status
246367
await updateSchedulerExecution(executionId, execution.user, {
247368
status: 'failed',
248369
endTime: new Date(),
249370
error: error.message,
371+
output: errorOutput,
250372
});
251373

252374
return {
@@ -264,9 +386,10 @@ class WorkflowExecutor {
264386
* @param {string} currentStepId - Current step ID to execute
265387
* @param {Object} context - Execution context
266388
* @param {Object} metadata - Execution metadata with step tracking
389+
* @param {Array} stepMessages - Array to accumulate step outputs as messages
267390
* @returns {Promise<Object>} Execution result
268391
*/
269-
async executeStepChain(workflow, execution, currentStepId, context, metadata) {
392+
async executeStepChain(workflow, execution, currentStepId, context, metadata, stepMessages) {
270393
let currentStep = currentStepId;
271394
const executionResult = { success: true, error: null };
272395
const accumulatedStepResults = [];
@@ -303,18 +426,73 @@ class WorkflowExecutor {
303426
});
304427
}
305428

429+
// Create input for the current step using buffer string approach
430+
let stepInput = context;
431+
if (stepMessages.length > 0) {
432+
// Convert accumulated step messages into a buffer string for the next step
433+
const bufferString = getBufferString(stepMessages);
434+
435+
// Add the buffer string as context for the current step
436+
stepInput = {
437+
...context,
438+
previousStepsOutput: bufferString,
439+
steps: context.steps // Keep the structured step results for metadata
440+
};
441+
442+
logger.debug(
443+
`[WorkflowExecutor] Passing accumulated output to step ${step.name}: ${bufferString.substring(0, 200)}...`,
444+
);
445+
}
446+
306447
// Execute the current step (each step gets a fresh agent)
307-
const stepResult = await executeStep(workflow, execution, step, context, executionData.abortController?.signal);
448+
const stepResult = await executeStep(workflow, execution, step, stepInput, executionData.abortController?.signal);
449+
450+
// Add step result to messages array for next step and capture meaningful output
451+
let meaningfulOutput = '';
452+
if (stepResult.success && stepResult.result) {
453+
// First, try to extract meaningful content for both buffer and display
454+
meaningfulOutput = extractMeaningfulContent(stepResult.result);
455+
456+
// If we found meaningful content, use it for the buffer string
457+
if (meaningfulOutput) {
458+
// Create a HumanMessage with the meaningful content for the next step
459+
stepMessages.push(new HumanMessage(`Step "${step.name}" result: ${meaningfulOutput}`));
460+
461+
logger.debug(
462+
`[WorkflowExecutor] Added meaningful step result to message chain: ${meaningfulOutput.substring(0, 200)}...`,
463+
);
464+
} else {
465+
// Fallback to raw result if no meaningful content can be extracted
466+
let resultText = '';
467+
if (typeof stepResult.result === 'string') {
468+
resultText = stepResult.result;
469+
meaningfulOutput = resultText;
470+
} else {
471+
resultText = JSON.stringify(stepResult.result, null, 2);
472+
meaningfulOutput = resultText;
473+
}
474+
475+
// Create a HumanMessage with the fallback result for the next step
476+
stepMessages.push(new HumanMessage(`Step "${step.name}" result: ${resultText}`));
477+
478+
logger.debug(
479+
`[WorkflowExecutor] Added fallback step result to message chain: ${resultText.substring(0, 200)}...`,
480+
);
481+
}
482+
}
308483

309484
// Update step status and results in metadata
310485
if (stepMetadata) {
311486
stepMetadata.status = stepResult.success ? 'completed' : 'failed';
312487
stepMetadata.endTime = new Date();
313488

314-
// Capture step output and error
315-
if (stepResult.result && typeof stepResult.result === 'string') {
489+
// Store meaningful output instead of technical metadata
490+
if (meaningfulOutput) {
491+
stepMetadata.output = meaningfulOutput;
492+
} else if (stepResult.result && typeof stepResult.result === 'string') {
316493
stepMetadata.output = stepResult.result;
317494
} else if (stepResult.result && typeof stepResult.result === 'object') {
495+
// Fallback to technical metadata if no meaningful content can be extracted
318496
stepMetadata.output = JSON.stringify(stepResult.result, null, 2);
319497
}
320498

@@ -377,6 +555,26 @@ class WorkflowExecutor {
377555
}
378556

379557
executionResult.result = accumulatedStepResults;
558+
559+
// Create a final summary using the buffer string approach
560+
if (stepMessages.length > 0) {
561+
const finalSummary = getBufferString(stepMessages);
562+
executionResult.finalOutput = finalSummary;
563+
564+
// Update execution record with final summary
565+
await updateSchedulerExecution(execution.id, execution.user, {
566+
status: executionResult.success ? 'completed' : 'failed',
567+
endTime: new Date(),
568+
output: finalSummary, // Store the final buffer string as overall output
569+
});
570+
} else {
571+
// Update execution record without buffer string
572+
await updateSchedulerExecution(execution.id, execution.user, {
573+
status: executionResult.success ? 'completed' : 'failed',
574+
endTime: new Date(),
575+
});
576+
}
577+
380578
return executionResult;
381579
}
382580

api/server/services/Workflows/executor/PromptBuilder.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ function generateGenericToolGuidance(toolName, stepName, stepObjective, context)
512512
*/
513513
function generateContextUsageInstructions(step, context) {
514514
const stepName = step.name.toLowerCase();
515-
const hasPreviousSteps = context.steps && Object.keys(context.steps).length > 0;
515+
const hasPreviousSteps = (context.steps && Object.keys(context.steps).length > 0) || context.previousStepsOutput;
516516
const hasConfiguredParameters = step.config?.parameters && Object.keys(step.config.parameters).length > 0;
517517

518518
if (!hasPreviousSteps && !hasConfiguredParameters) {
@@ -576,6 +576,16 @@ function generateContextUsageInstructions(step, context) {
576576
* @returns {string} A formatted string of previous step results.
577577
*/
578578
function formatPreviousStepResults(context) {
579+
// Check if we have accumulated output from previous steps using the buffer string approach
580+
if (context.previousStepsOutput) {
581+
let resultsSection = '-- ACCUMULATED OUTPUT FROM PREVIOUS STEPS --\n\n';
582+
resultsSection += 'Use the following accumulated output from previous steps to perform your task. This contains the actual results and data from completed workflow steps.\n\n';
583+
resultsSection += context.previousStepsOutput;
584+
resultsSection += '\n\n';
585+
return resultsSection;
586+
}
587+
588+
// Fallback to the original structured approach if no buffer string is available
579589
if (!context.steps || Object.keys(context.steps).length === 0) {
580590
return 'No data from previous steps is available.';
581591
}

0 commit comments

Comments
 (0)