Write and register tools¶
A local tool is a plain Python function in tools/. There is no decorator, no
registration call and no manifest — writing a tool is exactly as much work as
writing a function.
Write it¶
"""Curve fitting tools."""
from __future__ import annotations
import math
def power_law_fit(x: list, y: list) -> dict:
"""Fit y = C * x^a by ordinary least squares in log-log space.
Returns {"exponent", "coefficient", "r_squared", "n"}. Raises ValueError
if the inputs differ in length, contain fewer than two usable points, or
contain non-positive values — the log transform is undefined there, and a
silently dropped point is a silently biased fit.
"""
if len(x) != len(y):
raise ValueError(f"x and y must be the same length, got {len(x)} and {len(y)}")
pairs = [(xi, yi) for xi, yi in zip(x, y, strict=False) if xi > 0 and yi > 0]
if len(pairs) < 2:
raise ValueError(f"need at least 2 positive points, got {len(pairs)}")
log_x = [math.log(xi) for xi, _ in pairs]
log_y = [math.log(yi) for _, yi in pairs]
...
return {
"exponent": slope,
"coefficient": math.exp(intercept),
"r_squared": r_squared,
"n": len(pairs),
}
Register it¶
You already did. Every .py file in the configured directory is imported, and
every public top-level function becomes a tool:
local.power_law_fit(x: list, y: list) -> dict
Fit y = C * x^a by ordinary least squares in log-log space.
local.query_exoplanets(min_period: float = 0.1, ...) -> list
Query the NASA Exoplanet Archive for confirmed planets.
| Element | Becomes |
|---|---|
| Function name | Tool name (local.power_law_fit). |
| First docstring line | The summary a coding agent reads. |
| Parameters + type hints | JSON schema properties. |
| Parameters without a default | The schema's required list. |
Leading underscore (_helper) |
Not exported. |
Type hints become the schema¶
def query_exoplanets(
min_period: float = 0.1,
max_period: float = 100000.0,
max_rows: int = 5000,
) -> list:
produces
{
"type": "object",
"properties": {
"min_period": {"type": "number", "default": 0.1},
"max_period": {"type": "number", "default": 100000.0},
"max_rows": {"type": "integer", "default": 5000}
},
"required": []
}
Annotate every parameter. An unannotated parameter falls back to string,
which is rarely what you meant and produces confusing errors at the call site.
Call it from an agent¶
Arguments are keyword-only. Declare the tool on the node so the provenance gate can check that the call really happened:
Rules worth following¶
Raise instead of returning a sentinel
# Wrong: poisons the dataset silently.
def morgan_fingerprint(smiles: str) -> list:
mol = parse(smiles)
if mol is None:
return [0] * 2048
# Right: the run fails, and the failure is visible.
def morgan_fingerprint(smiles: str) -> list:
mol = parse(smiles)
if mol is None:
raise ValueError(f"could not parse SMILES: {smiles!r}")
An exception ends the run and fails execution_completed. A zero vector
ends up in the training set and gets published.
Also:
- Keep tools pure and deterministic where you can. A tool that reads global state is a tool whose result you cannot reproduce.
- Validate inputs at the boundary.
power_law_fitchecks lengths and positivity before touching a logarithm. - Return plain, JSON-able data. Dicts, lists and numbers survive the trace; bespoke objects do not.
- Write the docstring for a stranger. It is what a coding agent uses to decide whether this is the right tool, and it is all the documentation the tool will ever have.
Organising¶
One file per topic; the filename does not appear in the tool name, so keep names unique across the directory.
Private helpers just take an underscore:
Testing¶
Tools are ordinary functions, so test them ordinarily:
import math
import pytest
from tools.fitting import power_law_fit
def test_recovers_a_known_exponent():
x = [1, 2, 4, 8]
y = [xi ** 1.5 for xi in x]
fit = power_law_fit(x, y)
assert math.isclose(fit["exponent"], 1.5, abs_tol=1e-9)
assert fit["r_squared"] > 0.999
def test_rejects_non_positive_input():
with pytest.raises(ValueError):
power_law_fit([0, 1], [1, 2])
A tool with tests is the cheapest place in the whole system to catch a scientific error.