Skip to content

Wrap existing code

Use registry.wrap() for code you cannot or will not edit. The code stays as it is. All options go at the call site.

1. The existing code

# Existing code. None of it changes.
def get_articles(article_list: list[str], lang: str, debug: bool = False) -> dict[str, str]:
    """Return the text of help-center articles.

    Args:
        article_list: Article topics, e.g. ["returns", "shipping"].
        lang: Language code.
        debug: Verbose output.
    """
    return {article: f"Article {article} ({lang})" for article in article_list}


class RateService:
    """Shipping rates for one region."""

    def __init__(self, region: str) -> None:
        self.region = region

    def lookup(self, item: str) -> float:
        """Return the shipping rate for an item, in percent of the order value.

        Args:
            item: Item name.
        """
        return 18.0 if self.region == "us" else 5.0


def convert(amount: float, rate: float) -> float:
    """Convert a USD amount with an exchange rate.

    Args:
        amount: Amount in USD.
        rate: Units of the target currency per USD.
    """
    return round(amount * rate, 2)

2. Wrap it

registry.wrap(
    get_articles,
    description="Full text of help-center articles.",
    fixed={"lang": "en"},  # bound value; the LLM never sees it
    hide="debug",  # hidden; its default applies
    rename={"article_list": "articles"},  # the LLM sees `articles`
    result=lambda texts: list(texts.values()),  # adapt the return shape
    parallel_safe=True,
)

# A bound method: register obj.method, not the method in the class body.
registry.wrap(RateService("us").lookup, name="shipping_rate")

# A partial: its keyword bindings act like fixed= values.
registry.wrap(functools.partial(convert, rate=0.79), name="usd_to_gbp")

# One function, several tools with different fixed values.
registry.wrap(convert, name="usd_to_eur", fixed={"rate": 0.92})
flowchart LR
  subgraph PY["Python signature"]
    p1["article_list: list[str]"]
    p2["lang: str"]
    p3["debug: bool = False"]
  end
  subgraph LLM["What the LLM sees"]
    s1["articles: array of string"]
  end
  p1 -- "rename=" --> s1
  p2 -. "fixed= (en)" .-> H["hidden"]
  p3 -. "hide=" .-> H
Option Effect Rule
fixed={"lang": "en"} bound value, hidden from the LLM the name must be a parameter
hide="debug" (or a list) hidden; the default applies needs a plain default, not Field()
rename={"article_list": "articles"} the LLM sees articles cannot rename a fixed or hidden parameter
result=fn transforms the return value turns off result_schema()
name=, description= override the function name and docstring name matches [A-Za-z_][A-Za-z0-9_]*, max 64
policy options kind, tags, parallel_safe, ... type-checked at build time; enforced by the executor

3. Call it

spec = registry.get("get_articles")
print(list(spec.json_schema()["properties"]))  # ['articles']
print(dict(spec.llm_to_python))  # {'articles': 'article_list'}
executor = Executor(registry)  # every call goes through the executor's policies
articles = await executor.run("get_articles", {"articles": ["returns"]})
print(articles)  # ['Article returns (en)']
print(await executor.run("shipping_rate", {"item": "laptop"}))  # 18.0
print(await executor.run("usd_to_gbp", {"amount": 10}))  # 7.9
print(await executor.run("usd_to_eur", {"amount": 10}))  # 9.2

The model sends articles. The function receives article_list, plus lang="en" and debug=False. spec.llm_to_python holds the reverse map. Run every call through the executor, as here: spec.invoke skips approval, budgets and guards, so keep it for tests.

Methods, classes and blocking code

Register the bound method: registry.wrap(RateService("us").lookup). A whole legacy class, its blocking sync calls and its own exceptions are on Wrap a legacy class.

Partials

A functools.partial takes its name and docstring from the wrapped function. Its keyword bindings act like fixed=: hidden and never overridable.

  • Passing fixed= for a name the partial already binds raises ToolDefinitionError.
  • A partial must not bind the ToolContext parameter. The context is injected at call time.

Callable objects

An object with __call__ works too. It has no __name__, so pass name=. Its description comes from the class or __call__ docstring.

This page's code is examples/wrap_existing.py. The test suite runs it.