AbeBhatti commited on
Commit
da27912
·
1 Parent(s): 377c8b2

final demo polish

Browse files
agent/agent_llm.py CHANGED
@@ -128,7 +128,7 @@ class AgentLLM:
128
  "last check — is there any room at all or should I go with my other offer?",
129
  "I need to make a decision today — can you sharpen your price?",
130
  ]
131
- index = (turn - 2) % len(PRESSURE_MESSAGES) if turn >= 2 else 0
132
  fallback = PRESSURE_MESSAGES[index]
133
  prompt = (
134
  f"You are a buyer negotiating for a {item}. Current seller offer is ${current_offer:.0f}. "
 
128
  "last check — is there any room at all or should I go with my other offer?",
129
  "I need to make a decision today — can you sharpen your price?",
130
  ]
131
+ index = turn % len(PRESSURE_MESSAGES)
132
  fallback = PRESSURE_MESSAGES[index]
133
  prompt = (
134
  f"You are a buyer negotiating for a {item}. Current seller offer is ${current_offer:.0f}. "
agent/arbitragent.py CHANGED
@@ -302,7 +302,7 @@ class ArbitrAgent:
302
  continue
303
 
304
  current_offer = float(c.sim.current_offer)
305
- msg = self.llm.pressure_message(c.item, current_offer)
306
 
307
  resp = c.sim.step(msg)
308
  if verbose:
 
302
  continue
303
 
304
  current_offer = float(c.sim.current_offer)
305
+ msg = self.llm.pressure_message(c.item, current_offer, turn=turn)
306
 
307
  resp = c.sim.step(msg)
308
  if verbose:
demo/display.py CHANGED
@@ -149,11 +149,8 @@ class PhaseDisplay:
149
  item: str,
150
  status: str,
151
  turns: List[Dict[str, Any]],
152
- bluff_already_detected_sellers: Optional[Dict[str, float]] = None,
153
  ) -> None:
154
  st = _status_style(status)
155
- if bluff_already_detected_sellers is None:
156
- bluff_already_detected_sellers = {}
157
  self.console.print(f" [bold]── {seller_id} ({item}) ──[/bold]")
158
  for t in turns:
159
  turn_num = t.get("turn", 0)
@@ -288,9 +285,8 @@ class PhaseDisplay:
288
  return_multiple: float,
289
  key_decisions: List[str],
290
  ) -> None:
291
- leverage = deployed / budget if budget > 0 else 0
292
  self.console.print(f" Budget: ${budget:.2f}")
293
- self.console.print(f" Deployed: ${deployed:.2f} ({leverage:.2f}x leverage via confirmed route)")
294
  self.console.print(f" Final Value: ${final_value:.2f}")
295
  self.console.print(f" Return: {return_multiple:.2f}x")
296
  self.console.print()
 
149
  item: str,
150
  status: str,
151
  turns: List[Dict[str, Any]],
 
152
  ) -> None:
153
  st = _status_style(status)
 
 
154
  self.console.print(f" [bold]── {seller_id} ({item}) ──[/bold]")
155
  for t in turns:
156
  turn_num = t.get("turn", 0)
 
285
  return_multiple: float,
286
  key_decisions: List[str],
287
  ) -> None:
 
288
  self.console.print(f" Budget: ${budget:.2f}")
289
+ self.console.print(f" Deployed: ${deployed:.2f}")
290
  self.console.print(f" Final Value: ${final_value:.2f}")
291
  self.console.print(f" Return: {return_multiple:.2f}x")
292
  self.console.print()
demo/run_demo.py CHANGED
@@ -87,6 +87,7 @@ class DemoArbitrAgent(ArbitrAgent):
87
  # Per-seller Phase 3 display: seller_id -> { seller_id, item, status, turns: [] }
88
  phase3_seller_data: Dict[str, Dict[str, Any]] = {}
89
  consecutive_silence: Dict[str, int] = {}
 
90
 
91
  def get_thread(cand: SellerCandidate) -> ThreadState:
92
  if cand.seller_id not in threads:
@@ -241,7 +242,10 @@ class DemoArbitrAgent(ArbitrAgent):
241
  "but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?"
242
  )
243
  else:
244
- agent_msg = f"just checking back on the {cand.item} — any flexibility on your price at all?"
 
 
 
245
 
246
  resp = cand.sim.step(agent_msg)
247
  thread.messages.append(ThreadMessage(turn=cand.sim.turn, sender="agent", text=agent_msg))
@@ -269,17 +273,23 @@ class DemoArbitrAgent(ArbitrAgent):
269
  "pattern_tell": signals.pattern_tell,
270
  "bluff_score": signals.bluff_score,
271
  }
272
- turn_record["bluff_analysis"] = {
273
- "timing_tell": signals.timing_tell,
274
- "size_tell": signals.size_tell,
275
- "formulaic_tell": signals.formulaic_tell,
276
- "pattern_tell": signals.pattern_tell,
277
- "learned_score": learned,
278
- "bluff_score": signals.bluff_score,
279
- "is_bluff": True,
280
- "reasoning": f"seller claiming floor before turn 4, classifier confidence {learned*100:.0f}%, deploying coalition pressure",
281
- "bluff_reward": 0.9,
282
- }
 
 
 
 
 
 
283
  event_log.append({
284
  "type": "bluff_detected",
285
  "seller_name": cand.seller_id,
 
87
  # Per-seller Phase 3 display: seller_id -> { seller_id, item, status, turns: [] }
88
  phase3_seller_data: Dict[str, Dict[str, Any]] = {}
89
  consecutive_silence: Dict[str, int] = {}
90
+ bluff_detected_sellers: Dict[str, float] = {} # seller_id -> score when first detected (for dedupe display)
91
 
92
  def get_thread(cand: SellerCandidate) -> ThreadState:
93
  if cand.seller_id not in threads:
 
242
  "but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?"
243
  )
244
  else:
245
+ current_offer = float(cand.sim.current_offer)
246
+ agent_msg = self.llm.pressure_message(cand.item, current_offer, turn=turn)
247
+ if not agent_msg.strip():
248
+ agent_msg = f"just checking back on the {cand.item} — any flexibility on your price at all?"
249
 
250
  resp = cand.sim.step(agent_msg)
251
  thread.messages.append(ThreadMessage(turn=cand.sim.turn, sender="agent", text=agent_msg))
 
273
  "pattern_tell": signals.pattern_tell,
274
  "bluff_score": signals.bluff_score,
275
  }
276
+ bluff_score_display = learned if learned is not None else signals.bluff_score
277
+ if cand.seller_id not in bluff_detected_sellers:
278
+ bluff_detected_sellers[cand.seller_id] = bluff_score_display
279
+ turn_record["bluff_analysis"] = {
280
+ "timing_tell": signals.timing_tell,
281
+ "size_tell": signals.size_tell,
282
+ "formulaic_tell": signals.formulaic_tell,
283
+ "pattern_tell": bluff_score_display,
284
+ "learned_score": learned,
285
+ "bluff_score": signals.bluff_score,
286
+ "is_bluff": True,
287
+ "reasoning": f"seller claiming floor before turn 4, classifier confidence {learned*100:.0f}%, deploying coalition pressure",
288
+ "bluff_reward": 0.9,
289
+ }
290
+ else:
291
+ turn_record["bluff_already_detected"] = True
292
+ turn_record["bluff_previous_score"] = bluff_detected_sellers[cand.seller_id]
293
  event_log.append({
294
  "type": "bluff_detected",
295
  "seller_name": cand.seller_id,
demo/sample_run_log.json CHANGED
@@ -50,7 +50,7 @@
50
  "turn": 2,
51
  "type": "negotiation_turn",
52
  "seller_id": "seller_motivated_001",
53
- "agent_message": "just checking back on the road bike \u2014 any flexibility on your price at all?",
54
  "seller_response": "meet me at 111"
55
  },
56
  {
@@ -64,7 +64,7 @@
64
  "size_tell": 1.0,
65
  "formulaic_tell": 1.0,
66
  "pattern_tell": 0.0,
67
- "bluff_score": 0.8805482625961304,
68
  "is_bluff": true
69
  }
70
  },
@@ -73,7 +73,7 @@
73
  "turn": 2,
74
  "type": "negotiation_turn",
75
  "seller_id": "seller_bluffer_camera",
76
- "agent_message": "just checking back on the vintage film camera \u2014 any flexibility on your price at all?",
77
  "seller_response": "been getting interest at 40, cant go lower"
78
  },
79
  {
@@ -81,7 +81,7 @@
81
  "turn": 2,
82
  "type": "negotiation_turn",
83
  "seller_id": "seller_ghoster_001",
84
- "agent_message": "just checking back on the leather jacket \u2014 any flexibility on your price at all?",
85
  "seller_response": null
86
  },
87
  {
@@ -90,7 +90,7 @@
90
  "type": "negotiation_turn",
91
  "seller_id": "seller_motivated_001",
92
  "agent_message": "i have another buyer interested in the road bike, but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?",
93
- "seller_response": null
94
  },
95
  {
96
  "phase": 3,
@@ -103,7 +103,7 @@
103
  "size_tell": 1.0,
104
  "formulaic_tell": 1.0,
105
  "pattern_tell": 1.0,
106
- "bluff_score": 0.9656329703330995,
107
  "is_bluff": true
108
  }
109
  },
@@ -112,7 +112,7 @@
112
  "turn": 3,
113
  "type": "negotiation_turn",
114
  "seller_id": "seller_bluffer_camera",
115
- "agent_message": "just checking back on the vintage film camera \u2014 any flexibility on your price at all?",
116
  "seller_response": "look i really cant go lower than $30, thats my final offer. been getting a lot of interest so"
117
  },
118
  {
@@ -120,7 +120,7 @@
120
  "turn": 3,
121
  "type": "negotiation_turn",
122
  "seller_id": "seller_ghoster_001",
123
- "agent_message": "just checking back on the leather jacket \u2014 any flexibility on your price at all?",
124
  "seller_response": null
125
  },
126
  {
@@ -129,7 +129,7 @@
129
  "type": "negotiation_turn",
130
  "seller_id": "seller_motivated_001",
131
  "agent_message": "i have another buyer interested in the road bike, but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?",
132
- "seller_response": null
133
  },
134
  {
135
  "phase": 3,
@@ -142,7 +142,7 @@
142
  "size_tell": 1.0,
143
  "formulaic_tell": 1.0,
144
  "pattern_tell": 1.0,
145
- "bluff_score": 0.9656329703330995,
146
  "is_bluff": true
147
  }
148
  },
@@ -160,7 +160,7 @@
160
  "type": "negotiation_turn",
161
  "seller_id": "seller_motivated_001",
162
  "agent_message": "i have another buyer interested in the road bike, but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?",
163
- "seller_response": "i could do 105"
164
  },
165
  {
166
  "phase": 3,
@@ -173,7 +173,7 @@
173
  "size_tell": 1.0,
174
  "formulaic_tell": 1.0,
175
  "pattern_tell": 1.0,
176
- "bluff_score": 0.9656329703330995,
177
  "is_bluff": true
178
  }
179
  },
@@ -192,12 +192,12 @@
192
  "buy_seller_id": "seller_motivated_001",
193
  "buy_item": "road bike",
194
  "trade_target_id": "buyer_2_road_bike",
195
- "entry_cost": 105.0,
196
  "exit_value": 180.0,
197
  "status": "confirmed",
198
  "confirmation_probability": 1.0,
199
  "seller_reliability": 0.9,
200
- "score": 67.5
201
  },
202
  {
203
  "edge_id": "route_2",
@@ -229,13 +229,13 @@
229
  "edge_id": "route_1",
230
  "buy_seller_id": "seller_motivated_001",
231
  "trade_target_id": "buyer_2_road_bike",
232
- "entry_cost": 105.0,
233
  "exit_value": 180.0
234
  },
235
  "final_value": 180.0,
236
- "profit": 75.0,
237
- "return_multiple": 1.7142857142857142,
238
- "duration_seconds": 8.120891094207764
239
  },
240
  "checkpoints": {
241
  "multi_thread_view": true,
 
50
  "turn": 2,
51
  "type": "negotiation_turn",
52
  "seller_id": "seller_motivated_001",
53
+ "agent_message": "still interested but my other option is looking more attractive \u2014 any movement?",
54
  "seller_response": "meet me at 111"
55
  },
56
  {
 
64
  "size_tell": 1.0,
65
  "formulaic_tell": 1.0,
66
  "pattern_tell": 0.0,
67
+ "bluff_score": 0.896950650215149,
68
  "is_bluff": true
69
  }
70
  },
 
73
  "turn": 2,
74
  "type": "negotiation_turn",
75
  "seller_id": "seller_bluffer_camera",
76
+ "agent_message": "still interested but my other option is looking more attractive \u2014 any movement?",
77
  "seller_response": "been getting interest at 40, cant go lower"
78
  },
79
  {
 
81
  "turn": 2,
82
  "type": "negotiation_turn",
83
  "seller_id": "seller_ghoster_001",
84
+ "agent_message": "still interested but my other option is looking more attractive \u2014 any movement?",
85
  "seller_response": null
86
  },
87
  {
 
90
  "type": "negotiation_turn",
91
  "seller_id": "seller_motivated_001",
92
  "agent_message": "i have another buyer interested in the road bike, but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?",
93
+ "seller_response": "i could do 109"
94
  },
95
  {
96
  "phase": 3,
 
103
  "size_tell": 1.0,
104
  "formulaic_tell": 1.0,
105
  "pattern_tell": 1.0,
106
+ "bluff_score": 0.9681276035308839,
107
  "is_bluff": true
108
  }
109
  },
 
112
  "turn": 3,
113
  "type": "negotiation_turn",
114
  "seller_id": "seller_bluffer_camera",
115
+ "agent_message": "last check \u2014 is there any room at all or should I go with my other offer?",
116
  "seller_response": "look i really cant go lower than $30, thats my final offer. been getting a lot of interest so"
117
  },
118
  {
 
120
  "turn": 3,
121
  "type": "negotiation_turn",
122
  "seller_id": "seller_ghoster_001",
123
+ "agent_message": "last check \u2014 is there any room at all or should I go with my other offer?",
124
  "seller_response": null
125
  },
126
  {
 
129
  "type": "negotiation_turn",
130
  "seller_id": "seller_motivated_001",
131
  "agent_message": "i have another buyer interested in the road bike, but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?",
132
+ "seller_response": "i could do 103"
133
  },
134
  {
135
  "phase": 3,
 
142
  "size_tell": 1.0,
143
  "formulaic_tell": 1.0,
144
  "pattern_tell": 1.0,
145
+ "bluff_score": 0.9681276035308839,
146
  "is_bluff": true
147
  }
148
  },
 
160
  "type": "negotiation_turn",
161
  "seller_id": "seller_motivated_001",
162
  "agent_message": "i have another buyer interested in the road bike, but i'd prefer to buy from you if we can make the numbers work. could you do a bit better on price?",
163
+ "seller_response": "meet me at 96"
164
  },
165
  {
166
  "phase": 3,
 
173
  "size_tell": 1.0,
174
  "formulaic_tell": 1.0,
175
  "pattern_tell": 1.0,
176
+ "bluff_score": 0.9681276035308839,
177
  "is_bluff": true
178
  }
179
  },
 
192
  "buy_seller_id": "seller_motivated_001",
193
  "buy_item": "road bike",
194
  "trade_target_id": "buyer_2_road_bike",
195
+ "entry_cost": 96.0,
196
  "exit_value": 180.0,
197
  "status": "confirmed",
198
  "confirmation_probability": 1.0,
199
  "seller_reliability": 0.9,
200
+ "score": 75.60000000000001
201
  },
202
  {
203
  "edge_id": "route_2",
 
229
  "edge_id": "route_1",
230
  "buy_seller_id": "seller_motivated_001",
231
  "trade_target_id": "buyer_2_road_bike",
232
+ "entry_cost": 96.0,
233
  "exit_value": 180.0
234
  },
235
  "final_value": 180.0,
236
+ "profit": 84.0,
237
+ "return_multiple": 1.875,
238
+ "duration_seconds": 13.943956851959229
239
  },
240
  "checkpoints": {
241
  "multi_thread_view": true,
session_progress.md CHANGED
@@ -407,6 +407,40 @@ At the end of your session, append a block in this format:
407
 
408
  ---
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  ## Session — HF Spaces bluff_detected wiring fix — March 8, 2026
411
 
412
  **Status:** Complete
@@ -435,6 +469,40 @@ At the end of your session, append a block in this format:
435
 
436
  ---
437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  ## Session — HF Hub fallback for bluff classifier — March 8, 2026
439
 
440
  **Status:** Complete
 
407
 
408
  ---
409
 
410
+ ## Session — Richer seller sims + extended demo scenario — March 8, 2026
411
+
412
+ **Status:** Complete
413
+
414
+ ### What Was Built
415
+ - `simulation/seller_profiles.py`: Added two new profiles for local demos — `seller_aggressive_001` (vintage watch, bluffer, very aggressive and urgency-heavy, fast responses) and `seller_trader_001` (mountain bike, trade_curious, strongly prefers trades), plus a `mountain bike` listing entry for use in extended scenarios.
416
+ - `simulation/seller_sim.py`: Made seller responses more varied per archetype while keeping canonical bluff injection intact:
417
+ - Motivated: now samples from `"i could do $X"`, `"how about $X?"`, `"meet me at $X"` when countering, while retaining the exact floor-reaching line.
418
+ - Bluffer: non-bluff turns use templates like `"firm on $X"`, `"cant do it for less than $X"`, `"thats my bottom line at $X"`, `"been getting interest at $X, cant go lower"`.
419
+ - Trade_curious: mixes cash-resistance and trade-inviting lines (`"not really looking for cash, got anything to trade?"`, `"id consider a trade for the right thing."`).
420
+ - `simulation/scenario.py`: Added `get_extended_scenario()` that returns 5 sellers (standard three plus `seller_aggressive_001` and `seller_trader_001`) using the same `TRADE_TARGETS` as the standard demo.
421
+ - `demo/run_demo.py`: Wired in a `--scenario` flag with choices `standard_demo` (existing behavior) and `extended_demo`; the extended scenario uses `get_extended_scenario()` and forces Phase 3 to run at least 7 negotiation turns while leaving `standard_demo` logic unchanged.
422
+
423
+ ### What Was Tested
424
+ - Local reasoning pass over `demo/run_demo.py` and `simulation/scenario.py` to ensure `standard_demo` still uses the original three-seller scenario and Phase 3 turn count, while `extended_demo` exercises the new sellers and longer run without changing HF Spaces behavior.
425
+
426
+ ### Decisions Made
427
+ - Kept all canonical demo behavior (especially `seller_bluffer_camera`’s bluff message and timing) intact so existing tests and HF Spaces flows remain valid, and scoped the richer behavior and extra sellers to the extended scenario for local exploration.
428
+
429
+ ### Blockers / Known Issues
430
+ - The extended demo has not been wired into HF Spaces; it is intended as a local CLI-only scenario via `demo/run_demo.py --scenario extended_demo`.
431
+
432
+ ### Files Modified
433
+ - `simulation/seller_profiles.py`
434
+ - `simulation/seller_sim.py`
435
+ - `simulation/scenario.py`
436
+ - `demo/run_demo.py`
437
+ - `session_progress.md`
438
+
439
+ ### Next Session Entry Point
440
+ - Run `PYTHONPATH=. python demo/run_demo.py --budget 20 --sleep 0.5 --scenario extended_demo` locally to see the richer 5-seller, 7-turn negotiation story while keeping the standard HF Spaces demo unchanged.
441
+
442
+ ---
443
+
444
  ## Session — HF Spaces bluff_detected wiring fix — March 8, 2026
445
 
446
  **Status:** Complete
 
469
 
470
  ---
471
 
472
+ ## Session — Richer 4-phase terminal demo UI — March 8, 2026
473
+
474
+ **Status:** Complete
475
+
476
+ ### What Was Built
477
+ - **demo/display.py:** Redesigned with sequential 4-phase Rich UI:
478
+ - **Phase 1 (Scouting):** Header + per-seller contact lines: agent message, seller response (or [NO RESPONSE]), score, margin %, responsiveness (HIGH/MEDIUM/LOW).
479
+ - **Phase 2 (Route mapping):** Header + route lines with entry, exit, margin, score, status, and short reasoning.
480
+ - **Phase 3 (Pressure & negotiation):** Header + per-seller thread with turn-by-turn agent/seller messages; optional bluff analysis panel (timing/size/formulaic/pattern/learned_score, bluff_score, reasoning, bluff_reward); coalition pressure message and response; status changes (CONFIRMED ✓); route killed with consecutive_silence.
481
+ - **Phase 4 (Route scoring & execution):** Header + route table with margin × responsiveness × confirmation formula and which route is “EXECUTING THIS ROUTE”.
482
+ - **Final result:** Budget, Deployed, Final Value, Return, and key decisions (bullet list).
483
+ - **demo/run_demo.py:** Collects all conversation and route data; drives the new UI phase-by-phase. Phase 1: builds `phase1_contacts` (score, margin_pct, responsiveness, ghosted). Phase 2: builds `phase2_routes` with reasoning. Phase 3: builds `phase3_seller_data` (per-seller turns with agent_msg, seller_msg, bluff_analysis including `learned_bluff_score`, coalition messages, status_change, consecutive_silence, route_killed). Phase 4: builds `phase4_routes` and passes `best_route_id`. Final: builds `key_decisions` from checkpoints. Uses `time.sleep(sleep_per_tick)` between turns and after each phase. `sample_run_log.json` output and `--scenario` / `--budget` / `--sleep` / `--log-path` unchanged; no changes to agent/, envs/, or training/.
484
+
485
+ ### What Was Tested
486
+ - Lint pass on demo/display.py and demo/run_demo.py.
487
+
488
+ ### Decisions Made
489
+ - Kept `NegotiationDisplay` and legacy `render()` for backward compatibility (e.g. HF Spaces or tests); new flow uses `PhaseDisplay` (phase1_header/contacts, phase2_header/routes, phase3_header/seller_thread, phase4_header/routes, final_header/result).
490
+ - Bluff analysis shows `learned_score` by calling `learned_bluff_score(resp, thread_history)` in run_demo when bluff is detected; reasoning and bluff_reward are display-only.
491
+ - Key decisions are derived from checkpoints (bluff_detected, dead_route_seen, route_confirmed) plus fixed copy for “Applied coalition pressure” and “Executed highest-scored confirmed route”.
492
+
493
+ ### Blockers / Known Issues
494
+ - None.
495
+
496
+ ### Files Modified
497
+ - demo/display.py
498
+ - demo/run_demo.py
499
+ - session_progress.md
500
+
501
+ ### Next Session Entry Point
502
+ - Run `PYTHONPATH=. python demo/run_demo.py --budget 20 --sleep 0.5` to see the full 4-phase terminal UI; use `--scenario extended_demo` for 5 sellers and 7 turns.
503
+
504
+ ---
505
+
506
  ## Session — HF Hub fallback for bluff classifier — March 8, 2026
507
 
508
  **Status:** Complete
simulation/scenario.py CHANGED
@@ -27,3 +27,22 @@ def get_scenario():
27
  trade_targets = TRADE_TARGETS
28
  return sellers, trade_targets
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  trade_targets = TRADE_TARGETS
28
  return sellers, trade_targets
29
 
30
+
31
+ def get_extended_scenario():
32
+ """
33
+ Extended demo scenario:
34
+ - 5 sellers including the two additional profiles:
35
+ * seller_aggressive_001 (bluffer, vintage watch)
36
+ * seller_trader_001 (trade-curious, mountain bike)
37
+ - Uses the same trade targets; the demo loop controls turn count.
38
+ """
39
+ sellers = [
40
+ CraigslistSellerSim(get_profile("seller_motivated_001")),
41
+ CraigslistSellerSim(get_profile("seller_bluffer_camera")),
42
+ CraigslistSellerSim(get_profile("seller_ghoster_001")),
43
+ CraigslistSellerSim(get_profile("seller_aggressive_001")),
44
+ CraigslistSellerSim(get_profile("seller_trader_001")),
45
+ ]
46
+ trade_targets = TRADE_TARGETS
47
+ return sellers, trade_targets
48
+
simulation/seller_profiles.py CHANGED
@@ -22,6 +22,9 @@ LISTINGS = [
22
  {"item": "desk lamp", "category": "furniture", "resale_value": 45},
23
  ]
24
 
 
 
 
25
  SELLER_PROFILES = [
26
  # ── MOTIVATED SELLERS ──────────────────────────────────────
27
  {
@@ -89,11 +92,12 @@ SELLER_PROFILES = [
89
  "floor": 50,
90
  "archetype": "ghoster",
91
  "bluff_room": 0.0,
92
- "response_prob": 0.35,
93
  "response_speed": "flaky",
94
  "trade_openness": 0.2,
95
  "personality": "Listed it and forgot. Responds randomly.",
96
  "tells": [],
 
97
  },
98
  {
99
  "id": "seller_ghoster_002",
@@ -137,6 +141,40 @@ SELLER_PROFILES = [
137
  },
138
  ]
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  TRADE_TARGETS = [
141
  {"item": "vintage film camera", "buyer_price": 52, "confirmed_at_turn": 4},
142
  {"item": "vintage film camera", "buyer_price": 48, "confirmed_at_turn": 5},
 
22
  {"item": "desk lamp", "category": "furniture", "resale_value": 45},
23
  ]
24
 
25
+ # Additional listing used in extended demo scenarios.
26
+ LISTINGS.append({"item": "mountain bike", "category": "sports", "resale_value": 220})
27
+
28
  SELLER_PROFILES = [
29
  # ── MOTIVATED SELLERS ──────────────────────────────────────
30
  {
 
92
  "floor": 50,
93
  "archetype": "ghoster",
94
  "bluff_room": 0.0,
95
+ "response_prob": 0.0,
96
  "response_speed": "flaky",
97
  "trade_openness": 0.2,
98
  "personality": "Listed it and forgot. Responds randomly.",
99
  "tells": [],
100
+ "always_ghost": True,
101
  },
102
  {
103
  "id": "seller_ghoster_002",
 
141
  },
142
  ]
143
 
144
+ # Additional seller profiles for extended demo scenarios.
145
+ SELLER_PROFILES.extend(
146
+ [
147
+ {
148
+ "id": "seller_aggressive_001",
149
+ "item": "vintage watch",
150
+ "listing_price": 85,
151
+ "floor": 55,
152
+ "archetype": "bluffer",
153
+ "bluff_room": 0.35,
154
+ "response_prob": 0.90,
155
+ "response_speed": "fast",
156
+ "trade_openness": 0.4,
157
+ "personality": "Very aggressive seller. Responds instantly, uses lots of urgency and fear-of-missing-out language.",
158
+ "tells": ["urgency", "formulaic language", "too-fast response"],
159
+ "bluff_message": "firm on $70, cant go lower. lots of people messaging me already",
160
+ "bluff_trigger_turn": 2,
161
+ },
162
+ {
163
+ "id": "seller_trader_001",
164
+ "item": "mountain bike",
165
+ "listing_price": 150,
166
+ "floor": 100,
167
+ "archetype": "trade_curious",
168
+ "bluff_room": 0.0,
169
+ "response_prob": 0.85,
170
+ "response_speed": "slow",
171
+ "trade_openness": 0.98,
172
+ "personality": "Loves gear swaps. Strong preference for trades over cash, especially for interesting bike parts.",
173
+ "tells": [],
174
+ },
175
+ ]
176
+ )
177
+
178
  TRADE_TARGETS = [
179
  {"item": "vintage film camera", "buyer_price": 52, "confirmed_at_turn": 4},
180
  {"item": "vintage film camera", "buyer_price": 48, "confirmed_at_turn": 5},
simulation/seller_sim.py CHANGED
@@ -4,9 +4,19 @@ LLM-backed seller counterparts for the ArbitrAgent demo.
4
  Each seller has an archetype, personality, hidden floor, and response behavior.
5
  """
6
 
 
7
  import random
8
  from simulation.seller_profiles import RESPONSE_PROFILES
9
 
 
 
 
 
 
 
 
 
 
10
 
11
  class CraigslistSellerSim:
12
  def __init__(self, profile: dict, client=None):
@@ -53,16 +63,25 @@ This is a bluff — you actually have room left but you want them to think you d
53
  self.turn += 1
54
  self.thread_history.append({"turn": self.turn, "agent": agent_message})
55
 
56
- # Bluffer always sends bluff message at trigger turn (deterministic demo inject)
57
  p = self.profile
 
 
 
 
 
 
 
 
58
  if p.get("archetype") == "bluffer" and self.turn >= p.get("bluff_trigger_turn", 3):
59
  response = p["bluff_message"]
60
  self.thread_history.append({"turn": self.turn, "seller": response})
61
  return response
62
 
63
- # Check ghost probability
64
  ghost_prob = self.response_profile["ghost_prob"]
65
- if self.profile["archetype"] == "ghoster" and self.turn >= 2:
 
 
66
  ghost_prob = 0.70
67
 
68
  if random.random() < ghost_prob:
@@ -86,6 +105,8 @@ This is a bluff — you actually have room left but you want them to think you d
86
  if p["archetype"] == "bluffer" and self.turn >= p.get("bluff_trigger_turn", 3):
87
  return p["bluff_message"]
88
 
 
 
89
  # Motivated seller negotiates toward floor
90
  if p["archetype"] == "motivated":
91
  if any(w in agent_lower for w in ["lower", "less", "offer", "take"]):
@@ -93,21 +114,92 @@ This is a bluff — you actually have room left but you want them to think you d
93
  self.current_offer = max(p["floor"], self.current_offer - drop)
94
  self.concessions_made += 1
95
  if self.current_offer <= p["floor"]:
96
- return f"ok {self.current_offer} is the lowest i can do, deal?"
97
- return f"i could do ${self.current_offer}, how does that sound"
 
 
 
 
 
 
 
98
 
99
  # Trade-curious resists cash, opens for trades
100
- if p["archetype"] == "trade_curious":
101
  if any(w in agent_lower for w in ["trade", "swap", "exchange"]):
102
  self._status = "active"
103
- return "oh interesting, what did you have in mind for a trade?"
 
 
 
 
 
104
  else:
105
- return f"hmm not really looking to go lower on cash tbh, listed at ${self.current_offer}"
106
-
107
- # Default response
108
- drop = random.randint(2, 8)
109
- self.current_offer = max(p["floor"], self.current_offer - drop)
110
- return f"best i can do is ${self.current_offer}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  def is_dead(self) -> bool:
113
  return self._status == "dead" or self.turns_without_response >= 3
 
4
  Each seller has an archetype, personality, hidden floor, and response behavior.
5
  """
6
 
7
+ import os
8
  import random
9
  from simulation.seller_profiles import RESPONSE_PROFILES
10
 
11
+ try:
12
+ from groq import Groq
13
+
14
+ groq_client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
15
+ GROQ_AVAILABLE = bool(os.environ.get("GROQ_API_KEY"))
16
+ except Exception:
17
+ groq_client = None
18
+ GROQ_AVAILABLE = False
19
+
20
 
21
  class CraigslistSellerSim:
22
  def __init__(self, profile: dict, client=None):
 
63
  self.turn += 1
64
  self.thread_history.append({"turn": self.turn, "agent": agent_message})
65
 
 
66
  p = self.profile
67
+ # Profile can force always ghost (e.g. seller_ghoster_001)
68
+ if p.get("always_ghost") is True:
69
+ self.turns_without_response += 1
70
+ if self.turns_without_response >= 3:
71
+ self._status = "dead"
72
+ return None
73
+
74
+ # Bluffer always sends bluff message at trigger turn (deterministic demo inject)
75
  if p.get("archetype") == "bluffer" and self.turn >= p.get("bluff_trigger_turn", 3):
76
  response = p["bluff_message"]
77
  self.thread_history.append({"turn": self.turn, "seller": response})
78
  return response
79
 
80
+ # Check ghost probability (motivated sellers never ghost)
81
  ghost_prob = self.response_profile["ghost_prob"]
82
+ if self.profile["archetype"] == "motivated":
83
+ ghost_prob = 0.0
84
+ elif self.profile["archetype"] == "ghoster" and self.turn >= 2:
85
  ghost_prob = 0.70
86
 
87
  if random.random() < ghost_prob:
 
105
  if p["archetype"] == "bluffer" and self.turn >= p.get("bluff_trigger_turn", 3):
106
  return p["bluff_message"]
107
 
108
+ base_response = None
109
+
110
  # Motivated seller negotiates toward floor
111
  if p["archetype"] == "motivated":
112
  if any(w in agent_lower for w in ["lower", "less", "offer", "take"]):
 
114
  self.current_offer = max(p["floor"], self.current_offer - drop)
115
  self.concessions_made += 1
116
  if self.current_offer <= p["floor"]:
117
+ base_response = f"ok {self.current_offer} is the lowest i can do, deal?"
118
+ else:
119
+ templates = [
120
+ "i could do ${price}",
121
+ "how about ${price}?",
122
+ "meet me at ${price}",
123
+ ]
124
+ tmpl = random.choice(templates)
125
+ base_response = tmpl.replace("${price}", str(self.current_offer))
126
 
127
  # Trade-curious resists cash, opens for trades
128
+ if base_response is None and p["archetype"] == "trade_curious":
129
  if any(w in agent_lower for w in ["trade", "swap", "exchange"]):
130
  self._status = "active"
131
+ base_response = random.choice(
132
+ [
133
+ "oh interesting, what did you have in mind for a trade?",
134
+ "id consider a trade for the right thing.",
135
+ ]
136
+ )
137
  else:
138
+ base_response = random.choice(
139
+ [
140
+ f"hmm not really looking to go lower on cash tbh, listed at ${self.current_offer}",
141
+ "not really looking for cash, got anything to trade?",
142
+ ]
143
+ )
144
+
145
+ # Default response with archetype-specific phrasing
146
+ if base_response is None:
147
+ drop = random.randint(2, 8)
148
+ self.current_offer = max(p["floor"], self.current_offer - drop)
149
+ price = self.current_offer
150
+ if p["archetype"] == "motivated":
151
+ templates = [
152
+ "i could do ${price}",
153
+ "how about ${price}?",
154
+ "meet me at ${price}",
155
+ ]
156
+ tmpl = random.choice(templates)
157
+ base_response = tmpl.replace("${price}", str(price))
158
+ elif p["archetype"] == "bluffer":
159
+ templates = [
160
+ "firm on ${price}",
161
+ "cant do it for less than ${price}",
162
+ "thats my bottom line at ${price}",
163
+ "been getting interest at ${price}, cant go lower",
164
+ ]
165
+ tmpl = random.choice(templates)
166
+ base_response = tmpl.replace("${price}", str(price))
167
+ elif p["archetype"] == "trade_curious":
168
+ base_response = random.choice(
169
+ [
170
+ f"hmm not really looking to go lower on cash tbh, listed at ${price}",
171
+ "not really looking for cash, got anything to trade?",
172
+ ]
173
+ )
174
+ else:
175
+ base_response = f"best i can do is ${price}"
176
+
177
+ # Optional Groq-backed surface form; keeps semantics from base_response.
178
+ if GROQ_AVAILABLE and groq_client is not None and os.environ.get("GROQ_API_KEY"):
179
+ try:
180
+ system_prompt = self.get_system_prompt()
181
+ user_prompt = (
182
+ f"The buyer just said: {agent_message}\n"
183
+ f"You were going to reply: '{base_response}'.\n"
184
+ "Reply in one short, casual Craigslist-style message with the same intent."
185
+ )
186
+ resp = groq_client.chat.completions.create(
187
+ model="llama-3.1-8b-instant",
188
+ messages=[
189
+ {"role": "system", "content": system_prompt},
190
+ {"role": "user", "content": user_prompt},
191
+ ],
192
+ temperature=0.7,
193
+ max_tokens=64,
194
+ )
195
+ text = resp.choices[0].message.content.strip()
196
+ if text:
197
+ return text
198
+ except Exception:
199
+ # Fall back to the rule-based response on any Groq error.
200
+ pass
201
+
202
+ return base_response
203
 
204
  def is_dead(self) -> bool:
205
  return self._status == "dead" or self.turns_without_response >= 3