Skip to content

Common Patterns

Scoped tool sets per agent trust level

Expose a narrow, read-only tool set to a fully autonomous agent and a wider read/write set to one operating under human review:

@expose_as_tool(actions=["list", "retrieve"], name_prefix="ro_article", registry=autonomous_registry)
class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
    ...

@expose_as_tool(actions=["list", "retrieve", "create", "update", "destroy"], registry=supervised_registry)
class ArticleViewSet(viewsets.ModelViewSet):
    ...

Human-in-the-loop confirmation before destructive tools

execute_tool doesn't have an opinion about confirmation flows — build it one layer up, in whatever code receives the model's tool call before handing it to execute_tool:

DESTRUCTIVE_ACTIONS = {"destroy", "update", "partial_update"}


def dispatch_tool_call(name, arguments, *, user, confirmed=False):
    tool = default_registry.get(name)
    if tool and tool.action in DESTRUCTIVE_ACTIONS and not confirmed:
        return {"requires_confirmation": True, "tool": name, "arguments": arguments}
    return execute_tool(name, arguments, user=user)

Rate-limiting agent tool calls

Since execute_tool dispatches through the real view, any throttle classes configured on the viewset (throttle_classes) apply automatically — no extra work needed. Configure them the same way you would for a regular API consumer.

Auditing every tool call

Wrap execute_tool at your call site to log every invocation — this package doesn't emit logs itself, keeping the audit trail's shape entirely under your control:

import logging

logger = logging.getLogger("llm_gateway.audit")


def audited_execute_tool(name, arguments, *, user):
    logger.info("tool_call", extra={"tool": name, "user": user.pk, "arguments": arguments})
    result = execute_tool(name, arguments, user=user)
    logger.info("tool_result", extra={"tool": name, "status": result.status_code})
    return result

Exposing a subset of a large ViewSet

If a viewset has many actions but only some make sense as agent tools, just list the ones you want:

@expose_as_tool(actions=["list", "retrieve"])  # no create/update/destroy exposed
class ArticleViewSet(viewsets.ModelViewSet):
    ...

The viewset continues to work exactly as before for normal HTTP clients — expose_as_tool only adds registry entries, it never changes the viewset's own behavior.

Generating a static tools.json for a non-Python agent runtime

python manage.py generate_llm_tools --format mcp > tools.json

Ship tools.json to a runtime written in another language — it's plain JSON with no Python-specific structure.