intentkit 0.5.0__py3-none-any.whl → 0.5.2__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.
Potentially problematic release.
This version of intentkit might be problematic. Click here for more details.
- intentkit/__init__.py +17 -0
- intentkit/abstracts/__init__.py +0 -0
- intentkit/abstracts/agent.py +60 -0
- intentkit/abstracts/api.py +4 -0
- intentkit/abstracts/engine.py +38 -0
- intentkit/abstracts/exception.py +9 -0
- intentkit/abstracts/graph.py +25 -0
- intentkit/abstracts/skill.py +129 -0
- intentkit/abstracts/twitter.py +54 -0
- intentkit/clients/__init__.py +14 -0
- intentkit/clients/cdp.py +53 -0
- intentkit/clients/twitter.py +445 -0
- intentkit/config/__init__.py +0 -0
- intentkit/config/config.py +164 -0
- intentkit/core/__init__.py +0 -0
- intentkit/core/agent.py +191 -0
- intentkit/core/api.py +40 -0
- intentkit/core/client.py +45 -0
- intentkit/core/credit.py +1767 -0
- intentkit/core/engine.py +1018 -0
- intentkit/core/node.py +223 -0
- intentkit/core/prompt.py +58 -0
- intentkit/core/skill.py +124 -0
- intentkit/models/agent.py +1689 -0
- intentkit/models/agent_data.py +810 -0
- intentkit/models/agent_schema.json +733 -0
- intentkit/models/app_setting.py +156 -0
- intentkit/models/base.py +9 -0
- intentkit/models/chat.py +581 -0
- intentkit/models/conversation.py +286 -0
- intentkit/models/credit.py +1406 -0
- intentkit/models/db.py +120 -0
- intentkit/models/db_mig.py +102 -0
- intentkit/models/generator.py +347 -0
- intentkit/models/llm.py +746 -0
- intentkit/models/redis.py +132 -0
- intentkit/models/skill.py +466 -0
- intentkit/models/user.py +243 -0
- intentkit/skills/__init__.py +12 -0
- intentkit/skills/acolyt/__init__.py +83 -0
- intentkit/skills/acolyt/acolyt.jpg +0 -0
- intentkit/skills/acolyt/ask.py +128 -0
- intentkit/skills/acolyt/base.py +28 -0
- intentkit/skills/acolyt/schema.json +89 -0
- intentkit/skills/aixbt/README.md +71 -0
- intentkit/skills/aixbt/__init__.py +73 -0
- intentkit/skills/aixbt/aixbt.jpg +0 -0
- intentkit/skills/aixbt/base.py +21 -0
- intentkit/skills/aixbt/projects.py +153 -0
- intentkit/skills/aixbt/schema.json +99 -0
- intentkit/skills/allora/__init__.py +83 -0
- intentkit/skills/allora/allora.jpeg +0 -0
- intentkit/skills/allora/base.py +28 -0
- intentkit/skills/allora/price.py +130 -0
- intentkit/skills/allora/schema.json +89 -0
- intentkit/skills/base.py +174 -0
- intentkit/skills/carv/README.md +95 -0
- intentkit/skills/carv/__init__.py +121 -0
- intentkit/skills/carv/base.py +183 -0
- intentkit/skills/carv/carv.webp +0 -0
- intentkit/skills/carv/fetch_news.py +92 -0
- intentkit/skills/carv/onchain_query.py +164 -0
- intentkit/skills/carv/schema.json +137 -0
- intentkit/skills/carv/token_info_and_price.py +110 -0
- intentkit/skills/cdp/__init__.py +137 -0
- intentkit/skills/cdp/base.py +21 -0
- intentkit/skills/cdp/cdp.png +0 -0
- intentkit/skills/cdp/get_balance.py +81 -0
- intentkit/skills/cdp/schema.json +473 -0
- intentkit/skills/chainlist/README.md +38 -0
- intentkit/skills/chainlist/__init__.py +54 -0
- intentkit/skills/chainlist/base.py +21 -0
- intentkit/skills/chainlist/chain_lookup.py +208 -0
- intentkit/skills/chainlist/chainlist.png +0 -0
- intentkit/skills/chainlist/schema.json +47 -0
- intentkit/skills/common/__init__.py +82 -0
- intentkit/skills/common/base.py +21 -0
- intentkit/skills/common/common.jpg +0 -0
- intentkit/skills/common/current_time.py +84 -0
- intentkit/skills/common/schema.json +57 -0
- intentkit/skills/cookiefun/README.md +121 -0
- intentkit/skills/cookiefun/__init__.py +78 -0
- intentkit/skills/cookiefun/base.py +41 -0
- intentkit/skills/cookiefun/constants.py +18 -0
- intentkit/skills/cookiefun/cookiefun.png +0 -0
- intentkit/skills/cookiefun/get_account_details.py +171 -0
- intentkit/skills/cookiefun/get_account_feed.py +282 -0
- intentkit/skills/cookiefun/get_account_smart_followers.py +181 -0
- intentkit/skills/cookiefun/get_sectors.py +128 -0
- intentkit/skills/cookiefun/schema.json +155 -0
- intentkit/skills/cookiefun/search_accounts.py +225 -0
- intentkit/skills/cryptocompare/__init__.py +130 -0
- intentkit/skills/cryptocompare/api.py +159 -0
- intentkit/skills/cryptocompare/base.py +303 -0
- intentkit/skills/cryptocompare/cryptocompare.png +0 -0
- intentkit/skills/cryptocompare/fetch_news.py +96 -0
- intentkit/skills/cryptocompare/fetch_price.py +99 -0
- intentkit/skills/cryptocompare/fetch_top_exchanges.py +113 -0
- intentkit/skills/cryptocompare/fetch_top_market_cap.py +109 -0
- intentkit/skills/cryptocompare/fetch_top_volume.py +108 -0
- intentkit/skills/cryptocompare/fetch_trading_signals.py +107 -0
- intentkit/skills/cryptocompare/schema.json +168 -0
- intentkit/skills/cryptopanic/__init__.py +108 -0
- intentkit/skills/cryptopanic/base.py +51 -0
- intentkit/skills/cryptopanic/cryptopanic.png +0 -0
- intentkit/skills/cryptopanic/fetch_crypto_news.py +153 -0
- intentkit/skills/cryptopanic/fetch_crypto_sentiment.py +136 -0
- intentkit/skills/cryptopanic/schema.json +103 -0
- intentkit/skills/dapplooker/README.md +92 -0
- intentkit/skills/dapplooker/__init__.py +83 -0
- intentkit/skills/dapplooker/base.py +26 -0
- intentkit/skills/dapplooker/dapplooker.jpg +0 -0
- intentkit/skills/dapplooker/dapplooker_token_data.py +476 -0
- intentkit/skills/dapplooker/schema.json +91 -0
- intentkit/skills/defillama/__init__.py +323 -0
- intentkit/skills/defillama/api.py +315 -0
- intentkit/skills/defillama/base.py +135 -0
- intentkit/skills/defillama/coins/__init__.py +0 -0
- intentkit/skills/defillama/coins/fetch_batch_historical_prices.py +116 -0
- intentkit/skills/defillama/coins/fetch_block.py +98 -0
- intentkit/skills/defillama/coins/fetch_current_prices.py +105 -0
- intentkit/skills/defillama/coins/fetch_first_price.py +100 -0
- intentkit/skills/defillama/coins/fetch_historical_prices.py +110 -0
- intentkit/skills/defillama/coins/fetch_price_chart.py +109 -0
- intentkit/skills/defillama/coins/fetch_price_percentage.py +93 -0
- intentkit/skills/defillama/config/__init__.py +0 -0
- intentkit/skills/defillama/config/chains.py +433 -0
- intentkit/skills/defillama/defillama.jpeg +0 -0
- intentkit/skills/defillama/fees/__init__.py +0 -0
- intentkit/skills/defillama/fees/fetch_fees_overview.py +130 -0
- intentkit/skills/defillama/schema.json +383 -0
- intentkit/skills/defillama/stablecoins/__init__.py +0 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoin_chains.py +100 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoin_charts.py +129 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoin_prices.py +83 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoins.py +126 -0
- intentkit/skills/defillama/tests/__init__.py +0 -0
- intentkit/skills/defillama/tests/api_integration.test.py +192 -0
- intentkit/skills/defillama/tests/api_unit.test.py +583 -0
- intentkit/skills/defillama/tvl/__init__.py +0 -0
- intentkit/skills/defillama/tvl/fetch_chain_historical_tvl.py +106 -0
- intentkit/skills/defillama/tvl/fetch_chains.py +107 -0
- intentkit/skills/defillama/tvl/fetch_historical_tvl.py +91 -0
- intentkit/skills/defillama/tvl/fetch_protocol.py +207 -0
- intentkit/skills/defillama/tvl/fetch_protocol_current_tvl.py +93 -0
- intentkit/skills/defillama/tvl/fetch_protocols.py +196 -0
- intentkit/skills/defillama/volumes/__init__.py +0 -0
- intentkit/skills/defillama/volumes/fetch_dex_overview.py +157 -0
- intentkit/skills/defillama/volumes/fetch_dex_summary.py +123 -0
- intentkit/skills/defillama/volumes/fetch_options_overview.py +131 -0
- intentkit/skills/defillama/yields/__init__.py +0 -0
- intentkit/skills/defillama/yields/fetch_pool_chart.py +100 -0
- intentkit/skills/defillama/yields/fetch_pools.py +126 -0
- intentkit/skills/dexscreener/__init__.py +93 -0
- intentkit/skills/dexscreener/base.py +133 -0
- intentkit/skills/dexscreener/dexscreener.png +0 -0
- intentkit/skills/dexscreener/model/__init__.py +0 -0
- intentkit/skills/dexscreener/model/search_token_response.py +82 -0
- intentkit/skills/dexscreener/schema.json +48 -0
- intentkit/skills/dexscreener/search_token.py +321 -0
- intentkit/skills/dune_analytics/__init__.py +103 -0
- intentkit/skills/dune_analytics/base.py +46 -0
- intentkit/skills/dune_analytics/dune.png +0 -0
- intentkit/skills/dune_analytics/fetch_kol_buys.py +128 -0
- intentkit/skills/dune_analytics/fetch_nation_metrics.py +237 -0
- intentkit/skills/dune_analytics/schema.json +99 -0
- intentkit/skills/elfa/README.md +100 -0
- intentkit/skills/elfa/__init__.py +123 -0
- intentkit/skills/elfa/base.py +28 -0
- intentkit/skills/elfa/elfa.jpg +0 -0
- intentkit/skills/elfa/mention.py +504 -0
- intentkit/skills/elfa/schema.json +153 -0
- intentkit/skills/elfa/stats.py +118 -0
- intentkit/skills/elfa/tokens.py +126 -0
- intentkit/skills/enso/README.md +75 -0
- intentkit/skills/enso/__init__.py +114 -0
- intentkit/skills/enso/abi/__init__.py +0 -0
- intentkit/skills/enso/abi/approval.py +279 -0
- intentkit/skills/enso/abi/erc20.py +14 -0
- intentkit/skills/enso/abi/route.py +129 -0
- intentkit/skills/enso/base.py +44 -0
- intentkit/skills/enso/best_yield.py +286 -0
- intentkit/skills/enso/enso.jpg +0 -0
- intentkit/skills/enso/networks.py +105 -0
- intentkit/skills/enso/prices.py +93 -0
- intentkit/skills/enso/route.py +300 -0
- intentkit/skills/enso/schema.json +212 -0
- intentkit/skills/enso/tokens.py +223 -0
- intentkit/skills/enso/wallet.py +381 -0
- intentkit/skills/github/README.md +63 -0
- intentkit/skills/github/__init__.py +54 -0
- intentkit/skills/github/base.py +21 -0
- intentkit/skills/github/github.jpg +0 -0
- intentkit/skills/github/github_search.py +183 -0
- intentkit/skills/github/schema.json +59 -0
- intentkit/skills/heurist/__init__.py +143 -0
- intentkit/skills/heurist/base.py +26 -0
- intentkit/skills/heurist/heurist.png +0 -0
- intentkit/skills/heurist/image_generation_animagine_xl.py +162 -0
- intentkit/skills/heurist/image_generation_arthemy_comics.py +162 -0
- intentkit/skills/heurist/image_generation_arthemy_real.py +162 -0
- intentkit/skills/heurist/image_generation_braindance.py +162 -0
- intentkit/skills/heurist/image_generation_cyber_realistic_xl.py +162 -0
- intentkit/skills/heurist/image_generation_flux_1_dev.py +162 -0
- intentkit/skills/heurist/image_generation_sdxl.py +161 -0
- intentkit/skills/heurist/schema.json +196 -0
- intentkit/skills/lifi/README.md +294 -0
- intentkit/skills/lifi/__init__.py +141 -0
- intentkit/skills/lifi/base.py +21 -0
- intentkit/skills/lifi/lifi.png +0 -0
- intentkit/skills/lifi/schema.json +89 -0
- intentkit/skills/lifi/token_execute.py +472 -0
- intentkit/skills/lifi/token_quote.py +190 -0
- intentkit/skills/lifi/utils.py +656 -0
- intentkit/skills/moralis/README.md +490 -0
- intentkit/skills/moralis/__init__.py +110 -0
- intentkit/skills/moralis/api.py +281 -0
- intentkit/skills/moralis/base.py +55 -0
- intentkit/skills/moralis/fetch_chain_portfolio.py +191 -0
- intentkit/skills/moralis/fetch_nft_portfolio.py +284 -0
- intentkit/skills/moralis/fetch_solana_portfolio.py +331 -0
- intentkit/skills/moralis/fetch_wallet_portfolio.py +301 -0
- intentkit/skills/moralis/moralis.png +0 -0
- intentkit/skills/moralis/schema.json +156 -0
- intentkit/skills/moralis/tests/__init__.py +0 -0
- intentkit/skills/moralis/tests/test_wallet.py +511 -0
- intentkit/skills/nation/__init__.py +62 -0
- intentkit/skills/nation/base.py +31 -0
- intentkit/skills/nation/nation.png +0 -0
- intentkit/skills/nation/nft_check.py +106 -0
- intentkit/skills/nation/schema.json +58 -0
- intentkit/skills/openai/__init__.py +107 -0
- intentkit/skills/openai/base.py +32 -0
- intentkit/skills/openai/dalle_image_generation.py +128 -0
- intentkit/skills/openai/gpt_image_generation.py +152 -0
- intentkit/skills/openai/gpt_image_to_image.py +186 -0
- intentkit/skills/openai/image_to_text.py +126 -0
- intentkit/skills/openai/openai.png +0 -0
- intentkit/skills/openai/schema.json +139 -0
- intentkit/skills/portfolio/README.md +55 -0
- intentkit/skills/portfolio/__init__.py +151 -0
- intentkit/skills/portfolio/base.py +107 -0
- intentkit/skills/portfolio/constants.py +9 -0
- intentkit/skills/portfolio/moralis.png +0 -0
- intentkit/skills/portfolio/schema.json +237 -0
- intentkit/skills/portfolio/token_balances.py +155 -0
- intentkit/skills/portfolio/wallet_approvals.py +102 -0
- intentkit/skills/portfolio/wallet_defi_positions.py +80 -0
- intentkit/skills/portfolio/wallet_history.py +155 -0
- intentkit/skills/portfolio/wallet_net_worth.py +112 -0
- intentkit/skills/portfolio/wallet_nfts.py +139 -0
- intentkit/skills/portfolio/wallet_profitability.py +101 -0
- intentkit/skills/portfolio/wallet_profitability_summary.py +91 -0
- intentkit/skills/portfolio/wallet_stats.py +79 -0
- intentkit/skills/portfolio/wallet_swaps.py +147 -0
- intentkit/skills/skills.toml +103 -0
- intentkit/skills/slack/__init__.py +98 -0
- intentkit/skills/slack/base.py +55 -0
- intentkit/skills/slack/get_channel.py +109 -0
- intentkit/skills/slack/get_message.py +136 -0
- intentkit/skills/slack/schedule_message.py +92 -0
- intentkit/skills/slack/schema.json +135 -0
- intentkit/skills/slack/send_message.py +81 -0
- intentkit/skills/slack/slack.jpg +0 -0
- intentkit/skills/system/__init__.py +90 -0
- intentkit/skills/system/base.py +22 -0
- intentkit/skills/system/read_agent_api_key.py +87 -0
- intentkit/skills/system/regenerate_agent_api_key.py +77 -0
- intentkit/skills/system/schema.json +53 -0
- intentkit/skills/system/system.svg +76 -0
- intentkit/skills/tavily/README.md +86 -0
- intentkit/skills/tavily/__init__.py +91 -0
- intentkit/skills/tavily/base.py +27 -0
- intentkit/skills/tavily/schema.json +119 -0
- intentkit/skills/tavily/tavily.jpg +0 -0
- intentkit/skills/tavily/tavily_extract.py +147 -0
- intentkit/skills/tavily/tavily_search.py +139 -0
- intentkit/skills/token/README.md +89 -0
- intentkit/skills/token/__init__.py +107 -0
- intentkit/skills/token/base.py +154 -0
- intentkit/skills/token/constants.py +9 -0
- intentkit/skills/token/erc20_transfers.py +145 -0
- intentkit/skills/token/moralis.png +0 -0
- intentkit/skills/token/schema.json +141 -0
- intentkit/skills/token/token_analytics.py +81 -0
- intentkit/skills/token/token_price.py +132 -0
- intentkit/skills/token/token_search.py +121 -0
- intentkit/skills/twitter/__init__.py +146 -0
- intentkit/skills/twitter/base.py +68 -0
- intentkit/skills/twitter/follow_user.py +69 -0
- intentkit/skills/twitter/get_mentions.py +124 -0
- intentkit/skills/twitter/get_timeline.py +111 -0
- intentkit/skills/twitter/get_user_by_username.py +84 -0
- intentkit/skills/twitter/get_user_tweets.py +123 -0
- intentkit/skills/twitter/like_tweet.py +65 -0
- intentkit/skills/twitter/post_tweet.py +90 -0
- intentkit/skills/twitter/reply_tweet.py +98 -0
- intentkit/skills/twitter/retweet.py +76 -0
- intentkit/skills/twitter/schema.json +258 -0
- intentkit/skills/twitter/search_tweets.py +115 -0
- intentkit/skills/twitter/twitter.png +0 -0
- intentkit/skills/unrealspeech/__init__.py +55 -0
- intentkit/skills/unrealspeech/base.py +21 -0
- intentkit/skills/unrealspeech/schema.json +100 -0
- intentkit/skills/unrealspeech/text_to_speech.py +177 -0
- intentkit/skills/unrealspeech/unrealspeech.jpg +0 -0
- intentkit/skills/venice_audio/__init__.py +106 -0
- intentkit/skills/venice_audio/base.py +119 -0
- intentkit/skills/venice_audio/input.py +41 -0
- intentkit/skills/venice_audio/schema.json +152 -0
- intentkit/skills/venice_audio/venice_audio.py +240 -0
- intentkit/skills/venice_audio/venice_logo.jpg +0 -0
- intentkit/skills/venice_image/README.md +119 -0
- intentkit/skills/venice_image/__init__.py +154 -0
- intentkit/skills/venice_image/api.py +138 -0
- intentkit/skills/venice_image/base.py +188 -0
- intentkit/skills/venice_image/config.py +35 -0
- intentkit/skills/venice_image/image_enhance/README.md +119 -0
- intentkit/skills/venice_image/image_enhance/__init__.py +0 -0
- intentkit/skills/venice_image/image_enhance/image_enhance.py +80 -0
- intentkit/skills/venice_image/image_enhance/image_enhance_base.py +23 -0
- intentkit/skills/venice_image/image_enhance/image_enhance_input.py +40 -0
- intentkit/skills/venice_image/image_generation/README.md +144 -0
- intentkit/skills/venice_image/image_generation/__init__.py +0 -0
- intentkit/skills/venice_image/image_generation/image_generation_base.py +117 -0
- intentkit/skills/venice_image/image_generation/image_generation_fluently_xl.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_flux_dev.py +27 -0
- intentkit/skills/venice_image/image_generation/image_generation_flux_dev_uncensored.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_input.py +158 -0
- intentkit/skills/venice_image/image_generation/image_generation_lustify_sdxl.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_pony_realism.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_stable_diffusion_3_5.py +28 -0
- intentkit/skills/venice_image/image_generation/image_generation_venice_sd35.py +28 -0
- intentkit/skills/venice_image/image_upscale/README.md +111 -0
- intentkit/skills/venice_image/image_upscale/__init__.py +0 -0
- intentkit/skills/venice_image/image_upscale/image_upscale.py +90 -0
- intentkit/skills/venice_image/image_upscale/image_upscale_base.py +23 -0
- intentkit/skills/venice_image/image_upscale/image_upscale_input.py +22 -0
- intentkit/skills/venice_image/image_vision/README.md +112 -0
- intentkit/skills/venice_image/image_vision/__init__.py +0 -0
- intentkit/skills/venice_image/image_vision/image_vision.py +100 -0
- intentkit/skills/venice_image/image_vision/image_vision_base.py +17 -0
- intentkit/skills/venice_image/image_vision/image_vision_input.py +9 -0
- intentkit/skills/venice_image/schema.json +267 -0
- intentkit/skills/venice_image/utils.py +78 -0
- intentkit/skills/venice_image/venice_image.jpg +0 -0
- intentkit/skills/web_scraper/README.md +82 -0
- intentkit/skills/web_scraper/__init__.py +92 -0
- intentkit/skills/web_scraper/base.py +21 -0
- intentkit/skills/web_scraper/langchain.png +0 -0
- intentkit/skills/web_scraper/schema.json +115 -0
- intentkit/skills/web_scraper/scrape_and_index.py +327 -0
- intentkit/utils/__init__.py +1 -0
- intentkit/utils/chain.py +436 -0
- intentkit/utils/error.py +134 -0
- intentkit/utils/logging.py +70 -0
- intentkit/utils/middleware.py +61 -0
- intentkit/utils/random.py +16 -0
- intentkit/utils/s3.py +267 -0
- intentkit/utils/slack_alert.py +79 -0
- intentkit/utils/tx.py +37 -0
- {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/METADATA +1 -1
- intentkit-0.5.2.dist-info/RECORD +365 -0
- intentkit-0.5.0.dist-info/RECORD +0 -4
- {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/WHEEL +0 -0
- {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/licenses/LICENSE +0 -0
intentkit/skills/base.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Callable, Dict, Literal, NotRequired, Optional, TypedDict, Union
|
|
3
|
+
|
|
4
|
+
from langchain_core.runnables import RunnableConfig
|
|
5
|
+
from langchain_core.tools import BaseTool
|
|
6
|
+
from langchain_core.tools.base import ToolException
|
|
7
|
+
from pydantic import (
|
|
8
|
+
BaseModel,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
from pydantic.v1 import ValidationError as ValidationErrorV1
|
|
12
|
+
from redis.exceptions import RedisError
|
|
13
|
+
|
|
14
|
+
from intentkit.abstracts.exception import RateLimitExceeded
|
|
15
|
+
from intentkit.abstracts.skill import SkillStoreABC
|
|
16
|
+
from intentkit.models.agent import Agent
|
|
17
|
+
from intentkit.models.redis import get_redis
|
|
18
|
+
|
|
19
|
+
SkillState = Literal["disabled", "public", "private"]
|
|
20
|
+
SkillOwnerState = Literal["disabled", "private"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SkillConfig(TypedDict):
|
|
24
|
+
"""Abstract base class for skill configuration."""
|
|
25
|
+
|
|
26
|
+
enabled: bool
|
|
27
|
+
states: Dict[str, SkillState | SkillOwnerState]
|
|
28
|
+
api_key_provider: NotRequired[str]
|
|
29
|
+
__extra__: NotRequired[Dict[str, Any]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SkillContext(BaseModel):
|
|
33
|
+
agent: Agent
|
|
34
|
+
config: Dict[str, Any]
|
|
35
|
+
user_id: Optional[str]
|
|
36
|
+
entrypoint: Literal["web", "twitter", "telegram", "trigger", "api"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class IntentKitSkill(BaseTool):
|
|
40
|
+
"""Abstract base class for IntentKit skills.
|
|
41
|
+
Will have predefined abilities.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
skill_store: SkillStoreABC
|
|
45
|
+
# overwrite the value of BaseTool
|
|
46
|
+
handle_tool_error: Optional[Union[bool, str, Callable[[ToolException], str]]] = (
|
|
47
|
+
lambda e: f"tool error: {e}"
|
|
48
|
+
)
|
|
49
|
+
"""Handle the content of the ToolException thrown."""
|
|
50
|
+
|
|
51
|
+
# overwrite the value of BaseTool
|
|
52
|
+
handle_validation_error: Optional[
|
|
53
|
+
Union[bool, str, Callable[[Union[ValidationError, ValidationErrorV1]], str]]
|
|
54
|
+
] = lambda e: f"validation error: {e}"
|
|
55
|
+
"""Handle the content of the ValidationError thrown."""
|
|
56
|
+
|
|
57
|
+
# Logger for the class
|
|
58
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def category(self) -> str:
|
|
62
|
+
"""Get the category of the skill."""
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
async def user_rate_limit(
|
|
66
|
+
self, user_id: str, limit: int, minutes: int, key: str
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Check if a user has exceeded the rate limit for this skill.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
user_id: The ID of the user to check
|
|
72
|
+
limit: Maximum number of requests allowed
|
|
73
|
+
minutes: Time window in minutes
|
|
74
|
+
key: The key to use for rate limiting (e.g., skill name or category)
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
RateLimitExceeded: If the user has exceeded the rate limit
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
None: Always returns None if no exception is raised
|
|
81
|
+
"""
|
|
82
|
+
if not user_id:
|
|
83
|
+
return None # No rate limiting for users without ID
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
redis = get_redis()
|
|
87
|
+
# Create a unique key for this rate limit and user
|
|
88
|
+
rate_limit_key = f"rate_limit:{key}:{user_id}"
|
|
89
|
+
|
|
90
|
+
# Get the current count
|
|
91
|
+
count = await redis.incr(rate_limit_key)
|
|
92
|
+
|
|
93
|
+
# Set expiration if this is the first request
|
|
94
|
+
if count == 1:
|
|
95
|
+
await redis.expire(
|
|
96
|
+
rate_limit_key, minutes * 60
|
|
97
|
+
) # Convert minutes to seconds
|
|
98
|
+
|
|
99
|
+
# Check if user has exceeded the limit
|
|
100
|
+
if count > limit:
|
|
101
|
+
raise RateLimitExceeded(f"Rate limit exceeded for {key}")
|
|
102
|
+
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
except RuntimeError:
|
|
106
|
+
# Redis client not initialized, log and allow the request
|
|
107
|
+
self.logger.info(f"Redis not initialized, skipping rate limit for {key}")
|
|
108
|
+
return None
|
|
109
|
+
except RedisError as e:
|
|
110
|
+
# Redis error, log and allow the request
|
|
111
|
+
self.logger.info(
|
|
112
|
+
f"Redis error in rate limiting: {e}, skipping rate limit for {key}"
|
|
113
|
+
)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
async def user_rate_limit_by_skill(
|
|
117
|
+
self, user_id: str, limit: int, minutes: int
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Check if a user has exceeded the rate limit for this specific skill.
|
|
120
|
+
|
|
121
|
+
This uses the skill name as the rate limit key.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
user_id: The ID of the user to check
|
|
125
|
+
limit: Maximum number of requests allowed
|
|
126
|
+
minutes: Time window in minutes
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
RateLimitExceeded: If the user has exceeded the rate limit
|
|
130
|
+
"""
|
|
131
|
+
return await self.user_rate_limit(user_id, limit, minutes, self.name)
|
|
132
|
+
|
|
133
|
+
async def user_rate_limit_by_category(
|
|
134
|
+
self, user_id: str, limit: int, minutes: int
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Check if a user has exceeded the rate limit for this skill category.
|
|
137
|
+
|
|
138
|
+
This uses the skill category as the rate limit key, which means the limit
|
|
139
|
+
is shared across all skills in the same category.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
user_id: The ID of the user to check
|
|
143
|
+
limit: Maximum number of requests allowed
|
|
144
|
+
minutes: Time window in minutes
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
RateLimitExceeded: If the user has exceeded the rate limit
|
|
148
|
+
"""
|
|
149
|
+
return await self.user_rate_limit(user_id, limit, minutes, self.category)
|
|
150
|
+
|
|
151
|
+
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
|
152
|
+
raise NotImplementedError(
|
|
153
|
+
"Use _arun instead, IntentKit only supports synchronous skill calls"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def context_from_config(self, runner_config: RunnableConfig) -> SkillContext:
|
|
157
|
+
if "configurable" not in runner_config:
|
|
158
|
+
raise ValueError("configurable not in runner_config")
|
|
159
|
+
if "agent" not in runner_config["configurable"]:
|
|
160
|
+
raise ValueError("agent not in runner_config['configurable']")
|
|
161
|
+
agent: Agent = runner_config["configurable"].get("agent")
|
|
162
|
+
config = None
|
|
163
|
+
if agent.skills:
|
|
164
|
+
config = agent.skills.get(self.category)
|
|
165
|
+
if not config:
|
|
166
|
+
config = getattr(agent, self.category + "_config", {})
|
|
167
|
+
if not config:
|
|
168
|
+
config = {}
|
|
169
|
+
return SkillContext(
|
|
170
|
+
agent=agent,
|
|
171
|
+
config=config,
|
|
172
|
+
user_id=runner_config["configurable"].get("user_id"),
|
|
173
|
+
entrypoint=runner_config["configurable"].get("entrypoint"),
|
|
174
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# CARV API Skills: Your Gateway to Blockchain & Crypto Data
|
|
2
|
+
|
|
3
|
+
This collection of tools helps your AI agent connect to the [CARV API](https://docs.carv.io/d.a.t.a.-ai-framework/api-documentation) to get useful information about cryptocurrencies, blockchain activity, and the latest news in the space. Think of them as special abilities your agent can use!
|
|
4
|
+
|
|
5
|
+
**Icon:** 
|
|
6
|
+
**Tags:** AI, Data, Information, Analytics, Market Data
|
|
7
|
+
|
|
8
|
+
## What Can Your Agent Do With These Skills?
|
|
9
|
+
|
|
10
|
+
Here are the tools available:
|
|
11
|
+
|
|
12
|
+
### 1. Fetch News (`FetchNewsTool`)
|
|
13
|
+
|
|
14
|
+
* **What it does:** Gets the latest news articles from CARV.
|
|
15
|
+
* **What you need to provide:** Nothing! Just ask for the news.
|
|
16
|
+
* **Example Agent Interaction:** "Hey agent, what's the latest crypto news?"
|
|
17
|
+
* **What it returns:** A list of news items, each with a:
|
|
18
|
+
* `title`: The headline of the news.
|
|
19
|
+
* `url`: A link to the full article.
|
|
20
|
+
* `card_text`: A short summary.
|
|
21
|
+
* *Example output snippet:*
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"infos": [
|
|
25
|
+
{
|
|
26
|
+
"title": "Big Blockchain Conference Announced",
|
|
27
|
+
"url": "https://example.com/news/conference",
|
|
28
|
+
"card_text": "A major conference focusing on blockchain technology will be held next month..."
|
|
29
|
+
}
|
|
30
|
+
// ... more news items
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. On-Chain Query (`OnchainQueryTool`)
|
|
36
|
+
|
|
37
|
+
* **What it does:** Lets you ask questions in plain English about what's happening on blockchains like Ethereum, Base, Bitcoin, or Solana. CARV figures out how to get the answer from the blockchain data.
|
|
38
|
+
* **What you need to provide:**
|
|
39
|
+
* `question` (text): Your question about blockchain data (e.g., "What was the biggest Bitcoin transaction yesterday?").
|
|
40
|
+
* `chain` (text): The blockchain you're interested in (e.g., "ethereum", "bitcoin").
|
|
41
|
+
* **Example Agent Interaction:** "Agent, show me the top 5 most active wallets on Solana in the last week."
|
|
42
|
+
* **What it returns:** A structured table of data that answers your question. If your question involves token amounts (like ETH or BTC), the tool automatically converts them into easy-to-read numbers (e.g., "1.5 ETH" instead of a very long number).
|
|
43
|
+
* *Example output snippet (conceptual for "biggest ETH transaction last 24h"):*
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"data": {
|
|
47
|
+
"column_infos": ["transaction_hash", "from_address", "to_address", "value", "timestamp"],
|
|
48
|
+
"rows": [
|
|
49
|
+
{
|
|
50
|
+
"items": ["0xabc...", "0x123...", "0x456...", "1500.75 ETH", "2023-10-27T10:30:00Z"]
|
|
51
|
+
}
|
|
52
|
+
// ... potentially more rows if your question implies multiple results
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"query": "SELECT ... FROM ethereum.transactions ... ORDER BY value DESC LIMIT 1" // The SQL CARV generated
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
* If something goes wrong (e.g., you ask about an unsupported blockchain), it will return an error message.
|
|
59
|
+
|
|
60
|
+
### 3. Token Information and Price (`TokenInfoAndPriceTool`)
|
|
61
|
+
|
|
62
|
+
* **What it does:** Gets details about a specific cryptocurrency (like its name, symbol, what platform it's on) and its current price in USD.
|
|
63
|
+
* **What you need to provide:**
|
|
64
|
+
* `ticker` (text): The token's symbol (e.g., "BTC", "ETH", "SOL").
|
|
65
|
+
* `token_name` (text): The full name of the token (e.g., "Bitcoin", "Ethereum").
|
|
66
|
+
* `amount` (number, optional): If you want to know the value of a specific amount of the token, include this (e.g., if you provide `amount: 2.5` and `ticker: "BTC"`, it will tell you what 2.5 BTC is worth).
|
|
67
|
+
* **Example Agent Interaction:** "Agent, what's the current price of Ethereum? Also, what would 5 ETH be worth?"
|
|
68
|
+
* **What it returns:** Information about the token, including its price. If you provided an amount, it also tells you the total value.
|
|
69
|
+
* *Example output snippet (for `ticker: "ETH"`, `token_name: "Ethereum"`, `amount: 5`):*
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"name": "Ethereum",
|
|
73
|
+
"symbol": "ETH",
|
|
74
|
+
"price": 2000.50, // Current price of 1 ETH in USD
|
|
75
|
+
"platform": {"id": "ethereum", "name": "Ethereum"},
|
|
76
|
+
"categories": ["Smart Contract Platform"],
|
|
77
|
+
// ... other details
|
|
78
|
+
"additional_info": "5 ETH is worth $10002.50" // Calculated if amount was given
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
* If it can't find the token or its price, it will return an error.
|
|
82
|
+
|
|
83
|
+
## How to Get Started (For Developers)
|
|
84
|
+
|
|
85
|
+
These tools are designed to be integrated into AI agent systems.
|
|
86
|
+
|
|
87
|
+
* **Configuration:** You'll need to set up how these tools access the CARV API. This usually involves:
|
|
88
|
+
* Enabling the CARV skills.
|
|
89
|
+
* Deciding if the tools can be used by everyone or just the agent owner.
|
|
90
|
+
* Providing a CARV API key. This key can either be supplied directly in your agent's settings or managed by the platform your agent runs on.
|
|
91
|
+
* Details on how to configure this are in a `schema.json` file within the `skills/carv/` directory.
|
|
92
|
+
|
|
93
|
+
* **Using the Tools:** Your agent's code will call these tools, providing the necessary inputs (like the ticker for `TokenInfoAndPriceTool`). The tools will then contact the CARV API and return the information.
|
|
94
|
+
|
|
95
|
+
These CARV skills make it easy for your AI agent to become knowledgeable about the crypto world!
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List, Literal, Optional, TypedDict
|
|
3
|
+
|
|
4
|
+
from intentkit.abstracts.skill import SkillStoreABC
|
|
5
|
+
from intentkit.skills.base import SkillConfig, SkillState
|
|
6
|
+
from intentkit.skills.carv.base import CarvBaseTool
|
|
7
|
+
from intentkit.skills.carv.fetch_news import FetchNewsTool
|
|
8
|
+
from intentkit.skills.carv.onchain_query import OnchainQueryTool
|
|
9
|
+
from intentkit.skills.carv.token_info_and_price import TokenInfoAndPriceTool
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_cache: dict[str, CarvBaseTool] = {}
|
|
15
|
+
|
|
16
|
+
_SKILL_NAME_TO_CLASS_MAP: dict[str, type[CarvBaseTool]] = {
|
|
17
|
+
"onchain_query": OnchainQueryTool,
|
|
18
|
+
"token_info_and_price": TokenInfoAndPriceTool,
|
|
19
|
+
"fetch_news": FetchNewsTool,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SkillStates(TypedDict):
|
|
24
|
+
onchain_query: SkillState
|
|
25
|
+
token_info_and_price: SkillState
|
|
26
|
+
fetch_news: SkillState
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Config(SkillConfig):
|
|
30
|
+
enabled: bool
|
|
31
|
+
states: SkillStates # type: ignore
|
|
32
|
+
api_key_provider: Optional[Literal["agent_owner", "platform"]]
|
|
33
|
+
|
|
34
|
+
# conditionally required
|
|
35
|
+
api_key: Optional[str]
|
|
36
|
+
|
|
37
|
+
# optional
|
|
38
|
+
rate_limit_number: Optional[int]
|
|
39
|
+
rate_limit_minutes: Optional[int]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def get_skills(
|
|
43
|
+
config: "Config",
|
|
44
|
+
is_private: bool,
|
|
45
|
+
store: SkillStoreABC,
|
|
46
|
+
**_,
|
|
47
|
+
) -> list[CarvBaseTool]:
|
|
48
|
+
"""
|
|
49
|
+
Factory function to create and return CARV skill tools based on the provided configuration.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
config: The configuration object for the CARV skill.
|
|
53
|
+
is_private: A boolean indicating whether the request is from a private context.
|
|
54
|
+
store: An instance of `SkillStoreABC`.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A list of `CarvBaseTool` instances.
|
|
58
|
+
"""
|
|
59
|
+
# Check if the entire category is disabled first
|
|
60
|
+
if not config.get("enabled", False):
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
available_skills: List[CarvBaseTool] = []
|
|
64
|
+
skill_states = config.get("states", {})
|
|
65
|
+
|
|
66
|
+
# Iterate through all known skills defined in the map
|
|
67
|
+
for skill_name in _SKILL_NAME_TO_CLASS_MAP:
|
|
68
|
+
state = skill_states.get(
|
|
69
|
+
skill_name, "disabled"
|
|
70
|
+
) # Default to disabled if not in config
|
|
71
|
+
|
|
72
|
+
if state == "disabled":
|
|
73
|
+
continue
|
|
74
|
+
elif state == "public" or (state == "private" and is_private):
|
|
75
|
+
# If enabled, get the skill instance using the factory function
|
|
76
|
+
skill_instance = get_carv_skill(skill_name, store)
|
|
77
|
+
if skill_instance:
|
|
78
|
+
available_skills.append(skill_instance)
|
|
79
|
+
else:
|
|
80
|
+
logger.warning(f"Could not instantiate known skill: {skill_name}")
|
|
81
|
+
|
|
82
|
+
return available_skills
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_carv_skill(
|
|
86
|
+
name: str,
|
|
87
|
+
store: SkillStoreABC,
|
|
88
|
+
) -> Optional[CarvBaseTool]:
|
|
89
|
+
"""
|
|
90
|
+
Factory function to retrieve a cached CARV skill instance by name.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: The name of the CARV skill to retrieve.
|
|
94
|
+
store: An instance of `SkillStoreABC`.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The requested `CarvBaseTool` instance if found and enabled, otherwise None.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
# Return from cache immediately if already exists
|
|
101
|
+
if name in _cache:
|
|
102
|
+
return _cache[name]
|
|
103
|
+
|
|
104
|
+
# Get the class from the map
|
|
105
|
+
skill_class = _SKILL_NAME_TO_CLASS_MAP.get(name)
|
|
106
|
+
|
|
107
|
+
if skill_class:
|
|
108
|
+
try:
|
|
109
|
+
# Instantiate the skill and add to cache
|
|
110
|
+
instance = skill_class(skill_store=store) # type: ignore
|
|
111
|
+
_cache[name] = instance
|
|
112
|
+
return instance
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(
|
|
115
|
+
f"Failed to instantiate Carv skill '{name}': {e}", exc_info=True
|
|
116
|
+
)
|
|
117
|
+
return None # Failed to instantiate
|
|
118
|
+
else:
|
|
119
|
+
# This handles cases where a name might be in config but not in our map
|
|
120
|
+
logger.warning(f"Attempted to get unknown Carv skill: {name}")
|
|
121
|
+
return None
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, Optional, Tuple, Type
|
|
3
|
+
|
|
4
|
+
import httpx # Ensure httpx is installed: pip install httpx
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from intentkit.abstracts.skill import SkillStoreABC
|
|
8
|
+
from intentkit.skills.base import IntentKitSkill, SkillContext, ToolException
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
CARV_API_BASE_URL = "https://interface.carv.io"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CarvBaseTool(IntentKitSkill):
|
|
16
|
+
"""Base class for CARV API tools."""
|
|
17
|
+
|
|
18
|
+
name: str = Field(description="Tool name") # type: ignore
|
|
19
|
+
description: str = Field(description="Tool description")
|
|
20
|
+
args_schema: Type[BaseModel] # type: ignore
|
|
21
|
+
skill_store: SkillStoreABC = Field(description="Skill store for data persistence")
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def category(self) -> str:
|
|
25
|
+
return "carv"
|
|
26
|
+
|
|
27
|
+
def get_api_key(self, context: SkillContext) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Retrieves the CARV API key based on the api_key_provider setting.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The API key if found.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ToolException: If the API key is not found or provider is invalid.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
skillConfig = context.config
|
|
39
|
+
api_key_provider = skillConfig.get("api_key_provider")
|
|
40
|
+
if api_key_provider == "agent_owner":
|
|
41
|
+
agent_api_key: Optional[str] = context.config.get("api_key")
|
|
42
|
+
if agent_api_key:
|
|
43
|
+
logger.debug(
|
|
44
|
+
f"Using agent-specific CARV API key for skill {self.name} in category {self.category}"
|
|
45
|
+
)
|
|
46
|
+
return agent_api_key
|
|
47
|
+
raise ToolException(
|
|
48
|
+
f"No agent-owned CARV API key found for skill '{self.name}' in category '{self.category}'."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
elif api_key_provider == "platform":
|
|
52
|
+
system_api_key = self.skill_store.get_system_config("carv_api_key")
|
|
53
|
+
if system_api_key:
|
|
54
|
+
logger.debug(
|
|
55
|
+
f"Using system CARV API key for skill {self.name} in category {self.category}"
|
|
56
|
+
)
|
|
57
|
+
return system_api_key
|
|
58
|
+
raise ToolException(
|
|
59
|
+
f"No platform-hosted CARV API key found for skill '{self.name}' in category '{self.category}'."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
raise ToolException(
|
|
64
|
+
f"Invalid API key provider '{api_key_provider}' for skill '{self.name}'"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
# Re-raise ToolException if it's already one, otherwise wrap
|
|
69
|
+
if isinstance(e, ToolException):
|
|
70
|
+
raise
|
|
71
|
+
raise ToolException(f"Failed to retrieve CARV API key: {str(e)}") from e
|
|
72
|
+
|
|
73
|
+
async def apply_rate_limit(self, context: SkillContext) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Applies rate limiting ONLY if specified in the agent's config ('skill_config').
|
|
76
|
+
Checks for 'rate_limit_number' and 'rate_limit_minutes'.
|
|
77
|
+
If not configured, NO rate limiting is applied.
|
|
78
|
+
Raises ConnectionAbortedError if the configured limit is exceeded.
|
|
79
|
+
"""
|
|
80
|
+
skill_config = context.config
|
|
81
|
+
user_id = context.user_id
|
|
82
|
+
|
|
83
|
+
limit_num = skill_config.get("rate_limit_number")
|
|
84
|
+
limit_min = skill_config.get("rate_limit_minutes")
|
|
85
|
+
|
|
86
|
+
# Apply limit ONLY if both values are present and valid (truthy check handles None and 0)
|
|
87
|
+
if limit_num and limit_min:
|
|
88
|
+
logger.debug(
|
|
89
|
+
f"Applying rate limit ({limit_num}/{limit_min} min) for user {user_id} on {self.name}"
|
|
90
|
+
)
|
|
91
|
+
if user_id:
|
|
92
|
+
await self.user_rate_limit_by_category(user_id, limit_num, limit_min)
|
|
93
|
+
else:
|
|
94
|
+
# No valid agent configuration found, so do nothing.
|
|
95
|
+
logger.debug(
|
|
96
|
+
f"No agent rate limits configured for category '{self.category}'. Skipping rate limit for user {user_id}."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def _call_carv_api(
|
|
100
|
+
self,
|
|
101
|
+
context: SkillContext,
|
|
102
|
+
endpoint: str,
|
|
103
|
+
method: str = "GET",
|
|
104
|
+
params: Optional[Dict[str, Any]] = None,
|
|
105
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
106
|
+
) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
|
|
107
|
+
"""
|
|
108
|
+
Makes a call to the CARV API and returns a tuple of (success, error).
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
context: The skill context.
|
|
112
|
+
endpoint: The API endpoint path (e.g., "/ai-agent-backend/token_info").
|
|
113
|
+
method: HTTP method ("GET", "POST", etc.).
|
|
114
|
+
params: Query parameters for the request.
|
|
115
|
+
payload: JSON payload for POST/PUT requests.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Tuple where the first element is the response data if successful,
|
|
119
|
+
and the second element is an error dict if an error occurred.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
url = f"{CARV_API_BASE_URL}{endpoint}"
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
api_key = self.get_api_key(context)
|
|
126
|
+
|
|
127
|
+
headers = {
|
|
128
|
+
"Authorization": api_key,
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
logger.debug(
|
|
133
|
+
f"Calling CARV API: {method} {url} with params {params}, payload {payload}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
137
|
+
if method == "GET":
|
|
138
|
+
response = await client.get(url, headers=headers, params=params)
|
|
139
|
+
elif method == "POST":
|
|
140
|
+
response = await client.post(
|
|
141
|
+
url, headers=headers, json=payload, params=params
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
return None, {"error": f"Unsupported HTTP method: {method}"}
|
|
145
|
+
|
|
146
|
+
# Do NOT raise for status here; always parse JSON
|
|
147
|
+
try:
|
|
148
|
+
response_json: dict[str, Any] = response.json()
|
|
149
|
+
except Exception as json_err:
|
|
150
|
+
err_msg = f"Failed to parse JSON response: {json_err}"
|
|
151
|
+
logger.error(err_msg)
|
|
152
|
+
return None, {"error": err_msg}
|
|
153
|
+
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"CARV API Response (status {response.status_code}): {response_json}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Check if response_json signals an error explicitly (custom API error)
|
|
159
|
+
if response.status_code >= 400 or "error" in response_json:
|
|
160
|
+
# Return full error info (including status code, body, etc.)
|
|
161
|
+
return None, {
|
|
162
|
+
"error": response_json.get("error", "Unknown API error"),
|
|
163
|
+
"status_code": response.status_code,
|
|
164
|
+
"response": response_json,
|
|
165
|
+
"url": url,
|
|
166
|
+
"method": method,
|
|
167
|
+
"params": params,
|
|
168
|
+
"payload": payload,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Otherwise return the 'data' field if present, else full response
|
|
172
|
+
return response_json.get("data", response_json), None
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(
|
|
176
|
+
f"Error calling CARV API to {method} > {url}: {e}", exc_info=True
|
|
177
|
+
)
|
|
178
|
+
return None, {
|
|
179
|
+
"error": str(e),
|
|
180
|
+
"url": url,
|
|
181
|
+
"method": method,
|
|
182
|
+
"params": params,
|
|
183
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, Type
|
|
3
|
+
|
|
4
|
+
from langchain_core.runnables import RunnableConfig
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from intentkit.skills.carv.base import CarvBaseTool
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CarvNewsInput(BaseModel):
|
|
13
|
+
"""
|
|
14
|
+
Input schema for CARV News API.
|
|
15
|
+
This API endpoint does not require any specific parameters from the user.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FetchNewsTool(CarvBaseTool):
|
|
22
|
+
"""
|
|
23
|
+
Tool for fetching the latest news articles from the CARV API.
|
|
24
|
+
This tool retrieves a list of recent news items, each including a title, URL, and a short description (card_text).
|
|
25
|
+
It's useful for getting up-to-date information on various topics covered by CARV's news aggregation.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name: str = "carv_fetch_news"
|
|
29
|
+
description: str = (
|
|
30
|
+
"Fetches the latest news articles from the CARV API. "
|
|
31
|
+
"Returns a list of news items, each with a title, URL, and a short summary (card_text)."
|
|
32
|
+
)
|
|
33
|
+
args_schema: Type[BaseModel] = CarvNewsInput
|
|
34
|
+
|
|
35
|
+
async def _arun(
|
|
36
|
+
self,
|
|
37
|
+
config: RunnableConfig = None, # type: ignore
|
|
38
|
+
**kwargs: Any,
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Fetches news from the CARV API and returns the response.
|
|
42
|
+
The expected successful response structure is a dictionary containing an "infos" key,
|
|
43
|
+
which holds a list of news articles.
|
|
44
|
+
Example: {"infos": [{"title": "...", "url": "...", "card_text": "..."}, ...]}
|
|
45
|
+
"""
|
|
46
|
+
context = self.context_from_config(config)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
await self.apply_rate_limit(context)
|
|
50
|
+
|
|
51
|
+
result, error = await self._call_carv_api(
|
|
52
|
+
context=context,
|
|
53
|
+
endpoint="/ai-agent-backend/news",
|
|
54
|
+
method="GET",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if error is not None or result is None:
|
|
58
|
+
logger.error(f"Error returned from CARV API (News): {error}")
|
|
59
|
+
return {
|
|
60
|
+
"error": True,
|
|
61
|
+
"error_type": "APIError",
|
|
62
|
+
"message": "Failed to fetch news from CARV API.",
|
|
63
|
+
"details": error, # error is the detailed error dict from _call_carv_api
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# _call_carv_api returns response_json.get("data", response_json) on success.
|
|
67
|
+
# For this endpoint, the "data" field should be {"infos": [...]}.
|
|
68
|
+
# So, 'result' should be {"infos": [...]}.
|
|
69
|
+
if "infos" not in result or not isinstance(result.get("infos"), list):
|
|
70
|
+
logger.warning(
|
|
71
|
+
f"CARV API (News) response did not contain 'infos' list as expected: {result}"
|
|
72
|
+
)
|
|
73
|
+
return {
|
|
74
|
+
"error": True,
|
|
75
|
+
"error_type": "UnexpectedResponseFormat",
|
|
76
|
+
"message": "News data from CARV API is missing the 'infos' list or has incorrect format.",
|
|
77
|
+
"details": result,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Successfully fetched and validated news data
|
|
81
|
+
return result # This will be {"infos": [...]}
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(
|
|
85
|
+
f"An unexpected error occurred while fetching news: {e}", exc_info=True
|
|
86
|
+
)
|
|
87
|
+
return {
|
|
88
|
+
"error": True,
|
|
89
|
+
"error_type": type(e).__name__,
|
|
90
|
+
"message": "An unexpected error occurred while processing the news request.",
|
|
91
|
+
"details": str(e),
|
|
92
|
+
}
|