Files
ChatAIBot/routers/sessions.py
T
2026-06-04 08:05:06 +03:00

276 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from fastapi import APIRouter, HTTPException
from services.memory import (
get_all_sessions,
get_session,
get_or_create_session,
get_history,
delete_session,
update_session_title,
update_session_persona,
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,
update_quest_by_id,
set_session_affinity,
update_session_narrative_stats,
update_session_outfit,
update_session_scene,
update_session_plot_arc,
get_last_message_preview,
fork_session,
)
from models.schemas import (
ForkSessionRequest,
RebindPersonaRequest,
QuestStatusPatch,
RpgStateDebugPatch,
SessionContextPatch,
)
from services.rpg_plot import reconcile_plot_arc
from services.rpg_state import parse_stats_json
from services.chat_prompt import get_system_prompt
from services.memory import rebind_session_persona
from services.personas import get_persona
router = APIRouter(prefix="/sessions", tags=["sessions"])
@router.get("/")
async def list_sessions():
sessions = await get_all_sessions()
result = []
for s in sessions:
count = await get_message_count(s["session_id"])
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):
session = await get_session(session_id)
if session and session.get("rpg_enabled"):
persona = await get_persona(session.get("persona_id") or "default") or {}
await reconcile_plot_arc(
session_id,
persona_name=persona.get("name", session.get("persona_id") or "Character"),
recent_context=(session.get("status_quo") or "")[:2000],
genre=session.get("genre") or "adventure",
)
return await get_quests(session_id)
@router.patch("/{session_id}/context")
async def patch_session_context(session_id: str, body: SessionContextPatch):
"""Live-edit session context (outfit, scene, plot, facts, status quo)."""
from services.outfit_tags import parse_and_normalize_outfit_json
session = await get_session(session_id)
if not session:
raise HTTPException(status_code=404, detail="Сессия не найдена")
if body.status_quo is not None:
await update_session_status_quo(session_id, body.status_quo)
if body.global_plot is not None:
await update_session_global_plot(session_id, body.global_plot)
if body.outfit_json is not None:
try:
normalized = parse_and_normalize_outfit_json(body.outfit_json)
json.loads(normalized)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"outfit_json: {e}") from e
await update_session_outfit(session_id, normalized)
if body.scene_json is not None:
try:
json.loads(body.scene_json or "{}")
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"scene_json: {e}") from e
await update_session_scene(session_id, body.scene_json)
if body.facts_json is not None:
from services.rpg_facts import (
parse_facts_list,
facts_list_to_json,
dedupe_facts_fuzzy,
compress_facts,
FACTS_DEDUP_THRESHOLD,
FACTS_COMPRESS_TARGET,
)
try:
facts = dedupe_facts_fuzzy(parse_facts_list(body.facts_json))
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"facts_json: {e}") from e
if len(facts) > FACTS_DEDUP_THRESHOLD:
facts = await compress_facts(
facts,
status_quo=(session.get("status_quo") or ""),
scene_context=session.get("scene_json") or "{}",
target=FACTS_COMPRESS_TARGET,
)
facts = dedupe_facts_fuzzy(facts)
normalized = facts_list_to_json(facts)
await update_session_facts(session_id, normalized)
if body.plot_arc_json is not None:
try:
json.loads(body.plot_arc_json or "{}")
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"plot_arc_json: {e}") from e
await update_session_plot_arc(session_id, body.plot_arc_json)
if body.affinity is not None:
await set_session_affinity(session_id, body.affinity)
stats_changed = any(
getattr(body, k) is not None for k in ("lust", "stamina", "tension")
)
if stats_changed:
stats = parse_stats_json(session.get("narrative_stats_json"))
for key in ("lust", "stamina", "tension"):
val = getattr(body, key, None)
if val is not None:
stats[key] = max(0, min(10, int(val)))
await update_session_narrative_stats(
session_id, json.dumps(stats, ensure_ascii=False)
)
updated = await get_session(session_id) or session
return {
"status": "updated",
"outfit_json": updated.get("outfit_json", "[]"),
"scene_json": updated.get("scene_json", "{}"),
"affinity": updated.get("affinity", 0),
"narrative_stats": parse_stats_json(updated.get("narrative_stats_json")),
}
@router.patch("/{session_id}/rpg-state")
async def patch_rpg_state(session_id: str, body: RpgStateDebugPatch):
"""Debug: set affinity and/or narrative stats (lust/stamina/tension 010)."""
session = await get_session(session_id)
if not session:
raise HTTPException(status_code=404, detail="Сессия не найдена")
affinity = session.get("affinity", 0)
if body.affinity is not None:
affinity = await set_session_affinity(session_id, body.affinity)
stats = parse_stats_json(session.get("narrative_stats_json"))
changed_stats = False
for key in ("lust", "stamina", "tension"):
val = getattr(body, key, None)
if val is not None:
stats[key] = max(0, min(10, int(val)))
changed_stats = True
if changed_stats:
import json as _json
await update_session_narrative_stats(
session_id, _json.dumps(stats, ensure_ascii=False)
)
return {
"affinity": affinity,
"narrative_stats": stats,
"target": "current_player",
}
@router.patch("/{session_id}/quests/{quest_id}")
async def patch_quest(session_id: str, quest_id: int, body: QuestStatusPatch):
status = body.status.strip()
if status not in ("active", "done", "failed"):
raise HTTPException(status_code=400, detail="status must be active, done, or failed")
ok = await update_quest_by_id(quest_id, session_id, status)
if not ok:
raise HTTPException(status_code=404, detail="Quest not found")
if status in ("done", "failed"):
session = await get_session(session_id)
if session and session.get("rpg_enabled"):
persona = await get_persona(session.get("persona_id") or "default") or {}
await reconcile_plot_arc(
session_id,
persona_name=persona.get("name", session.get("persona_id") or "Character"),
recent_context=(session.get("status_quo") or "")[:2000],
genre=session.get("genre") or "adventure",
)
return {"status": "updated", "quest_id": quest_id, "new_status": status}
@router.get("/{session_id}")
async def get_session_route(session_id: str):
s = await get_session(session_id)
if not s:
raise HTTPException(status_code=404, detail="Сессия не найдена")
return s
@router.post("/{session_id}/rebind-persona")
async def rebind_persona(session_id: str, body: RebindPersonaRequest):
session = await get_session(session_id)
if not session:
raise HTTPException(status_code=404, detail="Сессия не найдена")
persona = await get_persona(body.persona_id)
if not persona:
raise HTTPException(status_code=400, detail="Персонаж не найден")
hist = [] if body.clear_history else await get_history(session_id)
static = await get_system_prompt(body.persona_id, hist, "")
first_mes = (persona.get("first_mes") or "").strip() if body.clear_history else None
try:
await rebind_session_persona(
session_id,
body.persona_id,
clear_history=body.clear_history,
static_prompt=static,
first_mes=first_mes or None,
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
return {
"persona_id": body.persona_id,
"persona_name": persona.get("name", body.persona_id),
"system_prompt_preview": static[:500],
"clear_history": body.clear_history,
}
@router.patch("/{session_id}")
async def patch_session(session_id: str, data: dict):
create_pid = data.get("persona_id") if "persona_id" in data else None
await get_or_create_session(session_id, create_pid)
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}