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> { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const javascriptFunction = nodeData.inputs?.customFunctionJavascriptFunction as string 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 _customFunctionUpdateState = nodeData.inputs?.customFunctionUpdateState
const state = options.agentflowRuntime?.state as ICommonObject 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 variables = await getVars(appDataSource, databaseEntities, nodeData, options)
const flow = { const flow = {
input,
state,
chatflowId: options.chatflowid, chatflowId: options.chatflowid,
sessionId: options.sessionId, sessionId: options.sessionId,
chatId: options.chatId, chatId: options.chatId,
input, rawOutput: options.postProcessing?.rawOutput || '',
state 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 // 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 variables = await getVars(appDataSource, databaseEntities, nodeData, options)
const flow = { const flow = {
input,
chatflowId: options.chatflowid, chatflowId: options.chatflowid,
sessionId: options.sessionId, sessionId: options.sessionId,
chatId: options.chatId, chatId: options.chatId,
rawOutput: options.rawOutput || '', rawOutput: options.postProcessing?.rawOutput || '',
input chatHistory: options.postProcessing?.chatHistory || [],
sourceDocuments: options.postProcessing?.sourceDocuments,
usedTools: options.postProcessing?.usedTools,
artifacts: options.postProcessing?.artifacts,
fileAnnotations: options.postProcessing?.fileAnnotations
} }
let inputVars: ICommonObject = {} let inputVars: ICommonObject = {}

View File

@ -2122,7 +2122,62 @@ export const executeAgentFlow = async ({
// check if last agentFlowExecutedData.data.output contains the key "content" // check if last agentFlowExecutedData.data.output contains the key "content"
const lastNodeOutput = agentFlowExecutedData[agentFlowExecutedData.length - 1].data?.output as ICommonObject | undefined 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 // remove credentialId from agentFlowExecutedData
agentFlowExecutedData = agentFlowExecutedData.map((data) => _removeCredentialId(data)) agentFlowExecutedData = agentFlowExecutedData.map((data) => _removeCredentialId(data))

View File

@ -2,7 +2,7 @@ import { Request } from 'express'
import * as path from 'path' import * as path from 'path'
import { DataSource } from 'typeorm' import { DataSource } from 'typeorm'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { omit } from 'lodash' import { omit, cloneDeep } from 'lodash'
import { import {
IFileUpload, IFileUpload,
convertSpeechToText, convertSpeechToText,
@ -817,7 +817,14 @@ export const executeFlow = async ({
sessionId, sessionId,
chatId, chatId,
input: question, input: question,
postProcessing: {
rawOutput: resultText, 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, appDataSource,
databaseEntities, databaseEntities,
workspaceId, workspaceId,

View File

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

View File

@ -4,8 +4,25 @@ import PropTypes from 'prop-types'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
// material-ui // material-ui
import { IconButton, Button, Box, Typography } from '@mui/material' import {
import { IconArrowsMaximize, IconBulb, IconX } from '@tabler/icons-react' 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' import { useTheme } from '@mui/material/styles'
// Project import // Project import
@ -21,7 +38,11 @@ import useNotifier from '@/utils/useNotifier'
// API // API
import chatflowsApi from '@/api/chatflows' 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 PostProcessing = ({ dialogProps }) => {
const dispatch = useDispatch() const dispatch = useDispatch()
@ -175,31 +196,105 @@ const PostProcessing = ({ dialogProps }) => {
/> />
</div> </div>
</Box> </Box>
<div <Card sx={{ borderColor: theme.palette.primary[200] + 75, mt: 2, mb: 2 }} variant='outlined'>
style={{ <Accordion
display: 'flex', disableGutters
flexDirection: 'column', sx={{
borderRadius: 10, '&:before': {
background: '#d8f3dc', display: 'none'
padding: 10, }
marginTop: 10
}} }}
> >
<div <AccordionSummary expandIcon={<ExpandMoreIcon />}>
style={{ <Typography>Available Variables</Typography>
display: 'flex', </AccordionSummary>
flexDirection: 'row', <AccordionDetails sx={{ p: 0 }}>
alignItems: 'center', <TableContainer component={Paper}>
paddingTop: 10 <Table aria-label='available variables table'>
}} <TableHead>
> <TableRow>
<IconBulb size={30} color='#2d6a4f' /> <TableCell sx={{ width: '30%' }}>Variable</TableCell>
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}> <TableCell sx={{ width: '15%' }}>Type</TableCell>
The following variables are available to use in the custom function:{' '} <TableCell sx={{ width: '55%' }}>Description</TableCell>
<pre>$flow.rawOutput, $flow.input, $flow.chatflowId, $flow.sessionId, $flow.chatId</pre> </TableRow>
</span> </TableHead>
</div> <TableBody>
</div> <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 <StyledButton
style={{ marginBottom: 10, marginTop: 10 }} style={{ marginBottom: 10, marginTop: 10 }}
variant='contained' variant='contained'