ref:分离know模块和cli模块
This commit is contained in:
@@ -15,18 +15,18 @@ from rich.pretty import Pretty
|
||||
from rich.text import Text
|
||||
|
||||
from src.chat.message_receive.message import SessionMessage
|
||||
from src.cli.console import console
|
||||
from src.common.data_models.llm_service_data_models import LLMGenerationOptions
|
||||
from src.common.logger import get_logger
|
||||
from src.common.prompt_i18n import load_prompt
|
||||
from src.config.config import global_config
|
||||
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.tool_option import ToolCall, ToolDefinitionInput, ToolOption, normalize_tool_options
|
||||
from src.services.llm_service import LLMServiceClient
|
||||
|
||||
from .builtin_tools import get_builtin_tools
|
||||
from .console import console
|
||||
from .knowledge import extract_category_ids_from_result
|
||||
from .message_adapter import (
|
||||
build_message,
|
||||
format_speaker_content,
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
"""
|
||||
MaiSaka CLI and conversation loop.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
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.chat.message_receive.message import SessionMessage
|
||||
from src.chat.replyer.maisaka_generator import MaisakaReplyGenerator
|
||||
from src.config.config import config_manager, global_config
|
||||
from src.mcp_module import MCPManager
|
||||
|
||||
from .chat_loop_service import MaisakaChatLoopService
|
||||
from .console import console
|
||||
from .input_reader import InputReader
|
||||
from .knowledge import retrieve_relevant_knowledge
|
||||
from .knowledge_store import get_knowledge_store
|
||||
from .message_adapter import build_message, format_speaker_content, remove_last_perception
|
||||
from .tool_handlers import (
|
||||
ToolHandlerContext,
|
||||
handle_mcp_tool,
|
||||
handle_stop,
|
||||
handle_unknown_tool,
|
||||
handle_wait,
|
||||
)
|
||||
|
||||
|
||||
class BufferCLI:
|
||||
"""Maisaka 命令行交互入口。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._chat_loop_service: Optional[MaisakaChatLoopService] = None
|
||||
self._reply_generator = MaisakaReplyGenerator()
|
||||
self._reader = InputReader()
|
||||
self._chat_history: Optional[list[SessionMessage]] = 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 store: {knowledge_stats['total_items']} item(s)[/success]")
|
||||
else:
|
||||
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] = []
|
||||
self._mcp_manager: Optional[MCPManager] = None
|
||||
self._init_llm()
|
||||
|
||||
def _init_llm(self) -> None:
|
||||
"""初始化 Maisaka 使用的聊天服务。"""
|
||||
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
|
||||
|
||||
_ = enable_thinking
|
||||
self._chat_loop_service = MaisakaChatLoopService()
|
||||
|
||||
model_name = self._get_current_model_name()
|
||||
console.print(f"[success][OK] LLM service initialized[/success] [muted](model: {model_name})[/muted]")
|
||||
|
||||
@staticmethod
|
||||
def _get_current_model_name() -> str:
|
||||
"""读取当前 planner 模型名。"""
|
||||
try:
|
||||
model_task_config = config_manager.get_model_config().model_task_config
|
||||
if model_task_config.planner.model_list:
|
||||
return model_task_config.planner.model_list[0]
|
||||
except Exception:
|
||||
pass
|
||||
return "unconfigured"
|
||||
|
||||
def _build_tool_context(self) -> ToolHandlerContext:
|
||||
"""构建工具处理的共享上下文。"""
|
||||
tool_context = ToolHandlerContext(
|
||||
reader=self._reader,
|
||||
user_input_times=self._user_input_times,
|
||||
)
|
||||
tool_context.last_user_input_time = self._last_user_input_time
|
||||
return tool_context
|
||||
|
||||
def _show_banner(self) -> None:
|
||||
"""渲染启动横幅。"""
|
||||
banner = Text()
|
||||
banner.append("MaiSaka", style="bold cyan")
|
||||
banner.append(" v2.0\n", 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()
|
||||
|
||||
async def _start_chat(self, user_text: str) -> None:
|
||||
"""追加用户输入并继续内部循环。"""
|
||||
if self._chat_loop_service is None:
|
||||
console.print("[warning]LLM service is not initialized; skipping chat.[/warning]")
|
||||
return
|
||||
|
||||
now = datetime.now()
|
||||
self._last_user_input_time = now
|
||||
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._chat_loop_service.build_chat_context(user_text)
|
||||
else:
|
||||
self._chat_history.append(
|
||||
build_message(
|
||||
role="user",
|
||||
content=format_speaker_content(
|
||||
global_config.maisaka.user_name.strip() or "User",
|
||||
user_text,
|
||||
now,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
await self._run_llm_loop(self._chat_history)
|
||||
|
||||
async def _run_llm_loop(self, chat_history: list[SessionMessage]) -> None:
|
||||
"""
|
||||
Main inner loop for the Maisaka planner.
|
||||
|
||||
Each round may produce internal thoughts and optionally call tools:
|
||||
- reply(msg_id): 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
|
||||
"""
|
||||
if self._chat_loop_service is None:
|
||||
return
|
||||
|
||||
consecutive_errors = 0
|
||||
last_had_tool_calls = True
|
||||
|
||||
while True:
|
||||
if last_had_tool_calls:
|
||||
tasks = []
|
||||
status_text_parts = []
|
||||
|
||||
if global_config.maisaka.enable_knowledge_module:
|
||||
tasks.append(("knowledge", retrieve_relevant_knowledge(self._chat_loop_service, chat_history)))
|
||||
status_text_parts.append("knowledge")
|
||||
|
||||
with console.status(
|
||||
f"[info]{' + '.join(status_text_parts)} analyzing...[/info]",
|
||||
spinner="dots",
|
||||
):
|
||||
results = await asyncio.gather(*[task for _, task in tasks], return_exceptions=True)
|
||||
|
||||
knowledge_analysis = ""
|
||||
if global_config.maisaka.enable_knowledge_module:
|
||||
knowledge_result = results[0] if results else None
|
||||
if isinstance(knowledge_result, Exception):
|
||||
console.print(f"[warning]Knowledge analysis failed: {knowledge_result}[/warning]")
|
||||
elif knowledge_result:
|
||||
knowledge_analysis = knowledge_result
|
||||
if global_config.maisaka.show_thinking:
|
||||
console.print(
|
||||
Panel(
|
||||
Markdown(knowledge_analysis),
|
||||
title="Knowledge",
|
||||
border_style="bright_magenta",
|
||||
padding=(0, 1),
|
||||
style="dim",
|
||||
)
|
||||
)
|
||||
|
||||
remove_last_perception(chat_history)
|
||||
|
||||
perception_parts = []
|
||||
if knowledge_analysis:
|
||||
perception_parts.append(f"Knowledge\n{knowledge_analysis}")
|
||||
|
||||
if perception_parts:
|
||||
chat_history.append(
|
||||
build_message(
|
||||
role="assistant",
|
||||
content="\n\n".join(perception_parts),
|
||||
message_kind="perception",
|
||||
source="assistant",
|
||||
)
|
||||
)
|
||||
elif global_config.maisaka.show_thinking:
|
||||
console.print("[muted]Skipping module analysis because the last round used no tools.[/muted]")
|
||||
|
||||
with console.status("[info]AI is thinking...[/info]", spinner="dots"):
|
||||
try:
|
||||
response = await self._chat_loop_service.chat_loop_step(chat_history)
|
||||
consecutive_errors = 0
|
||||
except Exception as exc:
|
||||
consecutive_errors += 1
|
||||
console.print(f"[error]LLM call failed: {exc}[/error]")
|
||||
if consecutive_errors >= 3:
|
||||
console.print("[error]Too many consecutive errors. Exiting chat.[/error]\n")
|
||||
break
|
||||
continue
|
||||
|
||||
chat_history.append(response.raw_message)
|
||||
self._last_assistant_response_time = datetime.now()
|
||||
|
||||
if global_config.maisaka.show_thinking and response.content:
|
||||
console.print(
|
||||
Panel(
|
||||
Markdown(response.content),
|
||||
title="Thought",
|
||||
border_style="dim",
|
||||
padding=(1, 2),
|
||||
style="dim",
|
||||
)
|
||||
)
|
||||
|
||||
if response.content and not response.tool_calls:
|
||||
last_had_tool_calls = False
|
||||
continue
|
||||
|
||||
if not response.tool_calls:
|
||||
last_had_tool_calls = False
|
||||
continue
|
||||
|
||||
should_stop = False
|
||||
tool_context = self._build_tool_context()
|
||||
|
||||
for tool_call in response.tool_calls:
|
||||
if tool_call.func_name == "stop":
|
||||
await handle_stop(tool_call, chat_history)
|
||||
should_stop = True
|
||||
|
||||
elif tool_call.func_name == "reply":
|
||||
reply = await self._generate_visible_reply(chat_history, response.content)
|
||||
chat_history.append(
|
||||
build_message(
|
||||
role="tool",
|
||||
content="Visible reply generated and recorded.",
|
||||
source="tool",
|
||||
tool_call_id=tool_call.call_id,
|
||||
)
|
||||
)
|
||||
chat_history.append(
|
||||
build_message(
|
||||
role="user",
|
||||
content=format_speaker_content(
|
||||
global_config.bot.nickname.strip() or "MaiSaka",
|
||||
reply,
|
||||
datetime.now(),
|
||||
),
|
||||
source="guided_reply",
|
||||
)
|
||||
)
|
||||
|
||||
elif tool_call.func_name == "no_reply":
|
||||
if global_config.maisaka.show_thinking:
|
||||
console.print("[muted]No visible reply this round.[/muted]")
|
||||
chat_history.append(
|
||||
build_message(
|
||||
role="tool",
|
||||
content="No visible reply was sent for this round.",
|
||||
source="tool",
|
||||
tool_call_id=tool_call.call_id,
|
||||
)
|
||||
)
|
||||
|
||||
elif tool_call.func_name == "wait":
|
||||
tool_result = await handle_wait(tool_call, chat_history, tool_context)
|
||||
if tool_context.last_user_input_time != self._last_user_input_time:
|
||||
self._last_user_input_time = tool_context.last_user_input_time
|
||||
if tool_result.startswith("[[QUIT]]"):
|
||||
should_stop = True
|
||||
|
||||
elif self._mcp_manager and self._mcp_manager.is_mcp_tool(tool_call.func_name):
|
||||
await handle_mcp_tool(tool_call, chat_history, self._mcp_manager)
|
||||
|
||||
else:
|
||||
await handle_unknown_tool(tool_call, chat_history)
|
||||
|
||||
if should_stop:
|
||||
console.print("[muted]Conversation paused. Waiting for new input...[/muted]\n")
|
||||
break
|
||||
|
||||
last_had_tool_calls = True
|
||||
|
||||
async def _init_mcp(self) -> None:
|
||||
"""初始化 MCP 服务并注册暴露的工具。"""
|
||||
config_path = Path(__file__).resolve().parents[2] / "config" / "mcp_config.json"
|
||||
self._mcp_manager = await MCPManager.from_config(str(config_path))
|
||||
|
||||
if self._mcp_manager and self._chat_loop_service:
|
||||
mcp_tools = self._mcp_manager.get_openai_tools()
|
||||
if mcp_tools:
|
||||
self._chat_loop_service.set_extra_tools(mcp_tools)
|
||||
summary = self._mcp_manager.get_tool_summary()
|
||||
console.print(
|
||||
Panel(
|
||||
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[SessionMessage], latest_thought: str) -> str:
|
||||
"""根据最新思考生成并输出可见回复。"""
|
||||
if not latest_thought:
|
||||
return ""
|
||||
|
||||
with console.status("[info]Generating visible reply...[/info]", spinner="dots"):
|
||||
success, result = await self._reply_generator.generate_reply_with_context(
|
||||
reply_reason=latest_thought,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
if success and result.text_fragments:
|
||||
reply = result.text_fragments[0]
|
||||
else:
|
||||
reply = "..."
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
Markdown(reply),
|
||||
title="MaiSaka",
|
||||
border_style="magenta",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
|
||||
return reply
|
||||
|
||||
async def run(self) -> None:
|
||||
"""主交互循环。"""
|
||||
if global_config.maisaka.enable_mcp:
|
||||
await self._init_mcp()
|
||||
else:
|
||||
console.print("[muted]MCP is disabled (ENABLE_MCP=false)[/muted]")
|
||||
|
||||
self._reader.start(asyncio.get_event_loop())
|
||||
self._show_banner()
|
||||
|
||||
try:
|
||||
while True:
|
||||
console.print("[bold cyan]> [/bold cyan]", end="")
|
||||
raw_input = await self._reader.get_line()
|
||||
|
||||
if raw_input is None:
|
||||
console.print("\n[muted]Goodbye![/muted]")
|
||||
break
|
||||
|
||||
raw_input = raw_input.strip()
|
||||
if not raw_input:
|
||||
continue
|
||||
|
||||
await self._start_chat(raw_input)
|
||||
finally:
|
||||
if self._mcp_manager:
|
||||
await self._mcp_manager.close()
|
||||
@@ -1,17 +0,0 @@
|
||||
"""MaiSaka 终端输出组件。"""
|
||||
|
||||
from rich.console import Console
|
||||
from rich.theme import Theme
|
||||
|
||||
custom_theme = Theme(
|
||||
{
|
||||
"info": "cyan",
|
||||
"success": "green",
|
||||
"warning": "yellow",
|
||||
"error": "bold red",
|
||||
"muted": "dim",
|
||||
"accent": "bold magenta",
|
||||
}
|
||||
)
|
||||
|
||||
console = Console(theme=custom_theme)
|
||||
@@ -1,56 +0,0 @@
|
||||
"""
|
||||
MaiSaka - 异步输入读取器
|
||||
将阻塞的标准输入读取放到后台线程中,供 asyncio 循环安全消费。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class InputReader:
|
||||
"""后台读取标准输入,并通过 asyncio.Queue 向主循环投递结果。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._queue: asyncio.Queue[Optional[str]] = asyncio.Queue()
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
def start(self, loop: asyncio.AbstractEventLoop) -> None:
|
||||
"""启动后台输入线程。重复调用时忽略。"""
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
|
||||
self._loop = loop
|
||||
self._stop_event.clear()
|
||||
self._thread = threading.Thread(target=self._read_loop, name="maisaka-input-reader", daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _read_loop(self) -> None:
|
||||
"""在后台线程中阻塞读取 stdin。"""
|
||||
while not self._stop_event.is_set():
|
||||
line = sys.stdin.readline()
|
||||
if self._loop is None:
|
||||
return
|
||||
|
||||
if line == "":
|
||||
self._loop.call_soon_threadsafe(self._queue.put_nowait, None)
|
||||
return
|
||||
|
||||
self._loop.call_soon_threadsafe(self._queue.put_nowait, line.rstrip("\r\n"))
|
||||
|
||||
async def get_line(self, timeout: Optional[int] = None) -> Optional[str]:
|
||||
"""异步获取一行输入;设置 timeout 时支持超时返回。"""
|
||||
if timeout is None:
|
||||
return await self._queue.get()
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(self._queue.get(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
return None
|
||||
|
||||
def close(self) -> None:
|
||||
"""请求后台线程停止。"""
|
||||
self._stop_event.set()
|
||||
@@ -1,58 +0,0 @@
|
||||
"""
|
||||
MaiSaka knowledge retrieval helpers.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from src.chat.message_receive.message import SessionMessage
|
||||
|
||||
from .knowledge_store import KNOWLEDGE_CATEGORIES, get_knowledge_store
|
||||
|
||||
NO_RESULT_KEYWORDS = [
|
||||
"\u65e0",
|
||||
"\u6ca1\u6709",
|
||||
"\u4e0d\u9002\u7528",
|
||||
"\u65e0\u9700",
|
||||
"\u65e0\u76f8\u5173",
|
||||
]
|
||||
|
||||
|
||||
def extract_category_ids_from_result(result: str) -> List[str]:
|
||||
"""Extract valid category ids from an LLM result string."""
|
||||
if not result:
|
||||
return []
|
||||
|
||||
normalized = result.strip()
|
||||
if not normalized:
|
||||
return []
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def retrieve_relevant_knowledge(
|
||||
knowledge_analyzer,
|
||||
chat_history: List[SessionMessage],
|
||||
) -> str:
|
||||
"""Retrieve formatted knowledge snippets relevant to the current chat history."""
|
||||
store = get_knowledge_store()
|
||||
categories_summary = store.get_categories_summary()
|
||||
|
||||
try:
|
||||
category_ids = await knowledge_analyzer.analyze_knowledge_need(chat_history, categories_summary)
|
||||
if not category_ids:
|
||||
return ""
|
||||
return store.get_formatted_knowledge(category_ids)
|
||||
except Exception:
|
||||
return ""
|
||||
@@ -1,190 +0,0 @@
|
||||
"""
|
||||
MaiSaka - 了解列表持久化存储
|
||||
存储用户个人特征信息,支持层级结构和本地持久化。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
# 数据目录 - 项目根目录下的 mai_knowledge
|
||||
PROJECT_ROOT = Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
KNOWLEDGE_DATA_DIR = PROJECT_ROOT / "mai_knowledge"
|
||||
KNOWLEDGE_FILE = KNOWLEDGE_DATA_DIR / "knowledge.json"
|
||||
|
||||
|
||||
# 个人特征分类列表(预定义)
|
||||
KNOWLEDGE_CATEGORIES = {
|
||||
"1": "性别",
|
||||
"2": "性格",
|
||||
"3": "饮食口味",
|
||||
"4": "交友喜好",
|
||||
"5": "情绪/理性倾向",
|
||||
"6": "兴趣爱好",
|
||||
"7": "职业/专业",
|
||||
"8": "生活习惯",
|
||||
"9": "价值观",
|
||||
"10": "沟通风格",
|
||||
"11": "学习方式",
|
||||
"12": "压力应对方式",
|
||||
}
|
||||
|
||||
|
||||
class KnowledgeStore:
|
||||
"""
|
||||
了解列表存储。
|
||||
|
||||
特性:
|
||||
- 持久化到 JSON 文件
|
||||
- 层级结构存储(按分类)
|
||||
- 支持增量更新
|
||||
- 启动时自动加载
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化了解存储"""
|
||||
self._knowledge: Dict[str, List[Dict[str, Any]]] = {category_id: [] for category_id in KNOWLEDGE_CATEGORIES}
|
||||
self._ensure_data_dir()
|
||||
self._load()
|
||||
|
||||
def _ensure_data_dir(self):
|
||||
"""确保数据目录存在"""
|
||||
KNOWLEDGE_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _load(self):
|
||||
"""从文件加载了解数据"""
|
||||
if not KNOWLEDGE_FILE.exists():
|
||||
self._knowledge = {category_id: [] for category_id in KNOWLEDGE_CATEGORIES}
|
||||
return
|
||||
|
||||
try:
|
||||
with open(KNOWLEDGE_FILE, "r", encoding="utf-8") as f:
|
||||
loaded = json.load(f)
|
||||
# 确保所有分类都存在
|
||||
for category_id in KNOWLEDGE_CATEGORIES:
|
||||
if category_id not in loaded:
|
||||
loaded[category_id] = []
|
||||
self._knowledge = loaded
|
||||
except Exception as e:
|
||||
print(f"[warning]加载了解数据失败: {e}[/warning]")
|
||||
self._knowledge = {category_id: [] for category_id in KNOWLEDGE_CATEGORIES}
|
||||
|
||||
def _save(self):
|
||||
"""保存了解数据到文件"""
|
||||
try:
|
||||
with open(KNOWLEDGE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self._knowledge, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"[warning]保存了解数据失败: {e}[/warning]")
|
||||
|
||||
def add_knowledge(
|
||||
self,
|
||||
category_id: str,
|
||||
content: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
添加一条了解信息。
|
||||
|
||||
Args:
|
||||
category_id: 分类编号
|
||||
content: 了解内容
|
||||
metadata: 元数据
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
if category_id not in KNOWLEDGE_CATEGORIES:
|
||||
return False
|
||||
|
||||
try:
|
||||
knowledge_item = {
|
||||
"id": f"know_{category_id}_{datetime.now().timestamp()}",
|
||||
"content": content,
|
||||
"metadata": metadata or {},
|
||||
"created_at": datetime.now().isoformat(),
|
||||
}
|
||||
self._knowledge[category_id].append(knowledge_item)
|
||||
self._save()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_category_knowledge(self, category_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取某个分类的所有了解信息。
|
||||
|
||||
Args:
|
||||
category_id: 分类编号
|
||||
|
||||
Returns:
|
||||
该分类的所有了解信息
|
||||
"""
|
||||
return self._knowledge.get(category_id, [])
|
||||
|
||||
def get_all_knowledge(self) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""获取所有了解信息"""
|
||||
return self._knowledge
|
||||
|
||||
def get_category_name(self, category_id: str) -> str:
|
||||
"""获取分类名称"""
|
||||
return KNOWLEDGE_CATEGORIES.get(category_id, "未知分类")
|
||||
|
||||
def get_categories_summary(self) -> str:
|
||||
"""获取所有分类的摘要(用于 LLM 展示)"""
|
||||
lines = []
|
||||
for category_id, category_name in KNOWLEDGE_CATEGORIES.items():
|
||||
count = len(self._knowledge.get(category_id, []))
|
||||
if count > 0:
|
||||
lines.append(f"{category_id}. {category_name} ({count}条)")
|
||||
else:
|
||||
lines.append(f"{category_id}. {category_name} (无数据)")
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_formatted_knowledge(self, category_ids: List[str]) -> str:
|
||||
"""
|
||||
获取指定分类的了解内容,格式化为文本。
|
||||
|
||||
Args:
|
||||
category_ids: 分类编号列表
|
||||
|
||||
Returns:
|
||||
格式化后的了解内容文本
|
||||
"""
|
||||
parts = []
|
||||
for category_id in category_ids:
|
||||
category_name = self.get_category_name(category_id)
|
||||
items = self.get_category_knowledge(category_id)
|
||||
|
||||
if items:
|
||||
parts.append(f"【{category_name}】")
|
||||
for item in items:
|
||||
content = item.get("content", "")
|
||||
parts.append(f" - {content}")
|
||||
|
||||
return "\n".join(parts) if parts else "暂无相关了解信息"
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取了解数据统计信息"""
|
||||
total_items = sum(len(items) for items in self._knowledge.values())
|
||||
return {
|
||||
"total_categories": len(KNOWLEDGE_CATEGORIES),
|
||||
"total_items": total_items,
|
||||
"data_file": str(KNOWLEDGE_FILE),
|
||||
"data_exists": KNOWLEDGE_FILE.exists(),
|
||||
"data_size_kb": KNOWLEDGE_FILE.stat().st_size / 1024 if KNOWLEDGE_FILE.exists() else 0,
|
||||
}
|
||||
|
||||
|
||||
# 全局单例
|
||||
_knowledge_store_instance: Optional[KnowledgeStore] = None
|
||||
|
||||
|
||||
def get_knowledge_store() -> KnowledgeStore:
|
||||
"""获取了解存储实例(单例模式)"""
|
||||
global _knowledge_store_instance
|
||||
if _knowledge_store_instance is None:
|
||||
_knowledge_store_instance = KnowledgeStore()
|
||||
return _knowledge_store_instance
|
||||
@@ -13,9 +13,10 @@ from src.common.data_models.mai_message_data_model import GroupInfo, UserInfo
|
||||
from src.common.logger import get_logger
|
||||
from src.common.utils.utils_config import ExpressionConfigUtils
|
||||
from src.config.config import global_config
|
||||
from src.mcp_module import MCPManager
|
||||
from src.know_u.knowledge import KnowledgeLearner
|
||||
from src.learners.expression_learner import ExpressionLearner
|
||||
from src.learners.jargon_miner import JargonMiner
|
||||
from src.mcp_module import MCPManager
|
||||
|
||||
from .chat_loop_service import MaisakaChatLoopService
|
||||
from .reasoning_engine import MaisakaReasoningEngine
|
||||
@@ -66,9 +67,11 @@ class MaisakaHeartFlowChatting:
|
||||
self._enable_jargon_learning = jargon_learn
|
||||
self._min_messages_for_extraction = 10
|
||||
self._min_extraction_interval = 30
|
||||
self._last_extraction_time = 0.0
|
||||
self._last_expression_extraction_time = 0.0
|
||||
self._last_knowledge_extraction_time = 0.0
|
||||
self._expression_learner = ExpressionLearner(session_id)
|
||||
self._jargon_miner = JargonMiner(session_id, session_name=session_name)
|
||||
self._knowledge_learner = KnowledgeLearner(session_id)
|
||||
|
||||
self._reasoning_engine = MaisakaReasoningEngine(self)
|
||||
|
||||
@@ -157,7 +160,7 @@ class MaisakaHeartFlowChatting:
|
||||
if not cached_messages:
|
||||
break
|
||||
await self._internal_turn_queue.put(cached_messages)
|
||||
asyncio.create_task(self._trigger_expression_learning(cached_messages))
|
||||
asyncio.create_task(self._trigger_batch_learning(cached_messages))
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"{self.log_prefix} Maisaka runtime loop cancelled")
|
||||
|
||||
@@ -223,6 +226,18 @@ class MaisakaHeartFlowChatting:
|
||||
self._agent_state = self._STATE_STOP
|
||||
self._wait_until = None
|
||||
|
||||
async def _trigger_batch_learning(self, messages: list[SessionMessage]) -> None:
|
||||
"""按同一批消息触发表达方式、黑话和 knowledge 学习。"""
|
||||
expression_result, knowledge_result = await asyncio.gather(
|
||||
self._trigger_expression_learning(messages),
|
||||
self._trigger_knowledge_learning(messages),
|
||||
return_exceptions=True,
|
||||
)
|
||||
if isinstance(expression_result, Exception):
|
||||
logger.error(f"{self.log_prefix} expression learning task crashed: {expression_result}")
|
||||
if isinstance(knowledge_result, Exception):
|
||||
logger.error(f"{self.log_prefix} knowledge learning task crashed: {knowledge_result}")
|
||||
|
||||
async def _trigger_expression_learning(self, messages: list[SessionMessage]) -> None:
|
||||
"""Trigger expression learning from the newly collected batch."""
|
||||
self._expression_learner.add_messages(messages)
|
||||
@@ -231,7 +246,7 @@ class MaisakaHeartFlowChatting:
|
||||
logger.debug(f"{self.log_prefix} expression learning disabled, skip this batch")
|
||||
return
|
||||
|
||||
elapsed = time.time() - self._last_extraction_time
|
||||
elapsed = time.time() - self._last_expression_extraction_time
|
||||
if elapsed < self._min_extraction_interval:
|
||||
logger.debug(
|
||||
f"{self.log_prefix} expression learning interval not reached: "
|
||||
@@ -248,7 +263,7 @@ class MaisakaHeartFlowChatting:
|
||||
)
|
||||
return
|
||||
|
||||
self._last_extraction_time = time.time()
|
||||
self._last_expression_extraction_time = time.time()
|
||||
logger.info(
|
||||
f"{self.log_prefix} starting expression learning: "
|
||||
f"new_batch={len(messages)} learner_cache={cache_size} "
|
||||
@@ -266,6 +281,47 @@ class MaisakaHeartFlowChatting:
|
||||
except Exception:
|
||||
logger.exception(f"{self.log_prefix} expression learning failed")
|
||||
|
||||
async def _trigger_knowledge_learning(self, messages: list[SessionMessage]) -> None:
|
||||
"""Trigger knowledge learning from the newly collected batch."""
|
||||
self._knowledge_learner.add_messages(messages)
|
||||
|
||||
if not global_config.maisaka.enable_knowledge_module:
|
||||
logger.debug(f"{self.log_prefix} knowledge learning disabled, skip this batch")
|
||||
return
|
||||
|
||||
elapsed = time.time() - self._last_knowledge_extraction_time
|
||||
if elapsed < self._min_extraction_interval:
|
||||
logger.debug(
|
||||
f"{self.log_prefix} knowledge learning interval not reached: "
|
||||
f"elapsed={elapsed:.2f}s threshold={self._min_extraction_interval}s"
|
||||
)
|
||||
return
|
||||
|
||||
cache_size = self._knowledge_learner.get_cache_size()
|
||||
if cache_size < self._min_messages_for_extraction:
|
||||
logger.debug(
|
||||
f"{self.log_prefix} knowledge learning skipped due to cache size: "
|
||||
f"learner_cache={cache_size} threshold={self._min_messages_for_extraction} "
|
||||
f"message_cache_total={len(self.message_cache)}"
|
||||
)
|
||||
return
|
||||
|
||||
self._last_knowledge_extraction_time = time.time()
|
||||
logger.info(
|
||||
f"{self.log_prefix} starting knowledge learning: "
|
||||
f"new_batch={len(messages)} learner_cache={cache_size} "
|
||||
f"message_cache_total={len(self.message_cache)}"
|
||||
)
|
||||
|
||||
try:
|
||||
added_count = await self._knowledge_learner.learn()
|
||||
if added_count > 0:
|
||||
logger.info(f"{self.log_prefix} knowledge learning finished: added={added_count}")
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix} knowledge learning finished without usable result")
|
||||
except Exception:
|
||||
logger.exception(f"{self.log_prefix} knowledge learning failed")
|
||||
|
||||
async def _init_mcp(self) -> None:
|
||||
"""Initialize MCP tools and inject them into the planner."""
|
||||
config_path = Path(__file__).resolve().parents[2] / "config" / "mcp_config.json"
|
||||
|
||||
@@ -12,10 +12,10 @@ import os
|
||||
from rich.panel import Panel
|
||||
|
||||
from src.chat.message_receive.message import SessionMessage
|
||||
from src.cli.console import console
|
||||
from src.cli.input_reader import InputReader
|
||||
from src.llm_models.payload_content.tool_option import ToolCall
|
||||
|
||||
from .console import console
|
||||
from .input_reader import InputReader
|
||||
from .message_adapter import build_message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
Reference in New Issue
Block a user