agno 2.2.13__py3-none-any.whl → 2.4.3__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 (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
agno/tools/redshift.py ADDED
@@ -0,0 +1,406 @@
1
+ import csv
2
+ from os import getenv
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ try:
6
+ import redshift_connector
7
+ from redshift_connector import Connection
8
+ except ImportError:
9
+ raise ImportError("`redshift_connector` not installed. Please install using `pip install redshift-connector`.")
10
+
11
+ from agno.tools import Toolkit
12
+ from agno.utils.log import log_debug, log_error, log_info
13
+
14
+
15
+ class RedshiftTools(Toolkit):
16
+ """
17
+ A toolkit for interacting with Amazon Redshift databases.
18
+
19
+ Supports these authentication methods:
20
+ - Standard username and password authentication
21
+ - IAM authentication with AWS profile
22
+ - IAM authentication with AWS credentials
23
+
24
+ Args:
25
+ host (Optional[str]): Redshift cluster endpoint hostname. Falls back to REDSHIFT_HOST env var.
26
+ port (int): Redshift cluster port number. Default is 5439.
27
+ database (Optional[str]): Database name to connect to. Falls back to REDSHIFT_DATABASE env var.
28
+ user (Optional[str]): Username for standard authentication.
29
+ password (Optional[str]): Password for standard authentication.
30
+ iam (bool): Enable IAM authentication. Default is False.
31
+ cluster_identifier (Optional[str]): Redshift cluster identifier for IAM auth with provisioned clusters. Falls back to REDSHIFT_CLUSTER_IDENTIFIER env var.
32
+ region (Optional[str]): AWS region for IAM credential retrieval. Falls back to AWS_REGION or AWS_DEFAULT_REGION env vars.
33
+ db_user (Optional[str]): Database user for IAM auth with provisioned clusters. Falls back to REDSHIFT_DB_USER env var.
34
+ access_key_id (Optional[str]): AWS access key ID for IAM auth. Falls back to AWS_ACCESS_KEY_ID env var.
35
+ secret_access_key (Optional[str]): AWS secret access key for IAM auth. Falls back to AWS_SECRET_ACCESS_KEY env var.
36
+ session_token (Optional[str]): AWS session token for temporary credentials. Falls back to AWS_SESSION_TOKEN env var.
37
+ profile (Optional[str]): AWS profile name for IAM auth. Falls back to AWS_PROFILE env var.
38
+ ssl (bool): Enable SSL connection. Default is True.
39
+ table_schema (str): Default schema for table operations. Default is "public".
40
+ """
41
+
42
+ _requires_connect: bool = True
43
+
44
+ def __init__(
45
+ self,
46
+ # Connection parameters
47
+ host: Optional[str] = None,
48
+ port: int = 5439,
49
+ database: Optional[str] = None,
50
+ # Standard authentication (username/password)
51
+ user: Optional[str] = None,
52
+ password: Optional[str] = None,
53
+ # IAM Authentication
54
+ iam: bool = False,
55
+ cluster_identifier: Optional[str] = None,
56
+ region: Optional[str] = None,
57
+ db_user: Optional[str] = None,
58
+ # AWS Credentials (for IAM auth)
59
+ access_key_id: Optional[str] = None,
60
+ secret_access_key: Optional[str] = None,
61
+ session_token: Optional[str] = None,
62
+ profile: Optional[str] = None,
63
+ # Connection settings
64
+ ssl: bool = True,
65
+ table_schema: str = "public",
66
+ **kwargs,
67
+ ):
68
+ # Connection parameters
69
+ self.host: Optional[str] = host or getenv("REDSHIFT_HOST")
70
+ self.port: int = port
71
+ self.database: Optional[str] = database or getenv("REDSHIFT_DATABASE")
72
+
73
+ # Standard authentication
74
+ self.user: Optional[str] = user
75
+ self.password: Optional[str] = password
76
+
77
+ # IAM authentication parameters
78
+ self.iam: bool = iam
79
+ self.cluster_identifier: Optional[str] = cluster_identifier or getenv("REDSHIFT_CLUSTER_IDENTIFIER")
80
+ self.region: Optional[str] = region or getenv("AWS_REGION") or getenv("AWS_DEFAULT_REGION")
81
+ self.db_user: Optional[str] = db_user or getenv("REDSHIFT_DB_USER")
82
+
83
+ # AWS credentials
84
+ self.access_key_id: Optional[str] = access_key_id or getenv("AWS_ACCESS_KEY_ID")
85
+ self.secret_access_key: Optional[str] = secret_access_key or getenv("AWS_SECRET_ACCESS_KEY")
86
+ self.session_token: Optional[str] = session_token or getenv("AWS_SESSION_TOKEN")
87
+ self.profile: Optional[str] = profile or getenv("AWS_PROFILE")
88
+
89
+ # Connection settings
90
+ self.ssl: bool = ssl
91
+ self.table_schema: str = table_schema
92
+
93
+ # Connection instance
94
+ self._connection: Optional[Connection] = None
95
+
96
+ tools: List[Any] = [
97
+ self.show_tables,
98
+ self.describe_table,
99
+ self.summarize_table,
100
+ self.inspect_query,
101
+ self.run_query,
102
+ self.export_table_to_path,
103
+ ]
104
+
105
+ super().__init__(name="redshift_tools", tools=tools, **kwargs)
106
+
107
+ def connect(self) -> Connection:
108
+ """
109
+ Establish a connection to the Redshift database.
110
+
111
+ Returns:
112
+ The database connection object.
113
+
114
+ Raises:
115
+ redshift_connector.Error: If connection fails.
116
+ """
117
+ if self._connection is not None:
118
+ log_debug("Connection already established, reusing existing connection")
119
+ return self._connection
120
+
121
+ log_info("Establishing connection to Redshift")
122
+ self._connection = redshift_connector.connect(**self._get_connection_kwargs())
123
+ return self._connection
124
+
125
+ def close(self) -> None:
126
+ """
127
+ Close the database connection if it exists.
128
+ """
129
+ if self._connection is not None:
130
+ log_info("Closing Redshift connection")
131
+ try:
132
+ self._connection.close()
133
+ except Exception:
134
+ pass # Connection might already be closed
135
+ self._connection = None
136
+
137
+ @property
138
+ def is_connected(self) -> bool:
139
+ """Check if a connection is currently established."""
140
+ return self._connection is not None
141
+
142
+ def _ensure_connection(self) -> Connection:
143
+ """
144
+ Ensure a connection exists, creating one if necessary.
145
+
146
+ Returns:
147
+ The database connection object.
148
+ """
149
+ if self._connection is None:
150
+ return self.connect()
151
+ return self._connection
152
+
153
+ def _get_connection_kwargs(self) -> Dict[str, Any]:
154
+ """Build connection kwargs from instance."""
155
+ connection_kwargs: Dict[str, Any] = {}
156
+
157
+ # Common connection parameters
158
+ if self.host:
159
+ connection_kwargs["host"] = self.host
160
+ if self.port:
161
+ connection_kwargs["port"] = self.port
162
+ if self.database:
163
+ connection_kwargs["database"] = self.database
164
+ connection_kwargs["ssl"] = self.ssl
165
+
166
+ # IAM Authentication
167
+ if self.iam:
168
+ connection_kwargs["iam"] = True
169
+
170
+ # For provisioned clusters (not serverless)
171
+ if self.cluster_identifier:
172
+ connection_kwargs["cluster_identifier"] = self.cluster_identifier
173
+ # db_user required for provisioned clusters with IAM
174
+ if self.db_user:
175
+ connection_kwargs["db_user"] = self.db_user
176
+
177
+ # Region for IAM credential retrieval
178
+ if self.region:
179
+ connection_kwargs["region"] = self.region
180
+
181
+ # AWS credentials - either profile or explicit
182
+ if self.profile:
183
+ connection_kwargs["profile"] = self.profile
184
+ else:
185
+ # Explicit AWS credentials
186
+ if self.access_key_id:
187
+ connection_kwargs["access_key_id"] = self.access_key_id
188
+ if self.secret_access_key:
189
+ connection_kwargs["secret_access_key"] = self.secret_access_key
190
+ if self.session_token:
191
+ connection_kwargs["session_token"] = self.session_token
192
+
193
+ else:
194
+ # Standard username/password authentication
195
+ if self.user:
196
+ connection_kwargs["user"] = self.user
197
+ if self.password:
198
+ connection_kwargs["password"] = self.password
199
+
200
+ return connection_kwargs
201
+
202
+ def _execute_query(self, query: str, params: Optional[tuple] = None) -> str:
203
+ try:
204
+ connection = self._ensure_connection()
205
+ with connection.cursor() as cursor:
206
+ log_debug("Running Redshift query")
207
+ cursor.execute(query, params)
208
+
209
+ if cursor.description is None:
210
+ return "Query executed successfully."
211
+
212
+ columns = [desc[0] for desc in cursor.description]
213
+ rows = cursor.fetchall()
214
+
215
+ if not rows:
216
+ return f"Query returned no results.\nColumns: {', '.join(columns)}"
217
+
218
+ header = ",".join(columns)
219
+ data_rows = [",".join(map(str, row)) for row in rows]
220
+ return f"{header}\n" + "\n".join(data_rows)
221
+
222
+ except redshift_connector.Error as e:
223
+ log_error(f"Database error: {e}")
224
+ if self._connection:
225
+ try:
226
+ self._connection.rollback()
227
+ except Exception:
228
+ pass # Connection might be closed
229
+ return f"Error executing query: {e}"
230
+ except Exception as e:
231
+ log_error(f"An unexpected error occurred: {e}")
232
+ return f"An unexpected error occurred: {e}"
233
+
234
+ def show_tables(self) -> str:
235
+ """Lists all tables in the configured schema."""
236
+
237
+ stmt = "SELECT table_name FROM information_schema.tables WHERE table_schema = %s;"
238
+ return self._execute_query(stmt, (self.table_schema,))
239
+
240
+ def describe_table(self, table: str) -> str:
241
+ """
242
+ Provides the schema (column name, data type, is nullable) for a given table.
243
+
244
+ Args:
245
+ table: The name of the table to describe.
246
+
247
+ Returns:
248
+ A string describing the table's columns and data types.
249
+ """
250
+ stmt = """
251
+ SELECT column_name, data_type, is_nullable
252
+ FROM information_schema.columns
253
+ WHERE table_schema = %s AND table_name = %s;
254
+ """
255
+ return self._execute_query(stmt, (self.table_schema, table))
256
+
257
+ def summarize_table(self, table: str) -> str:
258
+ """
259
+ Computes and returns key summary statistics for a table's columns.
260
+
261
+ Args:
262
+ table: The name of the table to summarize.
263
+
264
+ Returns:
265
+ A string containing a summary of the table.
266
+ """
267
+ try:
268
+ connection = self._ensure_connection()
269
+ with connection.cursor() as cursor:
270
+ # First, get column information using a parameterized query
271
+ schema_query = """
272
+ SELECT column_name, data_type
273
+ FROM information_schema.columns
274
+ WHERE table_schema = %s AND table_name = %s;
275
+ """
276
+ cursor.execute(schema_query, (self.table_schema, table))
277
+ columns = cursor.fetchall()
278
+ if not columns:
279
+ return f"Error: Table '{table}' not found in schema '{self.table_schema}'."
280
+
281
+ summary_parts = [f"Summary for table: {table}\n"]
282
+
283
+ # Redshift uses schema.table format for fully qualified names
284
+ full_table_name = f'"{self.table_schema}"."{table}"'
285
+
286
+ for col in columns:
287
+ col_name = col[0]
288
+ data_type = col[1]
289
+
290
+ query = None
291
+ if any(
292
+ t in data_type.lower()
293
+ for t in [
294
+ "integer",
295
+ "int",
296
+ "bigint",
297
+ "smallint",
298
+ "numeric",
299
+ "decimal",
300
+ "real",
301
+ "double precision",
302
+ "float",
303
+ ]
304
+ ):
305
+ query = f"""
306
+ SELECT
307
+ COUNT(*) AS total_rows,
308
+ COUNT("{col_name}") AS non_null_rows,
309
+ MIN("{col_name}") AS min,
310
+ MAX("{col_name}") AS max,
311
+ AVG("{col_name}") AS average,
312
+ STDDEV("{col_name}") AS std_deviation
313
+ FROM {full_table_name};
314
+ """
315
+ elif any(t in data_type.lower() for t in ["char", "varchar", "text", "uuid"]):
316
+ query = f"""
317
+ SELECT
318
+ COUNT(*) AS total_rows,
319
+ COUNT("{col_name}") AS non_null_rows,
320
+ COUNT(DISTINCT "{col_name}") AS unique_values,
321
+ AVG(LEN("{col_name}")) as avg_length
322
+ FROM {full_table_name};
323
+ """
324
+
325
+ if query:
326
+ cursor.execute(query)
327
+ stats = cursor.fetchone()
328
+ summary_parts.append(f"\n--- Column: {col_name} (Type: {data_type}) ---")
329
+ if stats is not None:
330
+ stats_dict = dict(zip([desc[0] for desc in cursor.description], stats))
331
+ for key, value in stats_dict.items():
332
+ val_str = (
333
+ f"{value:.2f}" if isinstance(value, float) and value is not None else str(value)
334
+ )
335
+ summary_parts.append(f" {key}: {val_str}")
336
+ else:
337
+ summary_parts.append(" No statistics available")
338
+
339
+ return "\n".join(summary_parts)
340
+
341
+ except redshift_connector.Error as e:
342
+ return f"Error summarizing table: {e}"
343
+
344
+ def inspect_query(self, query: str) -> str:
345
+ """
346
+ Shows the execution plan for a SQL query (using EXPLAIN).
347
+
348
+ Args:
349
+ query: The SQL query to inspect.
350
+
351
+ Returns:
352
+ The query's execution plan.
353
+ """
354
+ return self._execute_query(f"EXPLAIN {query}")
355
+
356
+ def export_table_to_path(self, table: str, path: str) -> str:
357
+ """
358
+ Exports a table's data to a local CSV file.
359
+
360
+ Args:
361
+ table: The name of the table to export.
362
+ path: The local file path to save the file.
363
+
364
+ Returns:
365
+ A confirmation message with the file path.
366
+ """
367
+ log_debug(f"Exporting table {table} to {path}")
368
+
369
+ full_table_name = f'"{self.table_schema}"."{table}"'
370
+ stmt = f"SELECT * FROM {full_table_name};"
371
+
372
+ try:
373
+ connection = self._ensure_connection()
374
+ with connection.cursor() as cursor:
375
+ cursor.execute(stmt)
376
+
377
+ if cursor.description is None:
378
+ return f"Error: Query returned no description for table '{table}'."
379
+
380
+ columns = [desc[0] for desc in cursor.description]
381
+
382
+ with open(path, "w", newline="", encoding="utf-8") as f:
383
+ writer = csv.writer(f)
384
+ writer.writerow(columns)
385
+ writer.writerows(cursor)
386
+
387
+ return f"Successfully exported table '{table}' to '{path}'."
388
+ except (redshift_connector.Error, IOError) as e:
389
+ if self._connection:
390
+ try:
391
+ self._connection.rollback()
392
+ except Exception:
393
+ pass # Connection might be closed
394
+ return f"Error exporting table: {e}"
395
+
396
+ def run_query(self, query: str) -> str:
397
+ """
398
+ Runs a read-only SQL query and returns the result.
399
+
400
+ Args:
401
+ query: The SQL query to run.
402
+
403
+ Returns:
404
+ The query result as a formatted string.
405
+ """
406
+ return self._execute_query(query)