Build an agent that combines knowledge and tools¶
At the end of this tutorial the agent support answers a question from the handbook and, in the
same turn, creates a ticket when the handbook offers no solution. You have put the tools in one
toolset, taught the agent in which order to use them, and seen when a knowledge graph adds
something.
| Building blocks | Agents, Knowledge: index and graph. Tools: toolsets. Insight: traces |
| You need | a key with the capabilities agents, knowledge and tooling; for the trace in the check, trace storage must be on for your organisation (Read a trace) |
| Builds on | Give an agent knowledge: the search tool knowledge__handbook, INDEX_ID. Let an agent carry out an action: the connector api__create_ticket |
An agent with one search tool and an agent with one action are each simple. Together they raise the question this tutorial answers: who decides when the agent searches and when it acts? You do, in three places: the description of each tool, the agent's instruction and the limit on the number of tool rounds.
1. Put the tools in a toolset¶
curl -s -X POST "$CORE_URL/toolsets" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "support",
"description": "What the support assistant needs: the handbook and tickets.",
"tools": ["knowledge__handbook", "api__create_ticket"]}'
export TOOLSET_ID=tls_4Hw9...
A toolset holds patterns. An exact name is a pattern too; mcp__tickets__* would take in every
tool of the MCP server from the previous tutorial. Check what the set holds now:
curl -s "$CORE_URL/toolsets/$TOOLSET_ID/tools" -H "Authorization: Bearer $CORE_KEY"
You use a toolset instead of two single names on the agent because you can extend the set later without a new agent version. That is also the other side of it: an agent version stores the name of the set and not its content. When you want a version to fix exactly which tools run, name them one by one.
A toolset grants nothing. Every tool in it is checked against the key's policy on every call.
2. Store the order in the agent¶
curl -s -X POST "$CORE_URL/agents/$AGENT_ID/versions" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"model": "<a model id>",
"instructions": "You are the support assistant of Noordwind. Answer briefly.\n\n1. For every question, search the handbook first and refer to the passage with [n].\n2. If the handbook solves the question, do not create a ticket.\n3. If the handbook does not solve the question, or the customer asks for a ticket, create one. Put in the description what the customer reported and what the handbook says about it.\n4. Name the ticket number and say what the customer can expect.",
"temperature": 0.2,
"max_tool_rounds": 4,
"tools": [{"type": "toolset", "name": "support"}]}'
The instruction describes a way of working: search first, then decide, then act. That keeps the order in one place, and a trace shows you which step a turn skipped.
max_tool_rounds limits how often the model may call tools in one turn. This task needs two
rounds: searching and creating a ticket. Four leaves room for a second search and keeps a turn
from searching on and on. A turn that reaches the limit ends with status: incomplete.
3. Test the way without a ticket¶
curl -s -X POST "$CORE_URL/responses" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"agent": {"id": "support"},
"input": "I threw the box away. Can I still send my order back?"}'
The handbook says a return must be in its original packaging. The answer should therefore hold
one mcp_call, for knowledge__handbook, and no ticket.
4. Test the way with a ticket¶
curl -s -X POST "$CORE_URL/responses" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"agent": {"id": "support"},
"input": "My wind meter stopped measuring after six weeks. I want someone to look at this."}'
The handbook names the return period of 30 days and says nothing about a defect after that period. The customer also asks for help from a person. The answer should hold two tool calls.
Check that it works¶
The answer to step 4 (abridged):
{
"id": "resp_0KQ2mR...",
"status": "completed",
"output": [
{ "type": "mcp_call", "name": "knowledge__handbook", "server_label": "knowledge",
"status": "completed" },
{ "type": "mcp_call", "name": "api__create_ticket", "server_label": "api",
"arguments": "{\"subject\": \"Wind meter defective after six weeks\", \"description\": \"...\"}",
"status": "completed" },
{ "type": "message", "role": "assistant",
"content": [{ "type": "output_text", "text": "The return period of 30 days has passed [1]. I have created ticket T-1044 ..." }] }
]
}
The order in output is the order in which the agent worked. The trace shows per round what
happened:
curl -s "$CORE_URL/responses/resp_0KQ2mR.../trace" -H "Authorization: Bearer $CORE_KEY"
Each row is one model call, with the tool calls the model asked for under tool_calls. Read the
rows from top to bottom to see in which round the agent searched and in which it created the
ticket. When the agent does not search, or creates a ticket too soon, GET /traces/{call_id}
shows under document.interpretation which tools the model was offered and what it answered
(Read a trace).
When a knowledge graph belongs¶
For the handbook you need no graph. An index answers "which passage is about this", and that is what a handbook asks for.
A graph earns its place when a question is about a connection that is not in one passage. Noordwind sells products from dozens of suppliers, and warranty runs through the supplier. The question "which supplier does the Wind meter WM-2 come from, and what does the handbook say about warranty?" needs two kinds of knowledge: a fact from a list (who supplies what) and a passage from the handbook. An index over the supplier list finds at most rows that resemble the question. A graph knows the relation itself.
If you have no such list, skip this part. A graph asks for a schema and upkeep, and adds nothing for loose documents.
Create a collection and a graph that reads from it. The template graph/from-tables turns
every row of a table into entities and relations, with no model. It reads the columns supplier
and product as the types Company and Product with the relation supplies; the schema below
uses those names for that reason.
curl -s -X POST "$CORE_URL/knowledge/collections" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "suppliers", "description": "Noordwind supplier list"}'
export SUPPLIERS_ID=col_7Rk4...
curl -s -X POST "$CORE_URL/knowledge/graphs" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "suppliers",
"description": "Which supplier supplies which product.",
"visibility": "shared",
"schema": {
"entity_types": [
{"name": "Company", "description": "A supplier of Noordwind."},
{"name": "Product", "description": "A product Noordwind sells."}],
"relation_types": [
{"name": "supplies", "description": "The supplier supplies the product.",
"from": ["Company"], "to": ["Product"]}]},
"collection_ids": [{"collection_id": "'"$SUPPLIERS_ID"'"}],
"ingest_pipeline": {"template": "graph/from-tables"}}'
export GRAPH_ID=kg_5Wn1...
Put the list in. A CSV file with the columns supplier and product:
printf 'supplier,product\nVento BV,Wind meter WM-2\nVento BV,Rain gauge RM-1\nPolaris Instruments,Compass K-7\n' > suppliers.csv
curl -s -X POST "$CORE_URL/knowledge/collections/$SUPPLIERS_ID/sources/upload" \
-H "Authorization: Bearer $CORE_KEY" \
-F "file=@suppliers.csv"
GET /knowledge/graphs/$GRAPH_ID/sources shows the source with status: ready and a fact_count
greater than zero once the graph is filled.
Let the search tool search both. The template retrieval/hybrid-graph searches an index and a
graph and merges passages and facts:
curl -s -X POST "$CORE_URL/knowledge/pipelines/from-template" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"template": "retrieval/hybrid-graph", "name": "handbook-and-suppliers",
"sources": {"index": "'"$INDEX_ID"'", "graph": "'"$GRAPH_ID"'"}}'
Put the existing search tool on the new pipeline. You read the tool's id (rtl_...) with
GET /knowledge/retrieval-tools:
curl -s -X PATCH "$CORE_URL/knowledge/retrieval-tools/rtl_2Vc6..." \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"pipeline": {"pipeline_id": "pip_...", "version": 1},
"description": "Searches the Noordwind customer service handbook and knows which supplier supplies which product."}'
The tool is still called knowledge__handbook, so the toolset and the agent do not change. Now
ask the agent "Which supplier does the Wind meter WM-2 come from, and how does warranty work?".
The search tool's result then holds, besides passages, a fact such as
Vento BV -- supplies -> Wind meter WM-2.
More on schemas, your own mappings and reading facts out of running text is in Graphs through the API.
If it does not work¶
| You see | Cause | Fix |
|---|---|---|
404 on POST /responses |
the toolset support does not exist for your key |
check the name with GET /toolsets |
the same 404, while the toolset exists |
your key's policy refuses an index, collection or graph the pipeline of a search tool in the set names | read the rules about Knowledge indexes, Knowledge collections and Knowledge graphs with GET /key (Knowledge through the API) |
| the agent gets only one tool | the other pattern matches nothing, or your key's policy does not allow that tool | GET /toolsets/$TOOLSET_ID/tools, and the rules over Tools in GET /key |
status: incomplete |
the turn reached max_tool_rounds |
read the trace; raise the limit or sharpen the instruction |
| the agent creates a ticket without searching | the instruction or the ticket tool's description allows it | put the order in the instruction, and in the connector's description when it is meant to be used |
a source of the graph is failed with no_schema or no_pipeline |
no_schema: the graph has no schema. no_pipeline: the pipeline the graph pinned was deleted |
send the schema, or put an existing pipeline in ingest_pipeline, and read again (Read again) |
the graph stays empty and GET .../sources shows no sources |
the graph has no ingest_pipeline; it then reads no source at all |
set ingest_pipeline with PATCH and read again |
fact_count is 0 |
the columns are named something other than supplier and product |
rename the columns, or make a pipeline of your own with your own mapping (Choose the pipeline) |
Read on¶
- Tools in the console: the overview, a tool's page and trying it, and toolsets with their rules.
- Knowledge: an index or a graph.
- Evaluations through the API: storing the two test questions as a suite, so that every new version shows whether the behaviour holds.
- An agent as a tool: leaving a subtask to a second agent.
Next tutorial: Set up a new customer organisation.