Skip to content
tnsaijava agent framework

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:

LayerModuleProvides
Primitivestnsai-core (com.tnsai.knowledge, com.tnsai.memory.advanced)KnowledgeBase, Document, EmbeddingFunction, BM25Index, VectorMemoryStore, HybridMemoryRetriever
Strategiestnsai-intelligence (com.tnsai.intelligence.rag)RAGPipeline, RAGStrategy, RetrievedDocument, RAGContext
Service / HTTPtnsai-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:

ConditionIllegalStateException
combined with knowledgeBase(...)chatKnowledge cannot be combined with legacy knowledgeBase
no retrieval(...) suppliedchatKnowledge requires a retrieval configuration
name matches no declared sourcechatKnowledge references unknown builder source
name declared more than oncemust be declared exactly once
source declared but disabledmust be enabled
retrieval topK not positivechatKnowledge retrieval topK must be positive
retrieval contextWindow not positivechatKnowledge 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:

pathsource described byresolved
@ChatKnowledge + @KnowledgeSourceannotationsat init
chatKnowledge(name)a declared KnowledgeSourceConfig + retrieval(...)at init; needs an installed RetrievalEngineProvider
liveChatKnowledge(name)nothing — the application supplies the bindingnever; 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 fieldEffectStatus
@ChatKnowledge.sourceSelects the named source on the same agent class.live
@ChatKnowledge.topKLimits documents retrieved for each chat turn.live
@ChatKnowledge.enabledEnables or bypasses chat-level retrieval.live

Knowledge-source fields

@KnowledgeSource fieldEffectStatus
nameDefines the source key used by retrieval policies.live
typeSelects a SourceLoader; only FILE is bundled.partial
formatSelects AUTO detection or strict per-file parsing for FILE sources.live
pathSupplies the file/directory path, or a provider-defined URL.live
connectionSupplies provider-defined database connection data.partial
querySupplies a provider-defined database ingestion query.partial
enabledIncludes or skips the source during binding.live
includeIncludes matching FILE paths before ingestion.live
excludeExcludes matching FILE paths before ingestion.live
unitSelects AUTO, DOCUMENT, LINE, HEADING, or CHUNK slicing at ingest. @since 0.15.0.live
chunkContextSelects NONE, STRUCTURAL, or LLM context prepended for indexing. LINE rejects non-NONE. @since 0.15.0.live
chunkContextModelSelects the generator model for chunkContext = LLM; ignored for NONE and STRUCTURAL. @since 0.15.0.live

Retrieval-policy fields

@Retrieval fieldEffectStatus
strategySelects 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
sourcesSelects named sources; empty means all configured sources.live
queryParamSelects a named action parameter when compiled with -parameters; blank preserves positional inference.live
rerankEnables provider-backed reranking.live
rerankerModelSelects the installed reranker provider and model.live
contextWindowCaps the rendered retrieval context in tokens.live
topKCaps total initial results from the selected strategy call.live
topNCaps results after reranking.live
minScoreDrops results below the relevance threshold.live
deduplicateEnables post-fusion duplicate removal.live
dedupeThresholdSets the duplicate similarity threshold.live
queryExpansionSelects the query-expansion mode.live
queryExpansionModelSelects the installed query-expansion provider and model.live
navigatorModelSelects the installed TreeNavigator when strategy is REASONING. Required on that path; blank or unmatched identifiers fail before onFailure. @since 0.14.0.live
expandedQueriesCaps generated query variants.live
includeSpecControls source/score metadata in rendered context.live
contextFormatDefines the validated context template.live
cacheEnables the bounded per-Role retrieval cache.live
cacheTTLSets cache lifetime in seconds.live
maxStalenessCaps stale-cache age for USE_CACHE.live
onFailureSelects the transport-failure policy.live

Pages

  • Knowledge Base — Create, populate, query a KnowledgeBase. @VectorMemory fail-loud SPI.
  • File formatsDocumentFormat vs KnowledgeType.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 REASONING descent (@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 FileIndexer over shared FileIngestionService, ignore rules, admission caps, hybrid search.