- Add LiteLLM provider implementing AiProvider trait - Support both Ollama and LiteLLM via LLM_PROVIDER env var - Add cover letter generation endpoint - Improve chat orchestrator with better intent detection - Add search_kb, explain_plan_limits, check_ai_pack_balance intents - Add LiteLLM config env vars
282 lines
11 KiB
Rust
282 lines
11 KiB
Rust
use std::sync::Arc;
|
|
|
|
use crate::{
|
|
chat::models::{ChatMessageRequest, ChatMessageResponse},
|
|
error::AppError,
|
|
forms::{models::FormExtractRequest, service::FormService},
|
|
jobs::{models::GenerateJobDescriptionRequest, service::JobsService},
|
|
providers::{
|
|
help_center::help_center_provider::HelpCenterProvider, llm::ai_provider::AiProvider,
|
|
},
|
|
tickets::{models::CreateTicketRequest, service::TicketService},
|
|
};
|
|
|
|
#[derive(Clone)]
|
|
pub struct ChatOrchestrator {
|
|
jobs_service: JobsService,
|
|
form_service: FormService,
|
|
help_center: Arc<dyn HelpCenterProvider>,
|
|
ticket_service: TicketService,
|
|
ai_provider: Arc<dyn AiProvider>,
|
|
}
|
|
|
|
impl ChatOrchestrator {
|
|
pub fn new(
|
|
jobs_service: JobsService,
|
|
form_service: FormService,
|
|
help_center: Arc<dyn HelpCenterProvider>,
|
|
ticket_service: TicketService,
|
|
ai_provider: Arc<dyn AiProvider>,
|
|
) -> Self {
|
|
Self {
|
|
jobs_service,
|
|
form_service,
|
|
help_center,
|
|
ticket_service,
|
|
ai_provider,
|
|
}
|
|
}
|
|
|
|
pub async fn handle_chat(
|
|
&self,
|
|
request: ChatMessageRequest,
|
|
) -> Result<ChatMessageResponse, AppError> {
|
|
let intent = classify_intent(&request.message);
|
|
let conversation_id = request.conversation_id.clone();
|
|
|
|
match intent.as_str() {
|
|
"job_description_generation" => {
|
|
let jd = self
|
|
.jobs_service
|
|
.generate_description(GenerateJobDescriptionRequest {
|
|
role_title: request.message.clone(),
|
|
seniority: None,
|
|
department: None,
|
|
employment_type: None,
|
|
required_skills: vec!["communication".to_string()],
|
|
optional_skills: None,
|
|
responsibilities: None,
|
|
company_context: None,
|
|
})
|
|
.await?;
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply: "Generated a draft job description.".to_string(),
|
|
data: serde_json::to_value(jd).unwrap_or(serde_json::Value::Null),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
"form_filling_assistance" => {
|
|
let extracted = self
|
|
.form_service
|
|
.extract(FormExtractRequest {
|
|
raw_user_input: request.message.clone(),
|
|
expected_fields: None,
|
|
})
|
|
.await?;
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply: extracted.suggested_next_step.clone(),
|
|
data: serde_json::to_value(extracted).unwrap_or(serde_json::Value::Null),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
"search_kb" => {
|
|
let matches = self.help_center.search(&request.message).await?;
|
|
let reply = if matches.is_empty() {
|
|
"No help article match found. Try rephrasing your question.".to_string()
|
|
} else {
|
|
let mut response = format!("Found {} help article(s):\n\n", matches.len());
|
|
for (i, article) in matches.iter().take(5).enumerate() {
|
|
response.push_str(&format!("{}. **{}**\n{}\n\n", i + 1, article.title, article.summary));
|
|
}
|
|
if matches.len() > 5 {
|
|
response.push_str(&format!("...and {} more articles.", matches.len() - 5));
|
|
}
|
|
response
|
|
};
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply,
|
|
data: serde_json::json!({ "matches": matches }),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
"support_ticket_creation" => {
|
|
let created = self
|
|
.ticket_service
|
|
.create(CreateTicketRequest {
|
|
subject: request.message.chars().take(80).collect::<String>(),
|
|
description: request.message.clone(),
|
|
priority: "medium".to_string(),
|
|
category: "general".to_string(),
|
|
user_id: request.user_id.unwrap_or_else(|| "anonymous".to_string()),
|
|
conversation_id: conversation_id.clone(),
|
|
source: Some("chatbot".to_string()),
|
|
tags: Some(vec!["chat".to_string()]),
|
|
metadata: None,
|
|
})
|
|
.await?;
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply: format!("Support ticket created: {}. Our team will respond shortly.", created.ticket_id),
|
|
data: serde_json::to_value(created).unwrap_or(serde_json::Value::Null),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
"explain_plan_limits" => {
|
|
let reply = "Your AI plan determines how many AI actions you can use per month.\n\n\
|
|
- **Free AI**: 10 actions/month\n\
|
|
- **Starter AI**: 100 actions/month\n\
|
|
- **Growth AI**: 500 actions/month\n\
|
|
- **Pro AI**: 2000 actions/month\n\n\
|
|
You can purchase add-on packs for more usage. Would you like to upgrade your plan?";
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply: reply.to_string(),
|
|
data: serde_json::json!({}),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
"check_ai_pack_balance" => {
|
|
let reply = "To check your AI balance, please visit the AI Usage section in your dashboard.\n\n\
|
|
You can view:\n\
|
|
- Monthly usage vs limit\n\
|
|
- Add-on balance remaining\n\
|
|
- Renewal date\n\n\
|
|
Would you like me to help with anything else?";
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply: reply.to_string(),
|
|
data: serde_json::json!({}),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
"generate_cover_letter" => {
|
|
let reply = "I can help you generate a cover letter. Please provide:\n\
|
|
- The job title or position\n\
|
|
- Your key skills and experience\n\
|
|
- Any specific company or role details (optional)\n\n\
|
|
Or you can use the 'Generate Cover Letter' button on the job application page.";
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply: reply.to_string(),
|
|
data: serde_json::json!({}),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
"improve_resume_summary" => {
|
|
let reply = "I can help improve your resume summary. Please share:\n\
|
|
- Your current resume summary (or paste it here)\n\
|
|
- The type of role you're targeting\n\
|
|
- Your key skills and experience\n\n\
|
|
Or you can use the 'Improve Resume' feature in your profile page.";
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent: intent.clone(),
|
|
reply: reply.to_string(),
|
|
data: serde_json::json!({}),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
_ => {
|
|
let generic = self
|
|
.ai_provider
|
|
.complete(
|
|
"You are Nxtgauge workflow assistant. Keep answers concise and actionable. If users ask about features, guide them to use the appropriate buttons or pages.",
|
|
&request.message,
|
|
)
|
|
.await?;
|
|
|
|
Ok(ChatMessageResponse {
|
|
intent,
|
|
reply: generic,
|
|
data: serde_json::json!({}),
|
|
conversation_id: conversation_id.clone(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn classify_intent(message: &str) -> String {
|
|
let text = message.to_lowercase();
|
|
|
|
if text.contains("job description")
|
|
|| text.contains("generate job")
|
|
|| text.contains("create job")
|
|
|| text.contains("write job")
|
|
|| (text.contains("jd") && text.len() < 10)
|
|
{
|
|
return "job_description_generation".to_string();
|
|
}
|
|
|
|
if text.contains("cover letter")
|
|
|| text.contains("write a letter")
|
|
|| text.contains("generate letter")
|
|
{
|
|
return "generate_cover_letter".to_string();
|
|
}
|
|
|
|
if text.contains("resume")
|
|
&& (text.contains("improve")
|
|
|| text.contains("rewrite")
|
|
|| text.contains("summary")
|
|
|| text.contains("tailor"))
|
|
{
|
|
return "improve_resume_summary".to_string();
|
|
}
|
|
|
|
if text.contains("form") || text.contains("field") || text.contains("fill") {
|
|
return "form_filling_assistance".to_string();
|
|
}
|
|
|
|
if text.contains("search kb")
|
|
|| text.contains("find article")
|
|
|| text.contains("how do i")
|
|
|| text.contains("how to")
|
|
|| text.contains("where do i")
|
|
|| (text.contains("help") && !text.contains("help me"))
|
|
|| text.contains("article")
|
|
|| text.contains("kb")
|
|
|| text.contains("docs")
|
|
|| text.contains("documentation")
|
|
{
|
|
return "search_kb".to_string();
|
|
}
|
|
|
|
if text.contains("ticket")
|
|
|| text.contains("support")
|
|
|| text.contains("issue")
|
|
|| text.contains("bug")
|
|
|| text.contains("problem")
|
|
|| text.contains("not working")
|
|
|| text.contains("error")
|
|
{
|
|
return "support_ticket_creation".to_string();
|
|
}
|
|
|
|
if text.contains("ai plan")
|
|
|| text.contains("ai limit")
|
|
|| text.contains("ai package")
|
|
|| text.contains("ai credit")
|
|
|| text.contains("upgrade ai")
|
|
{
|
|
return "explain_plan_limits".to_string();
|
|
}
|
|
|
|
if text.contains("balance")
|
|
&& (text.contains("ai") || text.contains("credit") || text.contains("action"))
|
|
{
|
|
return "check_ai_pack_balance".to_string();
|
|
}
|
|
|
|
"general".to_string()
|
|
}
|