Developing a custom persona#
A persona is a subclass of BasePersona. At minimum you implement two things
— a defaults property and a process_message method — and register the class
via an entry point. Everything else has a working default you can override as
needed. The full contract, grouped by what you must vs. may implement, is in
the Python API.
1. Subclass BasePersona#
from jupyter_ai_persona_manager import BasePersona, PersonaDefaults
from jupyterlab_chat.models import Message
import os
AVATAR_PATH = os.path.join(os.path.dirname(__file__), "assets", "avatar.svg")
class MyPersona(BasePersona):
@property
def defaults(self) -> PersonaDefaults:
return PersonaDefaults(
name="MyPersona",
description="A helpful custom assistant",
avatar_path=AVATAR_PATH,
system_prompt="You are a helpful assistant specialized in…",
)
async def process_message(self, message: Message) -> None:
# Turn the message into a reply. Stream a model response, or just:
self.send_message(f"You said: {message.body}")
defaults(defaults) — Required. Returns aPersonaDefaultswith the persona’s name, description, avatar path, and system prompt.process_message(process_message()) — Required. The main entry point: produce a reply, typically viastream_message()(streaming) orsend_message()(one shot).
2. Register it via an entry point#
In your package’s pyproject.toml:
[project.entry-points."jupyter_ai.personas"]
my-persona = "my_package.my_persona:MyPersona"
The manager discovers personas from this entry point group at startup. (For
quick local iteration you can also drop a *persona*.py file into a chat’s
.jupyter/personas/ directory and run /refresh-personas — no reinstall.)
3. Implement more of the contract as needed#
Beyond the two required members, the pieces you’re most likely to add:
Streaming a model reply. Pass an async iterator of chunks to
stream_message(); it creates and progressively updates a single chat message and manages your typing indicator automatically.Interrupting (
cancel_response(), Recommended for streaming personas). Override it to stop your backend when the user hits the interrupt button.Being configurable. If users can pick your model or settings, override the Optional
update_model/update_model_settings/update_settingsmethods to switch your backend, and publish your options with thereport_model_configuration/report_settings_configurationmethods. The base class handles broadcasting the current selection for you — see Architecture for the full flow.Cleanup (
shutdown(), Recommended). Stop background tasks; callawait super().shutdown()first.Reading attachments. Use
process_attachments()to pull attached file contents into your prompt.
See the Python API for the exhaustive list — each member is labeled by its contract level.
Marking your own API for the docs#
The contract levels on the Python API page are not hand- maintained — they come from documentation markers on each method. If you build an API that others subclass or call (a new base class, a mixin), mark its public members the same way so its generated docs group them and a CI check keeps every member marked.
The markers live in jupyter_ai_persona_manager/doc_markers.py. They are
documentation-only — each mark_* decorator just stamps a metadata attribute
and returns the function unchanged, so there is no runtime effect. Apply one per
public member:
from jupyter_ai_persona_manager.doc_markers import (
mark_required, mark_recommended, mark_optional,
mark_subclass_api, mark_consumer_api,
)
class MyBase(ABC):
@mark_required
@abstractmethod
def must_implement(self): ...
@mark_recommended
def should_override(self): ...
@mark_subclass_api
def call_me_from_a_subclass(self): ...
@mark_consumer_api
def call_me_from_the_framework(self): ...
doc_markers.py is self-contained and dependency-free. To add this to another
package, copy that one file over and mark its members — there is no shared
package for it yet. Then render the grouped reference with the contract-api
directive (see the main Jupyter AI contributor guide on subpackage
documentation).