backplanes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backplanes/__init__.py +95 -0
- backplanes/_integration_events.py +97 -0
- backplanes/_payload_fields.py +24 -0
- backplanes/_stream_recording.py +548 -0
- backplanes/_tool_calls.py +237 -0
- backplanes/_web_integrations.py +353 -0
- backplanes/audit.py +120 -0
- backplanes/causality.py +61 -0
- backplanes/client.py +1850 -0
- backplanes/config.py +276 -0
- backplanes/context.py +278 -0
- backplanes/filesystem.py +35 -0
- backplanes/identity.py +246 -0
- backplanes/integrations.py +595 -0
- backplanes/llm_telemetry.py +276 -0
- backplanes/payloads.py +81 -0
- backplanes/pii.py +118 -0
- backplanes/py.typed +0 -0
- backplanes/recording.py +115 -0
- backplanes/runtime_identity.py +54 -0
- backplanes/signing.py +113 -0
- backplanes/tools.py +229 -0
- backplanes/v1/__init__.py +0 -0
- backplanes/v1/audit_pb2.py +58 -0
- backplanes/v1/audit_pb2.pyi +465 -0
- backplanes/v1/audit_pb2_grpc.py +4 -0
- backplanes/v1/audit_pb2_grpc.pyi +17 -0
- backplanes/v1/collector_pb2.py +106 -0
- backplanes/v1/collector_pb2.pyi +905 -0
- backplanes/v1/collector_pb2_grpc.py +776 -0
- backplanes/v1/collector_pb2_grpc.pyi +491 -0
- backplanes/v1/events_pb2.py +59 -0
- backplanes/v1/events_pb2.pyi +416 -0
- backplanes/v1/events_pb2_grpc.py +4 -0
- backplanes/v1/events_pb2_grpc.pyi +17 -0
- backplanes/v1/spotlight_pb2.py +55 -0
- backplanes/v1/spotlight_pb2.pyi +299 -0
- backplanes/v1/spotlight_pb2_grpc.py +4 -0
- backplanes/v1/spotlight_pb2_grpc.pyi +17 -0
- backplanes/wrappers.py +575 -0
- backplanes-0.1.0.dist-info/METADATA +361 -0
- backplanes-0.1.0.dist-info/RECORD +45 -0
- backplanes-0.1.0.dist-info/WHEEL +5 -0
- backplanes-0.1.0.dist-info/licenses/LICENSE +49 -0
- backplanes-0.1.0.dist-info/top_level.txt +1 -0
backplanes/__init__.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Backplanes Python SDK for agent telemetry and identity management.
|
|
2
|
+
|
|
3
|
+
The five-minute path:
|
|
4
|
+
|
|
5
|
+
>>> import backplanes
|
|
6
|
+
>>> client = backplanes.init(agent_name="my-agent")
|
|
7
|
+
>>> # Anthropic/OpenAI clients constructed after this line are
|
|
8
|
+
>>> # instrumented automatically; nothing else to write.
|
|
9
|
+
|
|
10
|
+
Instrumentation comes in three composable tiers — semantic wrappers
|
|
11
|
+
(`wrap_anthropic` / `wrap_openai`), `init()`'s constructor hooks, and the
|
|
12
|
+
transport net (`patch_httpx` / `patch_requests`) — described in
|
|
13
|
+
docs/design/INSTRUMENTATION_TIERS.md. Custom events are always available:
|
|
14
|
+
|
|
15
|
+
>>> event = client.create_event(edge_type="tool", direction="egress")
|
|
16
|
+
>>> client.ingest(event)
|
|
17
|
+
|
|
18
|
+
Handling an incoming workflow from another agent:
|
|
19
|
+
|
|
20
|
+
>>> context_jwt = request.headers.get("Backplanes-Context")
|
|
21
|
+
>>> with client.request_context(context_jwt):
|
|
22
|
+
... client.ingest(client.create_event(edge_type="tool", direction="ingress"))
|
|
23
|
+
|
|
24
|
+
Anything not exported here is still importable from its submodule
|
|
25
|
+
(`backplanes.config`, `backplanes.identity`, `backplanes.integrations`,
|
|
26
|
+
`backplanes.signing`, `backplanes.v1`) — a stable but quieter surface.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from backplanes.client import BackplanesClient, BackplanesNotReadyError
|
|
30
|
+
from backplanes.integrations import (
|
|
31
|
+
instrument_asgi,
|
|
32
|
+
instrument_fastapi,
|
|
33
|
+
instrument_wsgi,
|
|
34
|
+
patch_httpx,
|
|
35
|
+
patch_requests,
|
|
36
|
+
)
|
|
37
|
+
from backplanes.wrappers import auto_wrap, wrap_anthropic, wrap_openai
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
|
40
|
+
__all__ = [
|
|
41
|
+
"init",
|
|
42
|
+
"BackplanesClient",
|
|
43
|
+
"BackplanesNotReadyError",
|
|
44
|
+
"wrap_anthropic",
|
|
45
|
+
"wrap_openai",
|
|
46
|
+
"patch_httpx",
|
|
47
|
+
"patch_requests",
|
|
48
|
+
"instrument_fastapi",
|
|
49
|
+
"instrument_asgi",
|
|
50
|
+
"instrument_wsgi",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def init(
|
|
55
|
+
agent_name=None,
|
|
56
|
+
*,
|
|
57
|
+
auto_instrument=True,
|
|
58
|
+
**client_kwargs,
|
|
59
|
+
) -> BackplanesClient:
|
|
60
|
+
"""One-call setup: construct the client and instrument the process.
|
|
61
|
+
|
|
62
|
+
Builds a `BackplanesClient`, hooks the Anthropic/OpenAI SDK
|
|
63
|
+
constructors so clients created afterwards come out wrapped (tier 1
|
|
64
|
+
fidelity), and installs the httpx/requests transport net for everything
|
|
65
|
+
else. SDK clients constructed before `init()` are covered by the
|
|
66
|
+
transport net alone.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
agent_name: Human-readable name for agent registration.
|
|
70
|
+
auto_instrument: Whether to hook SDK constructors and install the
|
|
71
|
+
transport patches at all.
|
|
72
|
+
**client_kwargs: Passed through to `BackplanesClient`.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The constructed BackplanesClient.
|
|
76
|
+
"""
|
|
77
|
+
client = BackplanesClient(agent_name=agent_name, **client_kwargs)
|
|
78
|
+
if not auto_instrument:
|
|
79
|
+
return client
|
|
80
|
+
|
|
81
|
+
auto_wrap(client)
|
|
82
|
+
for patch in (patch_httpx, patch_requests):
|
|
83
|
+
# The transport patches import their target library lazily; a
|
|
84
|
+
# missing library just narrows the net.
|
|
85
|
+
try:
|
|
86
|
+
patch(
|
|
87
|
+
client,
|
|
88
|
+
)
|
|
89
|
+
except ImportError as exc:
|
|
90
|
+
import logging
|
|
91
|
+
|
|
92
|
+
logging.getLogger(__name__).debug(
|
|
93
|
+
"Skipping transport patch %s: %s", patch.__name__, exc
|
|
94
|
+
)
|
|
95
|
+
return client
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Shared event construction for inbound and outbound HTTP integrations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from contextlib import nullcontext
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
10
|
+
|
|
11
|
+
from backplanes import causality
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def safe_emit_http_event(
|
|
17
|
+
client: Any,
|
|
18
|
+
direction: str,
|
|
19
|
+
method: str,
|
|
20
|
+
url: str,
|
|
21
|
+
started_at: float,
|
|
22
|
+
status: str,
|
|
23
|
+
request_size_bytes: int,
|
|
24
|
+
response_size_bytes: int,
|
|
25
|
+
metadata: Optional[dict[str, str]] = None,
|
|
26
|
+
error_info: Optional[dict[str, str]] = None,
|
|
27
|
+
extra: Optional[dict[str, Any]] = None,
|
|
28
|
+
detached: bool = False,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Best-effort HTTP event emission that never interrupts user traffic."""
|
|
31
|
+
try:
|
|
32
|
+
merged_metadata: dict[str, str] = dict(metadata or {})
|
|
33
|
+
extra_fields = dict(extra or {})
|
|
34
|
+
merged_metadata.update(extra_fields.pop("metadata", None) or {})
|
|
35
|
+
fields: dict[str, Any] = {
|
|
36
|
+
"edge_type": "network",
|
|
37
|
+
"direction": direction,
|
|
38
|
+
"status": status,
|
|
39
|
+
"timestamp_unix_ms": int(started_at * 1000),
|
|
40
|
+
"duration_ms": duration_ms(started_at),
|
|
41
|
+
"request_size_bytes": request_size_bytes,
|
|
42
|
+
"response_size_bytes": response_size_bytes,
|
|
43
|
+
"metadata": merged_metadata,
|
|
44
|
+
"error_info": error_info,
|
|
45
|
+
}
|
|
46
|
+
fields.update(extra_fields)
|
|
47
|
+
scope = causality.detached() if detached else nullcontext()
|
|
48
|
+
with scope:
|
|
49
|
+
client.ingest(client.create_event(**fields))
|
|
50
|
+
except Exception as exc: # pragma: no cover - defensive only
|
|
51
|
+
logger.debug("Failed to emit Backplanes HTTP event: %s", exc)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def duration_ms(started_at: float) -> int:
|
|
55
|
+
"""Convert a start timestamp into duration milliseconds."""
|
|
56
|
+
return max(0, int((time.time() - started_at) * 1000))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def status_from_http_code(status_code: Optional[int]) -> str:
|
|
60
|
+
"""Map an HTTP status code to a Backplanes event status."""
|
|
61
|
+
if status_code is None:
|
|
62
|
+
return "ok"
|
|
63
|
+
if status_code == 408:
|
|
64
|
+
return "timeout"
|
|
65
|
+
if status_code == 429:
|
|
66
|
+
return "rate_limited"
|
|
67
|
+
if status_code >= 400:
|
|
68
|
+
return "error"
|
|
69
|
+
return "ok"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def exception_status(exc: BaseException) -> str:
|
|
73
|
+
"""Map an exception to a Backplanes event status."""
|
|
74
|
+
return "timeout" if "timeout" in exc.__class__.__name__.lower() else "error"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def http_metadata(
|
|
78
|
+
method: str,
|
|
79
|
+
url: str,
|
|
80
|
+
status_code: Optional[int],
|
|
81
|
+
) -> dict[str, str]:
|
|
82
|
+
"""Build lightweight HTTP metadata for emitted events."""
|
|
83
|
+
try:
|
|
84
|
+
parsed_url = urlsplit(url)
|
|
85
|
+
sanitized_url = urlunsplit(
|
|
86
|
+
(parsed_url.scheme, parsed_url.netloc, parsed_url.path, "", "")
|
|
87
|
+
)
|
|
88
|
+
except ValueError:
|
|
89
|
+
# Malformed URLs must not let best-effort telemetry break user traffic.
|
|
90
|
+
sanitized_url = url.split("#", 1)[0].split("?", 1)[0]
|
|
91
|
+
metadata = {
|
|
92
|
+
"http.method": method.upper(),
|
|
93
|
+
"http.url": sanitized_url,
|
|
94
|
+
}
|
|
95
|
+
if status_code is not None:
|
|
96
|
+
metadata["http.status_code"] = str(status_code)
|
|
97
|
+
return metadata
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Event fields derived from request and response bodies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from backplanes import payloads, pii
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def payload_fields(request_body: bytes, response_body: bytes) -> dict[str, Any]:
|
|
11
|
+
"""PII counts and optional captured content for one wire exchange."""
|
|
12
|
+
counts = pii.scan(request_body[: pii.MAX_SCAN_BYTES])
|
|
13
|
+
for category, count in pii.scan(response_body[: pii.MAX_SCAN_BYTES]).items():
|
|
14
|
+
counts[category] = counts.get(category, 0) + count
|
|
15
|
+
|
|
16
|
+
fields: dict[str, Any] = {}
|
|
17
|
+
if counts:
|
|
18
|
+
fields["pii_categories"] = pii.categories(counts)
|
|
19
|
+
fields["pii_counts"] = counts
|
|
20
|
+
|
|
21
|
+
captured = payloads.capture(request_body, response_body)
|
|
22
|
+
if captured:
|
|
23
|
+
fields["json_passed"] = captured
|
|
24
|
+
return fields
|