nxtgauge-ai-assistant/src/services/action_confirmation.rs
Tracewebstudio Dev 4505d8987e feat: add Ask Ash AI assistant implementation
- Add LiteLLM provider with LLM_PROVIDER env var support
- Add Fake LLM provider for testing
- Add action registry with 17 AI actions
- Add permission checker with role verification
- Add UI events in chat responses (fill_form, open_preview, etc)
- Add versioned prompt files system
- Add confirm action endpoint
- Add 80 unit tests

New endpoints:
- POST /api/ai/actions/confirm

New files:
- src/handlers/actions.rs
- src/handlers/confirm_action.rs
- src/services/action_confirmation.rs
- src/prompts.rs
- src/tests.rs
- prompts/v1/*.txt
2026-06-14 20:28:17 +02:00

263 lines
8.8 KiB
Rust

use std::sync::Arc;
use reqwest::Client;
use crate::{
actions::get_action,
error::AppError,
handlers::actions::{ConfirmActionRequest, ConfirmActionResponse, UiEvent},
};
#[derive(Clone)]
pub struct ActionConfirmationService {
http_client: Client,
jobs_service_url: String,
users_service_url: String,
customers_service_url: String,
}
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 async fn confirm_action(
&self,
request: ConfirmActionRequest,
user_id: &str,
) -> Result<ConfirmActionResponse, AppError> {
if !request.confirmed {
return Ok(ConfirmActionResponse {
success: false,
message: "Action cancelled by user.".to_string(),
action: request.action.clone(),
record_id: 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("");
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)),
}]),
})
}
}