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,723 +1,730 @@
1
- import os
2
- import asyncio
3
- import json
4
- from datetime import datetime
5
- from typing import ClassVar, Dict, List, Optional, Any
6
-
7
- from langchain_core.messages import SystemMessage
8
-
9
- from botrun_flow_lang.constants import LANG_EN, LANG_ZH_TW
10
-
11
- from langgraph.checkpoint.memory import MemorySaver
12
- from langchain_core.runnables import RunnableConfig
13
-
14
- from langchain_core.tools import BaseTool
15
-
16
- from langchain_core.tools import tool
17
-
18
- from botrun_flow_lang.utils.botrun_logger import get_default_botrun_logger
19
-
20
- # All tools now provided by MCP server - no local tool imports needed
21
-
22
- from botrun_flow_lang.langgraph_agents.agents.checkpointer.firestore_checkpointer import (
23
- AsyncFirestoreCheckpointer,
24
- )
25
-
26
- from langgraph.prebuilt import create_react_agent
27
-
28
- from dotenv import load_dotenv
29
-
30
- import copy # 用於深拷貝 schema,避免意外修改原始對象
31
-
32
- # Removed DALL-E and rate limiting imports - tools now provided by MCP server
33
-
34
- # =========
35
- # 📋 STAGE 4 REFACTORING COMPLETED (MCP Integration)
36
- #
37
- # This file has been refactored to integrate with MCP (Model Context Protocol):
38
- #
39
- # ✅ REMOVED (~600 lines):
40
- # - Language-specific system prompts (zh_tw_system_prompt, en_system_prompt)
41
- # - Local tool definitions: scrape, chat_with_pdf, chat_with_imgs, generate_image,
42
- # generate_tmp_public_url, create_html_page, compare_date_time
43
- # - Complex conditional logic (if botrun_flow_lang_url and user_id)
44
- # - Rate limiting exception and related imports
45
- # - Unused utility imports
46
- #
47
- # ✅ SIMPLIFIED:
48
- # - Direct system_prompt usage (no concatenation)
49
- # - Streamlined tools list (only language-specific tools)
50
- # - Clean MCP integration via mcp_config parameter
51
- # - Maintained backward compatibility for all parameters
52
- #
53
- # 🎯 RESULT:
54
- # - Reduced complexity while maintaining full functionality
55
- # - All tools available via MCP server at /mcp/default/mcp/
56
- # - Ready for Phase 2: language-specific tools migration
57
- # =========
58
-
59
- # 放到要用的時候才 init,不然loading 會花時間
60
- # 因為要讓 langgraph 在本地端執行,所以這一段又搬回到外面了
61
- from langchain_google_genai import ChatGoogleGenerativeAI
62
-
63
- # =========
64
- # 放到要用的時候才 import,不然loading 會花時間
65
- # 因為LangGraph 在本地端執行,所以這一段又搬回到外面了
66
- from botrun_flow_lang.langgraph_agents.agents.util.model_utils import (
67
- RotatingChatAnthropic,
68
- )
69
-
70
- # =========
71
- # 放到要用的時候才 init,不然loading 會花時間
72
- # 因為LangGraph 在本地端執行,所以這一段又搬回到外面了
73
- from langchain_openai import ChatOpenAI
74
-
75
- # =========
76
- # 放到要用的時候才 init,不然loading 會花時間
77
- # 因為LangGraph 在本地端執行,所以這一段又搬回到外面了
78
- from langchain_anthropic import ChatAnthropic
79
-
80
- # =========
81
-
82
- # 假設 MultiServerMCPClient 和 StructuredTool 已經被正確導入
83
- from langchain.tools import StructuredTool # 或 langchain_core.tools
84
- from langchain_mcp_adapters.client import MultiServerMCPClient
85
-
86
- # ========
87
- # for Vertex AI
88
- from google.oauth2 import service_account
89
- from langchain_google_vertexai import ChatVertexAI
90
- from langchain_google_vertexai.model_garden import ChatAnthropicVertex
91
-
92
- load_dotenv()
93
-
94
- # logger = default_logger
95
- logger = get_default_botrun_logger()
96
-
97
-
98
- # Removed BotrunRateLimitException - rate limiting now handled by MCP server
99
-
100
-
101
- # Load Anthropic API keys from environment
102
- # anthropic_api_keys_str = os.getenv("ANTHROPIC_API_KEYS", "")
103
- # anthropic_api_keys = [
104
- # key.strip() for key in anthropic_api_keys_str.split(",") if key.strip()
105
- # ]
106
-
107
- # Initialize the model with key rotation if multiple keys are available
108
- # if anthropic_api_keys:
109
- # model = RotatingChatAnthropic(
110
- # model_name="claude-3-7-sonnet-latest",
111
- # keys=anthropic_api_keys,
112
- # temperature=0,
113
- # max_tokens=8192,
114
- # )
115
- # 建立 AWS Session
116
- # session = boto3.Session(
117
- # aws_access_key_id="",
118
- # aws_secret_access_key="",
119
- # region_name="us-west-2",
120
- # )
121
-
122
-
123
- # # 使用該 Session 初始化 Bedrock 客戶端
124
- # bedrock_runtime = session.client(
125
- # service_name="bedrock-runtime",
126
- # )
127
- # model = ChatBedrockConverse(
128
- # model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
129
- # client=bedrock_runtime,
130
- # temperature=0,
131
- # max_tokens=8192,
132
- # )
133
- # else:
134
- # Fallback to traditional initialization if no keys are specified
135
- def get_react_agent_model_name(model_name: str = ""):
136
- final_model_name = model_name
137
- if final_model_name == "":
138
- final_model_name = "claude-sonnet-4-5-20250929"
139
- logger.info(f"final_model_name: {final_model_name}")
140
- return final_model_name
141
-
142
-
143
- ANTHROPIC_MAX_TOKENS = 64000
144
- GEMINI_MAX_TOKENS = 32000
145
- TAIDE_MAX_TOKENS = 8192
146
-
147
-
148
- def get_react_agent_model(model_name: str = ""):
149
- final_model_name = get_react_agent_model_name(model_name).strip()
150
-
151
- # 處理 taide/ 前綴的模型
152
- if final_model_name.startswith("taide/"):
153
- taide_api_key = os.getenv("TAIDE_API_KEY", "")
154
- taide_base_url = os.getenv("TAIDE_BASE_URL", "")
155
-
156
- if not taide_api_key or not taide_base_url:
157
- raise ValueError(
158
- f"Model name starts with 'taide/' but TAIDE_API_KEY or TAIDE_BASE_URL not set. "
159
- f"Both environment variables are required for: {final_model_name}"
160
- )
161
-
162
- # 取得 taide/ 後面的模型名稱
163
- taide_model_name = final_model_name[len("taide/"):]
164
-
165
- if not taide_model_name:
166
- raise ValueError(
167
- f"Invalid taide model format: {final_model_name}. "
168
- "Expected format: taide/<model_name>"
169
- )
170
-
171
- model = ChatOpenAI(
172
- openai_api_key=taide_api_key,
173
- openai_api_base=taide_base_url,
174
- model_name=taide_model_name,
175
- temperature=0,
176
- max_tokens=TAIDE_MAX_TOKENS,
177
- )
178
- logger.info(f"model ChatOpenAI (TAIDE) {taide_model_name} @ {taide_base_url}")
179
- return model
180
-
181
- # 處理 vertexai/ 前綴的模型
182
- if final_model_name.startswith("vertex-ai/"):
183
- vertex_project = os.getenv("VERTEX_AI_LANGCHAIN_PROJECT", "")
184
-
185
- # 如果沒有設定 VERTEX_AI_LANGCHAIN_PROJECT,則不處理 vertex-ai/ 前綴
186
- if not vertex_project:
187
- logger.warning(
188
- f"Model name starts with 'vertex-ai/' but VERTEX_AI_LANGCHAIN_PROJECT not set. "
189
- f"Skipping vertex-ai/ processing for {final_model_name}"
190
- )
191
- # 移除 vertex-ai/ 前綴後繼續處理
192
- final_model_name = final_model_name[len("vertex-ai/"):]
193
- # 移除 region 部分 (如果有的話)
194
- if "/" in final_model_name:
195
- parts = final_model_name.split("/", 1)
196
- if len(parts) == 2:
197
- final_model_name = parts[1]
198
- else:
199
- # 解析 vertex-ai/region/model_name 格式
200
- parts = final_model_name.split("/")
201
-
202
- if len(parts) != 3:
203
- raise ValueError(
204
- f"Invalid vertexai model format: {final_model_name}. "
205
- "Expected format: vertex-ai/<region>/<model_name>"
206
- )
207
-
208
- vertex_region = parts[1]
209
- vertex_model_name = parts[2]
210
-
211
- if not vertex_region or not vertex_model_name:
212
- raise ValueError(
213
- f"Missing region or model_name in: {final_model_name}. "
214
- "Both region and model_name are required."
215
- )
216
-
217
- # 取得 credentials
218
- vertex_sa_path = os.getenv(
219
- "VERTEX_AI_LANGCHAIN_GOOGLE_APPLICATION_CREDENTIALS", ""
220
- )
221
-
222
- credentials = None
223
- if vertex_sa_path and os.path.exists(vertex_sa_path):
224
- SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
225
- credentials = service_account.Credentials.from_service_account_file(
226
- vertex_sa_path, scopes=SCOPES
227
- )
228
- logger.info(f"Using Vertex AI service account from {vertex_sa_path}")
229
- else:
230
- logger.warning(
231
- "VERTEX_AI_LANGCHAIN_GOOGLE_APPLICATION_CREDENTIALS not set. Using ADC."
232
- )
233
-
234
- # 判斷模型類型並創建相應實例
235
- if vertex_model_name.startswith("gemini-"):
236
- # Gemini 系列:gemini-2.5-pro, gemini-2.5-flash, gemini-pro
237
- model = ChatVertexAI(
238
- model=vertex_model_name,
239
- location=vertex_region,
240
- project=vertex_project,
241
- credentials=credentials,
242
- temperature=0,
243
- max_tokens=GEMINI_MAX_TOKENS,
244
- )
245
- logger.info(
246
- f"model ChatVertexAI {vertex_model_name} @ {vertex_region} (project: {vertex_project})"
247
- )
248
-
249
- elif "claude" in vertex_model_name.lower() or vertex_model_name.startswith("maison/"):
250
- # Anthropic Claude (model garden)
251
- model = ChatAnthropicVertex(
252
- model=vertex_model_name,
253
- location=vertex_region,
254
- project=vertex_project,
255
- credentials=credentials,
256
- temperature=0,
257
- max_tokens=ANTHROPIC_MAX_TOKENS,
258
- )
259
- logger.info(
260
- f"model ChatAnthropicVertex {vertex_model_name} @ {vertex_region} (project: {vertex_project})"
261
- )
262
-
263
- else:
264
- raise ValueError(
265
- f"Unsupported Vertex AI model: {vertex_model_name}. "
266
- "Supported types: gemini-*, claude*, maison/*"
267
- )
268
-
269
- return model
270
-
271
- if final_model_name.startswith("gemini-"):
272
- model = ChatGoogleGenerativeAI(
273
- model=final_model_name, temperature=0, max_tokens=GEMINI_MAX_TOKENS
274
- )
275
- logger.info(f"model ChatGoogleGenerativeAI {final_model_name}")
276
- elif final_model_name.startswith("claude-"):
277
- # use_vertex_ai = os.getenv("USE_VERTEX_AI", "false").lower() in ("true", "1", "yes")
278
- vertex_project = os.getenv("VERTEX_AI_LANGCHAIN_PROJECT", "")
279
- vertex_location = os.getenv("VERTEX_AI_LANGCHAIN_LOCATION", "")
280
- vertex_model = os.getenv("VERTEX_AI_LANGCHAIN_MODEL", "")
281
- vertex_sa_path = os.getenv(
282
- "VERTEX_AI_LANGCHAIN_GOOGLE_APPLICATION_CREDENTIALS", ""
283
- )
284
-
285
- if vertex_location and vertex_model and vertex_sa_path and vertex_project:
286
- # 從環境變數讀取設定
287
-
288
- # 驗證 service account
289
- credentials = None
290
- if vertex_sa_path and os.path.exists(vertex_sa_path):
291
- # 加入 Vertex AI 需要的 scopes
292
- SCOPES = [
293
- "https://www.googleapis.com/auth/cloud-platform",
294
- ]
295
- credentials = service_account.Credentials.from_service_account_file(
296
- vertex_sa_path, scopes=SCOPES
297
- )
298
- logger.info(f"Using Vertex AI service account from {vertex_sa_path}")
299
- else:
300
- logger.warning(
301
- "VERTEX_AI_GOOGLE_APPLICATION_CREDENTIALS not set or file not found. Using ADC if available."
302
- )
303
-
304
- # 初始化 ChatAnthropicVertex
305
- model = ChatAnthropicVertex(
306
- project=vertex_project,
307
- model=vertex_model,
308
- location=vertex_location,
309
- credentials=credentials,
310
- temperature=0,
311
- max_tokens=ANTHROPIC_MAX_TOKENS,
312
- )
313
- logger.info(
314
- f"model ChatAnthropicVertex {vertex_project} @ {vertex_model} @ {vertex_location}"
315
- )
316
-
317
- else:
318
- anthropic_api_keys_str = os.getenv("ANTHROPIC_API_KEYS", "")
319
- anthropic_api_keys = [
320
- key.strip() for key in anthropic_api_keys_str.split(",") if key.strip()
321
- ]
322
- if anthropic_api_keys:
323
-
324
- model = RotatingChatAnthropic(
325
- model_name=final_model_name,
326
- keys=anthropic_api_keys,
327
- temperature=0,
328
- max_tokens=ANTHROPIC_MAX_TOKENS,
329
- )
330
- logger.info(f"model RotatingChatAnthropic {final_model_name}")
331
- elif os.getenv("OPENROUTER_API_KEY") and os.getenv("OPENROUTER_BASE_URL"):
332
-
333
- openrouter_model_name = "anthropic/claude-sonnet-4.5"
334
- # openrouter_model_name = "openai/o4-mini-high"
335
- # openrouter_model_name = "openai/gpt-4.1"
336
- model = ChatOpenAI(
337
- openai_api_key=os.getenv("OPENROUTER_API_KEY"),
338
- openai_api_base=os.getenv("OPENROUTER_BASE_URL"),
339
- model_name=openrouter_model_name,
340
- temperature=0,
341
- max_tokens=ANTHROPIC_MAX_TOKENS,
342
- model_kwargs={
343
- # "headers": {
344
- # "HTTP-Referer": getenv("YOUR_SITE_URL"),
345
- # "X-Title": getenv("YOUR_SITE_NAME"),
346
- # }
347
- },
348
- )
349
- logger.info(f"model OpenRouter {openrouter_model_name}")
350
- else:
351
-
352
- model = ChatAnthropic(
353
- model=final_model_name,
354
- temperature=0,
355
- max_tokens=ANTHROPIC_MAX_TOKENS,
356
- # model_kwargs={
357
- # "extra_headers": {
358
- # "anthropic-beta": "token-efficient-tools-2025-02-19",
359
- # "anthropic-beta": "output-128k-2025-02-19",
360
- # }
361
- # },
362
- )
363
- logger.info(f"model ChatAnthropic {final_model_name}")
364
-
365
- else:
366
- raise ValueError(f"Unknown model name prefix: {final_model_name}")
367
-
368
- return model
369
-
370
-
371
- # model = ChatOpenAI(model="gpt-4o", temperature=0)
372
- # model = ChatGoogleGenerativeAI(model="gemini-2.0-pro-exp-02-05", temperature=0)
373
- # model = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)
374
-
375
-
376
- # For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)
377
-
378
-
379
- # Removed scrape and compare_date_time tools - now provided by MCP server
380
-
381
-
382
- # Removed chat_with_pdf tool - now provided by MCP server
383
-
384
-
385
- # Removed generate_image tool - now provided by MCP server
386
-
387
-
388
- # Removed chat_with_imgs tool - now provided by MCP server
389
-
390
-
391
- # Removed generate_tmp_public_url tool - now provided by MCP server
392
-
393
-
394
- def format_dates(dt):
395
- """
396
- 將日期時間格式化為西元和民國格式
397
- 西元格式:yyyy-mm-dd hh:mm:ss
398
- 民國格式:(yyyy-1911)-mm-dd hh:mm:ss
399
- """
400
- western_date = dt.strftime("%Y-%m-%d %H:%M:%S")
401
- taiwan_year = dt.year - 1911
402
- taiwan_date = f"{taiwan_year}-{dt.strftime('%m-%d %H:%M:%S')}"
403
-
404
- return {"western_date": western_date, "taiwan_date": taiwan_date}
405
-
406
-
407
- # Removed create_html_page tool - now provided by MCP server
408
-
409
-
410
- # DICT_VAR = {}
411
-
412
- # Define the graph
413
-
414
- # Removed language-specific system prompts - now using user-provided system_prompt directly
415
-
416
-
417
- def transform_anthropic_incompatible_schema(
418
- schema_dict: dict,
419
- ) -> tuple[dict, bool, str]:
420
- """
421
- 轉換可能與 Anthropic 不相容的頂層 schema 結構。
422
-
423
- Args:
424
- schema_dict: 原始 schema 字典。
425
-
426
- Returns:
427
- tuple: (轉換後的 schema 字典, 是否進行了轉換, 附加到 description 的提示信息)
428
- """
429
- if not isinstance(schema_dict, dict):
430
- return schema_dict, False, ""
431
-
432
- keys_to_check = ["anyOf", "allOf", "oneOf"]
433
- problematic_key = None
434
- for key in keys_to_check:
435
- if key in schema_dict:
436
- problematic_key = key
437
- break
438
-
439
- if problematic_key:
440
- print(f" 發現頂層 '{problematic_key}',進行轉換...")
441
- transformed = True
442
- new_schema = {"type": "object", "properties": {}, "required": []}
443
- description_notes = f"\n[開發者註記:此工具參數原使用 '{problematic_key}' 結構,已轉換。請依賴參數描述判斷必要輸入。]"
444
-
445
- # 1. 合併 Properties
446
- # 先加入頂層的 properties (如果存在)
447
- if "properties" in schema_dict:
448
- new_schema["properties"].update(copy.deepcopy(schema_dict["properties"]))
449
- # 再合併來自 problematic_key 內部的 properties
450
- for sub_schema in schema_dict.get(problematic_key, []):
451
- if isinstance(sub_schema, dict) and "properties" in sub_schema:
452
- # 注意:如果不同 sub_schema 有同名 property,後者會覆蓋前者
453
- new_schema["properties"].update(copy.deepcopy(sub_schema["properties"]))
454
-
455
- # 2. 處理 Required
456
- top_level_required = set(schema_dict.get("required", []))
457
-
458
- if problematic_key == "allOf":
459
- # allOf: 合併所有 required
460
- combined_required = top_level_required
461
- for sub_schema in schema_dict.get(problematic_key, []):
462
- if isinstance(sub_schema, dict) and "required" in sub_schema:
463
- combined_required.update(sub_schema["required"])
464
- # 只保留實際存在於合併後 properties 中的 required 欄位
465
- new_schema["required"] = sorted(
466
- [req for req in combined_required if req in new_schema["properties"]]
467
- )
468
- description_notes += " 所有相關參數均需考慮。]" # 簡單提示
469
- elif problematic_key in ["anyOf", "oneOf"]:
470
- # anyOf/oneOf: 只保留頂層 required,並在描述中說明選擇性
471
- new_schema["required"] = sorted(
472
- [req for req in top_level_required if req in new_schema["properties"]]
473
- )
474
- # 嘗試生成更具體的提示 (如果 sub_schema 結構簡單)
475
- options = []
476
- for sub_schema in schema_dict.get(problematic_key, []):
477
- if isinstance(sub_schema, dict) and "required" in sub_schema:
478
- options.append(f"提供 '{', '.join(sub_schema['required'])}'")
479
- if options:
480
- description_notes += (
481
- f" 通常需要滿足以下條件之一:{'; '.join(options)}。]"
482
- )
483
- else:
484
- description_notes += " 請注意參數間的選擇關係。]"
485
-
486
- print(
487
- f" 轉換後 schema: {json.dumps(new_schema, indent=2, ensure_ascii=False)}"
488
- )
489
- return new_schema, transformed, description_notes
490
- else:
491
- return schema_dict, False, ""
492
-
493
-
494
- # --- Schema 轉換輔助函數 ( _get_mcp_tools_async 提取) ---
495
- def _process_mcp_tools_for_anthropic(langchain_tools: List[Any]) -> List[Any]:
496
- """處理 MCP 工具列表,轉換不相容的 Schema 並記錄日誌"""
497
- if not langchain_tools:
498
- logger.info("[_process_mcp_tools_for_anthropic] 警告 - 未找到任何工具。")
499
- return []
500
-
501
- logger.info(
502
- f"[_process_mcp_tools_for_anthropic] --- 開始處理 {len(langchain_tools)} 個原始 MCP 工具 ---"
503
- )
504
-
505
- processed_tools = []
506
- for mcp_tool in langchain_tools:
507
- # 只處理 StructuredTool 或類似的有 args_schema 的工具
508
- if not hasattr(mcp_tool, "args_schema") or not mcp_tool.args_schema:
509
- logger.debug(
510
- f"[_process_mcp_tools_for_anthropic] 工具 '{mcp_tool.name}' 沒有 args_schema,直接加入。"
511
- )
512
- processed_tools.append(mcp_tool)
513
- continue
514
-
515
- original_schema_dict = {}
516
- try:
517
- # 嘗試獲取 schema 字典 (根據 Pydantic 版本可能不同)
518
- if hasattr(mcp_tool.args_schema, "model_json_schema"): # Pydantic V2
519
- original_schema_dict = mcp_tool.args_schema.model_json_schema()
520
- elif hasattr(mcp_tool.args_schema, "schema"): # Pydantic V1
521
- original_schema_dict = mcp_tool.args_schema.schema()
522
- elif isinstance(mcp_tool.args_schema, dict): # 已經是字典?
523
- original_schema_dict = mcp_tool.args_schema
524
- else:
525
- logger.warning(
526
- f"[_process_mcp_tools_for_anthropic] 無法獲取工具 '{mcp_tool.name}' 的 schema 字典 ({type(mcp_tool.args_schema)}),跳過轉換。"
527
- )
528
- processed_tools.append(mcp_tool)
529
- continue
530
-
531
- # 進行轉換檢查
532
- logger.debug(
533
- f"[_process_mcp_tools_for_anthropic] 檢查工具 '{mcp_tool.name}' 的 schema..."
534
- )
535
- new_schema_dict, transformed, desc_notes = (
536
- transform_anthropic_incompatible_schema(
537
- copy.deepcopy(original_schema_dict) # 使用深拷貝操作
538
- )
539
- )
540
-
541
- if transformed:
542
- mcp_tool.description += desc_notes
543
- logger.info(
544
- f"[_process_mcp_tools_for_anthropic] 工具 '{mcp_tool.name}' 的描述已更新。"
545
- )
546
- if isinstance(mcp_tool.args_schema, dict):
547
- logger.debug(
548
- f"[_process_mcp_tools_for_anthropic] args_schema 是字典,直接替換工具 '{mcp_tool.name}' 的 schema。"
549
- )
550
- mcp_tool.args_schema = new_schema_dict
551
- else:
552
- # 如果 args_schema 是 Pydantic 模型,直接修改可能無效或困難
553
- # 附加轉換後的字典可能是一種備選方案,但 Langchain/LangGraph 可能不直接使用它
554
- # 最好的方法是確保 get_tools 返回的工具的 args_schema 可以被修改,
555
- # 或者在創建工具時就使用轉換後的 schema。
556
- # 如果不能直接修改,附加屬性是一種標記方式,但可能需要在工具調用處處理。
557
- logger.warning(
558
- f"[_process_mcp_tools_for_anthropic] args_schema 不是字典 ({type(mcp_tool.args_schema)}),僅添加 _transformed_args_schema_dict 屬性到工具 '{mcp_tool.name}'。這可能不足以解決根本問題。"
559
- )
560
- setattr(mcp_tool, "_transformed_args_schema_dict", new_schema_dict)
561
- processed_tools.append(mcp_tool)
562
-
563
- except Exception as e_schema:
564
- logger.error(
565
- f"[_process_mcp_tools_for_anthropic] 處理工具 '{mcp_tool.name}' schema 時發生錯誤: {e_schema}",
566
- exc_info=True,
567
- )
568
- processed_tools.append(mcp_tool) # 保留原始工具
569
-
570
- logger.info(
571
- f"[_process_mcp_tools_for_anthropic] --- 完成工具處理,返回 {len(processed_tools)} 個工具 ---"
572
- )
573
- return processed_tools
574
-
575
-
576
- async def create_react_agent_graph(
577
- system_prompt: str = "",
578
- botrun_flow_lang_url: str = "",
579
- user_id: str = "",
580
- model_name: str = "",
581
- lang: str = LANG_EN,
582
- mcp_config: Optional[Dict[str, Any]] = None, # <--- 接收配置而非客戶端實例
583
- ):
584
- """
585
- Create a react agent graph with simplified architecture.
586
-
587
- This function now creates a fully MCP-integrated agent with:
588
- - Direct system prompt usage (no language-specific prompt concatenation)
589
- - Zero local tools - all functionality provided by MCP server
590
- - Complete MCP server integration for all tools (web search, scraping, PDF/image analysis, time/date, visualizations, etc.)
591
- - Removed all complex conditional logic and local tool definitions
592
-
593
- Args:
594
- system_prompt: The system prompt to use for the agent (used directly, no concatenation)
595
- botrun_flow_lang_url: URL for botrun flow lang service (reserved for future use)
596
- user_id: User identifier (reserved for future use)
597
- model_name: AI model name to use (defaults to claude-sonnet-4-5-20250929)
598
- lang: Language code affecting language-specific tools (e.g., "en", "zh-TW")
599
- mcp_config: MCP servers configuration dict providing tools like scrape, chat_with_pdf, etc.
600
-
601
- Returns:
602
- A LangGraph react agent configured with simplified architecture
603
-
604
- Note:
605
- - Local MCP tools (scrape, chat_with_pdf, etc.) have been removed
606
- - compare_date_time tool has been completely removed
607
- - All advanced tools are now provided via MCP server configuration
608
- - Language-specific prompts have been removed for simplification
609
- """
610
-
611
- # Complete MCP migration - all tools are now provided by MCP server
612
- # No local tools remain - all functionality accessed via mcp_config
613
- tools = [
614
- # ALL MIGRATED TO MCP: scrape, chat_with_pdf, chat_with_imgs, generate_image,
615
- # generate_tmp_public_url, create_html_page, create_plotly_chart,
616
- # create_mermaid_diagram, current_date_time, web_search
617
- # ❌ REMOVED: compare_date_time (completely eliminated)
618
- ]
619
-
620
- mcp_tools = []
621
- if mcp_config:
622
- logger.info("偵測到 MCP 配置,直接創建 MCP 工具...")
623
- try:
624
- # 直接創建 MCP client 並獲取工具,不使用 context manager
625
-
626
- client = MultiServerMCPClient(mcp_config)
627
- raw_mcp_tools = await client.get_tools()
628
- print("raw_mcp_tools============>", raw_mcp_tools)
629
-
630
- if raw_mcp_tools:
631
- logger.info(f"從 MCP 配置獲取了 {len(raw_mcp_tools)} 個原始工具。")
632
- # 處理 Schema (使用提取的輔助函數)
633
- mcp_tools = _process_mcp_tools_for_anthropic(raw_mcp_tools)
634
- if mcp_tools:
635
- tools.extend(mcp_tools)
636
- logger.info(f"已加入 {len(mcp_tools)} 個處理後的 MCP 工具。")
637
- logger.debug(
638
- f"加入的 MCP 工具名稱: {[tool.name for tool in mcp_tools]}"
639
- )
640
- else:
641
- logger.warning("MCP 工具處理後列表為空。")
642
- else:
643
- logger.info("MCP Client 返回了空的工具列表。")
644
-
645
- # 注意:我們不在這裡關閉 client,因為 tools 可能需要它來執行
646
- # client 會在 graph 執行完畢後自動清理
647
- logger.info("MCP client 和工具創建完成,client 將保持活動狀態")
648
-
649
- except Exception as e_get:
650
- import traceback
651
-
652
- traceback.print_exc()
653
- logger.error(f"從 MCP 配置獲取或處理工具時發生錯誤: {e_get}", exc_info=True)
654
- # 即使出錯,也可能希望繼續執行(不帶 MCP 工具)
655
- else:
656
- logger.info("未提供 MCP 配置,跳過 MCP 工具。")
657
-
658
- # Simplified: use user-provided system_prompt directly (no language-specific prompts)
659
- new_system_prompt = system_prompt
660
- if botrun_flow_lang_url and user_id:
661
- new_system_prompt = (
662
- f"""IMPORTANT: Any URL returned by tools MUST be included in your response as a markdown link [text](URL).
663
- Please use the standard [text](URL) format to present links, ensuring the link text remains plain and unformatted.
664
- Example:
665
- User: "Create a new page for our project documentation"
666
- Tool returns: {{"page_url": "https://notion.so/workspace/abc123"}}
667
- Assistant: "I've created the new page for your project documentation. You can access it here: [Project Documentation](https://notion.so/workspace/abc123)"
668
- """
669
- + system_prompt
670
- + f"""\n\n
671
- - If the tool needs parameter like botrun_flow_lang_url or user_id, please use the following:
672
- botrun_flow_lang_url: {botrun_flow_lang_url}
673
- user_id: {user_id}
674
- """
675
- )
676
- system_message = SystemMessage(
677
- content=[
678
- {
679
- "text": new_system_prompt,
680
- "type": "text",
681
- "cache_control": {"type": "ephemeral"},
682
- }
683
- ]
684
- )
685
-
686
- # 目前先使用了 https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
687
- # 這一段會遇到
688
- # File "/Users/seba/Projects/botrun_flow_lang/.venv/lib/python3.11/site-packages/langgraph/prebuilt/tool_node.py", line 218, in __init__
689
- # tool_ = create_tool(tool_)
690
- # ^^^^^^^^^^^^^^^^^^
691
- # File "/Users/seba/Projects/botrun_flow_lang/.venv/lib/python3.11/site-packages/langchain_core/tools/convert.py", line 334, in tool
692
- # raise ValueError(msg)
693
- # ValueError: The first argument must be a string or a callable with a __name__ for tool decorator. Got <class 'dict'>
694
- # 所以先不使用這一段,這一段是參考 https://python.langchain.com/docs/integrations/chat/anthropic/#tools
695
- # 也許未來可以引用
696
- # if get_react_agent_model_name(model_name).startswith("claude-"):
697
- # new_tools = []
698
- # for tool in tools:
699
- # new_tool = convert_to_anthropic_tool(tool)
700
- # new_tool["cache_control"] = {"type": "ephemeral"}
701
- # new_tools.append(new_tool)
702
- # tools = new_tools
703
-
704
- env_name = os.getenv("ENV_NAME", "botrun-flow-lang-dev")
705
- result = create_react_agent(
706
- get_react_agent_model(model_name),
707
- tools=tools,
708
- prompt=system_message,
709
- checkpointer=MemorySaver(), # 如果要執行在 botrun_back 裡面,就不需要 firestore 的 checkpointer
710
- # checkpointer=AsyncFirestoreCheckpointer(env_name=env_name),
711
- )
712
- return result
713
-
714
-
715
- # Default graph instance with empty prompt
716
- # if True:
717
- # react_agent_graph = create_react_agent_graph()
718
- # LangGraph Studio 測試用,把以下 un-comment 就可以測試
719
- # react_agent_graph = create_react_agent_graph(
720
- # system_prompt="",
721
- # botrun_flow_lang_url="https://botrun-flow-lang-fastapi-dev-36186877499.asia-east1.run.app",
722
- # user_id="sebastian.hsu@gmail.com",
723
- # )
1
+ import os
2
+ import asyncio
3
+ import json
4
+ from datetime import datetime
5
+ from typing import ClassVar, Dict, List, Optional, Any
6
+
7
+ from langchain_core.messages import SystemMessage
8
+
9
+ from botrun_flow_lang.constants import LANG_EN, LANG_ZH_TW
10
+
11
+ from langgraph.checkpoint.memory import MemorySaver
12
+ from langchain_core.runnables import RunnableConfig
13
+
14
+ from langchain_core.tools import BaseTool
15
+
16
+ from langchain_core.tools import tool
17
+
18
+ from botrun_flow_lang.utils.botrun_logger import get_default_botrun_logger
19
+
20
+ # All tools now provided by MCP server - no local tool imports needed
21
+
22
+ from botrun_flow_lang.langgraph_agents.agents.checkpointer.firestore_checkpointer import (
23
+ AsyncFirestoreCheckpointer,
24
+ )
25
+
26
+ from langgraph.prebuilt import create_react_agent
27
+
28
+ from dotenv import load_dotenv
29
+
30
+ import copy # 用於深拷貝 schema,避免意外修改原始對象
31
+
32
+ # Removed DALL-E and rate limiting imports - tools now provided by MCP server
33
+
34
+ # =========
35
+ # 📋 STAGE 4 REFACTORING COMPLETED (MCP Integration)
36
+ #
37
+ # This file has been refactored to integrate with MCP (Model Context Protocol):
38
+ #
39
+ # ✅ REMOVED (~600 lines):
40
+ # - Language-specific system prompts (zh_tw_system_prompt, en_system_prompt)
41
+ # - Local tool definitions: scrape, chat_with_pdf, chat_with_imgs, generate_image,
42
+ # generate_tmp_public_url, create_html_page, compare_date_time
43
+ # - Complex conditional logic (if botrun_flow_lang_url and user_id)
44
+ # - Rate limiting exception and related imports
45
+ # - Unused utility imports
46
+ #
47
+ # ✅ SIMPLIFIED:
48
+ # - Direct system_prompt usage (no concatenation)
49
+ # - Streamlined tools list (only language-specific tools)
50
+ # - Clean MCP integration via mcp_config parameter
51
+ # - Maintained backward compatibility for all parameters
52
+ #
53
+ # 🎯 RESULT:
54
+ # - Reduced complexity while maintaining full functionality
55
+ # - All tools available via MCP server at /mcp/default/mcp/
56
+ # - Ready for Phase 2: language-specific tools migration
57
+ # =========
58
+
59
+ # 放到要用的時候才 init,不然loading 會花時間
60
+ # 因為要讓 langgraph 在本地端執行,所以這一段又搬回到外面了
61
+ from langchain_google_genai import ChatGoogleGenerativeAI
62
+
63
+ # =========
64
+ # 放到要用的時候才 import,不然loading 會花時間
65
+ # 因為LangGraph 在本地端執行,所以這一段又搬回到外面了
66
+ from botrun_flow_lang.langgraph_agents.agents.util.model_utils import (
67
+ RotatingChatAnthropic,
68
+ )
69
+
70
+ # =========
71
+ # 放到要用的時候才 init,不然loading 會花時間
72
+ # 因為LangGraph 在本地端執行,所以這一段又搬回到外面了
73
+ from langchain_openai import ChatOpenAI
74
+
75
+ # =========
76
+ # 放到要用的時候才 init,不然loading 會花時間
77
+ # 因為LangGraph 在本地端執行,所以這一段又搬回到外面了
78
+ from langchain_anthropic import ChatAnthropic
79
+
80
+ # =========
81
+
82
+ # 假設 MultiServerMCPClient 和 StructuredTool 已經被正確導入
83
+ from langchain_core.tools import StructuredTool
84
+ from langchain_mcp_adapters.client import MultiServerMCPClient
85
+
86
+ # ========
87
+ # for Vertex AI
88
+ from google.oauth2 import service_account
89
+ # 重型 import 改為延遲載入,避免啟動時載入 google-cloud-aiplatform(約 26 秒)
90
+ # ChatVertexAI 已遷移至 ChatGoogleGenerativeAI(vertexai=True)
91
+ # ChatAnthropicVertex 在需要時才 import(見 get_react_agent_model 函數內)
92
+
93
+ load_dotenv()
94
+
95
+ # logger = default_logger
96
+ logger = get_default_botrun_logger()
97
+
98
+
99
+ # Removed BotrunRateLimitException - rate limiting now handled by MCP server
100
+
101
+
102
+ # Load Anthropic API keys from environment
103
+ # anthropic_api_keys_str = os.getenv("ANTHROPIC_API_KEYS", "")
104
+ # anthropic_api_keys = [
105
+ # key.strip() for key in anthropic_api_keys_str.split(",") if key.strip()
106
+ # ]
107
+
108
+ # Initialize the model with key rotation if multiple keys are available
109
+ # if anthropic_api_keys:
110
+ # model = RotatingChatAnthropic(
111
+ # model_name="claude-3-7-sonnet-latest",
112
+ # keys=anthropic_api_keys,
113
+ # temperature=0,
114
+ # max_tokens=8192,
115
+ # )
116
+ # 建立 AWS Session
117
+ # session = boto3.Session(
118
+ # aws_access_key_id="",
119
+ # aws_secret_access_key="",
120
+ # region_name="us-west-2",
121
+ # )
122
+
123
+
124
+ # # 使用該 Session 初始化 Bedrock 客戶端
125
+ # bedrock_runtime = session.client(
126
+ # service_name="bedrock-runtime",
127
+ # )
128
+ # model = ChatBedrockConverse(
129
+ # model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
130
+ # client=bedrock_runtime,
131
+ # temperature=0,
132
+ # max_tokens=8192,
133
+ # )
134
+ # else:
135
+ # Fallback to traditional initialization if no keys are specified
136
+ def get_react_agent_model_name(model_name: str = ""):
137
+ final_model_name = model_name
138
+ if final_model_name == "":
139
+ final_model_name = "claude-sonnet-4-5-20250929"
140
+ logger.info(f"final_model_name: {final_model_name}")
141
+ return final_model_name
142
+
143
+
144
+ ANTHROPIC_MAX_TOKENS = 64000
145
+ GEMINI_MAX_TOKENS = 32000
146
+ TAIDE_MAX_TOKENS = 8192
147
+
148
+
149
+ def get_react_agent_model(model_name: str = ""):
150
+ final_model_name = get_react_agent_model_name(model_name).strip()
151
+
152
+ # 處理 taide/ 前綴的模型
153
+ if final_model_name.startswith("taide/"):
154
+ taide_api_key = os.getenv("TAIDE_API_KEY", "")
155
+ taide_base_url = os.getenv("TAIDE_BASE_URL", "")
156
+
157
+ if not taide_api_key or not taide_base_url:
158
+ raise ValueError(
159
+ f"Model name starts with 'taide/' but TAIDE_API_KEY or TAIDE_BASE_URL not set. "
160
+ f"Both environment variables are required for: {final_model_name}"
161
+ )
162
+
163
+ # 取得 taide/ 後面的模型名稱
164
+ taide_model_name = final_model_name[len("taide/"):]
165
+
166
+ if not taide_model_name:
167
+ raise ValueError(
168
+ f"Invalid taide model format: {final_model_name}. "
169
+ "Expected format: taide/<model_name>"
170
+ )
171
+
172
+ model = ChatOpenAI(
173
+ openai_api_key=taide_api_key,
174
+ openai_api_base=taide_base_url,
175
+ model_name=taide_model_name,
176
+ temperature=0,
177
+ max_tokens=TAIDE_MAX_TOKENS,
178
+ )
179
+ logger.info(f"model ChatOpenAI (TAIDE) {taide_model_name} @ {taide_base_url}")
180
+ return model
181
+
182
+ # 處理 vertexai/ 前綴的模型
183
+ if final_model_name.startswith("vertex-ai/"):
184
+ vertex_project = os.getenv("VERTEX_AI_LANGCHAIN_PROJECT", "")
185
+
186
+ # 如果沒有設定 VERTEX_AI_LANGCHAIN_PROJECT,則不處理 vertex-ai/ 前綴
187
+ if not vertex_project:
188
+ logger.warning(
189
+ f"Model name starts with 'vertex-ai/' but VERTEX_AI_LANGCHAIN_PROJECT not set. "
190
+ f"Skipping vertex-ai/ processing for {final_model_name}"
191
+ )
192
+ # 移除 vertex-ai/ 前綴後繼續處理
193
+ final_model_name = final_model_name[len("vertex-ai/"):]
194
+ # 移除 region 部分 (如果有的話)
195
+ if "/" in final_model_name:
196
+ parts = final_model_name.split("/", 1)
197
+ if len(parts) == 2:
198
+ final_model_name = parts[1]
199
+ else:
200
+ # 解析 vertex-ai/region/model_name 格式
201
+ parts = final_model_name.split("/")
202
+
203
+ if len(parts) != 3:
204
+ raise ValueError(
205
+ f"Invalid vertexai model format: {final_model_name}. "
206
+ "Expected format: vertex-ai/<region>/<model_name>"
207
+ )
208
+
209
+ vertex_region = parts[1]
210
+ vertex_model_name = parts[2]
211
+
212
+ if not vertex_region or not vertex_model_name:
213
+ raise ValueError(
214
+ f"Missing region or model_name in: {final_model_name}. "
215
+ "Both region and model_name are required."
216
+ )
217
+
218
+ # 取得 credentials
219
+ vertex_sa_path = os.getenv(
220
+ "VERTEX_AI_LANGCHAIN_GOOGLE_APPLICATION_CREDENTIALS", ""
221
+ )
222
+
223
+ credentials = None
224
+ if vertex_sa_path and os.path.exists(vertex_sa_path):
225
+ SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
226
+ credentials = service_account.Credentials.from_service_account_file(
227
+ vertex_sa_path, scopes=SCOPES
228
+ )
229
+ logger.info(f"Using Vertex AI service account from {vertex_sa_path}")
230
+ else:
231
+ logger.warning(
232
+ "VERTEX_AI_LANGCHAIN_GOOGLE_APPLICATION_CREDENTIALS not set. Using ADC."
233
+ )
234
+
235
+ # 判斷模型類型並創建相應實例
236
+ if vertex_model_name.startswith("gemini-"):
237
+ # Gemini 系列:gemini-2.5-pro, gemini-2.5-flash, gemini-pro
238
+ # 使用 ChatGoogleGenerativeAI + vertexai=True,避免載入重型的 langchain_google_vertexai
239
+ model = ChatGoogleGenerativeAI(
240
+ model=vertex_model_name,
241
+ vertexai=True,
242
+ location=vertex_region,
243
+ project=vertex_project,
244
+ credentials=credentials,
245
+ temperature=0,
246
+ max_tokens=GEMINI_MAX_TOKENS,
247
+ )
248
+ logger.info(
249
+ f"model ChatGoogleGenerativeAI(vertexai=True) {vertex_model_name} @ {vertex_region} (project: {vertex_project})"
250
+ )
251
+
252
+ elif "claude" in vertex_model_name.lower() or vertex_model_name.startswith("maison/"):
253
+ # Anthropic Claude (model garden)
254
+ # 延遲載入 ChatAnthropicVertex,只有在需要時才觸發 langchain_google_vertexai
255
+ from langchain_google_vertexai.model_garden import ChatAnthropicVertex
256
+ model = ChatAnthropicVertex(
257
+ model=vertex_model_name,
258
+ location=vertex_region,
259
+ project=vertex_project,
260
+ credentials=credentials,
261
+ temperature=0,
262
+ max_tokens=ANTHROPIC_MAX_TOKENS,
263
+ )
264
+ logger.info(
265
+ f"model ChatAnthropicVertex {vertex_model_name} @ {vertex_region} (project: {vertex_project})"
266
+ )
267
+
268
+ else:
269
+ raise ValueError(
270
+ f"Unsupported Vertex AI model: {vertex_model_name}. "
271
+ "Supported types: gemini-*, claude*, maison/*"
272
+ )
273
+
274
+ return model
275
+
276
+ if final_model_name.startswith("gemini-"):
277
+ model = ChatGoogleGenerativeAI(
278
+ model=final_model_name, temperature=0, max_tokens=GEMINI_MAX_TOKENS
279
+ )
280
+ logger.info(f"model ChatGoogleGenerativeAI {final_model_name}")
281
+ elif final_model_name.startswith("claude-"):
282
+ # use_vertex_ai = os.getenv("USE_VERTEX_AI", "false").lower() in ("true", "1", "yes")
283
+ vertex_project = os.getenv("VERTEX_AI_LANGCHAIN_PROJECT", "")
284
+ vertex_location = os.getenv("VERTEX_AI_LANGCHAIN_LOCATION", "")
285
+ vertex_model = os.getenv("VERTEX_AI_LANGCHAIN_MODEL", "")
286
+ vertex_sa_path = os.getenv(
287
+ "VERTEX_AI_LANGCHAIN_GOOGLE_APPLICATION_CREDENTIALS", ""
288
+ )
289
+
290
+ if vertex_location and vertex_model and vertex_sa_path and vertex_project:
291
+ # 從環境變數讀取設定
292
+
293
+ # 驗證 service account
294
+ credentials = None
295
+ if vertex_sa_path and os.path.exists(vertex_sa_path):
296
+ # 加入 Vertex AI 需要的 scopes
297
+ SCOPES = [
298
+ "https://www.googleapis.com/auth/cloud-platform",
299
+ ]
300
+ credentials = service_account.Credentials.from_service_account_file(
301
+ vertex_sa_path, scopes=SCOPES
302
+ )
303
+ logger.info(f"Using Vertex AI service account from {vertex_sa_path}")
304
+ else:
305
+ logger.warning(
306
+ "VERTEX_AI_GOOGLE_APPLICATION_CREDENTIALS not set or file not found. Using ADC if available."
307
+ )
308
+
309
+ # 初始化 ChatAnthropicVertex
310
+ # 延遲載入,只有在需要時才觸發 langchain_google_vertexai
311
+ from langchain_google_vertexai.model_garden import ChatAnthropicVertex
312
+ model = ChatAnthropicVertex(
313
+ project=vertex_project,
314
+ model=vertex_model,
315
+ location=vertex_location,
316
+ credentials=credentials,
317
+ temperature=0,
318
+ max_tokens=ANTHROPIC_MAX_TOKENS,
319
+ )
320
+ logger.info(
321
+ f"model ChatAnthropicVertex {vertex_project} @ {vertex_model} @ {vertex_location}"
322
+ )
323
+
324
+ else:
325
+ anthropic_api_keys_str = os.getenv("ANTHROPIC_API_KEYS", "")
326
+ anthropic_api_keys = [
327
+ key.strip() for key in anthropic_api_keys_str.split(",") if key.strip()
328
+ ]
329
+ if anthropic_api_keys:
330
+
331
+ model = RotatingChatAnthropic(
332
+ model_name=final_model_name,
333
+ keys=anthropic_api_keys,
334
+ temperature=0,
335
+ max_tokens=ANTHROPIC_MAX_TOKENS,
336
+ )
337
+ logger.info(f"model RotatingChatAnthropic {final_model_name}")
338
+ elif os.getenv("OPENROUTER_API_KEY") and os.getenv("OPENROUTER_BASE_URL"):
339
+
340
+ openrouter_model_name = "anthropic/claude-sonnet-4.5"
341
+ # openrouter_model_name = "openai/o4-mini-high"
342
+ # openrouter_model_name = "openai/gpt-4.1"
343
+ model = ChatOpenAI(
344
+ openai_api_key=os.getenv("OPENROUTER_API_KEY"),
345
+ openai_api_base=os.getenv("OPENROUTER_BASE_URL"),
346
+ model_name=openrouter_model_name,
347
+ temperature=0,
348
+ max_tokens=ANTHROPIC_MAX_TOKENS,
349
+ model_kwargs={
350
+ # "headers": {
351
+ # "HTTP-Referer": getenv("YOUR_SITE_URL"),
352
+ # "X-Title": getenv("YOUR_SITE_NAME"),
353
+ # }
354
+ },
355
+ )
356
+ logger.info(f"model OpenRouter {openrouter_model_name}")
357
+ else:
358
+
359
+ model = ChatAnthropic(
360
+ model=final_model_name,
361
+ temperature=0,
362
+ max_tokens=ANTHROPIC_MAX_TOKENS,
363
+ # model_kwargs={
364
+ # "extra_headers": {
365
+ # "anthropic-beta": "token-efficient-tools-2025-02-19",
366
+ # "anthropic-beta": "output-128k-2025-02-19",
367
+ # }
368
+ # },
369
+ )
370
+ logger.info(f"model ChatAnthropic {final_model_name}")
371
+
372
+ else:
373
+ raise ValueError(f"Unknown model name prefix: {final_model_name}")
374
+
375
+ return model
376
+
377
+
378
+ # model = ChatOpenAI(model="gpt-4o", temperature=0)
379
+ # model = ChatGoogleGenerativeAI(model="gemini-2.0-pro-exp-02-05", temperature=0)
380
+ # model = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)
381
+
382
+
383
+ # For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)
384
+
385
+
386
+ # Removed scrape and compare_date_time tools - now provided by MCP server
387
+
388
+
389
+ # Removed chat_with_pdf tool - now provided by MCP server
390
+
391
+
392
+ # Removed generate_image tool - now provided by MCP server
393
+
394
+
395
+ # Removed chat_with_imgs tool - now provided by MCP server
396
+
397
+
398
+ # Removed generate_tmp_public_url tool - now provided by MCP server
399
+
400
+
401
+ def format_dates(dt):
402
+ """
403
+ 將日期時間格式化為西元和民國格式
404
+ 西元格式:yyyy-mm-dd hh:mm:ss
405
+ 民國格式:(yyyy-1911)-mm-dd hh:mm:ss
406
+ """
407
+ western_date = dt.strftime("%Y-%m-%d %H:%M:%S")
408
+ taiwan_year = dt.year - 1911
409
+ taiwan_date = f"{taiwan_year}-{dt.strftime('%m-%d %H:%M:%S')}"
410
+
411
+ return {"western_date": western_date, "taiwan_date": taiwan_date}
412
+
413
+
414
+ # Removed create_html_page tool - now provided by MCP server
415
+
416
+
417
+ # DICT_VAR = {}
418
+
419
+ # Define the graph
420
+
421
+ # Removed language-specific system prompts - now using user-provided system_prompt directly
422
+
423
+
424
+ def transform_anthropic_incompatible_schema(
425
+ schema_dict: dict,
426
+ ) -> tuple[dict, bool, str]:
427
+ """
428
+ 轉換可能與 Anthropic 不相容的頂層 schema 結構。
429
+
430
+ Args:
431
+ schema_dict: 原始 schema 字典。
432
+
433
+ Returns:
434
+ tuple: (轉換後的 schema 字典, 是否進行了轉換, 附加到 description 的提示信息)
435
+ """
436
+ if not isinstance(schema_dict, dict):
437
+ return schema_dict, False, ""
438
+
439
+ keys_to_check = ["anyOf", "allOf", "oneOf"]
440
+ problematic_key = None
441
+ for key in keys_to_check:
442
+ if key in schema_dict:
443
+ problematic_key = key
444
+ break
445
+
446
+ if problematic_key:
447
+ print(f" 發現頂層 '{problematic_key}',進行轉換...")
448
+ transformed = True
449
+ new_schema = {"type": "object", "properties": {}, "required": []}
450
+ description_notes = f"\n[開發者註記:此工具參數原使用 '{problematic_key}' 結構,已轉換。請依賴參數描述判斷必要輸入。]"
451
+
452
+ # 1. 合併 Properties
453
+ # 先加入頂層的 properties (如果存在)
454
+ if "properties" in schema_dict:
455
+ new_schema["properties"].update(copy.deepcopy(schema_dict["properties"]))
456
+ # 再合併來自 problematic_key 內部的 properties
457
+ for sub_schema in schema_dict.get(problematic_key, []):
458
+ if isinstance(sub_schema, dict) and "properties" in sub_schema:
459
+ # 注意:如果不同 sub_schema 有同名 property,後者會覆蓋前者
460
+ new_schema["properties"].update(copy.deepcopy(sub_schema["properties"]))
461
+
462
+ # 2. 處理 Required
463
+ top_level_required = set(schema_dict.get("required", []))
464
+
465
+ if problematic_key == "allOf":
466
+ # allOf: 合併所有 required
467
+ combined_required = top_level_required
468
+ for sub_schema in schema_dict.get(problematic_key, []):
469
+ if isinstance(sub_schema, dict) and "required" in sub_schema:
470
+ combined_required.update(sub_schema["required"])
471
+ # 只保留實際存在於合併後 properties 中的 required 欄位
472
+ new_schema["required"] = sorted(
473
+ [req for req in combined_required if req in new_schema["properties"]]
474
+ )
475
+ description_notes += " 所有相關參數均需考慮。]" # 簡單提示
476
+ elif problematic_key in ["anyOf", "oneOf"]:
477
+ # anyOf/oneOf: 只保留頂層 required,並在描述中說明選擇性
478
+ new_schema["required"] = sorted(
479
+ [req for req in top_level_required if req in new_schema["properties"]]
480
+ )
481
+ # 嘗試生成更具體的提示 (如果 sub_schema 結構簡單)
482
+ options = []
483
+ for sub_schema in schema_dict.get(problematic_key, []):
484
+ if isinstance(sub_schema, dict) and "required" in sub_schema:
485
+ options.append(f"提供 '{', '.join(sub_schema['required'])}'")
486
+ if options:
487
+ description_notes += (
488
+ f" 通常需要滿足以下條件之一:{'; 或 '.join(options)}。]"
489
+ )
490
+ else:
491
+ description_notes += " 請注意參數間的選擇關係。]"
492
+
493
+ print(
494
+ f" 轉換後 schema: {json.dumps(new_schema, indent=2, ensure_ascii=False)}"
495
+ )
496
+ return new_schema, transformed, description_notes
497
+ else:
498
+ return schema_dict, False, ""
499
+
500
+
501
+ # --- Schema 轉換輔助函數 (從 _get_mcp_tools_async 提取) ---
502
+ def _process_mcp_tools_for_anthropic(langchain_tools: List[Any]) -> List[Any]:
503
+ """處理 MCP 工具列表,轉換不相容的 Schema 並記錄日誌"""
504
+ if not langchain_tools:
505
+ logger.info("[_process_mcp_tools_for_anthropic] 警告 - 未找到任何工具。")
506
+ return []
507
+
508
+ logger.info(
509
+ f"[_process_mcp_tools_for_anthropic] --- 開始處理 {len(langchain_tools)} 個原始 MCP 工具 ---"
510
+ )
511
+
512
+ processed_tools = []
513
+ for mcp_tool in langchain_tools:
514
+ # 只處理 StructuredTool 或類似的有 args_schema 的工具
515
+ if not hasattr(mcp_tool, "args_schema") or not mcp_tool.args_schema:
516
+ logger.debug(
517
+ f"[_process_mcp_tools_for_anthropic] 工具 '{mcp_tool.name}' 沒有 args_schema,直接加入。"
518
+ )
519
+ processed_tools.append(mcp_tool)
520
+ continue
521
+
522
+ original_schema_dict = {}
523
+ try:
524
+ # 嘗試獲取 schema 字典 (根據 Pydantic 版本可能不同)
525
+ if hasattr(mcp_tool.args_schema, "model_json_schema"): # Pydantic V2
526
+ original_schema_dict = mcp_tool.args_schema.model_json_schema()
527
+ elif hasattr(mcp_tool.args_schema, "schema"): # Pydantic V1
528
+ original_schema_dict = mcp_tool.args_schema.schema()
529
+ elif isinstance(mcp_tool.args_schema, dict): # 已經是字典?
530
+ original_schema_dict = mcp_tool.args_schema
531
+ else:
532
+ logger.warning(
533
+ f"[_process_mcp_tools_for_anthropic] 無法獲取工具 '{mcp_tool.name}' 的 schema 字典 ({type(mcp_tool.args_schema)}),跳過轉換。"
534
+ )
535
+ processed_tools.append(mcp_tool)
536
+ continue
537
+
538
+ # 進行轉換檢查
539
+ logger.debug(
540
+ f"[_process_mcp_tools_for_anthropic] 檢查工具 '{mcp_tool.name}' 的 schema..."
541
+ )
542
+ new_schema_dict, transformed, desc_notes = (
543
+ transform_anthropic_incompatible_schema(
544
+ copy.deepcopy(original_schema_dict) # 使用深拷貝操作
545
+ )
546
+ )
547
+
548
+ if transformed:
549
+ mcp_tool.description += desc_notes
550
+ logger.info(
551
+ f"[_process_mcp_tools_for_anthropic] 工具 '{mcp_tool.name}' 的描述已更新。"
552
+ )
553
+ if isinstance(mcp_tool.args_schema, dict):
554
+ logger.debug(
555
+ f"[_process_mcp_tools_for_anthropic] args_schema 是字典,直接替換工具 '{mcp_tool.name}' 的 schema。"
556
+ )
557
+ mcp_tool.args_schema = new_schema_dict
558
+ else:
559
+ # 如果 args_schema 是 Pydantic 模型,直接修改可能無效或困難
560
+ # 附加轉換後的字典可能是一種備選方案,但 Langchain/LangGraph 可能不直接使用它
561
+ # 最好的方法是確保 get_tools 返回的工具的 args_schema 可以被修改,
562
+ # 或者在創建工具時就使用轉換後的 schema。
563
+ # 如果不能直接修改,附加屬性是一種標記方式,但可能需要在工具調用處處理。
564
+ logger.warning(
565
+ f"[_process_mcp_tools_for_anthropic] args_schema 不是字典 ({type(mcp_tool.args_schema)}),僅添加 _transformed_args_schema_dict 屬性到工具 '{mcp_tool.name}'。這可能不足以解決根本問題。"
566
+ )
567
+ setattr(mcp_tool, "_transformed_args_schema_dict", new_schema_dict)
568
+ processed_tools.append(mcp_tool)
569
+
570
+ except Exception as e_schema:
571
+ logger.error(
572
+ f"[_process_mcp_tools_for_anthropic] 處理工具 '{mcp_tool.name}' schema 時發生錯誤: {e_schema}",
573
+ exc_info=True,
574
+ )
575
+ processed_tools.append(mcp_tool) # 保留原始工具
576
+
577
+ logger.info(
578
+ f"[_process_mcp_tools_for_anthropic] --- 完成工具處理,返回 {len(processed_tools)} 個工具 ---"
579
+ )
580
+ return processed_tools
581
+
582
+
583
+ async def create_react_agent_graph(
584
+ system_prompt: str = "",
585
+ botrun_flow_lang_url: str = "",
586
+ user_id: str = "",
587
+ model_name: str = "",
588
+ lang: str = LANG_EN,
589
+ mcp_config: Optional[Dict[str, Any]] = None, # <--- 接收配置而非客戶端實例
590
+ ):
591
+ """
592
+ Create a react agent graph with simplified architecture.
593
+
594
+ This function now creates a fully MCP-integrated agent with:
595
+ - Direct system prompt usage (no language-specific prompt concatenation)
596
+ - Zero local tools - all functionality provided by MCP server
597
+ - Complete MCP server integration for all tools (web search, scraping, PDF/image analysis, time/date, visualizations, etc.)
598
+ - Removed all complex conditional logic and local tool definitions
599
+
600
+ Args:
601
+ system_prompt: The system prompt to use for the agent (used directly, no concatenation)
602
+ botrun_flow_lang_url: URL for botrun flow lang service (reserved for future use)
603
+ user_id: User identifier (reserved for future use)
604
+ model_name: AI model name to use (defaults to claude-sonnet-4-5-20250929)
605
+ lang: Language code affecting language-specific tools (e.g., "en", "zh-TW")
606
+ mcp_config: MCP servers configuration dict providing tools like scrape, chat_with_pdf, etc.
607
+
608
+ Returns:
609
+ A LangGraph react agent configured with simplified architecture
610
+
611
+ Note:
612
+ - Local MCP tools (scrape, chat_with_pdf, etc.) have been removed
613
+ - compare_date_time tool has been completely removed
614
+ - All advanced tools are now provided via MCP server configuration
615
+ - Language-specific prompts have been removed for simplification
616
+ """
617
+
618
+ # Complete MCP migration - all tools are now provided by MCP server
619
+ # No local tools remain - all functionality accessed via mcp_config
620
+ tools = [
621
+ # ✅ ALL MIGRATED TO MCP: scrape, chat_with_pdf, chat_with_imgs, generate_image,
622
+ # generate_tmp_public_url, create_html_page, create_plotly_chart,
623
+ # create_mermaid_diagram, current_date_time, web_search
624
+ # REMOVED: compare_date_time (completely eliminated)
625
+ ]
626
+
627
+ mcp_tools = []
628
+ if mcp_config:
629
+ logger.info("偵測到 MCP 配置,直接創建 MCP 工具...")
630
+ try:
631
+ # 直接創建 MCP client 並獲取工具,不使用 context manager
632
+
633
+ client = MultiServerMCPClient(mcp_config)
634
+ raw_mcp_tools = await client.get_tools()
635
+ print("raw_mcp_tools============>", raw_mcp_tools)
636
+
637
+ if raw_mcp_tools:
638
+ logger.info(f" MCP 配置獲取了 {len(raw_mcp_tools)} 個原始工具。")
639
+ # 處理 Schema (使用提取的輔助函數)
640
+ mcp_tools = _process_mcp_tools_for_anthropic(raw_mcp_tools)
641
+ if mcp_tools:
642
+ tools.extend(mcp_tools)
643
+ logger.info(f"已加入 {len(mcp_tools)} 個處理後的 MCP 工具。")
644
+ logger.debug(
645
+ f"加入的 MCP 工具名稱: {[tool.name for tool in mcp_tools]}"
646
+ )
647
+ else:
648
+ logger.warning("MCP 工具處理後列表為空。")
649
+ else:
650
+ logger.info("MCP Client 返回了空的工具列表。")
651
+
652
+ # 注意:我們不在這裡關閉 client,因為 tools 可能需要它來執行
653
+ # client 會在 graph 執行完畢後自動清理
654
+ logger.info("MCP client 和工具創建完成,client 將保持活動狀態")
655
+
656
+ except Exception as e_get:
657
+ import traceback
658
+
659
+ traceback.print_exc()
660
+ logger.error(f"從 MCP 配置獲取或處理工具時發生錯誤: {e_get}", exc_info=True)
661
+ # 即使出錯,也可能希望繼續執行(不帶 MCP 工具)
662
+ else:
663
+ logger.info("未提供 MCP 配置,跳過 MCP 工具。")
664
+
665
+ # Simplified: use user-provided system_prompt directly (no language-specific prompts)
666
+ new_system_prompt = system_prompt
667
+ if botrun_flow_lang_url and user_id:
668
+ new_system_prompt = (
669
+ f"""IMPORTANT: Any URL returned by tools MUST be included in your response as a markdown link [text](URL).
670
+ Please use the standard [text](URL) format to present links, ensuring the link text remains plain and unformatted.
671
+ Example:
672
+ User: "Create a new page for our project documentation"
673
+ Tool returns: {{"page_url": "https://notion.so/workspace/abc123"}}
674
+ Assistant: "I've created the new page for your project documentation. You can access it here: [Project Documentation](https://notion.so/workspace/abc123)"
675
+ """
676
+ + system_prompt
677
+ + f"""\n\n
678
+ - If the tool needs parameter like botrun_flow_lang_url or user_id, please use the following:
679
+ botrun_flow_lang_url: {botrun_flow_lang_url}
680
+ user_id: {user_id}
681
+ """
682
+ )
683
+ system_message = SystemMessage(
684
+ content=[
685
+ {
686
+ "text": new_system_prompt,
687
+ "type": "text",
688
+ "cache_control": {"type": "ephemeral"},
689
+ }
690
+ ]
691
+ )
692
+
693
+ # 目前先使用了 https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
694
+ # 這一段會遇到
695
+ # File "/Users/seba/Projects/botrun_flow_lang/.venv/lib/python3.11/site-packages/langgraph/prebuilt/tool_node.py", line 218, in __init__
696
+ # tool_ = create_tool(tool_)
697
+ # ^^^^^^^^^^^^^^^^^^
698
+ # File "/Users/seba/Projects/botrun_flow_lang/.venv/lib/python3.11/site-packages/langchain_core/tools/convert.py", line 334, in tool
699
+ # raise ValueError(msg)
700
+ # ValueError: The first argument must be a string or a callable with a __name__ for tool decorator. Got <class 'dict'>
701
+ # 所以先不使用這一段,這一段是參考 https://python.langchain.com/docs/integrations/chat/anthropic/#tools
702
+ # 也許未來可以引用
703
+ # if get_react_agent_model_name(model_name).startswith("claude-"):
704
+ # new_tools = []
705
+ # for tool in tools:
706
+ # new_tool = convert_to_anthropic_tool(tool)
707
+ # new_tool["cache_control"] = {"type": "ephemeral"}
708
+ # new_tools.append(new_tool)
709
+ # tools = new_tools
710
+
711
+ env_name = os.getenv("ENV_NAME", "botrun-flow-lang-dev")
712
+ result = create_react_agent(
713
+ get_react_agent_model(model_name),
714
+ tools=tools,
715
+ prompt=system_message,
716
+ checkpointer=MemorySaver(), # 如果要執行在 botrun_back 裡面,就不需要 firestore 的 checkpointer
717
+ # checkpointer=AsyncFirestoreCheckpointer(env_name=env_name),
718
+ )
719
+ return result
720
+
721
+
722
+ # Default graph instance with empty prompt
723
+ # if True:
724
+ # react_agent_graph = create_react_agent_graph()
725
+ # LangGraph Studio 測試用,把以下 un-comment 就可以測試
726
+ # react_agent_graph = create_react_agent_graph(
727
+ # system_prompt="",
728
+ # botrun_flow_lang_url="https://botrun-flow-lang-fastapi-dev-36186877499.asia-east1.run.app",
729
+ # user_id="sebastian.hsu@gmail.com",
730
+ # )