MemoryOS 0.2.0__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 (114) hide show
  1. {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/METADATA +67 -26
  2. memoryos-0.2.2.dist-info/RECORD +169 -0
  3. memoryos-0.2.2.dist-info/entry_points.txt +3 -0
  4. memos/__init__.py +1 -1
  5. memos/api/config.py +562 -0
  6. memos/api/context/context.py +147 -0
  7. memos/api/context/dependencies.py +90 -0
  8. memos/api/exceptions.py +28 -0
  9. memos/api/mcp_serve.py +502 -0
  10. memos/api/product_api.py +35 -0
  11. memos/api/product_models.py +163 -0
  12. memos/api/routers/__init__.py +1 -0
  13. memos/api/routers/product_router.py +386 -0
  14. memos/chunkers/sentence_chunker.py +8 -2
  15. memos/cli.py +113 -0
  16. memos/configs/embedder.py +27 -0
  17. memos/configs/graph_db.py +132 -3
  18. memos/configs/internet_retriever.py +6 -0
  19. memos/configs/llm.py +47 -0
  20. memos/configs/mem_cube.py +1 -1
  21. memos/configs/mem_os.py +5 -0
  22. memos/configs/mem_reader.py +9 -0
  23. memos/configs/mem_scheduler.py +107 -7
  24. memos/configs/mem_user.py +58 -0
  25. memos/configs/memory.py +5 -4
  26. memos/dependency.py +52 -0
  27. memos/embedders/ark.py +92 -0
  28. memos/embedders/factory.py +4 -0
  29. memos/embedders/sentence_transformer.py +8 -2
  30. memos/embedders/universal_api.py +32 -0
  31. memos/graph_dbs/base.py +11 -3
  32. memos/graph_dbs/factory.py +4 -0
  33. memos/graph_dbs/nebular.py +1364 -0
  34. memos/graph_dbs/neo4j.py +333 -124
  35. memos/graph_dbs/neo4j_community.py +300 -0
  36. memos/llms/base.py +9 -0
  37. memos/llms/deepseek.py +54 -0
  38. memos/llms/factory.py +10 -1
  39. memos/llms/hf.py +170 -13
  40. memos/llms/hf_singleton.py +114 -0
  41. memos/llms/ollama.py +4 -0
  42. memos/llms/openai.py +67 -1
  43. memos/llms/qwen.py +63 -0
  44. memos/llms/vllm.py +153 -0
  45. memos/log.py +1 -1
  46. memos/mem_cube/general.py +77 -16
  47. memos/mem_cube/utils.py +109 -0
  48. memos/mem_os/core.py +251 -51
  49. memos/mem_os/main.py +94 -12
  50. memos/mem_os/product.py +1220 -43
  51. memos/mem_os/utils/default_config.py +352 -0
  52. memos/mem_os/utils/format_utils.py +1401 -0
  53. memos/mem_reader/simple_struct.py +18 -10
  54. memos/mem_scheduler/base_scheduler.py +441 -40
  55. memos/mem_scheduler/general_scheduler.py +249 -248
  56. memos/mem_scheduler/modules/base.py +14 -5
  57. memos/mem_scheduler/modules/dispatcher.py +67 -4
  58. memos/mem_scheduler/modules/misc.py +104 -0
  59. memos/mem_scheduler/modules/monitor.py +240 -50
  60. memos/mem_scheduler/modules/rabbitmq_service.py +319 -0
  61. memos/mem_scheduler/modules/redis_service.py +32 -22
  62. memos/mem_scheduler/modules/retriever.py +167 -23
  63. memos/mem_scheduler/modules/scheduler_logger.py +255 -0
  64. memos/mem_scheduler/mos_for_test_scheduler.py +140 -0
  65. memos/mem_scheduler/schemas/__init__.py +0 -0
  66. memos/mem_scheduler/schemas/general_schemas.py +43 -0
  67. memos/mem_scheduler/{modules/schemas.py → schemas/message_schemas.py} +63 -61
  68. memos/mem_scheduler/schemas/monitor_schemas.py +329 -0
  69. memos/mem_scheduler/utils/__init__.py +0 -0
  70. memos/mem_scheduler/utils/filter_utils.py +176 -0
  71. memos/mem_scheduler/utils/misc_utils.py +61 -0
  72. memos/mem_user/factory.py +94 -0
  73. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  74. memos/mem_user/mysql_user_manager.py +500 -0
  75. memos/mem_user/persistent_factory.py +96 -0
  76. memos/mem_user/persistent_user_manager.py +260 -0
  77. memos/mem_user/user_manager.py +4 -4
  78. memos/memories/activation/item.py +29 -0
  79. memos/memories/activation/kv.py +10 -3
  80. memos/memories/activation/vllmkv.py +219 -0
  81. memos/memories/factory.py +2 -0
  82. memos/memories/textual/base.py +1 -1
  83. memos/memories/textual/general.py +43 -97
  84. memos/memories/textual/item.py +5 -33
  85. memos/memories/textual/tree.py +22 -12
  86. memos/memories/textual/tree_text_memory/organize/conflict.py +9 -5
  87. memos/memories/textual/tree_text_memory/organize/manager.py +26 -18
  88. memos/memories/textual/tree_text_memory/organize/redundancy.py +25 -44
  89. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +50 -48
  90. memos/memories/textual/tree_text_memory/organize/reorganizer.py +81 -56
  91. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  92. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
  93. memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
  94. memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
  95. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  96. memos/memories/textual/tree_text_memory/retrieve/searcher.py +52 -28
  97. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -15
  98. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  99. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  100. memos/memos_tools/dinding_report_bot.py +422 -0
  101. memos/memos_tools/notification_service.py +44 -0
  102. memos/memos_tools/notification_utils.py +96 -0
  103. memos/parsers/markitdown.py +8 -2
  104. memos/settings.py +3 -1
  105. memos/templates/mem_reader_prompts.py +66 -23
  106. memos/templates/mem_scheduler_prompts.py +126 -43
  107. memos/templates/mos_prompts.py +87 -0
  108. memos/templates/tree_reorganize_prompts.py +85 -30
  109. memos/vec_dbs/base.py +12 -0
  110. memos/vec_dbs/qdrant.py +46 -20
  111. memoryos-0.2.0.dist-info/RECORD +0 -128
  112. memos/mem_scheduler/utils.py +0 -26
  113. {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
  114. {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/WHEEL +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)
@@ -1,6 +1,5 @@
1
- from markitdown import MarkItDown
2
-
3
1
  from memos.configs.parser import MarkItDownParserConfig
2
+ from memos.dependency import require_python_package
4
3
  from memos.log import get_logger
5
4
  from memos.parsers.base import BaseParser
6
5
 
@@ -14,7 +13,14 @@ class MarkItDownParser(BaseParser):
14
13
  def __init__(self, config: MarkItDownParserConfig):
15
14
  self.config = config
16
15
 
16
+ @require_python_package(
17
+ import_name="markitdown",
18
+ install_command="pip install markitdown[all]",
19
+ install_link="https://github.com/microsoft/markitdown",
20
+ )
17
21
  def parse(self, file_path: str) -> str:
22
+ from markitdown import MarkItDown
23
+
18
24
  """Parse the file at the given path and return its content as a MarkDown string."""
19
25
  md = MarkItDown(enable_plugins=False)
20
26
  result = md.convert(file_path)
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