Files
ai-rpg/CHANGELOG.md

194 lines
20 KiB
Markdown
Raw Normal View History

2026-06-20 19:13:05 +03:00
# Changelog
All notable changes to AI-RPG are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2026-06-21 02:41:48 +03:00
## [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 NULL``NULL`, `ondelete=CASCADE``SET 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).
2026-06-21 01:02:25 +03:00
## [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.
2026-06-21 00:42:29 +03:00
## [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.
2026-06-21 00:36:02 +03:00
## [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.
2026-06-21 00:23:48 +03:00
## [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.
2026-06-20 22:21:47 +03:00
## [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 `/api``backend: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).
2026-06-20 19:13:05 +03:00
## [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/`).