643 lines
25 KiB
Python
643 lines
25 KiB
Python
"""人物服务。"""
|
||
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.core.config import settings
|
||
from app.core.errors import AppError
|
||
from app.core.time import utc_now
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.models.character import Character
|
||
from app.models.personality import PersonalityVersion
|
||
from app.modules.character import (
|
||
MODEL_POLICY_CREATE_DEFAULTS,
|
||
SYSTEM_TEMPLATE_OWNER_SCOPE,
|
||
extract_model_hint,
|
||
sanitize_profile,
|
||
)
|
||
from app.repositories import characters as character_repo
|
||
from app.repositories import personality as personality_repo
|
||
from app.repositories.json_utils import dumps, loads_dict
|
||
from app.schemas.character import (
|
||
ActivateCharacterResponse,
|
||
CharacterResponse,
|
||
CharacterSummaryResponse,
|
||
CharacterTemplateSummaryResponse,
|
||
CreateCharacterRequest,
|
||
UpdateModelPolicyRequest,
|
||
UpdateProfileRequest,
|
||
)
|
||
from app.schemas.common import Page
|
||
from app.services.audit import record_audit_event
|
||
from app.services.character_templates import CharacterTemplateConfig, get_default_template
|
||
from app.services.serializers import character_to_response, character_to_summary
|
||
|
||
|
||
async def get_active_character_model(
|
||
session: AsyncSession, owner_scope: str = settings.default_owner_scope
|
||
) -> Character:
|
||
character = await character_repo.get_active_character(session, owner_scope)
|
||
if character is None:
|
||
raise AppError("not_found", "未找到当前激活人物。")
|
||
return character
|
||
|
||
|
||
async def resolve_active_character(
|
||
session: AsyncSession,
|
||
character_id: str | None = None,
|
||
owner_scope: str = settings.default_owner_scope,
|
||
) -> Character:
|
||
"""读取当前激活人物;传入 character_id 时校验归属。"""
|
||
character = await get_active_character_model(session, owner_scope)
|
||
if character_id is not None and character_id != character.id:
|
||
raise AppError("not_found", "人物不存在。")
|
||
return character
|
||
|
||
|
||
async def get_active_character(
|
||
owner_scope: str = settings.default_owner_scope,
|
||
) -> CharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
return character_to_response(await get_active_character_model(session, owner_scope))
|
||
|
||
|
||
def default_model_hint(character: Character) -> str:
|
||
return extract_model_hint(character.model_policy_json, settings.default_model_hint)
|
||
|
||
|
||
async def _create_template_from_config(
|
||
session: AsyncSession, config: CharacterTemplateConfig
|
||
) -> Character:
|
||
"""从配置构建系统模板 ORM(使用 config/character_templates.yaml 内容)。"""
|
||
model_policy = dict(config.model_policy)
|
||
model_policy.setdefault("default_chat_model_hint", settings.default_model_hint)
|
||
profile = dict(config.profile)
|
||
sanitize_profile(profile)
|
||
character = Character(
|
||
name=config.name,
|
||
display_name=config.display_name,
|
||
status="active",
|
||
language=config.language,
|
||
profile_json=dumps(profile),
|
||
model_policy_json=dumps(model_policy),
|
||
privacy_level=config.privacy_level,
|
||
owner_scope=SYSTEM_TEMPLATE_OWNER_SCOPE,
|
||
is_system_template=True,
|
||
source_template_id=None,
|
||
template_key=config.key,
|
||
)
|
||
session.add(character)
|
||
await session.flush()
|
||
await _ensure_default_birthday(session, character)
|
||
return character
|
||
|
||
|
||
async def create_default_character(
|
||
session: AsyncSession, owner_scope: str = settings.default_owner_scope
|
||
) -> Character:
|
||
template = await ensure_default_system_template(session)
|
||
return await create_character_from_template(session, template, owner_scope)
|
||
|
||
|
||
async def ensure_character_personality(
|
||
session: AsyncSession, character: Character
|
||
) -> PersonalityVersion:
|
||
from app.services.personality import create_personality_from_profile
|
||
|
||
personality = None
|
||
if character.current_personality_version_id:
|
||
personality = await personality_repo.get_personality(
|
||
session, character.current_personality_version_id
|
||
)
|
||
if personality is None:
|
||
personality = await personality_repo.get_latest_personality(session, character.id)
|
||
if personality is None:
|
||
personality = await create_personality_from_profile(session, character)
|
||
if character.current_personality_version_id != personality.id:
|
||
character.current_personality_version_id = personality.id
|
||
await session.flush()
|
||
return personality
|
||
|
||
|
||
async def ensure_default_system_template(session: AsyncSession) -> Character:
|
||
"""按 config/character_templates.yaml 确保系统模板存在且最新。
|
||
|
||
Config 为单一事实源:DB 行缺失时 insert,字段不一致时覆盖更新。
|
||
子人物(is_system_template=False)不受影响。
|
||
"""
|
||
config = get_default_template()
|
||
template = await character_repo.get_system_template_by_key(session, config.key)
|
||
if template is None:
|
||
template = await _create_template_from_config(session, config)
|
||
else:
|
||
await _sync_template_from_config(session, template, config)
|
||
# create 路径已由 _create_template_from_config 兜底 birthday,
|
||
# sync 路径可能因旧数据缺失 birthday,再兜一次确保存在。
|
||
await _ensure_default_birthday(session, template)
|
||
await ensure_character_personality(session, template)
|
||
return template
|
||
|
||
|
||
async def ensure_all_system_templates(session: AsyncSession) -> list[Character]:
|
||
"""确保 ``config/character_templates.yaml`` 中定义的所有系统模板在 DB 中存在且最新。
|
||
|
||
遍历所有配置的模板(包括 ``key='default'``),
|
||
缺失则创建、字段不一致则覆盖更新,并确保 birthday 兜底与人格初始化。
|
||
供 ``seed_default_data`` 在应用启动时调用。
|
||
"""
|
||
from app.services.character_templates import load_character_templates
|
||
|
||
configs = load_character_templates()
|
||
if not configs:
|
||
return []
|
||
|
||
results: list[Character] = []
|
||
for key, config in configs.items():
|
||
template = await character_repo.get_system_template_by_key(session, key)
|
||
if template is None:
|
||
template = await _create_template_from_config(session, config)
|
||
else:
|
||
await _sync_template_from_config(session, template, config)
|
||
await _ensure_default_birthday(session, template)
|
||
await ensure_character_personality(session, template)
|
||
results.append(template)
|
||
|
||
return results
|
||
|
||
|
||
async def _sync_template_from_config(
|
||
session: AsyncSession, template: Character, config: CharacterTemplateConfig
|
||
) -> None:
|
||
"""比对 DB 字段与 config,不一致则覆盖更新并写入审计事件。
|
||
|
||
Config 可显式定义 birthday(含有效值时优先),缺失或不合法时
|
||
由 ``_ensure_default_birthday`` 用 created_at 兜底。
|
||
"""
|
||
model_policy = dict(config.model_policy)
|
||
model_policy.setdefault("default_chat_model_hint", settings.default_model_hint)
|
||
profile = dict(config.profile)
|
||
sanitize_profile(profile)
|
||
|
||
changed = False
|
||
|
||
if template.name != config.name:
|
||
template.name = config.name
|
||
changed = True
|
||
if template.display_name != config.display_name:
|
||
template.display_name = config.display_name
|
||
changed = True
|
||
if template.language != config.language:
|
||
template.language = config.language
|
||
changed = True
|
||
if template.privacy_level != config.privacy_level:
|
||
template.privacy_level = config.privacy_level
|
||
changed = True
|
||
|
||
# profile_json 比对:比较时排除 birthday(config 可能显式设、也可能留空兜底),
|
||
# 写入时 config 有值优先,否则保留 DB 端已有值(含 _ensure_default_birthday 兜底值)。
|
||
current_profile = loads_dict(template.profile_json)
|
||
config_birthday = profile.pop("birthday", None)
|
||
existing_birthday = current_profile.pop("birthday", None)
|
||
if current_profile != profile:
|
||
if config_birthday:
|
||
profile["birthday"] = config_birthday
|
||
elif existing_birthday:
|
||
profile["birthday"] = existing_birthday
|
||
template.profile_json = dumps(profile)
|
||
changed = True
|
||
|
||
# model_policy_json 比对
|
||
current_policy = loads_dict(template.model_policy_json)
|
||
if current_policy != model_policy:
|
||
template.model_policy_json = dumps(model_policy)
|
||
changed = True
|
||
|
||
if changed:
|
||
await record_audit_event(
|
||
session,
|
||
action="character.template_resynced",
|
||
character_id=template.id,
|
||
actor="system",
|
||
decision="auto",
|
||
input_summary=f"模板 '{config.key}' 由 config 文件覆盖更新",
|
||
resource_type="character_template",
|
||
resource_id=template.id,
|
||
)
|
||
|
||
|
||
async def create_character_from_template(
|
||
session: AsyncSession, template: Character, owner_scope: str
|
||
) -> Character:
|
||
# 模板的 birthday 仅在兜底(空/缺失)时不继承;显式设置的有效生日原样保留,
|
||
# 由 _ensure_default_birthday 写入子角色的 profile。
|
||
profile = loads_dict(template.profile_json)
|
||
birthday = profile.get("birthday")
|
||
if not (isinstance(birthday, str) and birthday.strip()):
|
||
profile.pop("birthday", None)
|
||
character = Character(
|
||
name=template.name,
|
||
display_name=template.display_name,
|
||
status="active",
|
||
language=template.language,
|
||
profile_json=dumps(profile),
|
||
model_policy_json=template.model_policy_json,
|
||
safety_policy_id=template.safety_policy_id,
|
||
privacy_level=template.privacy_level,
|
||
owner_scope=owner_scope,
|
||
is_system_template=False,
|
||
source_template_id=template.id,
|
||
template_key=None,
|
||
)
|
||
session.add(character)
|
||
await session.flush()
|
||
await _ensure_default_birthday(session, character)
|
||
return character
|
||
|
||
|
||
async def ensure_default_character_for_owner(session: AsyncSession, owner_scope: str) -> Character:
|
||
"""确保当前 owner 有默认模板对应的用户人物。
|
||
|
||
只处理 ``character_templates.yaml`` 中标记 ``is_default: true`` 的模板。
|
||
不存在任何默认模板时回退到 ``key='default'``。
|
||
"""
|
||
from app.services.character_templates import load_character_templates
|
||
|
||
configs = load_character_templates()
|
||
if not configs:
|
||
template = await ensure_default_system_template(session)
|
||
character = await create_character_from_template(session, template, owner_scope)
|
||
await _ensure_character_personality_from_template(session, template, character)
|
||
return character
|
||
|
||
# 先确保所有系统模板在 DB 中存在
|
||
templates: dict[str, Character] = {}
|
||
for key in configs:
|
||
tpl = await character_repo.get_system_template_by_key(session, key)
|
||
if tpl is None:
|
||
config = configs[key]
|
||
tpl = await _create_template_from_config(session, config)
|
||
await _ensure_default_birthday(session, tpl)
|
||
await ensure_character_personality(session, tpl)
|
||
templates[key] = tpl
|
||
|
||
# 筛选 is_default=true 的配置 key
|
||
default_keys = [k for k, c in configs.items() if c.is_default]
|
||
if not default_keys:
|
||
default_keys = ["default"] if "default" in configs else []
|
||
|
||
primary: Character | None = None
|
||
for key in default_keys:
|
||
tpl = templates.get(key)
|
||
if tpl is None:
|
||
continue
|
||
existing = await character_repo.get_character_by_source_template(
|
||
session, tpl.id, owner_scope
|
||
)
|
||
if existing is not None:
|
||
await ensure_character_personality(session, existing)
|
||
if primary is None:
|
||
primary = existing
|
||
continue
|
||
|
||
character = await create_character_from_template(session, tpl, owner_scope)
|
||
from app.services.personality import copy_personality_to_character
|
||
|
||
template_personality = await ensure_character_personality(session, tpl)
|
||
personality = await copy_personality_to_character(
|
||
session, template_personality, character.id
|
||
)
|
||
character.current_personality_version_id = personality.id
|
||
await session.flush()
|
||
|
||
await record_audit_event(
|
||
session,
|
||
action="character.created_from_template",
|
||
character_id=character.id,
|
||
actor="system",
|
||
decision="auto",
|
||
input_summary=f"启动时从模板 '{key}' 自动创建用户人物",
|
||
resource_type="character",
|
||
resource_id=character.id,
|
||
)
|
||
if primary is None:
|
||
primary = character
|
||
|
||
# 兜底:没有找到主人物时从 default 模板创建
|
||
if primary is None:
|
||
template = await ensure_default_system_template(session)
|
||
primary = await create_character_from_template(session, template, owner_scope)
|
||
template_personality = await ensure_character_personality(session, template)
|
||
from app.services.personality import copy_personality_to_character
|
||
|
||
personality = await copy_personality_to_character(session, template_personality, primary.id)
|
||
primary.current_personality_version_id = personality.id
|
||
await session.flush()
|
||
|
||
return primary
|
||
|
||
|
||
async def _ensure_character_personality_from_template(
|
||
session: AsyncSession,
|
||
template: Character,
|
||
character: Character,
|
||
) -> None:
|
||
"""从模板复制人格到目标人物。"""
|
||
from app.services.personality import copy_personality_to_character
|
||
|
||
template_personality = await ensure_character_personality(session, template)
|
||
personality = await copy_personality_to_character(session, template_personality, character.id)
|
||
character.current_personality_version_id = personality.id
|
||
await session.flush()
|
||
|
||
|
||
# —— 人物管理 API(api-spec §6)——
|
||
|
||
|
||
async def get_owned_character(
|
||
session: AsyncSession, character_id: str, owner_scope: str = settings.default_owner_scope
|
||
) -> Character:
|
||
"""按 ID 取当前归属范围下的可操作人物;系统模板与跨范围 ID 一律视为不存在。"""
|
||
character = await character_repo.get_character(session, character_id)
|
||
if character is None or character.is_system_template or character.owner_scope != owner_scope:
|
||
raise AppError("not_found", "人物不存在。")
|
||
return character
|
||
|
||
|
||
async def _personality_version_number(session: AsyncSession, character: Character) -> int:
|
||
version_id = character.current_personality_version_id
|
||
if not version_id:
|
||
return 0
|
||
personality = await personality_repo.get_personality(session, version_id)
|
||
return personality.version if personality else 0
|
||
|
||
|
||
async def _audit_character(session: AsyncSession, character: Character, action: str) -> None:
|
||
await record_audit_event(
|
||
session,
|
||
action=action,
|
||
character_id=character.id,
|
||
actor="user",
|
||
decision="auto",
|
||
input_summary=character.name,
|
||
resource_type="character",
|
||
resource_id=character.id,
|
||
)
|
||
|
||
|
||
async def _ensure_default_birthday(session: AsyncSession, character: Character) -> None:
|
||
"""birthday 缺失时用 created_at 兜底:仅写 profile.birthday。
|
||
|
||
纪念日表(time_anniversaries)随旧时间模型退役删除;辰正(M8 重实现)落地后
|
||
生日提醒改由时态感知按 profile.birthday 计算,不再预写行。
|
||
"""
|
||
profile = loads_dict(character.profile_json)
|
||
birthday = profile.get("birthday")
|
||
if isinstance(birthday, str) and birthday.strip():
|
||
return
|
||
profile["birthday"] = character.created_at.strftime("%Y-%m-%d")
|
||
character.profile_json = dumps(profile)
|
||
|
||
async def create_character(
|
||
session: AsyncSession,
|
||
payload: CreateCharacterRequest,
|
||
owner_scope: str = settings.default_owner_scope,
|
||
) -> CharacterResponse:
|
||
"""API 创建人物。
|
||
|
||
- 传 ``template_key`` 时从系统模板复制(继承 profile/model_policy,status=active);
|
||
- 不传时创建空白人物(status=wuji,不激活、不建人格)。
|
||
"""
|
||
if payload.template_key:
|
||
template = await character_repo.get_system_template_by_key(session, payload.template_key)
|
||
if template is None:
|
||
raise AppError("not_found", f"模板 '{payload.template_key}' 不存在。")
|
||
|
||
profile = loads_dict(template.profile_json)
|
||
profile.update(dict(payload.profile or {}))
|
||
|
||
model_policy = loads_dict(template.model_policy_json)
|
||
model_policy.update(dict(payload.model_policy or {}))
|
||
model_policy.setdefault("default_chat_model_hint", settings.default_model_hint)
|
||
|
||
character = Character(
|
||
name=payload.name,
|
||
display_name=payload.display_name or template.display_name,
|
||
status="active",
|
||
language=template.language,
|
||
profile_json=dumps(profile),
|
||
model_policy_json=dumps(model_policy),
|
||
privacy_level=template.privacy_level,
|
||
owner_scope=owner_scope,
|
||
is_system_template=False,
|
||
source_template_id=template.id,
|
||
template_key=None,
|
||
)
|
||
await character_repo.add_character(session, character)
|
||
await _ensure_default_birthday(session, character)
|
||
await ensure_character_personality(session, character)
|
||
await _audit_character(session, character, "character.created_from_template")
|
||
await session.commit()
|
||
await session.refresh(character)
|
||
return character_to_response(character)
|
||
|
||
# ── 空白创建(原有逻辑)──
|
||
model_policy = MODEL_POLICY_CREATE_DEFAULTS | dict(payload.model_policy or {})
|
||
model_policy["default_chat_model_hint"] = (
|
||
model_policy.get("default_chat_model_hint") or settings.default_model_hint
|
||
)
|
||
character = Character(
|
||
name=payload.name,
|
||
display_name=payload.display_name,
|
||
status="wuji",
|
||
language="zh-CN",
|
||
profile_json=dumps(dict(payload.profile or {})),
|
||
model_policy_json=dumps(model_policy),
|
||
privacy_level=model_policy.get("privacy_level", "normal"),
|
||
owner_scope=owner_scope,
|
||
is_system_template=False,
|
||
source_template_id=None,
|
||
template_key=None,
|
||
)
|
||
await character_repo.add_character(session, character)
|
||
await _ensure_default_birthday(session, character)
|
||
await _audit_character(session, character, "character.created")
|
||
await session.commit()
|
||
await session.refresh(character)
|
||
return character_to_response(character)
|
||
|
||
|
||
async def list_characters_page(
|
||
*,
|
||
owner_scope: str = settings.default_owner_scope,
|
||
status: str | None = None,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
) -> Page[CharacterSummaryResponse]:
|
||
async with AsyncSessionLocal() as session:
|
||
safe_page = max(page, 1)
|
||
safe_page_size = min(max(page_size, 1), 100)
|
||
items, total = await character_repo.list_characters(
|
||
session,
|
||
owner_scope,
|
||
status=status,
|
||
page=safe_page,
|
||
page_size=safe_page_size,
|
||
)
|
||
summaries = [
|
||
character_to_summary(
|
||
character,
|
||
memory_count=await character_repo.count_memories(session, character.id),
|
||
personality_version=await _personality_version_number(session, character),
|
||
)
|
||
for character in items
|
||
]
|
||
return Page(
|
||
items=summaries,
|
||
page=safe_page,
|
||
page_size=safe_page_size,
|
||
total=total,
|
||
)
|
||
|
||
|
||
async def get_character_by_id(
|
||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||
) -> CharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
return character_to_response(await get_owned_character(session, character_id, owner_scope))
|
||
|
||
|
||
async def update_character_profile(
|
||
character_id: str,
|
||
payload: UpdateProfileRequest,
|
||
owner_scope: str = settings.default_owner_scope,
|
||
) -> CharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_owned_character(session, character_id, owner_scope)
|
||
profile = loads_dict(character.profile_json)
|
||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||
if key == "display_name":
|
||
if value is not None:
|
||
character.display_name = value
|
||
continue
|
||
if value is not None:
|
||
profile[key] = value
|
||
character.profile_json = dumps(profile)
|
||
await _audit_character(session, character, "character.profile_updated")
|
||
await session.commit()
|
||
await session.refresh(character)
|
||
return character_to_response(character)
|
||
|
||
|
||
async def update_character_model_policy(
|
||
character_id: str,
|
||
payload: UpdateModelPolicyRequest,
|
||
owner_scope: str = settings.default_owner_scope,
|
||
) -> CharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_owned_character(session, character_id, owner_scope)
|
||
policy = loads_dict(character.model_policy_json)
|
||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||
if value is None:
|
||
continue
|
||
if key == "privacy_level":
|
||
character.privacy_level = value
|
||
policy[key] = value
|
||
character.model_policy_json = dumps(policy)
|
||
await _audit_character(session, character, "character.model_policy_updated")
|
||
await session.commit()
|
||
await session.refresh(character)
|
||
return character_to_response(character)
|
||
|
||
|
||
async def _deactivate_others(
|
||
session: AsyncSession, character: Character, owner_scope: str = settings.default_owner_scope
|
||
) -> None:
|
||
"""A1 不变量:一个 owner_scope 至多一个 active;激活时把其余改 paused。"""
|
||
others = await character_repo.list_active_in_scope(session, owner_scope, except_id=character.id)
|
||
for other in others:
|
||
other.status = "paused"
|
||
|
||
|
||
async def activate_character(
|
||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||
) -> ActivateCharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_owned_character(session, character_id, owner_scope)
|
||
if character.status == "archived":
|
||
raise AppError("conflict", "已归档人物不可直接激活,请先恢复。")
|
||
await _deactivate_others(session, character, owner_scope)
|
||
character.status = "active"
|
||
character.activated_at = utc_now()
|
||
await ensure_character_personality(session, character)
|
||
await _audit_character(session, character, "character.activated")
|
||
await session.commit()
|
||
return ActivateCharacterResponse(active_character_id=character.id, status="active")
|
||
|
||
|
||
async def pause_character(
|
||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||
) -> CharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_owned_character(session, character_id, owner_scope)
|
||
if character.status not in ("active", "initialized"):
|
||
raise AppError("conflict", "仅运行中人物可暂停。")
|
||
character.status = "paused"
|
||
await _audit_character(session, character, "character.paused")
|
||
await session.commit()
|
||
await session.refresh(character)
|
||
return character_to_response(character)
|
||
|
||
|
||
async def resume_character(
|
||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||
) -> CharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_owned_character(session, character_id, owner_scope)
|
||
if character.status != "paused":
|
||
raise AppError("conflict", "仅暂停状态的人物可恢复。")
|
||
await _deactivate_others(session, character, owner_scope)
|
||
character.status = "active"
|
||
character.activated_at = utc_now()
|
||
await ensure_character_personality(session, character)
|
||
await _audit_character(session, character, "character.resumed")
|
||
await session.commit()
|
||
await session.refresh(character)
|
||
return character_to_response(character)
|
||
|
||
|
||
async def archive_character(
|
||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||
) -> CharacterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await get_owned_character(session, character_id, owner_scope)
|
||
if character.status == "archived":
|
||
raise AppError("conflict", "人物已归档。")
|
||
character.status = "archived"
|
||
character.archived_at = utc_now()
|
||
await _audit_character(session, character, "character.archived")
|
||
await session.commit()
|
||
await session.refresh(character)
|
||
return character_to_response(character)
|
||
|
||
|
||
async def list_system_templates_page() -> list[CharacterTemplateSummaryResponse]:
|
||
"""列出所有系统模板(供前端模板选择器使用)。"""
|
||
async with AsyncSessionLocal() as session:
|
||
templates = await character_repo.list_system_templates(session)
|
||
result: list[CharacterTemplateSummaryResponse] = []
|
||
for t in templates:
|
||
profile = loads_dict(t.profile_json)
|
||
result.append(
|
||
CharacterTemplateSummaryResponse(
|
||
key=t.template_key or "",
|
||
name=t.name,
|
||
display_name=t.display_name,
|
||
description=profile.get("description"),
|
||
temperament=profile.get("temperament"),
|
||
speaking_style=profile.get("speaking_style"),
|
||
language=t.language,
|
||
)
|
||
)
|
||
return result
|