Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
7e3d17c
1
Parent(s): bc3dd51
Deploy from GitHub Actions: 347c54a3c8bb23a024f5952b8812538513a507f7
Browse files- src/agents/routing/pre_extractor.py +37 -8
- src/agents/swap/agent.py +2 -1
- src/agents/swap/prompt.py +30 -17
- src/agents/swap/tools.py +7 -0
- src/graphs/nodes.py +61 -1
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 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
params.
|
| 131 |
-
params.
|
| 132 |
-
|
| 133 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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.
|
| 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
|
| 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 |
-
|
| 19 |
- ALWAYS call `update_swap_intent` when the user provides new swap information.
|
| 20 |
-
-
|
| 21 |
-
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
Example 1 –
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
User: I want to swap some AVAX to USDC.
|
| 25 |
-
Assistant: (call `update_swap_intent` with
|
| 26 |
-
Tool:
|
| 27 |
-
Assistant: "
|
| 28 |
User: Avalanche, amount is 12.
|
| 29 |
-
Assistant: (call `update_swap_intent` with
|
| 30 |
-
Tool:
|
| 31 |
-
Assistant: "
|
| 32 |
|
| 33 |
-
Example
|
| 34 |
User: Swap 50 USDC from Ethereum to WBTC on Arbitrum.
|
| 35 |
-
Assistant: (call `update_swap_intent` with
|
| 36 |
-
Tool:
|
| 37 |
-
Assistant: "All set
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
Keep responses concise
|
| 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 3 – cross-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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|