intentkit 0.6.9.dev2__py3-none-any.whl → 0.6.10.dev1__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.

Files changed (168) hide show
  1. intentkit/__init__.py +1 -1
  2. intentkit/abstracts/graph.py +17 -2
  3. intentkit/core/engine.py +29 -30
  4. intentkit/core/node.py +10 -20
  5. intentkit/models/agent.py +3 -0
  6. intentkit/models/chat.py +9 -1
  7. intentkit/skills/acolyt/ask.py +2 -5
  8. intentkit/skills/acolyt/base.py +16 -6
  9. intentkit/skills/aixbt/__init__.py +3 -7
  10. intentkit/skills/aixbt/projects.py +12 -36
  11. intentkit/skills/allora/base.py +16 -6
  12. intentkit/skills/allora/price.py +2 -4
  13. intentkit/skills/base.py +8 -1
  14. intentkit/skills/carv/base.py +12 -10
  15. intentkit/skills/carv/fetch_news.py +90 -92
  16. intentkit/skills/carv/onchain_query.py +162 -164
  17. intentkit/skills/carv/token_info_and_price.py +108 -110
  18. intentkit/skills/chainlist/chain_lookup.py +1 -2
  19. intentkit/skills/common/current_time.py +1 -2
  20. intentkit/skills/cookiefun/base.py +20 -12
  21. intentkit/skills/cookiefun/get_account_details.py +1 -3
  22. intentkit/skills/cookiefun/get_account_feed.py +1 -3
  23. intentkit/skills/cookiefun/get_account_smart_followers.py +1 -3
  24. intentkit/skills/cookiefun/get_sectors.py +2 -3
  25. intentkit/skills/cookiefun/search_accounts.py +1 -3
  26. intentkit/skills/cryptocompare/fetch_news.py +3 -4
  27. intentkit/skills/cryptocompare/fetch_price.py +3 -4
  28. intentkit/skills/cryptocompare/fetch_top_exchanges.py +3 -4
  29. intentkit/skills/cryptocompare/fetch_top_market_cap.py +3 -4
  30. intentkit/skills/cryptocompare/fetch_top_volume.py +3 -4
  31. intentkit/skills/cryptocompare/fetch_trading_signals.py +3 -4
  32. intentkit/skills/cryptopanic/base.py +13 -9
  33. intentkit/skills/cryptopanic/fetch_crypto_news.py +150 -153
  34. intentkit/skills/cryptopanic/fetch_crypto_sentiment.py +133 -136
  35. intentkit/skills/dapplooker/base.py +16 -6
  36. intentkit/skills/dapplooker/dapplooker_token_data.py +2 -4
  37. intentkit/skills/defillama/coins/fetch_batch_historical_prices.py +2 -3
  38. intentkit/skills/defillama/coins/fetch_block.py +2 -3
  39. intentkit/skills/defillama/coins/fetch_current_prices.py +2 -5
  40. intentkit/skills/defillama/coins/fetch_first_price.py +2 -5
  41. intentkit/skills/defillama/coins/fetch_historical_prices.py +2 -3
  42. intentkit/skills/defillama/coins/fetch_price_chart.py +2 -5
  43. intentkit/skills/defillama/coins/fetch_price_percentage.py +2 -5
  44. intentkit/skills/defillama/fees/fetch_fees_overview.py +2 -3
  45. intentkit/skills/defillama/stablecoins/fetch_stablecoin_chains.py +2 -3
  46. intentkit/skills/defillama/stablecoins/fetch_stablecoin_charts.py +2 -3
  47. intentkit/skills/defillama/stablecoins/fetch_stablecoin_prices.py +2 -3
  48. intentkit/skills/defillama/stablecoins/fetch_stablecoins.py +2 -3
  49. intentkit/skills/defillama/tvl/fetch_chain_historical_tvl.py +2 -5
  50. intentkit/skills/defillama/tvl/fetch_chains.py +2 -3
  51. intentkit/skills/defillama/tvl/fetch_historical_tvl.py +2 -3
  52. intentkit/skills/defillama/tvl/fetch_protocol.py +2 -5
  53. intentkit/skills/defillama/tvl/fetch_protocol_current_tvl.py +2 -5
  54. intentkit/skills/defillama/tvl/fetch_protocols.py +2 -3
  55. intentkit/skills/defillama/volumes/fetch_dex_overview.py +2 -3
  56. intentkit/skills/defillama/volumes/fetch_dex_summary.py +2 -5
  57. intentkit/skills/defillama/volumes/fetch_options_overview.py +2 -3
  58. intentkit/skills/defillama/yields/fetch_pool_chart.py +2 -5
  59. intentkit/skills/defillama/yields/fetch_pools.py +2 -3
  60. intentkit/skills/dune_analytics/base.py +15 -9
  61. intentkit/skills/dune_analytics/fetch_kol_buys.py +125 -128
  62. intentkit/skills/dune_analytics/fetch_nation_metrics.py +234 -237
  63. intentkit/skills/elfa/base.py +16 -6
  64. intentkit/skills/elfa/mention.py +2 -7
  65. intentkit/skills/elfa/stats.py +2 -6
  66. intentkit/skills/elfa/tokens.py +1 -4
  67. intentkit/skills/enso/base.py +25 -13
  68. intentkit/skills/enso/best_yield.py +1 -4
  69. intentkit/skills/enso/networks.py +2 -5
  70. intentkit/skills/enso/prices.py +1 -5
  71. intentkit/skills/enso/route.py +2 -5
  72. intentkit/skills/enso/tokens.py +1 -4
  73. intentkit/skills/enso/wallet.py +3 -9
  74. intentkit/skills/firecrawl/base.py +16 -6
  75. intentkit/skills/firecrawl/clear.py +1 -3
  76. intentkit/skills/firecrawl/crawl.py +7 -8
  77. intentkit/skills/firecrawl/query.py +7 -9
  78. intentkit/skills/firecrawl/scrape.py +7 -8
  79. intentkit/skills/github/github_search.py +1 -3
  80. intentkit/skills/heurist/base.py +15 -0
  81. intentkit/skills/heurist/image_generation_animagine_xl.py +3 -4
  82. intentkit/skills/heurist/image_generation_arthemy_comics.py +3 -4
  83. intentkit/skills/heurist/image_generation_arthemy_real.py +3 -4
  84. intentkit/skills/heurist/image_generation_braindance.py +3 -4
  85. intentkit/skills/heurist/image_generation_cyber_realistic_xl.py +3 -4
  86. intentkit/skills/heurist/image_generation_flux_1_dev.py +3 -4
  87. intentkit/skills/heurist/image_generation_sdxl.py +3 -4
  88. intentkit/skills/http/get.py +0 -2
  89. intentkit/skills/http/post.py +0 -2
  90. intentkit/skills/http/put.py +0 -2
  91. intentkit/skills/lifi/token_execute.py +1 -3
  92. intentkit/skills/lifi/token_quote.py +0 -2
  93. intentkit/skills/moralis/base.py +15 -1
  94. intentkit/skills/nation/nft_check.py +2 -5
  95. intentkit/skills/openai/base.py +14 -5
  96. intentkit/skills/openai/dalle_image_generation.py +6 -5
  97. intentkit/skills/openai/gpt_image_generation.py +6 -5
  98. intentkit/skills/openai/gpt_image_to_image.py +6 -5
  99. intentkit/skills/openai/image_to_text.py +6 -6
  100. intentkit/skills/portfolio/base.py +4 -3
  101. intentkit/skills/portfolio/token_balances.py +2 -4
  102. intentkit/skills/portfolio/wallet_approvals.py +2 -4
  103. intentkit/skills/portfolio/wallet_defi_positions.py +3 -4
  104. intentkit/skills/portfolio/wallet_history.py +2 -4
  105. intentkit/skills/portfolio/wallet_net_worth.py +2 -4
  106. intentkit/skills/portfolio/wallet_nfts.py +2 -4
  107. intentkit/skills/portfolio/wallet_profitability.py +2 -4
  108. intentkit/skills/portfolio/wallet_profitability_summary.py +2 -4
  109. intentkit/skills/portfolio/wallet_stats.py +2 -4
  110. intentkit/skills/portfolio/wallet_swaps.py +2 -4
  111. intentkit/skills/slack/base.py +18 -0
  112. intentkit/skills/slack/get_channel.py +3 -4
  113. intentkit/skills/slack/get_message.py +3 -4
  114. intentkit/skills/slack/schedule_message.py +3 -4
  115. intentkit/skills/slack/send_message.py +3 -4
  116. intentkit/skills/supabase/delete_data.py +3 -6
  117. intentkit/skills/supabase/fetch_data.py +3 -6
  118. intentkit/skills/supabase/insert_data.py +3 -6
  119. intentkit/skills/supabase/invoke_function.py +3 -6
  120. intentkit/skills/supabase/update_data.py +3 -6
  121. intentkit/skills/supabase/upsert_data.py +3 -6
  122. intentkit/skills/system/add_autonomous_task.py +1 -3
  123. intentkit/skills/system/delete_autonomous_task.py +1 -3
  124. intentkit/skills/system/edit_autonomous_task.py +1 -3
  125. intentkit/skills/system/list_autonomous_tasks.py +1 -3
  126. intentkit/skills/system/read_agent_api_key.py +2 -3
  127. intentkit/skills/system/regenerate_agent_api_key.py +2 -5
  128. intentkit/skills/tavily/base.py +14 -5
  129. intentkit/skills/tavily/tavily_extract.py +7 -8
  130. intentkit/skills/tavily/tavily_search.py +11 -9
  131. intentkit/skills/token/base.py +4 -6
  132. intentkit/skills/token/erc20_transfers.py +2 -4
  133. intentkit/skills/token/token_analytics.py +2 -4
  134. intentkit/skills/token/token_price.py +2 -4
  135. intentkit/skills/token/token_search.py +2 -4
  136. intentkit/skills/twitter/base.py +41 -0
  137. intentkit/skills/twitter/follow_user.py +4 -4
  138. intentkit/skills/twitter/get_mentions.py +4 -4
  139. intentkit/skills/twitter/get_timeline.py +4 -4
  140. intentkit/skills/twitter/get_user_by_username.py +4 -4
  141. intentkit/skills/twitter/get_user_tweets.py +4 -4
  142. intentkit/skills/twitter/like_tweet.py +4 -4
  143. intentkit/skills/twitter/post_tweet.py +3 -4
  144. intentkit/skills/twitter/reply_tweet.py +3 -4
  145. intentkit/skills/twitter/retweet.py +4 -4
  146. intentkit/skills/twitter/search_tweets.py +4 -4
  147. intentkit/skills/unrealspeech/base.py +16 -0
  148. intentkit/skills/unrealspeech/text_to_speech.py +4 -4
  149. intentkit/skills/venice_audio/base.py +11 -9
  150. intentkit/skills/venice_audio/venice_audio.py +238 -240
  151. intentkit/skills/venice_image/base.py +23 -19
  152. intentkit/skills/venice_image/image_enhance/image_enhance.py +78 -80
  153. intentkit/skills/venice_image/image_generation/image_generation_base.py +115 -117
  154. intentkit/skills/venice_image/image_upscale/image_upscale.py +88 -90
  155. intentkit/skills/venice_image/image_vision/image_vision.py +98 -100
  156. intentkit/skills/web_scraper/document_indexer.py +3 -5
  157. intentkit/skills/web_scraper/scrape_and_index.py +14 -17
  158. intentkit/skills/web_scraper/website_indexer.py +8 -10
  159. intentkit/skills/xmtp/README.md +110 -0
  160. intentkit/skills/xmtp/__init__.py +82 -0
  161. intentkit/skills/xmtp/base.py +13 -0
  162. intentkit/skills/xmtp/schema.json +41 -0
  163. intentkit/skills/xmtp/transfer.py +170 -0
  164. intentkit/skills/xmtp/xmtp.svg +26 -0
  165. {intentkit-0.6.9.dev2.dist-info → intentkit-0.6.10.dev1.dist-info}/METADATA +3 -3
  166. {intentkit-0.6.9.dev2.dist-info → intentkit-0.6.10.dev1.dist-info}/RECORD +168 -162
  167. {intentkit-0.6.9.dev2.dist-info → intentkit-0.6.10.dev1.dist-info}/WHEEL +0 -0
  168. {intentkit-0.6.9.dev2.dist-info → intentkit-0.6.10.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -1,237 +1,234 @@
1
- """Skill to fetch Crestal Nation metrics from Dune Analytics API.
2
-
3
- Supports predefined metrics (e.g., total_users, unique_ai_citizens) or direct query IDs.
4
- """
5
-
6
- import difflib
7
- import re
8
- from typing import Any, Dict, Type
9
-
10
- import httpx
11
- from langchain_core.runnables import RunnableConfig
12
- from pydantic import BaseModel, Field
13
- from tenacity import retry, stop_after_attempt, wait_exponential
14
-
15
- from intentkit.abstracts.skill import SkillStoreABC
16
- from intentkit.skills.dune_analytics.base import DuneBaseTool
17
-
18
- SUPPORTED_QUERIES = {
19
- "total_users": 4858003,
20
- "weekly_active_users": 4867200,
21
- "unique_ai_citizens": 4857629,
22
- "unique_creators": 4844506,
23
- "ai_citizens_over_time": 4857629,
24
- "chat_messages_over_time": 4857870,
25
- "onchain_transactions": 4859895,
26
- "total_chat_messages": 4857870,
27
- "daily_skill_executions": 4861785,
28
- "goods_services": 4859895,
29
- "agent_tvl": 4859887,
30
- "citizen_market_cap": 4859887,
31
- }
32
- QUERY_ALIASES = {
33
- "agents": "unique_ai_citizens",
34
- "citizens": "unique_ai_citizens",
35
- "market_cap": "citizen_market_cap",
36
- "nation_market_cap": "citizen_market_cap",
37
- "number_of_agents": "unique_ai_citizens",
38
- "number_of_citizens": "unique_ai_citizens",
39
- "ai_citizens": "unique_ai_citizens",
40
- "users": "total_users",
41
- "active_users": "weekly_active_users",
42
- "creators": "unique_creators",
43
- "transactions": "onchain_transactions",
44
- "messages": "total_chat_messages",
45
- "skill_executions": "daily_skill_executions",
46
- "tvl": "agent_tvl",
47
- }
48
- BASE_URL = "https://api.dune.com/api/v1/query"
49
-
50
-
51
- class NationMetricsInput(BaseModel):
52
- """Input schema for fetching Crestal Nation metrics."""
53
-
54
- metric: str = Field(
55
- default="",
56
- description="Metric name (e.g., total_users, agents) or query ID (e.g., 4858003). Empty for all configured metrics.",
57
- )
58
- limit: int = Field(
59
- default=1000, description="Maximum number of results to fetch (default 1000)."
60
- )
61
-
62
-
63
- class MetricData(BaseModel):
64
- """Data model for a single metric result."""
65
-
66
- metric: str = Field(description="Metric name or query ID")
67
- data: Dict[str, Any] = Field(description="Metric data from Dune API")
68
- error: str = Field(default="", description="Error message if fetch failed")
69
-
70
-
71
- class NationMetricsOutput(BaseModel):
72
- """Output schema for Crestal Nation metrics."""
73
-
74
- metrics: Dict[str, MetricData] = Field(
75
- description="Dictionary of metric names or query IDs to their data"
76
- )
77
- summary: str = Field(description="Summary of fetched metrics")
78
-
79
-
80
- class FetchNationMetrics(DuneBaseTool):
81
- """Skill to fetch Crestal Nation metrics from Dune Analytics API."""
82
-
83
- name: str = "dune_fetch_nation_metrics"
84
- description: str = (
85
- "Fetches Crestal Nation metrics (e.g., total_users, agents/citizens, market_cap) from Dune Analytics API. "
86
- "Supports predefined metrics, direct query IDs, or all configured metrics if none specified. "
87
- "Handles rate limits with retries."
88
- )
89
- args_schema: Type[BaseModel] = NationMetricsInput
90
- skill_store: SkillStoreABC = Field(description="Skill store for data persistence")
91
-
92
- def normalize_metric(self, metric: str) -> str:
93
- """Normalize a metric string for matching.
94
-
95
- Args:
96
- metric: Raw metric string from input.
97
-
98
- Returns:
99
- Normalized metric string (lowercase, underscores, no punctuation).
100
- """
101
- if not metric:
102
- return ""
103
- metric = re.sub(r"[^\w\s]", "", metric.lower()).replace(" ", "_")
104
- return re.sub(r"_+", "_", metric).strip("_")
105
-
106
- def find_closest_metrics(self, metric: str, max_suggestions: int = 3) -> list[str]:
107
- """Find the closest matching metrics using fuzzy matching.
108
-
109
- Args:
110
- metric: Input metric to match against.
111
- max_suggestions: Maximum number of suggestions to return.
112
-
113
- Returns:
114
- List of closest metric names.
115
- """
116
- all_metrics = list(SUPPORTED_QUERIES.keys()) + list(QUERY_ALIASES.keys())
117
- if not metric or not all_metrics:
118
- return []
119
- return difflib.get_close_matches(
120
- metric, all_metrics, n=max_suggestions, cutoff=0.6
121
- )
122
-
123
- @retry(
124
- stop=stop_after_attempt(3), wait=wait_exponential(multiplier=5, min=5, max=60)
125
- )
126
- async def fetch_data(
127
- self, query_id: int, api_key: str, limit: int = 1000
128
- ) -> Dict[str, Any]:
129
- """Fetch data for a specific Dune query.
130
-
131
- Args:
132
- query_id: Dune query ID.
133
- api_key: Dune API key.
134
- limit: Maximum number of results (default 1000).
135
-
136
- Returns:
137
- Dictionary of query results.
138
-
139
- Raises:
140
- ToolException: If the API request fails.
141
- """
142
- from langchain.tools.base import ToolException
143
-
144
- url = f"{BASE_URL}/{query_id}/results?limit={limit}"
145
- headers = {"X-Dune-API-Key": api_key}
146
-
147
- async with httpx.AsyncClient() as client:
148
- try:
149
- response = await client.get(url, headers=headers, timeout=10)
150
- response.raise_for_status()
151
- return response.json().get("result", {})
152
- except (httpx.RequestError, httpx.HTTPStatusError) as e:
153
- raise ToolException(f"Error fetching data from Dune API: {e}")
154
-
155
- async def _arun(
156
- self,
157
- metric: str = "",
158
- limit: int = 1000,
159
- config: RunnableConfig = None,
160
- **kwargs,
161
- ) -> NationMetricsOutput:
162
- """Fetch Crestal Nation metrics asynchronously.
163
-
164
- Args:
165
- metric: Metric name (e.g., total_users) or query ID (e.g., 4858003). Empty for all configured metrics.
166
- limit: Maximum number of results (default 1000).
167
- config: Runnable configuration.
168
- **kwargs: Additional keyword arguments.
169
-
170
- Returns:
171
- NationMetricsOutput with metric data and summary.
172
- """
173
- import logging
174
-
175
- logger = logging.getLogger(__name__)
176
- context = self.context_from_config(config)
177
- api_key = self.get_api_key(context)
178
-
179
- metric = self.normalize_metric(metric)
180
- metric = QUERY_ALIASES.get(metric, metric)
181
-
182
- results = {}
183
- metrics_to_fetch = {}
184
-
185
- try:
186
- query_id = int(metric)
187
- metrics_to_fetch[str(query_id)] = query_id
188
- except (ValueError, TypeError):
189
- metrics_to_fetch = (
190
- SUPPORTED_QUERIES
191
- if not metric
192
- else (
193
- {metric: SUPPORTED_QUERIES[metric]}
194
- if metric in SUPPORTED_QUERIES
195
- else {}
196
- )
197
- )
198
-
199
- if not metrics_to_fetch:
200
- closest_metrics = self.find_closest_metrics(metric)
201
- supported = ", ".join(SUPPORTED_QUERIES.keys())
202
- suggestions = (
203
- f" Did you mean: {', '.join(closest_metrics)}?"
204
- if closest_metrics
205
- else ""
206
- )
207
- logger.warning(
208
- "Unrecognized metric or query ID: %s. Suggested: %s",
209
- metric,
210
- closest_metrics,
211
- )
212
- return NationMetricsOutput(
213
- metrics={},
214
- summary=(
215
- f"Invalid metric or query ID: {metric}. Supported metrics include: {supported}.{suggestions} "
216
- "Try 'fetch nation metrics total_users' or a valid query ID, or submit a feature request at "
217
- "https://github.com/crestalnetwork/intentkit."
218
- ),
219
- )
220
-
221
- for metric_name, query_id in metrics_to_fetch.items():
222
- try:
223
- data = await self.fetch_data(query_id, api_key, limit)
224
- results[metric_name] = MetricData(metric=metric_name, data=data)
225
- except Exception as e:
226
- results[metric_name] = MetricData(
227
- metric=metric_name, data={}, error=str(e)
228
- )
229
-
230
- summary = f"Fetched data for {len([m for m in results.values() if not m.error])}/{len(metrics_to_fetch)} metrics."
231
- if any(m.error for m in results.values()):
232
- summary += f" Errors occurred for: {', '.join(m.metric for m in results.values() if m.error)}."
233
-
234
- return NationMetricsOutput(metrics=results, summary=summary)
235
-
236
- def _run(self, question: str):
237
- raise NotImplementedError("Use _arun for async execution")
1
+ """Skill to fetch Crestal Nation metrics from Dune Analytics API.
2
+
3
+ Supports predefined metrics (e.g., total_users, unique_ai_citizens) or direct query IDs.
4
+ """
5
+
6
+ import difflib
7
+ import re
8
+ from typing import Any, Dict, Type
9
+
10
+ import httpx
11
+ from pydantic import BaseModel, Field
12
+ from tenacity import retry, stop_after_attempt, wait_exponential
13
+
14
+ from intentkit.abstracts.skill import SkillStoreABC
15
+ from intentkit.skills.dune_analytics.base import DuneBaseTool
16
+
17
+ SUPPORTED_QUERIES = {
18
+ "total_users": 4858003,
19
+ "weekly_active_users": 4867200,
20
+ "unique_ai_citizens": 4857629,
21
+ "unique_creators": 4844506,
22
+ "ai_citizens_over_time": 4857629,
23
+ "chat_messages_over_time": 4857870,
24
+ "onchain_transactions": 4859895,
25
+ "total_chat_messages": 4857870,
26
+ "daily_skill_executions": 4861785,
27
+ "goods_services": 4859895,
28
+ "agent_tvl": 4859887,
29
+ "citizen_market_cap": 4859887,
30
+ }
31
+ QUERY_ALIASES = {
32
+ "agents": "unique_ai_citizens",
33
+ "citizens": "unique_ai_citizens",
34
+ "market_cap": "citizen_market_cap",
35
+ "nation_market_cap": "citizen_market_cap",
36
+ "number_of_agents": "unique_ai_citizens",
37
+ "number_of_citizens": "unique_ai_citizens",
38
+ "ai_citizens": "unique_ai_citizens",
39
+ "users": "total_users",
40
+ "active_users": "weekly_active_users",
41
+ "creators": "unique_creators",
42
+ "transactions": "onchain_transactions",
43
+ "messages": "total_chat_messages",
44
+ "skill_executions": "daily_skill_executions",
45
+ "tvl": "agent_tvl",
46
+ }
47
+ BASE_URL = "https://api.dune.com/api/v1/query"
48
+
49
+
50
+ class NationMetricsInput(BaseModel):
51
+ """Input schema for fetching Crestal Nation metrics."""
52
+
53
+ metric: str = Field(
54
+ default="",
55
+ description="Metric name (e.g., total_users, agents) or query ID (e.g., 4858003). Empty for all configured metrics.",
56
+ )
57
+ limit: int = Field(
58
+ default=1000, description="Maximum number of results to fetch (default 1000)."
59
+ )
60
+
61
+
62
+ class MetricData(BaseModel):
63
+ """Data model for a single metric result."""
64
+
65
+ metric: str = Field(description="Metric name or query ID")
66
+ data: Dict[str, Any] = Field(description="Metric data from Dune API")
67
+ error: str = Field(default="", description="Error message if fetch failed")
68
+
69
+
70
+ class NationMetricsOutput(BaseModel):
71
+ """Output schema for Crestal Nation metrics."""
72
+
73
+ metrics: Dict[str, MetricData] = Field(
74
+ description="Dictionary of metric names or query IDs to their data"
75
+ )
76
+ summary: str = Field(description="Summary of fetched metrics")
77
+
78
+
79
+ class FetchNationMetrics(DuneBaseTool):
80
+ """Skill to fetch Crestal Nation metrics from Dune Analytics API."""
81
+
82
+ name: str = "dune_fetch_nation_metrics"
83
+ description: str = (
84
+ "Fetches Crestal Nation metrics (e.g., total_users, agents/citizens, market_cap) from Dune Analytics API. "
85
+ "Supports predefined metrics, direct query IDs, or all configured metrics if none specified. "
86
+ "Handles rate limits with retries."
87
+ )
88
+ args_schema: Type[BaseModel] = NationMetricsInput
89
+ skill_store: SkillStoreABC = Field(description="Skill store for data persistence")
90
+
91
+ def normalize_metric(self, metric: str) -> str:
92
+ """Normalize a metric string for matching.
93
+
94
+ Args:
95
+ metric: Raw metric string from input.
96
+
97
+ Returns:
98
+ Normalized metric string (lowercase, underscores, no punctuation).
99
+ """
100
+ if not metric:
101
+ return ""
102
+ metric = re.sub(r"[^\w\s]", "", metric.lower()).replace(" ", "_")
103
+ return re.sub(r"_+", "_", metric).strip("_")
104
+
105
+ def find_closest_metrics(self, metric: str, max_suggestions: int = 3) -> list[str]:
106
+ """Find the closest matching metrics using fuzzy matching.
107
+
108
+ Args:
109
+ metric: Input metric to match against.
110
+ max_suggestions: Maximum number of suggestions to return.
111
+
112
+ Returns:
113
+ List of closest metric names.
114
+ """
115
+ all_metrics = list(SUPPORTED_QUERIES.keys()) + list(QUERY_ALIASES.keys())
116
+ if not metric or not all_metrics:
117
+ return []
118
+ return difflib.get_close_matches(
119
+ metric, all_metrics, n=max_suggestions, cutoff=0.6
120
+ )
121
+
122
+ @retry(
123
+ stop=stop_after_attempt(3), wait=wait_exponential(multiplier=5, min=5, max=60)
124
+ )
125
+ async def fetch_data(
126
+ self, query_id: int, api_key: str, limit: int = 1000
127
+ ) -> Dict[str, Any]:
128
+ """Fetch data for a specific Dune query.
129
+
130
+ Args:
131
+ query_id: Dune query ID.
132
+ api_key: Dune API key.
133
+ limit: Maximum number of results (default 1000).
134
+
135
+ Returns:
136
+ Dictionary of query results.
137
+
138
+ Raises:
139
+ ToolException: If the API request fails.
140
+ """
141
+ from langchain.tools.base import ToolException
142
+
143
+ url = f"{BASE_URL}/{query_id}/results?limit={limit}"
144
+ headers = {"X-Dune-API-Key": api_key}
145
+
146
+ async with httpx.AsyncClient() as client:
147
+ try:
148
+ response = await client.get(url, headers=headers, timeout=10)
149
+ response.raise_for_status()
150
+ return response.json().get("result", {})
151
+ except (httpx.RequestError, httpx.HTTPStatusError) as e:
152
+ raise ToolException(f"Error fetching data from Dune API: {e}")
153
+
154
+ async def _arun(
155
+ self,
156
+ metric: str = "",
157
+ limit: int = 1000,
158
+ **kwargs,
159
+ ) -> NationMetricsOutput:
160
+ """Fetch Crestal Nation metrics asynchronously.
161
+
162
+ Args:
163
+ metric: Metric name (e.g., total_users) or query ID (e.g., 4858003). Empty for all configured metrics.
164
+ limit: Maximum number of results (default 1000).
165
+ config: Runnable configuration.
166
+ **kwargs: Additional keyword arguments.
167
+
168
+ Returns:
169
+ NationMetricsOutput with metric data and summary.
170
+ """
171
+ import logging
172
+
173
+ logger = logging.getLogger(__name__)
174
+ api_key = self.get_api_key()
175
+
176
+ metric = self.normalize_metric(metric)
177
+ metric = QUERY_ALIASES.get(metric, metric)
178
+
179
+ results = {}
180
+ metrics_to_fetch = {}
181
+
182
+ try:
183
+ query_id = int(metric)
184
+ metrics_to_fetch[str(query_id)] = query_id
185
+ except (ValueError, TypeError):
186
+ metrics_to_fetch = (
187
+ SUPPORTED_QUERIES
188
+ if not metric
189
+ else (
190
+ {metric: SUPPORTED_QUERIES[metric]}
191
+ if metric in SUPPORTED_QUERIES
192
+ else {}
193
+ )
194
+ )
195
+
196
+ if not metrics_to_fetch:
197
+ closest_metrics = self.find_closest_metrics(metric)
198
+ supported = ", ".join(SUPPORTED_QUERIES.keys())
199
+ suggestions = (
200
+ f" Did you mean: {', '.join(closest_metrics)}?"
201
+ if closest_metrics
202
+ else ""
203
+ )
204
+ logger.warning(
205
+ "Unrecognized metric or query ID: %s. Suggested: %s",
206
+ metric,
207
+ closest_metrics,
208
+ )
209
+ return NationMetricsOutput(
210
+ metrics={},
211
+ summary=(
212
+ f"Invalid metric or query ID: {metric}. Supported metrics include: {supported}.{suggestions} "
213
+ "Try 'fetch nation metrics total_users' or a valid query ID, or submit a feature request at "
214
+ "https://github.com/crestalnetwork/intentkit."
215
+ ),
216
+ )
217
+
218
+ for metric_name, query_id in metrics_to_fetch.items():
219
+ try:
220
+ data = await self.fetch_data(query_id, api_key, limit)
221
+ results[metric_name] = MetricData(metric=metric_name, data=data)
222
+ except Exception as e:
223
+ results[metric_name] = MetricData(
224
+ metric=metric_name, data={}, error=str(e)
225
+ )
226
+
227
+ summary = f"Fetched data for {len([m for m in results.values() if not m.error])}/{len(metrics_to_fetch)} metrics."
228
+ if any(m.error for m in results.values()):
229
+ summary += f" Errors occurred for: {', '.join(m.metric for m in results.values() if m.error)}."
230
+
231
+ return NationMetricsOutput(metrics=results, summary=summary)
232
+
233
+ def _run(self, question: str):
234
+ raise NotImplementedError("Use _arun for async execution")
@@ -1,9 +1,10 @@
1
- from typing import Optional, Type
1
+ from typing import Type
2
2
 
3
+ from langchain.tools.base import ToolException
3
4
  from pydantic import BaseModel, Field
4
5
 
5
6
  from intentkit.abstracts.skill import SkillStoreABC
6
- from intentkit.skills.base import IntentKitSkill, SkillContext
7
+ from intentkit.skills.base import IntentKitSkill
7
8
 
8
9
  base_url = "https://api.elfa.ai/v2"
9
10
 
@@ -18,10 +19,19 @@ class ElfaBaseTool(IntentKitSkill):
18
19
  description="The skill store for persisting data"
19
20
  )
20
21
 
21
- def get_api_key(self, context: SkillContext) -> Optional[str]:
22
- if "api_key" in context.config and context.config["api_key"]:
23
- return context.config["api_key"]
24
- return self.skill_store.get_system_config("elfa_api_key")
22
+ def get_api_key(self) -> str:
23
+ context = self.get_context()
24
+ skill_config = context.agent.skill_config(self.category)
25
+ api_key_provider = skill_config.get("api_key_provider")
26
+ if api_key_provider == "platform":
27
+ return self.skill_store.get_system_config("elfa_api_key")
28
+ # for backward compatibility, may only have api_key in skill_config
29
+ elif skill_config.get("api_key"):
30
+ return skill_config.get("api_key")
31
+ else:
32
+ raise ToolException(
33
+ f"Invalid API key provider: {api_key_provider}, or no api_key in config"
34
+ )
25
35
 
26
36
  @property
27
37
  def category(self) -> str:
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import Any, Dict, List, Optional, Type
4
4
 
5
- from langchain_core.runnables import RunnableConfig
6
5
  from pydantic import BaseModel, Field
7
6
 
8
7
  from .base import ElfaBaseTool
@@ -55,7 +54,6 @@ class ElfaGetTopMentions(ElfaBaseTool):
55
54
  timeWindow: str = "1h",
56
55
  page: int = 1,
57
56
  pageSize: int = 10,
58
- config: RunnableConfig = None,
59
57
  **kwargs,
60
58
  ) -> ElfaGetTopMentionsOutput:
61
59
  """
@@ -76,8 +74,7 @@ class ElfaGetTopMentions(ElfaBaseTool):
76
74
  ValueError: If API key is not found
77
75
  ToolException: If there's an error with the API request
78
76
  """
79
- context = self.context_from_config(config)
80
- api_key = self.get_api_key(context)
77
+ api_key = self.get_api_key()
81
78
 
82
79
  # Prepare parameters according to API spec
83
80
  params = {
@@ -163,7 +160,6 @@ class ElfaSearchMentions(ElfaBaseTool):
163
160
  limit: int = 20,
164
161
  searchType: str = "or",
165
162
  cursor: Optional[str] = None,
166
- config: RunnableConfig = None,
167
163
  **kwargs,
168
164
  ) -> ElfaSearchMentionsOutput:
169
165
  """
@@ -186,8 +182,7 @@ class ElfaSearchMentions(ElfaBaseTool):
186
182
  ValueError: If API key is not found or neither keywords nor accountName provided
187
183
  ToolException: If there's an error with the API request
188
184
  """
189
- context = self.context_from_config(config)
190
- api_key = self.get_api_key(context)
185
+ api_key = self.get_api_key()
191
186
 
192
187
  # Validate that at least one search criteria is provided
193
188
  if not keywords and not accountName:
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import Any, Dict, Optional, Type
4
4
 
5
- from langchain_core.runnables import RunnableConfig
6
5
  from pydantic import BaseModel, Field
7
6
 
8
7
  from .base import ElfaBaseTool
@@ -46,9 +45,7 @@ class ElfaGetSmartStats(ElfaBaseTool):
46
45
  and social media performance audits."""
47
46
  args_schema: Type[BaseModel] = ElfaGetSmartStatsInput
48
47
 
49
- async def _arun(
50
- self, username: str, config: RunnableConfig = None, **kwargs
51
- ) -> ElfaGetSmartStatsOutput:
48
+ async def _arun(self, username: str, **kwargs) -> ElfaGetSmartStatsOutput:
52
49
  """
53
50
  Execute the smart stats request.
54
51
 
@@ -64,8 +61,7 @@ class ElfaGetSmartStats(ElfaBaseTool):
64
61
  ValueError: If API key is not found
65
62
  ToolException: If there's an error with the API request
66
63
  """
67
- context = self.context_from_config(config)
68
- api_key = self.get_api_key(context)
64
+ api_key = self.get_api_key()
69
65
 
70
66
  # Prepare parameters according to API spec
71
67
  params = {"username": username}
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import Any, Dict, List, Optional, Type
4
4
 
5
- from langchain_core.runnables import RunnableConfig
6
5
  from pydantic import BaseModel, Field
7
6
 
8
7
  from .base import ElfaBaseTool
@@ -69,7 +68,6 @@ class ElfaGetTrendingTokens(ElfaBaseTool):
69
68
  page: int = 1,
70
69
  pageSize: int = 50,
71
70
  minMentions: int = 5,
72
- config: RunnableConfig = None,
73
71
  **kwargs,
74
72
  ) -> ElfaGetTrendingTokensOutput:
75
73
  """
@@ -90,8 +88,7 @@ class ElfaGetTrendingTokens(ElfaBaseTool):
90
88
  ValueError: If API key is not found
91
89
  ToolException: If there's an error with the API request
92
90
  """
93
- context = self.context_from_config(config)
94
- api_key = self.get_api_key(context)
91
+ api_key = self.get_api_key()
95
92
 
96
93
  # Prepare parameters according to API spec
97
94
  params = {