fix: preserve persona default tool selection

Fix https://github.com/AstrBotDevs/AstrBot/issues/8828
This commit is contained in:
Weilong Liao
2026-06-17 11:08:46 +08:00
committed by GitHub
parent d3b52356a6
commit fda5161451
4 changed files with 56 additions and 8 deletions

View File

@@ -27,6 +27,7 @@ from astrbot.core.db.po import (
UmoAlias,
WebChatThread,
)
from astrbot.core.sentinels import NOT_GIVEN
@dataclass
@@ -444,11 +445,23 @@ class BaseDatabase(abc.ABC):
persona_id: str,
system_prompt: str | None = None,
begin_dialogs: list[str] | None = None,
tools: list[str] | None = None,
skills: list[str] | None = None,
custom_error_message: str | None = None,
tools: list[str] | None | object = NOT_GIVEN,
skills: list[str] | None | object = NOT_GIVEN,
custom_error_message: str | None | object = NOT_GIVEN,
) -> Persona | None:
"""Update a persona's system prompt or begin dialogs."""
"""Update a persona record.
Args:
persona_id: Persona ID to update.
system_prompt: Optional replacement system prompt.
begin_dialogs: Optional replacement begin dialogs.
tools: Tool names, None for all tools, or NOT_GIVEN to leave unchanged.
skills: Skill names, None for all skills, or NOT_GIVEN to leave unchanged.
custom_error_message: Custom fallback message, None to clear, or NOT_GIVEN to leave unchanged.
Returns:
Updated persona, or None when no fields were updated.
"""
...
@abc.abstractmethod

View File

@@ -45,7 +45,15 @@ async def _json_or_empty(request: Request) -> dict[str, Any]:
def _model_dict(payload) -> dict[str, Any]:
return payload.model_dump(exclude_none=True)
"""Serialize a request model while preserving explicit null updates.
Args:
payload: Pydantic request model.
Returns:
Request data without fields omitted by the caller.
"""
return payload.model_dump(exclude_unset=True)
def _raise_persona_error(exc: PersonaServiceError | ValueError) -> None:

View File

@@ -40,8 +40,12 @@ class PersonaService:
async def create_persona(self, data: object) -> dict:
payload = self._payload(data)
persona_id = str(payload.get("persona_id", "")).strip()
system_prompt = str(payload.get("system_prompt", "")).strip()
raw_persona_id = payload.get("persona_id")
raw_system_prompt = payload.get("system_prompt")
persona_id = str(raw_persona_id).strip() if raw_persona_id is not None else ""
system_prompt = (
str(raw_system_prompt).strip() if raw_system_prompt is not None else ""
)
begin_dialogs = payload.get("begin_dialogs", [])
tools = payload.get("tools")
skills = payload.get("skills")

View File

@@ -473,7 +473,7 @@ class FakePersonaManager:
async def update_persona(self, persona_id: str, **kwargs) -> None:
persona = self.personas[persona_id]
for key, value in kwargs.items():
if value is not None:
if key in ("tools", "skills", "custom_error_message") or value is not None:
setattr(persona, key, value)
async def delete_persona(self, persona_id: str) -> None:
@@ -2476,6 +2476,29 @@ async def test_v1_safe_persona_routes_accept_slash_ids(
assert persona_id not in persona_mgr.personas
@pytest.mark.asyncio
async def test_v1_persona_by_id_update_preserves_explicit_null_tools_and_skills(
asgi_client: httpx.AsyncClient,
fake_core_lifecycle,
):
persona_id = "persona/foo"
headers = _jwt_headers()
persona = fake_core_lifecycle.persona_mgr.personas[persona_id]
persona.tools = ["tool-a"]
persona.skills = ["skill-a"]
response = await asgi_client.put(
"/api/v1/personas/by-id",
json={"persona_id": persona_id, "tools": None, "skills": None},
headers=headers,
)
assert response.status_code == 200
assert response.json()["data"] == {"message": "人格更新成功"}
assert persona.tools is None
assert persona.skills is None
@pytest.mark.asyncio
async def test_v1_im_routes_use_im_scope_and_running_platform(
asgi_client: httpx.AsyncClient,