Apollo Places: enrich insights with personal place name + notes

Optional integration with the sibling Apollo project's user-defined
Places (name + lat/lon + radius_m + description + category). When
APOLLO_API_BASE_URL is set, the per-photo location resolver folds the
most-specific containing Place into the LLM prompt's location string —
"Home (My house in Cambridge) — near Cambridge, MA" rather than the
city name alone. Smallest-radius wins; Apollo sorts server-side via
/api/places/contains, so the carousel badge in Apollo and the prompt
string here always agree.

Adds an agentic tool `get_personal_place_at(latitude, longitude)` that
the LLM can call during chat continuation. Tool description tells the
model the call returns the user's free-text notes, not just a name.
Deliberately narrow — no enumerate-all variant, lat/lon required.

Unset APOLLO_API_BASE_URL = legacy Nominatim-only path, tool is not
registered. 5 s timeout; all errors degrade silently.

Tests: 5 unit tests for compose_location_string (Apollo only, Nominatim
only, both, both-with-description, neither).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-04-28 19:11:12 +00:00
parent 9d58547ce3
commit 4ae7be35e9
7 changed files with 339 additions and 20 deletions

View File

@@ -280,6 +280,12 @@ OLLAMA_REQUEST_TIMEOUT_SECONDS=120 # Per-request generation timeout
SMS_API_URL=http://localhost:8000 # SMS message API endpoint (default: localhost:8000)
SMS_API_TOKEN=your-api-token # SMS API authentication token (optional)
# Apollo Places integration (optional). When set, photo-insight enrichment
# folds the user's personal place name (Home, Work, Cabin, ...) into the
# location string fed to the LLM, and the agentic loop gains a
# `get_personal_place_at` tool. Unset = legacy Nominatim-only path.
APOLLO_API_BASE_URL=http://apollo.lan:8000 # Base URL of the sibling Apollo backend
# OpenRouter (Hybrid Backend) - keeps embeddings + vision local, routes chat to OpenRouter
OPENROUTER_API_KEY=sk-or-... # Required to enable hybrid backend
OPENROUTER_DEFAULT_MODEL=anthropic/claude-sonnet-4 # Used when client doesn't pick a model
@@ -371,6 +377,31 @@ Configurable env:
- `AGENTIC_CHAT_MAX_ITERATIONS` — cap on tool-calling iterations per turn
(default 6). Per-request `max_iterations` is clamped to this cap.
**Apollo Places integration (optional):**
The sibling Apollo project (personal location-history viewer) owns
user-defined Places: `name + lat/lon + radius_m + description (+ optional
category)`. When `APOLLO_API_BASE_URL` is set, ImageApi queries
`/api/places/contains?lat=&lon=` to enrich the LLM prompt's location
string. See `src/ai/apollo_client.rs` and `src/ai/insight_generator.rs`:
- **Auto-enrichment** (always on when configured): the per-photo location
resolver folds the most-specific containing Place ("Home — near
Cambridge, MA" or "Home (My house in Cambridge) — near Cambridge, MA"
when a description is set) into the location field of `combine_contexts`.
Smallest-radius wins — Apollo sorts server-side, this code takes `[0]`.
- **Agentic tool** `get_personal_place_at(latitude, longitude)`: registered
alongside `reverse_geocode` only when `apollo_enabled()` returns true.
Returns "- Name [category]: description (radius N m)" lines, smallest
radius first. The tool is **deliberately narrow** — no enumerate-all
variant; auto-enrichment covers the photo-context path and the agentic
tool covers ad-hoc lat/lon questions in chat continuation.
Failure modes degrade silently to the legacy Nominatim path: 5 s timeout,
errors logged at `warn`, empty results returned. Apollo's routes are
unauthenticated (single-user, LAN-trust); add JWT auth here + on Apollo's
side if exposing beyond a trusted network.
## Dependencies of Note
### Rust crates