feat: integrate Memori long-term memory system

Add Memori (https://github.com/MemoriLabs/Memori) integration for
persistent cross-session memory capabilities in both create_agent
and create_deep_agent.

## New Files

- agent/memori_config.py: MemoriConfig dataclass for configuration
- agent/memori_manager.py: MemoriManager for connection and instance management
- agent/memori_middleware.py: MemoriMiddleware for memory recall/storage
- tests/: Unit tests for Memori components

## Modified Files

- agent/agent_config.py: Added enable_memori, memori_semantic_search_top_k, etc.
- agent/deep_assistant.py: Integrated MemoriMiddleware into init_agent()
- utils/settings.py: Added MEMORI_* environment variables
- pyproject.toml: Added memori>=3.1.0 dependency

## Features

- Semantic memory search with configurable top-k and threshold
- Multi-tenant isolation (entity_id=user, process_id=bot, session_id)
- Memory injection into system prompt
- Background asynchronous memory augmentation
- Graceful degradation when Memori is unavailable

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
朱潮 2026-01-20 00:12:43 +08:00
parent af63c54778
commit 455a48409d
11 changed files with 1545 additions and 25 deletions

View File

@ -41,6 +41,12 @@ class AgentConfig:
logging_handler: Optional['LoggingCallbackHandler'] = None logging_handler: Optional['LoggingCallbackHandler'] = None
# Memori 长期记忆配置
enable_memori: bool = False
memori_semantic_search_top_k: int = 5
memori_semantic_search_threshold: float = 0.7
memori_inject_to_system_prompt: bool = True
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式,用于传递给需要**kwargs的函数""" """转换为字典格式,用于传递给需要**kwargs的函数"""
return { return {
@ -62,6 +68,10 @@ class AgentConfig:
'tool_response': self.tool_response, 'tool_response': self.tool_response,
'preamble_text': self.preamble_text, 'preamble_text': self.preamble_text,
'messages': self.messages, 'messages': self.messages,
'enable_memori': self.enable_memori,
'memori_semantic_search_top_k': self.memori_semantic_search_top_k,
'memori_semantic_search_threshold': self.memori_semantic_search_threshold,
'memori_inject_to_system_prompt': self.memori_inject_to_system_prompt,
} }
def safe_print(self): def safe_print(self):
@ -77,6 +87,11 @@ class AgentConfig:
# 延迟导入避免循环依赖 # 延迟导入避免循环依赖
from .logging_handler import LoggingCallbackHandler from .logging_handler import LoggingCallbackHandler
from utils.fastapi_utils import get_preamble_text from utils.fastapi_utils import get_preamble_text
from utils.settings import (
MEMORI_ENABLED,
MEMORI_SEMANTIC_SEARCH_TOP_K,
MEMORI_SEMANTIC_SEARCH_THRESHOLD,
)
if messages is None: if messages is None:
messages = [] messages = []
@ -87,6 +102,11 @@ class AgentConfig:
preamble_text, system_prompt = get_preamble_text(request.language, request.system_prompt) preamble_text, system_prompt = get_preamble_text(request.language, request.system_prompt)
enable_thinking = request.enable_thinking and "<guidelines>" in request.system_prompt enable_thinking = request.enable_thinking and "<guidelines>" in request.system_prompt
# 从请求中获取 Memori 配置,如果没有则使用全局配置
enable_memori = getattr(request, 'enable_memori', None)
if enable_memori is None:
enable_memori = MEMORI_ENABLED
config = cls( config = cls(
bot_id=request.bot_id, bot_id=request.bot_id,
api_key=api_key, api_key=api_key,
@ -108,6 +128,9 @@ class AgentConfig:
_origin_messages=messages, _origin_messages=messages,
preamble_text=preamble_text, preamble_text=preamble_text,
dataset_ids=request.dataset_ids, dataset_ids=request.dataset_ids,
enable_memori=enable_memori,
memori_semantic_search_top_k=getattr(request, 'memori_semantic_search_top_k', None) or MEMORI_SEMANTIC_SEARCH_TOP_K,
memori_semantic_search_threshold=getattr(request, 'memori_semantic_search_threshold', None) or MEMORI_SEMANTIC_SEARCH_THRESHOLD,
) )
config.safe_print() config.safe_print()
return config return config
@ -119,6 +142,11 @@ class AgentConfig:
# 延迟导入避免循环依赖 # 延迟导入避免循环依赖
from .logging_handler import LoggingCallbackHandler from .logging_handler import LoggingCallbackHandler
from utils.fastapi_utils import get_preamble_text from utils.fastapi_utils import get_preamble_text
from utils.settings import (
MEMORI_ENABLED,
MEMORI_SEMANTIC_SEARCH_TOP_K,
MEMORI_SEMANTIC_SEARCH_THRESHOLD,
)
if messages is None: if messages is None:
messages = [] messages = []
language = request.language or bot_config.get("language", "zh") language = request.language or bot_config.get("language", "zh")
@ -128,6 +156,11 @@ class AgentConfig:
robot_type = "deep_agent" robot_type = "deep_agent"
enable_thinking = request.enable_thinking and "<guidelines>" in bot_config.get("system_prompt") enable_thinking = request.enable_thinking and "<guidelines>" in bot_config.get("system_prompt")
# 从请求或后端配置中获取 Memori 配置
enable_memori = getattr(request, 'enable_memori', None)
if enable_memori is None:
enable_memori = bot_config.get("enable_memori", MEMORI_ENABLED)
config = cls( config = cls(
bot_id=request.bot_id, bot_id=request.bot_id,
@ -150,6 +183,9 @@ class AgentConfig:
_origin_messages=messages, _origin_messages=messages,
preamble_text=preamble_text, preamble_text=preamble_text,
dataset_ids=bot_config.get("dataset_ids", []), # 从后端配置获取dataset_ids dataset_ids=bot_config.get("dataset_ids", []), # 从后端配置获取dataset_ids
enable_memori=enable_memori,
memori_semantic_search_top_k=bot_config.get("memori_semantic_search_top_k", MEMORI_SEMANTIC_SEARCH_TOP_K),
memori_semantic_search_threshold=bot_config.get("memori_semantic_search_threshold", MEMORI_SEMANTIC_SEARCH_THRESHOLD),
) )
config.safe_print() config.safe_print()
return config return config

View File

@ -11,6 +11,7 @@ from deepagents.backends.filesystem import FilesystemBackend
from deepagents.backends.sandbox import SandboxBackendProtocol from deepagents.backends.sandbox import SandboxBackendProtocol
from deepagents_cli.agent import create_cli_agent from deepagents_cli.agent import create_cli_agent
from langchain.agents import create_agent from langchain.agents import create_agent
from langgraph.store.base import BaseStore
from langchain.agents.middleware import SummarizationMiddleware from langchain.agents.middleware import SummarizationMiddleware
from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_mcp_adapters.client import MultiServerMCPClient
from sympy.printing.cxx import none from sympy.printing.cxx import none
@ -18,8 +19,22 @@ from utils.fastapi_utils import detect_provider
from .guideline_middleware import GuidelineMiddleware from .guideline_middleware import GuidelineMiddleware
from .tool_output_length_middleware import ToolOutputLengthMiddleware from .tool_output_length_middleware import ToolOutputLengthMiddleware
from .tool_use_cleanup_middleware import ToolUseCleanupMiddleware from .tool_use_cleanup_middleware import ToolUseCleanupMiddleware
from utils.settings import SUMMARIZATION_MAX_TOKENS, SUMMARIZATION_MESSAGES_TO_KEEP, TOOL_OUTPUT_MAX_LENGTH, MCP_HTTP_TIMEOUT, MCP_SSE_READ_TIMEOUT from utils.settings import (
SUMMARIZATION_MAX_TOKENS,
SUMMARIZATION_MESSAGES_TO_KEEP,
TOOL_OUTPUT_MAX_LENGTH,
MCP_HTTP_TIMEOUT,
MCP_SSE_READ_TIMEOUT,
MEMORI_ENABLED,
MEMORI_API_KEY,
MEMORI_SEMANTIC_SEARCH_TOP_K,
MEMORI_SEMANTIC_SEARCH_THRESHOLD,
MEMORI_INJECT_TO_SYSTEM_PROMPT,
)
from agent.agent_config import AgentConfig from agent.agent_config import AgentConfig
from .memori_manager import get_memori_manager, init_global_memori
from .memori_middleware import create_memori_middleware
from .memori_config import MemoriConfig
from agent.prompt_loader import load_system_prompt_async, load_mcp_settings_async from agent.prompt_loader import load_system_prompt_async, load_mcp_settings_async
from agent.agent_memory_cache import get_memory_cache_manager from agent.agent_memory_cache import get_memory_cache_manager
from .checkpoint_utils import prepare_checkpoint_message from .checkpoint_utils import prepare_checkpoint_message
@ -165,6 +180,16 @@ async def init_agent(config: AgentConfig):
checkpointer = None checkpointer = None
create_start = time.time() create_start = time.time()
# 从连接池获取 checkpointer需要在 Memori 初始化之前完成)
if config.session_id:
try:
manager = get_checkpointer_manager()
checkpointer = manager.checkpointer
if checkpointer:
await prepare_checkpoint_message(config, checkpointer)
except Exception as e:
logger.warning(f"Failed to load checkpointer: {e}")
# 构建中间件列表 # 构建中间件列表
middleware = [] middleware = []
# 首先添加 ToolUseCleanupMiddleware 来清理孤立的 tool_use # 首先添加 ToolUseCleanupMiddleware 来清理孤立的 tool_use
@ -180,15 +205,42 @@ async def init_agent(config: AgentConfig):
) )
middleware.append(tool_output_middleware) middleware.append(tool_output_middleware)
# 从连接池获取 checkpointer # 添加 Memori 记忆中间件(如果启用)
if config.session_id: memori_middleware = None
if config.enable_memori:
try: try:
manager = get_checkpointer_manager() # 确保有 user_identifier
checkpointer = manager.checkpointer if not config.user_identifier:
if checkpointer: logger.warning("Memori enabled but user_identifier is missing, skipping Memori")
await prepare_checkpoint_message(config, checkpointer) else:
# 获取 MemoriManager使用共享的连接池
memori_manager = get_memori_manager()
# 如果 MemoriManager 未初始化,则初始化
if not memori_manager._initialized:
db_pool = get_checkpointer_manager().pool if checkpointer else None
await init_global_memori(
db_pool=db_pool,
api_key=MEMORI_API_KEY
)
# 创建 Memori 中间件
memori_middleware = create_memori_middleware(
bot_id=config.bot_id,
user_identifier=config.user_identifier,
session_id=config.session_id or "default",
enabled=config.enable_memori,
semantic_search_top_k=config.memori_semantic_search_top_k,
semantic_search_threshold=config.memori_semantic_search_threshold,
memori_manager=memori_manager,
)
if memori_middleware:
middleware.append(memori_middleware)
logger.info("Memori middleware added to agent")
except Exception as e: except Exception as e:
logger.warning(f"Failed to load checkpointer: {e}") logger.error(f"Failed to initialize Memori middleware: {e}, continuing without Memori")
if config.robot_type == "deep_agent": if config.robot_type == "deep_agent":

87
agent/memori_config.py Normal file
View File

@ -0,0 +1,87 @@
"""
Memori 配置数据类
用于管理 Memori 长期记忆系统的配置参数
"""
from dataclasses import dataclass
from typing import Optional
@dataclass
class MemoriConfig:
"""Memori 长期记忆配置类"""
# 功能开关
enabled: bool = False
# API 配置
api_key: Optional[str] = None
# 语义搜索配置
semantic_search_top_k: int = 5
semantic_search_threshold: float = 0.7
semantic_search_embeddings_limit: int = 1000
# 记忆注入配置
inject_memory_to_system_prompt: bool = True
memory_prompt_template: str = (
"\n\n=== 相关记忆 ===\n"
"以下是从历史对话中检索到的相关信息,可以帮助你更好地回答用户问题:\n"
"{memories}\n"
"==================\n"
)
# 增强配置
augmentation_enabled: bool = True
augmentation_wait_timeout: Optional[float] = None # None 表示后台异步执行
# 多租户配置
entity_id: Optional[str] = None # 用户标识
process_id: Optional[str] = None # Bot 标识
session_id: Optional[str] = None # 会话标识
def get_attribution_tuple(self) -> tuple[str, str]:
"""获取 attribution 所需的元组 (entity_id, process_id)
Returns:
(entity_id, process_id) 元组
"""
if not self.entity_id or not self.process_id:
raise ValueError("entity_id and process_id are required for attribution")
return (self.entity_id, self.process_id)
def is_enabled(self) -> bool:
"""检查 Memori 功能是否启用
Returns:
bool: 是否启用
"""
return self.enabled
def get_memory_prompt(self, memories: list[str]) -> str:
"""根据记忆列表生成注入提示词
Args:
memories: 记忆内容列表
Returns:
str: 格式化的记忆提示词
"""
if not memories:
return ""
memory_text = "\n".join(f"- {m}" for m in memories)
return self.memory_prompt_template.format(memories=memory_text)
def with_session(self, session_id: str) -> "MemoriConfig":
"""创建带有新 session_id 的配置副本
Args:
session_id: 新的会话 ID
Returns:
新的 MemoriConfig 实例
"""
new_config = MemoriConfig(**self.__dict__)
new_config.session_id = session_id
return new_config

383
agent/memori_manager.py Normal file
View File

@ -0,0 +1,383 @@
"""
Memori 连接和实例管理器
负责管理 Memori 客户端实例的创建缓存和生命周期
"""
import asyncio
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from psycopg_pool import AsyncConnectionPool
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from .memori_config import MemoriConfig
logger = logging.getLogger("app")
class MemoriManager:
"""
Memori 连接和实例管理器
主要功能
1. 管理 Memori 实例的创建和缓存
2. 支持多租户隔离entity_id + process_id
3. 处理数据库连接和会话管理
4. 提供记忆召回和存储接口
"""
def __init__(
self,
db_pool: Optional[AsyncConnectionPool] = None,
db_url: Optional[str] = None,
api_key: Optional[str] = None,
):
"""初始化 MemoriManager
Args:
db_pool: PostgreSQL 异步连接池 Checkpointer 共享
db_url: 数据库连接 URL如果不使用连接池
api_key: Memori API 密钥用于高级增强功能
"""
self._db_pool = db_pool
self._db_url = db_url
self._api_key = api_key
# 缓存 Memori 实例: key = f"{entity_id}:{process_id}"
self._instances: Dict[str, Any] = {}
self._sync_engines: Dict[str, Any] = {}
self._initialized = False
async def initialize(self) -> None:
"""初始化 MemoriManager
创建数据库表结构如果不存在
"""
if self._initialized:
return
logger.info("Initializing MemoriManager...")
try:
# 创建第一个 Memori 实例来初始化表结构
if self._db_pool or self._db_url:
db_url = self._db_url or getattr(self._db_pool, "_url", None)
if db_url:
await self._build_schema(db_url)
self._initialized = True
logger.info("MemoriManager initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize MemoriManager: {e}")
# 不抛出异常,允许系统在没有 Memori 的情况下运行
async def _build_schema(self, db_url: str) -> None:
"""构建 Memori 数据库表结构
Args:
db_url: 数据库连接 URL
"""
try:
from memori import Memori
# 创建同步引擎用于初始化
engine = create_engine(db_url)
SessionLocal = sessionmaker(bind=engine)
# 创建 Memori 实例并构建表结构
mem = Memori(conn=SessionLocal)
mem.config.storage.build()
logger.info("Memori schema built successfully")
except ImportError:
logger.warning("memori package not available, skipping schema build")
except Exception as e:
logger.error(f"Failed to build Memori schema: {e}")
def _get_sync_session(self, db_url: str) -> Session:
"""获取同步数据库会话Memori 需要)
Args:
db_url: 数据库连接 URL
Returns:
SQLAlchemy Session
"""
if db_url not in self._sync_engines:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine(db_url, pool_pre_ping=True)
self._sync_engines[db_url] = sessionmaker(bind=engine)
return self._sync_engines[db_url]()
async def get_memori(
self,
entity_id: str,
process_id: str,
session_id: str,
config: Optional[MemoriConfig] = None,
) -> Any:
"""获取或创建 Memori 实例
Args:
entity_id: 实体 ID通常是 user_identifier
process_id: 进程 ID通常是 bot_id
session_id: 会话 ID
config: Memori 配置
Returns:
Memori 实例
"""
cache_key = f"{entity_id}:{process_id}"
# 检查缓存
if cache_key in self._instances:
memori_instance = self._instances[cache_key]
# 更新会话
memori_instance.config.session_id = session_id
return memori_instance
# 创建新实例
memori_instance = await self._create_memori_instance(
entity_id=entity_id,
process_id=process_id,
session_id=session_id,
config=config,
)
# 缓存实例
self._instances[cache_key] = memori_instance
return memori_instance
async def _create_memori_instance(
self,
entity_id: str,
process_id: str,
session_id: str,
config: Optional[MemoriConfig] = None,
) -> Any:
"""创建新的 Memori 实例
Args:
entity_id: 实体 ID
process_id: 进程 ID
session_id: 会话 ID
config: Memori 配置
Returns:
Memori 实例
"""
try:
from memori import Memori
except ImportError:
logger.error("memori package not installed")
raise RuntimeError("memori package is required but not installed")
# 获取数据库连接 URL
db_url = self._db_url
if self._db_pool and hasattr(self._db_pool, "_url"):
db_url = str(self._db_pool._url)
if not db_url:
raise ValueError("Either db_pool or db_url must be provided")
# 创建同步会话Memori 目前需要同步连接)
session_factory = self._get_sync_session(db_url)
# 创建 Memori 实例
mem = Memori(conn=session_factory)
# 设置 API 密钥(如果提供)
if self._api_key or (config and config.api_key):
api_key = config.api_key if config else self._api_key
mem.config.api_key = api_key
# 设置 attribution
mem.attribution(entity_id=entity_id, process_id=process_id)
# 设置会话
mem.config.session_id = session_id
# 配置召回参数
if config:
mem.config.recall_facts_limit = config.semantic_search_top_k
mem.config.recall_relevance_threshold = config.semantic_search_threshold
mem.config.recall_embeddings_limit = config.semantic_search_embeddings_limit
logger.info(
f"Created Memori instance: entity={entity_id}, process={process_id}, session={session_id}"
)
return mem
async def recall_memories(
self,
query: str,
entity_id: str,
process_id: str,
session_id: str,
config: Optional[MemoriConfig] = None,
) -> List[Dict[str, Any]]:
"""召回相关记忆
Args:
query: 查询文本
entity_id: 实体 ID
process_id: 进程 ID
session_id: 会话 ID
config: Memori 配置
Returns:
记忆列表每个记忆包含 content, similarity 等字段
"""
try:
mem = await self.get_memori(entity_id, process_id, session_id, config)
# 调用 recall 进行语义搜索
results = mem.recall(query=query, limit=config.semantic_search_top_k if config else 5)
# 转换为统一格式
memories = []
for result in results:
memory = {
"content": result.get("content", ""),
"similarity": result.get("similarity", 0.0),
"fact_type": result.get("fact_type", "unknown"),
"created_at": result.get("created_at"),
}
# 过滤低相关度记忆
threshold = config.semantic_search_threshold if config else 0.7
if memory["similarity"] >= threshold:
memories.append(memory)
logger.info(f"Recalled {len(memories)} memories for query: {query[:50]}...")
return memories
except Exception as e:
logger.error(f"Failed to recall memories: {e}")
return []
async def wait_for_augmentation(
self,
entity_id: str,
process_id: str,
session_id: str,
timeout: Optional[float] = None,
) -> None:
"""等待后台增强任务完成
Args:
entity_id: 实体 ID
process_id: 进程 ID
session_id: 会话 ID
timeout: 超时时间
"""
try:
mem = await self.get_memori(entity_id, process_id, session_id)
if timeout:
# 在线程池中运行同步的 wait()
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, lambda: mem.augmentation.wait(timeout=timeout))
else:
# 无限等待
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, mem.augmentation.wait)
except Exception as e:
logger.error(f"Failed to wait for augmentation: {e}")
def clear_cache(self, entity_id: Optional[str] = None, process_id: Optional[str] = None) -> None:
"""清除缓存的 Memori 实例
Args:
entity_id: 实体 ID如果为 None清除所有
process_id: 进程 ID如果为 None清除所有
"""
if entity_id is None and process_id is None:
self._instances.clear()
logger.info("Cleared all Memori instances from cache")
else:
keys_to_remove = []
for key in self._instances:
e_id, p_id = key.split(":")
if entity_id and e_id != entity_id:
continue
if process_id and p_id != process_id:
continue
keys_to_remove.append(key)
for key in keys_to_remove:
del self._instances[key]
logger.info(f"Cleared {len(keys_to_remove)} Memori instances from cache")
async def close(self) -> None:
"""关闭管理器并清理资源"""
logger.info("Closing MemoriManager...")
# 清理缓存的实例
self._instances.clear()
# 关闭同步引擎
for engine in self._sync_engines.values():
try:
engine.dispose()
except Exception as e:
logger.error(f"Error closing engine: {e}")
self._sync_engines.clear()
self._initialized = False
logger.info("MemoriManager closed")
# 全局单例
_global_manager: Optional[MemoriManager] = None
def get_memori_manager() -> MemoriManager:
"""获取全局 MemoriManager 单例
Returns:
MemoriManager 实例
"""
global _global_manager
if _global_manager is None:
_global_manager = MemoriManager()
return _global_manager
async def init_global_memori(
db_pool: Optional[AsyncConnectionPool] = None,
db_url: Optional[str] = None,
api_key: Optional[str] = None,
) -> MemoriManager:
"""初始化全局 MemoriManager
Args:
db_pool: PostgreSQL 连接池
db_url: 数据库连接 URL
api_key: Memori API 密钥
Returns:
MemoriManager 实例
"""
manager = get_memori_manager()
manager._db_pool = db_pool
manager._db_url = db_url
manager._api_key = api_key
await manager.initialize()
return manager
async def close_global_memori() -> None:
"""关闭全局 MemoriManager"""
global _global_manager
if _global_manager is not None:
await _global_manager.close()

342
agent/memori_middleware.py Normal file
View File

@ -0,0 +1,342 @@
"""
Memori Agent 中间件
实现记忆召回和存储的 AgentMiddleware
"""
import asyncio
import logging
from typing import Any, Dict, List, Optional
from langchain.agents.middleware import AgentMiddleware, AgentState
from langgraph.runtime import Runtime
from .memori_config import MemoriConfig
from .memori_manager import MemoriManager, get_memori_manager
logger = logging.getLogger("app")
class MemoriMiddleware(AgentMiddleware):
"""
Memori 记忆中间件
功能
1. before_agent: 召回相关记忆并注入到上下文
2. after_agent: 后台异步提取和存储新记忆
"""
def __init__(
self,
memori_manager: MemoriManager,
config: MemoriConfig,
):
"""初始化 MemoriMiddleware
Args:
memori_manager: MemoriManager 实例
config: MemoriConfig 配置
"""
self.memori_manager = memori_manager
self.config = config
def _extract_user_query(self, state: AgentState) -> str:
"""从状态中提取用户查询
Args:
state: Agent 状态
Returns:
用户查询文本
"""
messages = state.get("messages", [])
if not messages:
return ""
# 获取最后一条消息
last_message = messages[-1]
# 尝试获取内容
content = getattr(last_message, "content", None)
if content is None:
content = last_message.get("content", "") if isinstance(last_message, dict) else ""
return str(content) if content else ""
def _format_memories(self, memories: List[Dict[str, Any]]) -> str:
"""格式化记忆列表为文本
Args:
memories: 记忆列表
Returns:
格式化的记忆文本
"""
if not memories:
return ""
lines = []
for i, memory in enumerate(memories, 1):
content = memory.get("content", "")
similarity = memory.get("similarity", 0.0)
fact_type = memory.get("fact_type", "fact")
# 添加相似度分数(调试用)
lines.append(f"{i}. [{fact_type}] {content}")
return "\n".join(lines)
def _inject_memory_context(self, state: AgentState, memory_text: str) -> AgentState:
"""将记忆上下文注入到状态中
Args:
state: 原始状态
memory_text: 记忆文本
Returns:
更新后的状态
"""
if not memory_text or not self.config.inject_memory_to_system_prompt:
return state
# 生成记忆提示
memory_prompt = self.config.get_memory_prompt([memory_text])
# 检查是否有系统消息
messages = state.get("messages", [])
if not messages:
return state
# 在系统消息后添加记忆上下文
from langchain_core.messages import SystemMessage
# 查找系统消息
system_message = None
for msg in messages:
if hasattr(msg, "type") and msg.type == "system":
system_message = msg
break
elif isinstance(msg, dict) and msg.get("role") == "system":
system_message = msg
break
if system_message:
# 修改现有系统消息
if hasattr(system_message, "content"):
original_content = system_message.content
system_message.content = original_content + memory_prompt
elif isinstance(system_message, dict):
original_content = system_message.get("content", "")
system_message["content"] = original_content + memory_prompt
else:
# 添加新的系统消息
new_messages = list(messages)
new_messages.insert(0, SystemMessage(content=memory_prompt))
state = {**state, "messages": new_messages}
return state
def before_agent(self, state: AgentState, runtime: Runtime) -> Dict[str, Any] | None:
"""Agent 执行前:召回相关记忆(同步版本)
Args:
state: Agent 状态
runtime: 运行时上下文
Returns:
更新后的状态或 None
"""
if not self.config.is_enabled():
return None
try:
# 提取用户查询
query = self._extract_user_query(state)
if not query:
return None
# 获取 attribution 参数
entity_id, process_id = self.config.get_attribution_tuple()
session_id = self.config.session_id or runtime.config.get("configurable", {}).get("thread_id", "default")
# 召回记忆(同步方式 - 在后台任务中执行)
memories = asyncio.run(self._recall_memories_async(query, entity_id, process_id, session_id))
if memories:
# 格式化记忆
memory_text = self._format_memories(memories)
# 注入到状态
updated_state = self._inject_memory_context(state, memory_text)
logger.info(f"Injected {len(memories)} memories into context")
return updated_state
return None
except Exception as e:
logger.error(f"Error in MemoriMiddleware.before_agent: {e}")
return None
async def abefore_agent(self, state: AgentState, runtime: Runtime) -> Dict[str, Any] | None:
"""Agent 执行前:召回相关记忆(异步版本)
Args:
state: Agent 状态
runtime: 运行时上下文
Returns:
更新后的状态或 None
"""
if not self.config.is_enabled():
return None
try:
# 提取用户查询
query = self._extract_user_query(state)
if not query:
logger.debug("No user query found, skipping memory recall")
return None
# 获取 attribution 参数
entity_id, process_id = self.config.get_attribution_tuple()
session_id = self.config.session_id or runtime.config.get("configurable", {}).get("thread_id", "default")
# 召回记忆
memories = await self._recall_memories_async(query, entity_id, process_id, session_id)
if memories:
# 格式化记忆
memory_text = self._format_memories(memories)
# 注入到状态
updated_state = self._inject_memory_context(state, memory_text)
logger.info(f"Injected {len(memories)} memories into context (similarity > {self.config.semantic_search_threshold})")
return updated_state
return None
except Exception as e:
logger.error(f"Error in MemoriMiddleware.abefore_agent: {e}")
return None
async def _recall_memories_async(
self, query: str, entity_id: str, process_id: str, session_id: str
) -> List[Dict[str, Any]]:
"""异步召回记忆
Args:
query: 查询文本
entity_id: 实体 ID
process_id: 进程 ID
session_id: 会话 ID
Returns:
记忆列表
"""
return await self.memori_manager.recall_memories(
query=query,
entity_id=entity_id,
process_id=process_id,
session_id=session_id,
config=self.config,
)
def after_agent(self, state: AgentState, runtime: Runtime) -> None:
"""Agent 执行后:触发记忆增强(同步版本)
Args:
state: Agent 状态
runtime: 运行时上下文
"""
if not self.config.is_enabled() or not self.config.augmentation_enabled:
return
try:
# 触发后台增强任务
asyncio.create_task(self._trigger_augmentation_async(state, runtime))
except Exception as e:
logger.error(f"Error in MemoriMiddleware.after_agent: {e}")
async def aafter_agent(self, state: AgentState, runtime: Runtime) -> None:
"""Agent 执行后:触发记忆增强(异步版本)
注意Memori 的增强会自动在后台执行这里主要是记录日志
Args:
state: Agent 状态
runtime: 运行时上下文
"""
if not self.config.is_enabled() or not self.config.augmentation_enabled:
return
try:
# 如果配置了等待超时,则等待增强完成
if self.config.augmentation_wait_timeout is not None:
entity_id, process_id = self.config.get_attribution_tuple()
session_id = self.config.session_id or runtime.config.get("configurable", {}).get("thread_id", "default")
await self.memori_manager.wait_for_augmentation(
entity_id=entity_id,
process_id=process_id,
session_id=session_id,
timeout=self.config.augmentation_wait_timeout,
)
except Exception as e:
logger.error(f"Error in MemoriMiddleware.aafter_agent: {e}")
async def _trigger_augmentation_async(self, state: AgentState, runtime: Runtime) -> None:
"""触发记忆增强任务
注意Memori LLM 客户端注册后会自动捕获对话并进行增强
这里不需要手动触发只是确保会话正确设置
Args:
state: Agent 状态
runtime: 运行时上下文
"""
# Memori 的增强是自动的,这里主要是确保配置正确
# 如果需要手动触发,可以在这里实现
pass
def create_memori_middleware(
bot_id: str,
user_identifier: str,
session_id: str,
enabled: bool = True,
semantic_search_top_k: int = 5,
semantic_search_threshold: float = 0.7,
memori_manager: Optional[MemoriManager] = None,
) -> Optional[MemoriMiddleware]:
"""创建 MemoriMiddleware 的工厂函数
Args:
bot_id: Bot ID
user_identifier: 用户标识
session_id: 会话 ID
enabled: 是否启用
semantic_search_top_k: 语义搜索返回数量
semantic_search_threshold: 语义搜索相似度阈值
memori_manager: MemoriManager 实例如果为 None使用全局实例
Returns:
MemoriMiddleware 实例或 None
"""
if not enabled:
return None
# 获取或使用提供的 manager
manager = memori_manager or get_memori_manager()
# 创建配置
config = MemoriConfig(
enabled=True,
entity_id=user_identifier,
process_id=bot_id,
session_id=session_id,
semantic_search_top_k=semantic_search_top_k,
semantic_search_threshold=semantic_search_threshold,
)
return MemoriMiddleware(memori_manager=manager, config=config)

349
poetry.lock generated
View File

@ -291,6 +291,26 @@ charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"] html5lib = ["html5lib"]
lxml = ["lxml"] lxml = ["lxml"]
[[package]]
name = "botocore"
version = "1.42.30"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02"},
{file = "botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116"},
]
[package.dependencies]
jmespath = ">=0.7.1,<2.0.0"
python-dateutil = ">=2.1,<3.0.0"
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
[package.extras]
crt = ["awscrt (==0.29.2)"]
[[package]] [[package]]
name = "bracex" name = "bracex"
version = "2.6" version = "2.6"
@ -543,12 +563,12 @@ version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"] groups = ["main", "dev"]
markers = "platform_system == \"Windows\""
files = [ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""}
[[package]] [[package]]
name = "daytona" name = "daytona"
@ -778,6 +798,58 @@ files = [
{file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"},
] ]
[[package]]
name = "faiss-cpu"
version = "1.11.0.post1"
description = "A library for efficient similarity search and clustering of dense vectors."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "faiss_cpu-1.11.0.post1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:e079d44ea22919f6477fea553b05854c68838ab553e1c6b1237437a8becdf89d"},
{file = "faiss_cpu-1.11.0.post1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:4ded0c91cb67f462ae00a4d339718ea2fbb23eedbf260c3a07de77c32c23205a"},
{file = "faiss_cpu-1.11.0.post1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78812f4d7ff9d3773f50009efcf294f3da787cd8c835c1fc41d997a58100f7b5"},
{file = "faiss_cpu-1.11.0.post1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:76b133d746ddb3e6d39e6de62ff717cf4d45110d4af101a62d6a4fed4cd1d4d1"},
{file = "faiss_cpu-1.11.0.post1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9443bc89447f9988f2288477584d2f1c59424a5e9f9a202e4ada8708df816db1"},
{file = "faiss_cpu-1.11.0.post1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6acc20021b69bd30d3cb5cadb4f8dc1c338aec887cd5411b0982e8a3e48b3d7f"},
{file = "faiss_cpu-1.11.0.post1-cp310-cp310-win_amd64.whl", hash = "sha256:9dccf67d4087f9b0f937d4dccd1183929ebb6fe7622b75cba51b53e4f0055a0c"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2c8c384e65cc1b118d2903d9f3a27cd35f6c45337696fc0437f71e05f732dbc0"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:36af46945274ed14751b788673125a8a4900408e4837a92371b0cad5708619ea"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b15412b22a05865433aecfdebf7664b9565bd49b600d23a0a27c74a5526893e"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81c169ea74213b2c055b8240befe7e9b42a1f3d97cda5238b3b401035ce1a18b"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0794eb035c6075e931996cf2b2703fbb3f47c8c34bc2d727819ddc3e5e486a31"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18d2221014813dc9a4236e47f9c4097a71273fbf17c3fe66243e724e2018a67a"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:3ce8a8984a7dcc689fd192c69a476ecd0b2611c61f96fe0799ff432aa73ff79c"},
{file = "faiss_cpu-1.11.0.post1-cp311-cp311-win_arm64.whl", hash = "sha256:8384e05afb7c7968e93b81566759f862e744c0667b175086efb3d8b20949b39f"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:68f6ce2d9c510a5765af2f5711bd76c2c37bd598af747f3300224bdccf45378c"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b940c530a8236cc0b9fd9d6e87b3d70b9c6c216bc2baf2649356c908902e52c9"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fafae1dcbcba3856a0bb82ffb0c3cae5922bdd6566fdd3b7feb2425cf4fca247"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d1262702c19aba2d23144b73f4b5730ca988c1f4e43ecec87edf25171cafe3d"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:925feb69c06bfcc7f28869c99ab172f123e4b9d97a7e1353316fcc2748696f5b"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:00a837581b675f099c80c8c46908648dcf944a8992dd21e3887c61c6b110fe5f"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-win_amd64.whl", hash = "sha256:8bbaef5b56d1b0c01357ee6449d464ea4e52732fdb53a40bb5b9d77923af905f"},
{file = "faiss_cpu-1.11.0.post1-cp312-cp312-win_arm64.whl", hash = "sha256:57f85dbefe590f8399a95c07e839ee64373cfcc6db5dd35232a41137e3deefeb"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:caedaddfbfe365e3f1a57d5151cf94ea7b73c0e4789caf68eae05e0e10ca9fbf"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:202d11f1d973224ca0bde13e7ee8b862b6de74287e626f9f8820b360e6253d12"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6086e25ef680301350d6db72db7315e3531582cf896a7ee3f26295b1da73c44"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b93131842996efbbf76f07dba1775d3a5f355f74b9ba34334f1149aef046b37f"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f26e3e93f537b2e1633212a1b0a7dab74d77825366ed575ca434dac2fa14cea6"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4b0e03cd758d03012d88aa4a70e673d10b66f31f7c122adc0c8c323cad2e33"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-win_amd64.whl", hash = "sha256:bc53fe59b546dbab63144dc19dcee534ad7a213db617b37aa4d0e33c26f9bbaf"},
{file = "faiss_cpu-1.11.0.post1-cp313-cp313-win_arm64.whl", hash = "sha256:9cebb720cd57afdbe9dd7ed8a689c65dc5cf1bad475c5aa6fa0d0daea890beb6"},
{file = "faiss_cpu-1.11.0.post1-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:3663059682589a42e3c4da0f3915492c466c886954cf9280273f92257bcfa0b4"},
{file = "faiss_cpu-1.11.0.post1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:0348794ae91fb1454f2cddf7a9c7de23510f2a63e60c0fba0ae73bc7bf23a060"},
{file = "faiss_cpu-1.11.0.post1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8baf46be73b4fce99f4620d99a52cdb01f7823a849f00064f02802f554d8b59f"},
{file = "faiss_cpu-1.11.0.post1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388a590ab2847e421ba2702ff2774835287f137fb77e24e679f0063c1c10a96f"},
{file = "faiss_cpu-1.11.0.post1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dc12b3f89cf48be3f2a20b37f310c3f1a7a5708fdf705f88d639339a24bb590b"},
{file = "faiss_cpu-1.11.0.post1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:773fa45aa98a210ab4e2c17c1b5fb45f6d7e9acb4979c9a0b320b678984428ac"},
{file = "faiss_cpu-1.11.0.post1-cp39-cp39-win_amd64.whl", hash = "sha256:6240c4b1551eedc07e76813c2e14a1583a1db6c319a92a3934bf212d0e4c7791"},
]
[package.dependencies]
numpy = ">=1.25.0,<3.0"
packaging = "*"
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.116.1" version = "0.116.1"
@ -992,6 +1064,83 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""] test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""]
tqdm = ["tqdm"] tqdm = ["tqdm"]
[[package]]
name = "grpcio"
version = "1.76.0"
description = "HTTP/2-based RPC framework"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc"},
{file = "grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde"},
{file = "grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3"},
{file = "grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990"},
{file = "grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af"},
{file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2"},
{file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6"},
{file = "grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3"},
{file = "grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b"},
{file = "grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b"},
{file = "grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a"},
{file = "grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c"},
{file = "grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465"},
{file = "grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48"},
{file = "grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da"},
{file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397"},
{file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749"},
{file = "grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00"},
{file = "grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054"},
{file = "grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d"},
{file = "grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8"},
{file = "grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280"},
{file = "grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4"},
{file = "grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11"},
{file = "grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6"},
{file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8"},
{file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980"},
{file = "grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882"},
{file = "grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958"},
{file = "grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347"},
{file = "grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2"},
{file = "grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468"},
{file = "grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3"},
{file = "grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb"},
{file = "grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae"},
{file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77"},
{file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03"},
{file = "grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42"},
{file = "grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f"},
{file = "grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8"},
{file = "grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62"},
{file = "grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd"},
{file = "grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc"},
{file = "grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a"},
{file = "grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba"},
{file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09"},
{file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc"},
{file = "grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc"},
{file = "grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e"},
{file = "grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e"},
{file = "grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783"},
{file = "grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d"},
{file = "grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd"},
{file = "grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378"},
{file = "grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70"},
{file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416"},
{file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c"},
{file = "grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886"},
{file = "grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f"},
{file = "grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a"},
{file = "grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73"},
]
[package.dependencies]
typing-extensions = ">=4.12,<5.0"
[package.extras]
protobuf = ["grpcio-tools (>=1.76.0)"]
[[package]] [[package]]
name = "grpclib" name = "grpclib"
version = "0.4.8" version = "0.4.8"
@ -1213,6 +1362,18 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "iniconfig"
version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.6" version = "3.1.6"
@ -1343,6 +1504,18 @@ files = [
{file = "jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc"}, {file = "jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc"},
] ]
[[package]]
name = "jmespath"
version = "1.0.1"
description = "JSON Matching Expressions"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
{file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
[[package]] [[package]]
name = "joblib" name = "joblib"
version = "1.5.2" version = "1.5.2"
@ -1841,6 +2014,30 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
] ]
[[package]]
name = "memori"
version = "3.1.3"
description = "Memori Python SDK"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "memori-3.1.3-py3-none-any.whl", hash = "sha256:81934d61ecf0574ba948b9012f2cde52912f3d30887ad504408ecfa171734e36"},
{file = "memori-3.1.3.tar.gz", hash = "sha256:9f936c15ba0a684a1bc62619d7558af6a6e99171bf1ef1e6eb19bb952a9f6d94"},
]
[package.dependencies]
aiohttp = ">=3.9.0"
botocore = ">=1.34.0"
faiss-cpu = ">=1.7.0"
grpcio = ">=1.60.0"
numpy = ">=1.24.0"
protobuf = ">=4.25.0,<6.0.0"
psycopg = {version = ">=3.1.0", extras = ["binary"]}
pyfiglet = ">=0.8.0"
requests = ">=2.32.5"
sentence-transformers = ">=3.0.0"
[[package]] [[package]]
name = "modal" name = "modal"
version = "1.2.1" version = "1.2.1"
@ -2586,7 +2783,7 @@ version = "25.0"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main", "dev"]
files = [ files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
@ -2797,6 +2994,22 @@ test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"]
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
xmp = ["defusedxml"] xmp = ["defusedxml"]
[[package]]
name = "pluggy"
version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "prompt-toolkit" name = "prompt-toolkit"
version = "3.0.52" version = "3.0.52"
@ -2946,22 +3159,23 @@ files = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "6.33.2" version = "5.29.5"
description = "" description = ""
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d"}, {file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"},
{file = "protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4"}, {file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"},
{file = "protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43"}, {file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"},
{file = "protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e"}, {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"},
{file = "protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872"}, {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"},
{file = "protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f"}, {file = "protobuf-5.29.5-cp38-cp38-win32.whl", hash = "sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238"},
{file = "protobuf-6.33.2-cp39-cp39-win32.whl", hash = "sha256:7109dcc38a680d033ffb8bf896727423528db9163be1b6a02d6a49606dcadbfe"}, {file = "protobuf-5.29.5-cp38-cp38-win_amd64.whl", hash = "sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e"},
{file = "protobuf-6.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:2981c58f582f44b6b13173e12bb8656711189c2a70250845f264b877f00b1913"}, {file = "protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736"},
{file = "protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c"}, {file = "protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353"},
{file = "protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4"}, {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"},
{file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"},
] ]
[[package]] [[package]]
@ -3010,6 +3224,7 @@ files = [
] ]
[package.dependencies] [package.dependencies]
psycopg-binary = {version = "3.3.2", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""}
typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""}
tzdata = {version = "*", markers = "sys_platform == \"win32\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""}
@ -3021,6 +3236,72 @@ docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)"
pool = ["psycopg-pool"] pool = ["psycopg-pool"]
test = ["anyio (>=4.0)", "mypy (>=1.19.0) ; implementation_name != \"pypy\"", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] test = ["anyio (>=4.0)", "mypy (>=1.19.0) ; implementation_name != \"pypy\"", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
[[package]]
name = "psycopg-binary"
version = "3.3.2"
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
optional = false
python-versions = ">=3.10"
groups = ["main"]
markers = "implementation_name != \"pypy\""
files = [
{file = "psycopg_binary-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41"},
{file = "psycopg_binary-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4"},
{file = "psycopg_binary-3.3.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068"},
{file = "psycopg_binary-3.3.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e"},
{file = "psycopg_binary-3.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b"},
{file = "psycopg_binary-3.3.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391"},
{file = "psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c"},
{file = "psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8"},
{file = "psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186"},
{file = "psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19"},
{file = "psycopg_binary-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640"},
{file = "psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa"},
{file = "psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b"},
{file = "psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0"},
{file = "psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924"},
{file = "psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c"},
{file = "psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d"},
{file = "psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7"},
{file = "psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7"},
{file = "psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7"},
{file = "psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4"},
{file = "psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9"},
{file = "psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634"},
{file = "psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688"},
{file = "psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4"},
{file = "psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578"},
{file = "psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed"},
{file = "psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860"},
{file = "psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b"},
{file = "psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3"},
{file = "psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca"},
{file = "psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12"},
{file = "psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf"},
{file = "psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b"},
{file = "psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2"},
{file = "psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f"},
{file = "psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d"},
{file = "psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e"},
{file = "psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed"},
{file = "psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30"},
{file = "psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d"},
{file = "psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da"},
{file = "psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121"},
{file = "psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc"},
{file = "psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390"},
{file = "psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443"},
{file = "psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9"},
{file = "psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c"},
{file = "psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a"},
{file = "psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d"},
{file = "psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0"},
{file = "psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14"},
{file = "psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678"},
{file = "psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e"},
{file = "psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1"},
]
[[package]] [[package]]
name = "psycopg-pool" name = "psycopg-pool"
version = "3.3.0" version = "3.3.0"
@ -3197,13 +3478,25 @@ gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
toml = ["tomli (>=2.0.1)"] toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"] yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pyfiglet"
version = "1.0.4"
description = "Pure-python FIGlet implementation"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pyfiglet-1.0.4-py3-none-any.whl", hash = "sha256:65b57b7a8e1dff8a67dc8e940a117238661d5e14c3e49121032bd404d9b2b39f"},
{file = "pyfiglet-1.0.4.tar.gz", hash = "sha256:db9c9940ed1bf3048deff534ed52ff2dafbbc2cd7610b17bb5eca1df6d4278ef"},
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.19.2" version = "2.19.2"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main", "dev"]
files = [ files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
@ -3212,6 +3505,28 @@ files = [
[package.extras] [package.extras]
windows-terminal = ["colorama (>=0.4.6)"] windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pytest"
version = "9.0.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"},
{file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
iniconfig = ">=1.0.1"
packaging = ">=22"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.2" version = "2.8.2"
@ -5428,4 +5743,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.12,<4.0" python-versions = ">=3.12,<4.0"
content-hash = "02ca6f912b2b258fd07ba45bece31692b3f8a937d4c77d4f50714fe49eeab77e" content-hash = "745e14c58bdedcdd10459dc47d9c8f59b8a5a04f4b02f1a9d6e01298a3c19f67"

View File

@ -32,6 +32,7 @@ dependencies = [
"cachetools (>=6.2.4,<7.0.0)", "cachetools (>=6.2.4,<7.0.0)",
"langgraph-checkpoint-postgres (>=2.0.0,<3.0.0)", "langgraph-checkpoint-postgres (>=2.0.0,<3.0.0)",
"deepagents-cli (>=0.0.11,<0.0.12)", "deepagents-cli (>=0.0.11,<0.0.12)",
"memori (>=3.1.0,<4.0.0)",
] ]
[tool.poetry.requires-plugins] [tool.poetry.requires-plugins]
@ -45,3 +46,7 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[dependency-groups]
dev = [
"pytest (>=9.0.2,<10.0.0)"
]

View File

@ -0,0 +1,75 @@
"""
Memori 配置测试
"""
import pytest
from agent.memori_config import MemoriConfig
class TestMemoriConfig:
"""测试 MemoriConfig 配置类"""
def test_default_values(self):
"""测试默认值"""
config = MemoriConfig()
assert config.enabled is False
assert config.api_key is None
assert config.semantic_search_top_k == 5
assert config.semantic_search_threshold == 0.7
assert config.inject_memory_to_system_prompt is True
def test_enabled_check(self):
"""测试 is_enabled 方法"""
config = MemoriConfig(enabled=False)
assert config.is_enabled() is False
config.enabled = True
assert config.is_enabled() is True
def test_get_attribution_tuple(self):
"""测试 get_attribution_tuple 方法"""
config = MemoriConfig(
entity_id="user_123",
process_id="bot_456"
)
entity_id, process_id = config.get_attribution_tuple()
assert entity_id == "user_123"
assert process_id == "bot_456"
def test_get_attribution_tuple_missing_values(self):
"""测试缺少 entity_id 或 process_id 时抛出异常"""
config = MemoriConfig(entity_id="user_123")
with pytest.raises(ValueError, match="entity_id and process_id are required"):
config.get_attribution_tuple()
def test_get_memory_prompt(self):
"""测试 get_memory_prompt 方法"""
config = MemoriConfig()
memories = ["User likes coffee", "User prefers dark mode"]
prompt = config.get_memory_prompt(memories)
assert "相关记忆" in prompt
assert "User likes coffee" in prompt
assert "User prefers dark mode" in prompt
def test_get_memory_prompt_empty(self):
"""测试空记忆列表"""
config = MemoriConfig()
prompt = config.get_memory_prompt([])
assert prompt == ""
def test_with_session(self):
"""测试 with_session 方法"""
config = MemoriConfig(
entity_id="user_123",
process_id="bot_456",
session_id="session_789"
)
new_config = config.with_session("new_session_999")
assert new_config.session_id == "new_session_999"
assert new_config.entity_id == "user_123"
assert new_config.process_id == "bot_456"
# 原配置不受影响
assert config.session_id == "session_789"

View File

@ -0,0 +1,65 @@
"""
Memori 管理器测试
"""
import pytest
from agent.memori_manager import MemoriManager, get_memori_manager
class TestMemoriManager:
"""测试 MemoriManager 类"""
def test_singleton(self):
"""测试全局单例模式"""
manager1 = get_memori_manager()
manager2 = get_memori_manager()
assert manager1 is manager2
def test_initialization_state(self):
"""测试初始状态"""
manager = MemoriManager()
assert manager._initialized is False
assert manager._instances == {}
assert manager._sync_engines == {}
def test_clear_cache_all(self):
"""测试清除所有缓存"""
manager = MemoriManager()
manager._instances["key1"] = "value1"
manager._instances["key2"] = "value2"
manager.clear_cache()
assert manager._instances == {}
def test_clear_cache_selective(self):
"""测试选择性清除缓存"""
manager = MemoriManager()
manager._instances["user1:bot1"] = "value1"
manager._instances["user1:bot2"] = "value2"
manager._instances["user2:bot1"] = "value3"
# 只清除 user1 的缓存
manager.clear_cache(entity_id="user1")
assert "user1:bot1" not in manager._instances
assert "user1:bot2" not in manager._instances
assert "user2:bot1" in manager._instances
# 只清除 bot1 的缓存
manager.clear_cache(process_id="bot1")
assert "user2:bot1" not in manager._instances
def test_missing_db_url_and_pool(self):
"""测试没有提供 db_url 和 pool 的情况"""
import pytest
config = {
"entity_id": "user_123",
"process_id": "bot_456",
"session_id": "session_789",
}
# 需要 mock _create_memori_instance 或提供 db_url
# 这里只测试初始化逻辑
manager = MemoriManager()
assert manager._db_url is None
assert manager._db_pool is None

View File

@ -0,0 +1,120 @@
"""
Memori 中间件测试
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch
from langchain.agents.middleware import AgentState
from langgraph.runtime import Runtime
from agent.memori_middleware import MemoriMiddleware, create_memori_middleware
from agent.memori_config import MemoriConfig
class TestMemoriMiddleware:
"""测试 MemoriMiddleware 类"""
def test_extract_user_query_empty(self):
"""测试空状态"""
config = MemoriConfig(
enabled=True,
entity_id="user_123",
process_id="bot_456",
)
manager = Mock()
middleware = MemoriMiddleware(manager, config)
state: AgentState = {"messages": []}
query = middleware._extract_user_query(state)
assert query == ""
def test_extract_user_query_from_message(self):
"""测试从消息中提取查询"""
config = MemoriConfig(
enabled=True,
entity_id="user_123",
process_id="bot_456",
)
manager = Mock()
middleware = MemoriMiddleware(manager, config)
# 创建模拟消息
mock_message = Mock()
mock_message.content = "What is the weather today?"
state: AgentState = {"messages": [mock_message]}
query = middleware._extract_user_query(state)
assert query == "What is the weather today?"
def test_extract_user_query_from_dict(self):
"""测试从字典消息中提取查询"""
config = MemoriConfig(
enabled=True,
entity_id="user_123",
process_id="bot_456",
)
manager = Mock()
middleware = MemoriMiddleware(manager, config)
state: AgentState = {"messages": [{"content": "Hello, world!"}]}
query = middleware._extract_user_query(state)
assert query == "Hello, world!"
def test_format_memories(self):
"""测试记忆格式化"""
config = MemoriConfig(
enabled=True,
entity_id="user_123",
process_id="bot_456",
)
manager = Mock()
middleware = MemoriMiddleware(manager, config)
memories = [
{"content": "User likes coffee", "similarity": 0.9, "fact_type": "preference"},
{"content": "User lives in Tokyo", "similarity": 0.8, "fact_type": "fact"},
]
formatted = middleware._format_memories(memories)
assert "User likes coffee" in formatted
assert "User lives in Tokyo" in formatted
assert "[preference]" in formatted
assert "[fact]" in formatted
def test_format_memories_empty(self):
"""测试空记忆列表"""
config = MemoriConfig(
enabled=True,
entity_id="user_123",
process_id="bot_456",
)
manager = Mock()
middleware = MemoriMiddleware(manager, config)
formatted = middleware._format_memories([])
assert formatted == ""
def test_create_memori_middleware_disabled(self):
"""测试创建中间件(禁用状态)"""
middleware = create_memori_middleware(
bot_id="bot_456",
user_identifier="user_123",
session_id="session_789",
enabled=False,
)
assert middleware is None
def test_create_memori_middleware_enabled(self):
"""测试创建中间件(启用状态)"""
middleware = create_memori_middleware(
bot_id="bot_456",
user_identifier="user_123",
session_id="session_789",
enabled=True,
semantic_search_top_k=10,
semantic_search_threshold=0.8,
)
assert middleware is not None
assert isinstance(middleware, MemoriMiddleware)
assert middleware.config.semantic_search_top_k == 10
assert middleware.config.semantic_search_threshold == 0.8

View File

@ -67,3 +67,43 @@ CHECKPOINT_CLEANUP_INACTIVE_DAYS = int(os.getenv("CHECKPOINT_CLEANUP_INACTIVE_DA
CHECKPOINT_CLEANUP_INTERVAL_HOURS = int(os.getenv("CHECKPOINT_CLEANUP_INTERVAL_HOURS", "24")) CHECKPOINT_CLEANUP_INTERVAL_HOURS = int(os.getenv("CHECKPOINT_CLEANUP_INTERVAL_HOURS", "24"))
# ============================================================
# Memori 长期记忆配置
# ============================================================
# Memori 功能开关(全局)
MEMORI_ENABLED = os.getenv("MEMORI_ENABLED", "false") == "true"
# Memori API 密钥(用于高级增强功能)
MEMORI_API_KEY = os.getenv("MEMORI_API_KEY", "")
# 语义搜索配置
# 召回记忆数量
MEMORI_SEMANTIC_SEARCH_TOP_K = int(os.getenv("MEMORI_SEMANTIC_SEARCH_TOP_K", "5"))
# 相关性阈值0.0 - 1.0
MEMORI_SEMANTIC_SEARCH_THRESHOLD = float(os.getenv("MEMORI_SEMANTIC_SEARCH_THRESHOLD", "0.7"))
# 搜索嵌入限制
MEMORI_SEMANTIC_SEARCH_EMBEDDINGS_LIMIT = int(os.getenv("MEMORI_SEMANTIC_SEARCH_EMBEDDINGS_LIMIT", "1000"))
# 记忆注入配置
# 是否将记忆注入到系统提示
MEMORI_INJECT_TO_SYSTEM_PROMPT = os.getenv("MEMORI_INJECT_TO_SYSTEM_PROMPT", "true") == "true"
# 增强配置
# 是否启用后台增强
MEMORI_AUGMENTATION_ENABLED = os.getenv("MEMORI_AUGMENTATION_ENABLED", "true") == "true"
# 增强等待超时None 表示后台异步执行
MEMORI_AUGMENTATION_WAIT_TIMEOUT = os.getenv("MEMORI_AUGMENTATION_WAIT_TIMEOUT")
if MEMORI_AUGMENTATION_WAIT_TIMEOUT:
MEMORI_AUGMENTATION_WAIT_TIMEOUT = float(MEMORI_AUGMENTATION_WAIT_TIMEOUT)
else:
MEMORI_AUGMENTATION_WAIT_TIMEOUT = None
# 嵌入模型(多语言支持)
MEMORI_EMBEDDING_MODEL = os.getenv(
"MEMORI_EMBEDDING_MODEL",
"paraphrase-multilingual-MiniLM-L12-v2"
)