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,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-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}"
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}"