intentkit 0.7.5.dev17__py3-none-any.whl → 0.7.5.dev19__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/core/node.py CHANGED
@@ -81,10 +81,10 @@ class PreModelNode(RunnableCallable):
81
81
  self.func_accepts_config = True
82
82
 
83
83
  def _parse_input(
84
- self, input: AgentState
84
+ self, state: AgentState
85
85
  ) -> tuple[list[AnyMessage], dict[str, Any]]:
86
- messages = input.get("messages")
87
- context = input.get("context", {})
86
+ messages = state.get("messages")
87
+ context = state.get("context", {})
88
88
  if messages is None or not isinstance(messages, list) or len(messages) == 0:
89
89
  raise ValueError("Missing required field `messages` in the input.")
90
90
  return messages, context
@@ -107,13 +107,13 @@ class PreModelNode(RunnableCallable):
107
107
  def _func(self, AgentState) -> dict[str, Any]:
108
108
  raise NotImplementedError("Not implemented yet")
109
109
 
110
- async def _afunc(self, input: AgentState) -> dict[str, Any]:
111
- messages, context = self._parse_input(input)
110
+ async def _afunc(self, state: AgentState) -> dict[str, Any]:
111
+ messages, context = self._parse_input(state)
112
112
  try:
113
113
  _validate_chat_history(messages)
114
114
  except ValueError as e:
115
115
  logger.error(f"Invalid chat history: {e}")
116
- logger.info(input)
116
+ logger.info(state)
117
117
  # delete all messages
118
118
  return {"messages": [RemoveMessage(REMOVE_ALL_MESSAGES)]}
119
119
  if self.short_term_memory_strategy == "trim":
@@ -162,7 +162,7 @@ class PreModelNode(RunnableCallable):
162
162
  return self._prepare_state_update(context, summarization_result)
163
163
  except ValueError as e:
164
164
  logger.error(f"Invalid chat history: {e}")
165
- logger.info(input)
165
+ logger.info(state)
166
166
  # delete all messages
167
167
  return {"messages": [RemoveMessage(REMOVE_ALL_MESSAGES)]}
168
168
  raise ValueError(
@@ -175,15 +175,15 @@ class PostModelNode(RunnableCallable):
175
175
  super().__init__(self._func, self._afunc, name="post_model_node", trace=False)
176
176
  self.func_accepts_config = True
177
177
 
178
- def _func(self, input: AgentState) -> dict[str, Any]:
178
+ def _func(self, state: AgentState) -> dict[str, Any]:
179
179
  raise NotImplementedError("Not implemented yet")
180
180
 
181
- async def _afunc(self, input: AgentState) -> dict[str, Any]:
181
+ async def _afunc(self, state: AgentState) -> dict[str, Any]:
182
182
  runtime = get_runtime(AgentContext)
183
183
  context = runtime.context
184
- logger.debug(f"Running PostModelNode, input: {input}, context: {context}")
184
+ logger.debug(f"Running PostModelNode, input: {state}, context: {context}")
185
185
  state_update = {}
186
- messages = input.get("messages")
186
+ messages = state.get("messages")
187
187
  if messages is None or not isinstance(messages, list) or len(messages) == 0:
188
188
  raise ValueError("Missing required field `messages` in the input.")
189
189
  payer = context.payer
intentkit/models/llm.py CHANGED
@@ -12,7 +12,7 @@ from intentkit.models.base import Base
12
12
  from intentkit.models.db import get_session
13
13
  from intentkit.models.redis import get_redis
14
14
  from intentkit.utils.error import IntentKitLookUpError
15
- from langchain_core.language_models import LanguageModelLike
15
+ from langchain.chat_models.base import BaseChatModel
16
16
  from pydantic import BaseModel, ConfigDict, Field
17
17
  from sqlalchemy import Boolean, Column, DateTime, Integer, Numeric, String, func, select
18
18
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -344,7 +344,7 @@ class LLMModel(BaseModel):
344
344
  return model_info
345
345
 
346
346
  # This will be implemented by subclasses to return the appropriate LLM instance
347
- async def create_instance(self, config: Any) -> LanguageModelLike:
347
+ async def create_instance(self, config: Any) -> BaseChatModel:
348
348
  """Create and return the LLM instance based on the configuration."""
349
349
  raise NotImplementedError("Subclasses must implement create_instance")
350
350
 
@@ -362,7 +362,7 @@ class LLMModel(BaseModel):
362
362
  class OpenAILLM(LLMModel):
363
363
  """OpenAI LLM configuration."""
364
364
 
365
- async def create_instance(self, config: Any) -> LanguageModelLike:
365
+ async def create_instance(self, config: Any) -> BaseChatModel:
366
366
  """Create and return a ChatOpenAI instance."""
367
367
  from langchain_openai import ChatOpenAI
368
368
 
@@ -398,7 +398,7 @@ class OpenAILLM(LLMModel):
398
398
  class DeepseekLLM(LLMModel):
399
399
  """Deepseek LLM configuration."""
400
400
 
401
- async def create_instance(self, config: Any) -> LanguageModelLike:
401
+ async def create_instance(self, config: Any) -> BaseChatModel:
402
402
  """Create and return a ChatDeepseek instance."""
403
403
 
404
404
  from langchain_deepseek import ChatDeepSeek
@@ -431,7 +431,7 @@ class DeepseekLLM(LLMModel):
431
431
  class XAILLM(LLMModel):
432
432
  """XAI (Grok) LLM configuration."""
433
433
 
434
- async def create_instance(self, config: Any) -> LanguageModelLike:
434
+ async def create_instance(self, config: Any) -> BaseChatModel:
435
435
  """Create and return a ChatXAI instance."""
436
436
 
437
437
  from langchain_xai import ChatXAI
@@ -463,7 +463,7 @@ class XAILLM(LLMModel):
463
463
  class EternalLLM(LLMModel):
464
464
  """Eternal AI LLM configuration."""
465
465
 
466
- async def create_instance(self, config: Any) -> LanguageModelLike:
466
+ async def create_instance(self, config: Any) -> BaseChatModel:
467
467
  """Create and return a ChatOpenAI instance configured for Eternal AI."""
468
468
  from langchain_openai import ChatOpenAI
469
469
 
@@ -495,7 +495,7 @@ class EternalLLM(LLMModel):
495
495
  class ReigentLLM(LLMModel):
496
496
  """Reigent LLM configuration."""
497
497
 
498
- async def create_instance(self, config: Any) -> LanguageModelLike:
498
+ async def create_instance(self, config: Any) -> BaseChatModel:
499
499
  """Create and return a ChatOpenAI instance configured for Reigent."""
500
500
  from langchain_openai import ChatOpenAI
501
501
 
@@ -517,7 +517,7 @@ class ReigentLLM(LLMModel):
517
517
  class VeniceLLM(LLMModel):
518
518
  """Venice LLM configuration."""
519
519
 
520
- async def create_instance(self, config: Any) -> LanguageModelLike:
520
+ async def create_instance(self, config: Any) -> BaseChatModel:
521
521
  """Create and return a ChatOpenAI instance configured for Venice."""
522
522
  from langchain_openai import ChatOpenAI
523
523
 
@@ -36,7 +36,7 @@ class Config(SkillConfig):
36
36
 
37
37
  states: SkillStates
38
38
  api_token: NotRequired[str]
39
- main_tokens: List[str]
39
+ main_tokens: NotRequired[List[str]]
40
40
 
41
41
 
42
42
  async def get_skills(
@@ -1,6 +1,6 @@
1
+ from decimal import Decimal
1
2
  from typing import Optional, Type
2
3
 
3
- from cdp import EvmServerAccount
4
4
  from coinbase_agentkit import CdpEvmWalletProvider
5
5
  from langchain.tools.base import ToolException
6
6
  from pydantic import BaseModel, Field
@@ -9,10 +9,9 @@ from intentkit.abstracts.graph import AgentContext
9
9
  from intentkit.abstracts.skill import SkillStoreABC
10
10
  from intentkit.clients import CdpClient, get_cdp_client
11
11
  from intentkit.skills.base import IntentKitSkill
12
- from intentkit.utils.chain import ChainProvider, NetworkId
12
+ from intentkit.utils.chain import ChainProvider, Network, network_to_id
13
13
 
14
14
  base_url = "https://api.enso.finance"
15
- default_chain_id = int(NetworkId.BaseMainnet)
16
15
 
17
16
 
18
17
  class EnsoBaseTool(IntentKitSkill):
@@ -25,18 +24,6 @@ class EnsoBaseTool(IntentKitSkill):
25
24
  description="The skill store for persisting data"
26
25
  )
27
26
 
28
- async def get_account(self, context: AgentContext) -> Optional[EvmServerAccount]:
29
- """Get the account object from the CDP client.
30
-
31
- Args:
32
- context: The skill context containing agent information.
33
-
34
- Returns:
35
- Optional[EvmServerAccount]: The account object if available.
36
- """
37
- client: CdpClient = await get_cdp_client(context.agent.id, self.skill_store)
38
- return await client.get_account()
39
-
40
27
  async def get_wallet_provider(
41
28
  self, context: AgentContext
42
29
  ) -> Optional[CdpEvmWalletProvider]:
@@ -51,6 +38,13 @@ class EnsoBaseTool(IntentKitSkill):
51
38
  client: CdpClient = await get_cdp_client(context.agent.id, self.skill_store)
52
39
  return await client.get_wallet_provider()
53
40
 
41
+ async def get_wallet_address(self, context: AgentContext) -> str:
42
+ client: CdpClient = await get_cdp_client(context.agent.id, self.skill_store)
43
+ provider_config = await client.get_provider_config()
44
+ if not provider_config.address:
45
+ raise ToolException("wallet address not found for agent")
46
+ return provider_config.address
47
+
54
48
  def get_chain_provider(self, context: AgentContext) -> Optional[ChainProvider]:
55
49
  return self.skill_store.get_system_config("chain_provider")
56
50
 
@@ -60,8 +54,7 @@ class EnsoBaseTool(IntentKitSkill):
60
54
  return skill_config["main_tokens"]
61
55
  return []
62
56
 
63
- def get_api_key(self) -> str:
64
- context = self.get_context()
57
+ def get_api_token(self, context: AgentContext) -> str:
65
58
  skill_config = context.agent.skill_config(self.category)
66
59
  api_key_provider = skill_config.get("api_key_provider")
67
60
  if api_key_provider == "platform":
@@ -74,6 +67,40 @@ class EnsoBaseTool(IntentKitSkill):
74
67
  f"Invalid API key provider: {api_key_provider}, or no api_token in config"
75
68
  )
76
69
 
70
+ def resolve_chain_id(
71
+ self, context: AgentContext, chain_id: Optional[int] = None
72
+ ) -> int:
73
+ if chain_id:
74
+ return chain_id
75
+
76
+ agent = context.agent
77
+ try:
78
+ network = Network(agent.network_id)
79
+ except ValueError as exc: # pragma: no cover - defensive
80
+ raise ToolException(
81
+ f"Unsupported network configured for agent: {agent.network_id}"
82
+ ) from exc
83
+
84
+ network_id = network_to_id.get(network)
85
+ if network_id is None:
86
+ raise ToolException(
87
+ f"Unable to determine chain id for network: {agent.network_id}"
88
+ )
89
+ return int(network_id)
90
+
77
91
  @property
78
92
  def category(self) -> str:
79
93
  return "enso"
94
+
95
+
96
+ def format_amount_with_decimals(
97
+ amount: object, decimals: Optional[int]
98
+ ) -> Optional[str]:
99
+ if amount is None or decimals is None:
100
+ return None
101
+
102
+ try:
103
+ value = Decimal(str(amount)) / (Decimal(10) ** decimals)
104
+ return format(value, "f")
105
+ except Exception: # pragma: no cover - defensive
106
+ return None
@@ -4,14 +4,7 @@ import httpx
4
4
  from langchain.tools.base import ToolException
5
5
  from pydantic import BaseModel, Field
6
6
 
7
- from intentkit.skills.enso.base import (
8
- EnsoBaseTool,
9
- base_url,
10
- )
11
- from intentkit.utils.chain import NetworkId
12
-
13
- # Chain ID for Base Mainnet
14
- BASE_CHAIN_ID = int(NetworkId.BaseMainnet)
7
+ from intentkit.skills.enso.base import EnsoBaseTool, base_url
15
8
 
16
9
 
17
10
  class EnsoGetBestYieldInput(BaseModel):
@@ -21,9 +14,9 @@ class EnsoGetBestYieldInput(BaseModel):
21
14
  "USDC",
22
15
  description="Symbol of the token to find the best yield for (e.g., 'USDC', 'ETH', 'USDT')",
23
16
  )
24
- chain_id: int = Field(
25
- BASE_CHAIN_ID,
26
- description="The blockchain chain ID. Default is Base Mainnet (8453)",
17
+ chain_id: int | None = Field(
18
+ None,
19
+ description="The blockchain chain ID. Defaults to the agent's configured network.",
27
20
  )
28
21
  top_n: int = Field(
29
22
  5,
@@ -80,7 +73,7 @@ class EnsoGetBestYield(EnsoBaseTool):
80
73
  async def _arun(
81
74
  self,
82
75
  token_symbol: str = "USDC",
83
- chain_id: int = BASE_CHAIN_ID,
76
+ chain_id: int | None = None,
84
77
  top_n: int = 5,
85
78
  **kwargs,
86
79
  ) -> EnsoGetBestYieldOutput:
@@ -89,7 +82,7 @@ class EnsoGetBestYield(EnsoBaseTool):
89
82
 
90
83
  Args:
91
84
  token_symbol (str): Symbol of the token to find the best yield for (default: USDC)
92
- chain_id (int): The chain id of the network (default: Base Mainnet)
85
+ chain_id (int | None): The chain id of the network. Defaults to the agent's configured network.
93
86
  top_n (int): Number of top yield options to return
94
87
 
95
88
  Returns:
@@ -99,16 +92,17 @@ class EnsoGetBestYield(EnsoBaseTool):
99
92
  ToolException: If there's an error accessing the Enso API.
100
93
  """
101
94
  context = self.get_context()
95
+ resolved_chain_id = self.resolve_chain_id(context, chain_id)
102
96
  api_token = self.get_api_token(context)
103
97
 
104
98
  if not api_token:
105
99
  raise ToolException("No API token found for Enso Finance")
106
100
 
107
101
  # Get the chain name for the given chain ID
108
- chain_name = await self._get_chain_name(api_token, chain_id)
102
+ chain_name = await self._get_chain_name(api_token, resolved_chain_id)
109
103
 
110
104
  # Get all protocols on the specified chain
111
- protocols = await self._get_protocols(api_token, chain_id)
105
+ protocols = await self._get_protocols(api_token, resolved_chain_id)
112
106
 
113
107
  # Collect all yield options from all protocols
114
108
  all_yield_options = []
@@ -119,7 +113,7 @@ class EnsoGetBestYield(EnsoBaseTool):
119
113
 
120
114
  # Get yield-bearing tokens for this protocol
121
115
  tokens = await self._get_protocol_tokens(
122
- api_token, chain_id, protocol_slug, token_symbol
116
+ api_token, resolved_chain_id, protocol_slug, token_symbol
123
117
  )
124
118
 
125
119
  # Process tokens to extract yield options
@@ -170,7 +164,7 @@ class EnsoGetBestYield(EnsoBaseTool):
170
164
  return EnsoGetBestYieldOutput(
171
165
  best_options=top_options,
172
166
  token_symbol=token_symbol,
173
- chain_id=chain_id,
167
+ chain_id=resolved_chain_id,
174
168
  chain_name=chain_name,
175
169
  )
176
170
 
@@ -7,8 +7,6 @@ from pydantic import BaseModel, Field
7
7
 
8
8
  from .base import EnsoBaseTool, base_url
9
9
 
10
- logger = logging.getLogger(__name__)
11
-
12
10
 
13
11
  class EnsoGetNetworksInput(BaseModel):
14
12
  """
@@ -38,6 +36,9 @@ class EnsoGetNetworksOutput(BaseModel):
38
36
  )
39
37
 
40
38
 
39
+ logger = logging.getLogger(__name__)
40
+
41
+
41
42
  class EnsoGetNetworks(EnsoBaseTool):
42
43
  """
43
44
  Tool for retrieving networks and their corresponding chainId, the output should be kept.
@@ -4,13 +4,11 @@ import httpx
4
4
  from langchain.tools.base import ToolException
5
5
  from pydantic import BaseModel, Field
6
6
 
7
- from .base import EnsoBaseTool, base_url, default_chain_id
7
+ from .base import EnsoBaseTool, base_url
8
8
 
9
9
 
10
10
  class EnsoGetPricesInput(BaseModel):
11
- chainId: int = Field(
12
- default_chain_id, description="Blockchain chain ID of the token"
13
- )
11
+ chainId: int | None = Field(None, description="Blockchain chain ID of the token")
14
12
  address: str = Field(
15
13
  "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
16
14
  description="Contract address of the token",
@@ -43,22 +41,21 @@ class EnsoGetPrices(EnsoBaseTool):
43
41
  async def _arun(
44
42
  self,
45
43
  address: str,
46
- chainId: int = default_chain_id,
44
+ chainId: int | None = None,
47
45
  **kwargs,
48
46
  ) -> EnsoGetPricesOutput:
49
47
  """
50
48
  Asynchronous function to request the token price from the API.
51
49
 
52
50
  Args:
53
- chainId (int): The blockchain's chain ID.
51
+ chainId (int | None): The blockchain's chain ID. Defaults to the agent's configured network.
54
52
  address (str): Contract address of the token.
55
53
 
56
54
  Returns:
57
55
  EnsoGetPricesOutput: Token price response or error message.
58
56
  """
59
- url = f"{base_url}/api/v1/prices/{str(chainId)}/{address}"
60
-
61
57
  context = self.get_context()
58
+ resolved_chain_id = self.resolve_chain_id(context, chainId)
62
59
  api_token = self.get_api_token(context)
63
60
 
64
61
  headers = {
@@ -68,7 +65,10 @@ class EnsoGetPrices(EnsoBaseTool):
68
65
 
69
66
  async with httpx.AsyncClient() as client:
70
67
  try:
71
- response = await client.get(url, headers=headers)
68
+ response = await client.get(
69
+ f"{base_url}/api/v1/prices/{str(resolved_chain_id)}/{address}",
70
+ headers=headers,
71
+ )
72
72
  response.raise_for_status()
73
73
  json_dict = response.json()
74
74
 
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
6
6
 
7
7
  from intentkit.skills.enso.networks import EnsoGetNetworks
8
8
 
9
- from .base import EnsoBaseTool, base_url, default_chain_id
9
+ from .base import EnsoBaseTool, base_url, format_amount_with_decimals
10
10
 
11
11
 
12
12
  class EnsoRouteShortcutInput(BaseModel):
@@ -18,9 +18,9 @@ class EnsoRouteShortcutInput(BaseModel):
18
18
  False,
19
19
  description="Whether to broadcast the transaction or not, this is false by default.",
20
20
  )
21
- chainId: int = Field(
22
- default_chain_id,
23
- description="(Optional) Chain ID of the network to execute the transaction on. the default value is the chain_id extracted from networks according to tokenIn and tokenOut",
21
+ chainId: int | None = Field(
22
+ None,
23
+ description="(Optional) Chain ID of the network to execute the transaction on. Defaults to the agent's configured network.",
24
24
  )
25
25
  amountIn: list[int] = Field(
26
26
  description="Amount of tokenIn to swap in wei, you should multiply user's requested value by token decimals."
@@ -162,7 +162,7 @@ class EnsoRouteShortcut(EnsoBaseTool):
162
162
  amountIn: list[int],
163
163
  tokenIn: list[str],
164
164
  tokenOut: list[str],
165
- chainId: int = default_chain_id,
165
+ chainId: int | None = None,
166
166
  broadcast_requested: bool = False,
167
167
  **kwargs,
168
168
  ) -> EnsoRouteShortcutOutput:
@@ -173,7 +173,7 @@ class EnsoRouteShortcut(EnsoBaseTool):
173
173
  amountIn (list[int]): Amount of tokenIn to swap in wei, you should multiply user's requested value by token decimals.
174
174
  tokenIn (list[str]): Ethereum address of the token to swap or enter into a position from (For ETH, use 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee).
175
175
  tokenOut (list[str]): Ethereum address of the token to swap or enter into a position to (For ETH, use 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee).
176
- chainId (int): The chain id of the network to be used for swap, deposit and routing.
176
+ chainId (int | None): The chain id of the network to be used for swap, deposit and routing. Defaults to the agent's configured network.
177
177
  broadcast_requested (bool): User should ask for broadcasting the transaction explicitly, otherwise it is always false.
178
178
 
179
179
  Returns:
@@ -182,8 +182,9 @@ class EnsoRouteShortcut(EnsoBaseTool):
182
182
 
183
183
  context = self.get_context()
184
184
  agent_id = context.agent_id
185
+ resolved_chain_id = self.resolve_chain_id(context, chainId)
185
186
  api_token = self.get_api_token(context)
186
- account = await self.get_account(context)
187
+ wallet_address = await self.get_wallet_address(context)
187
188
 
188
189
  async with httpx.AsyncClient() as client:
189
190
  try:
@@ -193,9 +194,10 @@ class EnsoRouteShortcut(EnsoBaseTool):
193
194
  )
194
195
 
195
196
  if networks:
197
+ resolved_key = str(resolved_chain_id)
196
198
  network_name = (
197
- networks.get(str(chainId)).get("name")
198
- if networks.get(str(chainId))
199
+ networks.get(resolved_key).get("name")
200
+ if networks.get(resolved_key)
199
201
  else None
200
202
  )
201
203
  if network_name is None:
@@ -204,12 +206,12 @@ class EnsoRouteShortcut(EnsoBaseTool):
204
206
  ).arun()
205
207
 
206
208
  for network in networks.res:
207
- if network.id == chainId:
209
+ if network.id == resolved_chain_id:
208
210
  network_name = network.name
209
211
 
210
212
  if not network_name:
211
213
  raise ToolException(
212
- f"network name not found for chainId: {chainId}"
214
+ f"network name not found for chainId: {resolved_chain_id}"
213
215
  )
214
216
 
215
217
  headers = {
@@ -242,13 +244,13 @@ class EnsoRouteShortcut(EnsoBaseTool):
242
244
 
243
245
  # Prepare query parameters
244
246
  params = EnsoRouteShortcutInput(
245
- chainId=chainId,
247
+ chainId=resolved_chain_id,
246
248
  amountIn=amountIn,
247
249
  tokenIn=tokenIn,
248
250
  tokenOut=tokenOut,
249
251
  ).model_dump(exclude_none=True)
250
252
 
251
- params["fromAddress"] = account.address
253
+ params["fromAddress"] = wallet_address
252
254
 
253
255
  response = await client.get(url, headers=headers, params=params)
254
256
  response.raise_for_status() # Raise HTTPError for non-2xx responses
@@ -256,10 +258,12 @@ class EnsoRouteShortcut(EnsoBaseTool):
256
258
 
257
259
  res = EnsoRouteShortcutOutput(**json_dict)
258
260
  res.network = network_name
259
-
260
- res.amountOut = str(
261
- float(res.amountOut) / 10 ** token_decimals[tokenOut[0]]
261
+ decimals = token_decimals.get(tokenOut[0])
262
+ amount_out = format_amount_with_decimals(
263
+ json_dict.get("amountOut"), decimals
262
264
  )
265
+ if amount_out is not None:
266
+ res.amountOut = amount_out
263
267
 
264
268
  if broadcast_requested:
265
269
  # Use the wallet provider to send the transaction
@@ -269,13 +273,13 @@ class EnsoRouteShortcut(EnsoBaseTool):
269
273
  tx_data = json_dict.get("tx", {})
270
274
  if tx_data:
271
275
  # Send the transaction using the wallet provider
272
- tx_hash = wallet_provider.send_transaction(
273
- {
274
- "to": tx_data.get("to"),
275
- "data": tx_data.get("data", "0x"),
276
- "value": tx_data.get("value", 0),
277
- }
278
- )
276
+ tx_params = {
277
+ "to": tx_data.get("to"),
278
+ "data": tx_data.get("data", "0x"),
279
+ "value": tx_data.get("value", 0),
280
+ "from": wallet_address,
281
+ }
282
+ tx_hash = wallet_provider.send_transaction(tx_params)
279
283
 
280
284
  # Wait for transaction confirmation
281
285
  wallet_provider.wait_for_transaction_receipt(tx_hash)
@@ -4,11 +4,7 @@ import httpx
4
4
  from langchain.tools.base import ToolException
5
5
  from pydantic import BaseModel, Field
6
6
 
7
- from intentkit.skills.enso.base import (
8
- EnsoBaseTool,
9
- base_url,
10
- default_chain_id,
11
- )
7
+ from intentkit.skills.enso.base import EnsoBaseTool, base_url
12
8
 
13
9
  # Actual Enso output types
14
10
  # class UnderlyingToken(BaseModel):
@@ -50,9 +46,9 @@ from intentkit.skills.enso.base import (
50
46
 
51
47
 
52
48
  class EnsoGetTokensInput(BaseModel):
53
- chainId: int = Field(
54
- default_chain_id,
55
- description="The blockchain chain ID",
49
+ chainId: int | None = Field(
50
+ None,
51
+ description="The blockchain chain ID. Defaults to the agent's configured network.",
56
52
  )
57
53
  protocolSlug: str | None = Field(
58
54
  None,
@@ -110,7 +106,10 @@ class TokenResponseCompact(BaseModel):
110
106
 
111
107
 
112
108
  class EnsoGetTokensOutput(BaseModel):
113
- res: list[TokenResponseCompact] | None
109
+ res: list[TokenResponseCompact] = Field(
110
+ default_factory=list,
111
+ description="List of token information entries",
112
+ )
114
113
 
115
114
 
116
115
  class EnsoGetTokens(EnsoBaseTool):
@@ -138,7 +137,7 @@ class EnsoGetTokens(EnsoBaseTool):
138
137
 
139
138
  async def _arun(
140
139
  self,
141
- chainId: int = default_chain_id,
140
+ chainId: int | None = None,
142
141
  protocolSlug: str | None = None,
143
142
  **kwargs,
144
143
  ) -> EnsoGetTokensOutput:
@@ -156,15 +155,17 @@ class EnsoGetTokens(EnsoBaseTool):
156
155
 
157
156
  context = self.get_context()
158
157
  agent_id = context.agent_id
158
+ resolved_chain_id = self.resolve_chain_id(context, chainId)
159
159
  api_token = self.get_api_token(context)
160
160
  main_tokens = self.get_main_tokens(context)
161
+ main_tokens_upper = {token.upper() for token in main_tokens}
161
162
  headers = {
162
163
  "accept": "application/json",
163
164
  "Authorization": f"Bearer {api_token}",
164
165
  }
165
166
 
166
167
  params = EnsoGetTokensInput(
167
- chainId=chainId,
168
+ chainId=resolved_chain_id,
168
169
  protocolSlug=protocolSlug,
169
170
  ).model_dump(exclude_none=True)
170
171
 
@@ -186,19 +187,21 @@ class EnsoGetTokens(EnsoBaseTool):
186
187
  token_decimals = {}
187
188
 
188
189
  # filter the main tokens from config or the ones that have apy assigned.
189
- res = EnsoGetTokensOutput(res=list[TokenResponseCompact]())
190
- for item in json_dict["data"]:
191
- main_tokens = [item.upper() for item in main_tokens]
192
- if item.get("apy") or (item.get("symbol").upper() in main_tokens):
190
+ res = EnsoGetTokensOutput()
191
+ for item in json_dict.get("data", []):
192
+ symbol = item.get("symbol", "").upper()
193
+ has_apy = bool(item.get("apy"))
194
+ if has_apy or symbol in main_tokens_upper:
193
195
  token_response = TokenResponseCompact(**item)
194
196
  res.res.append(token_response)
195
- token_decimals[token_response.address] = token_response.decimals
196
- if (
197
- token_response.underlyingTokens
198
- and len(token_response.underlyingTokens) > 0
199
- ):
197
+ if token_response.address:
198
+ token_decimals[token_response.address] = (
199
+ token_response.decimals
200
+ )
201
+ if token_response.underlyingTokens:
200
202
  for u_token in token_response.underlyingTokens:
201
- token_decimals[u_token.address] = u_token.decimals
203
+ if u_token.address:
204
+ token_decimals[u_token.address] = u_token.decimals
202
205
 
203
206
  await self.skill_store.save_agent_skill_data(
204
207
  agent_id,