Multimodal AI Guide¶
Build agents that see, hear, and understand. Helix agents and pipelines can process text, images, audio, and video natively. This guide covers how each data type flows through the platform — from ingestion to LLM processing to tool responses — enabling vision agents, audio transcription, multimodal RAG, and rich content analysis.
Table of Contents¶
- Architecture Overview
- Content Types
- ContentPart
- Supported Media Types
- Agents with Multimodal Input
- Text + Image (Vision)
- Arbitrary Multimodal Parts
- Base64-Encoded Images
- Audio Input
- Provider-Specific Formats
- OpenAI (GPT-4o, GPT-4o-mini)
- Anthropic (Claude Sonnet/Opus)
- Vertex / Gemini
- Ollama
- OpenAI-Compatible Providers
- Multimodal Exchanges
- Setting Multimodal Body
- Multimodal Exchange Headers
- HTTP Workflow with Image Processing
- RAG with Multimodal Documents
- Image Ingestion
- Audio Ingestion
- Multimodal Documents
- Multimodal Embeddings (CLIP)
- Multimodal Tools
- Complete Examples
- Image Q&A API
- Multi-Provider Vision Comparison
- Image + Document RAG
- Audio Transcription Pipeline
- Interactive Multimodal Input
- Voice Input (Speech-to-Text)
- File Upload (Images, PDF, CSV, Text)
- PDF OCR via Docling
- Auto-Prompt for Multimodal Input
- Base64 Image Stripping for Large OCR Output
- Content Part Reference
- Constructors
- Agent Methods
- Message Helpers
- Exchange Helpers
- Provider Vision Model Support
Architecture Overview¶
Multimodal data flows through three layers:
Input (HTTP, file, exchange)
↓
Content Parts (LLM messages / Exchange body)
↓
Provider-specific format (OpenAI vision, Anthropic image, Gemini inlineData)
The key abstraction is ContentPart -- a union type that carries text, images, audio, or video alongside metadata (MIME type, URL, binary data). Providers automatically convert ContentParts to their native multimodal format.
Content Types¶
ContentPart¶
Every piece of multimodal content is represented as a ContentPart:
| Field | Type | Description |
|---|---|---|
Type |
ContentType |
text, image, audio, video, file |
Text |
string |
Text content (for text type) |
MIMEType |
string |
Media type (e.g., image/png, audio/wav) |
Data |
[]byte |
Raw binary data (base64-decoded) |
URL |
string |
URL reference (image URL, audio URL) |
Detail |
string |
Resolution hint: low, high, auto (images only) |
Supported Media Types¶
| Content Type | Supported Formats | Providers |
|---|---|---|
| Text | Any text | All providers |
| Image | JPEG, PNG, GIF, WebP, BMP, SVG | OpenAI, Anthropic, Vertex/Gemini, Ollama |
| Audio | WAV, MP3, OGG, FLAC, M4A | OpenAI (gpt-4o-audio), Gemini |
| Video | MP4, WebM | Gemini |
| File | Any binary | Via tool/exchange passthrough |
Agents with Multimodal Input¶
Text + Image (Vision)¶
The most common multimodal pattern -- send an image with a text prompt:
agents:
- id: image-analyzer
type: react
trigger: http-server://0.0.0.0:8080/api/analyze
model:
provider: openai
model: gpt-4o
system_prompt: "You are an image analysis assistant."
From a Go client, use RunWithImage:
agent := adk.New(adk.AgentConfig{
Type: adk.TypeReAct,
Model: model, // OpenAI gpt-4o, Anthropic Claude, Gemini, etc.
SystemPrompt: "You are an image analysis assistant.",
})
// Send text + image URL
result, err := agent.RunWithImage(ctx,
"What objects are in this image?",
"https://example.com/photo.jpg",
)
fmt.Println(result.Content) // "The image shows a cat sitting on a windowsill..."
Arbitrary Multimodal Parts¶
For complex inputs with multiple images, audio, or mixed media:
result, err := agent.RunWithParts(ctx, []llm.ContentPart{
llm.TextPart("Compare these two images:"),
llm.ImageURLPart("https://example.com/before.jpg", "high"),
llm.ImageURLPart("https://example.com/after.jpg", "high"),
})
Base64-Encoded Images¶
For images from file uploads or binary sources:
imageData, _ := os.ReadFile("photo.png")
result, err := agent.RunWithParts(ctx, []llm.ContentPart{
llm.TextPart("Describe this photo"),
llm.ImageDataPart("image/png", imageData),
})
Audio Input¶
For models that support audio (OpenAI gpt-4o-audio, Gemini):
audioData, _ := os.ReadFile("recording.wav")
result, err := agent.RunWithParts(ctx, []llm.ContentPart{
llm.TextPart("Transcribe this audio"),
llm.AudioDataPart("audio/wav", audioData),
})
Provider-Specific Formats¶
Each provider receives ContentParts in its native format. This is automatic -- you write ContentParts once and they work across all vision-capable providers.
OpenAI (GPT-4o, GPT-4o-mini)¶
ContentParts are converted to OpenAI's content array format:
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,...", "detail": "high"}}
]
}
Audio inputs use input_audio blocks:
Anthropic (Claude Sonnet/Opus)¶
Images use Anthropic's source format:
URL images are fetched and sent as base64 (Anthropic requires base64).
Vertex / Gemini¶
Images use inline data:
Gemini natively supports images, audio, and video in inline data blocks.
Ollama¶
Images use Ollama's native images field (array of base64 strings). Models like llava, bakllava, and moondream support vision.
OpenAI-Compatible Providers¶
Groq, Mistral, Together, Fireworks, DeepInfra, xAI, and vLLM all use the same OpenAI content array format. Vision support depends on the model chosen.
Multimodal Exchanges¶
For pipeline routing (non-agent flows), exchanges carry multimodal content via MultipartContent:
Setting Multimodal Body¶
import "github.com/arvenza/helix/pkg/exchange"
// Create multipart content
exchange.SetMultipartBody(ex,
exchange.NewTextContent("Process this image"),
exchange.NewImageURLContent("https://example.com/photo.jpg"),
)
// Check if multipart
if exchange.IsMultipart(ex) {
content := exchange.GetMultipartBody(ex)
for _, part := range content.Parts {
fmt.Printf("Type: %s, MIME: %s\n", part.Type, part.MIMEType)
}
}
Multimodal Exchange Headers¶
| Header | Description |
|---|---|
HelixContentType |
MIME type of body content |
HelixContentEncoding |
Content encoding (e.g., base64) |
HelixMediaType |
High-level type: text, image, audio, video |
HelixImageURL |
Direct image URL for vision processing |
HelixAudioURL |
Direct audio URL |
HelixVideoURL |
Direct video URL |
HelixMultipart |
"true" if body is MultipartContent |
HTTP Workflow with Image Processing¶
agents:
- id: vision-analyze
type: react
trigger: direct://vision-analyze
model:
provider: openai
model: gpt-4o
system_prompt: "Analyze the provided image and describe what you see."
workflows:
- id: image-pipeline
trigger:
uri: http-server://0.0.0.0:8080/api/process-image
steps:
- transform:
starlang: |
# Image data arrives as binary body
# Set content type header
headers["HelixMediaType"] = "image"
headers["HelixContentType"] = "image/jpeg"
result = payload
- target: direct://vision-analyze
RAG with Multimodal Documents¶
Image Ingestion¶
The ImageReader processes image files for RAG pipelines:
workflows:
- id: image-rag
trigger:
uri: timer://ingest?period=1h
steps:
- rag-ingest:
reader: image
- rag-chunk:
strategy: fixed
chunk_size: 500
- rag-embed:
provider: openai
model: text-embedding-3-small
- rag-store:
store: memory
collection: images
Without a vision model: Generates metadata descriptions (filename, format, dimensions).
With a vision model: Sends the image to a vision-capable LLM for rich text descriptions:
reader := &ingest.ImageReader{
DescriptionModel: visionModel, // e.g., OpenAI gpt-4o
IncludeBase64: false,
}
doc, err := reader.Parse(ctx, imageBytes, metadata)
// doc.Content = "A photograph showing a golden retriever sitting in a park..."
Supported formats: .jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
Audio Ingestion¶
The AudioReader handles audio files:
workflows:
- id: audio-rag
trigger:
uri: timer://ingest?period=1h
steps:
- rag-ingest:
reader: audio
- rag-chunk:
strategy: sentence
chunk_size: 1000
- rag-embed:
provider: openai
model: text-embedding-3-small
- rag-store:
store: memory
collection: audio
Without a model: Metadata descriptions only.
With a transcription model: Full audio-to-text transcription.
Supported formats: .wav, .mp3, .ogg, .flac, .m4a
Multimodal Documents¶
The MultimodalDocument type extends the standard Document with media attachments:
import "github.com/arvenza/helix/pkg/rag"
doc := &rag.MultimodalDocument{
Document: rag.Document{
ID: "doc-1",
Content: "Product photo showing the new widget",
Source: "catalog/widget.jpg",
},
}
// Attach media
doc.AddMedia(rag.MediaContent{
Type: rag.MediaImage,
MIMEType: "image/jpeg",
Data: imageBytes,
Caption: "Front view of the widget in blue colorway",
})
// Check media
if doc.HasMedia() {
images := doc.MediaOfType(rag.MediaImage)
fmt.Printf("Document has %d images\n", len(images))
}
// Convert to plain document (includes captions in content)
plainDoc := doc.ToDocument(true) // true = include captions
Multimodal Embeddings (CLIP)¶
For image-based similarity search, use the CLIPEmbedder:
import "github.com/arvenza/helix/pkg/rag/embed"
clipEmbedder := &embed.CLIPEmbedder{
BaseURL: "http://localhost:8000",
APIKey: "",
Model: "clip-vit-large-patch14",
Dimensions: 768,
}
// Embed text
textVec, _ := clipEmbedder.EmbedSingle(ctx, "a photo of a cat")
// Embed image
imageVec, _ := clipEmbedder.EmbedImage(ctx, imageBytes, "image/jpeg")
// Both vectors are in the same embedding space -- you can search text→image or image→image
The MultimodalEmbedder interface extends the standard Embedder:
type MultimodalEmbedder interface {
Embedder
EmbedImage(ctx context.Context, imageData []byte, mimeType string) ([]float32, error)
EmbedImages(ctx context.Context, images []ImageInput) ([][]float32, error)
}
Multimodal Tools¶
Agents can use tools that return multimodal content:
import (
"github.com/arvenza/helix/pkg/agent/llm"
"github.com/arvenza/helix/pkg/agent/tool"
)
// A tool that generates images
type ImageGenTool struct{}
func (t *ImageGenTool) Info() *tool.ToolInfo {
return &tool.ToolInfo{
Name: "generate_image",
Description: "Generate an image from a text description",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"prompt": map[string]any{"type": "string"},
},
},
}
}
func (t *ImageGenTool) Invoke(ctx context.Context, args string) (string, error) {
// Fallback text response
return "Image generated successfully", nil
}
func (t *ImageGenTool) InvokeMultimodal(ctx context.Context, args string) ([]llm.ContentPart, error) {
// Parse args, call image generation API...
imageData := generateImage(args)
return []llm.ContentPart{
llm.TextPart("Here is the generated image:"),
llm.ImageDataPart("image/png", imageData),
}, nil
}
The MultimodalTool interface:
type MultimodalTool interface {
Tool
InvokeMultimodal(ctx context.Context, arguments string) ([]llm.ContentPart, error)
}
When the agent framework detects a MultimodalTool, it calls InvokeMultimodal and includes the returned content parts in the conversation.
Complete Examples¶
Image Q&A API¶
Accept image uploads via HTTP and answer questions about them:
workflows:
- id: image-qa
trigger:
uri: http-server://0.0.0.0:8080/api/image-qa
steps:
- transform:
starlang: |
# Extract question from query parameter
question = headers.question
if question == "":
question = "What is in this image?"
headers["HelixMediaType"] = "image"
result = payload
- agent:
type: react
model:
provider: openai
model: gpt-4o
system_prompt: |
You are a visual assistant. Analyze images and answer questions about them.
Be specific and detailed in your descriptions.
Test with curl:
curl -X POST http://localhost:8080/api/image-qa \
-H "Content-Type: image/jpeg" \
-H "question: How many people are in this photo?" \
--data-binary @photo.jpg
Multi-Provider Vision Comparison¶
Send the same image to multiple vision models and compare responses:
agents:
- id: openai-vision
type: react
trigger: direct://openai-vision
model:
provider: openai
model: gpt-4o
system_prompt: "Describe this image in one paragraph."
- id: anthropic-vision
type: react
trigger: direct://anthropic-vision
model:
provider: anthropic
model: claude-sonnet-4-20250514
system_prompt: "Describe this image in one paragraph."
- id: gemini-vision
type: react
trigger: direct://gemini-vision
model:
provider: gemini
model: gemini-2.0-flash
system_prompt: "Describe this image in one paragraph."
workflows:
- id: vision-compare
trigger:
uri: http-server://0.0.0.0:8080/api/compare-vision
steps:
- multicast:
uris:
- direct://openai-vision
- direct://anthropic-vision
- direct://gemini-vision
- aggregate:
strategy: collect
Image + Document RAG¶
Build a knowledge base from both text documents and images:
workflows:
# Ingest text documents
- id: ingest-text
trigger:
uri: timer://ingest-text?period=1h
steps:
- rag-ingest:
reader: directory
path: ./docs
recursive: true
extensions: [".md", ".txt"]
- rag-chunk:
strategy: markdown
chunk_size: 512
retain_headers: true
- rag-embed:
provider: openai
model: text-embedding-3-small
- rag-store:
store: pgvector
connection: "postgres://user:pass@db:5432/rag"
collection: knowledge
dimensions: 1536
# Ingest images (with vision-generated descriptions)
- id: ingest-images
trigger:
uri: timer://ingest-images?period=1h
steps:
- rag-ingest:
reader: image
- rag-chunk:
strategy: sentence
chunk_size: 500
- rag-embed:
provider: openai
model: text-embedding-3-small
- rag-store:
store: pgvector
connection: "postgres://user:pass@db:5432/rag"
collection: knowledge
dimensions: 1536
# Unified query endpoint
- id: query
trigger:
uri: http-server://0.0.0.0:8080/api/ask
steps:
- rag-retrieve:
strategy: hybrid
top_k: 10
vector_weight: 0.7
keyword_weight: 0.3
- rag-postprocess:
steps:
- type: cohere-rerank
params:
api_key: "${env:COHERE_API_KEY}"
top_n: "5"
- rag-synthesize:
mode: compact
model:
provider: openai
model: gpt-4o
Audio Transcription Pipeline¶
Transcribe audio files and make them searchable:
workflows:
- id: audio-pipeline
trigger:
uri: http-server://0.0.0.0:8080/api/transcribe
steps:
- rag-ingest:
reader: audio
- rag-chunk:
strategy: sentence
chunk_size: 1000
- rag-embed:
provider: openai
model: text-embedding-3-small
- rag-store:
store: memory
collection: transcripts
- log:
message: "Audio transcribed and indexed"
Interactive Multimodal Input¶
Beyond programmatic APIs, Helix agents support interactive multimodal input from frontends via the AG-UI protocol. These features enable real-time voice, file upload, and document processing in chat-style interfaces.
Voice Input (Speech-to-Text)¶
Agents exposed via AG-UI can receive voice input. The frontend captures audio via the browser's MediaRecorder API and sends it as a binary audio content part. The agent pipeline handles speech-to-text conversion before LLM processing.
agents:
- id: voice-assistant
type: react
trigger: agui://0.0.0.0:4100/agent
model:
provider: openai
model: gpt-4o
system_prompt: "You are a voice-enabled assistant. Respond concisely."
When audio arrives without accompanying text, the agent automatically transcribes the audio using the configured STT provider before sending the transcript to the LLM. Supported audio formats: WAV, WebM, MP3, OGG, M4A.
File Upload (Images, PDF, CSV, Text)¶
AG-UI frontends can upload files as content parts. The agent detects the file type and processes accordingly:
| File Type | Processing |
|---|---|
| Images (JPEG, PNG, GIF, WebP) | Sent directly to vision-capable LLMs as image content parts |
| Text extracted via built-in reader or OCR (if scanned) | |
| CSV | Parsed into structured text with column headers |
| Plain text (.txt, .md) | Passed through as text content |
agents:
- id: file-processor
type: react
trigger: agui://0.0.0.0:4100/agent
model:
provider: openai
model: gpt-4o
system_prompt: |
You can process uploaded files. For images, describe what you see.
For documents, summarize the content. For data files, analyze the data.
PDF OCR via Docling¶
For scanned PDFs and image-heavy documents, configure the OCR backend to extract text with layout awareness:
helix:
ocr:
backend: docling
endpoint: http://localhost:5001
agents:
- id: document-reader
type: react
trigger: agui://0.0.0.0:4100/agent
model:
provider: openai
model: gpt-4o
system_prompt: |
You process uploaded documents. Extract key information,
summarize tables, and answer questions about the content.
When a PDF is uploaded, the pipeline: 1. Detects the PDF content type 2. Sends the PDF to the Docling endpoint for OCR processing 3. Receives structured markdown with tables, headings, and text 4. Passes the extracted text to the LLM as the user message
Auto-Prompt for Multimodal Input¶
When a user sends a file (image, PDF, audio) without accompanying text, the agent generates an appropriate default prompt based on the content type:
| Content Type | Auto-Prompt |
|---|---|
| Image | "Describe this image in detail." |
| PDF / Document | "Summarize this document." |
| Audio | "Transcribe and summarize this audio." |
| CSV / Data | "Analyze this data and provide key insights." |
This means users can simply drag-and-drop a file into the chat without typing anything, and the agent will respond meaningfully.
Base64 Image Stripping for Large OCR Output¶
When OCR processes multi-page documents, the raw output can contain embedded base64 images (e.g., figures, charts extracted from PDFs). These base64 strings can be extremely large and waste LLM context tokens. Helix automatically strips base64 image data from OCR output before sending it to the LLM, replacing it with a placeholder reference:
This optimization can reduce token usage by 10-100x for image-heavy documents. The original images remain available as separate content parts if the agent needs to reference them via a vision model.
Content Part Reference¶
Constructors¶
| Constructor | Description |
|---|---|
llm.TextPart(text) |
Plain text content |
llm.ImageURLPart(url, detail) |
Image from URL (detail: low, high, auto) |
llm.ImageDataPart(mimeType, data) |
Image from binary data |
llm.AudioURLPart(url) |
Audio from URL |
llm.AudioDataPart(mimeType, data) |
Audio from binary data |
llm.VideoPart(url) |
Video from URL |
llm.FilePart(mimeType, data) |
Generic file attachment |
Agent Methods¶
| Method | Description |
|---|---|
agent.Run(ctx, text) |
Text-only input (existing) |
agent.RunWithImage(ctx, text, imageURL) |
Text + single image URL |
agent.RunWithParts(ctx, parts) |
Arbitrary multimodal input |
Message Helpers¶
| Method | Description |
|---|---|
msg.HasParts() |
True if message contains ContentParts |
msg.TextContent() |
Extracts text (from Parts or Content field) |
part.DataURI() |
Returns data:mime;base64,... URI for binary parts |
Exchange Helpers¶
| Function | Description |
|---|---|
exchange.SetMultipartBody(ex, parts...) |
Set multipart content as body |
exchange.GetMultipartBody(ex) |
Extract multipart content from body |
exchange.IsMultipart(ex) |
Check if exchange has multipart content |
exchange.NewTextContent(text) |
Create text content part |
exchange.NewImageContent(mime, data) |
Create image content part |
exchange.NewImageURLContent(url) |
Create image URL content part |
exchange.NewAudioContent(mime, data) |
Create audio content part |
Provider Vision Model Support¶
| Provider | Vision Models | Image | Audio | Video |
|---|---|---|---|---|
| OpenAI | gpt-4o, gpt-4o-mini, gpt-4-turbo | Yes | Yes (gpt-4o-audio) | No |
| Anthropic | claude-sonnet-4-20250514, claude-opus-4-20250514 | Yes | No | No |
| Vertex/Gemini | gemini-2.0-flash, gemini-1.5-pro | Yes | Yes | Yes |
| Ollama | llava, bakllava, moondream | Yes | No | No |
| Groq | llama-3.2-90b-vision-preview | Yes | No | No |
| Together | Llama-3.2-90B-Vision-Instruct-Turbo | Yes | No | No |
| Fireworks | Model dependent | Yes | No | No |
| Azure | gpt-4o (deployed) | Yes | Yes | No |
| Bedrock | anthropic.claude-3-sonnet | Yes | No | No |