MemoryOS 0.1.13__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.

Files changed (84) hide show
  1. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/METADATA +78 -49
  2. memoryos-0.2.1.dist-info/RECORD +152 -0
  3. memoryos-0.2.1.dist-info/entry_points.txt +3 -0
  4. memos/__init__.py +1 -1
  5. memos/api/config.py +471 -0
  6. memos/api/exceptions.py +28 -0
  7. memos/api/mcp_serve.py +502 -0
  8. memos/api/product_api.py +35 -0
  9. memos/api/product_models.py +159 -0
  10. memos/api/routers/__init__.py +1 -0
  11. memos/api/routers/product_router.py +358 -0
  12. memos/chunkers/sentence_chunker.py +8 -2
  13. memos/cli.py +113 -0
  14. memos/configs/embedder.py +27 -0
  15. memos/configs/graph_db.py +83 -2
  16. memos/configs/llm.py +48 -0
  17. memos/configs/mem_cube.py +1 -1
  18. memos/configs/mem_reader.py +4 -0
  19. memos/configs/mem_scheduler.py +91 -5
  20. memos/configs/memory.py +10 -4
  21. memos/dependency.py +52 -0
  22. memos/embedders/ark.py +92 -0
  23. memos/embedders/factory.py +4 -0
  24. memos/embedders/sentence_transformer.py +8 -2
  25. memos/embedders/universal_api.py +32 -0
  26. memos/graph_dbs/base.py +2 -2
  27. memos/graph_dbs/factory.py +2 -0
  28. memos/graph_dbs/item.py +46 -0
  29. memos/graph_dbs/neo4j.py +377 -101
  30. memos/graph_dbs/neo4j_community.py +300 -0
  31. memos/llms/base.py +9 -0
  32. memos/llms/deepseek.py +54 -0
  33. memos/llms/factory.py +10 -1
  34. memos/llms/hf.py +170 -13
  35. memos/llms/hf_singleton.py +114 -0
  36. memos/llms/ollama.py +4 -0
  37. memos/llms/openai.py +68 -1
  38. memos/llms/qwen.py +63 -0
  39. memos/llms/vllm.py +153 -0
  40. memos/mem_cube/general.py +77 -16
  41. memos/mem_cube/utils.py +102 -0
  42. memos/mem_os/core.py +131 -41
  43. memos/mem_os/main.py +93 -11
  44. memos/mem_os/product.py +1098 -35
  45. memos/mem_os/utils/default_config.py +352 -0
  46. memos/mem_os/utils/format_utils.py +1154 -0
  47. memos/mem_reader/simple_struct.py +13 -8
  48. memos/mem_scheduler/base_scheduler.py +467 -36
  49. memos/mem_scheduler/general_scheduler.py +125 -244
  50. memos/mem_scheduler/modules/base.py +9 -0
  51. memos/mem_scheduler/modules/dispatcher.py +68 -2
  52. memos/mem_scheduler/modules/misc.py +39 -0
  53. memos/mem_scheduler/modules/monitor.py +228 -49
  54. memos/mem_scheduler/modules/rabbitmq_service.py +317 -0
  55. memos/mem_scheduler/modules/redis_service.py +32 -22
  56. memos/mem_scheduler/modules/retriever.py +250 -23
  57. memos/mem_scheduler/modules/schemas.py +189 -7
  58. memos/mem_scheduler/mos_for_test_scheduler.py +143 -0
  59. memos/mem_scheduler/utils.py +51 -2
  60. memos/mem_user/persistent_user_manager.py +260 -0
  61. memos/memories/activation/item.py +25 -0
  62. memos/memories/activation/kv.py +10 -3
  63. memos/memories/activation/vllmkv.py +219 -0
  64. memos/memories/factory.py +2 -0
  65. memos/memories/textual/general.py +7 -5
  66. memos/memories/textual/item.py +3 -1
  67. memos/memories/textual/tree.py +14 -6
  68. memos/memories/textual/tree_text_memory/organize/conflict.py +198 -0
  69. memos/memories/textual/tree_text_memory/organize/manager.py +72 -23
  70. memos/memories/textual/tree_text_memory/organize/redundancy.py +193 -0
  71. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +233 -0
  72. memos/memories/textual/tree_text_memory/organize/reorganizer.py +606 -0
  73. memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
  74. memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
  75. memos/memories/textual/tree_text_memory/retrieve/searcher.py +6 -5
  76. memos/parsers/markitdown.py +8 -2
  77. memos/templates/mem_reader_prompts.py +105 -36
  78. memos/templates/mem_scheduler_prompts.py +96 -47
  79. memos/templates/tree_reorganize_prompts.py +223 -0
  80. memos/vec_dbs/base.py +12 -0
  81. memos/vec_dbs/qdrant.py +46 -20
  82. memoryos-0.1.13.dist-info/RECORD +0 -122
  83. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/LICENSE +0 -0
  84. {memoryos-0.1.13.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
- """Neo4j-specific configuration."""
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, description="Whether to create the DB if it doesn't exist"
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")