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.
- intentkit/__init__.py +17 -0
- intentkit/abstracts/__init__.py +0 -0
- intentkit/abstracts/agent.py +60 -0
- intentkit/abstracts/api.py +4 -0
- intentkit/abstracts/engine.py +38 -0
- intentkit/abstracts/exception.py +9 -0
- intentkit/abstracts/graph.py +25 -0
- intentkit/abstracts/skill.py +129 -0
- intentkit/abstracts/twitter.py +54 -0
- intentkit/clients/__init__.py +14 -0
- intentkit/clients/cdp.py +53 -0
- intentkit/clients/twitter.py +445 -0
- intentkit/config/__init__.py +0 -0
- intentkit/config/config.py +164 -0
- intentkit/core/__init__.py +0 -0
- intentkit/core/agent.py +191 -0
- intentkit/core/api.py +40 -0
- intentkit/core/client.py +45 -0
- intentkit/core/credit.py +1767 -0
- intentkit/core/engine.py +1018 -0
- intentkit/core/node.py +223 -0
- intentkit/core/prompt.py +58 -0
- intentkit/core/skill.py +124 -0
- intentkit/models/agent.py +1689 -0
- intentkit/models/agent_data.py +810 -0
- intentkit/models/agent_schema.json +733 -0
- intentkit/models/app_setting.py +156 -0
- intentkit/models/base.py +9 -0
- intentkit/models/chat.py +581 -0
- intentkit/models/conversation.py +286 -0
- intentkit/models/credit.py +1406 -0
- intentkit/models/db.py +120 -0
- intentkit/models/db_mig.py +102 -0
- intentkit/models/generator.py +347 -0
- intentkit/models/llm.py +746 -0
- intentkit/models/redis.py +132 -0
- intentkit/models/skill.py +466 -0
- intentkit/models/user.py +243 -0
- intentkit/skills/__init__.py +12 -0
- intentkit/skills/acolyt/__init__.py +83 -0
- intentkit/skills/acolyt/acolyt.jpg +0 -0
- intentkit/skills/acolyt/ask.py +128 -0
- intentkit/skills/acolyt/base.py +28 -0
- intentkit/skills/acolyt/schema.json +89 -0
- intentkit/skills/aixbt/README.md +71 -0
- intentkit/skills/aixbt/__init__.py +73 -0
- intentkit/skills/aixbt/aixbt.jpg +0 -0
- intentkit/skills/aixbt/base.py +21 -0
- intentkit/skills/aixbt/projects.py +153 -0
- intentkit/skills/aixbt/schema.json +99 -0
- intentkit/skills/allora/__init__.py +83 -0
- intentkit/skills/allora/allora.jpeg +0 -0
- intentkit/skills/allora/base.py +28 -0
- intentkit/skills/allora/price.py +130 -0
- intentkit/skills/allora/schema.json +89 -0
- intentkit/skills/base.py +174 -0
- intentkit/skills/carv/README.md +95 -0
- intentkit/skills/carv/__init__.py +121 -0
- intentkit/skills/carv/base.py +183 -0
- intentkit/skills/carv/carv.webp +0 -0
- intentkit/skills/carv/fetch_news.py +92 -0
- intentkit/skills/carv/onchain_query.py +164 -0
- intentkit/skills/carv/schema.json +137 -0
- intentkit/skills/carv/token_info_and_price.py +110 -0
- intentkit/skills/cdp/__init__.py +137 -0
- intentkit/skills/cdp/base.py +21 -0
- intentkit/skills/cdp/cdp.png +0 -0
- intentkit/skills/cdp/get_balance.py +81 -0
- intentkit/skills/cdp/schema.json +473 -0
- intentkit/skills/chainlist/README.md +38 -0
- intentkit/skills/chainlist/__init__.py +54 -0
- intentkit/skills/chainlist/base.py +21 -0
- intentkit/skills/chainlist/chain_lookup.py +208 -0
- intentkit/skills/chainlist/chainlist.png +0 -0
- intentkit/skills/chainlist/schema.json +47 -0
- intentkit/skills/common/__init__.py +82 -0
- intentkit/skills/common/base.py +21 -0
- intentkit/skills/common/common.jpg +0 -0
- intentkit/skills/common/current_time.py +84 -0
- intentkit/skills/common/schema.json +57 -0
- intentkit/skills/cookiefun/README.md +121 -0
- intentkit/skills/cookiefun/__init__.py +78 -0
- intentkit/skills/cookiefun/base.py +41 -0
- intentkit/skills/cookiefun/constants.py +18 -0
- intentkit/skills/cookiefun/cookiefun.png +0 -0
- intentkit/skills/cookiefun/get_account_details.py +171 -0
- intentkit/skills/cookiefun/get_account_feed.py +282 -0
- intentkit/skills/cookiefun/get_account_smart_followers.py +181 -0
- intentkit/skills/cookiefun/get_sectors.py +128 -0
- intentkit/skills/cookiefun/schema.json +155 -0
- intentkit/skills/cookiefun/search_accounts.py +225 -0
- intentkit/skills/cryptocompare/__init__.py +130 -0
- intentkit/skills/cryptocompare/api.py +159 -0
- intentkit/skills/cryptocompare/base.py +303 -0
- intentkit/skills/cryptocompare/cryptocompare.png +0 -0
- intentkit/skills/cryptocompare/fetch_news.py +96 -0
- intentkit/skills/cryptocompare/fetch_price.py +99 -0
- intentkit/skills/cryptocompare/fetch_top_exchanges.py +113 -0
- intentkit/skills/cryptocompare/fetch_top_market_cap.py +109 -0
- intentkit/skills/cryptocompare/fetch_top_volume.py +108 -0
- intentkit/skills/cryptocompare/fetch_trading_signals.py +107 -0
- intentkit/skills/cryptocompare/schema.json +168 -0
- intentkit/skills/cryptopanic/__init__.py +108 -0
- intentkit/skills/cryptopanic/base.py +51 -0
- intentkit/skills/cryptopanic/cryptopanic.png +0 -0
- intentkit/skills/cryptopanic/fetch_crypto_news.py +153 -0
- intentkit/skills/cryptopanic/fetch_crypto_sentiment.py +136 -0
- intentkit/skills/cryptopanic/schema.json +103 -0
- intentkit/skills/dapplooker/README.md +92 -0
- intentkit/skills/dapplooker/__init__.py +83 -0
- intentkit/skills/dapplooker/base.py +26 -0
- intentkit/skills/dapplooker/dapplooker.jpg +0 -0
- intentkit/skills/dapplooker/dapplooker_token_data.py +476 -0
- intentkit/skills/dapplooker/schema.json +91 -0
- intentkit/skills/defillama/__init__.py +323 -0
- intentkit/skills/defillama/api.py +315 -0
- intentkit/skills/defillama/base.py +135 -0
- intentkit/skills/defillama/coins/__init__.py +0 -0
- intentkit/skills/defillama/coins/fetch_batch_historical_prices.py +116 -0
- intentkit/skills/defillama/coins/fetch_block.py +98 -0
- intentkit/skills/defillama/coins/fetch_current_prices.py +105 -0
- intentkit/skills/defillama/coins/fetch_first_price.py +100 -0
- intentkit/skills/defillama/coins/fetch_historical_prices.py +110 -0
- intentkit/skills/defillama/coins/fetch_price_chart.py +109 -0
- intentkit/skills/defillama/coins/fetch_price_percentage.py +93 -0
- intentkit/skills/defillama/config/__init__.py +0 -0
- intentkit/skills/defillama/config/chains.py +433 -0
- intentkit/skills/defillama/defillama.jpeg +0 -0
- intentkit/skills/defillama/fees/__init__.py +0 -0
- intentkit/skills/defillama/fees/fetch_fees_overview.py +130 -0
- intentkit/skills/defillama/schema.json +383 -0
- intentkit/skills/defillama/stablecoins/__init__.py +0 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoin_chains.py +100 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoin_charts.py +129 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoin_prices.py +83 -0
- intentkit/skills/defillama/stablecoins/fetch_stablecoins.py +126 -0
- intentkit/skills/defillama/tests/__init__.py +0 -0
- intentkit/skills/defillama/tests/api_integration.test.py +192 -0
- intentkit/skills/defillama/tests/api_unit.test.py +583 -0
- intentkit/skills/defillama/tvl/__init__.py +0 -0
- intentkit/skills/defillama/tvl/fetch_chain_historical_tvl.py +106 -0
- intentkit/skills/defillama/tvl/fetch_chains.py +107 -0
- intentkit/skills/defillama/tvl/fetch_historical_tvl.py +91 -0
- intentkit/skills/defillama/tvl/fetch_protocol.py +207 -0
- intentkit/skills/defillama/tvl/fetch_protocol_current_tvl.py +93 -0
- intentkit/skills/defillama/tvl/fetch_protocols.py +196 -0
- intentkit/skills/defillama/volumes/__init__.py +0 -0
- intentkit/skills/defillama/volumes/fetch_dex_overview.py +157 -0
- intentkit/skills/defillama/volumes/fetch_dex_summary.py +123 -0
- intentkit/skills/defillama/volumes/fetch_options_overview.py +131 -0
- intentkit/skills/defillama/yields/__init__.py +0 -0
- intentkit/skills/defillama/yields/fetch_pool_chart.py +100 -0
- intentkit/skills/defillama/yields/fetch_pools.py +126 -0
- intentkit/skills/dexscreener/__init__.py +93 -0
- intentkit/skills/dexscreener/base.py +133 -0
- intentkit/skills/dexscreener/dexscreener.png +0 -0
- intentkit/skills/dexscreener/model/__init__.py +0 -0
- intentkit/skills/dexscreener/model/search_token_response.py +82 -0
- intentkit/skills/dexscreener/schema.json +48 -0
- intentkit/skills/dexscreener/search_token.py +321 -0
- intentkit/skills/dune_analytics/__init__.py +103 -0
- intentkit/skills/dune_analytics/base.py +46 -0
- intentkit/skills/dune_analytics/dune.png +0 -0
- intentkit/skills/dune_analytics/fetch_kol_buys.py +128 -0
- intentkit/skills/dune_analytics/fetch_nation_metrics.py +237 -0
- intentkit/skills/dune_analytics/schema.json +99 -0
- intentkit/skills/elfa/README.md +100 -0
- intentkit/skills/elfa/__init__.py +123 -0
- intentkit/skills/elfa/base.py +28 -0
- intentkit/skills/elfa/elfa.jpg +0 -0
- intentkit/skills/elfa/mention.py +504 -0
- intentkit/skills/elfa/schema.json +153 -0
- intentkit/skills/elfa/stats.py +118 -0
- intentkit/skills/elfa/tokens.py +126 -0
- intentkit/skills/enso/README.md +75 -0
- intentkit/skills/enso/__init__.py +114 -0
- intentkit/skills/enso/abi/__init__.py +0 -0
- intentkit/skills/enso/abi/approval.py +279 -0
- intentkit/skills/enso/abi/erc20.py +14 -0
- intentkit/skills/enso/abi/route.py +129 -0
- intentkit/skills/enso/base.py +44 -0
- intentkit/skills/enso/best_yield.py +286 -0
- intentkit/skills/enso/enso.jpg +0 -0
- intentkit/skills/enso/networks.py +105 -0
- intentkit/skills/enso/prices.py +93 -0
- intentkit/skills/enso/route.py +300 -0
- intentkit/skills/enso/schema.json +212 -0
- intentkit/skills/enso/tokens.py +223 -0
- intentkit/skills/enso/wallet.py +381 -0
- intentkit/skills/github/README.md +63 -0
- intentkit/skills/github/__init__.py +54 -0
- intentkit/skills/github/base.py +21 -0
- intentkit/skills/github/github.jpg +0 -0
- intentkit/skills/github/github_search.py +183 -0
- intentkit/skills/github/schema.json +59 -0
- intentkit/skills/heurist/__init__.py +143 -0
- intentkit/skills/heurist/base.py +26 -0
- intentkit/skills/heurist/heurist.png +0 -0
- intentkit/skills/heurist/image_generation_animagine_xl.py +162 -0
- intentkit/skills/heurist/image_generation_arthemy_comics.py +162 -0
- intentkit/skills/heurist/image_generation_arthemy_real.py +162 -0
- intentkit/skills/heurist/image_generation_braindance.py +162 -0
- intentkit/skills/heurist/image_generation_cyber_realistic_xl.py +162 -0
- intentkit/skills/heurist/image_generation_flux_1_dev.py +162 -0
- intentkit/skills/heurist/image_generation_sdxl.py +161 -0
- intentkit/skills/heurist/schema.json +196 -0
- intentkit/skills/lifi/README.md +294 -0
- intentkit/skills/lifi/__init__.py +141 -0
- intentkit/skills/lifi/base.py +21 -0
- intentkit/skills/lifi/lifi.png +0 -0
- intentkit/skills/lifi/schema.json +89 -0
- intentkit/skills/lifi/token_execute.py +472 -0
- intentkit/skills/lifi/token_quote.py +190 -0
- intentkit/skills/lifi/utils.py +656 -0
- intentkit/skills/moralis/README.md +490 -0
- intentkit/skills/moralis/__init__.py +110 -0
- intentkit/skills/moralis/api.py +281 -0
- intentkit/skills/moralis/base.py +55 -0
- intentkit/skills/moralis/fetch_chain_portfolio.py +191 -0
- intentkit/skills/moralis/fetch_nft_portfolio.py +284 -0
- intentkit/skills/moralis/fetch_solana_portfolio.py +331 -0
- intentkit/skills/moralis/fetch_wallet_portfolio.py +301 -0
- intentkit/skills/moralis/moralis.png +0 -0
- intentkit/skills/moralis/schema.json +156 -0
- intentkit/skills/moralis/tests/__init__.py +0 -0
- intentkit/skills/moralis/tests/test_wallet.py +511 -0
- intentkit/skills/nation/__init__.py +62 -0
- intentkit/skills/nation/base.py +31 -0
- intentkit/skills/nation/nation.png +0 -0
- intentkit/skills/nation/nft_check.py +106 -0
- intentkit/skills/nation/schema.json +58 -0
- intentkit/skills/openai/__init__.py +107 -0
- intentkit/skills/openai/base.py +32 -0
- intentkit/skills/openai/dalle_image_generation.py +128 -0
- intentkit/skills/openai/gpt_image_generation.py +152 -0
- intentkit/skills/openai/gpt_image_to_image.py +186 -0
- intentkit/skills/openai/image_to_text.py +126 -0
- intentkit/skills/openai/openai.png +0 -0
- intentkit/skills/openai/schema.json +139 -0
- intentkit/skills/portfolio/README.md +55 -0
- intentkit/skills/portfolio/__init__.py +151 -0
- intentkit/skills/portfolio/base.py +107 -0
- intentkit/skills/portfolio/constants.py +9 -0
- intentkit/skills/portfolio/moralis.png +0 -0
- intentkit/skills/portfolio/schema.json +237 -0
- intentkit/skills/portfolio/token_balances.py +155 -0
- intentkit/skills/portfolio/wallet_approvals.py +102 -0
- intentkit/skills/portfolio/wallet_defi_positions.py +80 -0
- intentkit/skills/portfolio/wallet_history.py +155 -0
- intentkit/skills/portfolio/wallet_net_worth.py +112 -0
- intentkit/skills/portfolio/wallet_nfts.py +139 -0
- intentkit/skills/portfolio/wallet_profitability.py +101 -0
- intentkit/skills/portfolio/wallet_profitability_summary.py +91 -0
- intentkit/skills/portfolio/wallet_stats.py +79 -0
- intentkit/skills/portfolio/wallet_swaps.py +147 -0
- intentkit/skills/skills.toml +103 -0
- intentkit/skills/slack/__init__.py +98 -0
- intentkit/skills/slack/base.py +55 -0
- intentkit/skills/slack/get_channel.py +109 -0
- intentkit/skills/slack/get_message.py +136 -0
- intentkit/skills/slack/schedule_message.py +92 -0
- intentkit/skills/slack/schema.json +135 -0
- intentkit/skills/slack/send_message.py +81 -0
- intentkit/skills/slack/slack.jpg +0 -0
- intentkit/skills/system/__init__.py +90 -0
- intentkit/skills/system/base.py +22 -0
- intentkit/skills/system/read_agent_api_key.py +87 -0
- intentkit/skills/system/regenerate_agent_api_key.py +77 -0
- intentkit/skills/system/schema.json +53 -0
- intentkit/skills/system/system.svg +76 -0
- intentkit/skills/tavily/README.md +86 -0
- intentkit/skills/tavily/__init__.py +91 -0
- intentkit/skills/tavily/base.py +27 -0
- intentkit/skills/tavily/schema.json +119 -0
- intentkit/skills/tavily/tavily.jpg +0 -0
- intentkit/skills/tavily/tavily_extract.py +147 -0
- intentkit/skills/tavily/tavily_search.py +139 -0
- intentkit/skills/token/README.md +89 -0
- intentkit/skills/token/__init__.py +107 -0
- intentkit/skills/token/base.py +154 -0
- intentkit/skills/token/constants.py +9 -0
- intentkit/skills/token/erc20_transfers.py +145 -0
- intentkit/skills/token/moralis.png +0 -0
- intentkit/skills/token/schema.json +141 -0
- intentkit/skills/token/token_analytics.py +81 -0
- intentkit/skills/token/token_price.py +132 -0
- intentkit/skills/token/token_search.py +121 -0
- intentkit/skills/twitter/__init__.py +146 -0
- intentkit/skills/twitter/base.py +68 -0
- intentkit/skills/twitter/follow_user.py +69 -0
- intentkit/skills/twitter/get_mentions.py +124 -0
- intentkit/skills/twitter/get_timeline.py +111 -0
- intentkit/skills/twitter/get_user_by_username.py +84 -0
- intentkit/skills/twitter/get_user_tweets.py +123 -0
- intentkit/skills/twitter/like_tweet.py +65 -0
- intentkit/skills/twitter/post_tweet.py +90 -0
- intentkit/skills/twitter/reply_tweet.py +98 -0
- intentkit/skills/twitter/retweet.py +76 -0
- intentkit/skills/twitter/schema.json +258 -0
- intentkit/skills/twitter/search_tweets.py +115 -0
- intentkit/skills/twitter/twitter.png +0 -0
- intentkit/skills/unrealspeech/__init__.py +55 -0
- intentkit/skills/unrealspeech/base.py +21 -0
- intentkit/skills/unrealspeech/schema.json +100 -0
- intentkit/skills/unrealspeech/text_to_speech.py +177 -0
- intentkit/skills/unrealspeech/unrealspeech.jpg +0 -0
- intentkit/skills/venice_audio/__init__.py +106 -0
- intentkit/skills/venice_audio/base.py +119 -0
- intentkit/skills/venice_audio/input.py +41 -0
- intentkit/skills/venice_audio/schema.json +152 -0
- intentkit/skills/venice_audio/venice_audio.py +240 -0
- intentkit/skills/venice_audio/venice_logo.jpg +0 -0
- intentkit/skills/venice_image/README.md +119 -0
- intentkit/skills/venice_image/__init__.py +154 -0
- intentkit/skills/venice_image/api.py +138 -0
- intentkit/skills/venice_image/base.py +188 -0
- intentkit/skills/venice_image/config.py +35 -0
- intentkit/skills/venice_image/image_enhance/README.md +119 -0
- intentkit/skills/venice_image/image_enhance/__init__.py +0 -0
- intentkit/skills/venice_image/image_enhance/image_enhance.py +80 -0
- intentkit/skills/venice_image/image_enhance/image_enhance_base.py +23 -0
- intentkit/skills/venice_image/image_enhance/image_enhance_input.py +40 -0
- intentkit/skills/venice_image/image_generation/README.md +144 -0
- intentkit/skills/venice_image/image_generation/__init__.py +0 -0
- intentkit/skills/venice_image/image_generation/image_generation_base.py +117 -0
- intentkit/skills/venice_image/image_generation/image_generation_fluently_xl.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_flux_dev.py +27 -0
- intentkit/skills/venice_image/image_generation/image_generation_flux_dev_uncensored.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_input.py +158 -0
- intentkit/skills/venice_image/image_generation/image_generation_lustify_sdxl.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_pony_realism.py +26 -0
- intentkit/skills/venice_image/image_generation/image_generation_stable_diffusion_3_5.py +28 -0
- intentkit/skills/venice_image/image_generation/image_generation_venice_sd35.py +28 -0
- intentkit/skills/venice_image/image_upscale/README.md +111 -0
- intentkit/skills/venice_image/image_upscale/__init__.py +0 -0
- intentkit/skills/venice_image/image_upscale/image_upscale.py +90 -0
- intentkit/skills/venice_image/image_upscale/image_upscale_base.py +23 -0
- intentkit/skills/venice_image/image_upscale/image_upscale_input.py +22 -0
- intentkit/skills/venice_image/image_vision/README.md +112 -0
- intentkit/skills/venice_image/image_vision/__init__.py +0 -0
- intentkit/skills/venice_image/image_vision/image_vision.py +100 -0
- intentkit/skills/venice_image/image_vision/image_vision_base.py +17 -0
- intentkit/skills/venice_image/image_vision/image_vision_input.py +9 -0
- intentkit/skills/venice_image/schema.json +267 -0
- intentkit/skills/venice_image/utils.py +78 -0
- intentkit/skills/venice_image/venice_image.jpg +0 -0
- intentkit/skills/web_scraper/README.md +82 -0
- intentkit/skills/web_scraper/__init__.py +92 -0
- intentkit/skills/web_scraper/base.py +21 -0
- intentkit/skills/web_scraper/langchain.png +0 -0
- intentkit/skills/web_scraper/schema.json +115 -0
- intentkit/skills/web_scraper/scrape_and_index.py +327 -0
- intentkit/utils/__init__.py +1 -0
- intentkit/utils/chain.py +436 -0
- intentkit/utils/error.py +134 -0
- intentkit/utils/logging.py +70 -0
- intentkit/utils/middleware.py +61 -0
- intentkit/utils/random.py +16 -0
- intentkit/utils/s3.py +267 -0
- intentkit/utils/slack_alert.py +79 -0
- intentkit/utils/tx.py +37 -0
- {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/METADATA +1 -1
- intentkit-0.5.2.dist-info/RECORD +365 -0
- intentkit-0.5.0.dist-info/RECORD +0 -4
- {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/WHEEL +0 -0
- {intentkit-0.5.0.dist-info → intentkit-0.5.2.dist-info}/licenses/LICENSE +0 -0
intentkit/core/credit.py
ADDED
|
@@ -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)
|