The product had three agents: a customer support agent, a technical documentation agent, and a sales agent. Each had different names, different tones, different capabilities listed in the system prompt.
The problem: three separate code paths assembled three separate system prompts. Developers updated the customer support persona in one place and forgot to update the fallback path. For two weeks, the fallback persona introduced itself as "Maya" while the primary introduced itself as "Alex." Support tickets mentioned the inconsistency.
llm-persona is a named registry for agent personas. One definition. Many uses. No drift.
The Shape of the Fix
from llm_persona import PersonaRegistry, Persona
registry = PersonaRegistry()
registry.define(Persona(
name="Maya",
role="Customer Support Specialist",
company="Acme Corp",
tone="helpful, concise, empathetic",
capabilities=[
"Look up order status",
"Process refunds under $50",
"Escalate complex issues",
],
constraints=[
"Never promise specific delivery dates",
"Never approve refunds over $50 without approval",
],
greeting="Hi, I'm Maya from Acme support.",
))
# Build system prompt
system = registry.get("Maya").build_system_prompt()
# Or access specific fields
print(registry.get("Maya").greeting)
# "Hi, I'm Maya from Acme support."
Define once. Use everywhere. Change the definition in one place.
What It Does NOT Do
llm-persona does not manage conversation history or state. It provides persona definition and system prompt generation. The actual conversation is your responsibility.
It does not validate that the model actually behaves according to the persona. It defines the instructions; whether the model follows them depends on prompt quality and model capability.
It does not handle persona switching mid-conversation. Each persona is for a fresh context. Switching personas mid-conversation requires careful prompt construction that this library does not help with.
Inside the Library
Persona is a dataclass:
@dataclass
class Persona:
name: str
role: str
company: str | None = None
tone: str = "helpful and professional"
capabilities: list[str] = field(default_factory=list)
constraints: list[str] = field(default_factory=list)
greeting: str | None = None
custom_sections: dict[str, str] = field(default_factory=dict)
def build_system_prompt(self) -> str:
lines = [f"You are {self.name}, a {self.role}"]
if self.company:
lines[0] += f" at {self.company}."
else:
lines[0] += "."
lines.append(f"Tone: {self.tone}")
if self.capabilities:
lines.append("You can:\n" + "\n".join(f"- {c}" for c in self.capabilities))
if self.constraints:
lines.append("Do not:\n" + "\n".join(f"- {c}" for c in self.constraints))
for section_name, content in self.custom_sections.items():
lines.append(content)
return "\n\n".join(lines)
PersonaRegistry.define() stores the persona by name. get(name) retrieves it. list_names() returns all registered names. Personas can be loaded from YAML or JSON files via PersonaRegistry.from_file(path).
Template fields: persona fields can contain {variable} placeholders. persona.build_system_prompt(company="Acme", user_name="John") fills them. Useful for personas that need to reference per-session context.
When to Use It
Use it when you have more than one agent persona in your product. The registry pattern pays off immediately: one definition, many uses, no drift.
Use it for persona-heavy products. Customer support bots, sales assistants, persona-based education tools. Any product where the agent "is" a specific character with a defined identity.
Use it with agent-context-builder for the full system prompt. The persona defines the core identity sections. The context builder adds tool descriptions, capability flags, and per-session context on top.
Skip it for single-persona products where the system prompt never changes. If you have one agent with one fixed persona, a constant string is simpler.
Install
pip install git+https://github.com/MukundaKatta/llm-persona
from llm_persona import PersonaRegistry, Persona
import yaml
# Load personas from config file
with open("personas.yaml") as f:
personas_config = yaml.safe_load(f)
registry = PersonaRegistry()
for name, config in personas_config.items():
registry.define(Persona(name=name, **config))
# In your request handler
def handle_request(agent_id: str, user_message: str) -> str:
persona = registry.get(agent_id)
response = client.messages.create(
model="claude-sonnet-4-6",
system=persona.build_system_prompt(),
messages=[{"role": "user", "content": user_message}],
max_tokens=1024,
)
return response.content[0].text
Sibling Libraries
| Library | What it solves |
|---|---|
agent-context-builder |
Build full system prompts from named sections |
prompt-template-version |
Version and fingerprint prompt templates |
prompt-cache-warmer |
Pre-warm Anthropic cache for persona system prompts |
agent-fn-registry |
Registry for tools associated with each persona |
conversation-codec |
Persist persona-specific conversation history |
The full persona stack: llm-persona defines the identity, agent-context-builder adds context-specific sections, prompt-cache-warmer keeps the combined prompt warm in Anthropic's cache.
What's Next
Persona versioning: tag each persona definition with a version. Track which version handled each conversation. When you update a persona, old conversations were handled by v1, new ones by v2. This is essential for A/B testing persona changes.
Inheritance: Persona(name="MayaPremium", parent="Maya", capabilities=[...extra...]) that inherits from a base persona and adds or overrides fields. This avoids duplicating the base persona definition for each variant.
YAML schema validation: a JSON schema for the persona YAML format with a validate_file() helper that gives clear error messages when the config is malformed. Right now loading a malformed YAML will crash with a KeyError or pydantic-less dict mismatch.
Built as part of the agent-stack family: composable Python primitives for production LLM agents.
Top comments (0)