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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from agno.agent.agent import Agent
|
|
7
|
+
from agno.os.interfaces.slack.security import verify_slack_signature
|
|
8
|
+
from agno.team.team import Team
|
|
9
|
+
from agno.tools.slack import SlackTools
|
|
10
|
+
from agno.utils.log import log_info
|
|
11
|
+
from agno.workflow.workflow import Workflow
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SlackEventResponse(BaseModel):
|
|
15
|
+
"""Response model for Slack event processing"""
|
|
16
|
+
|
|
17
|
+
status: str = Field(default="ok", description="Processing status")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SlackChallengeResponse(BaseModel):
|
|
21
|
+
"""Response model for Slack URL verification challenge"""
|
|
22
|
+
|
|
23
|
+
challenge: str = Field(description="Challenge string to echo back to Slack")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def attach_routes(
|
|
27
|
+
router: APIRouter,
|
|
28
|
+
agent: Optional[Agent] = None,
|
|
29
|
+
team: Optional[Team] = None,
|
|
30
|
+
workflow: Optional[Workflow] = None,
|
|
31
|
+
reply_to_mentions_only: bool = True,
|
|
32
|
+
) -> APIRouter:
|
|
33
|
+
# Determine entity type for documentation
|
|
34
|
+
entity_type = "agent" if agent else "team" if team else "workflow" if workflow else "unknown"
|
|
35
|
+
|
|
36
|
+
@router.post(
|
|
37
|
+
"/events",
|
|
38
|
+
operation_id=f"slack_events_{entity_type}",
|
|
39
|
+
name="slack_events",
|
|
40
|
+
description="Process incoming Slack events",
|
|
41
|
+
response_model=Union[SlackChallengeResponse, SlackEventResponse],
|
|
42
|
+
response_model_exclude_none=True,
|
|
43
|
+
responses={
|
|
44
|
+
200: {"description": "Event processed successfully"},
|
|
45
|
+
400: {"description": "Missing Slack headers"},
|
|
46
|
+
403: {"description": "Invalid Slack signature"},
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
async def slack_events(request: Request, background_tasks: BackgroundTasks):
|
|
50
|
+
body = await request.body()
|
|
51
|
+
timestamp = request.headers.get("X-Slack-Request-Timestamp")
|
|
52
|
+
slack_signature = request.headers.get("X-Slack-Signature", "")
|
|
53
|
+
|
|
54
|
+
if not timestamp or not slack_signature:
|
|
55
|
+
raise HTTPException(status_code=400, detail="Missing Slack headers")
|
|
56
|
+
|
|
57
|
+
if not verify_slack_signature(body, timestamp, slack_signature):
|
|
58
|
+
raise HTTPException(status_code=403, detail="Invalid signature")
|
|
59
|
+
|
|
60
|
+
data = await request.json()
|
|
61
|
+
|
|
62
|
+
# Handle URL verification
|
|
63
|
+
if data.get("type") == "url_verification":
|
|
64
|
+
return SlackChallengeResponse(challenge=data.get("challenge"))
|
|
65
|
+
|
|
66
|
+
# Process other event types (e.g., message events) asynchronously
|
|
67
|
+
if "event" in data:
|
|
68
|
+
event = data["event"]
|
|
69
|
+
if event.get("bot_id"):
|
|
70
|
+
log_info("bot event")
|
|
71
|
+
pass
|
|
72
|
+
else:
|
|
73
|
+
background_tasks.add_task(_process_slack_event, event)
|
|
74
|
+
|
|
75
|
+
return SlackEventResponse(status="ok")
|
|
76
|
+
|
|
77
|
+
async def _process_slack_event(event: dict):
|
|
78
|
+
event_type = event.get("type")
|
|
79
|
+
|
|
80
|
+
# Only handle app_mention and message events
|
|
81
|
+
if event_type not in ("app_mention", "message"):
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
channel_type = event.get("channel_type", "")
|
|
85
|
+
|
|
86
|
+
# Handle duplicate replies
|
|
87
|
+
if not reply_to_mentions_only and event_type == "app_mention":
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# If reply_to_mentions_only is True, ignore every message that is not a DM
|
|
91
|
+
if reply_to_mentions_only and event_type == "message" and channel_type != "im":
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Extract event data
|
|
95
|
+
user = None
|
|
96
|
+
message_text = event.get("text", "")
|
|
97
|
+
channel_id = event.get("channel", "")
|
|
98
|
+
user = event.get("user")
|
|
99
|
+
if event.get("thread_ts"):
|
|
100
|
+
ts = event.get("thread_ts", "")
|
|
101
|
+
else:
|
|
102
|
+
ts = event.get("ts", "")
|
|
103
|
+
|
|
104
|
+
# Use the timestamp as the session id, so that each thread is a separate session
|
|
105
|
+
session_id = ts
|
|
106
|
+
|
|
107
|
+
if agent:
|
|
108
|
+
response = await agent.arun(message_text, user_id=user, session_id=session_id)
|
|
109
|
+
elif team:
|
|
110
|
+
response = await team.arun(message_text, user_id=user, session_id=session_id) # type: ignore
|
|
111
|
+
elif workflow:
|
|
112
|
+
response = await workflow.arun(message_text, user_id=user, session_id=session_id) # type: ignore
|
|
113
|
+
|
|
114
|
+
if response:
|
|
115
|
+
if hasattr(response, "reasoning_content") and response.reasoning_content:
|
|
116
|
+
_send_slack_message(
|
|
117
|
+
channel=channel_id,
|
|
118
|
+
message=f"Reasoning: \n{response.reasoning_content}",
|
|
119
|
+
thread_ts=ts,
|
|
120
|
+
italics=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
_send_slack_message(channel=channel_id, message=response.content or "", thread_ts=ts)
|
|
124
|
+
|
|
125
|
+
def _send_slack_message(channel: str, thread_ts: str, message: str, italics: bool = False):
|
|
126
|
+
if len(message) <= 40000:
|
|
127
|
+
if italics:
|
|
128
|
+
# Handle multi-line messages by making each line italic
|
|
129
|
+
formatted_message = "\n".join([f"_{line}_" for line in message.split("\n")])
|
|
130
|
+
SlackTools().send_message_thread(channel=channel, text=formatted_message or "", thread_ts=thread_ts)
|
|
131
|
+
else:
|
|
132
|
+
SlackTools().send_message_thread(channel=channel, text=message or "", thread_ts=thread_ts)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Split message into batches of 4000 characters (WhatsApp message limit is 4096)
|
|
136
|
+
message_batches = [message[i : i + 40000] for i in range(0, len(message), 40000)]
|
|
137
|
+
|
|
138
|
+
# Add a prefix with the batch number
|
|
139
|
+
for i, batch in enumerate(message_batches, 1):
|
|
140
|
+
batch_message = f"[{i}/{len(message_batches)}] {batch}"
|
|
141
|
+
if italics:
|
|
142
|
+
# Handle multi-line messages by making each line italic
|
|
143
|
+
formatted_batch = "\n".join([f"_{line}_" for line in batch_message.split("\n")])
|
|
144
|
+
SlackTools().send_message_thread(channel=channel, text=formatted_batch or "", thread_ts=thread_ts)
|
|
145
|
+
else:
|
|
146
|
+
SlackTools().send_message_thread(channel=channel, text=batch_message or "", thread_ts=thread_ts)
|
|
147
|
+
|
|
148
|
+
return router
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import time
|
|
4
|
+
from os import getenv
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException
|
|
7
|
+
|
|
8
|
+
from agno.utils.log import log_error
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
SLACK_SIGNING_SECRET = getenv("SLACK_SIGNING_SECRET")
|
|
12
|
+
except Exception as e:
|
|
13
|
+
log_error(f"Slack signin secret missing: {e}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def verify_slack_signature(body: bytes, timestamp: str, slack_signature: str) -> bool:
|
|
17
|
+
if not SLACK_SIGNING_SECRET:
|
|
18
|
+
raise HTTPException(status_code=500, detail="SLACK_SIGNING_SECRET is not set")
|
|
19
|
+
|
|
20
|
+
# Ensure the request timestamp is recent (e.g., to prevent replay attacks)
|
|
21
|
+
if abs(time.time() - int(timestamp)) > 60 * 5:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
|
25
|
+
my_signature = (
|
|
26
|
+
"v0="
|
|
27
|
+
+ hmac.new(SLACK_SIGNING_SECRET.encode("utf-8"), sig_basestring.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return hmac.compare_digest(my_signature, slack_signature)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi.routing import APIRouter
|
|
4
|
+
|
|
5
|
+
from agno.agent.agent import Agent
|
|
6
|
+
from agno.os.interfaces.base import BaseInterface
|
|
7
|
+
from agno.os.interfaces.slack.router import attach_routes
|
|
8
|
+
from agno.team.team import Team
|
|
9
|
+
from agno.workflow.workflow import Workflow
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Slack(BaseInterface):
|
|
13
|
+
type = "slack"
|
|
14
|
+
|
|
15
|
+
router: APIRouter
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
agent: Optional[Agent] = None,
|
|
20
|
+
team: Optional[Team] = None,
|
|
21
|
+
workflow: Optional[Workflow] = None,
|
|
22
|
+
prefix: str = "/slack",
|
|
23
|
+
tags: Optional[List[str]] = None,
|
|
24
|
+
reply_to_mentions_only: bool = True,
|
|
25
|
+
):
|
|
26
|
+
self.agent = agent
|
|
27
|
+
self.team = team
|
|
28
|
+
self.workflow = workflow
|
|
29
|
+
self.prefix = prefix
|
|
30
|
+
self.tags = tags or ["Slack"]
|
|
31
|
+
self.reply_to_mentions_only = reply_to_mentions_only
|
|
32
|
+
|
|
33
|
+
if not (self.agent or self.team or self.workflow):
|
|
34
|
+
raise ValueError("Slack requires an agent, team or workflow")
|
|
35
|
+
|
|
36
|
+
def get_router(self) -> APIRouter:
|
|
37
|
+
self.router = APIRouter(prefix=self.prefix, tags=self.tags) # type: ignore
|
|
38
|
+
|
|
39
|
+
self.router = attach_routes(
|
|
40
|
+
router=self.router,
|
|
41
|
+
agent=self.agent,
|
|
42
|
+
team=self.team,
|
|
43
|
+
workflow=self.workflow,
|
|
44
|
+
reply_to_mentions_only=self.reply_to_mentions_only,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return self.router
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
|
|
6
|
+
from fastapi.responses import PlainTextResponse
|
|
7
|
+
|
|
8
|
+
from agno.agent.agent import Agent
|
|
9
|
+
from agno.media import Audio, File, Image, Video
|
|
10
|
+
from agno.team.team import Team
|
|
11
|
+
from agno.tools.whatsapp import WhatsAppTools
|
|
12
|
+
from agno.utils.log import log_error, log_info, log_warning
|
|
13
|
+
from agno.utils.whatsapp import get_media_async, send_image_message_async, typing_indicator_async, upload_media_async
|
|
14
|
+
|
|
15
|
+
from .security import validate_webhook_signature
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Optional[Team] = None) -> APIRouter:
|
|
19
|
+
if agent is None and team is None:
|
|
20
|
+
raise ValueError("Either agent or team must be provided.")
|
|
21
|
+
|
|
22
|
+
# Create WhatsApp tools instance once for reuse
|
|
23
|
+
whatsapp_tools = WhatsAppTools(async_mode=True)
|
|
24
|
+
|
|
25
|
+
@router.get("/status")
|
|
26
|
+
async def status():
|
|
27
|
+
return {"status": "available"}
|
|
28
|
+
|
|
29
|
+
@router.get("/webhook")
|
|
30
|
+
async def verify_webhook(request: Request):
|
|
31
|
+
"""Handle WhatsApp webhook verification"""
|
|
32
|
+
mode = request.query_params.get("hub.mode")
|
|
33
|
+
token = request.query_params.get("hub.verify_token")
|
|
34
|
+
challenge = request.query_params.get("hub.challenge")
|
|
35
|
+
|
|
36
|
+
verify_token = getenv("WHATSAPP_VERIFY_TOKEN")
|
|
37
|
+
if not verify_token:
|
|
38
|
+
raise HTTPException(status_code=500, detail="WHATSAPP_VERIFY_TOKEN is not set")
|
|
39
|
+
|
|
40
|
+
if mode == "subscribe" and token == verify_token:
|
|
41
|
+
if not challenge:
|
|
42
|
+
raise HTTPException(status_code=400, detail="No challenge received")
|
|
43
|
+
return PlainTextResponse(content=challenge)
|
|
44
|
+
|
|
45
|
+
raise HTTPException(status_code=403, detail="Invalid verify token or mode")
|
|
46
|
+
|
|
47
|
+
@router.post("/webhook")
|
|
48
|
+
async def webhook(request: Request, background_tasks: BackgroundTasks):
|
|
49
|
+
"""Handle incoming WhatsApp messages"""
|
|
50
|
+
try:
|
|
51
|
+
# Get raw payload for signature validation
|
|
52
|
+
payload = await request.body()
|
|
53
|
+
signature = request.headers.get("X-Hub-Signature-256")
|
|
54
|
+
|
|
55
|
+
# Validate webhook signature
|
|
56
|
+
if not validate_webhook_signature(payload, signature):
|
|
57
|
+
log_warning("Invalid webhook signature")
|
|
58
|
+
raise HTTPException(status_code=403, detail="Invalid signature")
|
|
59
|
+
|
|
60
|
+
body = await request.json()
|
|
61
|
+
|
|
62
|
+
# Validate webhook data
|
|
63
|
+
if body.get("object") != "whatsapp_business_account":
|
|
64
|
+
log_warning(f"Received non-WhatsApp webhook object: {body.get('object')}")
|
|
65
|
+
return {"status": "ignored"}
|
|
66
|
+
|
|
67
|
+
# Process messages in background
|
|
68
|
+
for entry in body.get("entry", []):
|
|
69
|
+
for change in entry.get("changes", []):
|
|
70
|
+
messages = change.get("value", {}).get("messages", [])
|
|
71
|
+
|
|
72
|
+
if not messages:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
message = messages[0]
|
|
76
|
+
background_tasks.add_task(process_message, message, agent, team)
|
|
77
|
+
|
|
78
|
+
return {"status": "processing"}
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
log_error(f"Error processing webhook: {str(e)}")
|
|
82
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
83
|
+
|
|
84
|
+
async def process_message(message: dict, agent: Optional[Agent], team: Optional[Team]):
|
|
85
|
+
"""Process a single WhatsApp message in the background"""
|
|
86
|
+
try:
|
|
87
|
+
message_image = None
|
|
88
|
+
message_video = None
|
|
89
|
+
message_audio = None
|
|
90
|
+
message_doc = None
|
|
91
|
+
|
|
92
|
+
message_id = message.get("id")
|
|
93
|
+
await typing_indicator_async(message_id)
|
|
94
|
+
|
|
95
|
+
if message.get("type") == "text":
|
|
96
|
+
message_text = message["text"]["body"]
|
|
97
|
+
elif message.get("type") == "image":
|
|
98
|
+
try:
|
|
99
|
+
message_text = message["image"]["caption"]
|
|
100
|
+
except Exception:
|
|
101
|
+
message_text = "Describe the image"
|
|
102
|
+
message_image = message["image"]["id"]
|
|
103
|
+
elif message.get("type") == "video":
|
|
104
|
+
try:
|
|
105
|
+
message_text = message["video"]["caption"]
|
|
106
|
+
except Exception:
|
|
107
|
+
message_text = "Describe the video"
|
|
108
|
+
message_video = message["video"]["id"]
|
|
109
|
+
elif message.get("type") == "audio":
|
|
110
|
+
message_text = "Reply to audio"
|
|
111
|
+
message_audio = message["audio"]["id"]
|
|
112
|
+
elif message.get("type") == "document":
|
|
113
|
+
message_text = "Process the document"
|
|
114
|
+
message_doc = message["document"]["id"]
|
|
115
|
+
else:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
phone_number = message["from"]
|
|
119
|
+
log_info(f"Processing message from {phone_number}: {message_text}")
|
|
120
|
+
|
|
121
|
+
# Generate and send response
|
|
122
|
+
if agent:
|
|
123
|
+
response = await agent.arun(
|
|
124
|
+
message_text,
|
|
125
|
+
user_id=phone_number,
|
|
126
|
+
session_id=f"wa:{phone_number}",
|
|
127
|
+
images=[Image(content=await get_media_async(message_image))] if message_image else None,
|
|
128
|
+
files=[File(content=await get_media_async(message_doc))] if message_doc else None,
|
|
129
|
+
videos=[Video(content=await get_media_async(message_video))] if message_video else None,
|
|
130
|
+
audio=[Audio(content=await get_media_async(message_audio))] if message_audio else None,
|
|
131
|
+
)
|
|
132
|
+
elif team:
|
|
133
|
+
response = await team.arun( # type: ignore
|
|
134
|
+
message_text,
|
|
135
|
+
user_id=phone_number,
|
|
136
|
+
session_id=f"wa:{phone_number}",
|
|
137
|
+
files=[File(content=await get_media_async(message_doc))] if message_doc else None,
|
|
138
|
+
images=[Image(content=await get_media_async(message_image))] if message_image else None,
|
|
139
|
+
videos=[Video(content=await get_media_async(message_video))] if message_video else None,
|
|
140
|
+
audio=[Audio(content=await get_media_async(message_audio))] if message_audio else None,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if response.reasoning_content:
|
|
144
|
+
await _send_whatsapp_message(phone_number, f"Reasoning: \n{response.reasoning_content}", italics=True)
|
|
145
|
+
|
|
146
|
+
if response.images:
|
|
147
|
+
number_of_images = len(response.images)
|
|
148
|
+
log_info(f"images generated: f{number_of_images}")
|
|
149
|
+
for i in range(number_of_images):
|
|
150
|
+
image_content = response.images[i].content
|
|
151
|
+
image_bytes = None
|
|
152
|
+
if isinstance(image_content, bytes):
|
|
153
|
+
try:
|
|
154
|
+
decoded_string = image_content.decode("utf-8")
|
|
155
|
+
|
|
156
|
+
image_bytes = base64.b64decode(decoded_string)
|
|
157
|
+
except UnicodeDecodeError:
|
|
158
|
+
image_bytes = image_content
|
|
159
|
+
elif isinstance(image_content, str):
|
|
160
|
+
image_bytes = base64.b64decode(image_content)
|
|
161
|
+
else:
|
|
162
|
+
log_error(f"Unexpected image content type: {type(image_content)} for user {phone_number}")
|
|
163
|
+
|
|
164
|
+
if image_bytes:
|
|
165
|
+
media_id = await upload_media_async(
|
|
166
|
+
media_data=image_bytes, mime_type="image/png", filename="image.png"
|
|
167
|
+
)
|
|
168
|
+
await send_image_message_async(media_id=media_id, recipient=phone_number, text=response.content)
|
|
169
|
+
else:
|
|
170
|
+
log_warning(
|
|
171
|
+
f"Could not process image content for user {phone_number}. Type: {type(image_content)}"
|
|
172
|
+
)
|
|
173
|
+
await _send_whatsapp_message(phone_number, response.content) # type: ignore
|
|
174
|
+
else:
|
|
175
|
+
await _send_whatsapp_message(phone_number, response.content) # type: ignore
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
log_error(f"Error processing message: {str(e)}")
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
await _send_whatsapp_message(
|
|
182
|
+
phone_number, "Sorry, there was an error processing your message. Please try again later."
|
|
183
|
+
)
|
|
184
|
+
except Exception as send_error:
|
|
185
|
+
log_error(f"Error sending error message: {str(send_error)}")
|
|
186
|
+
|
|
187
|
+
async def _send_whatsapp_message(recipient: str, message: str, italics: bool = False):
|
|
188
|
+
if len(message) <= 4096:
|
|
189
|
+
if italics:
|
|
190
|
+
# Handle multi-line messages by making each line italic
|
|
191
|
+
formatted_message = "\n".join([f"_{line}_" for line in message.split("\n")])
|
|
192
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=formatted_message)
|
|
193
|
+
else:
|
|
194
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=message)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Split message into batches of 4000 characters (WhatsApp message limit is 4096)
|
|
198
|
+
message_batches = [message[i : i + 4000] for i in range(0, len(message), 4000)]
|
|
199
|
+
|
|
200
|
+
# Add a prefix with the batch number
|
|
201
|
+
for i, batch in enumerate(message_batches, 1):
|
|
202
|
+
batch_message = f"[{i}/{len(message_batches)}] {batch}"
|
|
203
|
+
if italics:
|
|
204
|
+
# Handle multi-line messages by making each line italic
|
|
205
|
+
formatted_batch = "\n".join([f"_{line}_" for line in batch_message.split("\n")])
|
|
206
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=formatted_batch)
|
|
207
|
+
else:
|
|
208
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=batch_message)
|
|
209
|
+
|
|
210
|
+
return router
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from agno.utils.log import log_warning
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_development_mode() -> bool:
|
|
10
|
+
"""Check if the application is running in development mode."""
|
|
11
|
+
return os.getenv("APP_ENV", "development").lower() == "development"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_app_secret() -> str:
|
|
15
|
+
"""
|
|
16
|
+
Get the WhatsApp app secret from environment variables.
|
|
17
|
+
In development mode, returns a dummy secret if WHATSAPP_APP_SECRET is not set.
|
|
18
|
+
"""
|
|
19
|
+
app_secret = os.getenv("WHATSAPP_APP_SECRET")
|
|
20
|
+
|
|
21
|
+
if not app_secret:
|
|
22
|
+
raise ValueError("WHATSAPP_APP_SECRET environment variable is not set in production mode")
|
|
23
|
+
|
|
24
|
+
return app_secret
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_webhook_signature(payload: bytes, signature_header: Optional[str]) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Validate the webhook payload using SHA256 signature.
|
|
30
|
+
In development mode, signature validation can be bypassed.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
payload: The raw request payload bytes
|
|
34
|
+
signature_header: The X-Hub-Signature-256 header value
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: True if signature is valid or in development mode, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
# In development mode, we can bypass signature validation
|
|
40
|
+
if is_development_mode():
|
|
41
|
+
log_warning("Bypassing signature validation in development mode")
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
if not signature_header or not signature_header.startswith("sha256="):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
app_secret = get_app_secret()
|
|
48
|
+
expected_signature = signature_header.split("sha256=")[1]
|
|
49
|
+
|
|
50
|
+
# Calculate signature
|
|
51
|
+
hmac_obj = hmac.new(app_secret.encode("utf-8"), payload, hashlib.sha256)
|
|
52
|
+
calculated_signature = hmac_obj.hexdigest()
|
|
53
|
+
|
|
54
|
+
# Compare signatures using constant-time comparison
|
|
55
|
+
return hmac.compare_digest(calculated_signature, expected_signature)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi.routing import APIRouter
|
|
4
|
+
|
|
5
|
+
from agno.agent import Agent
|
|
6
|
+
from agno.os.interfaces.base import BaseInterface
|
|
7
|
+
from agno.os.interfaces.whatsapp.router import attach_routes
|
|
8
|
+
from agno.team import Team
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Whatsapp(BaseInterface):
|
|
12
|
+
type = "whatsapp"
|
|
13
|
+
|
|
14
|
+
router: APIRouter
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
agent: Optional[Agent] = None,
|
|
19
|
+
team: Optional[Team] = None,
|
|
20
|
+
prefix: str = "/whatsapp",
|
|
21
|
+
tags: Optional[List[str]] = None,
|
|
22
|
+
):
|
|
23
|
+
self.agent = agent
|
|
24
|
+
self.team = team
|
|
25
|
+
self.prefix = prefix
|
|
26
|
+
self.tags = tags or ["Whatsapp"]
|
|
27
|
+
|
|
28
|
+
if not (self.agent or self.team):
|
|
29
|
+
raise ValueError("Whatsapp requires an agent or a team")
|
|
30
|
+
|
|
31
|
+
def get_router(self) -> APIRouter:
|
|
32
|
+
self.router = APIRouter(prefix=self.prefix, tags=self.tags) # type: ignore
|
|
33
|
+
|
|
34
|
+
self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
|
|
35
|
+
|
|
36
|
+
return self.router
|