Support for initiate call using process tracker#192
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughReplaces call_data with lead_call_tracker across DB schemas, queries, accessors, and decoders. Updates providers to accept a customer number string. Introduces a cron-driven backlog processor and completion handler. Refactors API endpoints and WebSocket flow to the new lead-tracker model. Removes CallQueueManager and related main/app wiring. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant API as API (POST /order-confirmation)
participant DB as DB (lead_call_tracker)
participant Cfg as DB (call_execution_config)
API->>Cfg: get config for merchant/workflow
alt config found
API->>API: compute next_attempt_at (initial_offset)
API->>DB: create lead_call_tracker (BACKLOG, payload)
DB-->>API: lead id
API-->>API: respond queued with lead id
else no config
API-->>API: 404 Not Found
end
sequenceDiagram
autonumber
participant Cron as Cron (process_backlog_leads)
participant DB as DB (lead_call_tracker, outbound_numbers)
participant Prov as Voice Provider
participant Tel as Telephony
Cron->>DB: get BACKLOG leads <= now
loop per lead
Cron->>DB: get merchant call config
Cron->>DB: select outbound number (by provider/channels)
Cron->>DB: mark number IN_USE
Cron->>Prov: resolve provider (aiohttp session)
Cron->>Prov: make_call(customer_mobile_number)
Prov->>Tel: initiate call
Tel-->>Prov: sid
Prov-->>Cron: {status, sid}
Cron->>DB: update lead (PROCESSING, call_id)
end
sequenceDiagram
autonumber
participant WS as WebSocket (telephony)
participant Bot as WebsocketBot
participant DB as DB (lead_call_tracker)
participant Cron as handle_call_completion
WS->>Bot: events (transcripts, status)
Bot->>DB: get_lead_by_call_id(call_sid)
alt lead found
Bot->>Cron: handle_call_completion(call_id, outcome, transcription)
Cron->>DB: update lead (release number, FINISHED/outcome)
alt outcome BUSY/NO_ANSWER and retry allowed
Cron->>DB: schedule RETRY (increment attempt_count, next_attempt_at)
end
else lead missing
Bot-->>WS: log/close
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
app/agents/voice/breeze_buddy/call_providers/twillio.py (2)
33-50: Validate E.164 input and map Twilio errors to accurate HTTP status codesPreempt bad inputs and surface TwilioRestException.status instead of a blanket 400.
-from fastapi import WebSocket, HTTPException +from fastapi import WebSocket, HTTPException +from twilio.base.exceptions import TwilioRestException @@ - def make_call(self, customer_mobile_number: str): + def make_call(self, customer_mobile_number: str): ws_url = self.config.TWILIO_WEBSOCKET_URL @@ - try: + try: + if not isinstance(customer_mobile_number, str) or not customer_mobile_number.strip(): + raise HTTPException(status_code=422, detail="customer_mobile_number is required") + if not customer_mobile_number.startswith("+"): + # Twilio expects E.164; enforce to reduce provider errors. + raise HTTPException(status_code=422, detail="customer_mobile_number must be E.164 (+<countrycode><number>)") call = self.client.calls.create( to=customer_mobile_number, from_=self.config.TWILIO_FROM_NUMBER, twiml=str(voice_call_payload) ) return {"status": "call_initiated", "sid": call.sid} - except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + except TwilioRestException as e: + # Preserve provider status if available; default to 502 (upstream error). + raise HTTPException(status_code=(e.status or 502), detail=e.msg) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")
14-16: Ensure hangup_callback is invoked on all exit paths
Overriding_hang_up_calldisables serializer-driven hangups, buttelephony_websocket_connonly calls yourhangup_functionin the end‐conversation handler. Early returns on JSON parse failures, missing lead, invalid price/order data, and theon_client_disconnectedevent never invokehangup_function, risking orphaned calls. Add explicithangup_function(self.call_sid)in those branches or wraprun()in a top‐leveltry/finallyto guarantee hang‐up on every terminal path.app/database/accessor/breeze_buddy/outbound_number.py (1)
36-37: Avoid logging full objects/phone numbers (PII).Current info logs may include the raw outbound number. Prefer logging identifiers and metadata only, or mask numbers.
Example change:
-logger.info(f"Outbound number created successfully: {decoded_result}") +logger.info("Outbound number created successfully: id=%s provider=%s status=%s", + decoded_result.id, decoded_result.provider, decoded_result.status)Helper (outside diff) you can add once:
def mask_msisdn(num: str) -> str: return f"{'*' * (len(num)-4)}{num[-4:]}" if num else numAlso applies to: 51-52, 73-74, 95-96, 117-118, 139-140, 161-162
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
25-41: Don’t embed API credentials in the URL; add timeouts; avoid logging PII.
- Credentials in the URL can leak via proxies/logs. Use HTTP Basic Auth.
- Add an explicit timeout to prevent hangs.
- Mask the customer number in logs.
Apply:
@@ -from fastapi import WebSocket, HTTPException +from fastapi import WebSocket, HTTPException +from requests.auth import HTTPBasicAuth @@ - flow_url = f"http://my.exotel.com/{self.config.EXOTEL_ACCOUNT_SID}/exoml/start_voice/{self.config.EXOTEL_APPLET_APP_ID}" + flow_url = f"https://my.exotel.com/{self.config.EXOTEL_ACCOUNT_SID}/exoml/start_voice/{self.config.EXOTEL_APPLET_APP_ID}" @@ - payload = { - "From": customer_mobile_number, + masked = f"{'*' * (len(customer_mobile_number)-4)}{customer_mobile_number[-4:]}" if customer_mobile_number and len(customer_mobile_number) > 4 else customer_mobile_number + payload = { + "From": customer_mobile_number, "CallerId": self.config.EXOTEL_FROM_NUMBER, "Url": flow_url } - url = f"https://{self.config.EXOTEL_API_KEY}:{self.config.EXOTEL_API_TOKEN}@{self.config.EXOTEL_SUBDOMAIN}/v1/Accounts/{self.config.EXOTEL_ACCOUNT_SID}/Calls/connect.json" + url = f"https://{self.config.EXOTEL_SUBDOMAIN}/v1/Accounts/{self.config.EXOTEL_ACCOUNT_SID}/Calls/connect.json" @@ - logger.info(f"Making Exotel API call to: {self.config.EXOTEL_SUBDOMAIN}") - logger.info(f"Payload: {payload}") + logger.info(f"Making Exotel API call to: {self.config.EXOTEL_SUBDOMAIN}") + logger.info(f"Payload: {{'From': '{masked}', 'CallerId': '***', 'Url': '...'}}") @@ - resp = requests.post(url, data=payload) + resp = requests.post( + url, + data=payload, + auth=HTTPBasicAuth(self.config.EXOTEL_API_KEY, self.config.EXOTEL_API_TOKEN), + timeout=getattr(self.config, 'EXOTEL_TIMEOUT', 10), + )app/api/routers/breeze_buddy.py (2)
124-126: Bug: Enum compared to string always triggers 404.Compare to the enum value.
- if identity != "breeze": + if identity != RequestedBy.BREEZE: raise HTTPException(status_code=404, detail="Feature not supported")
139-150: Bug: payload not JSON-serializable due to Enum instance.Use
identity.value.- "identity": identity, + "identity": identity.value,
🧹 Nitpick comments (24)
app/agents/voice/breeze_buddy/call_providers/main.py (1)
22-27: Clarifymake_callcontract (return type + E.164 expectation) and keep providers consistentRecommend locking down the interface: declare the return type and document number format and failure mode so Exotel/Twilio behave uniformly.
from abc import ABC, abstractmethod from fastapi import WebSocket +from typing import Dict, Any @@ @abstractmethod - def make_call(self, customer_mobile_number: str): + def make_call(self, customer_mobile_number: str) -> Dict[str, Any]: """ - Initiate a call. + Initiate a call to the given E.164-formatted number (e.g., +14155552671). + Returns a dict like {"status": "call_initiated", "sid": "<provider-call-id>"} on success. + Implementations SHOULD raise HTTPException on failure, with an appropriate status code. """ passapp/agents/voice/breeze_buddy/call_providers/twillio.py (1)
21-22: Hardenhangup_callwith basic error handlingTwilio SDK can raise; avoid silent failures.
def hangup_call(self, call_sid: str): - self.client.calls(call_sid).update(status="completed") + try: + self.client.calls(call_sid).update(status="completed") + except Exception as e: + logger.error(f"Failed to hang up Twilio call {call_sid}: {e}") + # Optional: surface to caller + # raise HTTPException(status_code=502, detail="Failed to hang up call")app/schemas.py (3)
58-58: NormalizemetaDatato snake_case with aliasKeep JSON compatibility while making the Python field idiomatic.
- metaData: Optional[Dict[str, Any]] = None + from pydantic import Field + meta_data: Optional[Dict[str, Any]] = Field(default=None, alias="metaData")
64-67: Specify units forcall_durationand currency forcostAmbiguity here causes downstream bugs and reporting errors. Add docstrings or rename to
call_duration_secondsandcost_in_minor_units(or document currency).
24-29: Consolidate status/outcome enums to avoid drift
- Migrate all uses of
CallStatus/CallOutcometoLeadCallStatus/LeadCallOutcome(e.g. inapp/agents/voice/breeze_buddy/breeze/order_confirmation/utils.py,websocket_bot.py,cron_manager.pyand related database accessor/query modules).- In
app/schemas.py, add aliases or deprecation warnings forCallStatus/CallOutcomepointing to the new enums, then phase out the old ones once all references have been updated.- Ensure tests and any serialization logic are updated to use the unified enums.
app/database/accessor/breeze_buddy/call_execution_config.py (1)
16-20: De-duplicate get_row_count helper across accessors.This helper appears in multiple accessors; consider centralizing in a shared db utils module.
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
50-61: Unify success response shape.Elsewhere (Twilio) returns {"status": "call_initiated", "sid": ...}. When Exotel body is empty or Sid missing, consider consistent "status" values to simplify callers.
app/database/decoder/breeze_buddy/lead_call_tracker.py (1)
9-35: Add a list decoder for batch queries.Many accessors use list decoders; add parity for LeadCallTracker.
Example:
def decode_lead_call_tracker_list(result: List[asyncpg.Record]) -> List[LeadCallTracker]: if not result: return [] return [decode_lead_call_tracker([row]) for row in result]app/agents/voice/breeze_buddy/breeze/order_confirmation/websocket_bot.py (2)
121-127: Close WS when lead is missing to avoid dangling connectionIf lead lookup fails, also close the websocket with a reason, mirroring other error paths.
- lead = await get_lead_by_call_id(self.call_sid) - if not lead: - logger.error(f"Could not find lead for call_sid: {self.call_sid}") - return + lead = await get_lead_by_call_id(self.call_sid) + if not lead: + logger.error(f"Could not find lead for call_sid: {self.call_sid}") + try: + if self.ws.client_state.name != "DISCONNECTED": + await self.ws.close(code=4000, reason="Lead not found") + except Exception as close_error: + logger.warning(f"Could not close websocket (likely already closed): {close_error}") + return
271-276: Pass call_end_time on disconnect for parity with normal completionKeep the DB/completion payloads consistent in both code paths.
await self.completion_function( call_id=self.call_sid, outcome=CallOutcome.BUSY, - transcription={"messages": transcription, "call_sid": self.call_sid} + transcription={"messages": transcription, "call_sid": self.call_sid}, + call_end_time=datetime.now().isoformat() ) - logger.info(f"Updated database for call_id: {self.call_sid} with outcome: INTERRUPTED") + logger.info(f"Updated database for call_id: {self.call_sid} with outcome: BUSY")app/database/decoder/breeze_buddy/outbound_number.py (1)
27-46: Prefer list decoder usage in accessors when multiple rows are expectedIf upstream queries can return multiple rows, ensure the accessor uses
decode_outbound_number_listrather thandecode_outbound_numberto avoid accidental truncation to the first row.Would you like me to patch the accessor to consume
decode_outbound_number_listand returnList[OutboundNumber]?app/scripts/create_tables.py (3)
68-86: Optional: strengthen outbound_number constraintsConsider CHECKs for non-negative channels and
channels <= maximum_channelsto prevent invalid states.CREATE TABLE IF NOT EXISTS "{OUTBOUND_NUMBERS_TABLE}" ( "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, + "channels" INTEGER, + "maximum_channels" INTEGER, + CHECK (channels IS NULL OR channels >= 0), + CHECK (maximum_channels IS NULL OR maximum_channels >= 0), + CHECK ( + channels IS NULL OR maximum_channels IS NULL OR channels <= maximum_channels + ),
116-129: Narrow exception handling or log tracebacksCatching bare
Exceptionhides actionable errors; at least log stack traces withtraceback.print_exc()or narrow to DB exceptions.- except Exception as e: - print(f"Error creating lead_call_tracker table: {e}") + except Exception as e: + import traceback + print(f"Error creating lead_call_tracker table: {e}") + traceback.print_exc() return False
141-146: Stop on partial failures to simplify opsIf a table creation fails, consider short-circuiting rather than continuing; avoids half-initialized states.
- if outbound_numbers_success and call_execution_config_success and lead_call_tracker_success: + if outbound_numbers_success and call_execution_config_success and lead_call_tracker_success: print("All database tables created successfully") return True else: print("Failed to create some database tables") - return False + return False(Alternatively, raise after printing to fail fast.)
app/agents/voice/breeze_buddy/cron_manager.py (3)
26-31: Narrow broad exception handlers and log tracebacksCatching bare
Exceptiontwice hides actionable failures; at least log stack traces withlogger.exception.- try: + try: leads = await get_leads_based_on_status_and_next_attempt(LeadCallStatus.BACKLOG, datetime.now(timezone.utc)) @@ - except Exception as e: - logger.error(f"Error processing lead {lead.id}: {e}") + except Exception: + logger.exception(f"Error processing lead {lead.id}") @@ - except Exception as e: - logger.error(f"Error processing backlog leads: {e}") + except Exception: + logger.exception("Error processing backlog leads")Also applies to: 67-71
73-73: Unused parameter: transcriptionIf not needed, rename to
_transcriptionor store it with the lead for auditing.
21-27: Aiohttp session is unused by providersProviders use
requests/Twilio SDK; theaiohttpsession is currently unused. Either switch providers to asyncaiohttpor drop the session wrapper.app/database/accessor/breeze_buddy/lead_call_tracker.py (2)
55-62: Minor: collapse the post-query error branch into an else.Improves readability.
- if result and get_row_count(result) > 0: + if result and get_row_count(result) > 0: decoded_result = decode_lead_call_tracker(result) logger.info(f"Lead call tracker created successfully: {decoded_result}") return decoded_result - - logger.error("Failed to create lead call tracker") - return None + else: + logger.error("Failed to create lead call tracker") + return None
64-66: Narrow exception handling and log tracebacks.Catch
Exceptionis broad; at least addexc_info=Trueto preserve stack traces.- except Exception as e: - logger.error(f"Error creating lead call tracker: {e}") + except Exception as e: + logger.error(f"Error creating lead call tracker: {e}", exc_info=True) return NoneApply similarly to other try/except blocks in this file.
Also applies to: 80-82, 101-104, 122-125, 141-145
app/database/queries/breeze_buddy/lead_call_tracker.py (2)
5-5: Use timezone-aware timestamps for created_at/updated_at.Prevents mixed naive/aware datetime issues.
-from datetime import datetime +from datetime import datetime, timezone ... - datetime.now(), - datetime.now() + datetime.now(timezone.utc), + datetime.now(timezone.utc)Also applies to: 57-58
67-73: Order backlog fetch by next_attempt_at.Deterministic processing and better fairness.
text = f""" SELECT * FROM "{LEAD_CALL_TRACKER_TABLE}" WHERE "status" = $1 - AND "next_attempt_at" <= $2; + AND "next_attempt_at" <= $2 + ORDER BY "next_attempt_at" ASC; """app/api/routers/breeze_buddy.py (3)
244-245: Fix log message.Wrong context string.
- logger.error("Error disabling outbound number", exc_info=True) + logger.error("Error adding call execution config", exc_info=True)
247-258: Consider POST for cron initiation.Side-effecting action; GET is semantically odd. Keep GET if you rely on idempotent triggers, otherwise switch to POST.
200-209: Broadened exception handling; consider special-casing known errors.Handle
ValueErrorfrom unsupported providers separately for clearer logs.- except Exception as e: + except ValueError as e: + logger.error(f"Unsupported provider: {e}") + try: + if websocket.client_state.name != "DISCONNECTED": + await websocket.close(code=1003, reason="Unsupported provider") + except Exception as close_error: + logger.warning(f"Could not close websocket (likely already closed): {close_error}") + except Exception as e: error_type = type(e).__name__
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
app/agents/voice/breeze_buddy/breeze/order_confirmation/websocket_bot.py(3 hunks)app/agents/voice/breeze_buddy/call_providers/exotel.py(1 hunks)app/agents/voice/breeze_buddy/call_providers/main.py(1 hunks)app/agents/voice/breeze_buddy/call_providers/twillio.py(2 hunks)app/agents/voice/breeze_buddy/cron_manager.py(1 hunks)app/api/routers/breeze_buddy.py(6 hunks)app/database/accessor/breeze_buddy/call_data.py(0 hunks)app/database/accessor/breeze_buddy/call_execution_config.py(1 hunks)app/database/accessor/breeze_buddy/lead_call_tracker.py(1 hunks)app/database/accessor/breeze_buddy/outbound_number.py(1 hunks)app/database/accessor/decoder.py(0 hunks)app/database/accessor/main.py(2 hunks)app/database/decoder/breeze_buddy/call_execution_config.py(1 hunks)app/database/decoder/breeze_buddy/lead_call_tracker.py(1 hunks)app/database/decoder/breeze_buddy/outbound_number.py(1 hunks)app/database/queries/breeze_buddy/call_data.py(0 hunks)app/database/queries/breeze_buddy/lead_call_tracker.py(1 hunks)app/main.py(0 hunks)app/schemas.py(4 hunks)app/scripts/create_tables.py(3 hunks)app/services/call_queue_manager.py(0 hunks)
💤 Files with no reviewable changes (5)
- app/database/accessor/breeze_buddy/call_data.py
- app/database/queries/breeze_buddy/call_data.py
- app/main.py
- app/services/call_queue_manager.py
- app/database/accessor/decoder.py
🧰 Additional context used
🧬 Code graph analysis (15)
app/database/decoder/breeze_buddy/call_execution_config.py (1)
app/schemas.py (3)
Workflow(34-35)CallExecutionConfig(95-106)CallProvider(14-16)
app/database/accessor/breeze_buddy/call_execution_config.py (1)
app/database/decoder/breeze_buddy/call_execution_config.py (2)
decode_call_execution_config(32-52)decode_call_execution_config_list(8-30)
app/database/accessor/breeze_buddy/outbound_number.py (1)
app/database/decoder/breeze_buddy/outbound_number.py (2)
decode_outbound_number(8-25)decode_outbound_number_list(27-46)
app/database/decoder/breeze_buddy/lead_call_tracker.py (2)
app/schemas.py (1)
LeadCallTracker(50-67)app/utils/common.py (1)
parse_json(49-50)
app/database/decoder/breeze_buddy/outbound_number.py (1)
app/schemas.py (3)
OutboundNumber(75-83)OutboundNumberStatus(8-11)CallProvider(14-16)
app/agents/voice/breeze_buddy/call_providers/exotel.py (2)
app/agents/voice/breeze_buddy/call_providers/main.py (1)
make_call(23-27)app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
make_call(33-50)
app/agents/voice/breeze_buddy/cron_manager.py (7)
app/database/accessor/breeze_buddy/lead_call_tracker.py (5)
get_leads_based_on_status_and_next_attempt(68-82)update_lead_status_and_call_id(84-103)get_lead_by_call_id(105-124)update_lead_status_and_outcome(126-145)create_lead_call_tracker(26-66)app/database/accessor/breeze_buddy/call_execution_config.py (1)
get_call_execution_config_by_merchant_id(64-84)app/database/accessor/breeze_buddy/outbound_number.py (2)
get_outbound_number_based_on_status_and_provider(149-169)update_outbound_number_status(83-103)app/schemas.py (3)
LeadCallStatus(37-41)OutboundNumberStatus(8-11)LeadCallOutcome(43-48)app/agents/voice/breeze_buddy/call_providers/factory.py (1)
get_voice_provider(5-10)app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
make_call(24-77)app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
make_call(33-50)
app/database/queries/breeze_buddy/lead_call_tracker.py (1)
app/schemas.py (4)
Workflow(34-35)LeadCallStatus(37-41)LeadCallOutcome(43-48)RequestedBy(30-32)
app/database/accessor/main.py (1)
app/database/accessor/breeze_buddy/lead_call_tracker.py (5)
create_lead_call_tracker(26-66)get_leads_based_on_status_and_next_attempt(68-82)update_lead_status_and_call_id(84-103)get_lead_by_call_id(105-124)update_lead_status_and_outcome(126-145)
app/agents/voice/breeze_buddy/call_providers/twillio.py (2)
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
make_call(24-77)app/agents/voice/breeze_buddy/call_providers/main.py (1)
make_call(23-27)
app/database/accessor/breeze_buddy/lead_call_tracker.py (4)
app/schemas.py (5)
LeadCallTracker(50-67)LeadCallStatus(37-41)Workflow(34-35)RequestedBy(30-32)LeadCallOutcome(43-48)app/database/queries/main.py (1)
run_parameterized_query(10-24)app/database/decoder/breeze_buddy/lead_call_tracker.py (1)
decode_lead_call_tracker(9-35)app/database/queries/breeze_buddy/lead_call_tracker.py (5)
insert_lead_call_tracker_query(13-61)get_leads_based_on_status_and_next_attempt_query(63-73)update_lead_status_and_call_id_query(75-86)get_lead_by_call_id_query(88-97)update_lead_status_and_outcome_query(99-110)
app/scripts/create_tables.py (1)
app/database/__init__.py (1)
get_db_connection(44-52)
app/agents/voice/breeze_buddy/call_providers/main.py (2)
app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
make_call(24-77)app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
make_call(33-50)
app/agents/voice/breeze_buddy/breeze/order_confirmation/websocket_bot.py (1)
app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
get_lead_by_call_id(105-124)
app/api/routers/breeze_buddy.py (10)
app/core/security/jwt.py (1)
get_current_user(121-141)app/schemas.py (3)
TokenData(126-131)RequestedBy(30-32)Workflow(34-35)app/agents/voice/breeze_buddy/breeze/order_confirmation/types.py (1)
BreezeOrderData(11-20)app/agents/voice/breeze_buddy/cron_manager.py (2)
process_backlog_leads(21-71)handle_call_completion(73-106)app/agents/voice/breeze_buddy/call_providers/factory.py (1)
get_voice_provider(5-10)app/database/accessor/breeze_buddy/call_execution_config.py (1)
get_call_execution_config_by_merchant_id(64-84)app/database/accessor/breeze_buddy/lead_call_tracker.py (1)
create_lead_call_tracker(26-66)app/agents/voice/breeze_buddy/call_providers/main.py (2)
set_completion_callback(29-33)handle_websocket(16-20)app/agents/voice/breeze_buddy/call_providers/exotel.py (1)
handle_websocket(17-22)app/agents/voice/breeze_buddy/call_providers/twillio.py (1)
handle_websocket(24-31)
🪛 Ruff (0.12.2)
app/agents/voice/breeze_buddy/cron_manager.py
67-67: Do not catch blind exception: Exception
(BLE001)
70-70: Do not catch blind exception: Exception
(BLE001)
73-73: Unused function argument: transcription
(ARG001)
app/database/queries/breeze_buddy/lead_call_tracker.py
27-44: Possible SQL injection vector through string-based query construction
(S608)
67-71: Possible SQL injection vector through string-based query construction
(S608)
79-84: Possible SQL injection vector through string-based query construction
(S608)
92-95: Possible SQL injection vector through string-based query construction
(S608)
103-108: Possible SQL injection vector through string-based query construction
(S608)
app/database/accessor/breeze_buddy/lead_call_tracker.py
62-62: Consider moving this statement to an else block
(TRY300)
64-64: Do not catch blind exception: Exception
(BLE001)
79-79: Consider moving this statement to an else block
(TRY300)
80-80: Do not catch blind exception: Exception
(BLE001)
99-99: Consider moving this statement to an else block
(TRY300)
101-101: Do not catch blind exception: Exception
(BLE001)
120-120: Consider moving this statement to an else block
(TRY300)
122-122: Do not catch blind exception: Exception
(BLE001)
141-141: Consider moving this statement to an else block
(TRY300)
143-143: Do not catch blind exception: Exception
(BLE001)
app/scripts/create_tables.py
126-126: Do not catch blind exception: Exception
(BLE001)
app/api/routers/breeze_buddy.py
118-118: 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)
133-133: Abstract raise to an inner function
(TRY301)
137-137: Abstract raise to an inner function
(TRY301)
175-175: Abstract raise to an inner function
(TRY301)
200-200: Do not catch blind exception: Exception
(BLE001)
207-207: Do not catch blind exception: Exception
(BLE001)
250-250: 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)
🔇 Additional comments (14)
app/database/accessor/breeze_buddy/outbound_number.py (1)
9-9: Import path change looks good.The decoder reorg alignment is correct; no functional impact.
app/database/accessor/breeze_buddy/call_execution_config.py (1)
10-10: Decoder import rewire is fine.Consistent with the new decoder package layout.
app/agents/voice/breeze_buddy/call_providers/exotel.py (2)
25-25: Use HTTPS for flow_url (Exotel supports and recommends it)
Exotel accepts HTTPS endpoints for its start_voice (Exoml applet) URLs and recommends using HTTPS in production for security.
24-24: Approve code changes Allmake_callusages now match the updated signature and no lingeringCallDataResponsereferences remain.app/database/decoder/breeze_buddy/lead_call_tracker.py (1)
30-31: No action needed:call_initaited_timematches existing DB schema
The column namecall_initaited_timeis defined inapp/scripts/create_tables.pyand consistently used in accessors, decoders, queries, and schemas—leaving it as-is avoids a breaking migration.app/database/decoder/breeze_buddy/outbound_number.py (1)
8-26: LGTM: Single-record decoder is correct and aligns with schemaDecoding and enum mapping look good, including optional timestamps.
app/database/decoder/breeze_buddy/call_execution_config.py (2)
8-31: LGTM: List decoder correctly maps enums and timestamps
32-53: LGTM: Single-record decoder mirrors list decoder faithfullyapp/database/accessor/main.py (2)
18-24: Swap to lead_call_tracker accessors: looks good.Imports are correct and align with the new accessor module.
35-39: No lingering call_data references detected
Ripgrep search for*_call_datareturned no matches, confirming all accessors have been removed.app/database/accessor/breeze_buddy/lead_call_tracker.py (2)
10-17: Schema/decoder field mismatch: metaData vs meta_data will raise validation errors.
LeadCallTrackerusesmetaData(camelCase) but the decoder constructs withmeta_data=.... Fix the decoder to usemetaData=....Outside this file, apply in app/database/decoder/breeze_buddy/lead_call_tracker.py:
- meta_data=parse_json(row, "meta_data"), + metaData=parse_json(row, "meta_data"),Optionally add
Field(..., alias="meta_data")to the schema if you want to accept either.Likely an incorrect or invalid review comment.
76-79: Fix decoder call: pass a list (row wrapper) and filter None.
decode_lead_call_trackerexpects a list, not a single row.- if result: - return [decode_lead_call_tracker(row) for row in result] + if result: + decoded = [decode_lead_call_tracker([row]) for row in result] + return [d for d in decoded if d is not None] return []Likely an incorrect or invalid review comment.
app/database/queries/breeze_buddy/lead_call_tracker.py (2)
27-44: S608 (SQL injection) note: safe as written.Table name is a constant; all user data is parameterized. No change needed.
Also applies to: 63-87, 88-98, 99-110
37-41: Spelling is consistent: “call_initaited_time” matches the table DDL and all code references.
| "From": customer_mobile_number, | ||
| "CallerId": self.config.EXOTEL_FROM_NUMBER, | ||
| "Url": flow_url | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Validate and normalize the mobile number (E.164).
Exotel typically expects E.164 format. Normalize/validate before sending to reduce provider errors.
Example:
def to_e164(n: str, default_cc: str) -> str:
n = n.strip().replace(' ', '')
if n.startswith('+'): return n
if n.startswith('00'): return '+' + n[2:]
return f'+{default_cc}{n}'Use before building payload.
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/call_providers/exotel.py around lines 28 to 31,
the "From" phone number is used raw; validate and normalize it to E.164 before
building the payload to avoid provider errors. Add a small helper that strips
whitespace, handles leading '+' and '00', and prepends a default country code
when missing; call this helper on customer_mobile_number, validate the result
(ensure it starts with '+', contains only digits after '+', and meets length
expectations), and if invalid either log and abort the call creation or raise a
clear error so you never send a malformed number to Exotel. Ensure this
normalization runs before constructing the payload and that failures are handled
consistently with existing error/logging patterns.
| numbers = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider) | ||
| if not numbers: | ||
| logger.warning(f"No available outbound numbers found for provider: {config.calling_provider}") | ||
| continue | ||
|
|
||
| number_to_use = None | ||
| if config.calling_provider == "EXOTEL": | ||
| for number in numbers: | ||
| if number.channels < number.maximum_channels: | ||
| number_to_use = number | ||
| break | ||
| else: | ||
| number_to_use = numbers[0] | ||
|
|
There was a problem hiding this comment.
Accessor returns a single number, but code treats it as a list
get_outbound_number_based_on_status_and_provider (per accessor snippet) returns Optional[OutboundNumber], not a list. Iteration/indexing will break.
Immediate fix (treat as single value):
- numbers = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider)
- if not numbers:
+ number = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider)
+ if not number:
logger.warning(f"No available outbound numbers found for provider: {config.calling_provider}")
continue
- number_to_use = None
- if config.calling_provider == "EXOTEL":
- for number in numbers:
- if number.channels < number.maximum_channels:
- number_to_use = number
- break
- else:
- number_to_use = numbers[0]
+ number_to_use = numberPreferred fix (outside this file): make the accessor return List[OutboundNumber] and use decode_outbound_number_list.
📝 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.
| numbers = await get_outbound_number_based_on_status_and_provider(OutboundNumberStatus.AVAILABLE, config.calling_provider) | |
| if not numbers: | |
| logger.warning(f"No available outbound numbers found for provider: {config.calling_provider}") | |
| continue | |
| number_to_use = None | |
| if config.calling_provider == "EXOTEL": | |
| for number in numbers: | |
| if number.channels < number.maximum_channels: | |
| number_to_use = number | |
| break | |
| else: | |
| number_to_use = numbers[0] | |
| number = await get_outbound_number_based_on_status_and_provider( | |
| OutboundNumberStatus.AVAILABLE, | |
| config.calling_provider | |
| ) | |
| if not number: | |
| logger.warning( | |
| f"No available outbound numbers found for provider: {config.calling_provider}" | |
| ) | |
| continue | |
| number_to_use = number |
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 43 to 56, the
accessor get_outbound_number_based_on_status_and_provider returns an
Optional[OutboundNumber] (a single object) but the code treats it like a list
(iterating and indexing). Replace the list handling with a single-value flow:
assign the returned value to number_to_use directly, check for None and
warn/continue, and if config.calling_provider == "EXOTEL" verify number.channels
< number.maximum_channels before using it (warn/continue if it fails);
alternatively, if you want list semantics change the accessor to return
List[OutboundNumber] and use decode_outbound_number_list elsewhere.
| if config.calling_provider == "EXOTEL": | ||
| for number in numbers: | ||
| if number.channels < number.maximum_channels: | ||
| number_to_use = number | ||
| break | ||
| else: | ||
| number_to_use = numbers[0] | ||
|
|
There was a problem hiding this comment.
Enum vs string comparison bug
config.calling_provider is CallProvider, but you're comparing to "EXOTEL". This branch never executes.
If you keep single-number selection (above), drop this branch. If you later return lists, compare with the enum:
- if config.calling_provider == "EXOTEL":
+ if config.calling_provider == CallProvider.EXOTEL:(Ensure from app.schemas import CallProvider is imported.)
📝 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.
| if config.calling_provider == "EXOTEL": | |
| for number in numbers: | |
| if number.channels < number.maximum_channels: | |
| number_to_use = number | |
| break | |
| else: | |
| number_to_use = numbers[0] | |
| # at the top of cron_manager.py, ensure you import the enum | |
| from app.schemas import CallProvider | |
| … # (other code) | |
| if config.calling_provider == CallProvider.EXOTEL: | |
| for number in numbers: | |
| if number.channels < number.maximum_channels: | |
| number_to_use = number | |
| break | |
| else: | |
| number_to_use = numbers[0] | |
| … # (following code) |
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 49 to 56, the code
compares config.calling_provider to the string "EXOTEL" but
config.calling_provider is a CallProvider enum so that branch never runs; fix by
either removing the EXOTEL-specific branch if you will always choose a single
number, or change the comparison to use the enum (e.g. compare to
CallProvider.EXOTEL) and ensure you add "from app.schemas import CallProvider"
at the top of the file; update logic accordingly so number_to_use is selected
correctly when provider is EXOTEL.
| await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE) | ||
|
|
||
| call_provider = get_voice_provider(config.calling_provider.value, session) | ||
| call = await call_provider.make_call(lead.payload.get("customer_mobile_number")) | ||
| await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid")) | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error processing lead {lead.id}: {e}") | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Handle call initiation failures robustly
If the provider fails or returns no SID, you still mark the lead PROCESSING and keep the number IN_USE. Add failure handling and rollback.
- call = call_provider.make_call(lead.payload.get("customer_mobile_number"))
- await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))
+ call = call_provider.make_call(lead.payload.get("customer_mobile_number"))
+ if not call or call.get("status") != "call_initiated" or not call.get("sid"):
+ logger.error(f"Call initiation failed for lead {lead.id}: {call}")
+ await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.AVAILABLE)
+ continue
+ await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 Ruff (0.12.2)
67-67: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 61-69, the code
marks the lead PROCESSING and sets the outbound number to IN_USE before ensuring
a call was actually created; if the provider fails or returns no SID we must
roll back those changes. Change the flow so you first reserve the number,
attempt to initiate the call, then check the call response: if a valid SID is
returned, update the lead status to PROCESSING and save the call_id; if the
provider raises or returns no SID, update the lead to a failure/error status (or
keep as PENDING per domain rules) and set the outbound number back to AVAILABLE
(use update_outbound_number_status), and ensure this rollback also runs inside
the except block so any exception releases the number; only mark PROCESSING and
persist call_id after confirming a non-empty SID.
| await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE) | ||
|
|
||
| call_provider = get_voice_provider(config.calling_provider.value, session) | ||
| call = await call_provider.make_call(lead.payload.get("customer_mobile_number")) | ||
| await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid")) | ||
|
|
There was a problem hiding this comment.
Persist chosen outbound_number_id on the lead
handle_call_completion releases lead.outbound_number_id, but it’s never set here. This will be None, breaking release.
If your accessor supports it, update outbound_number_id together with status and call_id:
- await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid"))
+ await update_lead_status_and_call_id(
+ id=lead.id,
+ status=LeadCallStatus.PROCESSING,
+ call_id=call.get("sid"),
+ outbound_number_id=number_to_use.id, # requires accessor support
+ )If not supported, add a new accessor update_lead_status_call_id_and_number(...) and use it here.
📝 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.
| await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE) | |
| call_provider = get_voice_provider(config.calling_provider.value, session) | |
| call = await call_provider.make_call(lead.payload.get("customer_mobile_number")) | |
| await update_lead_status_and_call_id(lead.id, LeadCallStatus.PROCESSING, call.get("sid")) | |
| await update_outbound_number_status(number_to_use.id, OutboundNumberStatus.IN_USE) | |
| call_provider = get_voice_provider(config.calling_provider.value, session) | |
| call = await call_provider.make_call(lead.payload.get("customer_mobile_number")) | |
| await update_lead_status_and_call_id( | |
| id=lead.id, | |
| status=LeadCallStatus.PROCESSING, | |
| call_id=call.get("sid"), | |
| outbound_number_id=number_to_use.id, # requires accessor support | |
| ) |
🤖 Prompt for AI Agents
In app/agents/voice/breeze_buddy/cron_manager.py around lines 61 to 66, the
chosen outbound_number_id is never persisted to the lead before making the call
which causes handle_call_completion to fail when releasing the number; update
the lead record together with its status and call_id to include
outbound_number_id. If your existing data accessor can update status and call_id
together, extend that call to also set outbound_number_id; otherwise add a new
accessor update_lead_status_call_id_and_number(lead_id, status, call_id,
outbound_number_id) and call it here with number_to_use.id,
LeadCallStatus.PROCESSING, and the returned call sid so the
lead.outbound_number_id is persisted.
| payload=parse_json(row, "payload"), | ||
| meta_data=parse_json(row, "meta_data"), | ||
| recording_url=row["recording_url"], | ||
| status=row["status"], | ||
| outcome=row["outcome"], | ||
| call_id=row["call_id"], | ||
| call_initaited_time=row["call_initaited_time"], | ||
| call_duration=row["call_duration"], | ||
| cost=row["cost"], | ||
| created_at=row["created_at"], | ||
| updated_at=row["updated_at"], | ||
| ) |
There was a problem hiding this comment.
Wrong field name: meta_data vs metaData (constructor will fail).
Pydantic model uses metaData; passing meta_data raises an unexpected keyword error.
Apply:
- meta_data=parse_json(row, "meta_data"),
+ metaData=parse_json(row, "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.
| payload=parse_json(row, "payload"), | |
| meta_data=parse_json(row, "meta_data"), | |
| recording_url=row["recording_url"], | |
| status=row["status"], | |
| outcome=row["outcome"], | |
| call_id=row["call_id"], | |
| call_initaited_time=row["call_initaited_time"], | |
| call_duration=row["call_duration"], | |
| cost=row["cost"], | |
| created_at=row["created_at"], | |
| updated_at=row["updated_at"], | |
| ) | |
| payload=parse_json(row, "payload"), | |
| metaData=parse_json(row, "meta_data"), | |
| recording_url=row["recording_url"], | |
| status=row["status"], | |
| outcome=row["outcome"], | |
| call_id=row["call_id"], | |
| call_initaited_time=row["call_initaited_time"], | |
| call_duration=row["call_duration"], | |
| cost=row["cost"], | |
| created_at=row["created_at"], | |
| updated_at=row["updated_at"], | |
| ) |
🤖 Prompt for AI Agents
In app/database/decoder/breeze_buddy/lead_call_tracker.py around lines 24 to 35,
the code passes meta_data to the Pydantic constructor but the model expects
metaData; change the argument name to metaData while still parsing the same
source key (e.g., metaData=parse_json(row, "meta_data")) so the constructor
receives the correct field name and no unexpected keyword error occurs.
| merchant_id: RequestedBy | ||
| workflow: Workflow | ||
| attempt_count: int = 0 |
There was a problem hiding this comment.
merchant_id typed as RequestedBy (semantic/type mismatch)
merchant_id should be an identifier (str/UUID). RequestedBy denotes the source (breeze/shopify). This will break consumers and conflate two concepts.
class LeadCallTracker(BaseModel):
id: str
outbound_number_id: Optional[str] = None
- merchant_id: RequestedBy
+ merchant_id: str
+ # Track source separately to preserve prior semantics
+ requested_by: Optional[RequestedBy] = None
workflow: Workflow📝 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.
| merchant_id: RequestedBy | |
| workflow: Workflow | |
| attempt_count: int = 0 | |
| class LeadCallTracker(BaseModel): | |
| id: str | |
| outbound_number_id: Optional[str] = None | |
| merchant_id: str | |
| # Track source separately to preserve prior semantics | |
| requested_by: Optional[RequestedBy] = None | |
| workflow: Workflow | |
| attempt_count: int = 0 |
🤖 Prompt for AI Agents
In app/schemas.py around lines 53-55 the field merchant_id is incorrectly typed
as RequestedBy (which represents the source, e.g., breeze/shopify) instead of an
identifier; change merchant_id to the appropriate identifier type (e.g., str or
UUID) and leave RequestedBy as a separate field (or add a separate requested_by:
RequestedBy field if missing). Update the type annotation and any necessary
imports, run static type checks, and adjust any downstream code/tests that
expect merchant_id to be a RequestedBy to use the new requested_by field
instead.
app/schemas.py
Outdated
| assigned_number: Optional[str] = None | ||
| created_at: str | ||
| updated_at: str | ||
| call_initaited_time: Optional[datetime] = None |
There was a problem hiding this comment.
Typo: call_initaited_time → call_initiated_time (add alias for backward-compat)
The current misspelling will leak into DB/JSON and downstream code.
- call_initaited_time: Optional[datetime] = None
+ from pydantic import Field
+ call_initiated_time: Optional[datetime] = Field(default=None, alias="call_initaited_time")
+
+ class Config:
+ allow_population_by_field_name = TrueNote: After this change, serialize with by_alias=True only if you must keep the old key during a transition window.
📝 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.
| call_initaited_time: Optional[datetime] = None | |
| from pydantic import Field | |
| # rename the field to the correct spelling, but keep the old key for backwards compatibility | |
| call_initiated_time: Optional[datetime] = Field(default=None, alias="call_initaited_time") | |
| class Config: | |
| # allow initializing via the new “call_initiated_time” name as well as the old alias | |
| allow_population_by_field_name = True |
🤖 Prompt for AI Agents
In app/schemas.py at line 63, rename the misspelled field to call_initiated_time
and keep backward compatibility by defining the field with the correct Python
attribute name and a Pydantic alias equal to the old key (call_initaited_time);
update the default/typing (Optional[datetime] = None) accordingly and ensure any
serialization during transition only uses by_alias=True when you need to emit
the old key.
| def create_lead_call_tracker_table_query() -> str: | ||
| """ | ||
| Generate query to create lead_call_tracker table. | ||
| """ | ||
| return f""" | ||
| CREATE TABLE IF NOT EXISTS "{LEAD_CALL_TRACKER_TABLE}" ( | ||
| "id" VARCHAR(255) PRIMARY KEY, | ||
| "outbound_number_id" VARCHAR(255), | ||
| "merchant_id" VARCHAR(100) NOT NULL, | ||
| "workflow" VARCHAR(50) CHECK ("workflow" IN ('order-confirmation')) NOT NULL, | ||
| "attempt_count" INTEGER DEFAULT 0, | ||
| "next_attempt_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW(), | ||
| "payload" JSONB, | ||
| "meta_data" JSONB, | ||
| "recording_url" VARCHAR(500), | ||
| "status" VARCHAR(50) CHECK ("status" IN ('BACKLOG', 'PROCESSING', 'FINISHED', 'RETRY')) NOT NULL, | ||
| "outcome" VARCHAR(50) CHECK ("outcome" IN ('NO_ANSWER', 'BUSY', 'CANCEL', 'CONFIRM', 'UNKNOWN')), | ||
| "call_id" VARCHAR(100), | ||
| "call_initaited_time" TIMESTAMP WITH TIME ZONE, | ||
| "call_duration" INTEGER, | ||
| "cost" REAL, | ||
| "created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, | ||
| "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL | ||
| ); |
There was a problem hiding this comment.
Fix column name typo: call_initaited_time → call_initiated_time (schema change)
This is a persistent schema typo. Correct before it ships; retrofitting later is costly.
- "call_initaited_time" TIMESTAMP WITH TIME ZONE,
+ "call_initiated_time" TIMESTAMP WITH TIME ZONE,Follow-ups (outside this file):
- Update accessors/queries/models using the same misspelling.
📝 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.
| def create_lead_call_tracker_table_query() -> str: | |
| """ | |
| Generate query to create lead_call_tracker table. | |
| """ | |
| return f""" | |
| CREATE TABLE IF NOT EXISTS "{LEAD_CALL_TRACKER_TABLE}" ( | |
| "id" VARCHAR(255) PRIMARY KEY, | |
| "outbound_number_id" VARCHAR(255), | |
| "merchant_id" VARCHAR(100) NOT NULL, | |
| "workflow" VARCHAR(50) CHECK ("workflow" IN ('order-confirmation')) NOT NULL, | |
| "attempt_count" INTEGER DEFAULT 0, | |
| "next_attempt_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW(), | |
| "payload" JSONB, | |
| "meta_data" JSONB, | |
| "recording_url" VARCHAR(500), | |
| "status" VARCHAR(50) CHECK ("status" IN ('BACKLOG', 'PROCESSING', 'FINISHED', 'RETRY')) NOT NULL, | |
| "outcome" VARCHAR(50) CHECK ("outcome" IN ('NO_ANSWER', 'BUSY', 'CANCEL', 'CONFIRM', 'UNKNOWN')), | |
| "call_id" VARCHAR(100), | |
| "call_initaited_time" TIMESTAMP WITH TIME ZONE, | |
| "call_duration" INTEGER, | |
| "cost" REAL, | |
| "created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, | |
| "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL | |
| ); | |
| def create_lead_call_tracker_table_query() -> str: | |
| """ | |
| Generate query to create lead_call_tracker table. | |
| """ | |
| return f""" | |
| CREATE TABLE IF NOT EXISTS "{LEAD_CALL_TRACKER_TABLE}" ( | |
| "id" VARCHAR(255) PRIMARY KEY, | |
| "outbound_number_id" VARCHAR(255), | |
| "merchant_id" VARCHAR(100) NOT NULL, | |
| "workflow" VARCHAR(50) CHECK ("workflow" IN ('order-confirmation')) NOT NULL, | |
| "attempt_count" INTEGER DEFAULT 0, | |
| "next_attempt_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW(), | |
| "payload" JSONB, | |
| "meta_data" JSONB, | |
| "recording_url" VARCHAR(500), | |
| "status" VARCHAR(50) CHECK ("status" IN ('BACKLOG', 'PROCESSING', 'FINISHED', 'RETRY')) NOT NULL, | |
| "outcome" VARCHAR(50) CHECK ("outcome" IN ('NO_ANSWER', 'BUSY', 'CANCEL', 'CONFIRM', 'UNKNOWN')), | |
| "call_id" VARCHAR(100), | |
| "call_initiated_time" TIMESTAMP WITH TIME ZONE, | |
| "call_duration" INTEGER, | |
| "cost" REAL, | |
| "created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, | |
| "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL | |
| ); |
🤖 Prompt for AI Agents
In app/scripts/create_tables.py around lines 16 to 39, the column name
"call_initaited_time" is misspelled; change it to "call_initiated_time" in the
CREATE TABLE query to correct the schema, and update the column name in any
related migration or deployment scripts to apply the change. Also search the
codebase for the misspelled identifier and update all accessors, queries,
models, and tests to use "call_initiated_time" (or add a migration that renames
the column in existing DBs if data must be preserved). Ensure any references in
SELECT/INSERT/UPDATE statements and ORM mappings are updated and run
schema/migration tests.
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id"); | ||
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status"); | ||
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome"); | ||
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at"); | ||
| """ |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add an index on call_id to speed up get_lead_by_call_id lookups
This path is hot; an index materially reduces latency.
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome");
CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at");
+ CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_call_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("call_id");📝 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.
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id"); | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status"); | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome"); | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at"); | |
| """ | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_merchant_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("merchant_id"); | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_status" ON "{LEAD_CALL_TRACKER_TABLE}" ("status"); | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_outcome" ON "{LEAD_CALL_TRACKER_TABLE}" ("outcome"); | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_created_at" ON "{LEAD_CALL_TRACKER_TABLE}" ("created_at"); | |
| CREATE INDEX IF NOT EXISTS "idx_lead_call_tracker_call_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("call_id"); | |
| """ |
🤖 Prompt for AI Agents
In app/scripts/create_tables.py around lines 40 to 44, the SQL block creates
several indexes but is missing an index on call_id used by get_lead_by_call_id;
add a new line to the CREATE INDEX list like CREATE INDEX IF NOT EXISTS
"idx_lead_call_tracker_call_id" ON "{LEAD_CALL_TRACKER_TABLE}" ("call_id")
before the closing triple-quote so the migration creates the call_id index (then
save and run the migration to apply).
6f9de57 to
9e086c1
Compare
9e086c1 to
97a3d7d
Compare
Summary by CodeRabbit