mirror of
https://github.com/Nikhil-Doye/workflow-builder.git
synced 2026-07-22 02:01:56 +02:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,11 +15,6 @@ import {
|
||||
Globe,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
MoreVertical,
|
||||
Copy,
|
||||
Share2,
|
||||
Star,
|
||||
TrendingUp,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useWorkflowStore } from "../store/workflowStore";
|
||||
import {
|
||||
Download,
|
||||
|
||||
@@ -6,15 +6,12 @@ import {
|
||||
FileText,
|
||||
Brain,
|
||||
Search,
|
||||
Database,
|
||||
ArrowDownToLine,
|
||||
ArrowUpFromLine,
|
||||
Loader2,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Zap,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user