Fixed RPG
This commit is contained in:
@@ -56,6 +56,23 @@ async def patch_card(card_id: str, body: CardPatch):
|
||||
return await get_character(card_id)
|
||||
|
||||
|
||||
@router.post("/{card_id}/avatar")
|
||||
async def upload_avatar(card_id: str, file: UploadFile = File(...)):
|
||||
card = await get_character(card_id)
|
||||
if not card:
|
||||
raise HTTPException(status_code=404, detail="Карточка не найдена")
|
||||
content = await file.read()
|
||||
if not content.startswith(b"\x89PNG"):
|
||||
raise HTTPException(status_code=400, detail="Нужен PNG")
|
||||
from services.character_card import _save_avatar_bytes
|
||||
rel = _save_avatar_bytes(content, f"card_{card_id}")
|
||||
await update_character(card_id, {"avatar_path": rel})
|
||||
# sync persona
|
||||
from services.personas import patch_persona
|
||||
await patch_persona(f"card_{card_id}", {"avatar_path": rel})
|
||||
return {"avatar_path": f"/static/{rel}"}
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
async def import_card(
|
||||
file: UploadFile = File(...),
|
||||
|
||||
+372
-10
@@ -1,22 +1,38 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
import aiosqlite
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from database.db import DB_PATH
|
||||
from models.schemas import ChatRequest, ChatResponse
|
||||
from models.schemas import ChatRequest, ChatResponse, MessageEditRequest, RegenerateRequest
|
||||
from services.llm import send_message, stream_message
|
||||
from services.memory import (
|
||||
get_history,
|
||||
add_message,
|
||||
clear_history,
|
||||
get_or_create_session,
|
||||
get_session,
|
||||
update_session_title,
|
||||
update_session_persona,
|
||||
get_message_count,
|
||||
get_last_assistant_message_id,
|
||||
update_message_image,
|
||||
update_session_facts,
|
||||
update_session_status_quo,
|
||||
update_session_affinity,
|
||||
update_session_genre,
|
||||
update_session_rpg_settings,
|
||||
upsert_quest,
|
||||
get_quests,
|
||||
add_action_resolution,
|
||||
get_message,
|
||||
update_message_content,
|
||||
delete_messages_after,
|
||||
delete_message,
|
||||
)
|
||||
from services.personas import get_persona
|
||||
from services.sd_prompt import (
|
||||
@@ -27,12 +43,51 @@ from services.sd_prompt import (
|
||||
from services.lorebook import get_lorebook_context
|
||||
from services.character_card import get_character
|
||||
from services import sdbackend as sd_service
|
||||
from services.rpg_facts import extract_facts, merge_facts, facts_to_prompt
|
||||
from services.rpg_plot import generate_plot_arc, should_advance_arc, pop_matching_beats
|
||||
from services.rpg_narrator import narrator_pre, narrator_post
|
||||
|
||||
router = APIRouter(prefix="/chat", tags=["chat"])
|
||||
|
||||
DEFAULT_PROMPT = "Ты — полезный AI ассистент. Отвечай чётко и по делу."
|
||||
SD_AUTO_GENERATE = os.getenv("SD_AUTO_GENERATE", "false").lower() in ("1", "true", "yes")
|
||||
|
||||
def affinity_prompt_block(affinity: int) -> str:
|
||||
if affinity >= 10:
|
||||
tone = "very warm, trusting, affectionate"
|
||||
elif affinity >= 5:
|
||||
tone = "friendly and open"
|
||||
elif affinity >= 1:
|
||||
tone = "slightly positive"
|
||||
elif affinity <= -5:
|
||||
tone = "hostile or deeply distrustful"
|
||||
elif affinity <= -1:
|
||||
tone = "cold and wary"
|
||||
else:
|
||||
tone = "neutral"
|
||||
return (
|
||||
f"\n\n--- Relationship ---\n"
|
||||
f"Affinity toward player: {affinity} ({tone}). "
|
||||
f"Reflect this in your attitude and word choice.\n---"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_RPG_SETTINGS = {
|
||||
"dice": True,
|
||||
"narrator": True,
|
||||
"quests": True,
|
||||
"affinity": True,
|
||||
"choices": True,
|
||||
}
|
||||
|
||||
|
||||
def get_rpg_settings(session: dict) -> dict:
|
||||
try:
|
||||
s = json.loads(session.get("rpg_settings_json") or "{}")
|
||||
return {**DEFAULT_RPG_SETTINGS, **s}
|
||||
except Exception:
|
||||
return DEFAULT_RPG_SETTINGS
|
||||
|
||||
|
||||
async def get_system_prompt(persona_id: str, history: list, user_message: str = "") -> str:
|
||||
persona = await get_persona(persona_id)
|
||||
@@ -41,11 +96,17 @@ async def get_system_prompt(persona_id: str, history: list, user_message: str =
|
||||
|
||||
prompt = persona["prompt"]
|
||||
|
||||
if persona.get("lorebook_json"):
|
||||
recent = [m for m in history if m["role"] in ("user", "assistant")][-5:]
|
||||
context = recent + [{"role": "user", "content": user_message}]
|
||||
lore = get_lorebook_context(persona.get("lorebook_json", "[]"), context)
|
||||
if lore:
|
||||
prompt = prompt + "\n\n" + lore
|
||||
|
||||
if persona_id.startswith("card_"):
|
||||
card_id = persona_id[5:]
|
||||
card = await get_character(card_id)
|
||||
if card:
|
||||
# match lorebook against recent context + current message
|
||||
recent = [m for m in history if m["role"] in ("user", "assistant")][-5:]
|
||||
context = recent + [{"role": "user", "content": user_message}]
|
||||
lore = get_lorebook_context(card.get("lorebook_json", "[]"), context)
|
||||
@@ -60,20 +121,37 @@ async def get_chat_history(session_id: str):
|
||||
return await get_history(session_id)
|
||||
|
||||
|
||||
@router.get("/system/{session_id}")
|
||||
async def get_system_blob(session_id: str):
|
||||
history = await get_history(session_id)
|
||||
system_msg = next((m for m in history if m.get("role") == "system"), None)
|
||||
session = await get_session(session_id)
|
||||
return {
|
||||
"system_prompt": system_msg.get("content") if system_msg else "",
|
||||
"facts_json": session.get("facts_json") if session else "[]",
|
||||
"status_quo": session.get("status_quo") if session else "",
|
||||
"plot_arc_json": session.get("plot_arc_json") if session else "{}",
|
||||
"rpg_enabled": bool(session.get("rpg_enabled")) if session else False,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/init")
|
||||
async def init_chat(request: ChatRequest):
|
||||
"""Called when opening a new chat. Seeds system prompt and first_mes if card persona."""
|
||||
persona_id = request.persona_id or "default"
|
||||
await get_or_create_session(request.session_id, persona_id)
|
||||
history = await get_history(request.session_id)
|
||||
if history:
|
||||
return {"first_mes": None} # already initialized
|
||||
return {"first_mes": None}
|
||||
|
||||
system_prompt = await get_system_prompt(persona_id, [], "")
|
||||
await add_message(request.session_id, "system", system_prompt)
|
||||
|
||||
first_mes = None
|
||||
if persona_id.startswith("card_"):
|
||||
persona = await get_persona(persona_id)
|
||||
if persona and persona.get("first_mes"):
|
||||
first_mes = persona["first_mes"]
|
||||
await add_message(request.session_id, "assistant", first_mes)
|
||||
elif persona_id.startswith("card_"):
|
||||
card = await get_character(persona_id[5:])
|
||||
if card and card.get("first_mes"):
|
||||
first_mes = card["first_mes"]
|
||||
@@ -82,6 +160,50 @@ async def init_chat(request: ChatRequest):
|
||||
return {"first_mes": first_mes}
|
||||
|
||||
|
||||
class RpgBootstrapRequest(BaseModel):
|
||||
session_id: str
|
||||
persona_id: str = "default"
|
||||
genre: str = "adventure"
|
||||
|
||||
|
||||
@router.post("/rpg/bootstrap")
|
||||
async def rpg_bootstrap(req: RpgBootstrapRequest):
|
||||
await get_or_create_session(req.session_id, req.persona_id)
|
||||
session = await get_session(req.session_id)
|
||||
persona = await get_persona(req.persona_id) or {}
|
||||
|
||||
# Save genre
|
||||
await update_session_genre(req.session_id, req.genre)
|
||||
|
||||
arc_json = (session.get("plot_arc_json") or "{}") if session else "{}"
|
||||
try:
|
||||
arc = json.loads(arc_json) if isinstance(arc_json, str) else {}
|
||||
except Exception:
|
||||
arc = {}
|
||||
if not arc:
|
||||
facts_block = facts_to_prompt((session or {}).get("facts_json", "[]"))
|
||||
arc = await generate_plot_arc(
|
||||
persona.get("name", req.persona_id),
|
||||
persona.get("description", ""),
|
||||
persona.get("scenario", ""),
|
||||
persona.get("first_mes", ""),
|
||||
facts_block=facts_block,
|
||||
genre=req.genre,
|
||||
)
|
||||
if arc:
|
||||
from services.memory import update_session_plot_arc
|
||||
await update_session_plot_arc(req.session_id, json.dumps(arc, ensure_ascii=False))
|
||||
|
||||
# Seed quests from beats
|
||||
for beat in arc.get("beats", []):
|
||||
injection = beat.get("injection", "").strip()
|
||||
if injection:
|
||||
await upsert_quest(req.session_id, injection[:120])
|
||||
|
||||
quests = await get_quests(req.session_id)
|
||||
return {"plot_arc": arc, "quests": quests}
|
||||
|
||||
|
||||
@router.post("/stream")
|
||||
async def chat_stream(request: ChatRequest):
|
||||
persona_id = request.persona_id or "default"
|
||||
@@ -89,8 +211,114 @@ async def chat_stream(request: ChatRequest):
|
||||
await get_or_create_session(request.session_id, persona_id)
|
||||
|
||||
history = await get_history(request.session_id)
|
||||
session = await get_session(request.session_id)
|
||||
system_prompt = await get_system_prompt(persona_id, history, request.message)
|
||||
|
||||
arc = {}
|
||||
roll = None
|
||||
outcome = None
|
||||
resolution_text = ""
|
||||
narrator_msg = None # shown as narrator bubble before assistant reply
|
||||
rpg_settings = {}
|
||||
|
||||
if session and session.get("rpg_enabled"):
|
||||
rpg_settings = get_rpg_settings(session)
|
||||
facts_block = facts_to_prompt(session.get("facts_json", "[]"))
|
||||
if facts_block:
|
||||
system_prompt = system_prompt + "\n\n" + facts_block
|
||||
try:
|
||||
arc = json.loads(session.get("plot_arc_json") or "{}")
|
||||
except Exception:
|
||||
arc = {}
|
||||
if arc:
|
||||
system_prompt = system_prompt + "\n\n--- PlotArc ---\n" + json.dumps(
|
||||
{k: arc.get(k) for k in ("title", "phase", "next_beat_hint")}, ensure_ascii=False
|
||||
) + "\n---"
|
||||
status_quo = (session.get("status_quo") or "").strip()
|
||||
if status_quo:
|
||||
system_prompt = system_prompt + "\n\n--- Status quo ---\n" + status_quo + "\n---"
|
||||
if rpg_settings.get("affinity", True):
|
||||
aff = int(session.get("affinity") or 0)
|
||||
system_prompt = system_prompt + affinity_prompt_block(aff)
|
||||
|
||||
if rpg_settings.get("narrator", True):
|
||||
persona = await get_persona(persona_id) or {}
|
||||
recent_txt = "\n".join(
|
||||
f"{m['role']}: {m['content']}" for m in history[-8:]
|
||||
if m.get("role") in ("user", "assistant")
|
||||
)
|
||||
|
||||
# Phase 1: ask narrator if check is needed (no roll yet)
|
||||
pre = await narrator_pre(
|
||||
persona.get("name", persona_id),
|
||||
recent_txt,
|
||||
json.dumps(arc, ensure_ascii=False) if arc else "",
|
||||
facts_block,
|
||||
request.message,
|
||||
)
|
||||
|
||||
needs_check = pre.get("needs_check", False) and rpg_settings.get("dice", True)
|
||||
|
||||
if needs_check:
|
||||
# Phase 2: roll and get resolution
|
||||
roll = random.randint(1, 20)
|
||||
if roll == 1:
|
||||
outcome = "critical failure"
|
||||
elif roll <= 8:
|
||||
outcome = "failure"
|
||||
elif roll >= 20:
|
||||
outcome = "critical success"
|
||||
else:
|
||||
outcome = "success"
|
||||
|
||||
pre2 = await narrator_pre(
|
||||
persona.get("name", persona_id),
|
||||
recent_txt,
|
||||
json.dumps(arc, ensure_ascii=False) if arc else "",
|
||||
facts_block,
|
||||
request.message,
|
||||
roll=roll,
|
||||
outcome=outcome,
|
||||
)
|
||||
resolution_text = (pre2.get("resolution_text") or "").strip()
|
||||
directives = pre2.get("directives") or []
|
||||
pre_sq = (pre2.get("status_quo_update") or "").strip()
|
||||
else:
|
||||
directives = pre.get("directives") or []
|
||||
pre_sq = (pre.get("status_quo_update") or "").strip()
|
||||
|
||||
if directives:
|
||||
system_prompt = system_prompt + "\n\n--- Narrator directives ---\n" + "\n".join(f"- {d}" for d in directives) + "\n---"
|
||||
if pre_sq:
|
||||
await update_session_status_quo(request.session_id, pre_sq)
|
||||
session["status_quo"] = pre_sq
|
||||
|
||||
if resolution_text:
|
||||
await add_action_resolution(
|
||||
request.session_id,
|
||||
intent_text=request.message,
|
||||
roll=roll,
|
||||
outcome=outcome,
|
||||
resolution_text=resolution_text,
|
||||
message_id=None,
|
||||
)
|
||||
narrator_msg = {"roll": roll, "outcome": outcome, "text": resolution_text}
|
||||
|
||||
# Inject outcome into system prompt so character reply is consistent
|
||||
if roll is not None:
|
||||
system_prompt = (
|
||||
system_prompt
|
||||
+ f"\n\n--- Mechanics ---\n"
|
||||
+ f"Roll d20={roll}. Outcome: {outcome}.\n"
|
||||
+ "Your reply MUST be consistent with this outcome. Do NOT contradict the narrator resolution.\n"
|
||||
+ "---"
|
||||
)
|
||||
|
||||
# is_narrator_choice: wrap message so LLM understands context
|
||||
user_message_content = request.message
|
||||
if request.is_narrator_choice:
|
||||
user_message_content = f"[Player chose: {request.message}]"
|
||||
|
||||
if not history:
|
||||
await add_message(request.session_id, "system", system_prompt)
|
||||
elif history[0]["role"] == "system" and history[0]["content"] != system_prompt:
|
||||
@@ -103,12 +331,14 @@ async def chat_stream(request: ChatRequest):
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
await add_message(request.session_id, "user", request.message)
|
||||
if not request.skip_user_add:
|
||||
await add_message(request.session_id, "user", user_message_content)
|
||||
messages = await get_history(request.session_id)
|
||||
|
||||
full_reply = []
|
||||
|
||||
async def generate():
|
||||
nonlocal arc
|
||||
async for chunk in stream_message(
|
||||
[{"role": m["role"], "content": m["content"]} for m in messages]
|
||||
):
|
||||
@@ -133,10 +363,97 @@ async def chat_stream(request: ChatRequest):
|
||||
image_prompt=prompt_str,
|
||||
)
|
||||
|
||||
choices = []
|
||||
debug_blocks = []
|
||||
quests_updated = []
|
||||
|
||||
if session and session.get("rpg_enabled"):
|
||||
if not arc:
|
||||
persona = await get_persona(persona_id) or {}
|
||||
genre = (session.get("genre") or "adventure")
|
||||
arc = await generate_plot_arc(
|
||||
persona.get("name", persona_id),
|
||||
persona.get("description", ""),
|
||||
persona.get("scenario", ""),
|
||||
persona.get("first_mes", ""),
|
||||
facts_block=facts_to_prompt(session.get("facts_json", "[]")),
|
||||
genre=genre,
|
||||
)
|
||||
if arc:
|
||||
from services.memory import update_session_plot_arc
|
||||
await update_session_plot_arc(request.session_id, json.dumps(arc, ensure_ascii=False))
|
||||
debug_blocks.append({"type": "plot_arc", "text": json.dumps(arc, ensure_ascii=False, indent=2)})
|
||||
if rpg_settings.get("quests", True):
|
||||
for beat in arc.get("beats", []):
|
||||
inj = beat.get("injection", "").strip()
|
||||
if inj:
|
||||
await upsert_quest(request.session_id, inj[:120])
|
||||
|
||||
trig = should_advance_arc(request.message)
|
||||
if trig and arc:
|
||||
arc, beats = pop_matching_beats(arc, trig, max_beats=1)
|
||||
if beats:
|
||||
from services.memory import update_session_plot_arc
|
||||
await update_session_plot_arc(request.session_id, json.dumps(arc, ensure_ascii=False))
|
||||
inj = beats[0].get("injection", "")
|
||||
if inj:
|
||||
debug_blocks.append({"type": "narrator_injection", "text": inj})
|
||||
if rpg_settings.get("choices", True):
|
||||
beat_choices = beats[0].get("choices") or []
|
||||
if beat_choices:
|
||||
choices = choices + beat_choices
|
||||
|
||||
ctx = [m for m in (await get_history(request.session_id)) if m["role"] in ("user", "assistant")][-10:]
|
||||
new_facts = await extract_facts(ctx)
|
||||
if new_facts:
|
||||
merged = merge_facts(session.get("facts_json", "[]"), new_facts)
|
||||
await update_session_facts(request.session_id, merged)
|
||||
session["facts_json"] = merged
|
||||
debug_blocks.append({"type": "facts", "text": facts_to_prompt(merged)})
|
||||
|
||||
persona = await get_persona(persona_id) or {}
|
||||
ctx_txt = "\n".join(
|
||||
f"{m['role']}: {m['content']}" for m in ctx[-8:]
|
||||
if m.get("role") in ("user", "assistant")
|
||||
)
|
||||
post = await narrator_post(
|
||||
persona.get("name", persona_id),
|
||||
ctx_txt,
|
||||
json.dumps(arc, ensure_ascii=False) if arc else "",
|
||||
facts_to_prompt(session.get("facts_json", "[]")),
|
||||
)
|
||||
sq = (post.get("status_quo_update") or "").strip()
|
||||
if sq:
|
||||
await update_session_status_quo(request.session_id, sq)
|
||||
session["status_quo"] = sq
|
||||
debug_blocks.append({"type": "status_quo", "text": f"--- Status quo ---\n{sq}\n---"})
|
||||
|
||||
if rpg_settings.get("choices", True):
|
||||
extra_choices = post.get("choices") or []
|
||||
if extra_choices:
|
||||
choices = choices + extra_choices
|
||||
|
||||
if rpg_settings.get("affinity", True):
|
||||
delta = int(post.get("affinity_delta") or 0)
|
||||
if delta:
|
||||
await update_session_affinity(request.session_id, delta)
|
||||
|
||||
if rpg_settings.get("quests", True):
|
||||
for qu in (post.get("quest_updates") or []):
|
||||
title = (qu.get("title") or "").strip()
|
||||
status = qu.get("status", "active")
|
||||
if title:
|
||||
await upsert_quest(request.session_id, title[:120], status)
|
||||
quests_updated = await get_quests(request.session_id)
|
||||
|
||||
count = await get_message_count(request.session_id)
|
||||
if count == 2:
|
||||
title = request.message[:40] + ("…" if len(request.message) > 40 else "")
|
||||
await update_session_title(request.session_id, title)
|
||||
if count == 2 and not request.skip_user_add:
|
||||
persona = await get_persona(persona_id) or {}
|
||||
persona_name = persona.get("name", persona_id)
|
||||
preview = request.message[:40] + ("…" if len(request.message) > 40 else "")
|
||||
current = (session or {}).get("title") or "Новый чат"
|
||||
if current in ("", "Новый чат"):
|
||||
await update_session_title(request.session_id, f"{persona_name} — {preview}")
|
||||
|
||||
image_path = None
|
||||
image_error = None
|
||||
@@ -150,11 +467,20 @@ async def chat_stream(request: ChatRequest):
|
||||
else:
|
||||
image_error = err
|
||||
|
||||
# Fetch current affinity for UI
|
||||
updated_session = await get_session(request.session_id)
|
||||
affinity = updated_session.get("affinity", 0) if updated_session else 0
|
||||
|
||||
yield f"data: {json.dumps({
|
||||
'done': True,
|
||||
'image_prompt': prompt_str,
|
||||
'image_path': f'/static/{image_path}' if image_path else None,
|
||||
'image_error': image_error,
|
||||
'choices': choices,
|
||||
'debug': debug_blocks,
|
||||
'narrator': narrator_msg,
|
||||
'affinity': affinity,
|
||||
'quests': quests_updated,
|
||||
})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
@@ -193,6 +519,42 @@ async def chat(request: ChatRequest):
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/messages/{message_id}")
|
||||
async def edit_message(message_id: int, req: MessageEditRequest):
|
||||
msg = await get_message(message_id)
|
||||
if not msg:
|
||||
raise HTTPException(status_code=404, detail="Сообщение не найдено")
|
||||
await update_message_content(message_id, req.content)
|
||||
if req.truncate_after:
|
||||
await delete_messages_after(msg["session_id"], message_id)
|
||||
return {"status": "updated", "message_id": message_id}
|
||||
|
||||
|
||||
@router.post("/regenerate")
|
||||
async def regenerate_chat(req: RegenerateRequest):
|
||||
msg_id = req.message_id or await get_last_assistant_message_id(req.session_id)
|
||||
if not msg_id:
|
||||
raise HTTPException(status_code=400, detail="Нет сообщения для перегенерации")
|
||||
msg = await get_message(msg_id)
|
||||
if not msg or msg.get("role") != "assistant":
|
||||
raise HTTPException(status_code=400, detail="Неверное сообщение")
|
||||
await delete_message(msg_id)
|
||||
history = await get_history(req.session_id)
|
||||
last_user = next((m for m in reversed(history) if m["role"] == "user"), None)
|
||||
if not last_user:
|
||||
raise HTTPException(status_code=400, detail="Нет сообщения пользователя")
|
||||
user_text = last_user["content"]
|
||||
if user_text.startswith("[Player chose: ") and user_text.endswith("]"):
|
||||
user_text = user_text[15:-1]
|
||||
stream_req = ChatRequest(
|
||||
message=user_text,
|
||||
session_id=req.session_id,
|
||||
persona_id=req.persona_id,
|
||||
skip_user_add=True,
|
||||
)
|
||||
return await chat_stream(stream_req)
|
||||
|
||||
|
||||
@router.delete("/{session_id}")
|
||||
async def clear_chat(session_id: str):
|
||||
await clear_history(session_id)
|
||||
|
||||
+70
-2
@@ -1,6 +1,14 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, File, UploadFile
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from models.schemas import PersonaCreate
|
||||
from services.personas import get_all_personas, get_persona, create_persona, delete_persona
|
||||
from services.personas import (
|
||||
get_all_personas,
|
||||
get_persona,
|
||||
create_persona,
|
||||
delete_persona,
|
||||
patch_persona,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/personas", tags=["personas"])
|
||||
|
||||
@@ -31,10 +39,70 @@ async def create_new_persona(data: PersonaCreate):
|
||||
lora_name=data.lora_name,
|
||||
lora_weight=data.lora_weight,
|
||||
appearance_tags=data.appearance_tags,
|
||||
personality=data.personality,
|
||||
scenario=data.scenario,
|
||||
first_mes=data.first_mes,
|
||||
mes_example=data.mes_example,
|
||||
lorebook_json=data.lorebook_json,
|
||||
)
|
||||
return {"persona_id": data.persona_id, **persona}
|
||||
|
||||
|
||||
class PersonaPatch(BaseModel):
|
||||
name: Optional[str] = None
|
||||
emoji: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
prompt: Optional[str] = None
|
||||
sd_enabled: Optional[bool] = None
|
||||
lora_name: Optional[str] = None
|
||||
lora_weight: Optional[float] = None
|
||||
appearance_tags: Optional[str] = None
|
||||
personality: Optional[str] = None
|
||||
scenario: Optional[str] = None
|
||||
first_mes: Optional[str] = None
|
||||
mes_example: Optional[str] = None
|
||||
lorebook_json: Optional[str] = None
|
||||
avatar_path: Optional[str] = None
|
||||
|
||||
|
||||
@router.patch("/{persona_id}")
|
||||
async def patch_one_persona(persona_id: str, body: PersonaPatch):
|
||||
fields = {k: v for k, v in body.model_dump().items() if v is not None}
|
||||
ok = await patch_persona(persona_id, fields)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=400, detail="Нельзя редактировать этого персонажа")
|
||||
persona = await get_persona(persona_id)
|
||||
if not persona:
|
||||
raise HTTPException(status_code=404, detail="Персонаж не найден")
|
||||
return {"persona_id": persona_id, **persona}
|
||||
|
||||
|
||||
@router.post("/{persona_id}/avatar")
|
||||
async def upload_persona_avatar(persona_id: str, file: UploadFile = File(...)):
|
||||
# only custom personas editable
|
||||
persona = await get_persona(persona_id)
|
||||
if not persona:
|
||||
raise HTTPException(status_code=404, detail="Персонаж не найден")
|
||||
if not persona.get("custom"):
|
||||
raise HTTPException(status_code=400, detail="Нельзя менять аватар встроенного персонажа")
|
||||
content = await file.read()
|
||||
if not content.startswith(b"\x89PNG"):
|
||||
raise HTTPException(status_code=400, detail="Нужен PNG")
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
avatars_dir = Path("static/avatars")
|
||||
avatars_dir.mkdir(parents=True, exist_ok=True)
|
||||
fname = f"persona_{persona_id}_{uuid.uuid4().hex[:8]}.png"
|
||||
path = avatars_dir / fname
|
||||
path.write_bytes(content)
|
||||
rel = f"avatars/{fname}"
|
||||
ok = await patch_persona(persona_id, {"avatar_path": rel})
|
||||
if not ok:
|
||||
raise HTTPException(status_code=400, detail="Нельзя изменить аватар")
|
||||
return {"avatar_path": f"/static/{rel}"}
|
||||
|
||||
|
||||
@router.delete("/{persona_id}")
|
||||
async def remove_persona(persona_id: str):
|
||||
if not await delete_persona(persona_id):
|
||||
|
||||
+44
-3
@@ -6,8 +6,19 @@ from services.memory import (
|
||||
update_session_title,
|
||||
update_session_persona,
|
||||
get_history,
|
||||
get_message_count
|
||||
get_message_count,
|
||||
update_session_rpg,
|
||||
update_session_facts,
|
||||
update_session_global_plot,
|
||||
update_session_status_quo,
|
||||
update_session_genre,
|
||||
update_session_rpg_settings,
|
||||
get_quests,
|
||||
get_last_message_preview,
|
||||
fork_session,
|
||||
get_session,
|
||||
)
|
||||
from models.schemas import ForkSessionRequest
|
||||
|
||||
router = APIRouter(prefix="/sessions", tags=["sessions"])
|
||||
|
||||
@@ -18,10 +29,20 @@ async def list_sessions():
|
||||
result = []
|
||||
for s in sessions:
|
||||
count = await get_message_count(s["session_id"])
|
||||
result.append({**s, "message_count": count})
|
||||
preview = await get_last_message_preview(s["session_id"])
|
||||
result.append({
|
||||
**s,
|
||||
"message_count": count,
|
||||
"last_message_preview": preview,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{session_id}/quests")
|
||||
async def list_quests(session_id: str):
|
||||
return await get_quests(session_id)
|
||||
|
||||
|
||||
@router.get("/{session_id}")
|
||||
async def get_session(session_id: str):
|
||||
sessions = await get_all_sessions()
|
||||
@@ -33,16 +54,36 @@ async def get_session(session_id: str):
|
||||
|
||||
@router.patch("/{session_id}")
|
||||
async def patch_session(session_id: str, data: dict):
|
||||
# ensure session exists before patching
|
||||
await get_or_create_session(session_id, data.get("persona_id", "default"))
|
||||
if "title" in data:
|
||||
await update_session_title(session_id, data["title"])
|
||||
if "persona_id" in data:
|
||||
await update_session_persona(session_id, data["persona_id"])
|
||||
if "rpg_enabled" in data:
|
||||
await update_session_rpg(session_id, bool(data["rpg_enabled"]))
|
||||
if "facts_json" in data:
|
||||
await update_session_facts(session_id, data["facts_json"])
|
||||
if "global_plot" in data:
|
||||
await update_session_global_plot(session_id, data["global_plot"])
|
||||
if "status_quo" in data:
|
||||
await update_session_status_quo(session_id, data["status_quo"])
|
||||
if "genre" in data:
|
||||
await update_session_genre(session_id, data["genre"])
|
||||
if "rpg_settings_json" in data:
|
||||
await update_session_rpg_settings(session_id, data["rpg_settings_json"])
|
||||
return {"status": "updated"}
|
||||
|
||||
|
||||
@router.post("/{session_id}/fork")
|
||||
async def fork_session_route(session_id: str, req: ForkSessionRequest):
|
||||
new_id = await fork_session(session_id, req.until_message_id)
|
||||
if not new_id:
|
||||
raise HTTPException(status_code=404, detail="Сессия не найдена")
|
||||
return {"session_id": new_id, "source_session_id": session_id}
|
||||
|
||||
|
||||
@router.delete("/{session_id}")
|
||||
async def remove_session(session_id: str):
|
||||
await delete_session(session_id)
|
||||
return {"status": "deleted", "session_id": session_id}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user