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.
Files changed (723) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +44 -5
  3. agno/agent/agent.py +10531 -2975
  4. agno/api/agent.py +14 -53
  5. agno/api/api.py +7 -46
  6. agno/api/evals.py +22 -0
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -25
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +6 -9
  11. agno/api/schemas/evals.py +16 -0
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +10 -10
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +16 -0
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +22 -26
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/compression/__init__.py +3 -0
  25. agno/compression/manager.py +247 -0
  26. agno/culture/__init__.py +3 -0
  27. agno/culture/manager.py +956 -0
  28. agno/db/__init__.py +24 -0
  29. agno/db/async_postgres/__init__.py +3 -0
  30. agno/db/base.py +946 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2781 -0
  33. agno/db/dynamo/schemas.py +442 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +2379 -0
  37. agno/db/firestore/schemas.py +181 -0
  38. agno/db/firestore/utils.py +376 -0
  39. agno/db/gcs_json/__init__.py +3 -0
  40. agno/db/gcs_json/gcs_json_db.py +1791 -0
  41. agno/db/gcs_json/utils.py +228 -0
  42. agno/db/in_memory/__init__.py +3 -0
  43. agno/db/in_memory/in_memory_db.py +1312 -0
  44. agno/db/in_memory/utils.py +230 -0
  45. agno/db/json/__init__.py +3 -0
  46. agno/db/json/json_db.py +1777 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/manager.py +199 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/migrations/versions/v2_3_0.py +938 -0
  51. agno/db/mongo/__init__.py +17 -0
  52. agno/db/mongo/async_mongo.py +2760 -0
  53. agno/db/mongo/mongo.py +2597 -0
  54. agno/db/mongo/schemas.py +119 -0
  55. agno/db/mongo/utils.py +276 -0
  56. agno/db/mysql/__init__.py +4 -0
  57. agno/db/mysql/async_mysql.py +2912 -0
  58. agno/db/mysql/mysql.py +2923 -0
  59. agno/db/mysql/schemas.py +186 -0
  60. agno/db/mysql/utils.py +488 -0
  61. agno/db/postgres/__init__.py +4 -0
  62. agno/db/postgres/async_postgres.py +2579 -0
  63. agno/db/postgres/postgres.py +2870 -0
  64. agno/db/postgres/schemas.py +187 -0
  65. agno/db/postgres/utils.py +442 -0
  66. agno/db/redis/__init__.py +3 -0
  67. agno/db/redis/redis.py +2141 -0
  68. agno/db/redis/schemas.py +159 -0
  69. agno/db/redis/utils.py +346 -0
  70. agno/db/schemas/__init__.py +4 -0
  71. agno/db/schemas/culture.py +120 -0
  72. agno/db/schemas/evals.py +34 -0
  73. agno/db/schemas/knowledge.py +40 -0
  74. agno/db/schemas/memory.py +61 -0
  75. agno/db/singlestore/__init__.py +3 -0
  76. agno/db/singlestore/schemas.py +179 -0
  77. agno/db/singlestore/singlestore.py +2877 -0
  78. agno/db/singlestore/utils.py +384 -0
  79. agno/db/sqlite/__init__.py +4 -0
  80. agno/db/sqlite/async_sqlite.py +2911 -0
  81. agno/db/sqlite/schemas.py +181 -0
  82. agno/db/sqlite/sqlite.py +2908 -0
  83. agno/db/sqlite/utils.py +429 -0
  84. agno/db/surrealdb/__init__.py +3 -0
  85. agno/db/surrealdb/metrics.py +292 -0
  86. agno/db/surrealdb/models.py +334 -0
  87. agno/db/surrealdb/queries.py +71 -0
  88. agno/db/surrealdb/surrealdb.py +1908 -0
  89. agno/db/surrealdb/utils.py +147 -0
  90. agno/db/utils.py +118 -0
  91. agno/eval/__init__.py +24 -0
  92. agno/eval/accuracy.py +666 -276
  93. agno/eval/agent_as_judge.py +861 -0
  94. agno/eval/base.py +29 -0
  95. agno/eval/performance.py +779 -0
  96. agno/eval/reliability.py +241 -62
  97. agno/eval/utils.py +120 -0
  98. agno/exceptions.py +143 -1
  99. agno/filters.py +354 -0
  100. agno/guardrails/__init__.py +6 -0
  101. agno/guardrails/base.py +19 -0
  102. agno/guardrails/openai.py +144 -0
  103. agno/guardrails/pii.py +94 -0
  104. agno/guardrails/prompt_injection.py +52 -0
  105. agno/hooks/__init__.py +3 -0
  106. agno/hooks/decorator.py +164 -0
  107. agno/integrations/discord/__init__.py +3 -0
  108. agno/integrations/discord/client.py +203 -0
  109. agno/knowledge/__init__.py +5 -1
  110. agno/{document → knowledge}/chunking/agentic.py +22 -14
  111. agno/{document → knowledge}/chunking/document.py +2 -2
  112. agno/{document → knowledge}/chunking/fixed.py +7 -6
  113. agno/knowledge/chunking/markdown.py +151 -0
  114. agno/{document → knowledge}/chunking/recursive.py +15 -3
  115. agno/knowledge/chunking/row.py +39 -0
  116. agno/knowledge/chunking/semantic.py +91 -0
  117. agno/knowledge/chunking/strategy.py +165 -0
  118. agno/knowledge/content.py +74 -0
  119. agno/knowledge/document/__init__.py +5 -0
  120. agno/{document → knowledge/document}/base.py +12 -2
  121. agno/knowledge/embedder/__init__.py +5 -0
  122. agno/knowledge/embedder/aws_bedrock.py +343 -0
  123. agno/knowledge/embedder/azure_openai.py +210 -0
  124. agno/{embedder → knowledge/embedder}/base.py +8 -0
  125. agno/knowledge/embedder/cohere.py +323 -0
  126. agno/knowledge/embedder/fastembed.py +62 -0
  127. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  128. agno/knowledge/embedder/google.py +258 -0
  129. agno/knowledge/embedder/huggingface.py +94 -0
  130. agno/knowledge/embedder/jina.py +182 -0
  131. agno/knowledge/embedder/langdb.py +22 -0
  132. agno/knowledge/embedder/mistral.py +206 -0
  133. agno/knowledge/embedder/nebius.py +13 -0
  134. agno/knowledge/embedder/ollama.py +154 -0
  135. agno/knowledge/embedder/openai.py +195 -0
  136. agno/knowledge/embedder/sentence_transformer.py +63 -0
  137. agno/{embedder → knowledge/embedder}/together.py +1 -1
  138. agno/knowledge/embedder/vllm.py +262 -0
  139. agno/knowledge/embedder/voyageai.py +165 -0
  140. agno/knowledge/knowledge.py +3006 -0
  141. agno/knowledge/reader/__init__.py +7 -0
  142. agno/knowledge/reader/arxiv_reader.py +81 -0
  143. agno/knowledge/reader/base.py +95 -0
  144. agno/knowledge/reader/csv_reader.py +164 -0
  145. agno/knowledge/reader/docx_reader.py +82 -0
  146. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  147. agno/knowledge/reader/firecrawl_reader.py +201 -0
  148. agno/knowledge/reader/json_reader.py +88 -0
  149. agno/knowledge/reader/markdown_reader.py +137 -0
  150. agno/knowledge/reader/pdf_reader.py +431 -0
  151. agno/knowledge/reader/pptx_reader.py +101 -0
  152. agno/knowledge/reader/reader_factory.py +313 -0
  153. agno/knowledge/reader/s3_reader.py +89 -0
  154. agno/knowledge/reader/tavily_reader.py +193 -0
  155. agno/knowledge/reader/text_reader.py +127 -0
  156. agno/knowledge/reader/web_search_reader.py +325 -0
  157. agno/knowledge/reader/website_reader.py +455 -0
  158. agno/knowledge/reader/wikipedia_reader.py +91 -0
  159. agno/knowledge/reader/youtube_reader.py +78 -0
  160. agno/knowledge/remote_content/remote_content.py +88 -0
  161. agno/knowledge/reranker/__init__.py +3 -0
  162. agno/{reranker → knowledge/reranker}/base.py +1 -1
  163. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  164. agno/knowledge/reranker/infinity.py +195 -0
  165. agno/knowledge/reranker/sentence_transformer.py +54 -0
  166. agno/knowledge/types.py +39 -0
  167. agno/knowledge/utils.py +234 -0
  168. agno/media.py +439 -95
  169. agno/memory/__init__.py +16 -3
  170. agno/memory/manager.py +1474 -123
  171. agno/memory/strategies/__init__.py +15 -0
  172. agno/memory/strategies/base.py +66 -0
  173. agno/memory/strategies/summarize.py +196 -0
  174. agno/memory/strategies/types.py +37 -0
  175. agno/models/aimlapi/__init__.py +5 -0
  176. agno/models/aimlapi/aimlapi.py +62 -0
  177. agno/models/anthropic/__init__.py +4 -0
  178. agno/models/anthropic/claude.py +960 -496
  179. agno/models/aws/__init__.py +15 -0
  180. agno/models/aws/bedrock.py +686 -451
  181. agno/models/aws/claude.py +190 -183
  182. agno/models/azure/__init__.py +18 -1
  183. agno/models/azure/ai_foundry.py +489 -0
  184. agno/models/azure/openai_chat.py +89 -40
  185. agno/models/base.py +2477 -550
  186. agno/models/cerebras/__init__.py +12 -0
  187. agno/models/cerebras/cerebras.py +565 -0
  188. agno/models/cerebras/cerebras_openai.py +131 -0
  189. agno/models/cohere/__init__.py +4 -0
  190. agno/models/cohere/chat.py +306 -492
  191. agno/models/cometapi/__init__.py +5 -0
  192. agno/models/cometapi/cometapi.py +74 -0
  193. agno/models/dashscope/__init__.py +5 -0
  194. agno/models/dashscope/dashscope.py +90 -0
  195. agno/models/deepinfra/__init__.py +5 -0
  196. agno/models/deepinfra/deepinfra.py +45 -0
  197. agno/models/deepseek/__init__.py +4 -0
  198. agno/models/deepseek/deepseek.py +110 -9
  199. agno/models/fireworks/__init__.py +4 -0
  200. agno/models/fireworks/fireworks.py +19 -22
  201. agno/models/google/__init__.py +3 -7
  202. agno/models/google/gemini.py +1717 -662
  203. agno/models/google/utils.py +22 -0
  204. agno/models/groq/__init__.py +4 -0
  205. agno/models/groq/groq.py +391 -666
  206. agno/models/huggingface/__init__.py +4 -0
  207. agno/models/huggingface/huggingface.py +266 -538
  208. agno/models/ibm/__init__.py +5 -0
  209. agno/models/ibm/watsonx.py +432 -0
  210. agno/models/internlm/__init__.py +3 -0
  211. agno/models/internlm/internlm.py +20 -3
  212. agno/models/langdb/__init__.py +1 -0
  213. agno/models/langdb/langdb.py +60 -0
  214. agno/models/litellm/__init__.py +14 -0
  215. agno/models/litellm/chat.py +503 -0
  216. agno/models/litellm/litellm_openai.py +42 -0
  217. agno/models/llama_cpp/__init__.py +5 -0
  218. agno/models/llama_cpp/llama_cpp.py +22 -0
  219. agno/models/lmstudio/__init__.py +5 -0
  220. agno/models/lmstudio/lmstudio.py +25 -0
  221. agno/models/message.py +361 -39
  222. agno/models/meta/__init__.py +12 -0
  223. agno/models/meta/llama.py +502 -0
  224. agno/models/meta/llama_openai.py +79 -0
  225. agno/models/metrics.py +120 -0
  226. agno/models/mistral/__init__.py +4 -0
  227. agno/models/mistral/mistral.py +293 -393
  228. agno/models/nebius/__init__.py +3 -0
  229. agno/models/nebius/nebius.py +53 -0
  230. agno/models/nexus/__init__.py +3 -0
  231. agno/models/nexus/nexus.py +22 -0
  232. agno/models/nvidia/__init__.py +4 -0
  233. agno/models/nvidia/nvidia.py +22 -3
  234. agno/models/ollama/__init__.py +4 -2
  235. agno/models/ollama/chat.py +257 -492
  236. agno/models/openai/__init__.py +7 -0
  237. agno/models/openai/chat.py +725 -770
  238. agno/models/openai/like.py +16 -2
  239. agno/models/openai/responses.py +1121 -0
  240. agno/models/openrouter/__init__.py +4 -0
  241. agno/models/openrouter/openrouter.py +62 -5
  242. agno/models/perplexity/__init__.py +5 -0
  243. agno/models/perplexity/perplexity.py +203 -0
  244. agno/models/portkey/__init__.py +3 -0
  245. agno/models/portkey/portkey.py +82 -0
  246. agno/models/requesty/__init__.py +5 -0
  247. agno/models/requesty/requesty.py +69 -0
  248. agno/models/response.py +177 -7
  249. agno/models/sambanova/__init__.py +4 -0
  250. agno/models/sambanova/sambanova.py +23 -4
  251. agno/models/siliconflow/__init__.py +5 -0
  252. agno/models/siliconflow/siliconflow.py +42 -0
  253. agno/models/together/__init__.py +4 -0
  254. agno/models/together/together.py +21 -164
  255. agno/models/utils.py +266 -0
  256. agno/models/vercel/__init__.py +3 -0
  257. agno/models/vercel/v0.py +43 -0
  258. agno/models/vertexai/__init__.py +0 -1
  259. agno/models/vertexai/claude.py +190 -0
  260. agno/models/vllm/__init__.py +3 -0
  261. agno/models/vllm/vllm.py +83 -0
  262. agno/models/xai/__init__.py +2 -0
  263. agno/models/xai/xai.py +111 -7
  264. agno/os/__init__.py +3 -0
  265. agno/os/app.py +1027 -0
  266. agno/os/auth.py +244 -0
  267. agno/os/config.py +126 -0
  268. agno/os/interfaces/__init__.py +1 -0
  269. agno/os/interfaces/a2a/__init__.py +3 -0
  270. agno/os/interfaces/a2a/a2a.py +42 -0
  271. agno/os/interfaces/a2a/router.py +249 -0
  272. agno/os/interfaces/a2a/utils.py +924 -0
  273. agno/os/interfaces/agui/__init__.py +3 -0
  274. agno/os/interfaces/agui/agui.py +47 -0
  275. agno/os/interfaces/agui/router.py +147 -0
  276. agno/os/interfaces/agui/utils.py +574 -0
  277. agno/os/interfaces/base.py +25 -0
  278. agno/os/interfaces/slack/__init__.py +3 -0
  279. agno/os/interfaces/slack/router.py +148 -0
  280. agno/os/interfaces/slack/security.py +30 -0
  281. agno/os/interfaces/slack/slack.py +47 -0
  282. agno/os/interfaces/whatsapp/__init__.py +3 -0
  283. agno/os/interfaces/whatsapp/router.py +210 -0
  284. agno/os/interfaces/whatsapp/security.py +55 -0
  285. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  286. agno/os/mcp.py +293 -0
  287. agno/os/middleware/__init__.py +9 -0
  288. agno/os/middleware/jwt.py +797 -0
  289. agno/os/router.py +258 -0
  290. agno/os/routers/__init__.py +3 -0
  291. agno/os/routers/agents/__init__.py +3 -0
  292. agno/os/routers/agents/router.py +599 -0
  293. agno/os/routers/agents/schema.py +261 -0
  294. agno/os/routers/evals/__init__.py +3 -0
  295. agno/os/routers/evals/evals.py +450 -0
  296. agno/os/routers/evals/schemas.py +174 -0
  297. agno/os/routers/evals/utils.py +231 -0
  298. agno/os/routers/health.py +31 -0
  299. agno/os/routers/home.py +52 -0
  300. agno/os/routers/knowledge/__init__.py +3 -0
  301. agno/os/routers/knowledge/knowledge.py +1008 -0
  302. agno/os/routers/knowledge/schemas.py +178 -0
  303. agno/os/routers/memory/__init__.py +3 -0
  304. agno/os/routers/memory/memory.py +661 -0
  305. agno/os/routers/memory/schemas.py +88 -0
  306. agno/os/routers/metrics/__init__.py +3 -0
  307. agno/os/routers/metrics/metrics.py +190 -0
  308. agno/os/routers/metrics/schemas.py +47 -0
  309. agno/os/routers/session/__init__.py +3 -0
  310. agno/os/routers/session/session.py +997 -0
  311. agno/os/routers/teams/__init__.py +3 -0
  312. agno/os/routers/teams/router.py +512 -0
  313. agno/os/routers/teams/schema.py +257 -0
  314. agno/os/routers/traces/__init__.py +3 -0
  315. agno/os/routers/traces/schemas.py +414 -0
  316. agno/os/routers/traces/traces.py +499 -0
  317. agno/os/routers/workflows/__init__.py +3 -0
  318. agno/os/routers/workflows/router.py +624 -0
  319. agno/os/routers/workflows/schema.py +75 -0
  320. agno/os/schema.py +534 -0
  321. agno/os/scopes.py +469 -0
  322. agno/{playground → os}/settings.py +7 -15
  323. agno/os/utils.py +973 -0
  324. agno/reasoning/anthropic.py +80 -0
  325. agno/reasoning/azure_ai_foundry.py +67 -0
  326. agno/reasoning/deepseek.py +63 -0
  327. agno/reasoning/default.py +97 -0
  328. agno/reasoning/gemini.py +73 -0
  329. agno/reasoning/groq.py +71 -0
  330. agno/reasoning/helpers.py +24 -1
  331. agno/reasoning/ollama.py +67 -0
  332. agno/reasoning/openai.py +86 -0
  333. agno/reasoning/step.py +2 -1
  334. agno/reasoning/vertexai.py +76 -0
  335. agno/run/__init__.py +6 -0
  336. agno/run/agent.py +822 -0
  337. agno/run/base.py +247 -0
  338. agno/run/cancel.py +81 -0
  339. agno/run/requirement.py +181 -0
  340. agno/run/team.py +767 -0
  341. agno/run/workflow.py +708 -0
  342. agno/session/__init__.py +10 -0
  343. agno/session/agent.py +260 -0
  344. agno/session/summary.py +265 -0
  345. agno/session/team.py +342 -0
  346. agno/session/workflow.py +501 -0
  347. agno/table.py +10 -0
  348. agno/team/__init__.py +37 -0
  349. agno/team/team.py +9536 -0
  350. agno/tools/__init__.py +7 -0
  351. agno/tools/agentql.py +120 -0
  352. agno/tools/airflow.py +22 -12
  353. agno/tools/api.py +122 -0
  354. agno/tools/apify.py +276 -83
  355. agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
  356. agno/tools/aws_lambda.py +28 -7
  357. agno/tools/aws_ses.py +66 -0
  358. agno/tools/baidusearch.py +11 -4
  359. agno/tools/bitbucket.py +292 -0
  360. agno/tools/brandfetch.py +213 -0
  361. agno/tools/bravesearch.py +106 -0
  362. agno/tools/brightdata.py +367 -0
  363. agno/tools/browserbase.py +209 -0
  364. agno/tools/calcom.py +32 -23
  365. agno/tools/calculator.py +24 -37
  366. agno/tools/cartesia.py +187 -0
  367. agno/tools/{clickup_tool.py → clickup.py} +17 -28
  368. agno/tools/confluence.py +91 -26
  369. agno/tools/crawl4ai.py +139 -43
  370. agno/tools/csv_toolkit.py +28 -22
  371. agno/tools/dalle.py +36 -22
  372. agno/tools/daytona.py +475 -0
  373. agno/tools/decorator.py +169 -14
  374. agno/tools/desi_vocal.py +23 -11
  375. agno/tools/discord.py +32 -29
  376. agno/tools/docker.py +716 -0
  377. agno/tools/duckdb.py +76 -81
  378. agno/tools/duckduckgo.py +43 -40
  379. agno/tools/e2b.py +703 -0
  380. agno/tools/eleven_labs.py +65 -54
  381. agno/tools/email.py +13 -5
  382. agno/tools/evm.py +129 -0
  383. agno/tools/exa.py +324 -42
  384. agno/tools/fal.py +39 -35
  385. agno/tools/file.py +196 -30
  386. agno/tools/file_generation.py +356 -0
  387. agno/tools/financial_datasets.py +288 -0
  388. agno/tools/firecrawl.py +108 -33
  389. agno/tools/function.py +960 -122
  390. agno/tools/giphy.py +34 -12
  391. agno/tools/github.py +1294 -97
  392. agno/tools/gmail.py +922 -0
  393. agno/tools/google_bigquery.py +117 -0
  394. agno/tools/google_drive.py +271 -0
  395. agno/tools/google_maps.py +253 -0
  396. agno/tools/googlecalendar.py +607 -107
  397. agno/tools/googlesheets.py +377 -0
  398. agno/tools/hackernews.py +20 -12
  399. agno/tools/jina.py +24 -14
  400. agno/tools/jira.py +48 -19
  401. agno/tools/knowledge.py +218 -0
  402. agno/tools/linear.py +82 -43
  403. agno/tools/linkup.py +58 -0
  404. agno/tools/local_file_system.py +15 -7
  405. agno/tools/lumalab.py +41 -26
  406. agno/tools/mcp/__init__.py +10 -0
  407. agno/tools/mcp/mcp.py +331 -0
  408. agno/tools/mcp/multi_mcp.py +347 -0
  409. agno/tools/mcp/params.py +24 -0
  410. agno/tools/mcp_toolbox.py +284 -0
  411. agno/tools/mem0.py +193 -0
  412. agno/tools/memory.py +419 -0
  413. agno/tools/mlx_transcribe.py +11 -9
  414. agno/tools/models/azure_openai.py +190 -0
  415. agno/tools/models/gemini.py +203 -0
  416. agno/tools/models/groq.py +158 -0
  417. agno/tools/models/morph.py +186 -0
  418. agno/tools/models/nebius.py +124 -0
  419. agno/tools/models_labs.py +163 -82
  420. agno/tools/moviepy_video.py +18 -13
  421. agno/tools/nano_banana.py +151 -0
  422. agno/tools/neo4j.py +134 -0
  423. agno/tools/newspaper.py +15 -4
  424. agno/tools/newspaper4k.py +19 -6
  425. agno/tools/notion.py +204 -0
  426. agno/tools/openai.py +181 -17
  427. agno/tools/openbb.py +27 -20
  428. agno/tools/opencv.py +321 -0
  429. agno/tools/openweather.py +233 -0
  430. agno/tools/oxylabs.py +385 -0
  431. agno/tools/pandas.py +25 -15
  432. agno/tools/parallel.py +314 -0
  433. agno/tools/postgres.py +238 -185
  434. agno/tools/pubmed.py +125 -13
  435. agno/tools/python.py +48 -35
  436. agno/tools/reasoning.py +283 -0
  437. agno/tools/reddit.py +207 -29
  438. agno/tools/redshift.py +406 -0
  439. agno/tools/replicate.py +69 -26
  440. agno/tools/resend.py +11 -6
  441. agno/tools/scrapegraph.py +179 -19
  442. agno/tools/searxng.py +23 -31
  443. agno/tools/serpapi.py +15 -10
  444. agno/tools/serper.py +255 -0
  445. agno/tools/shell.py +23 -12
  446. agno/tools/shopify.py +1519 -0
  447. agno/tools/slack.py +56 -14
  448. agno/tools/sleep.py +8 -6
  449. agno/tools/spider.py +35 -11
  450. agno/tools/spotify.py +919 -0
  451. agno/tools/sql.py +34 -19
  452. agno/tools/tavily.py +158 -8
  453. agno/tools/telegram.py +18 -8
  454. agno/tools/todoist.py +218 -0
  455. agno/tools/toolkit.py +134 -9
  456. agno/tools/trafilatura.py +388 -0
  457. agno/tools/trello.py +25 -28
  458. agno/tools/twilio.py +18 -9
  459. agno/tools/user_control_flow.py +78 -0
  460. agno/tools/valyu.py +228 -0
  461. agno/tools/visualization.py +467 -0
  462. agno/tools/webbrowser.py +28 -0
  463. agno/tools/webex.py +76 -0
  464. agno/tools/website.py +23 -19
  465. agno/tools/webtools.py +45 -0
  466. agno/tools/whatsapp.py +286 -0
  467. agno/tools/wikipedia.py +28 -19
  468. agno/tools/workflow.py +285 -0
  469. agno/tools/{twitter.py → x.py} +142 -46
  470. agno/tools/yfinance.py +41 -39
  471. agno/tools/youtube.py +34 -17
  472. agno/tools/zendesk.py +15 -5
  473. agno/tools/zep.py +454 -0
  474. agno/tools/zoom.py +86 -37
  475. agno/tracing/__init__.py +12 -0
  476. agno/tracing/exporter.py +157 -0
  477. agno/tracing/schemas.py +276 -0
  478. agno/tracing/setup.py +111 -0
  479. agno/utils/agent.py +938 -0
  480. agno/utils/audio.py +37 -1
  481. agno/utils/certs.py +27 -0
  482. agno/utils/code_execution.py +11 -0
  483. agno/utils/common.py +103 -20
  484. agno/utils/cryptography.py +22 -0
  485. agno/utils/dttm.py +33 -0
  486. agno/utils/events.py +700 -0
  487. agno/utils/functions.py +107 -37
  488. agno/utils/gemini.py +426 -0
  489. agno/utils/hooks.py +171 -0
  490. agno/utils/http.py +185 -0
  491. agno/utils/json_schema.py +159 -37
  492. agno/utils/knowledge.py +36 -0
  493. agno/utils/location.py +19 -0
  494. agno/utils/log.py +221 -8
  495. agno/utils/mcp.py +214 -0
  496. agno/utils/media.py +335 -14
  497. agno/utils/merge_dict.py +22 -1
  498. agno/utils/message.py +77 -2
  499. agno/utils/models/ai_foundry.py +50 -0
  500. agno/utils/models/claude.py +373 -0
  501. agno/utils/models/cohere.py +94 -0
  502. agno/utils/models/llama.py +85 -0
  503. agno/utils/models/mistral.py +100 -0
  504. agno/utils/models/openai_responses.py +140 -0
  505. agno/utils/models/schema_utils.py +153 -0
  506. agno/utils/models/watsonx.py +41 -0
  507. agno/utils/openai.py +257 -0
  508. agno/utils/pickle.py +1 -1
  509. agno/utils/pprint.py +124 -8
  510. agno/utils/print_response/agent.py +930 -0
  511. agno/utils/print_response/team.py +1914 -0
  512. agno/utils/print_response/workflow.py +1668 -0
  513. agno/utils/prompts.py +111 -0
  514. agno/utils/reasoning.py +108 -0
  515. agno/utils/response.py +163 -0
  516. agno/utils/serialize.py +32 -0
  517. agno/utils/shell.py +4 -4
  518. agno/utils/streamlit.py +487 -0
  519. agno/utils/string.py +204 -51
  520. agno/utils/team.py +139 -0
  521. agno/utils/timer.py +9 -2
  522. agno/utils/tokens.py +657 -0
  523. agno/utils/tools.py +19 -1
  524. agno/utils/whatsapp.py +305 -0
  525. agno/utils/yaml_io.py +3 -3
  526. agno/vectordb/__init__.py +2 -0
  527. agno/vectordb/base.py +87 -9
  528. agno/vectordb/cassandra/__init__.py +5 -1
  529. agno/vectordb/cassandra/cassandra.py +383 -27
  530. agno/vectordb/chroma/__init__.py +4 -0
  531. agno/vectordb/chroma/chromadb.py +748 -83
  532. agno/vectordb/clickhouse/__init__.py +7 -1
  533. agno/vectordb/clickhouse/clickhousedb.py +554 -53
  534. agno/vectordb/couchbase/__init__.py +3 -0
  535. agno/vectordb/couchbase/couchbase.py +1446 -0
  536. agno/vectordb/lancedb/__init__.py +5 -0
  537. agno/vectordb/lancedb/lance_db.py +730 -98
  538. agno/vectordb/langchaindb/__init__.py +5 -0
  539. agno/vectordb/langchaindb/langchaindb.py +163 -0
  540. agno/vectordb/lightrag/__init__.py +5 -0
  541. agno/vectordb/lightrag/lightrag.py +388 -0
  542. agno/vectordb/llamaindex/__init__.py +3 -0
  543. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  544. agno/vectordb/milvus/__init__.py +3 -0
  545. agno/vectordb/milvus/milvus.py +966 -78
  546. agno/vectordb/mongodb/__init__.py +9 -1
  547. agno/vectordb/mongodb/mongodb.py +1175 -172
  548. agno/vectordb/pgvector/__init__.py +8 -0
  549. agno/vectordb/pgvector/pgvector.py +599 -115
  550. agno/vectordb/pineconedb/__init__.py +5 -1
  551. agno/vectordb/pineconedb/pineconedb.py +406 -43
  552. agno/vectordb/qdrant/__init__.py +4 -0
  553. agno/vectordb/qdrant/qdrant.py +914 -61
  554. agno/vectordb/redis/__init__.py +9 -0
  555. agno/vectordb/redis/redisdb.py +682 -0
  556. agno/vectordb/singlestore/__init__.py +8 -1
  557. agno/vectordb/singlestore/singlestore.py +771 -0
  558. agno/vectordb/surrealdb/__init__.py +3 -0
  559. agno/vectordb/surrealdb/surrealdb.py +663 -0
  560. agno/vectordb/upstashdb/__init__.py +5 -0
  561. agno/vectordb/upstashdb/upstashdb.py +718 -0
  562. agno/vectordb/weaviate/__init__.py +8 -0
  563. agno/vectordb/weaviate/index.py +15 -0
  564. agno/vectordb/weaviate/weaviate.py +1009 -0
  565. agno/workflow/__init__.py +23 -1
  566. agno/workflow/agent.py +299 -0
  567. agno/workflow/condition.py +759 -0
  568. agno/workflow/loop.py +756 -0
  569. agno/workflow/parallel.py +853 -0
  570. agno/workflow/router.py +723 -0
  571. agno/workflow/step.py +1564 -0
  572. agno/workflow/steps.py +613 -0
  573. agno/workflow/types.py +556 -0
  574. agno/workflow/workflow.py +4327 -514
  575. agno-2.3.13.dist-info/METADATA +639 -0
  576. agno-2.3.13.dist-info/RECORD +613 -0
  577. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
  578. agno-2.3.13.dist-info/licenses/LICENSE +201 -0
  579. agno/api/playground.py +0 -91
  580. agno/api/schemas/playground.py +0 -22
  581. agno/api/schemas/user.py +0 -22
  582. agno/api/schemas/workspace.py +0 -46
  583. agno/api/user.py +0 -160
  584. agno/api/workspace.py +0 -151
  585. agno/cli/auth_server.py +0 -118
  586. agno/cli/config.py +0 -275
  587. agno/cli/console.py +0 -88
  588. agno/cli/credentials.py +0 -23
  589. agno/cli/entrypoint.py +0 -571
  590. agno/cli/operator.py +0 -355
  591. agno/cli/settings.py +0 -85
  592. agno/cli/ws/ws_cli.py +0 -817
  593. agno/constants.py +0 -13
  594. agno/document/__init__.py +0 -1
  595. agno/document/chunking/semantic.py +0 -47
  596. agno/document/chunking/strategy.py +0 -31
  597. agno/document/reader/__init__.py +0 -1
  598. agno/document/reader/arxiv_reader.py +0 -41
  599. agno/document/reader/base.py +0 -22
  600. agno/document/reader/csv_reader.py +0 -84
  601. agno/document/reader/docx_reader.py +0 -46
  602. agno/document/reader/firecrawl_reader.py +0 -99
  603. agno/document/reader/json_reader.py +0 -43
  604. agno/document/reader/pdf_reader.py +0 -219
  605. agno/document/reader/s3/pdf_reader.py +0 -46
  606. agno/document/reader/s3/text_reader.py +0 -51
  607. agno/document/reader/text_reader.py +0 -41
  608. agno/document/reader/website_reader.py +0 -175
  609. agno/document/reader/youtube_reader.py +0 -50
  610. agno/embedder/__init__.py +0 -1
  611. agno/embedder/azure_openai.py +0 -86
  612. agno/embedder/cohere.py +0 -72
  613. agno/embedder/fastembed.py +0 -37
  614. agno/embedder/google.py +0 -73
  615. agno/embedder/huggingface.py +0 -54
  616. agno/embedder/mistral.py +0 -80
  617. agno/embedder/ollama.py +0 -57
  618. agno/embedder/openai.py +0 -74
  619. agno/embedder/sentence_transformer.py +0 -38
  620. agno/embedder/voyageai.py +0 -64
  621. agno/eval/perf.py +0 -201
  622. agno/file/__init__.py +0 -1
  623. agno/file/file.py +0 -16
  624. agno/file/local/csv.py +0 -32
  625. agno/file/local/txt.py +0 -19
  626. agno/infra/app.py +0 -240
  627. agno/infra/base.py +0 -144
  628. agno/infra/context.py +0 -20
  629. agno/infra/db_app.py +0 -52
  630. agno/infra/resource.py +0 -205
  631. agno/infra/resources.py +0 -55
  632. agno/knowledge/agent.py +0 -230
  633. agno/knowledge/arxiv.py +0 -22
  634. agno/knowledge/combined.py +0 -22
  635. agno/knowledge/csv.py +0 -28
  636. agno/knowledge/csv_url.py +0 -19
  637. agno/knowledge/document.py +0 -20
  638. agno/knowledge/docx.py +0 -30
  639. agno/knowledge/json.py +0 -28
  640. agno/knowledge/langchain.py +0 -71
  641. agno/knowledge/llamaindex.py +0 -66
  642. agno/knowledge/pdf.py +0 -28
  643. agno/knowledge/pdf_url.py +0 -26
  644. agno/knowledge/s3/base.py +0 -60
  645. agno/knowledge/s3/pdf.py +0 -21
  646. agno/knowledge/s3/text.py +0 -23
  647. agno/knowledge/text.py +0 -30
  648. agno/knowledge/website.py +0 -88
  649. agno/knowledge/wikipedia.py +0 -31
  650. agno/knowledge/youtube.py +0 -22
  651. agno/memory/agent.py +0 -392
  652. agno/memory/classifier.py +0 -104
  653. agno/memory/db/__init__.py +0 -1
  654. agno/memory/db/base.py +0 -42
  655. agno/memory/db/mongodb.py +0 -189
  656. agno/memory/db/postgres.py +0 -203
  657. agno/memory/db/sqlite.py +0 -193
  658. agno/memory/memory.py +0 -15
  659. agno/memory/row.py +0 -36
  660. agno/memory/summarizer.py +0 -192
  661. agno/memory/summary.py +0 -19
  662. agno/memory/workflow.py +0 -38
  663. agno/models/google/gemini_openai.py +0 -26
  664. agno/models/ollama/hermes.py +0 -221
  665. agno/models/ollama/tools.py +0 -362
  666. agno/models/vertexai/gemini.py +0 -595
  667. agno/playground/__init__.py +0 -3
  668. agno/playground/async_router.py +0 -421
  669. agno/playground/deploy.py +0 -249
  670. agno/playground/operator.py +0 -92
  671. agno/playground/playground.py +0 -91
  672. agno/playground/schemas.py +0 -76
  673. agno/playground/serve.py +0 -55
  674. agno/playground/sync_router.py +0 -405
  675. agno/reasoning/agent.py +0 -68
  676. agno/run/response.py +0 -112
  677. agno/storage/agent/__init__.py +0 -0
  678. agno/storage/agent/base.py +0 -38
  679. agno/storage/agent/dynamodb.py +0 -350
  680. agno/storage/agent/json.py +0 -92
  681. agno/storage/agent/mongodb.py +0 -228
  682. agno/storage/agent/postgres.py +0 -367
  683. agno/storage/agent/session.py +0 -79
  684. agno/storage/agent/singlestore.py +0 -303
  685. agno/storage/agent/sqlite.py +0 -357
  686. agno/storage/agent/yaml.py +0 -93
  687. agno/storage/workflow/__init__.py +0 -0
  688. agno/storage/workflow/base.py +0 -40
  689. agno/storage/workflow/mongodb.py +0 -233
  690. agno/storage/workflow/postgres.py +0 -366
  691. agno/storage/workflow/session.py +0 -60
  692. agno/storage/workflow/sqlite.py +0 -359
  693. agno/tools/googlesearch.py +0 -88
  694. agno/utils/defaults.py +0 -57
  695. agno/utils/filesystem.py +0 -39
  696. agno/utils/git.py +0 -52
  697. agno/utils/json_io.py +0 -30
  698. agno/utils/load_env.py +0 -19
  699. agno/utils/py_io.py +0 -19
  700. agno/utils/pyproject.py +0 -18
  701. agno/utils/resource_filter.py +0 -31
  702. agno/vectordb/singlestore/s2vectordb.py +0 -390
  703. agno/vectordb/singlestore/s2vectordb2.py +0 -355
  704. agno/workspace/__init__.py +0 -0
  705. agno/workspace/config.py +0 -325
  706. agno/workspace/enums.py +0 -6
  707. agno/workspace/helpers.py +0 -48
  708. agno/workspace/operator.py +0 -758
  709. agno/workspace/settings.py +0 -63
  710. agno-0.1.2.dist-info/LICENSE +0 -375
  711. agno-0.1.2.dist-info/METADATA +0 -502
  712. agno-0.1.2.dist-info/RECORD +0 -352
  713. agno-0.1.2.dist-info/entry_points.txt +0 -3
  714. /agno/{cli → db/migrations}/__init__.py +0 -0
  715. /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
  716. /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
  717. /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
  718. /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
  719. /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
  720. /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
  721. /agno/{reranker → utils/models}/__init__.py +0 -0
  722. /agno/{storage → utils/print_response}/__init__.py +0 -0
  723. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,797 @@
1
+ """JWT Middleware for AgentOS - JWT Authentication with optional RBAC."""
2
+
3
+ import fnmatch
4
+ import json
5
+ import re
6
+ from enum import Enum
7
+ from os import getenv
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import jwt
11
+ from fastapi import Request, Response
12
+ from fastapi.responses import JSONResponse
13
+ from jwt import PyJWK
14
+ from starlette.middleware.base import BaseHTTPMiddleware
15
+
16
+ from agno.os.scopes import (
17
+ AgentOSScope,
18
+ get_accessible_resource_ids,
19
+ get_default_scope_mappings,
20
+ has_required_scopes,
21
+ )
22
+ from agno.utils.log import log_debug, log_warning
23
+
24
+
25
+ class TokenSource(str, Enum):
26
+ """Enum for JWT token source options."""
27
+
28
+ HEADER = "header"
29
+ COOKIE = "cookie"
30
+ BOTH = "both" # Try header first, then cookie
31
+
32
+
33
+ class JWTValidator:
34
+ """
35
+ JWT token validator that can be used standalone or within JWTMiddleware.
36
+
37
+ This class handles:
38
+ - Loading verification keys (static keys or JWKS files)
39
+ - Validating JWT signatures
40
+ - Extracting claims from tokens
41
+
42
+ It can be stored on app.state for use by WebSocket handlers or other
43
+ components that need JWT validation outside of the HTTP middleware chain.
44
+
45
+ Example:
46
+ # Create validator
47
+ validator = JWTValidator(
48
+ verification_keys=["your-public-key"],
49
+ algorithm="RS256",
50
+ )
51
+
52
+ # Validate a token
53
+ try:
54
+ payload = validator.validate(token)
55
+ user_id = payload.get("sub")
56
+ scopes = payload.get("scopes", [])
57
+ except jwt.InvalidTokenError as e:
58
+ print(f"Invalid token: {e}")
59
+
60
+ # Store on app.state for WebSocket access
61
+ app.state.jwt_validator = validator
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ verification_keys: Optional[List[str]] = None,
67
+ jwks_file: Optional[str] = None,
68
+ algorithm: str = "RS256",
69
+ validate: bool = True,
70
+ scopes_claim: str = "scopes",
71
+ user_id_claim: str = "sub",
72
+ session_id_claim: str = "session_id",
73
+ audience_claim: str = "aud",
74
+ leeway: int = 10,
75
+ ):
76
+ """
77
+ Initialize the JWT validator.
78
+
79
+ Args:
80
+ verification_keys: List of keys for verifying JWT signatures.
81
+ For asymmetric algorithms (RS256, ES256), these should be public keys.
82
+ For symmetric algorithms (HS256), these are shared secrets.
83
+ jwks_file: Path to a static JWKS (JSON Web Key Set) file containing public keys.
84
+ algorithm: JWT algorithm (default: RS256).
85
+ validate: Whether to validate the JWT token (default: True).
86
+ scopes_claim: JWT claim name for scopes (default: "scopes").
87
+ user_id_claim: JWT claim name for user ID (default: "sub").
88
+ session_id_claim: JWT claim name for session ID (default: "session_id").
89
+ audience_claim: JWT claim name for audience (default: "aud").
90
+ leeway: Seconds of leeway for clock skew tolerance (default: 10).
91
+ """
92
+ self.algorithm = algorithm
93
+ self.validate = validate
94
+ self.scopes_claim = scopes_claim
95
+ self.user_id_claim = user_id_claim
96
+ self.session_id_claim = session_id_claim
97
+ self.audience_claim = audience_claim
98
+ self.leeway = leeway
99
+
100
+ # Build list of verification keys
101
+ self.verification_keys: List[str] = []
102
+ if verification_keys:
103
+ self.verification_keys.extend(verification_keys)
104
+
105
+ # Add key from environment variable if not already provided
106
+ env_key = getenv("JWT_VERIFICATION_KEY", "")
107
+ if env_key and env_key not in self.verification_keys:
108
+ self.verification_keys.append(env_key)
109
+
110
+ # JWKS configuration - load keys from JWKS file or environment variable
111
+ self.jwks_keys: Dict[str, PyJWK] = {} # kid -> PyJWK mapping
112
+
113
+ # Try jwks_file parameter first
114
+ if jwks_file:
115
+ self._load_jwks_file(jwks_file)
116
+ else:
117
+ # Try JWT_JWKS_FILE env var (path to file)
118
+ jwks_file_env = getenv("JWT_JWKS_FILE", "")
119
+ if jwks_file_env:
120
+ self._load_jwks_file(jwks_file_env)
121
+
122
+ # Validate that at least one key source is provided if validate=True
123
+ if self.validate and not self.verification_keys and not self.jwks_keys:
124
+ raise ValueError(
125
+ "At least one JWT verification key or JWKS file is required when validate=True. "
126
+ "Set via verification_keys parameter, JWT_VERIFICATION_KEY environment variable, "
127
+ "jwks_file parameter or JWT_JWKS_FILE environment variable."
128
+ )
129
+
130
+ def _load_jwks_file(self, file_path: str) -> None:
131
+ """
132
+ Load keys from a static JWKS file.
133
+
134
+ Args:
135
+ file_path: Path to the JWKS JSON file
136
+ """
137
+ try:
138
+ with open(file_path) as f:
139
+ jwks_data = json.load(f)
140
+ self._parse_jwks_data(jwks_data)
141
+ log_debug(f"Loaded {len(self.jwks_keys)} key(s) from JWKS file: {file_path}")
142
+ except FileNotFoundError:
143
+ raise ValueError(f"JWKS file not found: {file_path}")
144
+ except json.JSONDecodeError as e:
145
+ raise ValueError(f"Invalid JSON in JWKS file {file_path}: {e}")
146
+
147
+ def _parse_jwks_data(self, jwks_data: Dict[str, Any]) -> None:
148
+ """
149
+ Parse JWKS data and populate self.jwks_keys.
150
+
151
+ Args:
152
+ jwks_data: Parsed JWKS dictionary with "keys" array
153
+ """
154
+ keys = jwks_data.get("keys", [])
155
+ if not keys:
156
+ log_warning("JWKS contains no keys")
157
+ return
158
+
159
+ for key_data in keys:
160
+ try:
161
+ kid = key_data.get("kid")
162
+ jwk = PyJWK.from_dict(key_data)
163
+ if kid:
164
+ self.jwks_keys[kid] = jwk
165
+ else:
166
+ # If no kid, use a default key (for single-key JWKS)
167
+ self.jwks_keys["_default"] = jwk
168
+ except Exception as e:
169
+ log_warning(f"Failed to parse JWKS key: {e}")
170
+
171
+ def validate_token(self, token: str, expected_audience: Optional[str] = None) -> Dict[str, Any]:
172
+ """
173
+ Validate JWT token and extract claims.
174
+
175
+ Args:
176
+ token: The JWT token to validate
177
+ expected_audience: The expected audience to verify (optional)
178
+
179
+ Returns:
180
+ Dictionary of claims if valid
181
+
182
+ Raises:
183
+ jwt.InvalidAudienceError: If audience claim doesn't match expected
184
+ jwt.ExpiredSignatureError: If token has expired
185
+ jwt.InvalidTokenError: If token is invalid
186
+ """
187
+ decode_options: Dict[str, Any] = {}
188
+ decode_kwargs: Dict[str, Any] = {
189
+ "algorithms": [self.algorithm],
190
+ "leeway": self.leeway,
191
+ }
192
+
193
+ # Configure audience verification
194
+ if expected_audience:
195
+ decode_kwargs["audience"] = expected_audience
196
+ else:
197
+ decode_options["verify_aud"] = False
198
+
199
+ # If validation is disabled, decode without signature verification
200
+ if not self.validate:
201
+ decode_options["verify_signature"] = False
202
+ decode_kwargs["options"] = decode_options
203
+ return jwt.decode(token, **decode_kwargs)
204
+
205
+ if decode_options:
206
+ decode_kwargs["options"] = decode_options
207
+
208
+ last_exception: Optional[Exception] = None
209
+
210
+ # Try JWKS keys first if configured
211
+ if self.jwks_keys:
212
+ try:
213
+ # Get the kid from the token header to find the right key
214
+ unverified_header = jwt.get_unverified_header(token)
215
+ kid = unverified_header.get("kid")
216
+
217
+ jwk = None
218
+ if kid and kid in self.jwks_keys:
219
+ jwk = self.jwks_keys[kid]
220
+ elif "_default" in self.jwks_keys:
221
+ # Fall back to default key if no kid match
222
+ jwk = self.jwks_keys["_default"]
223
+
224
+ if jwk:
225
+ return jwt.decode(token, jwk.key, **decode_kwargs)
226
+ except jwt.InvalidAudienceError:
227
+ raise
228
+ except jwt.ExpiredSignatureError:
229
+ raise
230
+ except jwt.InvalidTokenError as e:
231
+ if not self.verification_keys:
232
+ raise
233
+ last_exception = e
234
+
235
+ # Try each static verification key until one succeeds
236
+ for key in self.verification_keys:
237
+ try:
238
+ return jwt.decode(token, key, **decode_kwargs)
239
+ except jwt.InvalidAudienceError:
240
+ raise
241
+ except jwt.ExpiredSignatureError:
242
+ raise
243
+ except jwt.InvalidTokenError as e:
244
+ last_exception = e
245
+ continue
246
+
247
+ if last_exception:
248
+ raise last_exception
249
+ raise jwt.InvalidTokenError("No verification keys configured")
250
+
251
+ def extract_claims(self, payload: Dict[str, Any]) -> Dict[str, Any]:
252
+ """
253
+ Extract standard claims from a JWT payload.
254
+
255
+ Args:
256
+ payload: The decoded JWT payload
257
+
258
+ Returns:
259
+ Dictionary with user_id, session_id, scopes, and audience
260
+ """
261
+ scopes = payload.get(self.scopes_claim, [])
262
+ if isinstance(scopes, str):
263
+ scopes = [scopes]
264
+ elif not isinstance(scopes, list):
265
+ scopes = []
266
+
267
+ return {
268
+ "user_id": payload.get(self.user_id_claim),
269
+ "session_id": payload.get(self.session_id_claim),
270
+ "scopes": scopes,
271
+ "audience": payload.get(self.audience_claim),
272
+ }
273
+
274
+
275
+ class JWTMiddleware(BaseHTTPMiddleware):
276
+ """
277
+ JWT Authentication Middleware with optional RBAC (Role-Based Access Control).
278
+
279
+ This middleware:
280
+ 1. Extracts JWT token from Authorization header or cookies
281
+ 2. Decodes and validates the token
282
+ 3. Validates the `aud` (audience) claim matches the AgentOS ID (if configured)
283
+ 4. Stores JWT claims (user_id, session_id, scopes) in request.state
284
+ 5. Optionally checks if the request path requires specific scopes (if scope_mappings provided)
285
+ 6. Validates that the authenticated user has the required scopes
286
+ 7. Returns 401 for invalid tokens, 403 for insufficient scopes
287
+
288
+ RBAC is opt-in: Only enabled when authorization=True or scope_mappings are provided.
289
+ Without authorization enabled, the middleware only extracts and validates JWT tokens.
290
+
291
+ Audience Verification:
292
+ - The `aud` claim in JWT tokens should contain the AgentOS ID
293
+ - This is verified against the AgentOS instance ID from app.state.agent_os_id
294
+ - Tokens with mismatched audience will be rejected with 401
295
+
296
+ Scope Format (simplified):
297
+ - Global resource scopes: `resource:action` (e.g., "agents:read")
298
+ - Per-resource scopes: `resource:<resource-id>:action` (e.g., "agents:web-agent:run")
299
+ - Wildcards: `resource:*:action` (e.g., "agents:*:run")
300
+ - Admin scope: `admin` (grants all permissions)
301
+
302
+ Token Sources:
303
+ - "header": Extract from Authorization header (default)
304
+ - "cookie": Extract from HTTP cookie
305
+ - "both": Try header first, then cookie as fallback
306
+
307
+ Example:
308
+ from agno.os.middleware import JWTMiddleware
309
+ from agno.os.scopes import AgentOSScope
310
+
311
+ # Single verification key
312
+ app.add_middleware(
313
+ JWTMiddleware,
314
+ verification_keys=["your-public-key"],
315
+ authorization=True,
316
+ verify_audience=True, # Verify aud claim matches AgentOS ID
317
+ scope_mappings={
318
+ # Override default scope for this endpoint
319
+ "GET /agents": ["agents:read"],
320
+ # Add new endpoint mapping
321
+ "POST /custom/endpoint": ["agents:run"],
322
+ # Allow access without scopes
323
+ "GET /public/stats": [],
324
+ }
325
+ )
326
+
327
+ # Multiple verification keys (accept tokens from multiple issuers)
328
+ app.add_middleware(
329
+ JWTMiddleware,
330
+ verification_keys=[
331
+ "public-key-from-issuer-1",
332
+ "public-key-from-issuer-2",
333
+ ],
334
+ authorization=True,
335
+ )
336
+
337
+ # Using a static JWKS file
338
+ app.add_middleware(
339
+ JWTMiddleware,
340
+ jwks_file="/path/to/jwks.json",
341
+ authorization=True,
342
+ )
343
+
344
+ # No validation (extract claims only, useful for development)
345
+ app.add_middleware(
346
+ JWTMiddleware,
347
+ validate=False, # No verification key needed
348
+ )
349
+ """
350
+
351
+ def __init__(
352
+ self,
353
+ app,
354
+ verification_keys: Optional[List[str]] = None,
355
+ jwks_file: Optional[str] = None,
356
+ secret_key: Optional[str] = None, # Deprecated: Use verification_keys instead
357
+ algorithm: str = "RS256",
358
+ validate: bool = True,
359
+ authorization: Optional[bool] = None,
360
+ token_source: TokenSource = TokenSource.HEADER,
361
+ token_header_key: str = "Authorization",
362
+ cookie_name: str = "access_token",
363
+ scopes_claim: str = "scopes",
364
+ user_id_claim: str = "sub",
365
+ session_id_claim: str = "session_id",
366
+ audience_claim: str = "aud",
367
+ verify_audience: bool = False,
368
+ dependencies_claims: Optional[List[str]] = None,
369
+ session_state_claims: Optional[List[str]] = None,
370
+ scope_mappings: Optional[Dict[str, List[str]]] = None,
371
+ excluded_route_paths: Optional[List[str]] = None,
372
+ admin_scope: Optional[str] = None,
373
+ ):
374
+ """
375
+ Initialize the JWT middleware.
376
+
377
+ Args:
378
+ app: The FastAPI app instance
379
+ verification_keys: List of keys for verifying JWT signatures.
380
+ For asymmetric algorithms (RS256, ES256), these should be public keys.
381
+ For symmetric algorithms (HS256), these are shared secrets.
382
+ Each key will be tried in order until one successfully validates the token.
383
+ Useful when accepting tokens signed by different private keys.
384
+ If not provided, will use JWT_VERIFICATION_KEY env var (as a single-item list).
385
+ jwks_file: Path to a static JWKS (JSON Web Key Set) file containing public keys.
386
+ The file should contain a JSON object with a "keys" array.
387
+ Keys are looked up by the "kid" (key ID) claim in the JWT header.
388
+ If not provided, will check JWT_JWKS_FILE env var for a file path,
389
+ or JWT_JWKS env var for inline JWKS JSON content.
390
+ secret_key: (deprecated) Use verification_keys instead. If provided, will be added to verification_keys.
391
+ algorithm: JWT algorithm (default: RS256). Common options: RS256 (asymmetric), HS256 (symmetric).
392
+ validate: Whether to validate the JWT token (default: True). If False, tokens are decoded
393
+ without signature verification and no verification key is required.
394
+ authorization: Whether to add authorization checks to the request (i.e. validation of scopes)
395
+ token_source: Where to extract JWT token from (header, cookie, or both)
396
+ token_header_key: Header key for Authorization (default: "Authorization")
397
+ cookie_name: Cookie name for JWT token (default: "access_token")
398
+ scopes_claim: JWT claim name for scopes (default: "scopes")
399
+ user_id_claim: JWT claim name for user ID (default: "sub")
400
+ session_id_claim: JWT claim name for session ID (default: "session_id")
401
+ audience_claim: JWT claim name for audience/OS ID (default: "aud")
402
+ verify_audience: Whether to verify the audience claim matches AgentOS ID (default: False)
403
+ dependencies_claims: A list of claims to extract from the JWT token for dependencies
404
+ session_state_claims: A list of claims to extract from the JWT token for session state
405
+ scope_mappings: Optional dictionary mapping route patterns to required scopes.
406
+ If None, RBAC is disabled and only JWT extraction/validation happens.
407
+ If provided, mappings are ADDITIVE to default scope mappings (overrides on conflict).
408
+ Use empty list [] to explicitly allow access without scopes for a route.
409
+ Format: {"POST /agents/*/runs": ["agents:run"], "GET /public": []}
410
+ excluded_route_paths: List of route paths to exclude from JWT/RBAC checks
411
+ admin_scope: The scope that grants admin access (default: "agent_os:admin")
412
+
413
+ Note:
414
+ - At least one verification key or JWKS file must be provided if validate=True
415
+ - If validate=False, no verification key is needed (claims are extracted without verification)
416
+ - JWKS keys are tried first (by kid), then static verification_keys as fallback
417
+ - CORS allowed origins are read from app.state.cors_allowed_origins (set by AgentOS).
418
+ This allows error responses to include proper CORS headers.
419
+ """
420
+ super().__init__(app)
421
+
422
+ # Handle deprecated secret_key parameter
423
+ all_verification_keys = list(verification_keys) if verification_keys else []
424
+ if secret_key:
425
+ log_warning("secret_key is deprecated. Use verification_keys instead.")
426
+ if secret_key not in all_verification_keys:
427
+ all_verification_keys.append(secret_key)
428
+
429
+ # Create the JWT validator (handles key loading and token validation)
430
+ self.validator = JWTValidator(
431
+ verification_keys=all_verification_keys if all_verification_keys else None,
432
+ jwks_file=jwks_file,
433
+ algorithm=algorithm,
434
+ validate=validate,
435
+ scopes_claim=scopes_claim,
436
+ user_id_claim=user_id_claim,
437
+ session_id_claim=session_id_claim,
438
+ audience_claim=audience_claim,
439
+ )
440
+
441
+ # Store config for easy access
442
+ self.validate = validate
443
+ self.algorithm = algorithm
444
+ self.token_source = token_source
445
+ self.token_header_key = token_header_key
446
+ self.cookie_name = cookie_name
447
+ self.scopes_claim = scopes_claim
448
+ self.user_id_claim = user_id_claim
449
+ self.session_id_claim = session_id_claim
450
+ self.audience_claim = audience_claim
451
+ self.verify_audience = verify_audience
452
+ self.dependencies_claims: List[str] = dependencies_claims or []
453
+ self.session_state_claims: List[str] = session_state_claims or []
454
+
455
+ # RBAC configuration (opt-in via scope_mappings)
456
+ self.authorization = authorization
457
+
458
+ # If scope_mappings are provided, enable authorization
459
+ if scope_mappings is not None and self.authorization is None:
460
+ self.authorization = True
461
+
462
+ # Build final scope mappings (additive approach)
463
+ if self.authorization:
464
+ # Start with default scope mappings
465
+ self.scope_mappings = get_default_scope_mappings()
466
+
467
+ # Merge user-provided scope mappings (overrides defaults)
468
+ if scope_mappings is not None:
469
+ self.scope_mappings.update(scope_mappings)
470
+ else:
471
+ self.scope_mappings = scope_mappings or {}
472
+
473
+ self.excluded_route_paths = (
474
+ excluded_route_paths if excluded_route_paths is not None else self._get_default_excluded_routes()
475
+ )
476
+ self.admin_scope = admin_scope or AgentOSScope.ADMIN.value
477
+
478
+ def _get_default_excluded_routes(self) -> List[str]:
479
+ """Get default routes that should be excluded from RBAC checks."""
480
+ return [
481
+ "/",
482
+ "/health",
483
+ "/docs",
484
+ "/redoc",
485
+ "/openapi.json",
486
+ "/docs/oauth2-redirect",
487
+ ]
488
+
489
+ def _extract_resource_id_from_path(self, path: str, resource_type: str) -> Optional[str]:
490
+ """
491
+ Extract resource ID from a path.
492
+
493
+ Args:
494
+ path: The request path
495
+ resource_type: Type of resource ("agents", "teams", "workflows")
496
+
497
+ Returns:
498
+ The resource ID if found, None otherwise
499
+
500
+ Examples:
501
+ >>> _extract_resource_id_from_path("/agents/my-agent/runs", "agents")
502
+ "my-agent"
503
+ """
504
+ # Pattern: /{resource_type}/{resource_id}/...
505
+ pattern = f"^/{resource_type}/([^/]+)"
506
+ match = re.search(pattern, path)
507
+ if match:
508
+ return match.group(1)
509
+ return None
510
+
511
+ def _is_route_excluded(self, path: str) -> bool:
512
+ """Check if a route path matches any of the excluded patterns."""
513
+ if not self.excluded_route_paths:
514
+ return False
515
+
516
+ for excluded_path in self.excluded_route_paths:
517
+ # Support both exact matches and wildcard patterns
518
+ if fnmatch.fnmatch(path, excluded_path):
519
+ return True
520
+ # Also check without trailing slash
521
+ if fnmatch.fnmatch(path.rstrip("/"), excluded_path):
522
+ return True
523
+
524
+ return False
525
+
526
+ def _get_required_scopes(self, method: str, path: str) -> List[str]:
527
+ """
528
+ Get required scopes for a given method and path.
529
+
530
+ Args:
531
+ method: HTTP method (GET, POST, etc.)
532
+ path: Request path
533
+
534
+ Returns:
535
+ List of required scopes. Empty list [] means no scopes required (allow access).
536
+ Routes not in scope_mappings also return [], allowing access.
537
+ """
538
+ route_key = f"{method} {path}"
539
+
540
+ # First, try exact match
541
+ if route_key in self.scope_mappings:
542
+ return self.scope_mappings[route_key]
543
+
544
+ # Then try pattern matching
545
+ for pattern, scopes in self.scope_mappings.items():
546
+ pattern_method, pattern_path = pattern.split(" ", 1)
547
+
548
+ # Check if method matches
549
+ if pattern_method != method:
550
+ continue
551
+
552
+ # Convert pattern to fnmatch pattern (replace {param} with *)
553
+ # This handles both /agents/* and /agents/{agent_id} style patterns
554
+ normalized_pattern = pattern_path
555
+ if "{" in normalized_pattern:
556
+ # Replace {param} with * for pattern matching
557
+ normalized_pattern = re.sub(r"\{[^}]+\}", "*", normalized_pattern)
558
+
559
+ if fnmatch.fnmatch(path, normalized_pattern):
560
+ return scopes
561
+
562
+ return []
563
+
564
+ def _extract_token_from_header(self, request: Request) -> Optional[str]:
565
+ """Extract JWT token from Authorization header."""
566
+ authorization = request.headers.get(self.token_header_key, "")
567
+ if not authorization:
568
+ return None
569
+
570
+ # Support both "Bearer <token>" and just "<token>"
571
+ if authorization.lower().startswith("bearer "):
572
+ return authorization[7:].strip()
573
+ return authorization.strip()
574
+
575
+ def _extract_token_from_cookie(self, request: Request) -> Optional[str]:
576
+ """Extract JWT token from cookie."""
577
+ cookie_value = request.cookies.get(self.cookie_name)
578
+ if cookie_value:
579
+ return cookie_value.strip()
580
+ return None
581
+
582
+ def _get_missing_token_error_message(self) -> str:
583
+ """Get appropriate error message for missing token based on token source."""
584
+ if self.token_source == TokenSource.HEADER:
585
+ return "Authorization header missing"
586
+ elif self.token_source == TokenSource.COOKIE:
587
+ return f"JWT cookie '{self.cookie_name}' missing"
588
+ elif self.token_source == TokenSource.BOTH:
589
+ return f"JWT token missing from both Authorization header and '{self.cookie_name}' cookie"
590
+ else:
591
+ return "JWT token missing"
592
+
593
+ def _create_error_response(
594
+ self,
595
+ status_code: int,
596
+ detail: str,
597
+ origin: Optional[str] = None,
598
+ cors_allowed_origins: Optional[List[str]] = None,
599
+ ) -> JSONResponse:
600
+ """Create an error response with CORS headers."""
601
+ response = JSONResponse(status_code=status_code, content={"detail": detail})
602
+
603
+ # Add CORS headers to the error response
604
+ if origin and self._is_origin_allowed(origin, cors_allowed_origins):
605
+ response.headers["Access-Control-Allow-Origin"] = origin
606
+ response.headers["Access-Control-Allow-Credentials"] = "true"
607
+ response.headers["Access-Control-Allow-Methods"] = "*"
608
+ response.headers["Access-Control-Allow-Headers"] = "*"
609
+ response.headers["Access-Control-Expose-Headers"] = "*"
610
+
611
+ return response
612
+
613
+ def _is_origin_allowed(self, origin: str, cors_allowed_origins: Optional[List[str]] = None) -> bool:
614
+ """Check if the origin is in the allowed origins list."""
615
+ if not cors_allowed_origins:
616
+ # If no allowed origins configured, allow all (fallback to default behavior)
617
+ return True
618
+
619
+ # Check if origin is in the allowed list
620
+ return origin in cors_allowed_origins
621
+
622
+ async def dispatch(self, request: Request, call_next) -> Response:
623
+ """Process the request: extract JWT, validate, and check RBAC scopes."""
624
+ path = request.url.path
625
+ method = request.method
626
+
627
+ # Skip OPTIONS requests (CORS preflight)
628
+ if method == "OPTIONS":
629
+ return await call_next(request)
630
+
631
+ # Skip excluded routes
632
+ if self._is_route_excluded(path):
633
+ return await call_next(request)
634
+
635
+ # Get origin and CORS allowed origins for error responses
636
+ origin = request.headers.get("origin")
637
+ cors_allowed_origins = getattr(request.app.state, "cors_allowed_origins", None)
638
+
639
+ # Get agent_os_id from app state for audience verification
640
+ agent_os_id = getattr(request.app.state, "agent_os_id", None)
641
+
642
+ # Extract JWT token
643
+ token = self._extract_token(request)
644
+ if not token:
645
+ if self.validate:
646
+ error_msg = self._get_missing_token_error_message()
647
+ return self._create_error_response(401, error_msg, origin, cors_allowed_origins)
648
+
649
+ try:
650
+ # Validate token and extract claims (with audience verification if configured)
651
+ expected_audience = agent_os_id if self.verify_audience else None
652
+ payload: Dict[str, Any] = self.validator.validate_token(token, expected_audience) # type: ignore
653
+
654
+ # Extract standard claims and store in request.state
655
+ user_id = payload.get(self.user_id_claim)
656
+ session_id = payload.get(self.session_id_claim)
657
+ scopes = payload.get(self.scopes_claim, [])
658
+ audience = payload.get(self.audience_claim)
659
+
660
+ # Ensure scopes is a list
661
+ if isinstance(scopes, str):
662
+ scopes = [scopes]
663
+ elif not isinstance(scopes, list):
664
+ scopes = []
665
+
666
+ # Store claims in request.state
667
+ request.state.authenticated = True
668
+ request.state.user_id = user_id
669
+ request.state.session_id = session_id
670
+ request.state.scopes = scopes
671
+ request.state.audience = audience
672
+ request.state.authorization_enabled = self.authorization or False
673
+
674
+ # Extract dependencies claims
675
+ dependencies = {}
676
+ if self.dependencies_claims:
677
+ for claim in self.dependencies_claims:
678
+ if claim in payload:
679
+ dependencies[claim] = payload[claim]
680
+
681
+ if dependencies:
682
+ log_debug(f"Extracted dependencies: {dependencies}")
683
+ request.state.dependencies = dependencies
684
+
685
+ # Extract session state claims
686
+ session_state = {}
687
+ if self.session_state_claims:
688
+ for claim in self.session_state_claims:
689
+ if claim in payload:
690
+ session_state[claim] = payload[claim]
691
+
692
+ if session_state:
693
+ log_debug(f"Extracted session state: {session_state}")
694
+ request.state.session_state = session_state
695
+
696
+ # RBAC scope checking (only if enabled)
697
+ if self.authorization:
698
+ # Extract resource type and ID from path
699
+ resource_type = None
700
+ resource_id = None
701
+
702
+ if "/agents" in path:
703
+ resource_type = "agents"
704
+ elif "/teams" in path:
705
+ resource_type = "teams"
706
+ elif "/workflows" in path:
707
+ resource_type = "workflows"
708
+
709
+ if resource_type:
710
+ resource_id = self._extract_resource_id_from_path(path, resource_type)
711
+
712
+ required_scopes = self._get_required_scopes(method, path)
713
+
714
+ # Empty list [] means no scopes required (allow access)
715
+ if required_scopes:
716
+ # Use the scope validation system
717
+ has_access = has_required_scopes(
718
+ scopes,
719
+ required_scopes,
720
+ resource_type=resource_type,
721
+ resource_id=resource_id,
722
+ admin_scope=self.admin_scope,
723
+ )
724
+
725
+ # Special handling for listing endpoints (no resource_id)
726
+ if not has_access and not resource_id and resource_type:
727
+ # For listing endpoints, always allow access but store accessible IDs for filtering
728
+ # This allows endpoints to return filtered results (including empty list) instead of 403
729
+ accessible_ids = get_accessible_resource_ids(
730
+ scopes, resource_type, admin_scope=self.admin_scope
731
+ )
732
+ has_access = True # Always allow listing endpoints
733
+ request.state.accessible_resource_ids = accessible_ids
734
+
735
+ if accessible_ids:
736
+ log_debug(f"User has specific {resource_type} scopes. Accessible IDs: {accessible_ids}")
737
+ else:
738
+ log_debug(f"User has no {resource_type} scopes. Will return empty list.")
739
+
740
+ if not has_access:
741
+ log_warning(
742
+ f"Insufficient scopes for {method} {path}. Required: {required_scopes}, User has: {scopes}"
743
+ )
744
+ return self._create_error_response(
745
+ 403, "Insufficient permissions", origin, cors_allowed_origins
746
+ )
747
+
748
+ log_debug(f"Scope check passed for {method} {path}. User scopes: {scopes}")
749
+ else:
750
+ log_debug(f"No scopes required for {method} {path}")
751
+
752
+ log_debug(f"JWT decoded successfully for user: {user_id}")
753
+
754
+ request.state.token = token
755
+ request.state.authenticated = True
756
+
757
+ except jwt.InvalidAudienceError:
758
+ log_warning(f"Invalid audience - expected: {agent_os_id}")
759
+ return self._create_error_response(
760
+ 401, "Invalid audience - token not valid for this AgentOS instance", origin, cors_allowed_origins
761
+ )
762
+
763
+ except jwt.ExpiredSignatureError as e:
764
+ if self.validate:
765
+ log_warning(f"Token has expired: {str(e)}")
766
+ return self._create_error_response(401, "Token has expired", origin, cors_allowed_origins)
767
+ request.state.authenticated = False
768
+ request.state.token = token
769
+
770
+ except jwt.InvalidTokenError as e:
771
+ if self.validate:
772
+ log_warning(f"Invalid token: {str(e)}")
773
+ return self._create_error_response(401, f"Invalid token: {str(e)}", origin, cors_allowed_origins)
774
+ request.state.authenticated = False
775
+ request.state.token = token
776
+ except Exception as e:
777
+ if self.validate:
778
+ log_warning(f"Error decoding token: {str(e)}")
779
+ return self._create_error_response(401, f"Error decoding token: {str(e)}", origin, cors_allowed_origins)
780
+ request.state.authenticated = False
781
+ request.state.token = token
782
+
783
+ return await call_next(request)
784
+
785
+ def _extract_token(self, request: Request) -> Optional[str]:
786
+ """Extract JWT token based on configured source."""
787
+ if self.token_source == TokenSource.HEADER:
788
+ return self._extract_token_from_header(request)
789
+ elif self.token_source == TokenSource.COOKIE:
790
+ return self._extract_token_from_cookie(request)
791
+ elif self.token_source == TokenSource.BOTH:
792
+ # Try header first, then cookie
793
+ token = self._extract_token_from_header(request)
794
+ if token:
795
+ return token
796
+ return self._extract_token_from_cookie(request)
797
+ return None