Cookbook: LLM Tool Integration

On this page

Practical recipes for wiring plugins into an LLM agent loop, all built strictly from what the source supports.

Recipe: Anthropic tool-use loop

Python · 20 lines
import anthropic
from zerda.plugins import PluginManager, ExecutionContext

manager = PluginManager(plugin_dirs=["./plugins"])
await manager.startup()

client = anthropic.Anthropic()
tools = manager.anthropic_tools()   # PluginRegistry.to_anthropic_tools(), enabled plugins only

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "Search for recent RAG papers."}],
)

for block in response.content:
    if block.type == "tool_use":
        result = await manager.execute(block.name, block.input, ExecutionContext())
        # feed `result` back as a tool_result content block in your next message

Since PluginRegistry.to_anthropic_tools() filters to config.enabled plugins only, disabling a plugin (plugin.config.enabled = False) is enough to hide it from the LLM without unregistering it.

Recipe: let the framework pick the plugin instead of the LLM

If you don't want the LLM to see raw tool schemas at all — e.g. a simpler assistant that just routes a free-text request to the right plugin — use auto_execute instead of exposing tools:

Python
result = await manager.auto_execute("summarize this document for me")

This is select_one() + execute() in one call, using the keyword (and optionally semantic) scoring described in AIPluginSelector. It returns None (not an exception) if no plugin scores above zero for the query, so always check for None before using the result.

Recipe: cost-aware selection

Python · 3 lines
from zerda.plugins import CostTier

cheap_plugin = manager.select_one("search the web", max_cost=CostTier.CHEAP)

AIPluginSelector.select() filters candidates by CostTier enum ordering (FREE < CHEAP < MODERATE < EXPENSIVE) before scoring, so this guarantees you never select a plugin more expensive than the tier you specify.

Recipe: semantic re-ranking with your own embedding model

Python · 4 lines
def my_embed_fn(text: str) -> list[float]:
    return my_embedding_model.embed(text)   # any function returning a fixed-length float vector

manager = PluginManager(plugin_dirs=["./plugins"], embed_fn=my_embed_fn)

With embed_fn supplied, AIPluginSelector._semantic_rerank blends keyword_score * 0.4 + cosine_similarity * 0.6 for every candidate. If embed_fn raises for any reason (network error, rate limit, etc.), the selector logs a warning and silently falls back to keyword-only scoring for that call — your application does not need to handle embedding failures itself.

Recipe: observability dashboard for a health-check endpoint

Python · 3 lines
@app.get("/plugins/health")
async def plugin_health():
    return manager.dashboard()

dashboard() returns a JSON-serializable dict combining per-plugin stats (get_stats()), per-plugin health scores (get_health()), and manager-wide execution metrics (metrics.summary()) — suitable to return directly from a health-check or admin endpoint.

Recipe: safe permission bootstrapping for untrusted plugin sources

Python · 8 lines
manager = PluginManager(plugin_dirs=["./third_party_plugins"], safe_mode=True)
await manager.startup()   # nothing auto-granted; every discovered plugin starts with zero permissions

for plugin in manager.registry.all():
    print(plugin.name, plugin.metadata.permissions)   # inspect before granting anything

# Explicitly review and grant only what you've decided to trust:
manager.grant_permission("trusted_search_plugin", "network_access")

This pattern — discover first, inspect declared permissions, grant selectively — is the only path in the source for auditing third-party plugin permission requests before they take effect, since safe_mode=False grants everything automatically at startup().