Zum Hauptinhalt springen
connic
Build

Middleware

Middleware führt vor und nach der Agent-Ausführung eigene Logik aus: Dokumente anhängen, Kontext anreichern, Eingaben validieren und Antworten transformieren.

Zuletzt aktualisiert

Was ist Middleware?

Hooks, die vor und nach der Verarbeitung einer Anfrage durch den Agenten ausgeführt werden

Middleware sind Python-Funktionen, die Anfragen abfangen, bevor sie den Agenten erreichen, und Antworten, bevor sie zurückgegeben werden. Damit lassen sich Dokumente anhängen, Kontext anreichern, Eingaben validieren, Interaktionen protokollieren oder Antworten transformieren. Middleware und Tool-Hooks können vordefinierte Tools aus connic.tools aufrufen, auch die Browser-Tools. Ein in before() geöffneter Browser nutzt das gespeicherte Browserprofil der Session, wenn sich der Session-Schlüssel zu diesem Zeitpunkt schon auflösen lässt.

Automatische Erkennung anhand des Namens des Agenten

Erstelle im Verzeichnis middleware/ eine Datei mit demselben Namen wie der Agent. So gilt middleware/assistant.py beispielsweise für den Agenten, dessen YAML name: assistant enthält – selbst wenn diese YAML-Datei in einem Unterordner von agents/ liegt. Eine YAML-Konfiguration ist nicht erforderlich.

Ausführungsablauf

Anfrage
before()
Agent
after()
cleanup()
Antwort

Einfache Middleware

middleware/assistant.py
"""Middleware for the assistant agent."""
import json
from datetime import datetime
from typing import Any, Dict

async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    """
    Called before the agent runs.

    Args:
        content: Dict with "role" and "parts" keys
        context: Shared run context dict (see Context docs)

    Returns:
        Modified content to pass to the agent
    """
    print(f"Before middleware running for run {context.get('run_id')}")

    # Add current timestamp so the agent knows today's date
    now = datetime.now()
    time_context = f"[Current time: {now.strftime('%A, %B %d, %Y')}]"
    content["parts"].insert(0, {"text": time_context})

    return content

async def after(response: str, context: Dict[str, Any]) -> str:
    """
    Called after the agent completes.

    Args:
        response: The agent's response text
        context: Shared run context dict (see Context docs)

    Returns:
        Modified response (or original)
    """
    print(f"After middleware: response length: {len(response)}")

    # Wrap response in structured JSON with metadata
    return json.dumps({
        "response": response,
        "run_id": context.get("run_id"),
        "duration_ms": context.get("duration_ms"),
    })

Jeder Hook ist optional. Definiere eine beliebige Kombination aus before(), after() und cleanup().before() muss Content zurückgeben; der Rückgabewert von after() wird zur Antwort.

Das Content-Dictionary

Der Hook before() erhält ein content-Dictionary, das die Nachricht des Benutzers darstellt. Es enthält eine role (immer 'user') und eine Liste von parts. Jeder Part ist ein Dictionary mit Text oder Binärdaten.

middleware/example.py
# The content dict structure
content = {
    "role": "user",         # Immer 'user': before() erhält die eingehende Nachricht
    "parts": [              # List of part dicts
        {"text": "Hello, analyze this document"},
        {"data": b"...", "mime_type": "application/pdf"},
    ]
}

# Accessing content parts
for part in content["parts"]:
    if "text" in part:
        # Text content
        text = part["text"]
    elif "data" in part:
        # Binary file content
        mime_type = part["mime_type"]
        data = part["data"]

Die ursprüngliche Payload

Die rohe Payload der Verbindung ist ebenfalls als context["payload"] verfügbar. Verwende sie für Request-Metadaten, Authentifizierungsfelder, JSON-Bodys von Webhooks, Formularfelder oder GET-Query-Parameter. Verwende content, um den sichtbaren Agent-Input zu ändern.

middleware/example.py
# The original connector payload is available in context
payload = context.get("payload", {})

# For a JSON webhook body:
#   {"auth_token": "abc", "question": "Hello"}
# payload["auth_token"] == "abc"
# payload["question"] == "Hello"

# For a GET webhook:
#   <webhook-url>?user_id=123&query=hello
# payload["user_id"] == "123"
# payload["query"] == "hello"

Parts erstellen

Erstelle Parts als einfache Dictionaries: mit text für Textinhalte oder mit data und mime_type für Dateien.

middleware/example.py
# Creating parts as dicts

# Text part
text_part = {"text": "Analyze this document"}

# File from bytes (PDFs, images, audio, video)
with open("document.pdf", "rb") as f:
    pdf_part = {"data": f.read(), "mime_type": "application/pdf"}

# Image from bytes
with open("image.png", "rb") as f:
    image_part = {"data": f.read(), "mime_type": "image/png"}

# Adding parts to content
content["parts"].append(pdf_part)
content["parts"].insert(0, text_part)

Häufige Use Cases

Dokumente dynamisch anhängen

Füge dem Kontext des Agenten abhängig von der Anfrage PDFs, Bilder oder andere Dateien hinzu.

middleware/document-agent.py
"""Attach documents dynamically based on request context."""
from typing import Any, Dict

async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    """Attach relevant documents to the agent context."""

    # Get the user's text message
    user_text = ""
    for part in content["parts"]:
        if "text" in part:
            user_text = part["text"]
            break

    # Attach a retrieval document based on topic
    if "pricing" in user_text.lower():
        with open("docs/pricing.pdf", "rb") as f:
            content["parts"].append({
                "data": f.read(),
                "mime_type": "application/pdf"
            })

    elif "technical" in user_text.lower():
        with open("docs/technical-specs.pdf", "rb") as f:
            content["parts"].append({
                "data": f.read(),
                "mime_type": "application/pdf"
            })

    return content

Kontext und Kundendaten ergänzen

Ergänze die Anfrage um benötigte Kontextinformationen oder Daten aus externen APIs.

middleware/support-agent.py
"""Add system context and customer data to requests."""
from typing import Any, Dict
import httpx

async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    """Prepend system context to the user message."""

    # Read request data from the original connector payload.
    payload = context.get("payload", {})
    customer_id = payload.get("customer_id")

    # Fetch customer data from your API
    customer_info = ""

    if customer_id:
        print(f"Fetching customer data for {customer_id}")
        async with httpx.AsyncClient() as client:
            resp = await client.get(f"https://api.example.com/customers/{customer_id}")
            if resp.status_code == 200:
                data = resp.json()
                customer_info = f"""
Customer: {data['name']}
Plan: {data['plan']}
Account Status: {data['status']}
"""
                print(f"Loaded customer: {data['name']} ({data['plan']})")

    # Prepend context as text part
    content["parts"].insert(0, {
        "text": f"[CUSTOMER CONTEXT]\n{customer_info}\n[END CONTEXT]"
    })

    return content

Nachrichtentext ersetzen

Hochgeladene Dateien kommen als eigene Parts neben dem Text-Part an. Um zu ändern, was der Agent liest, ersetze nur den Text-Part. Wenn du content["parts"] auf eine reine Textliste setzt, gehen alle Anhänge verloren.

middleware/chat-agent.py
"""Show the agent only the chat message and keep uploaded files."""
from typing import Any, Dict

async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    # Webhook body: {"user_id": "u123", "message": "What is in this file?"} plus uploads
    payload = context.get("payload", {})
    context["user_id"] = payload.get("user_id")

    # Replace only the text part; file parts stay in the list
    files = [part for part in content["parts"] if "text" not in part]
    content["parts"] = [{"text": payload.get("message", "")}, *files]

    return content

Sauberer Abbruch mit StopProcessing

middleware/auth.py
"""Authentication middleware - stops processing if invalid."""
from typing import Any, Dict
from connic import StopProcessing

async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    """Validate request before agent runs."""

    # Read the original connector payload. For webhooks, this is the JSON body,
    # form fields, or GET query parameters.
    payload = context.get("payload", {})
    auth_token = payload.get("auth_token")

    if not auth_token:
        raise StopProcessing("Authentication required")

    # Store verified request data in context for prompts and tools.
    context["user_id"] = verify_auth_token(auth_token)

    return content

StopProcessing und Exceptions: StopProcessing beendet die Verarbeitung mit einer regulären Antwort (der Run wird als abgeschlossen markiert). Normale Exceptions markieren den Run als fehlgeschlagen. Da der Run abgeschlossen wird, stellen automatische ausgehende Verbindungen ihn standardmäßig weiterhin zu; die StopProcessing-Antwort ist dabei die Ausgabe. Übergib publish_outbound=False, um die automatische ausgehende Verbindung für diesen kontrollierten Abbruch zu überspringen. Dieses Flag bricht keinen Aufruf einer ausgehenden Agent-Tool- oder Middleware-Verbindung ab. Derselbe Mechanismus funktioniert für Tools in tools/. Siehe Tools schreiben

Analytics und Logging

middleware/analytics.py
"""Send analytics to external service after agent completes."""
import httpx
from typing import Any, Dict

async def after(response: str, context: Dict[str, Any]) -> str:
    """Send analytics after agent completes."""

    try:
        async with httpx.AsyncClient() as client:
            await client.post(
                "https://analytics.internal/events",
                json={
                    "event": "agent_run_completed",
                    "run_id": context.get("run_id"),
                    "agent": context.get("agent_name"),
                    "duration_ms": context.get("duration_ms"),
                    "tokens": context.get("token_usage", {}),
                }
            )
    except Exception:
        pass  # Don't fail the request if analytics fails

    return response

Ausgaben über print oder logging aus der Python-Standardbibliothek werden für jeden Run erfasst. Das gilt für alle Hooks, before(), after() und cleanup(). Die Ausgaben erscheinen im Projekt auf dem Tab Logs sowie in der Run-Detailansicht mit der Quelle middleware.before, middleware.after oder middleware.cleanup.

Ausgehende Middleware-Verbindungen

Setze den Modus einer ausgehenden Verbindung auf Middleware, vergib einen Aufrufnamen und rufe anschließend send_connector(action_name, payload) aus before() oder after() auf. Nutze diesen Modus, wenn Code statt des KI-Modells entscheiden soll, ob gesendet wird.

middleware/support-agent.py
"""Send an urgent result through a configured middleware outbound connector."""
from typing import Any, Dict
from connic.tools import send_connector

async def after(response: str, context: Dict[str, Any]) -> str:
    payload = context.get("payload", {})
    if payload.get("priority") == "urgent":
        await send_connector(
            "send_to_ops_slack",
            {"text": response},
        )

    return response

Die Payload muss dem dokumentierten Payload-Schema der ausgehenden Verbindung entsprechen. connic verwaltet Verbindung, Ziel, Zugangsdaten, Formatierung und Retries serverseitig; die Middleware erhält keinen dieser gespeicherten Werte. Übergib idempotency_key=..., wenn der Code denselben Aufruf möglicherweise wiederholt. Siehe Modi ausgehender Verbindungen.

Antwort transformieren

middleware/formatter.py
"""Transform response format for specific use cases."""
import json
from typing import Any, Dict

def after(response: str, context: Dict[str, Any]) -> str:
    """Wrap response in a standard API format."""

    return json.dumps({
        "success": True,
        "data": response,
        "metadata": {
            "run_id": context.get("run_id"),
            "processed_at": context.get("timestamp"),
        }
    })

Aufräumen nach jedem Run

after() läuft nur, wenn der Agent fertig wird. Um etwas freizugeben, das before() eingerichtet hat, etwa eine Sperre oder einen beanspruchten Datensatz, definiere cleanup(run, context). Die Funktion läuft, sobald der Run beendet ist, egal ob er abgeschlossen wurde, fehlgeschlagen ist, das Zeitlimit überschritten hat oder abgebrochen wurde.

middleware/order-agent.py
"""Claim an order while the agent works on it and release it however the run ends."""
from typing import Any, Dict
import httpx
from connic import StopProcessing

ORDERS_API = "https://api.example.com/orders"

async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    order_id = context["payload"]["order_id"]
    async with httpx.AsyncClient() as client:
        resp = await client.post(f"{ORDERS_API}/{order_id}/claim")
    if resp.status_code == 409:
        raise StopProcessing("Order is already being processed")

    context["claimed_order_id"] = order_id
    return content

async def cleanup(run: Dict[str, Any], context: Dict[str, Any]) -> None:
    order_id = context.get("claimed_order_id")
    if not order_id:
        return  # before() stopped or failed before it claimed the order

    async with httpx.AsyncClient() as client:
        await client.post(
            f"{ORDERS_API}/{order_id}/release",
            json={"status": run["status"], "error": run["error"]},
        )

run["status"] ist completed, failed oder cancelled. Ein Timeout zählt als failed, ein Abbruch mit StopProcessing als completed. run["error"] enthält die Fehlermeldung, die der Run anzeigt, oder None, und run["output"] die endgültige Antwort.

Wann cleanup() läuft
  • Einmal pro Run, nach after() und den Output-Guardrails. Sync-Verbindungen geben die Antwort erst zurück, wenn cleanup() fertig ist. Halte die Funktion deshalb kurz; sie hat 30 Sekunden Zeit.
  • Auch wenn before() gestoppt hat oder fehlgeschlagen ist. Setze einen context-Schlüssel, sobald before() die Ressource hält, und prüfe ihn in cleanup(), wie im Beispiel oben.
  • Nicht, während der Run auf eine Freigabe oder einen sequenziellen Schritt wartet. Die Funktion läuft, wenn der fortgesetzte Run endet.
  • Fehler und Timeouts in cleanup() werden im Run protokolliert und ändern seinen Status nie.
Runs, die ohne cleanup() enden

Ein Run, der vor seinem Start abgebrochen wird, ruft cleanup() nie auf. Dasselbe gilt für einen Run, der fehlschlägt oder abgebrochen wird, während er auf eine Freigabe wartet. Stoppt der Agent-Prozess unerwartet, kann cleanup() ebenfalls nicht laufen. Speichere bei Sperren deshalb zusätzlich, wann sie gesetzt wurden, und behandle alte Sperren als abgelaufen.

Das context-Dictionary wird geteilt und ist veränderbar. In before() gesetzte Werte stehen in Prompts über die Syntax {var}, in Tools und im Hook after() zur Verfügung. Alle Details stehen in der Kontext-Dokumentation.

Projekt-Struktur

Projektstruktur
my-agent-project/
agents/
assistant.yaml
customer-support.yaml
invoice-processor.yaml
middleware/
assistant.pyApplied to 'assistant' agent
customer-support.pyApplied to 'customer-support' agent
analytics.py❌No matching agent - ignored
tools/
...

Sync- und Async-Unterstützung

middleware/simple.py
"""Sync middleware also works."""
from typing import Any, Dict

def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    """Sync functions are automatically handled."""
    # Add a simple prefix to the first text part
    for part in content["parts"]:
        if "text" in part:
            part["text"] = "[Processed] " + part["text"]
            break
    return content

def after(response: str, context: Dict[str, Any]) -> str:
    """Both sync and async are supported."""
    return response.strip()

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

Fehlerbehandlung

Wenn eine Middleware-Funktion eine Exception auslöst, schlägt die gesamte Anfrage fehl. Der Run wird als fehlgeschlagen markiert; bei before() wird der Agent gar nicht ausgeführt. Nutze Middleware als Gatekeeper für Validierung und Authentifizierung. Ausnahme ist cleanup(): Fehler dort werden nur protokolliert.

Middleware und Retries

Standardmäßig wird before() vor der Ausführung einmal ausgeführt. Setze für Agenten vom Typ tool und sequential retry_options.rerun_middleware: true, um den Hook bei einer erneuten Ausführung des Vorgangs erneut auszuführen.

Das Wiederholen fehlgeschlagener KI-Modell-Anfragen, der Wechsel zum Fallback-KI-Modell und die Verarbeitung von Tool-Fehlern durch das KI-Modell finden innerhalb des laufenden Runs statt. Sie führen Middleware nie erneut aus; deshalb wird rerun_middleware für type: llm ignoriert.

Ist die Option aktiviert, wird bei jedem weiteren Versuch der Input aus der ursprünglichen Payload neu erstellt und before() erneut aufgerufen. So kann Middleware dynamischen Kontext aktualisieren, etwa indem sie nach teilweise ausgeführten Tool-Aufrufen einen externen Zustand erneut abruft. Das context-Dictionary behält gespeicherte Werte über die Wiederholungsversuche hinweg. context.get("retry_attempt", 0) zeigt, welcher Versuch gerade läuft. Die after()-Middleware läuft einmal nach Abschluss aller Retries, sofern before() nicht StopProcessing auslöst.cleanup() läuft ebenfalls einmal, nach dem letzten Versuch.