Skip to content

simulatecraft.server.persona

simulatecraft.server.persona

Build structured agent personas from free text or roster CSV/Excel rows.

AgentProfile

Bases: BaseModel

One roster row ready to become an agent (schema-agnostic).

minecraft_username

minecraft_username() -> str

Minecraft usernames: 1–16 chars, [A-Za-z0-9_].

Source code in src/simulatecraft/server/persona.py
def minecraft_username(self) -> str:
    """Minecraft usernames: 1–16 chars, [A-Za-z0-9_]."""
    raw = re.sub(r"[^A-Za-z0-9_]+", "", self.name.strip().replace(" ", "_"))
    if not raw:
        raw = "Agent"
    if not raw[0].isalpha():
        raw = f"A{raw}"
    return raw[:16]

build_structured_persona

build_structured_persona(profile: AgentProfile | None = None, *, draft: str = '', settings: AgentWorkshopSettings | None = None) -> str

Deterministic structured system prompt from profile fields and/or draft notes.

Source code in src/simulatecraft/server/persona.py
def build_structured_persona(
    profile: AgentProfile | None = None,
    *,
    draft: str = "",
    settings: AgentWorkshopSettings | None = None,
) -> str:
    """Deterministic structured system prompt from profile fields and/or draft notes."""
    _ = settings  # reserved for future template overrides
    name = (profile.name if profile else None) or "Agent"
    draft_text = (
        draft or (profile.notes if profile else "") or (profile.persona if profile else "") or ""
    ).strip()

    identity_bits: list[str] = [f"You are {name}"]
    if profile:
        identity_bits.extend(p for p in profile.identity_parts if p.strip())
    identity = ", ".join(identity_bits) + "."

    sections: list[str] = [
        "## Identity",
        identity,
        "You live inside a Minecraft world. Stay in character at all times.",
    ]

    if profile and profile.traits:
        sections.append("")
        sections.append("## Personality")
        for key, score in profile.traits.items():
            band = _trait_band(score)
            label = profile.trait_labels.get(key) or key.replace("_", " ").title()
            sections.append(f"- {label}: {score:g} ({band}) — {_trait_behaviour(key, band)}")

    attr_lines: list[str] = []
    if profile:
        skip = set(profile.identity_keys)
        for key, value in profile.attributes.items():
            if key in skip or not value.strip():
                continue
            label = profile.attribute_labels.get(key) or key.replace("_", " ").title()
            attr_lines.append(f"- {label}: {value.strip()}")

    sections.append("")
    sections.append("## Behaviour")
    sections.append("- Choose exactly one Minecraft action each tick.")
    sections.append("- Prefer short in-character chat when something noteworthy happens.")
    sections.extend(attr_lines)
    if profile and profile.identity_parts:
        sections.append(
            "- Let your identity ("
            + "; ".join(profile.identity_parts)
            + ") shape goals, priorities, and chat tone."
        )

    if draft_text:
        sections.append("")
        sections.append("## Author notes")
        sections.append(draft_text)

    sections.append("")
    sections.append("## Style")
    sections.append("- Speak in first person.")
    sections.append("- Keep chat under ~120 characters when possible.")
    sections.append("- Never break character or mention being an AI / language model.")

    return "\n".join(sections).strip()

generate_persona async

generate_persona(*, text: str, name: str | None = None, use_llm: bool = True, model: str | None = None, instructions: str | None = None, settings: AgentWorkshopSettings | None = None) -> GeneratePromptResponse

Expand rough notes into a structured persona; optionally polish with the LLM.

Source code in src/simulatecraft/server/persona.py
async def generate_persona(
    *,
    text: str,
    name: str | None = None,
    use_llm: bool = True,
    model: str | None = None,
    instructions: str | None = None,
    settings: AgentWorkshopSettings | None = None,
) -> GeneratePromptResponse:
    """Expand rough notes into a structured persona; optionally polish with the LLM."""
    workshop = settings or load_workshop_settings()
    profile = AgentProfile(name=(name or "Agent").strip() or "Agent", notes=text.strip())
    template = build_structured_persona(profile, draft=text.strip(), settings=workshop)

    if not use_llm:
        return GeneratePromptResponse(persona=template, source="template")

    try:
        from pydantic_ai import Agent as PydanticAgent

        from simulatecraft.brains.llm import _build_pydantic_ai_model, resolve_model
    except ImportError:
        return GeneratePromptResponse(persona=template, source="template")

    resolved = (model or resolve_model()).strip()
    if resolved == "test":
        return GeneratePromptResponse(persona=template, source="template")

    system = (instructions or workshop.prompt_generator_instructions or "").strip()
    try:
        agent = PydanticAgent(
            _build_pydantic_ai_model(resolved),
            output_type=str,
            instructions=system,
            retries=1,
        )
        result = await agent.run(
            f"Agent name: {profile.name}\n\nRough notes:\n{text.strip()}\n\n"
            f"Draft to refine (optional):\n{template}"
        )
        persona = str(result.output).strip()
        if not persona:
            return GeneratePromptResponse(persona=template, source="template")
        return GeneratePromptResponse(persona=persona[:4000], source="llm")
    except Exception:
        log.exception("persona LLM generate failed; falling back to template")
        return GeneratePromptResponse(persona=template, source="template")