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.
- agno/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {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)
|