Feat/Add API Trigger and Webhooks in Agent Builder (#83)

* feat: add api trigger node for agent runs

* feat: add webhook node

* Execute webhook nodes post workflow run

* Add hint to go to API keys
This commit is contained in:
Abhishek 2025-12-22 14:08:30 +05:30 committed by GitHub
parent 4ddb144dd0
commit 55b727a872
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 3667 additions and 494 deletions

View file

@ -19,7 +19,6 @@ from pipecat.audio.mixers.soundfile_mixer import SoundfileMixer
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
from pipecat.audio.vad.silero import SileroVADAnalyzer, VADParams
from pipecat.serializers.plivo import PlivoFrameSerializer
from pipecat.serializers.twilio import TwilioFrameSerializer
from pipecat.serializers.vobiz import VobizFrameSerializer
from pipecat.serializers.vonage import VonageFrameSerializer

View file

@ -0,0 +1,122 @@
"""Quota checking service for Dograh credits.
This module provides reusable quota checking functionality that can be used
across different endpoints (WebRTC signaling, telephony, public API triggers).
"""
from dataclasses import dataclass
from loguru import logger
from api.db import db_client
from api.db.models import UserModel
from api.services.configuration.registry import ServiceProviders
from api.services.mps_service_key_client import mps_service_key_client
@dataclass
class QuotaCheckResult:
"""Result of a quota check."""
has_quota: bool
error_message: str = ""
async def check_dograh_quota(user: UserModel) -> QuotaCheckResult:
"""Check if user has sufficient Dograh quota for making a call.
This function checks if the user is using any Dograh services (LLM, STT, TTS)
and validates that they have sufficient credits remaining.
Args:
user: The user to check quota for
Returns:
QuotaCheckResult with has_quota=True if user has sufficient quota or
is not using Dograh services, or has_quota=False with error_message
if quota is insufficient.
"""
try:
# Get user configurations
user_config = await db_client.get_user_configurations(user.id)
# Check if user is using any Dograh service
using_dograh = False
dograh_api_keys = set()
if user_config.llm and user_config.llm.provider == ServiceProviders.DOGRAH:
using_dograh = True
dograh_api_keys.add(user_config.llm.api_key)
if user_config.stt and user_config.stt.provider == ServiceProviders.DOGRAH:
using_dograh = True
dograh_api_keys.add(user_config.stt.api_key)
if user_config.tts and user_config.tts.provider == ServiceProviders.DOGRAH:
using_dograh = True
dograh_api_keys.add(user_config.tts.api_key)
# If not using Dograh, quota check passes
if not using_dograh:
return QuotaCheckResult(has_quota=True)
# Check quota for ALL Dograh keys
for api_key in dograh_api_keys:
try:
usage = await mps_service_key_client.check_service_key_usage(
api_key, created_by=user.provider_id
)
remaining = usage.get("remaining_credits", 0.0)
# Require at least $0.10 for a short call
if remaining < 0.10:
logger.warning(
f"Insufficient Dograh credits for key ...{api_key[-8:]}: "
f"${remaining:.2f} remaining"
)
return QuotaCheckResult(
has_quota=False,
error_message=(
"You have exhausted your trial credits. "
"Please email founders@dograh.com for additional Dograh credits "
"or change providers in Models configurations."
),
)
logger.info(
f"Dograh quota check passed for key ...{api_key[-8:]}: "
f"${remaining:.2f} remaining"
)
except Exception as e:
logger.error(f"Failed to check quota for Dograh key: {str(e)}")
return QuotaCheckResult(
has_quota=False,
error_message="Could not verify Dograh credits. Please try again.",
)
return QuotaCheckResult(has_quota=True)
except Exception as e:
logger.error(f"Error during quota check: {str(e)}")
# On unexpected error, allow the call to proceed
return QuotaCheckResult(has_quota=True)
async def check_dograh_quota_by_user_id(user_id: int) -> QuotaCheckResult:
"""Check Dograh quota by user ID.
Convenience function that fetches the user and then checks quota.
Args:
user_id: The ID of the user to check quota for
Returns:
QuotaCheckResult with quota status
"""
user = await db_client.get_user_by_id(user_id)
if not user:
return QuotaCheckResult(
has_quota=False,
error_message="User not found",
)
return await check_dograh_quota(user)

View file

@ -299,11 +299,11 @@ class VobizProvider(TelephonyProvider):
message handling to VobizFrameSerializer.
"""
from api.services.pipecat.run_pipeline import run_pipeline_vobiz
first_msg = await websocket.receive_text()
start_msg = json.loads(first_msg)
logger.debug(f"Received the first message: {start_msg}")
# Validate that this is a start event
if start_msg.get("event") != "start":
logger.error(f"Expected 'start' event, got: {start_msg.get('event')}")
@ -317,7 +317,7 @@ class VobizProvider(TelephonyProvider):
start_data = start_msg.get("start", {})
stream_id = start_data.get("streamId")
call_id = start_data.get("callId")
if not stream_id or not call_id:
logger.error(f"Missing streamId or callId in start event: {start_data}")
await websocket.close(code=4400, reason="Missing streamId or callId")

View file

@ -9,6 +9,8 @@ class NodeType(str, Enum):
endNode = "endCall"
agentNode = "agentNode"
globalNode = "globalNode"
trigger = "trigger"
webhook = "webhook"
class Position(BaseModel):
@ -28,9 +30,20 @@ class ExtractionVariableDTO(BaseModel):
prompt: Optional[str] = None
class CustomHeaderDTO(BaseModel):
key: str
value: str
class RetryConfigDTO(BaseModel):
enabled: bool = False
max_retries: int = 3
retry_delay_seconds: int = 5
class NodeDataDTO(BaseModel):
name: str = Field(..., min_length=1)
prompt: str = Field(..., min_length=1)
prompt: Optional[str] = Field(default=None)
is_static: bool = False
is_start: bool = False
is_end: bool = False
@ -44,6 +57,15 @@ class NodeDataDTO(BaseModel):
detect_voicemail: bool = True
delayed_start: bool = False
delayed_start_duration: Optional[float] = None
trigger_path: Optional[str] = None
# Webhook node specific fields
enabled: bool = True
http_method: Optional[str] = None
endpoint_url: Optional[str] = None
credential_uuid: Optional[str] = None
custom_headers: Optional[list[CustomHeaderDTO]] = None
payload_template: Optional[dict] = None
retry_config: Optional[RetryConfigDTO] = None
class RFNodeDTO(BaseModel):
@ -52,6 +74,14 @@ class RFNodeDTO(BaseModel):
position: Position
data: NodeDataDTO
@model_validator(mode="after")
def _validate_prompt_required(self):
"""Require prompt for all node types except trigger and webhook."""
if self.type not in (NodeType.trigger, NodeType.webhook):
if not self.data.prompt or len(self.data.prompt.strip()) == 0:
raise ValueError("Prompt is required for non-trigger nodes")
return self
class EdgeDataDTO(BaseModel):
label: str = Field(..., min_length=1)