Spaces:
Sleeping
Sleeping
Update agent.py
Browse files
agent.py
CHANGED
|
@@ -12,13 +12,15 @@ from openai import OpenAI
|
|
| 12 |
from models import AgentState, Message, ExtractedIntelligence
|
| 13 |
|
| 14 |
# --- Configuration ---
|
| 15 |
-
CALLBACK_URL = "https://hackathon.guvi.in/api/updateHoneyPotFinalResult"
|
| 16 |
HONEYPOT_API_KEY = os.environ.get("HONEYPOT_API_KEY", "sk_test_123456789")
|
| 17 |
|
| 18 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
|
| 20 |
-
# Using a highly capable model for complex reasoning
|
| 21 |
-
OPENROUTER_MODEL = os.environ.get("OPENROUTER_MODEL", "openrouter/free")
|
| 22 |
|
| 23 |
client = OpenAI(
|
| 24 |
base_url="https://openrouter.ai/api/v1",
|
|
@@ -26,21 +28,29 @@ client = OpenAI(
|
|
| 26 |
)
|
| 27 |
|
| 28 |
def call_openrouter(messages: List[Dict[str, str]], max_tokens: int = 512) -> str:
|
| 29 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
if not OPENROUTER_API_KEY:
|
| 31 |
raise ValueError("OPENROUTER_API_KEY is not set.")
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
# --- LangGraph Nodes (Functions) ---
|
| 46 |
|
|
@@ -50,7 +60,7 @@ def detect_scam(state: AgentState) -> AgentState:
|
|
| 50 |
text = latest_message.text
|
| 51 |
is_scam = False
|
| 52 |
reason = "No scam indicators found"
|
| 53 |
-
|
| 54 |
# Detailed prompt focusing on specific Indian scam vectors and subtle indicators
|
| 55 |
prompt = (
|
| 56 |
"You are a Senior Fraud Analyst specializing in Indian Cybercrime patterns. "
|
|
@@ -68,12 +78,13 @@ def detect_scam(state: AgentState) -> AgentState:
|
|
| 68 |
"Respond ONLY in the format 'true|<reason>' if it is a scam or 'false|<reason>' if not. "
|
| 69 |
f"Message: {text}"
|
| 70 |
)
|
| 71 |
-
|
| 72 |
try:
|
| 73 |
response = call_openrouter([{"role": "user", "content": prompt}], max_tokens=150)
|
| 74 |
resp = (response or "").strip()
|
| 75 |
if not resp:
|
| 76 |
raise ValueError("OpenRouter returned empty response")
|
|
|
|
| 77 |
first_line = resp.splitlines()[0].strip()
|
| 78 |
|
| 79 |
# Expected format: true|<reason> or false|<reason>
|
|
@@ -99,8 +110,9 @@ def detect_scam(state: AgentState) -> AgentState:
|
|
| 99 |
|
| 100 |
if not reason:
|
| 101 |
reason = "OpenRouter classification did not provide a reason"
|
|
|
|
| 102 |
except Exception as e:
|
| 103 |
-
|
| 104 |
lower_text = text.lower()
|
| 105 |
scam_keywords = [
|
| 106 |
"bank", "account", "blocked", "verify", "otp", "password", "upi", "urgent", "link", "update",
|
|
@@ -112,7 +124,7 @@ def detect_scam(state: AgentState) -> AgentState:
|
|
| 112 |
is_scam = True
|
| 113 |
reason = f"Keyword '{kw}' found in message (fallback)"
|
| 114 |
break
|
| 115 |
-
|
| 116 |
state["scamDetected"] = is_scam
|
| 117 |
if "agentNotes" not in state:
|
| 118 |
state["agentNotes"] = ""
|
|
@@ -127,7 +139,7 @@ def agent_persona_response(state: AgentState) -> AgentState:
|
|
| 127 |
return state
|
| 128 |
|
| 129 |
latest_text = state["conversationHistory"][-1].text
|
| 130 |
-
|
| 131 |
# Detailed prompt for persona engagement
|
| 132 |
prompt = (
|
| 133 |
"You are an AI Honeypot Agent. Your goal is to keep a scammer engaged to extract intelligence (UPI IDs, Bank Accounts, Links, Phone Numbers). "
|
|
@@ -140,13 +152,16 @@ def agent_persona_response(state: AgentState) -> AgentState:
|
|
| 140 |
f"Scammer's latest message: {latest_text}\n\n"
|
| 141 |
"Respond in under 40 words. Be polite and encouraging."
|
| 142 |
)
|
| 143 |
-
|
| 144 |
try:
|
| 145 |
response_text = call_openrouter([{"role": "user", "content": prompt}], max_tokens=150)
|
| 146 |
response_text = response_text.strip().split('\n')[0]
|
| 147 |
-
except Exception
|
| 148 |
-
|
| 149 |
-
response_text =
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
agent_message = Message(
|
| 152 |
sender="user",
|
|
@@ -349,8 +364,8 @@ def decide_engagement_end(state: AgentState) -> AgentState:
|
|
| 349 |
continue_engagement = True
|
| 350 |
|
| 351 |
# End if we have gathered significant actionable intelligence
|
| 352 |
-
if (len(intelligence.bankAccounts) > 0 or
|
| 353 |
-
len(intelligence.upiIds) > 0 or
|
| 354 |
len(intelligence.phishingLinks) > 0):
|
| 355 |
continue_engagement = False
|
| 356 |
|
|
@@ -368,13 +383,12 @@ def decide_engagement_end(state: AgentState) -> AgentState:
|
|
| 368 |
state["should_continue_engagement"] = continue_engagement
|
| 369 |
return state
|
| 370 |
|
| 371 |
-
|
| 372 |
def final_callback(state: AgentState) -> AgentState:
|
| 373 |
"""Node 5: Sends the mandatory final result callback."""
|
| 374 |
# If it's a scam and we haven't sent the callback yet, send it
|
| 375 |
if not state["scamDetected"] or state.get("callbackSent", False):
|
| 376 |
return state
|
| 377 |
-
|
| 378 |
intelligence = state.get("extractedIntelligence", ExtractedIntelligence())
|
| 379 |
payload = {
|
| 380 |
"sessionId": state.get("sessionId"),
|
|
@@ -383,12 +397,12 @@ def final_callback(state: AgentState) -> AgentState:
|
|
| 383 |
"extractedIntelligence": intelligence.model_dump(),
|
| 384 |
"agentNotes": state.get("agentNotes", "")
|
| 385 |
}
|
| 386 |
-
|
| 387 |
headers = {
|
| 388 |
"Content-Type": "application/json",
|
| 389 |
"x-api-key": HONEYPOT_API_KEY
|
| 390 |
}
|
| 391 |
-
|
| 392 |
try:
|
| 393 |
response = requests.post(CALLBACK_URL, json=payload, headers=headers, timeout=10)
|
| 394 |
response.raise_for_status()
|
|
@@ -397,11 +411,10 @@ def final_callback(state: AgentState) -> AgentState:
|
|
| 397 |
state["agentNotes"] = ""
|
| 398 |
state["agentNotes"] += "Final callback sent successfully. "
|
| 399 |
except Exception as e:
|
| 400 |
-
print(f"Final callback failed: {e}")
|
| 401 |
if "agentNotes" not in state:
|
| 402 |
state["agentNotes"] = ""
|
| 403 |
state["agentNotes"] += f"Final callback failed: {e}. "
|
| 404 |
-
|
| 405 |
return state
|
| 406 |
|
| 407 |
def create_honeypot_graph(checkpoint_saver: BaseCheckpointSaver):
|
|
|
|
| 12 |
from models import AgentState, Message, ExtractedIntelligence
|
| 13 |
|
| 14 |
# --- Configuration ---
|
| 15 |
+
CALLBACK_URL = "https://hackathon.guvi.in/api/updateHoneyPotFinalResult"
|
| 16 |
HONEYPOT_API_KEY = os.environ.get("HONEYPOT_API_KEY", "sk_test_123456789")
|
| 17 |
|
| 18 |
+
# Choose a specific, supported model instead of the router to avoid empty messages
|
| 19 |
+
# Models like anthropic/claude-3-haiku or meta-llama/llama-3.1-8b-instruct are robust for classification
|
| 20 |
+
OPENROUTER_MODEL = os.environ.get("OPENROUTER_MODEL", "anthropic/claude-3-haiku")
|
| 21 |
+
|
| 22 |
+
# API key for OpenRouter
|
| 23 |
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
|
|
|
|
|
|
|
| 24 |
|
| 25 |
client = OpenAI(
|
| 26 |
base_url="https://openrouter.ai/api/v1",
|
|
|
|
| 28 |
)
|
| 29 |
|
| 30 |
def call_openrouter(messages: List[Dict[str, str]], max_tokens: int = 512) -> str:
|
| 31 |
+
"""
|
| 32 |
+
Call the OpenRouter API to generate a text response.
|
| 33 |
+
This version avoids reasoning tokens (which can leave message.content empty)
|
| 34 |
+
and checks for empty content, raising an error to trigger fallback logic.
|
| 35 |
+
"""
|
| 36 |
if not OPENROUTER_API_KEY:
|
| 37 |
raise ValueError("OPENROUTER_API_KEY is not set.")
|
| 38 |
+
|
| 39 |
+
response = client.chat.completions.create(
|
| 40 |
+
model=OPENROUTER_MODEL,
|
| 41 |
+
messages=messages,
|
| 42 |
+
max_tokens=max_tokens
|
| 43 |
+
# No extra_body to avoid reasoning tokens; reasoning can cause empty content:contentReference[oaicite:2]{index=2}
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# Guard against empty responses: if no content, raise to trigger fallback
|
| 47 |
+
message_obj = response.choices[0].message
|
| 48 |
+
content = getattr(message_obj, "content", "")
|
| 49 |
+
if not content or not content.strip():
|
| 50 |
+
# Optionally, you could parse reasoning_details here, but that complicates your flow
|
| 51 |
+
raise ValueError("Empty model response from OpenRouter; check model or parameters.")
|
| 52 |
+
|
| 53 |
+
return content
|
| 54 |
|
| 55 |
# --- LangGraph Nodes (Functions) ---
|
| 56 |
|
|
|
|
| 60 |
text = latest_message.text
|
| 61 |
is_scam = False
|
| 62 |
reason = "No scam indicators found"
|
| 63 |
+
|
| 64 |
# Detailed prompt focusing on specific Indian scam vectors and subtle indicators
|
| 65 |
prompt = (
|
| 66 |
"You are a Senior Fraud Analyst specializing in Indian Cybercrime patterns. "
|
|
|
|
| 78 |
"Respond ONLY in the format 'true|<reason>' if it is a scam or 'false|<reason>' if not. "
|
| 79 |
f"Message: {text}"
|
| 80 |
)
|
| 81 |
+
|
| 82 |
try:
|
| 83 |
response = call_openrouter([{"role": "user", "content": prompt}], max_tokens=150)
|
| 84 |
resp = (response or "").strip()
|
| 85 |
if not resp:
|
| 86 |
raise ValueError("OpenRouter returned empty response")
|
| 87 |
+
|
| 88 |
first_line = resp.splitlines()[0].strip()
|
| 89 |
|
| 90 |
# Expected format: true|<reason> or false|<reason>
|
|
|
|
| 110 |
|
| 111 |
if not reason:
|
| 112 |
reason = "OpenRouter classification did not provide a reason"
|
| 113 |
+
|
| 114 |
except Exception as e:
|
| 115 |
+
# Fallback heuristic if the model fails or returns empty content
|
| 116 |
lower_text = text.lower()
|
| 117 |
scam_keywords = [
|
| 118 |
"bank", "account", "blocked", "verify", "otp", "password", "upi", "urgent", "link", "update",
|
|
|
|
| 124 |
is_scam = True
|
| 125 |
reason = f"Keyword '{kw}' found in message (fallback)"
|
| 126 |
break
|
| 127 |
+
|
| 128 |
state["scamDetected"] = is_scam
|
| 129 |
if "agentNotes" not in state:
|
| 130 |
state["agentNotes"] = ""
|
|
|
|
| 139 |
return state
|
| 140 |
|
| 141 |
latest_text = state["conversationHistory"][-1].text
|
| 142 |
+
|
| 143 |
# Detailed prompt for persona engagement
|
| 144 |
prompt = (
|
| 145 |
"You are an AI Honeypot Agent. Your goal is to keep a scammer engaged to extract intelligence (UPI IDs, Bank Accounts, Links, Phone Numbers). "
|
|
|
|
| 152 |
f"Scammer's latest message: {latest_text}\n\n"
|
| 153 |
"Respond in under 40 words. Be polite and encouraging."
|
| 154 |
)
|
| 155 |
+
|
| 156 |
try:
|
| 157 |
response_text = call_openrouter([{"role": "user", "content": prompt}], max_tokens=150)
|
| 158 |
response_text = response_text.strip().split('\n')[0]
|
| 159 |
+
except Exception:
|
| 160 |
+
# Fallback phrase if the model fails or returns empty content
|
| 161 |
+
response_text = (
|
| 162 |
+
"Sir, I am trying to do as you said but it is not working. "
|
| 163 |
+
"Can you please guide me again? I don't want my account to be blocked."
|
| 164 |
+
)
|
| 165 |
|
| 166 |
agent_message = Message(
|
| 167 |
sender="user",
|
|
|
|
| 364 |
continue_engagement = True
|
| 365 |
|
| 366 |
# End if we have gathered significant actionable intelligence
|
| 367 |
+
if (len(intelligence.bankAccounts) > 0 or
|
| 368 |
+
len(intelligence.upiIds) > 0 or
|
| 369 |
len(intelligence.phishingLinks) > 0):
|
| 370 |
continue_engagement = False
|
| 371 |
|
|
|
|
| 383 |
state["should_continue_engagement"] = continue_engagement
|
| 384 |
return state
|
| 385 |
|
|
|
|
| 386 |
def final_callback(state: AgentState) -> AgentState:
|
| 387 |
"""Node 5: Sends the mandatory final result callback."""
|
| 388 |
# If it's a scam and we haven't sent the callback yet, send it
|
| 389 |
if not state["scamDetected"] or state.get("callbackSent", False):
|
| 390 |
return state
|
| 391 |
+
|
| 392 |
intelligence = state.get("extractedIntelligence", ExtractedIntelligence())
|
| 393 |
payload = {
|
| 394 |
"sessionId": state.get("sessionId"),
|
|
|
|
| 397 |
"extractedIntelligence": intelligence.model_dump(),
|
| 398 |
"agentNotes": state.get("agentNotes", "")
|
| 399 |
}
|
| 400 |
+
|
| 401 |
headers = {
|
| 402 |
"Content-Type": "application/json",
|
| 403 |
"x-api-key": HONEYPOT_API_KEY
|
| 404 |
}
|
| 405 |
+
|
| 406 |
try:
|
| 407 |
response = requests.post(CALLBACK_URL, json=payload, headers=headers, timeout=10)
|
| 408 |
response.raise_for_status()
|
|
|
|
| 411 |
state["agentNotes"] = ""
|
| 412 |
state["agentNotes"] += "Final callback sent successfully. "
|
| 413 |
except Exception as e:
|
|
|
|
| 414 |
if "agentNotes" not in state:
|
| 415 |
state["agentNotes"] = ""
|
| 416 |
state["agentNotes"] += f"Final callback failed: {e}. "
|
| 417 |
+
|
| 418 |
return state
|
| 419 |
|
| 420 |
def create_honeypot_graph(checkpoint_saver: BaseCheckpointSaver):
|