github-actions[bot] commited on
Commit
7e3d17c
·
1 Parent(s): bc3dd51

Deploy from GitHub Actions: 347c54a3c8bb23a024f5952b8812538513a507f7

Browse files
src/agents/routing/pre_extractor.py CHANGED
@@ -74,6 +74,19 @@ _SWAP_PATTERN = re.compile(
74
  re.IGNORECASE,
75
  )
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  _LENDING_PATTERN = re.compile(
78
  r"(supply|borrow|repay|withdraw|deposit|lend)\s+"
79
  r"(?:(\d+(?:[.,]\d+)?)\s+)?"
@@ -124,14 +137,28 @@ def pre_extract(text: str, intent: str) -> PreExtractedParams:
124
  params = PreExtractedParams()
125
 
126
  if intent == "swap":
127
- m = _SWAP_PATTERN.search(text)
128
- if m:
129
- params.amount = _safe_decimal(m.group(1))
130
- params.from_token = m.group(2).upper()
131
- params.to_token = m.group(3).upper()
132
- if m.group(4):
133
- params.from_network = m.group(4).lower()
 
134
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  # Try to extract at least amount + token
136
  am = _AMOUNT_TOKEN.search(text)
137
  if am:
@@ -139,7 +166,9 @@ def pre_extract(text: str, intent: str) -> PreExtractedParams:
139
  params.from_token = am.group(2).upper()
140
  nm = _NETWORK_MENTION.search(text)
141
  if nm:
142
- params.from_network = nm.group(1).lower()
 
 
143
 
144
  elif intent == "lending":
145
  m = _LENDING_PATTERN.search(text)
 
74
  re.IGNORECASE,
75
  )
76
 
77
+ # Cross-chain pattern: "swap X from Ethereum to Y on Arbitrum"
78
+ # or "swap X on Ethereum for Y on Arbitrum"
79
+ _CROSS_CHAIN_PATTERN = re.compile(
80
+ r"(?:swap|exchange|convert|trade|troque|trocar)\s+"
81
+ r"(?:(\d+(?:[.,]\d+)?)\s+)?" # optional amount
82
+ r"(\w+)\s+" # from_token
83
+ r"(?:from|on|na|no|em)\s+(\S+)\s+" # from_network
84
+ r"(?:for|to|into|por|para)\s+"
85
+ r"(\w+)" # to_token
86
+ r"(?:\s+(?:on|na|no|em)\s+(\S+))?", # to_network (optional)
87
+ re.IGNORECASE,
88
+ )
89
+
90
  _LENDING_PATTERN = re.compile(
91
  r"(supply|borrow|repay|withdraw|deposit|lend)\s+"
92
  r"(?:(\d+(?:[.,]\d+)?)\s+)?"
 
137
  params = PreExtractedParams()
138
 
139
  if intent == "swap":
140
+ # Try cross-chain pattern first: "swap X from Ethereum to Y on Arbitrum"
141
+ xm = _CROSS_CHAIN_PATTERN.search(text)
142
+ if xm:
143
+ params.amount = _safe_decimal(xm.group(1))
144
+ params.from_token = xm.group(2).upper()
145
+ params.from_network = xm.group(3).lower()
146
+ params.to_token = xm.group(4).upper()
147
+ params.to_network = (xm.group(5) or xm.group(3)).lower()
148
  else:
149
+ # Standard pattern: "swap X ETH to USDC on Base"
150
+ m = _SWAP_PATTERN.search(text)
151
+ if m:
152
+ params.amount = _safe_decimal(m.group(1))
153
+ params.from_token = m.group(2).upper()
154
+ params.to_token = m.group(3).upper()
155
+ if m.group(4):
156
+ network = m.group(4).lower()
157
+ params.from_network = network
158
+ # Same-chain swap default: if only one network is
159
+ # mentioned (e.g. "on Base"), assume both sides use it.
160
+ params.to_network = network
161
+ if not params.has_any():
162
  # Try to extract at least amount + token
163
  am = _AMOUNT_TOKEN.search(text)
164
  if am:
 
166
  params.from_token = am.group(2).upper()
167
  nm = _NETWORK_MENTION.search(text)
168
  if nm:
169
+ network = nm.group(1).lower()
170
+ params.from_network = network
171
+ params.to_network = network
172
 
173
  elif intent == "lending":
174
  m = _LENDING_PATTERN.search(text)
src/agents/swap/agent.py CHANGED
@@ -1,5 +1,6 @@
1
  import logging
2
  from src.agents.swap.tools import get_tools
 
3
  from langgraph.prebuilt import create_react_agent
4
 
5
  logger = logging.getLogger(__name__)
@@ -11,6 +12,6 @@ class SwapAgent:
11
  self.llm = llm
12
  self.agent = create_react_agent(
13
  model=llm,
14
- tools=get_tools(),
15
  name="swap_agent"
16
  )
 
1
  import logging
2
  from src.agents.swap.tools import get_tools
3
+ from src.agents.portfolio.tools import get_user_portfolio_tool
4
  from langgraph.prebuilt import create_react_agent
5
 
6
  logger = logging.getLogger(__name__)
 
12
  self.llm = llm
13
  self.agent = create_react_agent(
14
  model=llm,
15
+ tools=get_tools() + [get_user_portfolio_tool],
16
  name="swap_agent"
17
  )
src/agents/swap/prompt.py CHANGED
@@ -9,32 +9,45 @@ Always respond in English, regardless of the user's language.
9
 
10
  Primary responsibilities:
11
  1. Collect all swap intent fields (`from_network`, `from_token`, `to_network`, `to_token`, `amount`) by invoking the `update_swap_intent` tool.
12
- 2. Use progressive loading: start with the minimum detail you need, then request the remaining fields one at a time.
13
  3. Validate user inputs via tools. Ask the user to pick from the returned choices whenever a value is invalid or missing.
14
  4. Never fabricate swap rates, quotes, or execution results. Only confirm that the intent is ready once the tool reports `event == "swap_intent_ready"`.
15
- 5. Preserve context: if the tool response says the intent is still collecting, summarize what you know, ask the next question, and remind the user how to continue.
16
  6. Reinforce guardrails: warn if the user requests unsupported networks/tokens or amounts outside the allowed range, and guide them back to valid options.
17
 
18
- Interaction pattern:
19
  - ALWAYS call `update_swap_intent` when the user provides new swap information.
20
- - Use `list_networks` or `list_tokens` before suggesting options, so your choices mirror the backend configuration.
21
- - After each tool call, read the returned `event`, `ask`, and `next_action` fields to decide whether to ask follow-up questions or conclude the intent.
 
 
22
 
23
- Example 1 – progressive collection:
 
 
 
 
 
 
24
  User: I want to swap some AVAX to USDC.
25
- Assistant: (call `update_swap_intent` with `from_token="AVAX"`, `to_token="USDC"`)
26
- Tool: `ask` -> "From which network?"
27
- Assistant: "Sure — which network will you be swapping from?"
28
  User: Avalanche, amount is 12.
29
- Assistant: (call `update_swap_intent` with `from_network="avalanche"`, `amount=12`)
30
- Tool: `ask` -> "To which network?"
31
- Assistant: "Got it. Which destination network do you prefer?"
32
 
33
- Example 2validation and completion:
34
  User: Swap 50 USDC from Ethereum to WBTC on Arbitrum.
35
- Assistant: (call `update_swap_intent` with all fields)
36
- Tool: `event` -> `swap_intent_ready`
37
- Assistant: "All set. Ready to swap 50 USDC on Ethereum for WBTC on Arbitrum. Let me know if you want to execute or adjust values."
 
 
 
 
 
38
 
39
- Keep responses concise, reference the remaining required field explicitly, and never skip the tool call even if you believe all details are already known.
40
  {MARKDOWN_INSTRUCTIONS}"""
 
9
 
10
  Primary responsibilities:
11
  1. Collect all swap intent fields (`from_network`, `from_token`, `to_network`, `to_token`, `amount`) by invoking the `update_swap_intent` tool.
12
+ 2. ALWAYS pass ALL fields you can infer from the user message in a SINGLE tool call. Never hold back known fields for a later call.
13
  3. Validate user inputs via tools. Ask the user to pick from the returned choices whenever a value is invalid or missing.
14
  4. Never fabricate swap rates, quotes, or execution results. Only confirm that the intent is ready once the tool reports `event == "swap_intent_ready"`.
15
+ 5. Preserve context: if the tool response says the intent is still collecting, summarize what you know and ask ONLY about the missing field(s).
16
  6. Reinforce guardrails: warn if the user requests unsupported networks/tokens or amounts outside the allowed range, and guide them back to valid options.
17
 
18
+ Critical rules:
19
  - ALWAYS call `update_swap_intent` when the user provides new swap information.
20
+ - Send EVERY field you can extract from the message in one call. For example, if the user says "Swap 0.1 ETH to USDC on Base", you MUST send from_token="ETH", to_token="USDC", amount=0.1, from_network="base", to_network="base" all at once.
21
+ - When the user mentions a single network (e.g. "on Base"), assume both from_network and to_network are the same network unless the user explicitly names two different networks.
22
+ - After each tool call, read the returned `event`, `ask`, and `next_action` fields. If `event` is `swap_intent_ready`, confirm the swap — do NOT ask further questions.
23
+ - Only use `list_networks` or `list_tokens` when the user asks what is available or when you need to present choices for an invalid/missing value.
24
 
25
+ Example 1 – complete in one shot:
26
+ User: Swap 0.1 ETH to USDC on Base.
27
+ Assistant: (call `update_swap_intent` with from_token="ETH", to_token="USDC", amount=0.1, from_network="base", to_network="base")
28
+ Tool: event -> "swap_intent_ready"
29
+ Assistant: "All set! Ready to swap 0.1 ETH for USDC on Base."
30
+
31
+ Example 2 – partial info, minimal follow-up:
32
  User: I want to swap some AVAX to USDC.
33
+ Assistant: (call `update_swap_intent` with from_token="AVAX", to_token="USDC")
34
+ Tool: ask -> "From which network?"
35
+ Assistant: "Which network are you swapping on?"
36
  User: Avalanche, amount is 12.
37
+ Assistant: (call `update_swap_intent` with from_network="avalanche", to_network="avalanche", amount=12)
38
+ Tool: event -> "swap_intent_ready"
39
+ Assistant: "All set! Ready to swap 12 AVAX for USDC on Avalanche."
40
 
41
+ Example 3cross-chain swap:
42
  User: Swap 50 USDC from Ethereum to WBTC on Arbitrum.
43
+ Assistant: (call `update_swap_intent` with from_token="USDC", to_token="WBTC", amount=50, from_network="ethereum", to_network="arbitrum")
44
+ Tool: event -> "swap_intent_ready"
45
+ Assistant: "All set! Ready to swap 50 USDC on Ethereum for WBTC on Arbitrum."
46
+
47
+ Balance / Portfolio queries:
48
+ - If the user asks about their balance, holdings, or "how much do I have", call `get_user_portfolio` to fetch their wallet balances.
49
+ - Keep the balance response concise — show only the relevant token(s) and total value. The user is mid-swap, not requesting a full portfolio analysis.
50
+ - After answering the balance question, resume collecting any missing swap fields.
51
 
52
+ Keep responses concise. Never ask for a field the user already provided.
53
  {MARKDOWN_INSTRUCTIONS}"""
src/agents/swap/tools.py CHANGED
@@ -461,6 +461,13 @@ def update_swap_intent_tool(
461
  except ValueError:
462
  intent.to_token = None
463
 
 
 
 
 
 
 
 
464
  if intent.to_network is None and to_token is not None:
465
  return _response(
466
  intent,
 
461
  except ValueError:
462
  intent.to_token = None
463
 
464
+ # Same-chain swap heuristic: if from_network is set but to_network
465
+ # is missing, default to the same network. This avoids an extra
466
+ # round-trip for the very common case where the user mentions only
467
+ # one network (e.g. "swap 0.1 ETH to USDC on Base").
468
+ if intent.from_network and not intent.to_network and to_network is None:
469
+ intent.to_network = intent.from_network
470
+
471
  if intent.to_network is None and to_token is not None:
472
  return _response(
473
  intent,
src/graphs/nodes.py CHANGED
@@ -29,6 +29,7 @@ from src.graphs.utils import (
29
  detect_pending_followups,
30
  extract_response_from_graph,
31
  get_text_content,
 
32
  )
33
 
34
  # --- Agent imports ---
@@ -402,7 +403,66 @@ def _invoke_defi_agent(
402
 
403
 
404
  def swap_agent_node(state: AgentState, config: RunnableConfig | None = None) -> dict:
405
- return _invoke_defi_agent("swap_agent", SWAP_AGENT_SYSTEM_PROMPT, swap_session, state, "swap", config)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
 
407
 
408
  def lending_agent_node(state: AgentState, config: RunnableConfig | None = None) -> dict:
 
29
  detect_pending_followups,
30
  extract_response_from_graph,
31
  get_text_content,
32
+ is_swap_like_request,
33
  )
34
 
35
  # --- Agent imports ---
 
403
 
404
 
405
  def swap_agent_node(state: AgentState, config: RunnableConfig | None = None) -> dict:
406
+ """Invoke swap agent with both swap and portfolio session contexts.
407
+
408
+ The portfolio session gives the swap agent access to ``get_user_portfolio``
409
+ so users can check balances mid-swap without leaving the flow.
410
+ """
411
+ user_id = state.get("user_id")
412
+ conversation_id = state.get("conversation_id")
413
+ wallet_address = state.get("wallet_address")
414
+ langchain_messages = list(state.get("langchain_messages", []))
415
+ nodes = list(state.get("nodes_executed", []))
416
+ nodes.append("swap_agent_node")
417
+
418
+ agent = _agents.get("swap_agent")
419
+ if not agent:
420
+ return {
421
+ "final_response": "Agent not available.",
422
+ "response_agent": "swap_agent",
423
+ "response_metadata": {},
424
+ "raw_agent_messages": [],
425
+ "nodes_executed": nodes,
426
+ }
427
+
428
+ # Inject system prompt + DeFi guidance
429
+ scoped_messages = [SystemMessage(content=SWAP_AGENT_SYSTEM_PROMPT)]
430
+
431
+ defi_state = state.get("swap_state")
432
+ guidance = build_defi_guidance("swap", defi_state)
433
+ if guidance:
434
+ scoped_messages.append(SystemMessage(content=guidance))
435
+
436
+ hint = state.get("pre_extracted_hint")
437
+ if hint:
438
+ scoped_messages.append(SystemMessage(content=hint))
439
+
440
+ scoped_messages.extend(langchain_messages)
441
+
442
+ try:
443
+ with swap_session(user_id=user_id, conversation_id=conversation_id):
444
+ with portfolio_session(user_id=user_id, conversation_id=conversation_id, wallet_address=wallet_address):
445
+ response = agent.invoke({"messages": scoped_messages}, config=config)
446
+ except Exception:
447
+ logger.exception("Error invoking swap_agent")
448
+ return {
449
+ "final_response": "Sorry, an error occurred while processing your request.",
450
+ "response_agent": "swap_agent",
451
+ "response_metadata": {},
452
+ "raw_agent_messages": [],
453
+ "nodes_executed": nodes,
454
+ }
455
+
456
+ agent_name, text, messages_out = extract_response_from_graph(response)
457
+ meta = build_metadata(agent_name or "swap_agent", user_id, conversation_id, messages_out)
458
+
459
+ return {
460
+ "final_response": text,
461
+ "response_agent": agent_name or "swap_agent",
462
+ "response_metadata": meta,
463
+ "raw_agent_messages": messages_out,
464
+ "nodes_executed": nodes,
465
+ }
466
 
467
 
468
  def lending_agent_node(state: AgentState, config: RunnableConfig | None = None) -> dict: