Spaces:
Runtime error
Runtime error
AbeBhatti commited on
Commit ·
da27912
1
Parent(s): 377c8b2
final demo polish
Browse files- agent/agent_llm.py +1 -1
- agent/arbitragent.py +1 -1
- demo/display.py +1 -5
- demo/run_demo.py +22 -12
- demo/sample_run_log.json +18 -18
- session_progress.md +68 -0
- simulation/scenario.py +19 -0
- simulation/seller_profiles.py +39 -1
- simulation/seller_sim.py +105 -13
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 =
|
| 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}
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
"
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 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": "
|
| 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.
|
| 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": "
|
| 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": "
|
| 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":
|
| 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.
|
| 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": "
|
| 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": "
|
| 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":
|
| 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.
|
| 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": "
|
| 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.
|
| 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":
|
| 196 |
"exit_value": 180.0,
|
| 197 |
"status": "confirmed",
|
| 198 |
"confirmation_probability": 1.0,
|
| 199 |
"seller_reliability": 0.9,
|
| 200 |
-
"score":
|
| 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":
|
| 233 |
"exit_value": 180.0
|
| 234 |
},
|
| 235 |
"final_value": 180.0,
|
| 236 |
-
"profit":
|
| 237 |
-
"return_multiple": 1.
|
| 238 |
-
"duration_seconds":
|
| 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.
|
| 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"] == "
|
|
|
|
|
|
|
| 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 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
else:
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|