feat: add voice alert notification endpoint and DB layer#709
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR introduces a Voice Alert Notification System featuring a new Changes
Sequence DiagramsequenceDiagram
participant Client
participant API as POST /alerts/fire
participant Redis
participant DB as Alert Groups DB
participant ValidSvc as Template/Config Validator
participant LeadSvc as Lead Tracking Service
Client->>API: AlertFireRequest (alert_id, group_name, template, ...)
API->>API: Verify ADMIN/ALERT_SYSTEM role
API->>Redis: SET dedup_key NX with TTL
alt Dedup key exists
Redis-->>API: Key already exists
API-->>Client: HTTP 202 {status: "deduplicated"}
else Dedup key set
Redis-->>API: Key set successfully
API->>DB: Fetch alert_groups by name
alt Group not found
DB-->>API: No record
API->>Redis: DEL dedup_key
API-->>Client: HTTP 404 Alert group not found
else Group found
DB-->>API: {id, members: [phone1, phone2, ...]}
API->>ValidSvc: Validate template + config
alt Validation fails
ValidSvc-->>API: Error
API->>Redis: DEL dedup_key
API-->>Client: HTTP 404 Template/config invalid
else Validation passes
ValidSvc-->>API: OK
loop For each group member
API->>LeadSvc: create_lead_call_tracker(payload, phone)
alt Lead creation succeeds
LeadSvc-->>API: lead_id
API->>API: Track queued lead
else Lead creation fails
LeadSvc-->>API: Error/None
API->>API: Track failed member
end
end
API-->>Client: HTTP 202 {status: "queued", queued: [...], failed: [...]}
end
end
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 8
🧹 Nitpick comments (4)
docs/plans/2026-04-10-voice-alert-notification.md (2)
686-691: Use Redis namespace in the dedup snippet.The snippet uses
redis.set(...)withoutnamespace, which can cause cross-service key collisions in shared Redis deployments.As per coding guidelines "Always use
namespaceparameter inredis_get/redis_setcalls to prevent key collisions across services".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-04-10-voice-alert-notification.md` around lines 686 - 691, The dedup snippet calls redis.set(...) without a namespace which can cause cross-service key collisions; update the call to include the namespace parameter (e.g., namespace=req.redis_namespace or a service-specific constant) when setting the dedup key so that the Redis operation is namespaced; locate the redis.set invocation that uses dedup_key (and the surrounding dedup logic) and add the namespace argument accordingly.
893-911: Update config snippet to avoid directos.environreads.The static config example should use the project’s env helper pattern for mandatory vars rather than direct
os.environaccess.As per coding guidelines "Load ALL configuration from
app/core/config/static.pyusingget_required_env()for mandatory variables; never import directly fromos.environelsewhere".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-04-10-voice-alert-notification.md` around lines 893 - 911, Replace direct os.environ reads in this config block with the project's env helpers from app.core.config.static: import get_required_env for mandatory vars and get_env (or equivalent) for optional ones, then replace ENABLE_HEALTH_MONITOR, HEALTH_MONITOR_INTERVAL_SECONDS, ALERT_SYSTEM_JWT_TOKEN, ALERT_RESELLER_ID, ALERT_MERCHANT_ID, ALERT_TEMPLATE, and ALERT_GROUP_NAME to use those helpers; use get_required_env for truly mandatory values (e.g., ALERT_SYSTEM_JWT_TOKEN and any required IDs), use the optional helper with default values for flags and intervals (convert the returned string to bool/int for ENABLE_HEALTH_MONITOR and HEALTH_MONITOR_INTERVAL_SECONDS), and ensure no direct os.environ usage remains in this module.app/database/queries/breeze_buddy/alert_groups.py (1)
22-22: Tighten thememberstype annotation.
members: listis too generic; use a concrete typed shape (e.g.,List[Dict[str, str]]or a typed alias) to improve static checks.As per coding guidelines "Use type hints on all function signatures (Optional[T], List[T], Dict[str, Any], Union)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/queries/breeze_buddy/alert_groups.py` at line 22, The function upsert_alert_group_query currently types members as a bare list; change its signature to a concrete typed collection (e.g., members: List[Dict[str, str]] or a typed alias like AlertMember = Dict[str, str]) and update imports (from typing import List, Dict or the alias) so static checkers can validate member shape; ensure any references inside upsert_alert_group_query and its callers use the chosen concrete type.app/database/accessor/breeze_buddy/alert_groups.py (1)
16-27: Use a typed decoder model for alert groups.Returning raw
Dict[str, Any]from_decode_alert_groupmakes field shape drift harder to catch. Prefer decoding into a Pydantic model for the DB decoder layer.As per coding guidelines "Follow three-layer database pattern: queries (SQL builders) -> accessor (business logic) -> decoder (row to Pydantic)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/database/accessor/breeze_buddy/alert_groups.py` around lines 16 - 27, The _decode_alert_group function currently returns a raw Dict[str, Any]; change it to construct and return a typed Pydantic model (e.g., AlertGroup) instead so the decoder layer guarantees shape. Update the function signature to -> AlertGroup, parse members the same way (json.loads if str), build a dict with id, name, members, created_at, updated_at and pass it to AlertGroup.parse_obj (or AlertGroup(**data)) and return that instance; ensure you import or define the AlertGroup model with matching fields so callers of _decode_alert_group receive a validated model.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/routers/breeze_buddy/alerts/__init__.py`:
- Around line 32-67: The string fields alert_id, alert_group_name, template,
reseller_id, merchant_id and alert_message currently accept empty or
whitespace-only values; add validation to reject blank identifiers by either
switching their types to constrained strings (e.g., use
pydantic.constr(strip_whitespace=True, min_length=1) for alert_id,
alert_group_name, template, reseller_id and alert_message and
Optional[constr(...)] for merchant_id) or add a `@validator` on those fields that
strips whitespace and raises ValueError if the result is empty, so
dedup/group/template lookups never receive empty keys.
In `@app/api/routers/breeze_buddy/alerts/handlers.py`:
- Around line 40-48: The dedup key is currently built only from req.alert_id and
redis.set is called without a namespace, allowing cross-tenant/service
collisions; update the dedup key construction and redis call to include
tenant/service context (e.g., incorporate req.reseller_id or req.tenant_id into
dedup_key alongside req.alert_id) and pass the namespace argument to the Redis
helper (use the project's redis_set/redis_get wrapper or add
namespace="breeze_buddy.alerts" to redis.set) so keys are both namespaced and
scoped per-tenant; adjust references to dedup_key and the redis.set call
accordingly.
- Around line 75-84: The early return when no members (checking members and
returning status "no_members") and the final return when leads_queued == 0 must
release the dedup lock so retries aren't suppressed; locate the dedup key
variable (dedup_key or similar) and the dedup store API used elsewhere (e.g.,
set_dedup_key/remove_dedup_key or release_dedup_lock) and call the appropriate
release/remove function just before each return (both in the members-empty
branch and the final block that returns leads_queued 0). Ensure you reference
the same dedup identifier used when the lock was set so the lock is cleared
reliably on these zero-leads exit paths.
- Around line 39-43: The code eagerly calls get_redis_service() even when dedup
is disabled, which can cause Redis outages to block alert handling; change the
logic so get_redis_service() is only called when req.dedup_ttl_seconds > 0
(i.e., move the redis = await get_redis_service() and dedup_key creation into
the dedup branch), use get_redis_service() and dedup_key only inside the dedup
block that checks req.dedup_ttl_seconds, and ensure any later references to
redis/dedup_key are guarded by that same condition.
- Around line 165-177: The logs currently emit raw phone numbers in logger.info
and logger.error; replace those uses of the phone variable with a masked value
(e.g., keep only last 4 digits and replace the rest with asterisks) by
creating/using a small helper like mask_phone_number(phone) and assign
masked_phone = mask_phone_number(phone) and then use masked_phone in the
logger.info(...) and logger.error(...) calls (and optionally in the
failed.append dict) so PII is not written to logs.
In `@app/database/migrations/023_create_alert_groups_table.sql`:
- Line 13: Remove the redundant index creation for alert_groups.name: the column
is declared as UNIQUE (name TEXT NOT NULL UNIQUE) which already creates a unique
index, so delete the CREATE INDEX IF NOT EXISTS idx_alert_groups_name ON
alert_groups (name); line from the migration (referencing idx_alert_groups_name
and table alert_groups) to avoid duplicate indexing and extra write overhead.
In `@docs/plans/2026-04-10-voice-alert-notification.md`:
- Around line 1404-1426: The markdown under "Part 4" uses level-3 headings (###)
but should use level-2 (##) to avoid an MD001 heading level jump; update the
headings "Adding a Twilio Balance Poller", "Adding sequential call mode
(escalation)", "Adding a pre-recorded fallback MP3", and "Admin API for alert
groups" to be level-2 (prefix with '##') so they are directly under the "# Part
4" header and conform to the lint rule.
- Line 21: Several fenced code blocks in the markdown (currently just triple
backticks ``` with no language) are missing language identifiers and trigger
MD040; update each ``` block in
docs/plans/2026-04-10-voice-alert-notification.md to include an appropriate
language tag (for example `text`, `yaml`, `python`, `sql`, etc.) matching the
block content so the linter accepts them (this applies to the code fences
flagged by the review and any other triple-backtick blocks in the same file).
---
Nitpick comments:
In `@app/database/accessor/breeze_buddy/alert_groups.py`:
- Around line 16-27: The _decode_alert_group function currently returns a raw
Dict[str, Any]; change it to construct and return a typed Pydantic model (e.g.,
AlertGroup) instead so the decoder layer guarantees shape. Update the function
signature to -> AlertGroup, parse members the same way (json.loads if str),
build a dict with id, name, members, created_at, updated_at and pass it to
AlertGroup.parse_obj (or AlertGroup(**data)) and return that instance; ensure
you import or define the AlertGroup model with matching fields so callers of
_decode_alert_group receive a validated model.
In `@app/database/queries/breeze_buddy/alert_groups.py`:
- Line 22: The function upsert_alert_group_query currently types members as a
bare list; change its signature to a concrete typed collection (e.g., members:
List[Dict[str, str]] or a typed alias like AlertMember = Dict[str, str]) and
update imports (from typing import List, Dict or the alias) so static checkers
can validate member shape; ensure any references inside upsert_alert_group_query
and its callers use the chosen concrete type.
In `@docs/plans/2026-04-10-voice-alert-notification.md`:
- Around line 686-691: The dedup snippet calls redis.set(...) without a
namespace which can cause cross-service key collisions; update the call to
include the namespace parameter (e.g., namespace=req.redis_namespace or a
service-specific constant) when setting the dedup key so that the Redis
operation is namespaced; locate the redis.set invocation that uses dedup_key
(and the surrounding dedup logic) and add the namespace argument accordingly.
- Around line 893-911: Replace direct os.environ reads in this config block with
the project's env helpers from app.core.config.static: import get_required_env
for mandatory vars and get_env (or equivalent) for optional ones, then replace
ENABLE_HEALTH_MONITOR, HEALTH_MONITOR_INTERVAL_SECONDS, ALERT_SYSTEM_JWT_TOKEN,
ALERT_RESELLER_ID, ALERT_MERCHANT_ID, ALERT_TEMPLATE, and ALERT_GROUP_NAME to
use those helpers; use get_required_env for truly mandatory values (e.g.,
ALERT_SYSTEM_JWT_TOKEN and any required IDs), use the optional helper with
default values for flags and intervals (convert the returned string to bool/int
for ENABLE_HEALTH_MONITOR and HEALTH_MONITOR_INTERVAL_SECONDS), and ensure no
direct os.environ usage remains in this module.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1b1785bb-b074-4e6d-9e5e-d1057f04a863
📒 Files selected for processing (9)
app/api/routers/breeze_buddy/__init__.pyapp/api/routers/breeze_buddy/alerts/__init__.pyapp/api/routers/breeze_buddy/alerts/handlers.pyapp/database/accessor/__init__.pyapp/database/accessor/breeze_buddy/alert_groups.pyapp/database/migrations/023_create_alert_groups_table.sqlapp/database/queries/breeze_buddy/alert_groups.pyapp/schemas/breeze_buddy/auth.pydocs/plans/2026-04-10-voice-alert-notification.md
| alert_id: str = Field( | ||
| ..., | ||
| description="Unique alert identifier for deduplication", | ||
| examples=["stt-degraded", "tts-elevenlabs-down"], | ||
| ) | ||
|
|
||
| # Which group of people to call | ||
| alert_group_name: str = Field( | ||
| ..., | ||
| description="Name of the alert group (must exist in alert_groups table)", | ||
| examples=["platform-oncall"], | ||
| ) | ||
|
|
||
| # Human-readable message spoken via TTS | ||
| # The caller builds this string — the endpoint just passes it through | ||
| alert_message: str = Field( | ||
| ..., | ||
| description="Alert message spoken via TTS on the call", | ||
| examples=["STT degraded: 47 errors in 5 minutes"], | ||
| ) | ||
|
|
||
| # BB template config — fully reuses existing template machinery | ||
| reseller_id: str = Field(..., description="Reseller ID for the alert template") | ||
| merchant_id: Optional[str] = Field(None, description="Merchant ID (optional)") | ||
| template: str = Field( | ||
| ..., | ||
| description="BB template name (must exist in templates table)", | ||
| examples=["alert-voice-notification"], | ||
| ) | ||
|
|
||
| # How long (seconds) to suppress duplicate alerts for this alert_id | ||
| dedup_ttl_seconds: int = Field( | ||
| default=300, | ||
| ge=0, | ||
| description="Dedup window in seconds. 0 = no dedup.", | ||
| ) |
There was a problem hiding this comment.
Reject blank identifiers to prevent invalid dedup/group/template operations.
Required string fields currently allow ""/whitespace-only values. That can produce invalid dedup keys (e.g., empty alert_id) and ambiguous downstream lookups.
Proposed validation hardening
alert_id: str = Field(
...,
+ min_length=1,
description="Unique alert identifier for deduplication",
examples=["stt-degraded", "tts-elevenlabs-down"],
)
@@
alert_group_name: str = Field(
...,
+ min_length=1,
description="Name of the alert group (must exist in alert_groups table)",
examples=["platform-oncall"],
)
@@
alert_message: str = Field(
...,
+ min_length=1,
description="Alert message spoken via TTS on the call",
examples=["STT degraded: 47 errors in 5 minutes"],
)
@@
- reseller_id: str = Field(..., description="Reseller ID for the alert template")
- merchant_id: Optional[str] = Field(None, description="Merchant ID (optional)")
+ reseller_id: str = Field(..., min_length=1, description="Reseller ID for the alert template")
+ merchant_id: Optional[str] = Field(None, min_length=1, description="Merchant ID (optional)")
@@
template: str = Field(
...,
+ min_length=1,
description="BB template name (must exist in templates table)",
examples=["alert-voice-notification"],
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/alerts/__init__.py` around lines 32 - 67, The
string fields alert_id, alert_group_name, template, reseller_id, merchant_id and
alert_message currently accept empty or whitespace-only values; add validation
to reject blank identifiers by either switching their types to constrained
strings (e.g., use pydantic.constr(strip_whitespace=True, min_length=1) for
alert_id, alert_group_name, template, reseller_id and alert_message and
Optional[constr(...)] for merchant_id) or add a `@validator` on those fields that
strips whitespace and raises ValueError if the result is empty, so
dedup/group/template lookups never receive empty keys.
| redis = await get_redis_service() | ||
| dedup_key = f"alert:dedup:{req.alert_id}" | ||
|
|
||
| # ── Step 1: Dedup ────────────────────────────────────────────── | ||
| if req.dedup_ttl_seconds > 0: |
There was a problem hiding this comment.
Avoid requiring Redis when dedup is disabled.
Line 39 initializes Redis even when dedup_ttl_seconds == 0, so Redis outages can block non-dedup alert firing.
💡 Suggested change
- redis = await get_redis_service()
+ redis = None
dedup_key = f"alert:dedup:{req.alert_id}"
# ── Step 1: Dedup ──────────────────────────────────────────────
if req.dedup_ttl_seconds > 0:
+ redis = await get_redis_service()
acquired = await redis.set(
key=dedup_key,
value="1",
nx=True,
ex=req.dedup_ttl_seconds,
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/alerts/handlers.py` around lines 39 - 43, The
code eagerly calls get_redis_service() even when dedup is disabled, which can
cause Redis outages to block alert handling; change the logic so
get_redis_service() is only called when req.dedup_ttl_seconds > 0 (i.e., move
the redis = await get_redis_service() and dedup_key creation into the dedup
branch), use get_redis_service() and dedup_key only inside the dedup block that
checks req.dedup_ttl_seconds, and ensure any later references to redis/dedup_key
are guarded by that same condition.
| dedup_key = f"alert:dedup:{req.alert_id}" | ||
|
|
||
| # ── Step 1: Dedup ────────────────────────────────────────────── | ||
| if req.dedup_ttl_seconds > 0: | ||
| acquired = await redis.set( | ||
| key=dedup_key, | ||
| value="1", | ||
| nx=True, | ||
| ex=req.dedup_ttl_seconds, |
There was a problem hiding this comment.
Scope and namespace the dedup key to prevent cross-tenant/service suppression.
Line 40 keys only by alert_id, so identical IDs across resellers can deduplicate each other. Also, this flow does not use a Redis namespace, which increases collision risk across services.
As per coding guidelines, "Always use namespace parameter in redis_get/redis_set calls to prevent key collisions across services".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/alerts/handlers.py` around lines 40 - 48, The
dedup key is currently built only from req.alert_id and redis.set is called
without a namespace, allowing cross-tenant/service collisions; update the dedup
key construction and redis call to include tenant/service context (e.g.,
incorporate req.reseller_id or req.tenant_id into dedup_key alongside
req.alert_id) and pass the namespace argument to the Redis helper (use the
project's redis_set/redis_get wrapper or add namespace="breeze_buddy.alerts" to
redis.set) so keys are both namespaced and scoped per-tenant; adjust references
to dedup_key and the redis.set call accordingly.
| if not members: | ||
| logger.warning( | ||
| f"Alert group '{req.alert_group_name}' has no members — no calls fired" | ||
| ) | ||
| return { | ||
| "status": "no_members", | ||
| "alert_id": req.alert_id, | ||
| "alert_group_name": req.alert_group_name, | ||
| "leads_queued": 0, | ||
| } |
There was a problem hiding this comment.
Release dedup lock when zero leads are queued.
When group has no members (Lines 75-84) or all member queue attempts fail (final return block), the dedup key remains set and suppresses retries even though nothing was fired.
💡 Suggested change
if not members:
+ if req.dedup_ttl_seconds > 0:
+ await redis.delete(dedup_key)
logger.warning(
f"Alert group '{req.alert_group_name}' has no members — no calls fired"
)
return {
"status": "no_members",
@@
- return {
+ if req.dedup_ttl_seconds > 0 and len(queued_leads) == 0:
+ await redis.delete(dedup_key)
+
+ return {
"status": "queued",
"alert_id": req.alert_id,
"alert_group_name": req.alert_group_name,
"leads_queued": len(queued_leads),
"leads": queued_leads,
"failed": failed,
}Also applies to: 182-189
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/alerts/handlers.py` around lines 75 - 84, The
early return when no members (checking members and returning status
"no_members") and the final return when leads_queued == 0 must release the dedup
lock so retries aren't suppressed; locate the dedup key variable (dedup_key or
similar) and the dedup store API used elsewhere (e.g.,
set_dedup_key/remove_dedup_key or release_dedup_lock) and call the appropriate
release/remove function just before each return (both in the members-empty
branch and the final block that returns leads_queued 0). Ensure you reference
the same dedup identifier used when the lock was set so the lock is cleared
reliably on these zero-leads exit paths.
| logger.info( | ||
| f"Alert lead queued: {lead_id} → {phone} " f"(alert={req.alert_id})" | ||
| ) | ||
| else: | ||
| failed.append( | ||
| { | ||
| "phone": phone, | ||
| "reason": "create_lead_call_tracker returned None", | ||
| } | ||
| ) | ||
| except Exception as e: | ||
| logger.error( | ||
| f"Failed to queue alert lead for {phone}: {e}", |
There was a problem hiding this comment.
Mask phone numbers in logs to reduce PII exposure.
Lines 166 and 177 log full phone numbers. This is a privacy/compliance risk in centralized logs.
💡 Suggested change
+def _mask_phone(phone: str) -> str:
+ return f"***{phone[-4:]}" if len(phone) >= 4 else "***"
@@
- logger.info(
- f"Alert lead queued: {lead_id} → {phone} " f"(alert={req.alert_id})"
- )
+ logger.info(
+ f"Alert lead queued: {lead_id} → {_mask_phone(phone)} "
+ f"(alert={req.alert_id})"
+ )
@@
- logger.error(
- f"Failed to queue alert lead for {phone}: {e}",
+ logger.error(
+ f"Failed to queue alert lead for {_mask_phone(phone)}: {e}",
exc_info=True,
)🧰 Tools
🪛 Ruff (0.15.10)
[warning] 175-175: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/routers/breeze_buddy/alerts/handlers.py` around lines 165 - 177, The
logs currently emit raw phone numbers in logger.info and logger.error; replace
those uses of the phone variable with a masked value (e.g., keep only last 4
digits and replace the rest with asterisks) by creating/using a small helper
like mask_phone_number(phone) and assign masked_phone = mask_phone_number(phone)
and then use masked_phone in the logger.info(...) and logger.error(...) calls
(and optionally in the failed.append dict) so PII is not written to logs.
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_alert_groups_name ON alert_groups (name); |
There was a problem hiding this comment.
Remove redundant index on name.
name TEXT NOT NULL UNIQUE already creates an index; idx_alert_groups_name duplicates it and adds unnecessary write overhead.
Proposed migration adjustment
-CREATE INDEX IF NOT EXISTS idx_alert_groups_name ON alert_groups (name);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/database/migrations/023_create_alert_groups_table.sql` at line 13, Remove
the redundant index creation for alert_groups.name: the column is declared as
UNIQUE (name TEXT NOT NULL UNIQUE) which already creates a unique index, so
delete the CREATE INDEX IF NOT EXISTS idx_alert_groups_name ON alert_groups
(name); line from the migration (referencing idx_alert_groups_name and table
alert_groups) to avoid duplicate indexing and extra write overhead.
|
|
||
| ## 1.1 System Architecture — Two Decoupled Layers | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Add fenced-code languages to pass markdown lint.
These code fences are missing a language identifier (text, python, sql, etc.), which triggers MD040.
Also applies to: 103-103, 124-124
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 21-21: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-04-10-voice-alert-notification.md` at line 21, Several fenced
code blocks in the markdown (currently just triple backticks ``` with no
language) are missing language identifiers and trigger MD040; update each ```
block in docs/plans/2026-04-10-voice-alert-notification.md to include an
appropriate language tag (for example `text`, `yaml`, `python`, `sql`, etc.)
matching the block content so the linter accepts them (this applies to the code
fences flagged by the review and any other triple-backtick blocks in the same
file).
| ### Adding a Twilio Balance Poller | ||
|
|
||
| 1. Create `app/services/health_monitor/balance_poller.py` — call Twilio API, check balance | ||
| 2. In `app/services/health_monitor/task.py`, add a second `register_task()` call | ||
| 3. The poller calls `_fire_voice_alert()` from `monitor.py` — reuses the same `/alerts/fire` flow | ||
| 4. Zero changes to the alert endpoint, router, DB, or dedup logic | ||
|
|
||
| ### Adding sequential call mode (escalation) | ||
|
|
||
| 1. Alert endpoint pushes first member immediately, stores remaining members in Redis list `alert:escalation:{alert_id}` | ||
| 2. Telephony webhook handler checks: if lead finished with NO_ANSWER/BUSY → pop next from Redis list → push next lead | ||
| 3. Requires webhook handler modification — out of scope for v1 | ||
|
|
||
| ### Adding a pre-recorded fallback MP3 | ||
|
|
||
| 1. Upload a generic alert MP3 to GCS | ||
| 2. Configure the `alert-voice-notification` template with a `fallback_audio_url` | ||
| 3. When TTS synthesis fails in `prepare_and_store_initial_greeting()`, the pipeline plays the fallback | ||
| 4. Requires minor pipeline change to support `fallback_audio_url` — out of scope for v1 | ||
|
|
||
| ### Admin API for alert groups | ||
|
|
||
| 1. Add CRUD endpoints: `POST /alert-groups`, `GET /alert-groups`, `PUT /alert-groups/{name}`, `DELETE /alert-groups/{name}` |
There was a problem hiding this comment.
Fix heading level jump under Part 4.
After # Part 4, the next heading should be ## ... (not ### ...) to satisfy MD001.
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 1404-1404: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-04-10-voice-alert-notification.md` around lines 1404 - 1426,
The markdown under "Part 4" uses level-3 headings (###) but should use level-2
(##) to avoid an MD001 heading level jump; update the headings "Adding a Twilio
Balance Poller", "Adding sequential call mode (escalation)", "Adding a
pre-recorded fallback MP3", and "Admin API for alert groups" to be level-2
(prefix with '##') so they are directly under the "# Part 4" header and conform
to the lint rule.
There was a problem hiding this comment.
Pull request overview
Adds a new voice-alert firing surface area to Clairvoyance’s Breeze Buddy stack by introducing an /alerts/fire endpoint and an alert_groups DB table used to resolve on-call phone lists, plus a new JWT role for system-driven alerting.
Changes:
- Adds
alert_groupsmigration, query builder, and DB accessor for resolving alert group members. - Introduces
POST /alerts/firerouter + handler with Redis SETNX-based deduplication and lead creation. - Extends Breeze Buddy auth roles with
ALERT_SYSTEMand registers the alerts router.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/plans/2026-04-10-voice-alert-notification.md | Design/implementation plan for the voice alert notification system. |
| app/schemas/breeze_buddy/auth.py | Adds ALERT_SYSTEM to UserRole. |
| app/database/migrations/023_create_alert_groups_table.sql | Creates the alert_groups table. |
| app/database/queries/breeze_buddy/alert_groups.py | Adds parameterized SQL builders for alert group get/upsert. |
| app/database/accessor/breeze_buddy/alert_groups.py | Adds async accessors for alert group retrieval/upsert. |
| app/database/accessor/init.py | Exports new alert group accessors. |
| app/api/routers/breeze_buddy/alerts/init.py | Adds /alerts/fire endpoint + request schema + role gate. |
| app/api/routers/breeze_buddy/alerts/handlers.py | Implements dedup, group resolution, template/config validation, and lead enqueueing. |
| app/api/routers/breeze_buddy/init.py | Registers alerts router under Breeze Buddy API. |
| CREATE TABLE IF NOT EXISTS alert_groups ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| name TEXT NOT NULL UNIQUE, | ||
| members JSONB NOT NULL DEFAULT '[]', |
| > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. | ||
|
|
| def _decode_alert_group(row) -> Dict[str, Any]: | ||
| """Decode an asyncpg Record into a dict.""" | ||
| members = row["members"] | ||
| if isinstance(members, str): | ||
| members = json.loads(members) | ||
| return { | ||
| "id": str(row["id"]), | ||
| "name": row["name"], | ||
| "members": members, | ||
| "created_at": row["created_at"], | ||
| "updated_at": row["updated_at"], | ||
| } |
| RESELLER = "reseller" | ||
| MERCHANT = "merchant" | ||
| USER = "user" # Renamed from SHOP | ||
| ALERT_SYSTEM = "alert_system" |
| if current_user.role not in (UserRole.ADMIN, UserRole.ALERT_SYSTEM): | ||
| raise HTTPException( | ||
| status_code=status.HTTP_403_FORBIDDEN, | ||
| detail="Only ADMIN or ALERT_SYSTEM roles can fire alerts", | ||
| ) | ||
|
|
||
| return await fire_alert_handler(req, current_user) |
| members: List[Dict[str, str]] = group.get("members", []) | ||
| if not members: | ||
| logger.warning( | ||
| f"Alert group '{req.alert_group_name}' has no members — no calls fired" | ||
| ) | ||
| return { | ||
| "status": "no_members", | ||
| "alert_id": req.alert_id, | ||
| "alert_group_name": req.alert_group_name, | ||
| "leads_queued": 0, | ||
| } |
| "customer_mobile_number": phone, | ||
| "customer_name": name, | ||
| "alert_message": req.alert_message, | ||
| "alert_id": req.alert_id, | ||
| **(req.extra_payload or {}), |
| return { | ||
| "status": "queued", | ||
| "alert_id": req.alert_id, | ||
| "alert_group_name": req.alert_group_name, | ||
| "leads_queued": len(queued_leads), | ||
| "leads": queued_leads, | ||
| "failed": failed, | ||
| } |
| async def fire_alert_handler(req: Any, current_user: UserInfo) -> Dict[str, Any]: | ||
| """ |
|
|
||
| # ── Step 1: Dedup ────────────────────────────────────────────── | ||
| if req.dedup_ttl_seconds > 0: | ||
| acquired = await redis.set( | ||
| key=dedup_key, | ||
| value="1", | ||
| nx=True, | ||
| ex=req.dedup_ttl_seconds, | ||
| ) | ||
| if not acquired: | ||
| logger.info( | ||
| f"Alert '{req.alert_id}' deduplicated (TTL key exists in Redis)" | ||
| ) | ||
| return { | ||
| "status": "deduplicated", | ||
| "alert_id": req.alert_id, | ||
| "message": ( | ||
| f"Alert suppressed — already fired within " | ||
| f"{req.dedup_ttl_seconds}s window" | ||
| ), | ||
| } | ||
|
|
||
| # ── Step 2: Resolve alert group ──────────────────────────────── | ||
| group = await get_alert_group_by_name(req.alert_group_name) | ||
| if not group: | ||
| # Release dedup key so next retry can attempt again | ||
| if req.dedup_ttl_seconds > 0: |
| "alert_group": req.alert_group_name, | ||
| }, | ||
| request_id=request_id, | ||
| execution_mode=ExecutionMode.TELEPHONY, |
There was a problem hiding this comment.
Can we defined a new executionMode for alerts such as TELEPHONY_ALERT, so that for tracking it will be easy.
| CREATE TABLE IF NOT EXISTS alert_groups ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| name TEXT NOT NULL UNIQUE, | ||
| members JSONB NOT NULL DEFAULT '[]', |
There was a problem hiding this comment.
If you want to give this alerting system for external people, let's say for infra, then without proper reseller and merchantId how will we configure alerts ?
Standalone alerting service for firing voice calls to on-call groups. - POST /alerts/fire endpoint with Redis SETNX deduplication - alert_groups DB table (migration 023) + queries + accessors - ALERT_SYSTEM role added to UserRole enum for JWT auth - Fully parameterized via request body (no config dependencies) - Reuses existing BB template/lead machinery end-to-end
Standalone alerting service for firing voice calls to on-call groups.
Summary by CodeRabbit
New Features
Documentation