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.
This commit is contained in:
Nikhil-Doye
2025-10-18 00:09:18 -04:00
parent 127aab67b0
commit 09ec52aa5a
15 changed files with 3209 additions and 31 deletions
+415
View File
@@ -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<CopilotPanelProps> = ({
isOpen,
onClose,
}) => {
const {
generateWorkflowFromDescription,
getCopilotSuggestions,
validateGeneratedWorkflow,
currentWorkflow,
} = useWorkflowStore();
const [input, setInput] = useState("");
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [validation, setValidation] = useState<ValidationResult | null>(null);
const [showPreview, setShowPreview] = useState(false);
const [suggestions, setSuggestions] = useState<string[]>([]);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl h-[80vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-indigo-50">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-xl flex items-center justify-center">
<Sparkles className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900">
AI Copilot
</h2>
<p className="text-sm text-gray-600">
Describe your workflow in natural language
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setShowPreview(!showPreview)}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title={showPreview ? "Hide preview" : "Show preview"}
>
{showPreview ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
<button
onClick={clearChat}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Clear chat"
>
<RefreshCw className="w-4 h-4" />
</button>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
<div className="flex flex-1 overflow-hidden">
{/* Chat Area */}
<div className="flex-1 flex flex-col">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{messages.length === 0 ? (
<div className="text-center py-12">
<MessageCircle className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Welcome to AI Copilot
</h3>
<p className="text-gray-600 mb-6">
Describe what you want your workflow to do, and I'll help
you build it!
</p>
{/* Quick Suggestions */}
<div className="space-y-2">
<p className="text-sm text-gray-500 mb-3">
Try these examples:
</p>
<div className="flex flex-wrap gap-2 justify-center">
{suggestions.slice(0, 3).map((suggestion, index) => (
<button
key={index}
onClick={() => handleSuggestionClick(suggestion)}
className="px-4 py-2 bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors text-sm"
>
{suggestion}
</button>
))}
</div>
</div>
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.type === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] p-4 rounded-2xl ${
message.type === "user"
? "bg-blue-500 text-white"
: message.type === "system"
? "bg-gray-100 text-gray-700"
: "bg-gray-50 text-gray-900"
}`}
>
<p className="text-sm">{message.content}</p>
{message.data?.validation && (
<div className="mt-3 p-3 bg-white rounded-lg border">
<div className="flex items-center space-x-2 mb-2">
<CheckCircle className="w-4 h-4 text-green-500" />
<span className="text-sm font-medium">
Validation Results
</span>
</div>
<div className="text-xs space-y-1">
<p>
Complexity: {message.data.validation.complexity}
</p>
<p>
Issues: {message.data.validation.issues.length}
</p>
<p>
Suggestions:{" "}
{message.data.validation.suggestions.length}
</p>
</div>
</div>
)}
</div>
</div>
))
)}
{isProcessing && (
<div className="flex justify-start">
<div className="bg-gray-50 p-4 rounded-2xl">
<div className="flex items-center space-x-2">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
<span className="text-sm text-gray-600">
Processing...
</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="p-6 border-t border-gray-200 bg-gray-50">
<form onSubmit={handleSubmit} className="flex space-x-3">
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => 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}
/>
<button
type="submit"
disabled={!input.trim() || isProcessing}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl hover:from-blue-700 hover:to-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
>
{isProcessing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
</button>
</form>
</div>
</div>
{/* Preview Panel */}
{showPreview && (
<div className="w-80 border-l border-gray-200 bg-gray-50 p-6 overflow-y-auto">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Workflow Preview
</h3>
{currentWorkflow ? (
<div className="space-y-4">
<div className="bg-white rounded-lg p-4 border">
<h4 className="font-medium text-gray-900 mb-2">
Current Workflow
</h4>
<div className="text-sm text-gray-600 space-y-1">
<p>Name: {currentWorkflow.name}</p>
<p>Nodes: {currentWorkflow.nodes.length}</p>
<p>Connections: {currentWorkflow.edges.length}</p>
</div>
</div>
{validation && (
<div className="bg-white rounded-lg p-4 border">
<h4 className="font-medium text-gray-900 mb-2">
Validation
</h4>
<div className="text-sm space-y-2">
<div className="flex items-center space-x-2">
{validation.isValid ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : (
<AlertTriangle className="w-4 h-4 text-yellow-500" />
)}
<span
className={
validation.isValid
? "text-green-700"
: "text-yellow-700"
}
>
{validation.isValid ? "Valid" : "Has Issues"}
</span>
</div>
{validation.issues.length > 0 && (
<div>
<p className="text-red-600 font-medium">Issues:</p>
<ul className="text-red-600 text-xs list-disc list-inside">
{validation.issues.map((issue, index) => (
<li key={index}>{issue}</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
<div className="bg-white rounded-lg p-4 border">
<h4 className="font-medium text-gray-900 mb-2">
Suggestions
</h4>
<div className="space-y-2">
{suggestions.map((suggestion, index) => (
<button
key={index}
onClick={() => handleSuggestionClick(suggestion)}
className="w-full text-left p-2 text-sm bg-blue-50 text-blue-700 rounded hover:bg-blue-100 transition-colors"
>
{suggestion}
</button>
))}
</div>
</div>
</div>
) : (
<div className="text-center py-8">
<Lightbulb className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-sm text-gray-600">
No workflow generated yet. Start a conversation to see the
preview!
</p>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
-16
View File
@@ -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)) {
+1 -1
View File
@@ -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",
+17 -4
View File
@@ -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<WorkflowEditorProps> = ({ 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<WorkflowEditorProps> = ({ onClose }) => {
</span>
</button>
<button
onClick={() => setShowCopilot(true)}
className="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-purple-50 to-indigo-50 text-purple-700 rounded-lg hover:from-purple-100 hover:to-indigo-100 transition-colors border border-purple-200"
>
<Sparkles className="w-4 h-4" />
<span className="font-medium">AI Copilot</span>
</button>
{selectedNodeId && (
<button
onClick={handleDeleteSelected}
@@ -558,6 +565,12 @@ export const WorkflowEditor: React.FC<WorkflowEditorProps> = ({ onClose }) => {
onClose={() => setShowConfig(false)}
/>
)}
{/* Copilot Panel */}
<CopilotPanel
isOpen={showCopilot}
onClose={() => setShowCopilot(false)}
/>
</div>
);
};
-5
View File
@@ -15,11 +15,6 @@ import {
Globe,
ArrowRight,
Clock,
MoreVertical,
Copy,
Share2,
Star,
TrendingUp,
HelpCircle,
} from "lucide-react";
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useRef, useState, useEffect } from "react";
import React, { useRef, useState } from "react";
import { useWorkflowStore } from "../store/workflowStore";
import {
Download,
-3
View File
@@ -6,15 +6,12 @@ import {
FileText,
Brain,
Search,
Database,
ArrowDownToLine,
ArrowUpFromLine,
Loader2,
CheckCircle,
XCircle,
Zap,
AlertCircle,
Clock,
} from "lucide-react";
import { clsx } from "clsx";
+137 -1
View File
@@ -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<DataInputNodeProps> = (props) => {
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
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 (
<div className="min-w-[280px] bg-white rounded-2xl border-2 border-blue-200 shadow-lg">
{/* Header */}
<div className="p-4 pb-3 border-b border-gray-200">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl bg-blue-50 border border-blue-200 flex items-center justify-center">
<FileText className="w-5 h-5 text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 text-sm truncate">
{props.data.label}
</h3>
<p className="text-xs text-gray-500">PDF Input</p>
</div>
</div>
</div>
{/* File Upload Area */}
<div className="p-4">
{!uploadedFile ? (
<div
onClick={handleUploadClick}
className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<Upload className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600 mb-1">Click to upload PDF</p>
<p className="text-xs text-gray-500">or drag and drop</p>
<input
ref={fileInputRef}
type="file"
accept=".pdf"
onChange={handleFileUpload}
className="hidden"
/>
</div>
) : (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<FileText className="w-4 h-4 text-green-600" />
<span className="text-sm font-medium text-green-800">
{uploadedFile.name}
</span>
</div>
<button
onClick={handleRemoveFile}
className="text-green-600 hover:text-green-800"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="text-xs text-green-600">
{getPDFInfo(uploadedFile).size}
{isProcessing && " • Processing..."}
</div>
{props.data.outputs.length > 0 && (
<div className="mt-2 text-xs text-green-700">
Text extracted successfully
</div>
)}
</div>
)}
</div>
{/* Bottom Handle */}
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-1/2">
<div className="w-4 h-4 border-2 border-white bg-gray-400 hover:bg-gray-500 transition-colors rounded-full" />
</div>
</div>
);
}
// For other data types, use the standard BaseNode
return <BaseNode {...props} />;
};
+634
View File
@@ -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<string, ParsedIntent>();
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
/**
* Parse natural language input and generate workflow structure
*/
async parseNaturalLanguage(userInput: string): Promise<ParsedIntent> {
// 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<IntentClassification> {
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<EntityExtraction> {
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<WorkflowStructure> {
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<MixedIntentAnalysis> {
// 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<WorkflowStructure> {
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<string[]> {
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();
+174
View File
@@ -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<PDFResponse> => {
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<string> => {
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];
};
+158
View File
@@ -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<void>;
applyCopilotSuggestions: (suggestions: any[]) => void;
validateGeneratedWorkflow: () => ValidationResult | null;
getCopilotSuggestions: (context?: string) => Promise<string[]>;
}
const createEmptyWorkflow = (name: string): Workflow => ({
@@ -470,6 +479,155 @@ export const useWorkflowStore = create<WorkflowStore>((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<string[]> => {
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
+105
View File
@@ -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<string, number>;
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<string, any>;
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;
}
+416
View File
@@ -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<string, number> = {};
// 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<string, string> = {
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<string, string> = {
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;
}
+621
View File
@@ -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<string, any> {
const baseConfigs: Record<string, Record<string, any>> = {
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<string, number> = {
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;
}
+530
View File
@@ -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<string>();
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<string>();
const recursionStack = new Set<string>();
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;
}