intentkit 0.5.0__py3-none-any.whl → 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of intentkit might be problematic. Click here for more details.

Files changed (366) hide show
  1. intentkit/__init__.py +17 -0
  2. intentkit/abstracts/__init__.py +0 -0
  3. intentkit/abstracts/agent.py +60 -0
  4. intentkit/abstracts/api.py +4 -0
  5. intentkit/abstracts/engine.py +38 -0
  6. intentkit/abstracts/exception.py +9 -0
  7. intentkit/abstracts/graph.py +25 -0
  8. intentkit/abstracts/skill.py +129 -0
  9. intentkit/abstracts/twitter.py +54 -0
  10. intentkit/clients/__init__.py +14 -0
  11. intentkit/clients/cdp.py +53 -0
  12. intentkit/clients/twitter.py +445 -0
  13. intentkit/config/__init__.py +0 -0
  14. intentkit/config/config.py +164 -0
  15. intentkit/core/__init__.py +0 -0
  16. intentkit/core/agent.py +191 -0
  17. intentkit/core/api.py +40 -0
  18. intentkit/core/client.py +45 -0
  19. intentkit/core/credit.py +1767 -0
  20. intentkit/core/engine.py +1018 -0
  21. intentkit/core/node.py +223 -0
  22. intentkit/core/prompt.py +58 -0
  23. intentkit/core/skill.py +124 -0
  24. intentkit/models/agent.py +1689 -0
  25. intentkit/models/agent_data.py +810 -0
  26. intentkit/models/agent_schema.json +733 -0
  27. intentkit/models/app_setting.py +156 -0
  28. intentkit/models/base.py +9 -0
  29. intentkit/models/chat.py +581 -0
  30. intentkit/models/conversation.py +286 -0
  31. intentkit/models/credit.py +1406 -0
  32. intentkit/models/db.py +120 -0
  33. intentkit/models/db_mig.py +102 -0
  34. intentkit/models/generator.py +347 -0
  35. intentkit/models/llm.py +746 -0
  36. intentkit/models/redis.py +132 -0
  37. intentkit/models/skill.py +466 -0
  38. intentkit/models/user.py +243 -0
  39. intentkit/skills/__init__.py +12 -0
  40. intentkit/skills/acolyt/__init__.py +83 -0
  41. intentkit/skills/acolyt/acolyt.jpg +0 -0
  42. intentkit/skills/acolyt/ask.py +128 -0
  43. intentkit/skills/acolyt/base.py +28 -0
  44. intentkit/skills/acolyt/schema.json +89 -0
  45. intentkit/skills/aixbt/README.md +71 -0
  46. intentkit/skills/aixbt/__init__.py +73 -0
  47. intentkit/skills/aixbt/aixbt.jpg +0 -0
  48. intentkit/skills/aixbt/base.py +21 -0
  49. intentkit/skills/aixbt/projects.py +153 -0
  50. intentkit/skills/aixbt/schema.json +99 -0
  51. intentkit/skills/allora/__init__.py +83 -0
  52. intentkit/skills/allora/allora.jpeg +0 -0
  53. intentkit/skills/allora/base.py +28 -0
  54. intentkit/skills/allora/price.py +130 -0
  55. intentkit/skills/allora/schema.json +89 -0
  56. intentkit/skills/base.py +174 -0
  57. intentkit/skills/carv/README.md +95 -0
  58. intentkit/skills/carv/__init__.py +121 -0
  59. intentkit/skills/carv/base.py +183 -0
  60. intentkit/skills/carv/carv.webp +0 -0
  61. intentkit/skills/carv/fetch_news.py +92 -0
  62. intentkit/skills/carv/onchain_query.py +164 -0
  63. intentkit/skills/carv/schema.json +137 -0
  64. intentkit/skills/carv/token_info_and_price.py +110 -0
  65. intentkit/skills/cdp/__init__.py +137 -0
  66. intentkit/skills/cdp/base.py +21 -0
  67. intentkit/skills/cdp/cdp.png +0 -0
  68. intentkit/skills/cdp/get_balance.py +81 -0
  69. intentkit/skills/cdp/schema.json +473 -0
  70. intentkit/skills/chainlist/README.md +38 -0
  71. intentkit/skills/chainlist/__init__.py +54 -0
  72. intentkit/skills/chainlist/base.py +21 -0
  73. intentkit/skills/chainlist/chain_lookup.py +208 -0
  74. intentkit/skills/chainlist/chainlist.png +0 -0
  75. intentkit/skills/chainlist/schema.json +47 -0
  76. intentkit/skills/common/__init__.py +82 -0
  77. intentkit/skills/common/base.py +21 -0
  78. intentkit/skills/common/common.jpg +0 -0
  79. intentkit/skills/common/current_time.py +84 -0
  80. intentkit/skills/common/schema.json +57 -0
  81. intentkit/skills/cookiefun/README.md +121 -0
  82. intentkit/skills/cookiefun/__init__.py +78 -0
  83. intentkit/skills/cookiefun/base.py +41 -0
  84. intentkit/skills/cookiefun/constants.py +18 -0
  85. intentkit/skills/cookiefun/cookiefun.png +0 -0
  86. intentkit/skills/cookiefun/get_account_details.py +171 -0
  87. intentkit/skills/cookiefun/get_account_feed.py +282 -0
  88. intentkit/skills/cookiefun/get_account_smart_followers.py +181 -0
  89. intentkit/skills/cookiefun/get_sectors.py +128 -0
  90. intentkit/skills/cookiefun/schema.json +155 -0
  91. intentkit/skills/cookiefun/search_accounts.py +225 -0
  92. intentkit/skills/cryptocompare/__init__.py +130 -0
  93. intentkit/skills/cryptocompare/api.py +159 -0
  94. intentkit/skills/cryptocompare/base.py +303 -0
  95. intentkit/skills/cryptocompare/cryptocompare.png +0 -0
  96. intentkit/skills/cryptocompare/fetch_news.py +96 -0
  97. intentkit/skills/cryptocompare/fetch_price.py +99 -0
  98. intentkit/skills/cryptocompare/fetch_top_exchanges.py +113 -0
  99. intentkit/skills/cryptocompare/fetch_top_market_cap.py +109 -0
  100. intentkit/skills/cryptocompare/fetch_top_volume.py +108 -0
  101. intentkit/skills/cryptocompare/fetch_trading_signals.py +107 -0
  102. intentkit/skills/cryptocompare/schema.json +168 -0
  103. intentkit/skills/cryptopanic/__init__.py +108 -0
  104. intentkit/skills/cryptopanic/base.py +51 -0
  105. intentkit/skills/cryptopanic/cryptopanic.png +0 -0
  106. intentkit/skills/cryptopanic/fetch_crypto_news.py +153 -0
  107. intentkit/skills/cryptopanic/fetch_crypto_sentiment.py +136 -0
  108. intentkit/skills/cryptopanic/schema.json +103 -0
  109. intentkit/skills/dapplooker/README.md +92 -0
  110. intentkit/skills/dapplooker/__init__.py +83 -0
  111. intentkit/skills/dapplooker/base.py +26 -0
  112. intentkit/skills/dapplooker/dapplooker.jpg +0 -0
  113. intentkit/skills/dapplooker/dapplooker_token_data.py +476 -0
  114. intentkit/skills/dapplooker/schema.json +91 -0
  115. intentkit/skills/defillama/__init__.py +323 -0
  116. intentkit/skills/defillama/api.py +315 -0
  117. intentkit/skills/defillama/base.py +135 -0
  118. intentkit/skills/defillama/coins/__init__.py +0 -0
  119. intentkit/skills/defillama/coins/fetch_batch_historical_prices.py +116 -0
  120. intentkit/skills/defillama/coins/fetch_block.py +98 -0
  121. intentkit/skills/defillama/coins/fetch_current_prices.py +105 -0
  122. intentkit/skills/defillama/coins/fetch_first_price.py +100 -0
  123. intentkit/skills/defillama/coins/fetch_historical_prices.py +110 -0
  124. intentkit/skills/defillama/coins/fetch_price_chart.py +109 -0
  125. intentkit/skills/defillama/coins/fetch_price_percentage.py +93 -0
  126. intentkit/skills/defillama/config/__init__.py +0 -0
  127. intentkit/skills/defillama/config/chains.py +433 -0
  128. intentkit/skills/defillama/defillama.jpeg +0 -0
  129. intentkit/skills/defillama/fees/__init__.py +0 -0
  130. intentkit/skills/defillama/fees/fetch_fees_overview.py +130 -0
  131. intentkit/skills/defillama/schema.json +383 -0
  132. intentkit/skills/defillama/stablecoins/__init__.py +0 -0
  133. intentkit/skills/defillama/stablecoins/fetch_stablecoin_chains.py +100 -0
  134. intentkit/skills/defillama/stablecoins/fetch_stablecoin_charts.py +129 -0
  135. intentkit/skills/defillama/stablecoins/fetch_stablecoin_prices.py +83 -0
  136. intentkit/skills/defillama/stablecoins/fetch_stablecoins.py +126 -0
  137. intentkit/skills/defillama/tests/__init__.py +0 -0
  138. intentkit/skills/defillama/tests/api_integration.test.py +192 -0
  139. intentkit/skills/defillama/tests/api_unit.test.py +583 -0
  140. intentkit/skills/defillama/tvl/__init__.py +0 -0
  141. intentkit/skills/defillama/tvl/fetch_chain_historical_tvl.py +106 -0
  142. intentkit/skills/defillama/tvl/fetch_chains.py +107 -0
  143. intentkit/skills/defillama/tvl/fetch_historical_tvl.py +91 -0
  144. intentkit/skills/defillama/tvl/fetch_protocol.py +207 -0
  145. intentkit/skills/defillama/tvl/fetch_protocol_current_tvl.py +93 -0
  146. intentkit/skills/defillama/tvl/fetch_protocols.py +196 -0
  147. intentkit/skills/defillama/volumes/__init__.py +0 -0
  148. intentkit/skills/defillama/volumes/fetch_dex_overview.py +157 -0
  149. intentkit/skills/defillama/volumes/fetch_dex_summary.py +123 -0
  150. intentkit/skills/defillama/volumes/fetch_options_overview.py +131 -0
  151. intentkit/skills/defillama/yields/__init__.py +0 -0
  152. intentkit/skills/defillama/yields/fetch_pool_chart.py +100 -0
  153. intentkit/skills/defillama/yields/fetch_pools.py +126 -0
  154. intentkit/skills/dexscreener/__init__.py +93 -0
  155. intentkit/skills/dexscreener/base.py +133 -0
  156. intentkit/skills/dexscreener/dexscreener.png +0 -0
  157. intentkit/skills/dexscreener/model/__init__.py +0 -0
  158. intentkit/skills/dexscreener/model/search_token_response.py +82 -0
  159. intentkit/skills/dexscreener/schema.json +48 -0
  160. intentkit/skills/dexscreener/search_token.py +321 -0
  161. intentkit/skills/dune_analytics/__init__.py +103 -0
  162. intentkit/skills/dune_analytics/base.py +46 -0
  163. intentkit/skills/dune_analytics/dune.png +0 -0
  164. intentkit/skills/dune_analytics/fetch_kol_buys.py +128 -0
  165. intentkit/skills/dune_analytics/fetch_nation_metrics.py +237 -0
  166. intentkit/skills/dune_analytics/schema.json +99 -0
  167. intentkit/skills/elfa/README.md +100 -0
  168. intentkit/skills/elfa/__init__.py +123 -0
  169. intentkit/skills/elfa/base.py +28 -0
  170. intentkit/skills/elfa/elfa.jpg +0 -0
  171. intentkit/skills/elfa/mention.py +504 -0
  172. intentkit/skills/elfa/schema.json +153 -0
  173. intentkit/skills/elfa/stats.py +118 -0
  174. intentkit/skills/elfa/tokens.py +126 -0
  175. intentkit/skills/enso/README.md +75 -0
  176. intentkit/skills/enso/__init__.py +114 -0
  177. intentkit/skills/enso/abi/__init__.py +0 -0
  178. intentkit/skills/enso/abi/approval.py +279 -0
  179. intentkit/skills/enso/abi/erc20.py +14 -0
  180. intentkit/skills/enso/abi/route.py +129 -0
  181. intentkit/skills/enso/base.py +44 -0
  182. intentkit/skills/enso/best_yield.py +286 -0
  183. intentkit/skills/enso/enso.jpg +0 -0
  184. intentkit/skills/enso/networks.py +105 -0
  185. intentkit/skills/enso/prices.py +93 -0
  186. intentkit/skills/enso/route.py +300 -0
  187. intentkit/skills/enso/schema.json +212 -0
  188. intentkit/skills/enso/tokens.py +223 -0
  189. intentkit/skills/enso/wallet.py +381 -0
  190. intentkit/skills/github/README.md +63 -0
  191. intentkit/skills/github/__init__.py +54 -0
  192. intentkit/skills/github/base.py +21 -0
  193. intentkit/skills/github/github.jpg +0 -0
  194. intentkit/skills/github/github_search.py +183 -0
  195. intentkit/skills/github/schema.json +59 -0
  196. intentkit/skills/heurist/__init__.py +143 -0
  197. intentkit/skills/heurist/base.py +26 -0
  198. intentkit/skills/heurist/heurist.png +0 -0
  199. intentkit/skills/heurist/image_generation_animagine_xl.py +162 -0
  200. intentkit/skills/heurist/image_generation_arthemy_comics.py +162 -0
  201. intentkit/skills/heurist/image_generation_arthemy_real.py +162 -0
  202. intentkit/skills/heurist/image_generation_braindance.py +162 -0
  203. intentkit/skills/heurist/image_generation_cyber_realistic_xl.py +162 -0
  204. intentkit/skills/heurist/image_generation_flux_1_dev.py +162 -0
  205. intentkit/skills/heurist/image_generation_sdxl.py +161 -0
  206. intentkit/skills/heurist/schema.json +196 -0
  207. intentkit/skills/lifi/README.md +294 -0
  208. intentkit/skills/lifi/__init__.py +141 -0
  209. intentkit/skills/lifi/base.py +21 -0
  210. intentkit/skills/lifi/lifi.png +0 -0
  211. intentkit/skills/lifi/schema.json +89 -0
  212. intentkit/skills/lifi/token_execute.py +472 -0
  213. intentkit/skills/lifi/token_quote.py +190 -0
  214. intentkit/skills/lifi/utils.py +656 -0
  215. intentkit/skills/moralis/README.md +490 -0
  216. intentkit/skills/moralis/__init__.py +110 -0
  217. intentkit/skills/moralis/api.py +281 -0
  218. intentkit/skills/moralis/base.py +55 -0
  219. intentkit/skills/moralis/fetch_chain_portfolio.py +191 -0
  220. intentkit/skills/moralis/fetch_nft_portfolio.py +284 -0
  221. intentkit/skills/moralis/fetch_solana_portfolio.py +331 -0
  222. intentkit/skills/moralis/fetch_wallet_portfolio.py +301 -0
  223. intentkit/skills/moralis/moralis.png +0 -0
  224. intentkit/skills/moralis/schema.json +156 -0
  225. intentkit/skills/moralis/tests/__init__.py +0 -0
  226. intentkit/skills/moralis/tests/test_wallet.py +511 -0
  227. intentkit/skills/nation/__init__.py +62 -0
  228. intentkit/skills/nation/base.py +31 -0
  229. intentkit/skills/nation/nation.png +0 -0
  230. intentkit/skills/nation/nft_check.py +106 -0
  231. intentkit/skills/nation/schema.json +58 -0
  232. intentkit/skills/openai/__init__.py +107 -0
  233. intentkit/skills/openai/base.py +32 -0
  234. intentkit/skills/openai/dalle_image_generation.py +128 -0
  235. intentkit/skills/openai/gpt_image_generation.py +152 -0
  236. intentkit/skills/openai/gpt_image_to_image.py +186 -0
  237. intentkit/skills/openai/image_to_text.py +126 -0
  238. intentkit/skills/openai/openai.png +0 -0
  239. intentkit/skills/openai/schema.json +139 -0
  240. intentkit/skills/portfolio/README.md +55 -0
  241. intentkit/skills/portfolio/__init__.py +151 -0
  242. intentkit/skills/portfolio/base.py +107 -0
  243. intentkit/skills/portfolio/constants.py +9 -0
  244. intentkit/skills/portfolio/moralis.png +0 -0
  245. intentkit/skills/portfolio/schema.json +237 -0
  246. intentkit/skills/portfolio/token_balances.py +155 -0
  247. intentkit/skills/portfolio/wallet_approvals.py +102 -0
  248. intentkit/skills/portfolio/wallet_defi_positions.py +80 -0
  249. intentkit/skills/portfolio/wallet_history.py +155 -0
  250. intentkit/skills/portfolio/wallet_net_worth.py +112 -0
  251. intentkit/skills/portfolio/wallet_nfts.py +139 -0
  252. intentkit/skills/portfolio/wallet_profitability.py +101 -0
  253. intentkit/skills/portfolio/wallet_profitability_summary.py +91 -0
  254. intentkit/skills/portfolio/wallet_stats.py +79 -0
  255. intentkit/skills/portfolio/wallet_swaps.py +147 -0
  256. intentkit/skills/skills.toml +103 -0
  257. intentkit/skills/slack/__init__.py +98 -0
  258. intentkit/skills/slack/base.py +55 -0
  259. intentkit/skills/slack/get_channel.py +109 -0
  260. intentkit/skills/slack/get_message.py +136 -0
  261. intentkit/skills/slack/schedule_message.py +92 -0
  262. intentkit/skills/slack/schema.json +135 -0
  263. intentkit/skills/slack/send_message.py +81 -0
  264. intentkit/skills/slack/slack.jpg +0 -0
  265. intentkit/skills/system/__init__.py +90 -0
  266. intentkit/skills/system/base.py +22 -0
  267. intentkit/skills/system/read_agent_api_key.py +87 -0
  268. intentkit/skills/system/regenerate_agent_api_key.py +77 -0
  269. intentkit/skills/system/schema.json +53 -0
  270. intentkit/skills/system/system.svg +76 -0
  271. intentkit/skills/tavily/README.md +86 -0
  272. intentkit/skills/tavily/__init__.py +91 -0
  273. intentkit/skills/tavily/base.py +27 -0
  274. intentkit/skills/tavily/schema.json +119 -0
  275. intentkit/skills/tavily/tavily.jpg +0 -0
  276. intentkit/skills/tavily/tavily_extract.py +147 -0
  277. intentkit/skills/tavily/tavily_search.py +139 -0
  278. intentkit/skills/token/README.md +89 -0
  279. intentkit/skills/token/__init__.py +107 -0
  280. intentkit/skills/token/base.py +154 -0
  281. intentkit/skills/token/constants.py +9 -0
  282. intentkit/skills/token/erc20_transfers.py +145 -0
  283. intentkit/skills/token/moralis.png +0 -0
  284. intentkit/skills/token/schema.json +141 -0
  285. intentkit/skills/token/token_analytics.py +81 -0
  286. intentkit/skills/token/token_price.py +132 -0
  287. intentkit/skills/token/token_search.py +121 -0
  288. intentkit/skills/twitter/__init__.py +146 -0
  289. intentkit/skills/twitter/base.py +68 -0
  290. intentkit/skills/twitter/follow_user.py +69 -0
  291. intentkit/skills/twitter/get_mentions.py +124 -0
  292. intentkit/skills/twitter/get_timeline.py +111 -0
  293. intentkit/skills/twitter/get_user_by_username.py +84 -0
  294. intentkit/skills/twitter/get_user_tweets.py +123 -0
  295. intentkit/skills/twitter/like_tweet.py +65 -0
  296. intentkit/skills/twitter/post_tweet.py +90 -0
  297. intentkit/skills/twitter/reply_tweet.py +98 -0
  298. intentkit/skills/twitter/retweet.py +76 -0
  299. intentkit/skills/twitter/schema.json +258 -0
  300. intentkit/skills/twitter/search_tweets.py +115 -0
  301. intentkit/skills/twitter/twitter.png +0 -0
  302. intentkit/skills/unrealspeech/__init__.py +55 -0
  303. intentkit/skills/unrealspeech/base.py +21 -0
  304. intentkit/skills/unrealspeech/schema.json +100 -0
  305. intentkit/skills/unrealspeech/text_to_speech.py +177 -0
  306. intentkit/skills/unrealspeech/unrealspeech.jpg +0 -0
  307. intentkit/skills/venice_audio/__init__.py +106 -0
  308. intentkit/skills/venice_audio/base.py +119 -0
  309. intentkit/skills/venice_audio/input.py +41 -0
  310. intentkit/skills/venice_audio/schema.json +152 -0
  311. intentkit/skills/venice_audio/venice_audio.py +240 -0
  312. intentkit/skills/venice_audio/venice_logo.jpg +0 -0
  313. intentkit/skills/venice_image/README.md +119 -0
  314. intentkit/skills/venice_image/__init__.py +154 -0
  315. intentkit/skills/venice_image/api.py +138 -0
  316. intentkit/skills/venice_image/base.py +188 -0
  317. intentkit/skills/venice_image/config.py +35 -0
  318. intentkit/skills/venice_image/image_enhance/README.md +119 -0
  319. intentkit/skills/venice_image/image_enhance/__init__.py +0 -0
  320. intentkit/skills/venice_image/image_enhance/image_enhance.py +80 -0
  321. intentkit/skills/venice_image/image_enhance/image_enhance_base.py +23 -0
  322. intentkit/skills/venice_image/image_enhance/image_enhance_input.py +40 -0
  323. intentkit/skills/venice_image/image_generation/README.md +144 -0
  324. intentkit/skills/venice_image/image_generation/__init__.py +0 -0
  325. intentkit/skills/venice_image/image_generation/image_generation_base.py +117 -0
  326. intentkit/skills/venice_image/image_generation/image_generation_fluently_xl.py +26 -0
  327. intentkit/skills/venice_image/image_generation/image_generation_flux_dev.py +27 -0
  328. intentkit/skills/venice_image/image_generation/image_generation_flux_dev_uncensored.py +26 -0
  329. intentkit/skills/venice_image/image_generation/image_generation_input.py +158 -0
  330. intentkit/skills/venice_image/image_generation/image_generation_lustify_sdxl.py +26 -0
  331. intentkit/skills/venice_image/image_generation/image_generation_pony_realism.py +26 -0
  332. intentkit/skills/venice_image/image_generation/image_generation_stable_diffusion_3_5.py +28 -0
  333. intentkit/skills/venice_image/image_generation/image_generation_venice_sd35.py +28 -0
  334. intentkit/skills/venice_image/image_upscale/README.md +111 -0
  335. intentkit/skills/venice_image/image_upscale/__init__.py +0 -0
  336. intentkit/skills/venice_image/image_upscale/image_upscale.py +90 -0
  337. intentkit/skills/venice_image/image_upscale/image_upscale_base.py +23 -0
  338. intentkit/skills/venice_image/image_upscale/image_upscale_input.py +22 -0
  339. intentkit/skills/venice_image/image_vision/README.md +112 -0
  340. intentkit/skills/venice_image/image_vision/__init__.py +0 -0
  341. intentkit/skills/venice_image/image_vision/image_vision.py +100 -0
  342. intentkit/skills/venice_image/image_vision/image_vision_base.py +17 -0
  343. intentkit/skills/venice_image/image_vision/image_vision_input.py +9 -0
  344. intentkit/skills/venice_image/schema.json +267 -0
  345. intentkit/skills/venice_image/utils.py +78 -0
  346. intentkit/skills/venice_image/venice_image.jpg +0 -0
  347. intentkit/skills/web_scraper/README.md +82 -0
  348. intentkit/skills/web_scraper/__init__.py +92 -0
  349. intentkit/skills/web_scraper/base.py +21 -0
  350. intentkit/skills/web_scraper/langchain.png +0 -0
  351. intentkit/skills/web_scraper/schema.json +115 -0
  352. intentkit/skills/web_scraper/scrape_and_index.py +327 -0
  353. intentkit/utils/__init__.py +1 -0
  354. intentkit/utils/chain.py +436 -0
  355. intentkit/utils/error.py +134 -0
  356. intentkit/utils/logging.py +70 -0
  357. intentkit/utils/middleware.py +61 -0
  358. intentkit/utils/random.py +16 -0
  359. intentkit/utils/s3.py +267 -0
  360. intentkit/utils/slack_alert.py +79 -0
  361. intentkit/utils/tx.py +37 -0
  362. {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/METADATA +1 -1
  363. intentkit-0.5.2.dist-info/RECORD +365 -0
  364. intentkit-0.5.0.dist-info/RECORD +0 -4
  365. {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/WHEEL +0 -0
  366. {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1767 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from decimal import ROUND_HALF_UP, Decimal
4
+ from typing import List, Optional, Tuple
5
+
6
+ from epyxid import XID
7
+ from fastapi import HTTPException
8
+ from pydantic import BaseModel
9
+ from sqlalchemy import desc, select
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from intentkit.models.agent import Agent
13
+ from intentkit.models.app_setting import AppSetting
14
+ from intentkit.models.credit import (
15
+ DEFAULT_PLATFORM_ACCOUNT_ADJUSTMENT,
16
+ DEFAULT_PLATFORM_ACCOUNT_DEV,
17
+ DEFAULT_PLATFORM_ACCOUNT_FEE,
18
+ DEFAULT_PLATFORM_ACCOUNT_MEMORY,
19
+ DEFAULT_PLATFORM_ACCOUNT_MESSAGE,
20
+ DEFAULT_PLATFORM_ACCOUNT_RECHARGE,
21
+ DEFAULT_PLATFORM_ACCOUNT_REFILL,
22
+ DEFAULT_PLATFORM_ACCOUNT_REWARD,
23
+ DEFAULT_PLATFORM_ACCOUNT_SKILL,
24
+ CreditAccount,
25
+ CreditAccountTable,
26
+ CreditDebit,
27
+ CreditEvent,
28
+ CreditEventTable,
29
+ CreditTransactionTable,
30
+ CreditType,
31
+ Direction,
32
+ EventType,
33
+ OwnerType,
34
+ RewardType,
35
+ TransactionType,
36
+ UpstreamType,
37
+ )
38
+ from intentkit.models.db import get_session
39
+ from intentkit.models.skill import Skill
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Define the precision for all decimal calculations (4 decimal places)
44
+ FOURPLACES = Decimal("0.0001")
45
+
46
+
47
+ async def update_credit_event_note(
48
+ session: AsyncSession,
49
+ event_id: str,
50
+ note: Optional[str] = None,
51
+ ) -> CreditEvent:
52
+ """
53
+ Update the note of a credit event.
54
+
55
+ Args:
56
+ session: Async session to use for database operations
57
+ event_id: ID of the event to update
58
+ note: New note for the event
59
+
60
+ Returns:
61
+ Updated credit event
62
+
63
+ Raises:
64
+ HTTPException: If event is not found
65
+ """
66
+ # Find the event
67
+ stmt = select(CreditEventTable).where(CreditEventTable.id == event_id)
68
+ result = await session.execute(stmt)
69
+ event = result.scalar_one_or_none()
70
+
71
+ if not event:
72
+ raise HTTPException(status_code=404, detail="Credit event not found")
73
+
74
+ # Update the note
75
+ event.note = note
76
+ await session.commit()
77
+ await session.refresh(event)
78
+
79
+ return CreditEvent.model_validate(event)
80
+
81
+
82
+ async def recharge(
83
+ session: AsyncSession,
84
+ user_id: str,
85
+ amount: Decimal,
86
+ upstream_tx_id: str,
87
+ note: Optional[str] = None,
88
+ ) -> CreditAccount:
89
+ """
90
+ Recharge credits to a user account.
91
+
92
+ Args:
93
+ session: Async session to use for database operations
94
+ user_id: ID of the user to recharge
95
+ amount: Amount of credits to recharge
96
+ upstream_tx_id: ID of the upstream transaction
97
+ note: Optional note for the transaction
98
+
99
+ Returns:
100
+ Updated user credit account
101
+ """
102
+ # Check for idempotency - prevent duplicate transactions
103
+ await CreditEvent.check_upstream_tx_id_exists(
104
+ session, UpstreamType.API, upstream_tx_id
105
+ )
106
+
107
+ if amount <= Decimal("0"):
108
+ raise ValueError("Recharge amount must be positive")
109
+
110
+ # 1. Create credit event record first to get event_id
111
+ event_id = str(XID())
112
+
113
+ # 2. Update user account - add credits
114
+ user_account = await CreditAccount.income_in_session(
115
+ session=session,
116
+ owner_type=OwnerType.USER,
117
+ owner_id=user_id,
118
+ amount=amount,
119
+ credit_type=CreditType.PERMANENT, # Recharge adds to permanent credits
120
+ event_id=event_id,
121
+ )
122
+
123
+ # 3. Update platform recharge account - deduct credits
124
+ platform_account = await CreditAccount.deduction_in_session(
125
+ session=session,
126
+ owner_type=OwnerType.PLATFORM,
127
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_RECHARGE,
128
+ credit_type=CreditType.PERMANENT,
129
+ amount=amount,
130
+ event_id=event_id,
131
+ )
132
+
133
+ # 4. Create credit event record
134
+ event = CreditEventTable(
135
+ id=event_id,
136
+ event_type=EventType.RECHARGE,
137
+ user_id=user_id,
138
+ upstream_type=UpstreamType.API,
139
+ upstream_tx_id=upstream_tx_id,
140
+ direction=Direction.INCOME,
141
+ account_id=user_account.id,
142
+ total_amount=amount,
143
+ credit_type=CreditType.PERMANENT,
144
+ credit_types=[CreditType.PERMANENT],
145
+ balance_after=user_account.credits
146
+ + user_account.free_credits
147
+ + user_account.reward_credits,
148
+ base_amount=amount,
149
+ base_original_amount=amount,
150
+ permanent_amount=amount, # Set permanent_amount since this is a permanent credit
151
+ free_amount=Decimal("0"), # No free credits involved
152
+ reward_amount=Decimal("0"), # No reward credits involved
153
+ note=note,
154
+ )
155
+ session.add(event)
156
+ await session.flush()
157
+
158
+ # 4. Create credit transaction records
159
+ # 4.1 User account transaction (credit)
160
+ user_tx = CreditTransactionTable(
161
+ id=str(XID()),
162
+ account_id=user_account.id,
163
+ event_id=event_id,
164
+ tx_type=TransactionType.RECHARGE,
165
+ credit_debit=CreditDebit.CREDIT,
166
+ change_amount=amount,
167
+ credit_type=CreditType.PERMANENT,
168
+ )
169
+ session.add(user_tx)
170
+
171
+ # 4.2 Platform recharge account transaction (debit)
172
+ platform_tx = CreditTransactionTable(
173
+ id=str(XID()),
174
+ account_id=platform_account.id,
175
+ event_id=event_id,
176
+ tx_type=TransactionType.RECHARGE,
177
+ credit_debit=CreditDebit.DEBIT,
178
+ change_amount=amount,
179
+ credit_type=CreditType.PERMANENT,
180
+ )
181
+ session.add(platform_tx)
182
+
183
+ # Commit all changes
184
+ await session.commit()
185
+
186
+ return user_account
187
+
188
+
189
+ async def reward(
190
+ session: AsyncSession,
191
+ user_id: str,
192
+ amount: Decimal,
193
+ upstream_tx_id: str,
194
+ note: Optional[str] = None,
195
+ reward_type: Optional[RewardType] = RewardType.REWARD,
196
+ ) -> CreditAccount:
197
+ """
198
+ Reward a user account with reward credits.
199
+
200
+ Args:
201
+ session: Async session to use for database operations
202
+ user_id: ID of the user to reward
203
+ amount: Amount of reward credits to add
204
+ upstream_tx_id: ID of the upstream transaction
205
+ note: Optional note for the transaction
206
+
207
+ Returns:
208
+ Updated user credit account
209
+ """
210
+ # Check for idempotency - prevent duplicate transactions
211
+ await CreditEvent.check_upstream_tx_id_exists(
212
+ session, UpstreamType.API, upstream_tx_id
213
+ )
214
+
215
+ if amount <= Decimal("0"):
216
+ raise ValueError("Reward amount must be positive")
217
+
218
+ # 1. Create credit event record first to get event_id
219
+ event_id = str(XID())
220
+
221
+ # 2. Update user account - add reward credits
222
+ user_account = await CreditAccount.income_in_session(
223
+ session=session,
224
+ owner_type=OwnerType.USER,
225
+ owner_id=user_id,
226
+ amount=amount,
227
+ credit_type=CreditType.REWARD, # Reward adds to reward credits
228
+ event_id=event_id,
229
+ )
230
+
231
+ # 3. Update platform reward account - deduct credits
232
+ platform_account = await CreditAccount.deduction_in_session(
233
+ session=session,
234
+ owner_type=OwnerType.PLATFORM,
235
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_REWARD,
236
+ credit_type=CreditType.REWARD,
237
+ amount=amount,
238
+ event_id=event_id,
239
+ )
240
+
241
+ # 4. Create credit event record
242
+ event = CreditEventTable(
243
+ id=event_id,
244
+ event_type=reward_type,
245
+ user_id=user_id,
246
+ upstream_type=UpstreamType.API,
247
+ upstream_tx_id=upstream_tx_id,
248
+ direction=Direction.INCOME,
249
+ account_id=user_account.id,
250
+ total_amount=amount,
251
+ credit_type=CreditType.REWARD,
252
+ credit_types=[CreditType.REWARD],
253
+ balance_after=user_account.credits
254
+ + user_account.free_credits
255
+ + user_account.reward_credits,
256
+ base_amount=amount,
257
+ base_original_amount=amount,
258
+ reward_amount=amount, # Set reward_amount since this is a reward credit
259
+ free_amount=Decimal("0"), # No free credits involved
260
+ permanent_amount=Decimal("0"), # No permanent credits involved
261
+ note=note,
262
+ )
263
+ session.add(event)
264
+ await session.flush()
265
+
266
+ # 4. Create credit transaction records
267
+ # 4.1 User account transaction (credit)
268
+ user_tx = CreditTransactionTable(
269
+ id=str(XID()),
270
+ account_id=user_account.id,
271
+ event_id=event_id,
272
+ tx_type=reward_type,
273
+ credit_debit=CreditDebit.CREDIT,
274
+ change_amount=amount,
275
+ credit_type=CreditType.REWARD,
276
+ )
277
+ session.add(user_tx)
278
+
279
+ # 4.2 Platform reward account transaction (debit)
280
+ platform_tx = CreditTransactionTable(
281
+ id=str(XID()),
282
+ account_id=platform_account.id,
283
+ event_id=event_id,
284
+ tx_type=reward_type,
285
+ credit_debit=CreditDebit.DEBIT,
286
+ change_amount=amount,
287
+ credit_type=CreditType.REWARD,
288
+ )
289
+ session.add(platform_tx)
290
+
291
+ # Commit all changes
292
+ await session.commit()
293
+
294
+ return user_account
295
+
296
+
297
+ async def adjustment(
298
+ session: AsyncSession,
299
+ user_id: str,
300
+ credit_type: CreditType,
301
+ amount: Decimal,
302
+ upstream_tx_id: str,
303
+ note: str,
304
+ ) -> CreditAccount:
305
+ """
306
+ Adjust a user account's credits (can be positive or negative).
307
+
308
+ Args:
309
+ session: Async session to use for database operations
310
+ user_id: ID of the user to adjust
311
+ credit_type: Type of credit to adjust (FREE, REWARD, or PERMANENT)
312
+ amount: Amount to adjust (positive for increase, negative for decrease)
313
+ upstream_tx_id: ID of the upstream transaction
314
+ note: Required explanation for the adjustment
315
+
316
+ Returns:
317
+ Updated user credit account
318
+ """
319
+ # Check for idempotency - prevent duplicate transactions
320
+ await CreditEvent.check_upstream_tx_id_exists(
321
+ session, UpstreamType.API, upstream_tx_id
322
+ )
323
+
324
+ if amount == Decimal("0"):
325
+ raise ValueError("Adjustment amount cannot be zero")
326
+
327
+ if not note:
328
+ raise ValueError("Adjustment requires a note explaining the reason")
329
+
330
+ # Determine direction based on amount sign
331
+ is_income = amount > Decimal("0")
332
+ abs_amount = abs(amount)
333
+ direction = Direction.INCOME if is_income else Direction.EXPENSE
334
+ credit_debit_user = CreditDebit.CREDIT if is_income else CreditDebit.DEBIT
335
+ credit_debit_platform = CreditDebit.DEBIT if is_income else CreditDebit.CREDIT
336
+
337
+ # 1. Create credit event record first to get event_id
338
+ event_id = str(XID())
339
+
340
+ # 2. Update user account
341
+ if is_income:
342
+ user_account = await CreditAccount.income_in_session(
343
+ session=session,
344
+ owner_type=OwnerType.USER,
345
+ owner_id=user_id,
346
+ amount=abs_amount,
347
+ credit_type=credit_type,
348
+ event_id=event_id,
349
+ )
350
+ else:
351
+ # Deduct the credits using deduction_in_session
352
+ # For adjustment, we don't check if the user has enough credits
353
+ # It can be positive or negative
354
+ user_account = await CreditAccount.deduction_in_session(
355
+ session=session,
356
+ owner_type=OwnerType.USER,
357
+ owner_id=user_id,
358
+ credit_type=credit_type,
359
+ amount=abs_amount,
360
+ event_id=event_id,
361
+ )
362
+
363
+ # 3. Update platform adjustment account
364
+ if is_income:
365
+ platform_account = await CreditAccount.deduction_in_session(
366
+ session=session,
367
+ owner_type=OwnerType.PLATFORM,
368
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_ADJUSTMENT,
369
+ credit_type=credit_type,
370
+ amount=abs_amount,
371
+ event_id=event_id,
372
+ )
373
+ else:
374
+ platform_account = await CreditAccount.income_in_session(
375
+ session=session,
376
+ owner_type=OwnerType.PLATFORM,
377
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_ADJUSTMENT,
378
+ amount=abs_amount,
379
+ credit_type=credit_type,
380
+ event_id=event_id,
381
+ )
382
+
383
+ # 4. Create credit event record
384
+ # Set the appropriate credit amount field based on credit type
385
+ free_amount = Decimal("0")
386
+ reward_amount = Decimal("0")
387
+ permanent_amount = Decimal("0")
388
+
389
+ if credit_type == CreditType.FREE:
390
+ free_amount = abs_amount
391
+ elif credit_type == CreditType.REWARD:
392
+ reward_amount = abs_amount
393
+ elif credit_type == CreditType.PERMANENT:
394
+ permanent_amount = abs_amount
395
+
396
+ event = CreditEventTable(
397
+ id=event_id,
398
+ event_type=EventType.ADJUSTMENT,
399
+ user_id=user_id,
400
+ upstream_type=UpstreamType.API,
401
+ upstream_tx_id=upstream_tx_id,
402
+ direction=direction,
403
+ account_id=user_account.id,
404
+ total_amount=abs_amount,
405
+ credit_type=credit_type,
406
+ credit_types=[credit_type],
407
+ balance_after=user_account.credits
408
+ + user_account.free_credits
409
+ + user_account.reward_credits,
410
+ base_amount=abs_amount,
411
+ base_original_amount=abs_amount,
412
+ free_amount=free_amount,
413
+ reward_amount=reward_amount,
414
+ permanent_amount=permanent_amount,
415
+ note=note,
416
+ )
417
+ session.add(event)
418
+ await session.flush()
419
+
420
+ # 4. Create credit transaction records
421
+ # 4.1 User account transaction
422
+ user_tx = CreditTransactionTable(
423
+ id=str(XID()),
424
+ account_id=user_account.id,
425
+ event_id=event_id,
426
+ tx_type=TransactionType.ADJUSTMENT,
427
+ credit_debit=credit_debit_user,
428
+ change_amount=abs_amount,
429
+ credit_type=credit_type,
430
+ )
431
+ session.add(user_tx)
432
+
433
+ # 4.2 Platform adjustment account transaction
434
+ platform_tx = CreditTransactionTable(
435
+ id=str(XID()),
436
+ account_id=platform_account.id,
437
+ event_id=event_id,
438
+ tx_type=TransactionType.ADJUSTMENT,
439
+ credit_debit=credit_debit_platform,
440
+ change_amount=abs_amount,
441
+ credit_type=credit_type,
442
+ )
443
+ session.add(platform_tx)
444
+
445
+ # Commit all changes
446
+ await session.commit()
447
+
448
+ return user_account
449
+
450
+
451
+ async def update_daily_quota(
452
+ session: AsyncSession,
453
+ user_id: str,
454
+ free_quota: Optional[Decimal] = None,
455
+ refill_amount: Optional[Decimal] = None,
456
+ upstream_tx_id: str = "",
457
+ note: str = "",
458
+ ) -> CreditAccount:
459
+ """
460
+ Update the daily quota and refill amount of a user's credit account.
461
+
462
+ Args:
463
+ session: Async session to use for database operations
464
+ user_id: ID of the user to update
465
+ free_quota: Optional new daily quota value
466
+ refill_amount: Optional amount to refill hourly, not exceeding free_quota
467
+ upstream_tx_id: ID of the upstream transaction (for logging purposes)
468
+ note: Explanation for changing the daily quota
469
+
470
+ Returns:
471
+ Updated user credit account
472
+ """
473
+ return await CreditAccount.update_daily_quota(
474
+ session, user_id, free_quota, refill_amount, upstream_tx_id, note
475
+ )
476
+
477
+
478
+ async def list_credit_events_by_user(
479
+ session: AsyncSession,
480
+ user_id: str,
481
+ direction: Optional[Direction] = None,
482
+ cursor: Optional[str] = None,
483
+ limit: int = 20,
484
+ event_type: Optional[EventType] = None,
485
+ ) -> Tuple[List[CreditEvent], Optional[str], bool]:
486
+ """
487
+ List credit events for a user account with cursor pagination.
488
+
489
+ Args:
490
+ session: Async database session.
491
+ user_id: The ID of the user.
492
+ direction: The direction of the events (INCOME or EXPENSE).
493
+ cursor: The ID of the last event from the previous page.
494
+ limit: Maximum number of events to return per page.
495
+ event_type: Optional filter for specific event type.
496
+
497
+ Returns:
498
+ A tuple containing:
499
+ - A list of CreditEvent models.
500
+ - The cursor for the next page (ID of the last event in the list).
501
+ - A boolean indicating if there are more events available.
502
+ """
503
+ # 1. Find the account for the owner
504
+ account = await CreditAccount.get_in_session(session, OwnerType.USER, user_id)
505
+ if not account:
506
+ # Decide if returning empty or raising error is better. Empty list seems reasonable.
507
+ # Or raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"{owner_type.value.capitalize()} account not found")
508
+ return [], None, False
509
+
510
+ # 2. Build the query
511
+ stmt = (
512
+ select(CreditEventTable)
513
+ .where(CreditEventTable.account_id == account.id)
514
+ .order_by(desc(CreditEventTable.id))
515
+ .limit(limit + 1) # Fetch one extra to check if there are more
516
+ )
517
+
518
+ # 3. Apply optional filter if provided
519
+ if direction:
520
+ stmt = stmt.where(CreditEventTable.direction == direction.value)
521
+ if event_type:
522
+ stmt = stmt.where(CreditEventTable.event_type == event_type.value)
523
+
524
+ # 4. Apply cursor filter if provided
525
+ if cursor:
526
+ stmt = stmt.where(CreditEventTable.id < cursor)
527
+
528
+ # 5. Execute query
529
+ result = await session.execute(stmt)
530
+ events_data = result.scalars().all()
531
+
532
+ # 6. Determine pagination details
533
+ has_more = len(events_data) > limit
534
+ events_to_return = events_data[:limit] # Slice to the requested limit
535
+
536
+ next_cursor = events_to_return[-1].id if events_to_return and has_more else None
537
+
538
+ # 7. Convert to Pydantic models
539
+ events_models = [CreditEvent.model_validate(event) for event in events_to_return]
540
+
541
+ return events_models, next_cursor, has_more
542
+
543
+
544
+ async def list_credit_events(
545
+ session: AsyncSession,
546
+ direction: Optional[Direction] = Direction.EXPENSE,
547
+ cursor: Optional[str] = None,
548
+ limit: int = 20,
549
+ event_type: Optional[EventType] = None,
550
+ start_at: Optional[datetime] = None,
551
+ end_at: Optional[datetime] = None,
552
+ ) -> Tuple[List[CreditEvent], Optional[str], bool]:
553
+ """
554
+ List all credit events with cursor pagination.
555
+
556
+ Args:
557
+ session: Async database session.
558
+ direction: The direction of the events (INCOME or EXPENSE). Default is EXPENSE.
559
+ cursor: The ID of the last event from the previous page.
560
+ limit: Maximum number of events to return per page.
561
+ event_type: Optional filter for specific event type.
562
+ start_at: Optional start datetime to filter events by created_at.
563
+ end_at: Optional end datetime to filter events by created_at.
564
+
565
+ Returns:
566
+ A tuple containing:
567
+ - A list of CreditEvent models.
568
+ - The cursor for the next page (ID of the last event in the list).
569
+ - A boolean indicating if there are more events available.
570
+ """
571
+ # Build the query
572
+ stmt = (
573
+ select(CreditEventTable)
574
+ .order_by(CreditEventTable.id) # Ascending order as required
575
+ .limit(limit + 1) # Fetch one extra to check if there are more
576
+ )
577
+
578
+ # Apply direction filter (default is EXPENSE)
579
+ if direction:
580
+ stmt = stmt.where(CreditEventTable.direction == direction.value)
581
+
582
+ # Apply optional event_type filter if provided
583
+ if event_type:
584
+ stmt = stmt.where(CreditEventTable.event_type == event_type.value)
585
+
586
+ # Apply datetime filters if provided
587
+ if start_at:
588
+ stmt = stmt.where(CreditEventTable.created_at >= start_at)
589
+ if end_at:
590
+ stmt = stmt.where(CreditEventTable.created_at < end_at)
591
+
592
+ # Apply cursor filter if provided
593
+ if cursor:
594
+ stmt = stmt.where(CreditEventTable.id > cursor) # Using > for ascending order
595
+
596
+ # Execute query
597
+ result = await session.execute(stmt)
598
+ events_data = result.scalars().all()
599
+
600
+ # Determine pagination details
601
+ has_more = len(events_data) > limit
602
+ events_to_return = events_data[:limit] # Slice to the requested limit
603
+
604
+ # always return a cursor even there is no next page
605
+ next_cursor = events_to_return[-1].id if events_to_return else None
606
+
607
+ # Convert to Pydantic models
608
+ events_models = [CreditEvent.model_validate(event) for event in events_to_return]
609
+
610
+ return events_models, next_cursor, has_more
611
+
612
+
613
+ async def list_fee_events_by_agent(
614
+ session: AsyncSession,
615
+ agent_id: str,
616
+ cursor: Optional[str] = None,
617
+ limit: int = 20,
618
+ ) -> Tuple[List[CreditEvent], Optional[str], bool]:
619
+ """
620
+ List fee events for an agent with cursor pagination.
621
+ These events represent income for the agent from users' expenses.
622
+
623
+ Args:
624
+ session: Async database session.
625
+ agent_id: The ID of the agent.
626
+ cursor: The ID of the last event from the previous page.
627
+ limit: Maximum number of events to return per page.
628
+
629
+ Returns:
630
+ A tuple containing:
631
+ - A list of CreditEvent models.
632
+ - The cursor for the next page (ID of the last event in the list).
633
+ - A boolean indicating if there are more events available.
634
+ """
635
+ # 1. Find the account for the agent
636
+ agent_account = await CreditAccount.get_in_session(
637
+ session, OwnerType.AGENT, agent_id
638
+ )
639
+ if not agent_account:
640
+ return [], None, False
641
+
642
+ # 2. Build the query to find events where fee_agent_amount > 0 and fee_agent_account = agent_account.id
643
+ stmt = (
644
+ select(CreditEventTable)
645
+ .where(CreditEventTable.fee_agent_account == agent_account.id)
646
+ .where(CreditEventTable.fee_agent_amount > 0)
647
+ .order_by(desc(CreditEventTable.id))
648
+ .limit(limit + 1) # Fetch one extra to check if there are more
649
+ )
650
+
651
+ # 3. Apply cursor filter if provided
652
+ if cursor:
653
+ stmt = stmt.where(CreditEventTable.id < cursor)
654
+
655
+ # 4. Execute query
656
+ result = await session.execute(stmt)
657
+ events_data = result.scalars().all()
658
+
659
+ # 5. Determine pagination details
660
+ has_more = len(events_data) > limit
661
+ events_to_return = events_data[:limit] # Slice to the requested limit
662
+
663
+ next_cursor = events_to_return[-1].id if events_to_return and has_more else None
664
+
665
+ # 6. Convert to Pydantic models
666
+ events_models = [CreditEvent.model_validate(event) for event in events_to_return]
667
+
668
+ return events_models, next_cursor, has_more
669
+
670
+
671
+ async def fetch_credit_event_by_upstream_tx_id(
672
+ session: AsyncSession,
673
+ upstream_tx_id: str,
674
+ ) -> CreditEvent:
675
+ """
676
+ Fetch a credit event by its upstream transaction ID.
677
+
678
+ Args:
679
+ session: Async database session.
680
+ upstream_tx_id: ID of the upstream transaction.
681
+
682
+ Returns:
683
+ The credit event if found.
684
+
685
+ Raises:
686
+ HTTPException: If the credit event is not found.
687
+ """
688
+ # Build the query to find the event by upstream_tx_id
689
+ stmt = select(CreditEventTable).where(
690
+ CreditEventTable.upstream_tx_id == upstream_tx_id
691
+ )
692
+
693
+ # Execute query
694
+ result = await session.scalar(stmt)
695
+
696
+ # Raise 404 if not found
697
+ if not result:
698
+ raise HTTPException(
699
+ status_code=404,
700
+ detail=f"Credit event with upstream_tx_id '{upstream_tx_id}' not found",
701
+ )
702
+
703
+ # Convert to Pydantic model and return
704
+ return CreditEvent.model_validate(result)
705
+
706
+
707
+ async def fetch_credit_event_by_id(
708
+ session: AsyncSession,
709
+ event_id: str,
710
+ ) -> CreditEvent:
711
+ """
712
+ Fetch a credit event by its ID.
713
+
714
+ Args:
715
+ session: Async database session.
716
+ event_id: ID of the credit event.
717
+
718
+ Returns:
719
+ The credit event if found.
720
+
721
+ Raises:
722
+ HTTPException: If the credit event is not found.
723
+ """
724
+ # Build the query to find the event by ID
725
+ stmt = select(CreditEventTable).where(CreditEventTable.id == event_id)
726
+
727
+ # Execute query
728
+ result = await session.scalar(stmt)
729
+
730
+ # Raise 404 if not found
731
+ if not result:
732
+ raise HTTPException(
733
+ status_code=404,
734
+ detail=f"Credit event with ID '{event_id}' not found",
735
+ )
736
+
737
+ # Convert to Pydantic model and return
738
+ return CreditEvent.model_validate(result)
739
+
740
+
741
+ async def expense_message(
742
+ session: AsyncSession,
743
+ user_id: str,
744
+ message_id: str,
745
+ start_message_id: str,
746
+ base_llm_amount: Decimal,
747
+ agent: Agent,
748
+ ) -> CreditEvent:
749
+ """
750
+ Deduct credits from a user account for message expenses.
751
+ Don't forget to commit the session after calling this function.
752
+
753
+ Args:
754
+ session: Async session to use for database operations
755
+ user_id: ID of the user to deduct credits from
756
+ message_id: ID of the message that incurred the expense
757
+ start_message_id: ID of the starting message in a conversation
758
+ base_llm_amount: Amount of LLM costs
759
+
760
+ Returns:
761
+ Updated user credit account
762
+ """
763
+ # Check for idempotency - prevent duplicate transactions
764
+ await CreditEvent.check_upstream_tx_id_exists(
765
+ session, UpstreamType.EXECUTOR, message_id
766
+ )
767
+
768
+ # Ensure base_llm_amount has 4 decimal places
769
+ base_llm_amount = base_llm_amount.quantize(FOURPLACES, rounding=ROUND_HALF_UP)
770
+
771
+ if base_llm_amount < Decimal("0"):
772
+ raise ValueError("Base LLM amount must be non-negative")
773
+
774
+ # Get payment settings
775
+ payment_settings = await AppSetting.payment()
776
+
777
+ # Calculate amount with exact 4 decimal places
778
+ base_original_amount = base_llm_amount
779
+ base_amount = base_original_amount
780
+ fee_platform_amount = (
781
+ base_amount * payment_settings.fee_platform_percentage / Decimal("100")
782
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
783
+ fee_agent_amount = Decimal("0")
784
+ if agent.fee_percentage and user_id != agent.owner:
785
+ fee_agent_amount = (
786
+ (base_amount + fee_platform_amount) * agent.fee_percentage / Decimal("100")
787
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
788
+ total_amount = (base_amount + fee_platform_amount + fee_agent_amount).quantize(
789
+ FOURPLACES, rounding=ROUND_HALF_UP
790
+ )
791
+
792
+ # 1. Create credit event record first to get event_id
793
+ event_id = str(XID())
794
+
795
+ # 2. Update user account - deduct credits
796
+ user_account, details = await CreditAccount.expense_in_session(
797
+ session=session,
798
+ owner_type=OwnerType.USER,
799
+ owner_id=user_id,
800
+ amount=total_amount,
801
+ event_id=event_id,
802
+ )
803
+
804
+ # If using free credits, add to agent's free_income_daily
805
+ if details.get(CreditType.FREE):
806
+ from intentkit.models.agent_data import AgentQuota
807
+
808
+ await AgentQuota.add_free_income_in_session(
809
+ session=session, id=agent.id, amount=details.get(CreditType.FREE)
810
+ )
811
+
812
+ # 3. Update fee account - add credits
813
+ message_account = await CreditAccount.income_in_session(
814
+ session=session,
815
+ owner_type=OwnerType.PLATFORM,
816
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_MESSAGE,
817
+ credit_type=CreditType.PERMANENT,
818
+ amount=base_amount,
819
+ event_id=event_id,
820
+ )
821
+ platform_fee_account = await CreditAccount.income_in_session(
822
+ session=session,
823
+ owner_type=OwnerType.PLATFORM,
824
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_FEE,
825
+ credit_type=CreditType.PERMANENT,
826
+ amount=fee_platform_amount,
827
+ event_id=event_id,
828
+ )
829
+ if fee_agent_amount > 0:
830
+ agent_account = await CreditAccount.income_in_session(
831
+ session=session,
832
+ owner_type=OwnerType.AGENT,
833
+ owner_id=agent.id,
834
+ credit_type=CreditType.REWARD,
835
+ amount=fee_agent_amount,
836
+ event_id=event_id,
837
+ )
838
+
839
+ # 4. Create credit event record
840
+ # Set the appropriate credit amount field based on credit type
841
+ free_amount = details.get(CreditType.FREE, Decimal("0"))
842
+ reward_amount = details.get(CreditType.REWARD, Decimal("0"))
843
+ permanent_amount = details.get(CreditType.PERMANENT, Decimal("0"))
844
+ if CreditType.PERMANENT in details:
845
+ credit_type = CreditType.PERMANENT
846
+ elif CreditType.REWARD in details:
847
+ credit_type = CreditType.REWARD
848
+ else:
849
+ credit_type = CreditType.FREE
850
+
851
+ # Calculate fee_platform amounts by credit type
852
+ fee_platform_free_amount = Decimal("0")
853
+ fee_platform_reward_amount = Decimal("0")
854
+ fee_platform_permanent_amount = Decimal("0")
855
+
856
+ if fee_platform_amount > Decimal("0") and total_amount > Decimal("0"):
857
+ # Calculate proportions based on the formula
858
+ if free_amount > Decimal("0"):
859
+ fee_platform_free_amount = (
860
+ free_amount * fee_platform_amount / total_amount
861
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
862
+
863
+ if reward_amount > Decimal("0"):
864
+ fee_platform_reward_amount = (
865
+ reward_amount * fee_platform_amount / total_amount
866
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
867
+
868
+ # Calculate permanent amount as the remainder to ensure the sum equals fee_platform_amount
869
+ fee_platform_permanent_amount = (
870
+ fee_platform_amount - fee_platform_free_amount - fee_platform_reward_amount
871
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
872
+
873
+ # Calculate fee_agent amounts by credit type
874
+ fee_agent_free_amount = Decimal("0")
875
+ fee_agent_reward_amount = Decimal("0")
876
+ fee_agent_permanent_amount = Decimal("0")
877
+
878
+ if fee_agent_amount > Decimal("0") and total_amount > Decimal("0"):
879
+ # Calculate proportions based on the formula
880
+ if free_amount > Decimal("0"):
881
+ fee_agent_free_amount = (
882
+ free_amount * fee_agent_amount / total_amount
883
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
884
+
885
+ if reward_amount > Decimal("0"):
886
+ fee_agent_reward_amount = (
887
+ reward_amount * fee_agent_amount / total_amount
888
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
889
+
890
+ # Calculate permanent amount as the remainder to ensure the sum equals fee_agent_amount
891
+ fee_agent_permanent_amount = (
892
+ fee_agent_amount - fee_agent_free_amount - fee_agent_reward_amount
893
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
894
+
895
+ event = CreditEventTable(
896
+ id=event_id,
897
+ account_id=user_account.id,
898
+ event_type=EventType.MESSAGE,
899
+ user_id=user_id,
900
+ upstream_type=UpstreamType.EXECUTOR,
901
+ upstream_tx_id=message_id,
902
+ direction=Direction.EXPENSE,
903
+ agent_id=agent.id,
904
+ message_id=message_id,
905
+ start_message_id=start_message_id,
906
+ model=agent.model,
907
+ total_amount=total_amount,
908
+ credit_type=credit_type,
909
+ credit_types=list(details.keys()),
910
+ balance_after=user_account.credits
911
+ + user_account.free_credits
912
+ + user_account.reward_credits,
913
+ base_amount=base_amount,
914
+ base_original_amount=base_original_amount,
915
+ base_llm_amount=base_llm_amount,
916
+ fee_platform_amount=fee_platform_amount,
917
+ fee_platform_free_amount=fee_platform_free_amount,
918
+ fee_platform_reward_amount=fee_platform_reward_amount,
919
+ fee_platform_permanent_amount=fee_platform_permanent_amount,
920
+ fee_agent_amount=fee_agent_amount,
921
+ fee_agent_account=agent_account.id if fee_agent_amount > 0 else None,
922
+ fee_agent_free_amount=fee_agent_free_amount,
923
+ fee_agent_reward_amount=fee_agent_reward_amount,
924
+ fee_agent_permanent_amount=fee_agent_permanent_amount,
925
+ free_amount=free_amount,
926
+ reward_amount=reward_amount,
927
+ permanent_amount=permanent_amount,
928
+ )
929
+ session.add(event)
930
+ await session.flush()
931
+
932
+ # 4. Create credit transaction records
933
+ # 4.1 User account transaction (debit)
934
+ user_tx = CreditTransactionTable(
935
+ id=str(XID()),
936
+ account_id=user_account.id,
937
+ event_id=event_id,
938
+ tx_type=TransactionType.PAY,
939
+ credit_debit=CreditDebit.DEBIT,
940
+ change_amount=total_amount,
941
+ credit_type=credit_type,
942
+ )
943
+ session.add(user_tx)
944
+
945
+ # 4.2 Message account transaction (credit)
946
+ message_tx = CreditTransactionTable(
947
+ id=str(XID()),
948
+ account_id=message_account.id,
949
+ event_id=event_id,
950
+ tx_type=TransactionType.RECEIVE_BASE_LLM,
951
+ credit_debit=CreditDebit.CREDIT,
952
+ change_amount=base_amount,
953
+ credit_type=credit_type,
954
+ )
955
+ session.add(message_tx)
956
+
957
+ # 4.3 Platform fee account transaction (credit)
958
+ platform_tx = CreditTransactionTable(
959
+ id=str(XID()),
960
+ account_id=platform_fee_account.id,
961
+ event_id=event_id,
962
+ tx_type=TransactionType.RECEIVE_FEE_PLATFORM,
963
+ credit_debit=CreditDebit.CREDIT,
964
+ change_amount=fee_platform_amount,
965
+ credit_type=credit_type,
966
+ )
967
+ session.add(platform_tx)
968
+
969
+ # 4.4 Agent fee account transaction (credit)
970
+ if fee_agent_amount > 0:
971
+ agent_tx = CreditTransactionTable(
972
+ id=str(XID()),
973
+ account_id=agent_account.id,
974
+ event_id=event_id,
975
+ tx_type=TransactionType.RECEIVE_FEE_AGENT,
976
+ credit_debit=CreditDebit.CREDIT,
977
+ change_amount=fee_agent_amount,
978
+ credit_type=credit_type,
979
+ )
980
+ session.add(agent_tx)
981
+
982
+ await session.refresh(event)
983
+
984
+ return CreditEvent.model_validate(event)
985
+
986
+
987
+ class SkillCost(BaseModel):
988
+ total_amount: Decimal
989
+ base_amount: Decimal
990
+ base_discount_amount: Decimal
991
+ base_original_amount: Decimal
992
+ base_skill_amount: Decimal
993
+ fee_platform_amount: Decimal
994
+ fee_dev_user: str
995
+ fee_dev_user_type: OwnerType
996
+ fee_dev_amount: Decimal
997
+ fee_agent_amount: Decimal
998
+
999
+
1000
+ async def skill_cost(
1001
+ skill_name: str,
1002
+ user_id: str,
1003
+ agent: Agent,
1004
+ ) -> SkillCost:
1005
+ """
1006
+ Calculate the cost for a skill call including all fees.
1007
+
1008
+ Args:
1009
+ skill_name: Name of the skill
1010
+ user_id: ID of the user making the skill call
1011
+ agent: Agent using the skill
1012
+
1013
+ Returns:
1014
+ SkillCost: Object containing all cost components
1015
+ """
1016
+
1017
+ skill = await Skill.get(skill_name)
1018
+ if not skill:
1019
+ raise ValueError(f"The price of {skill_name} not set yet")
1020
+ agent_skill_config = agent.skills.get(skill.category)
1021
+ if (
1022
+ agent_skill_config
1023
+ and agent_skill_config.get("api_key_provider") == "agent_owner"
1024
+ ):
1025
+ base_skill_amount = skill.price_self_key.quantize(
1026
+ FOURPLACES, rounding=ROUND_HALF_UP
1027
+ )
1028
+ else:
1029
+ base_skill_amount = skill.price.quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1030
+ # Get payment settings
1031
+ payment_settings = await AppSetting.payment()
1032
+
1033
+ # Calculate fee
1034
+ if skill.author:
1035
+ fee_dev_user = skill.author
1036
+ fee_dev_user_type = OwnerType.USER
1037
+ else:
1038
+ fee_dev_user = DEFAULT_PLATFORM_ACCOUNT_DEV
1039
+ fee_dev_user_type = OwnerType.PLATFORM
1040
+ fee_dev_percentage = payment_settings.fee_dev_percentage
1041
+
1042
+ if base_skill_amount < Decimal("0"):
1043
+ raise ValueError("Base skill amount must be non-negative")
1044
+
1045
+ # Calculate amount with exact 4 decimal places
1046
+ base_original_amount = base_skill_amount
1047
+ base_amount = base_original_amount
1048
+ fee_platform_amount = (
1049
+ base_amount * payment_settings.fee_platform_percentage / Decimal("100")
1050
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1051
+ fee_dev_amount = (base_amount * fee_dev_percentage / Decimal("100")).quantize(
1052
+ FOURPLACES, rounding=ROUND_HALF_UP
1053
+ )
1054
+ fee_agent_amount = Decimal("0")
1055
+ if agent.fee_percentage and user_id != agent.owner:
1056
+ fee_agent_amount = (
1057
+ (base_amount + fee_platform_amount + fee_dev_amount)
1058
+ * agent.fee_percentage
1059
+ / Decimal("100")
1060
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1061
+ total_amount = (
1062
+ base_amount + fee_platform_amount + fee_dev_amount + fee_agent_amount
1063
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1064
+
1065
+ # Return the SkillCost object with all calculated values
1066
+ return SkillCost(
1067
+ total_amount=total_amount,
1068
+ base_amount=base_amount,
1069
+ base_discount_amount=Decimal("0"), # No discount in this implementation
1070
+ base_original_amount=base_original_amount,
1071
+ base_skill_amount=base_skill_amount,
1072
+ fee_platform_amount=fee_platform_amount,
1073
+ fee_dev_user=fee_dev_user,
1074
+ fee_dev_user_type=fee_dev_user_type,
1075
+ fee_dev_amount=fee_dev_amount,
1076
+ fee_agent_amount=fee_agent_amount,
1077
+ )
1078
+
1079
+
1080
+ async def expense_skill(
1081
+ session: AsyncSession,
1082
+ user_id: str,
1083
+ message_id: str,
1084
+ start_message_id: str,
1085
+ skill_call_id: str,
1086
+ skill_name: str,
1087
+ agent: Agent,
1088
+ ) -> CreditEvent:
1089
+ """
1090
+ Deduct credits from a user account for message expenses.
1091
+ Don't forget to commit the session after calling this function.
1092
+
1093
+ Args:
1094
+ session: Async session to use for database operations
1095
+ user_id: ID of the user to deduct credits from
1096
+ message_id: ID of the message that incurred the expense
1097
+ start_message_id: ID of the starting message in a conversation
1098
+ skill_call_id: ID of the skill call
1099
+ skill_name: Name of the skill being used
1100
+ agent: Agent using the skill
1101
+
1102
+ Returns:
1103
+ CreditEvent: The created credit event
1104
+ """
1105
+ # Check for idempotency - prevent duplicate transactions
1106
+ upstream_tx_id = f"{message_id}_{skill_call_id}"
1107
+ await CreditEvent.check_upstream_tx_id_exists(
1108
+ session, UpstreamType.EXECUTOR, upstream_tx_id
1109
+ )
1110
+ logger.info(f"[{agent.id}] skill payment {skill_name}")
1111
+
1112
+ # Calculate skill cost using the skill_cost function
1113
+ skill_cost_info = await skill_cost(skill_name, user_id, agent)
1114
+
1115
+ # 1. Create credit event record first to get event_id
1116
+ event_id = str(XID())
1117
+
1118
+ # 2. Update user account - deduct credits
1119
+ user_account, details = await CreditAccount.expense_in_session(
1120
+ session=session,
1121
+ owner_type=OwnerType.USER,
1122
+ owner_id=user_id,
1123
+ amount=skill_cost_info.total_amount,
1124
+ event_id=event_id,
1125
+ )
1126
+
1127
+ # If using free credits, add to agent's free_income_daily
1128
+ if CreditType.FREE in details:
1129
+ from intentkit.models.agent_data import AgentQuota
1130
+
1131
+ await AgentQuota.add_free_income_in_session(
1132
+ session=session, id=agent.id, amount=details[CreditType.FREE]
1133
+ )
1134
+
1135
+ # 3. Update fee account - add credits
1136
+ skill_account = await CreditAccount.income_in_session(
1137
+ session=session,
1138
+ owner_type=OwnerType.PLATFORM,
1139
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_SKILL,
1140
+ credit_type=CreditType.PERMANENT,
1141
+ amount=skill_cost_info.base_amount,
1142
+ event_id=event_id,
1143
+ )
1144
+ platform_account = await CreditAccount.income_in_session(
1145
+ session=session,
1146
+ owner_type=OwnerType.PLATFORM,
1147
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_FEE,
1148
+ credit_type=CreditType.PERMANENT,
1149
+ amount=skill_cost_info.fee_platform_amount,
1150
+ event_id=event_id,
1151
+ )
1152
+ if skill_cost_info.fee_dev_amount > 0:
1153
+ dev_account = await CreditAccount.income_in_session(
1154
+ session=session,
1155
+ owner_type=skill_cost_info.fee_dev_user_type,
1156
+ owner_id=skill_cost_info.fee_dev_user,
1157
+ credit_type=CreditType.REWARD, # put dev fee in reward
1158
+ amount=skill_cost_info.fee_dev_amount,
1159
+ event_id=event_id,
1160
+ )
1161
+ if skill_cost_info.fee_agent_amount > 0:
1162
+ agent_account = await CreditAccount.income_in_session(
1163
+ session=session,
1164
+ owner_type=OwnerType.AGENT,
1165
+ owner_id=agent.id,
1166
+ credit_type=CreditType.REWARD,
1167
+ amount=skill_cost_info.fee_agent_amount,
1168
+ event_id=event_id,
1169
+ )
1170
+
1171
+ # 4. Create credit event record
1172
+ # Set the appropriate credit amount field based on credit type
1173
+ free_amount = details.get(CreditType.FREE, Decimal("0"))
1174
+ reward_amount = details.get(CreditType.REWARD, Decimal("0"))
1175
+ permanent_amount = details.get(CreditType.PERMANENT, Decimal("0"))
1176
+ if CreditType.PERMANENT in details:
1177
+ credit_type = CreditType.PERMANENT
1178
+ elif CreditType.REWARD in details:
1179
+ credit_type = CreditType.REWARD
1180
+ else:
1181
+ credit_type = CreditType.FREE
1182
+
1183
+ # Calculate fee_platform amounts by credit type
1184
+ fee_platform_free_amount = Decimal("0")
1185
+ fee_platform_reward_amount = Decimal("0")
1186
+ fee_platform_permanent_amount = Decimal("0")
1187
+
1188
+ if skill_cost_info.fee_platform_amount > Decimal(
1189
+ "0"
1190
+ ) and skill_cost_info.total_amount > Decimal("0"):
1191
+ # Calculate proportions based on the formula
1192
+ if free_amount > Decimal("0"):
1193
+ fee_platform_free_amount = (
1194
+ free_amount
1195
+ * skill_cost_info.fee_platform_amount
1196
+ / skill_cost_info.total_amount
1197
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1198
+
1199
+ if reward_amount > Decimal("0"):
1200
+ fee_platform_reward_amount = (
1201
+ reward_amount
1202
+ * skill_cost_info.fee_platform_amount
1203
+ / skill_cost_info.total_amount
1204
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1205
+
1206
+ # Calculate permanent amount as the remainder to ensure the sum equals fee_platform_amount
1207
+ fee_platform_permanent_amount = (
1208
+ skill_cost_info.fee_platform_amount
1209
+ - fee_platform_free_amount
1210
+ - fee_platform_reward_amount
1211
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1212
+
1213
+ # Calculate fee_agent amounts by credit type
1214
+ fee_agent_free_amount = Decimal("0")
1215
+ fee_agent_reward_amount = Decimal("0")
1216
+ fee_agent_permanent_amount = Decimal("0")
1217
+
1218
+ if skill_cost_info.fee_agent_amount > Decimal(
1219
+ "0"
1220
+ ) and skill_cost_info.total_amount > Decimal("0"):
1221
+ # Calculate proportions based on the formula
1222
+ if free_amount > Decimal("0"):
1223
+ fee_agent_free_amount = (
1224
+ free_amount
1225
+ * skill_cost_info.fee_agent_amount
1226
+ / skill_cost_info.total_amount
1227
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1228
+
1229
+ if reward_amount > Decimal("0"):
1230
+ fee_agent_reward_amount = (
1231
+ reward_amount
1232
+ * skill_cost_info.fee_agent_amount
1233
+ / skill_cost_info.total_amount
1234
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1235
+
1236
+ # Calculate permanent amount as the remainder to ensure the sum equals fee_agent_amount
1237
+ fee_agent_permanent_amount = (
1238
+ skill_cost_info.fee_agent_amount
1239
+ - fee_agent_free_amount
1240
+ - fee_agent_reward_amount
1241
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1242
+
1243
+ # Calculate fee_dev amounts by credit type
1244
+ fee_dev_free_amount = Decimal("0")
1245
+ fee_dev_reward_amount = Decimal("0")
1246
+ fee_dev_permanent_amount = Decimal("0")
1247
+
1248
+ if skill_cost_info.fee_dev_amount > Decimal(
1249
+ "0"
1250
+ ) and skill_cost_info.total_amount > Decimal("0"):
1251
+ # Calculate proportions based on the formula
1252
+ if free_amount > Decimal("0"):
1253
+ fee_dev_free_amount = (
1254
+ free_amount
1255
+ * skill_cost_info.fee_dev_amount
1256
+ / skill_cost_info.total_amount
1257
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1258
+
1259
+ if reward_amount > Decimal("0"):
1260
+ fee_dev_reward_amount = (
1261
+ reward_amount
1262
+ * skill_cost_info.fee_dev_amount
1263
+ / skill_cost_info.total_amount
1264
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1265
+
1266
+ # Calculate permanent amount as the remainder to ensure the sum equals fee_dev_amount
1267
+ fee_dev_permanent_amount = (
1268
+ skill_cost_info.fee_dev_amount - fee_dev_free_amount - fee_dev_reward_amount
1269
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1270
+
1271
+ event = CreditEventTable(
1272
+ id=event_id,
1273
+ account_id=user_account.id,
1274
+ event_type=EventType.SKILL_CALL,
1275
+ user_id=user_id,
1276
+ upstream_type=UpstreamType.EXECUTOR,
1277
+ upstream_tx_id=upstream_tx_id,
1278
+ direction=Direction.EXPENSE,
1279
+ agent_id=agent.id,
1280
+ message_id=message_id,
1281
+ start_message_id=start_message_id,
1282
+ skill_call_id=skill_call_id,
1283
+ skill_name=skill_name,
1284
+ total_amount=skill_cost_info.total_amount,
1285
+ credit_type=credit_type,
1286
+ credit_types=details.keys(),
1287
+ balance_after=user_account.credits
1288
+ + user_account.free_credits
1289
+ + user_account.reward_credits,
1290
+ base_amount=skill_cost_info.base_amount,
1291
+ base_original_amount=skill_cost_info.base_original_amount,
1292
+ base_skill_amount=skill_cost_info.base_skill_amount,
1293
+ fee_platform_amount=skill_cost_info.fee_platform_amount,
1294
+ fee_platform_free_amount=fee_platform_free_amount,
1295
+ fee_platform_reward_amount=fee_platform_reward_amount,
1296
+ fee_platform_permanent_amount=fee_platform_permanent_amount,
1297
+ fee_agent_amount=skill_cost_info.fee_agent_amount,
1298
+ fee_agent_account=agent_account.id
1299
+ if skill_cost_info.fee_agent_amount > 0
1300
+ else None,
1301
+ fee_agent_free_amount=fee_agent_free_amount,
1302
+ fee_agent_reward_amount=fee_agent_reward_amount,
1303
+ fee_agent_permanent_amount=fee_agent_permanent_amount,
1304
+ fee_dev_amount=skill_cost_info.fee_dev_amount,
1305
+ fee_dev_account=dev_account.id if skill_cost_info.fee_dev_amount > 0 else None,
1306
+ fee_dev_free_amount=fee_dev_free_amount,
1307
+ fee_dev_reward_amount=fee_dev_reward_amount,
1308
+ fee_dev_permanent_amount=fee_dev_permanent_amount,
1309
+ free_amount=free_amount,
1310
+ reward_amount=reward_amount,
1311
+ permanent_amount=permanent_amount,
1312
+ )
1313
+ session.add(event)
1314
+ await session.flush()
1315
+
1316
+ # 4. Create credit transaction records
1317
+ # 4.1 User account transaction (debit)
1318
+ user_tx = CreditTransactionTable(
1319
+ id=str(XID()),
1320
+ account_id=user_account.id,
1321
+ event_id=event_id,
1322
+ tx_type=TransactionType.PAY,
1323
+ credit_debit=CreditDebit.DEBIT,
1324
+ change_amount=skill_cost_info.total_amount,
1325
+ credit_type=credit_type,
1326
+ )
1327
+ session.add(user_tx)
1328
+
1329
+ # 4.2 Skill account transaction (credit)
1330
+ skill_tx = CreditTransactionTable(
1331
+ id=str(XID()),
1332
+ account_id=skill_account.id,
1333
+ event_id=event_id,
1334
+ tx_type=TransactionType.RECEIVE_BASE_SKILL,
1335
+ credit_debit=CreditDebit.CREDIT,
1336
+ change_amount=skill_cost_info.base_amount,
1337
+ credit_type=credit_type,
1338
+ )
1339
+ session.add(skill_tx)
1340
+
1341
+ # 4.3 Platform fee account transaction (credit)
1342
+ platform_tx = CreditTransactionTable(
1343
+ id=str(XID()),
1344
+ account_id=platform_account.id,
1345
+ event_id=event_id,
1346
+ tx_type=TransactionType.RECEIVE_FEE_PLATFORM,
1347
+ credit_debit=CreditDebit.CREDIT,
1348
+ change_amount=skill_cost_info.fee_platform_amount,
1349
+ credit_type=credit_type,
1350
+ )
1351
+ session.add(platform_tx)
1352
+
1353
+ # 4.4 Dev user transaction (credit)
1354
+ if skill_cost_info.fee_dev_amount > 0:
1355
+ dev_tx = CreditTransactionTable(
1356
+ id=str(XID()),
1357
+ account_id=dev_account.id,
1358
+ event_id=event_id,
1359
+ tx_type=TransactionType.RECEIVE_FEE_DEV,
1360
+ credit_debit=CreditDebit.CREDIT,
1361
+ change_amount=skill_cost_info.fee_dev_amount,
1362
+ credit_type=CreditType.REWARD,
1363
+ )
1364
+ session.add(dev_tx)
1365
+
1366
+ # 4.5 Agent fee account transaction (credit)
1367
+ if skill_cost_info.fee_agent_amount > 0:
1368
+ agent_tx = CreditTransactionTable(
1369
+ id=str(XID()),
1370
+ account_id=agent_account.id,
1371
+ event_id=event_id,
1372
+ tx_type=TransactionType.RECEIVE_FEE_AGENT,
1373
+ credit_debit=CreditDebit.CREDIT,
1374
+ change_amount=skill_cost_info.fee_agent_amount,
1375
+ credit_type=credit_type,
1376
+ )
1377
+ session.add(agent_tx)
1378
+
1379
+ # Commit all changes
1380
+ await session.refresh(event)
1381
+
1382
+ return CreditEvent.model_validate(event)
1383
+
1384
+
1385
+ async def refill_free_credits_for_account(
1386
+ session: AsyncSession,
1387
+ account: CreditAccount,
1388
+ ):
1389
+ """
1390
+ Refill free credits for a single account based on its refill_amount and free_quota.
1391
+
1392
+ Args:
1393
+ session: Async session to use for database operations
1394
+ account: The credit account to refill
1395
+ """
1396
+ # Skip if refill_amount is zero or free_credits already equals or exceeds free_quota
1397
+ if (
1398
+ account.refill_amount <= Decimal("0")
1399
+ or account.free_credits >= account.free_quota
1400
+ ):
1401
+ return
1402
+
1403
+ # Calculate the amount to add
1404
+ # If adding refill_amount would exceed free_quota, only add what's needed to reach free_quota
1405
+ amount_to_add = min(
1406
+ account.refill_amount, account.free_quota - account.free_credits
1407
+ )
1408
+
1409
+ if amount_to_add <= Decimal("0"):
1410
+ return # Nothing to add
1411
+
1412
+ # 1. Create credit event record first to get event_id
1413
+ event_id = str(XID())
1414
+
1415
+ # 2. Update user account - add free credits
1416
+ updated_account = await CreditAccount.income_in_session(
1417
+ session=session,
1418
+ owner_type=account.owner_type,
1419
+ owner_id=account.owner_id,
1420
+ amount=amount_to_add,
1421
+ credit_type=CreditType.FREE,
1422
+ event_id=event_id,
1423
+ )
1424
+
1425
+ # 3. Update platform refill account - deduct credits
1426
+ platform_account = await CreditAccount.deduction_in_session(
1427
+ session=session,
1428
+ owner_type=OwnerType.PLATFORM,
1429
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_REFILL,
1430
+ credit_type=CreditType.FREE,
1431
+ amount=amount_to_add,
1432
+ event_id=event_id,
1433
+ )
1434
+
1435
+ # 4. Create credit event record
1436
+ event = CreditEventTable(
1437
+ id=event_id,
1438
+ account_id=updated_account.id,
1439
+ event_type=EventType.REFILL,
1440
+ user_id=account.owner_id,
1441
+ upstream_type=UpstreamType.SCHEDULER,
1442
+ upstream_tx_id=str(XID()),
1443
+ direction=Direction.INCOME,
1444
+ credit_type=CreditType.FREE,
1445
+ credit_types=[CreditType.FREE],
1446
+ total_amount=amount_to_add,
1447
+ balance_after=updated_account.credits
1448
+ + updated_account.free_credits
1449
+ + updated_account.reward_credits,
1450
+ base_amount=amount_to_add,
1451
+ base_original_amount=amount_to_add,
1452
+ free_amount=amount_to_add, # Set free_amount since this is a free credit refill
1453
+ reward_amount=Decimal("0"), # No reward credits involved
1454
+ permanent_amount=Decimal("0"), # No permanent credits involved
1455
+ note=f"Hourly free credits refill of {amount_to_add}",
1456
+ )
1457
+ session.add(event)
1458
+ await session.flush()
1459
+
1460
+ # 4. Create credit transaction records
1461
+ # 4.1 User account transaction (credit)
1462
+ user_tx = CreditTransactionTable(
1463
+ id=str(XID()),
1464
+ account_id=updated_account.id,
1465
+ event_id=event_id,
1466
+ tx_type=TransactionType.REFILL,
1467
+ credit_debit=CreditDebit.CREDIT,
1468
+ change_amount=amount_to_add,
1469
+ credit_type=CreditType.FREE,
1470
+ )
1471
+ session.add(user_tx)
1472
+
1473
+ # 4.2 Platform refill account transaction (debit)
1474
+ platform_tx = CreditTransactionTable(
1475
+ id=str(XID()),
1476
+ account_id=platform_account.id,
1477
+ event_id=event_id,
1478
+ tx_type=TransactionType.REFILL,
1479
+ credit_debit=CreditDebit.DEBIT,
1480
+ change_amount=amount_to_add,
1481
+ credit_type=CreditType.FREE,
1482
+ )
1483
+ session.add(platform_tx)
1484
+
1485
+ # Commit changes
1486
+ await session.commit()
1487
+ logger.info(
1488
+ f"Refilled {amount_to_add} free credits for account {account.owner_type} {account.owner_id}"
1489
+ )
1490
+
1491
+
1492
+ async def refill_all_free_credits():
1493
+ """
1494
+ Find all eligible accounts and refill their free credits.
1495
+ Eligible accounts are those with refill_amount > 0 and free_credits < free_quota.
1496
+ """
1497
+ async with get_session() as session:
1498
+ # Find all accounts that need refilling
1499
+ stmt = select(CreditAccountTable).where(
1500
+ CreditAccountTable.refill_amount > 0,
1501
+ CreditAccountTable.free_credits < CreditAccountTable.free_quota,
1502
+ )
1503
+ result = await session.execute(stmt)
1504
+ accounts_data = result.scalars().all()
1505
+
1506
+ # Convert to Pydantic models
1507
+ accounts = [CreditAccount.model_validate(account) for account in accounts_data]
1508
+
1509
+ # Process each account
1510
+ refilled_count = 0
1511
+ for account in accounts:
1512
+ async with get_session() as session:
1513
+ try:
1514
+ await refill_free_credits_for_account(session, account)
1515
+ refilled_count += 1
1516
+ except Exception as e:
1517
+ logger.error(f"Error refilling account {account.id}: {str(e)}")
1518
+ # Continue with other accounts even if one fails
1519
+ continue
1520
+ logger.info(f"Refilled {refilled_count} accounts")
1521
+
1522
+
1523
+ async def expense_summarize(
1524
+ session: AsyncSession,
1525
+ user_id: str,
1526
+ message_id: str,
1527
+ start_message_id: str,
1528
+ base_llm_amount: Decimal,
1529
+ agent: Agent,
1530
+ ) -> CreditEvent:
1531
+ """
1532
+ Deduct credits from a user account for memory/summarize expenses.
1533
+ Don't forget to commit the session after calling this function.
1534
+
1535
+ Args:
1536
+ session: Async session to use for database operations
1537
+ user_id: ID of the user to deduct credits from
1538
+ message_id: ID of the message that incurred the expense
1539
+ start_message_id: ID of the starting message in a conversation
1540
+ base_llm_amount: Amount of LLM costs
1541
+ agent: Agent instance
1542
+
1543
+ Returns:
1544
+ Updated user credit account
1545
+ """
1546
+ # Check for idempotency - prevent duplicate transactions
1547
+ await CreditEvent.check_upstream_tx_id_exists(
1548
+ session, UpstreamType.EXECUTOR, message_id
1549
+ )
1550
+
1551
+ # Ensure base_llm_amount has 4 decimal places
1552
+ base_llm_amount = base_llm_amount.quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1553
+
1554
+ if base_llm_amount < Decimal("0"):
1555
+ raise ValueError("Base LLM amount must be non-negative")
1556
+
1557
+ # Get payment settings
1558
+ payment_settings = await AppSetting.payment()
1559
+
1560
+ # Calculate amount with exact 4 decimal places
1561
+ base_original_amount = base_llm_amount
1562
+ base_amount = base_original_amount
1563
+ fee_platform_amount = (
1564
+ base_amount * payment_settings.fee_platform_percentage / Decimal("100")
1565
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1566
+ fee_agent_amount = Decimal("0")
1567
+ if agent.fee_percentage and user_id != agent.owner:
1568
+ fee_agent_amount = (
1569
+ (base_amount + fee_platform_amount) * agent.fee_percentage / Decimal("100")
1570
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1571
+ total_amount = (base_amount + fee_platform_amount + fee_agent_amount).quantize(
1572
+ FOURPLACES, rounding=ROUND_HALF_UP
1573
+ )
1574
+
1575
+ # 1. Create credit event record first to get event_id
1576
+ event_id = str(XID())
1577
+
1578
+ # 2. Update user account - deduct credits
1579
+ user_account, details = await CreditAccount.expense_in_session(
1580
+ session=session,
1581
+ owner_type=OwnerType.USER,
1582
+ owner_id=user_id,
1583
+ amount=total_amount,
1584
+ event_id=event_id,
1585
+ )
1586
+
1587
+ # If using free credits, add to agent's free_income_daily
1588
+ if details.get(CreditType.FREE):
1589
+ from intentkit.models.agent_data import AgentQuota
1590
+
1591
+ await AgentQuota.add_free_income_in_session(
1592
+ session=session, id=agent.id, amount=details.get(CreditType.FREE)
1593
+ )
1594
+
1595
+ # 3. Update fee account - add credits
1596
+ memory_account = await CreditAccount.income_in_session(
1597
+ session=session,
1598
+ owner_type=OwnerType.PLATFORM,
1599
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_MEMORY,
1600
+ credit_type=CreditType.PERMANENT,
1601
+ amount=base_amount,
1602
+ event_id=event_id,
1603
+ )
1604
+ platform_fee_account = await CreditAccount.income_in_session(
1605
+ session=session,
1606
+ owner_type=OwnerType.PLATFORM,
1607
+ owner_id=DEFAULT_PLATFORM_ACCOUNT_FEE,
1608
+ credit_type=CreditType.PERMANENT,
1609
+ amount=fee_platform_amount,
1610
+ event_id=event_id,
1611
+ )
1612
+ if fee_agent_amount > 0:
1613
+ agent_account = await CreditAccount.income_in_session(
1614
+ session=session,
1615
+ owner_type=OwnerType.AGENT,
1616
+ owner_id=agent.id,
1617
+ credit_type=CreditType.REWARD,
1618
+ amount=fee_agent_amount,
1619
+ event_id=event_id,
1620
+ )
1621
+
1622
+ # 4. Create credit event record
1623
+ # Set the appropriate credit amount field based on credit type
1624
+ free_amount = details.get(CreditType.FREE, Decimal("0"))
1625
+ reward_amount = details.get(CreditType.REWARD, Decimal("0"))
1626
+ permanent_amount = details.get(CreditType.PERMANENT, Decimal("0"))
1627
+ if CreditType.PERMANENT in details:
1628
+ credit_type = CreditType.PERMANENT
1629
+ elif CreditType.REWARD in details:
1630
+ credit_type = CreditType.REWARD
1631
+ else:
1632
+ credit_type = CreditType.FREE
1633
+
1634
+ # Calculate fee_platform amounts by credit type
1635
+ fee_platform_free_amount = Decimal("0")
1636
+ fee_platform_reward_amount = Decimal("0")
1637
+ fee_platform_permanent_amount = Decimal("0")
1638
+
1639
+ if fee_platform_amount > Decimal("0") and total_amount > Decimal("0"):
1640
+ # Calculate proportions based on the formula
1641
+ if free_amount > Decimal("0"):
1642
+ fee_platform_free_amount = (
1643
+ free_amount * fee_platform_amount / total_amount
1644
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1645
+
1646
+ if reward_amount > Decimal("0"):
1647
+ fee_platform_reward_amount = (
1648
+ reward_amount * fee_platform_amount / total_amount
1649
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1650
+
1651
+ # Calculate permanent amount as the remainder to ensure the sum equals fee_platform_amount
1652
+ fee_platform_permanent_amount = (
1653
+ fee_platform_amount - fee_platform_free_amount - fee_platform_reward_amount
1654
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1655
+
1656
+ # Calculate fee_agent amounts by credit type
1657
+ fee_agent_free_amount = Decimal("0")
1658
+ fee_agent_reward_amount = Decimal("0")
1659
+ fee_agent_permanent_amount = Decimal("0")
1660
+
1661
+ if fee_agent_amount > Decimal("0") and total_amount > Decimal("0"):
1662
+ # Calculate proportions based on the formula
1663
+ if free_amount > Decimal("0"):
1664
+ fee_agent_free_amount = (
1665
+ free_amount * fee_agent_amount / total_amount
1666
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1667
+
1668
+ if reward_amount > Decimal("0"):
1669
+ fee_agent_reward_amount = (
1670
+ reward_amount * fee_agent_amount / total_amount
1671
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1672
+
1673
+ # Calculate permanent amount as the remainder to ensure the sum equals fee_agent_amount
1674
+ fee_agent_permanent_amount = (
1675
+ fee_agent_amount - fee_agent_free_amount - fee_agent_reward_amount
1676
+ ).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
1677
+
1678
+ event = CreditEventTable(
1679
+ id=event_id,
1680
+ account_id=user_account.id,
1681
+ event_type=EventType.MEMORY,
1682
+ user_id=user_id,
1683
+ upstream_type=UpstreamType.EXECUTOR,
1684
+ upstream_tx_id=message_id,
1685
+ direction=Direction.EXPENSE,
1686
+ agent_id=agent.id,
1687
+ message_id=message_id,
1688
+ start_message_id=start_message_id,
1689
+ model=agent.model,
1690
+ total_amount=total_amount,
1691
+ credit_type=credit_type,
1692
+ credit_types=details.keys(),
1693
+ balance_after=user_account.credits
1694
+ + user_account.free_credits
1695
+ + user_account.reward_credits,
1696
+ base_amount=base_amount,
1697
+ base_original_amount=base_original_amount,
1698
+ base_llm_amount=base_llm_amount,
1699
+ fee_platform_amount=fee_platform_amount,
1700
+ fee_platform_free_amount=fee_platform_free_amount,
1701
+ fee_platform_reward_amount=fee_platform_reward_amount,
1702
+ fee_platform_permanent_amount=fee_platform_permanent_amount,
1703
+ fee_agent_amount=fee_agent_amount,
1704
+ fee_agent_free_amount=fee_agent_free_amount,
1705
+ fee_agent_reward_amount=fee_agent_reward_amount,
1706
+ fee_agent_permanent_amount=fee_agent_permanent_amount,
1707
+ free_amount=free_amount,
1708
+ reward_amount=reward_amount,
1709
+ permanent_amount=permanent_amount,
1710
+ )
1711
+ session.add(event)
1712
+
1713
+ # 4. Create credit transaction records
1714
+ # 4.1 User account transaction (debit)
1715
+ user_tx = CreditTransactionTable(
1716
+ id=str(XID()),
1717
+ account_id=user_account.id,
1718
+ event_id=event_id,
1719
+ tx_type=TransactionType.PAY,
1720
+ credit_debit=CreditDebit.DEBIT,
1721
+ change_amount=total_amount,
1722
+ credit_type=credit_type,
1723
+ )
1724
+ session.add(user_tx)
1725
+
1726
+ # 4.2 Memory account transaction (credit)
1727
+ memory_tx = CreditTransactionTable(
1728
+ id=str(XID()),
1729
+ account_id=memory_account.id,
1730
+ event_id=event_id,
1731
+ tx_type=TransactionType.RECEIVE_BASE_MEMORY,
1732
+ credit_debit=CreditDebit.CREDIT,
1733
+ change_amount=base_amount,
1734
+ credit_type=credit_type,
1735
+ )
1736
+ session.add(memory_tx)
1737
+
1738
+ # 4.3 Platform fee account transaction (credit)
1739
+ platform_tx = CreditTransactionTable(
1740
+ id=str(XID()),
1741
+ account_id=platform_fee_account.id,
1742
+ event_id=event_id,
1743
+ tx_type=TransactionType.RECEIVE_FEE_PLATFORM,
1744
+ credit_debit=CreditDebit.CREDIT,
1745
+ change_amount=fee_platform_amount,
1746
+ credit_type=credit_type,
1747
+ )
1748
+ session.add(platform_tx)
1749
+
1750
+ # 4.4 Agent fee account transaction (credit) - only if there's an agent fee
1751
+ if fee_agent_amount > 0:
1752
+ agent_tx = CreditTransactionTable(
1753
+ id=str(XID()),
1754
+ account_id=agent_account.id,
1755
+ event_id=event_id,
1756
+ tx_type=TransactionType.RECEIVE_FEE_AGENT,
1757
+ credit_debit=CreditDebit.CREDIT,
1758
+ change_amount=fee_agent_amount,
1759
+ credit_type=CreditType.REWARD,
1760
+ )
1761
+ session.add(agent_tx)
1762
+
1763
+ # 5. Refresh session to get updated data
1764
+ await session.refresh(user_account)
1765
+
1766
+ # 6. Return credit event model
1767
+ return CreditEvent.model_validate(event)