diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts new file mode 100644 index 0000000000..07deff7d07 --- /dev/null +++ b/app/api/chat/route.ts @@ -0,0 +1,208 @@ +import { OpenAI } from 'openai' +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' + +// Validation schemas +const MessageSchema = z.object({ + role: z.enum(['user', 'assistant', 'system']), + content: z.string() +}) + +const RequestSchema = z.object({ + messages: z.array(MessageSchema), + workflowState: z.object({ + blocks: z.record(z.any()), + edges: z.array(z.any()) + }) +}) + +// Define function schemas with strict typing +const workflowActions = { + addBlock: { + description: "Add one new block to the workflow", + parameters: { + type: "object", + required: ["type"], + properties: { + type: { + type: "string", + enum: ["agent", "api", "condition", "function", "router"], + description: "The type of block to add" + }, + name: { + type: "string", + description: "Optional custom name for the block. Do not provide a name unless the user has specified it." + }, + position: { + type: "object", + description: "Optional position for the block. Do not provide a position unless the user has specified it.", + properties: { + x: { type: "number" }, + y: { type: "number" } + } + } + } + } + }, + addEdge: { + description: "Create a connection between two blocks", + parameters: { + type: "object", + required: ["sourceId", "targetId"], + properties: { + sourceId: { + type: "string", + description: "ID of the source block" + }, + targetId: { + type: "string", + description: "ID of the target block" + }, + sourceHandle: { + type: "string", + description: "Optional handle identifier for the source connection point" + }, + targetHandle: { + type: "string", + description: "Optional handle identifier for the target connection point" + } + } + } + }, + removeBlock: { + description: "Remove a block from the workflow", + parameters: { + type: "object", + required: ["id"], + properties: { + id: { type: "string", description: "ID of the block to remove" } + } + } + }, + removeEdge: { + description: "Remove a connection between blocks", + parameters: { + type: "object", + required: ["id"], + properties: { + id: { type: "string", description: "ID of the edge to remove" } + } + } + } +} + +// System prompt that references workflow state +const getSystemPrompt = (workflowState: any) => { + const blockCount = Object.keys(workflowState.blocks).length + const edgeCount = workflowState.edges.length + + // Create a summary of existing blocks + const blockSummary = Object.values(workflowState.blocks) + .map((block: any) => `- ${block.type} block named "${block.name}" with id ${block.id}`) + .join('\n') + + // Create a summary of existing edges + const edgeSummary = workflowState.edges + .map((edge: any) => `- ${edge.source} -> ${edge.target} with id ${edge.id}`) + .join('\n') + + return `You are a workflow assistant that helps users modify their workflow by adding/removing blocks and connections. + +Current Workflow State: +${blockCount === 0 ? 'The workflow is empty.' : `${blockSummary} + +Connections: +${edgeCount === 0 ? 'No connections between blocks.' : edgeSummary}`} + +When users request changes: +- Consider existing blocks when suggesting connections +- Provide clear feedback about what actions you've taken + +Use the following functions to modify the workflow: +1. Use the addBlock function to create new blocks +2. Use the addEdge function to connect blocks +3. Use the removeBlock function to remove blocks +4. Use the removeEdge function to remove connections + +Only use the provided functions and respond naturally to the user's requests.` +} + +export async function POST(request: Request) { + try { + // Validate API key + const apiKey = request.headers.get('X-OpenAI-Key') + if (!apiKey) { + return NextResponse.json( + { error: 'OpenAI API key is required' }, + { status: 401 } + ) + } + + // Parse and validate request body + const body = await request.json() + const validatedData = RequestSchema.parse(body) + const { messages, workflowState } = validatedData + + // Initialize OpenAI client + const openai = new OpenAI({ apiKey }) + + // Create message history with workflow context + const messageHistory = [ + { role: 'system', content: getSystemPrompt(workflowState) }, + ...messages + ] + + // Make OpenAI API call with workflow context + const completion = await openai.chat.completions.create({ + model: "gpt-4o", + messages: messageHistory as ChatCompletionMessageParam[], + tools: Object.entries(workflowActions).map(([name, config]) => ({ + type: 'function', + function: { + name, + description: config.description, + parameters: config.parameters + } + })), + tool_choice: "auto" + }) + + const message = completion.choices[0].message + + // Process tool calls if present + if (message.tool_calls) { + console.log(message.tool_calls) + const actions = message.tool_calls.map(call => ({ + name: call.function.name, + parameters: JSON.parse(call.function.arguments) + })) + + return NextResponse.json({ + message: message.content || "I've updated the workflow based on your request.", + actions + }) + } + + // Return response with no actions + return NextResponse.json({ + message: message.content || "I'm not sure what changes to make to the workflow. Can you please provide more specific instructions?" + }) + + } catch (error) { + console.error('Chat API error:', error) + + // Handle specific error types + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request format', details: error.errors }, + { status: 400 } + ) + } + + return NextResponse.json( + { error: 'Failed to process chat message' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/w/components/chat/chat.tsx b/app/w/components/chat/chat.tsx index c896f18dac..f132f88b4b 100644 --- a/app/w/components/chat/chat.tsx +++ b/app/w/components/chat/chat.tsx @@ -5,16 +5,19 @@ import { MessageCircle, Send, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { useAIChatStore } from '@/stores/chat/store' export function Chat() { + const { messages, isProcessing, error, sendMessage } = useAIChatStore() const [isOpen, setIsOpen] = useState(false) const [message, setMessage] = useState('') - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - // Handle message submission here + if (!message.trim()) return + + await sendMessage(message) setMessage('') - setIsOpen(false) // Close the chat after sending } const handleKeyDown = (e: React.KeyboardEvent) => { @@ -42,7 +45,7 @@ export function Chat() { } return ( -
+