From 6d85044d26cfe5c1207bb7ab493d83ceb244cdf9 Mon Sep 17 00:00:00 2001 From: harvey_xiang Date: Fri, 19 Sep 2025 17:11:54 +0800 Subject: [PATCH 1/7] feat: add api client --- src/memos/api/client.py | 109 ++++++++++++++++++++++++++++++++ src/memos/api/product_models.py | 101 +++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 src/memos/api/client.py diff --git a/src/memos/api/client.py b/src/memos/api/client.py new file mode 100644 index 000000000..2cb8faab5 --- /dev/null +++ b/src/memos/api/client.py @@ -0,0 +1,109 @@ +import json +import os + +from typing import Any + +import requests + +from memos.api.product_models import MemOSAddResponse, MemOSGetMessagesResponse, MemOSSearchResponse +from memos.log import get_logger + + +logger = get_logger(__name__) + +MAX_RETRY_COUNT = 3 + + +class MemOSClient: + """MemOS API client""" + + def __init__(self, api_key: str | None = None, base_url: str | None = None): + self.base_url = ( + base_url or os.getenv("MEMOS_BASE_URL") or "https://memos.memtensor.cn/api/openmem" + ) + api_key = api_key or os.getenv("MEMOS_API_KEY") + + if not api_key: + raise ValueError("MemOS API key is required") + + self.headers = {"Content-Type": "application/json", "Authorization": f"Token {api_key}"} + + def _validate_required_params(self, **params): + """Validate required parameters - if passed, they must not be empty""" + for param_name, param_value in params.items(): + if not param_value: + raise ValueError(f"{param_name} is required") + + def get_messages( + self, user_id: str, conversation_id: str | None = None + ) -> MemOSGetMessagesResponse: + """Get messages""" + # Validate required parameters + self._validate_required_params(user_id=user_id) + + url = f"{self.base_url}/get/message" + payload = {"userId": user_id, "conversationId": conversation_id} + for retry in range(MAX_RETRY_COUNT): + try: + response = requests.post( + url, data=json.dumps(payload), headers=self.headers, timeout=30 + ) + response.raise_for_status() + response_data = response.json() + return MemOSGetMessagesResponse(**response_data) + except Exception as e: + logger.error(f"Failed to get messages (retry {retry + 1}/3): {e}") + if retry == MAX_RETRY_COUNT - 1: + raise + + def add( + self, messages: list[dict[str, Any]], user_id: str, conversation_id: str + ) -> MemOSAddResponse: + """Add memories""" + # Validate required parameters + self._validate_required_params( + messages=messages, user_id=user_id, conversation_id=conversation_id + ) + + url = f"{self.base_url}/add/message" + payload = {"messages": messages, "userId": user_id, "conversationId": conversation_id} + for retry in range(MAX_RETRY_COUNT): + try: + response = requests.post( + url, data=json.dumps(payload), headers=self.headers, timeout=30 + ) + response.raise_for_status() + response_data = response.json() + return MemOSAddResponse(**response_data) + except Exception as e: + logger.error(f"Failed to add memory (retry {retry + 1}/3): {e}") + if retry == MAX_RETRY_COUNT - 1: + raise + + def search( + self, query: str, user_id: str, conversation_id: str, memory_limit_number: int = 6 + ) -> MemOSSearchResponse: + """Search memories""" + # Validate required parameters + self._validate_required_params(query=query, user_id=user_id) + + url = f"{self.base_url}/search/memory" + payload = { + "query": query, + "userId": user_id, + "conversationId": conversation_id, + "memoryLimitNumber": memory_limit_number, + } + + for retry in range(MAX_RETRY_COUNT): + try: + response = requests.post( + url, data=json.dumps(payload), headers=self.headers, timeout=30 + ) + response.raise_for_status() + response_data = response.json() + return MemOSSearchResponse(**response_data) + except Exception as e: + logger.error(f"Failed to search memory (retry {retry + 1}/3): {e}") + if retry == MAX_RETRY_COUNT - 1: + raise diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index f2f731460..7371c05b2 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -178,3 +178,104 @@ class SuggestionRequest(BaseRequest): user_id: str = Field(..., description="User ID") language: Literal["zh", "en"] = Field("zh", description="Language for suggestions") message: list[MessageDict] | None = Field(None, description="List of messages to store.") + + +# ─── MemOS Client Response Models ────────────────────────────────────────────── + + +class MessageDetail(BaseModel): + """Individual message detail model based on actual API response.""" + + role: str = Field(..., description="Message role (user/assistant)") + content: str = Field(..., description="Message content") + create_time: int | None = Field( + None, alias="createTime", description="Message creation timestamp" + ) + update_time: int | None = Field( + None, alias="updateTime", description="Message update timestamp" + ) + + +class MemoryDetail(BaseModel): + """Individual memory detail model based on actual API response.""" + + id: str = Field(..., description="Memory ID") + memory_key: str = Field(..., alias="memoryKey", description="Memory key/title") + memory_value: str = Field(..., alias="memoryValue", description="Memory content") + memory_type: str = Field( + ..., alias="memoryType", description="Memory type (e.g., WorkingMemory)" + ) + memory_time: int | None = Field(None, alias="memoryTime", description="Memory timestamp") + conversation_id: str = Field(..., alias="conversationId", description="Conversation ID") + status: str = Field(..., description="Memory status (e.g., activated)") + confidence: float = Field(..., description="Memory confidence score") + tags: list[str] = Field(default_factory=list, description="Memory tags") + update_time: int = Field(..., alias="updateTime", description="Last update timestamp") + relativity: float = Field(..., description="Memory relativity/similarity score") + + +class GetMessagesData(BaseModel): + """Data model for get messages response based on actual API.""" + + message_detail_list: list[MessageDetail] = Field( + default_factory=list, alias="messageDetailList", description="List of message details" + ) + + +class SearchMemoryData(BaseModel): + """Data model for search memory response based on actual API.""" + + memory_detail_list: list[MemoryDetail] = Field( + default_factory=list, alias="memoryDetailList", description="List of memory details" + ) + message_detail_list: list[MessageDetail] | None = Field( + None, alias="messageDetailList", description="List of message details (usually None)" + ) + + +class AddMessageData(BaseModel): + """Data model for add message response based on actual API.""" + + success: bool = Field(..., description="Operation success status") + + +# ─── MemOS Response Models (Similar to OpenAI ChatCompletion) ────────────────── + + +class MemOSGetMessagesResponse(BaseModel): + """Response model for get messages operation based on actual API.""" + + code: int = Field(..., description="Response status code") + message: str = Field(..., description="Response message") + data: GetMessagesData = Field(..., description="Messages data") + + @property + def messages(self) -> list[MessageDetail]: + """Convenient access to message list.""" + return self.data.message_detail_list + + +class MemOSSearchResponse(BaseModel): + """Response model for search memory operation based on actual API.""" + + code: int = Field(..., description="Response status code") + message: str = Field(..., description="Response message") + data: SearchMemoryData = Field(..., description="Search results data") + + @property + def memories(self) -> list[MemoryDetail]: + """Convenient access to memory list.""" + return self.data.memory_detail_list + + +class MemOSAddResponse(BaseModel): + """Response model for add message operation based on actual API.""" + + code: int = Field(..., description="Response status code") + message: str = Field(..., description="Response message") + data: AddMessageData = Field(..., description="Add operation data") + + @property + def success(self) -> bool: + """Convenient access to success status.""" + return self.data.success From 6b33bd4ad6be9045025a5d6d27e5a882dbe0b542 Mon Sep 17 00:00:00 2001 From: harvey_xiang Date: Tue, 23 Sep 2025 21:28:56 +0800 Subject: [PATCH 2/7] feat: update api client --- src/memos/api/client.py | 18 +++++++++--------- src/memos/api/product_models.py | 29 +++++------------------------ 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/memos/api/client.py b/src/memos/api/client.py index 2cb8faab5..d45276f2c 100644 --- a/src/memos/api/client.py +++ b/src/memos/api/client.py @@ -19,7 +19,7 @@ class MemOSClient: def __init__(self, api_key: str | None = None, base_url: str | None = None): self.base_url = ( - base_url or os.getenv("MEMOS_BASE_URL") or "https://memos.memtensor.cn/api/openmem" + base_url or os.getenv("MEMOS_BASE_URL") or "https://memos.memtensor.cn/api/openmem/v1" ) api_key = api_key or os.getenv("MEMOS_API_KEY") @@ -34,7 +34,7 @@ def _validate_required_params(self, **params): if not param_value: raise ValueError(f"{param_name} is required") - def get_messages( + def get_message( self, user_id: str, conversation_id: str | None = None ) -> MemOSGetMessagesResponse: """Get messages""" @@ -42,7 +42,7 @@ def get_messages( self._validate_required_params(user_id=user_id) url = f"{self.base_url}/get/message" - payload = {"userId": user_id, "conversationId": conversation_id} + payload = {"user_id": user_id, "conversation_id": conversation_id} for retry in range(MAX_RETRY_COUNT): try: response = requests.post( @@ -56,7 +56,7 @@ def get_messages( if retry == MAX_RETRY_COUNT - 1: raise - def add( + def add_message( self, messages: list[dict[str, Any]], user_id: str, conversation_id: str ) -> MemOSAddResponse: """Add memories""" @@ -66,7 +66,7 @@ def add( ) url = f"{self.base_url}/add/message" - payload = {"messages": messages, "userId": user_id, "conversationId": conversation_id} + payload = {"messages": messages, "user_id": user_id, "conversation_id": conversation_id} for retry in range(MAX_RETRY_COUNT): try: response = requests.post( @@ -80,7 +80,7 @@ def add( if retry == MAX_RETRY_COUNT - 1: raise - def search( + def search_memory( self, query: str, user_id: str, conversation_id: str, memory_limit_number: int = 6 ) -> MemOSSearchResponse: """Search memories""" @@ -90,9 +90,9 @@ def search( url = f"{self.base_url}/search/memory" payload = { "query": query, - "userId": user_id, - "conversationId": conversation_id, - "memoryLimitNumber": memory_limit_number, + "user_id": user_id, + "conversation_id": conversation_id, + "memory_limit_number": memory_limit_number, } for retry in range(MAX_RETRY_COUNT): diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index 7371c05b2..9c3af42f8 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -186,39 +186,20 @@ class SuggestionRequest(BaseRequest): class MessageDetail(BaseModel): """Individual message detail model based on actual API response.""" - role: str = Field(..., description="Message role (user/assistant)") - content: str = Field(..., description="Message content") - create_time: int | None = Field( - None, alias="createTime", description="Message creation timestamp" - ) - update_time: int | None = Field( - None, alias="updateTime", description="Message update timestamp" - ) + model_config = {"extra": "allow"} class MemoryDetail(BaseModel): """Individual memory detail model based on actual API response.""" - id: str = Field(..., description="Memory ID") - memory_key: str = Field(..., alias="memoryKey", description="Memory key/title") - memory_value: str = Field(..., alias="memoryValue", description="Memory content") - memory_type: str = Field( - ..., alias="memoryType", description="Memory type (e.g., WorkingMemory)" - ) - memory_time: int | None = Field(None, alias="memoryTime", description="Memory timestamp") - conversation_id: str = Field(..., alias="conversationId", description="Conversation ID") - status: str = Field(..., description="Memory status (e.g., activated)") - confidence: float = Field(..., description="Memory confidence score") - tags: list[str] = Field(default_factory=list, description="Memory tags") - update_time: int = Field(..., alias="updateTime", description="Last update timestamp") - relativity: float = Field(..., description="Memory relativity/similarity score") + model_config = {"extra": "allow"} class GetMessagesData(BaseModel): """Data model for get messages response based on actual API.""" message_detail_list: list[MessageDetail] = Field( - default_factory=list, alias="messageDetailList", description="List of message details" + default_factory=list, alias="memory_detail_list", description="List of message details" ) @@ -226,10 +207,10 @@ class SearchMemoryData(BaseModel): """Data model for search memory response based on actual API.""" memory_detail_list: list[MemoryDetail] = Field( - default_factory=list, alias="memoryDetailList", description="List of memory details" + default_factory=list, alias="memory_detail_list", description="List of memory details" ) message_detail_list: list[MessageDetail] | None = Field( - None, alias="messageDetailList", description="List of message details (usually None)" + None, alias="message_detail_list", description="List of message details (usually None)" ) From 6ca54149f8c8a4700633fa3cefab0d0179dd2564 Mon Sep 17 00:00:00 2001 From: harvey_xiang Date: Tue, 23 Sep 2025 21:33:49 +0800 Subject: [PATCH 3/7] feat: update api name --- src/memos/api/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/memos/api/client.py b/src/memos/api/client.py index d45276f2c..17037cb0d 100644 --- a/src/memos/api/client.py +++ b/src/memos/api/client.py @@ -34,9 +34,7 @@ def _validate_required_params(self, **params): if not param_value: raise ValueError(f"{param_name} is required") - def get_message( - self, user_id: str, conversation_id: str | None = None - ) -> MemOSGetMessagesResponse: + def get(self, user_id: str, conversation_id: str | None = None) -> MemOSGetMessagesResponse: """Get messages""" # Validate required parameters self._validate_required_params(user_id=user_id) @@ -56,7 +54,7 @@ def get_message( if retry == MAX_RETRY_COUNT - 1: raise - def add_message( + def add( self, messages: list[dict[str, Any]], user_id: str, conversation_id: str ) -> MemOSAddResponse: """Add memories""" @@ -80,7 +78,7 @@ def add_message( if retry == MAX_RETRY_COUNT - 1: raise - def search_memory( + def search( self, query: str, user_id: str, conversation_id: str, memory_limit_number: int = 6 ) -> MemOSSearchResponse: """Search memories""" From f764b31044f512eb2901bac8cd94b2fe289b122d Mon Sep 17 00:00:00 2001 From: harvey_xiang Date: Tue, 23 Sep 2025 22:08:07 +0800 Subject: [PATCH 4/7] feat: update api name --- src/memos/api/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/memos/api/client.py b/src/memos/api/client.py index 17037cb0d..9942c385e 100644 --- a/src/memos/api/client.py +++ b/src/memos/api/client.py @@ -14,7 +14,7 @@ MAX_RETRY_COUNT = 3 -class MemOSClient: +class MemOSAPIClient: """MemOS API client""" def __init__(self, api_key: str | None = None, base_url: str | None = None): From 0eb16a5794a98f8f18b2be49a3200ba0376d5cc3 Mon Sep 17 00:00:00 2001 From: harvey_xiang Date: Tue, 23 Sep 2025 22:13:36 +0800 Subject: [PATCH 5/7] feat: update usecase docs --- src/memos/api/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/memos/api/client.py b/src/memos/api/client.py index 9942c385e..17037cb0d 100644 --- a/src/memos/api/client.py +++ b/src/memos/api/client.py @@ -14,7 +14,7 @@ MAX_RETRY_COUNT = 3 -class MemOSAPIClient: +class MemOSClient: """MemOS API client""" def __init__(self, api_key: str | None = None, base_url: str | None = None): From 94642ee445000303f8ce8593eb8554f5591022f1 Mon Sep 17 00:00:00 2001 From: harvey_xiang Date: Wed, 24 Sep 2025 10:37:19 +0800 Subject: [PATCH 6/7] feat: update client api name --- src/memos/api/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/memos/api/client.py b/src/memos/api/client.py index 17037cb0d..d45276f2c 100644 --- a/src/memos/api/client.py +++ b/src/memos/api/client.py @@ -34,7 +34,9 @@ def _validate_required_params(self, **params): if not param_value: raise ValueError(f"{param_name} is required") - def get(self, user_id: str, conversation_id: str | None = None) -> MemOSGetMessagesResponse: + def get_message( + self, user_id: str, conversation_id: str | None = None + ) -> MemOSGetMessagesResponse: """Get messages""" # Validate required parameters self._validate_required_params(user_id=user_id) @@ -54,7 +56,7 @@ def get(self, user_id: str, conversation_id: str | None = None) -> MemOSGetMessa if retry == MAX_RETRY_COUNT - 1: raise - def add( + def add_message( self, messages: list[dict[str, Any]], user_id: str, conversation_id: str ) -> MemOSAddResponse: """Add memories""" @@ -78,7 +80,7 @@ def add( if retry == MAX_RETRY_COUNT - 1: raise - def search( + def search_memory( self, query: str, user_id: str, conversation_id: str, memory_limit_number: int = 6 ) -> MemOSSearchResponse: """Search memories""" From 3704d9f285027d77acc030dd62e98f12979eb226 Mon Sep 17 00:00:00 2001 From: harvey_xiang Date: Thu, 13 Nov 2025 17:44:57 +0800 Subject: [PATCH 7/7] feat: test ci --- src/memos/api/product_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/memos/api/product_api.py b/src/memos/api/product_api.py index 089409979..d7694b81f 100644 --- a/src/memos/api/product_api.py +++ b/src/memos/api/product_api.py @@ -25,7 +25,6 @@ # Exception handlers app.exception_handler(ValueError)(APIExceptionHandler.value_error_handler) -app.exception_handler(Exception)(APIExceptionHandler.global_exception_handler) if __name__ == "__main__":