-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat(ai): add endpoints to chat with ai service #8585
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
Changes from all commits
699e64f
86b3590
e36239e
d1e93db
cbcbec2
d51606e
73abca3
f06d12f
dee0cf2
a86dd3e
1903e14
99ab29e
2b2a407
26fbaba
46aca04
4841595
09e9c12
f92fe4d
633acbb
2b97571
eaad65a
cbcd834
97e9d60
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,18 @@ | ||
use common_utils::id_type; | ||
use masking::Secret; | ||
|
||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] | ||
pub struct ChatRequest { | ||
pub message: Secret<String>, | ||
} | ||
|
||
#[derive(Debug, serde::Deserialize, serde::Serialize)] | ||
pub struct ChatResponse { | ||
pub response: Secret<serde_json::Value>, | ||
pub merchant_id: id_type::MerchantId, | ||
pub status: String, | ||
#[serde(skip_serializing)] | ||
pub query_executed: Option<Secret<String>>, | ||
#[serde(skip_serializing)] | ||
pub row_count: Option<i32>, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
use common_utils::events::{ApiEventMetric, ApiEventsType}; | ||
|
||
use crate::chat::{ChatRequest, ChatResponse}; | ||
|
||
common_utils::impl_api_event_type!(Chat, (ChatRequest, ChatResponse)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String>, | ||
} | ||
|
||
#[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, | ||
Comment on lines
+11
to
+14
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. Would there be a situation where tenant ID might come into picture when calling the AI service? 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. For now we are only considering merchant_id and giving result for merchant level data, if tenant id comes into picture we may have to make changes in the ai service as well to talk to the corresponding schema. Not sure, will depend on the tenant, how they want this service. This all can be as per the use case. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,4 +72,5 @@ pub mod relay; | |
#[cfg(feature = "v2")] | ||
pub mod revenue_recovery; | ||
|
||
pub mod chat; | ||
pub mod tokenization; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
apoorvdixit88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
state: SessionState, | ||
user_from_token: auth::UserFromToken, | ||
req: chat_api::ChatRequest, | ||
) -> CustomResult<ApplicationResponse<chat_api::ChatResponse>, 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)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
pub mod chat; | ||
pub mod customers_error_response; | ||
pub mod error_handlers; | ||
pub mod transformers; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<api_models::errors::types::ApiErrorResponse> 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(), | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
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. Nit: Should we consider having |
||
state: web::Data<AppState>, | ||
http_req: HttpRequest, | ||
payload: web::Json<chat_api::ChatRequest>, | ||
) -> 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, | ||
}, | ||
Comment on lines
+32
to
+34
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. Any specific reason for choosing this permission? If yes, should we consider explaining it in code comments? |
||
api_locking::LockAction::NotApplicable, | ||
)) | ||
.await | ||
} |
Uh oh!
There was an error while loading. Please reload this page.