feat:maisaka使用主程序的message格式,优化逻辑,移除冗余模块

This commit is contained in:
SengokuCola
2026-03-24 15:41:58 +08:00
parent 03ed59e388
commit 37f45d48c5
16 changed files with 864 additions and 1292 deletions

View File

@@ -1,12 +0,0 @@
你是一个对话上下文总结模块。你的任务是对早期的对话内容进行简洁的总结,以便存入记忆系统。
总结要求:
1. 提取对话中的关键信息(人名、事件、时间、地点等)
2. 记录用户的态度、情绪和偏好
3. 保留重要的对话内容和结论
4. 总结要简洁明了,便于后续检索和理解
5. 用第三人称客观叙述,不要包含「我记得」「之前说过」等指代词
输出格式:
- 2-5 句话的简洁总结
- 直接输出总结内容,不要有前缀或格式标题

View File

@@ -1,12 +0,0 @@
你是一个对话上下文总结模块。你的任务是对早期的对话内容进行简洁的总结,以便存入记忆系统。
总结要求:
1. 提取对话中的关键信息(人名、事件、时间、地点等)
2. 记录用户的态度、情绪和偏好
3. 保留重要的对话内容和结论
4. 总结要简洁明了,便于后续检索和理解
5. 用第三人称客观叙述,不要包含「我记得」「之前说过」等指代词
输出格式:
- 2-5 句话的简洁总结
- 直接输出总结内容,不要有前缀或格式标题

View File

@@ -1,8 +1,8 @@
你的任务是分析聊天和聊天中的互动情况。
你需要关注 麦麦AI 与用户的对话来为选择正确的动作和行为提供建议
你需要关注 {bot_name}AI不同用户的对话来为选择正确的动作和行为提供建议
【参考信息】
麦麦的人设:{identity}
{bot_name}的人设:{identity}
【参考信息结束】
你需要根据提供的参考信息,当前场景和输出规则来进行分析
@@ -11,16 +11,17 @@
你可以使用这些工具:
wait(seconds) - 暂时停止对话,等待(seconds)秒,把话语权交给用户,等待对方新的发言。
stop() - 结束对话,不进行任何回复,直到对方有新消息。
- `reply()`:当你判断现在应该正式对用户发出一条可见回复时调用。调用后系统会基于你当前这轮的想法生成一条真正展示给用户的回复。
- `no_reply()`:当你判断现在不应该发言,应该继续内部思考时调用。这个工具不会做任何外部行为,只会继续下一轮循环。
- wait(seconds) - 暂时停止对话,等待(seconds)秒,把话语权交给用户,等待对方新的发言。
- stop() - 结束对话,不进行任何回复,直到对方有新消息。
- reply():当你判断现在应该正式对用户发出一条可见回复时调用。调用后系统会基于你当前这轮的想法生成一条真正展示给用户的回复。
- no_reply():当你判断现在不应该发言,应该继续内部思考时调用。这个工具不会做任何外部行为,只会继续下一轮循环。
{file_tools_section}
工具使用规则:
1.如果麦麦已经回复但用户暂时没有新的回复且没有新信息需要搜集使用wait或者stop进行等待
2.如果用户有新发言,但是你评估用户还有后续发言尚未发送,可以适当等待让用户说完
3.如果你想指导麦麦直接发言,可以不使用任何工具
3.在特定情况下也可以连续回复例如想要追问或者补充自己先前的发言可以不使用stop或者wait
4.如果你想指导麦麦直接发言,可以不使用任何工具
你的输出规则:
1. 默认直接输出你当前的最新分析,不要重复之前的分析内容。

View File

@@ -1,12 +0,0 @@
你是一个对话上下文总结模块。你的任务是对早期的对话内容进行简洁的总结,以便存入记忆系统。
总结要求:
1. 提取对话中的关键信息(人名、事件、时间、地点等)
2. 记录用户的态度、情绪和偏好
3. 保留重要的对话内容和结论
4. 总结要简洁明了,便于后续检索和理解
5. 用第三人称客观叙述,不要包含「我记得」「之前说过」等指代词
输出格式:
- 2-5 句话的简洁总结
- 直接输出总结内容,不要有前缀或格式标题

View File

@@ -55,7 +55,7 @@ CONFIG_DIR: Path = PROJECT_ROOT / "config"
BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
MMC_VERSION: str = "1.0.0"
CONFIG_VERSION: str = "8.1.2"
CONFIG_VERSION: str = "8.1.4"
MODEL_CONFIG_VERSION: str = "1.12.0"
logger = get_logger("config")

View File

@@ -1600,6 +1600,24 @@ class MaiSakaConfig(ConfigBase):
)
"""是否在 CLI 中显示 analyze_timing 的 Prompt"""
show_thinking: bool = Field(
default=True,
json_schema_extra={
"x-widget": "switch",
"x-icon": "brain",
},
)
"""鏄惁鍦?CLI 涓樉绀哄唴蹇冩€濊€冨拰瀹屾暣 Prompt"""
user_name: str = Field(
default="用户",
json_schema_extra={
"x-widget": "input",
"x-icon": "user",
},
)
"""MaiSaka 涓敤鎴风殑鏄剧ず鍚嶇О"""
class PluginRuntimeConfig(ConfigBase):
"""插件运行时配置类"""

View File

@@ -2,7 +2,7 @@
MaiSaka built-in tool definitions.
"""
from typing import Any, Dict, List
from typing import List
from src.llm_models.payload_content.tool_option import ToolOption, ToolParamType
@@ -43,44 +43,6 @@ def create_builtin_tools() -> List[ToolOption]:
return tools
def builtin_tools_as_dicts() -> List[Dict[str, Any]]:
"""Return built-in tools as plain dictionaries."""
return [
{
"name": "wait",
"description": "Pause speaking and wait for the user to provide more input.",
"parameters": {
"type": "object",
"properties": {
"seconds": {
"type": "number",
"description": "How many seconds to wait before timing out.",
}
},
"required": ["seconds"],
},
},
{
"name": "reply",
"description": "Generate and emit a visible reply based on the current thought.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
{
"name": "no_reply",
"description": "Do not emit a visible reply this round and continue thinking.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
{
"name": "stop",
"description": "Stop the current inner loop and return control to the outer chat flow.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
]
def get_builtin_tools() -> List[ToolOption]:
"""Return built-in tools."""
return create_builtin_tools()
BUILTIN_TOOLS_DICTS = builtin_tools_as_dicts()

View File

@@ -1,30 +1,36 @@
"""
MaiSaka - CLI 交互界面与对话引擎
BufferCLI 整合主循环、对话引擎、子代理管理。
"""
MaiSaka CLI and conversation loop.
"""
import os
import asyncio
from datetime import datetime
from typing import Optional
from rich.panel import Panel
from rich.markdown import Markdown
from rich.text import Text
import asyncio
import os
from rich import box
from rich.markdown import Markdown
from rich.panel import Panel
from rich.text import Text
from src.common.data_models.mai_message_data_model import MaiMessage
from src.config.config import global_config
from .config import (
console,
ENABLE_EMOTION_MODULE,
ENABLE_COGNITION_MODULE,
ENABLE_TIMING_MODULE,
ENABLE_EMOTION_MODULE,
ENABLE_KNOWLEDGE_MODULE,
ENABLE_MCP,
ENABLE_TIMING_MODULE,
SHOW_THINKING,
USER_NAME,
console,
)
from .input_reader import InputReader
from .knowledge import retrieve_relevant_knowledge, store_knowledge_from_context
from .knowledge import retrieve_relevant_knowledge
from .knowledge_store import get_knowledge_store
from .llm_service import MaiSakaLLMService, build_message, remove_last_perception
from .message_adapter import format_speaker_content
from .mcp_client import MCPManager
from .timing import build_timing_info
from .tool_handlers import (
@@ -40,36 +46,32 @@ from .tool_handlers import (
class BufferCLI:
"""命令行交互界面"""
"""Command line interface for Maisaka."""
def __init__(self):
self.llm_service: Optional[MaiSakaLLMService] = None
self._reader = InputReader()
self._chat_history: Optional[list] = None # 持久化的对话历史
self._knowledge_store = get_knowledge_store() # 了解存储实例
self._chat_history: Optional[list[MaiMessage]] = None
self._knowledge_store = get_knowledge_store()
# 显示了解存储统计
knowledge_stats = self._knowledge_store.get_stats()
if knowledge_stats["total_items"] > 0:
console.print(f"[success][OK] 了解系统: {knowledge_stats['total_items']}条特征信息[/success]")
console.print(f"[success][OK] Knowledge store: {knowledge_stats['total_items']} item(s)[/success]")
else:
console.print("[muted][OK] 了解系统: 已初始化 (暂无数据)[/muted]")
# Timing 模块时间戳跟踪
console.print("[muted][OK] Knowledge store: initialized with no data[/muted]")
self._chat_start_time: Optional[datetime] = None
self._last_user_input_time: Optional[datetime] = None
self._last_assistant_response_time: Optional[datetime] = None
self._user_input_times: list[datetime] = [] # 所有用户输入时间戳
# MCP 管理器(异步初始化,在 run() 中完成)
self._user_input_times: list[datetime] = []
self._mcp_manager: Optional[MCPManager] = None
self._init_llm()
def _init_llm(self):
"""初始化 LLM 服务 - 使用主项目配置系统"""
"""Initialize the LLM service from the main project config."""
thinking_env = os.getenv("ENABLE_THINKING", "").strip().lower()
enable_thinking: Optional[bool] = True if thinking_env == "true" else False if thinking_env == "false" else None
# MaiSakaLLMService 现在使用主项目的配置系统
# 参数仅为兼容性保留,实际从 config_manager 读取配置
self.llm_service = MaiSakaLLMService(
api_key="",
base_url=None,
@@ -77,12 +79,11 @@ class BufferCLI:
enable_thinking=enable_thinking,
)
# 获取实际使用的模型名称
model_name = self.llm_service._model_name
console.print(f"[success][OK] LLM 服务已初始化[/success] [muted](模型: {model_name})[/muted]")
console.print(f"[success][OK] LLM service initialized[/success] [muted](model: {model_name})[/muted]")
def _build_tool_context(self) -> ToolHandlerContext:
"""构建工具处理器所需的上下文。"""
"""Build the shared tool handler context."""
ctx = ToolHandlerContext(
llm_service=self.llm_service,
reader=self._reader,
@@ -92,169 +93,19 @@ class BufferCLI:
return ctx
def _show_banner(self):
"""显示欢迎横幅"""
"""Render the startup banner."""
banner = Text()
banner.append("MaiSaka", style="bold cyan")
banner.append(" v2.0\n", style="muted")
banner.append("直接输入文字开始对话 | Ctrl+C 退出", style="muted")
banner.append("Type to chat | Ctrl+C to exit", style="muted")
console.print(Panel(banner, box=box.DOUBLE_EDGE, border_style="cyan", padding=(1, 2)))
console.print()
# ──────── 上下文管理 ────────
def _get_safe_removal_indices(self, chat_history: list, count: int) -> list[int]:
"""
获取可以安全删除的消息索引。
确保 tool_calls 和 tool 响应消息成对删除,避免破坏 API 要求的配对关系。
只删除完整的消息块user/assistant + 可选的 tool 响应序列)。
保留最后 3 条非 tool 消息,避免删除可能还在处理中的内容。
Returns:
可以安全删除的消息索引列表(从后往前排序)
"""
indices_to_remove = []
removed_count = 0
i = 0
# 计算保留的消息数量(最后 3 条非 tool 消息)
safe_zone_count = 3
non_tool_count = 0
for msg in reversed(chat_history):
if msg.get("role") != "tool":
non_tool_count += 1
if non_tool_count >= safe_zone_count:
break
# 只处理前 (len - non_tool_count) 条消息
max_process_index = len(chat_history) - non_tool_count
while i < max_process_index and removed_count < count:
msg = chat_history[i]
role = msg.get("role", "")
# 跳过 role=tool 的消息(它们会被对应的 assistant 消息一起处理)
if role == "tool":
i += 1
continue
# 检查这是否是一个带 tool_calls 的 assistant 消息
if role == "assistant" and "tool_calls" in msg:
# 收集这个 assistant 消息及其后续的 tool 响应消息
block_indices = [i]
j = i + 1
while j < len(chat_history):
next_msg = chat_history[j]
if next_msg.get("role") == "tool":
block_indices.append(j)
j += 1
else:
break
indices_to_remove.extend(block_indices)
removed_count += 1
i = j
elif role in ["user", "assistant"]:
# 普通消息,可以直接删除
indices_to_remove.append(i)
removed_count += 1
i += 1
else:
i += 1
# 从后往前排序,避免索引问题
return sorted(indices_to_remove, reverse=True)
async def _manage_context_length(self, chat_history: list) -> None:
"""
上下文管理:当对话历史过长时进行压缩。
当达到 20 条上下文时:
1. 移除最早 10 条上下文
2. 对这 10 条内容进行 LLM 总结
3. 将总结后的内容存入记忆
"""
CONTEXT_LIMIT = 20
COMPRESS_COUNT = 10
# 计算实际消息数量(排除 role=tool 的工具返回消息)
actual_messages = [m for m in chat_history if m.get("role") != "tool"]
if len(actual_messages) >= CONTEXT_LIMIT:
# 获取安全删除的索引
indices_to_remove = self._get_safe_removal_indices(chat_history, COMPRESS_COUNT)
if indices_to_remove:
# 收集要总结的消息(在删除前)
to_compress = []
for i in sorted(indices_to_remove):
if 0 <= i < len(chat_history):
to_compress.append(chat_history[i])
if to_compress:
# 总结上下文
try:
console.print("[accent]🧠 上下文过长,正在压缩并存入记忆...[/accent]")
summary = await self.llm_service.summarize_context(to_compress)
# 存储了解信息(如果启用)
if ENABLE_KNOWLEDGE_MODULE:
try:
knowledge_count = await store_knowledge_from_context(
self.llm_service,
to_compress,
store_result_callback=lambda cat_id, cat_name, content: console.print(
f"[muted] [OK] 存储了解信息: {cat_name}[/muted]"
),
)
if knowledge_count > 0:
console.print(f"[success][OK] 了解模块: 存储{knowledge_count}条特征信息[/success]")
except Exception as e:
console.print(f"[warning]了解存储失败: {e}[/warning]")
if summary:
# 存入记忆
# 显示压缩结果
console.print(
Panel(
Markdown(summary),
title="📝 上下文已压缩",
border_style="green",
padding=(0, 1),
style="dim",
)
)
except Exception as e:
console.print(f"[warning]上下文总结失败: {e}[/warning]")
# 从后往前删除
for i in indices_to_remove:
if 0 <= i < len(chat_history):
chat_history.pop(i)
# 清理"孤儿" tool 消息(没有对应 tool_calls 的 tool 消息)
valid_tool_call_ids = set()
for msg in chat_history:
if msg.get("role") == "assistant" and "tool_calls" in msg:
for tool_call in msg["tool_calls"]:
valid_tool_call_ids.add(tool_call.get("id", ""))
# 删除无效的 tool 消息(从后往前)
i = len(chat_history) - 1
while i >= 0:
msg = chat_history[i]
if msg.get("role") == "tool":
tool_call_id = msg.get("tool_call_id", "")
if tool_call_id not in valid_tool_call_ids:
chat_history.pop(i)
i -= 1
# ──────── LLM 循环架构 ────────
async def _start_chat(self, user_text: str):
"""接收用户输入并启动/继续 LLM 对话循环"""
"""Append user input and continue the inner loop."""
if not self.llm_service:
console.print("[warning]LLM 服务未初始化,跳过对话。[/warning]")
console.print("[warning]LLM service is not initialized; skipping chat.[/warning]")
return
now = datetime.now()
@@ -262,41 +113,33 @@ class BufferCLI:
self._user_input_times.append(now)
if self._chat_history is None:
# 首次对话:初始化上下文
self._chat_start_time = now
self._last_assistant_response_time = None
self._chat_history = self.llm_service.build_chat_context(user_text)
else:
# 后续对话:追加用户消息到已有上下文
self._chat_history.append(build_message(role="user", content=user_text))
self._chat_history.append(build_message(role="user", content=format_speaker_content(USER_NAME, user_text)))
await self._run_llm_loop(self._chat_history)
async def _run_llm_loop(self, chat_history: list):
async def _run_llm_loop(self, chat_history: list[MaiMessage]):
"""
LLM 循环架构核心。
Main inner loop for the Maisaka planner.
LLM 持续运行,每步可能输出文本(内心思考)和/或调用工具:
- say(text): 对用户说话
- wait(seconds): 暂停等待用户输入,超时或收到输入后继续
- stop(): 结束循环,进入待机,直到用户下次输入
- 不调用工具: 继续下一轮思考/生成
Each round may produce internal thoughts and optionally call tools:
- reply(): generate a visible reply for the current round
- no_reply(): skip visible output and continue the loop
- wait(seconds): wait for new user input
- stop(): stop the current inner loop and return to idle
每轮流程:
1. 上下文管理:达到上限时自动压缩
2. 情商 + Timing + 了解模块(并行):分析用户情绪、对话时间节奏、检索用户特征
*注:如果上次没有调用工具,跳过模块分析
3. 调用主 LLM基于完整上下文生成响应
Per round:
1. Run enabled analysis modules in parallel when the previous round used tools.
2. Call the planner model with the current history.
3. Append the assistant thought and execute any requested tools.
"""
consecutive_errors = 0
last_had_tool_calls = True # 第一次循环总是执行模块分析
last_had_tool_calls = True
while True:
# ── 上下文管理 ──
await self._manage_context_length(chat_history)
# ── 情商模块 + Timing 模块 + 了解模块(并行) ──
# 只有上次调用了工具才重新分析(首次循环除外)
if last_had_tool_calls:
timing_info = build_timing_info(
self._chat_start_time,
@@ -305,30 +148,28 @@ class BufferCLI:
self._user_input_times,
)
# 根据配置决定要执行的模块
tasks = []
status_text_parts = []
if ENABLE_EMOTION_MODULE:
tasks.append(("eq", self.llm_service.analyze_emotion(chat_history)))
status_text_parts.append("🎭")
status_text_parts.append("emotion")
if ENABLE_COGNITION_MODULE:
tasks.append(("cognition", self.llm_service.analyze_cognition(chat_history)))
status_text_parts.append("🧩")
status_text_parts.append("cognition")
if ENABLE_TIMING_MODULE:
tasks.append(("timing", self.llm_service.analyze_timing(chat_history, timing_info)))
status_text_parts.append("⏱️🪞")
status_text_parts.append("timing")
if ENABLE_KNOWLEDGE_MODULE:
tasks.append(("knowledge", retrieve_relevant_knowledge(self.llm_service, chat_history)))
status_text_parts.append("👤")
status_text_parts.append("knowledge")
with console.status(
f"[info]{' '.join(status_text_parts)} {' + '.join(status_text_parts)} 模块并行分析中...[/info]",
f"[info]{' + '.join(status_text_parts)} analyzing...[/info]",
spinner="dots",
):
results = await asyncio.gather(*[task for _, task in tasks], return_exceptions=True)
# 解析结果
eq_result, cognition_result, timing_result, knowledge_result = None, None, None, None
result_idx = 0
if ENABLE_EMOTION_MODULE:
@@ -344,133 +185,125 @@ class BufferCLI:
knowledge_result = results[result_idx]
result_idx += 1
# 处理情商模块结果
eq_analysis = ""
if ENABLE_EMOTION_MODULE:
if isinstance(eq_result, Exception):
console.print(f"[warning]情商模块分析失败: {eq_result}[/warning]")
console.print(f"[warning]Emotion analysis failed: {eq_result}[/warning]")
elif eq_result:
eq_analysis = eq_result
console.print(
Panel(
Markdown(eq_analysis),
title="🎭 情绪感知",
border_style="bright_yellow",
padding=(0, 1),
style="dim",
if SHOW_THINKING:
console.print(
Panel(
Markdown(eq_analysis),
title="Emotion",
border_style="bright_yellow",
padding=(0, 1),
style="dim",
)
)
)
# 处理认知模块结果
cognition_analysis = ""
if ENABLE_COGNITION_MODULE:
if isinstance(cognition_result, Exception):
console.print(f"[warning]认知模块分析失败: {cognition_result}[/warning]")
console.print(f"[warning]Cognition analysis failed: {cognition_result}[/warning]")
elif cognition_result:
cognition_analysis = cognition_result
console.print(
Panel(
Markdown(cognition_analysis),
title="🧩 意图感知",
border_style="bright_cyan",
padding=(0, 1),
style="dim",
if SHOW_THINKING:
console.print(
Panel(
Markdown(cognition_analysis),
title="Cognition",
border_style="bright_cyan",
padding=(0, 1),
style="dim",
)
)
)
# 处理 Timing 模块结果(含自我反思功能)
timing_analysis = ""
if ENABLE_TIMING_MODULE:
if isinstance(timing_result, Exception):
console.print(f"[warning]Timing 模块分析失败: {timing_result}[/warning]")
console.print(f"[warning]Timing analysis failed: {timing_result}[/warning]")
elif timing_result:
timing_analysis = timing_result
console.print(
Panel(
Markdown(timing_analysis),
title="⏱️🪞 时间感知 & 自我反思",
border_style="bright_blue",
padding=(0, 1),
style="dim",
if SHOW_THINKING:
console.print(
Panel(
Markdown(timing_analysis),
title="Timing",
border_style="bright_blue",
padding=(0, 1),
style="dim",
)
)
)
# 处理了解模块结果
knowledge_analysis = ""
if ENABLE_KNOWLEDGE_MODULE:
if isinstance(knowledge_result, Exception):
console.print(f"[warning]了解模块分析失败: {knowledge_result}[/warning]")
console.print(f"[warning]Knowledge analysis failed: {knowledge_result}[/warning]")
elif knowledge_result:
knowledge_analysis = knowledge_result
console.print(
Panel(
Markdown(knowledge_analysis),
title="👤 用户特征",
border_style="bright_magenta",
padding=(0, 1),
style="dim",
if SHOW_THINKING:
console.print(
Panel(
Markdown(knowledge_analysis),
title="Knowledge",
border_style="bright_magenta",
padding=(0, 1),
style="dim",
)
)
)
# 注入感知信息(作为 assistant 的感知消息)
# 移除上一条感知消息(如果存在)
remove_last_perception(chat_history)
# 构建感知内容
perception_parts = []
if eq_analysis:
perception_parts.append(f"情绪感知\n{eq_analysis}")
perception_parts.append(f"Emotion\n{eq_analysis}")
if cognition_analysis:
perception_parts.append(f"意图感知\n{cognition_analysis}")
perception_parts.append(f"Cognition\n{cognition_analysis}")
if timing_analysis:
perception_parts.append(f"时间感知 & 自我反思\n{timing_analysis}")
perception_parts.append(f"Timing\n{timing_analysis}")
if knowledge_analysis:
perception_parts.append(f"用户特征\n{knowledge_analysis}")
perception_parts.append(f"Knowledge\n{knowledge_analysis}")
if perception_parts:
# 添加感知消息AI 的感知能力结果)
chat_history.append(
build_message(
role="assistant",
content="\n\n".join(perception_parts),
msg_type="perception",
message_kind="perception",
source="assistant",
)
)
else:
# 上次没有调用工具,跳过模块分析
console.print("[muted] 上次未调用工具,跳过模块分析[/muted]")
if SHOW_THINKING:
console.print("[muted]Skipping module analysis because the last round used no tools.[/muted]")
# ── 调用 LLM ──
with console.status("[info]💬 AI 正在思考...[/info]", spinner="dots"):
with console.status("[info]AI is thinking...[/info]", spinner="dots"):
try:
response = await self.llm_service.chat_loop_step(chat_history)
consecutive_errors = 0
except Exception as e:
except Exception as exc:
consecutive_errors += 1
console.print(f"[error]LLM 调用出错: {e}[/error]")
console.print(f"[error]LLM call failed: {exc}[/error]")
if consecutive_errors >= 3:
console.print("[error]连续出错,退出对话[/error]\n")
console.print("[error]Too many consecutive errors. Exiting chat.[/error]\n")
break
continue
# 将 assistant 消息追加到历史
chat_history.append(response.raw_message)
self._last_assistant_response_time = datetime.now()
# 显示内心思考content 部分,淡色呈现)
if response.content:
if SHOW_THINKING and response.content:
console.print(
Panel(
Markdown(response.content),
title="💭 内心思考",
title="Thought",
border_style="dim",
padding=(1, 2),
style="dim",
)
)
# ── 处理工具调用 ──
if response.content and not response.tool_calls:
last_had_tool_calls = False
continue
@@ -480,76 +313,73 @@ class BufferCLI:
ctx = self._build_tool_context()
for tc in response.tool_calls:
if tc.name == "stop":
if tc.func_name == "stop":
await handle_stop(tc, chat_history)
should_stop = True
elif tc.name == "reply":
elif tc.func_name == "reply":
reply = await self._generate_visible_reply(chat_history, response.content)
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": "Visible reply generated and recorded.",
}
build_message(
role="tool",
content="Visible reply generated and recorded.",
source="tool",
tool_call_id=tc.call_id,
)
)
chat_history.append(
build_message(
role="user",
content=f"\u3010\u9ea6\u9ea6\u7684\u53d1\u8a00\u3011{reply}",
content=format_speaker_content(global_config.bot.nickname.strip() or "MaiSaka", reply),
source="guided_reply",
)
)
elif tc.name == "no_reply":
console.print("[muted]No visible reply this round.[/muted]")
elif tc.func_name == "no_reply":
if SHOW_THINKING:
console.print("[muted]No visible reply this round.[/muted]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": "No visible reply was sent for this round.",
}
build_message(
role="tool",
content="No visible reply was sent for this round.",
source="tool",
tool_call_id=tc.call_id,
)
)
elif tc.name == "wait":
elif tc.func_name == "wait":
tool_result = await handle_wait(tc, chat_history, ctx)
# 同步回 timing 时间戳
if ctx.last_user_input_time != self._last_user_input_time:
self._last_user_input_time = ctx.last_user_input_time
if tool_result.startswith("[[QUIT]]"):
should_stop = True
elif tc.name == "write_file":
elif tc.func_name == "write_file":
await handle_write_file(tc, chat_history)
elif tc.name == "read_file":
elif tc.func_name == "read_file":
await handle_read_file(tc, chat_history)
elif tc.name == "list_files":
elif tc.func_name == "list_files":
await handle_list_files(tc, chat_history)
elif self._mcp_manager and self._mcp_manager.is_mcp_tool(tc.name):
elif self._mcp_manager and self._mcp_manager.is_mcp_tool(tc.func_name):
await handle_mcp_tool(tc, chat_history, self._mcp_manager)
else:
await handle_unknown_tool(tc, chat_history)
if should_stop:
console.print("[muted]对话暂停,等待新输入...[/muted]\n")
console.print("[muted]Conversation paused. Waiting for new input...[/muted]\n")
break
# 调用了工具,下次循环需要重新分析模块
last_had_tool_calls = True
else:
# LLM 未调用任何工具 → 继续下一轮思考
# (不做任何额外操作,直接回到循环顶部再次调用 LLM
# 标记上次没有调用工具,下次循环跳过模块分析
last_had_tool_calls = False
continue
# ──────── 主循环 ────────
async def _init_mcp(self):
"""初始化 MCP 服务器连接,发现并注册外部工具。"""
"""Initialize MCP servers and register exposed tools."""
config_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"mcp_config.json",
@@ -563,14 +393,14 @@ class BufferCLI:
summary = self._mcp_manager.get_tool_summary()
console.print(
Panel(
f"已加载 {len(mcp_tools)} MCP 工具:\n{summary}",
title="🔌 MCP 工具",
f"Loaded {len(mcp_tools)} MCP tool(s):\n{summary}",
title="MCP Tools",
border_style="green",
padding=(0, 1),
)
)
async def _generate_visible_reply(self, chat_history: list, latest_thought: str) -> str:
async def _generate_visible_reply(self, chat_history: list[MaiMessage], latest_thought: str) -> str:
"""Generate and emit a visible reply based on the latest thought."""
if not self.llm_service or not latest_thought:
return ""
@@ -586,19 +416,17 @@ class BufferCLI:
padding=(1, 2),
)
)
return reply
async def run(self):
async def run(self):
"""主循环:直接输入文本即可对话"""
"""Main interactive loop."""
if ENABLE_MCP:
await self._init_mcp()
else:
else:
console.print("[muted]MCP is disabled (ENABLE_MCP=false)[/muted]")
self._reader.start(asyncio.get_event_loop())
self._reader.start(asyncio.get_event_loop())
self._show_banner()
try:
@@ -606,8 +434,8 @@ class BufferCLI:
console.print("[bold cyan]> [/bold cyan]", end="")
raw_input = await self._reader.get_line()
if raw_input is None: # EOF
if raw_input is None:
console.print("\n[muted]Goodbye![/muted]")
break
raw_input = raw_input.strip()
@@ -618,6 +446,3 @@ class BufferCLI:
finally:
if self._mcp_manager:
await self._mcp_manager.close()
await self._mcp_manager.close()

View File

@@ -27,6 +27,8 @@ ENABLE_READ_FILE = global_config.maisaka.enable_read_file
ENABLE_LIST_FILES = global_config.maisaka.enable_list_files
SHOW_ANALYZE_COGNITION_PROMPT = global_config.maisaka.show_analyze_cognition_prompt
SHOW_ANALYZE_TIMING_PROMPT = global_config.maisaka.show_analyze_timing_prompt
SHOW_THINKING = global_config.maisaka.show_thinking
USER_NAME = global_config.maisaka.user_name.strip() or "用户"
# ──────────────────── Rich 主题 & Console ────────────────────

View File

@@ -8,8 +8,13 @@ MaiSaka - Emotion 模块
from typing import List, Optional
from src.common.data_models.mai_message_data_model import MaiMessage
def extract_user_messages(chat_history: List[dict], limit: Optional[int] = None) -> List[dict]:
from .config import USER_NAME
from .message_adapter import get_message_role, get_message_text
def extract_user_messages(chat_history: List[MaiMessage], limit: Optional[int] = None) -> List[MaiMessage]:
"""
从对话历史中提取用户消息。
@@ -20,13 +25,13 @@ def extract_user_messages(chat_history: List[dict], limit: Optional[int] = None)
Returns:
只包含用户消息的列表
"""
user_messages = [msg for msg in chat_history if msg.get("role") == "user"]
user_messages = [msg for msg in chat_history if get_message_role(msg) == "user"]
if limit and len(user_messages) > limit:
return user_messages[-limit:]
return user_messages
def build_emotion_context(chat_history: List[dict]) -> str:
def build_emotion_context(chat_history: List[MaiMessage]) -> str:
"""
构建用于情绪分析的对话上下文文本。
@@ -41,11 +46,11 @@ def build_emotion_context(chat_history: List[dict]) -> str:
context_parts = []
for msg in recent_messages:
role = msg.get("role", "")
content = msg.get("content", "")
role = get_message_role(msg)
content = get_message_text(msg)
if role == "user":
context_parts.append(f"用户: {content}")
context_parts.append(f"{USER_NAME}: {content}")
elif role == "assistant":
# 只显示 assistant 的实际发言,跳过感知信息
if "【AI 感知】" not in content:

View File

@@ -1,171 +1,58 @@
"""
MaiSaka - 了解模块
负责从对话中提取和存储用户个人特征信息。
MaiSaka knowledge retrieval helpers.
"""
from typing import List
from src.common.data_models.mai_message_data_model import MaiMessage
from .knowledge_store import KNOWLEDGE_CATEGORIES, get_knowledge_store
def build_knowledge_summary() -> str:
"""
构建了解分类摘要,用于 LLM 请求。
Returns:
格式化的分类列表文本
"""
store = get_knowledge_store()
return store.get_categories_summary()
NO_RESULT_KEYWORDS = [
"\u65e0",
"\u6ca1\u6709",
"\u4e0d\u9002\u7528",
"\u65e0\u9700",
"\u65e0\u76f8\u5173",
]
def extract_category_ids_from_result(result: str) -> List[str]:
"""
从 LLM 返回结果中提取分类编号。
Args:
result: LLM 返回的结果文本
Returns:
分类编号列表
"""
"""Extract valid category ids from an LLM result string."""
if not result:
return []
# 检查是否表示"无相关内容"
if any(keyword in result for keyword in ["", "没有", "不适用", "无需", "无相关"]):
normalized = result.strip()
if not normalized:
return []
# 解析编号(支持逗号分隔、空格分隔、换行分隔)
category_ids = []
for part in result.replace(",", " ").replace("", " ").replace("\n", " ").split():
part = part.strip()
if part in KNOWLEDGE_CATEGORIES:
category_ids.append(part)
lowered = normalized.lower()
if any(keyword in lowered for keyword in ["none", "no relevant", "no_need", "no need"]):
return []
if any(keyword in normalized for keyword in NO_RESULT_KEYWORDS):
return []
category_ids: List[str] = []
for part in normalized.replace(",", " ").replace("\uff0c", " ").replace("\n", " ").split():
candidate = part.strip()
if candidate in KNOWLEDGE_CATEGORIES and candidate not in category_ids:
category_ids.append(candidate)
return category_ids
def format_context_for_memory(context_messages: List[dict]) -> str:
"""
格式化上下文消息为文本,用于记忆分析。
Args:
context_messages: 上下文消息列表
Returns:
格式化后的文本
"""
parts = []
for msg in context_messages:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "user":
parts.append(f"用户: {content}")
elif role == "assistant":
# 跳过感知消息
if "【AI 感知】" not in content:
parts.append(f"助手: {content}")
return "\n".join(parts)
async def store_knowledge_from_context(
llm_service,
context_messages: List[dict],
store_result_callback=None,
) -> int:
"""
记忆部分:从上下文中提取并存储了解信息。
在上下文裁切时触发:
1. 请求 LLM 分析聊天内容涉及哪些分类
2. 为每个分类创建 subAgent 提取相关内容
3. 存入了解列表
Args:
llm_service: LLM 服务实例
context_messages: 需要分析的上下文消息
store_result_callback: 存储结果回调函数
Returns:
成功存储的了解信息数量
"""
store = get_knowledge_store()
context_text = format_context_for_memory(context_messages)
categories_summary = build_knowledge_summary()
if not context_text:
return 0
try:
# 第一步:分析涉及哪些分类
category_ids = await llm_service.analyze_knowledge_categories(context_messages, categories_summary)
if not category_ids:
return 0
# 第二步:为每个分类提取内容并存储
stored_count = 0
for category_id in category_ids:
try:
# 提取该分类的相关内容
extracted_content = await llm_service.extract_knowledge_for_category(
context_messages, category_id, store.get_category_name(category_id)
)
if extracted_content:
# 存储到了解列表
success = store.add_knowledge(
category_id=category_id, content=extracted_content, metadata={"source": "context_compression"}
)
if success:
stored_count += 1
if store_result_callback:
store_result_callback(category_id, store.get_category_name(category_id), extracted_content)
except Exception:
# 单个分类失败不影响其他分类
continue
return stored_count
except Exception:
return 0
async def retrieve_relevant_knowledge(
llm_service,
chat_history: List[dict],
chat_history: List[MaiMessage],
) -> str:
"""
提取部分:根据当前上下文检索相关的了解信息。
在每次对话前触发EQ 模块和 timing 模块位置):
1. 请求 LLM 分析需要哪些分类的了解内容
2. 提取对应分类的所有内容并拼接
3. 返回格式化后的了解内容
Args:
llm_service: LLM 服务实例
chat_history: 当前对话历史
Returns:
格式化后的了解内容文本
"""
"""Retrieve formatted knowledge snippets relevant to the current chat history."""
store = get_knowledge_store()
categories_summary = store.get_categories_summary()
try:
# 分析需要哪些分类
category_ids = await llm_service.analyze_knowledge_need(chat_history, categories_summary)
if not category_ids:
return ""
# 获取并格式化了解内容
formatted_knowledge = store.get_formatted_knowledge(category_ids)
return formatted_knowledge
return store.get_formatted_knowledge(category_ids)
except Exception:
return ""

View File

@@ -5,78 +5,47 @@ MaiSaka LLM 服务 - 使用主项目 LLM 系统
from datetime import datetime
import json
import random
from dataclasses import dataclass
from typing import Any, List, Literal, Optional
from typing import Any, List, Optional
from rich.console import Group
from rich.panel import Panel
from rich.pretty import Pretty
from rich.text import Text
from src.common.data_models.mai_message_data_model import MaiMessage
from src.common.logger import get_logger
from src.config.config import config_manager, global_config
from src.llm_models.payload_content.message import MessageBuilder, RoleType
from src.llm_models.payload_content.tool_option import ToolCall as ToolCallOption, ToolOption
from src.llm_models.payload_content.message import Message, MessageBuilder, RoleType
from src.llm_models.payload_content.tool_option import ToolCall, ToolOption
from src.llm_models.utils_model import LLMRequest
from src.prompt.prompt_manager import prompt_manager
from . import config
from .config import console
from .builtin_tools import get_builtin_tools
from .message_adapter import (
build_message,
format_speaker_content,
get_message_kind,
get_message_role,
get_message_text,
get_tool_call_id,
get_tool_calls,
remove_last_perception,
to_llm_message,
)
logger = get_logger("maisaka_llm")
# ──────────────────── 消息类型 ────────────────────
MessageType = Literal["user", "assistant", "system", "perception"]
# 内部使用的字段前缀,用于标记不应发送给 API 的元数据
INTERNAL_FIELD_PREFIX = "_"
# 消息类型字段名
MSG_TYPE_FIELD = "_type"
@dataclass
class ToolCall:
"""工具调用信息"""
id: str
name: str
arguments: dict
@dataclass
class ChatResponse:
"""LLM 对话循环单步响应"""
content: Optional[str]
tool_calls: List[ToolCall]
raw_message: dict # 可直接追加到对话历史的消息字典
# ──────────────────── 工具函数 ────────────────────
def build_message(role: str, content: str, msg_type: MessageType = "user", **kwargs) -> dict:
"""构建消息字典,包含消息类型标记。"""
msg = {
"role": role,
"content": content,
MSG_TYPE_FIELD: msg_type,
"_time": datetime.now().strftime("%H:%M:%S"),
**kwargs,
}
return msg
def remove_last_perception(messages: list[dict]) -> None:
"""移除最后一条感知消息(直接修改原列表)。"""
for i in range(len(messages) - 1, -1, -1):
if messages[i].get(MSG_TYPE_FIELD) == "perception":
messages.pop(i)
break
raw_message: MaiMessage
class MaiSakaLLMService:
@@ -132,7 +101,6 @@ class MaiSakaLLMService:
if chat_system_prompt is None:
try:
chat_prompt = prompt_manager.get_prompt("maidairy_chat")
logger.info("成功加载 maidairy_chat 提示词模板")
tools_section = ""
if config.ENABLE_WRITE_FILE:
tools_section += "\n• write_file(filename, content) — 在 mai_files 目录下写入文件。"
@@ -142,6 +110,7 @@ class MaiSakaLLMService:
tools_section += "\n• list_files() — 获取 mai_files 目录下所有文件的元信息列表。"
chat_prompt.add_context("file_tools_section", tools_section if tools_section else "")
chat_prompt.add_context("bot_name", global_config.bot.nickname)
chat_prompt.add_context("identity", personality_prompt)
import asyncio
@@ -167,8 +136,6 @@ class MaiSakaLLMService:
self._emotion_prompt: Optional[str] = None
self._cognition_prompt: Optional[str] = None
self._timing_prompt: Optional[str] = None
self._context_summarize_prompt: Optional[str] = None
try:
import asyncio
@@ -184,9 +151,6 @@ class MaiSakaLLMService:
self._timing_prompt = loop.run_until_complete(
prompt_manager.render_prompt(prompt_manager.get_prompt("maidairy_timing"))
)
self._context_summarize_prompt = loop.run_until_complete(
prompt_manager.render_prompt(prompt_manager.get_prompt("maidairy_context_summarize"))
)
logger.info("成功加载 MaiSaka 子模块提示词")
finally:
loop.close()
@@ -367,12 +331,12 @@ class MaiSakaLLMService:
params.append((param.name, param.param_type, param.description, param.required, param.enum_values))
return {"name": tool.name, "description": tool.description, "parameters": params}
async def chat_loop_step(self, chat_history: List[dict]) -> ChatResponse:
async def chat_loop_step(self, chat_history: list[MaiMessage]) -> ChatResponse:
"""执行对话循环的一步 - 使用 tool_use 模型"""
def message_factory(client) -> List:
def message_factory(client) -> list[Message]:
"""将 MaiSaka 的 chat_history 转换为主项目的 Message 格式"""
messages = []
messages: list[Message] = []
# 首先添加系统提示词
system_msg = MessageBuilder().set_role(RoleType.System)
@@ -381,48 +345,9 @@ class MaiSakaLLMService:
# 然后添加对话历史
for msg in chat_history:
role = msg.get("role", "")
content = msg.get("content", "")
# 跳过内部字段类型的消息和系统消息(已经有系统提示词了)
if role in ("perception", "system"):
continue
# 映射角色类型
if role == "user":
role_type = RoleType.User
elif role == "assistant":
role_type = RoleType.Assistant
elif role == "tool":
role_type = RoleType.Tool
else:
continue
builder = MessageBuilder().set_role(role_type)
# 处理工具调用
if role == "assistant" and "tool_calls" in msg:
# 转换 tool_calls 格式:从 MaiSaka 格式转为主项目格式
tool_calls_list = []
for tc in msg["tool_calls"]:
tc_func = tc.get("function", {})
# 主项目的 ToolCall: call_id, func_name, args
tool_calls_list.append(
ToolCallOption(
call_id=tc.get("id", ""),
func_name=tc_func.get("name", ""),
args=json.loads(tc_func.get("arguments", "{}")) if tc_func.get("arguments") else {},
)
)
builder.set_tool_calls(tool_calls_list)
elif role == "tool" and "tool_call_id" in msg:
builder.add_tool_call(msg["tool_call_id"])
# 添加文本内容
if content:
builder.add_text_content(content)
messages.append(builder.build())
llm_message = to_llm_message(msg)
if llm_message is not None:
messages.append(llm_message)
return messages
@@ -435,33 +360,18 @@ class MaiSakaLLMService:
# 打印消息列表
built_messages = message_factory(None)
# 将消息分为普通消息和 tool 消息
non_tool_panels = []
tool_panels = []
ordered_panels = [self._render_message_panel(msg, index + 1) for index, msg in enumerate(built_messages)]
for index, msg in enumerate(built_messages):
panel = self._render_message_panel(msg, index + 1)
role = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
if role == "tool":
tool_panels.append(panel)
else:
non_tool_panels.append(panel)
# 先显示普通消息group 在一个 panel 内)
if non_tool_panels:
if config.SHOW_THINKING and ordered_panels:
console.print(
Panel(
Group(*non_tool_panels),
Group(*ordered_panels),
title="MaiSaka LLM Request - chat_loop_step",
border_style="cyan",
padding=(0, 1),
)
)
# tool 消息作为单独的块展示
for panel in tool_panels:
console.print(panel)
response, (reasoning, model, tool_calls) = await self._llm_chat.generate_response_with_message_async(
message_factory=message_factory,
@@ -469,86 +379,60 @@ class MaiSakaLLMService:
temperature=self._temperature,
max_tokens=self._max_tokens,
)
# 转换 tool_calls 格式:从主项目格式转为 MaiSaka 格式
converted_tool_calls = []
if tool_calls:
for tc in tool_calls:
# 主项目的 ToolCall 有 call_id, func_name, args
call_id = tc.call_id if hasattr(tc, "call_id") else ""
func_name = tc.func_name if hasattr(tc, "func_name") else ""
args = tc.args if hasattr(tc, "args") else {}
converted_tool_calls.append(
ToolCall(
id=call_id,
name=func_name,
arguments=args,
)
)
# 构建原始消息格式MaiSaka 风格)
raw_message = {
"role": "assistant",
"content": response,
"_time": datetime.now().strftime("%H:%M:%S"),
}
if converted_tool_calls:
raw_message["tool_calls"] = [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.name,
"arguments": json.dumps(tc.arguments),
},
}
for tc in converted_tool_calls
]
raw_message = build_message(
role=RoleType.Assistant.value,
content=response or "",
source="assistant",
tool_calls=tool_calls or None,
)
return ChatResponse(
content=response,
tool_calls=converted_tool_calls,
tool_calls=tool_calls or [],
raw_message=raw_message,
)
def _filter_for_api(self, chat_history: List[dict]) -> str:
def _filter_for_api(self, chat_history: list[MaiMessage]) -> str:
"""过滤对话历史为 API 格式"""
parts = []
for msg in chat_history:
role = msg.get("role", "")
content = msg.get("content", "")
role = get_message_role(msg)
content = get_message_text(msg)
# 跳过内部字段
if role in ("perception", "tool"):
if get_message_kind(msg) == "perception" or role == RoleType.Tool.value:
continue
if role == "system":
if role == RoleType.System.value:
parts.append(f"System: {content}")
elif role == "user":
elif role == RoleType.User.value:
parts.append(f"User: {content}")
elif role == "assistant":
elif role == RoleType.Assistant.value:
# 处理工具调用
if "tool_calls" in msg:
tool_desc = ", ".join([tc.get("name", "") for tc in msg["tool_calls"]])
tool_calls = get_tool_calls(msg)
if tool_calls:
tool_desc = ", ".join([tc.func_name for tc in tool_calls if tc.func_name])
parts.append(f"Assistant (called tools: {tool_desc})")
else:
parts.append(f"Assistant: {content}")
return "\n\n".join(parts)
def build_chat_context(self, user_text: str) -> List[dict]:
def build_chat_context(self, user_text: str) -> list[MaiMessage]:
"""构建对话上下文"""
return [
{"role": "system", "content": self._chat_system_prompt},
{"role": "user", "content": user_text},
build_message(
role=RoleType.User.value,
content=format_speaker_content(config.USER_NAME, user_text),
source="user",
)
]
# ──────── 分析模块(使用 utils 模型) ────────
async def analyze_emotion(self, chat_history: List[dict]) -> str:
async def analyze_emotion(self, chat_history: list[MaiMessage]) -> str:
"""情绪分析 - 使用 utils 模型"""
filtered = [m for m in chat_history if m.get("_type") != "perception"]
filtered = [m for m in chat_history if get_message_kind(m) != "perception"]
recent = filtered[-10:] if len(filtered) > 10 else filtered
# 使用加载的系统提示词
@@ -556,17 +440,20 @@ class MaiSakaLLMService:
prompt_parts = [f"{system_prompt}\n\n【对话内容】\n"]
for msg in recent:
if msg.get("role") == "user":
prompt_parts.append(f"用户: {msg.get('content', '')}")
elif msg.get("role") == "assistant":
prompt_parts.append(f"助手: {msg.get('content', '')}")
role = get_message_role(msg)
content = get_message_text(msg)
if role == RoleType.User.value:
prompt_parts.append(f"{config.USER_NAME}: {content}")
elif role == RoleType.Assistant.value:
prompt_parts.append(f"助手: {content}")
prompt = "\n".join(prompt_parts)
print("\n" + "=" * 60)
print("MaiSaka LLM Request - analyze_emotion:")
print(f" {prompt}")
print("=" * 60 + "\n")
if config.SHOW_THINKING:
print("\n" + "=" * 60)
print("MaiSaka LLM Request - analyze_emotion:")
print(f" {prompt}")
print("=" * 60 + "\n")
try:
response, _ = await self._llm_utils.generate_response_async(
@@ -580,9 +467,9 @@ class MaiSakaLLMService:
logger.error(f"情绪分析 LLM 调用出错: {e}")
return ""
async def analyze_cognition(self, chat_history: List[dict]) -> str:
async def analyze_cognition(self, chat_history: list[MaiMessage]) -> str:
"""认知分析 - 使用 utils 模型"""
filtered = [m for m in chat_history if m.get("_type") != "perception"]
filtered = [m for m in chat_history if get_message_kind(m) != "perception"]
recent = filtered[-10:] if len(filtered) > 10 else filtered
# 使用加载的系统提示词
@@ -590,14 +477,16 @@ class MaiSakaLLMService:
prompt_parts = [f"{system_prompt}\n\n【对话内容】\n"]
for msg in recent:
if msg.get("role") == "user":
prompt_parts.append(f"用户: {msg.get('content', '')}")
elif msg.get("role") == "assistant":
prompt_parts.append(f"助手: {msg.get('content', '')}")
role = get_message_role(msg)
content = get_message_text(msg)
if role == RoleType.User.value:
prompt_parts.append(f"{config.USER_NAME}: {content}")
elif role == RoleType.Assistant.value:
prompt_parts.append(f"助手: {content}")
prompt = "\n".join(prompt_parts)
if config.SHOW_ANALYZE_COGNITION_PROMPT:
if config.SHOW_THINKING and config.SHOW_ANALYZE_COGNITION_PROMPT:
print("\n" + "=" * 60)
print("MaiSaka LLM Request - analyze_cognition:")
print(f" {prompt}")
@@ -615,25 +504,29 @@ class MaiSakaLLMService:
logger.error(f"认知分析 LLM 调用出错: {e}")
return ""
async def analyze_timing(self, chat_history: List[dict], timing_info: str) -> str:
async def analyze_timing(self, chat_history: list[MaiMessage], timing_info: str) -> str:
"""时间分析 - 使用 utils 模型"""
filtered = [m for m in chat_history if m.get("_type") not in ("perception", "system")]
filtered = [
m
for m in chat_history
if get_message_kind(m) != "perception" and get_message_role(m) != RoleType.System.value
]
# 使用加载的系统提示词
system_prompt = self._timing_prompt or "请分析以下对话的时间节奏和用户状态:"
prompt_parts = [f"{system_prompt}\n\n【系统时间戳信息】\n{timing_info}\n\n【当前对话记录】\n"]
for msg in filtered:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "user":
prompt_parts.append(f"用户: {content}")
elif role == "assistant":
role = get_message_role(msg)
content = get_message_text(msg)
if role == RoleType.User.value:
prompt_parts.append(f"{config.USER_NAME}: {content}")
elif role == RoleType.Assistant.value:
prompt_parts.append(f"助手: {content}")
prompt = "\n".join(prompt_parts)
if config.SHOW_ANALYZE_TIMING_PROMPT:
if config.SHOW_THINKING and config.SHOW_ANALYZE_TIMING_PROMPT:
print("\n" + "=" * 60)
print("MaiSaka LLM Request - analyze_timing:")
print(f" {prompt}")
@@ -651,44 +544,9 @@ class MaiSakaLLMService:
logger.error(f"时间分析 LLM 调用出错: {e}")
return ""
async def summarize_context(self, context_messages: List[dict]) -> str:
"""上下文总结 - 使用 utils 模型"""
filtered = [m for m in context_messages if m.get("role") != "system"]
# 使用加载的系统提示词
system_prompt = self._context_summarize_prompt or "请对以下对话内容进行总结:"
prompt_parts = [f"{system_prompt}\n\n【对话内容】\n"]
for msg in filtered:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "user":
prompt_parts.append(f"用户: {content}")
elif role == "assistant":
prompt_parts.append(f"助手: {content}")
prompt = "\n".join(prompt_parts)
print("\n" + "=" * 60)
print("MaiSaka LLM Request - summarize_context:")
print(f" {prompt}")
print("=" * 60 + "\n")
try:
response, _ = await self._llm_utils.generate_response_async(
prompt=prompt,
temperature=0.3,
max_tokens=1024,
)
return response
except Exception as e:
logger.error(f"上下文总结 LLM 调用出错: {e}")
return ""
# ──────── 回复生成(使用 replyer 模型) ────────
async def generate_reply(self, reason: str, chat_history: List[dict]) -> str:
async def generate_reply(self, reason: str, chat_history: list[MaiMessage]) -> str:
"""
生成回复 - 使用 replyer 模型
可供 Replyer 类直接调用
@@ -700,7 +558,9 @@ class MaiSakaLLMService:
# 格式化对话历史
filtered_history = [
msg for msg in chat_history if msg.get("role") != "system" and msg.get("_type") != "perception"
msg
for msg in chat_history
if get_message_role(msg) != RoleType.System.value and get_message_kind(msg) != "perception"
]
formatted_history = format_chat_history(filtered_history)
@@ -717,10 +577,11 @@ class MaiSakaLLMService:
messages = f"System: {system_prompt}\n\nUser: {user_prompt}"
print("\n" + "=" * 60)
print("MaiSaka LLM Request - generate_reply:")
print(f" {messages}")
print("=" * 60 + "\n")
if config.SHOW_THINKING:
print("\n" + "=" * 60)
print("MaiSaka LLM Request - generate_reply:")
print(f" {messages}")
print("=" * 60 + "\n")
try:
response, _ = await self._llm_replyer.generate_response_async(

View File

@@ -0,0 +1,181 @@
"""
MaiSaka message adapters built on top of the main project's MaiMessage model.
"""
from datetime import datetime
import re
from typing import Optional
from uuid import uuid4
from src.common.data_models.mai_message_data_model import MaiMessage, MessageInfo, UserInfo
from src.common.data_models.message_component_data_model import MessageSequence
from src.config.config import global_config
from src.llm_models.payload_content.message import Message, MessageBuilder, RoleType
from src.llm_models.payload_content.tool_option import ToolCall
from .config import USER_NAME
MAISAKA_PLATFORM = "maisaka"
MAISAKA_SESSION_ID = "maisaka_cli"
MESSAGE_KIND_KEY = "maisaka_message_kind"
SOURCE_KEY = "maisaka_source"
LLM_ROLE_KEY = "maisaka_llm_role"
TOOL_CALL_ID_KEY = "maisaka_tool_call_id"
TOOL_CALLS_KEY = "maisaka_tool_calls"
SPEAKER_PREFIX_PATTERN = re.compile(r"^\[(?P<speaker>[^\]]+)\](?P<content>.*)$", re.DOTALL)
def _build_user_info_for_role(role: str) -> UserInfo:
if role == RoleType.User.value:
return UserInfo(user_id="maisaka_user", user_nickname=USER_NAME, user_cardname=None)
if role == RoleType.Tool.value:
return UserInfo(user_id="maisaka_tool", user_nickname="tool", user_cardname=None)
return UserInfo(
user_id="maisaka_assistant",
user_nickname=global_config.bot.nickname.strip() or "MaiSaka",
user_cardname=None,
)
def _serialize_tool_call(tool_call: ToolCall) -> dict:
return {
"call_id": tool_call.call_id,
"func_name": tool_call.func_name,
"args": tool_call.args or {},
}
def _deserialize_tool_call(data: dict) -> ToolCall:
return ToolCall(
call_id=str(data.get("call_id", "")),
func_name=str(data.get("func_name", "")),
args=data.get("args", {}) or {},
)
def build_message(
role: str,
content: str,
*,
message_kind: str = "normal",
source: Optional[str] = None,
tool_call_id: Optional[str] = None,
tool_calls: Optional[list[ToolCall]] = None,
timestamp: Optional[datetime] = None,
message_id: Optional[str] = None,
) -> MaiMessage:
"""Build a MaiMessage for the Maisaka session history."""
resolved_timestamp = timestamp or datetime.now()
resolved_role = role.value if isinstance(role, RoleType) else role
message = MaiMessage(
message_id=message_id or f"maisaka_{uuid4().hex}",
timestamp=resolved_timestamp,
platform=MAISAKA_PLATFORM,
)
message.message_info = MessageInfo(
user_info=_build_user_info_for_role(resolved_role),
group_info=None,
additional_config={
LLM_ROLE_KEY: resolved_role,
MESSAGE_KIND_KEY: message_kind,
SOURCE_KEY: source or resolved_role,
TOOL_CALL_ID_KEY: tool_call_id,
TOOL_CALLS_KEY: [_serialize_tool_call(tool_call) for tool_call in (tool_calls or [])],
},
)
message.session_id = MAISAKA_SESSION_ID
message.raw_message = MessageSequence([])
if content:
message.raw_message.text(content)
message.processed_plain_text = content
message.display_message = content
return message
def format_speaker_content(speaker_name: str, content: str) -> str:
"""Format visible conversation content with an explicit speaker label."""
return f"[{speaker_name}]{content}"
def parse_speaker_content(content: str) -> tuple[Optional[str], str]:
"""Parse content formatted as [speaker]message."""
match = SPEAKER_PREFIX_PATTERN.match(content or "")
if not match:
return None, content or ""
return match.group("speaker"), match.group("content")
def get_message_text(message: MaiMessage) -> str:
if message.processed_plain_text is not None:
return message.processed_plain_text
if message.display_message is not None:
return message.display_message
parts: list[str] = []
for component in message.raw_message.components:
text = getattr(component, "text", None)
if isinstance(text, str):
parts.append(text)
return "".join(parts)
def get_message_role(message: MaiMessage) -> str:
return str(message.message_info.additional_config.get(LLM_ROLE_KEY, RoleType.User.value))
def get_message_kind(message: MaiMessage) -> str:
return str(message.message_info.additional_config.get(MESSAGE_KIND_KEY, "normal"))
def get_message_source(message: MaiMessage) -> str:
return str(message.message_info.additional_config.get(SOURCE_KEY, get_message_role(message)))
def is_perception_message(message: MaiMessage) -> bool:
return get_message_kind(message) == "perception"
def get_tool_call_id(message: MaiMessage) -> Optional[str]:
value = message.message_info.additional_config.get(TOOL_CALL_ID_KEY)
return str(value) if value else None
def get_tool_calls(message: MaiMessage) -> list[ToolCall]:
raw_tool_calls = message.message_info.additional_config.get(TOOL_CALLS_KEY, [])
if not isinstance(raw_tool_calls, list):
return []
return [_deserialize_tool_call(item) for item in raw_tool_calls if isinstance(item, dict)]
def remove_last_perception(messages: list[MaiMessage]) -> None:
for index in range(len(messages) - 1, -1, -1):
if is_perception_message(messages[index]):
messages.pop(index)
break
def to_llm_message(message: MaiMessage) -> Optional[Message]:
role = get_message_role(message)
content = get_message_text(message)
tool_call_id = get_tool_call_id(message)
tool_calls = get_tool_calls(message)
if role == RoleType.System.value:
role_type = RoleType.System
elif role == RoleType.User.value:
role_type = RoleType.User
elif role == RoleType.Assistant.value:
role_type = RoleType.Assistant
elif role == RoleType.Tool.value:
role_type = RoleType.Tool
else:
return None
builder = MessageBuilder().set_role(role_type)
if role_type == RoleType.Assistant and tool_calls:
builder.set_tool_calls(tool_calls)
if role_type == RoleType.Tool and tool_call_id:
builder.add_tool_call(tool_call_id)
if content:
builder.add_text_content(content)
return builder.build()

View File

@@ -2,14 +2,14 @@
MaiSaka reply helper.
"""
from datetime import datetime
from typing import Any, Optional
from typing import Optional
from src.common.data_models.mai_message_data_model import MaiMessage
from src.config.config import global_config
from .config import USER_NAME
from .llm_service import MaiSakaLLMService
VISIBLE_REPLY_PREFIX = "\u3010\u9ea6\u9ea6\u7684\u53d1\u8a00\u3011"
from .message_adapter import get_message_role, get_message_text, is_perception_message, parse_speaker_content
def _normalize_content(content: str, limit: int = 500) -> str:
@@ -19,57 +19,49 @@ def _normalize_content(content: str, limit: int = 500) -> str:
return normalized
def _format_message_time(_: dict[str, Any]) -> str:
return datetime.now().strftime("%H:%M:%S")
def _format_message_time(message: MaiMessage) -> str:
return message.timestamp.strftime("%H:%M:%S")
def _extract_visible_assistant_reply(message: dict[str, Any]) -> str:
if message.get("_type") == "perception":
def _extract_visible_assistant_reply(message: MaiMessage) -> str:
if is_perception_message(message):
return ""
content = (message.get("content", "") or "").strip()
if not content:
return ""
marker = "[generated_reply]"
if marker in content:
_, visible_reply = content.rsplit(marker, 1)
return _normalize_content(visible_reply)
return ""
def _extract_guided_bot_reply(message: dict[str, Any]) -> str:
content = (message.get("content", "") or "").strip()
if content.startswith(VISIBLE_REPLY_PREFIX):
return _normalize_content(content[len(VISIBLE_REPLY_PREFIX) :].strip())
def _extract_guided_bot_reply(message: MaiMessage) -> str:
speaker_name, body = parse_speaker_content(get_message_text(message).strip())
bot_nickname = global_config.bot.nickname.strip() or "Bot"
if speaker_name == bot_nickname:
return _normalize_content(body.strip())
return ""
def format_chat_history(messages: list[dict[str, Any]]) -> str:
def format_chat_history(messages: list[MaiMessage]) -> str:
"""Format visible chat history for reply generation."""
bot_nickname = global_config.bot.nickname.strip() or "Bot"
parts: list[str] = []
for message in messages:
role = message.get("role", "")
role = get_message_role(message)
timestamp = _format_message_time(message)
if role == "user":
guided_reply = _extract_guided_bot_reply(message)
if guided_reply:
parts.append(f"{timestamp} {bot_nickname}(分析器指导的麦麦发言):{guided_reply}")
parts.append(f"{timestamp} {bot_nickname}(you): {guided_reply}")
continue
content = _normalize_content(message.get("content", "") or "")
_, content_body = parse_speaker_content(get_message_text(message))
content = _normalize_content(content_body)
if content:
parts.append(f"{timestamp} 用户:{content}")
parts.append(f"{timestamp} {USER_NAME}: {content}")
continue
if role == "assistant":
visible_reply = _extract_visible_assistant_reply(message)
if visible_reply:
parts.append(f"{timestamp} {bot_nickname}(你):{visible_reply}")
parts.append(f"{timestamp} {bot_nickname}(you): {visible_reply}")
return "\n".join(parts)
@@ -87,7 +79,7 @@ class Replyer:
def set_enabled(self, enabled: bool) -> None:
self._enabled = enabled
async def reply(self, reason: str, chat_history: list[dict[str, Any]]) -> str:
async def reply(self, reason: str, chat_history: list[MaiMessage]) -> str:
if not self._enabled or not reason or self._llm_service is None:
return "..."

View File

@@ -1,75 +1,67 @@
"""
MaiSaka - Timing 模块(含自我反思功能)
构建对话时间戳信息,供 Timing 分析模块使用。
该模块同时负责分析对话的时间维度和进行自我反思分析。
MaiSaka timing helpers.
"""
from datetime import datetime
from typing import Optional
def _format_duration(total_seconds: int) -> str:
hours, remainder = divmod(total_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if hours > 0:
return f"{hours}h {minutes}m {seconds}s"
if minutes > 0:
return f"{minutes}m {seconds}s"
return f"{seconds}s"
def _get_time_period_label(hour: int) -> str:
if 0 <= hour < 6:
return "late_night"
if 6 <= hour < 9:
return "morning"
if 9 <= hour < 12:
return "late_morning"
if 12 <= hour < 14:
return "noon"
if 14 <= hour < 18:
return "afternoon"
if 18 <= hour < 22:
return "evening"
return "night"
def build_timing_info(
chat_start_time: Optional[datetime],
last_user_input_time: Optional[datetime],
last_assistant_response_time: Optional[datetime],
user_input_times: list[datetime],
) -> str:
"""
构建当前时间戳信息文本,供 Timing 模块分析。
Args:
chat_start_time: 对话开始时间
last_user_input_time: 用户上次输入时间
last_assistant_response_time: 助手上次回复时间
user_input_times: 所有用户输入时间戳列表
"""
"""Build readable timing context for the timing analysis prompt."""
now = datetime.now()
parts: list[str] = []
parts.append(f"当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')}")
parts: list[str] = [f"Current time: {now.strftime('%Y-%m-%d %H:%M:%S')}"]
if chat_start_time:
elapsed = now - chat_start_time
minutes, seconds = divmod(int(elapsed.total_seconds()), 60)
hours, minutes = divmod(minutes, 60)
if hours > 0:
parts.append(f"对话已持续: {hours}小时{minutes}{seconds}")
elif minutes > 0:
parts.append(f"对话已持续: {minutes}{seconds}")
else:
parts.append(f"对话已持续: {seconds}")
elapsed_seconds = int((now - chat_start_time).total_seconds())
parts.append(f"Conversation duration: {_format_duration(elapsed_seconds)}")
if last_user_input_time:
since_user = now - last_user_input_time
parts.append(f"距用户上次输入: {int(since_user.total_seconds())}")
since_user_seconds = int((now - last_user_input_time).total_seconds())
parts.append(f"Seconds since last user input: {since_user_seconds}")
if last_assistant_response_time:
since_assistant = now - last_assistant_response_time
parts.append(f"距助手上次回复: {int(since_assistant.total_seconds())}")
since_assistant_seconds = int((now - last_assistant_response_time).total_seconds())
parts.append(f"Seconds since last Maisaka reply: {since_assistant_seconds}")
if len(user_input_times) >= 2:
intervals = [
(user_input_times[i] - user_input_times[i - 1]).total_seconds() for i in range(1, len(user_input_times))
int((user_input_times[index] - user_input_times[index - 1]).total_seconds())
for index in range(1, len(user_input_times))
]
avg_interval = sum(intervals) / len(intervals)
parts.append(f"用户平均回复间隔: {int(avg_interval)}")
parts.append(f"用户总共发言次数: {len(user_input_times)}")
# 时段判断
hour = now.hour
if 0 <= hour < 6:
parts.append("当前时段: 深夜/凌晨")
elif 6 <= hour < 9:
parts.append("当前时段: 早晨")
elif 9 <= hour < 12:
parts.append("当前时段: 上午")
elif 12 <= hour < 14:
parts.append("当前时段: 中午")
elif 14 <= hour < 18:
parts.append("当前时段: 下午")
elif 18 <= hour < 22:
parts.append("当前时段: 晚上")
else:
parts.append("当前时段: 深夜")
average_interval = sum(intervals) / len(intervals)
parts.append(f"Average user input interval: {int(average_interval)}s")
parts.append(f"Total user input count: {len(user_input_times)}")
parts.append(f"Current time period: {_get_time_period_label(now.hour)}")
return "\n".join(parts)

View File

@@ -9,13 +9,15 @@ from typing import TYPE_CHECKING, Any, Optional
import json as _json
import os
from rich.markdown import Markdown
from rich.panel import Panel
from src.common.data_models.mai_message_data_model import MaiMessage
from src.llm_models.payload_content.tool_option import ToolCall
from .config import console
from .input_reader import InputReader
from .llm_service import MaiSakaLLMService
from .replyer import Replyer
from .message_adapter import build_message
if TYPE_CHECKING:
from .mcp_client import MCPManager
@@ -23,18 +25,6 @@ if TYPE_CHECKING:
MAI_FILES_DIR = Path(os.path.join(os.path.dirname(os.path.abspath(__file__)), "mai_files"))
_replyer: Optional[Replyer] = None
def get_replyer(llm_service: MaiSakaLLMService) -> Replyer:
"""Return a shared replyer instance."""
global _replyer
if _replyer is None:
_replyer = Replyer(llm_service)
elif _replyer._llm_service is None:
_replyer.set_llm_service(llm_service)
return _replyer
class ToolHandlerContext:
"""Shared context for tool handlers."""
@@ -51,78 +41,22 @@ class ToolHandlerContext:
self.last_user_input_time: Optional[datetime] = None
async def handle_send_message(tc: Any, chat_history: list[dict[str, Any]], ctx: ToolHandlerContext) -> None:
"""Backward-compatible handler for legacy send-message style tools."""
reason = tc.arguments.get("reason", "")
console.print("[accent]Calling tool: send_message(...)[/accent]")
if not reason:
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": "Missing required argument: reason",
}
)
return
console.print(
Panel(
Markdown(reason),
title="Reply Reason",
border_style="dim",
padding=(0, 1),
style="dim",
)
)
with console.status("[info]Generating visible reply...[/info]", spinner="dots"):
replyer = get_replyer(ctx.llm_service)
reply = await replyer.reply(reason, chat_history)
console.print(
Panel(
Markdown(reply),
title="MaiSaka",
border_style="magenta",
padding=(1, 2),
)
)
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": f"Visible reply generated:\n{reply}",
}
)
async def handle_stop(tc: Any, chat_history: list[dict[str, Any]]) -> None:
async def handle_stop(tc: ToolCall, chat_history: list[MaiMessage]) -> None:
"""Handle the stop tool."""
console.print("[accent]Calling tool: stop()[/accent]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": "Conversation loop will stop after this round.",
}
build_message(role="tool", content="Conversation loop will stop after this round.", tool_call_id=tc.call_id)
)
async def handle_wait(tc: Any, chat_history: list[dict[str, Any]], ctx: ToolHandlerContext) -> str:
async def handle_wait(tc: ToolCall, chat_history: list[MaiMessage], ctx: ToolHandlerContext) -> str:
"""Handle the wait tool."""
seconds = tc.arguments.get("seconds", 30)
seconds = (tc.args or {}).get("seconds", 30)
seconds = max(5, min(seconds, 300))
console.print(f"[accent]Calling tool: wait({seconds})[/accent]")
tool_result = await _do_wait(seconds, ctx)
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": tool_result,
}
)
chat_history.append(build_message(role="tool", content=tool_result, tool_call_id=tc.call_id))
return tool_result
@@ -152,49 +86,37 @@ async def _do_wait(seconds: int, ctx: ToolHandlerContext) -> str:
return f"User input received: {user_input}"
async def handle_mcp_tool(tc: Any, chat_history: list[dict[str, Any]], mcp_manager: "MCPManager") -> None:
async def handle_mcp_tool(tc: ToolCall, chat_history: list[MaiMessage], mcp_manager: "MCPManager") -> None:
"""Handle an MCP tool call."""
args_str = _json.dumps(tc.arguments, ensure_ascii=False)
args_str = _json.dumps(tc.args or {}, ensure_ascii=False)
args_preview = args_str if len(args_str) <= 120 else args_str[:120] + "..."
console.print(f"[accent]Calling MCP tool: {tc.name}({args_preview})[/accent]")
console.print(f"[accent]Calling MCP tool: {tc.func_name}({args_preview})[/accent]")
with console.status(f"[info]Running MCP tool {tc.name}...[/info]", spinner="dots"):
result = await mcp_manager.call_tool(tc.name, tc.arguments)
with console.status(f"[info]Running MCP tool {tc.func_name}...[/info]", spinner="dots"):
result = await mcp_manager.call_tool(tc.func_name, tc.args or {})
display_text = result if len(result) <= 800 else result[:800] + "\n... (truncated)"
console.print(
Panel(
display_text,
title=f"MCP: {tc.name}",
title=f"MCP: {tc.func_name}",
border_style="bright_green",
padding=(0, 1),
)
)
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": result,
}
)
chat_history.append(build_message(role="tool", content=result, tool_call_id=tc.call_id))
async def handle_unknown_tool(tc: Any, chat_history: list[dict[str, Any]]) -> None:
async def handle_unknown_tool(tc: ToolCall, chat_history: list[MaiMessage]) -> None:
"""Handle an unknown tool call."""
console.print(f"[accent]Calling unknown tool: {tc.name}({tc.arguments})[/accent]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": f"Unknown tool: {tc.name}",
}
)
console.print(f"[accent]Calling unknown tool: {tc.func_name}({tc.args})[/accent]")
chat_history.append(build_message(role="tool", content=f"Unknown tool: {tc.func_name}", tool_call_id=tc.call_id))
async def handle_write_file(tc: Any, chat_history: list[dict[str, Any]]) -> None:
async def handle_write_file(tc: ToolCall, chat_history: list[MaiMessage]) -> None:
"""Write a file under the local mai_files workspace."""
filename = tc.arguments.get("filename", "")
content = tc.arguments.get("content", "")
filename = (tc.args or {}).get("filename", "")
content = (tc.args or {}).get("content", "")
console.print(f'[accent]Calling tool: write_file("{filename}")[/accent]')
MAI_FILES_DIR.mkdir(parents=True, exist_ok=True)
@@ -215,27 +137,21 @@ async def handle_write_file(tc: Any, chat_history: list[dict[str, Any]]) -> None
)
)
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": f"File written successfully: {filename} ({file_size} bytes)",
}
build_message(
role="tool",
content=f"File written successfully: {filename} ({file_size} bytes)",
tool_call_id=tc.call_id,
)
)
except Exception as exc:
error_msg = f"Failed to write file: {exc}"
console.print(f"[error]{error_msg}[/error]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": error_msg,
}
)
chat_history.append(build_message(role="tool", content=error_msg, tool_call_id=tc.call_id))
async def handle_read_file(tc: Any, chat_history: list[dict[str, Any]]) -> None:
async def handle_read_file(tc: ToolCall, chat_history: list[MaiMessage]) -> None:
"""Read a file from the local mai_files workspace."""
filename = tc.arguments.get("filename", "")
filename = (tc.args or {}).get("filename", "")
console.print(f'[accent]Calling tool: read_file("{filename}")[/accent]')
file_path = MAI_FILES_DIR / filename
@@ -244,25 +160,13 @@ async def handle_read_file(tc: Any, chat_history: list[dict[str, Any]]) -> None:
if not file_path.exists():
error_msg = f"File does not exist: {filename}"
console.print(f"[warning]{error_msg}[/warning]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": error_msg,
}
)
chat_history.append(build_message(role="tool", content=error_msg, tool_call_id=tc.call_id))
return
if not file_path.is_file():
error_msg = f"Path is not a file: {filename}"
console.print(f"[warning]{error_msg}[/warning]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": error_msg,
}
)
chat_history.append(build_message(role="tool", content=error_msg, tool_call_id=tc.call_id))
return
with open(file_path, "r", encoding="utf-8") as file:
@@ -278,25 +182,15 @@ async def handle_read_file(tc: Any, chat_history: list[dict[str, Any]]) -> None:
)
)
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": f"File content of {filename}:\n{file_content}",
}
build_message(role="tool", content=f"File content of {filename}:\n{file_content}", tool_call_id=tc.call_id)
)
except Exception as exc:
error_msg = f"Failed to read file: {exc}"
console.print(f"[error]{error_msg}[/error]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": error_msg,
}
)
chat_history.append(build_message(role="tool", content=error_msg, tool_call_id=tc.call_id))
async def handle_list_files(tc: Any, chat_history: list[dict[str, Any]]) -> None:
async def handle_list_files(tc: ToolCall, chat_history: list[MaiMessage]) -> None:
"""List files under the local mai_files workspace."""
console.print("[accent]Calling tool: list_files()[/accent]")
@@ -332,23 +226,11 @@ async def handle_list_files(tc: Any, chat_history: list[dict[str, Any]]) -> None
padding=(0, 1),
)
)
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": result_text,
}
)
chat_history.append(build_message(role="tool", content=result_text, tool_call_id=tc.call_id))
except Exception as exc:
error_msg = f"Failed to list files: {exc}"
console.print(f"[error]{error_msg}[/error]")
chat_history.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": error_msg,
}
)
chat_history.append(build_message(role="tool", content=error_msg, tool_call_id=tc.call_id))
try: