ref:分离know模块和cli模块

This commit is contained in:
SengokuCola
2026-03-29 15:06:50 +08:00
parent f32edfa732
commit 8cb0ad3d42
15 changed files with 556 additions and 271 deletions

View File

@@ -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,

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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 ""

View File

@@ -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

View File

@@ -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"

View File

@@ -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: