MemoryOS 0.2.1__py3-none-any.whl → 0.2.2__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.1.dist-info → memoryos-0.2.2.dist-info}/METADATA +2 -1
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/RECORD +72 -55
- memos/__init__.py +1 -1
- memos/api/config.py +156 -65
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +90 -0
- memos/api/product_models.py +5 -1
- memos/api/routers/product_router.py +54 -26
- memos/configs/graph_db.py +49 -1
- memos/configs/internet_retriever.py +6 -0
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +18 -4
- memos/configs/mem_user.py +58 -0
- memos/graph_dbs/base.py +9 -1
- memos/graph_dbs/factory.py +2 -0
- memos/graph_dbs/nebular.py +1364 -0
- memos/graph_dbs/neo4j.py +4 -4
- memos/log.py +1 -1
- memos/mem_cube/utils.py +13 -6
- memos/mem_os/core.py +140 -30
- memos/mem_os/main.py +1 -1
- memos/mem_os/product.py +266 -152
- memos/mem_os/utils/format_utils.py +314 -67
- memos/mem_reader/simple_struct.py +13 -5
- memos/mem_scheduler/base_scheduler.py +220 -250
- memos/mem_scheduler/general_scheduler.py +193 -73
- memos/mem_scheduler/modules/base.py +5 -5
- memos/mem_scheduler/modules/dispatcher.py +6 -9
- memos/mem_scheduler/modules/misc.py +81 -16
- memos/mem_scheduler/modules/monitor.py +52 -41
- memos/mem_scheduler/modules/rabbitmq_service.py +9 -7
- memos/mem_scheduler/modules/retriever.py +108 -191
- memos/mem_scheduler/modules/scheduler_logger.py +255 -0
- memos/mem_scheduler/mos_for_test_scheduler.py +16 -19
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +43 -0
- memos/mem_scheduler/schemas/message_schemas.py +148 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +329 -0
- memos/mem_scheduler/utils/__init__.py +0 -0
- memos/mem_scheduler/utils/filter_utils.py +176 -0
- memos/mem_scheduler/utils/misc_utils.py +61 -0
- memos/mem_user/factory.py +94 -0
- memos/mem_user/mysql_persistent_user_manager.py +271 -0
- memos/mem_user/mysql_user_manager.py +500 -0
- memos/mem_user/persistent_factory.py +96 -0
- memos/mem_user/user_manager.py +4 -4
- memos/memories/activation/item.py +4 -0
- memos/memories/textual/base.py +1 -1
- memos/memories/textual/general.py +35 -91
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +13 -7
- memos/memories/textual/tree_text_memory/organize/conflict.py +4 -2
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +47 -43
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +8 -5
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -23
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -15
- memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
- memos/memos_tools/dinding_report_bot.py +422 -0
- memos/memos_tools/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +96 -0
- memos/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +2 -1
- memos/templates/mem_scheduler_prompts.py +41 -7
- memos/templates/mos_prompts.py +87 -0
- memos/mem_scheduler/modules/schemas.py +0 -328
- memos/mem_scheduler/utils.py +0 -75
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/entry_points.txt +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 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,90 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, Header, Request
|
|
4
|
+
|
|
5
|
+
from memos.api.context.context import RequestContext, set_request_context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
# Type alias for the RequestContext from context module
|
|
11
|
+
G = RequestContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_trace_id_from_header(
|
|
15
|
+
trace_id: str | None = Header(None, alias="trace-id"),
|
|
16
|
+
x_trace_id: str | None = Header(None, alias="x-trace-id"),
|
|
17
|
+
g_trace_id: str | None = Header(None, alias="g-trace-id"),
|
|
18
|
+
) -> str | None:
|
|
19
|
+
"""
|
|
20
|
+
Extract trace_id from various possible headers.
|
|
21
|
+
|
|
22
|
+
Priority: g-trace-id > x-trace-id > trace-id
|
|
23
|
+
"""
|
|
24
|
+
return g_trace_id or x_trace_id or trace_id
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_request_context(
|
|
28
|
+
request: Request, trace_id: str | None = Depends(get_trace_id_from_header)
|
|
29
|
+
) -> RequestContext:
|
|
30
|
+
"""
|
|
31
|
+
Get request context object with trace_id and request metadata.
|
|
32
|
+
|
|
33
|
+
This function creates a RequestContext and automatically sets it
|
|
34
|
+
in the global context for use throughout the request lifecycle.
|
|
35
|
+
"""
|
|
36
|
+
# Create context object
|
|
37
|
+
ctx = RequestContext(trace_id=trace_id)
|
|
38
|
+
|
|
39
|
+
# Set the context globally for this request
|
|
40
|
+
set_request_context(ctx)
|
|
41
|
+
|
|
42
|
+
# Log request start
|
|
43
|
+
logger.info(f"Request started with trace_id: {ctx.trace_id}")
|
|
44
|
+
|
|
45
|
+
# Add request metadata to context
|
|
46
|
+
ctx.set("method", request.method)
|
|
47
|
+
ctx.set("path", request.url.path)
|
|
48
|
+
ctx.set("client_ip", request.client.host if request.client else None)
|
|
49
|
+
|
|
50
|
+
return ctx
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_g_object(trace_id: str | None = Depends(get_trace_id_from_header)) -> G:
|
|
54
|
+
"""
|
|
55
|
+
Get Flask g-like object for the current request.
|
|
56
|
+
|
|
57
|
+
This creates a RequestContext and sets it globally for access
|
|
58
|
+
throughout the request lifecycle.
|
|
59
|
+
"""
|
|
60
|
+
g = RequestContext(trace_id=trace_id)
|
|
61
|
+
set_request_context(g)
|
|
62
|
+
logger.info(f"Request g object created with trace_id: {g.trace_id}")
|
|
63
|
+
return g
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_current_g() -> G | None:
|
|
67
|
+
"""
|
|
68
|
+
Get the current request's g object from anywhere in the application.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The current request's g object if available, None otherwise.
|
|
72
|
+
"""
|
|
73
|
+
from memos.context import get_current_context
|
|
74
|
+
|
|
75
|
+
return get_current_context()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def require_g() -> G:
|
|
79
|
+
"""
|
|
80
|
+
Get the current request's g object, raising an error if not available.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The current request's g object.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
RuntimeError: If called outside of a request context.
|
|
87
|
+
"""
|
|
88
|
+
from memos.context import require_context
|
|
89
|
+
|
|
90
|
+
return require_context()
|
memos/api/product_models.py
CHANGED
|
@@ -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("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
def generate_chat_response():
|
|
214
240
|
"""Generate chat response as SSE stream."""
|
|
215
241
|
try:
|
|
216
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,11 @@ 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
|
+
)
|
|
50
56
|
|
|
51
57
|
|
|
52
58
|
class InternetRetrieverConfigFactory(BaseConfig):
|
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",
|
memos/configs/mem_reader.py
CHANGED
|
@@ -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"
|
memos/configs/mem_scheduler.py
CHANGED
|
@@ -6,12 +6,12 @@ from typing import Any, ClassVar
|
|
|
6
6
|
from pydantic import ConfigDict, Field, field_validator, model_validator
|
|
7
7
|
|
|
8
8
|
from memos.configs.base import BaseConfig
|
|
9
|
-
from memos.mem_scheduler.modules.
|
|
9
|
+
from memos.mem_scheduler.modules.misc import DictConversionMixin
|
|
10
|
+
from memos.mem_scheduler.schemas.general_schemas import (
|
|
10
11
|
BASE_DIR,
|
|
11
12
|
DEFAULT_ACT_MEM_DUMP_PATH,
|
|
12
13
|
DEFAULT_CONSUME_INTERVAL_SECONDS,
|
|
13
14
|
DEFAULT_THREAD__POOL_MAX_WORKERS,
|
|
14
|
-
DictConversionMixin,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
|
|
@@ -21,6 +21,7 @@ class BaseSchedulerConfig(BaseConfig):
|
|
|
21
21
|
top_k: int = Field(
|
|
22
22
|
default=10, description="Number of top candidates to consider in initial retrieval"
|
|
23
23
|
)
|
|
24
|
+
# TODO: The 'top_n' field is deprecated and will be removed in future versions.
|
|
24
25
|
top_n: int = Field(default=5, description="Number of final results to return after processing")
|
|
25
26
|
enable_parallel_dispatch: bool = Field(
|
|
26
27
|
default=True, description="Whether to enable parallel message processing using thread pool"
|
|
@@ -48,7 +49,7 @@ class GeneralSchedulerConfig(BaseSchedulerConfig):
|
|
|
48
49
|
default=300, description="Interval in seconds for updating activation memory"
|
|
49
50
|
)
|
|
50
51
|
context_window_size: int | None = Field(
|
|
51
|
-
default=
|
|
52
|
+
default=10, description="Size of the context window for conversation history"
|
|
52
53
|
)
|
|
53
54
|
act_mem_dump_path: str | None = Field(
|
|
54
55
|
default=DEFAULT_ACT_MEM_DUMP_PATH, # Replace with DEFAULT_ACT_MEM_DUMP_PATH
|
|
@@ -105,7 +106,20 @@ class RabbitMQConfig(
|
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
class GraphDBAuthConfig(BaseConfig):
|
|
108
|
-
uri: str = Field(
|
|
109
|
+
uri: str = Field(
|
|
110
|
+
default="bolt://localhost:7687",
|
|
111
|
+
description="URI for graph database access (e.g., bolt://host:port)",
|
|
112
|
+
)
|
|
113
|
+
user: str = Field(default="neo4j", description="Username for graph database authentication")
|
|
114
|
+
password: str = Field(
|
|
115
|
+
default="",
|
|
116
|
+
description="Password for graph database authentication",
|
|
117
|
+
min_length=8, # 建议密码最小长度
|
|
118
|
+
)
|
|
119
|
+
db_name: str = Field(default="neo4j", description="Database name to connect to")
|
|
120
|
+
auto_create: bool = Field(
|
|
121
|
+
default=True, description="Whether to automatically create the database if it doesn't exist"
|
|
122
|
+
)
|
|
109
123
|
|
|
110
124
|
|
|
111
125
|
class OpenAIConfig(BaseConfig):
|