lebiraja Claude Sonnet 4.6 commited on
Commit
d65eebc
·
1 Parent(s): 1fa04c3

fix: natural conversation flow — greet before querying DB

Browse files

- Add premature_query_penalty (-0.15) when agent issues a DB query as
its first action before greeting the customer; wired into both
compute_step_reward and compute_hierarchy_reward, and into
_handle_query_action which builds its own Reward directly
- Update system prompts (SYSTEM_PROMPT, SUPPORT_AGENT_PROMPT in
inference.py; SUPPORT_AGENT_PROMPT in frontend ai-action route) to
mandate greet → gather info → query flow; remove old "query the DB
first" instruction that drove robotic behaviour
- Fix inference.py missing X-API-Key header on all httpx calls (401s
on reset/step); add _ENV_HEADERS from ENV_API_KEY env var
- Fix frontend Dockerfile healthcheck: wget resolves localhost to ::1
on BusyBox but Next.js only binds IPv4; switch to 127.0.0.1 and
target /demo (which returns 200, not the 307 that / redirects from)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

env/environment.py CHANGED
@@ -260,15 +260,21 @@ class CustomerSupportEnv:
260
  # Minimal reward — actual signal computed in reward_engine via DB signals.
261
  # Using _clamp_db_total so spam queries can't push the reward above the
262
  # normal cap, and so wasted-query penalties are bounded.
263
- from env.reward_engine import compute_db_signals, _clamp_db_total
264
  db_signals = compute_db_signals(action, self._ticket, self._history, self._retrieved_data)
265
  raw_db = _clamp_db_total(db_signals)
 
266
  import numpy as np
267
  reward = Reward(
268
- value=float(np.clip(0.5 + raw_db, 0.0, 1.0)),
269
  resolution_score=0.0, tone_score=0.5,
270
  efficiency_score=0.0, accuracy_score=0.0,
271
- breakdown={"db_signals": db_signals, "is_terminal": False, "action_type": at.value},
 
 
 
 
 
272
  )
273
 
274
  self._action_log.append({
 
260
  # Minimal reward — actual signal computed in reward_engine via DB signals.
261
  # Using _clamp_db_total so spam queries can't push the reward above the
262
  # normal cap, and so wasted-query penalties are bounded.
263
+ from env.reward_engine import compute_db_signals, _clamp_db_total, compute_premature_query_penalty
264
  db_signals = compute_db_signals(action, self._ticket, self._history, self._retrieved_data)
265
  raw_db = _clamp_db_total(db_signals)
266
+ premature_penalty = compute_premature_query_penalty(action, self._history)
267
  import numpy as np
268
  reward = Reward(
269
+ value=float(np.clip(0.5 + raw_db + premature_penalty, 0.0, 1.0)),
270
  resolution_score=0.0, tone_score=0.5,
271
  efficiency_score=0.0, accuracy_score=0.0,
272
+ breakdown={
273
+ "db_signals": db_signals,
274
+ "premature_query_penalty": premature_penalty,
275
+ "is_terminal": False,
276
+ "action_type": at.value,
277
+ },
278
  )
279
 
280
  self._action_log.append({
env/reward_engine.py CHANGED
@@ -215,6 +215,27 @@ def compute_contradiction_penalty(action: Action, history: List[Message]) -> flo
215
  return -0.15 if (prev_claimed_done and now_asking_info) else 0.0
216
 
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  def compute_keyword_stuffing_penalty(message: str) -> float:
219
  """
220
  Detect and penalize keyword stuffing.
@@ -449,6 +470,7 @@ def compute_step_reward(
449
  loop_penalty = compute_loop_penalty(history)
450
  contradiction_penalty = compute_contradiction_penalty(action, history)
451
  stuffing_penalty = compute_keyword_stuffing_penalty(tone_msg)
 
452
 
453
  if is_terminal:
454
  resolution_score = compute_resolution_score(action, ticket, history)
@@ -481,6 +503,7 @@ def compute_step_reward(
481
  + escalation_penalty
482
  + stuffing_penalty
483
  + info_gathering_bonus
 
484
  + db_total
485
  )
486
 
@@ -499,6 +522,7 @@ def compute_step_reward(
499
  "escalation_penalty": escalation_penalty,
500
  "keyword_stuffing_penalty": stuffing_penalty,
501
  "info_gathering_bonus": info_gathering_bonus,
 
502
  **{f"db_{k}": v for k, v in db_signals.items()},
503
  "is_terminal": is_terminal,
504
  },
@@ -539,6 +563,7 @@ def compute_hierarchy_reward(
539
  loop_penalty = compute_loop_penalty(history)
540
  contradiction_penalty = compute_contradiction_penalty(action, history)
541
  stuffing_penalty = compute_keyword_stuffing_penalty(tone_msg)
 
542
  efficiency_score = compute_efficiency_score(steps_used, max_steps)
543
  accuracy_score = compute_accuracy_score(history, ticket)
544
 
@@ -666,6 +691,7 @@ def compute_hierarchy_reward(
666
  + escalation_penalty
667
  + ignored_feedback_penalty
668
  + unnecessary_manager_penalty
 
669
  + db_total
670
  )
671
  else:
@@ -681,6 +707,7 @@ def compute_hierarchy_reward(
681
  + stuffing_penalty
682
  + ignored_feedback_penalty
683
  + unnecessary_manager_penalty
 
684
  + db_total
685
  )
686
 
@@ -754,6 +781,7 @@ def compute_hierarchy_reward(
754
  "keyword_stuffing_penalty": stuffing_penalty,
755
  "ignored_feedback_penalty": ignored_feedback_penalty,
756
  "unnecessary_manager_penalty": unnecessary_manager_penalty,
 
757
  **{f"db_{k}": v for k, v in db_signals.items()},
758
  "is_terminal": is_terminal,
759
  "role": role,
 
215
  return -0.15 if (prev_claimed_done and now_asking_info) else 0.0
216
 
217
 
218
+ def compute_premature_query_penalty(action: Action, history: List[Message]) -> float:
219
+ """
220
+ -0.15 if the agent fires a DB query before ever greeting the customer.
221
+
222
+ A professional support agent always acknowledges the customer first.
223
+ Querying the DB as the opening move is robotic, hurts empathy scores, and
224
+ signals the model is pattern-matching identifiers rather than conversing.
225
+
226
+ Only triggers when there are zero prior agent messages in the history, so
227
+ legitimate second-or-later queries (after a greeting) are never penalized.
228
+ """
229
+ if action.action_type not in (
230
+ ActionType.QUERY_USER_PROFILE, ActionType.QUERY_ORDER_DETAILS
231
+ ):
232
+ return 0.0
233
+ prior_agent = [m for m in history if m.role == "agent" and m.content.strip()]
234
+ if not prior_agent:
235
+ return -0.15
236
+ return 0.0
237
+
238
+
239
  def compute_keyword_stuffing_penalty(message: str) -> float:
240
  """
241
  Detect and penalize keyword stuffing.
 
470
  loop_penalty = compute_loop_penalty(history)
471
  contradiction_penalty = compute_contradiction_penalty(action, history)
472
  stuffing_penalty = compute_keyword_stuffing_penalty(tone_msg)
473
+ premature_query_penalty = compute_premature_query_penalty(action, history)
474
 
475
  if is_terminal:
476
  resolution_score = compute_resolution_score(action, ticket, history)
 
503
  + escalation_penalty
504
  + stuffing_penalty
505
  + info_gathering_bonus
506
+ + premature_query_penalty
507
  + db_total
508
  )
509
 
 
522
  "escalation_penalty": escalation_penalty,
523
  "keyword_stuffing_penalty": stuffing_penalty,
524
  "info_gathering_bonus": info_gathering_bonus,
525
+ "premature_query_penalty": premature_query_penalty,
526
  **{f"db_{k}": v for k, v in db_signals.items()},
527
  "is_terminal": is_terminal,
528
  },
 
563
  loop_penalty = compute_loop_penalty(history)
564
  contradiction_penalty = compute_contradiction_penalty(action, history)
565
  stuffing_penalty = compute_keyword_stuffing_penalty(tone_msg)
566
+ premature_query_penalty = compute_premature_query_penalty(action, history)
567
  efficiency_score = compute_efficiency_score(steps_used, max_steps)
568
  accuracy_score = compute_accuracy_score(history, ticket)
569
 
 
691
  + escalation_penalty
692
  + ignored_feedback_penalty
693
  + unnecessary_manager_penalty
694
+ + premature_query_penalty
695
  + db_total
696
  )
697
  else:
 
707
  + stuffing_penalty
708
  + ignored_feedback_penalty
709
  + unnecessary_manager_penalty
710
+ + premature_query_penalty
711
  + db_total
712
  )
713
 
 
781
  "keyword_stuffing_penalty": stuffing_penalty,
782
  "ignored_feedback_penalty": ignored_feedback_penalty,
783
  "unnecessary_manager_penalty": unnecessary_manager_penalty,
784
+ "premature_query_penalty": premature_query_penalty,
785
  **{f"db_{k}": v for k, v in db_signals.items()},
786
  "is_terminal": is_terminal,
787
  "role": role,
frontend/Dockerfile CHANGED
@@ -45,6 +45,6 @@ ENV PORT=3000
45
  ENV HOSTNAME=0.0.0.0
46
 
47
  HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
48
- CMD wget -qO- http://localhost:3000/ > /dev/null 2>&1 || exit 1
49
 
50
  CMD ["node", "server.js"]
 
45
  ENV HOSTNAME=0.0.0.0
46
 
47
  HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
48
+ CMD wget -qO- http://127.0.0.1:3000/demo > /dev/null 2>&1 || exit 1
49
 
50
  CMD ["node", "server.js"]
frontend/src/app/api/ai-action/route.ts CHANGED
@@ -19,11 +19,20 @@ ACTION TYPES — output exactly one per step:
19
  - "query_user_profile" → look up customer DB (internal) → requires: "email"
20
  - "query_order_details" → look up order DB (internal) → requires: "order_id"
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  SCORING: Empathy(30%) + Accuracy(25%) + Resolution(25%) + Efficiency(20%)
23
- Be warm, gather info from "Unresolved issues", use specific resolution language.
24
- If supervisor gave feedback, INCORPORATE it into your next action.
25
- If the ticket references an email or order-id you haven't looked up yet, query
26
- the DB first — never invent facts not present in KNOWN DATA or the conversation.
27
 
28
  OUTPUT FORMAT — return ONLY this JSON, no code fences, no explanation:
29
  {"action_type": "respond", "message": "..."} or {"action_type": "escalate", "reason": "..."}`;
 
19
  - "query_user_profile" → look up customer DB (internal) → requires: "email"
20
  - "query_order_details" → look up order DB (internal) → requires: "order_id"
21
 
22
+ CONVERSATION FLOW — MANDATORY ORDER:
23
+ 1. FIRST action MUST be "respond": greet the customer warmly and acknowledge their specific issue.
24
+ 2. THEN use "request_info" if you still need details before you can help.
25
+ 3. ONLY THEN query the DB — but only when the customer has confirmed the email or order ID
26
+ in this conversation AND you need that data to answer accurately.
27
+ 4. Respond with grounded facts, then "close" or "escalate".
28
+
29
+ CRITICAL DB QUERY RULES:
30
+ - NEVER query the DB as your very first action, even if the ticket already shows an email or order ID.
31
+ Always greet and acknowledge first. Querying without greeting feels robotic and is penalized.
32
+ - After querying, cite ONLY values from KNOWN DATA or the customer's own messages — never invent facts.
33
+ - If supervisor gave feedback, INCORPORATE it into your next action.
34
+
35
  SCORING: Empathy(30%) + Accuracy(25%) + Resolution(25%) + Efficiency(20%)
 
 
 
 
36
 
37
  OUTPUT FORMAT — return ONLY this JSON, no code fences, no explanation:
38
  {"action_type": "respond", "message": "..."} or {"action_type": "escalate", "reason": "..."}`;
inference.py CHANGED
@@ -27,6 +27,8 @@ INFERENCE_MODEL = (
27
  or "unsloth/Qwen2.5-1.5B-Instruct"
28
  )
29
  ENV_URL = os.getenv("ENV_URL", "http://localhost:7860")
 
 
30
  BENCHMARK = "customer-support-env"
31
  TASKS = ["easy", "medium", "hard"]
32
  HIERARCHY_TASKS = ["hierarchy_easy", "hierarchy_medium", "hierarchy_hard"]
@@ -158,6 +160,20 @@ ACTION TYPES — output exactly one per step:
158
  - "query_user_profile" -> look up customer account (internal) -> requires: "email"
159
  - "query_order_details" -> look up order details (internal) -> requires: "order_id"
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  OUTPUT FORMAT — return ONLY this JSON, no code fences, no preamble:
162
  {"action_type": "...", "message": "..."} <- for respond / request_info / close
163
  {"action_type": "escalate", "reason": "..."} <- for escalate
@@ -190,9 +206,22 @@ ACTION TYPES — output exactly one per step:
190
  - "query_user_profile" -> look up customer account (internal) -> requires: "email"
191
  - "query_order_details" -> look up order details (internal) -> requires: "order_id"
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  SCORING: Empathy(30%) + Accuracy(25%) + Resolution(25%) + Efficiency(20%)
194
- Be warm, gather info from "Unresolved issues", use specific resolution language.
195
- If supervisor gave feedback, INCORPORATE it into your next action.
196
 
197
  OUTPUT FORMAT — return ONLY this JSON:
198
  {{"action_type": "...", "message": "..."}} or {{"action_type": "escalate", "reason": "..."}}"""
@@ -337,7 +366,7 @@ _FALLBACKS = {
337
 
338
  def run_task(task_name: str) -> None:
339
  try:
340
- r = httpx.post(f"{ENV_URL}/reset", params={"task": task_name}, timeout=30)
341
  r.raise_for_status()
342
  except Exception as exc:
343
  print(f"[ERROR] Reset failed task={task_name}: {exc}", file=sys.stderr)
@@ -359,7 +388,7 @@ def run_task(task_name: str) -> None:
359
  action = _FALLBACKS["support_agent"]
360
 
361
  try:
362
- sr = httpx.post(f"{ENV_URL}/step", params={"session_id": session_id}, json=action, timeout=30)
363
  sr.raise_for_status()
364
  result = sr.json()
365
  except Exception as exc:
@@ -379,7 +408,7 @@ def run_task(task_name: str) -> None:
379
 
380
  def run_hierarchy_task(task_name: str) -> None:
381
  try:
382
- r = httpx.post(f"{ENV_URL}/reset", params={"task": task_name}, timeout=30)
383
  r.raise_for_status()
384
  except Exception as exc:
385
  print(f"[ERROR] Reset failed task={task_name}: {exc}", file=sys.stderr)
@@ -403,7 +432,7 @@ def run_hierarchy_task(task_name: str) -> None:
403
  action = _FALLBACKS.get(role, _FALLBACKS["support_agent"])
404
 
405
  try:
406
- sr = httpx.post(f"{ENV_URL}/step", params={"session_id": session_id}, json=action, timeout=60)
407
  sr.raise_for_status()
408
  result = sr.json()
409
  except Exception as exc:
 
27
  or "unsloth/Qwen2.5-1.5B-Instruct"
28
  )
29
  ENV_URL = os.getenv("ENV_URL", "http://localhost:7860")
30
+ ENV_API_KEY = os.getenv("ENV_API_KEY", os.getenv("API_KEY", "meta_hack_2026"))
31
+ _ENV_HEADERS = {"X-API-Key": ENV_API_KEY}
32
  BENCHMARK = "customer-support-env"
33
  TASKS = ["easy", "medium", "hard"]
34
  HIERARCHY_TASKS = ["hierarchy_easy", "hierarchy_medium", "hierarchy_hard"]
 
160
  - "query_user_profile" -> look up customer account (internal) -> requires: "email"
161
  - "query_order_details" -> look up order details (internal) -> requires: "order_id"
162
 
163
+ CONVERSATION FLOW — MANDATORY ORDER:
164
+ 1. FIRST action MUST be "respond": greet the customer warmly and acknowledge their specific issue.
165
+ 2. THEN use "request_info" if you still need information to resolve the issue.
166
+ 3. ONLY THEN use "query_user_profile" or "query_order_details" — and only when the customer has
167
+ explicitly confirmed or provided the email / order ID during the conversation AND you need that
168
+ data to answer their question.
169
+ 4. Finally "respond" with grounded facts, then "close" or "escalate".
170
+
171
+ CRITICAL DB QUERY RULES:
172
+ - NEVER query the DB as your first action, even if the ticket already shows an email or order ID.
173
+ Always greet and acknowledge first — querying without greeting is robotic and is penalized.
174
+ - Only query AFTER the customer has confirmed the identifier in the current conversation thread.
175
+ - Never invent data not present in KNOWN DATA or the customer's own messages.
176
+
177
  OUTPUT FORMAT — return ONLY this JSON, no code fences, no preamble:
178
  {"action_type": "...", "message": "..."} <- for respond / request_info / close
179
  {"action_type": "escalate", "reason": "..."} <- for escalate
 
206
  - "query_user_profile" -> look up customer account (internal) -> requires: "email"
207
  - "query_order_details" -> look up order details (internal) -> requires: "order_id"
208
 
209
+ CONVERSATION FLOW — MANDATORY ORDER:
210
+ 1. FIRST action MUST be "respond": greet the customer warmly by name if possible, acknowledge their issue.
211
+ 2. THEN use "request_info" if you still need details to help them.
212
+ 3. ONLY THEN query the DB — but ONLY when the customer has explicitly confirmed the email or order ID
213
+ during this conversation AND you need that data to answer accurately.
214
+ 4. Respond with the grounded facts, then "close" or "escalate" as appropriate.
215
+
216
+ CRITICAL DB QUERY RULES:
217
+ - NEVER issue a DB query as your very first action — always acknowledge the customer first.
218
+ The ticket description may mention an email or order ID, but greet first. Skipping the greeting
219
+ is robotic, hurts empathy scores, and incurs a penalty.
220
+ - After querying, cite ONLY values from KNOWN DATA or from the customer's own messages.
221
+ Never invent facts (amounts, dates, status) not present in retrieved data.
222
+ - If supervisor gave feedback, INCORPORATE it into your next action.
223
+
224
  SCORING: Empathy(30%) + Accuracy(25%) + Resolution(25%) + Efficiency(20%)
 
 
225
 
226
  OUTPUT FORMAT — return ONLY this JSON:
227
  {{"action_type": "...", "message": "..."}} or {{"action_type": "escalate", "reason": "..."}}"""
 
366
 
367
  def run_task(task_name: str) -> None:
368
  try:
369
+ r = httpx.post(f"{ENV_URL}/reset", params={"task": task_name}, headers=_ENV_HEADERS, timeout=30)
370
  r.raise_for_status()
371
  except Exception as exc:
372
  print(f"[ERROR] Reset failed task={task_name}: {exc}", file=sys.stderr)
 
388
  action = _FALLBACKS["support_agent"]
389
 
390
  try:
391
+ sr = httpx.post(f"{ENV_URL}/step", params={"session_id": session_id}, json=action, headers=_ENV_HEADERS, timeout=30)
392
  sr.raise_for_status()
393
  result = sr.json()
394
  except Exception as exc:
 
408
 
409
  def run_hierarchy_task(task_name: str) -> None:
410
  try:
411
+ r = httpx.post(f"{ENV_URL}/reset", params={"task": task_name}, headers=_ENV_HEADERS, timeout=30)
412
  r.raise_for_status()
413
  except Exception as exc:
414
  print(f"[ERROR] Reset failed task={task_name}: {exc}", file=sys.stderr)
 
432
  action = _FALLBACKS.get(role, _FALLBACKS["support_agent"])
433
 
434
  try:
435
+ sr = httpx.post(f"{ENV_URL}/step", params={"session_id": session_id}, json=action, headers=_ENV_HEADERS, timeout=60)
436
  sr.raise_for_status()
437
  result = sr.json()
438
  except Exception as exc: