mixle.task.toolcall module

distill_tool_caller – train a tiny model to do function calling, with the honesty contract.

Tool calling decomposes into two problems the task spine already solves, composed under one gate:

  • which tool (or none) – a calibrated classification student (solve() over the request text: conformal answer-or-escalate + optional OOD gate);

  • the arguments – one token-level extractor per tool (distill_extractor()), distilled from the teacher’s own argument fills.

teacher(request) -> {"tool": name, "args": {...}} ({"tool": None} for no-op) is whatever does the job today – a frontier LLM behind mixle.task.llm, an agent loop, or a rule. The returned ToolCaller emits a call ONLY when the selector is conformally confident AND every required argument extracts; anything else escalates to the teacher – a malformed or guessed call is never emitted silently. Escalations are harvested as traces for the next distillation round, exactly like solve.

This covers single-step function calling. Multi-step planning / problem decomposition is NOT claimed here: that needs trace-SFT on the causal LM (the primitives exist – LM.fit + the SFT loss-mask) and an execution-verified loop, and is tracked as its own rung.

class ToolSpec(name, args, required=None)[source]

Bases: object

One callable tool: its name and the argument fields to extract from the request text.

Parameters:
name: str
args: list[str]
required: list[str] | None = None
property required_args: list[str]
class ToolCaller(selector, extractors, tools, teacher, selection_agreement, n_requests=0, n_escalated=0, harvested=<factory>)[source]

Bases: object

A distilled function-caller: emit a call only when selection AND arguments are trustworthy.

Parameters:
selector: Solution
extractors: dict[str, Any]
tools: dict[str, ToolSpec]
teacher: Callable[[str], dict]
selection_agreement: float
n_requests: int = 0
n_escalated: int = 0
harvested: list[tuple[str, dict]]
try_local(request)[source]

The local decision alone: a trustworthy call / no-op, or None (= must escalate).

No teacher, no stats — this is what a server without the frontier can run.

Parameters:

request (str)

Return type:

dict[str, Any] | None

report()[source]
Return type:

dict[str, Any]

save(path)[source]

Persist selector + per-tool extractors + specs as one artifact directory; load() restores.

Parameters:

path (str)

Return type:

str

classmethod load(path, teacher, *, device='cpu')[source]

Reconstitute a serving ToolCaller from save() output plus the teacher fallback.

Parameters:
Return type:

ToolCaller

distill_tool_caller(teacher, requests, tools, *, seed=0, selector_kw=None, extractor_kw=None)[source]

Distill the teacher’s function-calling into a tiny selector + per-tool argument extractors.

Parameters:
  • teacher (Callable[[str], dict]) – teacher(request) -> {"tool": name-or-None, "args": {field: value}} — the frontier LLM / agent / rule currently doing the calling. It labels everything; it remains the fallback.

  • requests (Sequence[str]) – example request texts covering the tools.

  • tools (Sequence[ToolSpec]) – the tool specs (names + argument fields; required defaults to all).

  • extractor_kw (dict | None) – knobs forwarded to solve() and distill_extractor().

  • seed (int)

  • selector_kw (dict | None)

  • extractor_kw

Return type:

ToolCaller