RAG (Retrieval-Augmented Generation) framework for the Embabel Agent platform providing content ingestion, chunking, hierarchical navigation, and semantic search capabilities
Deep dive into the architectural design, extensibility points, and design principles of the Embabel Agent RAG Core framework.
The Embabel Agent RAG Core framework is built around a layered architecture that separates concerns while maintaining flexibility and extensibility. The system is designed to handle everything from document ingestion to complex multi-modal retrieval operations.
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ (LLM Tools, Search Services, Domain Logic) │
└───────────────────┬─────────────────────────────────────┘
│
┌───────────────────┴─────────────────────────────────────┐
│ Service Layer │
│ (Search Operations, Filtering, Entity Management) │
└───────────────────┬─────────────────────────────────────┘
│
┌───────────────────┴─────────────────────────────────────┐
│ Ingestion Layer │
│ (Content Reading, Chunking, Transformation) │
└───────────────────┬─────────────────────────────────────┘
│
┌───────────────────┴─────────────────────────────────────┐
│ Storage Layer │
│ (Repositories, Vector Stores, Embeddings) │
└───────────────────┬─────────────────────────────────────┘
│
┌───────────────────┴─────────────────────────────────────┐
│ Infrastructure Layer │
│ (Embedding Services, Spring AI Integration) │
└─────────────────────────────────────────────────────────┘Documents are represented as hierarchical structures that preserve organization and context.
NavigableDocument (Root)
├─ ContentRoot (metadata, URI, timestamp)
│
├─ NavigableContainerSection
│ ├─ LeafSection (actual content)
│ ├─ NavigableContainerSection
│ │ ├─ LeafSection
│ │ └─ LeafSection
│ └─ LeafSection
│
└─ NavigableContainerSection
└─ LeafSectionKey Characteristics:
This hierarchy enables:
// Foundation for all data objects
sealed interface Datum {
val id: String
val uri: String?
val metadata: Map<String, Any?>
fun propertiesToPersist(): Map<String, Any?>
fun labels(): Set<String>
}
// Objects that can be embedded
interface Embeddable {
fun embeddableValue(): String
}
// Objects with embeddings
interface Embedded {
val embedding: Embedding?
}
// RAG-retrievable objects
interface Retrievable : HasInfoString, Datum, Embeddable
// Hierarchical content
interface HierarchicalContentElement : ContentElement {
val parentId: String?
}Datum (base)
│
├─ ContentElement
│ ├─ HierarchicalContentElement
│ │ ├─ ContentRoot
│ │ │ └─ NavigableDocument
│ │ ├─ Section
│ │ │ ├─ ContainerSection
│ │ │ │ └─ NavigableContainerSection
│ │ │ └─ LeafSection
│ │ └─ Chunk
│ │
│ └─ Retrievable
│ ├─ Source
│ │ ├─ Chunk
│ │ └─ Fact
│ └─ NamedEntity
│ └─ NamedEntityData
│
└─ [Custom Domain Types]This hierarchy provides:
Input → Reader → Document → Chunker → Chunks → Transformer → Enhanced Chunks → Repository
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ └─ Embedding
│ │ │ │ │ │ │ Generation
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ Metadata
│ │ │ │ │ │ Enrichment
│ │ │ │ │ │
│ │ │ │ │ └─ Text
│ │ │ │ │ Modification
│ │ │ │ │
│ │ │ │ └─ Chunk
│ │ │ │ Creation
│ │ │ │
│ │ │ └─ Content
│ │ │ Chunking
│ │ │
│ │ └─ Hierarchical
│ │ Document
│ │
│ └─ Content
│ Parsing
│
└─ Source
(URL, File, Stream, Directory)Purpose: Parse raw content into structured NavigableDocument objects
interface HierarchicalContentReader {
fun parseUrl(url: String): NavigableDocument
fun parseFile(file: File): NavigableDocument
fun parseStream(stream: InputStream, uri: String): NavigableDocument
fun parseDirectory(directory: File, recursive: Boolean): List<NavigableDocument>
}Responsibilities:
Extensibility:
Purpose: Determine when documents should be re-ingested
interface ContentRefreshPolicy {
fun shouldReread(
repository: ChunkingContentElementRepository,
rootUri: String
): Boolean
fun shouldRefreshDocument(
repository: ChunkingContentElementRepository,
root: NavigableDocument
): Boolean
fun ingestUriIfNeeded(
repository: ChunkingContentElementRepository,
hierarchicalContentReader: HierarchicalContentReader,
rootUri: String
): NavigableDocument?
}Strategies:
Design Rationale:
Purpose: Break documents into Chunk objects for indexing
class ContentChunker(
val config: Config,
val chunkTransformer: ChunkTransformer = ChunkTransformer.NO_OP
) {
fun chunk(document: NavigableDocument): Sequence<Chunk>
fun chunk(section: Section): Sequence<Chunk>
data class Config(
val maxChunkSize: Int = 1500,
val overlapSize: Int = 200,
val respectSentenceBoundaries: Boolean = true
)
}Strategy:
Design Rationale:
Purpose: Enrich and modify chunks before storage
interface ChunkTransformer {
val name: String
fun transform(chunk: Chunk, context: ChunkTransformationContext): Chunk
}
abstract class AbstractChunkTransformer : ChunkTransformer {
open fun additionalMetadata(
chunk: Chunk,
context: ChunkTransformationContext
): Map<String, Any> = emptyMap()
open fun newText(
chunk: Chunk,
context: ChunkTransformationContext
): String = chunk.text
}Capabilities:
Design Rationale:
Purpose: Persist documents, sections, and chunks with embeddings
interface ChunkingContentElementRepository : ContentElementRepository {
val enhancers: List<RetrievableEnhancer>
fun writeAndChunkDocument(root: NavigableDocument): List<String>
fun deleteRootAndDescendants(uri: String): DocumentDeletionResult?
fun findContentRootByUri(uri: String): ContentRoot?
fun existsRootWithUri(uri: String): Boolean
fun <T : Retrievable> enhance(retrievable: T): T
fun onNewRetrievables(retrievables: List<Retrievable>)
}Implementation Strategy:
Design Rationale:
// Basic vector search
interface VectorSearch {
fun <T : Retrievable> vectorSearch(
request: TextSimilaritySearchRequest,
clazz: Class<T>
): List<SimilarityResult<T>>
}
// Vector search with filtering
interface FilteringVectorSearch : VectorSearch {
fun <T : Retrievable> vectorSearchWithFilter(
request: TextSimilaritySearchRequest,
clazz: Class<T>,
metadataFilter: PropertyFilter?,
entityFilter: EntityFilter?
): List<SimilarityResult<T>>
}
// Full-text search
interface TextSearch {
fun <T : Retrievable> textSearch(
request: TextSearchRequest,
clazz: Class<T>
): List<SimilarityResult<T>>
}
// Regex search
interface RegexSearch {
fun <T : Retrievable> regexSearch(
request: RegexSearchRequest,
clazz: Class<T>
): List<SimilarityResult<T>>
}Mechanism: Semantic similarity using embeddings
Query Text → Embedding → Vector Space → Nearest Neighbors → Results
↓
Cosine Similarity
↓
Similarity ScoresCharacteristics:
Use Cases:
Mechanism: Full-text search with Lucene-like syntax
Characteristics:
Use Cases:
Mechanism: Pattern-based matching
Characteristics:
Use Cases:
Composable filter expressions for metadata and properties.
sealed interface PropertyFilter {
operator fun not(): PropertyFilter
infix fun and(other: PropertyFilter): PropertyFilter
infix fun or(other: PropertyFilter): PropertyFilter
}
// Comparison filters
data class Eq(val key: String, val value: Any) : PropertyFilter
data class Ne(val key: String, val value: Any) : PropertyFilter
data class Gt(val key: String, val value: Number) : PropertyFilter
data class Gte(val key: String, val value: Number) : PropertyFilter
data class Lt(val key: String, val value: Number) : PropertyFilter
data class Lte(val key: String, val value: Number) : PropertyFilter
// Collection filters
data class In(val key: String, val values: List<Any>) : PropertyFilter
data class Nin(val key: String, val values: List<Any>) : PropertyFilter
// String filters
data class Contains(val key: String, val value: String) : PropertyFilter
data class StartsWith(val key: String, val value: String) : PropertyFilter
data class EndsWith(val key: String, val value: String) : PropertyFilter
// Logical operators
data class And(val filters: List<PropertyFilter>) : PropertyFilter
data class Or(val filters: List<PropertyFilter>) : PropertyFilter
data class Not(val filter: PropertyFilter) : PropertyFilterDesign Rationale:
interface NamedEntity : Retrievable, NamedAndDescribed {
override val id: String
override val name: String
override val description: String
val uri: String?
val metadata: Map<String, Any?>
fun labels(): Set<String>
}
interface NamedEntityData : NamedEntity {
val properties: Map<String, Any>
val linkedDomainType: DomainType?
fun <T : NamedEntity> toTypedInstance(objectMapper: ObjectMapper): T?
fun <T : NamedEntity> toInstance(
vararg interfaces: Class<out NamedEntity>
): T
}@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Relationship(
val name: String = "",
val direction: RelationshipDirection = RelationshipDirection.OUTGOING
)
enum class RelationshipDirection {
OUTGOING, INCOMING, BOTH
}
interface RelationshipNavigator {
fun findRelated(
source: RetrievableIdentifier,
relationshipName: String,
direction: RelationshipDirection
): List<NamedEntityData>
}Capabilities:
Design Rationale:
class SpringVectorStoreVectorSearch(
val vectorStore: VectorStore
) : FilteringVectorSearch, TypeRetrievalOperations {
// Adapts Spring AI VectorStore to RAG interface
}
fun PropertyFilter.toSpringAiExpression(): Filter.Expression {
// Converts RAG filters to Spring AI expressions
}Integration Points:
Benefits:
Implement HierarchicalContentReader for custom formats:
class CustomFormatReader : HierarchicalContentReader {
override fun parseUrl(url: String): NavigableDocument {
// Custom parsing logic
}
}Use Cases:
Implement ContentRefreshPolicy for custom refresh logic:
class CustomRefreshPolicy : ContentRefreshPolicy {
override fun shouldReread(
repository: ChunkingContentElementRepository,
rootUri: String
): Boolean {
// Custom refresh decision logic
}
}Use Cases:
Extend AbstractChunkTransformer for custom enrichment:
class CustomTransformer : AbstractChunkTransformer() {
override val name = "custom-transformer"
override fun additionalMetadata(
chunk: Chunk,
context: ChunkTransformationContext
): Map<String, Any> {
// Custom metadata generation
}
override fun newText(
chunk: Chunk,
context: ChunkTransformationContext
): String {
// Custom text transformation
}
}Use Cases:
Extend AbstractChunkingContentElementRepository for custom backends:
class CustomRepository : AbstractChunkingContentElementRepository() {
override fun persistChunksWithEmbeddings(
chunks: List<Chunk>,
embeddings: Map<String, FloatArray>
) {
// Custom persistence logic
}
override fun createInternalRelationships(root: NavigableDocument) {
// Custom relationship creation
}
override fun commit() {
// Custom transaction management
}
}Use Cases:
Define entity interfaces with relationships:
interface Project : NamedEntity {
@Relationship(name = "HAS_CONTRIBUTOR")
fun getContributors(): List<Employee>
@Relationship(name = "DEPENDS_ON")
fun getDependencies(): List<Project>
}
interface Employee : NamedEntity {
val department: String
val role: String
@Relationship(name = "WORKS_ON")
fun getProjects(): List<Project>
}Use Cases:
Each component has a single, well-defined responsibility:
Benefits:
Dependencies defined through interfaces, not implementations:
Benefits:
Components can be combined flexibly:
Benefits:
Optimizations throughout the pipeline:
Benefits:
Easy to extend without modifying core:
Benefits:
Time Complexity:
Space Complexity:
Optimization Strategies:
Vector Search:
Text Search:
Optimization Strategies:
┌─────────────────┐
│ Application │
├─────────────────┤
│ RAG Core │
├─────────────────┤
│ Embeddings API │
├─────────────────┤
│ Vector Store │
└─────────────────┘Characteristics:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Ingestion │ │ Search │ │ Entities │
│ Service │ │ Service │ │ Service │
└───────┬──────┘ └───────┬──────┘ └───────┬──────┘
│ │ │
└───────────────────┴────────────────────┘
│
┌───────┴────────┐
│ Vector Store │
│ (Shared State) │
└────────────────┘Characteristics:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Lambda: │ │ Lambda: │ │ Lambda: │
│ Ingest │ │ Search │ │ Query │
└───────┬──────┘ └───────┬──────┘ └───────┬──────┘
│ │ │
└───────────────────┴────────────────────┘
│
┌───────┴────────┐
│ Managed Store │
│ (Pinecone, │
│ Weaviate) │
└────────────────┘Characteristics: