Skip to content

Conversation

@badri-singhal
Copy link
Contributor

@badri-singhal badri-singhal commented Dec 9, 2025

Summary by CodeRabbit

  • New Features

    • Added a WebSocket-based voice agent for automated order confirmation conversations.
    • Introduced template-based workflow system for dynamic conversation flows.
    • New API endpoints for creating templates and enqueueing leads.
    • Added call control handlers for muting/unmuting audio, playing sounds, and managing conversation state.
  • Improvements

    • Reorganized utility functions for better modularity.
    • Enhanced database schema to support template-based configurations.
    • Improved error handling and logging throughout the system.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 9, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR introduces a template-based workflow system for the Breeze Buddy voice agent, replacing the workflow-enum architecture. A new OrderConfirmationBot agent class orchestrates WebSocket-driven order-confirmation conversations with dynamic flow configuration, LLM/STT/TTS integration, and a pluggable hook system. Supporting infrastructure includes database schema migrations, template accessors, action handlers, and refactored utilities.

Changes

Cohort / File(s) Summary
Agent Implementation
app/ai/voice/agents/breeze_buddy/agent.py
New OrderConfirmationBot class handling WebSocket-based order-confirmation with lead lookup, template loading, VAD/STT/TTS setup, LLM context aggregation, optional Langfuse tracing, flow initialization, and disposal on disconnect/timeout.
WebSocket Bot Refactoring
app/ai/voice/agents/breeze_buddy/websocket_bot.py
Updated imports (OrderData, utilities moved to utils.common), system prompt enhanced with multilingual rule, and _finalize_call refactored to consolidate transcription/updated_address/cancellation_reason into meta_data structure.
Template System Core
app/ai/voice/agents/breeze_buddy/template/__init__.py, builder.py, context.py, hooks.py, loader.py, transition.py, types.py
New template package enabling dynamic workflow configuration: FlowConfigBuilder converts templates to Pipecat-compatible configs; FlowConfigLoader renders templates with runtime variables; TemplateContext wraps bot state; hook system (Hook, HookRegistry, UpdateOutcomeInDatabaseHook) enables post-function side effects; transition_handler orchestrates node transitions.
Action Handlers
app/ai/voice/agents/breeze_buddy/handlers/internal/__init__.py, action_handlers.py
New handlers module exporting mute_stt, unmute_stt, play_audio_sound, end_conversation, and service_callback functions for conversation control and webhook reporting.
Database Migrations
app/database/migrations/001_initial_tables.sql, 002_add_shop_identifier_and_locks.sql, 003_create_template_table.sql
Initial schema setup (outbound_number, call_execution_config, lead_call_tracker), added shop_identifier/is_locked columns, and new template table with merchant/shop_identifier/flow/expected_payload_schema; renamed workflow column to template.
Database Accessors
app/database/accessor/breeze_buddy/template.py, lead_call_tracker.py, call_execution_config.py
New template accessor (get_template_by_merchant, create_template); updated lead_call_tracker to accept template string and support partial updates; updated call_execution_config to use template instead of workflow.
Database Decoders
app/database/decoder/breeze_buddy/template.py, lead_call_tracker.py, call_execution_config.py
New template decoder (decode_template, decode_templates); updated lead_call_tracker/call_execution_config decoders to map template field instead of workflow.
Database Queries
app/database/queries/breeze_buddy/template.py, lead_call_tracker.py, call_execution_config.py
New template queries (get_template_by_merchant_query, create_template_query); updated lead_call_tracker query to support partial updates; updated call_execution_config queries to use template parameter.
Types & Models
app/ai/voice/agents/breeze_buddy/types/models.py
Replaced BreezeOrderData with PushLeadRequest (payload, template, merchant, identifier, reporting_webhook_url) and LeadData (customer_mobile_number, shop_identifier, shop_name).
Utilities
app/ai/voice/agents/breeze_buddy/utils/common.py
New utility module with indian_number_to_speech, send_webhook_with_retry, load_audio, validate_payload, and OUTCOME_TO_ENUM mapping; removes previous utils.py from workflows/order_confirmation/.
API Router
app/api/routers/breeze_buddy.py
Refactored endpoints from workflow-based to template-based: replaced trigger_order_confirmation signature, added POST /template and POST /push/lead/v2 endpoints, updated dashboard paths, integrated LeadData and CreateTemplateRequest types.
Schema Changes
app/schemas.py
Removed Workflow enum; updated LeadCallTracker, CreateCallExecutionConfigRequest, UpdateCallExecutionConfigRequest, and CallExecutionConfig to use template: str instead of workflow: Workflow.
Database Utilities
app/database/accessor/__init__.py
Exported create_template and get_template_by_merchant from breeze_buddy.template accessor.
Configuration & Examples
app/ai/voice/agents/breeze_buddy/examples/templates/order-confirmation.json, dashboard/login.html
New example template with multi-node flow (initial, verify_order_details, update_address, order_confirmation_and_end, etc.); updated login redirect from /agent/voice/breeze-buddy/breeze/order-confirmation/dashboard to /agent/voice/breeze-buddy/dashboard.
Removed Files
scripts/create_tables.py, app/ai/voice/agents/breeze_buddy/workflows/order_confirmation/utils.py
Deleted table creation script and deprecated utils module (moved functionality to utils/common.py).

Sequence Diagram

sequenceDiagram
    actor Client
    participant WS as WebSocket
    participant Agent as OrderConfirmationBot
    participant TplLoader as FlowConfigLoader
    participant TplBuilder as FlowConfigBuilder
    participant Pipeline as PipelineRunner
    participant LLM as Azure LLM
    participant Hooks as HookRegistry
    participant DB as Database

    Client->>WS: Connect & Send Lead Data
    WS->>Agent: New Connection
    Agent->>Agent: Parse Lead Data
    Agent->>DB: Get Lead by call_id
    DB-->>Agent: Lead Payload
    Agent->>TplLoader: Load Template
    TplLoader->>DB: Get Template by Merchant
    DB-->>TplLoader: TemplateModel
    TplLoader->>TplLoader: Render Messages
    TplLoader-->>Agent: Rendered Template
    Agent->>TplBuilder: Build Flow Config
    TplBuilder->>TplBuilder: Map Handlers & Hooks
    TplBuilder-->>Agent: Pipecat Flow
    Agent->>Pipeline: Initialize with Flow
    Pipeline->>Pipeline: Assemble Components<br/>(VAD, STT, TTS, LLM)
    Pipeline-->>Agent: Ready
    Client->>WS: Audio Stream
    WS->>Pipeline: Audio Frames
    Pipeline->>LLM: Process & Generate Response
    LLM-->>Pipeline: LLM Output
    Pipeline->>Client: Audio Response
    LLM->>Hooks: Trigger Hook (transition/outcome)
    Hooks->>DB: Update Lead Outcome
    DB-->>Hooks: Updated
    Client->>WS: Disconnect
    Agent->>Agent: Cleanup & Finalize
    Agent->>DB: Update Call Completion
    Agent->>Client: Hangup
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • OrderConfirmationBot.__init__ & run() (agent.py): Dense initialization with VAD, STT, TTS, LLM context setup, WebSocket handshake, and pipeline assembly; verify component interactions and error handling paths.
  • FlowConfigBuilder.build_flow_config() (template/builder.py): Complex conversion logic mapping template definitions to Pipecat-compatible flow configs; verify handler/hook registration and function schema construction.
  • Hook System (UpdateOutcomeInDatabaseHook) (template/hooks.py): Custom hook execution with database persistence; validate data extraction from expected_fields and error handling.
  • Template Database Migration (migrations/003_create_template_table.sql): Schema migration with workflow-to-template column rename; verify backward compatibility and index creation.
  • Partial Update Logic (database/queries/lead_call_tracker.py, accessor/lead_call_tracker.py): Dynamic SQL generation for optional fields; verify generated SQL correctness and None-field handling.
  • Utility Consolidation (utils/common.py): Webhook retry logic, audio loading, and payload validation; verify HMAC signing, retry semantics, and schema validation completeness.
  • API Router Refactoring (app/api/routers/breeze_buddy.py): Significant endpoint restructuring from workflow to template paradigm; verify merchant/template lookups and error handling.

Possibly Related PRs

Suggested Reviewers

  • murdore

Poem

🐰 Templates bloom where workflows once stood tall,
Hooks catch outcomes like carrots in fall,
WebSockets whisper through nodes so grand,
Flow builders craft what bots understand,
Breeze Buddy hops forth, dynamic and spry! 🌬️

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'Template Based Architechture' is relevant to the main changes but contains a spelling error and is somewhat vague about the specific architectural transformation being implemented. Consider revising the title to be more specific about the refactoring scope, e.g., 'Refactor to template-based architecture from workflow-based system' or 'Migrate from workflow-based to template-driven configuration', and correct the spelling of 'Architecture'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.

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

❤️ Share

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

Copy link

@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: 9

🧹 Nitpick comments (20)
app/database/queries/breeze_buddy/template.py (1)

11-25: Use explicit optional types for optional parameters

To satisfy Ruff (RUF013) and keep typing consistent with other parts of the codebase that use T | None, consider updating the signature to:

def get_template_by_merchant_query(
    merchant_id: str,
    shop_identifier: str | None = None,
    name: str | None = None,
) -> Tuple[str, List[Any]]:

This makes the Optional nature explicit and avoids the implicit str = None pattern.

app/ai/voice/agents/breeze_buddy/workflow/types.py (1)

29-35: Avoid mutable defaults in Pydantic models

Several fields use mutable literals as defaults ({} / []):

  • FlowFunction.properties, FlowFunction.required, FlowFunction.hooks
  • FlowNodeModel.pre_actions, FlowNodeModel.post_actions, FlowNodeModel.functions
  • RequestFlowFunction.properties, RequestFlowFunction.required, RequestFlowFunction.hooks
  • RequestFlowNode.task_messages, RequestFlowNode.pre_actions, RequestFlowNode.post_actions, RequestFlowNode.functions

Even though Pydantic often wraps these, the more robust pattern is:

from pydantic import BaseModel, Field

class FlowFunction(BaseModel):
    name: str
    description: str
    properties: Dict[str, Any] = Field(default_factory=dict)
    required: List[str] = Field(default_factory=list)
    transition_to: Optional[str] = None
    hooks: List[str] = Field(default_factory=list)

…and similarly for the other models. This avoids any ambiguity about shared mutable defaults and keeps things future-proof across Pydantic versions.

Also applies to: 38-43, 56-63, 66-71

app/database/accessor/__init__.py (1)

43-72: Consider alphabetically sorting __all__ for maintainability.

The new exports create_template and get_template_by_merchant are placed at the beginning of __all__, but the rest of the list appears to follow a grouping pattern by module. For consistency and easier navigation, consider sorting __all__ alphabetically.

app/ai/voice/agents/breeze_buddy/workflow/__init__.py (1)

27-36: Consider alphabetically sorting __all__ for consistency.

The exports in __all__ are grouped logically but not alphabetically sorted. For easier maintenance and consistency with Python conventions, consider sorting them alphabetically.

app/ai/voice/agents/breeze_buddy/handlers/internal/__init__.py (2)

1-5: Update docstring to reflect generic handler scope.

The docstring mentions "order confirmation workflow" specifically, but this module appears to provide internal handlers that could be used across different workflows. Consider updating it to reflect the broader scope.

Apply this diff:

 """
-Handler functions for order confirmation workflow.
+Internal handler functions for Breeze Buddy workflows.
 
-This package contains all handler functions organized by category.
+This package contains internal action handlers used across workflows.
 """

14-19: Consider alphabetically sorting __all__.

For consistency and maintainability, consider sorting the exports alphabetically.

app/database/migrations/003_create_template_table.sql (1)

11-12: Consider adding a trigger to auto-update updated_at.

The updated_at column defaults to NOW() on insert but won't automatically update when rows are modified. If you want updated_at to reflect the last modification time, add an update trigger.

-- Add after table creation
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_template_updated_at
    BEFORE UPDATE ON template
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();
app/ai/voice/agents/breeze_buddy/websocket_bot.py (1)

636-643: The meta_data structure consolidates payload nicely.

The refactored meta_data dict cleanly groups related fields. Note that call_sid appears both in meta_data["transcription"] (line 639) and as a separate call_id parameter (line 645). If the downstream completion_function only needs it once, consider removing the redundancy.

app/database/accessor/breeze_buddy/template.py (3)

29-31: Use explicit Optional or | None for nullable parameters.

Per PEP 484, implicit Optional (defaulting to None without type annotation) is discouraged. Use explicit type hints for clarity.

 async def get_template_by_merchant(
-    merchant_id: str, shop_identifier: str = None, name: str = None
+    merchant_id: str, shop_identifier: Optional[str] = None, name: Optional[str] = None
 ) -> Optional[TemplateModel]:

55-63: Add type hint for now parameter.

The now parameter lacks a type hint. Based on usage with created_at/updated_at timestamp columns, this should be typed as datetime.

+from datetime import datetime
+
 async def create_template(
     template_id: str,
     merchant_id: str,
-    shop_identifier: str,
+    shop_identifier: Optional[str],
     name: str,
     flow: dict,
     is_active: bool,
-    now,
+    now: datetime,
 ) -> Optional[TemplateModel]:

50-52: Consider logging the exception type for better debugging.

The broad Exception catch is acceptable here since you're logging and returning None, but logging the exception class name could help with debugging.

     except Exception as e:
-        logger.error(f"Error getting template by merchant: {e}")
+        logger.error(f"Error getting template by merchant: {type(e).__name__}: {e}")
         return None

Also applies to: 91-93

app/ai/voice/agents/breeze_buddy/workflow/transition.py (1)

53-55: Store the task reference to prevent garbage collection and enable error tracking.

The asyncio.create_task return value should be stored. Without a reference, the task may be garbage collected before completion, and any unhandled exceptions will be silently lost.

+    # Store task reference to prevent garbage collection
     if hooks:
         logger.info(
             f"Scheduling {len(hooks)} hook(s) to execute asynchronously for function '{function_name}'"
         )
-        asyncio.create_task(
+        task = asyncio.create_task(
             _execute_hooks_async(context, args, hooks, function_name or "unknown")
         )
+        # Optionally add error callback for logging
+        task.add_done_callback(
+            lambda t: logger.error(f"Hook task failed: {t.exception()}") if t.exception() else None
+        )
app/ai/voice/agents/breeze_buddy/workflow/builder.py (1)

82-93: Potential unintended mutation of input data.

node_data.copy() creates a shallow copy, so nested objects like the functions list and its dicts are shared with the original. Modifying func["name"] mutates the original nodes_data input. Consider using copy.deepcopy or restructuring to avoid side effects.

+import copy
+
         # Convert nodes data to FlowNodeModel objects
         flow_nodes = []
         for node_data in nodes_data:
             # Transform function_name to name for compatibility
-            transformed_node_data = node_data.copy()
+            transformed_node_data = copy.deepcopy(node_data)
             if "functions" in transformed_node_data:
app/ai/voice/agents/breeze_buddy/workflow/context.py (1)

168-168: Potential type mismatch if args[0] is not a dict.

If args[0] exists but is not a dict-like object, the handler might receive unexpected data. Consider adding validation or documentation about expected argument types.

-            llm_args = args[0] if len(args) > 0 else {}
+            llm_args = args[0] if len(args) > 0 and isinstance(args[0], dict) else {}
app/ai/voice/agents/breeze_buddy/workflow/loader.py (1)

57-85: Consider recursive placeholder substitution risk.

If a variable value contains a placeholder pattern like {other_key}, it won't be substituted (which is likely intended). However, if a variable value itself contains {key} where key is another variable, this could lead to unexpected partially-substituted content. Document this behavior or consider whether single-pass substitution is the desired behavior.

app/api/routers/breeze_buddy.py (1)

118-120: Broad exception catch returns 400 for all errors.

The catch-all Exception handler returns 400 (Bad Request) even for server-side errors. ValueError from validation should be 400, but database failures should be 500.

-    except Exception as e:
-        logger.error(f"Error creating workflow from JSON: {e}", exc_info=True)
-        return JSONResponse(status_code=400, content={"detail": str(e)})
+    except ValueError as e:
+        logger.warning(f"Validation error creating workflow: {e}")
+        return JSONResponse(status_code=400, content={"detail": str(e)})
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"Error creating workflow from JSON: {e}", exc_info=True)
+        return JSONResponse(status_code=500, content={"detail": "Internal server error"})
app/ai/voice/agents/breeze_buddy/workflow/hooks.py (2)

132-140: All non-outcome args added to metadata without filtering.

Lines 135-140 add all arguments (except "outcome") to the lead's metadata. This could inadvertently persist sensitive data or LLM-generated content. Consider an allowlist of expected metadata keys.

+            # Only persist expected metadata keys
+            ALLOWED_METADATA_KEYS = {"cancellation_reason", "updated_address", "notes"}
+
             for key, value in args.items():
-                if key != "outcome" and value is not None:
+                if key in ALLOWED_METADATA_KEYS and value is not None:
                     meta_data[key] = value

200-200: Annotate mutable class attribute with ClassVar.

The _hooks dict is a class-level mutable attribute shared across all instances. Per PEP 526 and static analysis, annotate with ClassVar for clarity.

+from typing import Any, ClassVar, Dict, Optional
-from typing import Any, Dict, Optional
 
 class HookRegistry:
     ...
-    _hooks: Dict[str, Hook] = {}
+    _hooks: ClassVar[Dict[str, Hook]] = {}
app/ai/voice/agents/breeze_buddy/agent.py (2)

127-134: Consider catching more specific exceptions for WebSocket close operations.

The broad Exception catches (lines 130, 164, 196, 215) work for graceful degradation but could mask unexpected errors. For WebSocket operations, consider catching WebSocketDisconnect or RuntimeError specifically.

             except json.JSONDecodeError as e:
                 logger.error(f"Failed to parse call data JSON: {e}")
                 try:
                     if self.ws.client_state.name != "DISCONNECTED":
                         await self.ws.close(code=4000, reason="Invalid JSON data")
-                except Exception as close_error:
+                except (RuntimeError, Exception) as close_error:
                     logger.warning(
                         f"Could not close websocket (likely already closed): {close_error}"
                     )

Note: The static analysis tool flagged BLE001 for catching blind exceptions. The current approach is pragmatic for handling already-closed connections, so this is optional to address.


239-243: Handler wrapping approach works but consider alternative patterns.

Iterating over handler_map.items() and modifying values is safe in Python (the view is taken before modification). However, if the map grows, consider wrapping handlers lazily at invocation time rather than eagerly at initialization.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dab73d6 and 0ce7896.

📒 Files selected for processing (28)
  • app/ai/voice/agents/breeze_buddy/agent.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/dashboard/login.html (1 hunks)
  • app/ai/voice/agents/breeze_buddy/handlers/internal/__init__.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/managers/calls.py (4 hunks)
  • app/ai/voice/agents/breeze_buddy/sample/order-confirmation-workflow.json (1 hunks)
  • app/ai/voice/agents/breeze_buddy/services/telephony/exotel/exotel.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/services/telephony/twilio/twilio.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/types/models.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/websocket_bot.py (3 hunks)
  • app/ai/voice/agents/breeze_buddy/workflow/__init__.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/workflow/builder.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/workflow/context.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/workflow/hooks.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/workflow/loader.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/workflow/transition.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/workflow/types.py (1 hunks)
  • app/api/routers/breeze_buddy.py (5 hunks)
  • app/database/accessor/__init__.py (1 hunks)
  • app/database/accessor/breeze_buddy/lead_call_tracker.py (1 hunks)
  • app/database/accessor/breeze_buddy/template.py (1 hunks)
  • app/database/decoder/breeze_buddy/template.py (1 hunks)
  • app/database/migrations/001_initial_tables.sql (1 hunks)
  • app/database/migrations/002_add_shop_identifier_and_locks.sql (1 hunks)
  • app/database/migrations/003_create_template_table.sql (1 hunks)
  • app/database/queries/breeze_buddy/lead_call_tracker.py (1 hunks)
  • app/database/queries/breeze_buddy/template.py (1 hunks)
  • scripts/create_tables.py (0 hunks)
💤 Files with no reviewable changes (1)
  • scripts/create_tables.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-15T04:10:50.071Z
Learnt from: badri-singhal
Repo: juspay/clairvoyance PR: 313
File: app/api/routers/breeze_buddy.py:487-489
Timestamp: 2025-10-15T04:10:50.071Z
Learning: In the Breeze Buddy dashboard (`app/agents/voice/breeze_buddy/workflows/order_confirmation/dashboard.html`), analytics metrics are dynamically rendered using a for loop in the HTML script. When adding new metrics to the JSON response in the `get_analytics()` endpoint, no manual HTML template updates are required as the loop handles all metrics automatically.

Applied to files:

  • app/ai/voice/agents/breeze_buddy/sample/order-confirmation-workflow.json
  • app/api/routers/breeze_buddy.py
  • app/ai/voice/agents/breeze_buddy/dashboard/login.html
  • app/ai/voice/agents/breeze_buddy/websocket_bot.py
🧬 Code graph analysis (11)
app/database/accessor/__init__.py (1)
app/database/accessor/breeze_buddy/template.py (2)
  • create_template (55-93)
  • get_template_by_merchant (29-52)
app/database/accessor/breeze_buddy/template.py (4)
app/ai/voice/agents/breeze_buddy/workflow/types.py (1)
  • TemplateModel (46-53)
app/database/decoder/breeze_buddy/template.py (1)
  • decode_template (15-36)
app/database/queries/__init__.py (1)
  • run_parameterized_query (14-30)
app/database/queries/breeze_buddy/template.py (2)
  • create_template_query (36-62)
  • get_template_by_merchant_query (11-33)
app/ai/voice/agents/breeze_buddy/workflow/__init__.py (5)
app/ai/voice/agents/breeze_buddy/workflow/builder.py (1)
  • FlowConfigBuilder (27-249)
app/ai/voice/agents/breeze_buddy/workflow/context.py (3)
  • context (52-54)
  • WorkflowContext (14-127)
  • with_context (130-200)
app/ai/voice/agents/breeze_buddy/workflow/hooks.py (3)
  • Hook (26-73)
  • HookRegistry (193-234)
  • UpdateOutcomeInDatabaseHook (76-190)
app/ai/voice/agents/breeze_buddy/workflow/loader.py (1)
  • FlowConfigLoader (16-160)
app/ai/voice/agents/breeze_buddy/workflow/transition.py (1)
  • transition_handler (19-69)
app/database/decoder/breeze_buddy/template.py (1)
app/ai/voice/agents/breeze_buddy/workflow/types.py (1)
  • TemplateModel (46-53)
app/ai/voice/agents/breeze_buddy/workflow/hooks.py (3)
app/ai/voice/agents/breeze_buddy/workflow/context.py (3)
  • context (52-54)
  • WorkflowContext (14-127)
  • lead (57-59)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
  • update_lead_call_completion_details (282-312)
app/schemas.py (1)
  • LeadCallOutcome (32-38)
app/api/routers/breeze_buddy.py (4)
app/ai/voice/agents/breeze_buddy/types/models.py (1)
  • LeadData (15-25)
app/ai/voice/agents/breeze_buddy/workflow/types.py (1)
  • CreateTemplateRequest (74-80)
app/database/accessor/breeze_buddy/template.py (1)
  • create_template (55-93)
app/schemas.py (1)
  • TokenData (146-152)
app/ai/voice/agents/breeze_buddy/workflow/types.py (1)
app/schemas.py (1)
  • Workflow (21-22)
app/ai/voice/agents/breeze_buddy/workflow/transition.py (3)
app/ai/voice/agents/breeze_buddy/analytics/tracing_setup.py (1)
  • auto_trace (57-106)
app/ai/voice/agents/breeze_buddy/workflow/context.py (3)
  • context (52-54)
  • WorkflowContext (14-127)
  • create_node_from_template (96-127)
app/ai/voice/agents/breeze_buddy/workflow/hooks.py (3)
  • HookRegistry (193-234)
  • get (214-224)
  • safe_execute (50-73)
app/ai/voice/agents/breeze_buddy/workflow/loader.py (3)
app/ai/voice/agents/breeze_buddy/workflow/types.py (1)
  • TemplateModel (46-53)
app/schemas.py (1)
  • Workflow (21-22)
app/database/accessor/breeze_buddy/template.py (1)
  • get_template_by_merchant (29-52)
app/database/queries/breeze_buddy/lead_call_tracker.py (1)
app/schemas.py (2)
  • LeadCallStatus (25-29)
  • LeadCallOutcome (32-38)
app/ai/voice/agents/breeze_buddy/websocket_bot.py (2)
app/ai/voice/agents/breeze_buddy/types/models.py (1)
  • OrderData (11-12)
app/ai/voice/agents/breeze_buddy/workflow/context.py (1)
  • call_sid (62-64)
🪛 Ruff (0.14.8)
app/ai/voice/agents/breeze_buddy/handlers/internal/__init__.py

14-19: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

app/database/accessor/breeze_buddy/template.py

30-30: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


30-30: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


48-48: Consider moving this statement to an else block

(TRY300)


50-50: Do not catch blind exception: Exception

(BLE001)


89-89: Consider moving this statement to an else block

(TRY300)


91-91: Do not catch blind exception: Exception

(BLE001)

app/ai/voice/agents/breeze_buddy/workflow/__init__.py

27-36: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py

21-21: Unused function argument: args

(ARG001)


21-21: Unused function argument: transition_to

(ARG001)


45-45: Unused function argument: args

(ARG001)


45-45: Unused function argument: transition_to

(ARG001)


69-69: Unused function argument: transition_to

(ARG001)


105-105: Do not catch blind exception: Exception

(BLE001)


118-118: Unused function argument: args

(ARG001)


118-118: Unused function argument: transition_to

(ARG001)


200-200: f-string without any placeholders

Remove extraneous f prefix

(F541)


202-202: Do not catch blind exception: Exception

(BLE001)


204-204: Use explicit conversion flag

Replace with conversion flag

(RUF010)


217-217: Unused function argument: transition_to

(ARG001)


314-314: Do not catch blind exception: Exception

(BLE001)


326-326: Do not catch blind exception: Exception

(BLE001)

app/database/queries/breeze_buddy/template.py

12-12: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


12-12: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


26-31: Possible SQL injection vector through string-based query construction

(S608)


47-51: Possible SQL injection vector through string-based query construction

(S608)

app/ai/voice/agents/breeze_buddy/workflow/hooks.py

69-69: Do not catch blind exception: Exception

(BLE001)


71-71: Use explicit conversion flag

Replace with conversion flag

(RUF010)


185-185: Do not catch blind exception: Exception

(BLE001)


200-200: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

app/api/routers/breeze_buddy.py

71-71: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


83-83: Abstract raise to an inner function

(TRY301)


83-83: Avoid specifying long messages outside the exception class

(TRY003)


86-86: Abstract raise to an inner function

(TRY301)


86-86: Avoid specifying long messages outside the exception class

(TRY003)


89-89: Abstract raise to an inner function

(TRY301)


89-89: Avoid specifying long messages outside the exception class

(TRY003)


104-104: Abstract raise to an inner function

(TRY301)


104-104: Create your own exception

(TRY002)


104-104: Avoid specifying long messages outside the exception class

(TRY003)


118-118: Do not catch blind exception: Exception

(BLE001)

app/ai/voice/agents/breeze_buddy/agent.py

130-130: Do not catch blind exception: Exception

(BLE001)


164-164: Do not catch blind exception: Exception

(BLE001)


196-196: Do not catch blind exception: Exception

(BLE001)


215-215: Do not catch blind exception: Exception

(BLE001)


360-360: Unused function argument: transport

(ARG001)


386-386: Unused function argument: transport

(ARG001)


391-391: Unused function argument: task

(ARG001)

app/ai/voice/agents/breeze_buddy/workflow/builder.py

64-64: Avoid specifying long messages outside the exception class

(TRY003)


69-69: Avoid specifying long messages outside the exception class

(TRY003)


76-76: Avoid specifying long messages outside the exception class

(TRY003)


102-102: Avoid specifying long messages outside the exception class

(TRY003)


193-193: Avoid specifying long messages outside the exception class

(TRY003)


242-242: Avoid specifying long messages outside the exception class

(TRY003)

app/ai/voice/agents/breeze_buddy/workflow/transition.py

53-55: Store a reference to the return value of asyncio.create_task

(RUF006)

app/ai/voice/agents/breeze_buddy/workflow/loader.py

118-120: Avoid specifying long messages outside the exception class

(TRY003)

app/database/queries/breeze_buddy/lead_call_tracker.py

233-238: Possible SQL injection vector through string-based query construction

(S608)

🔇 Additional comments (40)
app/database/queries/breeze_buddy/template.py (1)

26-31: Static SQL-injection warning is a false positive here

Ruff’s S608 warning is triggered by the f-strings, but in this file:

  • TEMPLATE_TABLE and the column names are hard-coded.
  • All dynamic values go through positional parameters ($1, $2, …) and values.

There is no user-controlled identifier interpolation, so these queries are safe under normal usage. No change needed, but you may want to suppress S608 for this file or function if it’s noisy in your CI.

Also applies to: 47-51

app/ai/voice/agents/breeze_buddy/managers/calls.py (2)

10-12: Import move to common utils looks good

Switching send_webhook_with_retry to the shared utils.common module aligns with the broader refactor and avoids workflow-specific coupling. No issues here.


374-379: Confirm callers and updater handle optional outcome/meta_data correctly

The new handle_call_completion signature and returning updated_lead make sense, but a couple of things are worth double-checking:

  • Ensure all call sites now pass meta_data as a dict when they have details, and are fine with None otherwise.
  • Verify update_lead_call_completion_details accepts None for outcome and meta_data without attempting to mutate them (e.g., no .update on None).

If both are true, the new API is a solid improvement.

Also applies to: 401-407, 412-413

app/ai/voice/agents/breeze_buddy/services/telephony/twilio/twilio.py (1)

10-12: Centralizing websocket bot import is consistent with new architecture

Using app.ai.voice.agents.breeze_buddy.websocket_bot instead of the workflow-specific module keeps Twilio tied to the generic Breeze Buddy websocket entrypoint. No functional issues spotted.

app/ai/voice/agents/breeze_buddy/services/telephony/exotel/exotel.py (1)

10-12: Exotel websocket handler now uses shared Breeze Buddy websocket bot

Aligning Exotel with the shared breeze_buddy.websocket_bot keeps providers consistent and reduces workflow-specific coupling. Change looks safe.

app/ai/voice/agents/breeze_buddy/dashboard/login.html (1)

373-376: Redirecting to unified /dashboard route after login

Updating the post-login redirect to /agent/voice/breeze-buddy/dashboard matches the new consolidated dashboard route and keeps the login flow aligned with the backend router changes. No further template changes are needed since the dashboard renders metrics dynamically from the API. Based on learnings, this won’t require manual metric wiring.

app/ai/voice/agents/breeze_buddy/types/models.py (1)

15-26: LeadData model shape looks aligned with usage

The new LeadData model cleanly groups the customer, shop, order, and reporting fields that the Breeze Buddy flows use. The structure is sensible and should be a drop-in replacement for the previous order payload type.

Ensure all previous references (e.g., BreezeOrderData or similar models) have been updated across the codebase and in API handlers. Also review the inconsistent Optional syntax—the model uses both Optional[str] and str | None which should be standardized for consistency.

app/ai/voice/agents/breeze_buddy/workflow/__init__.py (1)

1-36: LGTM! Clean public API surface for the workflow engine.

The module provides a well-organized public interface for the workflow engine components. The docstring clearly describes the package's purpose.

app/database/decoder/breeze_buddy/template.py (1)

39-50: LGTM! Proper handling of list decoding.

The function correctly handles empty results and applies the decoder to each row.

app/database/migrations/002_add_shop_identifier_and_locks.sql (1)

1-34: LGTM! Well-designed migration with proper safeguards.

The migration effectively handles multi-shop scenarios and adds locking capabilities. Key highlights:

  • Uses IF NOT EXISTS/IF EXISTS for idempotent execution
  • Partial unique indexes (lines 20-27) elegantly handle NULL vs. non-NULL shop_identifier cases
  • Composite index on (status, is_locked, next_attempt_at) (line 33-34) efficiently supports the locking query pattern
  • Default values ensure existing rows work correctly
app/ai/voice/agents/breeze_buddy/sample/order-confirmation-workflow.json (3)

1-67: LGTM! Well-structured initial node with clear instructions.

The initial node provides comprehensive guidance for the agent, including:

  • Gender-based addressing logic
  • Clear conversation flow
  • Pincode/phone number reading rules
  • Proper function definitions with transitions and hooks

346-371: LGTM! Proper end-node configuration.

The order confirmation end node correctly:

  • Mutes STT before final message (prevents interruption)
  • Calls end_conversation_handler
  • Calls update_calling_service_handler for external notification
  • Has no functions (terminal node)

14-14: Verify template variable handling in TTS conversion pipeline.

The system prompt includes template variables with curly braces ({shop_name}, {customer_name}, {order_summary}, etc.) while explicitly stating to avoid special characters for TTS conversion. Confirm that these variables are substituted with plain text before TTS processing and that curly braces do not reach the TTS engine.

app/database/queries/breeze_buddy/lead_call_tracker.py (1)

196-240: LGTM! Safe dynamic SQL construction for partial updates.

The function correctly builds parameterized queries dynamically based on which fields are provided. Key points:

  • Only non-None fields are included in the UPDATE
  • All values are properly parameterized (no SQL injection risk despite the static analysis warning)
  • The incrementing parameter index pattern (${len(values)}) is correct
  • Always updates updated_at for audit trail

The static analysis warning (S608) is a false positive—the SQL string construction uses only hardcoded column names, and all values are parameterized.

app/database/accessor/breeze_buddy/lead_call_tracker.py (1)

282-312: LGTM! Consistent partial update implementation.

The function signature correctly aligns with the query layer changes to support partial updates. The docstring accurately describes the behavior, and all optional parameters are properly forwarded to the query function.

app/database/migrations/003_create_template_table.sql (1)

4-31: Schema design looks good overall.

The partial unique indexes correctly handle the NULL shop_identifier case for uniqueness constraints, and the JSONB type is appropriate for flexible flow storage. The performance indexes are well-chosen.

app/ai/voice/agents/breeze_buddy/websocket_bot.py (2)

39-47: Import reorganization looks good.

The imports have been correctly updated to reflect the new module structure with OrderData from types/models and utilities from utils/common.


416-416: System prompt language instruction simplified appropriately.

The instruction for multilingual response is clear and maintains the expected behavior.

app/database/accessor/breeze_buddy/template.py (1)

22-27: Implementation is sound.

The accessor functions properly delegate to query builders, handle results via decoders, and include appropriate error handling with logging.

Also applies to: 35-52

app/ai/voice/agents/breeze_buddy/workflow/transition.py (1)

72-109: Async hook execution implementation is well-structured.

Sequential hook execution with logging and graceful handling of missing hooks via HookRegistry.get is appropriate. The safe_execute call ensures individual hook failures don't crash the background task.

app/ai/voice/agents/breeze_buddy/workflow/builder.py (3)

196-206: Closure captures func correctly in this context.

The wrapper function correctly captures func.transition_to, func.hooks, and func.name at schema creation time. Since each iteration creates a new wrapper_handler function, the captured values are specific to each function schema.


247-249: Unknown action types are silently degraded.

Returning {"type": str(action_type)} for unknown action types may cause downstream failures that are hard to debug. Consider raising an error or at least logging at warning level (which you do) but also documenting expected behavior.

Verify that downstream Pipecat flow processing handles unknown action types gracefully, or consider raising an error here for fail-fast behavior:

         else:
-            logger.warning(f"Unknown action type: {action_type}, returning as string")
-            return {"type": str(action_type)}
+            logger.error(f"Unknown action type: {action_type}")
+            raise ValueError(f"Unknown action type: {action_type}")

27-41: FlowConfigBuilder structure is clean and well-organized.

The builder pattern with a handler map, node building, and function schema generation is well-designed. Logging throughout aids debugging during workflow construction.

Also applies to: 118-171

app/ai/voice/agents/breeze_buddy/workflow/context.py (2)

14-95: Well-structured context wrapper with clean property accessors.

The WorkflowContext class provides a clean abstraction for handlers to access bot state while maintaining the stateless handler pattern. The read-only properties with only conversation_ended being mutable is a good design choice.


130-200: Clean decorator factory pattern for context injection.

The with_context decorator cleanly separates transition handlers from action handlers and properly passes through the context. The logging is helpful for debugging.

app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py (3)

21-43: STT mute handler looks good.

The handler properly checks for VAD analyzer presence before modifying confidence and logs the state change appropriately. The unused args and transition_to parameters are intentional for maintaining a uniform handler interface.


216-332: Webhook handler implementation is thorough.

Good error handling, proper timezone usage for call duration calculation, and conditional webhook sending based on outcome. The logging provides good visibility into the webhook lifecycle.


135-139: Unable to locate the referenced file for verification.

The file app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py does not exist in the repository, making it impossible to verify the code context and concerns. However, regarding the general concern about task.cancel() being called multiple times: Python's asyncio.Task.cancel() is idempotent and safe to call repeatedly. It returns False if the task is already done or cancelled, and True if the cancellation request was successfully registered. Therefore, even if the finally block executes after an early return, calling cancel() multiple times poses no issue.

If you're reviewing code in a pending commit or branch, please ensure the file is available in the current repository state for verification.

app/ai/voice/agents/breeze_buddy/workflow/loader.py (3)

19-55: Template loading with lazy import is appropriate.

The lazy import at line 42 to avoid circular dependency is a reasonable approach. Good logging of template loading progress.


141-150: Flexible task message format handling is good.

The code gracefully handles Pydantic models (with model_dump), dicts, and fallback formats. This makes the loader resilient to different data sources.


125-128: In-place mutation of template flow nodes.

The template's flow["nodes"] is mutated in place at line 128. This is efficient but means the original template object is modified. If the same template instance is reused elsewhere, this could cause unexpected behavior. Consider whether a deep copy is needed or if the template is intended to be modified in place.

app/api/routers/breeze_buddy.py (2)

26-30: Type rename from BreezeOrderData to LeadData applied correctly.

The import and parameter type changes are consistent with the model rename mentioned in the summary.

Also applies to: 346-346


618-618: Dashboard route changed to /dashboard — preserve links or add redirect

Route moved from /breeze/order-confirmation/dashboard to /dashboard; update internal/external links or add a redirect to avoid breaking bookmarks. File: app/api/routers/breeze_buddy.py:618

app/ai/voice/agents/breeze_buddy/workflow/hooks.py (2)

26-73: Clean hook abstraction with safe execution wrapper.

The Hook ABC provides a good pattern with safe_execute handling errors gracefully so hooks don't crash the main workflow. The logging in safe_execute provides good observability.


237-238: Module-level hook registration is appropriate.

Auto-registering hooks at module load is a clean pattern for this singleton registry approach.

app/ai/voice/agents/breeze_buddy/agent.py (5)

359-393: Unused event handler arguments are expected.

The static analysis flags unused transport, client, and task arguments (ARG001), but these are required by the event handler signature from the pipecat framework. You can silence these warnings with underscore prefixes if desired:


320-331: Pipeline construction looks correct.

The pipeline stages follow a logical flow: transport input → STT → mute filter → context aggregation → LLM → TTS → transport output → assistant context. The bidirectional context aggregator placement is appropriate.


432-439: Disconnect handling is well-guarded.

The conversation_ended flag prevents double-processing of disconnect events. Setting outcome to "busy" as a fallback for unexpected disconnects is a reasonable default.


442-453: Clean entry point.

The main() function provides a simple factory interface for creating and running the bot.


1-2: audioop is deprecated and will be removed in Python 3.13.

The audioop module was deprecated in Python 3.11 and removed from the standard library in Python 3.13 (last included in 3.12). Consider using an alternative library such as soundfile, or the audioop-lts PyPI backport if Python 3.13+ support is required.

context.bot.lead = await context.bot.completion_function(
call_id=context.call_sid,
outcome=context.lead.outcome,
call_end_time=datetime.now(),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use timezone-aware datetime.now(timezone.utc) for consistency.

Line 195 uses datetime.now() without timezone, while timezone is already imported and used elsewhere in this file (line 8). This could cause issues when comparing with timezone-aware datetimes.

-                call_end_time=datetime.now(),
+                call_end_time=datetime.now(timezone.utc),
📝 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
call_end_time=datetime.now(),
call_end_time=datetime.now(timezone.utc),
🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py around
line 195, replace the naive datetime.now() with a timezone-aware call
datetime.now(timezone.utc) so the timestamp is consistent with other
timezone-aware datetimes in the file; ensure timezone is imported (it already is
on line 8) and update any tests or comparisons that assume naive datetimes
accordingly.

)
logger.info(f"Successfully updated database for call {context.call_sid}")
else:
logger.warning(f"No call_sid found, skipping database update")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove extraneous f prefix from string without placeholders.

Static analysis correctly flagged this as an f-string without placeholders.

-            logger.warning(f"No call_sid found, skipping database update")
+            logger.warning("No call_sid found, skipping database update")
📝 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
logger.warning(f"No call_sid found, skipping database update")
logger.warning("No call_sid found, skipping database update")
🧰 Tools
🪛 Ruff (0.14.8)

200-200: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py around
line 200, the logger.warning call uses an unnecessary f-string prefix for a
literal without placeholders; remove the leading "f" so the message is a normal
string literal (e.g., replace f"... " with "..." ) to satisfy static analysis
and avoid misleading f-string usage.

Comment on lines +636 to 637
meta_data = {
"transcription": {
"messages": transcription,
"call_sid": self.call_sid,
},
"updated_address": self.updated_address,
"cancellation_reason": self.cancellation_reason,
}
await self.completion_function(
call_id=self.call_sid,
outcome=call_outcome,
call_end_time=datetime.now(),
updated_address=self.updated_address,
cancellation_reason=self.cancellation_reason,
meta_data=meta_data,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use timezone-aware datetime for call_end_time.

Line 647 uses datetime.now() without timezone, but the rest of this file uses datetime.now(timezone.utc) (e.g., lines 109, 513-517). This inconsistency could cause issues when comparing or storing timestamps.

                 await self.completion_function(
                     call_id=self.call_sid,
                     outcome=call_outcome,
-                    call_end_time=datetime.now(),
+                    call_end_time=datetime.now(timezone.utc),
                     meta_data=meta_data,
                 )
📝 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
meta_data = {
"transcription": {
"messages": transcription,
"call_sid": self.call_sid,
},
"updated_address": self.updated_address,
"cancellation_reason": self.cancellation_reason,
}
await self.completion_function(
call_id=self.call_sid,
outcome=call_outcome,
call_end_time=datetime.now(),
updated_address=self.updated_address,
cancellation_reason=self.cancellation_reason,
meta_data=meta_data,
)
meta_data = {
"transcription": {
"messages": transcription,
"call_sid": self.call_sid,
},
"updated_address": self.updated_address,
"cancellation_reason": self.cancellation_reason,
}
await self.completion_function(
call_id=self.call_sid,
outcome=call_outcome,
call_end_time=datetime.now(timezone.utc),
meta_data=meta_data,
)
🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/websocket_bot.py around lines 636 to 649,
the call_end_time is set using naive datetime.now(), which is inconsistent with
the file's use of timezone-aware datetimes; change datetime.now() to
datetime.now(timezone.utc) (and add/import timezone from datetime at the top of
the file if not already present) so the timestamp is timezone-aware and
consistent with other timestamps in this module.

Comment on lines 59 to 69
# Handle immediate node transition
if transition_to:
logger.info(
f"Transitioning from current node to '{transition_to}' for function '{function_name}'"
)
next_node = context.create_node_from_template(transition_to)
return {}, next_node
else:
logger.debug(f"No transition specified for function '{function_name}'")

return {}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return type is inconsistent between branches.

When transition_to is specified, the function returns ({}, next_node) (a tuple), but when not specified, it returns {} (a dict). This inconsistency could cause issues for callers expecting a consistent return type.

Consider returning a consistent type:

     # Handle immediate node transition
     if transition_to:
         logger.info(
             f"Transitioning from current node to '{transition_to}' for function '{function_name}'"
         )
         next_node = context.create_node_from_template(transition_to)
         return {}, next_node
     else:
         logger.debug(f"No transition specified for function '{function_name}'")
+        return {}, None

-    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
# Handle immediate node transition
if transition_to:
logger.info(
f"Transitioning from current node to '{transition_to}' for function '{function_name}'"
)
next_node = context.create_node_from_template(transition_to)
return {}, next_node
else:
logger.debug(f"No transition specified for function '{function_name}'")
return {}
# Handle immediate node transition
if transition_to:
logger.info(
f"Transitioning from current node to '{transition_to}' for function '{function_name}'"
)
next_node = context.create_node_from_template(transition_to)
return {}, next_node
else:
logger.debug(f"No transition specified for function '{function_name}'")
return {}, None
🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/workflow/transition.py around lines 59 to
69, the function returns a tuple ({}, next_node) when transition_to is set but a
plain dict {} otherwise; make the return type consistent by always returning a
tuple (dict, next_node_or_none) — e.g., set next_node = None in the
no-transition branch and return ({}, next_node) in both paths so callers always
receive the same (dict, Optional[node]) shape.

Comment on lines 68 to 135
@router.post("/workflow")
async def create_workflow_from_json(
workflow_data: CreateTemplateRequest,
current_user: TokenData = Depends(get_current_user),
):
"""
Creates a complete workflow from a JSON object using the new simplified architecture.
Requires JWT authentication.
"""
logger.info(f"User {current_user.user_id} is creating/updating a workflow.")

try:
# Validate that flow structure contains required fields
flow = workflow_data.flow
if not flow:
raise ValueError("Flow structure is required")

if "initial_node" not in flow:
raise ValueError("initial_node must be specified in flow structure")

if "nodes" not in flow or not flow["nodes"]:
raise ValueError("nodes must be specified in flow structure")

# Create the template with flow stored as JSON
now = datetime.now(timezone.utc)
template = await create_template(
template_id=str(uuid4()),
merchant_id=workflow_data.merchant_id,
shop_identifier=workflow_data.shop_identifier,
name=workflow_data.workflow.value,
flow=flow,
is_active=workflow_data.is_active,
now=now,
)

if not template:
raise Exception("Failed to create template")

logger.info(
f"Successfully created template with id: {template.id} containing flow with {len(flow.get('nodes', []))} nodes"
)

return JSONResponse(
status_code=201,
content={
"status": "success",
"template_id": template.id,
"message": f"Template '{workflow_data.workflow.value}' created successfully with {len(flow.get('nodes', []))} nodes",
},
)
except Exception as e:
logger.error(f"Error creating workflow from JSON: {e}", exc_info=True)
return JSONResponse(status_code=400, content={"detail": str(e)})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

New workflow creation endpoint needs refinement.

Several concerns with this endpoint:

  1. Line 104: Using bare Exception is not recommended. Consider using HTTPException directly or a custom exception.

  2. Duplicate templates: No check for existing templates with the same merchant_id + workflow combination. This could lead to duplicate templates.

  3. Authorization: The endpoint only checks authentication but not authorization - any authenticated user can create templates for any merchant.

Consider these improvements:

+        # Check for existing template
+        from app.database.accessor.breeze_buddy.template import get_template_by_merchant
+        existing = await get_template_by_merchant(
+            workflow_data.merchant_id, 
+            workflow_data.shop_identifier, 
+            workflow_data.workflow.value
+        )
+        if existing:
+            raise HTTPException(
+                status_code=409, 
+                detail=f"Template already exists for this merchant and workflow"
+            )
+
         # Create the template with flow stored as JSON
         now = datetime.now(timezone.utc)
         template = await create_template(

For the exception at line 104:

-            raise Exception("Failed to create template")
+            raise HTTPException(status_code=500, detail="Failed to create template")
🧰 Tools
🪛 Ruff (0.14.8)

71-71: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


83-83: Abstract raise to an inner function

(TRY301)


83-83: Avoid specifying long messages outside the exception class

(TRY003)


86-86: Abstract raise to an inner function

(TRY301)


86-86: Avoid specifying long messages outside the exception class

(TRY003)


89-89: Abstract raise to an inner function

(TRY301)


89-89: Avoid specifying long messages outside the exception class

(TRY003)


104-104: Abstract raise to an inner function

(TRY301)


104-104: Create your own exception

(TRY002)


104-104: Avoid specifying long messages outside the exception class

(TRY003)


118-118: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In app/api/routers/breeze_buddy.py around lines 68-120, the
create_workflow_from_json endpoint uses a bare Exception at line 104, lacks a
uniqueness check for merchant_id+workflow (allowing duplicate templates), and
enforces authentication but not authorization. Replace the bare Exception with
raising an appropriate HTTPException (e.g., HTTPException(status_code=500,
detail="...") for server errors or raise HTTPException/RequestValidationError
with precise status codes), add a pre-create query to check for an existing
template with the same merchant_id and workflow name and return
HTTPException(status_code=409, detail="Template already exists") if found, and
enforce authorization by verifying current_user is allowed to act for the
provided merchant_id (e.g., compare current_user merchant list/role or require
merchant ownership) and return HTTPException(status_code=403) when unauthorized;
ensure error responses use HTTPException instead of generic exceptions and log
context as needed.

Comment on lines +5 to +14
CREATE TABLE IF NOT EXISTS outbound_number (
id VARCHAR(255) PRIMARY KEY,
number VARCHAR(20) NOT NULL UNIQUE,
provider VARCHAR(50) CHECK (provider IN ('TWILIO', 'EXOTEL')) NOT NULL,
status VARCHAR(50) CHECK (status IN ('AVAILABLE', 'IN_USE', 'DISABLED')) NOT NULL,
channels INTEGER,
maximum_channels INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make channels and maximum_channels non-null with sane defaults

Runtime code in app/ai/voice/agents/breeze_buddy/managers/calls.py assumes channels and maximum_channels are integers (it compares them and increments channels). Allowing NULLs here can cause TypeError when comparing or adding.

Consider tightening the schema to something like:

channels INTEGER NOT NULL DEFAULT 0,
maximum_channels INTEGER NOT NULL,

(and optionally CHECK (channels >= 0 AND maximum_channels > 0)).

This will prevent bad rows from breaking call processing at runtime.

[scratchpad_start]
[done]
File 1 reviewed.
[scratchpad_end] -->

🤖 Prompt for AI Agents
In app/database/migrations/001_initial_tables.sql around lines 5 to 14, the
columns channels and maximum_channels are nullable which can cause TypeError in
runtime code that treats them as integers; update the schema to make channels
INTEGER NOT NULL DEFAULT 0 and maximum_channels INTEGER NOT NULL (no NULLs), and
add optional CHECK constraints such as CHECK (channels >= 0 AND maximum_channels
> 0) to enforce sensible ranges so code can safely compare and increment these
fields.

@badri-singhal badri-singhal force-pushed the template-based-architechture branch from 0ce7896 to d86db6d Compare December 9, 2025 09:52
"mute_stt_handler",
"unmute_stt_handler",
"play_audio_sound_handler",
"end_conversation_handler",
Copy link
Collaborator

Choose a reason for hiding this comment

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

. handler specification again is not req

)


async def play_audio_sound_handler(context: WorkflowContext, args, transition_to=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

take sound name to be played

end_conversation_handler,
mute_stt_handler,
play_audio_sound_handler,
unmute_stt_handler,
Copy link
Collaborator

Choose a reason for hiding this comment

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

service_callback

return {}


async def end_conversation_handler(context: WorkflowContext, args, transition_to=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

see how pipecat officially handles things inside end-conversation

)
context.bot.lead = await context.bot.completion_function(
call_id=context.call_sid,
outcome=context.lead.outcome,
Copy link
Collaborator

Choose a reason for hiding this comment

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

outcome should be string in db for leads and enum in code - template based

},
body: JSON.stringify({ username, password }),
});

Copy link
Collaborator

Choose a reason for hiding this comment

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

merchant ID should come in body, nothing should come in path

if (response.ok) {
window.location.href =
"/agent/voice/breeze-buddy/breeze/order-confirmation/dashboard";
"/agent/voice/breeze-buddy/dashboard";
Copy link
Collaborator

Choose a reason for hiding this comment

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

merchant

Copy link
Collaborator

Choose a reason for hiding this comment

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

identifier

Copy link
Collaborator

Choose a reason for hiding this comment

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

data_payload idk wtf you decide

Copy link
Collaborator

Choose a reason for hiding this comment

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

remove shop_name - it should ideally it should come from template / call exec config

Copy link
Collaborator

Choose a reason for hiding this comment

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

expected payload in template

Copy link
Collaborator

Choose a reason for hiding this comment

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

examples/templates/order-confirmation.json

@badri-singhal badri-singhal force-pushed the template-based-architechture branch 2 times, most recently from 2b35c97 to 7f78bf4 Compare December 11, 2025 09:58
@swaroopvarma1
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 12, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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: 11

Caution

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

⚠️ Outside diff range comments (3)
app/database/accessor/breeze_buddy/call_execution_config.py (1)

32-63: create_call_execution_config should likely accept shop_identifier: Optional[str] = None (generic config support).

Right now reads explicitly fall back to shop_identifier IS NULL, but create forces a non-null string, which can prevent creating the generic row.

 async def create_call_execution_config(
     id: str,
     initial_offset: int,
     retry_offset: int,
     call_start_time: time,
     call_end_time: time,
     max_retry: int,
     calling_provider: CallProvider,
     merchant_id: str,
     template: str,
-    shop_identifier: str,
+    shop_identifier: Optional[str] = None,
     enable_international_call: bool,
 ) -> Optional[CallExecutionConfig]:
app/api/routers/breeze_buddy.py (2)

610-657: Inconsistent terminology: workflow vs template.

Line 633 passes config.workflow as the template parameter to create_call_execution_config. While functionally correct, this inconsistency suggests the CreateCallExecutionConfigRequest schema still uses the old workflow field name instead of template.

Consider updating the request schema to use consistent template-based terminology:

# In app/schemas.py (CreateCallExecutionConfigRequest)
class CreateCallExecutionConfigRequest(BaseModel):
    merchant_id: str
-   workflow: str
+   template: str
    shop_identifier: Optional[str] = None
    # ... other fields

Then update line 633:

-            template=config.workflow,
+            template=config.template,

Apply the same change to UpdateCallExecutionConfigRequest at lines 676, 689, 694.


574-607: Remove unused template parameter or implement its intended purpose.

The template parameter is captured from the WebSocket path (line 574) and logged (line 583), but it's never used in the handler logic and is not passed to provider.handle_websocket() (line 589). The method signature for handle_websocket only accepts (websocket: WebSocket, provider: CallProvider), so the template cannot be passed there.

Either remove template from the route path and function signature, or implement the intended use case (e.g., fetching template-specific configuration, validating the template, or passing it through the provider chain).

♻️ Duplicate comments (4)
app/ai/voice/agents/breeze_buddy/websocket_bot.py (1)

624-637: Use timezone-aware datetime and approve meta_data refactor.

The meta_data structure consolidates transcription, updated_address, and cancellation_reason into a cleaner payload format. However, Line 635 uses datetime.now() without timezone, inconsistent with the rest of the file that uses datetime.now(timezone.utc).

Apply this diff to use timezone-aware datetime:

             if self.call_sid:
                 meta_data = {
                     "transcription": {
                         "messages": transcription,
                         "call_sid": self.call_sid,
                     },
                     "updated_address": self.updated_address,
                     "cancellation_reason": self.cancellation_reason,
                 }
                 await self.completion_function(
                     call_id=self.call_sid,
                     outcome=call_outcome,
-                    call_end_time=datetime.now(),
+                    call_end_time=datetime.now(timezone.utc),
                     meta_data=meta_data,
                 )
app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py (1)

120-217: Robustness gaps in finalization: guard lead/metaData/task and use UTC timestamp.

  • context.lead / context.lead.metaData can be None → context.lead.metaData["transcription"] = ... can crash.
  • await context.task.queue_frame(EndFrame()) in finally can crash if task is missing/ended.
  • Line 197 uses datetime.now() (naive); this matches earlier review feedback (timezone-aware UTC recommended).
app/ai/voice/agents/breeze_buddy/agent.py (1)

128-153: Incorrect file path - missing ai/ directory component.

The module is located at app/ai/voice/agents/breeze_buddy/ but the hardcoded path references app/ai/voice/agents/breeze_buddy/ which appears correct now. However, consider using pathlib.Path relative to the module location for more robust path handling across different working directories.

+from pathlib import Path

+# At module level:
+STATIC_AUDIO_DIR = Path(__file__).parent / "static" / "audio"

 # In the method:
-                wav_file_path = (
-                    "app/ai/voice/agents/breeze_buddy/static/audio/dial-tone.wav"
-                )
+                wav_file_path = str(STATIC_AUDIO_DIR / "dial-tone.wav")
app/api/routers/breeze_buddy.py (1)

73-141: Enforce merchant-level authorization in template creation.

While the duplicate template check has been implemented (lines 96-106), the endpoint still lacks authorization enforcement. Any authenticated user can create templates for any merchant by providing an arbitrary merchant value in the request payload.

Consider adding merchant ownership or permission verification before template creation:

     logger.info(f"User {current_user.user_id} is creating/updating a template.")
 
     try:
+        # Verify user has permission for this merchant
+        # Example: check if current_user.merchant_id == template_data.merchant
+        # or validate against a user-merchant permission table
+        if not await user_has_merchant_permission(current_user.user_id, template_data.merchant):
+            raise HTTPException(
+                status_code=403,
+                detail=f"User does not have permission to create templates for merchant: {template_data.merchant}"
+            )
+
         # Validate that flow structure contains required fields
         flow = template_data.flow

Based on past review comments, this authorization gap was previously flagged. Do you need help implementing the merchant permission verification logic?

🧹 Nitpick comments (11)
app/database/decoder/breeze_buddy/template.py (1)

22-35: Consider guarding json.loads to avoid hard-failing on bad DB content.

If flow / expected_payload_schema can ever be malformed, wrap json.loads with try/except json.JSONDecodeError and fall back to {} / None (and log at warning/debug).

app/database/accessor/breeze_buddy/template.py (2)

29-53: Fix implicit Optional params + keep traceback on failures (logger.exception).

-from typing import Optional
+from datetime import datetime
+from typing import Optional
@@
 async def get_template_by_merchant(
-    merchant_id: str, shop_identifier: str = None, name: str = None
+    merchant_id: str, shop_identifier: Optional[str] = None, name: Optional[str] = None
 ) -> Optional[TemplateModel]:
@@
-    except Exception as e:
-        logger.error(f"Error getting template by merchant: {e}")
+    except Exception:
+        logger.exception("Error getting template by merchant")
         return None

55-98: Type now (likely datetime) and prefer logger.exception over stringifying the exception.

 async def create_template(
@@
-    now,
+    now: datetime,
 ) -> Optional[TemplateModel]:
@@
-    except Exception as e:
-        logger.error(f"Error creating template: {e}")
+    except Exception:
+        logger.exception("Error creating template")
         return None
app/ai/voice/agents/breeze_buddy/template/loader.py (2)

18-55: Consider not logging high-cardinality identifiers at INFO (merchant_id), or redact/hash.

If merchant_id is sensitive / high-volume, switching to DEBUG or redaction reduces noisy logs and privacy risk.


56-85: Template rendering via .replace() is brittle; consider a safer formatter (and types).

A minimal improvement is str.format_map with a “safe dict” (leave unknown placeholders untouched), or a small templating helper, plus tightening types to list[dict[str, Any]] / Mapping[str, str] so callers know what shapes are supported.

app/database/queries/breeze_buddy/template.py (2)

11-34: Modernize Optional typing (str | None) to satisfy Ruff / clarity.

Ruff’s RUF013 is reasonable here; it also makes call sites clearer.


36-64: Add concrete types for created_at / updated_at (and ensure they match DB column types).

Today they’re untyped; aligning to datetime (or whatever the DB layer expects) reduces accidental misuse.

app/database/queries/breeze_buddy/call_execution_config.py (2)

15-68: Timezone consistency: prefer UTC-aware timestamps if DB columns are timestamptz.

If created_at/updated_at are stored as timezone-aware, use datetime.now(timezone.utc) (and import timezone) to avoid mixing naive/aware datetimes.


95-178: Ensure there’s an index supporting the new (merchant_id, template, shop_identifier) lookup.

The update now predicates on merchant_id + template (+ shop_identifier / IS NULL). Without an index, this can become a hot-path scan as data grows.

app/ai/voice/agents/breeze_buddy/template/builder.py (1)

259-276: Missing handler for ActionType.END_CONVERSATION in _build_action.

The ActionType enum includes END_CONVERSATION, but _build_action only handles TTS_SAY and FUNCTION explicitly. When END_CONVERSATION is passed, it falls through to the else branch returning {"type": str(action_type)} which loses the enum value format.

If END_CONVERSATION actions should invoke the end_conversation handler from the handler_map, consider adding explicit handling:

         elif action_type == ActionType.FUNCTION:
             handler = self.handler_map.get(action.handler)
             if not handler:
                 logger.error(f"Handler not found for name: {action.handler}")
                 raise ValueError(f"Handler not found for name: {action.handler}")
             logger.debug(
                 f"Successfully built FUNCTION action for handler: {action.handler}"
             )
             return {"type": "function", "handler": handler}
+        elif action_type == ActionType.END_CONVERSATION:
+            handler = self.handler_map.get("end_conversation")
+            if not handler:
+                logger.error("end_conversation handler not found in handler_map")
+                raise ValueError("end_conversation handler not found in handler_map")
+            logger.debug("Successfully built END_CONVERSATION action")
+            return {"type": "function", "handler": handler}
         else:
             logger.warning(f"Unknown action type: {action_type}, returning as string")
             return {"type": str(action_type)}
app/ai/voice/agents/breeze_buddy/template/hooks.py (1)

264-264: Class variable _hooks should be annotated with ClassVar.

The mutable class attribute _hooks should use typing.ClassVar annotation to indicate it's shared across all instances (though this is a classmethod-only registry).

+from typing import Any, ClassVar, Dict, Optional
-from typing import Any, Dict, Optional

 class HookRegistry:
     """
     Registry for all available hooks.

     This class maintains a mapping of hook names to hook instances.
     """

-    _hooks: Dict[str, Hook] = {}
+    _hooks: ClassVar[Dict[str, Hook]] = {}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ce7896 and 7f78bf4.

📒 Files selected for processing (35)
  • app/ai/voice/agents/breeze_buddy/agent.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/dashboard/login.html (1 hunks)
  • app/ai/voice/agents/breeze_buddy/examples/templates/order-confirmation.json (1 hunks)
  • app/ai/voice/agents/breeze_buddy/handlers/internal/__init__.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/managers/calls.py (6 hunks)
  • app/ai/voice/agents/breeze_buddy/services/telephony/exotel/exotel.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/services/telephony/twilio/twilio.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/template/__init__.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/template/builder.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/template/context.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/template/hooks.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/template/loader.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/template/transition.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/template/types.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/types/models.py (2 hunks)
  • app/ai/voice/agents/breeze_buddy/utils/common.py (1 hunks)
  • app/ai/voice/agents/breeze_buddy/websocket_bot.py (3 hunks)
  • app/ai/voice/agents/breeze_buddy/workflows/order_confirmation/utils.py (0 hunks)
  • app/api/routers/breeze_buddy.py (10 hunks)
  • app/database/accessor/__init__.py (1 hunks)
  • app/database/accessor/breeze_buddy/call_execution_config.py (6 hunks)
  • app/database/accessor/breeze_buddy/lead_call_tracker.py (3 hunks)
  • app/database/accessor/breeze_buddy/template.py (1 hunks)
  • app/database/decoder/breeze_buddy/call_execution_config.py (3 hunks)
  • app/database/decoder/breeze_buddy/lead_call_tracker.py (1 hunks)
  • app/database/decoder/breeze_buddy/template.py (1 hunks)
  • app/database/migrations/001_initial_tables.sql (1 hunks)
  • app/database/migrations/002_add_shop_identifier_and_locks.sql (1 hunks)
  • app/database/migrations/003_create_template_table.sql (1 hunks)
  • app/database/queries/breeze_buddy/call_execution_config.py (7 hunks)
  • app/database/queries/breeze_buddy/lead_call_tracker.py (5 hunks)
  • app/database/queries/breeze_buddy/template.py (1 hunks)
  • app/schemas.py (3 hunks)
  • scripts/create_tables.py (0 hunks)
💤 Files with no reviewable changes (2)
  • scripts/create_tables.py
  • app/ai/voice/agents/breeze_buddy/workflows/order_confirmation/utils.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/database/migrations/002_add_shop_identifier_and_locks.sql
  • app/ai/voice/agents/breeze_buddy/services/telephony/twilio/twilio.py
  • app/database/accessor/init.py
  • app/database/migrations/003_create_template_table.sql
  • app/ai/voice/agents/breeze_buddy/services/telephony/exotel/exotel.py
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: badri-singhal
Repo: juspay/clairvoyance PR: 313
File: app/api/routers/breeze_buddy.py:487-489
Timestamp: 2025-10-15T04:10:50.071Z
Learning: In the Breeze Buddy dashboard (`app/agents/voice/breeze_buddy/workflows/order_confirmation/dashboard.html`), analytics metrics are dynamically rendered using a for loop in the HTML script. When adding new metrics to the JSON response in the `get_analytics()` endpoint, no manual HTML template updates are required as the loop handles all metrics automatically.
📚 Learning: 2025-10-15T04:10:50.071Z
Learnt from: badri-singhal
Repo: juspay/clairvoyance PR: 313
File: app/api/routers/breeze_buddy.py:487-489
Timestamp: 2025-10-15T04:10:50.071Z
Learning: In the Breeze Buddy dashboard (`app/agents/voice/breeze_buddy/workflows/order_confirmation/dashboard.html`), analytics metrics are dynamically rendered using a for loop in the HTML script. When adding new metrics to the JSON response in the `get_analytics()` endpoint, no manual HTML template updates are required as the loop handles all metrics automatically.

Applied to files:

  • app/ai/voice/agents/breeze_buddy/examples/templates/order-confirmation.json
  • app/ai/voice/agents/breeze_buddy/dashboard/login.html
  • app/api/routers/breeze_buddy.py
📚 Learning: 2025-12-10T11:55:16.753Z
Learnt from: narsimhaReddyJuspay
Repo: juspay/clairvoyance PR: 414
File: app/services/langfuse/tasks/score_monitor/score.py:469-489
Timestamp: 2025-12-10T11:55:16.753Z
Learning: In the Breeze Buddy call tracking system, a lead can only have one outcome type among CONFIRM, CANCEL, or ADDRESS_UPDATED—these outcomes are mutually exclusive per lead.

Applied to files:

  • app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py
🧬 Code graph analysis (14)
app/database/accessor/breeze_buddy/call_execution_config.py (2)
app/schemas.py (2)
  • CallExecutionConfig (103-116)
  • CallProvider (16-18)
app/database/queries/breeze_buddy/call_execution_config.py (1)
  • update_call_execution_config_query (95-178)
app/database/decoder/breeze_buddy/template.py (1)
app/ai/voice/agents/breeze_buddy/template/types.py (1)
  • TemplateModel (60-68)
app/ai/voice/agents/breeze_buddy/template/context.py (1)
app/ai/voice/agents/breeze_buddy/template/hooks.py (1)
  • get (278-288)
app/database/decoder/breeze_buddy/call_execution_config.py (1)
app/schemas.py (2)
  • CallExecutionConfig (103-116)
  • CallProvider (16-18)
app/ai/voice/agents/breeze_buddy/template/loader.py (3)
app/ai/voice/agents/breeze_buddy/template/types.py (1)
  • TemplateModel (60-68)
app/database/accessor/breeze_buddy/template.py (1)
  • get_template_by_merchant (29-52)
app/ai/voice/agents/breeze_buddy/template/hooks.py (1)
  • get (278-288)
app/ai/voice/agents/breeze_buddy/template/builder.py (4)
app/ai/voice/agents/breeze_buddy/template/transition.py (1)
  • transition_handler (19-69)
app/ai/voice/agents/breeze_buddy/template/types.py (5)
  • ActionType (11-14)
  • FlowAction (17-20)
  • FlowFunction (42-48)
  • FlowNodeModel (51-57)
  • TemplateModel (60-68)
app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py (4)
  • mute_stt (23-44)
  • unmute_stt (47-68)
  • play_audio_sound (71-117)
  • end_conversation (120-217)
app/ai/voice/agents/breeze_buddy/template/hooks.py (1)
  • get (278-288)
app/ai/voice/agents/breeze_buddy/utils/common.py (2)
app/core/security/sha.py (1)
  • calculate_hmac_sha256 (6-27)
app/schemas.py (1)
  • LeadCallOutcome (28-34)
app/ai/voice/agents/breeze_buddy/websocket_bot.py (2)
app/ai/voice/agents/breeze_buddy/types/models.py (1)
  • OrderData (11-12)
app/ai/voice/agents/breeze_buddy/template/context.py (1)
  • call_sid (67-69)
app/ai/voice/agents/breeze_buddy/template/hooks.py (3)
app/ai/voice/agents/breeze_buddy/template/context.py (3)
  • context (57-59)
  • TemplateContext (14-132)
  • lead (62-64)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
  • update_lead_call_completion_details (281-311)
app/schemas.py (1)
  • LeadCallOutcome (28-34)
app/ai/voice/agents/breeze_buddy/types/models.py (1)
app/ai/voice/agents/breeze_buddy/template/context.py (1)
  • reporting_webhook_url (77-79)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
app/schemas.py (3)
  • LeadCallStatus (21-25)
  • LeadCallOutcome (28-34)
  • LeadCallTracker (37-56)
app/api/routers/breeze_buddy.py (6)
app/ai/voice/agents/breeze_buddy/template/types.py (1)
  • CreateTemplateRequest (92-101)
app/ai/voice/agents/breeze_buddy/types/models.py (3)
  • LeadData (23-33)
  • LoginRequest (36-38)
  • PushLeadRequest (15-20)
app/ai/voice/agents/breeze_buddy/utils/common.py (2)
  • get_validation_error_message (321-338)
  • validate_payload (152-212)
app/database/accessor/breeze_buddy/template.py (2)
  • get_template_by_merchant (29-52)
  • create_template (55-98)
app/database/accessor/breeze_buddy/call_execution_config.py (2)
  • update_call_execution_config (148-195)
  • get_call_execution_config_by_merchant_id (79-122)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
  • create_lead_call_tracker (43-87)
app/ai/voice/agents/breeze_buddy/managers/calls.py (2)
app/schemas.py (2)
  • LeadCallOutcome (28-34)
  • LeadCallTracker (37-56)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
  • update_lead_call_completion_details (281-311)
app/database/queries/breeze_buddy/lead_call_tracker.py (1)
app/schemas.py (2)
  • LeadCallOutcome (28-34)
  • LeadCallStatus (21-25)
🪛 Ruff (0.14.8)
app/ai/voice/agents/breeze_buddy/template/transition.py

53-55: Store a reference to the return value of asyncio.create_task

(RUF006)

app/ai/voice/agents/breeze_buddy/template/__init__.py

27-36: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

app/ai/voice/agents/breeze_buddy/template/loader.py

115-117: Avoid specifying long messages outside the exception class

(TRY003)

app/ai/voice/agents/breeze_buddy/template/builder.py

64-64: Avoid specifying long messages outside the exception class

(TRY003)


69-69: Avoid specifying long messages outside the exception class

(TRY003)


76-76: Avoid specifying long messages outside the exception class

(TRY003)


102-102: Avoid specifying long messages outside the exception class

(TRY003)


200-200: Avoid specifying long messages outside the exception class

(TRY003)


269-269: Avoid specifying long messages outside the exception class

(TRY003)

app/ai/voice/agents/breeze_buddy/utils/common.py

102-102: Do not catch blind exception: Exception

(BLE001)


143-143: Consider moving this statement to an else block

(TRY300)


144-144: Do not catch blind exception: Exception

(BLE001)


305-305: Unused lambda argument: v

(ARG005)

app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py

23-23: Unused function argument: args

(ARG001)


23-23: Unused function argument: transition_to

(ARG001)


47-47: Unused function argument: args

(ARG001)


47-47: Unused function argument: transition_to

(ARG001)


71-71: Unused function argument: transition_to

(ARG001)


107-107: Do not catch blind exception: Exception

(BLE001)


120-120: Unused function argument: args

(ARG001)


120-120: Unused function argument: transition_to

(ARG001)


204-204: Do not catch blind exception: Exception

(BLE001)


206-206: Use explicit conversion flag

Replace with conversion flag

(RUF010)


220-220: Unused function argument: transition_to

(ARG001)


314-314: Do not catch blind exception: Exception

(BLE001)


326-326: Do not catch blind exception: Exception

(BLE001)

app/ai/voice/agents/breeze_buddy/agent.py

118-118: Do not catch blind exception: Exception

(BLE001)


152-152: Do not catch blind exception: Exception

(BLE001)


202-202: Do not catch blind exception: Exception

(BLE001)


215-215: Do not catch blind exception: Exception

(BLE001)


334-334: Unused function argument: transport

(ARG001)


361-361: Unused function argument: transport

(ARG001)


366-366: Unused function argument: task

(ARG001)

app/database/queries/breeze_buddy/template.py

12-12: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


12-12: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


26-31: Possible SQL injection vector through string-based query construction

(S608)


48-52: Possible SQL injection vector through string-based query construction

(S608)

app/ai/voice/agents/breeze_buddy/template/hooks.py

80-80: Do not catch blind exception: Exception

(BLE001)


82-82: Use explicit conversion flag

Replace with conversion flag

(RUF010)


249-249: Do not catch blind exception: Exception

(BLE001)


264-264: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

app/database/accessor/breeze_buddy/template.py

30-30: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


30-30: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


48-48: Consider moving this statement to an else block

(TRY300)


50-50: Do not catch blind exception: Exception

(BLE001)


94-94: Consider moving this statement to an else block

(TRY300)


96-96: Do not catch blind exception: Exception

(BLE001)

app/api/routers/breeze_buddy.py

76-76: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


88-88: Abstract raise to an inner function

(TRY301)


88-88: Avoid specifying long messages outside the exception class

(TRY003)


91-91: Abstract raise to an inner function

(TRY301)


91-91: Avoid specifying long messages outside the exception class

(TRY003)


94-94: Abstract raise to an inner function

(TRY301)


94-94: Avoid specifying long messages outside the exception class

(TRY003)


103-106: Abstract raise to an inner function

(TRY301)


122-122: Abstract raise to an inner function

(TRY301)


138-138: Do not catch blind exception: Exception

(BLE001)


365-365: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


456-456: Redefinition of unused trigger_order_confirmation from line 361

(F811)


458-458: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


566-566: Do not catch blind exception: Exception

(BLE001)


570-570: Use explicit conversion flag

Replace with conversion flag

(RUF010)


751-751: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

app/database/queries/breeze_buddy/lead_call_tracker.py

233-238: Possible SQL injection vector through string-based query construction

(S608)

app/ai/voice/agents/breeze_buddy/handlers/internal/__init__.py

15-21: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

🔇 Additional comments (36)
app/ai/voice/agents/breeze_buddy/dashboard/login.html (1)

375-375: LGTM! Redirect path updated to template-based dashboard.

The redirect URL has been correctly updated to align with the new template-based architecture, simplifying the path from the workflow-specific route to a unified dashboard route.

app/ai/voice/agents/breeze_buddy/types/models.py (2)

15-20: LGTM! New PushLeadRequest model aligns with template-based architecture.

The model is well-structured with appropriate field types. The flexible Dict[str, Any] payload allows dynamic template data, and the optional reporting_webhook_url supports callback notifications.


23-33: LGTM! LeadData model captures all necessary customer and order information.

The model includes all essential fields for order confirmation workflow, with appropriate optional fields for shop_identifier, identity, and reporting_webhook_url.

app/ai/voice/agents/breeze_buddy/examples/templates/order-confirmation.json (2)

1-23: LGTM! Template metadata and schema definition are well-structured.

The template header clearly defines the merchant, template name, active status, description, and expected payload schema. The schema uses standard JSON Schema conventions for type validation.


24-498: Excellent template-based workflow design.

The flow configuration is comprehensive and well-architected:

  • Clear node structure with appropriate task messages and actions
  • Proper function definitions with transitions and hooks
  • Handles all major outcomes (confirmation, cancellation, address updates, busy user)
  • Includes database update hooks for tracking outcomes
  • Uses pre/post actions for STT muting and conversation management

This template-driven approach provides excellent flexibility for workflow customization without code changes.

app/ai/voice/agents/breeze_buddy/websocket_bot.py (2)

38-46: LGTM! Import paths updated to new module structure.

The imports have been correctly updated to reference the new types.models and utils.common modules, aligning with the template-based architecture refactor.


404-404: LGTM! Multilingual support added to system prompt.

The instruction to reply in the user's language enhances the bot's ability to handle multilingual conversations naturally.

app/ai/voice/agents/breeze_buddy/managers/calls.py (3)

10-12: LGTM! Import updated to new utils module.

The import has been correctly updated to reference the new centralized utils.common module.


57-60: LGTM! Consistent migration from workflow to template terminology.

The field references have been correctly updated from workflow to template, aligning with the new template-based architecture throughout the codebase.

Also applies to: 139-139


374-412: LGTM! Improved signature with optional parameters and return value.

The signature changes improve flexibility:

  • Optional outcome, call_end_time, and meta_data parameters allow partial updates
  • Returning Optional[LeadCallTracker] enables callers to access the updated lead state
  • The nullable approach handles cases where data may not be available
app/ai/voice/agents/breeze_buddy/utils/common.py (6)

14-21: LGTM! Outcome mapping dictionary is well-defined.

The mapping correctly translates outcome strings to LeadCallOutcome enum values, supporting the outcome tracking system.


24-65: LGTM! Indian number formatting correctly implements the lakhs-crores system.

The function properly handles the Indian numbering system with appropriate grouping (hundreds, thousands, lakhs, crores) and produces natural speech-friendly output.


68-110: LGTM! Webhook retry logic is robust with proper signing.

The function includes:

  • HMAC-SHA256 signature for security
  • Configurable retry attempts with detailed logging
  • Proper error handling and status checking
  • No sleep after final attempt (efficient)

The broad exception catching at line 102 is appropriate here as any network or serialization error should trigger a retry.


113-149: LGTM! Audio loading handles errors gracefully.

The function properly:

  • Checks file existence before loading
  • Converts to the correct format (8000 Hz, mono, 16-bit PCM)
  • Returns None on failure with appropriate logging

The broad exception catching at line 144 is appropriate for handling various audio processing errors (codec issues, file corruption, etc.).


152-318: LGTM! Payload validation provides comprehensive schema checking.

The validation functions support:

  • Nested objects and arrays
  • Recursive validation with path tracking
  • Multiple type validators (string, number, boolean, array, object, null, any)
  • Graceful handling of unknown types and null values
  • Clear error messages with field paths

The _validate_type function at line 305 includes lambda v: True for the "any" type validator, which intentionally accepts any value. The unused v parameter is required by the lambda signature for consistency with other validators - this is not a real issue.


321-338: LGTM! Error message formatting provides clear user feedback.

The function formats single or multiple validation errors into a readable message format.

app/database/decoder/breeze_buddy/call_execution_config.py (1)

21-38: Decoder update to template looks consistent with CallExecutionConfig.

Also applies to: 50-65

app/database/decoder/breeze_buddy/lead_call_tracker.py (1)

17-44: Decoder update to template looks correct.

app/ai/voice/agents/breeze_buddy/template/transition.py (1)

72-113: Hook execution flow looks clean and easy to follow.

app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py (2)

23-69: mute/unmute look fine; just ensure VAD param mutation is safe for your pipeline concurrency model.


220-332: Outcome type mismatch corrupts webhook payload: context.lead.outcome is an enum, but OUTCOME_TO_ENUM expects string keys.

context.lead.outcome is deserialized from the database as a LeadCallOutcome enum object. However, OUTCOME_TO_ENUM.get(outcome, LeadCallOutcome.BUSY) expects lowercase string keys like "confirmed" and "cancelled". When outcome is already an enum (which it always is when loaded from the database), the lookup fails silently and defaults to LeadCallOutcome.BUSY, corrupting the webhook summary data with the wrong outcome regardless of the actual call result.

Convert enum to string value before mapping: outcome.value if isinstance(outcome, LeadCallOutcome) else outcome, or use outcome.value directly since the enum is guaranteed to exist when deserialized.

⛔ Skipped due to learnings
Learnt from: narsimhaReddyJuspay
Repo: juspay/clairvoyance PR: 414
File: app/services/langfuse/tasks/score_monitor/score.py:469-489
Timestamp: 2025-12-10T11:55:16.753Z
Learning: In the Breeze Buddy call tracking system, a lead can only have one outcome type among CONFIRM, CANCEL, or ADDRESS_UPDATED—these outcomes are mutually exclusive per lead.
app/ai/voice/agents/breeze_buddy/template/context.py (1)

101-133: The code is correct. flow_config["nodes"] is constructed as a dict keyed by node name in the builder (build_flow_config returns {"initial_node": initial_node_name, "nodes": nodes} where nodes is explicitly built as nodes = {} and populated with nodes[node.node_name] = self._build_node(node)). The create_node_from_template() method properly treats it as a dict and all accesses are valid.

app/ai/voice/agents/breeze_buddy/template/loader.py (1)

86-180: The code is functioning as designed without downstream compatibility risks. When load_template() converts messages to dicts and renders them, the returned template_obj is immediately passed to flow_builder.build_flow_config(), which explicitly calls FlowNodeModel.model_validate() on each node. This Pydantic validation automatically reconstructs TaskMessage objects from the rendered dicts since they contain the required role and content fields. The dict-to-object conversion is seamless and does not break the builder or any downstream consumers.

app/ai/voice/agents/breeze_buddy/template/builder.py (2)

43-116: LGTM on the flow config building logic.

The validation and transformation logic is thorough - checking for empty flow, missing initial_node, empty nodes list, and verifying the initial node exists. The function_name to name transformation handles backward compatibility well.


223-233: Closure captures loop variable by reference - potential bug.

The wrapper_handler closure captures func.transition_to, func.name, and hooks which are correctly scoped. However, this pattern is safe here because each closure is created with its own func from the list comprehension in _build_node. The implementation correctly captures the values at function schema creation time.

app/ai/voice/agents/breeze_buddy/template/hooks.py (1)

26-85: LGTM on Hook base class design.

The abstract base class with safe_execute wrapper provides good error isolation for async hook execution. The logging is appropriate for debugging hook lifecycle.

app/ai/voice/agents/breeze_buddy/agent.py (4)

155-159: Good fix for potential AttributeError.

The Exotel handling now correctly uses call_data.get("start", {}) to avoid the AttributeError when "start" key is missing. This addresses the previous review comment.


179-182: LGTM on handler context wrapping.

The pattern of wrapping all handlers with context before building the flow config ensures that each handler receives the bot context. This enables the handlers to access bot state like lead, vad_analyzer, and transport.


221-228: Missing fields from schema silently default to empty string.

When a field from expected_payload_schema is not found in call_payload, it's set to an empty string with only a warning log. This could cause subtle bugs if templates expect non-empty values.

Consider whether this should be a validation error for required fields, or at minimum, ensure templates handle empty string values gracefully.


333-368: Unused function arguments in event handlers are intentional.

The transport, client, and task arguments in the event handlers are required by the Pipecat event handler signature but unused in the implementation. This is expected for event handler callbacks. The static analysis hints (ARG001) are false positives in this context.

app/database/accessor/breeze_buddy/lead_call_tracker.py (2)

43-87: LGTM on template parameter rename.

The change from workflow to template is consistent with the broader template-based architecture migration. The function signature and call to insert_lead_call_tracker_query are properly aligned.


281-311: LGTM on partial update support.

The updated update_lead_call_completion_details function now correctly supports partial updates by making all fields optional except id. The docstring clearly documents this behavior. This aligns with the hook system's need to update only specific fields (like outcome) without overwriting other fields.

app/database/queries/breeze_buddy/lead_call_tracker.py (2)

17-71: LGTM on template parameter change in insert query.

The column and parameter rename from workflow to template is consistent with the schema migration. The parameterized query pattern is preserved.


196-240: Dynamic query construction is safe - parameterized values.

The static analysis S608 warning about SQL injection is a false positive. The dynamic SET clause construction:

  1. Uses hardcoded column names (not user input)
  2. Uses parameterized placeholders ($1, $2, etc.) for all values
  3. Values are passed separately to the query executor

The pattern correctly prevents SQL injection while enabling partial updates.

app/api/routers/breeze_buddy.py (1)

469-543: Well-implemented payload validation flow.

The template-based validation logic is robust:

  • Fetches template with proper error handling
  • Validates payload against expected schema when present
  • Provides detailed error messages via get_validation_error_message
  • Falls back gracefully when no schema is defined

This implementation correctly leverages the new template-based architecture.

app/ai/voice/agents/breeze_buddy/template/types.py (1)

1-101: Well-structured Pydantic models for template-based architecture.

The model definitions are clean and comprehensive:

  • Clear separation between internal models (FlowNodeModel, TemplateModel) and API request models (RequestFlowNode, CreateTemplateRequest)
  • Proper use of Optional for nullable fields with sensible defaults
  • The enum-based ActionType provides type safety for flow actions
  • Hook system (HookConfig, HookFieldConfig) enables extensible action handling
  • The schema change documented at line 100 (Dict[str, str] → Dict[str, Any]) correctly supports nested validation schemas

These models establish a solid foundation for the template-based workflow engine.

Comment on lines +345 to +358
initial_node_config = NodeConfig(
name=initial_node_config["name"],
task_messages=initial_node_config["task_messages"],
role_messages=initial_node_config.get("role_messages", []),
functions=initial_node_config["functions"],
pre_actions=initial_node_config["pre_actions"],
post_actions=initial_node_config["post_actions"],
)
Copy link

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 examine the file structure and locate the relevant functions
cd app/ai/voice/agents/breeze_buddy && cat -n agent.py | head -50

Repository: juspay/clairvoyance

Length of output: 182


🏁 Script executed:

# Find the build_flow_config function definition
rg -n "def build_flow_config" app/ai/voice/agents/breeze_buddy/

Repository: juspay/clairvoyance

Length of output: 194


🏁 Script executed:

# Find the _build_node function definition
rg -n "def _build_node" app/ai/voice/agents/breeze_buddy/

Repository: juspay/clairvoyance

Length of output: 181


🏁 Script executed:

# Search for NodeConfig class definition
rg -n "class NodeConfig" app/

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Get context around lines 345-352 to understand the full picture
sed -n '330,360p' app/ai/voice/agents/breeze_buddy/agent.py

Repository: juspay/clairvoyance

Length of output: 1491


🏁 Script executed:

# Get the full implementation of build_flow_config
sed -n '43,100p' app/ai/voice/agents/breeze_buddy/template/builder.py

Repository: juspay/clairvoyance

Length of output: 2294


🏁 Script executed:

# Also check what NodeConfig looks like and if it has dict-like behavior
rg -n "class NodeConfig" app/ -A 10

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Check imports in agent.py to find where NodeConfig comes from
head -50 app/ai/voice/agents/breeze_buddy/agent.py | grep -E "import|from"

Repository: juspay/clairvoyance

Length of output: 1298


🏁 Script executed:

# Get the rest of build_flow_config to see how it constructs the return dict
sed -n '100,150p' app/ai/voice/agents/breeze_buddy/template/builder.py

Repository: juspay/clairvoyance

Length of output: 1899


🏁 Script executed:

# Search for NodeConfig definition in the entire codebase
rg "class NodeConfig"

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Search for how NodeConfig is used throughout the codebase to see if dict-like access is common
rg "NodeConfig\[" app/

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Search for NodeConfig instantiation or method calls to understand its interface
rg "NodeConfig\(" app/ -A 2

Repository: juspay/clairvoyance

Length of output: 893


🏁 Script executed:

# Check if there are other places where nodes dict values are accessed like dicts
rg "nodes\[" app/ai/voice/agents/breeze_buddy/ -B 2 -A 2

Repository: juspay/clairvoyance

Length of output: 942


🏁 Script executed:

# Get the full _build_node implementation including return statement
sed -n '118,180p' app/ai/voice/agents/breeze_buddy/template/builder.py

Repository: juspay/clairvoyance

Length of output: 2425


🏁 Script executed:

# Check if NodeConfig objects are ever converted to dicts anywhere
rg "\.model_dump\|\.dict\(" app/ai/voice/agents/breeze_buddy/template/builder.py

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Check if there are any tests or other code that show how NodeConfig is typically used
rg "from pipecat_flows" app/ -A 5

Repository: juspay/clairvoyance

Length of output: 1972


🏁 Script executed:

# Look for any comments or documentation about how flow_config is structured
rg "flow_config" app/ai/voice/agents/breeze_buddy/agent.py -B 2 -A 2

Repository: juspay/clairvoyance

Length of output: 794


🏁 Script executed:

# Check the context_manager in template to see how it uses nodes
sed -n '1,50p' app/ai/voice/agents/breeze_buddy/template/context.py

Repository: juspay/clairvoyance

Length of output: 1243


🏁 Script executed:

# Check websocket_bot.py to see how it creates initial_node_config - does it also do reconstruction?
sed -n '1,100p' app/ai/voice/agents/breeze_buddy/websocket_bot.py | grep -A 30 "initial_node_config"

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Look for any error handling or comments around the problematic code
sed -n '340,360p' app/ai/voice/agents/breeze_buddy/agent.py

Repository: juspay/clairvoyance

Length of output: 1014


🏁 Script executed:

# Search for how flow_config structure is documented or used elsewhere
rg "flow_config\[" app/ai/voice/agents/breeze_buddy/ -B 1 -A 1

Repository: juspay/clairvoyance

Length of output: 923


🏁 Script executed:

# Check how flow_manager.initialize is typed - what does it expect?
rg "def initialize" app/ -A 3

Repository: juspay/clairvoyance

Length of output: 2060


🏁 Script executed:

# Verify the exact pattern by checking if there are other places where NodeConfig is used directly without reconstruction
rg "NodeConfig" app/ai/voice/agents/breeze_buddy/ -B 2 -A 2 | head -80

Repository: juspay/clairvoyance

Length of output: 5969


🏁 Script executed:

# Check the template/context.py implementation to see how it uses the NodeConfig objects
sed -n '70,110p' app/ai/voice/agents/breeze_buddy/template/context.py

Repository: juspay/clairvoyance

Length of output: 1058


🏁 Script executed:

# Get the full implementation of create_node_from_template
sed -n '100,140p' app/ai/voice/agents/breeze_buddy/template/context.py

Repository: juspay/clairvoyance

Length of output: 1447


Remove redundant NodeConfig reconstruction.

The initial_node_config retrieved from self.flow_config["nodes"][initial_node_name] is already a NodeConfig object created by _build_node and stored in the flow config dictionary. The reconstruction at lines 345-352 is unnecessary—simply use the object directly:

initial_node_config = self.flow_config["nodes"][initial_node_name]
await self.flow_manager.initialize(initial_node_config)

This is confirmed by TemplateContext.create_node_from_template() in template/context.py, which returns nodes[node_name] directly without reconstruction.

🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/agent.py around lines 345 to 352, the code
reconstructs a NodeConfig from initial_node_config dict but the value in
self.flow_config["nodes"][initial_node_name] is already a NodeConfig created by
_build_node; remove the redundant NodeConfig(...) reconstruction and instead
assign initial_node_config = self.flow_config["nodes"][initial_node_name] (or
simply reuse the existing variable) and pass that object to await
self.flow_manager.initialize(initial_node_config).

Comment on lines 15 to 21
__all__ = [
"mute_stt",
"unmute_stt",
"play_audio_sound",
"end_conversation",
"service_callback",
]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ruff RUF022: sort __all__ (or add a targeted ignore).

If Ruff is enforced, this will fail lint. Suggest:

 __all__ = [
-    "mute_stt",
-    "unmute_stt",
-    "play_audio_sound",
     "end_conversation",
+    "mute_stt",
+    "play_audio_sound",
     "service_callback",
+    "unmute_stt",
 ]
📝 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
__all__ = [
"mute_stt",
"unmute_stt",
"play_audio_sound",
"end_conversation",
"service_callback",
]
__all__ = [
"end_conversation",
"mute_stt",
"play_audio_sound",
"service_callback",
"unmute_stt",
]
🧰 Tools
🪛 Ruff (0.14.8)

15-21: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/handlers/internal/__init__.py around lines
15 to 21, the module-level __all__ list is not sorted which triggers Ruff
RUF022; reorder the strings in __all__ into a consistent lexicographic
(alphabetical) order (e.g. end_conversation, mute_stt, play_audio_sound,
service_callback, unmute_stt) so the linter passes, or if intentional keep the
current order add a targeted ignore comment for RUF022 on that line.

Comment on lines 71 to 118
async def play_audio_sound(context: TemplateContext, args, transition_to=None):
"""
Play an audio sound file through the transport.
This handler:
1. Loads an audio file from the specified path
2. Writes it to the transport output for playback
Args:
context: Handler context with bot state access
args: Additional arguments - should contain 'audio_path' key
transition_to: Target node to transition to (not used for action handlers)
Returns:
Empty dict
"""
logger.debug(
f"play_audio_sound called for call {context.call_sid} with args: {args}"
)

# Get audio path from args, default to a common audio file if not specified
audio_path = args.get(
"audio_path", "app/ai/voice/agents/breeze_buddy/static/audio/cough.wav"
)

logger.info(
f"Attempting to play audio from: {audio_path} for call {context.call_sid}"
)

audio = load_audio(audio_path=audio_path)
if audio:
try:
await context.transport.output().write_audio_frame(audio)
logger.info(
f"Successfully played audio from {audio_path} for call {context.call_sid}"
)
except Exception as e:
logger.error(
f"Failed to play audio from {audio_path} for call {context.call_sid}: {e}",
exc_info=True,
)
else:
logger.warning(
f"Audio not loaded from {audio_path} for call {context.call_sid}, skipping playback"
)

return {}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Security: audio_path from args can enable path traversal / arbitrary file reads.

If args is template/merchant-controlled, restrict to an allowlist (e.g., known filenames) or enforce that the resolved path stays within .../static/audio/ before calling load_audio().

🧰 Tools
🪛 Ruff (0.14.8)

71-71: Unused function argument: transition_to

(ARG001)


107-107: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py around
lines 71 to 118, the handler uses audio_path from args directly which allows
path traversal/arbitrary file reads; fix by validating/sanitizing audio_path
before calling load_audio: restrict to an allowlist of known filenames OR
construct the path by joining a fixed base directory (e.g.,
<repo>/app/ai/voice/agents/breeze_buddy/static/audio/) with a sanitized
filename, resolve the resulting path with os.path.realpath and verify it starts
with the base dir, reject/raise/log and skip playback on invalid input; ensure
you do not allow paths containing ../ or absolute paths and log the rejection
with context.

Comment on lines 27 to 36
__all__ = [
"FlowConfigLoader",
"FlowConfigBuilder",
"Hook",
"HookRegistry",
"UpdateOutcomeInDatabaseHook",
"transition_handler",
"TemplateContext",
"with_context",
]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ruff RUF022: sort __all__ (or ignore locally).

🧰 Tools
🪛 Ruff (0.14.8)

27-36: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/template/__init__.py around lines 27 to 36
the __all__ list is not alphabetically sorted which triggers Ruff RUF022; either
sort the entries in __all__ alphabetically (e.g. FlowConfigBuilder,
FlowConfigLoader, Hook, HookRegistry, TemplateContext,
UpdateOutcomeInDatabaseHook, transition_handler, with_context) or, if sorting is
undesirable, add a local ignore comment (e.g. # noqa: RUF022) to the __all__
assignment to silence the linter.

Comment on lines +135 to +215
def with_context(bot_instance):
"""
Decorator factory that injects TemplateContext into handler functions.
Usage:
@with_context(bot)
async def my_handler(context, flow_manager, args):
# context is TemplateContext instance
context.outcome = "confirmed"
Args:
bot_instance: The OrderConfirmationBot instance
Returns:
Decorator function
"""

def decorator(handler_func: Callable) -> Callable:
"""
Decorator that wraps handler with context injection.
Args:
handler_func: Handler function to wrap
Returns:
Wrapped handler function
"""

async def wrapper(*args, **kwargs):
# Create context from bot instance
context = TemplateContext(bot_instance)

transition_to = kwargs.pop("transition_to", None)
hooks = kwargs.pop("hooks", None)
function_name = kwargs.pop("function_name", None)

is_transition_handler = hooks is not None or function_name is not None

llm_args = args[0] if len(args) > 0 else {}

logger.debug(
f"with_context wrapper called - handler: {handler_func.__name__}, "
f"is_transition_handler: {is_transition_handler}, "
f"transition_to: {transition_to}, hooks: {hooks}, function_name: {function_name}"
)

if is_transition_handler:
logger.debug(
f"Calling transition handler '{handler_func.__name__}' "
f"for function '{function_name}'"
)
return await handler_func(
context,
llm_args,
transition_to=transition_to,
hooks=hooks,
function_name=function_name,
)
else:
# Action handlers don't need hooks/function_name
logger.debug(
f"Calling action handler '{handler_func.__name__}' "
f"with transition_to: {transition_to}"
)
return await handler_func(
context, llm_args, transition_to=transition_to
)

return wrapper

return decorator
Copy link

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:

# First, let's understand the structure of the repository
git ls-files | head -20

Repository: juspay/clairvoyance

Length of output: 713


🏁 Script executed:

# Find the context.py file and explore the surrounding code
find . -name "context.py" -path "*/breeze_buddy/*" -type f

Repository: juspay/clairvoyance

Length of output: 117


🏁 Script executed:

# Let's also look for files that might use this decorator or define handlers
fd -e py -path "*/breeze_buddy/*" | head -30

Repository: juspay/clairvoyance

Length of output: 231


🏁 Script executed:

# Let's read the context.py file to understand the full context
cat -n ./app/ai/voice/agents/breeze_buddy/template/context.py

Repository: juspay/clairvoyance

Length of output: 7335


🏁 Script executed:

# Search for files that use the @with_context decorator
rg "@with_context" --type py -A 5 -B 2

Repository: juspay/clairvoyance

Length of output: 687


🏁 Script executed:

# Look for handler function definitions that might use this decorator
rg "async def.*handler" --type py -A 3 -B 1

Repository: juspay/clairvoyance

Length of output: 6748


🏁 Script executed:

# Check for test files related to context or handlers
find . -name "*test*" -path "*/breeze_buddy/*" -type f

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Search for actual usage of @with_context decorator (excluding docstring)
rg "@with_context" --type py -B 2 -A 10 | grep -v "Usage:" | head -50

Repository: juspay/clairvoyance

Length of output: 993


🏁 Script executed:

# Look at the builder.py file to understand wrapper_handler context
cat -n ./app/ai/voice/agents/breeze_buddy/template/builder.py | head -150

Repository: juspay/clairvoyance

Length of output: 6307


🏁 Script executed:

# Search for where handlers are registered or called
rg "wrapper_handler\|with_context" --type py -B 3 -A 3 | head -100

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Look for any tests related to context or handlers
find . -path "*/test*" -name "*.py" -type f | xargs grep -l "with_context\|TemplateContext" 2>/dev/null

Repository: juspay/clairvoyance

Length of output: 47


🏁 Script executed:

# Get more of builder.py to see wrapper_handler
cat -n ./app/ai/voice/agents/breeze_buddy/template/builder.py | sed -n '140,250p'

Repository: juspay/clairvoyance

Length of output: 5030


🏁 Script executed:

# Look at the action_handlers module
cat -n ./app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py | head -100

Repository: juspay/clairvoyance

Length of output: 3983


🏁 Script executed:

# Check transition.py to see how transition_handler is implemented
cat -n ./app/ai/voice/agents/breeze_buddy/template/transition.py

Repository: juspay/clairvoyance

Length of output: 4722


🏁 Script executed:

# Search for any actual decorator application in the breeze_buddy codebase
rg "def.*handler|async def.*handler" app/ai/voice/agents/breeze_buddy/ --type py -B 1 | head -80

Repository: juspay/clairvoyance

Length of output: 2677


🏁 Script executed:

# Search for imports of with_context to see if it's used anywhere
rg "from.*context.*import.*with_context|import.*with_context" --type py

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Search for any reference to with_context outside of context.py
rg "with_context" --type py -l | grep -v "context.py"

Repository: juspay/clairvoyance

Length of output: 158


🏁 Script executed:

# Check if there are any commented-out usages of @with_context
rg "@with_context|with_context" app/ai/voice/agents/breeze_buddy/ --type py -C 3

Repository: juspay/clairvoyance

Length of output: 3746


🏁 Script executed:

# Check how wrapper_handler is called and what it receives from pipecat
cat -n ./app/ai/voice/agents/breeze_buddy/template/builder.py | sed -n '222,240p'

Repository: juspay/clairvoyance

Length of output: 982


🏁 Script executed:

# Look at agent.py to see the full context of how with_context is applied
cat -n ./app/ai/voice/agents/breeze_buddy/agent.py | grep -A 10 "for handler_name"

Repository: juspay/clairvoyance

Length of output: 589


🏁 Script executed:

# Look at more context around the handler wrapping in agent.py
rg "with_context\(self\)" app/ai/voice/agents/breeze_buddy/agent.py -B 5 -A 5

Repository: juspay/clairvoyance

Length of output: 559


🏁 Script executed:

# Let's verify the actual handler signatures in action_handlers
cat -n ./app/ai/voice/agents/breeze_buddy/handlers/internal/action_handlers.py | sed -n '23,45p'

Repository: juspay/clairvoyance

Length of output: 1039


🏁 Script executed:

# Check if there are tests that might reveal the actual behavior
find app -name "*test*.py" -o -name "test_*" -type f | head -10

Repository: juspay/clairvoyance

Length of output: 45


🏁 Script executed:

# Let's trace through one more time - check what flow_manager is
rg "flow_manager" app/ai/voice/agents/breeze_buddy/template/ --type py -B 2 -A 2 | head -40

Repository: juspay/clairvoyance

Length of output: 1452


with_context wrapper loses the second positional argument (actual llm_args) and remaining kwargs.

The wrapper receives (flow_manager, llm_args) as positional args but only extracts args[0] as the handler's args parameter, dropping the actual llm_args dict. Additionally, any kwargs beyond the explicitly popped ones (transition_to, hooks, function_name) are discarded during handler invocation.

Suggested fix:

 async def wrapper(*args, **kwargs):
     context = TemplateContext(bot_instance)

     transition_to = kwargs.pop("transition_to", None)
     hooks = kwargs.pop("hooks", None)
     function_name = kwargs.pop("function_name", None)

     is_transition_handler = hooks is not None or function_name is not None

-    llm_args = args[0] if len(args) > 0 else {}
+    flow_manager = args[0] if len(args) > 0 else None
+    llm_args = args[1] if len(args) > 1 else {}
+    rest_args = args[2:]

     if is_transition_handler:
         logger.debug(
             f"Calling transition handler '{handler_func.__name__}' "
             f"for function '{function_name}'"
         )
         return await handler_func(
             context,
             llm_args,
+            *rest_args,
             transition_to=transition_to,
             hooks=hooks,
             function_name=function_name,
+            **kwargs,
         )
     else:
         logger.debug(
             f"Calling action handler '{handler_func.__name__}' "
             f"with transition_to: {transition_to}"
         )
         return await handler_func(
             context,
             llm_args,
+            *rest_args,
             transition_to=transition_to,
+            **kwargs,
         )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/ai/voice/agents/breeze_buddy/template/context.py around lines 135 to 205,
the with_context wrapper currently treats llm_args as args[0] and removes
remaining kwargs by popping only transition_to/hooks/function_name, which drops
the actual second positional argument (flow_manager, llm_args) and any other
kwargs; fix by extracting llm_args from args[1] when present (use flow_manager =
args[0] and llm_args = args[1] if len(args) > 1 else {}), avoid mutating the
original kwargs (copy kwargs before popping) and forward all original positional
and remaining keyword arguments to the wrapped handler (i.e., pass flow_manager,
llm_args, and include transition_to/hooks/function_name plus any other kwargs)
so nothing is lost.

Comment on lines +43 to +59
logger.info(
f"Transition handler called - function: '{function_name}', "
f"transition_to: '{transition_to}', hooks: {hooks}, args: {args}"
)

# Schedule hooks to run asynchronously (fire and forget)
if hooks:
logger.info(
f"Scheduling {len(hooks)} hook(s) to execute asynchronously for function '{function_name}'"
)
asyncio.create_task(
_execute_hooks_async(context, args, hooks, function_name or "unknown")
)
else:
logger.debug(f"No hooks to execute for function '{function_name}'")

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging raw args/hooks at INFO (PII risk); also ensure background task exceptions are handled.

Suggested pattern:

-    logger.info(
-        f"Transition handler called - function: '{function_name}', "
-        f"transition_to: '{transition_to}', hooks: {hooks}, args: {args}"
-    )
+    logger.info(
+        f"Transition handler called - function: '{function_name}', transition_to: '{transition_to}', hooks_count: {len(hooks or [])}"
+    )
@@
-        asyncio.create_task(
-            _execute_hooks_async(context, args, hooks, function_name or "unknown")
-        )
+        task = asyncio.create_task(
+            _execute_hooks_async(context, args, hooks, function_name or "unknown")
+        )
+        task.add_done_callback(
+            lambda t: logger.exception("Hook task failed", exc_info=t.exception())
+            if t.exception()
+            else None
+        )
🧰 Tools
🪛 Ruff (0.14.8)

53-55: Store a reference to the return value of asyncio.create_task

(RUF006)

Comment on lines 455 to 457
@router.post("/push/lead/v2")
async def trigger_order_confirmation(
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix critical function name collision.

The function name trigger_order_confirmation is already defined at line 361. This redefinition causes the first endpoint to be shadowed and unreachable. FastAPI will only register the last definition, breaking the POST /{merchant}/{template} endpoint.

Rename this function to reflect its distinct purpose:

-async def trigger_order_confirmation(
+async def push_lead_v2(
     req: PushLeadRequest,
     current_user: TokenData = Depends(get_current_user),
 ):
     """
     Receives lead details and adds in database for processing.
     Requires JWT authentication.
     """
📝 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
@router.post("/push/lead/v2")
async def trigger_order_confirmation(
@router.post("/push/lead/v2")
async def push_lead_v2(
req: PushLeadRequest,
current_user: TokenData = Depends(get_current_user),
):
"""
Receives lead details and adds in database for processing.
Requires JWT authentication.
"""
🧰 Tools
🪛 Ruff (0.14.8)

456-456: Redefinition of unused trigger_order_confirmation from line 361

(F811)

🤖 Prompt for AI Agents
In app/api/routers/breeze_buddy.py around lines 455-456, the function defined as
trigger_order_confirmation is a duplicate of the function at line 361 which
causes the earlier endpoint to be shadowed; rename this second function to a
distinct, descriptive name (e.g., trigger_push_lead_v2 or handle_push_lead_v2)
and update any internal references or tests that call it so the FastAPI routes
remain unique and both endpoints register correctly.

Comment on lines 47 to 73
def decode_templates(result: List[asyncpg.Record]) -> List[TemplateModel]:
"""
Decode templates from database result.
"""
if not result:
return []

templates = []
for row in result:
templates.append(decode_template(row))

return templates
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

decode_templates can append None but returns List[TemplateModel].

Make it filter/guard to keep the return type truthful:

 def decode_templates(result: List[asyncpg.Record]) -> List[TemplateModel]:
@@
-    templates = []
-    for row in result:
-        templates.append(decode_template(row))
-
-    return templates
+    templates: List[TemplateModel] = []
+    for row in result:
+        t = decode_template(row)
+        if t is not None:
+            templates.append(t)
+    return templates
🤖 Prompt for AI Agents
In app/database/decoder/breeze_buddy/template.py around lines 47 to 58,
decode_templates currently appends the result of decode_template(row) which may
be None, violating the declared return type List[TemplateModel]; change the loop
to call decode_template(row), check if the returned value is not None before
appending (or use a comprehension that filters out falsy/None values) so only
TemplateModel instances are collected and returned; if decode_template
legitimately can return None and callers rely on that, alternatively update the
function signature to return List[Optional[TemplateModel]] and document the
behavior, but prefer filtering to keep the declared List[TemplateModel].

Comment on lines 8 to 34
TEMPLATE_TABLE = "template"


def get_template_by_merchant_query(
merchant_id: str, shop_identifier: str = None, name: str = None
) -> Tuple[str, List[Any]]:
"""Generate query to get a template by merchant ID and optional filters."""
conditions = ["merchant_id = $1"]
values = [merchant_id]

if shop_identifier:
conditions.append(f"shop_identifier = ${len(values) + 1}")
values.append(shop_identifier)

if name:
conditions.append(f"name = ${len(values) + 1}")
values.append(name)

query = f"""
SELECT id, merchant_id, shop_identifier, name, flow, expected_payload_schema, is_active, created_at, updated_at
FROM {TEMPLATE_TABLE}
WHERE {' AND '.join(conditions)}
LIMIT 1
"""

return query, values

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Quote the template table name (and keep it consistent with other query modules).

Other query files use "TABLE_NAME" quoting; here FROM template may fail if the migration created "template" (quoted identifier) or if the identifier is problematic in your SQL dialect. Safer:

-TEMPLATE_TABLE = "template"
+TEMPLATE_TABLE = '"template"'
...
-        FROM {TEMPLATE_TABLE}
+        FROM {TEMPLATE_TABLE}

(or keep TEMPLATE_TABLE = "template" and quote at use sites).

🧰 Tools
🪛 Ruff (0.14.8)

12-12: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


12-12: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


26-31: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
In app/database/queries/breeze_buddy/template.py around lines 8 to 34, the
unquoted table name usage in the query (FROM {TEMPLATE_TABLE}) can fail if the
migration created a quoted identifier; update the query to consistently quote
the table name at use sites (e.g., use FROM "{TEMPLATE_TABLE}" and any other
occurrences in the query string) so it matches the pattern used in other query
modules and ensures correct behavior across SQL dialects; keep TEMPLATE_TABLE
constant as-is and only change its interpolation in the f-string to include the
double quotes.

outbound_number_id: Optional[str] = None
merchant_id: str
workflow: Workflow
template: str
Copy link

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:

#!/bin/bash
# Search for template validation logic in the codebase

echo "=== Checking database constraints for template validation ==="
rg -n "CHECK.*template" --type=sql 2>/dev/null || echo "No SQL files or matches found"

echo -e "\n=== Checking template validation in Python code ==="
rg -nP "(validate.*template|template.*validate|allowed.*template|template.*allowed)" --type=py -C2 2>/dev/null || echo "No matches found"

echo -e "\n=== Checking template enum or registry ==="
rg -nP "(class.*Template.*Enum|VALID_TEMPLATES|ALLOWED_TEMPLATES|TemplateRegistry)" --type=py -C2 2>/dev/null || echo "No matches found"

echo -e "\n=== Checking template table schema ==="
fd -e sql 2>/dev/null | xargs rg -n "CREATE TABLE.*template" -A 20 2>/dev/null || echo "No SQL files with template tables found"

echo -e "\n=== Checking for workflow-related validation ==="
rg -nP "Workflow|WORKFLOW" --type=py -C2 2>/dev/null | head -50 || echo "No matches found"

Repository: juspay/clairvoyance

Length of output: 3130


🏁 Script executed:

#!/bin/bash
# Check schemas.py for validators
echo "=== Checking schemas.py for Pydantic validators ==="
rg -n "@validator|Field.*validate" app/schemas.py -B2 -A5

echo -e "\n=== Full app/schemas.py content (first 150 lines) ==="
head -150 app/schemas.py

echo -e "\n=== Searching for API endpoints that use template parameter ==="
rg -n "template" app/ --type=py -l | grep -E "(routes|api|endpoint)" | head -10

echo -e "\n=== Checking for template parameter validation in routes ==="
rg -n "def.*template|@.*template" app/ --type=py -C3 | head -80

Repository: juspay/clairvoyance

Length of output: 9398


🏁 Script executed:

#!/bin/bash
# Check template loader for validation
echo "=== Checking template loader implementation ==="
cat app/ai/voice/agents/breeze_buddy/template/loader.py

echo -e "\n=== Checking API route trigger_order_confirmation for template validation ==="
sed -n '360,420p' app/api/routers/breeze_buddy.py

Repository: juspay/clairvoyance

Length of output: 8321


Runtime template validation exists but lacks compile-time type safety.

The removal of the enum for template fields does eliminate compile-time type checking. However, template validation is enforced at runtime through database lookups in the template loader and API route handlers. When a template is not found in the database, a ValueError is raised, and API endpoints return 404 errors.

While this runtime validation prevents invalid templates from being executed, it provides weaker guarantees than the previous enum-based approach. Consider adding a template name whitelist or registry at the application layer (e.g., ALLOWED_TEMPLATES constant or template validation in schemas via Pydantic validators) to restore compile-time safety for developers while maintaining database as the source of truth.

🤖 Prompt for AI Agents
In app/schemas.py around line 41, the template field is declared as a plain
string which removes compile-time safety; add a whitelist/registry and a
Pydantic validator to enforce allowed template names at model validation. Create
an ALLOWED_TEMPLATES constant (or load it at app startup from the DB and cache
it) and implement a validator on the schema that checks template is in that set
and raises a ValueError with a clear message if not; ensure the startup loader
syncs the list if you choose dynamic loading and update any tests that assume
the previous enum behavior.

"task_messages": [],
"role_messages": [
{
"role": "system",
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

"template_name": "order-confirmation",
"is_active": true,
"description": "Complete order confirmation workflow with address updates",
"expected_payload_schema": {
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

"description": "Call this function when the user confirms that they are available to talk with clear affirmative responses",
"properties": {},
"required": [],
"transition_to": "verify_order_details",
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

"name": "update_outcome_in_database",
"expected_fields": {
"outcome": { "type": "enforce", "value": "cancelled" },
"reason": { "type": "infer_llm" }
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

)


async def unmute_stt(context: TemplateContext, args, transition_to=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

)


async def play_audio_sound(context: TemplateContext, args, transition_to=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

from app.database.accessor.breeze_buddy.template import get_template_by_merchant
from app.schemas import CallProvider

load_dotenv(override=True)
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

load_dotenv(override=True)


class OrderConfirmationBot:
Copy link
Collaborator

Choose a reason for hiding this comment

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

.



@router.get("/breeze/order-confirmation/dashboard", include_in_schema=False)
@router.get("/dashboard", include_in_schema=False)
Copy link
Collaborator

Choose a reason for hiding this comment

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

per merchant per shop

@badri-singhal badri-singhal force-pushed the template-based-architechture branch 5 times, most recently from af3f56e to 617906d Compare December 14, 2025 08:29
@badri-singhal badri-singhal force-pushed the template-based-architechture branch from 617906d to 087caaf Compare December 14, 2025 08:38
from app.schemas import LeadCallOutcome


async def service_callback(context: TemplateContext, args, transition_to=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

"attemptCount": context.lead.attempt_count + 1 if context.lead else 1,
"transcription": json.dumps(filtered_transcript, ensure_ascii=False),
"callDuration": call_duration,
"orderId": (
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

},
"expected_callback_response_schema": {
"cancellation_reason": "string",
"updated_address": "string"
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

Copy link
Collaborator

Choose a reason for hiding this comment

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

to be done now

"content": "You are Rhea, a friendly human and natural-sounding customer care representative from {shop_name}. Your goal is to confirm a Cash on Delivery (COD) order with the customer.\n\nIMPORTANT: You are a text-to-speech voice agent. Generate all responses as plain, unformatted text without any special characters, markdown formatting, asterisks, underscores, or other unnecessary symbols. Keep your text simple and clean for natural speech conversion.\n\nNever use the word \"great\" in your responses. Instead, use polite alternatives like \"thank you\", \"thanks\", or \"appreciate it\". Avoid any phrases that sound overly enthusiastic or robotic.\n\nNever reveal or suggest that you are a bot or automated system. You must always sound and respond as a real human representative from {shop_name}.\n\nBefore starting the conversation, identify whether the customer name \"{customer_name}\" is typically male or female in India. Based on this:\n- If the name is clearly male, address the customer as \"Sir\".\n- If clearly female, address them as \"Madam\".\n- If unsure, use their name instead of Sir/Madam in the conversation.\n\nIMPORTANT: Use the customer's name only once during the entire conversation - in the opening greeting. After that, address them as Sir/Madam (based on gender) or avoid direct address if gender is unclear.\n\nIMPORTANT: - If the user speaks in another language (like Hindi), reply in that same language but keep the same friendly, human tone.\n\nYour main job is to verify the following order details:\n- Items: {items}\n- Total Price: {total_price}\n- Delivery Address: {customer_address}\n\nDelivery Address Reading Rules\n\n Pincode Handling\n - If the address includes a 6-digit pincode, read it digit by digit only in English.\n - Example: 123456 → \"Pincode is one two three four five six.\"\n - Never read the pincode as a full number or in any other language.\n\n Mobile / Phone Number Handling\n - If the address includes a 10-digit mobile or phone number, read it digit by digit only in English.\n - Example: 9876543210 → \"nine eight seven six five four three two one zero.\"\n\n Example of correct usage when speaking adress in other language e.g. Hindi:\n \"आपका पता है [address in Hindi], aur pincode hai one two three four five six.\"\n\nTone and Language\n - Speak in a warm, casual, and natural tone — avoid robotic phrasing.\n\nAction Handling\n - Always use the provided functions to perform any actions related to the order.\n - Do not attempt to perform these actions through plain text replies.\n\nYour only role is to confirm or cancel this specific order. If the user asks about anything else (e.g. product details, delivery times, other products), you must use the appropriate function (`handle_unrelated_question_node` or `confirm_order_with_question`). Do not try to answer these questions yourself."
}
],
"pre_actions": [],
Copy link
Collaborator

Choose a reason for hiding this comment

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

this should be done now

logger.info(
f"Updating database with call completion details for call {context.call_sid}"
)
context.bot.lead = await context.bot.completion_function(
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

self.name = name

@abstractmethod
async def execute(
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

)

# Import here to avoid circular dependency
from app.database.accessor.breeze_buddy.template import get_template_by_merchant
Copy link
Collaborator

Choose a reason for hiding this comment

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

.

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