RAG (Retrieval-Augmented Generation) framework for the Embabel Agent platform providing content ingestion, chunking, hierarchical navigation, and semantic search capabilities
—
This guide covers integrating RAG capabilities with Large Language Models using ToolishRag, including tool configuration, filtering, monitoring, and Spring AI integration.
LLM integration enables language models to:
Create a simple RAG tool for LLM access.
import com.embabel.agent.rag.tools.ToolishRag
import com.embabel.agent.rag.service.*
val searchOperations: CoreSearchOperations = // implementation
// Create basic RAG tool
val ragTool = ToolishRag(
name = "documentation_search",
description = "Search technical documentation and user guides",
searchOperations = searchOperations
)
// Get tools for LLM
val tools = ragTool.tools()
println("Exposed ${tools.size} tools to LLM")
// Get usage notes for LLM context
val notes = ragTool.notes()
println(notes)// ToolishRag automatically creates these tools:
// - vectorSearch(query, topK, threshold) - Semantic similarity search
// - textSearch(query, topK, threshold) - Full-text search with Lucene syntax
// - broadenChunk(chunkId, chunksToAdd) - Get surrounding chunks
// - zoomOut(id) - Get parent section
// - findById(id, typeName) - Look up by identifier
tools.forEach { tool ->
println("Tool: ${tool.name}")
println("Description: ${tool.description}")
println()
}Specify which content types to search.
import com.embabel.agent.rag.model.*
val ragTool = ToolishRag(
name = "knowledge_base",
description = "Search knowledge base including docs, facts, and entities",
searchOperations = searchOperations
)
// Configure search types
val configured = ragTool.withSearchFor(
vectorSearchFor = listOf(
Chunk::class.java,
Fact::class.java,
NamedEntity::class.java
),
textSearchFor = listOf(
Chunk::class.java,
Fact::class.java
)
)// Create separate tools for different content types
// Documentation search
val docsTool = ToolishRag(
name = "docs_search",
description = "Search technical documentation",
searchOperations = docsRepository
).withSearchFor(
vectorSearchFor = listOf(Chunk::class.java),
textSearchFor = listOf(Chunk::class.java)
)
// Entity search
val entityTool = ToolishRag(
name = "entity_search",
description = "Search people, projects, and organizations",
searchOperations = entityRepository
).withSearchFor(
vectorSearchFor = listOf(NamedEntity::class.java),
textSearchFor = listOf(NamedEntity::class.java)
)
// Combine tools
val allTools = docsTool.tools() + entityTool.tools()Add default filters to all searches.
import com.embabel.agent.rag.filter.*
// Add metadata filter
val filteredRag = ToolishRag(
name = "security_docs",
description = "Search security-related documentation",
searchOperations = searchOperations
).withMetadataFilter(
PropertyFilter.eq("category", "security")
.and(PropertyFilter.gte("version", 3.0))
.and(PropertyFilter.ne("status", "archived"))
)
// All searches will automatically apply these filters// Filter by entity labels
val peopleRag = ToolishRag(
name = "people_search",
description = "Search employees and contractors",
searchOperations = entityRepository
).withEntityFilter(
EntityFilter.hasAnyLabel("Employee", "Contractor")
).withMetadataFilter(
PropertyFilter.eq("status", "active")
)// Combine multiple filter types
val comprehensiveRag = ToolishRag(
name = "platform_knowledge",
description = "Search platform team knowledge base",
searchOperations = searchOperations
)
.withMetadataFilter(
PropertyFilter.eq("team", "platform")
.and(PropertyFilter.gte("lastUpdated", recentTimestamp))
)
.withEntityFilter(
EntityFilter.hasAnyLabel("Chunk", "Fact", "Person")
)Use HyDE (Hypothetical Document Embeddings) to improve search quality.
import com.embabel.agent.rag.tools.TryHyDE
// Add HyDE hint using conversation context
val hydeRag = ToolishRag(
name = "code_search",
description = "Search code documentation and examples",
searchOperations = searchOperations
).withHint(
TryHyDE.usingConversationContext()
.withMaxWords(75)
)// Provide specific context for HyDE
val contextualRag = ragTool.withHint(
TryHyDE.withContext(
"Kotlin programming language best practices and design patterns"
).withMaxWords(60)
)// Chain multiple hints (though typically you'd use one HyDE)
val multiHintRag = ragTool
.withHint(TryHyDE.usingConversationContext())
.withGoal(
"Use this tool to find relevant information. " +
"For code examples, try exact text search. " +
"For concepts, use vector search."
)Format search results for optimal LLM consumption.
import com.embabel.agent.rag.service.*
// Create custom formatter
val markdownFormatter = RetrievableResultsFormatter { results ->
buildString {
appendLine("# Search Results")
appendLine()
appendLine("Found ${results.results.size} relevant matches:")
appendLine()
results.results.forEachIndexed { index, result ->
appendLine("## ${index + 1}. Relevance: ${"%.1f".format(result.score * 100)}%")
appendLine()
when (val content = result.content) {
is Chunk -> {
val section = content.metadata["container_section_title"]
if (section != null) {
appendLine("**Source:** $section")
}
appendLine()
appendLine(content.text)
}
is NamedEntity -> {
appendLine("**Entity:** ${content.name}")
appendLine()
appendLine(content.description)
}
}
appendLine()
appendLine("---")
appendLine()
}
}
}
// Use custom formatter
val ragTool = ToolishRag(
name = "knowledge_search",
description = "Search knowledge base",
searchOperations = searchOperations,
formatter = markdownFormatter
)// Minimal formatter for token efficiency
val compactFormatter = RetrievableResultsFormatter { results ->
results.results.joinToString("\n\n") { result ->
when (val content = result.content) {
is Chunk -> "[${"%.2f".format(result.score)}] ${content.text}"
is NamedEntity -> "[${"%.2f".format(result.score)}] ${content.name}: ${content.description}"
else -> result.content.infoString()
}
}
}Track and analyze search operations.
import com.embabel.agent.rag.tools.*
// Create results listener
val listener = ResultsListener { event ->
println("Query: ${event.query}")
println("Results: ${event.results.size}")
println("Time: ${event.runningTime.toMillis()}ms")
println("Timestamp: ${event.timestamp}")
// Log top result
event.results.firstOrNull()?.let { top ->
println("Top score: ${"%.3f".format(top.score)}")
}
}
// Add listener
val monitoredRag = ToolishRag(
name = "monitored_search",
description = "Search with monitoring",
searchOperations = searchOperations
).withListener(listener)import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
class SearchAnalytics {
private val queryCount = ConcurrentHashMap<String, AtomicLong>()
private val avgScores = ConcurrentHashMap<String, Double>()
private val totalSearches = AtomicLong(0)
private val totalTime = AtomicLong(0)
val listener = ResultsListener { event ->
// Track query frequency
queryCount.computeIfAbsent(event.query) {
AtomicLong(0)
}.incrementAndGet()
// Track performance
totalSearches.incrementAndGet()
totalTime.addAndGet(event.runningTime.toMillis())
// Track quality
if (event.results.isNotEmpty()) {
val avgScore = event.results.map { it.score }.average()
avgScores[event.query] = avgScore
}
}
fun getStats(): Map<String, Any> {
val searches = totalSearches.get()
return mapOf(
"totalSearches" to searches,
"uniqueQueries" to queryCount.size,
"avgLatency" to if (searches > 0) totalTime.get() / searches else 0,
"topQueries" to queryCount.entries
.sortedByDescending { it.value.get() }
.take(10)
.associate { it.key to it.value.get() },
"avgScores" to avgScores.toMap()
)
}
fun reset() {
queryCount.clear()
avgScores.clear()
totalSearches.set(0)
totalTime.set(0)
}
}
// Use analytics
val analytics = SearchAnalytics()
val ragTool = ToolishRag(
name = "analytics_search",
description = "Search with analytics",
searchOperations = searchOperations
).withListener(analytics.listener)
// Later, check stats
val stats = analytics.getStats()
println("Search analytics: $stats")val ragTool = ToolishRag(
name = "api_docs",
description = "Search API documentation",
searchOperations = searchOperations
).withGoal(
"Use this tool to find API endpoints, request/response formats, and usage examples. " +
"For exact endpoint names, use text search. " +
"For conceptual queries, use vector search. " +
"If results are unclear, try expanding chunks for more context."
)import com.embabel.agent.rag.filter.*
import com.embabel.agent.rag.tools.*
import com.embabel.agent.rag.model.*
val comprehensiveRag = ToolishRag(
name = "platform_knowledge",
description = "Comprehensive platform team knowledge base",
searchOperations = searchOperations
)
// Configure search types
.withSearchFor(
vectorSearchFor = listOf(
Chunk::class.java,
Fact::class.java,
NamedEntity::class.java
),
textSearchFor = listOf(
Chunk::class.java,
Fact::class.java
)
)
// Apply filters
.withMetadataFilter(
PropertyFilter.ne("status", "archived")
.and(PropertyFilter.eq("team", "platform"))
)
.withEntityFilter(
EntityFilter.hasAnyLabel("Chunk", "Fact", "Person", "Project")
)
// Add search hints
.withHint(
TryHyDE.usingConversationContext()
.withMaxWords(60)
)
// Add monitoring
.withListener { event ->
println("Search: ${event.query} → ${event.results.size} results in ${event.runningTime}")
}
// Set goal
.withGoal(
"Use this tool to find platform team information including documentation, " +
"team members, projects, and technical facts. Try both vector and text search " +
"for comprehensive coverage."
)
// Expose to LLM
val tools = comprehensiveRag.tools()Integrate with Spring AI framework for vector store operations.
import com.embabel.agent.rag.service.spring.*
import org.springframework.ai.vectorstore.VectorStore
// Wrap Spring AI VectorStore
val springVectorStore: VectorStore = // Spring AI configuration
val ragSearch = SpringVectorStoreVectorSearch(springVectorStore)
// Use with ToolishRag
val ragTool = ToolishRag(
name = "spring_search",
description = "Search using Spring AI vector store",
searchOperations = ragSearch
)import com.embabel.agent.rag.filter.*
import com.embabel.agent.rag.service.spring.*
import org.springframework.ai.vectorstore.filter.Filter
// Create RAG filter
val ragFilter = PropertyFilter.eq("status", "active")
.and(PropertyFilter.gte("priority", 5))
.and(PropertyFilter.`in`("team", "platform", "infrastructure"))
// Convert to Spring AI expression
val springExpression = ragFilter.toSpringAiExpression()
// Use directly with Spring AI VectorStore
val request = SearchRequest.query("search text")
.withFilterExpression(springExpression)
.withTopK(10)
val documents = springVectorStore.similaritySearch(request)import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.ai.vectorstore.VectorStore
import com.embabel.agent.rag.service.spring.SpringVectorStoreVectorSearch
import com.embabel.agent.rag.tools.ToolishRag
@Configuration
class RagConfiguration {
@Bean
fun ragVectorSearch(springVectorStore: VectorStore): FilteringVectorSearch {
return SpringVectorStoreVectorSearch(springVectorStore)
}
@Bean
fun documentationSearch(ragVectorSearch: FilteringVectorSearch): ToolishRag {
return ToolishRag(
name = "docs_search",
description = "Search technical documentation",
searchOperations = ragVectorSearch
).withHint(TryHyDE.usingConversationContext())
}
@Bean
fun llmTools(
documentationSearch: ToolishRag,
entitySearch: ToolishRag
): List<Tool> {
return documentationSearch.tools() + entitySearch.tools()
}
}Configure multiple RAG tools for different data sources.
// Documentation repository
val docsRag = ToolishRag(
name = "documentation",
description = "Search technical documentation and guides",
searchOperations = docsRepository
).withMetadataFilter(
PropertyFilter.eq("type", "documentation")
)
// Code examples repository
val codeRag = ToolishRag(
name = "code_examples",
description = "Search code snippets and examples",
searchOperations = codeRepository
).withMetadataFilter(
PropertyFilter.eq("type", "code")
).withHint(
TryHyDE.withContext("Code examples and implementation patterns")
)
// Entity repository
val entityRag = ToolishRag(
name = "entities",
description = "Search people, projects, and teams",
searchOperations = entityRepository
).withSearchFor(
vectorSearchFor = listOf(NamedEntity::class.java),
textSearchFor = listOf(NamedEntity::class.java)
).withEntityFilter(
EntityFilter.hasAnyLabel("Person", "Project", "Team")
)
// Combine all tools
val allTools = docsRag.tools() + codeRag.tools() + entityRag.tools()
println("Total tools available to LLM: ${allTools.size}")// Create tools with different configurations for different contexts
fun createContextualRag(context: String): ToolishRag {
return when (context) {
"security" -> ToolishRag(
name = "security_search",
description = "Search security documentation and guidelines",
searchOperations = searchOperations
).withMetadataFilter(
PropertyFilter.eq("category", "security")
)
"api" -> ToolishRag(
name = "api_search",
description = "Search API documentation and examples",
searchOperations = searchOperations
).withMetadataFilter(
PropertyFilter.eq("category", "api")
).withGoal("Find API endpoints, parameters, and response formats")
else -> ToolishRag(
name = "general_search",
description = "Search all documentation",
searchOperations = searchOperations
)
}
}// Create RAG tool with dynamic filter updates
class DynamicRagTool(
private val baseRag: ToolishRag
) {
private var currentFilter: PropertyFilter? = null
fun updateFilter(filter: PropertyFilter?) {
currentFilter = filter
}
fun getTools(): List<Tool> {
return if (currentFilter != null) {
baseRag.withMetadataFilter(currentFilter!!).tools()
} else {
baseRag.tools()
}
}
}
// Use
val dynamicRag = DynamicRagTool(baseRag)
// Update filter based on context
dynamicRag.updateFilter(
PropertyFilter.eq("department", "engineering")
)
val tools = dynamicRag.getTools()import java.time.Duration
import java.time.Instant
class CachedRagTool(
private val baseRag: ToolishRag,
private val cacheDuration: Duration = Duration.ofMinutes(5)
) {
private data class CacheEntry(
val results: String,
val timestamp: Instant
)
private val cache = mutableMapOf<String, CacheEntry>()
fun getToolsWithCache(): List<Tool> {
// Wrap the original tools with caching logic
return baseRag.tools()
}
fun clearCache() {
cache.clear()
}
fun cleanExpired() {
val now = Instant.now()
cache.entries.removeIf { (_, entry) ->
Duration.between(entry.timestamp, now) > cacheDuration
}
}
}// Add listener to handle empty results
val gracefulRag = ToolishRag(
name = "graceful_search",
description = "Search with fallback handling",
searchOperations = searchOperations
).withListener { event ->
if (event.results.isEmpty()) {
println("Warning: No results for query '${event.query}'")
println("Consider using text search or broadening the query")
}
}// Limit result formatting for large sets
val efficientFormatter = RetrievableResultsFormatter { results ->
val maxResults = 10
val displayResults = results.results.take(maxResults)
buildString {
appendLine("Showing ${displayResults.size} of ${results.results.size} results:")
appendLine()
displayResults.forEachIndexed { index, result ->
appendLine("${index + 1}. ${result.content.infoString(verbose = false)}")
}
if (results.results.size > maxResults) {
appendLine()
appendLine("... and ${results.results.size - maxResults} more results")
}
}
}// Validate filters before creating tool
fun createValidatedRag(
name: String,
description: String,
searchOperations: SearchOperations,
metadataFilter: PropertyFilter?
): ToolishRag? {
// Validate filter logic
if (metadataFilter != null) {
try {
// Test filter (implementation-specific)
println("Validating filter: $metadataFilter")
} catch (e: Exception) {
println("Invalid filter: ${e.message}")
return null
}
}
return ToolishRag(
name = name,
description = description,
searchOperations = searchOperations
).apply {
if (metadataFilter != null) {
withMetadataFilter(metadataFilter)
}
}
}