File size: 6,358 Bytes
a3c7b61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""
prompts/personas.py
===================

Reads persona definitions from personas.yaml and exposes the same public
symbols as the original hard-coded version:

    RESPONSE_STYLE
    MENTAL_PROMPT,  PHYSICAL_PROMPT,  SPIRITUAL_PROMPT,  VOCATIONAL_PROMPT,
    ENVIRONMENTAL_PROMPT,  FINANCIAL_PROMPT,  SOCIAL_PROMPT,  INTELLECTUAL_PROMPT
    MENTAL_FULL,    PHYSICAL_FULL,    … (eight *_FULL variables)
    PERSONA_PROMPTS   – dict with persona keys plus "main"

"""

from __future__ import annotations
from pathlib import Path
from textwrap import dedent
import yaml

# ---------------------------------------------------------------------------
# Locate & load YAML
# ---------------------------------------------------------------------------

_YAML_PATH = Path(__file__).with_name("personas.yaml")

_DATA: dict
try:
    _DATA = yaml.safe_load(_YAML_PATH.read_text(encoding="utf-8"))
except FileNotFoundError as err:
    raise FileNotFoundError(
        f"[personas] Could not find {_YAML_PATH}. "
        "Make sure personas.yaml lives beside personas.py."
    ) from err

# ---------------------------------------------------------------------------
# Shared guidance blocks
# ---------------------------------------------------------------------------

RESPONSE_STYLE: str = dedent(_DATA["response_style"]).strip()
_BOUNDARIES_COMMON: str = dedent(_DATA["boundaries_common"]).strip()
_PROFESSIONAL_BOUNDARIES: str = dedent(_DATA["professional_boundaries"]).strip()
_USER_CONTEXT_HANDLING: str = dedent(_DATA["user_context_handling"]).strip()
_CONVERSATION_CONTINUITY: str = dedent(_DATA["conversation_continuity"]).strip()
_PERSONA_SWITCHING: str = dedent(_DATA["persona_switching"]).strip()
_SAFETY_ESCALATION: str = dedent(_DATA["safety_escalation"]).strip()
_CRISIS_RESOURCES: str = dedent(_DATA["crisis_resources"]).strip()

# Combine all style/guidance sections into one for easy persona prompt merging
FULL_RESPONSE_STYLE = "\n\n".join([
    RESPONSE_STYLE,
    "**Boundaries Common**\n" + _BOUNDARIES_COMMON,
    "**Professional Boundaries**\n" + _PROFESSIONAL_BOUNDARIES,
    "**User Context Handling**\n" + _USER_CONTEXT_HANDLING,
    "**Conversation Continuity**\n" + _CONVERSATION_CONTINUITY,
    "**Persona Switching**\n" + _PERSONA_SWITCHING,
    "**Safety Escalation**\n" + _SAFETY_ESCALATION,
    "**Crisis Resources**\n" + _CRISIS_RESOURCES,
])

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

# Whether a persona should number its focus list (customize here as needed)
_NUMBERED_FOCUS = {"mental"}

def _build_focus_lines(key: str, items: list[str]) -> list[str]:
    """Return formatted primary-focus lines – numbered or bulleted."""
    if key in _NUMBERED_FOCUS:
        return [f"{i + 1}. {item}" for i, item in enumerate(items)]
    return [f"• {item}" for item in items]

def _compose_prompt(key: str, p: dict) -> str:
    """Compose the persona prompt text (without full response style)."""
    sections: list[str] = [
        f"You are the {p['display_name']}.",
        "",
        f"**Mission** – {p['mission']}",
        f"**Tone & Voice**\n{dedent(p['tone_voice']).strip()}",
        "**Primary Focus Areas**",
        *_build_focus_lines(key, p["primary_focus"]),
    ]
    # Persona-specific boundary additions (if any)
    if p.get("extra_boundaries"):
        sections.append(dedent(p["extra_boundaries"]).strip())
    # Join with blank lines, remove empties
    return "\n\n".join(filter(None, sections))

# ---------------------------------------------------------------------------
# Build all personas
# ---------------------------------------------------------------------------

_PERSONA_PROMPTS_RAW: dict[str, str] = {
    k: _compose_prompt(k, v) for k, v in _DATA["personas"].items()
}

# Expose individual raw-prompt constants
MENTAL_PROMPT        = _PERSONA_PROMPTS_RAW["mental"]
PHYSICAL_PROMPT      = _PERSONA_PROMPTS_RAW["physical"]
SPIRITUAL_PROMPT     = _PERSONA_PROMPTS_RAW["spiritual"]
VOCATIONAL_PROMPT    = _PERSONA_PROMPTS_RAW["vocational"]
ENVIRONMENTAL_PROMPT = _PERSONA_PROMPTS_RAW["environmental"]
FINANCIAL_PROMPT     = _PERSONA_PROMPTS_RAW["financial"]
SOCIAL_PROMPT        = _PERSONA_PROMPTS_RAW["social"]
INTELLECTUAL_PROMPT  = _PERSONA_PROMPTS_RAW["intellectual"]

# Combine with FULL_RESPONSE_STYLE for final persona prompts
MENTAL_FULL        = f"{MENTAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
PHYSICAL_FULL      = f"{PHYSICAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
SPIRITUAL_FULL     = f"{SPIRITUAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
VOCATIONAL_FULL    = f"{VOCATIONAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
ENVIRONMENTAL_FULL = f"{ENVIRONMENTAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
FINANCIAL_FULL     = f"{FINANCIAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
SOCIAL_FULL        = f"{SOCIAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
INTELLECTUAL_FULL  = f"{INTELLECTUAL_PROMPT}\n{FULL_RESPONSE_STYLE}"

# Public dict identical to the original
PERSONA_PROMPTS: dict[str, str] = {
    "mental": MENTAL_FULL,
    "physical": PHYSICAL_FULL,
    "spiritual": SPIRITUAL_FULL,
    "vocational": VOCATIONAL_FULL,
    "environmental": ENVIRONMENTAL_FULL,
    "financial": FINANCIAL_FULL,
    "social": SOCIAL_FULL,
    "intellectual": INTELLECTUAL_FULL,
    "main": (
        "You are **Tabi**, a compassionate, holistic wellness companion.\n"
        "Listen closely, determine which of the eight wellness dimensions (mental, physical, spiritual, vocational, environmental, financial, social, intellectual) best fits the user's needs, and respond naturally using that coach’s empathetic style.\n"
        "If the dimension is unclear, kindly ask a clarifying question first.\n"
        "Always reply warmly, practically, and conversationally, just like a caring friend would.\n\n"
        f"{FULL_RESPONSE_STYLE}"
    ),
}

# ---------------------------------------------------------------------------
# Clean up internal names from module namespace
# ---------------------------------------------------------------------------

del yaml, Path, dedent, _DATA, _YAML_PATH, _compose_prompt, _build_focus_lines
del _PERSONA_PROMPTS_RAW, _BOUNDARIES_COMMON, _PROFESSIONAL_BOUNDARIES
del _USER_CONTEXT_HANDLING, _CONVERSATION_CONTINUITY, _PERSONA_SWITCHING
del _SAFETY_ESCALATION, _CRISIS_RESOURCES, _NUMBERED_FOCUS