-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent.py
More file actions
193 lines (153 loc) · 7.87 KB
/
Copy pathagent.py
File metadata and controls
193 lines (153 loc) · 7.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""
Triage agent: given a support ticket + retrieved corpus docs, calls Claude API
and returns structured output for all 5 required fields.
"""
import json
import os
import re
from re import IGNORECASE
import anthropic
ESCALATION_KEYWORDS = [
"fraud", "fraudulent", "unauthorized transaction",
"lawsuit", "legal action", "attorney", "lawyer", "sue",
"account locked", "account suspended", "account banned", "account terminated",
"data breach", "identity theft",
"chargeback",
"life threatening",
]
SYSTEM_PROMPT = """You are a support triage agent for three products: HackerRank, Claude (Anthropic), and Visa.
You will complete TWO independent tasks. Read each section carefully and do NOT let one task influence the other.
════════════════════════════════════════
TASK 1 — SAFETY CHECK (independent)
════════════════════════════════════════
Ask yourself ONE question about the ticket:
Is this ticket malicious, harmful, or requesting something unauthorized?
Set is_harmful = true ONLY if the ticket:
- Requests harmful code (e.g. deleting files, hacking)
- Attempts to access someone else's account
- Tries to scrape or steal data
- Contains clear intent to harm a person or system
Set is_harmful = false for everything else — including sensitive topics like
fraud, stolen cards, legal threats. Those are handled separately.
Do NOT let is_harmful affect your status decision in Task 2.
════════════════════════════════════════
TASK 2 — TRIAGE DECISION (independent)
════════════════════════════════════════
Use ONLY the provided corpus excerpts to ground your response.
Do not use outside knowledge or invent policies.
Do NOT factor in is_harmful here — that is already handled in Task 1.
Decide status using these rules in order:
1. REPLY if the corpus excerpts contain enough information to answer the question.
Always prefer replying over escalating when corpus is relevant.
2. REPLY with an out-of-scope message if the ticket is completely unrelated to
HackerRank, Claude, or Visa. Set request_type="invalid".
Example: "I'm sorry, this is outside the scope of our support."
3. REPLY with a short friendly acknowledgement if the ticket is a greeting,
thank-you, or meaningless message. Set request_type="invalid".
4. ESCALATE only when:
- The ticket involves fraud, unauthorized transactions, active legal threats,
or confirmed data breach.
- The issue clearly requires human identity verification or account
reinstatement that cannot be self-served.
- The entire site/service is reported as down or completely inaccessible.
- The corpus has no relevant information AND the topic is sensitive or high-risk.
Do not hallucinate policies or steps not present in the corpus.
Do not escalate just because a topic sounds sensitive — if the corpus answers it, reply.
════════════════════════════════════════
OUTPUT
════════════════════════════════════════
Combine both tasks into ONE valid JSON object with these fields:
{
"status": "replied" | "escalated",
"is_harmful": true | false,
"product_area": "<most relevant support category from the corpus>",
"response": "<user-facing answer grounded in the corpus, or out-of-scope message, or escalation message>",
"justification": "<1-3 sentences: why you replied or escalated, citing corpus>",
"request_type": "product_issue" | "feature_request" | "bug" | "invalid"
}
Output ONLY the JSON. No explanation outside the JSON."""
def _build_user_message(issue: str, subject: str, company: str, corpus_docs: list[dict]) -> str:
corpus_text = ""
for i, doc in enumerate(corpus_docs, 1):
corpus_text += f"\n--- Document {i} (company={doc['company']}, area={doc['product_area']}) ---\n"
corpus_text += doc["text"] + "\n"
return f"""SUPPORT TICKET
==============
Company: {company or 'Unknown'}
Subject: {subject or '(none)'}
Issue: {issue}
RELEVANT CORPUS EXCERPTS
========================
{corpus_text}
Now produce the JSON output."""
def _hard_escalation_check(issue: str) -> bool:
return any(re.search(r'\b' + re.escape(kw) + r'\b', issue, IGNORECASE)
for kw in ESCALATION_KEYWORDS)
class TriageAgent:
def __init__(self):
self.client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
def triage(self, issue: str, subject: str, company: str, corpus_docs: list[dict]) -> dict:
# Hard-coded safety check before LLM
force_escalate = _hard_escalation_check(issue)
user_msg = _build_user_message(issue, subject, company, corpus_docs)
message = self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
temperature=0, # deterministic
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_msg}],
)
raw = message.content[0].text.strip()
# Extract JSON even if model wraps it in markdown fences
json_match = re.search(r"\{.*\}", raw, re.DOTALL)
if not json_match:
return _fallback_escalation(issue)
try:
result = json.loads(json_match.group())
except json.JSONDecodeError:
return _fallback_escalation(issue)
# Enforce hard escalation regardless of LLM decision
if force_escalate and result.get("status") != "escalated":
result["status"] = "escalated"
result["response"] = (
"Thank you for reaching out. Your case involves a sensitive matter "
"that requires review by our support team. A human agent will contact "
"you shortly."
)
result["justification"] = (
"Hard-escalated: ticket contains sensitive keywords (fraud, legal, "
"account suspension, etc.) that require human review."
)
# is_harmful override — escalate if Claude flagged harmful intent
if result.get("is_harmful") is True and result.get("status") != "escalated":
result["status"] = "escalated"
result["response"] = (
"We're sorry, but we're unable to process this request as it appears "
"to involve harmful or unauthorized activity. If you believe this is "
"a mistake, please contact our support team directly."
)
result["justification"] = (
"Escalated: ticket flagged as harmful or malicious by safety check."
)
# Validate and sanitize fields
result["status"] = result.get("status", "escalated").lower()
if result["status"] not in ("replied", "escalated"):
result["status"] = "escalated"
result["request_type"] = result.get("request_type", "product_issue").lower()
if result["request_type"] not in ("product_issue", "feature_request", "bug", "invalid"):
result["request_type"] = "product_issue"
for field in ("product_area", "response", "justification"):
if not result.get(field):
result[field] = ""
return result
def _fallback_escalation(issue: str) -> dict:
return {
"status": "escalated",
"product_area": "unknown",
"response": (
"We were unable to process your request automatically. "
"A human support agent will review your ticket and respond shortly."
),
"justification": "Agent failed to parse a structured response; escalating as a precaution.",
"request_type": "product_issue",
}