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.

Files changed (74) hide show
  1. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/METADATA +2 -1
  2. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/RECORD +72 -55
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +156 -65
  5. memos/api/context/context.py +147 -0
  6. memos/api/context/dependencies.py +90 -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 +6 -0
  11. memos/configs/mem_os.py +5 -0
  12. memos/configs/mem_reader.py +9 -0
  13. memos/configs/mem_scheduler.py +18 -4
  14. memos/configs/mem_user.py +58 -0
  15. memos/graph_dbs/base.py +9 -1
  16. memos/graph_dbs/factory.py +2 -0
  17. memos/graph_dbs/nebular.py +1364 -0
  18. memos/graph_dbs/neo4j.py +4 -4
  19. memos/log.py +1 -1
  20. memos/mem_cube/utils.py +13 -6
  21. memos/mem_os/core.py +140 -30
  22. memos/mem_os/main.py +1 -1
  23. memos/mem_os/product.py +266 -152
  24. memos/mem_os/utils/format_utils.py +314 -67
  25. memos/mem_reader/simple_struct.py +13 -5
  26. memos/mem_scheduler/base_scheduler.py +220 -250
  27. memos/mem_scheduler/general_scheduler.py +193 -73
  28. memos/mem_scheduler/modules/base.py +5 -5
  29. memos/mem_scheduler/modules/dispatcher.py +6 -9
  30. memos/mem_scheduler/modules/misc.py +81 -16
  31. memos/mem_scheduler/modules/monitor.py +52 -41
  32. memos/mem_scheduler/modules/rabbitmq_service.py +9 -7
  33. memos/mem_scheduler/modules/retriever.py +108 -191
  34. memos/mem_scheduler/modules/scheduler_logger.py +255 -0
  35. memos/mem_scheduler/mos_for_test_scheduler.py +16 -19
  36. memos/mem_scheduler/schemas/__init__.py +0 -0
  37. memos/mem_scheduler/schemas/general_schemas.py +43 -0
  38. memos/mem_scheduler/schemas/message_schemas.py +148 -0
  39. memos/mem_scheduler/schemas/monitor_schemas.py +329 -0
  40. memos/mem_scheduler/utils/__init__.py +0 -0
  41. memos/mem_scheduler/utils/filter_utils.py +176 -0
  42. memos/mem_scheduler/utils/misc_utils.py +61 -0
  43. memos/mem_user/factory.py +94 -0
  44. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  45. memos/mem_user/mysql_user_manager.py +500 -0
  46. memos/mem_user/persistent_factory.py +96 -0
  47. memos/mem_user/user_manager.py +4 -4
  48. memos/memories/activation/item.py +4 -0
  49. memos/memories/textual/base.py +1 -1
  50. memos/memories/textual/general.py +35 -91
  51. memos/memories/textual/item.py +5 -33
  52. memos/memories/textual/tree.py +13 -7
  53. memos/memories/textual/tree_text_memory/organize/conflict.py +4 -2
  54. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +47 -43
  55. memos/memories/textual/tree_text_memory/organize/reorganizer.py +8 -5
  56. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  57. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
  58. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  59. memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -23
  60. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -15
  61. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  62. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  63. memos/memos_tools/dinding_report_bot.py +422 -0
  64. memos/memos_tools/notification_service.py +44 -0
  65. memos/memos_tools/notification_utils.py +96 -0
  66. memos/settings.py +3 -1
  67. memos/templates/mem_reader_prompts.py +2 -1
  68. memos/templates/mem_scheduler_prompts.py +41 -7
  69. memos/templates/mos_prompts.py +87 -0
  70. memos/mem_scheduler/modules/schemas.py +0 -328
  71. memos/mem_scheduler/utils.py +0 -75
  72. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
  73. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
  74. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,422 @@
1
+ """dinding_report_bot.py"""
2
+
3
+ import base64
4
+ import contextlib
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import os
9
+ import time
10
+ import urllib.parse
11
+
12
+ from datetime import datetime
13
+ from uuid import uuid4
14
+
15
+ from dotenv import load_dotenv
16
+
17
+
18
+ load_dotenv()
19
+
20
+ try:
21
+ import io
22
+
23
+ import matplotlib
24
+ import matplotlib.font_manager as fm
25
+ import numpy as np
26
+ import oss2
27
+ import requests
28
+
29
+ from PIL import Image, ImageDraw, ImageFont
30
+
31
+ matplotlib.use("Agg")
32
+ from alibabacloud_dingtalk.robot_1_0 import models as robot_models
33
+ from alibabacloud_dingtalk.robot_1_0.client import Client as DingtalkRobotClient
34
+ from alibabacloud_tea_openapi import models as open_api_models
35
+ from alibabacloud_tea_util import models as util_models
36
+ except ImportError as e:
37
+ raise ImportError(
38
+ f"DingDing bot dependencies not found: {e}. "
39
+ "Please install required packages: pip install requests oss2 pillow matplotlib alibabacloud-dingtalk"
40
+ ) from e
41
+
42
+ # =========================
43
+ # 🔧 common tools
44
+ # =========================
45
+ ACCESS_TOKEN_USER = os.getenv("DINGDING_ACCESS_TOKEN_USER")
46
+ SECRET_USER = os.getenv("DINGDING_SECRET_USER")
47
+ ACCESS_TOKEN_ERROR = os.getenv("DINGDING_ACCESS_TOKEN_ERROR")
48
+ SECRET_ERROR = os.getenv("DINGDING_SECRET_ERROR")
49
+ OSS_CONFIG = {
50
+ "endpoint": os.getenv("OSS_ENDPOINT"),
51
+ "region": os.getenv("OSS_REGION"),
52
+ "bucket_name": os.getenv("OSS_BUCKET_NAME"),
53
+ "oss_access_key_id": os.getenv("OSS_ACCESS_KEY_ID"),
54
+ "oss_access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET"),
55
+ "public_base_url": os.getenv("OSS_PUBLIC_BASE_URL"),
56
+ }
57
+ ROBOT_CODE = os.getenv("DINGDING_ROBOT_CODE")
58
+ DING_APP_KEY = os.getenv("DINGDING_APP_KEY")
59
+ DING_APP_SECRET = os.getenv("DINGDING_APP_SECRET")
60
+
61
+
62
+ # Get access_token
63
+ def get_access_token():
64
+ url = f"https://oapi.dingtalk.com/gettoken?appkey={DING_APP_KEY}&appsecret={DING_APP_SECRET}"
65
+ resp = requests.get(url)
66
+ return resp.json()["access_token"]
67
+
68
+
69
+ def _pick_font(size: int = 48) -> ImageFont.ImageFont:
70
+ """
71
+ Try to find a font from the following candidates (macOS / Windows / Linux are common):
72
+ Helvetica → Arial → DejaVu Sans
73
+ If found, use truetype, otherwise return the default bitmap font.
74
+ """
75
+ candidates = ["Helvetica", "Arial", "DejaVu Sans"]
76
+ for name in candidates:
77
+ try:
78
+ font_path = fm.findfont(name, fallback_to_default=False)
79
+ return ImageFont.truetype(font_path, size)
80
+ except Exception:
81
+ continue
82
+ # Cannot find truetype, fallback to default and manually scale up
83
+ bitmap = ImageFont.load_default()
84
+ return ImageFont.FreeTypeFont(bitmap.path, size) if hasattr(bitmap, "path") else bitmap
85
+
86
+
87
+ def make_header(
88
+ title: str,
89
+ subtitle: str,
90
+ size=(1080, 260),
91
+ colors=("#C8F6E1", "#E8F8F5"), # Stylish mint green → lighter green
92
+ fg="#00956D",
93
+ ) -> bytes:
94
+ """
95
+ Generate a "Notification" banner with green gradient and bold large text.
96
+ title: main title (suggested ≤ 35 characters)
97
+ subtitle: sub title (e.g. "Notification")
98
+ """
99
+
100
+ # Can be placed inside or outside make_header
101
+ def _text_wh(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont):
102
+ """
103
+ return (width, height), compatible with both Pillow old version (textsize) and new version (textbbox)
104
+ """
105
+ if hasattr(draw, "textbbox"): # Pillow ≥ 8.0
106
+ left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
107
+ return right - left, bottom - top
108
+ else: # Pillow < 10.0
109
+ return draw.textsize(text, font=font)
110
+
111
+ w, h = size
112
+ # --- 1) background gradient ---
113
+ g = np.linspace(0, 1, w)
114
+ grad = np.outer(np.ones(h), g)
115
+ rgb0 = tuple(int(colors[0].lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
116
+ rgb1 = tuple(int(colors[1].lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
117
+ img = np.zeros((h, w, 3), dtype=np.uint8)
118
+ for i in range(3):
119
+ img[:, :, i] = rgb0[i] * (1 - grad) + rgb1[i] * grad
120
+ im = Image.fromarray(img)
121
+
122
+ # --- 2) text ---
123
+ draw = ImageDraw.Draw(im)
124
+ font_title = _pick_font(54) # main title
125
+ font_sub = _pick_font(30) # sub title
126
+
127
+ # center alignment
128
+ title_w, title_h = _text_wh(draw, title, font_title)
129
+ sub_w, sub_h = _text_wh(draw, subtitle, font_sub)
130
+
131
+ title_x = (w - title_w) // 2
132
+ title_y = h // 2 - title_h
133
+ sub_x = (w - sub_w) // 2
134
+ sub_y = title_y + title_h + 8
135
+
136
+ draw.text((title_x, title_y), title, fill=fg, font=font_title)
137
+ draw.text((sub_x, sub_y), subtitle, fill=fg, font=font_sub)
138
+
139
+ # --- 3) PNG bytes ---
140
+ buf = io.BytesIO()
141
+ im.save(buf, "PNG")
142
+ return buf.getvalue()
143
+
144
+
145
+ def _sign(secret: str, ts: str):
146
+ s = f"{ts}\n{secret}"
147
+ return urllib.parse.quote_plus(
148
+ base64.b64encode(hmac.new(secret.encode(), s.encode(), hashlib.sha256).digest())
149
+ )
150
+
151
+
152
+ def _send_md(title: str, md: str, type="user", at=None):
153
+ if type == "user":
154
+ access_token = ACCESS_TOKEN_USER
155
+ secret = SECRET_USER
156
+ else:
157
+ access_token = ACCESS_TOKEN_ERROR
158
+ secret = SECRET_ERROR
159
+ ts = str(round(time.time() * 1000))
160
+ url = (
161
+ f"https://oapi.dingtalk.com/robot/send?access_token={access_token}"
162
+ f"&timestamp={ts}&sign={_sign(secret, ts)}"
163
+ )
164
+ payload = {
165
+ "msgtype": "markdown",
166
+ "markdown": {"title": title, "text": md},
167
+ "at": at or {"atUserIds": [], "isAtAll": False},
168
+ }
169
+ requests.post(url, headers={"Content-Type": "application/json"}, data=json.dumps(payload))
170
+
171
+
172
+ # ------------------------- OSS -------------------------
173
+ def upload_bytes_to_oss(
174
+ data: bytes,
175
+ oss_dir: str = "xcy-share/jfzt/",
176
+ filename: str | None = None,
177
+ keep_latest: int = 1, # Keep latest N files; 0 = delete all
178
+ ) -> str:
179
+ """
180
+ - If filename_prefix is provided, delete the older files in {oss_dir}/{prefix}_*.png, only keep the latest keep_latest files
181
+ - Always create <prefix>_<timestamp>_<uuid>.png → ensure the URL is unique
182
+ """
183
+ filename_prefix = filename
184
+
185
+ conf = OSS_CONFIG
186
+ auth = oss2.Auth(conf["oss_access_key_id"], conf["oss_access_key_secret"])
187
+ bucket = oss2.Bucket(auth, conf["endpoint"], conf["bucket_name"])
188
+
189
+ # ---------- delete old files ----------
190
+ if filename_prefix and keep_latest >= 0:
191
+ prefix_path = f"{oss_dir.rstrip('/')}/{filename_prefix}_"
192
+ objs = bucket.list_objects(prefix=prefix_path).object_list
193
+ old_files = [(o.key, o.last_modified) for o in objs if o.key.endswith(".png")]
194
+ if old_files and len(old_files) > keep_latest:
195
+ # sort by last_modified from new to old
196
+ old_files.sort(key=lambda x: x[1], reverse=True)
197
+ to_del = [k for k, _ in old_files[keep_latest:]]
198
+ for k in to_del:
199
+ with contextlib.suppress(Exception):
200
+ bucket.delete_object(k)
201
+
202
+ # ---------- upload new file ----------
203
+ ts = int(time.time())
204
+ uniq = uuid4().hex
205
+ prefix = f"{filename_prefix}_" if filename_prefix else ""
206
+ object_name = f"{oss_dir.rstrip('/')}/{prefix}{ts}_{uniq}.png"
207
+ bucket.put_object(object_name, data)
208
+
209
+ return f"{conf['public_base_url'].rstrip('/')}/{object_name}"
210
+
211
+
212
+ # --------- Markdown Table Helper ---------
213
+ def _md_table(data: dict, is_error: bool = False) -> str:
214
+ """
215
+ Render a dict to a DingTalk-compatible Markdown table
216
+ - Normal statistics: single row, multiple columns
217
+ - Error distribution: two columns, multiple rows (error information/occurrence count)
218
+ """
219
+ if is_error: # {"error_info":{idx:val}, "occurrence_count":{idx:val}}
220
+ header = "| error | count |\n|---|---|"
221
+ rows = "\n".join(
222
+ f"| {err} | {cnt} |"
223
+ for err, cnt in zip(data["error"].values(), data["count"].values(), strict=False)
224
+ )
225
+ return f"{header}\n{rows}"
226
+
227
+ # normal statistics
228
+ header = "| " + " | ".join(data.keys()) + " |\n|" + "|".join(["---"] * len(data)) + "|"
229
+ row = "| " + " | ".join(map(str, data.values())) + " |"
230
+ return f"{header}\n{row}"
231
+
232
+
233
+ def upload_to_oss(
234
+ local_path: str,
235
+ oss_dir: str = "xcy-share/jfzt/",
236
+ filename: str | None = None, # ← Same addition
237
+ ) -> str:
238
+ """Upload a local file to OSS, support overwrite"""
239
+ with open(local_path, "rb") as f:
240
+ return upload_bytes_to_oss(f.read(), oss_dir=oss_dir, filename=filename)
241
+
242
+
243
+ def send_ding_reminder(
244
+ access_token: str, robot_code: str, user_ids: list[str], content: str, remind_type: int = 0
245
+ ):
246
+ """
247
+ :param access_token: DingTalk access_token (usually permanent when using a robot)
248
+ :param robot_code: Robot code applied on the open platform
249
+ :param user_ids: DingTalk user_id list
250
+ :param content: Message content to send
251
+ :param remind_type: 1=in-app notification, 2=phone reminder, 3=SMS reminder
252
+ """
253
+ # initialize client
254
+ config = open_api_models.Config(protocol="https", region_id="central")
255
+ client = DingtalkRobotClient(config)
256
+
257
+ # request headers
258
+ headers = robot_models.RobotSendDingHeaders(x_acs_dingtalk_access_token=access_token)
259
+
260
+ # request body
261
+ req = robot_models.RobotSendDingRequest(
262
+ robot_code=robot_code,
263
+ remind_type=remind_type,
264
+ receiver_user_id_list=user_ids,
265
+ content=content,
266
+ )
267
+
268
+ # send
269
+ try:
270
+ client.robot_send_ding_with_options(req, headers, util_models.RuntimeOptions())
271
+ print("✅ DING message sent successfully")
272
+ except Exception as e:
273
+ print("❌ DING message sent failed:", e)
274
+
275
+
276
+ def error_bot(
277
+ err: str,
278
+ title: str = "Error Alert",
279
+ level: str = "P2", # ← Add alert level
280
+ user_ids: list[str] | None = None, # ← @users in group
281
+ ):
282
+ """
283
+ send error alert
284
+ level can be set to P0 / P1 / P2, corresponding to red / orange / yellow
285
+ if title_color is provided, it will be overridden by level
286
+ """
287
+ # ---------- Level → Color scheme & Emoji ----------
288
+ level_map = {
289
+ "P0": {"color": "#C62828", "grad": ("#FFE4E4", "#FFD3D3"), "emoji": "🔴"},
290
+ "P1": {"color": "#E65100", "grad": ("#FFE9D6", "#FFD7B5"), "emoji": "🟠"},
291
+ "P2": {"color": "#EF6C00", "grad": ("#FFF6D8", "#FFECB5"), "emoji": "🟡"},
292
+ }
293
+ lv = level.upper()
294
+ if lv not in level_map:
295
+ lv = "P0" # Default to P0 if invalid
296
+ style = level_map[lv]
297
+
298
+ # If external title_color is specified, override with level color scheme
299
+ title_color = style["color"]
300
+
301
+ # ---------- Generate gradient banner ----------
302
+ banner_bytes = make_header(
303
+ title=f"Level {lv}", # Fixed English
304
+ subtitle="Error Alert", # Display level
305
+ colors=style["grad"],
306
+ fg=style["color"],
307
+ )
308
+ banner_url = upload_bytes_to_oss(
309
+ banner_bytes,
310
+ filename=f"error_banner_{title}_{lv.lower()}.png", # Overwrite fixed file for each level
311
+ )
312
+
313
+ # ---------- Markdown ----------
314
+ colored_title = f"<font color='{title_color}' size='4'><b>{title}</b></font>"
315
+ at_suffix = ""
316
+ if user_ids:
317
+ at_suffix = "\n\n" + " ".join([f"@{m}" for m in user_ids])
318
+
319
+ md = (
320
+ f"![banner]({banner_url})\n\n"
321
+ f"### {style['emoji']} <font color='{style['color']}' size='4'><b>{colored_title}</b></font>\n\n"
322
+ f"**Detail:**\n```\n{err}\n```\n"
323
+ # Visual indicator, pure color, no notification trigger
324
+ f"### 🔵 <font color='#1565C0' size='4'><b>Attention:{at_suffix}</b></font>\n\n"
325
+ f"<font color='#9E9E9E' size='1'>Time: "
326
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</font>\n"
327
+ )
328
+
329
+ # ---------- Send Markdown in group and @users ----------
330
+ at_config = {"atUserIds": user_ids or [], "isAtAll": False}
331
+ _send_md(title, md, type="error", at=at_config)
332
+
333
+ user_ids_for_ding = user_ids # DingTalk user_id list
334
+ message = f"{title}\nMemos system error, please handle immediately"
335
+
336
+ token = get_access_token()
337
+
338
+ send_ding_reminder(
339
+ access_token=token,
340
+ robot_code=ROBOT_CODE,
341
+ user_ids=user_ids_for_ding,
342
+ content=message,
343
+ remind_type=3 if level == "P0" else 1, # 1 in-app DING 2 SMS DING 3 phone DING
344
+ )
345
+
346
+
347
+ # --------- online_bot ---------
348
+ # ---------- Convert dict → colored KV lines ----------
349
+ def _kv_lines(d: dict, emoji: str = "", heading: str = "", heading_color: str = "#00956D") -> str:
350
+ """
351
+ Returns:
352
+ ### 📅 <font color='#00956D'><b>Daily Summary</b></font>
353
+ - **Request count:** 1364
354
+ ...
355
+ """
356
+ parts = [f"### {emoji} <font color='{heading_color}' size='3'><b>{heading}</b></font>"]
357
+ parts += [f"- **{k}:** {v}" for k, v in d.items()]
358
+ return "\n".join(parts)
359
+
360
+
361
+ # -------------- online_bot(colored title version) -----------------
362
+ def online_bot(
363
+ header_name: str,
364
+ sub_title_name: str,
365
+ title_color: str,
366
+ other_data1: dict,
367
+ other_data2: dict,
368
+ emoji: dict,
369
+ ):
370
+ heading_color = "#00956D" # Green for subtitle
371
+
372
+ # 0) Banner
373
+ banner_bytes = make_header(header_name, sub_title_name)
374
+ banner_url = upload_bytes_to_oss(banner_bytes, filename="online_report.png")
375
+
376
+ # 1) Colored main title
377
+ colored_title = f"<font color='{title_color}' size='4'><b>{header_name}</b></font>"
378
+
379
+ # 3) Markdown
380
+ md = "\n\n".join(
381
+ filter(
382
+ None,
383
+ [
384
+ f"![banner]({banner_url})",
385
+ f"### 🙄 <font color='{heading_color}' size='4'><b>{colored_title}</b></font>\n\n",
386
+ _kv_lines(
387
+ other_data1,
388
+ next(iter(emoji.keys())),
389
+ next(iter(emoji.values())),
390
+ heading_color=heading_color,
391
+ ),
392
+ _kv_lines(
393
+ other_data2,
394
+ list(emoji.keys())[1],
395
+ list(emoji.values())[1],
396
+ heading_color=heading_color,
397
+ ),
398
+ f"<font color='#9E9E9E' size='1'>Time: "
399
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</font>\n",
400
+ ],
401
+ )
402
+ )
403
+
404
+ _send_md(colored_title, md, type="user")
405
+
406
+
407
+ if __name__ == "__main__":
408
+ other_data = {
409
+ "recent_overall_data": "what is memos",
410
+ "site_data": "**📊 Simulated content\nLa la la <font color='red'>320</font>hahaha<font "
411
+ "color='red'>155</font>",
412
+ }
413
+
414
+ online_bot(
415
+ header_name="TextualMemory", # must in English
416
+ sub_title_name="Search", # must in English
417
+ title_color="#00956D",
418
+ other_data1={"Retrieval source 1": "This is plain text memory retrieval content blablabla"},
419
+ other_data2=other_data,
420
+ emoji={"Plain text memory retrieval source": "😨", "Retrieval content": "🕰🐛"},
421
+ )
422
+ print("All messages sent successfully")
@@ -0,0 +1,44 @@
1
+ """
2
+ Simple online_bot integration utility.
3
+ """
4
+
5
+ import logging
6
+
7
+ from collections.abc import Callable
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def get_online_bot_function() -> Callable | None:
14
+ """
15
+ Get online_bot function if available, otherwise return None.
16
+
17
+ Returns:
18
+ online_bot function if available, None otherwise
19
+ """
20
+ try:
21
+ from memos.memos_tools.dinding_report_bot import online_bot
22
+
23
+ logger.info("online_bot function loaded successfully")
24
+ return online_bot
25
+ except ImportError as e:
26
+ logger.warning(f"Failed to import online_bot: {e}, returning None")
27
+ return None
28
+
29
+
30
+ def get_error_bot_function() -> Callable | None:
31
+ """
32
+ Get error_bot function if available, otherwise return None.
33
+
34
+ Returns:
35
+ error_bot function if available, None otherwise
36
+ """
37
+ try:
38
+ from memos.memos_tools.dinding_report_bot import error_bot
39
+
40
+ logger.info("error_bot function loaded successfully")
41
+ return error_bot
42
+ except ImportError as e:
43
+ logger.warning(f"Failed to import error_bot: {e}, returning None")
44
+ return None
@@ -0,0 +1,96 @@
1
+ """
2
+ Notification utilities for MemOS product.
3
+ """
4
+
5
+ import logging
6
+
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def send_online_bot_notification(
15
+ online_bot: Callable | None,
16
+ header_name: str,
17
+ sub_title_name: str,
18
+ title_color: str,
19
+ other_data1: dict[str, Any],
20
+ other_data2: dict[str, Any],
21
+ emoji: dict[str, str],
22
+ ) -> None:
23
+ """
24
+ Send notification via online_bot if available.
25
+
26
+ Args:
27
+ online_bot: The online_bot function or None
28
+ header_name: Header name for the report
29
+ sub_title_name: Subtitle for the report
30
+ title_color: Title color
31
+ other_data1: First data dict
32
+ other_data2: Second data dict
33
+ emoji: Emoji configuration dict
34
+ """
35
+ if online_bot is None:
36
+ return
37
+
38
+ try:
39
+ online_bot(
40
+ header_name=header_name,
41
+ sub_title_name=sub_title_name,
42
+ title_color=title_color,
43
+ other_data1=other_data1,
44
+ other_data2=other_data2,
45
+ emoji=emoji,
46
+ )
47
+
48
+ logger.info(f"Online bot notification sent successfully: {header_name}")
49
+
50
+ except Exception as e:
51
+ logger.warning(f"Failed to send online bot notification: {e}")
52
+
53
+
54
+ def send_error_bot_notification(
55
+ error_bot: Callable | None,
56
+ err: str,
57
+ title: str = "MemOS Error",
58
+ level: str = "P2",
59
+ user_ids: list | None = None,
60
+ ) -> None:
61
+ """
62
+ Send error alert if error_bot is available.
63
+
64
+ Args:
65
+ error_bot: The error_bot function or None
66
+ err: Error message
67
+ title: Alert title
68
+ level: Alert level (P0, P1, P2)
69
+ user_ids: List of user IDs to notify
70
+ """
71
+ if error_bot is None:
72
+ return
73
+
74
+ try:
75
+ error_bot(
76
+ err=err,
77
+ title=title,
78
+ level=level,
79
+ user_ids=user_ids or [],
80
+ )
81
+ logger.info(f"Error alert sent successfully: {title}")
82
+ except Exception as e:
83
+ logger.warning(f"Failed to send error alert: {e}")
84
+
85
+
86
+ # Keep backward compatibility
87
+ def send_error_alert(
88
+ error_bot: Callable | None,
89
+ error_message: str,
90
+ title: str = "MemOS Error",
91
+ level: str = "P2",
92
+ ) -> None:
93
+ """
94
+ Send error alert if error_bot is available (backward compatibility).
95
+ """
96
+ send_error_bot_notification(error_bot, error_message, title, level)
memos/settings.py CHANGED
@@ -1,7 +1,9 @@
1
+ import os
2
+
1
3
  from pathlib import Path
2
4
 
3
5
 
4
- MEMOS_DIR = Path.cwd() / ".memos"
6
+ MEMOS_DIR = Path(os.getenv("MEMOS_BASE_PATH", Path.cwd())) / ".memos"
5
7
  DEBUG = False
6
8
 
7
9
  # "memos" or "memos.submodules" ... to filter logs from specific packages
@@ -16,6 +16,7 @@ For example, write "The user felt exhausted..." instead of "I felt exhausted..."
16
16
  - Include all key experiences, thoughts, emotional responses, and plans — even if they seem minor.
17
17
  - Prioritize completeness and fidelity over conciseness.
18
18
  - Do not generalize or skip details that could be personally meaningful to user.
19
+ 5. Please avoid any content that violates national laws and regulations or involves politically sensitive information in the memories you extract.
19
20
 
20
21
  Return a single valid JSON object with the following structure:
21
22
 
@@ -150,7 +151,7 @@ Output:
150
151
  "summary": "Tom is currently focused on managing a new project with a tight schedule. After a team meeting on June 25, 2025, he realized the original deadline of December 15 might not be feasible due to backend delays. Concerned about insufficient testing time, he welcomed Jerry’s suggestion of proposing an extension. Tom plans to raise the idea of shifting the deadline to January 5, 2026 in the next morning’s meeting. His actions reflect both stress about timelines and a proactive, team-oriented problem-solving approach."
151
152
  }
152
153
 
153
- Another Example in Chinese (注意: 你的输出必须和输入的user语言一致):
154
+ Another Example in Chinese (注意: user的语言为中文时,你就需要也输出中文):
154
155
  {
155
156
  "memory list": [
156
157
  {
@@ -72,13 +72,18 @@ Reorganize the provided memory evidence list by:
72
72
  3. Sorting evidence in descending order of relevance
73
73
  4. Maintaining all original items (no additions or deletions)
74
74
 
75
+ ## Temporal Priority Rules
76
+ - Query recency matters: Index 0 is the MOST RECENT query
77
+ - Evidence matching recent queries gets higher priority
78
+ - For equal relevance scores: Favor items matching newer queries
79
+
75
80
  ## Input Format
76
81
  - Queries: Recent user questions/requests (list)
77
- - Current Order: Existing memory sequence (list)
82
+ - Current Order: Existing memory sequence (list of strings with indices)
78
83
 
79
84
  ## Output Requirements
80
85
  Return a JSON object with:
81
- - "new_order": The reordered list (maintaining all original items)
86
+ - "new_order": The reordered indices (array of integers)
82
87
  - "reasoning": Brief explanation of your ranking logic (1-2 sentences)
83
88
 
84
89
  ## Processing Guidelines
@@ -89,26 +94,55 @@ Return a JSON object with:
89
94
  - Shows temporal relevance (newer > older)
90
95
  2. For ambiguous cases, maintain original relative ordering
91
96
 
97
+ ## Scoring Priorities (Descending Order)
98
+ 1. Direct matches to newer queries
99
+ 2. Exact keyword matches in recent queries
100
+ 3. Contextual support for recent topics
101
+ 4. General relevance to older queries
102
+
92
103
  ## Example
93
- Input queries: ["python threading best practices"]
94
- Input order: ["basic python syntax", "thread safety patterns", "data structures"]
104
+ Input queries: ["[0] python threading", "[1] data visualization"]
105
+ Input order: ["[0] syntax", "[1] matplotlib", "[2] threading"]
95
106
 
96
107
  Output:
97
108
  {{
98
- "new_order": ["thread safety patterns", "data structures", "basic python syntax"],
99
- "reasoning": "Prioritized threading-related content while maintaining general python references"
109
+ "new_order": [2, 1, 0],
110
+ "reasoning": "Threading (2) prioritized for matching newest query, followed by matplotlib (1) for older visualization query",
100
111
  }}
101
112
 
102
113
  ## Current Task
103
- Queries: {queries}
114
+ Queries: {queries} (recency-ordered)
104
115
  Current order: {current_order}
105
116
 
106
117
  Please provide your reorganization:
107
118
  """
108
119
 
120
+ QUERY_KEYWORDS_EXTRACTION_PROMPT = """
121
+ ## Role
122
+ You are an intelligent keyword extraction system. Your task is to identify and extract the most important words or short phrases from user queries.
123
+
124
+ ## Instructions
125
+ - They have to be single words or short phrases that make sense.
126
+ - Only nouns (naming words) or verbs (action words) are allowed.
127
+ - Don't include stop words (like "the", "is") or adverbs (words that describe verbs, like "quickly").
128
+ - Keep them as the smallest possible units that still have meaning.
129
+
130
+ ## Example
131
+ - Input Query: "What breed is Max?"
132
+ - Output Keywords (list of string): ["breed", "Max"]
133
+
134
+ ## Current Task
135
+ - Query: {query}
136
+ - Output Format: A Json list of keywords.
137
+
138
+ Answer:
139
+ """
140
+
141
+
109
142
  PROMPT_MAPPING = {
110
143
  "intent_recognizing": INTENT_RECOGNIZING_PROMPT,
111
144
  "memory_reranking": MEMORY_RERANKING_PROMPT,
145
+ "query_keywords_extraction": QUERY_KEYWORDS_EXTRACTION_PROMPT,
112
146
  }
113
147
 
114
148
  MEMORY_ASSEMBLY_TEMPLATE = """The retrieved memories are listed as follows:\n\n {memory_text}"""