Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## v0.8.23 - 2025-11-10

### Bug Fixes
- Enhanced chain utility functions with better error handling
- Improved ENS resolution with fallback mechanisms
- Updated logging and error reporting
- Fixed various bugs in utility functions

### Features
- Added comprehensive test coverage for chain utilities
- Enhanced ENS utilities for improved reliability
- Improved agent and chat model functionality
- Updated configuration handling

### Improvements
- Refactored chain utility functions for better performance
- Enhanced error handling in various components
- Dependency updates and optimizations
- Improved code organization and structure

**Full Changelog**: https://github.com/crestalnetwork/intentkit/compare/v0.8.22...v0.8.23

## v0.8.22

### Features
Expand Down Expand Up @@ -262,7 +284,7 @@ This release focuses on improving the x402 integration with better error handlin
- **Improved @clear command matching**: Enhanced the @clear command with case-insensitive regex matching and support for both @clear and /clear formats
- Case-insensitive matching: Now supports @Clear, @CLEAR, /Clear, /CLEAR, etc.
- Multiple formats: Added support for both @clear and /clear commands
- Word boundary matching: Uses regex with \b to ensure exact word matching
- Word boundary matching: Uses regex with  to ensure exact word matching
- Trim support: Messages are trimmed before matching to handle whitespace

**Full Changelog**: https://github.com/crestalnetwork/intentkit/compare/v0.8.1...v0.8.2
Expand Down Expand Up @@ -1256,4 +1278,4 @@ This release focuses on improving the x402 integration with better error handlin
- Improve twitter skills

### Bug Fixes
- Fix bug in db base code
- Fix bug in db base code
194 changes: 113 additions & 81 deletions intentkit/config/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging
import os
from typing import overload

from dotenv import load_dotenv

Expand All @@ -9,13 +10,16 @@
from intentkit.utils.s3 import init_s3
from intentkit.utils.slack_alert import init_slack

SecretsMap = dict[str, str | int]
DatabaseConfig = dict[str, str | int | bool]

# Load environment variables from .env file
load_dotenv()
_ = load_dotenv()

logger = logging.getLogger(__name__)


def load_from_aws(name):
def load_from_aws(name: str) -> SecretsMap:
import botocore.session
from aws_secretsmanager_caching import SecretCache, SecretCacheConfig

Expand All @@ -27,25 +31,25 @@ def load_from_aws(name):


class Config:
def __init__(self):
def __init__(self) -> None:
# ==== this part can only be load from env
self.env = os.getenv("ENV", "local")
self.release = os.getenv("RELEASE", "local")
secret_name = os.getenv("AWS_SECRET_NAME")
db_secret_name = os.getenv("AWS_DB_SECRET_NAME")
self.env: str = os.getenv("ENV", "local")
self.release: str = os.getenv("RELEASE", "local")
secret_name: str | None = os.getenv("AWS_SECRET_NAME")
db_secret_name: str | None = os.getenv("AWS_DB_SECRET_NAME")
# ==== load from aws secrets manager
self.secrets: SecretsMap = {}
if secret_name:
self.secrets = load_from_aws(secret_name)
else:
self.secrets = {}
self.db: DatabaseConfig
if db_secret_name:
self.db = load_from_aws(db_secret_name)
secret_db: SecretsMap = load_from_aws(db_secret_name)
# format the db config
self.db["port"] = str(self.db["port"])
secret_db["port"] = str(secret_db["port"])
# only keep the necessary fields
self.db = {
k: v
for k, v in self.db.items()
for k, v in secret_db.items()
if k in ["username", "password", "host", "dbname", "port"]
}
else:
Expand All @@ -59,102 +63,117 @@ def __init__(self):
# ==== this part can be load from env or aws secrets manager
self.db["auto_migrate"] = self.load("DB_AUTO_MIGRATE", "true") == "true"
self.db["pool_size"] = self.load_int("DB_POOL_SIZE", 3)
self.debug = self.load("DEBUG") == "true"
self.debug_checkpoint = (
self.debug: bool = self.load("DEBUG") == "true"
self.debug_checkpoint: bool = (
self.load("DEBUG_CHECKPOINT", "false") == "true"
) # log with checkpoint
# Redis
self.redis_host = self.load("REDIS_HOST")
self.redis_port = self.load_int("REDIS_PORT", 6379)
self.redis_db = self.load_int("REDIS_DB", 0)
self.redis_host: str | None = self.load("REDIS_HOST")
self.redis_port: int = self.load_int("REDIS_PORT", 6379)
self.redis_db: int = self.load_int("REDIS_DB", 0)
# AWS
self.aws_s3_bucket = self.load("AWS_S3_BUCKET")
self.aws_s3_cdn_url = self.load("AWS_S3_CDN_URL")
self.aws_s3_bucket: str | None = self.load("AWS_S3_BUCKET")
self.aws_s3_cdn_url: str | None = self.load("AWS_S3_CDN_URL")
# Internal API
self.internal_base_url = self.load("INTERNAL_BASE_URL", "http://intent-api")
self.admin_auth_enabled = self.load("ADMIN_AUTH_ENABLED", "false") == "true"
self.admin_jwt_secret = self.load("ADMIN_JWT_SECRET")
self.debug_auth_enabled = self.load("DEBUG_AUTH_ENABLED", "false") == "true"
self.debug_username = self.load("DEBUG_USERNAME")
self.debug_password = self.load("DEBUG_PASSWORD")
self.internal_base_url: str = self.load(
"INTERNAL_BASE_URL", "http://intent-api"
)
self.admin_auth_enabled: bool = (
self.load("ADMIN_AUTH_ENABLED", "false") == "true"
)
self.admin_jwt_secret: str | None = self.load("ADMIN_JWT_SECRET")
self.debug_auth_enabled: bool = (
self.load("DEBUG_AUTH_ENABLED", "false") == "true"
)
self.debug_username: str | None = self.load("DEBUG_USERNAME")
self.debug_password: str | None = self.load("DEBUG_PASSWORD")
# Payment
self.payment_enabled = self.load("PAYMENT_ENABLED", "false") == "true"
self.x402_fee_address = self.load("X402_FEE_ADDRESS")
self.payment_enabled: bool = self.load("PAYMENT_ENABLED", "false") == "true"
self.x402_fee_address: str | None = self.load("X402_FEE_ADDRESS")
# Open API for agent
self.open_api_base_url = self.load("OPEN_API_BASE_URL", "http://localhost:8000")
self.open_api_base_url: str = self.load(
"OPEN_API_BASE_URL", "http://localhost:8000"
)
# CDP - AgentKit 0.7.x Configuration
self.cdp_api_key_id = self.load("CDP_API_KEY_ID")
self.cdp_api_key_secret = self.load("CDP_API_KEY_SECRET")
self.cdp_wallet_secret = self.load("CDP_WALLET_SECRET")
self.cdp_api_key_id: str | None = self.load("CDP_API_KEY_ID")
self.cdp_api_key_secret: str | None = self.load("CDP_API_KEY_SECRET")
self.cdp_wallet_secret: str | None = self.load("CDP_WALLET_SECRET")
# LLM providers
self.openai_api_key = self.load("OPENAI_API_KEY")
self.deepseek_api_key = self.load("DEEPSEEK_API_KEY")
self.xai_api_key = self.load("XAI_API_KEY")
self.eternal_api_key = self.load("ETERNAL_API_KEY")
self.reigent_api_key = self.load("REIGENT_API_KEY")
self.venice_api_key = self.load("VENICE_API_KEY")
self.gatewayz_api_key = self.load("GATEWAYZ_API_KEY")
self.openai_api_key: str | None = self.load("OPENAI_API_KEY")
self.deepseek_api_key: str | None = self.load("DEEPSEEK_API_KEY")
self.xai_api_key: str | None = self.load("XAI_API_KEY")
self.eternal_api_key: str | None = self.load("ETERNAL_API_KEY")
self.reigent_api_key: str | None = self.load("REIGENT_API_KEY")
self.venice_api_key: str | None = self.load("VENICE_API_KEY")
self.gatewayz_api_key: str | None = self.load("GATEWAYZ_API_KEY")
# LLM Config
self.system_prompt = self.load("SYSTEM_PROMPT")
self.intentkit_prompt = self.load("INTENTKIT_PROMPT")
self.input_token_limit = self.load_int("INPUT_TOKEN_LIMIT", 60000)
self.system_prompt: str | None = self.load("SYSTEM_PROMPT")
self.intentkit_prompt: str | None = self.load("INTENTKIT_PROMPT")
self.input_token_limit: int = self.load_int("INPUT_TOKEN_LIMIT", 60000)
# XMTP
self.xmtp_system_prompt = self.load(
self.xmtp_system_prompt: str | None = self.load(
"XMTP_SYSTEM_PROMPT",
"You are assisting a user who uses an XMTP client that only displays plain-text messages, so do not use Markdown formatting.",
)
# Telegram server settings
self.tg_system_prompt = self.load("TG_SYSTEM_PROMPT")
self.tg_base_url = self.load("TG_BASE_URL")
self.tg_server_host = self.load("TG_SERVER_HOST", "127.0.0.1")
self.tg_server_port = self.load("TG_SERVER_PORT", "8081")
self.tg_new_agent_poll_interval = self.load("TG_NEW_AGENT_POLL_INTERVAL", "60")
self.tg_system_prompt: str | None = self.load("TG_SYSTEM_PROMPT")
self.tg_base_url: str | None = self.load("TG_BASE_URL")
self.tg_server_host: str = self.load("TG_SERVER_HOST", "127.0.0.1")
self.tg_server_port: str = self.load("TG_SERVER_PORT", "8081")
self.tg_new_agent_poll_interval: str = self.load(
"TG_NEW_AGENT_POLL_INTERVAL", "60"
)
# Discord server settings
self.discord_new_agent_poll_interval = self.load(
self.discord_new_agent_poll_interval: str = self.load(
"DISCORD_NEW_AGENT_POLL_INTERVAL", "30"
)
# Twitter
self.twitter_oauth2_client_id = self.load("TWITTER_OAUTH2_CLIENT_ID")
self.twitter_oauth2_client_secret = self.load("TWITTER_OAUTH2_CLIENT_SECRET")
self.twitter_oauth2_redirect_uri = self.load("TWITTER_OAUTH2_REDIRECT_URI")
self.twitter_entrypoint_interval = self.load_int(
self.twitter_oauth2_client_id: str | None = self.load(
"TWITTER_OAUTH2_CLIENT_ID"
)
self.twitter_oauth2_client_secret: str | None = self.load(
"TWITTER_OAUTH2_CLIENT_SECRET"
)
self.twitter_oauth2_redirect_uri: str | None = self.load(
"TWITTER_OAUTH2_REDIRECT_URI"
)
self.twitter_entrypoint_interval: int = self.load_int(
"TWITTER_ENTRYPOINT_INTERVAL", 5
) # in minutes
# Slack Alert
self.slack_alert_token = self.load("SLACK_ALERT_TOKEN")
self.slack_alert_channel = self.load("SLACK_ALERT_CHANNEL")
self.slack_alert_token: str | None = self.load("SLACK_ALERT_TOKEN")
self.slack_alert_channel: str | None = self.load("SLACK_ALERT_CHANNEL")
# Skills - Platform Hosted Keys
self.acolyt_api_key = self.load("ACOLYT_API_KEY")
self.allora_api_key = self.load("ALLORA_API_KEY")
self.elfa_api_key = self.load("ELFA_API_KEY")
self.heurist_api_key = self.load("HEURIST_API_KEY")
self.enso_api_token = self.load("ENSO_API_TOKEN")
self.dapplooker_api_key = self.load("DAPPLOOKER_API_KEY")
self.moralis_api_key = self.load("MORALIS_API_KEY")
self.tavily_api_key = self.load("TAVILY_API_KEY")
self.cookiefun_api_key = self.load("COOKIEFUN_API_KEY")
self.firecrawl_api_key = self.load("FIRECRAWL_API_KEY")
self.acolyt_api_key: str | None = self.load("ACOLYT_API_KEY")
self.allora_api_key: str | None = self.load("ALLORA_API_KEY")
self.elfa_api_key: str | None = self.load("ELFA_API_KEY")
self.heurist_api_key: str | None = self.load("HEURIST_API_KEY")
self.enso_api_token: str | None = self.load("ENSO_API_TOKEN")
self.dapplooker_api_key: str | None = self.load("DAPPLOOKER_API_KEY")
self.moralis_api_key: str | None = self.load("MORALIS_API_KEY")
self.tavily_api_key: str | None = self.load("TAVILY_API_KEY")
self.cookiefun_api_key: str | None = self.load("COOKIEFUN_API_KEY")
self.firecrawl_api_key: str | None = self.load("FIRECRAWL_API_KEY")
# Sentry
self.sentry_dsn = self.load("SENTRY_DSN")
self.sentry_sample_rate = self.load_float("SENTRY_SAMPLE_RATE", 0.1)
self.sentry_traces_sample_rate = self.load_float(
self.sentry_dsn: str | None = self.load("SENTRY_DSN")
self.sentry_sample_rate: float = self.load_float("SENTRY_SAMPLE_RATE", 0.1)
self.sentry_traces_sample_rate: float = self.load_float(
"SENTRY_TRACES_SAMPLE_RATE", 0.01
)
self.sentry_profiles_sample_rate = self.load_float(
self.sentry_profiles_sample_rate: float = self.load_float(
"SENTRY_PROFILES_SAMPLE_RATE", 0.01
)
# RPC Providers
self.quicknode_api_key = self.load("QUICKNODE_API_KEY")
self.quicknode_api_key: str | None = self.load("QUICKNODE_API_KEY")
self.chain_provider: ChainProvider | None = None
if self.quicknode_api_key:
self.chain_provider: ChainProvider = QuicknodeChainProvider(
self.quicknode_api_key
)
if hasattr(self, "chain_provider"):
self.chain_provider = QuicknodeChainProvider(self.quicknode_api_key)
if self.chain_provider:
self.chain_provider.init_chain_configs()

# Nation
self.nation_api_key = self.load("NATION_API_KEY")
self.nation_api_url = self.load("NATION_API_URL", "")
self.nation_api_key: str | None = self.load("NATION_API_KEY")
self.nation_api_url: str = self.load("NATION_API_URL", "")

# ===== config loaded
# Now we know the env, set up logging
Expand All @@ -168,9 +187,22 @@ def __init__(self):
if self.aws_s3_bucket and self.aws_s3_cdn_url:
init_s3(self.aws_s3_bucket, self.aws_s3_cdn_url, self.env)

def load(self, key, default=None):
@overload
def load(self, key: str) -> str | None: ... # noqa: F811

@overload
def load(self, key: str, default: str) -> str: ... # noqa: F811

def load(self, key: str, default: str | None = None) -> str | None:
"""Load a secret from the secrets map or env"""
value = self.secrets.get(key, os.getenv(key, default))
env_value = os.getenv(key, default)
raw_value = self.secrets.get(key, env_value)
if raw_value is None:
value: str | None = default
elif isinstance(raw_value, str):
value = raw_value
else:
value = str(raw_value)

# If value is empty string, use default instead
if value == "":
Expand All @@ -182,21 +214,21 @@ def load(self, key, default=None):
value = value[1:-1]
return value

def load_int(self, key, default=0):
def load_int(self, key: str, default: int = 0) -> int:
"""Load an integer value from env, handling empty strings gracefully"""
value = self.load(key, str(default))
if value is None or value == "":
if not value:
return default
try:
return int(value)
except (ValueError, TypeError):
logger.warning(f"Invalid integer value for {key}, using default: {default}")
return default

def load_float(self, key, default=0.0):
def load_float(self, key: str, default: float = 0.0) -> float:
"""Load a float value from env, handling empty strings gracefully"""
value = self.load(key, str(default))
if value is None or value == "":
if not value:
return default
try:
return float(value)
Expand Down
11 changes: 5 additions & 6 deletions intentkit/models/llm.csv
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
id,name,provider,enabled,input_price,output_price,price_level,context_length,output_length,intelligence,speed,supports_image_input,supports_skill_calls,supports_structured_output,has_reasoning,supports_search,supports_temperature,supports_frequency_penalty,supports_presence_penalty,api_base,timeout
minimax/minimax-m2:free,MiniMax M2,gatewayz,TRUE,0,0,1,204800,131000,4,2,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
qwen/qwen3-235b-a22b-2507,Qwen3 235B A22B Instruct 2507,gatewayz,TRUE,0.1,0.6,1,262000,262000,3,3,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
qwen/qwen3-max,Qwen3 Max,gatewayz,TRUE,1.2,6,4,128000,32000,4,2,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
google/gemini-2.5-flash,Gemini 2.5 Flash,gatewayz,TRUE,0.3,2.5,2,1050000,65000,3,4,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
google/gemini-2.5-pro,Gemini 2.5 Pro,gatewayz,TRUE,1.25,10,4,1050000,65000,4,2,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
anthropic/claude-sonnet-4.5,Anthropic Claude Sonnet 4.5,gatewayz,TRUE,3,15,5,200000,64000,5,1,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
minimax/minimax-m2:free,MiniMax M2,gatewayz,FALSE,0,0,1,204800,131000,4,2,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
qwen/qwen3-coder:exacto,Qwen3 Coder,gatewayz,FALSE,0.38,1.53,2,262000,262000,3,3,FALSE,TRUE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
google/gemini-2.5-flash,Gemini 2.5 Flash,gatewayz,FALSE,0.3,2.5,2,1050000,65000,3,4,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
google/gemini-2.5-pro,Gemini 2.5 Pro,gatewayz,FALSE,1.25,10,4,1050000,65000,4,2,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
anthropic/claude-sonnet-4.5,Anthropic Claude Sonnet 4.5,gatewayz,FALSE,3,15,5,200000,64000,5,1,TRUE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,FALSE,https://api.gatewayz.ai/v1,300
gpt-5-nano,GPT-5 Nano,openai,TRUE,0.05,0.4,1,400000,128000,3,5,TRUE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,FALSE,,180
gpt-5-mini,GPT-5 Mini,openai,TRUE,0.25,2,2,400000,128000,4,4,TRUE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,FALSE,,180
gpt-5,GPT-5,openai,TRUE,1.25,10,4,400000,128000,5,3,TRUE,TRUE,TRUE,TRUE,TRUE,FALSE,FALSE,FALSE,,180
Expand Down
2 changes: 2 additions & 0 deletions intentkit/models/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ def _load_default_llm_models() -> dict[str, "LLMModelInfo"]:
created_at=timestamp,
updated_at=timestamp,
)
if not model.enabled:
continue
except Exception as exc:
logger.error(
"Failed to load default LLM model %s: %s", row.get("id"), exc
Expand Down
Loading
Loading