botrun-flow-lang 5.12.263__py3-none-any.whl → 6.2.21__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 (89) 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 +816 -811
  7. botrun_flow_lang/api/langgraph_constants.py +11 -0
  8. botrun_flow_lang/api/line_bot_api.py +1484 -1484
  9. botrun_flow_lang/api/model_api.py +300 -300
  10. botrun_flow_lang/api/rate_limit_api.py +32 -32
  11. botrun_flow_lang/api/routes.py +79 -79
  12. botrun_flow_lang/api/search_api.py +53 -53
  13. botrun_flow_lang/api/storage_api.py +395 -395
  14. botrun_flow_lang/api/subsidy_api.py +290 -290
  15. botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
  16. botrun_flow_lang/api/user_setting_api.py +70 -70
  17. botrun_flow_lang/api/version_api.py +31 -31
  18. botrun_flow_lang/api/youtube_api.py +26 -26
  19. botrun_flow_lang/constants.py +13 -13
  20. botrun_flow_lang/langgraph_agents/agents/agent_runner.py +178 -178
  21. botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
  22. botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
  23. botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +460 -460
  25. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
  26. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  27. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +730 -723
  28. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  29. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  30. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  31. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  32. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  33. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +336 -294
  34. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
  35. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  36. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  37. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +562 -486
  38. botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
  39. botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
  40. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  41. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  42. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  43. botrun_flow_lang/langgraph_agents/agents/util/usage_metadata.py +34 -0
  44. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  45. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  46. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  47. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  48. botrun_flow_lang/log/.gitignore +2 -2
  49. botrun_flow_lang/main.py +61 -61
  50. botrun_flow_lang/main_fast.py +51 -51
  51. botrun_flow_lang/mcp_server/__init__.py +10 -10
  52. botrun_flow_lang/mcp_server/default_mcp.py +854 -744
  53. botrun_flow_lang/models/nodes/utils.py +205 -205
  54. botrun_flow_lang/models/token_usage.py +34 -34
  55. botrun_flow_lang/requirements.txt +21 -21
  56. botrun_flow_lang/services/base/firestore_base.py +30 -30
  57. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  58. botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
  59. botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
  60. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  61. botrun_flow_lang/services/storage/storage_store.py +65 -65
  62. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  63. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  64. botrun_flow_lang/static/docs/tools/index.html +926 -926
  65. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  66. botrun_flow_lang/tests/api_stress_test.py +357 -357
  67. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  68. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  69. botrun_flow_lang/tests/test_html_util.py +31 -31
  70. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  71. botrun_flow_lang/tests/test_img_util.py +39 -39
  72. botrun_flow_lang/tests/test_local_files.py +114 -114
  73. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  74. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  75. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  76. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  77. botrun_flow_lang/tools/generate_docs.py +133 -133
  78. botrun_flow_lang/tools/templates/tools.html +153 -153
  79. botrun_flow_lang/utils/__init__.py +7 -7
  80. botrun_flow_lang/utils/botrun_logger.py +344 -344
  81. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  82. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  83. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  84. botrun_flow_lang/utils/langchain_utils.py +324 -324
  85. botrun_flow_lang/utils/yaml_utils.py +9 -9
  86. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-6.2.21.dist-info}/METADATA +6 -6
  87. botrun_flow_lang-6.2.21.dist-info/RECORD +104 -0
  88. botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
  89. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-6.2.21.dist-info}/WHEEL +0 -0
@@ -1,419 +1,419 @@
1
- import requests
2
- import mimetypes
3
- import time
4
- import random
5
- import os
6
- from tempfile import NamedTemporaryFile
7
- from pathlib import Path
8
- from io import BytesIO
9
- from .img_util import get_img_content_type
10
- from botrun_flow_lang.services.storage.storage_factory import storage_store_factory
11
-
12
-
13
- def get_file_content_type(file_path: str) -> str:
14
- """根據檔案類型取得對應的 MIME type
15
- 如果是圖片檔案,會使用 get_img_content_type 來檢測實際的圖片格式
16
- 如果是其他檔案,則使用副檔名來判斷
17
-
18
- Args:
19
- file_path: 檔案路徑
20
-
21
- Returns:
22
- str: MIME type,如果無法判斷則返回 application/octet-stream
23
- """
24
- # 先用副檔名判斷是否為圖片
25
- content_type, _ = mimetypes.guess_type(file_path)
26
-
27
- # 如果副檔名顯示是圖片,使用 get_img_content_type 進行實際檢測
28
- if content_type and content_type.startswith("image/"):
29
- try:
30
- return get_img_content_type(file_path)
31
- except (ValueError, FileNotFoundError):
32
- # 如果圖片檢測失敗,回傳原本用副檔名判斷的結果
33
- return content_type
34
-
35
- # 非圖片檔案或無法判斷類型,回傳原本的結果
36
- return content_type or "application/octet-stream"
37
-
38
-
39
- def upload_and_get_tmp_public_url(
40
- file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
41
- ) -> str:
42
- """上傳檔案到 GCS,並取得公開存取的 URL
43
- 如果是圖片檔案,會根據實際的圖片格式來調整檔案副檔名
44
-
45
- Args:
46
- file_path: 本地檔案路徑
47
- botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
48
- user_id: 使用者 ID
49
-
50
- Returns:
51
- str: 上傳後的公開存取 URL
52
- """
53
- # 外層 try-except: 處理第一次嘗試與重試邏輯
54
- try:
55
- return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
56
- except Exception as e:
57
- import traceback
58
-
59
- # 第一次嘗試失敗,記錄錯誤但不立即返回
60
- print(f"First attempt failed: {str(e)}")
61
- traceback.print_exc()
62
-
63
- # 隨機等待 7-15 秒
64
- retry_delay = random.randint(7, 20)
65
- print(f"Retrying in {retry_delay} seconds...")
66
- time.sleep(retry_delay)
67
-
68
- # 第二次嘗試
69
- try:
70
- print("Retry attempt...")
71
- return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
72
- except Exception as retry_e:
73
- # 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
74
- print(f"Retry attempt failed: {str(retry_e)}")
75
- traceback.print_exc()
76
- return "Error uploading file"
77
-
78
-
79
- def _perform_tmp_file_upload(
80
- file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
81
- ) -> str:
82
- """執行實際的上傳操作
83
-
84
- Args:
85
- file_path: 本地檔案路徑
86
- botrun_flow_lang_url: botrun flow lang API 的 URL
87
- user_id: 使用者 ID
88
-
89
- Returns:
90
- str: 上傳後的公開存取 URL
91
- """
92
- try:
93
- # 如果沒有提供 API URL,使用預設值
94
- if not botrun_flow_lang_url or not user_id:
95
- raise ValueError("botrun_flow_lang_url and user_id are required")
96
-
97
- # 取得檔案的 MIME type
98
- content_type = get_file_content_type(file_path)
99
-
100
- # 從檔案路徑取得檔案名稱
101
- file_name = Path(file_path).name
102
-
103
- # 如果是圖片檔案,根據實際的 content type 調整副檔名
104
- if content_type.startswith("image/"):
105
- # 取得檔案名稱(不含副檔名)
106
- name_without_ext = Path(file_name).stem
107
- # 根據 content type 決定新的副檔名
108
- ext_map = {
109
- "image/jpeg": ".jpg",
110
- "image/png": ".png",
111
- "image/gif": ".gif",
112
- "image/webp": ".webp",
113
- }
114
- new_ext = ext_map.get(content_type, Path(file_name).suffix)
115
- file_name = f"{name_without_ext}{new_ext}"
116
-
117
- # 準備 API endpoint
118
- url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
119
-
120
- # 準備檔案
121
- files = {
122
- "file": (file_name, open(file_path, "rb"), content_type),
123
- "file_name": (None, file_name),
124
- "content_type": (None, content_type),
125
- }
126
-
127
- # 發送請求
128
- response = requests.post(url, files=files)
129
- response.raise_for_status() # 如果請求失敗會拋出異常
130
-
131
- # 從回應中取得 URL
132
- result = response.json()
133
- return result.get("url", "")
134
-
135
- except Exception as e:
136
- # 這裡把異常往上拋,讓外層的重試邏輯處理
137
- raise e
138
-
139
-
140
- def _upload_text_file_with_utf8(
141
- file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
142
- ) -> str:
143
- """上傳文字檔案到 GCS,並設定正確的 UTF-8 charset
144
-
145
- Args:
146
- file_path: 本地檔案路徑
147
- botrun_flow_lang_url: botrun flow lang API 的 URL
148
- user_id: 使用者 ID
149
-
150
- Returns:
151
- str: 上傳後的公開存取 URL
152
- """
153
- try:
154
- # 檢查必要參數
155
- if not botrun_flow_lang_url or not user_id:
156
- raise ValueError("botrun_flow_lang_url and user_id are required")
157
-
158
- # 設定正確的 MIME type with UTF-8 charset
159
- content_type = "text/plain; charset=utf-8"
160
-
161
- # 從檔案路徑取得檔案名稱
162
- file_name = Path(file_path).name
163
-
164
- # 準備 API endpoint
165
- url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
166
-
167
- # 準備檔案
168
- files = {
169
- "file": (file_name, open(file_path, "rb"), content_type),
170
- "file_name": (None, file_name),
171
- "content_type": (None, content_type),
172
- "public": (None, False),
173
- }
174
-
175
- # 發送請求
176
- response = requests.post(url, files=files)
177
- response.raise_for_status() # 如果請求失敗會拋出異常
178
-
179
- # 從回應中取得 URL
180
- result = response.json()
181
- return result.get("url", "")
182
-
183
- except Exception as e:
184
- # 這裡把異常往上拋,讓外層的重試邏輯處理
185
- raise e
186
-
187
-
188
- async def generate_tmp_text_file(text_content: str) -> str:
189
- """
190
- Generate a temporary text file from content and upload it to GCS.
191
-
192
- Args:
193
- text_content: Text content to write to the file
194
- user_id: User ID for file upload
195
-
196
- Returns:
197
- str: Storage path for the text file or error message starting with "Error: "
198
- """
199
- try:
200
- # Generate a unique filename
201
- import uuid
202
-
203
- file_name = f"tmp_{uuid.uuid4().hex[:8]}.txt"
204
-
205
- # Create file object from text content
206
- file_content_bytes = text_content.encode("utf-8")
207
- file_object = BytesIO(file_content_bytes)
208
-
209
- # Set content type
210
- content_type = "text/plain; charset=utf-8"
211
-
212
- # Build storage path
213
- storage_path = f"tmp/{file_name}"
214
-
215
- # Store file to GCS
216
- storage = storage_store_factory()
217
- success, _ = await storage.store_file(
218
- storage_path, file_object, public=False, content_type=content_type
219
- )
220
-
221
- if not success:
222
- return "Error: Failed to store file"
223
-
224
- return storage_path
225
-
226
- except Exception as e:
227
- return f"Error: {str(e)}"
228
-
229
-
230
- async def read_tmp_text_file(storage_path: str) -> str:
231
- """
232
- Read a temporary text file from GCS storage.
233
-
234
- Args:
235
- storage_path: Storage path of the file in GCS (e.g., "tmp/user_id/filename.txt")
236
-
237
- Returns:
238
- str: Text content of the file or error message starting with "Error: "
239
- """
240
- try:
241
- storage = storage_store_factory()
242
- file_object = await storage.retrieve_file(storage_path)
243
-
244
- if not file_object:
245
- return "Error: File not found"
246
-
247
- # Decode the file content as UTF-8 text
248
- text_content = file_object.getvalue().decode("utf-8")
249
- return text_content
250
-
251
- except Exception as e:
252
- return f"Error: {str(e)}"
253
-
254
-
255
- async def upload_html_and_get_public_url(
256
- file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
257
- ) -> str:
258
- """上傳 HTML 檔案到 GCS,並取得公開存取的 URL
259
- 此函數僅接受 .html 檔案
260
-
261
- Args:
262
- file_path: 本地 HTML 檔案路徑
263
- botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
264
- user_id: 使用者 ID
265
-
266
- Returns:
267
- str: 上傳後的公開存取 URL
268
- """
269
- # 檢查檔案是否為 HTML 檔案
270
- if not file_path.lower().endswith(".html"):
271
- raise ValueError("Only HTML files are allowed")
272
-
273
- # 外層 try-except: 處理第一次嘗試與重試邏輯
274
- try:
275
- return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
276
- except Exception as e:
277
- import traceback
278
-
279
- # 第一次嘗試失敗,記錄錯誤但不立即返回
280
- print(f"First attempt failed: {str(e)}")
281
- traceback.print_exc()
282
-
283
- # 隨機等待 7-15 秒
284
- retry_delay = random.randint(7, 20)
285
- print(f"Retrying in {retry_delay} seconds...")
286
- time.sleep(retry_delay)
287
-
288
- # 第二次嘗試
289
- try:
290
- print("Retry attempt...")
291
- return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
292
- except Exception as retry_e:
293
- # 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
294
- print(f"Retry attempt failed: {str(retry_e)}")
295
- traceback.print_exc()
296
- return "Error uploading HTML file"
297
-
298
-
299
- async def _perform_html_upload(
300
- file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
301
- ) -> str:
302
- """執行 HTML 檔案的上傳操作
303
-
304
- Args:
305
- file_path: 本地 HTML 檔案路徑
306
- botrun_flow_lang_url: botrun flow lang API 的 URL (不再使用,保留向後相容性)
307
- user_id: 使用者 ID
308
-
309
- Returns:
310
- str: 上傳後的公開存取 URL
311
- """
312
- try:
313
- # 檢查必要參數
314
- if not user_id:
315
- raise ValueError("user_id is required")
316
-
317
- # 檢查檔案是否為 HTML 檔案
318
- if not file_path.lower().endswith(".html"):
319
- raise ValueError("Only HTML files are allowed")
320
-
321
- # 取得檔案的 MIME type
322
- content_type = "text/html"
323
-
324
- # 從檔案路徑取得檔案名稱
325
- file_name = Path(file_path).name
326
-
327
- # 讀取檔案內容
328
- with open(file_path, "rb") as f:
329
- file_content = f.read()
330
-
331
- # 使用內部函數直接上傳,避免 HTTP 自我呼叫
332
- from botrun_flow_lang.api.storage_api import _upload_html_file_internal
333
-
334
- # 在同步函數中運行異步函數
335
- return await _upload_html_file_internal(
336
- user_id, file_content, file_name, content_type
337
- )
338
-
339
- except Exception as e:
340
- import traceback
341
-
342
- print(f"Error uploading HTML file: {str(e)}")
343
- traceback.print_exc()
344
- # 這裡把異常往上拋,讓外層的重試邏輯處理
345
- raise e
346
-
347
-
348
- async def download_image_from_url(image_url: str) -> tuple[BytesIO, str]:
349
- """
350
- 從 URL 下載圖片到記憶體
351
-
352
- Args:
353
- image_url: 圖片的 URL
354
-
355
- Returns:
356
- tuple[BytesIO, str]: (圖片內容, content_type)
357
-
358
- Raises:
359
- Exception: 下載失敗時拋出例外
360
- """
361
- try:
362
- import httpx
363
-
364
- async with httpx.AsyncClient(timeout=30.0) as client:
365
- response = await client.get(image_url, follow_redirects=True)
366
- response.raise_for_status()
367
-
368
- # 從 response headers 取得 content type
369
- content_type = response.headers.get("content-type", "image/png")
370
-
371
- # 將圖片內容存入 BytesIO
372
- image_data = BytesIO(response.content)
373
-
374
- return image_data, content_type
375
-
376
- except Exception as e:
377
- raise Exception(f"Failed to download image from URL: {str(e)}")
378
-
379
-
380
- async def upload_image_and_get_public_url(
381
- image_url: str, botrun_flow_lang_url: str = "", user_id: str = ""
382
- ) -> str:
383
- """
384
- 從 URL 下載圖片並上傳到 GCS /img 目錄,取得永久公開 URL
385
-
386
- Args:
387
- image_url: 圖片來源 URL(如 DALL-E 生成的臨時 URL)
388
- botrun_flow_lang_url: botrun flow lang API 的 URL
389
- user_id: 使用者 ID
390
-
391
- Returns:
392
- str: 上傳後的永久公開存取 URL
393
- """
394
- try:
395
- # 1. 從 URL 下載圖片
396
- image_data, content_type = await download_image_from_url(image_url)
397
-
398
- # 2. 生成唯一檔案名稱
399
- import uuid
400
- from datetime import datetime
401
-
402
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
403
- file_name = f"dalle_{timestamp}_{uuid.uuid4().hex[:8]}.png"
404
-
405
- # 3. 使用內部函數上傳
406
- from botrun_flow_lang.api.storage_api import _upload_img_file_internal
407
-
408
- public_url = await _upload_img_file_internal(
409
- user_id, image_data.getvalue(), file_name, content_type
410
- )
411
-
412
- return public_url
413
-
414
- except Exception as e:
415
- import traceback
416
-
417
- print(f"Error uploading image: {str(e)}")
418
- traceback.print_exc()
419
- raise e
1
+ import requests
2
+ import mimetypes
3
+ import time
4
+ import random
5
+ import os
6
+ from tempfile import NamedTemporaryFile
7
+ from pathlib import Path
8
+ from io import BytesIO
9
+ from .img_util import get_img_content_type
10
+ from botrun_flow_lang.services.storage.storage_factory import storage_store_factory
11
+
12
+
13
+ def get_file_content_type(file_path: str) -> str:
14
+ """根據檔案類型取得對應的 MIME type
15
+ 如果是圖片檔案,會使用 get_img_content_type 來檢測實際的圖片格式
16
+ 如果是其他檔案,則使用副檔名來判斷
17
+
18
+ Args:
19
+ file_path: 檔案路徑
20
+
21
+ Returns:
22
+ str: MIME type,如果無法判斷則返回 application/octet-stream
23
+ """
24
+ # 先用副檔名判斷是否為圖片
25
+ content_type, _ = mimetypes.guess_type(file_path)
26
+
27
+ # 如果副檔名顯示是圖片,使用 get_img_content_type 進行實際檢測
28
+ if content_type and content_type.startswith("image/"):
29
+ try:
30
+ return get_img_content_type(file_path)
31
+ except (ValueError, FileNotFoundError):
32
+ # 如果圖片檢測失敗,回傳原本用副檔名判斷的結果
33
+ return content_type
34
+
35
+ # 非圖片檔案或無法判斷類型,回傳原本的結果
36
+ return content_type or "application/octet-stream"
37
+
38
+
39
+ def upload_and_get_tmp_public_url(
40
+ file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
41
+ ) -> str:
42
+ """上傳檔案到 GCS,並取得公開存取的 URL
43
+ 如果是圖片檔案,會根據實際的圖片格式來調整檔案副檔名
44
+
45
+ Args:
46
+ file_path: 本地檔案路徑
47
+ botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
48
+ user_id: 使用者 ID
49
+
50
+ Returns:
51
+ str: 上傳後的公開存取 URL
52
+ """
53
+ # 外層 try-except: 處理第一次嘗試與重試邏輯
54
+ try:
55
+ return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
56
+ except Exception as e:
57
+ import traceback
58
+
59
+ # 第一次嘗試失敗,記錄錯誤但不立即返回
60
+ print(f"First attempt failed: {str(e)}")
61
+ traceback.print_exc()
62
+
63
+ # 隨機等待 7-15 秒
64
+ retry_delay = random.randint(7, 20)
65
+ print(f"Retrying in {retry_delay} seconds...")
66
+ time.sleep(retry_delay)
67
+
68
+ # 第二次嘗試
69
+ try:
70
+ print("Retry attempt...")
71
+ return _perform_tmp_file_upload(file_path, botrun_flow_lang_url, user_id)
72
+ except Exception as retry_e:
73
+ # 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
74
+ print(f"Retry attempt failed: {str(retry_e)}")
75
+ traceback.print_exc()
76
+ return "Error uploading file"
77
+
78
+
79
+ def _perform_tmp_file_upload(
80
+ file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
81
+ ) -> str:
82
+ """執行實際的上傳操作
83
+
84
+ Args:
85
+ file_path: 本地檔案路徑
86
+ botrun_flow_lang_url: botrun flow lang API 的 URL
87
+ user_id: 使用者 ID
88
+
89
+ Returns:
90
+ str: 上傳後的公開存取 URL
91
+ """
92
+ try:
93
+ # 如果沒有提供 API URL,使用預設值
94
+ if not botrun_flow_lang_url or not user_id:
95
+ raise ValueError("botrun_flow_lang_url and user_id are required")
96
+
97
+ # 取得檔案的 MIME type
98
+ content_type = get_file_content_type(file_path)
99
+
100
+ # 從檔案路徑取得檔案名稱
101
+ file_name = Path(file_path).name
102
+
103
+ # 如果是圖片檔案,根據實際的 content type 調整副檔名
104
+ if content_type.startswith("image/"):
105
+ # 取得檔案名稱(不含副檔名)
106
+ name_without_ext = Path(file_name).stem
107
+ # 根據 content type 決定新的副檔名
108
+ ext_map = {
109
+ "image/jpeg": ".jpg",
110
+ "image/png": ".png",
111
+ "image/gif": ".gif",
112
+ "image/webp": ".webp",
113
+ }
114
+ new_ext = ext_map.get(content_type, Path(file_name).suffix)
115
+ file_name = f"{name_without_ext}{new_ext}"
116
+
117
+ # 準備 API endpoint
118
+ url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
119
+
120
+ # 準備檔案
121
+ files = {
122
+ "file": (file_name, open(file_path, "rb"), content_type),
123
+ "file_name": (None, file_name),
124
+ "content_type": (None, content_type),
125
+ }
126
+
127
+ # 發送請求
128
+ response = requests.post(url, files=files)
129
+ response.raise_for_status() # 如果請求失敗會拋出異常
130
+
131
+ # 從回應中取得 URL
132
+ result = response.json()
133
+ return result.get("url", "")
134
+
135
+ except Exception as e:
136
+ # 這裡把異常往上拋,讓外層的重試邏輯處理
137
+ raise e
138
+
139
+
140
+ def _upload_text_file_with_utf8(
141
+ file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
142
+ ) -> str:
143
+ """上傳文字檔案到 GCS,並設定正確的 UTF-8 charset
144
+
145
+ Args:
146
+ file_path: 本地檔案路徑
147
+ botrun_flow_lang_url: botrun flow lang API 的 URL
148
+ user_id: 使用者 ID
149
+
150
+ Returns:
151
+ str: 上傳後的公開存取 URL
152
+ """
153
+ try:
154
+ # 檢查必要參數
155
+ if not botrun_flow_lang_url or not user_id:
156
+ raise ValueError("botrun_flow_lang_url and user_id are required")
157
+
158
+ # 設定正確的 MIME type with UTF-8 charset
159
+ content_type = "text/plain; charset=utf-8"
160
+
161
+ # 從檔案路徑取得檔案名稱
162
+ file_name = Path(file_path).name
163
+
164
+ # 準備 API endpoint
165
+ url = f"{botrun_flow_lang_url}/api/tmp-files/{user_id}"
166
+
167
+ # 準備檔案
168
+ files = {
169
+ "file": (file_name, open(file_path, "rb"), content_type),
170
+ "file_name": (None, file_name),
171
+ "content_type": (None, content_type),
172
+ "public": (None, False),
173
+ }
174
+
175
+ # 發送請求
176
+ response = requests.post(url, files=files)
177
+ response.raise_for_status() # 如果請求失敗會拋出異常
178
+
179
+ # 從回應中取得 URL
180
+ result = response.json()
181
+ return result.get("url", "")
182
+
183
+ except Exception as e:
184
+ # 這裡把異常往上拋,讓外層的重試邏輯處理
185
+ raise e
186
+
187
+
188
+ async def generate_tmp_text_file(text_content: str) -> str:
189
+ """
190
+ Generate a temporary text file from content and upload it to GCS.
191
+
192
+ Args:
193
+ text_content: Text content to write to the file
194
+ user_id: User ID for file upload
195
+
196
+ Returns:
197
+ str: Storage path for the text file or error message starting with "Error: "
198
+ """
199
+ try:
200
+ # Generate a unique filename
201
+ import uuid
202
+
203
+ file_name = f"tmp_{uuid.uuid4().hex[:8]}.txt"
204
+
205
+ # Create file object from text content
206
+ file_content_bytes = text_content.encode("utf-8")
207
+ file_object = BytesIO(file_content_bytes)
208
+
209
+ # Set content type
210
+ content_type = "text/plain; charset=utf-8"
211
+
212
+ # Build storage path
213
+ storage_path = f"tmp/{file_name}"
214
+
215
+ # Store file to GCS
216
+ storage = storage_store_factory()
217
+ success, _ = await storage.store_file(
218
+ storage_path, file_object, public=False, content_type=content_type
219
+ )
220
+
221
+ if not success:
222
+ return "Error: Failed to store file"
223
+
224
+ return storage_path
225
+
226
+ except Exception as e:
227
+ return f"Error: {str(e)}"
228
+
229
+
230
+ async def read_tmp_text_file(storage_path: str) -> str:
231
+ """
232
+ Read a temporary text file from GCS storage.
233
+
234
+ Args:
235
+ storage_path: Storage path of the file in GCS (e.g., "tmp/user_id/filename.txt")
236
+
237
+ Returns:
238
+ str: Text content of the file or error message starting with "Error: "
239
+ """
240
+ try:
241
+ storage = storage_store_factory()
242
+ file_object = await storage.retrieve_file(storage_path)
243
+
244
+ if not file_object:
245
+ return "Error: File not found"
246
+
247
+ # Decode the file content as UTF-8 text
248
+ text_content = file_object.getvalue().decode("utf-8")
249
+ return text_content
250
+
251
+ except Exception as e:
252
+ return f"Error: {str(e)}"
253
+
254
+
255
+ async def upload_html_and_get_public_url(
256
+ file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
257
+ ) -> str:
258
+ """上傳 HTML 檔案到 GCS,並取得公開存取的 URL
259
+ 此函數僅接受 .html 檔案
260
+
261
+ Args:
262
+ file_path: 本地 HTML 檔案路徑
263
+ botrun_flow_lang_url: botrun flow lang API 的 URL,預設為空字串
264
+ user_id: 使用者 ID
265
+
266
+ Returns:
267
+ str: 上傳後的公開存取 URL
268
+ """
269
+ # 檢查檔案是否為 HTML 檔案
270
+ if not file_path.lower().endswith(".html"):
271
+ raise ValueError("Only HTML files are allowed")
272
+
273
+ # 外層 try-except: 處理第一次嘗試與重試邏輯
274
+ try:
275
+ return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
276
+ except Exception as e:
277
+ import traceback
278
+
279
+ # 第一次嘗試失敗,記錄錯誤但不立即返回
280
+ print(f"First attempt failed: {str(e)}")
281
+ traceback.print_exc()
282
+
283
+ # 隨機等待 7-15 秒
284
+ retry_delay = random.randint(7, 20)
285
+ print(f"Retrying in {retry_delay} seconds...")
286
+ time.sleep(retry_delay)
287
+
288
+ # 第二次嘗試
289
+ try:
290
+ print("Retry attempt...")
291
+ return await _perform_html_upload(file_path, botrun_flow_lang_url, user_id)
292
+ except Exception as retry_e:
293
+ # 第二次嘗試也失敗,記錄錯誤並返回錯誤訊息
294
+ print(f"Retry attempt failed: {str(retry_e)}")
295
+ traceback.print_exc()
296
+ return "Error uploading HTML file"
297
+
298
+
299
+ async def _perform_html_upload(
300
+ file_path: str, botrun_flow_lang_url: str = "", user_id: str = ""
301
+ ) -> str:
302
+ """執行 HTML 檔案的上傳操作
303
+
304
+ Args:
305
+ file_path: 本地 HTML 檔案路徑
306
+ botrun_flow_lang_url: botrun flow lang API 的 URL (不再使用,保留向後相容性)
307
+ user_id: 使用者 ID
308
+
309
+ Returns:
310
+ str: 上傳後的公開存取 URL
311
+ """
312
+ try:
313
+ # 檢查必要參數
314
+ if not user_id:
315
+ raise ValueError("user_id is required")
316
+
317
+ # 檢查檔案是否為 HTML 檔案
318
+ if not file_path.lower().endswith(".html"):
319
+ raise ValueError("Only HTML files are allowed")
320
+
321
+ # 取得檔案的 MIME type
322
+ content_type = "text/html"
323
+
324
+ # 從檔案路徑取得檔案名稱
325
+ file_name = Path(file_path).name
326
+
327
+ # 讀取檔案內容
328
+ with open(file_path, "rb") as f:
329
+ file_content = f.read()
330
+
331
+ # 使用內部函數直接上傳,避免 HTTP 自我呼叫
332
+ from botrun_flow_lang.api.storage_api import _upload_html_file_internal
333
+
334
+ # 在同步函數中運行異步函數
335
+ return await _upload_html_file_internal(
336
+ user_id, file_content, file_name, content_type
337
+ )
338
+
339
+ except Exception as e:
340
+ import traceback
341
+
342
+ print(f"Error uploading HTML file: {str(e)}")
343
+ traceback.print_exc()
344
+ # 這裡把異常往上拋,讓外層的重試邏輯處理
345
+ raise e
346
+
347
+
348
+ async def download_image_from_url(image_url: str) -> tuple[BytesIO, str]:
349
+ """
350
+ 從 URL 下載圖片到記憶體
351
+
352
+ Args:
353
+ image_url: 圖片的 URL
354
+
355
+ Returns:
356
+ tuple[BytesIO, str]: (圖片內容, content_type)
357
+
358
+ Raises:
359
+ Exception: 下載失敗時拋出例外
360
+ """
361
+ try:
362
+ import httpx
363
+
364
+ async with httpx.AsyncClient(timeout=30.0) as client:
365
+ response = await client.get(image_url, follow_redirects=True)
366
+ response.raise_for_status()
367
+
368
+ # 從 response headers 取得 content type
369
+ content_type = response.headers.get("content-type", "image/png")
370
+
371
+ # 將圖片內容存入 BytesIO
372
+ image_data = BytesIO(response.content)
373
+
374
+ return image_data, content_type
375
+
376
+ except Exception as e:
377
+ raise Exception(f"Failed to download image from URL: {str(e)}")
378
+
379
+
380
+ async def upload_image_and_get_public_url(
381
+ image_url: str, botrun_flow_lang_url: str = "", user_id: str = ""
382
+ ) -> str:
383
+ """
384
+ 從 URL 下載圖片並上傳到 GCS /img 目錄,取得永久公開 URL
385
+
386
+ Args:
387
+ image_url: 圖片來源 URL(如 DALL-E 生成的臨時 URL)
388
+ botrun_flow_lang_url: botrun flow lang API 的 URL
389
+ user_id: 使用者 ID
390
+
391
+ Returns:
392
+ str: 上傳後的永久公開存取 URL
393
+ """
394
+ try:
395
+ # 1. 從 URL 下載圖片
396
+ image_data, content_type = await download_image_from_url(image_url)
397
+
398
+ # 2. 生成唯一檔案名稱
399
+ import uuid
400
+ from datetime import datetime
401
+
402
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
403
+ file_name = f"dalle_{timestamp}_{uuid.uuid4().hex[:8]}.png"
404
+
405
+ # 3. 使用內部函數上傳
406
+ from botrun_flow_lang.api.storage_api import _upload_img_file_internal
407
+
408
+ public_url = await _upload_img_file_internal(
409
+ user_id, image_data.getvalue(), file_name, content_type
410
+ )
411
+
412
+ return public_url
413
+
414
+ except Exception as e:
415
+ import traceback
416
+
417
+ print(f"Error uploading image: {str(e)}")
418
+ traceback.print_exc()
419
+ raise e