From a8e0b737523735128e3f32997f0c8d516912ee9d Mon Sep 17 00:00:00 2001 From: ishaksebsib Date: Mon, 8 Dec 2025 14:49:41 +0300 Subject: [PATCH] feat(ai-gateway): add generate embeddings Action --- README.md | 3 +- plugins/ai-gateway/index.ts | 71 ++++++++- .../ai-gateway/steps/generate-embeddings.ts | 144 ++++++++++++++++++ 3 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 plugins/ai-gateway/steps/generate-embeddings.ts diff --git a/README.md b/README.md index fe4dccd0..2b297e85 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started. ### Action Nodes -- **AI Gateway**: Generate Text, Generate Image +- **AI Gateway**: Generate Text, Generate Image, Generate Embeddings - **Blob**: Put Blob, List Blobs - **Clerk**: Get User, Create User, Update User, Delete User - **fal.ai**: Generate Image, Generate Video, Upscale Image, Remove Background, Image to Image @@ -92,6 +92,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started. - **Stripe**: Create Customer, Get Customer, Create Invoice - **Superagent**: Guard, Redact - **v0**: Create Chat, Send Message +- **Webflow**: List Sites, Get Site, Publish Site ## Code Generation diff --git a/plugins/ai-gateway/index.ts b/plugins/ai-gateway/index.ts index d74c55c1..2478c8ad 100644 --- a/plugins/ai-gateway/index.ts +++ b/plugins/ai-gateway/index.ts @@ -5,7 +5,7 @@ import { AiGatewayIcon } from "./icon"; const aiGatewayPlugin: IntegrationPlugin = { type: "ai-gateway", label: "AI Gateway", - description: "Generate text and images using AI models", + description: "Generate text, images, and embeddings using AI models", icon: AiGatewayIcon, @@ -141,6 +141,75 @@ const aiGatewayPlugin: IntegrationPlugin = { }, ], }, + { + slug: "generate-embeddings", + label: "Generate Embeddings", + description: "Generate text embeddings using AI models", + category: "AI Gateway", + stepFunction: "generateEmbeddingsStep", + stepImportPath: "generate-embeddings", + outputFields: [ + { field: "embedding", description: "Single embedding vector" }, + { field: "embeddings", description: "Batch embedding vectors" }, + ], + configFields: [ + { + key: "embeddingMode", + label: "Embedding Mode", + type: "select", + defaultValue: "single", + options: [ + { value: "single", label: "Single Value" }, + { value: "batch", label: "Batch Values" }, + ], + required: true, + }, + { + key: "embeddingModel", + label: "Model", + type: "select", + defaultValue: "openai/text-embedding-3-small", + options: [ + { value: "amazon/titan-embed-text-v2", label: "Amazon Titan Embed Text V2"}, + { value: "cohere/embed-v4.0", label: "Cohere Embed V4.0"}, + { value: "google/gemini-embedding-001", label: "Gemini Embedding 001"}, + { value: "google/text-embedding-005", label: "Google Text Embedding 005" }, + { value: "google/text-multilingual-embedding-002", label: "Google Text Multilingual Embedding 002" }, + { value: "mistral/codestral-embed", label: "Mistral Codestral Embed" }, + { value: "mistral/mistral-embed", label: "Mistral Embed" }, + { value: "openai/text-embedding-3-large", label: "OpenAI Text Embedding 3 Large" }, + { value: "openai/text-embedding-3-small", label: "OpenAI Text Embedding 3 Small" }, + { value: "openai/text-embedding-ada-002", label: "OpenAI Text Embedding Ada 002" }, + { value: "voyage/voyage-3-large", label: "Voyage 3 Large" }, + { value: "voyage/voyage-3.5", label: "Voyage 3.5" }, + { value: "voyage/voyage-3.5-lite", label: "Voyage 3.5 Lite" }, + { value: "voyage/voyage-code-3", label: "Voyage Code 3" }, + ], + required: true, + }, + { + key: "embeddingValue", + label: "Text to Embed", + type: "template-input", + placeholder: "Enter text to embed. Use {{NodeName.field}} to reference previous outputs.", + example: "sunny day at the beach", + showWhen: { field: "embeddingMode", equals: "single" }, + required: true, + }, + { + key: "embeddingValues", + label: "Texts to Embed", + type: "template-textarea", + placeholder: + "Enter one text per line for batch embedding:\n- First text\n- Second text\nUse {{NodeName.field}} to reference previous outputs.", + example: + "sunny day at the beach\nrainy afternoon in the city\nsnowy night in the mountains", + rows: 6, + showWhen: { field: "embeddingMode", equals: "batch" }, + required: true, + } + ], + }, ], }; diff --git a/plugins/ai-gateway/steps/generate-embeddings.ts b/plugins/ai-gateway/steps/generate-embeddings.ts new file mode 100644 index 00000000..ef904e73 --- /dev/null +++ b/plugins/ai-gateway/steps/generate-embeddings.ts @@ -0,0 +1,144 @@ +import "server-only"; + +import { createGateway, embed, embedMany } from "ai"; +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import { getErrorMessageAsync } from "@/lib/utils"; +import type { AiGatewayCredentials } from "../credentials"; + +type GenerateEmbeddingsResult = + | { success: true; embedding: number[]; } + | { + success: true; + embeddings: number[][]; + } + | { success: false; error: string }; + +export type GenerateEmbeddingsCoreInput = { + embeddingMode: "single" | "batch"; + embeddingModel: string; + embeddingValue?: string; + embeddingValues?: string; +}; + +export type GenerateEmbeddingsInput = StepInput & + GenerateEmbeddingsCoreInput & { + integrationId?: string; + }; + + +/** + * Parse batch embedding values from textarea input (one per line) + */ +function parseEmbeddingValues(valuesText?: string): string[] { + if (!valuesText) { + return []; + } + + return valuesText + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); +} + +/** + * Core logic - portable between app and export + */ +async function stepHandler( + input: GenerateEmbeddingsCoreInput, + credentials: AiGatewayCredentials, +): Promise { + const apiKey = credentials.AI_GATEWAY_API_KEY; + + if (!apiKey) { + return { + success: false, + error: + "AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations.", + }; + } + + const mode = input.embeddingMode || "single"; + const modelId = input.embeddingModel || "openai/text-embedding-3-small"; + + try { + const gateway = createGateway({ + apiKey, + }); + + // Handle single embedding mode + if (mode === "single") { + const value = input.embeddingValue?.trim() || ""; + + if (!value) { + return { + success: false, + error: "Embedding value is required for single mode", + }; + } + + const { embedding } = await embed({ + model: gateway.textEmbeddingModel(modelId), + value, + }); + + return { + success: true, + embedding, + }; + } + + // Handle batch embedding mode + if (mode === "batch") { + const valuesText = input.embeddingValues?.trim() || ""; + const values = parseEmbeddingValues(valuesText); + + if (values.length === 0) { + return { + success: false, + error: + "At least one embedding value is required for batch mode (one per line)", + }; + } + + const { embeddings } = await embedMany({ + model: gateway.textEmbeddingModel(modelId), + values, + }); + + return { + success: true, + embeddings, + }; + } + + return { + success: false, + error: `Invalid embedding mode: ${mode}`, + }; + } catch (error) { + const message = await getErrorMessageAsync(error); + return { + success: false, + error: `Embedding generation failed: ${message}`, + }; + } +} + +/** + * App entry point - fetches credentials and wraps with logging + */ +export async function generateEmbeddingsStep( + input: GenerateEmbeddingsInput, +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +generateEmbeddingsStep.maxRetries = 0; + +export const _integrationType = "ai-gateway";