botrun-flow-lang 5.9.301__py3-none-any.whl → 5.10.82__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 (84) 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 +481 -481
  6. botrun_flow_lang/api/langgraph_api.py +796 -796
  7. botrun_flow_lang/api/line_bot_api.py +1357 -1357
  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 +316 -316
  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 +174 -174
  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/gov_researcher_2_graph.py +1002 -1002
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  25. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +548 -542
  26. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  27. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  28. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  29. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  30. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  31. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  32. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +345 -345
  33. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  34. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  35. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +160 -160
  36. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  37. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  38. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  39. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  40. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  41. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  42. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  43. botrun_flow_lang/log/.gitignore +2 -2
  44. botrun_flow_lang/main.py +61 -61
  45. botrun_flow_lang/main_fast.py +51 -51
  46. botrun_flow_lang/mcp_server/__init__.py +10 -10
  47. botrun_flow_lang/mcp_server/default_mcp.py +711 -711
  48. botrun_flow_lang/models/nodes/utils.py +205 -205
  49. botrun_flow_lang/models/token_usage.py +34 -34
  50. botrun_flow_lang/requirements.txt +21 -21
  51. botrun_flow_lang/services/base/firestore_base.py +30 -30
  52. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  53. botrun_flow_lang/services/hatch/hatch_fs_store.py +372 -372
  54. botrun_flow_lang/services/storage/storage_cs_store.py +202 -202
  55. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  56. botrun_flow_lang/services/storage/storage_store.py +65 -65
  57. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  58. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  59. botrun_flow_lang/static/docs/tools/index.html +926 -926
  60. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  61. botrun_flow_lang/tests/api_stress_test.py +357 -357
  62. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  63. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  64. botrun_flow_lang/tests/test_html_util.py +31 -31
  65. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  66. botrun_flow_lang/tests/test_img_util.py +39 -39
  67. botrun_flow_lang/tests/test_local_files.py +114 -114
  68. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  69. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  70. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  71. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  72. botrun_flow_lang/tools/generate_docs.py +133 -133
  73. botrun_flow_lang/tools/templates/tools.html +153 -153
  74. botrun_flow_lang/utils/__init__.py +7 -7
  75. botrun_flow_lang/utils/botrun_logger.py +344 -344
  76. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  77. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  78. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  79. botrun_flow_lang/utils/langchain_utils.py +324 -324
  80. botrun_flow_lang/utils/yaml_utils.py +9 -9
  81. {botrun_flow_lang-5.9.301.dist-info → botrun_flow_lang-5.10.82.dist-info}/METADATA +2 -2
  82. botrun_flow_lang-5.10.82.dist-info/RECORD +99 -0
  83. botrun_flow_lang-5.9.301.dist-info/RECORD +0 -99
  84. {botrun_flow_lang-5.9.301.dist-info → botrun_flow_lang-5.10.82.dist-info}/WHEEL +0 -0
@@ -1,294 +1,294 @@
1
- import anthropic
2
- import base64
3
- import httpx
4
- import os
5
- import imghdr
6
- from pathlib import Path
7
- from dotenv import load_dotenv
8
-
9
- load_dotenv()
10
-
11
-
12
- def get_img_content_type(file_path: str | Path) -> str:
13
- """
14
- Get the content type (MIME type) of a local file.
15
- This function checks the actual image format rather than relying on file extension.
16
-
17
- Args:
18
- file_path: Path to the local file (can be string or Path object)
19
-
20
- Returns:
21
- str: The content type of the file (e.g., 'image/jpeg', 'image/png')
22
-
23
- Raises:
24
- FileNotFoundError: If the file does not exist
25
- ValueError: If the file type is not recognized or not supported
26
- """
27
- if not os.path.exists(file_path):
28
- raise FileNotFoundError(f"File not found: {file_path}")
29
-
30
- # Check actual image type using imghdr
31
- img_type = imghdr.what(file_path)
32
- if not img_type:
33
- raise ValueError(f"File is not a recognized image format: {file_path}")
34
-
35
- # Map image type to MIME type
36
- mime_types = {
37
- "jpeg": "image/jpeg",
38
- "jpg": "image/jpeg",
39
- "png": "image/png",
40
- "gif": "image/gif",
41
- "webp": "image/webp",
42
- }
43
-
44
- content_type = mime_types.get(img_type.lower())
45
- if not content_type:
46
- raise ValueError(f"Unsupported image format '{img_type}': {file_path}")
47
-
48
- return content_type
49
-
50
-
51
- def analyze_imgs_with_claude(
52
- img_urls: list[str], user_input: str, model_name: str = "claude-sonnet-4-20250514"
53
- ) -> str:
54
- """
55
- Analyze multiple images using Claude Vision API
56
-
57
- Args:
58
- img_urls: List of URLs to the image files
59
- user_input: User's query about the image content(s)
60
- model_name: Claude model name to use
61
-
62
- Returns:
63
- str: Claude's analysis of the image content(s) based on the query
64
-
65
- Raises:
66
- ValueError: If image URLs are invalid or model parameters are incorrect
67
- anthropic.APIError: If there's an error with the Claude API
68
- Exception: For other errors during processing
69
- """
70
- # Initialize message content
71
- message_content = []
72
-
73
- # Download and encode each image file from URLs
74
- with httpx.Client(follow_redirects=True) as client:
75
- for img_url in img_urls:
76
- response = client.get(img_url)
77
- if response.status_code != 200:
78
- raise ValueError(f"Failed to download image from URL: {img_url}")
79
-
80
- # Detect content type from response headers
81
- content_type = response.headers.get("content-type", "")
82
- if not content_type.startswith("image/"):
83
- raise ValueError(f"URL does not point to a valid image: {img_url}")
84
-
85
- # Check file size (5MB limit for API)
86
- if len(response.content) > 5 * 1024 * 1024:
87
- raise ValueError(f"Image file size exceeds 5MB limit: {img_url}")
88
-
89
- # Encode image data
90
- img_data = base64.standard_b64encode(response.content).decode("utf-8")
91
-
92
- # Add image to message content
93
- message_content.append(
94
- {
95
- "type": "image",
96
- "source": {
97
- "type": "base64",
98
- "media_type": content_type,
99
- "data": img_data,
100
- },
101
- }
102
- )
103
-
104
- # Add user input text
105
- message_content.append({"type": "text", "text": user_input})
106
-
107
- # Initialize Anthropic client
108
- client = anthropic.Anthropic()
109
-
110
- try:
111
- # Send to Claude
112
- message = client.messages.create(
113
- model=model_name,
114
- max_tokens=1024,
115
- messages=[
116
- {
117
- "role": "user",
118
- "content": message_content,
119
- }
120
- ],
121
- )
122
-
123
- print(
124
- f"analyze_imgs_with_claude============> input_token: {message.usage.input_tokens} output_token: {message.usage.output_tokens}",
125
- )
126
- return message.content[0].text
127
- except anthropic.APIError as e:
128
- import traceback
129
-
130
- traceback.print_exc()
131
- raise anthropic.APIError(
132
- f"Claude API error with model {model_name}: {str(e)}"
133
- )
134
- except Exception as e:
135
- import traceback
136
-
137
- traceback.print_exc()
138
- raise Exception(
139
- f"Error analyzing image(s) with Claude {model_name}: {str(e)}"
140
- )
141
-
142
-
143
- def analyze_imgs_with_gemini(
144
- img_urls: list[str],
145
- user_input: str,
146
- model_name: str = "gemini-2.5-flash",
147
- ) -> str:
148
- """
149
- Analyze multiple images using Gemini Vision API
150
-
151
- Args:
152
- img_urls: List of URLs to the image files
153
- user_input: User's query about the image content(s)
154
- model_name: Gemini model name to use
155
-
156
- Returns:
157
- str: Gemini's analysis of the image content(s) based on the query
158
-
159
- Raises:
160
- ValueError: If image URLs are invalid or model parameters are incorrect
161
- Exception: For errors during API calls or other processing
162
- """
163
- # 放到要用的時候才 import,不然loading 會花時間
164
- from google import genai
165
- from google.genai.types import HttpOptions, Part
166
- from google.oauth2 import service_account
167
-
168
- # Initialize the Gemini client
169
- api_key = os.getenv("GEMINI_API_KEY", "")
170
- if not api_key:
171
- raise ValueError("GEMINI_API_KEY environment variable not set")
172
-
173
- # 設定 API 金鑰
174
-
175
- try:
176
- # 初始化模型並準備內容列表
177
- credentials = service_account.Credentials.from_service_account_file(
178
- os.getenv("GOOGLE_APPLICATION_CREDENTIALS_FOR_FASTAPI"),
179
- scopes=["https://www.googleapis.com/auth/cloud-platform"],
180
- )
181
-
182
- client = genai.Client(
183
- credentials=credentials,
184
- project="scoop-386004",
185
- location="us-central1",
186
- )
187
- contents = [user_input]
188
-
189
- # 下載並處理每個圖片
190
- with httpx.Client(follow_redirects=True) as http_client:
191
- for img_url in img_urls:
192
- response = http_client.get(img_url)
193
- if response.status_code != 200:
194
- raise ValueError(f"Failed to download image from URL: {img_url}")
195
-
196
- # 檢測內容類型
197
- content_type = response.headers.get("content-type", "")
198
- if not content_type.startswith("image/"):
199
- raise ValueError(f"URL does not point to a valid image: {img_url}")
200
-
201
- # 檢查檔案大小
202
- if len(response.content) > 20 * 1024 * 1024: # 20MB 限制
203
- raise ValueError(f"Image file size too large: {img_url}")
204
-
205
- # 將圖片添加到內容中
206
- contents.append(
207
- Part.from_bytes(
208
- data=response.content,
209
- mime_type=content_type,
210
- )
211
- )
212
-
213
- # 使用 genai 生成內容
214
- response = client.models.generate_content(
215
- model=model_name,
216
- contents=contents,
217
- )
218
-
219
- print(
220
- f"analyze_imgs_with_gemini============> input_token: {response.usage_metadata.prompt_token_count} output_token: {response.usage_metadata.candidates_token_count}"
221
- )
222
- return response.text
223
-
224
- except httpx.RequestError as e:
225
- import traceback
226
-
227
- traceback.print_exc()
228
- raise ValueError(f"Failed to download image(s): {str(e)}")
229
- except Exception as e:
230
- import traceback
231
-
232
- traceback.print_exc()
233
- raise Exception(f"Error analyzing image(s) with Gemini {model_name}: {str(e)}")
234
-
235
-
236
- def analyze_imgs(img_urls: list[str], user_input: str) -> str:
237
- """
238
- Analyze multiple images using configured AI models.
239
-
240
- Uses models specified in IMG_ANALYZER_MODEL environment variable.
241
- When multiple models are specified (comma-separated), tries them in order
242
- until one succeeds, falling back to next model if a model fails.
243
-
244
- Example: IMG_ANALYZER_MODEL=claude-3-7-sonnet-latest,gemini-2.0-flash
245
-
246
- Args:
247
- img_urls: List of URLs to the image files
248
- user_input: User's query about the image content(s)
249
-
250
- Returns:
251
- str: AI analysis of the image content(s) based on the query
252
- """
253
- # Get models from environment variable, split by comma if multiple models
254
- models_str = os.getenv("IMG_ANALYZER_MODEL", "gemini-2.5-flash")
255
- print(f"[analyze_imgs] 分析IMG使用模型: {models_str}")
256
- models = models_str.split(",")
257
-
258
- # Remove whitespace around model names
259
- models = [model.strip() for model in models]
260
- print(f"[analyze_imgs] 處理後模型列表: {models}")
261
-
262
- last_error = None
263
- errors = []
264
-
265
- # Try each model in sequence until one succeeds
266
- for model in models:
267
- try:
268
- if model.startswith("gemini-"):
269
- print(f"[analyze_imgs] 嘗試使用 Gemini 模型: {model}")
270
- result = analyze_imgs_with_gemini(img_urls, user_input, model)
271
- return result
272
- elif model.startswith("claude-"):
273
- print(f"[analyze_imgs] 嘗試使用 Claude 模型: {model}")
274
- result = analyze_imgs_with_claude(img_urls, user_input, model)
275
- return result
276
- else:
277
- print(f"[analyze_imgs] 不支持的模型格式: {model}, 跳過")
278
- errors.append(f"不支持的模型格式: {model}")
279
- continue
280
-
281
- except Exception as e:
282
- last_error = e
283
- error_msg = str(e)
284
- print(f"[analyze_imgs] 模型 {model} 失敗,錯誤: {error_msg}")
285
- import traceback
286
-
287
- traceback.print_exc()
288
- errors.append(f"模型 {model} 異常: {error_msg}")
289
- # Continue to the next model in the list
290
- continue
291
-
292
- # If we've tried all models and none succeeded, return all errors
293
- error_summary = "\n".join(errors)
294
- return f"錯誤: 所有配置的模型都失敗了。詳細錯誤:\n{error_summary}"
1
+ import anthropic
2
+ import base64
3
+ import httpx
4
+ import os
5
+ import imghdr
6
+ from pathlib import Path
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+
12
+ def get_img_content_type(file_path: str | Path) -> str:
13
+ """
14
+ Get the content type (MIME type) of a local file.
15
+ This function checks the actual image format rather than relying on file extension.
16
+
17
+ Args:
18
+ file_path: Path to the local file (can be string or Path object)
19
+
20
+ Returns:
21
+ str: The content type of the file (e.g., 'image/jpeg', 'image/png')
22
+
23
+ Raises:
24
+ FileNotFoundError: If the file does not exist
25
+ ValueError: If the file type is not recognized or not supported
26
+ """
27
+ if not os.path.exists(file_path):
28
+ raise FileNotFoundError(f"File not found: {file_path}")
29
+
30
+ # Check actual image type using imghdr
31
+ img_type = imghdr.what(file_path)
32
+ if not img_type:
33
+ raise ValueError(f"File is not a recognized image format: {file_path}")
34
+
35
+ # Map image type to MIME type
36
+ mime_types = {
37
+ "jpeg": "image/jpeg",
38
+ "jpg": "image/jpeg",
39
+ "png": "image/png",
40
+ "gif": "image/gif",
41
+ "webp": "image/webp",
42
+ }
43
+
44
+ content_type = mime_types.get(img_type.lower())
45
+ if not content_type:
46
+ raise ValueError(f"Unsupported image format '{img_type}': {file_path}")
47
+
48
+ return content_type
49
+
50
+
51
+ def analyze_imgs_with_claude(
52
+ img_urls: list[str], user_input: str, model_name: str = "claude-sonnet-4-5-20250929"
53
+ ) -> str:
54
+ """
55
+ Analyze multiple images using Claude Vision API
56
+
57
+ Args:
58
+ img_urls: List of URLs to the image files
59
+ user_input: User's query about the image content(s)
60
+ model_name: Claude model name to use
61
+
62
+ Returns:
63
+ str: Claude's analysis of the image content(s) based on the query
64
+
65
+ Raises:
66
+ ValueError: If image URLs are invalid or model parameters are incorrect
67
+ anthropic.APIError: If there's an error with the Claude API
68
+ Exception: For other errors during processing
69
+ """
70
+ # Initialize message content
71
+ message_content = []
72
+
73
+ # Download and encode each image file from URLs
74
+ with httpx.Client(follow_redirects=True) as client:
75
+ for img_url in img_urls:
76
+ response = client.get(img_url)
77
+ if response.status_code != 200:
78
+ raise ValueError(f"Failed to download image from URL: {img_url}")
79
+
80
+ # Detect content type from response headers
81
+ content_type = response.headers.get("content-type", "")
82
+ if not content_type.startswith("image/"):
83
+ raise ValueError(f"URL does not point to a valid image: {img_url}")
84
+
85
+ # Check file size (5MB limit for API)
86
+ if len(response.content) > 5 * 1024 * 1024:
87
+ raise ValueError(f"Image file size exceeds 5MB limit: {img_url}")
88
+
89
+ # Encode image data
90
+ img_data = base64.standard_b64encode(response.content).decode("utf-8")
91
+
92
+ # Add image to message content
93
+ message_content.append(
94
+ {
95
+ "type": "image",
96
+ "source": {
97
+ "type": "base64",
98
+ "media_type": content_type,
99
+ "data": img_data,
100
+ },
101
+ }
102
+ )
103
+
104
+ # Add user input text
105
+ message_content.append({"type": "text", "text": user_input})
106
+
107
+ # Initialize Anthropic client
108
+ client = anthropic.Anthropic()
109
+
110
+ try:
111
+ # Send to Claude
112
+ message = client.messages.create(
113
+ model=model_name,
114
+ max_tokens=1024,
115
+ messages=[
116
+ {
117
+ "role": "user",
118
+ "content": message_content,
119
+ }
120
+ ],
121
+ )
122
+
123
+ print(
124
+ f"analyze_imgs_with_claude============> input_token: {message.usage.input_tokens} output_token: {message.usage.output_tokens}",
125
+ )
126
+ return message.content[0].text
127
+ except anthropic.APIError as e:
128
+ import traceback
129
+
130
+ traceback.print_exc()
131
+ raise anthropic.APIError(
132
+ f"Claude API error with model {model_name}: {str(e)}"
133
+ )
134
+ except Exception as e:
135
+ import traceback
136
+
137
+ traceback.print_exc()
138
+ raise Exception(
139
+ f"Error analyzing image(s) with Claude {model_name}: {str(e)}"
140
+ )
141
+
142
+
143
+ def analyze_imgs_with_gemini(
144
+ img_urls: list[str],
145
+ user_input: str,
146
+ model_name: str = "gemini-2.5-flash",
147
+ ) -> str:
148
+ """
149
+ Analyze multiple images using Gemini Vision API
150
+
151
+ Args:
152
+ img_urls: List of URLs to the image files
153
+ user_input: User's query about the image content(s)
154
+ model_name: Gemini model name to use
155
+
156
+ Returns:
157
+ str: Gemini's analysis of the image content(s) based on the query
158
+
159
+ Raises:
160
+ ValueError: If image URLs are invalid or model parameters are incorrect
161
+ Exception: For errors during API calls or other processing
162
+ """
163
+ # 放到要用的時候才 import,不然loading 會花時間
164
+ from google import genai
165
+ from google.genai.types import HttpOptions, Part
166
+ from google.oauth2 import service_account
167
+
168
+ # Initialize the Gemini client
169
+ api_key = os.getenv("GEMINI_API_KEY", "")
170
+ if not api_key:
171
+ raise ValueError("GEMINI_API_KEY environment variable not set")
172
+
173
+ # 設定 API 金鑰
174
+
175
+ try:
176
+ # 初始化模型並準備內容列表
177
+ credentials = service_account.Credentials.from_service_account_file(
178
+ os.getenv("GOOGLE_APPLICATION_CREDENTIALS_FOR_FASTAPI"),
179
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
180
+ )
181
+
182
+ client = genai.Client(
183
+ credentials=credentials,
184
+ project="scoop-386004",
185
+ location="us-central1",
186
+ )
187
+ contents = [user_input]
188
+
189
+ # 下載並處理每個圖片
190
+ with httpx.Client(follow_redirects=True) as http_client:
191
+ for img_url in img_urls:
192
+ response = http_client.get(img_url)
193
+ if response.status_code != 200:
194
+ raise ValueError(f"Failed to download image from URL: {img_url}")
195
+
196
+ # 檢測內容類型
197
+ content_type = response.headers.get("content-type", "")
198
+ if not content_type.startswith("image/"):
199
+ raise ValueError(f"URL does not point to a valid image: {img_url}")
200
+
201
+ # 檢查檔案大小
202
+ if len(response.content) > 20 * 1024 * 1024: # 20MB 限制
203
+ raise ValueError(f"Image file size too large: {img_url}")
204
+
205
+ # 將圖片添加到內容中
206
+ contents.append(
207
+ Part.from_bytes(
208
+ data=response.content,
209
+ mime_type=content_type,
210
+ )
211
+ )
212
+
213
+ # 使用 genai 生成內容
214
+ response = client.models.generate_content(
215
+ model=model_name,
216
+ contents=contents,
217
+ )
218
+
219
+ print(
220
+ f"analyze_imgs_with_gemini============> input_token: {response.usage_metadata.prompt_token_count} output_token: {response.usage_metadata.candidates_token_count}"
221
+ )
222
+ return response.text
223
+
224
+ except httpx.RequestError as e:
225
+ import traceback
226
+
227
+ traceback.print_exc()
228
+ raise ValueError(f"Failed to download image(s): {str(e)}")
229
+ except Exception as e:
230
+ import traceback
231
+
232
+ traceback.print_exc()
233
+ raise Exception(f"Error analyzing image(s) with Gemini {model_name}: {str(e)}")
234
+
235
+
236
+ def analyze_imgs(img_urls: list[str], user_input: str) -> str:
237
+ """
238
+ Analyze multiple images using configured AI models.
239
+
240
+ Uses models specified in IMG_ANALYZER_MODEL environment variable.
241
+ When multiple models are specified (comma-separated), tries them in order
242
+ until one succeeds, falling back to next model if a model fails.
243
+
244
+ Example: IMG_ANALYZER_MODEL=claude-3-7-sonnet-latest,gemini-2.0-flash
245
+
246
+ Args:
247
+ img_urls: List of URLs to the image files
248
+ user_input: User's query about the image content(s)
249
+
250
+ Returns:
251
+ str: AI analysis of the image content(s) based on the query
252
+ """
253
+ # Get models from environment variable, split by comma if multiple models
254
+ models_str = os.getenv("IMG_ANALYZER_MODEL", "gemini-2.5-flash")
255
+ print(f"[analyze_imgs] 分析IMG使用模型: {models_str}")
256
+ models = models_str.split(",")
257
+
258
+ # Remove whitespace around model names
259
+ models = [model.strip() for model in models]
260
+ print(f"[analyze_imgs] 處理後模型列表: {models}")
261
+
262
+ last_error = None
263
+ errors = []
264
+
265
+ # Try each model in sequence until one succeeds
266
+ for model in models:
267
+ try:
268
+ if model.startswith("gemini-"):
269
+ print(f"[analyze_imgs] 嘗試使用 Gemini 模型: {model}")
270
+ result = analyze_imgs_with_gemini(img_urls, user_input, model)
271
+ return result
272
+ elif model.startswith("claude-"):
273
+ print(f"[analyze_imgs] 嘗試使用 Claude 模型: {model}")
274
+ result = analyze_imgs_with_claude(img_urls, user_input, model)
275
+ return result
276
+ else:
277
+ print(f"[analyze_imgs] 不支持的模型格式: {model}, 跳過")
278
+ errors.append(f"不支持的模型格式: {model}")
279
+ continue
280
+
281
+ except Exception as e:
282
+ last_error = e
283
+ error_msg = str(e)
284
+ print(f"[analyze_imgs] 模型 {model} 失敗,錯誤: {error_msg}")
285
+ import traceback
286
+
287
+ traceback.print_exc()
288
+ errors.append(f"模型 {model} 異常: {error_msg}")
289
+ # Continue to the next model in the list
290
+ continue
291
+
292
+ # If we've tried all models and none succeeded, return all errors
293
+ error_summary = "\n".join(errors)
294
+ return f"錯誤: 所有配置的模型都失敗了。詳細錯誤:\n{error_summary}"