> ## Documentation Index
> Fetch the complete documentation index at: https://docs.epode.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK: ASGI and WSGI Middleware for Agent Feedback

> Add Agent Feedback to any Python web app with one middleware line. Supports FastAPI, Starlette, Flask, Django, and every other ASGI or WSGI framework.

The `agent-feedback` Python package is a dependency-free middleware that works with any ASGI or WSGI framework. A single constructor call wraps your existing application object, and Agent Feedback instruments eligible responses without touching your handlers, adding third-party packages, or blocking the product response path.

## Installation

<Steps>
  <Step title="Install the package">
    ```bash theme={null}
    pip install agent-feedback
    ```
  </Step>

  <Step title="Create a product key">
    Open the Agent Feedback dashboard, create a product, and copy the API key. Store it as `AGENT_FEEDBACK_KEY` in your environment — never hard-code it.
  </Step>
</Steps>

<Note>
  `agent-feedback` has no third-party runtime dependencies. It uses only the Python standard library (`urllib.request`, `threading`, `hmac`, `hashlib`, and `queue`).
</Note>

***

## ASGI middleware

Wrap your ASGI application with `AgentFeedbackASGI`. This works with FastAPI, Starlette, Quart, Django (ASGI mode), and any other framework that exposes a standard ASGI callable. JSON object responses gain an `_agentFeedback` field, and HTML responses receive an injected `<script id="agent-feedback">` tag.

<Tabs>
  <Tab title="FastAPI">
    ```python theme={null}
    import os
    from fastapi import FastAPI
    from agent_feedback import AgentFeedbackASGI

    app = FastAPI()
    app = AgentFeedbackASGI(
        app,
        api_key=os.environ["AGENT_FEEDBACK_KEY"],
        include=("/search", "/docs/*"),
    )
    ```
  </Tab>

  <Tab title="Starlette">
    ```python theme={null}
    import os
    from starlette.applications import Starlette
    from agent_feedback import AgentFeedbackASGI

    app = Starlette(routes=[...])
    app = AgentFeedbackASGI(
        app,
        api_key=os.environ["AGENT_FEEDBACK_KEY"],
        include=("/search",),
    )
    ```
  </Tab>

  <Tab title="Django ASGI">
    ```python theme={null}
    # asgi.py
    import os
    from django.core.asgi import get_asgi_application
    from agent_feedback import AgentFeedbackASGI

    django_app = get_asgi_application()
    application = AgentFeedbackASGI(
        django_app,
        api_key=os.environ["AGENT_FEEDBACK_KEY"],
        include=("/api/search",),
    )
    ```
  </Tab>
</Tabs>

### With customer context

```python theme={null}
import os
from agent_feedback import AgentFeedbackASGI

app = AgentFeedbackASGI(
    app,
    api_key=os.environ["AGENT_FEEDBACK_KEY"],
    include=("/search", "/docs/*"),
    customer_ref=lambda scope: scope.get("account_id"),
    session_ref=lambda scope: scope.get("session_id"),
)
```

***

## WSGI middleware

Wrap your WSGI application's `wsgi_app` attribute with `AgentFeedbackWSGI`. This works with Flask, Django (WSGI mode), Bottle, Pyramid, and any other WSGI framework.

<Tabs>
  <Tab title="Flask">
    ```python theme={null}
    import os
    from flask import Flask
    from agent_feedback import AgentFeedbackWSGI

    app = Flask(__name__)
    app.wsgi_app = AgentFeedbackWSGI(
        app.wsgi_app,
        api_key=os.environ["AGENT_FEEDBACK_KEY"],
        include=("/search",),
    )
    ```
  </Tab>

  <Tab title="Django WSGI">
    ```python theme={null}
    # wsgi.py
    import os
    from django.core.wsgi import get_wsgi_application
    from agent_feedback import AgentFeedbackWSGI

    django_app = get_wsgi_application()
    application = AgentFeedbackWSGI(
        django_app,
        api_key=os.environ["AGENT_FEEDBACK_KEY"],
        include=("/api/search",),
    )
    ```
  </Tab>
</Tabs>

***

## ASGI vs WSGI: what gets instrumented

The two middleware classes use different strategies based on what each protocol allows.

|                       | ASGI                            | WSGI                                                     |
| --------------------- | ------------------------------- | -------------------------------------------------------- |
| JSON object responses | `_agentFeedback` field injected | `_agentFeedback` field injected (finite only)            |
| HTML responses        | `<script>` tag injected         | `<script>` tag injected (finite only)                    |
| Body buffering        | Yes, for finite responses       | Yes, for finite responses with declared `Content-Length` |
| Streaming responses   | Left untouched                  | Left untouched                                           |

Both middleware classes read the response body for `application/json` and `text/html` responses and inject the feedback contract directly. WSGI instrumentation is limited to finite responses that declare a `Content-Length` — streaming and chunked responses are forwarded without modification. Both add `Cache-Control: private, no-store` to instrumented responses.

<Warning>
  The WSGI adapter cannot inject feedback into streaming or chunked responses. If your product streams data, consider switching to ASGI or using the `feedback_from_response` agent helper on the client side.
</Warning>

***

## Configuration options

Both `AgentFeedbackASGI` and `AgentFeedbackWSGI` accept the same options after `app`. All fields except `api_key` are optional.

<ParamField body="api_key" type="str" required>
  Your Agent Feedback product key. Must match the pattern `af_live_<keyId>_<secret>`. Store this in an environment variable.
</ParamField>

<ParamField body="include" type="tuple[str, ...]" default="()">
  Glob-style path patterns that opt routes in. When empty, all routes are instrumented (subject to `exclude`). Supports `*` (any path segment) and `**` (any path). Example: `("/search", "/docs/*")`.
</ParamField>

<ParamField body="exclude" type="tuple[str, ...]" default="()">
  Glob-style patterns to skip. The default exclusion list (`/health`, `/healthz`, `/metrics`, `/favicon.ico`, `/robots.txt`, `/_agent-feedback/*`, `/api/v2/outcomes`) is always applied. Patterns you add here extend that list.
</ParamField>

<ParamField body="customer_ref" type="Callable[[Any], str | None] | None">
  Callback that receives the ASGI `scope` object (or WSGI `environ`) and returns an opaque customer identifier. Used for grouping interactions by account in the dashboard.
</ParamField>

<ParamField body="session_ref" type="Callable[[Any], str | None] | None">
  Callback that returns a session identifier. Links a sequence of interactions for session-level analytics.
</ParamField>

<ParamField body="feedback_mode" type="&#x22;auto&#x22; | &#x22;ask&#x22; | &#x22;off&#x22;" default="&#x22;auto&#x22;">
  Controls the instruction sent to agents inside the feedback envelope.

  * `"auto"` — instructs the agent to submit a review autonomously.
  * `"ask"` — instructs the agent to submit only when the outcome is clear.
  * `"off"` — disables instrumentation entirely.
</ParamField>

<ParamField body="runtime_hint" type="Callable[[Any], str | None] | None">
  Callback that receives the ASGI `scope` object (or WSGI `environ`) and returns a hint about the agent runtime making the request (e.g. `"claude"`, `"gpt-4o"`). Used for runtime-level breakdown in the dashboard.
</ParamField>

<ParamField body="flush_interval" type="float" default="0.5">
  How often the background thread flushes telemetry events, in seconds. The queue also flushes when it reaches 50 events.
</ParamField>

<ParamField body="max_queue_size" type="int" default="1000">
  Maximum number of telemetry events held in memory. When the queue is full, the oldest event is dropped to make room for the newest.
</ParamField>

***

## Agent helper functions

Import `feedback_from_response` and `submit_product_outcome` from `agent_feedback` to build a feedback-aware agent runtime that submits outcomes deterministically.

### `feedback_from_response`

Extracts the `FeedbackEnvelope` dictionary from a response. Checks the parsed body first (JSON `_agentFeedback` field or HTML `<script id="agent-feedback">` tag), then falls back to the `Agent-Feedback` response header.

```python theme={null}
import urllib.request
import json
from agent_feedback import feedback_from_response

with urllib.request.urlopen("https://api.example.com/search?q=logs") as resp:
    raw = resp.read()
    headers = dict(resp.headers)
    body = json.loads(raw)

feedback = feedback_from_response(headers, body)
# feedback is a dict (the envelope) or None
```

### `submit_product_outcome`

Submits a compact outcome review to the URL embedded in the envelope. Validates the envelope, enforces the allowed-origin allow-list, and raises `ValueError` on invalid input.

```python theme={null}
from agent_feedback import feedback_from_response, submit_product_outcome
import urllib.request, json

with urllib.request.urlopen("https://api.example.com/search?q=logs") as resp:
    body = json.loads(resp.read())
    headers = dict(resp.headers)

feedback = feedback_from_response(headers, body)

if feedback:
    result = submit_product_outcome(
        feedback,
        outcome="success",
        note="The search returned the log entries needed to resolve the incident.",
    )
```

<Tip>
  Pass `allowed_submit_origins=("https://your-staging-endpoint.example.com",)` when testing against a non-production Agent Feedback instance. By default only the hosted service is trusted.
</Tip>
