botrun-flow-lang 5.12.263__py3-none-any.whl → 5.12.264__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.
- botrun_flow_lang/api/auth_api.py +39 -39
- botrun_flow_lang/api/auth_utils.py +183 -183
- botrun_flow_lang/api/botrun_back_api.py +65 -65
- botrun_flow_lang/api/flow_api.py +3 -3
- botrun_flow_lang/api/hatch_api.py +508 -508
- botrun_flow_lang/api/langgraph_api.py +811 -811
- botrun_flow_lang/api/line_bot_api.py +1484 -1484
- botrun_flow_lang/api/model_api.py +300 -300
- botrun_flow_lang/api/rate_limit_api.py +32 -32
- botrun_flow_lang/api/routes.py +79 -79
- botrun_flow_lang/api/search_api.py +53 -53
- botrun_flow_lang/api/storage_api.py +395 -395
- botrun_flow_lang/api/subsidy_api.py +290 -290
- botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
- botrun_flow_lang/api/user_setting_api.py +70 -70
- botrun_flow_lang/api/version_api.py +31 -31
- botrun_flow_lang/api/youtube_api.py +26 -26
- botrun_flow_lang/constants.py +13 -13
- botrun_flow_lang/langgraph_agents/agents/agent_runner.py +178 -178
- botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
- botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +460 -460
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
- botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +723 -723
- botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
- botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
- botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
- botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
- botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
- botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
- botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
- botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
- botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
- botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +486 -486
- botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
- botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
- botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
- botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
- botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
- botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
- botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
- botrun_flow_lang/llm_agent/llm_agent.py +19 -19
- botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
- botrun_flow_lang/log/.gitignore +2 -2
- botrun_flow_lang/main.py +61 -61
- botrun_flow_lang/main_fast.py +51 -51
- botrun_flow_lang/mcp_server/__init__.py +10 -10
- botrun_flow_lang/mcp_server/default_mcp.py +744 -744
- botrun_flow_lang/models/nodes/utils.py +205 -205
- botrun_flow_lang/models/token_usage.py +34 -34
- botrun_flow_lang/requirements.txt +21 -21
- botrun_flow_lang/services/base/firestore_base.py +30 -30
- botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
- botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
- botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
- botrun_flow_lang/services/storage/storage_factory.py +12 -12
- botrun_flow_lang/services/storage/storage_store.py +65 -65
- botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
- botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
- botrun_flow_lang/static/docs/tools/index.html +926 -926
- botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
- botrun_flow_lang/tests/api_stress_test.py +357 -357
- botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
- botrun_flow_lang/tests/test_botrun_app.py +46 -46
- botrun_flow_lang/tests/test_html_util.py +31 -31
- botrun_flow_lang/tests/test_img_analyzer.py +190 -190
- botrun_flow_lang/tests/test_img_util.py +39 -39
- botrun_flow_lang/tests/test_local_files.py +114 -114
- botrun_flow_lang/tests/test_mermaid_util.py +103 -103
- botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
- botrun_flow_lang/tests/test_plotly_util.py +151 -151
- botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
- botrun_flow_lang/tools/generate_docs.py +133 -133
- botrun_flow_lang/tools/templates/tools.html +153 -153
- botrun_flow_lang/utils/__init__.py +7 -7
- botrun_flow_lang/utils/botrun_logger.py +344 -344
- botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
- botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
- botrun_flow_lang/utils/google_drive_utils.py +654 -654
- botrun_flow_lang/utils/langchain_utils.py +324 -324
- botrun_flow_lang/utils/yaml_utils.py +9 -9
- {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/METADATA +1 -1
- botrun_flow_lang-5.12.264.dist-info/RECORD +102 -0
- botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
- {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/WHEEL +0 -0
|
@@ -1,344 +1,344 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import traceback
|
|
3
|
-
import inspect
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from typing import Dict, Any, Optional
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
import sys
|
|
8
|
-
import logging
|
|
9
|
-
|
|
10
|
-
from google.cloud import logging as cloud_logging
|
|
11
|
-
from google.cloud.logging import Resource
|
|
12
|
-
from google.oauth2 import service_account
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class BotrunLogger(logging.Logger):
|
|
16
|
-
"""Botrun 專用日誌系統,基於 Python 標準 logging 和 Google Cloud Logging
|
|
17
|
-
|
|
18
|
-
提供結構化日誌記錄功能,支援多種日誌級別、自定義項目 ID 和服務帳戶認證。
|
|
19
|
-
可根據環境變數配置日誌行為,支援在 Cloud Run 環境中自動獲取資源信息。
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def __init__(
|
|
23
|
-
self,
|
|
24
|
-
name: str = "botrun",
|
|
25
|
-
level: str = "INFO",
|
|
26
|
-
log_name: str = "",
|
|
27
|
-
resource_type: str = "cloud_run_revision",
|
|
28
|
-
project_id: Optional[str] = None,
|
|
29
|
-
service_account_file: Optional[str] = None,
|
|
30
|
-
session_id: Optional[str] = None,
|
|
31
|
-
user_id: Optional[str] = None,
|
|
32
|
-
):
|
|
33
|
-
"""初始化 BotrunLogger
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
name: logger 名稱
|
|
37
|
-
level: 日誌級別
|
|
38
|
-
log_name: 日誌名稱,用於在 Cloud Logging 中區分不同服務的日誌
|
|
39
|
-
resource_type: Google Cloud 資源類型,通常為 cloud_run_revision 或 global
|
|
40
|
-
project_id: 指定 GCP 項目 ID,如不指定則使用默認項目
|
|
41
|
-
service_account_file: 服務帳戶密鑰文件路徑,如不指定則使用應用默認憑證
|
|
42
|
-
session_id: 會話 ID
|
|
43
|
-
user_id: 用戶 ID
|
|
44
|
-
"""
|
|
45
|
-
# 初始化基礎 Logger
|
|
46
|
-
level = os.getenv("BOTRUN_LOGGER_LEVEL", "INFO")
|
|
47
|
-
super().__init__(name, getattr(logging, level.upper()))
|
|
48
|
-
|
|
49
|
-
# 如果沒有 handler,加入一個基本的 console handler
|
|
50
|
-
if not self.handlers:
|
|
51
|
-
console_handler = logging.StreamHandler()
|
|
52
|
-
# Use the custom fields passed in 'extra'
|
|
53
|
-
formatter = logging.Formatter(
|
|
54
|
-
"[%(levelname)s][%(caller_filename)s:%(caller_funcName)s:%(caller_lineno)d] %(message)s"
|
|
55
|
-
)
|
|
56
|
-
console_handler.setFormatter(formatter)
|
|
57
|
-
self.addHandler(console_handler)
|
|
58
|
-
|
|
59
|
-
# 從環境變數獲取配置
|
|
60
|
-
self.log_payload = os.getenv("LOG_PAYLOAD", "true").lower() == "true"
|
|
61
|
-
|
|
62
|
-
if not log_name:
|
|
63
|
-
log_name = os.getenv("BOTRUN_LOG_NAME", "log-dev-botrun-ai")
|
|
64
|
-
print(f"[BotrunLogger]log_name: {log_name}")
|
|
65
|
-
# 如果未指定項目 ID,嘗試從環境變數獲取
|
|
66
|
-
if not project_id:
|
|
67
|
-
project_id = os.getenv("BOTRUN_LOG_PROJECT_ID", "scoop-386004")
|
|
68
|
-
print(f"[BotrunLogger]project_id: {project_id}")
|
|
69
|
-
|
|
70
|
-
# 如果未指定服務帳戶文件,嘗試從環境變數獲取
|
|
71
|
-
if not service_account_file:
|
|
72
|
-
service_account_file = os.getenv("BOTRUN_LOG_CREDENTIALS_PATH", "")
|
|
73
|
-
print(f"[BotrunLogger]service_account_file: {service_account_file}")
|
|
74
|
-
|
|
75
|
-
self.session_id = session_id
|
|
76
|
-
self.user_id = user_id
|
|
77
|
-
|
|
78
|
-
# 初始化 Cloud Logging 客戶端
|
|
79
|
-
self.client = self._initialize_client(project_id, service_account_file)
|
|
80
|
-
self.use_cloud_logging = self.client is not None
|
|
81
|
-
if self.use_cloud_logging:
|
|
82
|
-
self.cloud_logger = self.client.logger(log_name)
|
|
83
|
-
self.project_id = self.client.project
|
|
84
|
-
else:
|
|
85
|
-
self.cloud_logger = None
|
|
86
|
-
self.project_id = project_id or "unknown-project"
|
|
87
|
-
|
|
88
|
-
self.resource_type = resource_type
|
|
89
|
-
self.resource_labels = self._get_resource_labels()
|
|
90
|
-
|
|
91
|
-
# 記錄初始化信息
|
|
92
|
-
self.debug(
|
|
93
|
-
"BotrunLogger 初始化完成",
|
|
94
|
-
event="logger_init",
|
|
95
|
-
log_name=log_name,
|
|
96
|
-
resource_type=resource_type,
|
|
97
|
-
project_id=self.project_id,
|
|
98
|
-
cloud_logging_enabled=self.use_cloud_logging,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def _log(
|
|
102
|
-
self,
|
|
103
|
-
level: int,
|
|
104
|
-
msg: Any,
|
|
105
|
-
args: tuple,
|
|
106
|
-
exc_info=None,
|
|
107
|
-
extra=None,
|
|
108
|
-
stack_info=False,
|
|
109
|
-
stacklevel=1,
|
|
110
|
-
**kwargs,
|
|
111
|
-
) -> None:
|
|
112
|
-
"""重寫 _log 方法以支援結構化日誌和 Cloud Logging
|
|
113
|
-
|
|
114
|
-
這是 logging.Logger 的核心方法,所有日誌方法最終都會調用這個方法。
|
|
115
|
-
"""
|
|
116
|
-
# --- Find the correct caller frame outside this logger class --- #
|
|
117
|
-
f = inspect.currentframe()
|
|
118
|
-
# Default stacklevel for logging methods is 1. We need to go up further.
|
|
119
|
-
# Levels: _log -> std library caller (e.g., info) -> actual user call
|
|
120
|
-
frames_to_go_up = stacklevel + 1
|
|
121
|
-
for _ in range(frames_to_go_up):
|
|
122
|
-
if f is None:
|
|
123
|
-
break
|
|
124
|
-
f = f.f_back
|
|
125
|
-
|
|
126
|
-
if f:
|
|
127
|
-
caller_file = Path(f.f_code.co_filename).name
|
|
128
|
-
caller_func = f.f_code.co_name
|
|
129
|
-
caller_line = f.f_lineno
|
|
130
|
-
prefix = f"[{caller_file}:{caller_func}:{caller_line}] "
|
|
131
|
-
else:
|
|
132
|
-
caller_file = "unknown"
|
|
133
|
-
caller_func = "unknown"
|
|
134
|
-
caller_line = 0
|
|
135
|
-
prefix = ""
|
|
136
|
-
|
|
137
|
-
# --- Prepare data for Cloud Logging --- #
|
|
138
|
-
log_data_for_cloud = {
|
|
139
|
-
"message": f"{prefix}{msg % args if args else msg}", # Format message for cloud
|
|
140
|
-
"timestamp": datetime.now().isoformat(),
|
|
141
|
-
"environment": os.getenv("ENV_NAME", "botrun-flow-lang-dev"),
|
|
142
|
-
"application": "botrun_flow_lang",
|
|
143
|
-
"file": caller_file,
|
|
144
|
-
"function": caller_func,
|
|
145
|
-
"line": caller_line,
|
|
146
|
-
"session_id": self.session_id,
|
|
147
|
-
"user_id": self.user_id,
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
# Handle exception info for Cloud Logging
|
|
151
|
-
computed_exc_info = None
|
|
152
|
-
if exc_info:
|
|
153
|
-
if isinstance(exc_info, BaseException):
|
|
154
|
-
computed_exc_info = (type(exc_info), exc_info, exc_info.__traceback__)
|
|
155
|
-
elif not isinstance(exc_info, tuple):
|
|
156
|
-
computed_exc_info = sys.exc_info()
|
|
157
|
-
|
|
158
|
-
if computed_exc_info and computed_exc_info[0] is not None:
|
|
159
|
-
log_data_for_cloud["exception"] = {
|
|
160
|
-
"type": computed_exc_info[0].__name__,
|
|
161
|
-
"message": str(computed_exc_info[1]),
|
|
162
|
-
"traceback": "".join(
|
|
163
|
-
traceback.format_exception(*computed_exc_info)
|
|
164
|
-
),
|
|
165
|
-
}
|
|
166
|
-
else:
|
|
167
|
-
computed_exc_info = None # Ensure it's None if exc_info was false
|
|
168
|
-
|
|
169
|
-
# Add extra kwargs passed directly to the log call for Cloud Logging
|
|
170
|
-
if kwargs:
|
|
171
|
-
log_data_for_cloud.update(kwargs)
|
|
172
|
-
|
|
173
|
-
# --- Prepare data for standard logging handlers (like console) --- #
|
|
174
|
-
# Create the 'extra' dict to pass correct caller info to handlers
|
|
175
|
-
handler_extra = extra or {}
|
|
176
|
-
handler_extra["caller_filename"] = caller_file
|
|
177
|
-
handler_extra["caller_funcName"] = caller_func
|
|
178
|
-
handler_extra["caller_lineno"] = caller_line
|
|
179
|
-
|
|
180
|
-
# --- Call parent _log for standard handlers --- #
|
|
181
|
-
# Pass the original msg and args so Formatter works correctly
|
|
182
|
-
# Pass the computed exception info
|
|
183
|
-
# Pass the enriched 'extra' dictionary
|
|
184
|
-
# Pass stack_info if provided
|
|
185
|
-
# stacklevel=1 here tells the *parent* _log it doesn't need to look further up
|
|
186
|
-
super()._log(
|
|
187
|
-
level,
|
|
188
|
-
msg,
|
|
189
|
-
args,
|
|
190
|
-
exc_info=computed_exc_info,
|
|
191
|
-
extra=handler_extra,
|
|
192
|
-
stack_info=stack_info,
|
|
193
|
-
stacklevel=1,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
# --- Send to Cloud Logging if enabled --- #
|
|
197
|
-
if self.use_cloud_logging and self.isEnabledFor(level):
|
|
198
|
-
try:
|
|
199
|
-
severity = logging.getLevelName(level)
|
|
200
|
-
self.cloud_logger.log_struct(
|
|
201
|
-
log_data_for_cloud, # Use the prepared cloud data
|
|
202
|
-
severity=severity,
|
|
203
|
-
resource=Resource(
|
|
204
|
-
type=self.resource_type, labels=self.resource_labels
|
|
205
|
-
),
|
|
206
|
-
)
|
|
207
|
-
except Exception as e:
|
|
208
|
-
# Avoid Cloud Logging failure crashing the app
|
|
209
|
-
# Log the failure using the standard handler
|
|
210
|
-
|
|
211
|
-
traceback.print_exc()
|
|
212
|
-
print(f"Cloud Logging failed: {e}")
|
|
213
|
-
# self.handle(
|
|
214
|
-
# self.makeRecord(
|
|
215
|
-
# self.name,
|
|
216
|
-
# logging.ERROR,
|
|
217
|
-
# "(unknown file)",
|
|
218
|
-
# 0,
|
|
219
|
-
# f"Cloud Logging failed: {e}",
|
|
220
|
-
# (),
|
|
221
|
-
# None,
|
|
222
|
-
# func="_log",
|
|
223
|
-
# )
|
|
224
|
-
# )
|
|
225
|
-
|
|
226
|
-
def _initialize_client(
|
|
227
|
-
self, project_id: Optional[str], service_account_file: Optional[str]
|
|
228
|
-
) -> Optional[cloud_logging.Client]:
|
|
229
|
-
"""初始化 Cloud Logging 客戶端
|
|
230
|
-
|
|
231
|
-
Args:
|
|
232
|
-
project_id: GCP 項目 ID
|
|
233
|
-
service_account_file: 服務帳戶密鑰文件路徑
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
Optional[cloud_logging.Client]: 已配置的日誌客戶端,若初始化失敗則為 None
|
|
237
|
-
"""
|
|
238
|
-
try:
|
|
239
|
-
# 使用服務帳戶認證
|
|
240
|
-
if service_account_file and os.path.exists(service_account_file):
|
|
241
|
-
credentials = service_account.Credentials.from_service_account_file(
|
|
242
|
-
service_account_file
|
|
243
|
-
)
|
|
244
|
-
return cloud_logging.Client(project=project_id, credentials=credentials)
|
|
245
|
-
# 使用應用默認憑證
|
|
246
|
-
return cloud_logging.Client(project=project_id)
|
|
247
|
-
except Exception as e:
|
|
248
|
-
# 如果初始化失敗,打印錯誤並回退到標準輸出
|
|
249
|
-
print(f"⚠️ 無法初始化 Cloud Logging 客戶端: {str(e)}")
|
|
250
|
-
print(f"將使用標準輸出記錄日誌。錯誤詳情: {traceback.format_exc()}")
|
|
251
|
-
return None
|
|
252
|
-
|
|
253
|
-
def _get_resource_labels(self) -> Dict[str, str]:
|
|
254
|
-
"""獲取 Cloud Run 資源標籤
|
|
255
|
-
|
|
256
|
-
從環境變數中獲取 Cloud Run 相關信息,用於資源標籤
|
|
257
|
-
|
|
258
|
-
Returns:
|
|
259
|
-
Dict[str, str]: 資源標籤字典
|
|
260
|
-
"""
|
|
261
|
-
return {}
|
|
262
|
-
|
|
263
|
-
# 為了保持與原有代碼的相容性,保留這些方法,但現在它們直接調用父類的方法
|
|
264
|
-
# These methods are now redundant because the base class methods will call our overridden _log method.
|
|
265
|
-
# Remove debug, info, warning, error, critical methods
|
|
266
|
-
# def debug(self, msg: str, *args, **kwargs) -> None:
|
|
267
|
-
# """記錄 DEBUG 級別的日誌"""
|
|
268
|
-
# super().debug(msg, *args, **kwargs)
|
|
269
|
-
#
|
|
270
|
-
# def info(self, msg: str, *args, **kwargs) -> None:
|
|
271
|
-
# """記錄 INFO 級別的日誌"""
|
|
272
|
-
# super().info(msg, *args, **kwargs)
|
|
273
|
-
#
|
|
274
|
-
# def warning(self, msg: str, *args, **kwargs) -> None:
|
|
275
|
-
# """記錄 WARNING 級別的日誌"""
|
|
276
|
-
# super().warning(msg, *args, **kwargs)
|
|
277
|
-
#
|
|
278
|
-
# def error(self, msg: str, *args, exc_info: bool = True, **kwargs) -> None:
|
|
279
|
-
# """記錄 ERROR 級別的日誌"""
|
|
280
|
-
# super().error(msg, *args, exc_info=exc_info, **kwargs)
|
|
281
|
-
#
|
|
282
|
-
# def critical(self, msg: str, *args, **kwargs) -> None:
|
|
283
|
-
# """記錄 CRITICAL 級別的日誌"""
|
|
284
|
-
# super().critical(msg, *args, **kwargs)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
# --- Default Standard Logger --- #
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _create_default_logger() -> logging.Logger:
|
|
291
|
-
"""Creates a default standard Python logger that streams to console."""
|
|
292
|
-
logger = logging.getLogger("botrun.default")
|
|
293
|
-
logger.setLevel(logging.INFO) # Set default level
|
|
294
|
-
|
|
295
|
-
# Avoid adding duplicate handlers if this is called multiple times
|
|
296
|
-
if not logger.handlers:
|
|
297
|
-
handler = logging.StreamHandler()
|
|
298
|
-
# Use format similar to BotrunLogger's prefix
|
|
299
|
-
formatter = logging.Formatter(
|
|
300
|
-
"[%(levelname)s][%(filename)s:%(funcName)s:%(lineno)d] %(message)s"
|
|
301
|
-
)
|
|
302
|
-
handler.setFormatter(formatter)
|
|
303
|
-
logger.addHandler(handler)
|
|
304
|
-
|
|
305
|
-
return logger
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
# 建立一個預設的標準 logger 實例
|
|
309
|
-
# This logger only prints to console and does NOT send to Cloud Logging.
|
|
310
|
-
# Use BotrunLogger explicitly when Cloud Logging features are needed.
|
|
311
|
-
default_logger = _create_default_logger()
|
|
312
|
-
|
|
313
|
-
# 建立一個預設的 BotrunLogger 全域實例
|
|
314
|
-
# 大部分模組可以重用這個實例,避免重複初始化
|
|
315
|
-
_default_botrun_logger = None
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def get_default_botrun_logger() -> BotrunLogger:
|
|
319
|
-
"""
|
|
320
|
-
獲取預設的 BotrunLogger 實例,只會初始化一次
|
|
321
|
-
|
|
322
|
-
Returns:
|
|
323
|
-
BotrunLogger: 預設的 BotrunLogger 實例
|
|
324
|
-
"""
|
|
325
|
-
global _default_botrun_logger
|
|
326
|
-
if _default_botrun_logger is None:
|
|
327
|
-
_default_botrun_logger = BotrunLogger()
|
|
328
|
-
return _default_botrun_logger
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def get_session_botrun_logger(
|
|
332
|
-
session_id: str = None, user_id: str = None
|
|
333
|
-
) -> BotrunLogger:
|
|
334
|
-
"""
|
|
335
|
-
獲取帶有 session 和 user 信息的 BotrunLogger 實例
|
|
336
|
-
|
|
337
|
-
Args:
|
|
338
|
-
session_id: 會話 ID
|
|
339
|
-
user_id: 用戶 ID
|
|
340
|
-
|
|
341
|
-
Returns:
|
|
342
|
-
BotrunLogger: 帶有特定 session/user 信息的 BotrunLogger 實例
|
|
343
|
-
"""
|
|
344
|
-
return BotrunLogger(session_id=session_id, user_id=user_id)
|
|
1
|
+
import os
|
|
2
|
+
import traceback
|
|
3
|
+
import inspect
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from google.cloud import logging as cloud_logging
|
|
11
|
+
from google.cloud.logging import Resource
|
|
12
|
+
from google.oauth2 import service_account
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BotrunLogger(logging.Logger):
|
|
16
|
+
"""Botrun 專用日誌系統,基於 Python 標準 logging 和 Google Cloud Logging
|
|
17
|
+
|
|
18
|
+
提供結構化日誌記錄功能,支援多種日誌級別、自定義項目 ID 和服務帳戶認證。
|
|
19
|
+
可根據環境變數配置日誌行為,支援在 Cloud Run 環境中自動獲取資源信息。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
name: str = "botrun",
|
|
25
|
+
level: str = "INFO",
|
|
26
|
+
log_name: str = "",
|
|
27
|
+
resource_type: str = "cloud_run_revision",
|
|
28
|
+
project_id: Optional[str] = None,
|
|
29
|
+
service_account_file: Optional[str] = None,
|
|
30
|
+
session_id: Optional[str] = None,
|
|
31
|
+
user_id: Optional[str] = None,
|
|
32
|
+
):
|
|
33
|
+
"""初始化 BotrunLogger
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name: logger 名稱
|
|
37
|
+
level: 日誌級別
|
|
38
|
+
log_name: 日誌名稱,用於在 Cloud Logging 中區分不同服務的日誌
|
|
39
|
+
resource_type: Google Cloud 資源類型,通常為 cloud_run_revision 或 global
|
|
40
|
+
project_id: 指定 GCP 項目 ID,如不指定則使用默認項目
|
|
41
|
+
service_account_file: 服務帳戶密鑰文件路徑,如不指定則使用應用默認憑證
|
|
42
|
+
session_id: 會話 ID
|
|
43
|
+
user_id: 用戶 ID
|
|
44
|
+
"""
|
|
45
|
+
# 初始化基礎 Logger
|
|
46
|
+
level = os.getenv("BOTRUN_LOGGER_LEVEL", "INFO")
|
|
47
|
+
super().__init__(name, getattr(logging, level.upper()))
|
|
48
|
+
|
|
49
|
+
# 如果沒有 handler,加入一個基本的 console handler
|
|
50
|
+
if not self.handlers:
|
|
51
|
+
console_handler = logging.StreamHandler()
|
|
52
|
+
# Use the custom fields passed in 'extra'
|
|
53
|
+
formatter = logging.Formatter(
|
|
54
|
+
"[%(levelname)s][%(caller_filename)s:%(caller_funcName)s:%(caller_lineno)d] %(message)s"
|
|
55
|
+
)
|
|
56
|
+
console_handler.setFormatter(formatter)
|
|
57
|
+
self.addHandler(console_handler)
|
|
58
|
+
|
|
59
|
+
# 從環境變數獲取配置
|
|
60
|
+
self.log_payload = os.getenv("LOG_PAYLOAD", "true").lower() == "true"
|
|
61
|
+
|
|
62
|
+
if not log_name:
|
|
63
|
+
log_name = os.getenv("BOTRUN_LOG_NAME", "log-dev-botrun-ai")
|
|
64
|
+
print(f"[BotrunLogger]log_name: {log_name}")
|
|
65
|
+
# 如果未指定項目 ID,嘗試從環境變數獲取
|
|
66
|
+
if not project_id:
|
|
67
|
+
project_id = os.getenv("BOTRUN_LOG_PROJECT_ID", "scoop-386004")
|
|
68
|
+
print(f"[BotrunLogger]project_id: {project_id}")
|
|
69
|
+
|
|
70
|
+
# 如果未指定服務帳戶文件,嘗試從環境變數獲取
|
|
71
|
+
if not service_account_file:
|
|
72
|
+
service_account_file = os.getenv("BOTRUN_LOG_CREDENTIALS_PATH", "")
|
|
73
|
+
print(f"[BotrunLogger]service_account_file: {service_account_file}")
|
|
74
|
+
|
|
75
|
+
self.session_id = session_id
|
|
76
|
+
self.user_id = user_id
|
|
77
|
+
|
|
78
|
+
# 初始化 Cloud Logging 客戶端
|
|
79
|
+
self.client = self._initialize_client(project_id, service_account_file)
|
|
80
|
+
self.use_cloud_logging = self.client is not None
|
|
81
|
+
if self.use_cloud_logging:
|
|
82
|
+
self.cloud_logger = self.client.logger(log_name)
|
|
83
|
+
self.project_id = self.client.project
|
|
84
|
+
else:
|
|
85
|
+
self.cloud_logger = None
|
|
86
|
+
self.project_id = project_id or "unknown-project"
|
|
87
|
+
|
|
88
|
+
self.resource_type = resource_type
|
|
89
|
+
self.resource_labels = self._get_resource_labels()
|
|
90
|
+
|
|
91
|
+
# 記錄初始化信息
|
|
92
|
+
self.debug(
|
|
93
|
+
"BotrunLogger 初始化完成",
|
|
94
|
+
event="logger_init",
|
|
95
|
+
log_name=log_name,
|
|
96
|
+
resource_type=resource_type,
|
|
97
|
+
project_id=self.project_id,
|
|
98
|
+
cloud_logging_enabled=self.use_cloud_logging,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _log(
|
|
102
|
+
self,
|
|
103
|
+
level: int,
|
|
104
|
+
msg: Any,
|
|
105
|
+
args: tuple,
|
|
106
|
+
exc_info=None,
|
|
107
|
+
extra=None,
|
|
108
|
+
stack_info=False,
|
|
109
|
+
stacklevel=1,
|
|
110
|
+
**kwargs,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""重寫 _log 方法以支援結構化日誌和 Cloud Logging
|
|
113
|
+
|
|
114
|
+
這是 logging.Logger 的核心方法,所有日誌方法最終都會調用這個方法。
|
|
115
|
+
"""
|
|
116
|
+
# --- Find the correct caller frame outside this logger class --- #
|
|
117
|
+
f = inspect.currentframe()
|
|
118
|
+
# Default stacklevel for logging methods is 1. We need to go up further.
|
|
119
|
+
# Levels: _log -> std library caller (e.g., info) -> actual user call
|
|
120
|
+
frames_to_go_up = stacklevel + 1
|
|
121
|
+
for _ in range(frames_to_go_up):
|
|
122
|
+
if f is None:
|
|
123
|
+
break
|
|
124
|
+
f = f.f_back
|
|
125
|
+
|
|
126
|
+
if f:
|
|
127
|
+
caller_file = Path(f.f_code.co_filename).name
|
|
128
|
+
caller_func = f.f_code.co_name
|
|
129
|
+
caller_line = f.f_lineno
|
|
130
|
+
prefix = f"[{caller_file}:{caller_func}:{caller_line}] "
|
|
131
|
+
else:
|
|
132
|
+
caller_file = "unknown"
|
|
133
|
+
caller_func = "unknown"
|
|
134
|
+
caller_line = 0
|
|
135
|
+
prefix = ""
|
|
136
|
+
|
|
137
|
+
# --- Prepare data for Cloud Logging --- #
|
|
138
|
+
log_data_for_cloud = {
|
|
139
|
+
"message": f"{prefix}{msg % args if args else msg}", # Format message for cloud
|
|
140
|
+
"timestamp": datetime.now().isoformat(),
|
|
141
|
+
"environment": os.getenv("ENV_NAME", "botrun-flow-lang-dev"),
|
|
142
|
+
"application": "botrun_flow_lang",
|
|
143
|
+
"file": caller_file,
|
|
144
|
+
"function": caller_func,
|
|
145
|
+
"line": caller_line,
|
|
146
|
+
"session_id": self.session_id,
|
|
147
|
+
"user_id": self.user_id,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Handle exception info for Cloud Logging
|
|
151
|
+
computed_exc_info = None
|
|
152
|
+
if exc_info:
|
|
153
|
+
if isinstance(exc_info, BaseException):
|
|
154
|
+
computed_exc_info = (type(exc_info), exc_info, exc_info.__traceback__)
|
|
155
|
+
elif not isinstance(exc_info, tuple):
|
|
156
|
+
computed_exc_info = sys.exc_info()
|
|
157
|
+
|
|
158
|
+
if computed_exc_info and computed_exc_info[0] is not None:
|
|
159
|
+
log_data_for_cloud["exception"] = {
|
|
160
|
+
"type": computed_exc_info[0].__name__,
|
|
161
|
+
"message": str(computed_exc_info[1]),
|
|
162
|
+
"traceback": "".join(
|
|
163
|
+
traceback.format_exception(*computed_exc_info)
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
else:
|
|
167
|
+
computed_exc_info = None # Ensure it's None if exc_info was false
|
|
168
|
+
|
|
169
|
+
# Add extra kwargs passed directly to the log call for Cloud Logging
|
|
170
|
+
if kwargs:
|
|
171
|
+
log_data_for_cloud.update(kwargs)
|
|
172
|
+
|
|
173
|
+
# --- Prepare data for standard logging handlers (like console) --- #
|
|
174
|
+
# Create the 'extra' dict to pass correct caller info to handlers
|
|
175
|
+
handler_extra = extra or {}
|
|
176
|
+
handler_extra["caller_filename"] = caller_file
|
|
177
|
+
handler_extra["caller_funcName"] = caller_func
|
|
178
|
+
handler_extra["caller_lineno"] = caller_line
|
|
179
|
+
|
|
180
|
+
# --- Call parent _log for standard handlers --- #
|
|
181
|
+
# Pass the original msg and args so Formatter works correctly
|
|
182
|
+
# Pass the computed exception info
|
|
183
|
+
# Pass the enriched 'extra' dictionary
|
|
184
|
+
# Pass stack_info if provided
|
|
185
|
+
# stacklevel=1 here tells the *parent* _log it doesn't need to look further up
|
|
186
|
+
super()._log(
|
|
187
|
+
level,
|
|
188
|
+
msg,
|
|
189
|
+
args,
|
|
190
|
+
exc_info=computed_exc_info,
|
|
191
|
+
extra=handler_extra,
|
|
192
|
+
stack_info=stack_info,
|
|
193
|
+
stacklevel=1,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# --- Send to Cloud Logging if enabled --- #
|
|
197
|
+
if self.use_cloud_logging and self.isEnabledFor(level):
|
|
198
|
+
try:
|
|
199
|
+
severity = logging.getLevelName(level)
|
|
200
|
+
self.cloud_logger.log_struct(
|
|
201
|
+
log_data_for_cloud, # Use the prepared cloud data
|
|
202
|
+
severity=severity,
|
|
203
|
+
resource=Resource(
|
|
204
|
+
type=self.resource_type, labels=self.resource_labels
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
# Avoid Cloud Logging failure crashing the app
|
|
209
|
+
# Log the failure using the standard handler
|
|
210
|
+
|
|
211
|
+
traceback.print_exc()
|
|
212
|
+
print(f"Cloud Logging failed: {e}")
|
|
213
|
+
# self.handle(
|
|
214
|
+
# self.makeRecord(
|
|
215
|
+
# self.name,
|
|
216
|
+
# logging.ERROR,
|
|
217
|
+
# "(unknown file)",
|
|
218
|
+
# 0,
|
|
219
|
+
# f"Cloud Logging failed: {e}",
|
|
220
|
+
# (),
|
|
221
|
+
# None,
|
|
222
|
+
# func="_log",
|
|
223
|
+
# )
|
|
224
|
+
# )
|
|
225
|
+
|
|
226
|
+
def _initialize_client(
|
|
227
|
+
self, project_id: Optional[str], service_account_file: Optional[str]
|
|
228
|
+
) -> Optional[cloud_logging.Client]:
|
|
229
|
+
"""初始化 Cloud Logging 客戶端
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
project_id: GCP 項目 ID
|
|
233
|
+
service_account_file: 服務帳戶密鑰文件路徑
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Optional[cloud_logging.Client]: 已配置的日誌客戶端,若初始化失敗則為 None
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
# 使用服務帳戶認證
|
|
240
|
+
if service_account_file and os.path.exists(service_account_file):
|
|
241
|
+
credentials = service_account.Credentials.from_service_account_file(
|
|
242
|
+
service_account_file
|
|
243
|
+
)
|
|
244
|
+
return cloud_logging.Client(project=project_id, credentials=credentials)
|
|
245
|
+
# 使用應用默認憑證
|
|
246
|
+
return cloud_logging.Client(project=project_id)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
# 如果初始化失敗,打印錯誤並回退到標準輸出
|
|
249
|
+
print(f"⚠️ 無法初始化 Cloud Logging 客戶端: {str(e)}")
|
|
250
|
+
print(f"將使用標準輸出記錄日誌。錯誤詳情: {traceback.format_exc()}")
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
def _get_resource_labels(self) -> Dict[str, str]:
|
|
254
|
+
"""獲取 Cloud Run 資源標籤
|
|
255
|
+
|
|
256
|
+
從環境變數中獲取 Cloud Run 相關信息,用於資源標籤
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Dict[str, str]: 資源標籤字典
|
|
260
|
+
"""
|
|
261
|
+
return {}
|
|
262
|
+
|
|
263
|
+
# 為了保持與原有代碼的相容性,保留這些方法,但現在它們直接調用父類的方法
|
|
264
|
+
# These methods are now redundant because the base class methods will call our overridden _log method.
|
|
265
|
+
# Remove debug, info, warning, error, critical methods
|
|
266
|
+
# def debug(self, msg: str, *args, **kwargs) -> None:
|
|
267
|
+
# """記錄 DEBUG 級別的日誌"""
|
|
268
|
+
# super().debug(msg, *args, **kwargs)
|
|
269
|
+
#
|
|
270
|
+
# def info(self, msg: str, *args, **kwargs) -> None:
|
|
271
|
+
# """記錄 INFO 級別的日誌"""
|
|
272
|
+
# super().info(msg, *args, **kwargs)
|
|
273
|
+
#
|
|
274
|
+
# def warning(self, msg: str, *args, **kwargs) -> None:
|
|
275
|
+
# """記錄 WARNING 級別的日誌"""
|
|
276
|
+
# super().warning(msg, *args, **kwargs)
|
|
277
|
+
#
|
|
278
|
+
# def error(self, msg: str, *args, exc_info: bool = True, **kwargs) -> None:
|
|
279
|
+
# """記錄 ERROR 級別的日誌"""
|
|
280
|
+
# super().error(msg, *args, exc_info=exc_info, **kwargs)
|
|
281
|
+
#
|
|
282
|
+
# def critical(self, msg: str, *args, **kwargs) -> None:
|
|
283
|
+
# """記錄 CRITICAL 級別的日誌"""
|
|
284
|
+
# super().critical(msg, *args, **kwargs)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# --- Default Standard Logger --- #
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _create_default_logger() -> logging.Logger:
|
|
291
|
+
"""Creates a default standard Python logger that streams to console."""
|
|
292
|
+
logger = logging.getLogger("botrun.default")
|
|
293
|
+
logger.setLevel(logging.INFO) # Set default level
|
|
294
|
+
|
|
295
|
+
# Avoid adding duplicate handlers if this is called multiple times
|
|
296
|
+
if not logger.handlers:
|
|
297
|
+
handler = logging.StreamHandler()
|
|
298
|
+
# Use format similar to BotrunLogger's prefix
|
|
299
|
+
formatter = logging.Formatter(
|
|
300
|
+
"[%(levelname)s][%(filename)s:%(funcName)s:%(lineno)d] %(message)s"
|
|
301
|
+
)
|
|
302
|
+
handler.setFormatter(formatter)
|
|
303
|
+
logger.addHandler(handler)
|
|
304
|
+
|
|
305
|
+
return logger
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# 建立一個預設的標準 logger 實例
|
|
309
|
+
# This logger only prints to console and does NOT send to Cloud Logging.
|
|
310
|
+
# Use BotrunLogger explicitly when Cloud Logging features are needed.
|
|
311
|
+
default_logger = _create_default_logger()
|
|
312
|
+
|
|
313
|
+
# 建立一個預設的 BotrunLogger 全域實例
|
|
314
|
+
# 大部分模組可以重用這個實例,避免重複初始化
|
|
315
|
+
_default_botrun_logger = None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_default_botrun_logger() -> BotrunLogger:
|
|
319
|
+
"""
|
|
320
|
+
獲取預設的 BotrunLogger 實例,只會初始化一次
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
BotrunLogger: 預設的 BotrunLogger 實例
|
|
324
|
+
"""
|
|
325
|
+
global _default_botrun_logger
|
|
326
|
+
if _default_botrun_logger is None:
|
|
327
|
+
_default_botrun_logger = BotrunLogger()
|
|
328
|
+
return _default_botrun_logger
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def get_session_botrun_logger(
|
|
332
|
+
session_id: str = None, user_id: str = None
|
|
333
|
+
) -> BotrunLogger:
|
|
334
|
+
"""
|
|
335
|
+
獲取帶有 session 和 user 信息的 BotrunLogger 實例
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
session_id: 會話 ID
|
|
339
|
+
user_id: 用戶 ID
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
BotrunLogger: 帶有特定 session/user 信息的 BotrunLogger 實例
|
|
343
|
+
"""
|
|
344
|
+
return BotrunLogger(session_id=session_id, user_id=user_id)
|