feat(ai): add missing intents, admin guards, and validation checks
- Add missing AI intents: generate_cover_letter, improve_resume, request_view_contact, auto_apply_job, unknown - Add is_internal_admin helper to prevent admin/super_admin users from using user-facing AI flows - Add admin guards to: ai_generate_job_field, ai_generate_cover_letter, ai_tailor_resume, ai_auto_apply, ai_auto_respond_to_lead - Add professional approval check in ai_auto_respond_to_lead - must be APPROVED status - Add tracecoin balance check before contact reveal (requires 30 tracecoins) - Add KB escalation: when no articles found, suggest creating support ticket - Add explicit unknown intent handler with helpful message
This commit is contained in:
parent
2aba45c9fa
commit
f75a348fc7
1 changed files with 86 additions and 3 deletions
|
|
@ -95,7 +95,9 @@ async fn call_ollama(_state: &AppState, model: &str, prompt: &str) -> Result<Str
|
|||
|
||||
async fn classify_intent(message: &str, ollama_base: &str, model: &str) -> (String, f32) {
|
||||
let prompt = format!(
|
||||
"Classify this user message into one intent category. Categories: ticket_creation, form_filling, help_search, job_description_generation, general. \
|
||||
"Classify this user message into one intent category. Categories: \
|
||||
ticket_creation, form_filling, help_search, job_description_generation, \
|
||||
generate_cover_letter, improve_resume, request_view_contact, auto_apply_job, unknown, general. \
|
||||
Return ONLY the intent name, nothing else.\n\nMessage: {}",
|
||||
message
|
||||
);
|
||||
|
|
@ -109,14 +111,27 @@ async fn classify_intent(message: &str, ollama_base: &str, model: &str) -> (Stri
|
|||
"form_filling" => "form_filling",
|
||||
"help_search" => "help_search",
|
||||
"job_description_generation" => "job_description_generation",
|
||||
"generate_cover_letter" => "generate_cover_letter",
|
||||
"improve_resume" => "improve_resume",
|
||||
"request_view_contact" => "request_view_contact",
|
||||
"auto_apply_job" => "auto_apply_job",
|
||||
"unknown" => "unknown",
|
||||
_ => "general",
|
||||
};
|
||||
(intent.to_string(), confidence)
|
||||
}
|
||||
Err(_) => ("general".to_string(), 0.5),
|
||||
Err(_) => ("unknown".to_string(), 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_internal_admin(auth: &AuthUser) -> bool {
|
||||
let active = auth.claims.active_role.as_str();
|
||||
active == "ADMIN"
|
||||
|| active == "SUPER_ADMIN"
|
||||
|| auth.claims.roles.contains(&"ADMIN".to_string())
|
||||
|| auth.claims.roles.contains(&"SUPER_ADMIN".to_string())
|
||||
}
|
||||
|
||||
async fn call_ollama_inline(base_url: &str, model: &str, prompt: &str) -> Result<String, String> {
|
||||
let url = format!("{}/api/generate", base_url);
|
||||
let req = OllamaGenerateRequest {
|
||||
|
|
@ -199,7 +214,8 @@ async fn ai_chat_message(
|
|||
}
|
||||
_ => {
|
||||
"I couldn't find any help articles matching your question. \
|
||||
Try rephrasing or contact support if you need further assistance."
|
||||
If you need further assistance, I can help you create a support ticket instead. \
|
||||
Just describe your issue and I'll guide you through the ticket creation process."
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +263,17 @@ async fn ai_chat_message(
|
|||
}
|
||||
}
|
||||
}
|
||||
"unknown" => {
|
||||
"I'm not sure I understand your request. I can help you with:\n\n\
|
||||
- Creating support tickets\n\
|
||||
- Searching help articles\n\
|
||||
- Generating job descriptions\n\
|
||||
- Writing cover letters\n\
|
||||
- Improving your resume\n\
|
||||
- Applying to jobs\n\
|
||||
- Requesting to view lead contacts\n\n\
|
||||
Could you please rephrase your request?".to_string()
|
||||
}
|
||||
_ => {
|
||||
let system_prompt = "You are a helpful AI assistant for Nxtgauge platform. Provide clear, concise responses. \
|
||||
If the user needs support, guide them to create a ticket.";
|
||||
|
|
@ -570,6 +597,10 @@ async fn ai_generate_job_field(
|
|||
auth: AuthUser,
|
||||
Json(body): Json<GenerateJobFieldBody>,
|
||||
) -> impl IntoResponse {
|
||||
if is_internal_admin(&auth) {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI job description generation. Use the admin panel to manage jobs." }))).into_response();
|
||||
}
|
||||
|
||||
let company: Option<Uuid> = sqlx::query_scalar(
|
||||
"SELECT id FROM company_profiles WHERE user_id = $1"
|
||||
)
|
||||
|
|
@ -667,6 +698,10 @@ async fn ai_generate_cover_letter(
|
|||
auth: AuthUser,
|
||||
Json(body): Json<CoverLetterBody>,
|
||||
) -> impl IntoResponse {
|
||||
if is_internal_admin(&auth) {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI cover letter generation." }))).into_response();
|
||||
}
|
||||
|
||||
let seeker: Option<(Uuid, String, Option<String>, i32, Vec<String>)> = sqlx::query_as(
|
||||
"SELECT id, full_name, summary, experience_years, skills FROM job_seeker_profiles WHERE user_id = $1"
|
||||
)
|
||||
|
|
@ -776,6 +811,10 @@ async fn ai_tailor_resume(
|
|||
auth: AuthUser,
|
||||
Json(body): Json<TailorResumeBody>,
|
||||
) -> impl IntoResponse {
|
||||
if is_internal_admin(&auth) {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI resume tailoring." }))).into_response();
|
||||
}
|
||||
|
||||
let seeker: Option<(Uuid, String, Option<String>, i32, Vec<String>)> = sqlx::query_as(
|
||||
"SELECT id, full_name, summary, experience_years, skills FROM job_seeker_profiles WHERE user_id = $1"
|
||||
)
|
||||
|
|
@ -892,6 +931,10 @@ async fn ai_auto_apply(
|
|||
auth: AuthUser,
|
||||
Json(body): Json<AutoApplyBody>,
|
||||
) -> impl IntoResponse {
|
||||
if is_internal_admin(&auth) {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI auto-apply." }))).into_response();
|
||||
}
|
||||
|
||||
if body.job_ids.is_empty() || body.job_ids.len() > 10 {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Select 1-10 jobs at a time" }))).into_response();
|
||||
}
|
||||
|
|
@ -1057,6 +1100,10 @@ async fn ai_auto_respond_to_lead(
|
|||
auth: AuthUser,
|
||||
Json(body): Json<AutoRespondToLeadBody>,
|
||||
) -> impl IntoResponse {
|
||||
if is_internal_admin(&auth) {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI contact reveal. Use the admin panel to manage leads." }))).into_response();
|
||||
}
|
||||
|
||||
let leads_service_url = std::env::var("LEADS_SERVICE_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:9118".to_string());
|
||||
|
||||
|
|
@ -1074,6 +1121,42 @@ async fn ai_auto_respond_to_lead(
|
|||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Profile not found" }))).into_response();
|
||||
};
|
||||
|
||||
let approval_status: Option<String> = sqlx::query_scalar(
|
||||
"SELECT status FROM user_role_profiles WHERE id = $1"
|
||||
)
|
||||
.bind(profile_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let Some(status) = approval_status else {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Profile not found" }))).into_response();
|
||||
};
|
||||
|
||||
if status != "APPROVED" {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Your profile must be approved before you can request lead contact access. Please complete verification." }))).into_response();
|
||||
}
|
||||
|
||||
let wallet: Option<(Uuid, i64)> = sqlx::query_as(
|
||||
"SELECT id, balance FROM tracecoin_wallets WHERE user_id = $1"
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let (wallet_id, balance) = match wallet {
|
||||
Some((id, bal)) => (id, bal),
|
||||
None => return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Wallet not found. Please contact support." }))).into_response(),
|
||||
};
|
||||
|
||||
let tracecoins_cost = 30;
|
||||
if balance < tracecoins_cost as i64 {
|
||||
return (StatusCode::PAYMENT_REQUIRED, Json(serde_json::json!({ "error": format!("Insufficient balance. You need {} Tracecoins but have {}. Please top up your wallet.", tracecoins_cost, balance) }))).into_response();
|
||||
}
|
||||
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
let used: Option<i32> = sqlx::query_scalar(
|
||||
"SELECT generations_used FROM job_seeker_ai_usage WHERE job_seeker_id = $1 AND usage_date = $2"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue