Skip to content

Commit 1d71884

Browse files
authored
feat: add user manager factory pattern and product API enhancements (#166)
* feat: add user manager factory pattern and product API enhancements - Add user manager factory pattern with SQLite and MySQL backends - Add user manager configuration to MOSConfig - Add product API router and configuration - Add DingDing notification integration - Add notification service utilities - Update OpenAPI documentation * fix: change py to meos_tools * fix: code reorganize
1 parent bcc672c commit 1d71884

File tree

14 files changed

+1827
-28
lines changed

14 files changed

+1827
-28
lines changed

docs/openapi.json

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@
884884
"type": "string",
885885
"title": "Session Id",
886886
"description": "Session ID for the MOS. This is used to distinguish between different dialogue",
887-
"default": "a47d75a0-5ee8-473f-86c4-3f09073fd59f"
887+
"default": "842877f4-c3f7-4c22-ad38-5950026870fe"
888888
},
889889
"chat_model": {
890890
"$ref": "#/components/schemas/LLMConfigFactory",
@@ -905,6 +905,10 @@
905905
],
906906
"description": "Memory scheduler configuration for managing memory operations"
907907
},
908+
"user_manager": {
909+
"$ref": "#/components/schemas/UserManagerConfigFactory",
910+
"description": "User manager configuration for database operations"
911+
},
908912
"max_turns_window": {
909913
"type": "integer",
910914
"title": "Max Turns Window",
@@ -1370,6 +1374,25 @@
13701374
"title": "UserListResponse",
13711375
"description": "Response model for user list operations."
13721376
},
1377+
"UserManagerConfigFactory": {
1378+
"properties": {
1379+
"backend": {
1380+
"type": "string",
1381+
"title": "Backend",
1382+
"description": "Backend for user manager",
1383+
"default": "sqlite"
1384+
},
1385+
"config": {
1386+
"additionalProperties": true,
1387+
"type": "object",
1388+
"title": "Config",
1389+
"description": "Configuration for the user manager backend"
1390+
}
1391+
},
1392+
"type": "object",
1393+
"title": "UserManagerConfigFactory",
1394+
"description": "Factory for user manager configurations."
1395+
},
13731396
"UserResponse": {
13741397
"properties": {
13751398
"code": {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Example demonstrating the use of UserManagerFactory with different backends."""
2+
3+
from memos.configs.mem_user import UserManagerConfigFactory
4+
from memos.mem_user.factory import UserManagerFactory
5+
from memos.mem_user.persistent_factory import PersistentUserManagerFactory
6+
7+
8+
def example_sqlite_default():
9+
"""Example: Create SQLite user manager with default settings."""
10+
print("=== SQLite Default Example ===")
11+
12+
# Method 1: Using factory with minimal config
13+
user_manager = UserManagerFactory.create_sqlite()
14+
15+
# Method 2: Using config factory (equivalent)
16+
UserManagerConfigFactory(
17+
backend="sqlite",
18+
config={}, # Uses all defaults
19+
)
20+
21+
print(f"Created user manager: {type(user_manager).__name__}")
22+
print(f"Database path: {user_manager.db_path}")
23+
24+
# Test basic operations
25+
users = user_manager.list_users()
26+
print(f"Initial users: {[user.user_name for user in users]}")
27+
28+
user_manager.close()
29+
30+
31+
def example_sqlite_custom():
32+
"""Example: Create SQLite user manager with custom settings."""
33+
print("\n=== SQLite Custom Example ===")
34+
35+
config_factory = UserManagerConfigFactory(
36+
backend="sqlite", config={"db_path": "/tmp/custom_memos.db", "user_id": "admin"}
37+
)
38+
39+
user_manager = UserManagerFactory.from_config(config_factory)
40+
print(f"Created user manager: {type(user_manager).__name__}")
41+
print(f"Database path: {user_manager.db_path}")
42+
43+
# Test operations
44+
user_id = user_manager.create_user("test_user")
45+
print(f"Created user: {user_id}")
46+
47+
user_manager.close()
48+
49+
50+
def example_mysql():
51+
"""Example: Create MySQL user manager."""
52+
print("\n=== MySQL Example ===")
53+
54+
# Method 1: Using factory with parameters
55+
try:
56+
user_manager = UserManagerFactory.create_mysql(
57+
host="localhost",
58+
port=3306,
59+
username="root",
60+
password="your_password", # Replace with actual password
61+
database="test_memos_users",
62+
)
63+
64+
print(f"Created user manager: {type(user_manager).__name__}")
65+
print(f"Connection URL: {user_manager.connection_url}")
66+
67+
# Test operations
68+
users = user_manager.list_users()
69+
print(f"Users: {[user.user_name for user in users]}")
70+
71+
user_manager.close()
72+
73+
except Exception as e:
74+
print(f"MySQL connection failed (expected if not set up): {e}")
75+
76+
77+
def example_persistent_managers():
78+
"""Example: Create persistent user managers with configuration storage."""
79+
print("\n=== Persistent User Manager Examples ===")
80+
81+
# SQLite persistent manager
82+
config_factory = UserManagerConfigFactory(backend="sqlite", config={})
83+
84+
persistent_manager = PersistentUserManagerFactory.from_config(config_factory)
85+
print(f"Created persistent manager: {type(persistent_manager).__name__}")
86+
87+
# Test config operations
88+
from memos.configs.mem_os import MOSConfig
89+
90+
# Create a sample config (you might need to adjust this based on MOSConfig structure)
91+
try:
92+
# This is a simplified example - adjust based on actual MOSConfig requirements
93+
sample_config = MOSConfig() # Use default config
94+
95+
# Save user config
96+
success = persistent_manager.save_user_config("test_user", sample_config)
97+
print(f"Config saved: {success}")
98+
99+
# Retrieve user config
100+
retrieved_config = persistent_manager.get_user_config("test_user")
101+
print(f"Config retrieved: {retrieved_config is not None}")
102+
103+
except Exception as e:
104+
print(f"Config operations failed: {e}")
105+
106+
persistent_manager.close()
107+
108+
109+
if __name__ == "__main__":
110+
# Run all examples
111+
example_sqlite_default()

src/memos/api/config.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,34 @@ def is_default_cube_config_enabled() -> bool:
254254
"""Check if default cube config is enabled via environment variable."""
255255
return os.getenv("MOS_ENABLE_DEFAULT_CUBE_CONFIG", "false").lower() == "true"
256256

257+
@staticmethod
258+
def is_dingding_bot_enabled() -> bool:
259+
"""Check if DingDing bot is enabled via environment variable."""
260+
return os.getenv("ENABLE_DINGDING_BOT", "false").lower() == "true"
261+
262+
@staticmethod
263+
def get_dingding_bot_config() -> dict[str, Any] | None:
264+
"""Get DingDing bot configuration if enabled."""
265+
if not APIConfig.is_dingding_bot_enabled():
266+
return None
267+
268+
return {
269+
"enabled": True,
270+
"access_token_user": os.getenv("DINGDING_ACCESS_TOKEN_USER", ""),
271+
"secret_user": os.getenv("DINGDING_SECRET_USER", ""),
272+
"access_token_error": os.getenv("DINGDING_ACCESS_TOKEN_ERROR", ""),
273+
"secret_error": os.getenv("DINGDING_SECRET_ERROR", ""),
274+
"robot_code": os.getenv("DINGDING_ROBOT_CODE", ""),
275+
"app_key": os.getenv("DINGDING_APP_KEY", ""),
276+
"app_secret": os.getenv("DINGDING_APP_SECRET", ""),
277+
"oss_endpoint": os.getenv("OSS_ENDPOINT", ""),
278+
"oss_region": os.getenv("OSS_REGION", ""),
279+
"oss_bucket_name": os.getenv("OSS_BUCKET_NAME", ""),
280+
"oss_access_key_id": os.getenv("OSS_ACCESS_KEY_ID", ""),
281+
"oss_access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET", ""),
282+
"oss_public_base_url": os.getenv("OSS_PUBLIC_BASE_URL", ""),
283+
}
284+
257285
@staticmethod
258286
def get_product_default_config() -> dict[str, Any]:
259287
"""Get default configuration for Product API."""
@@ -431,10 +459,6 @@ def create_user_config(user_name: str, user_id: str) -> tuple[MOSConfig, General
431459
)
432460
else:
433461
raise ValueError(f"Invalid Neo4j backend: {graph_db_backend}")
434-
if os.getenv("ENABLE_INTERNET", "false").lower() == "true":
435-
default_cube_config.text_mem.config["internet_retriever"] = (
436-
APIConfig.get_internet_config()
437-
)
438462
default_mem_cube = GeneralMemCube(default_cube_config)
439463
return default_config, default_mem_cube
440464

src/memos/api/routers/product_router.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from memos.configs.mem_os import MOSConfig
2828
from memos.mem_os.product import MOSProduct
29+
from memos.memos_tools.notification_service import get_error_bot_function, get_online_bot_function
2930

3031

3132
logger = logging.getLogger(__name__)
@@ -49,8 +50,17 @@ def get_mos_product_instance():
4950
# Get default cube config from APIConfig (may be None if disabled)
5051
default_cube_config = APIConfig.get_default_cube_config()
5152
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+
5259
MOS_PRODUCT_INSTANCE = MOSProduct(
53-
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,
5464
)
5565
logger.info("MOSProduct instance created successfully with inheritance architecture")
5666
return MOS_PRODUCT_INSTANCE
@@ -60,15 +70,15 @@ def get_mos_product_instance():
6070

6171

6272
@router.post("/configure", summary="Configure MOSProduct", response_model=SimpleResponse)
63-
async def set_config(config):
73+
def set_config(config):
6474
"""Set MOSProduct configuration."""
6575
global MOS_PRODUCT_INSTANCE
6676
MOS_PRODUCT_INSTANCE = MOSProduct(default_config=config)
6777
return SimpleResponse(message="Configuration set successfully")
6878

6979

7080
@router.post("/users/register", summary="Register a new user", response_model=UserRegisterResponse)
71-
async def register_user(user_req: UserRegisterRequest, g: Annotated[G, Depends(get_g_object)]):
81+
def register_user(user_req: UserRegisterRequest, g: Annotated[G, Depends(get_g_object)]):
7282
"""Register a new user with configuration and default cube."""
7383
try:
7484
# Set request-related information in g object
@@ -113,7 +123,7 @@ async def register_user(user_req: UserRegisterRequest, g: Annotated[G, Depends(g
113123
@router.get(
114124
"/suggestions/{user_id}", summary="Get suggestion queries", response_model=SuggestionResponse
115125
)
116-
async def get_suggestion_queries(user_id: str):
126+
def get_suggestion_queries(user_id: str):
117127
"""Get suggestion queries for a specific user."""
118128
try:
119129
mos_product = get_mos_product_instance()
@@ -133,7 +143,7 @@ async def get_suggestion_queries(user_id: str):
133143
summary="Get suggestion queries with language",
134144
response_model=SuggestionResponse,
135145
)
136-
async def get_suggestion_queries_post(suggestion_req: SuggestionRequest):
146+
def get_suggestion_queries_post(suggestion_req: SuggestionRequest):
137147
"""Get suggestion queries for a specific user with language preference."""
138148
try:
139149
mos_product = get_mos_product_instance()
@@ -151,7 +161,7 @@ async def get_suggestion_queries_post(suggestion_req: SuggestionRequest):
151161

152162

153163
@router.post("/get_all", summary="Get all memories for user", response_model=MemoryResponse)
154-
async def get_all_memories(memory_req: GetMemoryRequest):
164+
def get_all_memories(memory_req: GetMemoryRequest):
155165
"""Get all memories for a specific user."""
156166
try:
157167
mos_product = get_mos_product_instance()
@@ -178,7 +188,7 @@ async def get_all_memories(memory_req: GetMemoryRequest):
178188

179189

180190
@router.post("/add", summary="add a new memory", response_model=SimpleResponse)
181-
async def create_memory(memory_req: MemoryCreateRequest):
191+
def create_memory(memory_req: MemoryCreateRequest):
182192
"""Create a new memory for a specific user."""
183193
try:
184194
mos_product = get_mos_product_instance()
@@ -199,7 +209,7 @@ async def create_memory(memory_req: MemoryCreateRequest):
199209

200210

201211
@router.post("/search", summary="Search memories", response_model=SearchResponse)
202-
async def search_memories(search_req: SearchRequest):
212+
def search_memories(search_req: SearchRequest):
203213
"""Search memories for a specific user."""
204214
try:
205215
mos_product = get_mos_product_instance()
@@ -219,37 +229,38 @@ async def search_memories(search_req: SearchRequest):
219229

220230

221231
@router.post("/chat", summary="Chat with MemOS")
222-
async def chat(chat_req: ChatRequest):
232+
def chat(chat_req: ChatRequest):
223233
"""Chat with MemOS for a specific user. Returns SSE stream."""
224234
try:
225235
mos_product = get_mos_product_instance()
226236

227-
async def generate_chat_response():
237+
def generate_chat_response():
228238
"""Generate chat response as SSE stream."""
229239
try:
230-
import asyncio
231-
232-
for chunk in mos_product.chat_with_references(
240+
# Directly yield from the generator without async wrapper
241+
yield from mos_product.chat_with_references(
233242
query=chat_req.query,
234243
user_id=chat_req.user_id,
235244
cube_id=chat_req.mem_cube_id,
236245
history=chat_req.history,
237246
internet_search=chat_req.internet_search,
238-
):
239-
yield chunk
240-
await asyncio.sleep(0.00001) # 50ms delay between chunks
247+
)
248+
241249
except Exception as e:
242250
logger.error(f"Error in chat stream: {e}")
243251
error_data = f"data: {json.dumps({'type': 'error', 'content': str(traceback.format_exc())})}\n\n"
244252
yield error_data
245253

246254
return StreamingResponse(
247255
generate_chat_response(),
248-
media_type="text/plain",
256+
media_type="text/event-stream",
249257
headers={
250258
"Cache-Control": "no-cache",
251259
"Connection": "keep-alive",
252260
"Content-Type": "text/event-stream",
261+
"Access-Control-Allow-Origin": "*",
262+
"Access-Control-Allow-Headers": "*",
263+
"Access-Control-Allow-Methods": "*",
253264
},
254265
)
255266

@@ -261,7 +272,7 @@ async def generate_chat_response():
261272

262273

263274
@router.get("/users", summary="List all users", response_model=BaseResponse[list])
264-
async def list_users():
275+
def list_users():
265276
"""List all registered users."""
266277
try:
267278
mos_product = get_mos_product_instance()
@@ -289,7 +300,7 @@ async def get_user_info(user_id: str):
289300
@router.get(
290301
"/configure/{user_id}", summary="Get MOSProduct configuration", response_model=SimpleResponse
291302
)
292-
async def get_config(user_id: str):
303+
def get_config(user_id: str):
293304
"""Get MOSProduct configuration."""
294305
global MOS_PRODUCT_INSTANCE
295306
config = MOS_PRODUCT_INSTANCE.default_config
@@ -299,7 +310,7 @@ async def get_config(user_id: str):
299310
@router.get(
300311
"/users/{user_id}/config", summary="Get user configuration", response_model=BaseResponse[dict]
301312
)
302-
async def get_user_config(user_id: str):
313+
def get_user_config(user_id: str):
303314
"""Get user-specific configuration."""
304315
try:
305316
mos_product = get_mos_product_instance()
@@ -323,7 +334,7 @@ async def get_user_config(user_id: str):
323334
@router.put(
324335
"/users/{user_id}/config", summary="Update user configuration", response_model=SimpleResponse
325336
)
326-
async def update_user_config(user_id: str, config_data: dict):
337+
def update_user_config(user_id: str, config_data: dict):
327338
"""Update user-specific configuration."""
328339
try:
329340
mos_product = get_mos_product_instance()
@@ -348,7 +359,7 @@ async def update_user_config(user_id: str, config_data: dict):
348359
@router.get(
349360
"/instances/status", summary="Get user configuration status", response_model=BaseResponse[dict]
350361
)
351-
async def get_instance_status():
362+
def get_instance_status():
352363
"""Get information about active user configurations in memory."""
353364
try:
354365
mos_product = get_mos_product_instance()
@@ -362,7 +373,7 @@ async def get_instance_status():
362373

363374

364375
@router.get("/instances/count", summary="Get active user count", response_model=BaseResponse[int])
365-
async def get_active_user_count():
376+
def get_active_user_count():
366377
"""Get the number of active user configurations in memory."""
367378
try:
368379
mos_product = get_mos_product_instance()

0 commit comments

Comments
 (0)