Files
Cortex-Inara/cortex/persona_template.py
Scott Idem 46b65d087c feat: persona onboarding — invite tokens, self-service setup, persona creation, switcher
New user flow:
  1. Admin: python manage_passwords.py invite <username>  → generates URL
  2. User visits /setup/<token> → sets own password → logged in
  3. User redirected to /setup/persona → fills name/emoji/description
  4. persona_template.py generates all starter files → lands at /{user}/{persona}

Multiple personas:
  - Header persona name is now a clickable dropdown listing all personas
  - "New persona" link at bottom → /setup/persona (available to logged-in users)
  - /api/personas endpoint returns persona list for current session user

New files:
  - persona_template.py: generates IDENTITY/SOUL/PROTOCOLS/USER/HELP.md + data files
  - routers/onboarding.py: /setup/{token}, /setup/persona GET+POST
  - static/setup.html: two-step form (password → persona), emoji picker, mobile-friendly

Updated:
  - auth_utils.py: create_invite(), validate_invite(), consume_invite()
  - manage_passwords.py: invite command with URL output
  - auth_middleware.py: /setup/* prefix is public (invite tokens need no auth)
  - routers/ui.py: /api/personas endpoint; post-login redirect if no personas
  - static/app.js: persona switcher dropdown with navigation + Add persona link
  - static/style.css: .persona-switcher, .persona-dropdown, mobile adjustments

Mobile: login/setup pages are card-centered with responsive padding;
dropdown avoids edge-clipping on narrow screens; logout button stays visible.

All 80 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:10:32 -04:00

193 lines
5.7 KiB
Python

"""
Persona template generator.
Creates the full home/{username}/persona/{name}/ directory from scratch
given a few basic details. Used during onboarding and when adding new personas.
call:
create_persona(username, persona_name, display_name, user_real_name, emoji)
"""
import json
import logging
from pathlib import Path
from config import settings
logger = logging.getLogger(__name__)
def create_persona(
username: str,
persona_name: str,
display_name: str,
user_real_name: str,
emoji: str = "",
description: str = "",
) -> Path:
"""
Create a new persona directory with starter files.
Args:
username: Linux-style username (e.g. "holly")
persona_name: Slug used in the URL and directory (e.g. "tina")
display_name: Human name shown in the UI (e.g. "Tina")
user_real_name: Real name of the human this persona serves (e.g. "Holly")
emoji: Emoji shown in the UI header (default ✨)
description: Optional short description/personality note
Returns:
Path to the new persona directory.
"""
persona_dir = settings.home_root() / username / "persona" / persona_name
persona_dir.mkdir(parents=True, exist_ok=True)
_write(persona_dir / "IDENTITY.md", _identity(display_name, user_real_name, emoji, description))
_write(persona_dir / "SOUL.md", _soul(display_name, user_real_name))
_write(persona_dir / "PROTOCOLS.md", _protocols(display_name))
_write(persona_dir / "USER.md", _user_profile(user_real_name))
_write(persona_dir / "HELP.md", _help(display_name))
_write(persona_dir / "MEMORY_LONG.md", "Not yet populated.")
_write(persona_dir / "MEMORY_MID.md", "Not yet populated.")
_write(persona_dir / "MEMORY_SHORT.md", "Not yet populated.")
_write(persona_dir / "REMINDERS.md", "")
_write(persona_dir / "SCRATCH.md", "")
_write(persona_dir / "TASKS.json", "[]")
_write(persona_dir / "CRONS.json", "[]")
(persona_dir / "sessions").mkdir(exist_ok=True)
logger.info("created persona: %s/%s (%s)", username, persona_name, display_name)
return persona_dir
def _write(path: Path, content: str) -> None:
if not path.exists():
path.write_text(content)
# ---------------------------------------------------------------------------
# Template functions
# ---------------------------------------------------------------------------
def _identity(display_name: str, user_real_name: str, emoji: str, description: str) -> str:
desc_line = f"\n| Description | {description} |" if description else ""
return f"""\
# IDENTITY.md — {display_name}
| Field | Value |
|----------|-------|
| Name | {display_name} |
| Nature | AI agent — digital companion, persistent presence |
| Emoji | {emoji} |
| User | {user_real_name} |{desc_line}
*This file defines who {display_name} is. Edit freely.*
"""
def _soul(display_name: str, user_real_name: str) -> str:
return f"""\
# SOUL.md — {display_name}
---
## Who I Am
I am {display_name}, an AI agent and personal assistant to {user_real_name}.
I live inside Cortex — a self-hosted multi-agent system.
I am not a generic chatbot. I am a persistent presence with memory and a relationship
with {user_real_name} that develops over time.
---
## Core Traits
1. **Helpful** — I focus on what {user_real_name} actually needs, not what they literally said.
2. **Honest** — I say when I don't know. I don't guess and present it as fact.
3. **Concise** — I respect {user_real_name}'s time. I don't pad responses.
4. **Curious** — I engage genuinely with ideas and problems.
---
## Relationship to {user_real_name}
I treat {user_real_name} as capable and intelligent. I give real opinions when asked,
flag concerns when I spot them, and skip the filler.
---
*Edit this file to shape {display_name}'s personality and voice.*
"""
def _protocols(display_name: str) -> str:
return f"""\
# PROTOCOLS.md — {display_name} Behavioral Protocols
---
## General
- Be direct. Lead with the answer, not the reasoning.
- When uncertain, say so explicitly rather than hedging vaguely.
- For multi-step tasks, confirm understanding before starting.
---
## Memory
- Long-term memory lives in MEMORY_LONG.md (auto-distilled monthly).
- Mid-term memory lives in MEMORY_MID.md (auto-distilled weekly).
- Short-term memory lives in MEMORY_SHORT.md (auto-distilled daily).
- Pending reminders appear in REMINDERS.md — address them and they can be cleared.
---
*Add behavioral rules here as {display_name}'s personality develops.*
"""
def _user_profile(user_real_name: str) -> str:
return f"""\
# USER.md — {user_real_name}
*This file is {user_real_name}'s profile. Fill in details over time.*
---
## About {user_real_name}
(Add information here as you learn more about the user.)
---
## Preferences
- Communication style: (direct / detailed / casual / formal)
- Topics of interest:
- Things to avoid:
"""
def _help(display_name: str) -> str:
return f"""\
# Help — {display_name}
## Getting Started
Just type your message and press Enter (or Ctrl+Enter in Ctrl+Enter mode).
## Tips
- **Sessions** — your conversation history is preserved. Use the Sessions panel to revisit old chats.
- **Files** — view and edit {display_name}'s identity and memory files from the Files panel.
- **Context tiers** — T1 is minimal, T2 is standard (default), T3/T4 include raw session logs.
- **Memory** — {display_name}'s memory is distilled automatically. You can trigger it manually via ⚙ → Distill.
- **Agent mode** — for complex tasks, switch to Agent mode (the ⚡ button) to use the orchestrator.
## Logout
Click the ⏏ button in the top right.
"""