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
|
@@ -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
|
+
]
|