camel-ai 0.2.65__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

Files changed (505) hide show
  1. camel/__init__.py +3 -3
  2. camel/agents/__init__.py +2 -2
  3. camel/agents/_types.py +9 -4
  4. camel/agents/_utils.py +40 -2
  5. camel/agents/base.py +2 -2
  6. camel/agents/chat_agent.py +4835 -947
  7. camel/agents/critic_agent.py +2 -2
  8. camel/agents/deductive_reasoner_agent.py +56 -56
  9. camel/agents/embodied_agent.py +2 -2
  10. camel/agents/knowledge_graph_agent.py +20 -20
  11. camel/agents/mcp_agent.py +35 -36
  12. camel/agents/multi_hop_generator_agent.py +3 -3
  13. camel/agents/programmed_agent_instruction.py +2 -2
  14. camel/agents/repo_agent.py +4 -3
  15. camel/agents/role_assignment_agent.py +2 -2
  16. camel/agents/search_agent.py +2 -2
  17. camel/agents/task_agent.py +2 -2
  18. camel/agents/tool_agents/__init__.py +2 -2
  19. camel/agents/tool_agents/base.py +2 -2
  20. camel/agents/tool_agents/hugging_face_tool_agent.py +3 -3
  21. camel/benchmarks/__init__.py +2 -2
  22. camel/benchmarks/apibank.py +5 -5
  23. camel/benchmarks/apibench.py +2 -2
  24. camel/benchmarks/base.py +2 -2
  25. camel/benchmarks/browsecomp.py +44 -33
  26. camel/benchmarks/gaia.py +17 -13
  27. camel/benchmarks/mock_website/README.md +1 -3
  28. camel/benchmarks/mock_website/mock_web.py +2 -2
  29. camel/benchmarks/mock_website/requirements.txt +1 -1
  30. camel/benchmarks/mock_website/shopping_mall/app.py +2 -2
  31. camel/benchmarks/mock_website/task.json +1 -1
  32. camel/benchmarks/nexus.py +3 -3
  33. camel/benchmarks/ragbench.py +2 -2
  34. camel/bots/__init__.py +2 -2
  35. camel/bots/discord/__init__.py +2 -2
  36. camel/bots/discord/discord_app.py +2 -2
  37. camel/bots/discord/discord_installation.py +2 -2
  38. camel/bots/discord/discord_store.py +3 -3
  39. camel/bots/slack/__init__.py +2 -2
  40. camel/bots/slack/models.py +4 -4
  41. camel/bots/slack/slack_app.py +2 -2
  42. camel/bots/telegram_bot.py +2 -2
  43. camel/configs/__init__.py +23 -2
  44. camel/configs/aihubmix_config.py +90 -0
  45. camel/configs/aiml_config.py +2 -2
  46. camel/configs/amd_config.py +70 -0
  47. camel/configs/anthropic_config.py +2 -2
  48. camel/configs/base_config.py +2 -2
  49. camel/configs/bedrock_config.py +5 -3
  50. camel/configs/cerebras_config.py +98 -0
  51. camel/configs/cohere_config.py +2 -2
  52. camel/configs/cometapi_config.py +106 -0
  53. camel/configs/crynux_config.py +2 -2
  54. camel/configs/deepseek_config.py +9 -8
  55. camel/configs/gemini_config.py +6 -4
  56. camel/configs/groq_config.py +6 -4
  57. camel/configs/internlm_config.py +6 -4
  58. camel/configs/litellm_config.py +2 -2
  59. camel/configs/lmstudio_config.py +6 -4
  60. camel/configs/minimax_config.py +95 -0
  61. camel/configs/mistral_config.py +2 -2
  62. camel/configs/modelscope_config.py +5 -3
  63. camel/configs/moonshot_config.py +2 -2
  64. camel/configs/nebius_config.py +105 -0
  65. camel/configs/netmind_config.py +2 -2
  66. camel/configs/novita_config.py +2 -2
  67. camel/configs/nvidia_config.py +2 -2
  68. camel/configs/ollama_config.py +2 -2
  69. camel/configs/openai_config.py +5 -3
  70. camel/configs/openrouter_config.py +6 -4
  71. camel/configs/ppio_config.py +2 -2
  72. camel/configs/qianfan_config.py +85 -0
  73. camel/configs/qwen_config.py +2 -2
  74. camel/configs/reka_config.py +2 -2
  75. camel/configs/samba_config.py +6 -4
  76. camel/configs/sglang_config.py +2 -2
  77. camel/configs/siliconflow_config.py +2 -2
  78. camel/configs/togetherai_config.py +2 -2
  79. camel/configs/vllm_config.py +4 -2
  80. camel/configs/watsonx_config.py +2 -2
  81. camel/configs/yi_config.py +6 -4
  82. camel/configs/zhipuai_config.py +6 -4
  83. camel/data_collectors/__init__.py +2 -2
  84. camel/data_collectors/alpaca_collector.py +18 -9
  85. camel/data_collectors/base.py +2 -2
  86. camel/data_collectors/sharegpt_collector.py +2 -2
  87. camel/datagen/__init__.py +2 -2
  88. camel/datagen/cot_datagen.py +3 -3
  89. camel/datagen/evol_instruct/__init__.py +2 -2
  90. camel/datagen/evol_instruct/evol_instruct.py +2 -2
  91. camel/datagen/evol_instruct/scorer.py +12 -12
  92. camel/datagen/evol_instruct/templates.py +16 -16
  93. camel/datagen/self_improving_cot.py +5 -5
  94. camel/datagen/self_instruct/__init__.py +2 -2
  95. camel/datagen/self_instruct/filter/__init__.py +2 -2
  96. camel/datagen/self_instruct/filter/filter_function.py +2 -2
  97. camel/datagen/self_instruct/filter/filter_registry.py +2 -2
  98. camel/datagen/self_instruct/filter/instruction_filter.py +2 -2
  99. camel/datagen/self_instruct/self_instruct.py +2 -2
  100. camel/datagen/self_instruct/templates.py +47 -47
  101. camel/datagen/source2synth/__init__.py +2 -2
  102. camel/datagen/source2synth/data_processor.py +2 -2
  103. camel/datagen/source2synth/models.py +2 -2
  104. camel/datagen/source2synth/user_data_processor_config.py +2 -2
  105. camel/datahubs/__init__.py +2 -2
  106. camel/datahubs/base.py +2 -2
  107. camel/datahubs/huggingface.py +2 -2
  108. camel/datahubs/models.py +2 -2
  109. camel/datasets/__init__.py +2 -2
  110. camel/datasets/base_generator.py +41 -12
  111. camel/datasets/few_shot_generator.py +18 -18
  112. camel/datasets/models.py +2 -2
  113. camel/datasets/self_instruct_generator.py +2 -2
  114. camel/datasets/static_dataset.py +2 -2
  115. camel/embeddings/__init__.py +2 -2
  116. camel/embeddings/azure_embedding.py +2 -2
  117. camel/embeddings/base.py +2 -2
  118. camel/embeddings/gemini_embedding.py +2 -2
  119. camel/embeddings/jina_embedding.py +2 -2
  120. camel/embeddings/mistral_embedding.py +2 -2
  121. camel/embeddings/openai_compatible_embedding.py +2 -2
  122. camel/embeddings/openai_embedding.py +2 -2
  123. camel/embeddings/sentence_transformers_embeddings.py +2 -2
  124. camel/embeddings/together_embedding.py +2 -2
  125. camel/embeddings/vlm_embedding.py +2 -2
  126. camel/environments/__init__.py +14 -2
  127. camel/environments/models.py +2 -2
  128. camel/environments/multi_step.py +2 -2
  129. camel/environments/rlcards_env.py +860 -0
  130. camel/environments/single_step.py +30 -5
  131. camel/environments/tic_tac_toe.py +3 -3
  132. camel/extractors/__init__.py +2 -2
  133. camel/extractors/base.py +2 -2
  134. camel/extractors/python_strategies.py +2 -2
  135. camel/generators.py +2 -2
  136. camel/human.py +2 -2
  137. camel/interpreters/__init__.py +4 -2
  138. camel/interpreters/base.py +2 -2
  139. camel/interpreters/docker/Dockerfile +14 -24
  140. camel/interpreters/docker_interpreter.py +5 -4
  141. camel/interpreters/e2b_interpreter.py +36 -3
  142. camel/interpreters/internal_python_interpreter.py +53 -4
  143. camel/interpreters/interpreter_error.py +2 -2
  144. camel/interpreters/ipython_interpreter.py +2 -2
  145. camel/interpreters/microsandbox_interpreter.py +395 -0
  146. camel/interpreters/subprocess_interpreter.py +2 -2
  147. camel/loaders/__init__.py +13 -4
  148. camel/loaders/apify_reader.py +2 -2
  149. camel/loaders/base_io.py +2 -2
  150. camel/loaders/base_loader.py +85 -0
  151. camel/loaders/chunkr_reader.py +11 -2
  152. camel/loaders/crawl4ai_reader.py +2 -2
  153. camel/loaders/firecrawl_reader.py +6 -6
  154. camel/loaders/jina_url_reader.py +2 -2
  155. camel/loaders/markitdown.py +2 -2
  156. camel/loaders/mineru_extractor.py +2 -2
  157. camel/loaders/mistral_reader.py +2 -2
  158. camel/loaders/scrapegraph_reader.py +2 -2
  159. camel/loaders/unstructured_io.py +2 -2
  160. camel/logger.py +5 -5
  161. camel/memories/__init__.py +2 -2
  162. camel/memories/agent_memories.py +86 -3
  163. camel/memories/base.py +36 -2
  164. camel/memories/blocks/__init__.py +2 -2
  165. camel/memories/blocks/chat_history_block.py +125 -7
  166. camel/memories/blocks/vectordb_block.py +10 -3
  167. camel/memories/context_creators/__init__.py +2 -2
  168. camel/memories/context_creators/score_based.py +31 -239
  169. camel/memories/records.py +90 -10
  170. camel/messages/__init__.py +2 -2
  171. camel/messages/base.py +178 -43
  172. camel/messages/conversion/__init__.py +2 -2
  173. camel/messages/conversion/alpaca.py +2 -2
  174. camel/messages/conversion/conversation_models.py +2 -2
  175. camel/messages/conversion/sharegpt/__init__.py +2 -2
  176. camel/messages/conversion/sharegpt/function_call_formatter.py +2 -2
  177. camel/messages/conversion/sharegpt/hermes/__init__.py +2 -2
  178. camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +2 -2
  179. camel/messages/func_message.py +54 -17
  180. camel/models/__init__.py +16 -2
  181. camel/models/_utils.py +3 -3
  182. camel/models/aihubmix_model.py +83 -0
  183. camel/models/aiml_model.py +11 -18
  184. camel/models/amd_model.py +101 -0
  185. camel/models/anthropic_model.py +127 -20
  186. camel/models/aws_bedrock_model.py +12 -35
  187. camel/models/azure_openai_model.py +212 -89
  188. camel/models/base_audio_model.py +5 -3
  189. camel/models/base_model.py +195 -26
  190. camel/models/cerebras_model.py +83 -0
  191. camel/models/cohere_model.py +16 -21
  192. camel/models/cometapi_model.py +83 -0
  193. camel/models/crynux_model.py +11 -18
  194. camel/models/deepseek_model.py +18 -58
  195. camel/models/fish_audio_model.py +8 -2
  196. camel/models/gemini_model.py +389 -26
  197. camel/models/groq_model.py +11 -19
  198. camel/models/internlm_model.py +11 -18
  199. camel/models/litellm_model.py +56 -34
  200. camel/models/lmstudio_model.py +17 -20
  201. camel/models/minimax_model.py +83 -0
  202. camel/models/mistral_model.py +18 -19
  203. camel/models/model_factory.py +37 -3
  204. camel/models/model_manager.py +26 -8
  205. camel/models/modelscope_model.py +13 -193
  206. camel/models/moonshot_model.py +195 -21
  207. camel/models/nebius_model.py +83 -0
  208. camel/models/nemotron_model.py +19 -9
  209. camel/models/netmind_model.py +11 -18
  210. camel/models/novita_model.py +11 -18
  211. camel/models/nvidia_model.py +11 -18
  212. camel/models/ollama_model.py +14 -21
  213. camel/models/openai_audio_models.py +2 -2
  214. camel/models/openai_compatible_model.py +188 -45
  215. camel/models/openai_model.py +216 -71
  216. camel/models/openrouter_model.py +11 -19
  217. camel/models/ppio_model.py +11 -18
  218. camel/models/qianfan_model.py +89 -0
  219. camel/models/qwen_model.py +13 -193
  220. camel/models/reka_model.py +21 -21
  221. camel/models/reward/__init__.py +2 -2
  222. camel/models/reward/base_reward_model.py +2 -2
  223. camel/models/reward/evaluator.py +2 -2
  224. camel/models/reward/nemotron_model.py +2 -2
  225. camel/models/reward/skywork_model.py +2 -2
  226. camel/models/samba_model.py +48 -47
  227. camel/models/sglang_model.py +88 -40
  228. camel/models/siliconflow_model.py +12 -35
  229. camel/models/stub_model.py +10 -7
  230. camel/models/togetherai_model.py +11 -18
  231. camel/models/vllm_model.py +10 -18
  232. camel/models/volcano_model.py +16 -20
  233. camel/models/watsonx_model.py +7 -19
  234. camel/models/yi_model.py +11 -18
  235. camel/models/zhipuai_model.py +70 -18
  236. camel/parsers/__init__.py +18 -0
  237. camel/parsers/mcp_tool_call_parser.py +176 -0
  238. camel/personas/__init__.py +2 -2
  239. camel/personas/persona.py +2 -2
  240. camel/personas/persona_hub.py +2 -2
  241. camel/prompts/__init__.py +2 -2
  242. camel/prompts/ai_society.py +2 -2
  243. camel/prompts/base.py +2 -2
  244. camel/prompts/code.py +2 -2
  245. camel/prompts/evaluation.py +2 -2
  246. camel/prompts/generate_text_embedding_data.py +2 -2
  247. camel/prompts/image_craft.py +2 -2
  248. camel/prompts/misalignment.py +2 -2
  249. camel/prompts/multi_condition_image_craft.py +2 -2
  250. camel/prompts/object_recognition.py +2 -2
  251. camel/prompts/persona_hub.py +3 -3
  252. camel/prompts/prompt_templates.py +2 -2
  253. camel/prompts/role_description_prompt_template.py +2 -2
  254. camel/prompts/solution_extraction.py +8 -8
  255. camel/prompts/task_prompt_template.py +2 -2
  256. camel/prompts/translation.py +2 -2
  257. camel/prompts/video_description_prompt.py +3 -3
  258. camel/responses/__init__.py +2 -2
  259. camel/responses/agent_responses.py +2 -2
  260. camel/retrievers/__init__.py +2 -2
  261. camel/retrievers/auto_retriever.py +3 -2
  262. camel/retrievers/base.py +2 -2
  263. camel/retrievers/bm25_retriever.py +2 -2
  264. camel/retrievers/cohere_rerank_retriever.py +2 -2
  265. camel/retrievers/hybrid_retrival.py +2 -2
  266. camel/retrievers/vector_retriever.py +2 -2
  267. camel/runtimes/Dockerfile.multi-toolkit +90 -0
  268. camel/runtimes/__init__.py +2 -2
  269. camel/runtimes/api.py +79 -23
  270. camel/runtimes/base.py +2 -2
  271. camel/runtimes/configs.py +13 -13
  272. camel/runtimes/daytona_runtime.py +17 -18
  273. camel/runtimes/docker_runtime.py +12 -12
  274. camel/runtimes/llm_guard_runtime.py +26 -26
  275. camel/runtimes/remote_http_runtime.py +11 -11
  276. camel/runtimes/ubuntu_docker_runtime.py +2 -2
  277. camel/runtimes/utils/__init__.py +2 -2
  278. camel/runtimes/utils/function_risk_toolkit.py +2 -2
  279. camel/runtimes/utils/ignore_risk_toolkit.py +2 -2
  280. camel/schemas/__init__.py +2 -2
  281. camel/schemas/base.py +2 -2
  282. camel/schemas/openai_converter.py +3 -3
  283. camel/schemas/outlines_converter.py +2 -2
  284. camel/services/agent_openapi_server.py +380 -0
  285. camel/societies/__init__.py +4 -2
  286. camel/societies/babyagi_playing.py +2 -2
  287. camel/societies/role_playing.py +201 -80
  288. camel/societies/workforce/__init__.py +10 -3
  289. camel/societies/workforce/base.py +2 -2
  290. camel/societies/workforce/events.py +143 -0
  291. camel/societies/workforce/prompts.py +258 -33
  292. camel/societies/workforce/role_playing_worker.py +88 -31
  293. camel/societies/workforce/single_agent_worker.py +638 -40
  294. camel/societies/workforce/structured_output_handler.py +512 -0
  295. camel/societies/workforce/task_channel.py +182 -38
  296. camel/societies/workforce/utils.py +780 -65
  297. camel/societies/workforce/worker.py +92 -26
  298. camel/societies/workforce/workflow_memory_manager.py +1746 -0
  299. camel/societies/workforce/workforce.py +5276 -355
  300. camel/societies/workforce/workforce_callback.py +103 -0
  301. camel/societies/workforce/workforce_logger.py +647 -0
  302. camel/societies/workforce/workforce_metrics.py +33 -0
  303. camel/storages/__init__.py +6 -2
  304. camel/storages/graph_storages/__init__.py +2 -2
  305. camel/storages/graph_storages/base.py +2 -2
  306. camel/storages/graph_storages/graph_element.py +2 -2
  307. camel/storages/graph_storages/nebula_graph.py +4 -4
  308. camel/storages/graph_storages/neo4j_graph.py +7 -7
  309. camel/storages/key_value_storages/__init__.py +2 -2
  310. camel/storages/key_value_storages/base.py +2 -2
  311. camel/storages/key_value_storages/in_memory.py +2 -2
  312. camel/storages/key_value_storages/json.py +17 -4
  313. camel/storages/key_value_storages/mem0_cloud.py +50 -49
  314. camel/storages/key_value_storages/redis.py +2 -2
  315. camel/storages/object_storages/__init__.py +2 -2
  316. camel/storages/object_storages/amazon_s3.py +2 -2
  317. camel/storages/object_storages/azure_blob.py +2 -2
  318. camel/storages/object_storages/base.py +2 -2
  319. camel/storages/object_storages/google_cloud.py +3 -3
  320. camel/storages/vectordb_storages/__init__.py +8 -2
  321. camel/storages/vectordb_storages/base.py +2 -2
  322. camel/storages/vectordb_storages/chroma.py +731 -0
  323. camel/storages/vectordb_storages/faiss.py +2 -2
  324. camel/storages/vectordb_storages/milvus.py +2 -2
  325. camel/storages/vectordb_storages/oceanbase.py +15 -15
  326. camel/storages/vectordb_storages/pgvector.py +349 -0
  327. camel/storages/vectordb_storages/qdrant.py +6 -6
  328. camel/storages/vectordb_storages/surreal.py +372 -0
  329. camel/storages/vectordb_storages/tidb.py +11 -8
  330. camel/storages/vectordb_storages/weaviate.py +2 -2
  331. camel/tasks/__init__.py +2 -2
  332. camel/tasks/task.py +348 -26
  333. camel/tasks/task_prompt.py +3 -3
  334. camel/terminators/__init__.py +2 -2
  335. camel/terminators/base.py +2 -2
  336. camel/terminators/response_terminator.py +2 -2
  337. camel/terminators/token_limit_terminator.py +2 -2
  338. camel/toolkits/__init__.py +54 -10
  339. camel/toolkits/aci_toolkit.py +66 -21
  340. camel/toolkits/arxiv_toolkit.py +8 -8
  341. camel/toolkits/ask_news_toolkit.py +2 -2
  342. camel/toolkits/async_browser_toolkit.py +4 -4
  343. camel/toolkits/audio_analysis_toolkit.py +3 -3
  344. camel/toolkits/base.py +65 -7
  345. camel/toolkits/bohrium_toolkit.py +2 -2
  346. camel/toolkits/browser_toolkit.py +34 -21
  347. camel/toolkits/browser_toolkit_commons.py +4 -4
  348. camel/toolkits/code_execution.py +31 -4
  349. camel/toolkits/context_summarizer_toolkit.py +684 -0
  350. camel/toolkits/craw4ai_toolkit.py +93 -0
  351. camel/toolkits/dappier_toolkit.py +12 -8
  352. camel/toolkits/data_commons_toolkit.py +2 -2
  353. camel/toolkits/dingtalk.py +1135 -0
  354. camel/toolkits/earth_science_toolkit.py +5367 -0
  355. camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
  356. camel/toolkits/excel_toolkit.py +905 -71
  357. camel/toolkits/file_toolkit.py +1402 -0
  358. camel/toolkits/function_tool.py +126 -18
  359. camel/toolkits/github_toolkit.py +109 -22
  360. camel/toolkits/gmail_toolkit.py +1839 -0
  361. camel/toolkits/google_calendar_toolkit.py +40 -6
  362. camel/toolkits/google_drive_mcp_toolkit.py +54 -0
  363. camel/toolkits/google_maps_toolkit.py +2 -2
  364. camel/toolkits/google_scholar_toolkit.py +2 -2
  365. camel/toolkits/human_toolkit.py +36 -12
  366. camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
  367. camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
  368. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
  369. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
  370. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  371. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4589 -0
  372. camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
  373. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  374. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1929 -0
  375. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
  376. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +589 -0
  377. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  378. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  379. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  380. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  381. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +129 -0
  382. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +27 -0
  383. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
  384. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1037 -0
  385. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  386. camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
  387. camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
  388. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
  389. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
  390. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
  391. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
  392. camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
  393. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
  394. camel/toolkits/image_analysis_toolkit.py +3 -6
  395. camel/toolkits/image_generation_toolkit.py +390 -0
  396. camel/toolkits/jina_reranker_toolkit.py +5 -6
  397. camel/toolkits/klavis_toolkit.py +7 -3
  398. camel/toolkits/linkedin_toolkit.py +2 -2
  399. camel/toolkits/markitdown_toolkit.py +104 -0
  400. camel/toolkits/math_toolkit.py +66 -12
  401. camel/toolkits/mcp_toolkit.py +412 -36
  402. camel/toolkits/memory_toolkit.py +7 -3
  403. camel/toolkits/meshy_toolkit.py +2 -2
  404. camel/toolkits/message_agent_toolkit.py +608 -0
  405. camel/toolkits/message_integration.py +724 -0
  406. camel/toolkits/mineru_toolkit.py +2 -2
  407. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  408. camel/toolkits/networkx_toolkit.py +2 -2
  409. camel/toolkits/note_taking_toolkit.py +277 -0
  410. camel/toolkits/notion_mcp_toolkit.py +224 -0
  411. camel/toolkits/notion_toolkit.py +2 -2
  412. camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
  413. camel/toolkits/open_api_specs/biztoc/ai-plugin.json +1 -1
  414. camel/toolkits/open_api_specs/coursera/__init__.py +2 -2
  415. camel/toolkits/open_api_specs/create_qr_code/__init__.py +2 -2
  416. camel/toolkits/open_api_specs/klarna/__init__.py +2 -2
  417. camel/toolkits/open_api_specs/nasa_apod/__init__.py +2 -2
  418. camel/toolkits/open_api_specs/outschool/__init__.py +2 -2
  419. camel/toolkits/open_api_specs/outschool/ai-plugin.json +1 -1
  420. camel/toolkits/open_api_specs/outschool/openapi.yaml +1 -1
  421. camel/toolkits/open_api_specs/outschool/paths/__init__.py +2 -2
  422. camel/toolkits/open_api_specs/outschool/paths/get_classes.py +2 -2
  423. camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +2 -2
  424. camel/toolkits/open_api_specs/security_config.py +2 -2
  425. camel/toolkits/open_api_specs/speak/__init__.py +2 -2
  426. camel/toolkits/open_api_specs/web_scraper/__init__.py +2 -2
  427. camel/toolkits/open_api_specs/web_scraper/ai-plugin.json +1 -1
  428. camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +2 -2
  429. camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +2 -2
  430. camel/toolkits/open_api_toolkit.py +2 -2
  431. camel/toolkits/openbb_toolkit.py +7 -3
  432. camel/toolkits/origene_mcp_toolkit.py +56 -0
  433. camel/toolkits/page_script.js +53 -53
  434. camel/toolkits/playwright_mcp_toolkit.py +13 -31
  435. camel/toolkits/pptx_toolkit.py +36 -23
  436. camel/toolkits/pubmed_toolkit.py +2 -2
  437. camel/toolkits/pulse_mcp_search_toolkit.py +2 -2
  438. camel/toolkits/pyautogui_toolkit.py +2 -2
  439. camel/toolkits/reddit_toolkit.py +2 -2
  440. camel/toolkits/resend_toolkit.py +168 -0
  441. camel/toolkits/retrieval_toolkit.py +2 -2
  442. camel/toolkits/screenshot_toolkit.py +213 -0
  443. camel/toolkits/search_toolkit.py +539 -146
  444. camel/toolkits/searxng_toolkit.py +2 -2
  445. camel/toolkits/semantic_scholar_toolkit.py +2 -2
  446. camel/toolkits/slack_toolkit.py +108 -58
  447. camel/toolkits/sql_toolkit.py +712 -0
  448. camel/toolkits/stripe_toolkit.py +2 -2
  449. camel/toolkits/sympy_toolkit.py +3 -3
  450. camel/toolkits/task_planning_toolkit.py +5 -5
  451. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  452. camel/toolkits/terminal_toolkit/terminal_toolkit.py +1070 -0
  453. camel/toolkits/terminal_toolkit/utils.py +532 -0
  454. camel/toolkits/thinking_toolkit.py +3 -3
  455. camel/toolkits/twitter_toolkit.py +2 -2
  456. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  457. camel/toolkits/video_analysis_toolkit.py +109 -29
  458. camel/toolkits/video_download_toolkit.py +19 -16
  459. camel/toolkits/weather_toolkit.py +2 -2
  460. camel/toolkits/web_deploy_toolkit.py +1219 -0
  461. camel/toolkits/wechat_official_toolkit.py +483 -0
  462. camel/toolkits/whatsapp_toolkit.py +2 -2
  463. camel/toolkits/wolfram_alpha_toolkit.py +2 -2
  464. camel/toolkits/zapier_toolkit.py +7 -3
  465. camel/types/__init__.py +4 -4
  466. camel/types/agents/__init__.py +2 -2
  467. camel/types/agents/tool_calling_record.py +6 -3
  468. camel/types/enums.py +378 -39
  469. camel/types/mcp_registries.py +2 -2
  470. camel/types/openai_types.py +4 -4
  471. camel/types/unified_model_type.py +38 -6
  472. camel/utils/__init__.py +2 -2
  473. camel/utils/async_func.py +2 -2
  474. camel/utils/chunker/__init__.py +2 -2
  475. camel/utils/chunker/base.py +2 -2
  476. camel/utils/chunker/code_chunker.py +2 -2
  477. camel/utils/chunker/uio_chunker.py +2 -2
  478. camel/utils/commons.py +38 -7
  479. camel/utils/constants.py +5 -2
  480. camel/utils/context_utils.py +1134 -0
  481. camel/utils/deduplication.py +2 -2
  482. camel/utils/filename.py +2 -2
  483. camel/utils/langfuse.py +2 -2
  484. camel/utils/mcp.py +140 -6
  485. camel/utils/mcp_client.py +48 -38
  486. camel/utils/message_summarizer.py +148 -0
  487. camel/utils/response_format.py +2 -2
  488. camel/utils/token_counting.py +45 -22
  489. camel/utils/tool_result.py +44 -0
  490. camel/verifiers/__init__.py +2 -2
  491. camel/verifiers/base.py +2 -2
  492. camel/verifiers/math_verifier.py +2 -2
  493. camel/verifiers/models.py +2 -2
  494. camel/verifiers/physics_verifier.py +2 -2
  495. camel/verifiers/python_verifier.py +2 -2
  496. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/METADATA +327 -94
  497. camel_ai-0.2.82.dist-info/RECORD +507 -0
  498. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/WHEEL +1 -1
  499. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/licenses/LICENSE +1 -1
  500. camel/loaders/pandas_reader.py +0 -368
  501. camel/toolkits/dalle_toolkit.py +0 -175
  502. camel/toolkits/file_write_toolkit.py +0 -444
  503. camel/toolkits/openai_agent_toolkit.py +0 -135
  504. camel/toolkits/terminal_toolkit.py +0 -1037
  505. camel_ai-0.2.65.dist-info/RECORD +0 -426
@@ -0,0 +1,1839 @@
1
+ # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
14
+
15
+ import os
16
+ import re
17
+ from typing import Any, Dict, List, Literal, Optional, Union
18
+
19
+ from camel.logger import get_logger
20
+ from camel.toolkits import FunctionTool
21
+ from camel.toolkits.base import BaseToolkit
22
+ from camel.utils import MCPServer
23
+
24
+ logger = get_logger(__name__)
25
+
26
+ SCOPES = [
27
+ 'https://www.googleapis.com/auth/gmail.readonly',
28
+ 'https://www.googleapis.com/auth/gmail.send',
29
+ 'https://www.googleapis.com/auth/gmail.modify',
30
+ 'https://www.googleapis.com/auth/gmail.compose',
31
+ 'https://www.googleapis.com/auth/gmail.labels',
32
+ 'https://www.googleapis.com/auth/contacts.readonly',
33
+ 'https://www.googleapis.com/auth/userinfo.profile',
34
+ ]
35
+
36
+
37
+ @MCPServer()
38
+ class GmailToolkit(BaseToolkit):
39
+ r"""A comprehensive toolkit for Gmail operations.
40
+
41
+ This class provides methods for Gmail operations including sending emails,
42
+ managing drafts, fetching messages, managing labels, and handling contacts.
43
+ API keys can be accessed in google cloud console (https://console.cloud.google.com/)
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ timeout: Optional[float] = None,
49
+ ):
50
+ r"""Initializes a new instance of the GmailToolkit class.
51
+
52
+ Args:
53
+ timeout (Optional[float]): The timeout value for API requests
54
+ in seconds. If None, no timeout is applied.
55
+ (default: :obj:`None`)
56
+ """
57
+ super().__init__(timeout=timeout)
58
+
59
+ self._credentials = self._authenticate()
60
+
61
+ self.gmail_service: Any = self._get_gmail_service()
62
+ self._people_service: Any = None
63
+
64
+ @property
65
+ def people_service(self) -> Any:
66
+ r"""Lazily initialize and return the Google People service."""
67
+ if self._people_service is None:
68
+ self._people_service = self._get_people_service()
69
+ return self._people_service
70
+
71
+ @people_service.setter
72
+ def people_service(self, service: Any) -> None:
73
+ r"""Allow overriding/injecting the People service (e.g., in tests)."""
74
+ self._people_service = service
75
+
76
+ def gmail_send_email(
77
+ self,
78
+ to: Union[str, List[str]],
79
+ subject: str,
80
+ body: str,
81
+ cc: Optional[Union[str, List[str]]] = None,
82
+ bcc: Optional[Union[str, List[str]]] = None,
83
+ attachments: Optional[List[str]] = None,
84
+ is_html: bool = False,
85
+ ) -> Dict[str, Any]:
86
+ r"""Send an email through Gmail.
87
+
88
+ Args:
89
+ to (Union[str, List[str]]): Recipient email address(es).
90
+ subject (str): Email subject.
91
+ body (str): Email body content.
92
+ cc (Optional[Union[str, List[str]]]): CC recipient email
93
+ address(es).
94
+ bcc (Optional[Union[str, List[str]]]): BCC recipient email
95
+ address(es).
96
+ attachments (Optional[List[str]]): List of file paths to attach.
97
+ is_html (bool): Whether the body is HTML format. Set to True when
98
+ sending formatted emails with HTML tags (e.g., bold,
99
+ links, images). Use False (default) for plain text emails.
100
+
101
+ Returns:
102
+ Dict[str, Any]: A dictionary containing the result of the
103
+ operation.
104
+ """
105
+ try:
106
+ # Normalize recipients to lists
107
+ to_list = [to] if isinstance(to, str) else to
108
+ cc_list = [cc] if isinstance(cc, str) else (cc or [])
109
+ bcc_list = [bcc] if isinstance(bcc, str) else (bcc or [])
110
+
111
+ # Validate email addresses
112
+ all_recipients = to_list + cc_list + bcc_list
113
+ for email in all_recipients:
114
+ if not self._is_valid_email(email):
115
+ return {"error": f"Invalid email address: {email}"}
116
+
117
+ # Create message
118
+ message = self._create_message(
119
+ to_list, subject, body, cc_list, bcc_list, attachments, is_html
120
+ )
121
+
122
+ # Send message
123
+ sent_message = (
124
+ self.gmail_service.users()
125
+ .messages()
126
+ .send(userId='me', body=message)
127
+ .execute()
128
+ )
129
+
130
+ return {
131
+ "success": True,
132
+ "message_id": sent_message.get('id'),
133
+ "thread_id": sent_message.get('threadId'),
134
+ "message": "Email sent successfully",
135
+ }
136
+
137
+ except Exception as e:
138
+ logger.error("Failed to send email: %s", e)
139
+ return {"error": f"Failed to send email: {e!s}"}
140
+
141
+ def gmail_reply_to_email(
142
+ self,
143
+ message_id: str,
144
+ reply_body: str,
145
+ reply_all: bool = False,
146
+ is_html: bool = False,
147
+ ) -> Dict[str, Any]:
148
+ r"""Reply to an email message.
149
+
150
+ Args:
151
+ message_id (str): The unique identifier of the message to reply to.
152
+ To get a message ID, first use fetch_emails() to list messages,
153
+ or use the 'message_id' returned from send_email() or
154
+ create_email_draft().
155
+ reply_body (str): The reply message body.
156
+ reply_all (bool): Whether to reply to all recipients.
157
+ is_html (bool): Whether the body is HTML format. Set to True when
158
+ sending formatted emails with HTML tags (e.g., bold,
159
+ links, images). Use False (default) for plain text emails.
160
+
161
+ Returns:
162
+ Dict[str, Any]: A dictionary containing the result of the
163
+ operation.
164
+ """
165
+ try:
166
+ # Get the original message
167
+ original_message = (
168
+ self.gmail_service.users()
169
+ .messages()
170
+ .get(userId='me', id=message_id)
171
+ .execute()
172
+ )
173
+
174
+ # Extract headers (single pass, case-insensitive)
175
+ headers = original_message['payload'].get('headers', [])
176
+ subject = from_email = to_emails = cc_emails = None
177
+ missing = {'subject', 'from', 'to', 'cc'}
178
+
179
+ for header in headers:
180
+ name = (header.get('name') or '').lower()
181
+ if name not in missing:
182
+ continue
183
+ value = header.get('value')
184
+ if name == 'subject':
185
+ subject = value
186
+ elif name == 'from':
187
+ from_email = value
188
+ elif name == 'to':
189
+ to_emails = value
190
+ elif name == 'cc':
191
+ cc_emails = value
192
+ missing.discard(name)
193
+ if not missing:
194
+ break
195
+
196
+ # Extract identifiers for reply context
197
+ message_id_header = self._get_header_value(
198
+ headers, 'Message-Id'
199
+ ) or self._get_header_value(headers, 'Message-ID')
200
+ thread_id = original_message.get('threadId')
201
+
202
+ # Prepare reply subject
203
+ if subject and not subject.startswith('Re: '):
204
+ subject = f"Re: {subject}"
205
+ elif not subject:
206
+ subject = "Re: (No Subject)"
207
+
208
+ # Validate from_email
209
+ if not from_email:
210
+ return {"error": "Original message has no sender address"}
211
+
212
+ # Prepare recipients
213
+ if reply_all:
214
+ recipients = [from_email]
215
+ if to_emails:
216
+ recipients.extend(
217
+ [email.strip() for email in to_emails.split(',')]
218
+ )
219
+ if cc_emails:
220
+ recipients.extend(
221
+ [email.strip() for email in cc_emails.split(',')]
222
+ )
223
+ # Remove duplicates and None values
224
+ recipients = [r for r in list(set(recipients)) if r]
225
+
226
+ # Get current user's email and remove it from recipients
227
+ try:
228
+ profile_result = self.gmail_get_profile()
229
+ if profile_result.get('success'):
230
+ current_user_email = profile_result['profile'][
231
+ 'email_address'
232
+ ]
233
+ # Remove current user from recipients (handle both
234
+ # plain email and "Name <email>" format)
235
+ filtered_recipients = []
236
+ for email in recipients:
237
+ # Extract email from "Name <email>" format
238
+ match = re.search(r'<([^>]+)>$', email.strip())
239
+ email_addr = (
240
+ match.group(1) if match else email.strip()
241
+ )
242
+ if email_addr != current_user_email:
243
+ filtered_recipients.append(email)
244
+ recipients = filtered_recipients
245
+ except Exception as e:
246
+ logger.warning(
247
+ "Could not get current user email to filter from "
248
+ "recipients: %s",
249
+ e,
250
+ )
251
+ else:
252
+ recipients = [from_email]
253
+
254
+ # Create reply message with reply headers
255
+ message = self._create_message(
256
+ recipients,
257
+ subject,
258
+ reply_body,
259
+ is_html=is_html,
260
+ in_reply_to=message_id_header or original_message.get('id'),
261
+ references=[message_id_header] if message_id_header else None,
262
+ )
263
+
264
+ # Send reply in the same thread
265
+ sent_message = (
266
+ self.gmail_service.users()
267
+ .messages()
268
+ .send(userId='me', body={**message, 'threadId': thread_id})
269
+ .execute()
270
+ )
271
+
272
+ return {
273
+ "success": True,
274
+ "message_id": sent_message.get('id'),
275
+ "thread_id": sent_message.get('threadId'),
276
+ "message": "Reply sent successfully",
277
+ }
278
+
279
+ except Exception as e:
280
+ logger.error("Failed to reply to email: %s", e)
281
+ return {"error": f"Failed to reply to email: {e!s}"}
282
+
283
+ def gmail_forward_email(
284
+ self,
285
+ message_id: str,
286
+ to: Union[str, List[str]],
287
+ forward_body: Optional[str] = None,
288
+ cc: Optional[Union[str, List[str]]] = None,
289
+ bcc: Optional[Union[str, List[str]]] = None,
290
+ include_attachments: bool = True,
291
+ ) -> Dict[str, Any]:
292
+ r"""Forward an email message.
293
+
294
+ Args:
295
+ message_id (str): The unique identifier of the message to forward.
296
+ To get a message ID, first use fetch_emails() to list messages,
297
+ or use the 'message_id' returned from send_email() or
298
+ create_email_draft().
299
+ to (Union[str, List[str]]): Recipient email address(es).
300
+ forward_body (Optional[str]): Additional message to include at
301
+ the top of the forwarded email, before the original message
302
+ content. If not provided, only the original message will be
303
+ forwarded.
304
+ cc (Optional[Union[str, List[str]]]): CC recipient email
305
+ address(es).
306
+ bcc (Optional[Union[str, List[str]]]): BCC recipient email
307
+ address(es).
308
+ include_attachments (bool): Whether to include original
309
+ attachments. Defaults to True. Only includes real
310
+ attachments, not inline images.
311
+
312
+ Returns:
313
+ Dict[str, Any]: A dictionary containing the result of the
314
+ operation, including the number of attachments forwarded.
315
+ """
316
+ try:
317
+ import tempfile
318
+
319
+ # Get the original message
320
+ original_message = (
321
+ self.gmail_service.users()
322
+ .messages()
323
+ .get(userId='me', id=message_id)
324
+ .execute()
325
+ )
326
+
327
+ # Extract headers (single pass, case-insensitive)
328
+ headers = original_message['payload'].get('headers', [])
329
+ subject = from_email = date = None
330
+ missing = {'subject', 'from', 'date'}
331
+
332
+ for header in headers:
333
+ name = (header.get('name') or '').lower()
334
+ if name not in missing:
335
+ continue
336
+ value = header.get('value')
337
+ if name == 'subject':
338
+ subject = value
339
+ elif name == 'from':
340
+ from_email = value
341
+ elif name == 'date':
342
+ date = value
343
+ missing.discard(name)
344
+ if not missing:
345
+ break
346
+
347
+ # Prepare forward subject
348
+ if subject and not subject.startswith('Fwd: '):
349
+ subject = f"Fwd: {subject}"
350
+ elif not subject:
351
+ subject = "Fwd: (No Subject)"
352
+
353
+ # Prepare forward body
354
+ if forward_body:
355
+ body = f"{forward_body}\n\n--- Forwarded message ---\n"
356
+ else:
357
+ body = "--- Forwarded message ---\n"
358
+
359
+ body += f"From: {from_email}\n"
360
+ body += f"Date: {date}\n"
361
+ body += f"Subject: {subject.replace('Fwd: ', '')}\n\n"
362
+
363
+ # Add original message body
364
+ body += self._extract_message_body(original_message)
365
+
366
+ # Normalize recipients
367
+ to_list = [to] if isinstance(to, str) else to
368
+ cc_list = [cc] if isinstance(cc, str) else (cc or [])
369
+ bcc_list = [bcc] if isinstance(bcc, str) else (bcc or [])
370
+
371
+ # Handle attachments
372
+ attachment_paths = []
373
+ temp_files: List[str] = []
374
+
375
+ try:
376
+ if include_attachments:
377
+ # Extract attachment metadata
378
+ attachments = self._extract_attachments(original_message)
379
+ for att in attachments:
380
+ try:
381
+ # Create temp file
382
+ temp_file = tempfile.NamedTemporaryFile(
383
+ delete=False, suffix=f"_{att['filename']}"
384
+ )
385
+ temp_files.append(temp_file.name)
386
+
387
+ # Download attachment
388
+ result = self.gmail_get_attachment(
389
+ message_id=message_id,
390
+ attachment_id=att['attachment_id'],
391
+ save_path=temp_file.name,
392
+ )
393
+
394
+ if result.get('success'):
395
+ attachment_paths.append(temp_file.name)
396
+
397
+ except Exception as e:
398
+ logger.warning(
399
+ f"Failed to download attachment "
400
+ f"{att['filename']}: {e}"
401
+ )
402
+
403
+ # Create forward message (now with attachments!)
404
+ message = self._create_message(
405
+ to_list,
406
+ subject,
407
+ body,
408
+ cc_list,
409
+ bcc_list,
410
+ attachments=attachment_paths if attachment_paths else None,
411
+ )
412
+
413
+ # Send forward
414
+ sent_message = (
415
+ self.gmail_service.users()
416
+ .messages()
417
+ .send(userId='me', body=message)
418
+ .execute()
419
+ )
420
+
421
+ return {
422
+ "success": True,
423
+ "message_id": sent_message.get('id'),
424
+ "thread_id": sent_message.get('threadId'),
425
+ "message": "Email forwarded successfully",
426
+ "attachments_forwarded": len(attachment_paths),
427
+ }
428
+
429
+ finally:
430
+ # Clean up temp files
431
+ for temp_file_path in temp_files:
432
+ try:
433
+ os.unlink(temp_file_path)
434
+ except Exception as e:
435
+ logger.warning(
436
+ f"Failed to delete temp file {temp_file_path}: {e}"
437
+ )
438
+
439
+ except Exception as e:
440
+ logger.error("Failed to forward email: %s", e)
441
+ return {"error": f"Failed to forward email: {e!s}"}
442
+
443
+ def gmail_create_draft(
444
+ self,
445
+ to: Union[str, List[str]],
446
+ subject: str,
447
+ body: str,
448
+ cc: Optional[Union[str, List[str]]] = None,
449
+ bcc: Optional[Union[str, List[str]]] = None,
450
+ attachments: Optional[List[str]] = None,
451
+ is_html: bool = False,
452
+ ) -> Dict[str, Any]:
453
+ r"""Create an email draft.
454
+
455
+ Args:
456
+ to (Union[str, List[str]]): Recipient email address(es).
457
+ subject (str): Email subject.
458
+ body (str): Email body content.
459
+ cc (Optional[Union[str, List[str]]]): CC recipient email
460
+ address(es).
461
+ bcc (Optional[Union[str, List[str]]]): BCC recipient email
462
+ address(es).
463
+ attachments (Optional[List[str]]): List of file paths to attach.
464
+ is_html (bool): Whether the body is HTML format. Set to True when
465
+ sending formatted emails with HTML tags (e.g., bold,
466
+ links, images). Use False (default) for plain text emails.
467
+
468
+ Returns:
469
+ Dict[str, Any]: A dictionary containing the result of the
470
+ operation.
471
+ """
472
+ try:
473
+ # Normalize recipients to lists
474
+ to_list = [to] if isinstance(to, str) else to
475
+ cc_list = [cc] if isinstance(cc, str) else (cc or [])
476
+ bcc_list = [bcc] if isinstance(bcc, str) else (bcc or [])
477
+
478
+ # Validate email addresses
479
+ all_recipients = to_list + cc_list + bcc_list
480
+ for email in all_recipients:
481
+ if not self._is_valid_email(email):
482
+ return {"error": f"Invalid email address: {email}"}
483
+
484
+ # Create message
485
+ message = self._create_message(
486
+ to_list, subject, body, cc_list, bcc_list, attachments, is_html
487
+ )
488
+
489
+ # Create draft
490
+ draft = (
491
+ self.gmail_service.users()
492
+ .drafts()
493
+ .create(userId='me', body={'message': message})
494
+ .execute()
495
+ )
496
+
497
+ return {
498
+ "success": True,
499
+ "draft_id": draft.get('id'),
500
+ "message_id": draft.get('message', {}).get('id'),
501
+ "message": "Draft created successfully",
502
+ }
503
+
504
+ except Exception as e:
505
+ logger.error("Failed to create draft: %s", e)
506
+ return {"error": f"Failed to create draft: {e!s}"}
507
+
508
+ def gmail_send_draft(self, draft_id: str) -> Dict[str, Any]:
509
+ r"""Send a draft email.
510
+
511
+ Args:
512
+ draft_id (str): The unique identifier of the draft to send.
513
+ To get a draft ID, first use list_drafts() to list drafts,
514
+ or use the 'draft_id' returned from create_email_draft().
515
+
516
+ Returns:
517
+ Dict[str, Any]: A dictionary containing the result of the
518
+ operation.
519
+ """
520
+ try:
521
+ # Send draft
522
+ sent_message = (
523
+ self.gmail_service.users()
524
+ .drafts()
525
+ .send(userId='me', body={'id': draft_id})
526
+ .execute()
527
+ )
528
+
529
+ return {
530
+ "success": True,
531
+ "message_id": sent_message.get('id'),
532
+ "thread_id": sent_message.get('threadId'),
533
+ "message": "Draft sent successfully",
534
+ }
535
+
536
+ except Exception as e:
537
+ logger.error("Failed to send draft: %s", e)
538
+ return {"error": f"Failed to send draft: {e!s}"}
539
+
540
+ def gmail_fetch_emails(
541
+ self,
542
+ query: str = "",
543
+ max_results: int = 10,
544
+ include_spam_trash: bool = False,
545
+ label_ids: Optional[List[str]] = None,
546
+ page_token: Optional[str] = None,
547
+ ) -> Dict[str, Any]:
548
+ r"""Fetch emails with filters and pagination.
549
+
550
+ Args:
551
+ query (str): Gmail search query string. Use Gmail's search syntax:
552
+ - 'from:example@domain.com' - emails from specific sender
553
+ - 'subject:meeting' - emails with specific subject text
554
+ - 'has:attachment' - emails with attachments
555
+ - 'is:unread' - unread emails
556
+ - 'in:sent' - emails in sent folder
557
+ - 'after:2024/01/01 before:2024/12/31' - date range
558
+ Examples: 'from:john@example.com subject:project', 'is:unread
559
+ has:attachment'
560
+ max_results (int): Maximum number of emails to fetch.
561
+ include_spam_trash (bool): Whether to include spam and trash.
562
+ label_ids (Optional[List[str]]): List of label IDs to filter
563
+ emails by. Only emails with ALL of the specified
564
+ labels will be returned.
565
+ Label IDs can be:
566
+ - System labels: 'INBOX', 'SENT', 'DRAFT', 'SPAM', 'TRASH',
567
+ 'UNREAD', 'STARRED', 'IMPORTANT', 'CATEGORY_PERSONAL', etc.
568
+ - Custom label IDs: Retrieved from list_gmail_labels() method.
569
+ page_token (Optional[str]): Pagination token from a previous
570
+ response. If provided, fetches the next page of results.
571
+
572
+ Returns:
573
+ Dict[str, Any]: A dictionary containing the fetched emails.
574
+ """
575
+ try:
576
+ # Build request parameters
577
+ request_params = {
578
+ 'userId': 'me',
579
+ 'maxResults': max_results,
580
+ 'includeSpamTrash': include_spam_trash,
581
+ }
582
+
583
+ if query:
584
+ request_params['q'] = query
585
+ if label_ids:
586
+ request_params['labelIds'] = label_ids
587
+
588
+ # List messages
589
+ if page_token:
590
+ request_params['pageToken'] = page_token
591
+
592
+ messages_result = (
593
+ self.gmail_service.users()
594
+ .messages()
595
+ .list(**request_params)
596
+ .execute()
597
+ )
598
+
599
+ messages = messages_result.get('messages', [])
600
+ emails = []
601
+
602
+ # Fetch detailed information for each message
603
+ for msg in messages:
604
+ email_detail = self._get_message_details(msg['id'])
605
+ if email_detail:
606
+ emails.append(email_detail)
607
+
608
+ return {
609
+ "success": True,
610
+ "emails": emails,
611
+ "total_count": len(emails),
612
+ "next_page_token": messages_result.get('nextPageToken'),
613
+ }
614
+
615
+ except Exception as e:
616
+ logger.error("Failed to fetch emails: %s", e)
617
+ return {"error": f"Failed to fetch emails: {e!s}"}
618
+
619
+ def gmail_fetch_thread_by_id(self, thread_id: str) -> Dict[str, Any]:
620
+ r"""Fetch a thread by ID.
621
+
622
+ Args:
623
+ thread_id (str): The unique identifier of the thread to fetch.
624
+ To get a thread ID, first use list_threads() to list threads,
625
+ or use the 'thread_id' returned from send_email() or
626
+ reply_to_email().
627
+
628
+ Returns:
629
+ Dict[str, Any]: A dictionary containing the thread details.
630
+ """
631
+ try:
632
+ thread = (
633
+ self.gmail_service.users()
634
+ .threads()
635
+ .get(userId='me', id=thread_id)
636
+ .execute()
637
+ )
638
+
639
+ messages = []
640
+ for message in thread.get('messages', []):
641
+ message_detail = self._get_message_details(message['id'])
642
+ if message_detail:
643
+ messages.append(message_detail)
644
+
645
+ return {
646
+ "success": True,
647
+ "thread_id": thread_id,
648
+ "messages": messages,
649
+ "message_count": len(messages),
650
+ }
651
+
652
+ except Exception as e:
653
+ logger.error("Failed to fetch thread: %s", e)
654
+ return {"error": f"Failed to fetch thread: {e!s}"}
655
+
656
+ def gmail_modify_email_labels(
657
+ self,
658
+ message_id: str,
659
+ add_labels: Optional[List[str]] = None,
660
+ remove_labels: Optional[List[str]] = None,
661
+ ) -> Dict[str, Any]:
662
+ r"""Modify labels on an email message.
663
+
664
+ Args:
665
+ message_id (str): The unique identifier of the message to modify.
666
+ To get a message ID, first use fetch_emails() to list messages,
667
+ or use the 'message_id' returned from send_email() or
668
+ create_email_draft().
669
+ add_labels (Optional[List[str]]): List of label IDs to add to
670
+ the message.
671
+ Label IDs can be:
672
+ - System labels: 'INBOX', 'STARRED', 'IMPORTANT',
673
+ 'UNREAD', etc.
674
+ - Custom label IDs: Retrieved from list_gmail_labels() method.
675
+ Example: ['STARRED', 'IMPORTANT'] marks email as starred
676
+ and important.
677
+ remove_labels (Optional[List[str]]): List of label IDs to
678
+ remove from the message. Uses the same format as add_labels.
679
+ Example: ['UNREAD'] marks the email as read.
680
+
681
+ Returns:
682
+ Dict[str, Any]: A dictionary containing the result of the
683
+ operation.
684
+ """
685
+ try:
686
+ body = {}
687
+ if add_labels:
688
+ body['addLabelIds'] = add_labels
689
+ if remove_labels:
690
+ body['removeLabelIds'] = remove_labels
691
+
692
+ if not body:
693
+ return {"error": "No labels to add or remove"}
694
+
695
+ modified_message = (
696
+ self.gmail_service.users()
697
+ .messages()
698
+ .modify(userId='me', id=message_id, body=body)
699
+ .execute()
700
+ )
701
+
702
+ return {
703
+ "success": True,
704
+ "message_id": message_id,
705
+ "label_ids": modified_message.get('labelIds', []),
706
+ "message": "Labels modified successfully",
707
+ }
708
+
709
+ except Exception as e:
710
+ logger.error("Failed to modify labels: %s", e)
711
+ return {"error": f"Failed to modify labels: {e!s}"}
712
+
713
+ def gmail_move_to_trash(self, message_id: str) -> Dict[str, Any]:
714
+ r"""Move a message to trash.
715
+
716
+ Args:
717
+ message_id (str): The unique identifier of the message to move to
718
+ trash. To get a message ID, first use fetch_emails() to list
719
+ messages, or use the 'message_id' returned from send_email()
720
+ or create_email_draft().
721
+
722
+ Returns:
723
+ Dict[str, Any]: A dictionary containing the result of the
724
+ operation.
725
+ """
726
+ try:
727
+ trashed_message = (
728
+ self.gmail_service.users()
729
+ .messages()
730
+ .trash(userId='me', id=message_id)
731
+ .execute()
732
+ )
733
+
734
+ return {
735
+ "success": True,
736
+ "message_id": message_id,
737
+ "label_ids": trashed_message.get('labelIds', []),
738
+ "message": "Message moved to trash successfully",
739
+ }
740
+
741
+ except Exception as e:
742
+ logger.error("Failed to move message to trash: %s", e)
743
+ return {"error": f"Failed to move message to trash: {e!s}"}
744
+
745
+ def gmail_get_attachment(
746
+ self,
747
+ message_id: str,
748
+ attachment_id: str,
749
+ save_path: Optional[str] = None,
750
+ ) -> Dict[str, Any]:
751
+ r"""Get an attachment from a message.
752
+
753
+ Args:
754
+ message_id (str): The unique identifier of the message containing
755
+ the attachment. To get a message ID, first use fetch_emails()
756
+ to list messages, or use the 'message_id' returned from
757
+ send_email() or create_email_draft().
758
+ attachment_id (str): The unique identifier of the attachment to
759
+ download. To get an attachment ID, first use fetch_emails() to
760
+ get message details, then look for 'attachment_id' in the
761
+ 'attachments' list of each message.
762
+ save_path (Optional[str]): Local file path where the attachment
763
+ should be saved. If provided, the attachment will be saved to
764
+ this location and the response will include a success message.
765
+ If not provided, the attachment data will be returned as
766
+ base64-encoded content in the response.
767
+
768
+ Returns:
769
+ Dict[str, Any]: A dictionary containing the attachment data or
770
+ save result.
771
+ """
772
+ try:
773
+ import base64
774
+
775
+ attachment = (
776
+ self.gmail_service.users()
777
+ .messages()
778
+ .attachments()
779
+ .get(userId='me', messageId=message_id, id=attachment_id)
780
+ .execute()
781
+ )
782
+
783
+ # Decode the attachment data
784
+ file_data = base64.urlsafe_b64decode(attachment['data'])
785
+
786
+ if save_path:
787
+ with open(save_path, 'wb') as f:
788
+ f.write(file_data)
789
+ return {
790
+ "success": True,
791
+ "message": f"Attachment saved to {save_path}",
792
+ "file_size": len(file_data),
793
+ }
794
+ else:
795
+ return {
796
+ "success": True,
797
+ "data": base64.b64encode(file_data).decode('utf-8'),
798
+ "file_size": len(file_data),
799
+ }
800
+
801
+ except Exception as e:
802
+ logger.error("Failed to get attachment: %s", e)
803
+ return {"error": f"Failed to get attachment: {e!s}"}
804
+
805
+ def gmail_list_threads(
806
+ self,
807
+ query: str = "",
808
+ max_results: int = 10,
809
+ include_spam_trash: bool = False,
810
+ label_ids: Optional[List[str]] = None,
811
+ page_token: Optional[str] = None,
812
+ ) -> Dict[str, Any]:
813
+ r"""List email threads.
814
+
815
+ Args:
816
+ query (str): Gmail search query string. Use Gmail's search syntax:
817
+ - 'from:example@domain.com' - threads from specific sender
818
+ - 'subject:meeting' - threads with specific subject text
819
+ - 'has:attachment' - threads with attachments
820
+ - 'is:unread' - unread threads
821
+ - 'in:sent' - threads in sent folder
822
+ - 'after:2024/01/01 before:2024/12/31' - date range
823
+ Examples: 'from:john@example.com subject:project', 'is:unread
824
+ has:attachment'
825
+ max_results (int): Maximum number of threads to fetch.
826
+ include_spam_trash (bool): Whether to include spam and trash.
827
+ label_ids (Optional[List[str]]): List of label IDs to filter
828
+ threads by. Only threads with ALL of the specified labels
829
+ will be returned.
830
+ Label IDs can be:
831
+ - System labels: 'INBOX', 'SENT', 'DRAFT', 'SPAM', 'TRASH',
832
+ 'UNREAD', 'STARRED', 'IMPORTANT', 'CATEGORY_PERSONAL', etc.
833
+ - Custom label IDs: Retrieved from list_gmail_labels() method.
834
+ page_token (Optional[str]): Pagination token from a previous
835
+ response. If provided, fetches the next page of results.
836
+
837
+ Returns:
838
+ Dict[str, Any]: A dictionary containing the thread list.
839
+ """
840
+ try:
841
+ # Build request parameters
842
+ request_params = {
843
+ 'userId': 'me',
844
+ 'maxResults': max_results,
845
+ 'includeSpamTrash': include_spam_trash,
846
+ }
847
+
848
+ if query:
849
+ request_params['q'] = query
850
+ if label_ids:
851
+ request_params['labelIds'] = label_ids
852
+
853
+ # List threads
854
+ if page_token:
855
+ request_params['pageToken'] = page_token
856
+
857
+ threads_result = (
858
+ self.gmail_service.users()
859
+ .threads()
860
+ .list(**request_params)
861
+ .execute()
862
+ )
863
+
864
+ threads = threads_result.get('threads', [])
865
+ thread_list = []
866
+
867
+ for thread in threads:
868
+ thread_list.append(
869
+ {
870
+ "thread_id": thread['id'],
871
+ "snippet": thread.get('snippet', ''),
872
+ "history_id": thread.get('historyId', ''),
873
+ }
874
+ )
875
+
876
+ return {
877
+ "success": True,
878
+ "threads": thread_list,
879
+ "total_count": len(thread_list),
880
+ "next_page_token": threads_result.get('nextPageToken'),
881
+ }
882
+
883
+ except Exception as e:
884
+ logger.error("Failed to list threads: %s", e)
885
+ return {"error": f"Failed to list threads: {e!s}"}
886
+
887
+ def gmail_list_drafts(
888
+ self, max_results: int = 10, page_token: Optional[str] = None
889
+ ) -> Dict[str, Any]:
890
+ r"""List email drafts.
891
+
892
+ Args:
893
+ max_results (int): Maximum number of drafts to fetch.
894
+ page_token (Optional[str]): Pagination token from a previous
895
+ response. If provided, fetches the next page of results.
896
+
897
+ Returns:
898
+ Dict[str, Any]: A dictionary containing the draft list.
899
+ """
900
+ try:
901
+ drafts_result = (
902
+ self.gmail_service.users()
903
+ .drafts()
904
+ .list(
905
+ userId='me',
906
+ maxResults=max_results,
907
+ **({"pageToken": page_token} if page_token else {}),
908
+ )
909
+ .execute()
910
+ )
911
+
912
+ drafts = drafts_result.get('drafts', [])
913
+ draft_list = []
914
+
915
+ for draft in drafts:
916
+ draft_info = {
917
+ "draft_id": draft['id'],
918
+ "message_id": draft.get('message', {}).get('id', ''),
919
+ "thread_id": draft.get('message', {}).get('threadId', ''),
920
+ "snippet": draft.get('message', {}).get('snippet', ''),
921
+ }
922
+ draft_list.append(draft_info)
923
+
924
+ return {
925
+ "success": True,
926
+ "drafts": draft_list,
927
+ "total_count": len(draft_list),
928
+ "next_page_token": drafts_result.get('nextPageToken'),
929
+ }
930
+
931
+ except Exception as e:
932
+ logger.error("Failed to list drafts: %s", e)
933
+ return {"error": f"Failed to list drafts: {e!s}"}
934
+
935
+ def gmail_list_labels(self) -> Dict[str, Any]:
936
+ r"""List all Gmail labels.
937
+
938
+ Returns:
939
+ Dict[str, Any]: A dictionary containing the label list.
940
+ """
941
+ try:
942
+ labels_result = (
943
+ self.gmail_service.users().labels().list(userId='me').execute()
944
+ )
945
+
946
+ labels = labels_result.get('labels', [])
947
+ label_list = []
948
+
949
+ for label in labels:
950
+ label_info = {
951
+ "id": label['id'],
952
+ "name": label['name'],
953
+ "type": label.get('type', 'user'),
954
+ "messages_total": label.get('messagesTotal', 0),
955
+ "messages_unread": label.get('messagesUnread', 0),
956
+ "threads_total": label.get('threadsTotal', 0),
957
+ "threads_unread": label.get('threadsUnread', 0),
958
+ }
959
+ label_list.append(label_info)
960
+
961
+ return {
962
+ "success": True,
963
+ "labels": label_list,
964
+ "total_count": len(label_list),
965
+ }
966
+
967
+ except Exception as e:
968
+ logger.error("Failed to list labels: %s", e)
969
+ return {"error": f"Failed to list labels: {e!s}"}
970
+
971
+ def gmail_create_label(
972
+ self,
973
+ name: str,
974
+ label_list_visibility: Literal["labelShow", "labelHide"] = "labelShow",
975
+ message_list_visibility: Literal["show", "hide"] = "show",
976
+ ) -> Dict[str, Any]:
977
+ r"""Create a new Gmail label.
978
+
979
+ Args:
980
+ name (str): The name of the label to create.
981
+ label_list_visibility (str): How the label appears in Gmail's
982
+ label list. - 'labelShow': Label is visible in the label list
983
+ sidebar (default) - 'labelHide': Label is hidden from the
984
+ label list sidebar
985
+ message_list_visibility (str): How the label appears in message
986
+ lists. - 'show': Label is visible on messages in inbox/lists
987
+ (default) - 'hide': Label is hidden from message displays
988
+
989
+ Returns:
990
+ Dict[str, Any]: A dictionary containing the result of the
991
+ operation.
992
+ """
993
+ try:
994
+ label_object = {
995
+ 'name': name,
996
+ 'labelListVisibility': label_list_visibility,
997
+ 'messageListVisibility': message_list_visibility,
998
+ }
999
+
1000
+ created_label = (
1001
+ self.gmail_service.users()
1002
+ .labels()
1003
+ .create(userId='me', body=label_object)
1004
+ .execute()
1005
+ )
1006
+
1007
+ return {
1008
+ "success": True,
1009
+ "label_id": created_label['id'],
1010
+ "label_name": created_label['name'],
1011
+ "message": "Label created successfully",
1012
+ }
1013
+
1014
+ except Exception as e:
1015
+ logger.error("Failed to create label: %s", e)
1016
+ return {"error": f"Failed to create label: {e!s}"}
1017
+
1018
+ def gmail_delete_label(self, label_id: str) -> Dict[str, Any]:
1019
+ r"""Delete a Gmail label.
1020
+
1021
+ Args:
1022
+ label_id (str): The unique identifier of the user-created label to
1023
+ delete. To get a label ID, first use list_gmail_labels() to
1024
+ list all labels. Note: System labels (e.g., 'INBOX', 'SENT',
1025
+ 'DRAFT', 'SPAM', 'TRASH', 'UNREAD', 'STARRED', 'IMPORTANT',
1026
+ 'CATEGORY_PERSONAL', etc.) cannot be deleted.
1027
+
1028
+ Returns:
1029
+ Dict[str, Any]: A dictionary containing the result of the
1030
+ operation.
1031
+ """
1032
+ try:
1033
+ self.gmail_service.users().labels().delete(
1034
+ userId='me', id=label_id
1035
+ ).execute()
1036
+
1037
+ return {
1038
+ "success": True,
1039
+ "label_id": label_id,
1040
+ "message": "Label deleted successfully",
1041
+ }
1042
+
1043
+ except Exception as e:
1044
+ logger.error("Failed to delete label: %s", e)
1045
+ return {"error": f"Failed to delete label: {e!s}"}
1046
+
1047
+ def gmail_modify_thread_labels(
1048
+ self,
1049
+ thread_id: str,
1050
+ add_labels: Optional[List[str]] = None,
1051
+ remove_labels: Optional[List[str]] = None,
1052
+ ) -> Dict[str, Any]:
1053
+ r"""Modify labels on a thread.
1054
+
1055
+ Args:
1056
+ thread_id (str): The unique identifier of the thread to modify.
1057
+ To get a thread ID, first use list_threads() to list threads,
1058
+ or use the 'thread_id' returned from send_email() or
1059
+ reply_to_email().
1060
+ add_labels (Optional[List[str]]): List of label IDs to add to all
1061
+ messages in the thread.
1062
+ Label IDs can be:
1063
+ - System labels: 'INBOX', 'STARRED', 'IMPORTANT',
1064
+ 'UNREAD', etc.
1065
+ - Custom label IDs: Retrieved from list_gmail_labels().
1066
+ Example: ['STARRED', 'IMPORTANT'] marks thread as
1067
+ starred and important.
1068
+ remove_labels (Optional[List[str]]): List of label IDs to
1069
+ remove from all messages in the thread. Uses the same
1070
+ format as add_labels.
1071
+ Example: ['UNREAD'] marks the entire thread as read.
1072
+
1073
+ Returns:
1074
+ Dict[str, Any]: A dictionary containing the result of the
1075
+ operation.
1076
+ """
1077
+ try:
1078
+ body = {}
1079
+ if add_labels:
1080
+ body['addLabelIds'] = add_labels
1081
+ if remove_labels:
1082
+ body['removeLabelIds'] = remove_labels
1083
+
1084
+ if not body:
1085
+ return {"error": "No labels to add or remove"}
1086
+
1087
+ modified_thread = (
1088
+ self.gmail_service.users()
1089
+ .threads()
1090
+ .modify(userId='me', id=thread_id, body=body)
1091
+ .execute()
1092
+ )
1093
+
1094
+ return {
1095
+ "success": True,
1096
+ "thread_id": thread_id,
1097
+ "label_ids": modified_thread.get('labelIds', []),
1098
+ "message": "Thread labels modified successfully",
1099
+ }
1100
+
1101
+ except Exception as e:
1102
+ logger.error("Failed to modify thread labels: %s", e)
1103
+ return {"error": f"Failed to modify thread labels: {e!s}"}
1104
+
1105
+ def gmail_get_profile(self) -> Dict[str, Any]:
1106
+ r"""Get Gmail profile information.
1107
+
1108
+ Returns:
1109
+ Dict[str, Any]: A dictionary containing the profile information.
1110
+ """
1111
+ try:
1112
+ profile = (
1113
+ self.gmail_service.users().getProfile(userId='me').execute()
1114
+ )
1115
+
1116
+ return {
1117
+ "success": True,
1118
+ "profile": {
1119
+ "email_address": profile.get('emailAddress', ''),
1120
+ "messages_total": profile.get('messagesTotal', 0),
1121
+ "threads_total": profile.get('threadsTotal', 0),
1122
+ "history_id": profile.get('historyId', ''),
1123
+ },
1124
+ }
1125
+
1126
+ except Exception as e:
1127
+ logger.error("Failed to get profile: %s", e)
1128
+ return {"error": f"Failed to get profile: {e!s}"}
1129
+
1130
+ def gmail_get_contacts(
1131
+ self,
1132
+ max_results: int = 100,
1133
+ page_token: Optional[str] = None,
1134
+ ) -> Dict[str, Any]:
1135
+ r"""List connections from Google People API.
1136
+
1137
+ Args:
1138
+ max_results (int): Maximum number of contacts to fetch.
1139
+ page_token (Optional[str]): Pagination token from a previous
1140
+ response. If provided, fetches the next page of results.
1141
+
1142
+ Returns:
1143
+ Dict[str, Any]: A dictionary containing the contacts.
1144
+ """
1145
+ try:
1146
+ # Build request parameters
1147
+ request_params = {
1148
+ 'resourceName': 'people/me',
1149
+ 'personFields': (
1150
+ 'names,emailAddresses,phoneNumbers,organizations'
1151
+ ),
1152
+ 'pageSize': max_results,
1153
+ }
1154
+
1155
+ # Search contacts
1156
+ if page_token:
1157
+ request_params['pageToken'] = page_token
1158
+
1159
+ contacts_result = (
1160
+ self.people_service.people()
1161
+ .connections()
1162
+ .list(**request_params)
1163
+ .execute()
1164
+ )
1165
+
1166
+ connections = contacts_result.get('connections', [])
1167
+ contact_list = []
1168
+
1169
+ for person in connections:
1170
+ contact_info = {
1171
+ "resource_name": person.get('resourceName', ''),
1172
+ "names": person.get('names', []),
1173
+ "email_addresses": person.get('emailAddresses', []),
1174
+ "phone_numbers": person.get('phoneNumbers', []),
1175
+ "organizations": person.get('organizations', []),
1176
+ }
1177
+ contact_list.append(contact_info)
1178
+
1179
+ return {
1180
+ "success": True,
1181
+ "contacts": contact_list,
1182
+ "total_count": len(contact_list),
1183
+ "next_page_token": contacts_result.get('nextPageToken'),
1184
+ }
1185
+
1186
+ except Exception as e:
1187
+ logger.error("Failed to get contacts: %s", e)
1188
+ return {"error": f"Failed to get contacts: {e!s}"}
1189
+
1190
+ def gmail_search_people(
1191
+ self,
1192
+ query: str,
1193
+ max_results: int = 10,
1194
+ ) -> Dict[str, Any]:
1195
+ r"""Search for people in contacts.
1196
+
1197
+ Args:
1198
+ query (str): Search query for people in contacts. Can search by:
1199
+ - Name: 'John Smith' or partial names like 'John'
1200
+ - Email: 'john@example.com'
1201
+ - Organization: 'Google' or 'Acme Corp'
1202
+ - Phone number: '+1234567890'
1203
+ Examples: 'John Smith', 'john@example.com', 'Google'
1204
+ max_results (int): Maximum number of results to fetch.
1205
+
1206
+ Returns:
1207
+ Dict[str, Any]: A dictionary containing the search results.
1208
+ """
1209
+ try:
1210
+ # Search people
1211
+ search_result = (
1212
+ self.people_service.people()
1213
+ .searchContacts(
1214
+ query=query,
1215
+ readMask='names,emailAddresses,phoneNumbers,organizations',
1216
+ pageSize=max_results,
1217
+ )
1218
+ .execute()
1219
+ )
1220
+
1221
+ results = search_result.get('results', [])
1222
+ people_list = []
1223
+
1224
+ for result in results:
1225
+ person = result.get('person', {})
1226
+ person_info = {
1227
+ "resource_name": person.get('resourceName', ''),
1228
+ "names": person.get('names', []),
1229
+ "email_addresses": person.get('emailAddresses', []),
1230
+ "phone_numbers": person.get('phoneNumbers', []),
1231
+ "organizations": person.get('organizations', []),
1232
+ }
1233
+ people_list.append(person_info)
1234
+
1235
+ return {
1236
+ "success": True,
1237
+ "people": people_list,
1238
+ "total_count": len(people_list),
1239
+ }
1240
+
1241
+ except Exception as e:
1242
+ logger.error("Failed to search people: %s", e)
1243
+ return {"error": f"Failed to search people: {e!s}"}
1244
+
1245
+ # Helper methods
1246
+ def _get_gmail_service(self):
1247
+ r"""Get Gmail service object."""
1248
+ from googleapiclient.discovery import build
1249
+
1250
+ try:
1251
+ # Build service with optional timeout
1252
+ if self.timeout is not None:
1253
+ import httplib2
1254
+
1255
+ http = httplib2.Http(timeout=self.timeout)
1256
+ http = self._credentials.authorize(http)
1257
+ service = build('gmail', 'v1', http=http)
1258
+ else:
1259
+ service = build('gmail', 'v1', credentials=self._credentials)
1260
+ return service
1261
+ except Exception as e:
1262
+ raise ValueError(f"Failed to build Gmail service: {e}") from e
1263
+
1264
+ def _get_people_service(self):
1265
+ r"""Get People service object."""
1266
+ from googleapiclient.discovery import build
1267
+
1268
+ try:
1269
+ # Build service with optional timeout
1270
+ if self.timeout is not None:
1271
+ import httplib2
1272
+
1273
+ http = httplib2.Http(timeout=self.timeout)
1274
+ http = self._credentials.authorize(http)
1275
+ service = build('people', 'v1', http=http)
1276
+ else:
1277
+ service = build('people', 'v1', credentials=self._credentials)
1278
+ return service
1279
+ except Exception as e:
1280
+ raise ValueError(f"Failed to build People service: {e}") from e
1281
+
1282
+ def _authenticate(self):
1283
+ r"""Authenticate with Google APIs using OAuth2.
1284
+
1285
+ Automatically saves and loads credentials from
1286
+ ~/.camel/gmail_token.json to avoid repeated
1287
+ browser logins.
1288
+ """
1289
+ import json
1290
+ from pathlib import Path
1291
+
1292
+ from dotenv import load_dotenv
1293
+ from google.auth.transport.requests import Request
1294
+ from google.oauth2.credentials import Credentials
1295
+ from google_auth_oauthlib.flow import InstalledAppFlow
1296
+
1297
+ # Look for .env file in the project root (camel/)
1298
+ env_file = Path(__file__).parent.parent.parent / '.env'
1299
+ load_dotenv(env_file)
1300
+
1301
+ client_id = os.environ.get('GOOGLE_CLIENT_ID')
1302
+ client_secret = os.environ.get('GOOGLE_CLIENT_SECRET')
1303
+
1304
+ if not client_id or not client_secret:
1305
+ missing_vars = []
1306
+ if not client_id:
1307
+ missing_vars.append('GOOGLE_CLIENT_ID')
1308
+ if not client_secret:
1309
+ missing_vars.append('GOOGLE_CLIENT_SECRET')
1310
+ raise ValueError(
1311
+ f"Missing required environment variables: "
1312
+ f"{', '.join(missing_vars)}. "
1313
+ "Please set these in your .env file or environment variables."
1314
+ )
1315
+
1316
+ token_file = Path.home() / '.camel' / 'gmail_token.json'
1317
+ creds = None
1318
+
1319
+ # COMPONENT 1: Load saved credentials
1320
+ if token_file.exists():
1321
+ try:
1322
+ with open(token_file, 'r') as f:
1323
+ data = json.load(f)
1324
+ creds = Credentials(
1325
+ token=data.get('token'),
1326
+ refresh_token=data.get('refresh_token'),
1327
+ token_uri=data.get(
1328
+ 'token_uri', 'https://oauth2.googleapis.com/token'
1329
+ ),
1330
+ client_id=client_id,
1331
+ client_secret=client_secret,
1332
+ scopes=SCOPES,
1333
+ )
1334
+ except Exception as e:
1335
+ logger.warning(f"Failed to load saved token: {e}")
1336
+ creds = None
1337
+
1338
+ # COMPONENT 2: Refresh if expired
1339
+ if creds and creds.expired and creds.refresh_token:
1340
+ try:
1341
+ creds.refresh(Request())
1342
+ logger.info("Access token refreshed")
1343
+
1344
+ # Save refreshed credentials to disk
1345
+ token_file.parent.mkdir(parents=True, exist_ok=True)
1346
+ try:
1347
+ with open(token_file, 'w') as f:
1348
+ json.dump(
1349
+ {
1350
+ 'token': creds.token,
1351
+ 'refresh_token': creds.refresh_token,
1352
+ 'token_uri': creds.token_uri,
1353
+ 'scopes': creds.scopes,
1354
+ },
1355
+ f,
1356
+ )
1357
+ os.chmod(token_file, 0o600)
1358
+ logger.info(f"Refreshed credentials saved to {token_file}")
1359
+ except Exception as e:
1360
+ logger.warning(
1361
+ f"Failed to save refreshed credentials to "
1362
+ f"{token_file}: {e}. "
1363
+ "Token refreshed but not persisted."
1364
+ )
1365
+
1366
+ return creds
1367
+ except Exception as e:
1368
+ logger.warning(f"Token refresh failed: {e}")
1369
+ creds = None
1370
+
1371
+ # COMPONENT 3: Return if valid
1372
+ if creds and creds.valid:
1373
+ return creds
1374
+
1375
+ # COMPONENT 4: Browser OAuth (first-time or invalid credentials)
1376
+ client_config = {
1377
+ "installed": {
1378
+ "client_id": client_id,
1379
+ "client_secret": client_secret,
1380
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
1381
+ "token_uri": "https://oauth2.googleapis.com/token",
1382
+ "redirect_uris": ["http://localhost"],
1383
+ }
1384
+ }
1385
+
1386
+ flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
1387
+ creds = flow.run_local_server(port=0)
1388
+
1389
+ # Save new credentials
1390
+ token_file.parent.mkdir(parents=True, exist_ok=True)
1391
+ try:
1392
+ with open(token_file, 'w') as f:
1393
+ json.dump(
1394
+ {
1395
+ 'token': creds.token,
1396
+ 'refresh_token': creds.refresh_token,
1397
+ 'token_uri': creds.token_uri,
1398
+ 'scopes': creds.scopes,
1399
+ },
1400
+ f,
1401
+ )
1402
+ os.chmod(token_file, 0o600)
1403
+ logger.info(f"Credentials saved to {token_file}")
1404
+ except Exception as e:
1405
+ logger.warning(
1406
+ f"Failed to save credentials to {token_file}: {e}. "
1407
+ "You may need to re-authenticate next time."
1408
+ )
1409
+
1410
+ return creds
1411
+
1412
+ def _create_message(
1413
+ self,
1414
+ to_list: List[str],
1415
+ subject: str,
1416
+ body: str,
1417
+ cc_list: Optional[List[str]] = None,
1418
+ bcc_list: Optional[List[str]] = None,
1419
+ attachments: Optional[List[str]] = None,
1420
+ is_html: bool = False,
1421
+ in_reply_to: Optional[str] = None,
1422
+ references: Optional[List[str]] = None,
1423
+ ) -> Dict[str, str]:
1424
+ r"""Create a message object for sending."""
1425
+
1426
+ import base64
1427
+ from email import encoders
1428
+ from email.mime.base import MIMEBase
1429
+ from email.mime.multipart import MIMEMultipart
1430
+ from email.mime.text import MIMEText
1431
+
1432
+ message = MIMEMultipart()
1433
+ message['to'] = ', '.join(to_list)
1434
+ message['subject'] = subject
1435
+
1436
+ if cc_list:
1437
+ message['cc'] = ', '.join(cc_list)
1438
+ if bcc_list:
1439
+ message['bcc'] = ', '.join(bcc_list)
1440
+
1441
+ # Set reply headers when provided
1442
+ if in_reply_to:
1443
+ message['In-Reply-To'] = in_reply_to
1444
+ if references:
1445
+ message['References'] = ' '.join(references)
1446
+
1447
+ # Add body
1448
+ if is_html:
1449
+ message.attach(MIMEText(body, 'html'))
1450
+ else:
1451
+ message.attach(MIMEText(body, 'plain'))
1452
+
1453
+ # Add attachments
1454
+ if attachments:
1455
+ for file_path in attachments:
1456
+ if os.path.isfile(file_path):
1457
+ with open(file_path, "rb") as attachment:
1458
+ part = MIMEBase('application', 'octet-stream')
1459
+ part.set_payload(attachment.read())
1460
+ encoders.encode_base64(part)
1461
+ part.add_header(
1462
+ 'Content-Disposition',
1463
+ f'attachment; filename= '
1464
+ f'{os.path.basename(file_path)}',
1465
+ )
1466
+ message.attach(part)
1467
+
1468
+ # Encode message
1469
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode(
1470
+ 'utf-8'
1471
+ )
1472
+ return {'raw': raw_message}
1473
+
1474
+ def _get_message_details(
1475
+ self, message_id: str
1476
+ ) -> Optional[Dict[str, Any]]:
1477
+ r"""Get detailed information about a message."""
1478
+ try:
1479
+ message = (
1480
+ self.gmail_service.users()
1481
+ .messages()
1482
+ .get(userId='me', id=message_id)
1483
+ .execute()
1484
+ )
1485
+
1486
+ headers = message['payload'].get('headers', [])
1487
+ # Build a name->value map in one pass (case-insensitive)
1488
+ header_map = {}
1489
+ for header in headers:
1490
+ name = header.get('name')
1491
+ if name:
1492
+ header_map[name.lower()] = header.get('value', '')
1493
+
1494
+ return {
1495
+ "message_id": message['id'],
1496
+ "thread_id": message['threadId'],
1497
+ "snippet": message.get('snippet', ''),
1498
+ "subject": header_map.get('subject', ''),
1499
+ "from": header_map.get('from', ''),
1500
+ "to": header_map.get('to', ''),
1501
+ "cc": header_map.get('cc', ''),
1502
+ "bcc": header_map.get('bcc', ''),
1503
+ "date": header_map.get('date', ''),
1504
+ "body": self._extract_message_body(message),
1505
+ "attachments": self._extract_attachments(
1506
+ message, include_inline=True
1507
+ ),
1508
+ "label_ids": message.get('labelIds', []),
1509
+ "size_estimate": message.get('sizeEstimate', 0),
1510
+ }
1511
+ except Exception as e:
1512
+ logger.error("Failed to get message details: %s", e)
1513
+ return None
1514
+
1515
+ def _get_header_value(
1516
+ self, headers: List[Dict[str, str]], name: str
1517
+ ) -> str:
1518
+ r"""Get header value by name."""
1519
+ for header in headers:
1520
+ if header['name'].lower() == name.lower():
1521
+ return header['value']
1522
+ return ""
1523
+
1524
+ def _extract_message_body(self, message: Dict[str, Any]) -> str:
1525
+ r"""Extract message body from message payload.
1526
+
1527
+ Recursively traverses the entire message tree and collects all text
1528
+ content from text/plain and text/html parts. Special handling for
1529
+ multipart/alternative containers: recursively searches for one format
1530
+ (preferring plain text) to avoid duplication when both formats contain
1531
+ the same content. All other text parts are collected to ensure no
1532
+ information is lost.
1533
+
1534
+ Args:
1535
+ message (Dict[str, Any]): The Gmail message dictionary containing
1536
+ the payload to extract text from.
1537
+
1538
+ Returns:
1539
+ str: The extracted message body text with multiple parts separated
1540
+ by double newlines, or an empty string if no text content is
1541
+ found.
1542
+ """
1543
+ import base64
1544
+ import re
1545
+
1546
+ text_parts = []
1547
+
1548
+ def decode_text_data(data: str, mime_type: str) -> Optional[str]:
1549
+ """Helper to decode base64 text data.
1550
+
1551
+ Args:
1552
+ data: Base64 encoded text data.
1553
+ mime_type: MIME type for logging purposes.
1554
+
1555
+ Returns:
1556
+ Decoded text string, or None if decoding fails or text
1557
+ is empty.
1558
+ """
1559
+ if not data:
1560
+ return None
1561
+ try:
1562
+ text = base64.urlsafe_b64decode(data).decode('utf-8')
1563
+ return text if text.strip() else None
1564
+ except Exception as e:
1565
+ logger.warning(f"Failed to decode {mime_type}: {e}")
1566
+ return None
1567
+
1568
+ def strip_html_tags(html_content: str) -> str:
1569
+ """Strip HTML tags and convert to readable plain text.
1570
+
1571
+ Uses regex to remove tags and clean up formatting while preserving
1572
+ basic document structure.
1573
+
1574
+ Args:
1575
+ html_content: HTML content to strip.
1576
+
1577
+ Returns:
1578
+ Plain text version of HTML content.
1579
+ """
1580
+ if not html_content or not html_content.strip():
1581
+ return ""
1582
+
1583
+ text = html_content
1584
+
1585
+ # Remove script and style elements completely
1586
+ text = re.sub(
1587
+ r'<script[^>]*>.*?</script>',
1588
+ '',
1589
+ text,
1590
+ flags=re.DOTALL | re.IGNORECASE,
1591
+ )
1592
+ text = re.sub(
1593
+ r'<style[^>]*>.*?</style>',
1594
+ '',
1595
+ text,
1596
+ flags=re.DOTALL | re.IGNORECASE,
1597
+ )
1598
+
1599
+ # Convert common HTML entities
1600
+ text = text.replace('&nbsp;', ' ')
1601
+ text = text.replace('&amp;', '&')
1602
+ text = text.replace('&lt;', '<')
1603
+ text = text.replace('&gt;', '>')
1604
+ text = text.replace('&quot;', '"')
1605
+ text = text.replace('&#39;', "'")
1606
+ text = text.replace('&rsquo;', "'")
1607
+ text = text.replace('&lsquo;', "'")
1608
+ text = text.replace('&rdquo;', '"')
1609
+ text = text.replace('&ldquo;', '"')
1610
+ text = text.replace('&mdash;', '—')
1611
+ text = text.replace('&ndash;', '-')
1612
+
1613
+ # Convert <br> and <br/> to newlines
1614
+ text = re.sub(r'<br\s*/?>', '\n', text, flags=re.IGNORECASE)
1615
+
1616
+ # Convert block-level closing tags to newlines
1617
+ text = re.sub(
1618
+ r'</(p|div|h[1-6]|tr|li)>', '\n', text, flags=re.IGNORECASE
1619
+ )
1620
+
1621
+ # Convert <hr> to separator
1622
+ text = re.sub(r'<hr\s*/?>', '\n---\n', text, flags=re.IGNORECASE)
1623
+
1624
+ # Remove all remaining HTML tags
1625
+ text = re.sub(r'<[^>]+>', '', text)
1626
+
1627
+ # Clean up whitespace
1628
+ text = re.sub(
1629
+ r'\n\s*\n\s*\n+', '\n\n', text
1630
+ ) # Multiple blank lines to double newline
1631
+ text = re.sub(r' +', ' ', text) # Multiple spaces to single space
1632
+ text = re.sub(r'\n ', '\n', text) # Remove leading spaces on lines
1633
+ text = re.sub(
1634
+ r' \n', '\n', text
1635
+ ) # Remove trailing spaces on lines
1636
+
1637
+ return text.strip()
1638
+
1639
+ def find_text_recursive(
1640
+ part: Dict[str, Any], target_mime: str
1641
+ ) -> Optional[str]:
1642
+ """Recursively search for text content of a specific MIME type.
1643
+
1644
+ Args:
1645
+ part: Message part to search in.
1646
+ target_mime: Target MIME type ('text/plain' or 'text/html').
1647
+
1648
+ Returns:
1649
+ Decoded text string if found, None otherwise.
1650
+ """
1651
+ mime = part.get('mimeType', '')
1652
+
1653
+ # Found the target type at this level
1654
+ if mime == target_mime:
1655
+ data = part.get('body', {}).get('data', '')
1656
+ decoded = decode_text_data(data, target_mime)
1657
+ # Strip HTML tags if this is HTML content
1658
+ if decoded and target_mime == 'text/html':
1659
+ return strip_html_tags(decoded)
1660
+ return decoded
1661
+
1662
+ # Not found, but has nested parts? Search recursively
1663
+ if 'parts' in part:
1664
+ for nested_part in part['parts']:
1665
+ result = find_text_recursive(nested_part, target_mime)
1666
+ if result:
1667
+ return result
1668
+
1669
+ return None
1670
+
1671
+ def extract_from_part(part: Dict[str, Any]):
1672
+ """Recursively collect all text from message parts."""
1673
+ mime_type = part.get('mimeType', '')
1674
+
1675
+ # Special handling for multipart/alternative
1676
+ if mime_type == 'multipart/alternative' and 'parts' in part:
1677
+ # Recursively search for one format (prefer plain text)
1678
+ plain_text = None
1679
+ html_text = None
1680
+
1681
+ # Search each alternative branch recursively
1682
+ for nested_part in part['parts']:
1683
+ if not plain_text:
1684
+ plain_text = find_text_recursive(
1685
+ nested_part, 'text/plain'
1686
+ )
1687
+ if not html_text:
1688
+ html_text = find_text_recursive(
1689
+ nested_part, 'text/html'
1690
+ )
1691
+
1692
+ # Prefer plain text, fall back to HTML
1693
+ chosen_text = plain_text if plain_text else html_text
1694
+ if chosen_text:
1695
+ text_parts.append(chosen_text)
1696
+
1697
+ # If this part has nested parts (but not multipart/alternative)
1698
+ elif 'parts' in part:
1699
+ for nested_part in part['parts']:
1700
+ extract_from_part(nested_part)
1701
+
1702
+ # If this is a text leaf, extract and collect it
1703
+ elif mime_type == 'text/plain':
1704
+ data = part.get('body', {}).get('data', '')
1705
+ text = decode_text_data(data, 'plain text body')
1706
+ if text:
1707
+ text_parts.append(text)
1708
+
1709
+ elif mime_type == 'text/html':
1710
+ data = part.get('body', {}).get('data', '')
1711
+ html_text = decode_text_data(data, 'HTML body')
1712
+ if html_text:
1713
+ text = strip_html_tags(html_text)
1714
+ if text:
1715
+ text_parts.append(text)
1716
+
1717
+ # Traverse the entire tree and collect all text parts
1718
+ payload = message.get('payload', {})
1719
+ extract_from_part(payload)
1720
+
1721
+ if not text_parts:
1722
+ return ""
1723
+
1724
+ # Return all text parts combined
1725
+ return '\n\n'.join(text_parts)
1726
+
1727
+ def _extract_attachments(
1728
+ self, message: Dict[str, Any], include_inline: bool = False
1729
+ ) -> List[Dict[str, Any]]:
1730
+ r"""Extract attachment information from message payload.
1731
+
1732
+ Recursively traverses the message tree to find all attachments
1733
+ and extracts their metadata. Distinguishes between regular attachments
1734
+ and inline images embedded in HTML content.
1735
+
1736
+ Args:
1737
+ message (Dict[str, Any]): The Gmail message dictionary containing
1738
+ the payload to extract attachments from.
1739
+
1740
+ Returns:
1741
+ List[Dict[str, Any]]: List of attachment dictionaries, each
1742
+ containing:
1743
+ - attachment_id: Gmail's unique identifier for the attachment
1744
+ - filename: Name of the attached file
1745
+ - mime_type: MIME type of the attachment
1746
+ - size: Size of the attachment in bytes
1747
+ - is_inline: Whether this is an inline image (embedded in HTML)
1748
+ """
1749
+ attachments = []
1750
+
1751
+ def is_inline_image(part: Dict[str, Any]) -> bool:
1752
+ """Check if this part is an inline image."""
1753
+ headers = part.get('headers', [])
1754
+ for header in headers:
1755
+ name = header.get('name', '').lower()
1756
+ value = header.get('value', '').lower()
1757
+ # Check for Content-Disposition: inline
1758
+ if name == 'content-disposition' and 'inline' in value:
1759
+ return True
1760
+ # Check for Content-ID (usually indicates inline)
1761
+ if name == 'content-id':
1762
+ return True
1763
+ return False
1764
+
1765
+ def find_attachments(part: Dict[str, Any]):
1766
+ """Recursively find attachments in message parts."""
1767
+ # Check if this part has an attachmentId (indicates it's an
1768
+ # attachment)
1769
+ if 'body' in part and 'attachmentId' in part['body']:
1770
+ attachment_info = {
1771
+ 'attachment_id': part['body']['attachmentId'],
1772
+ 'filename': part.get('filename', 'unnamed'),
1773
+ 'mime_type': part.get(
1774
+ 'mimeType', 'application/octet-stream'
1775
+ ),
1776
+ 'size': part['body'].get('size', 0),
1777
+ 'is_inline': is_inline_image(part),
1778
+ }
1779
+ attachments.append(attachment_info)
1780
+
1781
+ # Recurse into nested parts
1782
+ if 'parts' in part:
1783
+ for nested_part in part['parts']:
1784
+ find_attachments(nested_part)
1785
+
1786
+ # Start traversal from the message payload
1787
+ payload = message.get('payload', {})
1788
+ if payload:
1789
+ find_attachments(payload)
1790
+
1791
+ # Return based on include_inline toggle
1792
+ if include_inline:
1793
+ return attachments
1794
+ return [att for att in attachments if not att['is_inline']]
1795
+
1796
+ def _is_valid_email(self, email: str) -> bool:
1797
+ r"""Validate email address format.
1798
+
1799
+ Supports both formats:
1800
+ - Plain email: john@example.com
1801
+ - Named email: John Doe <john@example.com>
1802
+ """
1803
+ # Extract email from "Name <email>" format if present
1804
+ match = re.search(r'<([^>]+)>$', email.strip())
1805
+ email_to_check = match.group(1) if match else email.strip()
1806
+
1807
+ # Validate the email address
1808
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
1809
+ return re.match(pattern, email_to_check) is not None
1810
+
1811
+ def get_tools(self) -> List[FunctionTool]:
1812
+ r"""Returns a list of FunctionTool objects representing the
1813
+ functions in the toolkit.
1814
+
1815
+ Returns:
1816
+ List[FunctionTool]: A list of FunctionTool objects
1817
+ representing the functions in the toolkit.
1818
+ """
1819
+ return [
1820
+ FunctionTool(self.gmail_send_email),
1821
+ FunctionTool(self.gmail_reply_to_email),
1822
+ FunctionTool(self.gmail_forward_email),
1823
+ FunctionTool(self.gmail_create_draft),
1824
+ FunctionTool(self.gmail_send_draft),
1825
+ FunctionTool(self.gmail_fetch_emails),
1826
+ FunctionTool(self.gmail_fetch_thread_by_id),
1827
+ FunctionTool(self.gmail_modify_email_labels),
1828
+ FunctionTool(self.gmail_move_to_trash),
1829
+ FunctionTool(self.gmail_get_attachment),
1830
+ FunctionTool(self.gmail_list_threads),
1831
+ FunctionTool(self.gmail_list_drafts),
1832
+ FunctionTool(self.gmail_list_labels),
1833
+ FunctionTool(self.gmail_create_label),
1834
+ FunctionTool(self.gmail_delete_label),
1835
+ FunctionTool(self.gmail_modify_thread_labels),
1836
+ FunctionTool(self.gmail_get_profile),
1837
+ FunctionTool(self.gmail_get_contacts),
1838
+ FunctionTool(self.gmail_search_people),
1839
+ ]