Update workflows and source files

This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-02 06:51:03 +05:30
parent d66ef693e5
commit e33ed2192c
8 changed files with 340 additions and 389 deletions

View file

@ -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

View file

@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub form_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessageRequest {
pub message: String,
pub user_id: Option<String>,
pub conversation_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<ChatContext>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub missing_fields: Option<Vec<String>>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub missing_fields: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_confirmation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggested_action: Option<SuggestedAction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ui_events: Option<Vec<UiEvent>>,
}

View file

@ -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<dyn HelpCenterProvider>,
ticket_service: TicketService,
ai_provider: Arc<dyn AiProvider>,
}
@ -26,14 +26,13 @@ impl ChatOrchestrator {
jobs_service: JobsService,
form_service: FormService,
help_center: Arc<dyn HelpCenterProvider>,
ticket_service: TicketService,
_ticket_service: TicketService,
ai_provider: Arc<dyn AiProvider>,
) -> Self {
Self {
jobs_service,
form_service,
help_center,
ticket_service,
ai_provider,
}
}
@ -42,7 +41,7 @@ impl ChatOrchestrator {
&self,
request: ChatMessageRequest,
) -> Result<ChatMessageResponse, AppError> {
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::<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?;
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<serde_json::Value>,
missing_fields: Option<Vec<String>>,
) -> Option<SuggestedAction> {
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::<String>() + "..."
}
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)
}

View file

@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub record_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ui_events: Option<Vec<UiEvent>>,
}

View file

@ -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))

View file

@ -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<ConfirmActionResponse, AppError> {
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<ConfirmActionResponse, AppError> {
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<ConfirmActionResponse, AppError> {
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<ConfirmActionResponse, AppError> {
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<ConfirmActionResponse, AppError> {
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<ConfirmActionResponse, AppError> {
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<ConfirmActionResponse, AppError> {
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<ConfirmActionResponse, AppError> {
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,
})
}
}

View file

@ -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,

View file

@ -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());