diff --git a/README.md b/README.md index fe4dccd0..13b4b23f 100644 --- a/README.md +++ b/README.md @@ -86,12 +86,14 @@ Visit [http://localhost:3000](http://localhost:3000) to get started. - **Firecrawl**: Scrape URL, Search Web - **GitHub**: Create Issue, List Issues, Get Issue, Update Issue - **Linear**: Create Ticket, Find Issues +- **Mixedbread**: Ingest File - **Perplexity**: Search Web, Ask Question, Research Topic - **Resend**: Send Email - **Slack**: Send Slack Message - **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/index.ts b/plugins/index.ts index 495c6e33..5d58ba06 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -21,6 +21,7 @@ import "./fal"; import "./firecrawl"; import "./github"; import "./linear"; +import "./mixedbread"; import "./perplexity"; import "./resend"; import "./slack"; diff --git a/plugins/mixedbread/credentials.ts b/plugins/mixedbread/credentials.ts new file mode 100644 index 00000000..9bf97633 --- /dev/null +++ b/plugins/mixedbread/credentials.ts @@ -0,0 +1,3 @@ +export type MixedbreadCredentials = { + MIXEDBREAD_API_KEY?: string; +}; diff --git a/plugins/mixedbread/icon.tsx b/plugins/mixedbread/icon.tsx new file mode 100644 index 00000000..4b4f0be7 --- /dev/null +++ b/plugins/mixedbread/icon.tsx @@ -0,0 +1,77 @@ +export function MixedbreadIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/plugins/mixedbread/index.ts b/plugins/mixedbread/index.ts new file mode 100644 index 00000000..c7ad89fe --- /dev/null +++ b/plugins/mixedbread/index.ts @@ -0,0 +1,96 @@ +import type { IntegrationPlugin } from "../registry"; +import { registerIntegration } from "../registry"; +import { MixedbreadIcon } from "./icon"; + +const mixedbreadPlugin: IntegrationPlugin = { + type: "mixedbread", + label: "Mixedbread", + description: "Upload files to Mixedbread document stores", + + icon: MixedbreadIcon, + + formFields: [ + { + id: "mixedbreadApiKey", + label: "API Key", + type: "password", + placeholder: "mxbai-...", + configKey: "mixedbreadApiKey", + envVar: "MIXEDBREAD_API_KEY", + helpText: "Get your API key from ", + helpLink: { + text: "mixedbread.com", + url: "https://www.mixedbread.com/dashboard/api-keys", + }, + }, + ], + + testConfig: { + getTestFunction: async () => { + const { testMixedbread } = await import("./test"); + return testMixedbread; + }, + }, + + actions: [ + { + slug: "ingest-file", + label: "Ingest File", + description: "Upload a file to a Mixedbread document store", + category: "Mixedbread", + stepFunction: "mixedbreadIngestFileStep", + stepImportPath: "ingest-file", + outputFields: [ + { field: "fileId", description: "The ID of the uploaded file" }, + { field: "status", description: "The status of the upload" }, + ], + configFields: [ + { + key: "storeIdentifier", + label: "Store Identifier", + type: "template-input", + placeholder: "my-store or {{NodeName.storeId}}", + example: "my-document-store", + required: true, + }, + { + key: "externalId", + label: "External ID", + type: "template-input", + placeholder: "unique-id or {{NodeName.id}}", + example: "doc-123", + required: true, + }, + { + key: "content", + label: "Content", + type: "template-textarea", + placeholder: "File content or {{NodeName.content}}", + example: "Document content here...", + required: true, + rows: 5, + }, + { + key: "mimetype", + label: "MIME Type", + type: "template-input", + placeholder: "text/plain or {{NodeName.mimetype}}", + example: "text/plain", + defaultValue: "text/plain", + required: true, + }, + { + key: "metadata", + label: "Metadata (JSON)", + type: "template-textarea", + placeholder: '{"key": "value"} or {{NodeName.metadata}}', + example: '{"source": "workflow", "category": "docs"}', + rows: 3, + }, + ], + }, + ], +}; + +registerIntegration(mixedbreadPlugin); +export default mixedbreadPlugin; diff --git a/plugins/mixedbread/steps/ingest-file.ts b/plugins/mixedbread/steps/ingest-file.ts new file mode 100644 index 00000000..281d6116 --- /dev/null +++ b/plugins/mixedbread/steps/ingest-file.ts @@ -0,0 +1,113 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import type { MixedbreadCredentials } from "../credentials"; + +const MIXEDBREAD_API_URL = "https://api.mixedbread.com/v1"; + +type IngestFileResult = + | { success: true; fileId: string; status: string } + | { success: false; error: string }; + +export type MixedbreadIngestFileCoreInput = { + storeIdentifier: string; + externalId: string; + content: string; + mimetype: string; + metadata?: string; +}; + +export type MixedbreadIngestFileInput = StepInput & + MixedbreadIngestFileCoreInput & { + integrationId?: string; + }; + +async function stepHandler( + input: MixedbreadIngestFileCoreInput, + credentials: MixedbreadCredentials +): Promise { + const apiKey = credentials.MIXEDBREAD_API_KEY; + + if (!apiKey) { + return { + success: false, + error: + "MIXEDBREAD_API_KEY is not configured. Please add it in Project Integrations.", + }; + } + + let parsedMetadata: Record | undefined; + if (input.metadata) { + try { + parsedMetadata = JSON.parse(input.metadata); + } catch { + return { + success: false, + error: "Invalid JSON in metadata field", + }; + } + } + + try { + const fileBlob = new Blob([input.content], { type: input.mimetype }); + const formData = new FormData(); + + const params: Record = { + external_id: input.externalId, + }; + if (parsedMetadata) { + params.metadata = parsedMetadata; + } + formData.append("params", JSON.stringify(params)); + formData.append("file", fileBlob, input.externalId); + + const response = await fetch( + `${MIXEDBREAD_API_URL}/stores/${encodeURIComponent(input.storeIdentifier)}/files/upload`, + { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + body: formData, + } + ); + + if (!response.ok) { + const errorText = await response.text(); + return { + success: false, + error: `HTTP ${response.status}: ${errorText}`, + }; + } + + const result = await response.json(); + + return { + success: true, + fileId: result.id, + status: result.status, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + success: false, + error: `Failed to ingest file: ${message}`, + }; + } +} + +export async function mixedbreadIngestFileStep( + input: MixedbreadIngestFileInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +mixedbreadIngestFileStep.maxRetries = 0; + +export const _integrationType = "mixedbread"; diff --git a/plugins/mixedbread/test.ts b/plugins/mixedbread/test.ts new file mode 100644 index 00000000..0875d87e --- /dev/null +++ b/plugins/mixedbread/test.ts @@ -0,0 +1,36 @@ +export async function testMixedbread(credentials: Record) { + try { + const apiKey = credentials.MIXEDBREAD_API_KEY; + + if (!apiKey) { + return { + success: false, + error: "MIXEDBREAD_API_KEY is required", + }; + } + + // Make a lightweight API call to validate the key + const response = await fetch("https://api.mixedbread.com/v1/stores", { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + + if (response.ok) { + return { success: true }; + } + + if (response.status === 401) { + return { success: false, error: "Invalid API key" }; + } + + const error = await response.text(); + return { success: false, error: `API error: ${error}` }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +}