botanu 0.1.dev61__tar.gz → 0.1.dev68__tar.gz

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 (32) hide show
  1. {botanu-0.1.dev61 → botanu-0.1.dev68}/PKG-INFO +10 -6
  2. {botanu-0.1.dev61 → botanu-0.1.dev68}/README.md +9 -5
  3. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/__init__.py +6 -0
  4. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/models/run_context.py +5 -1
  5. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/processors/__init__.py +3 -1
  6. botanu-0.1.dev68/src/botanu/processors/resource_enricher.py +179 -0
  7. botanu-0.1.dev68/src/botanu/processors/sampled.py +86 -0
  8. botanu-0.1.dev68/src/botanu/register.py +50 -0
  9. botanu-0.1.dev68/src/botanu/sampling/__init__.py +8 -0
  10. botanu-0.1.dev68/src/botanu/sampling/content_sampler.py +39 -0
  11. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/sdk/__init__.py +6 -1
  12. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/sdk/bootstrap.py +194 -17
  13. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/sdk/config.py +145 -7
  14. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/sdk/decorators.py +88 -4
  15. botanu-0.1.dev68/src/botanu/sdk/span_helpers.py +217 -0
  16. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/tracking/data.py +51 -0
  17. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/tracking/llm.py +45 -0
  18. botanu-0.1.dev61/src/botanu/sdk/span_helpers.py +0 -143
  19. {botanu-0.1.dev61 → botanu-0.1.dev68}/.gitignore +0 -0
  20. {botanu-0.1.dev61 → botanu-0.1.dev68}/LICENSE +0 -0
  21. {botanu-0.1.dev61 → botanu-0.1.dev68}/NOTICE +0 -0
  22. {botanu-0.1.dev61 → botanu-0.1.dev68}/pyproject.toml +0 -0
  23. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/_version.py +0 -0
  24. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/integrations/__init__.py +0 -0
  25. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/integrations/tenacity.py +0 -0
  26. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/models/__init__.py +0 -0
  27. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/processors/enricher.py +0 -0
  28. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/py.typed +0 -0
  29. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/resources/__init__.py +0 -0
  30. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/sdk/context.py +0 -0
  31. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/sdk/middleware.py +0 -0
  32. {botanu-0.1.dev61 → botanu-0.1.dev68}/src/botanu/tracking/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: botanu
3
- Version: 0.1.dev61
3
+ Version: 0.1.dev68
4
4
  Summary: OpenTelemetry-native run-level cost attribution for AI workflows
5
5
  Project-URL: Homepage, https://github.com/botanu-ai/botanu-sdk-python
6
6
  Project-URL: Documentation, https://docs.botanu.ai
@@ -100,21 +100,24 @@ Provides-Extra: gcp
100
100
  Requires-Dist: opentelemetry-resource-detector-gcp>=0.1b0; extra == 'gcp'
101
101
  Description-Content-Type: text/markdown
102
102
 
103
- # Botanu SDK for Python
103
+ # botanu SDK for Python
104
104
 
105
105
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
106
106
 
107
+ [botanu](https://botanu.ai/) is platform that helps AI companies understand the real cost of their AI features per customer, enabling outcome-based pricing and smarter scaling.
108
+ This SDK is built on [OpenTelemetry](https://opentelemetry.io/) for event-level cost attribution for AI workflow. For more email- deborah@botanu.ai
109
+
110
+
111
+ ## Getting Started
112
+
107
113
 
108
- Event-level cost attribution for AI workflows, built on [OpenTelemetry](https://opentelemetry.io/).
109
114
 
110
115
  An **event** is one business transaction — resolving a support ticket, processing
111
116
  an order, generating a report. Each event may involve multiple **runs** (LLM calls,
112
117
  retries, sub-workflows) across multiple services. By correlating every run to a
113
- stable `event_id`, Botanu gives you per-event cost attribution and outcome
118
+ stable `event_id`, botanu gives you per-event cost attribution and outcome
114
119
  tracking without sampling artifacts.
115
120
 
116
- ## Getting Started
117
-
118
121
  ```bash
119
122
  pip install botanu
120
123
  ```
@@ -193,6 +196,7 @@ To report a security vulnerability, please use
193
196
  [GitHub Security Advisories](https://github.com/botanu-ai/botanu-sdk-python/security/advisories/new)
194
197
  or see [SECURITY.md](./SECURITY.md) for full details. **Do not file a public issue.**
195
198
 
199
+
196
200
  ## Code of Conduct
197
201
 
198
202
  This project follows the
@@ -1,18 +1,21 @@
1
- # Botanu SDK for Python
1
+ # botanu SDK for Python
2
2
 
3
3
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
4
4
 
5
+ [botanu](https://botanu.ai/) is platform that helps AI companies understand the real cost of their AI features per customer, enabling outcome-based pricing and smarter scaling.
6
+ This SDK is built on [OpenTelemetry](https://opentelemetry.io/) for event-level cost attribution for AI workflow. For more email- deborah@botanu.ai
7
+
8
+
9
+ ## Getting Started
10
+
5
11
 
6
- Event-level cost attribution for AI workflows, built on [OpenTelemetry](https://opentelemetry.io/).
7
12
 
8
13
  An **event** is one business transaction — resolving a support ticket, processing
9
14
  an order, generating a report. Each event may involve multiple **runs** (LLM calls,
10
15
  retries, sub-workflows) across multiple services. By correlating every run to a
11
- stable `event_id`, Botanu gives you per-event cost attribution and outcome
16
+ stable `event_id`, botanu gives you per-event cost attribution and outcome
12
17
  tracking without sampling artifacts.
13
18
 
14
- ## Getting Started
15
-
16
19
  ```bash
17
20
  pip install botanu
18
21
  ```
@@ -91,6 +94,7 @@ To report a security vulnerability, please use
91
94
  [GitHub Security Advisories](https://github.com/botanu-ai/botanu-sdk-python/security/advisories/new)
92
95
  or see [SECURITY.md](./SECURITY.md) for full details. **Do not file a public issue.**
93
96
 
97
+
94
98
  ## Code of Conduct
95
99
 
96
100
  This project follows the
@@ -23,6 +23,9 @@ from botanu._version import __version__
23
23
  # Run context model
24
24
  from botanu.models.run_context import RunContext, RunOutcome, RunStatus
25
25
 
26
+ # Processors
27
+ from botanu.processors import RunContextEnricher, SampledSpanProcessor
28
+
26
29
  # Bootstrap
27
30
  from botanu.sdk.bootstrap import (
28
31
  disable,
@@ -73,4 +76,7 @@ __all__ = [
73
76
  "RunContext",
74
77
  "RunStatus",
75
78
  "RunOutcome",
79
+ # Processors
80
+ "RunContextEnricher",
81
+ "SampledSpanProcessor",
76
82
  ]
@@ -89,6 +89,7 @@ class RunContext:
89
89
  event_id: str
90
90
  customer_id: str
91
91
  environment: str
92
+ step: Optional[str] = None
92
93
  workflow_version: Optional[str] = None
93
94
  tenant_id: Optional[str] = None
94
95
  parent_run_id: Optional[str] = None
@@ -270,7 +271,10 @@ class RunContext:
270
271
  if self.cancelled_at:
271
272
  attrs["botanu.run.cancelled_at"] = self.cancelled_at
272
273
  if self.outcome:
273
- attrs["botanu.outcome.status"] = self.outcome.status.value
274
+ # `botanu.outcome.status` is NOT emitted (removed 2026-04-16):
275
+ # customer-reported outcome is trivially fakeable. Event outcome
276
+ # is derived from eval verdict rollup / HITL / SoR instead.
277
+ # Remaining fields are diagnostic only and stay for debugging.
274
278
  if self.outcome.reason_code:
275
279
  attrs["botanu.outcome.reason_code"] = self.outcome.reason_code
276
280
  if self.outcome.error_class:
@@ -8,5 +8,7 @@ All other processing should happen in the OTel Collector.
8
8
  """
9
9
 
10
10
  from botanu.processors.enricher import RunContextEnricher
11
+ from botanu.processors.resource_enricher import ResourceEnricher
12
+ from botanu.processors.sampled import SampledSpanProcessor
11
13
 
12
- __all__ = ["RunContextEnricher"]
14
+ __all__ = ["RunContextEnricher", "ResourceEnricher", "SampledSpanProcessor"]
@@ -0,0 +1,179 @@
1
+ # SPDX-FileCopyrightText: 2026 The Botanu Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """ResourceEnricher — infer `botanu.cloud_provider` + `botanu.bytes_transferred`
5
+ from OTel semantic-convention attributes set by auto-instrumentation.
6
+
7
+ Why this exists: the cost worker (botanu-cost-engine-workflow) prices non-LLM
8
+ spans via `rate × bytes_transferred` and looks up rate cards keyed by
9
+ `cloud_provider + system_name`. OTel auto-instrumentation emits the raw
10
+ attributes (`db.system`, `http.request.body.size`, `aws.service`, etc.) but
11
+ does NOT emit botanu-namespaced attributes in the shape the cost worker
12
+ reads. Without this enricher, S3 PUTs, DynamoDB ops, and egress all price to
13
+ $0 — see the `pricing.md` problem statement.
14
+
15
+ Attributes written:
16
+
17
+ - `botanu.cloud_provider` ("aws" | "gcp" | "azure" | …)
18
+ - `botanu.bytes_transferred` (int, sent + received combined)
19
+
20
+ The enricher is purely additive. It leaves all original OTel attributes
21
+ intact — no customer observability breaks.
22
+
23
+ Explicit values set by `set_bytes_transferred()` / `cloud_provider=` kwarg on
24
+ trackers take precedence: this enricher only writes if the target attribute
25
+ is not already present (checked at `on_end` time via the span's attribute
26
+ dict).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ from typing import Mapping, Optional
33
+
34
+ from opentelemetry import context
35
+ from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ # System/service → cloud provider. Used when the semconv `cloud.provider`
41
+ # attribute is absent (most auto-instrumentations don't set it, so we infer
42
+ # from the db/messaging system name or the AWS/Azure/GCP service name).
43
+ _SYSTEM_TO_CLOUD_PROVIDER: dict[str, str] = {
44
+ # AWS
45
+ "dynamodb": "aws",
46
+ "s3": "aws",
47
+ "sqs": "aws",
48
+ "sns": "aws",
49
+ "kinesis": "aws",
50
+ "eventbridge": "aws",
51
+ "lambda": "aws",
52
+ "elasticache": "aws",
53
+ "redshift": "aws",
54
+ "athena": "aws",
55
+ "neptune": "aws",
56
+ "efs": "aws",
57
+ # GCP
58
+ "firestore": "gcp",
59
+ "bigquery": "gcp",
60
+ "gcs": "gcp",
61
+ "pubsub": "gcp",
62
+ # Azure
63
+ "cosmosdb": "azure",
64
+ "azure_blob": "azure",
65
+ "servicebus": "azure",
66
+ "eventhub": "azure",
67
+ "synapse": "azure",
68
+ }
69
+
70
+ _BOTANU_CLOUD_PROVIDER = "botanu.cloud_provider"
71
+ _BOTANU_BYTES_TRANSFERRED = "botanu.bytes_transferred"
72
+
73
+
74
+ class ResourceEnricher(SpanProcessor):
75
+ """Write botanu-namespaced resource attributes from OTel semconv data.
76
+
77
+ Runs at `on_end` (not `on_start`) — auto-instrumentation populates the
78
+ source attributes on span start, but some (notably http.*.body.size) are
79
+ only known when the response completes.
80
+ """
81
+
82
+ def on_start(self, span: Span, parent_context: Optional[context.Context] = None) -> None:
83
+ # Cheap path: no work at start. Waiting until on_end lets us read
84
+ # response-time attributes that auto-instrumentation sets after the
85
+ # wrapped call returns (bytes, status codes, etc.).
86
+ return
87
+
88
+ def on_end(self, span: ReadableSpan) -> None:
89
+ attrs = span.attributes or {}
90
+
91
+ # Skip LLM spans entirely — LLM pricing goes through pricing_model_tokens
92
+ # (prompt/completion tokens), not bytes_transferred. Writing bytes here
93
+ # would double-count into cost_infra_usd.
94
+ if _is_llm_span(attrs):
95
+ return
96
+
97
+ cloud_provider = _infer_cloud_provider(attrs)
98
+ bytes_transferred = _infer_bytes_transferred(attrs)
99
+
100
+ if cloud_provider is None and bytes_transferred is None:
101
+ return
102
+
103
+ # Writing to a ReadableSpan: OTel SDK's ReadableSpan is read-only by
104
+ # contract, but the concrete _Span class exposes set_attribute. If
105
+ # the attribute is already set (explicit API or customer), skip —
106
+ # explicit beats inferred.
107
+ setter = getattr(span, "set_attribute", None)
108
+ if setter is None:
109
+ return
110
+
111
+ if cloud_provider is not None and _BOTANU_CLOUD_PROVIDER not in attrs:
112
+ setter(_BOTANU_CLOUD_PROVIDER, cloud_provider)
113
+ if bytes_transferred is not None and _BOTANU_BYTES_TRANSFERRED not in attrs:
114
+ setter(_BOTANU_BYTES_TRANSFERRED, bytes_transferred)
115
+
116
+ def shutdown(self) -> None:
117
+ pass
118
+
119
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
120
+ return True
121
+
122
+
123
+ def _is_llm_span(attrs: Mapping[str, object]) -> bool:
124
+ return (
125
+ "gen_ai.request.model" in attrs
126
+ or "gen_ai.system" in attrs
127
+ or "llm.request.model" in attrs
128
+ )
129
+
130
+
131
+ def _infer_cloud_provider(attrs: Mapping[str, object]) -> Optional[str]:
132
+ # 1. Explicit semconv `cloud.provider` (if set, trust it)
133
+ explicit = attrs.get("cloud.provider")
134
+ if isinstance(explicit, str) and explicit:
135
+ return explicit.lower()
136
+
137
+ # 2. AWS auto-instrumentation sets `aws.service` or `rpc.system="aws-api"`
138
+ if attrs.get("rpc.system") == "aws-api" or "aws.service" in attrs or "aws.region" in attrs:
139
+ return "aws"
140
+ if "gcp.service" in attrs or "gcp.project_id" in attrs:
141
+ return "gcp"
142
+ if "azure.resource" in attrs or "azure.namespace" in attrs:
143
+ return "azure"
144
+
145
+ # 3. Infer from system name (db.system, messaging.system, botanu.storage.system)
146
+ for key in ("db.system", "messaging.system", "botanu.storage.system"):
147
+ val = attrs.get(key)
148
+ if isinstance(val, str):
149
+ provider = _SYSTEM_TO_CLOUD_PROVIDER.get(val.lower())
150
+ if provider:
151
+ return provider
152
+ return None
153
+
154
+
155
+ def _infer_bytes_transferred(attrs: Mapping[str, object]) -> Optional[int]:
156
+ total = 0
157
+ saw_any = False
158
+
159
+ # OTel HTTP semconv (stable)
160
+ for key in ("http.request.body.size", "http.response.body.size"):
161
+ val = attrs.get(key)
162
+ if isinstance(val, int) and val >= 0:
163
+ total += val
164
+ saw_any = True
165
+
166
+ # botanu tracker attrs (fallback — populated by DBTracker.set_result etc.)
167
+ if not saw_any:
168
+ for key in (
169
+ "botanu.data.bytes_read",
170
+ "botanu.data.bytes_written",
171
+ "botanu.messaging.bytes_transferred",
172
+ "botanu.warehouse.bytes_scanned",
173
+ ):
174
+ val = attrs.get(key)
175
+ if isinstance(val, int) and val >= 0:
176
+ total += val
177
+ saw_any = True
178
+
179
+ return total if saw_any else None
@@ -0,0 +1,86 @@
1
+ # SPDX-FileCopyrightText: 2026 The Botanu Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """SampledSpanProcessor — preserves the customer's sampling ratio.
5
+
6
+ When botanu changes the TracerProvider sampler to AlwaysOn (to capture 100%),
7
+ existing customer processors (Datadog exporter, Jaeger exporter, etc.) would
8
+ suddenly see 10x the span volume if the customer had ratio-based sampling.
9
+
10
+ This processor wraps an existing processor and applies the customer's original
11
+ ratio at the export level. Result: the customer's exporter sees the same volume
12
+ as before, their bill is unchanged, their dashboards are unchanged.
13
+
14
+ botanu's own processor is NOT wrapped — it sees 100%.
15
+
16
+ Sampling is deterministic: the same trace_id always gets the same decision.
17
+ This matches OTel's ``TraceIdRatioBasedSampler`` algorithm.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import Optional
24
+
25
+ from opentelemetry import context
26
+ from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor
27
+ from opentelemetry.trace import Span
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class SampledSpanProcessor(SpanProcessor):
33
+ """Wraps a SpanProcessor with deterministic ratio sampling.
34
+
35
+ Args:
36
+ wrapped: The original processor to wrap (e.g., BatchSpanProcessor
37
+ sending to Datadog).
38
+ ratio: Sampling ratio (0.0 to 1.0). 0.1 means 10% of spans are
39
+ forwarded to the wrapped processor.
40
+ """
41
+
42
+ def __init__(self, wrapped: SpanProcessor, ratio: float) -> None:
43
+ if not 0.0 <= ratio <= 1.0:
44
+ raise ValueError(f"ratio must be between 0.0 and 1.0, got {ratio}")
45
+ self._wrapped = wrapped
46
+ self._ratio = ratio
47
+ # Pre-compute bound for comparison (avoids per-span float math)
48
+ self._bound = int(ratio * (2**64 - 1))
49
+
50
+ def _should_sample(self, trace_id: int) -> bool:
51
+ """Deterministic sampling decision based on trace_id.
52
+
53
+ Uses the upper 64 bits of the 128-bit trace_id, matching OTel's
54
+ TraceIdRatioBasedSampler algorithm. Same trace_id always produces
55
+ the same decision.
56
+ """
57
+ if self._ratio >= 1.0:
58
+ return True
59
+ if self._ratio <= 0.0:
60
+ return False
61
+ # Upper 64 bits of trace_id for deterministic comparison
62
+ upper = trace_id >> 64 if trace_id.bit_length() > 64 else trace_id
63
+ return upper <= self._bound
64
+
65
+ def on_start(
66
+ self,
67
+ span: Span,
68
+ parent_context: Optional[context.Context] = None,
69
+ ) -> None:
70
+ # Gate on_start with the same decision as on_end. Forwarding on_start
71
+ # unconditionally while gating on_end orphans spans inside wrapped
72
+ # processors (BatchSpanProcessor, Datadog exporter, etc.) — they hold
73
+ # start-time bookkeeping for spans whose on_end never fires. Over time
74
+ # this leaks memory in the customer's process.
75
+ if self._should_sample(span.context.trace_id):
76
+ self._wrapped.on_start(span, parent_context)
77
+
78
+ def on_end(self, span: ReadableSpan) -> None:
79
+ if self._should_sample(span.context.trace_id):
80
+ self._wrapped.on_end(span)
81
+
82
+ def shutdown(self) -> None:
83
+ self._wrapped.shutdown()
84
+
85
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
86
+ return self._wrapped.force_flush(timeout_millis)
@@ -0,0 +1,50 @@
1
+ # SPDX-FileCopyrightText: 2026 The Botanu Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Zero-code initialization entry point.
5
+
6
+ Import this module to auto-initialize Botanu SDK with no code changes.
7
+ All configuration is read from environment variables or botanu.yaml.
8
+
9
+ Usage::
10
+
11
+ # As a Python module flag
12
+ python -m botanu.register && python app.py
13
+
14
+ # Or via PYTHONPATH preload (works with gunicorn, uvicorn, etc.)
15
+ python -c "import botanu.register" && python app.py
16
+
17
+ # Or in gunicorn config
18
+ # gunicorn.conf.py:
19
+ def on_starting(server):
20
+ import botanu.register # noqa: F401
21
+
22
+ # Or in uvicorn
23
+ uvicorn app:app --env-file .env
24
+
25
+ # Or in Dockerfile
26
+ ENV BOTANU_API_KEY=btnu_live_...
27
+ ENV BOTANU_SERVICE_NAME=my-service
28
+ CMD ["python", "-c", "import botanu.register; import uvicorn; uvicorn.run('app:app')"]
29
+
30
+ Configuration (env vars or botanu.yaml):
31
+
32
+ BOTANU_API_KEY - API key (required for Botanu Cloud)
33
+ BOTANU_SERVICE_NAME - Service name (recommended)
34
+ BOTANU_ENVIRONMENT - Environment (default: production)
35
+
36
+ See docs/getting-started/configuration.md for full options.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import logging
42
+
43
+ from botanu.sdk.bootstrap import enable
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ result = enable()
48
+
49
+ if result:
50
+ logger.info("Botanu SDK auto-initialized via botanu.register")
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2026 The Botanu Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Sampling primitives — content capture gate, future trace samplers."""
5
+
6
+ from botanu.sampling.content_sampler import should_capture_content
7
+
8
+ __all__ = ["should_capture_content"]
@@ -0,0 +1,39 @@
1
+ # SPDX-FileCopyrightText: 2026 The Botanu Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Content capture sampling gate for eval.
5
+
6
+ MVP: simple ``random.random() < rate`` check. The ``event_id`` parameter is
7
+ accepted now so that a Month 2+ upgrade to hash-based deterministic sampling
8
+ (SHA-256 of ``tenant_id || event_id``) won't break callers. Deterministic
9
+ sampling matters for replays and backfills; simple random is sufficient for
10
+ MVP volume.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import random
16
+ from typing import Optional
17
+
18
+
19
+ def should_capture_content(rate: float, event_id: Optional[str] = None) -> bool:
20
+ """Return True if this call's content should be captured.
21
+
22
+ Args:
23
+ rate: Capture rate in [0.0, 1.0]. 0.0 disables capture (default,
24
+ privacy-safe). 1.0 captures everything (sandbox/shadow).
25
+ Production typically uses 0.10–0.20.
26
+ event_id: Currently unused. Present so a future deterministic-hash
27
+ implementation can be swapped in without API churn.
28
+
29
+ Examples:
30
+ >>> should_capture_content(0.0)
31
+ False
32
+ >>> should_capture_content(1.0)
33
+ True
34
+ """
35
+ if rate <= 0.0:
36
+ return False
37
+ if rate >= 1.0:
38
+ return True
39
+ return random.random() < rate
@@ -15,7 +15,11 @@ from botanu.sdk.context import (
15
15
  set_baggage,
16
16
  )
17
17
  from botanu.sdk.decorators import botanu_outcome, botanu_workflow, run_botanu, workflow
18
- from botanu.sdk.span_helpers import emit_outcome, set_business_context
18
+ from botanu.sdk.span_helpers import (
19
+ emit_outcome,
20
+ set_business_context,
21
+ set_correlation,
22
+ )
19
23
 
20
24
  __all__ = [
21
25
  "BotanuConfig",
@@ -33,5 +37,6 @@ __all__ = [
33
37
  "run_botanu",
34
38
  "set_baggage",
35
39
  "set_business_context",
40
+ "set_correlation",
36
41
  "workflow",
37
42
  ]