MemoryOS 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of MemoryOS might be problematic. Click here for more details.
- {memoryos-0.2.0.dist-info → memoryos-0.2.1.dist-info}/METADATA +66 -26
- {memoryos-0.2.0.dist-info → memoryos-0.2.1.dist-info}/RECORD +80 -56
- memoryos-0.2.1.dist-info/entry_points.txt +3 -0
- memos/__init__.py +1 -1
- memos/api/config.py +471 -0
- memos/api/exceptions.py +28 -0
- memos/api/mcp_serve.py +502 -0
- memos/api/product_api.py +35 -0
- memos/api/product_models.py +159 -0
- memos/api/routers/__init__.py +1 -0
- memos/api/routers/product_router.py +358 -0
- memos/chunkers/sentence_chunker.py +8 -2
- memos/cli.py +113 -0
- memos/configs/embedder.py +27 -0
- memos/configs/graph_db.py +83 -2
- memos/configs/llm.py +47 -0
- memos/configs/mem_cube.py +1 -1
- memos/configs/mem_scheduler.py +91 -5
- memos/configs/memory.py +5 -4
- memos/dependency.py +52 -0
- memos/embedders/ark.py +92 -0
- memos/embedders/factory.py +4 -0
- memos/embedders/sentence_transformer.py +8 -2
- memos/embedders/universal_api.py +32 -0
- memos/graph_dbs/base.py +2 -2
- memos/graph_dbs/factory.py +2 -0
- memos/graph_dbs/neo4j.py +331 -122
- memos/graph_dbs/neo4j_community.py +300 -0
- memos/llms/base.py +9 -0
- memos/llms/deepseek.py +54 -0
- memos/llms/factory.py +10 -1
- memos/llms/hf.py +170 -13
- memos/llms/hf_singleton.py +114 -0
- memos/llms/ollama.py +4 -0
- memos/llms/openai.py +67 -1
- memos/llms/qwen.py +63 -0
- memos/llms/vllm.py +153 -0
- memos/mem_cube/general.py +77 -16
- memos/mem_cube/utils.py +102 -0
- memos/mem_os/core.py +131 -41
- memos/mem_os/main.py +93 -11
- memos/mem_os/product.py +1098 -35
- memos/mem_os/utils/default_config.py +352 -0
- memos/mem_os/utils/format_utils.py +1154 -0
- memos/mem_reader/simple_struct.py +5 -5
- memos/mem_scheduler/base_scheduler.py +467 -36
- memos/mem_scheduler/general_scheduler.py +125 -244
- memos/mem_scheduler/modules/base.py +9 -0
- memos/mem_scheduler/modules/dispatcher.py +68 -2
- memos/mem_scheduler/modules/misc.py +39 -0
- memos/mem_scheduler/modules/monitor.py +228 -49
- memos/mem_scheduler/modules/rabbitmq_service.py +317 -0
- memos/mem_scheduler/modules/redis_service.py +32 -22
- memos/mem_scheduler/modules/retriever.py +250 -23
- memos/mem_scheduler/modules/schemas.py +189 -7
- memos/mem_scheduler/mos_for_test_scheduler.py +143 -0
- memos/mem_scheduler/utils.py +51 -2
- memos/mem_user/persistent_user_manager.py +260 -0
- memos/memories/activation/item.py +25 -0
- memos/memories/activation/kv.py +10 -3
- memos/memories/activation/vllmkv.py +219 -0
- memos/memories/factory.py +2 -0
- memos/memories/textual/general.py +7 -5
- memos/memories/textual/tree.py +9 -5
- memos/memories/textual/tree_text_memory/organize/conflict.py +5 -3
- memos/memories/textual/tree_text_memory/organize/manager.py +26 -18
- memos/memories/textual/tree_text_memory/organize/redundancy.py +25 -44
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +11 -13
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +73 -51
- memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +6 -5
- memos/parsers/markitdown.py +8 -2
- memos/templates/mem_reader_prompts.py +65 -23
- memos/templates/mem_scheduler_prompts.py +96 -47
- memos/templates/tree_reorganize_prompts.py +85 -30
- memos/vec_dbs/base.py +12 -0
- memos/vec_dbs/qdrant.py +46 -20
- {memoryos-0.2.0.dist-info → memoryos-0.2.1.dist-info}/LICENSE +0 -0
- {memoryos-0.2.0.dist-info → memoryos-0.2.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# API routers module
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
from fastapi.responses import StreamingResponse
|
|
7
|
+
|
|
8
|
+
from memos.api.config import APIConfig
|
|
9
|
+
from memos.api.product_models import (
|
|
10
|
+
BaseResponse,
|
|
11
|
+
ChatRequest,
|
|
12
|
+
GetMemoryRequest,
|
|
13
|
+
MemoryCreateRequest,
|
|
14
|
+
MemoryResponse,
|
|
15
|
+
SearchRequest,
|
|
16
|
+
SearchResponse,
|
|
17
|
+
SimpleResponse,
|
|
18
|
+
SuggestionRequest,
|
|
19
|
+
SuggestionResponse,
|
|
20
|
+
UserRegisterRequest,
|
|
21
|
+
UserRegisterResponse,
|
|
22
|
+
)
|
|
23
|
+
from memos.configs.mem_os import MOSConfig
|
|
24
|
+
from memos.mem_os.product import MOSProduct
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
router = APIRouter(prefix="/product", tags=["Product API"])
|
|
30
|
+
|
|
31
|
+
# Initialize MOSProduct instance with lazy initialization
|
|
32
|
+
MOS_PRODUCT_INSTANCE = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_mos_product_instance():
|
|
36
|
+
"""Get or create MOSProduct instance."""
|
|
37
|
+
global MOS_PRODUCT_INSTANCE
|
|
38
|
+
if MOS_PRODUCT_INSTANCE is None:
|
|
39
|
+
default_config = APIConfig.get_product_default_config()
|
|
40
|
+
print(default_config)
|
|
41
|
+
from memos.configs.mem_os import MOSConfig
|
|
42
|
+
|
|
43
|
+
mos_config = MOSConfig(**default_config)
|
|
44
|
+
|
|
45
|
+
# Get default cube config from APIConfig (may be None if disabled)
|
|
46
|
+
default_cube_config = APIConfig.get_default_cube_config()
|
|
47
|
+
print("*********default_cube_config*********", default_cube_config)
|
|
48
|
+
MOS_PRODUCT_INSTANCE = MOSProduct(
|
|
49
|
+
default_config=mos_config, default_cube_config=default_cube_config
|
|
50
|
+
)
|
|
51
|
+
logger.info("MOSProduct instance created successfully with inheritance architecture")
|
|
52
|
+
return MOS_PRODUCT_INSTANCE
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
get_mos_product_instance()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.post("/configure", summary="Configure MOSProduct", response_model=SimpleResponse)
|
|
59
|
+
async def set_config(config):
|
|
60
|
+
"""Set MOSProduct configuration."""
|
|
61
|
+
global MOS_PRODUCT_INSTANCE
|
|
62
|
+
MOS_PRODUCT_INSTANCE = MOSProduct(default_config=config)
|
|
63
|
+
return SimpleResponse(message="Configuration set successfully")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.post("/users/register", summary="Register a new user", response_model=UserRegisterResponse)
|
|
67
|
+
async def register_user(user_req: UserRegisterRequest):
|
|
68
|
+
"""Register a new user with configuration and default cube."""
|
|
69
|
+
try:
|
|
70
|
+
# Get configuration for the user
|
|
71
|
+
user_config, default_mem_cube = APIConfig.create_user_config(
|
|
72
|
+
user_name=user_req.user_id, user_id=user_req.user_id
|
|
73
|
+
)
|
|
74
|
+
logger.info(f"user_config: {user_config.model_dump(mode='json')}")
|
|
75
|
+
logger.info(f"default_mem_cube: {default_mem_cube.config.model_dump(mode='json')}")
|
|
76
|
+
mos_product = get_mos_product_instance()
|
|
77
|
+
|
|
78
|
+
# Register user with default config and mem cube
|
|
79
|
+
result = mos_product.user_register(
|
|
80
|
+
user_id=user_req.user_id,
|
|
81
|
+
user_name=user_req.user_name,
|
|
82
|
+
interests=user_req.interests,
|
|
83
|
+
config=user_config,
|
|
84
|
+
default_mem_cube=default_mem_cube,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if result["status"] == "success":
|
|
88
|
+
return UserRegisterResponse(
|
|
89
|
+
message="User registered successfully",
|
|
90
|
+
data={"user_id": result["user_id"], "mem_cube_id": result["default_cube_id"]},
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
raise HTTPException(status_code=400, detail=result["message"])
|
|
94
|
+
|
|
95
|
+
except Exception as err:
|
|
96
|
+
logger.error(f"Failed to register user: {traceback.format_exc()}")
|
|
97
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.get(
|
|
101
|
+
"/suggestions/{user_id}", summary="Get suggestion queries", response_model=SuggestionResponse
|
|
102
|
+
)
|
|
103
|
+
async def get_suggestion_queries(user_id: str):
|
|
104
|
+
"""Get suggestion queries for a specific user."""
|
|
105
|
+
try:
|
|
106
|
+
mos_product = get_mos_product_instance()
|
|
107
|
+
suggestions = mos_product.get_suggestion_query(user_id)
|
|
108
|
+
return SuggestionResponse(
|
|
109
|
+
message="Suggestions retrieved successfully", data={"query": suggestions}
|
|
110
|
+
)
|
|
111
|
+
except ValueError as err:
|
|
112
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
113
|
+
except Exception as err:
|
|
114
|
+
logger.error(f"Failed to get suggestions: {traceback.format_exc()}")
|
|
115
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.post(
|
|
119
|
+
"/suggestions",
|
|
120
|
+
summary="Get suggestion queries with language",
|
|
121
|
+
response_model=SuggestionResponse,
|
|
122
|
+
)
|
|
123
|
+
async def get_suggestion_queries_post(suggestion_req: SuggestionRequest):
|
|
124
|
+
"""Get suggestion queries for a specific user with language preference."""
|
|
125
|
+
try:
|
|
126
|
+
mos_product = get_mos_product_instance()
|
|
127
|
+
suggestions = mos_product.get_suggestion_query(
|
|
128
|
+
user_id=suggestion_req.user_id, language=suggestion_req.language
|
|
129
|
+
)
|
|
130
|
+
return SuggestionResponse(
|
|
131
|
+
message="Suggestions retrieved successfully", data={"query": suggestions}
|
|
132
|
+
)
|
|
133
|
+
except ValueError as err:
|
|
134
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
135
|
+
except Exception as err:
|
|
136
|
+
logger.error(f"Failed to get suggestions: {traceback.format_exc()}")
|
|
137
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.post("/get_all", summary="Get all memories for user", response_model=MemoryResponse)
|
|
141
|
+
async def get_all_memories(memory_req: GetMemoryRequest):
|
|
142
|
+
"""Get all memories for a specific user."""
|
|
143
|
+
try:
|
|
144
|
+
mos_product = get_mos_product_instance()
|
|
145
|
+
if memory_req.search_query:
|
|
146
|
+
result = mos_product.get_subgraph(
|
|
147
|
+
user_id=memory_req.user_id,
|
|
148
|
+
query=memory_req.search_query,
|
|
149
|
+
mem_cube_ids=memory_req.mem_cube_ids,
|
|
150
|
+
)
|
|
151
|
+
return MemoryResponse(message="Memories retrieved successfully", data=result)
|
|
152
|
+
else:
|
|
153
|
+
result = mos_product.get_all(
|
|
154
|
+
user_id=memory_req.user_id,
|
|
155
|
+
memory_type=memory_req.memory_type,
|
|
156
|
+
mem_cube_ids=memory_req.mem_cube_ids,
|
|
157
|
+
)
|
|
158
|
+
return MemoryResponse(message="Memories retrieved successfully", data=result)
|
|
159
|
+
|
|
160
|
+
except ValueError as err:
|
|
161
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
162
|
+
except Exception as err:
|
|
163
|
+
logger.error(f"Failed to get memories: {traceback.format_exc()}")
|
|
164
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@router.post("/add", summary="add a new memory", response_model=SimpleResponse)
|
|
168
|
+
async def create_memory(memory_req: MemoryCreateRequest):
|
|
169
|
+
"""Create a new memory for a specific user."""
|
|
170
|
+
try:
|
|
171
|
+
mos_product = get_mos_product_instance()
|
|
172
|
+
mos_product.add(
|
|
173
|
+
user_id=memory_req.user_id,
|
|
174
|
+
memory_content=memory_req.memory_content,
|
|
175
|
+
messages=memory_req.messages,
|
|
176
|
+
doc_path=memory_req.doc_path,
|
|
177
|
+
mem_cube_id=memory_req.mem_cube_id,
|
|
178
|
+
)
|
|
179
|
+
return SimpleResponse(message="Memory created successfully")
|
|
180
|
+
|
|
181
|
+
except ValueError as err:
|
|
182
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
183
|
+
except Exception as err:
|
|
184
|
+
logger.error(f"Failed to create memory: {traceback.format_exc()}")
|
|
185
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@router.post("/search", summary="Search memories", response_model=SearchResponse)
|
|
189
|
+
async def search_memories(search_req: SearchRequest):
|
|
190
|
+
"""Search memories for a specific user."""
|
|
191
|
+
try:
|
|
192
|
+
mos_product = get_mos_product_instance()
|
|
193
|
+
result = mos_product.search(
|
|
194
|
+
query=search_req.query,
|
|
195
|
+
user_id=search_req.user_id,
|
|
196
|
+
install_cube_ids=[search_req.mem_cube_id] if search_req.mem_cube_id else None,
|
|
197
|
+
)
|
|
198
|
+
return SearchResponse(message="Search completed successfully", data=result)
|
|
199
|
+
|
|
200
|
+
except ValueError as err:
|
|
201
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
202
|
+
except Exception as err:
|
|
203
|
+
logger.error(f"Failed to search memories: {traceback.format_exc()}")
|
|
204
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.post("/chat", summary="Chat with MemOS")
|
|
208
|
+
async def chat(chat_req: ChatRequest):
|
|
209
|
+
"""Chat with MemOS for a specific user. Returns SSE stream."""
|
|
210
|
+
try:
|
|
211
|
+
mos_product = get_mos_product_instance()
|
|
212
|
+
|
|
213
|
+
async def generate_chat_response():
|
|
214
|
+
"""Generate chat response as SSE stream."""
|
|
215
|
+
try:
|
|
216
|
+
import asyncio
|
|
217
|
+
|
|
218
|
+
for chunk in mos_product.chat_with_references(
|
|
219
|
+
query=chat_req.query,
|
|
220
|
+
user_id=chat_req.user_id,
|
|
221
|
+
cube_id=chat_req.mem_cube_id,
|
|
222
|
+
history=chat_req.history,
|
|
223
|
+
):
|
|
224
|
+
yield chunk
|
|
225
|
+
await asyncio.sleep(0.00001) # 50ms delay between chunks
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Error in chat stream: {e}")
|
|
228
|
+
error_data = f"data: {json.dumps({'type': 'error', 'content': str(traceback.format_exc())})}\n\n"
|
|
229
|
+
yield error_data
|
|
230
|
+
|
|
231
|
+
return StreamingResponse(
|
|
232
|
+
generate_chat_response(),
|
|
233
|
+
media_type="text/plain",
|
|
234
|
+
headers={
|
|
235
|
+
"Cache-Control": "no-cache",
|
|
236
|
+
"Connection": "keep-alive",
|
|
237
|
+
"Content-Type": "text/event-stream",
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
except ValueError as err:
|
|
242
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
243
|
+
except Exception as err:
|
|
244
|
+
logger.error(f"Failed to start chat: {traceback.format_exc()}")
|
|
245
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@router.get("/users", summary="List all users", response_model=BaseResponse[list])
|
|
249
|
+
async def list_users():
|
|
250
|
+
"""List all registered users."""
|
|
251
|
+
try:
|
|
252
|
+
mos_product = get_mos_product_instance()
|
|
253
|
+
users = mos_product.list_users()
|
|
254
|
+
return BaseResponse(message="Users retrieved successfully", data=users)
|
|
255
|
+
except Exception as err:
|
|
256
|
+
logger.error(f"Failed to list users: {traceback.format_exc()}")
|
|
257
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@router.get("/users/{user_id}", summary="Get user info", response_model=BaseResponse[dict])
|
|
261
|
+
async def get_user_info(user_id: str):
|
|
262
|
+
"""Get user information including accessible cubes."""
|
|
263
|
+
try:
|
|
264
|
+
mos_product = get_mos_product_instance()
|
|
265
|
+
user_info = mos_product.get_user_info(user_id)
|
|
266
|
+
return BaseResponse(message="User info retrieved successfully", data=user_info)
|
|
267
|
+
except ValueError as err:
|
|
268
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
269
|
+
except Exception as err:
|
|
270
|
+
logger.error(f"Failed to get user info: {traceback.format_exc()}")
|
|
271
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@router.get(
|
|
275
|
+
"/configure/{user_id}", summary="Get MOSProduct configuration", response_model=SimpleResponse
|
|
276
|
+
)
|
|
277
|
+
async def get_config(user_id: str):
|
|
278
|
+
"""Get MOSProduct configuration."""
|
|
279
|
+
global MOS_PRODUCT_INSTANCE
|
|
280
|
+
config = MOS_PRODUCT_INSTANCE.default_config
|
|
281
|
+
return SimpleResponse(message="Configuration retrieved successfully", data=config)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@router.get(
|
|
285
|
+
"/users/{user_id}/config", summary="Get user configuration", response_model=BaseResponse[dict]
|
|
286
|
+
)
|
|
287
|
+
async def get_user_config(user_id: str):
|
|
288
|
+
"""Get user-specific configuration."""
|
|
289
|
+
try:
|
|
290
|
+
mos_product = get_mos_product_instance()
|
|
291
|
+
config = mos_product.get_user_config(user_id)
|
|
292
|
+
if config:
|
|
293
|
+
return BaseResponse(
|
|
294
|
+
message="User configuration retrieved successfully",
|
|
295
|
+
data=config.model_dump(mode="json"),
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
raise HTTPException(
|
|
299
|
+
status_code=404, detail=f"Configuration not found for user {user_id}"
|
|
300
|
+
)
|
|
301
|
+
except ValueError as err:
|
|
302
|
+
raise HTTPException(status_code=404, detail=str(traceback.format_exc())) from err
|
|
303
|
+
except Exception as err:
|
|
304
|
+
logger.error(f"Failed to get user config: {traceback.format_exc()}")
|
|
305
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@router.put(
|
|
309
|
+
"/users/{user_id}/config", summary="Update user configuration", response_model=SimpleResponse
|
|
310
|
+
)
|
|
311
|
+
async def update_user_config(user_id: str, config_data: dict):
|
|
312
|
+
"""Update user-specific configuration."""
|
|
313
|
+
try:
|
|
314
|
+
mos_product = get_mos_product_instance()
|
|
315
|
+
|
|
316
|
+
# Create MOSConfig from the provided data
|
|
317
|
+
config = MOSConfig(**config_data)
|
|
318
|
+
|
|
319
|
+
# Update the configuration
|
|
320
|
+
success = mos_product.update_user_config(user_id, config)
|
|
321
|
+
if success:
|
|
322
|
+
return SimpleResponse(message="User configuration updated successfully")
|
|
323
|
+
else:
|
|
324
|
+
raise HTTPException(status_code=500, detail="Failed to update user configuration")
|
|
325
|
+
|
|
326
|
+
except ValueError as err:
|
|
327
|
+
raise HTTPException(status_code=400, detail=str(traceback.format_exc())) from err
|
|
328
|
+
except Exception as err:
|
|
329
|
+
logger.error(f"Failed to update user config: {traceback.format_exc()}")
|
|
330
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@router.get(
|
|
334
|
+
"/instances/status", summary="Get user configuration status", response_model=BaseResponse[dict]
|
|
335
|
+
)
|
|
336
|
+
async def get_instance_status():
|
|
337
|
+
"""Get information about active user configurations in memory."""
|
|
338
|
+
try:
|
|
339
|
+
mos_product = get_mos_product_instance()
|
|
340
|
+
status_info = mos_product.get_user_instance_info()
|
|
341
|
+
return BaseResponse(
|
|
342
|
+
message="User configuration status retrieved successfully", data=status_info
|
|
343
|
+
)
|
|
344
|
+
except Exception as err:
|
|
345
|
+
logger.error(f"Failed to get user configuration status: {traceback.format_exc()}")
|
|
346
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@router.get("/instances/count", summary="Get active user count", response_model=BaseResponse[int])
|
|
350
|
+
async def get_active_user_count():
|
|
351
|
+
"""Get the number of active user configurations in memory."""
|
|
352
|
+
try:
|
|
353
|
+
mos_product = get_mos_product_instance()
|
|
354
|
+
count = mos_product.get_active_user_count()
|
|
355
|
+
return BaseResponse(message="Active user count retrieved successfully", data=count)
|
|
356
|
+
except Exception as err:
|
|
357
|
+
logger.error(f"Failed to get active user count: {traceback.format_exc()}")
|
|
358
|
+
raise HTTPException(status_code=500, detail=str(traceback.format_exc())) from err
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
from chonkie import SentenceChunker as ChonkieSentenceChunker
|
|
2
|
-
|
|
3
1
|
from memos.configs.chunker import SentenceChunkerConfig
|
|
2
|
+
from memos.dependency import require_python_package
|
|
4
3
|
from memos.log import get_logger
|
|
5
4
|
|
|
6
5
|
from .base import BaseChunker, Chunk
|
|
@@ -12,7 +11,14 @@ logger = get_logger(__name__)
|
|
|
12
11
|
class SentenceChunker(BaseChunker):
|
|
13
12
|
"""Sentence-based text chunker."""
|
|
14
13
|
|
|
14
|
+
@require_python_package(
|
|
15
|
+
import_name="chonkie",
|
|
16
|
+
install_command="pip install chonkie",
|
|
17
|
+
install_link="https://docs.chonkie.ai/python-sdk/getting-started/installation",
|
|
18
|
+
)
|
|
15
19
|
def __init__(self, config: SentenceChunkerConfig):
|
|
20
|
+
from chonkie import SentenceChunker as ChonkieSentenceChunker
|
|
21
|
+
|
|
16
22
|
self.config = config
|
|
17
23
|
self.chunker = ChonkieSentenceChunker(
|
|
18
24
|
tokenizer_or_token_counter=config.tokenizer_or_token_counter,
|
memos/cli.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MemOS CLI Tool
|
|
3
|
+
This script provides command-line interface for MemOS operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import zipfile
|
|
10
|
+
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def export_openapi(output: str) -> bool:
|
|
15
|
+
"""Export OpenAPI schema to JSON file."""
|
|
16
|
+
from memos.api.start_api import app
|
|
17
|
+
|
|
18
|
+
# Create directory if it doesn't exist
|
|
19
|
+
if os.path.dirname(output):
|
|
20
|
+
os.makedirs(os.path.dirname(output), exist_ok=True)
|
|
21
|
+
|
|
22
|
+
with open(output, "w") as f:
|
|
23
|
+
json.dump(app.openapi(), f, indent=2)
|
|
24
|
+
f.write("\n")
|
|
25
|
+
|
|
26
|
+
print(f"✅ OpenAPI schema exported to: {output}")
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def download_examples(dest: str) -> bool:
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
"""Download examples from the MemOS repository."""
|
|
34
|
+
zip_url = "https://github.com/MemTensor/MemOS/archive/refs/heads/main.zip"
|
|
35
|
+
print(f"📥 Downloading examples from {zip_url}...")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
response = requests.get(zip_url)
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
|
|
41
|
+
with zipfile.ZipFile(BytesIO(response.content)) as z:
|
|
42
|
+
extracted_files = []
|
|
43
|
+
for file in z.namelist():
|
|
44
|
+
if "MemOS-main/examples/" in file and not file.endswith("/"):
|
|
45
|
+
# Remove the prefix and extract to dest
|
|
46
|
+
relative_path = file.replace("MemOS-main/examples/", "")
|
|
47
|
+
extract_path = os.path.join(dest, relative_path)
|
|
48
|
+
|
|
49
|
+
# Create directory if it doesn't exist
|
|
50
|
+
os.makedirs(os.path.dirname(extract_path), exist_ok=True)
|
|
51
|
+
|
|
52
|
+
# Extract the file
|
|
53
|
+
with z.open(file) as source, open(extract_path, "wb") as target:
|
|
54
|
+
target.write(source.read())
|
|
55
|
+
extracted_files.append(extract_path)
|
|
56
|
+
|
|
57
|
+
print(f"✅ Examples downloaded to: {dest}")
|
|
58
|
+
print(f"📁 {len(extracted_files)} files extracted")
|
|
59
|
+
|
|
60
|
+
except requests.RequestException as e:
|
|
61
|
+
print(f"❌ Error downloading examples: {e}")
|
|
62
|
+
return False
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"❌ Error extracting examples: {e}")
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
"""Main CLI entry point."""
|
|
72
|
+
parser = argparse.ArgumentParser(
|
|
73
|
+
prog="memos",
|
|
74
|
+
description="MemOS Command Line Interface",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Create subparsers for different commands
|
|
78
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
79
|
+
|
|
80
|
+
# Download examples command
|
|
81
|
+
examples_parser = subparsers.add_parser("download_examples", help="Download example files")
|
|
82
|
+
examples_parser.add_argument(
|
|
83
|
+
"--dest",
|
|
84
|
+
type=str,
|
|
85
|
+
default="./examples",
|
|
86
|
+
help="Destination directory for examples (default: ./examples)",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Export API command
|
|
90
|
+
api_parser = subparsers.add_parser("export_openapi", help="Export OpenAPI schema to JSON file")
|
|
91
|
+
api_parser.add_argument(
|
|
92
|
+
"--output",
|
|
93
|
+
type=str,
|
|
94
|
+
default="openapi.json",
|
|
95
|
+
help="Output path for OpenAPI schema (default: openapi.json)",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Parse arguments
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
|
|
101
|
+
# Handle commands
|
|
102
|
+
if args.command == "download_examples":
|
|
103
|
+
success = download_examples(args.dest)
|
|
104
|
+
exit(0 if success else 1)
|
|
105
|
+
elif args.command == "export_openapi":
|
|
106
|
+
success = export_openapi(args.output)
|
|
107
|
+
exit(0 if success else 1)
|
|
108
|
+
else:
|
|
109
|
+
parser.print_help()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
memos/configs/embedder.py
CHANGED
|
@@ -18,6 +18,18 @@ class OllamaEmbedderConfig(BaseEmbedderConfig):
|
|
|
18
18
|
api_base: str = Field(default="http://localhost:11434", description="Base URL for Ollama API")
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
class ArkEmbedderConfig(BaseEmbedderConfig):
|
|
22
|
+
api_key: str = Field(..., description="Ark API key")
|
|
23
|
+
api_base: str = Field(
|
|
24
|
+
default="https://ark.cn-beijing.volces.com/api/v3/", description="Base URL for Ark API"
|
|
25
|
+
)
|
|
26
|
+
chunk_size: int = Field(default=1, description="Chunk size for Ark API")
|
|
27
|
+
multi_modal: bool = Field(
|
|
28
|
+
default=False,
|
|
29
|
+
description="Whether to use multi-modal embedding (text + image) with Ark",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
class SenTranEmbedderConfig(BaseEmbedderConfig):
|
|
22
34
|
"""Configuration class for Sentence Transformer embeddings."""
|
|
23
35
|
|
|
@@ -27,6 +39,19 @@ class SenTranEmbedderConfig(BaseEmbedderConfig):
|
|
|
27
39
|
)
|
|
28
40
|
|
|
29
41
|
|
|
42
|
+
class UniversalAPIEmbedderConfig(BaseEmbedderConfig):
|
|
43
|
+
"""
|
|
44
|
+
Configuration class for universal API embedding providers, e.g.,
|
|
45
|
+
OpenAI, etc.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
provider: str = Field(..., description="Provider name, e.g., 'openai'")
|
|
49
|
+
api_key: str = Field(..., description="API key for the embedding provider")
|
|
50
|
+
base_url: str | None = Field(
|
|
51
|
+
default=None, description="Optional base URL for custom or proxied endpoint"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
30
55
|
class EmbedderConfigFactory(BaseConfig):
|
|
31
56
|
"""Factory class for creating embedder configurations."""
|
|
32
57
|
|
|
@@ -36,6 +61,8 @@ class EmbedderConfigFactory(BaseConfig):
|
|
|
36
61
|
backend_to_class: ClassVar[dict[str, Any]] = {
|
|
37
62
|
"ollama": OllamaEmbedderConfig,
|
|
38
63
|
"sentence_transformer": SenTranEmbedderConfig,
|
|
64
|
+
"ark": ArkEmbedderConfig,
|
|
65
|
+
"universal_api": UniversalAPIEmbedderConfig,
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
@field_validator("backend")
|
memos/configs/graph_db.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Any, ClassVar
|
|
|
3
3
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
4
4
|
|
|
5
5
|
from memos.configs.base import BaseConfig
|
|
6
|
+
from memos.configs.vec_db import VectorDBConfigFactory
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class BaseGraphDBConfig(BaseConfig):
|
|
@@ -14,14 +15,93 @@ class BaseGraphDBConfig(BaseConfig):
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class Neo4jGraphDBConfig(BaseGraphDBConfig):
|
|
17
|
-
"""
|
|
18
|
+
"""
|
|
19
|
+
Neo4j-specific configuration.
|
|
20
|
+
|
|
21
|
+
This config supports:
|
|
22
|
+
1) Physical isolation (multi-db) — each user gets a dedicated Neo4j database.
|
|
23
|
+
2) Logical isolation (single-db) — all users share one or more databases, but each node is tagged with `user_name`.
|
|
24
|
+
|
|
25
|
+
How to use:
|
|
26
|
+
- If `use_multi_db=True`, then `db_name` should usually be the same as `user_name`.
|
|
27
|
+
Each user gets a separate database for physical isolation.
|
|
28
|
+
Example: db_name = "alice", user_name = None or "alice".
|
|
29
|
+
|
|
30
|
+
- If `use_multi_db=False`, then `db_name` is your shared database (e.g., "neo4j" or "shared_db").
|
|
31
|
+
You must provide `user_name` to logically isolate each user's data.
|
|
32
|
+
All nodes and queries must respect this tag.
|
|
33
|
+
|
|
34
|
+
Example configs:
|
|
35
|
+
---
|
|
36
|
+
# Physical isolation:
|
|
37
|
+
db_name = "alice"
|
|
38
|
+
use_multi_db = True
|
|
39
|
+
user_name = None
|
|
40
|
+
|
|
41
|
+
# Logical isolation:
|
|
42
|
+
db_name = "shared_db_student_group"
|
|
43
|
+
use_multi_db = False
|
|
44
|
+
user_name = "alice"
|
|
45
|
+
"""
|
|
18
46
|
|
|
19
47
|
db_name: str = Field(..., description="The name of the target Neo4j database")
|
|
20
48
|
auto_create: bool = Field(
|
|
21
|
-
default=False,
|
|
49
|
+
default=False,
|
|
50
|
+
description="If True, automatically create the target db_name in multi-db mode if it does not exist.",
|
|
22
51
|
)
|
|
52
|
+
|
|
53
|
+
use_multi_db: bool = Field(
|
|
54
|
+
default=True,
|
|
55
|
+
description=(
|
|
56
|
+
"If True: use Neo4j's multi-database feature for physical isolation; "
|
|
57
|
+
"each user typically gets a separate database. "
|
|
58
|
+
"If False: use a single shared database with logical isolation by user_name."
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
user_name: str | None = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description=(
|
|
65
|
+
"Logical user or tenant ID for data isolation. "
|
|
66
|
+
"Required if use_multi_db is False. "
|
|
67
|
+
"All nodes must be tagged with this and all queries must filter by this."
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
23
71
|
embedding_dimension: int = Field(default=768, description="Dimension of vector embedding")
|
|
24
72
|
|
|
73
|
+
@model_validator(mode="after")
|
|
74
|
+
def validate_config(self):
|
|
75
|
+
"""Validate logical constraints to avoid misconfiguration."""
|
|
76
|
+
if not self.use_multi_db and not self.user_name:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"In single-database mode (use_multi_db=False), `user_name` must be provided for logical isolation."
|
|
79
|
+
)
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Neo4jCommunityGraphDBConfig(Neo4jGraphDBConfig):
|
|
84
|
+
"""
|
|
85
|
+
Community edition config for Neo4j.
|
|
86
|
+
|
|
87
|
+
Notes:
|
|
88
|
+
- Must set `use_multi_db = False`
|
|
89
|
+
- Must provide `user_name` for logical isolation
|
|
90
|
+
- Embedding vector DB config is required
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
vec_config: VectorDBConfigFactory = Field(
|
|
94
|
+
..., description="Vector DB config for embedding search"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@model_validator(mode="after")
|
|
98
|
+
def validate_community(self):
|
|
99
|
+
if self.use_multi_db:
|
|
100
|
+
raise ValueError("Neo4j Community Edition does not support use_multi_db=True.")
|
|
101
|
+
if not self.user_name:
|
|
102
|
+
raise ValueError("Neo4j Community config requires user_name for logical isolation.")
|
|
103
|
+
return self
|
|
104
|
+
|
|
25
105
|
|
|
26
106
|
class GraphDBConfigFactory(BaseModel):
|
|
27
107
|
backend: str = Field(..., description="Backend for graph database")
|
|
@@ -29,6 +109,7 @@ class GraphDBConfigFactory(BaseModel):
|
|
|
29
109
|
|
|
30
110
|
backend_to_class: ClassVar[dict[str, Any]] = {
|
|
31
111
|
"neo4j": Neo4jGraphDBConfig,
|
|
112
|
+
"neo4j-community": Neo4jCommunityGraphDBConfig,
|
|
32
113
|
}
|
|
33
114
|
|
|
34
115
|
@field_validator("backend")
|