36 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.4.0] — 2026-06-21
Major UX release: redirect to edit after world creation, environment panel, admin separate routes, collapsible settings, name bank, page titles.
Backend — Critical fixes
- KeyError in world_builder_schema prompt: the prompt contained a literal
{name, type, required, min, max, properties}brace group thatstr.format()tried to interpret as a format field. Rewrote the prompt to describe field properties in prose. All 11 prompts verified to format correctly. - propose_changes not applying world.name / world.description:
apply_diffonly handledenvironment.*paths. Now supports shorthand paths:name,description,language,player.*,current_location,plot_rails.*,schemas. Also skips empty{}new values (model sometimes returns empty objects). Also syncsworld.plot_railscolumn from environment. - action_source validation error:
IterateRequest.action_sourcewasLiteral["custom", "suggested"]which rejected any other string. Changed tostrwith default "custom" — accepts any value. - Tool-call retry with nudge: when the model returns no tool calls, the loop now appends a system message: "You did not call any tools. You MUST use the available tools. If you tried to call a tool but it didn't work, try again with proper JSON arguments." and retries (up to max_substeps).
- Text replacements applied earlier: now applied to LLM content in the tool loop (not just scene_text), so reasoning/comments are also cleaned before being shown to the user or fed back to the model.
- Language instruction in prompts: added "Language rule" section to
orchestrator_phase1,world_builder_schema, and other prompts: "All entity names, location names, character names, item names, and descriptions that the PLAYER will see MUST be in{language}. Internal reasoning and tool arguments stay in English." - World description from preset: when creating a world from a preset,
world.descriptionis now set frompreset.description(was usingbody.notesor null). - Schema
showfield:world_builder_schemaprompt now instructs the LLM to include ashowboolean on each field (default true; if false, the field is hidden from the player UI).
Backend — New settings & endpoints
ui.header_titlesetting: separate title for the navbar header. If empty, falls back toui.page_title. Returned inGET /api/settings/publicasheader_title.character_names.en/character_names.rusettings: name banks for the random name button. Each is a JSON array of ~20 names. Extensible via admin settings.GET /api/names/{language}(no auth) — returns{name: "random name", language, count}. Used by the world builder form's 🎲 button.
Frontend — 16 files changed
- Redirect to edit after world creation: WorldBuilder now redirects to
/worlds/{id}/edit(not/play) after the builder stream completes. The user manually clicks "Generate intro scene" then "Play". - Play page — intro_scene + suggested actions: if no recent_steps but
world.intro_sceneexists, it's shown as the first assistant message.next_actionsshown as clickable buttons. Empty states with links to edit page. - Environment panel: shows Player (name, HP progress bar, mana/strength, inventory), Current Location, Plot Rails (hooks, current_goals, completed_goals). "No environment data" empty state.
- Edit world button: ⚙️ button in PlayPage header → links to
/worlds/{id}/edit. - Admin separate routes:
/admin/stats,/admin/logs,/admin/users,/admin/settings,/admin/test,/admin/icons./adminredirects to/admin/stats. Page reload preserves the current tab. - LLM logs auto-refresh: polls every 5 seconds (first page only, paused when filter inputs are focused). Pause/Resume button. "N new" badge when new logs arrive.
- Collapsible settings cards: all 7 cards (LLM, Embeddings, Qdrant, Context, Game, UI, Text Replacements) are collapsible, default collapsed. Chevron icon (▶/▼). "Expand all" / "Collapse all" buttons.
- Boolean settings → checkboxes: replaced dropdowns with
<input type="checkbox">for all boolean settings. - Localized setting descriptions: all ~36 setting descriptions now have translation keys (
admin.setting_desc.{key}). Also localized the test page hints and the "Each card saves independently..." text. - Page title: PlayPage and WorldEditPage set
document.title = "{world.name} | {headerTitle}". Navbar usesheader_titlefrom/api/settings/public. - World builder form simplified: removed
form_dataJSON. Now has: Setting (textarea), World name (default "New World" / "Новый Мир"), Player name (text + 🎲 random button), Language (select), Notes (textarea). Preset mode hides Setting field, defaults world name to preset name. - Name bank random button: 🎲 button next to player name. Calls
GET /api/names/{language}and fills the field. Disabled while fetching. - World edit — current_time_human: shows "Day 1, 08:00" instead of "day_1_hour_8".
- World editor — comment + tool_call display:
commentSSE events shown as assistant chat bubbles.tool_callevents shown as ToolCallBubble components in the chat log.
Verification
- Backend: 68 unit tests pass, 53 routes.
- Frontend:
tsc --noEmit→ 0 errors.npm run build→ success (393 KB JS / 24 KB CSS, ~119 KB gzipped).
[1.3.0] — 2026-06-21
Major release: world builder rewritten to use tools (instead of JSON), resumable builder flow, admin recovery, LLM model list, text replacements, human-readable time.
Backend — Critical: World Builder rewritten to use tools
world_builder_schemastage: previously asked the LLM to output a JSON object with schemas, which frequently failed validation. Now the LLM callsschema_add_typetool for each entity type. Addedworld_builder_schemaandworld_builder_envto thestagesset of all relevant tools (schema_add_type, env_update, entity_create, submit_plan, etc.).world_builder_envstage: previously asked for JSON. Now the LLM callsenv_updatetool to set player, current_location, and plot_rails.- Resumable builder: each stage checks if the world already has the needed data and skips if so. If schemas exist → skip schema generation. If environment has current_location → skip env generation. If entities exist → skip entity generation. If intro_scene exists → skip intro generation. This allows re-running the builder after a failure at any stage without redoing earlier stages.
- Validation warnings instead of failures: if
validate_worldfinds issues after env generation, the builder emits awarningSSE event but continues (instead of failing). The world may still be usable. - Better tool-loop prompt: the user message now says "Use the available tools to accomplish the task. When done, call {terminal_tool}." to encourage tool use.
- Updated prompts:
world_builder_schemaandworld_builder_envprompts now describe the tools to use and give examples of tool arguments.
Backend — Critical: World Editor fixes
- No extra LLM call after propose_changes: after the user accepts or rejects proposed changes, the editor loop now breaks immediately. Previously it made another LLM call (which returned empty text), wasting API requests.
'str' object has no attribute 'get'fix: already in v1.2.0, but now also handles cases wherefunctionis not a dict.
Backend — New: Admin recovery
POST /api/admin/recover(NO AUTH required) — creates a new admin user using theadmin.setup_token(printed on every backend startup). Body:{token, email, username, password}. For disaster recovery when all existing admins lost access. Returns 403invalid_admin_tokenif the token doesn't match.
Backend — New: LLM model list
POST /api/admin/llm/models(admin) — fetches the list of available models from an OpenAI-compatible API (GET {api_url}/models). Returns{ok: true, models: [...], count: N}or{ok: false, error: {...}, models: []}. Uses the same_resolvehelper as test endpoints (ignores masked api_key values).
Backend — New: Text replacements
- New setting
llm.text_replacements: a JSON array of{from: string, to: string}pairs. Applied to all LLM scene_text output (both orchestrator Phase 2 and intro_scene). Use emptytoto remove a word/phrase entirely. apply_text_replacements(session, text)helper insettings_service.py.- Applied in
game_master.py(Phase 2 writer) andworld_builder.py(intro scene).
Backend — New: Human-readable time
format_time_human(time_str, language)intime_utils.py— converts"day_1_hour_8"→"Day 1, 08:00"(en) or"День 1, 08:00"(ru). Supports years, days, hours, minutes.GET /api/worldsnow returnscurrent_time_humanalongsidecurrent_time.GET /api/sessions/worlds/{id}/statenow returnscurrent_time_humanandstatusin the world object.
Backend — New: world_id in LLM logs
GET /api/admin/llm-logsnow includesworld_id(string UUID or null) on each log item. Useful for the admin UI to show which world a log belongs to, even when not filtering by world_id.
Backend — Route fix
- 404 on generate-intro: the frontend was calling
/api/worlds/{id}/generate-introbut the route is at/api/sessions/worlds/{id}/generate-intro. Fixed the frontend API helper to use the correct path.
Frontend — 14 files changed, 1 new
- Admin recovery page: new
/recoverroute (public, no auth). Form with token/email/username/password. Link from LoginPage: "Lost admin access? Recover here". - LLM model list dropdown: "Fetch models" button next to the model input in SettingsPanel. Fetches from
POST /api/admin/llm/models. Shows a<select>dropdown on success, or a "Could not fetch" hint on failure. Cached in component state. - Text replacements UI: new card in SettingsPanel with add/remove rows for
{from, to}pairs. Saved tollm.text_replacementssetting. - World column in LLM logs table: shows first 8 chars of
world_id(or "—"). Filter indicator in header. Fullworld_idin detail modal. - Human-readable time:
current_time_humanused in WorldCard, PlayPage, WorldEditPage. Falls back tocurrent_timeif not available. - World builder skipped stages:
skipping_*step events shown in muted blue color. - IntroSceneGenerator visibility: shown for draft worlds OR worlds with empty intro_scene (not just stuck worlds).
Verification
- Backend: 68 unit tests pass, 52 routes.
- Frontend:
tsc --noEmit→ 0 errors.npm run build→ success (378 KB JS / 23 KB CSS, ~114 KB gzipped).
[1.2.0] — 2026-06-21
This release fixes critical bugs that prevented world creation, world editing, and LLM tool-calling with local models (gemma, qwen, etc.).
Backend — Critical fixes
- Prompt templates: fixed
KeyError: 'scene_text, delta_time'that crashed world_builder during intro scene generation. The prompts contained literal{scene_text, delta_time}braces whichstr.format()interpreted as format fields. Rewroteorchestrator_phase2andintro_sceneprompts to describe the tool arguments in prose instead of using brace notation. All 11 prompt templates now format correctly (verified with a test). - LLM tool-call parsing for local models: many local models (gemma4, qwen, etc.) don't use the OpenAI function-calling format — they emit tool calls as text like
call:calc{"expression": "2+2"}or<tool_call>calc{...}</tool_call>. Added_parse_text_tool_calls()toapp/core/llm.pythat detects these patterns and converts them to OpenAI-formattool_calls. The LLM client now automatically parses text-based tool calls when the model doesn't return them in the standard format. This fixes the issue whererequest_toolswas[](empty) in logs even though tools were sent — actually tools WERE sent, but the model returned calls as text and they were ignored. - Test endpoints — ascii codec error:
POST /api/admin/test/llm-toolsfailed with'ascii' codec can't encode character '\u2026'when the client sent a masked api_key (containing…) back as a query parameter. Added_is_masked()and_resolve()helpers that detect masked values (…or****) and fall back to the raw DB value. All 4 test endpoints (test/llm, test/llm-tools, test/embeddings, test/embeddings/probe-dimension) now use these helpers. - Test LLM tools — better prompt: the test now sends a system message "You must use the calc tool" and a user message "You MUST call the calc tool with expression '2+2'" to encourage tool use. Also increased timeout from 15s to 30s. The response now includes
raw_response(the full LLM message) for debugging. - World editor —
'str' object has no attribute 'get': the world_editor crashed when tool_calls contained string entries instead of dicts (some models return non-standard formats). Added type normalization: each tool_call is checked withisinstance(tc, dict), strings are parsed as JSON, non-dicts are skipped. Also handles cases wherefunctionis not a dict. - World editor — propose_changes now waits for user: previously
propose_changeswas auto-accepted (simplified). Now the editor emitschange_proposedand WAITS for the user to accept/reject viaPOST /api/sessions/worlds/{id}/applyor/discard. Implemented usingasyncio.Futurestored in_pending_changesdict keyed by world_id. 120s timeout. - World editor — ask_user now waits for answer: similarly,
ask_usernow waits forPOST /api/sessions/worlds/{id}/answerbody{text: "..."}. 120s timeout. - World editor — apply_diff improved: now supports paths
world.name,world.description,schemas(full replace), andenvironment.<field>(via apply_patch). Previously onlyenvironment.*paths worked. - World editor — better empty-state handling: when world has no schemas/entities/environment, the prompt now shows "(no schemas yet)", "(no entities yet)", and "(empty — world has no environment yet. Use env_update to add player, current_location, plot_rails.)" instead of empty strings, so the LLM understands the context.
Backend — New endpoints
POST /api/sessions/worlds/{id}/apply— accept proposed changes (resolves the pending Future)POST /api/sessions/worlds/{id}/discard— reject proposed changesPOST /api/sessions/worlds/{id}/answerbody{text: "..."}— answer a clarificationPOST /api/worlds/{id}/generate-intro→{stream_url}— re-generate intro scene for draft worldsGET /api/sessions/worlds/{id}/intro/stream(SSE) — runs the intro_scene stage, sets world.status to "ready" on success. Emitsstep,intro_scene_complete,doneevents.
Frontend — Critical fixes (10 files changed, 1 new)
- World Editor — Accept/Reject buttons:
change_proposedevents now show a card with the diff (color-coded: green=add, red=remove, yellow=replace) and two buttons. Accept →POST /apply, Reject →POST /discard. After decision, the card collapses to a status line. - World Editor — Answer input:
clarificationevents show a card with either free-text input + Send button, or clickable option buttons (ifoptionsprovided). Submit →POST /answer. - World Edit Page — Generate Intro Scene: new
IntroSceneGeneratorcomponent. Ifworld.status === 'draft', shows a "Generate Intro Scene" button. Clicking it opens the SSE stream, shows progress, displays the generated scene, and ondonerefreshes the world (status → "ready") + shows toast "World is ready!". - Settings panel — autocomplete="off": all inputs now have
autoComplete="off". API key fields usetype="text"(browsers won't save them as passwords). Added a hidden decoy password input to absorb the password manager's attention. - Username/email autocomplete: verified
LoginPageusesautoComplete="username"for the login field,RegisterPageusesautoComplete="email"for email andautoComplete="username"for username. - Test LLM Tools — raw response display: when
has_tool_calls=false, shows a warning + collapsible<details>with the raw LLM response so the user can see what the model returned. - World Builder — Retry button: on error, shows a "Retry" button that re-subscribes to the builder stream. Also fixed a stale-closure bug where
done/errorevents didn't close the active SSE controller (useduseRefto track the current controller). - World Card — draft state: draft worlds show a "Continue setup" button (links to edit page) and a "Draft" badge. No "Play" button for drafts.
- Play page — better not-ready toast: "This world is not ready yet. Generate the intro scene first."
Verification
- Backend: 68 unit tests pass, 50 routes.
- Frontend:
tsc --noEmit→ 0 errors.npm run build→ success (368 KB JS / 23 KB CSS, ~112 KB gzipped).
[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 protocolthat broke ALL SSE streams (world builder, world editor, orchestrator). The_session_scope()function was an async generator (usedyield) but was called withasync with. Added@asynccontextmanagerdecorator. Without this fix, no LLM calls were ever made — the background task crashed immediately. - app/models/init.py: made
WorldPreset.owner_idnullable (NOT NULL→NULL,ondelete=CASCADE→SET NULL). Builtin presets are now system presets withowner_id=NULL. Previously,seed_builtin_presetscrashed on first startup with a foreign key violation because no admin user existed yet. - app/main.py: added
_apply_schema_fixups(engine)that runsALTER TABLE world_presets ALTER COLUMN owner_id DROP NOT NULLon 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_presetsnow usesowner_id=Noneinstead 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 versionwarning 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/worldsnow excludes archived worlds by default. Pass?status_filter=archivedto see only archived, or?status_filter=allto see everything. - app/api/admin.py:
GET /api/admin/statsnow 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 genericparse_error. - HTTP 4xx/5xx errors now include the response body (or extracted
error.messagefrom JSON). - All error messages now include the HTTP status code for easier debugging.
- Non-JSON responses now raise
Frontend — Critical fixes (19 files changed, 1 new)
- Black page after settings change: root cause was
AdminApi.updateSettingsreturning{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.messagefromApiErrorobjects. All error toasts now show human-readable strings. - Play page access control:
/worlds/:id/playnow redirects to/worlds/:id/editwith a toast ifworld.status !== 'ready'. - Retry/Rollback buttons: disabled when
recentSteps.length === 0withopacity-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 (
PATCHstatus→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>withoffline_hash/openai; boolean settings →<select>withtrue/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">withmin/maxattributes. 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.providersetting (was hardcoded toopenai). 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.modelfields. - UI settings applied: new
uiSettingsStorefetchesGET /api/settings/publicon app load. Setsdocument.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/#ffffffwith 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:
sessionStorenow shows toast on SSEerrorevents. 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 oflifespan. Without this, the backend started but crashed on every DB query withrelation "settings" does not existbecause tables were never created in PostgreSQL. Tables are now created idempotently on every startup viaBase.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, sofrom app.migrations.versions.001_initial_schema import create_all_tableswas 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_originsis now declared as a plainstr(comma-separated, e.g."http://a,http://b"or"*").- Added a
cors_origins_listproperty that splits the string into alist[str]on demand. - This sidesteps the entire
EnvSettingsSource.decode_complex_value/ JSON-parsing codepath — pydantic-settings sees astrfield, 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,
NoDecodeis importable but_annotation_is_complex()doesn't actually check for it (only checks forJson). The marker was added to the package surface but the inner logic was only wired up in a later version. Switching to a plainstrfield 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 whenCORS_ORIGINSwas 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]—NoDecodeis a pydantic-settings marker that disables JSON pre-parsing, so the raw string reaches our_split_corsvalidator unchanged.
- 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
- requirements.txt: bumped
pydantic2.7.1 → 2.9.2 andpydantic-settings2.2.1 → 2.7.0. TheNoDecodeannotation 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.0dependency. Pydantic'sEmailStrtype (used inRegisterRequest,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 withImportError: email-validator is not installed, run pip install pydantic[email]. Also removed a duplicatehttpx==0.27.0line.
[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(brokeCOPY deploy/nginx.confandCOPY frontend/package*.jsonin Dockerfile.frontend). Now correctly.(project root) withdockerfile: deploy/Dockerfile.frontend. - docker-compose.yml: fixed frontend port mapping — was
5173:5173but the frontend container is nginx on port 80. Now8080:80so the app is accessible athttp://localhost:8080. - docker-compose.yml: added
extra_hosts: ["host.docker.internal:host-gateway"]to the backend service — enablesLLM_API_URL=http://host.docker.internal:11434/v1to work on Linux hosts (not just Docker Desktop on Mac/Windows). - docker-compose.yml: added
VITE_API_BASE_URL: /apibuild arg for the frontend service — bakes the relative/apiURL 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+ENVso the build arg actually gets baked into the Vite bundle. - deploy/nginx.conf: extended SSE timeouts from 300s → 600s; added
proxy_send_timeout; addedUpgrade/Connectionheaders for future WebSocket support; added gzip for static assets. - .env.example: clarified
VITE_API_BASE_URL— only used by localnpm run dev, ignored by docker build (which uses/apirelative). Removed the misleadinghttp://localhost/apidefault. - frontend/src/lib/api.ts: now respects
VITE_API_BASE_URLenv 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. NativeEventSourcecannot sendAuthorizationheaders, 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.pycreates collectionsentitiesandstory_entrieswith payload indexes onworld_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/healthreturns{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_credentialsfor "user not found" and "wrong password". - Admin setup token: auto-generated on first startup, printed in logs.
GET/PATCH /api/admin/settingswith secret masking.GET /api/admin/llm-logswith 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/worldscreates draft world + returns SSE URL.GET /api/sessions/worlds/{id}/builder/streamruns 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}/editbody{instruction}→ SSE URL.GET /api/sessions/worlds/{id}/editor/stream?instruction=runs LLM with world_editor tools.propose_changestool returns diff,ask_useremits clarification event,comment_to_userfor chat.- Optimistic locking via
updated_atonPATCH /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}/iteratebody{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_planterminator, max 8 substeps (configurable). - Phase 2: writer LLM call with
submit_step, streamsscene_chunkevents. - Phase 3: persist + deferred triggers + summary (if history > threshold) + suggest_actions.
- Phase 1: tool-calling loop with
POST /api/sessions/worlds/{id}/retrysoft-deletes last step + creates new one with same action.POST /api/sessions/worlds/{id}/rollbacksoft-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 buildsucceeds (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) andOpenAIEmbedder(OpenAI-compatible API) implementations.- Embedder provider selection via
embeddings.providersetting (offline_hash|openai). - Auto-fallback:
embeddings.api_urlfalls back tollm.api_urlif empty (and same for api_key). - Embedder cache with
reset_embedder_cache()(called on settings update). rag_queryreturns empty list on embedder/Qdrant failure (non-blocking).rag_addsaves story entry withembedding_status='pending'if embedding fails (background indexer can retry).index_entityhelper 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, marksis_fired=true. - Summary: Phase 3.2 generates summary when
len(recent_steps) > compression_threshold_messages, stores as StoryEntry withmetadata.type=summary. - Context manager: builds messages with system prompt + optional summary + last N guaranteed messages + current action.
Added — Sprint 8: Production
README.mdwith quickstart, architecture diagram, API overview, testing instructions.pytest.ini+ 65+ unit tests across 8 test files.CHANGELOG.md(this file).docker-compose.ymlwith healthchecks for db and qdrant.deploy/Dockerfile.backend,deploy/Dockerfile.frontend,deploy/nginx.conf..env.examplewith all 24 env vars documented..gitignorefor Python, Node, env, IDE, data dirs.- Frontend production build verified (
npm run buildproducesdist/).