fix:表情包发送无记录,两次wait结果,notice不显示msg_id,展示每次token上下文
This commit is contained in:
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Sequence
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
|
||||
from PIL import Image as PILImage
|
||||
from pydantic import BaseModel, Field as PydanticField
|
||||
@@ -27,7 +28,7 @@ from src.config.config import global_config
|
||||
from src.core.tooling import ToolRegistry, ToolSpec
|
||||
from src.know_u.knowledge import extract_category_ids_from_result
|
||||
from src.llm_models.model_client.base_client import BaseClient
|
||||
from src.llm_models.payload_content.message import Message, MessageBuilder, RoleType
|
||||
from src.llm_models.payload_content.message import ImageMessagePart, Message, MessageBuilder, RoleType, TextMessagePart
|
||||
from src.llm_models.payload_content.resp_format import RespFormat, RespFormatType
|
||||
from src.llm_models.payload_content.tool_option import ToolCall, ToolDefinitionInput, ToolOption, normalize_tool_options
|
||||
from src.services.llm_service import LLMServiceClient
|
||||
@@ -137,7 +138,7 @@ class MaisakaChatLoopService:
|
||||
|
||||
try:
|
||||
self._chat_system_prompt = load_prompt(
|
||||
"maidairy_chat",
|
||||
"maisaka_chat",
|
||||
file_tools_section=tools_section,
|
||||
bot_name=global_config.bot.nickname,
|
||||
identity=self._personality_prompt,
|
||||
@@ -695,6 +696,61 @@ class MaisakaChatLoopService:
|
||||
padding=(0, 1),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _estimate_text_tokens(text: str) -> int:
|
||||
"""估算单段文本的输入 token 数。"""
|
||||
normalized_text = text.strip()
|
||||
if not normalized_text:
|
||||
return 0
|
||||
|
||||
cjk_char_count = sum(1 for char in normalized_text if "\u4e00" <= char <= "\u9fff")
|
||||
latin_chunks = re.findall(r"[A-Za-z0-9_]+", normalized_text)
|
||||
latin_token_count = sum(max(1, (len(chunk) + 3) // 4) for chunk in latin_chunks)
|
||||
punctuation_count = len(re.findall(r"[^\w\s]", normalized_text))
|
||||
whitespace_bonus = max(1, normalized_text.count("\n"))
|
||||
return cjk_char_count + latin_token_count + punctuation_count + whitespace_bonus
|
||||
|
||||
@classmethod
|
||||
def _estimate_request_tokens(cls, messages: Sequence[Message]) -> int:
|
||||
"""估算本轮请求消息的总输入 token 数。"""
|
||||
total_tokens = 0
|
||||
for message in messages:
|
||||
total_tokens += 4
|
||||
total_tokens += cls._estimate_text_tokens(str(message.role.value))
|
||||
if message.tool_call_id:
|
||||
total_tokens += cls._estimate_text_tokens(message.tool_call_id)
|
||||
if message.tool_calls:
|
||||
for tool_call in message.tool_calls:
|
||||
total_tokens += cls._estimate_text_tokens(getattr(tool_call, "func_name", "") or "")
|
||||
total_tokens += cls._estimate_text_tokens(
|
||||
json.dumps(getattr(tool_call, "args", {}) or {}, ensure_ascii=False)
|
||||
)
|
||||
for part in message.parts:
|
||||
if isinstance(part, TextMessagePart):
|
||||
total_tokens += cls._estimate_text_tokens(part.text)
|
||||
continue
|
||||
if isinstance(part, ImageMessagePart):
|
||||
total_tokens += max(256, len(part.image_base64) // 12)
|
||||
return total_tokens
|
||||
|
||||
@staticmethod
|
||||
def _build_prompt_stats_text(
|
||||
*,
|
||||
selected_history_count: int,
|
||||
built_message_count: int,
|
||||
input_token_count: int,
|
||||
) -> str:
|
||||
"""构造本轮 prompt 的统计信息文本。"""
|
||||
if input_token_count >= 10_000:
|
||||
input_token_text = f"{input_token_count / 1000:.1f}k"
|
||||
else:
|
||||
input_token_text = str(input_token_count)
|
||||
return (
|
||||
f"已选上下文消息数={selected_history_count} "
|
||||
f"大模型消息数={built_message_count} "
|
||||
f"估算输入Token={input_token_text}"
|
||||
)
|
||||
|
||||
async def chat_loop_step(self, chat_history: List[LLMContextMessage]) -> ChatResponse:
|
||||
"""执行一轮 Maisaka 规划器请求。
|
||||
|
||||
@@ -708,6 +764,13 @@ class MaisakaChatLoopService:
|
||||
await self.ensure_chat_prompt_loaded()
|
||||
selected_history, selection_reason = self._select_llm_context_messages(chat_history)
|
||||
built_messages = self._build_request_messages(selected_history)
|
||||
input_token_count = self._estimate_request_tokens(built_messages)
|
||||
prompt_stats_text = self._build_prompt_stats_text(
|
||||
selected_history_count=len(selected_history),
|
||||
built_message_count=len(built_messages),
|
||||
input_token_count=input_token_count,
|
||||
)
|
||||
display_subtitle = f"{selection_reason} | {prompt_stats_text}"
|
||||
|
||||
def message_factory(_client: BaseClient) -> List[Message]:
|
||||
"""返回当前轮次已经构建好的请求消息。
|
||||
@@ -743,7 +806,7 @@ class MaisakaChatLoopService:
|
||||
Panel(
|
||||
Group(*ordered_panels),
|
||||
title="MaiSaka 大模型请求 - 对话单步",
|
||||
subtitle=selection_reason,
|
||||
subtitle=display_subtitle,
|
||||
border_style="cyan",
|
||||
padding=(0, 1),
|
||||
)
|
||||
@@ -757,6 +820,7 @@ class MaisakaChatLoopService:
|
||||
f"工具数={len(all_tools)} "
|
||||
f"启用打断={self._interrupt_flag is not None}"
|
||||
)
|
||||
logger.info(f"??Prompt??: {prompt_stats_text}")
|
||||
generation_result = await self._llm_chat.generate_response_with_messages(
|
||||
message_factory=message_factory,
|
||||
options=LLMGenerationOptions(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Maisaka 推理引擎。"""
|
||||
|
||||
from base64 import b64decode
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Optional, cast
|
||||
|
||||
@@ -15,7 +16,7 @@ from src.chat.heart_flow.heartFC_utils import CycleDetail
|
||||
from src.chat.message_receive.message import SessionMessage
|
||||
from src.chat.replyer.replyer_manager import replyer_manager
|
||||
from src.chat.utils.utils import process_llm_response
|
||||
from src.common.data_models.message_component_data_model import MessageSequence, TextComponent
|
||||
from src.common.data_models.message_component_data_model import EmojiComponent, MessageSequence, TextComponent
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import PersonInfo
|
||||
from src.common.logger import get_logger
|
||||
@@ -82,7 +83,7 @@ class MaisakaReasoningEngine:
|
||||
|
||||
self._runtime._agent_state = self._runtime._STATE_RUNNING
|
||||
if cached_messages:
|
||||
self._append_wait_interrupted_message_if_needed()
|
||||
self._clear_pending_wait_tool_call_id()
|
||||
await self._ingest_messages(cached_messages)
|
||||
anchor_message = cached_messages[-1]
|
||||
else:
|
||||
@@ -94,7 +95,7 @@ class MaisakaReasoningEngine:
|
||||
self._runtime._internal_turn_queue.task_done()
|
||||
continue
|
||||
logger.info(f"{self._runtime.log_prefix} 等待超时后开始新一轮思考")
|
||||
self._runtime._chat_history.append(self._build_wait_timeout_message())
|
||||
self._clear_pending_wait_tool_call_id()
|
||||
self._trim_chat_history()
|
||||
try:
|
||||
for round_index in range(self._runtime._max_internal_rounds):
|
||||
@@ -179,6 +180,10 @@ class MaisakaReasoningEngine:
|
||||
return self._runtime.message_cache[-1]
|
||||
return None
|
||||
|
||||
def _clear_pending_wait_tool_call_id(self) -> None:
|
||||
"""清理等待状态残留的 wait 工具调用编号。"""
|
||||
self._runtime._pending_wait_tool_call_id = None
|
||||
|
||||
def _build_wait_timeout_message(self) -> ToolResultMessage:
|
||||
"""构造 wait 超时后的工具结果消息。"""
|
||||
tool_call_id = self._runtime._pending_wait_tool_call_id or "wait_timeout"
|
||||
@@ -260,20 +265,22 @@ class MaisakaReasoningEngine:
|
||||
timestamp_text = message.timestamp.strftime("%H:%M:%S")
|
||||
user_name = user_info.user_nickname or user_info.user_id
|
||||
group_card = user_info.user_cardname or ""
|
||||
message_id = message.message_id or ""
|
||||
return (
|
||||
f"[时间]{timestamp_text}\n"
|
||||
f"[用户]{user_name}\n"
|
||||
f"[用户群昵称]{group_card}\n"
|
||||
f"[msg_id]{message_id}\n"
|
||||
"[发言内容]"
|
||||
)
|
||||
prefix_parts = [
|
||||
f"[时间]{timestamp_text}\n",
|
||||
f"[用户]{user_name}\n",
|
||||
f"[用户群昵称]{group_card}\n",
|
||||
]
|
||||
if not message.is_notify and message.message_id:
|
||||
prefix_parts.append(f"[msg_id]{message.message_id}\n")
|
||||
prefix_parts.append("[发言内容]")
|
||||
return "".join(prefix_parts)
|
||||
|
||||
def _build_legacy_visible_text(self, message: SessionMessage, source_sequence: MessageSequence) -> str:
|
||||
user_info = message.message_info.user_info
|
||||
speaker_name = user_info.user_cardname or user_info.user_nickname or user_info.user_id
|
||||
legacy_sequence = MessageSequence([])
|
||||
legacy_sequence.text(format_speaker_content(speaker_name, "", message.timestamp, message.message_id))
|
||||
visible_message_id = None if message.is_notify else message.message_id
|
||||
legacy_sequence.text(format_speaker_content(speaker_name, "", message.timestamp, visible_message_id))
|
||||
for component in clone_message_sequence(source_sequence).components:
|
||||
legacy_sequence.components.append(component)
|
||||
return build_visible_text_from_sequence(legacy_sequence).strip()
|
||||
@@ -281,7 +288,8 @@ class MaisakaReasoningEngine:
|
||||
def _build_legacy_visible_text_from_text(self, message: SessionMessage, content: str) -> str:
|
||||
user_info = message.message_info.user_info
|
||||
speaker_name = user_info.user_cardname or user_info.user_nickname or user_info.user_id
|
||||
return format_speaker_content(speaker_name, content, message.timestamp, message.message_id).strip()
|
||||
visible_message_id = None if message.is_notify else message.message_id
|
||||
return format_speaker_content(speaker_name, content, message.timestamp, visible_message_id).strip()
|
||||
|
||||
def _insert_chat_history_message(self, message: LLMContextMessage) -> int:
|
||||
"""将消息按处理顺序追加到聊天历史末尾。"""
|
||||
@@ -1385,3 +1393,154 @@ class MaisakaReasoningEngine:
|
||||
tool_call.func_name,
|
||||
"发送表情包失败。",
|
||||
)
|
||||
|
||||
async def _handle_send_emoji(self, tool_call: ToolCall) -> ToolExecutionResult:
|
||||
"""?????????????"""
|
||||
from src.chat.emoji_system.emoji_manager import emoji_manager
|
||||
from src.common.utils.utils_image import ImageUtils
|
||||
import random
|
||||
|
||||
tool_args = tool_call.args or {}
|
||||
emotion = str(tool_args.get("emotion") or "").strip()
|
||||
structured_result: dict[str, Any] = {
|
||||
"success": False,
|
||||
"message": "",
|
||||
"description": "",
|
||||
"emotion": [],
|
||||
"requested_emotion": emotion,
|
||||
}
|
||||
|
||||
logger.info(f"{self._runtime.log_prefix} ??????????: ??={emotion!r}")
|
||||
|
||||
if not emoji_manager.emojis:
|
||||
structured_result["message"] = "??????????????"
|
||||
return self._build_tool_failure_result(
|
||||
tool_call.func_name,
|
||||
structured_result["message"],
|
||||
structured_content=structured_result,
|
||||
)
|
||||
|
||||
selected_emoji = None
|
||||
if emotion:
|
||||
matching_emojis = [
|
||||
emoji
|
||||
for emoji in emoji_manager.emojis
|
||||
if emotion.lower() in (item.lower() for item in emoji.emotion)
|
||||
]
|
||||
if matching_emojis:
|
||||
selected_emoji = random.choice(matching_emojis)
|
||||
logger.info(
|
||||
f"{self._runtime.log_prefix} ?? {len(matching_emojis)} ????? {emotion!r} ?????"
|
||||
f"????{selected_emoji.description}"
|
||||
)
|
||||
|
||||
if selected_emoji is None:
|
||||
selected_emoji = random.choice(emoji_manager.emojis)
|
||||
logger.info(
|
||||
f"{self._runtime.log_prefix} ????????? {emotion!r}?"
|
||||
f"??????{selected_emoji.description}"
|
||||
)
|
||||
|
||||
emoji_description = selected_emoji.description.strip()
|
||||
emoji_emotions = [str(item).strip() for item in selected_emoji.emotion if str(item).strip()]
|
||||
structured_result["description"] = emoji_description
|
||||
structured_result["emotion"] = emoji_emotions
|
||||
|
||||
emoji_manager.update_emoji_usage(selected_emoji)
|
||||
|
||||
try:
|
||||
emoji_base64 = ImageUtils.image_path_to_base64(str(selected_emoji.full_path))
|
||||
if not emoji_base64:
|
||||
raise ValueError("??????? base64 ??")
|
||||
except Exception as exc:
|
||||
logger.error(f"{self._runtime.log_prefix} ??????? base64 ??: {exc}")
|
||||
structured_result["message"] = f"????????{exc}"
|
||||
return self._build_tool_failure_result(
|
||||
tool_call.func_name,
|
||||
structured_result["message"],
|
||||
structured_content=structured_result,
|
||||
)
|
||||
|
||||
try:
|
||||
sent = await send_service.emoji_to_stream(
|
||||
emoji_base64=emoji_base64,
|
||||
stream_id=self._runtime.session_id,
|
||||
storage_message=True,
|
||||
set_reply=False,
|
||||
reply_message=None,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception(f"{self._runtime.log_prefix} ??????????: {exc}")
|
||||
structured_result["message"] = f"???????????{exc}"
|
||||
return self._build_tool_failure_result(
|
||||
tool_call.func_name,
|
||||
structured_result["message"],
|
||||
structured_content=structured_result,
|
||||
)
|
||||
|
||||
if sent:
|
||||
success_message = (
|
||||
f"???????{emoji_description}????{', '.join(emoji_emotions)}?"
|
||||
if emoji_emotions
|
||||
else f"???????{emoji_description}"
|
||||
)
|
||||
logger.info(
|
||||
f"{self._runtime.log_prefix} ???????: "
|
||||
f"??={selected_emoji.description!r} ????={selected_emoji.emotion}"
|
||||
)
|
||||
self._append_sent_emoji_to_chat_history(
|
||||
emoji_base64=emoji_base64,
|
||||
success_message=success_message,
|
||||
)
|
||||
structured_result["success"] = True
|
||||
structured_result["message"] = success_message
|
||||
return self._build_tool_success_result(
|
||||
tool_call.func_name,
|
||||
success_message,
|
||||
structured_content=structured_result,
|
||||
)
|
||||
|
||||
logger.warning(f"{self._runtime.log_prefix} ???????")
|
||||
structured_result["message"] = "????????"
|
||||
return self._build_tool_failure_result(
|
||||
tool_call.func_name,
|
||||
structured_result["message"],
|
||||
structured_content=structured_result,
|
||||
)
|
||||
|
||||
def _append_sent_emoji_to_chat_history(
|
||||
self,
|
||||
*,
|
||||
emoji_base64: str,
|
||||
success_message: str,
|
||||
) -> None:
|
||||
"""? bot ?????????????? Maisaka ?????"""
|
||||
bot_name = global_config.bot.nickname.strip() or "MaiSaka"
|
||||
reply_timestamp = datetime.now()
|
||||
planner_prefix = (
|
||||
f"[??]{reply_timestamp.strftime('%H:%M:%S')}\n"
|
||||
f"[??]{bot_name}\n"
|
||||
"[?????]\n"
|
||||
"[msg_id]\n"
|
||||
"[????]"
|
||||
)
|
||||
history_message = SessionBackedMessage(
|
||||
raw_message=MessageSequence(
|
||||
[
|
||||
TextComponent(planner_prefix),
|
||||
EmojiComponent(
|
||||
binary_hash="",
|
||||
content=success_message,
|
||||
binary_data=b64decode(emoji_base64),
|
||||
),
|
||||
]
|
||||
),
|
||||
visible_text=format_speaker_content(
|
||||
bot_name,
|
||||
"[???]",
|
||||
reply_timestamp,
|
||||
),
|
||||
timestamp=reply_timestamp,
|
||||
source_kind="guided_reply",
|
||||
)
|
||||
self._runtime._chat_history.append(history_message)
|
||||
|
||||
Reference in New Issue
Block a user