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.
- memoryos-2.0.3.dist-info/METADATA +418 -0
- memoryos-2.0.3.dist-info/RECORD +315 -0
- memoryos-2.0.3.dist-info/WHEEL +4 -0
- memoryos-2.0.3.dist-info/entry_points.txt +3 -0
- memoryos-2.0.3.dist-info/licenses/LICENSE +201 -0
- memos/__init__.py +20 -0
- memos/api/client.py +571 -0
- memos/api/config.py +1018 -0
- memos/api/context/dependencies.py +50 -0
- memos/api/exceptions.py +53 -0
- memos/api/handlers/__init__.py +62 -0
- memos/api/handlers/add_handler.py +158 -0
- memos/api/handlers/base_handler.py +194 -0
- memos/api/handlers/chat_handler.py +1401 -0
- memos/api/handlers/component_init.py +388 -0
- memos/api/handlers/config_builders.py +190 -0
- memos/api/handlers/feedback_handler.py +93 -0
- memos/api/handlers/formatters_handler.py +237 -0
- memos/api/handlers/memory_handler.py +316 -0
- memos/api/handlers/scheduler_handler.py +497 -0
- memos/api/handlers/search_handler.py +222 -0
- memos/api/handlers/suggestion_handler.py +117 -0
- memos/api/mcp_serve.py +614 -0
- memos/api/middleware/request_context.py +101 -0
- memos/api/product_api.py +38 -0
- memos/api/product_models.py +1206 -0
- memos/api/routers/__init__.py +1 -0
- memos/api/routers/product_router.py +477 -0
- memos/api/routers/server_router.py +394 -0
- memos/api/server_api.py +44 -0
- memos/api/start_api.py +433 -0
- memos/chunkers/__init__.py +4 -0
- memos/chunkers/base.py +24 -0
- memos/chunkers/charactertext_chunker.py +41 -0
- memos/chunkers/factory.py +24 -0
- memos/chunkers/markdown_chunker.py +62 -0
- memos/chunkers/sentence_chunker.py +54 -0
- memos/chunkers/simple_chunker.py +50 -0
- memos/cli.py +113 -0
- memos/configs/__init__.py +0 -0
- memos/configs/base.py +82 -0
- memos/configs/chunker.py +59 -0
- memos/configs/embedder.py +88 -0
- memos/configs/graph_db.py +236 -0
- memos/configs/internet_retriever.py +100 -0
- memos/configs/llm.py +151 -0
- memos/configs/mem_agent.py +54 -0
- memos/configs/mem_chat.py +81 -0
- memos/configs/mem_cube.py +105 -0
- memos/configs/mem_os.py +83 -0
- memos/configs/mem_reader.py +91 -0
- memos/configs/mem_scheduler.py +385 -0
- memos/configs/mem_user.py +70 -0
- memos/configs/memory.py +324 -0
- memos/configs/parser.py +38 -0
- memos/configs/reranker.py +18 -0
- memos/configs/utils.py +8 -0
- memos/configs/vec_db.py +80 -0
- memos/context/context.py +355 -0
- memos/dependency.py +52 -0
- memos/deprecation.py +262 -0
- memos/embedders/__init__.py +0 -0
- memos/embedders/ark.py +95 -0
- memos/embedders/base.py +106 -0
- memos/embedders/factory.py +29 -0
- memos/embedders/ollama.py +77 -0
- memos/embedders/sentence_transformer.py +49 -0
- memos/embedders/universal_api.py +51 -0
- memos/exceptions.py +30 -0
- memos/graph_dbs/__init__.py +0 -0
- memos/graph_dbs/base.py +274 -0
- memos/graph_dbs/factory.py +27 -0
- memos/graph_dbs/item.py +46 -0
- memos/graph_dbs/nebular.py +1794 -0
- memos/graph_dbs/neo4j.py +1942 -0
- memos/graph_dbs/neo4j_community.py +1058 -0
- memos/graph_dbs/polardb.py +5446 -0
- memos/hello_world.py +97 -0
- memos/llms/__init__.py +0 -0
- memos/llms/base.py +25 -0
- memos/llms/deepseek.py +13 -0
- memos/llms/factory.py +38 -0
- memos/llms/hf.py +443 -0
- memos/llms/hf_singleton.py +114 -0
- memos/llms/ollama.py +135 -0
- memos/llms/openai.py +222 -0
- memos/llms/openai_new.py +198 -0
- memos/llms/qwen.py +13 -0
- memos/llms/utils.py +14 -0
- memos/llms/vllm.py +218 -0
- memos/log.py +237 -0
- memos/mem_agent/base.py +19 -0
- memos/mem_agent/deepsearch_agent.py +391 -0
- memos/mem_agent/factory.py +36 -0
- memos/mem_chat/__init__.py +0 -0
- memos/mem_chat/base.py +30 -0
- memos/mem_chat/factory.py +21 -0
- memos/mem_chat/simple.py +200 -0
- memos/mem_cube/__init__.py +0 -0
- memos/mem_cube/base.py +30 -0
- memos/mem_cube/general.py +240 -0
- memos/mem_cube/navie.py +172 -0
- memos/mem_cube/utils.py +169 -0
- memos/mem_feedback/base.py +15 -0
- memos/mem_feedback/feedback.py +1192 -0
- memos/mem_feedback/simple_feedback.py +40 -0
- memos/mem_feedback/utils.py +230 -0
- memos/mem_os/client.py +5 -0
- memos/mem_os/core.py +1203 -0
- memos/mem_os/main.py +582 -0
- memos/mem_os/product.py +1608 -0
- memos/mem_os/product_server.py +455 -0
- memos/mem_os/utils/default_config.py +359 -0
- memos/mem_os/utils/format_utils.py +1403 -0
- memos/mem_os/utils/reference_utils.py +162 -0
- memos/mem_reader/__init__.py +0 -0
- memos/mem_reader/base.py +47 -0
- memos/mem_reader/factory.py +53 -0
- memos/mem_reader/memory.py +298 -0
- memos/mem_reader/multi_modal_struct.py +965 -0
- memos/mem_reader/read_multi_modal/__init__.py +43 -0
- memos/mem_reader/read_multi_modal/assistant_parser.py +311 -0
- memos/mem_reader/read_multi_modal/base.py +273 -0
- memos/mem_reader/read_multi_modal/file_content_parser.py +826 -0
- memos/mem_reader/read_multi_modal/image_parser.py +359 -0
- memos/mem_reader/read_multi_modal/multi_modal_parser.py +252 -0
- memos/mem_reader/read_multi_modal/string_parser.py +139 -0
- memos/mem_reader/read_multi_modal/system_parser.py +327 -0
- memos/mem_reader/read_multi_modal/text_content_parser.py +131 -0
- memos/mem_reader/read_multi_modal/tool_parser.py +210 -0
- memos/mem_reader/read_multi_modal/user_parser.py +218 -0
- memos/mem_reader/read_multi_modal/utils.py +358 -0
- memos/mem_reader/simple_struct.py +912 -0
- memos/mem_reader/strategy_struct.py +163 -0
- memos/mem_reader/utils.py +157 -0
- memos/mem_scheduler/__init__.py +0 -0
- memos/mem_scheduler/analyzer/__init__.py +0 -0
- memos/mem_scheduler/analyzer/api_analyzer.py +714 -0
- memos/mem_scheduler/analyzer/eval_analyzer.py +219 -0
- memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +571 -0
- memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
- memos/mem_scheduler/base_scheduler.py +1319 -0
- memos/mem_scheduler/general_modules/__init__.py +0 -0
- memos/mem_scheduler/general_modules/api_misc.py +137 -0
- memos/mem_scheduler/general_modules/base.py +80 -0
- memos/mem_scheduler/general_modules/init_components_for_scheduler.py +425 -0
- memos/mem_scheduler/general_modules/misc.py +313 -0
- memos/mem_scheduler/general_modules/scheduler_logger.py +389 -0
- memos/mem_scheduler/general_modules/task_threads.py +315 -0
- memos/mem_scheduler/general_scheduler.py +1495 -0
- memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
- memos/mem_scheduler/memory_manage_modules/memory_filter.py +306 -0
- memos/mem_scheduler/memory_manage_modules/retriever.py +547 -0
- memos/mem_scheduler/monitors/__init__.py +0 -0
- memos/mem_scheduler/monitors/dispatcher_monitor.py +366 -0
- memos/mem_scheduler/monitors/general_monitor.py +394 -0
- memos/mem_scheduler/monitors/task_schedule_monitor.py +254 -0
- memos/mem_scheduler/optimized_scheduler.py +410 -0
- memos/mem_scheduler/orm_modules/__init__.py +0 -0
- memos/mem_scheduler/orm_modules/api_redis_model.py +518 -0
- memos/mem_scheduler/orm_modules/base_model.py +729 -0
- memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
- memos/mem_scheduler/orm_modules/redis_model.py +699 -0
- memos/mem_scheduler/scheduler_factory.py +23 -0
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/analyzer_schemas.py +52 -0
- memos/mem_scheduler/schemas/api_schemas.py +233 -0
- memos/mem_scheduler/schemas/general_schemas.py +55 -0
- memos/mem_scheduler/schemas/message_schemas.py +173 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +406 -0
- memos/mem_scheduler/schemas/task_schemas.py +132 -0
- memos/mem_scheduler/task_schedule_modules/__init__.py +0 -0
- memos/mem_scheduler/task_schedule_modules/dispatcher.py +740 -0
- memos/mem_scheduler/task_schedule_modules/local_queue.py +247 -0
- memos/mem_scheduler/task_schedule_modules/orchestrator.py +74 -0
- memos/mem_scheduler/task_schedule_modules/redis_queue.py +1385 -0
- memos/mem_scheduler/task_schedule_modules/task_queue.py +162 -0
- memos/mem_scheduler/utils/__init__.py +0 -0
- memos/mem_scheduler/utils/api_utils.py +77 -0
- memos/mem_scheduler/utils/config_utils.py +100 -0
- memos/mem_scheduler/utils/db_utils.py +50 -0
- memos/mem_scheduler/utils/filter_utils.py +176 -0
- memos/mem_scheduler/utils/metrics.py +125 -0
- memos/mem_scheduler/utils/misc_utils.py +290 -0
- memos/mem_scheduler/utils/monitor_event_utils.py +67 -0
- memos/mem_scheduler/utils/status_tracker.py +229 -0
- memos/mem_scheduler/webservice_modules/__init__.py +0 -0
- memos/mem_scheduler/webservice_modules/rabbitmq_service.py +485 -0
- memos/mem_scheduler/webservice_modules/redis_service.py +380 -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 +502 -0
- memos/mem_user/persistent_factory.py +98 -0
- memos/mem_user/persistent_user_manager.py +260 -0
- memos/mem_user/redis_persistent_user_manager.py +225 -0
- memos/mem_user/user_manager.py +488 -0
- memos/memories/__init__.py +0 -0
- memos/memories/activation/__init__.py +0 -0
- memos/memories/activation/base.py +42 -0
- memos/memories/activation/item.py +56 -0
- memos/memories/activation/kv.py +292 -0
- memos/memories/activation/vllmkv.py +219 -0
- memos/memories/base.py +19 -0
- memos/memories/factory.py +42 -0
- memos/memories/parametric/__init__.py +0 -0
- memos/memories/parametric/base.py +19 -0
- memos/memories/parametric/item.py +11 -0
- memos/memories/parametric/lora.py +41 -0
- memos/memories/textual/__init__.py +0 -0
- memos/memories/textual/base.py +92 -0
- memos/memories/textual/general.py +236 -0
- memos/memories/textual/item.py +304 -0
- memos/memories/textual/naive.py +187 -0
- memos/memories/textual/prefer_text_memory/__init__.py +0 -0
- memos/memories/textual/prefer_text_memory/adder.py +504 -0
- memos/memories/textual/prefer_text_memory/config.py +106 -0
- memos/memories/textual/prefer_text_memory/extractor.py +221 -0
- memos/memories/textual/prefer_text_memory/factory.py +85 -0
- memos/memories/textual/prefer_text_memory/retrievers.py +177 -0
- memos/memories/textual/prefer_text_memory/spliter.py +132 -0
- memos/memories/textual/prefer_text_memory/utils.py +93 -0
- memos/memories/textual/preference.py +344 -0
- memos/memories/textual/simple_preference.py +161 -0
- memos/memories/textual/simple_tree.py +69 -0
- memos/memories/textual/tree.py +459 -0
- memos/memories/textual/tree_text_memory/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/handler.py +184 -0
- memos/memories/textual/tree_text_memory/organize/manager.py +518 -0
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +238 -0
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +622 -0
- memos/memories/textual/tree_text_memory/retrieve/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/retrieve/advanced_searcher.py +364 -0
- memos/memories/textual/tree_text_memory/retrieve/bm25_util.py +186 -0
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +419 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +270 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +102 -0
- memos/memories/textual/tree_text_memory/retrieve/reasoner.py +61 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +497 -0
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +111 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +16 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieve_utils.py +472 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +848 -0
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +135 -0
- memos/memories/textual/tree_text_memory/retrieve/utils.py +54 -0
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +387 -0
- memos/memos_tools/dinding_report_bot.py +453 -0
- memos/memos_tools/lockfree_dict.py +120 -0
- memos/memos_tools/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +142 -0
- memos/memos_tools/singleton.py +174 -0
- memos/memos_tools/thread_safe_dict.py +310 -0
- memos/memos_tools/thread_safe_dict_segment.py +382 -0
- memos/multi_mem_cube/__init__.py +0 -0
- memos/multi_mem_cube/composite_cube.py +86 -0
- memos/multi_mem_cube/single_cube.py +874 -0
- memos/multi_mem_cube/views.py +54 -0
- memos/parsers/__init__.py +0 -0
- memos/parsers/base.py +15 -0
- memos/parsers/factory.py +21 -0
- memos/parsers/markitdown.py +28 -0
- memos/reranker/__init__.py +4 -0
- memos/reranker/base.py +25 -0
- memos/reranker/concat.py +103 -0
- memos/reranker/cosine_local.py +102 -0
- memos/reranker/factory.py +72 -0
- memos/reranker/http_bge.py +324 -0
- memos/reranker/http_bge_strategy.py +327 -0
- memos/reranker/noop.py +19 -0
- memos/reranker/strategies/__init__.py +4 -0
- memos/reranker/strategies/base.py +61 -0
- memos/reranker/strategies/concat_background.py +94 -0
- memos/reranker/strategies/concat_docsource.py +110 -0
- memos/reranker/strategies/dialogue_common.py +109 -0
- memos/reranker/strategies/factory.py +31 -0
- memos/reranker/strategies/single_turn.py +107 -0
- memos/reranker/strategies/singleturn_outmem.py +98 -0
- memos/settings.py +10 -0
- memos/templates/__init__.py +0 -0
- memos/templates/advanced_search_prompts.py +211 -0
- memos/templates/cloud_service_prompt.py +107 -0
- memos/templates/instruction_completion.py +66 -0
- memos/templates/mem_agent_prompts.py +85 -0
- memos/templates/mem_feedback_prompts.py +822 -0
- memos/templates/mem_reader_prompts.py +1096 -0
- memos/templates/mem_reader_strategy_prompts.py +238 -0
- memos/templates/mem_scheduler_prompts.py +626 -0
- memos/templates/mem_search_prompts.py +93 -0
- memos/templates/mos_prompts.py +403 -0
- memos/templates/prefer_complete_prompt.py +735 -0
- memos/templates/tool_mem_prompts.py +139 -0
- memos/templates/tree_reorganize_prompts.py +230 -0
- memos/types/__init__.py +34 -0
- memos/types/general_types.py +151 -0
- memos/types/openai_chat_completion_types/__init__.py +15 -0
- memos/types/openai_chat_completion_types/chat_completion_assistant_message_param.py +56 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py +27 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_input_audio_param.py +23 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_param.py +43 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_refusal_param.py +16 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_text_param.py +16 -0
- memos/types/openai_chat_completion_types/chat_completion_message_custom_tool_call_param.py +27 -0
- memos/types/openai_chat_completion_types/chat_completion_message_function_tool_call_param.py +32 -0
- memos/types/openai_chat_completion_types/chat_completion_message_param.py +18 -0
- memos/types/openai_chat_completion_types/chat_completion_message_tool_call_union_param.py +15 -0
- memos/types/openai_chat_completion_types/chat_completion_system_message_param.py +36 -0
- memos/types/openai_chat_completion_types/chat_completion_tool_message_param.py +30 -0
- memos/types/openai_chat_completion_types/chat_completion_user_message_param.py +34 -0
- memos/utils.py +123 -0
- memos/vec_dbs/__init__.py +0 -0
- memos/vec_dbs/base.py +117 -0
- memos/vec_dbs/factory.py +23 -0
- memos/vec_dbs/item.py +50 -0
- memos/vec_dbs/milvus.py +654 -0
- 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"×tamp={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"\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"",
|
|
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
|