fix
This commit is contained in:
@@ -68,14 +68,36 @@ async def public_settings(db: AsyncSession = Depends(get_db)) -> dict:
|
||||
favicon_url = await get_setting(db, "ui.favicon_url")
|
||||
logo_url = await get_setting(db, "ui.logo_url")
|
||||
og_image_url = await get_setting(db, "ui.og_image_url")
|
||||
header_title = await get_setting(db, "ui.header_title")
|
||||
return {
|
||||
"page_title": page_title or "AI-RPG",
|
||||
"favicon_url": favicon_url or "/icon.png",
|
||||
"logo_url": logo_url or "/icon.png",
|
||||
"og_image_url": og_image_url or "",
|
||||
"header_title": header_title or page_title or "AI-RPG",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/names/{language}")
|
||||
async def get_name_bank(
|
||||
language: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Return a random character name for the given language.
|
||||
|
||||
No auth required — used by the world builder form's "random name" button.
|
||||
"""
|
||||
import random
|
||||
|
||||
key = f"character_names.{language}"
|
||||
names = await get_setting(db, key)
|
||||
if not names or not isinstance(names, list):
|
||||
# Fallback to English
|
||||
names = await get_setting(db, "character_names.en") or ["Hero"]
|
||||
pick = random.choice(names) if names else "Hero"
|
||||
return {"name": pick, "language": language, "count": len(names)}
|
||||
|
||||
|
||||
@router.get("/i18n/{lang}")
|
||||
async def i18n(lang: str) -> dict:
|
||||
"""Return translation JSON for the given language."""
|
||||
|
||||
@@ -105,7 +105,7 @@ async def create_world(
|
||||
owner_id=user.id,
|
||||
preset_id=preset.id if preset else None,
|
||||
name=body.name,
|
||||
description=body.notes,
|
||||
description=preset.description if preset else (body.notes or None),
|
||||
language=body.language,
|
||||
status="draft",
|
||||
current_time="day_1_hour_8",
|
||||
|
||||
@@ -60,6 +60,20 @@ DEFAULT_SETTINGS: dict[str, dict[str, Any]] = {
|
||||
"value": [],
|
||||
"description": "List of {from, to} pairs. Each 'from' substring in LLM scene_text output is replaced with 'to' (can be empty string to remove).",
|
||||
},
|
||||
"ui.header_title": {
|
||||
"value": "",
|
||||
"description": "Header title in the navbar. If empty, falls back to ui.page_title.",
|
||||
},
|
||||
"character_names.en": {
|
||||
"value": ["Eric", "Lyra", "Kael", "Mira", "Thorne", "Elara", "Gareth", "Sera", "Darian", "Ivy",
|
||||
"Bran", "Wren", "Callum", "Astrid", "Rurik", "Faye", "Owen", "Selene", "Magnus", "Tara"],
|
||||
"description": "Character name bank for English worlds (used by random name button).",
|
||||
},
|
||||
"character_names.ru": {
|
||||
"value": ["Эрик", "Элара", "Мира", "Каэль", "Гарет", "Сера", "Дариан", "Тара", "Бран", "Рен",
|
||||
"Алексей", "Мария", "Иван", "Ольга", "Дмитрий", "Анна", "Сергей", "Елена", "Андрей", "Наталья"],
|
||||
"description": "Character name bank for Russian worlds (used by random name button).",
|
||||
},
|
||||
}
|
||||
|
||||
# Keys whose values should never be returned to the client in plaintext.
|
||||
|
||||
@@ -341,13 +341,26 @@ async def _run_tool_loop(
|
||||
"tokens": (resp.get("prompt_tokens") or 0) + (resp.get("completion_tokens") or 0),
|
||||
})
|
||||
msg = resp.get("message", {})
|
||||
# Apply text replacements to content
|
||||
content = msg.get("content", "") or ""
|
||||
if content:
|
||||
from app.core.settings_service import apply_text_replacements
|
||||
content = await apply_text_replacements(db, content)
|
||||
msg = dict(msg)
|
||||
msg["content"] = content
|
||||
tool_calls = msg.get("tool_calls") or []
|
||||
if not tool_calls:
|
||||
# No tool calls — append assistant message and ask again
|
||||
# No tool calls — append assistant message and retry with a nudge.
|
||||
# Up to 3 retries.
|
||||
messages.append({"role": "assistant", "content": msg.get("content", "")})
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": f"You must call a tool. Available terminal tool: {terminal_tool}. If you are done with your work, call {terminal_tool} now.",
|
||||
"content": (
|
||||
f"You did not call any tools in your previous response. "
|
||||
f"You MUST use the available tools to accomplish the task. "
|
||||
f"If you tried to call a tool but it didn't work, try again with proper JSON arguments. "
|
||||
f"When you are done, call {terminal_tool}."
|
||||
),
|
||||
})
|
||||
continue
|
||||
|
||||
|
||||
@@ -217,10 +217,16 @@ async def _apply_diff(world: World, diff: list[dict[str, Any]]) -> None:
|
||||
"""Apply a propose_changes diff to the world.
|
||||
|
||||
Supported path formats:
|
||||
- `environment.<field_path>` → apply_patch to world.environment
|
||||
- `world.name` → set world.name
|
||||
- `world.description` → set world.description
|
||||
- `world.language` → set world.language
|
||||
- `schemas` → replace entire world.schemas
|
||||
- `schemas.<type>` → replace a single schema type
|
||||
- `environment.<field_path>` → apply_patch to world.environment
|
||||
- `environment` → replace entire world.environment
|
||||
- `player.<field>` → shorthand for environment.player.<field>
|
||||
- `current_location` → shorthand for environment.current_location
|
||||
- `plot_rails.<field>` → shorthand for environment.plot_rails.<field>
|
||||
"""
|
||||
from app.core.state_validator import apply_patch
|
||||
|
||||
@@ -231,25 +237,69 @@ async def _apply_diff(world: World, diff: list[dict[str, Any]]) -> None:
|
||||
path = d.get("path", "") or ""
|
||||
op = d.get("op", "replace")
|
||||
new = d.get("new")
|
||||
if path.startswith("environment."):
|
||||
field = path[len("environment."):]
|
||||
env_patch[field] = new
|
||||
elif path == "world.name":
|
||||
if isinstance(new, str):
|
||||
old = d.get("old") # noqa: F841 — not used but part of the schema
|
||||
|
||||
# Skip if new is empty dict {} (model sometimes returns empty)
|
||||
if isinstance(new, dict) and not new and op == "replace":
|
||||
_logger.warning("apply_diff_skip_empty", path=path)
|
||||
continue
|
||||
if new is None and op == "replace":
|
||||
continue
|
||||
|
||||
# World-level fields
|
||||
if path == "world.name" or path == "name":
|
||||
if isinstance(new, str) and new.strip():
|
||||
world.name = new
|
||||
elif path == "world.description":
|
||||
if new is None or isinstance(new, str):
|
||||
elif path == "world.description" or path == "description":
|
||||
if isinstance(new, str):
|
||||
world.description = new
|
||||
elif path == "world.language" or path == "language":
|
||||
if isinstance(new, str):
|
||||
world.language = new
|
||||
# Schemas
|
||||
elif path == "schemas":
|
||||
if isinstance(new, list):
|
||||
world.schemas = new
|
||||
elif path.startswith("schemas."):
|
||||
# For simplicity, replace entire schemas
|
||||
if isinstance(new, list):
|
||||
world.schemas = new
|
||||
# Environment (full replace)
|
||||
elif path == "environment":
|
||||
if isinstance(new, dict):
|
||||
world.environment = new
|
||||
# Environment field paths
|
||||
elif path.startswith("environment."):
|
||||
field = path[len("environment."):]
|
||||
env_patch[field] = new
|
||||
# Shorthand: player.xxx → environment.player.xxx
|
||||
elif path.startswith("player."):
|
||||
field = path[len("player."):]
|
||||
env_patch[f"player.{field}"] = new
|
||||
elif path == "player":
|
||||
if isinstance(new, dict):
|
||||
env_patch["player"] = new
|
||||
# Shorthand: current_location
|
||||
elif path == "current_location":
|
||||
if isinstance(new, str):
|
||||
env_patch["current_location"] = new
|
||||
elif isinstance(new, dict) and "name" in new:
|
||||
env_patch["current_location"] = new["name"]
|
||||
# Shorthand: plot_rails.xxx
|
||||
elif path.startswith("plot_rails."):
|
||||
field = path[len("plot_rails."):]
|
||||
env_patch[f"plot_rails.{field}"] = new
|
||||
elif path == "plot_rails":
|
||||
if isinstance(new, dict):
|
||||
env_patch["plot_rails"] = new
|
||||
else:
|
||||
_logger.warning("apply_diff_unknown_path", path=path)
|
||||
|
||||
if env_patch:
|
||||
new_env, errors = apply_patch(dict(world.environment or {}), env_patch)
|
||||
if not errors:
|
||||
world.environment = new_env
|
||||
# Sync plot_rails column
|
||||
if isinstance(new_env.get("plot_rails"), dict):
|
||||
world.plot_rails = new_env["plot_rails"]
|
||||
else:
|
||||
_logger.warning("apply_diff_errors", errors=errors)
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
PROMPTS = {
|
||||
"en": """You are the Game Master (GM) of a text RPG in the world "{world_name}".
|
||||
|
||||
# Language rule
|
||||
The world's language is `{language}`. 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.
|
||||
|
||||
# Your responsibilities
|
||||
1. Evaluate the player's action and decide what happened mechanically.
|
||||
2. Call tools for ANY state change in the world.
|
||||
|
||||
@@ -13,29 +13,32 @@ Player name: {player_name}
|
||||
Language: {language}
|
||||
Notes: {notes}
|
||||
|
||||
# Language rule
|
||||
The world's language is `{language}`. All entity names, location names, character names, item names, and descriptions that the PLAYER will see MUST be in `{language}`. Internal field names (like "health", "stats") stay in English.
|
||||
|
||||
# Required entity types
|
||||
You MUST create at minimum these entity types (call schema_add_type for each):
|
||||
1. `character` — with fields: name (string, required), description (string), stats (object, required, with sub-fields: health integer 0-100 required, mana integer 0-100, strength integer 1-20), inventory (array), relationship (string)
|
||||
2. `item` — with fields: name (string, required), description (string), qty (integer 1-9999), value (integer)
|
||||
3. `location` — with fields: name (string, required), description (string, required), exits (array of strings), is_safe (boolean)
|
||||
4. `faction` — with fields: name (string, required), description (string), alignment (string)
|
||||
1. `character` — fields: name (string, required), description (string), stats (object, required, with sub-fields: health integer 0-100 required, mana integer 0-100, strength integer 1-20), inventory (array), relationship (string)
|
||||
2. `item` — fields: name (string, required), description (string), qty (integer 1-9999), value (integer)
|
||||
3. `location` — fields: name (string, required), description (string, required), exits (array of strings), is_safe (boolean)
|
||||
4. `faction` — fields: name (string, required), description (string), alignment (string)
|
||||
|
||||
You may add additional entity types if the setting requires (e.g. `quest`, `spell`, `vehicle`).
|
||||
|
||||
# schema_add_type arguments
|
||||
Each call to schema_add_type needs:
|
||||
Each call to schema_add_type needs these arguments:
|
||||
- `type`: lowercase identifier like "character"
|
||||
- `verbose`: display name like "Character"
|
||||
- `plural`: plural form like "characters"
|
||||
- `properties`: array of field definitions, each with {name, type, required, min, max, properties}
|
||||
- `properties`: an array of field definition objects. Each field object has these keys: "name" (string), "type" (one of: string, integer, number, boolean, object, array), "required" (boolean), "min" (integer, optional), "max" (integer, optional), "properties" (array, only for type=object), "show" (boolean, default true — if false, the field is hidden from the player UI).
|
||||
|
||||
Example properties for a character type:
|
||||
Example properties array for a character type:
|
||||
```json
|
||||
[
|
||||
{{"name": "name", "type": "string", "required": true}},
|
||||
{{"name": "stats", "type": "object", "required": true, "properties": [
|
||||
{{"name": "health", "type": "integer", "required": true, "min": 0, "max": 100}},
|
||||
{{"name": "mana", "type": "integer", "required": false, "min": 0, "max": 100}}
|
||||
{{"name": "name", "type": "string", "required": true, "show": true}},
|
||||
{{"name": "stats", "type": "object", "required": true, "show": false, "properties": [
|
||||
{{"name": "health", "type": "integer", "required": true, "min": 0, "max": 100, "show": true}},
|
||||
{{"name": "mana", "type": "integer", "required": false, "min": 0, "max": 100, "show": true}}
|
||||
]}}
|
||||
]
|
||||
```
|
||||
@@ -47,6 +50,7 @@ Call `submit_plan` with a 1-sentence summary of the world you designed.
|
||||
- Call schema_add_type for EACH entity type (one call per type).
|
||||
- After all schema_add_type calls, call submit_plan exactly once.
|
||||
- After max 10 tool calls you MUST call submit_plan.
|
||||
- If you previously tried to call tools but they didn't work, try again — make sure to use proper JSON arguments.
|
||||
""",
|
||||
"ru": "",
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ class WorldEditRequest(BaseModel):
|
||||
# --------------------------------------------------------------------------- #
|
||||
class IterateRequest(BaseModel):
|
||||
action: str = Field(min_length=1, max_length=4000)
|
||||
action_source: Literal["custom", "suggested"] = "custom"
|
||||
action_source: str = "custom" # accept any string, default to "custom"
|
||||
|
||||
|
||||
class AnswerRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user