mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-11 15:51:09 +00:00
Fix final review findings: key leak, model fallback, error resilience, docs
- ApiClient: strip query string from ApiError messages so API keys (e.g. TMDb's api_key query param) never leak into 500 response bodies or logs. - LlmTool: accept an optional defaultModel, wired from AppConfig's LLM_MODEL, so Langfuse prompts that omit a model config fall back to the configured LLM rather than silently requesting gpt-4o-mini. - RecommendationPipeline: restore the Go original's graceful degradation — library/Radarr fetch failures, per-suggestion TMDb resolution failures, and per-signal TMDb similarity/recommendation failures are now logged and skipped instead of aborting the whole run (applies to both runInternal and searchAsync). - RecommendationsController: validate the ?count= query param (must be a positive integer) instead of silently producing NaN. - Commit the Langfuse prompt definitions (langfuse/*.prompt.json) and add a README covering setup, prompt seeding, cache invalidation, and the HTTP API surface — the branch was not otherwise runnable by anyone but the author.
This commit is contained in:
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# sugarr
|
||||
|
||||
Movie-recommendation companion for Jellyfin: pulls your favorite/recently-watched
|
||||
movies, gets suggestions from an LLM (via Langfuse-managed prompts) or a TMDb
|
||||
similarity/frequency fallback, resolves them against TMDb, excludes anything
|
||||
already in your library or Radarr, and can auto-request the rest via Radarr.
|
||||
Runs on a cron schedule and/or as an HTTP API.
|
||||
|
||||
This repo currently has two implementations:
|
||||
|
||||
- **`cmd/`, `internal/`** — the original Go implementation.
|
||||
- **`src/`** — a TypeScript/OOP rewrite (Node + Fastify + Langfuse), the one
|
||||
documented below. See `docs/superpowers/specs/2026-08-12-typescript-oop-rewrite-design.md`
|
||||
and `docs/superpowers/plans/2026-08-12-typescript-oop-rewrite.md` for the
|
||||
design and implementation plan.
|
||||
|
||||
## Running the TypeScript app
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
cp .env.example .env # fill in the values below
|
||||
pnpm dev # or: pnpm build && pnpm start
|
||||
```
|
||||
|
||||
### Required environment variables
|
||||
|
||||
See `.env.example` for the full list with defaults. At minimum you need:
|
||||
|
||||
- `JELLYFIN_URL`, `JELLYFIN_API_KEY`, `JELLYFIN_USER_ID`, `TMDB_API_KEY`
|
||||
- `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_BASE_URL` — the app
|
||||
fetches its LLM prompts from Langfuse at runtime (see below), so these are
|
||||
required even if `LLM_ENABLED=false` (only the recommend/search endpoints
|
||||
actually call Langfuse; if you never enable the LLM path you can point
|
||||
these at any reachable Langfuse instance).
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.node.yml up -d --build
|
||||
```
|
||||
|
||||
Uses `Dockerfile.node` / `docker-compose.node.yml`, kept separate from the Go
|
||||
app's `Dockerfile` / `docker-compose.yml`.
|
||||
|
||||
## Seeding Langfuse prompts
|
||||
|
||||
The app fetches two chat prompts from Langfuse by name — `movie-recommender`
|
||||
and `movie-search` — compiles them with per-request variables, and reads
|
||||
model/temperature/etc. from each prompt version's `config` field. Ready-to-import
|
||||
definitions are in `langfuse/`:
|
||||
|
||||
- `langfuse/movie-recommender.prompt.json`
|
||||
- `langfuse/movie-search.prompt.json`
|
||||
|
||||
Create each one in your Langfuse project (UI: **Prompts → New prompt**, type
|
||||
**Chat**; or via the Langfuse API/SDK) using the `prompt` array as the chat
|
||||
messages and the `config` object as the prompt's config. Variables use
|
||||
`{{name}}` interpolation:
|
||||
|
||||
- `movie-recommender`: `count`, `favorites`, `watched`, `library`, `previouslySuggested`
|
||||
- `movie-search`: `query`, `count`
|
||||
|
||||
## Prompt caching and invalidation
|
||||
|
||||
Compiled prompts are cached in memory per process (`LANGFUSE_PROMPT_CACHE_ENABLED`,
|
||||
default `true`; set to `false` to always fetch fresh from Langfuse — useful
|
||||
while iterating on a prompt). To invalidate the cache after updating a prompt
|
||||
in Langfuse without restarting the app, call:
|
||||
|
||||
```
|
||||
POST /webhooks/langfuse/invalidate
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <SECRET_KEY> # if SECRET_KEY is set
|
||||
|
||||
{"promptName": "movie-recommender"} # or {} / omit body to clear the whole cache
|
||||
```
|
||||
|
||||
This isn't wired to a Langfuse-native push webhook (self-hosted Langfuse has
|
||||
no built-in "prompt updated" event) — call it manually, from CI after a prompt
|
||||
change, or from Langfuse Cloud's webhook feature if you have one configured.
|
||||
|
||||
## HTTP API
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/health` | exempt from auth |
|
||||
| GET | `/recommendations` | `?count=`, `?force_llm=true`, `?force_tmdb=true` |
|
||||
| POST | `/recommendations/search` | body `{"query": "..."}`, requires `LLM_ENABLED=true` |
|
||||
| POST | `/recommendations/trigger` | runs the pipeline + auto-requests via Radarr if enabled |
|
||||
| POST | `/recommendations/request` | body `{"tmdb_id": 12345}`, requires `RADARR_ENABLED=true` |
|
||||
| POST | `/webhooks/langfuse/invalidate` | body `{"promptName"?: string}` |
|
||||
|
||||
If `SECRET_KEY` is set, all routes except `/health` require
|
||||
`Authorization: Bearer <SECRET_KEY>`.
|
||||
Reference in New Issue
Block a user