Gankit12 commited on
Commit
cf063c1
·
1 Parent(s): ed26b37

addding honeypot

Browse files
Files changed (1) hide show
  1. app/agent/honeypot.py +144 -83
app/agent/honeypot.py CHANGED
@@ -15,7 +15,7 @@ Acceptance Criteria:
15
 
16
  import os
17
  import re
18
- from typing import Dict, List, Optional, TypedDict, Any, Literal
19
  from datetime import datetime
20
 
21
  from app.config import settings
@@ -47,6 +47,7 @@ class HoneypotState(TypedDict, total=False):
47
  persona: Active persona name
48
  max_turns_reached: Whether max turns limit was hit
49
  terminated: Whether the conversation has ended
 
50
  """
51
 
52
  messages: List[Dict]
@@ -59,6 +60,7 @@ class HoneypotState(TypedDict, total=False):
59
  persona: str
60
  max_turns_reached: bool
61
  terminated: bool
 
62
 
63
 
64
  class HoneypotAgent:
@@ -210,7 +212,7 @@ class HoneypotAgent:
210
  state: Current honeypot state
211
 
212
  Returns:
213
- Dict with updated messages list
214
  """
215
  from app.agent.personas import get_persona_prompt
216
  from app.agent.prompts import get_system_prompt
@@ -221,6 +223,7 @@ class HoneypotAgent:
221
  strategy = state.get("strategy", "build_trust")
222
  turn_count = state.get("turn_count", 1)
223
  messages = state.get("messages", [])
 
224
 
225
  # Get last scammer message
226
  scammer_messages = [m for m in messages if m.get("sender") == "scammer"]
@@ -241,10 +244,13 @@ class HoneypotAgent:
241
  "phishing_links": [],
242
  }
243
 
244
- # Generate response - pass current intel so we don't repeat questions
 
 
 
245
  if self.llm is not None:
246
  try:
247
- agent_message = self._generate_llm_response(
248
  persona=persona,
249
  language=language,
250
  strategy=strategy,
@@ -252,17 +258,26 @@ class HoneypotAgent:
252
  last_message=last_message,
253
  messages=messages,
254
  extracted_intel=current_intel,
 
255
  )
256
  except Exception as e:
257
  logger.error(f"LLM generation failed: {e}")
258
- agent_message = self._generate_fallback_response(
259
- persona, language, strategy, turn_count, last_message, messages, current_intel
260
  )
261
  else:
262
- agent_message = self._generate_fallback_response(
263
- persona, language, strategy, turn_count, last_message, messages, current_intel
264
  )
265
 
 
 
 
 
 
 
 
 
266
  # Add to conversation
267
  new_message = {
268
  "turn": turn_count,
@@ -274,7 +289,7 @@ class HoneypotAgent:
274
  updated_messages = messages.copy()
275
  updated_messages.append(new_message)
276
 
277
- return {"messages": updated_messages}
278
 
279
  def _generate_llm_response(
280
  self,
@@ -285,7 +300,8 @@ class HoneypotAgent:
285
  last_message: str,
286
  messages: List[Dict],
287
  extracted_intel: Dict = None,
288
- ) -> str:
 
289
  """
290
  Generate response using the LLM.
291
 
@@ -297,9 +313,10 @@ class HoneypotAgent:
297
  last_message: Last scammer message
298
  messages: Full conversation history
299
  extracted_intel: Already extracted intelligence to avoid redundant questions
 
300
 
301
  Returns:
302
- Generated agent response
303
  """
304
  from app.agent.prompts import (
305
  get_system_prompt, is_greeting_message, get_greeting_response,
@@ -307,16 +324,19 @@ class HoneypotAgent:
307
  )
308
  from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
309
 
 
 
 
310
  # Check if this is just a greeting (works for first few turns)
311
  if turn_count <= 2 and is_greeting_message(last_message):
312
  logger.debug(f"Detected greeting message at turn {turn_count}, responding naturally")
313
- return get_greeting_response(language, turn_count)
314
 
315
  # Check if scammer provided an invalid phone number
316
  phone_in_message = extract_phone_from_message(last_message)
317
  if phone_in_message and not validate_phone_number(phone_in_message):
318
  logger.debug(f"Detected invalid phone number: {phone_in_message}")
319
- return get_invalid_phone_response(language)
320
 
321
  # Build context about what we already have
322
  intel_context = ""
@@ -390,11 +410,11 @@ class HoneypotAgent:
390
  generated = str(response)
391
 
392
  # CRITICAL: Filter out bot-like responses and replace with context-aware ones
393
- # Pass extracted intel so we know what NOT to ask for again
394
- natural_response = self._filter_bot_response(
395
- generated, turn_count, language, last_message, messages, extracted_intel
396
  )
397
- return natural_response
398
 
399
  def _filter_bot_response(
400
  self,
@@ -404,7 +424,8 @@ class HoneypotAgent:
404
  last_message: str,
405
  messages: List[Dict] = None,
406
  extracted_intel: Dict = None,
407
- ) -> str:
 
408
  """
409
  Filter out bot-like responses and replace with CONTEXT-AWARE responses.
410
 
@@ -418,12 +439,31 @@ class HoneypotAgent:
418
  last_message: Scammer's last message
419
  messages: Full conversation history for context detection
420
  extracted_intel: Already extracted intelligence to avoid asking again
 
421
 
422
  Returns:
423
- Filtered response that is contextually appropriate
 
 
424
  """
425
  import random
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  response_lower = response.lower()
428
  last_msg_lower = last_message.lower()
429
 
@@ -500,7 +540,7 @@ class HoneypotAgent:
500
 
501
  # If response is good and not redundant/suspicious, return it
502
  if not is_redundant and not is_suspicious:
503
- return response
504
 
505
  logger.warning(f"Filtering response: '{response[:50]}...' (redundant={is_redundant}, suspicious={is_suspicious})")
506
 
@@ -534,7 +574,8 @@ class HoneypotAgent:
534
  "Forget OTP! Give me bank account, I'll do NEFT transfer directly!",
535
  "This OTP is not working! Tell me another way to send the money!",
536
  ]
537
- return random.choice(alternatives)
 
538
 
539
  # PRIORITY 1: Respond to scammer's question/confusion FIRST
540
  if scammer_asking_for_number:
@@ -546,7 +587,8 @@ class HoneypotAgent:
546
  "I want to save YOUR number! What is your phone number?",
547
  "Your contact number! Tell me, I'll save it and then send money!",
548
  ]
549
- return random.choice(alternatives)
 
550
 
551
  if scammer_confused:
552
  # Scammer is confused - clarify what we want
@@ -574,7 +616,8 @@ class HoneypotAgent:
574
  "Sorry, I don't understand technology well. Guide me step by step!",
575
  "Can you explain again? I really want to do this correctly!",
576
  ]
577
- return random.choice(alternatives)
 
578
 
579
  if scammer_said_already_told:
580
  # Scammer frustrated that they already gave info - acknowledge and proceed
@@ -621,7 +664,8 @@ class HoneypotAgent:
621
  "Apologies! I noted it wrong. Let me try again!",
622
  "Yes, I remember now! Processing the payment...",
623
  ]
624
- return random.choice(alternatives)
 
625
 
626
  # PRIORITY 2: Respond based on what scammer just provided
627
  if gave_upi_now:
@@ -650,7 +694,8 @@ class HoneypotAgent:
650
  "Noted! Last thing - confirm the beneficiary name?",
651
  "OK! What name should I put for the transfer?",
652
  ]
653
- return random.choice(alternatives)
 
654
 
655
  if gave_number_now:
656
  # They just gave phone number - acknowledge and ask for what we don't have
@@ -678,7 +723,8 @@ class HoneypotAgent:
678
  "Perfect! Confirm full name as per bank records?",
679
  "Got it! What name will show on my transaction?",
680
  ]
681
- return random.choice(alternatives)
 
682
 
683
  # PRIORITY 3: Generate response based on what we still need
684
  # CORRECT ORDER: UPI -> Phone -> Bank Account -> IFSC -> Name (once only)
@@ -735,7 +781,8 @@ class HoneypotAgent:
735
  "OK! Making the payment with these details.",
736
  ]
737
 
738
- return random.choice(alternatives)
 
739
 
740
  def _generate_fallback_response(
741
  self,
@@ -746,7 +793,8 @@ class HoneypotAgent:
746
  last_message: str = "",
747
  all_messages: List[Dict] = None,
748
  extracted_intel: Dict = None,
749
- ) -> str:
 
750
  """
751
  Generate fallback response when LLM is unavailable.
752
 
@@ -763,9 +811,10 @@ class HoneypotAgent:
763
  last_message: The last message from scammer
764
  all_messages: Full conversation history for context detection
765
  extracted_intel: Already extracted info to avoid redundant questions
 
766
 
767
  Returns:
768
- Fallback response string (varies by turn and scam type)
769
  """
770
  import random
771
  from app.agent.strategies import get_example_response, get_context_aware_response
@@ -775,15 +824,26 @@ class HoneypotAgent:
775
  extract_phone_from_message, validate_phone_number, get_invalid_phone_response
776
  )
777
 
 
 
 
 
 
 
 
 
 
 
 
778
  # Check if this is just a greeting - respond naturally
779
  if turn_count <= 2 and last_message and is_greeting_message(last_message):
780
- return get_greeting_response(language, turn_count)
781
 
782
  # Check if scammer provided an invalid phone number
783
  if last_message:
784
  phone_in_message = extract_phone_from_message(last_message)
785
  if phone_in_message and not validate_phone_number(phone_in_message):
786
- return get_invalid_phone_response(language)
787
 
788
  # Track what we already have
789
  has_upi = bool(extracted_intel and extracted_intel.get("upi_ids"))
@@ -816,158 +876,158 @@ class HoneypotAgent:
816
  # PRIORITY 0: Handle OTP requests with varied responses
817
  if scammer_asking_otp:
818
  if otp_ask_count <= 1:
819
- return random.choice([
820
  "OTP? I didn't receive any message. Can you send it again?",
821
  "I don't see any OTP on my phone. Where does it come from?",
822
  "What OTP? Nothing came to my phone. Please resend!",
823
- ])
824
  elif otp_ask_count <= 3:
825
- return random.choice([
826
  "Still no OTP! Give me your UPI, I'll send money directly!",
827
  "OTP not coming! Can you call me? What's your number?",
828
  "OTP still not received! Let me do bank transfer - give me account!",
829
- ])
830
  else:
831
- return random.choice([
832
  "Sir, no OTP even now! Just give me UPI or bank account!",
833
  "Forget OTP! Tell me where to send money - UPI or bank!",
834
  "OTP problem! Let me call you - what's your number?",
835
- ])
 
 
836
 
837
  # PRIORITY 1: Handle scammer's confusion or frustration
838
  if scammer_confused:
839
  if not has_upi:
840
- return random.choice([
841
  "I want to send you money! Just tell me your UPI ID!",
842
  "Sir, where should I send the payment? Give me UPI ID!",
843
  "I'm ready to pay! Just tell me where - what's your UPI?",
844
- ])
845
  elif not has_phone:
846
- return random.choice([
847
  "I want your phone number to call if there's problem!",
848
  "Give me your number so I can confirm the payment!",
849
  "What's your phone number? I'll call you after sending!",
850
- ])
851
  else:
852
- return random.choice([
853
  "I'm trying to help you! What should I do next?",
854
  "Tell me clearly - what exactly do you need from me?",
855
  "Can you explain again? I really want to do this correctly!",
856
- ])
 
 
857
 
858
  if scammer_said_already:
859
  # When scammer says "I already sent", check what we ACTUALLY have
860
  if has_bank and has_ifsc:
861
  # We have everything needed for bank transfer
862
- return random.choice([
863
  "Yes yes, sorry! I see all the details now. Sending the payment!",
864
  "Okay okay, I found everything! Processing the transfer now!",
865
  "Apologies! I have account and IFSC. Making the payment now!",
866
  "Yes, I see it now! Account number and IFSC noted. Transferring...",
867
- ])
868
  elif has_bank and not has_ifsc:
869
  # We have bank but need IFSC
870
- return random.choice([
871
  "Yes, I found the account! Just need IFSC code to complete the transfer.",
872
  "Sorry, I see the account now! What's the IFSC code?",
873
  "Got the account! My bank needs IFSC. Please share!",
874
- ])
875
  elif has_upi and has_phone and not has_bank:
876
  # We have UPI and phone, need bank
877
- return random.choice([
878
  "Yes, I have UPI and phone! But UPI is failing. Give me bank account?",
879
  "Sorry, I found UPI! But it's showing error. What's your account number?",
880
  "Got it! UPI not working. Can I do bank transfer? Account number?",
881
- ])
882
  elif has_upi and not has_phone:
883
- return random.choice([
884
  "Sorry sorry! Yes, I see the UPI now! What's your phone number in case it fails?",
885
  "Oh yes, I found it! Sending now. Give me your phone number also please!",
886
  "Okay got it! I'm transferring now. What's your number?",
887
- ])
888
  elif has_upi and has_phone:
889
- return random.choice([
890
  "Yes yes, sorry! I'm old and forgetful. I'm doing it now!",
891
  "Okay okay, I found it! Let me try again. Please wait!",
892
  "Apologies! I noted it wrong. Let me try again!",
893
- ])
894
  else:
895
- return random.choice([
896
  "Sorry, my memory is bad! Please send the details one more time?",
897
  "Arey, I couldn't find it! Can you please repeat?",
898
  "I missed it, please tell me one more time!",
899
- ])
 
 
900
 
901
  # PRIORITY 2: Acknowledge what scammer just gave
902
  if gave_upi_now and not has_phone:
903
- return random.choice([
904
  "Okay, got the UPI! Let me try. What's your phone number in case it fails?",
905
  "Noted! Sending now. Also give me your number to call if there's problem.",
906
  "Got it! What's your phone number? My son wants to verify first.",
907
- ])
 
 
908
 
909
  if gave_number_now and not has_upi:
910
- return random.choice([
911
  "Saved your number! Now tell me where to send - what's your UPI ID?",
912
  "Got your number! Now what's the UPI ID for the transfer?",
913
  "Number noted! Tell me your UPI ID so I can send the payment.",
914
- ])
 
 
915
 
916
  # PRIORITY 3: Ask for what we still need
917
  # CORRECT ORDER: UPI -> Phone -> Bank Account -> IFSC -> Name (once only)
918
  if not has_upi:
919
- return random.choice([
920
  "Okay, I understand! Where should I send the money? What's your UPI ID?",
921
  "Yes, I'm ready! Tell me your UPI ID and I'll transfer!",
922
  "I want to pay! Just give me your UPI ID!",
923
- ])
924
  elif not has_phone:
925
- return random.choice([
926
  "I've noted the UPI! What's your phone number in case there's any issue?",
927
  "Got it! Also give me your number - I'll call to confirm.",
928
  "Okay! Sending now. What's your phone number for confirmation?",
929
- ])
930
  elif not has_bank:
931
  # Ask for bank account BEFORE IFSC
932
- return random.choice([
933
  "UPI is not going through! What's your bank account number?",
934
  "Getting error on UPI! Can you give your bank account number?",
935
  "Payment stuck! Tell me your bank account number.",
936
- ])
937
  elif not has_ifsc:
938
  # Only ask for IFSC AFTER we have bank account
939
- return random.choice([
940
  "Got the account number! What's the IFSC code?",
941
  "Need IFSC code to complete the transfer. What is it?",
942
  "Account noted! What's the IFSC code?",
943
- ])
944
  elif not already_asked_name:
945
  # Only ask for name ONCE
946
- return random.choice([
947
  "Got all details! What name should appear on the transfer?",
948
  "Almost done! What's the account holder name?",
949
  "Just need to confirm - what's the beneficiary name?",
950
- ])
951
  else:
952
  # We have everything - confirm and proceed
953
- return random.choice([
954
  "Perfect! I have all the details. Processing payment now.",
955
  "Got everything! Let me send the money.",
956
  "All noted! Making the transfer now.",
957
- ])
958
-
959
- # Build context from all messages for scam type detection
960
- context = last_message
961
- if all_messages:
962
- context = " ".join(m.get("message", "") for m in all_messages)
963
-
964
- # Get context-aware response based on scam type
965
- response = get_context_aware_response(context, turn_count, language)
966
- if response:
967
- return response
968
 
969
- # Fall back to persona sample
970
- return get_sample_response(persona, language)
971
 
972
  def _extract_intelligence(self, state: HoneypotState) -> Dict[str, Any]:
973
  """
@@ -1177,6 +1237,7 @@ class HoneypotAgent:
1177
  "persona": persona,
1178
  "max_turns_reached": False,
1179
  "terminated": False,
 
1180
  }
1181
 
1182
  logger.info(
 
15
 
16
  import os
17
  import re
18
+ from typing import Dict, List, Optional, TypedDict, Any, Literal, Tuple
19
  from datetime import datetime
20
 
21
  from app.config import settings
 
47
  persona: Active persona name
48
  max_turns_reached: Whether max turns limit was hit
49
  terminated: Whether the conversation has ended
50
+ used_responses: List of previously used responses to prevent repetition
51
  """
52
 
53
  messages: List[Dict]
 
60
  persona: str
61
  max_turns_reached: bool
62
  terminated: bool
63
+ used_responses: List[str]
64
 
65
 
66
  class HoneypotAgent:
 
212
  state: Current honeypot state
213
 
214
  Returns:
215
+ Dict with updated messages list and used_responses
216
  """
217
  from app.agent.personas import get_persona_prompt
218
  from app.agent.prompts import get_system_prompt
 
223
  strategy = state.get("strategy", "build_trust")
224
  turn_count = state.get("turn_count", 1)
225
  messages = state.get("messages", [])
226
+ used_responses = state.get("used_responses", [])
227
 
228
  # Get last scammer message
229
  scammer_messages = [m for m in messages if m.get("sender") == "scammer"]
 
244
  "phishing_links": [],
245
  }
246
 
247
+ # Track the chosen alternative response (if any)
248
+ chosen_alternative = ""
249
+
250
+ # Generate response - pass current intel and used_responses
251
  if self.llm is not None:
252
  try:
253
+ agent_message, chosen_alternative = self._generate_llm_response(
254
  persona=persona,
255
  language=language,
256
  strategy=strategy,
 
258
  last_message=last_message,
259
  messages=messages,
260
  extracted_intel=current_intel,
261
+ used_responses=used_responses,
262
  )
263
  except Exception as e:
264
  logger.error(f"LLM generation failed: {e}")
265
+ agent_message, chosen_alternative = self._generate_fallback_response(
266
+ persona, language, strategy, turn_count, last_message, messages, current_intel, used_responses
267
  )
268
  else:
269
+ agent_message, chosen_alternative = self._generate_fallback_response(
270
+ persona, language, strategy, turn_count, last_message, messages, current_intel, used_responses
271
  )
272
 
273
+ # Update used_responses if an alternative was chosen
274
+ updated_used_responses = used_responses.copy()
275
+ if chosen_alternative:
276
+ updated_used_responses.append(chosen_alternative)
277
+ # Keep only last 10 to allow reuse after some time
278
+ if len(updated_used_responses) > 10:
279
+ updated_used_responses = updated_used_responses[-10:]
280
+
281
  # Add to conversation
282
  new_message = {
283
  "turn": turn_count,
 
289
  updated_messages = messages.copy()
290
  updated_messages.append(new_message)
291
 
292
+ return {"messages": updated_messages, "used_responses": updated_used_responses}
293
 
294
  def _generate_llm_response(
295
  self,
 
300
  last_message: str,
301
  messages: List[Dict],
302
  extracted_intel: Dict = None,
303
+ used_responses: List[str] = None,
304
+ ) -> Tuple[str, str]:
305
  """
306
  Generate response using the LLM.
307
 
 
313
  last_message: Last scammer message
314
  messages: Full conversation history
315
  extracted_intel: Already extracted intelligence to avoid redundant questions
316
+ used_responses: List of previously used responses to avoid repetition
317
 
318
  Returns:
319
+ Tuple of (generated_response, chosen_alternative_or_empty)
320
  """
321
  from app.agent.prompts import (
322
  get_system_prompt, is_greeting_message, get_greeting_response,
 
324
  )
325
  from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
326
 
327
+ if used_responses is None:
328
+ used_responses = []
329
+
330
  # Check if this is just a greeting (works for first few turns)
331
  if turn_count <= 2 and is_greeting_message(last_message):
332
  logger.debug(f"Detected greeting message at turn {turn_count}, responding naturally")
333
+ return get_greeting_response(language, turn_count), ""
334
 
335
  # Check if scammer provided an invalid phone number
336
  phone_in_message = extract_phone_from_message(last_message)
337
  if phone_in_message and not validate_phone_number(phone_in_message):
338
  logger.debug(f"Detected invalid phone number: {phone_in_message}")
339
+ return get_invalid_phone_response(language), ""
340
 
341
  # Build context about what we already have
342
  intel_context = ""
 
410
  generated = str(response)
411
 
412
  # CRITICAL: Filter out bot-like responses and replace with context-aware ones
413
+ # Pass extracted intel and used_responses so we know what NOT to ask for again
414
+ natural_response, chosen_alternative = self._filter_bot_response(
415
+ generated, turn_count, language, last_message, messages, extracted_intel, used_responses
416
  )
417
+ return natural_response, chosen_alternative
418
 
419
  def _filter_bot_response(
420
  self,
 
424
  last_message: str,
425
  messages: List[Dict] = None,
426
  extracted_intel: Dict = None,
427
+ used_responses: List[str] = None,
428
+ ) -> Tuple[str, str]:
429
  """
430
  Filter out bot-like responses and replace with CONTEXT-AWARE responses.
431
 
 
439
  last_message: Scammer's last message
440
  messages: Full conversation history for context detection
441
  extracted_intel: Already extracted intelligence to avoid asking again
442
+ used_responses: List of previously used responses to avoid repetition
443
 
444
  Returns:
445
+ Tuple of (filtered_response, chosen_alternative_or_empty)
446
+ - filtered_response: The response to send
447
+ - chosen_alternative: The alternative that was chosen (for tracking), or empty string if LLM response was used
448
  """
449
  import random
450
 
451
+ if used_responses is None:
452
+ used_responses = []
453
+
454
+ def pick_unique_response(alternatives: List[str]) -> str:
455
+ """Pick a response that hasn't been used recently, avoiding repetition."""
456
+ # Filter out responses we've already used
457
+ available = [alt for alt in alternatives if alt not in used_responses]
458
+
459
+ # If all alternatives have been used, reset and use all (but prefer less recent)
460
+ if not available:
461
+ # Use all alternatives but shuffle to avoid same order
462
+ available = alternatives.copy()
463
+ random.shuffle(available)
464
+
465
+ return random.choice(available)
466
+
467
  response_lower = response.lower()
468
  last_msg_lower = last_message.lower()
469
 
 
540
 
541
  # If response is good and not redundant/suspicious, return it
542
  if not is_redundant and not is_suspicious:
543
+ return response, "" # Empty string indicates LLM response was used (no tracking needed)
544
 
545
  logger.warning(f"Filtering response: '{response[:50]}...' (redundant={is_redundant}, suspicious={is_suspicious})")
546
 
 
574
  "Forget OTP! Give me bank account, I'll do NEFT transfer directly!",
575
  "This OTP is not working! Tell me another way to send the money!",
576
  ]
577
+ chosen = pick_unique_response(alternatives)
578
+ return chosen, chosen
579
 
580
  # PRIORITY 1: Respond to scammer's question/confusion FIRST
581
  if scammer_asking_for_number:
 
587
  "I want to save YOUR number! What is your phone number?",
588
  "Your contact number! Tell me, I'll save it and then send money!",
589
  ]
590
+ chosen = pick_unique_response(alternatives)
591
+ return chosen, chosen
592
 
593
  if scammer_confused:
594
  # Scammer is confused - clarify what we want
 
616
  "Sorry, I don't understand technology well. Guide me step by step!",
617
  "Can you explain again? I really want to do this correctly!",
618
  ]
619
+ chosen = pick_unique_response(alternatives)
620
+ return chosen, chosen
621
 
622
  if scammer_said_already_told:
623
  # Scammer frustrated that they already gave info - acknowledge and proceed
 
664
  "Apologies! I noted it wrong. Let me try again!",
665
  "Yes, I remember now! Processing the payment...",
666
  ]
667
+ chosen = pick_unique_response(alternatives)
668
+ return chosen, chosen
669
 
670
  # PRIORITY 2: Respond based on what scammer just provided
671
  if gave_upi_now:
 
694
  "Noted! Last thing - confirm the beneficiary name?",
695
  "OK! What name should I put for the transfer?",
696
  ]
697
+ chosen = pick_unique_response(alternatives)
698
+ return chosen, chosen
699
 
700
  if gave_number_now:
701
  # They just gave phone number - acknowledge and ask for what we don't have
 
723
  "Perfect! Confirm full name as per bank records?",
724
  "Got it! What name will show on my transaction?",
725
  ]
726
+ chosen = pick_unique_response(alternatives)
727
+ return chosen, chosen
728
 
729
  # PRIORITY 3: Generate response based on what we still need
730
  # CORRECT ORDER: UPI -> Phone -> Bank Account -> IFSC -> Name (once only)
 
781
  "OK! Making the payment with these details.",
782
  ]
783
 
784
+ chosen = pick_unique_response(alternatives)
785
+ return chosen, chosen
786
 
787
  def _generate_fallback_response(
788
  self,
 
793
  last_message: str = "",
794
  all_messages: List[Dict] = None,
795
  extracted_intel: Dict = None,
796
+ used_responses: List[str] = None,
797
+ ) -> Tuple[str, str]:
798
  """
799
  Generate fallback response when LLM is unavailable.
800
 
 
811
  last_message: The last message from scammer
812
  all_messages: Full conversation history for context detection
813
  extracted_intel: Already extracted info to avoid redundant questions
814
+ used_responses: List of previously used responses to avoid repetition
815
 
816
  Returns:
817
+ Tuple of (response_string, chosen_alternative_or_empty)
818
  """
819
  import random
820
  from app.agent.strategies import get_example_response, get_context_aware_response
 
824
  extract_phone_from_message, validate_phone_number, get_invalid_phone_response
825
  )
826
 
827
+ if used_responses is None:
828
+ used_responses = []
829
+
830
+ def pick_unique_response(alternatives: List[str]) -> str:
831
+ """Pick a response that hasn't been used recently, avoiding repetition."""
832
+ available = [alt for alt in alternatives if alt not in used_responses]
833
+ if not available:
834
+ available = alternatives.copy()
835
+ random.shuffle(available)
836
+ return random.choice(available)
837
+
838
  # Check if this is just a greeting - respond naturally
839
  if turn_count <= 2 and last_message and is_greeting_message(last_message):
840
+ return get_greeting_response(language, turn_count), ""
841
 
842
  # Check if scammer provided an invalid phone number
843
  if last_message:
844
  phone_in_message = extract_phone_from_message(last_message)
845
  if phone_in_message and not validate_phone_number(phone_in_message):
846
+ return get_invalid_phone_response(language), ""
847
 
848
  # Track what we already have
849
  has_upi = bool(extracted_intel and extracted_intel.get("upi_ids"))
 
876
  # PRIORITY 0: Handle OTP requests with varied responses
877
  if scammer_asking_otp:
878
  if otp_ask_count <= 1:
879
+ alternatives = [
880
  "OTP? I didn't receive any message. Can you send it again?",
881
  "I don't see any OTP on my phone. Where does it come from?",
882
  "What OTP? Nothing came to my phone. Please resend!",
883
+ ]
884
  elif otp_ask_count <= 3:
885
+ alternatives = [
886
  "Still no OTP! Give me your UPI, I'll send money directly!",
887
  "OTP not coming! Can you call me? What's your number?",
888
  "OTP still not received! Let me do bank transfer - give me account!",
889
+ ]
890
  else:
891
+ alternatives = [
892
  "Sir, no OTP even now! Just give me UPI or bank account!",
893
  "Forget OTP! Tell me where to send money - UPI or bank!",
894
  "OTP problem! Let me call you - what's your number?",
895
+ ]
896
+ chosen = pick_unique_response(alternatives)
897
+ return chosen, chosen
898
 
899
  # PRIORITY 1: Handle scammer's confusion or frustration
900
  if scammer_confused:
901
  if not has_upi:
902
+ alternatives = [
903
  "I want to send you money! Just tell me your UPI ID!",
904
  "Sir, where should I send the payment? Give me UPI ID!",
905
  "I'm ready to pay! Just tell me where - what's your UPI?",
906
+ ]
907
  elif not has_phone:
908
+ alternatives = [
909
  "I want your phone number to call if there's problem!",
910
  "Give me your number so I can confirm the payment!",
911
  "What's your phone number? I'll call you after sending!",
912
+ ]
913
  else:
914
+ alternatives = [
915
  "I'm trying to help you! What should I do next?",
916
  "Tell me clearly - what exactly do you need from me?",
917
  "Can you explain again? I really want to do this correctly!",
918
+ ]
919
+ chosen = pick_unique_response(alternatives)
920
+ return chosen, chosen
921
 
922
  if scammer_said_already:
923
  # When scammer says "I already sent", check what we ACTUALLY have
924
  if has_bank and has_ifsc:
925
  # We have everything needed for bank transfer
926
+ alternatives = [
927
  "Yes yes, sorry! I see all the details now. Sending the payment!",
928
  "Okay okay, I found everything! Processing the transfer now!",
929
  "Apologies! I have account and IFSC. Making the payment now!",
930
  "Yes, I see it now! Account number and IFSC noted. Transferring...",
931
+ ]
932
  elif has_bank and not has_ifsc:
933
  # We have bank but need IFSC
934
+ alternatives = [
935
  "Yes, I found the account! Just need IFSC code to complete the transfer.",
936
  "Sorry, I see the account now! What's the IFSC code?",
937
  "Got the account! My bank needs IFSC. Please share!",
938
+ ]
939
  elif has_upi and has_phone and not has_bank:
940
  # We have UPI and phone, need bank
941
+ alternatives = [
942
  "Yes, I have UPI and phone! But UPI is failing. Give me bank account?",
943
  "Sorry, I found UPI! But it's showing error. What's your account number?",
944
  "Got it! UPI not working. Can I do bank transfer? Account number?",
945
+ ]
946
  elif has_upi and not has_phone:
947
+ alternatives = [
948
  "Sorry sorry! Yes, I see the UPI now! What's your phone number in case it fails?",
949
  "Oh yes, I found it! Sending now. Give me your phone number also please!",
950
  "Okay got it! I'm transferring now. What's your number?",
951
+ ]
952
  elif has_upi and has_phone:
953
+ alternatives = [
954
  "Yes yes, sorry! I'm old and forgetful. I'm doing it now!",
955
  "Okay okay, I found it! Let me try again. Please wait!",
956
  "Apologies! I noted it wrong. Let me try again!",
957
+ ]
958
  else:
959
+ alternatives = [
960
  "Sorry, my memory is bad! Please send the details one more time?",
961
  "Arey, I couldn't find it! Can you please repeat?",
962
  "I missed it, please tell me one more time!",
963
+ ]
964
+ chosen = pick_unique_response(alternatives)
965
+ return chosen, chosen
966
 
967
  # PRIORITY 2: Acknowledge what scammer just gave
968
  if gave_upi_now and not has_phone:
969
+ alternatives = [
970
  "Okay, got the UPI! Let me try. What's your phone number in case it fails?",
971
  "Noted! Sending now. Also give me your number to call if there's problem.",
972
  "Got it! What's your phone number? My son wants to verify first.",
973
+ ]
974
+ chosen = pick_unique_response(alternatives)
975
+ return chosen, chosen
976
 
977
  if gave_number_now and not has_upi:
978
+ alternatives = [
979
  "Saved your number! Now tell me where to send - what's your UPI ID?",
980
  "Got your number! Now what's the UPI ID for the transfer?",
981
  "Number noted! Tell me your UPI ID so I can send the payment.",
982
+ ]
983
+ chosen = pick_unique_response(alternatives)
984
+ return chosen, chosen
985
 
986
  # PRIORITY 3: Ask for what we still need
987
  # CORRECT ORDER: UPI -> Phone -> Bank Account -> IFSC -> Name (once only)
988
  if not has_upi:
989
+ alternatives = [
990
  "Okay, I understand! Where should I send the money? What's your UPI ID?",
991
  "Yes, I'm ready! Tell me your UPI ID and I'll transfer!",
992
  "I want to pay! Just give me your UPI ID!",
993
+ ]
994
  elif not has_phone:
995
+ alternatives = [
996
  "I've noted the UPI! What's your phone number in case there's any issue?",
997
  "Got it! Also give me your number - I'll call to confirm.",
998
  "Okay! Sending now. What's your phone number for confirmation?",
999
+ ]
1000
  elif not has_bank:
1001
  # Ask for bank account BEFORE IFSC
1002
+ alternatives = [
1003
  "UPI is not going through! What's your bank account number?",
1004
  "Getting error on UPI! Can you give your bank account number?",
1005
  "Payment stuck! Tell me your bank account number.",
1006
+ ]
1007
  elif not has_ifsc:
1008
  # Only ask for IFSC AFTER we have bank account
1009
+ alternatives = [
1010
  "Got the account number! What's the IFSC code?",
1011
  "Need IFSC code to complete the transfer. What is it?",
1012
  "Account noted! What's the IFSC code?",
1013
+ ]
1014
  elif not already_asked_name:
1015
  # Only ask for name ONCE
1016
+ alternatives = [
1017
  "Got all details! What name should appear on the transfer?",
1018
  "Almost done! What's the account holder name?",
1019
  "Just need to confirm - what's the beneficiary name?",
1020
+ ]
1021
  else:
1022
  # We have everything - confirm and proceed
1023
+ alternatives = [
1024
  "Perfect! I have all the details. Processing payment now.",
1025
  "Got everything! Let me send the money.",
1026
  "All noted! Making the transfer now.",
1027
+ ]
 
 
 
 
 
 
 
 
 
 
1028
 
1029
+ chosen = pick_unique_response(alternatives)
1030
+ return chosen, chosen
1031
 
1032
  def _extract_intelligence(self, state: HoneypotState) -> Dict[str, Any]:
1033
  """
 
1237
  "persona": persona,
1238
  "max_turns_reached": False,
1239
  "terminated": False,
1240
+ "used_responses": [], # Track used responses to prevent repetition
1241
  }
1242
 
1243
  logger.info(