Skip to content

Feat(voice agent)/documentation#452

Open
Co-vengers wants to merge 52 commits intoGetBindu:mainfrom
Co-vengers:feat(voice-agent)/documentation
Open

Feat(voice agent)/documentation#452
Co-vengers wants to merge 52 commits intoGetBindu:mainfrom
Co-vengers:feat(voice-agent)/documentation

Conversation

@Co-vengers
Copy link
Copy Markdown
Contributor

@Co-vengers Co-vengers commented Apr 14, 2026

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: Voice setup, env requirements, and review plan were scattered or out of date versus the implemented feature.
  • Why it matters: Reviewers and users need a stable runbook and accurate configuration reference to run/verify voice reliably.
  • What changed: Added/updated voice runbook docs and plan; documented the stacked PR ladder; updated READMEs to reference voice where appropriate.
  • What did NOT change (scope boundary): No runtime code changes; no dependency changes; no API changes.

Change Type (select all that apply)

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Security hardening
  • Tests
  • Chore/infra

Scope (select all touched areas)

  • Server / API endpoints
  • Extensions (DID, x402, etc.)
  • Storage backends
  • Scheduler backends
  • Observability / monitoring
  • Authentication / authorization
  • CLI / utilities
  • Tests
  • Documentation
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #

User-Visible / Behavior Changes

  • Documentation-only: updated voice runbook and plan; clarified provider-dependent env vars and the PR ladder.

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/credentials handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Database schema/migration changes? (Yes/No) No
  • Authentication/authorization changes? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Verification

Environment

  • OS:
  • Python version:
  • Storage backend:
  • Scheduler backend:

Steps to Test

  1. Review docs for accuracy vs implementation (settings/env/provider requirements).
  2. Follow the runbook to start a local example and confirm the documented commands match reality.

Expected Behavior

  • Documentation matches implemented env vars/providers and the current review workflow.

Actual Behavior

Evidence (attach at least one)

  • Failing test before + passing after
  • Test output / logs
  • Screenshot / recording
  • Performance metrics (if relevant)

Human Verification (required)

What you personally verified (not just CI):

  • Verified scenarios:
  • Edge cases checked:
  • What you did NOT verify:

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No (documentation-only)
  • Database migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert this PR.
  • Files/config to restore: docs/ and README files only.
  • Known bad symptoms reviewers should watch for: inaccurate setup steps causing voice session preflight failures.

Risks and Mitigations

None

Checklist

  • Tests pass (uv run pytest)
  • Pre-commit hooks pass (uv run pre-commit run --all-files)
  • Documentation updated (if needed)
  • Security impact assessed
  • Human verification completed
  • Backward compatibility considered

Summary by CodeRabbit

  • New Features

    • End-to-end real-time voice: server REST/WebSocket endpoints, session management, STT→agent→TTS pipeline, multi-provider STT/TTS, VAD, interruption handling, and frontend voice UI (call button, panel, live transcript) with client-side voice streaming.
  • Bug Fixes

    • Hardened key file permissions across platforms for improved local security.
  • Documentation

    • Added voice runbook, implementation plan, example README, env templates, and usage docs.
  • Tests

    • New unit/integration tests covering voice components, session manager, service factory, and endpoints.
  • Chores

    • Updated ignores, pre-commit config, dependency extras, examples, and frontend env defaults.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

Adds a real‑time voice extension: server REST/WebSocket endpoints, in‑memory and Redis session managers, STT/TTS service factories, a pipecat AgentBridge processor, frontend voice client/UI and stores, example voice agent, docs, tests, and supporting config/tooling updates.

Changes

Cohort / File(s) Summary
Voice Extension Core
bindu/extensions/voice/__init__.py, bindu/extensions/voice/voice_agent_extension.py, bindu/extensions/voice/agent_bridge.py, bindu/extensions/voice/audio_config.py
New voice package: exported VoiceAgentExtension, audio constants/validation, and AgentBridgeProcessor implementing STT→agent→TTS bridging, streaming/delta handling, timeouts, interruptions, and history management.
Voice Service & Pipeline
bindu/extensions/voice/service_factory.py, bindu/extensions/voice/pipeline_builder.py
Factories to create STT/TTS services (Deepgram, Piper, ElevenLabs, Azure) and build_voice_pipeline wiring STT → AgentBridge → TTS, with optional VAD.
Session Management
bindu/extensions/voice/session_manager.py, bindu/extensions/voice/redis_session_manager.py, bindu/extensions/voice/session_factory.py
In‑memory and Redis session manager implementations, TTL/cleanup loops, atomic Redis Lua scripts, and async factory/close helpers.
Server Endpoints & Integration
bindu/server/endpoints/voice_endpoints.py, bindu/server/applications.py, bindu/server/endpoints/utils.py
REST routes for session start/end/status and /ws/voice/{session_id} WebSocket; app lifecycle init/teardown of session manager; HTTPException passthrough in error wrapper.
Frontend Voice Client & Stores
frontend/src/lib/services/voice-client.ts, frontend/src/lib/stores/voice.ts
VoiceClient for session HTTP + WebSocket, audio capture via AudioWorklet, duplex/hold logic, binary/audio/frame handling; Svelte stores for session state, transcripts, audio, and control APIs.
Frontend Voice UI Components
frontend/src/lib/components/voice/VoiceCallButton.svelte, frontend/src/lib/components/voice/VoiceCallPanel.svelte, frontend/src/lib/components/voice/LiveTranscript.svelte
New UI components: call button, voice call panel with playback/mute/end controls and live transcript display.
Chat & File Part Integration (Frontend)
frontend/src/lib/components/chat/ChatInput.svelte, frontend/src/lib/components/chat/ChatWindow.svelte, frontend/src/lib/utils/agentMessageHandler.ts, frontend/src/lib/components/chat/ChatMessage.svelte
Message submission now supports base64 fileParts, async file conversion, updated send APIs, composer voice integration, and improved click/key handling.
Agent Bridge & Tests
tests/unit/extensions/voice/*, tests/unit/extensions/voice/test_voice_endpoints.py, tests/unit/extensions/voice/test_voice_websocket_integration.py
Extensive unit/integration tests for AgentBridgeProcessor, service factory, session managers, endpoints, WebSocket auth/timeouts, transcript trimming, rate limiting and control frames.
Examples & Docs
examples/voice-agent/*, docs/VOICE.md, docs/VOICE_AGENT_PLAN.md, SPLIT_PLAN.md
New runnable example voice agent, README, .env.example, skill, runbooks, implementation plan, and PR split workflow doc.
Notifications / Security Hardening
bindu/utils/notifications.py, bindu/utils/capabilities.py, bindu/utils/logging.py
Port-aware DNS resolution, destination validation and HTTP delivery rewrite, added get_voice_extension_from_capabilities reexport and minor typing casts.
Worker/File Handling & Protocols
bindu/utils/worker/messages.py, bindu/utils/worker/parts.py, bindu/common/protocol/types.py
File part shape changed to nested part["file"], added MAX_FILE_SIZE guard and robust text decoding; DataPart/FilePart TypedDict adjustments.
Tracing, Scheduler & Tests
bindu/server/workers/base.py, bindu/server/scheduler/memory_scheduler.py, tests/conftest_stubs.py
Span reconstruction helper for OpenTelemetry, bounded task queue with send timeout, and added span-context test stubs.
gRPC Optional Dependencies
bindu/grpc/client.py, bindu/grpc/server.py, bindu/grpc/service.py
Made gRPC/protobuf imports optional with _require_grpc_dependencies() guards and clearer install hints.
Settings, Packaging & CI hooks
bindu/settings.py, pyproject.toml, .pre-commit-config.yaml, pytest.ini, .gitignore, frontend/.env.example
Added VoiceSettings to main settings, voice extra and pipecat deps in pyproject, pre-commit exclusions, pytest default marker filter, and gitignore entries for frontend build/artifacts and runtime voice model files.
Frontend Misc & Typings
multiple frontend files (server/*, models.ts, routes, components, migrations, constants)
Various frontend fixes: PUBLIC_AGENT_BASE_URL handling, model metadata fields, in‑memory DB mock improvements, upload/download robustness, prompt truncation fix, and typing/import adjustments.
Examples & Small Fixes
various examples and tests (examples/pdf_research_agent/*, examples/*, tests/*)
PDF example stricter checks, skill metadata updates, example updates, test adjustments and markers.
Scripts & Tooling
scripts/test_voice_providers.sh, SPLIT_PLAN.md
Added voice provider connectivity smoke-test script and PR split plan document.
Other minor changes
.gitignore, .pre-commit-config.yaml, small server/store/util edits
Misc ignore entries, pre-commit exclude for .agents/, minor validator/logging tweaks and typing refinements across the codebase.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant WS as WebSocket Endpoint
    participant Pipeline as Voice Pipeline
    participant STT as STT Service
    participant Agent as Agent Bridge
    participant TTS as TTS Service

    Browser->>WS: Connect (optional session_token)
    WS->>Pipeline: build_voice_pipeline(stt, bridge, tts)

    rect rgba(200, 150, 255, 0.5)
    Browser->>WS: Send PCM16 audio frames
    WS->>Pipeline: forward audio frames
    Pipeline->>STT: stream audio
    STT->>Pipeline: emit transcription
    end

    rect rgba(150, 200, 255, 0.5)
    Pipeline->>Agent: deliver transcription
    Agent->>Agent: run manifest_run (agent logic)
    Agent->>Pipeline: emit text response (streamed/final)
    end

    rect rgba(200, 200, 150, 0.5)
    Pipeline->>TTS: stream text → TTS
    TTS->>Pipeline: emit PCM16 audio
    Pipeline->>WS: send audio frames
    WS->>Browser: playback audio
    end

    Browser->>WS: Send control (mute/stop)
    WS->>Pipeline: handle interruption / cancel agent task
    WS->>Browser: send state updates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through websockets, bytes in tow,

I whispered text where audio flows.
STT listens, agents think and play,
TTS sings the answers back your way.
A rabbit cheers—voice is here to stay!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@Co-vengers Co-vengers mentioned this pull request Apr 14, 2026
8 tasks
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

♻️ Duplicate comments (1)
frontend/src/lib/types/Session.ts (1)

1-1: ⚠️ Potential issue | 🔴 Critical

Same library import change found here.

This file has the identical bsonbson-objectid import change as ConvSidebar.ts. The same concerns apply: this is a code change that contradicts the documentation-only PR scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/types/Session.ts` at line 1, The import of ObjectId in
Session.ts was changed to "bson-objectid", which duplicates the same unintended
library swap from ConvSidebar.ts and violates the docs-only PR scope; revert the
change so Session.ts uses the original bson import used across the project by
restoring the previous import for the ObjectId type (i.e., match the original
"bson" import pattern) so the symbol referencing ObjectId in Session.ts aligns
with the rest of the codebase.
🟠 Major comments (22)
.pre-commit-config.yaml-1-1 (1)

1-1: ⚠️ Potential issue | 🟠 Major

Global .agents/ exclusion bypasses security hooks

Line 1 sets a top-level exclude, so hooks like detect-secrets/bandit won’t run on .agents/**. That weakens repo-wide security scanning. Prefer per-hook excludes for only the expensive/style hooks instead of a global exclusion.

Suggested adjustment
-exclude: ^\.agents/
 repos:
@@
   - repo: https://github.com/astral-sh/ruff-pre-commit
@@
-        exclude: ^(examples/|bindu/grpc/generated/)
+        exclude: ^(examples/|bindu/grpc/generated/|\.agents/)
@@
       - id: ruff-format
-        exclude: ^(examples/|bindu/grpc/generated/)
+        exclude: ^(examples/|bindu/grpc/generated/|\.agents/)
@@
       - id: ty
@@
-        exclude: ^(examples/|bindu/grpc/generated/)
+        exclude: ^(examples/|bindu/grpc/generated/|\.agents/)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.pre-commit-config.yaml at line 1, The top-level "exclude: ^\.agents/" entry
disables all pre-commit hooks for .agents/**; remove this global exclude and
instead add per-hook "exclude" patterns only to the specific expensive/style
hooks you want to skip (e.g., add excludes to the individual hook entries for
tools like detect-secrets, bandit, black, prettier, etc.), leaving security
hooks such as detect-secrets and bandit without the .agents/ exclusion so they
still run on that path.
frontend/src/lib/components/chat/ChatMessage.svelte-70-84 (1)

70-84: ⚠️ Potential issue | 🟠 Major

Don't let the wrapper match its own interactive guard.

At Line 271 the assistant container becomes role="button", so the closest(... [role='button'] ...) check at Line 75 now matches e.currentTarget for every click inside the message. handleMessageClick therefore returns before toggling or opening the image lightbox, which regresses the new tap behavior. The key handler should also ignore bubbled events from nested controls.

Suggested fix
 function handleMessageClick(e: MouseEvent) {
-	const target = e.target as HTMLElement | null;
-	if (
-		target &&
-		target.closest(
-			"button, a, input, textarea, select, [role='button'], [data-no-toggle]"
-		)
-	) {
+	const target = e.target;
+	const interactiveAncestor =
+		target instanceof Element
+			? target.closest(
+					"button, a, input, textarea, select, [role='button'], [data-no-toggle]"
+				)
+			: null;
+	if (interactiveAncestor && interactiveAncestor !== e.currentTarget) {
 		return;
 	}
 	handleContentClick(e);
-	if (!(e.target instanceof HTMLImageElement)) {
+	if (!(target instanceof HTMLImageElement)) {
 		isTapped = !isTapped;
 	}
 }
 		onkeydown={(event) => {
+			if (event.target !== event.currentTarget) return;
 			if (event.key === "Enter" || event.key === " ") {
 				event.preventDefault();
 				isTapped = !isTapped;
 			}
 		}}

Also applies to: 271-279

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/components/chat/ChatMessage.svelte` around lines 70 - 84,
The click guard in handleMessageClick is accidentally matching the message
wrapper itself (which has role="button") and also returns for bubbled events
from nested controls; update the guard to only bail out when the closest
interactive ancestor is present AND is not the message container itself by
checking the matched element !== e.currentTarget (or comparing to
e.currentTarget via target.closest(...) !== e.currentTarget), and also ensure
you ignore bubbled events by checking e.target !== e.currentTarget before
toggling/opening; keep calls to handleContentClick and toggling isTapped
unchanged otherwise.
bindu/extensions/did/did_agent_extension.py-220-226 (1)

220-226: ⚠️ Potential issue | 🟠 Major

Use a temporary file with atomic replacement to eliminate permission window.

When recreate_keys=True and the private key file already exists, os.open(..., O_TRUNC, 0o600) truncates the file but preserves its existing permissions until the subsequent os.chmod() call. If the pre-existing file had overly-permissive permissions (0o644 or worse), the newly-written private key content is briefly exposed with those permissions. Replace the in-place truncation with a secure temp file creation and os.replace() to ensure the new key is never readable with incorrect permissions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/did/did_agent_extension.py` around lines 220 - 226, The
current logic writing private_pem to self.private_key_path using os.open with
O_TRUNC can expose the key during the short window before os.chmod; instead,
create a secure temporary file in the same directory (e.g., using
tempfile.NamedTemporaryFile or os.open on a temp name) with mode 0o600, write
and flush/fsync the private_pem to that temp file, close it, then atomically
replace the target file with os.replace(temp_path, str(self.private_key_path)),
and finally ensure permissions on the final file are 0o600; update the code
paths in the method that uses self.private_key_path and the recreate_keys=True
branch to follow this temp-file + os.replace pattern.
bindu/utils/notifications.py-62-69 (1)

62-69: ⚠️ Potential issue | 🟠 Major

Normalize IPv4-mapped IPv6 addresses before checking against blocked networks.

IPv4-mapped IPv6 addresses like ::ffff:127.0.0.1 bypass the block checks because they don't match any IPv4 CIDR in _BLOCKED_NETWORKS. A caller can use these forms to target internal services, defeating the SSRF protection.

Add a helper function to unwrap IPv4-mapped addresses before the membership test in both locations (lines 62–69 and 165–171):

Suggested change
+def _normalize_checked_address(
+    addr: ipaddress.IPv4Address | ipaddress.IPv6Address,
+) -> ipaddress.IPv4Address | ipaddress.IPv6Address:
+    if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped is not None:
+        return addr.ipv4_mapped
+    return addr
+
     for addr in addrs:
+        addr = _normalize_checked_address(addr)
         for blocked in _BLOCKED_NETWORKS:
             if addr in blocked:
                 raise ValueError(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/utils/notifications.py` around lines 62 - 69, Normalize IPv4-mapped
IPv6 addresses before blocked-network checks by adding a small helper (e.g.,
_normalize_ip(ip_obj)) that detects IPv4-mapped IPv6 addresses
(addr.ipv4_mapped) and returns the corresponding IPv4Address, otherwise returns
the original ip object; call this helper on resolved_ip (and the other IP
variable used in the second check) before iterating over _BLOCKED_NETWORKS so
the membership test compares like-for-like and IPv4-mapped forms (e.g.,
"::ffff:127.0.0.1") are treated as 127.0.0.1 when checking containment.
frontend/src/lib/server/database.ts-147-159 (1)

147-159: ⚠️ Potential issue | 🟠 Major

Pipeline updates using field references and aggregation syntax are not properly supported.

The migration in 10-update-reports-assistantid.ts uses aggregation-pipeline semantics (contentId: "$assistantId" for field reference and $unset: "assistantId" as a string), but applyUpdate() only handles update-document operators. Specifically:

  • contentId: "$assistantId" will set the literal string "$assistantId" instead of copying the field value
  • $unset: "assistantId" (string) won't work because the code calls Object.keys() on it, expecting an object

This makes local migrations and in-memory tests produce incorrect results compared to production MongoDB behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/server/database.ts` around lines 147 - 159, The in-memory
pipeline update logic must emulate MongoDB aggregation semantics so field
references and string-form $unset work: modify applyUpdate (and the pipeline
application in the update loop) to resolve string values that start with "$" as
field references by reading the value from the current document (e.g., treat
"contentId: \"$assistantId\"" as contentId = currentDoc.assistantId), and handle
$unset when its operand is a string or array by deleting those keys (not calling
Object.keys on a string). Ensure the pipeline reducer passes the current working
document (not the original raw object) into applyUpdate and that applyUpdate
recognizes pipeline-stage operators ($set, $unset, etc.), supporting $unset:
"fieldName" and $unset: ["a","b"] as well as object forms.
.github/workflows/ci.yml-43-43 (1)

43-43: ⚠️ Potential issue | 🟠 Major

Unit-test job now runs broader test scope than intended.

Line 43 switches from unit-only targeting to tests/, which can include non-unit suites in a job explicitly labeled “Unit Tests.”

Proposed fix
-          uv run pytest tests/ \
+          uv run pytest tests/unit/ \

Based on learnings: "Applies to tests/unit/**/*.py : Place unit tests in tests/unit/ directory with pytest and pytest.mark.asyncio decorator for async tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 43, The CI unit-test step currently invokes
"uv run pytest tests/" which runs the entire test tree; change this to run only
the unit tests (e.g., "uv run pytest tests/unit/" or "uv run pytest
'tests/unit/**/*.py'") so the job labeled Unit Tests only executes tests in the
tests/unit directory; update the pytest invocation in the unit-test job to the
unit-only path pattern.
bindu/extensions/voice/agent_bridge.py-297-310 (1)

297-310: ⚠️ Potential issue | 🟠 Major

Cancel the pending first-chunk task on timeout before returning the fallback.

If the overall response timeout fires while first_task is still awaiting anext(chunks), this method returns the fallback without cancelling that child task. The agent stream can keep running in the background after the session has already moved back to listening.

Proposed fix
             chunks = self._iter_text_chunks(raw)
+            first_task: asyncio.Task[str] | None = None
             try:
                 async with asyncio.timeout(self._total_response_timeout_seconds):
                     if emit_frames:
                         try:
-                            first_task = asyncio.create_task(anext(chunks))
+                            first_task = asyncio.create_task(anext(chunks))
                             timeout_seconds = max(
                                 0.0, self._first_token_timeout_seconds
                             )
                             if timeout_seconds > 0:
                                 done, _pending = await asyncio.wait(
@@
             except TimeoutError:
+                if first_task and not first_task.done():
+                    first_task.cancel()
+                    await asyncio.gather(first_task, return_exceptions=True)
                 if emit_frames:
                     self._set_state("error")
                     fallback = DEFAULT_TIMEOUT_FALLBACK_TEXT
                     if self._on_agent_transcript:
                         self._safe_callback(self._on_agent_transcript, fallback, True)
                     await self.push_frame(TextFrame(text=fallback))
                     self._set_state("listening")
                     return fallback
                 return None

Also applies to: 330-339

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/agent_bridge.py` around lines 297 - 310, The timeout
branch around creating first_task (first_task =
asyncio.create_task(anext(chunks))) should cancel and await that pending task
before returning the fallback: when timeout_seconds > 0 and not done, call
first_task.cancel(), await it (catch asyncio.CancelledError/asyncio.InvalidState
as needed) to ensure the child coroutine is cleaned up, then push the
DEFAULT_THINKING_TEXT via push_frame; apply the same cancellation-and-await
pattern to the equivalent block handling the overall response timeout (the code
referenced at lines 330-339) so no anext(chunks) task continues running after
returning the fallback.
examples/document-analyzer/document_analyzer.py-76-83 (1)

76-83: ⚠️ Potential issue | 🟠 Major

Don't swallow DOCX extraction failures here.

Returning "" makes corrupt DOCX files look like successfully extracted documents, so handler() can call the model with an empty document body instead of reporting a parse error.

Proposed fix
 def extract_text_from_docx(file_bytes):
     """Extract text from docx bytes"""
     try:
         doc = Document(io.BytesIO(file_bytes))
         return "\n".join([p.text for p in doc.paragraphs])
     except Exception as e:
-        logger.error(f"Error extracting DOCX text: {e}")
-        return ""
+        logger.exception("Error extracting DOCX text")
+        raise ValueError(f"Invalid DOCX file: {e}") from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/document-analyzer/document_analyzer.py` around lines 76 - 83, The
extract_text_from_docx function currently swallows all exceptions and returns an
empty string, which hides parse failures from handler(); instead log the error
then let the failure propagate so handler() can report a parse error—modify
extract_text_from_docx to log the exception (using logger.error) and then
re-raise the exception (or raise a specific ParseError) rather than returning
"", so callers like handler() receive the error and can handle/report it.
bindu/extensions/voice/audio_config.py-32-34 (1)

32-34: ⚠️ Potential issue | 🟠 Major

Include channel count in generic frame-size calculations.

get_frame_size() ignores the number of channels, so any non-mono configuration will under-allocate frame bytes and mis-size the stream.

Proposed fix
-def get_frame_size(sample_rate: int, duration_ms: int, encoding: str) -> int:
+def get_frame_size(
+    sample_rate: int,
+    duration_ms: int,
+    encoding: str,
+    channels: int = DEFAULT_CHANNELS,
+) -> int:
     """Return a frame size in bytes for the given encoding."""
-    return sample_rate * duration_ms // 1000 * get_bytes_per_sample(encoding)
+    if channels < 1:
+        raise ValueError(f"Channel count must be positive, got {channels}")
+    return (
+        sample_rate
+        * duration_ms
+        // 1000
+        * channels
+        * get_bytes_per_sample(encoding)
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/audio_config.py` around lines 32 - 34, get_frame_size
currently computes bytes per frame without accounting for multi-channel audio,
causing under-allocation when channels != 1; update the function signature
get_frame_size(sample_rate: int, duration_ms: int, encoding: str, channels: int
= 1) to accept a channels parameter (default 1) and multiply the returned frame
byte count by channels (i.e., sample_rate * duration_ms // 1000 *
get_bytes_per_sample(encoding) * channels); also search for and update any
callers of get_frame_size to pass the appropriate channels value (or rely on the
default) so mono/stereo/etc. are handled correctly.
bindu/utils/worker/parts.py-38-38 (1)

38-38: ⚠️ Potential issue | 🟠 Major

Runtime behavior changed in a docs-scoped PR.

Line 38 modifies worker fallback payload construction, which is production behavior. The stated scope says documentation-only; please split runtime changes into a separate PR (or explicitly update scope and include targeted tests/release notes).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/utils/worker/parts.py` at line 38, The change to runtime behavior in
parts.py modifies how a worker fallback payload is constructed by returning
DataPart(kind="data", data=data); revert this runtime change in the docs-scoped
PR and move the DataPart construction into a separate functional PR (or
alternatively update this PR's scope to include runtime changes and add targeted
unit tests and release notes). Locate the code that returns the fallback payload
(the return that constructs DataPart and any surrounding function handling
worker fallbacks) and restore the original behavior in this PR, then create a
new PR that introduces DataPart(kind="data", data=data) along with tests for the
worker fallback path and a release note describing the behavior change.
frontend/src/routes/conversation/[id]/+page.svelte-252-257 (1)

252-257: ⚠️ Potential issue | 🟠 Major

Pass file attachments to direct-agent sendAgentMessage call.

The base64Files array is prepared at lines 139–145 and correctly passed to the backend flow at line 270, but when using the direct agent API (lines 249–257), fileParts is omitted from the sendAgentMessage options. File uploads will be silently dropped in the direct-agent path while working correctly via the backend. Pass the prepared files: fileParts: base64Files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/conversation/`[id]/+page.svelte around lines 252 - 257,
The direct-agent call to sendAgentMessage is missing the prepared file
attachments, so add fileParts: base64Files to the options object passed to
sendAgentMessage (the same object that currently includes abortSignal:
messageUpdatesAbortController.signal, currentTaskId, taskState, and
replyToTaskId). Locate the sendAgentMessage invocation and include the fileParts
property referencing the base64Files variable so file uploads are forwarded in
the direct-agent path.
bindu/server/scheduler/memory_scheduler.py-31-33 (1)

31-33: ⚠️ Potential issue | 🟠 Major

Move queue size and enqueue timeout to app_settings (no local constants).

Line 33 and Line 80 hardcode operational config in bindu/**/*.py. This should be sourced from bindu.settings.app_settings to keep runtime behavior centrally configurable.

As per coding guidelines "Use bindu.settings.app_settings for all configuration - never hardcode values such as URLs, ports, timeouts, API keys, or feature flags" and "NEVER create local config constants - use app_settings from bindu.settings."

Also applies to: 80-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/scheduler/memory_scheduler.py` around lines 31 - 33, Replace the
local hardcoded config in memory_scheduler.py with values from
bindu.settings.app_settings: remove _TASK_QUEUE_BUFFER_SIZE and any local
enqueue timeout constant and instead import app_settings from bindu.settings and
reference app_settings.task_queue_buffer_size (or the appropriate names used in
app_settings) where the queue is created, and app_settings.enqueue_timeout (or
configured timeout name) where enqueue/put with timeout is used; ensure you
update the module-level import to "from bindu.settings import app_settings" and
adjust any references in functions like the queue creation and the enqueue call
so no local config constants remain.
bindu/penguin/bindufy.py-498-499 (1)

498-499: ⚠️ Potential issue | 🟠 Major

Fail fast when voice is present but invalid instead of silently skipping.

Line 498 currently ignores mis-typed voice values (and empty dicts), which can hide configuration errors and make voice appear enabled but inactive.

Proposed fix
-    voice_config = validated_config.get("voice")
-    if voice_config and isinstance(voice_config, dict):
+    voice_config = validated_config.get("voice")
+    if voice_config is not None:
+        if not isinstance(voice_config, dict):
+            raise ValueError("Invalid voice configuration: 'voice' must be a dictionary")
         from bindu.extensions.voice import VoiceAgentExtension
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/penguin/bindufy.py` around lines 498 - 499, If a user provides a voice
configuration but it's not a non-empty dict, fail fast instead of silently
skipping: check the voice_config variable and if it's present but not an
instance of dict or is an empty dict, raise a clear error (e.g., ValueError)
describing the invalid/missing voice configuration; only proceed to import and
instantiate VoiceAgentExtension when voice_config is a non-empty dict. Use the
existing voice_config symbol and the VoiceAgentExtension import/usage in
bindufy.py to locate where to add the validation and the exception.
examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml-28-31 (1)

28-31: ⚠️ Potential issue | 🟠 Major

application/pdf MIME type is incorrect for file path input.

The implementation accepts text strings for both inputs: literal paths (e.g., /path/to/paper.pdf) and raw document text. The _read_content() function treats both as text and identifies file paths by string suffix (.endswith(".pdf")), not by MIME type. The skill.yaml should list text/plain for both input modes, as no binary PDF payload is ever processed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml` around
lines 28 - 31, The input_modes in skill.yaml incorrectly lists application/pdf
even though the code (see _read_content) treats inputs as text strings and
detects PDF paths by suffix; update skill.yaml so both accepted inputs are
declared as text/plain (remove application/pdf) to reflect that the skill
expects text strings for raw document text and literal file paths, ensuring the
manifest matches the _read_content behavior.
examples/multilingual-collab-agent/main.py-60-69 (1)

60-69: ⚠️ Potential issue | 🟠 Major

Don’t advertise memory when Mem0 is unavailable.

When MEM0_API_KEY is missing or Mem0Tools(...) fails, the tool list drops memory support, but the static instructions still tell the model it can remember previous conversations. That makes the example over-promise a capability it may not have. Build the memory section conditionally or add an explicit “memory disabled” instruction in the fallback path.

Also applies to: 115-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/multilingual-collab-agent/main.py` around lines 60 - 69, The example
currently always tells the model it can remember even when Mem0 is not
available; update the code that builds the static instructions so the
memory-related instructions are only added when mem0_api_key / _mem0_enabled is
true (i.e., after Mem0Tools(...) succeeds and tools includes Mem0Tools), or
alternatively append an explicit “memory disabled” instruction in the fallback
branch where _mem0_enabled is set to False and Mem0Tools initialization fails;
adjust the logic around mem0_api_key, Mem0Tools, _mem0_enabled and the place
that constructs the model instructions (also update the other occurrence noted
around lines 115-118) so the model prompt accurately reflects whether memory is
enabled.
bindu/utils/worker/messages.py-21-21 (1)

21-21: 🛠️ Refactor suggestion | 🟠 Major

Move the file-size limit into app_settings.

MAX_FILE_SIZE is a local config constant inside a bindu/**/*.py module. That makes the upload limit harder to tune per deployment and conflicts with the repo rule to keep configuration in bindu.settings.app_settings.

As per coding guidelines, "Use bindu.settings.app_settings for all configuration - never hardcode values such as URLs, ports, timeouts, API keys, or feature flags" and "NEVER create local config constants - use app_settings from bindu.settings."

Also applies to: 114-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/utils/worker/messages.py` at line 21, The MAX_FILE_SIZE constant in
bindu.utils.worker.messages must be removed and the code should read the value
from bindu.settings.app_settings instead; import app_settings from
bindu.settings at top, replace usages of MAX_FILE_SIZE (including the other
occurrences around the 114-119 block) with app_settings.MAX_FILE_SIZE, and add a
default to app_settings (e.g., DEFAULT_UPLOAD_MAX_FILE_SIZE) if it doesn't exist
so deployments can override it. Ensure any tests or callers referencing
MAX_FILE_SIZE are updated to use app_settings.MAX_FILE_SIZE and remove the local
constant declaration.
bindu/extensions/voice/service_factory.py-87-103 (1)

87-103: ⚠️ Potential issue | 🟠 Major

Restrict fallback to specific expected exceptions from the TTS provider setup.

The broad except Exception catches unexpected errors during service instantiation (e.g., parameter errors, bugs in pipecat) alongside recoverable config failures, causing them to be silently masked by fallback. The function explicitly raises ImportError (missing modules/attributes) and ValueError (missing configuration), so only these should trigger fallback. Additionally, when both attempts fail, chain from both primary_error and fallback_error to preserve diagnostic context instead of discarding the primary error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/service_factory.py` around lines 87 - 103, Limit the
broad exception handling in the TTS factory: only catch ImportError and
ValueError from _create_tts_service_for_provider to decide to try the
fallback_provider; do not catch all Exception types so parameter/bug errors
aren't masked. In the fallback branch, log using logger.warning as before,
attempt _create_tts_service_for_provider(fallback_provider, config), and if that
also fails, set fallback_error.__context__ = primary_error (or otherwise attach
primary_error to the fallback error) and raise a RuntimeError("TTS setup failed
for primary and fallback providers") from fallback_error so both the
primary_error and fallback_error are preserved in the exception chain.
frontend/src/lib/services/voice-client.ts-349-356 (1)

349-356: ⚠️ Potential issue | 🟠 Major

Flush the partial PCM buffer before sending commit_turn.

The worklet only posts audio when its 4096-sample buffer fills. On stop, you destroy the graph and immediately send commit_turn, so the last partial chunk never reaches the server and the final words of a turn can get clipped.

Also applies to: 399-402

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/services/voice-client.ts` around lines 349 - 356, The
worklet currently only posts audio when its full 4096-sample buffer fills, so
partial audio in flushBuffer() (method flushBuffer in voice-client worklet) must
be sent before the app sends commit_turn; update the shutdown/stop flow that
triggers commit_turn (see the stop/close logic around the commit_turn send and
the other flush call at the 399-402 region) to call flushBuffer() and wait for
the postMessage to complete if necessary whenever bufferIndex > 0 so the last
partial PCM chunk is delivered; ensure flushBuffer() handles bufferIndex==0 as a
no-op and that the caller resets bufferIndex only after the buffer has been
posted.
bindu/extensions/voice/redis_session_manager.py-262-272 (1)

262-272: ⚠️ Potential issue | 🟠 Major

Don’t collapse Redis outages into “session not found”.

get_session() returns None on RedisError, and the callers treat that exactly like a missing session. A transient Redis failure will therefore show up as 404/invalid-session instead of a backend failure, which is very hard to diagnose during active voice calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/redis_session_manager.py` around lines 262 - 272, In
get_session(), don't treat redis.RedisError the same as a missing key: keep the
existing logic that returns None only when data is None (after computing key via
_session_key and reading via _redis_client.get and deserializing with
_deserialize_session), but on redis.RedisError log the error with logger.error
and re-raise (or wrap and raise a specific session storage exception) instead of
returning None so callers can distinguish Redis outages from "session not
found"; update the except block to not swallow the exception and ensure callers
can catch the new/raised exception type.
bindu/server/endpoints/voice_endpoints.py-462-470 (1)

462-470: ⚠️ Potential issue | 🟠 Major

Push a disconnect into the filtered queue when handling stop.

This branch closes the real socket and returns, but it never forwards a disconnect message to inbound_queue. The transport reading from _FilteredWebSocket.receive() can stay blocked until session_timeout, so stopped sessions linger instead of tearing down immediately.

Suggested fix
             if frame_type == "stop":
                 control.stopped = True
                 await _send_json(
                     websocket, {"type": "state", "state": "ended"}, send_lock
                 )
-                try:
-                    await websocket.close()
-                finally:
-                    return
+                await inbound_queue.put({"type": "websocket.disconnect", "code": 1000})
+                try:
+                    await websocket.close()
+                except Exception:
+                    logger.exception("Failed to close voice websocket after stop")
+                return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 462 - 470, When
handling the "stop" frame in the branch that sets control.stopped and calls
_send_json, also enqueue a disconnect message into the inbound/filtered queue
used by _FilteredWebSocket.receive() so the transport unblocks immediately;
specifically, after setting control.stopped and sending the
{"type":"state","state":"ended"} message, push a disconnect (e.g.
{"type":"disconnect"} or whatever the consumer expects) into
inbound_queue/filtered_queue before awaiting websocket.close(), ensuring the
consumer reading from _FilteredWebSocket.receive() sees the disconnect and tears
down rather than waiting for session_timeout.
scripts/test_voice_providers.sh-58-60 (1)

58-60: ⚠️ Potential issue | 🟠 Major

Make failed smoke checks affect the process exit code.

fail() only logs, so the Deepgram and optional --bindu checks can print FAIL while the script still exits 0. That makes CI and reviewer smoke tests look green even when the providers are broken.

Suggested fix
+FAILED=0
+
 pass() { echo -e "${GREEN}PASS${NC} - $1"; }
-fail() { echo -e "${RED}FAIL${NC} - $1"; }
+fail() {
+  FAILED=1
+  echo -e "${RED}FAIL${NC} - $1"
+}
 warn() { echo -e "${YELLOW}WARN${NC} - $1"; }
 save_artifacts
 
 echo ""
 echo "Done."
+exit "$FAILED"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test_voice_providers.sh` around lines 58 - 60, The fail() helper
currently only prints a message so failing smoke checks don't affect the script
exit code; update fail() to mark the run as failed (e.g., set a global FAILED=1
inside fail()) while still printing the failure, and ensure the script exits
with a non-zero code at the end (check if FAILED is set and exit 1). Reference
the pass(), fail(), warn() helpers and any final exit/cleanup block to implement
the FAILED flag and final conditional exit.
frontend/src/lib/components/chat/ChatWindow.svelte-150-168 (1)

150-168: ⚠️ Potential issue | 🟠 Major

Allow attachment-only submissions again.

The new guards require non-empty text in both handleSubmit() and submit(), so users can no longer send just files. That regresses multimodal flows where the attachment is the entire prompt.

Suggested fix
 async function handleSubmit() {
-	if (!draft) {
-		return;
-	}
 	try {
-		const fileParts = (sources ? await Promise.all(sources) : []).filter(Boolean) as MessageFile[];
-		await submit(draft, { fileParts });
+		const message = draft.trim();
+		const fileParts = (sources ? await Promise.all(sources) : []).filter(Boolean) as MessageFile[];
+		if (!message && fileParts.length === 0) {
+			return;
+		}
+		await submit(message, { fileParts });
 	} catch (err) {
 		console.error("Error preparing chat submission:", err);
 		$error = err instanceof Error ? err.message : String(err);
 	}
 }
 
 async function submit(
 	message: string,
 	options: { fileParts?: MessageFile[] } = {}
 ) {
 	const fileParts = options.fileParts ?? [];
-	if (!message || loading || isReadOnly) return;
+	const trimmedMessage = message.trim();
+	if ((!trimmedMessage && fileParts.length === 0) || loading || isReadOnly) return;
 	if (requireAuthUser()) return;
 
 	const contextId = agentContextId ?? undefined;
 	let success = false;
 	try {
-		for await (const _update of sendAgentMessage(message, contextId, { fileParts })) {
+		for await (const _update of sendAgentMessage(trimmedMessage, contextId, { fileParts })) {
 			// Process updates if needed
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/components/chat/ChatWindow.svelte` around lines 150 - 168,
The guards in handleSubmit and submit prevent attachment-only sends; update
handleSubmit to allow submission when there is no draft but there are resolved
sources (i.e., proceed if sources produce fileParts) instead of returning
immediately when draft is falsy, and change submit's guard to only block when
both message is empty and fileParts is empty (preserve blocking when loading or
isReadOnly). Refer to handleSubmit, draft, sources, fileParts, submit, loading,
and isReadOnly when making the checks.
🟡 Minor comments (12)
bindu/extensions/did/did_agent_extension.py-243-261 (1)

243-261: ⚠️ Potential issue | 🟡 Minor

Consider using the full domain-qualified username for domain environments.

USERNAME is reliably set by Windows for the current process, but in domain environments it returns only the username portion (not DOMAIN\username). For icacls, this may resolve to an ambiguous or incorrect principal if the domain context is unclear. The current code is safe—it will raise an error if the ACL command fails—but using Windows APIs like GetUserNameEx(NameSamCompatible) or the process SID directly via win32security would be more robust for cross-environment reliability. As per coding guidelines, consider whether environment variables should be validated more strictly before use in security operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/did/did_agent_extension.py` around lines 243 - 261, The code
currently reads USERNAME from the environment and passes it to icacls (see
variable username and the cmd list used with subprocess.run), which can be
ambiguous in domain environments; replace the env lookup with a robust retrieval
of the full domain-qualified account (e.g., call the Windows API
GetUserNameEx(NameSamCompatible) or obtain the process token/owner SID via
win32security and translate it to a full account name) and use that result when
building the icacls arguments, falling back to raising the existing RuntimeError
if the API call fails; ensure the variable name stays username and the rest of
the icacls command construction (cmd list and subprocess.run usage) reuses this
value so authorization targets are unambiguous.
examples/voice-agent/.env.example-7-10 (1)

7-10: ⚠️ Potential issue | 🟡 Minor

Reorder keys to satisfy current dotenv-linter ordering checks.

Line 7–10, Line 18–25, and Line 34 are flagged as out-of-order. Sorting keys within each section will remove lint warnings and keep the template CI-friendly.

Proposed reorder
 # Optional fallback TTS (recommended for resiliency)
-VOICE__TTS_FALLBACK_PROVIDER=azure
 VOICE__AZURE_TTS_API_KEY=your-azure-speech-key
 VOICE__AZURE_TTS_REGION=eastus
 VOICE__AZURE_TTS_VOICE=en-US-SaraNeural
+VOICE__TTS_FALLBACK_PROVIDER=azure

 # Optional tuning
-VOICE__ENABLED=true
-VOICE__STT_MODEL=nova-3
-VOICE__STT_LANGUAGE=en
-VOICE__TTS_MODEL=piper-local
-VOICE__SAMPLE_RATE=16000
-VOICE__AUDIO_CHANNELS=1
-VOICE__AUDIO_ENCODING=linear16
-VOICE__VAD_ENABLED=true
 VOICE__ALLOW_INTERRUPTIONS=true
+VOICE__AUDIO_CHANNELS=1
+VOICE__AUDIO_ENCODING=linear16
+VOICE__ENABLED=true
+VOICE__SAMPLE_RATE=16000
 VOICE__SESSION_TIMEOUT=300
+VOICE__STT_LANGUAGE=en
+VOICE__STT_MODEL=nova-3
+VOICE__TTS_MODEL=piper-local
+VOICE__VAD_ENABLED=true

 # Optional LLM backend for smarter responses
 OPENROUTER_API_KEY=your-openrouter-api-key
-OPENROUTER_MODEL=openai/gpt-oss-120b:free
 OPENROUTER_MEMORY_TURNS=4
+OPENROUTER_MODEL=openai/gpt-oss-120b:free
 VOICE_MAX_SENTENCES=2

Also applies to: 18-25, 34-34

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/.env.example` around lines 7 - 10, The dotenv keys in
the voice-agent template are out of alphabetical order (e.g.,
VOICE__TTS_FALLBACK_PROVIDER, VOICE__AZURE_TTS_API_KEY, VOICE__AZURE_TTS_REGION,
VOICE__AZURE_TTS_VOICE) and are triggering dotenv-linter; fix by sorting the
variables alphabetically within each logical section (the Azure TTS block and
the other flagged blocks around lines 18–25 and the single key at line 34) so
that each group follows lexicographic order, preserving values and comments but
reordering the keys to satisfy the linter.
examples/cerina_bindu/cbt/README.md-106-108 (1)

106-108: ⚠️ Potential issue | 🟡 Minor

Fix relative paths in “Related Examples” links.

Line 106–108 currently use ../..., which resolves under examples/cerina_bindu/. These links should point to sibling directories under examples/, so use ../../....

Proposed fix
-- [`examples/collaborative-agents/`](../collaborative-agents/) — multi-agent A2A communication
-- [`examples/agent_swarm/`](../agent_swarm/) — agent swarm patterns
-- [`examples/beginner/`](../beginner/) — simple single-agent examples
+- [`examples/collaborative-agents/`](../../collaborative-agents/) — multi-agent A2A communication
+- [`examples/agent_swarm/`](../../agent_swarm/) — agent swarm patterns
+- [`examples/beginner/`](../../beginner/) — simple single-agent examples
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/cerina_bindu/cbt/README.md` around lines 106 - 108, Update the three
"Related Examples" markdown links that currently use ../ to point to the sibling
directories under examples by changing their paths to use ../../; specifically
update the links for `examples/collaborative-agents/` (`multi-agent A2A
communication`), `examples/agent_swarm/` (`agent swarm patterns`), and
`examples/beginner/` (`simple single-agent examples`) so each `../` becomes
`../../` in the README.
examples/multilingual-collab-agent/.env.example-1-3 (1)

1-3: ⚠️ Potential issue | 🟡 Minor

Reorder keys to satisfy dotenv-linter.
Current key order triggers UnorderedKey warnings; sorting keeps the example lint-clean.

Suggested diff
-OPENROUTER_API_KEY=your_openrouter_api_key_here
 MEM0_API_KEY=your_mem0_api_key_here
 MODEL_NAME=openai/gpt-4o-mini
+OPENROUTER_API_KEY=your_openrouter_api_key_here
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/multilingual-collab-agent/.env.example` around lines 1 - 3, Reorder
the keys in the .env example so they are alphabetically sorted to satisfy
dotenv-linter UnorderedKey warnings: change the sequence in
examples/multilingual-collab-agent/.env.example from OPENROUTER_API_KEY,
MEM0_API_KEY, MODEL_NAME to MEM0_API_KEY, MODEL_NAME, OPENROUTER_API_KEY so the
keys are in lexical order.
examples/pdf_research_agent/README.md-227-241 (1)

227-241: ⚠️ Potential issue | 🟡 Minor

Test example missing required message fields.

The simplified curl example is missing required fields (kind, messageId, contextId, taskId) that are present in the more complete Postman example (lines 62-84). Users following this simplified example may encounter validation errors.

Consider using a complete example or noting that the simplified form may not work with strict validation:

📝 Suggested complete example
 curl -X POST http://localhost:3773 \
   -H "Content-Type: application/json" \
   -d '{
     "jsonrpc": "2.0",
     "method": "message/send",
     "params": {
       "message": {
         "role": "user",
-        "parts": [{"kind": "text", "text": "Sample document text for testing..."}]
-      }
+        "kind": "message",
+        "messageId": "00000000-0000-0000-0000-000000000001",
+        "contextId": "00000000-0000-0000-0000-000000000002",
+        "taskId": "00000000-0000-0000-0000-000000000003",
+        "parts": [{"kind": "text", "text": "Sample document text for testing..."}]
+      },
+      "configuration": {"acceptedOutputModes": ["application/json"]}
     },
-    "id": "test-1"
+    "id": "00000000-0000-0000-0000-000000000004"
   }'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/README.md` around lines 227 - 241, The curl
example invoking the "message/send" JSON-RPC method omits required message
fields used by validation (e.g., kind, messageId, contextId, taskId); update the
example payload in README.md to include those fields inside the "message" object
(matching the fuller Postman example's structure) or add a clear note that the
simplified payload may fail strict validation, referencing the "message/send"
method and the message parts format so readers can copy a working payload.
examples/medical_agent/agent.py-26-28 (1)

26-28: ⚠️ Potential issue | 🟡 Minor

Guard against empty/malformed message payloads before invoking the model.

If the last message lacks content, this can throw at runtime; if it is empty, you still incur an unnecessary model call.

Proposed fix
 def medical_handler(messages: list[dict[str, str]]) -> str:
@@
-    user_query = messages[-1]["content"] if messages else ""
-    response = medical_agent.run(user_query)
+    if not messages or "content" not in messages[-1]:
+        return "Please share your medical question."
+    user_query = messages[-1]["content"].strip()
+    if not user_query:
+        return "Please share your medical question."
+    response = medical_agent.run(user_query)
     return response.content
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/medical_agent/agent.py` around lines 26 - 28, The code extracts
messages[-1]["content"] into user_query and unconditionally calls
medical_agent.run, which can throw if the last message is missing "content" or
waste a model call if content is empty. Before calling medical_agent.run,
validate the messages list and the last message: check that messages is
non-empty, that messages[-1] is a dict-like object with a non-empty "content"
string, and return an appropriate error/empty response (or skip the model call)
if validation fails. Update the logic around user_query, messages, and the call
to medical_agent.run to perform these checks and only invoke
medical_agent.run(user_query) when user_query is a non-empty string.
examples/multilingual-collab-agent/skills/research/skill.yaml-18-24 (1)

18-24: ⚠️ Potential issue | 🟡 Minor

Clarify or drop the advertised JSON contract.

Lines 18-24 say this skill accepts and returns application/json, but Lines 88-97 only describe plain-text input/output. That leaves callers without a concrete JSON shape to implement against. Either remove JSON here or document the request/response schema explicitly.

Also applies to: 88-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/multilingual-collab-agent/skills/research/skill.yaml` around lines
18 - 24, The skill.yaml currently advertises application/json in input_modes and
output_modes but the implementation/docs only describe plain-text I/O; either
remove the application/json entries from input_modes/output_modes to avoid
promising a JSON contract, or add an explicit JSON request/response schema
(e.g., top-level fields and types for the incoming request and the outgoing
response) next to the plain-text description so callers know the exact JSON
shape; update the input_modes/output_modes section and/or add a clear
request_schema/response_schema block in skill.yaml so the JSON contract is
unambiguous.
examples/multilingual-collab-agent/skills/collaborate/skill.yaml-20-26 (1)

20-26: ⚠️ Potential issue | 🟡 Minor

Clarify or drop the advertised JSON contract.

This manifest exposes application/json in both directions, but Lines 100-110 only document free-form text requests and drafted text responses. That ambiguity will leak into callers and tooling. Either remove JSON from the modes or define the JSON payloads explicitly.

Also applies to: 100-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/multilingual-collab-agent/skills/collaborate/skill.yaml` around
lines 20 - 26, The manifest lists application/json in input_modes and
output_modes but the skill only documents free-form text requests and drafted
text responses, creating an ambiguous contract; either remove application/json
from the input_modes/output_modes entries or explicitly define the JSON
request/response schema used by this skill (add concrete field names/types and
examples) and update the free-form text documentation to reference that schema
so callers/tools know which payload to send/expect; look for the
input_modes/output_modes keys and the free-form text request/response
documentation block and make the change consistently.
bindu/extensions/voice/pipeline_builder.py-64-71 (1)

64-71: ⚠️ Potential issue | 🟡 Minor

Use consistent error handling for VAD imports.

The VAD block (lines 65-66) lacks the try/except wrapper used for STT/TTS imports in service_factory.py. If a user enables app_settings.voice.vad_enabled but the silero component is unavailable, they receive a raw ImportError instead of an actionable message. Wrap the imports like the STT/TTS factory paths:

Example pattern
if app_settings.voice.vad_enabled:
    try:
        from pipecat.audio.vad.silero import SileroVADAnalyzer
        from pipecat.processors.audio.vad_processor import VADProcessor
    except (ImportError, AttributeError) as e:
        raise ImportError(
            "Voice VAD requires pipecat[silero]. "
            "Install with: pip install 'bindu[voice]'"
        ) from e
    
    vad_analyzer = SileroVADAnalyzer(
        sample_rate=app_settings.voice.sample_rate,
    )
    vad_component = VADProcessor(vad_analyzer=vad_analyzer)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/pipeline_builder.py` around lines 64 - 71, The VAD
import block triggered when app_settings.voice.vad_enabled is missing a
try/except like the STT/TTS factory; wrap the imports of SileroVADAnalyzer and
VADProcessor in a try/except (catch ImportError and AttributeError), and on
failure raise a clear ImportError with actionable text (e.g., "Voice VAD
requires pipecat[silero]. Install with: pip install 'bindu[voice]'") re-raising
from the caught exception; keep the subsequent creation of vad_analyzer =
SileroVADAnalyzer(...) and vad_component = VADProcessor(...) unchanged.
docs/VOICE_AGENT_PLAN.md-40-40 (1)

40-40: ⚠️ Potential issue | 🟡 Minor

Minor formatting inconsistency in ASCII diagram.

There's a trailing hyphen (-) that breaks the box alignment on line 40.

📝 Fix
-└───────────────────────────────────────────┼────────────────────────-┘
+└───────────────────────────────────────────┼─────────────────────────┘
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/VOICE_AGENT_PLAN.md` at line 40, The ASCII diagram line contains a
trailing hyphen character (the segment
"└───────────────────────────────────────────┼────────────────────────-┘") that
breaks box alignment; edit the diagram in VOICE_AGENT_PLAN.md to remove the
stray '-' so the line ends with the proper box corner ("┘") and matches the
other border characters, ensuring the box vertical and corner characters align
with the rest of the diagram.
examples/pdf_research_agent/pdf_research_agent.py-83-86 (1)

83-86: ⚠️ Potential issue | 🟡 Minor

Use raise ... from None for cleaner exception replacement.

When raising a new exception to replace the original (providing a clearer user-facing message), use from None to suppress the chained traceback, or from e if you want to preserve it.

📝 Fix
         except ImportError:
-            raise DocumentReadError(
+            raise DocumentReadError(
                 f"pypdf not installed and cannot read '{resolved_path}'. Run: uv add pypdf"
-            )
+            ) from None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 83 - 86, The
ImportError handler in the except ImportError block that raises
DocumentReadError for "'{resolved_path}'" should suppress exception chaining;
change the except to capture the ImportError (e.g., except ImportError as e:)
and re-raise DocumentReadError(...) using "from None" (or "from e" if you want
the original traceback), i.e., update the raise of DocumentReadError in the pdf
reading code to use "from None" to avoid the chained traceback while keeping the
same message referencing resolved_path.
bindu/server/endpoints/voice_endpoints.py-569-581 (1)

569-581: ⚠️ Potential issue | 🟡 Minor

Reject malformed context_id values instead of stringifying them.

str(body["context_id"]) turns null, objects, and arrays into synthetic IDs like "None" or "{}". That hides bad client input and can bind the session to the wrong context.

Suggested fix
         if isinstance(body, dict) and "context_id" in body:
-            context_id = str(body["context_id"])
+            raw_context_id = body["context_id"]
+            if not isinstance(raw_context_id, str) or not raw_context_id.strip():
+                raise HTTPException(
+                    status_code=400,
+                    detail="context_id must be a non-empty string",
+                )
+            context_id = raw_context_id.strip()

As per coding guidelines, "Validate all external input and use type hints for input validation in Python files".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 569 - 581, The
current parsing block assigns context_id = str(body["context_id"]) which
silently accepts nulls/objects/arrays; instead validate body["context_id"] is a
proper string (and optionally a UUID) before accepting it: after loading body,
if "context_id" in body check isinstance(body["context_id"], str) and that it is
non-empty (or parse with uuid.UUID to enforce UUID format) and if validation
fails raise HTTPException(status_code=400, detail="Invalid context_id"); only
then set context_id = body["context_id"]. Reference the variables/symbols
context_id, raw_body, body and the use of request.body() in the existing block
to locate where to apply this change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e3703db-45ea-4604-b894-231ed393bbf9

📥 Commits

Reviewing files that changed from the base of the PR and between 301624d and e703881.

⛔ Files ignored due to path filters (4)
  • bindu/grpc/generated/agent_handler_pb2.py is excluded by !**/generated/**
  • bindu/grpc/generated/agent_handler_pb2.pyi is excluded by !**/generated/**
  • bindu/grpc/generated/agent_handler_pb2_grpc.py is excluded by !**/generated/**
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (131)
  • .github/workflows/ci.yml
  • .gitignore
  • .pre-commit-config.yaml
  • README.bn.md
  • README.de.md
  • README.es.md
  • README.fr.md
  • README.hi.md
  • README.md
  • README.nl.md
  • README.ta.md
  • README.zh.md
  • SPLIT_PLAN.md
  • bindu/common/protocol/types.py
  • bindu/extensions/__init__.py
  • bindu/extensions/did/did_agent_extension.py
  • bindu/extensions/voice/__init__.py
  • bindu/extensions/voice/agent_bridge.py
  • bindu/extensions/voice/audio_config.py
  • bindu/extensions/voice/pipeline_builder.py
  • bindu/extensions/voice/redis_session_manager.py
  • bindu/extensions/voice/service_factory.py
  • bindu/extensions/voice/session_factory.py
  • bindu/extensions/voice/session_manager.py
  • bindu/extensions/voice/voice_agent_extension.py
  • bindu/penguin/bindufy.py
  • bindu/penguin/config_validator.py
  • bindu/server/applications.py
  • bindu/server/endpoints/utils.py
  • bindu/server/endpoints/voice_endpoints.py
  • bindu/server/handlers/message_handlers.py
  • bindu/server/metrics.py
  • bindu/server/scheduler/memory_scheduler.py
  • bindu/server/workers/base.py
  • bindu/settings.py
  • bindu/utils/__init__.py
  • bindu/utils/capabilities.py
  • bindu/utils/config/settings.py
  • bindu/utils/logging.py
  • bindu/utils/notifications.py
  • bindu/utils/retry.py
  • bindu/utils/task_telemetry.py
  • bindu/utils/worker/messages.py
  • bindu/utils/worker/parts.py
  • docs/VOICE.md
  • docs/VOICE_AGENT_PLAN.md
  • examples/README.md
  • examples/ag2_research_team/skills/research-team-skill/skill.yaml
  • examples/beginner/README.md
  • examples/beginner/motivational_agent.py
  • examples/cerina_bindu/cbt/README.md
  • examples/document-analyzer/document_analyzer.py
  • examples/medical_agent/.env.example
  • examples/medical_agent/agent.py
  • examples/multilingual-collab-agent/.env.example
  • examples/multilingual-collab-agent/README.md
  • examples/multilingual-collab-agent/agent_config.json
  • examples/multilingual-collab-agent/main.py
  • examples/multilingual-collab-agent/skills/collaborate/skill.yaml
  • examples/multilingual-collab-agent/skills/research/skill.yaml
  • examples/multilingual-collab-agent/skills/translate/skill.yaml
  • examples/pdf_research_agent/.env.example
  • examples/pdf_research_agent/README.md
  • examples/pdf_research_agent/pdf_research_agent.py
  • examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml
  • examples/typescript-langchain-agent/quiz-agent.ts
  • examples/typescript-langchain-quiz-agent/.env.example
  • examples/typescript-langchain-quiz-agent/quiz-agent.ts
  • examples/voice-agent/.env.example
  • examples/voice-agent/README.md
  • examples/voice-agent/main.py
  • examples/voice-agent/skills/voice-brief-response-skill/skill.yaml
  • frontend/.env.example
  • frontend/.gitignore
  • frontend/src/lib/buildPrompt.ts
  • frontend/src/lib/components/ShareConversationModal.svelte
  • frontend/src/lib/components/chat/ChatInput.svelte
  • frontend/src/lib/components/chat/ChatMessage.svelte
  • frontend/src/lib/components/chat/ChatWindow.svelte
  • frontend/src/lib/components/chat/ContextList.svelte
  • frontend/src/lib/components/voice/LiveTranscript.svelte
  • frontend/src/lib/components/voice/VoiceCallButton.svelte
  • frontend/src/lib/components/voice/VoiceCallPanel.svelte
  • frontend/src/lib/constants/mime.ts
  • frontend/src/lib/jobs/refresh-conversation-stats.ts
  • frontend/src/lib/migrations/lock.ts
  • frontend/src/lib/migrations/routines/02-update-assistants-models.ts
  • frontend/src/lib/migrations/routines/10-update-reports-assistantid.ts
  • frontend/src/lib/server/config.ts
  • frontend/src/lib/server/database.ts
  • frontend/src/lib/server/files/downloadFile.ts
  • frontend/src/lib/server/files/uploadFile.ts
  • frontend/src/lib/server/models.ts
  • frontend/src/lib/services/voice-client.ts
  • frontend/src/lib/stores/chat.ts
  • frontend/src/lib/stores/voice.ts
  • frontend/src/lib/types/ConvSidebar.ts
  • frontend/src/lib/types/Model.ts
  • frontend/src/lib/types/Session.ts
  • frontend/src/lib/utils/agentMessageHandler.ts
  • frontend/src/lib/utils/tree/addChildren.spec.ts
  • frontend/src/lib/utils/tree/addSibling.spec.ts
  • frontend/src/lib/utils/tree/buildSubtree.spec.ts
  • frontend/src/lib/utils/tree/convertLegacyConversation.spec.ts
  • frontend/src/routes/+layout.svelte
  • frontend/src/routes/+page.svelte
  • frontend/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts
  • frontend/src/routes/api/v2/export/+server.ts
  • frontend/src/routes/conversation/[id]/+page.svelte
  • frontend/src/routes/conversation/[id]/+server.ts
  • frontend/src/routes/settings/(nav)/authentication/+page.svelte
  • frontend/src/routes/settings/(nav)/negotiation/+page.svelte
  • pyproject.toml
  • pytest.ini
  • scripts/test_voice_providers.sh
  • tests/conftest_stubs.py
  • tests/integration/grpc/test_grpc_e2e.py
  • tests/unit/extensions/__init__.py
  • tests/unit/extensions/voice/__init__.py
  • tests/unit/extensions/voice/test_agent_bridge.py
  • tests/unit/extensions/voice/test_service_factory.py
  • tests/unit/extensions/voice/test_session_manager.py
  • tests/unit/extensions/voice/test_voice_endpoints.py
  • tests/unit/extensions/voice/test_voice_extension.py
  • tests/unit/extensions/voice/test_voice_websocket_integration.py
  • tests/unit/penguin/test_bindufy.py
  • tests/unit/penguin/test_config_validator.py
  • tests/unit/server/scheduler/test_memory_scheduler.py
  • tests/unit/test_minimax_example.py
  • tests/unit/utils/test_notifications.py
  • tests/unit/utils/worker/test_messages.py

Comment on lines +213 to +218
if platform.system() == "Windows":
# Windows does not enforce POSIX permissions — write directly
self.private_key_path.write_bytes(private_pem)
self.public_key_path.write_bytes(public_pem)
self._harden_windows_key_file_acl(self.private_key_path)
self._harden_windows_key_file_acl(self.public_key_path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -t f 'did_agent_extension.py' bindu/extensions/

Repository: GetBindu/Bindu

Length of output: 101


🏁 Script executed:

wc -l bindu/extensions/did/did_agent_extension.py

Repository: GetBindu/Bindu

Length of output: 105


🏁 Script executed:

sed -n '200,280p' bindu/extensions/did/did_agent_extension.py

Repository: GetBindu/Bindu

Length of output: 2967


Avoid creating the private key with inherited ACLs on Windows.

On Windows, write_bytes() creates the file with inherited parent directory ACLs before _harden_windows_key_file_acl() executes. This creates a window where the private key is readable under inherited permissions. Additionally, if icacls fails and raises RuntimeError (line 260), the unprotected key file remains on disk.

The POSIX approach (lines 219–230) demonstrates the safer pattern: use os.open() with 0o600 mode to create the file with restricted permissions atomically from the start. Apply an equivalent approach on Windows—either create the file with restricted security descriptors using Windows APIs, or write to a pre-hardened temporary directory and atomically rename it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/did/did_agent_extension.py` around lines 213 - 218, The
Windows branch currently uses
self.private_key_path.write_bytes()/self.public_key_path.write_bytes() then
calls _harden_windows_key_file_acl(), which leaves a window where inherited ACLs
expose the key and may leave an unprotected file if hardening fails; change this
to create the files already protected (or write into a pre-hardened temp file
and atomically rename) instead of writing then hardening: replace the
write_bytes + _harden_windows_key_file_acl sequence by either (a) using Windows
API CreateFile with an explicit SECURITY_DESCRIPTOR/SECURITY_ATTRIBUTES to set
restrictive ACLs at creation time for private_key_path (and similarly for public
if needed), or (b) write to a temp file inside a directory whose ACLs are
already hardened and then atomically move/rename the temp file to
self.private_key_path/self.public_key_path; ensure _harden_windows_key_file_acl
remains as a fallback but that any failure cleans up the unprotected temp file
and does not leave an exposed key (referencing private_key_path,
public_key_path, write_bytes, and _harden_windows_key_file_acl to locate the
change).

Comment thread bindu/utils/notifications.py Outdated
@chandan-1427
Copy link
Copy Markdown
Contributor

Hey @Co-vengers
All three PRs are currently throwing merge conflicts. Your code isn't actually conflicting with itself, the issue is just how the PR base branches are looking in GitHub.
Right now, #450, #451, and #452 are all aimed directly at main. Because they build on top of each other, GitHub gets confused seeing overlapping histories targeting main at the same time.
To fix the stack, we need to aim them like a ladder:

For PR #450 (Core):
Keep the base branch as main.
You will need to resolve the merge conflicts locally (it looks like main got some updates to did_agent_extension.py and notifications.py since you branched off). Rebase or merge main into your feat(voice-agent)/code-fixes branch and push.

For PR #451 (Examples):
At the top of the GitHub PR page, click the "Edit" button next to the title.
Change the base dropdown from main to feat(voice-agent)/code-fixes (the branch from PR #450).
Once you do this, the diff will instantly shrink to only show your example code, and the conflicts should disappear!

For PR #452 (Docs):
Click "Edit" on the PR page.
Change the base dropdown from main to feat(voice-agent)/example-agent (the branch from PR #451).
Once we get #450 reviewed and merged into main, GitHub will automatically retarget #451 to point to main next.

Let me know once you get the base branches updated and the conflicts on #450 resolved.
I know its been too long with review on this feature but we have to make sure this won't break or not work as expected in prod that's why we have to iterate on this again and again.
Thanks for understanding looking forward to the update!

@Co-vengers
Copy link
Copy Markdown
Contributor Author

@chandan-1427 I understand. I'll fix the branches as directed.

@Co-vengers
Copy link
Copy Markdown
Contributor Author

Co-vengers commented Apr 17, 2026

Hi @chandan-1427 I resolved the merge conflicts in PR #450 (Core) but when I am trying to change base branch from main to feat(voice-agent)/code-fixes in PR #451 I can't find the branch feat(voice-agent)/code-fixes in the dropdown menu.
image

Do i need to rebase example branch on to core branch instead of main.

@Co-vengers Co-vengers force-pushed the feat(voice-agent)/documentation branch from e703881 to dc8a54d Compare April 18, 2026 14:45
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
frontend/src/lib/server/database.ts (1)

1-10: ⚠️ Potential issue | 🟡 Minor

Scope mismatch with PR objectives.

The PR is described as documentation-only ("no runtime code changes; no dependency changes; no API changes"), but this file contains substantive runtime changes to the in-memory DB mock (sort option, insertOne id preservation, stream event registry, generic withTransaction, etc.). Either the PR description should be updated to reflect these runtime changes, or these changes belong in the upstream core/stacked PR (#450) rather than the docs PR (#452).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/server/database.ts` around lines 1 - 10, PR is marked
documentation-only but frontend/src/lib/server/database.ts contains runtime
changes (sort option behavior, insertOne id preservation, stream event registry,
generic withTransaction) causing a scope mismatch; either revert those runtime
edits in this file or update the PR metadata and target the upstream
core/stacked PR (`#450`). To fix: if this must stay doc-only, remove or revert
modifications to the in-memory DB functions (restore original behavior for
insertOne, eliminate added sort option handling, remove stream event registry
logic, and revert the generic withTransaction change so no runtime semantics
change in database.ts); otherwise update the PR description and retarget branch
so these runtime changes are included in the appropriate core/stacked PR (`#450`).
Ensure references to the modified symbols (insertOne, withTransaction, any
stream registry code, and sort handling) are corrected accordingly.
frontend/src/lib/server/config.ts (1)

135-157: ⚠️ Potential issue | 🟠 Major

Don’t drop DB-backed public config keys.

keysFromDB is merged into config, but the final filter only keeps keys present in publicEnv plus PUBLIC_AGENT_BASE_URL. Any other PUBLIC_* value managed only through the config manager will be visible server-side but missing from the client config.

Possible fix
-		const publicEnvKeys = Object.keys(publicEnv);
-
 		return Object.fromEntries(
 			Object.entries(configWithDefaults).filter(
-				([key]) => publicEnvKeys.includes(key) || key === "PUBLIC_AGENT_BASE_URL"
+				([key]) => key.startsWith("PUBLIC_")
 			)
 		) as Record<PublicConfigKey, string>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/server/config.ts` around lines 135 - 157, The final client
config currently filters configWithDefaults by publicEnv (plus
PUBLIC_AGENT_BASE_URL), which drops DB-only PUBLIC_* keys merged earlier via
keysFromDB when ConfigManagerEnabled; update the filter logic in the function
that builds the return value to also include any keys from this.keysFromDB that
startWith("PUBLIC_") so DB-backed public keys are preserved — e.g., compute a
set from Object.keys(publicEnv) extended with
Object.keys(this.keysFromDB).filter(k => k.startsWith("PUBLIC_")) and use that
set when filtering configWithDefaults (symbols to update: keysFromDB,
ConfigManagerEnabled, configWithDefaults, publicEnv, PUBLIC_AGENT_BASE_URL).
bindu/common/protocol/types.py (1)

150-168: ⚠️ Potential issue | 🟠 Major

Remove bytes requirement from URI file parts.

FileWithUri inherits from FileWithBytes, forcing URI variants to require the bytes field. This contradicts the schema documentation stating content can be "either directly as bytes or as a URI". URI-only file payloads fail validation due to the inherited required bytes field.

Recommended fix
 `@pydantic.with_config`(ConfigDict(alias_generator=to_camel))
-class FileWithUri(FileWithBytes):
+class FileWithUri(TypedDict):
     """File representation with URI reference."""
 
     uri: Required[str]
     """The URI of the file."""
+
+    mimeType: NotRequired[str]
+    """The MIME type of the file."""
+
+    name: NotRequired[str]
+    """The name of the file."""
+
+    embeddings: NotRequired[list[float]]
+    """The embeddings of File. <NotPartOfA2A>"""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/common/protocol/types.py` around lines 150 - 168, The FileWithUri class
should not inherit the required bytes field from FileWithBytes; change
FileWithUri so it defines its own fields (keep
`@pydantic.with_config`(ConfigDict(alias_generator=to_camel))) with uri:
Required[str] and make any bytes field optional (e.g., bytes: Optional[bytes] =
None) or omit it entirely, so URI-only variants validate; update references to
FileWithUri and FileWithBytes (and FilePart which uses FileWithBytes |
FileWithUri) accordingly.
frontend/src/routes/conversation/[id]/+page.svelte (1)

247-258: ⚠️ Potential issue | 🟠 Major

Forward attachments in the direct-agent path.

base64Files is built at lines 139–148, and the non-agent backend path forwards files at line 272 with files: isRetry ? userMessage?.files : base64Files. However, the direct-agent path (lines 247–258) passes only prompt and task options, silently dropping attachments for Bindu model conversations.

The sendAgentMessage function in frontend/src/lib/utils/agentMessageHandler.ts accepts a fileParts option (lines 202–206) that matches the structure of base64Files. Add it to the options object:

Fix
 				messageUpdatesIterator = sendAgentMessage(
 					prompt ?? '',
 					page.params.id!,
 					{
 						abortSignal: messageUpdatesAbortController.signal,
 						currentTaskId: currentTaskId ?? undefined,
 						taskState: currentTaskState ?? undefined,
 						replyToTaskId: replyToTaskId ?? undefined,
+						fileParts: isRetry ? userMessage?.files : base64Files,
 					}
 				);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/conversation/`[id]/+page.svelte around lines 247 - 258,
The direct-agent path is dropping attachments: when useDirectAgentAPI is true
the sendAgentMessage call (in +page.svelte, where messageUpdatesIterator is set)
currently only passes prompt and task options and omits base64Files; update the
options object passed to sendAgentMessage to include fileParts: isRetry ?
userMessage?.files : base64Files (matching the non-agent path) so attachments
are forwarded to the agent; locate sendAgentMessage usage around
messageUpdatesIterator and add the fileParts property to the options.
frontend/src/lib/components/chat/ChatInput.svelte (1)

162-169: ⚠️ Potential issue | 🟠 Major

Allow file-only submissions.

The new file-part flow still requires non-empty text, so selecting a file and pressing Enter does nothing unless the user also types a message. Include files.length > 0 in the submit guard.

Proposed fix
-			value.trim() !== ""
+			(value.trim() !== "" || files.length > 0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/components/chat/ChatInput.svelte` around lines 162 - 169,
The Enter-key submit guard in handleKeydown currently blocks submission when
value.trim() is empty even if files are attached; update the condition in
handleKeydown to allow submit when there are files by replacing the value check
with (value.trim() !== "" || files.length > 0), keeping the other guards
(event.key === "Enter", !event.shiftKey, !isCompositionOn, !isVirtualKeyboard())
unchanged so Enter will submit when at least one file is present.
♻️ Duplicate comments (1)
bindu/extensions/did/did_agent_extension.py (1)

213-229: ⚠️ Potential issue | 🟠 Major

TOCTOU window on Windows is only partially mitigated — same concern as prior review.

The rollback improves the failure path, but the root issue from the previous review still stands: self.private_key_path.write_bytes(private_pem) materializes the private key on disk under the parent directory's inherited ACLs before _harden_windows_key_file_acl runs. Anyone with inherited read on the key dir has a window to read the key during that interval, regardless of later cleanup. unlink(missing_ok=True) after the fact also does not guarantee data destruction (NTFS may retain the content until the clusters are reused, and USN/Journal/VSS copies are unaffected).

For true confidentiality, create the file already restricted, e.g. one of:

  • Write to a pre-hardened temp path inside an ACL-locked subdirectory, then os.replace() to the final name.
  • Use win32security / CreateFile with an explicit SECURITY_ATTRIBUTES owned-by-current-user-only DACL at creation time.
  • As a weaker fallback: harden the parent directory's ACL first, then write.

Also note the rollback's inner except OSError only logs — if unlink fails, the outer raise still propagates, leaving an unprotected key on disk. Consider escalating that log to logger.error and including the underlying exception.

🟠 Major comments (21)
examples/pdf_research_agent/pdf_research_agent.py-38-70 (1)

38-70: ⚠️ Potential issue | 🟠 Major

ALLOWED_BASE_DIR restriction likely breaks the documented usage.

ALLOWED_BASE_DIR is hard-coded to the directory containing pdf_research_agent.py, and _read_content rejects any PDF whose resolved path is not under it. The module docstring and the example print ({"role": "user", "content": "/path/to/paper.pdf"}) still advertise arbitrary paths, so any realistic invocation (e.g. ~/Documents/paper.pdf) will now fail with "PDF path is outside the allowed document directory".

If the intent is path-traversal hardening, consider either (a) making the allowed directory configurable via env var (e.g. PDF_RESEARCH_DOCS_DIR) with a sensible default, or (b) using Path.is_relative_to / resolved_path.relative_to(ALLOWED_BASE_DIR) instead of os.path.commonpath (which can produce surprising results across drives/roots on Windows and with symlinks). At minimum, update the docstring/example to reflect the new constraint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 38 - 70,
ALLOWED_BASE_DIR being hard-coded breaks passing arbitrary user PDF paths; make
the allowed directory configurable and perform a robust containment check in
_read_content: read an env var PDF_RESEARCH_DOCS_DIR (fallback to
Path(__file__).parent.resolve()) to set ALLOWED_BASE_DIR, resolve both
ALLOWED_BASE_DIR and resolved_path with Path.resolve(), then use
Path.is_relative_to(resolved_path, ALLOWED_BASE_DIR) (or try
resolved_path.relative_to(ALLOWED_BASE_DIR) in a try/except for older Python) to
validate containment and still raise DocumentReadError on failure; also update
any docstring/example to mention the PDF_RESEARCH_DOCS_DIR override.
examples/document-analyzer/document_analyzer.py-76-83 (1)

76-83: ⚠️ Potential issue | 🟠 Major

Silent DOCX failures now yield empty documents instead of surfacing as errors.

The new try/except returns "" on failure, which _collect_prompt_and_documents then appends to extracted_docs as a valid (empty) document. As a result, if not extracted_docs: is never true for a broken DOCX, the errors branch is skipped, and the agent is asked to analyze an empty string — the user gets a hallucinated/empty analysis instead of the "Failed to process documents" message.

Consider re-raising (or raising a dedicated error) so the caller in _collect_prompt_and_documents records it in errors, and also addresses the Ruff BLE001 hint by catching a narrower exception where possible.

🛠️ Proposed fix
 def extract_text_from_docx(file_bytes):
     """Extract text from docx bytes"""
     try:
         doc = Document(io.BytesIO(file_bytes))
-        return "\n".join([p.text for p in doc.paragraphs])
-    except Exception as e:
-        logger.error(f"Error extracting DOCX text: {e}")
-        return ""
+    except Exception as e:
+        logger.error("Error extracting DOCX text: %s", e)
+        raise ValueError(f"Invalid DOCX file: {e}") from e
+    return "\n".join(p.text for p in doc.paragraphs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/document-analyzer/document_analyzer.py` around lines 76 - 83, The
function extract_text_from_docx currently swallows all errors and returns an
empty string, causing callers like _collect_prompt_and_documents to treat failed
DOCX parses as valid empty documents; change it to not return "" on failure but
instead re-raise (or raise a dedicated error) so the caller can record it in
errors: replace the broad except block in extract_text_from_docx with a narrower
exception catch if possible (or except Exception as e), log the error with
details via logger.error, then raise a new DocumentProcessingError (or re-raise
e) so _collect_prompt_and_documents can handle it; ensure you add the
DocumentProcessingError class (or appropriate exception) and update any
imports/typing as needed.
frontend/src/routes/api/v2/export/+server.ts-129-139 (1)

129-139: ⚠️ Potential issue | 🟠 Major

Mirror the stream cleanup pattern from downloadFile.ts to avoid resource leaks.

Lines 129–139 reject on invalid chunks but leave the stream flowing and listeners attached. When rejection occurs on line 134, the stream should be destroyed and all handlers removed. Use named handlers with a cleanup function and once() for terminal events, matching the proven pattern at frontend/src/lib/server/files/downloadFile.ts:28–57.

Suggested fix
 								const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
 									const chunks: Uint8Array[] = [];
-									fileStream.on("data", (chunk) => {
+									const onData = (chunk: unknown) => {
 										if (chunk instanceof Uint8Array) {
 											chunks.push(chunk);
 											return;
 										}
+
+										const err = new Error("Unexpected chunk type from avatar download stream");
+										fileStream.destroy();
+										cleanup();
-										reject(new Error("Unexpected chunk type from avatar download stream"));
-									});
-									fileStream.on("error", (err) =>
+										reject(err);
+									};
+
+									const onError = (err: Error | null | undefined) => {
+										cleanup();
 										reject(err ?? new Error("Avatar download failed"))
-									);
-									fileStream.on("end", () => resolve(Buffer.concat(chunks)));
+									};
+
+									const onEnd = () => {
+										cleanup();
+										resolve(Buffer.concat(chunks));
+									};
+
+									const cleanup = () => {
+										fileStream.off("data", onData);
+										fileStream.off("error", onError);
+										fileStream.off("end", onEnd);
+									};
+
+									fileStream.on("data", onData);
+									fileStream.once("error", onError);
+									fileStream.once("end", onEnd);
 								});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/api/v2/export/`+server.ts around lines 129 - 139, The
avatar download promise currently attaches anonymous "data", "error", and "end"
handlers to fileStream and rejects on invalid chunks without cleaning up; update
the logic to use named handlers for onData, onError, and onEnd, attach terminal
handlers with once(), implement a cleanup function that removes all three
listeners and calls fileStream.destroy() when rejecting or resolving, and call
that cleanup from both onError (and on invalid chunk rejection) and onEnd before
resolving the Buffer.concat(chunks); reference the existing fileStream variable
and the Buffer.concat(chunks) resolve path to mirror the downloadFile.ts
pattern.
frontend/src/lib/components/chat/ChatMessage.svelte-70-84 (1)

70-84: ⚠️ Potential issue | 🟠 Major

Exclude the wrapper itself from the interactive-target guard.

The wrapper element now has role="button" (line 271). Since Element.closest() includes ancestors, any click inside the message matches the wrapper and returns early (line 76), preventing handleContentClick() and isTapped from executing. This breaks image lightbox and message toggling. Apply the same guard to the onkeydown handler to prevent bubbled keyboard events from child controls from toggling the message.

Proposed fix
 function handleMessageClick(e: MouseEvent) {
-	const target = e.target as HTMLElement | null;
-	if (
-		target &&
-		target.closest(
-			"button, a, input, textarea, select, [role='button'], [data-no-toggle]"
-		)
-	) {
+	const target = e.target;
+	const root = e.currentTarget;
+	if (!(target instanceof Element) || !(root instanceof Element)) {
+		return;
+	}
+
+	const interactiveTarget = target.closest(
+		"button, a, input, textarea, select, [role='button'], [data-no-toggle]"
+	);
+	if (interactiveTarget && interactiveTarget !== root) {
 		return;
 	}
 	handleContentClick(e);
-	if (!(e.target instanceof HTMLImageElement)) {
+	if (!(target instanceof HTMLImageElement)) {
 		isTapped = !isTapped;
 	}
 }
 onkeydown={(event) => {
+	if (event.target !== event.currentTarget) {
+		return;
+	}
 	if (event.key === "Enter" || event.key === " ") {
 		event.preventDefault();
 		isTapped = !isTapped;
 	}
 }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/components/chat/ChatMessage.svelte` around lines 70 - 84,
handleMessageClick currently returns early because target.closest matches the
wrapper's role="button" ancestor; change the interactive-target guard in
handleMessageClick to ignore the wrapper itself by comparing the matched
ancestor against e.currentTarget (i.e., only return if closest(...) finds an
ancestor that is not the message wrapper/currentTarget). Apply the same guard
logic to the onkeydown handler (the keydown toggle handler) so keyboard events
originating from child interactive controls (but not the wrapper itself) don't
toggle the message; reference handleMessageClick and the onkeydown toggle
handler and use e.currentTarget to distinguish the wrapper from ancestor
matches.
bindu/extensions/did/did_agent_extension.py-251-287 (1)

251-287: ⚠️ Potential issue | 🟠 Major

Replace localized group names with well-known SIDs and narrow overly broad exception handlers.

This code will fail on non-English Windows systems:

  1. Localized built-in principal names (critical). "Users", "Authenticated Users", and "Everyone" are localized on non-English Windows (e.g., German "Benutzer", French "Tout le monde"). The icacls /remove:g command will return non-zero, which deletes freshly written keys and breaks agent startup. Use well-known SIDs instead (locale-independent):

    • Users → *S-1-5-32-545
    • Authenticated Users → *S-1-5-11
    • Everyone → *S-1-1-0
  2. Overly broad except Exception (lines 258, 263). Narrow to except OSError: to avoid masking unrelated errors. os.getlogin() specifically raises OSError when there is no controlling terminal.

  3. User-sourced string in command. Resolve the username to a SID (e.g., via win32security.LookupAccountName or whoami /user) to avoid ambiguity with homonymous domain accounts and exotic characters.

Suggested diff
         # Remove inherited ACLs and grant full control to current user only.
+        # Use well-known SIDs so this works on non-English Windows.
         cmd = [
             "icacls",
             str(path),
             "/inheritance:r",
             "/grant:r",
             f"{username}:F",
             "/remove:g",
-            "Users",
-            "Authenticated Users",
-            "Everyone",
+            "*S-1-5-32-545",  # BUILTIN\Users
+            "*S-1-5-11",      # NT AUTHORITY\Authenticated Users
+            "*S-1-1-0",       # Everyone
         ]
         result = subprocess.run(cmd, capture_output=True, text=True, check=False)

And at lines 258 and 263:

             except Exception:
+            except OSError:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/did/did_agent_extension.py` around lines 251 - 287, In
_harden_windows_key_file_acl, replace the localized group names ("Users",
"Authenticated Users", "Everyone") in the icacls /remove:g args with the
locale-independent well-known SIDs (*S-1-5-32-545, *S-1-5-11, *S-1-1-0), narrow
the broad except Exception handlers around os.getlogin() and getpass.getuser()
to except OSError, and resolve the current username to a SID before calling
icacls (e.g., via win32security.LookupAccountName or by parsing whoami /user
output) so the command uses the SID for granting/removing ACLs rather than a
potentially ambiguous or localized account name. Ensure the RuntimeError
messages remain but only raise after these narrower checks and SID resolution
fail.
frontend/src/lib/server/database.ts-471-553 (1)

471-553: ⚠️ Potential issue | 🟠 Major

Event ordering in openDownloadStream is registration-order dependent and fragile.

Initial data/end/error emissions are scheduled via setTimeout(..., 0) per event inside scheduleInitialEmit. Because each setTimeout enqueues independently, the execution order depends entirely on the order listeners are registered. A consumer registering .on("end", ...).on("data", ...) will observe end before data, violating readable-stream semantics.

While current consumers (downloadFile.ts, export/+server.ts) happen to register listeners in the correct order (dataerrorend), this is coincidental fragility. If a future consumer registers in a different order, it breaks.

Additionally, if the file is missing and a consumer only subscribes to data/end without error, nothing gets scheduled and the stream hangs.

Decouple the emission sequence from listener registration order by scheduling the full sequence (dataend or error) once on first subscription, regardless of event name:

♻️ Sketch
-		const scheduleInitialEmit = (event: string) => {
-			if (event === "error" && !scheduled.error && !file) {
-				scheduled.error = true;
-				setTimeout(() => emit("error", new Error("File not found")), 0);
-			}
-			if (event === "data" && !scheduled.data && file) {
-				scheduled.data = true;
-				setTimeout(() => emit("data", file.data), 0);
-			}
-			if (event === "end" && !scheduled.end && file) {
-				scheduled.end = true;
-				setTimeout(() => emit("end"), 0);
-			}
-		};
+		let emissionScheduled = false;
+		const scheduleInitialEmit = () => {
+			if (emissionScheduled) return;
+			emissionScheduled = true;
+			setTimeout(() => {
+				if (!file) {
+					emit("error", new Error("File not found"));
+					return;
+				}
+				emit("data", file.data);
+				emit("end");
+			}, 0);
+		};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/server/database.ts` around lines 471 - 553, The current
openDownloadStream schedules each initial event separately in
scheduleInitialEmit, making emission order depend on listener registration and
leaving no error emitted if only data/end are subscribed; instead, change it to
schedule a single initial emission sequence on first subscription: in
openDownloadStream (functions: addListener, scheduleInitialEmit, downloadStream)
add a single scheduledInitial boolean and when the first listener is added call
setTimeout once to run a deterministic sequence that if file exists emits "data"
then "end" (in that order) and if file is missing emits "error", remove the
per-event setTimeouts and per-event scheduled flags, and ensure destroy prevents
further emits; this guarantees correct ordering regardless of registration order
and still emits error when file is missing even if only data/end listeners were
attached.
frontend/src/routes/+layout.svelte-4-4 (1)

4-4: ⚠️ Potential issue | 🟠 Major

Restore the error-store subscription or remove the toast path.

After dropping the error store wiring, currentError is never assigned, so <Toast> no longer appears for updates from frontend/src/lib/stores/errors.ts.

Possible fix
-	import { onMount, untrack } from "svelte";
+	import { onDestroy, onMount, untrack } from "svelte";
+	import { error } from "$lib/stores/errors";
@@
 	let isNavCollapsed = $state(false);
 	let currentError: string | undefined = $state();
+	let errorToastTimeout: ReturnType<typeof setTimeout> | undefined;
+
+	const unsubscribeError = error.subscribe((message) => {
+		currentError = message;
+		if (errorToastTimeout) clearTimeout(errorToastTimeout);
+		if (message) {
+			errorToastTimeout = setTimeout(() => {
+				currentError = undefined;
+				error.set(undefined);
+			}, 5000);
+		}
+	});
+
+	onDestroy(() => {
+		unsubscribeError();
+		if (errorToastTimeout) clearTimeout(errorToastTimeout);
+	});

Also applies to: 105-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/`+layout.svelte at line 4, The currentError used by the
<Toast> component is never updated because the subscription to the error store
was removed; restore a subscription to the error store (from
frontend/src/lib/stores/errors.ts) in the +layout.svelte component (use onMount
to subscribe and set currentError, and unsubscribe on destroy) so that updates
to the store assign currentError and trigger <Toast>, or alternatively remove
the <Toast> path and any references to currentError; make sure to reference the
currentError variable, the Toast component, and the error store
subscription/unsubscribe logic when applying the fix.
frontend/src/lib/services/agent-api.ts-216-220 (1)

216-220: ⚠️ Potential issue | 🟠 Major

Don’t expose the unconfigured singleton as the agent base URL.

agentAPI is constructed without a base URL, so getAgentBaseUrl() returns the http://localhost:3773 fallback in deployed browsers unless something else mutates this instance. That will route users to their own machine instead of the configured agent service. Initialize the singleton from the public config, or make this helper resolve/accept PUBLIC_AGENT_BASE_URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/services/agent-api.ts` around lines 216 - 220, The exported
singleton agentAPI is created without a configured base URL so getAgentBaseUrl()
returns the local fallback; fix by wiring the public config into the singleton
or by making the getter read the configured env var: initialize agentAPI using
the public config value (e.g., pass PUBLIC_AGENT_BASE_URL into new
AgentAPI(...)) or change getAgentBaseUrl() to return the resolved
PUBLIC_AGENT_BASE_URL when available and otherwise fall back to
agentAPI.getBaseUrl(); update references to the symbols agentAPI,
getAgentBaseUrl, AgentAPI and PUBLIC_AGENT_BASE_URL accordingly so deployed
clients get the real agent service URL rather than localhost.
bindu/extensions/voice/session_manager.py-125-144 (1)

125-144: ⚠️ Potential issue | 🟠 Major

Route session limits and cleanup timing through app_settings.

max_sessions, session_timeout, and the 30-second cleanup interval are runtime configuration. Hardcoding them here makes voice capacity/timeout tuning drift from deployment settings.

Shape of the change
+from bindu.settings import app_settings
@@
-    def __init__(self, max_sessions: int = 10, session_timeout: int = 300):
+    def __init__(
+        self,
+        max_sessions: int | None = None,
+        session_timeout: int | None = None,
+        cleanup_interval: int | None = None,
+    ):
         """Initialize in-memory voice session manager limits and cleanup state."""
+        max_sessions = max_sessions if max_sessions is not None else app_settings.voice.max_sessions
+        session_timeout = (
+            session_timeout
+            if session_timeout is not None
+            else app_settings.voice.session_timeout
+        )
+        self._cleanup_interval = (
+            cleanup_interval
+            if cleanup_interval is not None
+            else app_settings.voice.cleanup_interval
+        )
@@
-                await asyncio.sleep(30)  # check every 30 s
+                await asyncio.sleep(self._cleanup_interval)

As per coding guidelines, bindu/**/*.py: “Use bindu.settings.app_settings for all configuration - never hardcode values such as URLs, ports, timeouts, API keys, or feature flags” and “Settings imports should use from bindu.settings import app_settings”.

Also applies to: 263-268

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/session_manager.py` around lines 125 - 144, Use
bindu.settings.app_settings for runtime config: import "from bindu.settings
import app_settings" and in VoiceSessionManager.__init__ pull default values
from app_settings instead of hardcoding—if max_sessions or session_timeout are
not passed (or are None) set them from app_settings (e.g.
app_settings.VOICE_MAX_SESSIONS and app_settings.VOICE_SESSION_TIMEOUT), then
validate them as before; also replace the hardcoded 30-second cleanup interval
used when scheduling or running the _cleanup_task (see the cleanup logic around
the current _cleanup_task usage at ~lines 263-268) with a value from
app_settings (e.g. app_settings.VOICE_CLEANUP_INTERVAL), preserving the existing
validation and lock usage.
bindu/utils/notifications.py-252-260 (1)

252-260: ⚠️ Potential issue | 🟠 Major

Reject CR/LF before writing raw HTTPS headers.

The HTTPS path manually interpolates request_headers into the wire request. Since Authorization can come from webhook config, CR/LF in a header value can inject additional headers or corrupt the request.

Possible fix
 			context = ssl.create_default_context()
 			request_headers = {"Host": host_header, "Connection": "close", **headers}
 			request_headers["Content-Length"] = str(len(payload))
+			for key, value in request_headers.items():
+				if "\r" in key or "\n" in key or "\r" in value or "\n" in value:
+					raise NotificationDeliveryError(
+						None, "Invalid push notification header value"
+					)
 			try:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/utils/notifications.py` around lines 252 - 260, The code builds raw
HTTP request lines from request_headers into request_lines and directly
concatenates payload, which is vulnerable to header injection if any header
value (e.g., Authorization) contains CR/LF; sanitize or reject such values
before writing them to the wire by validating every header key/value in
request_headers (and host_header) to ensure they contain no '\r' or '\n' (either
raise an error or strip/encode them), and only after validation construct
request_lines and request_bytes; update the logic that assembles
request_headers/request_lines/payload to perform this check (or safe encoding)
so CR/LF cannot be injected into the raw request.
bindu/server/applications.py-118-120 (1)

118-120: ⚠️ Potential issue | 🟠 Major

Guard voice capability detection when manifest is absent.

manifest is optional in BinduApplication.__init__, but get_voice_extension_from_capabilities() dereferences manifest.capabilities. Instantiating with manifest=None can now fail before the later assertion.

🐛 Proposed fix
-        x402_ext = get_x402_extension_from_capabilities(manifest)
-        voice_ext = get_voice_extension_from_capabilities(manifest)
+        x402_ext = get_x402_extension_from_capabilities(manifest)
+        voice_ext = (
+            get_voice_extension_from_capabilities(manifest)
+            if manifest is not None
+            else None
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/applications.py` around lines 118 - 120, The constructor
BinduApplication.__init__ currently calls
get_voice_extension_from_capabilities(manifest) and
get_x402_extension_from_capabilities(manifest) unguarded, which will dereference
manifest.capabilities when manifest is None; change the init to first check if
manifest is not None (or manifest and manifest.capabilities) and only call
get_voice_extension_from_capabilities and get_x402_extension_from_capabilities
when manifest is present, otherwise set voice_ext and x402_ext to None (or their
safe defaults) so instantiation with manifest=None won't raise before the later
assertion.
frontend/src/lib/stores/chat.ts-21-29 (1)

21-29: ⚠️ Potential issue | 🟠 Major

Widen FilePart.file.bytes type to match normalizeFileBytes() signature.

The normalizeFileBytes() function accepts string | ArrayBuffer | Uint8Array | Blob, but the FilePart type restricts bytes to string only. This forces a cast at line 59, which obscures the type mismatch and makes the actual capability of the function invisible to the type system.

🐛 Proposed type fix
 type FilePart = {
   kind: 'file';
   fileId?: string;
   file?: {
-    bytes?: string;
+    bytes?: string | ArrayBuffer | Uint8Array | Blob;
     mimeType?: string;
     name?: string;
   };
   metadata?: Record<string, unknown>;
 };

Then remove the cast at line 59:

-      const normalizedBytes = await normalizeFileBytes(file.bytes as string | ArrayBuffer | Uint8Array | Blob);
+      const normalizedBytes = await normalizeFileBytes(file.bytes);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/stores/chat.ts` around lines 21 - 29, The FilePart type
restricts file.bytes to string, but normalizeFileBytes accepts string |
ArrayBuffer | Uint8Array | Blob; update the FilePart definition (the
FilePart.file.bytes field) to union type string | ArrayBuffer | Uint8Array |
Blob to reflect actual inputs, then remove the unnecessary cast when calling
normalizeFileBytes() so the call uses the widened type directly; references:
FilePart, FilePart.file.bytes, and normalizeFileBytes().
bindu/server/endpoints/voice_endpoints.py-388-397 (1)

388-397: ⚠️ Potential issue | 🟠 Major

Do not let preflight block a configured TTS fallback.

create_tts_service() can fall back when the primary provider is missing credentials or dependencies, but this preflight rejects the request before fallback can run. For example, tts_provider="elevenlabs" without VOICE__TTS_API_KEY and tts_fallback_provider="piper" still fails here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 388 - 397, The
preflight currently fails when the configured primary tts_provider lacks
credentials even if a valid fallback exists; update the validation in
voice_endpoints.py to allow a configured fallback to satisfy requirements:
instead of immediately returning errors for missing primary credentials (e.g.,
when tts_provider == "elevenlabs" and no app_settings.voice.tts_api_key), check
app_settings.voice.tts_fallback_provider and validate that at least one provider
(primary or fallback) is supported and has required credentials/region;
reference tts_provider, tts_fallback_provider, app_settings.voice.*, and
create_tts_service to mirror the runtime fallback logic so preflight only errors
when neither primary nor fallback can be constructed.
frontend/src/lib/services/voice-client.ts-310-437 (1)

310-437: ⚠️ Potential issue | 🟠 Major

Harden audio setup cleanup and flush the final PCM buffer.

If resume()/addModule()/node setup fails, the mic stream and AudioContext can remain open. Also, the worklet never flushes its partial 4096-sample buffer on stopAudioStreaming(), so the end of a turn can be lost. Finally, verify audioContext.sampleRate; browsers may not honor 16000.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/services/voice-client.ts` around lines 310 - 437, The audio
setup can leak MediaStream/AudioContext on failures and never flush the
worklet's partial buffer at stop; wrap the setup in try/catch/finally to always
stop stream tracks and close audioContext if any step
(navigator.mediaDevices.getUserMedia, audioContext.resume,
audioContext.audioWorklet.addModule, node creation) fails, and ensure
URL.revokeObjectURL runs (already in finally) but also perform cleanup there if
audioContext or stream exist; modify the inline PCM16Processor (class
PCM16Processor / registerProcessor 'pcm16-processor') to handle a port message
like {cmd:'flush'} that calls flushBuffer(), and when tearing down
(stopAudioStreaming / cleanupAudioStreaming / before closing audioContext)
postMessage({cmd:'flush'}) to workletNode.port and await a short confirmation or
give a brief timeout to ensure the partial buffer is sent; finally check actual
audioContext.sampleRate after creation and, if it differs from desiredSampleRate
(16000), mark that you must resample audio in the processor or perform
client-side resampling before sending so sample-rate mismatches are handled.
frontend/src/lib/utils/agentMessageHandler.ts-122-141 (1)

122-141: ⚠️ Potential issue | 🟠 Major

Avoid byte-by-byte string concatenation for attachment base64 conversion.

For multi-MB Blob/ArrayBuffer attachments, this creates many intermediate strings and can freeze the UI. Build chunks and join them before btoa.

⚡ Suggested conversion improvement
 function base64FromBytes(bytes: Uint8Array): string {
-	let binary = '';
-	for (let i = 0; i < bytes.length; i++) {
-		binary += String.fromCharCode(bytes[i] ?? 0);
-	}
-	return btoa(binary);
+	const chunkSize = 0x8000;
+	const chunks: string[] = [];
+	for (let i = 0; i < bytes.length; i += chunkSize) {
+		chunks.push(String.fromCharCode(...bytes.subarray(i, i + chunkSize)));
+	}
+	return btoa(chunks.join(''));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/utils/agentMessageHandler.ts` around lines 122 - 141, The
current base64 conversion in base64FromBytes and normalizeFileValue concatenates
characters one-by-one which causes many intermediate strings and UI freezes for
large Blobs/ArrayBuffers; fix it by converting the Uint8Array in chunks (e.g.,
~32KB or 0x8000 bytes) using String.fromCharCode on each chunk, push each chunk
string into an array, join the array once to produce the full binary string,
then call btoa on the joined string; update base64FromBytes to implement chunked
processing and keep normalizeFileValue's Blob/ArrayBuffer branches to delegate
to the new chunked base64FromBytes implementation.
bindu/server/endpoints/voice_endpoints.py-960-981 (1)

960-981: ⚠️ Potential issue | 🟠 Major

Exit on normal reader completion, not only exceptions.

asyncio.FIRST_EXCEPTION does not return when the reader finishes normally from a disconnect/stop frame while the runner stays pending. That can keep the handler alive until session_timeout and report a timeout instead of cleaning up immediately.

🔁 Suggested wait mode fix
 			done, pending = await asyncio.wait(
 				{runner_task, reader_task},
-				return_when=asyncio.FIRST_EXCEPTION,
+				return_when=asyncio.FIRST_COMPLETED,
 			)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 960 - 981, The wait
currently uses asyncio.FIRST_EXCEPTION which won't return when reader_task
completes normally; change the asyncio.wait call in the handler that awaits
{runner_task, reader_task} to use return_when=asyncio.FIRST_COMPLETED (keeping
the surrounding timeout) so the function exits immediately when reader_task
finishes (e.g., disconnect/stop frame); after the wait, inspect done for
reader_task and runner_task, cancel the other if still pending, and raise any
exceptions found (reader_task.exception() / runner_task.exception()) and still
await pending via asyncio.gather(..., return_exceptions=True) to ensure clean
shutdown.
bindu/server/endpoints/voice_endpoints.py-449-459 (1)

449-459: ⚠️ Potential issue | 🟠 Major

Validate JSON control frames are objects before calling .get().

A client can send valid JSON like [], "x", or null; payload.get(...) then raises and gets reported as a 1011 pipeline error instead of a controlled malformed-frame close.

🛡️ Suggested validation
 			try:
-				payload = json.loads(text)
+				payload = json.loads(text)
 			except json.JSONDecodeError:
 				await _send_error_and_close(
 					websocket,
 					"Malformed JSON control frame",
 					send_lock=send_lock,
 				)
 				return
+			if not isinstance(payload, dict):
+				await _send_error_and_close(
+					websocket,
+					"Malformed JSON control frame",
+					send_lock=send_lock,
+				)
+				return
 
 			frame_type = payload.get("type")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 449 - 459, The code
assumes payload is a dict after json.loads and calls payload.get(...), which
raises for JSON values like [], "x", or null; fix by validating payload is a
mapping before using .get: after json.loads(text) check isinstance(payload,
dict) (or collections.abc.Mapping) and if not call
_send_error_and_close(websocket, "Malformed JSON control frame",
send_lock=send_lock) and return; then safely read frame_type =
payload.get("type").
frontend/src/lib/components/chat/ChatWindow.svelte-165-184 (1)

165-184: ⚠️ Potential issue | 🟠 Major

Forward task routing options when sending agent messages.

sendAgentMessage supports currentTaskId, taskState, and replyToTaskId, but this call only passes fileParts. That drops explicit replies and continuation of input-required tasks.

🔁 Suggested routing fix
 	const contextId = agentContextId ?? undefined;
+	const latestTaskMetadata = [...messages]
+		.reverse()
+		.find((message) => message.taskMetadata)?.taskMetadata;
 	let success = false;
 	try {
 		let streamed = "";
-		for await (const update of sendAgentMessage(message, contextId, { fileParts })) {
+		for await (const update of sendAgentMessage(message, contextId, {
+			fileParts,
+			currentTaskId: latestTaskMetadata?.taskId,
+			taskState: latestTaskMetadata?.status,
+			replyToTaskId: replyToTaskId ?? undefined,
+		})) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/components/chat/ChatWindow.svelte` around lines 165 - 184,
The submit function is dropping task-routing options when calling
sendAgentMessage (only fileParts is forwarded), which loses explicit replies and
continuation of input-required tasks; update the sendAgentMessage invocation
inside submit to include currentTaskId, taskState, and replyToTaskId in the
options object (e.g., sendAgentMessage(message, contextId, { fileParts,
currentTaskId, taskState, replyToTaskId })), and ensure those symbols
(currentTaskId, taskState, replyToTaskId) are read from the component
props/state and their types propagated so the call compiles and preserves
routing behavior.
frontend/src/lib/components/chat/ChatWindow.svelte-442-453 (1)

442-453: ⚠️ Potential issue | 🟠 Major

Make the active voice button stop the current session.

When $voiceSessionId is set, toggleVoiceSession() returns immediately, so the active “End voice call” button cannot end the call.

🎙️ Suggested behavior fix
 import {
 	startVoiceSession,
+	stopVoiceSession,
 	voiceSessionId,
 	voiceError,
 } from "$lib/stores/voice";
@@
 async function toggleVoiceSession() {
 	if ($voiceSessionId) {
+		await stopVoiceSession();
 		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/components/chat/ChatWindow.svelte` around lines 442 - 453,
toggleVoiceSession currently returns when $voiceSessionId is set, preventing the
active "End voice call" button from ending the call; change it so when
$voiceSessionId is truthy it calls the corresponding stop function (e.g.,
stopVoiceSession()) in a try/catch, clears any $error, and returns only after
the stop completes; otherwise, keep the existing logic to start a session by
calling startVoiceSession(agentContextId ?? undefined) with error handling that
sets $error on failure.
frontend/src/lib/stores/voice.ts-103-173 (1)

103-173: ⚠️ Potential issue | 🟠 Major

Reset isStarting when cancelling an in-flight start.

endVoiceSession() invalidates startTokenCounter while startVoiceSession() is pending. The pending start then hits the stale-token return path, but its finally skips isStarting = false because startToken !== startTokenCounter. After that, future starts always throw "A voice session is already starting".

🐛 Proposed fix
 export async function endVoiceSession(): Promise<void> {
 	startTokenCounter += 1;
+	isStarting = false;
 	const active = client;
 	client = null;

Also applies to: 176-178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/stores/voice.ts` around lines 103 - 173, The pending start
can be cancelled by endVoiceSession (which increments startTokenCounter),
causing early returns in startVoiceSession's stale-token checks that skip
resetting isStarting; update startVoiceSession so whenever you take the
stale-token return path (both after await localClient.startSession(...) and
after await localClient.connect(...)) you first set isStarting = false before
calling localClient.stopSession() and returning; ensure the same change is
applied at the second stale-token check (the one around lines 176-178) so
isStarting is always cleared when a start is aborted.
bindu/extensions/voice/agent_bridge.py-34-39 (1)

34-39: 🛠️ Refactor suggestion | 🟠 Major

Move voice defaults into app_settings.

This file defines local config defaults for timeouts/history/fallback text and then falls back to them via getattr. Those values should live in settings so runtime behavior is controlled from one configuration surface. As per coding guidelines, "bindu/**/*.py: Use bindu.settings.app_settings for all configuration - never hardcode values such as URLs, ports, timeouts, API keys, or feature flags" and "NEVER create local config constants - use app_settings from bindu.settings."

Also applies to: 83-115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/agent_bridge.py` around lines 34 - 39, Local config
constants MAX_HISTORY_TURNS and the DEFAULT_* timeout/text values must be moved
into bindu.settings.app_settings and the module should stop hardcoding/falling
back to locals; add new app_settings attributes (e.g., voice_max_history_turns,
voice_first_token_timeout_seconds, voice_total_response_timeout_seconds,
voice_cancellation_grace_seconds, voice_thinking_text,
voice_timeout_fallback_text), update agent_bridge.py to import app_settings and
replace any use of the local constants or getattr(..., 'SOME_CONST', <local>)
with direct reads from those new app_settings attributes, and remove the
now-unneeded local constant definitions (also apply the same replacement for the
other constants referenced later in the file around the DEFAULT_* group).
🧹 Nitpick comments (4)
examples/pdf_research_agent/pdf_research_agent.py (1)

83-92: Address Ruff B904/RUF010 in the PDF read error paths.

Minor cleanup to chain exception context and drop the redundant str() conversion flagged by static analysis:

🛠️ Proposed fix
-        except ImportError:
-            raise DocumentReadError(
-                f"pypdf not installed and cannot read '{resolved_path}'. Run: uv add pypdf"
-            )
+        except ImportError as exc:
+            raise DocumentReadError(
+                f"pypdf not installed and cannot read '{resolved_path}'. Run: uv add pypdf"
+            ) from exc
         except Exception as e:
             if isinstance(e, DocumentReadError):
                 raise
             raise DocumentReadError(
-                f"Error reading PDF '{resolved_path}': {str(e)}"
+                f"Error reading PDF '{resolved_path}': {e}"
             ) from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 83 - 92,
Change the ImportError handler to capture the original exception and chain it,
and remove the redundant str() conversion in the generic exception handler: use
"except ImportError as e:" and raise DocumentReadError(f\"pypdf not installed
and cannot read '{resolved_path}'. Run: uv add pypdf\") from e, and in the other
handler keep "except Exception as e:" but format the message using {e} instead
of {str(e)} (keeping the existing "raise ... from e" to preserve exception
context); reference symbols: DocumentReadError and resolved_path.
tests/unit/utils/test_notifications.py (1)

244-252: Return a concrete resolved IP in _post_once tests.

patch.object(service, "_resolve_and_validate_destination") returns a MagicMock, so _post_once skips the target_url rewrite guarded by isinstance(destination_ip, str). These tests don’t currently verify the new IP-pinning behavior.

Use a concrete return value and assert the request target/Host header.

Example adjustment
-        with patch.object(service, "_resolve_and_validate_destination"):
+        with patch.object(
+            service,
+            "_resolve_and_validate_destination",
+            return_value="93.184.216.34",
+        ):
             with patch(
                 "bindu.utils.notifications.request.urlopen",
                 return_value=context_manager,
-            ):
+            ) as urlopen:
                 with pytest.raises(
                     NotificationDeliveryError, match="Unexpected status code: 500"
                 ) as exc_info:
                     service._post_once("http://example.com/webhook", {}, b"{}")
 
         assert exc_info.value.status == 500
+        req = urlopen.call_args.args[0]
+        assert req.full_url == "http://93.184.216.34/webhook"
+        assert req.get_header("Host") == "example.com"

Also applies to: 267-275, 283-291

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/utils/test_notifications.py` around lines 244 - 252, The tests
patching service._resolve_and_validate_destination currently leave it as a
MagicMock so _post_once never exercises the IP-pinning branch; update those
tests (including the blocks around lines with similar patterns) to have
_resolve_and_validate_destination return a concrete IP string (e.g., "1.2.3.4"),
call service._post_once("http://example.com/webhook", ..., b"{}"), and assert
that the outgoing request target/Host header was rewritten to the original host
("example.com") while the request was sent to the resolved IP (the returned
"1.2.3.4") so the new IP-pinning behavior is verified (referencing
service._post_once, _resolve_and_validate_destination, target_url and Host
header assertions).
bindu/extensions/voice/session_factory.py (1)

21-24: Use the absolute import for the Redis session manager.

♻️ Suggested import change
-    from .redis_session_manager import RedisVoiceSessionManager
+    from bindu.extensions.voice.redis_session_manager import RedisVoiceSessionManager

As per coding guidelines, use absolute imports with format from bindu.server.storage import StorageBackend instead of relative imports across packages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/session_factory.py` around lines 21 - 24, The relative
import of RedisVoiceSessionManager in session_factory.py should be replaced with
an absolute import per project guidelines; locate the try/except block that
imports RedisVoiceSessionManager and change the import to the absolute path
(e.g., import RedisVoiceSessionManager from
bindu.extensions.voice.redis_session_manager or the canonical package path used
in the project), keeping the same try/except behavior and symbol name so
RedisVoiceSessionManager is still conditionally imported and referenced
elsewhere in the module.
bindu/server/endpoints/voice_endpoints.py (1)

168-175: Avoid per-audio-frame info logs.

These paths can run dozens of times per second per session, which can flood logs and add avoidable overhead. Downgrade to debug or sample the logs.

Also applies to: 499-504

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 168 - 175, The
receive method in the FilteredWebSocket (async def receive) is emitting an
info-level log for every audio frame; change it to a debug-level log (or
implement sampling/conditional logging using logger.isEnabledFor(logging.DEBUG))
to avoid flooding logs — update the logger.info call in receive to logger.debug
and apply the same change to the similar logging at the other location
referenced (lines ~499-504) so per-frame logs are downgraded or sampled
consistently.

self._conversation_history.append({"role": "user", "content": cleaned})
self._trim_history()

response_text = await self._invoke_agent_streaming(emit_frames=emit_frames)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Pass the user utterance to manifest.run, not the history list.

manifest.run is wired from bindu/penguin/manifest.py, where the first positional argument is resolved as input_msg: str. Passing list(self._conversation_history) changes the agent input type and can break existing text agents or prompt formatting. Keep history in session_context if needed, but pass the current utterance as the input message.

🐛 Proposed fix
-        response_text = await self._invoke_agent_streaming(emit_frames=emit_frames)
+        response_text = await self._invoke_agent_streaming(
+            cleaned,
+            emit_frames=emit_frames,
+        )
@@
-            response_text = await self._invoke_agent_streaming(emit_frames=True)
+            response_text = await self._invoke_agent_streaming(
+                user_text,
+                emit_frames=True,
+            )
@@
-    async def _invoke_agent_streaming(self, *, emit_frames: bool) -> str | None:
+    async def _invoke_agent_streaming(
+        self,
+        user_text: str,
+        *,
+        emit_frames: bool,
+    ) -> str | None:
@@
-            raw = self._manifest_run(list(self._conversation_history))
+            raw = self._manifest_run(
+                user_text,
+                session_context={
+                    "context_id": self._context_id,
+                    "conversation_history": list(self._conversation_history),
+                },
+            )

Also applies to: 222-222, 245-245, 287-288

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/agent_bridge.py` at line 186, The call chain currently
passes list(self._conversation_history) into manifest.run which expects the
current utterance as its first positional arg (input_msg: str); change the
invocation so manifest.run receives the user's latest utterance (the variable
holding the current user message) instead of the history list, and keep the full
history inside session_context (or pass it as a named session/context param) so
prompt formatting stays intact; update the calls routed through
self._invoke_agent_streaming and any direct manifest.run calls (references:
manifest.run, self._invoke_agent_streaming, self._conversation_history,
session_context) at the other sites flagged (around the other call locations) to
follow the same pattern.

Comment on lines +40 to +58
def _resolve_and_check_ip(hostname: str, port: int) -> str:
"""Resolve hostname to an IP address and verify it is not in a blocked range."""
try:
resolved_ip = str(socket.getaddrinfo(hostname, None)[0][4][0])
addr = ipaddress.ip_address(resolved_ip)
infos = socket.getaddrinfo(hostname, port, type=socket.SOCK_STREAM)
addrs = [ipaddress.ip_address(info[4][0]) for info in infos]
except (socket.gaierror, ValueError) as exc:
raise ValueError(
f"Push notification URL hostname could not be resolved: {exc}"
) from exc

for blocked in _BLOCKED_NETWORKS:
if addr in blocked:
raise ValueError(
f"Push notification URL resolves to a blocked address range "
f"({addr} is in {blocked}). Internal addresses are not allowed."
)
for addr in addrs:
for blocked in _BLOCKED_NETWORKS:
if addr in blocked:
raise ValueError(
f"Push notification URL resolves to a blocked address range "
f"({addr} is in {blocked}). Internal addresses are not allowed."
)

return resolved_ip
return str(addrs[0])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Demonstrates that IPv4-mapped IPv6 loopback is not contained in the current IPv4 loopback CIDR check.
python - <<'PY'
import ipaddress

addr = ipaddress.ip_address("::ffff:127.0.0.1")
blocked = [
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
]
print("direct_match:", any(addr in network for network in blocked))
print("mapped:", addr.ipv4_mapped)
print("mapped_match:", any(addr.ipv4_mapped in network for network in blocked))
PY

Repository: GetBindu/Bindu

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Read the relevant sections of bindu/utils/notifications.py
cat -n bindu/utils/notifications.py | sed -n '35,65p'

Repository: GetBindu/Bindu

Length of output: 1425


🏁 Script executed:

#!/bin/bash
# Read the second location mentioned in the review
cat -n bindu/utils/notifications.py | sed -n '135,170p'

Repository: GetBindu/Bindu

Length of output: 1803


Normalize IPv4-mapped IPv6 addresses before blocked-range checks.

ipaddress.ip_address("::ffff:127.0.0.1") is an IPv6 address and will not match the IPv4 loopback CIDR entries. Normalize addr.ipv4_mapped before checking blocked networks, otherwise IPv4-mapped loopback/private destinations can bypass the SSRF guard.

Possible fix
+def _normalize_ip(addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> ipaddress.IPv4Address | ipaddress.IPv6Address:
+    if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped is not None:
+        return addr.ipv4_mapped
+    return addr
+
+
+def _is_blocked_ip(addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
+    normalized = _normalize_ip(addr)
+    return any(normalized in blocked for blocked in _BLOCKED_NETWORKS)
+
@@
-    for addr in addrs:
-        for blocked in _BLOCKED_NETWORKS:
-            if addr in blocked:
-                raise ValueError(
-                    f"Push notification URL resolves to a blocked address range "
-                    f"({addr} is in {blocked}). Internal addresses are not allowed."
-                )
+    for addr in addrs:
+        if _is_blocked_ip(addr):
+            raise ValueError(
+                f"Push notification URL resolves to a blocked address range "
+                f"({addr}). Internal addresses are not allowed."
+            )
@@
-        for blocked in _BLOCKED_NETWORKS:
-            if addr in blocked:
-                raise ValueError(
-                    f"Push notification URL resolves to a blocked address range "
-                    f"({addr} is in {blocked}). Internal addresses are not allowed."
-                )
+        if _is_blocked_ip(addr):
+            raise ValueError(
+                f"Push notification URL resolves to a blocked address range "
+                f"({addr}). Internal addresses are not allowed."
+            )

Applies to lines 40–58 and 143–167.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/utils/notifications.py` around lines 40 - 58, The code in
_resolve_and_check_ip (and the similar block at lines 143–167) does not
normalize IPv4-mapped IPv6 addresses, so addresses like ::ffff:127.0.0.1 bypass
CIDR checks; update the logic to, for each ipaddress.ip_address object (e.g.,
variable addr), replace it with addr.ipv4_mapped if addr.version == 6 and
addr.ipv4_mapped is not None before comparing against _BLOCKED_NETWORKS and
before returning the resolved IP string; ensure the returned value uses the
normalized IPv4 form when mapped and keep using the same exception
types/messages on failure.

Comment on lines +296 to +304
req = request.Request(target_url, data=payload, method="POST")
req.add_header("Host", host_header)
for key, value in headers.items():
req.add_header(key, value)

try:
# URL scheme is validated in validate_config() to only allow http/https
with request.urlopen(req, timeout=self.timeout) as response: # nosec B310
status = response.getcode()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verifies the default urllib opener includes redirect handling.
python - <<'PY'
from urllib import request

opener = request.build_opener()
print([type(handler).__name__ for handler in opener.handlers if "Redirect" in type(handler).__name__])
# Expected: includes HTTPRedirectHandler, meaning urlopen/openers follow redirects unless disabled.
PY

Repository: GetBindu/Bindu

Length of output: 81


🏁 Script executed:

# First, let's locate and examine the notifications.py file
fd -t f notifications.py bindu/

Repository: GetBindu/Bindu

Length of output: 86


🏁 Script executed:

# Once we have the file, read the relevant section and surrounding context
head -310 bindu/utils/notifications.py | tail -50

Repository: GetBindu/Bindu

Length of output: 2264


🏁 Script executed:

# Find the validate_config method to understand existing protections
rg -n "validate_config" bindu/utils/notifications.py -B 2 -A 10

Repository: GetBindu/Bindu

Length of output: 1718


🏁 Script executed:

# Search for webhook/URL handling and SSRF-related code
rg -n "urlopen|redirect|ssrf|SSRF" bindu/utils/notifications.py -i -B 2 -A 2

Repository: GetBindu/Bindu

Length of output: 1622


🏁 Script executed:

# Check what triggers the webhook POST (understand the threat model)
rg -n "def.*webhook|send.*webhook" bindu/ -i -B 1 -A 5 --type py

Repository: GetBindu/Bindu

Length of output: 9751


🏁 Script executed:

# Check if there's existing redirect handling in the codebase
rg -n "NoRedirectHandler|HTTPRedirectHandler|redirect" bindu/utils/notifications.py -i

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

# Understand which code path is used (socket vs urlopen)
# Look at the full _post_with_retry method to see the logic flow
sed -n '200,350p' bindu/utils/notifications.py

Repository: GetBindu/Bindu

Length of output: 5932


Disable redirects for HTTP webhooks to prevent SSRF bypass.

urllib.request.urlopen follows HTTP redirects by default. The initial hostname is validated and replaced with a resolved IP address to prevent SSRF, but a 3xx redirect response bypasses this protection entirely—the redirect target is not validated against the blocked IP ranges. A public webhook can redirect the POST to an internal address (e.g., cloud metadata endpoints, private services).

This vulnerability only affects the HTTP code path (lines 267–318); the HTTPS path uses raw socket I/O and does not follow redirects.

Possible fix
+        class NoRedirectHandler(request.HTTPRedirectHandler):
+            def redirect_request(self, req, fp, code, msg, headers, newurl):
+                return None
+
+        opener = request.build_opener(NoRedirectHandler)
+
         try:
             # URL scheme is validated in validate_config() to only allow http/https
-            with request.urlopen(req, timeout=self.timeout) as response:  # nosec B310
+            with opener.open(req, timeout=self.timeout) as response:
                 status = response.getcode()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
req = request.Request(target_url, data=payload, method="POST")
req.add_header("Host", host_header)
for key, value in headers.items():
req.add_header(key, value)
try:
# URL scheme is validated in validate_config() to only allow http/https
with request.urlopen(req, timeout=self.timeout) as response: # nosec B310
status = response.getcode()
req = request.Request(target_url, data=payload, method="POST")
req.add_header("Host", host_header)
for key, value in headers.items():
req.add_header(key, value)
class NoRedirectHandler(request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
opener = request.build_opener(NoRedirectHandler)
try:
# URL scheme is validated in validate_config() to only allow http/https
with opener.open(req, timeout=self.timeout) as response: # nosec B310
status = response.getcode()
🧰 Tools
🪛 Ruff (0.15.10)

[error] 296-296: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 303-303: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/utils/notifications.py` around lines 296 - 304, The webhook HTTP path
currently uses request.urlopen(req, timeout=self.timeout) which follows
redirects and can bypass the hostname/IP validation; fix by creating and using a
urllib opener that disables redirects (e.g., a custom HTTPRedirectHandler that
returns None or raises on redirect) and call opener.open(req,
timeout=self.timeout) instead of request.urlopen; keep using the existing req,
target_url, headers, and self.timeout variables and ensure you treat any 3xx
response as a failure/error so redirects cannot forward the POST to an
unvalidated host.

Comment on lines +94 to 115
async insertOne(
doc: Partial<T> | T
): Promise<{ insertedId: ObjectId; acknowledged: boolean }> {
const source = doc as Record<string, unknown>;
let id: ObjectId;
if ("_id" in source && source._id) {
if (source._id instanceof ObjectId) {
id = source._id;
} else {
try {
id = new ObjectId(String(source._id));
} catch {
id = new ObjectId();
}
}
} else {
id = new ObjectId();
}
const docWithId = { ...source, _id: id } as T;
this.data.set(id.toString(), docWithId);
return { insertedId: id, acknowledged: true };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for callers that rely on duplicate-key errors from insertOne.
rg -nP -C3 '\binsertOne\s*\(' --type=ts -g '!**/database.ts'
rg -nP -C2 '11000|E11000|DuplicateKey' --type=ts

Repository: GetBindu/Bindu

Length of output: 20564


🏁 Script executed:

#!/bin/bash
# Examine the migration lock code that uses insertOne and checks acknowledged
cat -n frontend/src/lib/migrations/lock.ts | head -30

# Check the imports in database.ts to understand ObjectId sources
head -20 frontend/src/lib/server/database.ts | grep -n "import\|ObjectId"

# Search for any code that explicitly checks the acknowledged flag or handles duplicates
rg -n "acknowledged" --type=ts

Repository: GetBindu/Bindu

Length of output: 3089


🏁 Script executed:

#!/bin/bash
# Search for index definitions or constraint setup
rg -n "semaphores" --type=ts | grep -i "index\|unique\|constraint"

# Check if there's any collection initialization code
rg -n "createIndex\|createCollection" --type=ts -A 2 -B 2

# Look for schema or type definitions that might hint at constraints
cat -n frontend/src/lib/types/Semaphore.ts 2>/dev/null || echo "File not found"

Repository: GetBindu/Bindu

Length of output: 935


insertOne silently overwrites on duplicate _id — breaks the lock mechanism.

The insertOne method at line 113 silently overwrites an existing document with the same _id instead of throwing a duplicate-key error. This breaks the semaphore/lock mechanism:

  • frontend/src/lib/migrations/lock.ts (line 22–24) expects a unique-key violation exception when acquiring a lock on a duplicate key
  • The semaphores collection has a unique index on the key field (defined in migrations.spec.ts line 16), but the mock database does not enforce it
  • If acquireLock(key) is called twice with the same key, both calls succeed and return different lock IDs; the second overwrites the first without throwing

The lock mechanism will silently fail under concurrent access or repeated lock attempts on the same key. Implement constraint enforcement to throw a mock duplicate-key error when inserting documents that would violate a unique index.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/server/database.ts` around lines 94 - 115, The mock
insertOne in insertOne should not silently overwrite existing entries: before
setting this.data, check whether the generated or provided _id (id.toString())
already exists and, if so, throw a duplicate-key error (simulate Mongo's error
with code 11000 and a message naming the conflicting _id); additionally enforce
any unique-indexed fields used by tests (e.g., the semaphores collection's "key"
field) by scanning this.data for an existing document with the same unique field
value and throwing the same 11000-style error when found; update insertOne (and
any helpers it uses) to perform these existence checks using
this.data.get()/iteration and only set this.data.set(...) when no conflict is
detected.

@Co-vengers Co-vengers force-pushed the feat(voice-agent)/documentation branch from dc8a54d to 5bac2a9 Compare April 18, 2026 15:46
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

♻️ Duplicate comments (1)
bindu/extensions/voice/agent_bridge.py (1)

255-260: ⚠️ Potential issue | 🔴 Critical

manifest.run still receives the full history list instead of the current user utterance.

_invoke_agent_streaming calls self._manifest_run(list(self._conversation_history)), but manifest.run in bindu/penguin/manifest.py resolves the first positional argument as input_msg: str. Passing a list will either break existing text-mode handlers or force them to defensively sniff the argument type. The current utterance should be passed as the input, with history provided via a session/context kwarg (e.g., session_context={"context_id": self._context_id, "conversation_history": list(self._conversation_history)}). Also applies to the call site in _invoke_and_emit via _handle_user_utterance (line 217) and the convenience path in process_transcription (line 156).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/agent_bridge.py` around lines 255 - 260, The call
sites are passing the entire conversation list as the first positional arg to
manifest.run; change them to pass the current user utterance as the first arg
and provide history via a session/context kwarg: update _invoke_agent_streaming
to call self._manifest_run(current_utterance, session_context={"context_id":
self._context_id, "conversation_history": list(self._conversation_history)})
(use the variable holding the current user utterance), and make the same change
for the other sites invoking self._manifest_run in _invoke_and_emit (via
_handle_user_utterance) and in process_transcription so manifest.run’s first
positional param (input_msg: str) receives a string and history is supplied via
session_context.
🧹 Nitpick comments (4)
bindu/settings.py (1)

1126-1133: Silent clamp of session_token_ttl masks misconfiguration.

The intent to fail-safe at startup is reasonable, but the clamp happens without any observability, so an operator setting VOICE__SESSION_TOKEN_TTL=3600 alongside VOICE__SESSION_TIMEOUT=300 will never know the TTL was silently reduced to 300. Please log a warning so this is traceable in deployment logs.

♻️ Proposed refactor
     `@model_validator`(mode="after")
     def _validate_session_token_ttl(self) -> "VoiceSettings":
         # Enforce that tokens never outlive sessions.
         # If misconfigured via env (e.g. TTL > timeout), clamp TTL to timeout
         # to fail safe without preventing the server from starting.
         if self.session_token_ttl > self.session_timeout:
+            import warnings
+            warnings.warn(
+                f"VOICE__SESSION_TOKEN_TTL ({self.session_token_ttl}) exceeds "
+                f"VOICE__SESSION_TIMEOUT ({self.session_timeout}); clamping to session_timeout.",
+                stacklevel=2,
+            )
             self.session_token_ttl = self.session_timeout
         return self
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/settings.py` around lines 1126 - 1133, The _validate_session_token_ttl
model_validator on VoiceSettings silently clamps session_token_ttl to
session_timeout when TTL > timeout; change it to emit a warning when this clamp
occurs so operators can observe misconfiguration—inside
_validate_session_token_ttl, detect the condition (self.session_token_ttl >
self.session_timeout), log a warning that includes both the configured TTL and
the effective clamped value and the field names (session_token_ttl,
session_timeout, VoiceSettings), then set session_token_ttl = session_timeout
and return self; use the project logging facility (or Python's
logging.getLogger(__name__)) rather than printing.
bindu/extensions/voice/service_factory.py (1)

42-64: Minor: constant-string getattr calls flagged by Ruff B009.

All four getattr(module, "ClassName") sites (lines 45, 111, 148, 196) can be replaced with direct attribute access since the attribute name is a literal. Wrap in try/except AttributeError (already in the surrounding except) rather than using getattr for "safety." Purely stylistic — feel free to defer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/service_factory.py` around lines 42 - 64, Replace the
literal getattr calls with direct attribute access and rely on the existing
try/except to catch AttributeError: e.g., in the STT branch use
deepgram_module.DeepgramSTTService instead of getattr(deepgram_module,
"DeepgramSTTService") (keep the surrounding try/except
ImportError/AttributeError), and make the same change for the three other
literal getattr sites referenced in the review so their module attributes are
accessed directly rather than via getattr.
examples/voice-agent/main.py (1)

35-44: Minor: non-numeric env vars will crash module import with an unhelpful error.

int(os.environ.get("OPENROUTER_MEMORY_TURNS", "4") or "4") will raise ValueError if someone sets OPENROUTER_MEMORY_TURNS=four, and it's raised at import time. Since this is example code, it's low-risk, but wrapping with a try/except and falling back to the default (with a printed warning) would make the example more newcomer-friendly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/main.py` around lines 35 - 44, The module currently
parses OPENROUTER_MEMORY_TURNS and VOICE_MAX_SENTENCES using int(...) at import
time which will raise ValueError for non-numeric env values; update the parsing
for OPENROUTER_MEMORY_TURNS and VOICE_MAX_SENTENCES to safely coerce the
environment value inside a try/except (or use a helper like safe_int) that
catches ValueError/TypeError, falls back to the default (4 and 2 respectively),
and emits a simple warning/print mentioning the variable name and the fallback
so the import won’t crash on bad env input.
bindu/extensions/voice/agent_bridge.py (1)

33-38: Move user-facing TTS fallback strings to VoiceSettings for i18n/config.

DEFAULT_THINKING_TEXT and DEFAULT_TIMEOUT_FALLBACK_TEXT are spoken to end users but hardcoded at module scope. Per the repo rule to route configuration through app_settings, and given the voice stack already supports locale selection via stt_language, these should be exposed as VoiceSettings fields so operators can localize/customize them without patching code. The numeric DEFAULT_*_SECONDS constants are fine as pure fallbacks since voice_settings overrides them. As per coding guidelines: "Use bindu.settings.app_settings for all configuration — never hardcode values such as URLs, ports, timeouts, API keys, or feature flags" and "NEVER create local config constants - use app_settings from bindu.settings".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/agent_bridge.py` around lines 33 - 38, The two
user-facing TTS strings DEFAULT_THINKING_TEXT and DEFAULT_TIMEOUT_FALLBACK_TEXT
must be moved out of module-scope constants and into the VoiceSettings model so
they can be configured/localized via app_settings/voice_settings; update any use
sites in agent_bridge.py to read voice_settings.thinking_text and
voice_settings.timeout_fallback_text (fall back to the current DEFAULT_*
constants if voice_settings is None or fields are empty), and ensure
VoiceSettings is loaded from bindu.settings.app_settings (respecting
stt_language/locale already in voice_settings); keep numeric DEFAULT_*_SECONDS
as-is but do not hardcode spoken strings in agent_bridge.py.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bindu/extensions/voice/service_factory.py`:
- Around line 106-133: The current _create_tts_service_for_provider uses
getattr(piper_module, "PiperTTSSettings", None) and falls back to the deprecated
voice_id kwarg; change this to instantiate the nested settings class on the
service (PiperTTSService.Settings) instead of checking for a module-level
PiperTTSSettings so the code always calls
PiperTTSService(settings=PiperTTSService.Settings(voice=voice_id),
sample_rate=...) or switch to PiperHttpTTSService with its Settings/base_url if
you prefer HTTP mode; update the instantiation in
_create_tts_service_for_provider to remove the getattr branch and always use
PiperTTSService.Settings(voice=voice_id) (or use PiperHttpTTSService.Settings
with base_url) when creating the tts instance.

In `@bindu/server/endpoints/voice_endpoints.py`:
- Around line 318-343: Update the WebSocket subprotocol parsing and acceptance
so tokens are not echoed and label/token are separated: change
_extract_ws_session_token to parse websocket.headers["sec-websocket-protocol"]
as a comma-separated list, treat the first non-empty item as the protocol label
and the second as the candidate token (fallback to _extract_bearer_token for
Authorization), enforce that the label equals the fixed protocol name
"bindu.voice.v1" and reject otherwise, perform token validation using
hmac.compare_digest for constant-time comparison, and call
websocket.accept(subprotocol="bindu.voice.v1") (never the raw token) only after
successful comparison; also update _has_ws_subprotocols checks to expect the
fixed label.
- Around line 690-750: The websocket handshake can reject after a session was
created with session_manager.get_session, leaving the session stuck in
"connecting"; in voice_websocket, after obtaining session via
session_manager.get_session, ensure you call and await
session_manager.end_session(session_id) before closing the websocket on failure
branches (rate limit rejection from _rate_limit_allow_ip, missing session token
on receive, invalid/expired token checks, and any other early close paths where
session is truthy); also catch and ignore errors from end_session so cleanup is
best-effort and does not mask the original close.
- Around line 465-491: In the stop-frame handling block (where frame_type ==
"stop") avoid returning from inside the finally which masks exceptions from
await websocket.close(); instead, set control.stopped = True and send the
"ended" state via _send_json, then call await websocket.close() inside a
try/except Exception as e that logs the failure (e.g., logger.debug or
logger.exception) and after that try/except perform a single return; remove the
finally:return pattern so errors from websocket.close() are not swallowed (refer
to control.stopped, _send_json, and websocket.close()).

In `@examples/document-analyzer/document_analyzer.py`:
- Line 199: Replace the personal email in the example metadata: locate the
"author" entry (the string key "author" in document_analyzer.py) and change the
value from the personal address "vyomrohila@gmail.com" to a project-owned alias
or a neutral placeholder (e.g., "project@example.com" or
"maintainer@example.com") so the public example contains no personal contact
information.
- Around line 177-180: The current branch hides partial extraction errors when
some files succeeded (extracted_docs) because it only returns errors if no
extracted_docs; update the conditional around extracted_docs and errors so
partial failures are surfaced: either fail-fast by returning the joined errors
immediately when errors is non-empty, or (preferably) return a combined response
that includes the analysis result plus a visible warning line with
"\n".join(errors) when both extracted_docs and errors exist. Locate the block
using the variables extracted_docs and errors and change the logic to (a) check
errors first to return failure, or (b) when extracted_docs and errors are both
truthy, prepend/append a clear warning message containing the error list to the
successful result before returning.
- Around line 23-25: The extractor currently treats an empty string from
extract_text_from_docx as a valid document and appends it; change the collector
logic in document_analyzer.py so that when extract_text_from_docx (and
equivalent extraction calls around lines noted) returns an empty string or only
whitespace you treat it as a failure: log an error/warning with context (file
name or path and reason) via logger from get_logger, and skip appending that
document (or raise/return failure upstream) instead of appending "" — update all
extraction call sites (the extract_text_from_docx call and the similar blocks at
the other indicated regions) to perform this emptiness check before adding to
the documents list.
- Around line 183-194: The handler currently returns the full RunOutput object
from agent.run(), causing inconsistent return types versus error paths that
return strings; change the success path to return result.content (i.e., extract
the generated text from the RunOutput) so the handler always returns a
JSON-serializable string; locate the call to agent.run() that assigns to result
and replace the returned value with result.content to match the error responses.

In `@examples/pdf_research_agent/pdf_research_agent.py`:
- Around line 18-20: The ALLOWED_BASE_DIR constant and the path-containment
check reject absolute paths shown in the docs; make the sandbox
explicit/configurable by reading ALLOWED_BASE_DIR from an environment variable
(fallback to the current example dir) and update the example message strings to
show a path under that directory. Concretely: change the ALLOWED_BASE_DIR
constant to load os.environ.get("ALLOWED_BASE_DIR", "<current-default>"), keep
the containment check (the function that validates/normalizes incoming document
paths—e.g., the path validation helper used where incoming user content is
parsed), and update the example prompt texts (the printed/returned sample JSON
like "/path/to/paper.pdf") to format a sample path using ALLOWED_BASE_DIR so
examples always point inside the allowed sandbox.
- Around line 200-204: Replace the current broad except Exception as e return
that echoes raw exception text with a server-side logged error and a generic
client-facing message: inside the except block (the except Exception as e
handler that returns the dict with keys "success","data","error") call the
module or app logger (e.g., logger.error or app.logger.error) to record the full
exception details and stack trace, then return {"success": False, "data": None,
"error": "An internal error occurred while processing the request"} so no
internal paths or provider internals are exposed to clients.
- Around line 183-185: The code currently short-circuits based on document_text
starting with "[" or "Error" which is stale now that _read_content() raises
DocumentReadError; remove the conditional that checks
document_text.startswith("[") or document_text.startswith("Error") and let valid
document text beginning with those characters be processed normally, and instead
ensure any callers of _read_content() catch DocumentReadError (reference the
_read_content() function and the document_text variable) so actual read errors
are handled via exceptions rather than string matching.
- Around line 83-92: Change the ImportError handler to capture the original
exception and chain it when re-raising, e.g. use "except ImportError as exc" and
raise DocumentReadError(...) from exc; also update the generic exception raise
to use f-string conversion by replacing "{str(e)}" with "{e!s}" so the
DocumentReadError raise reads f"Error reading PDF '{resolved_path}': {e!s}".
Ensure these edits are applied in the same try/except block where
DocumentReadError and resolved_path are used.
- Around line 193-198: The return currently places the raw result from agent.run
into the response dict which can be non-serializable; update the post-run
handling for result (the variable assigned from
agent.run(input=enriched_messages)) to unwrap the actual serializable payload
before returning—check for attributes like "content" or "response" on result
(e.g., hasattr(result, "content") then use result.content, else hasattr(result,
"response") then use result.response) and fall back to result if neither exists,
then return {"success": True, "data": unwrapped, "error": None}; keep this logic
adjacent to the agent.run call so it replaces the current direct use of result.

In `@examples/voice-agent/.env.example`:
- Around line 3-19: The example .env shows VOICE__TTS_MODEL=piper-local which is
ineffective with VOICE__TTS_PROVIDER=piper because
bindu/extensions/voice/service_factory.py only uses config.tts_model for the
ElevenLabs branch and constructs PiperTTSService with voice_id/sample_rate;
update the example by either removing VOICE__TTS_MODEL or changing the provider
to ElevenLabs (e.g., set VOICE__TTS_PROVIDER=elevenlabs) so VOICE__TTS_MODEL is
meaningful, and optionally add a short comment explaining that Piper ignores
tts_model (referencing config.tts_model, PiperTTSService, and
VoiceAgentExtension for context).

---

Duplicate comments:
In `@bindu/extensions/voice/agent_bridge.py`:
- Around line 255-260: The call sites are passing the entire conversation list
as the first positional arg to manifest.run; change them to pass the current
user utterance as the first arg and provide history via a session/context kwarg:
update _invoke_agent_streaming to call self._manifest_run(current_utterance,
session_context={"context_id": self._context_id, "conversation_history":
list(self._conversation_history)}) (use the variable holding the current user
utterance), and make the same change for the other sites invoking
self._manifest_run in _invoke_and_emit (via _handle_user_utterance) and in
process_transcription so manifest.run’s first positional param (input_msg: str)
receives a string and history is supplied via session_context.

---

Nitpick comments:
In `@bindu/extensions/voice/agent_bridge.py`:
- Around line 33-38: The two user-facing TTS strings DEFAULT_THINKING_TEXT and
DEFAULT_TIMEOUT_FALLBACK_TEXT must be moved out of module-scope constants and
into the VoiceSettings model so they can be configured/localized via
app_settings/voice_settings; update any use sites in agent_bridge.py to read
voice_settings.thinking_text and voice_settings.timeout_fallback_text (fall back
to the current DEFAULT_* constants if voice_settings is None or fields are
empty), and ensure VoiceSettings is loaded from bindu.settings.app_settings
(respecting stt_language/locale already in voice_settings); keep numeric
DEFAULT_*_SECONDS as-is but do not hardcode spoken strings in agent_bridge.py.

In `@bindu/extensions/voice/service_factory.py`:
- Around line 42-64: Replace the literal getattr calls with direct attribute
access and rely on the existing try/except to catch AttributeError: e.g., in the
STT branch use deepgram_module.DeepgramSTTService instead of
getattr(deepgram_module, "DeepgramSTTService") (keep the surrounding try/except
ImportError/AttributeError), and make the same change for the three other
literal getattr sites referenced in the review so their module attributes are
accessed directly rather than via getattr.

In `@bindu/settings.py`:
- Around line 1126-1133: The _validate_session_token_ttl model_validator on
VoiceSettings silently clamps session_token_ttl to session_timeout when TTL >
timeout; change it to emit a warning when this clamp occurs so operators can
observe misconfiguration—inside _validate_session_token_ttl, detect the
condition (self.session_token_ttl > self.session_timeout), log a warning that
includes both the configured TTL and the effective clamped value and the field
names (session_token_ttl, session_timeout, VoiceSettings), then set
session_token_ttl = session_timeout and return self; use the project logging
facility (or Python's logging.getLogger(__name__)) rather than printing.

In `@examples/voice-agent/main.py`:
- Around line 35-44: The module currently parses OPENROUTER_MEMORY_TURNS and
VOICE_MAX_SENTENCES using int(...) at import time which will raise ValueError
for non-numeric env values; update the parsing for OPENROUTER_MEMORY_TURNS and
VOICE_MAX_SENTENCES to safely coerce the environment value inside a try/except
(or use a helper like safe_int) that catches ValueError/TypeError, falls back to
the default (4 and 2 respectively), and emits a simple warning/print mentioning
the variable name and the fallback so the import won’t crash on bad env input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08dd8046-417a-4149-88ba-28ae434957d1

📥 Commits

Reviewing files that changed from the base of the PR and between dc8a54d and 5bac2a9.

📒 Files selected for processing (29)
  • SPLIT_PLAN.md
  • bindu/extensions/voice/agent_bridge.py
  • bindu/extensions/voice/pipeline_builder.py
  • bindu/extensions/voice/redis_session_manager.py
  • bindu/extensions/voice/service_factory.py
  • bindu/extensions/voice/session_factory.py
  • bindu/server/applications.py
  • bindu/server/endpoints/voice_endpoints.py
  • bindu/settings.py
  • bindu/utils/worker/messages.py
  • docs/VOICE.md
  • docs/VOICE_AGENT_PLAN.md
  • examples/README.md
  • examples/cerina_bindu/cbt/README.md
  • examples/document-analyzer/document_analyzer.py
  • examples/multilingual-collab-agent/main.py
  • examples/multilingual-collab-agent/skills/collaborate/skill.yaml
  • examples/multilingual-collab-agent/skills/research/skill.yaml
  • examples/multilingual-collab-agent/skills/translate/skill.yaml
  • examples/pdf_research_agent/README.md
  • examples/pdf_research_agent/pdf_research_agent.py
  • examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml
  • examples/typescript-langchain-agent/quiz-agent.ts
  • examples/typescript-langchain-quiz-agent/quiz-agent.ts
  • examples/voice-agent/.env.example
  • examples/voice-agent/README.md
  • examples/voice-agent/main.py
  • examples/voice-agent/skills/voice-brief-response-skill/skill.yaml
  • tests/unit/extensions/voice/test_agent_bridge.py
✅ Files skipped from review due to trivial changes (13)
  • examples/README.md
  • examples/pdf_research_agent/README.md
  • examples/cerina_bindu/cbt/README.md
  • examples/multilingual-collab-agent/skills/research/skill.yaml
  • examples/multilingual-collab-agent/skills/collaborate/skill.yaml
  • SPLIT_PLAN.md
  • examples/voice-agent/skills/voice-brief-response-skill/skill.yaml
  • bindu/server/applications.py
  • bindu/extensions/voice/pipeline_builder.py
  • docs/VOICE.md
  • examples/typescript-langchain-agent/quiz-agent.ts
  • tests/unit/extensions/voice/test_agent_bridge.py
  • examples/voice-agent/README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • examples/typescript-langchain-quiz-agent/quiz-agent.ts
  • examples/multilingual-collab-agent/skills/translate/skill.yaml
  • examples/multilingual-collab-agent/main.py
  • bindu/utils/worker/messages.py
  • examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml
  • bindu/extensions/voice/redis_session_manager.py

Comment on lines +106 to +133
def _create_tts_service_for_provider(provider: str, config: VoiceAgentExtension) -> Any:
if provider == "piper":
voice_id = config.tts_voice_id
try:
piper_module = importlib.import_module("pipecat.services.piper.tts")
PiperTTSService = getattr(piper_module, "PiperTTSService")
PiperTTSSettings = getattr(piper_module, "PiperTTSSettings", None)
except (ImportError, AttributeError) as e:
raise ImportError(
"Piper TTS requires pipecat[piper]. "
"Install with: pip install 'bindu[voice]'"
) from e

if PiperTTSSettings is not None:
tts = PiperTTSService(
settings=PiperTTSSettings(
voice=voice_id,
),
sample_rate=config.sample_rate,
)
else:
tts = PiperTTSService(
voice_id=voice_id,
sample_rate=config.sample_rate,
)

logger.info(f"Created Piper TTS: voice={voice_id}")
return tts
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

pipecat PiperTTSService vs PiperHttpTTSService 0.0.105 initialization settings

💡 Result:

PiperTTSService vs PiperHttpTTSService in pipecat 0.0.105: PiperTTSService: Local TTS synthesis. Runs Piper inference directly in-process, automatically downloads voice models. Initialization: PiperTTSService( voice_id: str | None = None, # Deprecated 0.0.105, use settings download_dir: Path | None = None, force_redownload: bool = False, use_cuda: bool = False, settings: PiperTTSSettings | None = None, # Preferred: PiperTTSService.Settings(voice="en_US-ryan-high") kwargs ) Example: tts = PiperTTSService(settings=PiperTTSService.Settings(voice="en_US-ryan-high")) PiperHttpTTSService: Connects to external Piper HTTP server for synthesis. Supports streaming, requires running server (e.g., python -m piper.http_server -m en_US-ryan-high). Initialization: PiperHttpTTSService( base_url: str, # e.g. "http://localhost:5000" aiohttp_session: ClientSession, voice_id: str | None = None, # Deprecated 0.0.105, use settings settings: PiperHttpTTSSettings | None = None, # Preferred: PiperHttpTTSService.Settings(voice="en_US-ryan-high") kwargs ) Example: async with aiohttp.ClientSession as session: tts = PiperHttpTTSService( base_url="http://localhost:5000", aiohttp_session=session, settings=PiperHttpTTSService.Settings(voice="en_US-ryan-high") ) Key differences: - Local vs remote (HTTP server) - PiperTTSService: auto-downloads models, CUDA option, no network - PiperHttpTTSService: needs separate server, aiohttp session required, base_url In 0.0.105: voice_id deprecated; use settings.voice=... . Settings runtime-updatable via TTSUpdateSettingsFrame. Both inherit TTSSettings (model, voice, language, extra).

Citations:


Fix Piper settings instantiation: nested class reference, not module-level attribute.

In pipecat ≥0.0.105, the settings classes are nested (PiperTTSService.Settings, PiperHttpTTSService.Settings), not module-level PiperTTSSettings. The getattr(piper_module, "PiperTTSSettings", None) check will always return None, causing silent fallback to the deprecated voice_id= kwarg:

tts = PiperTTSService(
    voice_id=voice_id,  # deprecated; raises warning in 0.0.105, breaks in future releases
    sample_rate=config.sample_rate,
)

Either:

  • Remove the conditional; use PiperTTSService.Settings(voice=voice_id) directly, or
  • Switch to PiperHttpTTSService with an explicit base_url config setting (recommended for distributed deployments).
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 111-111: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/extensions/voice/service_factory.py` around lines 106 - 133, The
current _create_tts_service_for_provider uses getattr(piper_module,
"PiperTTSSettings", None) and falls back to the deprecated voice_id kwarg;
change this to instantiate the nested settings class on the service
(PiperTTSService.Settings) instead of checking for a module-level
PiperTTSSettings so the code always calls
PiperTTSService(settings=PiperTTSService.Settings(voice=voice_id),
sample_rate=...) or switch to PiperHttpTTSService with its Settings/base_url if
you prefer HTTP mode; update the instantiation in
_create_tts_service_for_provider to remove the getattr branch and always use
PiperTTSService.Settings(voice=voice_id) (or use PiperHttpTTSService.Settings
with base_url) when creating the tts instance.

Comment on lines +318 to +343
def _extract_bearer_token(value: str | None) -> str | None:
if not value:
return None
parts = value.split()
if len(parts) == 2 and parts[0].lower() == "bearer":
token = parts[1].strip()
return token or None
return None


def _extract_ws_session_token(websocket: WebSocket) -> str | None:
"""Extract session token from WS headers (subprotocol or Authorization)."""
subprotocols = websocket.headers.get("sec-websocket-protocol")
if subprotocols:
# Starlette exposes the raw comma-separated list.
for item in subprotocols.split(","):
token = item.strip()
if token:
return token

return _extract_bearer_token(websocket.headers.get("authorization"))


def _has_ws_subprotocols(websocket: WebSocket) -> bool:
return bool(websocket.headers.get("sec-websocket-protocol"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

WebSocket subprotocol auth flow is fragile and leaks the token as a negotiated protocol.

Two related problems in the handshake path:

  1. _extract_ws_session_token (lines 330–336) treats every comma-separated Sec-WebSocket-Protocol value equally and returns the first non-empty token. If a client uses the common pattern of sending a protocol name + token (e.g. Sec-WebSocket-Protocol: bindu.voice.v1, <token>), provided will be "bindu.voice.v1", auth will fail, and there is no way for the server to distinguish the protocol label from the credential.

  2. await websocket.accept(subprotocol=provided) (line 732) echoes whatever the client sent as the negotiated subprotocol. Because the client is forced to send the raw token as the subprotocol, that token is echoed back in the Sec-WebSocket-Protocol response header (visible in browser devtools, intermediary logs, etc.) — and is also accepted before we validate it at line 742.

Please adopt a fixed protocol name (e.g. "bindu.voice.v1") and a separate token slot:

Sec-WebSocket-Protocol: bindu.voice.v1, <token>

Then:

  • parse the subprotocol list, take the first entry as the protocol label and the second as the candidate token,
  • call accept(subprotocol="bindu.voice.v1") after a constant-time token comparison (hmac.compare_digest),
  • reject connections that don't present the expected label.

This also closes the timing side channel on provided != expected.

Also applies to: 724-747

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 318 - 343, Update the
WebSocket subprotocol parsing and acceptance so tokens are not echoed and
label/token are separated: change _extract_ws_session_token to parse
websocket.headers["sec-websocket-protocol"] as a comma-separated list, treat the
first non-empty item as the protocol label and the second as the candidate token
(fallback to _extract_bearer_token for Authorization), enforce that the label
equals the fixed protocol name "bindu.voice.v1" and reject otherwise, perform
token validation using hmac.compare_digest for constant-time comparison, and
call websocket.accept(subprotocol="bindu.voice.v1") (never the raw token) only
after successful comparison; also update _has_ws_subprotocols checks to expect
the fixed label.

Comment on lines +465 to +491
if frame_type == "stop":
control.stopped = True
await _send_json(
websocket, {"type": "state", "state": "ended"}, send_lock
)
try:
await websocket.close()
finally:
return
if frame_type == "user_text":
user_text = payload.get("text")
if isinstance(user_text, str) and user_text.strip() and on_user_text:
try:
await on_user_text(user_text.strip())
except Exception:
logger.exception("Failed to handle user_text control frame")
continue
if frame_type in {"start", "commit_turn"}:
# If VAD is disabled, the transport/STT may rely on explicit turn boundary
# control frames (e.g. commit_turn). Forward these to the transport.
if not vad_enabled:
await inbound_queue.put(message)
continue

# Unknown control frame: ignore to preserve forward-compat.
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

return inside finally silences errors from websocket.close() (Ruff B012).

If await websocket.close() raises (e.g. connection already torn down), the finally: return swallows it and the stop handler exits silently. Prefer an explicit try/except Exception: logger.debug(...) and then return after the block so you don't mask real failures:

♻️ Proposed tweak
             if frame_type == "stop":
                 control.stopped = True
                 await _send_json(
                     websocket, {"type": "state", "state": "ended"}, send_lock
                 )
                 try:
                     await websocket.close()
-                finally:
-                    return
+                except Exception:
+                    logger.debug("Error closing websocket after stop frame", exc_info=True)
+                return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if frame_type == "stop":
control.stopped = True
await _send_json(
websocket, {"type": "state", "state": "ended"}, send_lock
)
try:
await websocket.close()
finally:
return
if frame_type == "user_text":
user_text = payload.get("text")
if isinstance(user_text, str) and user_text.strip() and on_user_text:
try:
await on_user_text(user_text.strip())
except Exception:
logger.exception("Failed to handle user_text control frame")
continue
if frame_type in {"start", "commit_turn"}:
# If VAD is disabled, the transport/STT may rely on explicit turn boundary
# control frames (e.g. commit_turn). Forward these to the transport.
if not vad_enabled:
await inbound_queue.put(message)
continue
# Unknown control frame: ignore to preserve forward-compat.
continue
if frame_type == "stop":
control.stopped = True
await _send_json(
websocket, {"type": "state", "state": "ended"}, send_lock
)
try:
await websocket.close()
except Exception:
logger.debug("Error closing websocket after stop frame", exc_info=True)
return
if frame_type == "user_text":
user_text = payload.get("text")
if isinstance(user_text, str) and user_text.strip() and on_user_text:
try:
await on_user_text(user_text.strip())
except Exception:
logger.exception("Failed to handle user_text control frame")
continue
if frame_type in {"start", "commit_turn"}:
# If VAD is disabled, the transport/STT may rely on explicit turn boundary
# control frames (e.g. commit_turn). Forward these to the transport.
if not vad_enabled:
await inbound_queue.put(message)
continue
# Unknown control frame: ignore to preserve forward-compat.
continue
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 473-473: return inside finally blocks cause exceptions to be silenced

(B012)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 465 - 491, In the
stop-frame handling block (where frame_type == "stop") avoid returning from
inside the finally which masks exceptions from await websocket.close(); instead,
set control.stopped = True and send the "ended" state via _send_json, then call
await websocket.close() inside a try/except Exception as e that logs the failure
(e.g., logger.debug or logger.exception) and after that try/except perform a
single return; remove the finally:return pattern so errors from
websocket.close() are not swallowed (refer to control.stopped, _send_json, and
websocket.close()).

Comment on lines +690 to +750
async def voice_websocket(websocket: WebSocket) -> None:
"""Bidirectional voice WebSocket handler using Pipecat pipeline."""
app: BinduApplication = websocket.app # type: ignore[assignment]
session_id = websocket.path_params.get("session_id", "")

session_manager = getattr(app, "_voice_session_manager", None)
if session_manager is None:
await websocket.close(code=1008, reason="Voice extension is not enabled")
return
if not app_settings.voice.enabled:
await websocket.close(code=1008, reason="Voice is disabled")
return

preflight_error = _voice_preflight_error()
if preflight_error:
await websocket.close(code=1008, reason=preflight_error)
return

session = await session_manager.get_session(session_id)
if session is None:
await websocket.close(code=1008, reason="Invalid session ID")
return

# Per-IP rate limit on websocket connects (best-effort)
client_host = websocket.client.host if websocket.client else None
if client_host:
allowed = await _rate_limit_allow_ip(
client_host,
limit_per_minute=int(app_settings.voice.rate_limit_per_ip_per_minute),
)
if not allowed:
await websocket.close(code=1008, reason="Rate limit exceeded")
return

if app_settings.voice.session_auth_required:
expected = getattr(session, "session_token", None)
expires_at = getattr(session, "session_token_expires_at", None)
provided = _extract_ws_session_token(websocket)

# If the client sent Sec-WebSocket-Protocol, the server should select one.
# We select the token itself as the negotiated subprotocol.
if provided and _has_ws_subprotocols(websocket):
await websocket.accept(subprotocol=provided)
else:
await websocket.accept()
if not provided:
try:
provided = (await websocket.receive_text()).strip()
except Exception:
await websocket.close(code=1008, reason="Missing session token")
return

if not expected or provided != expected:
await websocket.close(code=1008, reason="Invalid session token")
return
if isinstance(expires_at, (int, float)) and time.time() > float(expires_at):
await websocket.close(code=1008, reason="Session token expired")
return
else:
await websocket.accept()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missed session-state update on rejected WebSocket handshake.

Along the failure paths (session is None, rate limit hit, bad/expired token, invalid session token) the session may already be in connecting state in the manager but is never moved to ended. Combined with the get_session call preceding both rate limiting and auth, a rejected WS leaves a live session record until the timeout reaper kicks in — which inflates get_active_count and can cause legitimate clients to get 429s on /voice/session/start. Consider awaiting session_manager.end_session(session_id) on the auth/rate-limit rejection branches to free the slot immediately.

🧰 Tools
🪛 Ruff (0.15.10)

[warning] 738-738: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/endpoints/voice_endpoints.py` around lines 690 - 750, The
websocket handshake can reject after a session was created with
session_manager.get_session, leaving the session stuck in "connecting"; in
voice_websocket, after obtaining session via session_manager.get_session, ensure
you call and await session_manager.end_session(session_id) before closing the
websocket on failure branches (rate limit rejection from _rate_limit_allow_ip,
missing session token on receive, invalid/expired token checks, and any other
early close paths where session is truthy); also catch and ignore errors from
end_session so cleanup is best-effort and does not mask the original close.

Comment on lines +23 to +25
from bindu.utils.logging import get_logger

logger = get_logger("examples.document_analyzer")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t treat failed or empty extraction as a valid document.

extract_text_from_docx currently logs and returns "", and the collector appends that value. A corrupt DOCX or textless document can bypass the failure path and send empty document content to the LLM.

Suggested fix
-from bindu.utils.logging import get_logger
-
-logger = get_logger("examples.document_analyzer")
-
...
 def extract_text_from_docx(file_bytes):
     """Extract text from docx bytes"""
-    try:
-        doc = Document(io.BytesIO(file_bytes))
-        return "\n".join([p.text for p in doc.paragraphs])
-    except Exception as e:
-        logger.error(f"Error extracting DOCX text: {e}")
-        return ""
+    doc = Document(io.BytesIO(file_bytes))
+    return "\n".join(p.text for p in doc.paragraphs)
...
                     doc_text = extract_document_text(file_bytes, mime_type)
+                    if not doc_text.strip():
+                        raise ValueError(
+                            f"No extractable text found for file type: {mime_type}"
+                        )
                     extracted_docs.append(doc_text)

Also applies to: 76-83, 154-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/document-analyzer/document_analyzer.py` around lines 23 - 25, The
extractor currently treats an empty string from extract_text_from_docx as a
valid document and appends it; change the collector logic in
document_analyzer.py so that when extract_text_from_docx (and equivalent
extraction calls around lines noted) returns an empty string or only whitespace
you treat it as a failure: log an error/warning with context (file name or path
and reason) via logger from get_logger, and skip appending that document (or
raise/return failure upstream) instead of appending "" — update all extraction
call sites (the extract_text_from_docx call and the similar blocks at the other
indicated regions) to perform this emptiness check before adding to the
documents list.

Comment on lines 83 to +92
except ImportError:
return (
f"[pypdf not installed — cannot read '{source.strip()}'. "
"Run: uv add pypdf]"
raise DocumentReadError(
f"pypdf not installed and cannot read '{resolved_path}'. Run: uv add pypdf"
)
except Exception as e:
return f"Error reading PDF '{source.strip()}': {str(e)}"
if isinstance(e, DocumentReadError):
raise
raise DocumentReadError(
f"Error reading PDF '{resolved_path}': {str(e)}"
) from e
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant lines
cat -n examples/pdf_research_agent/pdf_research_agent.py | sed -n '75,100p'

Repository: GetBindu/Bindu

Length of output: 1264


🏁 Script executed:

# Let's also check the full context around lines 83-92
cat -n examples/pdf_research_agent/pdf_research_agent.py | sed -n '70,105p'

Repository: GetBindu/Bindu

Length of output: 1775


🌐 Web query:

Ruff exception handling chaining rules ImportError B904 exception context

💡 Result:

Ruff's B904 rule, named "raise-without-from-inside-except", detects raise statements inside except or except* clauses that lack a 'from' clause. It enforces explicit exception chaining using 'raise ... from err' to set the cause attribute (for direct causation) or 'raise ... from None' to suppress chaining, distinguishing intentional raises from handling errors. Without 'from', Python implicitly chains via context, but explicit use improves debugging clarity in tracebacks (e.g., "The above exception was the direct cause" vs. "During handling"). This applies to ImportError or any exception. Example violation: try: ... except ImportError: raise ValueError("Failed to import") Suggested fix: try: ... except ImportError as exc: raise ValueError("Failed to import") from exc Or to suppress: raise ValueError("Failed to import") from None The rule originates from flake8-bugbear, added in Ruff v0.0.138, and supports except* (Python 3.11+). Recent docs (as of 2026 issues) clarify nuances between implicit/explicit chaining. No auto-fix available.

Citations:


🌐 Web query:

Ruff f-string conversion flag {e!s} vs str(e) rule

💡 Result:

Ruff's rule RUF010 (explicit-f-string-type-conversion) flags the use of explicit str(e), repr(e), or ascii(e) calls inside f-strings, recommending f-string conversion flags like {e!s} instead. This is because conversion flags are more succinct and idiomatic. For str(e), replace with {e!s}, though often {e} alone suffices since f-strings auto-convert to str unless the object has a custom format. The rule provides an unsafe fix to automate replacement, added in Ruff v0.0.267.

Citations:


Fix the Ruff exception-handling findings.

Line 83: ImportError must be chained with from to satisfy the B904 rule (raise-without-from-inside-except). Capture the exception with as exc and re-raise with from exc.

Line 91: Replace {str(e)} with {e!s} to satisfy the RUF010 rule (explicit-f-string-type-conversion), which recommends using f-string conversion flags instead of explicit function calls.

Suggested fix
-        except ImportError:
+        except ImportError as exc:
             raise DocumentReadError(
                 f"pypdf not installed and cannot read '{resolved_path}'. Run: uv add pypdf"
-            )
+            ) from exc
         except Exception as e:
             if isinstance(e, DocumentReadError):
                 raise
             raise DocumentReadError(
-                f"Error reading PDF '{resolved_path}': {str(e)}"
+                f"Error reading PDF '{resolved_path}': {e!s}"
             ) from e
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 84-86: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 91-91: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 83 - 92,
Change the ImportError handler to capture the original exception and chain it
when re-raising, e.g. use "except ImportError as exc" and raise
DocumentReadError(...) from exc; also update the generic exception raise to use
f-string conversion by replacing "{str(e)}" with "{e!s}" so the
DocumentReadError raise reads f"Error reading PDF '{resolved_path}': {e!s}".
Ensure these edits are applied in the same try/except block where
DocumentReadError and resolved_path are used.

Comment on lines 183 to 185
# Check if document processing failed
if document_text.startswith("[") or document_text.startswith("Error"):
return document_text
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove the stale string-error short-circuit.

_read_content() now raises DocumentReadError; valid document text beginning with [ or Error will currently be returned unsummarized.

Suggested fix
-        # Check if document processing failed
-        if document_text.startswith("[") or document_text.startswith("Error"):
-            return document_text
-
         # Limit document size to prevent token overflow
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Check if document processing failed
if document_text.startswith("[") or document_text.startswith("Error"):
return document_text
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 183 - 185,
The code currently short-circuits based on document_text starting with "[" or
"Error" which is stale now that _read_content() raises DocumentReadError; remove
the conditional that checks document_text.startswith("[") or
document_text.startswith("Error") and let valid document text beginning with
those characters be processed normally, and instead ensure any callers of
_read_content() catch DocumentReadError (reference the _read_content() function
and the document_text variable) so actual read errors are handled via exceptions
rather than string matching.

Comment on lines 193 to +198
# Build a prompt that includes the full document text
prompt = f"Summarize the following document and highlight the key insights:\n\n{document_text}"
enriched_messages = [{"role": "user", "content": prompt}]

result = agent.run(input=enriched_messages)
return result
return {"success": True, "data": result, "error": None}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect existing agent.run(input=...) usage and response unwrapping patterns.
rg -n -C4 'agent\.run\(input=|hasattr\(result,\s*["'\'']content["'\'']|hasattr\(result,\s*["'\'']response["'\'']' --iglob '*.py'

Repository: GetBindu/Bindu

Length of output: 11555


🏁 Script executed:

# Check if other examples return results wrapped in dicts with "data" field
rg -n '{"success"\s*:\s*True.*"data"' --iglob '*.py' -B2 -A2

Repository: GetBindu/Bindu

Length of output: 162


🏁 Script executed:

# Get full context of pdf_research_agent.py to understand the return pattern
sed -n '180,210p' examples/pdf_research_agent/pdf_research_agent.py

Repository: GetBindu/Bindu

Length of output: 1296


🏁 Script executed:

# Check what agent.run() accepts - is message list vs string actually different?
rg -n 'def run\(' --iglob '*.py' -A10 | head -100

Repository: GetBindu/Bindu

Length of output: 7893


🏁 Script executed:

# Check if any other examples wrap agent.run() results in dicts
rg -n 'return\s*{.*"success"' --iglob '*.py' -B5 -A1

Repository: GetBindu/Bindu

Length of output: 171


🏁 Script executed:

# Look at the handler function signature and return type expectations
sed -n '165,210p' examples/pdf_research_agent/pdf_research_agent.py

Repository: GetBindu/Bindu

Length of output: 1823


🏁 Script executed:

# Check the full handler signature and what it's expected to return
sed -n '155,165p' examples/pdf_research_agent/pdf_research_agent.py

Repository: GetBindu/Bindu

Length of output: 408


🏁 Script executed:

# Verify if message list vs string input format has any documented difference
rg -n 'agent\.run.*input=' examples/ -B2 -A2 | head -60

Repository: GetBindu/Bindu

Length of output: 3908


Unwrap agent result before wrapping in response dict.

The raw result object is being stored in data without extracting the serializable content. Other examples (premium-advisor, web-scraping-agent, weather-research) unwrap results using hasattr(result, "content") or hasattr(result, "response") before returning. This prevents JSON serialization failures.

Suggested fix
         # Build a prompt that includes the full document text
         prompt = f"Summarize the following document and highlight the key insights:\n\n{document_text}"
-        enriched_messages = [{"role": "user", "content": prompt}]
 
-        result = agent.run(input=enriched_messages)
-        return {"success": True, "data": result, "error": None}
+        result = agent.run(input=prompt)
+        if hasattr(result, "content"):
+            data = result.content
+        elif hasattr(result, "response"):
+            data = result.response
+        else:
+            data = str(result)
+        return {"success": True, "data": data, "error": None}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Build a prompt that includes the full document text
prompt = f"Summarize the following document and highlight the key insights:\n\n{document_text}"
enriched_messages = [{"role": "user", "content": prompt}]
result = agent.run(input=enriched_messages)
return result
return {"success": True, "data": result, "error": None}
# Build a prompt that includes the full document text
prompt = f"Summarize the following document and highlight the key insights:\n\n{document_text}"
result = agent.run(input=prompt)
if hasattr(result, "content"):
data = result.content
elif hasattr(result, "response"):
data = result.response
else:
data = str(result)
return {"success": True, "data": data, "error": None}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 193 - 198,
The return currently places the raw result from agent.run into the response dict
which can be non-serializable; update the post-run handling for result (the
variable assigned from agent.run(input=enriched_messages)) to unwrap the actual
serializable payload before returning—check for attributes like "content" or
"response" on result (e.g., hasattr(result, "content") then use result.content,
else hasattr(result, "response") then use result.response) and fall back to
result if neither exists, then return {"success": True, "data": unwrapped,
"error": None}; keep this logic adjacent to the agent.run call so it replaces
the current direct use of result.

Comment on lines 200 to +204
except Exception as e:
return f"Error processing request: {str(e)}"
return {
"success": False,
"data": None,
"error": f"Error processing request: {e}",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid echoing unexpected exception details to clients.

The broad catch is flagged by Ruff and returns raw exception text, which can expose local paths or provider internals. Prefer logging server-side and returning a generic error for unexpected failures.

Suggested fix
-    except Exception as e:
+    except Exception:
         return {
             "success": False,
             "data": None,
-            "error": f"Error processing request: {e}",
+            "error": "Error processing request.",
         }
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 200-200: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 200 - 204,
Replace the current broad except Exception as e return that echoes raw exception
text with a server-side logged error and a generic client-facing message: inside
the except block (the except Exception as e handler that returns the dict with
keys "success","data","error") call the module or app logger (e.g., logger.error
or app.logger.error) to record the full exception details and stack trace, then
return {"success": False, "data": None, "error": "An internal error occurred
while processing the request"} so no internal paths or provider internals are
exposed to clients.

Comment on lines +3 to +19
VOICE__TTS_PROVIDER=piper
VOICE__TTS_VOICE_ID=en_US-ryan-high

# Optional fallback TTS (recommended for resiliency)
VOICE__TTS_FALLBACK_PROVIDER=azure
VOICE__AZURE_TTS_API_KEY=your-azure-speech-key
VOICE__AZURE_TTS_REGION=eastus
VOICE__AZURE_TTS_VOICE=en-US-SaraNeural

# If you still use ElevenLabs as primary/fallback:
# VOICE__TTS_API_KEY=your-elevenlabs-api-key

# Optional tuning
VOICE__ENABLED=true
VOICE__STT_MODEL=nova-3
VOICE__STT_LANGUAGE=en
VOICE__TTS_MODEL=piper-local
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

VOICE__TTS_MODEL=piper-local is a no-op with VOICE__TTS_PROVIDER=piper.

In bindu/extensions/voice/service_factory.py, config.tts_model is only consumed by the ElevenLabs branch (and stored on VoiceAgentExtension for metadata). The Piper branch constructs PiperTTSService using only voice_id/sample_rate, so piper-local is inert and likely to confuse users who think it selects a Piper model. Consider dropping the line, or swapping the example to ElevenLabs where the field is meaningful.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 8-8: [UnorderedKey] The VOICE__AZURE_TTS_API_KEY key should go before the VOICE__TTS_FALLBACK_PROVIDER key

(UnorderedKey)


[warning] 9-9: [UnorderedKey] The VOICE__AZURE_TTS_REGION key should go before the VOICE__TTS_FALLBACK_PROVIDER key

(UnorderedKey)


[warning] 10-10: [UnorderedKey] The VOICE__AZURE_TTS_VOICE key should go before the VOICE__TTS_FALLBACK_PROVIDER key

(UnorderedKey)


[warning] 18-18: [UnorderedKey] The VOICE__STT_LANGUAGE key should go before the VOICE__STT_MODEL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/.env.example` around lines 3 - 19, The example .env
shows VOICE__TTS_MODEL=piper-local which is ineffective with
VOICE__TTS_PROVIDER=piper because bindu/extensions/voice/service_factory.py only
uses config.tts_model for the ElevenLabs branch and constructs PiperTTSService
with voice_id/sample_rate; update the example by either removing
VOICE__TTS_MODEL or changing the provider to ElevenLabs (e.g., set
VOICE__TTS_PROVIDER=elevenlabs) so VOICE__TTS_MODEL is meaningful, and
optionally add a short comment explaining that Piper ignores tts_model
(referencing config.tts_model, PiperTTSService, and VoiceAgentExtension for
context).

# Conflicts:
#	examples/cerina_bindu/cbt/README.md
#	examples/multilingual-collab-agent/agent_config.json
#	examples/multilingual-collab-agent/main.py
#	examples/multilingual-collab-agent/skills/collaborate/skill.yaml
#	examples/multilingual-collab-agent/skills/research/skill.yaml
#	examples/multilingual-collab-agent/skills/translate/skill.yaml
#	examples/pdf_research_agent/README.md
#	examples/pdf_research_agent/pdf_research_agent.py
#	examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml
@Co-vengers Co-vengers force-pushed the feat(voice-agent)/documentation branch from 5bac2a9 to 79523ee Compare April 18, 2026 15:59
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (10)
examples/document-analyzer/document_analyzer.py (4)

199-199: ⚠️ Potential issue | 🟡 Minor

Replace the personal email in example metadata.

Use a neutral placeholder or project-owned alias for public example config.

Suggested fix
-    "author": "vyomrohila@gmail.com",
+    "author": "example@example.com",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/document-analyzer/document_analyzer.py` at line 199, Update the
example metadata entry where the "author" field is set to a personal email (the
string "author": "vyomrohila@gmail.com") in
examples/document-analyzer/document_analyzer.py and replace it with a neutral
placeholder or project-owned alias (e.g., "author": "example@example.com" or
"author": "project@example.com") so the public example does not include a
personal email address.

175-180: ⚠️ Potential issue | 🟠 Major

Surface partial extraction failures.

Line 177 only checks errors when no documents were extracted, so mixed success/failure uploads silently hide failed files.

Fail-fast option
     prompt, extracted_docs, errors = _collect_prompt_and_documents(messages)
 
+    if errors:
+        return "Failed to process documents:\n" + "\n".join(errors)
+
     if not extracted_docs:
-        if errors:
-            return "Failed to process documents:\n" + "\n".join(errors)
         return "No valid document found in the messages."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/document-analyzer/document_analyzer.py` around lines 175 - 180, The
current logic after calling _collect_prompt_and_documents(prompt,
extracted_docs, errors) only surfaces errors when no documents were extracted,
which hides partial failures; update the handling around the variables prompt,
extracted_docs, and errors so that if extracted_docs is non-empty but errors is
also non-empty you still surface those errors (e.g., append or return a message
listing failed files or include errors in the response/log alongside successful
results), ensuring partial extraction failures are reported rather than silently
ignored.

183-194: ⚠️ Potential issue | 🟠 Major

Return the generated content, not the RunOutput object.

agent.run() returns a RunOutput; the response text is in .content, per the Agno run docs. Returning the object keeps the success path inconsistent with the string error paths.

Suggested fix
     result = agent.run(
         input=f"""
 User Prompt:
 {prompt}
@@
 """
     )
-    return result
+    return result.content or ""
Agno Agent.run returns RunOutput content attribute official documentation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/document-analyzer/document_analyzer.py` around lines 183 - 194,
agent.run() returns a RunOutput object, not the string response; currently the
code assigns that to result and returns it, causing inconsistency with string
error paths—change the return to return the text payload by returning
result.content (i.e., replace the final "return result" with "return
result.content") and optionally guard if result.content is None by converting to
an empty string or raising a clear error; locate the call to agent.run and the
variable result to make this change.

76-83: ⚠️ Potential issue | 🟠 Major

Don’t append failed or empty extraction results.

extract_text_from_docx() still turns failures into "", and Line 160 appends the result without validating content. A corrupt/textless file can still be analyzed as a valid empty document.

Suggested fix
 def extract_text_from_docx(file_bytes):
     """Extract text from docx bytes"""
-    try:
-        doc = Document(io.BytesIO(file_bytes))
-        return "\n".join([p.text for p in doc.paragraphs])
-    except Exception as e:
-        logger.error(f"Error extracting DOCX text: {e}")
-        return ""
+    doc = Document(io.BytesIO(file_bytes))
+    return "\n".join(p.text for p in doc.paragraphs)
@@
                     doc_text = extract_document_text(file_bytes, mime_type)
+                    if not doc_text.strip():
+                        raise ValueError(
+                            f"No extractable text found for file type: {mime_type}"
+                        )
                     extracted_docs.append(doc_text)

Also applies to: 159-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/document-analyzer/document_analyzer.py` around lines 76 - 83,
extract_text_from_docx currently returns an empty string on error which allows
callers to append empty/failed extractions as valid documents; change
extract_text_from_docx to return None (or raise a specific exception) when
extraction fails or yields no text, and update the caller that appends the
result (the code that does documents.append(extract_text_from_docx(...)) or
similar) to check the return value and only append when it's truthy/non-None and
non-empty; ensure logging still records errors in extract_text_from_docx but do
not propagate empty strings upstream.
examples/pdf_research_agent/pdf_research_agent.py (5)

20-20: ⚠️ Potential issue | 🟠 Major

Make the PDF sandbox match the advertised path usage.

ALLOWED_BASE_DIR is still fixed to the example directory, but both examples advertise /path/to/paper.pdf, which will be rejected unless that path is under this script directory. Make the sandbox configurable or update the examples to point inside the allowed directory.

Suggested fix
-ALLOWED_BASE_DIR = Path(__file__).parent.resolve()
+ALLOWED_BASE_DIR = Path(
+    os.getenv("PDF_RESEARCH_ALLOWED_DIR", Path(__file__).parent)
+).expanduser().resolve()
-    print('🔧 Example: {"role": "user", "content": "/path/to/paper.pdf"}')
+    sample_pdf = ALLOWED_BASE_DIR / "paper.pdf"
+    print(f'🔧 Example: {{"role": "user", "content": "{sample_pdf}"}}')

Also applies to: 38-38, 215-215

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` at line 20, The examples
use "/path/to/paper.pdf" but ALLOWED_BASE_DIR is hardcoded to the script's
example directory so those example inputs get rejected; update the code so the
sandbox matches the advertised usage by either (A) making ALLOWED_BASE_DIR
configurable (e.g., read from env var or a CLI arg used by the PDF
sandbox/validator) and ensure the PDF access check uses that configurable
ALLOWED_BASE_DIR, or (B) change the example inputs in the example calls to point
to files under the existing ALLOWED_BASE_DIR; look for the ALLOWED_BASE_DIR
variable and the PDF access/validation logic that enforces it and adjust those
spots and all example occurrences of "/path/to/paper.pdf" (lines around the
example usages) accordingly.

200-204: ⚠️ Potential issue | 🟡 Minor

Avoid echoing unexpected exception details to clients.

The broad handler still returns raw exception text, which can expose local paths or provider internals. Log the exception server-side and return a generic error.

Suggested fix
+import logging
 import os
-    except Exception as e:
+    except Exception:  # noqa: BLE001 - boundary handler converts failures to structured responses
+        logging.exception("Unexpected error processing PDF research request")
         return {
             "success": False,
             "data": None,
-            "error": f"Error processing request: {e}",
+            "error": "Error processing request.",
         }

Verify with:

#!/bin/bash
# Description: Verify the Ruff BLE001 finding in the PDF research example.
ruff check --select BLE001 examples/pdf_research_agent/pdf_research_agent.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 200 - 204,
The except Exception as e block returns raw exception text to clients; instead,
log the full exception server-side (e.g., use logger.exception or
processLogger.error with the exception/stack) inside that except handler and
change the returned payload from including f"{e}" to a generic message like
"Internal server error" or "An unexpected error occurred" while preserving
success=False and data=None; update the handler around the except Exception as e
to perform secure logging and return the non-sensitive generic error string.

183-185: ⚠️ Potential issue | 🟠 Major

Remove the stale string-error short-circuit.

_read_content() now raises DocumentReadError, so this branch can incorrectly return valid text starting with [ or Error without summarizing it, and it bypasses the structured response format.

Suggested fix
-        # Check if document processing failed
-        if document_text.startswith("[") or document_text.startswith("Error"):
-            return document_text
-
         # Limit document size to prevent token overflow
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 183 - 185,
The branch that returns document_text when it startswith "[" or "Error" is stale
because _read_content() now raises DocumentReadError; remove this short-circuit
so valid document text that begins with those prefixes isn't prematurely
returned and instead flows through the normal summarization/structured response
path—locate the conditional that checks document_text.startswith("[") or
document_text.startswith("Error") (around the call site of _read_content() and
variable document_text) and delete it, relying on DocumentReadError exceptions
for error handling and the existing structured-response logic to handle
successful text.

83-92: ⚠️ Potential issue | 🟡 Minor

Fix the Ruff exception-handling findings.

Line 83 should capture and chain the ImportError; Line 91 should use an f-string conversion flag instead of str(e).

Suggested fix
-        except ImportError:
+        except ImportError as exc:
             raise DocumentReadError(
                 f"pypdf not installed and cannot read '{resolved_path}'. Run: uv add pypdf"
-            )
+            ) from exc
         except Exception as e:
             if isinstance(e, DocumentReadError):
                 raise
             raise DocumentReadError(
-                f"Error reading PDF '{resolved_path}': {str(e)}"
+                f"Error reading PDF '{resolved_path}': {e!s}"
             ) from e

Verify with:

#!/bin/bash
# Description: Verify Ruff B904/RUF010 findings in the PDF research example.
ruff check --select B904,RUF010 examples/pdf_research_agent/pdf_research_agent.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 83 - 92,
Catch and chain the ImportError and use an f-string conversion flag for the
generic exception: in the except ImportError block (around the PDF open code)
change it to "except ImportError as e:" and re-raise DocumentReadError with the
original ImportError chained via "from e"; in the generic except block keep the
early "if isinstance(e, DocumentReadError): raise" check, and when re-raising
use an f-string conversion flag (e.g. f"{e!s}") instead of str(e) and retain the
"from e" chaining so both DocumentReadError and ImportError are properly chained
and reported (referencing DocumentReadError and resolved_path).

193-198: ⚠️ Potential issue | 🟠 Major

Unwrap the agent result before returning it.

result is still returned raw in data; if agent.run() returns a response object, the service response can fail JSON serialization.

Suggested fix
         # Build a prompt that includes the full document text
         prompt = f"Summarize the following document and highlight the key insights:\n\n{document_text}"
-        enriched_messages = [{"role": "user", "content": prompt}]
 
-        result = agent.run(input=enriched_messages)
-        return {"success": True, "data": result, "error": None}
+        result = agent.run(input=prompt)
+        if hasattr(result, "content"):
+            data = result.content
+        elif hasattr(result, "response"):
+            data = result.response
+        else:
+            data = str(result)
+        return {"success": True, "data": data, "error": None}

Verify existing response-unwrapping patterns with:

#!/bin/bash
# Description: Inspect agent.run result handling patterns without executing repository code.
rg -n -C3 'agent\.run\(input=|hasattr\(result,\s*["'"'"'](content|response)["'"'"']' --iglob '*.py'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 193 - 198,
The code returns the raw agent.run() output in result which may be a
non-serializable object; unwrap and normalize it before returning by inspecting
the agent.run() result (e.g., check for attributes like .content or .response or
if it's already a dict/list) and set data to that primitive (string/dict/list)
instead of the raw object; update the block that calls
agent.run(input=enriched_messages) to extract the actual text/content (fallback
to str(result) if necessary) and return that normalized value in the {"success":
True, "data": ... , "error": None} response so JSON serialization will succeed.
examples/voice-agent/.env.example (1)

19-19: ⚠️ Potential issue | 🟡 Minor

Remove the inert Piper TTS model setting.

With VOICE__TTS_PROVIDER=piper, this value is misleading because Piper selection is driven by the voice id/sample rate, not VOICE__TTS_MODEL.

Proposed fix
-VOICE__TTS_MODEL=piper-local
+# VOICE__TTS_MODEL is only needed for providers that consume model ids.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/.env.example` at line 19, Remove the inert
VOICE__TTS_MODEL setting from the .env.example file because Piper selection is
determined by VOICE__TTS_PROVIDER (set to "piper") and by the voice id/sample
rate, not VOICE__TTS_MODEL; locate and delete the VOICE__TTS_MODEL=piper-local
line so the example does not mislead users about configuration.
🧹 Nitpick comments (1)
docs/VOICE_AGENT_PLAN.md (1)

26-26: Minor: add language identifiers to fenced code blocks.

markdownlint (MD040) flags blocks at lines 26, 417, 444, 471, 579, 594, 661, 702, 883 as missing a language. For ASCII-art/diagram blocks, use ```text to satisfy the linter without changing rendering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/VOICE_AGENT_PLAN.md` at line 26, The markdown file has fenced code
blocks missing language identifiers (markdownlint MD040). Edit
VOICE_AGENT_PLAN.md and update the affected triple-backtick blocks (e.g., the
blocks at lines noted in the review) to include an appropriate language tag —
use ```text for ASCII-art/diagram blocks and the relevant language (```bash,
```json, ```js, etc.) for real code snippets so the linter stops flagging them;
ensure you only add the language after the opening ``` and do not change the
block contents.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/voice-agent/.env.example`:
- Around line 6-10: The example .env currently populates optional
fallback/provider variables (e.g., VOICE__TTS_FALLBACK_PROVIDER,
VOICE__AZURE_TTS_API_KEY, VOICE__AZURE_TTS_REGION, VOICE__AZURE_TTS_VOICE and
the OpenRouter placeholders referenced around lines 31-35) with non-empty
placeholders; change these to empty values or comment them out so copying .env
does not enable integrations with invalid credentials—leave provider unset
(empty) and clear API key/region/voice placeholders to force explicit opt-in by
users.

In `@examples/voice-agent/main.py`:
- Around line 233-243: The example currently hard-codes the "voice" dict in
main.py so changes to the documented environment knobs (VOICE__TTS_PROVIDER,
VOICE__TTS_VOICE_ID, VOICE__VAD_ENABLED, VOICE__SAMPLE_RATE,
VOICE__STT_PROVIDER, VOICE__STT_MODEL, VOICE__STT_LANGUAGE, etc.) have no
effect; replace the static "voice" dict creation with values read from the
environment (e.g., via os.getenv or the project's existing config loader) and
fall back to the current literals as defaults so the keys used elsewhere (the
"voice" dict referenced by the voice setup code) honor the VOICE__* env vars
documented in .env.example.
- Around line 37-44: The environment parsing for OPENROUTER_MEMORY_TURNS and
VOICE_MAX_SENTENCES currently calls int(...) at import time and will crash if
the env value is non-numeric; change these to robustly parse with a safe
fallback: read the raw string via os.environ.get(...), attempt to convert to int
inside a try/except (or use str.isdigit check), and on failure use the intended
default (4 for OPENROUTER_MEMORY_TURNS, 2 for VOICE_MAX_SENTENCES) while also
enforcing the min bounds via max(...). Update the constants
OPENROUTER_MEMORY_TURNS and VOICE_MAX_SENTENCES in main.py so invalid values
(e.g., "abc") do not raise and instead fall back to the defaults.
- Around line 182-198: The stream exception handler can fall back and produce a
second answer even after partial output has been yielded; add a flag (e.g.,
yielded_any or had_partial) initialized false before the async-for loop and set
it true whenever you yield clamped/partial output (where built,
_append_stream_piece and _clamp_sentences are used with VOICE_MAX_SENTENCES),
then in the except Exception block avoid running the demo fallback (or
re-raising/returning) if yielded_any is true so you don't emit a second,
unrelated response for the same user turn.

In `@pyproject.toml`:
- Around line 63-64: The pyproject currently lists "agno>=2.5.2" and
"openai>=2.17.0" as mandatory dependencies; change them to optional extras
instead (e.g., under an [project.optional-dependencies] group like "agents" or
"observability") so they are not installed by default, and ensure any mentions
in bindu/observability/openinference.py remain dynamically imported only when
the extra is present; update pyproject.toml to remove those two packages from
the main dependencies and add them to the chosen extras group (named "agents" or
"observability") so installs without the extra do not pull agno/openai.

---

Duplicate comments:
In `@examples/document-analyzer/document_analyzer.py`:
- Line 199: Update the example metadata entry where the "author" field is set to
a personal email (the string "author": "vyomrohila@gmail.com") in
examples/document-analyzer/document_analyzer.py and replace it with a neutral
placeholder or project-owned alias (e.g., "author": "example@example.com" or
"author": "project@example.com") so the public example does not include a
personal email address.
- Around line 175-180: The current logic after calling
_collect_prompt_and_documents(prompt, extracted_docs, errors) only surfaces
errors when no documents were extracted, which hides partial failures; update
the handling around the variables prompt, extracted_docs, and errors so that if
extracted_docs is non-empty but errors is also non-empty you still surface those
errors (e.g., append or return a message listing failed files or include errors
in the response/log alongside successful results), ensuring partial extraction
failures are reported rather than silently ignored.
- Around line 183-194: agent.run() returns a RunOutput object, not the string
response; currently the code assigns that to result and returns it, causing
inconsistency with string error paths—change the return to return the text
payload by returning result.content (i.e., replace the final "return result"
with "return result.content") and optionally guard if result.content is None by
converting to an empty string or raising a clear error; locate the call to
agent.run and the variable result to make this change.
- Around line 76-83: extract_text_from_docx currently returns an empty string on
error which allows callers to append empty/failed extractions as valid
documents; change extract_text_from_docx to return None (or raise a specific
exception) when extraction fails or yields no text, and update the caller that
appends the result (the code that does
documents.append(extract_text_from_docx(...)) or similar) to check the return
value and only append when it's truthy/non-None and non-empty; ensure logging
still records errors in extract_text_from_docx but do not propagate empty
strings upstream.

In `@examples/pdf_research_agent/pdf_research_agent.py`:
- Line 20: The examples use "/path/to/paper.pdf" but ALLOWED_BASE_DIR is
hardcoded to the script's example directory so those example inputs get
rejected; update the code so the sandbox matches the advertised usage by either
(A) making ALLOWED_BASE_DIR configurable (e.g., read from env var or a CLI arg
used by the PDF sandbox/validator) and ensure the PDF access check uses that
configurable ALLOWED_BASE_DIR, or (B) change the example inputs in the example
calls to point to files under the existing ALLOWED_BASE_DIR; look for the
ALLOWED_BASE_DIR variable and the PDF access/validation logic that enforces it
and adjust those spots and all example occurrences of "/path/to/paper.pdf"
(lines around the example usages) accordingly.
- Around line 200-204: The except Exception as e block returns raw exception
text to clients; instead, log the full exception server-side (e.g., use
logger.exception or processLogger.error with the exception/stack) inside that
except handler and change the returned payload from including f"{e}" to a
generic message like "Internal server error" or "An unexpected error occurred"
while preserving success=False and data=None; update the handler around the
except Exception as e to perform secure logging and return the non-sensitive
generic error string.
- Around line 183-185: The branch that returns document_text when it startswith
"[" or "Error" is stale because _read_content() now raises DocumentReadError;
remove this short-circuit so valid document text that begins with those prefixes
isn't prematurely returned and instead flows through the normal
summarization/structured response path—locate the conditional that checks
document_text.startswith("[") or document_text.startswith("Error") (around the
call site of _read_content() and variable document_text) and delete it, relying
on DocumentReadError exceptions for error handling and the existing
structured-response logic to handle successful text.
- Around line 83-92: Catch and chain the ImportError and use an f-string
conversion flag for the generic exception: in the except ImportError block
(around the PDF open code) change it to "except ImportError as e:" and re-raise
DocumentReadError with the original ImportError chained via "from e"; in the
generic except block keep the early "if isinstance(e, DocumentReadError): raise"
check, and when re-raising use an f-string conversion flag (e.g. f"{e!s}")
instead of str(e) and retain the "from e" chaining so both DocumentReadError and
ImportError are properly chained and reported (referencing DocumentReadError and
resolved_path).
- Around line 193-198: The code returns the raw agent.run() output in result
which may be a non-serializable object; unwrap and normalize it before returning
by inspecting the agent.run() result (e.g., check for attributes like .content
or .response or if it's already a dict/list) and set data to that primitive
(string/dict/list) instead of the raw object; update the block that calls
agent.run(input=enriched_messages) to extract the actual text/content (fallback
to str(result) if necessary) and return that normalized value in the {"success":
True, "data": ... , "error": None} response so JSON serialization will succeed.

In `@examples/voice-agent/.env.example`:
- Line 19: Remove the inert VOICE__TTS_MODEL setting from the .env.example file
because Piper selection is determined by VOICE__TTS_PROVIDER (set to "piper")
and by the voice id/sample rate, not VOICE__TTS_MODEL; locate and delete the
VOICE__TTS_MODEL=piper-local line so the example does not mislead users about
configuration.

---

Nitpick comments:
In `@docs/VOICE_AGENT_PLAN.md`:
- Line 26: The markdown file has fenced code blocks missing language identifiers
(markdownlint MD040). Edit VOICE_AGENT_PLAN.md and update the affected
triple-backtick blocks (e.g., the blocks at lines noted in the review) to
include an appropriate language tag — use ```text for ASCII-art/diagram blocks
and the relevant language (```bash, ```json, ```js, etc.) for real code snippets
so the linter stops flagging them; ensure you only add the language after the
opening ``` and do not change the block contents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4572beca-8b53-4137-8ba4-32292c38a459

📥 Commits

Reviewing files that changed from the base of the PR and between 5bac2a9 and 79523ee.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • SPLIT_PLAN.md
  • docs/VOICE.md
  • docs/VOICE_AGENT_PLAN.md
  • examples/README.md
  • examples/cerina_bindu/cbt/README.md
  • examples/document-analyzer/document_analyzer.py
  • examples/multilingual-collab-agent/main.py
  • examples/multilingual-collab-agent/skills/collaborate/skill.yaml
  • examples/multilingual-collab-agent/skills/research/skill.yaml
  • examples/multilingual-collab-agent/skills/translate/skill.yaml
  • examples/pdf_research_agent/README.md
  • examples/pdf_research_agent/pdf_research_agent.py
  • examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml
  • examples/typescript-langchain-agent/quiz-agent.ts
  • examples/typescript-langchain-quiz-agent/quiz-agent.ts
  • examples/voice-agent/.env.example
  • examples/voice-agent/README.md
  • examples/voice-agent/main.py
  • examples/voice-agent/skills/voice-brief-response-skill/skill.yaml
  • pyproject.toml
✅ Files skipped from review due to trivial changes (10)
  • examples/README.md
  • examples/multilingual-collab-agent/skills/research/skill.yaml
  • examples/pdf_research_agent/README.md
  • examples/cerina_bindu/cbt/README.md
  • examples/multilingual-collab-agent/skills/collaborate/skill.yaml
  • SPLIT_PLAN.md
  • examples/voice-agent/README.md
  • docs/VOICE.md
  • examples/typescript-langchain-agent/quiz-agent.ts
  • examples/voice-agent/skills/voice-brief-response-skill/skill.yaml
🚧 Files skipped from review as they are similar to previous changes (4)
  • examples/multilingual-collab-agent/skills/translate/skill.yaml
  • examples/multilingual-collab-agent/main.py
  • examples/typescript-langchain-quiz-agent/quiz-agent.ts
  • examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml

Comment on lines +6 to +10
# Optional fallback TTS (recommended for resiliency)
VOICE__TTS_FALLBACK_PROVIDER=azure
VOICE__AZURE_TTS_API_KEY=your-azure-speech-key
VOICE__AZURE_TTS_REGION=eastus
VOICE__AZURE_TTS_VOICE=en-US-SaraNeural
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep optional provider credentials disabled by default.

Copying this file to .env leaves non-empty placeholder credentials for Azure fallback and OpenRouter. That can activate integrations with invalid credentials instead of cleanly using Piper/demo fallback.

Proposed fix
 # Optional fallback TTS (recommended for resiliency)
-VOICE__TTS_FALLBACK_PROVIDER=azure
-VOICE__AZURE_TTS_API_KEY=your-azure-speech-key
-VOICE__AZURE_TTS_REGION=eastus
-VOICE__AZURE_TTS_VOICE=en-US-SaraNeural
+# VOICE__TTS_FALLBACK_PROVIDER=azure
+# VOICE__AZURE_TTS_API_KEY=your-azure-speech-key
+# VOICE__AZURE_TTS_REGION=eastus
+# VOICE__AZURE_TTS_VOICE=en-US-SaraNeural
@@
 # Optional LLM backend for smarter responses
-OPENROUTER_API_KEY=your-openrouter-api-key
+# OPENROUTER_API_KEY=your-openrouter-api-key
 OPENROUTER_MODEL=openai/gpt-oss-120b:free
 OPENROUTER_MEMORY_TURNS=4
 VOICE_MAX_SENTENCES=2

Also applies to: 31-35

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 8-8: [UnorderedKey] The VOICE__AZURE_TTS_API_KEY key should go before the VOICE__TTS_FALLBACK_PROVIDER key

(UnorderedKey)


[warning] 9-9: [UnorderedKey] The VOICE__AZURE_TTS_REGION key should go before the VOICE__TTS_FALLBACK_PROVIDER key

(UnorderedKey)


[warning] 10-10: [UnorderedKey] The VOICE__AZURE_TTS_VOICE key should go before the VOICE__TTS_FALLBACK_PROVIDER key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/.env.example` around lines 6 - 10, The example .env
currently populates optional fallback/provider variables (e.g.,
VOICE__TTS_FALLBACK_PROVIDER, VOICE__AZURE_TTS_API_KEY, VOICE__AZURE_TTS_REGION,
VOICE__AZURE_TTS_VOICE and the OpenRouter placeholders referenced around lines
31-35) with non-empty placeholders; change these to empty values or comment them
out so copying .env does not enable integrations with invalid credentials—leave
provider unset (empty) and clear API key/region/voice placeholders to force
explicit opt-in by users.

Comment on lines +37 to +44
OPENROUTER_MEMORY_TURNS = max(
0,
int(os.environ.get("OPENROUTER_MEMORY_TURNS", "4") or "4"),
)
VOICE_MAX_SENTENCES = max(
1,
int(os.environ.get("VOICE_MAX_SENTENCES", "2") or "2"),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard numeric env parsing so a typo does not crash the example.

Line 39 and Line 43 call int(...) at import time, so OPENROUTER_MEMORY_TURNS=abc or VOICE_MAX_SENTENCES=abc prevents the example from starting.

Proposed fix
+def _read_int_env(name: str, default: int, minimum: int) -> int:
+    raw = os.environ.get(name, "").strip()
+    if not raw:
+        return default
+    try:
+        value = int(raw)
+    except ValueError:
+        print(f"Invalid {name}={raw!r}; using default {default}")
+        return default
+    return max(minimum, value)
+
+
 OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "").strip()
 OPENROUTER_MODEL = os.environ.get("OPENROUTER_MODEL", "openai/gpt-4o-mini").strip()
-OPENROUTER_MEMORY_TURNS = max(
-    0,
-    int(os.environ.get("OPENROUTER_MEMORY_TURNS", "4") or "4"),
-)
-VOICE_MAX_SENTENCES = max(
-    1,
-    int(os.environ.get("VOICE_MAX_SENTENCES", "2") or "2"),
-)
+OPENROUTER_MEMORY_TURNS = _read_int_env("OPENROUTER_MEMORY_TURNS", 4, 0)
+VOICE_MAX_SENTENCES = _read_int_env("VOICE_MAX_SENTENCES", 2, 1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/main.py` around lines 37 - 44, The environment parsing
for OPENROUTER_MEMORY_TURNS and VOICE_MAX_SENTENCES currently calls int(...) at
import time and will crash if the env value is non-numeric; change these to
robustly parse with a safe fallback: read the raw string via
os.environ.get(...), attempt to convert to int inside a try/except (or use
str.isdigit check), and on failure use the intended default (4 for
OPENROUTER_MEMORY_TURNS, 2 for VOICE_MAX_SENTENCES) while also enforcing the min
bounds via max(...). Update the constants OPENROUTER_MEMORY_TURNS and
VOICE_MAX_SENTENCES in main.py so invalid values (e.g., "abc") do not raise and
instead fall back to the defaults.

Comment on lines +182 to +198
built = ""
async for chunk in stream:
delta = chunk.choices[0].delta.content if chunk.choices else None
if not delta:
continue
built = _append_stream_piece(built, delta)
clamped, done = _clamp_sentences(built, VOICE_MAX_SENTENCES)
if clamped:
yield clamped
if done:
break

if not built.strip():
yield "I couldn't generate a response this time. Please try again."
return
except Exception as exc:
print(f"OpenRouter error: {exc}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Do not fall back after already yielding partial LLM output.

If the OpenRouter stream raises after Line 190 has yielded text, execution falls through to the demo fallback and produces a second, unrelated answer for the same user turn.

Proposed fix
+            emitted_any = False
             built = ""
             async for chunk in stream:
                 delta = chunk.choices[0].delta.content if chunk.choices else None
                 if not delta:
                     continue
                 built = _append_stream_piece(built, delta)
                 clamped, done = _clamp_sentences(built, VOICE_MAX_SENTENCES)
                 if clamped:
                     yield clamped
+                    emitted_any = True
                 if done:
                     break
 
             if not built.strip():
                 yield "I couldn't generate a response this time. Please try again."
             return
         except Exception as exc:
             print(f"OpenRouter error: {exc}")
+            if "emitted_any" in locals() and emitted_any:
+                return
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 197-197: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/main.py` around lines 182 - 198, The stream exception
handler can fall back and produce a second answer even after partial output has
been yielded; add a flag (e.g., yielded_any or had_partial) initialized false
before the async-for loop and set it true whenever you yield clamped/partial
output (where built, _append_stream_piece and _clamp_sentences are used with
VOICE_MAX_SENTENCES), then in the except Exception block avoid running the demo
fallback (or re-raising/returning) if yielded_any is true so you don't emit a
second, unrelated response for the same user turn.

Comment on lines +233 to +243
"voice": {
"stt_provider": "deepgram",
"stt_model": "nova-3",
"stt_language": "en",
"tts_provider": "piper",
"tts_voice_id": "en_US-ryan-high",
"tts_model": "piper-local",
"sample_rate": 16000,
"allow_interruptions": True,
"vad_enabled": False,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wire the voice config to the documented .env knobs.

The example hard-codes provider/model/audio settings here, while .env.example documents VOICE__TTS_PROVIDER, VOICE__TTS_VOICE_ID, VOICE__VAD_ENABLED, VOICE__SAMPLE_RATE, etc. If bindufy consumes this explicit config, edits to those env vars will not affect the example.

Proposed fix
     "voice": {
-        "stt_provider": "deepgram",
-        "stt_model": "nova-3",
-        "stt_language": "en",
-        "tts_provider": "piper",
-        "tts_voice_id": "en_US-ryan-high",
-        "tts_model": "piper-local",
-        "sample_rate": 16000,
-        "allow_interruptions": True,
-        "vad_enabled": False,
+        "stt_provider": os.environ.get("VOICE__STT_PROVIDER", "deepgram").strip() or "deepgram",
+        "stt_model": os.environ.get("VOICE__STT_MODEL", "nova-3").strip() or "nova-3",
+        "stt_language": os.environ.get("VOICE__STT_LANGUAGE", "en").strip() or "en",
+        "tts_provider": os.environ.get("VOICE__TTS_PROVIDER", "piper").strip() or "piper",
+        "tts_voice_id": os.environ.get("VOICE__TTS_VOICE_ID", "en_US-ryan-high").strip() or "en_US-ryan-high",
+        "tts_model": os.environ.get("VOICE__TTS_MODEL", "").strip() or None,
+        "sample_rate": _read_int_env("VOICE__SAMPLE_RATE", 16000, 1),
+        "allow_interruptions": os.environ.get("VOICE__ALLOW_INTERRUPTIONS", "true").lower() == "true",
+        "vad_enabled": os.environ.get("VOICE__VAD_ENABLED", "false").lower() == "true",
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/voice-agent/main.py` around lines 233 - 243, The example currently
hard-codes the "voice" dict in main.py so changes to the documented environment
knobs (VOICE__TTS_PROVIDER, VOICE__TTS_VOICE_ID, VOICE__VAD_ENABLED,
VOICE__SAMPLE_RATE, VOICE__STT_PROVIDER, VOICE__STT_MODEL, VOICE__STT_LANGUAGE,
etc.) have no effect; replace the static "voice" dict creation with values read
from the environment (e.g., via os.getenv or the project's existing config
loader) and fall back to the current literals as defaults so the keys used
elsewhere (the "voice" dict referenced by the voice setup code) honor the
VOICE__* env vars documented in .env.example.

Comment thread pyproject.toml
Comment on lines +63 to +64
"agno>=2.5.2",
"openai>=2.17.0",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether agno/openai are imported unconditionally at package import time
rg -nP '^\s*(from|import)\s+(agno|openai)\b' --type=py -g '!tests/**' -g '!examples/**'

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

# Check the full dependency structure in pyproject.toml around those lines
cat -n pyproject.toml | head -100

Repository: GetBindu/Bindu

Length of output: 3362


🏁 Script executed:

# Search for conditional imports or try/except patterns with agno/openai
rg -nA5 -B5 'try:|except' --type=py -g '!tests/**' -g '!examples/**' | rg -A5 -B5 'agno|openai'

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

# Check the agents extra definition in pyproject.toml
rg -nA20 '\[project.optional-dependencies\]|\[project.extras\]|agents' pyproject.toml

Repository: GetBindu/Bindu

Length of output: 757


🏁 Script executed:

# Search for any references to agno or openai in Python files
rg 'agno|openai' --type=py -g '!tests/**' -g '!examples/**' -n

Repository: GetBindu/Bindu

Length of output: 828


🏁 Script executed:

# Also check if they appear in __init__.py or entry points
fd '(__init__|setup|__main__).py' --type=py | head -20

Repository: GetBindu/Bindu

Length of output: 227


🏁 Script executed:

# Check if agno/openai are imported in settings.py and openinference.py
rg -n 'import' bindu/settings.py | head -30

Repository: GetBindu/Bindu

Length of output: 269


🏁 Script executed:

# Check openinference.py for imports
cat -n bindu/observability/openinference.py | head -80

Repository: GetBindu/Bindu

Length of output: 3997


🏁 Script executed:

# Check the main package __init__.py
cat -n bindu/__init__.py

Repository: GetBindu/Bindu

Length of output: 475


Move agno and openai back to optional dependencies.

agno>=2.5.2 and openai>=2.17.0 are now mandatory runtime dependencies (lines 63–64), forcing every pip install bindu installation to pull both packages regardless of use case. However, they are only referenced in bindu/observability/openinference.py as optional instrumentation providers in a dynamically-detected frameworks list—they are never imported unconditionally at package init. This adds unnecessary bloat and licensing surface area for users who only need the core protocol framework without observability/LLM support. Keep them optional (under the agents extra or a dedicated observability extra) unless they're actually imported at import time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` around lines 63 - 64, The pyproject currently lists
"agno>=2.5.2" and "openai>=2.17.0" as mandatory dependencies; change them to
optional extras instead (e.g., under an [project.optional-dependencies] group
like "agents" or "observability") so they are not installed by default, and
ensure any mentions in bindu/observability/openinference.py remain dynamically
imported only when the extra is present; update pyproject.toml to remove those
two packages from the main dependencies and add them to the chosen extras group
(named "agents" or "observability") so installs without the extra do not pull
agno/openai.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants