mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-22 11:51:04 +02:00
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:
parent
4ddb144dd0
commit
55b727a872
37 changed files with 3667 additions and 494 deletions
|
|
@ -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
|
||||
|
|
|
|||
122
api/services/quota_service.py
Normal file
122
api/services/quota_service.py
Normal 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)
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue