agno 2.2.13__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 (575) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +51 -0
  3. agno/agent/agent.py +10405 -0
  4. agno/api/__init__.py +0 -0
  5. agno/api/agent.py +28 -0
  6. agno/api/api.py +40 -0
  7. agno/api/evals.py +22 -0
  8. agno/api/os.py +17 -0
  9. agno/api/routes.py +13 -0
  10. agno/api/schemas/__init__.py +9 -0
  11. agno/api/schemas/agent.py +16 -0
  12. agno/api/schemas/evals.py +16 -0
  13. agno/api/schemas/os.py +14 -0
  14. agno/api/schemas/response.py +6 -0
  15. agno/api/schemas/team.py +16 -0
  16. agno/api/schemas/utils.py +21 -0
  17. agno/api/schemas/workflows.py +16 -0
  18. agno/api/settings.py +53 -0
  19. agno/api/team.py +30 -0
  20. agno/api/workflow.py +28 -0
  21. agno/cloud/aws/base.py +214 -0
  22. agno/cloud/aws/s3/__init__.py +2 -0
  23. agno/cloud/aws/s3/api_client.py +43 -0
  24. agno/cloud/aws/s3/bucket.py +195 -0
  25. agno/cloud/aws/s3/object.py +57 -0
  26. agno/culture/__init__.py +3 -0
  27. agno/culture/manager.py +956 -0
  28. agno/db/__init__.py +24 -0
  29. agno/db/async_postgres/__init__.py +3 -0
  30. agno/db/base.py +598 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2042 -0
  33. agno/db/dynamo/schemas.py +314 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +1795 -0
  37. agno/db/firestore/schemas.py +140 -0
  38. agno/db/firestore/utils.py +376 -0
  39. agno/db/gcs_json/__init__.py +3 -0
  40. agno/db/gcs_json/gcs_json_db.py +1335 -0
  41. agno/db/gcs_json/utils.py +228 -0
  42. agno/db/in_memory/__init__.py +3 -0
  43. agno/db/in_memory/in_memory_db.py +1160 -0
  44. agno/db/in_memory/utils.py +230 -0
  45. agno/db/json/__init__.py +3 -0
  46. agno/db/json/json_db.py +1328 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/__init__.py +0 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/mongo/__init__.py +17 -0
  51. agno/db/mongo/async_mongo.py +2026 -0
  52. agno/db/mongo/mongo.py +1982 -0
  53. agno/db/mongo/schemas.py +87 -0
  54. agno/db/mongo/utils.py +259 -0
  55. agno/db/mysql/__init__.py +3 -0
  56. agno/db/mysql/mysql.py +2308 -0
  57. agno/db/mysql/schemas.py +138 -0
  58. agno/db/mysql/utils.py +355 -0
  59. agno/db/postgres/__init__.py +4 -0
  60. agno/db/postgres/async_postgres.py +1927 -0
  61. agno/db/postgres/postgres.py +2260 -0
  62. agno/db/postgres/schemas.py +139 -0
  63. agno/db/postgres/utils.py +442 -0
  64. agno/db/redis/__init__.py +3 -0
  65. agno/db/redis/redis.py +1660 -0
  66. agno/db/redis/schemas.py +123 -0
  67. agno/db/redis/utils.py +346 -0
  68. agno/db/schemas/__init__.py +4 -0
  69. agno/db/schemas/culture.py +120 -0
  70. agno/db/schemas/evals.py +33 -0
  71. agno/db/schemas/knowledge.py +40 -0
  72. agno/db/schemas/memory.py +46 -0
  73. agno/db/schemas/metrics.py +0 -0
  74. agno/db/singlestore/__init__.py +3 -0
  75. agno/db/singlestore/schemas.py +130 -0
  76. agno/db/singlestore/singlestore.py +2272 -0
  77. agno/db/singlestore/utils.py +384 -0
  78. agno/db/sqlite/__init__.py +4 -0
  79. agno/db/sqlite/async_sqlite.py +2293 -0
  80. agno/db/sqlite/schemas.py +133 -0
  81. agno/db/sqlite/sqlite.py +2288 -0
  82. agno/db/sqlite/utils.py +431 -0
  83. agno/db/surrealdb/__init__.py +3 -0
  84. agno/db/surrealdb/metrics.py +292 -0
  85. agno/db/surrealdb/models.py +309 -0
  86. agno/db/surrealdb/queries.py +71 -0
  87. agno/db/surrealdb/surrealdb.py +1353 -0
  88. agno/db/surrealdb/utils.py +147 -0
  89. agno/db/utils.py +116 -0
  90. agno/debug.py +18 -0
  91. agno/eval/__init__.py +14 -0
  92. agno/eval/accuracy.py +834 -0
  93. agno/eval/performance.py +773 -0
  94. agno/eval/reliability.py +306 -0
  95. agno/eval/utils.py +119 -0
  96. agno/exceptions.py +161 -0
  97. agno/filters.py +354 -0
  98. agno/guardrails/__init__.py +6 -0
  99. agno/guardrails/base.py +19 -0
  100. agno/guardrails/openai.py +144 -0
  101. agno/guardrails/pii.py +94 -0
  102. agno/guardrails/prompt_injection.py +52 -0
  103. agno/integrations/__init__.py +0 -0
  104. agno/integrations/discord/__init__.py +3 -0
  105. agno/integrations/discord/client.py +203 -0
  106. agno/knowledge/__init__.py +5 -0
  107. agno/knowledge/chunking/__init__.py +0 -0
  108. agno/knowledge/chunking/agentic.py +79 -0
  109. agno/knowledge/chunking/document.py +91 -0
  110. agno/knowledge/chunking/fixed.py +57 -0
  111. agno/knowledge/chunking/markdown.py +151 -0
  112. agno/knowledge/chunking/recursive.py +63 -0
  113. agno/knowledge/chunking/row.py +39 -0
  114. agno/knowledge/chunking/semantic.py +86 -0
  115. agno/knowledge/chunking/strategy.py +165 -0
  116. agno/knowledge/content.py +74 -0
  117. agno/knowledge/document/__init__.py +5 -0
  118. agno/knowledge/document/base.py +58 -0
  119. agno/knowledge/embedder/__init__.py +5 -0
  120. agno/knowledge/embedder/aws_bedrock.py +343 -0
  121. agno/knowledge/embedder/azure_openai.py +210 -0
  122. agno/knowledge/embedder/base.py +23 -0
  123. agno/knowledge/embedder/cohere.py +323 -0
  124. agno/knowledge/embedder/fastembed.py +62 -0
  125. agno/knowledge/embedder/fireworks.py +13 -0
  126. agno/knowledge/embedder/google.py +258 -0
  127. agno/knowledge/embedder/huggingface.py +94 -0
  128. agno/knowledge/embedder/jina.py +182 -0
  129. agno/knowledge/embedder/langdb.py +22 -0
  130. agno/knowledge/embedder/mistral.py +206 -0
  131. agno/knowledge/embedder/nebius.py +13 -0
  132. agno/knowledge/embedder/ollama.py +154 -0
  133. agno/knowledge/embedder/openai.py +195 -0
  134. agno/knowledge/embedder/sentence_transformer.py +63 -0
  135. agno/knowledge/embedder/together.py +13 -0
  136. agno/knowledge/embedder/vllm.py +262 -0
  137. agno/knowledge/embedder/voyageai.py +165 -0
  138. agno/knowledge/knowledge.py +1988 -0
  139. agno/knowledge/reader/__init__.py +7 -0
  140. agno/knowledge/reader/arxiv_reader.py +81 -0
  141. agno/knowledge/reader/base.py +95 -0
  142. agno/knowledge/reader/csv_reader.py +166 -0
  143. agno/knowledge/reader/docx_reader.py +82 -0
  144. agno/knowledge/reader/field_labeled_csv_reader.py +292 -0
  145. agno/knowledge/reader/firecrawl_reader.py +201 -0
  146. agno/knowledge/reader/json_reader.py +87 -0
  147. agno/knowledge/reader/markdown_reader.py +137 -0
  148. agno/knowledge/reader/pdf_reader.py +431 -0
  149. agno/knowledge/reader/pptx_reader.py +101 -0
  150. agno/knowledge/reader/reader_factory.py +313 -0
  151. agno/knowledge/reader/s3_reader.py +89 -0
  152. agno/knowledge/reader/tavily_reader.py +194 -0
  153. agno/knowledge/reader/text_reader.py +115 -0
  154. agno/knowledge/reader/web_search_reader.py +372 -0
  155. agno/knowledge/reader/website_reader.py +455 -0
  156. agno/knowledge/reader/wikipedia_reader.py +59 -0
  157. agno/knowledge/reader/youtube_reader.py +78 -0
  158. agno/knowledge/remote_content/__init__.py +0 -0
  159. agno/knowledge/remote_content/remote_content.py +88 -0
  160. agno/knowledge/reranker/__init__.py +3 -0
  161. agno/knowledge/reranker/base.py +14 -0
  162. agno/knowledge/reranker/cohere.py +64 -0
  163. agno/knowledge/reranker/infinity.py +195 -0
  164. agno/knowledge/reranker/sentence_transformer.py +54 -0
  165. agno/knowledge/types.py +39 -0
  166. agno/knowledge/utils.py +189 -0
  167. agno/media.py +462 -0
  168. agno/memory/__init__.py +3 -0
  169. agno/memory/manager.py +1327 -0
  170. agno/models/__init__.py +0 -0
  171. agno/models/aimlapi/__init__.py +5 -0
  172. agno/models/aimlapi/aimlapi.py +45 -0
  173. agno/models/anthropic/__init__.py +5 -0
  174. agno/models/anthropic/claude.py +757 -0
  175. agno/models/aws/__init__.py +15 -0
  176. agno/models/aws/bedrock.py +701 -0
  177. agno/models/aws/claude.py +378 -0
  178. agno/models/azure/__init__.py +18 -0
  179. agno/models/azure/ai_foundry.py +485 -0
  180. agno/models/azure/openai_chat.py +131 -0
  181. agno/models/base.py +2175 -0
  182. agno/models/cerebras/__init__.py +12 -0
  183. agno/models/cerebras/cerebras.py +501 -0
  184. agno/models/cerebras/cerebras_openai.py +112 -0
  185. agno/models/cohere/__init__.py +5 -0
  186. agno/models/cohere/chat.py +389 -0
  187. agno/models/cometapi/__init__.py +5 -0
  188. agno/models/cometapi/cometapi.py +57 -0
  189. agno/models/dashscope/__init__.py +5 -0
  190. agno/models/dashscope/dashscope.py +91 -0
  191. agno/models/deepinfra/__init__.py +5 -0
  192. agno/models/deepinfra/deepinfra.py +28 -0
  193. agno/models/deepseek/__init__.py +5 -0
  194. agno/models/deepseek/deepseek.py +61 -0
  195. agno/models/defaults.py +1 -0
  196. agno/models/fireworks/__init__.py +5 -0
  197. agno/models/fireworks/fireworks.py +26 -0
  198. agno/models/google/__init__.py +5 -0
  199. agno/models/google/gemini.py +1085 -0
  200. agno/models/groq/__init__.py +5 -0
  201. agno/models/groq/groq.py +556 -0
  202. agno/models/huggingface/__init__.py +5 -0
  203. agno/models/huggingface/huggingface.py +491 -0
  204. agno/models/ibm/__init__.py +5 -0
  205. agno/models/ibm/watsonx.py +422 -0
  206. agno/models/internlm/__init__.py +3 -0
  207. agno/models/internlm/internlm.py +26 -0
  208. agno/models/langdb/__init__.py +1 -0
  209. agno/models/langdb/langdb.py +48 -0
  210. agno/models/litellm/__init__.py +14 -0
  211. agno/models/litellm/chat.py +468 -0
  212. agno/models/litellm/litellm_openai.py +25 -0
  213. agno/models/llama_cpp/__init__.py +5 -0
  214. agno/models/llama_cpp/llama_cpp.py +22 -0
  215. agno/models/lmstudio/__init__.py +5 -0
  216. agno/models/lmstudio/lmstudio.py +25 -0
  217. agno/models/message.py +434 -0
  218. agno/models/meta/__init__.py +12 -0
  219. agno/models/meta/llama.py +475 -0
  220. agno/models/meta/llama_openai.py +78 -0
  221. agno/models/metrics.py +120 -0
  222. agno/models/mistral/__init__.py +5 -0
  223. agno/models/mistral/mistral.py +432 -0
  224. agno/models/nebius/__init__.py +3 -0
  225. agno/models/nebius/nebius.py +54 -0
  226. agno/models/nexus/__init__.py +3 -0
  227. agno/models/nexus/nexus.py +22 -0
  228. agno/models/nvidia/__init__.py +5 -0
  229. agno/models/nvidia/nvidia.py +28 -0
  230. agno/models/ollama/__init__.py +5 -0
  231. agno/models/ollama/chat.py +441 -0
  232. agno/models/openai/__init__.py +9 -0
  233. agno/models/openai/chat.py +883 -0
  234. agno/models/openai/like.py +27 -0
  235. agno/models/openai/responses.py +1050 -0
  236. agno/models/openrouter/__init__.py +5 -0
  237. agno/models/openrouter/openrouter.py +66 -0
  238. agno/models/perplexity/__init__.py +5 -0
  239. agno/models/perplexity/perplexity.py +187 -0
  240. agno/models/portkey/__init__.py +3 -0
  241. agno/models/portkey/portkey.py +81 -0
  242. agno/models/requesty/__init__.py +5 -0
  243. agno/models/requesty/requesty.py +52 -0
  244. agno/models/response.py +199 -0
  245. agno/models/sambanova/__init__.py +5 -0
  246. agno/models/sambanova/sambanova.py +28 -0
  247. agno/models/siliconflow/__init__.py +5 -0
  248. agno/models/siliconflow/siliconflow.py +25 -0
  249. agno/models/together/__init__.py +5 -0
  250. agno/models/together/together.py +25 -0
  251. agno/models/utils.py +266 -0
  252. agno/models/vercel/__init__.py +3 -0
  253. agno/models/vercel/v0.py +26 -0
  254. agno/models/vertexai/__init__.py +0 -0
  255. agno/models/vertexai/claude.py +70 -0
  256. agno/models/vllm/__init__.py +3 -0
  257. agno/models/vllm/vllm.py +78 -0
  258. agno/models/xai/__init__.py +3 -0
  259. agno/models/xai/xai.py +113 -0
  260. agno/os/__init__.py +3 -0
  261. agno/os/app.py +876 -0
  262. agno/os/auth.py +57 -0
  263. agno/os/config.py +104 -0
  264. agno/os/interfaces/__init__.py +1 -0
  265. agno/os/interfaces/a2a/__init__.py +3 -0
  266. agno/os/interfaces/a2a/a2a.py +42 -0
  267. agno/os/interfaces/a2a/router.py +250 -0
  268. agno/os/interfaces/a2a/utils.py +924 -0
  269. agno/os/interfaces/agui/__init__.py +3 -0
  270. agno/os/interfaces/agui/agui.py +47 -0
  271. agno/os/interfaces/agui/router.py +144 -0
  272. agno/os/interfaces/agui/utils.py +534 -0
  273. agno/os/interfaces/base.py +25 -0
  274. agno/os/interfaces/slack/__init__.py +3 -0
  275. agno/os/interfaces/slack/router.py +148 -0
  276. agno/os/interfaces/slack/security.py +30 -0
  277. agno/os/interfaces/slack/slack.py +47 -0
  278. agno/os/interfaces/whatsapp/__init__.py +3 -0
  279. agno/os/interfaces/whatsapp/router.py +211 -0
  280. agno/os/interfaces/whatsapp/security.py +53 -0
  281. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  282. agno/os/mcp.py +292 -0
  283. agno/os/middleware/__init__.py +7 -0
  284. agno/os/middleware/jwt.py +233 -0
  285. agno/os/router.py +1763 -0
  286. agno/os/routers/__init__.py +3 -0
  287. agno/os/routers/evals/__init__.py +3 -0
  288. agno/os/routers/evals/evals.py +430 -0
  289. agno/os/routers/evals/schemas.py +142 -0
  290. agno/os/routers/evals/utils.py +162 -0
  291. agno/os/routers/health.py +31 -0
  292. agno/os/routers/home.py +52 -0
  293. agno/os/routers/knowledge/__init__.py +3 -0
  294. agno/os/routers/knowledge/knowledge.py +997 -0
  295. agno/os/routers/knowledge/schemas.py +178 -0
  296. agno/os/routers/memory/__init__.py +3 -0
  297. agno/os/routers/memory/memory.py +515 -0
  298. agno/os/routers/memory/schemas.py +62 -0
  299. agno/os/routers/metrics/__init__.py +3 -0
  300. agno/os/routers/metrics/metrics.py +190 -0
  301. agno/os/routers/metrics/schemas.py +47 -0
  302. agno/os/routers/session/__init__.py +3 -0
  303. agno/os/routers/session/session.py +997 -0
  304. agno/os/schema.py +1055 -0
  305. agno/os/settings.py +43 -0
  306. agno/os/utils.py +630 -0
  307. agno/py.typed +0 -0
  308. agno/reasoning/__init__.py +0 -0
  309. agno/reasoning/anthropic.py +80 -0
  310. agno/reasoning/azure_ai_foundry.py +67 -0
  311. agno/reasoning/deepseek.py +63 -0
  312. agno/reasoning/default.py +97 -0
  313. agno/reasoning/gemini.py +73 -0
  314. agno/reasoning/groq.py +71 -0
  315. agno/reasoning/helpers.py +63 -0
  316. agno/reasoning/ollama.py +67 -0
  317. agno/reasoning/openai.py +86 -0
  318. agno/reasoning/step.py +31 -0
  319. agno/reasoning/vertexai.py +76 -0
  320. agno/run/__init__.py +6 -0
  321. agno/run/agent.py +787 -0
  322. agno/run/base.py +229 -0
  323. agno/run/cancel.py +81 -0
  324. agno/run/messages.py +32 -0
  325. agno/run/team.py +753 -0
  326. agno/run/workflow.py +708 -0
  327. agno/session/__init__.py +10 -0
  328. agno/session/agent.py +295 -0
  329. agno/session/summary.py +265 -0
  330. agno/session/team.py +392 -0
  331. agno/session/workflow.py +205 -0
  332. agno/team/__init__.py +37 -0
  333. agno/team/team.py +8793 -0
  334. agno/tools/__init__.py +10 -0
  335. agno/tools/agentql.py +120 -0
  336. agno/tools/airflow.py +69 -0
  337. agno/tools/api.py +122 -0
  338. agno/tools/apify.py +314 -0
  339. agno/tools/arxiv.py +127 -0
  340. agno/tools/aws_lambda.py +53 -0
  341. agno/tools/aws_ses.py +66 -0
  342. agno/tools/baidusearch.py +89 -0
  343. agno/tools/bitbucket.py +292 -0
  344. agno/tools/brandfetch.py +213 -0
  345. agno/tools/bravesearch.py +106 -0
  346. agno/tools/brightdata.py +367 -0
  347. agno/tools/browserbase.py +209 -0
  348. agno/tools/calcom.py +255 -0
  349. agno/tools/calculator.py +151 -0
  350. agno/tools/cartesia.py +187 -0
  351. agno/tools/clickup.py +244 -0
  352. agno/tools/confluence.py +240 -0
  353. agno/tools/crawl4ai.py +158 -0
  354. agno/tools/csv_toolkit.py +185 -0
  355. agno/tools/dalle.py +110 -0
  356. agno/tools/daytona.py +475 -0
  357. agno/tools/decorator.py +262 -0
  358. agno/tools/desi_vocal.py +108 -0
  359. agno/tools/discord.py +161 -0
  360. agno/tools/docker.py +716 -0
  361. agno/tools/duckdb.py +379 -0
  362. agno/tools/duckduckgo.py +91 -0
  363. agno/tools/e2b.py +703 -0
  364. agno/tools/eleven_labs.py +196 -0
  365. agno/tools/email.py +67 -0
  366. agno/tools/evm.py +129 -0
  367. agno/tools/exa.py +396 -0
  368. agno/tools/fal.py +127 -0
  369. agno/tools/file.py +240 -0
  370. agno/tools/file_generation.py +350 -0
  371. agno/tools/financial_datasets.py +288 -0
  372. agno/tools/firecrawl.py +143 -0
  373. agno/tools/function.py +1187 -0
  374. agno/tools/giphy.py +93 -0
  375. agno/tools/github.py +1760 -0
  376. agno/tools/gmail.py +922 -0
  377. agno/tools/google_bigquery.py +117 -0
  378. agno/tools/google_drive.py +270 -0
  379. agno/tools/google_maps.py +253 -0
  380. agno/tools/googlecalendar.py +674 -0
  381. agno/tools/googlesearch.py +98 -0
  382. agno/tools/googlesheets.py +377 -0
  383. agno/tools/hackernews.py +77 -0
  384. agno/tools/jina.py +101 -0
  385. agno/tools/jira.py +170 -0
  386. agno/tools/knowledge.py +218 -0
  387. agno/tools/linear.py +426 -0
  388. agno/tools/linkup.py +58 -0
  389. agno/tools/local_file_system.py +90 -0
  390. agno/tools/lumalab.py +183 -0
  391. agno/tools/mcp/__init__.py +10 -0
  392. agno/tools/mcp/mcp.py +331 -0
  393. agno/tools/mcp/multi_mcp.py +347 -0
  394. agno/tools/mcp/params.py +24 -0
  395. agno/tools/mcp_toolbox.py +284 -0
  396. agno/tools/mem0.py +193 -0
  397. agno/tools/memori.py +339 -0
  398. agno/tools/memory.py +419 -0
  399. agno/tools/mlx_transcribe.py +139 -0
  400. agno/tools/models/__init__.py +0 -0
  401. agno/tools/models/azure_openai.py +190 -0
  402. agno/tools/models/gemini.py +203 -0
  403. agno/tools/models/groq.py +158 -0
  404. agno/tools/models/morph.py +186 -0
  405. agno/tools/models/nebius.py +124 -0
  406. agno/tools/models_labs.py +195 -0
  407. agno/tools/moviepy_video.py +349 -0
  408. agno/tools/neo4j.py +134 -0
  409. agno/tools/newspaper.py +46 -0
  410. agno/tools/newspaper4k.py +93 -0
  411. agno/tools/notion.py +204 -0
  412. agno/tools/openai.py +202 -0
  413. agno/tools/openbb.py +160 -0
  414. agno/tools/opencv.py +321 -0
  415. agno/tools/openweather.py +233 -0
  416. agno/tools/oxylabs.py +385 -0
  417. agno/tools/pandas.py +102 -0
  418. agno/tools/parallel.py +314 -0
  419. agno/tools/postgres.py +257 -0
  420. agno/tools/pubmed.py +188 -0
  421. agno/tools/python.py +205 -0
  422. agno/tools/reasoning.py +283 -0
  423. agno/tools/reddit.py +467 -0
  424. agno/tools/replicate.py +117 -0
  425. agno/tools/resend.py +62 -0
  426. agno/tools/scrapegraph.py +222 -0
  427. agno/tools/searxng.py +152 -0
  428. agno/tools/serpapi.py +116 -0
  429. agno/tools/serper.py +255 -0
  430. agno/tools/shell.py +53 -0
  431. agno/tools/slack.py +136 -0
  432. agno/tools/sleep.py +20 -0
  433. agno/tools/spider.py +116 -0
  434. agno/tools/sql.py +154 -0
  435. agno/tools/streamlit/__init__.py +0 -0
  436. agno/tools/streamlit/components.py +113 -0
  437. agno/tools/tavily.py +254 -0
  438. agno/tools/telegram.py +48 -0
  439. agno/tools/todoist.py +218 -0
  440. agno/tools/tool_registry.py +1 -0
  441. agno/tools/toolkit.py +146 -0
  442. agno/tools/trafilatura.py +388 -0
  443. agno/tools/trello.py +274 -0
  444. agno/tools/twilio.py +186 -0
  445. agno/tools/user_control_flow.py +78 -0
  446. agno/tools/valyu.py +228 -0
  447. agno/tools/visualization.py +467 -0
  448. agno/tools/webbrowser.py +28 -0
  449. agno/tools/webex.py +76 -0
  450. agno/tools/website.py +54 -0
  451. agno/tools/webtools.py +45 -0
  452. agno/tools/whatsapp.py +286 -0
  453. agno/tools/wikipedia.py +63 -0
  454. agno/tools/workflow.py +278 -0
  455. agno/tools/x.py +335 -0
  456. agno/tools/yfinance.py +257 -0
  457. agno/tools/youtube.py +184 -0
  458. agno/tools/zendesk.py +82 -0
  459. agno/tools/zep.py +454 -0
  460. agno/tools/zoom.py +382 -0
  461. agno/utils/__init__.py +0 -0
  462. agno/utils/agent.py +820 -0
  463. agno/utils/audio.py +49 -0
  464. agno/utils/certs.py +27 -0
  465. agno/utils/code_execution.py +11 -0
  466. agno/utils/common.py +132 -0
  467. agno/utils/dttm.py +13 -0
  468. agno/utils/enum.py +22 -0
  469. agno/utils/env.py +11 -0
  470. agno/utils/events.py +696 -0
  471. agno/utils/format_str.py +16 -0
  472. agno/utils/functions.py +166 -0
  473. agno/utils/gemini.py +426 -0
  474. agno/utils/hooks.py +57 -0
  475. agno/utils/http.py +74 -0
  476. agno/utils/json_schema.py +234 -0
  477. agno/utils/knowledge.py +36 -0
  478. agno/utils/location.py +19 -0
  479. agno/utils/log.py +255 -0
  480. agno/utils/mcp.py +214 -0
  481. agno/utils/media.py +352 -0
  482. agno/utils/merge_dict.py +41 -0
  483. agno/utils/message.py +118 -0
  484. agno/utils/models/__init__.py +0 -0
  485. agno/utils/models/ai_foundry.py +43 -0
  486. agno/utils/models/claude.py +358 -0
  487. agno/utils/models/cohere.py +87 -0
  488. agno/utils/models/llama.py +78 -0
  489. agno/utils/models/mistral.py +98 -0
  490. agno/utils/models/openai_responses.py +140 -0
  491. agno/utils/models/schema_utils.py +153 -0
  492. agno/utils/models/watsonx.py +41 -0
  493. agno/utils/openai.py +257 -0
  494. agno/utils/pickle.py +32 -0
  495. agno/utils/pprint.py +178 -0
  496. agno/utils/print_response/__init__.py +0 -0
  497. agno/utils/print_response/agent.py +842 -0
  498. agno/utils/print_response/team.py +1724 -0
  499. agno/utils/print_response/workflow.py +1668 -0
  500. agno/utils/prompts.py +111 -0
  501. agno/utils/reasoning.py +108 -0
  502. agno/utils/response.py +163 -0
  503. agno/utils/response_iterator.py +17 -0
  504. agno/utils/safe_formatter.py +24 -0
  505. agno/utils/serialize.py +32 -0
  506. agno/utils/shell.py +22 -0
  507. agno/utils/streamlit.py +487 -0
  508. agno/utils/string.py +231 -0
  509. agno/utils/team.py +139 -0
  510. agno/utils/timer.py +41 -0
  511. agno/utils/tools.py +102 -0
  512. agno/utils/web.py +23 -0
  513. agno/utils/whatsapp.py +305 -0
  514. agno/utils/yaml_io.py +25 -0
  515. agno/vectordb/__init__.py +3 -0
  516. agno/vectordb/base.py +127 -0
  517. agno/vectordb/cassandra/__init__.py +5 -0
  518. agno/vectordb/cassandra/cassandra.py +501 -0
  519. agno/vectordb/cassandra/extra_param_mixin.py +11 -0
  520. agno/vectordb/cassandra/index.py +13 -0
  521. agno/vectordb/chroma/__init__.py +5 -0
  522. agno/vectordb/chroma/chromadb.py +929 -0
  523. agno/vectordb/clickhouse/__init__.py +9 -0
  524. agno/vectordb/clickhouse/clickhousedb.py +835 -0
  525. agno/vectordb/clickhouse/index.py +9 -0
  526. agno/vectordb/couchbase/__init__.py +3 -0
  527. agno/vectordb/couchbase/couchbase.py +1442 -0
  528. agno/vectordb/distance.py +7 -0
  529. agno/vectordb/lancedb/__init__.py +6 -0
  530. agno/vectordb/lancedb/lance_db.py +995 -0
  531. agno/vectordb/langchaindb/__init__.py +5 -0
  532. agno/vectordb/langchaindb/langchaindb.py +163 -0
  533. agno/vectordb/lightrag/__init__.py +5 -0
  534. agno/vectordb/lightrag/lightrag.py +388 -0
  535. agno/vectordb/llamaindex/__init__.py +3 -0
  536. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  537. agno/vectordb/milvus/__init__.py +4 -0
  538. agno/vectordb/milvus/milvus.py +1182 -0
  539. agno/vectordb/mongodb/__init__.py +9 -0
  540. agno/vectordb/mongodb/mongodb.py +1417 -0
  541. agno/vectordb/pgvector/__init__.py +12 -0
  542. agno/vectordb/pgvector/index.py +23 -0
  543. agno/vectordb/pgvector/pgvector.py +1462 -0
  544. agno/vectordb/pineconedb/__init__.py +5 -0
  545. agno/vectordb/pineconedb/pineconedb.py +747 -0
  546. agno/vectordb/qdrant/__init__.py +5 -0
  547. agno/vectordb/qdrant/qdrant.py +1134 -0
  548. agno/vectordb/redis/__init__.py +9 -0
  549. agno/vectordb/redis/redisdb.py +694 -0
  550. agno/vectordb/search.py +7 -0
  551. agno/vectordb/singlestore/__init__.py +10 -0
  552. agno/vectordb/singlestore/index.py +41 -0
  553. agno/vectordb/singlestore/singlestore.py +763 -0
  554. agno/vectordb/surrealdb/__init__.py +3 -0
  555. agno/vectordb/surrealdb/surrealdb.py +699 -0
  556. agno/vectordb/upstashdb/__init__.py +5 -0
  557. agno/vectordb/upstashdb/upstashdb.py +718 -0
  558. agno/vectordb/weaviate/__init__.py +8 -0
  559. agno/vectordb/weaviate/index.py +15 -0
  560. agno/vectordb/weaviate/weaviate.py +1005 -0
  561. agno/workflow/__init__.py +23 -0
  562. agno/workflow/agent.py +299 -0
  563. agno/workflow/condition.py +738 -0
  564. agno/workflow/loop.py +735 -0
  565. agno/workflow/parallel.py +824 -0
  566. agno/workflow/router.py +702 -0
  567. agno/workflow/step.py +1432 -0
  568. agno/workflow/steps.py +592 -0
  569. agno/workflow/types.py +520 -0
  570. agno/workflow/workflow.py +4321 -0
  571. agno-2.2.13.dist-info/METADATA +614 -0
  572. agno-2.2.13.dist-info/RECORD +575 -0
  573. agno-2.2.13.dist-info/WHEEL +5 -0
  574. agno-2.2.13.dist-info/licenses/LICENSE +201 -0
  575. agno-2.2.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1085 @@
1
+ import json
2
+ import time
3
+ from collections.abc import AsyncIterator
4
+ from dataclasses import dataclass
5
+ from os import getenv
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Iterator, List, Optional, Type, Union
8
+ from uuid import uuid4
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from agno.exceptions import ModelProviderError
13
+ from agno.media import Audio, File, Image, Video
14
+ from agno.models.base import Model
15
+ from agno.models.message import Citations, Message, UrlCitation
16
+ from agno.models.metrics import Metrics
17
+ from agno.models.response import ModelResponse
18
+ from agno.run.agent import RunOutput
19
+ from agno.utils.gemini import format_function_definitions, format_image_for_message, prepare_response_schema
20
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
21
+
22
+ try:
23
+ from google import genai
24
+ from google.genai import Client as GeminiClient
25
+ from google.genai.errors import ClientError, ServerError
26
+ from google.genai.types import (
27
+ Content,
28
+ DynamicRetrievalConfig,
29
+ FunctionCallingConfigMode,
30
+ GenerateContentConfig,
31
+ GenerateContentResponse,
32
+ GenerateContentResponseUsageMetadata,
33
+ GoogleSearch,
34
+ GoogleSearchRetrieval,
35
+ Part,
36
+ Retrieval,
37
+ ThinkingConfig,
38
+ Tool,
39
+ UrlContext,
40
+ VertexAISearch,
41
+ )
42
+ from google.genai.types import (
43
+ File as GeminiFile,
44
+ )
45
+ except ImportError:
46
+ raise ImportError("`google-genai` not installed. Please install it using `pip install google-genai`")
47
+
48
+
49
+ @dataclass
50
+ class Gemini(Model):
51
+ """
52
+ Gemini model class for Google's Generative AI models.
53
+
54
+ Vertex AI:
55
+ - You will need Google Cloud credentials to use the Vertex AI API. Run `gcloud auth application-default login` to set credentials.
56
+ - Set `vertexai` to `True` to use the Vertex AI API.
57
+ - Set your `project_id` (or set `GOOGLE_CLOUD_PROJECT` environment variable) and `location` (optional).
58
+ - Set `http_options` (optional) to configure the HTTP options.
59
+
60
+ Based on https://googleapis.github.io/python-genai/
61
+ """
62
+
63
+ id: str = "gemini-2.0-flash-001"
64
+ name: str = "Gemini"
65
+ provider: str = "Google"
66
+
67
+ supports_native_structured_outputs: bool = True
68
+
69
+ # Request parameters
70
+ function_declarations: Optional[List[Any]] = None
71
+ generation_config: Optional[Any] = None
72
+ safety_settings: Optional[List[Any]] = None
73
+ generative_model_kwargs: Optional[Dict[str, Any]] = None
74
+ search: bool = False
75
+ grounding: bool = False
76
+ grounding_dynamic_threshold: Optional[float] = None
77
+ url_context: bool = False
78
+ vertexai_search: bool = False
79
+ vertexai_search_datastore: Optional[str] = None
80
+
81
+ temperature: Optional[float] = None
82
+ top_p: Optional[float] = None
83
+ top_k: Optional[int] = None
84
+ max_output_tokens: Optional[int] = None
85
+ stop_sequences: Optional[list[str]] = None
86
+ logprobs: Optional[bool] = None
87
+ presence_penalty: Optional[float] = None
88
+ frequency_penalty: Optional[float] = None
89
+ seed: Optional[int] = None
90
+ response_modalities: Optional[list[str]] = None # "TEXT", "IMAGE", and/or "AUDIO"
91
+ speech_config: Optional[dict[str, Any]] = None
92
+ cached_content: Optional[Any] = None
93
+ thinking_budget: Optional[int] = None # Thinking budget for Gemini 2.5 models
94
+ include_thoughts: Optional[bool] = None # Include thought summaries in response
95
+ request_params: Optional[Dict[str, Any]] = None
96
+
97
+ # Client parameters
98
+ api_key: Optional[str] = None
99
+ vertexai: bool = False
100
+ project_id: Optional[str] = None
101
+ location: Optional[str] = None
102
+ client_params: Optional[Dict[str, Any]] = None
103
+
104
+ # Gemini client
105
+ client: Optional[GeminiClient] = None
106
+
107
+ # The role to map the Gemini response
108
+ role_map = {
109
+ "model": "assistant",
110
+ }
111
+
112
+ # The role to map the Message
113
+ reverse_role_map = {
114
+ "assistant": "model",
115
+ "tool": "user",
116
+ }
117
+
118
+ def get_client(self) -> GeminiClient:
119
+ """
120
+ Returns an instance of the GeminiClient client.
121
+
122
+ Returns:
123
+ GeminiClient: The GeminiClient client.
124
+ """
125
+ if self.client:
126
+ return self.client
127
+ client_params: Dict[str, Any] = {}
128
+ vertexai = self.vertexai or getenv("GOOGLE_GENAI_USE_VERTEXAI", "false").lower() == "true"
129
+
130
+ if not vertexai:
131
+ self.api_key = self.api_key or getenv("GOOGLE_API_KEY")
132
+ if not self.api_key:
133
+ log_error("GOOGLE_API_KEY not set. Please set the GOOGLE_API_KEY environment variable.")
134
+ client_params["api_key"] = self.api_key
135
+ else:
136
+ log_info("Using Vertex AI API")
137
+ client_params["vertexai"] = True
138
+ client_params["project"] = self.project_id or getenv("GOOGLE_CLOUD_PROJECT")
139
+ client_params["location"] = self.location or getenv("GOOGLE_CLOUD_LOCATION")
140
+
141
+ client_params = {k: v for k, v in client_params.items() if v is not None}
142
+
143
+ if self.client_params:
144
+ client_params.update(self.client_params)
145
+
146
+ self.client = genai.Client(**client_params)
147
+ return self.client
148
+
149
+ def get_request_params(
150
+ self,
151
+ system_message: Optional[str] = None,
152
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
153
+ tools: Optional[List[Dict[str, Any]]] = None,
154
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
155
+ ) -> Dict[str, Any]:
156
+ """
157
+ Returns the request keyword arguments for the GenerativeModel client.
158
+ """
159
+ request_params = {}
160
+ # User provides their own generation config
161
+ if self.generation_config is not None:
162
+ if isinstance(self.generation_config, GenerateContentConfig):
163
+ config = self.generation_config.model_dump()
164
+ else:
165
+ config = self.generation_config
166
+ else:
167
+ config = {}
168
+
169
+ if self.generative_model_kwargs:
170
+ config.update(self.generative_model_kwargs)
171
+
172
+ config.update(
173
+ {
174
+ "safety_settings": self.safety_settings,
175
+ "temperature": self.temperature,
176
+ "top_p": self.top_p,
177
+ "top_k": self.top_k,
178
+ "max_output_tokens": self.max_output_tokens,
179
+ "stop_sequences": self.stop_sequences,
180
+ "logprobs": self.logprobs,
181
+ "presence_penalty": self.presence_penalty,
182
+ "frequency_penalty": self.frequency_penalty,
183
+ "seed": self.seed,
184
+ "response_modalities": self.response_modalities,
185
+ "speech_config": self.speech_config,
186
+ "cached_content": self.cached_content,
187
+ }
188
+ )
189
+
190
+ if system_message is not None:
191
+ config["system_instruction"] = system_message # type: ignore
192
+
193
+ if response_format is not None and isinstance(response_format, type) and issubclass(response_format, BaseModel):
194
+ config["response_mime_type"] = "application/json" # type: ignore
195
+ # Convert Pydantic model using our hybrid approach
196
+ # This will handle complex schemas with nested models, dicts, and circular refs
197
+ config["response_schema"] = prepare_response_schema(response_format)
198
+
199
+ # Add thinking configuration
200
+ thinking_config_params = {}
201
+ if self.thinking_budget is not None:
202
+ thinking_config_params["thinking_budget"] = self.thinking_budget
203
+ if self.include_thoughts is not None:
204
+ thinking_config_params["include_thoughts"] = self.include_thoughts
205
+ if thinking_config_params:
206
+ config["thinking_config"] = ThinkingConfig(**thinking_config_params)
207
+
208
+ # Build tools array based on enabled built-in tools
209
+ builtin_tools = []
210
+
211
+ if self.grounding:
212
+ log_info(
213
+ "Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead."
214
+ )
215
+ builtin_tools.append(
216
+ Tool(
217
+ google_search=GoogleSearchRetrieval(
218
+ dynamic_retrieval_config=DynamicRetrievalConfig(
219
+ dynamic_threshold=self.grounding_dynamic_threshold
220
+ )
221
+ )
222
+ )
223
+ )
224
+
225
+ if self.search:
226
+ log_info("Google Search enabled.")
227
+ builtin_tools.append(Tool(google_search=GoogleSearch()))
228
+
229
+ if self.url_context:
230
+ log_info("URL context enabled.")
231
+ builtin_tools.append(Tool(url_context=UrlContext()))
232
+
233
+ if self.vertexai_search:
234
+ log_info("Vertex AI Search enabled.")
235
+ if not self.vertexai_search_datastore:
236
+ log_error("vertexai_search_datastore must be provided when vertexai_search is enabled.")
237
+ raise ValueError("vertexai_search_datastore must be provided when vertexai_search is enabled.")
238
+ builtin_tools.append(
239
+ Tool(retrieval=Retrieval(vertex_ai_search=VertexAISearch(datastore=self.vertexai_search_datastore)))
240
+ )
241
+
242
+ # Set tools in config
243
+ if builtin_tools:
244
+ if tools:
245
+ log_info("Built-in tools enabled. External tools will be disabled.")
246
+ config["tools"] = builtin_tools
247
+ elif tools:
248
+ config["tools"] = [format_function_definitions(tools)]
249
+
250
+ if tool_choice is not None:
251
+ if isinstance(tool_choice, str) and tool_choice.lower() == "auto":
252
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.AUTO}}
253
+ elif isinstance(tool_choice, str) and tool_choice.lower() == "none":
254
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.NONE}}
255
+ elif isinstance(tool_choice, str) and tool_choice.lower() == "validated":
256
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.VALIDATED}}
257
+ elif isinstance(tool_choice, str) and tool_choice.lower() == "any":
258
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.ANY}}
259
+ else:
260
+ config["tool_config"] = {"function_calling_config": {"mode": tool_choice}}
261
+
262
+ config = {k: v for k, v in config.items() if v is not None}
263
+
264
+ if config:
265
+ request_params["config"] = GenerateContentConfig(**config)
266
+
267
+ # Filter out None values
268
+ if self.request_params:
269
+ request_params.update(self.request_params)
270
+
271
+ if request_params:
272
+ log_debug(f"Calling {self.provider} with request parameters: {request_params}", log_level=2)
273
+ return request_params
274
+
275
+ def invoke(
276
+ self,
277
+ messages: List[Message],
278
+ assistant_message: Message,
279
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
280
+ tools: Optional[List[Dict[str, Any]]] = None,
281
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
282
+ run_response: Optional[RunOutput] = None,
283
+ ) -> ModelResponse:
284
+ """
285
+ Invokes the model with a list of messages and returns the response.
286
+ """
287
+ formatted_messages, system_message = self._format_messages(messages)
288
+ request_kwargs = self.get_request_params(
289
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
290
+ )
291
+ try:
292
+ if run_response and run_response.metrics:
293
+ run_response.metrics.set_time_to_first_token()
294
+
295
+ assistant_message.metrics.start_timer()
296
+ provider_response = self.get_client().models.generate_content(
297
+ model=self.id,
298
+ contents=formatted_messages,
299
+ **request_kwargs,
300
+ )
301
+ assistant_message.metrics.stop_timer()
302
+
303
+ model_response = self._parse_provider_response(provider_response, response_format=response_format)
304
+
305
+ return model_response
306
+
307
+ except (ClientError, ServerError) as e:
308
+ log_error(f"Error from Gemini API: {e}")
309
+ error_message = str(e.response) if hasattr(e, "response") else str(e)
310
+ raise ModelProviderError(
311
+ message=error_message,
312
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
313
+ model_name=self.name,
314
+ model_id=self.id,
315
+ ) from e
316
+ except Exception as e:
317
+ log_error(f"Unknown error from Gemini API: {e}")
318
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
319
+
320
+ def invoke_stream(
321
+ self,
322
+ messages: List[Message],
323
+ assistant_message: Message,
324
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
325
+ tools: Optional[List[Dict[str, Any]]] = None,
326
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
327
+ run_response: Optional[RunOutput] = None,
328
+ ) -> Iterator[ModelResponse]:
329
+ """
330
+ Invokes the model with a list of messages and returns the response as a stream.
331
+ """
332
+ formatted_messages, system_message = self._format_messages(messages)
333
+
334
+ request_kwargs = self.get_request_params(
335
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
336
+ )
337
+ try:
338
+ if run_response and run_response.metrics:
339
+ run_response.metrics.set_time_to_first_token()
340
+
341
+ assistant_message.metrics.start_timer()
342
+ for response in self.get_client().models.generate_content_stream(
343
+ model=self.id,
344
+ contents=formatted_messages,
345
+ **request_kwargs,
346
+ ):
347
+ yield self._parse_provider_response_delta(response)
348
+
349
+ assistant_message.metrics.stop_timer()
350
+
351
+ except (ClientError, ServerError) as e:
352
+ log_error(f"Error from Gemini API: {e}")
353
+ raise ModelProviderError(
354
+ message=str(e.response) if hasattr(e, "response") else str(e),
355
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
356
+ model_name=self.name,
357
+ model_id=self.id,
358
+ ) from e
359
+ except Exception as e:
360
+ log_error(f"Unknown error from Gemini API: {e}")
361
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
362
+
363
+ async def ainvoke(
364
+ self,
365
+ messages: List[Message],
366
+ assistant_message: Message,
367
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
368
+ tools: Optional[List[Dict[str, Any]]] = None,
369
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
370
+ run_response: Optional[RunOutput] = None,
371
+ ) -> ModelResponse:
372
+ """
373
+ Invokes the model with a list of messages and returns the response.
374
+ """
375
+ formatted_messages, system_message = self._format_messages(messages)
376
+
377
+ request_kwargs = self.get_request_params(
378
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
379
+ )
380
+
381
+ try:
382
+ if run_response and run_response.metrics:
383
+ run_response.metrics.set_time_to_first_token()
384
+
385
+ assistant_message.metrics.start_timer()
386
+ provider_response = await self.get_client().aio.models.generate_content(
387
+ model=self.id,
388
+ contents=formatted_messages,
389
+ **request_kwargs,
390
+ )
391
+ assistant_message.metrics.stop_timer()
392
+
393
+ model_response = self._parse_provider_response(provider_response, response_format=response_format)
394
+
395
+ return model_response
396
+
397
+ except (ClientError, ServerError) as e:
398
+ log_error(f"Error from Gemini API: {e}")
399
+ raise ModelProviderError(
400
+ message=str(e.response) if hasattr(e, "response") else str(e),
401
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
402
+ model_name=self.name,
403
+ model_id=self.id,
404
+ ) from e
405
+ except Exception as e:
406
+ log_error(f"Unknown error from Gemini API: {e}")
407
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
408
+
409
+ async def ainvoke_stream(
410
+ self,
411
+ messages: List[Message],
412
+ assistant_message: Message,
413
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
414
+ tools: Optional[List[Dict[str, Any]]] = None,
415
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
416
+ run_response: Optional[RunOutput] = None,
417
+ ) -> AsyncIterator[ModelResponse]:
418
+ """
419
+ Invokes the model with a list of messages and returns the response as a stream.
420
+ """
421
+ formatted_messages, system_message = self._format_messages(messages)
422
+
423
+ request_kwargs = self.get_request_params(
424
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
425
+ )
426
+
427
+ try:
428
+ if run_response and run_response.metrics:
429
+ run_response.metrics.set_time_to_first_token()
430
+
431
+ assistant_message.metrics.start_timer()
432
+
433
+ async_stream = await self.get_client().aio.models.generate_content_stream(
434
+ model=self.id,
435
+ contents=formatted_messages,
436
+ **request_kwargs,
437
+ )
438
+ async for chunk in async_stream:
439
+ yield self._parse_provider_response_delta(chunk)
440
+
441
+ assistant_message.metrics.stop_timer()
442
+
443
+ except (ClientError, ServerError) as e:
444
+ log_error(f"Error from Gemini API: {e}")
445
+ raise ModelProviderError(
446
+ message=str(e.response) if hasattr(e, "response") else str(e),
447
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
448
+ model_name=self.name,
449
+ model_id=self.id,
450
+ ) from e
451
+ except Exception as e:
452
+ log_error(f"Unknown error from Gemini API: {e}")
453
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
454
+
455
+ def _format_messages(self, messages: List[Message]):
456
+ """
457
+ Converts a list of Message objects to the Gemini-compatible format.
458
+
459
+ Args:
460
+ messages (List[Message]): The list of messages to convert.
461
+ """
462
+ formatted_messages: List = []
463
+ file_content: Optional[Union[GeminiFile, Part]] = None
464
+ system_message = None
465
+ for message in messages:
466
+ role = message.role
467
+ if role in ["system", "developer"]:
468
+ system_message = message.content
469
+ continue
470
+
471
+ # Set the role for the message according to Gemini's requirements
472
+ role = self.reverse_role_map.get(role, role)
473
+
474
+ # Add content to the message for the model
475
+ content = message.content
476
+ # Initialize message_parts to be used for Gemini
477
+ message_parts: List[Any] = []
478
+
479
+ # Function calls
480
+ if role == "model" and message.tool_calls is not None and len(message.tool_calls) > 0:
481
+ if content is not None:
482
+ content_str = content if isinstance(content, str) else str(content)
483
+ message_parts.append(Part.from_text(text=content_str))
484
+ for tool_call in message.tool_calls:
485
+ message_parts.append(
486
+ Part.from_function_call(
487
+ name=tool_call["function"]["name"],
488
+ args=json.loads(tool_call["function"]["arguments"]),
489
+ )
490
+ )
491
+ # Function call results
492
+ elif message.tool_calls is not None and len(message.tool_calls) > 0:
493
+ for tool_call in message.tool_calls:
494
+ message_parts.append(
495
+ Part.from_function_response(
496
+ name=tool_call["tool_name"], response={"result": tool_call["content"]}
497
+ )
498
+ )
499
+ # Regular text content
500
+ else:
501
+ if isinstance(content, str):
502
+ message_parts = [Part.from_text(text=content)]
503
+
504
+ if role == "user" and message.tool_calls is None:
505
+ # Add images to the message for the model
506
+ if message.images is not None:
507
+ for image in message.images:
508
+ if image.content is not None and isinstance(image.content, GeminiFile):
509
+ # Google recommends that if using a single image, place the text prompt after the image.
510
+ message_parts.insert(0, image.content)
511
+ else:
512
+ image_content = format_image_for_message(image)
513
+ if image_content:
514
+ message_parts.append(Part.from_bytes(**image_content))
515
+
516
+ # Add videos to the message for the model
517
+ if message.videos is not None:
518
+ try:
519
+ for video in message.videos:
520
+ # Case 1: Video is a file_types.File object (Recommended)
521
+ # Add it as a File object
522
+ if video.content is not None and isinstance(video.content, GeminiFile):
523
+ # Google recommends that if using a single video, place the text prompt after the video.
524
+ if video.content.uri and video.content.mime_type:
525
+ message_parts.insert(
526
+ 0, Part.from_uri(file_uri=video.content.uri, mime_type=video.content.mime_type)
527
+ )
528
+ else:
529
+ video_file = self._format_video_for_message(video)
530
+ if video_file is not None:
531
+ message_parts.insert(0, video_file)
532
+ except Exception as e:
533
+ log_warning(f"Failed to load video from {message.videos}: {e}")
534
+ continue
535
+
536
+ # Add audio to the message for the model
537
+ if message.audio is not None:
538
+ try:
539
+ for audio_snippet in message.audio:
540
+ if audio_snippet.content is not None and isinstance(audio_snippet.content, GeminiFile):
541
+ # Google recommends that if using a single audio file, place the text prompt after the audio file.
542
+ if audio_snippet.content.uri and audio_snippet.content.mime_type:
543
+ message_parts.insert(
544
+ 0,
545
+ Part.from_uri(
546
+ file_uri=audio_snippet.content.uri,
547
+ mime_type=audio_snippet.content.mime_type,
548
+ ),
549
+ )
550
+ else:
551
+ audio_content = self._format_audio_for_message(audio_snippet)
552
+ if audio_content:
553
+ message_parts.append(audio_content)
554
+ except Exception as e:
555
+ log_warning(f"Failed to load audio from {message.audio}: {e}")
556
+ continue
557
+
558
+ # Add files to the message for the model
559
+ if message.files is not None:
560
+ for file in message.files:
561
+ file_content = self._format_file_for_message(file)
562
+ if isinstance(file_content, Part):
563
+ formatted_messages.append(file_content)
564
+
565
+ final_message = Content(role=role, parts=message_parts)
566
+ formatted_messages.append(final_message)
567
+
568
+ if isinstance(file_content, GeminiFile):
569
+ formatted_messages.insert(0, file_content)
570
+
571
+ return formatted_messages, system_message
572
+
573
+ def _format_audio_for_message(self, audio: Audio) -> Optional[Union[Part, GeminiFile]]:
574
+ # Case 1: Audio is a bytes object
575
+ if audio.content and isinstance(audio.content, bytes):
576
+ mime_type = f"audio/{audio.format}" if audio.format else "audio/mp3"
577
+ return Part.from_bytes(mime_type=mime_type, data=audio.content)
578
+
579
+ # Case 2: Audio is an url
580
+ elif audio.url is not None:
581
+ audio_bytes = audio.get_content_bytes() # type: ignore
582
+ if audio_bytes is not None:
583
+ mime_type = f"audio/{audio.format}" if audio.format else "audio/mp3"
584
+ return Part.from_bytes(mime_type=mime_type, data=audio_bytes)
585
+ else:
586
+ log_warning(f"Failed to download audio from {audio}")
587
+ return None
588
+
589
+ # Case 3: Audio is a local file path
590
+ elif audio.filepath is not None:
591
+ audio_path = audio.filepath if isinstance(audio.filepath, Path) else Path(audio.filepath)
592
+
593
+ remote_file_name = f"files/{audio_path.stem.lower().replace('_', '')}"
594
+ # Check if video is already uploaded
595
+ existing_audio_upload = None
596
+ try:
597
+ if remote_file_name:
598
+ existing_audio_upload = self.get_client().files.get(name=remote_file_name)
599
+ except Exception as e:
600
+ log_warning(f"Error getting file {remote_file_name}: {e}")
601
+
602
+ if existing_audio_upload and existing_audio_upload.state and existing_audio_upload.state.name == "SUCCESS":
603
+ audio_file = existing_audio_upload
604
+ else:
605
+ # Upload the video file to the Gemini API
606
+ if audio_path.exists() and audio_path.is_file():
607
+ audio_file = self.get_client().files.upload(
608
+ file=audio_path,
609
+ config=dict(
610
+ name=remote_file_name,
611
+ display_name=audio_path.stem,
612
+ mime_type=f"audio/{audio.format}" if audio.format else "audio/mp3",
613
+ ),
614
+ )
615
+ else:
616
+ log_error(f"Audio file {audio_path} does not exist.")
617
+ return None
618
+
619
+ # Check whether the file is ready to be used.
620
+ while audio_file.state and audio_file.state.name == "PROCESSING":
621
+ if audio_file.name:
622
+ audio_file = self.get_client().files.get(name=audio_file.name)
623
+ time.sleep(2)
624
+
625
+ if audio_file.state and audio_file.state.name == "FAILED":
626
+ log_error(f"Audio file processing failed: {audio_file.state.name}")
627
+ return None
628
+
629
+ if audio_file.uri:
630
+ mime_type = f"audio/{audio.format}" if audio.format else "audio/mp3"
631
+ return Part.from_uri(file_uri=audio_file.uri, mime_type=mime_type)
632
+ return None
633
+ else:
634
+ log_warning(f"Unknown audio type: {type(audio.content)}")
635
+ return None
636
+
637
+ def _format_video_for_message(self, video: Video) -> Optional[Part]:
638
+ # Case 1: Video is a bytes object
639
+ if video.content and isinstance(video.content, bytes):
640
+ mime_type = f"video/{video.format}" if video.format else "video/mp4"
641
+ return Part.from_bytes(mime_type=mime_type, data=video.content)
642
+ # Case 2: Video is stored locally
643
+ elif video.filepath is not None:
644
+ video_path = video.filepath if isinstance(video.filepath, Path) else Path(video.filepath)
645
+
646
+ remote_file_name = f"files/{video_path.stem.lower().replace('_', '')}"
647
+ # Check if video is already uploaded
648
+ existing_video_upload = None
649
+ try:
650
+ if remote_file_name:
651
+ existing_video_upload = self.get_client().files.get(name=remote_file_name)
652
+ except Exception as e:
653
+ log_warning(f"Error getting file {remote_file_name}: {e}")
654
+
655
+ if existing_video_upload and existing_video_upload.state and existing_video_upload.state.name == "SUCCESS":
656
+ video_file = existing_video_upload
657
+ else:
658
+ # Upload the video file to the Gemini API
659
+ if video_path.exists() and video_path.is_file():
660
+ video_file = self.get_client().files.upload(
661
+ file=video_path,
662
+ config=dict(
663
+ name=remote_file_name,
664
+ display_name=video_path.stem,
665
+ mime_type=f"video/{video.format}" if video.format else "video/mp4",
666
+ ),
667
+ )
668
+ else:
669
+ log_error(f"Video file {video_path} does not exist.")
670
+ return None
671
+
672
+ # Check whether the file is ready to be used.
673
+ while video_file.state and video_file.state.name == "PROCESSING":
674
+ if video_file.name:
675
+ video_file = self.get_client().files.get(name=video_file.name)
676
+ time.sleep(2)
677
+
678
+ if video_file.state and video_file.state.name == "FAILED":
679
+ log_error(f"Video file processing failed: {video_file.state.name}")
680
+ return None
681
+
682
+ if video_file.uri:
683
+ mime_type = f"video/{video.format}" if video.format else "video/mp4"
684
+ return Part.from_uri(file_uri=video_file.uri, mime_type=mime_type)
685
+ return None
686
+ # Case 3: Video is a URL
687
+ elif video.url is not None:
688
+ mime_type = f"video/{video.format}" if video.format else "video/webm"
689
+ return Part.from_uri(
690
+ file_uri=video.url,
691
+ mime_type=mime_type,
692
+ )
693
+ else:
694
+ log_warning(f"Unknown video type: {type(video.content)}")
695
+ return None
696
+
697
+ def _format_file_for_message(self, file: File) -> Optional[Part]:
698
+ # Case 1: File is a bytes object
699
+ if file.content and isinstance(file.content, bytes) and file.mime_type:
700
+ return Part.from_bytes(mime_type=file.mime_type, data=file.content)
701
+
702
+ # Case 2: File is a URL
703
+ elif file.url is not None:
704
+ url_content = file.file_url_content
705
+ if url_content is not None:
706
+ content, mime_type = url_content
707
+ if mime_type and content:
708
+ return Part.from_bytes(mime_type=mime_type, data=content)
709
+ log_warning(f"Failed to download file from {file.url}")
710
+ return None
711
+
712
+ # Case 3: File is a local file path
713
+ elif file.filepath is not None:
714
+ file_path = file.filepath if isinstance(file.filepath, Path) else Path(file.filepath)
715
+ if file_path.exists() and file_path.is_file():
716
+ if file_path.stat().st_size < 20 * 1024 * 1024: # 20MB in bytes
717
+ if file.mime_type:
718
+ file_content = file_path.read_bytes()
719
+ if file_content:
720
+ return Part.from_bytes(mime_type=file.mime_type, data=file_content)
721
+ else:
722
+ import mimetypes
723
+
724
+ mime_type_guess = mimetypes.guess_type(file_path)[0]
725
+ if mime_type_guess is not None:
726
+ file_content = file_path.read_bytes()
727
+ if file_content:
728
+ mime_type_str: str = str(mime_type_guess)
729
+ return Part.from_bytes(mime_type=mime_type_str, data=file_content)
730
+ return None
731
+ else:
732
+ clean_file_name = f"files/{file_path.stem.lower().replace('_', '')}"
733
+ remote_file = None
734
+ try:
735
+ if clean_file_name:
736
+ remote_file = self.get_client().files.get(name=clean_file_name)
737
+ except Exception as e:
738
+ log_warning(f"Error getting file {clean_file_name}: {e}")
739
+
740
+ if (
741
+ remote_file
742
+ and remote_file.state
743
+ and remote_file.state.name == "SUCCESS"
744
+ and remote_file.uri
745
+ and remote_file.mime_type
746
+ ):
747
+ file_uri: str = remote_file.uri
748
+ file_mime_type: str = remote_file.mime_type
749
+ return Part.from_uri(file_uri=file_uri, mime_type=file_mime_type)
750
+ else:
751
+ log_error(f"File {file_path} does not exist.")
752
+ return None
753
+
754
+ # Case 4: File is a Gemini File object
755
+ elif isinstance(file.external, GeminiFile):
756
+ if file.external.uri and file.external.mime_type:
757
+ return Part.from_uri(file_uri=file.external.uri, mime_type=file.external.mime_type)
758
+ return None
759
+ return None
760
+
761
+ def format_function_call_results(
762
+ self, messages: List[Message], function_call_results: List[Message], **kwargs
763
+ ) -> None:
764
+ """
765
+ Format function call results.
766
+ """
767
+ combined_content: List = []
768
+ combined_function_result: List = []
769
+ message_metrics = Metrics()
770
+ if len(function_call_results) > 0:
771
+ for result in function_call_results:
772
+ combined_content.append(result.content)
773
+ combined_function_result.append({"tool_name": result.tool_name, "content": result.content})
774
+ message_metrics += result.metrics
775
+
776
+ if combined_content:
777
+ messages.append(
778
+ Message(
779
+ role="tool", content=combined_content, tool_calls=combined_function_result, metrics=message_metrics
780
+ )
781
+ )
782
+
783
+ def _parse_provider_response(self, response: GenerateContentResponse, **kwargs) -> ModelResponse:
784
+ """
785
+ Parse the OpenAI response into a ModelResponse.
786
+
787
+ Args:
788
+ response: Raw response from OpenAI
789
+
790
+ Returns:
791
+ ModelResponse: Parsed response data
792
+ """
793
+ model_response = ModelResponse()
794
+
795
+ # Get response message
796
+ response_message = Content(role="model", parts=[])
797
+ if response.candidates and response.candidates[0].content:
798
+ response_message = response.candidates[0].content
799
+
800
+ # Add role
801
+ if response_message.role is not None:
802
+ model_response.role = self.role_map[response_message.role]
803
+
804
+ # Add content
805
+ if response_message.parts is not None and len(response_message.parts) > 0:
806
+ for part in response_message.parts:
807
+ # Extract text if present
808
+ if hasattr(part, "text") and part.text is not None:
809
+ text_content: Optional[str] = getattr(part, "text")
810
+ if isinstance(text_content, str):
811
+ # Check if this is a thought summary
812
+ if hasattr(part, "thought") and part.thought:
813
+ # Add all parts as single message
814
+ if model_response.reasoning_content is None:
815
+ model_response.reasoning_content = text_content
816
+ else:
817
+ model_response.reasoning_content += text_content
818
+ else:
819
+ if model_response.content is None:
820
+ model_response.content = text_content
821
+ else:
822
+ model_response.content += text_content
823
+ else:
824
+ content_str = str(text_content) if text_content is not None else ""
825
+ if hasattr(part, "thought") and part.thought:
826
+ # Add all parts as single message
827
+ if model_response.reasoning_content is None:
828
+ model_response.reasoning_content = content_str
829
+ else:
830
+ model_response.reasoning_content += content_str
831
+ else:
832
+ if model_response.content is None:
833
+ model_response.content = content_str
834
+ else:
835
+ model_response.content += content_str
836
+
837
+ if hasattr(part, "inline_data") and part.inline_data is not None:
838
+ # Handle audio responses (for TTS models)
839
+ if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
840
+ # Store raw bytes data
841
+ model_response.audio = Audio(
842
+ id=str(uuid4()),
843
+ content=part.inline_data.data,
844
+ mime_type=part.inline_data.mime_type,
845
+ )
846
+ # Image responses
847
+ else:
848
+ if model_response.images is None:
849
+ model_response.images = []
850
+ model_response.images.append(
851
+ Image(id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type)
852
+ )
853
+
854
+ # Extract function call if present
855
+ if hasattr(part, "function_call") and part.function_call is not None:
856
+ call_id = part.function_call.id if part.function_call.id else str(uuid4())
857
+ tool_call = {
858
+ "id": call_id,
859
+ "type": "function",
860
+ "function": {
861
+ "name": part.function_call.name,
862
+ "arguments": json.dumps(part.function_call.args)
863
+ if part.function_call.args is not None
864
+ else "",
865
+ },
866
+ }
867
+
868
+ model_response.tool_calls.append(tool_call)
869
+
870
+ citations = Citations()
871
+ citations_raw = {}
872
+ citations_urls = []
873
+
874
+ if response.candidates and response.candidates[0].grounding_metadata is not None:
875
+ grounding_metadata = response.candidates[0].grounding_metadata.model_dump()
876
+ citations_raw["grounding_metadata"] = grounding_metadata
877
+
878
+ chunks = grounding_metadata.get("grounding_chunks", []) or []
879
+ citation_pairs = []
880
+ for chunk in chunks:
881
+ if not isinstance(chunk, dict):
882
+ continue
883
+ web = chunk.get("web")
884
+ if not isinstance(web, dict):
885
+ continue
886
+ uri = web.get("uri")
887
+ title = web.get("title")
888
+ if uri:
889
+ citation_pairs.append((uri, title))
890
+
891
+ # Create citation objects from filtered pairs
892
+ grounding_urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
893
+ citations_urls.extend(grounding_urls)
894
+
895
+ # Handle URLs from URL context tool
896
+ if (
897
+ response.candidates
898
+ and hasattr(response.candidates[0], "url_context_metadata")
899
+ and response.candidates[0].url_context_metadata is not None
900
+ ):
901
+ url_context_metadata = response.candidates[0].url_context_metadata.model_dump()
902
+ citations_raw["url_context_metadata"] = url_context_metadata
903
+
904
+ url_metadata_list = url_context_metadata.get("url_metadata", [])
905
+ for url_meta in url_metadata_list:
906
+ retrieved_url = url_meta.get("retrieved_url")
907
+ status = url_meta.get("url_retrieval_status", "UNKNOWN")
908
+ if retrieved_url and status == "URL_RETRIEVAL_STATUS_SUCCESS":
909
+ # Avoid duplicate URLs
910
+ existing_urls = [citation.url for citation in citations_urls]
911
+ if retrieved_url not in existing_urls:
912
+ citations_urls.append(UrlCitation(url=retrieved_url, title=retrieved_url))
913
+
914
+ if citations_raw or citations_urls:
915
+ citations.raw = citations_raw if citations_raw else None
916
+ citations.urls = citations_urls if citations_urls else None
917
+ model_response.citations = citations
918
+
919
+ # Extract usage metadata if present
920
+ if hasattr(response, "usage_metadata") and response.usage_metadata is not None:
921
+ model_response.response_usage = self._get_metrics(response.usage_metadata)
922
+
923
+ # If we have no content but have a role, add a default empty content
924
+ if model_response.role and model_response.content is None and not model_response.tool_calls:
925
+ model_response.content = ""
926
+
927
+ return model_response
928
+
929
+ def _parse_provider_response_delta(self, response_delta: GenerateContentResponse) -> ModelResponse:
930
+ model_response = ModelResponse()
931
+
932
+ if response_delta.candidates and len(response_delta.candidates) > 0:
933
+ candidate_content = response_delta.candidates[0].content
934
+ response_message: Content = Content(role="model", parts=[])
935
+ if candidate_content is not None:
936
+ response_message = candidate_content
937
+
938
+ # Add role
939
+ if response_message.role is not None:
940
+ model_response.role = self.role_map[response_message.role]
941
+
942
+ if response_message.parts is not None:
943
+ for part in response_message.parts:
944
+ # Extract text if present
945
+ if hasattr(part, "text") and part.text is not None:
946
+ text_content = str(part.text) if part.text is not None else ""
947
+ # Check if this is a thought summary
948
+ if hasattr(part, "thought") and part.thought:
949
+ if model_response.reasoning_content is None:
950
+ model_response.reasoning_content = text_content
951
+ else:
952
+ model_response.reasoning_content += text_content
953
+ else:
954
+ if model_response.content is None:
955
+ model_response.content = text_content
956
+ else:
957
+ model_response.content += text_content
958
+
959
+ if hasattr(part, "inline_data") and part.inline_data is not None:
960
+ # Audio responses
961
+ if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
962
+ # Store raw bytes audio data
963
+ model_response.audio = Audio(
964
+ id=str(uuid4()),
965
+ content=part.inline_data.data,
966
+ mime_type=part.inline_data.mime_type,
967
+ )
968
+ # Image responses
969
+ else:
970
+ if model_response.images is None:
971
+ model_response.images = []
972
+ model_response.images.append(
973
+ Image(
974
+ id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type
975
+ )
976
+ )
977
+
978
+ # Extract function call if present
979
+ if hasattr(part, "function_call") and part.function_call is not None:
980
+ call_id = part.function_call.id if part.function_call.id else str(uuid4())
981
+ tool_call = {
982
+ "id": call_id,
983
+ "type": "function",
984
+ "function": {
985
+ "name": part.function_call.name,
986
+ "arguments": json.dumps(part.function_call.args)
987
+ if part.function_call.args is not None
988
+ else "",
989
+ },
990
+ }
991
+
992
+ model_response.tool_calls.append(tool_call)
993
+
994
+ if response_delta.candidates[0].grounding_metadata is not None:
995
+ citations = Citations()
996
+ grounding_metadata = response_delta.candidates[0].grounding_metadata.model_dump()
997
+ citations.raw = grounding_metadata
998
+
999
+ # Extract url and title
1000
+ chunks = grounding_metadata.pop("grounding_chunks", None) or []
1001
+ citation_pairs = []
1002
+ for chunk in chunks:
1003
+ if not isinstance(chunk, dict):
1004
+ continue
1005
+ web = chunk.get("web")
1006
+ if not isinstance(web, dict):
1007
+ continue
1008
+ uri = web.get("uri")
1009
+ title = web.get("title")
1010
+ if uri:
1011
+ citation_pairs.append((uri, title))
1012
+
1013
+ # Create citation objects from filtered pairs
1014
+ citations.urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
1015
+
1016
+ model_response.citations = citations
1017
+
1018
+ # Extract usage metadata if present
1019
+ if hasattr(response_delta, "usage_metadata") and response_delta.usage_metadata is not None:
1020
+ model_response.response_usage = self._get_metrics(response_delta.usage_metadata)
1021
+
1022
+ return model_response
1023
+
1024
+ def __deepcopy__(self, memo):
1025
+ """
1026
+ Creates a deep copy of the Gemini model instance but sets the client to None.
1027
+
1028
+ This is useful when we need to copy the model configuration without duplicating
1029
+ the client connection.
1030
+
1031
+ This overrides the base class implementation.
1032
+ """
1033
+ from copy import copy, deepcopy
1034
+
1035
+ # Create a new instance without calling __init__
1036
+ cls = self.__class__
1037
+ new_instance = cls.__new__(cls)
1038
+
1039
+ # Update memo with the new instance to avoid circular references
1040
+ memo[id(self)] = new_instance
1041
+
1042
+ # Deep copy all attributes except client and unpickleable attributes
1043
+ for key, value in self.__dict__.items():
1044
+ # Skip client and other unpickleable attributes
1045
+ if key in {"client", "response_format", "_tools", "_functions", "_function_call_stack"}:
1046
+ continue
1047
+
1048
+ # Try deep copy first, fall back to shallow copy, then direct assignment
1049
+ try:
1050
+ setattr(new_instance, key, deepcopy(value, memo))
1051
+ except Exception:
1052
+ try:
1053
+ setattr(new_instance, key, copy(value))
1054
+ except Exception:
1055
+ setattr(new_instance, key, value)
1056
+
1057
+ # Explicitly set client to None
1058
+ setattr(new_instance, "client", None)
1059
+
1060
+ return new_instance
1061
+
1062
+ def _get_metrics(self, response_usage: GenerateContentResponseUsageMetadata) -> Metrics:
1063
+ """
1064
+ Parse the given Google Gemini usage into an Agno Metrics object.
1065
+
1066
+ Args:
1067
+ response_usage: Usage data from Google Gemini
1068
+
1069
+ Returns:
1070
+ Metrics: Parsed metrics data
1071
+ """
1072
+ metrics = Metrics()
1073
+
1074
+ metrics.input_tokens = response_usage.prompt_token_count or 0
1075
+ metrics.output_tokens = response_usage.candidates_token_count or 0
1076
+ if response_usage.thoughts_token_count is not None:
1077
+ metrics.output_tokens += response_usage.thoughts_token_count or 0
1078
+ metrics.total_tokens = metrics.input_tokens + metrics.output_tokens
1079
+
1080
+ metrics.cache_read_tokens = response_usage.cached_content_token_count or 0
1081
+
1082
+ if response_usage.traffic_type is not None:
1083
+ metrics.provider_metrics = {"traffic_type": response_usage.traffic_type}
1084
+
1085
+ return metrics