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();
|
.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 system_prompt = build_persona_pillar_system_prompt(persona, pillar);
|
||||||
let mut user_block = String::new();
|
let mut user_block = String::new();
|
||||||
if let Some(p) = persona {
|
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 => {
|
other => {
|
||||||
tracing::warn!("ai_chat_confirm: unknown action '{}'", other);
|
tracing::warn!("ai_chat_confirm: unknown action '{}'", other);
|
||||||
(
|
(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue