feat(ai): profile improvement draft + save_profile confirm action
Some checks failed
build-and-release / build (companies) (push) Failing after 10s
build-and-release / build (developers) (push) Failing after 8s
build-and-release / build (cron) (push) Successful in 12s
build-and-release / build (customers) (push) Successful in 17s
build-and-release / build (catering-services) (push) Successful in 21s
build-and-release / build (gateway) (push) Successful in 5s
build-and-release / build (employees) (push) Successful in 16s
build-and-release / build (fitness-trainers) (push) Successful in 16s
build-and-release / build (job-seekers) (push) Successful in 9s
build-and-release / build (graphic-designers) (push) Successful in 12s
build-and-release / build (jobs) (push) Successful in 12s
build-and-release / build (makeup-artists) (push) Successful in 7s
build-and-release / build (payments) (push) Successful in 8s
build-and-release / build (photographers) (push) Successful in 7s
build-and-release / build (ugc-content-creators) (push) Successful in 5s
build-and-release / build (social-media-managers) (push) Successful in 10s
build-and-release / build (video-editors) (push) Successful in 8s
build-and-release / build (tutors) (push) Successful in 12s
backend-integration-tests / ai-credits (push) Successful in 6s
build-and-release / build (users) (push) Successful in 2m59s
Some checks failed
build-and-release / build (companies) (push) Failing after 10s
build-and-release / build (developers) (push) Failing after 8s
build-and-release / build (cron) (push) Successful in 12s
build-and-release / build (customers) (push) Successful in 17s
build-and-release / build (catering-services) (push) Successful in 21s
build-and-release / build (gateway) (push) Successful in 5s
build-and-release / build (employees) (push) Successful in 16s
build-and-release / build (fitness-trainers) (push) Successful in 16s
build-and-release / build (job-seekers) (push) Successful in 9s
build-and-release / build (graphic-designers) (push) Successful in 12s
build-and-release / build (jobs) (push) Successful in 12s
build-and-release / build (makeup-artists) (push) Successful in 7s
build-and-release / build (payments) (push) Successful in 8s
build-and-release / build (photographers) (push) Successful in 7s
build-and-release / build (ugc-content-creators) (push) Successful in 5s
build-and-release / build (social-media-managers) (push) Successful in 10s
build-and-release / build (video-editors) (push) Successful in 8s
build-and-release / build (tutors) (push) Successful in 12s
backend-integration-tests / ai-credits (push) Successful in 6s
build-and-release / build (users) (push) Successful in 2m59s
When a job seeker asks Ask Ash to improve their resume/profile/summary: - Fetches current job_seeker_profiles.summary from DB - Calls LLM (via orchestrator, charges profile_improve feature credits) - Returns action_type: 'profile_draft' with the improved text - If no summary exists yet, returns a navigation response to profile page ai_chat_confirm now handles action='save_profile': - Takes draft_text from fields - UPDATEs job_seeker_profiles.summary for the authenticated user - Returns 404 if no profile row found (not yet onboarded) - Returns helpful error messages on failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
38981ffe70
commit
cd7e5bdc05
1 changed files with 150 additions and 0 deletions
|
|
@ -2426,6 +2426,101 @@ async fn ai_chat_ask(
|
|||
.into_response();
|
||||
}
|
||||
|
||||
// ── Profile improvement early return (Intent::Resume) ─────────────────────
|
||||
// When the user asks to improve/tailor their resume/profile, fetch their
|
||||
// current summary, generate an improved version via LLM, and return it as a
|
||||
// draft the user can review and save with a single button click.
|
||||
if matches!(routed.intent, phase3::Intent::Resume) {
|
||||
let current_summary: Option<String> = sqlx::query_scalar(
|
||||
"SELECT summary FROM job_seeker_profiles WHERE user_id = $1"
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some(summary) = current_summary.filter(|s| !s.is_empty()) {
|
||||
let improve_prompt = format!(
|
||||
"You are a professional career coach and resume writer. \
|
||||
Improve the following professional summary for a job seeker's profile. \
|
||||
Make it more compelling, achievement-focused, and engaging — \
|
||||
keep it 3–4 sentences and under 100 words. \
|
||||
Return ONLY the improved summary text, no preamble or explanation.\n\n\
|
||||
Current summary:\n{summary}"
|
||||
);
|
||||
|
||||
let draft_result = orchestrator::call_feature(
|
||||
&state,
|
||||
&auth,
|
||||
"profile_improve",
|
||||
None,
|
||||
&improve_prompt,
|
||||
body.model.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
match draft_result {
|
||||
Ok(r) => {
|
||||
let improved = r.text.trim().to_string();
|
||||
let msg = format!(
|
||||
"Here's an improved version of your profile summary:\n\n\"{improved}\"\n\nClick **Save to Profile** to apply it, or ask me to adjust the tone or focus."
|
||||
);
|
||||
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("profile_draft".to_string()),
|
||||
suggested_action: Some("save_profile".to_string()),
|
||||
action_type: Some("profile_draft".to_string()),
|
||||
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();
|
||||
}
|
||||
// LLM failed — fall through to generic path which will give a navigation response
|
||||
tracing::warn!("profile_improve LLM call failed, falling through: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No profile summary yet — guide them to set one up first
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(AskAshResponse {
|
||||
message: "I couldn't find a profile summary to improve. Head to your profile page to add one first, then I can help you make it shine!".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_resume_tailor".to_string()),
|
||||
action_type: Some("navigation".to_string()),
|
||||
remaining_credits: None,
|
||||
remaining_daily_actions: None,
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let system_prompt = build_persona_pillar_system_prompt(persona, pillar);
|
||||
let mut user_block = String::new();
|
||||
if let Some(p) = persona {
|
||||
|
|
@ -2648,6 +2743,61 @@ async fn ai_chat_confirm(
|
|||
}
|
||||
}
|
||||
|
||||
"save_profile" => {
|
||||
let fields = body.fields.unwrap_or_default();
|
||||
let draft_text = fields
|
||||
.get("draft_text")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if draft_text.is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "success": false, "error": "draft_text is required" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let result = sqlx::query(
|
||||
"UPDATE job_seeker_profiles SET summary = $2, updated_at = NOW() WHERE user_id = $1"
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(&draft_text)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(res) if res.rows_affected() > 0 => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"success": true,
|
||||
"action": "save_profile",
|
||||
"message": "Your profile summary has been updated! It will be visible to companies that view your profile."
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Ok(_) => (
|
||||
// UPDATE matched 0 rows — no job_seeker_profiles row for this user
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({
|
||||
"success": false,
|
||||
"error": "No job seeker profile found. Please create your profile first."
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("chat_confirm save_profile failed for user {}: {}", user_id, e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "success": false, "error": "Failed to save profile summary" })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
other => {
|
||||
tracing::warn!("ai_chat_confirm: unknown action '{}'", other);
|
||||
(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue