Update workflows and source files
This commit is contained in:
parent
d66ef693e5
commit
e33ed2192c
8 changed files with 340 additions and 389 deletions
|
|
@ -12,7 +12,10 @@ concurrency:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: self-hosted
|
runs-on: docker-ready
|
||||||
|
env:
|
||||||
|
DOCKER_HOST: tcp://127.0.0.1:2375
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
@ -22,6 +25,12 @@ jobs:
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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 version
|
||||||
docker buildx create --use --name nxtgauge-builder || docker buildx use nxtgauge-builder
|
docker buildx create --use --name nxtgauge-builder || docker buildx use nxtgauge-builder
|
||||||
docker buildx inspect --bootstrap
|
docker buildx inspect --bootstrap
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,42 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::handlers::actions::UiEvent;
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ChatMessageRequest {
|
pub struct ChatMessageRequest {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
pub conversation_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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -14,8 +45,19 @@ pub struct ChatMessageResponse {
|
||||||
pub intent: String,
|
pub intent: String,
|
||||||
pub reply: String,
|
pub reply: String,
|
||||||
pub data: serde_json::Value,
|
pub data: serde_json::Value,
|
||||||
|
pub status: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub conversation_id: Option<String>,
|
pub conversation_id: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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>>,
|
pub ui_events: Option<Vec<UiEvent>>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chat::models::{ChatMessageRequest, ChatMessageResponse},
|
actions::get_action,
|
||||||
|
chat::models::{ChatMessageRequest, ChatMessageResponse, SuggestedAction},
|
||||||
error::AppError,
|
error::AppError,
|
||||||
forms::{models::FormExtractRequest, service::FormService},
|
forms::{models::FormExtractRequest, service::FormService},
|
||||||
handlers::actions::UiEvent,
|
handlers::actions::UiEvent,
|
||||||
|
|
@ -9,7 +10,7 @@ use crate::{
|
||||||
providers::{
|
providers::{
|
||||||
help_center::help_center_provider::HelpCenterProvider, llm::ai_provider::AiProvider,
|
help_center::help_center_provider::HelpCenterProvider, llm::ai_provider::AiProvider,
|
||||||
},
|
},
|
||||||
tickets::{models::CreateTicketRequest, service::TicketService},
|
tickets::service::TicketService,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -17,7 +18,6 @@ pub struct ChatOrchestrator {
|
||||||
jobs_service: JobsService,
|
jobs_service: JobsService,
|
||||||
form_service: FormService,
|
form_service: FormService,
|
||||||
help_center: Arc<dyn HelpCenterProvider>,
|
help_center: Arc<dyn HelpCenterProvider>,
|
||||||
ticket_service: TicketService,
|
|
||||||
ai_provider: Arc<dyn AiProvider>,
|
ai_provider: Arc<dyn AiProvider>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,14 +26,13 @@ impl ChatOrchestrator {
|
||||||
jobs_service: JobsService,
|
jobs_service: JobsService,
|
||||||
form_service: FormService,
|
form_service: FormService,
|
||||||
help_center: Arc<dyn HelpCenterProvider>,
|
help_center: Arc<dyn HelpCenterProvider>,
|
||||||
ticket_service: TicketService,
|
_ticket_service: TicketService,
|
||||||
ai_provider: Arc<dyn AiProvider>,
|
ai_provider: Arc<dyn AiProvider>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
jobs_service,
|
jobs_service,
|
||||||
form_service,
|
form_service,
|
||||||
help_center,
|
help_center,
|
||||||
ticket_service,
|
|
||||||
ai_provider,
|
ai_provider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +41,7 @@ impl ChatOrchestrator {
|
||||||
&self,
|
&self,
|
||||||
request: ChatMessageRequest,
|
request: ChatMessageRequest,
|
||||||
) -> Result<ChatMessageResponse, AppError> {
|
) -> Result<ChatMessageResponse, AppError> {
|
||||||
let intent = classify_intent(&request.message);
|
let (intent, confidence) = classify_intent(&request.message);
|
||||||
let conversation_id = request.conversation_id.clone();
|
let conversation_id = request.conversation_id.clone();
|
||||||
|
|
||||||
match intent.as_str() {
|
match intent.as_str() {
|
||||||
|
|
@ -61,31 +60,33 @@ impl ChatOrchestrator {
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let ui_events = Some(vec![
|
let fields = serde_json::json!({
|
||||||
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,
|
"role_summary": jd.role_summary,
|
||||||
"responsibilities": jd.responsibilities,
|
"responsibilities": jd.responsibilities,
|
||||||
"requirements": jd.requirements,
|
"requirements": jd.requirements,
|
||||||
})),
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
Ok(ChatMessageResponse {
|
Ok(ChatMessageResponse {
|
||||||
intent: intent.clone(),
|
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),
|
data: serde_json::to_value(jd).unwrap_or(serde_json::Value::Null),
|
||||||
conversation_id: conversation_id.clone(),
|
status: "completed".to_string(),
|
||||||
ui_events,
|
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" => {
|
"form_filling_assistance" => {
|
||||||
|
|
@ -97,21 +98,43 @@ impl ChatOrchestrator {
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let ui_events = Some(vec![UiEvent {
|
let extracted_fields = serde_json::to_value(&extracted.fields)
|
||||||
event_type: "fill_form".to_string(),
|
.unwrap_or(serde_json::Value::Null);
|
||||||
target: "form_auto_fill".to_string(),
|
let missing_fields = Some(extracted.missing_fields.clone());
|
||||||
record_id: None,
|
let status = if extracted.missing_fields.is_empty() {
|
||||||
fields: Some(serde_json::json!({
|
"completed"
|
||||||
"fields": extracted.fields,
|
} else {
|
||||||
})),
|
"needs_input"
|
||||||
}]);
|
};
|
||||||
|
|
||||||
Ok(ChatMessageResponse {
|
Ok(ChatMessageResponse {
|
||||||
intent: intent.clone(),
|
intent: intent.clone(),
|
||||||
reply: extracted.suggested_next_step.clone(),
|
reply: extracted.suggested_next_step.clone(),
|
||||||
data: serde_json::to_value(extracted).unwrap_or(serde_json::Value::Null),
|
data: serde_json::to_value(extracted).unwrap_or(serde_json::Value::Null),
|
||||||
conversation_id: conversation_id.clone(),
|
status: status.to_string(),
|
||||||
ui_events,
|
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" => {
|
"search_kb" => {
|
||||||
|
|
@ -119,9 +142,14 @@ impl ChatOrchestrator {
|
||||||
let reply = if matches.is_empty() {
|
let reply = if matches.is_empty() {
|
||||||
"No help article match found. Try rephrasing your question.".to_string()
|
"No help article match found. Try rephrasing your question.".to_string()
|
||||||
} else {
|
} 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() {
|
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 {
|
if matches.len() > 5 {
|
||||||
response.push_str(&format!("...and {} more articles.", matches.len() - 5));
|
response.push_str(&format!("...and {} more articles.", matches.len() - 5));
|
||||||
|
|
@ -130,118 +158,131 @@ impl ChatOrchestrator {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ChatMessageResponse {
|
Ok(ChatMessageResponse {
|
||||||
intent: intent.clone(),
|
intent,
|
||||||
reply,
|
reply,
|
||||||
data: serde_json::json!({ "matches": matches }),
|
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,
|
ui_events: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"support_ticket_creation" => {
|
"support_ticket_creation" => {
|
||||||
let created = self
|
let fields = serde_json::json!({
|
||||||
.ticket_service
|
"subject": summarize_subject(&request.message),
|
||||||
.create(CreateTicketRequest {
|
"description": request.message,
|
||||||
subject: request.message.chars().take(80).collect::<String>(),
|
"priority": "medium",
|
||||||
description: request.message.clone(),
|
"category": "general",
|
||||||
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 {
|
Ok(ChatMessageResponse {
|
||||||
|
intent: intent.clone(),
|
||||||
|
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(),
|
event_type: "show_confirmation".to_string(),
|
||||||
target: "ticket_created".to_string(),
|
target: "support_ticket".to_string(),
|
||||||
record_id: Some(created.ticket_id.clone()),
|
|
||||||
fields: None,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"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,
|
record_id: None,
|
||||||
fields: 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" => {
|
"explain_plan_limits" => Ok(ChatMessageResponse {
|
||||||
let ui_events = Some(vec![UiEvent {
|
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(),
|
event_type: "show_usage_modal".to_string(),
|
||||||
target: "ai_usage_modal".to_string(),
|
target: "ai_plan_usage".to_string(),
|
||||||
record_id: None,
|
record_id: None,
|
||||||
fields: None,
|
fields: None,
|
||||||
}]);
|
}]),
|
||||||
|
}),
|
||||||
let reply = "Opening your AI usage dashboard...";
|
"check_ai_pack_balance" => Ok(ChatMessageResponse {
|
||||||
|
|
||||||
Ok(ChatMessageResponse {
|
|
||||||
intent: intent.clone(),
|
intent: intent.clone(),
|
||||||
reply: reply.to_string(),
|
reply: "Open the product usage view to see remaining AI actions, add-ons, and renewal details.".to_string(),
|
||||||
data: serde_json::json!({}),
|
data: serde_json::json!({}),
|
||||||
conversation_id: conversation_id.clone(),
|
status: "info".to_string(),
|
||||||
ui_events,
|
conversation_id,
|
||||||
})
|
confidence: Some(confidence),
|
||||||
}
|
fields: None,
|
||||||
"generate_cover_letter" => {
|
missing_fields: None,
|
||||||
let reply = "I can help you generate a cover letter. Please provide:\n\
|
requires_confirmation: Some(false),
|
||||||
- The job title or position\n\
|
suggested_action: build_suggested_action("check_ai_pack_balance", None, None),
|
||||||
- Your key skills and experience\n\
|
ui_events: Some(vec![UiEvent {
|
||||||
- Any specific company or role details (optional)\n\n\
|
event_type: "show_usage_modal".to_string(),
|
||||||
Or you can use the 'Generate Cover Letter' button on the job application page.";
|
target: "ai_usage".to_string(),
|
||||||
|
record_id: None,
|
||||||
Ok(ChatMessageResponse {
|
fields: None,
|
||||||
|
}]),
|
||||||
|
}),
|
||||||
|
"generate_cover_letter" => Ok(ChatMessageResponse {
|
||||||
intent: intent.clone(),
|
intent: intent.clone(),
|
||||||
reply: reply.to_string(),
|
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!({}),
|
data: serde_json::json!({}),
|
||||||
conversation_id: conversation_id.clone(),
|
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,
|
ui_events: None,
|
||||||
})
|
}),
|
||||||
}
|
"improve_resume_summary" => Ok(ChatMessageResponse {
|
||||||
"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(),
|
intent: intent.clone(),
|
||||||
reply: reply.to_string(),
|
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!({}),
|
data: serde_json::json!({}),
|
||||||
conversation_id: conversation_id.clone(),
|
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,
|
ui_events: None,
|
||||||
})
|
}),
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
let system_prompt = crate::prompts::get_prompt("general_system")
|
let system_prompt = crate::prompts::get_prompt("general_system").unwrap_or_else(|| {
|
||||||
.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());
|
"You are a reusable workflow assistant. Suggest structured next steps, avoid product-specific promises, and keep answers concise."
|
||||||
|
.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
let generic = self
|
let generic = self
|
||||||
.ai_provider
|
.ai_provider
|
||||||
|
|
@ -252,7 +293,13 @@ impl ChatOrchestrator {
|
||||||
intent,
|
intent,
|
||||||
reply: generic,
|
reply: generic,
|
||||||
data: serde_json::json!({}),
|
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,
|
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();
|
let text = message.to_lowercase();
|
||||||
|
|
||||||
if text.contains("job description")
|
if text.contains("job description")
|
||||||
|
|
@ -269,14 +340,14 @@ fn classify_intent(message: &str) -> String {
|
||||||
|| text.contains("write job")
|
|| text.contains("write job")
|
||||||
|| (text.contains("jd") && text.len() < 10)
|
|| (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")
|
if text.contains("cover letter")
|
||||||
|| text.contains("write a letter")
|
|| text.contains("write a letter")
|
||||||
|| text.contains("generate letter")
|
|| text.contains("generate letter")
|
||||||
{
|
{
|
||||||
return "generate_cover_letter".to_string();
|
return ("generate_cover_letter".to_string(), 0.88);
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.contains("resume")
|
if text.contains("resume")
|
||||||
|
|
@ -285,11 +356,11 @@ fn classify_intent(message: &str) -> String {
|
||||||
|| text.contains("summary")
|
|| text.contains("summary")
|
||||||
|| text.contains("tailor"))
|
|| 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") {
|
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")
|
if text.contains("search kb")
|
||||||
|
|
@ -303,7 +374,7 @@ fn classify_intent(message: &str) -> String {
|
||||||
|| text.contains("docs")
|
|| text.contains("docs")
|
||||||
|| text.contains("documentation")
|
|| text.contains("documentation")
|
||||||
{
|
{
|
||||||
return "search_kb".to_string();
|
return ("search_kb".to_string(), 0.86);
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.contains("ticket")
|
if text.contains("ticket")
|
||||||
|
|
@ -314,7 +385,7 @@ fn classify_intent(message: &str) -> String {
|
||||||
|| text.contains("not working")
|
|| text.contains("not working")
|
||||||
|| text.contains("error")
|
|| text.contains("error")
|
||||||
{
|
{
|
||||||
return "support_ticket_creation".to_string();
|
return ("support_ticket_creation".to_string(), 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.contains("ai plan")
|
if text.contains("ai plan")
|
||||||
|
|
@ -323,14 +394,14 @@ fn classify_intent(message: &str) -> String {
|
||||||
|| text.contains("ai credit")
|
|| text.contains("ai credit")
|
||||||
|| text.contains("upgrade ai")
|
|| text.contains("upgrade ai")
|
||||||
{
|
{
|
||||||
return "explain_plan_limits".to_string();
|
return ("explain_plan_limits".to_string(), 0.78);
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.contains("balance")
|
if text.contains("balance")
|
||||||
&& (text.contains("ai") || text.contains("credit") || text.contains("action"))
|
&& (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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,16 @@ pub struct ConfirmActionRequest {
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct ConfirmActionResponse {
|
pub struct ConfirmActionResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
|
pub delegated: bool,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub action: String,
|
pub action: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub backend_handler: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub record_id: Option<String>,
|
pub record_id: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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>>,
|
pub ui_events: Option<Vec<UiEvent>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use axum::{extract::State, Json};
|
||||||
use crate::{
|
use crate::{
|
||||||
error::AppError,
|
error::AppError,
|
||||||
handlers::actions::{ConfirmActionRequest, ConfirmActionResponse},
|
handlers::actions::{ConfirmActionRequest, ConfirmActionResponse},
|
||||||
services::action_confirmation::ActionConfirmationService,
|
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,11 +18,9 @@ pub async fn confirm_action(
|
||||||
return Err(AppError::BadRequest("action is required".to_string()));
|
return Err(AppError::BadRequest("action is required".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = request.conversation_id.clone();
|
|
||||||
|
|
||||||
let response = state
|
let response = state
|
||||||
.action_confirmation_service
|
.action_confirmation_service
|
||||||
.confirm_action(request, &user_id)
|
.confirm_action(request)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
|
|
|
||||||
|
|
@ -1,263 +1,52 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use reqwest::Client;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::get_action,
|
actions::get_action,
|
||||||
error::AppError,
|
error::AppError,
|
||||||
handlers::actions::{ConfirmActionRequest, ConfirmActionResponse, UiEvent},
|
handlers::actions::{ConfirmActionRequest, ConfirmActionResponse},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ActionConfirmationService {
|
pub struct ActionConfirmationService;
|
||||||
http_client: Client,
|
|
||||||
jobs_service_url: String,
|
|
||||||
users_service_url: String,
|
|
||||||
customers_service_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActionConfirmationService {
|
impl ActionConfirmationService {
|
||||||
pub fn new(
|
pub fn new() -> Self {
|
||||||
users_service_url: String,
|
Self
|
||||||
jobs_service_url: String,
|
|
||||||
customers_service_url: String,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
http_client: Client::new(),
|
|
||||||
users_service_url,
|
|
||||||
jobs_service_url,
|
|
||||||
customers_service_url,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn confirm_action(
|
pub async fn confirm_action(
|
||||||
&self,
|
&self,
|
||||||
request: ConfirmActionRequest,
|
request: ConfirmActionRequest,
|
||||||
user_id: &str,
|
|
||||||
) -> Result<ConfirmActionResponse, AppError> {
|
) -> Result<ConfirmActionResponse, AppError> {
|
||||||
if !request.confirmed {
|
if !request.confirmed {
|
||||||
return Ok(ConfirmActionResponse {
|
return Ok(ConfirmActionResponse {
|
||||||
success: false,
|
success: false,
|
||||||
|
delegated: false,
|
||||||
message: "Action cancelled by user.".to_string(),
|
message: "Action cancelled by user.".to_string(),
|
||||||
action: request.action.clone(),
|
action: request.action.clone(),
|
||||||
|
backend_handler: None,
|
||||||
record_id: None,
|
record_id: None,
|
||||||
|
fields: None,
|
||||||
ui_events: None,
|
ui_events: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = get_action(&request.action)
|
let action = get_action(&request.action);
|
||||||
.ok_or_else(|| AppError::BadRequest(format!("Unknown action: {}", request.action)))?;
|
let backend_handler = action
|
||||||
|
.as_ref()
|
||||||
|
.map(|item| item.backend_handler.clone())
|
||||||
|
.unwrap_or_else(|| request.action.clone());
|
||||||
|
|
||||||
match request.action.as_str() {
|
Ok(ConfirmActionResponse {
|
||||||
"create_job_draft" => {
|
success: true,
|
||||||
self.create_job_draft(&request, user_id).await
|
delegated: true,
|
||||||
}
|
message: format!(
|
||||||
"fill_company_profile_form" => {
|
"Action approved. Execute '{}' in the product backend.",
|
||||||
self.fill_company_profile(&request, user_id).await
|
backend_handler
|
||||||
}
|
),
|
||||||
"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(),
|
action: request.action.clone(),
|
||||||
|
backend_handler: Some(backend_handler),
|
||||||
record_id: None,
|
record_id: None,
|
||||||
|
fields: Some(request.fields),
|
||||||
ui_events: 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("");
|
|
||||||
|
|
||||||
Ok(ConfirmActionResponse {
|
|
||||||
success: true,
|
|
||||||
message: format!("Job draft created: {}", title),
|
|
||||||
action: request.action.clone(),
|
|
||||||
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)),
|
|
||||||
}]),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,7 @@ impl AppState {
|
||||||
ticket_service.clone(),
|
ticket_service.clone(),
|
||||||
ai_provider.clone(),
|
ai_provider.clone(),
|
||||||
);
|
);
|
||||||
let action_confirmation_service = ActionConfirmationService::new(
|
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(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
|
|
|
||||||
42
src/tests.rs
42
src/tests.rs
|
|
@ -137,9 +137,12 @@ mod tests {
|
||||||
fn test_confirm_action_response_serialization() {
|
fn test_confirm_action_response_serialization() {
|
||||||
let response = ConfirmActionResponse {
|
let response = ConfirmActionResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
delegated: true,
|
||||||
message: "Created".to_string(),
|
message: "Created".to_string(),
|
||||||
action: "create_job_draft".to_string(),
|
action: "create_job_draft".to_string(),
|
||||||
|
backend_handler: Some("jobs.create_draft".to_string()),
|
||||||
record_id: Some("456".to_string()),
|
record_id: Some("456".to_string()),
|
||||||
|
fields: None,
|
||||||
ui_events: Some(vec![UiEvent {
|
ui_events: Some(vec![UiEvent {
|
||||||
event_type: "refresh_data".to_string(),
|
event_type: "refresh_data".to_string(),
|
||||||
target: "jobs_list".to_string(),
|
target: "jobs_list".to_string(),
|
||||||
|
|
@ -157,9 +160,12 @@ mod tests {
|
||||||
fn test_confirm_action_response_skips_none_fields() {
|
fn test_confirm_action_response_skips_none_fields() {
|
||||||
let response = ConfirmActionResponse {
|
let response = ConfirmActionResponse {
|
||||||
success: false,
|
success: false,
|
||||||
|
delegated: false,
|
||||||
message: "Cancelled".to_string(),
|
message: "Cancelled".to_string(),
|
||||||
action: "test".to_string(),
|
action: "test".to_string(),
|
||||||
|
backend_handler: None,
|
||||||
record_id: None,
|
record_id: None,
|
||||||
|
fields: None,
|
||||||
ui_events: None,
|
ui_events: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&response).unwrap();
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
|
@ -195,7 +201,13 @@ mod tests {
|
||||||
intent: "job_description_generation".to_string(),
|
intent: "job_description_generation".to_string(),
|
||||||
reply: "Generated".to_string(),
|
reply: "Generated".to_string(),
|
||||||
data: serde_json::json!({"title": "Engineer"}),
|
data: serde_json::json!({"title": "Engineer"}),
|
||||||
|
status: "completed".to_string(),
|
||||||
conversation_id: Some("conv-123".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,
|
ui_events: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&response).unwrap();
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
|
@ -209,7 +221,13 @@ mod tests {
|
||||||
intent: "form_filling".to_string(),
|
intent: "form_filling".to_string(),
|
||||||
reply: "Form filled".to_string(),
|
reply: "Form filled".to_string(),
|
||||||
data: serde_json::json!({}),
|
data: serde_json::json!({}),
|
||||||
|
status: "completed".to_string(),
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
|
confidence: Some(0.8),
|
||||||
|
fields: None,
|
||||||
|
missing_fields: None,
|
||||||
|
requires_confirmation: Some(false),
|
||||||
|
suggested_action: None,
|
||||||
ui_events: Some(vec![UiEvent {
|
ui_events: Some(vec![UiEvent {
|
||||||
event_type: "fill_form".to_string(),
|
event_type: "fill_form".to_string(),
|
||||||
target: "test_form".to_string(),
|
target: "test_form".to_string(),
|
||||||
|
|
@ -437,7 +455,13 @@ mod tests {
|
||||||
{"title": "Article 2", "url": "https://example.com/2"}
|
{"title": "Article 2", "url": "https://example.com/2"}
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
status: "completed".to_string(),
|
||||||
conversation_id: Some("conv-789".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,
|
ui_events: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&response).unwrap();
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
|
@ -521,7 +545,13 @@ mod tests {
|
||||||
intent: "test_intent".to_string(),
|
intent: "test_intent".to_string(),
|
||||||
reply: "Test reply".to_string(),
|
reply: "Test reply".to_string(),
|
||||||
data: serde_json::json!({"key": "value"}),
|
data: serde_json::json!({"key": "value"}),
|
||||||
|
status: "completed".to_string(),
|
||||||
conversation_id: Some("conv-123".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,
|
ui_events: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&response).unwrap();
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
|
@ -549,9 +579,12 @@ mod tests {
|
||||||
fn test_confirm_action_response_success() {
|
fn test_confirm_action_response_success() {
|
||||||
let response = ConfirmActionResponse {
|
let response = ConfirmActionResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
delegated: true,
|
||||||
message: "Action completed".to_string(),
|
message: "Action completed".to_string(),
|
||||||
action: "create_job_draft".to_string(),
|
action: "create_job_draft".to_string(),
|
||||||
|
backend_handler: Some("jobs.create_draft".to_string()),
|
||||||
record_id: Some("789".to_string()),
|
record_id: Some("789".to_string()),
|
||||||
|
fields: None,
|
||||||
ui_events: None,
|
ui_events: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&response).unwrap();
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
|
@ -563,9 +596,12 @@ mod tests {
|
||||||
fn test_confirm_action_response_with_ui_events() {
|
fn test_confirm_action_response_with_ui_events() {
|
||||||
let response = ConfirmActionResponse {
|
let response = ConfirmActionResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
delegated: true,
|
||||||
message: "Created".to_string(),
|
message: "Created".to_string(),
|
||||||
action: "test".to_string(),
|
action: "test".to_string(),
|
||||||
|
backend_handler: Some("test.handler".to_string()),
|
||||||
record_id: None,
|
record_id: None,
|
||||||
|
fields: None,
|
||||||
ui_events: Some(vec![UiEvent {
|
ui_events: Some(vec![UiEvent {
|
||||||
event_type: "show_confirmation".to_string(),
|
event_type: "show_confirmation".to_string(),
|
||||||
target: "modal".to_string(),
|
target: "modal".to_string(),
|
||||||
|
|
@ -584,6 +620,9 @@ mod tests {
|
||||||
message: "Hello AI".to_string(),
|
message: "Hello AI".to_string(),
|
||||||
user_id: Some("user-123".to_string()),
|
user_id: Some("user-123".to_string()),
|
||||||
conversation_id: Some("conv-456".to_string()),
|
conversation_id: Some("conv-456".to_string()),
|
||||||
|
role: None,
|
||||||
|
product: None,
|
||||||
|
context: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&request).unwrap();
|
let json = serde_json::to_string(&request).unwrap();
|
||||||
let parsed: ChatMessageRequest = serde_json::from_str(&json).unwrap();
|
let parsed: ChatMessageRequest = serde_json::from_str(&json).unwrap();
|
||||||
|
|
@ -597,6 +636,9 @@ mod tests {
|
||||||
message: "Test".to_string(),
|
message: "Test".to_string(),
|
||||||
user_id: None,
|
user_id: None,
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
|
role: None,
|
||||||
|
product: None,
|
||||||
|
context: None,
|
||||||
};
|
};
|
||||||
assert!(request.conversation_id.is_none());
|
assert!(request.conversation_id.is_none());
|
||||||
assert!(request.user_id.is_none());
|
assert!(request.user_id.is_none());
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue