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: