Skip to content

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}
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.

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}")

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; innovate and riff may 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. Check usage.cache_read_input_tokens to confirm cache hits.
  • strict_corpus only applies to ask and review; innovate and riff always call the model.
  • A missing ANTHROPIC_API_KEY surfaces only when a method is called, as an error from the Anthropic SDK.