How to serve the API and use the HTTP client
The [api] extra ships a FastAPI app that serves a directory of profiles over
HTTP. The [client] extra provides RemoteResearcherProfile, which behaves like
a locally-loaded profile but fetches its data from that server.
Start the server
Section titled “Start the server”The CLI entry point serves a profiles directory:
python -m researcher_profiles.api \ --profiles-dir /path/to/profiles \ --host 127.0.0.1 \ --port 8109--profiles-dir defaults to the RESEARCHER_PROFILES_DIR environment variable,
--host to RESEARCHER_PROFILES_HOST (default 127.0.0.1), and --port to
RESEARCHER_PROFILES_PORT (default 8109). See the
API reference for all environment
variables.
Set a bearer token
Section titled “Set a bearer token”Set RESEARCHER_PROFILES_TOKEN to require Authorization: Bearer <token> on
every /api/v1/* route:
export RESEARCHER_PROFILES_TOKEN="a-long-random-string"python -m researcher_profiles.api --profiles-dir /path/to/profilesIf the token is unset or empty, the server runs in open mode and logs a
warning — every request is accepted. Do not expose an open-mode server to
untrusted networks. The /health route is always unauthenticated.
Call the server directly
Section titled “Call the server directly”List profiles, then fetch one:
curl -H "Authorization: Bearer $RESEARCHER_PROFILES_TOKEN" \ http://127.0.0.1:8109/api/v1/profiles
curl -H "Authorization: Bearer $RESEARCHER_PROFILES_TOKEN" \ http://127.0.0.1:8109/api/v1/profiles/jane-doeBoth GET /api/v1/profiles (each list entry) and GET /api/v1/profiles/{slug}
(under metadata) include a level field — one of lite, full, or deep (see
profile depth levels). Clients can
filter or branch on the tier: for example, skip persona calls for a lite profile,
which is not persona-ready.
Use the Python client
Section titled “Use the Python client”RemoteResearcherProfile (via ResearcherProfile.from_api) exposes the same
properties and method signatures as a local profile:
from researcher_profiles import ResearcherProfile
p = ResearcherProfile.from_api( "http://127.0.0.1:8109", slug="jane-doe", token="a-long-random-string", # or set RESEARCHER_PROFILES_TOKEN)print(p.name, p.affiliation)print(len(p.papers))print(list(p.summaries.keys()))
hits = p.search("region set enrichment", k=3)for h in hits: print(round(h.score, 3), h.source_type, h.source_id)You can also pass a slug-qualified URL and omit slug:
p = ResearcherProfile.from_api( "http://127.0.0.1:8109/api/v1/profiles/jane-doe", token="a-long-random-string",)If token is not passed, the client reads RESEARCHER_PROFILES_TOKEN from the
environment. Use the profile as a context manager to close its HTTP connection:
with ResearcherProfile.from_api("http://127.0.0.1:8109", slug="jane-doe") as p: print(p.name)List remote profiles without loading each one:
summaries = ResearcherProfile.list_remote("http://127.0.0.1:8109", token="...")for s in summaries: print(s["slug"], s["name"], s["level"], s["paper_count"])Each summary carries a level, so you can branch before making a persona call. A
persona call (ask/review/innovate/riff) against a lite profile — or any
profile without a synthesized persona — returns 409, which the client raises as
a generic client error. See
How to use the persona methods.
Match profiles over HTTP
Section titled “Match profiles over HTTP”Cross-profile ranking is not profile-scoped, so it is a module-level function
that POSTs to /api/v1/match:
from researcher_profiles.client import rank_against
matches = rank_against( "http://127.0.0.1:8109", "region set enrichment analysis", k=5, token="a-long-random-string",)for m in matches: print(round(m["score"], 3), m["slug"], m["name"], m["orcid"])Each match is a dict shaped like MatchResult: {slug, name, orcid, score, evidence}. Pass include_chunks=True to have the server return chunk-level
evidence in evidence.top_chunks.
The /api/v1/match endpoint is embedding-backed. On a server without the
embeddings extra, or when no profile is indexed, it returns 503 rather than
crashing, so callers can degrade gracefully. See the
API reference.
Push a built profile to a server
Section titled “Push a built profile to a server”Profiles are built locally (e.g. a lite build via
python -m pipelines.rprofiles.build_cli <slug> --level lite --no-commit,
then researcher-profiles index <dir> for matchability) and pushed to the
serving instance — the server never builds profiles itself.
researcher-profiles push /path/to/profiles/jane-doe \ --url http://127.0.0.1:8109The slug defaults to the directory name (--slug overrides); the token
defaults to RESEARCHER_PROFILES_TOKEN. Or from Python:
from researcher_profiles.client import push_profile
summary = push_profile("http://127.0.0.1:8109", "/path/to/profiles/jane-doe")print(summary) # {"slug": ..., "name": ..., "level": ..., "indexed": ...}The push tars the directory’s contents (dotfiles such as .archive/
excluded), PUTs it to /api/v1/profiles/{slug}, and the server swaps it into
place atomically. The pushed profile is immediately visible to
GET /api/v1/profiles and — when meta/embeddings.sqlite was included — to
/api/v1/match, with no server restart. See the
API reference for validation
rules and the size cap.