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 +1 -1
- intentkit/core/agent.py +212 -33
- intentkit/core/asset.py +208 -0
- intentkit/core/statistics.py +169 -0
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev28.dist-info}/METADATA +1 -1
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev28.dist-info}/RECORD +8 -8
- intentkit/skills/moralis/tests/__init__.py +0 -0
- intentkit/skills/moralis/tests/test_wallet.py +0 -511
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev28.dist-info}/WHEEL +0 -0
- {intentkit-0.7.5.dev26.dist-info → intentkit-0.7.5.dev28.dist-info}/licenses/LICENSE +0 -0
intentkit/__init__.py
CHANGED
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
920
|
+
"Finished updating statistics for %s agents in %.3fs",
|
|
921
|
+
total_updated,
|
|
922
|
+
total_time,
|
|
744
923
|
)
|
|
745
924
|
|
|
746
925
|
|
intentkit/core/asset.py
ADDED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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.
|
|
454
|
-
intentkit-0.7.5.
|
|
455
|
-
intentkit-0.7.5.
|
|
456
|
-
intentkit-0.7.5.
|
|
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()
|
|
File without changes
|
|
File without changes
|