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