zico-agent / src /graphs /edges.py
github-actions[bot]
Deploy from GitHub Actions: 25f0efbf9a8fcc4d5894a569297e8aeecffe8b08
156dd84
"""
Conditional edge functions for the StateGraph.
These are pure functions that inspect the AgentState and return
the name of the next node to execute.
"""
from __future__ import annotations
import logging
from src.agents.routing.semantic_router import IntentCategory, SemanticRouter
from src.graphs.state import AgentState
from src.graphs import nodes as _nodes_mod
from src.graphs.utils import (
is_swap_like_request,
is_lending_like_request,
is_staking_like_request,
is_liquidity_like_request,
)
logger = logging.getLogger(__name__)
# Agent name β†’ node name mapping
_INTENT_TO_NODE = {
IntentCategory.SWAP.value: "swap_agent_node",
IntentCategory.LENDING.value: "lending_agent_node",
IntentCategory.STAKING.value: "staking_agent_node",
IntentCategory.LIQUIDITY.value: "liquidity_agent_node",
IntentCategory.DCA.value: "dca_agent_node",
IntentCategory.STRATEGY.value: "strategy_agent_node",
IntentCategory.MARKET_DATA.value: "crypto_agent_node",
IntentCategory.PORTFOLIO.value: "portfolio_advisor_node",
IntentCategory.SEARCH.value: "search_agent_node",
IntentCategory.EDUCATION.value: "default_agent_node",
IntentCategory.GENERAL.value: "default_agent_node",
}
_AGENT_NAME_TO_NODE = {
"swap_agent": "swap_agent_node",
"lending_agent": "lending_agent_node",
"staking_agent": "staking_agent_node",
"liquidity_agent": "liquidity_agent_node",
"dca_agent": "dca_agent_node",
"strategy_agent": "strategy_agent_node",
"crypto_agent": "crypto_agent_node",
"search_agent": "search_agent_node",
"default_agent": "default_agent_node",
"database_agent": "database_agent_node",
"portfolio_advisor": "portfolio_advisor_node",
}
# DeFi state key β†’ node mapping
_DEFI_STATE_NODE = {
"swap_state": "swap_agent_node",
"lending_state": "lending_agent_node",
"staking_state": "staking_agent_node",
"liquidity_state": "liquidity_agent_node",
"dca_state": "dca_agent_node",
"strategy_state": "strategy_agent_node",
}
# DeFi in-progress statuses
_DEFI_ACTIVE_STATUSES = {
"swap_state": {"collecting"},
"lending_state": {"collecting"},
"staking_state": {"collecting"},
"liquidity_state": {"collecting"},
"dca_state": {"consulting", "recommendation", "confirmation"},
"strategy_state": {"profiling", "discovery", "recommendation", "comparison", "confirmation"},
}
_AMBIGUOUS_LIQUIDITY_FOLLOWUPS = {
"ok",
"okay",
"yes",
"yep",
"continue",
"go ahead",
"go",
"next",
"first",
"first one",
"the first one",
"1",
"1st",
"max",
"put max",
"all",
"use max",
"use maximum",
"full",
}
def _route_with_reason(node: str, reason: str, *, intent: str | None = None, confidence: float | None = None) -> str:
logger.info(
"routing.decide node=%s reason=%s intent=%s confidence=%s",
node,
reason,
intent or "none",
f"{confidence:.3f}" if isinstance(confidence, float) else "n/a",
)
return node
def _is_ambiguous_liquidity_followup(message: str) -> bool:
normalized = " ".join((message or "").strip().lower().split())
if not normalized:
return False
if normalized in _AMBIGUOUS_LIQUIDITY_FOLLOWUPS:
return True
if normalized.startswith("max "):
return True
return False
def decide_route(state: AgentState) -> str:
"""
Main routing decision after semantic_router_node.
Priority order:
1. Preflight errors β†’ error_node
2. Active DeFi flow β†’ matching agent node
3. Awaiting swap/DCA followup β†’ matching agent node
4. High confidence (>= 0.78) β†’ direct to agent node
5. DeFi intent + medium confidence (>= 0.50) + keyword match β†’ DeFi agent node
6. Non-DeFi + medium confidence (>= 0.50) β†’ direct to agent node
7. Else β†’ llm_router_node
"""
# 1. Preflight errors
if state.get("preflight_errors"):
return _route_with_reason("error_node", "preflight_errors")
intent = state.get("route_intent")
confidence = state.get("route_confidence", 0.0)
windowed = state.get("windowed_messages", [])
# 2. Active DeFi flow β€” route based on state
for state_key, node_name in _DEFI_STATE_NODE.items():
defi_state = state.get(state_key)
if defi_state:
active_statuses = _DEFI_ACTIVE_STATUSES.get(state_key, set())
if defi_state.get("status") in active_statuses:
return _route_with_reason(node_name, f"active_defi:{state_key}", intent=intent, confidence=confidence)
# 3. Awaiting followup
if state.get("awaiting_swap"):
return _route_with_reason("swap_agent_node", "pending_followup:swap", intent=intent, confidence=confidence)
if state.get("awaiting_dca"):
return _route_with_reason("dca_agent_node", "pending_followup:dca", intent=intent, confidence=confidence)
if state.get("awaiting_liquidity"):
last_user_message = str(state.get("last_user_message") or "")
if _is_ambiguous_liquidity_followup(last_user_message):
return _route_with_reason("liquidity_agent_node", "sticky_followup:liquidity", intent=intent, confidence=confidence)
# 4. High confidence direct routing
if confidence >= SemanticRouter.HIGH_CONFIDENCE and intent:
node = _INTENT_TO_NODE.get(intent, "default_agent_node")
return _route_with_reason(node, "semantic_high_confidence", intent=intent, confidence=confidence)
# 5. DeFi intent + medium confidence + keyword match
if confidence >= SemanticRouter.LOW_CONFIDENCE and intent in ("swap", "lending", "staking", "liquidity", "dca", "strategy"):
# For DeFi, medium confidence + keyword fallback is sufficient
if intent == "swap":
if _nodes_mod.is_swap_like_request(
windowed, _nodes_mod._swap_network_terms, _nodes_mod._swap_token_terms
):
return _route_with_reason("swap_agent_node", "semantic_medium+keyword:swap", intent=intent, confidence=confidence)
# Even without keyword match, semantic router says swap
return _route_with_reason("swap_agent_node", "semantic_medium:swap", intent=intent, confidence=confidence)
if intent == "lending":
return _route_with_reason("lending_agent_node", "semantic_medium:lending", intent=intent, confidence=confidence)
if intent == "staking":
return _route_with_reason("staking_agent_node", "semantic_medium:staking", intent=intent, confidence=confidence)
if intent == "liquidity":
return _route_with_reason("liquidity_agent_node", "semantic_medium:liquidity", intent=intent, confidence=confidence)
if intent == "dca":
return _route_with_reason("dca_agent_node", "semantic_medium:dca", intent=intent, confidence=confidence)
if intent == "strategy":
return _route_with_reason("strategy_agent_node", "semantic_medium:strategy", intent=intent, confidence=confidence)
# 6. Non-DeFi + medium confidence
if confidence >= SemanticRouter.LOW_CONFIDENCE and intent:
node = _INTENT_TO_NODE.get(intent, "default_agent_node")
return _route_with_reason(node, "semantic_medium_non_defi", intent=intent, confidence=confidence)
# 7. Keyword-only fallback (no semantic match)
if is_swap_like_request(
windowed, _nodes_mod._swap_network_terms, _nodes_mod._swap_token_terms
):
return _route_with_reason("swap_agent_node", "keyword_only:swap", intent=intent, confidence=confidence)
if is_lending_like_request(
windowed, _nodes_mod._lending_network_terms, _nodes_mod._lending_asset_terms
):
return _route_with_reason("lending_agent_node", "keyword_only:lending", intent=intent, confidence=confidence)
if is_staking_like_request(windowed):
return _route_with_reason("staking_agent_node", "keyword_only:staking", intent=intent, confidence=confidence)
if is_liquidity_like_request(windowed):
return _route_with_reason("liquidity_agent_node", "keyword_only:liquidity", intent=intent, confidence=confidence)
# 8. Low confidence β†’ LLM router
return _route_with_reason("llm_router_node", "llm_router_low_confidence", intent=intent, confidence=confidence)
def after_llm_router(state: AgentState) -> str:
"""Route after LLM refinement β€” route_agent has been set by llm_router_node."""
agent = state.get("route_agent", "default_agent")
node = _AGENT_NAME_TO_NODE.get(agent, "default_agent_node")
logger.info("routing.after_llm node=%s reason=llm_router agent=%s", node, agent)
return node