agno 0.1.2__py3-none-any.whl → 2.3.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 (723) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +44 -5
  3. agno/agent/agent.py +10531 -2975
  4. agno/api/agent.py +14 -53
  5. agno/api/api.py +7 -46
  6. agno/api/evals.py +22 -0
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -25
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +6 -9
  11. agno/api/schemas/evals.py +16 -0
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +10 -10
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +16 -0
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +22 -26
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/compression/__init__.py +3 -0
  25. agno/compression/manager.py +247 -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 +946 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2781 -0
  33. agno/db/dynamo/schemas.py +442 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +2379 -0
  37. agno/db/firestore/schemas.py +181 -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 +1791 -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 +1312 -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 +1777 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/manager.py +199 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/migrations/versions/v2_3_0.py +938 -0
  51. agno/db/mongo/__init__.py +17 -0
  52. agno/db/mongo/async_mongo.py +2760 -0
  53. agno/db/mongo/mongo.py +2597 -0
  54. agno/db/mongo/schemas.py +119 -0
  55. agno/db/mongo/utils.py +276 -0
  56. agno/db/mysql/__init__.py +4 -0
  57. agno/db/mysql/async_mysql.py +2912 -0
  58. agno/db/mysql/mysql.py +2923 -0
  59. agno/db/mysql/schemas.py +186 -0
  60. agno/db/mysql/utils.py +488 -0
  61. agno/db/postgres/__init__.py +4 -0
  62. agno/db/postgres/async_postgres.py +2579 -0
  63. agno/db/postgres/postgres.py +2870 -0
  64. agno/db/postgres/schemas.py +187 -0
  65. agno/db/postgres/utils.py +442 -0
  66. agno/db/redis/__init__.py +3 -0
  67. agno/db/redis/redis.py +2141 -0
  68. agno/db/redis/schemas.py +159 -0
  69. agno/db/redis/utils.py +346 -0
  70. agno/db/schemas/__init__.py +4 -0
  71. agno/db/schemas/culture.py +120 -0
  72. agno/db/schemas/evals.py +34 -0
  73. agno/db/schemas/knowledge.py +40 -0
  74. agno/db/schemas/memory.py +61 -0
  75. agno/db/singlestore/__init__.py +3 -0
  76. agno/db/singlestore/schemas.py +179 -0
  77. agno/db/singlestore/singlestore.py +2877 -0
  78. agno/db/singlestore/utils.py +384 -0
  79. agno/db/sqlite/__init__.py +4 -0
  80. agno/db/sqlite/async_sqlite.py +2911 -0
  81. agno/db/sqlite/schemas.py +181 -0
  82. agno/db/sqlite/sqlite.py +2908 -0
  83. agno/db/sqlite/utils.py +429 -0
  84. agno/db/surrealdb/__init__.py +3 -0
  85. agno/db/surrealdb/metrics.py +292 -0
  86. agno/db/surrealdb/models.py +334 -0
  87. agno/db/surrealdb/queries.py +71 -0
  88. agno/db/surrealdb/surrealdb.py +1908 -0
  89. agno/db/surrealdb/utils.py +147 -0
  90. agno/db/utils.py +118 -0
  91. agno/eval/__init__.py +24 -0
  92. agno/eval/accuracy.py +666 -276
  93. agno/eval/agent_as_judge.py +861 -0
  94. agno/eval/base.py +29 -0
  95. agno/eval/performance.py +779 -0
  96. agno/eval/reliability.py +241 -62
  97. agno/eval/utils.py +120 -0
  98. agno/exceptions.py +143 -1
  99. agno/filters.py +354 -0
  100. agno/guardrails/__init__.py +6 -0
  101. agno/guardrails/base.py +19 -0
  102. agno/guardrails/openai.py +144 -0
  103. agno/guardrails/pii.py +94 -0
  104. agno/guardrails/prompt_injection.py +52 -0
  105. agno/hooks/__init__.py +3 -0
  106. agno/hooks/decorator.py +164 -0
  107. agno/integrations/discord/__init__.py +3 -0
  108. agno/integrations/discord/client.py +203 -0
  109. agno/knowledge/__init__.py +5 -1
  110. agno/{document → knowledge}/chunking/agentic.py +22 -14
  111. agno/{document → knowledge}/chunking/document.py +2 -2
  112. agno/{document → knowledge}/chunking/fixed.py +7 -6
  113. agno/knowledge/chunking/markdown.py +151 -0
  114. agno/{document → knowledge}/chunking/recursive.py +15 -3
  115. agno/knowledge/chunking/row.py +39 -0
  116. agno/knowledge/chunking/semantic.py +91 -0
  117. agno/knowledge/chunking/strategy.py +165 -0
  118. agno/knowledge/content.py +74 -0
  119. agno/knowledge/document/__init__.py +5 -0
  120. agno/{document → knowledge/document}/base.py +12 -2
  121. agno/knowledge/embedder/__init__.py +5 -0
  122. agno/knowledge/embedder/aws_bedrock.py +343 -0
  123. agno/knowledge/embedder/azure_openai.py +210 -0
  124. agno/{embedder → knowledge/embedder}/base.py +8 -0
  125. agno/knowledge/embedder/cohere.py +323 -0
  126. agno/knowledge/embedder/fastembed.py +62 -0
  127. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  128. agno/knowledge/embedder/google.py +258 -0
  129. agno/knowledge/embedder/huggingface.py +94 -0
  130. agno/knowledge/embedder/jina.py +182 -0
  131. agno/knowledge/embedder/langdb.py +22 -0
  132. agno/knowledge/embedder/mistral.py +206 -0
  133. agno/knowledge/embedder/nebius.py +13 -0
  134. agno/knowledge/embedder/ollama.py +154 -0
  135. agno/knowledge/embedder/openai.py +195 -0
  136. agno/knowledge/embedder/sentence_transformer.py +63 -0
  137. agno/{embedder → knowledge/embedder}/together.py +1 -1
  138. agno/knowledge/embedder/vllm.py +262 -0
  139. agno/knowledge/embedder/voyageai.py +165 -0
  140. agno/knowledge/knowledge.py +3006 -0
  141. agno/knowledge/reader/__init__.py +7 -0
  142. agno/knowledge/reader/arxiv_reader.py +81 -0
  143. agno/knowledge/reader/base.py +95 -0
  144. agno/knowledge/reader/csv_reader.py +164 -0
  145. agno/knowledge/reader/docx_reader.py +82 -0
  146. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  147. agno/knowledge/reader/firecrawl_reader.py +201 -0
  148. agno/knowledge/reader/json_reader.py +88 -0
  149. agno/knowledge/reader/markdown_reader.py +137 -0
  150. agno/knowledge/reader/pdf_reader.py +431 -0
  151. agno/knowledge/reader/pptx_reader.py +101 -0
  152. agno/knowledge/reader/reader_factory.py +313 -0
  153. agno/knowledge/reader/s3_reader.py +89 -0
  154. agno/knowledge/reader/tavily_reader.py +193 -0
  155. agno/knowledge/reader/text_reader.py +127 -0
  156. agno/knowledge/reader/web_search_reader.py +325 -0
  157. agno/knowledge/reader/website_reader.py +455 -0
  158. agno/knowledge/reader/wikipedia_reader.py +91 -0
  159. agno/knowledge/reader/youtube_reader.py +78 -0
  160. agno/knowledge/remote_content/remote_content.py +88 -0
  161. agno/knowledge/reranker/__init__.py +3 -0
  162. agno/{reranker → knowledge/reranker}/base.py +1 -1
  163. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  164. agno/knowledge/reranker/infinity.py +195 -0
  165. agno/knowledge/reranker/sentence_transformer.py +54 -0
  166. agno/knowledge/types.py +39 -0
  167. agno/knowledge/utils.py +234 -0
  168. agno/media.py +439 -95
  169. agno/memory/__init__.py +16 -3
  170. agno/memory/manager.py +1474 -123
  171. agno/memory/strategies/__init__.py +15 -0
  172. agno/memory/strategies/base.py +66 -0
  173. agno/memory/strategies/summarize.py +196 -0
  174. agno/memory/strategies/types.py +37 -0
  175. agno/models/aimlapi/__init__.py +5 -0
  176. agno/models/aimlapi/aimlapi.py +62 -0
  177. agno/models/anthropic/__init__.py +4 -0
  178. agno/models/anthropic/claude.py +960 -496
  179. agno/models/aws/__init__.py +15 -0
  180. agno/models/aws/bedrock.py +686 -451
  181. agno/models/aws/claude.py +190 -183
  182. agno/models/azure/__init__.py +18 -1
  183. agno/models/azure/ai_foundry.py +489 -0
  184. agno/models/azure/openai_chat.py +89 -40
  185. agno/models/base.py +2477 -550
  186. agno/models/cerebras/__init__.py +12 -0
  187. agno/models/cerebras/cerebras.py +565 -0
  188. agno/models/cerebras/cerebras_openai.py +131 -0
  189. agno/models/cohere/__init__.py +4 -0
  190. agno/models/cohere/chat.py +306 -492
  191. agno/models/cometapi/__init__.py +5 -0
  192. agno/models/cometapi/cometapi.py +74 -0
  193. agno/models/dashscope/__init__.py +5 -0
  194. agno/models/dashscope/dashscope.py +90 -0
  195. agno/models/deepinfra/__init__.py +5 -0
  196. agno/models/deepinfra/deepinfra.py +45 -0
  197. agno/models/deepseek/__init__.py +4 -0
  198. agno/models/deepseek/deepseek.py +110 -9
  199. agno/models/fireworks/__init__.py +4 -0
  200. agno/models/fireworks/fireworks.py +19 -22
  201. agno/models/google/__init__.py +3 -7
  202. agno/models/google/gemini.py +1717 -662
  203. agno/models/google/utils.py +22 -0
  204. agno/models/groq/__init__.py +4 -0
  205. agno/models/groq/groq.py +391 -666
  206. agno/models/huggingface/__init__.py +4 -0
  207. agno/models/huggingface/huggingface.py +266 -538
  208. agno/models/ibm/__init__.py +5 -0
  209. agno/models/ibm/watsonx.py +432 -0
  210. agno/models/internlm/__init__.py +3 -0
  211. agno/models/internlm/internlm.py +20 -3
  212. agno/models/langdb/__init__.py +1 -0
  213. agno/models/langdb/langdb.py +60 -0
  214. agno/models/litellm/__init__.py +14 -0
  215. agno/models/litellm/chat.py +503 -0
  216. agno/models/litellm/litellm_openai.py +42 -0
  217. agno/models/llama_cpp/__init__.py +5 -0
  218. agno/models/llama_cpp/llama_cpp.py +22 -0
  219. agno/models/lmstudio/__init__.py +5 -0
  220. agno/models/lmstudio/lmstudio.py +25 -0
  221. agno/models/message.py +361 -39
  222. agno/models/meta/__init__.py +12 -0
  223. agno/models/meta/llama.py +502 -0
  224. agno/models/meta/llama_openai.py +79 -0
  225. agno/models/metrics.py +120 -0
  226. agno/models/mistral/__init__.py +4 -0
  227. agno/models/mistral/mistral.py +293 -393
  228. agno/models/nebius/__init__.py +3 -0
  229. agno/models/nebius/nebius.py +53 -0
  230. agno/models/nexus/__init__.py +3 -0
  231. agno/models/nexus/nexus.py +22 -0
  232. agno/models/nvidia/__init__.py +4 -0
  233. agno/models/nvidia/nvidia.py +22 -3
  234. agno/models/ollama/__init__.py +4 -2
  235. agno/models/ollama/chat.py +257 -492
  236. agno/models/openai/__init__.py +7 -0
  237. agno/models/openai/chat.py +725 -770
  238. agno/models/openai/like.py +16 -2
  239. agno/models/openai/responses.py +1121 -0
  240. agno/models/openrouter/__init__.py +4 -0
  241. agno/models/openrouter/openrouter.py +62 -5
  242. agno/models/perplexity/__init__.py +5 -0
  243. agno/models/perplexity/perplexity.py +203 -0
  244. agno/models/portkey/__init__.py +3 -0
  245. agno/models/portkey/portkey.py +82 -0
  246. agno/models/requesty/__init__.py +5 -0
  247. agno/models/requesty/requesty.py +69 -0
  248. agno/models/response.py +177 -7
  249. agno/models/sambanova/__init__.py +4 -0
  250. agno/models/sambanova/sambanova.py +23 -4
  251. agno/models/siliconflow/__init__.py +5 -0
  252. agno/models/siliconflow/siliconflow.py +42 -0
  253. agno/models/together/__init__.py +4 -0
  254. agno/models/together/together.py +21 -164
  255. agno/models/utils.py +266 -0
  256. agno/models/vercel/__init__.py +3 -0
  257. agno/models/vercel/v0.py +43 -0
  258. agno/models/vertexai/__init__.py +0 -1
  259. agno/models/vertexai/claude.py +190 -0
  260. agno/models/vllm/__init__.py +3 -0
  261. agno/models/vllm/vllm.py +83 -0
  262. agno/models/xai/__init__.py +2 -0
  263. agno/models/xai/xai.py +111 -7
  264. agno/os/__init__.py +3 -0
  265. agno/os/app.py +1027 -0
  266. agno/os/auth.py +244 -0
  267. agno/os/config.py +126 -0
  268. agno/os/interfaces/__init__.py +1 -0
  269. agno/os/interfaces/a2a/__init__.py +3 -0
  270. agno/os/interfaces/a2a/a2a.py +42 -0
  271. agno/os/interfaces/a2a/router.py +249 -0
  272. agno/os/interfaces/a2a/utils.py +924 -0
  273. agno/os/interfaces/agui/__init__.py +3 -0
  274. agno/os/interfaces/agui/agui.py +47 -0
  275. agno/os/interfaces/agui/router.py +147 -0
  276. agno/os/interfaces/agui/utils.py +574 -0
  277. agno/os/interfaces/base.py +25 -0
  278. agno/os/interfaces/slack/__init__.py +3 -0
  279. agno/os/interfaces/slack/router.py +148 -0
  280. agno/os/interfaces/slack/security.py +30 -0
  281. agno/os/interfaces/slack/slack.py +47 -0
  282. agno/os/interfaces/whatsapp/__init__.py +3 -0
  283. agno/os/interfaces/whatsapp/router.py +210 -0
  284. agno/os/interfaces/whatsapp/security.py +55 -0
  285. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  286. agno/os/mcp.py +293 -0
  287. agno/os/middleware/__init__.py +9 -0
  288. agno/os/middleware/jwt.py +797 -0
  289. agno/os/router.py +258 -0
  290. agno/os/routers/__init__.py +3 -0
  291. agno/os/routers/agents/__init__.py +3 -0
  292. agno/os/routers/agents/router.py +599 -0
  293. agno/os/routers/agents/schema.py +261 -0
  294. agno/os/routers/evals/__init__.py +3 -0
  295. agno/os/routers/evals/evals.py +450 -0
  296. agno/os/routers/evals/schemas.py +174 -0
  297. agno/os/routers/evals/utils.py +231 -0
  298. agno/os/routers/health.py +31 -0
  299. agno/os/routers/home.py +52 -0
  300. agno/os/routers/knowledge/__init__.py +3 -0
  301. agno/os/routers/knowledge/knowledge.py +1008 -0
  302. agno/os/routers/knowledge/schemas.py +178 -0
  303. agno/os/routers/memory/__init__.py +3 -0
  304. agno/os/routers/memory/memory.py +661 -0
  305. agno/os/routers/memory/schemas.py +88 -0
  306. agno/os/routers/metrics/__init__.py +3 -0
  307. agno/os/routers/metrics/metrics.py +190 -0
  308. agno/os/routers/metrics/schemas.py +47 -0
  309. agno/os/routers/session/__init__.py +3 -0
  310. agno/os/routers/session/session.py +997 -0
  311. agno/os/routers/teams/__init__.py +3 -0
  312. agno/os/routers/teams/router.py +512 -0
  313. agno/os/routers/teams/schema.py +257 -0
  314. agno/os/routers/traces/__init__.py +3 -0
  315. agno/os/routers/traces/schemas.py +414 -0
  316. agno/os/routers/traces/traces.py +499 -0
  317. agno/os/routers/workflows/__init__.py +3 -0
  318. agno/os/routers/workflows/router.py +624 -0
  319. agno/os/routers/workflows/schema.py +75 -0
  320. agno/os/schema.py +534 -0
  321. agno/os/scopes.py +469 -0
  322. agno/{playground → os}/settings.py +7 -15
  323. agno/os/utils.py +973 -0
  324. agno/reasoning/anthropic.py +80 -0
  325. agno/reasoning/azure_ai_foundry.py +67 -0
  326. agno/reasoning/deepseek.py +63 -0
  327. agno/reasoning/default.py +97 -0
  328. agno/reasoning/gemini.py +73 -0
  329. agno/reasoning/groq.py +71 -0
  330. agno/reasoning/helpers.py +24 -1
  331. agno/reasoning/ollama.py +67 -0
  332. agno/reasoning/openai.py +86 -0
  333. agno/reasoning/step.py +2 -1
  334. agno/reasoning/vertexai.py +76 -0
  335. agno/run/__init__.py +6 -0
  336. agno/run/agent.py +822 -0
  337. agno/run/base.py +247 -0
  338. agno/run/cancel.py +81 -0
  339. agno/run/requirement.py +181 -0
  340. agno/run/team.py +767 -0
  341. agno/run/workflow.py +708 -0
  342. agno/session/__init__.py +10 -0
  343. agno/session/agent.py +260 -0
  344. agno/session/summary.py +265 -0
  345. agno/session/team.py +342 -0
  346. agno/session/workflow.py +501 -0
  347. agno/table.py +10 -0
  348. agno/team/__init__.py +37 -0
  349. agno/team/team.py +9536 -0
  350. agno/tools/__init__.py +7 -0
  351. agno/tools/agentql.py +120 -0
  352. agno/tools/airflow.py +22 -12
  353. agno/tools/api.py +122 -0
  354. agno/tools/apify.py +276 -83
  355. agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
  356. agno/tools/aws_lambda.py +28 -7
  357. agno/tools/aws_ses.py +66 -0
  358. agno/tools/baidusearch.py +11 -4
  359. agno/tools/bitbucket.py +292 -0
  360. agno/tools/brandfetch.py +213 -0
  361. agno/tools/bravesearch.py +106 -0
  362. agno/tools/brightdata.py +367 -0
  363. agno/tools/browserbase.py +209 -0
  364. agno/tools/calcom.py +32 -23
  365. agno/tools/calculator.py +24 -37
  366. agno/tools/cartesia.py +187 -0
  367. agno/tools/{clickup_tool.py → clickup.py} +17 -28
  368. agno/tools/confluence.py +91 -26
  369. agno/tools/crawl4ai.py +139 -43
  370. agno/tools/csv_toolkit.py +28 -22
  371. agno/tools/dalle.py +36 -22
  372. agno/tools/daytona.py +475 -0
  373. agno/tools/decorator.py +169 -14
  374. agno/tools/desi_vocal.py +23 -11
  375. agno/tools/discord.py +32 -29
  376. agno/tools/docker.py +716 -0
  377. agno/tools/duckdb.py +76 -81
  378. agno/tools/duckduckgo.py +43 -40
  379. agno/tools/e2b.py +703 -0
  380. agno/tools/eleven_labs.py +65 -54
  381. agno/tools/email.py +13 -5
  382. agno/tools/evm.py +129 -0
  383. agno/tools/exa.py +324 -42
  384. agno/tools/fal.py +39 -35
  385. agno/tools/file.py +196 -30
  386. agno/tools/file_generation.py +356 -0
  387. agno/tools/financial_datasets.py +288 -0
  388. agno/tools/firecrawl.py +108 -33
  389. agno/tools/function.py +960 -122
  390. agno/tools/giphy.py +34 -12
  391. agno/tools/github.py +1294 -97
  392. agno/tools/gmail.py +922 -0
  393. agno/tools/google_bigquery.py +117 -0
  394. agno/tools/google_drive.py +271 -0
  395. agno/tools/google_maps.py +253 -0
  396. agno/tools/googlecalendar.py +607 -107
  397. agno/tools/googlesheets.py +377 -0
  398. agno/tools/hackernews.py +20 -12
  399. agno/tools/jina.py +24 -14
  400. agno/tools/jira.py +48 -19
  401. agno/tools/knowledge.py +218 -0
  402. agno/tools/linear.py +82 -43
  403. agno/tools/linkup.py +58 -0
  404. agno/tools/local_file_system.py +15 -7
  405. agno/tools/lumalab.py +41 -26
  406. agno/tools/mcp/__init__.py +10 -0
  407. agno/tools/mcp/mcp.py +331 -0
  408. agno/tools/mcp/multi_mcp.py +347 -0
  409. agno/tools/mcp/params.py +24 -0
  410. agno/tools/mcp_toolbox.py +284 -0
  411. agno/tools/mem0.py +193 -0
  412. agno/tools/memory.py +419 -0
  413. agno/tools/mlx_transcribe.py +11 -9
  414. agno/tools/models/azure_openai.py +190 -0
  415. agno/tools/models/gemini.py +203 -0
  416. agno/tools/models/groq.py +158 -0
  417. agno/tools/models/morph.py +186 -0
  418. agno/tools/models/nebius.py +124 -0
  419. agno/tools/models_labs.py +163 -82
  420. agno/tools/moviepy_video.py +18 -13
  421. agno/tools/nano_banana.py +151 -0
  422. agno/tools/neo4j.py +134 -0
  423. agno/tools/newspaper.py +15 -4
  424. agno/tools/newspaper4k.py +19 -6
  425. agno/tools/notion.py +204 -0
  426. agno/tools/openai.py +181 -17
  427. agno/tools/openbb.py +27 -20
  428. agno/tools/opencv.py +321 -0
  429. agno/tools/openweather.py +233 -0
  430. agno/tools/oxylabs.py +385 -0
  431. agno/tools/pandas.py +25 -15
  432. agno/tools/parallel.py +314 -0
  433. agno/tools/postgres.py +238 -185
  434. agno/tools/pubmed.py +125 -13
  435. agno/tools/python.py +48 -35
  436. agno/tools/reasoning.py +283 -0
  437. agno/tools/reddit.py +207 -29
  438. agno/tools/redshift.py +406 -0
  439. agno/tools/replicate.py +69 -26
  440. agno/tools/resend.py +11 -6
  441. agno/tools/scrapegraph.py +179 -19
  442. agno/tools/searxng.py +23 -31
  443. agno/tools/serpapi.py +15 -10
  444. agno/tools/serper.py +255 -0
  445. agno/tools/shell.py +23 -12
  446. agno/tools/shopify.py +1519 -0
  447. agno/tools/slack.py +56 -14
  448. agno/tools/sleep.py +8 -6
  449. agno/tools/spider.py +35 -11
  450. agno/tools/spotify.py +919 -0
  451. agno/tools/sql.py +34 -19
  452. agno/tools/tavily.py +158 -8
  453. agno/tools/telegram.py +18 -8
  454. agno/tools/todoist.py +218 -0
  455. agno/tools/toolkit.py +134 -9
  456. agno/tools/trafilatura.py +388 -0
  457. agno/tools/trello.py +25 -28
  458. agno/tools/twilio.py +18 -9
  459. agno/tools/user_control_flow.py +78 -0
  460. agno/tools/valyu.py +228 -0
  461. agno/tools/visualization.py +467 -0
  462. agno/tools/webbrowser.py +28 -0
  463. agno/tools/webex.py +76 -0
  464. agno/tools/website.py +23 -19
  465. agno/tools/webtools.py +45 -0
  466. agno/tools/whatsapp.py +286 -0
  467. agno/tools/wikipedia.py +28 -19
  468. agno/tools/workflow.py +285 -0
  469. agno/tools/{twitter.py → x.py} +142 -46
  470. agno/tools/yfinance.py +41 -39
  471. agno/tools/youtube.py +34 -17
  472. agno/tools/zendesk.py +15 -5
  473. agno/tools/zep.py +454 -0
  474. agno/tools/zoom.py +86 -37
  475. agno/tracing/__init__.py +12 -0
  476. agno/tracing/exporter.py +157 -0
  477. agno/tracing/schemas.py +276 -0
  478. agno/tracing/setup.py +111 -0
  479. agno/utils/agent.py +938 -0
  480. agno/utils/audio.py +37 -1
  481. agno/utils/certs.py +27 -0
  482. agno/utils/code_execution.py +11 -0
  483. agno/utils/common.py +103 -20
  484. agno/utils/cryptography.py +22 -0
  485. agno/utils/dttm.py +33 -0
  486. agno/utils/events.py +700 -0
  487. agno/utils/functions.py +107 -37
  488. agno/utils/gemini.py +426 -0
  489. agno/utils/hooks.py +171 -0
  490. agno/utils/http.py +185 -0
  491. agno/utils/json_schema.py +159 -37
  492. agno/utils/knowledge.py +36 -0
  493. agno/utils/location.py +19 -0
  494. agno/utils/log.py +221 -8
  495. agno/utils/mcp.py +214 -0
  496. agno/utils/media.py +335 -14
  497. agno/utils/merge_dict.py +22 -1
  498. agno/utils/message.py +77 -2
  499. agno/utils/models/ai_foundry.py +50 -0
  500. agno/utils/models/claude.py +373 -0
  501. agno/utils/models/cohere.py +94 -0
  502. agno/utils/models/llama.py +85 -0
  503. agno/utils/models/mistral.py +100 -0
  504. agno/utils/models/openai_responses.py +140 -0
  505. agno/utils/models/schema_utils.py +153 -0
  506. agno/utils/models/watsonx.py +41 -0
  507. agno/utils/openai.py +257 -0
  508. agno/utils/pickle.py +1 -1
  509. agno/utils/pprint.py +124 -8
  510. agno/utils/print_response/agent.py +930 -0
  511. agno/utils/print_response/team.py +1914 -0
  512. agno/utils/print_response/workflow.py +1668 -0
  513. agno/utils/prompts.py +111 -0
  514. agno/utils/reasoning.py +108 -0
  515. agno/utils/response.py +163 -0
  516. agno/utils/serialize.py +32 -0
  517. agno/utils/shell.py +4 -4
  518. agno/utils/streamlit.py +487 -0
  519. agno/utils/string.py +204 -51
  520. agno/utils/team.py +139 -0
  521. agno/utils/timer.py +9 -2
  522. agno/utils/tokens.py +657 -0
  523. agno/utils/tools.py +19 -1
  524. agno/utils/whatsapp.py +305 -0
  525. agno/utils/yaml_io.py +3 -3
  526. agno/vectordb/__init__.py +2 -0
  527. agno/vectordb/base.py +87 -9
  528. agno/vectordb/cassandra/__init__.py +5 -1
  529. agno/vectordb/cassandra/cassandra.py +383 -27
  530. agno/vectordb/chroma/__init__.py +4 -0
  531. agno/vectordb/chroma/chromadb.py +748 -83
  532. agno/vectordb/clickhouse/__init__.py +7 -1
  533. agno/vectordb/clickhouse/clickhousedb.py +554 -53
  534. agno/vectordb/couchbase/__init__.py +3 -0
  535. agno/vectordb/couchbase/couchbase.py +1446 -0
  536. agno/vectordb/lancedb/__init__.py +5 -0
  537. agno/vectordb/lancedb/lance_db.py +730 -98
  538. agno/vectordb/langchaindb/__init__.py +5 -0
  539. agno/vectordb/langchaindb/langchaindb.py +163 -0
  540. agno/vectordb/lightrag/__init__.py +5 -0
  541. agno/vectordb/lightrag/lightrag.py +388 -0
  542. agno/vectordb/llamaindex/__init__.py +3 -0
  543. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  544. agno/vectordb/milvus/__init__.py +3 -0
  545. agno/vectordb/milvus/milvus.py +966 -78
  546. agno/vectordb/mongodb/__init__.py +9 -1
  547. agno/vectordb/mongodb/mongodb.py +1175 -172
  548. agno/vectordb/pgvector/__init__.py +8 -0
  549. agno/vectordb/pgvector/pgvector.py +599 -115
  550. agno/vectordb/pineconedb/__init__.py +5 -1
  551. agno/vectordb/pineconedb/pineconedb.py +406 -43
  552. agno/vectordb/qdrant/__init__.py +4 -0
  553. agno/vectordb/qdrant/qdrant.py +914 -61
  554. agno/vectordb/redis/__init__.py +9 -0
  555. agno/vectordb/redis/redisdb.py +682 -0
  556. agno/vectordb/singlestore/__init__.py +8 -1
  557. agno/vectordb/singlestore/singlestore.py +771 -0
  558. agno/vectordb/surrealdb/__init__.py +3 -0
  559. agno/vectordb/surrealdb/surrealdb.py +663 -0
  560. agno/vectordb/upstashdb/__init__.py +5 -0
  561. agno/vectordb/upstashdb/upstashdb.py +718 -0
  562. agno/vectordb/weaviate/__init__.py +8 -0
  563. agno/vectordb/weaviate/index.py +15 -0
  564. agno/vectordb/weaviate/weaviate.py +1009 -0
  565. agno/workflow/__init__.py +23 -1
  566. agno/workflow/agent.py +299 -0
  567. agno/workflow/condition.py +759 -0
  568. agno/workflow/loop.py +756 -0
  569. agno/workflow/parallel.py +853 -0
  570. agno/workflow/router.py +723 -0
  571. agno/workflow/step.py +1564 -0
  572. agno/workflow/steps.py +613 -0
  573. agno/workflow/types.py +556 -0
  574. agno/workflow/workflow.py +4327 -514
  575. agno-2.3.13.dist-info/METADATA +639 -0
  576. agno-2.3.13.dist-info/RECORD +613 -0
  577. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
  578. agno-2.3.13.dist-info/licenses/LICENSE +201 -0
  579. agno/api/playground.py +0 -91
  580. agno/api/schemas/playground.py +0 -22
  581. agno/api/schemas/user.py +0 -22
  582. agno/api/schemas/workspace.py +0 -46
  583. agno/api/user.py +0 -160
  584. agno/api/workspace.py +0 -151
  585. agno/cli/auth_server.py +0 -118
  586. agno/cli/config.py +0 -275
  587. agno/cli/console.py +0 -88
  588. agno/cli/credentials.py +0 -23
  589. agno/cli/entrypoint.py +0 -571
  590. agno/cli/operator.py +0 -355
  591. agno/cli/settings.py +0 -85
  592. agno/cli/ws/ws_cli.py +0 -817
  593. agno/constants.py +0 -13
  594. agno/document/__init__.py +0 -1
  595. agno/document/chunking/semantic.py +0 -47
  596. agno/document/chunking/strategy.py +0 -31
  597. agno/document/reader/__init__.py +0 -1
  598. agno/document/reader/arxiv_reader.py +0 -41
  599. agno/document/reader/base.py +0 -22
  600. agno/document/reader/csv_reader.py +0 -84
  601. agno/document/reader/docx_reader.py +0 -46
  602. agno/document/reader/firecrawl_reader.py +0 -99
  603. agno/document/reader/json_reader.py +0 -43
  604. agno/document/reader/pdf_reader.py +0 -219
  605. agno/document/reader/s3/pdf_reader.py +0 -46
  606. agno/document/reader/s3/text_reader.py +0 -51
  607. agno/document/reader/text_reader.py +0 -41
  608. agno/document/reader/website_reader.py +0 -175
  609. agno/document/reader/youtube_reader.py +0 -50
  610. agno/embedder/__init__.py +0 -1
  611. agno/embedder/azure_openai.py +0 -86
  612. agno/embedder/cohere.py +0 -72
  613. agno/embedder/fastembed.py +0 -37
  614. agno/embedder/google.py +0 -73
  615. agno/embedder/huggingface.py +0 -54
  616. agno/embedder/mistral.py +0 -80
  617. agno/embedder/ollama.py +0 -57
  618. agno/embedder/openai.py +0 -74
  619. agno/embedder/sentence_transformer.py +0 -38
  620. agno/embedder/voyageai.py +0 -64
  621. agno/eval/perf.py +0 -201
  622. agno/file/__init__.py +0 -1
  623. agno/file/file.py +0 -16
  624. agno/file/local/csv.py +0 -32
  625. agno/file/local/txt.py +0 -19
  626. agno/infra/app.py +0 -240
  627. agno/infra/base.py +0 -144
  628. agno/infra/context.py +0 -20
  629. agno/infra/db_app.py +0 -52
  630. agno/infra/resource.py +0 -205
  631. agno/infra/resources.py +0 -55
  632. agno/knowledge/agent.py +0 -230
  633. agno/knowledge/arxiv.py +0 -22
  634. agno/knowledge/combined.py +0 -22
  635. agno/knowledge/csv.py +0 -28
  636. agno/knowledge/csv_url.py +0 -19
  637. agno/knowledge/document.py +0 -20
  638. agno/knowledge/docx.py +0 -30
  639. agno/knowledge/json.py +0 -28
  640. agno/knowledge/langchain.py +0 -71
  641. agno/knowledge/llamaindex.py +0 -66
  642. agno/knowledge/pdf.py +0 -28
  643. agno/knowledge/pdf_url.py +0 -26
  644. agno/knowledge/s3/base.py +0 -60
  645. agno/knowledge/s3/pdf.py +0 -21
  646. agno/knowledge/s3/text.py +0 -23
  647. agno/knowledge/text.py +0 -30
  648. agno/knowledge/website.py +0 -88
  649. agno/knowledge/wikipedia.py +0 -31
  650. agno/knowledge/youtube.py +0 -22
  651. agno/memory/agent.py +0 -392
  652. agno/memory/classifier.py +0 -104
  653. agno/memory/db/__init__.py +0 -1
  654. agno/memory/db/base.py +0 -42
  655. agno/memory/db/mongodb.py +0 -189
  656. agno/memory/db/postgres.py +0 -203
  657. agno/memory/db/sqlite.py +0 -193
  658. agno/memory/memory.py +0 -15
  659. agno/memory/row.py +0 -36
  660. agno/memory/summarizer.py +0 -192
  661. agno/memory/summary.py +0 -19
  662. agno/memory/workflow.py +0 -38
  663. agno/models/google/gemini_openai.py +0 -26
  664. agno/models/ollama/hermes.py +0 -221
  665. agno/models/ollama/tools.py +0 -362
  666. agno/models/vertexai/gemini.py +0 -595
  667. agno/playground/__init__.py +0 -3
  668. agno/playground/async_router.py +0 -421
  669. agno/playground/deploy.py +0 -249
  670. agno/playground/operator.py +0 -92
  671. agno/playground/playground.py +0 -91
  672. agno/playground/schemas.py +0 -76
  673. agno/playground/serve.py +0 -55
  674. agno/playground/sync_router.py +0 -405
  675. agno/reasoning/agent.py +0 -68
  676. agno/run/response.py +0 -112
  677. agno/storage/agent/__init__.py +0 -0
  678. agno/storage/agent/base.py +0 -38
  679. agno/storage/agent/dynamodb.py +0 -350
  680. agno/storage/agent/json.py +0 -92
  681. agno/storage/agent/mongodb.py +0 -228
  682. agno/storage/agent/postgres.py +0 -367
  683. agno/storage/agent/session.py +0 -79
  684. agno/storage/agent/singlestore.py +0 -303
  685. agno/storage/agent/sqlite.py +0 -357
  686. agno/storage/agent/yaml.py +0 -93
  687. agno/storage/workflow/__init__.py +0 -0
  688. agno/storage/workflow/base.py +0 -40
  689. agno/storage/workflow/mongodb.py +0 -233
  690. agno/storage/workflow/postgres.py +0 -366
  691. agno/storage/workflow/session.py +0 -60
  692. agno/storage/workflow/sqlite.py +0 -359
  693. agno/tools/googlesearch.py +0 -88
  694. agno/utils/defaults.py +0 -57
  695. agno/utils/filesystem.py +0 -39
  696. agno/utils/git.py +0 -52
  697. agno/utils/json_io.py +0 -30
  698. agno/utils/load_env.py +0 -19
  699. agno/utils/py_io.py +0 -19
  700. agno/utils/pyproject.py +0 -18
  701. agno/utils/resource_filter.py +0 -31
  702. agno/vectordb/singlestore/s2vectordb.py +0 -390
  703. agno/vectordb/singlestore/s2vectordb2.py +0 -355
  704. agno/workspace/__init__.py +0 -0
  705. agno/workspace/config.py +0 -325
  706. agno/workspace/enums.py +0 -6
  707. agno/workspace/helpers.py +0 -48
  708. agno/workspace/operator.py +0 -758
  709. agno/workspace/settings.py +0 -63
  710. agno-0.1.2.dist-info/LICENSE +0 -375
  711. agno-0.1.2.dist-info/METADATA +0 -502
  712. agno-0.1.2.dist-info/RECORD +0 -352
  713. agno-0.1.2.dist-info/entry_points.txt +0 -3
  714. /agno/{cli → db/migrations}/__init__.py +0 -0
  715. /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
  716. /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
  717. /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
  718. /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
  719. /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
  720. /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
  721. /agno/{reranker → utils/models}/__init__.py +0 -0
  722. /agno/{storage → utils/print_response}/__init__.py +0 -0
  723. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
@@ -1,827 +1,1882 @@
1
+ import asyncio
2
+ import base64
1
3
  import json
2
4
  import time
3
- import traceback
4
- from dataclasses import dataclass, field
5
+ from collections.abc import AsyncIterator
6
+ from dataclasses import dataclass
5
7
  from os import getenv
6
8
  from pathlib import Path
7
- from typing import Any, Callable, Dict, Iterator, List, Optional, Union
8
-
9
- from agno.media import Audio, Image, Video
10
- from agno.models.base import Metrics, Model
11
- from agno.models.message import Message
12
- from agno.models.response import ModelResponse, ModelResponseEvent
13
- from agno.tools import Function, Toolkit
14
- from agno.utils.log import logger
9
+ from typing import Any, Dict, Iterator, List, Optional, Type, Union
10
+ from uuid import uuid4
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from agno.exceptions import ModelProviderError
15
+ from agno.media import Audio, File, Image, Video
16
+ from agno.models.base import Model, RetryableModelProviderError
17
+ from agno.models.google.utils import MALFORMED_FUNCTION_CALL_GUIDANCE, GeminiFinishReason
18
+ from agno.models.message import Citations, Message, UrlCitation
19
+ from agno.models.metrics import Metrics
20
+ from agno.models.response import ModelResponse
21
+ from agno.run.agent import RunOutput
22
+ from agno.tools.function import Function
23
+ from agno.utils.gemini import format_function_definitions, format_image_for_message, prepare_response_schema
24
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
25
+ from agno.utils.tokens import count_schema_tokens, count_text_tokens, count_tool_tokens
15
26
 
16
27
  try:
17
- import google.generativeai as genai
18
- from google.ai.generativelanguage_v1beta.types import (
19
- FunctionCall as GeminiFunctionCall,
20
- )
21
- from google.ai.generativelanguage_v1beta.types import (
22
- FunctionResponse as GeminiFunctionResponse,
23
- )
24
- from google.ai.generativelanguage_v1beta.types import (
28
+ from google import genai
29
+ from google.genai import Client as GeminiClient
30
+ from google.genai.errors import ClientError, ServerError
31
+ from google.genai.types import (
32
+ Content,
33
+ DynamicRetrievalConfig,
34
+ FileSearch,
35
+ FunctionCallingConfigMode,
36
+ GenerateContentConfig,
37
+ GenerateContentResponse,
38
+ GenerateContentResponseUsageMetadata,
39
+ GoogleSearch,
40
+ GoogleSearchRetrieval,
41
+ GroundingMetadata,
42
+ Operation,
25
43
  Part,
44
+ Retrieval,
45
+ ThinkingConfig,
46
+ Tool,
47
+ UrlContext,
48
+ VertexAISearch,
49
+ )
50
+ from google.genai.types import (
51
+ File as GeminiFile,
26
52
  )
27
- from google.ai.generativelanguage_v1beta.types.generative_service import (
28
- GenerateContentResponse as ResultGenerateContentResponse,
53
+ except ImportError:
54
+ raise ImportError(
55
+ "`google-genai` not installed or not at the latest version. Please install it using `pip install -U google-genai`"
29
56
  )
30
- from google.api_core.exceptions import PermissionDenied
31
- from google.generativeai import GenerativeModel
32
- from google.generativeai.types import file_types
33
- from google.generativeai.types.content_types import FunctionDeclaration
34
- from google.generativeai.types.content_types import Tool as GeminiTool
35
- from google.generativeai.types.generation_types import GenerateContentResponse
36
- from google.protobuf.struct_pb2 import Struct
37
- except (ModuleNotFoundError, ImportError):
38
- raise ImportError("`google-generativeai` not installed. Please install it using `pip install google-generativeai`")
39
57
 
40
58
 
41
59
  @dataclass
42
- class MessageData:
43
- response_content: str = ""
44
- response_block: Optional[GenerateContentResponse] = None
45
- response_role: Optional[str] = None
46
- response_parts: Optional[List] = None
47
- valid_response_parts: Optional[List] = None
48
- response_tool_calls: List[Dict[str, Any]] = field(default_factory=list)
49
- response_usage: Optional[ResultGenerateContentResponse] = None
50
-
51
-
52
- def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
53
- # Case 1: Image is a URL
54
- # Download the image from the URL and add it as base64 encoded data
55
- if image.url is not None and image.image_url_content is not None:
56
- try:
57
- import base64
58
-
59
- content_bytes = image.image_url_content
60
- image_data = {
61
- "mime_type": "image/jpeg",
62
- "data": base64.b64encode(content_bytes).decode("utf-8"),
63
- }
64
- return image_data
65
- except Exception as e:
66
- logger.warning(f"Failed to download image from {image}: {e}")
67
- return None
68
- # Case 2: Image is a local path
69
- # Open the image file and add it as base64 encoded data
70
- elif image.filepath is not None:
71
- try:
72
- import PIL.Image
73
- except ImportError:
74
- logger.error("`PIL.Image not installed. Please install it using 'pip install pillow'`")
75
- raise
60
+ class Gemini(Model):
61
+ """
62
+ Gemini model class for Google's Generative AI models.
76
63
 
77
- try:
78
- image_path = Path(image.filepath)
79
- if image_path.exists() and image_path.is_file():
80
- image_data = PIL.Image.open(image_path) # type: ignore
81
- else:
82
- logger.error(f"Image file {image_path} does not exist.")
83
- raise
84
- return image_data # type: ignore
85
- except Exception as e:
86
- logger.warning(f"Failed to load image from {image.filepath}: {e}")
87
- return None
64
+ Vertex AI:
65
+ - You will need Google Cloud credentials to use the Vertex AI API. Run `gcloud auth application-default login` to set credentials.
66
+ - Set `vertexai` to `True` to use the Vertex AI API.
67
+ - Set your `project_id` (or set `GOOGLE_CLOUD_PROJECT` environment variable) and `location` (optional).
68
+ - Set `http_options` (optional) to configure the HTTP options.
88
69
 
89
- # Case 3: Image is a bytes object
90
- # Add it as base64 encoded data
91
- elif image.content is not None and isinstance(image.content, bytes):
92
- import base64
70
+ Based on https://googleapis.github.io/python-genai/
71
+ """
93
72
 
94
- image_data = {"mime_type": "image/jpeg", "data": base64.b64encode(image.content).decode("utf-8")}
95
- return image_data
96
- else:
97
- logger.warning(f"Unknown image type: {type(image)}")
98
- return None
73
+ id: str = "gemini-2.0-flash-001"
74
+ name: str = "Gemini"
75
+ provider: str = "Google"
99
76
 
77
+ supports_native_structured_outputs: bool = True
100
78
 
101
- def _format_audio_for_message(audio: Audio) -> Optional[Union[Dict[str, Any], file_types.File]]:
102
- if audio.content and isinstance(audio.content, bytes):
103
- audio_content = {"mime_type": "audio/mp3", "data": audio.content}
104
- return audio_content
79
+ # Request parameters
80
+ function_declarations: Optional[List[Any]] = None
81
+ generation_config: Optional[Any] = None
82
+ safety_settings: Optional[List[Any]] = None
83
+ generative_model_kwargs: Optional[Dict[str, Any]] = None
84
+ search: bool = False
85
+ grounding: bool = False
86
+ grounding_dynamic_threshold: Optional[float] = None
87
+ url_context: bool = False
88
+ vertexai_search: bool = False
89
+ vertexai_search_datastore: Optional[str] = None
90
+
91
+ # Gemini File Search capabilities
92
+ file_search_store_names: Optional[List[str]] = None
93
+ file_search_metadata_filter: Optional[str] = None
94
+
95
+ temperature: Optional[float] = None
96
+ top_p: Optional[float] = None
97
+ top_k: Optional[int] = None
98
+ max_output_tokens: Optional[int] = None
99
+ stop_sequences: Optional[list[str]] = None
100
+ logprobs: Optional[bool] = None
101
+ presence_penalty: Optional[float] = None
102
+ frequency_penalty: Optional[float] = None
103
+ seed: Optional[int] = None
104
+ response_modalities: Optional[list[str]] = None # "TEXT", "IMAGE", and/or "AUDIO"
105
+ speech_config: Optional[dict[str, Any]] = None
106
+ cached_content: Optional[Any] = None
107
+ thinking_budget: Optional[int] = None # Thinking budget for Gemini 2.5 models
108
+ include_thoughts: Optional[bool] = None # Include thought summaries in response
109
+ thinking_level: Optional[str] = None # "low", "high"
110
+ request_params: Optional[Dict[str, Any]] = None
105
111
 
106
- elif audio.filepath is not None:
107
- audio_path = audio.filepath if isinstance(audio.filepath, Path) else Path(audio.filepath)
112
+ # Client parameters
113
+ api_key: Optional[str] = None
114
+ vertexai: bool = False
115
+ project_id: Optional[str] = None
116
+ location: Optional[str] = None
117
+ client_params: Optional[Dict[str, Any]] = None
108
118
 
109
- remote_file_name = f"files/{audio_path.stem.lower()}"
110
- # Check if video is already uploaded
111
- existing_audio_upload = None
112
- try:
113
- existing_audio_upload = genai.get_file(remote_file_name)
114
- except PermissionDenied:
115
- pass
119
+ # Gemini client
120
+ client: Optional[GeminiClient] = None
116
121
 
117
- if existing_audio_upload:
118
- audio_file = existing_audio_upload
119
- else:
120
- # Upload the video file to the Gemini API
121
- if audio_path.exists() and audio_path.is_file():
122
- audio_file = genai.upload_file(path=audio_path, name=remote_file_name, display_name=audio_path.stem)
123
- else:
124
- logger.error(f"Audio file {audio_path} does not exist.")
125
- raise Exception(f"Audio file {audio_path} does not exist.")
126
-
127
- # Check whether the file is ready to be used.
128
- while audio_file.state.name == "PROCESSING":
129
- time.sleep(2)
130
- audio_file = genai.get_file(audio_file.name)
131
-
132
- if audio_file.state.name == "FAILED":
133
- raise ValueError(audio_file.state.name)
134
- return audio_file
135
- else:
136
- logger.warning(f"Unknown audio type: {type(audio.content)}")
137
- return None
122
+ # The role to map the Gemini response
123
+ role_map = {
124
+ "model": "assistant",
125
+ }
138
126
 
127
+ # The role to map the Message
128
+ reverse_role_map = {
129
+ "assistant": "model",
130
+ "tool": "user",
131
+ }
139
132
 
140
- def _format_video_for_message(video: Video) -> Optional[file_types.File]:
141
- # If video is stored locally
142
- if video.filepath is not None:
143
- video_path = video.filepath if isinstance(video.filepath, Path) else Path(video.filepath)
133
+ def get_client(self) -> GeminiClient:
134
+ """
135
+ Returns an instance of the GeminiClient client.
144
136
 
145
- remote_file_name = f"files/{video_path.stem.lower()}"
146
- # Check if video is already uploaded
147
- existing_video_upload = None
148
- try:
149
- existing_video_upload = genai.get_file(remote_file_name)
150
- except PermissionDenied:
151
- pass
137
+ Returns:
138
+ GeminiClient: The GeminiClient client.
139
+ """
140
+ if self.client:
141
+ return self.client
142
+ client_params: Dict[str, Any] = {}
143
+ vertexai = self.vertexai or getenv("GOOGLE_GENAI_USE_VERTEXAI", "false").lower() == "true"
152
144
 
153
- if existing_video_upload:
154
- video_file = existing_video_upload
145
+ if not vertexai:
146
+ self.api_key = self.api_key or getenv("GOOGLE_API_KEY")
147
+ if not self.api_key:
148
+ log_error("GOOGLE_API_KEY not set. Please set the GOOGLE_API_KEY environment variable.")
149
+ client_params["api_key"] = self.api_key
155
150
  else:
156
- # Upload the video file to the Gemini API
157
- if video_path.exists() and video_path.is_file():
158
- video_file = genai.upload_file(path=video_path, name=remote_file_name, display_name=video_path.stem)
159
- else:
160
- logger.error(f"Video file {video_path} does not exist.")
161
- raise Exception(f"Video file {video_path} does not exist.")
162
-
163
- # Check whether the file is ready to be used.
164
- while video_file.state.name == "PROCESSING":
165
- time.sleep(2)
166
- video_file = genai.get_file(video_file.name)
151
+ log_info("Using Vertex AI API")
152
+ client_params["vertexai"] = True
153
+ project_id = self.project_id or getenv("GOOGLE_CLOUD_PROJECT")
154
+ if not project_id:
155
+ log_error("GOOGLE_CLOUD_PROJECT not set. Please set the GOOGLE_CLOUD_PROJECT environment variable.")
156
+ location = self.location or getenv("GOOGLE_CLOUD_LOCATION")
157
+ if not location:
158
+ log_error("GOOGLE_CLOUD_LOCATION not set. Please set the GOOGLE_CLOUD_LOCATION environment variable.")
159
+ client_params["project"] = project_id
160
+ client_params["location"] = location
161
+
162
+ client_params = {k: v for k, v in client_params.items() if v is not None}
167
163
 
168
- if video_file.state.name == "FAILED":
169
- raise ValueError(video_file.state.name)
164
+ if self.client_params:
165
+ client_params.update(self.client_params)
170
166
 
171
- return video_file
172
- else:
173
- logger.warning(f"Unknown video type: {type(video.content)}")
174
- return None
167
+ self.client = genai.Client(**client_params)
168
+ return self.client
175
169
 
170
+ def _append_file_search_tool(self, builtin_tools: List[Tool]) -> None:
171
+ """Append Gemini File Search tool to builtin_tools if file search is enabled.
176
172
 
177
- def _format_messages(messages: List[Message]) -> List[Dict[str, Any]]:
178
- """
179
- Converts a list of Message objects to the Gemini-compatible format.
173
+ Args:
174
+ builtin_tools: List of built-in tools to append to.
175
+ """
176
+ if not self.file_search_store_names:
177
+ return
180
178
 
181
- Args:
182
- messages (List[Message]): The list of messages to convert.
179
+ log_debug("Gemini File Search enabled.")
180
+ file_search_config: Dict[str, Any] = {"file_search_store_names": self.file_search_store_names}
181
+ if self.file_search_metadata_filter:
182
+ file_search_config["metadata_filter"] = self.file_search_metadata_filter
183
+ builtin_tools.append(Tool(file_search=FileSearch(**file_search_config))) # type: ignore[arg-type]
183
184
 
184
- Returns:
185
- List[Dict[str, Any]]: The formatted_messages list of messages.
186
- """
187
- formatted_messages: List = []
188
- for message in messages:
189
- message_for_model: Dict[str, Any] = {}
185
+ def get_request_params(
186
+ self,
187
+ system_message: Optional[str] = None,
188
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
189
+ tools: Optional[List[Dict[str, Any]]] = None,
190
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
191
+ ) -> Dict[str, Any]:
192
+ """
193
+ Returns the request keyword arguments for the GenerativeModel client.
194
+ """
195
+ request_params = {}
196
+ # User provides their own generation config
197
+ if self.generation_config is not None:
198
+ if isinstance(self.generation_config, GenerateContentConfig):
199
+ config = self.generation_config.model_dump()
200
+ else:
201
+ config = self.generation_config
202
+ else:
203
+ config = {}
190
204
 
191
- # Add role to the message for the model
192
- role = (
193
- "model" if message.role in ["system", "developer"] else "user" if message.role == "tool" else message.role
205
+ if self.generative_model_kwargs:
206
+ config.update(self.generative_model_kwargs)
207
+
208
+ config.update(
209
+ {
210
+ "safety_settings": self.safety_settings,
211
+ "temperature": self.temperature,
212
+ "top_p": self.top_p,
213
+ "top_k": self.top_k,
214
+ "max_output_tokens": self.max_output_tokens,
215
+ "stop_sequences": self.stop_sequences,
216
+ "logprobs": self.logprobs,
217
+ "presence_penalty": self.presence_penalty,
218
+ "frequency_penalty": self.frequency_penalty,
219
+ "seed": self.seed,
220
+ "response_modalities": self.response_modalities,
221
+ "speech_config": self.speech_config,
222
+ "cached_content": self.cached_content,
223
+ }
194
224
  )
195
- message_for_model["role"] = role
196
-
197
- # Add content to the message for the model
198
- content = message.content
199
- # Initialize message_parts to be used for Gemini
200
- message_parts: List[Any] = []
201
-
202
- # Function calls
203
- if (not content or message.role == "model") and message.tool_calls:
204
- for tool_call in message.tool_calls:
205
- message_parts.append(
206
- Part(
207
- function_call=GeminiFunctionCall(
208
- name=tool_call["function"]["name"],
209
- args=json.loads(tool_call["function"]["arguments"]),
225
+
226
+ if system_message is not None:
227
+ config["system_instruction"] = system_message # type: ignore
228
+
229
+ if response_format is not None and isinstance(response_format, type) and issubclass(response_format, BaseModel):
230
+ config["response_mime_type"] = "application/json" # type: ignore
231
+ # Convert Pydantic model using our hybrid approach
232
+ # This will handle complex schemas with nested models, dicts, and circular refs
233
+ config["response_schema"] = prepare_response_schema(response_format)
234
+
235
+ # Add thinking configuration
236
+ thinking_config_params: Dict[str, Any] = {}
237
+ if self.thinking_budget is not None:
238
+ thinking_config_params["thinking_budget"] = self.thinking_budget
239
+ if self.include_thoughts is not None:
240
+ thinking_config_params["include_thoughts"] = self.include_thoughts
241
+ if self.thinking_level is not None:
242
+ thinking_config_params["thinking_level"] = self.thinking_level
243
+ if thinking_config_params:
244
+ config["thinking_config"] = ThinkingConfig(**thinking_config_params)
245
+
246
+ # Build tools array based on enabled built-in tools
247
+ builtin_tools = []
248
+
249
+ if self.grounding:
250
+ log_debug(
251
+ "Gemini Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead."
252
+ )
253
+ builtin_tools.append(
254
+ Tool(
255
+ google_search=GoogleSearchRetrieval(
256
+ dynamic_retrieval_config=DynamicRetrievalConfig(
257
+ dynamic_threshold=self.grounding_dynamic_threshold
210
258
  )
211
259
  )
212
260
  )
213
- # Function results
214
- elif message.role == "tool" and hasattr(message, "combined_function_result"):
215
- s = Struct()
216
- for combined_result in message.combined_function_result:
217
- function_name = combined_result[0]
218
- function_response = combined_result[1]
219
- s.update({"result": [function_response]})
220
- message_parts.append(Part(function_response=GeminiFunctionResponse(name=function_name, response=s)))
221
- # Normal content
222
- else:
223
- if isinstance(content, str):
224
- message_parts = [content]
225
- elif isinstance(content, list):
226
- message_parts = content
261
+ )
262
+
263
+ if self.search:
264
+ log_debug("Gemini Google Search enabled.")
265
+ builtin_tools.append(Tool(google_search=GoogleSearch()))
266
+
267
+ if self.url_context:
268
+ log_debug("Gemini URL context enabled.")
269
+ builtin_tools.append(Tool(url_context=UrlContext()))
270
+
271
+ if self.vertexai_search:
272
+ log_debug("Gemini Vertex AI Search enabled.")
273
+ if not self.vertexai_search_datastore:
274
+ log_error("vertexai_search_datastore must be provided when vertexai_search is enabled.")
275
+ raise ValueError("vertexai_search_datastore must be provided when vertexai_search is enabled.")
276
+ builtin_tools.append(
277
+ Tool(retrieval=Retrieval(vertex_ai_search=VertexAISearch(datastore=self.vertexai_search_datastore)))
278
+ )
279
+
280
+ self._append_file_search_tool(builtin_tools)
281
+
282
+ # Set tools in config
283
+ if builtin_tools:
284
+ if tools:
285
+ log_info("Built-in tools enabled. External tools will be disabled.")
286
+ config["tools"] = builtin_tools
287
+ elif tools:
288
+ config["tools"] = [format_function_definitions(tools)]
289
+
290
+ if tool_choice is not None:
291
+ if isinstance(tool_choice, str) and tool_choice.lower() == "auto":
292
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.AUTO}}
293
+ elif isinstance(tool_choice, str) and tool_choice.lower() == "none":
294
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.NONE}}
295
+ elif isinstance(tool_choice, str) and tool_choice.lower() == "validated":
296
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.VALIDATED}}
297
+ elif isinstance(tool_choice, str) and tool_choice.lower() == "any":
298
+ config["tool_config"] = {"function_calling_config": {"mode": FunctionCallingConfigMode.ANY}}
227
299
  else:
228
- message_parts = [" "]
229
-
230
- if message.role == "user":
231
- # Add images to the message for the model
232
- if message.images is not None:
233
- for image in message.images:
234
- if image.content is not None and isinstance(image.content, file_types.File):
235
- # Google recommends that if using a single image, place the text prompt after the image.
236
- message_parts.insert(0, image.content)
237
- else:
238
- image_content = _format_image_for_message(image)
239
- if image_content:
240
- message_parts.append(image_content)
300
+ config["tool_config"] = {"function_calling_config": {"mode": tool_choice}}
241
301
 
242
- # Add videos to the message for the model
243
- if message.videos is not None:
244
- try:
245
- for video in message.videos:
246
- # Case 1: Video is a file_types.File object (Recommended)
247
- # Add it as a File object
248
- if video.content is not None and isinstance(video.content, file_types.File):
249
- # Google recommends that if using a single image, place the text prompt after the image.
250
- message_parts.insert(0, video.content)
251
- else:
252
- video_file = _format_video_for_message(video)
253
-
254
- # Google recommends that if using a single video, place the text prompt after the video.
255
- if video_file is not None:
256
- message_parts.insert(0, video_file) # type: ignore
257
- except Exception as e:
258
- traceback.print_exc()
259
- logger.warning(f"Failed to load video from {message.videos}: {e}")
260
- continue
261
-
262
- # Add audio to the message for the model
263
- if message.audio is not None:
264
- try:
265
- for audio_snippet in message.audio:
266
- if audio_snippet.content is not None and isinstance(audio_snippet.content, file_types.File):
267
- # Google recommends that if using a single image, place the text prompt after the image.
268
- message_parts.insert(0, audio_snippet.content)
269
- else:
270
- audio_content = _format_audio_for_message(audio_snippet)
271
- if audio_content:
272
- message_parts.append(audio_content)
273
- except Exception as e:
274
- logger.warning(f"Failed to load audio from {message.audio}: {e}")
275
- continue
302
+ config = {k: v for k, v in config.items() if v is not None}
276
303
 
277
- message_for_model["parts"] = message_parts
278
- formatted_messages.append(message_for_model)
279
- return formatted_messages
304
+ if config:
305
+ request_params["config"] = GenerateContentConfig(**config)
280
306
 
307
+ # Filter out None values
308
+ if self.request_params:
309
+ request_params.update(self.request_params)
281
310
 
282
- def _format_functions(params: Dict[str, Any]) -> Dict[str, Any]:
283
- """
284
- Converts function parameters to a Gemini-compatible format.
311
+ if request_params:
312
+ log_debug(f"Calling {self.provider} with request parameters: {request_params}", log_level=2)
313
+ return request_params
314
+
315
+ def count_tokens(
316
+ self,
317
+ messages: List[Message],
318
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
319
+ output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
320
+ ) -> int:
321
+ contents, system_instruction = self._format_messages(messages, compress_tool_results=True)
322
+ schema_tokens = count_schema_tokens(output_schema, self.id)
323
+
324
+ if self.vertexai:
325
+ # VertexAI supports full token counting with system_instruction and tools
326
+ config: Dict[str, Any] = {}
327
+ if system_instruction:
328
+ config["system_instruction"] = system_instruction
329
+ if tools:
330
+ formatted_tools = self._format_tools(tools)
331
+ gemini_tools = format_function_definitions(formatted_tools)
332
+ if gemini_tools:
333
+ config["tools"] = [gemini_tools]
334
+
335
+ response = self.get_client().models.count_tokens(
336
+ model=self.id,
337
+ contents=contents,
338
+ config=config if config else None, # type: ignore
339
+ )
340
+ return (response.total_tokens or 0) + schema_tokens
341
+ else:
342
+ # Google AI Studio: Use API for content tokens + local estimation for system/tools
343
+ # The API doesn't support system_instruction or tools in config, so we use a hybrid approach:
344
+ # 1. Get accurate token count for contents (text + multimodal) from API
345
+ # 2. Add estimated tokens for system_instruction and tools locally
346
+ try:
347
+ response = self.get_client().models.count_tokens(
348
+ model=self.id,
349
+ contents=contents,
350
+ )
351
+ total = response.total_tokens or 0
352
+ except Exception as e:
353
+ log_warning(f"Gemini count_tokens API failed: {e}. Falling back to tiktoken-based estimation.")
354
+ return super().count_tokens(messages, tools, output_schema)
285
355
 
286
- Args:
287
- params (Dict[str, Any]): The original parameters dictionary.
356
+ # Add estimated tokens for system instruction (not supported by Google AI Studio API)
357
+ if system_instruction:
358
+ system_text = system_instruction if isinstance(system_instruction, str) else str(system_instruction)
359
+ total += count_text_tokens(system_text, self.id)
288
360
 
289
- Returns:
290
- Dict[str, Any]: The converted parameters dictionary compatible with Gemini.
291
- """
292
- formatted_params = {}
293
-
294
- for key, value in params.items():
295
- if key == "properties" and isinstance(value, dict):
296
- converted_properties = {}
297
- for prop_key, prop_value in value.items():
298
- property_type = prop_value.get("type")
299
- if property_type == "array":
300
- converted_properties[prop_key] = prop_value
301
- continue
302
- if isinstance(property_type, list):
303
- # Create a copy to avoid modifying the original list
304
- non_null_types = [t for t in property_type if t != "null"]
305
- if non_null_types:
306
- # Use the first non-null type
307
- converted_type = non_null_types[0]
308
- if converted_type == "array":
309
- prop_value["type"] = converted_type
310
- converted_properties[prop_key] = prop_value
311
- continue
312
- else:
313
- # Default type if all types are 'null'
314
- converted_type = "string"
315
- else:
316
- converted_type = property_type
361
+ # Add estimated tokens for tools (not supported by Google AI Studio API)
362
+ if tools:
363
+ total += count_tool_tokens(tools, self.id)
364
+
365
+ # Add estimated tokens for response_format/output_schema
366
+ total += schema_tokens
317
367
 
318
- converted_properties[prop_key] = {"type": converted_type}
319
- formatted_params[key] = converted_properties
368
+ return total
369
+
370
+ async def acount_tokens(
371
+ self,
372
+ messages: List[Message],
373
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
374
+ output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
375
+ ) -> int:
376
+ contents, system_instruction = self._format_messages(messages, compress_tool_results=True)
377
+ schema_tokens = count_schema_tokens(output_schema, self.id)
378
+
379
+ # VertexAI supports full token counting with system_instruction and tools
380
+ if self.vertexai:
381
+ config: Dict[str, Any] = {}
382
+ if system_instruction:
383
+ config["system_instruction"] = system_instruction
384
+ if tools:
385
+ formatted_tools = self._format_tools(tools)
386
+ gemini_tools = format_function_definitions(formatted_tools)
387
+ if gemini_tools:
388
+ config["tools"] = [gemini_tools]
389
+
390
+ response = await self.get_client().aio.models.count_tokens(
391
+ model=self.id,
392
+ contents=contents,
393
+ config=config if config else None, # type: ignore
394
+ )
395
+ return (response.total_tokens or 0) + schema_tokens
320
396
  else:
321
- formatted_params[key] = value
397
+ # Hybrid approach - Google AI Studio does not support system_instruction or tools in config
398
+ try:
399
+ response = await self.get_client().aio.models.count_tokens(
400
+ model=self.id,
401
+ contents=contents,
402
+ )
403
+ total = response.total_tokens or 0
404
+ except Exception as e:
405
+ log_warning(f"Gemini count_tokens API failed: {e}. Falling back to tiktoken-based estimation.")
406
+ return await super().acount_tokens(messages, tools, output_schema)
322
407
 
323
- return formatted_params
408
+ # Add estimated tokens for system instruction
409
+ if system_instruction:
410
+ system_text = system_instruction if isinstance(system_instruction, str) else str(system_instruction)
411
+ total += count_text_tokens(system_text, self.id)
324
412
 
413
+ # Add estimated tokens for tools
414
+ if tools:
415
+ total += count_tool_tokens(tools, self.id)
325
416
 
326
- def _build_function_declaration(func: Function) -> FunctionDeclaration:
327
- """
328
- Builds the function declaration for Gemini tool calling.
417
+ # Add estimated tokens for response_format/output_schema
418
+ total += schema_tokens
329
419
 
330
- Args:
331
- func: An instance of the function.
420
+ return total
332
421
 
333
- Returns:
334
- FunctionDeclaration: The formatted function declaration.
335
- """
336
- formatted_params = _format_functions(func.parameters)
337
- if "properties" in formatted_params and formatted_params["properties"]:
338
- # We have parameters to add
339
- return FunctionDeclaration(
340
- name=func.name,
341
- description=func.description,
342
- parameters=formatted_params,
422
+ def invoke(
423
+ self,
424
+ messages: List[Message],
425
+ assistant_message: Message,
426
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
427
+ tools: Optional[List[Dict[str, Any]]] = None,
428
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
429
+ run_response: Optional[RunOutput] = None,
430
+ compress_tool_results: bool = False,
431
+ retry_with_guidance: bool = False,
432
+ ) -> ModelResponse:
433
+ """
434
+ Invokes the model with a list of messages and returns the response.
435
+ """
436
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
437
+ request_kwargs = self.get_request_params(
438
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
343
439
  )
344
- else:
345
- return FunctionDeclaration(
346
- name=func.name,
347
- description=func.description,
440
+ try:
441
+ if run_response and run_response.metrics:
442
+ run_response.metrics.set_time_to_first_token()
443
+
444
+ assistant_message.metrics.start_timer()
445
+ provider_response = self.get_client().models.generate_content(
446
+ model=self.id,
447
+ contents=formatted_messages,
448
+ **request_kwargs,
449
+ )
450
+ assistant_message.metrics.stop_timer()
451
+
452
+ model_response = self._parse_provider_response(
453
+ provider_response, response_format=response_format, retry_with_guidance=retry_with_guidance
454
+ )
455
+
456
+ # If we were retrying the invoke with guidance, remove the guidance message
457
+ if retry_with_guidance is True:
458
+ self._remove_temporary_messages(messages)
459
+
460
+ return model_response
461
+
462
+ except (ClientError, ServerError) as e:
463
+ log_error(f"Error from Gemini API: {e}")
464
+ error_message = str(e.response) if hasattr(e, "response") else str(e)
465
+ raise ModelProviderError(
466
+ message=error_message,
467
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
468
+ model_name=self.name,
469
+ model_id=self.id,
470
+ ) from e
471
+ except RetryableModelProviderError:
472
+ raise
473
+ except Exception as e:
474
+ log_error(f"Unknown error from Gemini API: {e}")
475
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
476
+
477
+ def invoke_stream(
478
+ self,
479
+ messages: List[Message],
480
+ assistant_message: Message,
481
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
482
+ tools: Optional[List[Dict[str, Any]]] = None,
483
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
484
+ run_response: Optional[RunOutput] = None,
485
+ compress_tool_results: bool = False,
486
+ retry_with_guidance: bool = False,
487
+ ) -> Iterator[ModelResponse]:
488
+ """
489
+ Invokes the model with a list of messages and returns the response as a stream.
490
+ """
491
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
492
+
493
+ request_kwargs = self.get_request_params(
494
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
348
495
  )
496
+ try:
497
+ if run_response and run_response.metrics:
498
+ run_response.metrics.set_time_to_first_token()
499
+
500
+ assistant_message.metrics.start_timer()
501
+ for response in self.get_client().models.generate_content_stream(
502
+ model=self.id,
503
+ contents=formatted_messages,
504
+ **request_kwargs,
505
+ ):
506
+ yield self._parse_provider_response_delta(response, retry_with_guidance=retry_with_guidance)
507
+
508
+ # If we were retrying the invoke with guidance, remove the guidance message
509
+ if retry_with_guidance is True:
510
+ self._remove_temporary_messages(messages)
511
+
512
+ assistant_message.metrics.stop_timer()
513
+
514
+ except (ClientError, ServerError) as e:
515
+ log_error(f"Error from Gemini API: {e}")
516
+ raise ModelProviderError(
517
+ message=str(e.response) if hasattr(e, "response") else str(e),
518
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
519
+ model_name=self.name,
520
+ model_id=self.id,
521
+ ) from e
522
+ except RetryableModelProviderError:
523
+ raise
524
+ except Exception as e:
525
+ log_error(f"Unknown error from Gemini API: {e}")
526
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
349
527
 
528
+ async def ainvoke(
529
+ self,
530
+ messages: List[Message],
531
+ assistant_message: Message,
532
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
533
+ tools: Optional[List[Dict[str, Any]]] = None,
534
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
535
+ run_response: Optional[RunOutput] = None,
536
+ compress_tool_results: bool = False,
537
+ retry_with_guidance: bool = False,
538
+ ) -> ModelResponse:
539
+ """
540
+ Invokes the model with a list of messages and returns the response.
541
+ """
542
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
350
543
 
351
- @dataclass
352
- class Gemini(Model):
353
- """
354
- Gemini model class for Google's Generative AI models.
544
+ request_kwargs = self.get_request_params(
545
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
546
+ )
355
547
 
356
- Based on https://ai.google.dev/gemini-api/docs/function-calling
357
-
358
- Attributes:
359
- id (str): Model ID. Default is `gemini-2.0-flash-exp`.
360
- name (str): The name of this chat model instance. Default is `Gemini`.
361
- provider (str): Model provider. Default is `Google`.
362
- function_declarations (List[FunctionDeclaration]): List of function declarations.
363
- generation_config (Any): Generation configuration.
364
- safety_settings (Any): Safety settings.
365
- generative_model_kwargs (Dict[str, Any]): Generative model keyword arguments.
366
- api_key (str): API key.
367
- client (GenerativeModel): Generative model client.
368
- """
548
+ try:
549
+ if run_response and run_response.metrics:
550
+ run_response.metrics.set_time_to_first_token()
551
+
552
+ assistant_message.metrics.start_timer()
553
+ provider_response = await self.get_client().aio.models.generate_content(
554
+ model=self.id,
555
+ contents=formatted_messages,
556
+ **request_kwargs,
557
+ )
558
+ assistant_message.metrics.stop_timer()
369
559
 
370
- id: str = "gemini-2.0-flash-exp"
371
- name: str = "Gemini"
372
- provider: str = "Google"
560
+ model_response = self._parse_provider_response(
561
+ provider_response, response_format=response_format, retry_with_guidance=retry_with_guidance
562
+ )
373
563
 
374
- # Request parameters
375
- function_declarations: Optional[List[FunctionDeclaration]] = None
376
- generation_config: Optional[Any] = None
377
- safety_settings: Optional[Any] = None
378
- generative_model_kwargs: Optional[Dict[str, Any]] = None
564
+ # If we were retrying the invoke with guidance, remove the guidance message
565
+ if retry_with_guidance is True:
566
+ self._remove_temporary_messages(messages)
379
567
 
380
- # Client parameters
381
- api_key: Optional[str] = None
382
- client_params: Optional[Dict[str, Any]] = None
568
+ return model_response
383
569
 
384
- # Gemini client
385
- client: Optional[GenerativeModel] = None
570
+ except (ClientError, ServerError) as e:
571
+ log_error(f"Error from Gemini API: {e}")
572
+ raise ModelProviderError(
573
+ message=str(e.response) if hasattr(e, "response") else str(e),
574
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
575
+ model_name=self.name,
576
+ model_id=self.id,
577
+ ) from e
578
+ except RetryableModelProviderError:
579
+ raise
580
+ except Exception as e:
581
+ log_error(f"Unknown error from Gemini API: {e}")
582
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
386
583
 
387
- def get_client(self) -> GenerativeModel:
584
+ async def ainvoke_stream(
585
+ self,
586
+ messages: List[Message],
587
+ assistant_message: Message,
588
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
589
+ tools: Optional[List[Dict[str, Any]]] = None,
590
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
591
+ run_response: Optional[RunOutput] = None,
592
+ compress_tool_results: bool = False,
593
+ retry_with_guidance: bool = False,
594
+ ) -> AsyncIterator[ModelResponse]:
388
595
  """
389
- Returns an instance of the GenerativeModel client.
390
-
391
- Returns:
392
- GenerativeModel: The GenerativeModel client.
596
+ Invokes the model with a list of messages and returns the response as a stream.
393
597
  """
394
- if self.client:
395
- return self.client
598
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
396
599
 
397
- client_params: Dict[str, Any] = {}
600
+ request_kwargs = self.get_request_params(
601
+ system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
602
+ )
398
603
 
399
- self.api_key = self.api_key or getenv("GOOGLE_API_KEY")
400
- if not self.api_key:
401
- logger.error("GOOGLE_API_KEY not set. Please set the GOOGLE_API_KEY environment variable.")
402
- client_params["api_key"] = self.api_key
604
+ try:
605
+ if run_response and run_response.metrics:
606
+ run_response.metrics.set_time_to_first_token()
403
607
 
404
- if self.client_params:
405
- client_params.update(self.client_params)
406
- genai.configure(**client_params)
407
- return genai.GenerativeModel(model_name=self.id, **self.request_kwargs)
608
+ assistant_message.metrics.start_timer()
408
609
 
409
- @property
410
- def request_kwargs(self) -> Dict[str, Any]:
610
+ async_stream = await self.get_client().aio.models.generate_content_stream(
611
+ model=self.id,
612
+ contents=formatted_messages,
613
+ **request_kwargs,
614
+ )
615
+ async for chunk in async_stream:
616
+ yield self._parse_provider_response_delta(chunk, retry_with_guidance=retry_with_guidance)
617
+
618
+ # If we were retrying the invoke with guidance, remove the guidance message
619
+ if retry_with_guidance is True:
620
+ self._remove_temporary_messages(messages)
621
+
622
+ assistant_message.metrics.stop_timer()
623
+
624
+ except (ClientError, ServerError) as e:
625
+ log_error(f"Error from Gemini API: {e}")
626
+ raise ModelProviderError(
627
+ message=str(e.response) if hasattr(e, "response") else str(e),
628
+ status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
629
+ model_name=self.name,
630
+ model_id=self.id,
631
+ ) from e
632
+ except RetryableModelProviderError:
633
+ raise
634
+ except Exception as e:
635
+ log_error(f"Unknown error from Gemini API: {e}")
636
+ raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
637
+
638
+ def _format_messages(self, messages: List[Message], compress_tool_results: bool = False):
411
639
  """
412
- Returns the request keyword arguments for the GenerativeModel client.
640
+ Converts a list of Message objects to the Gemini-compatible format.
413
641
 
414
- Returns:
415
- Dict[str, Any]: The request keyword arguments.
642
+ Args:
643
+ messages (List[Message]): The list of messages to convert.
644
+ compress_tool_results: Whether to compress tool results.
416
645
  """
417
- request_params: Dict[str, Any] = {}
418
- if self.generation_config:
419
- request_params["generation_config"] = self.generation_config
420
- if self.safety_settings:
421
- request_params["safety_settings"] = self.safety_settings
422
- if self.generative_model_kwargs:
423
- request_params.update(self.generative_model_kwargs)
424
- if self.function_declarations:
425
- request_params["tools"] = [GeminiTool(function_declarations=self.function_declarations)]
426
- return request_params
646
+ formatted_messages: List = []
647
+ file_content: Optional[Union[GeminiFile, Part]] = None
648
+ system_message = None
649
+
650
+ for message in messages:
651
+ role = message.role
652
+ if role in ["system", "developer"]:
653
+ system_message = message.content
654
+ continue
655
+
656
+ # Set the role for the message according to Gemini's requirements
657
+ role = self.reverse_role_map.get(role, role)
658
+
659
+ # Add content to the message for the model
660
+ content = message.get_content(use_compressed_content=compress_tool_results)
661
+
662
+ # Initialize message_parts to be used for Gemini
663
+ message_parts: List[Any] = []
664
+
665
+ # Function calls
666
+ if role == "model" and message.tool_calls is not None and len(message.tool_calls) > 0:
667
+ if content is not None:
668
+ content_str = content if isinstance(content, str) else str(content)
669
+ part = Part.from_text(text=content_str)
670
+ if message.provider_data and "thought_signature" in message.provider_data:
671
+ part.thought_signature = base64.b64decode(message.provider_data["thought_signature"])
672
+ message_parts.append(part)
673
+ for tool_call in message.tool_calls:
674
+ part = Part.from_function_call(
675
+ name=tool_call["function"]["name"],
676
+ args=json.loads(tool_call["function"]["arguments"]),
677
+ )
678
+ if "thought_signature" in tool_call:
679
+ part.thought_signature = base64.b64decode(tool_call["thought_signature"])
680
+ message_parts.append(part)
681
+ # Function call results
682
+ elif message.tool_calls is not None and len(message.tool_calls) > 0:
683
+ for idx, tool_call in enumerate(message.tool_calls):
684
+ if isinstance(content, list) and idx < len(content):
685
+ original_from_list = content[idx]
686
+
687
+ if compress_tool_results:
688
+ compressed_from_tool_call = tool_call.get("content")
689
+ tc_content = compressed_from_tool_call if compressed_from_tool_call else original_from_list
690
+ else:
691
+ tc_content = original_from_list
692
+ else:
693
+ tc_content = message.get_content(use_compressed_content=compress_tool_results)
694
+
695
+ if tc_content is None:
696
+ tc_content = tool_call.get("content")
697
+ if tc_content is None:
698
+ tc_content = content
699
+
700
+ message_parts.append(
701
+ Part.from_function_response(name=tool_call["tool_name"], response={"result": tc_content})
702
+ )
703
+ # Regular text content
704
+ else:
705
+ if isinstance(content, str):
706
+ part = Part.from_text(text=content)
707
+ if message.provider_data and "thought_signature" in message.provider_data:
708
+ part.thought_signature = base64.b64decode(message.provider_data["thought_signature"])
709
+ message_parts = [part]
710
+
711
+ if role == "user" and message.tool_calls is None:
712
+ # Add images to the message for the model
713
+ if message.images is not None:
714
+ for image in message.images:
715
+ if image.content is not None and isinstance(image.content, GeminiFile):
716
+ # Google recommends that if using a single image, place the text prompt after the image.
717
+ message_parts.insert(0, image.content)
718
+ else:
719
+ image_content = format_image_for_message(image)
720
+ if image_content:
721
+ message_parts.append(Part.from_bytes(**image_content))
722
+
723
+ # Add videos to the message for the model
724
+ if message.videos is not None:
725
+ try:
726
+ for video in message.videos:
727
+ # Case 1: Video is a file_types.File object (Recommended)
728
+ # Add it as a File object
729
+ if video.content is not None and isinstance(video.content, GeminiFile):
730
+ # Google recommends that if using a single video, place the text prompt after the video.
731
+ if video.content.uri and video.content.mime_type:
732
+ message_parts.insert(
733
+ 0, Part.from_uri(file_uri=video.content.uri, mime_type=video.content.mime_type)
734
+ )
735
+ else:
736
+ video_file = self._format_video_for_message(video)
737
+ if video_file is not None:
738
+ message_parts.insert(0, video_file)
739
+ except Exception as e:
740
+ log_warning(f"Failed to load video from {message.videos}: {e}")
741
+ continue
742
+
743
+ # Add audio to the message for the model
744
+ if message.audio is not None:
745
+ try:
746
+ for audio_snippet in message.audio:
747
+ if audio_snippet.content is not None and isinstance(audio_snippet.content, GeminiFile):
748
+ # Google recommends that if using a single audio file, place the text prompt after the audio file.
749
+ if audio_snippet.content.uri and audio_snippet.content.mime_type:
750
+ message_parts.insert(
751
+ 0,
752
+ Part.from_uri(
753
+ file_uri=audio_snippet.content.uri,
754
+ mime_type=audio_snippet.content.mime_type,
755
+ ),
756
+ )
757
+ else:
758
+ audio_content = self._format_audio_for_message(audio_snippet)
759
+ if audio_content:
760
+ message_parts.append(audio_content)
761
+ except Exception as e:
762
+ log_warning(f"Failed to load audio from {message.audio}: {e}")
763
+ continue
764
+
765
+ # Add files to the message for the model
766
+ if message.files is not None:
767
+ for file in message.files:
768
+ file_content = self._format_file_for_message(file)
769
+ if isinstance(file_content, Part):
770
+ formatted_messages.append(file_content)
771
+
772
+ final_message = Content(role=role, parts=message_parts)
773
+ formatted_messages.append(final_message)
774
+
775
+ if isinstance(file_content, GeminiFile):
776
+ formatted_messages.insert(0, file_content)
777
+
778
+ return formatted_messages, system_message
779
+
780
+ def _format_audio_for_message(self, audio: Audio) -> Optional[Union[Part, GeminiFile]]:
781
+ # Case 1: Audio is a bytes object
782
+ if audio.content and isinstance(audio.content, bytes):
783
+ mime_type = f"audio/{audio.format}" if audio.format else "audio/mp3"
784
+ return Part.from_bytes(mime_type=mime_type, data=audio.content)
785
+
786
+ # Case 2: Audio is an url
787
+ elif audio.url is not None:
788
+ audio_bytes = audio.get_content_bytes() # type: ignore
789
+ if audio_bytes is not None:
790
+ mime_type = f"audio/{audio.format}" if audio.format else "audio/mp3"
791
+ return Part.from_bytes(mime_type=mime_type, data=audio_bytes)
792
+ else:
793
+ log_warning(f"Failed to download audio from {audio}")
794
+ return None
795
+
796
+ # Case 3: Audio is a local file path
797
+ elif audio.filepath is not None:
798
+ audio_path = audio.filepath if isinstance(audio.filepath, Path) else Path(audio.filepath)
799
+
800
+ remote_file_name = f"files/{audio_path.stem.lower().replace('_', '')}"
801
+ # Check if video is already uploaded
802
+ existing_audio_upload = None
803
+ try:
804
+ if remote_file_name:
805
+ existing_audio_upload = self.get_client().files.get(name=remote_file_name)
806
+ except Exception as e:
807
+ log_warning(f"Error getting file {remote_file_name}: {e}")
808
+
809
+ if existing_audio_upload and existing_audio_upload.state and existing_audio_upload.state.name == "SUCCESS":
810
+ audio_file = existing_audio_upload
811
+ else:
812
+ # Upload the video file to the Gemini API
813
+ if audio_path.exists() and audio_path.is_file():
814
+ audio_file = self.get_client().files.upload(
815
+ file=audio_path,
816
+ config=dict(
817
+ name=remote_file_name,
818
+ display_name=audio_path.stem,
819
+ mime_type=f"audio/{audio.format}" if audio.format else "audio/mp3",
820
+ ),
821
+ )
822
+ else:
823
+ log_error(f"Audio file {audio_path} does not exist.")
824
+ return None
825
+
826
+ # Check whether the file is ready to be used.
827
+ while audio_file.state and audio_file.state.name == "PROCESSING":
828
+ if audio_file.name:
829
+ audio_file = self.get_client().files.get(name=audio_file.name)
830
+ time.sleep(2)
831
+
832
+ if audio_file.state and audio_file.state.name == "FAILED":
833
+ log_error(f"Audio file processing failed: {audio_file.state.name}")
834
+ return None
835
+
836
+ if audio_file.uri:
837
+ mime_type = f"audio/{audio.format}" if audio.format else "audio/mp3"
838
+ return Part.from_uri(file_uri=audio_file.uri, mime_type=mime_type)
839
+ return None
840
+ else:
841
+ log_warning(f"Unknown audio type: {type(audio.content)}")
842
+ return None
843
+
844
+ def _format_video_for_message(self, video: Video) -> Optional[Part]:
845
+ # Case 1: Video is a bytes object
846
+ if video.content and isinstance(video.content, bytes):
847
+ mime_type = f"video/{video.format}" if video.format else "video/mp4"
848
+ return Part.from_bytes(mime_type=mime_type, data=video.content)
849
+ # Case 2: Video is stored locally
850
+ elif video.filepath is not None:
851
+ video_path = video.filepath if isinstance(video.filepath, Path) else Path(video.filepath)
852
+
853
+ remote_file_name = f"files/{video_path.stem.lower().replace('_', '')}"
854
+ # Check if video is already uploaded
855
+ existing_video_upload = None
856
+ try:
857
+ if remote_file_name:
858
+ existing_video_upload = self.get_client().files.get(name=remote_file_name)
859
+ except Exception as e:
860
+ log_warning(f"Error getting file {remote_file_name}: {e}")
861
+
862
+ if existing_video_upload and existing_video_upload.state and existing_video_upload.state.name == "SUCCESS":
863
+ video_file = existing_video_upload
864
+ else:
865
+ # Upload the video file to the Gemini API
866
+ if video_path.exists() and video_path.is_file():
867
+ video_file = self.get_client().files.upload(
868
+ file=video_path,
869
+ config=dict(
870
+ name=remote_file_name,
871
+ display_name=video_path.stem,
872
+ mime_type=f"video/{video.format}" if video.format else "video/mp4",
873
+ ),
874
+ )
875
+ else:
876
+ log_error(f"Video file {video_path} does not exist.")
877
+ return None
878
+
879
+ # Check whether the file is ready to be used.
880
+ while video_file.state and video_file.state.name == "PROCESSING":
881
+ if video_file.name:
882
+ video_file = self.get_client().files.get(name=video_file.name)
883
+ time.sleep(2)
884
+
885
+ if video_file.state and video_file.state.name == "FAILED":
886
+ log_error(f"Video file processing failed: {video_file.state.name}")
887
+ return None
888
+
889
+ if video_file.uri:
890
+ mime_type = f"video/{video.format}" if video.format else "video/mp4"
891
+ return Part.from_uri(file_uri=video_file.uri, mime_type=mime_type)
892
+ return None
893
+ # Case 3: Video is a URL
894
+ elif video.url is not None:
895
+ mime_type = f"video/{video.format}" if video.format else "video/webm"
896
+ return Part.from_uri(
897
+ file_uri=video.url,
898
+ mime_type=mime_type,
899
+ )
900
+ else:
901
+ log_warning(f"Unknown video type: {type(video.content)}")
902
+ return None
903
+
904
+ def _format_file_for_message(self, file: File) -> Optional[Part]:
905
+ # Case 1: File is a bytes object
906
+ if file.content and isinstance(file.content, bytes) and file.mime_type:
907
+ return Part.from_bytes(mime_type=file.mime_type, data=file.content)
908
+
909
+ # Case 2: File is a URL
910
+ elif file.url is not None:
911
+ url_content = file.file_url_content
912
+ if url_content is not None:
913
+ content, mime_type = url_content
914
+ if mime_type and content:
915
+ return Part.from_bytes(mime_type=mime_type, data=content)
916
+ log_warning(f"Failed to download file from {file.url}")
917
+ return None
918
+
919
+ # Case 3: File is a local file path
920
+ elif file.filepath is not None:
921
+ file_path = file.filepath if isinstance(file.filepath, Path) else Path(file.filepath)
922
+ if file_path.exists() and file_path.is_file():
923
+ if file_path.stat().st_size < 20 * 1024 * 1024: # 20MB in bytes
924
+ if file.mime_type:
925
+ file_content = file_path.read_bytes()
926
+ if file_content:
927
+ return Part.from_bytes(mime_type=file.mime_type, data=file_content)
928
+ else:
929
+ import mimetypes
930
+
931
+ mime_type_guess = mimetypes.guess_type(file_path)[0]
932
+ if mime_type_guess is not None:
933
+ file_content = file_path.read_bytes()
934
+ if file_content:
935
+ mime_type_str: str = str(mime_type_guess)
936
+ return Part.from_bytes(mime_type=mime_type_str, data=file_content)
937
+ return None
938
+ else:
939
+ clean_file_name = f"files/{file_path.stem.lower().replace('_', '')}"
940
+ remote_file = None
941
+ try:
942
+ if clean_file_name:
943
+ remote_file = self.get_client().files.get(name=clean_file_name)
944
+ except Exception as e:
945
+ log_warning(f"Error getting file {clean_file_name}: {e}")
946
+
947
+ if (
948
+ remote_file
949
+ and remote_file.state
950
+ and remote_file.state.name == "SUCCESS"
951
+ and remote_file.uri
952
+ and remote_file.mime_type
953
+ ):
954
+ file_uri: str = remote_file.uri
955
+ file_mime_type: str = remote_file.mime_type
956
+ return Part.from_uri(file_uri=file_uri, mime_type=file_mime_type)
957
+ else:
958
+ log_error(f"File {file_path} does not exist.")
959
+ return None
427
960
 
428
- def add_tool(
961
+ # Case 4: File is a Gemini File object
962
+ elif isinstance(file.external, GeminiFile):
963
+ if file.external.uri and file.external.mime_type:
964
+ return Part.from_uri(file_uri=file.external.uri, mime_type=file.external.mime_type)
965
+ return None
966
+ return None
967
+
968
+ def format_function_call_results(
429
969
  self,
430
- tool: Union[Toolkit, Callable, Dict, Function],
431
- strict: bool = False,
432
- agent: Optional[Any] = None,
970
+ messages: List[Message],
971
+ function_call_results: List[Message],
972
+ compress_tool_results: bool = False,
973
+ **kwargs,
433
974
  ) -> None:
434
975
  """
435
- Adds tools to the model.
976
+ Format function call results for Gemini.
977
+
978
+ For combined messages:
979
+ - content: list of ORIGINAL content (for preservation)
980
+ - tool_calls[i]["content"]: compressed content if available (for API sending)
981
+
982
+ This allows the message to be saved with both original and compressed versions.
983
+ """
984
+ combined_original_content: List = []
985
+ combined_function_result: List = []
986
+ tool_names: List[str] = []
987
+
988
+ message_metrics = Metrics()
989
+
990
+ if len(function_call_results) > 0:
991
+ for idx, result in enumerate(function_call_results):
992
+ combined_original_content.append(result.content)
993
+ compressed_content = result.get_content(use_compressed_content=compress_tool_results)
994
+ combined_function_result.append(
995
+ {"tool_call_id": result.tool_call_id, "tool_name": result.tool_name, "content": compressed_content}
996
+ )
997
+ if result.tool_name:
998
+ tool_names.append(result.tool_name)
999
+ message_metrics += result.metrics
1000
+
1001
+ tool_name = ", ".join(tool_names) if tool_names else None
1002
+
1003
+ if combined_original_content:
1004
+ messages.append(
1005
+ Message(
1006
+ role="tool",
1007
+ content=combined_original_content,
1008
+ tool_name=tool_name,
1009
+ tool_calls=combined_function_result,
1010
+ metrics=message_metrics,
1011
+ )
1012
+ )
1013
+
1014
+ def _parse_provider_response(self, response: GenerateContentResponse, **kwargs) -> ModelResponse:
1015
+ """
1016
+ Parse the Gemini response into a ModelResponse.
436
1017
 
437
1018
  Args:
438
- tool: The tool to add. Can be a Tool, Toolkit, Callable, dict, or Function.
439
- strict: If True, raise an error if the tool is not a Toolkit or Callable.
440
- agent: The agent to associate with the tool.
441
- """
442
- if self.function_declarations is None:
443
- self.function_declarations = []
444
-
445
- # If the tool is a Tool or Dict, log a warning.
446
- if isinstance(tool, Dict):
447
- logger.warning("Tool of type 'dict' is not yet supported by Gemini.")
448
-
449
- # If the tool is a Callable or Toolkit, add its functions to the Model
450
- elif callable(tool) or isinstance(tool, Toolkit) or isinstance(tool, Function):
451
- if self._functions is None:
452
- self._functions: Dict[str, Any] = {}
453
-
454
- if isinstance(tool, Toolkit):
455
- # For each function in the toolkit, process entrypoint and add to self.tools
456
- for name, func in tool.functions.items():
457
- # If the function does not exist in self._functions, add to self.tools
458
- if name not in self._functions:
459
- func._agent = agent
460
- func.process_entrypoint()
461
- self._functions[name] = func
462
- function_declaration = _build_function_declaration(func)
463
- self.function_declarations.append(function_declaration)
464
- logger.debug(f"Function {name} from {tool.name} added to model.")
465
-
466
- elif isinstance(tool, Function):
467
- if tool.name not in self._functions:
468
- tool._agent = agent
469
- tool.process_entrypoint()
470
- self._functions[tool.name] = tool
471
-
472
- function_declaration = _build_function_declaration(tool)
473
- self.function_declarations.append(function_declaration)
474
- logger.debug(f"Function {tool.name} added to model.")
475
-
476
- elif callable(tool):
1019
+ response: Raw response from Gemini
1020
+
1021
+ Returns:
1022
+ ModelResponse: Parsed response data
1023
+ """
1024
+ model_response = ModelResponse()
1025
+
1026
+ # Get response message
1027
+ response_message = Content(role="model", parts=[])
1028
+ if response.candidates and len(response.candidates) > 0:
1029
+ candidate = response.candidates[0]
1030
+
1031
+ # Raise if the request failed because of a malformed function call
1032
+ if hasattr(candidate, "finish_reason") and candidate.finish_reason:
1033
+ if candidate.finish_reason == GeminiFinishReason.MALFORMED_FUNCTION_CALL.value:
1034
+ if self.retry_with_guidance:
1035
+ raise RetryableModelProviderError(
1036
+ retry_guidance_message=MALFORMED_FUNCTION_CALL_GUIDANCE,
1037
+ original_error=f"Generation ended with finish reason: {candidate.finish_reason}",
1038
+ )
1039
+
1040
+ if candidate.content:
1041
+ response_message = candidate.content
1042
+
1043
+ # Add role
1044
+ if response_message.role is not None:
1045
+ model_response.role = self.role_map[response_message.role]
1046
+
1047
+ # Add content
1048
+ if response_message.parts is not None and len(response_message.parts) > 0:
1049
+ for part in response_message.parts:
1050
+ # Extract text if present
1051
+ if hasattr(part, "text") and part.text is not None:
1052
+ text_content: Optional[str] = getattr(part, "text")
1053
+ if isinstance(text_content, str):
1054
+ # Check if this is a thought summary
1055
+ if hasattr(part, "thought") and part.thought:
1056
+ # Add all parts as single message
1057
+ if model_response.reasoning_content is None:
1058
+ model_response.reasoning_content = text_content
1059
+ else:
1060
+ model_response.reasoning_content += text_content
1061
+ else:
1062
+ if model_response.content is None:
1063
+ model_response.content = text_content
1064
+ else:
1065
+ model_response.content += text_content
1066
+ else:
1067
+ content_str = str(text_content) if text_content is not None else ""
1068
+ if hasattr(part, "thought") and part.thought:
1069
+ # Add all parts as single message
1070
+ if model_response.reasoning_content is None:
1071
+ model_response.reasoning_content = content_str
1072
+ else:
1073
+ model_response.reasoning_content += content_str
1074
+ else:
1075
+ if model_response.content is None:
1076
+ model_response.content = content_str
1077
+ else:
1078
+ model_response.content += content_str
1079
+
1080
+ # Capture thought signature for text parts
1081
+ if hasattr(part, "thought_signature") and part.thought_signature:
1082
+ if model_response.provider_data is None:
1083
+ model_response.provider_data = {}
1084
+ model_response.provider_data["thought_signature"] = base64.b64encode(
1085
+ part.thought_signature
1086
+ ).decode("ascii")
1087
+
1088
+ if hasattr(part, "inline_data") and part.inline_data is not None:
1089
+ # Handle audio responses (for TTS models)
1090
+ if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
1091
+ # Store raw bytes data
1092
+ model_response.audio = Audio(
1093
+ id=str(uuid4()),
1094
+ content=part.inline_data.data,
1095
+ mime_type=part.inline_data.mime_type,
1096
+ )
1097
+ # Image responses
1098
+ else:
1099
+ if model_response.images is None:
1100
+ model_response.images = []
1101
+ model_response.images.append(
1102
+ Image(id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type)
1103
+ )
1104
+
1105
+ # Extract function call if present
1106
+ if hasattr(part, "function_call") and part.function_call is not None:
1107
+ call_id = part.function_call.id if part.function_call.id else str(uuid4())
1108
+ tool_call = {
1109
+ "id": call_id,
1110
+ "type": "function",
1111
+ "function": {
1112
+ "name": part.function_call.name,
1113
+ "arguments": json.dumps(part.function_call.args)
1114
+ if part.function_call.args is not None
1115
+ else "",
1116
+ },
1117
+ }
1118
+
1119
+ # Capture thought signature for function calls
1120
+ if hasattr(part, "thought_signature") and part.thought_signature:
1121
+ tool_call["thought_signature"] = base64.b64encode(part.thought_signature).decode("ascii")
1122
+
1123
+ model_response.tool_calls.append(tool_call)
1124
+
1125
+ citations = Citations()
1126
+ citations_raw = {}
1127
+ citations_urls = []
1128
+ web_search_queries: List[str] = []
1129
+
1130
+ if response.candidates and response.candidates[0].grounding_metadata is not None:
1131
+ grounding_metadata: GroundingMetadata = response.candidates[0].grounding_metadata
1132
+ citations_raw["grounding_metadata"] = grounding_metadata.model_dump()
1133
+
1134
+ chunks = grounding_metadata.grounding_chunks or []
1135
+ web_search_queries = grounding_metadata.web_search_queries or []
1136
+ for chunk in chunks:
1137
+ if not chunk:
1138
+ continue
1139
+ web = chunk.web
1140
+ if not web:
1141
+ continue
1142
+ uri = web.uri
1143
+ title = web.title
1144
+ if uri:
1145
+ citations_urls.append(UrlCitation(url=uri, title=title))
1146
+
1147
+ # Handle URLs from URL context tool
1148
+ if (
1149
+ response.candidates
1150
+ and hasattr(response.candidates[0], "url_context_metadata")
1151
+ and response.candidates[0].url_context_metadata is not None
1152
+ ):
1153
+ url_context_metadata = response.candidates[0].url_context_metadata
1154
+ citations_raw["url_context_metadata"] = url_context_metadata.model_dump()
1155
+
1156
+ url_metadata_list = url_context_metadata.url_metadata or []
1157
+ for url_meta in url_metadata_list:
1158
+ retrieved_url = url_meta.retrieved_url
1159
+ status = "UNKNOWN"
1160
+ if url_meta.url_retrieval_status:
1161
+ status = url_meta.url_retrieval_status.value
1162
+ if retrieved_url and status == "URL_RETRIEVAL_STATUS_SUCCESS":
1163
+ # Avoid duplicate URLs
1164
+ existing_urls = [citation.url for citation in citations_urls]
1165
+ if retrieved_url not in existing_urls:
1166
+ citations_urls.append(UrlCitation(url=retrieved_url, title=retrieved_url))
1167
+
1168
+ if citations_raw:
1169
+ citations.raw = citations_raw
1170
+ if citations_urls:
1171
+ citations.urls = citations_urls
1172
+ if web_search_queries:
1173
+ citations.search_queries = web_search_queries
1174
+
1175
+ if citations_raw or citations_urls:
1176
+ model_response.citations = citations
1177
+
1178
+ # Extract usage metadata if present
1179
+ if hasattr(response, "usage_metadata") and response.usage_metadata is not None:
1180
+ model_response.response_usage = self._get_metrics(response.usage_metadata)
1181
+
1182
+ # If we have no content but have a role, add a default empty content
1183
+ if model_response.role and model_response.content is None and not model_response.tool_calls:
1184
+ model_response.content = ""
1185
+
1186
+ return model_response
1187
+
1188
+ def _parse_provider_response_delta(self, response_delta: GenerateContentResponse, **kwargs) -> ModelResponse:
1189
+ model_response = ModelResponse()
1190
+
1191
+ if response_delta.candidates and len(response_delta.candidates) > 0:
1192
+ candidate = response_delta.candidates[0]
1193
+ candidate_content = candidate.content
1194
+
1195
+ # Raise if the request failed because of a malformed function call
1196
+ if hasattr(candidate, "finish_reason") and candidate.finish_reason:
1197
+ if candidate.finish_reason == GeminiFinishReason.MALFORMED_FUNCTION_CALL.value:
1198
+ if self.retry_with_guidance:
1199
+ raise RetryableModelProviderError(
1200
+ retry_guidance_message=MALFORMED_FUNCTION_CALL_GUIDANCE,
1201
+ original_error=f"Generation ended with finish reason: {candidate.finish_reason}",
1202
+ )
1203
+
1204
+ response_message: Content = Content(role="model", parts=[])
1205
+ if candidate_content is not None:
1206
+ response_message = candidate_content
1207
+
1208
+ # Add role
1209
+ if response_message.role is not None:
1210
+ model_response.role = self.role_map[response_message.role]
1211
+
1212
+ if response_message.parts is not None:
1213
+ for part in response_message.parts:
1214
+ # Extract text if present
1215
+ if hasattr(part, "text") and part.text is not None:
1216
+ text_content = str(part.text) if part.text is not None else ""
1217
+ # Check if this is a thought summary
1218
+ if hasattr(part, "thought") and part.thought:
1219
+ if model_response.reasoning_content is None:
1220
+ model_response.reasoning_content = text_content
1221
+ else:
1222
+ model_response.reasoning_content += text_content
1223
+ else:
1224
+ if model_response.content is None:
1225
+ model_response.content = text_content
1226
+ else:
1227
+ model_response.content += text_content
1228
+
1229
+ # Capture thought signature for text parts
1230
+ if hasattr(part, "thought_signature") and part.thought_signature:
1231
+ if model_response.provider_data is None:
1232
+ model_response.provider_data = {}
1233
+ model_response.provider_data["thought_signature"] = base64.b64encode(
1234
+ part.thought_signature
1235
+ ).decode("ascii")
1236
+
1237
+ if hasattr(part, "inline_data") and part.inline_data is not None:
1238
+ # Audio responses
1239
+ if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
1240
+ # Store raw bytes audio data
1241
+ model_response.audio = Audio(
1242
+ id=str(uuid4()),
1243
+ content=part.inline_data.data,
1244
+ mime_type=part.inline_data.mime_type,
1245
+ )
1246
+ # Image responses
1247
+ else:
1248
+ if model_response.images is None:
1249
+ model_response.images = []
1250
+ model_response.images.append(
1251
+ Image(
1252
+ id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type
1253
+ )
1254
+ )
1255
+
1256
+ # Extract function call if present
1257
+ if hasattr(part, "function_call") and part.function_call is not None:
1258
+ call_id = part.function_call.id if part.function_call.id else str(uuid4())
1259
+ tool_call = {
1260
+ "id": call_id,
1261
+ "type": "function",
1262
+ "function": {
1263
+ "name": part.function_call.name,
1264
+ "arguments": json.dumps(part.function_call.args)
1265
+ if part.function_call.args is not None
1266
+ else "",
1267
+ },
1268
+ }
1269
+
1270
+ # Capture thought signature for function calls
1271
+ if hasattr(part, "thought_signature") and part.thought_signature:
1272
+ tool_call["thought_signature"] = base64.b64encode(part.thought_signature).decode("ascii")
1273
+
1274
+ model_response.tool_calls.append(tool_call)
1275
+
1276
+ citations = Citations()
1277
+ citations.raw = {}
1278
+ citations.urls = []
1279
+
1280
+ if (
1281
+ hasattr(response_delta.candidates[0], "grounding_metadata")
1282
+ and response_delta.candidates[0].grounding_metadata is not None
1283
+ ):
1284
+ grounding_metadata = response_delta.candidates[0].grounding_metadata
1285
+ citations.raw["grounding_metadata"] = grounding_metadata.model_dump()
1286
+ citations.search_queries = grounding_metadata.web_search_queries or []
1287
+ # Extract url and title
1288
+ chunks = grounding_metadata.grounding_chunks or []
1289
+ for chunk in chunks:
1290
+ if not chunk:
1291
+ continue
1292
+ web = chunk.web
1293
+ if not web:
1294
+ continue
1295
+ uri = web.uri
1296
+ title = web.title
1297
+ if uri:
1298
+ citations.urls.append(UrlCitation(url=uri, title=title))
1299
+
1300
+ # Handle URLs from URL context tool
1301
+ if (
1302
+ hasattr(response_delta.candidates[0], "url_context_metadata")
1303
+ and response_delta.candidates[0].url_context_metadata is not None
1304
+ ):
1305
+ url_context_metadata = response_delta.candidates[0].url_context_metadata
1306
+
1307
+ citations.raw["url_context_metadata"] = url_context_metadata.model_dump()
1308
+
1309
+ url_metadata_list = url_context_metadata.url_metadata or []
1310
+ for url_meta in url_metadata_list:
1311
+ retrieved_url = url_meta.retrieved_url
1312
+ status = "UNKNOWN"
1313
+ if url_meta.url_retrieval_status:
1314
+ status = url_meta.url_retrieval_status.value
1315
+ if retrieved_url and status == "URL_RETRIEVAL_STATUS_SUCCESS":
1316
+ # Avoid duplicate URLs
1317
+ existing_urls = [citation.url for citation in citations.urls]
1318
+ if retrieved_url not in existing_urls:
1319
+ citations.urls.append(UrlCitation(url=retrieved_url, title=retrieved_url))
1320
+
1321
+ if citations.raw or citations.urls:
1322
+ model_response.citations = citations
1323
+
1324
+ # Extract usage metadata if present
1325
+ if hasattr(response_delta, "usage_metadata") and response_delta.usage_metadata is not None:
1326
+ model_response.response_usage = self._get_metrics(response_delta.usage_metadata)
1327
+
1328
+ return model_response
1329
+
1330
+ def __deepcopy__(self, memo):
1331
+ """
1332
+ Creates a deep copy of the Gemini model instance but sets the client to None.
1333
+
1334
+ This is useful when we need to copy the model configuration without duplicating
1335
+ the client connection.
1336
+
1337
+ This overrides the base class implementation.
1338
+ """
1339
+ from copy import copy, deepcopy
1340
+
1341
+ # Create a new instance without calling __init__
1342
+ cls = self.__class__
1343
+ new_instance = cls.__new__(cls)
1344
+
1345
+ # Update memo with the new instance to avoid circular references
1346
+ memo[id(self)] = new_instance
1347
+
1348
+ # Deep copy all attributes except client and unpickleable attributes
1349
+ for key, value in self.__dict__.items():
1350
+ # Skip client and other unpickleable attributes
1351
+ if key in {"client", "response_format", "_tools", "_functions", "_function_call_stack"}:
1352
+ continue
1353
+
1354
+ # Try deep copy first, fall back to shallow copy, then direct assignment
1355
+ try:
1356
+ setattr(new_instance, key, deepcopy(value, memo))
1357
+ except Exception:
477
1358
  try:
478
- function_name = tool.__name__
479
- if function_name not in self._functions:
480
- func = Function.from_callable(tool)
481
- self._functions[func.name] = func
482
- function_declaration = _build_function_declaration(func)
483
- self.function_declarations.append(function_declaration)
484
- logger.debug(f"Function '{func.name}' added to model.")
485
- except Exception as e:
486
- logger.warning(f"Could not add function {tool}: {e}")
487
-
488
- def invoke(self, messages: List[Message]):
1359
+ setattr(new_instance, key, copy(value))
1360
+ except Exception:
1361
+ setattr(new_instance, key, value)
1362
+
1363
+ # Explicitly set client to None
1364
+ setattr(new_instance, "client", None)
1365
+
1366
+ return new_instance
1367
+
1368
+ def _get_metrics(self, response_usage: GenerateContentResponseUsageMetadata) -> Metrics:
489
1369
  """
490
- Invokes the model with a list of messages and returns the response.
1370
+ Parse the given Google Gemini usage into an Agno Metrics object.
491
1371
 
492
1372
  Args:
493
- messages (List[Message]): The list of messages to send to the model.
1373
+ response_usage: Usage data from Google Gemini
494
1374
 
495
1375
  Returns:
496
- GenerateContentResponse: The response from the model.
1376
+ Metrics: Parsed metrics data
497
1377
  """
498
- return self.get_client().generate_content(contents=_format_messages(messages))
1378
+ metrics = Metrics()
1379
+
1380
+ metrics.input_tokens = response_usage.prompt_token_count or 0
1381
+ metrics.output_tokens = response_usage.candidates_token_count or 0
1382
+ if response_usage.thoughts_token_count is not None:
1383
+ metrics.output_tokens += response_usage.thoughts_token_count or 0
1384
+ metrics.total_tokens = metrics.input_tokens + metrics.output_tokens
499
1385
 
500
- def invoke_stream(self, messages: List[Message]):
1386
+ metrics.cache_read_tokens = response_usage.cached_content_token_count or 0
1387
+
1388
+ if response_usage.traffic_type is not None:
1389
+ metrics.provider_metrics = {"traffic_type": response_usage.traffic_type}
1390
+
1391
+ return metrics
1392
+
1393
+ def create_file_search_store(self, display_name: Optional[str] = None) -> Any:
501
1394
  """
502
- Invokes the model with a list of messages and returns the response as a stream.
1395
+ Create a new File Search store.
503
1396
 
504
1397
  Args:
505
- messages (List[Message]): The list of messages to send to the model.
1398
+ display_name: Optional display name for the store
506
1399
 
507
1400
  Returns:
508
- Iterator[GenerateContentResponse]: The response from the model as a stream.
1401
+ FileSearchStore: The created File Search store object
509
1402
  """
510
- yield from self.get_client().generate_content(
511
- contents=_format_messages(messages),
512
- stream=True,
513
- )
1403
+ config: Dict[str, Any] = {}
1404
+ if display_name:
1405
+ config["display_name"] = display_name
514
1406
 
515
- def update_usage_metrics(
516
- self,
517
- assistant_message: Message,
518
- usage: Optional[ResultGenerateContentResponse] = None,
519
- metrics: Metrics = Metrics(),
520
- ) -> None:
521
- """
522
- Update the usage metrics.
1407
+ try:
1408
+ store = self.get_client().file_search_stores.create(config=config or None) # type: ignore[arg-type]
1409
+ log_info(f"Created File Search store: {store.name}")
1410
+ return store
1411
+ except Exception as e:
1412
+ log_error(f"Error creating File Search store: {e}")
1413
+ raise
523
1414
 
1415
+ async def async_create_file_search_store(self, display_name: Optional[str] = None) -> Any:
1416
+ """
524
1417
  Args:
525
- assistant_message (Message): The assistant message.
526
- usage (ResultGenerateContentResponse): The usage metrics.
527
- stream_usage (Optional[StreamUsageData]): The stream usage metrics.
1418
+ display_name: Optional display name for the store
1419
+
1420
+ Returns:
1421
+ FileSearchStore: The created File Search store object
528
1422
  """
529
- if usage:
530
- metrics.input_tokens = usage.prompt_token_count or 0
531
- metrics.output_tokens = usage.candidates_token_count or 0
532
- metrics.total_tokens = usage.total_token_count or 0
1423
+ config: Dict[str, Any] = {}
1424
+ if display_name:
1425
+ config["display_name"] = display_name
533
1426
 
534
- self._update_model_metrics(metrics_for_run=metrics)
535
- self._update_assistant_message_metrics(assistant_message=assistant_message, metrics_for_run=metrics)
1427
+ try:
1428
+ store = await self.get_client().aio.file_search_stores.create(config=config or None) # type: ignore[arg-type]
1429
+ log_info(f"Created File Search store: {store.name}")
1430
+ return store
1431
+ except Exception as e:
1432
+ log_error(f"Error creating File Search store: {e}")
1433
+ raise
536
1434
 
537
- def create_assistant_message(self, response: GenerateContentResponse, metrics: Metrics) -> Message:
1435
+ def list_file_search_stores(self, page_size: int = 100) -> List[Any]:
538
1436
  """
539
- Create an assistant message from the response.
1437
+ List all File Search stores.
540
1438
 
541
1439
  Args:
542
- response (GenerateContentResponse): The model response.
543
- response_timer (Timer): The response timer.
1440
+ page_size: Maximum number of stores to return per page
544
1441
 
545
1442
  Returns:
546
- Message: The assistant message.
1443
+ List: List of FileSearchStore objects
547
1444
  """
548
- message_data = MessageData()
1445
+ try:
1446
+ stores = []
1447
+ for store in self.get_client().file_search_stores.list(config={"page_size": page_size}):
1448
+ stores.append(store)
1449
+ log_debug(f"Found {len(stores)} File Search stores")
1450
+ return stores
1451
+ except Exception as e:
1452
+ log_error(f"Error listing File Search stores: {e}")
1453
+ raise
549
1454
 
550
- message_data.response_block = response.candidates[0].content
551
- message_data.response_role = message_data.response_block.role
552
- message_data.response_parts = message_data.response_block.parts
553
- message_data.response_usage = response.usage_metadata
1455
+ async def async_list_file_search_stores(self, page_size: int = 100) -> List[Any]:
1456
+ """
1457
+ Async version of list_file_search_stores.
554
1458
 
555
- if message_data.response_parts is not None:
556
- for part in message_data.response_parts:
557
- part_dict = type(part).to_dict(part)
1459
+ Args:
1460
+ page_size: Maximum number of stores to return per page
558
1461
 
559
- # Extract text if present
560
- if "text" in part_dict:
561
- message_data.response_content = part_dict.get("text")
1462
+ Returns:
1463
+ List: List of FileSearchStore objects
1464
+ """
1465
+ try:
1466
+ stores = []
1467
+ async for store in await self.get_client().aio.file_search_stores.list(config={"page_size": page_size}):
1468
+ stores.append(store)
1469
+ log_debug(f"Found {len(stores)} File Search stores")
1470
+ return stores
1471
+ except Exception as e:
1472
+ log_error(f"Error listing File Search stores: {e}")
1473
+ raise
562
1474
 
563
- # Parse function calls
564
- if "function_call" in part_dict:
565
- message_data.response_tool_calls.append(
566
- {
567
- "type": "function",
568
- "function": {
569
- "name": part_dict.get("function_call").get("name"),
570
- "arguments": json.dumps(part_dict.get("function_call").get("args")),
571
- },
572
- }
573
- )
1475
+ def get_file_search_store(self, name: str) -> Any:
1476
+ """
1477
+ Get a specific File Search store by name.
574
1478
 
575
- # -*- Create assistant message
576
- assistant_message = Message(
577
- role=message_data.response_role or "model",
578
- content=message_data.response_content,
579
- )
1479
+ Args:
1480
+ name: The name of the store (e.g., 'fileSearchStores/my-store-123')
580
1481
 
581
- # -*- Update assistant message if tool calls are present
582
- if len(message_data.response_tool_calls) > 0:
583
- assistant_message.tool_calls = message_data.response_tool_calls
1482
+ Returns:
1483
+ FileSearchStore: The File Search store object
1484
+ """
1485
+ try:
1486
+ store = self.get_client().file_search_stores.get(name=name)
1487
+ log_debug(f"Retrieved File Search store: {name}")
1488
+ return store
1489
+ except Exception as e:
1490
+ log_error(f"Error getting File Search store {name}: {e}")
1491
+ raise
584
1492
 
585
- # -*- Update usage metrics
586
- self.update_usage_metrics(assistant_message, message_data.response_usage, metrics)
587
- return assistant_message
1493
+ async def async_get_file_search_store(self, name: str) -> Any:
1494
+ """
1495
+ Args:
1496
+ name: The name of the store
588
1497
 
589
- def format_function_call_results(
590
- self,
591
- function_call_results: List[Message],
592
- messages: List[Message],
593
- ):
1498
+ Returns:
1499
+ FileSearchStore: The File Search store object
1500
+ """
1501
+ try:
1502
+ store = await self.get_client().aio.file_search_stores.get(name=name)
1503
+ log_debug(f"Retrieved File Search store: {name}")
1504
+ return store
1505
+ except Exception as e:
1506
+ log_error(f"Error getting File Search store {name}: {e}")
1507
+ raise
1508
+
1509
+ def delete_file_search_store(self, name: str, force: bool = False) -> None:
594
1510
  """
595
- Processes the results of function calls and appends them to messages.
1511
+ Delete a File Search store.
596
1512
 
597
1513
  Args:
598
- function_call_results (List[Message]): The results from running function calls.
599
- messages (List[Message]): The list of conversation messages.
1514
+ name: The name of the store to delete
1515
+ force: If True, force delete even if store contains documents
600
1516
  """
601
- if function_call_results:
602
- combined_content: List = []
603
- combined_function_result: List = []
1517
+ try:
1518
+ self.get_client().file_search_stores.delete(name=name, config={"force": force})
1519
+ log_info(f"Deleted File Search store: {name}")
1520
+ except Exception as e:
1521
+ log_error(f"Error deleting File Search store {name}: {e}")
1522
+ raise
604
1523
 
605
- for result in function_call_results:
606
- combined_content.append(result.content)
607
- combined_function_result.append((result.tool_name, result.content))
1524
+ async def async_delete_file_search_store(self, name: str, force: bool = True) -> None:
1525
+ """
1526
+ Async version of delete_file_search_store.
608
1527
 
609
- messages.append(
610
- Message(role="tool", content=combined_content, combined_function_details=combined_function_result)
611
- )
1528
+ Args:
1529
+ name: The name of the store to delete
1530
+ force: If True, force delete even if store contains documents
1531
+ """
1532
+ try:
1533
+ await self.get_client().aio.file_search_stores.delete(name=name, config={"force": force})
1534
+ log_info(f"Deleted File Search store: {name}")
1535
+ except Exception as e:
1536
+ log_error(f"Error deleting File Search store {name}: {e}")
1537
+ raise
612
1538
 
613
- def handle_tool_calls(self, assistant_message: Message, messages: List[Message], model_response: ModelResponse):
1539
+ def wait_for_operation(self, operation: Operation, poll_interval: int = 5, max_wait: int = 600) -> Operation:
614
1540
  """
615
- Handle tool calls in the assistant message.
1541
+ Wait for a long-running operation to complete.
616
1542
 
617
1543
  Args:
618
- assistant_message (Message): The assistant message.
619
- messages (List[Message]): A list of messages.
620
- model_response (ModelResponse): The model response.
1544
+ operation: The operation object to wait for
1545
+ poll_interval: Seconds to wait between status checks
1546
+ max_wait: Maximum seconds to wait before timing out
621
1547
 
622
1548
  Returns:
623
- Optional[ModelResponse]: The updated model response.
624
- """
625
- if assistant_message.tool_calls:
626
- if model_response.tool_calls is None:
627
- model_response.tool_calls = []
628
- model_response.content = assistant_message.get_content_string() or ""
629
- function_calls_to_run = self._get_function_calls_to_run(
630
- assistant_message, messages, error_response_role="tool"
631
- )
632
-
633
- if self.show_tool_calls:
634
- if len(function_calls_to_run) == 1:
635
- model_response.content += f"\n - Running: {function_calls_to_run[0].get_call_str()}\n\n"
636
- elif len(function_calls_to_run) > 1:
637
- model_response.content += "\nRunning:"
638
- for _f in function_calls_to_run:
639
- model_response.content += f"\n - {_f.get_call_str()}"
640
- model_response.content += "\n\n"
641
-
642
- function_call_results: List[Message] = []
643
- for function_call_response in self.run_function_calls(
644
- function_calls=function_calls_to_run,
645
- function_call_results=function_call_results,
646
- ):
647
- if (
648
- function_call_response.event == ModelResponseEvent.tool_call_completed.value
649
- and function_call_response.tool_calls is not None
650
- ):
651
- model_response.tool_calls.extend(function_call_response.tool_calls)
1549
+ Operation: The completed operation object
652
1550
 
653
- self.format_function_call_results(function_call_results, messages)
1551
+ Raises:
1552
+ TimeoutError: If operation doesn't complete within max_wait seconds
1553
+ """
1554
+ elapsed = 0
1555
+ while not operation.done:
1556
+ if elapsed >= max_wait:
1557
+ raise TimeoutError(f"Operation timed out after {max_wait} seconds")
1558
+ time.sleep(poll_interval)
1559
+ elapsed += poll_interval
1560
+ operation = self.get_client().operations.get(operation)
1561
+ log_debug(f"Waiting for operation... ({elapsed}s elapsed)")
1562
+
1563
+ log_info("Operation completed successfully")
1564
+ return operation
1565
+
1566
+ async def async_wait_for_operation(
1567
+ self, operation: Operation, poll_interval: int = 5, max_wait: int = 600
1568
+ ) -> Operation:
1569
+ """
1570
+ Async version of wait_for_operation.
654
1571
 
655
- return model_response
656
- return None
1572
+ Args:
1573
+ operation: The operation object to wait for
1574
+ poll_interval: Seconds to wait between status checks
1575
+ max_wait: Maximum seconds to wait before timing out
657
1576
 
658
- def response(self, messages: List[Message]) -> ModelResponse:
1577
+ Returns:
1578
+ Operation: The completed operation object
659
1579
  """
660
- Send a generate cone content request to the model and return the response.
1580
+ elapsed = 0
1581
+ while not operation.done:
1582
+ if elapsed >= max_wait:
1583
+ raise TimeoutError(f"Operation timed out after {max_wait} seconds")
1584
+ await asyncio.sleep(poll_interval)
1585
+ elapsed += poll_interval
1586
+ operation = await self.get_client().aio.operations.get(operation)
1587
+ log_debug(f"Waiting for operation... ({elapsed}s elapsed)")
1588
+
1589
+ log_info("Operation completed successfully")
1590
+ return operation
1591
+
1592
+ def upload_to_file_search_store(
1593
+ self,
1594
+ file_path: Union[str, Path],
1595
+ store_name: str,
1596
+ display_name: Optional[str] = None,
1597
+ chunking_config: Optional[Dict[str, Any]] = None,
1598
+ custom_metadata: Optional[List[Dict[str, Any]]] = None,
1599
+ ) -> Any:
1600
+ """
1601
+ Upload a file directly to a File Search store.
661
1602
 
662
1603
  Args:
663
- messages (List[Message]): The list of messages to send to the model.
1604
+ file_path: Path to the file to upload
1605
+ store_name: Name of the File Search store
1606
+ display_name: Optional display name for the file (will be visible in citations)
1607
+ chunking_config: Optional chunking configuration
1608
+ Example: {
1609
+ "white_space_config": {
1610
+ "max_tokens_per_chunk": 200,
1611
+ "max_overlap_tokens": 20
1612
+ }
1613
+ }
1614
+ custom_metadata: Optional custom metadata as list of dicts
1615
+ Example: [
1616
+ {"key": "author", "string_value": "John Doe"},
1617
+ {"key": "year", "numeric_value": 2024}
1618
+ ]
664
1619
 
665
1620
  Returns:
666
- ModelResponse: The model response.
1621
+ Operation: Long-running operation object. Use wait_for_operation() to wait for completion.
667
1622
  """
668
- logger.debug("---------- Gemini Response Start ----------")
669
- self._log_messages(messages)
670
- model_response = ModelResponse()
671
- metrics = Metrics()
1623
+ file_path = file_path if isinstance(file_path, Path) else Path(file_path)
672
1624
 
673
- # -*- Generate response
674
- metrics.start_response_timer()
675
- response: GenerateContentResponse = self.invoke(messages=messages)
676
- metrics.stop_response_timer()
1625
+ if not file_path.exists():
1626
+ raise FileNotFoundError(f"File not found: {file_path}")
677
1627
 
678
- # -*- Create assistant message
679
- assistant_message = self.create_assistant_message(response=response, metrics=metrics)
1628
+ config: Dict[str, Any] = {}
1629
+ if display_name:
1630
+ config["display_name"] = display_name
1631
+ if chunking_config:
1632
+ config["chunking_config"] = chunking_config
1633
+ if custom_metadata:
1634
+ config["custom_metadata"] = custom_metadata
680
1635
 
681
- # -*- Add assistant message to messages
682
- messages.append(assistant_message)
1636
+ try:
1637
+ log_info(f"Uploading file {file_path.name} to File Search store {store_name}")
1638
+ operation = self.get_client().file_search_stores.upload_to_file_search_store(
1639
+ file=file_path,
1640
+ file_search_store_name=store_name,
1641
+ config=config or None, # type: ignore[arg-type]
1642
+ )
1643
+ log_info(f"Upload initiated for {file_path.name}")
1644
+ return operation
1645
+ except Exception as e:
1646
+ log_error(f"Error uploading file to File Search store: {e}")
1647
+ raise
683
1648
 
684
- # -*- Log response and metrics
685
- assistant_message.log()
686
- metrics.log()
1649
+ async def async_upload_to_file_search_store(
1650
+ self,
1651
+ file_path: Union[str, Path],
1652
+ store_name: str,
1653
+ display_name: Optional[str] = None,
1654
+ chunking_config: Optional[Dict[str, Any]] = None,
1655
+ custom_metadata: Optional[List[Dict[str, Any]]] = None,
1656
+ ) -> Any:
1657
+ """
1658
+ Args:
1659
+ file_path: Path to the file to upload
1660
+ store_name: Name of the File Search store
1661
+ display_name: Optional display name for the file
1662
+ chunking_config: Optional chunking configuration
1663
+ custom_metadata: Optional custom metadata
687
1664
 
688
- # -*- Update model response with assistant message content
689
- if assistant_message.content is not None:
690
- model_response.content = assistant_message.get_content_string()
1665
+ Returns:
1666
+ Operation: Long-running operation object
1667
+ """
1668
+ file_path = file_path if isinstance(file_path, Path) else Path(file_path)
691
1669
 
692
- # -*- Handle tool calls
693
- if self.handle_tool_calls(assistant_message, messages, model_response) is not None:
694
- response_after_tool_calls = self.response(messages=messages)
695
- if response_after_tool_calls.content is not None:
696
- if model_response.content is None:
697
- model_response.content = ""
698
- model_response.content += response_after_tool_calls.content
1670
+ if not file_path.exists():
1671
+ raise FileNotFoundError(f"File not found: {file_path}")
699
1672
 
700
- return model_response
1673
+ config: Dict[str, Any] = {}
1674
+ if display_name:
1675
+ config["display_name"] = display_name
1676
+ if chunking_config:
1677
+ config["chunking_config"] = chunking_config
1678
+ if custom_metadata:
1679
+ config["custom_metadata"] = custom_metadata
701
1680
 
702
- logger.debug("---------- Gemini Response End ----------")
703
- return model_response
1681
+ try:
1682
+ log_info(f"Uploading file {file_path.name} to File Search store {store_name}")
1683
+ operation = await self.get_client().aio.file_search_stores.upload_to_file_search_store(
1684
+ file=file_path,
1685
+ file_search_store_name=store_name,
1686
+ config=config or None, # type: ignore[arg-type]
1687
+ )
1688
+ log_info(f"Upload initiated for {file_path.name}")
1689
+ return operation
1690
+ except Exception as e:
1691
+ log_error(f"Error uploading file to File Search store: {e}")
1692
+ raise
704
1693
 
705
- def handle_stream_tool_calls(self, assistant_message: Message, messages: List[Message]):
1694
+ def import_file_to_store(
1695
+ self,
1696
+ file_name: str,
1697
+ store_name: str,
1698
+ chunking_config: Optional[Dict[str, Any]] = None,
1699
+ custom_metadata: Optional[List[Dict[str, Any]]] = None,
1700
+ ) -> Any:
706
1701
  """
707
- Parse and run function calls and append the results to messages.
1702
+ Import an existing uploaded file (via Files API) into a File Search store.
708
1703
 
709
1704
  Args:
710
- assistant_message (Message): The assistant message containing tool calls.
711
- messages (List[Message]): The list of conversation messages.
1705
+ file_name: Name of the file already uploaded via Files API
1706
+ store_name: Name of the File Search store
1707
+ chunking_config: Optional chunking configuration
1708
+ custom_metadata: Optional custom metadata
712
1709
 
713
- Yields:
714
- Iterator[ModelResponse]: Yields model responses during function execution.
1710
+ Returns:
1711
+ Operation: Long-running operation object. Use wait_for_operation() to wait for completion.
715
1712
  """
716
- if assistant_message.tool_calls:
717
- function_calls_to_run = self._get_function_calls_to_run(
718
- assistant_message, messages, error_response_role="tool"
719
- )
1713
+ config: Dict[str, Any] = {}
1714
+ if chunking_config:
1715
+ config["chunking_config"] = chunking_config
1716
+ if custom_metadata:
1717
+ config["custom_metadata"] = custom_metadata
720
1718
 
721
- if self.show_tool_calls:
722
- if len(function_calls_to_run) == 1:
723
- yield ModelResponse(content=f"\n - Running: {function_calls_to_run[0].get_call_str()}\n\n")
724
- elif len(function_calls_to_run) > 1:
725
- yield ModelResponse(content="\nRunning:")
726
- for _f in function_calls_to_run:
727
- yield ModelResponse(content=f"\n - {_f.get_call_str()}")
728
- yield ModelResponse(content="\n\n")
729
-
730
- function_call_results: List[Message] = []
731
- for intermediate_model_response in self.run_function_calls(
732
- function_calls=function_calls_to_run, function_call_results=function_call_results
733
- ):
734
- yield intermediate_model_response
1719
+ try:
1720
+ log_info(f"Importing file {file_name} to File Search store {store_name}")
1721
+ operation = self.get_client().file_search_stores.import_file(
1722
+ file_search_store_name=store_name,
1723
+ file_name=file_name,
1724
+ config=config or None, # type: ignore[arg-type]
1725
+ )
1726
+ log_info(f"Import initiated for {file_name}")
1727
+ return operation
1728
+ except Exception as e:
1729
+ log_error(f"Error importing file to File Search store: {e}")
1730
+ raise
735
1731
 
736
- self.format_function_call_results(function_call_results, messages)
1732
+ async def async_import_file_to_store(
1733
+ self,
1734
+ file_name: str,
1735
+ store_name: str,
1736
+ chunking_config: Optional[Dict[str, Any]] = None,
1737
+ custom_metadata: Optional[List[Dict[str, Any]]] = None,
1738
+ ) -> Any:
1739
+ """
1740
+ Args:
1741
+ file_name: Name of the file already uploaded via Files API
1742
+ store_name: Name of the File Search store
1743
+ chunking_config: Optional chunking configuration
1744
+ custom_metadata: Optional custom metadata
737
1745
 
738
- def response_stream(self, messages: List[Message]) -> Iterator[ModelResponse]:
1746
+ Returns:
1747
+ Operation: Long-running operation object
739
1748
  """
740
- Send a generate content request to the model and return the response as a stream.
1749
+ config: Dict[str, Any] = {}
1750
+ if chunking_config:
1751
+ config["chunking_config"] = chunking_config
1752
+ if custom_metadata:
1753
+ config["custom_metadata"] = custom_metadata
1754
+
1755
+ try:
1756
+ log_info(f"Importing file {file_name} to File Search store {store_name}")
1757
+ operation = await self.get_client().aio.file_search_stores.import_file(
1758
+ file_search_store_name=store_name,
1759
+ file_name=file_name,
1760
+ config=config or None, # type: ignore[arg-type]
1761
+ )
1762
+ log_info(f"Import initiated for {file_name}")
1763
+ return operation
1764
+ except Exception as e:
1765
+ log_error(f"Error importing file to File Search store: {e}")
1766
+ raise
741
1767
 
1768
+ def list_documents(self, store_name: str, page_size: int = 20) -> List[Any]:
1769
+ """
742
1770
  Args:
743
- messages (List[Message]): The list of messages to send to the model.
1771
+ store_name: Name of the File Search store
1772
+ page_size: Maximum number of documents to return per page
744
1773
 
745
- Yields:
746
- Iterator[ModelResponse]: The model responses
1774
+ Returns:
1775
+ List: List of document objects
747
1776
  """
748
- logger.debug("---------- Gemini Response Start ----------")
749
- self._log_messages(messages)
750
- message_data = MessageData()
751
- metrics = Metrics()
1777
+ try:
1778
+ documents = []
1779
+ for doc in self.get_client().file_search_stores.documents.list(
1780
+ parent=store_name, config={"page_size": page_size}
1781
+ ):
1782
+ documents.append(doc)
1783
+ log_debug(f"Found {len(documents)} documents in store {store_name}")
1784
+ return documents
1785
+ except Exception as e:
1786
+ log_error(f"Error listing documents in store {store_name}: {e}")
1787
+ raise
752
1788
 
753
- metrics.start_response_timer()
754
- for response in self.invoke_stream(messages=messages):
755
- message_data.response_block = response.candidates[0].content
756
- message_data.response_role = message_data.response_block.role
757
- message_data.response_parts = message_data.response_block.parts
758
-
759
- if message_data.response_parts is not None:
760
- for part in message_data.response_parts:
761
- part_dict = type(part).to_dict(part)
762
-
763
- # -*- Yield text if present
764
- if "text" in part_dict:
765
- text = part_dict.get("text")
766
- yield ModelResponse(content=text)
767
- message_data.response_content += text
768
- metrics.output_tokens += 1
769
- if metrics.output_tokens == 1:
770
- metrics.time_to_first_token = metrics.response_timer.elapsed
771
- else:
772
- message_data.valid_response_parts = message_data.response_parts
1789
+ async def async_list_documents(self, store_name: str, page_size: int = 20) -> List[Any]:
1790
+ """
1791
+ Async version of list_documents.
773
1792
 
774
- # -*- Skip function calls if there are no parts
775
- if not message_data.response_block.parts and message_data.response_parts:
776
- continue
777
- # -*- Parse function calls
778
- if "function_call" in part_dict:
779
- message_data.response_tool_calls.append(
780
- {
781
- "type": "function",
782
- "function": {
783
- "name": part_dict.get("function_call").get("name"),
784
- "arguments": json.dumps(part_dict.get("function_call").get("args")),
785
- },
786
- }
787
- )
788
- message_data.response_usage = response.usage_metadata
789
- metrics.stop_response_timer()
1793
+ Args:
1794
+ store_name: Name of the File Search store
1795
+ page_size: Maximum number of documents to return per page
790
1796
 
791
- # -*- Create assistant message
792
- assistant_message = Message(
793
- role=message_data.response_role or "model",
794
- content=message_data.response_content,
795
- )
1797
+ Returns:
1798
+ List: List of document objects
1799
+ """
1800
+ try:
1801
+ documents = []
1802
+ # Await the AsyncPager first, then iterate
1803
+ async for doc in await self.get_client().aio.file_search_stores.documents.list(
1804
+ parent=store_name, config={"page_size": page_size}
1805
+ ):
1806
+ documents.append(doc)
1807
+ log_debug(f"Found {len(documents)} documents in store {store_name}")
1808
+ return documents
1809
+ except Exception as e:
1810
+ log_error(f"Error listing documents in store {store_name}: {e}")
1811
+ raise
796
1812
 
797
- # -*- Update assistant message if tool calls are present
798
- if len(message_data.response_tool_calls) > 0:
799
- assistant_message.tool_calls = message_data.response_tool_calls
1813
+ def get_document(self, document_name: str) -> Any:
1814
+ """
1815
+ Get a specific document by name.
800
1816
 
801
- # -*- Update usage metrics
802
- self.update_usage_metrics(assistant_message, message_data.response_usage, metrics)
1817
+ Args:
1818
+ document_name: Full name of the document
1819
+ (e.g., 'fileSearchStores/store-123/documents/doc-456')
803
1820
 
804
- # -*- Add assistant message to messages
805
- messages.append(assistant_message)
1821
+ Returns:
1822
+ Document object
1823
+ """
1824
+ try:
1825
+ doc = self.get_client().file_search_stores.documents.get(name=document_name)
1826
+ log_debug(f"Retrieved document: {document_name}")
1827
+ return doc
1828
+ except Exception as e:
1829
+ log_error(f"Error getting document {document_name}: {e}")
1830
+ raise
806
1831
 
807
- # -*- Log response and metrics
808
- assistant_message.log()
809
- metrics.log()
1832
+ async def async_get_document(self, document_name: str) -> Any:
1833
+ """
1834
+ Async version of get_document.
810
1835
 
811
- if assistant_message.tool_calls is not None and len(assistant_message.tool_calls) > 0:
812
- yield from self.handle_stream_tool_calls(assistant_message, messages)
813
- yield from self.response_stream(messages=messages)
1836
+ Args:
1837
+ document_name: Full name of the document
814
1838
 
815
- logger.debug("---------- Gemini Response End ----------")
1839
+ Returns:
1840
+ Document object
1841
+ """
1842
+ try:
1843
+ doc = await self.get_client().aio.file_search_stores.documents.get(name=document_name)
1844
+ log_debug(f"Retrieved document: {document_name}")
1845
+ return doc
1846
+ except Exception as e:
1847
+ log_error(f"Error getting document {document_name}: {e}")
1848
+ raise
816
1849
 
817
- async def ainvoke(self, *args, **kwargs) -> Any:
818
- raise NotImplementedError(f"Async not supported on {self.name}.")
1850
+ def delete_document(self, document_name: str) -> None:
1851
+ """
1852
+ Delete a document from a File Search store.
819
1853
 
820
- async def ainvoke_stream(self, *args, **kwargs) -> Any:
821
- raise NotImplementedError(f"Async not supported on {self.name}.")
1854
+ Args:
1855
+ document_name: Full name of the document to delete
822
1856
 
823
- async def aresponse(self, messages: List[Message]) -> ModelResponse:
824
- raise NotImplementedError(f"Async not supported on {self.name}.")
1857
+ Example:
1858
+ ```python
1859
+ model = Gemini(id="gemini-2.5-flash")
1860
+ model.delete_document("fileSearchStores/store-123/documents/doc-456")
1861
+ ```
1862
+ """
1863
+ try:
1864
+ self.get_client().file_search_stores.documents.delete(name=document_name)
1865
+ log_info(f"Deleted document: {document_name}")
1866
+ except Exception as e:
1867
+ log_error(f"Error deleting document {document_name}: {e}")
1868
+ raise
825
1869
 
826
- async def aresponse_stream(self, messages: List[Message]) -> ModelResponse:
827
- raise NotImplementedError(f"Async not supported on {self.name}.")
1870
+ async def async_delete_document(self, document_name: str) -> None:
1871
+ """
1872
+ Async version of delete_document.
1873
+
1874
+ Args:
1875
+ document_name: Full name of the document to delete
1876
+ """
1877
+ try:
1878
+ await self.get_client().aio.file_search_stores.documents.delete(name=document_name)
1879
+ log_info(f"Deleted document: {document_name}")
1880
+ except Exception as e:
1881
+ log_error(f"Error deleting document {document_name}: {e}")
1882
+ raise