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,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"×tamp={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"\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"",
|
|
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
|
@@ -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 (注意:
|
|
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
|
|
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
|
|
94
|
-
Input order: ["
|
|
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": [
|
|
99
|
-
"reasoning": "
|
|
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}"""
|