intentkit 0.6.10.dev7__py3-none-any.whl → 0.6.11__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/prompt.py CHANGED
@@ -1,57 +1,438 @@
1
+ import re
2
+ from typing import Callable, Optional
3
+
4
+ from eth_utils import is_address
5
+ from langchain_core.messages import BaseMessage
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+ from langgraph.runtime import Runtime
8
+
9
+ from intentkit.abstracts.graph import AgentContext, AgentState
1
10
  from intentkit.config.config import config
2
11
  from intentkit.models.agent import Agent
3
12
  from intentkit.models.agent_data import AgentData
13
+ from intentkit.models.chat import AuthorType
14
+ from intentkit.models.skill import Skill
4
15
 
16
+ # ============================================================================
17
+ # CONSTANTS AND CONFIGURATION
18
+ # ============================================================================
5
19
 
6
- def agent_prompt(agent: Agent, agent_data: AgentData) -> str:
20
+ # Base system prompt components
21
+ BASE_SYSTEM_PROMPT = """You are an AI agent built using IntentKit.
22
+ Your tools are called 'skills'.
23
+ If your skill fails to execute due to a technical error ask the user to try again later, don't retry by yourself. If someone asks you to do something you can't do with your currently available skills, you must say so, recommend them to submit their feedback to the IntentKit team at https://github.com/crestalnetwork/intentkit. Be concise and helpful with your responses."""
24
+
25
+ ENSO_SKILLS_GUIDE = """## ENSO Skills Guide
26
+
27
+ You are integrated with the Enso API. You can use enso_get_tokens to retrieve token information,
28
+ including APY, Protocol Slug, Symbol, Address, Decimals, and underlying tokens. When interacting with token amounts,
29
+ ensure to multiply input amounts by the token's decimal places and divide output amounts by the token's decimals.
30
+ Utilize enso_route_shortcut to find the best swap or deposit route. Set broadcast_request to True only when the
31
+ user explicitly requests a transaction broadcast. Insufficient funds or insufficient spending approval can cause
32
+ Route Shortcut broadcasts to fail. To avoid this, use the enso_broadcast_wallet_approve tool that requires explicit
33
+ user confirmation before broadcasting any approval transactions for security reasons.
34
+
35
+ """
36
+
37
+
38
+ # ============================================================================
39
+ # CORE PROMPT BUILDING FUNCTIONS
40
+ # ============================================================================
41
+
42
+
43
+ def _build_system_header() -> str:
44
+ """Build the system prompt header."""
7
45
  prompt = "# SYSTEM PROMPT\n\n"
8
46
  if config.system_prompt:
9
47
  prompt += config.system_prompt + "\n\n"
10
- prompt += "You are an AI agent built using IntentKit.\n"
11
- prompt += "Your tools are called 'skills'.\n"
12
- prompt += "If your skill fails to execute due to a technical error ask the user to try again later, don't retry by yourself. If someone asks you to do something you can't do with your currently available skills, you must say so, recommend them to submit their feedback to the IntentKit team at https://github.com/crestalnetwork/intentkit. Be concise and helpful with your responses.\n"
48
+ prompt += BASE_SYSTEM_PROMPT + "\n"
49
+ return prompt
50
+
51
+
52
+ def _build_agent_identity_section(agent: Agent) -> str:
53
+ """Build agent identity information section."""
54
+ identity_parts = []
55
+
13
56
  if agent.name:
14
- prompt += f"Your name is {agent.name}.\n"
57
+ identity_parts.append(f"Your name is {agent.name}.")
15
58
  if agent.ticker:
16
- prompt += f"Your ticker symbol is {agent.ticker}.\n"
17
- if agent_data:
18
- if agent_data.twitter_id:
19
- prompt += f"Your twitter id is {agent_data.twitter_id}, never reply or retweet yourself.\n"
20
- if agent_data.twitter_username:
21
- prompt += f"Your twitter username is {agent_data.twitter_username}.\n"
22
- if agent_data.twitter_name:
23
- prompt += f"Your twitter name is {agent_data.twitter_name}.\n"
24
- if agent_data.twitter_is_verified:
25
- prompt += "Your twitter account is verified.\n"
26
- else:
27
- prompt += "Your twitter account is not verified.\n"
28
- if agent_data.telegram_id:
29
- prompt += f"Your telegram bot id is {agent_data.telegram_id}.\n"
30
- if agent_data.telegram_username:
31
- prompt += f"Your telegram bot username is {agent_data.telegram_username}.\n"
32
- if agent_data.telegram_name:
33
- prompt += f"Your telegram bot name is {agent_data.telegram_name}.\n"
34
- # CDP
35
- network_id = agent.network_id or agent.cdp_network_id
36
- if agent_data.evm_wallet_address and network_id != "solana":
37
- prompt += f"Your wallet address in {network_id} is {agent_data.evm_wallet_address} .\n"
38
- if agent_data.solana_wallet_address and network_id == "solana":
39
- prompt += f"Your wallet address in {network_id} is {agent_data.solana_wallet_address} .\n"
40
- prompt += "\n"
59
+ identity_parts.append(f"Your ticker symbol is {agent.ticker}.")
60
+
61
+ return "\n".join(identity_parts) + ("\n" if identity_parts else "")
62
+
63
+
64
+ def _build_social_accounts_section(agent_data: AgentData) -> str:
65
+ """Build social accounts information section."""
66
+ if not agent_data:
67
+ return ""
68
+
69
+ social_parts = []
70
+
71
+ # Twitter info
72
+ if agent_data.twitter_id:
73
+ social_parts.append(
74
+ f"Your twitter id is {agent_data.twitter_id}, never reply or retweet yourself."
75
+ )
76
+ if agent_data.twitter_username:
77
+ social_parts.append(f"Your twitter username is {agent_data.twitter_username}.")
78
+ if agent_data.twitter_name:
79
+ social_parts.append(f"Your twitter name is {agent_data.twitter_name}.")
80
+
81
+ # Twitter verification status
82
+ if agent_data.twitter_is_verified:
83
+ social_parts.append("Your twitter account is verified.")
84
+ else:
85
+ social_parts.append("Your twitter account is not verified.")
86
+
87
+ # Telegram info
88
+ if agent_data.telegram_id:
89
+ social_parts.append(f"Your telegram bot id is {agent_data.telegram_id}.")
90
+ if agent_data.telegram_username:
91
+ social_parts.append(
92
+ f"Your telegram bot username is {agent_data.telegram_username}."
93
+ )
94
+ if agent_data.telegram_name:
95
+ social_parts.append(f"Your telegram bot name is {agent_data.telegram_name}.")
96
+
97
+ return "\n".join(social_parts) + ("\n" if social_parts else "")
98
+
99
+
100
+ def _build_wallet_section(agent: Agent, agent_data: AgentData) -> str:
101
+ """Build wallet information section."""
102
+ if not agent_data:
103
+ return ""
104
+
105
+ wallet_parts = []
106
+ network_id = agent.network_id or agent.cdp_network_id
107
+
108
+ if agent_data.evm_wallet_address and network_id != "solana":
109
+ wallet_parts.append(
110
+ f"Your wallet address in {network_id} is {agent_data.evm_wallet_address}."
111
+ )
112
+ if agent_data.solana_wallet_address and network_id == "solana":
113
+ wallet_parts.append(
114
+ f"Your wallet address in {network_id} is {agent_data.solana_wallet_address}."
115
+ )
116
+
117
+ return "\n".join(wallet_parts) + ("\n" if wallet_parts else "")
118
+
119
+
120
+ def _build_user_info_section(context: AgentContext) -> str:
121
+ """Build user information section when user_id is a valid EVM wallet address."""
122
+ if not context.user_id:
123
+ return ""
124
+
125
+ # Check if user_id is a valid EVM wallet address
126
+ try:
127
+ if is_address(context.user_id):
128
+ return f"## User Info\n\nThe person you are talking to has wallet address: {context.user_id}\n\n"
129
+ except Exception:
130
+ # If validation fails, don't include the section
131
+ pass
132
+
133
+ return ""
134
+
135
+
136
+ def _build_agent_characteristics_section(agent: Agent) -> str:
137
+ """Build agent characteristics section (purpose, personality, principles, etc.)."""
138
+ sections = []
139
+
41
140
  if agent.purpose:
42
- prompt += f"## Purpose\n\n{agent.purpose}\n\n"
141
+ sections.append(f"## Purpose\n\n{agent.purpose}")
43
142
  if agent.personality:
44
- prompt += f"## Personality\n\n{agent.personality}\n\n"
143
+ sections.append(f"## Personality\n\n{agent.personality}")
45
144
  if agent.principles:
46
- prompt += f"## Principles\n\n{agent.principles}\n\n"
145
+ sections.append(f"## Principles\n\n{agent.principles}")
47
146
  if agent.prompt:
48
- prompt += f"## Initial Rules\n\n{agent.prompt}\n\n"
147
+ sections.append(f"## Initial Rules\n\n{agent.prompt}")
148
+
149
+ return "\n\n".join(sections) + ("\n\n" if sections else "")
150
+
151
+
152
+ def _build_skills_guides_section(agent: Agent) -> str:
153
+ """Build skills-specific guides section."""
154
+ guides = []
155
+
156
+ # ENSO skills guide
49
157
  if agent.skills and "enso" in agent.skills and agent.skills["enso"].get("enabled"):
50
- prompt += """## ENSO Skills Guide\n\nYou are integrated with the Enso API. You can use enso_get_tokens to retrieve token information,
51
- including APY, Protocol Slug, Symbol, Address, Decimals, and underlying tokens. When interacting with token amounts,
52
- ensure to multiply input amounts by the token's decimal places and divide output amounts by the token's decimals.
53
- Utilize enso_route_shortcut to find the best swap or deposit route. Set broadcast_request to True only when the
54
- user explicitly requests a transaction broadcast. Insufficient funds or insufficient spending approval can cause
55
- Route Shortcut broadcasts to fail. To avoid this, use the enso_broadcast_wallet_approve tool that requires explicit
56
- user confirmation before broadcasting any approval transactions for security reasons.\n\n"""
57
- return prompt
158
+ guides.append(ENSO_SKILLS_GUIDE)
159
+
160
+ return "".join(guides)
161
+
162
+
163
+ def build_agent_prompt(agent: Agent, agent_data: AgentData) -> str:
164
+ """
165
+ Build the complete agent system prompt.
166
+
167
+ This function orchestrates the building of different prompt sections:
168
+ - System header and base prompt
169
+ - Agent identity (name, ticker)
170
+ - Social accounts (Twitter, Telegram)
171
+ - Wallet information
172
+ - Agent characteristics (purpose, personality, principles)
173
+ - Skills-specific guides
174
+
175
+ Args:
176
+ agent: The agent configuration
177
+ agent_data: The agent's runtime data
178
+
179
+ Returns:
180
+ str: The complete system prompt
181
+ """
182
+ prompt_sections = [
183
+ _build_system_header(),
184
+ _build_agent_identity_section(agent),
185
+ _build_social_accounts_section(agent_data),
186
+ _build_wallet_section(agent, agent_data),
187
+ "\n", # Add spacing before characteristics
188
+ _build_agent_characteristics_section(agent),
189
+ _build_skills_guides_section(agent),
190
+ ]
191
+
192
+ return "".join(section for section in prompt_sections if section)
193
+
194
+
195
+ # Legacy function name for backward compatibility
196
+ def agent_prompt(agent: Agent, agent_data: AgentData) -> str:
197
+ """Legacy function name. Use build_agent_prompt instead."""
198
+ return build_agent_prompt(agent, agent_data)
199
+
200
+
201
+ async def explain_prompt(message: str) -> str:
202
+ """
203
+ Process message to replace @skill:*:* patterns with (call skill xxxxx) format.
204
+ This function is used when admin_llm_skill_control is enabled.
205
+
206
+ Args:
207
+ message (str): The input message to process
208
+
209
+ Returns:
210
+ str: The processed message with @skill patterns replaced
211
+ """
212
+ # Pattern to match @skill:category:config_name with word boundaries
213
+ pattern = r"\b@skill:([^:]+):([^\s]+)\b"
214
+
215
+ async def replace_skill_pattern(match):
216
+ category = match.group(1)
217
+ config_name = match.group(2)
218
+
219
+ # Get skill by category and config_name
220
+ skill = await Skill.get_by_config_name(category, config_name)
221
+
222
+ if skill:
223
+ return f"(call skill {skill.name})"
224
+ else:
225
+ # If skill not found, keep original pattern
226
+ return match.group(0)
227
+
228
+ # Find all matches
229
+ matches = list(re.finditer(pattern, message))
230
+
231
+ # Process matches in reverse order to maintain string positions
232
+ result = message
233
+ for match in reversed(matches):
234
+ replacement = await replace_skill_pattern(match)
235
+ result = result[: match.start()] + replacement + result[match.end() :]
236
+
237
+ return result
238
+
239
+
240
+ # ============================================================================
241
+ # UTILITY FUNCTIONS
242
+ # ============================================================================
243
+
244
+
245
+ def escape_prompt(prompt: str) -> str:
246
+ """Escape curly braces in the prompt for template processing."""
247
+ return prompt.replace("{", "{{").replace("}", "}}")
248
+
249
+
250
+ # ============================================================================
251
+ # ENTRYPOINT PROCESSING FUNCTIONS
252
+ # ============================================================================
253
+
254
+
255
+ def _build_social_entrypoint_prompt(agent: Agent, entrypoint: str) -> Optional[str]:
256
+ """Build prompt for social media entrypoints (Twitter, Telegram)."""
257
+ if (
258
+ agent.twitter_entrypoint_enabled
259
+ and agent.twitter_entrypoint_prompt
260
+ and entrypoint == AuthorType.TWITTER.value
261
+ ):
262
+ return agent.twitter_entrypoint_prompt
263
+ elif (
264
+ agent.telegram_entrypoint_enabled
265
+ and agent.telegram_entrypoint_prompt
266
+ and entrypoint == AuthorType.TELEGRAM.value
267
+ ):
268
+ return agent.telegram_entrypoint_prompt
269
+ return None
270
+
271
+
272
+ def _build_autonomous_task_prompt(agent: Agent, context: AgentContext) -> str:
273
+ """Build prompt for autonomous task entrypoint."""
274
+ task_id = context.chat_id.removeprefix("autonomous-")
275
+
276
+ # Find the autonomous task by task_id
277
+ autonomous_task = None
278
+ if agent.autonomous:
279
+ for task in agent.autonomous:
280
+ if task.id == task_id:
281
+ autonomous_task = task
282
+ break
283
+
284
+ if not autonomous_task:
285
+ # Fallback if task not found
286
+ return f"You are running an autonomous task. The task id is {task_id}. "
287
+
288
+ # Build detailed task info - always include task_id
289
+ if autonomous_task.name:
290
+ task_info = f"You are running an autonomous task '{autonomous_task.name}' (ID: {task_id})"
291
+ else:
292
+ task_info = f"You are running an autonomous task (ID: {task_id})"
293
+
294
+ # Add description if available
295
+ if autonomous_task.description:
296
+ task_info += f": {autonomous_task.description}"
297
+
298
+ # Add cycle info
299
+ if autonomous_task.minutes:
300
+ task_info += f". This task runs every {autonomous_task.minutes} minute(s)"
301
+ elif autonomous_task.cron:
302
+ task_info += f". This task runs on schedule: {autonomous_task.cron}"
303
+
304
+ return f"{task_info}. "
305
+
306
+
307
+ async def build_entrypoint_prompt(agent: Agent, context: AgentContext) -> Optional[str]:
308
+ """
309
+ Build entrypoint-specific prompt based on context.
310
+
311
+ Supports different entrypoint types:
312
+ - Twitter: Uses agent.twitter_entrypoint_prompt
313
+ - Telegram: Uses agent.telegram_entrypoint_prompt
314
+ - Autonomous tasks: Builds task-specific prompt with scheduling info
315
+
316
+ Args:
317
+ agent: The agent configuration
318
+ context: The agent context containing entrypoint information
319
+
320
+ Returns:
321
+ Optional[str]: The entrypoint-specific prompt, or None if no entrypoint
322
+ """
323
+ if not context.entrypoint:
324
+ return None
325
+
326
+ entrypoint = context.entrypoint
327
+ entrypoint_prompt = None
328
+
329
+ # Handle social media entrypoints
330
+ entrypoint_prompt = _build_social_entrypoint_prompt(agent, entrypoint)
331
+
332
+ # Handle autonomous task entrypoint
333
+ if not entrypoint_prompt and entrypoint == AuthorType.TRIGGER.value:
334
+ entrypoint_prompt = _build_autonomous_task_prompt(agent, context)
335
+
336
+ # Process with admin LLM skill control if enabled
337
+ if entrypoint_prompt and config.admin_llm_skill_control:
338
+ entrypoint_prompt = await explain_prompt(entrypoint_prompt)
339
+
340
+ return entrypoint_prompt
341
+
342
+
343
+ def build_internal_info_prompt(context: AgentContext) -> str:
344
+ """Build internal info prompt with context information."""
345
+ internal_info = "## Internal Info\n\n"
346
+ internal_info += "These are for your internal use. You can use them when querying or storing data, "
347
+ internal_info += "but please do not directly share this information with users.\n\n"
348
+ internal_info += f"chat_id: {context.chat_id}\n\n"
349
+ if context.user_id:
350
+ internal_info += f"user_id: {context.user_id}\n\n"
351
+ return internal_info
352
+
353
+
354
+ # ============================================================================
355
+ # MAIN PROMPT FACTORY FUNCTION
356
+ # ============================================================================
357
+
358
+
359
+ def create_formatted_prompt_function(agent: Agent, agent_data: AgentData) -> Callable:
360
+ """
361
+ Create the formatted_prompt function with agent-specific configuration.
362
+
363
+ This is the main factory function that creates a prompt formatting function
364
+ tailored to a specific agent. The returned function will be used by the
365
+ agent's runtime to format prompts for each conversation.
366
+
367
+ Args:
368
+ agent: The agent configuration
369
+ agent_data: The agent's runtime data
370
+
371
+ Returns:
372
+ Callable: An async function that formats prompts based on agent state and context
373
+ """
374
+ # Build base prompt using the new function name
375
+ prompt = build_agent_prompt(agent, agent_data)
376
+ escaped_prompt = escape_prompt(prompt)
377
+
378
+ # Process with admin LLM skill control if enabled
379
+ async def get_base_prompt():
380
+ if config.admin_llm_skill_control:
381
+ return await explain_prompt(escaped_prompt)
382
+ return escaped_prompt
383
+
384
+ # Build prompt array
385
+ prompt_array = [
386
+ ("placeholder", "{system_prompt}"),
387
+ ("placeholder", "{messages}"),
388
+ ]
389
+
390
+ if agent.prompt_append:
391
+ # Escape any curly braces in prompt_append
392
+ escaped_append = escape_prompt(agent.prompt_append)
393
+ prompt_array.append(("system", escaped_append))
394
+
395
+ prompt_temp = ChatPromptTemplate.from_messages(prompt_array)
396
+
397
+ async def formatted_prompt(
398
+ state: AgentState, runtime: Runtime[AgentContext]
399
+ ) -> list[BaseMessage]:
400
+ # Get base prompt (with potential admin LLM skill control processing)
401
+ final_system_prompt = await get_base_prompt()
402
+
403
+ context = runtime.context
404
+
405
+ # Add entrypoint prompt if applicable
406
+ entrypoint_prompt = await build_entrypoint_prompt(agent, context)
407
+ if entrypoint_prompt:
408
+ final_system_prompt = (
409
+ f"{final_system_prompt}## Entrypoint rules\n\n{entrypoint_prompt}\n\n"
410
+ )
411
+
412
+ # Add user info if user_id is a valid EVM wallet address
413
+ user_info = _build_user_info_section(context)
414
+ if user_info:
415
+ final_system_prompt = f"{final_system_prompt}{user_info}"
416
+
417
+ # Add internal info
418
+ internal_info = build_internal_info_prompt(context)
419
+ final_system_prompt = f"{final_system_prompt}{internal_info}"
420
+
421
+ # Process prompt_append with admin LLM skill control if needed
422
+ if agent.prompt_append and config.admin_llm_skill_control:
423
+ # Find the system message in prompt_array and process it
424
+ for i, (role, content) in enumerate(prompt_array):
425
+ if role == "system":
426
+ processed_append = await explain_prompt(content)
427
+ prompt_array[i] = ("system", processed_append)
428
+ break
429
+
430
+ system_prompt = [("system", final_system_prompt)]
431
+ return prompt_temp.invoke(
432
+ {
433
+ "messages": state["messages"],
434
+ "system_prompt": system_prompt,
435
+ }
436
+ )
437
+
438
+ return formatted_prompt
intentkit/models/llm.py CHANGED
@@ -623,14 +623,15 @@ class DeepseekLLM(LLMModel):
623
623
  async def create_instance(self, config: Any) -> LanguageModelLike:
624
624
  """Create and return a ChatDeepseek instance."""
625
625
 
626
- from langchain_openai import ChatOpenAI
626
+ from langchain_deepseek import ChatDeepSeek
627
627
 
628
628
  info = await self.model_info()
629
629
 
630
630
  kwargs = {
631
- "model_name": self.model_name,
632
- "openai_api_key": config.deepseek_api_key,
631
+ "model": self.model_name,
632
+ "api_key": config.deepseek_api_key,
633
633
  "timeout": info.timeout,
634
+ "max_retries": 3,
634
635
  }
635
636
 
636
637
  # Add optional parameters based on model support
@@ -644,9 +645,9 @@ class DeepseekLLM(LLMModel):
644
645
  kwargs["presence_penalty"] = self.presence_penalty
645
646
 
646
647
  if info.api_base:
647
- kwargs["openai_api_base"] = info.api_base
648
+ kwargs["api_base"] = info.api_base
648
649
 
649
- return ChatOpenAI(**kwargs)
650
+ return ChatDeepSeek(**kwargs)
650
651
 
651
652
 
652
653
  class XAILLM(LLMModel):
@@ -6,6 +6,8 @@ from typing import TypedDict
6
6
  from intentkit.abstracts.skill import SkillStoreABC
7
7
  from intentkit.skills.base import SkillConfig, SkillState
8
8
  from intentkit.skills.xmtp.base import XmtpBaseTool
9
+ from intentkit.skills.xmtp.price import XmtpGetSwapPrice
10
+ from intentkit.skills.xmtp.swap import XmtpSwap
9
11
  from intentkit.skills.xmtp.transfer import XmtpTransfer
10
12
 
11
13
  # Cache skills at the module level, because they are stateless
@@ -16,6 +18,8 @@ logger = logging.getLogger(__name__)
16
18
 
17
19
  class SkillStates(TypedDict):
18
20
  xmtp_transfer: SkillState
21
+ xmtp_swap: SkillState
22
+ xmtp_get_swap_price: SkillState
19
23
 
20
24
 
21
25
  class Config(SkillConfig):
@@ -77,6 +81,18 @@ def get_xmtp_skill(
77
81
  skill_store=store,
78
82
  )
79
83
  return _cache[name]
84
+ elif name == "xmtp_swap":
85
+ if name not in _cache:
86
+ _cache[name] = XmtpSwap(
87
+ skill_store=store,
88
+ )
89
+ return _cache[name]
90
+ elif name == "xmtp_get_swap_price":
91
+ if name not in _cache:
92
+ _cache[name] = XmtpGetSwapPrice(
93
+ skill_store=store,
94
+ )
95
+ return _cache[name]
80
96
  else:
81
97
  logger.warning(f"Unknown XMTP skill: {name}")
82
98
  return None
@@ -0,0 +1,72 @@
1
+ from typing import Literal, Type
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from intentkit.clients.cdp import get_origin_cdp_client
6
+ from intentkit.skills.xmtp.base import XmtpBaseTool
7
+
8
+
9
+ class SwapPriceInput(BaseModel):
10
+ """Input for querying swap price via CDP."""
11
+
12
+ from_token: str = Field(description="The contract address to swap from")
13
+ to_token: str = Field(description="The contract address to swap to")
14
+ from_amount: str = Field(description="Input amount in smallest units (as string)")
15
+ from_address: str = Field(
16
+ description="The address where the from_token balance is located"
17
+ )
18
+
19
+
20
+ class XmtpGetSwapPrice(XmtpBaseTool):
21
+ """Skill for fetching indicative swap price using CDP SDK."""
22
+
23
+ name: str = "xmtp_get_swap_price"
24
+ description: str = "Get an indicative swap price/quote for token pair and amount on Base networks using CDP."
25
+ response_format: Literal["content", "content_and_artifact"] = "content"
26
+ args_schema: Type[BaseModel] = SwapPriceInput
27
+
28
+ async def _arun(
29
+ self,
30
+ from_token: str,
31
+ to_token: str,
32
+ from_amount: str,
33
+ from_address: str,
34
+ ) -> str:
35
+ context = self.get_context()
36
+ agent = context.agent
37
+
38
+ if agent.network_id not in ("base-mainnet", "base-sepolia"):
39
+ raise ValueError(
40
+ f"Swap price only supported on base-mainnet or base-sepolia. Current: {agent.network_id}"
41
+ )
42
+
43
+ network_for_cdp = {
44
+ "base-mainnet": "base",
45
+ "base-sepolia": "base-sepolia",
46
+ }[agent.network_id]
47
+
48
+ cdp_client = get_origin_cdp_client(self.skill_store)
49
+ # Note: Don't use async with context manager as get_origin_cdp_client returns a managed global client
50
+ price = await cdp_client.evm.get_swap_price(
51
+ from_token=from_token,
52
+ to_token=to_token,
53
+ from_amount=str(from_amount),
54
+ network=network_for_cdp,
55
+ taker=from_address,
56
+ )
57
+
58
+ # Try to format a readable message from typical fields
59
+ try:
60
+ amount_out = getattr(price, "to_amount", None) or (
61
+ price.get("to_amount") if isinstance(price, dict) else None
62
+ )
63
+ route = getattr(price, "route", None) or (
64
+ price.get("route") if isinstance(price, dict) else None
65
+ )
66
+ route_str = f" via {route}" if route else ""
67
+ if amount_out:
68
+ return f"Estimated output: {amount_out} units of {to_token}{route_str} on {agent.network_id}."
69
+ except Exception:
70
+ pass
71
+
72
+ return f"Swap price result (raw): {price}"
@@ -36,6 +36,38 @@
36
36
  ],
37
37
  "description": "Create XMTP transaction requests for transferring ETH or ERC20 tokens on Base mainnet. Supports both native ETH transfers and ERC20 token transfers. Generates wallet_sendCalls transaction data that users can sign.",
38
38
  "default": "disabled"
39
+ },
40
+ "xmtp_swap": {
41
+ "type": "string",
42
+ "title": "XMTP Swap",
43
+ "enum": [
44
+ "disabled",
45
+ "public",
46
+ "private"
47
+ ],
48
+ "x-enum-title": [
49
+ "Disabled",
50
+ "Agent Owner + All Users",
51
+ "Agent Owner Only"
52
+ ],
53
+ "description": "Create XMTP transaction requests for swapping tokens on Base using CDP swap quote. Returns a wallet_sendCalls payload that can include an optional approval call and the swap call. Only supports base-mainnet and base-sepolia.",
54
+ "default": "disabled"
55
+ },
56
+ "xmtp_get_swap_price": {
57
+ "type": "string",
58
+ "title": "XMTP Get Swap Price",
59
+ "enum": [
60
+ "disabled",
61
+ "public",
62
+ "private"
63
+ ],
64
+ "x-enum-title": [
65
+ "Disabled",
66
+ "Agent Owner + All Users",
67
+ "Agent Owner Only"
68
+ ],
69
+ "description": "Get an indicative swap price/quote for token pair and amount on Base networks using CDP. Provides estimated output amounts for token swaps without creating transactions.",
70
+ "default": "disabled"
39
71
  }
40
72
  }
41
73
  }