ColettoG commited on
Commit
70d4fac
·
1 Parent(s): 5fdbe77

update lending agent

Browse files
src/agents/lending/__init__.py ADDED
File without changes
src/agents/lending/agent.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from src.agents.lending.tools import get_tools
3
+ from langgraph.prebuilt import create_react_agent
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class LendingAgent:
9
+ """Agent for handling lending operations (supply, borrow, repay, withdraw)."""
10
+ def __init__(self, llm):
11
+ self.llm = llm
12
+ self.agent = create_react_agent(
13
+ model=llm,
14
+ tools=get_tools(),
15
+ name="lending_agent"
16
+ )
src/agents/lending/prompt.py CHANGED
@@ -1,9 +1,11 @@
1
  """System prompt for the specialized lending agent."""
2
 
3
- LENDING_AGENT_SYSTEM_PROMPT = \"\"\"
4
  You are Zico's lending orchestrator.
5
  Your goal is to help the user define a lending operation (supply, borrow, repay, withdraw) by collecting the necessary details.
6
 
 
 
7
  # Responsibilities
8
  1. Collect all lending intent fields (`action`, `network`, `asset`, `amount`) by invoking the `update_lending_intent` tool.
9
  2. If the tool returns a question (`ask`), present it to the user clearly.
@@ -31,4 +33,4 @@ User: 100.
31
  Assistant: (call `update_lending_intent` with `amount=100`)
32
  Tool: `event` -> `lending_intent_ready`
33
  Assistant: "All set. Ready to supply 100 USDC on Arbitrum."
34
- \"\"\"
 
1
  """System prompt for the specialized lending agent."""
2
 
3
+ LENDING_AGENT_SYSTEM_PROMPT = """
4
  You are Zico's lending orchestrator.
5
  Your goal is to help the user define a lending operation (supply, borrow, repay, withdraw) by collecting the necessary details.
6
 
7
+ Always respond in English, regardless of the user's language.
8
+
9
  # Responsibilities
10
  1. Collect all lending intent fields (`action`, `network`, `asset`, `amount`) by invoking the `update_lending_intent` tool.
11
  2. If the tool returns a question (`ask`), present it to the user clearly.
 
33
  Assistant: (call `update_lending_intent` with `amount=100`)
34
  Tool: `event` -> `lending_intent_ready`
35
  Assistant: "All set. Ready to supply 100 USDC on Arbitrum."
36
+ """
src/agents/supervisor/agent.py CHANGED
@@ -19,6 +19,10 @@ from src.agents.swap.prompt import SWAP_AGENT_SYSTEM_PROMPT
19
  from src.agents.dca.agent import DcaAgent
20
  from src.agents.dca.tools import dca_session
21
  from src.agents.dca.prompt import DCA_AGENT_SYSTEM_PROMPT
 
 
 
 
22
  from src.agents.search.agent import SearchAgent
23
  from src.agents.database.client import is_database_available
24
 
@@ -75,6 +79,13 @@ class Supervisor:
75
  "- dca_agent: Plans DCA swap workflows, consulting strategy docs, validating parameters, and confirming automation metadata.\n"
76
  )
77
 
 
 
 
 
 
 
 
78
  searchAgent = SearchAgent(llm)
79
  self.search_agent = searchAgent.agent
80
  agents.append(self.search_agent)
@@ -87,8 +98,8 @@ class Supervisor:
87
  agents.append(self.default_agent)
88
 
89
  # Track known agent names for response extraction
90
- self.known_agent_names = {"crypto_agent", "database_agent", "swap_agent", "dca_agent", "search_agent", "default_agent"}
91
- self.specialized_agents = {"crypto_agent", "database_agent", "swap_agent", "dca_agent", "search_agent"}
92
  self.failure_markers = (
93
  "cannot fulfill",
94
  "can't fulfill",
@@ -193,6 +204,13 @@ Examples of dca queries to delegate:
193
  - Suggest a weekly swap DCA strategy
194
  - I want to automate a monthly swap from token A to token B
195
 
 
 
 
 
 
 
 
196
  When a swap conversation is already underway (the user is still providing swap
197
  details or the swap_agent requested follow-up information), keep routing those
198
  messages to the swap_agent until it has gathered every field and signals the
@@ -200,6 +218,8 @@ swap intent is ready.
200
 
201
  When a DCA conversation is already underway (the user is reviewing strategy recommendations or adjusting schedule parameters), keep routing messages to the dca_agent until the workflow is confirmed or cancelled.
202
 
 
 
203
  {database_examples}
204
 
205
  {search_examples}
@@ -221,6 +241,7 @@ Examples of general queries to handle directly:
221
  self.app = self.supervisor.compile()
222
 
223
  self._swap_network_terms, self._swap_token_terms = self._build_swap_detection_terms()
 
224
 
225
  def _is_handoff_text(self, text: str) -> bool:
226
  if not text:
@@ -364,6 +385,25 @@ Examples of general queries to handle directly:
364
  agent, text, messages_out = self._extract_response_from_graph(response)
365
  return agent, text, messages_out
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  def _extract_response_from_graph(self, response: Any) -> Tuple[str, str, list]:
368
  messages_out = response.get("messages", []) if isinstance(response, dict) else []
369
  final_response = None
@@ -526,6 +566,22 @@ Examples of general queries to handle directly:
526
  else:
527
  dca_meta = dca_meta.copy()
528
  return dca_meta if dca_meta else {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  if agent_name == "crypto_agent":
530
  tool_meta = self._collect_tool_metadata(messages_out)
531
  if tool_meta:
@@ -577,6 +633,53 @@ Examples of general queries to handle directly:
577
  return True
578
  return False
579
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  def invoke(
581
  self,
582
  messages: List[ChatMessage],
@@ -603,6 +706,22 @@ Examples of general queries to handle directly:
603
 
604
  dca_state = metadata.get_dca_agent(user_id=user_id, conversation_id=conversation_id)
605
  swap_state = metadata.get_swap_agent(user_id=user_id, conversation_id=conversation_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
 
607
  if not swap_state and self._is_swap_like_request(messages):
608
  swap_result = self._invoke_swap_agent(langchain_messages)
@@ -701,11 +820,40 @@ Examples of general queries to handle directly:
701
  "metadata": meta,
702
  }
703
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  try:
705
  with swap_session(user_id=user_id, conversation_id=conversation_id):
706
  with dca_session(user_id=user_id, conversation_id=conversation_id):
707
- response = self.app.invoke({"messages": langchain_messages})
708
- print("DEBUG: response", response)
 
709
  except Exception as e:
710
  print(f"Error in Supervisor: {e}")
711
  return {
 
19
  from src.agents.dca.agent import DcaAgent
20
  from src.agents.dca.tools import dca_session
21
  from src.agents.dca.prompt import DCA_AGENT_SYSTEM_PROMPT
22
+ from src.agents.lending.agent import LendingAgent
23
+ from src.agents.lending.tools import lending_session
24
+ from src.agents.lending.prompt import LENDING_AGENT_SYSTEM_PROMPT
25
+ from src.agents.lending.config import LendingConfig
26
  from src.agents.search.agent import SearchAgent
27
  from src.agents.database.client import is_database_available
28
 
 
79
  "- dca_agent: Plans DCA swap workflows, consulting strategy docs, validating parameters, and confirming automation metadata.\n"
80
  )
81
 
82
+ lendingAgent = LendingAgent(llm)
83
+ self.lending_agent = lendingAgent.agent
84
+ agents.append(self.lending_agent)
85
+ available_agents_text += (
86
+ "- lending_agent: Handles lending operations (supply, borrow, repay, withdraw) on DeFi protocols like Aave.\n"
87
+ )
88
+
89
  searchAgent = SearchAgent(llm)
90
  self.search_agent = searchAgent.agent
91
  agents.append(self.search_agent)
 
98
  agents.append(self.default_agent)
99
 
100
  # Track known agent names for response extraction
101
+ self.known_agent_names = {"crypto_agent", "database_agent", "swap_agent", "dca_agent", "lending_agent", "search_agent", "default_agent"}
102
+ self.specialized_agents = {"crypto_agent", "database_agent", "swap_agent", "dca_agent", "lending_agent", "search_agent"}
103
  self.failure_markers = (
104
  "cannot fulfill",
105
  "can't fulfill",
 
204
  - Suggest a weekly swap DCA strategy
205
  - I want to automate a monthly swap from token A to token B
206
 
207
+ Examples of lending queries to delegate:
208
+ - I want to supply USDC
209
+ - I want to borrow ETH on Arbitrum
210
+ - Help me with a lending operation
211
+ - I want to make a supply of 100 USDC
212
+ - Withdraw my WETH from the lending protocol
213
+
214
  When a swap conversation is already underway (the user is still providing swap
215
  details or the swap_agent requested follow-up information), keep routing those
216
  messages to the swap_agent until it has gathered every field and signals the
 
218
 
219
  When a DCA conversation is already underway (the user is reviewing strategy recommendations or adjusting schedule parameters), keep routing messages to the dca_agent until the workflow is confirmed or cancelled.
220
 
221
+ When a lending conversation is already underway (the user is providing lending details like asset, amount, network), keep routing messages to the lending_agent until the lending intent is ready or cancelled.
222
+
223
  {database_examples}
224
 
225
  {search_examples}
 
241
  self.app = self.supervisor.compile()
242
 
243
  self._swap_network_terms, self._swap_token_terms = self._build_swap_detection_terms()
244
+ self._lending_network_terms, self._lending_asset_terms = self._build_lending_detection_terms()
245
 
246
  def _is_handoff_text(self, text: str) -> bool:
247
  if not text:
 
385
  agent, text, messages_out = self._extract_response_from_graph(response)
386
  return agent, text, messages_out
387
 
388
+ def _invoke_lending_agent(self, langchain_messages):
389
+ scoped_messages = [SystemMessage(content=LENDING_AGENT_SYSTEM_PROMPT)]
390
+ scoped_messages.extend(langchain_messages)
391
+ try:
392
+ with lending_session(
393
+ user_id=self._active_user_id,
394
+ conversation_id=self._active_conversation_id,
395
+ ):
396
+ response = self.lending_agent.invoke({"messages": scoped_messages})
397
+ except Exception as exc:
398
+ print(f"Error invoking lending agent directly: {exc}")
399
+ return None
400
+
401
+ if not response:
402
+ return None
403
+
404
+ agent, text, messages_out = self._extract_response_from_graph(response)
405
+ return agent, text, messages_out
406
+
407
  def _extract_response_from_graph(self, response: Any) -> Tuple[str, str, list]:
408
  messages_out = response.get("messages", []) if isinstance(response, dict) else []
409
  final_response = None
 
566
  else:
567
  dca_meta = dca_meta.copy()
568
  return dca_meta if dca_meta else {}
569
+ if agent_name == "lending_agent":
570
+ lending_meta = metadata.get_lending_agent(
571
+ user_id=self._active_user_id,
572
+ conversation_id=self._active_conversation_id,
573
+ )
574
+ if lending_meta:
575
+ history = metadata.get_lending_history(
576
+ user_id=self._active_user_id,
577
+ conversation_id=self._active_conversation_id,
578
+ )
579
+ if history:
580
+ lending_meta = lending_meta.copy()
581
+ lending_meta.setdefault("history", history)
582
+ else:
583
+ lending_meta = lending_meta.copy()
584
+ return lending_meta if lending_meta else {}
585
  if agent_name == "crypto_agent":
586
  tool_meta = self._collect_tool_metadata(messages_out)
587
  if tool_meta:
 
633
  return True
634
  return False
635
 
636
+ def _build_lending_detection_terms(self) -> tuple[set[str], set[str]]:
637
+ networks: set[str] = set()
638
+ assets: set[str] = set()
639
+ try:
640
+ for net in LendingConfig.list_networks():
641
+ lowered = net.lower()
642
+ networks.add(lowered)
643
+ try:
644
+ for asset in LendingConfig.list_assets(net):
645
+ assets.add(asset.lower())
646
+ except ValueError:
647
+ continue
648
+ except Exception:
649
+ return set(), set()
650
+ return networks, assets
651
+
652
+ def _is_lending_like_request(self, messages: List[ChatMessage]) -> bool:
653
+ for msg in reversed(messages):
654
+ if msg.get("role") != "user":
655
+ continue
656
+ content = (msg.get("content") or "").strip()
657
+ if not content:
658
+ continue
659
+ lowered = content.lower()
660
+ lending_keywords = (
661
+ "lend",
662
+ "lending",
663
+ "supply",
664
+ "borrow",
665
+ "repay",
666
+ "withdraw",
667
+ "deposit",
668
+ "aave",
669
+ "compound",
670
+ )
671
+ if not any(keyword in lowered for keyword in lending_keywords):
672
+ return False
673
+ if any(term and term in lowered for term in self._lending_network_terms):
674
+ return True
675
+ if any(term and term in lowered for term in self._lending_asset_terms):
676
+ return True
677
+ if any(ch.isdigit() for ch in lowered):
678
+ return True
679
+ # Default to True if the user explicitly mentioned lending-related keywords
680
+ return True
681
+ return False
682
+
683
  def invoke(
684
  self,
685
  messages: List[ChatMessage],
 
706
 
707
  dca_state = metadata.get_dca_agent(user_id=user_id, conversation_id=conversation_id)
708
  swap_state = metadata.get_swap_agent(user_id=user_id, conversation_id=conversation_id)
709
+ lending_state = metadata.get_lending_agent(user_id=user_id, conversation_id=conversation_id)
710
+
711
+ # Check for new lending request first
712
+ if not lending_state and self._is_lending_like_request(messages):
713
+ lending_result = self._invoke_lending_agent(langchain_messages)
714
+ if lending_result:
715
+ final_agent, cleaned_response, messages_out = lending_result
716
+ meta = self._build_metadata(final_agent, messages_out)
717
+ self._active_user_id = None
718
+ self._active_conversation_id = None
719
+ return {
720
+ "messages": messages_out,
721
+ "agent": final_agent,
722
+ "response": cleaned_response or "Sorry, no meaningful response was returned.",
723
+ "metadata": meta,
724
+ }
725
 
726
  if not swap_state and self._is_swap_like_request(messages):
727
  swap_result = self._invoke_swap_agent(langchain_messages)
 
820
  "metadata": meta,
821
  }
822
 
823
+ # Handle in-progress lending flow
824
+ if lending_state and lending_state.get("status") == "collecting":
825
+ lending_result = self._invoke_lending_agent(langchain_messages)
826
+ if lending_result:
827
+ final_agent, cleaned_response, messages_out = lending_result
828
+ meta = self._build_metadata(final_agent, messages_out)
829
+ self._active_user_id = None
830
+ self._active_conversation_id = None
831
+ return {
832
+ "messages": messages_out,
833
+ "agent": final_agent,
834
+ "response": cleaned_response or "Sorry, no meaningful response was returned.",
835
+ "metadata": meta,
836
+ }
837
+ # If direct lending invocation failed, fall through to supervisor graph with hints
838
+ next_field = lending_state.get("next_field")
839
+ pending_question = lending_state.get("pending_question")
840
+ guidance_parts = [
841
+ "There is an in-progress lending intent for this conversation.",
842
+ "Keep routing messages to the lending_agent until the intent is complete unless the user explicitly cancels or changes topic.",
843
+ ]
844
+ if next_field:
845
+ guidance_parts.append(f"The next field to collect is: {next_field}.")
846
+ if pending_question:
847
+ guidance_parts.append(f"Continue the lending flow by asking: {pending_question}")
848
+ guidance_text = " ".join(guidance_parts)
849
+ langchain_messages.insert(0, SystemMessage(content=guidance_text))
850
+
851
  try:
852
  with swap_session(user_id=user_id, conversation_id=conversation_id):
853
  with dca_session(user_id=user_id, conversation_id=conversation_id):
854
+ with lending_session(user_id=user_id, conversation_id=conversation_id):
855
+ response = self.app.invoke({"messages": langchain_messages})
856
+ print("DEBUG: response", response)
857
  except Exception as e:
858
  print(f"Error in Supervisor: {e}")
859
  return {