Knowledge Base & RAG
TnsAI provides a built-in Retrieval-Augmented Generation (RAG) system through the `KnowledgeBase` interface, `Document` model, and `@KnowledgeSource` annotation. Agents can retrieve relevant context from vector databases, files, URLs, or in-memory stores before making LLM calls.
Package: com.tnsai.knowledge
KnowledgeBase Interface
KnowledgeBase is the core abstraction for storing and searching documents. Implementations can use in-memory storage, vector databases (Pinecone, Weaviate, Milvus), full-text search (Elasticsearch, OpenSearch), or hybrid approaches. You program against this interface, and swap implementations without changing your agent code.
Methods
These are the operations every KnowledgeBase implementation must support. The most important ones are addDocument (to ingest content) and search (to retrieve relevant context for an LLM call).
| Method | Signature | Description |
|---|---|---|
addDocument | void addDocument(Document document) | Adds a single document. Throws KnowledgeBaseException on failure. |
addDocuments | default void addDocuments(List<Document> documents) | Adds multiple documents. Default implementation iterates addDocument. |
getDocument | Optional<Document> getDocument(String id) | Retrieves a document by ID. Returns empty if not found. |
removeDocument | boolean removeDocument(String id) | Removes a document by ID. Returns true if removed, false if not found. |
search | List<SearchResult> search(String query, int topK) | Natural language search. Returns results ordered by relevance (highest first). |
search | List<SearchResult> search(String query, int topK, Map<String, Object> filters) | Search with metadata filtering. Filter entries are key-value pairs that must match. |
searchByEmbedding | List<SearchResult> searchByEmbedding(float[] embedding, int topK) | Similarity search using a pre-computed embedding vector. |
size | int size() | Returns the total number of documents. |
isEmpty | default boolean isEmpty() | Returns true if the knowledge base has no documents. Delegates to size() == 0. |
clear | void clear() | Removes all documents from the knowledge base. |
contains | default boolean contains(String id) | Checks if a document with the given ID exists. Delegates to getDocument(id).isPresent(). |
Document
Document is an immutable value object representing a document or document chunk. Each document has:
- id -- Unique identifier (auto-generated UUID if not specified)
- content -- The text content (required, cannot be null or empty)
- metadata -- Arbitrary key-value pairs for filtering and context (immutable copy)
- embedding -- Optional vector representation for similarity search (defensive copy)
Factory Methods
The quickest way to create a Document is with the static of() methods. These are convenient for simple use cases where you do not need to set an explicit ID or embedding.
// Simple document (auto-generated ID)
Document doc = Document.of("This is the document content");
// Document with a single metadata entry
Document doc = Document.of("Product docs...", "source", "docs/product.md");Builder
For full control over the document's ID, metadata, and embedding vector, use the builder. This is the recommended approach when you need to attach metadata for filtered searches or pre-computed embeddings for similarity search.
Document doc = Document.builder()
.id("doc-001") // optional, UUID generated if omitted
.content("Product documentation...") // required
.metadata("source", "docs/product.md") // single entry
.metadata("category", "documentation") // chainable
.metadata(Map.of("version", "2.0")) // bulk metadata
.embedding(embeddingVector) // optional float[]
.build();Builder method content(String) throws NullPointerException if null. build() throws IllegalStateException if content is null or empty.
Accessors
These getter methods let you read the document's fields. Metadata is accessed through the getSpec methods, and embeddings are returned as defensive copies to preserve immutability.
| Method | Return Type | Description |
|---|---|---|
getId() | String | Unique document ID |
getContent() | String | Document text content |
getSpec() | Map<String, Object> | Unmodifiable metadata map |
getSpec(String key) | Object | Single metadata value, or null |
getSpec(String key, Class<T> type) | T | Type-safe metadata value, returns null if missing or wrong type |
hasEmbedding() | boolean | Whether an embedding is present |
getEmbedding() | float[] | Copy of embedding array, or null |
getEmbeddingDimension() | int | Embedding vector length, or 0 if none |
Immutable Copy with Embedding
Since Document is immutable, attaching an embedding returns a new Document instance rather than modifying the original. This is useful when you compute embeddings separately after initial document creation.
// Attach an embedding to an existing document (returns a new Document)
Document withVector = doc.withEmbedding(embeddingVector);Equality is based on id only.
SearchResult
SearchResult wraps a matched Document with a relevance score. Implements Comparable<SearchResult> -- natural ordering is by score descending (highest first).
| Method | Return Type | Description |
|---|---|---|
getDocument() | Document | The matched document |
getScore() | double | Relevance score (higher = more relevant) |
getContent() | String | Convenience: delegates to document.getContent() |
getDocumentId() | String | Convenience: delegates to document.getId() |
Constructor: new SearchResult(Document document, double score) -- document cannot be null.
List<SearchResult> results = knowledgeBase.search("query", 5);
for (SearchResult result : results) {
System.out.printf("Score: %.4f | %s%n", result.getScore(), result.getContent());
}InMemoryKnowledgeBase
InMemoryKnowledgeBase is a thread-safe, in-memory implementation suitable for testing and small datasets. It provides:
- ConcurrentHashMap storage for thread safety
- TF-IDF keyword search with stop-word removal for
search() - Cosine similarity for both TF-IDF vectors and raw embeddings (
searchByEmbedding) - Metadata filtering support
KnowledgeBase kb = new InMemoryKnowledgeBase();
kb.addDocument(Document.of("Java is a programming language"));
kb.addDocument(Document.of("Python is also a programming language"));
kb.addDocument(Document.builder()
.content("Rust is a systems programming language")
.metadata("category", "systems")
.build());
// Keyword search
List<SearchResult> results = kb.search("programming language", 5);
// Filtered search
List<SearchResult> filtered = kb.search("programming", 5,
Map.of("category", "systems"));
// Embedding search
List<SearchResult> similar = kb.searchByEmbedding(queryEmbedding, 3);For production workloads with large datasets, use a vector database implementation (Pinecone, Weaviate, Qdrant) instead.
@KnowledgeSource Annotation
Package: com.tnsai.annotations
Declarative configuration for RAG knowledge sources. Can be applied to types (agent classes) or methods (individual actions). Repeatable via @KnowledgeSources.
Targets: ElementType.TYPE, ElementType.METHOD
Retention: RetentionPolicy.RUNTIME
Fields
Each @KnowledgeSource annotation is configured through the fields below. At minimum you need name and type; the remaining fields let you tune connection details, retrieval parameters, and caching behavior.
| Field | Type | Default | Description |
|---|---|---|---|
name | String | (required) | Unique identifier for this knowledge source |
type | KnowledgeType | VECTOR_DB | The knowledge source type |
provider | String | "" | Vector database provider (for VECTOR_DB) |
index | String | "" | Index/collection name (for VECTOR_DB) |
path | String | "" | File path or URL (for FILE or URL) |
connection | String | "" | Database connection string (for DATABASE) |
query | String | "" | SQL query template with ${query} placeholder (for DATABASE) |
topK | int | 5 | Maximum number of results to retrieve |
minSimilarity | double | 0.7 | Minimum similarity score threshold (0.0--1.0) |
embeddingModel | String | "" | Embedding model name for vector search |
dimensions | int | 1536 | Embedding vector dimensions |
namespace | String | "" | Namespace/partition for multi-tenant sources |
filter | String | "" | Metadata filter in JSON format |
cache | boolean | true | Whether to cache retrieval results |
cacheTTL | int | 300 | Cache time-to-live in seconds |
enabled | boolean | true | Whether this source is active |
priority | int | 0 | Query priority (higher = queried first) |
KnowledgeType Enum
The type field of @KnowledgeSource determines where the framework looks for documents. Choose the type that matches your data source.
| Value | Description |
|---|---|
VECTOR_DB | Vector database (Pinecone, Weaviate, Qdrant, Chroma) |
FILE | Local file (JSON, YAML, TXT, PDF, DOCX) |
URL | Remote URL or REST API |
DATABASE | SQL or NoSQL database |
MEMORY | Agent's conversation memory |
WEB_SEARCH | Web search results |
CACHE | In-memory cache |
Annotation Examples
These examples show how to attach knowledge sources to an agent class or an individual action method. You can combine multiple sources on the same class using the repeatable annotation pattern.
// On a class -- multiple sources via @Repeatable
@KnowledgeSource(
name = "product-docs",
type = KnowledgeType.VECTOR_DB,
provider = "pinecone",
index = "products",
topK = 5,
minSimilarity = 0.8
)
@KnowledgeSource(
name = "faq",
type = KnowledgeType.FILE,
path = "knowledge/faq.json"
)
public class SupportAgent extends Agent { ... }
// On a method
@ActionSpec(type = ActionType.LLM_GENERATION, description = "Answer question")
@KnowledgeSource(name = "product-docs", topK = 3)
public String answerQuestion(String question) {
// Relevant context is automatically retrieved before the LLM call
}
// Database source
@KnowledgeSource(
name = "customer-data",
type = KnowledgeType.DATABASE,
connection = "jdbc:postgresql://localhost/mydb",
query = "SELECT content FROM docs WHERE content ILIKE '%${query}%' LIMIT 10"
)
// Web search source with caching
@KnowledgeSource(
name = "web-context",
type = KnowledgeType.WEB_SEARCH,
topK = 3,
cache = true,
cacheTTL = 600
)Integration with AgentBuilder
If you prefer programmatic configuration over annotations, you can attach a knowledge base directly through the AgentBuilder. This is useful when you want to populate the knowledge base dynamically at startup or share one instance across multiple agents.
Use .knowledgeBase() and .knowledgeBaseTopK() on AgentBuilder to attach a knowledge base programmatically:
KnowledgeBase kb = new InMemoryKnowledgeBase();
kb.addDocument(Document.of("Product X supports features A, B, C"));
kb.addDocument(Document.of("Pricing starts at $99/month"));
Agent agent = AgentBuilder.create()
.llm(llmClient)
.role(supportRole)
.knowledgeBase(kb) // attach the knowledge base
.knowledgeBaseTopK(3) // override default top-K (default: 5)
.build();Full RAG Example
This end-to-end example shows the complete RAG workflow: creating a knowledge base, adding documents with metadata, searching for relevant context, building an augmented prompt, and sending it to the agent. It also demonstrates filtered search to narrow results by metadata category.
// 1. Create and populate knowledge base
KnowledgeBase kb = new InMemoryKnowledgeBase();
kb.addDocuments(List.of(
Document.builder()
.content("Product X supports features A, B, and C.")
.metadata("source", "product-docs")
.metadata("category", "features")
.build(),
Document.builder()
.content("Pricing starts at $99/month for the Basic plan.")
.metadata("source", "pricing-page")
.metadata("category", "pricing")
.build(),
Document.builder()
.content("Enterprise plan includes SSO and dedicated support.")
.metadata("source", "pricing-page")
.metadata("category", "pricing")
.build()
));
// 2. Search for relevant context
String query = "What features does Product X have?";
List<SearchResult> context = kb.search(query, 3);
// 3. Build augmented prompt
String augmentedPrompt = "Context:\n" +
context.stream()
.map(r -> r.getContent())
.collect(Collectors.joining("\n")) +
"\n\nQuestion: " + query;
// 4. Send to agent
String answer = agent.chat(augmentedPrompt);
// Or use filtered search for specific categories
List<SearchResult> pricingResults = kb.search("plan cost", 3,
Map.of("category", "pricing"));Event System
The event system provides full observability into the agent lifecycle. Events use a sealed interface hierarchy with 20+ event types, enabling type-safe pattern matching.
Memory
TnsAI.Core provides a pluggable memory system for agent conversation history. The `MemoryStore` interface defines storage, retrieval, pruning, and search operations. Four implementations cover different persistence and sharing requirements. The `AgentBuilder.memoryStore()` method wires a store into an agent.