Zum Hauptinhalt springen
Connic
Build

Tool Hooks

Hooks führen vor und nach jedem Tool-Aufruf eigene Logik aus: Parameter validieren, Zugriffskontrollen durchsetzen, Ergebnisse verändern und Tool-Nutzung protokollieren.

Zuletzt aktualisiert

Was sind Tool Hooks?

Hooks, die vor und nach jedem Tool-Aufruf innerhalb eines Agenten ausgeführt werden

Tool-Hooks sind Python-Funktionen, die jeden Tool-Aufruf eines Agenten umschließen. Während Middleware einmal vor und nach der gesamten Ausführung des Agenten läuft, werden Tool-Hooks um jeden einzelnen Tool-Aufruf ausgeführt. Nutze sie für Zugriffskontrolle, Parametervalidierung, Ergebnistransformation und Logging.

Automatische Erkennung anhand des Namens des Agenten

Erstelle im Verzeichnis hooks/ eine Datei mit demselben Namen wie der Agent. So gilt hooks/order-manager.py beispielsweise für den Agenten, dessen YAML name: order-manager enthält. Eine YAML-Konfiguration ist nicht erforderlich. Middleware verwendet dieselbe Namenskonvention.

Ausführungsablauf

Jeder Tool-Aufruf ruft before(), dann das Tool und anschließend after() auf:

LLM ruft Tool auf
before()
Tool wird ausgeführt
after()
Ergebnis an LLM

Ruft der Agent in einem Run mehrere Tools auf, werden die Hooks für jedes davon unabhängig ausgelöst.

Middleware und Tool Hooks

Middleware umschließt den gesamten Agenten-Run – von der Anfrage bis zur Antwort. Tool-Hooks umschließen jeden einzelnen Tool-Aufruf innerhalb dieses Runs. Beides kann für denselben Agenten verwendet werden.

Einfacher Tool Hook

hooks/order-manager.py
"""Tool hooks for the order-manager agent."""
from typing import Any
from connic import AbortTool

async def before(tool_name: str, params: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
    """
    Called before every tool call.

    Args:
        tool_name: Name of the tool about to run (e.g. "get_order")
        params: Dict of parameters the LLM chose for the tool
        context: Shared run context dict (see Context docs)

    Returns:
        Parameter keys to add or replace. Return None to keep the original parameters.
    """
    # Block deletions for non-admin users
    if tool_name == "delete_order" and not context.get("is_admin"):
        raise AbortTool({"error": "Permission denied: only admins can delete orders"})

    return params

async def after(tool_name: str, params: dict[str, Any], result: Any, context: dict[str, Any]) -> Any:
    """
    Called after every tool call.

    Args:
        tool_name: Name of the tool that just ran
        params: The parameters the tool was called with
        result: The tool's return value
        context: Shared run context dict (see Context docs)

    Returns:
        Modified result (or original unchanged)
    """
    print(f"[hook] {tool_name}({params}) -> {result}")
    return result

Beide Hooks sind optional. Es kann nur before(), nur after() oder beides definiert werden. Auch der Parameter context ist optional. Lass ihn weg, wenn keine Run-Metadaten benötigt werden.

Funktionssignaturen

before()

ParameterTypBeschreibung
tool_namestrName des Tools, das gleich ausgeführt wird
paramsdictParameter, die das LLM für den Tool-Aufruf ausgewählt hat
contextdict (optional)Gemeinsamer Run-Kontext, dasselbe Dictionary wie in Middleware und Tools

Rückgabewert: ein Dictionary mit Parameterschlüsseln, die hinzugefügt oder ersetzt werden. Gib None zurück, um die ursprünglichen Parameter unverändert zu lassen.

after()

ParameterTypBeschreibung
tool_namestrName des gerade ausgeführten Tools
paramsdictParameter, mit denen das Tool aufgerufen wurde
resultAnyRückgabewert des Tools
contextdict (optional)Gemeinsamer Run-Kontext

Rückgabewert: das Ergebnis, das an das LLM zurückgegeben wird. Gib None zurück, um das ursprüngliche Ergebnis unverändert zu lassen.

Tool mit AbortTool überspringen

Löse AbortTool in before() aus, um das Tool vollständig zu überspringen und dem LLM ein eigenes Ergebnis zurückzugeben. Das Tool wird nicht ausgeführt und der Trace markiert den Call als Fehler.

hooks/order-manager.py
"""Block specific tools based on runtime conditions."""
from typing import Any
from connic import AbortTool

async def before(tool_name: str, params: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
    # Block destructive tools outside business hours
    from datetime import datetime
    hour = datetime.now().hour

    destructive = {"delete_order", "db_delete", "cancel_subscription"}
    if tool_name in destructive and not (9 <= hour < 17):
        raise AbortTool({
            "error": f"Tool '{tool_name}' is only available during business hours (9am-5pm)"
        })

    return params
AbortTool und StopProcessing

AbortTool überspringt nur den aktuellen Tool-Aufruf: Der Agent läuft weiter und kann andere Tools aufrufen oder antworten. StopProcessing bricht den gesamten Agenten-Run sofort ab.

Häufige Use Cases

Parameter validieren und normalisieren

Bereinige Tool-Parameter vor der Ausführung oder erzwinge Standardwerte.

hooks/order-manager.py
"""Normalise and validate tool parameters."""
from typing import Any

async def before(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
    # Normalise order IDs to uppercase
    if "order_id" in params:
        params["order_id"] = params["order_id"].upper()

    # Enforce default limit on search tools
    if tool_name == "search_orders" and "limit" not in params:
        params["limit"] = 10

    return params

Sensible Daten aus Ergebnissen entfernen

Entferne PII oder sensible Felder aus Tool-Ergebnissen, bevor sie das LLM erreichen.

hooks/data-agent.py
"""Redact sensitive data from tool results."""
from typing import Any
import re

async def after(tool_name: str, params: dict[str, Any], result: Any, context: dict[str, Any]) -> Any:
    # Redact email addresses from results
    if isinstance(result, str):
        return re.sub(r'[\w.-]+@[\w.-]+\.\w+', '[REDACTED]', result)

    if isinstance(result, dict) and "email" in result:
        result["email"] = "[REDACTED]"

    return result

Run mit StopProcessing abbrechen

Verwende StopProcessing, wenn sich aus einem Tool-Aufruf ergibt, dass der gesamte Run beendet werden soll.

hooks/payments.py
"""Use StopProcessing to abort the entire run from a hook."""
from typing import Any
from connic import StopProcessing

async def before(tool_name: str, params: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
    # If a critical tool fails validation, stop the entire run
    if tool_name == "charge_customer":
        amount = params.get("amount", 0)
        if amount > 10000:
            raise StopProcessing("Transaction blocked: amount exceeds safety limit")

    return params

Tool Calls protokollieren

hooks/analytics-agent.py
"""Log all tool calls to an external service."""
from typing import Any
import httpx

async def after(tool_name: str, params: dict[str, Any], result: Any, context: dict[str, Any]) -> Any:
    try:
        async with httpx.AsyncClient() as client:
            await client.post("https://logs.internal/tool-calls", json={
                "run_id": context.get("run_id"),
                "agent": context.get("agent_name"),
                "tool": tool_name,
                "params": params,
            })
    except Exception:
        pass  # Don't fail the tool call if logging fails

    return result

Ausgaben über print und logging aus der Python-Standardbibliothek werden in Hooks genauso erfasst wie in Tools und Middleware. Sie erscheinen im Projekt auf dem Tab Logs und in der Run-Detailansicht mit der Quelle hook.<tool_name>. So lassen sich Tool-Aufrufe nachvollziehen, ohne Log-Daten an einen externen Dienst zu senden.

Optionaler Kontext-Parameter

Der Parameter context wird anhand der Funktionssignatur automatisch erkannt. Füge ihn für Run-Metadaten hinzu und lass ihn für einfachere Hooks weg. Dabei gilt dieselbe Konvention wie für Custom Tools.

hooks/example.py
"""The context parameter is optional."""
from typing import Any

# Without context - simpler signature
async def before(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
    return params

# With context - access run metadata and middleware values
async def after(tool_name: str, params: dict[str, Any], result: Any, context: dict[str, Any]) -> Any:
    run_id = context.get("run_id")
    return result

Projekt-Struktur

Projektstruktur
my-agent-project/
agents/
assistant.yaml
order-manager.yaml
hooks/
assistant.pyApplied to 'assistant' agent
order-manager.pyApplied to 'order-manager' agent
middleware/
assistant.pyMiddleware and hooks can coexist
tools/
...

Sync- und Async-Unterstützung

hooks/simple.py
"""Sync hooks also work."""
from typing import Any

def before(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
    """Sync functions are automatically handled."""
    if "query" in params:
        params["query"] = params["query"].strip()
    return params

def after(tool_name: str, params: dict[str, Any], result: Any) -> Any:
    """Both sync and async are supported."""
    return result

Verwende Async-Funktionen für I/O-Operationen (API-Aufrufe, Datenbankabfragen). Für einfache Validierungen und Transformationen sind Sync-Funktionen geeignet.

Fehlerbehandlung

Löst ein Hook eine unbehandelte Exception aus – also weder AbortTool noch StopProcessing –, schlägt der Tool-Aufruf fehl. Der Fehler wird an das LLM zurückgegeben, das den Call wiederholen oder mit einer Fehlermeldung antworten kann. Der Trace erfasst die Exception.

Geltungsbereich

Tool-Hooks gelten für alle in tools/ definierten Tools, für vordefinierte Tools und für API Spec Tools. Sie gelten nicht für Tools, die von externen MCP Servern bereitgestellt werden.