feat: wire NxtgaugeTicketProvider and NxtgaugeHelpCenterProvider to real backend APIs
- Add NxtgaugeTicketProvider: calls /api/support/tickets/ai/create with service key auth - Add NxtgaugeHelpCenterProvider: calls /api/kb/articles for help search - Add ExternalService error variant for HTTP call failures - Add NXTGAUGE_USERS_URL config env var
This commit is contained in:
parent
dbba72478c
commit
72d44bbd63
9 changed files with 195 additions and 6 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
18
src/main.rs
18
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(),
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod help_center_provider;
|
||||
pub mod local_help_center_provider;
|
||||
pub mod nxtgauge_help_center_provider;
|
||||
|
|
|
|||
74
src/providers/help_center/nxtgauge_help_center_provider.rs
Normal file
74
src/providers/help_center/nxtgauge_help_center_provider.rs
Normal file
|
|
@ -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<Vec<HelpArticle>, 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<ArticleDto>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ArticleDto {
|
||||
id: String,
|
||||
title: String,
|
||||
summary: Option<String>,
|
||||
content: String,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod mock_ticket_provider;
|
||||
pub mod nxtgauge_ticket_provider;
|
||||
pub mod ticket_provider;
|
||||
|
|
|
|||
91
src/providers/tickets/nxtgauge_ticket_provider.rs
Normal file
91
src/providers/tickets/nxtgauge_ticket_provider.rs
Normal file
|
|
@ -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<String>,
|
||||
category: Option<String>,
|
||||
priority: Option<String>,
|
||||
#[serde(rename = "userId")]
|
||||
user_id: Option<String>,
|
||||
}
|
||||
|
||||
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<TicketCreateResult, AppError> {
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue