_private/qwestly-docs/Features/qwestly-agent/profile-edit-tool.md

Profile Edit Tools — Field Taxonomy & Implementation Plan

Goal: Add get_profile and a family of edit_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/email confirmed read-only.


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_chunk via RAG)

Provision Qwestly Card (LLM-generated, admin-editable):

  • Card summary (roles, achievementsqwestly_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_status are marked "provision-only" — set by recruiters, not shown in candidate self-onboarding
  • anonymized_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_verification hidden)
  • Education: Full CRUD
  • Achievements: Full CRUD (except stint_id which 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_at
  • referral_quota_total, referral_quota_used
  • linkedin_verification, work_email_verification (on stints)
  • signup_method, signup_code
  • onboarding_progress
  • networking_skipped_at
  • company_id (on stints — internal FK)
  • anonymized_for_search
  • created_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:

  • contactname, email (read-only), preferred_name, linkedin_user_name, linkedin_url, phone
  • statusprofile_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_title marked read-only
  • education — 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 + hidden anonymized_for_search
  • company_preferencesinterested_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:

  1. v_candidate_profile_cache_enhanced — Pre-assembled profile JSON
  2. candidate_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_card if they say yes.


9. Review Notes (2026-05-30)

Code Review Findings (Resolved)

  1. Monolithic tool → split into 8 focused tools. The original design had a single edit_profile with ~45 parameters. This violated the existing codebase convention and would cause LLM routing degradation.

  2. clear_end_date sentinel bug — original design used end_date="" as sentinel. Fixed with clear_end_date: bool parameter.

  3. Cascade order bugedit_achievements(action="remove") deleted competency evidence BEFORE the achievement. Fixed: check existence first, then delete achievement, then cascade.

  4. URL encodingedit_preferences sent auth0|... user_id unescaped. Fixed with urllib.parse.quote().

  5. profile_dirty_at — set on candidates_enhanced after every write (add/update/remove) across all tools.

  6. Company prefs read-before-write — added to match pattern used by all other edit tools.

  7. Dead constants removed_HIDDEN_CANDIDATE_FIELDS, _READ_ONLY_CANDIDATE_FIELDS, _READONLY_IF_PASSED, _READ_ONLY_PREFS cleaned up.

Architecture Review Findings (Resolved)

  1. Single responsibility — Split into 8 domain-specific tools.
  2. Direct MongoDB writes vs. API proxy — Hybrid approach: direct writes for simple collections, API proxy for preferences_enhanced.
  3. Dirty-write protection — Soft detection via read-before-write comparison.
  4. updated_fields computation — Read-before-write + diff approach.

10. Open Questions

  1. Should name become editable?Planned. Asana task created to make name editable with Auth0 sync. The agent would update both candidates_enhanced.name and Auth0's user profile. See related Asana task.

  2. Should email become editable?No. Will remain Auth0-managed. Users change email through Auth0 account settings.

  3. Should linkedin_basic_profile be editable?No. Raw LinkedIn API data. Users edit their LinkedIn profile and re-ingest via ingest_linkedin_profile.

  4. What about preferred_functions and preferred_levels? → Currently provision-only. Deferred — could become user-editable later.

  5. Force-cascade on remove_stint? → Currently blocked if achievements exist. Deferred.

  6. Competency labels on achievements — Not joined in get_profile. Deferred.

  7. Cache/index rebuild strategy — Current fire-and-forget approach works. Change-stream watcher is the long-term direction.