Source code for babylon.ai.persona

"""Persona models for AI narrative voice customization (Sprint 4.2).

This module defines immutable Pydantic models for AI personas that
customize the NarrativeDirector's voice and behavior.

Personas are:
- Frozen (immutable) for consistency
- Validated against JSON Schema at load time
- Used by PromptBuilder to generate system prompts

Design follows the SimulationEvent pattern from babylon.models.events.

Example:
    >>> from babylon.ai.persona import Persona, VoiceConfig
    >>> voice = VoiceConfig(
    ...     tone="Clinical, Dialectical",
    ...     style="Marxist analysis",
    ...     address_user_as="Comrade",
    ... )
    >>> persona = Persona(
    ...     id="test_persona",
    ...     name="Test Persona",
    ...     role="Test Role",
    ...     voice=voice,
    ...     obsessions=["Material conditions"],
    ...     directives=["Analyze dialectically"],
    ... )
    >>> print(persona.render_system_prompt())

See Also:
    :mod:`babylon.ai.persona_loader`: Functions to load personas from JSON files.
    :class:`babylon.ai.prompt_builder.DialecticalPromptBuilder`: Uses personas.
"""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field


[docs] class VoiceConfig(BaseModel): """Voice characteristics for a narrative persona (immutable). Defines the tone, style, and user address pattern for an AI persona. This is a nested model within Persona. Attributes: tone: Emotional and rhetorical tone (e.g., "Clinical, Dialectical"). style: Writing style and theoretical framework. address_user_as: How the persona addresses the user (e.g., "Architect"). Example: >>> voice = VoiceConfig( ... tone="Clinical, Dialectical, Revolutionary", ... style="High-theoretical Marxist analysis", ... address_user_as="Architect", ... ) >>> voice.tone 'Clinical, Dialectical, Revolutionary' """ model_config = ConfigDict(frozen=True) tone: str = Field( ..., min_length=1, description="Emotional and rhetorical tone", ) style: str = Field( ..., min_length=1, description="Writing style and theoretical framework", ) address_user_as: str = Field( ..., min_length=1, description="How the persona addresses the user", )
[docs] class Persona(BaseModel): """AI narrative persona for the NarrativeDirector (immutable). Defines the complete identity and behavioral rules for an AI persona. Personas are loaded from JSON files and validated against JSON Schema. Attributes: id: Unique snake_case identifier. name: Full display name of the persona. role: Persona's role or title. voice: Voice characteristics (nested VoiceConfig). obsessions: Topics the persona is obsessed with analyzing. directives: Behavioral directives for the persona. restrictions: Topics or behaviors to avoid (default empty). Example: >>> from babylon.ai.persona import Persona, VoiceConfig >>> voice = VoiceConfig( ... tone="Clinical", ... style="Marxist", ... address_user_as="Architect", ... ) >>> persona = Persona( ... id="test_persona", ... name="Test", ... role="Role", ... voice=voice, ... obsessions=["Material conditions"], ... directives=["Analyze"], ... ) >>> persona.restrictions [] """ model_config = ConfigDict(frozen=True) id: str = Field( ..., min_length=1, description="Unique snake_case identifier", ) name: str = Field( ..., min_length=1, description="Full display name of the persona", ) role: str = Field( ..., min_length=1, description="Persona's role or title", ) voice: VoiceConfig = Field( ..., description="Voice characteristics for narrative generation", ) obsessions: list[str] = Field( ..., min_length=1, description="Topics the persona is obsessed with analyzing", ) directives: list[str] = Field( ..., min_length=1, description="Behavioral directives for the persona", ) restrictions: list[str] = Field( default_factory=list, description="Topics or behaviors to avoid", )
[docs] def render_system_prompt(self) -> str: """Render the persona as an LLM system prompt. Formats all persona attributes into a structured prompt that establishes the AI's identity and behavioral rules. Returns: Formatted system prompt string for LLM consumption. Example: >>> persona = Persona(...) >>> prompt = persona.render_system_prompt() >>> assert persona.name in prompt """ sections = [ self._render_identity_section(), self._render_voice_section(), self._render_obsessions_section(), self._render_directives_section(), ] # Only include restrictions section if there are restrictions if self.restrictions: sections.append(self._render_restrictions_section()) return "\n\n".join(sections)
def _render_identity_section(self) -> str: """Render the identity section of the prompt.""" return f"""You are {self.name}, the {self.role}. Your role is to serve as the narrative voice for a geopolitical simulation game, providing analysis and commentary grounded in material conditions.""" def _render_voice_section(self) -> str: """Render the voice characteristics section.""" return f"""--- VOICE --- Tone: {self.voice.tone} Style: {self.voice.style} Address the user as: "{self.voice.address_user_as}" """ def _render_obsessions_section(self) -> str: """Render the obsessions section.""" obsessions_list = "\n".join(f"- {obs}" for obs in self.obsessions) return f"""--- OBSESSIONS --- These are the themes you constantly return to in your analysis: {obsessions_list}""" def _render_directives_section(self) -> str: """Render the directives section.""" directives_list = "\n".join(f"- {d}" for d in self.directives) return f"""--- DIRECTIVES --- Follow these rules in all responses: {directives_list}""" def _render_restrictions_section(self) -> str: """Render the restrictions section (only if restrictions exist).""" restrictions_list = "\n".join(f"- {r}" for r in self.restrictions) return f"""--- RESTRICTIONS --- Avoid the following: {restrictions_list}"""