intentkit 0.6.13.dev2__py3-none-any.whl → 0.8.17__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 (385) hide show
  1. intentkit/__init__.py +1 -1
  2. intentkit/abstracts/agent.py +4 -5
  3. intentkit/abstracts/engine.py +5 -5
  4. intentkit/abstracts/graph.py +14 -7
  5. intentkit/abstracts/skill.py +6 -144
  6. intentkit/abstracts/twitter.py +4 -5
  7. intentkit/clients/__init__.py +5 -2
  8. intentkit/clients/cdp.py +101 -141
  9. intentkit/clients/twitter.py +83 -62
  10. intentkit/clients/web3.py +29 -0
  11. intentkit/config/config.py +8 -5
  12. intentkit/core/agent.py +472 -195
  13. intentkit/core/asset.py +253 -0
  14. intentkit/core/chat.py +51 -0
  15. intentkit/core/client.py +1 -1
  16. intentkit/core/credit.py +460 -130
  17. intentkit/core/engine.py +262 -233
  18. intentkit/core/node.py +15 -16
  19. intentkit/core/prompt.py +62 -28
  20. intentkit/core/scheduler.py +92 -0
  21. intentkit/core/statistics.py +168 -0
  22. intentkit/models/agent.py +1096 -949
  23. intentkit/models/agent_data.py +68 -38
  24. intentkit/models/agent_public.json +98 -0
  25. intentkit/models/agent_schema.json +54 -439
  26. intentkit/models/app_setting.py +96 -33
  27. intentkit/models/chat.py +74 -27
  28. intentkit/models/conversation.py +8 -8
  29. intentkit/models/credit.py +362 -74
  30. intentkit/models/db.py +26 -8
  31. intentkit/models/db_mig.py +2 -2
  32. intentkit/models/llm.csv +28 -0
  33. intentkit/models/llm.py +185 -350
  34. intentkit/models/redis.py +6 -4
  35. intentkit/models/skill.py +186 -72
  36. intentkit/models/skills.csv +174 -0
  37. intentkit/models/user.py +82 -24
  38. intentkit/skills/acolyt/__init__.py +2 -9
  39. intentkit/skills/acolyt/ask.py +3 -4
  40. intentkit/skills/acolyt/base.py +4 -9
  41. intentkit/skills/acolyt/schema.json +4 -3
  42. intentkit/skills/aixbt/__init__.py +2 -13
  43. intentkit/skills/aixbt/base.py +1 -7
  44. intentkit/skills/aixbt/projects.py +14 -15
  45. intentkit/skills/aixbt/schema.json +4 -4
  46. intentkit/skills/allora/__init__.py +2 -9
  47. intentkit/skills/allora/base.py +4 -9
  48. intentkit/skills/allora/price.py +3 -4
  49. intentkit/skills/allora/schema.json +3 -2
  50. intentkit/skills/base.py +248 -85
  51. intentkit/skills/basename/__init__.py +51 -0
  52. intentkit/skills/basename/base.py +11 -0
  53. intentkit/skills/basename/basename.svg +11 -0
  54. intentkit/skills/basename/schema.json +58 -0
  55. intentkit/skills/carv/__init__.py +115 -121
  56. intentkit/skills/carv/base.py +184 -185
  57. intentkit/skills/carv/fetch_news.py +3 -3
  58. intentkit/skills/carv/onchain_query.py +4 -4
  59. intentkit/skills/carv/schema.json +134 -137
  60. intentkit/skills/carv/token_info_and_price.py +5 -5
  61. intentkit/skills/casino/README.md +254 -0
  62. intentkit/skills/casino/__init__.py +86 -0
  63. intentkit/skills/casino/base.py +17 -0
  64. intentkit/skills/casino/casino.png +0 -0
  65. intentkit/skills/casino/deck_draw.py +127 -0
  66. intentkit/skills/casino/deck_shuffle.py +118 -0
  67. intentkit/skills/casino/dice_roll.py +100 -0
  68. intentkit/skills/casino/schema.json +77 -0
  69. intentkit/skills/casino/utils.py +107 -0
  70. intentkit/skills/cdp/__init__.py +22 -84
  71. intentkit/skills/cdp/base.py +1 -7
  72. intentkit/skills/cdp/schema.json +11 -314
  73. intentkit/skills/chainlist/__init__.py +2 -7
  74. intentkit/skills/chainlist/base.py +1 -7
  75. intentkit/skills/chainlist/chain_lookup.py +18 -18
  76. intentkit/skills/chainlist/schema.json +3 -5
  77. intentkit/skills/common/__init__.py +2 -9
  78. intentkit/skills/common/base.py +1 -7
  79. intentkit/skills/common/current_time.py +1 -2
  80. intentkit/skills/common/schema.json +2 -2
  81. intentkit/skills/cookiefun/__init__.py +6 -9
  82. intentkit/skills/cookiefun/base.py +2 -7
  83. intentkit/skills/cookiefun/get_account_details.py +7 -7
  84. intentkit/skills/cookiefun/get_account_feed.py +19 -19
  85. intentkit/skills/cookiefun/get_account_smart_followers.py +7 -7
  86. intentkit/skills/cookiefun/get_sectors.py +3 -3
  87. intentkit/skills/cookiefun/schema.json +1 -3
  88. intentkit/skills/cookiefun/search_accounts.py +9 -9
  89. intentkit/skills/cryptocompare/__init__.py +7 -24
  90. intentkit/skills/cryptocompare/api.py +2 -3
  91. intentkit/skills/cryptocompare/base.py +11 -25
  92. intentkit/skills/cryptocompare/fetch_news.py +4 -5
  93. intentkit/skills/cryptocompare/fetch_price.py +6 -7
  94. intentkit/skills/cryptocompare/fetch_top_exchanges.py +4 -5
  95. intentkit/skills/cryptocompare/fetch_top_market_cap.py +4 -5
  96. intentkit/skills/cryptocompare/fetch_top_volume.py +4 -5
  97. intentkit/skills/cryptocompare/fetch_trading_signals.py +5 -6
  98. intentkit/skills/cryptocompare/schema.json +3 -3
  99. intentkit/skills/cryptopanic/__init__.py +7 -10
  100. intentkit/skills/cryptopanic/base.py +51 -55
  101. intentkit/skills/cryptopanic/fetch_crypto_news.py +4 -8
  102. intentkit/skills/cryptopanic/fetch_crypto_sentiment.py +5 -7
  103. intentkit/skills/cryptopanic/schema.json +105 -103
  104. intentkit/skills/dapplooker/__init__.py +2 -9
  105. intentkit/skills/dapplooker/base.py +4 -9
  106. intentkit/skills/dapplooker/dapplooker_token_data.py +7 -7
  107. intentkit/skills/dapplooker/schema.json +3 -5
  108. intentkit/skills/defillama/__init__.py +24 -74
  109. intentkit/skills/defillama/api.py +6 -9
  110. intentkit/skills/defillama/base.py +11 -21
  111. intentkit/skills/defillama/coins/fetch_batch_historical_prices.py +8 -10
  112. intentkit/skills/defillama/coins/fetch_block.py +6 -8
  113. intentkit/skills/defillama/coins/fetch_current_prices.py +8 -10
  114. intentkit/skills/defillama/coins/fetch_first_price.py +7 -9
  115. intentkit/skills/defillama/coins/fetch_historical_prices.py +9 -11
  116. intentkit/skills/defillama/coins/fetch_price_chart.py +9 -11
  117. intentkit/skills/defillama/coins/fetch_price_percentage.py +7 -9
  118. intentkit/skills/defillama/config/chains.py +1 -3
  119. intentkit/skills/defillama/fees/fetch_fees_overview.py +24 -26
  120. intentkit/skills/defillama/schema.json +5 -1
  121. intentkit/skills/defillama/stablecoins/fetch_stablecoin_chains.py +16 -18
  122. intentkit/skills/defillama/stablecoins/fetch_stablecoin_charts.py +8 -10
  123. intentkit/skills/defillama/stablecoins/fetch_stablecoin_prices.py +5 -7
  124. intentkit/skills/defillama/stablecoins/fetch_stablecoins.py +7 -9
  125. intentkit/skills/defillama/tests/api_integration.test.py +1 -1
  126. intentkit/skills/defillama/tvl/fetch_chain_historical_tvl.py +4 -6
  127. intentkit/skills/defillama/tvl/fetch_chains.py +9 -11
  128. intentkit/skills/defillama/tvl/fetch_historical_tvl.py +4 -6
  129. intentkit/skills/defillama/tvl/fetch_protocol.py +32 -38
  130. intentkit/skills/defillama/tvl/fetch_protocol_current_tvl.py +3 -5
  131. intentkit/skills/defillama/tvl/fetch_protocols.py +37 -45
  132. intentkit/skills/defillama/volumes/fetch_dex_overview.py +42 -48
  133. intentkit/skills/defillama/volumes/fetch_dex_summary.py +35 -37
  134. intentkit/skills/defillama/volumes/fetch_options_overview.py +24 -28
  135. intentkit/skills/defillama/yields/fetch_pool_chart.py +10 -12
  136. intentkit/skills/defillama/yields/fetch_pools.py +26 -30
  137. intentkit/skills/dexscreener/README.md +154 -0
  138. intentkit/skills/dexscreener/__init__.py +97 -93
  139. intentkit/skills/dexscreener/base.py +125 -133
  140. intentkit/skills/dexscreener/get_pair_info.py +158 -0
  141. intentkit/skills/dexscreener/get_token_pairs.py +165 -0
  142. intentkit/skills/dexscreener/get_tokens_info.py +212 -0
  143. intentkit/skills/dexscreener/model/search_token_response.py +80 -82
  144. intentkit/skills/dexscreener/schema.json +91 -48
  145. intentkit/skills/dexscreener/search_token.py +182 -321
  146. intentkit/skills/dexscreener/utils.py +420 -0
  147. intentkit/skills/dune_analytics/__init__.py +7 -9
  148. intentkit/skills/dune_analytics/base.py +48 -52
  149. intentkit/skills/dune_analytics/fetch_kol_buys.py +5 -7
  150. intentkit/skills/dune_analytics/fetch_nation_metrics.py +6 -8
  151. intentkit/skills/dune_analytics/schema.json +104 -99
  152. intentkit/skills/elfa/__init__.py +5 -18
  153. intentkit/skills/elfa/base.py +10 -14
  154. intentkit/skills/elfa/mention.py +19 -21
  155. intentkit/skills/elfa/schema.json +3 -2
  156. intentkit/skills/elfa/stats.py +4 -4
  157. intentkit/skills/elfa/tokens.py +12 -12
  158. intentkit/skills/elfa/utils.py +26 -28
  159. intentkit/skills/enso/__init__.py +11 -31
  160. intentkit/skills/enso/base.py +50 -35
  161. intentkit/skills/enso/best_yield.py +16 -24
  162. intentkit/skills/enso/networks.py +6 -11
  163. intentkit/skills/enso/prices.py +11 -13
  164. intentkit/skills/enso/route.py +34 -38
  165. intentkit/skills/enso/schema.json +3 -2
  166. intentkit/skills/enso/tokens.py +29 -38
  167. intentkit/skills/enso/wallet.py +76 -191
  168. intentkit/skills/erc20/__init__.py +50 -0
  169. intentkit/skills/erc20/base.py +11 -0
  170. intentkit/skills/erc20/erc20.svg +5 -0
  171. intentkit/skills/erc20/schema.json +74 -0
  172. intentkit/skills/erc721/__init__.py +53 -0
  173. intentkit/skills/erc721/base.py +11 -0
  174. intentkit/skills/erc721/erc721.svg +5 -0
  175. intentkit/skills/erc721/schema.json +90 -0
  176. intentkit/skills/firecrawl/README.md +11 -5
  177. intentkit/skills/firecrawl/__init__.py +5 -18
  178. intentkit/skills/firecrawl/base.py +4 -11
  179. intentkit/skills/firecrawl/clear.py +4 -8
  180. intentkit/skills/firecrawl/crawl.py +19 -19
  181. intentkit/skills/firecrawl/query.py +4 -3
  182. intentkit/skills/firecrawl/schema.json +6 -8
  183. intentkit/skills/firecrawl/scrape.py +150 -40
  184. intentkit/skills/firecrawl/utils.py +50 -42
  185. intentkit/skills/github/__init__.py +2 -7
  186. intentkit/skills/github/base.py +1 -7
  187. intentkit/skills/github/github_search.py +1 -2
  188. intentkit/skills/github/schema.json +3 -4
  189. intentkit/skills/heurist/__init__.py +8 -27
  190. intentkit/skills/heurist/base.py +4 -9
  191. intentkit/skills/heurist/image_generation_animagine_xl.py +12 -13
  192. intentkit/skills/heurist/image_generation_arthemy_comics.py +12 -13
  193. intentkit/skills/heurist/image_generation_arthemy_real.py +12 -13
  194. intentkit/skills/heurist/image_generation_braindance.py +12 -13
  195. intentkit/skills/heurist/image_generation_cyber_realistic_xl.py +12 -13
  196. intentkit/skills/heurist/image_generation_flux_1_dev.py +12 -13
  197. intentkit/skills/heurist/image_generation_sdxl.py +12 -13
  198. intentkit/skills/heurist/schema.json +2 -2
  199. intentkit/skills/http/__init__.py +4 -15
  200. intentkit/skills/http/base.py +1 -7
  201. intentkit/skills/http/get.py +21 -16
  202. intentkit/skills/http/post.py +23 -18
  203. intentkit/skills/http/put.py +23 -18
  204. intentkit/skills/http/schema.json +4 -5
  205. intentkit/skills/lifi/__init__.py +8 -13
  206. intentkit/skills/lifi/base.py +1 -7
  207. intentkit/skills/lifi/schema.json +17 -8
  208. intentkit/skills/lifi/token_execute.py +36 -30
  209. intentkit/skills/lifi/token_quote.py +8 -10
  210. intentkit/skills/lifi/utils.py +104 -51
  211. intentkit/skills/moralis/__init__.py +6 -10
  212. intentkit/skills/moralis/api.py +6 -7
  213. intentkit/skills/moralis/base.py +5 -10
  214. intentkit/skills/moralis/fetch_chain_portfolio.py +10 -11
  215. intentkit/skills/moralis/fetch_nft_portfolio.py +22 -22
  216. intentkit/skills/moralis/fetch_solana_portfolio.py +11 -12
  217. intentkit/skills/moralis/fetch_wallet_portfolio.py +8 -9
  218. intentkit/skills/moralis/schema.json +7 -2
  219. intentkit/skills/morpho/__init__.py +52 -0
  220. intentkit/skills/morpho/base.py +11 -0
  221. intentkit/skills/morpho/morpho.svg +12 -0
  222. intentkit/skills/morpho/schema.json +73 -0
  223. intentkit/skills/nation/__init__.py +4 -9
  224. intentkit/skills/nation/base.py +5 -10
  225. intentkit/skills/nation/nft_check.py +3 -4
  226. intentkit/skills/nation/schema.json +4 -3
  227. intentkit/skills/onchain.py +23 -0
  228. intentkit/skills/openai/__init__.py +17 -18
  229. intentkit/skills/openai/base.py +10 -14
  230. intentkit/skills/openai/dalle_image_generation.py +3 -8
  231. intentkit/skills/openai/gpt_avatar_generator.py +102 -0
  232. intentkit/skills/openai/gpt_image_generation.py +4 -8
  233. intentkit/skills/openai/gpt_image_mini_generator.py +91 -0
  234. intentkit/skills/openai/gpt_image_to_image.py +4 -8
  235. intentkit/skills/openai/image_to_text.py +3 -7
  236. intentkit/skills/openai/schema.json +34 -3
  237. intentkit/skills/portfolio/__init__.py +11 -35
  238. intentkit/skills/portfolio/base.py +33 -19
  239. intentkit/skills/portfolio/schema.json +3 -5
  240. intentkit/skills/portfolio/token_balances.py +21 -21
  241. intentkit/skills/portfolio/wallet_approvals.py +17 -18
  242. intentkit/skills/portfolio/wallet_defi_positions.py +3 -3
  243. intentkit/skills/portfolio/wallet_history.py +31 -31
  244. intentkit/skills/portfolio/wallet_net_worth.py +13 -13
  245. intentkit/skills/portfolio/wallet_nfts.py +19 -19
  246. intentkit/skills/portfolio/wallet_profitability.py +18 -18
  247. intentkit/skills/portfolio/wallet_profitability_summary.py +5 -5
  248. intentkit/skills/portfolio/wallet_stats.py +3 -3
  249. intentkit/skills/portfolio/wallet_swaps.py +19 -19
  250. intentkit/skills/pyth/__init__.py +50 -0
  251. intentkit/skills/pyth/base.py +11 -0
  252. intentkit/skills/pyth/pyth.svg +6 -0
  253. intentkit/skills/pyth/schema.json +75 -0
  254. intentkit/skills/skills.toml +40 -0
  255. intentkit/skills/slack/__init__.py +5 -17
  256. intentkit/skills/slack/base.py +3 -9
  257. intentkit/skills/slack/get_channel.py +8 -8
  258. intentkit/skills/slack/get_message.py +9 -9
  259. intentkit/skills/slack/schedule_message.py +5 -5
  260. intentkit/skills/slack/schema.json +2 -2
  261. intentkit/skills/slack/send_message.py +3 -5
  262. intentkit/skills/supabase/__init__.py +7 -23
  263. intentkit/skills/supabase/base.py +9 -13
  264. intentkit/skills/supabase/delete_data.py +5 -6
  265. intentkit/skills/supabase/fetch_data.py +13 -14
  266. intentkit/skills/supabase/insert_data.py +5 -6
  267. intentkit/skills/supabase/invoke_function.py +7 -8
  268. intentkit/skills/supabase/schema.json +2 -3
  269. intentkit/skills/supabase/update_data.py +7 -8
  270. intentkit/skills/supabase/upsert_data.py +5 -6
  271. intentkit/skills/superfluid/__init__.py +53 -0
  272. intentkit/skills/superfluid/base.py +11 -0
  273. intentkit/skills/superfluid/schema.json +89 -0
  274. intentkit/skills/superfluid/superfluid.svg +6 -0
  275. intentkit/skills/system/__init__.py +7 -24
  276. intentkit/skills/system/add_autonomous_task.py +10 -12
  277. intentkit/skills/system/delete_autonomous_task.py +2 -2
  278. intentkit/skills/system/edit_autonomous_task.py +14 -18
  279. intentkit/skills/system/list_autonomous_tasks.py +3 -5
  280. intentkit/skills/system/read_agent_api_key.py +6 -4
  281. intentkit/skills/system/regenerate_agent_api_key.py +6 -4
  282. intentkit/skills/system/schema.json +6 -8
  283. intentkit/skills/tavily/__init__.py +3 -12
  284. intentkit/skills/tavily/base.py +4 -9
  285. intentkit/skills/tavily/schema.json +3 -5
  286. intentkit/skills/tavily/tavily_extract.py +2 -4
  287. intentkit/skills/tavily/tavily_search.py +4 -6
  288. intentkit/skills/token/__init__.py +5 -10
  289. intentkit/skills/token/base.py +7 -11
  290. intentkit/skills/token/erc20_transfers.py +19 -19
  291. intentkit/skills/token/schema.json +3 -6
  292. intentkit/skills/token/token_analytics.py +3 -3
  293. intentkit/skills/token/token_price.py +13 -13
  294. intentkit/skills/token/token_search.py +9 -9
  295. intentkit/skills/twitter/__init__.py +11 -35
  296. intentkit/skills/twitter/base.py +23 -35
  297. intentkit/skills/twitter/follow_user.py +3 -7
  298. intentkit/skills/twitter/get_mentions.py +6 -13
  299. intentkit/skills/twitter/get_timeline.py +5 -13
  300. intentkit/skills/twitter/get_user_by_username.py +3 -7
  301. intentkit/skills/twitter/get_user_tweets.py +6 -14
  302. intentkit/skills/twitter/like_tweet.py +3 -7
  303. intentkit/skills/twitter/post_tweet.py +23 -12
  304. intentkit/skills/twitter/reply_tweet.py +21 -12
  305. intentkit/skills/twitter/retweet.py +3 -7
  306. intentkit/skills/twitter/schema.json +1 -0
  307. intentkit/skills/twitter/search_tweets.py +5 -13
  308. intentkit/skills/unrealspeech/__init__.py +2 -7
  309. intentkit/skills/unrealspeech/base.py +2 -8
  310. intentkit/skills/unrealspeech/schema.json +2 -5
  311. intentkit/skills/unrealspeech/text_to_speech.py +8 -8
  312. intentkit/skills/venice_audio/__init__.py +98 -106
  313. intentkit/skills/venice_audio/base.py +117 -121
  314. intentkit/skills/venice_audio/input.py +41 -41
  315. intentkit/skills/venice_audio/schema.json +151 -152
  316. intentkit/skills/venice_audio/venice_audio.py +38 -21
  317. intentkit/skills/venice_image/__init__.py +147 -154
  318. intentkit/skills/venice_image/api.py +138 -138
  319. intentkit/skills/venice_image/base.py +185 -192
  320. intentkit/skills/venice_image/config.py +33 -35
  321. intentkit/skills/venice_image/image_enhance/image_enhance.py +2 -3
  322. intentkit/skills/venice_image/image_enhance/image_enhance_base.py +21 -23
  323. intentkit/skills/venice_image/image_enhance/image_enhance_input.py +38 -40
  324. intentkit/skills/venice_image/image_generation/image_generation_base.py +9 -9
  325. intentkit/skills/venice_image/image_generation/image_generation_fluently_xl.py +26 -26
  326. intentkit/skills/venice_image/image_generation/image_generation_flux_dev.py +27 -27
  327. intentkit/skills/venice_image/image_generation/image_generation_flux_dev_uncensored.py +26 -26
  328. intentkit/skills/venice_image/image_generation/image_generation_input.py +158 -158
  329. intentkit/skills/venice_image/image_generation/image_generation_lustify_sdxl.py +26 -26
  330. intentkit/skills/venice_image/image_generation/image_generation_pony_realism.py +26 -26
  331. intentkit/skills/venice_image/image_generation/image_generation_stable_diffusion_3_5.py +28 -28
  332. intentkit/skills/venice_image/image_generation/image_generation_venice_sd35.py +28 -28
  333. intentkit/skills/venice_image/image_upscale/image_upscale.py +3 -3
  334. intentkit/skills/venice_image/image_upscale/image_upscale_base.py +21 -23
  335. intentkit/skills/venice_image/image_upscale/image_upscale_input.py +22 -22
  336. intentkit/skills/venice_image/image_vision/image_vision.py +2 -2
  337. intentkit/skills/venice_image/image_vision/image_vision_base.py +17 -17
  338. intentkit/skills/venice_image/image_vision/image_vision_input.py +9 -9
  339. intentkit/skills/venice_image/schema.json +267 -267
  340. intentkit/skills/venice_image/utils.py +77 -78
  341. intentkit/skills/web_scraper/__init__.py +5 -18
  342. intentkit/skills/web_scraper/base.py +21 -7
  343. intentkit/skills/web_scraper/document_indexer.py +7 -6
  344. intentkit/skills/web_scraper/schema.json +2 -6
  345. intentkit/skills/web_scraper/scrape_and_index.py +15 -15
  346. intentkit/skills/web_scraper/utils.py +62 -63
  347. intentkit/skills/web_scraper/website_indexer.py +17 -19
  348. intentkit/skills/weth/__init__.py +49 -0
  349. intentkit/skills/weth/base.py +11 -0
  350. intentkit/skills/weth/schema.json +58 -0
  351. intentkit/skills/weth/weth.svg +6 -0
  352. intentkit/skills/wow/__init__.py +51 -0
  353. intentkit/skills/wow/base.py +11 -0
  354. intentkit/skills/wow/schema.json +89 -0
  355. intentkit/skills/wow/wow.svg +7 -0
  356. intentkit/skills/x402/__init__.py +61 -0
  357. intentkit/skills/x402/ask_agent.py +98 -0
  358. intentkit/skills/x402/base.py +99 -0
  359. intentkit/skills/x402/http_request.py +117 -0
  360. intentkit/skills/x402/schema.json +45 -0
  361. intentkit/skills/x402/x402.webp +0 -0
  362. intentkit/skills/xmtp/__init__.py +4 -15
  363. intentkit/skills/xmtp/base.py +61 -2
  364. intentkit/skills/xmtp/price.py +18 -13
  365. intentkit/skills/xmtp/schema.json +69 -71
  366. intentkit/skills/xmtp/swap.py +22 -25
  367. intentkit/skills/xmtp/transfer.py +71 -32
  368. intentkit/utils/chain.py +3 -3
  369. intentkit/utils/error.py +14 -1
  370. intentkit/utils/logging.py +2 -4
  371. intentkit/utils/s3.py +59 -7
  372. intentkit/utils/schema.py +100 -0
  373. intentkit/utils/slack_alert.py +7 -8
  374. {intentkit-0.6.13.dev2.dist-info → intentkit-0.8.17.dist-info}/METADATA +14 -16
  375. intentkit-0.8.17.dist-info/RECORD +466 -0
  376. intentkit/abstracts/exception.py +0 -9
  377. intentkit/core/skill.py +0 -200
  378. intentkit/models/generator.py +0 -347
  379. intentkit/skills/cdp/get_balance.py +0 -110
  380. intentkit/skills/cdp/swap.py +0 -121
  381. intentkit/skills/moralis/tests/__init__.py +0 -0
  382. intentkit/skills/moralis/tests/test_wallet.py +0 -511
  383. intentkit-0.6.13.dev2.dist-info/RECORD +0 -409
  384. {intentkit-0.6.13.dev2.dist-info → intentkit-0.8.17.dist-info}/WHEEL +0 -0
  385. {intentkit-0.6.13.dev2.dist-info → intentkit-0.8.17.dist-info}/licenses/LICENSE +0 -0
intentkit/models/agent.py CHANGED
@@ -1,25 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
1
4
  import json
2
5
  import logging
3
6
  import re
4
7
  import textwrap
5
- from datetime import datetime, timezone
8
+ from datetime import UTC, datetime
6
9
  from decimal import Decimal
7
10
  from pathlib import Path
8
- from typing import Annotated, Any, Dict, List, Literal, Optional
11
+ from typing import Annotated, Any, Literal
9
12
 
10
13
  import jsonref
11
14
  import yaml
12
15
  from cron_validator import CronValidator
13
16
  from epyxid import XID
14
- from fastapi import HTTPException
15
17
  from intentkit.models.agent_data import AgentData
16
18
  from intentkit.models.base import Base
19
+ from intentkit.models.credit import CreditAccount
17
20
  from intentkit.models.db import get_session
18
- from intentkit.models.llm import LLMModelInfo, LLMModelInfoTable, LLMProvider
19
- from intentkit.models.skill import SkillTable
20
- from pydantic import BaseModel, ConfigDict, field_validator, model_validator
21
+ from intentkit.models.llm import LLMModelInfo, LLMProvider
22
+ from intentkit.models.skill import Skill
23
+ from intentkit.utils.error import IntentKitAPIError
24
+ from pydantic import BaseModel, ConfigDict, field_validator
21
25
  from pydantic import Field as PydanticField
22
26
  from pydantic.json_schema import SkipJsonSchema
27
+ from pydantic.main import IncEx
23
28
  from sqlalchemy import (
24
29
  Boolean,
25
30
  Column,
@@ -31,6 +36,7 @@ from sqlalchemy import (
31
36
  select,
32
37
  )
33
38
  from sqlalchemy.dialects.postgresql import JSON, JSONB
39
+ from sqlalchemy.exc import IntegrityError
34
40
  from sqlalchemy.ext.asyncio import AsyncSession
35
41
 
36
42
  logger = logging.getLogger(__name__)
@@ -53,7 +59,7 @@ class AgentAutonomous(BaseModel):
53
59
  ),
54
60
  ]
55
61
  name: Annotated[
56
- Optional[str],
62
+ str | None,
57
63
  PydanticField(
58
64
  default=None,
59
65
  description="Display name of the autonomous configuration",
@@ -64,7 +70,7 @@ class AgentAutonomous(BaseModel):
64
70
  ),
65
71
  ]
66
72
  description: Annotated[
67
- Optional[str],
73
+ str | None,
68
74
  PydanticField(
69
75
  default=None,
70
76
  description="Description of the autonomous configuration",
@@ -75,7 +81,7 @@ class AgentAutonomous(BaseModel):
75
81
  ),
76
82
  ]
77
83
  minutes: Annotated[
78
- Optional[int],
84
+ int | None,
79
85
  PydanticField(
80
86
  default=None,
81
87
  description="Interval in minutes between operations, mutually exclusive with cron",
@@ -85,7 +91,7 @@ class AgentAutonomous(BaseModel):
85
91
  ),
86
92
  ]
87
93
  cron: Annotated[
88
- Optional[str],
94
+ str | None,
89
95
  PydanticField(
90
96
  default=None,
91
97
  description="Cron expression for scheduling operations, mutually exclusive with minutes",
@@ -105,7 +111,7 @@ class AgentAutonomous(BaseModel):
105
111
  ),
106
112
  ]
107
113
  enabled: Annotated[
108
- Optional[bool],
114
+ bool | None,
109
115
  PydanticField(
110
116
  default=False,
111
117
  description="Whether the autonomous configuration is enabled",
@@ -128,33 +134,6 @@ class AgentAutonomous(BaseModel):
128
134
  )
129
135
  return v
130
136
 
131
- @field_validator("name")
132
- @classmethod
133
- def validate_name(cls, v: Optional[str]) -> Optional[str]:
134
- if v is not None and len(v.encode()) > 50:
135
- raise ValueError("name must be at most 50 bytes")
136
- return v
137
-
138
- @field_validator("description")
139
- @classmethod
140
- def validate_description(cls, v: Optional[str]) -> Optional[str]:
141
- if v is not None and len(v.encode()) > 200:
142
- raise ValueError("description must be at most 200 bytes")
143
- return v
144
-
145
- @field_validator("prompt")
146
- @classmethod
147
- def validate_prompt(cls, v: Optional[str]) -> Optional[str]:
148
- if v is not None and len(v.encode()) > 20000:
149
- raise ValueError("prompt must be at most 20000 bytes")
150
- return v
151
-
152
- @model_validator(mode="after")
153
- def validate_schedule(self) -> "AgentAutonomous":
154
- # This validator is kept for backward compatibility
155
- # The actual validation now happens in AgentUpdate.validate_autonomous_schedule
156
- return self
157
-
158
137
 
159
138
  class AgentExample(BaseModel):
160
139
  """Agent example configuration."""
@@ -165,7 +144,7 @@ class AgentExample(BaseModel):
165
144
  description="Name of the example",
166
145
  max_length=50,
167
146
  json_schema_extra={
168
- "x-group": "examples",
147
+ "x-placeholder": "Add a name for the example",
169
148
  },
170
149
  ),
171
150
  ]
@@ -175,7 +154,7 @@ class AgentExample(BaseModel):
175
154
  description="Description of the example",
176
155
  max_length=200,
177
156
  json_schema_extra={
178
- "x-group": "examples",
157
+ "x-placeholder": "Add a short description for the example",
179
158
  },
180
159
  ),
181
160
  ]
@@ -185,72 +164,28 @@ class AgentExample(BaseModel):
185
164
  description="Example prompt",
186
165
  max_length=2000,
187
166
  json_schema_extra={
188
- "x-group": "examples",
167
+ "x-placeholder": "The prompt will be sent to the agent",
189
168
  },
190
169
  ),
191
170
  ]
192
171
 
193
172
 
194
- class AgentTable(Base):
195
- """Agent table db model."""
173
+ class AgentUserInputColumns:
174
+ """Abstract base class containing columns that are common to AgentTable and other tables."""
196
175
 
197
- __tablename__ = "agents"
176
+ __abstract__ = True
198
177
 
199
- id = Column(
200
- String,
201
- primary_key=True,
202
- comment="Unique identifier for the agent. Must be URL-safe, containing only lowercase letters, numbers, and hyphens",
203
- )
178
+ # Basic information fields from AgentCore
204
179
  name = Column(
205
180
  String,
206
181
  nullable=True,
207
182
  comment="Display name of the agent",
208
183
  )
209
- slug = Column(
210
- String,
211
- nullable=True,
212
- comment="Slug of the agent, used for URL generation",
213
- )
214
- description = Column(
215
- String,
216
- nullable=True,
217
- comment="Description of the agent, for public view, not contained in prompt",
218
- )
219
- external_website = Column(
220
- String,
221
- nullable=True,
222
- comment="Link of external website of the agent, if you have one",
223
- )
224
184
  picture = Column(
225
185
  String,
226
186
  nullable=True,
227
187
  comment="Picture of the agent",
228
188
  )
229
- ticker = Column(
230
- String,
231
- nullable=True,
232
- comment="Ticker symbol of the agent",
233
- )
234
- token_address = Column(
235
- String,
236
- nullable=True,
237
- comment="Token address of the agent",
238
- )
239
- token_pool = Column(
240
- String,
241
- nullable=True,
242
- comment="Pool of the agent token",
243
- )
244
- mode = Column(
245
- String,
246
- nullable=True,
247
- comment="Mode of the agent, public or private",
248
- )
249
- fee_percentage = Column(
250
- Numeric(22, 4),
251
- nullable=True,
252
- comment="Fee percentage of the agent",
253
- )
254
189
  purpose = Column(
255
190
  String,
256
191
  nullable=True,
@@ -266,34 +201,8 @@ class AgentTable(Base):
266
201
  nullable=True,
267
202
  comment="Principles or values of the agent",
268
203
  )
269
- owner = Column(
270
- String,
271
- nullable=True,
272
- comment="Owner identifier of the agent, used for access control",
273
- )
274
- upstream_id = Column(
275
- String,
276
- index=True,
277
- nullable=True,
278
- comment="Upstream reference ID for idempotent operations",
279
- )
280
- upstream_extra = Column(
281
- JSON().with_variant(JSONB(), "postgresql"),
282
- nullable=True,
283
- comment="Additional data store for upstream use",
284
- )
285
- wallet_provider = Column(
286
- String,
287
- nullable=True,
288
- comment="Provider of the agent's wallet",
289
- )
290
- network_id = Column(
291
- String,
292
- nullable=True,
293
- default="base-mainnet",
294
- comment="Network identifier",
295
- )
296
- # AI part
204
+
205
+ # AI model configuration fields from AgentCore
297
206
  model = Column(
298
207
  String,
299
208
  nullable=True,
@@ -328,81 +237,192 @@ class AgentTable(Base):
328
237
  default=0.0,
329
238
  comment="Controls topic adherence (-2.0~2.0). Higher values allow more topic deviation, lower values enforce stricter topic adherence.",
330
239
  )
240
+
241
+ # Wallet and network configuration fields from AgentCore
242
+ wallet_provider = Column(
243
+ String,
244
+ nullable=True,
245
+ comment="Provider of the agent's wallet",
246
+ )
247
+ readonly_wallet_address = Column(
248
+ String,
249
+ nullable=True,
250
+ comment="Readonly wallet address of the agent",
251
+ )
252
+ network_id = Column(
253
+ String,
254
+ nullable=True,
255
+ default="base-mainnet",
256
+ comment="Network identifier",
257
+ )
258
+
259
+ # Skills configuration from AgentCore
260
+ skills = Column(
261
+ JSON().with_variant(JSONB(), "postgresql"),
262
+ nullable=True,
263
+ comment="Dict of skills and their corresponding configurations",
264
+ )
265
+
266
+ # Additional fields from AgentUserInput
331
267
  short_term_memory_strategy = Column(
332
268
  String,
333
269
  nullable=True,
334
270
  default="trim",
335
271
  comment="Strategy for managing short-term memory when context limit is reached. 'trim' removes oldest messages, 'summarize' creates summaries.",
336
272
  )
337
- # autonomous mode
338
273
  autonomous = Column(
339
274
  JSON().with_variant(JSONB(), "postgresql"),
340
275
  nullable=True,
341
276
  comment="Autonomous agent configurations",
342
277
  )
343
- # agent examples
344
- example_intro = Column(
278
+ telegram_entrypoint_enabled = Column(
279
+ Boolean,
280
+ nullable=True,
281
+ default=False,
282
+ comment="Whether the agent can receive events from Telegram",
283
+ )
284
+ telegram_entrypoint_prompt = Column(
345
285
  String,
346
286
  nullable=True,
347
- comment="Introduction for example interactions",
287
+ comment="Extra prompt for telegram entrypoint",
348
288
  )
349
- examples = Column(
289
+ telegram_config = Column(
350
290
  JSON().with_variant(JSONB(), "postgresql"),
351
291
  nullable=True,
352
- comment="List of example interactions for the agent",
292
+ comment="Telegram integration configuration settings",
353
293
  )
354
- # skills
355
- skills = Column(
294
+ discord_entrypoint_enabled = Column(
295
+ Boolean,
296
+ nullable=True,
297
+ default=False,
298
+ comment="Whether the agent can receive events from Discord",
299
+ )
300
+ discord_config = Column(
356
301
  JSON().with_variant(JSONB(), "postgresql"),
357
302
  nullable=True,
358
- comment="Dict of skills and their corresponding configurations",
303
+ comment="Discord integration configuration settings",
359
304
  )
305
+ xmtp_entrypoint_prompt = Column(
306
+ String,
307
+ nullable=True,
308
+ comment="Extra prompt for xmtp entrypoint",
309
+ )
310
+
311
+
312
+ class AgentTable(Base, AgentUserInputColumns):
313
+ """Agent table db model."""
360
314
 
361
- cdp_network_id = Column(
315
+ __tablename__ = "agents"
316
+
317
+ id = Column(
318
+ String,
319
+ primary_key=True,
320
+ comment="Unique identifier for the agent. Must be URL-safe, containing only lowercase letters, numbers, and hyphens",
321
+ )
322
+ slug = Column(
362
323
  String,
363
324
  nullable=True,
364
- default="base-mainnet",
365
- comment="Network identifier for CDP integration",
325
+ comment="Slug of the agent, used for URL generation",
366
326
  )
367
- # if twitter_enabled, the twitter_entrypoint will be enabled, twitter_config will be checked
368
- twitter_entrypoint_enabled = Column(
369
- Boolean,
327
+ owner = Column(
328
+ String,
370
329
  nullable=True,
371
- default=False,
372
- comment="Dangerous, reply all mentions from x.com",
330
+ comment="Owner identifier of the agent, used for access control",
373
331
  )
374
- twitter_entrypoint_prompt = Column(
332
+ upstream_id = Column(
375
333
  String,
334
+ index=True,
376
335
  nullable=True,
377
- comment="Extra prompt for twitter entrypoint",
336
+ comment="Upstream reference ID for idempotent operations",
378
337
  )
379
- twitter_config = Column(
338
+ upstream_extra = Column(
380
339
  JSON().with_variant(JSONB(), "postgresql"),
381
340
  nullable=True,
382
- comment="You must use your own key for twitter entrypoint, it is separated from twitter skills",
341
+ comment="Additional data store for upstream use",
383
342
  )
384
- # if telegram_entrypoint_enabled, the telegram_entrypoint_enabled will be enabled, telegram_config will be checked
385
- telegram_entrypoint_enabled = Column(
386
- Boolean,
343
+ version = Column(
344
+ String,
387
345
  nullable=True,
388
- default=False,
389
- comment="Whether the agent can receive events from Telegram",
346
+ comment="Version hash of the agent",
390
347
  )
391
- telegram_entrypoint_prompt = Column(
392
- String,
348
+ statistics = Column(
349
+ JSON().with_variant(JSONB(), "postgresql"),
393
350
  nullable=True,
394
- comment="Extra prompt for telegram entrypoint",
351
+ comment="Statistics of the agent, update every 1 hour for query",
395
352
  )
396
- telegram_config = Column(
353
+ assets = Column(
397
354
  JSON().with_variant(JSONB(), "postgresql"),
398
355
  nullable=True,
399
- comment="Telegram integration configuration settings",
356
+ comment="Assets of the agent, update every 1 hour for query",
400
357
  )
401
- xmtp_entrypoint_prompt = Column(
358
+ account_snapshot = Column(
359
+ JSON().with_variant(JSONB(), "postgresql"),
360
+ nullable=True,
361
+ comment="Account snapshot of the agent, update every 1 hour for query",
362
+ )
363
+ extra = Column(
364
+ JSON().with_variant(JSONB(), "postgresql"),
365
+ nullable=True,
366
+ comment="Other helper data fields for query, come from agent and agent data",
367
+ )
368
+
369
+ # Fields moved from AgentUserInputColumns that are no longer in AgentUserInput
370
+ description = Column(
402
371
  String,
403
372
  nullable=True,
404
- comment="Extra prompt for xmtp entrypoint",
373
+ comment="Description of the agent, for public view, not contained in prompt",
374
+ )
375
+ external_website = Column(
376
+ String,
377
+ nullable=True,
378
+ comment="Link of external website of the agent, if you have one",
379
+ )
380
+ ticker = Column(
381
+ String,
382
+ nullable=True,
383
+ comment="Ticker symbol of the agent",
384
+ )
385
+ token_address = Column(
386
+ String,
387
+ nullable=True,
388
+ comment="Token address of the agent",
389
+ )
390
+ token_pool = Column(
391
+ String,
392
+ nullable=True,
393
+ comment="Pool of the agent token",
394
+ )
395
+ fee_percentage = Column(
396
+ Numeric(22, 4),
397
+ nullable=True,
398
+ comment="Fee percentage of the agent",
399
+ )
400
+ example_intro = Column(
401
+ String,
402
+ nullable=True,
403
+ comment="Introduction for example interactions",
404
+ )
405
+ examples = Column(
406
+ JSON().with_variant(JSONB(), "postgresql"),
407
+ nullable=True,
408
+ comment="List of example interactions for the agent",
409
+ )
410
+ public_extra = Column(
411
+ JSON().with_variant(JSONB(), "postgresql"),
412
+ nullable=True,
413
+ comment="Public extra data of the agent",
414
+ )
415
+ deployed_at = Column(
416
+ DateTime(timezone=True),
417
+ nullable=True,
418
+ comment="Timestamp when the agent was deployed",
405
419
  )
420
+ public_info_updated_at = Column(
421
+ DateTime(timezone=True),
422
+ nullable=True,
423
+ comment="Timestamp when the agent public info was last updated",
424
+ )
425
+
406
426
  # auto timestamp
407
427
  created_at = Column(
408
428
  DateTime(timezone=True),
@@ -414,218 +434,52 @@ class AgentTable(Base):
414
434
  DateTime(timezone=True),
415
435
  nullable=False,
416
436
  server_default=func.now(),
417
- onupdate=lambda: datetime.now(timezone.utc),
437
+ onupdate=lambda: datetime.now(UTC),
418
438
  comment="Timestamp when the agent was last updated",
419
439
  )
420
440
 
421
441
 
422
- class AgentUpdate(BaseModel):
423
- """Agent update model."""
424
-
425
- model_config = ConfigDict(
426
- title="Agent",
427
- from_attributes=True,
428
- json_schema_extra={
429
- "required": ["name", "purpose", "personality", "principles"],
430
- },
431
- )
442
+ class AgentCore(BaseModel):
443
+ """Agent core model."""
432
444
 
433
445
  name: Annotated[
434
- Optional[str],
446
+ str | None,
435
447
  PydanticField(
436
448
  default=None,
437
449
  title="Name",
438
450
  description="Display name of the agent",
439
451
  max_length=50,
440
- json_schema_extra={
441
- "x-group": "basic",
442
- "x-placeholder": "Name your agent",
443
- },
444
452
  ),
445
453
  ]
446
- slug: Annotated[
447
- Optional[str],
454
+ picture: Annotated[
455
+ str | None,
448
456
  PydanticField(
449
457
  default=None,
450
- description="Slug of the agent, used for URL generation",
451
- max_length=30,
452
- min_length=2,
453
- json_schema_extra={
454
- "x-group": "internal",
455
- "readOnly": True,
456
- },
458
+ description="Avatar of the agent",
457
459
  ),
458
460
  ]
459
- description: Annotated[
460
- Optional[str],
461
+ purpose: Annotated[
462
+ str | None,
461
463
  PydanticField(
462
464
  default=None,
463
- description="Description of the agent, for public view, not contained in prompt",
464
- json_schema_extra={
465
- "x-group": "basic",
466
- "x-placeholder": "Introduce your agent",
467
- },
465
+ description="Purpose or role of the agent",
466
+ max_length=20000,
468
467
  ),
469
468
  ]
470
- external_website: Annotated[
471
- Optional[str],
472
- PydanticField(
473
- default=None,
474
- description="Link of external website of the agent, if you have one",
475
- json_schema_extra={
476
- "x-group": "basic",
477
- "x-placeholder": "Enter agent external website url",
478
- "format": "uri",
479
- },
480
- ),
481
- ]
482
- picture: Annotated[
483
- Optional[str],
484
- PydanticField(
485
- default=None,
486
- description="Picture of the agent",
487
- json_schema_extra={
488
- "x-group": "experimental",
489
- "x-placeholder": "Upload a picture of your agent",
490
- },
491
- ),
492
- ]
493
- ticker: Annotated[
494
- Optional[str],
495
- PydanticField(
496
- default=None,
497
- description="Ticker symbol of the agent",
498
- max_length=10,
499
- min_length=1,
500
- json_schema_extra={
501
- "x-group": "basic",
502
- "x-placeholder": "If one day, your agent has it's own token, what will it be?",
503
- },
504
- ),
505
- ]
506
- token_address: Annotated[
507
- Optional[str],
508
- PydanticField(
509
- default=None,
510
- description="Token address of the agent",
511
- max_length=42,
512
- json_schema_extra={
513
- "x-group": "internal",
514
- "readOnly": True,
515
- },
516
- ),
517
- ]
518
- token_pool: Annotated[
519
- Optional[str],
520
- PydanticField(
521
- default=None,
522
- description="Pool of the agent token",
523
- max_length=42,
524
- json_schema_extra={
525
- "x-group": "internal",
526
- "readOnly": True,
527
- },
528
- ),
529
- ]
530
- mode: Annotated[
531
- Optional[Literal["public", "private"]],
532
- PydanticField(
533
- default=None,
534
- description="Mode of the agent, public or private",
535
- json_schema_extra={
536
- "x-group": "basic",
537
- },
538
- ),
539
- ]
540
- fee_percentage: Annotated[
541
- Optional[Decimal],
542
- PydanticField(
543
- default=None,
544
- description="Fee percentage of the agent",
545
- ge=Decimal("0.0"),
546
- json_schema_extra={
547
- "x-group": "basic",
548
- },
549
- ),
550
- ]
551
- purpose: Annotated[
552
- Optional[str],
553
- PydanticField(
554
- default=None,
555
- description="Purpose or role of the agent",
556
- max_length=20000,
557
- json_schema_extra={
558
- "x-group": "basic",
559
- "x-placeholder": "Enter agent purpose, it will be a part of the system prompt",
560
- "pattern": "^(([^#].*)|#[^# ].*|#{3,}[ ].*|$)(\n(([^#].*)|#[^# ].*|#{3,}[ ].*|$))*$",
561
- "errorMessage": {
562
- "pattern": "Level 1 and 2 headings (# and ##) are not allowed. Please use level 3+ headings (###, ####, etc.) instead."
563
- },
564
- },
565
- ),
566
- ]
567
- personality: Annotated[
568
- Optional[str],
469
+ personality: Annotated[
470
+ str | None,
569
471
  PydanticField(
570
472
  default=None,
571
473
  description="Personality traits of the agent",
572
474
  max_length=20000,
573
- json_schema_extra={
574
- "x-group": "basic",
575
- "x-placeholder": "Enter agent personality, it will be a part of the system prompt",
576
- "pattern": "^(([^#].*)|#[^# ].*|#{3,}[ ].*|$)(\n(([^#].*)|#[^# ].*|#{3,}[ ].*|$))*$",
577
- "errorMessage": {
578
- "pattern": "Level 1 and 2 headings (# and ##) are not allowed. Please use level 3+ headings (###, ####, etc.) instead."
579
- },
580
- },
581
475
  ),
582
476
  ]
583
477
  principles: Annotated[
584
- Optional[str],
478
+ str | None,
585
479
  PydanticField(
586
480
  default=None,
587
481
  description="Principles or values of the agent",
588
482
  max_length=20000,
589
- json_schema_extra={
590
- "x-group": "basic",
591
- "x-placeholder": "Enter agent principles, it will be a part of the system prompt",
592
- "pattern": "^(([^#].*)|#[^# ].*|#{3,}[ ].*|$)(\n(([^#].*)|#[^# ].*|#{3,}[ ].*|$))*$",
593
- "errorMessage": {
594
- "pattern": "Level 1 and 2 headings (# and ##) are not allowed. Please use level 3+ headings (###, ####, etc.) instead."
595
- },
596
- },
597
- ),
598
- ]
599
- owner: Annotated[
600
- Optional[str],
601
- PydanticField(
602
- default=None,
603
- description="Owner identifier of the agent, used for access control",
604
- max_length=50,
605
- json_schema_extra={
606
- "x-group": "internal",
607
- },
608
- ),
609
- ]
610
- upstream_id: Annotated[
611
- Optional[str],
612
- PydanticField(
613
- default=None,
614
- description="External reference ID for idempotent operations",
615
- max_length=100,
616
- json_schema_extra={
617
- "x-group": "internal",
618
- },
619
- ),
620
- ]
621
- upstream_extra: Annotated[
622
- Optional[Dict[str, Any]],
623
- PydanticField(
624
- default=None,
625
- description="Additional data store for upstream use",
626
- json_schema_extra={
627
- "x-group": "internal",
628
- },
629
483
  ),
630
484
  ]
631
485
  # AI part
@@ -633,91 +487,137 @@ class AgentUpdate(BaseModel):
633
487
  str,
634
488
  PydanticField(
635
489
  default="gpt-5-mini",
636
- description="AI model identifier to be used by this agent for processing requests.",
637
- json_schema_extra={
638
- "x-group": "ai",
639
- },
490
+ description="LLM of the agent",
640
491
  ),
641
492
  ]
642
493
  prompt: Annotated[
643
- Optional[str],
494
+ str | None,
644
495
  PydanticField(
645
496
  default=None,
646
497
  description="Base system prompt that defines the agent's behavior and capabilities",
647
498
  max_length=20000,
648
- json_schema_extra={
649
- "x-group": "ai",
650
- "pattern": "^(([^#].*)|#[^# ].*|#{3,}[ ].*|$)(\n(([^#].*)|#[^# ].*|#{3,}[ ].*|$))*$",
651
- "errorMessage": {
652
- "pattern": "Level 1 and 2 headings (# and ##) are not allowed. Please use level 3+ headings (###, ####, etc.) instead."
653
- },
654
- },
655
499
  ),
656
500
  ]
657
501
  prompt_append: Annotated[
658
- Optional[str],
502
+ str | None,
659
503
  PydanticField(
660
504
  default=None,
661
505
  description="Additional system prompt that has higher priority than the base prompt",
662
506
  max_length=20000,
663
- json_schema_extra={
664
- "x-group": "ai",
665
- "pattern": "^(([^#].*)|#[^# ].*|#{3,}[ ].*|$)(\n(([^#].*)|#[^# ].*|#{3,}[ ].*|$))*$",
666
- "errorMessage": {
667
- "pattern": "Level 1 and 2 headings (# and ##) are not allowed. Please use level 3+ headings (###, ####, etc.) instead."
668
- },
669
- },
670
507
  ),
671
508
  ]
672
509
  temperature: Annotated[
673
- Optional[float],
510
+ float | None,
674
511
  PydanticField(
675
512
  default=0.7,
676
513
  description="The randomness of the generated results is such that the higher the number, the more creative the results will be. However, this also makes them wilder and increases the likelihood of errors. For creative tasks, you can adjust it to above 1, but for rigorous tasks, such as quantitative trading, it's advisable to set it lower, around 0.2. (0.0~2.0)",
677
514
  ge=0.0,
678
515
  le=2.0,
679
- json_schema_extra={
680
- "x-group": "ai",
681
- },
682
516
  ),
683
517
  ]
684
518
  frequency_penalty: Annotated[
685
- Optional[float],
519
+ float | None,
686
520
  PydanticField(
687
521
  default=0.0,
688
522
  description="The frequency penalty is a measure of how much the AI is allowed to repeat itself. A lower value means the AI is more likely to repeat previous responses, while a higher value means the AI is more likely to generate new content. For creative tasks, you can adjust it to 1 or a bit higher. (-2.0~2.0)",
689
523
  ge=-2.0,
690
524
  le=2.0,
691
- json_schema_extra={
692
- "x-group": "ai",
693
- },
694
525
  ),
695
526
  ]
696
527
  presence_penalty: Annotated[
697
- Optional[float],
528
+ float | None,
698
529
  PydanticField(
699
530
  default=0.0,
700
531
  description="The presence penalty is a measure of how much the AI is allowed to deviate from the topic. A higher value means the AI is more likely to deviate from the topic, while a lower value means the AI is more likely to follow the topic. For creative tasks, you can adjust it to 1 or a bit higher. (-2.0~2.0)",
701
532
  ge=-2.0,
702
533
  le=2.0,
703
- json_schema_extra={
704
- "x-group": "ai",
705
- },
706
534
  ),
707
535
  ]
536
+ wallet_provider: Annotated[
537
+ Literal["cdp", "readonly", "none"] | None,
538
+ PydanticField(
539
+ default=None,
540
+ description="Provider of the agent's wallet",
541
+ ),
542
+ ]
543
+ readonly_wallet_address: Annotated[
544
+ str | None,
545
+ PydanticField(
546
+ default=None,
547
+ description="Address of the agent's wallet, only used when wallet_provider is readonly. Agent will not be able to sign transactions.",
548
+ ),
549
+ ]
550
+ network_id: Annotated[
551
+ Literal[
552
+ "base-mainnet",
553
+ "ethereum-mainnet",
554
+ "polygon-mainnet",
555
+ "arbitrum-mainnet",
556
+ "optimism-mainnet",
557
+ "solana",
558
+ ]
559
+ | None,
560
+ PydanticField(
561
+ default="base-mainnet",
562
+ description="Network identifier",
563
+ ),
564
+ ]
565
+ skills: Annotated[
566
+ dict[str, Any] | None,
567
+ PydanticField(
568
+ default=None,
569
+ description="Dict of skills and their corresponding configurations",
570
+ ),
571
+ ]
572
+
573
+ def hash(self) -> str:
574
+ """
575
+ Generate a fixed-length hash based on the agent's content.
576
+
577
+ The hash remains unchanged if the content is the same and changes if the content changes.
578
+ This method serializes only AgentCore fields to JSON and generates a SHA-256 hash.
579
+ When called from subclasses, it will only use AgentCore fields, not subclass fields.
580
+
581
+ Returns:
582
+ str: A 64-character hexadecimal hash string
583
+ """
584
+ # Create a dictionary with only AgentCore fields for hashing
585
+ hash_data = {}
586
+
587
+ # Get only AgentCore field values, excluding None values for consistency
588
+ for field_name in AgentCore.model_fields:
589
+ value = getattr(self, field_name)
590
+ if value is not None:
591
+ hash_data[field_name] = value
592
+
593
+ # Convert to JSON string with sorted keys for consistent ordering
594
+ json_str = json.dumps(hash_data, sort_keys=True, default=str, ensure_ascii=True)
595
+
596
+ # Generate SHA-256 hash
597
+ return hashlib.sha256(json_str.encode("utf-8")).hexdigest()
598
+
599
+
600
+ class AgentUserInput(AgentCore):
601
+ """Agent update model."""
602
+
603
+ model_config = ConfigDict(
604
+ title="AgentUserInput",
605
+ from_attributes=True,
606
+ json_schema_extra={
607
+ "required": ["name"],
608
+ },
609
+ )
610
+
708
611
  short_term_memory_strategy: Annotated[
709
- Optional[Literal["trim", "summarize"]],
612
+ Literal["trim", "summarize"] | None,
710
613
  PydanticField(
711
614
  default="trim",
712
615
  description="Strategy for managing short-term memory when context limit is reached. 'trim' removes oldest messages, 'summarize' creates summaries.",
713
- json_schema_extra={
714
- "x-group": "ai",
715
- },
716
616
  ),
717
617
  ]
718
618
  # autonomous mode
719
619
  autonomous: Annotated[
720
- Optional[List[AgentAutonomous]],
620
+ list[AgentAutonomous] | None,
721
621
  PydanticField(
722
622
  default=None,
723
623
  description=(
@@ -734,158 +634,94 @@ class AgentUpdate(BaseModel):
734
634
  " prompt: |-\n"
735
635
  " Say hi [sequence], use number for sequence.\n"
736
636
  ),
737
- json_schema_extra={
738
- "x-group": "autonomous",
739
- "x-inline": True,
740
- },
741
637
  ),
742
638
  ]
743
- example_intro: Annotated[
744
- Optional[str],
639
+ # if telegram_entrypoint_enabled, the telegram_entrypoint_enabled will be enabled, telegram_config will be checked
640
+ telegram_entrypoint_enabled: Annotated[
641
+ bool | None,
745
642
  PydanticField(
746
- default=None,
747
- description="Introduction of the example",
748
- max_length=2000,
749
- json_schema_extra={
750
- "x-group": "examples",
751
- },
643
+ default=False,
644
+ description="Whether the agent can play telegram bot",
752
645
  ),
753
646
  ]
754
- examples: Annotated[
755
- Optional[List[AgentExample]],
647
+ telegram_entrypoint_prompt: Annotated[
648
+ str | None,
756
649
  PydanticField(
757
650
  default=None,
758
- description="List of example prompts for the agent",
759
- max_length=6,
760
- json_schema_extra={
761
- "x-group": "examples",
762
- "x-inline": True,
763
- },
651
+ description="Extra prompt for telegram entrypoint",
652
+ max_length=10000,
764
653
  ),
765
654
  ]
766
- # skills
767
- skills: Annotated[
768
- Optional[Dict[str, Any]],
655
+ telegram_config: Annotated[
656
+ dict | None,
769
657
  PydanticField(
770
658
  default=None,
771
- description="Dict of skills and their corresponding configurations",
772
- json_schema_extra={
773
- "x-group": "skills",
774
- "x-inline": True,
775
- },
659
+ description="Telegram integration configuration settings",
776
660
  ),
777
661
  ]
778
- wallet_provider: Annotated[
779
- Optional[Literal["cdp", "readonly"]],
662
+ discord_entrypoint_enabled: Annotated[
663
+ bool | None,
780
664
  PydanticField(
781
- default="cdp",
782
- description="Provider of the agent's wallet",
665
+ default=False,
666
+ description="Whether the agent can play discord bot",
783
667
  json_schema_extra={
784
- "x-group": "onchain",
668
+ "x-group": "entrypoint",
785
669
  },
786
670
  ),
787
671
  ]
788
- readonly_wallet_address: Annotated[
789
- Optional[str],
672
+ discord_config: Annotated[
673
+ dict | None,
790
674
  PydanticField(
791
675
  default=None,
792
- description="Address of the agent's wallet, only used when wallet_provider is readonly. Agent will not be able to sign transactions.",
793
- ),
794
- ]
795
- network_id: Annotated[
796
- Optional[
797
- Literal[
798
- "ethereum-mainnet",
799
- "ethereum-sepolia",
800
- "polygon-mainnet",
801
- "polygon-mumbai",
802
- "base-mainnet",
803
- "base-sepolia",
804
- "arbitrum-mainnet",
805
- "arbitrum-sepolia",
806
- "optimism-mainnet",
807
- "optimism-sepolia",
808
- "solana",
809
- ]
810
- ],
811
- PydanticField(
812
- default="base-mainnet",
813
- description="Network identifier",
814
- json_schema_extra={
815
- "x-group": "onchain",
816
- },
817
- ),
818
- ]
819
- cdp_network_id: Annotated[
820
- Optional[
821
- Literal[
822
- "ethereum-mainnet",
823
- "ethereum-sepolia",
824
- "polygon-mainnet",
825
- "polygon-mumbai",
826
- "base-mainnet",
827
- "base-sepolia",
828
- "arbitrum-mainnet",
829
- "arbitrum-sepolia",
830
- "optimism-mainnet",
831
- "optimism-sepolia",
832
- ]
833
- ],
834
- PydanticField(
835
- default="base-mainnet",
836
- description="Network identifier for CDP integration",
837
- json_schema_extra={
838
- "x-group": "deprecated",
839
- },
840
- ),
841
- ]
842
- # if telegram_entrypoint_enabled, the telegram_entrypoint_enabled will be enabled, telegram_config will be checked
843
- telegram_entrypoint_enabled: Annotated[
844
- Optional[bool],
845
- PydanticField(
846
- default=False,
847
- description="Whether the agent can play telegram bot",
676
+ description="Discord integration configuration settings including token, whitelists, and behavior settings",
848
677
  json_schema_extra={
849
678
  "x-group": "entrypoint",
850
679
  },
851
680
  ),
852
681
  ]
853
- telegram_entrypoint_prompt: Annotated[
854
- Optional[str],
682
+ xmtp_entrypoint_prompt: Annotated[
683
+ str | None,
855
684
  PydanticField(
856
685
  default=None,
857
- description="Extra prompt for telegram entrypoint",
686
+ description="Extra prompt for xmtp entrypoint, xmtp support is in beta",
858
687
  max_length=10000,
859
- json_schema_extra={
860
- "x-group": "entrypoint",
861
- },
862
688
  ),
863
689
  ]
864
- telegram_config: Annotated[
865
- Optional[dict],
690
+
691
+
692
+ class AgentUpdate(AgentUserInput):
693
+ """Agent update model."""
694
+
695
+ model_config = ConfigDict(
696
+ title="Agent",
697
+ from_attributes=True,
698
+ json_schema_extra={
699
+ "required": ["name"],
700
+ },
701
+ )
702
+
703
+ upstream_id: Annotated[
704
+ str | None,
866
705
  PydanticField(
867
706
  default=None,
868
- description="Telegram integration configuration settings",
869
- json_schema_extra={
870
- "x-group": "entrypoint",
871
- },
707
+ description="External reference ID for idempotent operations",
708
+ max_length=100,
872
709
  ),
873
710
  ]
874
- xmtp_entrypoint_prompt: Annotated[
875
- Optional[str],
711
+ upstream_extra: Annotated[
712
+ dict[str, Any] | None,
876
713
  PydanticField(
877
714
  default=None,
878
- description="Extra prompt for xmtp entrypoint, xmtp support is in beta",
879
- max_length=10000,
715
+ description="Additional data store for upstream use",
880
716
  json_schema_extra={
881
- "x-group": "entrypoint",
717
+ "x-group": "internal",
882
718
  },
883
719
  ),
884
720
  ]
885
721
 
886
722
  @field_validator("purpose", "personality", "principles", "prompt", "prompt_append")
887
723
  @classmethod
888
- def validate_no_level1_level2_headings(cls, v: Optional[str]) -> Optional[str]:
724
+ def validate_no_level1_level2_headings(cls, v: str | None) -> str | None:
889
725
  """Validate that the text doesn't contain level 1 or level 2 headings."""
890
726
  if v is None:
891
727
  return v
@@ -912,20 +748,25 @@ class AgentUpdate(BaseModel):
912
748
  for autonomous_config in self.autonomous:
913
749
  # Check that exactly one scheduling method is provided
914
750
  if not autonomous_config.minutes and not autonomous_config.cron:
915
- raise HTTPException(
916
- status_code=400, detail="either minutes or cron must have a value"
751
+ raise IntentKitAPIError(
752
+ status_code=400,
753
+ key="InvalidAutonomousConfig",
754
+ message="either minutes or cron must have a value",
917
755
  )
918
756
 
919
757
  if autonomous_config.minutes and autonomous_config.cron:
920
- raise HTTPException(
921
- status_code=400, detail="only one of minutes or cron can be set"
758
+ raise IntentKitAPIError(
759
+ status_code=400,
760
+ key="InvalidAutonomousConfig",
761
+ message="only one of minutes or cron can be set",
922
762
  )
923
763
 
924
764
  # Validate minimum interval of 5 minutes
925
765
  if autonomous_config.minutes and autonomous_config.minutes < 5:
926
- raise HTTPException(
766
+ raise IntentKitAPIError(
927
767
  status_code=400,
928
- detail="The shortest execution interval is 5 minutes",
768
+ key="InvalidAutonomousInterval",
769
+ message="The shortest execution interval is 5 minutes",
929
770
  )
930
771
 
931
772
  # Validate cron expression to ensure interval is at least 5 minutes
@@ -935,15 +776,18 @@ class AgentUpdate(BaseModel):
935
776
  try:
936
777
  CronValidator.parse(autonomous_config.cron)
937
778
  except ValueError:
938
- raise HTTPException(
779
+ raise IntentKitAPIError(
939
780
  status_code=400,
940
- detail=f"Invalid cron expression format: {autonomous_config.cron}",
781
+ key="InvalidCronExpression",
782
+ message=f"Invalid cron expression format: {autonomous_config.cron}",
941
783
  )
942
784
 
943
785
  parts = autonomous_config.cron.split()
944
786
  if len(parts) < 5:
945
- raise HTTPException(
946
- status_code=400, detail="Invalid cron expression format"
787
+ raise IntentKitAPIError(
788
+ status_code=400,
789
+ key="InvalidCronExpression",
790
+ message="Invalid cron expression format",
947
791
  )
948
792
 
949
793
  minute, hour, day_of_month, month, day_of_week = parts[:5]
@@ -951,27 +795,31 @@ class AgentUpdate(BaseModel):
951
795
  # Check if minutes or hours have too frequent intervals
952
796
  if "*" in minute and "*" in hour:
953
797
  # If both minute and hour are wildcards, it would run every minute
954
- raise HTTPException(
798
+ raise IntentKitAPIError(
955
799
  status_code=400,
956
- detail="The shortest execution interval is 5 minutes",
800
+ key="InvalidAutonomousInterval",
801
+ message="The shortest execution interval is 5 minutes",
957
802
  )
958
803
 
959
804
  if "/" in minute:
960
805
  # Check step value in minute field (e.g., */15)
961
806
  step = int(minute.split("/")[1])
962
807
  if step < 5 and hour == "*":
963
- raise HTTPException(
808
+ raise IntentKitAPIError(
964
809
  status_code=400,
965
- detail="The shortest execution interval is 5 minutes",
810
+ key="InvalidAutonomousInterval",
811
+ message="The shortest execution interval is 5 minutes",
966
812
  )
967
813
 
968
814
  # Check for comma-separated values or ranges that might result in multiple executions per hour
969
815
  if ("," in minute or "-" in minute) and hour == "*":
970
- raise HTTPException(
816
+ raise IntentKitAPIError(
971
817
  status_code=400,
972
- detail="The shortest execution interval is 5 minutes",
818
+ key="InvalidAutonomousInterval",
819
+ message="The shortest execution interval is 5 minutes",
973
820
  )
974
821
 
822
+ # deprecated, use override instead
975
823
  async def update(self, id: str) -> "Agent":
976
824
  # Validate autonomous schedule settings if present
977
825
  if "autonomous" in self.model_dump(exclude_unset=True):
@@ -980,16 +828,16 @@ class AgentUpdate(BaseModel):
980
828
  async with get_session() as db:
981
829
  db_agent = await db.get(AgentTable, id)
982
830
  if not db_agent:
983
- raise HTTPException(status_code=404, detail="Agent not found")
984
- # check owner
985
- if self.owner and db_agent.owner != self.owner:
986
- raise HTTPException(
987
- status_code=403,
988
- detail="You do not have permission to update this agent",
831
+ raise IntentKitAPIError(
832
+ status_code=404,
833
+ key="AgentNotFound",
834
+ message="Agent not found",
989
835
  )
990
836
  # update
991
837
  for key, value in self.model_dump(exclude_unset=True).items():
992
838
  setattr(db_agent, key, value)
839
+ db_agent.version = self.hash()
840
+ db_agent.deployed_at = func.now()
993
841
  await db.commit()
994
842
  await db.refresh(db_agent)
995
843
  return Agent.model_validate(db_agent)
@@ -1002,16 +850,17 @@ class AgentUpdate(BaseModel):
1002
850
  async with get_session() as db:
1003
851
  db_agent = await db.get(AgentTable, id)
1004
852
  if not db_agent:
1005
- raise HTTPException(status_code=404, detail="Agent not found")
1006
- # check owner
1007
- if db_agent.owner and db_agent.owner != self.owner:
1008
- raise HTTPException(
1009
- status_code=403,
1010
- detail="You do not have permission to update this agent",
853
+ raise IntentKitAPIError(
854
+ status_code=404,
855
+ key="AgentNotFound",
856
+ message="Agent not found",
1011
857
  )
1012
858
  # update
1013
859
  for key, value in self.model_dump().items():
1014
860
  setattr(db_agent, key, value)
861
+ # version
862
+ db_agent.version = self.hash()
863
+ db_agent.deployed_at = func.now()
1015
864
  await db.commit()
1016
865
  await db.refresh(db_agent)
1017
866
  return Agent.model_validate(db_agent)
@@ -1030,6 +879,14 @@ class AgentCreate(AgentUpdate):
1030
879
  max_length=67,
1031
880
  ),
1032
881
  ]
882
+ owner: Annotated[
883
+ str | None,
884
+ PydanticField(
885
+ default=None,
886
+ description="Owner identifier of the agent, used for access control",
887
+ max_length=50,
888
+ ),
889
+ ]
1033
890
 
1034
891
  async def check_upstream_id(self) -> None:
1035
892
  if not self.upstream_id:
@@ -1039,12 +896,13 @@ class AgentCreate(AgentUpdate):
1039
896
  select(AgentTable).where(AgentTable.upstream_id == self.upstream_id)
1040
897
  )
1041
898
  if existing:
1042
- raise HTTPException(
899
+ raise IntentKitAPIError(
1043
900
  status_code=400,
1044
- detail="Upstream id already in use",
901
+ key="UpstreamIdConflict",
902
+ message="Upstream id already in use",
1045
903
  )
1046
904
 
1047
- async def get_by_upstream_id(self) -> Optional["Agent"]:
905
+ async def get_by_upstream_id(self) -> Agent | None:
1048
906
  if not self.upstream_id:
1049
907
  return None
1050
908
  async with get_session() as db:
@@ -1061,46 +919,226 @@ class AgentCreate(AgentUpdate):
1061
919
  self.validate_autonomous_schedule()
1062
920
 
1063
921
  async with get_session() as db:
1064
- db_agent = AgentTable(**self.model_dump())
1065
- db.add(db_agent)
1066
- await db.commit()
1067
- await db.refresh(db_agent)
1068
- return Agent.model_validate(db_agent)
922
+ try:
923
+ db_agent = AgentTable(**self.model_dump())
924
+ db_agent.version = self.hash()
925
+ db_agent.deployed_at = func.now()
926
+ db.add(db_agent)
927
+ await db.commit()
928
+ await db.refresh(db_agent)
929
+ return Agent.model_validate(db_agent)
930
+ except IntegrityError:
931
+ await db.rollback()
932
+ raise IntentKitAPIError(
933
+ status_code=400,
934
+ key="AgentExists",
935
+ message=f"Agent with ID '{self.id}' already exists",
936
+ )
1069
937
 
1070
- async def create_or_update(self) -> ("Agent", bool):
1071
- # Validation is now handled by field validators
1072
- await self.check_upstream_id()
1073
938
 
1074
- # Validate autonomous schedule settings if present
1075
- if self.autonomous:
1076
- self.validate_autonomous_schedule()
939
+ class AgentPublicInfo(BaseModel):
940
+ """Public information of the agent."""
941
+
942
+ model_config = ConfigDict(
943
+ title="AgentPublicInfo",
944
+ from_attributes=True,
945
+ )
946
+
947
+ description: Annotated[
948
+ str | None,
949
+ PydanticField(
950
+ default=None,
951
+ description="Description of the agent, for public view, not contained in prompt",
952
+ json_schema_extra={
953
+ "x-placeholder": "Introduce your agent",
954
+ },
955
+ ),
956
+ ]
957
+ external_website: Annotated[
958
+ str | None,
959
+ PydanticField(
960
+ default=None,
961
+ description="Link of external website of the agent, if you have one",
962
+ json_schema_extra={
963
+ "x-placeholder": "Enter agent external website url",
964
+ "format": "uri",
965
+ },
966
+ ),
967
+ ]
968
+ ticker: Annotated[
969
+ str | None,
970
+ PydanticField(
971
+ default=None,
972
+ description="Ticker symbol of the agent",
973
+ max_length=10,
974
+ min_length=1,
975
+ json_schema_extra={
976
+ "x-placeholder": "If one day, your agent has it's own token, what will it be?",
977
+ },
978
+ ),
979
+ ]
980
+ token_address: Annotated[
981
+ str | None,
982
+ PydanticField(
983
+ default=None,
984
+ description="Token address of the agent",
985
+ max_length=66,
986
+ json_schema_extra={
987
+ "x-placeholder": "The contract address of the agent token",
988
+ },
989
+ ),
990
+ ]
991
+ token_pool: Annotated[
992
+ str | None,
993
+ PydanticField(
994
+ default=None,
995
+ description="Pool of the agent token",
996
+ max_length=66,
997
+ json_schema_extra={
998
+ "x-placeholder": "The contract address of the agent token pool",
999
+ },
1000
+ ),
1001
+ ]
1002
+ fee_percentage: Annotated[
1003
+ Decimal | None,
1004
+ PydanticField(
1005
+ default=None,
1006
+ description="Fee percentage of the agent",
1007
+ ge=Decimal("0.0"),
1008
+ json_schema_extra={
1009
+ "x-placeholder": "Agent will charge service fee according to this ratio.",
1010
+ },
1011
+ ),
1012
+ ]
1013
+ example_intro: Annotated[
1014
+ str | None,
1015
+ PydanticField(
1016
+ default=None,
1017
+ description="Introduction of the example",
1018
+ max_length=2000,
1019
+ json_schema_extra={
1020
+ "x-placeholder": "Add a short introduction in new chat",
1021
+ },
1022
+ ),
1023
+ ]
1024
+ examples: Annotated[
1025
+ list[AgentExample] | None,
1026
+ PydanticField(
1027
+ default=None,
1028
+ description="List of example prompts for the agent",
1029
+ max_length=6,
1030
+ json_schema_extra={
1031
+ "x-inline": True,
1032
+ },
1033
+ ),
1034
+ ]
1035
+ public_extra: Annotated[
1036
+ dict[str, Any] | None,
1037
+ PydanticField(
1038
+ default=None,
1039
+ description="Public extra data of the agent",
1040
+ ),
1041
+ ]
1042
+
1043
+ async def override(self, agent_id: str) -> "Agent":
1044
+ """Override agent public info with all fields from this instance.
1045
+
1046
+ Args:
1047
+ agent_id: The ID of the agent to override
1048
+
1049
+ Returns:
1050
+ The updated Agent instance
1051
+ """
1052
+ async with get_session() as session:
1053
+ # Get the agent from database
1054
+ result = await session.execute(
1055
+ select(AgentTable).where(AgentTable.id == agent_id)
1056
+ )
1057
+ db_agent = result.scalar_one_or_none()
1077
1058
 
1078
- is_new = False
1079
- async with get_session() as db:
1080
- db_agent = await db.get(AgentTable, self.id)
1081
1059
  if not db_agent:
1082
- db_agent = AgentTable(**self.model_dump())
1083
- db.add(db_agent)
1084
- is_new = True
1085
- else:
1086
- # check owner
1087
- if self.owner and db_agent.owner != self.owner:
1088
- raise HTTPException(
1089
- status_code=403,
1090
- detail="You do not have permission to update this agent",
1091
- )
1092
- for key, value in self.model_dump(exclude_unset=True).items():
1060
+ raise IntentKitAPIError(404, "NotFound", f"Agent {agent_id} not found")
1061
+
1062
+ # Update public info fields
1063
+ update_data = self.model_dump()
1064
+ for key, value in update_data.items():
1065
+ if hasattr(db_agent, key):
1093
1066
  setattr(db_agent, key, value)
1094
- await db.commit()
1095
- await db.refresh(db_agent)
1096
- return Agent.model_validate(db_agent), is_new
1067
+
1068
+ # Update public_info_updated_at timestamp
1069
+ db_agent.public_info_updated_at = func.now()
1070
+
1071
+ # Commit changes
1072
+ await session.commit()
1073
+ await session.refresh(db_agent)
1074
+
1075
+ return Agent.model_validate(db_agent)
1097
1076
 
1098
1077
 
1099
- class Agent(AgentCreate):
1078
+ class Agent(AgentCreate, AgentPublicInfo):
1100
1079
  """Agent model."""
1101
1080
 
1102
1081
  model_config = ConfigDict(from_attributes=True)
1103
1082
 
1083
+ slug: Annotated[
1084
+ str | None,
1085
+ PydanticField(
1086
+ default=None,
1087
+ description="Slug of the agent, used for URL generation",
1088
+ max_length=100,
1089
+ min_length=2,
1090
+ ),
1091
+ ]
1092
+ version: Annotated[
1093
+ str | None,
1094
+ PydanticField(
1095
+ default=None,
1096
+ description="Version hash of the agent",
1097
+ ),
1098
+ ]
1099
+ statistics: Annotated[
1100
+ dict[str, Any] | None,
1101
+ PydanticField(
1102
+ default=None,
1103
+ description="Statistics of the agent, update every 1 hour for query",
1104
+ ),
1105
+ ]
1106
+ assets: Annotated[
1107
+ dict[str, Any] | None,
1108
+ PydanticField(
1109
+ default=None,
1110
+ description="Assets of the agent, update every 1 hour for query",
1111
+ ),
1112
+ ]
1113
+ account_snapshot: Annotated[
1114
+ CreditAccount | None,
1115
+ PydanticField(
1116
+ default=None,
1117
+ description="Account snapshot of the agent, update every 1 hour for query",
1118
+ ),
1119
+ ]
1120
+ extra: Annotated[
1121
+ dict[str, Any] | None,
1122
+ PydanticField(
1123
+ default=None,
1124
+ description="Other helper data fields for query, come from agent and agent data",
1125
+ ),
1126
+ ]
1127
+ deployed_at: Annotated[
1128
+ datetime | None,
1129
+ PydanticField(
1130
+ default=None,
1131
+ description="Timestamp when the agent was deployed",
1132
+ ),
1133
+ ]
1134
+ public_info_updated_at: Annotated[
1135
+ datetime | None,
1136
+ PydanticField(
1137
+ default=None,
1138
+ description="Timestamp when the agent public info was last updated",
1139
+ ),
1140
+ ]
1141
+
1104
1142
  # auto timestamp
1105
1143
  created_at: Annotated[
1106
1144
  datetime,
@@ -1135,8 +1173,11 @@ class Agent(AgentCreate):
1135
1173
  return False
1136
1174
 
1137
1175
  async def is_model_support_image(self) -> bool:
1138
- model = await LLMModelInfo.get(self.model)
1139
- return model.supports_image_input
1176
+ try:
1177
+ model = await LLMModelInfo.get(self.model)
1178
+ return model.supports_image_input
1179
+ except Exception:
1180
+ return False
1140
1181
 
1141
1182
  def to_yaml(self) -> str:
1142
1183
  """
@@ -1229,7 +1270,7 @@ class Agent(AgentCreate):
1229
1270
  )
1230
1271
  yaml_lines.append(yaml_value.rstrip())
1231
1272
  elif isinstance(value, list) and value and hasattr(value[0], "model_dump"):
1232
- # Handle list of Pydantic models (e.g., List[AgentAutonomous])
1273
+ # Handle list of Pydantic models (e.g., list[AgentAutonomous])
1233
1274
  yaml_lines.append(f"{field_name}:")
1234
1275
  # Convert each Pydantic model to dict
1235
1276
  model_dicts = [item.model_dump(exclude_none=True) for item in value]
@@ -1244,20 +1285,22 @@ class Agent(AgentCreate):
1244
1285
  yaml_lines.append(indented_yaml.rstrip())
1245
1286
  elif hasattr(value, "model_dump"):
1246
1287
  # Handle individual Pydantic model
1247
- model_dict = value.model_dump(exclude_none=True)
1288
+ yaml_lines.append(f"{field_name}:")
1248
1289
  yaml_value = yaml.dump(
1249
- {field_name: model_dict},
1290
+ value.model_dump(exclude_none=True),
1250
1291
  default_flow_style=False,
1251
1292
  allow_unicode=True,
1252
1293
  )
1253
- yaml_lines.append(yaml_value.rstrip())
1294
+ # Indent all lines and append to yaml_lines
1295
+ indented_yaml = "\n".join(
1296
+ f" {line}" for line in yaml_value.split("\n") if line.strip()
1297
+ )
1298
+ yaml_lines.append(indented_yaml)
1254
1299
  else:
1255
- # Handle Decimal values specifically
1300
+ # Handle Decimal and other types
1256
1301
  if isinstance(value, Decimal):
1257
- # Convert Decimal to string to avoid !!python/object/apply:decimal.Decimal serialization
1258
- yaml_lines.append(f"{field_name}: {value}")
1302
+ yaml_lines.append(f"{field_name}: {str(value)}")
1259
1303
  else:
1260
- # Handle other non-string values
1261
1304
  yaml_value = yaml.dump(
1262
1305
  {field_name: value},
1263
1306
  default_flow_style=False,
@@ -1273,18 +1316,154 @@ class Agent(AgentCreate):
1273
1316
  return await db.scalar(select(func.count(AgentTable.id)))
1274
1317
 
1275
1318
  @classmethod
1276
- async def get(cls, agent_id: str) -> Optional["Agent"]:
1319
+ async def get(cls, agent_id: str) -> "Agent" | None:
1277
1320
  async with get_session() as db:
1278
1321
  item = await db.scalar(select(AgentTable).where(AgentTable.id == agent_id))
1279
1322
  if item is None:
1280
1323
  return None
1281
1324
  return cls.model_validate(item)
1282
1325
 
1283
- def skill_config(self, category: str) -> Dict[str, Any]:
1326
+ @classmethod
1327
+ async def get_by_id_or_slug(cls, agent_id: str) -> "Agent" | None:
1328
+ """Get agent by ID or slug.
1329
+
1330
+ First tries to get by ID if agent_id length <= 20,
1331
+ then falls back to searching by slug if not found.
1332
+
1333
+ Args:
1334
+ agent_id: Agent ID or slug to search for
1335
+
1336
+ Returns:
1337
+ Agent if found, None otherwise
1338
+ """
1339
+ async with get_session() as db:
1340
+ agent = None
1341
+
1342
+ # Try to get by ID if length <= 20
1343
+ if len(agent_id) <= 20:
1344
+ agent = await Agent.get(agent_id)
1345
+
1346
+ # If not found, try to get by slug
1347
+ if agent is None:
1348
+ slug_stmt = select(AgentTable).where(AgentTable.slug == agent_id)
1349
+ agent_row = await db.scalar(slug_stmt)
1350
+ if agent_row is not None:
1351
+ agent = Agent.model_validate(agent_row)
1352
+
1353
+ return agent
1354
+
1355
+ @staticmethod
1356
+ def _deserialize_autonomous(
1357
+ autonomous_data: list[Any] | None,
1358
+ ) -> list[AgentAutonomous]:
1359
+ if not autonomous_data:
1360
+ return []
1361
+
1362
+ deserialized: list[AgentAutonomous] = []
1363
+ for entry in autonomous_data:
1364
+ if isinstance(entry, AgentAutonomous):
1365
+ deserialized.append(entry)
1366
+ else:
1367
+ deserialized.append(AgentAutonomous.model_validate(entry))
1368
+ return deserialized
1369
+
1370
+ @staticmethod
1371
+ def _serialize_autonomous(tasks: list[AgentAutonomous]) -> list[dict[str, Any]]:
1372
+ return [task.model_dump() for task in tasks]
1373
+
1374
+ @staticmethod
1375
+ def _autonomous_not_allowed_error() -> IntentKitAPIError:
1376
+ return IntentKitAPIError(
1377
+ 400,
1378
+ "AgentNotDeployed",
1379
+ "Only deployed agents can call this feature.",
1380
+ )
1381
+
1382
+ async def list_autonomous_tasks(self) -> list[AgentAutonomous]:
1383
+ persisted = await Agent.get(self.id)
1384
+ if persisted is None:
1385
+ raise self._autonomous_not_allowed_error()
1386
+
1387
+ tasks = persisted.autonomous or []
1388
+ # Keep local state in sync with persisted data
1389
+ self.autonomous = tasks
1390
+ return tasks
1391
+
1392
+ async def add_autonomous_task(self, task: AgentAutonomous) -> AgentAutonomous:
1393
+ async with get_session() as session:
1394
+ db_agent = await session.get(AgentTable, self.id)
1395
+ if db_agent is None:
1396
+ raise self._autonomous_not_allowed_error()
1397
+
1398
+ current_tasks = self._deserialize_autonomous(db_agent.autonomous)
1399
+ current_tasks.append(task)
1400
+
1401
+ db_agent.autonomous = self._serialize_autonomous(current_tasks)
1402
+ await session.commit()
1403
+
1404
+ self.autonomous = current_tasks
1405
+ return task
1406
+
1407
+ async def delete_autonomous_task(self, task_id: str) -> None:
1408
+ async with get_session() as session:
1409
+ db_agent = await session.get(AgentTable, self.id)
1410
+ if db_agent is None:
1411
+ raise self._autonomous_not_allowed_error()
1412
+
1413
+ current_tasks = self._deserialize_autonomous(db_agent.autonomous)
1414
+
1415
+ updated_tasks = [task for task in current_tasks if task.id != task_id]
1416
+ if len(updated_tasks) == len(current_tasks):
1417
+ raise IntentKitAPIError(
1418
+ 404,
1419
+ "TaskNotFound",
1420
+ f"Autonomous task with ID {task_id} not found.",
1421
+ )
1422
+
1423
+ db_agent.autonomous = self._serialize_autonomous(updated_tasks)
1424
+ await session.commit()
1425
+
1426
+ self.autonomous = updated_tasks
1427
+
1428
+ async def update_autonomous_task(
1429
+ self, task_id: str, task_updates: dict
1430
+ ) -> AgentAutonomous:
1431
+ async with get_session() as session:
1432
+ db_agent = await session.get(AgentTable, self.id)
1433
+ if db_agent is None:
1434
+ raise self._autonomous_not_allowed_error()
1435
+
1436
+ current_tasks = self._deserialize_autonomous(db_agent.autonomous)
1437
+
1438
+ updated_task: AgentAutonomous | None = None
1439
+ rewritten_tasks: list[AgentAutonomous] = []
1440
+ for task in current_tasks:
1441
+ if task.id == task_id:
1442
+ task_dict = task.model_dump()
1443
+ task_dict.update(task_updates)
1444
+ updated_task = AgentAutonomous.model_validate(task_dict)
1445
+ rewritten_tasks.append(updated_task)
1446
+ else:
1447
+ rewritten_tasks.append(task)
1448
+
1449
+ if updated_task is None:
1450
+ raise IntentKitAPIError(
1451
+ 404,
1452
+ "TaskNotFound",
1453
+ f"Autonomous task with ID {task_id} not found.",
1454
+ )
1455
+
1456
+ db_agent.autonomous = self._serialize_autonomous(rewritten_tasks)
1457
+ await session.commit()
1458
+
1459
+ self.autonomous = rewritten_tasks
1460
+ return updated_task
1461
+
1462
+ def skill_config(self, category: str) -> dict[str, Any]:
1284
1463
  return self.skills.get(category, {}) if self.skills else {}
1285
1464
 
1286
1465
  @staticmethod
1287
- def _is_agent_owner_only_skill(skill_schema: Dict[str, Any]) -> bool:
1466
+ def _is_agent_owner_only_skill(skill_schema: dict[str, Any]) -> bool:
1288
1467
  """Check if a skill requires agent owner API keys only based on its resolved schema."""
1289
1468
  if (
1290
1469
  skill_schema
@@ -1303,8 +1482,7 @@ class Agent(AgentCreate):
1303
1482
  cls,
1304
1483
  db: AsyncSession = None,
1305
1484
  filter_owner_api_skills: bool = False,
1306
- admin_llm_skill_control: bool = True,
1307
- ) -> Dict:
1485
+ ) -> dict:
1308
1486
  """Get the JSON schema for Agent model with all $ref references resolved.
1309
1487
 
1310
1488
  This is the shared function that handles admin configuration filtering
@@ -1313,7 +1491,6 @@ class Agent(AgentCreate):
1313
1491
  Args:
1314
1492
  db: Database session (optional, will create if not provided)
1315
1493
  filter_owner_api_skills: Whether to filter out skills that require agent owner API keys
1316
- admin_llm_skill_control: Whether to enable admin LLM and skill control features
1317
1494
 
1318
1495
  Returns:
1319
1496
  Dict containing the complete JSON schema for the Agent model
@@ -1321,9 +1498,7 @@ class Agent(AgentCreate):
1321
1498
  # Get database session if not provided
1322
1499
  if db is None:
1323
1500
  async with get_session() as session:
1324
- return await cls.get_json_schema(
1325
- session, filter_owner_api_skills, admin_llm_skill_control
1326
- )
1501
+ return await cls.get_json_schema(session, filter_owner_api_skills)
1327
1502
 
1328
1503
  # Get the schema file path relative to this file
1329
1504
  current_dir = Path(__file__).parent
@@ -1336,400 +1511,260 @@ class Agent(AgentCreate):
1336
1511
  # Get the model property from the schema
1337
1512
  model_property = schema.get("properties", {}).get("model", {})
1338
1513
 
1339
- if admin_llm_skill_control:
1340
- # Process model property - use LLMModelInfo as primary source
1341
- if model_property:
1342
- # Query all LLM models from the database
1343
- stmt = select(LLMModelInfoTable).where(LLMModelInfoTable.enabled)
1344
- result = await db.execute(stmt)
1345
- models = result.scalars().all()
1346
-
1347
- # Create new lists based on LLMModelInfo
1348
- new_enum = []
1349
- new_enum_title = []
1350
- new_enum_category = []
1351
- new_enum_support_skill = []
1352
-
1353
- # Process each model from database
1354
- for model in models:
1355
- model_info = LLMModelInfo.model_validate(model)
1356
-
1357
- # Add model ID to enum
1358
- new_enum.append(model_info.id)
1359
-
1360
- # Add model name as title
1361
- new_enum_title.append(model_info.name)
1362
-
1363
- # Add provider display name as category
1364
- provider = (
1365
- LLMProvider(model_info.provider)
1366
- if isinstance(model_info.provider, str)
1367
- else model_info.provider
1368
- )
1369
- new_enum_category.append(provider.display_name())
1370
-
1371
- # Add skill support information
1372
- new_enum_support_skill.append(model_info.supports_skill_calls)
1373
-
1374
- # Update the schema with the new lists constructed from LLMModelInfo
1375
- model_property["enum"] = new_enum
1376
- model_property["x-enum-title"] = new_enum_title
1377
- model_property["x-enum-category"] = new_enum_category
1378
- model_property["x-support-skill"] = new_enum_support_skill
1379
-
1380
- # If the default model is not in the new enum, update it if possible
1381
- if (
1382
- "default" in model_property
1383
- and model_property["default"] not in new_enum
1384
- and new_enum
1385
- ):
1386
- model_property["default"] = new_enum[0]
1387
-
1388
- # Process skills property
1389
- skills_property = schema.get("properties", {}).get("skills", {})
1390
- skills_properties = skills_property.get("properties", {})
1391
-
1392
- if skills_properties:
1393
- # Load all skills from the database
1394
- # Query all skills grouped by category with enabled status
1395
- stmt = select(
1396
- SkillTable.category,
1397
- func.bool_or(SkillTable.enabled).label("any_enabled"),
1398
- ).group_by(SkillTable.category)
1399
- result = await db.execute(stmt)
1400
- category_status = {row.category: row.any_enabled for row in result}
1401
-
1402
- # Query all skills with their price levels for adding x-price-level fields
1403
- skills_stmt = select(
1404
- SkillTable.category,
1405
- SkillTable.config_name,
1406
- SkillTable.price_level,
1407
- SkillTable.enabled,
1408
- ).where(SkillTable.enabled)
1409
- skills_result = await db.execute(skills_stmt)
1410
- skills_data = {}
1411
- category_price_levels = {}
1412
-
1413
- for row in skills_result:
1414
- if row.category not in skills_data:
1415
- skills_data[row.category] = {}
1416
- category_price_levels[row.category] = []
1417
-
1418
- if row.config_name:
1419
- skills_data[row.category][row.config_name] = row.price_level
1420
-
1421
- if row.price_level is not None:
1422
- category_price_levels[row.category].append(row.price_level)
1423
-
1424
- # Calculate average price levels for categories
1425
- category_avg_price_levels = {}
1426
- for category, price_levels in category_price_levels.items():
1427
- if price_levels:
1428
- avg_price_level = int(sum(price_levels) / len(price_levels))
1429
- category_avg_price_levels[category] = avg_price_level
1430
-
1431
- # Create a copy of keys to avoid modifying during iteration
1432
- skill_keys = list(skills_properties.keys())
1433
-
1434
- # Process each skill in the schema
1435
- for skill_category in skill_keys:
1436
- if skill_category not in category_status:
1437
- # If category not found in database, remove it from schema
1438
- skills_properties.pop(skill_category, None)
1439
- elif not category_status[skill_category]:
1440
- # If category exists but all skills are disabled, remove it
1441
- skills_properties.pop(skill_category, None)
1442
- elif filter_owner_api_skills and cls._is_agent_owner_only_skill(
1443
- skills_properties[skill_category]
1514
+ # Process model property using defaults merged with database overrides
1515
+ if model_property:
1516
+ new_enum = []
1517
+ new_enum_title = []
1518
+ new_enum_category = []
1519
+ new_enum_support_skill = []
1520
+
1521
+ for model_info in await LLMModelInfo.get_all(db):
1522
+ if not model_info.enabled:
1523
+ continue
1524
+
1525
+ provider = (
1526
+ LLMProvider(model_info.provider)
1527
+ if isinstance(model_info.provider, str)
1528
+ else model_info.provider
1529
+ )
1530
+
1531
+ new_enum.append(model_info.id)
1532
+ new_enum_title.append(model_info.name)
1533
+ new_enum_category.append(provider.display_name())
1534
+ new_enum_support_skill.append(model_info.supports_skill_calls)
1535
+
1536
+ model_property["enum"] = new_enum
1537
+ model_property["x-enum-title"] = new_enum_title
1538
+ model_property["x-enum-category"] = new_enum_category
1539
+ model_property["x-support-skill"] = new_enum_support_skill
1540
+
1541
+ if (
1542
+ "default" in model_property
1543
+ and model_property["default"] not in new_enum
1544
+ and new_enum
1545
+ ):
1546
+ model_property["default"] = new_enum[0]
1547
+
1548
+ # Process skills property using data from Skill.get_all instead of agent_schema.json
1549
+ skills_property = schema.get("properties", {}).get("skills", {})
1550
+
1551
+ # Build skill_states_map from database
1552
+ skill_states_map: dict[str, dict[str, Skill]] = {}
1553
+ for skill_model in await Skill.get_all(db):
1554
+ if not skill_model.config_name:
1555
+ continue
1556
+ category_states = skill_states_map.setdefault(skill_model.category, {})
1557
+ if skill_model.enabled:
1558
+ category_states[skill_model.config_name] = skill_model
1559
+ else:
1560
+ category_states.pop(skill_model.config_name, None)
1561
+
1562
+ enabled_categories = {
1563
+ category for category, states in skill_states_map.items() if states
1564
+ }
1565
+
1566
+ # Calculate price levels and skills data
1567
+ category_avg_price_levels = {}
1568
+ skills_data = {}
1569
+ for category, states in skill_states_map.items():
1570
+ if not states:
1571
+ continue
1572
+ price_levels = [
1573
+ state.price_level
1574
+ for state in states.values()
1575
+ if state.price_level is not None
1576
+ ]
1577
+ if price_levels:
1578
+ category_avg_price_levels[category] = int(
1579
+ sum(price_levels) / len(price_levels)
1580
+ )
1581
+ skills_data[category] = {
1582
+ config_name: state.price_level
1583
+ for config_name, state in states.items()
1584
+ }
1585
+
1586
+ # Dynamically generate skills_properties from Skill.get_all data
1587
+ skills_properties = {}
1588
+ current_dir = Path(__file__).parent
1589
+
1590
+ for category in enabled_categories:
1591
+ # Skip if filtered for auto-generation
1592
+ skill_schema_path = current_dir / f"../skills/{category}/schema.json"
1593
+ if skill_schema_path.exists():
1594
+ try:
1595
+ with open(skill_schema_path) as f:
1596
+ skill_schema = json.load(f)
1597
+
1598
+ # Check if this skill should be filtered for owner API requirements
1599
+ if filter_owner_api_skills and cls._is_agent_owner_only_skill(
1600
+ skill_schema
1444
1601
  ):
1445
- # If filtering owner API skills and this skill requires it, remove it
1446
- skills_properties.pop(skill_category, None)
1447
1602
  logger.info(
1448
- f"Filtered out skill '{skill_category}' from auto-generation: requires agent owner API key"
1603
+ f"Filtered out skill '{category}' from auto-generation: requires agent owner API key"
1604
+ )
1605
+ continue
1606
+
1607
+ # Create skill property with embedded schema instead of reference
1608
+ # Load and embed the full skill schema directly
1609
+ base_uri = f"file://{skill_schema_path}"
1610
+ with open(skill_schema_path) as f:
1611
+ embedded_skill_schema = jsonref.load(
1612
+ f, base_uri=base_uri, proxies=False, lazy_load=False
1613
+ )
1614
+
1615
+ skills_properties[category] = {
1616
+ "title": skill_schema.get("title", category.title()),
1617
+ **embedded_skill_schema, # Embed the full schema instead of using $ref
1618
+ }
1619
+
1620
+ # Add price level information
1621
+ if category in category_avg_price_levels:
1622
+ skills_properties[category]["x-avg-price-level"] = (
1623
+ category_avg_price_levels[category]
1624
+ )
1625
+
1626
+ if category in skills_data:
1627
+ # Add price level to states in the embedded schema
1628
+ skill_states = (
1629
+ skills_properties[category]
1630
+ .get("properties", {})
1631
+ .get("states", {})
1632
+ .get("properties", {})
1449
1633
  )
1450
- else:
1451
- # Add x-avg-price-level to category level
1452
- if skill_category in category_avg_price_levels:
1453
- skills_properties[skill_category][
1454
- "x-avg-price-level"
1455
- ] = category_avg_price_levels[skill_category]
1456
-
1457
- # Add x-price-level to individual skill states
1458
- if skill_category in skills_data:
1459
- skill_states = (
1460
- skills_properties[skill_category]
1461
- .get("properties", {})
1462
- .get("states", {})
1463
- .get("properties", {})
1464
- )
1465
- for state_name, state_config in skill_states.items():
1466
- if (
1467
- state_name in skills_data[skill_category]
1468
- and skills_data[skill_category][state_name]
1469
- is not None
1470
- ):
1471
- state_config["x-price-level"] = skills_data[
1472
- skill_category
1473
- ][state_name]
1634
+ for state_name, state_config in skill_states.items():
1635
+ if (
1636
+ state_name in skills_data[category]
1637
+ and skills_data[category][state_name] is not None
1638
+ ):
1639
+ state_config["x-price-level"] = skills_data[
1640
+ category
1641
+ ][state_name]
1642
+ except (FileNotFoundError, json.JSONDecodeError) as e:
1643
+ logger.warning(
1644
+ f"Could not load schema for skill category '{category}': {e}"
1645
+ )
1646
+ continue
1647
+
1648
+ # Update the skills property in the schema
1649
+ if skills_property:
1650
+ skills_property["properties"] = skills_properties
1474
1651
 
1475
1652
  # Log the changes for debugging
1476
1653
  logger.debug(
1477
- f"Schema processed with LLM and skill controls enabled: {admin_llm_skill_control}, "
1478
- f"filtered owner API skills: {filter_owner_api_skills}"
1654
+ "Schema processed with merged LLM/skill defaults; filtered owner API skills: %s",
1655
+ filter_owner_api_skills,
1479
1656
  )
1480
1657
 
1481
1658
  return schema
1482
1659
 
1483
1660
 
1484
- class AgentResponse(BaseModel):
1485
- """Response model for Agent API."""
1661
+ class AgentResponse(Agent):
1662
+ """Agent response model that excludes sensitive fields from JSON output and schema."""
1486
1663
 
1487
1664
  model_config = ConfigDict(
1665
+ title="AgentPublic",
1488
1666
  from_attributes=True,
1489
- json_encoders={
1490
- datetime: lambda dt: dt.isoformat(),
1491
- },
1667
+ # json_encoders={
1668
+ # datetime: lambda v: v.isoformat(timespec="milliseconds"),
1669
+ # },
1492
1670
  )
1493
1671
 
1494
- id: Annotated[
1495
- str,
1496
- PydanticField(
1497
- description="Unique identifier for the agent. Must be URL-safe, containing only lowercase letters, numbers, and hyphens",
1498
- ),
1499
- ]
1500
- # auto timestamp
1501
- created_at: Annotated[
1502
- datetime,
1503
- PydanticField(
1504
- description="Timestamp when the agent was created, will ignore when importing"
1505
- ),
1506
- ]
1507
- updated_at: Annotated[
1508
- datetime,
1509
- PydanticField(
1510
- description="Timestamp when the agent was last updated, will ignore when importing"
1511
- ),
1512
- ]
1513
- # Agent part
1514
- name: Annotated[
1515
- Optional[str],
1516
- PydanticField(
1517
- default=None,
1518
- description="Display name of the agent",
1519
- ),
1520
- ]
1521
- slug: Annotated[
1522
- Optional[str],
1523
- PydanticField(
1524
- default=None,
1525
- description="Slug of the agent, used for URL generation",
1526
- ),
1527
- ]
1528
- description: Annotated[
1529
- Optional[str],
1530
- PydanticField(
1531
- default=None,
1532
- description="Description of the agent, for public view, not contained in prompt",
1533
- ),
1534
- ]
1535
- external_website: Annotated[
1536
- Optional[str],
1537
- PydanticField(
1538
- default=None,
1539
- description="Link of external website of the agent, if you have one",
1540
- ),
1541
- ]
1542
- picture: Annotated[
1543
- Optional[str],
1544
- PydanticField(
1545
- default=None,
1546
- description="Picture of the agent",
1547
- ),
1548
- ]
1549
- ticker: Annotated[
1550
- Optional[str],
1551
- PydanticField(
1552
- default=None,
1553
- description="Ticker symbol of the agent",
1554
- ),
1555
- ]
1556
- token_address: Annotated[
1557
- Optional[str],
1558
- PydanticField(
1559
- default=None,
1560
- description="Token address of the agent",
1561
- ),
1562
- ]
1563
- token_pool: Annotated[
1564
- Optional[str],
1565
- PydanticField(
1566
- default=None,
1567
- description="Pool of the agent token",
1568
- ),
1569
- ]
1570
- mode: Annotated[
1571
- Optional[Literal["public", "private"]],
1572
- PydanticField(
1573
- default=None,
1574
- description="Mode of the agent, public or private",
1575
- ),
1576
- ]
1577
- fee_percentage: Annotated[
1578
- Optional[Decimal],
1579
- PydanticField(
1580
- default=None,
1581
- description="Fee percentage of the agent",
1582
- ),
1583
- ]
1584
- owner: Annotated[
1585
- Optional[str],
1586
- PydanticField(
1587
- default=None,
1588
- description="Owner identifier of the agent, used for access control",
1589
- max_length=50,
1590
- json_schema_extra={
1591
- "x-group": "internal",
1592
- },
1593
- ),
1594
- ]
1595
- upstream_id: Annotated[
1596
- Optional[str],
1672
+ # Override privacy fields to exclude them from JSON schema
1673
+ purpose: SkipJsonSchema[str | None] = None
1674
+ personality: SkipJsonSchema[str | None] = None
1675
+ principles: SkipJsonSchema[str | None] = None
1676
+ prompt: SkipJsonSchema[str | None] = None
1677
+ prompt_append: SkipJsonSchema[str | None] = None
1678
+ temperature: SkipJsonSchema[float | None] = None
1679
+ frequency_penalty: SkipJsonSchema[float | None] = None
1680
+ telegram_entrypoint_prompt: SkipJsonSchema[str | None] = None
1681
+ telegram_config: SkipJsonSchema[dict | None] = None
1682
+ discord_config: SkipJsonSchema[dict | None] = None
1683
+ xmtp_entrypoint_prompt: SkipJsonSchema[str | None] = None
1684
+
1685
+ # Additional fields specific to AgentResponse
1686
+ cdp_wallet_address: Annotated[
1687
+ str | None,
1597
1688
  PydanticField(
1598
1689
  default=None,
1599
- description="External reference ID for idempotent operations",
1600
- max_length=100,
1601
- json_schema_extra={
1602
- "x-group": "internal",
1603
- },
1690
+ description="CDP wallet address of the agent",
1604
1691
  ),
1605
1692
  ]
1606
- upstream_extra: Annotated[
1607
- Optional[Dict[str, Any]],
1693
+ evm_wallet_address: Annotated[
1694
+ str | None,
1608
1695
  PydanticField(
1609
1696
  default=None,
1610
- description="Additional data store for upstream use",
1611
- ),
1612
- ]
1613
- # AI part
1614
- model: Annotated[
1615
- str,
1616
- PydanticField(
1617
- description="AI model identifier to be used by this agent for processing requests. Available models: gpt-4o, gpt-4o-mini, deepseek-chat, deepseek-reasoner, grok-2, eternalai, reigent",
1697
+ description="EVM wallet address of the agent",
1618
1698
  ),
1619
1699
  ]
1620
- # autonomous mode
1621
- autonomous: Annotated[
1622
- Optional[List[Dict[str, Any]]],
1700
+ solana_wallet_address: Annotated[
1701
+ str | None,
1623
1702
  PydanticField(
1624
1703
  default=None,
1625
- description=("Autonomous agent configurations."),
1704
+ description="Solana wallet address of the agent",
1626
1705
  ),
1627
1706
  ]
1628
- # agent examples
1629
- example_intro: Annotated[
1630
- Optional[str],
1707
+ has_twitter_linked: Annotated[
1708
+ bool,
1631
1709
  PydanticField(
1632
- default=None,
1633
- description="Introduction for example interactions",
1710
+ default=False,
1711
+ description="Whether the agent has Twitter linked",
1634
1712
  ),
1635
1713
  ]
1636
- examples: Annotated[
1637
- Optional[List[AgentExample]],
1714
+ linked_twitter_username: Annotated[
1715
+ str | None,
1638
1716
  PydanticField(
1639
1717
  default=None,
1640
- description="List of example prompts for the agent",
1718
+ description="Linked Twitter username",
1641
1719
  ),
1642
1720
  ]
1643
- # skills
1644
- skills: Annotated[
1645
- Optional[Dict[str, Any]],
1721
+ linked_twitter_name: Annotated[
1722
+ str | None,
1646
1723
  PydanticField(
1647
1724
  default=None,
1648
- description="Dict of skills and their corresponding configurations",
1649
- ),
1650
- ]
1651
- wallet_provider: Annotated[
1652
- Optional[Literal["cdp", "readonly"]],
1653
- PydanticField(
1654
- default="cdp",
1655
- description="Provider of the agent's wallet",
1656
- ),
1657
- ]
1658
- network_id: Annotated[
1659
- Optional[str],
1660
- PydanticField(
1661
- default="base-mainnet",
1662
- description="Network identifier",
1663
- ),
1664
- ]
1665
- cdp_network_id: Annotated[
1666
- Optional[str],
1667
- PydanticField(
1668
- default="base-mainnet",
1669
- description="Network identifier for CDP integration",
1670
- ),
1671
- ]
1672
- # telegram entrypoint
1673
- telegram_entrypoint_enabled: Annotated[
1674
- Optional[bool],
1675
- PydanticField(
1676
- default=False,
1677
- description="Whether the agent can play telegram bot",
1725
+ description="Linked Twitter display name",
1678
1726
  ),
1679
1727
  ]
1680
-
1681
- # data part
1682
- cdp_wallet_address: Annotated[
1683
- Optional[str], PydanticField(description="CDP wallet address for the agent")
1684
- ]
1685
- evm_wallet_address: Annotated[
1686
- Optional[str], PydanticField(description="EVM wallet address for the agent")
1687
- ]
1688
- solana_wallet_address: Annotated[
1689
- Optional[str], PydanticField(description="Solana wallet address for the agent")
1690
- ]
1691
- has_twitter_linked: Annotated[
1692
- bool,
1693
- PydanticField(description="Whether the agent has linked their Twitter account"),
1694
- ]
1695
- linked_twitter_username: Annotated[
1696
- Optional[str],
1697
- PydanticField(description="The username of the linked Twitter account"),
1698
- ]
1699
- linked_twitter_name: Annotated[
1700
- Optional[str],
1701
- PydanticField(description="The name of the linked Twitter account"),
1702
- ]
1703
1728
  has_twitter_self_key: Annotated[
1704
1729
  bool,
1705
1730
  PydanticField(
1706
- description="Whether the agent has self-keyed their Twitter account"
1731
+ default=False,
1732
+ description="Whether the agent has Twitter self key",
1707
1733
  ),
1708
1734
  ]
1709
1735
  has_telegram_self_key: Annotated[
1710
1736
  bool,
1711
1737
  PydanticField(
1712
- description="Whether the agent has self-keyed their Telegram account"
1738
+ default=False,
1739
+ description="Whether the agent has Telegram self key",
1713
1740
  ),
1714
1741
  ]
1715
1742
  linked_telegram_username: Annotated[
1716
- Optional[str],
1717
- PydanticField(description="The username of the linked Telegram account"),
1743
+ str | None,
1744
+ PydanticField(
1745
+ default=None,
1746
+ description="Linked Telegram username",
1747
+ ),
1718
1748
  ]
1719
1749
  linked_telegram_name: Annotated[
1720
- Optional[str],
1721
- PydanticField(description="The name of the linked Telegram account"),
1750
+ str | None,
1751
+ PydanticField(
1752
+ default=None,
1753
+ description="Linked Telegram display name",
1754
+ ),
1722
1755
  ]
1723
1756
  accept_image_input: Annotated[
1724
1757
  bool,
1725
1758
  PydanticField(
1726
- description="Whether the agent accepts image inputs in public mode"
1759
+ default=False,
1760
+ description="Whether the agent accepts image input",
1727
1761
  ),
1728
1762
  ]
1729
1763
  accept_image_input_private: Annotated[
1730
1764
  bool,
1731
1765
  PydanticField(
1732
- description="Whether the agent accepts image inputs in private mode"
1766
+ default=False,
1767
+ description="Whether the agent accepts image input in private mode",
1733
1768
  ),
1734
1769
  ]
1735
1770
 
@@ -1751,7 +1786,7 @@ class AgentResponse(BaseModel):
1751
1786
 
1752
1787
  @classmethod
1753
1788
  async def from_agent(
1754
- cls, agent: Agent, agent_data: Optional[AgentData] = None
1789
+ cls, agent: Agent, agent_data: AgentData | None = None
1755
1790
  ) -> "AgentResponse":
1756
1791
  """Create an AgentResponse from an Agent instance.
1757
1792
 
@@ -1762,45 +1797,6 @@ class AgentResponse(BaseModel):
1762
1797
  Returns:
1763
1798
  AgentResponse: Response model with additional processed data
1764
1799
  """
1765
- # Get base data from agent
1766
- data = agent.model_dump()
1767
-
1768
- # Filter sensitive fields from autonomous list
1769
- if data.get("autonomous"):
1770
- filtered_autonomous = []
1771
- for item in data["autonomous"]:
1772
- if isinstance(item, dict):
1773
- filtered_item = {
1774
- "id": item.get("id"),
1775
- "name": item.get("name"),
1776
- "enabled": item.get("enabled"),
1777
- }
1778
- filtered_autonomous.append(filtered_item)
1779
- data["autonomous"] = filtered_autonomous
1780
-
1781
- # Filter sensitive fields from skills dictionary
1782
- if data.get("skills"):
1783
- filtered_skills = {}
1784
- for skill_name, skill_config in data["skills"].items():
1785
- if isinstance(skill_config, dict):
1786
- # Only include skills that are enabled
1787
- if skill_config.get("enabled") is True:
1788
- filtered_config = {"enabled": True}
1789
- # Only keep states with public or private values
1790
- if "states" in skill_config and isinstance(
1791
- skill_config["states"], dict
1792
- ):
1793
- filtered_states = {}
1794
- for state_key, state_value in skill_config[
1795
- "states"
1796
- ].items():
1797
- if state_value in ["public", "private"]:
1798
- filtered_states[state_key] = state_value
1799
- if filtered_states:
1800
- filtered_config["states"] = filtered_states
1801
- filtered_skills[skill_name] = filtered_config
1802
- data["skills"] = filtered_skills
1803
-
1804
1800
  # Process CDP wallet address
1805
1801
  cdp_wallet_address = agent_data.evm_wallet_address if agent_data else None
1806
1802
  evm_wallet_address = agent_data.evm_wallet_address if agent_data else None
@@ -1815,8 +1811,7 @@ class AgentResponse(BaseModel):
1815
1811
  linked_twitter_name = agent_data.twitter_name
1816
1812
  if agent_data.twitter_access_token_expires_at:
1817
1813
  has_twitter_linked = (
1818
- agent_data.twitter_access_token_expires_at
1819
- > datetime.now(timezone.utc)
1814
+ agent_data.twitter_access_token_expires_at > datetime.now(UTC)
1820
1815
  )
1821
1816
  else:
1822
1817
  has_twitter_linked = True
@@ -1826,10 +1821,10 @@ class AgentResponse(BaseModel):
1826
1821
  agent_data and agent_data.twitter_self_key_refreshed_at
1827
1822
  )
1828
1823
 
1829
- # Process Telegram self-key status and remove token
1824
+ # Process Telegram self-key status
1830
1825
  linked_telegram_username = None
1831
1826
  linked_telegram_name = None
1832
- telegram_config = data.get("telegram_config", {})
1827
+ telegram_config = agent.telegram_config or {}
1833
1828
  has_telegram_self_key = bool(
1834
1829
  telegram_config and "token" in telegram_config and telegram_config["token"]
1835
1830
  )
@@ -1846,22 +1841,174 @@ class AgentResponse(BaseModel):
1846
1841
  or agent.has_image_parser_skill(is_private=True)
1847
1842
  )
1848
1843
 
1849
- # Add processed fields to response
1850
- data.update(
1851
- {
1852
- "cdp_wallet_address": cdp_wallet_address,
1853
- "evm_wallet_address": evm_wallet_address,
1854
- "solana_wallet_address": solana_wallet_address,
1855
- "has_twitter_linked": has_twitter_linked,
1856
- "linked_twitter_username": linked_twitter_username,
1857
- "linked_twitter_name": linked_twitter_name,
1858
- "has_twitter_self_key": has_twitter_self_key,
1859
- "has_telegram_self_key": has_telegram_self_key,
1860
- "linked_telegram_username": linked_telegram_username,
1861
- "linked_telegram_name": linked_telegram_name,
1862
- "accept_image_input": accept_image_input,
1863
- "accept_image_input_private": accept_image_input_private,
1864
- }
1844
+ # Create AgentResponse instance directly from agent with additional fields
1845
+ return cls(
1846
+ # Copy all fields from agent
1847
+ **agent.model_dump(),
1848
+ # Add computed fields
1849
+ cdp_wallet_address=cdp_wallet_address,
1850
+ evm_wallet_address=evm_wallet_address,
1851
+ solana_wallet_address=solana_wallet_address,
1852
+ has_twitter_linked=has_twitter_linked,
1853
+ linked_twitter_username=linked_twitter_username,
1854
+ linked_twitter_name=linked_twitter_name,
1855
+ has_twitter_self_key=has_twitter_self_key,
1856
+ has_telegram_self_key=has_telegram_self_key,
1857
+ linked_telegram_username=linked_telegram_username,
1858
+ linked_telegram_name=linked_telegram_name,
1859
+ accept_image_input=accept_image_input,
1860
+ accept_image_input_private=accept_image_input_private,
1861
+ )
1862
+
1863
+ def model_dump(
1864
+ self,
1865
+ *,
1866
+ mode: str | Literal["json", "python"] = "python",
1867
+ include: IncEx | None = None,
1868
+ exclude: IncEx | None = None,
1869
+ context: Any | None = None,
1870
+ by_alias: bool = False,
1871
+ exclude_unset: bool = False,
1872
+ exclude_defaults: bool = False,
1873
+ exclude_none: bool = False,
1874
+ round_trip: bool = False,
1875
+ warnings: bool | Literal["none", "warn", "error"] = True,
1876
+ serialize_as_any: bool = False,
1877
+ ) -> dict[str, Any]:
1878
+ """Override model_dump to exclude privacy fields and filter data."""
1879
+ # Get the base model dump
1880
+ data = super().model_dump(
1881
+ mode=mode,
1882
+ include=include,
1883
+ exclude=exclude,
1884
+ context=context,
1885
+ by_alias=by_alias,
1886
+ exclude_unset=exclude_unset,
1887
+ exclude_defaults=exclude_defaults,
1888
+ exclude_none=exclude_none,
1889
+ round_trip=round_trip,
1890
+ warnings=warnings,
1891
+ serialize_as_any=serialize_as_any,
1892
+ )
1893
+
1894
+ # Remove privacy fields that might still be present
1895
+ privacy_fields = {
1896
+ "purpose",
1897
+ "personality",
1898
+ "principles",
1899
+ "prompt",
1900
+ "prompt_append",
1901
+ "temperature",
1902
+ "frequency_penalty",
1903
+ "telegram_entrypoint_prompt",
1904
+ "telegram_config",
1905
+ "discord_config",
1906
+ "xmtp_entrypoint_prompt",
1907
+ }
1908
+ for field in privacy_fields:
1909
+ data.pop(field, None)
1910
+
1911
+ # Filter autonomous list to only keep safe fields
1912
+ if "autonomous" in data and data["autonomous"]:
1913
+ filtered_autonomous = []
1914
+ for item in data["autonomous"]:
1915
+ if isinstance(item, dict):
1916
+ # Only keep safe fields: id, name, description, enabled
1917
+ filtered_item = {
1918
+ key: item[key]
1919
+ for key in ["id", "name", "description", "enabled"]
1920
+ if key in item
1921
+ }
1922
+ filtered_autonomous.append(filtered_item)
1923
+ else:
1924
+ # Handle AgentAutonomous objects
1925
+ item_dict = (
1926
+ item.model_dump() if hasattr(item, "model_dump") else dict(item)
1927
+ )
1928
+ # Only keep safe fields: id, name, description, enabled
1929
+ filtered_item = {
1930
+ key: item_dict[key]
1931
+ for key in ["id", "name", "description", "enabled"]
1932
+ if key in item_dict
1933
+ }
1934
+ filtered_autonomous.append(filtered_item)
1935
+ data["autonomous"] = filtered_autonomous
1936
+
1937
+ # Convert examples to AgentExample instances if they're dictionaries
1938
+ if "examples" in data and data["examples"]:
1939
+ converted_examples = []
1940
+ for example in data["examples"]:
1941
+ if isinstance(example, dict):
1942
+ converted_examples.append(AgentExample(**example).model_dump())
1943
+ else:
1944
+ converted_examples.append(
1945
+ example.model_dump()
1946
+ if hasattr(example, "model_dump")
1947
+ else example
1948
+ )
1949
+ data["examples"] = converted_examples
1950
+
1951
+ # Filter skills to only include enabled ones with specific configurations
1952
+ if "skills" in data and data["skills"]:
1953
+ filtered_skills = {}
1954
+ for skill_name, skill_config in data["skills"].items():
1955
+ if (
1956
+ isinstance(skill_config, dict)
1957
+ and skill_config.get("enabled") is True
1958
+ ):
1959
+ # Filter out disabled states from the skill configuration
1960
+ original_states = skill_config.get("states", {})
1961
+ filtered_states = {
1962
+ state_name: state_value
1963
+ for state_name, state_value in original_states.items()
1964
+ if state_value != "disabled"
1965
+ }
1966
+
1967
+ # Only include the skill if it has at least one non-disabled state
1968
+ if filtered_states:
1969
+ filtered_config = {
1970
+ "enabled": skill_config["enabled"],
1971
+ "states": filtered_states,
1972
+ }
1973
+ # Add other non-sensitive config fields if needed
1974
+ for key in ["public", "private"]:
1975
+ if key in skill_config:
1976
+ filtered_config[key] = skill_config[key]
1977
+ filtered_skills[skill_name] = filtered_config
1978
+ data["skills"] = filtered_skills
1979
+
1980
+ return data
1981
+
1982
+ def model_dump_json(
1983
+ self,
1984
+ *,
1985
+ indent: int | str | None = None,
1986
+ include: IncEx | None = None,
1987
+ exclude: IncEx | None = None,
1988
+ context: Any | None = None,
1989
+ by_alias: bool = False,
1990
+ exclude_unset: bool = False,
1991
+ exclude_defaults: bool = False,
1992
+ exclude_none: bool = False,
1993
+ round_trip: bool = False,
1994
+ warnings: bool | Literal["none", "warn", "error"] = True,
1995
+ serialize_as_any: bool = False,
1996
+ ) -> str:
1997
+ """Override model_dump_json to exclude privacy fields and filter sensitive data."""
1998
+ # Get the filtered data using the same logic as model_dump
1999
+ data = self.model_dump(
2000
+ mode="json",
2001
+ include=include,
2002
+ exclude=exclude,
2003
+ context=context,
2004
+ by_alias=by_alias,
2005
+ exclude_unset=exclude_unset,
2006
+ exclude_defaults=exclude_defaults,
2007
+ exclude_none=exclude_none,
2008
+ round_trip=round_trip,
2009
+ warnings=warnings,
2010
+ serialize_as_any=serialize_as_any,
1865
2011
  )
1866
2012
 
1867
- return cls.model_validate(data)
2013
+ # Use json.dumps to serialize the filtered data with proper indentation
2014
+ return json.dumps(data, indent=indent, ensure_ascii=False)