How to Implement Custom Modal Processors in RAGAnything for New Content Types
To implement custom modal processors in RAGAnything, create a subclass of BaseModalProcessor in modalprocessors.py, implement generate_description_only and process_multimodal_content, then wire it into the pipeline via RAGAnything._initialize_processors and utils.get_processor_for_type.
RAGAnything is a multimodal RAG framework that processes heterogeneous content—images, tables, equations, and more—through a unified pipeline. Each content type is handled by a dedicated modal processor that transforms raw data into searchable text chunks and knowledge graph entities. This guide walks you through extending RAGAnything with custom modal processors for new content types, using the actual source architecture from HKUDS/RAG-Anything.
Understanding the BaseModalProcessor Architecture
The foundation of every modal processor is BaseModalProcessor, defined in raganything/modalprocessors.py (lines 360–462). This abstract base class provides:
- Shared infrastructure: References to the
LightRAGinstance, vector stores, and LLM functions - Context extraction: The
_get_context_for_itemmethod leveragesContextExtractorto pull surrounding text, captions, or metadata - Robust parsing: Utility methods like
_robust_json_parseand_strip_thinking_tagsfor handling LLM responses - Abstract API: Two methods you must implement—
generate_description_onlyandprocess_multimodal_content
All existing processors (ImageModalProcessor, TableModalProcessor, EquationModalProcessor, GenericModalProcessor) inherit from this base and follow the same contract.
Leveraging ContextExtractor for Rich Modal Processing
The ContextExtractor class (lines 49–338 in modalprocessors.py) enables processors to gather contextual information from surrounding document content. Key capabilities include:
- Configurable extraction: Controlled by
ContextConfiginraganything/config.pywith settings forcontext_window,include_captions, and more - Flexible source handling: Works with
minerUoutput lists, plain text, and dictionary structures - Simple processor integration: Call
self._get_context_for_item(item_info)from within your processor to obtain a context string
This context can be interpolated into your LLM prompts to generate more accurate, grounded descriptions for your custom content type.
Step-by-Step: Creating a Custom Modal Processor
Step 1: Subclass BaseModalProcessor
Create a new file or add to modalprocessors.py. Here's a complete AudioModalProcessor implementation:
# examples/audio_processor.py
from typing import Any, Dict, Tuple
from raganything.modalprocessors import BaseModalProcessor
from raganything.prompt import PROMPTS
class AudioModalProcessor(BaseModalProcessor):
"""Processor for audio content (e.g., speech or music)."""
async def generate_description_only(
self,
modal_content: Any,
content_type: str,
item_info: Dict[str, Any] = None,
entity_name: str = None,
) -> Tuple[str, Dict[str, Any]]:
"""
Turn an audio file into a textual description and entity record.
`modal_content` is expected to be a dict with ``audio_path`` and optional
``metadata`` fields.
"""
# Parse the incoming payload
if isinstance(modal_content, str):
import json
modal_content = json.loads(modal_content)
audio_path = modal_content.get("audio_path")
metadata = modal_content.get("metadata", {})
if not audio_path:
raise ValueError("AudioModalProcessor requires an ``audio_path`` key")
# Grab surrounding context if available
context = ""
if item_info:
context = self._get_context_for_item(item_info)
# Build the LLM prompt
if context:
prompt = PROMPTS.get(
"generic_prompt_with_context", PROMPTS["generic_prompt"]
).format(
context=context,
content_type=content_type,
entity_name=entity_name or "audio_clip",
content=f"Audio file: {audio_path}\nMetadata: {metadata}",
)
else:
prompt = PROMPTS["generic_prompt"].format(
content_type=content_type,
entity_name=entity_name or "audio_clip",
content=f"Audio file: {audio_path}\nMetadata: {metadata}",
)
# Call the LLM
response = await self.modal_caption_func(
prompt,
system_prompt=PROMPTS["GENERIC_ANALYSIS_SYSTEM"].format(content_type=content_type),
)
# Parse using the base class utility
description, entity_info = self._parse_generic_response(response, entity_name, content_type)
return description, entity_info
async def process_multimodal_content(
self,
modal_content: Any,
content_type: str,
file_path: str = "manual_creation",
entity_name: str = None,
item_info: Dict[str, Any] = None,
batch_mode: bool = False,
doc_id: str = None,
chunk_order_index: int = 0,
) -> Tuple[str, Dict[str, Any]]:
"""Full pipeline – generate description then store the chunk."""
description, entity_info = await self.generate_description_only(
modal_content, content_type, item_info, entity_name
)
# Build the final chunk for indexing
modal_chunk = PROMPTS["generic_chunk"].format(
content_type=content_type.title(),
content=str(modal_content),
enhanced_caption=description,
)
return await self._create_entity_and_chunk(
modal_chunk,
entity_info,
file_path,
batch_mode,
doc_id,
chunk_order_index,
)
Step 2: Register in _initialize_processors
Add your processor to raganything/raganything.py (lines 14–42):
# Inside RAGAnything._initialize_processors
if self.config.enable_audio_processing: # Add this config option
self.modal_processors["audio"] = AudioModalProcessor(
lightrag=self.lightrag,
modal_caption_func=self.llm_model_func,
context_extractor=self.context_extractor,
)
Step 3: Update get_processor_for_type
Extend raganything/utils.py (lines 28–48):
# Inside get_processor_for_type
elif content_type == "audio":
return modal_processors.get("audio")
Step 4: Add Configuration Toggle
Add to raganything/config.py:
enable_audio_processing: bool = field(
default=get_env_value("ENABLE_AUDIO_PROCESSING", True, bool)
)
"""Enable audio content processing."""
Using Your Custom Processor
Once registered, the core pipeline automatically invokes your processor for matching content types:
import asyncio
from raganything.raganything import RAGAnything
from raganything.config import RAGAnythingConfig
async def demo():
rag = RAGAnything(
config=RAGAnythingConfig(enable_audio_processing=True)
)
await rag._ensure_lightrag_initialized()
# Simulate parsed multimodal payload
multimodal = [
{
"type": "audio",
"audio_path": "samples/lecture.wav",
"metadata": {"duration_s": 360, "speaker": "Prof. X"},
"page_idx": 2,
}
]
# Process – RAGAnything selects AudioModalProcessor automatically
await rag.process_multimodal_content(multimodal, file_path="lecture.pdf")
asyncio.run(demo())
The pipeline inserts a text chunk describing the audio, creates an entity node (e.g., lecture.wav (audio)), and links it to surrounding document context using the same graph-building logic as images and tables.
Key Files Reference
| File | Role | Link |
|---|---|---|
raganything/modalprocessors.py |
Core class hierarchy (BaseModalProcessor, ContextExtractor, built-in processors) |
[modalprocessors.py] |
raganything/raganything.py |
Orchestrates processor creation, stores modal_processors dict, runs the pipeline |
[raganything.py] |
raganything/utils.py |
Helper get_processor_for_type that selects a processor at runtime |
[utils.py] |
raganything/config.py |
Configuration flags, including optional enable_*_processing switches |
[config.py] |
examples/modalprocessors_example.py |
Usage examples of existing processors; reference for new implementations | [modalprocessors_example.py] |
tests/test_strip_thinking_tags.py |
Unit tests for base class utilities – template for testing new processors | [test_strip_thinking_tags.py] |
Summary
- Subclass
BaseModalProcessorinmodalprocessors.pyto implementgenerate_description_onlyandprocess_multimodal_contentfor your new content type. - Wire into the pipeline by instantiating in
RAGAnything._initialize_processorsand extendingutils.get_processor_for_typeto recognize yourcontent_typestring. - Leverage
ContextExtractorvia_get_context_for_itemto enrich descriptions with surrounding document context. - Add configuration toggles in
config.pyfor optional processor enablement. - Follow existing patterns in
ImageModalProcessorandTableModalProcessorfor consistent behavior.
Frequently Asked Questions
What is the minimum code needed to implement a custom modal processor?
The minimum implementation requires a class inheriting from BaseModalProcessor with two async methods: generate_description_only to produce a text description from your raw content, and process_multimodal_content to orchestrate storage via _create_entity_and_chunk. The base class handles vector database operations, graph creation, and context extraction—you only write the modality-specific logic.
How does RAGAnything decide which processor to use for each content item?
The selection happens in utils.get_processor_for_type (lines 28–48), which receives the content_type string from a parsed document item and returns the matching processor from the modal_processors dictionary. This dictionary is populated during RAGAnything._initialize_processors based on configuration flags, so adding your processor to both locations ensures automatic routing.
Can I access surrounding text or captions from my custom processor?
Yes. The BaseModalProcessor provides _get_context_for_item(item_info), which internally uses the shared ContextExtractor instance. Call this method with the item_info dict (containing keys like page_idx, bbox, or custom metadata) to retrieve a formatted string of relevant surrounding content. This context can then be interpolated into your LLM prompts.
What configuration options should I expose for my custom processor?
Follow the pattern used by built-in processors: add a boolean flag like enable_yourmodality_processing to RAGAnythingConfig in config.py, with a default value and environment variable override via get_env_value. Then check this flag in RAGAnything._initialize_processors before instantiating your processor. This keeps your integration consistent with the RAGAnything configuration system.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →