improvement(chat-deploy): added conversation ID to input

This commit is contained in:
Emir Karabeg
2025-05-19 13:33:19 -07:00
parent 0af7fb2a7a
commit 533f765c34
5 changed files with 72 additions and 33 deletions

View File

@@ -72,7 +72,7 @@ export async function POST(
}
// Use the already parsed body
const { message, password, email } = parsedBody
const { message, password, email, conversationId } = parsedBody
// If this is an authentication request (has password or email but no message),
// set auth cookie and return success
@@ -105,8 +105,8 @@ export async function POST(
}
try {
// Execute the workflow using our helper function
const result = await executeWorkflowForChat(deployment.id, message)
// Execute workflow with structured input (message + conversationId for context)
const result = await executeWorkflowForChat(deployment.id, message, conversationId)
// If the executor returned a ReadableStream, stream it directly to the client
if (result instanceof ReadableStream) {

View File

@@ -237,13 +237,20 @@ function extractBlockOutput(logs: any[], blockId: string, path?: string) {
}
/**
* Executes a workflow for a chat and extracts the specified output.
* This function contains the same logic as the internal chat panel.
* Executes a workflow for a chat request and returns the formatted output.
*
* When workflows reference <start.response.input>, they receive a structured JSON
* containing both the message and conversationId for maintaining chat context.
*
* @param chatId - Chat deployment identifier
* @param message - User's chat message
* @param conversationId - Optional ID for maintaining conversation context
* @returns Workflow execution result formatted for the chat interface
*/
export async function executeWorkflowForChat(chatId: string, message: string) {
export async function executeWorkflowForChat(chatId: string, message: string, conversationId?: string) {
const requestId = crypto.randomUUID().slice(0, 8)
logger.debug(`[${requestId}] Executing workflow for chat: ${chatId}`)
logger.debug(`[${requestId}] Executing workflow for chat: ${chatId}${conversationId ? `, conversationId: ${conversationId}` : ''}`)
// Find the chat deployment
const deploymentResult = await db
@@ -418,7 +425,7 @@ export async function executeWorkflowForChat(chatId: string, message: string) {
workflow: serializedWorkflow,
currentBlockStates: processedBlockStates,
envVarValues: decryptedEnvVars,
workflowInput: { input: message },
workflowInput: { input: message, conversationId },
workflowVariables,
contextExtensions: {
// Always request streaming the executor will downgrade gracefully if unsupported
@@ -490,10 +497,22 @@ export async function executeWorkflowForChat(chatId: string, message: string) {
totalDuration,
}
// Add conversationId to metadata if available
if (conversationId) {
if (!enrichedResult.metadata) {
enrichedResult.metadata = {
duration: totalDuration,
};
}
(enrichedResult.metadata as any).conversationId = conversationId;
}
const executionId = uuidv4()
await persistExecutionLogs(workflowId, executionId, enrichedResult, 'chat')
logger.debug(
`[${requestId}] Persisted execution logs for streaming chat with ID: ${executionId}`
`[${requestId}] Persisted execution logs for streaming chat with ID: ${executionId}${
conversationId ? `, conversationId: ${conversationId}` : ''
}`
)
// Update user stats for successful streaming chat execution
@@ -562,6 +581,11 @@ export async function executeWorkflowForChat(chatId: string, message: string) {
...(result.metadata || {}),
source: 'chat',
}
// Add conversationId to metadata if available
if (conversationId) {
(result as any).metadata.conversationId = conversationId
}
}
// Update user stats to increment totalChatExecutions if the execution was successful
@@ -606,13 +630,26 @@ export async function executeWorkflowForChat(chatId: string, message: string) {
totalDuration,
}
// Add conversation ID to metadata if available
if (conversationId) {
if (!enrichedResult.metadata) {
enrichedResult.metadata = {
duration: totalDuration,
};
}
(enrichedResult.metadata as any).conversationId = conversationId;
}
// Generate a unique execution ID for this chat interaction
const executionId = uuidv4()
// Persist the logs with 'chat' trigger type
await persistExecutionLogs(workflowId, executionId, enrichedResult, 'chat')
logger.debug(`[${requestId}] Persisted execution logs for chat with ID: ${executionId}`)
logger.debug(
`[${requestId}] Persisted execution logs for chat with ID: ${executionId}${
conversationId ? `, conversationId: ${conversationId}` : ''
}`)
} catch (error) {
// Don't fail the chat response if logging fails
logger.error(`[${requestId}] Failed to persist chat execution logs:`, error)

View File

@@ -10,6 +10,7 @@ import {
useState,
} from 'react'
import { ArrowUp, Loader2, Lock, Mail } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { OTPInputForm } from '@/components/ui/input-otp-form'
@@ -99,6 +100,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
const messagesContainerRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const [starCount, setStarCount] = useState('3.4k')
const [conversationId, setConversationId] = useState('')
// Authentication state
const [authRequired, setAuthRequired] = useState<'password' | 'email' | null>(null)
@@ -163,9 +165,11 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
}
}
// Fetch chat config on mount
// Fetch chat config on mount and generate new conversation ID
useEffect(() => {
fetchChatConfig()
// Generate a new conversation ID whenever the page/chat is refreshed
setConversationId(uuidv4())
// Fetch GitHub stars
getFormattedGitHubStars()
@@ -380,6 +384,12 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
}
try {
// Send structured payload to maintain chat context
const payload = {
message: userMessage.content,
conversationId,
}
// Use relative URL with credentials
const response = await fetch(`/api/chat/${subdomain}`, {
method: 'POST',
@@ -388,7 +398,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ message: userMessage.content }),
body: JSON.stringify(payload),
})
if (!response.ok) {

View File

@@ -714,20 +714,16 @@ export class Executor {
})
} else {
*/
// No input format defined or not an array,
// Handle API call - prioritize using the input as-is
// Handle structured input (like API calls or chat messages)
if (this.workflowInput && typeof this.workflowInput === 'object') {
// For API calls, extract input from the nested structure if it exists
const inputData =
this.workflowInput.input !== undefined
? this.workflowInput.input // Use the nested input data
: this.workflowInput // Fallback to direct input
// Create starter output with both formats for maximum compatibility
// Preserve complete workflowInput structure to maintain JSON format
// when referenced through <start.response.input>
const starterOutput = {
response: {
input: inputData,
...inputData, // Make fields directly accessible at response level
input: this.workflowInput,
// Add top-level fields for backward compatibility
message: this.workflowInput.input,
conversationId: this.workflowInput.conversationId
},
}
@@ -737,7 +733,7 @@ export class Executor {
executionTime: 0,
})
} else {
// Fallback for other cases
// Fallback for primitive input values
const starterOutput = {
response: {
input: this.workflowInput,
@@ -754,17 +750,12 @@ export class Executor {
} catch (e) {
logger.warn('Error processing starter block input format:', e)
// Fallback to raw input with both paths accessible
// Ensure we handle both input formats
const inputData =
this.workflowInput?.input !== undefined
? this.workflowInput.input // Use nested input if available
: this.workflowInput // Fallback to direct input
// Error handler fallback - preserve structure for both direct access and backward compatibility
const starterOutput = {
response: {
input: inputData,
...inputData, // Add input fields directly at response level too
input: this.workflowInput,
message: this.workflowInput?.input,
conversationId: this.workflowInput?.conversationId
},
}

View File

@@ -465,6 +465,7 @@ export class InputResolver {
}
// For all other blocks, stringify objects
else {
// Preserve full JSON structure for objects (especially for structured inputs with conversationId)
formattedValue = JSON.stringify(replacementValue)
}
} else {