Update project configuration and enhance workflow execution

- Added .env to .gitignore to prevent sensitive data exposure.
- Updated package.json and package-lock.json to include OpenAI SDK version 6.3.0.
- Enhanced NodeConfiguration component to support additional model options for AI tasks.
- Modified TestingPanel to accept test input for workflow execution.
- Improved WorkflowToolbar with OpenAI configuration button and modal.
- Refactored workflow execution logic to handle test inputs and added processing functions for various node types.
This commit is contained in:
Nikhil-Doye
2025-10-14 20:33:53 -04:00
parent 863389e3e5
commit dd4add28ea
10 changed files with 667 additions and 53 deletions
+2 -1
View File
@@ -1 +1,2 @@
node_modules
node_modules
.env
+99
View File
@@ -0,0 +1,99 @@
{
"id": "c69ca19c-4174-4ec5-b2f8-6f941d1cd796",
"name": "Workflow 1760487469928",
"nodes": [
{
"id": "57861f51-a1ed-4efe-8646-3564de2cac10",
"type": "dataInput",
"position": {
"x": 372,
"y": 87
},
"data": {
"id": "960e2857-f602-46a9-af43-ec932d77153e",
"type": "dataInput",
"label": "dataInput Node",
"status": "success",
"config": {
"defaultValue": "Hello World!",
"dataType": "text"
},
"inputs": [],
"outputs": [
{
"output": "What is today's date?"
}
]
}
},
{
"id": "d11a7455-c591-4606-9e7f-11b002e8c20d",
"type": "llmTask",
"position": {
"x": 436.75,
"y": 118.75
},
"data": {
"id": "d52f145f-1fa6-415c-aca7-ca11895f3991",
"type": "llmTask",
"label": "llmTask Node",
"status": "success",
"config": {
"model": "gpt-3.5-turbo",
"temperature": 0.7,
"prompt": "Analyze the following text and provide insights: {{input}}"
},
"inputs": [],
"outputs": [
{
"output": "LLM Response (gpt-3.5-turbo): Analyze the following text and provide insights: What is today's date?",
"prompt": "Analyze the following text and provide insights: What is today's date?",
"model": "gpt-3.5-turbo"
}
]
}
},
{
"id": "40d139ed-8290-4d14-8c1a-940c2927105a",
"type": "dataOutput",
"position": {
"x": 504.5575564762356,
"y": 309.2556953849361
},
"data": {
"id": "9b38825f-ae74-491f-b088-36969d87385d",
"type": "dataOutput",
"label": "dataOutput Node",
"status": "success",
"config": {
"format": "text",
"filename": "analysis.txt"
},
"inputs": [],
"outputs": [
{
"output": "What is today's date?",
"format": "text",
"filename": "analysis.txt"
}
]
}
}
],
"edges": [
{
"id": "0e841dba-cdb5-4738-8311-2ec1c426c0a5",
"source": "57861f51-a1ed-4efe-8646-3564de2cac10",
"target": "d11a7455-c591-4606-9e7f-11b002e8c20d"
},
{
"id": "5447100b-4949-406f-a3e8-9f8dd336eeef",
"source": "d11a7455-c591-4606-9e7f-11b002e8c20d",
"target": "40d139ed-8290-4d14-8c1a-940c2927105a"
}
],
"createdAt": "2025-10-15T00:17:49.928Z",
"updatedAt": "2025-10-15T00:20:17.205Z",
"exportedAt": "2025-10-15T00:22:18.078Z",
"version": "1.0.0"
}
+48 -26
View File
@@ -15,6 +15,7 @@
"autoprefixer": "^10.4.14",
"clsx": "^1.2.1",
"lucide-react": "^0.263.1",
"openai": "^6.3.0",
"postcss": "^8.4.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -11318,6 +11319,27 @@
}
}
},
"node_modules/jsdom/node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -12259,6 +12281,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openai": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.3.0.tgz",
"integrity": "sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -17212,27 +17255,6 @@
}
}
},
"node_modules/webpack-dev-server/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/webpack-manifest-plugin": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz",
@@ -17859,16 +17881,16 @@
}
},
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
+12 -12
View File
@@ -7,20 +7,21 @@
"@types/node": "^16.18.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/uuid": "^9.0.2",
"autoprefixer": "^10.4.14",
"clsx": "^1.2.1",
"lucide-react": "^0.263.1",
"openai": "^6.3.0",
"postcss": "^8.4.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"reactflow": "^11.10.1",
"react-scripts": "5.0.1",
"typescript": "^4.9.0",
"lucide-react": "^0.263.1",
"tailwindcss": "^3.3.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
"clsx": "^1.2.1",
"react-hot-toast": "^2.4.1",
"zustand": "^4.4.1",
"react-scripts": "5.0.1",
"reactflow": "^11.10.1",
"tailwindcss": "^3.3.0",
"typescript": "^4.9.0",
"uuid": "^9.0.0",
"@types/uuid": "^9.0.2"
"zustand": "^4.4.1"
},
"scripts": {
"start": "react-scripts start",
@@ -45,6 +46,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {}
}
}
+14 -2
View File
@@ -46,7 +46,13 @@ const nodeTypeConfigs = {
key: "model",
label: "Model",
type: "select",
options: ["gpt-3.5-turbo", "gpt-4", "claude-3"],
options: [
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4",
"gpt-4-turbo-preview",
"gpt-4-32k",
],
},
{
key: "temperature",
@@ -114,7 +120,13 @@ const nodeTypeConfigs = {
key: "model",
label: "Model",
type: "select",
options: ["gpt-3.5-turbo", "gpt-4", "claude-3"],
options: [
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4",
"gpt-4-turbo-preview",
"gpt-4-32k",
],
},
],
},
+174
View File
@@ -0,0 +1,174 @@
import React, { useState } from "react";
import { Key, AlertCircle, CheckCircle } from "lucide-react";
interface OpenAIConfigProps {
onClose: () => void;
}
export const OpenAIConfig: React.FC<OpenAIConfigProps> = ({ onClose }) => {
const [apiKey, setApiKey] = useState("");
const [isValidating, setIsValidating] = useState(false);
const [validationStatus, setValidationStatus] = useState<
"idle" | "success" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState("");
const handleSave = async () => {
if (!apiKey.trim()) {
setErrorMessage("Please enter an API key");
setValidationStatus("error");
return;
}
setIsValidating(true);
setValidationStatus("idle");
try {
// In a real app, you'd validate the API key with a backend service
// For now, we'll just check if it looks like a valid OpenAI API key format
if (apiKey.startsWith("sk-") && apiKey.length > 20) {
// Save to localStorage for this session
localStorage.setItem("openai_api_key", apiKey);
setValidationStatus("success");
setTimeout(() => {
onClose();
window.location.reload(); // Reload to pick up the new API key
}, 1500);
} else {
setErrorMessage(
"Invalid API key format. OpenAI API keys should start with 'sk-'"
);
setValidationStatus("error");
}
} catch (error) {
setErrorMessage("Failed to validate API key");
setValidationStatus("error");
} finally {
setIsValidating(false);
}
};
const handleTest = async () => {
if (!apiKey.trim()) {
setErrorMessage("Please enter an API key first");
setValidationStatus("error");
return;
}
setIsValidating(true);
setValidationStatus("idle");
try {
// Test the API key by making a simple call
const response = await fetch("https://api.openai.com/v1/models", {
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
setValidationStatus("success");
setErrorMessage("");
} else {
setErrorMessage("API key is invalid or has insufficient permissions");
setValidationStatus("error");
}
} catch (error) {
setErrorMessage(
"Failed to test API key. Please check your internet connection."
);
setValidationStatus("error");
} finally {
setIsValidating(false);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[80vh] overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<div className="flex items-center space-x-2">
<Key className="w-5 h-5 text-gray-600" />
<h3 className="text-lg font-semibold text-gray-900">
OpenAI API Configuration
</h3>
</div>
<button
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
×
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
<p className="text-sm text-blue-800">
To use real AI responses, you need to configure your OpenAI API
key. Get your API key from{" "}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800"
>
OpenAI Platform
</a>
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
OpenAI API Key
</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-..."
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
{validationStatus === "error" && (
<div className="flex items-center space-x-2 text-red-600">
<AlertCircle className="w-4 h-4" />
<span className="text-sm">{errorMessage}</span>
</div>
)}
{validationStatus === "success" && (
<div className="flex items-center space-x-2 text-green-600">
<CheckCircle className="w-4 h-4" />
<span className="text-sm">API key is valid!</span>
</div>
)}
<div className="flex space-x-2">
<button
onClick={handleTest}
disabled={isValidating || !apiKey.trim()}
className="flex-1 px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isValidating ? "Testing..." : "Test Key"}
</button>
<button
onClick={handleSave}
disabled={isValidating || !apiKey.trim()}
className="flex-1 px-3 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isValidating ? "Saving..." : "Save & Use"}
</button>
</div>
<div className="text-xs text-gray-500">
<p> Your API key is stored locally in your browser</p>
<p> It's only used to make requests to OpenAI's API</p>
<p> You can change it anytime by reopening this dialog</p>
</div>
</div>
</div>
</div>
);
};
+1 -1
View File
@@ -35,7 +35,7 @@ export const TestingPanel: React.FC = () => {
if (!currentWorkflow) return;
setTestResults(null);
await executeWorkflow();
await executeWorkflow(testInput);
// Collect all results
const results = currentWorkflow.nodes.map((node) => ({
+26 -2
View File
@@ -1,10 +1,19 @@
import React, { useRef } from "react";
import React, { useRef, useState } from "react";
import { useWorkflowStore } from "../store/workflowStore";
import { Download, Upload, Save, Play, Square, RotateCcw } from "lucide-react";
import {
Download,
Upload,
Save,
Play,
Square,
RotateCcw,
Settings,
} from "lucide-react";
import {
downloadWorkflow,
loadWorkflowFromFile,
} from "../utils/workflowSerialization";
import { OpenAIConfig } from "./OpenAIConfig";
export const WorkflowToolbar: React.FC = () => {
const {
@@ -16,6 +25,7 @@ export const WorkflowToolbar: React.FC = () => {
} = useWorkflowStore();
const fileInputRef = useRef<HTMLInputElement>(null);
const [showOpenAIConfig, setShowOpenAIConfig] = useState(false);
const handleSave = () => {
if (currentWorkflow) {
@@ -101,6 +111,15 @@ export const WorkflowToolbar: React.FC = () => {
<span>Import</span>
</button>
<button
onClick={() => setShowOpenAIConfig(true)}
className="flex items-center space-x-2 px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors"
title="Configure OpenAI API"
>
<Settings className="w-4 h-4" />
<span>AI Config</span>
</button>
<div className="w-px h-6 bg-gray-300" />
<button
@@ -139,6 +158,11 @@ export const WorkflowToolbar: React.FC = () => {
onChange={handleFileChange}
className="hidden"
/>
{/* OpenAI Configuration Modal */}
{showOpenAIConfig && (
<OpenAIConfig onClose={() => setShowOpenAIConfig(false)} />
)}
</div>
);
};
+94
View File
@@ -0,0 +1,94 @@
import OpenAI from "openai";
// Get API key from environment or localStorage
const getApiKey = (): string => {
return (
localStorage.getItem("openai_api_key") ||
process.env.REACT_APP_OPENAI_API_KEY ||
""
);
};
// Initialize OpenAI client
const openai = new OpenAI({
apiKey: getApiKey(),
dangerouslyAllowBrowser: true, // Only for development - in production, use a backend
});
export interface OpenAIConfig {
model: string;
temperature: number;
maxTokens?: number;
}
export interface OpenAIResponse {
content: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}
export const callOpenAI = async (
prompt: string,
config: OpenAIConfig
): Promise<OpenAIResponse> => {
try {
// Check if API key is configured
const apiKey = getApiKey();
if (!apiKey || apiKey === "your_openai_api_key_here") {
throw new Error(
"OpenAI API key not configured. Please configure your API key in the settings."
);
}
// Update the OpenAI client with the current API key
openai.apiKey = apiKey;
const response = await openai.chat.completions.create({
model: config.model,
messages: [
{
role: "user",
content: prompt,
},
],
temperature: config.temperature,
max_tokens: config.maxTokens || 1000,
});
const content = response.choices[0]?.message?.content || "";
const usage = response.usage;
return {
content,
usage: usage
? {
promptTokens: usage.prompt_tokens,
completionTokens: usage.completion_tokens,
totalTokens: usage.total_tokens,
}
: undefined,
};
} catch (error) {
console.error("OpenAI API Error:", error);
// Return a fallback response if API fails
return {
content: `Error calling OpenAI API: ${
error instanceof Error ? error.message : "Unknown error"
}. Please check your API key and try again.`,
};
}
};
export const getAvailableModels = (): string[] => {
return [
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4",
"gpt-4-turbo-preview",
"gpt-4-32k",
];
};
+197 -9
View File
@@ -7,6 +7,7 @@ import {
NodeStatus,
} from "../types";
import { v4 as uuidv4 } from "uuid";
import { callOpenAI, OpenAIConfig } from "../services/openaiService";
interface WorkflowStore {
workflows: Workflow[];
@@ -37,7 +38,7 @@ interface WorkflowStore {
deleteEdge: (edgeId: string) => void;
// Execution
executeWorkflow: () => Promise<void>;
executeWorkflow: (testInput?: string) => Promise<void>;
updateNodeStatus: (
nodeId: string,
status: NodeStatus,
@@ -212,23 +213,68 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
}));
},
executeWorkflow: async () => {
executeWorkflow: async (testInput?: string) => {
const { currentWorkflow } = get();
if (!currentWorkflow) return;
set({ isExecuting: true, executionResults: {} });
try {
// Simple execution logic - in a real app, this would call your backend
for (const node of currentWorkflow.nodes) {
// Find the first data input node to start with
const dataInputNode = currentWorkflow.nodes.find(
(node) => node.data.type === "dataInput"
);
if (!dataInputNode) {
console.error("No data input node found in workflow");
return;
}
// Start with the data input node
get().updateNodeStatus(dataInputNode.id, "running");
await new Promise((resolve) => setTimeout(resolve, 500));
// Use test input if provided, otherwise use the node's default value
const inputData =
testInput || dataInputNode.data.config.defaultValue || "";
const dataInputResult = { output: inputData };
get().updateNodeStatus(dataInputNode.id, "success", dataInputResult);
// Process remaining nodes in order
const remainingNodes = currentWorkflow.nodes.filter(
(node) => node.id !== dataInputNode.id
);
for (const node of remainingNodes) {
get().updateNodeStatus(node.id, "running");
await new Promise((resolve) => setTimeout(resolve, 500));
// Simulate processing time
await new Promise((resolve) => setTimeout(resolve, 1000));
// Process node based on type
let result;
switch (node.data.type) {
case "llmTask":
result = await processLLMNode(node, inputData);
break;
case "dataOutput":
result = await processDataOutputNode(node, inputData);
break;
case "webScraping":
result = await processWebScrapingNode(node, inputData);
break;
case "embeddingGenerator":
result = await processEmbeddingNode(node, inputData);
break;
case "similaritySearch":
result = await processSimilaritySearchNode(node, inputData);
break;
case "structuredOutput":
result = await processStructuredOutputNode(node, inputData);
break;
default:
result = { output: `Processed by ${node.data.type} node` };
}
// Mock execution result
const mockResult = { output: `Processed by ${node.data.type} node` };
get().updateNodeStatus(node.id, "success", mockResult);
get().updateNodeStatus(node.id, "success", result);
}
} catch (error) {
console.error("Workflow execution failed:", error);
@@ -261,3 +307,145 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
set({ executionResults: {}, isExecuting: false });
},
}));
// Node processing functions
const processLLMNode = async (node: WorkflowNode, inputData: string) => {
const config = node.data.config;
const prompt = config.prompt || "Process the following input: {{input}}";
const model = config.model || "gpt-3.5-turbo";
const temperature = config.temperature || 0.7;
const maxTokens = config.maxTokens || 1000;
// Replace {{input}} placeholder with actual input data
const processedPrompt = prompt.replace(/\{\{input\}\}/g, inputData);
try {
// Call OpenAI API
const openaiConfig: OpenAIConfig = {
model,
temperature,
maxTokens,
};
const response = await callOpenAI(processedPrompt, openaiConfig);
return {
output: response.content,
prompt: processedPrompt,
model,
temperature,
maxTokens,
usage: response.usage,
};
} catch (error) {
console.error("Error processing LLM node:", error);
// Fallback to mock response if API fails
const fallbackResponse = `Error calling OpenAI API: ${
error instanceof Error ? error.message : "Unknown error"
}. Please check your API key and try again.`;
return {
output: fallbackResponse,
prompt: processedPrompt,
model,
temperature,
maxTokens,
error: error instanceof Error ? error.message : "Unknown error",
};
}
};
const processDataOutputNode = async (node: WorkflowNode, inputData: string) => {
const config = node.data.config;
const format = config.format || "text";
const filename = config.filename || "output.txt";
let output;
switch (format) {
case "json":
output = JSON.stringify(
{ data: inputData, timestamp: new Date().toISOString() },
null,
2
);
break;
case "csv":
output = `data\n"${inputData}"`;
break;
default:
output = inputData;
}
return { output, format, filename };
};
const processWebScrapingNode = async (
node: WorkflowNode,
inputData: string
) => {
const config = node.data.config;
const url = config.url || inputData;
const selector = config.selector || "body";
const maxLength = config.maxLength || 1000;
// In a real app, this would perform actual web scraping
const mockScrapedContent = `Scraped content from ${url} using selector "${selector}": ${inputData.substring(
0,
maxLength
)}...`;
return { output: mockScrapedContent, url, selector };
};
const processEmbeddingNode = async (node: WorkflowNode, inputData: string) => {
const config = node.data.config;
const model = config.model || "text-embedding-ada-002";
const dimensions = config.dimensions || 1536;
// In a real app, this would generate actual embeddings
const mockEmbedding = Array.from({ length: dimensions }, () => Math.random());
return { output: mockEmbedding, model, dimensions };
};
const processSimilaritySearchNode = async (
node: WorkflowNode,
inputData: string
) => {
const config = node.data.config;
const vectorStore = config.vectorStore || "pinecone";
const topK = config.topK || 5;
const threshold = config.threshold || 0.8;
// In a real app, this would perform actual similarity search
const mockResults = Array.from({ length: topK }, (_, i) => ({
id: `result_${i + 1}`,
content: `Similar content ${i + 1} for: ${inputData}`,
similarity: threshold + Math.random() * (1 - threshold),
}));
return { output: mockResults, vectorStore, topK, threshold };
};
const processStructuredOutputNode = async (
node: WorkflowNode,
inputData: string
) => {
const config = node.data.config;
const schema = config.schema || '{"type": "object"}';
const model = config.model || "gpt-3.5-turbo";
// In a real app, this would use an LLM to structure the data according to the schema
const mockStructuredOutput = {
input: inputData,
structured: {
text: inputData,
length: inputData.length,
timestamp: new Date().toISOString(),
},
schema: JSON.parse(schema),
};
return { output: mockStructuredOutput, model, schema };
};