fix(personas): expand PERSONA_ALLOWED_FIELDS + add library/clone endpoints
Port fixes from Semblance: - PERSONA_ALLOWED_FIELDS: 9 → 62 fields — oceanTraits, frustrations, motivations, scenarios, aiSynthesizedBio, household/lifestyle fields, audience_brief, research_objective, etc. were silently discarded at Persona.create(), causing all OCEAN traits to render as 50% on frontend - GET /personas/library — shared library endpoint (all users, bulk user lookup) - POST /personas/:id/clone — clone any persona to current user - GET /personas/:id — remove 403 for non-owners (read is public to auth users) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dd816783e7
commit
a8b0a40e08
2 changed files with 98 additions and 12 deletions
|
|
@ -7,12 +7,39 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
# Allowed fields for create/update (mass assignment protection)
|
||||
PERSONA_ALLOWED_FIELDS = {
|
||||
# Core demographics
|
||||
"name", "age", "gender", "occupation", "education", "location",
|
||||
"techSavviness", "personality", "interests", "brandLoyalty",
|
||||
"priceConsciousness", "environmentalConcern", "hasPurchasingPower",
|
||||
"hasChildren", "thinkFeelDo", "description", "imageUrl",
|
||||
"folder_ids", "llm_model", "traits", "background", "goals",
|
||||
"communication_style", "values", "demographics", "marketplace",
|
||||
"ethnicity", "socialGrade",
|
||||
# Psychographic sliders
|
||||
"techSavviness", "brandLoyalty", "priceConsciousness",
|
||||
"environmentalConcern", "hasPurchasingPower", "hasChildren",
|
||||
# Personality
|
||||
"personality", "oceanTraits",
|
||||
# Interests and rich text
|
||||
"interests", "description", "imageUrl",
|
||||
# AI-generated narrative fields
|
||||
"goals", "frustrations", "motivations", "scenarios", "scenarioType",
|
||||
"thinkFeelDo",
|
||||
# Household / lifestyle
|
||||
"householdComposition", "householdIncome", "livingSituation",
|
||||
"mediaConsumption", "deviceUsage", "shoppingHabits",
|
||||
"brandPreferences", "communicationPreferences", "additionalInformation",
|
||||
# Extended attitudinal fields
|
||||
"coreValues", "lifestyleChoices", "socialActivities",
|
||||
"categoryKnowledge", "paymentMethods", "purchaseBehaviour",
|
||||
"decisionInfluences", "painPoints", "journeyContext",
|
||||
"keyTouchpoints", "selfDeterminationNeeds", "fears", "narrative",
|
||||
# AI-synthesised summary fields (for persona cards)
|
||||
"aiSynthesizedBio", "qualitativeAttributes", "topPersonalityTraits",
|
||||
# Generation provenance
|
||||
"audience_brief", "research_objective",
|
||||
# Organisation
|
||||
"folder_ids", "folderId", "folder_id",
|
||||
# Cohorta-specific
|
||||
"marketplace",
|
||||
# Legacy / misc
|
||||
"llm_model", "traits", "background",
|
||||
"communication_style", "values", "demographics",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -48,21 +48,80 @@ async def get_all_personas():
|
|||
logger.error(f"Error in get_all_personas: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@personas_bp.route('/library', methods=['GET'])
|
||||
@jwt_required()
|
||||
@active_required
|
||||
async def get_library_personas():
|
||||
"""Return all personas from all users, enriched with owner username."""
|
||||
try:
|
||||
from app.db import get_db
|
||||
db = await get_db()
|
||||
|
||||
limit = min(int(request.args.get('limit', 500)), 1000)
|
||||
|
||||
# Single query for all users to build the username map
|
||||
user_map = {}
|
||||
async for u in db.users.find({}, {'_id': 1, 'username': 1, 'email': 1}):
|
||||
uid = str(u['_id'])
|
||||
user_map[uid] = u.get('username') or u.get('email', 'Unknown')
|
||||
|
||||
# Fetch personas — only fields needed for the library card
|
||||
LIBRARY_FIELDS = {
|
||||
'name': 1, 'age': 1, 'gender': 1, 'occupation': 1, 'location': 1,
|
||||
'personality': 1, 'interests': 1, 'avatar': 1,
|
||||
'oceanTraits': 1, 'aiSynthesizedBio': 1,
|
||||
'qualitativeAttributes': 1, 'topPersonalityTraits': 1,
|
||||
'research_objective': 1, 'audience_brief': 1, 'marketplace': 1,
|
||||
'created_by': 1, 'created_at': 1,
|
||||
}
|
||||
personas = []
|
||||
async for p in db.personas.find({}, LIBRARY_FIELDS).sort('created_at', -1).limit(limit):
|
||||
p['_id'] = str(p['_id'])
|
||||
p['owner_name'] = user_map.get(p.get('created_by', ''), 'Unknown')
|
||||
personas.append(p)
|
||||
|
||||
return jsonify(make_serializable(personas)), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_library_personas: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@personas_bp.route('/<persona_id>/clone', methods=['POST'])
|
||||
@jwt_required()
|
||||
@active_required
|
||||
@with_user_context
|
||||
async def clone_persona(persona_id):
|
||||
"""Clone any persona into the current user's library."""
|
||||
user_id = get_jwt_identity()
|
||||
try:
|
||||
original = await Persona.find_by_id(persona_id)
|
||||
if not original:
|
||||
return jsonify({"error": "Persona not found"}), 404
|
||||
|
||||
clone = {k: v for k, v in original.items()
|
||||
if k not in ('_id', 'created_at', 'created_by', 'folder_ids', 'updated_at')}
|
||||
clone['folder_ids'] = []
|
||||
|
||||
new_id = await Persona.create(clone, user_id)
|
||||
clone['_id'] = str(new_id)
|
||||
return jsonify({"message": "Persona cloned", "persona": make_serializable(clone), "persona_id": str(new_id)}), 201
|
||||
except Exception as e:
|
||||
logger.error(f"Error cloning persona {persona_id}: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@personas_bp.route('/<persona_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
async def get_persona(persona_id):
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
persona = await Persona.find_by_id(persona_id)
|
||||
if not persona:
|
||||
return jsonify({"message": "Persona not found"}), 404
|
||||
|
||||
if persona.get("created_by") and persona.get("created_by") != user_id:
|
||||
return jsonify({"message": "Permission denied"}), 403
|
||||
|
||||
# Make persona serializable
|
||||
serializable_persona = make_serializable(persona)
|
||||
return jsonify(serializable_persona), 200
|
||||
# Any authenticated user may read any persona (needed for shared library).
|
||||
# Write/delete endpoints retain ownership checks.
|
||||
return jsonify(make_serializable(persona)), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_persona: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue