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,1406 @@
1
+ import logging
2
+ from datetime import datetime, timezone
3
+ from decimal import ROUND_HALF_UP, Decimal
4
+ from enum import Enum
5
+ from typing import Annotated, Any, Dict, List, Optional, Tuple
6
+
7
+ from epyxid import XID
8
+ from fastapi import HTTPException
9
+ from intentkit.models.base import Base
10
+ from intentkit.models.db import get_session
11
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
12
+ from sqlalchemy import (
13
+ ARRAY,
14
+ JSON,
15
+ Column,
16
+ DateTime,
17
+ Index,
18
+ Numeric,
19
+ String,
20
+ func,
21
+ select,
22
+ update,
23
+ )
24
+ from sqlalchemy.ext.asyncio import AsyncSession
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class CreditType(str, Enum):
30
+ """Credit type is used in db column names, do not change it."""
31
+
32
+ FREE = "free_credits"
33
+ REWARD = "reward_credits"
34
+ PERMANENT = "credits"
35
+
36
+
37
+ class OwnerType(str, Enum):
38
+ """Type of credit account owner."""
39
+
40
+ USER = "user"
41
+ AGENT = "agent"
42
+ PLATFORM = "platform"
43
+
44
+
45
+ # Platform virtual account ids/owner ids, they are used for transaction balance tracing
46
+ # The owner id and account id are the same
47
+ DEFAULT_PLATFORM_ACCOUNT_RECHARGE = "platform_recharge"
48
+ DEFAULT_PLATFORM_ACCOUNT_REFILL = "platform_refill"
49
+ DEFAULT_PLATFORM_ACCOUNT_ADJUSTMENT = "platform_adjustment"
50
+ DEFAULT_PLATFORM_ACCOUNT_REWARD = "platform_reward"
51
+ DEFAULT_PLATFORM_ACCOUNT_REFUND = "platform_refund"
52
+ DEFAULT_PLATFORM_ACCOUNT_MESSAGE = "platform_message"
53
+ DEFAULT_PLATFORM_ACCOUNT_SKILL = "platform_skill"
54
+ DEFAULT_PLATFORM_ACCOUNT_MEMORY = "platform_memory"
55
+ DEFAULT_PLATFORM_ACCOUNT_VOICE = "platform_voice"
56
+ DEFAULT_PLATFORM_ACCOUNT_KNOWLEDGE = "platform_knowledge"
57
+ DEFAULT_PLATFORM_ACCOUNT_FEE = "platform_fee"
58
+ DEFAULT_PLATFORM_ACCOUNT_DEV = "platform_dev"
59
+
60
+
61
+ class CreditAccountTable(Base):
62
+ """Credit account database table model."""
63
+
64
+ __tablename__ = "credit_accounts"
65
+ __table_args__ = (Index("ix_credit_accounts_owner", "owner_type", "owner_id"),)
66
+
67
+ id = Column(
68
+ String,
69
+ primary_key=True,
70
+ )
71
+ owner_type = Column(
72
+ String,
73
+ nullable=False,
74
+ )
75
+ owner_id = Column(
76
+ String,
77
+ nullable=False,
78
+ )
79
+ free_quota = Column(
80
+ Numeric(22, 4),
81
+ default=0,
82
+ nullable=False,
83
+ )
84
+ refill_amount = Column(
85
+ Numeric(22, 4),
86
+ default=0,
87
+ nullable=False,
88
+ )
89
+ free_credits = Column(
90
+ Numeric(22, 4),
91
+ default=0,
92
+ nullable=False,
93
+ )
94
+ reward_credits = Column(
95
+ Numeric(22, 4),
96
+ default=0,
97
+ nullable=False,
98
+ )
99
+ credits = Column(
100
+ Numeric(22, 4),
101
+ default=0,
102
+ nullable=False,
103
+ )
104
+ income_at = Column(
105
+ DateTime(timezone=True),
106
+ nullable=True,
107
+ )
108
+ expense_at = Column(
109
+ DateTime(timezone=True),
110
+ nullable=True,
111
+ )
112
+ last_event_id = Column(
113
+ String,
114
+ nullable=True,
115
+ )
116
+ created_at = Column(
117
+ DateTime(timezone=True),
118
+ nullable=False,
119
+ server_default=func.now(),
120
+ )
121
+ updated_at = Column(
122
+ DateTime(timezone=True),
123
+ nullable=False,
124
+ server_default=func.now(),
125
+ onupdate=lambda: datetime.now(timezone.utc),
126
+ )
127
+
128
+
129
+ class CreditAccount(BaseModel):
130
+ """Credit account model with all fields."""
131
+
132
+ model_config = ConfigDict(
133
+ use_enum_values=True,
134
+ from_attributes=True,
135
+ json_encoders={
136
+ datetime: lambda v: v.isoformat(timespec="milliseconds"),
137
+ },
138
+ )
139
+
140
+ id: Annotated[
141
+ str,
142
+ Field(
143
+ default_factory=lambda: str(XID()),
144
+ description="Unique identifier for the credit account",
145
+ ),
146
+ ]
147
+ owner_type: Annotated[OwnerType, Field(description="Type of the account owner")]
148
+ owner_id: Annotated[str, Field(description="ID of the account owner")]
149
+ free_quota: Annotated[
150
+ Decimal,
151
+ Field(
152
+ default=Decimal("0"), description="Daily credit quota that resets each day"
153
+ ),
154
+ ]
155
+ refill_amount: Annotated[
156
+ Decimal,
157
+ Field(
158
+ default=Decimal("0"),
159
+ description="Amount to refill hourly, not exceeding free_quota",
160
+ ),
161
+ ]
162
+ free_credits: Annotated[
163
+ Decimal,
164
+ Field(default=Decimal("0"), description="Current available daily credits"),
165
+ ]
166
+ reward_credits: Annotated[
167
+ Decimal,
168
+ Field(
169
+ default=Decimal("0"), description="Reward credits earned through rewards"
170
+ ),
171
+ ]
172
+ credits: Annotated[
173
+ Decimal,
174
+ Field(default=Decimal("0"), description="Credits added through top-ups"),
175
+ ]
176
+ income_at: Annotated[
177
+ Optional[datetime],
178
+ Field(None, description="Timestamp of the last income transaction"),
179
+ ]
180
+ expense_at: Annotated[
181
+ Optional[datetime],
182
+ Field(None, description="Timestamp of the last expense transaction"),
183
+ ]
184
+ last_event_id: Annotated[
185
+ Optional[str],
186
+ Field(None, description="ID of the last event that modified this account"),
187
+ ]
188
+ created_at: Annotated[
189
+ datetime, Field(description="Timestamp when this account was created")
190
+ ]
191
+ updated_at: Annotated[
192
+ datetime, Field(description="Timestamp when this account was last updated")
193
+ ]
194
+
195
+ @field_validator(
196
+ "free_quota", "refill_amount", "free_credits", "reward_credits", "credits"
197
+ )
198
+ @classmethod
199
+ def round_decimal(cls, v: Any) -> Decimal:
200
+ """Round decimal values to 4 decimal places."""
201
+ if isinstance(v, Decimal):
202
+ return v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
203
+ elif isinstance(v, (int, float)):
204
+ return Decimal(str(v)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
205
+ return v
206
+
207
+ @property
208
+ def balance(self) -> Decimal:
209
+ """Return the total balance of the account."""
210
+ return self.free_credits + self.reward_credits + self.credits
211
+
212
+ @classmethod
213
+ async def get_in_session(
214
+ cls,
215
+ session: AsyncSession,
216
+ owner_type: OwnerType,
217
+ owner_id: str,
218
+ ) -> "CreditAccount":
219
+ """Get a credit account by owner type and ID.
220
+
221
+ Args:
222
+ session: Async session to use for database queries
223
+ owner_type: Type of the owner
224
+ owner_id: ID of the owner
225
+
226
+ Returns:
227
+ CreditAccount if found, None otherwise
228
+ """
229
+ stmt = select(CreditAccountTable).where(
230
+ CreditAccountTable.owner_type == owner_type,
231
+ CreditAccountTable.owner_id == owner_id,
232
+ )
233
+ result = await session.scalar(stmt)
234
+ if not result:
235
+ raise HTTPException(status_code=404, detail="Credit account not found")
236
+ return cls.model_validate(result)
237
+
238
+ @classmethod
239
+ async def get_or_create_in_session(
240
+ cls,
241
+ session: AsyncSession,
242
+ owner_type: OwnerType,
243
+ owner_id: str,
244
+ for_update: bool = False,
245
+ ) -> "CreditAccount":
246
+ """Get a credit account by owner type and ID.
247
+
248
+ Args:
249
+ session: Async session to use for database queries
250
+ owner_type: Type of the owner
251
+ owner_id: ID of the owner
252
+
253
+ Returns:
254
+ CreditAccount if found, None otherwise
255
+ """
256
+ stmt = select(CreditAccountTable).where(
257
+ CreditAccountTable.owner_type == owner_type,
258
+ CreditAccountTable.owner_id == owner_id,
259
+ )
260
+ if for_update:
261
+ stmt = stmt.with_for_update()
262
+ result = await session.scalar(stmt)
263
+ if not result:
264
+ account = await cls.create_in_session(session, owner_type, owner_id)
265
+ else:
266
+ account = cls.model_validate(result)
267
+
268
+ return account
269
+
270
+ @classmethod
271
+ async def get_or_create(
272
+ cls, owner_type: OwnerType, owner_id: str
273
+ ) -> "CreditAccount":
274
+ """Get a credit account by owner type and ID.
275
+
276
+ Args:
277
+ owner_type: Type of the owner
278
+ owner_id: ID of the owner
279
+
280
+ Returns:
281
+ CreditAccount if found, None otherwise
282
+ """
283
+ async with get_session() as session:
284
+ account = await cls.get_or_create_in_session(session, owner_type, owner_id)
285
+ await session.commit()
286
+ return account
287
+
288
+ @classmethod
289
+ async def deduction_in_session(
290
+ cls,
291
+ session: AsyncSession,
292
+ owner_type: OwnerType,
293
+ owner_id: str,
294
+ credit_type: CreditType,
295
+ amount: Decimal,
296
+ event_id: Optional[str] = None,
297
+ ) -> "CreditAccount":
298
+ """Deduct credits from an account. Not checking balance"""
299
+ # check first, create if not exists
300
+ await cls.get_or_create_in_session(session, owner_type, owner_id)
301
+
302
+ values_dict = {
303
+ credit_type.value: getattr(CreditAccountTable, credit_type.value) - amount,
304
+ "expense_at": datetime.now(timezone.utc),
305
+ }
306
+ if event_id:
307
+ values_dict["last_event_id"] = event_id
308
+
309
+ stmt = (
310
+ update(CreditAccountTable)
311
+ .where(
312
+ CreditAccountTable.owner_type == owner_type,
313
+ CreditAccountTable.owner_id == owner_id,
314
+ )
315
+ .values(values_dict)
316
+ .returning(CreditAccountTable)
317
+ )
318
+ res = await session.scalar(stmt)
319
+ if not res:
320
+ raise HTTPException(status_code=500, detail="Failed to expense credits")
321
+ return cls.model_validate(res)
322
+
323
+ @classmethod
324
+ async def expense_in_session(
325
+ cls,
326
+ session: AsyncSession,
327
+ owner_type: OwnerType,
328
+ owner_id: str,
329
+ amount: Decimal,
330
+ event_id: Optional[str] = None,
331
+ ) -> Tuple["CreditAccount", Dict[CreditType, Decimal]]:
332
+ """Expense credits and return account and credit type.
333
+ We are not checking balance here, since a conversation may have
334
+ multiple expenses, we can't interrupt the conversation.
335
+ """
336
+ # check first
337
+ account = await cls.get_or_create_in_session(session, owner_type, owner_id)
338
+
339
+ # expense
340
+ details = {}
341
+
342
+ amount_left = amount
343
+
344
+ if amount_left <= account.free_credits:
345
+ details[CreditType.FREE] = amount_left
346
+ amount_left = Decimal("0")
347
+ else:
348
+ if account.free_credits > 0:
349
+ details[CreditType.FREE] = account.free_credits
350
+ amount_left -= account.free_credits
351
+ if amount_left <= account.reward_credits:
352
+ details[CreditType.REWARD] = amount_left
353
+ amount_left = Decimal("0")
354
+ else:
355
+ if account.reward_credits > 0:
356
+ details[CreditType.REWARD] = account.reward_credits
357
+ amount_left -= account.reward_credits
358
+ details[CreditType.PERMANENT] = amount_left
359
+
360
+ # Create values dict based on what's in details, defaulting to 0 for missing keys
361
+ values_dict = {
362
+ "expense_at": datetime.now(timezone.utc),
363
+ }
364
+ if event_id:
365
+ values_dict["last_event_id"] = event_id
366
+
367
+ # Add credit type values only if they exist in details
368
+ for credit_type in [CreditType.FREE, CreditType.REWARD, CreditType.PERMANENT]:
369
+ if credit_type in details:
370
+ values_dict[credit_type.value] = (
371
+ getattr(CreditAccountTable, credit_type.value)
372
+ - details[credit_type]
373
+ )
374
+
375
+ stmt = (
376
+ update(CreditAccountTable)
377
+ .where(
378
+ CreditAccountTable.owner_type == owner_type,
379
+ CreditAccountTable.owner_id == owner_id,
380
+ )
381
+ .values(values_dict)
382
+ .returning(CreditAccountTable)
383
+ )
384
+ res = await session.scalar(stmt)
385
+ if not res:
386
+ raise HTTPException(status_code=500, detail="Failed to expense credits")
387
+ return cls.model_validate(res), details
388
+
389
+ def has_sufficient_credits(self, amount: Decimal) -> bool:
390
+ """Check if the account has enough credits to cover the specified amount.
391
+
392
+ Args:
393
+ amount: The amount of credits to check against
394
+
395
+ Returns:
396
+ bool: True if there are enough credits, False otherwise
397
+ """
398
+ return amount <= self.free_credits + self.reward_credits + self.credits
399
+
400
+ @classmethod
401
+ async def income_in_session(
402
+ cls,
403
+ session: AsyncSession,
404
+ owner_type: OwnerType,
405
+ owner_id: str,
406
+ amount: Decimal,
407
+ credit_type: CreditType,
408
+ event_id: Optional[str] = None,
409
+ ) -> "CreditAccount":
410
+ # check first, create if not exists
411
+ await cls.get_or_create_in_session(session, owner_type, owner_id)
412
+ # income
413
+ values_dict = {
414
+ credit_type.value: getattr(CreditAccountTable, credit_type.value) + amount,
415
+ "income_at": datetime.now(timezone.utc),
416
+ }
417
+ if event_id:
418
+ values_dict["last_event_id"] = event_id
419
+
420
+ stmt = (
421
+ update(CreditAccountTable)
422
+ .where(
423
+ CreditAccountTable.owner_type == owner_type,
424
+ CreditAccountTable.owner_id == owner_id,
425
+ )
426
+ .values(values_dict)
427
+ .returning(CreditAccountTable)
428
+ )
429
+ res = await session.scalar(stmt)
430
+ if not res:
431
+ raise HTTPException(status_code=500, detail="Failed to income credits")
432
+ return cls.model_validate(res)
433
+
434
+ @classmethod
435
+ async def create_in_session(
436
+ cls,
437
+ session: AsyncSession,
438
+ owner_type: OwnerType,
439
+ owner_id: str,
440
+ free_quota: Decimal = Decimal("480.0"),
441
+ refill_amount: Decimal = Decimal("20.0"),
442
+ ) -> "CreditAccount":
443
+ """Get an existing credit account or create a new one if it doesn't exist.
444
+
445
+ This is useful for silent creation of accounts when they're first accessed.
446
+
447
+ Args:
448
+ session: Async session to use for database queries
449
+ owner_type: Type of the owner
450
+ owner_id: ID of the owner
451
+ free_quota: Daily quota for a new account if created
452
+
453
+ Returns:
454
+ CreditAccount: The existing or newly created credit account
455
+ """
456
+ if owner_type != OwnerType.USER:
457
+ # only users have daily quota
458
+ free_quota = 0.0
459
+ refill_amount = 0.0
460
+ # Create event_id at the beginning for consistency
461
+ event_id = str(XID())
462
+
463
+ account = CreditAccountTable(
464
+ id=str(XID()),
465
+ owner_type=owner_type,
466
+ owner_id=owner_id,
467
+ free_quota=free_quota,
468
+ refill_amount=refill_amount,
469
+ free_credits=free_quota,
470
+ reward_credits=0.0,
471
+ credits=0.0,
472
+ income_at=datetime.now(timezone.utc),
473
+ expense_at=None,
474
+ last_event_id=event_id if owner_type == OwnerType.USER else None,
475
+ )
476
+ # Platform virtual accounts have fixed IDs, same as owner_id
477
+ if owner_type == OwnerType.PLATFORM:
478
+ account.id = owner_id
479
+ session.add(account)
480
+ await session.flush()
481
+ await session.refresh(account)
482
+ # Only user accounts have first refill
483
+ if owner_type == OwnerType.USER:
484
+ # First refill account
485
+ await cls.deduction_in_session(
486
+ session,
487
+ OwnerType.PLATFORM,
488
+ DEFAULT_PLATFORM_ACCOUNT_REFILL,
489
+ CreditType.FREE,
490
+ free_quota,
491
+ event_id,
492
+ )
493
+ # Create refill event record
494
+ event = CreditEventTable(
495
+ id=event_id,
496
+ event_type=EventType.REFILL,
497
+ user_id=owner_id,
498
+ upstream_type=UpstreamType.INITIALIZER,
499
+ upstream_tx_id=account.id,
500
+ direction=Direction.INCOME,
501
+ account_id=account.id,
502
+ credit_type=CreditType.FREE,
503
+ credit_types=[CreditType.FREE],
504
+ total_amount=free_quota,
505
+ balance_after=free_quota,
506
+ base_amount=free_quota,
507
+ base_original_amount=free_quota,
508
+ free_amount=free_quota, # Set free_amount since this is a free credit refill
509
+ reward_amount=Decimal("0"), # No reward credits involved
510
+ permanent_amount=Decimal("0"), # No permanent credits involved
511
+ note="Initial refill",
512
+ )
513
+ session.add(event)
514
+ await session.flush()
515
+
516
+ # Create credit transaction records
517
+ # 1. User account transaction (credit)
518
+ user_tx = CreditTransactionTable(
519
+ id=str(XID()),
520
+ account_id=account.id,
521
+ event_id=event_id,
522
+ tx_type=TransactionType.RECHARGE,
523
+ credit_debit=CreditDebit.CREDIT,
524
+ change_amount=free_quota,
525
+ credit_type=CreditType.FREE,
526
+ )
527
+ session.add(user_tx)
528
+
529
+ # 2. Platform recharge account transaction (debit)
530
+ platform_tx = CreditTransactionTable(
531
+ id=str(XID()),
532
+ account_id=DEFAULT_PLATFORM_ACCOUNT_REFILL,
533
+ event_id=event_id,
534
+ tx_type=TransactionType.REFILL,
535
+ credit_debit=CreditDebit.DEBIT,
536
+ change_amount=free_quota,
537
+ credit_type=CreditType.FREE,
538
+ )
539
+ session.add(platform_tx)
540
+
541
+ return cls.model_validate(account)
542
+
543
+ @classmethod
544
+ async def update_daily_quota(
545
+ cls,
546
+ session: AsyncSession,
547
+ user_id: str,
548
+ free_quota: Optional[Decimal] = None,
549
+ refill_amount: Optional[Decimal] = None,
550
+ upstream_tx_id: str = "",
551
+ note: str = "",
552
+ ) -> "CreditAccount":
553
+ """
554
+ Update the daily quota and refill amount of a user's credit account.
555
+
556
+ Args:
557
+ session: Async session to use for database operations
558
+ user_id: ID of the user to update
559
+ free_quota: Optional new daily quota value
560
+ refill_amount: Optional amount to refill hourly, not exceeding free_quota
561
+ upstream_tx_id: ID of the upstream transaction (for logging purposes)
562
+ note: Explanation for changing the daily quota
563
+
564
+ Returns:
565
+ Updated user credit account
566
+ """
567
+ # Log the upstream_tx_id for record keeping
568
+ logger.info(
569
+ f"Updating quota settings for user {user_id} with upstream_tx_id: {upstream_tx_id}"
570
+ )
571
+
572
+ # Check that at least one parameter is provided
573
+ if free_quota is None and refill_amount is None:
574
+ raise ValueError(
575
+ "At least one of free_quota or refill_amount must be provided"
576
+ )
577
+
578
+ # Get current account to check existing values and validate
579
+ user_account = await cls.get_or_create_in_session(
580
+ session, OwnerType.USER, user_id, for_update=True
581
+ )
582
+
583
+ # Use existing values if not provided
584
+ if free_quota is None:
585
+ free_quota = user_account.free_quota
586
+ elif free_quota <= Decimal("0"):
587
+ raise ValueError("Daily quota must be positive")
588
+
589
+ if refill_amount is None:
590
+ refill_amount = user_account.refill_amount
591
+ elif refill_amount < Decimal("0"):
592
+ raise ValueError("Refill amount cannot be negative")
593
+
594
+ # Ensure refill_amount doesn't exceed free_quota
595
+ if refill_amount > free_quota:
596
+ raise ValueError("Refill amount cannot exceed daily quota")
597
+
598
+ if not note:
599
+ raise ValueError("Quota update requires a note explaining the reason")
600
+
601
+ # Update the free_quota field
602
+ stmt = (
603
+ update(CreditAccountTable)
604
+ .where(
605
+ CreditAccountTable.owner_type == OwnerType.USER,
606
+ CreditAccountTable.owner_id == user_id,
607
+ )
608
+ .values(free_quota=free_quota, refill_amount=refill_amount)
609
+ .returning(CreditAccountTable)
610
+ )
611
+ result = await session.scalar(stmt)
612
+ if not result:
613
+ raise ValueError("Failed to update user account")
614
+
615
+ user_account = cls.model_validate(result)
616
+
617
+ # No credit event needed for updating account settings
618
+
619
+ return user_account
620
+
621
+
622
+ class RewardType(str, Enum):
623
+ """Reward type enumeration for reward-specific events."""
624
+
625
+ REWARD = "reward"
626
+ EVENT_REWARD = "event_reward"
627
+ RECHARGE_BONUS = "recharge_bonus"
628
+
629
+
630
+ class EventType(str, Enum):
631
+ """Type of credit event."""
632
+
633
+ MEMORY = "memory"
634
+ MESSAGE = "message"
635
+ SKILL_CALL = "skill_call"
636
+ VOICE = "voice"
637
+ KNOWLEDGE_BASE = "knowledge_base"
638
+ RECHARGE = "recharge"
639
+ REFUND = "refund"
640
+ ADJUSTMENT = "adjustment"
641
+ REFILL = "refill"
642
+ # Sync with RewardType values
643
+ REWARD = "reward"
644
+ EVENT_REWARD = "event_reward"
645
+ RECHARGE_BONUS = "recharge_bonus"
646
+
647
+ @classmethod
648
+ def get_reward_types(cls):
649
+ """Get all reward-related event types"""
650
+ return [cls.REWARD, cls.EVENT_REWARD, cls.RECHARGE_BONUS]
651
+
652
+
653
+ class UpstreamType(str, Enum):
654
+ """Type of upstream transaction."""
655
+
656
+ API = "api"
657
+ SCHEDULER = "scheduler"
658
+ EXECUTOR = "executor"
659
+ INITIALIZER = "initializer"
660
+
661
+
662
+ class Direction(str, Enum):
663
+ """Direction of credit flow."""
664
+
665
+ INCOME = "income"
666
+ EXPENSE = "expense"
667
+
668
+
669
+ class CreditEventTable(Base):
670
+ """Credit events database table model.
671
+
672
+ Records business events for user, like message processing, skill calls, etc.
673
+ """
674
+
675
+ __tablename__ = "credit_events"
676
+ __table_args__ = (
677
+ Index(
678
+ "ix_credit_events_upstream", "upstream_type", "upstream_tx_id", unique=True
679
+ ),
680
+ Index("ix_credit_events_account_id", "account_id"),
681
+ Index("ix_credit_events_user_id", "user_id"),
682
+ Index("ix_credit_events_agent_id", "agent_id"),
683
+ Index("ix_credit_events_fee_dev", "fee_dev_account"),
684
+ Index("ix_credit_events_created_at", "created_at"),
685
+ )
686
+
687
+ id = Column(
688
+ String,
689
+ primary_key=True,
690
+ )
691
+ account_id = Column(
692
+ String,
693
+ nullable=False,
694
+ )
695
+ event_type = Column(
696
+ String,
697
+ nullable=False,
698
+ )
699
+ user_id = Column(
700
+ String,
701
+ nullable=True,
702
+ )
703
+ upstream_type = Column(
704
+ String,
705
+ nullable=False,
706
+ )
707
+ upstream_tx_id = Column(
708
+ String,
709
+ nullable=False,
710
+ )
711
+ agent_id = Column(
712
+ String,
713
+ nullable=True,
714
+ )
715
+ start_message_id = Column(
716
+ String,
717
+ nullable=True,
718
+ )
719
+ message_id = Column(
720
+ String,
721
+ nullable=True,
722
+ )
723
+ model = Column(
724
+ String,
725
+ nullable=True,
726
+ )
727
+ skill_call_id = Column(
728
+ String,
729
+ nullable=True,
730
+ )
731
+ skill_name = Column(
732
+ String,
733
+ nullable=True,
734
+ )
735
+ direction = Column(
736
+ String,
737
+ nullable=False,
738
+ )
739
+ total_amount = Column(
740
+ Numeric(22, 4),
741
+ default=0,
742
+ nullable=False,
743
+ )
744
+ credit_type = Column(
745
+ String,
746
+ nullable=False,
747
+ )
748
+ credit_types = Column(
749
+ JSON().with_variant(ARRAY(String), "postgresql"),
750
+ nullable=True,
751
+ )
752
+ balance_after = Column(
753
+ Numeric(22, 4),
754
+ nullable=True,
755
+ default=None,
756
+ )
757
+ base_amount = Column(
758
+ Numeric(22, 4),
759
+ default=0,
760
+ nullable=False,
761
+ )
762
+ base_discount_amount = Column(
763
+ Numeric(22, 4),
764
+ default=0,
765
+ nullable=True,
766
+ )
767
+ base_original_amount = Column(
768
+ Numeric(22, 4),
769
+ default=0,
770
+ nullable=True,
771
+ )
772
+ base_llm_amount = Column(
773
+ Numeric(22, 4),
774
+ default=0,
775
+ nullable=True,
776
+ )
777
+ base_skill_amount = Column(
778
+ Numeric(22, 4),
779
+ default=0,
780
+ nullable=True,
781
+ )
782
+ fee_platform_amount = Column(
783
+ Numeric(22, 4),
784
+ default=0,
785
+ nullable=True,
786
+ )
787
+ fee_platform_free_amount = Column(
788
+ Numeric(22, 4),
789
+ nullable=True,
790
+ )
791
+ fee_platform_reward_amount = Column(
792
+ Numeric(22, 4),
793
+ nullable=True,
794
+ )
795
+ fee_platform_permanent_amount = Column(
796
+ Numeric(22, 4),
797
+ nullable=True,
798
+ )
799
+ fee_dev_account = Column(
800
+ String,
801
+ nullable=True,
802
+ )
803
+ fee_dev_amount = Column(
804
+ Numeric(22, 4),
805
+ default=0,
806
+ nullable=True,
807
+ )
808
+ fee_dev_free_amount = Column(
809
+ Numeric(22, 4),
810
+ nullable=True,
811
+ )
812
+ fee_dev_reward_amount = Column(
813
+ Numeric(22, 4),
814
+ nullable=True,
815
+ )
816
+ fee_dev_permanent_amount = Column(
817
+ Numeric(22, 4),
818
+ nullable=True,
819
+ )
820
+ fee_agent_account = Column(
821
+ String,
822
+ nullable=True,
823
+ )
824
+ fee_agent_amount = Column(
825
+ Numeric(22, 4),
826
+ default=0,
827
+ nullable=True,
828
+ )
829
+ fee_agent_free_amount = Column(
830
+ Numeric(22, 4),
831
+ nullable=True,
832
+ )
833
+ fee_agent_reward_amount = Column(
834
+ Numeric(22, 4),
835
+ nullable=True,
836
+ )
837
+ fee_agent_permanent_amount = Column(
838
+ Numeric(22, 4),
839
+ nullable=True,
840
+ )
841
+ free_amount = Column(
842
+ Numeric(22, 4),
843
+ default=0,
844
+ nullable=True,
845
+ )
846
+ reward_amount = Column(
847
+ Numeric(22, 4),
848
+ default=0,
849
+ nullable=True,
850
+ )
851
+ permanent_amount = Column(
852
+ Numeric(22, 4),
853
+ default=0,
854
+ nullable=True,
855
+ )
856
+ note = Column(
857
+ String,
858
+ nullable=True,
859
+ )
860
+ created_at = Column(
861
+ DateTime(timezone=True),
862
+ nullable=False,
863
+ server_default=func.now(),
864
+ )
865
+
866
+
867
+ class CreditEvent(BaseModel):
868
+ """Credit event model with all fields."""
869
+
870
+ model_config = ConfigDict(
871
+ use_enum_values=True,
872
+ from_attributes=True,
873
+ json_encoders={
874
+ datetime: lambda v: v.isoformat(timespec="milliseconds"),
875
+ },
876
+ )
877
+
878
+ id: Annotated[
879
+ str,
880
+ Field(
881
+ default_factory=lambda: str(XID()),
882
+ description="Unique identifier for the credit event",
883
+ ),
884
+ ]
885
+ account_id: Annotated[
886
+ str, Field(None, description="Account ID from which credits flow")
887
+ ]
888
+ event_type: Annotated[EventType, Field(description="Type of the event")]
889
+ user_id: Annotated[
890
+ Optional[str], Field(None, description="ID of the user if applicable")
891
+ ]
892
+ upstream_type: Annotated[
893
+ UpstreamType, Field(description="Type of upstream transaction")
894
+ ]
895
+ upstream_tx_id: Annotated[str, Field(description="Upstream transaction ID if any")]
896
+ agent_id: Annotated[
897
+ Optional[str], Field(None, description="ID of the agent if applicable")
898
+ ]
899
+ start_message_id: Annotated[
900
+ Optional[str],
901
+ Field(None, description="ID of the starting message if applicable"),
902
+ ]
903
+ message_id: Annotated[
904
+ Optional[str], Field(None, description="ID of the message if applicable")
905
+ ]
906
+ model: Annotated[
907
+ Optional[str], Field(None, description="LLM model used if applicable")
908
+ ]
909
+ skill_call_id: Annotated[
910
+ Optional[str], Field(None, description="ID of the skill call if applicable")
911
+ ]
912
+ skill_name: Annotated[
913
+ Optional[str], Field(None, description="Name of the skill if applicable")
914
+ ]
915
+ direction: Annotated[Direction, Field(description="Direction of the credit flow")]
916
+ total_amount: Annotated[
917
+ Decimal,
918
+ Field(
919
+ default=Decimal("0"),
920
+ description="Total amount (after discount) of credits involved",
921
+ ),
922
+ ]
923
+ credit_type: Annotated[CreditType, Field(description="Type of credits involved")]
924
+ credit_types: Annotated[
925
+ Optional[List[CreditType]],
926
+ Field(default=None, description="Array of credit types involved"),
927
+ ]
928
+ balance_after: Annotated[
929
+ Optional[Decimal],
930
+ Field(None, description="Account total balance after the transaction"),
931
+ ]
932
+ base_amount: Annotated[
933
+ Decimal,
934
+ Field(default=Decimal("0"), description="Base amount of credits involved"),
935
+ ]
936
+ base_discount_amount: Annotated[
937
+ Optional[Decimal],
938
+ Field(default=Decimal("0"), description="Base discount amount"),
939
+ ]
940
+ base_original_amount: Annotated[
941
+ Optional[Decimal],
942
+ Field(default=Decimal("0"), description="Base original amount"),
943
+ ]
944
+ base_llm_amount: Annotated[
945
+ Optional[Decimal],
946
+ Field(default=Decimal("0"), description="Base LLM cost amount"),
947
+ ]
948
+ base_skill_amount: Annotated[
949
+ Optional[Decimal],
950
+ Field(default=Decimal("0"), description="Base skill cost amount"),
951
+ ]
952
+ fee_platform_amount: Annotated[
953
+ Optional[Decimal],
954
+ Field(default=Decimal("0"), description="Platform fee amount"),
955
+ ]
956
+ fee_platform_free_amount: Annotated[
957
+ Optional[Decimal],
958
+ Field(
959
+ default=Decimal("0"), description="Platform fee amount from free credits"
960
+ ),
961
+ ]
962
+ fee_platform_reward_amount: Annotated[
963
+ Optional[Decimal],
964
+ Field(
965
+ default=Decimal("0"), description="Platform fee amount from reward credits"
966
+ ),
967
+ ]
968
+ fee_platform_permanent_amount: Annotated[
969
+ Optional[Decimal],
970
+ Field(
971
+ default=Decimal("0"),
972
+ description="Platform fee amount from permanent credits",
973
+ ),
974
+ ]
975
+ fee_dev_account: Annotated[
976
+ Optional[str], Field(None, description="Developer account ID receiving fee")
977
+ ]
978
+ fee_dev_amount: Annotated[
979
+ Optional[Decimal],
980
+ Field(default=Decimal("0"), description="Developer fee amount"),
981
+ ]
982
+ fee_dev_free_amount: Annotated[
983
+ Optional[Decimal],
984
+ Field(
985
+ default=Decimal("0"), description="Developer fee amount from free credits"
986
+ ),
987
+ ]
988
+ fee_dev_reward_amount: Annotated[
989
+ Optional[Decimal],
990
+ Field(
991
+ default=Decimal("0"), description="Developer fee amount from reward credits"
992
+ ),
993
+ ]
994
+ fee_dev_permanent_amount: Annotated[
995
+ Optional[Decimal],
996
+ Field(
997
+ default=Decimal("0"),
998
+ description="Developer fee amount from permanent credits",
999
+ ),
1000
+ ]
1001
+ fee_agent_account: Annotated[
1002
+ Optional[str], Field(None, description="Agent account ID receiving fee")
1003
+ ]
1004
+ fee_agent_amount: Annotated[
1005
+ Optional[Decimal], Field(default=Decimal("0"), description="Agent fee amount")
1006
+ ]
1007
+ fee_agent_free_amount: Annotated[
1008
+ Optional[Decimal],
1009
+ Field(default=Decimal("0"), description="Agent fee amount from free credits"),
1010
+ ]
1011
+ fee_agent_reward_amount: Annotated[
1012
+ Optional[Decimal],
1013
+ Field(default=Decimal("0"), description="Agent fee amount from reward credits"),
1014
+ ]
1015
+ fee_agent_permanent_amount: Annotated[
1016
+ Optional[Decimal],
1017
+ Field(
1018
+ default=Decimal("0"), description="Agent fee amount from permanent credits"
1019
+ ),
1020
+ ]
1021
+ free_amount: Annotated[
1022
+ Optional[Decimal],
1023
+ Field(default=Decimal("0"), description="Free credit amount involved"),
1024
+ ]
1025
+ reward_amount: Annotated[
1026
+ Optional[Decimal],
1027
+ Field(default=Decimal("0"), description="Reward credit amount involved"),
1028
+ ]
1029
+ permanent_amount: Annotated[
1030
+ Optional[Decimal],
1031
+ Field(default=Decimal("0"), description="Permanent credit amount involved"),
1032
+ ]
1033
+ note: Annotated[Optional[str], Field(None, description="Additional notes")]
1034
+ created_at: Annotated[
1035
+ datetime, Field(description="Timestamp when this event was created")
1036
+ ]
1037
+
1038
+ @field_validator(
1039
+ "total_amount",
1040
+ "balance_after",
1041
+ "base_amount",
1042
+ "base_discount_amount",
1043
+ "base_original_amount",
1044
+ "base_llm_amount",
1045
+ "base_skill_amount",
1046
+ "fee_platform_amount",
1047
+ "fee_platform_free_amount",
1048
+ "fee_platform_reward_amount",
1049
+ "fee_platform_permanent_amount",
1050
+ "fee_dev_amount",
1051
+ "fee_dev_free_amount",
1052
+ "fee_dev_reward_amount",
1053
+ "fee_dev_permanent_amount",
1054
+ "fee_agent_amount",
1055
+ "fee_agent_free_amount",
1056
+ "fee_agent_reward_amount",
1057
+ "fee_agent_permanent_amount",
1058
+ "free_amount",
1059
+ "reward_amount",
1060
+ "permanent_amount",
1061
+ )
1062
+ @classmethod
1063
+ def round_decimal(cls, v: Any) -> Optional[Decimal]:
1064
+ """Round decimal values to 4 decimal places."""
1065
+ if v is None:
1066
+ return None
1067
+ if isinstance(v, Decimal):
1068
+ return v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1069
+ elif isinstance(v, (int, float)):
1070
+ return Decimal(str(v)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1071
+ return v
1072
+
1073
+ @classmethod
1074
+ async def check_upstream_tx_id_exists(
1075
+ cls, session: AsyncSession, upstream_type: UpstreamType, upstream_tx_id: str
1076
+ ) -> None:
1077
+ """
1078
+ Check if an event with the given upstream_type and upstream_tx_id already exists.
1079
+ Raises HTTP 400 error if it exists to prevent duplicate transactions.
1080
+
1081
+ Args:
1082
+ session: Database session
1083
+ upstream_type: Type of the upstream transaction
1084
+ upstream_tx_id: ID of the upstream transaction
1085
+
1086
+ Raises:
1087
+ HTTPException: If a transaction with the same upstream_tx_id already exists
1088
+ """
1089
+ stmt = select(CreditEventTable).where(
1090
+ CreditEventTable.upstream_type == upstream_type,
1091
+ CreditEventTable.upstream_tx_id == upstream_tx_id,
1092
+ )
1093
+ result = await session.scalar(stmt)
1094
+ if result:
1095
+ raise HTTPException(
1096
+ status_code=400,
1097
+ detail=f"Transaction with upstream_tx_id '{upstream_tx_id}' already exists. Do not resubmit.",
1098
+ )
1099
+
1100
+
1101
+ class TransactionType(str, Enum):
1102
+ """Type of credit transaction."""
1103
+
1104
+ PAY = "pay"
1105
+ RECEIVE_BASE_LLM = "receive_base_llm"
1106
+ RECEIVE_BASE_SKILL = "receive_base_skill"
1107
+ RECEIVE_BASE_MEMORY = "receive_base_memory"
1108
+ RECEIVE_BASE_VOICE = "receive_base_voice"
1109
+ RECEIVE_BASE_KNOWLEDGE = "receive_base_knowledge"
1110
+ RECEIVE_FEE_DEV = "receive_fee_dev"
1111
+ RECEIVE_FEE_AGENT = "receive_fee_agent"
1112
+ RECEIVE_FEE_PLATFORM = "receive_fee_platform"
1113
+ RECHARGE = "recharge"
1114
+ REFUND = "refund"
1115
+ ADJUSTMENT = "adjustment"
1116
+ REFILL = "refill"
1117
+ # Sync with RewardType values
1118
+ REWARD = "reward"
1119
+ EVENT_REWARD = "event_reward"
1120
+ RECHARGE_BONUS = "recharge_bonus"
1121
+
1122
+
1123
+ class CreditDebit(str, Enum):
1124
+ """Credit or debit transaction."""
1125
+
1126
+ CREDIT = "credit"
1127
+ DEBIT = "debit"
1128
+
1129
+
1130
+ class CreditTransactionTable(Base):
1131
+ """Credit transactions database table model.
1132
+
1133
+ Records the flow of credits in and out of accounts.
1134
+ """
1135
+
1136
+ __tablename__ = "credit_transactions"
1137
+ __table_args__ = (
1138
+ Index("ix_credit_transactions_account", "account_id"),
1139
+ Index("ix_credit_transactions_event_id", "event_id"),
1140
+ )
1141
+
1142
+ id = Column(
1143
+ String,
1144
+ primary_key=True,
1145
+ )
1146
+ account_id = Column(
1147
+ String,
1148
+ nullable=False,
1149
+ )
1150
+ event_id = Column(
1151
+ String,
1152
+ nullable=False,
1153
+ )
1154
+ tx_type = Column(
1155
+ String,
1156
+ nullable=False,
1157
+ )
1158
+ credit_debit = Column(
1159
+ String,
1160
+ nullable=False,
1161
+ )
1162
+ change_amount = Column(
1163
+ Numeric(22, 4),
1164
+ default=0,
1165
+ nullable=False,
1166
+ )
1167
+ credit_type = Column(
1168
+ String,
1169
+ nullable=False,
1170
+ )
1171
+ created_at = Column(
1172
+ DateTime(timezone=True),
1173
+ nullable=False,
1174
+ server_default=func.now(),
1175
+ )
1176
+
1177
+
1178
+ class CreditTransaction(BaseModel):
1179
+ """Credit transaction model with all fields."""
1180
+
1181
+ model_config = ConfigDict(
1182
+ use_enum_values=True,
1183
+ from_attributes=True,
1184
+ json_encoders={datetime: lambda v: v.isoformat(timespec="milliseconds")},
1185
+ )
1186
+
1187
+ id: Annotated[
1188
+ str,
1189
+ Field(
1190
+ default_factory=lambda: str(XID()),
1191
+ description="Unique identifier for the credit transaction",
1192
+ ),
1193
+ ]
1194
+ account_id: Annotated[
1195
+ str, Field(description="ID of the account this transaction belongs to")
1196
+ ]
1197
+ event_id: Annotated[
1198
+ str, Field(description="ID of the event that triggered this transaction")
1199
+ ]
1200
+ tx_type: Annotated[TransactionType, Field(description="Type of the transaction")]
1201
+ credit_debit: Annotated[
1202
+ CreditDebit, Field(description="Whether this is a credit or debit transaction")
1203
+ ]
1204
+ change_amount: Annotated[
1205
+ Decimal, Field(default=Decimal("0"), description="Amount of credits changed")
1206
+ ]
1207
+
1208
+ @field_validator("change_amount")
1209
+ @classmethod
1210
+ def round_decimal(cls, v: Any) -> Decimal:
1211
+ """Round decimal values to 4 decimal places."""
1212
+ if isinstance(v, Decimal):
1213
+ return v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1214
+ elif isinstance(v, (int, float)):
1215
+ return Decimal(str(v)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1216
+ return v
1217
+
1218
+ credit_type: Annotated[CreditType, Field(description="Type of credits involved")]
1219
+ created_at: Annotated[
1220
+ datetime, Field(description="Timestamp when this transaction was created")
1221
+ ]
1222
+
1223
+
1224
+ class PriceEntity(str, Enum):
1225
+ """Type of credit price."""
1226
+
1227
+ SKILL_CALL = "skill_call"
1228
+
1229
+
1230
+ class DiscountType(str, Enum):
1231
+ """Type of discount."""
1232
+
1233
+ STANDARD = "standard"
1234
+ SELF_KEY = "self_key"
1235
+
1236
+
1237
+ DEFAULT_SKILL_CALL_PRICE = Decimal("10.0000")
1238
+ DEFAULT_SKILL_CALL_SELF_KEY_PRICE = Decimal("5.0000")
1239
+
1240
+
1241
+ class CreditPriceTable(Base):
1242
+ """Credit price database table model.
1243
+
1244
+ Stores price information for different types of services.
1245
+ """
1246
+
1247
+ __tablename__ = "credit_prices"
1248
+
1249
+ id = Column(
1250
+ String,
1251
+ primary_key=True,
1252
+ )
1253
+ price_entity = Column(
1254
+ String,
1255
+ nullable=False,
1256
+ )
1257
+ price_entity_id = Column(
1258
+ String,
1259
+ nullable=False,
1260
+ )
1261
+ discount_type = Column(
1262
+ String,
1263
+ nullable=False,
1264
+ )
1265
+ price = Column(
1266
+ Numeric(22, 4),
1267
+ default=0,
1268
+ nullable=False,
1269
+ )
1270
+ created_at = Column(
1271
+ DateTime(timezone=True),
1272
+ nullable=False,
1273
+ server_default=func.now(),
1274
+ )
1275
+ updated_at = Column(
1276
+ DateTime(timezone=True),
1277
+ nullable=False,
1278
+ server_default=func.now(),
1279
+ onupdate=lambda: datetime.now(timezone.utc),
1280
+ )
1281
+
1282
+
1283
+ class CreditPrice(BaseModel):
1284
+ """Credit price model with all fields."""
1285
+
1286
+ model_config = ConfigDict(
1287
+ use_enum_values=True,
1288
+ from_attributes=True,
1289
+ json_encoders={datetime: lambda v: v.isoformat(timespec="milliseconds")},
1290
+ )
1291
+
1292
+ id: Annotated[
1293
+ str,
1294
+ Field(
1295
+ default_factory=lambda: str(XID()),
1296
+ description="Unique identifier for the credit price",
1297
+ ),
1298
+ ]
1299
+ price_entity: Annotated[
1300
+ PriceEntity, Field(description="Type of the price (agent or skill_call)")
1301
+ ]
1302
+ price_entity_id: Annotated[
1303
+ str, Field(description="ID of the price entity, the skill is the name")
1304
+ ]
1305
+ discount_type: Annotated[
1306
+ DiscountType,
1307
+ Field(default=DiscountType.STANDARD, description="Type of discount"),
1308
+ ]
1309
+ price: Annotated[Decimal, Field(default=Decimal("0"), description="Standard price")]
1310
+
1311
+ @field_validator("price")
1312
+ @classmethod
1313
+ def round_decimal(cls, v: Any) -> Decimal:
1314
+ """Round decimal values to 4 decimal places."""
1315
+ if isinstance(v, Decimal):
1316
+ return v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1317
+ elif isinstance(v, (int, float)):
1318
+ return Decimal(str(v)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1319
+ return v
1320
+
1321
+ created_at: Annotated[
1322
+ datetime, Field(description="Timestamp when this price was created")
1323
+ ]
1324
+ updated_at: Annotated[
1325
+ datetime, Field(description="Timestamp when this price was last updated")
1326
+ ]
1327
+
1328
+
1329
+ class CreditPriceLogTable(Base):
1330
+ """Credit price log database table model.
1331
+
1332
+ Records history of price changes.
1333
+ """
1334
+
1335
+ __tablename__ = "credit_price_logs"
1336
+
1337
+ id = Column(
1338
+ String,
1339
+ primary_key=True,
1340
+ )
1341
+ price_id = Column(
1342
+ String,
1343
+ nullable=False,
1344
+ )
1345
+ old_price = Column(
1346
+ Numeric(22, 4),
1347
+ nullable=False,
1348
+ )
1349
+ new_price = Column(
1350
+ Numeric(22, 4),
1351
+ nullable=False,
1352
+ )
1353
+ note = Column(
1354
+ String,
1355
+ nullable=True,
1356
+ )
1357
+ modified_by = Column(
1358
+ String,
1359
+ nullable=False,
1360
+ )
1361
+ modified_at = Column(
1362
+ DateTime(timezone=True),
1363
+ nullable=False,
1364
+ server_default=func.now(),
1365
+ )
1366
+
1367
+
1368
+ class CreditPriceLog(BaseModel):
1369
+ """Credit price log model with all fields."""
1370
+
1371
+ model_config = ConfigDict(
1372
+ use_enum_values=True,
1373
+ from_attributes=True,
1374
+ json_encoders={datetime: lambda v: v.isoformat(timespec="milliseconds")},
1375
+ )
1376
+
1377
+ id: Annotated[
1378
+ str,
1379
+ Field(
1380
+ default_factory=lambda: str(XID()),
1381
+ description="Unique identifier for the log entry",
1382
+ ),
1383
+ ]
1384
+ price_id: Annotated[str, Field(description="ID of the price that was modified")]
1385
+ old_price: Annotated[Decimal, Field(description="Previous standard price")]
1386
+ new_price: Annotated[Decimal, Field(description="New standard price")]
1387
+
1388
+ @field_validator("old_price", "new_price")
1389
+ @classmethod
1390
+ def round_decimal(cls, v: Any) -> Decimal:
1391
+ """Round decimal values to 4 decimal places."""
1392
+ if isinstance(v, Decimal):
1393
+ return v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1394
+ elif isinstance(v, (int, float)):
1395
+ return Decimal(str(v)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
1396
+ return v
1397
+
1398
+ note: Annotated[
1399
+ Optional[str], Field(None, description="Note about the modification")
1400
+ ]
1401
+ modified_by: Annotated[
1402
+ str, Field(description="ID of the user who made the modification")
1403
+ ]
1404
+ modified_at: Annotated[
1405
+ datetime, Field(description="Timestamp when the modification was made")
1406
+ ]