From e33ed2192c3cf9172888a6b3d9e77a9cb52b2892 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Thu, 2 Jul 2026 06:51:03 +0530 Subject: [PATCH] Update workflows and source files --- .forgejo/workflows/build.yaml | 11 +- src/chat/models.rs | 42 ++++ src/chat/orchestrator.rs | 365 +++++++++++++++++----------- src/handlers/actions.rs | 5 + src/handlers/confirm_action.rs | 5 +- src/services/action_confirmation.rs | 253 ++----------------- src/state.rs | 6 +- src/tests.rs | 42 ++++ 8 files changed, 340 insertions(+), 389 deletions(-) diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 27f56e0..77918fc 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -12,7 +12,10 @@ concurrency: jobs: build: - runs-on: self-hosted + runs-on: docker-ready + env: + DOCKER_HOST: tcp://127.0.0.1:2375 + DOCKER_BUILDKIT: "1" steps: - name: Checkout uses: actions/checkout@v4 @@ -22,6 +25,12 @@ jobs: - name: Set up Docker Buildx run: | set -euo pipefail + for attempt in $(seq 1 30); do + if docker version >/dev/null 2>&1; then + break + fi + sleep 2 + done docker version docker buildx create --use --name nxtgauge-builder || docker buildx use nxtgauge-builder docker buildx inspect --bootstrap diff --git a/src/chat/models.rs b/src/chat/models.rs index d23a605..60cc605 100644 --- a/src/chat/models.rs +++ b/src/chat/models.rs @@ -2,11 +2,42 @@ use serde::{Deserialize, Serialize}; use crate::handlers::actions::UiEvent; +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ChatContext { + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub form_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub product: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatMessageRequest { pub message: String, pub user_id: Option, pub conversation_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub role: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub product: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SuggestedAction { + pub action: String, + pub backend_handler: String, + pub requires_confirmation: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub feature_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub missing_fields: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -14,8 +45,19 @@ pub struct ChatMessageResponse { pub intent: String, pub reply: String, pub data: serde_json::Value, + pub status: String, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub confidence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub missing_fields: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub requires_confirmation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub suggested_action: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub ui_events: Option>, } diff --git a/src/chat/orchestrator.rs b/src/chat/orchestrator.rs index da79dbd..646a42e 100644 --- a/src/chat/orchestrator.rs +++ b/src/chat/orchestrator.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use crate::{ - chat::models::{ChatMessageRequest, ChatMessageResponse}, + actions::get_action, + chat::models::{ChatMessageRequest, ChatMessageResponse, SuggestedAction}, error::AppError, forms::{models::FormExtractRequest, service::FormService}, handlers::actions::UiEvent, @@ -9,7 +10,7 @@ use crate::{ providers::{ help_center::help_center_provider::HelpCenterProvider, llm::ai_provider::AiProvider, }, - tickets::{models::CreateTicketRequest, service::TicketService}, + tickets::service::TicketService, }; #[derive(Clone)] @@ -17,7 +18,6 @@ pub struct ChatOrchestrator { jobs_service: JobsService, form_service: FormService, help_center: Arc, - ticket_service: TicketService, ai_provider: Arc, } @@ -26,14 +26,13 @@ impl ChatOrchestrator { jobs_service: JobsService, form_service: FormService, help_center: Arc, - ticket_service: TicketService, + _ticket_service: TicketService, ai_provider: Arc, ) -> Self { Self { jobs_service, form_service, help_center, - ticket_service, ai_provider, } } @@ -42,7 +41,7 @@ impl ChatOrchestrator { &self, request: ChatMessageRequest, ) -> Result { - let intent = classify_intent(&request.message); + let (intent, confidence) = classify_intent(&request.message); let conversation_id = request.conversation_id.clone(); match intent.as_str() { @@ -61,31 +60,33 @@ impl ChatOrchestrator { }) .await?; - let ui_events = Some(vec![ - UiEvent { - event_type: "refresh_data".to_string(), - target: "jobs_list".to_string(), - record_id: None, - fields: None, - }, - UiEvent { - event_type: "open_preview".to_string(), - target: "job_draft".to_string(), - record_id: None, - fields: Some(serde_json::json!({ - "role_summary": jd.role_summary, - "responsibilities": jd.responsibilities, - "requirements": jd.requirements, - })), - }, - ]); + let fields = serde_json::json!({ + "role_summary": jd.role_summary, + "responsibilities": jd.responsibilities, + "requirements": jd.requirements, + }); Ok(ChatMessageResponse { intent: intent.clone(), - reply: "Generated a draft job description.".to_string(), + reply: "Generated a draft job description. Your product backend can review or save it.".to_string(), data: serde_json::to_value(jd).unwrap_or(serde_json::Value::Null), - conversation_id: conversation_id.clone(), - ui_events, + status: "completed".to_string(), + conversation_id, + confidence: Some(confidence), + fields: Some(fields.clone()), + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: build_suggested_action( + "generate_job_description", + Some(fields), + None, + ), + ui_events: Some(vec![UiEvent { + event_type: "open_preview".to_string(), + target: "job_description".to_string(), + record_id: None, + fields: None, + }]), }) } "form_filling_assistance" => { @@ -97,21 +98,43 @@ impl ChatOrchestrator { }) .await?; - let ui_events = Some(vec![UiEvent { - event_type: "fill_form".to_string(), - target: "form_auto_fill".to_string(), - record_id: None, - fields: Some(serde_json::json!({ - "fields": extracted.fields, - })), - }]); + let extracted_fields = serde_json::to_value(&extracted.fields) + .unwrap_or(serde_json::Value::Null); + let missing_fields = Some(extracted.missing_fields.clone()); + let status = if extracted.missing_fields.is_empty() { + "completed" + } else { + "needs_input" + }; 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(), - ui_events, + status: status.to_string(), + conversation_id, + confidence: Some(confidence), + fields: Some(extracted_fields.clone()), + missing_fields: missing_fields.clone(), + requires_confirmation: Some(false), + suggested_action: Some(SuggestedAction { + action: "fill_form".to_string(), + backend_handler: "frontend.fill_form".to_string(), + requires_confirmation: false, + feature_code: None, + fields: Some(extracted_fields), + missing_fields, + }), + ui_events: Some(vec![UiEvent { + event_type: "fill_form".to_string(), + target: request + .context + .as_ref() + .and_then(|ctx| ctx.form_id.clone()) + .unwrap_or_else(|| "form_auto_fill".to_string()), + record_id: None, + fields: None, + }]), }) } "search_kb" => { @@ -119,9 +142,14 @@ impl ChatOrchestrator { 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()); + let mut response = format!("Found {} help article(s): + +", matches.len()); for (i, article) in matches.iter().take(5).enumerate() { - response.push_str(&format!("{}. **{}**\n{}\n\n", i + 1, article.title, article.summary)); + response.push_str(&format!("{}. **{}** +{} + +", i + 1, article.title, article.summary)); } if matches.len() > 5 { response.push_str(&format!("...and {} more articles.", matches.len() - 5)); @@ -130,118 +158,131 @@ impl ChatOrchestrator { }; Ok(ChatMessageResponse { - intent: intent.clone(), + intent, reply, data: serde_json::json!({ "matches": matches }), - conversation_id: conversation_id.clone(), + status: "completed".to_string(), + conversation_id, + confidence: Some(confidence), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: build_suggested_action("search_kb", None, None), ui_events: None, }) } "support_ticket_creation" => { - let created = self - .ticket_service - .create(CreateTicketRequest { - subject: request.message.chars().take(80).collect::(), - 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?; - - let ui_events = Some(vec![UiEvent { - event_type: "show_confirmation".to_string(), - target: "ticket_created".to_string(), - record_id: Some(created.ticket_id.clone()), - fields: None, - }]); + let fields = serde_json::json!({ + "subject": summarize_subject(&request.message), + "description": request.message, + "priority": "medium", + "category": "general", + }); 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(), - ui_events, + reply: "I can prepare a support ticket. Confirm this action in your product backend to create it.".to_string(), + data: fields.clone(), + status: "needs_confirmation".to_string(), + conversation_id, + confidence: Some(confidence), + fields: Some(fields.clone()), + missing_fields: None, + requires_confirmation: Some(true), + suggested_action: Some(SuggestedAction { + action: "create_support_ticket".to_string(), + backend_handler: "support.create_ticket".to_string(), + requires_confirmation: true, + feature_code: Some("support_ticket_creation".to_string()), + fields: Some(fields), + missing_fields: None, + }), + ui_events: Some(vec![UiEvent { + event_type: "show_confirmation".to_string(), + target: "support_ticket".to_string(), + record_id: None, + fields: None, + }]), }) } - "explain_plan_limits" => { - let ui_events = Some(vec![UiEvent { - event_type: "show_upgrade_modal".to_string(), - target: "ai_plan_upgrade_modal".to_string(), - record_id: None, - fields: None, - }]); - - 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. Opening the upgrade options..."; - - Ok(ChatMessageResponse { - intent: intent.clone(), - reply: reply.to_string(), - data: serde_json::json!({}), - conversation_id: conversation_id.clone(), - ui_events, - }) - } - "check_ai_pack_balance" => { - let ui_events = Some(vec![UiEvent { + "explain_plan_limits" => Ok(ChatMessageResponse { + intent: intent.clone(), + reply: "Your backend should explain plan limits in terms of monthly actions, add-on balance, and feature access. Use the billing or usage modal in the product UI for exact numbers.".to_string(), + data: serde_json::json!({}), + status: "info".to_string(), + conversation_id, + confidence: Some(confidence), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: build_suggested_action("explain_plan_limits", None, None), + ui_events: Some(vec![UiEvent { event_type: "show_usage_modal".to_string(), - target: "ai_usage_modal".to_string(), + target: "ai_plan_usage".to_string(), record_id: None, fields: None, - }]); - - let reply = "Opening your AI usage dashboard..."; - - Ok(ChatMessageResponse { - intent: intent.clone(), - reply: reply.to_string(), - data: serde_json::json!({}), - conversation_id: conversation_id.clone(), - ui_events, - }) - } - "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(), - ui_events: None, - }) - } - "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(), - ui_events: None, - }) - } + }]), + }), + "check_ai_pack_balance" => Ok(ChatMessageResponse { + intent: intent.clone(), + reply: "Open the product usage view to see remaining AI actions, add-ons, and renewal details.".to_string(), + data: serde_json::json!({}), + status: "info".to_string(), + conversation_id, + confidence: Some(confidence), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: build_suggested_action("check_ai_pack_balance", None, None), + ui_events: Some(vec![UiEvent { + event_type: "show_usage_modal".to_string(), + target: "ai_usage".to_string(), + record_id: None, + fields: None, + }]), + }), + "generate_cover_letter" => Ok(ChatMessageResponse { + intent: intent.clone(), + reply: "I can help generate a cover letter. Provide the target role, your key skills, and any company details you want included.".to_string(), + data: serde_json::json!({}), + status: "needs_input".to_string(), + conversation_id, + confidence: Some(confidence), + fields: None, + missing_fields: Some(vec![ + "job_title".to_string(), + "applicant_skills".to_string(), + ]), + requires_confirmation: Some(false), + suggested_action: build_suggested_action( + "generate_cover_letter", + None, + Some(vec!["job_title".to_string(), "applicant_skills".to_string()]), + ), + ui_events: None, + }), + "improve_resume_summary" => Ok(ChatMessageResponse { + intent: intent.clone(), + reply: "Share the current summary and the role you are targeting, and I will prepare an improved version for your backend to review.".to_string(), + data: serde_json::json!({}), + status: "needs_input".to_string(), + conversation_id, + confidence: Some(confidence), + fields: None, + missing_fields: Some(vec!["current_summary".to_string()]), + requires_confirmation: Some(false), + suggested_action: build_suggested_action( + "improve_resume_summary", + None, + Some(vec!["current_summary".to_string()]), + ), + ui_events: None, + }), _ => { - let system_prompt = crate::prompts::get_prompt("general_system") - .unwrap_or_else(|| "You are Nxtgauge workflow assistant. Keep answers concise and actionable. If users ask about features, guide them to use the appropriate buttons or pages.".to_string()); + let system_prompt = crate::prompts::get_prompt("general_system").unwrap_or_else(|| { + "You are a reusable workflow assistant. Suggest structured next steps, avoid product-specific promises, and keep answers concise." + .to_string() + }); let generic = self .ai_provider @@ -252,7 +293,13 @@ impl ChatOrchestrator { intent, reply: generic, data: serde_json::json!({}), - conversation_id: conversation_id.clone(), + status: "completed".to_string(), + conversation_id, + confidence: Some(confidence), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: None, ui_events: None, }) } @@ -260,7 +307,31 @@ impl ChatOrchestrator { } } -fn classify_intent(message: &str) -> String { +fn build_suggested_action( + action_code: &str, + fields: Option, + missing_fields: Option>, +) -> Option { + let action = get_action(action_code)?; + Some(SuggestedAction { + action: action.action_code, + backend_handler: action.backend_handler, + requires_confirmation: action.requires_confirmation, + feature_code: Some(action.feature_code), + fields, + missing_fields, + }) +} + +fn summarize_subject(message: &str) -> String { + let trimmed = message.trim(); + if trimmed.len() <= 80 { + return trimmed.to_string(); + } + trimmed.chars().take(77).collect::() + "..." +} + +fn classify_intent(message: &str) -> (String, f32) { let text = message.to_lowercase(); if text.contains("job description") @@ -269,14 +340,14 @@ fn classify_intent(message: &str) -> String { || text.contains("write job") || (text.contains("jd") && text.len() < 10) { - return "job_description_generation".to_string(); + return ("job_description_generation".to_string(), 0.92); } if text.contains("cover letter") || text.contains("write a letter") || text.contains("generate letter") { - return "generate_cover_letter".to_string(); + return ("generate_cover_letter".to_string(), 0.88); } if text.contains("resume") @@ -285,11 +356,11 @@ fn classify_intent(message: &str) -> String { || text.contains("summary") || text.contains("tailor")) { - return "improve_resume_summary".to_string(); + return ("improve_resume_summary".to_string(), 0.88); } if text.contains("form") || text.contains("field") || text.contains("fill") { - return "form_filling_assistance".to_string(); + return ("form_filling_assistance".to_string(), 0.84); } if text.contains("search kb") @@ -303,7 +374,7 @@ fn classify_intent(message: &str) -> String { || text.contains("docs") || text.contains("documentation") { - return "search_kb".to_string(); + return ("search_kb".to_string(), 0.86); } if text.contains("ticket") @@ -314,7 +385,7 @@ fn classify_intent(message: &str) -> String { || text.contains("not working") || text.contains("error") { - return "support_ticket_creation".to_string(); + return ("support_ticket_creation".to_string(), 0.82); } if text.contains("ai plan") @@ -323,14 +394,14 @@ fn classify_intent(message: &str) -> String { || text.contains("ai credit") || text.contains("upgrade ai") { - return "explain_plan_limits".to_string(); + return ("explain_plan_limits".to_string(), 0.78); } if text.contains("balance") && (text.contains("ai") || text.contains("credit") || text.contains("action")) { - return "check_ai_pack_balance".to_string(); + return ("check_ai_pack_balance".to_string(), 0.8); } - "general".to_string() + ("general".to_string(), 0.55) } diff --git a/src/handlers/actions.rs b/src/handlers/actions.rs index 1283e09..65487c6 100644 --- a/src/handlers/actions.rs +++ b/src/handlers/actions.rs @@ -12,11 +12,16 @@ pub struct ConfirmActionRequest { #[derive(Debug, Clone, Serialize)] pub struct ConfirmActionResponse { pub success: bool, + pub delegated: bool, pub message: String, pub action: String, #[serde(skip_serializing_if = "Option::is_none")] + pub backend_handler: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub record_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub ui_events: Option>, } diff --git a/src/handlers/confirm_action.rs b/src/handlers/confirm_action.rs index bee97cb..74a58b0 100644 --- a/src/handlers/confirm_action.rs +++ b/src/handlers/confirm_action.rs @@ -3,7 +3,6 @@ use axum::{extract::State, Json}; use crate::{ error::AppError, handlers::actions::{ConfirmActionRequest, ConfirmActionResponse}, - services::action_confirmation::ActionConfirmationService, state::AppState, }; @@ -19,11 +18,9 @@ pub async fn confirm_action( return Err(AppError::BadRequest("action is required".to_string())); } - let user_id = request.conversation_id.clone(); - let response = state .action_confirmation_service - .confirm_action(request, &user_id) + .confirm_action(request) .await?; Ok(Json(response)) diff --git a/src/services/action_confirmation.rs b/src/services/action_confirmation.rs index 5adae45..4e6348b 100644 --- a/src/services/action_confirmation.rs +++ b/src/services/action_confirmation.rs @@ -1,263 +1,52 @@ -use std::sync::Arc; - -use reqwest::Client; - use crate::{ actions::get_action, error::AppError, - handlers::actions::{ConfirmActionRequest, ConfirmActionResponse, UiEvent}, + handlers::actions::{ConfirmActionRequest, ConfirmActionResponse}, }; -#[derive(Clone)] -pub struct ActionConfirmationService { - http_client: Client, - jobs_service_url: String, - users_service_url: String, - customers_service_url: String, -} +#[derive(Clone, Default)] +pub struct ActionConfirmationService; impl ActionConfirmationService { - pub fn new( - users_service_url: String, - jobs_service_url: String, - customers_service_url: String, - ) -> Self { - Self { - http_client: Client::new(), - users_service_url, - jobs_service_url, - customers_service_url, - } + pub fn new() -> Self { + Self } pub async fn confirm_action( &self, request: ConfirmActionRequest, - user_id: &str, ) -> Result { if !request.confirmed { return Ok(ConfirmActionResponse { success: false, + delegated: false, message: "Action cancelled by user.".to_string(), action: request.action.clone(), + backend_handler: None, record_id: None, + fields: None, ui_events: None, }); } - let action = get_action(&request.action) - .ok_or_else(|| AppError::BadRequest(format!("Unknown action: {}", request.action)))?; - - match request.action.as_str() { - "create_job_draft" => { - self.create_job_draft(&request, user_id).await - } - "fill_company_profile_form" => { - self.fill_company_profile(&request, user_id).await - } - "improve_company_profile" => { - self.improve_company_profile(&request, user_id).await - } - "create_customer_requirement_draft" => { - self.create_requirement_draft(&request, user_id).await - } - "improve_requirement_description" => { - self.improve_requirement(&request, user_id).await - } - "improve_professional_profile" => { - self.improve_professional_profile(&request, user_id).await - } - "improve_jobseeker_profile" => { - self.improve_jobseeker_profile(&request, user_id).await - } - _ => Ok(ConfirmActionResponse { - success: false, - message: format!("Action {} not yet implemented for confirmation", request.action), - action: request.action.clone(), - record_id: None, - ui_events: None, - }), - } - } - - async fn create_job_draft( - &self, - request: &ConfirmActionRequest, - _user_id: &str, - ) -> Result { - let fields = request.fields.as_object().cloned().unwrap_or_default(); - - let title = fields.get("job_title") - .and_then(|v| v.as_str()) - .unwrap_or("Untitled Job"); - let description = fields.get("description") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let location = fields.get("location") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let action = get_action(&request.action); + let backend_handler = action + .as_ref() + .map(|item| item.backend_handler.clone()) + .unwrap_or_else(|| request.action.clone()); Ok(ConfirmActionResponse { success: true, - message: format!("Job draft created: {}", title), + delegated: true, + message: format!( + "Action approved. Execute '{}' in the product backend.", + backend_handler + ), action: request.action.clone(), + backend_handler: Some(backend_handler), record_id: None, - ui_events: Some(vec![ - UiEvent { - event_type: "refresh_data".to_string(), - target: "jobs_list".to_string(), - record_id: None, - fields: None, - }, - UiEvent { - event_type: "open_preview".to_string(), - target: "job_draft".to_string(), - record_id: None, - fields: Some(serde_json::json!({ - "title": title, - "description": description, - "location": location, - })), - }, - ]), - }) - } - - async fn fill_company_profile( - &self, - request: &ConfirmActionRequest, - _user_id: &str, - ) -> Result { - let fields = request.fields.as_object().cloned().unwrap_or_default(); - - Ok(ConfirmActionResponse { - success: true, - message: "Company profile data extracted. Review and save.".to_string(), - action: request.action.clone(), - record_id: None, - ui_events: Some(vec![UiEvent { - event_type: "fill_form".to_string(), - target: "company_profile_form".to_string(), - record_id: None, - fields: Some(serde_json::json!(fields)), - }]), - }) - } - - async fn improve_company_profile( - &self, - request: &ConfirmActionRequest, - _user_id: &str, - ) -> Result { - let fields = request.fields.as_object().cloned().unwrap_or_default(); - - Ok(ConfirmActionResponse { - success: true, - message: "Company profile improved. Review changes.".to_string(), - action: request.action.clone(), - record_id: None, - ui_events: Some(vec![UiEvent { - event_type: "fill_form".to_string(), - target: "company_profile_form".to_string(), - record_id: None, - fields: Some(serde_json::json!(fields)), - }]), - }) - } - - async fn create_requirement_draft( - &self, - request: &ConfirmActionRequest, - _user_id: &str, - ) -> Result { - let fields = request.fields.as_object().cloned().unwrap_or_default(); - - let title = fields.get("title") - .and_then(|v| v.as_str()) - .unwrap_or("Untitled Requirement"); - - Ok(ConfirmActionResponse { - success: true, - message: format!("Requirement draft created: {}", title), - action: request.action.clone(), - record_id: None, - ui_events: Some(vec![ - UiEvent { - event_type: "refresh_data".to_string(), - target: "requirements_list".to_string(), - record_id: None, - fields: None, - }, - UiEvent { - event_type: "open_preview".to_string(), - target: "requirement_draft".to_string(), - record_id: None, - fields: Some(serde_json::json!(fields)), - }, - ]), - }) - } - - async fn improve_requirement( - &self, - request: &ConfirmActionRequest, - _user_id: &str, - ) -> Result { - let fields = request.fields.as_object().cloned().unwrap_or_default(); - - Ok(ConfirmActionResponse { - success: true, - message: "Requirement improved. Review changes.".to_string(), - action: request.action.clone(), - record_id: None, - ui_events: Some(vec![UiEvent { - event_type: "fill_form".to_string(), - target: "requirement_form".to_string(), - record_id: None, - fields: Some(serde_json::json!(fields)), - }]), - }) - } - - async fn improve_professional_profile( - &self, - request: &ConfirmActionRequest, - _user_id: &str, - ) -> Result { - let fields = request.fields.as_object().cloned().unwrap_or_default(); - - Ok(ConfirmActionResponse { - success: true, - message: "Professional profile improved. Review changes.".to_string(), - action: request.action.clone(), - record_id: None, - ui_events: Some(vec![UiEvent { - event_type: "fill_form".to_string(), - target: "professional_profile_form".to_string(), - record_id: None, - fields: Some(serde_json::json!(fields)), - }]), - }) - } - - async fn improve_jobseeker_profile( - &self, - request: &ConfirmActionRequest, - _user_id: &str, - ) -> Result { - let fields = request.fields.as_object().cloned().unwrap_or_default(); - - Ok(ConfirmActionResponse { - success: true, - message: "Job seeker profile improved. Review changes.".to_string(), - action: request.action.clone(), - record_id: None, - ui_events: Some(vec![UiEvent { - event_type: "fill_form".to_string(), - target: "jobseeker_profile_form".to_string(), - record_id: None, - fields: Some(serde_json::json!(fields)), - }]), + fields: Some(request.fields), + ui_events: None, }) } } diff --git a/src/state.rs b/src/state.rs index 1b2a379..80cbd05 100644 --- a/src/state.rs +++ b/src/state.rs @@ -47,11 +47,7 @@ impl AppState { ticket_service.clone(), ai_provider.clone(), ); - let action_confirmation_service = ActionConfirmationService::new( - config.nxtgauge_users_url.clone(), - "http://nxtgauge-rust-jobs:9103".to_string(), - "http://nxtgauge-rust-customers:9105".to_string(), - ); + let action_confirmation_service = ActionConfirmationService::new(); Self { config, diff --git a/src/tests.rs b/src/tests.rs index 91d9652..f002a04 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -137,9 +137,12 @@ mod tests { fn test_confirm_action_response_serialization() { let response = ConfirmActionResponse { success: true, + delegated: true, message: "Created".to_string(), action: "create_job_draft".to_string(), + backend_handler: Some("jobs.create_draft".to_string()), record_id: Some("456".to_string()), + fields: None, ui_events: Some(vec![UiEvent { event_type: "refresh_data".to_string(), target: "jobs_list".to_string(), @@ -157,9 +160,12 @@ mod tests { fn test_confirm_action_response_skips_none_fields() { let response = ConfirmActionResponse { success: false, + delegated: false, message: "Cancelled".to_string(), action: "test".to_string(), + backend_handler: None, record_id: None, + fields: None, ui_events: None, }; let json = serde_json::to_string(&response).unwrap(); @@ -195,7 +201,13 @@ mod tests { intent: "job_description_generation".to_string(), reply: "Generated".to_string(), data: serde_json::json!({"title": "Engineer"}), + status: "completed".to_string(), conversation_id: Some("conv-123".to_string()), + confidence: Some(0.9), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: None, ui_events: None, }; let json = serde_json::to_string(&response).unwrap(); @@ -209,7 +221,13 @@ mod tests { intent: "form_filling".to_string(), reply: "Form filled".to_string(), data: serde_json::json!({}), + status: "completed".to_string(), conversation_id: None, + confidence: Some(0.8), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: None, ui_events: Some(vec![UiEvent { event_type: "fill_form".to_string(), target: "test_form".to_string(), @@ -437,7 +455,13 @@ mod tests { {"title": "Article 2", "url": "https://example.com/2"} ] }), + status: "completed".to_string(), conversation_id: Some("conv-789".to_string()), + confidence: Some(0.85), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: None, ui_events: None, }; let json = serde_json::to_string(&response).unwrap(); @@ -521,7 +545,13 @@ mod tests { intent: "test_intent".to_string(), reply: "Test reply".to_string(), data: serde_json::json!({"key": "value"}), + status: "completed".to_string(), conversation_id: Some("conv-123".to_string()), + confidence: Some(0.7), + fields: None, + missing_fields: None, + requires_confirmation: Some(false), + suggested_action: None, ui_events: None, }; let json = serde_json::to_string(&response).unwrap(); @@ -549,9 +579,12 @@ mod tests { fn test_confirm_action_response_success() { let response = ConfirmActionResponse { success: true, + delegated: true, message: "Action completed".to_string(), action: "create_job_draft".to_string(), + backend_handler: Some("jobs.create_draft".to_string()), record_id: Some("789".to_string()), + fields: None, ui_events: None, }; let json = serde_json::to_string(&response).unwrap(); @@ -563,9 +596,12 @@ mod tests { fn test_confirm_action_response_with_ui_events() { let response = ConfirmActionResponse { success: true, + delegated: true, message: "Created".to_string(), action: "test".to_string(), + backend_handler: Some("test.handler".to_string()), record_id: None, + fields: None, ui_events: Some(vec![UiEvent { event_type: "show_confirmation".to_string(), target: "modal".to_string(), @@ -584,6 +620,9 @@ mod tests { message: "Hello AI".to_string(), user_id: Some("user-123".to_string()), conversation_id: Some("conv-456".to_string()), + role: None, + product: None, + context: None, }; let json = serde_json::to_string(&request).unwrap(); let parsed: ChatMessageRequest = serde_json::from_str(&json).unwrap(); @@ -597,6 +636,9 @@ mod tests { message: "Test".to_string(), user_id: None, conversation_id: None, + role: None, + product: None, + context: None, }; assert!(request.conversation_id.is_none()); assert!(request.user_id.is_none());