feat(ai): cover letter inline generation + draft_content field on AskAshResponse
All checks were successful
build-and-release / build (photographers) (push) Successful in 7s
build-and-release / build (makeup-artists) (push) Successful in 12s
build-and-release / build (social-media-managers) (push) Successful in 8s
build-and-release / build (tutors) (push) Successful in 8s
build-and-release / build (ugc-content-creators) (push) Successful in 8s
build-and-release / build (video-editors) (push) Successful in 8s
backend-integration-tests / ai-credits (push) Successful in 13s
build-and-release / build (users) (push) Successful in 3m55s
build-and-release / build (developers) (push) Successful in 13s
build-and-release / build (customers) (push) Successful in 18s
build-and-release / build (employees) (push) Successful in 17s
build-and-release / build (companies) (push) Successful in 21s
build-and-release / build (cron) (push) Successful in 20s
build-and-release / build (catering-services) (push) Successful in 24s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (gateway) (push) Successful in 7s
build-and-release / build (job-seekers) (push) Successful in 7s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (payments) (push) Successful in 8s
build-and-release / build (jobs) (push) Successful in 11s
All checks were successful
build-and-release / build (photographers) (push) Successful in 7s
build-and-release / build (makeup-artists) (push) Successful in 12s
build-and-release / build (social-media-managers) (push) Successful in 8s
build-and-release / build (tutors) (push) Successful in 8s
build-and-release / build (ugc-content-creators) (push) Successful in 8s
build-and-release / build (video-editors) (push) Successful in 8s
backend-integration-tests / ai-credits (push) Successful in 13s
build-and-release / build (users) (push) Successful in 3m55s
build-and-release / build (developers) (push) Successful in 13s
build-and-release / build (customers) (push) Successful in 18s
build-and-release / build (employees) (push) Successful in 17s
build-and-release / build (companies) (push) Successful in 21s
build-and-release / build (cron) (push) Successful in 20s
build-and-release / build (catering-services) (push) Successful in 24s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (gateway) (push) Successful in 7s
build-and-release / build (job-seekers) (push) Successful in 7s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (payments) (push) Successful in 8s
build-and-release / build (jobs) (push) Successful in 11s
AskAshResponse now carries draft_content: Option<String> — a clean raw generated text separate from the prose wrapper in message. Used by: - profile_draft: the improved summary text (for Save to Profile) - cover_letter_draft: the letter body (for clipboard copy) When Intent::CoverLetter is detected in ask_ash: - Fetches job_seeker_profiles (full_name, summary, experience, skills) - Calls LLM with cover_letter_generate feature key (charges credits) - Returns action_type: 'cover_letter_draft', suggested_action: 'open_cover_letter' - draft_content holds the raw letter body; message wraps it in --- delimiters - If no profile found: returns navigation response to open_cover_letter - LLM failure falls through to generic path Profile draft updated to populate draft_content: Some(improved) so the frontend no longer needs regex parsing to extract the clean text. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd7e5bdc05
commit
ad4ab0477a
1 changed files with 120 additions and 5 deletions
|
|
@ -2216,12 +2216,18 @@ pub struct AskAshResponse {
|
|||
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
|
||||
/// "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
|
||||
/// "profile_draft" — AI-improved profile summary; show Save to Profile button
|
||||
/// "cover_letter_draft"— AI-generated cover letter; show Copy + open tool buttons
|
||||
/// "navigation" — navigate to the relevant feature page
|
||||
pub action_type: Option<String>,
|
||||
/// Raw generated content (profile draft text, cover letter body, etc.) separate from
|
||||
/// the display message. The frontend uses this for clipboard copy and save actions
|
||||
/// without needing to parse the prose wrapper out of `message`.
|
||||
pub draft_content: Option<String>,
|
||||
pub remaining_credits: Option<i32>,
|
||||
pub remaining_daily_actions: Option<i32>,
|
||||
}
|
||||
|
|
@ -2358,6 +2364,7 @@ async fn ai_chat_ask(
|
|||
status: Some("usage_summary".to_string()),
|
||||
suggested_action: Some("show_usage_modal".to_string()),
|
||||
action_type: Some("usage_info".to_string()),
|
||||
draft_content: None,
|
||||
remaining_credits: Some(remaining_credits),
|
||||
remaining_daily_actions: Some(remaining_daily_actions),
|
||||
}),
|
||||
|
|
@ -2384,6 +2391,7 @@ async fn ai_chat_ask(
|
|||
status: Some("kb_results".to_string()),
|
||||
suggested_action: Some("open_help_search".to_string()),
|
||||
action_type: Some("kb_results".to_string()),
|
||||
draft_content: None,
|
||||
remaining_credits: None,
|
||||
remaining_daily_actions: None,
|
||||
}),
|
||||
|
|
@ -2419,6 +2427,7 @@ async fn ai_chat_ask(
|
|||
status: Some("ticket_created".to_string()),
|
||||
suggested_action: Some("open_support_ticket".to_string()),
|
||||
action_type: Some("ticket_created".to_string()),
|
||||
draft_content: None,
|
||||
remaining_credits: None,
|
||||
remaining_daily_actions: None,
|
||||
}),
|
||||
|
|
@ -2482,6 +2491,7 @@ async fn ai_chat_ask(
|
|||
status: Some("profile_draft".to_string()),
|
||||
suggested_action: Some("save_profile".to_string()),
|
||||
action_type: Some("profile_draft".to_string()),
|
||||
draft_content: Some(improved),
|
||||
remaining_credits: Some(r.remaining_credits),
|
||||
remaining_daily_actions: Some(r.remaining_daily_actions),
|
||||
}),
|
||||
|
|
@ -2513,6 +2523,110 @@ async fn ai_chat_ask(
|
|||
status: Some("navigation".to_string()),
|
||||
suggested_action: Some("open_resume_tailor".to_string()),
|
||||
action_type: Some("navigation".to_string()),
|
||||
draft_content: None,
|
||||
remaining_credits: None,
|
||||
remaining_daily_actions: None,
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cover letter inline generation (Intent::CoverLetter) ─────────────────
|
||||
// Generates a general professional cover letter from the user's profile so
|
||||
// they have a strong starting point they can copy immediately or open the
|
||||
// dedicated Cover Letter Tool to tailor it to a specific job.
|
||||
if matches!(routed.intent, phase3::Intent::CoverLetter) {
|
||||
let seeker: Option<(String, Option<String>, i32, Vec<String>)> = sqlx::query_as(
|
||||
"SELECT full_name, summary, experience_years, skills \
|
||||
FROM job_seeker_profiles WHERE user_id = $1"
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|r| r);
|
||||
|
||||
if let Some((full_name, summary, experience, skills)) = seeker {
|
||||
let skills_str = skills.join(", ");
|
||||
let cover_prompt = format!(
|
||||
"You are a professional cover letter writer. \
|
||||
Write a compelling, concise cover letter for a job application. \
|
||||
IMPORTANT: Do NOT include phone number, email, or any contact information — \
|
||||
candidates keep their contacts private on this platform. \
|
||||
Write 3 short paragraphs (opening, body, closing). Under 200 words total. \
|
||||
Return ONLY the cover letter text with no heading, date, or address block.\n\n\
|
||||
Candidate: {full_name}\n\
|
||||
Experience: {experience} years\n\
|
||||
Summary: {}\n\
|
||||
Skills: {skills_str}",
|
||||
summary.as_deref().unwrap_or("Experienced professional")
|
||||
);
|
||||
|
||||
let result = orchestrator::call_feature(
|
||||
&state,
|
||||
&auth,
|
||||
"cover_letter_generate",
|
||||
None,
|
||||
&cover_prompt,
|
||||
body.model.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(r) => {
|
||||
let letter = r.text.trim().to_string();
|
||||
let msg = format!(
|
||||
"Here's a cover letter draft based on your profile. Copy it and tailor it to the specific role, or open the Cover Letter Tool to generate one matched to a job posting.\n\n---\n{letter}\n---"
|
||||
);
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(AskAshResponse {
|
||||
message: msg,
|
||||
persona: persona.map(|p| p.as_str().to_string()),
|
||||
pillar: pillar.map(|p| p.as_str().to_string()),
|
||||
intent,
|
||||
confidence,
|
||||
conversation_id,
|
||||
kb_matches,
|
||||
ticket: None,
|
||||
ollama_used: true,
|
||||
status: Some("cover_letter_draft".to_string()),
|
||||
suggested_action: Some("open_cover_letter".to_string()),
|
||||
action_type: Some("cover_letter_draft".to_string()),
|
||||
draft_content: Some(letter),
|
||||
remaining_credits: Some(r.remaining_credits),
|
||||
remaining_daily_actions: Some(r.remaining_daily_actions),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
if matches!(e, orchestrator::AiCallError::Plan(_) | orchestrator::AiCallError::Credit(_)) {
|
||||
return e.into_response();
|
||||
}
|
||||
tracing::warn!("cover_letter inline generation failed, falling through: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No job seeker profile — navigate to the tool
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(AskAshResponse {
|
||||
message: "I couldn't find your job seeker profile. Set up your profile first so I can generate a tailored cover letter for you!".to_string(),
|
||||
persona: persona.map(|p| p.as_str().to_string()),
|
||||
pillar: pillar.map(|p| p.as_str().to_string()),
|
||||
intent,
|
||||
confidence,
|
||||
conversation_id,
|
||||
kb_matches,
|
||||
ticket: None,
|
||||
ollama_used: false,
|
||||
status: Some("navigation".to_string()),
|
||||
suggested_action: Some("open_cover_letter".to_string()),
|
||||
action_type: Some("navigation".to_string()),
|
||||
draft_content: None,
|
||||
remaining_credits: None,
|
||||
remaining_daily_actions: None,
|
||||
}),
|
||||
|
|
@ -2656,6 +2770,7 @@ Assistant:");
|
|||
status: Some("answered".to_string()),
|
||||
suggested_action: Some(routed.suggested_action.to_string()),
|
||||
action_type: Some(action_type.to_string()),
|
||||
draft_content: None,
|
||||
remaining_credits,
|
||||
remaining_daily_actions,
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue