How to use the persona methods
The [llm] extra attaches four persona methods to ResearcherProfile. Each one
role-plays as the researcher, injecting their expertise.md and SOUL.md
into the system prompt and grounding the response in retrieved evidence from their
corpus.
The four methods are attached by importing the package; loading a profile is
enough. Retrieval uses the profile’s search, so build the index first for
grounded responses.
ask — question answering in the researcher’s voice
Section titled “ask — question answering in the researcher’s voice”from researcher_profiles import ResearcherProfile
p = ResearcherProfile.from_files("path/to/jane-doe")resp = p.ask("What is your approach to region-set analysis?", k=5)print(resp.text)for c in resp.citations: print(c.paper_id, c.relevance)print("grounded:", resp.grounded)ask returns an AskResponse with text, citations, model, usage,
request_id, refused, refusal_reason, and grounded. Set strict_corpus=True
to refuse (no LLM call) when the top retrieval score is below refusal_threshold
(default 0.4). Pass history=[{"role": ..., "content": ...}] for chat-style
turns.
review — critique material from the researcher’s perspective
Section titled “review — critique material from the researcher’s perspective”resp = p.review( "Aim 1: We will profile chromatin accessibility in 200 tumors using ATAC-seq...", focus="novelty and feasibility", k=5,)print(resp.text)review returns a ReviewResponse (same shape as AskResponse). focus seeds
retrieval and shapes the review; when omitted, retrieval is seeded from the first
lines of material. It supports the same strict_corpus / refusal_threshold
refusal gate as ask.
innovate — propose novel research directions
Section titled “innovate — propose novel research directions”ideas = p.innovate("spatial chromatin organization in cancer", n=3, k=12)for idea in ideas: print(idea.hypothesis) print(idea.approach) print(idea.related_works)innovate returns a list of Idea dataclasses (hypothesis, approach,
rationale, related_works). The model is constrained to return JSON and retries
once on a parse failure, then raises GenerativeParseError. Temperature defaults
to 0.7.
related_works are whatever the model emits and may not match real paper_ids.
Resolve them against the profile’s papers:
resolved = ideas[0].resolve_citations(p) # {citation_key: PaperRecord | None}riff — divergent brainstorm fragments
Section titled “riff — divergent brainstorm fragments”riffs = p.riff("every tool needs a config file", n=5)for r in riffs: print(r.angle, "—", r.text)riff returns a list of Riff dataclasses (angle, text, related_work).
Temperature defaults to 1.0 — higher than innovate — to encourage divergence.
Like innovate, it is JSON-constrained with one retry.
When a profile has no persona
Section titled “When a profile has no persona”A persona method only works on a profile that has a synthesized persona —
non-empty expertise.md and SOUL.md. The
has_persona property reports
this, and it is True only for a full/deep
profile with both documents present.
Calling ask, review, innovate, or riff when has_persona is False
raises PersonaUnavailableError
(a subclass of ProfileError) immediately, before any LLM call is made. The
common cause is a lite profile, which never synthesizes SOUL/expertise; the same
happens for any profile missing either document.
Guard the call by checking first:
if p.has_persona: resp = p.ask("What is your approach to region-set analysis?")else: print(f"{p.slug} has no persona; skipping")or by catching the error:
from researcher_profiles.responses import PersonaUnavailableError
try: resp = p.ask("What is your approach to region-set analysis?")except PersonaUnavailableError as e: print(f"no persona for {e.slug}")Over the API
Section titled “Over the API”The same four methods are exposed as POST /api/v1/profiles/{slug}/{ask,review,innovate,riff}
and are available on RemoteResearcherProfile with identical signatures. See the
API reference and
How to serve the API.
When the profile has no synthesized persona (for example a lite profile, or one
missing SOUL/expertise), all four endpoints return 409 with
{"detail": "..."} and make no LLM call. This is distinct from the 500/502
failure modes, which mean an LLM or parse failure on a persona-ready profile. The
HTTP client surfaces the 409 as a generic client error (any HTTP status >= 400
becomes a RuntimeError), not a typed PersonaUnavailableError — that typed
exception is raised only by the in-process methods. See the
persona-409 note in the API reference.
- Every successful call makes at least one Anthropic API call;
innovateandriffmay make two on a JSON parse retry. - The persona prefix (
expertise.md+SOUL.md) is sent with prompt caching, so repeated calls against the same profile and mode reuse the cache. Checkusage.cache_read_input_tokensto confirm cache hits. strict_corpusonly applies toaskandreview;innovateandriffalways call the model.- A missing
ANTHROPIC_API_KEYsurfaces only when a method is called, as an error from the Anthropic SDK.