Skip to content

feat(voice): instrument judge STT pre-pass - #880

Open
prajvalrasik wants to merge 3 commits into
langwatch:mainfrom
prajvalrasik:feat/judge-stt-telemetry
Open

feat(voice): instrument judge STT pre-pass#880
prajvalrasik wants to merge 3 commits into
langwatch:mainfrom
prajvalrasik:feat/judge-stt-telemetry

Conversation

@prajvalrasik

@prajvalrasik prajvalrasik commented Aug 9, 2026

Copy link
Copy Markdown

Why

The judge pre-pass performs a second STT pass that was absent from voice.stt.transcribe telemetry in both runtimes, so STT volume and latency were undercounted. Closes #785.

What changed

  • Make telemetry opt-in at both judge boundaries so direct and public transcription callers keep their existing uninstrumented behavior.
  • Sanitize provider exceptions before OpenTelemetry and application logs record them, while preserving the existing per-message failure isolation.
  • Keep the TypeScript and Python attributes aligned and cover mixed success and failure batches in both runtimes.

How it works

prepareJudgeInput and JudgeAgent opt into a voice.stt.transcribe span for each audio message or segment. Direct TypeScript and Python transcription callers remain unchanged and uninstrumented.

Test plan

  • python/.venv/Scripts/python.exe -m pytest python/tests/voice/test_judge_stt_telemetry.py python/tests/test_judge_agent.py -q - 28 passed
  • cd javascript && vitest run src/voice/__tests__/judge-stt-telemetry.test.ts - 3 passed
  • cd javascript && eslint src/voice/__tests__/judge-stt-telemetry.test.ts
  • cd javascript && tsc --noEmit
  • Python syntax compilation for the changed module and test passed.

Human verification

This is an internal telemetry change with no UI surface. The regression tests verify the exported span attributes, mixed-batch isolation, absence of raw provider error content, and that direct transcription remains uninstrumented by default.

@rogeriochaves

Copy link
Copy Markdown
Contributor

Reviewing this so it can land. I have not pushed anything and I have not marked it ready. It is your PR.

CI is unblocked and green

The workflow runs were sitting at action_required. That is what fork PRs do until a maintainer approves them, so none of the checks had ever run since you opened it. I approved them. python-complete, javascript-complete and docs-complete all pass now.

I also checked it locally: it rebases cleanly on current main, and the Python tests pass on that rebase.

1. The Python span scope is hardcoded where the TypeScript one is gated

_transcribe_one always sets voice.stt.scope="judge".

On the TS side the span is opt-in. telemetryScope is only set by prepareJudgeInput, so a direct transcribeAudioMessages caller stays uninstrumented. That is exactly what the PR description says it does.

Python has no equivalent gate. transcribe_segments is a public export in scenario/voice/__init__.py, and its own docstring names a second intended caller: "Used by the judge fallback path for non-multimodal judges and as an opt-in to save_segments for richer manifests." So any non-judge caller would emit a span labelled judge.

Nothing is mislabelled today. judge_agent.py:496 is the only production caller, so the label is correct in practice. But it goes wrong the moment a second caller appears, and it is the kind of py-vs-ts asymmetry #785 asked to avoid.

Suggestion, mirroring the TS shape:

async def transcribe_segments(
    recording: VoiceRecording,
    provider: Optional[STTProvider] = None,
    only_missing: bool = True,
    telemetry_scope: Optional[str] = None,
) -> None:

Emit the span only when telemetry_scope is set, and pass telemetry_scope="judge" from judge_agent.py:496. Then both runtimes opt in at the judge boundary, instead of one opting in and the other assuming.

2. The WARNING log now loses the provider's own message

Worth knowing, not a change request.

Before, the outer handler formatted the original exception, so a default-level log read roughly:

STT failed for agent segment at 1.20s: ElevenLabs STT HTTP 401 (see DEBUG log for response body)

Now it formats the sanitized RuntimeError, so it reads:

STT failed for agent segment at 1.20s: STT provider failed: RuntimeError

and the real detail only exists at DEBUG.

This is defensible. It is a faithful copy of the #783 guard in voice/adapter.py, which does the same thing, and keeping response bodies out of telemetry is the whole point. Just flagging that operators lose a level of detail at default verbosity, and that the sanitized text reads oddly when the source was already a RuntimeError.

Test coverage

Good. Both runtimes, mixed success and failure inside one batch, and the sanitization asserted against the raw key and body rather than just the message shape. That last part is the one people usually skip.

To land it

Mark it ready for review, then get one approving review. The auto-approve workflow skips fork PRs by design, so it will never approve this one.

One red check you can ignore: Validate PR Title fails with Resource not accessible by integration. That is conventional-commits.yml setting validateSingleCommit: true while granting only pull-requests: read and statuses: write, so it breaks on any one-commit PR. Your title is fine, and it is not a required check.

@prajvalrasik
prajvalrasik force-pushed the feat/judge-stt-telemetry branch from 32ada9e to e28991d Compare August 12, 2026 17:01
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79a67961-0cc3-4fbc-ae12-5542cde160dc

📥 Commits

Reviewing files that changed from the base of the PR and between e28991d and fac2541.

📒 Files selected for processing (2)
  • javascript/src/voice/__tests__/judge-stt-telemetry.test.ts
  • python/scenario/voice/_transcribe.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • javascript/src/voice/tests/judge-stt-telemetry.test.ts
  • python/scenario/voice/_transcribe.py

Walkthrough

Judge pre-pass STT transcription now supports optional judge telemetry scoping in JavaScript and Python. Instrumented calls record audio and transcript metadata, sanitize provider errors, preserve per-message failures, and retain uninstrumented behavior for default callers.

Changes

Judge STT telemetry

Layer / File(s) Summary
JavaScript judge STT instrumentation
javascript/src/voice/judge-stt.ts, javascript/src/voice/__tests__/judge-stt-telemetry.test.ts
The judge scope propagates through message transcription. Scoped calls emit voice.stt.transcribe spans with scope, speaker, audio size, and transcript length. Provider errors are sanitized. Tests cover successful spans, mixed outcomes, default behavior, and sensitive-error exclusion.
Python judge STT instrumentation
python/scenario/voice/_transcribe.py, python/scenario/judge_agent.py, python/tests/test_judge_agent.py, python/tests/voice/test_judge_stt_telemetry.py
transcribe_segments accepts and propagates telemetry_scope. Judge calls use "judge". Scoped calls emit STT spans and sanitize provider errors, while default calls remain uninstrumented. Tests cover attributes, failure isolation, logging, transcript updates, and call arguments.

Sequence Diagram(s)

sequenceDiagram
  participant JudgePrePass
  participant ScopedTranscription
  participant STTProvider
  participant Telemetry
  JudgePrePass->>ScopedTranscription: request transcription with scope="judge"
  ScopedTranscription->>Telemetry: start voice.stt.transcribe span
  ScopedTranscription->>STTProvider: transcribe audio segment
  STTProvider-->>ScopedTranscription: transcript or provider error
  ScopedTranscription->>Telemetry: record metadata or sanitized error
  ScopedTranscription-->>JudgePrePass: transcript result per segment
Loading

Suggested reviewers: rogeriochaves, github-actions

Poem

Poem

A rabbit hops through spans of sound,
With judge-scoped traces neatly bound.
Errors lose their secrets fast,
Good transcripts safely hop past.
“Telemetry!” the rabbit sings,
While carrots sprout on async wings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #785 by instrumenting both runtimes, sanitizing telemetry errors, preserving isolation, and testing mixed outcomes.
Out of Scope Changes check ✅ Passed The code and tests remain focused on judge pre-pass STT telemetry and its required error-handling behavior.
Title check ✅ Passed The title clearly and concisely describes the main change: telemetry instrumentation for the judge STT pre-pass.
Description check ✅ Passed The description directly explains the telemetry changes, sanitization, compatibility behavior, tests, and issue objective.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@prajvalrasik

Copy link
Copy Markdown
Author

Thanks for the careful review. I addressed the Python scope asymmetry in e28991d: transcribe_segments now accepts an optional telemetry_scope and remains uninstrumented by default, while JudgeAgent explicitly opts in with telemetry_scope="judge".

I also added coverage proving a direct public call still transcribes without emitting a judge span, and updated the judge-boundary call assertions. The branch is rebased onto current main; the focused checks pass (48 Python tests, 10 TypeScript tests, Pyright, Ruff, ESLint, and tsc). I'll mark it ready once the refreshed CI completes.

@prajvalrasik
prajvalrasik marked this pull request as ready for review August 12, 2026 17:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
javascript/src/voice/__tests__/judge-stt-telemetry.test.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the test requirements.

Line 1 does not state how to run this test, required dependencies, coverage expectations, or a test example. Add a short header or a link to the project test documentation that provides these details.

As per coding guidelines: “Document testing requirements explaining how to run tests, test coverage requirements, test dependencies, and providing test examples.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@javascript/src/voice/__tests__/judge-stt-telemetry.test.ts` at line 1, Expand
the header comment in the judge-stt telemetry test to document how to run it,
required test dependencies, expected coverage, and a representative test
example, or link to the project’s existing testing documentation that covers all
four requirements.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@javascript/src/voice/__tests__/judge-stt-telemetry.test.ts`:
- Line 1: Expand the header comment in the judge-stt telemetry test to document
how to run it, required test dependencies, expected coverage, and a
representative test example, or link to the project’s existing testing
documentation that covers all four requirements.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eaec8783-ea60-40a8-89e9-e9dbcfd01c9d

📥 Commits

Reviewing files that changed from the base of the PR and between 524966d and e28991d.

📒 Files selected for processing (6)
  • javascript/src/voice/__tests__/judge-stt-telemetry.test.ts
  • javascript/src/voice/judge-stt.ts
  • python/scenario/judge_agent.py
  • python/scenario/voice/_transcribe.py
  • python/tests/test_judge_agent.py
  • python/tests/voice/test_judge_stt_telemetry.py

@langwatch-agent langwatch-agent added the hound-checked Triaged by the pr-hound agent at the current head SHA label Aug 13, 2026
@langwatch-agent langwatch-agent added the ci-green Latest run of every check is passing (checks API, not the legacy commit-status index) label Aug 13, 2026
@langwatch-agent

Copy link
Copy Markdown
Contributor

Human Review Brief

Mode: Targeted Review. Closes #785, and the PR does what the issue asks: the judge pre-pass STT was absent from voice.stt.transcribe, so volume and latency were undercounted.

Decisions being ratified

  1. Telemetry is opt-in at the judge boundaries rather than on by default in the transcribe path. Direct and public transcription callers stay uninstrumented, so the span means "a judge transcribed this", not "a transcription happened". Anyone reading voice.stt.transcribe volume later has to know that.
  2. Provider exceptions are sanitized before OpenTelemetry records them. That decides what an operator can see when a provider fails, permanently, and it is the kind of filter that quietly removes the one field someone needed.
  3. Attributes are kept aligned across the TypeScript and Python runtimes. Two implementations of one attribute set means the alignment has to be tested, not intended.

Must Check

  • What sanitization drops. Confirm the provider's error type or status code survives, even if the message does not. A sanitized exception that reduces to a generic string makes the new telemetry unable to answer why STT failed.
  • Mixed success and failure batches. The body says per-message failure isolation is preserved. Confirm a failing segment produces a span with an error status rather than no span, otherwise the undercount this PR fixes comes back for exactly the failing case.

Ask Author

If the opt-in flag is ever set on a direct caller, do the two runtimes emit identical attributes? That is the moment the alignment claim gets tested for real.

@langwatch-agent langwatch-agent added the review: targeted PR Hound review mode label Aug 13, 2026

@langwatch-agent langwatch-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one privacy issue in the Python STT failure path. I reviewed this external contribution statically; I did not execute contributor code or install dependencies.

LangWatch-Review: verdict=findings sha=e28991d907daaac970b3d5c30d50954799ba1076 p0=0 p1=0 p2=1 p3=0

Comment thread python/scenario/voice/_transcribe.py Outdated
# Provider SDK errors can include response bodies and key fragments.
# Keep the raw detail local and let telemetry record only a minimal
# provider-agnostic exception, matching the #783 STT guard.
logger.debug(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Do not log the raw provider exception in the sanitized path. exc_info=True serializes the original exception message and traceback into debug logs. STT SDK failures can contain provider response bodies or credential fragments—the exact data this change prevents from reaching telemetry—so enabling debug logging reintroduces that disclosure under a second observable path. Log only the exception type (or remove this debug entry) before raising the sanitized RuntimeError.

@drewdrewthis drewdrewthis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated pr-review pass. Inline findings below; verdict comment upserted separately.

Comment thread python/scenario/voice/_transcribe.py Outdated
# provider-agnostic exception, matching the #783 STT guard.
logger.debug(
"scenario.voice.transcribe: STT provider error detail",
exc_info=True,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[review-clerk, Fix] This logger.debug(..., exc_info=True) still serializes the raw provider exception (message + traceback) into the scenario.voice DEBUG logger — the exact content this PR sanitizes out of the OTel span. If DEBUG logging is ever enabled for this logger (local debugging, a verbose log level in prod), provider response bodies / key fragments (e.g. sk-...) leak through a second, unsanitized channel, defeating the PR's own stated goal.

This concurs with the still-unresolved langwatch-agent review thread on this same line (posted 2026-08-13T06:52:00Z, isResolved=false) — flagging as a review-clerk finding too so it participates in this review's own gate. Suggest: drop exc_info=True (log only type(exc).__name__, already available at line 115) or move the raw detail behind an explicit opt-in flag, never a bare DEBUG level on this logger.

expect(warn.mock.calls[0]![0]).toContain("STT provider failed: Error");
expect(warn.mock.calls[0]![0]).not.toContain(rawError);
});
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[review-clerk, Fix/New AC] Missing test: nothing here proves a direct (non-judge) transcribeAudioMessages call stays uninstrumented — i.e. emits zero voice.stt.transcribe spans when telemetryScope is omitted.

The Python side has this exact test (test_direct_transcription_is_uninstrumented_by_default in python/tests/voice/test_judge_stt_telemetry.py), and the PR description explicitly claims parity: "Direct TypeScript and Python transcription callers remain unchanged and uninstrumented." Right now only the Python half of that claim is evidenced. Add a JS sibling test calling transcribeAudioMessages (or the underlying non-judge path) without telemetryScope and asserting exporter.getFinishedSpans() has no voice.stt.transcribe span.

@drewdrewthis drewdrewthis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See verdict comment below for full details. NOT-READY: 2 new review-clerk findings (DEBUG-log leak of raw provider exception; missing JS parity test) plus 1 pre-existing unresolved thread from langwatch-agent. CI has not actually run on this SHA — see verdict.

@drewdrewthis

drewdrewthis commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Review verdict: NOT-READY

Reviewed at: e28991d9 · Run: pr-review (reviewer: @drewdrewthis)

CI status — the green on this SHA does not cover the changed code

python-ci, javascript-ci, and docs-ci never ran on e28991d9 — all three check-suites are action_required (fork-PR workflow-approval gate), with zero corresponding entries in the Actions run list. A maintainer (rogeriochaves) approved these once for the prior commit 00193e5, but the follow-up push e28991d9 reset the gate and nobody re-approved. The checks that are green (Validate PR Title, action-semantic-pull-request, CodeRabbit, plus several SKIPPED unrelated ops-automation jobs — preflight/evaluate/firefighting/dismiss-firefighting-approval) are title-lint and AI-review only; none of them execute pytest or vitest. No test in this diff (JS or Python) has actually run against this SHA. This is a variant of fm.ci-green-on-unrun-mechanism, worse than the known step-skip case: here the whole workflow is blocked, not one guarded step inside a green rollup. A maintainer must re-approve the workflow runs before this SHA's test claims can be trusted.

PR-body gaps (pr-ready-check C5/C6)

Body has "## Test plan" and "## How I can prove I was successful" but no "## Human verification" heading, and no explicit backend-only-change declaration under that exact heading — this is an internal telemetry change with no UI surface, so the fix is administrative (rename/add the heading), not a design problem.

Blocking — must resolve before this PR is done

  1. [review-clerk] python/scenario/voice/_transcribe.py:112 logger.debug(..., exc_info=True) still serializes the raw provider exception into the scenario.voice DEBUG logger — the same content this PR removes from the OTel span. -> thread: feat(voice): instrument judge STT pre-pass #880 (comment)
  2. [review-clerk] javascript/src/voice/__tests__/judge-stt-telemetry.test.ts:110 No JS test proves a direct (non-judge) transcribeAudioMessages call stays uninstrumented, unlike the Python suite's test_direct_transcription_is_uninstrumented_by_default. -> thread: feat(voice): instrument judge STT pre-pass #880 (comment)
  3. [langwatch-agent, pre-existing, still unresolved] Same DEBUG-log leak as Add Mintlify documentation #1 above, flagged independently by langwatch-agent at 2026-08-13T06:52Z on python/scenario/voice/_transcribe.py:110 — not yet resolved or addressed by any commit. -> thread: feat(voice): instrument judge STT pre-pass #880 (comment)

Non-blocking

  • [review-clerk] The top-level langwatch-agent "Human Review Brief" comment (id 5276906102) is an orphaned, non-gating artifact by design (no resolvable thread, carries no verdict sentinel) — not stale, just not something that needs action.
  • [review-clerk] rogeriochaves's earlier top-level review (on commit 00193e5) raised the JS/Python telemetry-scope asymmetry; the author addressed it in e28991d9 (telemetry_scope is now Optional[str] = None, opt-in at the judge boundary in both runtimes). That thread's concern is resolved by the diff; only the log-detail-loss note in the same comment was explicitly "not a change request."

Verdict is prose, not a GitHub approval. Scope: review findings only — READY means no unresolved blocking review threads at this SHA. It is not a merge-readiness signal; that is pr-ready-check.sh (8 criteria, of which this verdict is C3), which also gates on CI, PR-body format, and visual proof.

@langwatch-agent langwatch-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No actionable correctness, security, or integrity finding in the external diff. The judge-only instrumentation leaves public direct transcription unchanged and replaces provider errors before telemetry/logging observes them. I performed static review only; contributor code and dependencies were not executed.

LangWatch-Review: verdict=clean sha=fac25413a1562c1f16b4dcb415cc8df7a056a5a5 p0=0 p1=0 p2=0 p3=0

@langwatch-agent langwatch-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

External static review: no blocking concern found in the current diff. I did not execute branch code, install dependencies, or run contributor-provided scripts. Residual risk: runtime behavior remains covered by the repository CI.

LangWatch-Review: verdict=clean sha=fac25413a1562c1f16b4dcb415cc8df7a056a5a5 p0=0 p1=0 p2=0 p3=0

@langwatch-agent langwatch-agent added the blocked-with-author Red CI, conflicts, or changes requested. With the author, not a reviewer. label Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked-with-author Red CI, conflicts, or changes requested. With the author, not a reviewer. ci-green Latest run of every check is passing (checks API, not the legacy commit-status index) hound-checked Triaged by the pr-hound agent at the current head SHA review: targeted PR Hound review mode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(voice): instrument the judge pre-pass STT (voice.stt.transcribe undercounts)

4 participants