MemoryOS 2.0.3__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.
Files changed (315) hide show
  1. memoryos-2.0.3.dist-info/METADATA +418 -0
  2. memoryos-2.0.3.dist-info/RECORD +315 -0
  3. memoryos-2.0.3.dist-info/WHEEL +4 -0
  4. memoryos-2.0.3.dist-info/entry_points.txt +3 -0
  5. memoryos-2.0.3.dist-info/licenses/LICENSE +201 -0
  6. memos/__init__.py +20 -0
  7. memos/api/client.py +571 -0
  8. memos/api/config.py +1018 -0
  9. memos/api/context/dependencies.py +50 -0
  10. memos/api/exceptions.py +53 -0
  11. memos/api/handlers/__init__.py +62 -0
  12. memos/api/handlers/add_handler.py +158 -0
  13. memos/api/handlers/base_handler.py +194 -0
  14. memos/api/handlers/chat_handler.py +1401 -0
  15. memos/api/handlers/component_init.py +388 -0
  16. memos/api/handlers/config_builders.py +190 -0
  17. memos/api/handlers/feedback_handler.py +93 -0
  18. memos/api/handlers/formatters_handler.py +237 -0
  19. memos/api/handlers/memory_handler.py +316 -0
  20. memos/api/handlers/scheduler_handler.py +497 -0
  21. memos/api/handlers/search_handler.py +222 -0
  22. memos/api/handlers/suggestion_handler.py +117 -0
  23. memos/api/mcp_serve.py +614 -0
  24. memos/api/middleware/request_context.py +101 -0
  25. memos/api/product_api.py +38 -0
  26. memos/api/product_models.py +1206 -0
  27. memos/api/routers/__init__.py +1 -0
  28. memos/api/routers/product_router.py +477 -0
  29. memos/api/routers/server_router.py +394 -0
  30. memos/api/server_api.py +44 -0
  31. memos/api/start_api.py +433 -0
  32. memos/chunkers/__init__.py +4 -0
  33. memos/chunkers/base.py +24 -0
  34. memos/chunkers/charactertext_chunker.py +41 -0
  35. memos/chunkers/factory.py +24 -0
  36. memos/chunkers/markdown_chunker.py +62 -0
  37. memos/chunkers/sentence_chunker.py +54 -0
  38. memos/chunkers/simple_chunker.py +50 -0
  39. memos/cli.py +113 -0
  40. memos/configs/__init__.py +0 -0
  41. memos/configs/base.py +82 -0
  42. memos/configs/chunker.py +59 -0
  43. memos/configs/embedder.py +88 -0
  44. memos/configs/graph_db.py +236 -0
  45. memos/configs/internet_retriever.py +100 -0
  46. memos/configs/llm.py +151 -0
  47. memos/configs/mem_agent.py +54 -0
  48. memos/configs/mem_chat.py +81 -0
  49. memos/configs/mem_cube.py +105 -0
  50. memos/configs/mem_os.py +83 -0
  51. memos/configs/mem_reader.py +91 -0
  52. memos/configs/mem_scheduler.py +385 -0
  53. memos/configs/mem_user.py +70 -0
  54. memos/configs/memory.py +324 -0
  55. memos/configs/parser.py +38 -0
  56. memos/configs/reranker.py +18 -0
  57. memos/configs/utils.py +8 -0
  58. memos/configs/vec_db.py +80 -0
  59. memos/context/context.py +355 -0
  60. memos/dependency.py +52 -0
  61. memos/deprecation.py +262 -0
  62. memos/embedders/__init__.py +0 -0
  63. memos/embedders/ark.py +95 -0
  64. memos/embedders/base.py +106 -0
  65. memos/embedders/factory.py +29 -0
  66. memos/embedders/ollama.py +77 -0
  67. memos/embedders/sentence_transformer.py +49 -0
  68. memos/embedders/universal_api.py +51 -0
  69. memos/exceptions.py +30 -0
  70. memos/graph_dbs/__init__.py +0 -0
  71. memos/graph_dbs/base.py +274 -0
  72. memos/graph_dbs/factory.py +27 -0
  73. memos/graph_dbs/item.py +46 -0
  74. memos/graph_dbs/nebular.py +1794 -0
  75. memos/graph_dbs/neo4j.py +1942 -0
  76. memos/graph_dbs/neo4j_community.py +1058 -0
  77. memos/graph_dbs/polardb.py +5446 -0
  78. memos/hello_world.py +97 -0
  79. memos/llms/__init__.py +0 -0
  80. memos/llms/base.py +25 -0
  81. memos/llms/deepseek.py +13 -0
  82. memos/llms/factory.py +38 -0
  83. memos/llms/hf.py +443 -0
  84. memos/llms/hf_singleton.py +114 -0
  85. memos/llms/ollama.py +135 -0
  86. memos/llms/openai.py +222 -0
  87. memos/llms/openai_new.py +198 -0
  88. memos/llms/qwen.py +13 -0
  89. memos/llms/utils.py +14 -0
  90. memos/llms/vllm.py +218 -0
  91. memos/log.py +237 -0
  92. memos/mem_agent/base.py +19 -0
  93. memos/mem_agent/deepsearch_agent.py +391 -0
  94. memos/mem_agent/factory.py +36 -0
  95. memos/mem_chat/__init__.py +0 -0
  96. memos/mem_chat/base.py +30 -0
  97. memos/mem_chat/factory.py +21 -0
  98. memos/mem_chat/simple.py +200 -0
  99. memos/mem_cube/__init__.py +0 -0
  100. memos/mem_cube/base.py +30 -0
  101. memos/mem_cube/general.py +240 -0
  102. memos/mem_cube/navie.py +172 -0
  103. memos/mem_cube/utils.py +169 -0
  104. memos/mem_feedback/base.py +15 -0
  105. memos/mem_feedback/feedback.py +1192 -0
  106. memos/mem_feedback/simple_feedback.py +40 -0
  107. memos/mem_feedback/utils.py +230 -0
  108. memos/mem_os/client.py +5 -0
  109. memos/mem_os/core.py +1203 -0
  110. memos/mem_os/main.py +582 -0
  111. memos/mem_os/product.py +1608 -0
  112. memos/mem_os/product_server.py +455 -0
  113. memos/mem_os/utils/default_config.py +359 -0
  114. memos/mem_os/utils/format_utils.py +1403 -0
  115. memos/mem_os/utils/reference_utils.py +162 -0
  116. memos/mem_reader/__init__.py +0 -0
  117. memos/mem_reader/base.py +47 -0
  118. memos/mem_reader/factory.py +53 -0
  119. memos/mem_reader/memory.py +298 -0
  120. memos/mem_reader/multi_modal_struct.py +965 -0
  121. memos/mem_reader/read_multi_modal/__init__.py +43 -0
  122. memos/mem_reader/read_multi_modal/assistant_parser.py +311 -0
  123. memos/mem_reader/read_multi_modal/base.py +273 -0
  124. memos/mem_reader/read_multi_modal/file_content_parser.py +826 -0
  125. memos/mem_reader/read_multi_modal/image_parser.py +359 -0
  126. memos/mem_reader/read_multi_modal/multi_modal_parser.py +252 -0
  127. memos/mem_reader/read_multi_modal/string_parser.py +139 -0
  128. memos/mem_reader/read_multi_modal/system_parser.py +327 -0
  129. memos/mem_reader/read_multi_modal/text_content_parser.py +131 -0
  130. memos/mem_reader/read_multi_modal/tool_parser.py +210 -0
  131. memos/mem_reader/read_multi_modal/user_parser.py +218 -0
  132. memos/mem_reader/read_multi_modal/utils.py +358 -0
  133. memos/mem_reader/simple_struct.py +912 -0
  134. memos/mem_reader/strategy_struct.py +163 -0
  135. memos/mem_reader/utils.py +157 -0
  136. memos/mem_scheduler/__init__.py +0 -0
  137. memos/mem_scheduler/analyzer/__init__.py +0 -0
  138. memos/mem_scheduler/analyzer/api_analyzer.py +714 -0
  139. memos/mem_scheduler/analyzer/eval_analyzer.py +219 -0
  140. memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +571 -0
  141. memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
  142. memos/mem_scheduler/base_scheduler.py +1319 -0
  143. memos/mem_scheduler/general_modules/__init__.py +0 -0
  144. memos/mem_scheduler/general_modules/api_misc.py +137 -0
  145. memos/mem_scheduler/general_modules/base.py +80 -0
  146. memos/mem_scheduler/general_modules/init_components_for_scheduler.py +425 -0
  147. memos/mem_scheduler/general_modules/misc.py +313 -0
  148. memos/mem_scheduler/general_modules/scheduler_logger.py +389 -0
  149. memos/mem_scheduler/general_modules/task_threads.py +315 -0
  150. memos/mem_scheduler/general_scheduler.py +1495 -0
  151. memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
  152. memos/mem_scheduler/memory_manage_modules/memory_filter.py +306 -0
  153. memos/mem_scheduler/memory_manage_modules/retriever.py +547 -0
  154. memos/mem_scheduler/monitors/__init__.py +0 -0
  155. memos/mem_scheduler/monitors/dispatcher_monitor.py +366 -0
  156. memos/mem_scheduler/monitors/general_monitor.py +394 -0
  157. memos/mem_scheduler/monitors/task_schedule_monitor.py +254 -0
  158. memos/mem_scheduler/optimized_scheduler.py +410 -0
  159. memos/mem_scheduler/orm_modules/__init__.py +0 -0
  160. memos/mem_scheduler/orm_modules/api_redis_model.py +518 -0
  161. memos/mem_scheduler/orm_modules/base_model.py +729 -0
  162. memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
  163. memos/mem_scheduler/orm_modules/redis_model.py +699 -0
  164. memos/mem_scheduler/scheduler_factory.py +23 -0
  165. memos/mem_scheduler/schemas/__init__.py +0 -0
  166. memos/mem_scheduler/schemas/analyzer_schemas.py +52 -0
  167. memos/mem_scheduler/schemas/api_schemas.py +233 -0
  168. memos/mem_scheduler/schemas/general_schemas.py +55 -0
  169. memos/mem_scheduler/schemas/message_schemas.py +173 -0
  170. memos/mem_scheduler/schemas/monitor_schemas.py +406 -0
  171. memos/mem_scheduler/schemas/task_schemas.py +132 -0
  172. memos/mem_scheduler/task_schedule_modules/__init__.py +0 -0
  173. memos/mem_scheduler/task_schedule_modules/dispatcher.py +740 -0
  174. memos/mem_scheduler/task_schedule_modules/local_queue.py +247 -0
  175. memos/mem_scheduler/task_schedule_modules/orchestrator.py +74 -0
  176. memos/mem_scheduler/task_schedule_modules/redis_queue.py +1385 -0
  177. memos/mem_scheduler/task_schedule_modules/task_queue.py +162 -0
  178. memos/mem_scheduler/utils/__init__.py +0 -0
  179. memos/mem_scheduler/utils/api_utils.py +77 -0
  180. memos/mem_scheduler/utils/config_utils.py +100 -0
  181. memos/mem_scheduler/utils/db_utils.py +50 -0
  182. memos/mem_scheduler/utils/filter_utils.py +176 -0
  183. memos/mem_scheduler/utils/metrics.py +125 -0
  184. memos/mem_scheduler/utils/misc_utils.py +290 -0
  185. memos/mem_scheduler/utils/monitor_event_utils.py +67 -0
  186. memos/mem_scheduler/utils/status_tracker.py +229 -0
  187. memos/mem_scheduler/webservice_modules/__init__.py +0 -0
  188. memos/mem_scheduler/webservice_modules/rabbitmq_service.py +485 -0
  189. memos/mem_scheduler/webservice_modules/redis_service.py +380 -0
  190. memos/mem_user/factory.py +94 -0
  191. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  192. memos/mem_user/mysql_user_manager.py +502 -0
  193. memos/mem_user/persistent_factory.py +98 -0
  194. memos/mem_user/persistent_user_manager.py +260 -0
  195. memos/mem_user/redis_persistent_user_manager.py +225 -0
  196. memos/mem_user/user_manager.py +488 -0
  197. memos/memories/__init__.py +0 -0
  198. memos/memories/activation/__init__.py +0 -0
  199. memos/memories/activation/base.py +42 -0
  200. memos/memories/activation/item.py +56 -0
  201. memos/memories/activation/kv.py +292 -0
  202. memos/memories/activation/vllmkv.py +219 -0
  203. memos/memories/base.py +19 -0
  204. memos/memories/factory.py +42 -0
  205. memos/memories/parametric/__init__.py +0 -0
  206. memos/memories/parametric/base.py +19 -0
  207. memos/memories/parametric/item.py +11 -0
  208. memos/memories/parametric/lora.py +41 -0
  209. memos/memories/textual/__init__.py +0 -0
  210. memos/memories/textual/base.py +92 -0
  211. memos/memories/textual/general.py +236 -0
  212. memos/memories/textual/item.py +304 -0
  213. memos/memories/textual/naive.py +187 -0
  214. memos/memories/textual/prefer_text_memory/__init__.py +0 -0
  215. memos/memories/textual/prefer_text_memory/adder.py +504 -0
  216. memos/memories/textual/prefer_text_memory/config.py +106 -0
  217. memos/memories/textual/prefer_text_memory/extractor.py +221 -0
  218. memos/memories/textual/prefer_text_memory/factory.py +85 -0
  219. memos/memories/textual/prefer_text_memory/retrievers.py +177 -0
  220. memos/memories/textual/prefer_text_memory/spliter.py +132 -0
  221. memos/memories/textual/prefer_text_memory/utils.py +93 -0
  222. memos/memories/textual/preference.py +344 -0
  223. memos/memories/textual/simple_preference.py +161 -0
  224. memos/memories/textual/simple_tree.py +69 -0
  225. memos/memories/textual/tree.py +459 -0
  226. memos/memories/textual/tree_text_memory/__init__.py +0 -0
  227. memos/memories/textual/tree_text_memory/organize/__init__.py +0 -0
  228. memos/memories/textual/tree_text_memory/organize/handler.py +184 -0
  229. memos/memories/textual/tree_text_memory/organize/manager.py +518 -0
  230. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +238 -0
  231. memos/memories/textual/tree_text_memory/organize/reorganizer.py +622 -0
  232. memos/memories/textual/tree_text_memory/retrieve/__init__.py +0 -0
  233. memos/memories/textual/tree_text_memory/retrieve/advanced_searcher.py +364 -0
  234. memos/memories/textual/tree_text_memory/retrieve/bm25_util.py +186 -0
  235. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +419 -0
  236. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +270 -0
  237. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +102 -0
  238. memos/memories/textual/tree_text_memory/retrieve/reasoner.py +61 -0
  239. memos/memories/textual/tree_text_memory/retrieve/recall.py +497 -0
  240. memos/memories/textual/tree_text_memory/retrieve/reranker.py +111 -0
  241. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +16 -0
  242. memos/memories/textual/tree_text_memory/retrieve/retrieve_utils.py +472 -0
  243. memos/memories/textual/tree_text_memory/retrieve/searcher.py +848 -0
  244. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +135 -0
  245. memos/memories/textual/tree_text_memory/retrieve/utils.py +54 -0
  246. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +387 -0
  247. memos/memos_tools/dinding_report_bot.py +453 -0
  248. memos/memos_tools/lockfree_dict.py +120 -0
  249. memos/memos_tools/notification_service.py +44 -0
  250. memos/memos_tools/notification_utils.py +142 -0
  251. memos/memos_tools/singleton.py +174 -0
  252. memos/memos_tools/thread_safe_dict.py +310 -0
  253. memos/memos_tools/thread_safe_dict_segment.py +382 -0
  254. memos/multi_mem_cube/__init__.py +0 -0
  255. memos/multi_mem_cube/composite_cube.py +86 -0
  256. memos/multi_mem_cube/single_cube.py +874 -0
  257. memos/multi_mem_cube/views.py +54 -0
  258. memos/parsers/__init__.py +0 -0
  259. memos/parsers/base.py +15 -0
  260. memos/parsers/factory.py +21 -0
  261. memos/parsers/markitdown.py +28 -0
  262. memos/reranker/__init__.py +4 -0
  263. memos/reranker/base.py +25 -0
  264. memos/reranker/concat.py +103 -0
  265. memos/reranker/cosine_local.py +102 -0
  266. memos/reranker/factory.py +72 -0
  267. memos/reranker/http_bge.py +324 -0
  268. memos/reranker/http_bge_strategy.py +327 -0
  269. memos/reranker/noop.py +19 -0
  270. memos/reranker/strategies/__init__.py +4 -0
  271. memos/reranker/strategies/base.py +61 -0
  272. memos/reranker/strategies/concat_background.py +94 -0
  273. memos/reranker/strategies/concat_docsource.py +110 -0
  274. memos/reranker/strategies/dialogue_common.py +109 -0
  275. memos/reranker/strategies/factory.py +31 -0
  276. memos/reranker/strategies/single_turn.py +107 -0
  277. memos/reranker/strategies/singleturn_outmem.py +98 -0
  278. memos/settings.py +10 -0
  279. memos/templates/__init__.py +0 -0
  280. memos/templates/advanced_search_prompts.py +211 -0
  281. memos/templates/cloud_service_prompt.py +107 -0
  282. memos/templates/instruction_completion.py +66 -0
  283. memos/templates/mem_agent_prompts.py +85 -0
  284. memos/templates/mem_feedback_prompts.py +822 -0
  285. memos/templates/mem_reader_prompts.py +1096 -0
  286. memos/templates/mem_reader_strategy_prompts.py +238 -0
  287. memos/templates/mem_scheduler_prompts.py +626 -0
  288. memos/templates/mem_search_prompts.py +93 -0
  289. memos/templates/mos_prompts.py +403 -0
  290. memos/templates/prefer_complete_prompt.py +735 -0
  291. memos/templates/tool_mem_prompts.py +139 -0
  292. memos/templates/tree_reorganize_prompts.py +230 -0
  293. memos/types/__init__.py +34 -0
  294. memos/types/general_types.py +151 -0
  295. memos/types/openai_chat_completion_types/__init__.py +15 -0
  296. memos/types/openai_chat_completion_types/chat_completion_assistant_message_param.py +56 -0
  297. memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py +27 -0
  298. memos/types/openai_chat_completion_types/chat_completion_content_part_input_audio_param.py +23 -0
  299. memos/types/openai_chat_completion_types/chat_completion_content_part_param.py +43 -0
  300. memos/types/openai_chat_completion_types/chat_completion_content_part_refusal_param.py +16 -0
  301. memos/types/openai_chat_completion_types/chat_completion_content_part_text_param.py +16 -0
  302. memos/types/openai_chat_completion_types/chat_completion_message_custom_tool_call_param.py +27 -0
  303. memos/types/openai_chat_completion_types/chat_completion_message_function_tool_call_param.py +32 -0
  304. memos/types/openai_chat_completion_types/chat_completion_message_param.py +18 -0
  305. memos/types/openai_chat_completion_types/chat_completion_message_tool_call_union_param.py +15 -0
  306. memos/types/openai_chat_completion_types/chat_completion_system_message_param.py +36 -0
  307. memos/types/openai_chat_completion_types/chat_completion_tool_message_param.py +30 -0
  308. memos/types/openai_chat_completion_types/chat_completion_user_message_param.py +34 -0
  309. memos/utils.py +123 -0
  310. memos/vec_dbs/__init__.py +0 -0
  311. memos/vec_dbs/base.py +117 -0
  312. memos/vec_dbs/factory.py +23 -0
  313. memos/vec_dbs/item.py +50 -0
  314. memos/vec_dbs/milvus.py +654 -0
  315. memos/vec_dbs/qdrant.py +355 -0
@@ -0,0 +1,453 @@
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 traceback
11
+ import urllib.parse
12
+
13
+ from datetime import datetime
14
+ from uuid import uuid4
15
+
16
+ from dotenv import load_dotenv
17
+
18
+ from memos.log import get_logger
19
+
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ load_dotenv()
25
+
26
+ try:
27
+ import io
28
+
29
+ import matplotlib
30
+ import matplotlib.font_manager as fm
31
+ import numpy as np
32
+ import oss2
33
+ import requests
34
+
35
+ from PIL import Image, ImageDraw, ImageFont
36
+
37
+ matplotlib.use("Agg")
38
+ from alibabacloud_dingtalk.robot_1_0 import models as robot_models
39
+ from alibabacloud_dingtalk.robot_1_0.client import Client as DingtalkRobotClient
40
+ from alibabacloud_tea_openapi import models as open_api_models
41
+ from alibabacloud_tea_util import models as util_models
42
+ except ImportError as e:
43
+ raise ImportError(
44
+ f"DingDing bot dependencies not found: {e}. "
45
+ "Please install required packages: pip install requests oss2 pillow matplotlib alibabacloud-dingtalk"
46
+ ) from e
47
+
48
+ # =========================
49
+ # 🔧 common tools
50
+ # =========================
51
+ ACCESS_TOKEN_USER = os.getenv("DINGDING_ACCESS_TOKEN_USER")
52
+ SECRET_USER = os.getenv("DINGDING_SECRET_USER")
53
+ ACCESS_TOKEN_ERROR = os.getenv("DINGDING_ACCESS_TOKEN_ERROR")
54
+ SECRET_ERROR = os.getenv("DINGDING_SECRET_ERROR")
55
+ OSS_CONFIG = {
56
+ "endpoint": os.getenv("OSS_ENDPOINT"),
57
+ "region": os.getenv("OSS_REGION"),
58
+ "bucket_name": os.getenv("OSS_BUCKET_NAME"),
59
+ "oss_access_key_id": os.getenv("OSS_ACCESS_KEY_ID"),
60
+ "oss_access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET"),
61
+ "public_base_url": os.getenv("OSS_PUBLIC_BASE_URL"),
62
+ }
63
+ ROBOT_CODE = os.getenv("DINGDING_ROBOT_CODE")
64
+ DING_APP_KEY = os.getenv("DINGDING_APP_KEY")
65
+ DING_APP_SECRET = os.getenv("DINGDING_APP_SECRET")
66
+ ENV_NAME = os.getenv("ENV_NAME", "PLAYGROUND_OFFLINE")
67
+
68
+ theme_map = {
69
+ "ONLINE": {
70
+ "color": "#2196F3",
71
+ "grad": ("#E3F2FD", "#BBDEFB"),
72
+ "emoji": "🩵",
73
+ },
74
+ "OFFLINE": {
75
+ "color": "#FFC107",
76
+ "grad": ("#FFF8E1", "#FFECB3"),
77
+ "emoji": "🤍",
78
+ },
79
+ }
80
+
81
+
82
+ # Get access_token
83
+ def get_access_token():
84
+ url = f"https://oapi.dingtalk.com/gettoken?appkey={DING_APP_KEY}&appsecret={DING_APP_SECRET}"
85
+ resp = requests.get(url)
86
+ return resp.json()["access_token"]
87
+
88
+
89
+ def _pick_font(size: int = 48) -> ImageFont.ImageFont:
90
+ """
91
+ Try to find a font from the following candidates (macOS / Windows / Linux are common):
92
+ Helvetica → Arial → DejaVu Sans
93
+ If found, use truetype, otherwise return the default bitmap font.
94
+ """
95
+ candidates = ["Helvetica", "Arial", "DejaVu Sans"]
96
+ for name in candidates:
97
+ try:
98
+ font_path = fm.findfont(name, fallback_to_default=False)
99
+ return ImageFont.truetype(font_path, size)
100
+ except Exception:
101
+ continue
102
+ # Cannot find truetype, fallback to default and manually scale up
103
+ bitmap = ImageFont.load_default()
104
+ return ImageFont.FreeTypeFont(bitmap.path, size) if hasattr(bitmap, "path") else bitmap
105
+
106
+
107
+ def make_header(
108
+ title: str,
109
+ subtitle: str,
110
+ size=(1080, 260),
111
+ colors=("#C8F6E1", "#E8F8F5"), # Stylish mint green → lighter green
112
+ fg="#00956D",
113
+ ) -> bytes:
114
+ """
115
+ Generate a "Notification" banner with green gradient and bold large text.
116
+ title: main title (suggested ≤ 35 characters)
117
+ subtitle: sub title (e.g. "Notification")
118
+ """
119
+
120
+ # Can be placed inside or outside make_header
121
+ def _text_wh(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont):
122
+ """
123
+ return (width, height), compatible with both Pillow old version (textsize) and new version (textbbox)
124
+ """
125
+ if hasattr(draw, "textbbox"): # Pillow ≥ 8.0
126
+ left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
127
+ return right - left, bottom - top
128
+ else: # Pillow < 10.0
129
+ return draw.textsize(text, font=font)
130
+
131
+ w, h = size
132
+ # --- 1) background gradient ---
133
+ g = np.linspace(0, 1, w)
134
+ grad = np.outer(np.ones(h), g)
135
+ rgb0 = tuple(int(colors[0].lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
136
+ rgb1 = tuple(int(colors[1].lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
137
+ img = np.zeros((h, w, 3), dtype=np.uint8)
138
+ for i in range(3):
139
+ img[:, :, i] = rgb0[i] * (1 - grad) + rgb1[i] * grad
140
+ im = Image.fromarray(img)
141
+
142
+ # --- 2) text ---
143
+ draw = ImageDraw.Draw(im)
144
+ font_title = _pick_font(54) # main title
145
+ font_sub = _pick_font(30) # sub title
146
+
147
+ # center alignment
148
+ title_w, title_h = _text_wh(draw, title, font_title)
149
+ sub_w, sub_h = _text_wh(draw, subtitle, font_sub)
150
+
151
+ title_x = (w - title_w) // 2
152
+ title_y = h // 2 - title_h
153
+ sub_x = (w - sub_w) // 2
154
+ sub_y = title_y + title_h + 8
155
+
156
+ draw.text((title_x, title_y), title, fill=fg, font=font_title)
157
+ draw.text((sub_x, sub_y), subtitle, fill=fg, font=font_sub)
158
+
159
+ # --- 3) PNG bytes ---
160
+ buf = io.BytesIO()
161
+ im.save(buf, "PNG")
162
+ return buf.getvalue()
163
+
164
+
165
+ def _sign(secret: str, ts: str):
166
+ s = f"{ts}\n{secret}"
167
+ return urllib.parse.quote_plus(
168
+ base64.b64encode(hmac.new(secret.encode(), s.encode(), hashlib.sha256).digest())
169
+ )
170
+
171
+
172
+ def _send_md(title: str, md: str, type="user", at=None):
173
+ if type == "user":
174
+ access_token = ACCESS_TOKEN_USER
175
+ secret = SECRET_USER
176
+ else:
177
+ access_token = ACCESS_TOKEN_ERROR
178
+ secret = SECRET_ERROR
179
+ ts = str(round(time.time() * 1000))
180
+ url = (
181
+ f"https://oapi.dingtalk.com/robot/send?access_token={access_token}"
182
+ f"&timestamp={ts}&sign={_sign(secret, ts)}"
183
+ )
184
+ payload = {
185
+ "msgtype": "markdown",
186
+ "markdown": {"title": title, "text": md},
187
+ "at": at or {"atUserIds": [], "isAtAll": False},
188
+ }
189
+ requests.post(url, headers={"Content-Type": "application/json"}, data=json.dumps(payload))
190
+
191
+
192
+ # ------------------------- OSS -------------------------
193
+ def upload_bytes_to_oss(
194
+ data: bytes,
195
+ oss_dir: str = "xcy-share/jfzt/",
196
+ filename: str | None = None,
197
+ keep_latest: int = 1, # Keep latest N files; 0 = delete all
198
+ ) -> str:
199
+ """
200
+ - If filename_prefix is provided, delete the older files in {oss_dir}/{prefix}_*.png, only keep the latest keep_latest files
201
+ - Always create <prefix>_<timestamp>_<uuid>.png → ensure the URL is unique
202
+ """
203
+ filename_prefix = filename
204
+
205
+ conf = OSS_CONFIG
206
+ auth = oss2.Auth(conf["oss_access_key_id"], conf["oss_access_key_secret"])
207
+ bucket = oss2.Bucket(auth, conf["endpoint"], conf["bucket_name"])
208
+
209
+ # ---------- delete old files ----------
210
+ if filename_prefix and keep_latest >= 0:
211
+ prefix_path = f"{oss_dir.rstrip('/')}/{filename_prefix}_"
212
+ objs = bucket.list_objects(prefix=prefix_path).object_list
213
+ old_files = [(o.key, o.last_modified) for o in objs if o.key.endswith(".png")]
214
+ if old_files and len(old_files) > keep_latest:
215
+ # sort by last_modified from new to old
216
+ old_files.sort(key=lambda x: x[1], reverse=True)
217
+ to_del = [k for k, _ in old_files[keep_latest:]]
218
+ for k in to_del:
219
+ with contextlib.suppress(Exception):
220
+ bucket.delete_object(k)
221
+
222
+ # ---------- upload new file ----------
223
+ ts = int(time.time())
224
+ uniq = uuid4().hex
225
+ prefix = f"{filename_prefix}_" if filename_prefix else ""
226
+ object_name = f"{oss_dir.rstrip('/')}/{prefix}{ts}_{uniq}.png"
227
+ bucket.put_object(object_name, data)
228
+
229
+ return f"{conf['public_base_url'].rstrip('/')}/{object_name}"
230
+
231
+
232
+ # --------- Markdown Table Helper ---------
233
+ def _md_table(data: dict, is_error: bool = False) -> str:
234
+ """
235
+ Render a dict to a DingTalk-compatible Markdown table
236
+ - Normal statistics: single row, multiple columns
237
+ - Error distribution: two columns, multiple rows (error information/occurrence count)
238
+ """
239
+ if is_error: # {"error_info":{idx:val}, "occurrence_count":{idx:val}}
240
+ header = "| error | count |\n|---|---|"
241
+ rows = "\n".join(
242
+ f"| {err} | {cnt} |"
243
+ for err, cnt in zip(data["error"].values(), data["count"].values(), strict=False)
244
+ )
245
+ return f"{header}\n{rows}"
246
+
247
+ # normal statistics
248
+ header = "| " + " | ".join(data.keys()) + " |\n|" + "|".join(["---"] * len(data)) + "|"
249
+ row = "| " + " | ".join(map(str, data.values())) + " |"
250
+ return f"{header}\n{row}"
251
+
252
+
253
+ def upload_to_oss(
254
+ local_path: str,
255
+ oss_dir: str = "xcy-share/jfzt/",
256
+ filename: str | None = None, # ← Same addition
257
+ ) -> str:
258
+ """Upload a local file to OSS, support overwrite"""
259
+ with open(local_path, "rb") as f:
260
+ return upload_bytes_to_oss(f.read(), oss_dir=oss_dir, filename=filename)
261
+
262
+
263
+ def send_ding_reminder(
264
+ access_token: str, robot_code: str, user_ids: list[str], content: str, remind_type: int = 0
265
+ ):
266
+ """
267
+ :param access_token: DingTalk access_token (usually permanent when using a robot)
268
+ :param robot_code: Robot code applied on the open platform
269
+ :param user_ids: DingTalk user_id list
270
+ :param content: Message content to send
271
+ :param remind_type: 1=in-app notification, 2=phone reminder, 3=SMS reminder
272
+ """
273
+ # initialize client
274
+ config = open_api_models.Config(protocol="https", region_id="central")
275
+ client = DingtalkRobotClient(config)
276
+
277
+ # request headers
278
+ headers = robot_models.RobotSendDingHeaders(x_acs_dingtalk_access_token=access_token)
279
+
280
+ # request body
281
+ req = robot_models.RobotSendDingRequest(
282
+ robot_code=robot_code,
283
+ remind_type=remind_type,
284
+ receiver_user_id_list=user_ids,
285
+ content=content,
286
+ )
287
+
288
+ # send
289
+ try:
290
+ client.robot_send_ding_with_options(req, headers, util_models.RuntimeOptions())
291
+ print("✅ DING message sent successfully")
292
+ except Exception as e:
293
+ print("❌ DING message sent failed:", e)
294
+
295
+
296
+ def error_bot(
297
+ err: str,
298
+ title: str = "Error Alert",
299
+ level: str = "P2", # ← Add alert level
300
+ user_ids: list[str] | None = None, # ← @users in group
301
+ ):
302
+ """
303
+ send error alert
304
+ level can be set to P0 / P1 / P2, corresponding to red / orange / yellow
305
+ if title_color is provided, it will be overridden by level
306
+ """
307
+ # ---------- Level → Color scheme & Emoji ----------
308
+ level_map = {
309
+ "P0": {"color": "#C62828", "grad": ("#FFE4E4", "#FFD3D3"), "emoji": "🔴"},
310
+ "P1": {"color": "#E65100", "grad": ("#FFE9D6", "#FFD7B5"), "emoji": "🟠"},
311
+ "P2": {"color": "#EF6C00", "grad": ("#FFF6D8", "#FFECB5"), "emoji": "🟡"},
312
+ }
313
+ lv = level.upper()
314
+ if lv not in level_map:
315
+ lv = "P0" # Default to P0 if invalid
316
+ style = level_map[lv]
317
+
318
+ # If external title_color is specified, override with level color scheme
319
+ title_color = style["color"]
320
+
321
+ # ---------- Generate gradient banner ----------
322
+ banner_bytes = make_header(
323
+ title=f"Level {lv}", # Fixed English
324
+ subtitle="Error Alert", # Display level
325
+ colors=style["grad"],
326
+ fg=style["color"],
327
+ )
328
+ banner_url = upload_bytes_to_oss(
329
+ banner_bytes,
330
+ filename=f"error_banner_{title}_{lv.lower()}.png", # Overwrite fixed file for each level
331
+ )
332
+
333
+ # ---------- Markdown ----------
334
+ colored_title = f"<font color='{title_color}' size='4'><b>{ENV_NAME}</b></font>"
335
+ at_suffix = ""
336
+ if user_ids:
337
+ at_suffix = "\n\n" + " ".join([f"@{m}" for m in user_ids])
338
+
339
+ md = (
340
+ f"![banner]({banner_url})\n\n"
341
+ f"### {style['emoji']} <font color='{style['color']}' size='4'><b>{colored_title}</b></font>\n\n"
342
+ f"**Detail:**\n```\n{err}\n```\n"
343
+ # Visual indicator, pure color, no notification trigger
344
+ f"### 🔵 <font color='#1565C0' size='4'><b>Attention:{at_suffix}</b></font>\n\n"
345
+ f"<font color='#9E9E9E' size='1'>Time: "
346
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</font>\n"
347
+ )
348
+
349
+ # ---------- Send Markdown in group and @users ----------
350
+ at_config = {"atUserIds": user_ids or [], "isAtAll": False}
351
+ _send_md(title, md, type="error", at=at_config)
352
+
353
+ user_ids_for_ding = user_ids # DingTalk user_id list
354
+ message = f"{title}\nMemos system error, please handle immediately"
355
+
356
+ token = get_access_token()
357
+
358
+ send_ding_reminder(
359
+ access_token=token,
360
+ robot_code=ROBOT_CODE,
361
+ user_ids=user_ids_for_ding,
362
+ content=message,
363
+ remind_type=3 if level == "P0" else 1, # 1 in-app DING 2 SMS DING 3 phone DING
364
+ )
365
+
366
+
367
+ # --------- online_bot ---------
368
+ # ---------- Convert dict → colored KV lines ----------
369
+ def _kv_lines(d: dict, emoji: str = "", heading: str = "", heading_color: str = "#00956D") -> str:
370
+ """
371
+ Returns:
372
+ ### 📅 <font color='#00956D'><b>Daily Summary</b></font>
373
+ - **Request count:** 1364
374
+ ...
375
+ """
376
+ parts = [f"### {emoji} <font color='{heading_color}' size='3'><b>{heading}</b></font>"]
377
+ parts += [f"- **{k}:** {v}" for k, v in d.items()]
378
+ return "\n".join(parts)
379
+
380
+
381
+ # -------------- online_bot(colored title version) -----------------
382
+ def online_bot(
383
+ header_name: str,
384
+ sub_title_name: str,
385
+ title_color: str,
386
+ other_data1: dict,
387
+ other_data2: dict,
388
+ emoji: dict,
389
+ ):
390
+ try:
391
+ logger.info("in online bot")
392
+ theme = "OFFLINE" if "OFFLINE" in ENV_NAME or "TEST" in ENV_NAME else "ONLINE"
393
+ style = theme_map.get(theme, theme_map["OFFLINE"])
394
+ heading_color = style["color"] # Use theme color for subtitle
395
+
396
+ # 0) Banner
397
+ banner_bytes = make_header(
398
+ header_name,
399
+ sub_title_name,
400
+ colors=style["grad"],
401
+ fg=style["color"],
402
+ )
403
+ banner_url = upload_bytes_to_oss(banner_bytes, filename=f"{ENV_NAME}_online_report.png")
404
+
405
+ # 1) Colored main title
406
+ colored_title = f"<font color='{style['color']}' size='4'><b>{ENV_NAME}</b></font>"
407
+
408
+ # 3) Markdown
409
+ md = "\n\n".join(
410
+ filter(
411
+ None,
412
+ [
413
+ f"![banner]({banner_url})",
414
+ f"### {style['emoji']} <font color='{heading_color}' size='4'><b>{colored_title}</b></font>\n\n",
415
+ _kv_lines(
416
+ other_data1,
417
+ next(iter(emoji.keys())),
418
+ next(iter(emoji.values())),
419
+ heading_color=heading_color,
420
+ ),
421
+ _kv_lines(
422
+ other_data2,
423
+ list(emoji.keys())[1],
424
+ list(emoji.values())[1],
425
+ heading_color=heading_color,
426
+ ),
427
+ f"<font color='#9E9E9E' size='1'>Time: "
428
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</font>\n",
429
+ ],
430
+ )
431
+ )
432
+
433
+ _send_md(colored_title, md, type="user")
434
+ except Exception:
435
+ logger.error(traceback.format_exc())
436
+
437
+
438
+ if __name__ == "__main__":
439
+ other_data = {
440
+ "recent_overall_data": "what is memos",
441
+ "site_data": "**📊 Simulated content\nLa la la <font color='red'>320</font>hahaha<font "
442
+ "color='red'>155</font>",
443
+ }
444
+
445
+ online_bot(
446
+ header_name="TextualMemory", # must in English
447
+ sub_title_name="Search", # must in English
448
+ title_color="#00956D",
449
+ other_data1={"Retrieval source 1": "This is plain text memory retrieval content blablabla"},
450
+ other_data2=other_data,
451
+ emoji={"Plain text memory retrieval source": "😨", "Retrieval content": "🕰🐛"},
452
+ )
453
+ print("All messages sent successfully")
@@ -0,0 +1,120 @@
1
+ """
2
+ Lock-free dictionary implementation using copy-on-write strategy.
3
+ This provides better performance but uses more memory.
4
+ """
5
+
6
+ import threading
7
+
8
+ from collections.abc import ItemsView, Iterator, KeysView, ValuesView
9
+ from typing import Generic, TypeVar
10
+
11
+
12
+ K = TypeVar("K")
13
+ V = TypeVar("V")
14
+
15
+
16
+ class CopyOnWriteDict(Generic[K, V]):
17
+ """
18
+ A lock-free dictionary using copy-on-write strategy.
19
+
20
+ Reads are completely lock-free and very fast.
21
+ Writes create a new copy of the dictionary.
22
+ Uses more memory but provides excellent read performance.
23
+ """
24
+
25
+ def __init__(self, initial_dict: dict[K, V] | None = None):
26
+ """Initialize with optional initial dictionary."""
27
+ self._dict = initial_dict.copy() if initial_dict else {}
28
+ self._write_lock = threading.Lock() # Only for writes
29
+
30
+ def __getitem__(self, key: K) -> V:
31
+ """Get item by key - completely lock-free."""
32
+ return self._dict[key]
33
+
34
+ def __setitem__(self, key: K, value: V) -> None:
35
+ """Set item by key - uses copy-on-write."""
36
+ with self._write_lock:
37
+ # Create a new dictionary with the update
38
+ new_dict = self._dict.copy()
39
+ new_dict[key] = value
40
+ # Atomic replacement
41
+ self._dict = new_dict
42
+
43
+ def __delitem__(self, key: K) -> None:
44
+ """Delete item by key - uses copy-on-write."""
45
+ with self._write_lock:
46
+ new_dict = self._dict.copy()
47
+ del new_dict[key]
48
+ self._dict = new_dict
49
+
50
+ def __contains__(self, key: K) -> bool:
51
+ """Check if key exists - completely lock-free."""
52
+ return key in self._dict
53
+
54
+ def __len__(self) -> int:
55
+ """Get length - completely lock-free."""
56
+ return len(self._dict)
57
+
58
+ def __bool__(self) -> bool:
59
+ """Check if not empty - completely lock-free."""
60
+ return bool(self._dict)
61
+
62
+ def __iter__(self) -> Iterator[K]:
63
+ """Iterate over keys - completely lock-free."""
64
+ return iter(self._dict.keys())
65
+
66
+ def get(self, key: K, default: V | None = None) -> V:
67
+ """Get with default - completely lock-free."""
68
+ return self._dict.get(key, default)
69
+
70
+ def keys(self) -> KeysView[K]:
71
+ """Get keys - completely lock-free."""
72
+ return self._dict.keys()
73
+
74
+ def values(self) -> ValuesView[V]:
75
+ """Get values - completely lock-free."""
76
+ return self._dict.values()
77
+
78
+ def items(self) -> ItemsView[K, V]:
79
+ """Get items - completely lock-free."""
80
+ return self._dict.items()
81
+
82
+ def copy(self) -> dict[K, V]:
83
+ """Create a copy - completely lock-free."""
84
+ return self._dict.copy()
85
+
86
+ def update(self, *args, **kwargs) -> None:
87
+ """Update dictionary - uses copy-on-write."""
88
+ with self._write_lock:
89
+ new_dict = self._dict.copy()
90
+ new_dict.update(*args, **kwargs)
91
+ self._dict = new_dict
92
+
93
+ def clear(self) -> None:
94
+ """Clear all items."""
95
+ with self._write_lock:
96
+ self._dict = {}
97
+
98
+ def pop(self, key: K, *args) -> V:
99
+ """Pop item by key."""
100
+ with self._write_lock:
101
+ new_dict = self._dict.copy()
102
+ result = new_dict.pop(key, *args)
103
+ self._dict = new_dict
104
+ return result
105
+
106
+ def setdefault(self, key: K, default: V | None = None) -> V:
107
+ """Set default value for key if not exists."""
108
+ # Fast path for existing keys
109
+ if key in self._dict:
110
+ return self._dict[key]
111
+
112
+ with self._write_lock:
113
+ # Double-check after acquiring lock
114
+ if key in self._dict:
115
+ return self._dict[key]
116
+
117
+ new_dict = self._dict.copy()
118
+ result = new_dict.setdefault(key, default)
119
+ self._dict = new_dict
120
+ return result
@@ -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