From 3d60e6c4c807f65fed93a341ef1d8063f818b31d Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 17 Jul 2025 01:30:13 +0530 Subject: [PATCH] feat(ai): add endpoints to chat with ai service (#8585) Co-authored-by: Riddhiagrawal001 Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 6 +- config/deployments/env_specific.toml | 6 +- config/development.toml | 4 ++ config/docker_compose.toml | 4 ++ crates/api_models/src/chat.rs | 18 ++++++ crates/api_models/src/events.rs | 1 + crates/api_models/src/events/chat.rs | 5 ++ crates/api_models/src/lib.rs | 1 + crates/common_utils/src/consts.rs | 3 + crates/common_utils/src/events.rs | 1 + crates/hyperswitch_domain_models/src/chat.rs | 15 +++++ crates/hyperswitch_domain_models/src/lib.rs | 1 + .../src/configs/secrets_transformers.rs | 1 + crates/router/src/configs/settings.rs | 9 +++ crates/router/src/configs/validations.rs | 12 ++++ crates/router/src/core.rs | 1 + crates/router/src/core/chat.rs | 57 +++++++++++++++++++ crates/router/src/core/errors.rs | 1 + crates/router/src/core/errors/chat.rs | 37 ++++++++++++ crates/router/src/lib.rs | 3 +- crates/router/src/routes.rs | 4 +- crates/router/src/routes/app.rs | 21 ++++++- crates/router/src/routes/chat.rs | 38 +++++++++++++ crates/router/src/routes/lock_utils.rs | 3 + crates/router_env/src/logger/types.rs | 2 + loadtest/config/development.toml | 4 ++ 26 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 crates/api_models/src/chat.rs create mode 100644 crates/api_models/src/events/chat.rs create mode 100644 crates/hyperswitch_domain_models/src/chat.rs create mode 100644 crates/router/src/core/chat.rs create mode 100644 crates/router/src/core/errors/chat.rs create mode 100644 crates/router/src/routes/chat.rs diff --git a/config/config.example.toml b/config/config.example.toml index 1b08a6b906..5d4a508c73 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1133,4 +1133,8 @@ version = "HOSTNAME" # value of HOSTNAME from deployment which tells its [platform] enabled = true # Enable or disable platform features -allow_connected_merchants = false # Enable or disable connected merchant account features \ No newline at end of file +allow_connected_merchants = false # Enable or disable connected merchant account features + +[chat] +enabled = false # Enable or disable chat features +hyperswitch_ai_host = "http://0.0.0.0:8000" # Hyperswitch ai workflow host \ No newline at end of file diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index cab8777aa9..4428b50801 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -380,4 +380,8 @@ connector_names = "connector_names" # Comma-separated list of allowed connec [grpc_client.unified_connector_service] host = "localhost" # Unified Connector Service Client Host port = 8000 # Unified Connector Service Client Port -connection_timeout = 10 # Connection Timeout Duration in Seconds \ No newline at end of file +connection_timeout = 10 # Connection Timeout Duration in Seconds + +[chat] +enabled = false # Enable or disable chat features +hyperswitch_ai_host = "http://0.0.0.0:8000" # Hyperswitch ai workflow host diff --git a/config/development.toml b/config/development.toml index 613de3a05e..1ccbd25063 100644 --- a/config/development.toml +++ b/config/development.toml @@ -1226,3 +1226,7 @@ connector_names = "stripe, adyen" # Comma-separated list of allowe [infra_values] cluster = "CLUSTER" version = "HOSTNAME" + +[chat] +enabled = false +hyperswitch_ai_host = "http://0.0.0.0:8000" \ No newline at end of file diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 124b06208f..d811cb111a 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -1109,6 +1109,10 @@ background_color = "#FFFFFF" enabled = true allow_connected_merchants = true +[chat] +enabled = false +hyperswitch_ai_host = "http://0.0.0.0:8000" + [authentication_providers] click_to_pay = {connector_list = "adyen, cybersource"} diff --git a/crates/api_models/src/chat.rs b/crates/api_models/src/chat.rs new file mode 100644 index 0000000000..c66b42cc4f --- /dev/null +++ b/crates/api_models/src/chat.rs @@ -0,0 +1,18 @@ +use common_utils::id_type; +use masking::Secret; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ChatRequest { + pub message: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ChatResponse { + pub response: Secret, + pub merchant_id: id_type::MerchantId, + pub status: String, + #[serde(skip_serializing)] + pub query_executed: Option>, + #[serde(skip_serializing)] + pub row_count: Option, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 768aed75a7..dd54cd6322 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,4 +1,5 @@ pub mod apple_pay_certificates_migration; +pub mod chat; pub mod connector_onboarding; pub mod customer; pub mod dispute; diff --git a/crates/api_models/src/events/chat.rs b/crates/api_models/src/events/chat.rs new file mode 100644 index 0000000000..42fefe487e --- /dev/null +++ b/crates/api_models/src/events/chat.rs @@ -0,0 +1,5 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::chat::{ChatRequest, ChatResponse}; + +common_utils::impl_api_event_type!(Chat, (ChatRequest, ChatResponse)); diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index cbc50ed7ff..3b343459e9 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod apple_pay_certificates_migration; pub mod authentication; pub mod blocklist; pub mod cards_info; +pub mod chat; pub mod conditional_configs; pub mod connector_enums; pub mod connector_onboarding; diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index c8cf1e8c57..3ef0ef555e 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -191,3 +191,6 @@ pub const METRICS_HOST_TAG_NAME: &str = "host"; /// API client request timeout (in seconds) pub const REQUEST_TIME_OUT: u64 = 30; + +/// API client request timeout for ai service (in seconds) +pub const REQUEST_TIME_OUT_FOR_AI_SERVICE: u64 = 120; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index e196429675..49be22fc93 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -137,6 +137,7 @@ pub enum ApiEventsType { profile_acquirer_id: id_type::ProfileAcquirerId, }, ThreeDsDecisionRule, + Chat, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/hyperswitch_domain_models/src/chat.rs b/crates/hyperswitch_domain_models/src/chat.rs new file mode 100644 index 0000000000..31b9f806db --- /dev/null +++ b/crates/hyperswitch_domain_models/src/chat.rs @@ -0,0 +1,15 @@ +use common_utils::id_type; +use masking::Secret; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct GetDataMessage { + pub message: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct HyperswitchAiDataRequest { + pub merchant_id: id_type::MerchantId, + pub profile_id: id_type::ProfileId, + pub org_id: id_type::OrganizationId, + pub query: GetDataMessage, +} diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index fd24c3d9f4..efa6505491 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -6,6 +6,7 @@ pub mod business_profile; pub mod callback_mapper; pub mod card_testing_guard_data; pub mod cards_info; +pub mod chat; pub mod connector_endpoints; pub mod consts; pub mod customer; diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 9129a9a78f..d4eb34838d 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -452,6 +452,7 @@ pub(crate) async fn fetch_raw_secrets( Settings { server: conf.server, + chat: conf.chat, master_database, redis: conf.redis, log: conf.log, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 6157a4fab2..ae4c8a3cda 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -70,6 +70,7 @@ pub struct Settings { pub server: Server, pub proxy: Proxy, pub env: Env, + pub chat: ChatSettings, pub master_database: SecretStateContainer, #[cfg(feature = "olap")] pub replica_database: SecretStateContainer, @@ -195,6 +196,13 @@ pub struct Platform { pub allow_connected_merchants: bool, } +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct ChatSettings { + pub enabled: bool, + pub hyperswitch_ai_host: String, +} + #[derive(Debug, Clone, Default, Deserialize)] pub struct Multitenancy { pub tenants: TenantConfig, @@ -1016,6 +1024,7 @@ impl Settings { self.secrets.get_inner().validate()?; self.locker.validate()?; self.connectors.validate("connectors")?; + self.chat.validate()?; self.cors.validate()?; diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 4fc254760a..023b9ee5be 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -343,3 +343,15 @@ impl super::settings::OpenRouter { ) } } + +impl super::settings::ChatSettings { + pub fn validate(&self) -> Result<(), ApplicationError> { + use common_utils::fp_utils::when; + + when(self.enabled && self.hyperswitch_ai_host.is_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "hyperswitch ai host must be set if chat is enabled".into(), + )) + }) + } +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 2334eddfd6..0fe3e57f87 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -72,4 +72,5 @@ pub mod relay; #[cfg(feature = "v2")] pub mod revenue_recovery; +pub mod chat; pub mod tokenization; diff --git a/crates/router/src/core/chat.rs b/crates/router/src/core/chat.rs new file mode 100644 index 0000000000..c30e27f048 --- /dev/null +++ b/crates/router/src/core/chat.rs @@ -0,0 +1,57 @@ +use api_models::chat as chat_api; +use common_utils::{ + consts, + errors::CustomResult, + request::{Method, RequestBuilder, RequestContent}, +}; +use error_stack::ResultExt; +use external_services::http_client; +use hyperswitch_domain_models::chat as chat_domain; +use router_env::{instrument, logger, tracing}; + +use crate::{ + db::errors::chat::ChatErrors, + routes::SessionState, + services::{authentication as auth, ApplicationResponse}, +}; + +#[instrument(skip_all)] +pub async fn get_data_from_hyperswitch_ai_workflow( + state: SessionState, + user_from_token: auth::UserFromToken, + req: chat_api::ChatRequest, +) -> CustomResult, ChatErrors> { + let url = format!("{}/webhook", state.conf.chat.hyperswitch_ai_host); + + let request_body = chat_domain::HyperswitchAiDataRequest { + query: chat_domain::GetDataMessage { + message: req.message, + }, + org_id: user_from_token.org_id, + merchant_id: user_from_token.merchant_id, + profile_id: user_from_token.profile_id, + }; + logger::info!("Request for AI service: {:?}", request_body); + + let request = RequestBuilder::new() + .method(Method::Post) + .url(&url) + .attach_default_headers() + .set_body(RequestContent::Json(Box::new(request_body.clone()))) + .build(); + + let response = http_client::send_request( + &state.conf.proxy, + request, + Some(consts::REQUEST_TIME_OUT_FOR_AI_SERVICE), + ) + .await + .change_context(ChatErrors::InternalServerError) + .attach_printable("Error when sending request to AI service")? + .json::<_>() + .await + .change_context(ChatErrors::InternalServerError) + .attach_printable("Error when deserializing response from AI service")?; + + Ok(ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 93d4414a5b..c8e7cb4e6f 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -1,3 +1,4 @@ +pub mod chat; pub mod customers_error_response; pub mod error_handlers; pub mod transformers; diff --git a/crates/router/src/core/errors/chat.rs b/crates/router/src/core/errors/chat.rs new file mode 100644 index 0000000000..a96afa67de --- /dev/null +++ b/crates/router/src/core/errors/chat.rs @@ -0,0 +1,37 @@ +#[derive(Debug, thiserror::Error)] +pub enum ChatErrors { + #[error("User InternalServerError")] + InternalServerError, + #[error("Missing Config error")] + MissingConfigError, + #[error("Chat response deserialization failed")] + ChatResponseDeserializationFailed, +} + +impl common_utils::errors::ErrorSwitch for ChatErrors { + fn switch(&self) -> api_models::errors::types::ApiErrorResponse { + use api_models::errors::types::{ApiError, ApiErrorResponse as AER}; + let sub_code = "AI"; + match self { + Self::InternalServerError => { + AER::InternalServerError(ApiError::new("HE", 0, self.get_error_message(), None)) + } + Self::MissingConfigError => { + AER::InternalServerError(ApiError::new(sub_code, 1, self.get_error_message(), None)) + } + Self::ChatResponseDeserializationFailed => { + AER::BadRequest(ApiError::new(sub_code, 2, self.get_error_message(), None)) + } + } + } +} + +impl ChatErrors { + pub fn get_error_message(&self) -> String { + match self { + Self::InternalServerError => "Something went wrong".to_string(), + Self::MissingConfigError => "Missing webhook url".to_string(), + Self::ChatResponseDeserializationFailed => "Failed to parse chat response".to_string(), + } + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 6bcb17ad1a..e002f7fd05 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -189,7 +189,8 @@ pub fn mk_app( .service(routes::MerchantAccount::server(state.clone())) .service(routes::User::server(state.clone())) .service(routes::ApiKeys::server(state.clone())) - .service(routes::Routing::server(state.clone())); + .service(routes::Routing::server(state.clone())) + .service(routes::Chat::server(state.clone())); #[cfg(feature = "v1")] { diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index f639ee06ab..fcf7270fa8 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -75,6 +75,8 @@ pub mod process_tracker; #[cfg(feature = "v2")] pub mod proxy; +pub mod chat; + #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(feature = "v2")] @@ -86,7 +88,7 @@ pub use self::app::Recon; #[cfg(feature = "v2")] pub use self::app::Tokenization; pub use self::app::{ - ApiKeys, AppState, ApplePayCertificatesMigration, Authentication, Cache, Cards, Configs, + ApiKeys, AppState, ApplePayCertificatesMigration, Authentication, Cache, Cards, Chat, Configs, ConnectorOnboarding, Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Hypersense, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, ProcessTracker, Profile, ProfileAcquirer, ProfileNew, Refunds, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 9935ab5fd9..3cbf532b95 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -59,8 +59,8 @@ use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_ve #[cfg(feature = "oltp")] use super::webhooks::*; use super::{ - admin, api_keys, cache::*, connector_onboarding, disputes, files, gsm, health::*, profiles, - relay, user, user_role, + admin, api_keys, cache::*, chat, connector_onboarding, disputes, files, gsm, health::*, + profiles, relay, user, user_role, }; #[cfg(feature = "v1")] use super::{apple_pay_certificates_migration, blocklist, payment_link, webhook_events}; @@ -2215,6 +2215,23 @@ impl Gsm { } } +pub struct Chat; + +#[cfg(feature = "olap")] +impl Chat { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/chat").app_data(web::Data::new(state.clone())); + if state.conf.chat.enabled { + route = route.service( + web::scope("/ai").service( + web::resource("/data") + .route(web::post().to(chat::get_data_from_hyperswitch_ai_workflow)), + ), + ); + } + route + } +} pub struct ThreeDsDecisionRule; #[cfg(feature = "oltp")] diff --git a/crates/router/src/routes/chat.rs b/crates/router/src/routes/chat.rs new file mode 100644 index 0000000000..555bf007d2 --- /dev/null +++ b/crates/router/src/routes/chat.rs @@ -0,0 +1,38 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +#[cfg(feature = "olap")] +use api_models::chat as chat_api; +use router_env::{instrument, tracing, Flow}; + +use super::AppState; +use crate::{ + core::{api_locking, chat as chat_core}, + services::{ + api, + authentication::{self as auth}, + authorization::permissions::Permission, + }, +}; + +#[instrument(skip_all)] +pub async fn get_data_from_hyperswitch_ai_workflow( + state: web::Data, + http_req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::GetDataFromHyperswitchAiFlow; + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + payload.into_inner(), + |state, user: auth::UserFromToken, payload, _| { + chat_core::get_data_from_hyperswitch_ai_workflow(state, user, payload) + }, + // At present, the AI service retrieves data scoped to the merchant level + &auth::JWTAuth { + permission: Permission::MerchantPaymentRead, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index fa616a3da3..365d54fc84 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -35,6 +35,7 @@ pub enum ApiIdentifier { UserRole, ConnectorOnboarding, Recon, + AiWorkflow, Poll, ApplePayCertificatesMigration, Relay, @@ -300,6 +301,8 @@ impl From for ApiIdentifier { | Flow::DeleteTheme | Flow::CloneConnector => Self::User, + Flow::GetDataFromHyperswitchAiFlow => Self::AiWorkflow, + Flow::ListRolesV2 | Flow::ListInvitableRolesAtEntityLevel | Flow::ListUpdatableRolesAtEntityLevel diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index d395cd1f6d..0eafefd112 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -349,6 +349,8 @@ pub enum Flow { ApplePayCertificatesMigration, /// Gsm Rule Delete flow GsmRuleDelete, + /// Get data from embedded flow + GetDataFromHyperswitchAiFlow, /// User Sign Up UserSignUp, /// User Sign Up diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 58196d18cf..ebf7d6462e 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -724,3 +724,7 @@ redsys = { payment_method = "card" } billing_connectors_which_require_payment_sync = "stripebilling, recurly" [billing_connectors_invoice_sync] billing_connectors_which_requires_invoice_sync_call = "recurly" + +[chat] +enabled = false +hyperswitch_ai_host = "http://0.0.0.0:8000" \ No newline at end of file