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.
Files changed (87) hide show
  1. botrun_flow_lang/api/auth_api.py +39 -39
  2. botrun_flow_lang/api/auth_utils.py +183 -183
  3. botrun_flow_lang/api/botrun_back_api.py +65 -65
  4. botrun_flow_lang/api/flow_api.py +3 -3
  5. botrun_flow_lang/api/hatch_api.py +508 -508
  6. botrun_flow_lang/api/langgraph_api.py +811 -811
  7. botrun_flow_lang/api/line_bot_api.py +1484 -1484
  8. botrun_flow_lang/api/model_api.py +300 -300
  9. botrun_flow_lang/api/rate_limit_api.py +32 -32
  10. botrun_flow_lang/api/routes.py +79 -79
  11. botrun_flow_lang/api/search_api.py +53 -53
  12. botrun_flow_lang/api/storage_api.py +395 -395
  13. botrun_flow_lang/api/subsidy_api.py +290 -290
  14. botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
  15. botrun_flow_lang/api/user_setting_api.py +70 -70
  16. botrun_flow_lang/api/version_api.py +31 -31
  17. botrun_flow_lang/api/youtube_api.py +26 -26
  18. botrun_flow_lang/constants.py +13 -13
  19. botrun_flow_lang/langgraph_agents/agents/agent_runner.py +178 -178
  20. botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
  21. botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
  22. botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
  23. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +460 -460
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
  25. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  26. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +723 -723
  27. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  28. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  29. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  30. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  31. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  32. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  33. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
  34. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  35. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  36. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +486 -486
  37. botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
  38. botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
  39. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  40. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  41. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  42. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  43. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  44. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  45. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  46. botrun_flow_lang/log/.gitignore +2 -2
  47. botrun_flow_lang/main.py +61 -61
  48. botrun_flow_lang/main_fast.py +51 -51
  49. botrun_flow_lang/mcp_server/__init__.py +10 -10
  50. botrun_flow_lang/mcp_server/default_mcp.py +744 -744
  51. botrun_flow_lang/models/nodes/utils.py +205 -205
  52. botrun_flow_lang/models/token_usage.py +34 -34
  53. botrun_flow_lang/requirements.txt +21 -21
  54. botrun_flow_lang/services/base/firestore_base.py +30 -30
  55. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  56. botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
  57. botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
  58. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  59. botrun_flow_lang/services/storage/storage_store.py +65 -65
  60. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  61. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  62. botrun_flow_lang/static/docs/tools/index.html +926 -926
  63. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  64. botrun_flow_lang/tests/api_stress_test.py +357 -357
  65. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  66. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  67. botrun_flow_lang/tests/test_html_util.py +31 -31
  68. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  69. botrun_flow_lang/tests/test_img_util.py +39 -39
  70. botrun_flow_lang/tests/test_local_files.py +114 -114
  71. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  72. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  73. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  74. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  75. botrun_flow_lang/tools/generate_docs.py +133 -133
  76. botrun_flow_lang/tools/templates/tools.html +153 -153
  77. botrun_flow_lang/utils/__init__.py +7 -7
  78. botrun_flow_lang/utils/botrun_logger.py +344 -344
  79. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  80. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  81. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  82. botrun_flow_lang/utils/langchain_utils.py +324 -324
  83. botrun_flow_lang/utils/yaml_utils.py +9 -9
  84. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/METADATA +1 -1
  85. botrun_flow_lang-5.12.264.dist-info/RECORD +102 -0
  86. botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
  87. {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)