RAG
What it does
Retrieval-Augmented Generation (RAG) grounds chat turns or action calls in application-owned knowledge before the LLM runs. TnsAI keeps corpus loading, retrieval policy, and invocation wiring separate so applications can choose the entry point and strategy independently.
Where RAG lives in the framework
RAG is intentionally split across three modules so each layer can evolve independently:
| Layer | Module | Provides |
|---|---|---|
| Primitives | tnsai-core (com.tnsai.knowledge, com.tnsai.memory.advanced) | KnowledgeBase, Document, EmbeddingFunction, BM25Index, VectorMemoryStore, HybridMemoryRetriever |
| Strategies | tnsai-intelligence (com.tnsai.intelligence.rag) | RAGPipeline, RAGStrategy, RetrievedDocument, RAGContext |
| Service / HTTP | tnsai-server (com.tnsai.server.rag) | RagService, FileIndexer, CodeChunker, HybridRetriever, retrieval streams |
Embedded agents typically use Core primitives plus Intelligence strategies. TnsAI.Server adds HTTP endpoints, indexing, and stream-based retrieval.
Declarative (annotations)
Chat-level RAG combines @ChatKnowledge with a named @KnowledgeSource on
the same agent class. Action-level RAG places @KnowledgeSource on the Role
or on a non-Role SCOP object; @Retrieval may sit on the class or an
action method. Maven Central 0.14.1 runs that action path for a
non-Role target through SCOPBridge.executeAction.
SCOP chat grounding is the additive owner-aware
SCOPBridge.sendToLLM overload (also on
0.14.1). The three-argument path remains ungrounded. Do not use
sendToLLM as a stand-in for action @Retrieval.
The published-release checks for this section use the Maven Central version shown on Installation. Separate source checks keep the examples compatible with the immutable framework commit pinned by this repository.
Named queryParam selection requires consumer classes compiled with Java's
-parameters flag. In Maven, set <maven.compiler.parameters>true</maven.compiler.parameters>;
with javac, pass -parameters. A class-level @Retrieval(queryParam = "question")
also requires every action on that Role to expose a parameter named question.
If that contract differs by action, put @Retrieval on each method instead.
Missing names are configuration errors before onFailure applies.
package com.example.tnsai.docs;
import com.tnsai.agents.Agent;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.ChatKnowledge;
import com.tnsai.annotations.KnowledgeSource;
import com.tnsai.annotations.Retrieval;
import com.tnsai.enums.ActionType;
import com.tnsai.llm.LLMClient;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;
import java.util.List;
public final class RagTopicAnnotationsExample {
private RagTopicAnnotationsExample() {}
@ChatKnowledge(source = "faq", topK = 5)
@KnowledgeSource(name = "faq", path = "knowledge/faq")
public static final class SupportAgent extends Agent {
@Override
protected LLMClient getLLM() {
return new AnthropicClient("claude-sonnet-4-20250514");
}
@Override
protected List<Role> getRoles() {
return List.of(Role.create(SupportRole.class));
}
}
@KnowledgeSource(
name = "product-docs",
type = KnowledgeSource.KnowledgeType.FILE,
path = "knowledge/products")
@Retrieval(
strategy = Retrieval.Strategy.HYBRID,
sources = "product-docs",
queryParam = "question",
topK = 5)
public static final class SupportRole extends Role {
@Override
public RoleIdentity getIdentity() {
return new RoleIdentity("support", "Product support", "support");
}
@ActionSpec(
type = ActionType.LLM,
description = "Answer from product documentation")
public String answer(String question) {
return question;
}
}
}Programmatic (builder)
For the equivalent programmatic paths, provide an existing KnowledgeBase or
immutable source/retrieval configs. The application composition root also
supplies the required principal, liability sink, and authority scope:
package com.example.tnsai.docs;
import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.annotations.Retrieval;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.rag.KnowledgeSourceConfig;
import com.tnsai.rag.RetrievalConfig;
import com.tnsai.roles.Role;
public final class RagTopicBuilderExample {
private RagTopicBuilderExample() {}
public static Agent buildActionAgent(
LLMClient llmClient,
Role supportRole,
AgentPrincipal principal,
LiabilitySink liabilitySink,
AuthorityScope authorityScope
) {
return AgentBuilder.create()
.llm(llmClient)
.role(supportRole)
.addKnowledgeSource(
KnowledgeSourceConfig.file(
"product-docs", "knowledge/products"))
.retrieval(RetrievalConfig.builder()
.strategy(Retrieval.Strategy.HYBRID)
.sources("product-docs")
.queryParam("question")
.topK(5)
.build())
.principal(principal)
.liabilitySink(liabilitySink)
.authorityScope(authorityScope)
.build();
}
public static Agent buildCanonicalChatAgent(
LLMClient llmClient,
Role supportRole,
AgentPrincipal principal,
LiabilitySink liabilitySink,
AuthorityScope authorityScope
) {
return AgentBuilder.create()
.llm(llmClient)
.role(supportRole)
.addKnowledgeSource(
KnowledgeSourceConfig.file("faq", "knowledge/faq"))
.retrieval(RetrievalConfig.builder()
.sources("faq")
.topK(5)
.build())
.chatKnowledge("faq")
.principal(principal)
.liabilitySink(liabilitySink)
.authorityScope(authorityScope)
.build();
}
}chatKnowledge(sourceName) (@since 0.15.0) grounds chat in one named
builder source. Three calls do the work together: addKnowledgeSource
declares the source, retrieval supplies the retrieval behaviour and
top-K, and chatKnowledge names which declared source grounds
conversation.
The chat binding receives a source-scoped copy of that retrieval configuration. The action-level source list and retrieval configuration are left unchanged, so one agent can retrieve across several sources for its actions while chat stays bound to exactly one.
Naming the source is required rather than a convenience: action
retrieval may list several sources, and the builder never guesses which
of them should ground chat. build() rejects the combinations that
would otherwise leave chat quietly ungrounded:
| Condition | IllegalStateException |
|---|---|
combined with knowledgeBase(...) | chatKnowledge cannot be combined with legacy knowledgeBase |
no retrieval(...) supplied | chatKnowledge requires a retrieval configuration |
| name matches no declared source | chatKnowledge references unknown builder source |
| name declared more than once | must be declared exactly once |
| source declared but disabled | must be enabled |
retrieval topK not positive | chatKnowledge retrieval topK must be positive |
retrieval contextWindow not positive | chatKnowledge retrieval contextWindow must be positive |
The first row is the one to know when reading the older builder story
below: the two chat paths are mutually exclusive, not layered. For how
AgentBuilder.knowledgeBase(KnowledgeBase) behaved before it was
removed in 0.16.0, see Which one to use.
Three ways to ground chat
What separates them is what describes the source, and when it is resolved:
| path | source described by | resolved |
|---|---|---|
@ChatKnowledge + @KnowledgeSource | annotations | at init |
chatKnowledge(name) | a declared KnowledgeSourceConfig + retrieval(...) | at init; needs an installed RetrievalEngineProvider |
liveChatKnowledge(name) | nothing — the application supplies the binding | never; chat grounds when the binding arrives |
KnowledgeSourceConfig is a declarative descriptor — path, format,
connection, query. It cannot wrap a live in-memory object, which is the
whole reason the third row exists.
⚠️ Reaching for row two with a runtime corpus does not leave chat
ungrounded — it makes construction throw. chatKnowledge(name) demands
a matching declared source, a retrieval configuration and an installed
provider, and build() rejects the agent when any is missing. The
answer in that case is row three, not a workaround for row two.
Application-supplied live sources
AgentBuilder.liveChatKnowledge(sourceName) and
ChatKnowledgeBinding.live(...) are @since 0.16.0. See
Installation for the current published
coordinates.
liveChatKnowledge declares that chat grounds on a source the
application supplies at runtime. Initialization resolves nothing for it
and builds no retrieval engine, so no RetrievalEngineProvider is
required on its account. The agent starts ungrounded and stays that way
until a binding arrives. Action retrieval is untouched: declare
knowledgeSources(...) and retrieval(...) as usual if actions also
retrieve — that declaration does not feed chat, and this one does not
feed actions.
ChatKnowledgeBinding.live(ownerId, sourceName, topK, knowledgeBase)
adapts an application-owned corpus. It holds the corpus rather than
copying it, so documents added after binding are visible to retrieval,
and closing the binding does not close the corpus — that stays the
application's to manage, which matters when the same corpus is rebound
on every message. Retrieval is keyword-strategy, fail-open and uncached,
matching what KnowledgeBase.search actually does.
🔑 The name is load-bearing, and both halves must agree. The binding
must be owned by that agent and carry exactly the declared
sourceName, or the agent refuses it — the same check the declarative
path applies, so chat never grounds on a source nobody named. Write the
two halves together:
package com.example.tnsai.docs;
import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.knowledge.KnowledgeBase;
import com.tnsai.llm.LLMClient;
import com.tnsai.rag.ChatKnowledgeBinding;
import com.tnsai.roles.Role;
public final class LiveChatKnowledgeExample {
private LiveChatKnowledgeExample() {}
// Both halves must name the same source, or the agent refuses the
// binding. Declaring it once is the cheapest way to keep them equal.
private static final String SOURCE = "session-corpus";
public static Agent buildAgent(
LLMClient llmClient,
Role supportRole,
AgentPrincipal principal,
LiabilitySink liabilitySink,
AuthorityScope authorityScope
) {
return AgentBuilder.create()
.llm(llmClient)
.role(supportRole)
.liveChatKnowledge(SOURCE)
.principal(principal)
.liabilitySink(liabilitySink)
.authorityScope(authorityScope)
.build();
}
public static void groundChat(
Agent agent,
String ownerId,
KnowledgeBase applicationOwnedCorpus
) {
agent.setChatKnowledgeBinding(
ChatKnowledgeBinding.live(
ownerId, SOURCE, 5, applicationOwnedCorpus));
}
}A refused binding is quiet by design: the agent keeps answering, just ungrounded. If chat replies without using the corpus you attached, check that the declared name and the bound name are the same string before looking anywhere else.
Which one to use
Use chat-level RAG when every conversational turn should be grounded in the same knowledge base. It retrieves before the LLM call, fences retrieved content, and adds the matching disclosure to the system prompt.
Use action-level RAG when retrieval belongs to specialized actions. It runs
during action dispatch and writes bounded context to _rag_context.
Explicit builder values win over annotations. A builder source replaces an
annotation source with the same name; other named sources remain available.
AgentBuilder.knowledgeBase(KnowledgeBase) and
knowledgeBaseTopK(int) are removed in 0.16.0, with
Agent.setKnowledgeBase / getKnowledgeBase, their orchestrator
equivalents, and ChatKnowledgeBinding.snapshot. They remain in Maven
Central 0.15.1, where they installed a snapshot binding under
source=knowledge-base and won over @ChatKnowledge at init.
The KnowledgeBase type is not removed — only the bridge that
attached one to chat. Declare the source and name it with
chatKnowledge(...), or, for a corpus the application owns and keeps
mutating, use liveChatKnowledge(...) with ChatKnowledgeBinding.live(...)
(@since 0.16.0).
TnsAI 0.14.0 field reference
live means the runtime consumes the field, partial means only the named
subset is available, and not implemented means changing the field has no
runtime effect. Every field carries the @since version that introduced
it, and the pages that introduce them repeat it — read the @since
rather than assuming the table tracks one release.
Chat-level fields
| Annotation field | Effect | Status |
|---|---|---|
@ChatKnowledge.source | Selects the named source on the same agent class. | live |
@ChatKnowledge.topK | Limits documents retrieved for each chat turn. | live |
@ChatKnowledge.enabled | Enables or bypasses chat-level retrieval. | live |
Knowledge-source fields
@KnowledgeSource field | Effect | Status |
|---|---|---|
name | Defines the source key used by retrieval policies. | live |
type | Selects a SourceLoader; only FILE is bundled. | partial |
format | Selects AUTO detection or strict per-file parsing for FILE sources. | live |
path | Supplies the file/directory path, or a provider-defined URL. | live |
connection | Supplies provider-defined database connection data. | partial |
query | Supplies a provider-defined database ingestion query. | partial |
enabled | Includes or skips the source during binding. | live |
include | Includes matching FILE paths before ingestion. | live |
exclude | Excludes matching FILE paths before ingestion. | live |
unit | Selects AUTO, DOCUMENT, LINE, HEADING, or CHUNK slicing at ingest. @since 0.15.0. | live |
chunkContext | Selects NONE, STRUCTURAL, or LLM context prepended for indexing. LINE rejects non-NONE. @since 0.15.0. | live |
chunkContextModel | Selects the generator model for chunkContext = LLM; ignored for NONE and STRUCTURAL. @since 0.15.0. | live |
Retrieval-policy fields
@Retrieval field | Effect | Status |
|---|---|---|
strategy | Selects semantic, keyword, hybrid, graph, multi-query, hierarchical, temporal, or reasoning retrieval. REASONING is @since 0.14.0 and is in Maven Central 0.14.1. | live |
sources | Selects named sources; empty means all configured sources. | live |
queryParam | Selects a named action parameter when compiled with -parameters; blank preserves positional inference. | live |
rerank | Enables provider-backed reranking. | live |
rerankerModel | Selects the installed reranker provider and model. | live |
contextWindow | Caps the rendered retrieval context in tokens. | live |
topK | Caps total initial results from the selected strategy call. | live |
topN | Caps results after reranking. | live |
minScore | Drops results below the relevance threshold. | live |
deduplicate | Enables post-fusion duplicate removal. | live |
dedupeThreshold | Sets the duplicate similarity threshold. | live |
queryExpansion | Selects the query-expansion mode. | live |
queryExpansionModel | Selects the installed query-expansion provider and model. | live |
navigatorModel | Selects the installed TreeNavigator when strategy is REASONING. Required on that path; blank or unmatched identifiers fail before onFailure. @since 0.14.0. | live |
expandedQueries | Caps generated query variants. | live |
includeSpec | Controls source/score metadata in rendered context. | live |
contextFormat | Defines the validated context template. | live |
cache | Enables the bounded per-Role retrieval cache. | live |
cacheTTL | Sets cache lifetime in seconds. | live |
maxStaleness | Caps stale-cache age for USE_CACHE. | live |
onFailure | Selects the transport-failure policy. | live |
Pages
- Knowledge Base — Create, populate, query a
KnowledgeBase.@VectorMemoryfail-loud SPI. - File formats —
DocumentFormatvsKnowledgeType.FILE, selectors, ignore/Limits, optional PDF/Office. - Embeddings Matryoshka prefix helper (
@since 0.14.0). Hash-fallback warning (@since 0.15.0). - Diagnostics resolved RAG config (
RagDiagnostics,@since 0.15.0). - Ordered consumption shared target/source cursor
(
@Sequential,@since 0.15.0). - Agent-level action RAG
@AgentSpec.knowledge()/retrieval()(@since 0.14.0). - Strategies — Swap retrieval algorithms via the RAG SPI.
- Hybrid graph stream third HYBRID ranking (
@since 0.14.0). - Vectorless reasoning — Root-to-leaf
REASONINGdescent (@since 0.14.0). - Smart segmentation heading-aware paper splitter (
@since 0.14.0). Not the FILE chunker. - Controlled write-back package-private overlay (
TnsAI@4b57a851). Default off. Not a public write API. - Selective re-embed hash-keyed local index (
@since 0.14.0). Not Qdrant/pgvector. - Pipeline — Server
FileIndexerover sharedFileIngestionService, ignore rules, admission caps, hybrid search.
Advanced Intelligence Patterns
Advanced cognitive capabilities in TnsAI.Intelligence for reasoning, memory consolidation, output validation, and iterative refinement.
Knowledge Base & RAG
TnsAI provides a built-in Retrieval-Augmented Generation (RAG) system through the KnowledgeBase interface, Document model, and @KnowledgeSource annotation.