agentpulse-asgi
AgentPulse for Python: detect AI agents, serve them clean Markdown and declare Content Signals in FastAPI, Starlette or any ASGI app. Standard library only.
pip install agentpulse-asgi
Usage
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
from agentpulse_asgi import AgentPulseMiddleware, ContentSignals, content_signals_robots_txt
signals = ContentSignals(search=True, ai_input=True, ai_train=False)
app = FastAPI()
app.add_middleware(AgentPulseMiddleware, content_signals=signals, on_event=print)
@app.get("/robots.txt", response_class=PlainTextResponse)
def robots() -> str:
return content_signals_robots_txt(signals)
Requests that prefer text/markdown (RFC 9110 q-values) get the HTML page converted to Markdown;
everyone else gets the original HTML. HTML responses carry Vary: Accept. Markdown responses carry
x-markdown-tokens / x-original-tokens estimates (Cloudflare's Markdown for Agents stopped sending
them on 2026-10-09) and, as Cloudflare does, no ETag/Last-Modified, since conditional requests cannot
be honored for a converted body.
What it does
- Agent detection from 22 operator-documented User-Agents (OpenAI, Anthropic, Perplexity, Google, Meta, Mistral, Apple, Amazon, ByteDance, Common Crawl). The catalog is generated from the same source as the TypeScript packages and both pass the same conformance vectors.
- Optional IP verification (
AgentVerifier+get_client_ip=client_ip_from_scope): checks the client IP against the ranges each operator publishes →verified,mismatch(likely spoofed) ordeclared. Lists download in the background; failures fall back todeclared. - Safe HTML → Markdown: no regex over HTML, scripts/styles/comments/hidden content dropped,
only
http(s)/mailto/relative URLs kept, nesting capped at 512. - Metadata: a YAML frontmatter (
title,description,imagefrom<meta>, withnametaking precedence over Open Graph) whose values are JSON-quoted strings that cannot inject keys, and the page's valid JSON-LD appended as one fenced JSON value (an array when there are several blocks; malformed blocks and blocks withjavascript:/vbscript:/data:strings are dropped; 64 KB total). - Resource limits: at most
max_body_bytes(2 MB) read per response; the app is told to stop (http.disconnect) once enough has been read; conversion runs in a worker thread; a conversion cache and per-client/global rate limits protect the CPU (rate-limited requests get the original HTML). - Telemetry:
on_event(sync or async) runs in the background with the path and agent — never the client IP or the query string.IngestClient(write_key)sends events to the AgentPulse API (app.add_middleware(AgentPulseMiddleware, on_event=telemetry.on_event), thenawait telemetry.shutdown()in your lifespan). It keeps a bounded queue (500 events; the oldest are dropped and counted), sends batches every 10 s or at 50 events, and retries with backoff andRetry-After. Paths are redacted (emails, UUIDs, long numbers, hex and tokens) before they leave your server. Plain human requests are sent only as a counter.
Requirements and security notes
- Python ≥ 3.10 with the 2025
html.parsersecurity fixes (CVE-2025-6069, gh-102555, gh-135661). Older patch releases leak unclosed HTML comments as text and parse some inputs in quadratic time; the middleware checks the interpreter at startup and refuses to run on a vulnerable one (UnsafeHtmlParserError). - asyncio servers (uvicorn, hypercorn).
- Behind a reverse proxy, let the server set the client address from trusted proxies only
(
uvicorn --proxy-headers --forwarded-allow-ips=...); never readX-Forwarded-Foryourself. - Shared caches/CDNs that ignore
Varycould serve Markdown to browsers; configure them to vary onAcceptor not to cache negotiated pages.
License
MIT
Metadata
Release files for agentpulse-asgi 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agentpulse_asgi-0.1.0.tar.gz | 116.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agentpulse_asgi-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 153.2 kB
Release files / agentpulse_asgi-0.1.0.tar.gz
| Download URL | agentpulse_asgi-0.1.0.tar.gz |
|---|---|
| Size | 116.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b4739419b835cd678873bb6b1e7986794bd7e2e851339abb7fb4246b12a93967
|
|
BLAKE2b-256 checksum How to use checksums |
258586c12f7ccbb1c4900f331cf5bf5a0cfd22e477e05035d54cd2fe8aaa469e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / agentpulse_asgi-0.1.0-py3-none-any.whl
| Download URL | agentpulse_asgi-0.1.0-py3-none-any.whl |
|---|---|
| Size | 37.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
724cada1620d503533c2507f880f74bbef25d576cef92720de3a071d98e80b6a
|
|
BLAKE2b-256 checksum How to use checksums |
cf71cf0a0f5d1aef63a32be581972dca99faf962e93582af6693ee831ace6dab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log