Feature: Add access to chat history and other useful variables in post-processing (#5511)

* access chat history and other useful variables in post-processing

* cloning data to prevent mutations in post-processing

* Enhance post-processing capabilities by adding support for additional variables and improving the UI for available variables display. Update CustomFunction implementations to utilize post-processing options consistently across components.

---------

Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
Nikitas Papadopoulos 2025-11-27 13:59:00 +01:00 committed by GitHub
parent 562370b8e2
commit 6a59af11e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 203 additions and 36 deletions

View File

@ -134,7 +134,7 @@ class CustomFunction_Agentflow implements INode {
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const javascriptFunction = nodeData.inputs?.customFunctionJavascriptFunction as string
const functionInputVariables = nodeData.inputs?.customFunctionInputVariables as ICustomFunctionInputVariables[]
const functionInputVariables = (nodeData.inputs?.customFunctionInputVariables as ICustomFunctionInputVariables[]) ?? []
const _customFunctionUpdateState = nodeData.inputs?.customFunctionUpdateState
const state = options.agentflowRuntime?.state as ICommonObject
@ -147,11 +147,17 @@ class CustomFunction_Agentflow implements INode {
const variables = await getVars(appDataSource, databaseEntities, nodeData, options)
const flow = {
input,
state,
chatflowId: options.chatflowid,
sessionId: options.sessionId,
chatId: options.chatId,
input,
state
rawOutput: options.postProcessing?.rawOutput || '',
chatHistory: options.postProcessing?.chatHistory || [],
sourceDocuments: options.postProcessing?.sourceDocuments,
usedTools: options.postProcessing?.usedTools,
artifacts: options.postProcessing?.artifacts,
fileAnnotations: options.postProcessing?.fileAnnotations
}
// Create additional sandbox variables for custom function inputs

View File

@ -84,11 +84,16 @@ class CustomFunction_Utilities implements INode {
const variables = await getVars(appDataSource, databaseEntities, nodeData, options)
const flow = {
input,
chatflowId: options.chatflowid,
sessionId: options.sessionId,
chatId: options.chatId,
rawOutput: options.rawOutput || '',
input
rawOutput: options.postProcessing?.rawOutput || '',
chatHistory: options.postProcessing?.chatHistory || [],
sourceDocuments: options.postProcessing?.sourceDocuments,
usedTools: options.postProcessing?.usedTools,
artifacts: options.postProcessing?.artifacts,
fileAnnotations: options.postProcessing?.fileAnnotations
}
let inputVars: ICommonObject = {}

View File

@ -2122,7 +2122,62 @@ export const executeAgentFlow = async ({
// check if last agentFlowExecutedData.data.output contains the key "content"
const lastNodeOutput = agentFlowExecutedData[agentFlowExecutedData.length - 1].data?.output as ICommonObject | undefined
const content = (lastNodeOutput?.content as string) ?? ' '
let content = (lastNodeOutput?.content as string) ?? ' '
/* Check for post-processing settings */
let chatflowConfig: ICommonObject = {}
try {
if (chatflow.chatbotConfig) {
chatflowConfig = typeof chatflow.chatbotConfig === 'string' ? JSON.parse(chatflow.chatbotConfig) : chatflow.chatbotConfig
}
} catch (e) {
logger.error('[server]: Error parsing chatflow config:', e)
}
if (chatflowConfig?.postProcessing?.enabled === true && content) {
try {
const postProcessingFunction = JSON.parse(chatflowConfig?.postProcessing?.customFunction)
const nodeInstanceFilePath = componentNodes['customFunctionAgentflow'].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
//set the outputs.output to EndingNode to prevent json escaping of content...
const nodeData = {
inputs: { customFunctionJavascriptFunction: postProcessingFunction }
}
const runtimeChatHistory = agentflowRuntime.chatHistory || []
const chatHistory = [...pastChatHistory, ...runtimeChatHistory]
const options: ICommonObject = {
chatflowid: chatflow.id,
sessionId,
chatId,
input: question || form,
postProcessing: {
rawOutput: content,
chatHistory: cloneDeep(chatHistory),
sourceDocuments: lastNodeOutput?.sourceDocuments ? cloneDeep(lastNodeOutput.sourceDocuments) : undefined,
usedTools: lastNodeOutput?.usedTools ? cloneDeep(lastNodeOutput.usedTools) : undefined,
artifacts: lastNodeOutput?.artifacts ? cloneDeep(lastNodeOutput.artifacts) : undefined,
fileAnnotations: lastNodeOutput?.fileAnnotations ? cloneDeep(lastNodeOutput.fileAnnotations) : undefined
},
appDataSource,
databaseEntities,
workspaceId,
orgId,
logger
}
const customFuncNodeInstance = new nodeModule.nodeClass()
const customFunctionResponse = await customFuncNodeInstance.run(nodeData, question || form, options)
const moderatedResponse = customFunctionResponse.output.content
if (typeof moderatedResponse === 'string') {
content = moderatedResponse
} else if (typeof moderatedResponse === 'object') {
content = '```json\n' + JSON.stringify(moderatedResponse, null, 2) + '\n```'
} else {
content = moderatedResponse
}
} catch (e) {
logger.error('[server]: Post Processing Error:', e)
}
}
// remove credentialId from agentFlowExecutedData
agentFlowExecutedData = agentFlowExecutedData.map((data) => _removeCredentialId(data))

View File

@ -2,7 +2,7 @@ import { Request } from 'express'
import * as path from 'path'
import { DataSource } from 'typeorm'
import { v4 as uuidv4 } from 'uuid'
import { omit } from 'lodash'
import { omit, cloneDeep } from 'lodash'
import {
IFileUpload,
convertSpeechToText,
@ -817,7 +817,14 @@ export const executeFlow = async ({
sessionId,
chatId,
input: question,
rawOutput: resultText,
postProcessing: {
rawOutput: resultText,
chatHistory: cloneDeep(chatHistory),
sourceDocuments: result?.sourceDocuments ? cloneDeep(result.sourceDocuments) : undefined,
usedTools: result?.usedTools ? cloneDeep(result.usedTools) : undefined,
artifacts: result?.artifacts ? cloneDeep(result.artifacts) : undefined,
fileAnnotations: result?.fileAnnotations ? cloneDeep(result.fileAnnotations) : undefined
},
appDataSource,
databaseEntities,
workspaceId,

View File

@ -53,8 +53,7 @@ const CHATFLOW_CONFIGURATION_TABS = [
},
{
label: 'Post Processing',
id: 'postProcessing',
hideInAgentFlow: true
id: 'postProcessing'
}
]

View File

@ -4,8 +4,25 @@ import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
// material-ui
import { IconButton, Button, Box, Typography } from '@mui/material'
import { IconArrowsMaximize, IconBulb, IconX } from '@tabler/icons-react'
import {
IconButton,
Button,
Box,
Typography,
TableContainer,
Table,
TableHead,
TableBody,
TableRow,
TableCell,
Paper,
Accordion,
AccordionSummary,
AccordionDetails,
Card
} from '@mui/material'
import { IconArrowsMaximize, IconX } from '@tabler/icons-react'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useTheme } from '@mui/material/styles'
// Project import
@ -21,7 +38,11 @@ import useNotifier from '@/utils/useNotifier'
// API
import chatflowsApi from '@/api/chatflows'
const sampleFunction = `return $flow.rawOutput + " This is a post processed response!";`
const sampleFunction = `// Access chat history as a string
const chatHistory = JSON.stringify($flow.chatHistory, null, 2);
// Return a modified response
return $flow.rawOutput + " This is a post processed response!";`
const PostProcessing = ({ dialogProps }) => {
const dispatch = useDispatch()
@ -175,31 +196,105 @@ const PostProcessing = ({ dialogProps }) => {
/>
</div>
</Box>
<div
style={{
display: 'flex',
flexDirection: 'column',
borderRadius: 10,
background: '#d8f3dc',
padding: 10,
marginTop: 10
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
paddingTop: 10
<Card sx={{ borderColor: theme.palette.primary[200] + 75, mt: 2, mb: 2 }} variant='outlined'>
<Accordion
disableGutters
sx={{
'&:before': {
display: 'none'
}
}}
>
<IconBulb size={30} color='#2d6a4f' />
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
The following variables are available to use in the custom function:{' '}
<pre>$flow.rawOutput, $flow.input, $flow.chatflowId, $flow.sessionId, $flow.chatId</pre>
</span>
</div>
</div>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Available Variables</Typography>
</AccordionSummary>
<AccordionDetails sx={{ p: 0 }}>
<TableContainer component={Paper}>
<Table aria-label='available variables table'>
<TableHead>
<TableRow>
<TableCell sx={{ width: '30%' }}>Variable</TableCell>
<TableCell sx={{ width: '15%' }}>Type</TableCell>
<TableCell sx={{ width: '55%' }}>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>
<code>$flow.rawOutput</code>
</TableCell>
<TableCell>string</TableCell>
<TableCell>The raw output response from the flow</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.input</code>
</TableCell>
<TableCell>string</TableCell>
<TableCell>The user input message</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.chatHistory</code>
</TableCell>
<TableCell>array</TableCell>
<TableCell>Array of previous messages in the conversation</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.chatflowId</code>
</TableCell>
<TableCell>string</TableCell>
<TableCell>Unique identifier for the chatflow</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.sessionId</code>
</TableCell>
<TableCell>string</TableCell>
<TableCell>Current session identifier</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.chatId</code>
</TableCell>
<TableCell>string</TableCell>
<TableCell>Current chat identifier</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.sourceDocuments</code>
</TableCell>
<TableCell>array</TableCell>
<TableCell>Source documents used in retrieval (if applicable)</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.usedTools</code>
</TableCell>
<TableCell>array</TableCell>
<TableCell>List of tools used during execution</TableCell>
</TableRow>
<TableRow>
<TableCell>
<code>$flow.artifacts</code>
</TableCell>
<TableCell>array</TableCell>
<TableCell>List of artifacts generated during execution</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ borderBottom: 'none' }}>
<code>$flow.fileAnnotations</code>
</TableCell>
<TableCell sx={{ borderBottom: 'none' }}>array</TableCell>
<TableCell sx={{ borderBottom: 'none' }}>File annotations associated with the response</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
</Card>
<StyledButton
style={{ marginBottom: 10, marginTop: 10 }}
variant='contained'