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.
- agno/agent/agent.py +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/models/google/gemini.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
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
|
|
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",
|
|
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
|
|
1016
|
+
Parse the Gemini response into a ModelResponse.
|
|
786
1017
|
|
|
787
1018
|
Args:
|
|
788
|
-
response: Raw response from
|
|
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
|
|
798
|
-
|
|
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
|
|
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.
|
|
879
|
-
|
|
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
|
|
1137
|
+
if not chunk:
|
|
882
1138
|
continue
|
|
883
|
-
web = chunk.
|
|
884
|
-
if not
|
|
1139
|
+
web = chunk.web
|
|
1140
|
+
if not web:
|
|
885
1141
|
continue
|
|
886
|
-
uri = web.
|
|
887
|
-
title = web.
|
|
1142
|
+
uri = web.uri
|
|
1143
|
+
title = web.title
|
|
888
1144
|
if uri:
|
|
889
|
-
|
|
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
|
|
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.
|
|
1156
|
+
url_metadata_list = url_context_metadata.url_metadata or []
|
|
905
1157
|
for url_meta in url_metadata_list:
|
|
906
|
-
retrieved_url = url_meta.
|
|
907
|
-
status =
|
|
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
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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.
|
|
1001
|
-
citation_pairs = []
|
|
1288
|
+
chunks = grounding_metadata.grounding_chunks or []
|
|
1002
1289
|
for chunk in chunks:
|
|
1003
|
-
if not
|
|
1290
|
+
if not chunk:
|
|
1004
1291
|
continue
|
|
1005
|
-
web = chunk.
|
|
1006
|
-
if not
|
|
1292
|
+
web = chunk.web
|
|
1293
|
+
if not web:
|
|
1007
1294
|
continue
|
|
1008
|
-
uri = web.
|
|
1009
|
-
title = web.
|
|
1295
|
+
uri = web.uri
|
|
1296
|
+
title = web.title
|
|
1010
1297
|
if uri:
|
|
1011
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
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
|