Skip to content

symfonic.capabilities.prompting.persona

persona

A domain's persona, as prompting inputs rather than a configuration object.

The generated plugin.py says who an assistant is in four different ways: a DomainTemplate with a name and a soul_schema, an onboarding_checklist on the same object, a description, and PluginContribution objects returned from a hook the engine calls. One idea, four mechanisms, and three of them reachable only through the compatibility facade.

This is that idea as one value and one function. The typed pieces already existed -- :class:~.identity.AgentIdentity renders legacy's prose byte for byte, :class:~.onboarding.OnboardingDirective applies legacy's cap -- and what was missing is a public way to ask for them together.

Sources, not a template. What comes back are ContributionSources the prompt compiler orders, budgets, delimits and caches like any other contribution. That is the difference from a template variable: a persona competes for budget on its merits and is attributed when it is dropped, rather than being interpolated into an authored prompt where nothing can see it.

An absent field contributes nothing. Not an empty source -- none. A source that renders "(none)" spends budget to tell the model nothing, and the placeholder then reads as a fact about the deployment.

DomainPersona dataclass

DomainPersona(name: str, role: str = 'assistant', tone: str = 'helpful', description: str = '', onboarding: Sequence[str] = tuple())

Who a domain's assistant is, in the four fields that reach the model.

Frozen for the reason load_plugin() disappears: a plan compiled from mutable inputs is a plan nobody can trust, and a persona that could be edited after composition would reintroduce that one field at a time.

Only name is required, because only name was. A domain that names itself and says nothing else is the ordinary case, and requiring a checklist to get an identity would make the smallest scaffold carry the largest one's shape.

persona_sources

persona_sources(persona: DomainPersona) -> tuple[Any, ...]

The contribution sources persona renders through.

Ordered identity first: it is the shortest, the most stable, and the one whose absence changes how every other contribution reads. A budget that has to drop something should drop the description before it drops who the assistant is.

Source code in src/symfonic/capabilities/prompting/persona.py
def persona_sources(persona: DomainPersona) -> tuple[Any, ...]:
    """The contribution sources ``persona`` renders through.

    Ordered identity first: it is the shortest, the most stable, and the one
    whose absence changes how every other contribution reads. A budget that
    has to drop something should drop the description before it drops who the
    assistant is.
    """
    sources: list[Any] = [
        AgentIdentitySource(
            identity=AgentIdentity.from_domain_name(
                persona.name, role=persona.role, tone=persona.tone
            )
        )
    ]

    directive = OnboardingDirective.from_checklist(persona.onboarding)
    if directive is not None:
        sources.append(OnboardingSource(directive=directive))

    if persona.description.strip():
        # Authored and offline, like the two above: it is text the deployment
        # wrote, not something fetched or supplied by a user, so the compiler
        # may place it at an authored tier rather than delimiting it as
        # untrusted.
        sources.append(
            StaticSource(
                text=persona.description.strip(),
                untrusted=False,
                offline_safe=True,
                scope_aware=False,
            )
        )

    return tuple(sources)