Tutorial 8: Python Tool Executors¶
Tool executors let a node call Python functions during its LLM conversation. When the model decides to use a tool, KeGAL calls the corresponding Python function, returns the result to the model, and continues the conversation. This loop repeats until the model produces a final text response — no extra code is required in the caller.
1. Basic: a single tool¶
Define the tool in YAML and wire up a Python function via tool_executors.
Step 1 — Declare the tool in YAML¶
models:
- llm: "ollama"
model: "qwen2.5:7b"
host: "http://localhost:11434"
tools:
- name: "get_weather"
description: "Returns the current weather for a given city."
parameters:
city:
type: "string"
description: "The city name."
required: ["city"]
prompts:
- template:
system_template:
role: |
You are a weather assistant. Use the get_weather tool to
answer weather questions.
prompt_template:
question: "{user_message}"
nodes:
- id: "weather_node"
model: 0
temperature: 0.0
max_tokens: 256
show: true
tools: ["get_weather"] # reference by tool name
prompt:
template: 0
user_message: true
edges:
- node: "weather_node"
Step 2 — Implement the executor¶
def get_weather(city: str) -> str:
# Call your actual weather API here
return f"The weather in {city} is sunny, 22°C."
Step 3 — Pass it to the compiler¶
from kegal import Compiler
with Compiler(
uri="weather.yml",
tool_executors={"get_weather": get_weather},
) as compiler:
compiler.user_message = "What's the weather in Rome?"
compiler.compile()
for msg in compiler.get_outputs().nodes[0].response.messages:
print(msg)
The tool name in
tool_executorsmust exactly matchnamein the YAMLtools:list. The function signature parameters must match the schema.
2. Intermediate: multiple tools¶
A node can have access to several tools. The model chooses which ones to call, and may call them multiple times in any order.
tools:
- name: "search_kb"
description: "Search the knowledge base for relevant documents."
parameters:
query:
type: "string"
description: "The search query."
required: ["query"]
- name: "get_product_price"
description: "Return the current price of a product by SKU."
parameters:
sku:
type: "string"
description: "Product SKU code."
required: ["sku"]
- name: "check_availability"
description: "Check whether a product is in stock."
parameters:
sku:
type: "string"
warehouse:
type: "string"
description: "Warehouse code (e.g. 'EU', 'US')."
required: ["sku", "warehouse"]
nodes:
- id: "sales_agent"
model: 0
temperature: 0.2
max_tokens: 512
show: true
tools: ["search_kb", "get_product_price", "check_availability"]
prompt:
template: 0
user_message: true
def search_kb(query: str) -> str:
results = knowledge_base.search(query)
return "\n".join(r["text"] for r in results[:3])
def get_product_price(sku: str) -> str:
price = product_db.get_price(sku)
return f"${price:.2f}"
def check_availability(sku: str, warehouse: str) -> str:
qty = inventory.get(sku, warehouse)
return f"{qty} units available" if qty > 0 else "Out of stock"
with Compiler(
uri="sales_agent.yml",
tool_executors={
"search_kb": search_kb,
"get_product_price": get_product_price,
"check_availability": check_availability,
},
) as compiler:
compiler.user_message = (
"Is SKU-4821 available in the EU warehouse, and what does it cost?"
)
compiler.compile()
3. Intermediate: tool results in the output¶
The compiler records every tool call and its result in
node.response.tool_results. Inspect them for debugging or logging.
with Compiler(uri="agent.yml", tool_executors=executors) as compiler:
compiler.user_message = "..."
compiler.compile()
node = compiler.get_outputs().nodes[0]
# Final text response from the model
for msg in node.response.messages or []:
print("Response:", msg)
# Tool calls and results
for tr in node.response.tool_results or []:
print(f"Tool: {tr['name']}({tr['input']}) → {tr['result']}")
4. Advanced: tool + structured output¶
Combine tool-calling with structured_output so the model produces a typed
response after using its tools.
tools:
- name: "search_kb"
description: "Search the knowledge base."
parameters:
query: { type: "string" }
required: ["query"]
nodes:
- id: "research_extractor"
model: 0
temperature: 0.0
max_tokens: 512
show: true
tools: ["search_kb"]
prompt:
template: 0
user_message: true
structured_output:
description: "Structured research findings"
parameters:
answer:
type: "string"
description: "The direct answer to the question."
sources_used:
type: "integer"
description: "Number of knowledge base documents consulted."
confidence:
type: "string"
enum: ["high", "medium", "low"]
required: ["answer", "sources_used", "confidence"]
with Compiler(uri="research.yml", tool_executors={"search_kb": search_kb}) as compiler:
compiler.user_message = "What are the main causes of supply chain disruption?"
compiler.compile()
data = compiler.get_outputs().nodes[0].response.json_output
print(data["answer"])
print(f"Confidence: {data['confidence']} ({data['sources_used']} sources)")
5. Advanced: tool + message passing pipeline¶
A tool-calling node passes its result to a downstream node for further processing. The tool calls happen inside the first node; only the final text response is forwarded.
flowchart LR
R["researcher\ntools + output: true"] -->|findings| A["analyst\ninput: true"]
nodes:
- id: "researcher"
model: 0
temperature: 0.1
max_tokens: 512
show: false
tools: ["search_kb", "get_document"]
message_passing:
output: true # final response is forwarded downstream
prompt:
template: 0
user_message: true
- id: "analyst"
model: 0
temperature: 0.5
max_tokens: 512
show: true
message_passing:
input: true
prompt:
template: 1 # uses {message_passing} — receives researcher's findings
6. Advanced: node-level tool scoping¶
Different nodes in the same graph can access different subsets of the
declared tools. A node only sees the tools listed in its own tools: field.
tools:
- name: "sql_query" ...
- name: "web_search" ...
- name: "send_email" ...
nodes:
- id: "data_agent"
tools: ["sql_query"] # database access only
- id: "research_agent"
tools: ["web_search"] # web search only
- id: "comms_agent"
tools: ["web_search", "send_email"] # search + email
This scoping is enforced at the YAML level — if a tool name is not in the
top-level tools: list, _validate_indices() raises ValueError at
Compiler construction time.
7. CLI: loading executors from a file with tools_module¶
When running kegal run from the command line there is no Python entry point
to pass tool_executors. Add a tools_module key to kegal.yml pointing to
a Python file. The CLI loads it automatically using importlib and passes the
executors to Compiler before the graph starts.
tools.py¶
The file must define a tool_executors dict at module level. No imports from
kegal are needed — it is plain Python.
# tools.py (lives next to kegal.yml)
def get_weather(city: str) -> str:
# Replace with a real API call
return f"Sunny, 22°C in {city}"
def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
# Replace with a real FX lookup
rate = 1.08 # example EUR → USD
return f"{amount * rate:.2f} {to_currency}"
tool_executors = {
"get_weather": get_weather,
"convert_currency": convert_currency,
}
kegal.yml¶
my_graph.yml (excerpt)¶
Declare each tool schema in YAML as normal — tools_module only wires up the
implementations, the schemas still live in the graph file.
tools:
- name: "get_weather"
description: "Return the current weather for a city."
parameters:
city: { type: string }
required: ["city"]
- name: "convert_currency"
description: "Convert an amount between two currencies."
parameters:
amount: { type: number }
from_currency: { type: string }
to_currency: { type: string }
required: ["amount", "from_currency", "to_currency"]
nodes:
- id: "assistant"
model: 0
temperature: 0.0
max_tokens: 512
show: true
tools: ["get_weather", "convert_currency"]
prompt:
template: 0
user_message: true
Run¶
Rules:
- tools_module is a path relative to the project directory (where kegal.yml lives).
- The file must exist — a missing file is a hard error printed to stderr (exit code 1).
- The file must define a non-empty tool_executors dict — anything else is a hard error.
- Tool names in tool_executors must exactly match names in the YAML tools: list.
Key points¶
- Tool names in
tool_executorsmust exactly match names in the YAMLtools:list. - The tool loop runs entirely inside the node — the caller never handles individual tool calls.
- A node may call tools multiple times per
compile(). All calls are recorded innode.response.tool_results. - Each node only sees the tools declared in its own
tools:list. - Tool executors must be thread-safe if parallel nodes share the same function.
- Combine with
structured_outputto get a typed final response after the tool loop completes.
Related tutorials: 09 MCP servers — out-of-process tools via the Model Context Protocol
12 ReAct loop — tools inside ReAct agent nodes
01 Message passing — forwarding tool results downstream