Skip to main content

Split a library between agents

By the end of this tutorial you will have two agents that each answer only from their own slice of a shared knowledge base — a billing agent grounded in a billing collection, a shipping agent grounded in a shipping collection, and no overlap between what either one can see.

Six steps:

  1. Create a collection per topic.
  2. Add a document to each.
  3. Confirm they're indexed.
  4. Give each collection its own search tool.
  5. Create an agent per tool.
  6. Ask each agent a question only its own topic can answer.

Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.

Prerequisites

  1. A credential. A nat_sk_… API key (or a session JWT from Auth) exported as NATURALI_TOKEN, and your client set up — the CLI, the SDK or plain curl:

    export NATURALI_TOKEN=nat_sk_...
    export NATURALI_API=https://api.naturali.ai/v1 # curl and tool-config examples
  2. A project with a working provider in it. Create a provider builds one — do it first if you haven't, and arrive with both ids exported:

    export PROJECT=proj_V1StGXR8Z5jdHi6B
    export PROVIDER=aip_V1StGXR8Z5jdHi6B
    export NATURALI_PROJECT=$PROJECT # lets the CLI omit --project-id

This tutorial uses inline text instead of uploaded files, so there is nothing else to have on hand. For chunking a large PDF library instead, see Search a library of PDFs — the collection and document calls there are the same ones used below.

1. Create a collection per topic

A collection is the unit retrieval is scoped to, so a "folder" in this library is one collection per topic — not a field inside a single collection. Two collections keep billing passages from ever surfacing on a shipping question, and vice versa.

naturali create-knowledge-collection \
--project-id "$PROJECT" \
--name billing \
--description 'Refunds, charges and invoices.'

naturali create-knowledge-collection \
--project-id "$PROJECT" \
--name shipping \
--description 'Delivery times and carriers.'
{
"id": "kcol_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "billing",
"document_count": 0
}

Export both ids — every later step needs them:

export COLLECTION_BILLING=kcol_V1StGXR8Z5jdHi6B
export COLLECTION_SHIPPING=kcol_V1StGXR8Z5jdHi6B # a different id than billing's

2. Add a document to each

A document can be inline content instead of an uploaded file — the simplest way to seed a topic with a policy paragraph.

naturali create-knowledge-document \
--project-id "$PROJECT" \
--collection-id "$COLLECTION_BILLING" \
--title 'Refund policy' \
--content 'Refunds are issued once a return is received. Store credit posts immediately; card refunds take 5 business days to appear.'

naturali create-knowledge-document \
--project-id "$PROJECT" \
--collection-id "$COLLECTION_SHIPPING" \
--title 'Shipping policy' \
--content 'Standard shipping takes 3-7 business days. Expedited shipping arrives in 1-2 business days for an extra $15.'
{
"id": "doc_V1StGXR8Z5jdHi6B",
"collection_id": "kcol_V1StGXR8Z5jdHi6B",
"filename": "Refund policy",
"status": "indexed",
"chunk_count": null
}

filename carries the title you sent — a document has no separate title field, so this doubles as its label. A short inline paragraph like this one typically finishes ingestion inside the request itself, which is why status already reads indexed here instead of pending.

export DOCUMENT_BILLING=doc_V1StGXR8Z5jdHi6B
export DOCUMENT_SHIPPING=doc_V1StGXR8Z5jdHi6B # a different id than billing's

3. Confirm they're indexed

A large upload ingests in the background (see step 3 of the PDF tutorial for polling a pending document to indexed). These two are already indexed from step 2, so reading either one back is just confirmation, not a wait:

naturali get-knowledge-document \
--project-id "$PROJECT" \
--collection-id "$COLLECTION_BILLING" \
--document-id "$DOCUMENT_BILLING"
{ "id": "doc_V1StGXR8Z5jdHi6B", "status": "indexed", "chunk_count": null }

Repeat for the shipping document. If either ever comes back pending — larger content, or a converter in the path — poll it, or subscribe a webhook to knowledge.document_ingested instead.

4. Give each collection its own search tool

An agent never binds to a collection directly — it calls a tool, and a tool can be any HTTP endpoint, including the platform's own :query endpoint. An http tool runs server-side, so an agent that calls it never leaves completed for requires_action the way a client-executed tool would.

Point one tool's execute.url at the billing collection and the other's at shipping — that fixed URL, baked in at creation time, is what actually scopes each tool to its topic. The model only ever supplies query (and optionally top_k); it never sees or chooses the collection.

naturali create-tool \
--project-id "$PROJECT" \
--name search-billing \
--type http \
--description 'Search the billing knowledge base for refund, charge and invoice questions.' \
--execute '{"url":"'"$NATURALI_API"'/projects/'"$PROJECT"'/knowledge/collections/'"$COLLECTION_BILLING"':query","method":"POST","headers":{"Authorization":"Bearer '"$NATURALI_TOKEN"'"}}' \
--parameters '{"type":"object","properties":{"query":{"type":"string"},"top_k":{"type":"integer"}},"required":["query"]}'

naturali create-tool \
--project-id "$PROJECT" \
--name search-shipping \
--type http \
--description 'Search the shipping knowledge base for delivery and carrier questions.' \
--execute '{"url":"'"$NATURALI_API"'/projects/'"$PROJECT"'/knowledge/collections/'"$COLLECTION_SHIPPING"':query","method":"POST","headers":{"Authorization":"Bearer '"$NATURALI_TOKEN"'"}}' \
--parameters '{"type":"object","properties":{"query":{"type":"string"},"top_k":{"type":"integer"}},"required":["query"]}'
{
"id": "tool_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "search-billing",
"type": "http",
"has_headers": true,
"status": "active"
}
export SEARCH_BILLING_TOOL=tool_V1StGXR8Z5jdHi6B
export SEARCH_SHIPPING_TOOL=tool_V1StGXR8Z5jdHi6B # a different id than billing's
Headers are write-only

has_headers: true is all a later read of the tool ever shows — the Authorization value itself is accepted on write and never returned. There is nothing to redact by hand later; the platform already never gives it back.

The raw :query response — chunks with content and score — is already something a model can read directly, so this tutorial leaves output_mapping unset. Reduce it if you want the model to see less: see Tools.

5. Create an agent per tool

Each agent gets exactly one search tool in its tool_bindings, and instructions that tell it what to do when that tool comes back empty — the same knowledge base has an answer for the other agent's question, but this agent has no way to reach it.

Tool-calling reliability depends on the model

Not every model behind a managed provider reaches for a tool as readily as the others — a smaller model can describe calling the tool in prose instead of actually invoking it, which silently defeats the grounding this tutorial is built on. Set model on the agent to one you've confirmed calls tools well — deepseek.v3.2 did so reliably in testing — rather than trusting whatever the provider's own default happens to be. Check the model catalog for what's available on your provider.

naturali create-agent \
--project-id "$PROJECT" \
--provider-id "$PROVIDER" \
--name billing-agent \
--model deepseek.v3.2 \
--instructions 'Answer billing questions using the search-billing tool. If it returns nothing relevant, say you do not have that information.' \
--tool-bindings '[{"tool_id":"'"$SEARCH_BILLING_TOOL"'"}]'

naturali create-agent \
--project-id "$PROJECT" \
--provider-id "$PROVIDER" \
--name shipping-agent \
--model deepseek.v3.2 \
--instructions 'Answer shipping questions using the search-shipping tool. If it returns nothing relevant, say you do not have that information.' \
--tool-bindings '[{"tool_id":"'"$SEARCH_SHIPPING_TOOL"'"}]'
{
"id": "agent_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "billing-agent",
"model": "deepseek.v3.2",
"tool_bindings": [{ "tool_id": "tool_V1StGXR8Z5jdHi6B" }],
"status": "active"
}
export BILLING_AGENT=agent_V1StGXR8Z5jdHi6B
export SHIPPING_AGENT=agent_V1StGXR8Z5jdHi6B # a different id than billing's

6. Ask each agent a question only its own topic can answer

Ask the billing agent about refunds, then about shipping. The first answer comes from the passage you indexed in step 2; the second proves the split — the agent has no tool that can reach the shipping collection, so it says so instead of guessing.

naturali create-generation \
--agent-id "$BILLING_AGENT" \
--action-id tutorial.split-library \
--messages '[{"role":"user","content":"How long do card refunds take to appear?"}]'
{
"status": "completed",
"text": "Card refunds typically take 5 business days to appear once your return is received."
}

Now ask the same agent about shipping:

naturali create-generation \
--agent-id "$BILLING_AGENT" \
--action-id tutorial.split-library \
--messages '[{"role":"user","content":"How long does expedited shipping take?"}]'
{
"status": "completed",
"text": "I don't have that specific shipping information — my knowledge base covers billing questions only."
}

That is the split working: the same question put to $SHIPPING_AGENT instead gets the "1-2 business days" answer from its own collection. Add a third topic by repeating steps 1, 2, 4 and 5 with a new name — collection, tool, agent — and the library grows without any existing agent gaining access to it.

What's next

  • Traces — see the search-billing tool call itself, not just its effect on the answer.
  • Search a library of PDFs — chunking strategies for when a topic's documents are longer than one paragraph.
  • Ingest images and audio — bring scanned receipts and recordings into either collection.
  • Toolsoutput_mapping, preset_parameters and MCP tools, beyond the plain http search tool built here.