-
Notifications
You must be signed in to change notification settings - Fork 11
Backend: feat - voice buddy lead and connect api integaration #1382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| module Kernel.External.VoiceSdk.BreezeBuddy.Flow where | ||
|
|
||
| import EulerHS.Types as Euler | ||
| import Kernel.External.VoiceSdk.BreezeBuddy.Types | ||
| import Kernel.Prelude | ||
| import qualified Kernel.Tools.Metrics.CoreMetrics as Metrics | ||
| import Kernel.Types.Error | ||
| import Kernel.Utils.Common | ||
| import Servant hiding (throwError) | ||
|
|
||
| type CreateLeadAPI = | ||
| "agent" | ||
| :> "voice" | ||
| :> "breeze-buddy" | ||
| :> "leads" | ||
| :> Header "Authorization" Text | ||
| :> ReqBody '[JSON] BreezeBuddyLeadRequest | ||
| :> Post '[JSON] BreezeBuddyLeadResponse | ||
|
|
||
| type ConnectAPI = | ||
| "agent" | ||
| :> "voice" | ||
| :> "breeze-buddy" | ||
| :> "connect" | ||
| :> Header "Authorization" Text | ||
| :> ReqBody '[JSON] BreezeBuddyConnectRequest | ||
| :> Post '[JSON] BreezeBuddyConnectResponse | ||
|
|
||
| createLead :: | ||
| (Metrics.CoreMetrics m, MonadFlow m, HasRequestId r, MonadReader r m) => | ||
| BaseUrl -> | ||
| Text -> | ||
| BreezeBuddyLeadRequest -> | ||
| m BreezeBuddyLeadResponse | ||
| createLead url apiKey request = do | ||
| let proxy = Proxy @CreateLeadAPI | ||
| eulerClient = Euler.client proxy (Just ("Bearer " <> apiKey)) request | ||
| callBreezeBuddyAPI url eulerClient "breeze-buddy-create-lead" proxy | ||
|
|
||
| connect :: | ||
| (Metrics.CoreMetrics m, MonadFlow m, HasRequestId r, MonadReader r m) => | ||
| BaseUrl -> | ||
| Text -> | ||
| BreezeBuddyConnectRequest -> | ||
| m BreezeBuddyConnectResponse | ||
| connect url apiKey request = do | ||
| let proxy = Proxy @ConnectAPI | ||
| eulerClient = Euler.client proxy (Just ("Bearer " <> apiKey)) request | ||
| callBreezeBuddyAPI url eulerClient "breeze-buddy-connect" proxy | ||
|
|
||
| callBreezeBuddyAPI :: (MonadFlow m, HasRequestId r, MonadReader r m) => CallAPI' m r api res res | ||
| callBreezeBuddyAPI url eulerClient description proxy = do | ||
| callAPI url eulerClient description proxy | ||
| >>= fromEitherM (\err -> InternalError $ "Failed to call " <> description <> " API: " <> show err) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| module Kernel.External.VoiceSdk.BreezeBuddy.Types where | ||
|
|
||
| import qualified Data.Aeson as A | ||
| import Kernel.External.Encryption | ||
| import Kernel.Prelude | ||
|
|
||
| data BreezeBuddySdkConfig = BreezeBuddySdkConfig | ||
| { url :: BaseUrl, | ||
| -- | Bearer token used to authenticate against the BreezeBuddy voice agent APIs. | ||
| apiKey :: EncryptedField 'AsEncrypted Text | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | ||
|
|
||
| -- | Request body for @POST /agent/voice/breeze-buddy/leads@. | ||
| -- Optional fields are omitted from the JSON when 'Nothing'. | ||
| data BreezeBuddyLeadRequest = BreezeBuddyLeadRequest | ||
| { request_id :: Text, | ||
| template :: Text, | ||
| -- | Template specific payload. @customer_mobile_number@ is mandatory and must be E.164 (e.g. "+14155551234"). | ||
| payload :: A.Value, | ||
| reseller_id :: Text, | ||
| merchant_id :: Maybe Text, | ||
| reporting_webhook_url :: Maybe Text, | ||
| execution_mode :: Maybe Text, | ||
| is_playground :: Maybe Bool, | ||
| configurations_override :: Maybe A.Value | ||
| } | ||
| deriving (Show, Eq, Generic) | ||
|
|
||
| instance FromJSON BreezeBuddyLeadRequest where | ||
| parseJSON = A.genericParseJSON A.defaultOptions {A.omitNothingFields = True} | ||
|
|
||
| instance ToJSON BreezeBuddyLeadRequest where | ||
| toJSON = A.genericToJSON A.defaultOptions {A.omitNothingFields = True} | ||
|
|
||
| -- | Success (201) response for @POST .../leads@. | ||
| data BreezeBuddyLeadResponse = BreezeBuddyLeadResponse | ||
| { -- | Always "queued" on success. | ||
| status :: Text, | ||
| -- | Canonical lead identifier; pass this as @lead_id@ to the connect endpoint. | ||
| lead_call_tracker_id :: Text, | ||
| -- | Echoes the submitted @request_id@. | ||
| order_id :: Text, | ||
| message :: Text | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | ||
|
|
||
| newtype BreezeBuddyConnectRequest = BreezeBuddyConnectRequest | ||
| { lead_id :: Text | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | ||
|
|
||
| -- | Success response for @POST .../connect@. | ||
| data BreezeBuddyConnectResponse = BreezeBuddyConnectResponse | ||
| { room_url :: Text, | ||
| token :: Text, | ||
| session_id :: Text, | ||
| lead_id :: Text | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||||||||||||||||||||
| module Kernel.External.VoiceSdk.Interface | ||||||||||||||||||||||||
| ( module Reexport, | ||||||||||||||||||||||||
| module Kernel.External.VoiceSdk.Interface, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| where | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import qualified Kernel.External.VoiceSdk.Interface.BreezeBuddy as BreezeBuddy | ||||||||||||||||||||||||
| import Kernel.External.VoiceSdk.Interface.Types | ||||||||||||||||||||||||
| import Kernel.External.VoiceSdk.Types as Reexport | ||||||||||||||||||||||||
| import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics) | ||||||||||||||||||||||||
| import Kernel.Utils.Common | ||||||||||||||||||||||||
|
Comment on lines
+7
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Import This is the only new Haskell module here that skips the required Prelude import. Please add Proposed fix import qualified Kernel.External.VoiceSdk.Interface.BreezeBuddy as BreezeBuddy
import Kernel.External.VoiceSdk.Interface.Types
import Kernel.External.VoiceSdk.Types as Reexport
+import Kernel.Prelude
import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics)
import Kernel.Utils.CommonAs per coding guidelines, " 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| createLead :: | ||||||||||||||||||||||||
| ( EncFlow m r, | ||||||||||||||||||||||||
| CoreMetrics m, | ||||||||||||||||||||||||
| HasRequestId r | ||||||||||||||||||||||||
| ) => | ||||||||||||||||||||||||
| VoiceSdkConfig -> | ||||||||||||||||||||||||
| CreateLeadReq -> | ||||||||||||||||||||||||
| m CreateLeadResp | ||||||||||||||||||||||||
| createLead serviceConfig req = case serviceConfig of | ||||||||||||||||||||||||
| BreezeBuddyVoiceSdkConfig cfg -> BreezeBuddy.createLead cfg req | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| connect :: | ||||||||||||||||||||||||
| ( EncFlow m r, | ||||||||||||||||||||||||
| CoreMetrics m, | ||||||||||||||||||||||||
| HasRequestId r | ||||||||||||||||||||||||
| ) => | ||||||||||||||||||||||||
| VoiceSdkConfig -> | ||||||||||||||||||||||||
| ConnectReq -> | ||||||||||||||||||||||||
| m ConnectResp | ||||||||||||||||||||||||
| connect serviceConfig req = case serviceConfig of | ||||||||||||||||||||||||
| BreezeBuddyVoiceSdkConfig cfg -> BreezeBuddy.connect cfg req | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| module Kernel.External.VoiceSdk.Interface.BreezeBuddy where | ||
|
|
||
| import Kernel.External.Encryption | ||
| import qualified Kernel.External.VoiceSdk.BreezeBuddy.Flow as BreezeBuddy | ||
| import qualified Kernel.External.VoiceSdk.BreezeBuddy.Types as BreezeBuddyTypes | ||
| import Kernel.External.VoiceSdk.Interface.Types | ||
| import Kernel.Prelude | ||
| import qualified Kernel.Tools.Metrics.CoreMetrics as Metrics | ||
| import Kernel.Utils.Common | ||
|
|
||
| createLead :: | ||
| ( Metrics.CoreMetrics m, | ||
| EncFlow m r, | ||
| HasRequestId r, | ||
| MonadReader r m | ||
| ) => | ||
| BreezeBuddyTypes.BreezeBuddySdkConfig -> | ||
| CreateLeadReq -> | ||
| m CreateLeadResp | ||
| createLead config req = do | ||
| apiKey <- decrypt config.apiKey | ||
| resp <- BreezeBuddy.createLead config.url apiKey (toBreezeBuddyLeadRequest req) | ||
| pure $ fromBreezeBuddyLeadResponse resp | ||
|
|
||
| connect :: | ||
| ( Metrics.CoreMetrics m, | ||
| EncFlow m r, | ||
| HasRequestId r, | ||
| MonadReader r m | ||
| ) => | ||
| BreezeBuddyTypes.BreezeBuddySdkConfig -> | ||
| ConnectReq -> | ||
| m ConnectResp | ||
| connect config req = do | ||
| apiKey <- decrypt config.apiKey | ||
| resp <- BreezeBuddy.connect config.url apiKey (toBreezeBuddyConnectRequest req) | ||
| pure $ fromBreezeBuddyConnectResponse resp | ||
|
|
||
| toBreezeBuddyLeadRequest :: CreateLeadReq -> BreezeBuddyTypes.BreezeBuddyLeadRequest | ||
| toBreezeBuddyLeadRequest req = | ||
| BreezeBuddyTypes.BreezeBuddyLeadRequest | ||
| { request_id = req.requestId, | ||
| template = req.template, | ||
| payload = req.payload, | ||
| reseller_id = req.resellerId, | ||
| merchant_id = req.merchantId, | ||
| reporting_webhook_url = req.reportingWebhookUrl, | ||
| execution_mode = req.executionMode, | ||
| is_playground = req.isPlayground, | ||
| configurations_override = req.configurationsOverride | ||
| } | ||
|
|
||
| fromBreezeBuddyLeadResponse :: BreezeBuddyTypes.BreezeBuddyLeadResponse -> CreateLeadResp | ||
| fromBreezeBuddyLeadResponse resp = | ||
| CreateLeadResp | ||
| { status = resp.status, | ||
| leadCallTrackerId = resp.lead_call_tracker_id, | ||
| orderId = resp.order_id, | ||
| message = resp.message | ||
| } | ||
|
|
||
| toBreezeBuddyConnectRequest :: ConnectReq -> BreezeBuddyTypes.BreezeBuddyConnectRequest | ||
| toBreezeBuddyConnectRequest req = | ||
| BreezeBuddyTypes.BreezeBuddyConnectRequest | ||
| { lead_id = req.leadId | ||
| } | ||
|
|
||
| fromBreezeBuddyConnectResponse :: BreezeBuddyTypes.BreezeBuddyConnectResponse -> ConnectResp | ||
| fromBreezeBuddyConnectResponse resp = | ||
| ConnectResp | ||
| { roomUrl = resp.room_url, | ||
| token = resp.token, | ||
| sessionId = resp.session_id, | ||
| leadId = resp.lead_id | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| module Kernel.External.VoiceSdk.Interface.Types where | ||
|
|
||
| import qualified Data.Aeson as A | ||
| import qualified Kernel.External.VoiceSdk.BreezeBuddy.Types as BreezeBuddy | ||
| import Kernel.Prelude | ||
|
|
||
| data CreateLeadReq = CreateLeadReq | ||
| { requestId :: Text, | ||
| template :: Text, | ||
| payload :: A.Value, | ||
| resellerId :: Text, | ||
| merchantId :: Maybe Text, | ||
| reportingWebhookUrl :: Maybe Text, | ||
| executionMode :: Maybe Text, | ||
| isPlayground :: Maybe Bool, | ||
| configurationsOverride :: Maybe A.Value | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | ||
|
|
||
| data CreateLeadResp = CreateLeadResp | ||
| { status :: Text, | ||
| -- | Canonical lead identifier; feed this into 'ConnectReq.leadId'. | ||
| leadCallTrackerId :: Text, | ||
| orderId :: Text, | ||
| message :: Text | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | ||
|
|
||
| newtype ConnectReq = ConnectReq | ||
| { leadId :: Text | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | ||
|
|
||
| data ConnectResp = ConnectResp | ||
| { roomUrl :: Text, | ||
| token :: Text, | ||
| sessionId :: Text, | ||
| leadId :: Text | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | ||
|
|
||
| data VoiceSdkConfig | ||
| = BreezeBuddyVoiceSdkConfig BreezeBuddy.BreezeBuddySdkConfig | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| {-# LANGUAGE TemplateHaskell #-} | ||
|
|
||
| module Kernel.External.VoiceSdk.Types where | ||
|
|
||
| import Data.Aeson.Types | ||
| import EulerHS.Prelude | ||
| import Kernel.Beam.Lib.UtilsTH (mkBeamInstancesForEnumAndList) | ||
| import Kernel.Storage.Esqueleto (derivePersistField) | ||
|
|
||
| data VoiceSdkService = BreezeBuddy | ||
| deriving (Show, Read, Eq, Ord, Generic) | ||
|
|
||
| $(mkBeamInstancesForEnumAndList ''VoiceSdkService) | ||
| derivePersistField "VoiceSdkService" | ||
|
|
||
| instance FromJSON VoiceSdkService where | ||
| parseJSON (String "BreezeBuddy") = pure BreezeBuddy | ||
| parseJSON (String _) = parseFail "Expected \"BreezeBuddy\"" | ||
| parseJSON e = typeMismatch "String" e | ||
|
|
||
| instance ToJSON VoiceSdkService where | ||
| toJSON = String . show |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Don’t surface raw
ClientErrordetails viaInternalError.callAPIalready redacts the underlyingshow errbefore logging, but Line 54 puts the raw error text back intoInternalError. SinceInternalErroris exposed as the API error message, this can leak third-party response bodies or request details to callers.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents