intentkit 0.7.5.dev26__py3-none-any.whl → 0.7.5.dev28__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 CHANGED
@@ -3,7 +3,7 @@
3
3
  A powerful platform for building AI agents with blockchain and cryptocurrency capabilities.
4
4
  """
5
5
 
6
- __version__ = "0.7.5-dev26"
6
+ __version__ = "0.7.5-dev28"
7
7
  __author__ = "hyacinthus"
8
8
  __email__ = "hyacinthus@gmail.com"
9
9
 
intentkit/core/agent.py CHANGED
@@ -2,7 +2,7 @@ import logging
2
2
  import time
3
3
  from datetime import datetime, timedelta, timezone
4
4
  from decimal import Decimal
5
- from typing import Any, Dict, List, Optional, Tuple
5
+ from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple
6
6
 
7
7
  from sqlalchemy import func, select, text, update
8
8
 
@@ -17,7 +17,13 @@ from intentkit.models.agent import (
17
17
  AgentUpdate,
18
18
  )
19
19
  from intentkit.models.agent_data import AgentData, AgentQuota, AgentQuotaTable
20
- from intentkit.models.credit import CreditEventTable, EventType, UpstreamType
20
+ from intentkit.models.credit import (
21
+ CreditAccount,
22
+ CreditEventTable,
23
+ EventType,
24
+ OwnerType,
25
+ UpstreamType,
26
+ )
21
27
  from intentkit.models.db import get_session
22
28
  from intentkit.models.skill import (
23
29
  AgentSkillData,
@@ -660,7 +666,31 @@ class AgentStore(SkillStoreABC):
660
666
  agent_store = AgentStore()
661
667
 
662
668
 
663
- async def update_agent_action_cost():
669
+ async def _iterate_agent_id_batches(
670
+ batch_size: int = 100,
671
+ ) -> AsyncGenerator[list[str], None]:
672
+ """Yield agent IDs in ascending batches to limit memory usage."""
673
+
674
+ last_id: Optional[str] = None
675
+ while True:
676
+ async with get_session() as session:
677
+ query = select(AgentTable.id).order_by(AgentTable.id)
678
+
679
+ if last_id:
680
+ query = query.where(AgentTable.id > last_id)
681
+
682
+ query = query.limit(batch_size)
683
+ result = await session.execute(query)
684
+ agent_ids = [row[0] for row in result]
685
+
686
+ if not agent_ids:
687
+ break
688
+
689
+ yield agent_ids
690
+ last_id = agent_ids[-1]
691
+
692
+
693
+ async def update_agent_action_cost(batch_size: int = 100) -> None:
664
694
  """
665
695
  Update action costs for all agents.
666
696
 
@@ -677,42 +707,20 @@ async def update_agent_action_cost():
677
707
  """
678
708
  logger.info("Starting update of agent average action costs")
679
709
  start_time = time.time()
680
- batch_size = 100
681
- last_id = None
682
710
  total_updated = 0
683
711
 
684
- while True:
685
- # Get a batch of agent IDs ordered by ID
686
- async with get_session() as session:
687
- query = select(AgentTable.id).order_by(AgentTable.id)
688
-
689
- # Apply pagination if we have a last_id from previous batch
690
- if last_id:
691
- query = query.where(AgentTable.id > last_id)
692
-
693
- query = query.limit(batch_size)
694
- result = await session.execute(query)
695
- agent_ids = [row[0] for row in result]
696
-
697
- # If no more agents, we're done
698
- if not agent_ids:
699
- break
700
-
701
- # Update last_id for next batch
702
- last_id = agent_ids[-1]
703
-
704
- # Process this batch of agents
712
+ async for agent_ids in _iterate_agent_id_batches(batch_size):
705
713
  logger.info(
706
- f"Processing batch of {len(agent_ids)} agents starting with ID {agent_ids[0]}"
714
+ "Processing batch of %s agents starting with ID %s",
715
+ len(agent_ids),
716
+ agent_ids[0],
707
717
  )
708
718
  batch_start_time = time.time()
709
719
 
710
720
  for agent_id in agent_ids:
711
721
  try:
712
- # Calculate action costs for this agent
713
722
  costs = await agent_action_cost(agent_id)
714
723
 
715
- # Update the agent's quota record
716
724
  async with get_session() as session:
717
725
  update_stmt = (
718
726
  update(AgentQuotaTable)
@@ -730,17 +738,188 @@ async def update_agent_action_cost():
730
738
  await session.commit()
731
739
 
732
740
  total_updated += 1
733
- except Exception as e:
741
+ except Exception as e: # pragma: no cover - log path only
742
+ logger.error(
743
+ "Error updating action costs for agent %s: %s", agent_id, str(e)
744
+ )
745
+
746
+ batch_time = time.time() - batch_start_time
747
+ logger.info("Completed batch in %.3fs", batch_time)
748
+
749
+ total_time = time.time() - start_time
750
+ logger.info(
751
+ "Finished updating action costs for %s agents in %.3fs",
752
+ total_updated,
753
+ total_time,
754
+ )
755
+
756
+
757
+ async def update_agents_account_snapshot(batch_size: int = 100) -> None:
758
+ """Refresh the cached credit account snapshot for every agent."""
759
+
760
+ logger.info("Starting update of agent account snapshots")
761
+ start_time = time.time()
762
+ total_updated = 0
763
+
764
+ async for agent_ids in _iterate_agent_id_batches(batch_size):
765
+ logger.info(
766
+ "Processing snapshot batch of %s agents starting with ID %s",
767
+ len(agent_ids),
768
+ agent_ids[0],
769
+ )
770
+ batch_start_time = time.time()
771
+
772
+ for agent_id in agent_ids:
773
+ try:
774
+ async with get_session() as session:
775
+ account = await CreditAccount.get_or_create_in_session(
776
+ session, OwnerType.AGENT, agent_id
777
+ )
778
+ await session.execute(
779
+ update(AgentTable)
780
+ .where(AgentTable.id == agent_id)
781
+ .values(
782
+ account_snapshot=account.model_dump(mode="json"),
783
+ )
784
+ )
785
+ await session.commit()
786
+
787
+ total_updated += 1
788
+ except Exception as exc: # pragma: no cover - log path only
789
+ logger.error(
790
+ "Error updating account snapshot for agent %s: %s",
791
+ agent_id,
792
+ exc,
793
+ )
794
+
795
+ batch_time = time.time() - batch_start_time
796
+ logger.info("Completed snapshot batch in %.3fs", batch_time)
797
+
798
+ total_time = time.time() - start_time
799
+ logger.info(
800
+ "Finished updating account snapshots for %s agents in %.3fs",
801
+ total_updated,
802
+ total_time,
803
+ )
804
+
805
+
806
+ async def update_agents_assets(batch_size: int = 100) -> None:
807
+ """Refresh cached asset information for all agents."""
808
+
809
+ from intentkit.core.asset import agent_asset
810
+
811
+ logger.info("Starting update of agent assets")
812
+ start_time = time.time()
813
+ total_updated = 0
814
+
815
+ async for agent_ids in _iterate_agent_id_batches(batch_size):
816
+ logger.info(
817
+ "Processing asset batch of %s agents starting with ID %s",
818
+ len(agent_ids),
819
+ agent_ids[0],
820
+ )
821
+ batch_start_time = time.time()
822
+
823
+ for agent_id in agent_ids:
824
+ try:
825
+ assets = await agent_asset(agent_id)
826
+ except IntentKitAPIError as exc: # pragma: no cover - log path only
827
+ logger.warning(
828
+ "Skipping asset update for agent %s due to API error: %s",
829
+ agent_id,
830
+ exc,
831
+ )
832
+ continue
833
+ except Exception as exc: # pragma: no cover - log path only
834
+ logger.error("Error retrieving assets for agent %s: %s", agent_id, exc)
835
+ continue
836
+
837
+ try:
838
+ async with get_session() as session:
839
+ await session.execute(
840
+ update(AgentTable)
841
+ .where(AgentTable.id == agent_id)
842
+ .values(assets=assets.model_dump(mode="json"))
843
+ )
844
+ await session.commit()
845
+
846
+ total_updated += 1
847
+ except Exception as exc: # pragma: no cover - log path only
848
+ logger.error(
849
+ "Error updating asset cache for agent %s: %s", agent_id, exc
850
+ )
851
+
852
+ batch_time = time.time() - batch_start_time
853
+ logger.info("Completed asset batch in %.3fs", batch_time)
854
+
855
+ total_time = time.time() - start_time
856
+ logger.info(
857
+ "Finished updating assets for %s agents in %.3fs",
858
+ total_updated,
859
+ total_time,
860
+ )
861
+
862
+
863
+ async def update_agents_statistics(
864
+ *, end_time: Optional[datetime] = None, batch_size: int = 100
865
+ ) -> None:
866
+ """Refresh cached statistics for every agent."""
867
+
868
+ from intentkit.core.statistics import get_agent_statistics
869
+
870
+ if end_time is None:
871
+ end_time = datetime.now(timezone.utc)
872
+ elif end_time.tzinfo is None:
873
+ end_time = end_time.replace(tzinfo=timezone.utc)
874
+ else:
875
+ end_time = end_time.astimezone(timezone.utc)
876
+
877
+ logger.info("Starting update of agent statistics using end_time %s", end_time)
878
+ start_time = time.time()
879
+ total_updated = 0
880
+
881
+ async for agent_ids in _iterate_agent_id_batches(batch_size):
882
+ logger.info(
883
+ "Processing statistics batch of %s agents starting with ID %s",
884
+ len(agent_ids),
885
+ agent_ids[0],
886
+ )
887
+ batch_start_time = time.time()
888
+
889
+ for agent_id in agent_ids:
890
+ try:
891
+ statistics = await get_agent_statistics(agent_id, end_time=end_time)
892
+ except Exception as exc: # pragma: no cover - log path only
893
+ logger.error(
894
+ "Error computing statistics for agent %s: %s", agent_id, exc
895
+ )
896
+ continue
897
+
898
+ try:
899
+ async with get_session() as session:
900
+ await session.execute(
901
+ update(AgentTable)
902
+ .where(AgentTable.id == agent_id)
903
+ .values(statistics=statistics.model_dump(mode="json"))
904
+ )
905
+ await session.commit()
906
+
907
+ total_updated += 1
908
+ except Exception as exc: # pragma: no cover - log path only
734
909
  logger.error(
735
- f"Error updating action costs for agent {agent_id}: {str(e)}"
910
+ "Error updating statistics cache for agent %s: %s",
911
+ agent_id,
912
+ exc,
736
913
  )
737
914
 
738
915
  batch_time = time.time() - batch_start_time
739
- logger.info(f"Completed batch in {batch_time:.3f}s")
916
+ logger.info("Completed statistics batch in %.3fs", batch_time)
740
917
 
741
918
  total_time = time.time() - start_time
742
919
  logger.info(
743
- f"Finished updating action costs for {total_updated} agents in {total_time:.3f}s"
920
+ "Finished updating statistics for %s agents in %.3fs",
921
+ total_updated,
922
+ total_time,
744
923
  )
745
924
 
746
925
 
@@ -0,0 +1,208 @@
1
+ """Shared asset retrieval utilities for agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from decimal import Decimal
7
+ from typing import Optional
8
+
9
+ import httpx
10
+ from pydantic import BaseModel, Field
11
+ from web3 import Web3
12
+
13
+ from intentkit.clients.web3 import get_web3_client
14
+ from intentkit.config.config import config
15
+ from intentkit.core.agent import agent_store
16
+ from intentkit.models.agent import Agent
17
+ from intentkit.models.agent_data import AgentData
18
+ from intentkit.utils.error import IntentKitAPIError
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # USDC contract addresses for different networks
23
+ USDC_ADDRESSES = {
24
+ "base-mainnet": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
25
+ "ethereum-mainnet": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
26
+ "arbitrum-mainnet": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
27
+ "optimism-mainnet": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
28
+ "polygon-mainnet": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
29
+ }
30
+
31
+ # NATION token address for base-mainnet
32
+ NATION_ADDRESS = "0x2f74f818e81685c8086dd783837a4605a90474b8"
33
+
34
+
35
+ class Asset(BaseModel):
36
+ """Model for individual asset with symbol and balance."""
37
+
38
+ symbol: str = Field(description="Asset symbol (e.g., ETH, USDC, NATION)")
39
+ balance: Decimal = Field(description="Asset balance as decimal")
40
+
41
+
42
+ class AgentAssets(BaseModel):
43
+ """Simplified agent asset response with wallet net worth and tokens."""
44
+
45
+ net_worth: str = Field(description="Total wallet net worth in USD")
46
+ tokens: list[Asset] = Field(description="List of assets with symbol and balance")
47
+
48
+
49
+ async def _get_token_balance(
50
+ web3_client: Web3, wallet_address: str, token_address: str
51
+ ) -> Decimal:
52
+ """Get ERC-20 token balance for a wallet address."""
53
+ try:
54
+ # ERC-20 standard ABI for balanceOf and decimals
55
+ erc20_abi = [
56
+ {
57
+ "constant": True,
58
+ "inputs": [{"name": "_owner", "type": "address"}],
59
+ "name": "balanceOf",
60
+ "outputs": [{"name": "balance", "type": "uint256"}],
61
+ "type": "function",
62
+ },
63
+ {
64
+ "constant": True,
65
+ "inputs": [],
66
+ "name": "decimals",
67
+ "outputs": [{"name": "", "type": "uint8"}],
68
+ "type": "function",
69
+ },
70
+ ]
71
+
72
+ contract = web3_client.eth.contract(
73
+ address=web3_client.to_checksum_address(token_address), abi=erc20_abi
74
+ )
75
+
76
+ balance_wei = contract.functions.balanceOf(
77
+ web3_client.to_checksum_address(wallet_address)
78
+ ).call()
79
+ decimals = contract.functions.decimals().call()
80
+
81
+ # Convert from wei to token units using actual decimals
82
+ balance = Decimal(balance_wei) / Decimal(10**decimals)
83
+ return balance
84
+ except Exception as exc: # pragma: no cover - log path only
85
+ logger.error("Error getting token balance: %s", exc)
86
+ return Decimal("0")
87
+
88
+
89
+ async def _get_eth_balance(web3_client: Web3, wallet_address: str) -> Decimal:
90
+ """Get ETH balance for a wallet address."""
91
+ try:
92
+ balance_wei = web3_client.eth.get_balance(
93
+ web3_client.to_checksum_address(wallet_address)
94
+ )
95
+ balance = Decimal(balance_wei) / Decimal(10**18)
96
+ return balance
97
+ except Exception as exc: # pragma: no cover - log path only
98
+ logger.error("Error getting ETH balance: %s", exc)
99
+ return Decimal("0")
100
+
101
+
102
+ async def _get_wallet_net_worth(wallet_address: str) -> str:
103
+ """Get wallet net worth using Moralis API."""
104
+ try:
105
+ async with httpx.AsyncClient() as client:
106
+ url = (
107
+ "https://deep-index.moralis.io/api/v2.2/wallets/"
108
+ f"{wallet_address}/net-worth"
109
+ )
110
+ headers = {
111
+ "accept": "application/json",
112
+ "X-API-Key": config.moralis_api_key,
113
+ }
114
+ params = {
115
+ "exclude_spam": "true",
116
+ "exclude_unverified_contracts": "true",
117
+ "chains": ["eth", "base", "polygon", "arbitrum", "optimism"],
118
+ }
119
+
120
+ response = await client.get(url, headers=headers, params=params)
121
+ response.raise_for_status()
122
+ data = response.json()
123
+ return data.get("total_networth_usd", "0")
124
+ except Exception as exc: # pragma: no cover - log path only
125
+ logger.error("Error getting wallet net worth for %s: %s", wallet_address, exc)
126
+ return "0"
127
+
128
+
129
+ async def _build_assets_list(
130
+ agent: Agent, agent_data: AgentData, web3_client: Web3
131
+ ) -> list[Asset]:
132
+ """Build the assets list based on network conditions and agent configuration."""
133
+ assets: list[Asset] = []
134
+
135
+ if not agent_data or not agent_data.evm_wallet_address:
136
+ return assets
137
+
138
+ wallet_address = agent_data.evm_wallet_address
139
+ network_id: Optional[str] = agent.network_id
140
+
141
+ # ETH is always included
142
+ eth_balance = await _get_eth_balance(web3_client, wallet_address)
143
+ assets.append(Asset(symbol="ETH", balance=eth_balance))
144
+
145
+ if network_id and network_id.endswith("-mainnet"):
146
+ usdc_address = USDC_ADDRESSES.get(str(network_id))
147
+ if usdc_address:
148
+ usdc_balance = await _get_token_balance(
149
+ web3_client, wallet_address, usdc_address
150
+ )
151
+ assets.append(Asset(symbol="USDC", balance=usdc_balance))
152
+
153
+ if network_id == "base-mainnet":
154
+ nation_balance = await _get_token_balance(
155
+ web3_client, wallet_address, NATION_ADDRESS
156
+ )
157
+ assets.append(Asset(symbol="NATION", balance=nation_balance))
158
+
159
+ if agent.ticker and agent.token_address:
160
+ lower_addresses = [addr.lower() for addr in USDC_ADDRESSES.values()]
161
+ is_usdc = agent.token_address.lower() in lower_addresses
162
+ is_nation = agent.token_address.lower() == NATION_ADDRESS.lower()
163
+
164
+ if not is_usdc and not is_nation:
165
+ custom_balance = await _get_token_balance(
166
+ web3_client, wallet_address, agent.token_address
167
+ )
168
+ assets.append(Asset(symbol=agent.ticker, balance=custom_balance))
169
+
170
+ return assets
171
+
172
+
173
+ async def agent_asset(agent_id: str) -> AgentAssets:
174
+ """Fetch wallet net worth and token balances for an agent."""
175
+ agent = await Agent.get(agent_id)
176
+ if not agent:
177
+ raise IntentKitAPIError(404, "AgentNotFound", "Agent not found")
178
+
179
+ agent_data = await AgentData.get(agent_id)
180
+ if not agent_data or not agent_data.evm_wallet_address:
181
+ return AgentAssets(net_worth="0", tokens=[])
182
+
183
+ if not agent.network_id:
184
+ return AgentAssets(net_worth="0", tokens=[])
185
+
186
+ try:
187
+ web3_client = get_web3_client(str(agent.network_id), agent_store)
188
+ tokens = await _build_assets_list(agent, agent_data, web3_client)
189
+ net_worth = await _get_wallet_net_worth(agent_data.evm_wallet_address)
190
+ return AgentAssets(net_worth=net_worth, tokens=tokens)
191
+ except IntentKitAPIError:
192
+ raise
193
+ except Exception as exc:
194
+ logger.error("Error getting agent assets for %s: %s", agent_id, exc)
195
+ raise IntentKitAPIError(
196
+ 500, "AgentAssetError", "Failed to retrieve agent assets"
197
+ ) from exc
198
+
199
+
200
+ __all__ = [
201
+ "Asset",
202
+ "AgentAssets",
203
+ "USDC_ADDRESSES",
204
+ "NATION_ADDRESS",
205
+ "agent_asset",
206
+ "_build_assets_list",
207
+ "_get_wallet_net_worth",
208
+ ]
@@ -0,0 +1,169 @@
1
+ """Agent statistics utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta, timezone
6
+ from decimal import Decimal
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+ from sqlalchemy import func, select
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+
13
+ from intentkit.models.agent_data import AgentQuota, AgentQuotaTable
14
+ from intentkit.models.credit import CreditAccount, CreditEventTable, OwnerType
15
+ from intentkit.models.db import get_session
16
+
17
+
18
+ class AgentStatistics(BaseModel):
19
+ """Aggregated statistics for an agent credit account."""
20
+
21
+ agent_id: str = Field(description="ID of the agent")
22
+ account_id: str = Field(description="ID of the associated credit account")
23
+ balance: Decimal = Field(description="Current credit account balance")
24
+ total_income: Decimal = Field(description="Total income across all events")
25
+ net_income: Decimal = Field(description="Net income from fee allocations")
26
+ permanent_income: Decimal = Field(
27
+ description="Total permanent income across all events"
28
+ )
29
+ permanent_profit: Decimal = Field(
30
+ description="Permanent profit allocated to the agent"
31
+ )
32
+ last_24h_income: Decimal = Field(
33
+ description="Income generated during the last 24 hours"
34
+ )
35
+ last_24h_permanent_income: Decimal = Field(
36
+ description="Permanent income generated during the last 24 hours"
37
+ )
38
+ avg_action_cost: Decimal = Field(description="Average action cost")
39
+ min_action_cost: Decimal = Field(description="Minimum action cost")
40
+ max_action_cost: Decimal = Field(description="Maximum action cost")
41
+ low_action_cost: Decimal = Field(description="20th percentile action cost")
42
+ medium_action_cost: Decimal = Field(description="60th percentile action cost")
43
+ high_action_cost: Decimal = Field(description="80th percentile action cost")
44
+
45
+
46
+ async def get_agent_statistics(
47
+ agent_id: str,
48
+ *,
49
+ end_time: Optional[datetime] = None,
50
+ session: Optional[AsyncSession] = None,
51
+ ) -> AgentStatistics:
52
+ """Calculate statistics for an agent credit account.
53
+
54
+ Args:
55
+ agent_id: ID of the agent.
56
+ end_time: Optional end time used as the inclusive boundary for
57
+ time-windowed aggregations. Defaults to the current UTC time.
58
+ session: Optional database session to reuse. When omitted, a
59
+ standalone session will be created and committed automatically.
60
+
61
+ Returns:
62
+ Aggregated statistics for the agent.
63
+ """
64
+
65
+ managed_session = session is None
66
+ if end_time is None:
67
+ end_time = datetime.now(timezone.utc)
68
+ elif end_time.tzinfo is None:
69
+ end_time = end_time.replace(tzinfo=timezone.utc)
70
+ else:
71
+ end_time = end_time.astimezone(timezone.utc)
72
+
73
+ async def _compute(session: AsyncSession) -> AgentStatistics:
74
+ account = await CreditAccount.get_or_create_in_session(
75
+ session, OwnerType.AGENT, agent_id
76
+ )
77
+ balance = account.free_credits + account.reward_credits + account.credits
78
+
79
+ totals_stmt = select(
80
+ func.sum(CreditEventTable.total_amount).label("total_income"),
81
+ func.sum(CreditEventTable.fee_agent_amount).label("net_income"),
82
+ func.sum(CreditEventTable.permanent_amount).label("permanent_income"),
83
+ func.sum(CreditEventTable.fee_agent_permanent_amount).label(
84
+ "permanent_profit"
85
+ ),
86
+ ).where(CreditEventTable.agent_id == agent_id)
87
+ totals_result = await session.execute(totals_stmt)
88
+ totals_row = totals_result.first()
89
+
90
+ total_income = (
91
+ totals_row.total_income
92
+ if totals_row and totals_row.total_income
93
+ else Decimal("0")
94
+ )
95
+ net_income = (
96
+ totals_row.net_income
97
+ if totals_row and totals_row.net_income
98
+ else Decimal("0")
99
+ )
100
+ permanent_income = (
101
+ totals_row.permanent_income
102
+ if totals_row and totals_row.permanent_income
103
+ else Decimal("0")
104
+ )
105
+ permanent_profit = (
106
+ totals_row.permanent_profit
107
+ if totals_row and totals_row.permanent_profit
108
+ else Decimal("0")
109
+ )
110
+
111
+ window_start = end_time - timedelta(hours=24)
112
+ window_stmt = select(
113
+ func.sum(CreditEventTable.total_amount).label("last_24h_income"),
114
+ func.sum(CreditEventTable.permanent_amount).label(
115
+ "last_24h_permanent_income"
116
+ ),
117
+ ).where(
118
+ CreditEventTable.agent_id == agent_id,
119
+ CreditEventTable.created_at >= window_start,
120
+ CreditEventTable.created_at <= end_time,
121
+ )
122
+ window_result = await session.execute(window_stmt)
123
+ window_row = window_result.first()
124
+
125
+ last_24h_income = (
126
+ window_row.last_24h_income
127
+ if window_row and window_row.last_24h_income
128
+ else Decimal("0")
129
+ )
130
+ last_24h_permanent_income = (
131
+ window_row.last_24h_permanent_income
132
+ if window_row and window_row.last_24h_permanent_income
133
+ else Decimal("0")
134
+ )
135
+
136
+ quota_row = await session.get(AgentQuotaTable, agent_id)
137
+ quota = (
138
+ AgentQuota.model_validate(quota_row)
139
+ if quota_row
140
+ else AgentQuota(id=agent_id)
141
+ )
142
+
143
+ return AgentStatistics(
144
+ agent_id=agent_id,
145
+ account_id=account.id,
146
+ balance=balance,
147
+ total_income=total_income,
148
+ net_income=net_income,
149
+ permanent_income=permanent_income,
150
+ permanent_profit=permanent_profit,
151
+ last_24h_income=last_24h_income,
152
+ last_24h_permanent_income=last_24h_permanent_income,
153
+ avg_action_cost=quota.avg_action_cost,
154
+ min_action_cost=quota.min_action_cost,
155
+ max_action_cost=quota.max_action_cost,
156
+ low_action_cost=quota.low_action_cost,
157
+ medium_action_cost=quota.medium_action_cost,
158
+ high_action_cost=quota.high_action_cost,
159
+ )
160
+
161
+ if managed_session:
162
+ async with get_session() as managed:
163
+ statistics = await _compute(managed)
164
+ await managed.commit()
165
+ return statistics
166
+ return await _compute(session)
167
+
168
+
169
+ __all__ = ["AgentStatistics", "get_agent_statistics"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intentkit
3
- Version: 0.7.5.dev26
3
+ Version: 0.7.5.dev28
4
4
  Summary: Intent-based AI Agent Platform - Core Package
5
5
  Project-URL: Homepage, https://github.com/crestalnetwork/intentkit
6
6
  Project-URL: Repository, https://github.com/crestalnetwork/intentkit
@@ -1,4 +1,4 @@
1
- intentkit/__init__.py,sha256=4rN9u7UCNJjYV4LQCR8urA6S1ezNFBSDgvOl-nbSg-A,384
1
+ intentkit/__init__.py,sha256=TLm-2HN623h5B0LDpMvBIq2Rf_D9u77MemTU2iwIQ0g,384
2
2
  intentkit/abstracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  intentkit/abstracts/agent.py,sha256=108gb5W8Q1Sy4G55F2_ZFv2-_CnY76qrBtpIr0Oxxqk,1489
4
4
  intentkit/abstracts/api.py,sha256=ZUc24vaQvQVbbjznx7bV0lbbQxdQPfEV8ZxM2R6wZWo,166
@@ -13,14 +13,16 @@ intentkit/clients/web3.py,sha256=A-w4vBPXHpDh8svsEFj_LkmvRgoDTZw4E-84S-UC9ws,102
13
13
  intentkit/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  intentkit/config/config.py,sha256=kw9F-uLsJd-knCKmYNb-hqR7x7HUXUkNg5FZCgOPH54,8868
15
15
  intentkit/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- intentkit/core/agent.py,sha256=7WTUouAV3uLvsJxHq4JziCMO8YdYeVujtmc--9L5ZJc,31981
16
+ intentkit/core/agent.py,sha256=YeeaqSMHIRAWUihL4OsWwrbZy8QbV26ORddMgPtRqMc,37932
17
17
  intentkit/core/api.py,sha256=WfoaHNquujYJIpNPuTR1dSaaxog0S3X2W4lG9Ehmkm4,3284
18
+ intentkit/core/asset.py,sha256=mswjgAhSkAzdkz8VlFCWFMrE4Di5R3tZlkHtC0Rt4D0,7397
18
19
  intentkit/core/chat.py,sha256=YN20CnDazWLjiOZFOHgV6uHmA2DKkvPCsD5Q5sfNcZg,1685
19
20
  intentkit/core/client.py,sha256=J5K7f08-ucszBKAbn9K3QNOFKIC__7amTbKYii1jFkI,3056
20
21
  intentkit/core/credit.py,sha256=b4f4T6G6eeBTMe0L_r8awWtXgUnqiog4IUaymDPYym0,75587
21
22
  intentkit/core/engine.py,sha256=8-dlYnwg8BIViAWIAFj65s3O29EXCeQlTjf5Yg9Xfww,37597
22
23
  intentkit/core/node.py,sha256=Cjekrg5RFtNNj3k_pLAWTZVGzm33Wnn2JtE__RSNMA8,8880
23
24
  intentkit/core/prompt.py,sha256=idNx1ono4Maz2i6IBKfaKOBBbEQiWbaSxr2Eb1vZTI4,15482
25
+ intentkit/core/statistics.py,sha256=-IZmxIBzyzZuai7QyfPEY1tx8Q8ydmmcm6eqbSSy_6o,6366
24
26
  intentkit/models/agent.py,sha256=-nRcaH5b-iMyds02KvjOlogHAfOC5X184qJZj_Gung4,69074
25
27
  intentkit/models/agent_data.py,sha256=5zq3EPKnygT2P1OHc2IfEmL8hXkjeBND6sJ0JJsvQJg,28370
26
28
  intentkit/models/agent_public.json,sha256=0X8Bd2WOobDJLsok8avWNzmzu4uvKSGEyy6Myn53eT4,2802
@@ -270,8 +272,6 @@ intentkit/skills/moralis/fetch_solana_portfolio.py,sha256=rc6uqqt6_13VoveNf1mxnC
270
272
  intentkit/skills/moralis/fetch_wallet_portfolio.py,sha256=tujwVQklkkaDTnLj6Ce-hUybg_0gWr-GLTaocEmzNA4,11166
271
273
  intentkit/skills/moralis/moralis.png,sha256=fUm771g9SmL3SO2m0yeEAzrnT0C_scj_r9rowCvFiVo,11232
272
274
  intentkit/skills/moralis/schema.json,sha256=jpD66pyw1LtUXejAjWHNJy_FGXeHK-YRlRMtvFIA33g,4734
273
- intentkit/skills/moralis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
274
- intentkit/skills/moralis/tests/test_wallet.py,sha256=MI4WwFNBoILDEXlK22glntiySgR9BWkpHUdIfKUu2-A,18610
275
275
  intentkit/skills/morpho/__init__.py,sha256=03BU7URDdBCu7PEl9JNvZ1JJ0GSy3Xk7tiQJzJuNc84,1482
276
276
  intentkit/skills/morpho/base.py,sha256=FYbgul_35OX-oHwTqA8Jb99V9azM1AVcsUqGScOvQAg,238
277
277
  intentkit/skills/morpho/morpho.svg,sha256=v3snRB2CkMbaw36YbdmNRuptu5I07O-6WUw__Htciv8,602
@@ -450,7 +450,7 @@ intentkit/utils/random.py,sha256=DymMxu9g0kuQLgJUqalvgksnIeLdS-v0aRk5nQU0mLI,452
450
450
  intentkit/utils/s3.py,sha256=A8Nsx5QJyLsxhj9g7oHNy2-m24tjQUhC9URm8Qb1jFw,10057
451
451
  intentkit/utils/slack_alert.py,sha256=s7UpRgyzLW7Pbmt8cKzTJgMA9bm4EP-1rQ5KXayHu6E,2264
452
452
  intentkit/utils/tx.py,sha256=2yLLGuhvfBEY5n_GJ8wmIWLCzn0FsYKv5kRNzw_sLUI,1454
453
- intentkit-0.7.5.dev26.dist-info/METADATA,sha256=Jq7PY9gUq6oZwLYs5QzItmy3Jeqj9rJEgRmgWxlriBA,6360
454
- intentkit-0.7.5.dev26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
455
- intentkit-0.7.5.dev26.dist-info/licenses/LICENSE,sha256=Bln6DhK-LtcO4aXy-PBcdZv2f24MlJFm_qn222biJtE,1071
456
- intentkit-0.7.5.dev26.dist-info/RECORD,,
453
+ intentkit-0.7.5.dev28.dist-info/METADATA,sha256=0o_TegRevOjTNiaoB8xrvgkSa3RLiOiWRulXkbrRr0Q,6360
454
+ intentkit-0.7.5.dev28.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
455
+ intentkit-0.7.5.dev28.dist-info/licenses/LICENSE,sha256=Bln6DhK-LtcO4aXy-PBcdZv2f24MlJFm_qn222biJtE,1071
456
+ intentkit-0.7.5.dev28.dist-info/RECORD,,
File without changes
@@ -1,511 +0,0 @@
1
- """Tests for the Moralis Wallet Portfolio skills."""
2
-
3
- import asyncio
4
- import json
5
- import unittest
6
- from unittest.mock import AsyncMock, MagicMock, patch
7
-
8
- from intentkit.skills.moralis import (
9
- FetchChainPortfolio,
10
- FetchSolanaPortfolio,
11
- FetchWalletPortfolio,
12
- get_skills,
13
- )
14
- from intentkit.skills.moralis.api import (
15
- fetch_moralis_data,
16
- fetch_wallet_balances,
17
- get_solana_portfolio,
18
- )
19
- from intentkit.skills.moralis.base import WalletBaseTool
20
-
21
-
22
- class DummyResponse:
23
- """Mock HTTP response for testing."""
24
-
25
- def __init__(self, status_code, json_data):
26
- self.status_code = status_code
27
- self._json_data = json_data
28
- self.text = json.dumps(json_data) if json_data else ""
29
-
30
- def json(self):
31
- return self._json_data
32
-
33
- async def raise_for_status(self):
34
- if self.status_code >= 400:
35
- raise Exception(f"HTTP Error: {self.status_code}")
36
-
37
-
38
- class TestWalletBaseClass(unittest.TestCase):
39
- """Test the base wallet portfolio tool class."""
40
-
41
- def setUp(self):
42
- self.loop = asyncio.new_event_loop()
43
- asyncio.set_event_loop(self.loop)
44
-
45
- self.mock_skill_store = MagicMock()
46
-
47
- def tearDown(self):
48
- self.loop.close()
49
-
50
- def test_base_class_init(self):
51
- """Test base class initialization."""
52
-
53
- # Create a concrete subclass for testing
54
- class TestTool(WalletBaseTool):
55
- async def _arun(self, *args, **kwargs):
56
- return "test"
57
-
58
- tool = TestTool(
59
- name="test_tool",
60
- description="Test tool",
61
- args_schema=MagicMock(),
62
- api_key="test_key",
63
- skill_store=self.mock_skill_store,
64
- agent_id="test_agent",
65
- )
66
-
67
- self.assertEqual(tool.api_key, "test_key")
68
- self.assertEqual(tool.agent_id, "test_agent")
69
- self.assertEqual(tool.skill_store, self.mock_skill_store)
70
- self.assertEqual(tool.category, "moralis")
71
-
72
- def test_get_chain_name(self):
73
- """Test chain name conversion."""
74
-
75
- class TestTool(WalletBaseTool):
76
- async def _arun(self, *args, **kwargs):
77
- return "test"
78
-
79
- tool = TestTool(
80
- name="test_tool",
81
- description="Test tool",
82
- args_schema=MagicMock(),
83
- api_key="test_key",
84
- skill_store=self.mock_skill_store,
85
- agent_id="test_agent",
86
- )
87
-
88
- # Test with known chain IDs
89
- self.assertEqual(tool._get_chain_name(1), "eth")
90
- self.assertEqual(tool._get_chain_name(56), "bsc")
91
- self.assertEqual(tool._get_chain_name(137), "polygon")
92
-
93
- # Test with unknown chain ID
94
- self.assertEqual(tool._get_chain_name(999999), "eth")
95
-
96
-
97
- class TestAPIFunctions(unittest.IsolatedAsyncioTestCase):
98
- """Test the API interaction functions."""
99
-
100
- async def test_fetch_moralis_data(self):
101
- """Test the base Moralis API function."""
102
- with patch("httpx.AsyncClient") as MockClient:
103
- client_instance = AsyncMock()
104
- client_instance.get.return_value = DummyResponse(
105
- 200, {"success": True, "data": "test_data"}
106
- )
107
- MockClient.return_value.__aenter__.return_value = client_instance
108
-
109
- result = await fetch_moralis_data(
110
- "test_api_key", "test_endpoint", "0xAddress", 1
111
- )
112
-
113
- self.assertEqual(result, {"success": True, "data": "test_data"})
114
-
115
- # Test error handling
116
- client_instance.get.return_value = DummyResponse(404, None)
117
- client_instance.get.return_value.raise_for_status = AsyncMock(
118
- side_effect=Exception("HTTP error 404")
119
- )
120
-
121
- result = await fetch_moralis_data(
122
- "test_api_key", "test_endpoint", "0xAddress", 1
123
- )
124
- self.assertIn("error", result)
125
-
126
- async def test_fetch_wallet_balances(self):
127
- """Test fetching wallet balances."""
128
- with patch("skills.moralis.api.fetch_moralis_data") as mock_fetch:
129
- mock_fetch.return_value = {
130
- "result": [
131
- {
132
- "token_address": "0x123",
133
- "symbol": "TEST",
134
- "balance": "1000000",
135
- "usd_value": 100,
136
- }
137
- ]
138
- }
139
-
140
- result = await fetch_wallet_balances("test_api_key", "0xAddress", 1)
141
-
142
- self.assertEqual(result["result"][0]["symbol"], "TEST")
143
- mock_fetch.assert_called_once_with(
144
- "test_api_key", "wallets/{address}/tokens", "0xAddress", 1, None
145
- )
146
-
147
- async def test_get_solana_portfolio(self):
148
- """Test getting Solana portfolio."""
149
- with patch("skills.moralis.api.fetch_solana_api") as mock_fetch:
150
- mock_fetch.return_value = {
151
- "nativeBalance": {"solana": 1.5, "lamports": 1500000000},
152
- "tokens": [
153
- {
154
- "symbol": "TEST",
155
- "name": "Test Token",
156
- "mint": "TokenMintAddress",
157
- "associatedTokenAddress": "AssocTokenAddress",
158
- "amount": 10,
159
- "decimals": 9,
160
- "amountRaw": "10000000000",
161
- }
162
- ],
163
- }
164
-
165
- result = await get_solana_portfolio("test_api_key", "SolAddress", "mainnet")
166
-
167
- mock_fetch.assert_called_once_with(
168
- "test_api_key", "/account/mainnet/SolAddress/portfolio"
169
- )
170
- self.assertEqual(result["nativeBalance"]["solana"], 1.5)
171
- self.assertEqual(len(result["tokens"]), 1)
172
- self.assertEqual(result["tokens"][0]["symbol"], "TEST")
173
-
174
-
175
- class TestFetchWalletPortfolio(unittest.IsolatedAsyncioTestCase):
176
- """Test the FetchWalletPortfolio skill."""
177
-
178
- async def test_wallet_portfolio_success(self):
179
- """Test successful wallet portfolio fetch."""
180
- mock_skill_store = MagicMock()
181
-
182
- with (
183
- patch(
184
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_wallet_balances"
185
- ) as mock_balances,
186
- patch(
187
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_net_worth"
188
- ) as mock_net_worth,
189
- ):
190
- # Mock successful responses
191
- mock_balances.return_value = {
192
- "result": [
193
- {
194
- "token_address": "0x123",
195
- "symbol": "TEST",
196
- "name": "Test Token",
197
- "balance": "1000000000000000000",
198
- "balance_formatted": "1.0",
199
- "usd_value": 100,
200
- }
201
- ]
202
- }
203
- mock_net_worth.return_value = {"result": {"total_networth_usd": 1000}}
204
-
205
- tool = FetchWalletPortfolio(
206
- name="fetch_wallet_portfolio",
207
- description="Test description",
208
- args_schema=MagicMock(),
209
- api_key="test_key",
210
- skill_store=mock_skill_store,
211
- agent_id="test_agent",
212
- )
213
-
214
- result = await tool._arun(address="0xAddress")
215
-
216
- self.assertEqual(result.address, "0xAddress")
217
- self.assertEqual(result.total_net_worth, 1000)
218
- self.assertEqual(len(result.tokens), 1)
219
- self.assertEqual(result.tokens[0].symbol, "TEST")
220
-
221
- async def test_wallet_portfolio_with_solana(self):
222
- """Test wallet portfolio with Solana support."""
223
- mock_skill_store = MagicMock()
224
-
225
- with (
226
- patch(
227
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_wallet_balances"
228
- ) as mock_evm_balances,
229
- patch(
230
- "skills.moralis.moralis_fetch_wallet_portfolio.fetch_net_worth"
231
- ) as mock_net_worth,
232
- patch(
233
- "skills.moralis.moralis_fetch_wallet_portfolio.get_solana_portfolio"
234
- ) as mock_sol_portfolio,
235
- patch(
236
- "skills.moralis.moralis_fetch_wallet_portfolio.get_token_price"
237
- ) as mock_token_price,
238
- ):
239
- # Mock EVM responses
240
- mock_evm_balances.return_value = {
241
- "result": [
242
- {
243
- "token_address": "0x123",
244
- "symbol": "ETH",
245
- "name": "Ethereum",
246
- "balance": "1000000000000000000",
247
- "balance_formatted": "1.0",
248
- "usd_value": 2000,
249
- }
250
- ]
251
- }
252
- mock_net_worth.return_value = {"result": {"total_networth_usd": 3000}}
253
-
254
- # Mock Solana responses
255
- mock_sol_portfolio.return_value = {
256
- "nativeBalance": {"solana": 2.0, "lamports": 2000000000},
257
- "tokens": [
258
- {
259
- "symbol": "SOL",
260
- "name": "Solana",
261
- "mint": "So11111111111111111111111111111111111111112",
262
- "associatedTokenAddress": "AssocTokenAddress",
263
- "amount": 2.0,
264
- "decimals": 9,
265
- "amountRaw": "2000000000",
266
- }
267
- ],
268
- }
269
-
270
- mock_token_price.return_value = {"usdPrice": 500}
271
-
272
- tool = FetchWalletPortfolio(
273
- name="fetch_wallet_portfolio",
274
- description="Test description",
275
- args_schema=MagicMock(),
276
- api_key="test_key",
277
- skill_store=mock_skill_store,
278
- agent_id="test_agent",
279
- )
280
-
281
- result = await tool._arun(address="0xAddress", include_solana=True)
282
-
283
- self.assertEqual(result.address, "0xAddress")
284
- self.assertEqual(
285
- result.total_net_worth, 3000
286
- ) # Using the net worth from mock
287
- self.assertIn("eth", result.chains)
288
- self.assertIn("solana", result.chains)
289
-
290
- # Check that we have both EVM and Solana tokens
291
- token_symbols = [token.symbol for token in result.tokens]
292
- self.assertIn("ETH", token_symbols)
293
- self.assertIn("SOL", token_symbols)
294
-
295
-
296
- class TestFetchSolanaPortfolio(unittest.IsolatedAsyncioTestCase):
297
- """Test the FetchSolanaPortfolio skill."""
298
-
299
- async def test_solana_portfolio_success(self):
300
- """Test successful Solana portfolio fetch."""
301
- mock_skill_store = MagicMock()
302
-
303
- with (
304
- patch(
305
- "skills.moralis.moralis_fetch_solana_portfolio.get_solana_portfolio"
306
- ) as mock_portfolio,
307
- patch(
308
- "skills.moralis.moralis_fetch_solana_portfolio.get_solana_nfts"
309
- ) as mock_nfts,
310
- patch(
311
- "skills.moralis.moralis_fetch_solana_portfolio.get_token_price"
312
- ) as mock_token_price,
313
- ):
314
- # Mock successful responses
315
- mock_portfolio.return_value = {
316
- "nativeBalance": {"solana": 1.5, "lamports": 1500000000},
317
- "tokens": [
318
- {
319
- "symbol": "TEST",
320
- "name": "Test Token",
321
- "mint": "TokenMintAddress",
322
- "associatedTokenAddress": "AssocTokenAddress",
323
- "amount": 10,
324
- "decimals": 9,
325
- "amountRaw": "10000000000",
326
- }
327
- ],
328
- }
329
-
330
- mock_nfts.return_value = [
331
- {
332
- "mint": "NFTMintAddress",
333
- "name": "Test NFT",
334
- "symbol": "TNFT",
335
- "associatedTokenAddress": "AssocTokenAddress",
336
- "metadata": {"name": "Test NFT", "image": "image.png"},
337
- }
338
- ]
339
-
340
- mock_token_price.return_value = {"usdPrice": 25}
341
-
342
- tool = FetchSolanaPortfolio(
343
- name="fetch_solana_portfolio",
344
- description="Test description",
345
- args_schema=MagicMock(),
346
- api_key="test_key",
347
- skill_store=mock_skill_store,
348
- agent_id="test_agent",
349
- )
350
-
351
- result = await tool._arun(address="SolanaAddress", include_nfts=True)
352
-
353
- self.assertEqual(result.address, "SolanaAddress")
354
- self.assertEqual(result.sol_balance, 1.5)
355
- self.assertEqual(len(result.tokens), 1)
356
- self.assertEqual(result.tokens[0].token_info.symbol, "TEST")
357
- self.assertEqual(len(result.nfts), 1)
358
- self.assertEqual(result.nfts[0].name, "Test NFT")
359
- self.assertEqual(result.sol_price_usd, 25)
360
- self.assertEqual(result.sol_value_usd, 37.5) # 1.5 SOL * $25
361
-
362
-
363
- class TestFetchChainPortfolio(unittest.IsolatedAsyncioTestCase):
364
- """Test the FetchChainPortfolio skill."""
365
-
366
- async def test_chain_portfolio_success(self):
367
- """Test successful chain portfolio fetch."""
368
- mock_skill_store = MagicMock()
369
-
370
- with patch(
371
- "skills.moralis.moralis_fetch_chain_portfolio.fetch_wallet_balances"
372
- ) as mock_balances:
373
- # Mock successful responses
374
- mock_balances.return_value = {
375
- "result": [
376
- {
377
- "token_address": "0x123",
378
- "symbol": "ETH",
379
- "name": "Ethereum",
380
- "logo": "logo.png",
381
- "decimals": 18,
382
- "balance": "1000000000000000000",
383
- "balance_formatted": "1.0",
384
- "usd_value": 2000,
385
- "native_token": True,
386
- },
387
- {
388
- "token_address": "0x456",
389
- "symbol": "TOKEN",
390
- "name": "Test Token",
391
- "logo": "logo2.png",
392
- "decimals": 18,
393
- "balance": "2000000000000000000",
394
- "balance_formatted": "2.0",
395
- "usd_value": 200,
396
- "native_token": False,
397
- },
398
- ]
399
- }
400
-
401
- tool = FetchChainPortfolio(
402
- name="fetch_chain_portfolio",
403
- description="Test description",
404
- args_schema=MagicMock(),
405
- api_key="test_key",
406
- skill_store=mock_skill_store,
407
- agent_id="test_agent",
408
- )
409
-
410
- result = await tool._arun(address="0xAddress", chain_id=1)
411
-
412
- self.assertEqual(result.address, "0xAddress")
413
- self.assertEqual(result.chain_id, 1)
414
- self.assertEqual(result.chain_name, "eth")
415
- self.assertEqual(result.total_usd_value, 2200) # 2000 + 200
416
- self.assertEqual(len(result.tokens), 1) # Regular tokens, not native
417
- self.assertIsNotNone(result.native_token)
418
- self.assertEqual(result.native_token.symbol, "ETH")
419
- self.assertEqual(result.tokens[0].symbol, "TOKEN")
420
-
421
-
422
- class TestSkillInitialization(unittest.TestCase):
423
- """Test skill initialization and configuration."""
424
-
425
- def setUp(self):
426
- self.mock_skill_store = MagicMock()
427
-
428
- def test_get_skills(self):
429
- """Test getting multiple skills from config."""
430
- config = {
431
- "api_key": "test_api_key",
432
- "states": {
433
- "fetch_wallet_portfolio": "public",
434
- "fetch_chain_portfolio": "public",
435
- "fetch_nft_portfolio": "private",
436
- "fetch_transaction_history": "private",
437
- "fetch_solana_portfolio": "public",
438
- },
439
- "supported_chains": {"evm": True, "solana": True},
440
- }
441
-
442
- # Test with mock implementation
443
- with patch("skills.moralis.base.WalletBaseTool") as mock_tool:
444
- mock_tool.return_value = MagicMock()
445
-
446
- # This is just a test structure - actual implementation would create the skills
447
- skills = get_skills(
448
- config,
449
- is_private=False, # Only get public skills
450
- skill_store=self.mock_skill_store,
451
- agent_id="test_agent",
452
- )
453
-
454
- # In a real implementation, we'd test that the correct skills were returned
455
- # For now, we just verify the function exists
456
- self.assertIsNotNone(skills)
457
-
458
-
459
- class TestIntegration(unittest.TestCase):
460
- """Integration tests for wallet skills."""
461
-
462
- def test_wallet_skill_configuration(self):
463
- """Test wallet skill configuration in agent config."""
464
- # Example agent configuration
465
- agent_config = {
466
- "id": "crypto-agent",
467
- "skills": {
468
- "moralis": {
469
- "api_key": "test_api_key",
470
- "states": {
471
- "fetch_wallet_portfolio": "public",
472
- "fetch_chain_portfolio": "public",
473
- "fetch_nft_portfolio": "private",
474
- "fetch_transaction_history": "private",
475
- "fetch_solana_portfolio": "public",
476
- },
477
- "supported_chains": {"evm": True, "solana": True},
478
- }
479
- },
480
- }
481
-
482
- # Verify the configuration structure is valid
483
- moralis_config = agent_config["skills"]["moralis"]
484
- self.assertIn("api_key", moralis_config)
485
- self.assertIn("states", moralis_config)
486
- self.assertIn("supported_chains", moralis_config)
487
-
488
- # Check that all required skills are configured
489
- states = moralis_config["states"]
490
- required_skills = [
491
- "fetch_wallet_portfolio",
492
- "fetch_chain_portfolio",
493
- "fetch_nft_portfolio",
494
- "fetch_transaction_history",
495
- "fetch_solana_portfolio",
496
- ]
497
-
498
- for skill in required_skills:
499
- self.assertIn(skill, states)
500
- self.assertIn(states[skill], ["public", "private", "disabled"])
501
-
502
- # Check chain configuration
503
- chains = moralis_config["supported_chains"]
504
- self.assertIn("evm", chains)
505
- self.assertIn("solana", chains)
506
- self.assertTrue(isinstance(chains["evm"], bool))
507
- self.assertTrue(isinstance(chains["solana"], bool))
508
-
509
-
510
- if __name__ == "__main__":
511
- unittest.main()