Skip to content

Commit 1dc230a

Browse files
CarltonXiangharvey_xiang
andauthored
feat: add api client (#316)
Co-authored-by: harvey_xiang <[email protected]>
1 parent 6b8cf85 commit 1dc230a

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

src/memos/api/client.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import json
2+
import os
3+
4+
from typing import Any
5+
6+
import requests
7+
8+
from memos.api.product_models import MemOSAddResponse, MemOSGetMessagesResponse, MemOSSearchResponse
9+
from memos.log import get_logger
10+
11+
12+
logger = get_logger(__name__)
13+
14+
MAX_RETRY_COUNT = 3
15+
16+
17+
class MemOSClient:
18+
"""MemOS API client"""
19+
20+
def __init__(self, api_key: str | None = None, base_url: str | None = None):
21+
self.base_url = (
22+
base_url or os.getenv("MEMOS_BASE_URL") or "https://memos.memtensor.cn/api/openmem"
23+
)
24+
api_key = api_key or os.getenv("MEMOS_API_KEY")
25+
26+
if not api_key:
27+
raise ValueError("MemOS API key is required")
28+
29+
self.headers = {"Content-Type": "application/json", "Authorization": f"Token {api_key}"}
30+
31+
def _validate_required_params(self, **params):
32+
"""Validate required parameters - if passed, they must not be empty"""
33+
for param_name, param_value in params.items():
34+
if not param_value:
35+
raise ValueError(f"{param_name} is required")
36+
37+
def get_messages(
38+
self, user_id: str, conversation_id: str | None = None
39+
) -> MemOSGetMessagesResponse:
40+
"""Get messages"""
41+
# Validate required parameters
42+
self._validate_required_params(user_id=user_id)
43+
44+
url = f"{self.base_url}/get/message"
45+
payload = {"userId": user_id, "conversationId": conversation_id}
46+
for retry in range(MAX_RETRY_COUNT):
47+
try:
48+
response = requests.post(
49+
url, data=json.dumps(payload), headers=self.headers, timeout=30
50+
)
51+
response.raise_for_status()
52+
response_data = response.json()
53+
return MemOSGetMessagesResponse(**response_data)
54+
except Exception as e:
55+
logger.error(f"Failed to get messages (retry {retry + 1}/3): {e}")
56+
if retry == MAX_RETRY_COUNT - 1:
57+
raise
58+
59+
def add(
60+
self, messages: list[dict[str, Any]], user_id: str, conversation_id: str
61+
) -> MemOSAddResponse:
62+
"""Add memories"""
63+
# Validate required parameters
64+
self._validate_required_params(
65+
messages=messages, user_id=user_id, conversation_id=conversation_id
66+
)
67+
68+
url = f"{self.base_url}/add/message"
69+
payload = {"messages": messages, "userId": user_id, "conversationId": conversation_id}
70+
for retry in range(MAX_RETRY_COUNT):
71+
try:
72+
response = requests.post(
73+
url, data=json.dumps(payload), headers=self.headers, timeout=30
74+
)
75+
response.raise_for_status()
76+
response_data = response.json()
77+
return MemOSAddResponse(**response_data)
78+
except Exception as e:
79+
logger.error(f"Failed to add memory (retry {retry + 1}/3): {e}")
80+
if retry == MAX_RETRY_COUNT - 1:
81+
raise
82+
83+
def search(
84+
self, query: str, user_id: str, conversation_id: str, memory_limit_number: int = 6
85+
) -> MemOSSearchResponse:
86+
"""Search memories"""
87+
# Validate required parameters
88+
self._validate_required_params(query=query, user_id=user_id)
89+
90+
url = f"{self.base_url}/search/memory"
91+
payload = {
92+
"query": query,
93+
"userId": user_id,
94+
"conversationId": conversation_id,
95+
"memoryLimitNumber": memory_limit_number,
96+
}
97+
98+
for retry in range(MAX_RETRY_COUNT):
99+
try:
100+
response = requests.post(
101+
url, data=json.dumps(payload), headers=self.headers, timeout=30
102+
)
103+
response.raise_for_status()
104+
response_data = response.json()
105+
return MemOSSearchResponse(**response_data)
106+
except Exception as e:
107+
logger.error(f"Failed to search memory (retry {retry + 1}/3): {e}")
108+
if retry == MAX_RETRY_COUNT - 1:
109+
raise

src/memos/api/product_models.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,104 @@ class SuggestionRequest(BaseRequest):
178178
user_id: str = Field(..., description="User ID")
179179
language: Literal["zh", "en"] = Field("zh", description="Language for suggestions")
180180
message: list[MessageDict] | None = Field(None, description="List of messages to store.")
181+
182+
183+
# ─── MemOS Client Response Models ──────────────────────────────────────────────
184+
185+
186+
class MessageDetail(BaseModel):
187+
"""Individual message detail model based on actual API response."""
188+
189+
role: str = Field(..., description="Message role (user/assistant)")
190+
content: str = Field(..., description="Message content")
191+
create_time: int | None = Field(
192+
None, alias="createTime", description="Message creation timestamp"
193+
)
194+
update_time: int | None = Field(
195+
None, alias="updateTime", description="Message update timestamp"
196+
)
197+
198+
199+
class MemoryDetail(BaseModel):
200+
"""Individual memory detail model based on actual API response."""
201+
202+
id: str = Field(..., description="Memory ID")
203+
memory_key: str = Field(..., alias="memoryKey", description="Memory key/title")
204+
memory_value: str = Field(..., alias="memoryValue", description="Memory content")
205+
memory_type: str = Field(
206+
..., alias="memoryType", description="Memory type (e.g., WorkingMemory)"
207+
)
208+
memory_time: int | None = Field(None, alias="memoryTime", description="Memory timestamp")
209+
conversation_id: str = Field(..., alias="conversationId", description="Conversation ID")
210+
status: str = Field(..., description="Memory status (e.g., activated)")
211+
confidence: float = Field(..., description="Memory confidence score")
212+
tags: list[str] = Field(default_factory=list, description="Memory tags")
213+
update_time: int = Field(..., alias="updateTime", description="Last update timestamp")
214+
relativity: float = Field(..., description="Memory relativity/similarity score")
215+
216+
217+
class GetMessagesData(BaseModel):
218+
"""Data model for get messages response based on actual API."""
219+
220+
message_detail_list: list[MessageDetail] = Field(
221+
default_factory=list, alias="messageDetailList", description="List of message details"
222+
)
223+
224+
225+
class SearchMemoryData(BaseModel):
226+
"""Data model for search memory response based on actual API."""
227+
228+
memory_detail_list: list[MemoryDetail] = Field(
229+
default_factory=list, alias="memoryDetailList", description="List of memory details"
230+
)
231+
message_detail_list: list[MessageDetail] | None = Field(
232+
None, alias="messageDetailList", description="List of message details (usually None)"
233+
)
234+
235+
236+
class AddMessageData(BaseModel):
237+
"""Data model for add message response based on actual API."""
238+
239+
success: bool = Field(..., description="Operation success status")
240+
241+
242+
# ─── MemOS Response Models (Similar to OpenAI ChatCompletion) ──────────────────
243+
244+
245+
class MemOSGetMessagesResponse(BaseModel):
246+
"""Response model for get messages operation based on actual API."""
247+
248+
code: int = Field(..., description="Response status code")
249+
message: str = Field(..., description="Response message")
250+
data: GetMessagesData = Field(..., description="Messages data")
251+
252+
@property
253+
def messages(self) -> list[MessageDetail]:
254+
"""Convenient access to message list."""
255+
return self.data.message_detail_list
256+
257+
258+
class MemOSSearchResponse(BaseModel):
259+
"""Response model for search memory operation based on actual API."""
260+
261+
code: int = Field(..., description="Response status code")
262+
message: str = Field(..., description="Response message")
263+
data: SearchMemoryData = Field(..., description="Search results data")
264+
265+
@property
266+
def memories(self) -> list[MemoryDetail]:
267+
"""Convenient access to memory list."""
268+
return self.data.memory_detail_list
269+
270+
271+
class MemOSAddResponse(BaseModel):
272+
"""Response model for add message operation based on actual API."""
273+
274+
code: int = Field(..., description="Response status code")
275+
message: str = Field(..., description="Response message")
276+
data: AddMessageData = Field(..., description="Add operation data")
277+
278+
@property
279+
def success(self) -> bool:
280+
"""Convenient access to success status."""
281+
return self.data.success

0 commit comments

Comments
 (0)