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

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:
Tracewebstudio Dev 2026-08-14 18:43:24 +02:00
parent 38981ffe70
commit cd7e5bdc05

View file

@ -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 34 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);
( (