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.
Files changed (45) hide show
  1. backplanes/__init__.py +95 -0
  2. backplanes/_integration_events.py +97 -0
  3. backplanes/_payload_fields.py +24 -0
  4. backplanes/_stream_recording.py +548 -0
  5. backplanes/_tool_calls.py +237 -0
  6. backplanes/_web_integrations.py +353 -0
  7. backplanes/audit.py +120 -0
  8. backplanes/causality.py +61 -0
  9. backplanes/client.py +1850 -0
  10. backplanes/config.py +276 -0
  11. backplanes/context.py +278 -0
  12. backplanes/filesystem.py +35 -0
  13. backplanes/identity.py +246 -0
  14. backplanes/integrations.py +595 -0
  15. backplanes/llm_telemetry.py +276 -0
  16. backplanes/payloads.py +81 -0
  17. backplanes/pii.py +118 -0
  18. backplanes/py.typed +0 -0
  19. backplanes/recording.py +115 -0
  20. backplanes/runtime_identity.py +54 -0
  21. backplanes/signing.py +113 -0
  22. backplanes/tools.py +229 -0
  23. backplanes/v1/__init__.py +0 -0
  24. backplanes/v1/audit_pb2.py +58 -0
  25. backplanes/v1/audit_pb2.pyi +465 -0
  26. backplanes/v1/audit_pb2_grpc.py +4 -0
  27. backplanes/v1/audit_pb2_grpc.pyi +17 -0
  28. backplanes/v1/collector_pb2.py +106 -0
  29. backplanes/v1/collector_pb2.pyi +905 -0
  30. backplanes/v1/collector_pb2_grpc.py +776 -0
  31. backplanes/v1/collector_pb2_grpc.pyi +491 -0
  32. backplanes/v1/events_pb2.py +59 -0
  33. backplanes/v1/events_pb2.pyi +416 -0
  34. backplanes/v1/events_pb2_grpc.py +4 -0
  35. backplanes/v1/events_pb2_grpc.pyi +17 -0
  36. backplanes/v1/spotlight_pb2.py +55 -0
  37. backplanes/v1/spotlight_pb2.pyi +299 -0
  38. backplanes/v1/spotlight_pb2_grpc.py +4 -0
  39. backplanes/v1/spotlight_pb2_grpc.pyi +17 -0
  40. backplanes/wrappers.py +575 -0
  41. backplanes-0.1.0.dist-info/METADATA +361 -0
  42. backplanes-0.1.0.dist-info/RECORD +45 -0
  43. backplanes-0.1.0.dist-info/WHEEL +5 -0
  44. backplanes-0.1.0.dist-info/licenses/LICENSE +49 -0
  45. 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