Skip to content

Commit 8a1c192

Browse files
Merge pull request #27 from botanu-ai/developer
Developer
2 parents 65b0b9c + 38148cd commit 8a1c192

7 files changed

Lines changed: 55 additions & 30 deletions

File tree

docs/api/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ from botanu.sdk.config import BotanuConfig
1919
| `auto_detect_resources` | `bool` | `True` | Auto-detect cloud resources |
2020
| `otlp_endpoint` | `str` | From env / auto-configured when `BOTANU_API_KEY` is set / `"http://localhost:4318"` | OTLP endpoint |
2121
| `otlp_headers` | `dict` | `None` | Custom headers for OTLP exporter — always honored |
22-
| `content_capture_rate` | `float` | `0.0` | Prompt/response capture rate (0.0–1.0). See [Content Capture](../tracking/content-capture.md). |
22+
| `content_capture_rate` | `float` | `0.10` | Prompt/response capture rate (0.0–1.0). Default 10% sample. See [Content Capture](../tracking/content-capture.md). |
2323
| `pii_scrub_enabled` | `bool` | `True` | In-process PII scrub of captured content |
2424
| `pii_scrub_use_presidio` | `bool` | `False` | Add Microsoft Presidio NER to the scrub pipeline |
2525
| `max_export_batch_size` | `int` | `512` | Max spans per batch |

docs/getting-started/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class BotanuConfig:
8080
schedule_delay_millis: int = 5000
8181
export_timeout_millis: int = 30000
8282

83-
content_capture_rate: float = 0.0
83+
content_capture_rate: float = 0.10
8484
```
8585

8686
`BOTANU_API_KEY` is not a field on the dataclass — when the env var is set, `BotanuConfig` auto-configures `otlp_endpoint` + `otlp_headers` for the botanu-trusted endpoint.

scripts/pre_publish_check.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def run(
9797
capture: bool = True,
9898
) -> Tuple[int, str, str]:
9999
"""Run a command and return (returncode, stdout, stderr)."""
100-
result = subprocess.run(
100+
result = subprocess.run( # noqa: S603
101101
cmd,
102102
cwd=str(cwd) if cwd else None,
103103
env=env,
@@ -249,7 +249,7 @@ def check_api_surface(venv: Path) -> bool:
249249
print(DIM + (err or out)[-1500:] + RESET)
250250
return False
251251
if "MISSING:" in out and "ALL OK" not in out:
252-
missing_line = [line for line in out.split("\n") if "MISSING:" in line][0]
252+
missing_line = next(line for line in out.split("\n") if "MISSING:" in line)
253253
fail(missing_line)
254254
return False
255255
exports = [line for line in out.split("\n") if line.startswith("EXPORTS:")]
@@ -346,7 +346,7 @@ def check_smoke_test(venv: Path) -> bool:
346346
"OTEL_LOGS_EXPORTER": "console",
347347
"OTEL_METRICS_EXPORTER": "none",
348348
}
349-
code, out, err = run([str(py), "-c", SMOKE_TEST_SCRIPT], env=env)
349+
_code, out, err = run([str(py), "-c", SMOKE_TEST_SCRIPT], env=env)
350350
if "SMOKE_OK" in out:
351351
ok("decorator + outcome + validation all pass")
352352
return True
@@ -383,37 +383,37 @@ def main() -> int:
383383

384384
step(3, total, "python -m build")
385385
if not check_build():
386-
return summarize(results + [False])
386+
return summarize([*results, False])
387387
results.append(True)
388388

389389
step(4, total, "twine check")
390390
if not check_twine():
391-
return summarize(results + [False])
391+
return summarize([*results, False])
392392
results.append(True)
393393

394394
step(5, total, "create clean venv + install wheel")
395395
try:
396396
venv_dir = make_venv()
397397
except RuntimeError as e:
398398
fail(str(e))
399-
return summarize(results + [False])
399+
return summarize([*results, False])
400400
if not check_install(venv_dir):
401-
return summarize(results + [False])
401+
return summarize([*results, False])
402402
results.append(True)
403403

404404
step(6, total, "version string")
405405
if not check_version(venv_dir):
406-
return summarize(results + [False])
406+
return summarize([*results, False])
407407
results.append(True)
408408

409409
step(7, total, "public API surface (__all__)")
410410
if not check_api_surface(venv_dir):
411-
return summarize(results + [False])
411+
return summarize([*results, False])
412412
results.append(True)
413413

414414
step(8, total, "end-to-end smoke test")
415415
if not check_smoke_test(venv_dir):
416-
return summarize(results + [False])
416+
return summarize([*results, False])
417417
results.append(True)
418418

419419
finally:

src/botanu/models/run_context.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,6 @@ def to_span_attributes(self) -> Dict[str, Union[str, float, int, bool]]:
279279
if self.cancelled_at:
280280
attrs["botanu.run.cancelled_at"] = self.cancelled_at
281281
if self.outcome:
282-
# `botanu.outcome.status` is NOT emitted (removed 2026-04-16):
283-
# customer-reported outcome is trivially fakeable. Event outcome
284-
# is derived from eval verdict rollup / HITL / SoR instead.
285-
# Remaining fields are diagnostic only and stay for debugging.
286282
if self.outcome.reason_code:
287283
attrs["botanu.outcome.reason_code"] = self.outcome.reason_code
288284
if self.outcome.error_class:

src/botanu/sdk/bootstrap.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,22 @@ def enable(
163163
from botanu.sdk.config import _redact_url_credentials
164164

165165
logger.info(
166-
"Initializing Botanu SDK: service=%s, env=%s, endpoint=%s",
166+
"Initializing Botanu SDK: service=%s, env=%s, endpoint=%s, content_capture_rate=%s",
167167
cfg.service_name,
168168
cfg.deployment_environment,
169169
_redact_url_credentials(traces_endpoint),
170+
cfg.content_capture_rate,
170171
)
172+
if cfg.content_capture_rate <= 0.0:
173+
# Louder signal when the customer explicitly turned capture off —
174+
# evaluator judge will no-op on every span. Not a failure, just a
175+
# disable-by-choice worth flagging once at startup so a bug hunt
176+
# for "why are eval rollups empty" ends here (Codex 2026-04-24 P0 #1).
177+
logger.warning(
178+
"Botanu content_capture_rate=0.0 — set_input_content / set_output_content "
179+
"will not write span attributes, and the L2 evaluator judge will return "
180+
"no test case for any span. See docs/tracking/content-capture.md to enable."
181+
)
171182

172183
try:
173184
from opentelemetry import trace

src/botanu/sdk/config.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,23 @@ class BotanuConfig:
126126
schedule_delay_millis: int = 5000
127127
export_timeout_millis: int = 30000
128128

129-
# Content capture for eval — 0.0 disables entirely (default, privacy-safe).
130-
# Set to 1.0 for sandbox/shadow, 0.10-0.20 for production. Customers must also
131-
# call set_input_content() / set_output_content() on their trackers; this rate
132-
# gates whether those calls actually write to span attributes. In-process PII
133-
# scrubbing runs on the captured text before it hits the span (see
134-
# pii_scrub_* fields below); collector regex + evaluator Presidio NER
135-
# remain belt-and-suspenders.
136-
content_capture_rate: float = 0.0
129+
# Content capture for eval — 0.10 default (~10% sample). Pre-2026-04-24
130+
# this defaulted to 0.0, which meant the default install silently
131+
# produced no judgeable content: set_input_content / set_output_content
132+
# no-op'd, the L2 judge returned empty test cases, and eval rollups
133+
# stayed pending forever. Customers had to read the docs AND flip a
134+
# config flag before the "SDK in, eval out" loop worked at all.
135+
#
136+
# 0.10 is what the configuration docs recommend for production; it's
137+
# enough traffic for the judge to have real cases while keeping PII
138+
# exposure + storage cost bounded. Three PII-defense layers still
139+
# fire on every captured attribute:
140+
# 1. SDK in-process scrub (pii_scrub_enabled — default True below)
141+
# 2. Collector credential-regex scrub on content attribute prefixes
142+
# 3. Evaluator Presidio NER pass before judge calls
143+
# Set to 1.0 for sandbox/shadow, 0.0 to fully disable (e.g. HIPAA
144+
# where any content capture is a legal hazard).
145+
content_capture_rate: float = 0.10
137146

138147
# In-process PII scrubbing — runs on text passed to set_input_content /
139148
# set_output_content / set_retrieval_content before the span attribute is
@@ -424,7 +433,7 @@ def _from_dict(
424433
max_queue_size=export.get("queue_size", 65536),
425434
schedule_delay_millis=export.get("delay_ms", 5000),
426435
export_timeout_millis=export.get("export_timeout_ms", 30000),
427-
content_capture_rate=max(0.0, min(1.0, float(eval_cfg.get("content_capture_rate", 0.0)))),
436+
content_capture_rate=max(0.0, min(1.0, float(eval_cfg.get("content_capture_rate", 0.10)))),
428437
pii_scrub_enabled=bool(pii_cfg.get("enabled", True)),
429438
pii_scrub_disable_patterns=pii_cfg.get("disable_patterns"),
430439
pii_scrub_custom_patterns=pii_cfg.get("custom_patterns"),

tests/unit/test_config.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -454,12 +454,20 @@ def test_default_packages(self):
454454
class TestContentCaptureRate:
455455
"""Tests for the content_capture_rate field."""
456456

457-
def test_default_is_zero(self):
458-
"""Privacy-safe default: no content captured unless explicitly enabled."""
457+
def test_default_is_ten_percent(self):
458+
"""Default changed from 0.0 to 0.10 on 2026-04-24 (Codex P0 #1).
459+
460+
Pre-change: SDK worked out of the box except the evaluator judge
461+
silently returned no test case for every span because the helper
462+
methods (set_input_content / set_output_content) gated on rate > 0.0.
463+
0.10 matches the docs' recommended production value; three PII-defense
464+
layers (SDK scrub + collector credential scrub + Presidio NER) still
465+
fire on every captured attribute.
466+
"""
459467
with mock.patch.dict(os.environ, {}, clear=True):
460468
os.environ.pop("BOTANU_CONTENT_CAPTURE_RATE", None)
461469
config = BotanuConfig()
462-
assert config.content_capture_rate == 0.0
470+
assert config.content_capture_rate == 0.10
463471

464472
def test_explicit_value_respected(self):
465473
config = BotanuConfig(content_capture_rate=0.15)
@@ -486,7 +494,8 @@ def test_env_var_invalid_ignored(self):
486494
"""Invalid env values are ignored (default retained)."""
487495
with mock.patch.dict(os.environ, {"BOTANU_CONTENT_CAPTURE_RATE": "not_a_number"}):
488496
config = BotanuConfig()
489-
assert config.content_capture_rate == 0.0
497+
# Default since 2026-04-24 is 0.10, not 0.0.
498+
assert config.content_capture_rate == 0.10
490499

491500
def test_to_dict_roundtrip(self):
492501
config = BotanuConfig(content_capture_rate=0.1)

0 commit comments

Comments
 (0)