Files
AstrBot/astrbot/core/provider/modalities.py
LIghtJUNction c4048bb8eb refactor: delete _internal
remove deprecated API modules and example scripts; update core modules
(agents, providers, tools, platform adapters, utils), dashboard package
and components, and unit tests.
2026-04-23 04:16:30 +08:00

159 lines
5.5 KiB
Python

from __future__ import annotations
import copy
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from astrbot import logger
from astrbot.core.agent.message import Message
@dataclass(slots=True)
class ContextSanitizeStats:
fixed_image_blocks: int = 0
fixed_audio_blocks: int = 0
fixed_tool_messages: int = 0
removed_tool_calls: int = 0
@property
def changed(self) -> bool:
return bool(
self.fixed_image_blocks
or self.fixed_audio_blocks
or self.fixed_tool_messages
or self.removed_tool_calls,
)
def _message_to_dict(message: dict[str, Any] | Message) -> dict[str, Any] | None:
if isinstance(message, Message):
return dict(message.model_dump())
if isinstance(message, dict):
return dict(copy.deepcopy(message))
return None
def sanitize_contexts_by_modalities(
contexts: Sequence[dict[str, Any] | Message],
modalities: list[str] | None,
) -> tuple[list[dict[str, Any]], ContextSanitizeStats]:
if not contexts:
return [], ContextSanitizeStats()
if not modalities or not isinstance(modalities, list):
copied_contexts = []
for msg in contexts:
copied_msg = _message_to_dict(msg)
if copied_msg:
copied_contexts.append(copied_msg)
return copied_contexts, ContextSanitizeStats()
supports_image = "image" in modalities
supports_audio = "audio" in modalities
supports_tool_use = "tool_use" in modalities
if supports_image and supports_audio and supports_tool_use:
copied_contexts = []
for msg in contexts:
copied_msg = _message_to_dict(msg)
if copied_msg:
copied_contexts.append(copied_msg)
return copied_contexts, ContextSanitizeStats()
sanitized_contexts: list[dict[str, Any]] = []
stats = ContextSanitizeStats()
for raw_msg in contexts:
msg = _message_to_dict(raw_msg)
if not msg:
continue
role = msg.get("role")
if not role:
continue
if not supports_tool_use:
if role == "tool":
stats.fixed_tool_messages += 1
fixed_msg: dict[str, Any] = {
"role": "user",
"content": _tool_result_placeholder(msg.get("content")),
}
msg = fixed_msg
if role == "assistant" and "tool_calls" in msg:
stats.removed_tool_calls += 1
msg.pop("tool_calls", None)
msg.pop("tool_call_id", None)
if not supports_image or not supports_audio:
content = msg.get("content")
if isinstance(content, list):
filtered_parts: list[Any] = []
removed_any_multimodal = False
for part in content:
if isinstance(part, dict):
part_type = str(part.get("type", "")).lower()
if not supports_image and part_type in {"image_url", "image"}:
removed_any_multimodal = True
stats.fixed_image_blocks += 1
filtered_parts.append({"type": "text", "text": "[Image]"})
continue
if not supports_audio and part_type in {
"audio_url",
"input_audio",
}:
removed_any_multimodal = True
stats.fixed_audio_blocks += 1
filtered_parts.append({"type": "text", "text": "[Audio]"})
continue
filtered_parts.append(part)
if removed_any_multimodal:
msg["content"] = filtered_parts
if role == "assistant":
content = msg.get("content")
has_tool_calls = bool(msg.get("tool_calls"))
if not has_tool_calls:
if not content:
continue
if isinstance(content, str) and not content.strip():
continue
sanitized_contexts.append(msg)
return sanitized_contexts, stats
def _tool_result_placeholder(content: Any) -> str:
if isinstance(content, str):
content_text = content.strip()
elif isinstance(content, list):
text_parts: list[str] = []
for part in content:
if isinstance(part, dict):
part_type = str(part.get("type", "")).lower()
if part_type == "text":
text_parts.append(str(part.get("text", "")))
elif part_type in {"image_url", "image"}:
text_parts.append("[Image]")
elif part_type in {"audio_url", "input_audio"}:
text_parts.append("[Audio]")
content_text = "\n".join(part for part in text_parts if part).strip()
else:
content_text = ""
if not content_text:
return "[Tool result]"
return f"[Tool result]\n{content_text}"
def log_context_sanitize_stats(stats: ContextSanitizeStats) -> None:
if not stats.changed:
return
logger.debug(
"context modality fix applied: "
"fixed_image_blocks=%s, fixed_audio_blocks=%s, "
"fixed_tool_messages=%s, removed_tool_calls=%s",
stats.fixed_image_blocks,
stats.fixed_audio_blocks,
stats.fixed_tool_messages,
stats.removed_tool_calls,
)