_private/qwestly-docs/Features/qwestly-agent/profile-edit-tool.md
Table of Contents
Profile Edit Tools — Field Taxonomy & Implementation Plan
Goal: Add
get_profileand a family ofedit_profile_*tools to the Qwestly Agent so users can view and update their structured profile data through the chat interface.Status: Implemented. This doc is the single source of truth for field editability. Last updated 2026-06-01 — merged from two docs;
name/
1. Data Sources Inventory
The candidate app stores profile data across multiple *_enhanced collections in MongoDB
(candidate_portal database). The agent reads/writes these directly via Motor (async MongoDB driver).
Each has a Mongoose model in candidate/src/models/ and a type definition in candidate/src/types/.
| Collection | Model File | Type File | Purpose |
|---|---|---|---|
candidates_enhanced |
Candidate.ts |
candidate.d.ts |
Core candidate record: contact, summaries, engagement metrics, state |
employment_stints_enhanced |
EmploymentStint.ts |
employment-stint.d.ts |
Work history rows (company, title, dates, details) |
education_items_enhanced |
Education.ts |
education.d.ts |
Education entries (institution, degree, field, year) |
achievements_enhanced |
Achievement.ts |
achievement.d.ts |
STAR-format achievements, each linked to an employment stint |
challenges_enhanced |
Challenge.ts |
challenge.d.ts |
Career challenges and how they were overcome |
preferences_enhanced |
Preference.ts |
preference.d.ts |
Job search preferences (titles, stages, comp, location, culture) |
competencies_enhanced |
Competency.ts |
competency.d.ts |
Lookup table — global competency catalog (not user data) |
competency_evidence_enhanced |
CompetencyEvidence.ts |
competency-evidence.d.ts |
Join table: achievement ↔ competency mappings (system-managed) |
candidate_summary_enhanced |
CandidateSummary.ts |
candidate-summary.d.ts |
Anonymized structured summaries for hire-side consumption |
v_candidate_profile_cache_enhanced |
CandidateProfileCache.ts |
candidate-profile-cache.d.ts |
Pre-assembled profile JSON (read-only cache, system-rebuilt) |
candidate_search_index_enhanced |
CandidateSearchIndex.ts |
candidate-search-index.d.ts |
Search-optimized denormalized index (system-rebuilt) |
qwestly_card_sections |
QwestlyCardSection.ts |
— | Card content (summary, experiences, competencies, preferences) |
first_interview_contexts |
FirstInterviewContext.ts |
first-interview-context.d.ts |
LLM-generated context prep for interviews |
Provision & Claim Flow Context
The admin provision page (candidate/src/app/admin/provision/[id]/) exposes these fields to recruiters at account creation:
Provision Contact Card (fields a recruiter sets):
name,email,linkedin_url
Provision Background Card (candidate-visible free-text + resume upload):
- Background/notes free-text (stored in knowledge base)
- Resume uploads (stored in
knowledge_base/knowledge_chunkvia RAG)
Provision Qwestly Card (LLM-generated, admin-editable):
- Card summary (
roles,achievements—qwestly_card_sections.type=summary) - Card experiences (
qwestly_card_sections.type=experiences) - Card competencies (
qwestly_card_sections.type=competencies)
Preference fields in the provision flow:
preferred_functions,preferred_levels,search_statusare marked "provision-only" — set by recruiters, not shown in candidate self-onboardinganonymized_for_search— admin-only toggle
The claim migration (claim-migration.ts) swaps user_id from provisional → Auth0 ID across 22 collections, proving which collections are user-owned.
Source of Truth
The existing get_user_profile tool reads from candidates_enhanced,
v_candidate_profile_cache_enhanced, employment_stints_enhanced, education_items_enhanced,
achievements_enhanced, challenges_enhanced, and preferences_enhanced.
2. Field-Level Edit Permissions (Ground Truth — from get_user_profile.py)
Legend
- ✅ User-Editable — Writable through agent edit tools
- 👁️ View-Only — Returned by
get_profile, but not writable - 🚫 Hidden — Not returned to user through agent tools
- ⚙️ System — Managed by internal processes, not profile editing
2.1 candidates_enhanced Fields
| Field | Classification | Rationale |
|---|---|---|
name |
👁️ View-Only | Synced from Auth0 on claim/login. Future: may become editable with Auth0 sync (see Asana task). |
email |
👁️ View-Only | Auth0-managed. Will remain read-only — users change email through Auth0 account settings. |
preferred_name |
✅ Editable | What the user wants to be called (used in VAPI calls) |
linkedin_user_name |
✅ Editable | LinkedIn handle. Typically set by OAuth, but can change |
linkedin_url |
✅ Editable | Full LinkedIn public URL |
phone |
✅ Editable | Contact phone number |
signup_method |
👁️ View-Only | How they joined (waitlist/direct/provision) — historical |
signup_code |
👁️ View-Only | Signup code used |
deleted |
🚫 Hidden | Account deletion flag — internal |
profile_status |
👁️ View-Only | draft/active/inactive/archived — lifecycle managed by onboarding flows and admin |
profile_state |
👁️ View-Only | Computed from card/interview state (no_card/card_no_interview/etc.) |
first_interviewed_at |
👁️ View-Only | Timestamp of first completed interview |
published_at |
👁️ View-Only | When profile was first published |
invite_card_dismissed_at |
🚫 Hidden | UI state tracking |
communication_style |
👁️ View-Only | LLM-inferred DISC style (director/socializer/relator/thinker). Editing would corrupt inference-based UX |
motivation_level |
👁️ View-Only | LLM-inferred career motivation. Same rationale |
last_profile_view_at |
🚫 Hidden | Internal analytics |
career_summary |
✅ Editable | "AI-generated, human-editable" per schema |
education_summary |
✅ Editable | Same |
competency_summary |
✅ Editable | Same |
high_level_background |
✅ Editable | Same |
current_project |
✅ Editable | Same |
resilience_summary |
✅ Editable | Same |
ambition_summary |
✅ Editable | Same |
alignment_summary |
✅ Editable | Same |
post_interview_summary |
✅ Editable | Same |
candidate_profile_summary |
✅ Editable | Short "about me" blurb for networking emails |
referral_quota_total |
🚫 Hidden | Admin-managed |
referral_quota_used |
🚫 Hidden | System-managed counter |
linkedin_basic_profile |
👁️ View-Only | Raw LinkedIn OAuth profile — read-only source data |
linkedin_verification |
👁️ View-Only | Verification status — system-managed flow |
interested_in_companies |
✅ Editable | Company preference list |
not_interested_companies |
✅ Editable | Exclusion list |
networking_opt_in_at |
👁️ View-Only | Managed through dedicated networking consent flow |
networking_skipped_at |
🚫 Hidden | UI state |
networking_opt_out_at |
👁️ View-Only | Managed through opt-out flow |
networking_target_opt_out_at |
👁️ View-Only | Managed through opt-out flow |
structural_data |
👁️ View-Only | Processing status — system-managed |
onboarding_progress |
👁️ View-Only | Progress tracker — managed by onboarding flow |
created_at / updated_at |
👁️ View-Only | System timestamps |
2.2 employment_stints_enhanced Fields
| Field | Classification | Rationale |
|---|---|---|
_id |
👁️ View-Only | MongoDB ID (needed for edit/delete targeting) |
user_id |
👁️ View-Only | FK |
company_id |
🚫 Hidden | FK to companies collection |
company_name_raw |
✅ Editable | The company name as entered by user |
title |
✅ Editable | Job title |
normalized_title |
👁️ View-Only | LLM-standardized title (e.g. "VP Engineering"). Display but don't let users edit |
start_date |
✅ Editable | Employment start |
end_date |
✅ Editable | Employment end (null = current) |
reports_to |
✅ Editable | Reporting line |
location |
✅ Editable | Work location |
is_manager |
✅ Editable | Management role flag |
team_size_managed |
✅ Editable | Direct reports count |
work_email_verification |
🚫 Hidden | Verification code lifecycle |
2.3 education_items_enhanced Fields
All fields are ✅ Editable:
institution, degree_type (enum validated), field_of_study, graduation_year, honors_md, gpa, relevant_coursework.
2.4 achievements_enhanced Fields
| Field | Classification | Rationale |
|---|---|---|
_id |
👁️ View-Only | MongoDB ID (needed for edit/delete targeting) |
user_id |
👁️ View-Only | FK |
stint_id |
👁️ View-Only | FK to employment stint — set at creation time via stint selection |
headline |
✅ Editable | Achievement headline |
situation_md |
✅ Editable | STAR: Situation |
task_md |
✅ Editable | STAR: Task |
action_md |
✅ Editable | STAR: Action |
result_md |
✅ Editable | STAR: Result |
metric_type |
✅ Editable | Enum: revenue, cost, users, efficiency, growth |
metric_value |
✅ Editable | Numeric value |
metric_unit |
✅ Editable | Unit ($, %, users, etc.) |
occurred_year |
✅ Editable | Year |
2.5 challenges_enhanced Fields
All fields are ✅ Editable:
title, context_md, approach_md, outcome_md, year.
2.6 preferences_enhanced Fields
| Field | Classification | Rationale |
|---|---|---|
ideal_titles |
✅ Editable | Target job titles |
preferred_stages |
✅ Editable | startup/growth/mature |
industries_avoid |
✅ Editable | Excluded industries |
work_modes |
✅ Editable | remote/hybrid/onsite |
relocation_willing |
✅ Editable | Boolean |
earliest_start_date |
✅ Editable | Availability |
target_comp_ranges_jsonb |
✅ Editable | Compensation expectations |
values_md |
✅ Editable | Work values narrative |
culture |
✅ Editable | Preferred culture |
location |
✅ Editable | Preferred locations |
advisory_fractional_interest |
✅ Editable | Boolean |
role_type |
✅ Editable | leadership/ic |
preferred_functions |
👁️ View-Only | Provision-only — set during account creation for recruiting |
preferred_levels |
👁️ View-Only | Provision-only |
search_status |
👁️ View-Only | active/passive/not_searching — admin-managed for recruiting |
anonymized_for_search |
🚫 Hidden | Admin-only toggle — not for user self-edit |
2.7 Collections NOT Exposed Through Profile Tools
| Collection | Reason |
|---|---|
competencies_enhanced |
Global lookup table — not user data |
competency_evidence_enhanced |
System-managed join table (achievement ↔ competency mappings) |
candidate_summary_enhanced |
Auto-generated anonymized summaries for hire-side. Users edit the source (stints, achievements, summaries) and the system regenerates this |
v_candidate_profile_cache_enhanced |
Read-only cache, rebuilt by the system. Users edit the source collections |
candidate_search_index_enhanced |
Denormalized search index, rebuilt by the system |
qwestly_card_sections |
Already managed by existing show_qwestly_card / edit_qwestly_card tools |
first_interview_contexts |
LLM-generated interview prep — not user-facing profile content |
knowledge_base / knowledge_chunk |
Managed through resume uploads, not profile editing |
leadership_quotes_enhanced |
Not yet implemented in profile tools (deferred) |
3. Summary of Edit Rules
✅ User-Editable (within agent edit tool scope)
- Contact:
preferred_name,linkedin_user_name,linkedin_url,phone - Summary fields: All 10 (
career_summary,education_summary,competency_summary,high_level_background,current_project,resilience_summary,ambition_summary,alignment_summary,post_interview_summary,candidate_profile_summary) - Company prefs:
interested_in_companies,not_interested_companies - Employment stints: Full CRUD (except
normalized_title;company_id/work_email_verificationhidden) - Education: Full CRUD
- Achievements: Full CRUD (except
stint_idwhich is set at creation; linked competency evidence cleaned up on delete) - Challenges: Full CRUD
- Preferences (writable subset):
ideal_titles,preferred_stages,industries_avoid,work_modes,relocation_willing,earliest_start_date,target_comp_ranges_jsonb,values_md,culture,location,advisory_fractional_interest,role_type
👁 Read-Only (visible to user, not editable through agent)
- Auth0-sourced:
name,email - LLM-inferred:
communication_style,motivation_level,normalized_title(on stints) - LinkedIn raw data:
linkedin_basic_profile - Profile lifecycle:
profile_status,profile_state,first_interviewed_at,published_at - Structural data status:
structural_data - Provision-only prefs:
preferred_functions,preferred_levels,search_status - Consent flows:
networking_opt_in_at,networking_opt_out_at,networking_target_opt_out_at
🚫 Hidden (not returned to user at all)
deleted,invite_card_dismissed_at,last_profile_view_atreferral_quota_total,referral_quota_usedlinkedin_verification,work_email_verification(on stints)signup_method,signup_codeonboarding_progressnetworking_skipped_atcompany_id(on stints — internal FK)anonymized_for_searchcreated_at,updated_at(shown as "Last updated" timestamp but not as editable fields)
4. Tool Architecture
Design decision: Family of small, focused tools (one per domain), following the existing
codebase convention (edit_qwestly_card, manage_qwestly_card_anonymity).
4.1 get_profile Tool
File: qwestly-agent/tools/get_user_profile.py
Returns the complete user profile organized by section:
contact—name,email(read-only),preferred_name,linkedin_user_name,linkedin_url,phonestatus—profile_status,profile_state,first_interviewed_at,published_at,communication_style,motivation_level(all read-only)summaries— all 10 summary fields (all editable)employment_stints— array with_id, all fields,normalized_titlemarked read-onlyeducation— array with_id+ all fields (all editable)achievements— array with_id,stint_id+ all fields (all editable)challenges— array with_id+ all fields (all editable)preferences— object with writable fields + read-only provision fields + hiddenanonymized_for_searchcompany_preferences—interested_in_companies,not_interested_companies(editable)
Read-only fields are tagged with __readonly metadata so the LLM knows which fields cannot be edited.
4.2 Edit Tools (8 tools)
| Tool | File | Writes To | Key Fields |
|---|---|---|---|
edit_profile_contact |
tools/edit_profile_contact.py |
candidates_enhanced |
preferred_name, linkedin_user_name, linkedin_url, phone |
edit_profile_summaries |
tools/edit_profile_summaries.py |
candidates_enhanced |
All 10 summary fields |
edit_employment_stints |
tools/edit_employment_stints.py |
employment_stints_enhanced |
Add/update/remove with action discriminator |
edit_education |
tools/edit_education.py |
education_items_enhanced |
Add/update/remove with action discriminator |
edit_achievements |
tools/edit_achievements.py |
achievements_enhanced + competency_evidence_enhanced |
Add/update/remove with cascade cleanup |
edit_challenges |
tools/edit_challenges.py |
challenges_enhanced |
Add/update/remove with action discriminator |
edit_preferences |
tools/edit_preferences.py |
preferences_enhanced |
Writable subset only; full-replace for lists |
edit_company_preferences |
tools/edit_company_preferences.py |
candidates_enhanced |
Full-replace for lists |
Upsert semantics: action="add" inserts a new document; action="update" requires _id from get_profile; action="remove" requires _id.
List field semantics: Full-replace — the LLM MUST call get_profile() first to see current values and merge client-side.
4.3 Orchestrator Prompt
The system prompt in agents/orchestrator.py describes all tools and instructs the LLM to:
- Always call
get_profile()before any edit tool - Confirm with the user before any
action="remove" - After editing employment, achievements, or summaries, offer to regenerate the Qwestly Card
5. Cache & Index Invalidation
When a user edits profile fields, two downstream caches become stale:
v_candidate_profile_cache_enhanced— Pre-assembled profile JSONcandidate_search_index_enhanced— Denormalized search index
Current approach: Each edit tool sets profile_dirty_at on candidates_enhanced after a successful write via lib/profile_dirty.py. This triggers downstream cache/index rebuild.
6. Safety & Guardrails
6.1 The LLM MUST call get_profile first
The tool descriptions require this. The LLM needs _id values for targeted updates.
6.2 Idempotency & User Scoping
All writes are filtered by user_id — the agent can only modify the authenticated user's own documents.
await db["employment_stints_enhanced"].update_one(
{"_id": ObjectId(stint_id), "user_id": ctx.deps.user_id},
{"$set": {...}}
)
6.3 Read-only field rejection
Tools explicitly reject edits to communication_style, motivation_level, normalized_title, preferred_functions, preferred_levels, search_status, anonymized_for_search, profile_status, profile_state, deleted, created_at, updated_at.
6.4 Enum validation on write
_VALID_DEGREE_TYPES = frozenset({"Associate's", "Bachelor's", "Master's", "PhD", "Doctorate", "Certificate", "High School"})
_VALID_METRIC_TYPES = frozenset({"revenue", "cost", "users", "efficiency", "growth", ""})
_VALID_STAGES = frozenset({"startup", "growth", "mature"})
_VALID_WORK_MODES = frozenset({"remote", "hybrid", "onsite"})
_VALID_ROLE_TYPES = frozenset({"leadership", "ic"})
6.5 Stint-achievement referential integrity
edit_employment_stints(action="remove") checks for linked achievements before deleting. If achievements exist, it returns linked IDs so the LLM can guide the user.
6.6 Achievement → stint linkage validation
When creating an achievement, stint_id must reference an employment stint belonging to the same user.
6.7 Dirty-write protection (soft)
Each edit tool reads the current document before writing and compares incoming values against current values. If current values differ (concurrent write), the tool still writes but includes a warning.
7. Implementation Status
| Step | File | Status |
|---|---|---|
| 1 | qwestly-agent/tools/get_user_profile.py |
✅ Rewrote — fixed user_id bug, added achievements/challenges, stringified _id, classified fields by editability |
| 2 | qwestly-agent/tools/edit_profile_contact.py |
✅ Created |
| 3 | qwestly-agent/tools/edit_profile_summaries.py |
✅ Created |
| 4 | qwestly-agent/tools/edit_employment_stints.py |
✅ Created |
| 5 | qwestly-agent/tools/edit_education.py |
✅ Created |
| 6 | qwestly-agent/tools/edit_achievements.py |
✅ Created |
| 7 | qwestly-agent/tools/edit_challenges.py |
✅ Created |
| 8 | qwestly-agent/tools/edit_preferences.py |
✅ Created |
| 9 | qwestly-agent/tools/edit_company_preferences.py |
✅ Created |
| 10 | qwestly-agent/agents/orchestrator.py |
✅ Updated — registered all 9 tools + updated system prompt |
| 11 | qwestly-agent/tests/test_tools.py |
✅ Added 23 test cases |
| 12 | lib/profile_dirty.py |
✅ Created — sets profile_dirty_at after every write |
8. Qwestly Card Staleness on Profile Edit
The agent does NOT auto-regenerate the card. The orchestrator prompt instructs the LLM to offer regeneration after any substantive edit:
After editing employment, achievements, or summaries, tell the user their Qwestly Card may be out of date and ask if they'd like you to regenerate it. Use
generate_qwestly_cardif they say yes.
9. Review Notes (2026-05-30)
Code Review Findings (Resolved)
-
Monolithic tool → split into 8 focused tools. The original design had a single
edit_profilewith ~45 parameters. This violated the existing codebase convention and would cause LLM routing degradation. -
clear_end_datesentinel bug — original design usedend_date=""as sentinel. Fixed withclear_end_date: boolparameter. -
Cascade order bug —
edit_achievements(action="remove")deleted competency evidence BEFORE the achievement. Fixed: check existence first, then delete achievement, then cascade. -
URL encoding —
edit_preferencessentauth0|...user_id unescaped. Fixed withurllib.parse.quote(). -
profile_dirty_at— set oncandidates_enhancedafter every write (add/update/remove) across all tools. -
Company prefs read-before-write — added to match pattern used by all other edit tools.
-
Dead constants removed —
_HIDDEN_CANDIDATE_FIELDS,_READ_ONLY_CANDIDATE_FIELDS,_READONLY_IF_PASSED,_READ_ONLY_PREFScleaned up.
Architecture Review Findings (Resolved)
- Single responsibility — Split into 8 domain-specific tools.
- Direct MongoDB writes vs. API proxy — Hybrid approach: direct writes for simple collections, API proxy for
preferences_enhanced. - Dirty-write protection — Soft detection via read-before-write comparison.
updated_fieldscomputation — Read-before-write + diff approach.
10. Open Questions
-
Should
namebecome editable? → Planned. Asana task created to makenameeditable with Auth0 sync. The agent would update bothcandidates_enhanced.nameand Auth0's user profile. See related Asana task. -
Should
emailbecome editable? → No. Will remain Auth0-managed. Users change email through Auth0 account settings. -
Should
linkedin_basic_profilebe editable? → No. Raw LinkedIn API data. Users edit their LinkedIn profile and re-ingest viaingest_linkedin_profile. -
What about
preferred_functionsandpreferred_levels? → Currently provision-only. Deferred — could become user-editable later. -
Force-cascade on
remove_stint? → Currently blocked if achievements exist. Deferred. -
Competency labels on achievements — Not joined in
get_profile. Deferred. -
Cache/index rebuild strategy — Current fire-and-forget approach works. Change-stream watcher is the long-term direction.