MemoryOS 0.2.1__py3-none-any.whl → 1.0.0__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 (92) hide show
  1. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/METADATA +7 -1
  2. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/RECORD +87 -64
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +158 -69
  5. memos/api/context/context.py +147 -0
  6. memos/api/context/dependencies.py +101 -0
  7. memos/api/product_models.py +5 -1
  8. memos/api/routers/product_router.py +54 -26
  9. memos/configs/graph_db.py +49 -1
  10. memos/configs/internet_retriever.py +19 -0
  11. memos/configs/mem_os.py +5 -0
  12. memos/configs/mem_reader.py +9 -0
  13. memos/configs/mem_scheduler.py +54 -18
  14. memos/configs/mem_user.py +58 -0
  15. memos/graph_dbs/base.py +38 -3
  16. memos/graph_dbs/factory.py +2 -0
  17. memos/graph_dbs/nebular.py +1612 -0
  18. memos/graph_dbs/neo4j.py +18 -9
  19. memos/log.py +6 -1
  20. memos/mem_cube/utils.py +13 -6
  21. memos/mem_os/core.py +157 -37
  22. memos/mem_os/main.py +2 -2
  23. memos/mem_os/product.py +252 -201
  24. memos/mem_os/utils/default_config.py +1 -1
  25. memos/mem_os/utils/format_utils.py +281 -70
  26. memos/mem_os/utils/reference_utils.py +133 -0
  27. memos/mem_reader/simple_struct.py +13 -5
  28. memos/mem_scheduler/base_scheduler.py +239 -266
  29. memos/mem_scheduler/{modules → general_modules}/base.py +4 -5
  30. memos/mem_scheduler/{modules → general_modules}/dispatcher.py +57 -21
  31. memos/mem_scheduler/general_modules/misc.py +104 -0
  32. memos/mem_scheduler/{modules → general_modules}/rabbitmq_service.py +12 -10
  33. memos/mem_scheduler/{modules → general_modules}/redis_service.py +1 -1
  34. memos/mem_scheduler/general_modules/retriever.py +199 -0
  35. memos/mem_scheduler/general_modules/scheduler_logger.py +261 -0
  36. memos/mem_scheduler/general_scheduler.py +243 -80
  37. memos/mem_scheduler/monitors/__init__.py +0 -0
  38. memos/mem_scheduler/monitors/dispatcher_monitor.py +305 -0
  39. memos/mem_scheduler/{modules/monitor.py → monitors/general_monitor.py} +106 -57
  40. memos/mem_scheduler/mos_for_test_scheduler.py +23 -20
  41. memos/mem_scheduler/schemas/__init__.py +0 -0
  42. memos/mem_scheduler/schemas/general_schemas.py +44 -0
  43. memos/mem_scheduler/schemas/message_schemas.py +149 -0
  44. memos/mem_scheduler/schemas/monitor_schemas.py +337 -0
  45. memos/mem_scheduler/utils/__init__.py +0 -0
  46. memos/mem_scheduler/utils/filter_utils.py +176 -0
  47. memos/mem_scheduler/utils/misc_utils.py +102 -0
  48. memos/mem_user/factory.py +94 -0
  49. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  50. memos/mem_user/mysql_user_manager.py +500 -0
  51. memos/mem_user/persistent_factory.py +96 -0
  52. memos/mem_user/user_manager.py +4 -4
  53. memos/memories/activation/item.py +5 -1
  54. memos/memories/activation/kv.py +20 -8
  55. memos/memories/textual/base.py +2 -2
  56. memos/memories/textual/general.py +36 -92
  57. memos/memories/textual/item.py +5 -33
  58. memos/memories/textual/tree.py +13 -7
  59. memos/memories/textual/tree_text_memory/organize/{conflict.py → handler.py} +34 -50
  60. memos/memories/textual/tree_text_memory/organize/manager.py +8 -96
  61. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +49 -43
  62. memos/memories/textual/tree_text_memory/organize/reorganizer.py +107 -142
  63. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +229 -0
  64. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  65. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +11 -0
  66. memos/memories/textual/tree_text_memory/retrieve/recall.py +15 -8
  67. memos/memories/textual/tree_text_memory/retrieve/reranker.py +1 -1
  68. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  69. memos/memories/textual/tree_text_memory/retrieve/searcher.py +191 -116
  70. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +47 -15
  71. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  72. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  73. memos/memos_tools/dinding_report_bot.py +422 -0
  74. memos/memos_tools/lockfree_dict.py +120 -0
  75. memos/memos_tools/notification_service.py +44 -0
  76. memos/memos_tools/notification_utils.py +96 -0
  77. memos/memos_tools/thread_safe_dict.py +288 -0
  78. memos/settings.py +3 -1
  79. memos/templates/mem_reader_prompts.py +4 -1
  80. memos/templates/mem_scheduler_prompts.py +62 -15
  81. memos/templates/mos_prompts.py +116 -0
  82. memos/templates/tree_reorganize_prompts.py +24 -17
  83. memos/utils.py +19 -0
  84. memos/mem_scheduler/modules/misc.py +0 -39
  85. memos/mem_scheduler/modules/retriever.py +0 -268
  86. memos/mem_scheduler/modules/schemas.py +0 -328
  87. memos/mem_scheduler/utils.py +0 -75
  88. memos/memories/textual/tree_text_memory/organize/redundancy.py +0 -193
  89. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/LICENSE +0 -0
  90. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/WHEEL +0 -0
  91. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/entry_points.txt +0 -0
  92. /memos/mem_scheduler/{modules → general_modules}/__init__.py +0 -0
@@ -0,0 +1,147 @@
1
+ """
2
+ Global request context management for trace_id and request-scoped data.
3
+
4
+ This module provides optional trace_id functionality that can be enabled
5
+ when using the API components. It uses ContextVar to ensure thread safety
6
+ and request isolation.
7
+ """
8
+
9
+ import uuid
10
+
11
+ from collections.abc import Callable
12
+ from contextvars import ContextVar
13
+ from typing import Any
14
+
15
+
16
+ # Global context variable for request-scoped data
17
+ _request_context: ContextVar[dict[str, Any] | None] = ContextVar("request_context", default=None)
18
+
19
+
20
+ class RequestContext:
21
+ """
22
+ Request-scoped context object that holds trace_id and other request data.
23
+
24
+ This provides a Flask g-like object for FastAPI applications.
25
+ """
26
+
27
+ def __init__(self, trace_id: str | None = None):
28
+ self.trace_id = trace_id or str(uuid.uuid4())
29
+ self._data: dict[str, Any] = {}
30
+
31
+ def set(self, key: str, value: Any) -> None:
32
+ """Set a value in the context."""
33
+ self._data[key] = value
34
+
35
+ def get(self, key: str, default: Any | None = None) -> Any:
36
+ """Get a value from the context."""
37
+ return self._data.get(key, default)
38
+
39
+ def __setattr__(self, name: str, value: Any) -> None:
40
+ if name.startswith("_") or name == "trace_id":
41
+ super().__setattr__(name, value)
42
+ else:
43
+ if not hasattr(self, "_data"):
44
+ super().__setattr__(name, value)
45
+ else:
46
+ self._data[name] = value
47
+
48
+ def __getattr__(self, name: str) -> Any:
49
+ if hasattr(self, "_data") and name in self._data:
50
+ return self._data[name]
51
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ """Convert context to dictionary."""
55
+ return {"trace_id": self.trace_id, "data": self._data.copy()}
56
+
57
+
58
+ def set_request_context(context: RequestContext) -> None:
59
+ """
60
+ Set the current request context.
61
+
62
+ This is typically called by the API dependency injection system.
63
+ """
64
+ _request_context.set(context.to_dict())
65
+
66
+
67
+ def get_current_trace_id() -> str | None:
68
+ """
69
+ Get the current request's trace_id.
70
+
71
+ Returns:
72
+ The trace_id if available, None otherwise.
73
+ """
74
+ context = _request_context.get()
75
+ if context:
76
+ return context.get("trace_id")
77
+ return None
78
+
79
+
80
+ def get_current_context() -> RequestContext | None:
81
+ """
82
+ Get the current request context.
83
+
84
+ Returns:
85
+ The current RequestContext if available, None otherwise.
86
+ """
87
+ context_dict = _request_context.get()
88
+ if context_dict:
89
+ ctx = RequestContext(trace_id=context_dict.get("trace_id"))
90
+ ctx._data = context_dict.get("data", {}).copy()
91
+ return ctx
92
+ return None
93
+
94
+
95
+ def require_context() -> RequestContext:
96
+ """
97
+ Get the current request context, raising an error if not available.
98
+
99
+ Returns:
100
+ The current RequestContext.
101
+
102
+ Raises:
103
+ RuntimeError: If called outside of a request context.
104
+ """
105
+ context = get_current_context()
106
+ if context is None:
107
+ raise RuntimeError(
108
+ "No request context available. This function must be called within a request handler."
109
+ )
110
+ return context
111
+
112
+
113
+ # Type for trace_id getter function
114
+ TraceIdGetter = Callable[[], str | None]
115
+
116
+ # Global variable to hold the trace_id getter function
117
+ _trace_id_getter: TraceIdGetter | None = None
118
+
119
+
120
+ def set_trace_id_getter(getter: TraceIdGetter) -> None:
121
+ """
122
+ Set a custom trace_id getter function.
123
+
124
+ This allows the logging system to retrieve trace_id without importing
125
+ API-specific general_modules.
126
+ """
127
+ global _trace_id_getter
128
+ _trace_id_getter = getter
129
+
130
+
131
+ def get_trace_id_for_logging() -> str | None:
132
+ """
133
+ Get trace_id for logging purposes.
134
+
135
+ This function is used by the logging system and will use either
136
+ the custom getter function or fall back to the default context.
137
+ """
138
+ if _trace_id_getter:
139
+ try:
140
+ return _trace_id_getter()
141
+ except Exception:
142
+ pass
143
+ return get_current_trace_id()
144
+
145
+
146
+ # Initialize the default trace_id getter
147
+ set_trace_id_getter(get_current_trace_id)
@@ -0,0 +1,101 @@
1
+ import logging
2
+ import os
3
+
4
+ from fastapi import Depends, Header, Request
5
+
6
+ from memos.api.context.context import RequestContext, set_request_context
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Type alias for the RequestContext from context module
12
+ G = RequestContext
13
+
14
+
15
+ def get_trace_id_from_header(
16
+ trace_id: str | None = Header(None, alias="trace-id"),
17
+ x_trace_id: str | None = Header(None, alias="x-trace-id"),
18
+ g_trace_id: str | None = Header(None, alias="g-trace-id"),
19
+ ) -> str | None:
20
+ """
21
+ Extract trace_id from various possible headers.
22
+
23
+ Priority: g-trace-id > x-trace-id > trace-id
24
+ """
25
+ return g_trace_id or x_trace_id or trace_id
26
+
27
+
28
+ def generate_trace_id() -> str:
29
+ """
30
+ Get a random trace_id.
31
+ """
32
+ return os.urandom(16).hex()
33
+
34
+
35
+ def get_request_context(
36
+ request: Request, trace_id: str | None = Depends(get_trace_id_from_header)
37
+ ) -> RequestContext:
38
+ """
39
+ Get request context object with trace_id and request metadata.
40
+
41
+ This function creates a RequestContext and automatically sets it
42
+ in the global context for use throughout the request lifecycle.
43
+ """
44
+ # Create context object
45
+ ctx = RequestContext(trace_id=trace_id)
46
+
47
+ # Set the context globally for this request
48
+ set_request_context(ctx)
49
+
50
+ # Log request start
51
+ logger.info(f"Request started with trace_id: {ctx.trace_id}")
52
+
53
+ # Add request metadata to context
54
+ ctx.set("method", request.method)
55
+ ctx.set("path", request.url.path)
56
+ ctx.set("client_ip", request.client.host if request.client else None)
57
+
58
+ return ctx
59
+
60
+
61
+ def get_g_object(trace_id: str | None = Depends(get_trace_id_from_header)) -> G:
62
+ """
63
+ Get Flask g-like object for the current request.
64
+
65
+ This creates a RequestContext and sets it globally for access
66
+ throughout the request lifecycle.
67
+ """
68
+ if trace_id is None:
69
+ trace_id = generate_trace_id()
70
+
71
+ g = RequestContext(trace_id=trace_id)
72
+ set_request_context(g)
73
+ logger.info(f"Request g object created with trace_id: {g.trace_id}")
74
+ return g
75
+
76
+
77
+ def get_current_g() -> G | None:
78
+ """
79
+ Get the current request's g object from anywhere in the application.
80
+
81
+ Returns:
82
+ The current request's g object if available, None otherwise.
83
+ """
84
+ from memos.context import get_current_context
85
+
86
+ return get_current_context()
87
+
88
+
89
+ def require_g() -> G:
90
+ """
91
+ Get the current request's g object, raising an error if not available.
92
+
93
+ Returns:
94
+ The current request's g object.
95
+
96
+ Raises:
97
+ RuntimeError: If called outside of a request context.
98
+ """
99
+ from memos.context import require_context
100
+
101
+ return require_context()
@@ -83,11 +83,12 @@ class ChatRequest(BaseRequest):
83
83
  query: str = Field(..., description="Chat query message")
84
84
  mem_cube_id: str | None = Field(None, description="Cube ID to use for chat")
85
85
  history: list[MessageDict] | None = Field(None, description="Chat history")
86
+ internet_search: bool = Field(True, description="Whether to use internet search")
86
87
 
87
88
 
88
89
  class UserCreate(BaseRequest):
89
90
  user_name: str | None = Field(None, description="Name of the user")
90
- role: str = Field("user", description="Role of the user")
91
+ role: str = Field("USER", description="Role of the user")
91
92
  user_id: str = Field(..., description="User ID")
92
93
 
93
94
 
@@ -142,6 +143,8 @@ class MemoryCreateRequest(BaseRequest):
142
143
  memory_content: str | None = Field(None, description="Memory content to store")
143
144
  doc_path: str | None = Field(None, description="Path to document to store")
144
145
  mem_cube_id: str | None = Field(None, description="Cube ID")
146
+ source: str | None = Field(None, description="Source of the memory")
147
+ user_profile: bool = Field(False, description="User profile memory")
145
148
 
146
149
 
147
150
  class SearchRequest(BaseRequest):
@@ -150,6 +153,7 @@ class SearchRequest(BaseRequest):
150
153
  user_id: str = Field(..., description="User ID")
151
154
  query: str = Field(..., description="Search query")
152
155
  mem_cube_id: str | None = Field(None, description="Cube ID to search in")
156
+ top_k: int = Field(10, description="Number of results to return")
153
157
 
154
158
 
155
159
  class SuggestionRequest(BaseRequest):
@@ -2,10 +2,14 @@ import json
2
2
  import logging
3
3
  import traceback
4
4
 
5
- from fastapi import APIRouter, HTTPException
5
+ from datetime import datetime
6
+ from typing import Annotated
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException
6
9
  from fastapi.responses import StreamingResponse
7
10
 
8
11
  from memos.api.config import APIConfig
12
+ from memos.api.context.dependencies import G, get_g_object
9
13
  from memos.api.product_models import (
10
14
  BaseResponse,
11
15
  ChatRequest,
@@ -22,6 +26,7 @@ from memos.api.product_models import (
22
26
  )
23
27
  from memos.configs.mem_os import MOSConfig
24
28
  from memos.mem_os.product import MOSProduct
29
+ from memos.memos_tools.notification_service import get_error_bot_function, get_online_bot_function
25
30
 
26
31
 
27
32
  logger = logging.getLogger(__name__)
@@ -37,16 +42,25 @@ def get_mos_product_instance():
37
42
  global MOS_PRODUCT_INSTANCE
38
43
  if MOS_PRODUCT_INSTANCE is None:
39
44
  default_config = APIConfig.get_product_default_config()
40
- print(default_config)
45
+ logger.info(f"*********init_default_mos_config********* {default_config}")
41
46
  from memos.configs.mem_os import MOSConfig
42
47
 
43
48
  mos_config = MOSConfig(**default_config)
44
49
 
45
50
  # Get default cube config from APIConfig (may be None if disabled)
46
51
  default_cube_config = APIConfig.get_default_cube_config()
47
- print("*********default_cube_config*********", default_cube_config)
52
+ logger.info(f"*********initdefault_cube_config******** {default_cube_config}")
53
+
54
+ # Get DingDing bot functions
55
+ dingding_enabled = APIConfig.is_dingding_bot_enabled()
56
+ online_bot = get_online_bot_function() if dingding_enabled else None
57
+ error_bot = get_error_bot_function() if dingding_enabled else None
58
+
48
59
  MOS_PRODUCT_INSTANCE = MOSProduct(
49
- default_config=mos_config, default_cube_config=default_cube_config
60
+ default_config=mos_config,
61
+ default_cube_config=default_cube_config,
62
+ online_bot=online_bot,
63
+ error_bot=error_bot,
50
64
  )
51
65
  logger.info("MOSProduct instance created successfully with inheritance architecture")
52
66
  return MOS_PRODUCT_INSTANCE
@@ -56,7 +70,7 @@ get_mos_product_instance()
56
70
 
57
71
 
58
72
  @router.post("/configure", summary="Configure MOSProduct", response_model=SimpleResponse)
59
- async def set_config(config):
73
+ def set_config(config):
60
74
  """Set MOSProduct configuration."""
61
75
  global MOS_PRODUCT_INSTANCE
62
76
  MOS_PRODUCT_INSTANCE = MOSProduct(default_config=config)
@@ -64,9 +78,18 @@ async def set_config(config):
64
78
 
65
79
 
66
80
  @router.post("/users/register", summary="Register a new user", response_model=UserRegisterResponse)
67
- async def register_user(user_req: UserRegisterRequest):
81
+ def register_user(user_req: UserRegisterRequest, g: Annotated[G, Depends(get_g_object)]):
68
82
  """Register a new user with configuration and default cube."""
69
83
  try:
84
+ # Set request-related information in g object
85
+ g.user_id = user_req.user_id
86
+ g.action = "user_register"
87
+ g.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
88
+
89
+ logger.info(f"Starting user registration for user_id: {user_req.user_id}")
90
+ logger.info(f"Request trace_id: {g.trace_id}")
91
+ logger.info(f"Request timestamp: {g.timestamp}")
92
+
70
93
  # Get configuration for the user
71
94
  user_config, default_mem_cube = APIConfig.create_user_config(
72
95
  user_name=user_req.user_id, user_id=user_req.user_id
@@ -100,7 +123,7 @@ async def register_user(user_req: UserRegisterRequest):
100
123
  @router.get(
101
124
  "/suggestions/{user_id}", summary="Get suggestion queries", response_model=SuggestionResponse
102
125
  )
103
- async def get_suggestion_queries(user_id: str):
126
+ def get_suggestion_queries(user_id: str):
104
127
  """Get suggestion queries for a specific user."""
105
128
  try:
106
129
  mos_product = get_mos_product_instance()
@@ -120,7 +143,7 @@ async def get_suggestion_queries(user_id: str):
120
143
  summary="Get suggestion queries with language",
121
144
  response_model=SuggestionResponse,
122
145
  )
123
- async def get_suggestion_queries_post(suggestion_req: SuggestionRequest):
146
+ def get_suggestion_queries_post(suggestion_req: SuggestionRequest):
124
147
  """Get suggestion queries for a specific user with language preference."""
125
148
  try:
126
149
  mos_product = get_mos_product_instance()
@@ -138,7 +161,7 @@ async def get_suggestion_queries_post(suggestion_req: SuggestionRequest):
138
161
 
139
162
 
140
163
  @router.post("/get_all", summary="Get all memories for user", response_model=MemoryResponse)
141
- async def get_all_memories(memory_req: GetMemoryRequest):
164
+ def get_all_memories(memory_req: GetMemoryRequest):
142
165
  """Get all memories for a specific user."""
143
166
  try:
144
167
  mos_product = get_mos_product_instance()
@@ -165,7 +188,7 @@ async def get_all_memories(memory_req: GetMemoryRequest):
165
188
 
166
189
 
167
190
  @router.post("/add", summary="add a new memory", response_model=SimpleResponse)
168
- async def create_memory(memory_req: MemoryCreateRequest):
191
+ def create_memory(memory_req: MemoryCreateRequest):
169
192
  """Create a new memory for a specific user."""
170
193
  try:
171
194
  mos_product = get_mos_product_instance()
@@ -175,6 +198,8 @@ async def create_memory(memory_req: MemoryCreateRequest):
175
198
  messages=memory_req.messages,
176
199
  doc_path=memory_req.doc_path,
177
200
  mem_cube_id=memory_req.mem_cube_id,
201
+ source=memory_req.source,
202
+ user_profile=memory_req.user_profile,
178
203
  )
179
204
  return SimpleResponse(message="Memory created successfully")
180
205
 
@@ -186,7 +211,7 @@ async def create_memory(memory_req: MemoryCreateRequest):
186
211
 
187
212
 
188
213
  @router.post("/search", summary="Search memories", response_model=SearchResponse)
189
- async def search_memories(search_req: SearchRequest):
214
+ def search_memories(search_req: SearchRequest):
190
215
  """Search memories for a specific user."""
191
216
  try:
192
217
  mos_product = get_mos_product_instance()
@@ -194,6 +219,7 @@ async def search_memories(search_req: SearchRequest):
194
219
  query=search_req.query,
195
220
  user_id=search_req.user_id,
196
221
  install_cube_ids=[search_req.mem_cube_id] if search_req.mem_cube_id else None,
222
+ top_k=search_req.top_k,
197
223
  )
198
224
  return SearchResponse(message="Search completed successfully", data=result)
199
225
 
@@ -205,24 +231,23 @@ async def search_memories(search_req: SearchRequest):
205
231
 
206
232
 
207
233
  @router.post("/chat", summary="Chat with MemOS")
208
- async def chat(chat_req: ChatRequest):
234
+ def chat(chat_req: ChatRequest):
209
235
  """Chat with MemOS for a specific user. Returns SSE stream."""
210
236
  try:
211
237
  mos_product = get_mos_product_instance()
212
238
 
213
- async def generate_chat_response():
239
+ def generate_chat_response():
214
240
  """Generate chat response as SSE stream."""
215
241
  try:
216
- import asyncio
217
-
218
- for chunk in mos_product.chat_with_references(
242
+ # Directly yield from the generator without async wrapper
243
+ yield from mos_product.chat_with_references(
219
244
  query=chat_req.query,
220
245
  user_id=chat_req.user_id,
221
246
  cube_id=chat_req.mem_cube_id,
222
247
  history=chat_req.history,
223
- ):
224
- yield chunk
225
- await asyncio.sleep(0.00001) # 50ms delay between chunks
248
+ internet_search=chat_req.internet_search,
249
+ )
250
+
226
251
  except Exception as e:
227
252
  logger.error(f"Error in chat stream: {e}")
228
253
  error_data = f"data: {json.dumps({'type': 'error', 'content': str(traceback.format_exc())})}\n\n"
@@ -230,11 +255,14 @@ async def chat(chat_req: ChatRequest):
230
255
 
231
256
  return StreamingResponse(
232
257
  generate_chat_response(),
233
- media_type="text/plain",
258
+ media_type="text/event-stream",
234
259
  headers={
235
260
  "Cache-Control": "no-cache",
236
261
  "Connection": "keep-alive",
237
262
  "Content-Type": "text/event-stream",
263
+ "Access-Control-Allow-Origin": "*",
264
+ "Access-Control-Allow-Headers": "*",
265
+ "Access-Control-Allow-Methods": "*",
238
266
  },
239
267
  )
240
268
 
@@ -246,7 +274,7 @@ async def chat(chat_req: ChatRequest):
246
274
 
247
275
 
248
276
  @router.get("/users", summary="List all users", response_model=BaseResponse[list])
249
- async def list_users():
277
+ def list_users():
250
278
  """List all registered users."""
251
279
  try:
252
280
  mos_product = get_mos_product_instance()
@@ -274,7 +302,7 @@ async def get_user_info(user_id: str):
274
302
  @router.get(
275
303
  "/configure/{user_id}", summary="Get MOSProduct configuration", response_model=SimpleResponse
276
304
  )
277
- async def get_config(user_id: str):
305
+ def get_config(user_id: str):
278
306
  """Get MOSProduct configuration."""
279
307
  global MOS_PRODUCT_INSTANCE
280
308
  config = MOS_PRODUCT_INSTANCE.default_config
@@ -284,7 +312,7 @@ async def get_config(user_id: str):
284
312
  @router.get(
285
313
  "/users/{user_id}/config", summary="Get user configuration", response_model=BaseResponse[dict]
286
314
  )
287
- async def get_user_config(user_id: str):
315
+ def get_user_config(user_id: str):
288
316
  """Get user-specific configuration."""
289
317
  try:
290
318
  mos_product = get_mos_product_instance()
@@ -308,7 +336,7 @@ async def get_user_config(user_id: str):
308
336
  @router.put(
309
337
  "/users/{user_id}/config", summary="Update user configuration", response_model=SimpleResponse
310
338
  )
311
- async def update_user_config(user_id: str, config_data: dict):
339
+ def update_user_config(user_id: str, config_data: dict):
312
340
  """Update user-specific configuration."""
313
341
  try:
314
342
  mos_product = get_mos_product_instance()
@@ -333,7 +361,7 @@ async def update_user_config(user_id: str, config_data: dict):
333
361
  @router.get(
334
362
  "/instances/status", summary="Get user configuration status", response_model=BaseResponse[dict]
335
363
  )
336
- async def get_instance_status():
364
+ def get_instance_status():
337
365
  """Get information about active user configurations in memory."""
338
366
  try:
339
367
  mos_product = get_mos_product_instance()
@@ -347,7 +375,7 @@ async def get_instance_status():
347
375
 
348
376
 
349
377
  @router.get("/instances/count", summary="Get active user count", response_model=BaseResponse[int])
350
- async def get_active_user_count():
378
+ def get_active_user_count():
351
379
  """Get the number of active user configurations in memory."""
352
380
  try:
353
381
  mos_product = get_mos_product_instance()
memos/configs/graph_db.py CHANGED
@@ -9,7 +9,7 @@ from memos.configs.vec_db import VectorDBConfigFactory
9
9
  class BaseGraphDBConfig(BaseConfig):
10
10
  """Base class for all graph database configurations."""
11
11
 
12
- uri: str
12
+ uri: str | list
13
13
  user: str
14
14
  password: str
15
15
 
@@ -103,6 +103,53 @@ class Neo4jCommunityGraphDBConfig(Neo4jGraphDBConfig):
103
103
  return self
104
104
 
105
105
 
106
+ class NebulaGraphDBConfig(BaseGraphDBConfig):
107
+ """
108
+ NebulaGraph-specific configuration.
109
+
110
+ Key concepts:
111
+ - `space`: Equivalent to a database or namespace. All tag/edge/schema live within a space.
112
+ - `user_name`: Used for logical tenant isolation if needed.
113
+ - `auto_create`: Whether to automatically create the target space if it does not exist.
114
+
115
+ Example:
116
+ ---
117
+ hosts = ["127.0.0.1:9669"]
118
+ user = "root"
119
+ password = "nebula"
120
+ space = "shared_graph"
121
+ user_name = "alice"
122
+ """
123
+
124
+ space: str = Field(
125
+ ..., description="The name of the target NebulaGraph space (like a database)"
126
+ )
127
+ user_name: str | None = Field(
128
+ default=None,
129
+ description="Logical user or tenant ID for data isolation (optional, used in metadata tagging)",
130
+ )
131
+ auto_create: bool = Field(
132
+ default=False,
133
+ description="Whether to auto-create the space if it does not exist",
134
+ )
135
+ use_multi_db: bool = Field(
136
+ default=True,
137
+ description=(
138
+ "If True: use Neo4j's multi-database feature for physical isolation; "
139
+ "each user typically gets a separate database. "
140
+ "If False: use a single shared database with logical isolation by user_name."
141
+ ),
142
+ )
143
+ embedding_dimension: int = Field(default=3072, description="Dimension of vector embedding")
144
+
145
+ @model_validator(mode="after")
146
+ def validate_config(self):
147
+ """Validate config."""
148
+ if not self.space:
149
+ raise ValueError("`space` must be provided")
150
+ return self
151
+
152
+
106
153
  class GraphDBConfigFactory(BaseModel):
107
154
  backend: str = Field(..., description="Backend for graph database")
108
155
  config: dict[str, Any] = Field(..., description="Configuration for the graph database backend")
@@ -110,6 +157,7 @@ class GraphDBConfigFactory(BaseModel):
110
157
  backend_to_class: ClassVar[dict[str, Any]] = {
111
158
  "neo4j": Neo4jGraphDBConfig,
112
159
  "neo4j-community": Neo4jCommunityGraphDBConfig,
160
+ "nebular": NebulaGraphDBConfig,
113
161
  }
114
162
 
115
163
  @field_validator("backend")
@@ -6,6 +6,7 @@ from pydantic import Field, field_validator, model_validator
6
6
 
7
7
  from memos.configs.base import BaseConfig
8
8
  from memos.exceptions import ConfigurationError
9
+ from memos.mem_reader.factory import MemReaderConfigFactory
9
10
 
10
11
 
11
12
  class BaseInternetRetrieverConfig(BaseConfig):
@@ -47,6 +48,23 @@ class XinyuSearchConfig(BaseInternetRetrieverConfig):
47
48
  num_per_request: int = Field(
48
49
  default=10, description="Number of results per API request (not used for Xinyu)"
49
50
  )
51
+ reader: MemReaderConfigFactory = Field(
52
+ ...,
53
+ default_factory=MemReaderConfigFactory,
54
+ description="Reader configuration",
55
+ )
56
+
57
+
58
+ class BochaSearchConfig(BaseInternetRetrieverConfig):
59
+ """Configuration class for Bocha Search API."""
60
+
61
+ max_results: int = Field(default=20, description="Maximum number of results to retrieve")
62
+ num_per_request: int = Field(default=10, description="Number of results per API request")
63
+ reader: MemReaderConfigFactory = Field(
64
+ ...,
65
+ default_factory=MemReaderConfigFactory,
66
+ description="Reader configuration",
67
+ )
50
68
 
51
69
 
52
70
  class InternetRetrieverConfigFactory(BaseConfig):
@@ -63,6 +81,7 @@ class InternetRetrieverConfigFactory(BaseConfig):
63
81
  "google": GoogleCustomSearchConfig,
64
82
  "bing": BingSearchConfig,
65
83
  "xinyu": XinyuSearchConfig,
84
+ "bocha": BochaSearchConfig,
66
85
  }
67
86
 
68
87
  @field_validator("backend")
memos/configs/mem_os.py CHANGED
@@ -8,6 +8,7 @@ from memos.configs.base import BaseConfig
8
8
  from memos.configs.llm import LLMConfigFactory
9
9
  from memos.configs.mem_reader import MemReaderConfigFactory
10
10
  from memos.configs.mem_scheduler import SchedulerConfigFactory
11
+ from memos.configs.mem_user import UserManagerConfigFactory
11
12
 
12
13
 
13
14
  class MOSConfig(BaseConfig):
@@ -33,6 +34,10 @@ class MOSConfig(BaseConfig):
33
34
  default=None,
34
35
  description="Memory scheduler configuration for managing memory operations",
35
36
  )
37
+ user_manager: UserManagerConfigFactory = Field(
38
+ default_factory=lambda: UserManagerConfigFactory(backend="sqlite", config={}),
39
+ description="User manager configuration for database operations",
40
+ )
36
41
  max_turns_window: int = Field(
37
42
  default=15,
38
43
  description="Maximum number of turns to keep in the conversation history",
@@ -15,6 +15,15 @@ class BaseMemReaderConfig(BaseConfig):
15
15
  created_at: datetime = Field(
16
16
  default_factory=datetime.now, description="Creation timestamp for the MemReader"
17
17
  )
18
+
19
+ @field_validator("created_at", mode="before")
20
+ @classmethod
21
+ def parse_datetime(cls, value):
22
+ """Parse datetime from string if needed."""
23
+ if isinstance(value, str):
24
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
25
+ return value
26
+
18
27
  llm: LLMConfigFactory = Field(..., description="LLM configuration for the MemReader")
19
28
  embedder: EmbedderConfigFactory = Field(
20
29
  ..., description="Embedder configuration for the MemReader"