feat(ai): add action_type to AskAshResponse + POST /api/ai/chat/confirm
All checks were successful
build-and-release / build (catering-services) (push) Successful in 6s
build-and-release / build (companies) (push) Successful in 9s
build-and-release / build (developers) (push) Successful in 5s
build-and-release / build (cron) (push) Successful in 12s
build-and-release / build (customers) (push) Successful in 11s
build-and-release / build (gateway) (push) Successful in 7s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (graphic-designers) (push) Successful in 5s
build-and-release / build (employees) (push) Successful in 10s
build-and-release / build (job-seekers) (push) Successful in 5s
build-and-release / build (makeup-artists) (push) Successful in 6s
build-and-release / build (jobs) (push) Successful in 9s
build-and-release / build (payments) (push) Successful in 5s
build-and-release / build (photographers) (push) Successful in 10s
build-and-release / build (social-media-managers) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 6s
build-and-release / build (tutors) (push) Successful in 10s
build-and-release / build (video-editors) (push) Successful in 9s
backend-integration-tests / ai-credits (push) Successful in 9s
build-and-release / build (users) (push) Successful in 3m14s
All checks were successful
build-and-release / build (catering-services) (push) Successful in 6s
build-and-release / build (companies) (push) Successful in 9s
build-and-release / build (developers) (push) Successful in 5s
build-and-release / build (cron) (push) Successful in 12s
build-and-release / build (customers) (push) Successful in 11s
build-and-release / build (gateway) (push) Successful in 7s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (graphic-designers) (push) Successful in 5s
build-and-release / build (employees) (push) Successful in 10s
build-and-release / build (job-seekers) (push) Successful in 5s
build-and-release / build (makeup-artists) (push) Successful in 6s
build-and-release / build (jobs) (push) Successful in 9s
build-and-release / build (payments) (push) Successful in 5s
build-and-release / build (photographers) (push) Successful in 10s
build-and-release / build (social-media-managers) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 6s
build-and-release / build (tutors) (push) Successful in 10s
build-and-release / build (video-editors) (push) Successful in 9s
backend-integration-tests / ai-credits (push) Successful in 9s
build-and-release / build (users) (push) Successful in 3m14s
AskAshResponse gains action_type: Option<String> so the frontend knows what kind of UI to render for each response: ticket_created — ticket was auto-created inline ticket_pending — user wants a ticket but none was created (show confirm button) kb_results — KB articles found usage_info — usage/credits summary navigation — navigate to the relevant feature page POST /api/ai/chat/confirm (new): executes AI-suggested actions the user confirms in the chat widget. action = 'create_ticket' creates a support ticket from the conversation text and returns the ticket id + subject. Additional actions (profile save, contact request) can be added here. Wired into ai_router() at /chat/confirm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cc2dab112b
commit
38981ffe70
1 changed files with 109 additions and 0 deletions
|
|
@ -2215,6 +2215,13 @@ pub struct AskAshResponse {
|
|||
pub ollama_used: bool,
|
||||
pub status: Option<String>,
|
||||
pub suggested_action: Option<String>,
|
||||
/// Semantic category of what the frontend should do next:
|
||||
/// "ticket_created" — ticket was auto-created inline
|
||||
/// "ticket_pending" — user wants a ticket but none was created yet (show confirm button)
|
||||
/// "kb_results" — KB articles were found (show link to help center)
|
||||
/// "usage_info" — usage/credits summary
|
||||
/// "navigation" — navigate to the relevant feature page
|
||||
pub action_type: Option<String>,
|
||||
pub remaining_credits: Option<i32>,
|
||||
pub remaining_daily_actions: Option<i32>,
|
||||
}
|
||||
|
|
@ -2350,6 +2357,7 @@ async fn ai_chat_ask(
|
|||
ollama_used: false,
|
||||
status: Some("usage_summary".to_string()),
|
||||
suggested_action: Some("show_usage_modal".to_string()),
|
||||
action_type: Some("usage_info".to_string()),
|
||||
remaining_credits: Some(remaining_credits),
|
||||
remaining_daily_actions: Some(remaining_daily_actions),
|
||||
}),
|
||||
|
|
@ -2375,6 +2383,7 @@ async fn ai_chat_ask(
|
|||
ollama_used: false,
|
||||
status: Some("kb_results".to_string()),
|
||||
suggested_action: Some("open_help_search".to_string()),
|
||||
action_type: Some("kb_results".to_string()),
|
||||
remaining_credits: None,
|
||||
remaining_daily_actions: None,
|
||||
}),
|
||||
|
|
@ -2409,6 +2418,7 @@ async fn ai_chat_ask(
|
|||
ollama_used: false,
|
||||
status: Some("ticket_created".to_string()),
|
||||
suggested_action: Some("open_support_ticket".to_string()),
|
||||
action_type: Some("ticket_created".to_string()),
|
||||
remaining_credits: None,
|
||||
remaining_daily_actions: None,
|
||||
}),
|
||||
|
|
@ -2530,6 +2540,12 @@ Assistant:");
|
|||
});
|
||||
}
|
||||
|
||||
// Classify action_type based on intent so the frontend knows what to render.
|
||||
let action_type = match routed.intent {
|
||||
phase3::Intent::TicketCreation | phase3::Intent::TechnicalSupport => "ticket_pending",
|
||||
_ => "navigation",
|
||||
};
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(AskAshResponse {
|
||||
|
|
@ -2544,6 +2560,7 @@ Assistant:");
|
|||
ollama_used,
|
||||
status: Some("answered".to_string()),
|
||||
suggested_action: Some(routed.suggested_action.to_string()),
|
||||
action_type: Some(action_type.to_string()),
|
||||
remaining_credits,
|
||||
remaining_daily_actions,
|
||||
}),
|
||||
|
|
@ -2551,6 +2568,97 @@ Assistant:");
|
|||
.into_response()
|
||||
}
|
||||
|
||||
// ── POST /api/ai/chat/confirm ─────────────────────────────────────────────────
|
||||
// Executes an AI-suggested action that the user confirmed in the chat widget.
|
||||
// Currently supports action = "create_ticket". Further actions can be added as
|
||||
// the confirm flow matures; unknown actions return 400.
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChatConfirmRequest {
|
||||
action: String,
|
||||
conversation_id: Option<String>,
|
||||
fields: Option<serde_json::Value>,
|
||||
user_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
async fn ai_chat_confirm(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<ChatConfirmRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = body
|
||||
.user_id
|
||||
.filter(|u| *u != Uuid::nil())
|
||||
.unwrap_or(auth.user_id);
|
||||
|
||||
match body.action.as_str() {
|
||||
"create_ticket" => {
|
||||
let fields = body.fields.unwrap_or_default();
|
||||
let subject = fields
|
||||
.get("subject")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Support Request via AI Chat");
|
||||
let description = fields.get("description").and_then(|v| v.as_str());
|
||||
let category = fields
|
||||
.get("category")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("ai_assisted");
|
||||
let priority = fields
|
||||
.get("priority")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("medium");
|
||||
|
||||
let result = sqlx::query_as::<_, TicketRow>(
|
||||
r#"
|
||||
INSERT INTO support_tickets (user_id, subject, description, category, priority, status)
|
||||
VALUES ($1, $2, $3, $4, $5, 'new')
|
||||
RETURNING id, reference_number, subject, description, category, priority, status,
|
||||
requester_name, requester_email, assigned_to, created_at, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(subject)
|
||||
.bind(description)
|
||||
.bind(category)
|
||||
.bind(priority)
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(t) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true,
|
||||
"action": "create_ticket",
|
||||
"ticket_id": t.id,
|
||||
"reference_number": t.reference_number,
|
||||
"subject": t.subject,
|
||||
"message": format!("Support ticket #{} created: {}. Our team will be in touch.", t.reference_number, t.subject)
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("chat_confirm ticket creation failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "success": false, "error": "Failed to create ticket" })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
other => {
|
||||
tracing::warn!("ai_chat_confirm: unknown action '{}'", other);
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "success": false, "error": format!("Unknown action: {other}") })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── GET /api/ai/suggestions ───────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
|
|
@ -4259,6 +4367,7 @@ pub fn ai_router() -> Router<AppState> {
|
|||
Router::new()
|
||||
.route("/chat/message", post(ai_chat_message))
|
||||
.route("/chat/ask", post(ai_chat_ask))
|
||||
.route("/chat/confirm", post(ai_chat_confirm))
|
||||
.route("/help/ask", post(ai_help_ask))
|
||||
.route("/suggestions", get(ai_suggestions))
|
||||
.route("/context", post(ai_save_context))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue