From dd4add28ea11e101db9ba168423d65cb193f1087 Mon Sep 17 00:00:00 2001 From: Nikhil-Doye Date: Tue, 14 Oct 2025 20:33:53 -0400 Subject: [PATCH] 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. --- .gitignore | 3 +- Workflow_2.json | 99 +++++++++++++ package-lock.json | 74 ++++++---- package.json | 24 ++-- src/components/NodeConfiguration.tsx | 16 ++- src/components/OpenAIConfig.tsx | 174 ++++++++++++++++++++++ src/components/TestingPanel.tsx | 2 +- src/components/WorkflowToolbar.tsx | 28 +++- src/services/openaiService.ts | 94 ++++++++++++ src/store/workflowStore.ts | 206 +++++++++++++++++++++++++-- 10 files changed, 667 insertions(+), 53 deletions(-) create mode 100644 Workflow_2.json create mode 100644 src/components/OpenAIConfig.tsx create mode 100644 src/services/openaiService.ts diff --git a/.gitignore b/.gitignore index b512c09..1dcef2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +.env \ No newline at end of file diff --git a/Workflow_2.json b/Workflow_2.json new file mode 100644 index 0000000..09b6b00 --- /dev/null +++ b/Workflow_2.json @@ -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" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8b95f0e..95f187d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 9f4af3f..9437144 100644 --- a/package.json +++ b/package.json @@ -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": {} + } } diff --git a/src/components/NodeConfiguration.tsx b/src/components/NodeConfiguration.tsx index 42411d2..5bdcb01 100644 --- a/src/components/NodeConfiguration.tsx +++ b/src/components/NodeConfiguration.tsx @@ -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", + ], }, ], }, diff --git a/src/components/OpenAIConfig.tsx b/src/components/OpenAIConfig.tsx new file mode 100644 index 0000000..3652941 --- /dev/null +++ b/src/components/OpenAIConfig.tsx @@ -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 = ({ 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 ( +
+
+
+
+ +

+ OpenAI API Configuration +

+
+ +
+ +
+
+

+ To use real AI responses, you need to configure your OpenAI API + key. Get your API key from{" "} + + OpenAI Platform + +

+
+ +
+ + 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" + /> +
+ + {validationStatus === "error" && ( +
+ + {errorMessage} +
+ )} + + {validationStatus === "success" && ( +
+ + API key is valid! +
+ )} + +
+ + +
+ +
+

• Your API key is stored locally in your browser

+

• It's only used to make requests to OpenAI's API

+

• You can change it anytime by reopening this dialog

+
+
+
+
+ ); +}; diff --git a/src/components/TestingPanel.tsx b/src/components/TestingPanel.tsx index d92b473..9f4bcd4 100644 --- a/src/components/TestingPanel.tsx +++ b/src/components/TestingPanel.tsx @@ -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) => ({ diff --git a/src/components/WorkflowToolbar.tsx b/src/components/WorkflowToolbar.tsx index 7c1c194..f8bb4b5 100644 --- a/src/components/WorkflowToolbar.tsx +++ b/src/components/WorkflowToolbar.tsx @@ -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(null); + const [showOpenAIConfig, setShowOpenAIConfig] = useState(false); const handleSave = () => { if (currentWorkflow) { @@ -101,6 +111,15 @@ export const WorkflowToolbar: React.FC = () => { Import + +
); }; diff --git a/src/services/openaiService.ts b/src/services/openaiService.ts new file mode 100644 index 0000000..2931f8d --- /dev/null +++ b/src/services/openaiService.ts @@ -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 => { + 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", + ]; +}; diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index a74c6ca..a7467a8 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -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; + executeWorkflow: (testInput?: string) => Promise; updateNodeStatus: ( nodeId: string, status: NodeStatus, @@ -212,23 +213,68 @@ export const useWorkflowStore = create((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((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 }; +};