Skip to content

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.

The CLI entry point serves a profiles directory:

Terminal window
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 RESEARCHER_PROFILES_TOKEN to require Authorization: Bearer <token> on every /api/v1/* route:

Terminal window
export RESEARCHER_PROFILES_TOKEN="a-long-random-string"
python -m researcher_profiles.api --profiles-dir /path/to/profiles

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

List profiles, then fetch one:

Terminal window
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-doe

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

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.

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.

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.

Terminal window
researcher-profiles push /path/to/profiles/jane-doe \
--url http://127.0.0.1:8109

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