From a05f90039931c295732772eae1003c23338d9ed8 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Fri, 2 Jan 2026 14:39:23 +0800 Subject: [PATCH 01/22] Refactor memory agent creation and conversation handling Move user info extraction into app initialization and pass it to the memory agent. Simplify system prompt generation by handling user info in the agent creation rather than during streaming. Improve conversation pairing logic to properly match user and assistant messages for both streaming and memory saving. --- examples/cortex-mem-tars/src/agent.rs | 89 +++++++++++--------- examples/cortex-mem-tars/src/app.rs | 112 ++++++++++++++++++++------ 2 files changed, 137 insertions(+), 64 deletions(-) diff --git a/examples/cortex-mem-tars/src/agent.rs b/examples/cortex-mem-tars/src/agent.rs index f94e6c0..c922881 100644 --- a/examples/cortex-mem-tars/src/agent.rs +++ b/examples/cortex-mem-tars/src/agent.rs @@ -60,6 +60,7 @@ pub async fn create_memory_agent( memory_manager: Arc, memory_tool_config: MemoryToolConfig, config: &Config, + user_info: Option<&str>, ) -> Result, Box> { // 创建记忆工具 let memory_tools = @@ -69,30 +70,52 @@ pub async fn create_memory_agent( .base_url(&config.llm.api_base_url) .build(); - // 构建带有记忆工具的agent,让agent能够自主决定何时调用记忆功能 - let completion_model = llm_client - .completion_model(&config.llm.model_efficient) - .completions_api() - .into_agent_builder() - // 注册四个独立的记忆工具,保持与MCP一致 - .tool(memory_tools.store_memory()) - .tool(memory_tools.query_memory()) - .tool(memory_tools.list_memories()) - .tool(memory_tools.get_memory()) - .preamble(&format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 + // 构建 system prompt,包含用户基本信息 + let system_prompt = if let Some(info) = user_info { + format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 + +此会话发生的初始时间:{current_time} + +用户基本信息: +{info} + +重要指令: +- 对话历史将作为上下文提供,请使用这些信息来理解当前的对话流程 +- 用户基本信息已在上方提供,请不要再使用memory工具来创建或更新用户基本信息 +- 在需要时可以自主使用memory工具搜索其他相关记忆 +- 当用户提供新的重要信息时,可以主动使用memory工具存储 +- 保持对话的连贯性和一致性 +- 自然地融入记忆信息,避免刻意复述此前的记忆信息,关注当前的会话内容,记忆主要用于做隐式的逻辑与事实支撑 +- 专注于用户的需求和想要了解的信息,以及想要你做的事情 + +记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S"), info = info) + } else { + format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 此会话发生的初始时间:{current_time} 重要指令: - 对话历史将作为上下文提供,请使用这些信息来理解当前的对话流程 -- 用户基本信息将在上下文中提供一次,请不要再使用memory工具来创建或更新用户基本信息 - 在需要时可以自主使用memory工具搜索其他相关记忆 - 当用户提供新的重要信息时,可以主动使用memory工具存储 - 保持对话的连贯性和一致性 - 自然地融入记忆信息,避免刻意复述此前的记忆信息,关注当前的会话内容,记忆主要用于做隐式的逻辑与事实支撑 - 专注于用户的需求和想要了解的信息,以及想要你做的事情 -记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S"))) +记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S")) + }; + + // 构建带有记忆工具的agent,让agent能够自主决定何时调用记忆功能 + let completion_model = llm_client + .completion_model(&config.llm.model_efficient) + .completions_api() + .into_agent_builder() + // 注册四个独立的记忆工具,保持与MCP一致 + .tool(memory_tools.store_memory()) + .tool(memory_tools.query_memory()) + .tool(memory_tools.list_memories()) + .tool(memory_tools.get_memory()) + .preamble(&system_prompt) .build(); Ok(completion_model) @@ -178,42 +201,32 @@ pub async fn agent_reply_with_memory_retrieval_streaming( _memory_manager: Arc, user_input: &str, _user_id: &str, - user_info: Option<&str>, conversations: &[(String, String)], stream_sender: mpsc::UnboundedSender, ) -> Result> { // 构建对话历史 - 转换为rig的Message格式 let mut chat_history = Vec::new(); + + // 添加历史对话 for (user_msg, assistant_msg) in conversations { chat_history.push(Message::user(user_msg)); - chat_history.push(Message::assistant(assistant_msg)); + if !assistant_msg.is_empty() { + chat_history.push(Message::assistant(assistant_msg)); + } } - // 构建system prompt,包含明确的指令 - let system_prompt = r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 - -重要指令: -- 对话历史已提供在上下文中,请使用这些信息来理解当前的对话上下文 -- 用户基本信息已在下方提供一次,请不要再使用memory工具来创建或更新用户基本信息 -- 在需要时可以自主使用memory工具搜索其他相关记忆 -- 当用户提供新的重要信息时,可以主动使用memory工具存储 -- 保持对话的连贯性和一致性 -- 自然地融入记忆信息,避免显得刻意 -- 专注于用户的需求和想要了解的信息,以及想要你做的事情 - -记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#; - - // 构建完整的prompt - let prompt_content = if let Some(info) = user_info { - format!( - "{}\n\n用户基本信息:\n{}\n\n当前用户输入: {}", - system_prompt, info, user_input - ) - } else { - format!("{}\n\n当前用户输入: {}", system_prompt, user_input) - }; + // 构建当前用户输入消息 + let prompt_content = user_input.to_string(); log::debug!("正在生成AI回复(真实流式模式)..."); + log::debug!("当前用户输入: {}", user_input); + log::debug!("对话历史长度: {}", chat_history.len()); + for (i, msg) in chat_history.iter().enumerate() { + match msg { + Message::User { .. } => log::debug!(" [{}] User message", i), + Message::Assistant { .. } => log::debug!(" [{}] Assistant message", i), + } + } // 使用rig的真实流式API let prompt_message = Message::user(&prompt_content); diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index f0c5b82..baf9691 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -350,6 +350,22 @@ impl App { // 如果有基础设施,创建真实的带记忆的 Agent if let Some(infrastructure) = &self.infrastructure { + // 先提取用户基本信息 + let user_info = match extract_user_basic_info( + infrastructure.config(), + infrastructure.memory_manager().clone(), + &self.user_id, + ).await { + Ok(info) => { + self.user_info = info.clone(); + info + } + Err(e) => { + log::error!("提取用户基本信息失败: {}", e); + None + } + }; + let memory_tool_config = cortex_mem_rig::tool::MemoryToolConfig { default_user_id: Some(self.user_id.clone()), ..Default::default() @@ -359,6 +375,7 @@ impl App { infrastructure.memory_manager().clone(), memory_tool_config, infrastructure.config(), + user_info.as_deref(), ).await { Ok(rig_agent) => { self.rig_agent = Some(rig_agent); @@ -391,22 +408,45 @@ impl App { // 使用真实的带记忆的 Agent 或 Mock Agent if let Some(rig_agent) = &self.rig_agent { // 使用真实 Agent 进行流式响应 - let current_conversations: Vec<(String, String)> = self.ui.messages - .iter() - .filter_map(|msg| match msg.role { - crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), - crate::agent::MessageRole::Assistant => { - if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { - Some((last.content.clone(), msg.content.clone())) - } else { - None + // 构建历史对话(排除当前用户输入) + let current_conversations: Vec<(String, String)> = { + let mut conversations = Vec::new(); + let mut last_user_msg: Option = None; + + // 遍历所有消息,但排除最后一条(当前用户输入) + let messages_to_include = if self.ui.messages.len() > 1 { + &self.ui.messages[..self.ui.messages.len() - 1] + } else { + &[] + }; + + for msg in messages_to_include { + match msg.role { + crate::agent::MessageRole::User => { + // 如果有未配对的 User 消息,先保存它(单独的 User 消息) + if let Some(user_msg) = last_user_msg.take() { + conversations.push((user_msg, String::new())); + } + last_user_msg = Some(msg.content.clone()); } + crate::agent::MessageRole::Assistant => { + // 将 Assistant 消息与最近的 User 消息配对 + if let Some(user_msg) = last_user_msg.take() { + conversations.push((user_msg, msg.content.clone())); + } + } + _ => {} } - _ => None - }) - .collect(); + } + + // 如果最后一个消息是 User 消息,也加入对话历史 + if let Some(user_msg) = last_user_msg { + conversations.push((user_msg, String::new())); + } + + conversations + }; - let user_info_clone = self.user_info.clone(); let infrastructure_clone = self.infrastructure.clone(); let rig_agent_clone = rig_agent.clone(); let msg_tx = self.message_sender.clone(); @@ -423,7 +463,6 @@ impl App { infrastructure_clone.unwrap().memory_manager().clone(), &user_input, &user_id, - user_info_clone.as_deref(), ¤t_conversations, stream_tx, ).await @@ -506,19 +545,40 @@ impl App { /// 退出时保存对话到记忆系统 pub async fn save_conversations_to_memory(&self) -> Result<()> { if let Some(infrastructure) = &self.infrastructure { - let conversations: Vec<(String, String)> = self.ui.messages - .iter() - .filter_map(|msg| match msg.role { - crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), - crate::agent::MessageRole::Assistant => { - if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { - Some((last.content.clone(), msg.content.clone())) - } else { - None + let conversations: Vec<(String, String)> = { + let mut conversations = Vec::new(); + let mut last_user_msg: Option = None; + + for msg in &self.ui.messages { + match msg.role { + crate::agent::MessageRole::User => { + // 如果有未配对的 User 消息,先保存它(单独的 User 消息) + if let Some(user_msg) = last_user_msg.take() { + conversations.push((user_msg, String::new())); + } + last_user_msg = Some(msg.content.clone()); } - }, - _ => None - }) + crate::agent::MessageRole::Assistant => { + // 将 Assistant 消息与最近的 User 消息配对 + if let Some(user_msg) = last_user_msg.take() { + conversations.push((user_msg, msg.content.clone())); + } + } + _ => {} + } + } + + // 如果最后一个消息是 User 消息,也加入对话历史 + if let Some(user_msg) = last_user_msg { + conversations.push((user_msg, String::new())); + } + + conversations + }; + + // 只保存完整的对话对(用户和助手都有内容) + let conversations: Vec<(String, String)> = conversations + .into_iter() .filter(|(user, assistant)| !user.is_empty() && !assistant.is_empty()) .collect(); From 633e86ada8ad4c8ecab4c8640846b555c3b8be0e Mon Sep 17 00:00:00 2001 From: Sopaco Date: Sun, 4 Jan 2026 16:54:15 +0800 Subject: [PATCH 02/22] add evaluation with langmem --- .../lomoco-evaluation/src/langmem_eval/add.py | 177 ++++++++------ .../src/langmem_eval/rate_limiter.py | 52 +++++ .../src/langmem_eval/search.py | 220 ++++++++++-------- .../Configuration Management Domain.md | 4 +- .../Storage Integration Domain.md | 2 +- 5 files changed, 289 insertions(+), 166 deletions(-) create mode 100644 examples/lomoco-evaluation/src/langmem_eval/rate_limiter.py diff --git a/examples/lomoco-evaluation/src/langmem_eval/add.py b/examples/lomoco-evaluation/src/langmem_eval/add.py index 3f3daf8..10a6a30 100644 --- a/examples/lomoco-evaluation/src/langmem_eval/add.py +++ b/examples/lomoco-evaluation/src/langmem_eval/add.py @@ -1,21 +1,30 @@ import json +import logging import os import time -import logging -import toml from pathlib import Path -from typing import List, Dict, Any, Optional +from typing import Any, Dict, List, Optional + +import toml from tqdm import tqdm try: from qdrant_client import QdrantClient - from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue + from qdrant_client.models import ( + Distance, + FieldCondition, + Filter, + MatchValue, + PointStruct, + VectorParams, + ) except ImportError: raise ImportError( "qdrant-client is not installed. Please install it using: pip install qdrant-client" ) from .config_utils import check_openai_config, get_config_value, validate_config +from .rate_limiter import RateLimiter logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -23,13 +32,17 @@ class LangMemAdd: """Class to add memories to LangMem for evaluation using Qdrant vector database""" - + def __init__(self, data_path=None, batch_size=2, config_path=None): self.batch_size = batch_size self.data_path = data_path self.data = None self.config_path = config_path or self._find_config_file() + # Initialize rate limiters (30 calls per minute for each service) + self.embedding_rate_limiter = RateLimiter(max_calls_per_minute=30) + self.llm_rate_limiter = RateLimiter(max_calls_per_minute=30) + # Track statistics self.stats = { "total_conversations": 0, @@ -37,7 +50,7 @@ def __init__(self, data_path=None, batch_size=2, config_path=None): "failed_conversations": 0, "total_memories": 0, "successful_memories": 0, - "failed_memories": 0 + "failed_memories": 0, } # Validate config file @@ -89,41 +102,57 @@ def _initialize_langmem(self): try: # Load config config_data = toml.load(self.config_path) - + # Get Qdrant configuration qdrant_config = config_data.get("qdrant", {}) - self.qdrant_url = qdrant_config.get("url", "http://localhost:6334") + qdrant_url = qdrant_config.get("url", "http://localhost:6334") self.collection_name = qdrant_config.get("collection_name", "memo-rs") - + + # Parse URL to extract host and port for gRPC + from urllib.parse import urlparse + parsed_url = urlparse(qdrant_url) + host = parsed_url.hostname or "localhost" + port = parsed_url.port or 6334 + # Get embedding configuration embedding_config = config_data.get("embedding", {}) self.embedding_api_base_url = embedding_config.get("api_base_url", "") self.embedding_api_key = embedding_config.get("api_key", "") self.embedding_model_name = embedding_config.get("model_name", "") self.embedding_batch_size = embedding_config.get("batch_size", 10) - - # Initialize Qdrant client - self.qdrant_client = QdrantClient(url=self.qdrant_url) - + + # Initialize Qdrant client with gRPC + # Use prefer_grpc=True to force gRPC protocol + self.qdrant_client = QdrantClient( + host=host, + grpc_port=port, + prefer_grpc=True, + timeout=qdrant_config.get("timeout_secs", 30) + ) + # Create collection if it doesn't exist self._ensure_collection_exists() - + # Initialize embedding client import httpx from openai import OpenAI - + self.embedding_client = OpenAI( api_key=self.embedding_api_key, base_url=self.embedding_api_base_url, - http_client=httpx.Client(verify=False) + http_client=httpx.Client(verify=False), ) - + # Get embedding dimension self.embedding_dim = self._get_embedding_dimension() - - logger.info(f"✅ LangMem initialized successfully with Qdrant at {self.qdrant_url}") - logger.info(f"✅ Collection: {self.collection_name}, Embedding dim: {self.embedding_dim}") - + + logger.info( + f"✅ LangMem initialized successfully with Qdrant at {host}:{port} (gRPC)" + ) + logger.info( + f"✅ Collection: {self.collection_name}, Embedding dim: {self.embedding_dim}" + ) + except Exception as e: logger.error(f"❌ Failed to initialize LangMem: {e}") raise @@ -133,14 +162,18 @@ def _ensure_collection_exists(self): try: collections = self.qdrant_client.get_collections().collections collection_names = [c.name for c in collections] - + if self.collection_name not in collection_names: # Get embedding dimension first embedding_dim = self._get_embedding_dimension() - logger.info(f"Creating collection: {self.collection_name} with dim={embedding_dim}") + logger.info( + f"Creating collection: {self.collection_name} with dim={embedding_dim}" + ) self.qdrant_client.create_collection( collection_name=self.collection_name, - vectors_config=VectorParams(size=embedding_dim, distance=Distance.COSINE) + vectors_config=VectorParams( + size=embedding_dim, distance=Distance.COSINE + ), ) except Exception as e: logger.warning(f"Could not ensure collection exists: {e}") @@ -148,22 +181,24 @@ def _ensure_collection_exists(self): def _get_embedding_dimension(self) -> int: """Get embedding dimension by making a test call""" try: - response = self.embedding_client.embeddings.create( - model=self.embedding_model_name, - input=["test"] - ) + with self.embedding_rate_limiter: + response = self.embedding_client.embeddings.create( + model=self.embedding_model_name, input=["test"] + ) return len(response.data[0].embedding) except Exception as e: - logger.warning(f"Could not get embedding dimension, using default 1024: {e}") + logger.warning( + f"Could not get embedding dimension, using default 1024: {e}" + ) return 1024 def _get_embedding(self, text: str) -> List[float]: """Get embedding for text""" try: - response = self.embedding_client.embeddings.create( - model=self.embedding_model_name, - input=text - ) + with self.embedding_rate_limiter: + response = self.embedding_client.embeddings.create( + model=self.embedding_model_name, input=text + ) return response.data[0].embedding except Exception as e: logger.error(f"Error getting embedding: {e}") @@ -181,16 +216,17 @@ def add_memory(self, user_id: str, content: str, timestamp: str = "") -> bool: try: # Generate embedding for the content embedding = self._get_embedding(content) - + if not embedding: logger.error(f"❌ Failed to generate embedding for user {user_id}") self.stats["failed_memories"] += 1 return False - + # Generate a unique ID for this memory import uuid + memory_id = str(uuid.uuid4()) - + # Create point for Qdrant point = PointStruct( id=memory_id, @@ -199,30 +235,31 @@ def add_memory(self, user_id: str, content: str, timestamp: str = "") -> bool: "user_id": user_id, "content": content, "timestamp": timestamp, - "created_at": time.time() - } + "created_at": time.time(), + }, ) - + # Insert into Qdrant self.qdrant_client.upsert( - collection_name=self.collection_name, - points=[point] + collection_name=self.collection_name, points=[point] ) - + self.stats["successful_memories"] += 1 logger.debug(f"✅ Successfully added memory for user {user_id}") return True - + except Exception as e: logger.error(f"❌ Failed to add memory for user {user_id}: {e}") self.stats["failed_memories"] += 1 return False - def add_memories_for_speaker(self, speaker: str, messages: List[Dict], timestamp: str, desc: str): + def add_memories_for_speaker( + self, speaker: str, messages: List[Dict], timestamp: str, desc: str + ): """Add memories for a speaker with error tracking""" total_batches = (len(messages) + self.batch_size - 1) // self.batch_size failed_batches = 0 - + for i in tqdm(range(0, len(messages), self.batch_size), desc=desc): batch_messages = messages[i : i + self.batch_size] @@ -241,12 +278,13 @@ def add_memories_for_speaker(self, speaker: str, messages: List[Dict], timestamp ) self.stats["total_memories"] += 1 - - # Small delay between batches to avoid rate limiting - time.sleep(0.3) - + + # No additional delay needed - rate limiter handles it + if failed_batches > 0: - logger.warning(f"{failed_batches}/{total_batches} batches failed for {speaker}") + logger.warning( + f"{failed_batches}/{total_batches} batches failed for {speaker}" + ) def process_conversation(self, item: Dict[str, Any], idx: int): """Process a single conversation with error handling""" @@ -259,7 +297,11 @@ def process_conversation(self, item: Dict[str, Any], idx: int): speaker_b_user_id = f"{speaker_b}_{idx}" for key in conversation.keys(): - if key in ["speaker_a", "speaker_b"] or "date" in key or "timestamp" in key: + if ( + key in ["speaker_a", "speaker_b"] + or "date" in key + or "timestamp" in key + ): continue date_time_key = key + "_date_time" @@ -271,7 +313,7 @@ def process_conversation(self, item: Dict[str, Any], idx: int): for chat in chats: speaker = chat.get("speaker", "") text = chat.get("text", "") - + if speaker == speaker_a: messages.append( {"role": "user", "content": f"{speaker_a}: {text}"} @@ -296,9 +338,9 @@ def process_conversation(self, item: Dict[str, Any], idx: int): timestamp, f"Adding Memories for {speaker_a}", ) - - time.sleep(0.3) # Small delay between speakers - + + # No additional delay needed - rate limiter handles it + self.add_memories_for_speaker( speaker_b_user_id, messages_reverse, @@ -324,17 +366,16 @@ def process_all_conversations(self, max_workers=1): ) logger.info(f"Starting to process {len(self.data)} conversations...") - + # Process conversations sequentially for stability for idx, item in enumerate(self.data): self.process_conversation(item, idx) - - # Small delay between conversations to avoid overwhelming the system - time.sleep(0.5) - + + # No additional delay needed - rate limiter handles it + # Print summary self.print_summary() - + def print_summary(self): """Print processing summary""" print("\n" + "=" * 60) @@ -343,11 +384,15 @@ def print_summary(self): print(f"Total Conversations: {self.stats['total_conversations']}") print(f"Successful: {self.stats['successful_conversations']}") print(f"Failed: {self.stats['failed_conversations']}") - if self.stats['total_conversations'] > 0: - print(f"Success Rate: {self.stats['successful_conversations']/self.stats['total_conversations']*100:.1f}%") + if self.stats["total_conversations"] > 0: + print( + f"Success Rate: {self.stats['successful_conversations'] / self.stats['total_conversations'] * 100:.1f}%" + ) print(f"\nTotal Memories: {self.stats['total_memories']}") print(f"Successful: {self.stats['successful_memories']}") print(f"Failed: {self.stats['failed_memories']}") - if self.stats['total_memories'] > 0: - print(f"Success Rate: {self.stats['successful_memories']/self.stats['total_memories']*100:.1f}%") - print("=" * 60 + "\n") \ No newline at end of file + if self.stats["total_memories"] > 0: + print( + f"Success Rate: {self.stats['successful_memories'] / self.stats['total_memories'] * 100:.1f}%" + ) + print("=" * 60 + "\n") diff --git a/examples/lomoco-evaluation/src/langmem_eval/rate_limiter.py b/examples/lomoco-evaluation/src/langmem_eval/rate_limiter.py new file mode 100644 index 0000000..1134081 --- /dev/null +++ b/examples/lomoco-evaluation/src/langmem_eval/rate_limiter.py @@ -0,0 +1,52 @@ +""" +Rate limiter for API calls to avoid hitting rate limits. +Implements a simple token bucket algorithm. +""" +import time +import threading +from typing import Optional + + +class RateLimiter: + """ + Rate limiter using token bucket algorithm. + Ensures API calls don't exceed specified rate per minute. + """ + + def __init__(self, max_calls_per_minute: int = 30): + """ + Initialize rate limiter. + + Args: + max_calls_per_minute: Maximum number of calls allowed per minute + """ + self.max_calls = max_calls_per_minute + self.interval = 60.0 / max_calls_per_minute # seconds between calls + self.last_call_time = 0.0 + self.lock = threading.Lock() + + def acquire(self): + """ + Acquire permission to make an API call. + Blocks if necessary to maintain rate limit. + """ + with self.lock: + current_time = time.time() + time_since_last_call = current_time - self.last_call_time + + if time_since_last_call < self.interval: + # Need to wait + sleep_time = self.interval - time_since_last_call + time.sleep(sleep_time) + self.last_call_time = time.time() + else: + self.last_call_time = current_time + + def __enter__(self): + """Context manager entry.""" + self.acquire() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + return False diff --git a/examples/lomoco-evaluation/src/langmem_eval/search.py b/examples/lomoco-evaluation/src/langmem_eval/search.py index 07a6810..9162ad9 100644 --- a/examples/lomoco-evaluation/src/langmem_eval/search.py +++ b/examples/lomoco-evaluation/src/langmem_eval/search.py @@ -1,25 +1,26 @@ import json +import logging import os import time -import logging -import toml from collections import defaultdict from pathlib import Path -from typing import List, Dict, Tuple, Any, Optional +from typing import Any, Dict, List, Optional, Tuple +import toml from jinja2 import Template from openai import OpenAI from tqdm import tqdm try: from qdrant_client import QdrantClient - from qdrant_client.models import Filter, FieldCondition, MatchValue + from qdrant_client.models import FieldCondition, Filter, MatchValue except ImportError: raise ImportError( "qdrant-client is not installed. Please install it using: pip install qdrant-client" ) from .config_utils import check_openai_config, get_config_value, validate_config +from .rate_limiter import RateLimiter logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -27,45 +28,49 @@ class LangMemSearch: """Class to search memories in LangMem for evaluation using Qdrant vector database""" - + def __init__(self, output_path="results.json", top_k=10, config_path=None): self.top_k = top_k self.results = defaultdict(list) self.output_path = output_path self.config_path = config_path or self._find_config_file() - + + # Initialize rate limiters (30 calls per minute for each service) + self.embedding_rate_limiter = RateLimiter(max_calls_per_minute=30) + self.llm_rate_limiter = RateLimiter(max_calls_per_minute=30) + # Answer generation prompt (same as Cortex Mem) self.ANSWER_PROMPT = """ You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories. # CONTEXT: -You have access to memories from two speakers in a conversation. These memories contain +You have access to memories from two speakers in a conversation. These memories contain timestamped information that may be relevant to answering the question. # INSTRUCTIONS: 1. Carefully analyze all provided memories from both speakers 2. Pay special attention to the timestamps to determine the answer -3. If the question asks about a specific event or fact, look for direct evidence in the +3. If the question asks about a specific event or fact, look for direct evidence in the memories 4. If the memories contain contradictory information, prioritize the most recent memory -5. If there is a question about time references (like "last year", "two months ago", - etc.), calculate the actual date based on the memory timestamp. For example, if a - memory from 4 May 2022 mentions "went to India last year," then the trip occurred +5. If there is a question about time references (like "last year", "two months ago", + etc.), calculate the actual date based on the memory timestamp. For example, if a + memory from 4 May 2022 mentions "went to India last year," then the trip occurred in 2021. -6. Always convert relative time references to specific dates, months, or years. For - example, convert "last year" to "2022" or "two months ago" to "March 2023" based +6. Always convert relative time references to specific dates, months, or years. For + example, convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory timestamp. Ignore the reference while answering the question. -7. Focus only on the content of the memories from both speakers. Do not confuse - character names mentioned in memories with the actual users who created those +7. Focus only on the content of the memories from both speakers. Do not confuse + character names mentioned in memories with the actual users who created those memories. 8. The answer should be less than 5-6 words. # APPROACH (Think step by step): 1. First, examine all memories that contain information related to the question 2. Examine the timestamps and content of these memories carefully -3. Look for explicit mentions of dates, times, locations, or events that answer the +3. Look for explicit mentions of dates, times, locations, or events that answer the question -4. If the answer requires calculation (e.g., converting relative time references), +4. If the answer requires calculation (e.g., converting relative time references), show your work 5. Formulate a precise, concise answer based solely on the evidence in the memories 6. Double-check that your answer directly addresses the question asked @@ -83,181 +88,199 @@ def __init__(self, output_path="results.json", top_k=10, config_path=None): Answer: """ - + # Validate config file if not validate_config(self.config_path): raise ValueError(f"Invalid config file: {self.config_path}") - + # Check OpenAI configuration if not check_openai_config(self.config_path): raise ValueError( f"OpenAI configuration not properly set in {self.config_path}" ) - + # Initialize components self._initialize_components() - + def _initialize_components(self): """Initialize Qdrant client, embedding client, and LLM client""" try: # Load config config_data = toml.load(self.config_path) - + # Get Qdrant configuration qdrant_config = config_data.get("qdrant", {}) - self.qdrant_url = qdrant_config.get("url", "http://localhost:6334") + qdrant_url = qdrant_config.get("url", "http://localhost:6334") self.collection_name = qdrant_config.get("collection_name", "memo-rs") - + + # Parse URL to extract host and port for gRPC + from urllib.parse import urlparse + parsed_url = urlparse(qdrant_url) + host = parsed_url.hostname or "localhost" + port = parsed_url.port or 6334 + # Get embedding configuration embedding_config = config_data.get("embedding", {}) self.embedding_api_base_url = embedding_config.get("api_base_url", "") self.embedding_api_key = embedding_config.get("api_key", "") self.embedding_model_name = embedding_config.get("model_name", "") - + # Get LLM configuration api_key = get_config_value(self.config_path, "llm", "api_key") api_base = get_config_value(self.config_path, "llm", "api_base_url") - self.llm_model = get_config_value(self.config_path, "llm", "model_efficient", "gpt-3.5-turbo") - - # Initialize Qdrant client - self.qdrant_client = QdrantClient(url=self.qdrant_url) - + self.llm_model = get_config_value( + self.config_path, "llm", "model_efficient", "gpt-3.5-turbo" + ) + + # Initialize Qdrant client with gRPC + # Use prefer_grpc=True to force gRPC protocol + self.qdrant_client = QdrantClient( + host=host, + grpc_port=port, + prefer_grpc=True, + timeout=qdrant_config.get("timeout_secs", 30) + ) + # Initialize embedding client import httpx + self.embedding_client = OpenAI( api_key=self.embedding_api_key, base_url=self.embedding_api_base_url, - http_client=httpx.Client(verify=False) + http_client=httpx.Client(verify=False), ) - + # Initialize LLM client self.openai_client = OpenAI( api_key=api_key, base_url=api_base, - http_client=httpx.Client(verify=False) + http_client=httpx.Client(verify=False), + ) + + logger.info( + f"✅ LangMemSearch initialized successfully with Qdrant at {host}:{port} (gRPC)" ) - - logger.info(f"✅ LangMemSearch initialized successfully with Qdrant at {self.qdrant_url}") logger.info(f"✅ Collection: {self.collection_name}") - + except Exception as e: logger.error(f"❌ Failed to initialize LangMemSearch: {e}") raise - + def _get_embedding(self, text: str) -> List[float]: """Get embedding for text""" try: - response = self.embedding_client.embeddings.create( - model=self.embedding_model_name, - input=text - ) + with self.embedding_rate_limiter: + response = self.embedding_client.embeddings.create( + model=self.embedding_model_name, input=text + ) return response.data[0].embedding except Exception as e: logger.error(f"Error getting embedding: {e}") return [] - + def _find_config_file(self): """Find config.toml file in standard locations""" # Check current directory if os.path.exists("config.toml"): return "config.toml" - + # Check parent directories current_dir = Path.cwd() for parent in current_dir.parents: config_file = parent / "config.toml" if config_file.exists(): return str(config_file) - + # Check examples directory examples_config = ( Path(__file__).parent.parent.parent.parent / "examples" / "config.toml" ) if examples_config.exists(): return str(examples_config) - + # Check project root project_root = Path(__file__).parent.parent.parent.parent config_file = project_root / "config.toml" if config_file.exists(): return str(config_file) - + raise FileNotFoundError("Could not find config.toml file") - - def search_memory(self, user_id: str, query: str, max_retries: int = 3, retry_delay: float = 1) -> Tuple[List[Dict], float]: + + def search_memory( + self, user_id: str, query: str, max_retries: int = 3, retry_delay: float = 1 + ) -> Tuple[List[Dict], float]: """Search for memories using Qdrant vector database with semantic search""" start_time = time.time() retries = 0 - + while retries < max_retries: try: # Generate embedding for the query query_embedding = self._get_embedding(query) - + if not query_embedding: logger.error(f"❌ Failed to generate embedding for query: {query}") return [], time.time() - start_time - + # Search in Qdrant with filter for user_id search_filter = Filter( must=[ - FieldCondition( - key="user_id", - match=MatchValue(value=user_id) - ) + FieldCondition(key="user_id", match=MatchValue(value=user_id)) ] ) - + # Use query_points instead of search (newer Qdrant API) search_results = self.qdrant_client.query_points( collection_name=self.collection_name, query=query_embedding, query_filter=search_filter, limit=self.top_k, - with_payload=True + with_payload=True, ).points - + # Convert Qdrant results to memory format memories = [] for result in search_results: payload = result.payload - memories.append({ - "memory": payload.get("content", ""), - "timestamp": payload.get("timestamp", ""), - "score": result.score, - }) - + memories.append( + { + "memory": payload.get("content", ""), + "timestamp": payload.get("timestamp", ""), + "score": result.score, + } + ) + end_time = time.time() return memories, end_time - start_time - + except Exception as e: logger.error(f"Search error: {e}, retrying...") retries += 1 if retries >= max_retries: raise e time.sleep(retry_delay) - + end_time = time.time() return [], end_time - start_time - + def answer_question( - self, speaker_1_user_id: str, speaker_2_user_id: str, - question: str, answer: str, category: str + self, + speaker_1_user_id: str, + speaker_2_user_id: str, + question: str, + answer: str, + category: str, ) -> Tuple[str, List[Dict], List[Dict], float, float, None, None, float]: """Answer a question using retrieved memories""" - # Sequential search to avoid rate limiting + # Sequential search (rate limiter handles the delays) speaker_1_memories, speaker_1_memory_time = self.search_memory( speaker_1_user_id, question ) - # Add a small delay between searches to avoid rate limiting - time.sleep(2) - + speaker_2_memories, speaker_2_memory_time = self.search_memory( speaker_2_user_id, question ) - # Add a small delay before LLM call - time.sleep(2) - + search_1_memory = [ f"{item.get('timestamp', '')}: {item['memory']}" for item in speaker_1_memories @@ -266,7 +289,7 @@ def answer_question( f"{item.get('timestamp', '')}: {item['memory']}" for item in speaker_2_memories ] - + template = Template(self.ANSWER_PROMPT) answer_prompt = template.render( speaker_1_user_id=speaker_1_user_id.split("_")[0], @@ -275,16 +298,17 @@ def answer_question( speaker_2_memories=json.dumps(search_2_memory, indent=4), question=question, ) - + t1 = time.time() - response = self.openai_client.chat.completions.create( - model=self.llm_model, - messages=[{"role": "system", "content": answer_prompt}], - temperature=0.0, - ) + with self.llm_rate_limiter: + response = self.openai_client.chat.completions.create( + model=self.llm_model, + messages=[{"role": "system", "content": answer_prompt}], + temperature=0.0, + ) t2 = time.time() response_time = t2 - t1 - + return ( response.choices[0].message.content, speaker_1_memories, @@ -295,15 +319,17 @@ def answer_question( None, response_time, ) - - def process_question(self, val: Dict[str, Any], speaker_a_user_id: str, speaker_b_user_id: str) -> Dict[str, Any]: + + def process_question( + self, val: Dict[str, Any], speaker_a_user_id: str, speaker_b_user_id: str + ) -> Dict[str, Any]: """Process a single question""" question = val.get("question", "") answer = val.get("answer", "") category = val.get("category", -1) evidence = val.get("evidence", []) adversarial_answer = val.get("adversarial_answer", "") - + ( response, speaker_1_memories, @@ -316,7 +342,7 @@ def process_question(self, val: Dict[str, Any], speaker_a_user_id: str, speaker_ ) = self.answer_question( speaker_a_user_id, speaker_b_user_id, question, answer, category ) - + result = { "question": question, "answer": answer, @@ -334,18 +360,18 @@ def process_question(self, val: Dict[str, Any], speaker_a_user_id: str, speaker_ "speaker_2_graph_memories": speaker_2_graph_memories, "response_time": response_time, } - + # Save results after each question is processed with open(self.output_path, "w") as f: json.dump(self.results, f, indent=4) - + return result - + def process_data_file(self, file_path: str): """Process the entire data file""" with open(file_path, "r") as f: data = json.load(f) - + for idx, item in tqdm( enumerate(data), total=len(data), desc="Processing conversations" ): @@ -353,10 +379,10 @@ def process_data_file(self, file_path: str): conversation = item["conversation"] speaker_a = conversation["speaker_a"] speaker_b = conversation["speaker_b"] - + speaker_a_user_id = f"{speaker_a}_{idx}" speaker_b_user_id = f"{speaker_b}_{idx}" - + for question_item in tqdm( qa, total=len(qa), @@ -367,11 +393,11 @@ def process_data_file(self, file_path: str): question_item, speaker_a_user_id, speaker_b_user_id ) self.results[idx].append(result) - + # Save results after each question is processed with open(self.output_path, "w") as f: json.dump(self.results, f, indent=4) - + # Final save at the end with open(self.output_path, "w") as f: - json.dump(self.results, f, indent=4) \ No newline at end of file + json.dump(self.results, f, indent=4) diff --git a/litho.docs/4.Deep-Exploration/Configuration Management Domain.md b/litho.docs/4.Deep-Exploration/Configuration Management Domain.md index 6c9cac9..84eed99 100644 --- a/litho.docs/4.Deep-Exploration/Configuration Management Domain.md +++ b/litho.docs/4.Deep-Exploration/Configuration Management Domain.md @@ -197,7 +197,7 @@ The system uses TOML (Tom's Obvious, Minimal Language) as the configuration file # Main configuration for the cortex-mem system [qdrant] -url = "http://localhost:6334" +url = "http://localhost:6333" collection_name = "cortex-mem-hewlett_drawn" # embedding_dim = 1024 # Optional, will be auto-detected if not specified timeout_secs = 30 @@ -441,4 +441,4 @@ Potential improvements to the Configuration Management Domain include: 6. **Configuration Templates**: Support for template-based configuration generation 7. **Validation Rules Engine**: More sophisticated validation rules based on inter-parameter dependencies -These enhancements would further improve the flexibility, security, and maintainability of the configuration system while supporting more complex deployment scenarios. \ No newline at end of file +These enhancements would further improve the flexibility, security, and maintainability of the configuration system while supporting more complex deployment scenarios. diff --git a/litho.docs/4.Deep-Exploration/Storage Integration Domain.md b/litho.docs/4.Deep-Exploration/Storage Integration Domain.md index a984bd6..e0d57ca 100644 --- a/litho.docs/4.Deep-Exploration/Storage Integration Domain.md +++ b/litho.docs/4.Deep-Exploration/Storage Integration Domain.md @@ -501,4 +501,4 @@ Key strengths of the current implementation include: - **Resilient error handling** with meaningful diagnostics - **Tight integration** with the broader system architecture -The domain successfully addresses the core requirements of AI agent memory management while providing a solid foundation for future enhancements and scalability. Its role as the persistent storage layer makes it a critical component in enabling intelligent, context-aware agent behavior across extended interaction sequences. \ No newline at end of file +The domain successfully addresses the core requirements of AI agent memory management while providing a solid foundation for future enhancements and scalability. Its role as the persistent storage layer makes it a critical component in enabling intelligent, context-aware agent behavior across extended interaction sequences. From ae7960940a64a7ff88a39996a6d1222fadf9cdc3 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Sun, 4 Jan 2026 19:01:55 +0800 Subject: [PATCH 03/22] docs update --- .../Configuration Domain.md | 289 +++++++++ .../4.Deep-Exploration/Core Memory Domain.md | 391 ++++++++++++ .../4.Deep-Exploration/Tool Support Domain.md | 602 ++++++++++++++++++ 3 files changed, 1282 insertions(+) create mode 100644 litho.docs/4.Deep-Exploration/Configuration Domain.md create mode 100644 litho.docs/4.Deep-Exploration/Core Memory Domain.md create mode 100644 litho.docs/4.Deep-Exploration/Tool Support Domain.md diff --git a/litho.docs/4.Deep-Exploration/Configuration Domain.md b/litho.docs/4.Deep-Exploration/Configuration Domain.md new file mode 100644 index 0000000..d2f4687 --- /dev/null +++ b/litho.docs/4.Deep-Exploration/Configuration Domain.md @@ -0,0 +1,289 @@ +# Technical Documentation: Configuration Domain in Cortex-Mem + +**Generation Time:** 2026-01-04 09:39:29 (UTC) +**Document Version:** 1.0 +**System:** Cortex-Mem – Cognitive Memory System for AI Agents + +--- + +## 1. Overview + +The **Configuration Domain** in the **Cortex-Mem** system is responsible for managing, loading, validating, and providing access to configuration settings across all components of the platform. It ensures consistent behavior across interfaces (CLI, HTTP API, MCP, Web Dashboard) by centralizing configuration logic and enabling environment-specific tuning through TOML-based configuration files. + +This domain plays a foundational role during system initialization, serving as the source of truth for subsystem parameters including: +- Qdrant vector database connectivity +- OpenAI LLM integration +- Embedding service settings +- Memory management policies +- Logging and server configurations + +It supports both file-based (`config.toml`) and programmatic configuration, with fallback mechanisms and default values to reduce deployment complexity. + +--- + +## 2. Key Components + +### 2.1 Central Configuration Loader + +#### Module: `cortex-mem-config/src/lib.rs` + +The **Central Configuration Loader** is the core component of the Configuration Domain. Implemented in Rust using `serde` for serialization and `anyhow` for error handling, it parses structured configuration from TOML files into a unified `Config` struct. + +#### Core Features: +- **TOML-based parsing**: Uses `toml::from_str()` to deserialize configuration. +- **Hierarchical structure**: Aggregates multiple subsystem configurations into one root object. +- **Error resilience**: Returns descriptive errors via `anyhow::Result`. +- **Extensibility**: New subsystems can be added without modifying loader logic. + +#### Data Model (`Config` Struct): +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub qdrant: QdrantConfig, + pub llm: LLMConfig, + pub server: ServerConfig, + pub embedding: EmbeddingConfig, + pub memory: MemoryConfig, + pub logging: LoggingConfig, +} +``` + +Each nested struct corresponds to a subsystem: + +| Subsystem | Purpose | +|---------|--------| +| `QdrantConfig` | Vector store connection URL, collection name, timeout, optional embedding dimension | +| `LLMConfig` | OpenAI API base URL, key, model selection, temperature, token limits | +| `ServerConfig` | Host/IP and port for HTTP/MCP services, CORS policy | +| `EmbeddingConfig` | Embedding model endpoint, batch size, request timeout | +| `MemoryConfig` | Memory retention rules, deduplication thresholds, search limits | +| `LoggingConfig` | Log output directory, verbosity level, enable/disable flag | + +#### Default Implementations: +To simplify deployment, two structs implement `Default`: + +##### `impl Default for MemoryConfig` +```rust +max_memories: 10000, +similarity_threshold: 0.65, +max_search_results: 50, +auto_summary_threshold: 32768, +auto_enhance: true, +deduplicate: true, +merge_threshold: 0.75, +search_similarity_threshold: Some(0.70), +``` + +##### `impl Default for LoggingConfig` +```rust +enabled: false, +log_directory: "logs".to_string(), +level: "info".to_string(), +``` + +These defaults allow minimal configuration while supporting advanced tuning when needed. + +#### Loading Workflow: +```mermaid +graph TD + A[Start] --> B[Read TOML file path] + B --> C{File exists?} + C -- No --> D[Return Error] + C -- Yes --> E[Read file content as string] + E --> F[Parse with toml::from_str] + F --> G{Parse successful?} + G -- No --> H[Return Error] + G -- Yes --> I[Return Config struct] + I --> J[End] +``` + +#### Sequence Diagram: +```mermaid +sequenceDiagram + participant Client + participant Loader + participant ConfigFile + + Client->>Loader: load("config.toml") + Loader->>ConfigFile: read_to_string() + ConfigFile-->>Loader: file content (TOML) + Loader->>Loader: toml::from_str::() + alt Parsing succeeds + Loader-->>Client: Ok(Config) + else Parsing fails + Loader-->>Client: Err(anyhow::Error) + end +``` + +--- + +### 2.2 Configuration Validator + +#### Module: `examples/lomoco-evaluation/src/cortex_mem/config_utils.py` + +A Python utility used primarily in evaluation scripts to validate the presence and correctness of required fields before running benchmarks. + +#### Key Functions: +- `validate_config(config_path)` + Checks that: + - File exists + - Required sections exist: `[llm]`, `[embedding]`, `[qdrant]`, `[memory]` + - Critical keys are present within each section + +- `get_config_value(config_path, section, key, default=None)` + Safely retrieves individual configuration values with fallback support. + +- `check_openai_config(config_path)` + Validates that both LLM and embedding services have valid API keys and endpoints set. + +#### Use Case: +Used in automated testing pipelines to prevent execution with incomplete or misconfigured setups. + +--- + +### 2.3 Agent Profile Manager + +#### Module: `examples/cortex-mem-tars/src/config.rs` + +Manages persistent storage of agent profiles (bots), implemented as a separate configuration manager that integrates with the main `cortex-mem-config`. + +#### Key Structures: +- `BotConfig`: Stores metadata about an AI agent (ID, name, system prompt, password, creation timestamp). +- `ConfigManager`: Handles persistence of bots in `bots.json` and manages lifecycle operations. + +#### Integration with Central Config: +The `ConfigManager` loads the global `Config` from `cortex-mem-config`, applying fallback logic: +1. First attempts to load `config.toml` from current working directory +2. Falls back to system-wide config directory (e.g., `~/.config/cortex/mem-tars/`) +3. If no config exists, creates a default one with sensible values (e.g., local Qdrant at `http://localhost:6334`, OpenAI defaults) + +#### Example Default Values: +```toml +[qdrant] +url = "http://localhost:6334" +collection_name = "cortex_mem" +embedding_dim = 1536 +timeout_secs = 30 + +[llm] +api_base_url = "https://api.openai.com/v1" +api_key = "" +model_efficient = "gpt-4o-mini" +temperature = 0.7 +max_tokens = 2000 + +[embedding] +api_base_url = "https://api.openai.com/v1" +model_name = "text-embedding-3-small" +api_key = "" +batch_size = 100 +timeout_secs = 30 +``` + +When created, this default config is written to disk using `toml::to_string_pretty()` for readability. + +--- + +## 3. Interaction with Other Domains + +### 3.1 Dependency Flow +The Configuration Domain provides essential setup data to other domains during initialization: + +| From → To | Interaction Type | Description | +|----------|------------------|-------------| +| **Configuration Domain → Core Memory Domain** | Configuration Injection | Supplies validated `Config` to initialize LLM client, Qdrant adapter, and memory policies | +| **Configuration Domain → Tool Support Domain** | Shared Initialization | CLI, HTTP server, MCP handler, and dashboard use config to connect to core services | +| **Tool Support Domain → Configuration Domain** | Validation & Access | Evaluation tools and CLI commands independently validate or read config files | + +### 3.2 Initialization Process +As part of the **Configuration and Initialization Process**, the workflow proceeds as follows: + +```mermaid +graph TD + A[System Startup] --> B[Load Config from TOML Files] + B --> C[Validate Required Fields] + C --> D[Initialize LLM Client] + D --> E[Initialize Qdrant Vector Store] + E --> F[Create MemoryManager Instance] + F --> G[System Ready for Operations] +``` + +All interface layers (CLI, HTTP, MCP) invoke this process on startup to ensure alignment with the same configuration state. + +--- + +## 4. Practical Implementation Details + +### 4.1 File Location and Search Order +Cortex-Mem searches for `config.toml` in the following order: +1. Current working directory +2. System configuration directory (via `directories` crate): + - Linux: `~/.config/cortex/mem-tars/` + - macOS: `~/Library/Application Support/com.cortex.mem-tars/` + - Windows: `%APPDATA%\com.cortex\mem-tars\` + +This allows per-project overrides while maintaining user-level defaults. + +### 4.2 Environment-Specific Configuration +While not explicitly shown in current code, the architecture supports: +- Multiple config profiles (e.g., `config.dev.toml`, `config.prod.toml`) +- Environment variable overrides (planned enhancement) +- Programmatic configuration injection (used in test suites) + +### 4.3 Extensibility Guidelines +New subsystems can integrate with the configuration system by: +1. Defining a new config struct with `Serialize + Deserialize` traits +2. Adding it as a field in the root `Config` struct +3. Optionally implementing `Default` if fallback values are appropriate +4. Updating documentation and validation utilities accordingly + +Example: +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheConfig { + pub enabled: bool, + pub ttl_minutes: u32, + pub max_entries: usize, +} + +// In Config: +pub cache: CacheConfig, +``` + +--- + +## 5. Best Practices and Recommendations + +### ✅ Recommended Usage Patterns +- **Always validate config early**: Use `validate_config()` in entry points to fail fast. +- **Use defaults wisely**: Leverage `Default` impls to reduce boilerplate in development. +- **Secure sensitive data**: Never commit API keys; rely on runtime input or secrets management. +- **Version control structure only**: Commit sample or schema files (e.g., `config.example.toml`), not actual keys. + +### ⚠️ Known Limitations +- No support for environment variables yet (hardcoded file paths only) +- Limited dynamic reloading (config is loaded once at startup) +- Python validator duplicates some logic already handled in Rust (potential refactoring opportunity) + +### 🔧 Suggested Improvements +1. **Add `.env` file support** for overriding TOML values +2. **Implement hot-reload capability** for long-running services +3. **Unify validation logic** in Rust and expose via FFI or CLI tool +4. **Support JSON/YAML formats** alongside TOML for broader compatibility + +--- + +## 6. Summary + +The **Configuration Domain** in Cortex-Mem provides a robust, type-safe foundation for managing system-wide settings. Built around a centralized TOML loader in Rust, it enables reliable initialization of all components and promotes consistency across deployment environments. + +Its modular design supports extensibility, with clear separation between: +- **Loading** (`cortex-mem-config`) +- **Validation** (Python/Rust utilities) +- **Application-specific extensions** (Agent Profile Manager) + +By combining strong defaults with flexible override mechanisms, the domain balances ease of use with production-grade configurability—making it a critical enabler of Cortex-Mem’s adaptability across diverse AI agent deployments. + +--- + +*End of Document* \ No newline at end of file diff --git a/litho.docs/4.Deep-Exploration/Core Memory Domain.md b/litho.docs/4.Deep-Exploration/Core Memory Domain.md new file mode 100644 index 0000000..1b0c8c7 --- /dev/null +++ b/litho.docs/4.Deep-Exploration/Core Memory Domain.md @@ -0,0 +1,391 @@ +# Technical Documentation: Core Memory Domain + +**Generation Time:** 2024-07-15T18:36:16.000Z +**Timestamp:** 1721068576 + +--- + +## 1. Overview + +The **Core Memory Domain** is the central intelligence engine of the `cortex-mem` system, responsible for managing the entire lifecycle of AI agent memories. It enables persistent, context-aware interactions by storing, retrieving, optimizing, and analyzing conversational and factual data using vector embeddings and Large Language Models (LLMs). + +This domain orchestrates all memory operations—including creation, retrieval, update, deletion, deduplication, classification, and importance evaluation—through a modular architecture that integrates with external services like Qdrant (vector database) and OpenAI (LLM provider). It serves as the foundational layer upon which all other interfaces (CLI, HTTP API, MCP, Web Dashboard) are built. + +### Key Characteristics +- **Primary Responsibility**: Persistent memory management for AI agents. +- **Core Value Proposition**: Enables long-term context retention, reduces redundant interactions, improves response accuracy, and supports personalized user experiences. +- **Integration Hub**: Acts as the central point connecting LLMs, vector storage, and application interfaces. +- **Optimization Focus**: Automatically enhances memory quality through deduplication, merging, and metadata enrichment. + +--- + +## 2. Architecture and Components + +The Core Memory Domain follows a dependency-injection-based, modular design in Rust, ensuring high cohesion and low coupling between components. The primary architectural pattern revolves around a central `MemoryManager` that coordinates specialized submodules via well-defined traits. + +### High-Level Component Diagram +```mermaid +graph TD + A[Memory Manager] --> B[Vector Store Adapter] + A --> C[LLM Client] + A --> D[Fact Extractor] + A --> E[Memory Updater] + A --> F[Importance Evaluator] + A --> G[Duplicate Detector] + A --> H[Memory Classifier] + B --> I[Qdrant Vector DB] + C --> J[OpenAI LLM API] +``` + +### Submodules + +| Module | Purpose | Key Functions | +|-------|--------|---------------| +| **Memory Manager** | Central orchestrator of all memory operations | `create_memory`, `search`, `add_memory`, `update`, `delete` | +| **LLM Client** | Unified interface to OpenAI services | `embed`, `complete`, `extract_structured_facts`, `summarize` | +| **Vector Store Adapter** | Abstracts interaction with Qdrant | `insert`, `search_with_threshold`, `get`, `list`, `delete` | +| **Fact Extractor** | Parses conversation messages into structured facts | `extract_facts`, `extract_user_facts`, `extract_assistant_facts` | +| **Memory Updater** | Determines create/update/merge/delete actions based on LLM analysis | `update_memories`, `should_merge`, `merge_memories` | +| **Importance Evaluator** | Scores memory relevance using LLM-driven analysis | `evaluate_importance` | +| **Duplicate Detector** | Identifies and merges redundant memories | `detect_duplicates`, `merge_memories` | +| **Memory Classifier** | Categorizes memory content into types (e.g., Conversational, Procedural) | `classify_memory`, `extract_entities`, `extract_topics` | + +All modules are designed to be replaceable via trait objects (`dyn Trait`), enabling future extensibility (e.g., swapping LLM providers or vector databases). + +--- + +## 3. Data Model + +The core data structures are defined in `cortex-mem-core/src/types.rs`. These types ensure consistency across serialization boundaries and support rich metadata filtering. + +### Primary Structures + +#### `Memory` +Represents a single stored memory entry. + +```rust +pub struct Memory { + pub id: String, + pub content: String, + pub embedding: Vec, + pub metadata: MemoryMetadata, + pub created_at: DateTime, + pub updated_at: DateTime, +} +``` + +#### `MemoryMetadata` +Contains semantic and operational metadata for filtering and categorization. + +```rust +pub struct MemoryMetadata { + pub user_id: Option, + pub agent_id: Option, + pub run_id: Option, + pub actor_id: Option, + pub role: Option, + pub memory_type: MemoryType, + pub hash: String, + pub importance_score: f32, + pub entities: Vec, + pub topics: Vec, + pub custom: HashMap, +} +``` + +#### `MemoryType` (Enum) +Categorizes memory content for targeted retrieval. + +```rust +pub enum MemoryType { + Conversational, + Procedural, + Factual, + Semantic, + Episodic, + Personal, +} +``` + +> **Note**: Default type is `Conversational` for unrecognized inputs. + +#### `ScoredMemory` +Used in search results to include similarity scores. + +```rust +pub struct ScoredMemory { + pub memory: Memory, + pub score: f32, +} +``` + +#### `Filters` +Enables complex queries during retrieval. + +```rust +pub struct Filters { + pub user_id: Option, + pub agent_id: Option, + pub memory_type: Option, + pub min_importance: Option, + pub max_importance: Option, + // ... additional fields +} +``` + +--- + +## 4. Core Workflows + +### 4.1 Memory Addition Process + +This workflow handles both direct content ingestion and conversation-based fact extraction. + +#### Sequence Flow +```mermaid +sequenceDiagram + participant User + participant MemoryManager + participant FactExtractor + participant LLMClient + participant VectorStore + participant MemoryUpdater + + User->>MemoryManager: add_memory(messages, metadata) + MemoryManager->>FactExtractor: extract_facts(messages) + FactExtractor->>LLMClient: extract_structured_facts(prompt) + LLMClient-->>FactExtractor: StructuredFactExtraction + FactExtractor-->>MemoryManager: [ExtractedFact1, ExtractedFact2] + + loop For each fact + MemoryManager->>VectorStore: search(query_embedding, filters, 5) + VectorStore-->>MemoryManager: [ScoredMemory1, ScoredMemory2] + MemoryManager->>MemoryUpdater: update_memories(fact, existing_memories, metadata) + MemoryUpdater->>LLMClient: complete(update_prompt) + LLMClient-->>MemoryUpdater: JSON decision + MemoryUpdater-->>MemoryManager: UpdateResult + end + + alt Create + MemoryManager->>LLMClient: embed(content) + LLMClient-->>MemoryManager: embedding + MemoryManager->>VectorStore: insert(memory) + else Update/Merge/Delete + MemoryManager->>VectorStore: update/delete + end + + MemoryManager-->>User: [MemoryResult] +``` + +#### Key Implementation Details +- **Content Hashing**: SHA-256 used to detect exact duplicates. +- **Fallback Extraction**: If no facts are extracted from full conversation: + 1. Try extracting only from user messages. + 2. Fall back to individual message processing. + 3. As last resort, store concatenated user messages. +- **Auto-Enhancement**: When enabled (`auto_enhance = true`), new memories are enriched with: + - Keywords + - Summary (if content > threshold) + - Entities & Topics + - Importance Score + - Memory Type Classification + +--- + +### 4.2 Memory Retrieval Process + +Semantic search powered by vector similarity and metadata filtering. + +#### Workflow +```mermaid +graph TD + A[Search Query] --> B[Generate Embedding via LLM] + B --> C[Semantic Search in Qdrant] + C --> D[Filter by Metadata & Threshold] + D --> E[Rank by Combined Score] + E --> F[Return Results] +``` + +#### Ranking Strategy +Results are ranked using a weighted combination: + +``` +combined_score = (similarity_score × 0.7) + (importance_score × 0.3) +``` + +This ensures semantically relevant but unimportant memories do not dominate results. + +#### Similarity Threshold +Configurable via `search_similarity_threshold`. Two modes supported: +- **Database-level filtering**: Passed directly to Qdrant. +- **Application-level filtering**: Post-process results after retrieval. + +Default behavior uses config-defined threshold. + +--- + +### 4.3 Memory Optimization Process + +Automated maintenance to improve memory quality and reduce redundancy. + +#### Steps +1. Analyze corpus using LLM-guided detectors. +2. Generate optimization plan (merge, update, delete). +3. Execute changes safely. +4. Report metrics (space saved, duplicates removed). + +#### Deduplication Logic +Uses dual-layer detection: +1. **Exact Match**: Content hashing. +2. **Semantic Similarity**: Cosine similarity ≥ `merge_threshold`. + +When merging, an LLM generates consolidated content preserving key information. + +--- + +## 5. Integration Points + +### 5.1 With External Systems + +| System | Role | Interaction Type | +|-------|------|------------------| +| **Qdrant Vector Database** | Primary storage for embeddings and metadata | gRPC/HTTP calls via `qdrant-client` crate | +| **OpenAI LLM Service** | Embedding generation, summarization, classification, reasoning | REST API calls via `rig` framework | +| **LangMem** | Benchmark baseline for evaluation | Side-by-side testing in `lomoco-evaluation` suite | + +### 5.2 Internal Dependencies + +| From | To | Relation | +|------|----|---------| +| Tool Support Domain (CLI, HTTP, MCP) | Core Memory Domain | Direct method calls to `MemoryManager` | +| Configuration Domain | Core Memory Domain | Injected at initialization (`MemoryConfig`) | +| Core Memory Domain | Tool Support Domain | Uses shared utilities (error types, serializers) | + +--- + +## 6. Configuration and Initialization + +The Core Memory Domain is initialized with dependencies injected from the Configuration Domain. + +### Initialization Flow +```mermaid +graph TD + A[System Startup] --> B[Load Config from TOML] + B --> C[Validate Required Fields] + C --> D[Initialize LLM Client] + D --> E[Initialize Qdrant Vector Store] + E --> F[Create MemoryManager Instance] + F --> G[System Ready] +``` + +### Key Configurable Parameters +| Parameter | Description | Example | +|---------|-----------|--------| +| `embedding_dim` | Dimensionality of embeddings | 1536 (text-embedding-ada-002) | +| `deduplicate` | Enable/disable duplicate detection | true | +| `auto_enhance` | Auto-generate metadata (summary, keywords, etc.) | true | +| `auto_summary_threshold` | Minimum length to trigger auto-summary | 500 characters | +| `similarity_threshold` | Minimum cosine similarity for relatedness | 0.75 | +| `merge_threshold` | Threshold for automatic merge decisions | 0.85 | +| `search_similarity_threshold` | Filter threshold for search results | 0.70 | + +> **Auto-detection**: If `embedding_dim` is not set, it's inferred by generating a test embedding. + +--- + +## 7. Error Handling and Resilience + +The system implements robust error handling strategies: + +- **LLM Failures**: Graceful fallbacks; empty or default values where appropriate. +- **Empty Content Protection**: Prevents storage of blank memories. +- **UUID Mapping for Hallucinations**: Handles cases where LLM refers to non-existent memory IDs by mapping hypothetical indices to real UUIDs. +- **Code Block Sanitization**: Removes Markdown code blocks before parsing JSON responses from LLMs. +- **JSON Extraction Robustness**: Multiple strategies to extract valid JSON from malformed LLM outputs. + +Example resilience mechanism in `MemoryUpdater::parse_update_decisions()`: +```rust +// Attempt direct parse +match serde_json::from_str(json_str) { + Ok(decisions) => return Ok(decisions), + Err(_) => { + // Try extracting JSON from code blocks + let extracted = self.extract_json_from_response(&cleaned_response)?; + match serde_json::from_str::>(&extracted) { + Ok(decisions) => return Ok(decisions), + Err(_) => return Ok(vec![]), // Silent fail with empty list + } + } +} +``` + +--- + +## 8. Performance and Scalability Considerations + +### Efficiency Features +- **Batch Operations**: Support for batch embedding and upserting. +- **Caching Layer**: Not currently implemented; potential area for improvement. +- **Asynchronous Design**: All critical paths use async/await for non-blocking I/O. +- **Selective Enhancement**: Only enhance long or important memories. + +### Bottlenecks +- **LLM Latency**: Each fact extraction and decision requires an LLM call. +- **Multiple Round Trips**: Add operation may involve multiple LLM interactions per fact. + +### Recommendations +- Implement local caching of frequent queries. +- Use streaming responses where possible. +- Introduce rate-limiting and retry logic for external APIs. + +--- + +## 9. Practical Usage Examples + +### Creating a Memory +```rust +let metadata = MemoryMetadata::new(MemoryType::Factual) + .with_user_id("user_123".to_string()) + .with_importance_score(0.9); + +let memory_id = memory_manager.store("I love hiking in the mountains".to_string(), metadata).await?; +``` + +### Searching Memories +```rust +let filters = Filters::for_user("user_123") + .with_memory_type(MemoryType::Personal); + +let results = memory_manager.search("outdoor activities", &filters, 5).await?; +``` + +### Adding Conversation-Based Memory +```rust +let messages = vec![ + Message::user("I'm planning a trip to Japan next spring."), + Message::assistant("That sounds exciting! What cities are you visiting?"), +]; + +let metadata = MemoryMetadata::new(MemoryType::Conversational); +let results = memory_manager.add_memory(&messages, metadata).await?; +``` + +--- + +## 10. Conclusion + +The **Core Memory Domain** is a sophisticated, production-ready module that provides AI agents with human-like memory capabilities. Its strength lies in its intelligent orchestration of LLM-powered reasoning and vector-based retrieval, combined with automated optimization to maintain high-quality memory stores over time. + +It successfully addresses key challenges in AI agent development: +- **Context Persistence**: Across sessions and conversations. +- **Information Density**: Avoiding duplication while preserving meaning. +- **Personalization**: Enabling tailored responses based on past interactions. +- **Operational Visibility**: Through analytics and monitoring. + +While highly functional, opportunities exist for further enhancement: +- **Caching Layer**: To reduce LLM call frequency. +- **Multi-modal Support**: Extending beyond text (e.g., images, audio). +- **Cross-user Knowledge Graphs**: Securely linking related memories across users. +- **Temporal Reasoning**: Better handling of time-sensitive memories. + +For developers integrating this system, the recommended approach is to use the HTTP API or MCP interface unless low-level control is required, leveraging the CLI for testing and bulk operations. \ No newline at end of file diff --git a/litho.docs/4.Deep-Exploration/Tool Support Domain.md b/litho.docs/4.Deep-Exploration/Tool Support Domain.md new file mode 100644 index 0000000..21a2dc2 --- /dev/null +++ b/litho.docs/4.Deep-Exploration/Tool Support Domain.md @@ -0,0 +1,602 @@ +# Technical Documentation: Tool Support Domain in Cortex-Mem + +## Overview + +The **Tool Support Domain** is a critical component of the Cortex-Mem system, serving as the primary interface layer between users, external systems, and the core memory engine. This domain provides supporting utilities, interfaces, and integration layers that enable interaction with the cognitive memory system through multiple channels including command-line tools, HTTP APIs, agent protocols, web dashboards, and evaluation frameworks. + +Based on comprehensive analysis of the system architecture, business flows, and codebase exploration, this documentation details the technical implementation, components, interactions, and practical usage patterns of the Tool Support Domain. + +--- + +## Architecture and Components + +### Component Structure + +The Tool Support Domain comprises six major sub-modules, each implementing specific interface types: + +| Sub-module | Primary Function | Key Technologies | +|-----------|------------------|------------------| +| CLI Interface | Direct memory management via terminal commands | Rust, Clap (CLI parsing) | +| HTTP API Server | RESTful service exposing CRUD operations | Rust, Axum (web framework) | +| MCP Protocol Handler | AI agent integration via Memory Control Protocol | Rust, rmcp framework | +| Web Dashboard | Real-time monitoring and management UI | Svelte, Elysia, TypeScript | +| Evaluation Framework | Benchmarking against external systems | Python, pytest | +| Frontend API Client | Secure communication between dashboard and API | TypeScript | + +These components are distributed across multiple repositories: +- `cortex-mem-cli/` - Command-line interface +- `cortex-mem-service/` - HTTP API server +- `cortex-mem-mcp/` - MCP protocol handler +- `cortex-mem-insights/` - Web dashboard and frontend client +- `examples/lomoco-evaluation/` - Evaluation framework + +### Data Flow Architecture + +```mermaid +graph TD + A[User] --> B[CLI Interface] + A --> C[Web Dashboard] + D[AI Agent] --> E[MCP Protocol Handler] + F[External System] --> G[HTTP API Server] + + B --> H[MemoryManager] + C --> I[Frontend API Client] + I --> G + E --> J[MemoryOperations] + J --> H + G --> H + + H --> K[Vector Store] + H --> L[LLM Client] + + M[Evaluation Framework] --> B + M --> G + C --> N[Optimization Engine] + N --> H +``` + +All interface components ultimately delegate to the `MemoryManager` in the Core Memory Domain, ensuring consistent behavior across different access methods. + +--- + +## Detailed Component Analysis + +### 1. CLI Interface (`cortex-mem-cli`) + +The CLI provides direct terminal-based access to memory operations with rich user feedback. + +#### Key Features: +- **Commands**: add, search, list, delete, optimize +- **Input Parsing**: Supports conversation format detection and metadata extraction +- **Error Handling**: Comprehensive logging with success/failure status reporting +- **Configuration**: Auto-detects config.toml in standard locations + +#### Implementation Details: +```rust +// Example from cortex-mem-cli/src/commands/add.rs +pub async fn execute( + &self, + content: String, + user_id: Option, + agent_id: Option, + memory_type: String, +) -> Result<(), Box> { + // Parse metadata and determine if input is a conversation + let is_conversation = memory_type == MemoryType::Procedural + || content.contains('\n') + || content.contains("Assistant:") + || content.contains("User:"); + + if is_conversation { + // Handle as conversation for advanced processing + match self.memory_manager.add_memory(&messages, metadata).await { + Ok(results) => { + info!("Memory added successfully with {} actions", results.len()); + println!("✅ Memory added successfully!"); + // Display detailed results + } + Err(e) => { + error!("Failed to add memory: {}", e); + println!("❌ Failed to add memory: {}", e); + return Err(e.into()); + } + } + } else { + // Handle as simple content storage + match self.memory_manager.store(content.clone(), metadata).await { + Ok(memory_id) => { + info!("Memory stored successfully with ID: {}", memory_id); + println!("✅ Memory added successfully!"); + println!("ID: {}", memory_id); + } + Err(e) => { + error!("Failed to store memory: {}", e); + println!("❌ Failed to add memory: {}", e); + return Err(e.into()); + } + } + } +} +``` + +### 2. HTTP API Server (`cortex-mem-service`) + +Provides RESTful endpoints for external system integration using Axum framework. + +#### Endpoints: +- `POST /api/memories` - Create new memory +- `GET /api/memories` - List memories with filtering +- `POST /api/memories/search` - Semantic search +- `PUT /api/memories/{id}` - Update memory +- `DELETE /api/memories/{id}` - Delete memory +- `POST /api/optimization` - Start optimization job +- `GET /health` - Health check endpoint + +#### Request Handling Pattern: +```rust +// From cortex-mem-service/src/handlers.rs +pub async fn create_memory( + State(state): State, + Json(request): Json, +) -> Result, (StatusCode, Json)> { + let memory_type = MemoryType::parse(request.memory_type.as_deref().unwrap_or("conversational")); + + let mut metadata = MemoryMetadata::new(memory_type.clone()); + + if let Some(user_id) = &request.user_id { + metadata = metadata.with_user_id(user_id.clone()); + } + + // Check if this should be handled as a conversation + let is_conversation = memory_type == MemoryType::Procedural + || request.content.contains('\n') + || request.content.contains("Assistant:") + || request.content.contains("User:"); + + if is_conversation { + match state.memory_manager.add_memory(&messages, metadata).await { + Ok(results) => { + info!("Memory created successfully with {} actions", results.len()); + Ok(Json(SuccessResponse { + message: format!("Memory created successfully with {} actions", results.len()), + id: Some(primary_id), + })) + } + Err(e) => { + error!("Failed to create memory: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { + error: format!("Failed to create memory: {}", e), + code: "MEMORY_CREATION_FAILED".to_string(), + }))) + } + } + } else { + match state.memory_manager.store(request.content, metadata).await { + Ok(memory_id) => { + info!("Memory created with ID: {}", memory_id); + Ok(Json(SuccessResponse { + message: "Memory created successfully".to_string(), + id: Some(memory_id), + })) + } + Err(e) => { + error!("Failed to create memory: {}", e); + Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { + error: format!("Failed to create memory: {}", e), + code: "MEMORY_CREATION_FAILED".to_string(), + }))) + } + } + } +} +``` + +### 3. MCP Protocol Handler (`cortex-mem-mcp`) + +Enables AI agents to interact with memory capabilities through standardized tool calls. + +#### Key Functions: +- Registers memory tools with Rig framework +- Translates domain errors into standardized MCP ErrorData +- Returns structured responses via MCP stdio transport + +#### Implementation: +```rust +// From cortex-mem-mcp/src/lib.rs +impl MemoryMcpService { + pub async fn store_memory( + &self, + arguments: &Map, + ) -> Result { + let payload = map_mcp_arguments_to_payload(arguments, &self.agent_id); + + match self.operations.store_memory(payload).await { + Ok(response) => { + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&response).unwrap(), + )])) + } + Err(e) => { + error!("Failed to store memory: {}", e); + Err(self.tools_error_to_mcp_error(e)) + } + } + } + + pub async fn query_memory( + &self, + arguments: &Map, + ) -> Result { + let payload = map_mcp_arguments_to_payload(arguments, &self.agent_id); + + match self.operations.query_memory(payload).await { + Ok(response) => { + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&response).unwrap(), + )])) + } + Err(e) => { + error!("Failed to query memories: {}", e); + Err(self.tools_error_to_mcp_error(e)) + } + } + } +} +``` + +### 4. Web Dashboard (`cortex-mem-insights`) + +A full-stack Svelte + Elysia application providing real-time monitoring and management capabilities. + +#### Architecture Layers: +- **Backend**: Elysia server with TypeScript API routes +- **Frontend**: Svelte components with reactive stores +- **API Client**: Type-safe TypeScript client for backend communication +- **Stores**: Reactive state management using writable stores + +#### Service Status Monitoring: +```typescript +// From cortex-mem-insights/src/lib/components/ServiceStatus.svelte +async function detectIndividualServices(timestamp: string) { + const mainService: ServiceStatus = { status: 'detecting', latency: 0, lastCheck: timestamp }; + const vectorStore: ServiceStatus = { status: 'detecting', latency: 0, lastCheck: timestamp }; + const llmService: ServiceStatus = { status: 'detecting', latency: 0, lastCheck: timestamp }; + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时 + + try { + const serviceResponse = await fetch('/api/system/status', { + signal: controller.signal + }); + clearTimeout(timeoutId); + const serviceLatency = Date.now() - serviceStartTime; + + if (serviceResponse.ok) { + const responseData = await serviceResponse.json(); + if (responseData.success && responseData.data) { + const cortexMemStatus = responseData.data.cortex_mem_service; + if (cortexMemStatus === true) { + mainService.status = 'connected'; + mainService.latency = serviceLatency; + cortexMemServiceAvailable = true; + } else { + mainService.status = 'disconnected'; + } + } + } + } catch (fetchErr) { + clearTimeout(timeoutId); + if (fetchErr.name === 'AbortError') { + console.warn('cortex-mem-service状态检查超时'); + mainService.status = 'disconnected'; + } + } + } +} +``` + +#### State Management Stores: +```typescript +// From cortex-mem-insights/src/lib/stores/system.ts +function createSystemStore() { + const { subscribe, set, update } = writable(initialState); + + return { + subscribe, + + loadStatus: async () => { + update(state => ({ ...state, loading: true, error: null })); + + try { + const response = await systemApi.status(); + update(state => ({ + ...state, + status: response.data, + loading: false, + lastUpdated: new Date().toISOString(), + })); + } catch (error) { + update(state => ({ + ...state, + loading: false, + error: error instanceof Error ? error.message : 'Failed to load system status', + })); + } + }, + + refreshAll: async () => { + update(state => ({ ...state, loading: true, error: null })); + + try { + const [status, metrics, info, logs] = await Promise.all([ + systemApi.status(), + systemApi.metrics(), + systemApi.info(), + systemApi.logs({ limit: 50 }), + ]); + + update(state => ({ + ...state, + status: status.data, + metrics: metrics.data, + info: info.data, + logs: logs.data, + loading: false, + lastUpdated: new Date().toISOString(), + })); + } catch (error) { + update(state => ({ + ...state, + loading: false, + error: error instanceof Error ? error.message : 'Failed to refresh system data', + })); + } + }, + }; +} + +export const systemStore = createSystemStore(); +``` + +### 5. Evaluation Framework (`examples/lomoco-evaluation`) + +Python-based benchmarking tools for comparing Cortex-Mem against external systems like LangMem. + +#### Key Features: +- Automated ingestion pipelines +- Comparative performance testing +- Retrieval accuracy measurement +- Batch operation support +- Retry logic with exponential backoff + +#### Implementation: +```python +# From examples/lomoco-evaluation/src/cortex_mem/add.py +class CortexMemAdd: + def _run_cortex_mem_cli(self, args, max_retries=3): + """Run cortex-mem-cli command with retry logic""" + for attempt in range(max_retries): + try: + # Use absolute path for config file to avoid path resolution issues + config_path = os.path.abspath(self.config_path) + + cmd = ["cargo", "run", "-p", "cortex-mem-cli", "--quiet", "--"] + cmd.extend(["--config", config_path]) + cmd.extend(args) + + # Use UTF-8 encoding to avoid GBK codec errors on Windows + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8', + timeout=60, + cwd=str(project_root) + ) + + if result.returncode != 0: + if attempt < max_retries - 1: + logger.warning(f"CLI command failed (attempt {attempt+1}/{max_retries}): {result.stderr}") + time.sleep(2 ** attempt) # Exponential backoff + continue + else: + logger.error(f"CLI command failed after {max_retries} attempts: {result.stderr}") + + return result.returncode == 0, result.stdout, result.stderr + + except subprocess.TimeoutExpired: + logger.warning(f"CLI command timed out (attempt {attempt+1}/{max_retries})") + if attempt < max_retries - 1: + time.sleep(2 ** attempt) + continue + return False, "", "Command timed out" +``` + +### 6. Frontend API Client (`cortex-mem-insights/src/lib/api/client.ts`) + +Type-safe TypeScript client for secure communication between the web dashboard and backend services. + +#### Implementation: +```typescript +// From cortex-mem-insights/src/lib/api/client.ts +const API_BASE_URL = import.meta.env.VITE_API_URL || ''; + +async function request( + endpoint: string, + options: RequestInit = {} +): Promise { + const url = `${API_BASE_URL}${endpoint}`; + + const defaultOptions: RequestInit = { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + credentials: 'include', + }; + + try { + const response = await fetch(url, { ...defaultOptions, ...options }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.error?.message || + errorData.message || + `HTTP ${response.status}: ${response.statusText}` + ); + } + + return await response.json(); + } catch (error) { + console.error(`API request failed: ${endpoint}`, error); + throw error; + } +} + +// Memory related API +export const memoryApi = { + list: (params?: { + user_id?: string; + agent_id?: string; + run_id?: string; + actor_id?: string; + memory_type?: string; + limit?: number; + page?: number; + }) => { + const queryParams = new URLSearchParams(); + if (params?.user_id) queryParams.append('user_id', params.user_id); + if (params?.agent_id) queryParams.append('agent_id', params.agent_id); + // ... other parameters + return request(`/api/memories${queryParams.toString() ? `?${queryParams}` : ''}`); + }, + + search: (query: string, params?: { + user_id?: string; + agent_id?: string; + run_id?: string; + actor_id?: string; + memory_type?: string; + limit?: number; + similarity_threshold?: number; + }) => { + return request('/api/memories/search', { + method: 'POST', + body: JSON.stringify({ query, ...params }), + }); + }, +}; +``` + +--- + +## Integration Patterns + +### Configuration Loading +All components follow a consistent configuration loading pattern with fallback paths: +1. Current directory +2. User home directory (`~/.config/memo/config.toml`) +3. System config directory (`/etc/memo/config.toml` on Linux, `/usr/local/etc/memo/config.toml` on macOS, `C:\ProgramData\memo\config.toml` on Windows) + +### Error Translation +The domain implements standardized error handling: +- Domain-specific errors are translated to appropriate response formats +- MCP protocol uses standardized ErrorData structure +- HTTP API returns structured ErrorResponse objects +- CLI provides human-readable error messages + +### Authentication and Security +- Uses API keys for OpenAI and Qdrant access +- Implements credential inclusion in HTTP requests +- Validates required configuration fields at startup +- Provides secure defaults where possible + +--- + +## Usage Scenarios + +### For AI Agent Developers +Use the MCP interface to integrate memory capabilities into autonomous agents: + +```python +# Register memory tools with your agent +agent.register_tool(cortex_mem_mcp.get_memory) +agent.register_tool(cortex_mem_mcp.store_memory) +agent.register_tool(cortex_mem_mcp.query_memory) +``` + +### For ML Engineers +Utilize the evaluation framework for benchmarking: + +```bash +# Run comparative evaluation +python -m lomoco_evaluation --baseline langmem --target cortex-mem +``` + +### For System Administrators +Monitor system health through the web dashboard or CLI: + +```bash +# Check system status +curl http://localhost:8080/health + +# View memory statistics +cortex-mem-cli list --user-id alice --memory-type conversational +``` + +### For Application Integration +Integrate via HTTP API: + +```javascript +// Add memory via HTTP API +fetch('/api/memories', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + content: 'Hello world', + user_id: 'alice', + memory_type: 'conversational' + }) +}); +``` + +--- + +## Best Practices + +### Performance Optimization +- Use batch operations when ingesting large datasets +- Implement caching for frequently accessed memories +- Monitor optimization jobs to maintain optimal memory quality +- Configure appropriate similarity thresholds for searches + +### Reliability Considerations +- Implement retry logic with exponential backoff +- Set reasonable timeouts for external service calls +- Monitor service health regularly +- Maintain backup configurations + +### Security Guidelines +- Store API keys securely (environment variables or secure vaults) +- Validate all inputs before processing +- Implement rate limiting for public APIs +- Regularly rotate credentials + +--- + +## Conclusion + +The Tool Support Domain in Cortex-Mem provides a comprehensive suite of interfaces that make the powerful core memory capabilities accessible through various channels. By maintaining a clean separation of concerns and consistent interaction patterns across all components, it enables seamless integration for diverse use cases—from individual developers building AI agents to enterprise teams managing production deployments. + +The modular design allows independent development and deployment of interface components while ensuring they all leverage the same robust core functionality. This approach maximizes flexibility without compromising consistency or reliability. + +For future enhancements, consider adding: +- WebSocket support for real-time updates +- GraphQL interface for flexible querying +- Enhanced authentication mechanisms +- Mobile SDKs for broader platform support + +This documentation provides a complete technical reference for understanding, using, and extending the Tool Support Domain in Cortex-Mem.2026-01-04 09:42:30 (UTC) \ No newline at end of file From eb5df0ac31c24d15d5e1bcc79917eb26054308cb Mon Sep 17 00:00:00 2001 From: Sopaco Date: Sun, 4 Jan 2026 23:06:18 +0800 Subject: [PATCH 04/22] add audio connect to ally audio service --- Cargo.lock | 32 ++++++++- examples/cortex-mem-tars/Cargo.toml | 5 ++ examples/cortex-mem-tars/config.example.toml | 10 ++- examples/cortex-mem-tars/src/app.rs | 62 +++++++++++------ examples/cortex-mem-tars/src/main.rs | 72 +++++++++++++++----- 5 files changed, 143 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d704de0..e899453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,8 @@ dependencies = [ "http", "http-body", "http-body-util", + "hyper", + "hyper-util", "itoa", "matchit 0.7.3", "memchr", @@ -179,10 +181,15 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", + "tokio", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -236,6 +243,7 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -687,7 +695,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tower 0.5.2", - "tower-http", + "tower-http 0.6.6", "tracing", "tracing-subscriber", "uuid", @@ -700,6 +708,7 @@ dependencies = [ "anyhow", "async-stream", "async-trait", + "axum 0.7.9", "chrono", "clap", "clipboard", @@ -720,6 +729,8 @@ dependencies = [ "serde_json", "tokio", "toml", + "tower 0.5.2", + "tower-http 0.5.2", "tracing", "tracing-subscriber", "tui-markdown", @@ -2791,7 +2802,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower 0.5.2", - "tower-http", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", @@ -3831,6 +3842,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.6.6" diff --git a/examples/cortex-mem-tars/Cargo.toml b/examples/cortex-mem-tars/Cargo.toml index 1cdc736..aeae19e 100644 --- a/examples/cortex-mem-tars/Cargo.toml +++ b/examples/cortex-mem-tars/Cargo.toml @@ -50,6 +50,11 @@ async-stream = "0.3" # HTTP client reqwest = { version = "0.12", features = ["json"] } +# HTTP server +axum = { version = "0.7", features = ["json"] } +tower = "0.5" +tower-http = { version = "0.5", features = ["cors", "trace"] } + # Utilities uuid = { version = "1.10", features = ["v4"] } clipboard = "0.5" diff --git a/examples/cortex-mem-tars/config.example.toml b/examples/cortex-mem-tars/config.example.toml index ce1475d..00f08b1 100644 --- a/examples/cortex-mem-tars/config.example.toml +++ b/examples/cortex-mem-tars/config.example.toml @@ -69,4 +69,12 @@ enabled = false # 日志目录 log_directory = "logs" # 日志级别 -level = "info" \ No newline at end of file +level = "info" + +[api] +# TARS API 服务器端口 +port = 8080 +# API 密钥(可选,如果启用认证) +api_key = "ANYTHING_YOU_LIKE" +# 是否启用 CORS +enable_cors = true diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index baf9691..60db281 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -1,4 +1,7 @@ -use crate::agent::{ChatMessage, create_memory_agent, extract_user_basic_info, store_conversations_batch, agent_reply_with_memory_retrieval_streaming}; +use crate::agent::{ + ChatMessage, agent_reply_with_memory_retrieval_streaming, create_memory_agent, + extract_user_basic_info, store_conversations_batch, +}; use crate::config::{BotConfig, ConfigManager}; use crate::infrastructure::Infrastructure; use crate::logger::LogManager; @@ -53,7 +56,11 @@ pub enum AppMessage { impl App { /// 创建新的应用 - pub fn new(config_manager: ConfigManager, log_manager: Arc, infrastructure: Option>) -> Result { + pub fn new( + config_manager: ConfigManager, + log_manager: Arc, + infrastructure: Option>, + ) -> Result { let mut ui = AppUi::new(); // 加载机器人列表 @@ -87,7 +94,9 @@ impl App { infrastructure.config(), infrastructure.memory_manager().clone(), &self.user_id, - ).await.map_err(|e| anyhow::anyhow!("加载用户信息失败: {}", e))?; + ) + .await + .map_err(|e| anyhow::anyhow!("加载用户信息失败: {}", e))?; if let Some(info) = user_info { log::info!("已加载用户基本信息"); @@ -108,18 +117,14 @@ impl App { // 拼接完整的 API 地址 let check_url = format!("{}/chat/completions", api_base_url.trim_end_matches('/')); - log::info!("检查服务可用性: {}", check_url); + // log::info!("检查服务可用性: {}", check_url); let client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .build() .context("无法创建 HTTP 客户端")?; - match client - .request(Method::OPTIONS, &check_url) - .send() - .await - { + match client.request(Method::OPTIONS, &check_url).send().await { Ok(response) => { if response.status().is_success() || response.status().as_u16() == 405 { // 200 OK 或 405 Method Not Allowed 都表示服务可用 @@ -196,7 +201,10 @@ impl App { // 确保自动滚动启用 self.ui.auto_scroll = true; } - AppMessage::StreamingComplete { user: _, full_response } => { + AppMessage::StreamingComplete { + user: _, + full_response, + } => { // 流式完成,确保完整响应已保存 if let Some(last_msg) = self.ui.messages.last_mut() { if last_msg.role == crate::agent::MessageRole::Assistant { @@ -253,7 +261,10 @@ impl App { if self.ui.state == AppState::Chat { log::info!("调用 show_themes()"); self.show_themes(); - log::info!("show_themes() 调用完成,theme_modal_visible: {}", self.ui.theme_modal_visible); + log::info!( + "show_themes() 调用完成,theme_modal_visible: {}", + self.ui.theme_modal_visible + ); } else { log::warn!("不在 Chat 状态,无法显示主题"); } @@ -355,7 +366,9 @@ impl App { infrastructure.config(), infrastructure.memory_manager().clone(), &self.user_id, - ).await { + ) + .await + { Ok(info) => { self.user_info = info.clone(); info @@ -376,7 +389,9 @@ impl App { memory_tool_config, infrastructure.config(), user_info.as_deref(), - ).await { + ) + .await + { Ok(rig_agent) => { self.rig_agent = Some(rig_agent); log::info!("已创建带记忆功能的真实 Agent"); @@ -465,7 +480,8 @@ impl App { &user_id, ¤t_conversations, stream_tx, - ).await + ) + .await }); while let Some(chunk) = stream_rx.recv().await { @@ -492,7 +508,6 @@ impl App { } } }); - } else { log::warn!("Agent 未初始化"); } @@ -588,7 +603,9 @@ impl App { infrastructure.memory_manager().clone(), &conversations, &self.user_id, - ).await.map_err(|e| anyhow::anyhow!("保存对话到记忆系统失败: {}", e))?; + ) + .await + .map_err(|e| anyhow::anyhow!("保存对话到记忆系统失败: {}", e))?; log::info!("对话保存完成"); } } @@ -597,17 +614,24 @@ impl App { /// 获取所有对话 pub fn get_conversations(&self) -> Vec<(String, String)> { - self.ui.messages + self.ui + .messages .iter() .filter_map(|msg| match msg.role { crate::agent::MessageRole::User => Some((msg.content.clone(), String::new())), crate::agent::MessageRole::Assistant => { - if let Some(last) = self.ui.messages.iter().rev().find(|m| m.role == crate::agent::MessageRole::User) { + if let Some(last) = self + .ui + .messages + .iter() + .rev() + .find(|m| m.role == crate::agent::MessageRole::User) + { Some((last.content.clone(), msg.content.clone())) } else { None } - }, + } _ => None, }) .collect() diff --git a/examples/cortex-mem-tars/src/main.rs b/examples/cortex-mem-tars/src/main.rs index 2dd46f4..881618c 100644 --- a/examples/cortex-mem-tars/src/main.rs +++ b/examples/cortex-mem-tars/src/main.rs @@ -1,4 +1,6 @@ mod agent; +mod api_models; +mod api_server; mod app; mod config; mod infrastructure; @@ -6,7 +8,7 @@ mod logger; mod ui; use anyhow::{Context, Result}; -use app::{create_default_bots, App}; +use app::{App, create_default_bots}; use clap::Parser; use config::ConfigManager; use infrastructure::Infrastructure; @@ -56,16 +58,36 @@ async fn main() -> Result<()> { } }; + // 启动 API 服务器(如果基础设施已初始化) + if let Some(inf) = infrastructure.clone() { + let api_port = std::env::var("TARS_API_PORT") + .unwrap_or_else(|_| "18199".to_string()) + .parse::() + .unwrap_or(8080); + + let api_state = api_server::ApiServerState { + memory_manager: inf.memory_manager().clone(), + }; + + // 在后台启动 API 服务器 + tokio::spawn(async move { + if let Err(e) = api_server::start_api_server(api_state, api_port).await { + log::error!("API 服务器错误: {}", e); + } + }); + + log::info!("✅ API 服务器已在后台启动,监听端口 {}", api_port); + } + // 创建并运行应用 - let mut app = App::new( - config_manager, - log_manager, - infrastructure.clone(), - ).context("无法创建应用")?; + let mut app = + App::new(config_manager, log_manager, infrastructure.clone()).context("无法创建应用")?; log::info!("应用创建成功"); // 检查服务可用性 - app.check_service_status().await.context("无法检查服务状态")?; + app.check_service_status() + .await + .context("无法检查服务状态")?; // 加载用户基本信息 app.load_user_info().await.context("无法加载用户信息")?; @@ -76,9 +98,15 @@ async fn main() -> Result<()> { // 退出时保存对话到记忆系统(仅在启用增强记忆保存功能时) if args.enhance_memory_saver { if let Some(_inf) = infrastructure { - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ 🧠 Cortex Memory - 退出流程 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + println!( + "\n╔══════════════════════════════════════════════════════════════════════════════╗" + ); + println!( + "║ 🧠 Cortex Memory - 退出流程 ║" + ); + println!( + "╚══════════════════════════════════════════════════════════════════════════════╝" + ); log::info!("🚀 开始退出流程,准备保存对话到记忆系统..."); @@ -91,9 +119,15 @@ async fn main() -> Result<()> { if conversations.is_empty() { println!("⚠️ 没有需要存储的内容"); - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ ✅ 退出流程完成 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + println!( + "\n╔══════════════════════════════════════════════════════════════════════════════╗" + ); + println!( + "║ ✅ 退出流程完成 ║" + ); + println!( + "╚══════════════════════════════════════════════════════════════════════════════╝" + ); println!("👋 Cortex TARS powering down. Goodbye!"); return Ok(()); } @@ -117,9 +151,15 @@ async fn main() -> Result<()> { } } - println!("\n╔══════════════════════════════════════════════════════════════════════════════╗"); - println!("║ 🎉 退出流程完成 ║"); - println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + println!( + "\n╔══════════════════════════════════════════════════════════════════════════════╗" + ); + println!( + "║ 🎉 退出流程完成 ║" + ); + println!( + "╚══════════════════════════════════════════════════════════════════════════════╝" + ); println!("👋 Cortex TARS powering down. Goodbye!"); } else { println!("\n⚠️ 基础设施未初始化,无法保存对话到记忆系统"); From 41b886d900a4c4a97afa8a56e037de29dc686696 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Sun, 4 Jan 2026 23:06:40 +0800 Subject: [PATCH 05/22] add audio connect to ally audio recorder service --- examples/cortex-mem-tars/src/api_models.rs | 90 ++++ examples/cortex-mem-tars/src/api_server.rs | 453 +++++++++++++++++++++ 2 files changed, 543 insertions(+) create mode 100644 examples/cortex-mem-tars/src/api_models.rs create mode 100644 examples/cortex-mem-tars/src/api_server.rs diff --git a/examples/cortex-mem-tars/src/api_models.rs b/examples/cortex-mem-tars/src/api_models.rs new file mode 100644 index 0000000..1aa7120 --- /dev/null +++ b/examples/cortex-mem-tars/src/api_models.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Serialize}; + +/// 存储记忆请求 +#[derive(Debug, Clone, Deserialize)] +pub struct StoreMemoryRequest { + /// 语音转录后的文本内容 + pub content: String, + /// 固定值 "audio_listener",标识来源为语音旁听服务 + pub source: String, + /// 语音识别的时间戳,RFC 3339 格式 + pub timestamp: String, + /// 说话人类型:"user"(本人)或 "other"(他人) + #[serde(skip_serializing_if = "Option::is_none")] + pub speaker_type: Option, + /// 说话人识别的置信度(0-1) + #[serde(skip_serializing_if = "Option::is_none")] + pub speaker_confidence: Option, +} + +/// 存储记忆响应 +#[derive(Debug, Clone, Serialize)] +pub struct StoreMemoryResponse { + /// 是否成功存储 + pub success: bool, + /// 存储的记忆唯一标识 + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_id: Option, + /// 成功或错误消息 + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +/// 健康检查响应 +#[derive(Debug, Clone, Serialize)] +pub struct HealthResponse { + /// API 状态 + pub status: String, + /// 当前时间戳 + pub timestamp: String, +} + +/// 错误响应 +#[derive(Debug, Clone, Serialize)] +pub struct ErrorResponse { + /// 是否成功 + pub success: bool, + /// 错误类型 + #[serde(skip_serializing_if = "Option::is_none")] + pub error_type: Option, + /// 错误信息 + pub error: String, +} + +/// 记忆项(用于查询和列表响应) +#[derive(Debug, Clone, Serialize)] +pub struct MemoryItem { + /// 记忆 ID + pub id: String, + /// 记忆内容 + pub content: String, + /// 来源 + pub source: String, + /// 时间戳 + pub timestamp: String, + /// 说话人类型 + #[serde(skip_serializing_if = "Option::is_none")] + pub speaker_type: Option, + /// 说话人置信度 + #[serde(skip_serializing_if = "Option::is_none")] + pub speaker_confidence: Option, + /// 相关性分数 + #[serde(skip_serializing_if = "Option::is_none")] + pub relevance: Option, +} + +/// 查询记忆响应 +#[derive(Debug, Clone, Serialize)] +pub struct RetrieveMemoryResponse { + /// 记忆列表 + pub memories: Vec, +} + +/// 列出记忆响应 +#[derive(Debug, Clone, Serialize)] +pub struct ListMemoryResponse { + /// 记忆列表 + pub memories: Vec, + /// 总数 + pub total: usize, +} \ No newline at end of file diff --git a/examples/cortex-mem-tars/src/api_server.rs b/examples/cortex-mem-tars/src/api_server.rs new file mode 100644 index 0000000..335e73a --- /dev/null +++ b/examples/cortex-mem-tars/src/api_server.rs @@ -0,0 +1,453 @@ +use anyhow::{Context, Result}; +use axum::{ + Router, + extract::{Query, State}, + http::StatusCode, + response::Json, + routing::{get, post}, +}; +use chrono::{DateTime, Utc}; +use cortex_mem_core::memory::MemoryManager; +use cortex_mem_core::types::{Filters, MemoryMetadata, MemoryType, Message}; +use serde::Deserialize; +use serde_json::{Value, json}; +use std::collections::HashMap; +use std::sync::Arc; +use tower_http::cors::{Any, CorsLayer}; +use uuid::Uuid; + +use crate::api_models::{ + ErrorResponse, HealthResponse, ListMemoryResponse, MemoryItem, RetrieveMemoryResponse, + StoreMemoryRequest, StoreMemoryResponse, +}; + +/// 查询记忆参数 +#[derive(Debug, Deserialize)] +pub struct RetrieveMemoryQuery { + /// 查询关键词 + pub query: Option, + /// 说话人类型过滤 + pub speaker_type: Option, + /// 返回数量限制 + pub limit: Option, +} + +/// 列出记忆参数 +#[derive(Debug, Deserialize)] +pub struct ListMemoryQuery { + /// 说话人类型过滤 + pub speaker_type: Option, + /// 返回数量限制 + pub limit: Option, + /// 偏移量 + pub offset: Option, +} + +/// 验证说话人类型 +fn validate_speaker_type(speaker_type: &str) -> Result<()> { + if speaker_type != "user" && speaker_type != "other" { + return Err(anyhow::anyhow!( + "speaker_type must be 'user' or 'other', got: '{}'", + speaker_type + )); + } + Ok(()) +} + +/// 验证说话人置信度 +fn validate_speaker_confidence(confidence: f32) -> Result<()> { + if confidence < 0.0 || confidence > 1.0 { + return Err(anyhow::anyhow!( + "speaker_confidence must be between 0 and 1, got: {}", + confidence + )); + } + Ok(()) +} + +/// API 服务器状态 +#[derive(Clone)] +pub struct ApiServerState { + pub memory_manager: Arc, +} + +/// 创建 API 路由器 +pub fn create_router(state: ApiServerState) -> Router { + // 配置 CORS + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + Router::new() + .route("/api/memory/health", get(health_check)) + .route("/api/memory/store", post(store_memory)) + .route("/api/memory/retrieve", get(retrieve_memory)) + .route("/api/memory/list", get(list_memory)) + .layer(cors) + .with_state(state) +} + +/// 健康检查端点 +async fn health_check() -> Result, StatusCode> { + let response = HealthResponse { + status: "healthy".to_string(), + timestamp: Utc::now().to_rfc3339(), + }; + Ok(Json(response)) +} + +/// 存储记忆端点 +async fn store_memory( + State(state): State, + Json(request): Json, +) -> Result, (StatusCode, Json)> { + // 验证必填字段 + if request.content.trim().is_empty() { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + success: false, + error_type: Some("invalid_content".to_string()), + error: "Missing required field: content".to_string(), + }), + )); + } + + if request.source != "audio_listener" { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + success: false, + error_type: Some("invalid_source".to_string()), + error: "Invalid source value. Expected 'audio_listener'".to_string(), + }), + )); + } + + // 验证说话人类型(如果提供) + if let Some(ref speaker_type) = request.speaker_type { + if let Err(e) = validate_speaker_type(speaker_type) { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + success: false, + error_type: Some("invalid_speaker_type".to_string()), + error: e.to_string(), + }), + )); + } + } + + // 验证说话人置信度(如果提供) + if let Some(confidence) = request.speaker_confidence { + if let Err(e) = validate_speaker_confidence(confidence) { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + success: false, + error_type: Some("invalid_speaker_confidence".to_string()), + error: e.to_string(), + }), + )); + } + } + + // 解析时间戳 + let timestamp: DateTime = match DateTime::parse_from_rfc3339(&request.timestamp) { + Ok(dt) => dt.with_timezone(&Utc), + Err(_) => { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + success: false, + error_type: Some("invalid_timestamp".to_string()), + error: "Invalid timestamp format. Expected RFC 3339".to_string(), + }), + )); + } + }; + + let timestamp_str = format!( + "{}_{}", + timestamp.format("%Y%m%d"), + timestamp.format("%H%M%S") + ); + // 生成记忆 ID + let memory_id = format!( + "mem_{}_{}", + timestamp_str, + Uuid::new_v4() + .to_string() + .split('-') + .next() + .unwrap_or("unknown") + ); + + // 创建消息 + let messages = vec![Message { + role: "user".to_string(), + content: format!( + "当前我所处的环境中,在{},能听到这样的声音:{}", + timestamp_str, + request.content.clone() + ), + name: None, + }]; + + // 创建元数据 + let mut custom_metadata = HashMap::new(); + custom_metadata.insert("source".to_string(), json!("audio_listener")); + custom_metadata.insert("original_timestamp".to_string(), json!(request.timestamp)); + + // 添加说话人信息到元数据 + if let Some(ref speaker_type) = request.speaker_type { + custom_metadata.insert("speaker_type".to_string(), json!(speaker_type)); + } + if let Some(confidence) = request.speaker_confidence { + custom_metadata.insert("speaker_confidence".to_string(), json!(confidence)); + } + + let metadata = MemoryMetadata { + user_id: Some("tars_user".to_string()), + agent_id: Some("tars_via_ally".to_string()), + run_id: None, + actor_id: None, + role: Some("user".to_string()), + memory_type: MemoryType::Episodic, + hash: Uuid::new_v4().to_string(), + importance_score: 0.8, + entities: vec![], + topics: vec![], + custom: custom_metadata, + }; + + // 保存到记忆系统 + match state.memory_manager.add_memory(&messages, metadata).await { + Ok(results) => { + log::info!( + "✅ Memory stored successfully: {} (content length: {}, speaker_type: {:?})", + memory_id, + request.content.len(), + request.speaker_type + ); + + Ok(Json(StoreMemoryResponse { + success: true, + memory_id: Some(memory_id), + message: Some(format!( + "Memory stored successfully, {} memories created", + results.len() + )), + })) + } + Err(e) => { + log::error!("❌ Failed to store memory: {}", e); + + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + success: false, + error_type: Some("internal_error".to_string()), + error: format!("Failed to store memory: {}", e), + }), + )) + } + } +} + +/// 查询记忆端点 +async fn retrieve_memory( + State(state): State, + Query(params): Query, +) -> Result, (StatusCode, Json)> { + // 验证 speaker_type 参数(如果提供) + if let Some(ref speaker_type) = params.speaker_type { + if let Err(e) = validate_speaker_type(speaker_type) { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + success: false, + error_type: Some("invalid_speaker_type".to_string()), + error: e.to_string(), + }), + )); + } + } + + let limit = params.limit.unwrap_or(5); + + // 构建过滤器 + let mut filters = Filters::default(); + if let Some(ref speaker_type) = params.speaker_type { + let mut custom = HashMap::new(); + custom.insert("speaker_type".to_string(), json!(speaker_type)); + filters.custom = custom; + } + + // 执行查询 + match state + .memory_manager + .search(params.query.as_deref().unwrap_or(""), &filters, limit) + .await + { + Ok(scored_memories) => { + let memories: Vec = scored_memories + .into_iter() + .map(|sm| MemoryItem { + id: sm.memory.id, + content: sm.memory.content, + source: "audio_listener".to_string(), + timestamp: sm.memory.created_at.to_rfc3339(), + speaker_type: sm + .memory + .metadata + .custom + .get("speaker_type") + .and_then(|v: &Value| v.as_str()) + .map(|s| s.to_string()), + speaker_confidence: sm + .memory + .metadata + .custom + .get("speaker_confidence") + .and_then(|v: &Value| v.as_f64()) + .map(|f| f as f32), + relevance: Some(sm.score), + }) + .collect(); + + log::info!( + "✅ Retrieved {} memories (filter: speaker_type={:?}, query={:?})", + memories.len(), + params.speaker_type, + params.query + ); + + Ok(Json(RetrieveMemoryResponse { memories })) + } + Err(e) => { + log::error!("❌ Failed to retrieve memories: {}", e); + + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + success: false, + error_type: Some("internal_error".to_string()), + error: format!("Failed to retrieve memories: {}", e), + }), + )) + } + } +} + +/// 列出记忆端点 +async fn list_memory( + State(state): State, + Query(params): Query, +) -> Result, (StatusCode, Json)> { + // 验证 speaker_type 参数(如果提供) + if let Some(ref speaker_type) = params.speaker_type { + if let Err(e) = validate_speaker_type(speaker_type) { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + success: false, + error_type: Some("invalid_speaker_type".to_string()), + error: e.to_string(), + }), + )); + } + } + + let limit = params.limit.unwrap_or(10); + let offset = params.offset.unwrap_or(0); + + // 构建过滤器 + let mut filters = Filters::default(); + if let Some(ref speaker_type) = params.speaker_type { + let mut custom = HashMap::new(); + custom.insert("speaker_type".to_string(), json!(speaker_type)); + filters.custom = custom; + } + + // 执行查询 + match state + .memory_manager + .list(&filters, Some(limit + offset)) + .await + { + Ok(memories) => { + // 应用分页 + let paginated_memories: Vec<_> = memories + .into_iter() + .skip(offset) + .take(limit) + .map(|memory| MemoryItem { + id: memory.id, + content: memory.content, + source: "audio_listener".to_string(), + timestamp: memory.created_at.to_rfc3339(), + speaker_type: memory + .metadata + .custom + .get("speaker_type") + .and_then(|v: &Value| v.as_str()) + .map(|s| s.to_string()), + speaker_confidence: memory + .metadata + .custom + .get("speaker_confidence") + .and_then(|v: &Value| v.as_f64()) + .map(|f| f as f32), + relevance: None, + }) + .collect(); + + let total = paginated_memories.len(); + + log::info!( + "✅ Listed {} memories (filter: speaker_type={:?}, limit={}, offset={})", + total, + params.speaker_type, + limit, + offset + ); + + Ok(Json(ListMemoryResponse { + memories: paginated_memories, + total, + })) + } + Err(e) => { + log::error!("❌ Failed to list memories: {}", e); + + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + success: false, + error_type: Some("internal_error".to_string()), + error: format!("Failed to list memories: {}", e), + }), + )) + } + } +} + +/// 启动 API 服务器 +pub async fn start_api_server(state: ApiServerState, port: u16) -> Result<()> { + let app = create_router(state); + let addr = format!("0.0.0.0:{}", port); + + log::info!("🚀 Starting TARS API server on http://{}", addr); + + let listener = tokio::net::TcpListener::bind(&addr) + .await + .context(format!("Failed to bind to address: {}", addr))?; + + axum::serve(listener, app) + .await + .context("API server failed")?; + + Ok(()) +} From 91555ba8ce77572db4b1bdff7911facd006918ae Mon Sep 17 00:00:00 2001 From: Sopaco Date: Mon, 5 Jan 2026 19:51:31 +0800 Subject: [PATCH 06/22] add audio connect to ally audio recorder service --- examples/cortex-mem-tars/src/api_server.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/cortex-mem-tars/src/api_server.rs b/examples/cortex-mem-tars/src/api_server.rs index 335e73a..97114b9 100644 --- a/examples/cortex-mem-tars/src/api_server.rs +++ b/examples/cortex-mem-tars/src/api_server.rs @@ -170,8 +170,8 @@ async fn store_memory( let timestamp_str = format!( "{}_{}", - timestamp.format("%Y%m%d"), - timestamp.format("%H%M%S") + timestamp.format("%Y-%m-%d"), + timestamp.format("%H:%M:%S") ); // 生成记忆 ID let memory_id = format!( @@ -188,7 +188,7 @@ async fn store_memory( let messages = vec![Message { role: "user".to_string(), content: format!( - "当前我所处的环境中,在{},能听到这样的声音:{}", + "当前我所处的办公与会议环境中,时间是{},能听到这样的声音:{}", timestamp_str, request.content.clone() ), From 77b544a1f1228363fd3b3dd6d07ae3cc47eca8fe Mon Sep 17 00:00:00 2001 From: Sopaco Date: Mon, 5 Jan 2026 20:28:55 +0800 Subject: [PATCH 07/22] update --- .gitignore | 1 + examples/cortex-mem-tars/.gitignore | 1 + examples/cortex-mem-tars/src/app.rs | 106 ++++++ examples/cortex-mem-tars/src/config.rs | 16 +- examples/cortex-mem-tars/src/ui.rs | 485 ++++++++++++++++++++++++- 5 files changed, 606 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4e3822e..4097dca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ logs/ config.toml /misc_summaries +bots.json diff --git a/examples/cortex-mem-tars/.gitignore b/examples/cortex-mem-tars/.gitignore index ea8c4bf..ccbe939 100644 --- a/examples/cortex-mem-tars/.gitignore +++ b/examples/cortex-mem-tars/.gitignore @@ -1 +1,2 @@ /target +bots.json diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index 60db281..04ab1bf 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -274,6 +274,24 @@ impl App { self.dump_chats(); } } + crate::ui::KeyAction::ShowBotManagement => { + // 机器人管理弹窗的显示由 UI 处理 + } + crate::ui::KeyAction::CreateBot => { + // 创建机器人的逻辑在 UI 中处理 + } + crate::ui::KeyAction::EditBot => { + // 编辑机器人的逻辑在 UI 中处理 + } + crate::ui::KeyAction::DeleteBot => { + self.delete_bot().await?; + } + crate::ui::KeyAction::SaveBot => { + self.save_bot().await?; + } + crate::ui::KeyAction::CancelBot => { + // 取消操作由 UI 处理 + } crate::ui::KeyAction::Continue => {} } } @@ -641,6 +659,94 @@ impl App { pub fn get_user_id(&self) -> String { self.user_id.clone() } + + /// 保存机器人(创建或更新) + async fn save_bot(&mut self) -> Result<()> { + let (name, prompt, password) = self.ui.get_bot_input_data(); + + if name.trim().is_empty() { + log::warn!("机器人名称不能为空"); + return Ok(()); + } + + if prompt.trim().is_empty() { + log::warn!("系统提示词不能为空"); + return Ok(()); + } + + match self.ui.bot_management_state { + crate::ui::BotManagementState::Creating => { + // 创建新机器人 + let bot_name = name.clone(); + let new_bot = crate::config::BotConfig::new(name, prompt, password); + self.config_manager.add_bot(new_bot)?; + log::info!("成功创建机器人: {}", bot_name); + + // 刷新机器人列表 + self.refresh_bot_list()?; + } + crate::ui::BotManagementState::Editing => { + // 更新现有机器人 + if let Some(index) = self.ui.get_selected_bot_index() { + if let Some(existing_bot) = self.config_manager.get_bots()?.get(index) { + let bot_name = name.clone(); + let updated_bot = crate::config::BotConfig { + id: existing_bot.id.clone(), + name: name.clone(), + system_prompt: prompt, + access_password: password, + created_at: existing_bot.created_at, + }; + self.config_manager.update_bot(&existing_bot.id, updated_bot)?; + log::info!("成功更新机器人: {}", bot_name); + + // 刷新机器人列表 + self.refresh_bot_list()?; + } + } + } + _ => {} + } + + // 返回列表状态 + self.ui.bot_management_state = crate::ui::BotManagementState::List; + Ok(()) + } + + /// 删除机器人 + async fn delete_bot(&mut self) -> Result<()> { + if let Some(index) = self.ui.get_selected_bot_index() { + if let Some(bot) = self.config_manager.get_bots()?.get(index) { + let bot_id = bot.id.clone(); + let bot_name = bot.name.clone(); + + if self.config_manager.remove_bot(&bot_id)? { + log::info!("成功删除机器人: {}", bot_name); + + // 刷新机器人列表 + self.refresh_bot_list()?; + + // 如果删除的是当前选中的机器人,重置选择 + if let Some(selected) = self.ui.bot_list_state.selected() { + if selected >= self.ui.bot_list.len() && !self.ui.bot_list.is_empty() { + self.ui.bot_list_state.select(Some(self.ui.bot_list.len() - 1)); + } + } + } + } + } + + // 返回列表状态 + self.ui.bot_management_state = crate::ui::BotManagementState::List; + Ok(()) + } + + /// 刷新机器人列表 + fn refresh_bot_list(&mut self) -> Result<()> { + let bots = self.config_manager.get_bots()?; + self.ui.set_bot_list(bots); + Ok(()) + } } /// 创建默认机器人 diff --git a/examples/cortex-mem-tars/src/config.rs b/examples/cortex-mem-tars/src/config.rs index 79307af..6b8d33d 100644 --- a/examples/cortex-mem-tars/src/config.rs +++ b/examples/cortex-mem-tars/src/config.rs @@ -40,6 +40,9 @@ impl ConfigManager { // 获取当前工作目录 let current_dir = std::env::current_dir().context("无法获取当前工作目录")?; + // 优先使用当前目录保存 bots.json + let local_bots_file = current_dir.join("bots.json"); + // 系统配置目录(用于 bots.json) let config_dir = directories::ProjectDirs::from("com", "cortex", "mem-tars") .context("无法获取项目目录")? @@ -48,7 +51,16 @@ impl ConfigManager { fs::create_dir_all(&config_dir).context("无法创建配置目录")?; - let bots_file = config_dir.join("bots.json"); + let system_bots_file = config_dir.join("bots.json"); + + // 确定使用哪个 bots.json 文件:优先当前目录 + let _bots_file = if local_bots_file.exists() { + log::info!("使用当前目录的机器人配置文件: {:?}", local_bots_file); + local_bots_file.clone() + } else { + log::info!("使用系统配置目录的机器人配置文件: {:?}", system_bots_file); + system_bots_file + }; // cortex-mem 配置文件:优先从当前目录读取 let local_config_file = current_dir.join("config.toml"); @@ -105,7 +117,7 @@ impl ConfigManager { Ok(Self { config_dir, - bots_file, + bots_file: local_bots_file, // 始终使用当前目录的 bots.json cortex_config, }) } diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index 6ff13ee..4b49a13 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -139,6 +139,31 @@ pub struct AppUi { pub current_theme: Theme, pub theme_modal_visible: bool, pub theme_list_state: ListState, + // 机器人管理弹窗相关字段 + pub bot_management_modal_visible: bool, + pub bot_management_state: BotManagementState, + pub bot_name_input: TextArea<'static>, + pub bot_prompt_input: TextArea<'static>, + pub bot_password_input: TextArea<'static>, + pub bot_management_list_state: ListState, + pub active_input_field: BotInputField, // 当前活动的输入框 +} + +/// 机器人管理弹窗状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BotManagementState { + List, // 机器人列表 + Creating, // 创建机器人 + Editing, // 编辑机器人 + ConfirmDelete, // 确认删除 +} + +/// 机器人输入框类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BotInputField { + Name, // 机器人名称 + Prompt, // 系统提示词 + Password, // 访问密码 } /// 键盘事件处理结果 @@ -151,6 +176,12 @@ pub enum KeyAction { ShowHelp, // 显示帮助 ShowThemes, // 显示主题选择 DumpChats, // 导出会话到剪贴板 + ShowBotManagement, // 显示机器人管理 + CreateBot, // 创建机器人 + EditBot, // 编辑机器人 + DeleteBot, // 删除机器人 + SaveBot, // 保存机器人 + CancelBot, // 取消机器人操作 } impl AppUi { @@ -170,6 +201,25 @@ impl AppUi { let mut theme_list_state = ListState::default(); theme_list_state.select(Some(0)); + // 初始化机器人管理输入框 + let mut bot_name_input = TextArea::default(); + let _ = bot_name_input.set_block(Block::default() + .borders(Borders::ALL) + .title("机器人名称")); + + let mut bot_prompt_input = TextArea::default(); + let _ = bot_prompt_input.set_block(Block::default() + .borders(Borders::ALL) + .title("系统提示词")); + + let mut bot_password_input = TextArea::default(); + let _ = bot_password_input.set_block(Block::default() + .borders(Borders::ALL) + .title("访问密码")); + + let mut bot_management_list_state = ListState::default(); + bot_management_list_state.select(Some(0)); + Self { state: AppState::BotSelection, service_status: ServiceStatus::Initing, @@ -196,6 +246,13 @@ impl AppUi { current_theme: Theme::DEFAULT, theme_modal_visible: false, theme_list_state, + bot_management_modal_visible: false, + bot_management_state: BotManagementState::List, + bot_name_input, + bot_prompt_input, + bot_password_input, + bot_management_list_state, + active_input_field: BotInputField::Name, } } @@ -231,6 +288,11 @@ impl AppUi { self.last_key_event = Some(key); + // 优先处理机器人管理弹窗 + if self.bot_management_modal_visible { + return self.handle_bot_management_key(key); + } + match self.state { AppState::BotSelection => { if self.handle_bot_selection_key(key) { @@ -272,6 +334,14 @@ impl AppUi { } true } + KeyCode::Char('m') => { + // 打开机器人管理 + log::info!("打开机器人管理"); + self.bot_management_modal_visible = true; + self.bot_management_state = BotManagementState::List; + self.bot_management_list_state.select(Some(0)); + true + } KeyCode::Char('q') => { log::info!("用户按 q 退出"); false @@ -837,6 +907,11 @@ impl AppUi { AppState::BotSelection => self.render_bot_selection(frame), AppState::Chat => self.render_chat(frame), } + + // 如果机器人管理弹窗可见,渲染弹窗 + if self.bot_management_modal_visible { + self.render_bot_management_modal(frame); + } } /// 渲染机器人选择界面 @@ -887,7 +962,7 @@ impl AppUi { frame.render_stateful_widget(list, chunks[1], &mut self.bot_list_state); // 帮助提示 - let help = Paragraph::new("↑/↓ 或 j/k: 选择 | Enter: 进入 | q 或 Ctrl-C: 退出") + let help = Paragraph::new("↑/↓ 或 j/k: 选择 | Enter: 进入 | m: 管理机器人 | q 或 Ctrl-C: 退出") .alignment(Alignment::Center); frame.render_widget(help, chunks[2]); @@ -1633,3 +1708,411 @@ impl Default for AppUi { Self::new() } } + +impl AppUi { + /// 渲染机器人管理弹窗 + fn render_bot_management_modal(&mut self, frame: &mut Frame) { + let area = frame.area(); + + // 计算弹窗大小(居中显示) + let modal_width = area.width.saturating_sub(20).min(80); + let modal_height = area.height.saturating_sub(10).min(30); + + let x = (area.width - modal_width) / 2; + let y = (area.height - modal_height) / 2; + + let modal_area = Rect::new(x, y, modal_width, modal_height); + + // 创建半透明背景遮罩 + let overlay_area = area; + let overlay_block = Block::default() + .style(Style::default().bg(Color::Rgb(20, 20, 20))); + frame.render_widget(overlay_block, overlay_area); + + match self.bot_management_state { + BotManagementState::List => { + self.render_bot_management_list(frame, modal_area); + } + BotManagementState::Creating => { + self.render_bot_create_edit(frame, modal_area, true); + } + BotManagementState::Editing => { + self.render_bot_create_edit(frame, modal_area, false); + } + BotManagementState::ConfirmDelete => { + self.render_bot_confirm_delete(frame, modal_area); + } + } + } + + /// 渲染机器人管理列表 + fn render_bot_management_list(&mut self, frame: &mut Frame, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ]) + .split(area); + + // 标题 + let title = Paragraph::new("机器人管理") + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(self.current_theme.primary_color)) + .border_type(ratatui::widgets::BorderType::Double) + .title_style(Style::default().fg(self.current_theme.primary_color).add_modifier(Modifier::BOLD)) + .title(" Esc 关闭 ") + ) + .alignment(Alignment::Center) + .style(Style::default().bg(self.current_theme.background_color)); + + frame.render_widget(title, chunks[0]); + + // 机器人列表 + let items: Vec = self + .bot_list + .iter() + .map(|bot| { + ListItem::new(Line::from(vec![ + Span::styled( + bot.name.clone(), + Style::default() + .fg(self.current_theme.primary_color) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" - "), + Span::styled( + format!("{}...", &bot.system_prompt.chars().take(30).collect::()), + Style::default().fg(Color::Gray), + ), + ])) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL)) + .highlight_style( + Style::default() + .bg(self.current_theme.secondary_color) + .add_modifier(Modifier::REVERSED), + ); + + frame.render_stateful_widget(list, chunks[1], &mut self.bot_management_list_state); + + // 帮助提示 + let help = Paragraph::new("↑/↓: 选择 | c: 创建 | e: 编辑 | d: 删除 | Esc: 关闭") + .alignment(Alignment::Center) + .style(Style::default().bg(self.current_theme.background_color)); + + frame.render_widget(help, chunks[2]); + } + + /// 渲染创建/编辑机器人界面 + fn render_bot_create_edit(&mut self, frame: &mut Frame, area: Rect, is_create: bool) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(5), + Constraint::Length(3), + Constraint::Length(3), + ]) + .split(area); + + // 标题 + let title_text = if is_create { "创建机器人" } else { "编辑机器人" }; + let title = Paragraph::new(title_text) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(self.current_theme.primary_color)) + .border_type(ratatui::widgets::BorderType::Double) + .title_style(Style::default().fg(self.current_theme.primary_color).add_modifier(Modifier::BOLD)) + .title(" Esc: 取消 | Ctrl+S: 保存 ") + ) + .alignment(Alignment::Center) + .style(Style::default().bg(self.current_theme.background_color)); + + frame.render_widget(title, chunks[0]); + + // 机器人名称输入 + let name_block = Block::default() + .borders(Borders::ALL) + .title("机器人名称") + .border_style(if self.active_input_field == BotInputField::Name { + Style::default().fg(self.current_theme.primary_color) + } else { + Style::default().fg(Color::Gray) + }); + let mut name_input = self.bot_name_input.clone(); + let _ = name_input.set_block(name_block); + // 只为活动的输入框设置光标样式 + if self.active_input_field == BotInputField::Name { + let _ = name_input.set_cursor_style(Style::default()); + } else { + let _ = name_input.set_cursor_style(Style::default().add_modifier(Modifier::HIDDEN)); + } + frame.render_widget(&name_input, chunks[1]); + + // 系统提示词输入 + let prompt_block = Block::default() + .borders(Borders::ALL) + .title("系统提示词") + .border_style(if self.active_input_field == BotInputField::Prompt { + Style::default().fg(self.current_theme.primary_color) + } else { + Style::default().fg(Color::Gray) + }); + let mut prompt_input = self.bot_prompt_input.clone(); + let _ = prompt_input.set_block(prompt_block); + if self.active_input_field == BotInputField::Prompt { + let _ = prompt_input.set_cursor_style(Style::default()); + } else { + let _ = prompt_input.set_cursor_style(Style::default().add_modifier(Modifier::HIDDEN)); + } + frame.render_widget(&prompt_input, chunks[2]); + + // 访问密码输入 + let password_block = Block::default() + .borders(Borders::ALL) + .title("访问密码") + .border_style(if self.active_input_field == BotInputField::Password { + Style::default().fg(self.current_theme.primary_color) + } else { + Style::default().fg(Color::Gray) + }); + let mut password_input = self.bot_password_input.clone(); + let _ = password_input.set_block(password_block); + if self.active_input_field == BotInputField::Password { + let _ = password_input.set_cursor_style(Style::default()); + } else { + let _ = password_input.set_cursor_style(Style::default().add_modifier(Modifier::HIDDEN)); + } + frame.render_widget(&password_input, chunks[3]); + + // 帮助提示 + let help = Paragraph::new("Tab: 切换输入框 | Ctrl+S: 保存 | Esc: 取消") + .alignment(Alignment::Center) + .style(Style::default().bg(self.current_theme.background_color)); + + frame.render_widget(help, chunks[4]); + } + + /// 渲染确认删除界面 + fn render_bot_confirm_delete(&mut self, frame: &mut Frame, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .split(area); + + // 标题 + let title = Paragraph::new("确认删除") + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)) + .border_type(ratatui::widgets::BorderType::Double) + .title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) + .title(" Esc: 取消 ") + ) + .alignment(Alignment::Center) + .style(Style::default().bg(self.current_theme.background_color)); + + frame.render_widget(title, chunks[0]); + + // 获取选中的机器人 + let bot_name = if let Some(index) = self.bot_management_list_state.selected() { + self.bot_list.get(index).map(|b| b.name.clone()).unwrap_or_else(|| "未知".to_string()) + } else { + "未知".to_string() + }; + + // 确认消息 + let confirm_msg = Paragraph::new(format!("确定要删除机器人 '{}' 吗?", bot_name)) + .alignment(Alignment::Center) + .style(Style::default().fg(self.current_theme.text_color).bg(self.current_theme.background_color)); + + frame.render_widget(confirm_msg, chunks[1]); + + // 帮助提示 + let help = Paragraph::new("y: 确认删除 | Esc: 取消") + .alignment(Alignment::Center) + .style(Style::default().bg(self.current_theme.background_color)); + + frame.render_widget(help, chunks[2]); + } + + /// 处理机器人管理弹窗的键盘事件 + pub fn handle_bot_management_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::{KeyCode, KeyModifiers}; + + match self.bot_management_state { + BotManagementState::List => { + self.handle_bot_management_list_key(key) + } + BotManagementState::Creating | BotManagementState::Editing => { + self.handle_bot_create_edit_key(key) + } + BotManagementState::ConfirmDelete => { + self.handle_bot_confirm_delete_key(key) + } + } + } + + /// 处理机器人管理列表的键盘事件 + fn handle_bot_management_list_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::KeyCode; + + match key.code { + KeyCode::Esc => { + self.bot_management_modal_visible = false; + KeyAction::Continue + } + KeyCode::Up | KeyCode::Char('k') => { + if let Some(selected) = self.bot_management_list_state.selected() { + if selected > 0 { + self.bot_management_list_state.select(Some(selected - 1)); + } + } + KeyAction::Continue + } + KeyCode::Down | KeyCode::Char('j') => { + if let Some(selected) = self.bot_management_list_state.selected() { + if selected < self.bot_list.len().saturating_sub(1) { + self.bot_management_list_state.select(Some(selected + 1)); + } + } + KeyAction::Continue + } + KeyCode::Char('c') => { + // 创建机器人 + self.bot_management_state = BotManagementState::Creating; + self.clear_bot_inputs(); + KeyAction::CreateBot + } + KeyCode::Char('e') => { + // 编辑机器人 + if let Some(index) = self.bot_management_list_state.selected() { + if let Some(bot) = self.bot_list.get(index) { + self.bot_management_state = BotManagementState::Editing; + self.bot_name_input = TextArea::from(vec![bot.name.clone()]); + self.bot_prompt_input = TextArea::from(vec![bot.system_prompt.clone()]); + self.bot_password_input = TextArea::from(vec![bot.access_password.clone()]); + return KeyAction::EditBot; + } + } + KeyAction::Continue + } + KeyCode::Char('d') => { + // 删除机器人 + if !self.bot_list.is_empty() { + self.bot_management_state = BotManagementState::ConfirmDelete; + } + KeyAction::Continue + } + _ => KeyAction::Continue, + } + } + + /// 处理创建/编辑机器人的键盘事件 + fn handle_bot_create_edit_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::{KeyCode, KeyModifiers}; + + match key.code { + KeyCode::Esc => { + self.bot_management_state = BotManagementState::List; + KeyAction::CancelBot + } + KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // 保存机器人 + KeyAction::SaveBot + } + KeyCode::Tab => { + // 切换输入框 + self.active_input_field = match self.active_input_field { + BotInputField::Name => BotInputField::Prompt, + BotInputField::Prompt => BotInputField::Password, + BotInputField::Password => BotInputField::Name, + }; + KeyAction::Continue + } + _ => { + // 只让当前活动的输入框处理按键 + match self.active_input_field { + BotInputField::Name => { + self.bot_name_input.input(key); + } + BotInputField::Prompt => { + self.bot_prompt_input.input(key); + } + BotInputField::Password => { + self.bot_password_input.input(key); + } + } + KeyAction::Continue + } + } + } + + /// 处理确认删除的键盘事件 + fn handle_bot_confirm_delete_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::KeyCode; + + match key.code { + KeyCode::Esc => { + self.bot_management_state = BotManagementState::List; + KeyAction::CancelBot + } + KeyCode::Char('y') => { + // 确认删除 + KeyAction::DeleteBot + } + _ => KeyAction::Continue, + } + } + + /// 清空机器人输入框 + fn clear_bot_inputs(&mut self) { + self.bot_name_input = TextArea::default(); + let _ = self.bot_name_input.set_block(Block::default() + .borders(Borders::ALL) + .title("机器人名称")); + + self.bot_prompt_input = TextArea::default(); + let _ = self.bot_prompt_input.set_block(Block::default() + .borders(Borders::ALL) + .title("系统提示词")); + + self.bot_password_input = TextArea::default(); + let _ = self.bot_password_input.set_block(Block::default() + .borders(Borders::ALL) + .title("访问密码")); + + // 重置活动输入框 + self.active_input_field = BotInputField::Name; + } + + /// 获取机器人输入框的内容 + pub fn get_bot_input_data(&self) -> (String, String, String) { + let name = self.bot_name_input.lines().first().map(|s| s.clone()).unwrap_or_default(); + let prompt = self.bot_prompt_input.lines().first().map(|s| s.clone()).unwrap_or_default(); + let password = self.bot_password_input.lines().first().map(|s| s.clone()).unwrap_or_default(); + (name, prompt, password) + } + + /// 获取当前选中的机器人索引(用于编辑和删除) + pub fn get_selected_bot_index(&self) -> Option { + self.bot_management_list_state.selected() + } +} From 0bdf6d640e26c04b5d0ac5a22912be2e3725a6fc Mon Sep 17 00:00:00 2001 From: Sopaco Date: Mon, 5 Jan 2026 20:36:44 +0800 Subject: [PATCH 08/22] update --- examples/cortex-mem-tars/src/ui.rs | 50 ++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index 4b49a13..dfdb49a 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -1850,15 +1850,22 @@ impl AppUi { } else { Style::default().fg(Color::Gray) }); - let mut name_input = self.bot_name_input.clone(); - let _ = name_input.set_block(name_block); - // 只为活动的输入框设置光标样式 + if self.active_input_field == BotInputField::Name { + // 活动的输入框:使用 TextArea 渲染(带光标) + let mut name_input = self.bot_name_input.clone(); + let _ = name_input.set_block(name_block); let _ = name_input.set_cursor_style(Style::default()); + let _ = name_input.set_cursor_line_style(Style::default()); + frame.render_widget(&name_input, chunks[1]); } else { - let _ = name_input.set_cursor_style(Style::default().add_modifier(Modifier::HIDDEN)); + // 非活动的输入框:使用 Paragraph 渲染(无光标) + let name_text = self.bot_name_input.lines().first().map(|s| s.as_str()).unwrap_or(""); + let name_para = Paragraph::new(name_text) + .block(name_block) + .style(Style::default().bg(self.current_theme.background_color)); + frame.render_widget(name_para, chunks[1]); } - frame.render_widget(&name_input, chunks[1]); // 系统提示词输入 let prompt_block = Block::default() @@ -1869,14 +1876,23 @@ impl AppUi { } else { Style::default().fg(Color::Gray) }); - let mut prompt_input = self.bot_prompt_input.clone(); - let _ = prompt_input.set_block(prompt_block); + if self.active_input_field == BotInputField::Prompt { + // 活动的输入框:使用 TextArea 渲染(带光标) + let mut prompt_input = self.bot_prompt_input.clone(); + let _ = prompt_input.set_block(prompt_block); let _ = prompt_input.set_cursor_style(Style::default()); + let _ = prompt_input.set_cursor_line_style(Style::default()); + frame.render_widget(&prompt_input, chunks[2]); } else { - let _ = prompt_input.set_cursor_style(Style::default().add_modifier(Modifier::HIDDEN)); + // 非活动的输入框:使用 Paragraph 渲染(无光标) + let prompt_text = self.bot_prompt_input.lines().first().map(|s| s.as_str()).unwrap_or(""); + let prompt_para = Paragraph::new(prompt_text) + .block(prompt_block) + .wrap(Wrap { trim: false }) + .style(Style::default().bg(self.current_theme.background_color)); + frame.render_widget(prompt_para, chunks[2]); } - frame.render_widget(&prompt_input, chunks[2]); // 访问密码输入 let password_block = Block::default() @@ -1887,14 +1903,22 @@ impl AppUi { } else { Style::default().fg(Color::Gray) }); - let mut password_input = self.bot_password_input.clone(); - let _ = password_input.set_block(password_block); + if self.active_input_field == BotInputField::Password { + // 活动的输入框:使用 TextArea 渲染(带光标) + let mut password_input = self.bot_password_input.clone(); + let _ = password_input.set_block(password_block); let _ = password_input.set_cursor_style(Style::default()); + let _ = password_input.set_cursor_line_style(Style::default()); + frame.render_widget(&password_input, chunks[3]); } else { - let _ = password_input.set_cursor_style(Style::default().add_modifier(Modifier::HIDDEN)); + // 非活动的输入框:使用 Paragraph 渲染(无光标) + let password_text = self.bot_password_input.lines().first().map(|s| s.as_str()).unwrap_or(""); + let password_para = Paragraph::new(password_text) + .block(password_block) + .style(Style::default().bg(self.current_theme.background_color)); + frame.render_widget(password_para, chunks[3]); } - frame.render_widget(&password_input, chunks[3]); // 帮助提示 let help = Paragraph::new("Tab: 切换输入框 | Ctrl+S: 保存 | Esc: 取消") From b7fbd9aa3420380dedddbb3a6a6f830eb2a8f940 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Mon, 5 Jan 2026 20:42:32 +0800 Subject: [PATCH 09/22] support multi-role management --- examples/cortex-mem-tars/src/agent.rs | 12 ++++++++++-- examples/cortex-mem-tars/src/app.rs | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/cortex-mem-tars/src/agent.rs b/examples/cortex-mem-tars/src/agent.rs index c922881..8625706 100644 --- a/examples/cortex-mem-tars/src/agent.rs +++ b/examples/cortex-mem-tars/src/agent.rs @@ -61,6 +61,7 @@ pub async fn create_memory_agent( memory_tool_config: MemoryToolConfig, config: &Config, user_info: Option<&str>, + bot_system_prompt: Option<&str>, ) -> Result, Box> { // 创建记忆工具 let memory_tools = @@ -70,8 +71,8 @@ pub async fn create_memory_agent( .base_url(&config.llm.api_base_url) .build(); - // 构建 system prompt,包含用户基本信息 - let system_prompt = if let Some(info) = user_info { + // 构建 base system prompt,包含用户基本信息 + let base_system_prompt = if let Some(info) = user_info { format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 此会话发生的初始时间:{current_time} @@ -105,6 +106,13 @@ pub async fn create_memory_agent( 记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S")) }; + // 追加机器人系统提示词 + let system_prompt = if let Some(bot_prompt) = bot_system_prompt { + format!("{}\n\n你的角色设定:\n{}", base_system_prompt, bot_prompt) + } else { + base_system_prompt + }; + // 构建带有记忆工具的agent,让agent能够自主决定何时调用记忆功能 let completion_model = llm_client .completion_model(&config.llm.model_efficient) diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index 04ab1bf..1f37a4c 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -407,6 +407,7 @@ impl App { memory_tool_config, infrastructure.config(), user_info.as_deref(), + Some(bot.system_prompt.as_str()), ) .await { From 98a5f3b735723f570f201d6b2783e6d6f8d06e19 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Mon, 5 Jan 2026 21:19:04 +0800 Subject: [PATCH 10/22] ``` Add agent_id parameter to memory tools and system prompts ``` --- examples/cortex-mem-tars/src/agent.rs | 43 +++++++++++++++++++++------ examples/cortex-mem-tars/src/app.rs | 35 +++++++--------------- examples/cortex-mem-tars/src/main.rs | 3 -- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/examples/cortex-mem-tars/src/agent.rs b/examples/cortex-mem-tars/src/agent.rs index 8625706..9d12c0d 100644 --- a/examples/cortex-mem-tars/src/agent.rs +++ b/examples/cortex-mem-tars/src/agent.rs @@ -62,7 +62,11 @@ pub async fn create_memory_agent( config: &Config, user_info: Option<&str>, bot_system_prompt: Option<&str>, + agent_id: &str, ) -> Result, Box> { + // 提前获取 user_id,避免所有权问题 + let user_id_str = memory_tool_config.default_user_id.clone().unwrap_or_else(|| "unknown".to_string()); + // 创建记忆工具 let memory_tools = create_memory_tools(memory_manager.clone(), &config, Some(memory_tool_config)); @@ -71,39 +75,58 @@ pub async fn create_memory_agent( .base_url(&config.llm.api_base_url) .build(); - // 构建 base system prompt,包含用户基本信息 + // 构建 base system prompt,包含用户基本信息和 agent_id 说明 let base_system_prompt = if let Some(info) = user_info { format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 此会话发生的初始时间:{current_time} +重要说明: +- 你的身份标识(agent_id):{agent_id} +- 你服务的用户标识(user_id):{user_id} +- 当你调用记忆工具时,必须明确传入 user_id="{user_id}" 和 agent_id="{agent_id}" 参数 +- 你的记忆是独立的,只属于你这个 agent,不会与其他 agent 混淆 + 用户基本信息: {info} 重要指令: - 对话历史将作为上下文提供,请使用这些信息来理解当前的对话流程 - 用户基本信息已在上方提供,请不要再使用memory工具来创建或更新用户基本信息 -- 在需要时可以自主使用memory工具搜索其他相关记忆 -- 当用户提供新的重要信息时,可以主动使用memory工具存储 +- 在需要时可以自主使用memory工具搜索其他相关记忆,但必须传入正确的 user_id 和 agent_id +- 当用户提供新的重要信息时,可以主动使用memory工具存储,确保使用正确的 user_id 和 agent_id - 保持对话的连贯性和一致性 - 自然地融入记忆信息,避免刻意复述此前的记忆信息,关注当前的会话内容,记忆主要用于做隐式的逻辑与事实支撑 - 专注于用户的需求和想要了解的信息,以及想要你做的事情 -记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S"), info = info) +记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, + current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S"), + agent_id = agent_id, + user_id = user_id_str, + info = info) } else { format!(r#"你是一个拥有记忆功能的智能AI助手。你可以访问和使用记忆工具来检索、存储和管理用户信息。 此会话发生的初始时间:{current_time} +重要说明: +- 你的身份标识(agent_id):{agent_id} +- 你服务的用户标识(user_id):{user_id} +- 当你调用记忆工具时,必须明确传入 user_id="{user_id}" 和 agent_id="{agent_id}" 参数 +- 你的记忆是独立的,只属于你这个 agent,不会与其他 agent 混淆 + 重要指令: - 对话历史将作为上下文提供,请使用这些信息来理解当前的对话流程 -- 在需要时可以自主使用memory工具搜索其他相关记忆 -- 当用户提供新的重要信息时,可以主动使用memory工具存储 +- 在需要时可以自主使用memory工具搜索其他相关记忆,但必须传入正确的 user_id 和 agent_id +- 当用户提供新的重要信息时,可以主动使用memory工具存储,确保使用正确的 user_id 和 agent_id - 保持对话的连贯性和一致性 - 自然地融入记忆信息,避免刻意复述此前的记忆信息,关注当前的会话内容,记忆主要用于做隐式的逻辑与事实支撑 - 专注于用户的需求和想要了解的信息,以及想要你做的事情 -记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S")) +记住:你正在与一个了解的用户进行连续对话,对话过程中不需要刻意表达你的记忆能力。"#, + current_time = chrono::Local::now().format("%Y年%m月%d日 %H:%M:%S"), + agent_id = agent_id, + user_id = user_id_str) }; // 追加机器人系统提示词 @@ -134,12 +157,14 @@ pub async fn extract_user_basic_info( config: &Config, memory_manager: Arc, user_id: &str, + agent_id: &str, ) -> Result, Box> { let memory_tools = create_memory_tools( memory_manager, config, Some(MemoryToolConfig { default_user_id: Some(user_id.to_string()), + default_agent_id: Some(agent_id.to_string()), ..Default::default() }), ); @@ -150,14 +175,14 @@ pub async fn extract_user_basic_info( limit: Some(20), memory_type: Some("personal".to_string()), // 使用小写以匹配新API user_id: Some(user_id.to_string()), - agent_id: None, + agent_id: Some(agent_id.to_string()), }; let search_args_factual = ListMemoriesArgs { limit: Some(20), memory_type: Some("factual".to_string()), // 使用小写以匹配新API user_id: Some(user_id.to_string()), - agent_id: None, + agent_id: Some(agent_id.to_string()), }; if let Ok(search_result) = memory_tools diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index 1f37a4c..eab35f2 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -87,27 +87,6 @@ impl App { }) } - /// 设置用户信息 - pub async fn load_user_info(&mut self) -> Result<()> { - if let Some(infrastructure) = &self.infrastructure { - let user_info = extract_user_basic_info( - infrastructure.config(), - infrastructure.memory_manager().clone(), - &self.user_id, - ) - .await - .map_err(|e| anyhow::anyhow!("加载用户信息失败: {}", e))?; - - if let Some(info) = user_info { - log::info!("已加载用户基本信息"); - self.user_info = Some(info); - } else { - log::info!("未找到用户基本信息"); - } - } - Ok(()) - } - /// 检查服务可用性 pub async fn check_service_status(&mut self) -> Result<()> { use reqwest::Method; @@ -379,11 +358,12 @@ impl App { // 如果有基础设施,创建真实的带记忆的 Agent if let Some(infrastructure) = &self.infrastructure { - // 先提取用户基本信息 + // 先提取用户基本信息(使用 bot.id 作为 agent_id) let user_info = match extract_user_basic_info( infrastructure.config(), infrastructure.memory_manager().clone(), &self.user_id, + &bot.id, ) .await { @@ -399,6 +379,7 @@ impl App { let memory_tool_config = cortex_mem_rig::tool::MemoryToolConfig { default_user_id: Some(self.user_id.clone()), + default_agent_id: Some(bot.id.clone()), ..Default::default() }; @@ -408,6 +389,7 @@ impl App { infrastructure.config(), user_info.as_deref(), Some(bot.system_prompt.as_str()), + &bot.id, ) .await { @@ -416,7 +398,7 @@ impl App { log::info!("已创建带记忆功能的真实 Agent"); } Err(e) => { - log::error!("创建真实 Agent 失败,使用 Mock Agent: {}", e); + log::error!("创建真实 Agent 失败 {}", e); } } } @@ -698,7 +680,8 @@ impl App { access_password: password, created_at: existing_bot.created_at, }; - self.config_manager.update_bot(&existing_bot.id, updated_bot)?; + self.config_manager + .update_bot(&existing_bot.id, updated_bot)?; log::info!("成功更新机器人: {}", bot_name); // 刷新机器人列表 @@ -730,7 +713,9 @@ impl App { // 如果删除的是当前选中的机器人,重置选择 if let Some(selected) = self.ui.bot_list_state.selected() { if selected >= self.ui.bot_list.len() && !self.ui.bot_list.is_empty() { - self.ui.bot_list_state.select(Some(self.ui.bot_list.len() - 1)); + self.ui + .bot_list_state + .select(Some(self.ui.bot_list.len() - 1)); } } } diff --git a/examples/cortex-mem-tars/src/main.rs b/examples/cortex-mem-tars/src/main.rs index 881618c..deb8e71 100644 --- a/examples/cortex-mem-tars/src/main.rs +++ b/examples/cortex-mem-tars/src/main.rs @@ -89,9 +89,6 @@ async fn main() -> Result<()> { .await .context("无法检查服务状态")?; - // 加载用户基本信息 - app.load_user_info().await.context("无法加载用户信息")?; - // 运行应用 app.run().await.context("应用运行失败")?; From 6699acfd25128c6ea0188b957bb317107e9a47ac Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 12:58:49 +0800 Subject: [PATCH 11/22] optimize multi-role management --- examples/cortex-mem-tars/src/app.rs | 6 +- examples/cortex-mem-tars/src/ui.rs | 134 +++++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index eab35f2..d3c2142 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -740,18 +740,18 @@ pub fn create_default_bots(config_manager: &ConfigManager) -> Result<()> { let bots = config_manager.get_bots()?; if bots.is_empty() { - // 创建默认机器人 + // 创建默认机器人(密码为空,不需要验证) let default_bot = BotConfig::new( "助手", "你是一个有用的 AI 助手,能够回答各种问题并提供帮助。", - "password", + "", ); config_manager.add_bot(default_bot)?; let coder_bot = BotConfig::new( "程序员", "你是一个经验丰富的程序员,精通多种编程语言,能够帮助解决编程问题。", - "password", + "", ); config_manager.add_bot(coder_bot)?; diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index dfdb49a..e4f1fe2 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -16,6 +16,7 @@ use tui_textarea::TextArea; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppState { BotSelection, + PasswordInput, // 密码输入状态 Chat, } @@ -147,6 +148,9 @@ pub struct AppUi { pub bot_password_input: TextArea<'static>, pub bot_management_list_state: ListState, pub active_input_field: BotInputField, // 当前活动的输入框 + // 密码验证相关字段 + pub password_input: TextArea<'static>, + pub pending_bot: Option, // 等待密码验证的机器人 } /// 机器人管理弹窗状态 @@ -220,6 +224,12 @@ impl AppUi { let mut bot_management_list_state = ListState::default(); bot_management_list_state.select(Some(0)); + // 初始化密码输入框 + let mut password_input = TextArea::default(); + let _ = password_input.set_block(Block::default() + .borders(Borders::ALL) + .title("请输入密码")); + Self { state: AppState::BotSelection, service_status: ServiceStatus::Initing, @@ -253,6 +263,8 @@ impl AppUi { bot_password_input, bot_management_list_state, active_input_field: BotInputField::Name, + password_input, + pending_bot: None, } } @@ -288,7 +300,11 @@ impl AppUi { self.last_key_event = Some(key); - // 优先处理机器人管理弹窗 + // 优先级:密码输入 > 机器人管理弹窗 > 正常状态 + if self.state == AppState::PasswordInput { + return self.handle_password_input_key(key); + } + if self.bot_management_modal_visible { return self.handle_bot_management_key(key); } @@ -302,6 +318,7 @@ impl AppUi { } } AppState::Chat => self.handle_chat_key(key), + _ => KeyAction::Continue, } } @@ -330,7 +347,19 @@ impl AppUi { KeyCode::Enter => { if let Some(bot) = self.selected_bot() { log::info!("选择机器人: {}", bot.name); - self.state = AppState::Chat; + // 检查是否需要密码验证 + if bot.access_password.trim().is_empty() { + // 密码为空,直接进入聊天 + self.state = AppState::Chat; + } else { + // 需要密码验证 + self.pending_bot = Some(bot.clone()); + self.password_input = TextArea::default(); + let _ = self.password_input.set_block(Block::default() + .borders(Borders::ALL) + .title("请输入密码")); + self.state = AppState::PasswordInput; + } } true } @@ -354,6 +383,51 @@ impl AppUi { } } + /// 处理密码输入界面的键盘事件 + fn handle_password_input_key(&mut self, key: KeyEvent) -> KeyAction { + use ratatui::crossterm::event::KeyCode; + + match key.code { + KeyCode::Esc => { + // 取消密码输入,返回机器人选择界面 + self.state = AppState::BotSelection; + self.pending_bot = None; + self.password_input = TextArea::default(); + KeyAction::Continue + } + KeyCode::Enter => { + // 验证密码 + let input_password = self.password_input.lines().first().map(|s| s.trim()).unwrap_or(""); + if let Some(bot) = &self.pending_bot { + if input_password == bot.access_password.trim() { + // 密码正确,进入聊天 + log::info!("密码验证成功"); + self.state = AppState::Chat; + self.pending_bot = None; + self.password_input = TextArea::default(); + KeyAction::Continue + } else { + // 密码错误 + log::warn!("密码错误"); + self.password_input = TextArea::default(); + let _ = self.password_input.set_block(Block::default() + .borders(Borders::ALL) + .title("密码错误,请重新输入")); + KeyAction::Continue + } + } else { + self.state = AppState::BotSelection; + KeyAction::Continue + } + } + _ => { + // 让密码输入框处理按键 + self.password_input.input(key); + KeyAction::Continue + } + } + } + /// 处理聊天界面的键盘事件 fn handle_chat_key(&mut self, key: KeyEvent) -> KeyAction { use ratatui::crossterm::event::{KeyCode, KeyModifiers}; @@ -905,6 +979,7 @@ impl AppUi { pub fn render(&mut self, frame: &mut Frame) { match self.state { AppState::BotSelection => self.render_bot_selection(frame), + AppState::PasswordInput => self.render_password_input(frame), AppState::Chat => self.render_chat(frame), } @@ -968,6 +1043,39 @@ impl AppUi { frame.render_widget(help, chunks[2]); } + /// 渲染密码输入界面 + fn render_password_input(&mut self, frame: &mut Frame) { + let area = frame.area(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ]) + .split(area); + + // 标题 + let bot_name = self.pending_bot.as_ref().map(|b| b.name.as_str()).unwrap_or("未知"); + let title = Paragraph::new(format!("访问机器人: {}", bot_name)) + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Center) + .style(Style::default().add_modifier(Modifier::BOLD)); + + frame.render_widget(title, chunks[0]); + + // 密码输入框 + frame.render_widget(&self.password_input, chunks[1]); + + // 帮助提示 + let help = Paragraph::new("Enter: 确认 | Esc: 取消") + .alignment(Alignment::Center); + + frame.render_widget(help, chunks[2]); + } + /// 渲染聊天界面 fn render_chat(&mut self, frame: &mut Frame) { let area = frame.area(); @@ -1859,10 +1967,11 @@ impl AppUi { let _ = name_input.set_cursor_line_style(Style::default()); frame.render_widget(&name_input, chunks[1]); } else { - // 非活动的输入框:使用 Paragraph 渲染(无光标) - let name_text = self.bot_name_input.lines().first().map(|s| s.as_str()).unwrap_or(""); + // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 + let name_text = self.bot_name_input.lines().join("\n"); let name_para = Paragraph::new(name_text) .block(name_block) + .wrap(Wrap { trim: false }) .style(Style::default().bg(self.current_theme.background_color)); frame.render_widget(name_para, chunks[1]); } @@ -1885,8 +1994,8 @@ impl AppUi { let _ = prompt_input.set_cursor_line_style(Style::default()); frame.render_widget(&prompt_input, chunks[2]); } else { - // 非活动的输入框:使用 Paragraph 渲染(无光标) - let prompt_text = self.bot_prompt_input.lines().first().map(|s| s.as_str()).unwrap_or(""); + // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 + let prompt_text = self.bot_prompt_input.lines().join("\n"); let prompt_para = Paragraph::new(prompt_text) .block(prompt_block) .wrap(Wrap { trim: false }) @@ -1912,10 +2021,11 @@ impl AppUi { let _ = password_input.set_cursor_line_style(Style::default()); frame.render_widget(&password_input, chunks[3]); } else { - // 非活动的输入框:使用 Paragraph 渲染(无光标) - let password_text = self.bot_password_input.lines().first().map(|s| s.as_str()).unwrap_or(""); + // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 + let password_text = self.bot_password_input.lines().join("\n"); let password_para = Paragraph::new(password_text) .block(password_block) + .wrap(Wrap { trim: false }) .style(Style::default().bg(self.current_theme.background_color)); frame.render_widget(password_para, chunks[3]); } @@ -2072,7 +2182,7 @@ impl AppUi { KeyAction::Continue } _ => { - // 只让当前活动的输入框处理按键 + // 所有其他按键都让当前活动的输入框处理(包括 Enter 键换行) match self.active_input_field { BotInputField::Name => { self.bot_name_input.input(key); @@ -2129,9 +2239,9 @@ impl AppUi { /// 获取机器人输入框的内容 pub fn get_bot_input_data(&self) -> (String, String, String) { - let name = self.bot_name_input.lines().first().map(|s| s.clone()).unwrap_or_default(); - let prompt = self.bot_prompt_input.lines().first().map(|s| s.clone()).unwrap_or_default(); - let password = self.bot_password_input.lines().first().map(|s| s.clone()).unwrap_or_default(); + let name = self.bot_name_input.lines().join("\n"); + let prompt = self.bot_prompt_input.lines().join("\n"); + let password = self.bot_password_input.lines().join("\n"); (name, prompt, password) } From 128b72b82064c8caa782dea291d713c3c1534fec Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 13:10:07 +0800 Subject: [PATCH 12/22] optimize multi-role management --- examples/cortex-mem-tars/src/ui.rs | 33 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index e4f1fe2..e4190c9 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -1960,12 +1960,11 @@ impl AppUi { }); if self.active_input_field == BotInputField::Name { - // 活动的输入框:使用 TextArea 渲染(带光标) - let mut name_input = self.bot_name_input.clone(); - let _ = name_input.set_block(name_block); - let _ = name_input.set_cursor_style(Style::default()); - let _ = name_input.set_cursor_line_style(Style::default()); - frame.render_widget(&name_input, chunks[1]); + // 活动的输入框:直接使用原始输入框渲染(带光标) + let _ = self.bot_name_input.set_block(name_block); + let _ = self.bot_name_input.set_cursor_style(Style::default()); + let _ = self.bot_name_input.set_cursor_line_style(Style::default()); + frame.render_widget(&self.bot_name_input, chunks[1]); } else { // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 let name_text = self.bot_name_input.lines().join("\n"); @@ -1987,12 +1986,11 @@ impl AppUi { }); if self.active_input_field == BotInputField::Prompt { - // 活动的输入框:使用 TextArea 渲染(带光标) - let mut prompt_input = self.bot_prompt_input.clone(); - let _ = prompt_input.set_block(prompt_block); - let _ = prompt_input.set_cursor_style(Style::default()); - let _ = prompt_input.set_cursor_line_style(Style::default()); - frame.render_widget(&prompt_input, chunks[2]); + // 活动的输入框:直接使用原始输入框渲染(带光标) + let _ = self.bot_prompt_input.set_block(prompt_block); + let _ = self.bot_prompt_input.set_cursor_style(Style::default()); + let _ = self.bot_prompt_input.set_cursor_line_style(Style::default()); + frame.render_widget(&self.bot_prompt_input, chunks[2]); } else { // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 let prompt_text = self.bot_prompt_input.lines().join("\n"); @@ -2014,12 +2012,11 @@ impl AppUi { }); if self.active_input_field == BotInputField::Password { - // 活动的输入框:使用 TextArea 渲染(带光标) - let mut password_input = self.bot_password_input.clone(); - let _ = password_input.set_block(password_block); - let _ = password_input.set_cursor_style(Style::default()); - let _ = password_input.set_cursor_line_style(Style::default()); - frame.render_widget(&password_input, chunks[3]); + // 活动的输入框:直接使用原始输入框渲染(带光标) + let _ = self.bot_password_input.set_block(password_block); + let _ = self.bot_password_input.set_cursor_style(Style::default()); + let _ = self.bot_password_input.set_cursor_line_style(Style::default()); + frame.render_widget(&self.bot_password_input, chunks[3]); } else { // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 let password_text = self.bot_password_input.lines().join("\n"); From c9512dfbc4ac128cc28064694bb1e2bf057f02be Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 14:44:16 +0800 Subject: [PATCH 13/22] optimize multi-role management --- examples/cortex-mem-tars/src/ui.rs | 76 +++++++++++------------------- 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index e4190c9..dc1d93b 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -148,6 +148,10 @@ pub struct AppUi { pub bot_password_input: TextArea<'static>, pub bot_management_list_state: ListState, pub active_input_field: BotInputField, // 当前活动的输入框 + // 光标位置(用于显示光标) + pub cursor_visible: bool, + pub cursor_row: usize, + pub cursor_col: usize, // 密码验证相关字段 pub password_input: TextArea<'static>, pub pending_bot: Option, // 等待密码验证的机器人 @@ -265,6 +269,9 @@ impl AppUi { active_input_field: BotInputField::Name, password_input, pending_bot: None, + cursor_visible: false, + cursor_row: 0, + cursor_col: 0, } } @@ -1949,7 +1956,7 @@ impl AppUi { frame.render_widget(title, chunks[0]); - // 机器人名称输入 + // 机器人名称输入 - 使用 Paragraph 渲染以支持多行显示 let name_block = Block::default() .borders(Borders::ALL) .title("机器人名称") @@ -1959,23 +1966,14 @@ impl AppUi { Style::default().fg(Color::Gray) }); - if self.active_input_field == BotInputField::Name { - // 活动的输入框:直接使用原始输入框渲染(带光标) - let _ = self.bot_name_input.set_block(name_block); - let _ = self.bot_name_input.set_cursor_style(Style::default()); - let _ = self.bot_name_input.set_cursor_line_style(Style::default()); - frame.render_widget(&self.bot_name_input, chunks[1]); - } else { - // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 - let name_text = self.bot_name_input.lines().join("\n"); - let name_para = Paragraph::new(name_text) - .block(name_block) - .wrap(Wrap { trim: false }) - .style(Style::default().bg(self.current_theme.background_color)); - frame.render_widget(name_para, chunks[1]); - } + let name_text = self.bot_name_input.lines().join("\n"); + let name_para = Paragraph::new(name_text) + .block(name_block) + .wrap(Wrap { trim: false }) + .style(Style::default().bg(self.current_theme.background_color)); + frame.render_widget(name_para, chunks[1]); - // 系统提示词输入 + // 系统提示词输入 - 使用 Paragraph 渲染以支持多行显示 let prompt_block = Block::default() .borders(Borders::ALL) .title("系统提示词") @@ -1985,23 +1983,14 @@ impl AppUi { Style::default().fg(Color::Gray) }); - if self.active_input_field == BotInputField::Prompt { - // 活动的输入框:直接使用原始输入框渲染(带光标) - let _ = self.bot_prompt_input.set_block(prompt_block); - let _ = self.bot_prompt_input.set_cursor_style(Style::default()); - let _ = self.bot_prompt_input.set_cursor_line_style(Style::default()); - frame.render_widget(&self.bot_prompt_input, chunks[2]); - } else { - // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 - let prompt_text = self.bot_prompt_input.lines().join("\n"); - let prompt_para = Paragraph::new(prompt_text) - .block(prompt_block) - .wrap(Wrap { trim: false }) - .style(Style::default().bg(self.current_theme.background_color)); - frame.render_widget(prompt_para, chunks[2]); - } + let prompt_text = self.bot_prompt_input.lines().join("\n"); + let prompt_para = Paragraph::new(prompt_text) + .block(prompt_block) + .wrap(Wrap { trim: false }) + .style(Style::default().bg(self.current_theme.background_color)); + frame.render_widget(prompt_para, chunks[2]); - // 访问密码输入 + // 访问密码输入 - 使用 Paragraph 渲染以支持多行显示 let password_block = Block::default() .borders(Borders::ALL) .title("访问密码") @@ -2011,21 +2000,12 @@ impl AppUi { Style::default().fg(Color::Gray) }); - if self.active_input_field == BotInputField::Password { - // 活动的输入框:直接使用原始输入框渲染(带光标) - let _ = self.bot_password_input.set_block(password_block); - let _ = self.bot_password_input.set_cursor_style(Style::default()); - let _ = self.bot_password_input.set_cursor_line_style(Style::default()); - frame.render_widget(&self.bot_password_input, chunks[3]); - } else { - // 非活动的输入框:使用 Paragraph 渲染(无光标),支持多行 - let password_text = self.bot_password_input.lines().join("\n"); - let password_para = Paragraph::new(password_text) - .block(password_block) - .wrap(Wrap { trim: false }) - .style(Style::default().bg(self.current_theme.background_color)); - frame.render_widget(password_para, chunks[3]); - } + let password_text = self.bot_password_input.lines().join("\n"); + let password_para = Paragraph::new(password_text) + .block(password_block) + .wrap(Wrap { trim: false }) + .style(Style::default().bg(self.current_theme.background_color)); + frame.render_widget(password_para, chunks[3]); // 帮助提示 let help = Paragraph::new("Tab: 切换输入框 | Ctrl+S: 保存 | Esc: 取消") From bd49e45fc76f19543dee40a2ddf74ca5a8ef4995 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 14:46:30 +0800 Subject: [PATCH 14/22] optimize multi-role management --- examples/cortex-mem-tars/src/ui.rs | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index dc1d93b..fc0ce86 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -2007,6 +2007,9 @@ impl AppUi { .style(Style::default().bg(self.current_theme.background_color)); frame.render_widget(password_para, chunks[3]); + // 在活动输入框位置绘制光标 + self.render_cursor_in_active_input(frame, chunks.to_vec()); + // 帮助提示 let help = Paragraph::new("Tab: 切换输入框 | Ctrl+S: 保存 | Esc: 取消") .alignment(Alignment::Center) @@ -2015,6 +2018,49 @@ impl AppUi { frame.render_widget(help, chunks[4]); } + /// 在活动输入框中绘制光标 + fn render_cursor_in_active_input(&mut self, frame: &mut Frame, chunks: Vec) { + let (input_area, input_lines) = match self.active_input_field { + BotInputField::Name => (chunks[1], &self.bot_name_input), + BotInputField::Prompt => (chunks[2], &self.bot_prompt_input), + BotInputField::Password => (chunks[3], &self.bot_password_input), + }; + + // 获取光标位置 + let (cursor_row, cursor_col) = input_lines.cursor(); + + // 计算光标在屏幕上的绝对位置 + let content_area = input_area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + // 计算光标列位置(考虑 Unicode 字符宽度) + let lines = input_lines.lines(); + if cursor_row < lines.len() { + let line = &lines[cursor_row]; + let mut col_offset = 0; + for (i, c) in line.chars().enumerate() { + if i >= cursor_col { + break; + } + col_offset += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); + } + + // 计算光标在屏幕上的位置 + let cursor_x = content_area.x + col_offset as u16; + let cursor_y = content_area.y + cursor_row as u16; + + // 绘制光标(使用反色块) + let cursor_area = Rect::new(cursor_x, cursor_y, 1, 1); + let cursor_block = Block::default() + .style(Style::default() + .fg(self.current_theme.background_color) + .bg(self.current_theme.text_color)); + frame.render_widget(cursor_block, cursor_area); + } + } + /// 渲染确认删除界面 fn render_bot_confirm_delete(&mut self, frame: &mut Frame, area: Rect) { let chunks = Layout::default() From 6a3d8d369c7910191e53c75ad935604e2b080cc8 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 15:28:31 +0800 Subject: [PATCH 15/22] optimize multi-role management --- examples/cortex-mem-tars/src/ui.rs | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index fc0ce86..ff2a71a 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -2035,29 +2035,41 @@ impl AppUi { horizontal: 1, }); - // 计算光标列位置(考虑 Unicode 字符宽度) let lines = input_lines.lines(); if cursor_row < lines.len() { let line = &lines[cursor_row]; - let mut col_offset = 0; - for (i, c) in line.chars().enumerate() { + + // 计算光标列位置(考虑 Unicode 字符宽度) + let mut col_offset = 0usize; + let chars: Vec = line.chars().collect(); + for (i, c) in chars.iter().enumerate() { if i >= cursor_col { break; } - col_offset += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); + col_offset += unicode_width::UnicodeWidthChar::width(*c).unwrap_or(0); } - // 计算光标在屏幕上的位置 - let cursor_x = content_area.x + col_offset as u16; - let cursor_y = content_area.y + cursor_row as u16; + // 获取内容区域的宽度 + let content_width = content_area.width as usize; + + // 计算换行后的光标位置 + let display_row = cursor_row + (col_offset / content_width); + let display_col = col_offset % content_width; - // 绘制光标(使用反色块) - let cursor_area = Rect::new(cursor_x, cursor_y, 1, 1); - let cursor_block = Block::default() - .style(Style::default() - .fg(self.current_theme.background_color) - .bg(self.current_theme.text_color)); - frame.render_widget(cursor_block, cursor_area); + // 确保光标在内容区域内 + if display_row < content_area.height as usize { + // 计算光标在屏幕上的位置 + let cursor_x = content_area.x + display_col as u16; + let cursor_y = content_area.y + display_row as u16; + + // 绘制光标(使用反色块) + let cursor_area = Rect::new(cursor_x, cursor_y, 1, 1); + let cursor_block = Block::default() + .style(Style::default() + .fg(self.current_theme.background_color) + .bg(self.current_theme.text_color)); + frame.render_widget(cursor_block, cursor_area); + } } } From d881f14cb8e925ea0c827e0e52e95047e9c02889 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 15:36:42 +0800 Subject: [PATCH 16/22] optimize multi-role management --- examples/cortex-mem-tars/src/ui.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index ff2a71a..350cb8e 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -1838,11 +1838,12 @@ impl AppUi { let modal_area = Rect::new(x, y, modal_width, modal_height); - // 创建半透明背景遮罩 - let overlay_area = area; - let overlay_block = Block::default() - .style(Style::default().bg(Color::Rgb(20, 20, 20))); - frame.render_widget(overlay_block, overlay_area); + // 创建纯黑色背景遮罩,完全遮挡主界面 + frame.render_widget( + Block::default() + .style(Style::default().bg(Color::Black)), + area + ); match self.bot_management_state { BotManagementState::List => { @@ -1910,6 +1911,7 @@ impl AppUi { let list = List::new(items) .block(Block::default().borders(Borders::ALL)) + .style(Style::default().bg(self.current_theme.background_color)) .highlight_style( Style::default() .bg(self.current_theme.secondary_color) From f20032f718a541de44143c82898993a2f0c9b073 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 16:06:05 +0800 Subject: [PATCH 17/22] optimize multi-role management --- examples/cortex-mem-tars/src/ui.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index 350cb8e..570b6ee 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -984,16 +984,17 @@ impl AppUi { /// 渲染 UI pub fn render(&mut self, frame: &mut Frame) { + // 如果机器人管理弹窗可见,只渲染弹窗,不渲染主界面 + if self.bot_management_modal_visible { + self.render_bot_management_modal(frame); + return; + } + match self.state { AppState::BotSelection => self.render_bot_selection(frame), AppState::PasswordInput => self.render_password_input(frame), AppState::Chat => self.render_chat(frame), } - - // 如果机器人管理弹窗可见,渲染弹窗 - if self.bot_management_modal_visible { - self.render_bot_management_modal(frame); - } } /// 渲染机器人选择界面 @@ -1845,6 +1846,14 @@ impl AppUi { area ); + // 在弹窗区域绘制实心背景块,确保完全遮挡 + frame.render_widget( + Paragraph::new("") + .block(Block::default()) + .style(Style::default().bg(self.current_theme.background_color)), + modal_area + ); + match self.bot_management_state { BotManagementState::List => { self.render_bot_management_list(frame, modal_area); From a1691ca42ad1b2dd693d587abb66ddfe14be001b Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 17:28:43 +0800 Subject: [PATCH 18/22] optimize multi-role management --- examples/cortex-mem-tars/src/ui.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/cortex-mem-tars/src/ui.rs b/examples/cortex-mem-tars/src/ui.rs index 570b6ee..47b36e9 100644 --- a/examples/cortex-mem-tars/src/ui.rs +++ b/examples/cortex-mem-tars/src/ui.rs @@ -1118,6 +1118,7 @@ impl AppUi { .split(area); // 创建简洁的标题文字 + let bot_name = self.selected_bot().map(|b| b.name.as_str()).unwrap_or("未知"); let title_line = Line::from(vec![ Span::styled( "Cortex TARS AI Program", @@ -1125,6 +1126,12 @@ impl AppUi { .fg(self.current_theme.primary_color) .add_modifier(Modifier::BOLD), ), + Span::styled( + format!(" (当前角色: {})", bot_name), + Style::default() + .fg(self.current_theme.accent_color) + .add_modifier(Modifier::BOLD), + ), ]); let title = Paragraph::new(title_line) @@ -1183,6 +1190,7 @@ impl AppUi { .split(area); // 创建简洁的标题文字 + let bot_name = self.selected_bot().map(|b| b.name.as_str()).unwrap_or("未知"); let title_line = Line::from(vec![ Span::styled( "Cortex TARS AI Program", @@ -1190,6 +1198,12 @@ impl AppUi { .fg(self.current_theme.primary_color) .add_modifier(Modifier::BOLD), ), + Span::styled( + format!(" (当前角色: {})", bot_name), + Style::default() + .fg(self.current_theme.accent_color) + .add_modifier(Modifier::BOLD), + ), ]); let title = Paragraph::new(title_line) From 6ffde05ad042dba83f43f97455e2023b68da5d44 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Tue, 6 Jan 2026 17:34:16 +0800 Subject: [PATCH 19/22] add launch options to enable audio recorder connector income --- examples/cortex-mem-tars/src/main.rs | 50 ++++++++++++++++++---------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/examples/cortex-mem-tars/src/main.rs b/examples/cortex-mem-tars/src/main.rs index deb8e71..264682a 100644 --- a/examples/cortex-mem-tars/src/main.rs +++ b/examples/cortex-mem-tars/src/main.rs @@ -24,6 +24,10 @@ struct Args { /// 启用增强记忆保存功能,退出时自动保存对话到记忆系统 #[arg(long, action)] enhance_memory_saver: bool, + + /// 启用音频连接功能,启动 API 服务器监听语音识别信息传入 + #[arg(long, action)] + enable_audio_connect: bool, } #[tokio::main] @@ -35,6 +39,10 @@ async fn main() -> Result<()> { log::info!("已启用增强记忆保存功能"); } + if args.enable_audio_connect { + log::info!("已启用音频连接功能"); + } + // 初始化配置管理器 let config_manager = ConfigManager::new().context("无法初始化配置管理器")?; log::info!("配置管理器初始化成功"); @@ -58,25 +66,31 @@ async fn main() -> Result<()> { } }; - // 启动 API 服务器(如果基础设施已初始化) - if let Some(inf) = infrastructure.clone() { - let api_port = std::env::var("TARS_API_PORT") - .unwrap_or_else(|_| "18199".to_string()) - .parse::() - .unwrap_or(8080); - - let api_state = api_server::ApiServerState { - memory_manager: inf.memory_manager().clone(), - }; - - // 在后台启动 API 服务器 - tokio::spawn(async move { - if let Err(e) = api_server::start_api_server(api_state, api_port).await { - log::error!("API 服务器错误: {}", e); - } - }); + // 启动 API 服务器(如果基础设施已初始化且启用了音频连接) + if args.enable_audio_connect { + if let Some(inf) = infrastructure.clone() { + let api_port = std::env::var("TARS_API_PORT") + .unwrap_or_else(|_| "18199".to_string()) + .parse::() + .unwrap_or(8080); + + let api_state = api_server::ApiServerState { + memory_manager: inf.memory_manager().clone(), + }; + + // 在后台启动 API 服务器 + tokio::spawn(async move { + if let Err(e) = api_server::start_api_server(api_state, api_port).await { + log::error!("API 服务器错误: {}", e); + } + }); - log::info!("✅ API 服务器已在后台启动,监听端口 {}", api_port); + log::info!("✅ API 服务器已在后台启动,监听端口 {}", api_port); + } else { + log::warn!("未启用音频连接:基础设施未初始化"); + } + } else { + log::info!("音频连接功能未启用(使用 --enable-audio-connect 参数启用)"); } // 创建并运行应用 From c21401a3b600f891d320d04fb3f94fd4ba9d427e Mon Sep 17 00:00:00 2001 From: Sopaco Date: Wed, 7 Jan 2026 12:58:09 +0800 Subject: [PATCH 20/22] add audio connect to ally audio recorder service --- examples/cortex-mem-tars/config.example.toml | 2 +- examples/cortex-mem-tars/src/api_server.rs | 53 ++++- examples/cortex-mem-tars/src/app.rs | 191 +++++++++++++++++++ examples/cortex-mem-tars/src/config.rs | 8 +- examples/cortex-mem-tars/src/lib.rs | 2 + examples/cortex-mem-tars/src/main.rs | 36 +--- 6 files changed, 250 insertions(+), 42 deletions(-) diff --git a/examples/cortex-mem-tars/config.example.toml b/examples/cortex-mem-tars/config.example.toml index 00f08b1..b9e3d5d 100644 --- a/examples/cortex-mem-tars/config.example.toml +++ b/examples/cortex-mem-tars/config.example.toml @@ -25,7 +25,7 @@ max_tokens = 2000 [server] # 服务器主机 -host = "127.0.0.1" +host = "localhost" # 服务器端口 port = 8080 # CORS 允许的来源 diff --git a/examples/cortex-mem-tars/src/api_server.rs b/examples/cortex-mem-tars/src/api_server.rs index 97114b9..585fe85 100644 --- a/examples/cortex-mem-tars/src/api_server.rs +++ b/examples/cortex-mem-tars/src/api_server.rs @@ -69,6 +69,7 @@ fn validate_speaker_confidence(confidence: f32) -> Result<()> { #[derive(Clone)] pub struct ApiServerState { pub memory_manager: Arc, + pub current_bot_id: Arc>>, } /// 创建 API 路由器 @@ -208,9 +209,30 @@ async fn store_memory( custom_metadata.insert("speaker_confidence".to_string(), json!(confidence)); } + // 获取当前选中的机器人 ID + let current_bot_id = state + .current_bot_id + .read() + .map(|bot_id| bot_id.clone()) + .unwrap_or(None); + + let agent_id = match current_bot_id { + Some(id) => id, + None => { + return Err(( + StatusCode::SERVICE_UNAVAILABLE, + Json(ErrorResponse { + success: false, + error_type: Some("no_bot_selected".to_string()), + error: "No bot selected. Please select a bot before storing memory.".to_string(), + }), + )); + } + }; + let metadata = MemoryMetadata { user_id: Some("tars_user".to_string()), - agent_id: Some("tars_via_ally".to_string()), + agent_id: Some(agent_id), run_id: None, actor_id: None, role: Some("user".to_string()), @@ -441,13 +463,24 @@ pub async fn start_api_server(state: ApiServerState, port: u16) -> Result<()> { log::info!("🚀 Starting TARS API server on http://{}", addr); - let listener = tokio::net::TcpListener::bind(&addr) - .await - .context(format!("Failed to bind to address: {}", addr))?; - - axum::serve(listener, app) - .await - .context("API server failed")?; - - Ok(()) + match tokio::net::TcpListener::bind(&addr).await { + Ok(listener) => { + log::info!("✅ Successfully bound to address: {}", addr); + + match axum::serve(listener, app).await { + Ok(_) => { + log::info!("✅ API server stopped gracefully"); + Ok(()) + } + Err(e) => { + log::error!("❌ API server error: {}", e); + Err(anyhow::anyhow!("API server error: {}", e)) + } + } + } + Err(e) => { + log::error!("❌ Failed to bind to address {}: {}", addr, e); + Err(anyhow::anyhow!("Failed to bind to address {}: {}", addr, e)) + } + } } diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index d3c2142..78f0fae 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -35,6 +35,10 @@ pub struct App { should_quit: bool, message_sender: mpsc::UnboundedSender, message_receiver: mpsc::UnboundedReceiver, + pub current_bot_id: Arc>>, + enable_audio_connect: bool, + api_server_started: std::sync::Arc, + previous_state: Option, } /// 应用消息类型 @@ -60,6 +64,7 @@ impl App { config_manager: ConfigManager, log_manager: Arc, infrastructure: Option>, + enable_audio_connect: bool, ) -> Result { let mut ui = AppUi::new(); @@ -72,6 +77,8 @@ impl App { log::info!("应用程序初始化完成"); + let initial_state = ui.state; + Ok(Self { config_manager, log_manager, @@ -84,6 +91,10 @@ impl App { should_quit: false, message_sender: msg_tx, message_receiver: msg_rx, + current_bot_id: Arc::new(std::sync::RwLock::new(None)), + enable_audio_connect, + api_server_started: Arc::new(std::sync::atomic::AtomicBool::new(false)), + previous_state: Some(initial_state), }) } @@ -215,6 +226,8 @@ impl App { Event::Key(key) => { let action = self.ui.handle_key_event(key); + log::debug!("事件处理完成,当前状态: {:?}", self.ui.state); + match action { crate::ui::KeyAction::Quit => { self.should_quit = true; @@ -283,6 +296,64 @@ impl App { } } + // 检测状态变化(在事件处理之后) + + log::trace!("状态检查: previous_state={:?}, current_state={:?}", self.previous_state, self.ui.state); + + + + if self.previous_state != Some(self.ui.state) { + + log::info!("🔄 状态变化: {:?} -> {:?}", self.previous_state, self.ui.state); + + + + // 如果从 BotSelection 切换到 Chat,启动 API 服务器 + + log::info!("检查条件: previous_state == BotSelection: {}", + + self.previous_state == Some(crate::ui::AppState::BotSelection)); + + log::info!("检查条件: current_state == Chat: {}", + + self.ui.state == crate::ui::AppState::Chat); + + + + if self.previous_state == Some(crate::ui::AppState::BotSelection) + + && self.ui.state == crate::ui::AppState::Chat + + { + + log::info!("✨ 检测到从机器人选择切换到聊天模式"); + + if let Some(bot) = self.ui.selected_bot().cloned() { + + log::info!("🤖 选中的机器人: {} (ID: {})", bot.name, bot.id); + + log::info!("即将调用 on_enter_chat_mode..."); + + self.on_enter_chat_mode(&bot); + + log::info!("on_enter_chat_mode 调用完成"); + + } else { + + log::warn!("⚠️ 没有选中的机器人"); + + } + + } else { + + log::info!("⏭️ 状态变化不符合启动 API 服务器的条件"); + + } + + self.previous_state = Some(self.ui.state); + + } + if self.should_quit { break; } @@ -356,6 +427,12 @@ impl App { if let Some(bot) = self.ui.selected_bot() { self.current_bot = Some(bot.clone()); + // 更新 current_bot_id + if let Ok(mut bot_id) = self.current_bot_id.write() { + *bot_id = Some(bot.id.clone()); + log::info!("已更新当前机器人 ID: {}", bot.id); + } + // 如果有基础设施,创建真实的带记忆的 Agent if let Some(infrastructure) = &self.infrastructure { // 先提取用户基本信息(使用 bot.id 作为 agent_id) @@ -733,6 +810,120 @@ impl App { self.ui.set_bot_list(bots); Ok(()) } + + /// 启动 API 服务器 + fn start_api_server(&self) { + log::info!("🚀 尝试启动 API 服务器..."); + log::info!(" - enable_audio_connect: {}", self.enable_audio_connect); + log::info!(" - api_server_started: {}", + self.api_server_started.load(std::sync::atomic::Ordering::Relaxed)); + log::info!(" - infrastructure: {}", self.infrastructure.is_some()); + + if !self.enable_audio_connect { + log::warn!("❌ 音频连接功能未启用,跳过 API 服务器启动"); + log::warn!(" 提示:请使用 --enable-audio-connect 参数启动应用"); + return; + } + + // 检查是否已经启动 + if self + .api_server_started + .load(std::sync::atomic::Ordering::Relaxed) + { + log::debug!("API 服务器已经启动,跳过"); + return; + } + + if let Some(infrastructure) = &self.infrastructure { + let api_port = std::env::var("TARS_API_PORT") + .unwrap_or_else(|_| "18199".to_string()) + .parse::() + .unwrap_or(8080); + + log::info!(" - API 端口: {}", api_port); + + // 获取当前机器人 ID + let current_bot_id = if let Ok(bot_id) = self.current_bot_id.read() { + bot_id.clone() + } else { + None + }; + log::info!(" - 当前机器人 ID: {:?}", current_bot_id); + + let api_state = crate::api_server::ApiServerState { + memory_manager: infrastructure.memory_manager().clone(), + current_bot_id: self.current_bot_id.clone(), + }; + + let api_server_started = self.api_server_started.clone(); + + // 在后台启动 API 服务器 + let handle = tokio::spawn(async move { + log::info!("🔄 正在启动 API 服务器任务..."); + match crate::api_server::start_api_server(api_state, api_port).await { + Ok(_) => { + log::info!("✅ API 服务器任务完成"); + } + Err(e) => { + log::error!("❌ API 服务器错误: {}", e); + log::error!(" 错误详情: {:?}", e); + } + } + }); + + // 立即检查任务是否启动成功 + log::info!("📋 API 服务器任务句柄: {:?}", handle.id()); + + // 标记为已启动 + api_server_started.store(true, std::sync::atomic::Ordering::Relaxed); + + log::info!("✅ API 服务器已在后台启动,监听端口 {}", api_port); + log::info!("💡 请稍等几秒钟,让服务器完全启动..."); + + // 添加一个异步任务来验证服务器是否真正启动 + let api_server_started_clone = api_server_started.clone(); + tokio::spawn(async move { + // 等待 2 秒让服务器启动 + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + // 尝试连接服务器 + let health_url = format!("http://localhost:{}/api/memory/health", api_port); + match reqwest::get(&health_url).await { + Ok(response) => { + if response.status().is_success() { + log::info!("✅ API 服务器健康检查成功!服务器已就绪"); + } else { + log::warn!("⚠️ API 服务器健康检查失败,状态码: {}", response.status()); + } + } + Err(e) => { + log::error!("❌ 无法连接到 API 服务器: {}", e); + // 如果连接失败,重置启动标志 + api_server_started_clone.store(false, std::sync::atomic::Ordering::Relaxed); + } + } + }); + } else { + log::warn!("❌ 未启用音频连接:基础设施未初始化"); + } + } + + /// 当切换到聊天状态时调用此方法 + pub fn on_enter_chat_mode(&mut self, bot: &BotConfig) { + log::info!("🎯 进入聊天模式,机器人: {} (ID: {})", bot.name, bot.id); + + // 更新 current_bot_id + if let Ok(mut bot_id) = self.current_bot_id.write() { + *bot_id = Some(bot.id.clone()); + log::info!("✅ 已更新当前机器人 ID: {}", bot.id); + } else { + log::error!("❌ 无法更新 current_bot_id"); + } + + // 启动 API 服务器(如果启用了音频连接) + log::info!("📡 准备启动 API 服务器..."); + self.start_api_server(); + } } /// 创建默认机器人 diff --git a/examples/cortex-mem-tars/src/config.rs b/examples/cortex-mem-tars/src/config.rs index 6b8d33d..bed4198 100644 --- a/examples/cortex-mem-tars/src/config.rs +++ b/examples/cortex-mem-tars/src/config.rs @@ -16,7 +16,11 @@ pub struct BotConfig { } impl BotConfig { - pub fn new(name: impl Into, system_prompt: impl Into, access_password: impl Into) -> Self { + pub fn new( + name: impl Into, + system_prompt: impl Into, + access_password: impl Into, + ) -> Self { Self { id: uuid::Uuid::new_v4().to_string(), name: name.into(), @@ -95,7 +99,7 @@ impl ConfigManager { max_tokens: 2000, }, server: cortex_mem_config::ServerConfig { - host: "127.0.0.1".to_string(), + host: "localhost".to_string(), port: 8080, cors_origins: vec!["*".to_string()], }, diff --git a/examples/cortex-mem-tars/src/lib.rs b/examples/cortex-mem-tars/src/lib.rs index 5c681a7..7ca4040 100644 --- a/examples/cortex-mem-tars/src/lib.rs +++ b/examples/cortex-mem-tars/src/lib.rs @@ -1,4 +1,6 @@ pub mod agent; +pub mod api_server; +pub mod api_models; pub mod config; pub mod app; pub mod infrastructure; diff --git a/examples/cortex-mem-tars/src/main.rs b/examples/cortex-mem-tars/src/main.rs index 264682a..8ee66dc 100644 --- a/examples/cortex-mem-tars/src/main.rs +++ b/examples/cortex-mem-tars/src/main.rs @@ -66,36 +66,14 @@ async fn main() -> Result<()> { } }; - // 启动 API 服务器(如果基础设施已初始化且启用了音频连接) - if args.enable_audio_connect { - if let Some(inf) = infrastructure.clone() { - let api_port = std::env::var("TARS_API_PORT") - .unwrap_or_else(|_| "18199".to_string()) - .parse::() - .unwrap_or(8080); - - let api_state = api_server::ApiServerState { - memory_manager: inf.memory_manager().clone(), - }; - - // 在后台启动 API 服务器 - tokio::spawn(async move { - if let Err(e) = api_server::start_api_server(api_state, api_port).await { - log::error!("API 服务器错误: {}", e); - } - }); - - log::info!("✅ API 服务器已在后台启动,监听端口 {}", api_port); - } else { - log::warn!("未启用音频连接:基础设施未初始化"); - } - } else { - log::info!("音频连接功能未启用(使用 --enable-audio-connect 参数启用)"); - } - // 创建并运行应用 - let mut app = - App::new(config_manager, log_manager, infrastructure.clone()).context("无法创建应用")?; + let mut app = App::new( + config_manager, + log_manager, + infrastructure.clone(), + args.enable_audio_connect, + ) + .context("无法创建应用")?; log::info!("应用创建成功"); // 检查服务可用性 From fb189e5d3b3d4e36a04b5d6a46ac5210229e77d6 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Wed, 7 Jan 2026 15:09:20 +0800 Subject: [PATCH 21/22] Optimize the startup timing of the Ally Audio Connection Service --- examples/cortex-mem-tars/src/app.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index 78f0fae..f27c78c 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -308,25 +308,31 @@ impl App { - // 如果从 BotSelection 切换到 Chat,启动 API 服务器 + // 如果从 BotSelection 或 PasswordInput 切换到 Chat,启动 API 服务器 log::info!("检查条件: previous_state == BotSelection: {}", self.previous_state == Some(crate::ui::AppState::BotSelection)); + log::info!("检查条件: previous_state == PasswordInput: {}", + + self.previous_state == Some(crate::ui::AppState::PasswordInput)); + log::info!("检查条件: current_state == Chat: {}", self.ui.state == crate::ui::AppState::Chat); - if self.previous_state == Some(crate::ui::AppState::BotSelection) + if (self.previous_state == Some(crate::ui::AppState::BotSelection) + + || self.previous_state == Some(crate::ui::AppState::PasswordInput)) && self.ui.state == crate::ui::AppState::Chat { - log::info!("✨ 检测到从机器人选择切换到聊天模式"); + log::info!("✨ 检测到进入聊天模式"); if let Some(bot) = self.ui.selected_bot().cloned() { From 8c2b69f720ca55dcd45e44093ce4f914f49d3459 Mon Sep 17 00:00:00 2001 From: Sopaco Date: Wed, 7 Jan 2026 20:01:18 +0800 Subject: [PATCH 22/22] add an option to shift the behavior between make store or chat when audio input --- examples/cortex-mem-tars/src/api_server.rs | 44 +++++ examples/cortex-mem-tars/src/app.rs | 200 +++++++++++++++++++++ examples/cortex-mem-tars/src/main.rs | 9 + 3 files changed, 253 insertions(+) diff --git a/examples/cortex-mem-tars/src/api_server.rs b/examples/cortex-mem-tars/src/api_server.rs index 585fe85..b3e1d1e 100644 --- a/examples/cortex-mem-tars/src/api_server.rs +++ b/examples/cortex-mem-tars/src/api_server.rs @@ -13,6 +13,7 @@ use serde::Deserialize; use serde_json::{Value, json}; use std::collections::HashMap; use std::sync::Arc; +use tokio::sync::mpsc; use tower_http::cors::{Any, CorsLayer}; use uuid::Uuid; @@ -70,6 +71,8 @@ fn validate_speaker_confidence(confidence: f32) -> Result<()> { pub struct ApiServerState { pub memory_manager: Arc, pub current_bot_id: Arc>>, + pub audio_connect_mode: String, + pub external_message_sender: Option>, } /// 创建 API 路由器 @@ -103,6 +106,47 @@ async fn store_memory( State(state): State, Json(request): Json, ) -> Result, (StatusCode, Json)> { + // 检查模式:如果是 chat 模式,返回特殊响应 + if state.audio_connect_mode == "chat" { + log::info!("Chat 模式:收到消息,将模拟用户输入: {}", request.content); + + // 将消息发送到外部消息通道,由 App 处理 + if let Some(ref sender) = state.external_message_sender { + if let Err(e) = sender.send(request.content.clone()) { + log::error!("发送外部消息失败: {}", e); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + success: false, + error_type: Some("channel_error".to_string()), + error: format!("Failed to send message to channel: {}", e), + }), + )); + } + log::info!("✅ 消息已发送到外部消息通道"); + } else { + log::error!("❌ external_message_sender 未初始化"); + return Err(( + StatusCode::SERVICE_UNAVAILABLE, + Json(ErrorResponse { + success: false, + error_type: Some("service_unavailable".to_string()), + error: "External message channel not initialized".to_string(), + }), + )); + } + + return Ok(Json(StoreMemoryResponse { + success: true, + memory_id: None, + message: Some(format!( + "Chat mode: Message received and queued - {}", + request.content + )), + })); + } + + // 以下是 store 模式的原有逻辑 // 验证必填字段 if request.content.trim().is_empty() { return Err(( diff --git a/examples/cortex-mem-tars/src/app.rs b/examples/cortex-mem-tars/src/app.rs index f27c78c..b0babb6 100644 --- a/examples/cortex-mem-tars/src/app.rs +++ b/examples/cortex-mem-tars/src/app.rs @@ -37,8 +37,11 @@ pub struct App { message_receiver: mpsc::UnboundedReceiver, pub current_bot_id: Arc>>, enable_audio_connect: bool, + audio_connect_mode: String, api_server_started: std::sync::Arc, previous_state: Option, + external_message_sender: mpsc::UnboundedSender, + external_message_receiver: mpsc::UnboundedReceiver, } /// 应用消息类型 @@ -65,6 +68,7 @@ impl App { log_manager: Arc, infrastructure: Option>, enable_audio_connect: bool, + audio_connect_mode: String, ) -> Result { let mut ui = AppUi::new(); @@ -74,6 +78,7 @@ impl App { // 创建消息通道 let (msg_tx, msg_rx) = mpsc::unbounded_channel::(); + let (external_msg_tx, external_msg_rx) = mpsc::unbounded_channel::(); log::info!("应用程序初始化完成"); @@ -93,8 +98,11 @@ impl App { message_receiver: msg_rx, current_bot_id: Arc::new(std::sync::RwLock::new(None)), enable_audio_connect, + audio_connect_mode, api_server_started: Arc::new(std::sync::atomic::AtomicBool::new(false)), previous_state: Some(initial_state), + external_message_sender: external_msg_tx, + external_message_receiver: external_msg_rx, }) } @@ -214,6 +222,15 @@ impl App { } } + // 处理外部消息(来自 API 的 chat 模式) + if let Ok(external_msg) = self.external_message_receiver.try_recv() { + log::info!("收到外部消息: {}", external_msg); + // 调用 handle_external_message 处理外部消息 + if let Err(e) = self.handle_external_message(external_msg).await { + log::error!("处理外部消息失败: {}", e); + } + } + // 渲染 UI terminal.draw(|f| self.ui.render(f)).context("渲染失败")?; @@ -726,6 +743,187 @@ impl App { self.user_id.clone() } + /// 处理来自 API 的外部消息(模拟用户输入) + pub async fn handle_external_message(&mut self, content: String) -> Result<()> { + log::info!("收到外部消息: {}", content); + + // 检查是否选择了机器人 + if self.current_bot.is_none() { + if let Some(bot) = self.ui.selected_bot() { + self.current_bot = Some(bot.clone()); + + // 更新 current_bot_id + if let Ok(mut bot_id) = self.current_bot_id.write() { + *bot_id = Some(bot.id.clone()); + log::info!("已更新当前机器人 ID: {}", bot.id); + } + + // 如果有基础设施,创建真实的带记忆的 Agent + if let Some(infrastructure) = &self.infrastructure { + // 先提取用户基本信息(使用 bot.id 作为 agent_id) + let user_info = match extract_user_basic_info( + infrastructure.config(), + infrastructure.memory_manager().clone(), + &self.user_id, + &bot.id, + ) + .await + { + Ok(info) => { + self.user_info = info.clone(); + info + } + Err(e) => { + log::error!("提取用户基本信息失败: {}", e); + None + } + }; + + let memory_tool_config = cortex_mem_rig::tool::MemoryToolConfig { + default_user_id: Some(self.user_id.clone()), + default_agent_id: Some(bot.id.clone()), + ..Default::default() + }; + + match create_memory_agent( + infrastructure.memory_manager().clone(), + memory_tool_config, + infrastructure.config(), + user_info.as_deref(), + Some(bot.system_prompt.as_str()), + &bot.id, + ) + .await + { + Ok(rig_agent) => { + self.rig_agent = Some(rig_agent); + log::info!("已创建带记忆功能的真实 Agent"); + } + Err(e) => { + log::error!("创建真实 Agent 失败 {}", e); + } + } + } + + log::info!("选择机器人: {}", bot.name); + } else { + log::warn!("没有选中的机器人"); + return Ok(()); + } + } + + // 添加用户消息到 UI + let user_message = ChatMessage::user(content.clone()); + self.ui.messages.push(user_message.clone()); + + // 用户发送新消息,重新启用自动滚动 + self.ui.auto_scroll = true; + + log::info!("外部消息已添加到对话: {}", content); + log::debug!("当前消息总数: {}", self.ui.messages.len()); + + // 使用真实的带记忆的 Agent 进行流式响应 + if let Some(rig_agent) = &self.rig_agent { + // 构建历史对话(排除当前用户输入) + let current_conversations: Vec<(String, String)> = { + let mut conversations = Vec::new(); + let mut last_user_msg: Option = None; + + // 遍历所有消息,但排除最后一条(当前用户输入) + let messages_to_include = if self.ui.messages.len() > 1 { + &self.ui.messages[..self.ui.messages.len() - 1] + } else { + &[] + }; + + for msg in messages_to_include { + match msg.role { + crate::agent::MessageRole::User => { + // 如果有未配对的 User 消息,先保存它(单独的 User 消息) + if let Some(user_msg) = last_user_msg.take() { + conversations.push((user_msg, String::new())); + } + last_user_msg = Some(msg.content.clone()); + } + crate::agent::MessageRole::Assistant => { + // 将 Assistant 消息与最近的 User 消息配对 + if let Some(user_msg) = last_user_msg.take() { + conversations.push((user_msg, msg.content.clone())); + } + } + _ => {} + } + } + + // 如果最后一个消息是 User 消息,也加入对话历史 + if let Some(user_msg) = last_user_msg { + conversations.push((user_msg, String::new())); + } + + conversations + }; + + let infrastructure_clone = self.infrastructure.clone(); + let rig_agent_clone = rig_agent.clone(); + let msg_tx = self.message_sender.clone(); + let user_input = content.clone(); + let user_id = self.user_id.clone(); + let user_input_for_stream = user_input.clone(); + + tokio::spawn(async move { + let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::(); + + let generation_task = tokio::spawn(async move { + agent_reply_with_memory_retrieval_streaming( + &rig_agent_clone, + infrastructure_clone.unwrap().memory_manager().clone(), + &user_input, + &user_id, + ¤t_conversations, + stream_tx, + ) + .await + }); + + while let Some(chunk) = stream_rx.recv().await { + if let Err(_) = msg_tx.send(AppMessage::StreamingChunk { + user: user_input_for_stream.clone(), + chunk, + }) { + break; + } + } + + match generation_task.await { + Ok(Ok(full_response)) => { + let _ = msg_tx.send(AppMessage::StreamingComplete { + user: user_input_for_stream.clone(), + full_response, + }); + } + Ok(Err(e)) => { + log::error!("生成回复失败: {}", e); + } + Err(e) => { + log::error!("任务执行失败: {}", e); + } + } + }); + } else { + log::warn!("Agent 未初始化"); + } + + // 滚动到底部 - 将在渲染时自动计算 + self.ui.auto_scroll = true; + + Ok(()) + } + + /// 获取外部消息发送器的克隆(用于 API server 发送消息) + pub fn get_external_message_sender(&self) -> mpsc::UnboundedSender { + self.external_message_sender.clone() + } + /// 保存机器人(创建或更新) async fn save_bot(&mut self) -> Result<()> { let (name, prompt, password) = self.ui.get_bot_input_data(); @@ -859,6 +1057,8 @@ impl App { let api_state = crate::api_server::ApiServerState { memory_manager: infrastructure.memory_manager().clone(), current_bot_id: self.current_bot_id.clone(), + audio_connect_mode: self.audio_connect_mode.clone(), + external_message_sender: Some(self.external_message_sender.clone()), }; let api_server_started = self.api_server_started.clone(); diff --git a/examples/cortex-mem-tars/src/main.rs b/examples/cortex-mem-tars/src/main.rs index 8ee66dc..928333a 100644 --- a/examples/cortex-mem-tars/src/main.rs +++ b/examples/cortex-mem-tars/src/main.rs @@ -28,6 +28,10 @@ struct Args { /// 启用音频连接功能,启动 API 服务器监听语音识别信息传入 #[arg(long, action)] enable_audio_connect: bool, + + /// 音频连接模式:store(存储到记忆系统)或 chat(模拟用户输入发送消息) + #[arg(long, default_value = "store")] + audio_connect_mode: String, } #[tokio::main] @@ -41,6 +45,10 @@ async fn main() -> Result<()> { if args.enable_audio_connect { log::info!("已启用音频连接功能"); + if args.audio_connect_mode != "store" && args.audio_connect_mode != "chat" { + log::warn!("无效的 audio_connect_mode 值: {},将使用默认值 'store'", args.audio_connect_mode); + } + log::info!("音频连接模式: {}", args.audio_connect_mode); } // 初始化配置管理器 @@ -72,6 +80,7 @@ async fn main() -> Result<()> { log_manager, infrastructure.clone(), args.enable_audio_connect, + args.audio_connect_mode.clone(), ) .context("无法创建应用")?; log::info!("应用创建成功");