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