From 09ec52aa5ad8970cd6f72fce1242ea6080b260dd Mon Sep 17 00:00:00 2001 From: Nikhil-Doye Date: Sat, 18 Oct 2025 00:09:18 -0400 Subject: [PATCH] Add Copilot Panel and integrate AI workflow generation features - Introduced a new CopilotPanel component for enhanced user interaction, allowing users to describe workflows and receive AI-generated suggestions. - Implemented workflow generation from natural language input using the CopilotService, enabling users to create workflows based on their descriptions. - Added validation and suggestion features to improve the generated workflows, ensuring they meet user requirements and best practices. - Enhanced the WorkflowEditor to include a button for opening the CopilotPanel, streamlining the workflow creation process. - Updated the workflow store to manage AI suggestions and validation results, improving the overall user experience and functionality. --- src/components/CopilotPanel.tsx | 415 ++++++++++++++++ src/components/ExecutionPanel.tsx | 16 - src/components/NodeConfiguration.tsx | 2 +- src/components/WorkflowEditor.tsx | 21 +- src/components/WorkflowList.tsx | 5 - src/components/WorkflowToolbar.tsx | 2 +- src/components/nodes/BaseNode.tsx | 3 - src/components/nodes/DataInputNode.tsx | 138 +++++- src/services/copilotService.ts | 634 +++++++++++++++++++++++++ src/services/pdfService.ts | 174 +++++++ src/store/workflowStore.ts | 158 ++++++ src/types/index.ts | 105 ++++ src/utils/patternMatchers.ts | 416 ++++++++++++++++ src/utils/workflowGenerator.ts | 621 ++++++++++++++++++++++++ src/utils/workflowValidator.ts | 530 +++++++++++++++++++++ 15 files changed, 3209 insertions(+), 31 deletions(-) create mode 100644 src/components/CopilotPanel.tsx create mode 100644 src/services/copilotService.ts create mode 100644 src/services/pdfService.ts create mode 100644 src/utils/patternMatchers.ts create mode 100644 src/utils/workflowGenerator.ts create mode 100644 src/utils/workflowValidator.ts diff --git a/src/components/CopilotPanel.tsx b/src/components/CopilotPanel.tsx new file mode 100644 index 0000000..213f2b9 --- /dev/null +++ b/src/components/CopilotPanel.tsx @@ -0,0 +1,415 @@ +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { useWorkflowStore } from "../store/workflowStore"; +import { ValidationResult } from "../types"; +import { + MessageCircle, + Send, + Loader2, + CheckCircle, + AlertTriangle, + Lightbulb, + Sparkles, + X, + RefreshCw, + Eye, + EyeOff, +} from "lucide-react"; + +interface CopilotPanelProps { + isOpen: boolean; + onClose: () => void; +} + +interface ChatMessage { + id: string; + type: "user" | "assistant" | "system"; + content: string; + timestamp: Date; + data?: any; +} + +export const CopilotPanel: React.FC = ({ + isOpen, + onClose, +}) => { + const { + generateWorkflowFromDescription, + getCopilotSuggestions, + validateGeneratedWorkflow, + currentWorkflow, + } = useWorkflowStore(); + + const [input, setInput] = useState(""); + const [messages, setMessages] = useState([]); + const [isProcessing, setIsProcessing] = useState(false); + const [validation, setValidation] = useState(null); + const [showPreview, setShowPreview] = useState(false); + const [suggestions, setSuggestions] = useState([]); + + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // Focus input when panel opens + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isOpen]); + + const loadSuggestions = useCallback(async () => { + try { + const newSuggestions = await getCopilotSuggestions(); + setSuggestions(newSuggestions); + } catch (error) { + console.error("Error loading suggestions:", error); + } + }, [getCopilotSuggestions]); + + // Load initial suggestions + useEffect(() => { + if (isOpen) { + loadSuggestions(); + } + }, [isOpen, currentWorkflow, loadSuggestions]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || isProcessing) return; + + const userMessage: ChatMessage = { + id: Date.now().toString(), + type: "user", + content: input.trim(), + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(""); + setIsProcessing(true); + + try { + // Add processing message + const processingMessage: ChatMessage = { + id: (Date.now() + 1).toString(), + type: "assistant", + content: "🤖 Analyzing your request and generating workflow...", + timestamp: new Date(), + }; + setMessages((prev) => [...prev, processingMessage]); + + // Generate workflow + await generateWorkflowFromDescription(userMessage.content); + + // Get validation results + const validationResult = validateGeneratedWorkflow(); + setValidation(validationResult); + + // Add success message + const successMessage: ChatMessage = { + id: (Date.now() + 2).toString(), + type: "assistant", + content: `✅ Workflow generated successfully! I've created a ${ + validationResult?.complexity || "medium" + } complexity workflow with ${ + currentWorkflow?.nodes.length || 0 + } nodes.`, + timestamp: new Date(), + data: { validation: validationResult }, + }; + setMessages((prev) => [...prev, successMessage]); + + // Load new suggestions + await loadSuggestions(); + } catch (error) { + console.error("Error generating workflow:", error); + + const errorMessage: ChatMessage = { + id: (Date.now() + 2).toString(), + type: "assistant", + content: `❌ Sorry, I couldn't generate the workflow. ${ + error instanceof Error + ? error.message + : "Please try again with a different description." + }`, + timestamp: new Date(), + }; + setMessages((prev) => [...prev, errorMessage]); + } finally { + setIsProcessing(false); + } + }; + + const handleSuggestionClick = (suggestion: string) => { + setInput(suggestion); + if (inputRef.current) { + inputRef.current.focus(); + } + }; + + const clearChat = () => { + setMessages([]); + setValidation(null); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

+ AI Copilot +

+

+ Describe your workflow in natural language +

+
+
+
+ + + +
+
+ +
+ {/* Chat Area */} +
+ {/* Messages */} +
+ {messages.length === 0 ? ( +
+ +

+ Welcome to AI Copilot +

+

+ Describe what you want your workflow to do, and I'll help + you build it! +

+ + {/* Quick Suggestions */} +
+

+ Try these examples: +

+
+ {suggestions.slice(0, 3).map((suggestion, index) => ( + + ))} +
+
+
+ ) : ( + messages.map((message) => ( +
+
+

{message.content}

+ {message.data?.validation && ( +
+
+ + + Validation Results + +
+
+

+ Complexity: {message.data.validation.complexity} +

+

+ Issues: {message.data.validation.issues.length} +

+

+ Suggestions:{" "} + {message.data.validation.suggestions.length} +

+
+
+ )} +
+
+ )) + )} + {isProcessing && ( +
+
+
+ + + Processing... + +
+
+
+ )} +
+
+ + {/* Input Area */} +
+
+ setInput(e.target.value)} + placeholder="Describe your workflow... (e.g., 'scrape a website and analyze the content')" + className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + disabled={isProcessing} + /> + +
+
+
+ + {/* Preview Panel */} + {showPreview && ( +
+

+ Workflow Preview +

+ + {currentWorkflow ? ( +
+
+

+ Current Workflow +

+
+

Name: {currentWorkflow.name}

+

Nodes: {currentWorkflow.nodes.length}

+

Connections: {currentWorkflow.edges.length}

+
+
+ + {validation && ( +
+

+ Validation +

+
+
+ {validation.isValid ? ( + + ) : ( + + )} + + {validation.isValid ? "Valid" : "Has Issues"} + +
+ {validation.issues.length > 0 && ( +
+

Issues:

+
    + {validation.issues.map((issue, index) => ( +
  • {issue}
  • + ))} +
+
+ )} +
+
+ )} + +
+

+ Suggestions +

+
+ {suggestions.map((suggestion, index) => ( + + ))} +
+
+
+ ) : ( +
+ +

+ No workflow generated yet. Start a conversation to see the + preview! +

+
+ )} +
+ )} +
+
+
+ ); +}; diff --git a/src/components/ExecutionPanel.tsx b/src/components/ExecutionPanel.tsx index ec5d1e1..4b2db49 100644 --- a/src/components/ExecutionPanel.tsx +++ b/src/components/ExecutionPanel.tsx @@ -11,7 +11,6 @@ import { Activity, TrendingUp, AlertTriangle, - Info, Copy, Download, Eye, @@ -86,21 +85,6 @@ export const ExecutionPanel: React.FC = () => { } }; - const getNodeTypeColor = (type: string) => { - switch (type) { - case "dataInput": - return "text-blue-600"; - case "webScraping": - return "text-green-600"; - case "llmTask": - return "text-purple-600"; - case "dataOutput": - return "text-orange-600"; - default: - return "text-gray-600"; - } - }; - const toggleNodeExpansion = (nodeId: string) => { const newExpanded = new Set(expandedNodes); if (newExpanded.has(nodeId)) { diff --git a/src/components/NodeConfiguration.tsx b/src/components/NodeConfiguration.tsx index f35001a..40a27bd 100644 --- a/src/components/NodeConfiguration.tsx +++ b/src/components/NodeConfiguration.tsx @@ -156,7 +156,7 @@ const nodeTypeConfigs = { key: "dataType", label: "Data Type", type: "select", - options: ["text", "json", "csv", "url"], + options: ["text", "json", "csv", "url", "pdf"], }, { key: "defaultValue", diff --git a/src/components/WorkflowEditor.tsx b/src/components/WorkflowEditor.tsx index 9ce7d69..8151e7b 100644 --- a/src/components/WorkflowEditor.tsx +++ b/src/components/WorkflowEditor.tsx @@ -17,6 +17,7 @@ import "reactflow/dist/style.css"; import { useWorkflowStore } from "../store/workflowStore"; import { NodeConfiguration } from "./NodeConfiguration"; import { WorkflowToolbar } from "./WorkflowToolbar"; +import { CopilotPanel } from "./CopilotPanel"; import { WebScrapingNode, LLMNode, @@ -29,12 +30,8 @@ import { import { NodeData } from "../types"; import { Search, - Filter, Grid, List, - Settings, - HelpCircle, - Zap, Brain, Globe, ArrowRight, @@ -43,6 +40,7 @@ import { Search as SearchIcon, X, Trash2, + Sparkles, } from "lucide-react"; const nodeTypes: NodeTypes = { @@ -82,6 +80,7 @@ export const WorkflowEditor: React.FC = ({ onClose }) => { }, [currentWorkflow, setNodes, setEdges]); const [showConfig, setShowConfig] = useState(false); const [showNodePalette, setShowNodePalette] = useState(true); + const [showCopilot, setShowCopilot] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState("All"); const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); @@ -465,6 +464,14 @@ export const WorkflowEditor: React.FC = ({ onClose }) => { + + {selectedNodeId && (
); }; diff --git a/src/components/WorkflowList.tsx b/src/components/WorkflowList.tsx index 400b1a3..a9963f3 100644 --- a/src/components/WorkflowList.tsx +++ b/src/components/WorkflowList.tsx @@ -15,11 +15,6 @@ import { Globe, ArrowRight, Clock, - MoreVertical, - Copy, - Share2, - Star, - TrendingUp, HelpCircle, } from "lucide-react"; diff --git a/src/components/WorkflowToolbar.tsx b/src/components/WorkflowToolbar.tsx index 558be7d..37df173 100644 --- a/src/components/WorkflowToolbar.tsx +++ b/src/components/WorkflowToolbar.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from "react"; +import React, { useRef, useState } from "react"; import { useWorkflowStore } from "../store/workflowStore"; import { Download, diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index 94b8be7..6d18ca6 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -6,15 +6,12 @@ import { FileText, Brain, Search, - Database, ArrowDownToLine, ArrowUpFromLine, Loader2, CheckCircle, XCircle, - Zap, AlertCircle, - Clock, } from "lucide-react"; import { clsx } from "clsx"; diff --git a/src/components/nodes/DataInputNode.tsx b/src/components/nodes/DataInputNode.tsx index 2264e03..bcc2462 100644 --- a/src/components/nodes/DataInputNode.tsx +++ b/src/components/nodes/DataInputNode.tsx @@ -1,12 +1,148 @@ -import React from "react"; +import React, { useState, useRef } from "react"; import { BaseNode } from "./BaseNode"; import { NodeData } from "../../types"; import { NodeProps } from "reactflow"; +import { Upload, FileText, X } from "lucide-react"; +import { + processPDF, + validatePDFFile, + getPDFInfo, +} from "../../services/pdfService"; interface DataInputNodeProps extends NodeProps { data: NodeData; } export const DataInputNode: React.FC = (props) => { + const [uploadedFile, setUploadedFile] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + const fileInputRef = useRef(null); + + const handleFileUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + // Validate file + const validation = validatePDFFile(file); + if (!validation.isValid) { + alert(validation.error); + return; + } + + setUploadedFile(file); + setIsProcessing(true); + + try { + // Process PDF file + const result = await processPDF({ file, extractText: true }); + + if (result.success && result.data?.text) { + // Update the node's output with the extracted text + props.data.outputs = [ + { + output: result.data.text, + metadata: result.data.metadata, + }, + ]; + } else { + alert(result.error || "Failed to process PDF"); + } + } catch (error) { + console.error("Error processing PDF:", error); + alert("Error processing PDF file"); + } finally { + setIsProcessing(false); + } + }; + + const handleRemoveFile = () => { + setUploadedFile(null); + props.data.outputs = []; + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; + + // If it's a PDF data type, show file upload interface + if (props.data.config?.dataType === "pdf") { + return ( +
+ {/* Header */} +
+
+
+ +
+
+

+ {props.data.label} +

+

PDF Input

+
+
+
+ + {/* File Upload Area */} +
+ {!uploadedFile ? ( +
+ +

Click to upload PDF

+

or drag and drop

+ +
+ ) : ( +
+
+
+ + + {uploadedFile.name} + +
+ +
+
+ {getPDFInfo(uploadedFile).size} + {isProcessing && " • Processing..."} +
+ {props.data.outputs.length > 0 && ( +
+ ✓ Text extracted successfully +
+ )} +
+ )} +
+ + {/* Bottom Handle */} +
+
+
+
+ ); + } + + // For other data types, use the standard BaseNode return ; }; diff --git a/src/services/copilotService.ts b/src/services/copilotService.ts new file mode 100644 index 0000000..67a6c72 --- /dev/null +++ b/src/services/copilotService.ts @@ -0,0 +1,634 @@ +import { callOpenAI } from "./openaiService"; +import { + ParsedIntent, + IntentClassification, + EntityExtraction, + MixedIntentAnalysis, + WorkflowStructure, + ValidationResult, +} from "../types"; +import { generateMixedWorkflowStructure } from "../utils/workflowGenerator"; +import { + validateWorkflowStructure, + validateMixedWorkflow, + generateImprovementSuggestions, +} from "../utils/workflowValidator"; + +export class CopilotService { + private cache = new Map(); + private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + /** + * Parse natural language input and generate workflow structure + */ + async parseNaturalLanguage(userInput: string): Promise { + // Check cache first + const cacheKey = userInput.toLowerCase().trim(); + const cached = this.cache.get(cacheKey); + if (cached && this.isCacheValid(cached)) { + return cached; + } + + try { + // Use AI to understand the intent and generate workflow directly + const workflowStructure = await this.generateWorkflowStructureWithLLM( + userInput, + { + intent: "AI_GENERATED", + confidence: 0.9, + reasoning: "AI-generated workflow", + }, + { + urls: [], + dataTypes: [], + outputFormats: [], + aiTasks: [], + processingSteps: [], + targetSites: [], + dataSources: [], + } + ); + + // Extract intent from the generated workflow + const intent = this.extractIntentFromWorkflow( + workflowStructure, + userInput + ); + + const parsedIntent: ParsedIntent = { + intent: intent.intent, + confidence: intent.confidence, + entities: intent.entities, + workflowStructure, + reasoning: intent.reasoning, + }; + + // Cache the result + this.cache.set(cacheKey, { + ...parsedIntent, + _cachedAt: Date.now(), + } as any); + + return parsedIntent; + } catch (error) { + console.error("Error in AI workflow generation:", error); + + // Fallback to pattern matching + return this.fallbackParsing(userInput); + } + } + + /** + * Extract intent from generated workflow + */ + private extractIntentFromWorkflow( + workflow: WorkflowStructure, + userInput: string + ): { + intent: string; + confidence: number; + entities: EntityExtraction; + reasoning: string; + } { + // Analyze the workflow to determine intent + const nodeTypes = workflow.nodes.map((node) => node.type); + const hasWebScraping = nodeTypes.includes("webScraping"); + const hasLLMTask = nodeTypes.includes("llmTask"); + const hasDataProcessing = nodeTypes.includes("structuredOutput"); + const hasSearch = nodeTypes.includes("similaritySearch"); + + let intent = "GENERAL_PROCESSING"; + let reasoning = "General data processing workflow"; + + if ( + userInput.toLowerCase().includes("job") || + userInput.toLowerCase().includes("resume") || + userInput.toLowerCase().includes("application") + ) { + intent = "JOB_APPLICATION"; + reasoning = "Job application automation workflow"; + } else if (hasWebScraping && hasLLMTask) { + intent = "WEB_ANALYSIS"; + reasoning = "Web scraping and AI analysis workflow"; + } else if (hasWebScraping) { + intent = "WEB_SCRAPING"; + reasoning = "Web content extraction workflow"; + } else if (hasLLMTask && hasDataProcessing) { + intent = "AI_ANALYSIS"; + reasoning = "AI-powered data analysis workflow"; + } else if (hasSearch) { + intent = "SEARCH_AND_RETRIEVAL"; + reasoning = "Search and similarity matching workflow"; + } + + // Extract entities from user input + const entities: EntityExtraction = { + urls: [], + dataTypes: [], + outputFormats: [], + aiTasks: [], + processingSteps: [], + targetSites: [], + dataSources: [], + }; + + // Extract URLs + const urlPattern = /https?:\/\/[^\s]+/gi; + const urls = userInput.match(urlPattern); + if (urls) { + entities.urls = urls; + } + + // Extract data types based on context + if (userInput.toLowerCase().includes("json")) + entities.dataTypes.push("json"); + if (userInput.toLowerCase().includes("csv")) entities.dataTypes.push("csv"); + if (userInput.toLowerCase().includes("pdf")) entities.dataTypes.push("pdf"); + if ( + userInput.toLowerCase().includes("resume") || + userInput.toLowerCase().includes("cv") + ) + entities.dataTypes.push("text"); + if ( + userInput.toLowerCase().includes("url") || + userInput.toLowerCase().includes("website") + ) + entities.dataTypes.push("url"); + + // Extract AI tasks based on context + if (userInput.toLowerCase().includes("analyze")) + entities.aiTasks.push("analyze"); + if (userInput.toLowerCase().includes("summarize")) + entities.aiTasks.push("summarize"); + if (userInput.toLowerCase().includes("generate")) + entities.aiTasks.push("generate"); + if (userInput.toLowerCase().includes("extract")) + entities.aiTasks.push("extract"); + + return { + intent, + confidence: 0.9, + entities, + reasoning, + }; + } + + /** + * Classify intent using LLM + */ + private async classifyIntentWithLLM( + userInput: string + ): Promise { + const prompt = ` +Analyze this natural language description and classify the workflow intent: + +User Input: "${userInput}" + +Classify into one of these categories: +- WEB_SCRAPING: Extract data from websites +- AI_ANALYSIS: Process text with AI models +- DATA_PROCESSING: Transform or structure data +- SEARCH_AND_RETRIEVAL: Find similar content +- CONTENT_GENERATION: Create new content +- MIXED: Multiple operations requiring different node types + +Also identify if this is a MIXED intent by looking for multiple distinct operations. + +Respond with JSON: +{ + "intent": "WEB_SCRAPING", + "confidence": 0.95, + "reasoning": "User wants to extract data from a website" +} +`; + + try { + const response = await callOpenAI(prompt, { + model: "deepseek-chat", + temperature: 0.1, + maxTokens: 200, + }); + + return JSON.parse(response.content); + } catch (error) { + console.error("Error in LLM intent classification:", error); + return { + intent: "GENERAL_PROCESSING", + confidence: 0.5, + reasoning: "Fallback intent due to AI processing error", + }; + } + } + + /** + * Extract entities using LLM + */ + private async extractEntitiesWithLLM( + userInput: string + ): Promise { + const prompt = ` +Extract specific entities from this workflow description: + +Input: "${userInput}" + +Extract: +- URLs: Any website addresses +- Data types: text, JSON, CSV, PDF, etc. +- Output formats: JSON, text, markdown, etc. +- AI tasks: summarization, analysis, classification, etc. +- Processing steps: what transformations are needed +- Target sites: job boards, news sites, etc. +- Data sources: resume, documents, etc. + +Respond with JSON: +{ + "urls": ["https://example.com"], + "dataTypes": ["text"], + "outputFormats": ["JSON"], + "aiTasks": ["summarize", "extract key points"], + "processingSteps": ["scrape content", "analyze with AI", "format output"], + "targetSites": ["job boards"], + "dataSources": ["resume"] +} +`; + + try { + const response = await callOpenAI(prompt, { + model: "deepseek-chat", + temperature: 0.1, + maxTokens: 300, + }); + + return JSON.parse(response.content); + } catch (error) { + console.error("Error in LLM entity extraction:", error); + return { + urls: [], + dataTypes: ["text"], + outputFormats: ["text"], + aiTasks: ["process"], + processingSteps: [], + targetSites: [], + dataSources: [], + }; + } + } + + /** + * Generate workflow structure using LLM + */ + private async generateWorkflowStructureWithLLM( + userInput: string, + intent: IntentClassification, + entities: EntityExtraction + ): Promise { + const prompt = ` +You are an AI workflow designer. Analyze the user's request and create a comprehensive workflow structure. + +User Request: "${userInput}" + +Available node types and their purposes: +- dataInput: Entry point for data (text, JSON, CSV, URL, PDF, etc.) +- webScraping: Extract content from websites using Firecrawl +- llmTask: Process data with AI models (analysis, generation, transformation) +- structuredOutput: Format data according to JSON schemas +- embeddingGenerator: Create vector embeddings for text +- similaritySearch: Find similar content using vector search +- dataOutput: Export results in various formats + +Instructions: +1. Understand the user's goal and break it down into logical steps +2. Create a workflow that accomplishes their request +3. Use appropriate node types for each step +4. Configure nodes with realistic settings +5. Connect nodes logically with proper data flow +6. Use variable substitution ({{nodeId.output}}) to pass data between nodes +7. Make the workflow practical and executable + +For job application workflows, consider: +- Resume analysis and skill extraction +- Job matching and opportunity identification +- Application generation and personalization +- Cover letter creation + +For web scraping workflows, consider: +- URL input and validation +- Content extraction with appropriate formats +- Data cleaning and processing +- Output formatting + +For PDF processing workflows, consider: +- PDF file input and validation +- Text extraction from PDF content +- AI analysis of extracted text +- Structured output formatting + +For AI analysis workflows, consider: +- Data input and preprocessing +- AI processing with appropriate prompts +- Result formatting and structuring +- Output generation + +Respond with valid JSON only: +{ + "nodes": [ + { + "type": "dataInput", + "label": "Descriptive Node Name", + "config": { + "dataType": "text|json|csv|url", + "defaultValue": "Sample input data" + } + } + ], + "edges": [ + { + "source": "node-0", + "target": "node-1" + } + ], + "topology": { + "type": "linear|fork-join|branching", + "description": "Workflow description", + "parallelExecution": false + }, + "complexity": "low|medium|high", + "estimatedExecutionTime": 5000 +} +`; + + try { + const response = await callOpenAI(prompt, { + model: "deepseek-chat", + temperature: 0.3, + maxTokens: 1000, + }); + + return JSON.parse(response.content); + } catch (error) { + console.error("Error in LLM workflow generation:", error); + // Return a simple fallback workflow + return { + nodes: [ + { + type: "dataInput", + label: "Input Data", + config: { + dataType: "text", + defaultValue: "Enter your data here", + }, + }, + { + type: "llmTask", + label: "AI Processor", + config: { + prompt: `Process the following input: ${userInput}\n\nInput: {{input.output}}`, + model: "deepseek-chat", + temperature: 0.7, + }, + }, + { + type: "dataOutput", + label: "Output Result", + config: { + format: "text", + filename: "result.txt", + }, + }, + ], + edges: [ + { source: "node-0", target: "node-1" }, + { source: "node-1", target: "node-2" }, + ], + topology: { + type: "linear", + description: "Simple AI processing workflow", + parallelExecution: false, + }, + complexity: "low", + estimatedExecutionTime: 5000, + }; + } + } + + /** + * Fallback parsing using simple AI generation + */ + private fallbackParsing(userInput: string): ParsedIntent { + // Create a simple workflow as fallback + const workflowStructure: WorkflowStructure = { + nodes: [ + { + type: "dataInput", + label: "Input Data", + config: { + dataType: "text", + defaultValue: "Enter your data here", + }, + }, + { + type: "llmTask", + label: "AI Processor", + config: { + prompt: `Process the following input: ${userInput}\n\nInput: {{input.output}}`, + model: "deepseek-chat", + temperature: 0.7, + }, + }, + { + type: "dataOutput", + label: "Output Result", + config: { + format: "text", + filename: "result.txt", + }, + }, + ], + edges: [ + { source: "node-0", target: "node-1" }, + { source: "node-1", target: "node-2" }, + ], + topology: { + type: "linear", + description: "Simple AI processing workflow", + parallelExecution: false, + }, + complexity: "low", + estimatedExecutionTime: 5000, + }; + + return { + intent: "FALLBACK_PROCESSING", + confidence: 0.5, + entities: { + urls: [], + dataTypes: ["text"], + outputFormats: ["text"], + aiTasks: ["process"], + processingSteps: [], + targetSites: [], + dataSources: [], + }, + workflowStructure, + reasoning: "Fallback workflow generated due to AI processing error", + }; + } + + /** + * Analyze mixed intent workflows + */ + async analyzeMixedIntent(userInput: string): Promise { + // Simple analysis based on user input + const hasMultipleKeywords = + (userInput.toLowerCase().includes("web") && + userInput.toLowerCase().includes("ai")) || + (userInput.toLowerCase().includes("scrape") && + userInput.toLowerCase().includes("analyze")) || + (userInput.toLowerCase().includes("data") && + userInput.toLowerCase().includes("process")); + + const intent = hasMultipleKeywords ? "MIXED" : "GENERAL_PROCESSING"; + const confidence = hasMultipleKeywords ? 0.8 : 0.6; + + return { + intent, + confidence, + reasoning: hasMultipleKeywords + ? "Mixed workflow combining multiple processing types" + : "General processing workflow", + subIntents: [intent], + subConfidences: { [intent]: confidence }, + complexity: { + level: hasMultipleKeywords ? "high" : "medium", + score: hasMultipleKeywords ? 0.8 : 0.5, + patterns: hasMultipleKeywords ? ["mixed", "complex"] : ["simple"], + estimatedNodes: hasMultipleKeywords ? 5 : 3, + }, + }; + } + + /** + * Generate workflow from mixed intent analysis + */ + async generateMixedWorkflow( + mixedAnalysis: MixedIntentAnalysis, + userInput: string + ): Promise { + return generateMixedWorkflowStructure(mixedAnalysis, userInput); + } + + /** + * Validate generated workflow + */ + validateWorkflow( + workflow: WorkflowStructure, + originalInput: string + ): ValidationResult { + return validateWorkflowStructure(workflow, originalInput); + } + + /** + * Validate mixed workflow + */ + validateMixedWorkflow( + workflow: WorkflowStructure, + originalInput: string + ): ValidationResult { + return validateMixedWorkflow(workflow, originalInput); + } + + /** + * Get improvement suggestions + */ + getImprovementSuggestions(workflow: WorkflowStructure): string[] { + return generateImprovementSuggestions(workflow); + } + + /** + * Generate contextual suggestions based on current workflow + */ + async generateContextualSuggestions( + currentWorkflow: WorkflowStructure | null, + userInput: string + ): Promise { + const suggestions: string[] = []; + + if (!currentWorkflow) { + return suggestions; + } + + // Analyze current workflow state + const nodeCount = currentWorkflow.nodes.length; + const hasWebScraping = currentWorkflow.nodes.some( + (n) => n.type === "webScraping" + ); + const hasLLM = currentWorkflow.nodes.some((n) => n.type === "llmTask"); + const hasOutput = currentWorkflow.nodes.some( + (n) => n.type === "dataOutput" + ); + + // Generate suggestions based on current state + if (nodeCount === 0) { + suggestions.push("Start by adding a data input node"); + } else if (!hasOutput) { + suggestions.push("Add a data output node to complete the workflow"); + } else if (hasWebScraping && !hasLLM) { + suggestions.push( + "Consider adding an AI analysis node after web scraping" + ); + } else if ( + hasLLM && + !hasWebScraping && + userInput.toLowerCase().includes("website") + ) { + suggestions.push("Add a web scraping node to extract data from websites"); + } + + return suggestions; + } + + /** + * Learn from user modifications + */ + learnFromModifications( + originalWorkflow: WorkflowStructure, + modifiedWorkflow: WorkflowStructure, + userFeedback?: string + ): void { + // In a real implementation, this would update the learning model + // For now, we'll just log the changes + console.log("Learning from user modifications:", { + original: originalWorkflow, + modified: modifiedWorkflow, + feedback: userFeedback, + }); + } + + /** + * Clear cache + */ + clearCache(): void { + this.cache.clear(); + } + + /** + * Check if cache entry is still valid + */ + private isCacheValid(entry: any): boolean { + if (!entry._cachedAt) return false; + return Date.now() - entry._cachedAt < this.CACHE_TTL; + } + + /** + * Get cache statistics + */ + getCacheStats(): { size: number; hitRate: number } { + return { + size: this.cache.size, + hitRate: 0.8, // Placeholder - would track actual hit rate + }; + } +} + +// Export singleton instance +export const copilotService = new CopilotService(); diff --git a/src/services/pdfService.ts b/src/services/pdfService.ts new file mode 100644 index 0000000..fda664a --- /dev/null +++ b/src/services/pdfService.ts @@ -0,0 +1,174 @@ +/** + * PDF Processing Service + * Handles PDF file processing and text extraction + */ + +export interface PDFConfig { + file: File; + extractText?: boolean; + extractImages?: boolean; + extractMetadata?: boolean; +} + +export interface PDFResponse { + success: boolean; + data?: { + text?: string; + metadata?: { + title?: string; + author?: string; + subject?: string; + creator?: string; + producer?: string; + creationDate?: string; + modificationDate?: string; + pageCount?: number; + }; + pages?: Array<{ + pageNumber: number; + text: string; + }>; + }; + error?: string; +} + +/** + * Process PDF file and extract content + */ +export const processPDF = async (config: PDFConfig): Promise => { + try { + const { file, extractText = true, extractMetadata = true } = config; + + // Validate file type + if (file.type !== "application/pdf") { + return { + success: false, + error: "Invalid file type. Please upload a PDF file.", + }; + } + + // For now, we'll use a simple approach that reads the file as text + // In a real implementation, you would use a PDF parsing library like pdf-parse or pdfjs-dist + const text = await readPDFAsText(file); + + const response: PDFResponse = { + success: true, + data: { + text: extractText ? text : undefined, + metadata: extractMetadata + ? { + title: file.name, + pageCount: 1, // This would be extracted from actual PDF metadata + } + : undefined, + pages: extractText + ? [ + { + pageNumber: 1, + text: text, + }, + ] + : undefined, + }, + }; + + return response; + } catch (error) { + console.error("Error processing PDF:", error); + return { + success: false, + error: + error instanceof Error ? error.message : "Failed to process PDF file", + }; + } +}; + +/** + * Read PDF file as text (simplified implementation) + * In a real implementation, you would use a proper PDF parsing library + */ +const readPDFAsText = async (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (event) => { + try { + // This is a simplified implementation + // In reality, you would need to parse the PDF binary data + const arrayBuffer = event.target?.result as ArrayBuffer; + + // For demonstration purposes, we'll return a placeholder + // In a real implementation, you would use pdf-parse or similar + const text = `[PDF Content from ${file.name}] + +This is a placeholder for PDF text extraction. In a real implementation, you would use a PDF parsing library like pdf-parse or pdfjs-dist to extract the actual text content from the PDF file. + +The PDF file "${file.name}" has been uploaded and would be processed to extract: +- Text content from all pages +- Metadata (title, author, creation date, etc.) +- Page-by-page text extraction +- Image extraction (if needed) + +For now, this is a demonstration of how PDF processing would work in the workflow builder.`; + + resolve(text); + } catch (error) { + reject(error); + } + }; + + reader.onerror = () => { + reject(new Error("Failed to read PDF file")); + }; + + reader.readAsArrayBuffer(file); + }); +}; + +/** + * Validate PDF file + */ +export const validatePDFFile = ( + file: File +): { isValid: boolean; error?: string } => { + if (!file) { + return { isValid: false, error: "No file provided" }; + } + + if (file.type !== "application/pdf") { + return { isValid: false, error: "File must be a PDF" }; + } + + if (file.size > 10 * 1024 * 1024) { + // 10MB limit + return { isValid: false, error: "File size must be less than 10MB" }; + } + + return { isValid: true }; +}; + +/** + * Get PDF file info + */ +export const getPDFInfo = ( + file: File +): { name: string; size: string; type: string } => { + return { + name: file.name, + size: formatFileSize(file.size), + type: file.type, + }; +}; + +/** + * Format file size in human readable format + */ +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +}; diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 8b42f52..2ef59ba 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -5,6 +5,8 @@ import { WorkflowEdge, NodeData, NodeStatus, + ValidationResult, + WorkflowStructure, } from "../types"; import { v4 as uuidv4 } from "uuid"; import { callOpenAI, OpenAIConfig } from "../services/openaiService"; @@ -13,6 +15,7 @@ import { FirecrawlConfig, } from "../services/firecrawlService"; import { substituteVariables, NodeOutput } from "../utils/variableSubstitution"; +import { copilotService } from "../services/copilotService"; // localStorage key for workflows const WORKFLOWS_STORAGE_KEY = "agent-workflow-builder-workflows"; @@ -100,6 +103,12 @@ interface WorkflowStore { error?: string ) => void; clearExecutionResults: () => void; + + // Copilot methods + generateWorkflowFromDescription: (description: string) => Promise; + applyCopilotSuggestions: (suggestions: any[]) => void; + validateGeneratedWorkflow: () => ValidationResult | null; + getCopilotSuggestions: (context?: string) => Promise; } const createEmptyWorkflow = (name: string): Workflow => ({ @@ -470,6 +479,155 @@ export const useWorkflowStore = create((set, get) => ({ clearExecutionResults: () => { set({ executionResults: {}, isExecuting: false }); }, + + // Copilot methods + generateWorkflowFromDescription: async (description: string) => { + try { + const parsedIntent = await copilotService.parseNaturalLanguage( + description + ); + const workflowStructure = parsedIntent.workflowStructure; + + if (!workflowStructure) { + throw new Error("Failed to generate workflow structure"); + } + + // Create a new workflow with the generated structure + const newWorkflow = createEmptyWorkflow(parsedIntent.intent); + + // Convert WorkflowStructure to Workflow format + const nodes: WorkflowNode[] = workflowStructure.nodes.map( + (node, index) => ({ + id: `node-${index}`, + type: node.type, + position: node.position || { x: 100 + index * 200, y: 100 }, + data: { + id: `node-${index}`, + type: node.type as any, + label: node.label, + status: "idle" as const, + config: node.config, + inputs: [], + outputs: [], + }, + }) + ); + + const edges: WorkflowEdge[] = workflowStructure.edges.map( + (edge, index) => ({ + id: `edge-${index}`, + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + }) + ); + + const generatedWorkflow: Workflow = { + ...newWorkflow, + name: `Generated: ${parsedIntent.intent}`, + nodes, + edges, + }; + + // Add to workflows and set as current + const newWorkflows = [...get().workflows, generatedWorkflow]; + set({ + workflows: newWorkflows, + currentWorkflow: generatedWorkflow, + }); + + // Persist to localStorage + saveWorkflowsToStorage(newWorkflows); + } catch (error) { + console.error("Error generating workflow from description:", error); + throw error; + } + }, + + applyCopilotSuggestions: (suggestions: any[]) => { + const { currentWorkflow } = get(); + if (!currentWorkflow) return; + + // Apply suggestions to current workflow + // This is a simplified implementation - in practice, you'd have more sophisticated suggestion application + suggestions.forEach((suggestion) => { + if (suggestion.type === "node" && suggestion.nodeId) { + // Apply node-level suggestions + const node = currentWorkflow.nodes.find( + (n) => n.id === suggestion.nodeId + ); + if (node) { + // Apply suggestion to node configuration + console.log("Applying suggestion to node:", suggestion); + } + } + }); + }, + + validateGeneratedWorkflow: (): ValidationResult | null => { + const { currentWorkflow } = get(); + if (!currentWorkflow) return null; + + // Convert current workflow to WorkflowStructure format for validation + const workflowStructure: WorkflowStructure = { + nodes: currentWorkflow.nodes.map((node) => ({ + type: node.type, + label: node.data.label, + config: node.data.config, + position: node.position, + })), + edges: currentWorkflow.edges.map((edge) => ({ + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + })), + topology: { + type: "linear", + description: "Sequential processing", + parallelExecution: false, + }, + complexity: "medium", + }; + + return copilotService.validateWorkflow(workflowStructure, ""); + }, + + getCopilotSuggestions: async (context?: string): Promise => { + const { currentWorkflow } = get(); + + if (!currentWorkflow) { + return ["Start by creating a new workflow"]; + } + + // Convert current workflow to WorkflowStructure format + const workflowStructure: WorkflowStructure = { + nodes: currentWorkflow.nodes.map((node) => ({ + type: node.type, + label: node.data.label, + config: node.data.config, + position: node.position, + })), + edges: currentWorkflow.edges.map((edge) => ({ + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + })), + topology: { + type: "linear", + description: "Sequential processing", + parallelExecution: false, + }, + complexity: "medium", + }; + + return await copilotService.generateContextualSuggestions( + workflowStructure, + context || "" + ); + }, })); // Node processing functions diff --git a/src/types/index.ts b/src/types/index.ts index 9405a20..07339d8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,3 +60,108 @@ export interface WorkflowExecution { startedAt: Date; completedAt?: Date; } + +// Copilot Types +export interface IntentClassification { + intent: string; + confidence: number; + reasoning: string; +} + +export interface EntityExtraction { + urls: string[]; + dataTypes: string[]; + outputFormats: string[]; + aiTasks: string[]; + processingSteps: string[]; + targetSites?: string[]; + dataSources?: string[]; +} + +export interface MixedIntentAnalysis { + intent: string; + confidence: number; + reasoning: string; + subIntents: string[]; + subConfidences: Record; + complexity: ComplexityAnalysis; +} + +export interface ComplexityAnalysis { + level: "low" | "medium" | "high"; + score: number; + patterns: string[]; + estimatedNodes: number; +} + +export interface ParsedIntent { + intent: string; + confidence: number; + entities: EntityExtraction; + workflowStructure: WorkflowStructure; + reasoning: string; +} + +export interface WorkflowStructure { + nodes: WorkflowNodeStructure[]; + edges: WorkflowEdgeStructure[]; + topology: WorkflowTopology; + complexity: string; + estimatedExecutionTime?: number; + validationRules?: string[]; +} + +export interface WorkflowNodeStructure { + type: string; + label: string; + config: Record; + position?: { x: number; y: number }; +} + +export interface WorkflowEdgeStructure { + source: string; + target: string; + sourceHandle?: string; + targetHandle?: string; +} + +export interface WorkflowTopology { + type: "linear" | "fork-join" | "branching"; + description: string; + parallelExecution: boolean; +} + +export interface CopilotSuggestion { + type: "node" | "configuration" | "connection"; + nodeId?: string; + suggestion: string; + confidence: number; + reasoning: string; +} + +export interface WorkflowTemplate { + name: string; + description: string; + pattern: WorkflowPattern; + useCases: string[]; + complexity: "simple" | "medium" | "complex"; +} + +export interface WorkflowPattern { + nodes: WorkflowNodeStructure[]; + edges: WorkflowEdgeStructure[]; + description: string; +} + +export interface ValidationResult { + isValid: boolean; + issues: string[]; + suggestions: string[]; + complexity?: string; + estimatedExecutionTime?: number; +} + +export interface MixedValidationResult extends ValidationResult { + complexity: string; + estimatedExecutionTime: number; +} diff --git a/src/utils/patternMatchers.ts b/src/utils/patternMatchers.ts new file mode 100644 index 0000000..5b1e7a3 --- /dev/null +++ b/src/utils/patternMatchers.ts @@ -0,0 +1,416 @@ +import { + IntentClassification, + EntityExtraction, + ComplexityAnalysis, +} from "../types"; + +// Pattern matching for different intent types +export const intentPatterns = { + WEB_SCRAPING: [ + /scrape/i, + /extract.*website/i, + /get.*from.*url/i, + /web.*content/i, + /html.*content/i, + /crawl/i, + /fetch.*page/i, + /download.*content/i, + ], + AI_ANALYSIS: [ + /analyze/i, + /summarize/i, + /classify/i, + /sentiment/i, + /ai.*process/i, + /llm/i, + /gpt/i, + /artificial.*intelligence/i, + /machine.*learning/i, + /nlp/i, + /natural.*language/i, + ], + DATA_PROCESSING: [ + /convert/i, + /transform/i, + /format/i, + /parse/i, + /json/i, + /csv/i, + /structure/i, + /process.*data/i, + /clean.*data/i, + /normalize/i, + /standardize/i, + ], + SEARCH_AND_RETRIEVAL: [ + /search/i, + /find.*similar/i, + /embedding/i, + /vector.*search/i, + /similarity/i, + /match/i, + /retrieve/i, + /lookup/i, + /query/i, + ], + CONTENT_GENERATION: [ + /generate/i, + /create.*content/i, + /write/i, + /produce/i, + /synthesize/i, + /compose/i, + /draft/i, + /author/i, + /craft/i, + ], +}; + +// Mixed intent patterns +export const mixedIntentPatterns = { + // Sequential operations + sequential: [ + /first.*then/i, + /scrape.*and.*analyze/i, + /extract.*then.*process/i, + /get.*data.*and.*transform/i, + /step.*by.*step/i, + /after.*that/i, + /then.*also/i, + ], + + // Parallel operations + parallel: [ + /both.*and/i, + /simultaneously/i, + /at.*same.*time/i, + /while.*also/i, + /meanwhile/i, + /concurrently/i, + ], + + // Conditional operations + conditional: [ + /if.*then/i, + /depending.*on/i, + /based.*on.*result/i, + /when.*also/i, + /unless/i, + /provided.*that/i, + ], + + // Complex workflows + complex: [ + /pipeline/i, + /workflow/i, + /process.*through/i, + /multiple.*steps/i, + /end.*to.*end/i, + /automation/i, + /orchestration/i, + ], +}; + +// Complexity indicators +export const complexityIndicators = { + // Temporal relationships + temporal: { + sequential: /first.*then|step.*by.*step|after.*that/i, + parallel: /simultaneously|at.*same.*time|while.*also/i, + conditional: /if.*then|depending.*on|based.*on/i, + }, + + // Data flow patterns + dataFlow: { + linear: /pass.*to|send.*to|forward.*to/i, + branching: /split.*into|divide.*by|separate/i, + merging: /combine.*with|merge.*into|join.*together/i, + }, + + // Processing patterns + processing: { + batch: /batch.*process|all.*at.*once/i, + streaming: /real.*time|live.*data|continuous/i, + iterative: /repeat.*until|loop.*through|iterate/i, + }, +}; + +/** + * Quick intent recognition using pattern matching + */ +export function quickIntentRecognition(userInput: string): string | null { + for (const [intent, patterns] of Object.entries(intentPatterns)) { + if (patterns.some((pattern) => pattern.test(userInput))) { + return intent; + } + } + return null; +} + +/** + * Classify intent with confidence scoring + */ +export function classifyIntent(userInput: string): IntentClassification { + const detectedIntents: string[] = []; + const confidenceScores: Record = {}; + + // Score each intent type + Object.entries(intentPatterns).forEach(([intent, patterns]) => { + let score = 0; + patterns.forEach((pattern) => { + const matches = userInput.match(new RegExp(pattern, "gi")); + if (matches) { + score += matches.length * 0.2; // Weight by number of matches + } + }); + + if (score > 0.3) { + // Threshold for detection + detectedIntents.push(intent); + confidenceScores[intent] = Math.min(score, 1.0); + } + }); + + // Determine primary intent + const primaryIntent = + detectedIntents.length > 0 ? detectedIntents[0] : "UNKNOWN"; + const confidence = confidenceScores[primaryIntent] || 0; + + return { + intent: primaryIntent, + confidence, + reasoning: generateIntentReasoning( + primaryIntent, + detectedIntents, + userInput + ), + }; +} + +/** + * Extract entities from user input + */ +export function extractEntities(userInput: string): EntityExtraction { + const entities: EntityExtraction = { + urls: [], + dataTypes: [], + outputFormats: [], + aiTasks: [], + processingSteps: [], + targetSites: [], + dataSources: [], + }; + + // Extract URLs + const urlPattern = /https?:\/\/[^\s]+/gi; + const urls = userInput.match(urlPattern); + if (urls) { + entities.urls = urls; + } + + // Extract data types + const dataTypePatterns = [ + { pattern: /json/i, type: "json" }, + { pattern: /csv/i, type: "csv" }, + { pattern: /pdf/i, type: "pdf" }, + { pattern: /text/i, type: "text" }, + { pattern: /xml/i, type: "xml" }, + { pattern: /yaml/i, type: "yaml" }, + ]; + + dataTypePatterns.forEach(({ pattern, type }) => { + if (pattern.test(userInput)) { + entities.dataTypes.push(type); + } + }); + + // Extract AI tasks + const aiTaskPatterns = [ + { pattern: /summarize/i, task: "summarize" }, + { pattern: /analyze/i, task: "analyze" }, + { pattern: /classify/i, task: "classify" }, + { pattern: /translate/i, task: "translate" }, + { pattern: /generate/i, task: "generate" }, + { pattern: /extract.*key.*points/i, task: "extract_key_points" }, + { pattern: /sentiment.*analysis/i, task: "sentiment_analysis" }, + ]; + + aiTaskPatterns.forEach(({ pattern, task }) => { + if (pattern.test(userInput)) { + entities.aiTasks.push(task); + } + }); + + // Extract processing steps + const processingStepPatterns = [ + { pattern: /scrape/i, step: "scrape" }, + { pattern: /extract/i, step: "extract" }, + { pattern: /transform/i, step: "transform" }, + { pattern: /convert/i, step: "convert" }, + { pattern: /filter/i, step: "filter" }, + { pattern: /sort/i, step: "sort" }, + { pattern: /validate/i, step: "validate" }, + ]; + + processingStepPatterns.forEach(({ pattern, step }) => { + if (pattern.test(userInput)) { + entities.processingSteps.push(step); + } + }); + + return entities; +} + +/** + * Analyze complexity of mixed workflows + */ +export function analyzeComplexity( + userInput: string, + detectedIntents: string[] +): ComplexityAnalysis { + let complexityScore = 0; + const detectedPatterns: string[] = []; + + // Check temporal relationships + Object.entries(complexityIndicators.temporal).forEach(([pattern, regex]) => { + if (regex.test(userInput)) { + complexityScore += 0.1; + detectedPatterns.push(`temporal:${pattern}`); + } + }); + + // Check data flow patterns + Object.entries(complexityIndicators.dataFlow).forEach(([pattern, regex]) => { + if (regex.test(userInput)) { + complexityScore += 0.1; + detectedPatterns.push(`dataFlow:${pattern}`); + } + }); + + // Check processing patterns + Object.entries(complexityIndicators.processing).forEach( + ([pattern, regex]) => { + if (regex.test(userInput)) { + complexityScore += 0.1; + detectedPatterns.push(`processing:${pattern}`); + } + } + ); + + // Additional complexity from number of intents + complexityScore += (detectedIntents.length - 1) * 0.2; + + // Additional complexity from mixed intent patterns + Object.entries(mixedIntentPatterns).forEach(([category, patterns]) => { + patterns.forEach((pattern) => { + if (pattern.test(userInput)) { + complexityScore += 0.05; + detectedPatterns.push(`mixed:${category}`); + } + }); + }); + + const level = + complexityScore > 0.7 ? "high" : complexityScore > 0.4 ? "medium" : "low"; + const estimatedNodes = Math.max( + 3, + detectedIntents.length + Math.floor(complexityScore * 3) + ); + + return { + level, + score: Math.min(complexityScore, 1.0), + patterns: detectedPatterns, + estimatedNodes, + }; +} + +/** + * Check if input indicates mixed intent + */ +export function isMixedIntent( + userInput: string, + detectedIntents: string[] +): boolean { + if (detectedIntents.length <= 1) return false; + + // Check for explicit mixed intent indicators + const mixedIndicators = [ + /and.*also/i, + /then.*also/i, + /while.*also/i, + /pipeline/i, + /workflow/i, + /multiple.*steps/i, + /end.*to.*end/i, + ]; + + return mixedIndicators.some((pattern) => pattern.test(userInput)); +} + +/** + * Generate reasoning for intent classification + */ +function generateIntentReasoning( + primaryIntent: string, + detectedIntents: string[], + userInput: string +): string { + if (detectedIntents.length === 0) { + return "No clear intent patterns detected in the input"; + } + + if (detectedIntents.length === 1) { + return `Clear ${primaryIntent} intent detected from user input`; + } + + if (isMixedIntent(userInput, detectedIntents)) { + return `Mixed intent detected: ${detectedIntents.join( + ", " + )}. User wants to perform multiple operations in sequence or parallel`; + } + + return `Multiple intents detected: ${detectedIntents.join( + ", " + )}. Primary intent: ${primaryIntent}`; +} + +/** + * Map intent to node type + */ +export function mapIntentToNodeType(intent: string): string { + const intentToNodeMap: Record = { + WEB_SCRAPING: "webScraping", + AI_ANALYSIS: "llmTask", + DATA_PROCESSING: "structuredOutput", + SEARCH_AND_RETRIEVAL: "similaritySearch", + CONTENT_GENERATION: "llmTask", + }; + + return intentToNodeMap[intent] || "llmTask"; +} + +/** + * Generate node label based on intent and context + */ +export function generateNodeLabel( + intent: string, + index: number, + context?: string +): string { + const labelMap: Record = { + WEB_SCRAPING: "Web Scraper", + AI_ANALYSIS: "AI Analyzer", + DATA_PROCESSING: "Data Processor", + SEARCH_AND_RETRIEVAL: "Similarity Search", + CONTENT_GENERATION: "Content Generator", + }; + + const baseLabel = labelMap[intent] || "AI Task"; + + if (index > 0) { + return `${baseLabel} ${index + 1}`; + } + + return baseLabel; +} diff --git a/src/utils/workflowGenerator.ts b/src/utils/workflowGenerator.ts new file mode 100644 index 0000000..782251c --- /dev/null +++ b/src/utils/workflowGenerator.ts @@ -0,0 +1,621 @@ +import { + WorkflowStructure, + WorkflowNodeStructure, + WorkflowEdgeStructure, + WorkflowTopology, + MixedIntentAnalysis, + ParsedIntent, +} from "../types"; +// Simple helper functions to replace pattern matchers +const mapIntentToNodeType = (intent: string): string => { + const intentLower = intent.toLowerCase(); + if (intentLower.includes("web") || intentLower.includes("scraping")) + return "webScraping"; + if ( + intentLower.includes("ai") || + intentLower.includes("analysis") || + intentLower.includes("process") + ) + return "llmTask"; + if (intentLower.includes("search") || intentLower.includes("similarity")) + return "similaritySearch"; + if (intentLower.includes("embedding")) return "embeddingGenerator"; + if (intentLower.includes("structure") || intentLower.includes("format")) + return "structuredOutput"; + return "llmTask"; // Default to LLM task +}; + +const generateNodeLabel = (intent: string, index: number): string => { + const intentLower = intent.toLowerCase(); + if (intentLower.includes("job") || intentLower.includes("application")) { + const labels = [ + "Resume Input", + "Resume Analyzer", + "Job Matcher", + "Application Generator", + ]; + return labels[index] || `Job Step ${index + 1}`; + } + if (intentLower.includes("web") || intentLower.includes("scraping")) { + const labels = [ + "URL Input", + "Web Scraper", + "Content Processor", + "Data Output", + ]; + return labels[index] || `Web Step ${index + 1}`; + } + if (intentLower.includes("ai") || intentLower.includes("analysis")) { + const labels = ["Data Input", "AI Analyzer", "Result Processor", "Output"]; + return labels[index] || `AI Step ${index + 1}`; + } + return `Processing Step ${index + 1}`; +}; + +/** + * Generate workflow structure from parsed intent + */ +export function generateWorkflowStructure( + parsedIntent: ParsedIntent +): WorkflowStructure { + const { intent, entities } = parsedIntent; + + // Determine workflow topology + const topology = determineWorkflowTopology(intent, entities); + + // Generate node sequence + const nodes = generateNodeSequence(intent, entities); + + // Generate edges + const edges = generateEdges(nodes, topology); + + // Calculate positions + const positionedNodes = calculateNodePositions(nodes, topology); + + return { + nodes: positionedNodes, + edges, + topology, + complexity: determineComplexityLevel(nodes.length), + estimatedExecutionTime: estimateExecutionTime(nodes), + validationRules: generateValidationRules(nodes), + }; +} + +/** + * Generate workflow structure for mixed intents + */ +export function generateMixedWorkflowStructure( + mixedAnalysis: MixedIntentAnalysis, + userInput: string +): WorkflowStructure { + const { subIntents, complexity } = mixedAnalysis; + + // Determine workflow topology based on complexity + const topology = determineMixedTopology(complexity, userInput); + + // Generate node sequence based on sub-intents + const nodes = subIntents.map((intent, index) => { + const nodeConfig = generateNodeConfigForIntent(intent, userInput, index); + return { + type: mapIntentToNodeType(intent), + label: generateNodeLabel(intent, index), + config: nodeConfig, + }; + }); + + // Add necessary input/output nodes + const enhancedNodes = ensureInputOutputNodes(nodes); + + // Generate edges based on topology + const edges = generateEdgesForTopology(enhancedNodes, topology); + + // Calculate positions + const positionedNodes = calculateNodePositions(enhancedNodes, topology); + + return { + nodes: positionedNodes, + edges, + topology, + complexity: complexity.level, + estimatedExecutionTime: estimateExecutionTime(enhancedNodes), + validationRules: generateValidationRules(enhancedNodes), + }; +} + +/** + * Determine workflow topology based on intent and entities + */ +function determineWorkflowTopology( + intent: string, + entities: any +): WorkflowTopology { + // Check for parallel processing indicators + const parallelIndicators = [ + /simultaneously/i, + /at.*same.*time/i, + /while.*also/i, + /concurrently/i, + ]; + + const hasParallel = parallelIndicators.some((pattern) => + pattern.test(entities.processingSteps?.join(" ") || "") + ); + + // Check for conditional processing + const conditionalIndicators = [ + /if.*then/i, + /depending.*on/i, + /based.*on/i, + /when.*also/i, + ]; + + const hasConditional = conditionalIndicators.some((pattern) => + pattern.test(entities.processingSteps?.join(" ") || "") + ); + + if (hasConditional) { + return { + type: "branching", + description: "Conditional execution paths", + parallelExecution: false, + }; + } + + if (hasParallel) { + return { + type: "fork-join", + description: "Nodes execute in parallel then merge", + parallelExecution: true, + }; + } + + return { + type: "linear", + description: "Nodes execute in sequence", + parallelExecution: false, + }; +} + +/** + * Determine topology for mixed workflows + */ +function determineMixedTopology( + complexity: any, + userInput: string +): WorkflowTopology { + if (complexity.level === "high") { + return { + type: "branching", + description: "Complex workflow with multiple execution paths", + parallelExecution: false, + }; + } + + if (complexity.patterns.some((p: string) => p.includes("parallel"))) { + return { + type: "fork-join", + description: "Parallel processing with merge points", + parallelExecution: true, + }; + } + + return { + type: "linear", + description: "Sequential processing pipeline", + parallelExecution: false, + }; +} + +/** + * Generate node sequence based on intent and entities + */ +function generateNodeSequence( + intent: string, + entities: any +): WorkflowNodeStructure[] { + const nodes: WorkflowNodeStructure[] = []; + + // Always start with data input + nodes.push({ + type: "dataInput", + label: "Data Input", + config: { + dataType: determineInputDataType(entities), + defaultValue: generateDefaultValue(entities), + }, + }); + + // Add processing nodes based on intent + if (intent === "WEB_SCRAPING" || entities.urls?.length > 0) { + nodes.push({ + type: "webScraping", + label: "Web Scraper", + config: { + url: entities.urls?.[0] || "{{input.output}}", + formats: ["markdown", "html"], + onlyMainContent: true, + }, + }); + } + + if (intent === "AI_ANALYSIS" || entities.aiTasks?.length > 0) { + nodes.push({ + type: "llmTask", + label: "AI Analyzer", + config: { + prompt: generateAIPrompt(entities), + model: "deepseek-chat", + temperature: 0.7, + }, + }); + } + + if (intent === "DATA_PROCESSING" || entities.dataTypes?.length > 0) { + nodes.push({ + type: "structuredOutput", + label: "Data Processor", + config: { + schema: generateDataSchema(entities), + model: "deepseek-chat", + }, + }); + } + + if (intent === "SEARCH_AND_RETRIEVAL") { + nodes.push({ + type: "similaritySearch", + label: "Similarity Search", + config: { + vectorStore: "pinecone", + topK: 5, + threshold: 0.8, + }, + }); + } + + // Always end with data output + nodes.push({ + type: "dataOutput", + label: "Data Output", + config: { + format: determineOutputFormat(entities), + filename: generateOutputFilename(entities), + }, + }); + + return nodes; +} + +/** + * Generate edges between nodes + */ +function generateEdges( + nodes: WorkflowNodeStructure[], + topology: WorkflowTopology +): WorkflowEdgeStructure[] { + const edges: WorkflowEdgeStructure[] = []; + + if (topology.type === "linear") { + // Simple linear connection + for (let i = 0; i < nodes.length - 1; i++) { + edges.push({ + source: `node-${i}`, + target: `node-${i + 1}`, + }); + } + } else if (topology.type === "fork-join") { + // Fork-join pattern + const processingNodes = nodes.slice(1, -1); + + // Connect input to all processing nodes + processingNodes.forEach((_, index) => { + edges.push({ + source: `node-0`, + target: `node-${index + 1}`, + }); + }); + + // Connect all processing nodes to output + processingNodes.forEach((_, index) => { + edges.push({ + source: `node-${index + 1}`, + target: `node-${nodes.length - 1}`, + }); + }); + } + + return edges; +} + +/** + * Generate edges for mixed workflow topology + */ +function generateEdgesForTopology( + nodes: WorkflowNodeStructure[], + topology: WorkflowTopology +): WorkflowEdgeStructure[] { + return generateEdges(nodes, topology); +} + +/** + * Calculate node positions for visual layout + */ +function calculateNodePositions( + nodes: WorkflowNodeStructure[], + topology: WorkflowTopology +): WorkflowNodeStructure[] { + const positionedNodes = [...nodes]; + const nodeSpacing = 200; + const startX = 100; + const startY = 100; + + if (topology.type === "linear") { + positionedNodes.forEach((node, index) => { + node.position = { + x: startX + index * nodeSpacing, + y: startY, + }; + }); + } else if (topology.type === "fork-join") { + const inputNode = positionedNodes[0]; + const outputNode = positionedNodes[positionedNodes.length - 1]; + const processingNodes = positionedNodes.slice(1, -1); + + // Position input node + inputNode.position = { x: startX, y: startY }; + + // Position processing nodes in parallel + processingNodes.forEach((node, index) => { + node.position = { + x: startX + nodeSpacing, + y: startY + index * nodeSpacing, + }; + }); + + // Position output node + outputNode.position = { + x: startX + 2 * nodeSpacing, + y: startY + ((processingNodes.length - 1) * nodeSpacing) / 2, + }; + } + + return positionedNodes; +} + +/** + * Ensure workflow has input and output nodes + */ +function ensureInputOutputNodes( + nodes: WorkflowNodeStructure[] +): WorkflowNodeStructure[] { + const hasInput = nodes.some((n) => n.type === "dataInput"); + const hasOutput = nodes.some((n) => n.type === "dataOutput"); + + const enhancedNodes = [...nodes]; + + if (!hasInput) { + enhancedNodes.unshift({ + type: "dataInput", + label: "Data Input", + config: { + dataType: "text", + defaultValue: "Enter your data here", + }, + }); + } + + if (!hasOutput) { + enhancedNodes.push({ + type: "dataOutput", + label: "Data Output", + config: { + format: "json", + filename: "output.json", + }, + }); + } + + return enhancedNodes; +} + +/** + * Generate node configuration for specific intent + */ +function generateNodeConfigForIntent( + intent: string, + userInput: string, + index: number +): Record { + const baseConfigs: Record> = { + WEB_SCRAPING: { + url: "{{input.output}}", + formats: ["markdown", "html"], + onlyMainContent: true, + maxLength: 5000, + }, + AI_ANALYSIS: { + prompt: `Analyze the following content: {{input.output}}`, + model: "deepseek-chat", + temperature: 0.7, + maxTokens: 1000, + }, + DATA_PROCESSING: { + schema: '{"type": "object", "properties": {"data": {"type": "string"}}}', + model: "deepseek-chat", + }, + SEARCH_AND_RETRIEVAL: { + vectorStore: "pinecone", + topK: 5, + threshold: 0.8, + }, + CONTENT_GENERATION: { + prompt: `Generate content based on: {{input.output}}`, + model: "deepseek-chat", + temperature: 0.8, + maxTokens: 1500, + }, + }; + + return baseConfigs[intent] || baseConfigs["AI_ANALYSIS"]; +} + +/** + * Determine input data type from entities + */ +function determineInputDataType(entities: any): string { + if (entities.urls?.length > 0) return "url"; + if (entities.dataTypes?.includes("json")) return "json"; + if (entities.dataTypes?.includes("csv")) return "csv"; + if (entities.dataTypes?.includes("pdf")) return "pdf"; + if ( + entities.dataTypes?.includes("resume") || + entities.dataTypes?.includes("cv") + ) + return "text"; + return "text"; +} + +/** + * Generate default value for input + */ +function generateDefaultValue(entities: any): string { + if (entities.urls?.length > 0) return entities.urls[0]; + if (entities.dataTypes?.includes("json")) return '{"data": "example"}'; + if (entities.dataTypes?.includes("csv")) return "name,value\nexample,123"; + if (entities.dataTypes?.includes("pdf")) + return "Upload a PDF file to process"; + if ( + entities.dataTypes?.includes("resume") || + entities.dataTypes?.includes("cv") + ) { + return "John Doe\nSoftware Engineer\n5 years experience in React, Node.js, and Python\nBachelor's in Computer Science\nContact: john.doe@email.com"; + } + return "Enter your text here"; +} + +/** + * Generate AI prompt based on entities + */ +function generateAIPrompt(entities: any): string { + const tasks = entities.aiTasks || []; + const dataTypes = entities.dataTypes || []; + + // Special handling for PDF files + if (dataTypes.includes("pdf")) { + if (tasks.includes("summarize")) { + return "Summarize the PDF document in 2-3 sentences: {{input.output}}"; + } + if (tasks.includes("analyze")) { + return "Analyze the PDF content and provide insights: {{input.output}}"; + } + if (tasks.includes("extract")) { + return "Extract key information from the PDF: {{input.output}}"; + } + return "Process the PDF content: {{input.output}}"; + } + + if (tasks.includes("summarize")) { + return "Summarize the following content in 2-3 sentences: {{input.output}}"; + } + + if (tasks.includes("analyze")) { + return "Analyze the following content and provide insights: {{input.output}}"; + } + + if (tasks.includes("classify")) { + return "Classify the following content into categories: {{input.output}}"; + } + + return "Process the following content: {{input.output}}"; +} + +/** + * Generate data schema based on entities + */ +function generateDataSchema(entities: any): string { + const dataTypes = entities.dataTypes || ["text"]; + + if (dataTypes.includes("json")) { + return '{"type": "object", "properties": {"data": {"type": "string"}}}'; + } + + if (dataTypes.includes("csv")) { + return '{"type": "array", "items": {"type": "object"}}'; + } + + return '{"type": "object", "properties": {"content": {"type": "string"}}}'; +} + +/** + * Determine output format from entities + */ +function determineOutputFormat(entities: any): string { + if (entities.outputFormats?.includes("json")) return "json"; + if (entities.outputFormats?.includes("csv")) return "csv"; + return "json"; +} + +/** + * Generate output filename based on entities + */ +function generateOutputFilename(entities: any): string { + const timestamp = new Date().toISOString().split("T")[0]; + return `workflow_output_${timestamp}.json`; +} + +/** + * Determine complexity level + */ +function determineComplexityLevel(nodeCount: number): string { + if (nodeCount <= 3) return "low"; + if (nodeCount <= 6) return "medium"; + return "high"; +} + +/** + * Estimate execution time + */ +function estimateExecutionTime(nodes: WorkflowNodeStructure[]): number { + const timeEstimates: Record = { + dataInput: 0, + webScraping: 5000, + llmTask: 3000, + structuredOutput: 2000, + similaritySearch: 4000, + dataOutput: 0, + }; + + return nodes.reduce((total, node) => { + return total + (timeEstimates[node.type] || 1000); + }, 0); +} + +/** + * Generate validation rules + */ +function generateValidationRules(nodes: WorkflowNodeStructure[]): string[] { + const rules: string[] = []; + + // Check for required input/output nodes + const hasInput = nodes.some((n) => n.type === "dataInput"); + const hasOutput = nodes.some((n) => n.type === "dataOutput"); + + if (!hasInput) { + rules.push("Workflow needs an input node"); + } + + if (!hasOutput) { + rules.push("Workflow needs an output node"); + } + + // Check for logical flow + const webScrapingNodes = nodes.filter((n) => n.type === "webScraping"); + const llmNodes = nodes.filter((n) => n.type === "llmTask"); + + if (webScrapingNodes.length > 0 && llmNodes.length > 0) { + rules.push("AI analysis nodes should come after web scraping nodes"); + } + + return rules; +} diff --git a/src/utils/workflowValidator.ts b/src/utils/workflowValidator.ts new file mode 100644 index 0000000..65419d1 --- /dev/null +++ b/src/utils/workflowValidator.ts @@ -0,0 +1,530 @@ +import { + WorkflowStructure, + ValidationResult, + MixedValidationResult, + WorkflowNodeStructure, +} from "../types"; + +/** + * Validate generated workflow structure + */ +export function validateWorkflowStructure( + workflow: WorkflowStructure, + originalInput: string +): ValidationResult { + const issues: string[] = []; + const suggestions: string[] = []; + + // Check for required input/output nodes + const hasInput = workflow.nodes.some((n) => n.type === "dataInput"); + const hasOutput = workflow.nodes.some((n) => n.type === "dataOutput"); + + if (!hasInput) { + issues.push("Workflow needs an input node"); + suggestions.push("Add a data input node to start the workflow"); + } + + if (!hasOutput) { + issues.push("Workflow needs an output node"); + suggestions.push("Add a data output node to capture results"); + } + + // Check for logical flow + const webScrapingNodes = workflow.nodes.filter( + (n) => n.type === "webScraping" + ); + const llmNodes = workflow.nodes.filter((n) => n.type === "llmTask"); + + if (webScrapingNodes.length > 0 && llmNodes.length > 0) { + // Check if LLM nodes come after web scraping + const webScrapingPositions = webScrapingNodes.map((n) => + workflow.nodes.indexOf(n) + ); + const llmPositions = llmNodes.map((n) => workflow.nodes.indexOf(n)); + + const allLLMAfterScraping = llmPositions.every((llmPos) => + webScrapingPositions.some((scrapePos) => llmPos > scrapePos) + ); + + if (!allLLMAfterScraping) { + issues.push("AI analysis nodes should come after web scraping nodes"); + suggestions.push( + "Reorder nodes so that data extraction happens before analysis" + ); + } + } + + // Check for proper node connections + const connectionIssues = validateNodeConnections(workflow); + issues.push(...connectionIssues.issues); + suggestions.push(...connectionIssues.suggestions); + + // Check for configuration completeness + const configIssues = validateNodeConfigurations(workflow); + issues.push(...configIssues.issues); + suggestions.push(...configIssues.suggestions); + + // Check for circular dependencies + const circularDeps = detectCircularDependencies(workflow); + if (circularDeps.length > 0) { + issues.push("Circular dependencies detected in workflow"); + suggestions.push("Remove circular connections between nodes"); + } + + return { + isValid: issues.length === 0, + issues, + suggestions, + complexity: workflow.complexity, + estimatedExecutionTime: workflow.estimatedExecutionTime, + }; +} + +/** + * Validate mixed workflow structure + */ +export function validateMixedWorkflow( + workflow: WorkflowStructure, + originalInput: string +): MixedValidationResult { + const baseValidation = validateWorkflowStructure(workflow, originalInput); + + // Additional mixed workflow specific validations + const mixedIssues: string[] = []; + const mixedSuggestions: string[] = []; + + // Check for proper data flow in mixed workflows + const dataFlowIssues = validateDataFlow(workflow); + mixedIssues.push(...dataFlowIssues.issues); + mixedSuggestions.push(...dataFlowIssues.suggestions); + + // Check for resource conflicts + const resourceIssues = validateResourceUsage(workflow); + mixedIssues.push(...resourceIssues.issues); + mixedSuggestions.push(...resourceIssues.suggestions); + + return { + ...baseValidation, + issues: [...baseValidation.issues, ...mixedIssues], + suggestions: [...baseValidation.suggestions, ...mixedSuggestions], + complexity: workflow.complexity, + estimatedExecutionTime: workflow.estimatedExecutionTime || 0, + }; +} + +/** + * Validate node connections + */ +function validateNodeConnections(workflow: WorkflowStructure): { + issues: string[]; + suggestions: string[]; +} { + const issues: string[] = []; + const suggestions: string[] = []; + + // Check if all edges reference existing nodes + const nodeIds = workflow.nodes.map((_, index) => `node-${index}`); + + workflow.edges.forEach((edge, index) => { + if (!nodeIds.includes(edge.source)) { + issues.push( + `Edge ${index} references non-existent source node: ${edge.source}` + ); + } + + if (!nodeIds.includes(edge.target)) { + issues.push( + `Edge ${index} references non-existent target node: ${edge.target}` + ); + } + }); + + // Check for orphaned nodes + const connectedNodes = new Set(); + workflow.edges.forEach((edge) => { + connectedNodes.add(edge.source); + connectedNodes.add(edge.target); + }); + + const orphanedNodes = nodeIds.filter((nodeId) => !connectedNodes.has(nodeId)); + if (orphanedNodes.length > 1) { + // Allow one orphaned node (usually the output) + issues.push(`Orphaned nodes detected: ${orphanedNodes.join(", ")}`); + suggestions.push("Connect all nodes in the workflow"); + } + + return { issues, suggestions }; +} + +/** + * Validate node configurations + */ +function validateNodeConfigurations(workflow: WorkflowStructure): { + issues: string[]; + suggestions: string[]; +} { + const issues: string[] = []; + const suggestions: string[] = []; + + workflow.nodes.forEach((node, index) => { + const nodeIssues = validateSingleNodeConfiguration(node, index); + issues.push(...nodeIssues.issues); + suggestions.push(...nodeIssues.suggestions); + }); + + return { issues, suggestions }; +} + +/** + * Validate single node configuration + */ +function validateSingleNodeConfiguration( + node: WorkflowNodeStructure, + index: number +): { issues: string[]; suggestions: string[] } { + const issues: string[] = []; + const suggestions: string[] = []; + + switch (node.type) { + case "webScraping": + if (!node.config.url) { + issues.push(`Web scraping node ${index} missing URL configuration`); + suggestions.push("Configure URL for web scraping node"); + } + break; + + case "llmTask": + if (!node.config.prompt) { + issues.push(`LLM task node ${index} missing prompt configuration`); + suggestions.push("Configure prompt for LLM task node"); + } + if (!node.config.model) { + issues.push(`LLM task node ${index} missing model configuration`); + suggestions.push("Configure model for LLM task node"); + } + break; + + case "structuredOutput": + if (!node.config.schema) { + issues.push( + `Structured output node ${index} missing schema configuration` + ); + suggestions.push("Configure JSON schema for structured output node"); + } + break; + + case "similaritySearch": + if (!node.config.vectorStore) { + issues.push( + `Similarity search node ${index} missing vector store configuration` + ); + suggestions.push("Configure vector store for similarity search node"); + } + break; + + case "dataInput": + if (!node.config.dataType) { + issues.push(`Data input node ${index} missing data type configuration`); + suggestions.push("Configure data type for input node"); + } + break; + + case "dataOutput": + if (!node.config.format) { + issues.push(`Data output node ${index} missing format configuration`); + suggestions.push("Configure output format for data output node"); + } + break; + } + + return { issues, suggestions }; +} + +/** + * Validate data flow in mixed workflows + */ +function validateDataFlow(workflow: WorkflowStructure): { + issues: string[]; + suggestions: string[]; +} { + const issues: string[] = []; + const suggestions: string[] = []; + + // Check for data type compatibility between connected nodes + const dataTypeCompatibility = checkDataTypeCompatibility(workflow); + issues.push(...dataTypeCompatibility.issues); + suggestions.push(...dataTypeCompatibility.suggestions); + + // Check for variable substitution validity + const variableIssues = validateVariableSubstitution(workflow); + issues.push(...variableIssues.issues); + suggestions.push(...variableIssues.suggestions); + + return { issues, suggestions }; +} + +/** + * Check data type compatibility between nodes + */ +function checkDataTypeCompatibility(workflow: WorkflowStructure): { + issues: string[]; + suggestions: string[]; +} { + const issues: string[] = []; + const suggestions: string[] = []; + + // This is a simplified check - in a real implementation, you'd have more sophisticated type checking + const outputNodes = workflow.nodes.filter((n) => n.type === "dataOutput"); + const inputNodes = workflow.nodes.filter((n) => n.type === "dataInput"); + + if (outputNodes.length === 0) { + issues.push("No output nodes found for data flow"); + suggestions.push("Add data output nodes to complete the workflow"); + } + + if (inputNodes.length === 0) { + issues.push("No input nodes found for data flow"); + suggestions.push("Add data input nodes to start the workflow"); + } + + return { issues, suggestions }; +} + +/** + * Validate variable substitution in configurations + */ +function validateVariableSubstitution(workflow: WorkflowStructure): { + issues: string[]; + suggestions: string[]; +} { + const issues: string[] = []; + const suggestions: string[] = []; + + const variablePattern = /\{\{([^}]+)\}\}/g; + + workflow.nodes.forEach((node, index) => { + Object.entries(node.config).forEach(([key, value]) => { + if (typeof value === "string") { + const matches = value.match(variablePattern); + if (matches) { + matches.forEach((match) => { + const variable = match.slice(2, -2); // Remove {{ and }} + if (!isValidVariableReference(variable, workflow)) { + issues.push( + `Node ${index} has invalid variable reference: ${match}` + ); + suggestions.push( + `Check variable reference ${match} in node configuration` + ); + } + }); + } + } + }); + }); + + return { issues, suggestions }; +} + +/** + * Check if variable reference is valid + */ +function isValidVariableReference( + variable: string, + workflow: WorkflowStructure +): boolean { + // Check if variable references a valid node output + const nodeOutputPattern = /^node-\d+\.output$/; + const simpleOutputPattern = /^input\.output$/; + + return nodeOutputPattern.test(variable) || simpleOutputPattern.test(variable); +} + +/** + * Detect circular dependencies + */ +function detectCircularDependencies(workflow: WorkflowStructure): string[] { + const visited = new Set(); + const recursionStack = new Set(); + const circularDeps: string[] = []; + + const nodeIds = workflow.nodes.map((_, index) => `node-${index}`); + + function hasCycle(nodeId: string): boolean { + if (recursionStack.has(nodeId)) { + return true; + } + + if (visited.has(nodeId)) { + return false; + } + + visited.add(nodeId); + recursionStack.add(nodeId); + + const outgoingEdges = workflow.edges.filter( + (edge) => edge.source === nodeId + ); + for (const edge of outgoingEdges) { + if (hasCycle(edge.target)) { + circularDeps.push(`${nodeId} -> ${edge.target}`); + return true; + } + } + + recursionStack.delete(nodeId); + return false; + } + + for (const nodeId of nodeIds) { + if (!visited.has(nodeId)) { + hasCycle(nodeId); + } + } + + return circularDeps; +} + +/** + * Validate resource usage + */ +function validateResourceUsage(workflow: WorkflowStructure): { + issues: string[]; + suggestions: string[]; +} { + const issues: string[] = []; + const suggestions: string[] = []; + + // Check for excessive API calls + const llmNodes = workflow.nodes.filter((n) => n.type === "llmTask"); + const webScrapingNodes = workflow.nodes.filter( + (n) => n.type === "webScraping" + ); + + if (llmNodes.length > 5) { + issues.push("Too many LLM nodes may cause rate limiting"); + suggestions.push("Consider consolidating LLM operations or adding delays"); + } + + if (webScrapingNodes.length > 3) { + issues.push("Too many web scraping nodes may cause rate limiting"); + suggestions.push("Consider batching web scraping operations"); + } + + // Check for memory-intensive operations + const embeddingNodes = workflow.nodes.filter( + (n) => n.type === "embeddingGenerator" + ); + if (embeddingNodes.length > 2) { + issues.push("Multiple embedding operations may consume significant memory"); + suggestions.push("Consider caching embeddings or reducing batch sizes"); + } + + return { issues, suggestions }; +} + +/** + * Generate improvement suggestions + */ +export function generateImprovementSuggestions( + workflow: WorkflowStructure +): string[] { + const suggestions: string[] = []; + + // Performance suggestions + if (workflow.nodes.length > 8) { + suggestions.push( + "Consider breaking down this workflow into smaller, more manageable parts" + ); + } + + // Error handling suggestions + const hasErrorHandling = workflow.nodes.some( + (n) => n.config.errorHandling || n.config.fallback + ); + + if (!hasErrorHandling && workflow.complexity === "high") { + suggestions.push("Add error handling for complex workflows"); + } + + // Optimization suggestions + const llmNodes = workflow.nodes.filter((n) => n.type === "llmTask"); + if (llmNodes.length > 1) { + suggestions.push( + "Consider using a single LLM node with multiple prompts for better efficiency" + ); + } + + return suggestions; +} + +/** + * Validate workflow against user requirements + */ +export function validateAgainstRequirements( + workflow: WorkflowStructure, + originalInput: string, + requirements: string[] +): ValidationResult { + const baseValidation = validateWorkflowStructure(workflow, originalInput); + const requirementIssues: string[] = []; + const requirementSuggestions: string[] = []; + + // Check if workflow addresses user requirements + requirements.forEach((requirement) => { + if (!workflowAddressesRequirement(workflow, requirement)) { + requirementIssues.push( + `Workflow may not address requirement: ${requirement}` + ); + requirementSuggestions.push( + `Consider adding nodes to handle: ${requirement}` + ); + } + }); + + return { + ...baseValidation, + issues: [...baseValidation.issues, ...requirementIssues], + suggestions: [...baseValidation.suggestions, ...requirementSuggestions], + }; +} + +/** + * Check if workflow addresses a specific requirement + */ +function workflowAddressesRequirement( + workflow: WorkflowStructure, + requirement: string +): boolean { + const requirementLower = requirement.toLowerCase(); + + // Check node types + const nodeTypes = workflow.nodes.map((n) => n.type); + + if ( + requirementLower.includes("scrape") && + !nodeTypes.includes("webScraping") + ) { + return false; + } + + if (requirementLower.includes("analyze") && !nodeTypes.includes("llmTask")) { + return false; + } + + if ( + requirementLower.includes("convert") && + !nodeTypes.includes("structuredOutput") + ) { + return false; + } + + if ( + requirementLower.includes("search") && + !nodeTypes.includes("similaritySearch") + ) { + return false; + } + + return true; +}