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.

Files changed (366) hide show
  1. intentkit/__init__.py +17 -0
  2. intentkit/abstracts/__init__.py +0 -0
  3. intentkit/abstracts/agent.py +60 -0
  4. intentkit/abstracts/api.py +4 -0
  5. intentkit/abstracts/engine.py +38 -0
  6. intentkit/abstracts/exception.py +9 -0
  7. intentkit/abstracts/graph.py +25 -0
  8. intentkit/abstracts/skill.py +129 -0
  9. intentkit/abstracts/twitter.py +54 -0
  10. intentkit/clients/__init__.py +14 -0
  11. intentkit/clients/cdp.py +53 -0
  12. intentkit/clients/twitter.py +445 -0
  13. intentkit/config/__init__.py +0 -0
  14. intentkit/config/config.py +164 -0
  15. intentkit/core/__init__.py +0 -0
  16. intentkit/core/agent.py +191 -0
  17. intentkit/core/api.py +40 -0
  18. intentkit/core/client.py +45 -0
  19. intentkit/core/credit.py +1767 -0
  20. intentkit/core/engine.py +1018 -0
  21. intentkit/core/node.py +223 -0
  22. intentkit/core/prompt.py +58 -0
  23. intentkit/core/skill.py +124 -0
  24. intentkit/models/agent.py +1689 -0
  25. intentkit/models/agent_data.py +810 -0
  26. intentkit/models/agent_schema.json +733 -0
  27. intentkit/models/app_setting.py +156 -0
  28. intentkit/models/base.py +9 -0
  29. intentkit/models/chat.py +581 -0
  30. intentkit/models/conversation.py +286 -0
  31. intentkit/models/credit.py +1406 -0
  32. intentkit/models/db.py +120 -0
  33. intentkit/models/db_mig.py +102 -0
  34. intentkit/models/generator.py +347 -0
  35. intentkit/models/llm.py +746 -0
  36. intentkit/models/redis.py +132 -0
  37. intentkit/models/skill.py +466 -0
  38. intentkit/models/user.py +243 -0
  39. intentkit/skills/__init__.py +12 -0
  40. intentkit/skills/acolyt/__init__.py +83 -0
  41. intentkit/skills/acolyt/acolyt.jpg +0 -0
  42. intentkit/skills/acolyt/ask.py +128 -0
  43. intentkit/skills/acolyt/base.py +28 -0
  44. intentkit/skills/acolyt/schema.json +89 -0
  45. intentkit/skills/aixbt/README.md +71 -0
  46. intentkit/skills/aixbt/__init__.py +73 -0
  47. intentkit/skills/aixbt/aixbt.jpg +0 -0
  48. intentkit/skills/aixbt/base.py +21 -0
  49. intentkit/skills/aixbt/projects.py +153 -0
  50. intentkit/skills/aixbt/schema.json +99 -0
  51. intentkit/skills/allora/__init__.py +83 -0
  52. intentkit/skills/allora/allora.jpeg +0 -0
  53. intentkit/skills/allora/base.py +28 -0
  54. intentkit/skills/allora/price.py +130 -0
  55. intentkit/skills/allora/schema.json +89 -0
  56. intentkit/skills/base.py +174 -0
  57. intentkit/skills/carv/README.md +95 -0
  58. intentkit/skills/carv/__init__.py +121 -0
  59. intentkit/skills/carv/base.py +183 -0
  60. intentkit/skills/carv/carv.webp +0 -0
  61. intentkit/skills/carv/fetch_news.py +92 -0
  62. intentkit/skills/carv/onchain_query.py +164 -0
  63. intentkit/skills/carv/schema.json +137 -0
  64. intentkit/skills/carv/token_info_and_price.py +110 -0
  65. intentkit/skills/cdp/__init__.py +137 -0
  66. intentkit/skills/cdp/base.py +21 -0
  67. intentkit/skills/cdp/cdp.png +0 -0
  68. intentkit/skills/cdp/get_balance.py +81 -0
  69. intentkit/skills/cdp/schema.json +473 -0
  70. intentkit/skills/chainlist/README.md +38 -0
  71. intentkit/skills/chainlist/__init__.py +54 -0
  72. intentkit/skills/chainlist/base.py +21 -0
  73. intentkit/skills/chainlist/chain_lookup.py +208 -0
  74. intentkit/skills/chainlist/chainlist.png +0 -0
  75. intentkit/skills/chainlist/schema.json +47 -0
  76. intentkit/skills/common/__init__.py +82 -0
  77. intentkit/skills/common/base.py +21 -0
  78. intentkit/skills/common/common.jpg +0 -0
  79. intentkit/skills/common/current_time.py +84 -0
  80. intentkit/skills/common/schema.json +57 -0
  81. intentkit/skills/cookiefun/README.md +121 -0
  82. intentkit/skills/cookiefun/__init__.py +78 -0
  83. intentkit/skills/cookiefun/base.py +41 -0
  84. intentkit/skills/cookiefun/constants.py +18 -0
  85. intentkit/skills/cookiefun/cookiefun.png +0 -0
  86. intentkit/skills/cookiefun/get_account_details.py +171 -0
  87. intentkit/skills/cookiefun/get_account_feed.py +282 -0
  88. intentkit/skills/cookiefun/get_account_smart_followers.py +181 -0
  89. intentkit/skills/cookiefun/get_sectors.py +128 -0
  90. intentkit/skills/cookiefun/schema.json +155 -0
  91. intentkit/skills/cookiefun/search_accounts.py +225 -0
  92. intentkit/skills/cryptocompare/__init__.py +130 -0
  93. intentkit/skills/cryptocompare/api.py +159 -0
  94. intentkit/skills/cryptocompare/base.py +303 -0
  95. intentkit/skills/cryptocompare/cryptocompare.png +0 -0
  96. intentkit/skills/cryptocompare/fetch_news.py +96 -0
  97. intentkit/skills/cryptocompare/fetch_price.py +99 -0
  98. intentkit/skills/cryptocompare/fetch_top_exchanges.py +113 -0
  99. intentkit/skills/cryptocompare/fetch_top_market_cap.py +109 -0
  100. intentkit/skills/cryptocompare/fetch_top_volume.py +108 -0
  101. intentkit/skills/cryptocompare/fetch_trading_signals.py +107 -0
  102. intentkit/skills/cryptocompare/schema.json +168 -0
  103. intentkit/skills/cryptopanic/__init__.py +108 -0
  104. intentkit/skills/cryptopanic/base.py +51 -0
  105. intentkit/skills/cryptopanic/cryptopanic.png +0 -0
  106. intentkit/skills/cryptopanic/fetch_crypto_news.py +153 -0
  107. intentkit/skills/cryptopanic/fetch_crypto_sentiment.py +136 -0
  108. intentkit/skills/cryptopanic/schema.json +103 -0
  109. intentkit/skills/dapplooker/README.md +92 -0
  110. intentkit/skills/dapplooker/__init__.py +83 -0
  111. intentkit/skills/dapplooker/base.py +26 -0
  112. intentkit/skills/dapplooker/dapplooker.jpg +0 -0
  113. intentkit/skills/dapplooker/dapplooker_token_data.py +476 -0
  114. intentkit/skills/dapplooker/schema.json +91 -0
  115. intentkit/skills/defillama/__init__.py +323 -0
  116. intentkit/skills/defillama/api.py +315 -0
  117. intentkit/skills/defillama/base.py +135 -0
  118. intentkit/skills/defillama/coins/__init__.py +0 -0
  119. intentkit/skills/defillama/coins/fetch_batch_historical_prices.py +116 -0
  120. intentkit/skills/defillama/coins/fetch_block.py +98 -0
  121. intentkit/skills/defillama/coins/fetch_current_prices.py +105 -0
  122. intentkit/skills/defillama/coins/fetch_first_price.py +100 -0
  123. intentkit/skills/defillama/coins/fetch_historical_prices.py +110 -0
  124. intentkit/skills/defillama/coins/fetch_price_chart.py +109 -0
  125. intentkit/skills/defillama/coins/fetch_price_percentage.py +93 -0
  126. intentkit/skills/defillama/config/__init__.py +0 -0
  127. intentkit/skills/defillama/config/chains.py +433 -0
  128. intentkit/skills/defillama/defillama.jpeg +0 -0
  129. intentkit/skills/defillama/fees/__init__.py +0 -0
  130. intentkit/skills/defillama/fees/fetch_fees_overview.py +130 -0
  131. intentkit/skills/defillama/schema.json +383 -0
  132. intentkit/skills/defillama/stablecoins/__init__.py +0 -0
  133. intentkit/skills/defillama/stablecoins/fetch_stablecoin_chains.py +100 -0
  134. intentkit/skills/defillama/stablecoins/fetch_stablecoin_charts.py +129 -0
  135. intentkit/skills/defillama/stablecoins/fetch_stablecoin_prices.py +83 -0
  136. intentkit/skills/defillama/stablecoins/fetch_stablecoins.py +126 -0
  137. intentkit/skills/defillama/tests/__init__.py +0 -0
  138. intentkit/skills/defillama/tests/api_integration.test.py +192 -0
  139. intentkit/skills/defillama/tests/api_unit.test.py +583 -0
  140. intentkit/skills/defillama/tvl/__init__.py +0 -0
  141. intentkit/skills/defillama/tvl/fetch_chain_historical_tvl.py +106 -0
  142. intentkit/skills/defillama/tvl/fetch_chains.py +107 -0
  143. intentkit/skills/defillama/tvl/fetch_historical_tvl.py +91 -0
  144. intentkit/skills/defillama/tvl/fetch_protocol.py +207 -0
  145. intentkit/skills/defillama/tvl/fetch_protocol_current_tvl.py +93 -0
  146. intentkit/skills/defillama/tvl/fetch_protocols.py +196 -0
  147. intentkit/skills/defillama/volumes/__init__.py +0 -0
  148. intentkit/skills/defillama/volumes/fetch_dex_overview.py +157 -0
  149. intentkit/skills/defillama/volumes/fetch_dex_summary.py +123 -0
  150. intentkit/skills/defillama/volumes/fetch_options_overview.py +131 -0
  151. intentkit/skills/defillama/yields/__init__.py +0 -0
  152. intentkit/skills/defillama/yields/fetch_pool_chart.py +100 -0
  153. intentkit/skills/defillama/yields/fetch_pools.py +126 -0
  154. intentkit/skills/dexscreener/__init__.py +93 -0
  155. intentkit/skills/dexscreener/base.py +133 -0
  156. intentkit/skills/dexscreener/dexscreener.png +0 -0
  157. intentkit/skills/dexscreener/model/__init__.py +0 -0
  158. intentkit/skills/dexscreener/model/search_token_response.py +82 -0
  159. intentkit/skills/dexscreener/schema.json +48 -0
  160. intentkit/skills/dexscreener/search_token.py +321 -0
  161. intentkit/skills/dune_analytics/__init__.py +103 -0
  162. intentkit/skills/dune_analytics/base.py +46 -0
  163. intentkit/skills/dune_analytics/dune.png +0 -0
  164. intentkit/skills/dune_analytics/fetch_kol_buys.py +128 -0
  165. intentkit/skills/dune_analytics/fetch_nation_metrics.py +237 -0
  166. intentkit/skills/dune_analytics/schema.json +99 -0
  167. intentkit/skills/elfa/README.md +100 -0
  168. intentkit/skills/elfa/__init__.py +123 -0
  169. intentkit/skills/elfa/base.py +28 -0
  170. intentkit/skills/elfa/elfa.jpg +0 -0
  171. intentkit/skills/elfa/mention.py +504 -0
  172. intentkit/skills/elfa/schema.json +153 -0
  173. intentkit/skills/elfa/stats.py +118 -0
  174. intentkit/skills/elfa/tokens.py +126 -0
  175. intentkit/skills/enso/README.md +75 -0
  176. intentkit/skills/enso/__init__.py +114 -0
  177. intentkit/skills/enso/abi/__init__.py +0 -0
  178. intentkit/skills/enso/abi/approval.py +279 -0
  179. intentkit/skills/enso/abi/erc20.py +14 -0
  180. intentkit/skills/enso/abi/route.py +129 -0
  181. intentkit/skills/enso/base.py +44 -0
  182. intentkit/skills/enso/best_yield.py +286 -0
  183. intentkit/skills/enso/enso.jpg +0 -0
  184. intentkit/skills/enso/networks.py +105 -0
  185. intentkit/skills/enso/prices.py +93 -0
  186. intentkit/skills/enso/route.py +300 -0
  187. intentkit/skills/enso/schema.json +212 -0
  188. intentkit/skills/enso/tokens.py +223 -0
  189. intentkit/skills/enso/wallet.py +381 -0
  190. intentkit/skills/github/README.md +63 -0
  191. intentkit/skills/github/__init__.py +54 -0
  192. intentkit/skills/github/base.py +21 -0
  193. intentkit/skills/github/github.jpg +0 -0
  194. intentkit/skills/github/github_search.py +183 -0
  195. intentkit/skills/github/schema.json +59 -0
  196. intentkit/skills/heurist/__init__.py +143 -0
  197. intentkit/skills/heurist/base.py +26 -0
  198. intentkit/skills/heurist/heurist.png +0 -0
  199. intentkit/skills/heurist/image_generation_animagine_xl.py +162 -0
  200. intentkit/skills/heurist/image_generation_arthemy_comics.py +162 -0
  201. intentkit/skills/heurist/image_generation_arthemy_real.py +162 -0
  202. intentkit/skills/heurist/image_generation_braindance.py +162 -0
  203. intentkit/skills/heurist/image_generation_cyber_realistic_xl.py +162 -0
  204. intentkit/skills/heurist/image_generation_flux_1_dev.py +162 -0
  205. intentkit/skills/heurist/image_generation_sdxl.py +161 -0
  206. intentkit/skills/heurist/schema.json +196 -0
  207. intentkit/skills/lifi/README.md +294 -0
  208. intentkit/skills/lifi/__init__.py +141 -0
  209. intentkit/skills/lifi/base.py +21 -0
  210. intentkit/skills/lifi/lifi.png +0 -0
  211. intentkit/skills/lifi/schema.json +89 -0
  212. intentkit/skills/lifi/token_execute.py +472 -0
  213. intentkit/skills/lifi/token_quote.py +190 -0
  214. intentkit/skills/lifi/utils.py +656 -0
  215. intentkit/skills/moralis/README.md +490 -0
  216. intentkit/skills/moralis/__init__.py +110 -0
  217. intentkit/skills/moralis/api.py +281 -0
  218. intentkit/skills/moralis/base.py +55 -0
  219. intentkit/skills/moralis/fetch_chain_portfolio.py +191 -0
  220. intentkit/skills/moralis/fetch_nft_portfolio.py +284 -0
  221. intentkit/skills/moralis/fetch_solana_portfolio.py +331 -0
  222. intentkit/skills/moralis/fetch_wallet_portfolio.py +301 -0
  223. intentkit/skills/moralis/moralis.png +0 -0
  224. intentkit/skills/moralis/schema.json +156 -0
  225. intentkit/skills/moralis/tests/__init__.py +0 -0
  226. intentkit/skills/moralis/tests/test_wallet.py +511 -0
  227. intentkit/skills/nation/__init__.py +62 -0
  228. intentkit/skills/nation/base.py +31 -0
  229. intentkit/skills/nation/nation.png +0 -0
  230. intentkit/skills/nation/nft_check.py +106 -0
  231. intentkit/skills/nation/schema.json +58 -0
  232. intentkit/skills/openai/__init__.py +107 -0
  233. intentkit/skills/openai/base.py +32 -0
  234. intentkit/skills/openai/dalle_image_generation.py +128 -0
  235. intentkit/skills/openai/gpt_image_generation.py +152 -0
  236. intentkit/skills/openai/gpt_image_to_image.py +186 -0
  237. intentkit/skills/openai/image_to_text.py +126 -0
  238. intentkit/skills/openai/openai.png +0 -0
  239. intentkit/skills/openai/schema.json +139 -0
  240. intentkit/skills/portfolio/README.md +55 -0
  241. intentkit/skills/portfolio/__init__.py +151 -0
  242. intentkit/skills/portfolio/base.py +107 -0
  243. intentkit/skills/portfolio/constants.py +9 -0
  244. intentkit/skills/portfolio/moralis.png +0 -0
  245. intentkit/skills/portfolio/schema.json +237 -0
  246. intentkit/skills/portfolio/token_balances.py +155 -0
  247. intentkit/skills/portfolio/wallet_approvals.py +102 -0
  248. intentkit/skills/portfolio/wallet_defi_positions.py +80 -0
  249. intentkit/skills/portfolio/wallet_history.py +155 -0
  250. intentkit/skills/portfolio/wallet_net_worth.py +112 -0
  251. intentkit/skills/portfolio/wallet_nfts.py +139 -0
  252. intentkit/skills/portfolio/wallet_profitability.py +101 -0
  253. intentkit/skills/portfolio/wallet_profitability_summary.py +91 -0
  254. intentkit/skills/portfolio/wallet_stats.py +79 -0
  255. intentkit/skills/portfolio/wallet_swaps.py +147 -0
  256. intentkit/skills/skills.toml +103 -0
  257. intentkit/skills/slack/__init__.py +98 -0
  258. intentkit/skills/slack/base.py +55 -0
  259. intentkit/skills/slack/get_channel.py +109 -0
  260. intentkit/skills/slack/get_message.py +136 -0
  261. intentkit/skills/slack/schedule_message.py +92 -0
  262. intentkit/skills/slack/schema.json +135 -0
  263. intentkit/skills/slack/send_message.py +81 -0
  264. intentkit/skills/slack/slack.jpg +0 -0
  265. intentkit/skills/system/__init__.py +90 -0
  266. intentkit/skills/system/base.py +22 -0
  267. intentkit/skills/system/read_agent_api_key.py +87 -0
  268. intentkit/skills/system/regenerate_agent_api_key.py +77 -0
  269. intentkit/skills/system/schema.json +53 -0
  270. intentkit/skills/system/system.svg +76 -0
  271. intentkit/skills/tavily/README.md +86 -0
  272. intentkit/skills/tavily/__init__.py +91 -0
  273. intentkit/skills/tavily/base.py +27 -0
  274. intentkit/skills/tavily/schema.json +119 -0
  275. intentkit/skills/tavily/tavily.jpg +0 -0
  276. intentkit/skills/tavily/tavily_extract.py +147 -0
  277. intentkit/skills/tavily/tavily_search.py +139 -0
  278. intentkit/skills/token/README.md +89 -0
  279. intentkit/skills/token/__init__.py +107 -0
  280. intentkit/skills/token/base.py +154 -0
  281. intentkit/skills/token/constants.py +9 -0
  282. intentkit/skills/token/erc20_transfers.py +145 -0
  283. intentkit/skills/token/moralis.png +0 -0
  284. intentkit/skills/token/schema.json +141 -0
  285. intentkit/skills/token/token_analytics.py +81 -0
  286. intentkit/skills/token/token_price.py +132 -0
  287. intentkit/skills/token/token_search.py +121 -0
  288. intentkit/skills/twitter/__init__.py +146 -0
  289. intentkit/skills/twitter/base.py +68 -0
  290. intentkit/skills/twitter/follow_user.py +69 -0
  291. intentkit/skills/twitter/get_mentions.py +124 -0
  292. intentkit/skills/twitter/get_timeline.py +111 -0
  293. intentkit/skills/twitter/get_user_by_username.py +84 -0
  294. intentkit/skills/twitter/get_user_tweets.py +123 -0
  295. intentkit/skills/twitter/like_tweet.py +65 -0
  296. intentkit/skills/twitter/post_tweet.py +90 -0
  297. intentkit/skills/twitter/reply_tweet.py +98 -0
  298. intentkit/skills/twitter/retweet.py +76 -0
  299. intentkit/skills/twitter/schema.json +258 -0
  300. intentkit/skills/twitter/search_tweets.py +115 -0
  301. intentkit/skills/twitter/twitter.png +0 -0
  302. intentkit/skills/unrealspeech/__init__.py +55 -0
  303. intentkit/skills/unrealspeech/base.py +21 -0
  304. intentkit/skills/unrealspeech/schema.json +100 -0
  305. intentkit/skills/unrealspeech/text_to_speech.py +177 -0
  306. intentkit/skills/unrealspeech/unrealspeech.jpg +0 -0
  307. intentkit/skills/venice_audio/__init__.py +106 -0
  308. intentkit/skills/venice_audio/base.py +119 -0
  309. intentkit/skills/venice_audio/input.py +41 -0
  310. intentkit/skills/venice_audio/schema.json +152 -0
  311. intentkit/skills/venice_audio/venice_audio.py +240 -0
  312. intentkit/skills/venice_audio/venice_logo.jpg +0 -0
  313. intentkit/skills/venice_image/README.md +119 -0
  314. intentkit/skills/venice_image/__init__.py +154 -0
  315. intentkit/skills/venice_image/api.py +138 -0
  316. intentkit/skills/venice_image/base.py +188 -0
  317. intentkit/skills/venice_image/config.py +35 -0
  318. intentkit/skills/venice_image/image_enhance/README.md +119 -0
  319. intentkit/skills/venice_image/image_enhance/__init__.py +0 -0
  320. intentkit/skills/venice_image/image_enhance/image_enhance.py +80 -0
  321. intentkit/skills/venice_image/image_enhance/image_enhance_base.py +23 -0
  322. intentkit/skills/venice_image/image_enhance/image_enhance_input.py +40 -0
  323. intentkit/skills/venice_image/image_generation/README.md +144 -0
  324. intentkit/skills/venice_image/image_generation/__init__.py +0 -0
  325. intentkit/skills/venice_image/image_generation/image_generation_base.py +117 -0
  326. intentkit/skills/venice_image/image_generation/image_generation_fluently_xl.py +26 -0
  327. intentkit/skills/venice_image/image_generation/image_generation_flux_dev.py +27 -0
  328. intentkit/skills/venice_image/image_generation/image_generation_flux_dev_uncensored.py +26 -0
  329. intentkit/skills/venice_image/image_generation/image_generation_input.py +158 -0
  330. intentkit/skills/venice_image/image_generation/image_generation_lustify_sdxl.py +26 -0
  331. intentkit/skills/venice_image/image_generation/image_generation_pony_realism.py +26 -0
  332. intentkit/skills/venice_image/image_generation/image_generation_stable_diffusion_3_5.py +28 -0
  333. intentkit/skills/venice_image/image_generation/image_generation_venice_sd35.py +28 -0
  334. intentkit/skills/venice_image/image_upscale/README.md +111 -0
  335. intentkit/skills/venice_image/image_upscale/__init__.py +0 -0
  336. intentkit/skills/venice_image/image_upscale/image_upscale.py +90 -0
  337. intentkit/skills/venice_image/image_upscale/image_upscale_base.py +23 -0
  338. intentkit/skills/venice_image/image_upscale/image_upscale_input.py +22 -0
  339. intentkit/skills/venice_image/image_vision/README.md +112 -0
  340. intentkit/skills/venice_image/image_vision/__init__.py +0 -0
  341. intentkit/skills/venice_image/image_vision/image_vision.py +100 -0
  342. intentkit/skills/venice_image/image_vision/image_vision_base.py +17 -0
  343. intentkit/skills/venice_image/image_vision/image_vision_input.py +9 -0
  344. intentkit/skills/venice_image/schema.json +267 -0
  345. intentkit/skills/venice_image/utils.py +78 -0
  346. intentkit/skills/venice_image/venice_image.jpg +0 -0
  347. intentkit/skills/web_scraper/README.md +82 -0
  348. intentkit/skills/web_scraper/__init__.py +92 -0
  349. intentkit/skills/web_scraper/base.py +21 -0
  350. intentkit/skills/web_scraper/langchain.png +0 -0
  351. intentkit/skills/web_scraper/schema.json +115 -0
  352. intentkit/skills/web_scraper/scrape_and_index.py +327 -0
  353. intentkit/utils/__init__.py +1 -0
  354. intentkit/utils/chain.py +436 -0
  355. intentkit/utils/error.py +134 -0
  356. intentkit/utils/logging.py +70 -0
  357. intentkit/utils/middleware.py +61 -0
  358. intentkit/utils/random.py +16 -0
  359. intentkit/utils/s3.py +267 -0
  360. intentkit/utils/slack_alert.py +79 -0
  361. intentkit/utils/tx.py +37 -0
  362. {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/METADATA +1 -1
  363. intentkit-0.5.2.dist-info/RECORD +365 -0
  364. intentkit-0.5.0.dist-info/RECORD +0 -4
  365. {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/WHEEL +0 -0
  366. {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,93 @@
1
+ import logging
2
+ from typing import Optional, TypedDict
3
+
4
+ from intentkit.abstracts.skill import SkillStoreABC
5
+ from intentkit.skills.base import SkillConfig, SkillState
6
+ from intentkit.skills.dexscreener.base import DexScreenerBaseTool
7
+ from intentkit.skills.dexscreener.search_token import SearchToken
8
+
9
+ # Cache skills at the system level, because they are stateless
10
+ _cache: dict[str, DexScreenerBaseTool] = {}
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SkillStates(TypedDict):
16
+ search_token: SkillState
17
+
18
+
19
+ _SKILL_NAME_TO_CLASS_MAP: dict[str, type[DexScreenerBaseTool]] = {
20
+ "search_token": SearchToken,
21
+ }
22
+
23
+
24
+ class Config(SkillConfig):
25
+ """Configuration for DexScreener skills."""
26
+
27
+ enabled: bool
28
+ states: SkillStates
29
+
30
+
31
+ async def get_skills(
32
+ config: "Config",
33
+ is_private: bool,
34
+ store: SkillStoreABC,
35
+ **_,
36
+ ) -> list[DexScreenerBaseTool]:
37
+ """Get all DexScreener skills.
38
+
39
+ Args:
40
+ config: The configuration for DexScreener skills.
41
+ is_private: Whether to include private skills.
42
+ store: The skill store for persisting data.
43
+
44
+ Returns:
45
+ A list of DexScreener skills.
46
+ """
47
+
48
+ available_skills = []
49
+
50
+ # Include skills based on their state
51
+ for skill_name, state in config["states"].items():
52
+ if state == "disabled":
53
+ continue
54
+ elif state == "public" or (state == "private" and is_private):
55
+ available_skills.append(skill_name)
56
+
57
+ logger.debug(f"Available Skills {available_skills}")
58
+ logger.debug(f"Hardcoded Skills {_SKILL_NAME_TO_CLASS_MAP}")
59
+
60
+ # Get each skill using the cached getter
61
+ result = []
62
+ for name in available_skills:
63
+ skill = get_dexscreener_skills(name, store)
64
+ if skill:
65
+ result.append(skill)
66
+ return result
67
+
68
+
69
+ def get_dexscreener_skills(
70
+ name: str,
71
+ store: SkillStoreABC,
72
+ ) -> Optional[DexScreenerBaseTool]:
73
+ """Get a DexScreener skill by name.
74
+
75
+ Args:
76
+ name: The name of the skill to get
77
+ store: The skill store for persisting data
78
+
79
+ Returns:
80
+ The requested DexScreener skill
81
+ """
82
+
83
+ # Return from cache immediately if already exists
84
+ if name in _cache:
85
+ return _cache[name]
86
+
87
+ skill_class = _SKILL_NAME_TO_CLASS_MAP.get(name)
88
+ if not skill_class:
89
+ logger.warning(f"Unknown Dexscreener skill: {name}")
90
+ return None
91
+
92
+ _cache[name] = skill_class(skill_store=store)
93
+ return _cache[name]
@@ -0,0 +1,133 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Dict, Optional, Tuple
4
+
5
+ import httpx
6
+ from pydantic import Field
7
+
8
+ from intentkit.abstracts.skill import SkillStoreABC
9
+ from intentkit.skills.base import IntentKitSkill
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ DEXSCREENER_BASE_URL = "https://api.dexscreener.com"
14
+
15
+ # Removed DexScreenerApiError as it won't be raised by _get anymore
16
+
17
+ # ApiResult still represents (success_data, error_data)
18
+ ApiResult = Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]
19
+
20
+
21
+ class DexScreenerBaseTool(IntentKitSkill):
22
+ """
23
+ Generic base class for tools interacting with the Dex Screener API.
24
+ Handles shared logic like API calls and error reporting via return values.
25
+ """
26
+
27
+ skill_store: SkillStoreABC = Field(
28
+ description="The skill store for persisting data and configs."
29
+ )
30
+ base_url: str = DEXSCREENER_BASE_URL
31
+
32
+ @property
33
+ def category(self) -> str:
34
+ return "dexscreener"
35
+
36
+ async def _get(
37
+ self,
38
+ path: str,
39
+ params: Optional[Dict[str, Any]] = None,
40
+ ) -> ApiResult:
41
+ """
42
+ Makes an asynchronous GET request to the DexScreener API.
43
+
44
+ Args:
45
+ path: The API endpoint path (e.g., "/dex/search").
46
+ params: Optional dictionary of query parameters.
47
+
48
+ Returns:
49
+ A tuple (data, error_details):
50
+ - (dict, None): On HTTP 2xx success with valid JSON response.
51
+ - (None, dict): On any error (API error, connection error,
52
+ JSON parsing error, unexpected error). The dict
53
+ contains details including an 'error_type'.
54
+ """
55
+ if not path.startswith("/"):
56
+ path = "/" + path
57
+
58
+ url = f"{self.base_url}{path}"
59
+ headers = {"Accept": "application/json"}
60
+ method = "GET"
61
+
62
+ logger.debug(f"Calling DexScreener API: {method} {url} with params: {params}")
63
+ response = None # Define response outside try block for access in except
64
+
65
+ try:
66
+ async with httpx.AsyncClient() as client:
67
+ response = await client.request(
68
+ method, url, params=params, headers=headers
69
+ )
70
+
71
+ # Attempt to parse JSON response text
72
+ try:
73
+ response_data = response.json()
74
+ except json.JSONDecodeError as json_err:
75
+ logger.error(
76
+ f"Failed to parse JSON response from {url}. Status: {response.status_code}. Response text: {response.text}",
77
+ exc_info=True,
78
+ )
79
+ error_details = {
80
+ "error": "Failed to parse DexScreener API response",
81
+ "error_type": "parsing_error",
82
+ "status_code": response.status_code,
83
+ "details": response.text, # Raw text causing the error
84
+ "original_exception": str(json_err),
85
+ "url": url,
86
+ }
87
+ return None, error_details # Return parsing error
88
+
89
+ # Check HTTP status *after* attempting JSON parse
90
+ if response.is_success: # 2xx
91
+ logger.debug(
92
+ f"DexScreener API success response status: {response.status_code}"
93
+ )
94
+ return response_data, None # Success
95
+ else: # 4xx/5xx
96
+ logger.warning(
97
+ f"DexScreener API returned error status: {response.status_code} - {response.text}"
98
+ )
99
+ error_details = {
100
+ "error": "DexScreener API request failed",
101
+ "error_type": "api_error",
102
+ "status_code": response.status_code,
103
+ "response_body": response_data, # Parsed error body if available
104
+ "url": url,
105
+ }
106
+ return None, error_details # Return API error
107
+
108
+ except httpx.RequestError as req_err:
109
+ logger.error(
110
+ f"Request error connecting to DexScreener API: {req_err}", exc_info=True
111
+ )
112
+ error_details = {
113
+ "error": "Failed to connect to DexScreener API",
114
+ "error_type": "connection_error",
115
+ "details": str(req_err),
116
+ "url": url,
117
+ }
118
+ return None, error_details # Return connection error
119
+
120
+ except Exception as e:
121
+ # Catch any other unexpected errors during the process
122
+ logger.exception(
123
+ f"An unexpected error occurred during DexScreener API GET call: {e}"
124
+ )
125
+ status_code = response.status_code if response else None
126
+ error_details = {
127
+ "error": "An unexpected error occurred during API call",
128
+ "error_type": "unexpected_error",
129
+ "status_code": status_code, # Include if available
130
+ "details": str(e),
131
+ "url": url,
132
+ }
133
+ return None, error_details # Return unexpected error
File without changes
@@ -0,0 +1,82 @@
1
+ from typing import List, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class TokenModel(BaseModel):
7
+ address: Optional[str] = None
8
+ name: Optional[str] = None
9
+ symbol: Optional[str] = None
10
+
11
+
12
+ class TxnsDetailsModel(BaseModel):
13
+ buys: Optional[int] = None
14
+ sells: Optional[int] = None
15
+
16
+
17
+ class TxnsModel(BaseModel):
18
+ m5: Optional[TxnsDetailsModel] = None
19
+ h1: Optional[TxnsDetailsModel] = None
20
+ h6: Optional[TxnsDetailsModel] = None
21
+ h24: Optional[TxnsDetailsModel] = None
22
+
23
+
24
+ class VolumeModel(BaseModel):
25
+ h24: Optional[float] = None
26
+ h6: Optional[float] = None
27
+ h1: Optional[float] = None
28
+ m5: Optional[float] = None
29
+
30
+
31
+ class PriceChangeModel(BaseModel):
32
+ m5: Optional[float] = None
33
+ h1: Optional[float] = None
34
+ h6: Optional[float] = None
35
+ h24: Optional[float] = None
36
+
37
+
38
+ class LiquidityModel(BaseModel):
39
+ usd: Optional[float] = None
40
+ base: Optional[float] = None
41
+ quote: Optional[float] = None
42
+
43
+
44
+ class WebsiteModel(BaseModel):
45
+ label: Optional[str] = None
46
+ url: Optional[str] = None
47
+
48
+
49
+ class SocialModel(BaseModel):
50
+ type: Optional[str] = None
51
+ url: Optional[str] = None
52
+
53
+
54
+ class InfoModel(BaseModel):
55
+ imageUrl: Optional[str] = None
56
+ websites: Optional[List[Optional[WebsiteModel]]] = None
57
+ socials: Optional[List[Optional[SocialModel]]] = None
58
+
59
+
60
+ class PairModel(BaseModel):
61
+ chainId: Optional[str] = None
62
+ dexId: Optional[str] = None
63
+ url: Optional[str] = None
64
+ pairAddress: Optional[str] = None
65
+ labels: Optional[List[Optional[str]]] = None
66
+ baseToken: Optional[TokenModel] = None
67
+ quoteToken: Optional[TokenModel] = None
68
+ priceNative: Optional[str] = None
69
+ priceUsd: Optional[str] = None
70
+ txns: Optional[TxnsModel] = None
71
+ volume: Optional[VolumeModel] = None
72
+ priceChange: Optional[PriceChangeModel] = None
73
+ liquidity: Optional[LiquidityModel] = None
74
+ fdv: Optional[float] = None
75
+ marketCap: Optional[float] = None
76
+ pairCreatedAt: Optional[int] = None
77
+ info: Optional[InfoModel] = None
78
+
79
+
80
+ class SearchTokenResponseModel(BaseModel):
81
+ schemaVersion: Optional[str] = None
82
+ pairs: Optional[List[Optional[PairModel]]] = None
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Dexscreener",
4
+ "description": "Integration with DexScreener API, enabling crypto token pair information",
5
+ "type": "object",
6
+ "x-icon": "https://ai.service.crestal.dev/skills/dexscreener/dexscreener.png",
7
+ "x-tags": [
8
+ "Crypto",
9
+ "Market Data",
10
+ "Finance",
11
+ "Blockchain"
12
+ ],
13
+ "properties": {
14
+ "enabled": {
15
+ "type": "boolean",
16
+ "title": "Enabled",
17
+ "description": "Enable or disable the Dexscreener skill.",
18
+ "default": false
19
+ },
20
+ "states": {
21
+ "type": "object",
22
+ "title": "Skill States",
23
+ "description": "Enable/disable specific tools. Only enable one if you want a consistent characteristic for your agent",
24
+ "properties": {
25
+ "search_token": {
26
+ "type": "string",
27
+ "enum": [
28
+ "disabled",
29
+ "public",
30
+ "private"
31
+ ],
32
+ "x-enum-title": [
33
+ "Disabled",
34
+ "Agent Owner + All Users",
35
+ "Agent Owner Only"
36
+ ],
37
+ "description": "Searches on DexScreener for token pairs matching a query (symbol, name, address). Returns up to 50 pairs sorted by 'liquidity' or 'volume24h' (required input), including price, volume, etc. Use this tool to find token information based on user queries.",
38
+ "default": "disabled"
39
+ }
40
+ }
41
+ }
42
+ },
43
+ "required": [
44
+ "enabled",
45
+ "states"
46
+ ],
47
+ "additionalProperties": true
48
+ }
@@ -0,0 +1,321 @@
1
+ import json
2
+ import logging
3
+ from enum import Enum
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Literal,
8
+ Optional,
9
+ Type,
10
+ )
11
+
12
+ from pydantic import BaseModel, Field, ValidationError
13
+
14
+ from intentkit.skills.dexscreener.base import DexScreenerBaseTool
15
+ from intentkit.skills.dexscreener.model.search_token_response import (
16
+ PairModel,
17
+ SearchTokenResponseModel,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ MAX_RESULTS_LIMIT = 25 # limit to 25 pair entries
23
+ SEARCH_TOKEN_API_PATH = "/latest/dex/search"
24
+
25
+ # Define the allowed sort options, including multiple volume types
26
+ SortByOption = Literal["liquidity", "volume"]
27
+ VolumeTimeframeOption = Literal["24_hour", "6_hour", "1_hour", "5_minutes"]
28
+
29
+
30
+ class QueryType(str, Enum):
31
+ TEXT = "TEXT"
32
+ TICKER = "TICKER"
33
+ ADDRESS = "ADDRESS"
34
+
35
+
36
+ # this will bring aloside with pairs information
37
+ DISCLAIMER_TEXT = {
38
+ "disclaimer": (
39
+ "Search results may include unofficial, duplicate, or potentially malicious tokens. "
40
+ "If multiple unrelated tokens share a similar name or ticker, ask the user for the exact token address. "
41
+ "If the correct token is not found, re-run the tool using the provided address. "
42
+ "Also advise the user to verify the token's legitimacy via its official social links included in the result."
43
+ )
44
+ }
45
+
46
+
47
+ class SearchTokenInput(BaseModel):
48
+ """Input schema for the DexScreener search_token tool."""
49
+
50
+ query: str = Field(
51
+ description="The search query string (e.g., token symbol 'WIF', pair address, token address '0x...', token name 'Dogwifhat', or ticker '$WIF'). Prefixing with '$' filters results to match the base token symbol exactly (case-insensitive)."
52
+ )
53
+ sort_by: Optional[SortByOption] = Field(
54
+ default="liquidity",
55
+ description="Sort preference for the results. Options: 'liquidity' (default) or 'volume'",
56
+ )
57
+ volume_timeframe: Optional[VolumeTimeframeOption] = Field(
58
+ default="24_hour",
59
+ description=f"define which timeframe should we use if the 'sort_by' is `volume` avalable options are {VolumeTimeframeOption}",
60
+ )
61
+
62
+
63
+ class SearchToken(DexScreenerBaseTool):
64
+ """
65
+ Tool to search for token pairs on DexScreener based on a query string.
66
+ """
67
+
68
+ name: str = "dexscreener_search_token"
69
+ description: str = (
70
+ f"Searches DexScreener for token pairs matching the provided query string "
71
+ f"(e.g., token symbol like 'WIF', pair address, token name like 'Dogwifhat', or ticker like '$WIF'). "
72
+ f"If the query starts with '$', it filters results to only include pairs where the base token symbol exactly matches the ticker (case-insensitive). "
73
+ f"Returns a list of matching pairs with details like price, volume, liquidity, etc., "
74
+ f"sorted by the specified criteria (via 'sort_by': 'liquidity', 'volume'; defaults to 'liquidity'), "
75
+ f"limited to the top {MAX_RESULTS_LIMIT}. "
76
+ f"Use this tool to find token information based on user queries."
77
+ )
78
+ args_schema: Type[BaseModel] = SearchTokenInput
79
+
80
+ async def _arun(
81
+ self,
82
+ query: str,
83
+ sort_by: Optional[SortByOption] = "liquidity",
84
+ volume_timeframe: Optional[VolumeTimeframeOption] = "24_hour",
85
+ **kwargs: Any,
86
+ ) -> str:
87
+ """Implementation to search token, with filtering based on query type."""
88
+
89
+ # dexscreener 300 request per minute (across all user) based on dexscreener docs
90
+ # https://docs.dexscreener.com/api/reference#get-latest-dex-search
91
+ await self.user_rate_limit_by_category(
92
+ # using hardcoded user_id to make sure it limit across all users
93
+ user_id=f"{self.category}{self.name}",
94
+ limit=300,
95
+ minutes=1,
96
+ )
97
+
98
+ sort_by = sort_by or "liquidity"
99
+ volume_timeframe = volume_timeframe or "24_hour"
100
+
101
+ # Determine query type
102
+ query_type = self.get_query_type(query)
103
+
104
+ # Process query based on type
105
+ if query_type == QueryType.TICKER:
106
+ search_query = query[1:] # Remove the '$' prefix
107
+ target_ticker = search_query.upper()
108
+ else:
109
+ search_query = query
110
+ target_ticker = None
111
+
112
+ logger.info(
113
+ f"Executing DexScreener search_token tool with query: '{query}' "
114
+ f"(interpreted as {query_type.value} search for '{search_query}'), "
115
+ f"sort_by: {sort_by}"
116
+ )
117
+
118
+ ### --- sort functions ---
119
+ def get_liquidity_usd(pair: PairModel) -> float:
120
+ return (
121
+ pair.liquidity.usd
122
+ if pair.liquidity and pair.liquidity.usd is not None
123
+ else 0.0
124
+ )
125
+
126
+ def get_volume(pair: PairModel) -> float:
127
+ if not pair.volume:
128
+ return 0.0
129
+ return {
130
+ "24_hour": pair.volume.h24,
131
+ "6_hour": pair.volume.h6,
132
+ "1_hour": pair.volume.h1,
133
+ "5_minutes": pair.volume.m5,
134
+ }.get(volume_timeframe, 0.0) or 0.0
135
+
136
+ def get_sort_key_func() -> Callable[[PairModel], float]:
137
+ if sort_by == "liquidity":
138
+ return get_liquidity_usd
139
+ if sort_by == "volume":
140
+ return get_volume
141
+ logger.warning(
142
+ f"Invalid sort_by value '{sort_by}', defaulting to liquidity."
143
+ )
144
+ return get_liquidity_usd
145
+
146
+ ### --- END sort functions ---
147
+
148
+ try:
149
+ data, error_details = await self._get(
150
+ path=SEARCH_TOKEN_API_PATH, params={"q": search_query}
151
+ )
152
+
153
+ if error_details:
154
+ return await self._handle_error_response(error_details)
155
+ if not data:
156
+ logger.error(f"No data or error details returned for query '{query}'")
157
+ return json.dumps(
158
+ {
159
+ "error": "API call returned empty success response.",
160
+ "error_type": "empty_success",
161
+ },
162
+ indent=2,
163
+ )
164
+
165
+ try:
166
+ result = SearchTokenResponseModel.model_validate(data)
167
+ except ValidationError as e:
168
+ return await self._handle_validation_error(e, query, data)
169
+
170
+ if not result.pairs:
171
+ return await self._no_pairs_found_response(
172
+ query, reason="returned null or empty for pairs"
173
+ )
174
+
175
+ pairs_list = [p for p in result.pairs if p is not None]
176
+
177
+ # Apply filtering based on query type
178
+ if query_type == QueryType.TICKER and target_ticker:
179
+ pairs_list = [
180
+ p
181
+ for p in pairs_list
182
+ if p.baseToken
183
+ and p.baseToken.symbol
184
+ and p.baseToken.symbol.upper() == target_ticker
185
+ ]
186
+ if not pairs_list:
187
+ return await self._no_pairs_found_response(
188
+ query, reason=f"no match for ticker '${target_ticker}'"
189
+ )
190
+ elif query_type == QueryType.ADDRESS:
191
+ # Filter by address (checking pairAddress, baseToken.address, quoteToken.address)
192
+ pairs_list = [
193
+ p
194
+ for p in pairs_list
195
+ if (p.pairAddress and p.pairAddress.lower() == search_query.lower())
196
+ or (
197
+ p.baseToken
198
+ and p.baseToken.address
199
+ and p.baseToken.address.lower() == search_query.lower()
200
+ )
201
+ or (
202
+ p.quoteToken
203
+ and p.quoteToken.address
204
+ and p.quoteToken.address.lower() == search_query.lower()
205
+ )
206
+ ]
207
+ if not pairs_list:
208
+ return await self._no_pairs_found_response(
209
+ query, reason=f"no match for address '{search_query}'"
210
+ )
211
+
212
+ try:
213
+ sort_func = get_sort_key_func()
214
+ pairs_list.sort(key=sort_func, reverse=True)
215
+ except Exception as sort_err:
216
+ logger.error(f"Sorting failed: {sort_err}", exc_info=True)
217
+ return json.dumps(
218
+ {
219
+ "error": "Failed to sort results.",
220
+ "error_type": "sorting_error",
221
+ "details": str(sort_err),
222
+ "unsorted_results": [
223
+ p.model_dump() for p in pairs_list[:MAX_RESULTS_LIMIT]
224
+ ],
225
+ **DISCLAIMER_TEXT,
226
+ },
227
+ indent=2,
228
+ )
229
+
230
+ final_count = min(len(pairs_list), MAX_RESULTS_LIMIT)
231
+ logger.info(f"Returning {final_count} pairs for query '{query}'")
232
+ return json.dumps(
233
+ {
234
+ **DISCLAIMER_TEXT,
235
+ "pairs": [p.model_dump() for p in pairs_list[:MAX_RESULTS_LIMIT]],
236
+ },
237
+ indent=2,
238
+ )
239
+ except Exception as e:
240
+ return await self._handle_unexpected_runtime_error(e, query)
241
+
242
+ def get_query_type(self, query: str) -> QueryType:
243
+ """
244
+ Determine whether the query is a TEXT, TICKER, or ADDRESS.
245
+
246
+ TICKER: starts with '$'
247
+ ADDRESS: starts with '0x'.
248
+ TEXT: anything else.
249
+ """
250
+ if query.startswith("0x"):
251
+ return QueryType.ADDRESS
252
+ if query.startswith("$"):
253
+ return QueryType.TICKER
254
+ return QueryType.TEXT
255
+
256
+ async def _handle_error_response(self, error_details: dict) -> str:
257
+ """Formats error details (from _get) into a JSON string."""
258
+ if error_details.get("error_type") in [
259
+ "connection_error",
260
+ "parsing_error",
261
+ "unexpected_error",
262
+ ]:
263
+ logger.error(f"DexScreener tool encountered an error: {error_details}")
264
+ else: # api_error
265
+ logger.warning(f"DexScreener API returned an error: {error_details}")
266
+
267
+ # Truncate potentially large fields before returning to user/LLM
268
+ for key in ["details", "response_body"]:
269
+ if (
270
+ isinstance(error_details.get(key), str)
271
+ and len(error_details[key]) > 500
272
+ ):
273
+ error_details[key] = error_details[key][:500] + "... (truncated)"
274
+
275
+ return json.dumps(error_details, indent=2)
276
+
277
+ async def _handle_validation_error(
278
+ self, e: ValidationError, query: str, data: Any
279
+ ) -> str:
280
+ """Formats validation error details into a JSON string."""
281
+ logger.error(
282
+ f"Failed to validate DexScreener response structure for query '{query}'. Error: {e}. Raw data length: {len(str(data))}",
283
+ exc_info=True,
284
+ )
285
+ # Avoid sending potentially huge raw data back
286
+ return json.dumps(
287
+ {
288
+ "error": "Failed to parse successful DexScreener API response",
289
+ "error_type": "validation_error",
290
+ "details": e.errors(),
291
+ },
292
+ indent=2,
293
+ )
294
+
295
+ async def _handle_unexpected_runtime_error(self, e: Exception, query: str) -> str:
296
+ """Formats unexpected runtime exception details into a JSON string."""
297
+ logger.exception(
298
+ f"An unexpected runtime error occurred in search_token tool _arun method for query '{query}': {e}"
299
+ )
300
+ return json.dumps(
301
+ {
302
+ "error": "An unexpected internal error occurred processing the search request",
303
+ "error_type": "runtime_error",
304
+ "details": str(e),
305
+ },
306
+ indent=2,
307
+ )
308
+
309
+ async def _no_pairs_found_response(
310
+ self, query: str, reason: str = "returned no matching pairs"
311
+ ) -> str:
312
+ """Generates the standard 'no pairs found' JSON response."""
313
+ logger.info(f"DexScreener search for query '{query}': {reason}.")
314
+ return json.dumps(
315
+ {
316
+ "message": f"No matching pairs found for the query '{query}'. Reason: {reason}.",
317
+ "query": query,
318
+ "pairs": [],
319
+ },
320
+ indent=2,
321
+ )