Recommendation Engine — HTTP API#
Standalone FastAPI service for Tangled repo/issue discovery.
Storage: DATA_STORAGE=sql (default, Postgres+pgvector) or DATA_STORAGE=git
(in-memory numpy+jsonl bundle cloned from REC_DATA_GIT_URL at boot). See
.env.example.
Endpoints: /recommendations, /questionnaire (sql only today), /health.
Base URL: whatever you deploy to (the Tangled appview points TANGLED_DISCOVER_ENDPOINT
at the /recommendations path).
GET /recommendations#
The contract consumed by the Tangled appview. Returns the user's interest chips plus ranked repo + issue recommendations, with the user's own/collaborated repos and self-authored issues excluded.
Query params
| Param | Required | Notes |
|---|---|---|
handle |
yes | The user's Tangled DID, e.g. did:plc:abc123. |
gh |
no | Connected GitHub username. Accepted but currently ignored (no GitHub data). |
Response 200 OK — see schema.md for the authoritative shape. Summary:
{
"profile": {
"interests": [{ "label": "nix", "slug": "nix" }], // from the user's repo topics
"languages": [], // no language signal yet
"sources": { "tangled": { "repos": 10 } } // github omitted (no data)
},
"repos": [{
"name": "...", "owner": "@handle", "language": "", "description": "...",
"stars": 0, "openIssues": 3, "lastActive": "<RFC3339>",
"url": "https://tangled.org/@handle/name",
"basedOnRepoUrl": "https://tangled.org/@you/your-seed-repo"
}],
"issues": [{
"title": "...", "repo": "handle/name", "owner": "@handle",
"issueUri": "at://did:plc:…/sh.tangled.repo.issue/3k…",
"repoDid": "did:plc:...", "rkey": "3k...",
"url": "https://tangled.org/@handle/name",
"basedOnRepoUrl": "https://tangled.org/@you/your-seed-repo",
"repoReadme": "...",
"labels": [], "comments": 0, "language": "", "lastActive": "<RFC3339>"
}]
}
Notes:
- Empty user →
"repos": [](the frontend then shows its cold-start view). stars/comments/language/languagesare stubbed (no source in the shared DB yet).- Issues omit
number(issue permalink); the frontend resolves it from(repoDid, rkey).urlis the parent repo;basedOnRepoUrlis the user's seed repo that surfaced the hit. basedOnRepoUrlon repos is the same seed attribution (the user's repo whose README embedding produced the closest match).
GET /questionnaire#
Return the cached AI-solve questionnaire JSON for an issue (written by the questionnaire
Cloud Run job). Does not generate on demand — returns 404 if not cached yet.
Query params
| Param | Required | Notes |
|---|---|---|
issue |
yes* | Full at://…/sh.tangled.repo.issue/<rkey> URI, or bare rkey (DB lookup). |
issue-uri |
yes* | Alias for issue. |
* Provide one of issue or issue-uri.
Response 200 OK — questionnaire object (version 2: introduction, items[], …).
Errors
| Status | When |
|---|---|
400 |
Missing param, invalid URI, or ambiguous rkey |
404 |
Issue URI valid but no cached questionnaire |
curl 'localhost:8000/questionnaire?issue=at://did:plc:…/sh.tangled.repo.issue/3lv…'
GET /health#
{ "status": "ok", "db": true }
status is "degraded" with db:false (and an error) if the database is unreachable.
Conventions#
- Timestamps (
lastActive) are RFC-3339; the frontend humanizes them. ownercarries a leading@; repourlis absolute.- Ordering is the engine's call — arrays are returned already ranked, most relevant first.
- Errors: any non-200 (or timeout) makes the appview fall back to its cold-start view; no structured error body is required.