8.5 KiB
8.5 KiB
title, type, permalink
| title | type | permalink |
|---|---|---|
| AGENTS | note | voyage/agents |
Voyage Development Instructions (OpenCode)
Project
- Name: Voyage
- Purpose: Self-hosted travel companion web app (fork of AdventureLog)
- Stack: SvelteKit 2 (TypeScript) frontend · Django REST Framework (Python) backend · PostgreSQL + PostGIS · Memcached · Docker · Bun (frontend package manager)
Pre-Release Policy
Voyage is pre-release — not yet in production use. During pre-release:
- Architecture-level changes are allowed, including replacing core libraries (e.g. LiteLLM).
- Prioritize correctness, simplicity, and maintainability over backward compatibility.
- Before launch, this policy must be revisited and tightened for production stability.
Architecture Overview
- API proxy pattern: Frontend never calls Django directly. All API calls go through
frontend/src/routes/api/[...path]/+server.ts, which proxies tohttp://server:8000, handles cookies, and injects CSRF behavior. - AI chat: Docked side panel (desktop drawer-end) / slide-over (mobile) on the itinerary page via
AITravelChat.svelte. No standalone/chatroute; no recommendations view. Chat persists across tab switches and page refreshes via localStorage. Permission-gated bycanModifyCollection. Provider list is dynamic from backendGET /api/chat/providers/(sourced from LiteLLM runtime + custom entries likeopencode_zen). Chat conversations use SSE streaming via/api/chat/conversations/. Default AI provider/model saved viaUserAISettingsin DB (authoritative over browser localStorage). LiteLLM errors are mapped to sanitized user-safe messages via_safe_error_payload()(never exposes raw exception text). Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history. Chat agent tools (get_trip_details,add_to_itinerary) respect collection sharing — both owners andshared_withmembers can use them;list_tripsremains owner-only. - Admin-editable system prompt:
ChatSystemPromptsingleton model in Django admin. Admins/hosters can edit the base system prompt at runtime without rebuilding Docker images. Falls back to hardcoded default when no DB row exists. Dynamic user/party preference injection is preserved on top of the admin-editable base. - Service ports:
web→:8015server→:8016db→:5432cache→ internal only
- Authentication: Session-based via
django-allauth; CSRF token from/auth/csrf/; mutating requests sendX-CSRFToken; mobile middleware path supportsX-Session-Token.
Codebase Layout
- Backend:
backend/server/- Apps:
adventures/,users/,worldtravel/,integrations/,achievements/,chat/ - Chat provider config:
backend/server/chat/llm_client.py(CHAT_PROVIDER_CONFIG)
- Apps:
- Frontend:
frontend/src/- Routes:
src/routes/ - Shared types:
src/lib/types.ts(includesChatProviderCatalogEntry) - Components:
src/lib/components/(includesAITravelChat.svelte) - i18n:
src/locales/
- Routes:
Development Commands
Frontend (prefer Bun)
cd frontend && bun run formatcd frontend && bun run lintcd frontend && bun run checkcd frontend && bun run buildcd frontend && bun install
Backend (Docker required; prefer uv for local Python tooling)
docker compose exec server python3 manage.py testdocker compose exec server python3 manage.py migrate
Docker
docker compose up -ddocker compose down
Pre-Commit Checklist
Run in this order:
cd frontend && bun run formatcd frontend && bun run lintcd frontend && bun run checkcd frontend && bun run build
Known Issues (Expected)
- Frontend
bun run check: 0 errors and 0 warnings expected (RegionCard warning resolved) - Backend tests: 6/41 fail (pre-existing: 2 user email key errors + 4 geocoding API mocks; 41 chat tests all pass)
- Docker dev setup has frontend-backend communication issues (500 errors beyond homepage)
Key Patterns
- i18n: use
$t('key')for user-facing strings; new keychat.travel_assistantfor the AI chat panel title - API calls: route through proxy at
/api/[...path]/+server.ts - Styling: use DaisyUI semantic colors/classes (
bg-primary,text-base-content, etc.) - Icons: use
~icons/mdi/pattern (e.g.import ChatIcon from '~icons/mdi/chat-outline'); do NOT uselucide-svelte - Security: handle CSRF tokens via
/auth/csrf/andX-CSRFToken - Chat providers: dynamic catalog from
GET /api/chat/providers/; configured inCHAT_PROVIDER_CONFIG - Chat model override: dropdown selector fed by
GET /api/chat/providers/{provider}/models/; persisted inlocalStoragekeyvoyage_chat_model_prefs; backend accepts optionalmodelparam insend_message - Chat context: collection chats inject collection UUID + multi-stop itinerary context; system prompt guides
get_trip_details-first reasoning and confirms only before firstadd_to_itinerary;search_placeshas a deterministic context-retry fallback — when the LLM omitslocation, the backend retries using the trip destination or first itinerary stop before asking the user for clarification; a dining-intent heuristic inferscategory="food"from user messages when the LLM omits category for restaurant/dining requests itinerary_stopsextraction: whencity/countryFK is null, city is extracted from the last non-numeric segment of a comma-separated address (e.g."Little Turnstile 6, London"→"London"); ensurestrip_context_locationis always a geocodable city name, not a street address.- After itinerary context retries: if all retries fail, result is converted to an execution failure rather than triggering a clarification prompt — the user is never asked for a location they already implied via collection context.
- Chat tool output:
role=toolmessages hidden from display; tool outputs render as concise summaries; persisted tool rows reconstructed on reload viarebuildConversationMessages(); tool results are deduplicated bytool_call_idat three layers — rebuild from persisted rows (discards server-side pre-populatedtool_results), SSE ingestion (appendToolResultDedup), and render-time (uniqueToolResultsByCallId) - Chat error surfacing:
_safe_error_payload()maps LiteLLM exceptions to sanitized user-safe categories (never forwards rawexc.message);execute_tool()catch-all returns a generic sanitized message (never rawstr(exc)) - Invalid tool calls (missing required args) are detected and short-circuited with a user-visible error — not replayed into history
- Tool execution failures (
search_places,web_search, catch-all errors) are classified separately from required-param validation errors; execution failures emit a boundedtool_execution_errorSSE event and stop — they are never replayed into LLM context;tool_iterationsincrements only on successful tool calls; all-failure rounds are capped atMAX_ALL_FAILURE_ROUNDS(3); permanent failures (e.g.web_searchimport error withretryable: false) stop immediately - Geocoding failures in
search_places(Could not geocode location: ...) are eligible for the existing context-retry path (trip destination → first itinerary stop → user clarification) - Geocoding:
background_geocode_and_assign()runs in a thread after Location save; populatesregion,city,country, and also fillsLocation.locationfrom reverse geocodedisplay_name(truncated to field max_length) if blank or different
Conventions
- Do not attempt to fix known test/configuration issues as part of feature work.
- Use
bunfor frontend commands,uvfor local Python tooling where applicable. - Commit and merge completed feature branches promptly once validation passes (avoid leaving finished work unmerged).
.memory Files
- At the start of any task, read
.memory/manifest.yamlto discover available files, then readsystem.mdand relevantknowledge/files for project context. - Read
.memory/decisions.mdfor architectural decisions and review verdicts. - Check relevant files in
.memory/plans/and.memory/research/for prior work on related topics. - These files capture architectural decisions, code review verdicts, security findings, and implementation plans from prior sessions.
- Do not duplicate information from
.memory/into code comments — keep.memory/as the single source of truth for project history.
Instruction File Sync
AGENTS.mdis the single source of truth for repository instructions.- Do not maintain mirrored instruction files for other tools in this repo.