agno 2.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/__init__.py +8 -0
- agno/agent/__init__.py +51 -0
- agno/agent/agent.py +10405 -0
- agno/api/__init__.py +0 -0
- agno/api/agent.py +28 -0
- agno/api/api.py +40 -0
- agno/api/evals.py +22 -0
- agno/api/os.py +17 -0
- agno/api/routes.py +13 -0
- agno/api/schemas/__init__.py +9 -0
- agno/api/schemas/agent.py +16 -0
- agno/api/schemas/evals.py +16 -0
- agno/api/schemas/os.py +14 -0
- agno/api/schemas/response.py +6 -0
- agno/api/schemas/team.py +16 -0
- agno/api/schemas/utils.py +21 -0
- agno/api/schemas/workflows.py +16 -0
- agno/api/settings.py +53 -0
- agno/api/team.py +30 -0
- agno/api/workflow.py +28 -0
- agno/cloud/aws/base.py +214 -0
- agno/cloud/aws/s3/__init__.py +2 -0
- agno/cloud/aws/s3/api_client.py +43 -0
- agno/cloud/aws/s3/bucket.py +195 -0
- agno/cloud/aws/s3/object.py +57 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/__init__.py +24 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +598 -0
- agno/db/dynamo/__init__.py +3 -0
- agno/db/dynamo/dynamo.py +2042 -0
- agno/db/dynamo/schemas.py +314 -0
- agno/db/dynamo/utils.py +743 -0
- agno/db/firestore/__init__.py +3 -0
- agno/db/firestore/firestore.py +1795 -0
- agno/db/firestore/schemas.py +140 -0
- agno/db/firestore/utils.py +376 -0
- agno/db/gcs_json/__init__.py +3 -0
- agno/db/gcs_json/gcs_json_db.py +1335 -0
- agno/db/gcs_json/utils.py +228 -0
- agno/db/in_memory/__init__.py +3 -0
- agno/db/in_memory/in_memory_db.py +1160 -0
- agno/db/in_memory/utils.py +230 -0
- agno/db/json/__init__.py +3 -0
- agno/db/json/json_db.py +1328 -0
- agno/db/json/utils.py +230 -0
- agno/db/migrations/__init__.py +0 -0
- agno/db/migrations/v1_to_v2.py +635 -0
- agno/db/mongo/__init__.py +17 -0
- agno/db/mongo/async_mongo.py +2026 -0
- agno/db/mongo/mongo.py +1982 -0
- agno/db/mongo/schemas.py +87 -0
- agno/db/mongo/utils.py +259 -0
- agno/db/mysql/__init__.py +3 -0
- agno/db/mysql/mysql.py +2308 -0
- agno/db/mysql/schemas.py +138 -0
- agno/db/mysql/utils.py +355 -0
- agno/db/postgres/__init__.py +4 -0
- agno/db/postgres/async_postgres.py +1927 -0
- agno/db/postgres/postgres.py +2260 -0
- agno/db/postgres/schemas.py +139 -0
- agno/db/postgres/utils.py +442 -0
- agno/db/redis/__init__.py +3 -0
- agno/db/redis/redis.py +1660 -0
- agno/db/redis/schemas.py +123 -0
- agno/db/redis/utils.py +346 -0
- agno/db/schemas/__init__.py +4 -0
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +33 -0
- agno/db/schemas/knowledge.py +40 -0
- agno/db/schemas/memory.py +46 -0
- agno/db/schemas/metrics.py +0 -0
- agno/db/singlestore/__init__.py +3 -0
- agno/db/singlestore/schemas.py +130 -0
- agno/db/singlestore/singlestore.py +2272 -0
- agno/db/singlestore/utils.py +384 -0
- agno/db/sqlite/__init__.py +4 -0
- agno/db/sqlite/async_sqlite.py +2293 -0
- agno/db/sqlite/schemas.py +133 -0
- agno/db/sqlite/sqlite.py +2288 -0
- agno/db/sqlite/utils.py +431 -0
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +309 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1353 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +116 -0
- agno/debug.py +18 -0
- agno/eval/__init__.py +14 -0
- agno/eval/accuracy.py +834 -0
- agno/eval/performance.py +773 -0
- agno/eval/reliability.py +306 -0
- agno/eval/utils.py +119 -0
- agno/exceptions.py +161 -0
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/integrations/__init__.py +0 -0
- agno/integrations/discord/__init__.py +3 -0
- agno/integrations/discord/client.py +203 -0
- agno/knowledge/__init__.py +5 -0
- agno/knowledge/chunking/__init__.py +0 -0
- agno/knowledge/chunking/agentic.py +79 -0
- agno/knowledge/chunking/document.py +91 -0
- agno/knowledge/chunking/fixed.py +57 -0
- agno/knowledge/chunking/markdown.py +151 -0
- agno/knowledge/chunking/recursive.py +63 -0
- agno/knowledge/chunking/row.py +39 -0
- agno/knowledge/chunking/semantic.py +86 -0
- agno/knowledge/chunking/strategy.py +165 -0
- agno/knowledge/content.py +74 -0
- agno/knowledge/document/__init__.py +5 -0
- agno/knowledge/document/base.py +58 -0
- agno/knowledge/embedder/__init__.py +5 -0
- agno/knowledge/embedder/aws_bedrock.py +343 -0
- agno/knowledge/embedder/azure_openai.py +210 -0
- agno/knowledge/embedder/base.py +23 -0
- agno/knowledge/embedder/cohere.py +323 -0
- agno/knowledge/embedder/fastembed.py +62 -0
- agno/knowledge/embedder/fireworks.py +13 -0
- agno/knowledge/embedder/google.py +258 -0
- agno/knowledge/embedder/huggingface.py +94 -0
- agno/knowledge/embedder/jina.py +182 -0
- agno/knowledge/embedder/langdb.py +22 -0
- agno/knowledge/embedder/mistral.py +206 -0
- agno/knowledge/embedder/nebius.py +13 -0
- agno/knowledge/embedder/ollama.py +154 -0
- agno/knowledge/embedder/openai.py +195 -0
- agno/knowledge/embedder/sentence_transformer.py +63 -0
- agno/knowledge/embedder/together.py +13 -0
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +165 -0
- agno/knowledge/knowledge.py +1988 -0
- agno/knowledge/reader/__init__.py +7 -0
- agno/knowledge/reader/arxiv_reader.py +81 -0
- agno/knowledge/reader/base.py +95 -0
- agno/knowledge/reader/csv_reader.py +166 -0
- agno/knowledge/reader/docx_reader.py +82 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +292 -0
- agno/knowledge/reader/firecrawl_reader.py +201 -0
- agno/knowledge/reader/json_reader.py +87 -0
- agno/knowledge/reader/markdown_reader.py +137 -0
- agno/knowledge/reader/pdf_reader.py +431 -0
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +313 -0
- agno/knowledge/reader/s3_reader.py +89 -0
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/reader/text_reader.py +115 -0
- agno/knowledge/reader/web_search_reader.py +372 -0
- agno/knowledge/reader/website_reader.py +455 -0
- agno/knowledge/reader/wikipedia_reader.py +59 -0
- agno/knowledge/reader/youtube_reader.py +78 -0
- agno/knowledge/remote_content/__init__.py +0 -0
- agno/knowledge/remote_content/remote_content.py +88 -0
- agno/knowledge/reranker/__init__.py +3 -0
- agno/knowledge/reranker/base.py +14 -0
- agno/knowledge/reranker/cohere.py +64 -0
- agno/knowledge/reranker/infinity.py +195 -0
- agno/knowledge/reranker/sentence_transformer.py +54 -0
- agno/knowledge/types.py +39 -0
- agno/knowledge/utils.py +189 -0
- agno/media.py +462 -0
- agno/memory/__init__.py +3 -0
- agno/memory/manager.py +1327 -0
- agno/models/__init__.py +0 -0
- agno/models/aimlapi/__init__.py +5 -0
- agno/models/aimlapi/aimlapi.py +45 -0
- agno/models/anthropic/__init__.py +5 -0
- agno/models/anthropic/claude.py +757 -0
- agno/models/aws/__init__.py +15 -0
- agno/models/aws/bedrock.py +701 -0
- agno/models/aws/claude.py +378 -0
- agno/models/azure/__init__.py +18 -0
- agno/models/azure/ai_foundry.py +485 -0
- agno/models/azure/openai_chat.py +131 -0
- agno/models/base.py +2175 -0
- agno/models/cerebras/__init__.py +12 -0
- agno/models/cerebras/cerebras.py +501 -0
- agno/models/cerebras/cerebras_openai.py +112 -0
- agno/models/cohere/__init__.py +5 -0
- agno/models/cohere/chat.py +389 -0
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/dashscope/__init__.py +5 -0
- agno/models/dashscope/dashscope.py +91 -0
- agno/models/deepinfra/__init__.py +5 -0
- agno/models/deepinfra/deepinfra.py +28 -0
- agno/models/deepseek/__init__.py +5 -0
- agno/models/deepseek/deepseek.py +61 -0
- agno/models/defaults.py +1 -0
- agno/models/fireworks/__init__.py +5 -0
- agno/models/fireworks/fireworks.py +26 -0
- agno/models/google/__init__.py +5 -0
- agno/models/google/gemini.py +1085 -0
- agno/models/groq/__init__.py +5 -0
- agno/models/groq/groq.py +556 -0
- agno/models/huggingface/__init__.py +5 -0
- agno/models/huggingface/huggingface.py +491 -0
- agno/models/ibm/__init__.py +5 -0
- agno/models/ibm/watsonx.py +422 -0
- agno/models/internlm/__init__.py +3 -0
- agno/models/internlm/internlm.py +26 -0
- agno/models/langdb/__init__.py +1 -0
- agno/models/langdb/langdb.py +48 -0
- agno/models/litellm/__init__.py +14 -0
- agno/models/litellm/chat.py +468 -0
- agno/models/litellm/litellm_openai.py +25 -0
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/lmstudio/__init__.py +5 -0
- agno/models/lmstudio/lmstudio.py +25 -0
- agno/models/message.py +434 -0
- agno/models/meta/__init__.py +12 -0
- agno/models/meta/llama.py +475 -0
- agno/models/meta/llama_openai.py +78 -0
- agno/models/metrics.py +120 -0
- agno/models/mistral/__init__.py +5 -0
- agno/models/mistral/mistral.py +432 -0
- agno/models/nebius/__init__.py +3 -0
- agno/models/nebius/nebius.py +54 -0
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/__init__.py +5 -0
- agno/models/nvidia/nvidia.py +28 -0
- agno/models/ollama/__init__.py +5 -0
- agno/models/ollama/chat.py +441 -0
- agno/models/openai/__init__.py +9 -0
- agno/models/openai/chat.py +883 -0
- agno/models/openai/like.py +27 -0
- agno/models/openai/responses.py +1050 -0
- agno/models/openrouter/__init__.py +5 -0
- agno/models/openrouter/openrouter.py +66 -0
- agno/models/perplexity/__init__.py +5 -0
- agno/models/perplexity/perplexity.py +187 -0
- agno/models/portkey/__init__.py +3 -0
- agno/models/portkey/portkey.py +81 -0
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +52 -0
- agno/models/response.py +199 -0
- agno/models/sambanova/__init__.py +5 -0
- agno/models/sambanova/sambanova.py +28 -0
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/models/together/__init__.py +5 -0
- agno/models/together/together.py +25 -0
- agno/models/utils.py +266 -0
- agno/models/vercel/__init__.py +3 -0
- agno/models/vercel/v0.py +26 -0
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +70 -0
- agno/models/vllm/__init__.py +3 -0
- agno/models/vllm/vllm.py +78 -0
- agno/models/xai/__init__.py +3 -0
- agno/models/xai/xai.py +113 -0
- agno/os/__init__.py +3 -0
- agno/os/app.py +876 -0
- agno/os/auth.py +57 -0
- agno/os/config.py +104 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +250 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/__init__.py +3 -0
- agno/os/interfaces/agui/agui.py +47 -0
- agno/os/interfaces/agui/router.py +144 -0
- agno/os/interfaces/agui/utils.py +534 -0
- agno/os/interfaces/base.py +25 -0
- agno/os/interfaces/slack/__init__.py +3 -0
- agno/os/interfaces/slack/router.py +148 -0
- agno/os/interfaces/slack/security.py +30 -0
- agno/os/interfaces/slack/slack.py +47 -0
- agno/os/interfaces/whatsapp/__init__.py +3 -0
- agno/os/interfaces/whatsapp/router.py +211 -0
- agno/os/interfaces/whatsapp/security.py +53 -0
- agno/os/interfaces/whatsapp/whatsapp.py +36 -0
- agno/os/mcp.py +292 -0
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +1763 -0
- agno/os/routers/__init__.py +3 -0
- agno/os/routers/evals/__init__.py +3 -0
- agno/os/routers/evals/evals.py +430 -0
- agno/os/routers/evals/schemas.py +142 -0
- agno/os/routers/evals/utils.py +162 -0
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/__init__.py +3 -0
- agno/os/routers/knowledge/knowledge.py +997 -0
- agno/os/routers/knowledge/schemas.py +178 -0
- agno/os/routers/memory/__init__.py +3 -0
- agno/os/routers/memory/memory.py +515 -0
- agno/os/routers/memory/schemas.py +62 -0
- agno/os/routers/metrics/__init__.py +3 -0
- agno/os/routers/metrics/metrics.py +190 -0
- agno/os/routers/metrics/schemas.py +47 -0
- agno/os/routers/session/__init__.py +3 -0
- agno/os/routers/session/session.py +997 -0
- agno/os/schema.py +1055 -0
- agno/os/settings.py +43 -0
- agno/os/utils.py +630 -0
- agno/py.typed +0 -0
- agno/reasoning/__init__.py +0 -0
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +67 -0
- agno/reasoning/deepseek.py +63 -0
- agno/reasoning/default.py +97 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +71 -0
- agno/reasoning/helpers.py +63 -0
- agno/reasoning/ollama.py +67 -0
- agno/reasoning/openai.py +86 -0
- agno/reasoning/step.py +31 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +787 -0
- agno/run/base.py +229 -0
- agno/run/cancel.py +81 -0
- agno/run/messages.py +32 -0
- agno/run/team.py +753 -0
- agno/run/workflow.py +708 -0
- agno/session/__init__.py +10 -0
- agno/session/agent.py +295 -0
- agno/session/summary.py +265 -0
- agno/session/team.py +392 -0
- agno/session/workflow.py +205 -0
- agno/team/__init__.py +37 -0
- agno/team/team.py +8793 -0
- agno/tools/__init__.py +10 -0
- agno/tools/agentql.py +120 -0
- agno/tools/airflow.py +69 -0
- agno/tools/api.py +122 -0
- agno/tools/apify.py +314 -0
- agno/tools/arxiv.py +127 -0
- agno/tools/aws_lambda.py +53 -0
- agno/tools/aws_ses.py +66 -0
- agno/tools/baidusearch.py +89 -0
- agno/tools/bitbucket.py +292 -0
- agno/tools/brandfetch.py +213 -0
- agno/tools/bravesearch.py +106 -0
- agno/tools/brightdata.py +367 -0
- agno/tools/browserbase.py +209 -0
- agno/tools/calcom.py +255 -0
- agno/tools/calculator.py +151 -0
- agno/tools/cartesia.py +187 -0
- agno/tools/clickup.py +244 -0
- agno/tools/confluence.py +240 -0
- agno/tools/crawl4ai.py +158 -0
- agno/tools/csv_toolkit.py +185 -0
- agno/tools/dalle.py +110 -0
- agno/tools/daytona.py +475 -0
- agno/tools/decorator.py +262 -0
- agno/tools/desi_vocal.py +108 -0
- agno/tools/discord.py +161 -0
- agno/tools/docker.py +716 -0
- agno/tools/duckdb.py +379 -0
- agno/tools/duckduckgo.py +91 -0
- agno/tools/e2b.py +703 -0
- agno/tools/eleven_labs.py +196 -0
- agno/tools/email.py +67 -0
- agno/tools/evm.py +129 -0
- agno/tools/exa.py +396 -0
- agno/tools/fal.py +127 -0
- agno/tools/file.py +240 -0
- agno/tools/file_generation.py +350 -0
- agno/tools/financial_datasets.py +288 -0
- agno/tools/firecrawl.py +143 -0
- agno/tools/function.py +1187 -0
- agno/tools/giphy.py +93 -0
- agno/tools/github.py +1760 -0
- agno/tools/gmail.py +922 -0
- agno/tools/google_bigquery.py +117 -0
- agno/tools/google_drive.py +270 -0
- agno/tools/google_maps.py +253 -0
- agno/tools/googlecalendar.py +674 -0
- agno/tools/googlesearch.py +98 -0
- agno/tools/googlesheets.py +377 -0
- agno/tools/hackernews.py +77 -0
- agno/tools/jina.py +101 -0
- agno/tools/jira.py +170 -0
- agno/tools/knowledge.py +218 -0
- agno/tools/linear.py +426 -0
- agno/tools/linkup.py +58 -0
- agno/tools/local_file_system.py +90 -0
- agno/tools/lumalab.py +183 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +193 -0
- agno/tools/memori.py +339 -0
- agno/tools/memory.py +419 -0
- agno/tools/mlx_transcribe.py +139 -0
- agno/tools/models/__init__.py +0 -0
- agno/tools/models/azure_openai.py +190 -0
- agno/tools/models/gemini.py +203 -0
- agno/tools/models/groq.py +158 -0
- agno/tools/models/morph.py +186 -0
- agno/tools/models/nebius.py +124 -0
- agno/tools/models_labs.py +195 -0
- agno/tools/moviepy_video.py +349 -0
- agno/tools/neo4j.py +134 -0
- agno/tools/newspaper.py +46 -0
- agno/tools/newspaper4k.py +93 -0
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +202 -0
- agno/tools/openbb.py +160 -0
- agno/tools/opencv.py +321 -0
- agno/tools/openweather.py +233 -0
- agno/tools/oxylabs.py +385 -0
- agno/tools/pandas.py +102 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +257 -0
- agno/tools/pubmed.py +188 -0
- agno/tools/python.py +205 -0
- agno/tools/reasoning.py +283 -0
- agno/tools/reddit.py +467 -0
- agno/tools/replicate.py +117 -0
- agno/tools/resend.py +62 -0
- agno/tools/scrapegraph.py +222 -0
- agno/tools/searxng.py +152 -0
- agno/tools/serpapi.py +116 -0
- agno/tools/serper.py +255 -0
- agno/tools/shell.py +53 -0
- agno/tools/slack.py +136 -0
- agno/tools/sleep.py +20 -0
- agno/tools/spider.py +116 -0
- agno/tools/sql.py +154 -0
- agno/tools/streamlit/__init__.py +0 -0
- agno/tools/streamlit/components.py +113 -0
- agno/tools/tavily.py +254 -0
- agno/tools/telegram.py +48 -0
- agno/tools/todoist.py +218 -0
- agno/tools/tool_registry.py +1 -0
- agno/tools/toolkit.py +146 -0
- agno/tools/trafilatura.py +388 -0
- agno/tools/trello.py +274 -0
- agno/tools/twilio.py +186 -0
- agno/tools/user_control_flow.py +78 -0
- agno/tools/valyu.py +228 -0
- agno/tools/visualization.py +467 -0
- agno/tools/webbrowser.py +28 -0
- agno/tools/webex.py +76 -0
- agno/tools/website.py +54 -0
- agno/tools/webtools.py +45 -0
- agno/tools/whatsapp.py +286 -0
- agno/tools/wikipedia.py +63 -0
- agno/tools/workflow.py +278 -0
- agno/tools/x.py +335 -0
- agno/tools/yfinance.py +257 -0
- agno/tools/youtube.py +184 -0
- agno/tools/zendesk.py +82 -0
- agno/tools/zep.py +454 -0
- agno/tools/zoom.py +382 -0
- agno/utils/__init__.py +0 -0
- agno/utils/agent.py +820 -0
- agno/utils/audio.py +49 -0
- agno/utils/certs.py +27 -0
- agno/utils/code_execution.py +11 -0
- agno/utils/common.py +132 -0
- agno/utils/dttm.py +13 -0
- agno/utils/enum.py +22 -0
- agno/utils/env.py +11 -0
- agno/utils/events.py +696 -0
- agno/utils/format_str.py +16 -0
- agno/utils/functions.py +166 -0
- agno/utils/gemini.py +426 -0
- agno/utils/hooks.py +57 -0
- agno/utils/http.py +74 -0
- agno/utils/json_schema.py +234 -0
- agno/utils/knowledge.py +36 -0
- agno/utils/location.py +19 -0
- agno/utils/log.py +255 -0
- agno/utils/mcp.py +214 -0
- agno/utils/media.py +352 -0
- agno/utils/merge_dict.py +41 -0
- agno/utils/message.py +118 -0
- agno/utils/models/__init__.py +0 -0
- agno/utils/models/ai_foundry.py +43 -0
- agno/utils/models/claude.py +358 -0
- agno/utils/models/cohere.py +87 -0
- agno/utils/models/llama.py +78 -0
- agno/utils/models/mistral.py +98 -0
- agno/utils/models/openai_responses.py +140 -0
- agno/utils/models/schema_utils.py +153 -0
- agno/utils/models/watsonx.py +41 -0
- agno/utils/openai.py +257 -0
- agno/utils/pickle.py +32 -0
- agno/utils/pprint.py +178 -0
- agno/utils/print_response/__init__.py +0 -0
- agno/utils/print_response/agent.py +842 -0
- agno/utils/print_response/team.py +1724 -0
- agno/utils/print_response/workflow.py +1668 -0
- agno/utils/prompts.py +111 -0
- agno/utils/reasoning.py +108 -0
- agno/utils/response.py +163 -0
- agno/utils/response_iterator.py +17 -0
- agno/utils/safe_formatter.py +24 -0
- agno/utils/serialize.py +32 -0
- agno/utils/shell.py +22 -0
- agno/utils/streamlit.py +487 -0
- agno/utils/string.py +231 -0
- agno/utils/team.py +139 -0
- agno/utils/timer.py +41 -0
- agno/utils/tools.py +102 -0
- agno/utils/web.py +23 -0
- agno/utils/whatsapp.py +305 -0
- agno/utils/yaml_io.py +25 -0
- agno/vectordb/__init__.py +3 -0
- agno/vectordb/base.py +127 -0
- agno/vectordb/cassandra/__init__.py +5 -0
- agno/vectordb/cassandra/cassandra.py +501 -0
- agno/vectordb/cassandra/extra_param_mixin.py +11 -0
- agno/vectordb/cassandra/index.py +13 -0
- agno/vectordb/chroma/__init__.py +5 -0
- agno/vectordb/chroma/chromadb.py +929 -0
- agno/vectordb/clickhouse/__init__.py +9 -0
- agno/vectordb/clickhouse/clickhousedb.py +835 -0
- agno/vectordb/clickhouse/index.py +9 -0
- agno/vectordb/couchbase/__init__.py +3 -0
- agno/vectordb/couchbase/couchbase.py +1442 -0
- agno/vectordb/distance.py +7 -0
- agno/vectordb/lancedb/__init__.py +6 -0
- agno/vectordb/lancedb/lance_db.py +995 -0
- agno/vectordb/langchaindb/__init__.py +5 -0
- agno/vectordb/langchaindb/langchaindb.py +163 -0
- agno/vectordb/lightrag/__init__.py +5 -0
- agno/vectordb/lightrag/lightrag.py +388 -0
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +166 -0
- agno/vectordb/milvus/__init__.py +4 -0
- agno/vectordb/milvus/milvus.py +1182 -0
- agno/vectordb/mongodb/__init__.py +9 -0
- agno/vectordb/mongodb/mongodb.py +1417 -0
- agno/vectordb/pgvector/__init__.py +12 -0
- agno/vectordb/pgvector/index.py +23 -0
- agno/vectordb/pgvector/pgvector.py +1462 -0
- agno/vectordb/pineconedb/__init__.py +5 -0
- agno/vectordb/pineconedb/pineconedb.py +747 -0
- agno/vectordb/qdrant/__init__.py +5 -0
- agno/vectordb/qdrant/qdrant.py +1134 -0
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +694 -0
- agno/vectordb/search.py +7 -0
- agno/vectordb/singlestore/__init__.py +10 -0
- agno/vectordb/singlestore/index.py +41 -0
- agno/vectordb/singlestore/singlestore.py +763 -0
- agno/vectordb/surrealdb/__init__.py +3 -0
- agno/vectordb/surrealdb/surrealdb.py +699 -0
- agno/vectordb/upstashdb/__init__.py +5 -0
- agno/vectordb/upstashdb/upstashdb.py +718 -0
- agno/vectordb/weaviate/__init__.py +8 -0
- agno/vectordb/weaviate/index.py +15 -0
- agno/vectordb/weaviate/weaviate.py +1005 -0
- agno/workflow/__init__.py +23 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +738 -0
- agno/workflow/loop.py +735 -0
- agno/workflow/parallel.py +824 -0
- agno/workflow/router.py +702 -0
- agno/workflow/step.py +1432 -0
- agno/workflow/steps.py +592 -0
- agno/workflow/types.py +520 -0
- agno/workflow/workflow.py +4321 -0
- agno-2.2.13.dist-info/METADATA +614 -0
- agno-2.2.13.dist-info/RECORD +575 -0
- agno-2.2.13.dist-info/WHEEL +5 -0
- agno-2.2.13.dist-info/licenses/LICENSE +201 -0
- agno-2.2.13.dist-info/top_level.txt +1 -0
agno/tools/function.py
ADDED
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import partial
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Type, TypeVar, get_type_hints
|
|
5
|
+
|
|
6
|
+
from docstring_parser import parse
|
|
7
|
+
from packaging.version import Version
|
|
8
|
+
from pydantic import BaseModel, Field, validate_call
|
|
9
|
+
|
|
10
|
+
from agno.exceptions import AgentRunException
|
|
11
|
+
from agno.media import Audio, File, Image, Video
|
|
12
|
+
from agno.run import RunContext
|
|
13
|
+
from agno.utils.log import log_debug, log_error, log_exception, log_warning
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_entrypoint_docstring(entrypoint: Callable) -> str:
|
|
19
|
+
from inspect import getdoc
|
|
20
|
+
|
|
21
|
+
if isinstance(entrypoint, partial):
|
|
22
|
+
return str(entrypoint)
|
|
23
|
+
|
|
24
|
+
docstring = getdoc(entrypoint)
|
|
25
|
+
if not docstring:
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
parsed_doc = parse(docstring)
|
|
29
|
+
|
|
30
|
+
# Combine short and long descriptions
|
|
31
|
+
lines = []
|
|
32
|
+
if parsed_doc.short_description:
|
|
33
|
+
lines.append(parsed_doc.short_description)
|
|
34
|
+
if parsed_doc.long_description:
|
|
35
|
+
lines.extend(parsed_doc.long_description.split("\n"))
|
|
36
|
+
|
|
37
|
+
return "\n".join(lines)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class UserInputField:
|
|
42
|
+
name: str
|
|
43
|
+
field_type: Type
|
|
44
|
+
description: Optional[str] = None
|
|
45
|
+
value: Optional[Any] = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"name": self.name,
|
|
50
|
+
"field_type": str(self.field_type.__name__),
|
|
51
|
+
"description": self.description,
|
|
52
|
+
"value": self.value,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, data: Dict[str, Any]) -> "UserInputField":
|
|
57
|
+
return cls(
|
|
58
|
+
name=data["name"],
|
|
59
|
+
field_type=eval(data["field_type"]), # Convert string type name to actual type
|
|
60
|
+
description=data["description"],
|
|
61
|
+
value=data["value"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Function(BaseModel):
|
|
66
|
+
"""Model for storing functions that can be called by an agent."""
|
|
67
|
+
|
|
68
|
+
# The name of the function to be called.
|
|
69
|
+
# Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.
|
|
70
|
+
name: str
|
|
71
|
+
# A description of what the function does, used by the model to choose when and how to call the function.
|
|
72
|
+
description: Optional[str] = None
|
|
73
|
+
# The parameters the functions accepts, described as a JSON Schema object.
|
|
74
|
+
# To describe a function that accepts no parameters, provide the value {"type": "object", "properties": {}}.
|
|
75
|
+
parameters: Dict[str, Any] = Field(
|
|
76
|
+
default_factory=lambda: {"type": "object", "properties": {}, "required": []},
|
|
77
|
+
description="JSON Schema object describing function parameters",
|
|
78
|
+
)
|
|
79
|
+
strict: Optional[bool] = None
|
|
80
|
+
|
|
81
|
+
instructions: Optional[str] = None
|
|
82
|
+
# If True, add instructions to the Agent's system message
|
|
83
|
+
add_instructions: bool = True
|
|
84
|
+
|
|
85
|
+
# The function to be called.
|
|
86
|
+
entrypoint: Optional[Callable] = None
|
|
87
|
+
# If True, the entrypoint processing is skipped and the Function is used as is.
|
|
88
|
+
skip_entrypoint_processing: bool = False
|
|
89
|
+
# If True, the function call will show the result along with sending it to the model.
|
|
90
|
+
show_result: bool = False
|
|
91
|
+
# If True, the agent will stop after the function call.
|
|
92
|
+
stop_after_tool_call: bool = False
|
|
93
|
+
# Hook that runs before the function is executed.
|
|
94
|
+
# If defined, can accept the FunctionCall instance as a parameter.
|
|
95
|
+
pre_hook: Optional[Callable] = None
|
|
96
|
+
# Hook that runs after the function is executed, regardless of success/failure.
|
|
97
|
+
# If defined, can accept the FunctionCall instance as a parameter.
|
|
98
|
+
post_hook: Optional[Callable] = None
|
|
99
|
+
|
|
100
|
+
# A list of hooks to run around tool calls.
|
|
101
|
+
tool_hooks: Optional[List[Callable]] = None
|
|
102
|
+
|
|
103
|
+
# If True, the function will require confirmation before execution
|
|
104
|
+
requires_confirmation: Optional[bool] = None
|
|
105
|
+
|
|
106
|
+
# If True, the function will require user input before execution
|
|
107
|
+
requires_user_input: Optional[bool] = None
|
|
108
|
+
# List of fields that the user will provide as input and that should be ignored by the agent (empty list means all fields are provided by the user)
|
|
109
|
+
user_input_fields: Optional[List[str]] = None
|
|
110
|
+
# This is set during parsing, not by the user
|
|
111
|
+
user_input_schema: Optional[List[UserInputField]] = None
|
|
112
|
+
|
|
113
|
+
# If True, the function will be executed outside the agent's control.
|
|
114
|
+
external_execution: Optional[bool] = None
|
|
115
|
+
|
|
116
|
+
# Caching configuration
|
|
117
|
+
cache_results: bool = False
|
|
118
|
+
cache_dir: Optional[str] = None
|
|
119
|
+
cache_ttl: int = 3600
|
|
120
|
+
|
|
121
|
+
# --*-- FOR INTERNAL USE ONLY --*--
|
|
122
|
+
# The agent that the function is associated with
|
|
123
|
+
_agent: Optional[Any] = None
|
|
124
|
+
# The team that the function is associated with
|
|
125
|
+
_team: Optional[Any] = None
|
|
126
|
+
# The run context that the function is associated with
|
|
127
|
+
_run_context: Optional[RunContext] = None
|
|
128
|
+
# The session state that the function is associated with
|
|
129
|
+
_session_state: Optional[Dict[str, Any]] = None
|
|
130
|
+
# The dependencies that the function is associated with
|
|
131
|
+
_dependencies: Optional[Dict[str, Any]] = None
|
|
132
|
+
|
|
133
|
+
# Media context that the function is associated with
|
|
134
|
+
_images: Optional[Sequence[Image]] = None
|
|
135
|
+
_videos: Optional[Sequence[Video]] = None
|
|
136
|
+
_audios: Optional[Sequence[Audio]] = None
|
|
137
|
+
_files: Optional[Sequence[File]] = None
|
|
138
|
+
|
|
139
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
140
|
+
return self.model_dump(
|
|
141
|
+
exclude_none=True,
|
|
142
|
+
include={"name", "description", "parameters", "strict", "requires_confirmation", "external_execution"},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def model_copy(self, *, deep: bool = False) -> "Function":
|
|
146
|
+
"""
|
|
147
|
+
Override model_copy to handle callable fields that can't be deep copied (pickled).
|
|
148
|
+
Callables should always be shallow copied (referenced), not deep copied.
|
|
149
|
+
"""
|
|
150
|
+
# For deep copy, we need to handle callable fields specially
|
|
151
|
+
if deep:
|
|
152
|
+
# Fields that should NOT be deep copied (callables and complex objects)
|
|
153
|
+
shallow_fields = {
|
|
154
|
+
"entrypoint",
|
|
155
|
+
"pre_hook",
|
|
156
|
+
"post_hook",
|
|
157
|
+
"tool_hooks",
|
|
158
|
+
"_agent",
|
|
159
|
+
"_team",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Create a copy with shallow references to callable fields
|
|
163
|
+
copied_data = {}
|
|
164
|
+
for field_name, field_value in self.__dict__.items():
|
|
165
|
+
if field_name in shallow_fields:
|
|
166
|
+
# Shallow copy - just reference the same object
|
|
167
|
+
copied_data[field_name] = field_value
|
|
168
|
+
elif field_name == "parameters":
|
|
169
|
+
# Deep copy the parameters dict
|
|
170
|
+
from copy import deepcopy
|
|
171
|
+
|
|
172
|
+
copied_data[field_name] = deepcopy(field_value)
|
|
173
|
+
else:
|
|
174
|
+
# For simple types, just copy the value
|
|
175
|
+
copied_data[field_name] = field_value
|
|
176
|
+
|
|
177
|
+
# Create new instance with copied data
|
|
178
|
+
new_instance = self.__class__.model_construct(**copied_data)
|
|
179
|
+
|
|
180
|
+
return new_instance
|
|
181
|
+
else:
|
|
182
|
+
# For shallow copy, use the default Pydantic behavior
|
|
183
|
+
return super().model_copy(deep=False)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_callable(cls, c: Callable, name: Optional[str] = None, strict: bool = False) -> "Function":
|
|
187
|
+
from inspect import getdoc, signature
|
|
188
|
+
|
|
189
|
+
from agno.utils.json_schema import get_json_schema
|
|
190
|
+
|
|
191
|
+
function_name = name or c.__name__
|
|
192
|
+
parameters = {"type": "object", "properties": {}, "required": []}
|
|
193
|
+
try:
|
|
194
|
+
sig = signature(c)
|
|
195
|
+
type_hints = get_type_hints(c)
|
|
196
|
+
|
|
197
|
+
# If function has an the agent argument, remove the agent parameter from the type hints
|
|
198
|
+
if "agent" in sig.parameters and "agent" in type_hints:
|
|
199
|
+
del type_hints["agent"]
|
|
200
|
+
if "team" in sig.parameters and "team" in type_hints:
|
|
201
|
+
del type_hints["team"]
|
|
202
|
+
if "run_context" in sig.parameters and "run_context" in type_hints:
|
|
203
|
+
del type_hints["run_context"]
|
|
204
|
+
if "session_state" in sig.parameters and "session_state" in type_hints:
|
|
205
|
+
del type_hints["session_state"]
|
|
206
|
+
if "dependencies" in sig.parameters and "dependencies" in type_hints:
|
|
207
|
+
del type_hints["dependencies"]
|
|
208
|
+
|
|
209
|
+
# Remove media parameters from type hints as they are injected automatically
|
|
210
|
+
if "images" in sig.parameters and "images" in type_hints:
|
|
211
|
+
del type_hints["images"]
|
|
212
|
+
if "videos" in sig.parameters and "videos" in type_hints:
|
|
213
|
+
del type_hints["videos"]
|
|
214
|
+
if "audios" in sig.parameters and "audios" in type_hints:
|
|
215
|
+
del type_hints["audios"]
|
|
216
|
+
if "files" in sig.parameters and "files" in type_hints:
|
|
217
|
+
del type_hints["files"]
|
|
218
|
+
# log_info(f"Type hints for {function_name}: {type_hints}")
|
|
219
|
+
|
|
220
|
+
# Filter out return type and only process parameters
|
|
221
|
+
param_type_hints = {
|
|
222
|
+
name: type_hints.get(name)
|
|
223
|
+
for name in sig.parameters
|
|
224
|
+
if name != "return"
|
|
225
|
+
and name
|
|
226
|
+
not in [
|
|
227
|
+
"agent",
|
|
228
|
+
"team",
|
|
229
|
+
"run_context",
|
|
230
|
+
"session_state",
|
|
231
|
+
"dependencies",
|
|
232
|
+
"self",
|
|
233
|
+
"images",
|
|
234
|
+
"videos",
|
|
235
|
+
"audios",
|
|
236
|
+
"files",
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Parse docstring for parameters
|
|
241
|
+
param_descriptions: Dict[str, Any] = {}
|
|
242
|
+
if docstring := getdoc(c):
|
|
243
|
+
parsed_doc = parse(docstring)
|
|
244
|
+
param_docs = parsed_doc.params
|
|
245
|
+
|
|
246
|
+
if param_docs is not None:
|
|
247
|
+
for param in param_docs:
|
|
248
|
+
param_name = param.arg_name
|
|
249
|
+
param_type = param.type_name
|
|
250
|
+
if param_type is None:
|
|
251
|
+
param_descriptions[param_name] = param.description
|
|
252
|
+
else:
|
|
253
|
+
param_descriptions[param_name] = f"({param_type}) {param.description}"
|
|
254
|
+
|
|
255
|
+
# Get JSON schema for parameters only
|
|
256
|
+
parameters = get_json_schema(
|
|
257
|
+
type_hints=param_type_hints, param_descriptions=param_descriptions, strict=strict
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# If strict=True mark all fields as required
|
|
261
|
+
# See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas#all-fields-must-be-required
|
|
262
|
+
if strict:
|
|
263
|
+
parameters["required"] = [
|
|
264
|
+
name
|
|
265
|
+
for name in parameters["properties"]
|
|
266
|
+
if name
|
|
267
|
+
not in [
|
|
268
|
+
"agent",
|
|
269
|
+
"team",
|
|
270
|
+
"run_context",
|
|
271
|
+
"session_state",
|
|
272
|
+
"dependencies",
|
|
273
|
+
"self",
|
|
274
|
+
"images",
|
|
275
|
+
"videos",
|
|
276
|
+
"audios",
|
|
277
|
+
"files",
|
|
278
|
+
]
|
|
279
|
+
]
|
|
280
|
+
else:
|
|
281
|
+
# Mark a field as required if it has no default value (this would include optional fields)
|
|
282
|
+
parameters["required"] = [
|
|
283
|
+
name
|
|
284
|
+
for name, param in sig.parameters.items()
|
|
285
|
+
if param.default == param.empty
|
|
286
|
+
and name
|
|
287
|
+
not in [
|
|
288
|
+
"agent",
|
|
289
|
+
"team",
|
|
290
|
+
"run_context",
|
|
291
|
+
"session_state",
|
|
292
|
+
"dependencies",
|
|
293
|
+
"self",
|
|
294
|
+
"images",
|
|
295
|
+
"videos",
|
|
296
|
+
"audios",
|
|
297
|
+
"files",
|
|
298
|
+
]
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
# log_debug(f"JSON schema for {function_name}: {parameters}")
|
|
302
|
+
except Exception as e:
|
|
303
|
+
log_warning(f"Could not parse args for {function_name}: {e}", exc_info=True)
|
|
304
|
+
|
|
305
|
+
entrypoint = cls._wrap_callable(c)
|
|
306
|
+
|
|
307
|
+
return cls(
|
|
308
|
+
name=function_name,
|
|
309
|
+
description=get_entrypoint_docstring(entrypoint=c),
|
|
310
|
+
parameters=parameters,
|
|
311
|
+
entrypoint=entrypoint,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def process_entrypoint(self, strict: bool = False):
|
|
315
|
+
"""Process the entrypoint and make it ready for use by an agent."""
|
|
316
|
+
from inspect import getdoc, signature
|
|
317
|
+
|
|
318
|
+
from agno.utils.json_schema import get_json_schema
|
|
319
|
+
|
|
320
|
+
if self.skip_entrypoint_processing:
|
|
321
|
+
if strict:
|
|
322
|
+
self.process_schema_for_strict()
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
if self.entrypoint is None:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
parameters = {"type": "object", "properties": {}, "required": []}
|
|
329
|
+
|
|
330
|
+
params_set_by_user = False
|
|
331
|
+
# If the user set the parameters (i.e. they are different from the default), we should keep them
|
|
332
|
+
if self.parameters != parameters:
|
|
333
|
+
params_set_by_user = True
|
|
334
|
+
|
|
335
|
+
if self.requires_user_input:
|
|
336
|
+
self.user_input_schema = self.user_input_schema or []
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
sig = signature(self.entrypoint)
|
|
340
|
+
type_hints = get_type_hints(self.entrypoint)
|
|
341
|
+
|
|
342
|
+
# If function has an the agent argument, remove the agent parameter from the type hints
|
|
343
|
+
if "agent" in sig.parameters and "agent" in type_hints:
|
|
344
|
+
del type_hints["agent"]
|
|
345
|
+
if "team" in sig.parameters and "team" in type_hints:
|
|
346
|
+
del type_hints["team"]
|
|
347
|
+
if "run_context" in sig.parameters and "run_context" in type_hints:
|
|
348
|
+
del type_hints["run_context"]
|
|
349
|
+
if "session_state" in sig.parameters and "session_state" in type_hints:
|
|
350
|
+
del type_hints["session_state"]
|
|
351
|
+
if "dependencies" in sig.parameters and "dependencies" in type_hints:
|
|
352
|
+
del type_hints["dependencies"]
|
|
353
|
+
if "images" in sig.parameters and "images" in type_hints:
|
|
354
|
+
del type_hints["images"]
|
|
355
|
+
if "videos" in sig.parameters and "videos" in type_hints:
|
|
356
|
+
del type_hints["videos"]
|
|
357
|
+
if "audios" in sig.parameters and "audios" in type_hints:
|
|
358
|
+
del type_hints["audios"]
|
|
359
|
+
if "files" in sig.parameters and "files" in type_hints:
|
|
360
|
+
del type_hints["files"]
|
|
361
|
+
# log_info(f"Type hints for {self.name}: {type_hints}")
|
|
362
|
+
|
|
363
|
+
# Filter out return type and only process parameters
|
|
364
|
+
excluded_params = [
|
|
365
|
+
"return",
|
|
366
|
+
"agent",
|
|
367
|
+
"team",
|
|
368
|
+
"run_context",
|
|
369
|
+
"session_state",
|
|
370
|
+
"dependencies",
|
|
371
|
+
"self",
|
|
372
|
+
"images",
|
|
373
|
+
"videos",
|
|
374
|
+
"audios",
|
|
375
|
+
"files",
|
|
376
|
+
]
|
|
377
|
+
if self.requires_user_input and self.user_input_fields:
|
|
378
|
+
if len(self.user_input_fields) == 0:
|
|
379
|
+
excluded_params.extend(list(type_hints.keys()))
|
|
380
|
+
else:
|
|
381
|
+
excluded_params.extend(self.user_input_fields)
|
|
382
|
+
|
|
383
|
+
# Get filtered list of parameter types
|
|
384
|
+
param_type_hints = {name: type_hints.get(name) for name in sig.parameters if name not in excluded_params}
|
|
385
|
+
|
|
386
|
+
# Parse docstring for parameters
|
|
387
|
+
param_descriptions = {}
|
|
388
|
+
param_descriptions_clean = {}
|
|
389
|
+
if docstring := getdoc(self.entrypoint):
|
|
390
|
+
parsed_doc = parse(docstring)
|
|
391
|
+
param_docs = parsed_doc.params
|
|
392
|
+
|
|
393
|
+
if param_docs is not None:
|
|
394
|
+
for param in param_docs:
|
|
395
|
+
param_name = param.arg_name
|
|
396
|
+
param_type = param.type_name
|
|
397
|
+
|
|
398
|
+
# TODO: We should use type hints first, then map param types in docs to json schema types.
|
|
399
|
+
# This is temporary to not lose information
|
|
400
|
+
param_descriptions[param_name] = f"({param_type}) {param.description}"
|
|
401
|
+
param_descriptions_clean[param_name] = param.description
|
|
402
|
+
|
|
403
|
+
# If the function requires user input, we should set the user_input_schema to all parameters. The arguments provided by the model are filled in later.
|
|
404
|
+
if self.requires_user_input:
|
|
405
|
+
self.user_input_schema = [
|
|
406
|
+
UserInputField(
|
|
407
|
+
name=name,
|
|
408
|
+
description=param_descriptions_clean.get(name),
|
|
409
|
+
field_type=type_hints.get(name, str),
|
|
410
|
+
)
|
|
411
|
+
for name in sig.parameters
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
# Get JSON schema for parameters only
|
|
415
|
+
parameters = get_json_schema(
|
|
416
|
+
type_hints=param_type_hints, param_descriptions=param_descriptions, strict=strict
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# If strict=True mark all fields as required
|
|
420
|
+
# See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas#all-fields-must-be-required
|
|
421
|
+
if strict:
|
|
422
|
+
parameters["required"] = [name for name in parameters["properties"] if name not in excluded_params]
|
|
423
|
+
else:
|
|
424
|
+
# Mark a field as required if it has no default value
|
|
425
|
+
parameters["required"] = [
|
|
426
|
+
name
|
|
427
|
+
for name, param in sig.parameters.items()
|
|
428
|
+
if param.default == param.empty and name != "self" and name not in excluded_params
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
if params_set_by_user:
|
|
432
|
+
self.parameters["additionalProperties"] = False
|
|
433
|
+
if strict:
|
|
434
|
+
self.parameters["required"] = [
|
|
435
|
+
name for name in self.parameters["properties"] if name not in excluded_params
|
|
436
|
+
]
|
|
437
|
+
else:
|
|
438
|
+
# Mark a field as required if it has no default value
|
|
439
|
+
self.parameters["required"] = [
|
|
440
|
+
name
|
|
441
|
+
for name, param in sig.parameters.items()
|
|
442
|
+
if param.default == param.empty and name != "self" and name not in excluded_params
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
self.description = self.description or get_entrypoint_docstring(self.entrypoint)
|
|
446
|
+
|
|
447
|
+
# log_debug(f"JSON schema for {self.name}: {parameters}")
|
|
448
|
+
except Exception as e:
|
|
449
|
+
log_warning(f"Could not parse args for {self.name}: {e}", exc_info=True)
|
|
450
|
+
|
|
451
|
+
if not params_set_by_user:
|
|
452
|
+
self.parameters = parameters
|
|
453
|
+
|
|
454
|
+
if strict:
|
|
455
|
+
self.process_schema_for_strict()
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
self.entrypoint = self._wrap_callable(self.entrypoint)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
log_warning(f"Failed to add validate decorator to entrypoint: {e}")
|
|
461
|
+
|
|
462
|
+
@staticmethod
|
|
463
|
+
def _wrap_callable(func: Callable) -> Callable:
|
|
464
|
+
"""Wrap a callable with Pydantic's validate_call decorator, if relevant"""
|
|
465
|
+
from inspect import isasyncgenfunction, iscoroutinefunction, signature
|
|
466
|
+
|
|
467
|
+
pydantic_version = Version(version("pydantic"))
|
|
468
|
+
|
|
469
|
+
# Don't wrap async generators validate_call
|
|
470
|
+
if isasyncgenfunction(func):
|
|
471
|
+
return func
|
|
472
|
+
|
|
473
|
+
# Don't wrap coroutines with validate_call if pydantic version is less than 2.10.0
|
|
474
|
+
if iscoroutinefunction(func) and pydantic_version < Version("2.10.0"):
|
|
475
|
+
log_debug(
|
|
476
|
+
f"Skipping validate_call for {func.__name__} because pydantic version is less than 2.10.0, please consider upgrading to pydantic 2.10.0 or higher"
|
|
477
|
+
)
|
|
478
|
+
return func
|
|
479
|
+
|
|
480
|
+
# Don't wrap callables that are already wrapped with validate_call
|
|
481
|
+
elif getattr(func, "_wrapped_for_validation", False):
|
|
482
|
+
return func
|
|
483
|
+
# Don't wrap functions with session_state parameter
|
|
484
|
+
# session_state needs to be passed by reference, not copied by pydantic's validation
|
|
485
|
+
elif "session_state" in signature(func).parameters:
|
|
486
|
+
return func
|
|
487
|
+
# Wrap the callable with validate_call
|
|
488
|
+
else:
|
|
489
|
+
wrapped = validate_call(func, config=dict(arbitrary_types_allowed=True)) # type: ignore
|
|
490
|
+
wrapped._wrapped_for_validation = True # Mark as wrapped to avoid infinite recursion
|
|
491
|
+
return wrapped
|
|
492
|
+
|
|
493
|
+
def process_schema_for_strict(self):
|
|
494
|
+
"""Process the schema to make it strict mode compliant."""
|
|
495
|
+
|
|
496
|
+
def make_nested_strict(schema):
|
|
497
|
+
"""Recursively ensure all object schemas have additionalProperties: false"""
|
|
498
|
+
if not isinstance(schema, dict):
|
|
499
|
+
return schema
|
|
500
|
+
|
|
501
|
+
# Make a copy to avoid modifying the original
|
|
502
|
+
result = schema.copy()
|
|
503
|
+
|
|
504
|
+
# If this is an object schema, ensure additionalProperties: false
|
|
505
|
+
if result.get("type") == "object" or "properties" in result:
|
|
506
|
+
result["additionalProperties"] = False
|
|
507
|
+
|
|
508
|
+
# If schema has no type but has other schema properties, give it a type
|
|
509
|
+
if "type" not in result:
|
|
510
|
+
if "properties" in result:
|
|
511
|
+
result["type"] = "object"
|
|
512
|
+
result["additionalProperties"] = False
|
|
513
|
+
elif result.get("title") and not any(
|
|
514
|
+
key in result for key in ["properties", "items", "anyOf", "oneOf", "allOf", "enum"]
|
|
515
|
+
):
|
|
516
|
+
result["type"] = "string"
|
|
517
|
+
|
|
518
|
+
# Recursively process nested schemas
|
|
519
|
+
for key, value in result.items():
|
|
520
|
+
if key == "properties" and isinstance(value, dict):
|
|
521
|
+
result[key] = {k: make_nested_strict(v) for k, v in value.items()}
|
|
522
|
+
elif key == "items" and isinstance(value, dict):
|
|
523
|
+
# This handles array items like List[KnowledgeFilter]
|
|
524
|
+
result[key] = make_nested_strict(value)
|
|
525
|
+
elif isinstance(value, dict):
|
|
526
|
+
result[key] = make_nested_strict(value)
|
|
527
|
+
|
|
528
|
+
return result
|
|
529
|
+
|
|
530
|
+
# Apply strict mode to the entire schema
|
|
531
|
+
self.parameters = make_nested_strict(self.parameters)
|
|
532
|
+
|
|
533
|
+
self.parameters["required"] = [
|
|
534
|
+
name
|
|
535
|
+
for name in self.parameters["properties"]
|
|
536
|
+
if name
|
|
537
|
+
not in [
|
|
538
|
+
"agent",
|
|
539
|
+
"team",
|
|
540
|
+
"run_context",
|
|
541
|
+
"session_state",
|
|
542
|
+
"dependencies",
|
|
543
|
+
"images",
|
|
544
|
+
"videos",
|
|
545
|
+
"audios",
|
|
546
|
+
"files",
|
|
547
|
+
"self",
|
|
548
|
+
]
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
def _get_cache_key(self, entrypoint_args: Dict[str, Any], call_args: Optional[Dict[str, Any]] = None) -> str:
|
|
552
|
+
"""Generate a cache key based on function name and arguments."""
|
|
553
|
+
import json
|
|
554
|
+
from hashlib import md5
|
|
555
|
+
|
|
556
|
+
copy_entrypoint_args = entrypoint_args.copy()
|
|
557
|
+
# Remove agent from entrypoint_args
|
|
558
|
+
if "agent" in copy_entrypoint_args:
|
|
559
|
+
del copy_entrypoint_args["agent"]
|
|
560
|
+
if "team" in copy_entrypoint_args:
|
|
561
|
+
del copy_entrypoint_args["team"]
|
|
562
|
+
if "run_context" in copy_entrypoint_args:
|
|
563
|
+
del copy_entrypoint_args["run_context"]
|
|
564
|
+
if "session_state" in copy_entrypoint_args:
|
|
565
|
+
del copy_entrypoint_args["session_state"]
|
|
566
|
+
if "dependencies" in copy_entrypoint_args:
|
|
567
|
+
del copy_entrypoint_args["dependencies"]
|
|
568
|
+
if "images" in copy_entrypoint_args:
|
|
569
|
+
del copy_entrypoint_args["images"]
|
|
570
|
+
if "videos" in copy_entrypoint_args:
|
|
571
|
+
del copy_entrypoint_args["videos"]
|
|
572
|
+
if "audios" in copy_entrypoint_args:
|
|
573
|
+
del copy_entrypoint_args["audios"]
|
|
574
|
+
if "files" in copy_entrypoint_args:
|
|
575
|
+
del copy_entrypoint_args["files"]
|
|
576
|
+
# Use json.dumps with sort_keys=True to ensure consistent ordering regardless of dict key order
|
|
577
|
+
args_str = json.dumps(copy_entrypoint_args, sort_keys=True, default=str)
|
|
578
|
+
|
|
579
|
+
kwargs_str = str(sorted((call_args or {}).items()))
|
|
580
|
+
key_str = f"{self.name}:{args_str}:{kwargs_str}"
|
|
581
|
+
return md5(key_str.encode()).hexdigest()
|
|
582
|
+
|
|
583
|
+
def _get_cache_file_path(self, cache_key: str) -> str:
|
|
584
|
+
"""Get the full path for the cache file."""
|
|
585
|
+
from pathlib import Path
|
|
586
|
+
from tempfile import gettempdir
|
|
587
|
+
|
|
588
|
+
base_cache_dir = self.cache_dir or Path(gettempdir()) / "agno_cache"
|
|
589
|
+
func_cache_dir = Path(base_cache_dir) / "functions" / self.name
|
|
590
|
+
func_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
591
|
+
return str(func_cache_dir / f"{cache_key}.json")
|
|
592
|
+
|
|
593
|
+
def _get_cached_result(self, cache_file: str) -> Optional[Any]:
|
|
594
|
+
"""Retrieve cached result if valid."""
|
|
595
|
+
import json
|
|
596
|
+
from pathlib import Path
|
|
597
|
+
from time import time
|
|
598
|
+
|
|
599
|
+
cache_path = Path(cache_file)
|
|
600
|
+
if not cache_path.exists():
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
try:
|
|
604
|
+
with cache_path.open("r") as f:
|
|
605
|
+
cache_data = json.load(f)
|
|
606
|
+
|
|
607
|
+
timestamp = cache_data.get("timestamp", 0)
|
|
608
|
+
result = cache_data.get("result")
|
|
609
|
+
|
|
610
|
+
if time() - timestamp <= self.cache_ttl:
|
|
611
|
+
return result
|
|
612
|
+
|
|
613
|
+
# Remove expired entry
|
|
614
|
+
cache_path.unlink()
|
|
615
|
+
except Exception as e:
|
|
616
|
+
log_error(f"Error reading cache: {e}")
|
|
617
|
+
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
def _save_to_cache(self, cache_file: str, result: Any):
|
|
621
|
+
"""Save result to cache."""
|
|
622
|
+
import json
|
|
623
|
+
from time import time
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
with open(cache_file, "w") as f:
|
|
627
|
+
json.dump({"timestamp": time(), "result": result}, f)
|
|
628
|
+
except Exception as e:
|
|
629
|
+
log_error(f"Error writing cache: {e}")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class FunctionExecutionResult(BaseModel):
|
|
633
|
+
status: Literal["success", "failure"]
|
|
634
|
+
result: Optional[Any] = None
|
|
635
|
+
error: Optional[str] = None
|
|
636
|
+
|
|
637
|
+
updated_session_state: Optional[Dict[str, Any]] = None
|
|
638
|
+
|
|
639
|
+
# New fields for media artifacts
|
|
640
|
+
images: Optional[List[Image]] = None
|
|
641
|
+
videos: Optional[List[Video]] = None
|
|
642
|
+
audios: Optional[List[Audio]] = None
|
|
643
|
+
files: Optional[List[File]] = None
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class FunctionCall(BaseModel):
|
|
647
|
+
"""Model for Function Calls"""
|
|
648
|
+
|
|
649
|
+
# The function to be called.
|
|
650
|
+
function: Function
|
|
651
|
+
# The arguments to call the function with.
|
|
652
|
+
arguments: Optional[Dict[str, Any]] = None
|
|
653
|
+
# The result of the function call.
|
|
654
|
+
result: Optional[Any] = None
|
|
655
|
+
# The ID of the function call.
|
|
656
|
+
call_id: Optional[str] = None
|
|
657
|
+
|
|
658
|
+
# Error while parsing arguments or running the function.
|
|
659
|
+
error: Optional[str] = None
|
|
660
|
+
|
|
661
|
+
def get_call_str(self) -> str:
|
|
662
|
+
"""Returns a string representation of the function call."""
|
|
663
|
+
import shutil
|
|
664
|
+
|
|
665
|
+
# Get terminal width, default to 80 if it can't be determined
|
|
666
|
+
term_width = shutil.get_terminal_size().columns or 80
|
|
667
|
+
max_arg_len = max(20, (term_width - len(self.function.name) - 4) // 2)
|
|
668
|
+
|
|
669
|
+
if self.arguments is None:
|
|
670
|
+
return f"{self.function.name}()"
|
|
671
|
+
|
|
672
|
+
trimmed_arguments = {}
|
|
673
|
+
for k, v in self.arguments.items():
|
|
674
|
+
if isinstance(v, str) and len(str(v)) > max_arg_len:
|
|
675
|
+
trimmed_arguments[k] = "..."
|
|
676
|
+
else:
|
|
677
|
+
trimmed_arguments[k] = v
|
|
678
|
+
|
|
679
|
+
call_str = f"{self.function.name}({', '.join([f'{k}={v}' for k, v in trimmed_arguments.items()])})"
|
|
680
|
+
|
|
681
|
+
# If call string is too long, truncate arguments
|
|
682
|
+
if len(call_str) > term_width:
|
|
683
|
+
return f"{self.function.name}(...)"
|
|
684
|
+
|
|
685
|
+
return call_str
|
|
686
|
+
|
|
687
|
+
def _handle_pre_hook(self):
|
|
688
|
+
"""Handles the pre-hook for the function call."""
|
|
689
|
+
if self.function.pre_hook is not None:
|
|
690
|
+
try:
|
|
691
|
+
from inspect import signature
|
|
692
|
+
|
|
693
|
+
pre_hook_args = {}
|
|
694
|
+
# Check if the pre-hook has and agent argument
|
|
695
|
+
if "agent" in signature(self.function.pre_hook).parameters:
|
|
696
|
+
pre_hook_args["agent"] = self.function._agent
|
|
697
|
+
# Check if the pre-hook has an team argument
|
|
698
|
+
if "team" in signature(self.function.pre_hook).parameters:
|
|
699
|
+
pre_hook_args["team"] = self.function._team
|
|
700
|
+
# Check if the pre-hook has an session_state argument
|
|
701
|
+
if "run_context" in signature(self.function.pre_hook).parameters:
|
|
702
|
+
pre_hook_args["run_context"] = self.function._run_context
|
|
703
|
+
# Check if the pre-hook has an session_state argument
|
|
704
|
+
if "session_state" in signature(self.function.pre_hook).parameters:
|
|
705
|
+
pre_hook_args["session_state"] = self.function._session_state
|
|
706
|
+
# Check if the pre-hook has an dependencies argument
|
|
707
|
+
if "dependencies" in signature(self.function.pre_hook).parameters:
|
|
708
|
+
pre_hook_args["dependencies"] = self.function._dependencies
|
|
709
|
+
# Check if the pre-hook has an fc argument
|
|
710
|
+
if "fc" in signature(self.function.pre_hook).parameters:
|
|
711
|
+
pre_hook_args["fc"] = self
|
|
712
|
+
self.function.pre_hook(**pre_hook_args)
|
|
713
|
+
except AgentRunException as e:
|
|
714
|
+
log_debug(f"{e.__class__.__name__}: {e}")
|
|
715
|
+
self.error = str(e)
|
|
716
|
+
raise
|
|
717
|
+
except Exception as e:
|
|
718
|
+
log_warning(f"Error in pre-hook callback: {e}")
|
|
719
|
+
log_exception(e)
|
|
720
|
+
|
|
721
|
+
def _handle_post_hook(self):
|
|
722
|
+
"""Handles the post-hook for the function call."""
|
|
723
|
+
if self.function.post_hook is not None:
|
|
724
|
+
try:
|
|
725
|
+
from inspect import signature
|
|
726
|
+
|
|
727
|
+
post_hook_args = {}
|
|
728
|
+
# Check if the post-hook has and agent argument
|
|
729
|
+
if "agent" in signature(self.function.post_hook).parameters:
|
|
730
|
+
post_hook_args["agent"] = self.function._agent
|
|
731
|
+
# Check if the post-hook has an team argument
|
|
732
|
+
if "team" in signature(self.function.post_hook).parameters:
|
|
733
|
+
post_hook_args["team"] = self.function._team
|
|
734
|
+
# Check if the post-hook has an session_state argument
|
|
735
|
+
if "run_context" in signature(self.function.post_hook).parameters:
|
|
736
|
+
post_hook_args["run_context"] = self.function._run_context
|
|
737
|
+
# Check if the post-hook has an session_state argument
|
|
738
|
+
if "session_state" in signature(self.function.post_hook).parameters:
|
|
739
|
+
post_hook_args["session_state"] = self.function._session_state
|
|
740
|
+
# Check if the post-hook has an dependencies argument
|
|
741
|
+
if "dependencies" in signature(self.function.post_hook).parameters:
|
|
742
|
+
post_hook_args["dependencies"] = self.function._dependencies
|
|
743
|
+
# Check if the post-hook has an fc argument
|
|
744
|
+
if "fc" in signature(self.function.post_hook).parameters:
|
|
745
|
+
post_hook_args["fc"] = self
|
|
746
|
+
self.function.post_hook(**post_hook_args)
|
|
747
|
+
except AgentRunException as e:
|
|
748
|
+
log_debug(f"{e.__class__.__name__}: {e}")
|
|
749
|
+
self.error = str(e)
|
|
750
|
+
raise
|
|
751
|
+
except Exception as e:
|
|
752
|
+
log_warning(f"Error in post-hook callback: {e}")
|
|
753
|
+
log_exception(e)
|
|
754
|
+
|
|
755
|
+
def _build_entrypoint_args(self) -> Dict[str, Any]:
|
|
756
|
+
"""Builds the arguments for the entrypoint."""
|
|
757
|
+
from inspect import signature
|
|
758
|
+
|
|
759
|
+
entrypoint_args = {}
|
|
760
|
+
# Check if the entrypoint has an agent argument
|
|
761
|
+
if "agent" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
762
|
+
entrypoint_args["agent"] = self.function._agent
|
|
763
|
+
# Check if the entrypoint has an team argument
|
|
764
|
+
if "team" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
765
|
+
entrypoint_args["team"] = self.function._team
|
|
766
|
+
# Check if the entrypoint has an run_context argument
|
|
767
|
+
if "run_context" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
768
|
+
entrypoint_args["run_context"] = self.function._run_context
|
|
769
|
+
# Check if the entrypoint has an session_state argument
|
|
770
|
+
if "session_state" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
771
|
+
entrypoint_args["session_state"] = self.function._session_state
|
|
772
|
+
# Check if the entrypoint has an dependencies argument
|
|
773
|
+
if "dependencies" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
774
|
+
entrypoint_args["dependencies"] = self.function._dependencies
|
|
775
|
+
# Check if the entrypoint has an fc argument
|
|
776
|
+
if "fc" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
777
|
+
entrypoint_args["fc"] = self
|
|
778
|
+
|
|
779
|
+
# Check if the entrypoint has media arguments
|
|
780
|
+
if "images" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
781
|
+
entrypoint_args["images"] = self.function._images
|
|
782
|
+
if "videos" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
783
|
+
entrypoint_args["videos"] = self.function._videos
|
|
784
|
+
if "audios" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
785
|
+
entrypoint_args["audios"] = self.function._audios
|
|
786
|
+
if "files" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
787
|
+
entrypoint_args["files"] = self.function._files
|
|
788
|
+
return entrypoint_args
|
|
789
|
+
|
|
790
|
+
def _build_hook_args(self, hook: Callable, name: str, func: Callable, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
791
|
+
"""Build the arguments for the hook."""
|
|
792
|
+
from inspect import signature
|
|
793
|
+
|
|
794
|
+
hook_args = {}
|
|
795
|
+
# Check if the hook has an agent argument
|
|
796
|
+
if "agent" in signature(hook).parameters:
|
|
797
|
+
hook_args["agent"] = self.function._agent
|
|
798
|
+
# Check if the hook has an team argument
|
|
799
|
+
if "team" in signature(hook).parameters:
|
|
800
|
+
hook_args["team"] = self.function._team
|
|
801
|
+
# Check if the hook has an run_context argument
|
|
802
|
+
if "run_context" in signature(hook).parameters:
|
|
803
|
+
hook_args["run_context"] = self.function._run_context
|
|
804
|
+
# Check if the hook has an session_state argument
|
|
805
|
+
if "session_state" in signature(hook).parameters:
|
|
806
|
+
hook_args["session_state"] = self.function._session_state
|
|
807
|
+
# Check if the hook has an dependencies argument
|
|
808
|
+
if "dependencies" in signature(hook).parameters:
|
|
809
|
+
hook_args["dependencies"] = self.function._dependencies
|
|
810
|
+
if "name" in signature(hook).parameters:
|
|
811
|
+
hook_args["name"] = name
|
|
812
|
+
if "function_name" in signature(hook).parameters:
|
|
813
|
+
hook_args["function_name"] = name
|
|
814
|
+
if "function" in signature(hook).parameters:
|
|
815
|
+
hook_args["function"] = func
|
|
816
|
+
if "func" in signature(hook).parameters:
|
|
817
|
+
hook_args["func"] = func
|
|
818
|
+
if "function_call" in signature(hook).parameters:
|
|
819
|
+
hook_args["function_call"] = func
|
|
820
|
+
if "args" in signature(hook).parameters:
|
|
821
|
+
hook_args["args"] = args
|
|
822
|
+
if "arguments" in signature(hook).parameters:
|
|
823
|
+
hook_args["arguments"] = args
|
|
824
|
+
return hook_args
|
|
825
|
+
|
|
826
|
+
def _build_nested_execution_chain(self, entrypoint_args: Dict[str, Any]):
|
|
827
|
+
"""Build a nested chain of hook executions with the entrypoint at the center.
|
|
828
|
+
|
|
829
|
+
This creates a chain where each hook wraps the next one, with the function call
|
|
830
|
+
at the innermost level. Returns bubble back up through each hook.
|
|
831
|
+
"""
|
|
832
|
+
from functools import reduce
|
|
833
|
+
from inspect import iscoroutinefunction
|
|
834
|
+
|
|
835
|
+
def execute_entrypoint(name, func, args):
|
|
836
|
+
"""Execute the entrypoint function."""
|
|
837
|
+
arguments = entrypoint_args.copy()
|
|
838
|
+
if self.arguments is not None:
|
|
839
|
+
arguments.update(self.arguments)
|
|
840
|
+
return self.function.entrypoint(**arguments) # type: ignore
|
|
841
|
+
|
|
842
|
+
# If no hooks, just return the entrypoint execution function
|
|
843
|
+
if not self.function.tool_hooks:
|
|
844
|
+
return execute_entrypoint
|
|
845
|
+
|
|
846
|
+
def create_hook_wrapper(inner_func, hook):
|
|
847
|
+
"""Create a nested wrapper for the hook."""
|
|
848
|
+
|
|
849
|
+
def wrapper(name, func, args):
|
|
850
|
+
# Pass the inner function as next_func to the hook
|
|
851
|
+
# The hook will call next_func to continue the chain
|
|
852
|
+
def next_func(**kwargs):
|
|
853
|
+
return inner_func(name, func, kwargs)
|
|
854
|
+
|
|
855
|
+
hook_args = self._build_hook_args(hook, name, next_func, args)
|
|
856
|
+
|
|
857
|
+
return hook(**hook_args)
|
|
858
|
+
|
|
859
|
+
return wrapper
|
|
860
|
+
|
|
861
|
+
# Remove coroutine hooks
|
|
862
|
+
final_hooks = []
|
|
863
|
+
for hook in self.function.tool_hooks:
|
|
864
|
+
if iscoroutinefunction(hook):
|
|
865
|
+
log_warning(f"Cannot use async hooks with sync function calls. Skipping hook: {hook.__name__}")
|
|
866
|
+
else:
|
|
867
|
+
final_hooks.append(hook)
|
|
868
|
+
|
|
869
|
+
# Build the chain from inside out - reverse the hooks to start from the innermost
|
|
870
|
+
hooks = list(reversed(final_hooks))
|
|
871
|
+
chain = reduce(create_hook_wrapper, hooks, execute_entrypoint)
|
|
872
|
+
return chain
|
|
873
|
+
|
|
874
|
+
def execute(self) -> FunctionExecutionResult:
|
|
875
|
+
"""Runs the function call."""
|
|
876
|
+
from inspect import isgenerator, isgeneratorfunction
|
|
877
|
+
|
|
878
|
+
if self.function.entrypoint is None:
|
|
879
|
+
return FunctionExecutionResult(status="failure", error="Entrypoint is not set")
|
|
880
|
+
|
|
881
|
+
log_debug(f"Running: {self.get_call_str()}")
|
|
882
|
+
|
|
883
|
+
# Execute pre-hook if it exists
|
|
884
|
+
self._handle_pre_hook()
|
|
885
|
+
|
|
886
|
+
entrypoint_args = self._build_entrypoint_args()
|
|
887
|
+
|
|
888
|
+
# Check cache if enabled and not a generator function
|
|
889
|
+
if self.function.cache_results and not isgeneratorfunction(self.function.entrypoint):
|
|
890
|
+
cache_key = self.function._get_cache_key(entrypoint_args, self.arguments)
|
|
891
|
+
cache_file = self.function._get_cache_file_path(cache_key)
|
|
892
|
+
cached_result = self.function._get_cached_result(cache_file)
|
|
893
|
+
|
|
894
|
+
if cached_result is not None:
|
|
895
|
+
log_debug(f"Cache hit for: {self.get_call_str()}")
|
|
896
|
+
self.result = cached_result
|
|
897
|
+
return FunctionExecutionResult(status="success", result=cached_result)
|
|
898
|
+
|
|
899
|
+
# Execute function
|
|
900
|
+
execution_result = None
|
|
901
|
+
exception_to_raise = None
|
|
902
|
+
|
|
903
|
+
try:
|
|
904
|
+
# Build and execute the nested chain of hooks
|
|
905
|
+
if self.function.tool_hooks is not None:
|
|
906
|
+
execution_chain = self._build_nested_execution_chain(entrypoint_args=entrypoint_args)
|
|
907
|
+
result = execution_chain(self.function.name, self.function.entrypoint, self.arguments or {})
|
|
908
|
+
else:
|
|
909
|
+
result = self.function.entrypoint(**entrypoint_args, **self.arguments) # type: ignore
|
|
910
|
+
|
|
911
|
+
updated_session_state = None
|
|
912
|
+
if entrypoint_args.get("run_context") is not None:
|
|
913
|
+
run_context = entrypoint_args.get("run_context")
|
|
914
|
+
updated_session_state = (
|
|
915
|
+
run_context.session_state
|
|
916
|
+
if run_context is not None and run_context.session_state is not None
|
|
917
|
+
else None
|
|
918
|
+
)
|
|
919
|
+
else:
|
|
920
|
+
if self.function._session_state is not None:
|
|
921
|
+
updated_session_state = self.function._session_state
|
|
922
|
+
|
|
923
|
+
# Handle generator case
|
|
924
|
+
if isgenerator(result):
|
|
925
|
+
self.result = result # Store generator directly, can't cache
|
|
926
|
+
else:
|
|
927
|
+
self.result = result
|
|
928
|
+
# Only cache non-generator results
|
|
929
|
+
if self.function.cache_results:
|
|
930
|
+
cache_key = self.function._get_cache_key(entrypoint_args, self.arguments)
|
|
931
|
+
cache_file = self.function._get_cache_file_path(cache_key)
|
|
932
|
+
self.function._save_to_cache(cache_file, self.result)
|
|
933
|
+
|
|
934
|
+
execution_result = FunctionExecutionResult(
|
|
935
|
+
status="success", result=self.result, updated_session_state=updated_session_state
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
except AgentRunException as e:
|
|
939
|
+
log_debug(f"{e.__class__.__name__}: {e}")
|
|
940
|
+
self.error = str(e)
|
|
941
|
+
exception_to_raise = e
|
|
942
|
+
except Exception as e:
|
|
943
|
+
log_warning(f"Could not run function {self.get_call_str()}")
|
|
944
|
+
log_exception(e)
|
|
945
|
+
self.error = str(e)
|
|
946
|
+
execution_result = FunctionExecutionResult(status="failure", error=str(e))
|
|
947
|
+
|
|
948
|
+
finally:
|
|
949
|
+
self._handle_post_hook()
|
|
950
|
+
|
|
951
|
+
if exception_to_raise is not None:
|
|
952
|
+
raise exception_to_raise
|
|
953
|
+
|
|
954
|
+
return execution_result # type: ignore[return-value]
|
|
955
|
+
|
|
956
|
+
async def _handle_pre_hook_async(self):
|
|
957
|
+
"""Handles the async pre-hook for the function call."""
|
|
958
|
+
if self.function.pre_hook is not None:
|
|
959
|
+
try:
|
|
960
|
+
from inspect import signature
|
|
961
|
+
|
|
962
|
+
pre_hook_args = {}
|
|
963
|
+
# Check if the pre-hook has an agent argument
|
|
964
|
+
if "agent" in signature(self.function.pre_hook).parameters:
|
|
965
|
+
pre_hook_args["agent"] = self.function._agent
|
|
966
|
+
# Check if the pre-hook has an team argument
|
|
967
|
+
if "team" in signature(self.function.pre_hook).parameters:
|
|
968
|
+
pre_hook_args["team"] = self.function._team
|
|
969
|
+
# Check if the pre-hook has an run_context argument
|
|
970
|
+
if "run_context" in signature(self.function.pre_hook).parameters:
|
|
971
|
+
pre_hook_args["run_context"] = self.function._run_context
|
|
972
|
+
# Check if the pre-hook has an session_state argument
|
|
973
|
+
if "session_state" in signature(self.function.pre_hook).parameters:
|
|
974
|
+
pre_hook_args["session_state"] = self.function._session_state
|
|
975
|
+
# Check if the pre-hook has an dependencies argument
|
|
976
|
+
if "dependencies" in signature(self.function.pre_hook).parameters:
|
|
977
|
+
pre_hook_args["dependencies"] = self.function._dependencies
|
|
978
|
+
# Check if the pre-hook has an fc argument
|
|
979
|
+
if "fc" in signature(self.function.pre_hook).parameters:
|
|
980
|
+
pre_hook_args["fc"] = self
|
|
981
|
+
|
|
982
|
+
await self.function.pre_hook(**pre_hook_args)
|
|
983
|
+
except AgentRunException as e:
|
|
984
|
+
log_debug(f"{e.__class__.__name__}: {e}")
|
|
985
|
+
self.error = str(e)
|
|
986
|
+
raise
|
|
987
|
+
except Exception as e:
|
|
988
|
+
log_warning(f"Error in pre-hook callback: {e}")
|
|
989
|
+
log_exception(e)
|
|
990
|
+
|
|
991
|
+
async def _handle_post_hook_async(self):
|
|
992
|
+
"""Handles the async post-hook for the function call."""
|
|
993
|
+
if self.function.post_hook is not None:
|
|
994
|
+
try:
|
|
995
|
+
from inspect import signature
|
|
996
|
+
|
|
997
|
+
post_hook_args = {}
|
|
998
|
+
# Check if the post-hook has an agent argument
|
|
999
|
+
if "agent" in signature(self.function.post_hook).parameters:
|
|
1000
|
+
post_hook_args["agent"] = self.function._agent
|
|
1001
|
+
# Check if the post-hook has an team argument
|
|
1002
|
+
if "team" in signature(self.function.post_hook).parameters:
|
|
1003
|
+
post_hook_args["team"] = self.function._team
|
|
1004
|
+
# Check if the post-hook has an run_context argument
|
|
1005
|
+
if "run_context" in signature(self.function.post_hook).parameters:
|
|
1006
|
+
post_hook_args["run_context"] = self.function._run_context
|
|
1007
|
+
# Check if the post-hook has an session_state argument
|
|
1008
|
+
if "session_state" in signature(self.function.post_hook).parameters:
|
|
1009
|
+
post_hook_args["session_state"] = self.function._session_state
|
|
1010
|
+
# Check if the post-hook has an dependencies argument
|
|
1011
|
+
if "dependencies" in signature(self.function.post_hook).parameters:
|
|
1012
|
+
post_hook_args["dependencies"] = self.function._dependencies
|
|
1013
|
+
|
|
1014
|
+
# Check if the post-hook has an fc argument
|
|
1015
|
+
if "fc" in signature(self.function.post_hook).parameters:
|
|
1016
|
+
post_hook_args["fc"] = self
|
|
1017
|
+
|
|
1018
|
+
await self.function.post_hook(**post_hook_args)
|
|
1019
|
+
except AgentRunException as e:
|
|
1020
|
+
log_debug(f"{e.__class__.__name__}: {e}")
|
|
1021
|
+
self.error = str(e)
|
|
1022
|
+
raise
|
|
1023
|
+
except Exception as e:
|
|
1024
|
+
log_warning(f"Error in post-hook callback: {e}")
|
|
1025
|
+
log_exception(e)
|
|
1026
|
+
|
|
1027
|
+
async def _build_nested_execution_chain_async(self, entrypoint_args: Dict[str, Any]):
|
|
1028
|
+
"""Build a nested chain of async hook executions with the entrypoint at the center.
|
|
1029
|
+
|
|
1030
|
+
Similar to _build_nested_execution_chain but for async execution.
|
|
1031
|
+
"""
|
|
1032
|
+
from functools import reduce
|
|
1033
|
+
from inspect import isasyncgenfunction, iscoroutinefunction
|
|
1034
|
+
|
|
1035
|
+
async def execute_entrypoint_async(name, func, args):
|
|
1036
|
+
"""Execute the entrypoint function asynchronously."""
|
|
1037
|
+
arguments = entrypoint_args.copy()
|
|
1038
|
+
if self.arguments is not None:
|
|
1039
|
+
arguments.update(self.arguments)
|
|
1040
|
+
|
|
1041
|
+
result = self.function.entrypoint(**arguments) # type: ignore
|
|
1042
|
+
if iscoroutinefunction(self.function.entrypoint) and not isasyncgenfunction(self.function.entrypoint):
|
|
1043
|
+
result = await result
|
|
1044
|
+
return result
|
|
1045
|
+
|
|
1046
|
+
def execute_entrypoint(name, func, args):
|
|
1047
|
+
"""Execute the entrypoint function synchronously."""
|
|
1048
|
+
arguments = entrypoint_args.copy()
|
|
1049
|
+
if self.arguments is not None:
|
|
1050
|
+
arguments.update(self.arguments)
|
|
1051
|
+
return self.function.entrypoint(**arguments) # type: ignore
|
|
1052
|
+
|
|
1053
|
+
# If no hooks, just return the entrypoint execution function
|
|
1054
|
+
if not self.function.tool_hooks:
|
|
1055
|
+
return execute_entrypoint
|
|
1056
|
+
|
|
1057
|
+
def create_hook_wrapper(inner_func, hook):
|
|
1058
|
+
"""Create a nested wrapper for the hook."""
|
|
1059
|
+
|
|
1060
|
+
async def wrapper(name, func, args):
|
|
1061
|
+
"""Create a nested wrapper for the hook."""
|
|
1062
|
+
|
|
1063
|
+
# Pass the inner function as next_func to the hook
|
|
1064
|
+
# The hook will call next_func to continue the chain
|
|
1065
|
+
async def next_func(**kwargs):
|
|
1066
|
+
if iscoroutinefunction(inner_func):
|
|
1067
|
+
return await inner_func(name, func, kwargs)
|
|
1068
|
+
else:
|
|
1069
|
+
return inner_func(name, func, kwargs)
|
|
1070
|
+
|
|
1071
|
+
hook_args = self._build_hook_args(hook, name, next_func, args)
|
|
1072
|
+
|
|
1073
|
+
if iscoroutinefunction(hook):
|
|
1074
|
+
return await hook(**hook_args)
|
|
1075
|
+
else:
|
|
1076
|
+
return hook(**hook_args)
|
|
1077
|
+
|
|
1078
|
+
return wrapper
|
|
1079
|
+
|
|
1080
|
+
# Build the chain from inside out - reverse the hooks to start from the innermost
|
|
1081
|
+
hooks = list(reversed(self.function.tool_hooks))
|
|
1082
|
+
|
|
1083
|
+
# Handle async and sync entrypoints
|
|
1084
|
+
if iscoroutinefunction(self.function.entrypoint):
|
|
1085
|
+
chain = reduce(create_hook_wrapper, hooks, execute_entrypoint_async)
|
|
1086
|
+
else:
|
|
1087
|
+
chain = reduce(create_hook_wrapper, hooks, execute_entrypoint)
|
|
1088
|
+
return chain
|
|
1089
|
+
|
|
1090
|
+
async def aexecute(self) -> FunctionExecutionResult:
|
|
1091
|
+
"""Runs the function call asynchronously."""
|
|
1092
|
+
from inspect import isasyncgen, isasyncgenfunction, iscoroutinefunction, isgenerator, isgeneratorfunction
|
|
1093
|
+
|
|
1094
|
+
if self.function.entrypoint is None:
|
|
1095
|
+
return FunctionExecutionResult(status="failure", error="Entrypoint is not set")
|
|
1096
|
+
|
|
1097
|
+
log_debug(f"Running: {self.get_call_str()}")
|
|
1098
|
+
|
|
1099
|
+
# Execute pre-hook if it exists
|
|
1100
|
+
if iscoroutinefunction(self.function.pre_hook):
|
|
1101
|
+
await self._handle_pre_hook_async()
|
|
1102
|
+
else:
|
|
1103
|
+
self._handle_pre_hook()
|
|
1104
|
+
|
|
1105
|
+
entrypoint_args = self._build_entrypoint_args()
|
|
1106
|
+
|
|
1107
|
+
# Check cache if enabled and not a generator function
|
|
1108
|
+
if self.function.cache_results and not (
|
|
1109
|
+
isasyncgenfunction(self.function.entrypoint) or isgeneratorfunction(self.function.entrypoint)
|
|
1110
|
+
):
|
|
1111
|
+
cache_key = self.function._get_cache_key(entrypoint_args, self.arguments)
|
|
1112
|
+
cache_file = self.function._get_cache_file_path(cache_key)
|
|
1113
|
+
cached_result = self.function._get_cached_result(cache_file)
|
|
1114
|
+
if cached_result is not None:
|
|
1115
|
+
log_debug(f"Cache hit for: {self.get_call_str()}")
|
|
1116
|
+
self.result = cached_result
|
|
1117
|
+
return FunctionExecutionResult(status="success", result=cached_result)
|
|
1118
|
+
|
|
1119
|
+
# Execute function
|
|
1120
|
+
execution_result = None
|
|
1121
|
+
exception_to_raise = None
|
|
1122
|
+
|
|
1123
|
+
try:
|
|
1124
|
+
# Build and execute the nested chain of hooks
|
|
1125
|
+
if self.function.tool_hooks is not None:
|
|
1126
|
+
execution_chain = await self._build_nested_execution_chain_async(entrypoint_args)
|
|
1127
|
+
self.result = await execution_chain(self.function.name, self.function.entrypoint, self.arguments or {})
|
|
1128
|
+
else:
|
|
1129
|
+
if self.arguments is None or self.arguments == {}:
|
|
1130
|
+
result = self.function.entrypoint(**entrypoint_args)
|
|
1131
|
+
else:
|
|
1132
|
+
result = self.function.entrypoint(**entrypoint_args, **self.arguments)
|
|
1133
|
+
|
|
1134
|
+
if isasyncgenfunction(self.function.entrypoint):
|
|
1135
|
+
self.result = result # Store async generator directly
|
|
1136
|
+
else:
|
|
1137
|
+
self.result = await result
|
|
1138
|
+
|
|
1139
|
+
# Only cache if not a generator
|
|
1140
|
+
if self.function.cache_results and not (isgenerator(self.result) or isasyncgen(self.result)):
|
|
1141
|
+
cache_key = self.function._get_cache_key(entrypoint_args, self.arguments)
|
|
1142
|
+
cache_file = self.function._get_cache_file_path(cache_key)
|
|
1143
|
+
self.function._save_to_cache(cache_file, self.result)
|
|
1144
|
+
|
|
1145
|
+
updated_session_state = None
|
|
1146
|
+
if entrypoint_args.get("run_context") is not None:
|
|
1147
|
+
run_context = entrypoint_args.get("run_context")
|
|
1148
|
+
updated_session_state = (
|
|
1149
|
+
run_context.session_state
|
|
1150
|
+
if run_context is not None and run_context.session_state is not None
|
|
1151
|
+
else None
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
execution_result = FunctionExecutionResult(
|
|
1155
|
+
status="success", result=self.result, updated_session_state=updated_session_state
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
except AgentRunException as e:
|
|
1159
|
+
log_debug(f"{e.__class__.__name__}: {e}")
|
|
1160
|
+
self.error = str(e)
|
|
1161
|
+
exception_to_raise = e
|
|
1162
|
+
except Exception as e:
|
|
1163
|
+
log_warning(f"Could not run function {self.get_call_str()}")
|
|
1164
|
+
log_exception(e)
|
|
1165
|
+
self.error = str(e)
|
|
1166
|
+
execution_result = FunctionExecutionResult(status="failure", error=str(e))
|
|
1167
|
+
|
|
1168
|
+
finally:
|
|
1169
|
+
if iscoroutinefunction(self.function.post_hook):
|
|
1170
|
+
await self._handle_post_hook_async()
|
|
1171
|
+
else:
|
|
1172
|
+
self._handle_post_hook()
|
|
1173
|
+
|
|
1174
|
+
if exception_to_raise is not None:
|
|
1175
|
+
raise exception_to_raise
|
|
1176
|
+
|
|
1177
|
+
return execution_result # type: ignore[return-value]
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
class ToolResult(BaseModel):
|
|
1181
|
+
"""Result from a tool that can include media artifacts."""
|
|
1182
|
+
|
|
1183
|
+
content: str
|
|
1184
|
+
images: Optional[List[Image]] = None
|
|
1185
|
+
videos: Optional[List[Video]] = None
|
|
1186
|
+
audios: Optional[List[Audio]] = None
|
|
1187
|
+
files: Optional[List[File]] = None
|