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.
- {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/METADATA +67 -26
- memoryos-0.2.2.dist-info/RECORD +169 -0
- memoryos-0.2.2.dist-info/entry_points.txt +3 -0
- memos/__init__.py +1 -1
- memos/api/config.py +562 -0
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +90 -0
- memos/api/exceptions.py +28 -0
- memos/api/mcp_serve.py +502 -0
- memos/api/product_api.py +35 -0
- memos/api/product_models.py +163 -0
- memos/api/routers/__init__.py +1 -0
- memos/api/routers/product_router.py +386 -0
- memos/chunkers/sentence_chunker.py +8 -2
- memos/cli.py +113 -0
- memos/configs/embedder.py +27 -0
- memos/configs/graph_db.py +132 -3
- memos/configs/internet_retriever.py +6 -0
- memos/configs/llm.py +47 -0
- memos/configs/mem_cube.py +1 -1
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +107 -7
- memos/configs/mem_user.py +58 -0
- memos/configs/memory.py +5 -4
- memos/dependency.py +52 -0
- memos/embedders/ark.py +92 -0
- memos/embedders/factory.py +4 -0
- memos/embedders/sentence_transformer.py +8 -2
- memos/embedders/universal_api.py +32 -0
- memos/graph_dbs/base.py +11 -3
- memos/graph_dbs/factory.py +4 -0
- memos/graph_dbs/nebular.py +1364 -0
- memos/graph_dbs/neo4j.py +333 -124
- memos/graph_dbs/neo4j_community.py +300 -0
- memos/llms/base.py +9 -0
- memos/llms/deepseek.py +54 -0
- memos/llms/factory.py +10 -1
- memos/llms/hf.py +170 -13
- memos/llms/hf_singleton.py +114 -0
- memos/llms/ollama.py +4 -0
- memos/llms/openai.py +67 -1
- memos/llms/qwen.py +63 -0
- memos/llms/vllm.py +153 -0
- memos/log.py +1 -1
- memos/mem_cube/general.py +77 -16
- memos/mem_cube/utils.py +109 -0
- memos/mem_os/core.py +251 -51
- memos/mem_os/main.py +94 -12
- memos/mem_os/product.py +1220 -43
- memos/mem_os/utils/default_config.py +352 -0
- memos/mem_os/utils/format_utils.py +1401 -0
- memos/mem_reader/simple_struct.py +18 -10
- memos/mem_scheduler/base_scheduler.py +441 -40
- memos/mem_scheduler/general_scheduler.py +249 -248
- memos/mem_scheduler/modules/base.py +14 -5
- memos/mem_scheduler/modules/dispatcher.py +67 -4
- memos/mem_scheduler/modules/misc.py +104 -0
- memos/mem_scheduler/modules/monitor.py +240 -50
- memos/mem_scheduler/modules/rabbitmq_service.py +319 -0
- memos/mem_scheduler/modules/redis_service.py +32 -22
- memos/mem_scheduler/modules/retriever.py +167 -23
- memos/mem_scheduler/modules/scheduler_logger.py +255 -0
- memos/mem_scheduler/mos_for_test_scheduler.py +140 -0
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +43 -0
- memos/mem_scheduler/{modules/schemas.py → schemas/message_schemas.py} +63 -61
- 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/persistent_user_manager.py +260 -0
- memos/mem_user/user_manager.py +4 -4
- memos/memories/activation/item.py +29 -0
- memos/memories/activation/kv.py +10 -3
- memos/memories/activation/vllmkv.py +219 -0
- memos/memories/factory.py +2 -0
- memos/memories/textual/base.py +1 -1
- memos/memories/textual/general.py +43 -97
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +22 -12
- memos/memories/textual/tree_text_memory/organize/conflict.py +9 -5
- memos/memories/textual/tree_text_memory/organize/manager.py +26 -18
- memos/memories/textual/tree_text_memory/organize/redundancy.py +25 -44
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +50 -48
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +81 -56
- 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/recall.py +0 -1
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +52 -28
- 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/parsers/markitdown.py +8 -2
- memos/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +66 -23
- memos/templates/mem_scheduler_prompts.py +126 -43
- memos/templates/mos_prompts.py +87 -0
- memos/templates/tree_reorganize_prompts.py +85 -30
- memos/vec_dbs/base.py +12 -0
- memos/vec_dbs/qdrant.py +46 -20
- memoryos-0.2.0.dist-info/RECORD +0 -128
- memos/mem_scheduler/utils.py +0 -26
- {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
- {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"×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/parsers/markitdown.py
CHANGED
|
@@ -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