Files
ai-rpg/CHANGELOG.md
2026-06-21 02:41:48 +03:00

20 KiB

Changelog

All notable changes to AI-RPG are documented here. The format follows Keep a Changelog, and this project adheres to Semantic Versioning.

[1.1.0] — 2026-06-21

This is a major bugfix release addressing 20+ issues found during user testing.

Backend — Critical fixes

  • app/api/sessions.py: fixed TypeError: 'async_generator' object does not support the asynchronous context manager protocol that broke ALL SSE streams (world builder, world editor, orchestrator). The _session_scope() function was an async generator (used yield) but was called with async with. Added @asynccontextmanager decorator. Without this fix, no LLM calls were ever made — the background task crashed immediately.
  • app/models/init.py: made WorldPreset.owner_id nullable (NOT NULLNULL, ondelete=CASCADESET NULL). Builtin presets are now system presets with owner_id=NULL. Previously, seed_builtin_presets crashed on first startup with a foreign key violation because no admin user existed yet.
  • app/main.py: added _apply_schema_fixups(engine) that runs ALTER TABLE world_presets ALTER COLUMN owner_id DROP NOT NULL on every startup (idempotent, wrapped in try/except). This fixes existing databases that were created with the old NOT NULL constraint.
  • app/main.py: admin setup URL is now ALWAYS printed on startup (even if admin already exists), per user request. The URL is blocked by the backend if an admin exists, but the token is visible for reference.
  • app/migrations/seed.py: seed_builtin_presets now uses owner_id=None instead of looking for an admin user or a sentinel UUID.
  • requirements.txt: pinned bcrypt==4.0.1. bcrypt 4.1+ removed the __about__ module which breaks passlib 1.7.4's version detection. This caused the annoying (trapped) error reading bcrypt version warning on every password hash operation.

Backend — API improvements

  • app/api/misc.py: added GET /api/settings/public (no auth) — returns {page_title, favicon_url, logo_url, og_image_url}. The frontend uses this on app load to set the document title, favicon, and navbar logo.
  • app/api/worlds.py: GET /api/worlds now excludes archived worlds by default. Pass ?status_filter=archived to see only archived, or ?status_filter=all to see everything.
  • app/api/admin.py: GET /api/admin/stats now returns separate counts: worlds (active), worlds_archived, worlds_total. Previously it counted ALL worlds including archived.
  • app/api/admin.py: added DELETE /api/admin/worlds/{id} — hard-delete a world (cascade deletes entities, steps, logs, triggers, story_entries). Also cleans up Qdrant points (best-effort). This gives admins a way to permanently remove archived worlds.
  • app/core/llm.py: improved error messages for LLM API failures:
    • Non-JSON responses now raise LLMResponseError("LLM provider returned non-JSON response (check that api_url points to an OpenAI-compatible endpoint). First 200 chars: ...") instead of a generic parse_error.
    • HTTP 4xx/5xx errors now include the response body (or extracted error.message from JSON).
    • All error messages now include the HTTP status code for easier debugging.

Frontend — Critical fixes (19 files changed, 1 new)

  • Black page after settings change: root cause was AdminApi.updateSettings returning {updated: ...} but the store expecting a different shape. Fixed the return type and removed the refetch-after-save that caused re-render loops.
  • [object Object] error in chat: added toErrorMessage() helper that properly extracts .message from ApiError objects. All error toasts now show human-readable strings.
  • Play page access control: /worlds/:id/play now redirects to /worlds/:id/edit with a toast if world.status !== 'ready'.
  • Retry/Rollback buttons: disabled when recentSteps.length === 0 with opacity-50 cursor-not-allowed.
  • Archived worlds: hidden from the worlds list (both server-side and client-side filter). After deleting, removed from local state immediately.
  • World card archived state: no Play button, "Archived" badge, "Restore" button (PATCH status→ready), admin-only "Delete permanently" button (DELETE /api/admin/worlds/{id}).
  • LLM logs detail modal: now shows request_messages, response_message, tool_calls, error_message, prompt_tokens, completion_tokens, latency_ms, temperature, model, stage, status (color-coded), created_at — all in pretty-printed JSON <pre> blocks.
  • Dropdowns: embeddings.provider<select> with offline_hash/openai; boolean settings → <select> with true/false; LLM log stage filter → <select> with all 16 stage values; LLM log status filter → <select> with all 6 status values.
  • Numeric inputs: all integer/float settings now use <input type="number"> with min/max attributes. String-to-number conversion on save.
  • Test page pre-fills: diagnostic forms now pre-fill from current settings (llm.api_url, llm.api_key, llm.model, embeddings.api_url, embeddings.model, embeddings.provider).
  • Embeddings test provider: defaults to current embeddings.provider setting (was hardcoded to openai). Added hint: "If provider=openai and api_url is empty, falls back to llm.api_url".
  • Embeddings URL fallback hints: added muted hint text next to embeddings.api_url, embeddings.api_key, embeddings.model fields.
  • UI settings applied: new uiSettingsStore fetches GET /api/settings/public on app load. Sets document.title, favicon <link>, and navbar logo. Re-fetches after admin settings save.
  • Light theme fix: rewrote CSS with CSS-variable-based theming. Light mode now uses #fafafa/#1a1a1a/#ffffff with high-contrast text. Dark mode unchanged.
  • Single Create button: removed redundant "Create World" from navbar. Only one Create button remains (top of worlds list page).
  • Username validation: client-side regex check + 422 error parsing with field-level inline messages. Specific toast: "Username can only contain letters, numbers, and underscores".
  • SSE error handling: sessionStore now shows toast on SSE error events. One-shot "Connection lost. Retrying…" toast on unexpected stream closure.
  • Settings panel UX: 6 grouped cards (LLM / Embeddings / Qdrant / Context / Game / UI), each with its own Save button. No page reload or full refetch after save — just a success toast.
  • Admin stats panel: now shows 6 cards: Users, Active Worlds, Archived, Total Worlds, Steps, Avg LLM Latency.

Verification

  • Backend: 68 unit tests pass.
  • Frontend: tsc --noEmit → 0 errors. npm run build → success (354 KB JS / 23 KB CSS, ~109 KB gzipped).

[1.0.5] — 2026-06-20

Fixed

  • app/main.py: added create_all_tables(engine) call at the start of lifespan. Without this, the backend started but crashed on every DB query with relation "settings" does not exist because tables were never created in PostgreSQL. Tables are now created idempotently on every startup via Base.metadata.create_all ( SQLAlchemy skips tables that already exist).
  • app/main.py: also added seed_builtin_presets(session) call so the 2 builtin presets (Classic Fantasy, Deep Space Outpost) are seeded on first startup, not just settings.
  • app/migrations/versions/: renamed 001_initial_schema.py_001_initial_schema.py. Python module names cannot start with a digit, so from app.migrations.versions.001_initial_schema import create_all_tables was a SyntaxError. The leading underscore is a conventional marker for "internal" modules.
  • README.md: updated references to the renamed migration file.

Why this happened

The lifespan handler was supposed to run DB migrations as its first step, but I forgot to wire it up. The seed_default_settings() call immediately tried to SELECT FROM settings against a fresh PostgreSQL database with no tables. SQLAlchemy 2.x's create_all is idempotent (skips existing tables), so this is safe to call on every startup — equivalent to alembic upgrade head for our single-migration MVP.

[1.0.4] — 2026-06-20

Fixed (proper fix for CORS env parsing)

  • app/config.py: replaced the broken Annotated[list[str], NoDecode] approach with a simpler, more robust one:
    • cors_origins is now declared as a plain str (comma-separated, e.g. "http://a,http://b" or "*").
    • Added a cors_origins_list property that splits the string into a list[str] on demand.
    • This sidesteps the entire EnvSettingsSource.decode_complex_value / JSON-parsing codepath — pydantic-settings sees a str field, takes the env value as-is, no JSON parsing attempted.
  • app/main.py: updated CORSMiddleware(allow_origins=cfg.cors_origins_list) to use the new property.
  • Why v1.0.3 didn't work: in pydantic-settings 2.7.0, NoDecode is importable but _annotation_is_complex() doesn't actually check for it (only checks for Json). The marker was added to the package surface but the inner logic was only wired up in a later version. Switching to a plain str field is the most reliable fix and works across all pydantic-settings 2.x versions.
  • All 68 unit tests still pass.

[1.0.3] — 2026-06-20

Fixed

  • app/config.py: fixed SettingsError: error parsing value for field "cors_origins" from source "EnvSettingsSource" that crashed the backend on startup when CORS_ORIGINS was set as a comma-separated string (e.g. http://localhost:8080,http://localhost:5173,http://localhost).
    • Root cause: pydantic-settings v2 by default tries to JSON-parse complex-typed env vars before applying field validators. The comma-separated string isn't valid JSON, so parsing failed before our @field_validator(mode="before") could split it.
    • Fix: declared cors_origins: Annotated[list[str], NoDecode]NoDecode is a pydantic-settings marker that disables JSON pre-parsing, so the raw string reaches our _split_cors validator unchanged.
  • requirements.txt: bumped pydantic 2.7.1 → 2.9.2 and pydantic-settings 2.2.1 → 2.7.0. The NoDecode annotation was only introduced in pydantic-settings 2.6+, so the older versions couldn't support the fix above. All 68 unit tests still pass with the new versions.

[1.0.2] — 2026-06-20

Fixed

  • requirements.txt: added missing email-validator==2.2.0 dependency. Pydantic's EmailStr type (used in RegisterRequest, AdminRegisterRequest, LoginRequest.user.email, UserPublic.email, TokenResponse.user.email) requires this package at runtime, but it's not bundled with pydantic itself. Without it the backend crashed at startup with ImportError: email-validator is not installed, run pip install pydantic[email]. Also removed a duplicate httpx==0.27.0 line.

[1.0.1] — 2026-06-20

Fixed

  • docker-compose.yml: removed obsolete version: "3.9" (caused warning in modern Docker Compose).
  • docker-compose.yml: fixed frontend build context — was ./frontend (broke COPY deploy/nginx.conf and COPY frontend/package*.json in Dockerfile.frontend). Now correctly . (project root) with dockerfile: deploy/Dockerfile.frontend.
  • docker-compose.yml: fixed frontend port mapping — was 5173:5173 but the frontend container is nginx on port 80. Now 8080:80 so the app is accessible at http://localhost:8080.
  • docker-compose.yml: added extra_hosts: ["host.docker.internal:host-gateway"] to the backend service — enables LLM_API_URL=http://host.docker.internal:11434/v1 to work on Linux hosts (not just Docker Desktop on Mac/Windows).
  • docker-compose.yml: added VITE_API_BASE_URL: /api build arg for the frontend service — bakes the relative /api URL into the Vite bundle so the browser uses the same origin + nginx proxies /apibackend:8000.
  • deploy/Dockerfile.frontend: added ARG VITE_API_BASE_URL=/api + ENV so the build arg actually gets baked into the Vite bundle.
  • deploy/nginx.conf: extended SSE timeouts from 300s → 600s; added proxy_send_timeout; added Upgrade/Connection headers for future WebSocket support; added gzip for static assets.
  • .env.example: clarified VITE_API_BASE_URL — only used by local npm run dev, ignored by docker build (which uses /api relative). Removed the misleading http://localhost/api default.
  • frontend/src/lib/api.ts: now respects VITE_API_BASE_URL env var with fallback to relative /api. Works both for local dev (point at separate backend) and Docker (nginx proxy).
  • frontend/src/vite-env.d.ts: added Vite env type declarations so TypeScript knows about import.meta.env.VITE_API_BASE_URL.
  • app/api/deps.py: added ?access_token=<jwt> query parameter fallback for SSE endpoints. Native EventSource cannot send Authorization headers, so the frontend SSE client passes the token via query string. Without this fix, all SSE endpoints (/iterate/stream, /builder/stream, /editor/stream) returned 401.
  • .dockerignore: added at project root — excludes node_modules/, __pycache__/, .venv/, data/, .git/, etc. from Docker build contexts (faster builds, smaller context transfer).

[1.0.0] — 2026-06-20

Added — Sprint 1: Foundation

  • docker-compose with 4 services: db (PostgreSQL 15), qdrant (1.9), backend (FastAPI/uvicorn), frontend (Vite dev / nginx prod).
  • SQLAlchemy 2.x async models for all 10 tables: users, settings, world_presets, worlds, entities, steps, step_tool_calls, deferred_triggers, story_entries, llm_call_logs.
  • Alembic-equivalent idempotent migration 001_initial_schema.py (creates all tables, no pgvector).
  • init_qdrant.py creates collections entities and story_entries with payload indexes on world_id, entity_type, entry_type, deleted, created_at.
  • Settings seed: 33 default keys (llm., embeddings., context., qdrant., game., ui., admin.setup_token).
  • 2 builtin presets: Classic Fantasy, Deep Space Outpost.
  • GET /api/health returns {status, db, qdrant, llm, embeddings, version}.

Added — Sprint 2: Auth + Admin

  • POST /api/register (only if at least one admin exists), POST /api/register/admin?token=, POST /api/auth/login (email or username), POST /api/auth/refresh, POST /api/auth/logout, GET /api/auth/me.
  • JWT (access + refresh) with python-jose, password hashing with passlib[bcrypt].
  • Password strength validation: ≥8 chars, ≥1 letter, ≥1 digit, blacklist of trivial passwords.
  • Anti-enumeration: same 401 invalid_credentials for "user not found" and "wrong password".
  • Admin setup token: auto-generated on first startup, printed in logs.
  • GET/PATCH /api/admin/settings with secret masking.
  • GET /api/admin/llm-logs with filters (world_id, stage, status, pagination) + detail view.
  • GET /api/admin/users, PATCH /api/admin/users/{id} (admin/active toggle).
  • GET /api/admin/stats (users, worlds, steps, avg LLM latency).
  • Diagnostic endpoints: POST /api/admin/test/llm, /test/llm-tools, /test/embeddings, /test/embeddings/probe-dimension, /embeddings/recreate-collections.
  • POST /api/admin/upload-icon (multipart, favicon/logo/og_image, ≤1MB, PNG/SVG/JPG/WebP/ICO).

Added — Sprint 3: World Builder

  • LlmClient — OpenAI-compatible chat completions client with retry (3 attempts, exp backoff), streaming, tool-calling, separate-transaction logging.
  • MockLlmClient — replay-based mock for tests/dev when no LLM configured.
  • app/prompts/ — 11 stage prompts in English (world_builder_schema/env/entities, world_editor, orchestrator_phase1/2/3_summary/3_suggest, intro_scene, subagent, summary).
  • POST /api/worlds creates draft world + returns SSE URL.
  • GET /api/sessions/worlds/{id}/builder/stream runs the 4-step builder flow: schemas → environment → entities (tool-calling loop) → intro scene (with submit_step + suggest_actions).
  • If preset_id provided, schemas/environment copied from preset (skip first LLM call).

Added — Sprint 4: World Editor

  • POST /api/worlds/{id}/edit body {instruction} → SSE URL.
  • GET /api/sessions/worlds/{id}/editor/stream?instruction= runs LLM with world_editor tools.
  • propose_changes tool returns diff, ask_user emits clarification event, comment_to_user for chat.
  • Optimistic locking via updated_at on PATCH /api/worlds/{id} (409 state_conflict on mismatch).
  • PATCH /api/worlds/{id} for direct JSON edits.
  • DELETE /api/worlds/{id} soft-delete (status='archived').

Added — Sprint 5: Orchestrator

  • POST /api/sessions/worlds/{id}/iterate body {action, action_source} creates new step + returns SSE URL.
  • GET /api/sessions/worlds/{id}/iterate/stream?step_id= runs the 3-phase orchestrator:
    • Phase 1: tool-calling loop with submit_plan terminator, max 8 substeps (configurable).
    • Phase 2: writer LLM call with submit_step, streams scene_chunk events.
    • Phase 3: persist + deferred triggers + summary (if history > threshold) + suggest_actions.
  • POST /api/sessions/worlds/{id}/retry soft-deletes last step + creates new one with same action.
  • POST /api/sessions/worlds/{id}/rollback soft-deletes last step.
  • 17 game tools: entity_create/get/list/update/delete, env_update/get, update_plot_rails, advance_time, schedule_trigger, calc (with dice), random_choice (deterministic), rag_query/add, run_subagent, submit_plan, submit_step, suggest_actions.
  • 4 schema tools: schema_add_type/add_field/remove_field/modify_field.

Added — Sprint 6: Frontend polish

  • React 18 + Vite 5 + TypeScript 5 + Tailwind CSS 3 + zustand 4 + react-i18next 14.
  • Dark theme by default (Tailwind dark: class on <html>).
  • 8 pages: Login, Register, AdminRegister, WorldsList, WorldBuilder, WorldEdit, Play, Admin.
  • 23 components: 9 UI primitives, 5 session, 3 worlds, 6 admin.
  • 5 zustand stores: auth, ui, worlds, session, toast.
  • i18n bundles in English + Russian (complete).
  • Native EventSource SSE client with reconnect + Last-Event-ID.
  • Mobile-first responsive layout.
  • TypeScript strict mode, 0 type errors, npm run build succeeds (340 KB JS / 22 KB CSS).

Added — Sprint 7: RAG + Triggers + Summary + Context

  • app/core/rag.py — two-stage retrieval: Qdrant vector search → PostgreSQL hydration.
  • HashEmbedder (offline, deterministic) and OpenAIEmbedder (OpenAI-compatible API) implementations.
  • Embedder provider selection via embeddings.provider setting (offline_hash | openai).
  • Auto-fallback: embeddings.api_url falls back to llm.api_url if empty (and same for api_key).
  • Embedder cache with reset_embedder_cache() (called on settings update).
  • rag_query returns empty list on embedder/Qdrant failure (non-blocking).
  • rag_add saves story entry with embedding_status='pending' if embedding fails (background indexer can retry).
  • index_entity helper for entity vectors.
  • World isolation via Qdrant payload filter world_id.
  • _cleanup_qdrant(world_id) deletes all points for a world on world delete (best-effort).
  • Deferred triggers: Phase 3.1 fires triggers where fire_at <= current_time, appends summary to scene_text, marks is_fired=true.
  • Summary: Phase 3.2 generates summary when len(recent_steps) > compression_threshold_messages, stores as StoryEntry with metadata.type=summary.
  • Context manager: builds messages with system prompt + optional summary + last N guaranteed messages + current action.

Added — Sprint 8: Production

  • README.md with quickstart, architecture diagram, API overview, testing instructions.
  • pytest.ini + 65+ unit tests across 8 test files.
  • CHANGELOG.md (this file).
  • docker-compose.yml with healthchecks for db and qdrant.
  • deploy/Dockerfile.backend, deploy/Dockerfile.frontend, deploy/nginx.conf.
  • .env.example with all 24 env vars documented.
  • .gitignore for Python, Node, env, IDE, data dirs.
  • Frontend production build verified (npm run build produces dist/).