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