agno 2.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 (314) hide show
  1. agno/agent/agent.py +5540 -2273
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/compression/__init__.py +3 -0
  5. agno/compression/manager.py +247 -0
  6. agno/culture/__init__.py +3 -0
  7. agno/culture/manager.py +956 -0
  8. agno/db/async_postgres/__init__.py +3 -0
  9. agno/db/base.py +689 -6
  10. agno/db/dynamo/dynamo.py +933 -37
  11. agno/db/dynamo/schemas.py +174 -10
  12. agno/db/dynamo/utils.py +63 -4
  13. agno/db/firestore/firestore.py +831 -9
  14. agno/db/firestore/schemas.py +51 -0
  15. agno/db/firestore/utils.py +102 -4
  16. agno/db/gcs_json/gcs_json_db.py +660 -12
  17. agno/db/gcs_json/utils.py +60 -26
  18. agno/db/in_memory/in_memory_db.py +287 -14
  19. agno/db/in_memory/utils.py +60 -2
  20. agno/db/json/json_db.py +590 -14
  21. agno/db/json/utils.py +60 -26
  22. agno/db/migrations/manager.py +199 -0
  23. agno/db/migrations/v1_to_v2.py +43 -13
  24. agno/db/migrations/versions/__init__.py +0 -0
  25. agno/db/migrations/versions/v2_3_0.py +938 -0
  26. agno/db/mongo/__init__.py +15 -1
  27. agno/db/mongo/async_mongo.py +2760 -0
  28. agno/db/mongo/mongo.py +879 -11
  29. agno/db/mongo/schemas.py +42 -0
  30. agno/db/mongo/utils.py +80 -8
  31. agno/db/mysql/__init__.py +2 -1
  32. agno/db/mysql/async_mysql.py +2912 -0
  33. agno/db/mysql/mysql.py +946 -68
  34. agno/db/mysql/schemas.py +72 -10
  35. agno/db/mysql/utils.py +198 -7
  36. agno/db/postgres/__init__.py +2 -1
  37. agno/db/postgres/async_postgres.py +2579 -0
  38. agno/db/postgres/postgres.py +942 -57
  39. agno/db/postgres/schemas.py +81 -18
  40. agno/db/postgres/utils.py +164 -2
  41. agno/db/redis/redis.py +671 -7
  42. agno/db/redis/schemas.py +50 -0
  43. agno/db/redis/utils.py +65 -7
  44. agno/db/schemas/__init__.py +2 -1
  45. agno/db/schemas/culture.py +120 -0
  46. agno/db/schemas/evals.py +1 -0
  47. agno/db/schemas/memory.py +17 -2
  48. agno/db/singlestore/schemas.py +63 -0
  49. agno/db/singlestore/singlestore.py +949 -83
  50. agno/db/singlestore/utils.py +60 -2
  51. agno/db/sqlite/__init__.py +2 -1
  52. agno/db/sqlite/async_sqlite.py +2911 -0
  53. agno/db/sqlite/schemas.py +62 -0
  54. agno/db/sqlite/sqlite.py +965 -46
  55. agno/db/sqlite/utils.py +169 -8
  56. agno/db/surrealdb/__init__.py +3 -0
  57. agno/db/surrealdb/metrics.py +292 -0
  58. agno/db/surrealdb/models.py +334 -0
  59. agno/db/surrealdb/queries.py +71 -0
  60. agno/db/surrealdb/surrealdb.py +1908 -0
  61. agno/db/surrealdb/utils.py +147 -0
  62. agno/db/utils.py +2 -0
  63. agno/eval/__init__.py +10 -0
  64. agno/eval/accuracy.py +75 -55
  65. agno/eval/agent_as_judge.py +861 -0
  66. agno/eval/base.py +29 -0
  67. agno/eval/performance.py +16 -7
  68. agno/eval/reliability.py +28 -16
  69. agno/eval/utils.py +35 -17
  70. agno/exceptions.py +27 -2
  71. agno/filters.py +354 -0
  72. agno/guardrails/prompt_injection.py +1 -0
  73. agno/hooks/__init__.py +3 -0
  74. agno/hooks/decorator.py +164 -0
  75. agno/integrations/discord/client.py +1 -1
  76. agno/knowledge/chunking/agentic.py +13 -10
  77. agno/knowledge/chunking/fixed.py +4 -1
  78. agno/knowledge/chunking/semantic.py +9 -4
  79. agno/knowledge/chunking/strategy.py +59 -15
  80. agno/knowledge/embedder/fastembed.py +1 -1
  81. agno/knowledge/embedder/nebius.py +1 -1
  82. agno/knowledge/embedder/ollama.py +8 -0
  83. agno/knowledge/embedder/openai.py +8 -8
  84. agno/knowledge/embedder/sentence_transformer.py +6 -2
  85. agno/knowledge/embedder/vllm.py +262 -0
  86. agno/knowledge/knowledge.py +1618 -318
  87. agno/knowledge/reader/base.py +6 -2
  88. agno/knowledge/reader/csv_reader.py +8 -10
  89. agno/knowledge/reader/docx_reader.py +5 -6
  90. agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
  91. agno/knowledge/reader/json_reader.py +5 -4
  92. agno/knowledge/reader/markdown_reader.py +8 -8
  93. agno/knowledge/reader/pdf_reader.py +17 -19
  94. agno/knowledge/reader/pptx_reader.py +101 -0
  95. agno/knowledge/reader/reader_factory.py +32 -3
  96. agno/knowledge/reader/s3_reader.py +3 -3
  97. agno/knowledge/reader/tavily_reader.py +193 -0
  98. agno/knowledge/reader/text_reader.py +22 -10
  99. agno/knowledge/reader/web_search_reader.py +1 -48
  100. agno/knowledge/reader/website_reader.py +10 -10
  101. agno/knowledge/reader/wikipedia_reader.py +33 -1
  102. agno/knowledge/types.py +1 -0
  103. agno/knowledge/utils.py +72 -7
  104. agno/media.py +22 -6
  105. agno/memory/__init__.py +14 -1
  106. agno/memory/manager.py +544 -83
  107. agno/memory/strategies/__init__.py +15 -0
  108. agno/memory/strategies/base.py +66 -0
  109. agno/memory/strategies/summarize.py +196 -0
  110. agno/memory/strategies/types.py +37 -0
  111. agno/models/aimlapi/aimlapi.py +17 -0
  112. agno/models/anthropic/claude.py +515 -40
  113. agno/models/aws/bedrock.py +102 -21
  114. agno/models/aws/claude.py +131 -274
  115. agno/models/azure/ai_foundry.py +41 -19
  116. agno/models/azure/openai_chat.py +39 -8
  117. agno/models/base.py +1249 -525
  118. agno/models/cerebras/cerebras.py +91 -21
  119. agno/models/cerebras/cerebras_openai.py +21 -2
  120. agno/models/cohere/chat.py +40 -6
  121. agno/models/cometapi/cometapi.py +18 -1
  122. agno/models/dashscope/dashscope.py +2 -3
  123. agno/models/deepinfra/deepinfra.py +18 -1
  124. agno/models/deepseek/deepseek.py +69 -3
  125. agno/models/fireworks/fireworks.py +18 -1
  126. agno/models/google/gemini.py +877 -80
  127. agno/models/google/utils.py +22 -0
  128. agno/models/groq/groq.py +51 -18
  129. agno/models/huggingface/huggingface.py +17 -6
  130. agno/models/ibm/watsonx.py +16 -6
  131. agno/models/internlm/internlm.py +18 -1
  132. agno/models/langdb/langdb.py +13 -1
  133. agno/models/litellm/chat.py +44 -9
  134. agno/models/litellm/litellm_openai.py +18 -1
  135. agno/models/message.py +28 -5
  136. agno/models/meta/llama.py +47 -14
  137. agno/models/meta/llama_openai.py +22 -17
  138. agno/models/mistral/mistral.py +8 -4
  139. agno/models/nebius/nebius.py +6 -7
  140. agno/models/nvidia/nvidia.py +20 -3
  141. agno/models/ollama/chat.py +24 -8
  142. agno/models/openai/chat.py +104 -29
  143. agno/models/openai/responses.py +101 -81
  144. agno/models/openrouter/openrouter.py +60 -3
  145. agno/models/perplexity/perplexity.py +17 -1
  146. agno/models/portkey/portkey.py +7 -6
  147. agno/models/requesty/requesty.py +24 -4
  148. agno/models/response.py +73 -2
  149. agno/models/sambanova/sambanova.py +20 -3
  150. agno/models/siliconflow/siliconflow.py +19 -2
  151. agno/models/together/together.py +20 -3
  152. agno/models/utils.py +254 -8
  153. agno/models/vercel/v0.py +20 -3
  154. agno/models/vertexai/__init__.py +0 -0
  155. agno/models/vertexai/claude.py +190 -0
  156. agno/models/vllm/vllm.py +19 -14
  157. agno/models/xai/xai.py +19 -2
  158. agno/os/app.py +549 -152
  159. agno/os/auth.py +190 -3
  160. agno/os/config.py +23 -0
  161. agno/os/interfaces/a2a/router.py +8 -11
  162. agno/os/interfaces/a2a/utils.py +1 -1
  163. agno/os/interfaces/agui/router.py +18 -3
  164. agno/os/interfaces/agui/utils.py +152 -39
  165. agno/os/interfaces/slack/router.py +55 -37
  166. agno/os/interfaces/slack/slack.py +9 -1
  167. agno/os/interfaces/whatsapp/router.py +0 -1
  168. agno/os/interfaces/whatsapp/security.py +3 -1
  169. agno/os/mcp.py +110 -52
  170. agno/os/middleware/__init__.py +2 -0
  171. agno/os/middleware/jwt.py +676 -112
  172. agno/os/router.py +40 -1478
  173. agno/os/routers/agents/__init__.py +3 -0
  174. agno/os/routers/agents/router.py +599 -0
  175. agno/os/routers/agents/schema.py +261 -0
  176. agno/os/routers/evals/evals.py +96 -39
  177. agno/os/routers/evals/schemas.py +65 -33
  178. agno/os/routers/evals/utils.py +80 -10
  179. agno/os/routers/health.py +10 -4
  180. agno/os/routers/knowledge/knowledge.py +196 -38
  181. agno/os/routers/knowledge/schemas.py +82 -22
  182. agno/os/routers/memory/memory.py +279 -52
  183. agno/os/routers/memory/schemas.py +46 -17
  184. agno/os/routers/metrics/metrics.py +20 -8
  185. agno/os/routers/metrics/schemas.py +16 -16
  186. agno/os/routers/session/session.py +462 -34
  187. agno/os/routers/teams/__init__.py +3 -0
  188. agno/os/routers/teams/router.py +512 -0
  189. agno/os/routers/teams/schema.py +257 -0
  190. agno/os/routers/traces/__init__.py +3 -0
  191. agno/os/routers/traces/schemas.py +414 -0
  192. agno/os/routers/traces/traces.py +499 -0
  193. agno/os/routers/workflows/__init__.py +3 -0
  194. agno/os/routers/workflows/router.py +624 -0
  195. agno/os/routers/workflows/schema.py +75 -0
  196. agno/os/schema.py +256 -693
  197. agno/os/scopes.py +469 -0
  198. agno/os/utils.py +514 -36
  199. agno/reasoning/anthropic.py +80 -0
  200. agno/reasoning/gemini.py +73 -0
  201. agno/reasoning/openai.py +5 -0
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +155 -32
  205. agno/run/base.py +55 -3
  206. agno/run/requirement.py +181 -0
  207. agno/run/team.py +125 -38
  208. agno/run/workflow.py +72 -18
  209. agno/session/agent.py +102 -89
  210. agno/session/summary.py +56 -15
  211. agno/session/team.py +164 -90
  212. agno/session/workflow.py +405 -40
  213. agno/table.py +10 -0
  214. agno/team/team.py +3974 -1903
  215. agno/tools/dalle.py +2 -4
  216. agno/tools/eleven_labs.py +23 -25
  217. agno/tools/exa.py +21 -16
  218. agno/tools/file.py +153 -23
  219. agno/tools/file_generation.py +16 -10
  220. agno/tools/firecrawl.py +15 -7
  221. agno/tools/function.py +193 -38
  222. agno/tools/gmail.py +238 -14
  223. agno/tools/google_drive.py +271 -0
  224. agno/tools/googlecalendar.py +36 -8
  225. agno/tools/googlesheets.py +20 -5
  226. agno/tools/jira.py +20 -0
  227. agno/tools/mcp/__init__.py +10 -0
  228. agno/tools/mcp/mcp.py +331 -0
  229. agno/tools/mcp/multi_mcp.py +347 -0
  230. agno/tools/mcp/params.py +24 -0
  231. agno/tools/mcp_toolbox.py +3 -3
  232. agno/tools/models/nebius.py +5 -5
  233. agno/tools/models_labs.py +20 -10
  234. agno/tools/nano_banana.py +151 -0
  235. agno/tools/notion.py +204 -0
  236. agno/tools/parallel.py +314 -0
  237. agno/tools/postgres.py +76 -36
  238. agno/tools/redshift.py +406 -0
  239. agno/tools/scrapegraph.py +1 -1
  240. agno/tools/shopify.py +1519 -0
  241. agno/tools/slack.py +18 -3
  242. agno/tools/spotify.py +919 -0
  243. agno/tools/tavily.py +146 -0
  244. agno/tools/toolkit.py +25 -0
  245. agno/tools/workflow.py +8 -1
  246. agno/tools/yfinance.py +12 -11
  247. agno/tracing/__init__.py +12 -0
  248. agno/tracing/exporter.py +157 -0
  249. agno/tracing/schemas.py +276 -0
  250. agno/tracing/setup.py +111 -0
  251. agno/utils/agent.py +938 -0
  252. agno/utils/cryptography.py +22 -0
  253. agno/utils/dttm.py +33 -0
  254. agno/utils/events.py +151 -3
  255. agno/utils/gemini.py +15 -5
  256. agno/utils/hooks.py +118 -4
  257. agno/utils/http.py +113 -2
  258. agno/utils/knowledge.py +12 -5
  259. agno/utils/log.py +1 -0
  260. agno/utils/mcp.py +92 -2
  261. agno/utils/media.py +187 -1
  262. agno/utils/merge_dict.py +3 -3
  263. agno/utils/message.py +60 -0
  264. agno/utils/models/ai_foundry.py +9 -2
  265. agno/utils/models/claude.py +49 -14
  266. agno/utils/models/cohere.py +9 -2
  267. agno/utils/models/llama.py +9 -2
  268. agno/utils/models/mistral.py +4 -2
  269. agno/utils/print_response/agent.py +109 -16
  270. agno/utils/print_response/team.py +223 -30
  271. agno/utils/print_response/workflow.py +251 -34
  272. agno/utils/streamlit.py +1 -1
  273. agno/utils/team.py +98 -9
  274. agno/utils/tokens.py +657 -0
  275. agno/vectordb/base.py +39 -7
  276. agno/vectordb/cassandra/cassandra.py +21 -5
  277. agno/vectordb/chroma/chromadb.py +43 -12
  278. agno/vectordb/clickhouse/clickhousedb.py +21 -5
  279. agno/vectordb/couchbase/couchbase.py +29 -5
  280. agno/vectordb/lancedb/lance_db.py +92 -181
  281. agno/vectordb/langchaindb/langchaindb.py +24 -4
  282. agno/vectordb/lightrag/lightrag.py +17 -3
  283. agno/vectordb/llamaindex/llamaindexdb.py +25 -5
  284. agno/vectordb/milvus/milvus.py +50 -37
  285. agno/vectordb/mongodb/__init__.py +7 -1
  286. agno/vectordb/mongodb/mongodb.py +36 -30
  287. agno/vectordb/pgvector/pgvector.py +201 -77
  288. agno/vectordb/pineconedb/pineconedb.py +41 -23
  289. agno/vectordb/qdrant/qdrant.py +67 -54
  290. agno/vectordb/redis/__init__.py +9 -0
  291. agno/vectordb/redis/redisdb.py +682 -0
  292. agno/vectordb/singlestore/singlestore.py +50 -29
  293. agno/vectordb/surrealdb/surrealdb.py +31 -41
  294. agno/vectordb/upstashdb/upstashdb.py +34 -6
  295. agno/vectordb/weaviate/weaviate.py +53 -14
  296. agno/workflow/__init__.py +2 -0
  297. agno/workflow/agent.py +299 -0
  298. agno/workflow/condition.py +120 -18
  299. agno/workflow/loop.py +77 -10
  300. agno/workflow/parallel.py +231 -143
  301. agno/workflow/router.py +118 -17
  302. agno/workflow/step.py +609 -170
  303. agno/workflow/steps.py +73 -6
  304. agno/workflow/types.py +96 -21
  305. agno/workflow/workflow.py +2039 -262
  306. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
  307. agno-2.3.13.dist-info/RECORD +613 -0
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -679
  310. agno/tools/memori.py +0 -339
  311. agno-2.1.2.dist-info/RECORD +0 -543
  312. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
  313. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ import asyncio
2
+ import base64
1
3
  import json
2
4
  import time
3
5
  from collections.abc import AsyncIterator
@@ -11,13 +13,16 @@ from pydantic import BaseModel
11
13
 
12
14
  from agno.exceptions import ModelProviderError
13
15
  from agno.media import Audio, File, Image, Video
14
- from agno.models.base import Model
16
+ from agno.models.base import Model, RetryableModelProviderError
17
+ from agno.models.google.utils import MALFORMED_FUNCTION_CALL_GUIDANCE, GeminiFinishReason
15
18
  from agno.models.message import Citations, Message, UrlCitation
16
19
  from agno.models.metrics import Metrics
17
20
  from agno.models.response import ModelResponse
18
21
  from agno.run.agent import RunOutput
22
+ from agno.tools.function import Function
19
23
  from agno.utils.gemini import format_function_definitions, format_image_for_message, prepare_response_schema
20
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
21
26
 
22
27
  try:
23
28
  from google import genai
@@ -26,12 +31,15 @@ try:
26
31
  from google.genai.types import (
27
32
  Content,
28
33
  DynamicRetrievalConfig,
34
+ FileSearch,
29
35
  FunctionCallingConfigMode,
30
36
  GenerateContentConfig,
31
37
  GenerateContentResponse,
32
38
  GenerateContentResponseUsageMetadata,
33
39
  GoogleSearch,
34
40
  GoogleSearchRetrieval,
41
+ GroundingMetadata,
42
+ Operation,
35
43
  Part,
36
44
  Retrieval,
37
45
  ThinkingConfig,
@@ -43,7 +51,9 @@ try:
43
51
  File as GeminiFile,
44
52
  )
45
53
  except ImportError:
46
- raise ImportError("`google-genai` not installed. Please install it using `pip install google-genai`")
54
+ raise ImportError(
55
+ "`google-genai` not installed or not at the latest version. Please install it using `pip install -U google-genai`"
56
+ )
47
57
 
48
58
 
49
59
  @dataclass
@@ -78,6 +88,10 @@ class Gemini(Model):
78
88
  vertexai_search: bool = False
79
89
  vertexai_search_datastore: Optional[str] = None
80
90
 
91
+ # Gemini File Search capabilities
92
+ file_search_store_names: Optional[List[str]] = None
93
+ file_search_metadata_filter: Optional[str] = None
94
+
81
95
  temperature: Optional[float] = None
82
96
  top_p: Optional[float] = None
83
97
  top_k: Optional[int] = None
@@ -92,6 +106,7 @@ class Gemini(Model):
92
106
  cached_content: Optional[Any] = None
93
107
  thinking_budget: Optional[int] = None # Thinking budget for Gemini 2.5 models
94
108
  include_thoughts: Optional[bool] = None # Include thought summaries in response
109
+ thinking_level: Optional[str] = None # "low", "high"
95
110
  request_params: Optional[Dict[str, Any]] = None
96
111
 
97
112
  # Client parameters
@@ -135,8 +150,14 @@ class Gemini(Model):
135
150
  else:
136
151
  log_info("Using Vertex AI API")
137
152
  client_params["vertexai"] = True
138
- client_params["project"] = self.project_id or getenv("GOOGLE_CLOUD_PROJECT")
139
- client_params["location"] = self.location or getenv("GOOGLE_CLOUD_LOCATION")
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
140
161
 
141
162
  client_params = {k: v for k, v in client_params.items() if v is not None}
142
163
 
@@ -146,6 +167,21 @@ class Gemini(Model):
146
167
  self.client = genai.Client(**client_params)
147
168
  return self.client
148
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.
172
+
173
+ Args:
174
+ builtin_tools: List of built-in tools to append to.
175
+ """
176
+ if not self.file_search_store_names:
177
+ return
178
+
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]
184
+
149
185
  def get_request_params(
150
186
  self,
151
187
  system_message: Optional[str] = None,
@@ -197,11 +233,13 @@ class Gemini(Model):
197
233
  config["response_schema"] = prepare_response_schema(response_format)
198
234
 
199
235
  # Add thinking configuration
200
- thinking_config_params = {}
236
+ thinking_config_params: Dict[str, Any] = {}
201
237
  if self.thinking_budget is not None:
202
238
  thinking_config_params["thinking_budget"] = self.thinking_budget
203
239
  if self.include_thoughts is not None:
204
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
205
243
  if thinking_config_params:
206
244
  config["thinking_config"] = ThinkingConfig(**thinking_config_params)
207
245
 
@@ -209,8 +247,8 @@ class Gemini(Model):
209
247
  builtin_tools = []
210
248
 
211
249
  if self.grounding:
212
- log_info(
213
- "Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead."
250
+ log_debug(
251
+ "Gemini Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead."
214
252
  )
215
253
  builtin_tools.append(
216
254
  Tool(
@@ -223,15 +261,15 @@ class Gemini(Model):
223
261
  )
224
262
 
225
263
  if self.search:
226
- log_info("Google Search enabled.")
264
+ log_debug("Gemini Google Search enabled.")
227
265
  builtin_tools.append(Tool(google_search=GoogleSearch()))
228
266
 
229
267
  if self.url_context:
230
- log_info("URL context enabled.")
268
+ log_debug("Gemini URL context enabled.")
231
269
  builtin_tools.append(Tool(url_context=UrlContext()))
232
270
 
233
271
  if self.vertexai_search:
234
- log_info("Vertex AI Search enabled.")
272
+ log_debug("Gemini Vertex AI Search enabled.")
235
273
  if not self.vertexai_search_datastore:
236
274
  log_error("vertexai_search_datastore must be provided when vertexai_search is enabled.")
237
275
  raise ValueError("vertexai_search_datastore must be provided when vertexai_search is enabled.")
@@ -239,6 +277,8 @@ class Gemini(Model):
239
277
  Tool(retrieval=Retrieval(vertex_ai_search=VertexAISearch(datastore=self.vertexai_search_datastore)))
240
278
  )
241
279
 
280
+ self._append_file_search_tool(builtin_tools)
281
+
242
282
  # Set tools in config
243
283
  if builtin_tools:
244
284
  if tools:
@@ -272,6 +312,113 @@ class Gemini(Model):
272
312
  log_debug(f"Calling {self.provider} with request parameters: {request_params}", log_level=2)
273
313
  return request_params
274
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)
355
+
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)
360
+
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
367
+
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
396
+ else:
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)
407
+
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)
412
+
413
+ # Add estimated tokens for tools
414
+ if tools:
415
+ total += count_tool_tokens(tools, self.id)
416
+
417
+ # Add estimated tokens for response_format/output_schema
418
+ total += schema_tokens
419
+
420
+ return total
421
+
275
422
  def invoke(
276
423
  self,
277
424
  messages: List[Message],
@@ -280,11 +427,13 @@ class Gemini(Model):
280
427
  tools: Optional[List[Dict[str, Any]]] = None,
281
428
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
282
429
  run_response: Optional[RunOutput] = None,
430
+ compress_tool_results: bool = False,
431
+ retry_with_guidance: bool = False,
283
432
  ) -> ModelResponse:
284
433
  """
285
434
  Invokes the model with a list of messages and returns the response.
286
435
  """
287
- formatted_messages, system_message = self._format_messages(messages)
436
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
288
437
  request_kwargs = self.get_request_params(
289
438
  system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
290
439
  )
@@ -300,7 +449,13 @@ class Gemini(Model):
300
449
  )
301
450
  assistant_message.metrics.stop_timer()
302
451
 
303
- model_response = self._parse_provider_response(provider_response, response_format=response_format)
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)
304
459
 
305
460
  return model_response
306
461
 
@@ -313,6 +468,8 @@ class Gemini(Model):
313
468
  model_name=self.name,
314
469
  model_id=self.id,
315
470
  ) from e
471
+ except RetryableModelProviderError:
472
+ raise
316
473
  except Exception as e:
317
474
  log_error(f"Unknown error from Gemini API: {e}")
318
475
  raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
@@ -325,11 +482,13 @@ class Gemini(Model):
325
482
  tools: Optional[List[Dict[str, Any]]] = None,
326
483
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
327
484
  run_response: Optional[RunOutput] = None,
485
+ compress_tool_results: bool = False,
486
+ retry_with_guidance: bool = False,
328
487
  ) -> Iterator[ModelResponse]:
329
488
  """
330
489
  Invokes the model with a list of messages and returns the response as a stream.
331
490
  """
332
- formatted_messages, system_message = self._format_messages(messages)
491
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
333
492
 
334
493
  request_kwargs = self.get_request_params(
335
494
  system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
@@ -344,7 +503,11 @@ class Gemini(Model):
344
503
  contents=formatted_messages,
345
504
  **request_kwargs,
346
505
  ):
347
- yield self._parse_provider_response_delta(response)
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)
348
511
 
349
512
  assistant_message.metrics.stop_timer()
350
513
 
@@ -356,6 +519,8 @@ class Gemini(Model):
356
519
  model_name=self.name,
357
520
  model_id=self.id,
358
521
  ) from e
522
+ except RetryableModelProviderError:
523
+ raise
359
524
  except Exception as e:
360
525
  log_error(f"Unknown error from Gemini API: {e}")
361
526
  raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
@@ -368,11 +533,13 @@ class Gemini(Model):
368
533
  tools: Optional[List[Dict[str, Any]]] = None,
369
534
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
370
535
  run_response: Optional[RunOutput] = None,
536
+ compress_tool_results: bool = False,
537
+ retry_with_guidance: bool = False,
371
538
  ) -> ModelResponse:
372
539
  """
373
540
  Invokes the model with a list of messages and returns the response.
374
541
  """
375
- formatted_messages, system_message = self._format_messages(messages)
542
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
376
543
 
377
544
  request_kwargs = self.get_request_params(
378
545
  system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
@@ -390,7 +557,13 @@ class Gemini(Model):
390
557
  )
391
558
  assistant_message.metrics.stop_timer()
392
559
 
393
- model_response = self._parse_provider_response(provider_response, response_format=response_format)
560
+ model_response = self._parse_provider_response(
561
+ provider_response, response_format=response_format, retry_with_guidance=retry_with_guidance
562
+ )
563
+
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)
394
567
 
395
568
  return model_response
396
569
 
@@ -402,6 +575,8 @@ class Gemini(Model):
402
575
  model_name=self.name,
403
576
  model_id=self.id,
404
577
  ) from e
578
+ except RetryableModelProviderError:
579
+ raise
405
580
  except Exception as e:
406
581
  log_error(f"Unknown error from Gemini API: {e}")
407
582
  raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
@@ -414,11 +589,13 @@ class Gemini(Model):
414
589
  tools: Optional[List[Dict[str, Any]]] = None,
415
590
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
416
591
  run_response: Optional[RunOutput] = None,
592
+ compress_tool_results: bool = False,
593
+ retry_with_guidance: bool = False,
417
594
  ) -> AsyncIterator[ModelResponse]:
418
595
  """
419
596
  Invokes the model with a list of messages and returns the response as a stream.
420
597
  """
421
- formatted_messages, system_message = self._format_messages(messages)
598
+ formatted_messages, system_message = self._format_messages(messages, compress_tool_results)
422
599
 
423
600
  request_kwargs = self.get_request_params(
424
601
  system_message, response_format=response_format, tools=tools, tool_choice=tool_choice
@@ -436,7 +613,11 @@ class Gemini(Model):
436
613
  **request_kwargs,
437
614
  )
438
615
  async for chunk in async_stream:
439
- yield self._parse_provider_response_delta(chunk)
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)
440
621
 
441
622
  assistant_message.metrics.stop_timer()
442
623
 
@@ -448,20 +629,24 @@ class Gemini(Model):
448
629
  model_name=self.name,
449
630
  model_id=self.id,
450
631
  ) from e
632
+ except RetryableModelProviderError:
633
+ raise
451
634
  except Exception as e:
452
635
  log_error(f"Unknown error from Gemini API: {e}")
453
636
  raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
454
637
 
455
- def _format_messages(self, messages: List[Message]):
638
+ def _format_messages(self, messages: List[Message], compress_tool_results: bool = False):
456
639
  """
457
640
  Converts a list of Message objects to the Gemini-compatible format.
458
641
 
459
642
  Args:
460
643
  messages (List[Message]): The list of messages to convert.
644
+ compress_tool_results: Whether to compress tool results.
461
645
  """
462
646
  formatted_messages: List = []
463
647
  file_content: Optional[Union[GeminiFile, Part]] = None
464
648
  system_message = None
649
+
465
650
  for message in messages:
466
651
  role = message.role
467
652
  if role in ["system", "developer"]:
@@ -472,7 +657,8 @@ class Gemini(Model):
472
657
  role = self.reverse_role_map.get(role, role)
473
658
 
474
659
  # Add content to the message for the model
475
- content = message.content
660
+ content = message.get_content(use_compressed_content=compress_tool_results)
661
+
476
662
  # Initialize message_parts to be used for Gemini
477
663
  message_parts: List[Any] = []
478
664
 
@@ -480,26 +666,47 @@ class Gemini(Model):
480
666
  if role == "model" and message.tool_calls is not None and len(message.tool_calls) > 0:
481
667
  if content is not None:
482
668
  content_str = content if isinstance(content, str) else str(content)
483
- message_parts.append(Part.from_text(text=content_str))
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)
484
673
  for tool_call in message.tool_calls:
485
- message_parts.append(
486
- Part.from_function_call(
487
- name=tool_call["function"]["name"],
488
- args=json.loads(tool_call["function"]["arguments"]),
489
- )
674
+ part = Part.from_function_call(
675
+ name=tool_call["function"]["name"],
676
+ args=json.loads(tool_call["function"]["arguments"]),
490
677
  )
678
+ if "thought_signature" in tool_call:
679
+ part.thought_signature = base64.b64decode(tool_call["thought_signature"])
680
+ message_parts.append(part)
491
681
  # Function call results
492
682
  elif message.tool_calls is not None and len(message.tool_calls) > 0:
493
- for tool_call in message.tool_calls:
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
+
494
700
  message_parts.append(
495
- Part.from_function_response(
496
- name=tool_call["tool_name"], response={"result": tool_call["content"]}
497
- )
701
+ Part.from_function_response(name=tool_call["tool_name"], response={"result": tc_content})
498
702
  )
499
703
  # Regular text content
500
704
  else:
501
705
  if isinstance(content, str):
502
- message_parts = [Part.from_text(text=content)]
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]
503
710
 
504
711
  if role == "user" and message.tool_calls is None:
505
712
  # Add images to the message for the model
@@ -759,33 +966,57 @@ class Gemini(Model):
759
966
  return None
760
967
 
761
968
  def format_function_call_results(
762
- self, messages: List[Message], function_call_results: List[Message], **kwargs
969
+ self,
970
+ messages: List[Message],
971
+ function_call_results: List[Message],
972
+ compress_tool_results: bool = False,
973
+ **kwargs,
763
974
  ) -> None:
764
975
  """
765
- Format function call results.
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.
766
983
  """
767
- combined_content: List = []
984
+ combined_original_content: List = []
768
985
  combined_function_result: List = []
986
+ tool_names: List[str] = []
987
+
769
988
  message_metrics = Metrics()
989
+
770
990
  if len(function_call_results) > 0:
771
- for result in function_call_results:
772
- combined_content.append(result.content)
773
- combined_function_result.append({"tool_name": result.tool_name, "content": result.content})
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)
774
999
  message_metrics += result.metrics
775
1000
 
776
- if combined_content:
1001
+ tool_name = ", ".join(tool_names) if tool_names else None
1002
+
1003
+ if combined_original_content:
777
1004
  messages.append(
778
1005
  Message(
779
- role="tool", content=combined_content, tool_calls=combined_function_result, metrics=message_metrics
1006
+ role="tool",
1007
+ content=combined_original_content,
1008
+ tool_name=tool_name,
1009
+ tool_calls=combined_function_result,
1010
+ metrics=message_metrics,
780
1011
  )
781
1012
  )
782
1013
 
783
1014
  def _parse_provider_response(self, response: GenerateContentResponse, **kwargs) -> ModelResponse:
784
1015
  """
785
- Parse the OpenAI response into a ModelResponse.
1016
+ Parse the Gemini response into a ModelResponse.
786
1017
 
787
1018
  Args:
788
- response: Raw response from OpenAI
1019
+ response: Raw response from Gemini
789
1020
 
790
1021
  Returns:
791
1022
  ModelResponse: Parsed response data
@@ -794,8 +1025,20 @@ class Gemini(Model):
794
1025
 
795
1026
  # Get response message
796
1027
  response_message = Content(role="model", parts=[])
797
- if response.candidates and response.candidates[0].content:
798
- response_message = response.candidates[0].content
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
799
1042
 
800
1043
  # Add role
801
1044
  if response_message.role is not None:
@@ -834,6 +1077,14 @@ class Gemini(Model):
834
1077
  else:
835
1078
  model_response.content += content_str
836
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
+
837
1088
  if hasattr(part, "inline_data") and part.inline_data is not None:
838
1089
  # Handle audio responses (for TTS models)
839
1090
  if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
@@ -865,32 +1116,33 @@ class Gemini(Model):
865
1116
  },
866
1117
  }
867
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
+
868
1123
  model_response.tool_calls.append(tool_call)
869
1124
 
870
1125
  citations = Citations()
871
1126
  citations_raw = {}
872
1127
  citations_urls = []
1128
+ web_search_queries: List[str] = []
873
1129
 
874
1130
  if response.candidates and response.candidates[0].grounding_metadata is not None:
875
- grounding_metadata = response.candidates[0].grounding_metadata.model_dump()
876
- citations_raw["grounding_metadata"] = grounding_metadata
1131
+ grounding_metadata: GroundingMetadata = response.candidates[0].grounding_metadata
1132
+ citations_raw["grounding_metadata"] = grounding_metadata.model_dump()
877
1133
 
878
- chunks = grounding_metadata.get("grounding_chunks", []) or []
879
- citation_pairs = []
1134
+ chunks = grounding_metadata.grounding_chunks or []
1135
+ web_search_queries = grounding_metadata.web_search_queries or []
880
1136
  for chunk in chunks:
881
- if not isinstance(chunk, dict):
1137
+ if not chunk:
882
1138
  continue
883
- web = chunk.get("web")
884
- if not isinstance(web, dict):
1139
+ web = chunk.web
1140
+ if not web:
885
1141
  continue
886
- uri = web.get("uri")
887
- title = web.get("title")
1142
+ uri = web.uri
1143
+ title = web.title
888
1144
  if uri:
889
- citation_pairs.append((uri, title))
890
-
891
- # Create citation objects from filtered pairs
892
- grounding_urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
893
- citations_urls.extend(grounding_urls)
1145
+ citations_urls.append(UrlCitation(url=uri, title=title))
894
1146
 
895
1147
  # Handle URLs from URL context tool
896
1148
  if (
@@ -898,22 +1150,29 @@ class Gemini(Model):
898
1150
  and hasattr(response.candidates[0], "url_context_metadata")
899
1151
  and response.candidates[0].url_context_metadata is not None
900
1152
  ):
901
- url_context_metadata = response.candidates[0].url_context_metadata.model_dump()
902
- citations_raw["url_context_metadata"] = url_context_metadata
1153
+ url_context_metadata = response.candidates[0].url_context_metadata
1154
+ citations_raw["url_context_metadata"] = url_context_metadata.model_dump()
903
1155
 
904
- url_metadata_list = url_context_metadata.get("url_metadata", [])
1156
+ url_metadata_list = url_context_metadata.url_metadata or []
905
1157
  for url_meta in url_metadata_list:
906
- retrieved_url = url_meta.get("retrieved_url")
907
- status = url_meta.get("url_retrieval_status", "UNKNOWN")
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
908
1162
  if retrieved_url and status == "URL_RETRIEVAL_STATUS_SUCCESS":
909
1163
  # Avoid duplicate URLs
910
1164
  existing_urls = [citation.url for citation in citations_urls]
911
1165
  if retrieved_url not in existing_urls:
912
1166
  citations_urls.append(UrlCitation(url=retrieved_url, title=retrieved_url))
913
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
+
914
1175
  if citations_raw or citations_urls:
915
- citations.raw = citations_raw if citations_raw else None
916
- citations.urls = citations_urls if citations_urls else None
917
1176
  model_response.citations = citations
918
1177
 
919
1178
  # Extract usage metadata if present
@@ -926,11 +1185,22 @@ class Gemini(Model):
926
1185
 
927
1186
  return model_response
928
1187
 
929
- def _parse_provider_response_delta(self, response_delta: GenerateContentResponse) -> ModelResponse:
1188
+ def _parse_provider_response_delta(self, response_delta: GenerateContentResponse, **kwargs) -> ModelResponse:
930
1189
  model_response = ModelResponse()
931
1190
 
932
1191
  if response_delta.candidates and len(response_delta.candidates) > 0:
933
- candidate_content = response_delta.candidates[0].content
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
+
934
1204
  response_message: Content = Content(role="model", parts=[])
935
1205
  if candidate_content is not None:
936
1206
  response_message = candidate_content
@@ -956,6 +1226,14 @@ class Gemini(Model):
956
1226
  else:
957
1227
  model_response.content += text_content
958
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
+
959
1237
  if hasattr(part, "inline_data") and part.inline_data is not None:
960
1238
  # Audio responses
961
1239
  if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
@@ -989,30 +1267,58 @@ class Gemini(Model):
989
1267
  },
990
1268
  }
991
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
+
992
1274
  model_response.tool_calls.append(tool_call)
993
1275
 
994
- if response_delta.candidates[0].grounding_metadata is not None:
995
- citations = Citations()
996
- grounding_metadata = response_delta.candidates[0].grounding_metadata.model_dump()
997
- citations.raw = grounding_metadata
1276
+ citations = Citations()
1277
+ citations.raw = {}
1278
+ citations.urls = []
998
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 []
999
1287
  # Extract url and title
1000
- chunks = grounding_metadata.pop("grounding_chunks", None) or []
1001
- citation_pairs = []
1288
+ chunks = grounding_metadata.grounding_chunks or []
1002
1289
  for chunk in chunks:
1003
- if not isinstance(chunk, dict):
1290
+ if not chunk:
1004
1291
  continue
1005
- web = chunk.get("web")
1006
- if not isinstance(web, dict):
1292
+ web = chunk.web
1293
+ if not web:
1007
1294
  continue
1008
- uri = web.get("uri")
1009
- title = web.get("title")
1295
+ uri = web.uri
1296
+ title = web.title
1010
1297
  if uri:
1011
- citation_pairs.append((uri, title))
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()
1012
1308
 
1013
- # Create citation objects from filtered pairs
1014
- citations.urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
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))
1015
1320
 
1321
+ if citations.raw or citations.urls:
1016
1322
  model_response.citations = citations
1017
1323
 
1018
1324
  # Extract usage metadata if present
@@ -1083,3 +1389,494 @@ class Gemini(Model):
1083
1389
  metrics.provider_metrics = {"traffic_type": response_usage.traffic_type}
1084
1390
 
1085
1391
  return metrics
1392
+
1393
+ def create_file_search_store(self, display_name: Optional[str] = None) -> Any:
1394
+ """
1395
+ Create a new File Search store.
1396
+
1397
+ Args:
1398
+ display_name: Optional display name for the store
1399
+
1400
+ Returns:
1401
+ FileSearchStore: The created File Search store object
1402
+ """
1403
+ config: Dict[str, Any] = {}
1404
+ if display_name:
1405
+ config["display_name"] = display_name
1406
+
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
1414
+
1415
+ async def async_create_file_search_store(self, display_name: Optional[str] = None) -> Any:
1416
+ """
1417
+ Args:
1418
+ display_name: Optional display name for the store
1419
+
1420
+ Returns:
1421
+ FileSearchStore: The created File Search store object
1422
+ """
1423
+ config: Dict[str, Any] = {}
1424
+ if display_name:
1425
+ config["display_name"] = display_name
1426
+
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
1434
+
1435
+ def list_file_search_stores(self, page_size: int = 100) -> List[Any]:
1436
+ """
1437
+ List all File Search stores.
1438
+
1439
+ Args:
1440
+ page_size: Maximum number of stores to return per page
1441
+
1442
+ Returns:
1443
+ List: List of FileSearchStore objects
1444
+ """
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
1454
+
1455
+ async def async_list_file_search_stores(self, page_size: int = 100) -> List[Any]:
1456
+ """
1457
+ Async version of list_file_search_stores.
1458
+
1459
+ Args:
1460
+ page_size: Maximum number of stores to return per page
1461
+
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
1474
+
1475
+ def get_file_search_store(self, name: str) -> Any:
1476
+ """
1477
+ Get a specific File Search store by name.
1478
+
1479
+ Args:
1480
+ name: The name of the store (e.g., 'fileSearchStores/my-store-123')
1481
+
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
1492
+
1493
+ async def async_get_file_search_store(self, name: str) -> Any:
1494
+ """
1495
+ Args:
1496
+ name: The name of the store
1497
+
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:
1510
+ """
1511
+ Delete a File Search store.
1512
+
1513
+ Args:
1514
+ name: The name of the store to delete
1515
+ force: If True, force delete even if store contains documents
1516
+ """
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
1523
+
1524
+ async def async_delete_file_search_store(self, name: str, force: bool = True) -> None:
1525
+ """
1526
+ Async version of delete_file_search_store.
1527
+
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
1538
+
1539
+ def wait_for_operation(self, operation: Operation, poll_interval: int = 5, max_wait: int = 600) -> Operation:
1540
+ """
1541
+ Wait for a long-running operation to complete.
1542
+
1543
+ Args:
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
1547
+
1548
+ Returns:
1549
+ Operation: The completed operation object
1550
+
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.
1571
+
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
1576
+
1577
+ Returns:
1578
+ Operation: The completed operation object
1579
+ """
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.
1602
+
1603
+ Args:
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
+ ]
1619
+
1620
+ Returns:
1621
+ Operation: Long-running operation object. Use wait_for_operation() to wait for completion.
1622
+ """
1623
+ file_path = file_path if isinstance(file_path, Path) else Path(file_path)
1624
+
1625
+ if not file_path.exists():
1626
+ raise FileNotFoundError(f"File not found: {file_path}")
1627
+
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
1635
+
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
1648
+
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
1664
+
1665
+ Returns:
1666
+ Operation: Long-running operation object
1667
+ """
1668
+ file_path = file_path if isinstance(file_path, Path) else Path(file_path)
1669
+
1670
+ if not file_path.exists():
1671
+ raise FileNotFoundError(f"File not found: {file_path}")
1672
+
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
1680
+
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
1693
+
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:
1701
+ """
1702
+ Import an existing uploaded file (via Files API) into a File Search store.
1703
+
1704
+ Args:
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
1709
+
1710
+ Returns:
1711
+ Operation: Long-running operation object. Use wait_for_operation() to wait for completion.
1712
+ """
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
1718
+
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
1731
+
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
1745
+
1746
+ Returns:
1747
+ Operation: Long-running operation object
1748
+ """
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
1767
+
1768
+ def list_documents(self, store_name: str, page_size: int = 20) -> List[Any]:
1769
+ """
1770
+ Args:
1771
+ store_name: Name of the File Search store
1772
+ page_size: Maximum number of documents to return per page
1773
+
1774
+ Returns:
1775
+ List: List of document objects
1776
+ """
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
1788
+
1789
+ async def async_list_documents(self, store_name: str, page_size: int = 20) -> List[Any]:
1790
+ """
1791
+ Async version of list_documents.
1792
+
1793
+ Args:
1794
+ store_name: Name of the File Search store
1795
+ page_size: Maximum number of documents to return per page
1796
+
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
1812
+
1813
+ def get_document(self, document_name: str) -> Any:
1814
+ """
1815
+ Get a specific document by name.
1816
+
1817
+ Args:
1818
+ document_name: Full name of the document
1819
+ (e.g., 'fileSearchStores/store-123/documents/doc-456')
1820
+
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
1831
+
1832
+ async def async_get_document(self, document_name: str) -> Any:
1833
+ """
1834
+ Async version of get_document.
1835
+
1836
+ Args:
1837
+ document_name: Full name of the document
1838
+
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
1849
+
1850
+ def delete_document(self, document_name: str) -> None:
1851
+ """
1852
+ Delete a document from a File Search store.
1853
+
1854
+ Args:
1855
+ document_name: Full name of the document to delete
1856
+
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
1869
+
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