diff --git a/Cargo.lock b/Cargo.lock index 8b7c02b..4a1dc3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1189,6 +1189,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "urlencoding", "uuid", ] @@ -2495,6 +2496,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 78de12a..8aab50c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ uuid = { version = "1", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid", "migrate", "macros"] } url = "2" +urlencoding = "2" [dev-dependencies] http-body-util = "0.1" diff --git a/src/config/mod.rs b/src/config/mod.rs index e3565d9..1af1249 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,6 +10,7 @@ pub struct AppConfig { pub ollama_embed_model: String, pub help_center_seed_path: String, pub tickets_source: String, + pub nxtgauge_users_url: String, } impl AppConfig { @@ -28,6 +29,10 @@ impl AppConfig { "./seeds/help_articles.json", ), tickets_source: env_or_default("TICKETS_SOURCE", "chatbot"), + nxtgauge_users_url: env_or_default( + "NXTGAUGE_USERS_URL", + "http://nxtgauge-rust-users:9101", + ), } } diff --git a/src/error.rs b/src/error.rs index 312f109..f9c9ceb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,8 @@ pub enum AppError { ProviderUnavailable(String), #[error("internal error: {0}")] Internal(String), + #[error("external service error: {0}")] + ExternalService(String), } #[derive(Debug, Serialize)] @@ -26,6 +28,7 @@ impl IntoResponse for AppError { AppError::BadRequest(_) => StatusCode::BAD_REQUEST, AppError::ProviderUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE, AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + AppError::ExternalService(_) => StatusCode::BAD_GATEWAY, }; let body = Json(ErrorBody { diff --git a/src/main.rs b/src/main.rs index 1202a18..b0e9057 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,9 +14,9 @@ mod tickets; use std::sync::Arc; use config::AppConfig; -use providers::help_center::local_help_center_provider::LocalHelpCenterProvider; +use providers::help_center::nxtgauge_help_center_provider::NxtgaugeHelpCenterProvider; use providers::llm::ollama_provider::OllamaAiProvider; -use providers::tickets::mock_ticket_provider::MockTicketProvider; +use providers::tickets::nxtgauge_ticket_provider::NxtgaugeTicketProvider; use retrieval::embeddings::ollama_embedding_provider::OllamaEmbeddingProvider; use state::AppState; use tracing::{info, warn}; @@ -45,12 +45,18 @@ async fn main() { cfg.ollama_embed_model.clone(), )); - let help_center_provider = Arc::new(LocalHelpCenterProvider::from_seed_file( - &cfg.help_center_seed_path, - embedding_provider, + let http_client = reqwest::Client::new(); + + let help_center_provider = Arc::new(NxtgaugeHelpCenterProvider::new( + http_client.clone(), + cfg.nxtgauge_users_url.clone(), )); - let ticket_provider = Arc::new(MockTicketProvider::new(cfg.tickets_source.clone())); + let ticket_provider = Arc::new(NxtgaugeTicketProvider::new( + http_client, + cfg.nxtgauge_users_url.clone(), + cfg.tickets_source.clone(), + )); let state = AppState::new( cfg.clone(), diff --git a/src/providers/help_center/mod.rs b/src/providers/help_center/mod.rs index 12a588d..2d667b9 100644 --- a/src/providers/help_center/mod.rs +++ b/src/providers/help_center/mod.rs @@ -1,2 +1,3 @@ pub mod help_center_provider; pub mod local_help_center_provider; +pub mod nxtgauge_help_center_provider; diff --git a/src/providers/help_center/nxtgauge_help_center_provider.rs b/src/providers/help_center/nxtgauge_help_center_provider.rs new file mode 100644 index 0000000..7f7bca8 --- /dev/null +++ b/src/providers/help_center/nxtgauge_help_center_provider.rs @@ -0,0 +1,74 @@ +use async_trait::async_trait; +use reqwest::Client; + +use crate::{ + error::AppError, + providers::help_center::help_center_provider::{HelpArticle, HelpCenterProvider}, +}; + +#[derive(Clone)] +pub struct NxtgaugeHelpCenterProvider { + client: Client, + base_url: String, +} + +impl NxtgaugeHelpCenterProvider { + pub fn new(client: Client, base_url: String) -> Self { + Self { client, base_url } + } +} + +#[async_trait] +impl HelpCenterProvider for NxtgaugeHelpCenterProvider { + async fn search(&self, query: &str) -> Result, AppError> { + let url = format!("{}/api/kb/articles?q={}", self.base_url, urlencoding::encode(query)); + + let response = self + .client + .get(&url) + .send() + .await + .map_err(|e| AppError::ExternalService(format!("failed to call nxtgauge help center: {}", e)))?; + + let status = response.status(); + if !status.is_success() { + let body = response.text().await.unwrap_or_default(); + return Err(AppError::ExternalService(format!( + "help center API returned {}: {}", + status, + body + ))); + } + + #[derive(serde::Deserialize)] + struct KbSearchResponse { + articles: Vec, + } + + #[derive(serde::Deserialize)] + struct ArticleDto { + id: String, + title: String, + summary: Option, + content: String, + tags: Vec, + } + + let search_result: KbSearchResponse = response + .json() + .await + .map_err(|e| AppError::ExternalService(format!("failed to parse KB response: {}", e)))?; + + Ok(search_result + .articles + .into_iter() + .map(|a| HelpArticle { + id: a.id, + title: a.title, + summary: a.summary.unwrap_or_default(), + content: a.content, + tags: a.tags, + }) + .collect()) + } +} \ No newline at end of file diff --git a/src/providers/tickets/mod.rs b/src/providers/tickets/mod.rs index 7ea7935..0b84a08 100644 --- a/src/providers/tickets/mod.rs +++ b/src/providers/tickets/mod.rs @@ -1,2 +1,3 @@ pub mod mock_ticket_provider; +pub mod nxtgauge_ticket_provider; pub mod ticket_provider; diff --git a/src/providers/tickets/nxtgauge_ticket_provider.rs b/src/providers/tickets/nxtgauge_ticket_provider.rs new file mode 100644 index 0000000..0b1b94a --- /dev/null +++ b/src/providers/tickets/nxtgauge_ticket_provider.rs @@ -0,0 +1,91 @@ +use async_trait::async_trait; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +use crate::{ + error::AppError, + providers::tickets::ticket_provider::{TicketCreatePayload, TicketCreateResult, TicketProvider}, +}; + +#[derive(Clone)] +pub struct NxtgaugeTicketProvider { + client: Client, + base_url: String, + source: String, +} + +#[derive(Debug, Deserialize)] +struct NxtgaugeTicketResponse { + id: String, + status: String, +} + +#[derive(Serialize)] +struct NxtgaugeTicketRequest { + subject: String, + description: Option, + category: Option, + priority: Option, + #[serde(rename = "userId")] + user_id: Option, +} + +impl NxtgaugeTicketProvider { + pub fn new(client: Client, base_url: String, source: String) -> Self { + Self { + client, + base_url, + source, + } + } +} + +#[async_trait] +impl TicketProvider for NxtgaugeTicketProvider { + async fn create_ticket( + &self, + payload: TicketCreatePayload, + ) -> Result { + let url = format!("{}/api/support/tickets/ai/create", self.base_url); + + let req = NxtgaugeTicketRequest { + subject: payload.subject, + description: Some(payload.description), + category: Some(payload.category), + priority: Some(payload.priority), + user_id: Some(payload.user_id), + }; + + let ai_service_key = std::env::var("AI_SERVICE_KEY") + .unwrap_or_else(|_| "nxtgauge-ai-assistant".to_string()); + + let response = self + .client + .post(&url) + .header("X-AI-Service-Key", ai_service_key) + .json(&req) + .send() + .await + .map_err(|e| AppError::ExternalService(format!("failed to call nxtgauge ticket API: {}", e)))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(AppError::ExternalService(format!( + "nxtgauge ticket API returned {}: {}", + status, body + ))); + } + + let result: NxtgaugeTicketResponse = response + .json() + .await + .map_err(|e| AppError::ExternalService(format!("failed to parse ticket response: {}", e)))?; + + Ok(TicketCreateResult { + ticket_id: result.id, + status: result.status, + provider: format!("nxtgauge:{}", self.source), + }) + } +} \ No newline at end of file