diff --git a/docker/requirements.txt b/docker/requirements.txt index d3268edae..f522dd3b6 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -160,3 +160,5 @@ xlrd==2.0.2 xlsxwriter==3.2.5 prometheus-client==0.23.1 pymilvus==2.5.12 +nltk==3.9.1 +rake-nltk==1.0.6 diff --git a/poetry.lock b/poetry.lock index bdb962f86..dc061b2f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "absl-py" @@ -2469,7 +2469,7 @@ version = "3.9.1" description = "Natural Language Toolkit" optional = false python-versions = ">=3.8" -groups = ["eval"] +groups = ["main", "eval"] files = [ {file = "nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1"}, {file = "nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868"}, @@ -4031,6 +4031,22 @@ urllib3 = ">=1.26.14,<3" fastembed = ["fastembed (>=0.7,<0.8)"] fastembed-gpu = ["fastembed-gpu (>=0.7,<0.8)"] +[[package]] +name = "rake-nltk" +version = "1.0.6" +description = "RAKE short for Rapid Automatic Keyword Extraction algorithm, is a domain independent keyword extraction algorithm which tries to determine key phrases in a body of text by analyzing the frequency of word appearance and its co-occurance with other words in the text." +optional = true +python-versions = ">=3.6,<4.0" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "rake-nltk-1.0.6.tar.gz", hash = "sha256:7813d680b2ce77b51cdac1757f801a87ff47682c9dbd2982aea3b66730346122"}, + {file = "rake_nltk-1.0.6-py3-none-any.whl", hash = "sha256:1c1ffdb64cae8cb99d169d53a5ffa4635f1c4abd3a02c6e22d5d083136bdc5c1"}, +] + +[package.dependencies] +nltk = ">=3.6.2,<4.0.0" + [[package]] name = "rank-bm25" version = "0.2.2" @@ -6216,7 +6232,7 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -all = ["cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "pika", "pymilvus", "pymysql", "qdrant-client", "rank-bm25", "redis", "schedule", "sentence-transformers", "torch", "volcengine-python-sdk"] +all = ["cachetools", "chonkie", "datasketch", "jieba", "langchain-text-splitters", "markitdown", "neo4j", "nltk", "pika", "pymilvus", "pymysql", "qdrant-client", "rake-nltk", "rank-bm25", "redis", "schedule", "sentence-transformers", "torch", "volcengine-python-sdk"] mem-reader = ["chonkie", "langchain-text-splitters", "markitdown"] mem-scheduler = ["pika", "redis"] mem-user = ["pymysql"] @@ -6226,4 +6242,4 @@ tree-mem = ["neo4j", "schedule"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "04c7b73bd8063f6c8ea8ed6a60b23d59a06de50b8607aff06581cc0e40192e38" +content-hash = "dab8e54c6f4c51597adbd0fa34be7a8adb3b3a9c733508f3cc2b93c0ed434ec1" diff --git a/pyproject.toml b/pyproject.toml index 74dfefc09..7358bdcbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,8 @@ all = [ "sentence-transformers (>=4.1.0,<5.0.0)", "qdrant-client (>=1.14.2,<2.0.0)", "volcengine-python-sdk (>=4.0.4,<5.0.0)", + "nltk (>=3.9.1,<4.0.0)", + "rake-nltk (>=1.0.6,<1.1.0)", # Uncategorized dependencies ] diff --git a/src/memos/api/handlers/chat_handler.py b/src/memos/api/handlers/chat_handler.py index 85a92c68c..614046dd6 100644 --- a/src/memos/api/handlers/chat_handler.py +++ b/src/memos/api/handlers/chat_handler.py @@ -395,16 +395,6 @@ def generate_chat_response() -> Generator[str, None, None]: [chat_req.mem_cube_id] if chat_req.mem_cube_id else [chat_req.user_id] ) - # for playground, add the query to memory without response - self._start_add_to_memory( - user_id=chat_req.user_id, - writable_cube_ids=writable_cube_ids, - session_id=chat_req.session_id or "default_session", - query=chat_req.query, - full_response=None, - async_mode="sync", - ) - # ====== first search text mem with parse goal ====== search_req = APISearchPlaygroundRequest( query=chat_req.query, @@ -450,7 +440,7 @@ def generate_chat_response() -> Generator[str, None, None]: pref_list = search_response.data.get("pref_mem") or [] pref_memories = pref_list[0].get("memories", []) if pref_list else [] pref_md_string = self._build_pref_md_string_for_playground(pref_memories) - yield f"data: {json.dumps({'type': 'pref_md_string', 'data': pref_md_string})}\n\n" + yield f"data: {json.dumps({'type': 'pref_md_string', 'data': pref_md_string}, ensure_ascii=False)}\n\n" # Use first readable cube ID for scheduler (backward compatibility) scheduler_cube_id = ( @@ -531,6 +521,16 @@ def generate_chat_response() -> Generator[str, None, None]: ) yield f"data: {json.dumps({'type': 'reference', 'data': reference})}\n\n" + # for playground, add the query to memory without response + self._start_add_to_memory( + user_id=chat_req.user_id, + writable_cube_ids=writable_cube_ids, + session_id=chat_req.session_id or "default_session", + query=chat_req.query, + full_response=None, + async_mode="sync", + ) + # Step 2: Build system prompt with memories system_prompt = self._build_enhance_system_prompt( filtered_memories, pref_string @@ -794,7 +794,7 @@ def _build_enhance_system_prompt( sys_body + "\n\n# Memories\n## PersonalMemory (ordered)\n" + mem_block_p - + "\n## OuterMemory (ordered)\n" + + "\n## OuterMemory (from Internet Search, ordered)\n" + mem_block_o + f"\n\n{pref_string}" ) diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py b/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py index 133a85631..a4aeca498 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/bochasearch.py @@ -9,9 +9,11 @@ import requests from memos.context.context import ContextThreadPoolExecutor +from memos.dependency import require_python_package from memos.embedders.factory import OllamaEmbedder from memos.log import get_logger from memos.mem_reader.base import BaseMemReader +from memos.mem_reader.read_multi_modal import detect_lang from memos.memories.textual.item import ( SearchedTreeNodeTextualMemoryMetadata, SourceMessage, @@ -121,6 +123,21 @@ def _post(self, url: str, body: dict) -> list[dict]: class BochaAISearchRetriever: """BochaAI retriever that converts search results into TextualMemoryItem objects""" + @require_python_package( + import_name="rake_nltk", + install_command="pip install rake_nltk", + install_link="https://pypi.org/project/rake-nltk/", + ) + @require_python_package( + import_name="nltk", + install_command="pip install nltk", + install_link="https://www.nltk.org/install.html", + ) + @require_python_package( + import_name="jieba", + install_command="pip install jieba", + install_link="https://github.com/fxsjy/jieba", + ) def __init__( self, access_key: str, @@ -137,9 +154,25 @@ def __init__( reader: MemReader instance for processing internet content max_results: Maximum number of search results to retrieve """ + import nltk + + try: + nltk.download("averaged_perceptron_tagger_eng") + except Exception as err: + raise Exception("Failed to download nltk averaged_perceptron_tagger_eng") from err + try: + nltk.download("stopwords") + except Exception as err: + raise Exception("Failed to download nltk stopwords") from err + + from jieba.analyse import TextRank + from rake_nltk import Rake + self.bocha_api = BochaAISearchAPI(access_key, max_results=max_results) self.embedder = embedder self.reader = reader + self.en_fast_keywords_extractor = Rake() + self.zh_fast_keywords_extractor = TextRank() def retrieve_from_internet( self, query: str, top_k: int = 10, parsed_goal=None, info=None, mode="fast" @@ -224,6 +257,13 @@ def _process_result( info_ = info.copy() user_id = info_.pop("user_id", "") session_id = info_.pop("session_id", "") + lang = detect_lang(summary) + tags = ( + self.zh_fast_keywords_extractor.textrank(summary)[:3] + if lang == "zh" + else self.en_fast_keywords_extractor.extract_keywords_from_text(summary)[:3] + ) + return [ TextualMemoryItem( memory=( @@ -244,6 +284,7 @@ def _process_result( background="", confidence=0.99, usage=[], + tags=tags, embedding=self.embedder.embed([content])[0], internet_info={ "title": title, diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/utils.py b/src/memos/memories/textual/tree_text_memory/retrieve/utils.py index 55c6243d8..8659b6112 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/utils.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/utils.py @@ -4,7 +4,7 @@ 1. Keys: the high-level keywords directly relevant to the user’s task. 2. Tags: thematic tags to help categorize and retrieve related memories. 3. Goal Type: retrieval | qa | generation -4. Rephrased instruction: Give a rephrased task instruction based on the former conversation to make it less confusing to look alone. Make full use of information related to the query. If you think the task instruction is easy enough to understand, or there is no former conversation, set "rephrased_instruction" to an empty string. +4. Rephrased instruction: Give a rephrased task instruction based on the former conversation to make it less confusing to look alone. Make full use of information related to the query, including user's personal information. If you think the task instruction is easy enough to understand, or there is no former conversation, set "rephrased_instruction" to an empty string. 5. Need for internet search: If the user's task instruction only involves objective facts or can be completed without introducing external knowledge, set "internet_search" to False. Otherwise, set it to True. 6. Memories: Provide 2–5 short semantic expansions or rephrasings of the rephrased/original user task instruction. These are used for improved embedding search coverage. Each should be clear, concise, and meaningful for retrieval. diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py b/src/memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py index ab12a0647..c8f8e4576 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py @@ -347,6 +347,7 @@ def _process_result( source="web", sources=[SourceMessage(type="web", url=url)] if url else [], visibility="public", + tags=self._extract_tags(title, content, summary), info=info_, background="", confidence=0.99, diff --git a/src/memos/templates/mos_prompts.py b/src/memos/templates/mos_prompts.py index 15f1a44b3..0d8b3019b 100644 --- a/src/memos/templates/mos_prompts.py +++ b/src/memos/templates/mos_prompts.py @@ -65,7 +65,6 @@ MEMOS_PRODUCT_BASE_PROMPT = """ # System - Role: You are MemOS🧚, nickname Little M(小忆🧚) — an advanced Memory Operating System assistant by 记忆张量(MemTensor Technology Co., Ltd.), a Shanghai-based AI research company advised by an academician of the Chinese Academy of Sciences. -- Date: {date} - Mission & Values: Uphold MemTensor’s vision of "low cost, low hallucination, high generalization, exploring AI development paths aligned with China’s national context and driving the adoption of trustworthy AI technologies. MemOS’s mission is to give large language models (LLMs) and autonomous agents **human-like long-term memory**, turning memory from a black-box inside model weights into a **manageable, schedulable, and auditable** core resource. @@ -105,12 +104,14 @@ - When using facts from memories, add citations at the END of the sentence with `[i:memId]`. - `i` is the order in the "Memories" section below (starting at 1). `memId` is the given short memory ID. - Multiple citations must be concatenated directly, e.g., `[1:sed23s], [ -2:1k3sdg], [3:ghi789]`. Do NOT use commas inside brackets. +2:1k3sdg], [3:ghi789]`. Do NOT use commas inside brackets. Do not use wrong format like `[def456]`. - Cite only relevant memories; keep citations minimal but sufficient. - Do not use a connected format like [1:abc123,2:def456]. - Brackets MUST be English half-width square brackets `[]`, NEVER use Chinese full-width brackets `【】` or any other symbols. - **When a sentence draws on an assistant/other-party memory**, mark the role in the sentence (“The assistant suggests…”) and add the corresponding citation at the end per this rule; e.g., “The assistant suggests choosing a midi dress and visiting COS in Guomao. [1:abc123]” +# Current Date: {date} + # Style - Tone: {tone}; Verbosity: {verbosity}. - Be direct, well-structured, and conversational. Avoid fluff. Use short lists when helpful.