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/base.py
CHANGED
|
@@ -1,77 +1,114 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import collections.abc
|
|
3
|
+
import json
|
|
2
4
|
from abc import ABC, abstractmethod
|
|
3
5
|
from dataclasses import dataclass, field
|
|
6
|
+
from hashlib import md5
|
|
4
7
|
from pathlib import Path
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
from time import sleep, time
|
|
9
|
+
from types import AsyncGeneratorType, GeneratorType
|
|
10
|
+
from typing import (
|
|
11
|
+
TYPE_CHECKING,
|
|
12
|
+
Any,
|
|
13
|
+
AsyncIterator,
|
|
14
|
+
Dict,
|
|
15
|
+
Iterator,
|
|
16
|
+
List,
|
|
17
|
+
Literal,
|
|
18
|
+
Optional,
|
|
19
|
+
Sequence,
|
|
20
|
+
Tuple,
|
|
21
|
+
Type,
|
|
22
|
+
Union,
|
|
23
|
+
get_args,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from agno.compression.manager import CompressionManager
|
|
28
|
+
from uuid import uuid4
|
|
29
|
+
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
|
|
32
|
+
from agno.exceptions import AgentRunException, ModelProviderError, RetryableModelProviderError
|
|
33
|
+
from agno.media import Audio, File, Image, Video
|
|
34
|
+
from agno.models.message import Citations, Message
|
|
35
|
+
from agno.models.metrics import Metrics
|
|
36
|
+
from agno.models.response import ModelResponse, ModelResponseEvent, ToolExecution
|
|
37
|
+
from agno.run.agent import CustomEvent, RunContentEvent, RunOutput, RunOutputEvent
|
|
38
|
+
from agno.run.requirement import RunRequirement
|
|
39
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
40
|
+
from agno.run.team import TeamRunOutput, TeamRunOutputEvent
|
|
41
|
+
from agno.run.workflow import WorkflowRunOutputEvent
|
|
42
|
+
from agno.tools.function import Function, FunctionCall, FunctionExecutionResult, UserInputField
|
|
43
|
+
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
15
44
|
from agno.utils.timer import Timer
|
|
16
|
-
from agno.utils.tools import get_function_call_for_tool_call
|
|
45
|
+
from agno.utils.tools import get_function_call_for_tool_call, get_function_call_for_tool_execution
|
|
17
46
|
|
|
18
47
|
|
|
19
48
|
@dataclass
|
|
20
|
-
class
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
class MessageData:
|
|
50
|
+
response_role: Optional[Literal["system", "user", "assistant", "tool"]] = None
|
|
51
|
+
response_content: Any = ""
|
|
52
|
+
response_reasoning_content: Any = ""
|
|
53
|
+
response_redacted_reasoning_content: Any = ""
|
|
54
|
+
response_citations: Optional[Citations] = None
|
|
55
|
+
response_tool_calls: List[Dict[str, Any]] = field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
response_audio: Optional[Audio] = None
|
|
58
|
+
response_image: Optional[Image] = None
|
|
59
|
+
response_video: Optional[Video] = None
|
|
60
|
+
response_file: Optional[File] = None
|
|
61
|
+
|
|
62
|
+
response_metrics: Optional[Metrics] = None
|
|
63
|
+
|
|
64
|
+
# Data from the provider that we might need on subsequent messages
|
|
65
|
+
response_provider_data: Optional[Dict[str, Any]] = None
|
|
66
|
+
|
|
67
|
+
extra: Optional[Dict[str, Any]] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _log_messages(messages: List[Message]) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Log messages for debugging.
|
|
73
|
+
"""
|
|
74
|
+
for m in messages:
|
|
75
|
+
# Don't log metrics for input messages
|
|
76
|
+
m.log(metrics=False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _handle_agent_exception(a_exc: AgentRunException, additional_input: Optional[List[Message]] = None) -> None:
|
|
80
|
+
"""Handle AgentRunException and collect additional messages."""
|
|
81
|
+
if additional_input is None:
|
|
82
|
+
additional_input = []
|
|
83
|
+
if a_exc.user_message is not None:
|
|
84
|
+
msg = (
|
|
85
|
+
Message(role="user", content=a_exc.user_message)
|
|
86
|
+
if isinstance(a_exc.user_message, str)
|
|
87
|
+
else a_exc.user_message
|
|
57
88
|
)
|
|
58
|
-
|
|
59
|
-
metric_lines.append(f"* Prompt tokens details: {self.prompt_tokens_details}")
|
|
60
|
-
if self.completion_tokens_details is not None:
|
|
61
|
-
metric_lines.append(f"* Completion tokens details: {self.completion_tokens_details}")
|
|
62
|
-
self._log(metric_lines=metric_lines)
|
|
89
|
+
additional_input.append(msg)
|
|
63
90
|
|
|
91
|
+
if a_exc.agent_message is not None:
|
|
92
|
+
msg = (
|
|
93
|
+
Message(role="assistant", content=a_exc.agent_message)
|
|
94
|
+
if isinstance(a_exc.agent_message, str)
|
|
95
|
+
else a_exc.agent_message
|
|
96
|
+
)
|
|
97
|
+
additional_input.append(msg)
|
|
64
98
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
if a_exc.messages:
|
|
100
|
+
for m in a_exc.messages:
|
|
101
|
+
if isinstance(m, Message):
|
|
102
|
+
additional_input.append(m)
|
|
103
|
+
elif isinstance(m, dict):
|
|
104
|
+
try:
|
|
105
|
+
additional_input.append(Message(**m))
|
|
106
|
+
except Exception as e:
|
|
107
|
+
log_warning(f"Failed to convert dict to Message: {e}")
|
|
108
|
+
|
|
109
|
+
if a_exc.stop_execution:
|
|
110
|
+
for m in additional_input:
|
|
111
|
+
m.stop_after_tool_call = True
|
|
75
112
|
|
|
76
113
|
|
|
77
114
|
@dataclass
|
|
@@ -82,16 +119,14 @@ class Model(ABC):
|
|
|
82
119
|
name: Optional[str] = None
|
|
83
120
|
# Provider for this Model. This is not sent to the Model API.
|
|
84
121
|
provider: Optional[str] = None
|
|
85
|
-
# Metrics collected for this Model. This is not sent to the Model API.
|
|
86
|
-
metrics: Dict[str, Any] = field(default_factory=dict)
|
|
87
|
-
# Used for structured_outputs
|
|
88
|
-
response_format: Optional[Any] = None
|
|
89
122
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
|
|
123
|
+
# -*- Do not set the following attributes directly -*-
|
|
124
|
+
# -*- Set them on the Agent instead -*-
|
|
125
|
+
|
|
126
|
+
# True if the Model supports structured outputs natively (e.g. OpenAI)
|
|
127
|
+
supports_native_structured_outputs: bool = False
|
|
128
|
+
# True if the Model requires a json_schema for structured outputs (e.g. LMStudio)
|
|
129
|
+
supports_json_schema_outputs: bool = False
|
|
95
130
|
|
|
96
131
|
# Controls which (if any) function is called by the model.
|
|
97
132
|
# "none" means the model will not call a function and instead generates a message.
|
|
@@ -99,588 +134,2472 @@ class Model(ABC):
|
|
|
99
134
|
# Specifying a particular function via {"type: "function", "function": {"name": "my_function"}}
|
|
100
135
|
# forces the model to call that function.
|
|
101
136
|
# "none" is the default when no functions are present. "auto" is the default if functions are present.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# If True, shows function calls in the response. Is not compatible with response_model
|
|
105
|
-
show_tool_calls: Optional[bool] = None
|
|
106
|
-
|
|
107
|
-
# Maximum number of tool calls allowed.
|
|
108
|
-
tool_call_limit: Optional[int] = None
|
|
109
|
-
|
|
110
|
-
# -*- Functions available to the Model to call -*-
|
|
111
|
-
# Functions extracted from the tools.
|
|
112
|
-
# Note: These are not sent to the Model API and are only used for execution + deduplication.
|
|
113
|
-
_functions: Optional[Dict[str, Function]] = None
|
|
114
|
-
# Function call stack.
|
|
115
|
-
_function_call_stack: Optional[List[FunctionCall]] = None
|
|
137
|
+
_tool_choice: Optional[Union[str, Dict[str, Any]]] = None
|
|
116
138
|
|
|
117
139
|
# System prompt from the model added to the Agent.
|
|
118
140
|
system_prompt: Optional[str] = None
|
|
119
141
|
# Instructions from the model added to the Agent.
|
|
120
142
|
instructions: Optional[List[str]] = None
|
|
121
143
|
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
# The role of the tool message.
|
|
145
|
+
tool_message_role: str = "tool"
|
|
146
|
+
# The role of the assistant message.
|
|
147
|
+
assistant_message_role: str = "assistant"
|
|
148
|
+
|
|
149
|
+
# Cache model responses to avoid redundant API calls during development
|
|
150
|
+
cache_response: bool = False
|
|
151
|
+
cache_ttl: Optional[int] = None
|
|
152
|
+
cache_dir: Optional[str] = None
|
|
153
|
+
|
|
154
|
+
# Retry configuration for model provider errors
|
|
155
|
+
# Number of retries to attempt when a ModelProviderError occurs
|
|
156
|
+
retries: int = 0
|
|
157
|
+
# Delay between retries (in seconds)
|
|
158
|
+
delay_between_retries: int = 1
|
|
159
|
+
# Exponential backoff: if True, the delay between retries is doubled each time
|
|
160
|
+
exponential_backoff: bool = False
|
|
161
|
+
# Enable retrying a model invocation once with a guidance message.
|
|
162
|
+
# This is useful for known errors avoidable with extra instructions.
|
|
163
|
+
retry_with_guidance: bool = True
|
|
164
|
+
# Set the number of times to retry the model invocation with guidance.
|
|
165
|
+
retry_with_guidance_limit: int = 1
|
|
132
166
|
|
|
133
167
|
def __post_init__(self):
|
|
134
168
|
if self.provider is None and self.name is not None:
|
|
135
169
|
self.provider = f"{self.name} ({self.id})"
|
|
136
170
|
|
|
171
|
+
def _get_retry_delay(self, attempt: int) -> float:
|
|
172
|
+
"""Calculate the delay before the next retry attempt."""
|
|
173
|
+
if self.exponential_backoff:
|
|
174
|
+
return self.delay_between_retries * (2**attempt)
|
|
175
|
+
return self.delay_between_retries
|
|
176
|
+
|
|
177
|
+
def _invoke_with_retry(self, **kwargs) -> ModelResponse:
|
|
178
|
+
"""
|
|
179
|
+
Invoke the model with retry logic for ModelProviderError.
|
|
180
|
+
|
|
181
|
+
This method wraps the invoke() call and retries on ModelProviderError
|
|
182
|
+
with optional exponential backoff.
|
|
183
|
+
"""
|
|
184
|
+
last_exception: Optional[ModelProviderError] = None
|
|
185
|
+
|
|
186
|
+
for attempt in range(self.retries + 1):
|
|
187
|
+
try:
|
|
188
|
+
retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
|
|
189
|
+
return self.invoke(**kwargs)
|
|
190
|
+
except ModelProviderError as e:
|
|
191
|
+
last_exception = e
|
|
192
|
+
if attempt < self.retries:
|
|
193
|
+
delay = self._get_retry_delay(attempt)
|
|
194
|
+
log_warning(
|
|
195
|
+
f"Model provider error (attempt {attempt + 1}/{self.retries + 1}): {e}. Retrying in {delay}s..."
|
|
196
|
+
)
|
|
197
|
+
sleep(delay)
|
|
198
|
+
else:
|
|
199
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
200
|
+
except RetryableModelProviderError as e:
|
|
201
|
+
current_count = retries_with_guidance_count
|
|
202
|
+
if current_count >= self.retry_with_guidance_limit:
|
|
203
|
+
raise ModelProviderError(
|
|
204
|
+
message=f"Max retries with guidance reached. Error: {e.original_error}",
|
|
205
|
+
model_name=self.name,
|
|
206
|
+
model_id=self.id,
|
|
207
|
+
)
|
|
208
|
+
kwargs.pop("retry_with_guidance", None)
|
|
209
|
+
kwargs["retries_with_guidance_count"] = current_count + 1
|
|
210
|
+
|
|
211
|
+
# Append the guidance message to help the model avoid the error in the next invoke.
|
|
212
|
+
kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
|
|
213
|
+
|
|
214
|
+
return self._invoke_with_retry(**kwargs, retry_with_guidance=True)
|
|
215
|
+
|
|
216
|
+
# If we've exhausted all retries, raise the last exception
|
|
217
|
+
raise last_exception # type: ignore
|
|
218
|
+
|
|
219
|
+
async def _ainvoke_with_retry(self, **kwargs) -> ModelResponse:
|
|
220
|
+
"""
|
|
221
|
+
Asynchronously invoke the model with retry logic for ModelProviderError.
|
|
222
|
+
|
|
223
|
+
This method wraps the ainvoke() call and retries on ModelProviderError
|
|
224
|
+
with optional exponential backoff.
|
|
225
|
+
"""
|
|
226
|
+
last_exception: Optional[ModelProviderError] = None
|
|
227
|
+
|
|
228
|
+
for attempt in range(self.retries + 1):
|
|
229
|
+
try:
|
|
230
|
+
retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
|
|
231
|
+
return await self.ainvoke(**kwargs)
|
|
232
|
+
except ModelProviderError as e:
|
|
233
|
+
last_exception = e
|
|
234
|
+
if attempt < self.retries:
|
|
235
|
+
delay = self._get_retry_delay(attempt)
|
|
236
|
+
log_warning(
|
|
237
|
+
f"Model provider error (attempt {attempt + 1}/{self.retries + 1}): {e}. Retrying in {delay}s..."
|
|
238
|
+
)
|
|
239
|
+
await asyncio.sleep(delay)
|
|
240
|
+
else:
|
|
241
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
242
|
+
except RetryableModelProviderError as e:
|
|
243
|
+
current_count = retries_with_guidance_count
|
|
244
|
+
if current_count >= self.retry_with_guidance_limit:
|
|
245
|
+
raise ModelProviderError(
|
|
246
|
+
message=f"Max retries with guidance reached. Error: {e.original_error}",
|
|
247
|
+
model_name=self.name,
|
|
248
|
+
model_id=self.id,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
kwargs.pop("retry_with_guidance", None)
|
|
252
|
+
kwargs["retries_with_guidance_count"] = current_count + 1
|
|
253
|
+
|
|
254
|
+
# Append the guidance message to help the model avoid the error in the next invoke.
|
|
255
|
+
kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
|
|
256
|
+
|
|
257
|
+
return await self._ainvoke_with_retry(**kwargs, retry_with_guidance=True)
|
|
258
|
+
|
|
259
|
+
# If we've exhausted all retries, raise the last exception
|
|
260
|
+
raise last_exception # type: ignore
|
|
261
|
+
|
|
262
|
+
def _invoke_stream_with_retry(self, **kwargs) -> Iterator[ModelResponse]:
|
|
263
|
+
"""
|
|
264
|
+
Invoke the model stream with retry logic for ModelProviderError.
|
|
265
|
+
|
|
266
|
+
This method wraps the invoke_stream() call and retries on ModelProviderError
|
|
267
|
+
with optional exponential backoff. Note that retries restart the entire stream.
|
|
268
|
+
"""
|
|
269
|
+
last_exception: Optional[ModelProviderError] = None
|
|
270
|
+
|
|
271
|
+
for attempt in range(self.retries + 1):
|
|
272
|
+
try:
|
|
273
|
+
retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
|
|
274
|
+
yield from self.invoke_stream(**kwargs)
|
|
275
|
+
return # Success, exit the retry loop
|
|
276
|
+
except ModelProviderError as e:
|
|
277
|
+
last_exception = e
|
|
278
|
+
if attempt < self.retries:
|
|
279
|
+
delay = self._get_retry_delay(attempt)
|
|
280
|
+
log_warning(
|
|
281
|
+
f"Model provider error during stream (attempt {attempt + 1}/{self.retries + 1}): {e}. "
|
|
282
|
+
f"Retrying in {delay}s..."
|
|
283
|
+
)
|
|
284
|
+
sleep(delay)
|
|
285
|
+
else:
|
|
286
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
287
|
+
except RetryableModelProviderError as e:
|
|
288
|
+
current_count = retries_with_guidance_count
|
|
289
|
+
if current_count >= self.retry_with_guidance_limit:
|
|
290
|
+
raise ModelProviderError(
|
|
291
|
+
message=f"Max retries with guidance reached. Error: {e.original_error}",
|
|
292
|
+
model_name=self.name,
|
|
293
|
+
model_id=self.id,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
kwargs.pop("retry_with_guidance", None)
|
|
297
|
+
kwargs["retries_with_guidance_count"] = current_count + 1
|
|
298
|
+
|
|
299
|
+
# Append the guidance message to help the model avoid the error in the next invoke.
|
|
300
|
+
kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
|
|
301
|
+
|
|
302
|
+
yield from self._invoke_stream_with_retry(**kwargs, retry_with_guidance=True)
|
|
303
|
+
return # Success, exit after regeneration
|
|
304
|
+
|
|
305
|
+
# If we've exhausted all retries, raise the last exception
|
|
306
|
+
raise last_exception # type: ignore
|
|
307
|
+
|
|
308
|
+
async def _ainvoke_stream_with_retry(self, **kwargs) -> AsyncIterator[ModelResponse]:
|
|
309
|
+
"""
|
|
310
|
+
Asynchronously invoke the model stream with retry logic for ModelProviderError.
|
|
311
|
+
|
|
312
|
+
This method wraps the ainvoke_stream() call and retries on ModelProviderError
|
|
313
|
+
with optional exponential backoff. Note that retries restart the entire stream.
|
|
314
|
+
"""
|
|
315
|
+
last_exception: Optional[ModelProviderError] = None
|
|
316
|
+
|
|
317
|
+
for attempt in range(self.retries + 1):
|
|
318
|
+
try:
|
|
319
|
+
retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
|
|
320
|
+
async for response in self.ainvoke_stream(**kwargs):
|
|
321
|
+
yield response
|
|
322
|
+
return # Success, exit the retry loop
|
|
323
|
+
except ModelProviderError as e:
|
|
324
|
+
last_exception = e
|
|
325
|
+
if attempt < self.retries:
|
|
326
|
+
delay = self._get_retry_delay(attempt)
|
|
327
|
+
log_warning(
|
|
328
|
+
f"Model provider error during stream (attempt {attempt + 1}/{self.retries + 1}): {e}. "
|
|
329
|
+
f"Retrying in {delay}s..."
|
|
330
|
+
)
|
|
331
|
+
await asyncio.sleep(delay)
|
|
332
|
+
else:
|
|
333
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
334
|
+
except RetryableModelProviderError as e:
|
|
335
|
+
current_count = retries_with_guidance_count
|
|
336
|
+
if current_count >= self.retry_with_guidance_limit:
|
|
337
|
+
raise ModelProviderError(
|
|
338
|
+
message=f"Max retries with guidance reached. Error: {e.original_error}",
|
|
339
|
+
model_name=self.name,
|
|
340
|
+
model_id=self.id,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
kwargs.pop("retry_with_guidance", None)
|
|
344
|
+
kwargs["retries_with_guidance_count"] = current_count + 1
|
|
345
|
+
|
|
346
|
+
# Append the guidance message to help the model avoid the error in the next invoke.
|
|
347
|
+
kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
|
|
348
|
+
|
|
349
|
+
async for response in self._ainvoke_stream_with_retry(**kwargs, retry_with_guidance=True):
|
|
350
|
+
yield response
|
|
351
|
+
return # Success, exit after regeneration
|
|
352
|
+
|
|
353
|
+
# If we've exhausted all retries, raise the last exception
|
|
354
|
+
raise last_exception # type: ignore
|
|
355
|
+
|
|
137
356
|
def to_dict(self) -> Dict[str, Any]:
|
|
138
|
-
fields = {"name", "id", "provider"
|
|
357
|
+
fields = {"name", "id", "provider"}
|
|
139
358
|
_dict = {field: getattr(self, field) for field in fields if getattr(self, field) is not None}
|
|
140
|
-
# Add functions if they exist
|
|
141
|
-
if self._functions:
|
|
142
|
-
_dict["functions"] = {k: v.to_dict() for k, v in self._functions.items()}
|
|
143
|
-
_dict["tool_call_limit"] = self.tool_call_limit
|
|
144
359
|
return _dict
|
|
145
360
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
pass
|
|
361
|
+
def _remove_temporary_messages(self, messages: List[Message]) -> None:
|
|
362
|
+
"""Remove temporary messages from the given list.
|
|
149
363
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
364
|
+
Args:
|
|
365
|
+
messages: The list of messages to filter (modified in place).
|
|
366
|
+
"""
|
|
367
|
+
messages[:] = [m for m in messages if not m.temporary]
|
|
368
|
+
|
|
369
|
+
def get_provider(self) -> str:
|
|
370
|
+
return self.provider or self.name or self.__class__.__name__
|
|
371
|
+
|
|
372
|
+
def _get_model_cache_key(self, messages: List[Message], stream: bool, **kwargs: Any) -> str:
|
|
373
|
+
"""Generate a cache key based on model messages and core parameters."""
|
|
374
|
+
message_data = []
|
|
375
|
+
for msg in messages:
|
|
376
|
+
msg_dict = {
|
|
377
|
+
"role": msg.role,
|
|
378
|
+
"content": msg.content,
|
|
379
|
+
}
|
|
380
|
+
message_data.append(msg_dict)
|
|
381
|
+
|
|
382
|
+
# Include tools parameter in cache key
|
|
383
|
+
has_tools = bool(kwargs.get("tools"))
|
|
384
|
+
|
|
385
|
+
cache_data = {
|
|
386
|
+
"model_id": self.id,
|
|
387
|
+
"messages": message_data,
|
|
388
|
+
"has_tools": has_tools,
|
|
389
|
+
"response_format": kwargs.get("response_format"),
|
|
390
|
+
"stream": stream,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
cache_str = json.dumps(cache_data, sort_keys=True)
|
|
394
|
+
return md5(cache_str.encode()).hexdigest()
|
|
395
|
+
|
|
396
|
+
def _get_model_cache_file_path(self, cache_key: str) -> Path:
|
|
397
|
+
"""Get the file path for a cache key."""
|
|
398
|
+
if self.cache_dir:
|
|
399
|
+
cache_dir = Path(self.cache_dir)
|
|
400
|
+
else:
|
|
401
|
+
cache_dir = Path.home() / ".agno" / "cache" / "model_responses"
|
|
402
|
+
|
|
403
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
404
|
+
return cache_dir / f"{cache_key}.json"
|
|
405
|
+
|
|
406
|
+
def _get_cached_model_response(self, cache_key: str) -> Optional[Dict[str, Any]]:
|
|
407
|
+
"""Retrieve a cached response if it exists and is not expired."""
|
|
408
|
+
cache_file = self._get_model_cache_file_path(cache_key)
|
|
409
|
+
|
|
410
|
+
if not cache_file.exists():
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
with open(cache_file, "r") as f:
|
|
415
|
+
cached_data = json.load(f)
|
|
416
|
+
|
|
417
|
+
# Check TTL if set (None means no expiration)
|
|
418
|
+
if self.cache_ttl is not None:
|
|
419
|
+
if time() - cached_data["timestamp"] > self.cache_ttl:
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
return cached_data
|
|
423
|
+
except Exception:
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
def _save_model_response_to_cache(self, cache_key: str, result: ModelResponse, is_streaming: bool = False) -> None:
|
|
427
|
+
"""Save a model response to cache."""
|
|
428
|
+
try:
|
|
429
|
+
cache_file = self._get_model_cache_file_path(cache_key)
|
|
430
|
+
|
|
431
|
+
cache_data = {
|
|
432
|
+
"timestamp": int(time()),
|
|
433
|
+
"is_streaming": is_streaming,
|
|
434
|
+
"result": result.to_dict(),
|
|
435
|
+
}
|
|
436
|
+
with open(cache_file, "w") as f:
|
|
437
|
+
json.dump(cache_data, f)
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
def _save_streaming_responses_to_cache(self, cache_key: str, responses: List[ModelResponse]) -> None:
|
|
442
|
+
"""Save streaming responses to cache."""
|
|
443
|
+
cache_file = self._get_model_cache_file_path(cache_key)
|
|
444
|
+
|
|
445
|
+
cache_data = {
|
|
446
|
+
"timestamp": int(time()),
|
|
447
|
+
"is_streaming": True,
|
|
448
|
+
"streaming_responses": [r.to_dict() for r in responses],
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
with open(cache_file, "w") as f:
|
|
453
|
+
json.dump(cache_data, f)
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
def _model_response_from_cache(self, cached_data: Dict[str, Any]) -> ModelResponse:
|
|
458
|
+
"""Reconstruct a ModelResponse from cached data."""
|
|
459
|
+
return ModelResponse.from_dict(cached_data["result"])
|
|
460
|
+
|
|
461
|
+
def _streaming_responses_from_cache(self, cached_data: list) -> Iterator[ModelResponse]:
|
|
462
|
+
"""Reconstruct streaming responses from cached data."""
|
|
463
|
+
for cached_response in cached_data:
|
|
464
|
+
yield ModelResponse.from_dict(cached_response)
|
|
153
465
|
|
|
154
466
|
@abstractmethod
|
|
155
|
-
def
|
|
467
|
+
def invoke(self, *args, **kwargs) -> ModelResponse:
|
|
156
468
|
pass
|
|
157
469
|
|
|
158
470
|
@abstractmethod
|
|
159
|
-
async def
|
|
471
|
+
async def ainvoke(self, *args, **kwargs) -> ModelResponse:
|
|
160
472
|
pass
|
|
161
473
|
|
|
162
474
|
@abstractmethod
|
|
163
|
-
def
|
|
475
|
+
def invoke_stream(self, *args, **kwargs) -> Iterator[ModelResponse]:
|
|
164
476
|
pass
|
|
165
477
|
|
|
166
478
|
@abstractmethod
|
|
167
|
-
|
|
479
|
+
def ainvoke_stream(self, *args, **kwargs) -> AsyncIterator[ModelResponse]:
|
|
168
480
|
pass
|
|
169
481
|
|
|
170
482
|
@abstractmethod
|
|
171
|
-
def
|
|
483
|
+
def _parse_provider_response(self, response: Any, **kwargs) -> ModelResponse:
|
|
484
|
+
"""
|
|
485
|
+
Parse the raw response from the model provider into a ModelResponse.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
response: Raw response from the model provider
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
ModelResponse: Parsed response data
|
|
492
|
+
"""
|
|
172
493
|
pass
|
|
173
494
|
|
|
174
495
|
@abstractmethod
|
|
175
|
-
|
|
496
|
+
def _parse_provider_response_delta(self, response: Any) -> ModelResponse:
|
|
497
|
+
"""
|
|
498
|
+
Parse the streaming response from the model provider into ModelResponse objects.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
response: Raw response chunk from the model provider
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
ModelResponse: Parsed response delta
|
|
505
|
+
"""
|
|
176
506
|
pass
|
|
177
507
|
|
|
178
|
-
def
|
|
508
|
+
def _format_tools(self, tools: Optional[List[Union[Function, dict]]]) -> List[Dict[str, Any]]:
|
|
509
|
+
_tool_dicts = []
|
|
510
|
+
for tool in tools or []:
|
|
511
|
+
if isinstance(tool, Function):
|
|
512
|
+
_tool_dicts.append({"type": "function", "function": tool.to_dict()})
|
|
513
|
+
else:
|
|
514
|
+
# If a dict is passed, it is a builtin tool
|
|
515
|
+
_tool_dicts.append(tool)
|
|
516
|
+
return _tool_dicts
|
|
517
|
+
|
|
518
|
+
def count_tokens(
|
|
519
|
+
self,
|
|
520
|
+
messages: List[Message],
|
|
521
|
+
tools: Optional[Sequence[Union[Function, Dict[str, Any]]]] = None,
|
|
522
|
+
output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
523
|
+
) -> int:
|
|
524
|
+
from agno.utils.tokens import count_tokens
|
|
525
|
+
|
|
526
|
+
return count_tokens(
|
|
527
|
+
messages,
|
|
528
|
+
tools=list(tools) if tools else None,
|
|
529
|
+
model_id=self.id,
|
|
530
|
+
output_schema=output_schema,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
async def acount_tokens(
|
|
534
|
+
self,
|
|
535
|
+
messages: List[Message],
|
|
536
|
+
tools: Optional[Sequence[Union[Function, Dict[str, Any]]]] = None,
|
|
537
|
+
output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
538
|
+
) -> int:
|
|
539
|
+
return self.count_tokens(messages, tools, output_schema=output_schema)
|
|
540
|
+
|
|
541
|
+
def response(
|
|
542
|
+
self,
|
|
543
|
+
messages: List[Message],
|
|
544
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
545
|
+
tools: Optional[List[Union[Function, dict]]] = None,
|
|
546
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
547
|
+
tool_call_limit: Optional[int] = None,
|
|
548
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
549
|
+
send_media_to_model: bool = True,
|
|
550
|
+
compression_manager: Optional["CompressionManager"] = None,
|
|
551
|
+
) -> ModelResponse:
|
|
552
|
+
"""
|
|
553
|
+
Generate a response from the model.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
messages: List of messages to send to the model
|
|
557
|
+
response_format: Response format to use
|
|
558
|
+
tools: List of tools to use. This includes the original Function objects and dicts for built-in tools.
|
|
559
|
+
tool_choice: Tool choice to use
|
|
560
|
+
tool_call_limit: Tool call limit
|
|
561
|
+
run_response: Run response to use
|
|
562
|
+
send_media_to_model: Whether to send media to the model
|
|
563
|
+
"""
|
|
564
|
+
try:
|
|
565
|
+
# Check cache if enabled
|
|
566
|
+
if self.cache_response:
|
|
567
|
+
cache_key = self._get_model_cache_key(
|
|
568
|
+
messages, stream=False, response_format=response_format, tools=tools
|
|
569
|
+
)
|
|
570
|
+
cached_data = self._get_cached_model_response(cache_key)
|
|
571
|
+
|
|
572
|
+
if cached_data:
|
|
573
|
+
log_info("Cache hit for model response")
|
|
574
|
+
return self._model_response_from_cache(cached_data)
|
|
575
|
+
|
|
576
|
+
log_debug(f"{self.get_provider()} Response Start", center=True, symbol="-")
|
|
577
|
+
log_debug(f"Model: {self.id}", center=True, symbol="-")
|
|
578
|
+
|
|
579
|
+
_log_messages(messages)
|
|
580
|
+
model_response = ModelResponse()
|
|
581
|
+
|
|
582
|
+
function_call_count = 0
|
|
583
|
+
|
|
584
|
+
_tool_dicts = self._format_tools(tools) if tools is not None else []
|
|
585
|
+
_functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
|
|
586
|
+
|
|
587
|
+
_compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
|
|
588
|
+
_compression_manager = compression_manager if _compress_tool_results else None
|
|
589
|
+
|
|
590
|
+
while True:
|
|
591
|
+
# Compress tool results if compression is enabled and threshold is met
|
|
592
|
+
if _compression_manager is not None and _compression_manager.should_compress(
|
|
593
|
+
messages, tools, model=self, response_format=response_format
|
|
594
|
+
):
|
|
595
|
+
_compression_manager.compress(messages)
|
|
596
|
+
|
|
597
|
+
# Get response from model
|
|
598
|
+
assistant_message = Message(role=self.assistant_message_role)
|
|
599
|
+
self._process_model_response(
|
|
600
|
+
messages=messages,
|
|
601
|
+
assistant_message=assistant_message,
|
|
602
|
+
model_response=model_response,
|
|
603
|
+
response_format=response_format,
|
|
604
|
+
tools=_tool_dicts,
|
|
605
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
606
|
+
run_response=run_response,
|
|
607
|
+
compress_tool_results=_compress_tool_results,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Add assistant message to messages
|
|
611
|
+
messages.append(assistant_message)
|
|
612
|
+
|
|
613
|
+
# Log response and metrics
|
|
614
|
+
assistant_message.log(metrics=True, use_compressed_content=_compress_tool_results)
|
|
615
|
+
|
|
616
|
+
# Handle tool calls if present
|
|
617
|
+
if assistant_message.tool_calls:
|
|
618
|
+
# Prepare function calls
|
|
619
|
+
function_calls_to_run = self._prepare_function_calls(
|
|
620
|
+
assistant_message=assistant_message,
|
|
621
|
+
messages=messages,
|
|
622
|
+
model_response=model_response,
|
|
623
|
+
functions=_functions,
|
|
624
|
+
)
|
|
625
|
+
function_call_results: List[Message] = []
|
|
626
|
+
|
|
627
|
+
# Execute function calls
|
|
628
|
+
for function_call_response in self.run_function_calls(
|
|
629
|
+
function_calls=function_calls_to_run,
|
|
630
|
+
function_call_results=function_call_results,
|
|
631
|
+
current_function_call_count=function_call_count,
|
|
632
|
+
function_call_limit=tool_call_limit,
|
|
633
|
+
):
|
|
634
|
+
if isinstance(function_call_response, ModelResponse):
|
|
635
|
+
# The session state is updated by the function call
|
|
636
|
+
if function_call_response.updated_session_state is not None:
|
|
637
|
+
model_response.updated_session_state = function_call_response.updated_session_state
|
|
638
|
+
|
|
639
|
+
# Media artifacts are generated by the function call
|
|
640
|
+
if function_call_response.images is not None:
|
|
641
|
+
if model_response.images is None:
|
|
642
|
+
model_response.images = []
|
|
643
|
+
model_response.images.extend(function_call_response.images)
|
|
644
|
+
|
|
645
|
+
if function_call_response.audios is not None:
|
|
646
|
+
if model_response.audios is None:
|
|
647
|
+
model_response.audios = []
|
|
648
|
+
model_response.audios.extend(function_call_response.audios)
|
|
649
|
+
|
|
650
|
+
if function_call_response.videos is not None:
|
|
651
|
+
if model_response.videos is None:
|
|
652
|
+
model_response.videos = []
|
|
653
|
+
model_response.videos.extend(function_call_response.videos)
|
|
654
|
+
|
|
655
|
+
if function_call_response.files is not None:
|
|
656
|
+
if model_response.files is None:
|
|
657
|
+
model_response.files = []
|
|
658
|
+
model_response.files.extend(function_call_response.files)
|
|
659
|
+
|
|
660
|
+
if (
|
|
661
|
+
function_call_response.event
|
|
662
|
+
in [
|
|
663
|
+
ModelResponseEvent.tool_call_completed.value,
|
|
664
|
+
ModelResponseEvent.tool_call_paused.value,
|
|
665
|
+
]
|
|
666
|
+
and function_call_response.tool_executions is not None
|
|
667
|
+
):
|
|
668
|
+
# Record the tool execution in the model response
|
|
669
|
+
if model_response.tool_executions is None:
|
|
670
|
+
model_response.tool_executions = []
|
|
671
|
+
model_response.tool_executions.extend(function_call_response.tool_executions)
|
|
672
|
+
|
|
673
|
+
# If the tool is currently paused (HITL flow), add the requirement to the run response
|
|
674
|
+
if (
|
|
675
|
+
function_call_response.event == ModelResponseEvent.tool_call_paused.value
|
|
676
|
+
and run_response is not None
|
|
677
|
+
):
|
|
678
|
+
current_tool_execution = function_call_response.tool_executions[-1]
|
|
679
|
+
if run_response.requirements is None:
|
|
680
|
+
run_response.requirements = []
|
|
681
|
+
run_response.requirements.append(
|
|
682
|
+
RunRequirement(tool_execution=current_tool_execution)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
elif function_call_response.event not in [
|
|
686
|
+
ModelResponseEvent.tool_call_started.value,
|
|
687
|
+
ModelResponseEvent.tool_call_completed.value,
|
|
688
|
+
]:
|
|
689
|
+
if function_call_response.content:
|
|
690
|
+
model_response.content += function_call_response.content # type: ignore
|
|
691
|
+
|
|
692
|
+
# Add a function call for each successful execution
|
|
693
|
+
function_call_count += len(function_call_results)
|
|
694
|
+
|
|
695
|
+
# Format and add results to messages
|
|
696
|
+
self.format_function_call_results(
|
|
697
|
+
messages=messages,
|
|
698
|
+
function_call_results=function_call_results,
|
|
699
|
+
compress_tool_results=_compress_tool_results,
|
|
700
|
+
**model_response.extra or {},
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
|
|
704
|
+
# Handle function call media
|
|
705
|
+
self._handle_function_call_media(
|
|
706
|
+
messages=messages,
|
|
707
|
+
function_call_results=function_call_results,
|
|
708
|
+
send_media_to_model=send_media_to_model,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
for function_call_result in function_call_results:
|
|
712
|
+
function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
|
|
713
|
+
|
|
714
|
+
# Check if we should stop after tool calls
|
|
715
|
+
if any(m.stop_after_tool_call for m in function_call_results):
|
|
716
|
+
break
|
|
717
|
+
|
|
718
|
+
# If we have any tool calls that require confirmation, break the loop
|
|
719
|
+
if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
|
|
720
|
+
break
|
|
721
|
+
|
|
722
|
+
# If we have any tool calls that require external execution, break the loop
|
|
723
|
+
if any(tc.external_execution_required for tc in model_response.tool_executions or []):
|
|
724
|
+
break
|
|
725
|
+
|
|
726
|
+
# If we have any tool calls that require user input, break the loop
|
|
727
|
+
if any(tc.requires_user_input for tc in model_response.tool_executions or []):
|
|
728
|
+
break
|
|
729
|
+
|
|
730
|
+
# Continue loop to get next response
|
|
731
|
+
continue
|
|
732
|
+
|
|
733
|
+
# No tool calls or finished processing them
|
|
734
|
+
break
|
|
735
|
+
|
|
736
|
+
log_debug(f"{self.get_provider()} Response End", center=True, symbol="-")
|
|
737
|
+
|
|
738
|
+
# Save to cache if enabled
|
|
739
|
+
if self.cache_response:
|
|
740
|
+
self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
|
|
741
|
+
finally:
|
|
742
|
+
# Close the Gemini client
|
|
743
|
+
if self.__class__.__name__ == "Gemini" and self.client is not None: # type: ignore
|
|
744
|
+
try:
|
|
745
|
+
self.client.close() # type: ignore
|
|
746
|
+
self.client = None
|
|
747
|
+
except AttributeError:
|
|
748
|
+
log_warning(
|
|
749
|
+
"Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
|
|
750
|
+
" please upgrade Gemini to the latest version: pip install -U google-genai"
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
return model_response
|
|
754
|
+
|
|
755
|
+
async def aresponse(
|
|
756
|
+
self,
|
|
757
|
+
messages: List[Message],
|
|
758
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
759
|
+
tools: Optional[List[Union[Function, dict]]] = None,
|
|
760
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
761
|
+
tool_call_limit: Optional[int] = None,
|
|
762
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
763
|
+
send_media_to_model: bool = True,
|
|
764
|
+
compression_manager: Optional["CompressionManager"] = None,
|
|
765
|
+
) -> ModelResponse:
|
|
179
766
|
"""
|
|
180
|
-
|
|
767
|
+
Generate an asynchronous response from the model.
|
|
181
768
|
"""
|
|
182
|
-
for m in messages:
|
|
183
|
-
m.log()
|
|
184
769
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
770
|
+
try:
|
|
771
|
+
# Check cache if enabled
|
|
772
|
+
if self.cache_response:
|
|
773
|
+
cache_key = self._get_model_cache_key(
|
|
774
|
+
messages, stream=False, response_format=response_format, tools=tools
|
|
775
|
+
)
|
|
776
|
+
cached_data = self._get_cached_model_response(cache_key)
|
|
777
|
+
|
|
778
|
+
if cached_data:
|
|
779
|
+
log_info("Cache hit for model response")
|
|
780
|
+
return self._model_response_from_cache(cached_data)
|
|
781
|
+
|
|
782
|
+
log_debug(f"{self.get_provider()} Async Response Start", center=True, symbol="-")
|
|
783
|
+
log_debug(f"Model: {self.id}", center=True, symbol="-")
|
|
784
|
+
_log_messages(messages)
|
|
785
|
+
model_response = ModelResponse()
|
|
786
|
+
|
|
787
|
+
_tool_dicts = self._format_tools(tools) if tools is not None else []
|
|
788
|
+
_functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
|
|
789
|
+
|
|
790
|
+
_compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
|
|
791
|
+
_compression_manager = compression_manager if _compress_tool_results else None
|
|
792
|
+
|
|
793
|
+
function_call_count = 0
|
|
794
|
+
|
|
795
|
+
while True:
|
|
796
|
+
# Compress existing tool results BEFORE making API call to avoid context overflow
|
|
797
|
+
if _compression_manager is not None and await _compression_manager.ashould_compress(
|
|
798
|
+
messages, tools, model=self, response_format=response_format
|
|
799
|
+
):
|
|
800
|
+
await _compression_manager.acompress(messages)
|
|
801
|
+
|
|
802
|
+
# Get response from model
|
|
803
|
+
assistant_message = Message(role=self.assistant_message_role)
|
|
804
|
+
await self._aprocess_model_response(
|
|
805
|
+
messages=messages,
|
|
806
|
+
assistant_message=assistant_message,
|
|
807
|
+
model_response=model_response,
|
|
808
|
+
response_format=response_format,
|
|
809
|
+
tools=_tool_dicts,
|
|
810
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
811
|
+
run_response=run_response,
|
|
812
|
+
compress_tool_results=_compress_tool_results,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
# Add assistant message to messages
|
|
816
|
+
messages.append(assistant_message)
|
|
817
|
+
|
|
818
|
+
# Log response and metrics
|
|
819
|
+
assistant_message.log(metrics=True)
|
|
820
|
+
|
|
821
|
+
# Handle tool calls if present
|
|
822
|
+
if assistant_message.tool_calls:
|
|
823
|
+
# Prepare function calls
|
|
824
|
+
function_calls_to_run = self._prepare_function_calls(
|
|
825
|
+
assistant_message=assistant_message,
|
|
826
|
+
messages=messages,
|
|
827
|
+
model_response=model_response,
|
|
828
|
+
functions=_functions,
|
|
829
|
+
)
|
|
830
|
+
function_call_results: List[Message] = []
|
|
831
|
+
|
|
832
|
+
# Execute function calls
|
|
833
|
+
async for function_call_response in self.arun_function_calls(
|
|
834
|
+
function_calls=function_calls_to_run,
|
|
835
|
+
function_call_results=function_call_results,
|
|
836
|
+
current_function_call_count=function_call_count,
|
|
837
|
+
function_call_limit=tool_call_limit,
|
|
838
|
+
):
|
|
839
|
+
if isinstance(function_call_response, ModelResponse):
|
|
840
|
+
# The session state is updated by the function call
|
|
841
|
+
if function_call_response.updated_session_state is not None:
|
|
842
|
+
model_response.updated_session_state = function_call_response.updated_session_state
|
|
843
|
+
|
|
844
|
+
# Media artifacts are generated by the function call
|
|
845
|
+
if function_call_response.images is not None:
|
|
846
|
+
if model_response.images is None:
|
|
847
|
+
model_response.images = []
|
|
848
|
+
model_response.images.extend(function_call_response.images)
|
|
849
|
+
|
|
850
|
+
if function_call_response.audios is not None:
|
|
851
|
+
if model_response.audios is None:
|
|
852
|
+
model_response.audios = []
|
|
853
|
+
model_response.audios.extend(function_call_response.audios)
|
|
854
|
+
|
|
855
|
+
if function_call_response.videos is not None:
|
|
856
|
+
if model_response.videos is None:
|
|
857
|
+
model_response.videos = []
|
|
858
|
+
model_response.videos.extend(function_call_response.videos)
|
|
859
|
+
|
|
860
|
+
if function_call_response.files is not None:
|
|
861
|
+
if model_response.files is None:
|
|
862
|
+
model_response.files = []
|
|
863
|
+
model_response.files.extend(function_call_response.files)
|
|
864
|
+
|
|
865
|
+
if (
|
|
866
|
+
function_call_response.event
|
|
867
|
+
in [
|
|
868
|
+
ModelResponseEvent.tool_call_completed.value,
|
|
869
|
+
ModelResponseEvent.tool_call_paused.value,
|
|
870
|
+
]
|
|
871
|
+
and function_call_response.tool_executions is not None
|
|
872
|
+
):
|
|
873
|
+
if model_response.tool_executions is None:
|
|
874
|
+
model_response.tool_executions = []
|
|
875
|
+
model_response.tool_executions.extend(function_call_response.tool_executions)
|
|
876
|
+
|
|
877
|
+
# If the tool is currently paused (HITL flow), add the requirement to the run response
|
|
878
|
+
if (
|
|
879
|
+
function_call_response.event == ModelResponseEvent.tool_call_paused.value
|
|
880
|
+
and run_response is not None
|
|
881
|
+
):
|
|
882
|
+
current_tool_execution = function_call_response.tool_executions[-1]
|
|
883
|
+
if run_response.requirements is None:
|
|
884
|
+
run_response.requirements = []
|
|
885
|
+
run_response.requirements.append(
|
|
886
|
+
RunRequirement(tool_execution=current_tool_execution)
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
elif function_call_response.event not in [
|
|
890
|
+
ModelResponseEvent.tool_call_started.value,
|
|
891
|
+
ModelResponseEvent.tool_call_completed.value,
|
|
892
|
+
]:
|
|
893
|
+
if function_call_response.content:
|
|
894
|
+
model_response.content += function_call_response.content # type: ignore
|
|
895
|
+
|
|
896
|
+
# Add a function call for each successful execution
|
|
897
|
+
function_call_count += len(function_call_results)
|
|
898
|
+
|
|
899
|
+
# Format and add results to messages
|
|
900
|
+
self.format_function_call_results(
|
|
901
|
+
messages=messages,
|
|
902
|
+
function_call_results=function_call_results,
|
|
903
|
+
compress_tool_results=_compress_tool_results,
|
|
904
|
+
**model_response.extra or {},
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
|
|
908
|
+
# Handle function call media
|
|
909
|
+
self._handle_function_call_media(
|
|
910
|
+
messages=messages,
|
|
911
|
+
function_call_results=function_call_results,
|
|
912
|
+
send_media_to_model=send_media_to_model,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
for function_call_result in function_call_results:
|
|
916
|
+
function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
|
|
917
|
+
|
|
918
|
+
# Check if we should stop after tool calls
|
|
919
|
+
if any(m.stop_after_tool_call for m in function_call_results):
|
|
920
|
+
break
|
|
921
|
+
|
|
922
|
+
# If we have any tool calls that require confirmation, break the loop
|
|
923
|
+
if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
|
|
924
|
+
break
|
|
925
|
+
|
|
926
|
+
# If we have any tool calls that require external execution, break the loop
|
|
927
|
+
if any(tc.external_execution_required for tc in model_response.tool_executions or []):
|
|
928
|
+
break
|
|
929
|
+
|
|
930
|
+
# If we have any tool calls that require user input, break the loop
|
|
931
|
+
if any(tc.requires_user_input for tc in model_response.tool_executions or []):
|
|
932
|
+
break
|
|
933
|
+
|
|
934
|
+
# Continue loop to get next response
|
|
935
|
+
continue
|
|
936
|
+
|
|
937
|
+
# No tool calls or finished processing them
|
|
938
|
+
break
|
|
939
|
+
|
|
940
|
+
log_debug(f"{self.get_provider()} Async Response End", center=True, symbol="-")
|
|
196
941
|
|
|
197
|
-
|
|
942
|
+
# Save to cache if enabled
|
|
943
|
+
if self.cache_response:
|
|
944
|
+
self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
|
|
945
|
+
finally:
|
|
946
|
+
# Close the Gemini client
|
|
947
|
+
if self.__class__.__name__ == "Gemini" and self.client is not None:
|
|
948
|
+
try:
|
|
949
|
+
await self.client.aio.aclose() # type: ignore
|
|
950
|
+
self.client = None
|
|
951
|
+
except AttributeError:
|
|
952
|
+
log_warning(
|
|
953
|
+
"Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
|
|
954
|
+
" please upgrade Gemini to the latest version: pip install -U google-genai"
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
return model_response
|
|
958
|
+
|
|
959
|
+
def _process_model_response(
|
|
198
960
|
self,
|
|
199
|
-
|
|
961
|
+
messages: List[Message],
|
|
962
|
+
assistant_message: Message,
|
|
963
|
+
model_response: ModelResponse,
|
|
964
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
965
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
966
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
967
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
968
|
+
compress_tool_results: bool = False,
|
|
200
969
|
) -> None:
|
|
201
|
-
self.metrics.setdefault("response_times", []).append(metrics_for_run.response_timer.elapsed)
|
|
202
|
-
if metrics_for_run.input_tokens is not None:
|
|
203
|
-
self.metrics["input_tokens"] = self.metrics.get("input_tokens", 0) + metrics_for_run.input_tokens
|
|
204
|
-
if metrics_for_run.output_tokens is not None:
|
|
205
|
-
self.metrics["output_tokens"] = self.metrics.get("output_tokens", 0) + metrics_for_run.output_tokens
|
|
206
|
-
if metrics_for_run.total_tokens is not None:
|
|
207
|
-
self.metrics["total_tokens"] = self.metrics.get("total_tokens", 0) + metrics_for_run.total_tokens
|
|
208
|
-
if metrics_for_run.time_to_first_token is not None:
|
|
209
|
-
self.metrics.setdefault("time_to_first_token", []).append(metrics_for_run.time_to_first_token)
|
|
210
|
-
|
|
211
|
-
def _get_function_calls_to_run(
|
|
212
|
-
self, assistant_message: Message, messages: List[Message], error_response_role: str = "user"
|
|
213
|
-
) -> List[FunctionCall]:
|
|
214
970
|
"""
|
|
215
|
-
|
|
971
|
+
Process a single model response and return the assistant message and whether to continue.
|
|
972
|
+
|
|
973
|
+
Returns:
|
|
974
|
+
Tuple[Message, bool]: (assistant_message, should_continue)
|
|
975
|
+
"""
|
|
976
|
+
# Generate response with retry logic for ModelProviderError
|
|
977
|
+
provider_response = self._invoke_with_retry(
|
|
978
|
+
assistant_message=assistant_message,
|
|
979
|
+
messages=messages,
|
|
980
|
+
response_format=response_format,
|
|
981
|
+
tools=tools,
|
|
982
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
983
|
+
run_response=run_response,
|
|
984
|
+
compress_tool_results=compress_tool_results,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
# Populate the assistant message
|
|
988
|
+
self._populate_assistant_message(assistant_message=assistant_message, provider_response=provider_response)
|
|
989
|
+
|
|
990
|
+
# Update model response with assistant message content and audio
|
|
991
|
+
if assistant_message.content is not None:
|
|
992
|
+
if model_response.content is None:
|
|
993
|
+
model_response.content = assistant_message.get_content_string()
|
|
994
|
+
else:
|
|
995
|
+
model_response.content += assistant_message.get_content_string()
|
|
996
|
+
if assistant_message.reasoning_content is not None:
|
|
997
|
+
model_response.reasoning_content = assistant_message.reasoning_content
|
|
998
|
+
if assistant_message.redacted_reasoning_content is not None:
|
|
999
|
+
model_response.redacted_reasoning_content = assistant_message.redacted_reasoning_content
|
|
1000
|
+
if assistant_message.citations is not None:
|
|
1001
|
+
model_response.citations = assistant_message.citations
|
|
1002
|
+
if assistant_message.audio_output is not None:
|
|
1003
|
+
if isinstance(assistant_message.audio_output, Audio):
|
|
1004
|
+
model_response.audio = assistant_message.audio_output
|
|
1005
|
+
if assistant_message.image_output is not None:
|
|
1006
|
+
model_response.images = [assistant_message.image_output]
|
|
1007
|
+
if assistant_message.video_output is not None:
|
|
1008
|
+
model_response.videos = [assistant_message.video_output]
|
|
1009
|
+
if provider_response.extra is not None:
|
|
1010
|
+
if model_response.extra is None:
|
|
1011
|
+
model_response.extra = {}
|
|
1012
|
+
model_response.extra.update(provider_response.extra)
|
|
1013
|
+
if provider_response.provider_data is not None:
|
|
1014
|
+
model_response.provider_data = provider_response.provider_data
|
|
1015
|
+
|
|
1016
|
+
async def _aprocess_model_response(
|
|
1017
|
+
self,
|
|
1018
|
+
messages: List[Message],
|
|
1019
|
+
assistant_message: Message,
|
|
1020
|
+
model_response: ModelResponse,
|
|
1021
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
1022
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
1023
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
1024
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
1025
|
+
compress_tool_results: bool = False,
|
|
1026
|
+
) -> None:
|
|
1027
|
+
"""
|
|
1028
|
+
Process a single async model response and return the assistant message and whether to continue.
|
|
1029
|
+
|
|
1030
|
+
Returns:
|
|
1031
|
+
Tuple[Message, bool]: (assistant_message, should_continue)
|
|
1032
|
+
"""
|
|
1033
|
+
# Generate response with retry logic for ModelProviderError
|
|
1034
|
+
provider_response = await self._ainvoke_with_retry(
|
|
1035
|
+
messages=messages,
|
|
1036
|
+
response_format=response_format,
|
|
1037
|
+
tools=tools,
|
|
1038
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
1039
|
+
assistant_message=assistant_message,
|
|
1040
|
+
run_response=run_response,
|
|
1041
|
+
compress_tool_results=compress_tool_results,
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
# Populate the assistant message
|
|
1045
|
+
self._populate_assistant_message(assistant_message=assistant_message, provider_response=provider_response)
|
|
1046
|
+
|
|
1047
|
+
# Update model response with assistant message content and audio
|
|
1048
|
+
if assistant_message.content is not None:
|
|
1049
|
+
if model_response.content is None:
|
|
1050
|
+
model_response.content = assistant_message.get_content_string()
|
|
1051
|
+
else:
|
|
1052
|
+
model_response.content += assistant_message.get_content_string()
|
|
1053
|
+
if assistant_message.reasoning_content is not None:
|
|
1054
|
+
model_response.reasoning_content = assistant_message.reasoning_content
|
|
1055
|
+
if assistant_message.redacted_reasoning_content is not None:
|
|
1056
|
+
model_response.redacted_reasoning_content = assistant_message.redacted_reasoning_content
|
|
1057
|
+
if assistant_message.citations is not None:
|
|
1058
|
+
model_response.citations = assistant_message.citations
|
|
1059
|
+
if assistant_message.audio_output is not None:
|
|
1060
|
+
if isinstance(assistant_message.audio_output, Audio):
|
|
1061
|
+
model_response.audio = assistant_message.audio_output
|
|
1062
|
+
if assistant_message.image_output is not None:
|
|
1063
|
+
model_response.images = [assistant_message.image_output]
|
|
1064
|
+
if assistant_message.video_output is not None:
|
|
1065
|
+
model_response.videos = [assistant_message.video_output]
|
|
1066
|
+
if provider_response.extra is not None:
|
|
1067
|
+
if model_response.extra is None:
|
|
1068
|
+
model_response.extra = {}
|
|
1069
|
+
model_response.extra.update(provider_response.extra)
|
|
1070
|
+
if provider_response.provider_data is not None:
|
|
1071
|
+
model_response.provider_data = provider_response.provider_data
|
|
1072
|
+
|
|
1073
|
+
def _populate_assistant_message(
|
|
1074
|
+
self,
|
|
1075
|
+
assistant_message: Message,
|
|
1076
|
+
provider_response: ModelResponse,
|
|
1077
|
+
) -> Message:
|
|
1078
|
+
"""
|
|
1079
|
+
Populate an assistant message with the provider response data.
|
|
216
1080
|
|
|
217
1081
|
Args:
|
|
218
|
-
assistant_message
|
|
219
|
-
|
|
1082
|
+
assistant_message: The assistant message to populate
|
|
1083
|
+
provider_response: Parsed response from the model provider
|
|
220
1084
|
|
|
221
1085
|
Returns:
|
|
222
|
-
|
|
1086
|
+
Message: The populated assistant message
|
|
1087
|
+
"""
|
|
1088
|
+
if provider_response.role is not None:
|
|
1089
|
+
assistant_message.role = provider_response.role
|
|
1090
|
+
|
|
1091
|
+
# Add content to assistant message
|
|
1092
|
+
if provider_response.content is not None:
|
|
1093
|
+
assistant_message.content = provider_response.content
|
|
1094
|
+
|
|
1095
|
+
# Add tool calls to assistant message
|
|
1096
|
+
if provider_response.tool_calls is not None and len(provider_response.tool_calls) > 0:
|
|
1097
|
+
assistant_message.tool_calls = provider_response.tool_calls
|
|
1098
|
+
|
|
1099
|
+
# Add audio to assistant message
|
|
1100
|
+
if provider_response.audio is not None:
|
|
1101
|
+
assistant_message.audio_output = provider_response.audio
|
|
1102
|
+
|
|
1103
|
+
# Add image to assistant message
|
|
1104
|
+
if provider_response.images is not None:
|
|
1105
|
+
if provider_response.images:
|
|
1106
|
+
assistant_message.image_output = provider_response.images[-1] # Taking last (most recent) image
|
|
1107
|
+
|
|
1108
|
+
# Add video to assistant message
|
|
1109
|
+
if provider_response.videos is not None:
|
|
1110
|
+
if provider_response.videos:
|
|
1111
|
+
assistant_message.video_output = provider_response.videos[-1] # Taking last (most recent) video
|
|
1112
|
+
|
|
1113
|
+
if provider_response.files is not None:
|
|
1114
|
+
if provider_response.files:
|
|
1115
|
+
assistant_message.file_output = provider_response.files[-1] # Taking last (most recent) file
|
|
1116
|
+
|
|
1117
|
+
if provider_response.audios is not None:
|
|
1118
|
+
if provider_response.audios:
|
|
1119
|
+
assistant_message.audio_output = provider_response.audios[-1] # Taking last (most recent) audio
|
|
1120
|
+
|
|
1121
|
+
# Add redacted thinking content to assistant message
|
|
1122
|
+
if provider_response.redacted_reasoning_content is not None:
|
|
1123
|
+
assistant_message.redacted_reasoning_content = provider_response.redacted_reasoning_content
|
|
1124
|
+
|
|
1125
|
+
# Add reasoning content to assistant message
|
|
1126
|
+
if provider_response.reasoning_content is not None:
|
|
1127
|
+
assistant_message.reasoning_content = provider_response.reasoning_content
|
|
1128
|
+
|
|
1129
|
+
# Add provider data to assistant message
|
|
1130
|
+
if provider_response.provider_data is not None:
|
|
1131
|
+
assistant_message.provider_data = provider_response.provider_data
|
|
1132
|
+
|
|
1133
|
+
# Add citations to assistant message
|
|
1134
|
+
if provider_response.citations is not None:
|
|
1135
|
+
assistant_message.citations = provider_response.citations
|
|
1136
|
+
|
|
1137
|
+
# Add usage metrics if provided
|
|
1138
|
+
if provider_response.response_usage is not None:
|
|
1139
|
+
assistant_message.metrics += provider_response.response_usage
|
|
1140
|
+
|
|
1141
|
+
return assistant_message
|
|
1142
|
+
|
|
1143
|
+
def process_response_stream(
|
|
1144
|
+
self,
|
|
1145
|
+
messages: List[Message],
|
|
1146
|
+
assistant_message: Message,
|
|
1147
|
+
stream_data: MessageData,
|
|
1148
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
1149
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
1150
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
1151
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
1152
|
+
compress_tool_results: bool = False,
|
|
1153
|
+
) -> Iterator[ModelResponse]:
|
|
1154
|
+
"""
|
|
1155
|
+
Process a streaming response from the model with retry logic for ModelProviderError.
|
|
1156
|
+
"""
|
|
1157
|
+
|
|
1158
|
+
for response_delta in self._invoke_stream_with_retry(
|
|
1159
|
+
messages=messages,
|
|
1160
|
+
assistant_message=assistant_message,
|
|
1161
|
+
response_format=response_format,
|
|
1162
|
+
tools=tools,
|
|
1163
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
1164
|
+
run_response=run_response,
|
|
1165
|
+
compress_tool_results=compress_tool_results,
|
|
1166
|
+
):
|
|
1167
|
+
for model_response_delta in self._populate_stream_data(
|
|
1168
|
+
stream_data=stream_data,
|
|
1169
|
+
model_response_delta=response_delta,
|
|
1170
|
+
):
|
|
1171
|
+
yield model_response_delta
|
|
1172
|
+
|
|
1173
|
+
# Populate assistant message from stream data after the stream ends
|
|
1174
|
+
self._populate_assistant_message_from_stream_data(assistant_message=assistant_message, stream_data=stream_data)
|
|
1175
|
+
|
|
1176
|
+
def response_stream(
|
|
1177
|
+
self,
|
|
1178
|
+
messages: List[Message],
|
|
1179
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
1180
|
+
tools: Optional[List[Union[Function, dict]]] = None,
|
|
1181
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
1182
|
+
tool_call_limit: Optional[int] = None,
|
|
1183
|
+
stream_model_response: bool = True,
|
|
1184
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
1185
|
+
send_media_to_model: bool = True,
|
|
1186
|
+
compression_manager: Optional["CompressionManager"] = None,
|
|
1187
|
+
) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
|
|
1188
|
+
"""
|
|
1189
|
+
Generate a streaming response from the model.
|
|
1190
|
+
"""
|
|
1191
|
+
try:
|
|
1192
|
+
# Check cache if enabled - capture key BEFORE streaming to avoid mismatch
|
|
1193
|
+
cache_key = None
|
|
1194
|
+
if self.cache_response:
|
|
1195
|
+
cache_key = self._get_model_cache_key(
|
|
1196
|
+
messages, stream=True, response_format=response_format, tools=tools
|
|
1197
|
+
)
|
|
1198
|
+
cached_data = self._get_cached_model_response(cache_key)
|
|
1199
|
+
|
|
1200
|
+
if cached_data:
|
|
1201
|
+
log_info("Cache hit for streaming model response")
|
|
1202
|
+
# Yield cached responses
|
|
1203
|
+
for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
|
|
1204
|
+
yield response
|
|
1205
|
+
return
|
|
1206
|
+
|
|
1207
|
+
log_info("Cache miss for streaming model response")
|
|
1208
|
+
|
|
1209
|
+
# Track streaming responses for caching
|
|
1210
|
+
streaming_responses: List[ModelResponse] = []
|
|
1211
|
+
|
|
1212
|
+
log_debug(f"{self.get_provider()} Response Stream Start", center=True, symbol="-")
|
|
1213
|
+
log_debug(f"Model: {self.id}", center=True, symbol="-")
|
|
1214
|
+
_log_messages(messages)
|
|
1215
|
+
|
|
1216
|
+
_tool_dicts = self._format_tools(tools) if tools is not None else []
|
|
1217
|
+
_functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
|
|
1218
|
+
|
|
1219
|
+
_compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
|
|
1220
|
+
_compression_manager = compression_manager if _compress_tool_results else None
|
|
1221
|
+
|
|
1222
|
+
function_call_count = 0
|
|
1223
|
+
|
|
1224
|
+
while True:
|
|
1225
|
+
# Compress existing tool results BEFORE invoke
|
|
1226
|
+
if _compression_manager is not None and _compression_manager.should_compress(
|
|
1227
|
+
messages, tools, model=self, response_format=response_format
|
|
1228
|
+
):
|
|
1229
|
+
_compression_manager.compress(messages)
|
|
1230
|
+
|
|
1231
|
+
assistant_message = Message(role=self.assistant_message_role)
|
|
1232
|
+
# Create assistant message and stream data
|
|
1233
|
+
stream_data = MessageData()
|
|
1234
|
+
model_response = ModelResponse()
|
|
1235
|
+
if stream_model_response:
|
|
1236
|
+
# Generate response
|
|
1237
|
+
for response in self.process_response_stream(
|
|
1238
|
+
messages=messages,
|
|
1239
|
+
assistant_message=assistant_message,
|
|
1240
|
+
stream_data=stream_data,
|
|
1241
|
+
response_format=response_format,
|
|
1242
|
+
tools=_tool_dicts,
|
|
1243
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
1244
|
+
run_response=run_response,
|
|
1245
|
+
compress_tool_results=_compress_tool_results,
|
|
1246
|
+
):
|
|
1247
|
+
if self.cache_response and isinstance(response, ModelResponse):
|
|
1248
|
+
streaming_responses.append(response)
|
|
1249
|
+
yield response
|
|
1250
|
+
|
|
1251
|
+
else:
|
|
1252
|
+
self._process_model_response(
|
|
1253
|
+
messages=messages,
|
|
1254
|
+
assistant_message=assistant_message,
|
|
1255
|
+
model_response=model_response,
|
|
1256
|
+
response_format=response_format,
|
|
1257
|
+
tools=_tool_dicts,
|
|
1258
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
1259
|
+
run_response=run_response,
|
|
1260
|
+
compress_tool_results=_compress_tool_results,
|
|
1261
|
+
)
|
|
1262
|
+
if self.cache_response:
|
|
1263
|
+
streaming_responses.append(model_response)
|
|
1264
|
+
yield model_response
|
|
1265
|
+
|
|
1266
|
+
# Add assistant message to messages
|
|
1267
|
+
messages.append(assistant_message)
|
|
1268
|
+
assistant_message.log(metrics=True)
|
|
1269
|
+
|
|
1270
|
+
# Handle tool calls if present
|
|
1271
|
+
if assistant_message.tool_calls is not None:
|
|
1272
|
+
# Prepare function calls
|
|
1273
|
+
function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
|
|
1274
|
+
assistant_message=assistant_message, messages=messages, functions=_functions
|
|
1275
|
+
)
|
|
1276
|
+
function_call_results: List[Message] = []
|
|
1277
|
+
|
|
1278
|
+
# Execute function calls
|
|
1279
|
+
for function_call_response in self.run_function_calls(
|
|
1280
|
+
function_calls=function_calls_to_run,
|
|
1281
|
+
function_call_results=function_call_results,
|
|
1282
|
+
current_function_call_count=function_call_count,
|
|
1283
|
+
function_call_limit=tool_call_limit,
|
|
1284
|
+
):
|
|
1285
|
+
if self.cache_response and isinstance(function_call_response, ModelResponse):
|
|
1286
|
+
streaming_responses.append(function_call_response)
|
|
1287
|
+
yield function_call_response
|
|
1288
|
+
|
|
1289
|
+
# Add a function call for each successful execution
|
|
1290
|
+
function_call_count += len(function_call_results)
|
|
1291
|
+
|
|
1292
|
+
# Format and add results to messages
|
|
1293
|
+
if stream_data and stream_data.extra is not None:
|
|
1294
|
+
self.format_function_call_results(
|
|
1295
|
+
messages=messages,
|
|
1296
|
+
function_call_results=function_call_results,
|
|
1297
|
+
compress_tool_results=_compress_tool_results,
|
|
1298
|
+
**stream_data.extra,
|
|
1299
|
+
)
|
|
1300
|
+
elif model_response and model_response.extra is not None:
|
|
1301
|
+
self.format_function_call_results(
|
|
1302
|
+
messages=messages,
|
|
1303
|
+
function_call_results=function_call_results,
|
|
1304
|
+
compress_tool_results=_compress_tool_results,
|
|
1305
|
+
**model_response.extra,
|
|
1306
|
+
)
|
|
1307
|
+
else:
|
|
1308
|
+
self.format_function_call_results(
|
|
1309
|
+
messages=messages,
|
|
1310
|
+
function_call_results=function_call_results,
|
|
1311
|
+
compress_tool_results=_compress_tool_results,
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
# Handle function call media
|
|
1315
|
+
if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
|
|
1316
|
+
self._handle_function_call_media(
|
|
1317
|
+
messages=messages,
|
|
1318
|
+
function_call_results=function_call_results,
|
|
1319
|
+
send_media_to_model=send_media_to_model,
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
for function_call_result in function_call_results:
|
|
1323
|
+
function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
|
|
1324
|
+
|
|
1325
|
+
# Check if we should stop after tool calls
|
|
1326
|
+
if any(m.stop_after_tool_call for m in function_call_results):
|
|
1327
|
+
break
|
|
1328
|
+
|
|
1329
|
+
# If we have any tool calls that require confirmation, break the loop
|
|
1330
|
+
if any(fc.function.requires_confirmation for fc in function_calls_to_run):
|
|
1331
|
+
break
|
|
1332
|
+
|
|
1333
|
+
# If we have any tool calls that require external execution, break the loop
|
|
1334
|
+
if any(fc.function.external_execution for fc in function_calls_to_run):
|
|
1335
|
+
break
|
|
1336
|
+
|
|
1337
|
+
# If we have any tool calls that require user input, break the loop
|
|
1338
|
+
if any(fc.function.requires_user_input for fc in function_calls_to_run):
|
|
1339
|
+
break
|
|
1340
|
+
|
|
1341
|
+
# Continue loop to get next response
|
|
1342
|
+
continue
|
|
1343
|
+
|
|
1344
|
+
# No tool calls or finished processing them
|
|
1345
|
+
break
|
|
1346
|
+
|
|
1347
|
+
log_debug(f"{self.get_provider()} Response Stream End", center=True, symbol="-")
|
|
1348
|
+
|
|
1349
|
+
# Save streaming responses to cache if enabled
|
|
1350
|
+
if self.cache_response and cache_key and streaming_responses:
|
|
1351
|
+
self._save_streaming_responses_to_cache(cache_key, streaming_responses)
|
|
1352
|
+
finally:
|
|
1353
|
+
# Close the Gemini client
|
|
1354
|
+
if self.__class__.__name__ == "Gemini" and self.client is not None:
|
|
1355
|
+
try:
|
|
1356
|
+
self.client.close() # type: ignore
|
|
1357
|
+
self.client = None
|
|
1358
|
+
except AttributeError:
|
|
1359
|
+
log_warning(
|
|
1360
|
+
"Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
|
|
1361
|
+
" please upgrade Gemini to the latest version: pip install -U google-genai"
|
|
1362
|
+
)
|
|
1363
|
+
|
|
1364
|
+
async def aprocess_response_stream(
|
|
1365
|
+
self,
|
|
1366
|
+
messages: List[Message],
|
|
1367
|
+
assistant_message: Message,
|
|
1368
|
+
stream_data: MessageData,
|
|
1369
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
1370
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
1371
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
1372
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
1373
|
+
compress_tool_results: bool = False,
|
|
1374
|
+
) -> AsyncIterator[ModelResponse]:
|
|
1375
|
+
"""
|
|
1376
|
+
Process a streaming response from the model with retry logic for ModelProviderError.
|
|
1377
|
+
"""
|
|
1378
|
+
async for response_delta in self._ainvoke_stream_with_retry(
|
|
1379
|
+
messages=messages,
|
|
1380
|
+
assistant_message=assistant_message,
|
|
1381
|
+
response_format=response_format,
|
|
1382
|
+
tools=tools,
|
|
1383
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
1384
|
+
run_response=run_response,
|
|
1385
|
+
compress_tool_results=compress_tool_results,
|
|
1386
|
+
):
|
|
1387
|
+
for model_response_delta in self._populate_stream_data(
|
|
1388
|
+
stream_data=stream_data,
|
|
1389
|
+
model_response_delta=response_delta,
|
|
1390
|
+
):
|
|
1391
|
+
yield model_response_delta
|
|
1392
|
+
|
|
1393
|
+
# Populate assistant message from stream data after the stream ends
|
|
1394
|
+
self._populate_assistant_message_from_stream_data(assistant_message=assistant_message, stream_data=stream_data)
|
|
1395
|
+
|
|
1396
|
+
async def aresponse_stream(
|
|
1397
|
+
self,
|
|
1398
|
+
messages: List[Message],
|
|
1399
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
1400
|
+
tools: Optional[List[Union[Function, dict]]] = None,
|
|
1401
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
1402
|
+
tool_call_limit: Optional[int] = None,
|
|
1403
|
+
stream_model_response: bool = True,
|
|
1404
|
+
run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
|
|
1405
|
+
send_media_to_model: bool = True,
|
|
1406
|
+
compression_manager: Optional["CompressionManager"] = None,
|
|
1407
|
+
) -> AsyncIterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
|
|
1408
|
+
"""
|
|
1409
|
+
Generate an asynchronous streaming response from the model.
|
|
1410
|
+
"""
|
|
1411
|
+
try:
|
|
1412
|
+
# Check cache if enabled - capture key BEFORE streaming to avoid mismatch
|
|
1413
|
+
cache_key = None
|
|
1414
|
+
if self.cache_response:
|
|
1415
|
+
cache_key = self._get_model_cache_key(
|
|
1416
|
+
messages, stream=True, response_format=response_format, tools=tools
|
|
1417
|
+
)
|
|
1418
|
+
cached_data = self._get_cached_model_response(cache_key)
|
|
1419
|
+
|
|
1420
|
+
if cached_data:
|
|
1421
|
+
log_info("Cache hit for async streaming model response")
|
|
1422
|
+
# Yield cached responses
|
|
1423
|
+
for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
|
|
1424
|
+
yield response
|
|
1425
|
+
return
|
|
1426
|
+
|
|
1427
|
+
log_info("Cache miss for async streaming model response")
|
|
1428
|
+
|
|
1429
|
+
# Track streaming responses for caching
|
|
1430
|
+
streaming_responses: List[ModelResponse] = []
|
|
1431
|
+
|
|
1432
|
+
log_debug(f"{self.get_provider()} Async Response Stream Start", center=True, symbol="-")
|
|
1433
|
+
log_debug(f"Model: {self.id}", center=True, symbol="-")
|
|
1434
|
+
_log_messages(messages)
|
|
1435
|
+
|
|
1436
|
+
_tool_dicts = self._format_tools(tools) if tools is not None else []
|
|
1437
|
+
_functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
|
|
1438
|
+
|
|
1439
|
+
_compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
|
|
1440
|
+
_compression_manager = compression_manager if _compress_tool_results else None
|
|
1441
|
+
|
|
1442
|
+
function_call_count = 0
|
|
1443
|
+
|
|
1444
|
+
while True:
|
|
1445
|
+
# Compress existing tool results BEFORE making API call to avoid context overflow
|
|
1446
|
+
if _compression_manager is not None and await _compression_manager.ashould_compress(
|
|
1447
|
+
messages, tools, model=self, response_format=response_format
|
|
1448
|
+
):
|
|
1449
|
+
await _compression_manager.acompress(messages)
|
|
1450
|
+
|
|
1451
|
+
# Create assistant message and stream data
|
|
1452
|
+
assistant_message = Message(role=self.assistant_message_role)
|
|
1453
|
+
stream_data = MessageData()
|
|
1454
|
+
model_response = ModelResponse()
|
|
1455
|
+
if stream_model_response:
|
|
1456
|
+
# Generate response
|
|
1457
|
+
async for model_response in self.aprocess_response_stream(
|
|
1458
|
+
messages=messages,
|
|
1459
|
+
assistant_message=assistant_message,
|
|
1460
|
+
stream_data=stream_data,
|
|
1461
|
+
response_format=response_format,
|
|
1462
|
+
tools=_tool_dicts,
|
|
1463
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
1464
|
+
run_response=run_response,
|
|
1465
|
+
compress_tool_results=_compress_tool_results,
|
|
1466
|
+
):
|
|
1467
|
+
if self.cache_response and isinstance(model_response, ModelResponse):
|
|
1468
|
+
streaming_responses.append(model_response)
|
|
1469
|
+
yield model_response
|
|
1470
|
+
|
|
1471
|
+
else:
|
|
1472
|
+
await self._aprocess_model_response(
|
|
1473
|
+
messages=messages,
|
|
1474
|
+
assistant_message=assistant_message,
|
|
1475
|
+
model_response=model_response,
|
|
1476
|
+
response_format=response_format,
|
|
1477
|
+
tools=_tool_dicts,
|
|
1478
|
+
tool_choice=tool_choice or self._tool_choice,
|
|
1479
|
+
run_response=run_response,
|
|
1480
|
+
compress_tool_results=_compress_tool_results,
|
|
1481
|
+
)
|
|
1482
|
+
if self.cache_response:
|
|
1483
|
+
streaming_responses.append(model_response)
|
|
1484
|
+
yield model_response
|
|
1485
|
+
|
|
1486
|
+
# Add assistant message to messages
|
|
1487
|
+
messages.append(assistant_message)
|
|
1488
|
+
assistant_message.log(metrics=True)
|
|
1489
|
+
|
|
1490
|
+
# Handle tool calls if present
|
|
1491
|
+
if assistant_message.tool_calls is not None:
|
|
1492
|
+
# Prepare function calls
|
|
1493
|
+
function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
|
|
1494
|
+
assistant_message=assistant_message, messages=messages, functions=_functions
|
|
1495
|
+
)
|
|
1496
|
+
function_call_results: List[Message] = []
|
|
1497
|
+
|
|
1498
|
+
# Execute function calls
|
|
1499
|
+
async for function_call_response in self.arun_function_calls(
|
|
1500
|
+
function_calls=function_calls_to_run,
|
|
1501
|
+
function_call_results=function_call_results,
|
|
1502
|
+
current_function_call_count=function_call_count,
|
|
1503
|
+
function_call_limit=tool_call_limit,
|
|
1504
|
+
):
|
|
1505
|
+
if self.cache_response and isinstance(function_call_response, ModelResponse):
|
|
1506
|
+
streaming_responses.append(function_call_response)
|
|
1507
|
+
yield function_call_response
|
|
1508
|
+
|
|
1509
|
+
# Add a function call for each successful execution
|
|
1510
|
+
function_call_count += len(function_call_results)
|
|
1511
|
+
|
|
1512
|
+
# Format and add results to messages
|
|
1513
|
+
if stream_data and stream_data.extra is not None:
|
|
1514
|
+
self.format_function_call_results(
|
|
1515
|
+
messages=messages,
|
|
1516
|
+
function_call_results=function_call_results,
|
|
1517
|
+
compress_tool_results=_compress_tool_results,
|
|
1518
|
+
**stream_data.extra,
|
|
1519
|
+
)
|
|
1520
|
+
elif model_response and model_response.extra is not None:
|
|
1521
|
+
self.format_function_call_results(
|
|
1522
|
+
messages=messages,
|
|
1523
|
+
function_call_results=function_call_results,
|
|
1524
|
+
compress_tool_results=_compress_tool_results,
|
|
1525
|
+
**model_response.extra or {},
|
|
1526
|
+
)
|
|
1527
|
+
else:
|
|
1528
|
+
self.format_function_call_results(
|
|
1529
|
+
messages=messages,
|
|
1530
|
+
function_call_results=function_call_results,
|
|
1531
|
+
compress_tool_results=_compress_tool_results,
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
# Handle function call media
|
|
1535
|
+
if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
|
|
1536
|
+
self._handle_function_call_media(
|
|
1537
|
+
messages=messages,
|
|
1538
|
+
function_call_results=function_call_results,
|
|
1539
|
+
send_media_to_model=send_media_to_model,
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
for function_call_result in function_call_results:
|
|
1543
|
+
function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
|
|
1544
|
+
|
|
1545
|
+
# Check if we should stop after tool calls
|
|
1546
|
+
if any(m.stop_after_tool_call for m in function_call_results):
|
|
1547
|
+
break
|
|
1548
|
+
|
|
1549
|
+
# If we have any tool calls that require confirmation, break the loop
|
|
1550
|
+
if any(fc.function.requires_confirmation for fc in function_calls_to_run):
|
|
1551
|
+
break
|
|
1552
|
+
|
|
1553
|
+
# If we have any tool calls that require external execution, break the loop
|
|
1554
|
+
if any(fc.function.external_execution for fc in function_calls_to_run):
|
|
1555
|
+
break
|
|
1556
|
+
|
|
1557
|
+
# If we have any tool calls that require user input, break the loop
|
|
1558
|
+
if any(fc.function.requires_user_input for fc in function_calls_to_run):
|
|
1559
|
+
break
|
|
1560
|
+
|
|
1561
|
+
# Continue loop to get next response
|
|
1562
|
+
continue
|
|
1563
|
+
|
|
1564
|
+
# No tool calls or finished processing them
|
|
1565
|
+
break
|
|
1566
|
+
|
|
1567
|
+
log_debug(f"{self.get_provider()} Async Response Stream End", center=True, symbol="-")
|
|
1568
|
+
|
|
1569
|
+
# Save streaming responses to cache if enabled
|
|
1570
|
+
if self.cache_response and cache_key and streaming_responses:
|
|
1571
|
+
self._save_streaming_responses_to_cache(cache_key, streaming_responses)
|
|
1572
|
+
|
|
1573
|
+
finally:
|
|
1574
|
+
# Close the Gemini client
|
|
1575
|
+
if self.__class__.__name__ == "Gemini" and self.client is not None:
|
|
1576
|
+
try:
|
|
1577
|
+
await self.client.aio.aclose() # type: ignore
|
|
1578
|
+
self.client = None
|
|
1579
|
+
except AttributeError:
|
|
1580
|
+
log_warning(
|
|
1581
|
+
"Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
|
|
1582
|
+
" please upgrade Gemini to the latest version: pip install -U google-genai"
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
def _populate_assistant_message_from_stream_data(
|
|
1586
|
+
self, assistant_message: Message, stream_data: MessageData
|
|
1587
|
+
) -> None:
|
|
1588
|
+
"""
|
|
1589
|
+
Populate an assistant message with the stream data.
|
|
1590
|
+
"""
|
|
1591
|
+
if stream_data.response_role is not None:
|
|
1592
|
+
assistant_message.role = stream_data.response_role
|
|
1593
|
+
if stream_data.response_metrics is not None:
|
|
1594
|
+
assistant_message.metrics = stream_data.response_metrics
|
|
1595
|
+
if stream_data.response_content:
|
|
1596
|
+
assistant_message.content = stream_data.response_content
|
|
1597
|
+
if stream_data.response_reasoning_content:
|
|
1598
|
+
assistant_message.reasoning_content = stream_data.response_reasoning_content
|
|
1599
|
+
if stream_data.response_redacted_reasoning_content:
|
|
1600
|
+
assistant_message.redacted_reasoning_content = stream_data.response_redacted_reasoning_content
|
|
1601
|
+
if stream_data.response_provider_data:
|
|
1602
|
+
assistant_message.provider_data = stream_data.response_provider_data
|
|
1603
|
+
if stream_data.response_citations:
|
|
1604
|
+
assistant_message.citations = stream_data.response_citations
|
|
1605
|
+
if stream_data.response_audio:
|
|
1606
|
+
assistant_message.audio_output = stream_data.response_audio
|
|
1607
|
+
if stream_data.response_image:
|
|
1608
|
+
assistant_message.image_output = stream_data.response_image
|
|
1609
|
+
if stream_data.response_video:
|
|
1610
|
+
assistant_message.video_output = stream_data.response_video
|
|
1611
|
+
if stream_data.response_file:
|
|
1612
|
+
assistant_message.file_output = stream_data.response_file
|
|
1613
|
+
if stream_data.response_tool_calls and len(stream_data.response_tool_calls) > 0:
|
|
1614
|
+
assistant_message.tool_calls = self.parse_tool_calls(stream_data.response_tool_calls)
|
|
1615
|
+
|
|
1616
|
+
def _populate_stream_data(
|
|
1617
|
+
self, stream_data: MessageData, model_response_delta: ModelResponse
|
|
1618
|
+
) -> Iterator[ModelResponse]:
|
|
1619
|
+
"""Update the stream data and assistant message with the model response."""
|
|
1620
|
+
|
|
1621
|
+
should_yield = False
|
|
1622
|
+
if model_response_delta.role is not None:
|
|
1623
|
+
stream_data.response_role = model_response_delta.role # type: ignore
|
|
1624
|
+
|
|
1625
|
+
if model_response_delta.response_usage is not None:
|
|
1626
|
+
if stream_data.response_metrics is None:
|
|
1627
|
+
stream_data.response_metrics = Metrics()
|
|
1628
|
+
stream_data.response_metrics += model_response_delta.response_usage
|
|
1629
|
+
|
|
1630
|
+
# Update stream_data content
|
|
1631
|
+
if model_response_delta.content is not None:
|
|
1632
|
+
stream_data.response_content += model_response_delta.content
|
|
1633
|
+
should_yield = True
|
|
1634
|
+
|
|
1635
|
+
if model_response_delta.reasoning_content is not None:
|
|
1636
|
+
stream_data.response_reasoning_content += model_response_delta.reasoning_content
|
|
1637
|
+
should_yield = True
|
|
1638
|
+
|
|
1639
|
+
if model_response_delta.redacted_reasoning_content is not None:
|
|
1640
|
+
stream_data.response_redacted_reasoning_content += model_response_delta.redacted_reasoning_content
|
|
1641
|
+
should_yield = True
|
|
1642
|
+
|
|
1643
|
+
if model_response_delta.citations is not None:
|
|
1644
|
+
stream_data.response_citations = model_response_delta.citations
|
|
1645
|
+
should_yield = True
|
|
1646
|
+
|
|
1647
|
+
if model_response_delta.provider_data:
|
|
1648
|
+
if stream_data.response_provider_data is None:
|
|
1649
|
+
stream_data.response_provider_data = {}
|
|
1650
|
+
stream_data.response_provider_data.update(model_response_delta.provider_data)
|
|
1651
|
+
|
|
1652
|
+
# Update stream_data tool calls
|
|
1653
|
+
if model_response_delta.tool_calls is not None:
|
|
1654
|
+
if stream_data.response_tool_calls is None:
|
|
1655
|
+
stream_data.response_tool_calls = []
|
|
1656
|
+
stream_data.response_tool_calls.extend(model_response_delta.tool_calls)
|
|
1657
|
+
should_yield = True
|
|
1658
|
+
|
|
1659
|
+
if model_response_delta.audio is not None and isinstance(model_response_delta.audio, Audio):
|
|
1660
|
+
if stream_data.response_audio is None:
|
|
1661
|
+
stream_data.response_audio = Audio(id=str(uuid4()), content="", transcript="")
|
|
1662
|
+
|
|
1663
|
+
from typing import cast
|
|
1664
|
+
|
|
1665
|
+
audio_response = cast(Audio, model_response_delta.audio)
|
|
1666
|
+
|
|
1667
|
+
# Update the stream data with audio information
|
|
1668
|
+
if audio_response.id is not None:
|
|
1669
|
+
stream_data.response_audio.id = audio_response.id # type: ignore
|
|
1670
|
+
if audio_response.content is not None:
|
|
1671
|
+
stream_data.response_audio.content += audio_response.content # type: ignore
|
|
1672
|
+
if audio_response.transcript is not None:
|
|
1673
|
+
stream_data.response_audio.transcript += audio_response.transcript # type: ignore
|
|
1674
|
+
if audio_response.expires_at is not None:
|
|
1675
|
+
stream_data.response_audio.expires_at = audio_response.expires_at
|
|
1676
|
+
if audio_response.mime_type is not None:
|
|
1677
|
+
stream_data.response_audio.mime_type = audio_response.mime_type
|
|
1678
|
+
stream_data.response_audio.sample_rate = audio_response.sample_rate
|
|
1679
|
+
stream_data.response_audio.channels = audio_response.channels
|
|
1680
|
+
|
|
1681
|
+
should_yield = True
|
|
1682
|
+
|
|
1683
|
+
if model_response_delta.images:
|
|
1684
|
+
if stream_data.response_image is None:
|
|
1685
|
+
stream_data.response_image = model_response_delta.images[-1]
|
|
1686
|
+
should_yield = True
|
|
1687
|
+
|
|
1688
|
+
if model_response_delta.videos:
|
|
1689
|
+
if stream_data.response_video is None:
|
|
1690
|
+
stream_data.response_video = model_response_delta.videos[-1]
|
|
1691
|
+
should_yield = True
|
|
1692
|
+
|
|
1693
|
+
if model_response_delta.extra is not None:
|
|
1694
|
+
if stream_data.extra is None:
|
|
1695
|
+
stream_data.extra = {}
|
|
1696
|
+
for key in model_response_delta.extra:
|
|
1697
|
+
if isinstance(model_response_delta.extra[key], list):
|
|
1698
|
+
if not stream_data.extra.get(key):
|
|
1699
|
+
stream_data.extra[key] = []
|
|
1700
|
+
stream_data.extra[key].extend(model_response_delta.extra[key])
|
|
1701
|
+
else:
|
|
1702
|
+
stream_data.extra[key] = model_response_delta.extra[key]
|
|
1703
|
+
|
|
1704
|
+
if should_yield:
|
|
1705
|
+
yield model_response_delta
|
|
1706
|
+
|
|
1707
|
+
def parse_tool_calls(self, tool_calls_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
1708
|
+
"""
|
|
1709
|
+
Parse the tool calls from the model provider into a list of tool calls.
|
|
1710
|
+
"""
|
|
1711
|
+
return tool_calls_data
|
|
1712
|
+
|
|
1713
|
+
def get_function_call_to_run_from_tool_execution(
|
|
1714
|
+
self,
|
|
1715
|
+
tool_execution: ToolExecution,
|
|
1716
|
+
functions: Optional[Dict[str, Function]] = None,
|
|
1717
|
+
) -> FunctionCall:
|
|
1718
|
+
function_call = get_function_call_for_tool_execution(
|
|
1719
|
+
tool_execution=tool_execution,
|
|
1720
|
+
functions=functions,
|
|
1721
|
+
)
|
|
1722
|
+
if function_call is None:
|
|
1723
|
+
raise ValueError("Function call not found")
|
|
1724
|
+
return function_call
|
|
1725
|
+
|
|
1726
|
+
def get_function_calls_to_run(
|
|
1727
|
+
self,
|
|
1728
|
+
assistant_message: Message,
|
|
1729
|
+
messages: List[Message],
|
|
1730
|
+
functions: Optional[Dict[str, Function]] = None,
|
|
1731
|
+
) -> List[FunctionCall]:
|
|
1732
|
+
"""
|
|
1733
|
+
Prepare function calls for the assistant message.
|
|
223
1734
|
"""
|
|
224
1735
|
function_calls_to_run: List[FunctionCall] = []
|
|
225
1736
|
if assistant_message.tool_calls is not None:
|
|
226
1737
|
for tool_call in assistant_message.tool_calls:
|
|
227
|
-
|
|
1738
|
+
_tool_call_id = tool_call.get("id")
|
|
1739
|
+
_function_call = get_function_call_for_tool_call(tool_call, functions)
|
|
228
1740
|
if _function_call is None:
|
|
229
|
-
messages.append(
|
|
1741
|
+
messages.append(
|
|
1742
|
+
Message(
|
|
1743
|
+
role=self.tool_message_role,
|
|
1744
|
+
tool_call_id=_tool_call_id,
|
|
1745
|
+
content="Error: The requested tool does not exist or is not available.",
|
|
1746
|
+
)
|
|
1747
|
+
)
|
|
230
1748
|
continue
|
|
231
1749
|
if _function_call.error is not None:
|
|
232
|
-
messages.append(
|
|
1750
|
+
messages.append(
|
|
1751
|
+
Message(role=self.tool_message_role, tool_call_id=_tool_call_id, content=_function_call.error)
|
|
1752
|
+
)
|
|
233
1753
|
continue
|
|
234
1754
|
function_calls_to_run.append(_function_call)
|
|
235
1755
|
return function_calls_to_run
|
|
236
1756
|
|
|
237
|
-
def
|
|
238
|
-
self,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
elif callable(tool):
|
|
278
|
-
try:
|
|
279
|
-
function_name = tool.__name__
|
|
280
|
-
if function_name not in self._functions:
|
|
281
|
-
func = Function.from_callable(tool, strict=strict)
|
|
282
|
-
func._agent = agent
|
|
283
|
-
if strict and self.supports_structured_outputs:
|
|
284
|
-
func.strict = True
|
|
285
|
-
self._functions[func.name] = func
|
|
286
|
-
self.tools.append({"type": "function", "function": func.to_dict()})
|
|
287
|
-
logger.debug(f"Function {func.name} added to model.")
|
|
288
|
-
except Exception as e:
|
|
289
|
-
logger.warning(f"Could not add function {tool}: {e}")
|
|
290
|
-
|
|
291
|
-
def run_function_calls(
|
|
292
|
-
self, function_calls: List[FunctionCall], function_call_results: List[Message], tool_role: str = "tool"
|
|
293
|
-
) -> Iterator[ModelResponse]:
|
|
294
|
-
for function_call in function_calls:
|
|
295
|
-
if self._function_call_stack is None:
|
|
296
|
-
self._function_call_stack = []
|
|
1757
|
+
def create_function_call_result(
|
|
1758
|
+
self,
|
|
1759
|
+
function_call: FunctionCall,
|
|
1760
|
+
success: bool,
|
|
1761
|
+
output: Optional[Union[List[Any], str]] = None,
|
|
1762
|
+
timer: Optional[Timer] = None,
|
|
1763
|
+
function_execution_result: Optional[FunctionExecutionResult] = None,
|
|
1764
|
+
) -> Message:
|
|
1765
|
+
"""Create a function call result message."""
|
|
1766
|
+
kwargs = {}
|
|
1767
|
+
if timer is not None:
|
|
1768
|
+
kwargs["metrics"] = Metrics(duration=timer.elapsed)
|
|
1769
|
+
|
|
1770
|
+
# Include media artifacts from function execution result in the tool message
|
|
1771
|
+
images = None
|
|
1772
|
+
videos = None
|
|
1773
|
+
audios = None
|
|
1774
|
+
files = None
|
|
1775
|
+
|
|
1776
|
+
if success and function_execution_result:
|
|
1777
|
+
# With unified classes, no conversion needed - use directly
|
|
1778
|
+
images = function_execution_result.images
|
|
1779
|
+
videos = function_execution_result.videos
|
|
1780
|
+
audios = function_execution_result.audios
|
|
1781
|
+
files = function_execution_result.files
|
|
1782
|
+
|
|
1783
|
+
return Message(
|
|
1784
|
+
role=self.tool_message_role,
|
|
1785
|
+
content=output if success else function_call.error,
|
|
1786
|
+
tool_call_id=function_call.call_id,
|
|
1787
|
+
tool_name=function_call.function.name,
|
|
1788
|
+
tool_args=function_call.arguments,
|
|
1789
|
+
tool_call_error=not success,
|
|
1790
|
+
stop_after_tool_call=function_call.function.stop_after_tool_call,
|
|
1791
|
+
images=images,
|
|
1792
|
+
videos=videos,
|
|
1793
|
+
audio=audios,
|
|
1794
|
+
files=files,
|
|
1795
|
+
**kwargs, # type: ignore
|
|
1796
|
+
)
|
|
297
1797
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"tool_name": function_call.function.name,
|
|
308
|
-
"tool_args": function_call.arguments,
|
|
309
|
-
}
|
|
310
|
-
],
|
|
311
|
-
event=ModelResponseEvent.tool_call_started.value,
|
|
312
|
-
)
|
|
1798
|
+
def create_tool_call_limit_error_result(self, function_call: FunctionCall) -> Message:
|
|
1799
|
+
return Message(
|
|
1800
|
+
role=self.tool_message_role,
|
|
1801
|
+
content=f"Tool call limit reached. Tool call {function_call.function.name} not executed. Don't try to execute it again.",
|
|
1802
|
+
tool_call_id=function_call.call_id,
|
|
1803
|
+
tool_name=function_call.function.name,
|
|
1804
|
+
tool_args=function_call.arguments,
|
|
1805
|
+
tool_call_error=True,
|
|
1806
|
+
)
|
|
313
1807
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
1808
|
+
def run_function_call(
|
|
1809
|
+
self,
|
|
1810
|
+
function_call: FunctionCall,
|
|
1811
|
+
function_call_results: List[Message],
|
|
1812
|
+
additional_input: Optional[List[Message]] = None,
|
|
1813
|
+
) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
|
|
1814
|
+
# Start function call
|
|
1815
|
+
function_call_timer = Timer()
|
|
1816
|
+
function_call_timer.start()
|
|
1817
|
+
# Yield a tool_call_started event
|
|
1818
|
+
yield ModelResponse(
|
|
1819
|
+
content=function_call.get_call_str(),
|
|
1820
|
+
tool_executions=[
|
|
1821
|
+
ToolExecution(
|
|
1822
|
+
tool_call_id=function_call.call_id,
|
|
1823
|
+
tool_name=function_call.function.name,
|
|
1824
|
+
tool_args=function_call.arguments,
|
|
1825
|
+
)
|
|
1826
|
+
],
|
|
1827
|
+
event=ModelResponseEvent.tool_call_started.value,
|
|
1828
|
+
)
|
|
320
1829
|
|
|
321
|
-
|
|
1830
|
+
# Run function calls sequentially
|
|
1831
|
+
function_execution_result: FunctionExecutionResult = FunctionExecutionResult(status="failure")
|
|
1832
|
+
stop_after_tool_call_from_exception = False
|
|
1833
|
+
try:
|
|
1834
|
+
function_execution_result = function_call.execute()
|
|
1835
|
+
except AgentRunException as a_exc:
|
|
1836
|
+
# Update additional messages from function call
|
|
1837
|
+
_handle_agent_exception(a_exc, additional_input)
|
|
1838
|
+
# If stop_execution is True, mark that we should stop after this tool call
|
|
1839
|
+
if a_exc.stop_execution:
|
|
1840
|
+
stop_after_tool_call_from_exception = True
|
|
1841
|
+
# Set function call success to False if an exception occurred
|
|
1842
|
+
except Exception as e:
|
|
1843
|
+
log_error(f"Error executing function {function_call.function.name}: {e}")
|
|
1844
|
+
raise e
|
|
1845
|
+
|
|
1846
|
+
function_call_success = function_execution_result.status == "success"
|
|
1847
|
+
|
|
1848
|
+
# Stop function call timer
|
|
1849
|
+
function_call_timer.stop()
|
|
1850
|
+
|
|
1851
|
+
# Process function call output
|
|
1852
|
+
function_call_output: str = ""
|
|
1853
|
+
|
|
1854
|
+
if isinstance(function_execution_result.result, (GeneratorType, collections.abc.Iterator)):
|
|
322
1855
|
try:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1856
|
+
for item in function_execution_result.result:
|
|
1857
|
+
# This function yields agent/team/workflow run events
|
|
1858
|
+
if (
|
|
1859
|
+
isinstance(item, tuple(get_args(RunOutputEvent)))
|
|
1860
|
+
or isinstance(item, tuple(get_args(TeamRunOutputEvent)))
|
|
1861
|
+
or isinstance(item, tuple(get_args(WorkflowRunOutputEvent)))
|
|
1862
|
+
):
|
|
1863
|
+
# We only capture content events for output accumulation
|
|
1864
|
+
if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
|
|
1865
|
+
if item.content is not None and isinstance(item.content, BaseModel):
|
|
1866
|
+
function_call_output += item.content.model_dump_json()
|
|
1867
|
+
else:
|
|
1868
|
+
# Capture output
|
|
1869
|
+
function_call_output += item.content or ""
|
|
1870
|
+
|
|
1871
|
+
if function_call.function.show_result and item.content is not None:
|
|
1872
|
+
yield ModelResponse(content=item.content)
|
|
1873
|
+
|
|
1874
|
+
if isinstance(item, CustomEvent):
|
|
1875
|
+
function_call_output += str(item)
|
|
1876
|
+
|
|
1877
|
+
# For WorkflowCompletedEvent, extract content for final output
|
|
1878
|
+
from agno.run.workflow import WorkflowCompletedEvent
|
|
1879
|
+
|
|
1880
|
+
if isinstance(item, WorkflowCompletedEvent):
|
|
1881
|
+
if item.content is not None:
|
|
1882
|
+
if isinstance(item.content, BaseModel):
|
|
1883
|
+
function_call_output += item.content.model_dump_json()
|
|
1884
|
+
else:
|
|
1885
|
+
function_call_output += str(item.content)
|
|
1886
|
+
|
|
1887
|
+
# Yield the event itself to bubble it up
|
|
1888
|
+
yield item
|
|
1889
|
+
|
|
335
1890
|
else:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1891
|
+
function_call_output += str(item)
|
|
1892
|
+
if function_call.function.show_result and item is not None:
|
|
1893
|
+
yield ModelResponse(content=str(item))
|
|
1894
|
+
except Exception as e:
|
|
1895
|
+
log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
|
|
1896
|
+
function_call.error = str(e)
|
|
1897
|
+
function_call_success = False
|
|
1898
|
+
|
|
1899
|
+
# For generators, re-capture updated_session_state after consumption
|
|
1900
|
+
# since session_state modifications were made during iteration
|
|
1901
|
+
if function_execution_result.updated_session_state is None:
|
|
1902
|
+
if (
|
|
1903
|
+
function_call.function._run_context is not None
|
|
1904
|
+
and function_call.function._run_context.session_state is not None
|
|
1905
|
+
):
|
|
1906
|
+
function_execution_result.updated_session_state = function_call.function._run_context.session_state
|
|
1907
|
+
elif function_call.function._session_state is not None:
|
|
1908
|
+
function_execution_result.updated_session_state = function_call.function._session_state
|
|
1909
|
+
else:
|
|
1910
|
+
from agno.tools.function import ToolResult
|
|
1911
|
+
|
|
1912
|
+
if isinstance(function_execution_result.result, ToolResult):
|
|
1913
|
+
# Extract content and media from ToolResult
|
|
1914
|
+
tool_result = function_execution_result.result
|
|
1915
|
+
function_call_output = tool_result.content
|
|
1916
|
+
|
|
1917
|
+
# Transfer media from ToolResult to FunctionExecutionResult
|
|
1918
|
+
if tool_result.images:
|
|
1919
|
+
function_execution_result.images = tool_result.images
|
|
1920
|
+
if tool_result.videos:
|
|
1921
|
+
function_execution_result.videos = tool_result.videos
|
|
1922
|
+
if tool_result.audios:
|
|
1923
|
+
function_execution_result.audios = tool_result.audios
|
|
1924
|
+
if tool_result.files:
|
|
1925
|
+
function_execution_result.files = tool_result.files
|
|
358
1926
|
else:
|
|
359
|
-
function_call_output =
|
|
360
|
-
|
|
361
|
-
|
|
1927
|
+
function_call_output = str(function_execution_result.result) if function_execution_result.result else ""
|
|
1928
|
+
|
|
1929
|
+
if function_call.function.show_result and function_call_output is not None:
|
|
1930
|
+
yield ModelResponse(content=function_call_output)
|
|
1931
|
+
|
|
1932
|
+
# Create and yield function call result
|
|
1933
|
+
function_call_result = self.create_function_call_result(
|
|
1934
|
+
function_call,
|
|
1935
|
+
success=function_call_success,
|
|
1936
|
+
output=function_call_output,
|
|
1937
|
+
timer=function_call_timer,
|
|
1938
|
+
function_execution_result=function_execution_result,
|
|
1939
|
+
)
|
|
1940
|
+
# Override stop_after_tool_call if set by exception
|
|
1941
|
+
if stop_after_tool_call_from_exception:
|
|
1942
|
+
function_call_result.stop_after_tool_call = True
|
|
1943
|
+
yield ModelResponse(
|
|
1944
|
+
content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s. ",
|
|
1945
|
+
tool_executions=[
|
|
1946
|
+
ToolExecution(
|
|
1947
|
+
tool_call_id=function_call_result.tool_call_id,
|
|
1948
|
+
tool_name=function_call_result.tool_name,
|
|
1949
|
+
tool_args=function_call_result.tool_args,
|
|
1950
|
+
tool_call_error=function_call_result.tool_call_error,
|
|
1951
|
+
result=str(function_call_result.content),
|
|
1952
|
+
stop_after_tool_call=function_call_result.stop_after_tool_call,
|
|
1953
|
+
metrics=function_call_result.metrics,
|
|
1954
|
+
)
|
|
1955
|
+
],
|
|
1956
|
+
event=ModelResponseEvent.tool_call_completed.value,
|
|
1957
|
+
updated_session_state=function_execution_result.updated_session_state,
|
|
1958
|
+
# Add media artifacts from function execution
|
|
1959
|
+
images=function_execution_result.images,
|
|
1960
|
+
videos=function_execution_result.videos,
|
|
1961
|
+
audios=function_execution_result.audios,
|
|
1962
|
+
files=function_execution_result.files,
|
|
1963
|
+
)
|
|
362
1964
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
# -*- Create function call result message
|
|
367
|
-
function_call_result = Message(
|
|
368
|
-
role=tool_role,
|
|
369
|
-
content=function_call_output if function_call_success else function_call.error,
|
|
370
|
-
tool_call_id=function_call.call_id,
|
|
371
|
-
tool_name=function_call.function.name,
|
|
372
|
-
tool_args=function_call.arguments,
|
|
373
|
-
tool_call_error=not function_call_success,
|
|
374
|
-
stop_after_tool_call=function_call.function.stop_after_tool_call or stop_execution_after_tool_call,
|
|
375
|
-
metrics={"time": function_call_timer.elapsed},
|
|
376
|
-
)
|
|
1965
|
+
# Add function call to function call results
|
|
1966
|
+
function_call_results.append(function_call_result)
|
|
377
1967
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1968
|
+
def run_function_calls(
|
|
1969
|
+
self,
|
|
1970
|
+
function_calls: List[FunctionCall],
|
|
1971
|
+
function_call_results: List[Message],
|
|
1972
|
+
additional_input: Optional[List[Message]] = None,
|
|
1973
|
+
current_function_call_count: int = 0,
|
|
1974
|
+
function_call_limit: Optional[int] = None,
|
|
1975
|
+
) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
|
|
1976
|
+
# Additional messages from function calls that will be added to the function call results
|
|
1977
|
+
if additional_input is None:
|
|
1978
|
+
additional_input = []
|
|
1979
|
+
|
|
1980
|
+
for fc in function_calls:
|
|
1981
|
+
if function_call_limit is not None:
|
|
1982
|
+
current_function_call_count += 1
|
|
1983
|
+
# We have reached the function call limit, so we add an error result to the function call results
|
|
1984
|
+
if current_function_call_count > function_call_limit:
|
|
1985
|
+
function_call_results.append(self.create_tool_call_limit_error_result(fc))
|
|
1986
|
+
continue
|
|
1987
|
+
|
|
1988
|
+
paused_tool_executions = []
|
|
1989
|
+
|
|
1990
|
+
# The function requires user confirmation (HITL)
|
|
1991
|
+
if fc.function.requires_confirmation:
|
|
1992
|
+
paused_tool_executions.append(
|
|
1993
|
+
ToolExecution(
|
|
1994
|
+
tool_call_id=fc.call_id,
|
|
1995
|
+
tool_name=fc.function.name,
|
|
1996
|
+
tool_args=fc.arguments,
|
|
1997
|
+
requires_confirmation=True,
|
|
392
1998
|
)
|
|
393
|
-
|
|
394
|
-
event=ModelResponseEvent.tool_call_completed.value,
|
|
395
|
-
)
|
|
1999
|
+
)
|
|
396
2000
|
|
|
397
|
-
#
|
|
398
|
-
if
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
2001
|
+
# The function requires user input (HITL)
|
|
2002
|
+
if fc.function.requires_user_input:
|
|
2003
|
+
user_input_schema = fc.function.user_input_schema
|
|
2004
|
+
if fc.arguments and user_input_schema:
|
|
2005
|
+
for name, value in fc.arguments.items():
|
|
2006
|
+
for user_input_field in user_input_schema:
|
|
2007
|
+
if user_input_field.name == name:
|
|
2008
|
+
user_input_field.value = value
|
|
2009
|
+
|
|
2010
|
+
paused_tool_executions.append(
|
|
2011
|
+
ToolExecution(
|
|
2012
|
+
tool_call_id=fc.call_id,
|
|
2013
|
+
tool_name=fc.function.name,
|
|
2014
|
+
tool_args=fc.arguments,
|
|
2015
|
+
requires_user_input=True,
|
|
2016
|
+
user_input_schema=user_input_schema,
|
|
2017
|
+
)
|
|
2018
|
+
)
|
|
403
2019
|
|
|
404
|
-
#
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if model_response.content is None:
|
|
421
|
-
model_response.content = ""
|
|
422
|
-
model_response.content += response_after_tool_calls.content
|
|
423
|
-
if response_after_tool_calls.parsed is not None:
|
|
424
|
-
# bubble up the parsed object, so that the final response has the parsed object
|
|
425
|
-
# that is visible to the agent
|
|
426
|
-
model_response.parsed = response_after_tool_calls.parsed
|
|
427
|
-
if response_after_tool_calls.audio is not None:
|
|
428
|
-
# bubble up the audio, so that the final response has the audio
|
|
429
|
-
# that is visible to the agent
|
|
430
|
-
model_response.audio = response_after_tool_calls.audio
|
|
431
|
-
|
|
432
|
-
def _handle_stop_after_tool_calls(self, last_message: Message, model_response: ModelResponse):
|
|
433
|
-
logger.debug("Stopping execution as stop_after_tool_call=True")
|
|
434
|
-
if (
|
|
435
|
-
last_message.role == "assistant"
|
|
436
|
-
and last_message.content is not None
|
|
437
|
-
and isinstance(last_message.content, str)
|
|
438
|
-
):
|
|
439
|
-
if model_response.content is None:
|
|
440
|
-
model_response.content = ""
|
|
441
|
-
model_response.content += last_message.content
|
|
2020
|
+
# If the function is from the user control flow (HITL) tools, we handle it here
|
|
2021
|
+
if fc.function.name == "get_user_input" and fc.arguments and fc.arguments.get("user_input_fields"):
|
|
2022
|
+
user_input_schema = []
|
|
2023
|
+
for input_field in fc.arguments.get("user_input_fields", []):
|
|
2024
|
+
field_type = input_field.get("field_type")
|
|
2025
|
+
try:
|
|
2026
|
+
python_type = eval(field_type) if isinstance(field_type, str) else field_type
|
|
2027
|
+
except (NameError, SyntaxError):
|
|
2028
|
+
python_type = str # Default to str if type is invalid
|
|
2029
|
+
user_input_schema.append(
|
|
2030
|
+
UserInputField(
|
|
2031
|
+
name=input_field.get("field_name"),
|
|
2032
|
+
field_type=python_type,
|
|
2033
|
+
description=input_field.get("field_description"),
|
|
2034
|
+
)
|
|
2035
|
+
)
|
|
442
2036
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
2037
|
+
paused_tool_executions.append(
|
|
2038
|
+
ToolExecution(
|
|
2039
|
+
tool_call_id=fc.call_id,
|
|
2040
|
+
tool_name=fc.function.name,
|
|
2041
|
+
tool_args=fc.arguments,
|
|
2042
|
+
requires_user_input=True,
|
|
2043
|
+
user_input_schema=user_input_schema,
|
|
2044
|
+
)
|
|
2045
|
+
)
|
|
451
2046
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
2047
|
+
# The function requires external execution (HITL)
|
|
2048
|
+
if fc.function.external_execution:
|
|
2049
|
+
paused_tool_executions.append(
|
|
2050
|
+
ToolExecution(
|
|
2051
|
+
tool_call_id=fc.call_id,
|
|
2052
|
+
tool_name=fc.function.name,
|
|
2053
|
+
tool_args=fc.arguments,
|
|
2054
|
+
external_execution_required=True,
|
|
2055
|
+
)
|
|
2056
|
+
)
|
|
2057
|
+
|
|
2058
|
+
if paused_tool_executions:
|
|
2059
|
+
yield ModelResponse(
|
|
2060
|
+
tool_executions=paused_tool_executions,
|
|
2061
|
+
event=ModelResponseEvent.tool_call_paused.value,
|
|
2062
|
+
)
|
|
2063
|
+
# We don't execute the function calls here
|
|
2064
|
+
continue
|
|
2065
|
+
|
|
2066
|
+
yield from self.run_function_call(
|
|
2067
|
+
function_call=fc, function_call_results=function_call_results, additional_input=additional_input
|
|
2068
|
+
)
|
|
2069
|
+
|
|
2070
|
+
# Add any additional messages at the end
|
|
2071
|
+
if additional_input:
|
|
2072
|
+
function_call_results.extend(additional_input)
|
|
2073
|
+
|
|
2074
|
+
async def arun_function_call(
|
|
2075
|
+
self,
|
|
2076
|
+
function_call: FunctionCall,
|
|
2077
|
+
) -> Tuple[Union[bool, AgentRunException], Timer, FunctionCall, FunctionExecutionResult]:
|
|
2078
|
+
"""Run a single function call and return its success status, timer, and the FunctionCall object."""
|
|
2079
|
+
from inspect import isasyncgenfunction, iscoroutine, iscoroutinefunction
|
|
462
2080
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
2081
|
+
function_call_timer = Timer()
|
|
2082
|
+
function_call_timer.start()
|
|
2083
|
+
success: Union[bool, AgentRunException] = False
|
|
2084
|
+
result: FunctionExecutionResult = FunctionExecutionResult(status="failure")
|
|
2085
|
+
|
|
2086
|
+
try:
|
|
467
2087
|
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
2088
|
+
iscoroutinefunction(function_call.function.entrypoint)
|
|
2089
|
+
or isasyncgenfunction(function_call.function.entrypoint)
|
|
2090
|
+
or iscoroutine(function_call.function.entrypoint)
|
|
471
2091
|
):
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
yield from self.response_stream(messages=messages)
|
|
2092
|
+
result = await function_call.aexecute()
|
|
2093
|
+
success = result.status == "success"
|
|
475
2094
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
2095
|
+
# If any of the hooks are async, we need to run the function call asynchronously
|
|
2096
|
+
elif function_call.function.tool_hooks is not None and any(
|
|
2097
|
+
iscoroutinefunction(f) for f in function_call.function.tool_hooks
|
|
2098
|
+
):
|
|
2099
|
+
result = await function_call.aexecute()
|
|
2100
|
+
success = result.status == "success"
|
|
2101
|
+
else:
|
|
2102
|
+
result = await asyncio.to_thread(function_call.execute)
|
|
2103
|
+
success = result.status == "success"
|
|
2104
|
+
except AgentRunException as e:
|
|
2105
|
+
success = e
|
|
2106
|
+
except Exception as e:
|
|
2107
|
+
log_error(f"Error executing function {function_call.function.name}: {e}")
|
|
2108
|
+
success = False
|
|
2109
|
+
raise e
|
|
2110
|
+
|
|
2111
|
+
function_call_timer.stop()
|
|
2112
|
+
return success, function_call_timer, function_call, result
|
|
2113
|
+
|
|
2114
|
+
async def arun_function_calls(
|
|
2115
|
+
self,
|
|
2116
|
+
function_calls: List[FunctionCall],
|
|
2117
|
+
function_call_results: List[Message],
|
|
2118
|
+
additional_input: Optional[List[Message]] = None,
|
|
2119
|
+
current_function_call_count: int = 0,
|
|
2120
|
+
function_call_limit: Optional[int] = None,
|
|
2121
|
+
skip_pause_check: bool = False,
|
|
2122
|
+
) -> AsyncIterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
|
|
2123
|
+
# Additional messages from function calls that will be added to the function call results
|
|
2124
|
+
if additional_input is None:
|
|
2125
|
+
additional_input = []
|
|
2126
|
+
|
|
2127
|
+
function_calls_to_run = []
|
|
2128
|
+
for fc in function_calls:
|
|
2129
|
+
if function_call_limit is not None:
|
|
2130
|
+
current_function_call_count += 1
|
|
2131
|
+
# We have reached the function call limit, so we add an error result to the function call results
|
|
2132
|
+
if current_function_call_count > function_call_limit:
|
|
2133
|
+
function_call_results.append(self.create_tool_call_limit_error_result(fc))
|
|
2134
|
+
# Skip this function call
|
|
2135
|
+
continue
|
|
2136
|
+
function_calls_to_run.append(fc)
|
|
2137
|
+
|
|
2138
|
+
# Yield tool_call_started events for all function calls or pause them
|
|
2139
|
+
for fc in function_calls_to_run:
|
|
2140
|
+
paused_tool_executions = []
|
|
2141
|
+
# The function cannot be executed without user confirmation
|
|
2142
|
+
if fc.function.requires_confirmation and not skip_pause_check:
|
|
2143
|
+
paused_tool_executions.append(
|
|
2144
|
+
ToolExecution(
|
|
2145
|
+
tool_call_id=fc.call_id,
|
|
2146
|
+
tool_name=fc.function.name,
|
|
2147
|
+
tool_args=fc.arguments,
|
|
2148
|
+
requires_confirmation=True,
|
|
2149
|
+
)
|
|
2150
|
+
)
|
|
2151
|
+
# If the function requires user input, we yield a message to the user
|
|
2152
|
+
if fc.function.requires_user_input and not skip_pause_check:
|
|
2153
|
+
user_input_schema = fc.function.user_input_schema
|
|
2154
|
+
if fc.arguments and user_input_schema:
|
|
2155
|
+
for name, value in fc.arguments.items():
|
|
2156
|
+
for user_input_field in user_input_schema:
|
|
2157
|
+
if user_input_field.name == name:
|
|
2158
|
+
user_input_field.value = value
|
|
2159
|
+
|
|
2160
|
+
paused_tool_executions.append(
|
|
2161
|
+
ToolExecution(
|
|
2162
|
+
tool_call_id=fc.call_id,
|
|
2163
|
+
tool_name=fc.function.name,
|
|
2164
|
+
tool_args=fc.arguments,
|
|
2165
|
+
requires_user_input=True,
|
|
2166
|
+
user_input_schema=user_input_schema,
|
|
2167
|
+
)
|
|
2168
|
+
)
|
|
2169
|
+
# If the function is from the user control flow tools, we handle it here
|
|
480
2170
|
if (
|
|
481
|
-
|
|
482
|
-
and
|
|
483
|
-
and
|
|
2171
|
+
fc.function.name == "get_user_input"
|
|
2172
|
+
and fc.arguments
|
|
2173
|
+
and fc.arguments.get("user_input_fields")
|
|
2174
|
+
and not skip_pause_check
|
|
484
2175
|
):
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
2176
|
+
fc.function.requires_user_input = True
|
|
2177
|
+
user_input_schema = []
|
|
2178
|
+
for input_field in fc.arguments.get("user_input_fields", []):
|
|
2179
|
+
field_type = input_field.get("field_type")
|
|
2180
|
+
try:
|
|
2181
|
+
python_type = eval(field_type) if isinstance(field_type, str) else field_type
|
|
2182
|
+
except (NameError, SyntaxError):
|
|
2183
|
+
python_type = str # Default to str if type is invalid
|
|
2184
|
+
user_input_schema.append(
|
|
2185
|
+
UserInputField(
|
|
2186
|
+
name=input_field.get("field_name"),
|
|
2187
|
+
field_type=python_type,
|
|
2188
|
+
description=input_field.get("field_description"),
|
|
2189
|
+
)
|
|
2190
|
+
)
|
|
489
2191
|
|
|
490
|
-
|
|
491
|
-
|
|
2192
|
+
paused_tool_executions.append(
|
|
2193
|
+
ToolExecution(
|
|
2194
|
+
tool_call_id=fc.call_id,
|
|
2195
|
+
tool_name=fc.function.name,
|
|
2196
|
+
tool_args=fc.arguments,
|
|
2197
|
+
requires_user_input=True,
|
|
2198
|
+
user_input_schema=user_input_schema,
|
|
2199
|
+
)
|
|
2200
|
+
)
|
|
2201
|
+
# If the function requires external execution, we yield a message to the user
|
|
2202
|
+
if fc.function.external_execution and not skip_pause_check:
|
|
2203
|
+
paused_tool_executions.append(
|
|
2204
|
+
ToolExecution(
|
|
2205
|
+
tool_call_id=fc.call_id,
|
|
2206
|
+
tool_name=fc.function.name,
|
|
2207
|
+
tool_args=fc.arguments,
|
|
2208
|
+
external_execution_required=True,
|
|
2209
|
+
)
|
|
2210
|
+
)
|
|
492
2211
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
2212
|
+
if paused_tool_executions:
|
|
2213
|
+
yield ModelResponse(
|
|
2214
|
+
tool_executions=paused_tool_executions,
|
|
2215
|
+
event=ModelResponseEvent.tool_call_paused.value,
|
|
2216
|
+
)
|
|
2217
|
+
# We don't execute the function calls here
|
|
2218
|
+
continue
|
|
497
2219
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
2220
|
+
yield ModelResponse(
|
|
2221
|
+
content=fc.get_call_str(),
|
|
2222
|
+
tool_executions=[
|
|
2223
|
+
ToolExecution(
|
|
2224
|
+
tool_call_id=fc.call_id,
|
|
2225
|
+
tool_name=fc.function.name,
|
|
2226
|
+
tool_args=fc.arguments,
|
|
2227
|
+
)
|
|
2228
|
+
],
|
|
2229
|
+
event=ModelResponseEvent.tool_call_started.value,
|
|
2230
|
+
)
|
|
503
2231
|
|
|
504
|
-
|
|
505
|
-
if
|
|
506
|
-
|
|
2232
|
+
# Create and run all function calls in parallel (skip ones that need confirmation)
|
|
2233
|
+
if skip_pause_check:
|
|
2234
|
+
function_calls_to_run = function_calls_to_run
|
|
2235
|
+
else:
|
|
2236
|
+
function_calls_to_run = [
|
|
2237
|
+
fc
|
|
2238
|
+
for fc in function_calls_to_run
|
|
2239
|
+
if not (
|
|
2240
|
+
fc.function.requires_confirmation
|
|
2241
|
+
or fc.function.external_execution
|
|
2242
|
+
or fc.function.requires_user_input
|
|
2243
|
+
)
|
|
2244
|
+
]
|
|
507
2245
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
image_url = f"data:{mime_type};base64,{base64_image}"
|
|
512
|
-
return {"type": "image_url", "image_url": {"url": image_url}}
|
|
2246
|
+
results = await asyncio.gather(
|
|
2247
|
+
*(self.arun_function_call(fc) for fc in function_calls_to_run), return_exceptions=True
|
|
2248
|
+
)
|
|
513
2249
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
2250
|
+
# Separate async generators from other results for concurrent processing
|
|
2251
|
+
async_generator_results: List[Any] = []
|
|
2252
|
+
non_async_generator_results: List[Any] = []
|
|
517
2253
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
2254
|
+
for result in results:
|
|
2255
|
+
if isinstance(result, BaseException):
|
|
2256
|
+
non_async_generator_results.append(result)
|
|
2257
|
+
continue
|
|
521
2258
|
|
|
522
|
-
|
|
523
|
-
"""Process an image based on the format."""
|
|
2259
|
+
function_call_success, function_call_timer, function_call, function_execution_result = result
|
|
524
2260
|
|
|
525
|
-
|
|
526
|
-
|
|
2261
|
+
# Check if this result contains an async generator
|
|
2262
|
+
if isinstance(function_call.result, (AsyncGeneratorType, AsyncIterator)):
|
|
2263
|
+
async_generator_results.append(result)
|
|
2264
|
+
else:
|
|
2265
|
+
non_async_generator_results.append(result)
|
|
527
2266
|
|
|
528
|
-
|
|
529
|
-
|
|
2267
|
+
# Process async generators with real-time event streaming using asyncio.Queue
|
|
2268
|
+
async_generator_outputs: Dict[int, Tuple[Any, str, Optional[BaseException]]] = {}
|
|
2269
|
+
event_queue: asyncio.Queue = asyncio.Queue()
|
|
2270
|
+
active_generators_count: int = len(async_generator_results)
|
|
530
2271
|
|
|
531
|
-
|
|
532
|
-
|
|
2272
|
+
# Create background tasks for each async generator
|
|
2273
|
+
async def process_async_generator(result, generator_id):
|
|
2274
|
+
function_call_success, function_call_timer, function_call, function_execution_result = result
|
|
2275
|
+
function_call_output = ""
|
|
533
2276
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
2277
|
+
try:
|
|
2278
|
+
async for item in function_call.result:
|
|
2279
|
+
# This function yields agent/team/workflow run events
|
|
2280
|
+
if isinstance(
|
|
2281
|
+
item,
|
|
2282
|
+
tuple(get_args(RunOutputEvent))
|
|
2283
|
+
+ tuple(get_args(TeamRunOutputEvent))
|
|
2284
|
+
+ tuple(get_args(WorkflowRunOutputEvent)),
|
|
2285
|
+
):
|
|
2286
|
+
# We only capture content events
|
|
2287
|
+
if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
|
|
2288
|
+
if item.content is not None and isinstance(item.content, BaseModel):
|
|
2289
|
+
function_call_output += item.content.model_dump_json()
|
|
2290
|
+
else:
|
|
2291
|
+
# Capture output
|
|
2292
|
+
function_call_output += item.content or ""
|
|
2293
|
+
|
|
2294
|
+
if function_call.function.show_result and item.content is not None:
|
|
2295
|
+
await event_queue.put(ModelResponse(content=item.content))
|
|
2296
|
+
continue
|
|
2297
|
+
|
|
2298
|
+
if isinstance(item, CustomEvent):
|
|
2299
|
+
function_call_output += str(item)
|
|
2300
|
+
|
|
2301
|
+
# For WorkflowCompletedEvent, extract content for final output
|
|
2302
|
+
from agno.run.workflow import WorkflowCompletedEvent
|
|
2303
|
+
|
|
2304
|
+
if isinstance(item, WorkflowCompletedEvent):
|
|
2305
|
+
if item.content is not None:
|
|
2306
|
+
if isinstance(item.content, BaseModel):
|
|
2307
|
+
function_call_output += item.content.model_dump_json()
|
|
2308
|
+
else:
|
|
2309
|
+
function_call_output += str(item.content)
|
|
2310
|
+
|
|
2311
|
+
# Put the event into the queue to be yielded
|
|
2312
|
+
await event_queue.put(item)
|
|
2313
|
+
|
|
2314
|
+
# Yield custom events emitted by the tool
|
|
2315
|
+
else:
|
|
2316
|
+
function_call_output += str(item)
|
|
2317
|
+
if function_call.function.show_result and item is not None:
|
|
2318
|
+
await event_queue.put(ModelResponse(content=str(item)))
|
|
537
2319
|
|
|
538
|
-
|
|
539
|
-
|
|
2320
|
+
# Store the final output for this generator
|
|
2321
|
+
async_generator_outputs[generator_id] = (result, function_call_output, None)
|
|
540
2322
|
|
|
541
|
-
|
|
2323
|
+
except Exception as e:
|
|
2324
|
+
# Store the exception
|
|
2325
|
+
async_generator_outputs[generator_id] = (result, "", e)
|
|
542
2326
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
Add images to a message for the model. By default, we use the OpenAI image format but other Models
|
|
546
|
-
can override this method to use a different image format.
|
|
2327
|
+
# Signal that this generator is done
|
|
2328
|
+
await event_queue.put(("GENERATOR_DONE", generator_id))
|
|
547
2329
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
- bytes: raw image data
|
|
2330
|
+
# Start all async generator tasks
|
|
2331
|
+
generator_tasks = []
|
|
2332
|
+
for i, result in enumerate(async_generator_results):
|
|
2333
|
+
task = asyncio.create_task(process_async_generator(result, i))
|
|
2334
|
+
generator_tasks.append(task)
|
|
554
2335
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return message
|
|
2336
|
+
# Stream events from the queue as they arrive
|
|
2337
|
+
completed_generators_count = 0
|
|
2338
|
+
while completed_generators_count < active_generators_count:
|
|
2339
|
+
try:
|
|
2340
|
+
event = await event_queue.get()
|
|
561
2341
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
2342
|
+
# Check if this is a completion signal
|
|
2343
|
+
if isinstance(event, tuple) and event[0] == "GENERATOR_DONE":
|
|
2344
|
+
completed_generators_count += 1
|
|
2345
|
+
continue
|
|
566
2346
|
|
|
567
|
-
|
|
568
|
-
|
|
2347
|
+
# Yield the actual event
|
|
2348
|
+
yield event
|
|
569
2349
|
|
|
570
|
-
# Add images to the message content
|
|
571
|
-
for image in images:
|
|
572
|
-
try:
|
|
573
|
-
image_data = self._process_image(image)
|
|
574
|
-
if image_data:
|
|
575
|
-
message_content_with_image.append(image_data)
|
|
576
2350
|
except Exception as e:
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
#
|
|
581
|
-
|
|
582
|
-
|
|
2351
|
+
log_error(f"Error processing async generator event: {e}")
|
|
2352
|
+
break
|
|
2353
|
+
|
|
2354
|
+
# Now process all results (non-async generators and completed async generators)
|
|
2355
|
+
for i, original_result in enumerate(results):
|
|
2356
|
+
# If result is an exception, skip processing it
|
|
2357
|
+
if isinstance(original_result, BaseException):
|
|
2358
|
+
log_error(f"Error during function call: {original_result}")
|
|
2359
|
+
raise original_result
|
|
2360
|
+
|
|
2361
|
+
# Unpack result
|
|
2362
|
+
function_call_success, function_call_timer, function_call, function_execution_result = original_result
|
|
2363
|
+
|
|
2364
|
+
# Check if this was an async generator that was already processed
|
|
2365
|
+
async_function_call_output = None
|
|
2366
|
+
if isinstance(function_call.result, (AsyncGeneratorType, collections.abc.AsyncIterator)):
|
|
2367
|
+
# Find the corresponding processed result
|
|
2368
|
+
async_gen_index = 0
|
|
2369
|
+
for j, result in enumerate(results[: i + 1]):
|
|
2370
|
+
if not isinstance(result, BaseException):
|
|
2371
|
+
_, _, fc, _ = result
|
|
2372
|
+
if isinstance(fc.result, (AsyncGeneratorType, collections.abc.AsyncIterator)):
|
|
2373
|
+
if j == i: # This is our async generator
|
|
2374
|
+
if async_gen_index in async_generator_outputs:
|
|
2375
|
+
_, async_function_call_output, error = async_generator_outputs[async_gen_index]
|
|
2376
|
+
if error:
|
|
2377
|
+
log_error(f"Error in async generator: {error}")
|
|
2378
|
+
raise error
|
|
2379
|
+
break
|
|
2380
|
+
async_gen_index += 1
|
|
2381
|
+
|
|
2382
|
+
updated_session_state = function_execution_result.updated_session_state
|
|
2383
|
+
|
|
2384
|
+
# Handle AgentRunException
|
|
2385
|
+
stop_after_tool_call_from_exception = False
|
|
2386
|
+
if isinstance(function_call_success, AgentRunException):
|
|
2387
|
+
a_exc = function_call_success
|
|
2388
|
+
# Update additional messages from function call
|
|
2389
|
+
_handle_agent_exception(a_exc, additional_input)
|
|
2390
|
+
# If stop_execution is True, mark that we should stop after this tool call
|
|
2391
|
+
if a_exc.stop_execution:
|
|
2392
|
+
stop_after_tool_call_from_exception = True
|
|
2393
|
+
# Set function call success to False if an exception occurred
|
|
2394
|
+
function_call_success = False
|
|
2395
|
+
|
|
2396
|
+
# Process function call output
|
|
2397
|
+
function_call_output: str = ""
|
|
2398
|
+
|
|
2399
|
+
# Check if this was an async generator that was already processed
|
|
2400
|
+
if async_function_call_output is not None:
|
|
2401
|
+
function_call_output = async_function_call_output
|
|
2402
|
+
# Events from async generators were already yielded in real-time above
|
|
2403
|
+
elif isinstance(function_call.result, (GeneratorType, collections.abc.Iterator)):
|
|
2404
|
+
try:
|
|
2405
|
+
for item in function_call.result:
|
|
2406
|
+
# This function yields agent/team/workflow run events
|
|
2407
|
+
if isinstance(
|
|
2408
|
+
item,
|
|
2409
|
+
tuple(get_args(RunOutputEvent))
|
|
2410
|
+
+ tuple(get_args(TeamRunOutputEvent))
|
|
2411
|
+
+ tuple(get_args(WorkflowRunOutputEvent)),
|
|
2412
|
+
):
|
|
2413
|
+
# We only capture content events
|
|
2414
|
+
if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
|
|
2415
|
+
if item.content is not None and isinstance(item.content, BaseModel):
|
|
2416
|
+
function_call_output += item.content.model_dump_json()
|
|
2417
|
+
else:
|
|
2418
|
+
# Capture output
|
|
2419
|
+
function_call_output += item.content or ""
|
|
2420
|
+
|
|
2421
|
+
if function_call.function.show_result and item.content is not None:
|
|
2422
|
+
yield ModelResponse(content=item.content)
|
|
2423
|
+
continue
|
|
2424
|
+
|
|
2425
|
+
# Yield the event itself to bubble it up
|
|
2426
|
+
yield item
|
|
2427
|
+
else:
|
|
2428
|
+
function_call_output += str(item)
|
|
2429
|
+
if function_call.function.show_result and item is not None:
|
|
2430
|
+
yield ModelResponse(content=str(item))
|
|
2431
|
+
except Exception as e:
|
|
2432
|
+
log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
|
|
2433
|
+
function_call.error = str(e)
|
|
2434
|
+
function_call_success = False
|
|
2435
|
+
|
|
2436
|
+
# For generators (sync or async), re-capture updated_session_state after consumption
|
|
2437
|
+
# since session_state modifications were made during iteration
|
|
2438
|
+
if async_function_call_output is not None or isinstance(
|
|
2439
|
+
function_call.result,
|
|
2440
|
+
(GeneratorType, collections.abc.Iterator, AsyncGeneratorType, collections.abc.AsyncIterator),
|
|
2441
|
+
):
|
|
2442
|
+
if updated_session_state is None:
|
|
2443
|
+
if (
|
|
2444
|
+
function_call.function._run_context is not None
|
|
2445
|
+
and function_call.function._run_context.session_state is not None
|
|
2446
|
+
):
|
|
2447
|
+
updated_session_state = function_call.function._run_context.session_state
|
|
2448
|
+
elif function_call.function._session_state is not None:
|
|
2449
|
+
updated_session_state = function_call.function._session_state
|
|
2450
|
+
|
|
2451
|
+
if not (
|
|
2452
|
+
async_function_call_output is not None
|
|
2453
|
+
or isinstance(
|
|
2454
|
+
function_call.result,
|
|
2455
|
+
(GeneratorType, collections.abc.Iterator, AsyncGeneratorType, collections.abc.AsyncIterator),
|
|
2456
|
+
)
|
|
2457
|
+
):
|
|
2458
|
+
from agno.tools.function import ToolResult
|
|
2459
|
+
|
|
2460
|
+
if isinstance(function_execution_result.result, ToolResult):
|
|
2461
|
+
tool_result = function_execution_result.result
|
|
2462
|
+
function_call_output = tool_result.content
|
|
2463
|
+
|
|
2464
|
+
if tool_result.images:
|
|
2465
|
+
function_execution_result.images = tool_result.images
|
|
2466
|
+
if tool_result.videos:
|
|
2467
|
+
function_execution_result.videos = tool_result.videos
|
|
2468
|
+
if tool_result.audios:
|
|
2469
|
+
function_execution_result.audios = tool_result.audios
|
|
2470
|
+
if tool_result.files:
|
|
2471
|
+
function_execution_result.files = tool_result.files
|
|
2472
|
+
else:
|
|
2473
|
+
function_call_output = str(function_call.result)
|
|
2474
|
+
|
|
2475
|
+
if function_call.function.show_result and function_call_output is not None:
|
|
2476
|
+
yield ModelResponse(content=function_call_output)
|
|
583
2477
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
2478
|
+
# Create and yield function call result
|
|
2479
|
+
function_call_result = self.create_function_call_result(
|
|
2480
|
+
function_call,
|
|
2481
|
+
success=function_call_success,
|
|
2482
|
+
output=function_call_output,
|
|
2483
|
+
timer=function_call_timer,
|
|
2484
|
+
function_execution_result=function_execution_result,
|
|
2485
|
+
)
|
|
2486
|
+
# Override stop_after_tool_call if set by exception
|
|
2487
|
+
if stop_after_tool_call_from_exception:
|
|
2488
|
+
function_call_result.stop_after_tool_call = True
|
|
2489
|
+
yield ModelResponse(
|
|
2490
|
+
content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s. ",
|
|
2491
|
+
tool_executions=[
|
|
2492
|
+
ToolExecution(
|
|
2493
|
+
tool_call_id=function_call_result.tool_call_id,
|
|
2494
|
+
tool_name=function_call_result.tool_name,
|
|
2495
|
+
tool_args=function_call_result.tool_args,
|
|
2496
|
+
tool_call_error=function_call_result.tool_call_error,
|
|
2497
|
+
result=str(function_call_result.content),
|
|
2498
|
+
stop_after_tool_call=function_call_result.stop_after_tool_call,
|
|
2499
|
+
metrics=function_call_result.metrics,
|
|
2500
|
+
)
|
|
2501
|
+
],
|
|
2502
|
+
event=ModelResponseEvent.tool_call_completed.value,
|
|
2503
|
+
updated_session_state=updated_session_state,
|
|
2504
|
+
images=function_execution_result.images,
|
|
2505
|
+
videos=function_execution_result.videos,
|
|
2506
|
+
audios=function_execution_result.audios,
|
|
2507
|
+
files=function_execution_result.files,
|
|
2508
|
+
)
|
|
589
2509
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
audio: Pre-formatted audio data like {
|
|
593
|
-
"content": encoded_string,
|
|
594
|
-
"format": "wav"
|
|
595
|
-
}
|
|
2510
|
+
# Add function call result to function call results
|
|
2511
|
+
function_call_results.append(function_call_result)
|
|
596
2512
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if len(audio) == 0:
|
|
601
|
-
return message
|
|
602
|
-
|
|
603
|
-
# Create a default message content with text
|
|
604
|
-
message_content_with_audio: List[Dict[str, Any]] = [{"type": "text", "text": message.content}]
|
|
605
|
-
|
|
606
|
-
for audio_snippet in audio:
|
|
607
|
-
# This means the audio is raw data
|
|
608
|
-
if audio_snippet.content:
|
|
609
|
-
import base64
|
|
610
|
-
|
|
611
|
-
encoded_string = base64.b64encode(audio_snippet.content).decode("utf-8")
|
|
612
|
-
|
|
613
|
-
# Create a message with audio
|
|
614
|
-
message_content_with_audio.append(
|
|
615
|
-
{
|
|
616
|
-
"type": "input_audio",
|
|
617
|
-
"input_audio": {
|
|
618
|
-
"data": encoded_string,
|
|
619
|
-
"format": audio_snippet.format,
|
|
620
|
-
},
|
|
621
|
-
},
|
|
622
|
-
)
|
|
2513
|
+
# Add any additional messages at the end
|
|
2514
|
+
if additional_input:
|
|
2515
|
+
function_call_results.extend(additional_input)
|
|
623
2516
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
2517
|
+
def _prepare_function_calls(
|
|
2518
|
+
self,
|
|
2519
|
+
assistant_message: Message,
|
|
2520
|
+
messages: List[Message],
|
|
2521
|
+
model_response: ModelResponse,
|
|
2522
|
+
functions: Optional[Dict[str, Function]] = None,
|
|
2523
|
+
) -> List[FunctionCall]:
|
|
2524
|
+
"""
|
|
2525
|
+
Prepare function calls from tool calls in the assistant message.
|
|
2526
|
+
"""
|
|
2527
|
+
if model_response.content is None:
|
|
2528
|
+
model_response.content = ""
|
|
2529
|
+
if model_response.tool_calls is None:
|
|
2530
|
+
model_response.tool_calls = []
|
|
627
2531
|
|
|
628
|
-
|
|
2532
|
+
function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
|
|
2533
|
+
assistant_message=assistant_message, messages=messages, functions=functions
|
|
2534
|
+
)
|
|
2535
|
+
return function_calls_to_run
|
|
629
2536
|
|
|
630
|
-
|
|
631
|
-
|
|
2537
|
+
def format_function_call_results(
|
|
2538
|
+
self,
|
|
2539
|
+
messages: List[Message],
|
|
2540
|
+
function_call_results: List[Message],
|
|
2541
|
+
compress_tool_results: bool = False,
|
|
2542
|
+
**kwargs,
|
|
2543
|
+
) -> None:
|
|
632
2544
|
"""
|
|
633
|
-
|
|
2545
|
+
Format function call results.
|
|
2546
|
+
"""
|
|
2547
|
+
if len(function_call_results) > 0:
|
|
2548
|
+
messages.extend(function_call_results)
|
|
634
2549
|
|
|
635
|
-
|
|
636
|
-
|
|
2550
|
+
def _handle_function_call_media(
|
|
2551
|
+
self, messages: List[Message], function_call_results: List[Message], send_media_to_model: bool = True
|
|
2552
|
+
) -> None:
|
|
2553
|
+
"""
|
|
2554
|
+
Handle media artifacts from function calls by adding follow-up user messages for generated media if needed.
|
|
2555
|
+
"""
|
|
2556
|
+
if not function_call_results:
|
|
2557
|
+
return
|
|
2558
|
+
|
|
2559
|
+
# Collect all media artifacts from function calls
|
|
2560
|
+
all_images: List[Image] = []
|
|
2561
|
+
all_videos: List[Video] = []
|
|
2562
|
+
all_audio: List[Audio] = []
|
|
2563
|
+
all_files: List[File] = []
|
|
2564
|
+
|
|
2565
|
+
for result_message in function_call_results:
|
|
2566
|
+
if result_message.images:
|
|
2567
|
+
all_images.extend(result_message.images)
|
|
2568
|
+
# Remove images from tool message to avoid errors on the LLMs
|
|
2569
|
+
result_message.images = None
|
|
2570
|
+
|
|
2571
|
+
if result_message.videos:
|
|
2572
|
+
all_videos.extend(result_message.videos)
|
|
2573
|
+
result_message.videos = None
|
|
2574
|
+
|
|
2575
|
+
if result_message.audio:
|
|
2576
|
+
all_audio.extend(result_message.audio)
|
|
2577
|
+
result_message.audio = None
|
|
2578
|
+
|
|
2579
|
+
if result_message.files:
|
|
2580
|
+
all_files.extend(result_message.files)
|
|
2581
|
+
result_message.files = None
|
|
2582
|
+
|
|
2583
|
+
# Only add media message if we should send media to model
|
|
2584
|
+
if send_media_to_model and (all_images or all_videos or all_audio or all_files):
|
|
2585
|
+
# If we have media artifacts, add a follow-up "user" message instead of a "tool"
|
|
2586
|
+
# message with the media artifacts which throws error for some models
|
|
2587
|
+
media_message = Message(
|
|
2588
|
+
role="user",
|
|
2589
|
+
content="Take note of the following content",
|
|
2590
|
+
images=all_images if all_images else None,
|
|
2591
|
+
videos=all_videos if all_videos else None,
|
|
2592
|
+
audio=all_audio if all_audio else None,
|
|
2593
|
+
files=all_files if all_files else None,
|
|
2594
|
+
)
|
|
2595
|
+
messages.append(media_message)
|
|
637
2596
|
|
|
638
|
-
|
|
639
|
-
List[Dict[str, Any]]: The built tool calls.
|
|
640
|
-
"""
|
|
641
|
-
tool_calls: List[Dict[str, Any]] = []
|
|
642
|
-
for _tool_call in tool_calls_data:
|
|
643
|
-
_index = _tool_call.index
|
|
644
|
-
_tool_call_id = _tool_call.id
|
|
645
|
-
_tool_call_type = _tool_call.type
|
|
646
|
-
_function_name = _tool_call.function.name if _tool_call.function else None
|
|
647
|
-
_function_arguments = _tool_call.function.arguments if _tool_call.function else None
|
|
648
|
-
|
|
649
|
-
if len(tool_calls) <= _index:
|
|
650
|
-
tool_calls.extend([{}] * (_index - len(tool_calls) + 1))
|
|
651
|
-
tool_call_entry = tool_calls[_index]
|
|
652
|
-
if not tool_call_entry:
|
|
653
|
-
tool_call_entry["id"] = _tool_call_id
|
|
654
|
-
tool_call_entry["type"] = _tool_call_type
|
|
655
|
-
tool_call_entry["function"] = {
|
|
656
|
-
"name": _function_name or "",
|
|
657
|
-
"arguments": _function_arguments or "",
|
|
658
|
-
}
|
|
659
|
-
else:
|
|
660
|
-
if _function_name:
|
|
661
|
-
tool_call_entry["function"]["name"] += _function_name
|
|
662
|
-
if _function_arguments:
|
|
663
|
-
tool_call_entry["function"]["arguments"] += _function_arguments
|
|
664
|
-
if _tool_call_id:
|
|
665
|
-
tool_call_entry["id"] = _tool_call_id
|
|
666
|
-
if _tool_call_type:
|
|
667
|
-
tool_call_entry["type"] = _tool_call_type
|
|
668
|
-
return tool_calls
|
|
669
|
-
|
|
670
|
-
def get_system_message_for_model(self) -> Optional[str]:
|
|
2597
|
+
def get_system_message_for_model(self, tools: Optional[List[Any]] = None) -> Optional[str]:
|
|
671
2598
|
return self.system_prompt
|
|
672
2599
|
|
|
673
|
-
def get_instructions_for_model(self) -> Optional[List[str]]:
|
|
2600
|
+
def get_instructions_for_model(self, tools: Optional[List[Any]] = None) -> Optional[List[str]]:
|
|
674
2601
|
return self.instructions
|
|
675
2602
|
|
|
676
|
-
def clear(self) -> None:
|
|
677
|
-
"""Clears the Model's state."""
|
|
678
|
-
|
|
679
|
-
self.metrics = {}
|
|
680
|
-
self._functions = None
|
|
681
|
-
self._function_call_stack = None
|
|
682
|
-
self.session_id = None
|
|
683
|
-
|
|
684
2603
|
def __deepcopy__(self, memo):
|
|
685
2604
|
"""Create a deep copy of the Model instance.
|
|
686
2605
|
|
|
@@ -690,19 +2609,27 @@ class Model(ABC):
|
|
|
690
2609
|
Returns:
|
|
691
2610
|
Model: A new Model instance with deeply copied attributes.
|
|
692
2611
|
"""
|
|
693
|
-
from copy import deepcopy
|
|
2612
|
+
from copy import copy, deepcopy
|
|
694
2613
|
|
|
695
2614
|
# Create a new instance without calling __init__
|
|
696
2615
|
cls = self.__class__
|
|
697
2616
|
new_model = cls.__new__(cls)
|
|
698
2617
|
memo[id(self)] = new_model
|
|
699
2618
|
|
|
700
|
-
# Deep copy all attributes
|
|
2619
|
+
# Deep copy all attributes except client objects
|
|
701
2620
|
for k, v in self.__dict__.items():
|
|
702
|
-
if k in {"
|
|
2621
|
+
if k in {"response_format", "_tools", "_functions"}:
|
|
703
2622
|
continue
|
|
704
|
-
|
|
2623
|
+
# Skip client objects
|
|
2624
|
+
if k in {"client", "async_client", "http_client", "mistral_client", "model_client"}:
|
|
2625
|
+
setattr(new_model, k, None)
|
|
2626
|
+
continue
|
|
2627
|
+
try:
|
|
2628
|
+
setattr(new_model, k, deepcopy(v, memo))
|
|
2629
|
+
except Exception:
|
|
2630
|
+
try:
|
|
2631
|
+
setattr(new_model, k, copy(v))
|
|
2632
|
+
except Exception:
|
|
2633
|
+
setattr(new_model, k, v)
|
|
705
2634
|
|
|
706
|
-
# Clear the new model to remove any references to the old model
|
|
707
|
-
new_model.clear()
|
|
708
2635
|
return new_model
|