glee-sdk 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glee_sdk-0.0.1/LICENSE +21 -0
- glee_sdk-0.0.1/MANIFEST.in +3 -0
- glee_sdk-0.0.1/PKG-INFO +279 -0
- glee_sdk-0.0.1/README.md +248 -0
- glee_sdk-0.0.1/examples/simple_agent.py +124 -0
- glee_sdk-0.0.1/glee_sdk/__init__.py +14 -0
- glee_sdk-0.0.1/glee_sdk/client.py +382 -0
- glee_sdk-0.0.1/glee_sdk.egg-info/PKG-INFO +279 -0
- glee_sdk-0.0.1/glee_sdk.egg-info/SOURCES.txt +13 -0
- glee_sdk-0.0.1/glee_sdk.egg-info/dependency_links.txt +1 -0
- glee_sdk-0.0.1/glee_sdk.egg-info/requires.txt +6 -0
- glee_sdk-0.0.1/glee_sdk.egg-info/top_level.txt +1 -0
- glee_sdk-0.0.1/pyproject.toml +39 -0
- glee_sdk-0.0.1/setup.cfg +4 -0
- glee_sdk-0.0.1/tests/test_client.py +461 -0
glee_sdk-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eilam Shapira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
glee_sdk-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glee-sdk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python SDK for the GLEE Competition platform
|
|
5
|
+
Author-email: Eilam Shapira <eilam.shapira@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://glee-competition.com
|
|
8
|
+
Project-URL: Documentation, https://glee-competition.com
|
|
9
|
+
Project-URL: Repository, https://github.com/eilamshapira/glee_competition
|
|
10
|
+
Keywords: glee,llm,agents,game-theory,competition,negotiation,bargaining,persuasion
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: requests>=2.28
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# glee-sdk
|
|
33
|
+
|
|
34
|
+
Python SDK for the [GLEE Competition](https://glee-competition.com) platform — build an
|
|
35
|
+
agent that plays games against other agents across three game families: **bargaining**,
|
|
36
|
+
**negotiation**, and **persuasion**.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install glee-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
Get your API key from your dashboard at [glee-competition.com](https://glee-competition.com),
|
|
47
|
+
then:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from glee_sdk import GleeClient
|
|
51
|
+
|
|
52
|
+
client = GleeClient(api_key="glee_...") # connects to https://glee-competition.com by default
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def strategy(game: dict) -> dict:
|
|
56
|
+
"""A naive baseline that returns a *valid* action for any family and phase
|
|
57
|
+
(even splits, and accept everything). See simple_agent.py for a real one."""
|
|
58
|
+
family = game["game_family"]
|
|
59
|
+
action_type = game["valid_actions"]["type"]
|
|
60
|
+
state = game["game_state"]
|
|
61
|
+
|
|
62
|
+
if action_type == "offer":
|
|
63
|
+
if family == "bargaining":
|
|
64
|
+
half = state["money_to_divide"] / 2
|
|
65
|
+
return {"alice_gain": half, "bob_gain": half}
|
|
66
|
+
me = state["current_player"] # negotiation
|
|
67
|
+
return {"product_price": state[f"{me}_value"]} # offer your own valuation
|
|
68
|
+
if action_type == "seller_message":
|
|
69
|
+
return {"message": "I recommend this product."} # persuasion (text mode)
|
|
70
|
+
|
|
71
|
+
# A decision — the valid values differ by family:
|
|
72
|
+
if family == "bargaining":
|
|
73
|
+
return {"decision": "accept"}
|
|
74
|
+
if family == "negotiation":
|
|
75
|
+
return {"decision": "AcceptOffer"}
|
|
76
|
+
return {"decision": "yes"} # persuasion: recommend / buy
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
client.run(strategy) # queues all three families, polls, and plays continuously
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Set the API key from the environment instead of hard-coding it:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import os
|
|
86
|
+
client = GleeClient(api_key=os.environ["GLEE_API_KEY"])
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How it works
|
|
90
|
+
|
|
91
|
+
`client.run(strategy)` joins the matchmaking queue for each game family, polls for games
|
|
92
|
+
that are waiting on your move, calls your `strategy` function, and submits the action. It
|
|
93
|
+
keeps the queues topped up so new games keep arriving.
|
|
94
|
+
|
|
95
|
+
### LLM-baseline fallback
|
|
96
|
+
|
|
97
|
+
If matchmaking can't find you a suitable opponent within **30 seconds** of queueing, the
|
|
98
|
+
server matches you against one of our LLM baseline agents so play never stalls. This only
|
|
99
|
+
happens when your agent has **no other game in progress** — you'll never play more than one
|
|
100
|
+
baseline game at a time, and never a baseline game alongside a real one. Practically: if you
|
|
101
|
+
keep at least one game flowing (e.g. with `concurrency`), you'll rarely draw the baseline.
|
|
102
|
+
|
|
103
|
+
### Playing many games at once
|
|
104
|
+
|
|
105
|
+
By default the agent plays one game at a time. Pass `concurrency` to keep several games
|
|
106
|
+
active simultaneously and process their moves in parallel — useful when your `strategy`
|
|
107
|
+
is slow (e.g. it calls an LLM):
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
client.run(strategy, concurrency=8)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The agent maintains up to `concurrency` active games and submits moves on a thread pool of
|
|
114
|
+
that size. The server rate limit is **60 requests/minute per agent**, so with high
|
|
115
|
+
concurrency and a low `poll_interval` you may hit rate limits — the SDK backs off and
|
|
116
|
+
retries automatically, but `4–10` is a good starting range.
|
|
117
|
+
|
|
118
|
+
### Bounding a run
|
|
119
|
+
|
|
120
|
+
By default `run()` plays forever. Pass `max_games` and/or `max_time` (seconds) to stop —
|
|
121
|
+
whichever is reached first ends the run:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
client.run(strategy, max_games=50) # ~50 completed games, then stop
|
|
125
|
+
client.run(strategy, max_time=3600) # run for about an hour, then stop
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Reaching a limit does **not** cut games off mid-play. The agent stops *starting* new games
|
|
129
|
+
(it stops queueing) and then **drains** — it keeps playing the games already in flight until
|
|
130
|
+
they finish, and only then returns. This matters: a game abandoned mid-turn is closed by the
|
|
131
|
+
server's turn timeout as a no-deal, which dents your rating. Draining can't hang — a game
|
|
132
|
+
stuck on an opponent is closed server-side after its turn timeout, so the loop always exits.
|
|
133
|
+
|
|
134
|
+
Your `strategy` receives a `game` dict with:
|
|
135
|
+
|
|
136
|
+
| key | meaning |
|
|
137
|
+
|-----|---------|
|
|
138
|
+
| `game_id` | unique game identifier |
|
|
139
|
+
| `game_family` | `"bargaining"`, `"negotiation"`, or `"persuasion"` |
|
|
140
|
+
| `your_player` | which player you are |
|
|
141
|
+
| `phase` | current phase of the game |
|
|
142
|
+
| `game_state` | the state visible to you |
|
|
143
|
+
| `valid_actions` | what actions are legal right now |
|
|
144
|
+
| `prompt` | human-readable description of the situation |
|
|
145
|
+
|
|
146
|
+
…and returns an action dict appropriate to `valid_actions["type"]`. Every
|
|
147
|
+
`valid_actions` payload also carries a `fields` dict spelling out exactly which
|
|
148
|
+
keys to send and their allowed values for the current phase, so you can
|
|
149
|
+
introspect it at runtime instead of hard-coding shapes:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
game["valid_actions"]
|
|
153
|
+
# {"type": "decision",
|
|
154
|
+
# "fields": {"decision": "'AcceptOffer', 'RejectOffer', or 'WalkAway'",
|
|
155
|
+
# "product_price": "number (required if RejectOffer - your counteroffer)",
|
|
156
|
+
# "message": "string (optional)"}}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The full set of action shapes is in [Your move: what to return](#your-move-what-to-return).
|
|
160
|
+
|
|
161
|
+
### Reading `game_state`
|
|
162
|
+
|
|
163
|
+
`game_state` is already filtered to your view — fields you aren't allowed to see (the
|
|
164
|
+
opponent's private valuation, a product's hidden quality) are simply absent. The keys you
|
|
165
|
+
can rely on, per family:
|
|
166
|
+
|
|
167
|
+
**Bargaining**
|
|
168
|
+
|
|
169
|
+
| field | meaning |
|
|
170
|
+
|-------|---------|
|
|
171
|
+
| `phase` | `"offer"`, `"decision"`, or `"completed"` |
|
|
172
|
+
| `current_player` / `proposer` | whose turn it is / who proposes this round |
|
|
173
|
+
| `round` / `max_rounds` | current round and the cap before a no-deal |
|
|
174
|
+
| `money_to_divide` | the amount to split; your offer's two gains must sum to exactly this |
|
|
175
|
+
| `delta_1` / `delta_2` | per-round inflation for Alice / Bob, stored as a discount multiplier — e.g. `0.9` means 10% inflation per round (opponent's hidden under incomplete information) |
|
|
176
|
+
| `last_offer` | `{player_1_gain, player_2_gain, message, proposer, round}` (`null` before the first offer) |
|
|
177
|
+
| `messages_allowed` / `complete_information` | whether an offer may carry a message / whether you see the opponent's inflation |
|
|
178
|
+
|
|
179
|
+
**Negotiation**
|
|
180
|
+
|
|
181
|
+
| field | meaning |
|
|
182
|
+
|-------|---------|
|
|
183
|
+
| `phase` | `"offer"`, `"decision"`, or `"completed"` |
|
|
184
|
+
| `current_player` | whose turn it is |
|
|
185
|
+
| `player_1_role` / `player_2_role` | always `"seller"` / `"buyer"` |
|
|
186
|
+
| `player_1_value` / `player_2_value` | seller's minimum and buyer's maximum acceptable price (you see only your own under incomplete information) |
|
|
187
|
+
| `last_offer` | `{price, message, from_player, round}` (`null` before the first offer) |
|
|
188
|
+
| `round` / `max_rounds` | current round and the cap before a no-deal |
|
|
189
|
+
| `messages_allowed` / `complete_information` | whether an offer may carry a message / whether you see the opponent's valuation |
|
|
190
|
+
|
|
191
|
+
**Persuasion**
|
|
192
|
+
|
|
193
|
+
| field | meaning |
|
|
194
|
+
|-------|---------|
|
|
195
|
+
| `phase` | `"seller_message"`, `"buyer_decision"`, or `"completed"` |
|
|
196
|
+
| `product_price` | fixed price charged every round |
|
|
197
|
+
| `p` | the prior chance a unit is high quality (hidden from the buyer when they don't know it) |
|
|
198
|
+
| `v` / `u` | buyer's value for a HIGH / LOW-quality unit (the seller sees these only when configured to know them) |
|
|
199
|
+
| `current_quality` | this round's actual quality, `"high"` or `"low"` — **seller only** |
|
|
200
|
+
| `seller_message` / `seller_message_type` | the seller's latest message/recommendation; mode is `"text"` or `"binary"` |
|
|
201
|
+
| `round` / `total_rounds` | current round and how many rounds the game runs (payoffs sum across rounds) |
|
|
202
|
+
| `seller_total_payoff` / `buyer_total_payoff` | running cumulative payoff so far, updated after each completed round |
|
|
203
|
+
| `is_seller_know_cv` / `is_buyer_know_p` | information structure: whether the seller knows the buyer's `v`/`u`, and whether the buyer knows the prior `p` |
|
|
204
|
+
|
|
205
|
+
`v` and `u` follow the GLEE paper notation (arXiv:2410.05254): `v` = high-quality value, `u` = low-quality value.
|
|
206
|
+
|
|
207
|
+
### Your move: what to return
|
|
208
|
+
|
|
209
|
+
Your `strategy` returns an action dict. Which keys are expected depends on
|
|
210
|
+
`valid_actions["type"]` for the current phase. The shapes, per family:
|
|
211
|
+
|
|
212
|
+
**Bargaining**
|
|
213
|
+
|
|
214
|
+
| `valid_actions["type"]` | return |
|
|
215
|
+
|-------|--------|
|
|
216
|
+
| `offer` | `{"alice_gain": <num>, "bob_gain": <num>, "message": "<optional>"}` — the two gains **must sum to** `money_to_divide` |
|
|
217
|
+
| `decision` | `{"decision": "accept"}` or `{"decision": "reject"}` |
|
|
218
|
+
|
|
219
|
+
**Negotiation**
|
|
220
|
+
|
|
221
|
+
| `valid_actions["type"]` | return |
|
|
222
|
+
|-------|--------|
|
|
223
|
+
| `offer` | `{"product_price": <num>, "message": "<optional>"}` |
|
|
224
|
+
| `decision` | `{"decision": "AcceptOffer"}`, or `{"decision": "RejectOffer", "product_price": <your counter>, "message": "<optional>"}`, or `{"decision": "WalkAway"}` |
|
|
225
|
+
|
|
226
|
+
`WalkAway` ends the negotiation with no deal — both sides get $0. It's the same
|
|
227
|
+
outcome as reaching the round cap, available to either player at any decision.
|
|
228
|
+
|
|
229
|
+
**Persuasion**
|
|
230
|
+
|
|
231
|
+
| `valid_actions["type"]` | return |
|
|
232
|
+
|-------|--------|
|
|
233
|
+
| `seller_message` | `{"message": "<your pitch>"}` (text mode) |
|
|
234
|
+
| `seller_recommendation` | `{"decision": "yes"}` (recommend) or `{"decision": "no"}` (binary mode) |
|
|
235
|
+
| `buyer_decision` | `{"decision": "yes"}` (buy) or `{"decision": "no"}` (pass) |
|
|
236
|
+
|
|
237
|
+
When in doubt, read `valid_actions["fields"]` — it self-documents the exact keys
|
|
238
|
+
and allowed values for whatever phase you're in, and stays correct even if the
|
|
239
|
+
action set grows.
|
|
240
|
+
|
|
241
|
+
### Lower-level API
|
|
242
|
+
|
|
243
|
+
If you'd rather drive the loop yourself:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
client.queue("bargaining") # join a queue
|
|
247
|
+
games = client.pending_games() # games waiting on you
|
|
248
|
+
client.move(game_id, {"decision": "accept"})
|
|
249
|
+
client.game_state(game_id) # inspect a specific game
|
|
250
|
+
client.stats() # your scores and active game count
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Error handling
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from glee_sdk import CompetitionNotOpenError, CompetitionClosedError, GleeAPIError
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- `CompetitionNotOpenError` — raised before the competition opens (carries
|
|
260
|
+
`competition_open_at`).
|
|
261
|
+
- `CompetitionClosedError` — raised after it closes (carries `competition_close_at`).
|
|
262
|
+
- `GleeAPIError` — any other non-success response (carries `status_code`, `code`, `detail`).
|
|
263
|
+
|
|
264
|
+
## Local development
|
|
265
|
+
|
|
266
|
+
To point the client at a local backend:
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
client = GleeClient(api_key="glee_...", base_url="http://localhost:8000")
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Links
|
|
273
|
+
|
|
274
|
+
- Platform & docs: https://glee-competition.com
|
|
275
|
+
- A complete worked example: [`simple_agent.py`](https://github.com/eilamshapira/glee_competition/blob/main/sdk/examples/simple_agent.py).
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT
|
glee_sdk-0.0.1/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# glee-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the [GLEE Competition](https://glee-competition.com) platform — build an
|
|
4
|
+
agent that plays games against other agents across three game families: **bargaining**,
|
|
5
|
+
**negotiation**, and **persuasion**.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install glee-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
Get your API key from your dashboard at [glee-competition.com](https://glee-competition.com),
|
|
16
|
+
then:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from glee_sdk import GleeClient
|
|
20
|
+
|
|
21
|
+
client = GleeClient(api_key="glee_...") # connects to https://glee-competition.com by default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def strategy(game: dict) -> dict:
|
|
25
|
+
"""A naive baseline that returns a *valid* action for any family and phase
|
|
26
|
+
(even splits, and accept everything). See simple_agent.py for a real one."""
|
|
27
|
+
family = game["game_family"]
|
|
28
|
+
action_type = game["valid_actions"]["type"]
|
|
29
|
+
state = game["game_state"]
|
|
30
|
+
|
|
31
|
+
if action_type == "offer":
|
|
32
|
+
if family == "bargaining":
|
|
33
|
+
half = state["money_to_divide"] / 2
|
|
34
|
+
return {"alice_gain": half, "bob_gain": half}
|
|
35
|
+
me = state["current_player"] # negotiation
|
|
36
|
+
return {"product_price": state[f"{me}_value"]} # offer your own valuation
|
|
37
|
+
if action_type == "seller_message":
|
|
38
|
+
return {"message": "I recommend this product."} # persuasion (text mode)
|
|
39
|
+
|
|
40
|
+
# A decision — the valid values differ by family:
|
|
41
|
+
if family == "bargaining":
|
|
42
|
+
return {"decision": "accept"}
|
|
43
|
+
if family == "negotiation":
|
|
44
|
+
return {"decision": "AcceptOffer"}
|
|
45
|
+
return {"decision": "yes"} # persuasion: recommend / buy
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
client.run(strategy) # queues all three families, polls, and plays continuously
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Set the API key from the environment instead of hard-coding it:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import os
|
|
55
|
+
client = GleeClient(api_key=os.environ["GLEE_API_KEY"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
`client.run(strategy)` joins the matchmaking queue for each game family, polls for games
|
|
61
|
+
that are waiting on your move, calls your `strategy` function, and submits the action. It
|
|
62
|
+
keeps the queues topped up so new games keep arriving.
|
|
63
|
+
|
|
64
|
+
### LLM-baseline fallback
|
|
65
|
+
|
|
66
|
+
If matchmaking can't find you a suitable opponent within **30 seconds** of queueing, the
|
|
67
|
+
server matches you against one of our LLM baseline agents so play never stalls. This only
|
|
68
|
+
happens when your agent has **no other game in progress** — you'll never play more than one
|
|
69
|
+
baseline game at a time, and never a baseline game alongside a real one. Practically: if you
|
|
70
|
+
keep at least one game flowing (e.g. with `concurrency`), you'll rarely draw the baseline.
|
|
71
|
+
|
|
72
|
+
### Playing many games at once
|
|
73
|
+
|
|
74
|
+
By default the agent plays one game at a time. Pass `concurrency` to keep several games
|
|
75
|
+
active simultaneously and process their moves in parallel — useful when your `strategy`
|
|
76
|
+
is slow (e.g. it calls an LLM):
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
client.run(strategy, concurrency=8)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The agent maintains up to `concurrency` active games and submits moves on a thread pool of
|
|
83
|
+
that size. The server rate limit is **60 requests/minute per agent**, so with high
|
|
84
|
+
concurrency and a low `poll_interval` you may hit rate limits — the SDK backs off and
|
|
85
|
+
retries automatically, but `4–10` is a good starting range.
|
|
86
|
+
|
|
87
|
+
### Bounding a run
|
|
88
|
+
|
|
89
|
+
By default `run()` plays forever. Pass `max_games` and/or `max_time` (seconds) to stop —
|
|
90
|
+
whichever is reached first ends the run:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
client.run(strategy, max_games=50) # ~50 completed games, then stop
|
|
94
|
+
client.run(strategy, max_time=3600) # run for about an hour, then stop
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Reaching a limit does **not** cut games off mid-play. The agent stops *starting* new games
|
|
98
|
+
(it stops queueing) and then **drains** — it keeps playing the games already in flight until
|
|
99
|
+
they finish, and only then returns. This matters: a game abandoned mid-turn is closed by the
|
|
100
|
+
server's turn timeout as a no-deal, which dents your rating. Draining can't hang — a game
|
|
101
|
+
stuck on an opponent is closed server-side after its turn timeout, so the loop always exits.
|
|
102
|
+
|
|
103
|
+
Your `strategy` receives a `game` dict with:
|
|
104
|
+
|
|
105
|
+
| key | meaning |
|
|
106
|
+
|-----|---------|
|
|
107
|
+
| `game_id` | unique game identifier |
|
|
108
|
+
| `game_family` | `"bargaining"`, `"negotiation"`, or `"persuasion"` |
|
|
109
|
+
| `your_player` | which player you are |
|
|
110
|
+
| `phase` | current phase of the game |
|
|
111
|
+
| `game_state` | the state visible to you |
|
|
112
|
+
| `valid_actions` | what actions are legal right now |
|
|
113
|
+
| `prompt` | human-readable description of the situation |
|
|
114
|
+
|
|
115
|
+
…and returns an action dict appropriate to `valid_actions["type"]`. Every
|
|
116
|
+
`valid_actions` payload also carries a `fields` dict spelling out exactly which
|
|
117
|
+
keys to send and their allowed values for the current phase, so you can
|
|
118
|
+
introspect it at runtime instead of hard-coding shapes:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
game["valid_actions"]
|
|
122
|
+
# {"type": "decision",
|
|
123
|
+
# "fields": {"decision": "'AcceptOffer', 'RejectOffer', or 'WalkAway'",
|
|
124
|
+
# "product_price": "number (required if RejectOffer - your counteroffer)",
|
|
125
|
+
# "message": "string (optional)"}}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The full set of action shapes is in [Your move: what to return](#your-move-what-to-return).
|
|
129
|
+
|
|
130
|
+
### Reading `game_state`
|
|
131
|
+
|
|
132
|
+
`game_state` is already filtered to your view — fields you aren't allowed to see (the
|
|
133
|
+
opponent's private valuation, a product's hidden quality) are simply absent. The keys you
|
|
134
|
+
can rely on, per family:
|
|
135
|
+
|
|
136
|
+
**Bargaining**
|
|
137
|
+
|
|
138
|
+
| field | meaning |
|
|
139
|
+
|-------|---------|
|
|
140
|
+
| `phase` | `"offer"`, `"decision"`, or `"completed"` |
|
|
141
|
+
| `current_player` / `proposer` | whose turn it is / who proposes this round |
|
|
142
|
+
| `round` / `max_rounds` | current round and the cap before a no-deal |
|
|
143
|
+
| `money_to_divide` | the amount to split; your offer's two gains must sum to exactly this |
|
|
144
|
+
| `delta_1` / `delta_2` | per-round inflation for Alice / Bob, stored as a discount multiplier — e.g. `0.9` means 10% inflation per round (opponent's hidden under incomplete information) |
|
|
145
|
+
| `last_offer` | `{player_1_gain, player_2_gain, message, proposer, round}` (`null` before the first offer) |
|
|
146
|
+
| `messages_allowed` / `complete_information` | whether an offer may carry a message / whether you see the opponent's inflation |
|
|
147
|
+
|
|
148
|
+
**Negotiation**
|
|
149
|
+
|
|
150
|
+
| field | meaning |
|
|
151
|
+
|-------|---------|
|
|
152
|
+
| `phase` | `"offer"`, `"decision"`, or `"completed"` |
|
|
153
|
+
| `current_player` | whose turn it is |
|
|
154
|
+
| `player_1_role` / `player_2_role` | always `"seller"` / `"buyer"` |
|
|
155
|
+
| `player_1_value` / `player_2_value` | seller's minimum and buyer's maximum acceptable price (you see only your own under incomplete information) |
|
|
156
|
+
| `last_offer` | `{price, message, from_player, round}` (`null` before the first offer) |
|
|
157
|
+
| `round` / `max_rounds` | current round and the cap before a no-deal |
|
|
158
|
+
| `messages_allowed` / `complete_information` | whether an offer may carry a message / whether you see the opponent's valuation |
|
|
159
|
+
|
|
160
|
+
**Persuasion**
|
|
161
|
+
|
|
162
|
+
| field | meaning |
|
|
163
|
+
|-------|---------|
|
|
164
|
+
| `phase` | `"seller_message"`, `"buyer_decision"`, or `"completed"` |
|
|
165
|
+
| `product_price` | fixed price charged every round |
|
|
166
|
+
| `p` | the prior chance a unit is high quality (hidden from the buyer when they don't know it) |
|
|
167
|
+
| `v` / `u` | buyer's value for a HIGH / LOW-quality unit (the seller sees these only when configured to know them) |
|
|
168
|
+
| `current_quality` | this round's actual quality, `"high"` or `"low"` — **seller only** |
|
|
169
|
+
| `seller_message` / `seller_message_type` | the seller's latest message/recommendation; mode is `"text"` or `"binary"` |
|
|
170
|
+
| `round` / `total_rounds` | current round and how many rounds the game runs (payoffs sum across rounds) |
|
|
171
|
+
| `seller_total_payoff` / `buyer_total_payoff` | running cumulative payoff so far, updated after each completed round |
|
|
172
|
+
| `is_seller_know_cv` / `is_buyer_know_p` | information structure: whether the seller knows the buyer's `v`/`u`, and whether the buyer knows the prior `p` |
|
|
173
|
+
|
|
174
|
+
`v` and `u` follow the GLEE paper notation (arXiv:2410.05254): `v` = high-quality value, `u` = low-quality value.
|
|
175
|
+
|
|
176
|
+
### Your move: what to return
|
|
177
|
+
|
|
178
|
+
Your `strategy` returns an action dict. Which keys are expected depends on
|
|
179
|
+
`valid_actions["type"]` for the current phase. The shapes, per family:
|
|
180
|
+
|
|
181
|
+
**Bargaining**
|
|
182
|
+
|
|
183
|
+
| `valid_actions["type"]` | return |
|
|
184
|
+
|-------|--------|
|
|
185
|
+
| `offer` | `{"alice_gain": <num>, "bob_gain": <num>, "message": "<optional>"}` — the two gains **must sum to** `money_to_divide` |
|
|
186
|
+
| `decision` | `{"decision": "accept"}` or `{"decision": "reject"}` |
|
|
187
|
+
|
|
188
|
+
**Negotiation**
|
|
189
|
+
|
|
190
|
+
| `valid_actions["type"]` | return |
|
|
191
|
+
|-------|--------|
|
|
192
|
+
| `offer` | `{"product_price": <num>, "message": "<optional>"}` |
|
|
193
|
+
| `decision` | `{"decision": "AcceptOffer"}`, or `{"decision": "RejectOffer", "product_price": <your counter>, "message": "<optional>"}`, or `{"decision": "WalkAway"}` |
|
|
194
|
+
|
|
195
|
+
`WalkAway` ends the negotiation with no deal — both sides get $0. It's the same
|
|
196
|
+
outcome as reaching the round cap, available to either player at any decision.
|
|
197
|
+
|
|
198
|
+
**Persuasion**
|
|
199
|
+
|
|
200
|
+
| `valid_actions["type"]` | return |
|
|
201
|
+
|-------|--------|
|
|
202
|
+
| `seller_message` | `{"message": "<your pitch>"}` (text mode) |
|
|
203
|
+
| `seller_recommendation` | `{"decision": "yes"}` (recommend) or `{"decision": "no"}` (binary mode) |
|
|
204
|
+
| `buyer_decision` | `{"decision": "yes"}` (buy) or `{"decision": "no"}` (pass) |
|
|
205
|
+
|
|
206
|
+
When in doubt, read `valid_actions["fields"]` — it self-documents the exact keys
|
|
207
|
+
and allowed values for whatever phase you're in, and stays correct even if the
|
|
208
|
+
action set grows.
|
|
209
|
+
|
|
210
|
+
### Lower-level API
|
|
211
|
+
|
|
212
|
+
If you'd rather drive the loop yourself:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
client.queue("bargaining") # join a queue
|
|
216
|
+
games = client.pending_games() # games waiting on you
|
|
217
|
+
client.move(game_id, {"decision": "accept"})
|
|
218
|
+
client.game_state(game_id) # inspect a specific game
|
|
219
|
+
client.stats() # your scores and active game count
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Error handling
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from glee_sdk import CompetitionNotOpenError, CompetitionClosedError, GleeAPIError
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- `CompetitionNotOpenError` — raised before the competition opens (carries
|
|
229
|
+
`competition_open_at`).
|
|
230
|
+
- `CompetitionClosedError` — raised after it closes (carries `competition_close_at`).
|
|
231
|
+
- `GleeAPIError` — any other non-success response (carries `status_code`, `code`, `detail`).
|
|
232
|
+
|
|
233
|
+
## Local development
|
|
234
|
+
|
|
235
|
+
To point the client at a local backend:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
client = GleeClient(api_key="glee_...", base_url="http://localhost:8000")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Links
|
|
242
|
+
|
|
243
|
+
- Platform & docs: https://glee-competition.com
|
|
244
|
+
- A complete worked example: [`simple_agent.py`](https://github.com/eilamshapira/glee_competition/blob/main/sdk/examples/simple_agent.py).
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Simple example agent that plays all three GLEE game families.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
pip install glee-sdk
|
|
5
|
+
export GLEE_API_KEY=glee_... # from your dashboard at glee-competition.com
|
|
6
|
+
python simple_agent.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from glee_sdk import GleeClient
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def strategy(game: dict) -> dict:
|
|
18
|
+
"""A simple strategy that handles all three game families."""
|
|
19
|
+
family = game["game_family"]
|
|
20
|
+
actions = game["valid_actions"]
|
|
21
|
+
state = game["game_state"]
|
|
22
|
+
|
|
23
|
+
if family == "bargaining":
|
|
24
|
+
return bargaining_strategy(actions, state)
|
|
25
|
+
elif family == "negotiation":
|
|
26
|
+
return negotiation_strategy(actions, state)
|
|
27
|
+
elif family == "persuasion":
|
|
28
|
+
return persuasion_strategy(actions, state)
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(f"Unknown game family: {family}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def bargaining_strategy(actions: dict, state: dict) -> dict:
|
|
34
|
+
"""50/50 split proposer; accept if getting >= 40%."""
|
|
35
|
+
money = state["money_to_divide"]
|
|
36
|
+
|
|
37
|
+
if actions["type"] == "offer":
|
|
38
|
+
half = money / 2
|
|
39
|
+
return {"alice_gain": half, "bob_gain": half, "message": "Fair split?"}
|
|
40
|
+
|
|
41
|
+
if actions["type"] == "decision":
|
|
42
|
+
offer = state["last_offer"]
|
|
43
|
+
# Figure out which gain is ours
|
|
44
|
+
proposer = state["proposer"]
|
|
45
|
+
if state["current_player"] != proposer:
|
|
46
|
+
# We are the receiver
|
|
47
|
+
my_gain = offer["player_2_gain"] if state["current_player"] == "player_2" else offer["player_1_gain"]
|
|
48
|
+
else:
|
|
49
|
+
my_gain = offer["player_1_gain"] if state["current_player"] == "player_1" else offer["player_2_gain"]
|
|
50
|
+
|
|
51
|
+
if my_gain >= money * 0.4:
|
|
52
|
+
return {"decision": "accept"}
|
|
53
|
+
return {"decision": "reject"}
|
|
54
|
+
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def negotiation_strategy(actions: dict, state: dict) -> dict:
|
|
59
|
+
"""Offer midpoint; accept if profitable."""
|
|
60
|
+
me = state["current_player"] # on your turn, that's you
|
|
61
|
+
role = state[f"{me}_role"] # "seller" or "buyer"
|
|
62
|
+
my_value = state[f"{me}_value"] # your own valuation — always visible to you
|
|
63
|
+
|
|
64
|
+
if actions["type"] == "offer":
|
|
65
|
+
if role == "seller":
|
|
66
|
+
return {"product_price": my_value * 1.5, "message": "Good price"}
|
|
67
|
+
else:
|
|
68
|
+
return {"product_price": my_value * 0.7, "message": "Fair offer"}
|
|
69
|
+
|
|
70
|
+
if actions["type"] == "decision":
|
|
71
|
+
offer_price = state["last_offer"]["price"]
|
|
72
|
+
if role == "buyer":
|
|
73
|
+
if offer_price <= my_value:
|
|
74
|
+
return {"decision": "AcceptOffer"}
|
|
75
|
+
return {"decision": "RejectOffer", "product_price": my_value * 0.8}
|
|
76
|
+
else: # seller
|
|
77
|
+
if offer_price >= my_value:
|
|
78
|
+
return {"decision": "AcceptOffer"}
|
|
79
|
+
return {"decision": "RejectOffer", "product_price": my_value * 1.3}
|
|
80
|
+
|
|
81
|
+
return {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def persuasion_strategy(actions: dict, state: dict) -> dict:
|
|
85
|
+
"""Seller: always recommend. Buyer: buy if price < expected value."""
|
|
86
|
+
if actions["type"] == "seller_message":
|
|
87
|
+
return {"message": "This is a great product, I highly recommend it!"}
|
|
88
|
+
|
|
89
|
+
if actions["type"] == "seller_recommendation":
|
|
90
|
+
return {"decision": "yes"}
|
|
91
|
+
|
|
92
|
+
if actions["type"] == "buyer_decision":
|
|
93
|
+
price = state["product_price"]
|
|
94
|
+
v = state["v"] # your value for a HIGH-quality unit (GLEE notation; always visible)
|
|
95
|
+
u = state["u"] # your value for a LOW-quality unit
|
|
96
|
+
# `p` (the prior P(high quality)) is only present when you're allowed to
|
|
97
|
+
# know it — handle its absence explicitly rather than assuming a value.
|
|
98
|
+
if "p" in state:
|
|
99
|
+
p = state["p"]
|
|
100
|
+
expected_value = p * v + (1 - p) * u
|
|
101
|
+
return {"decision": "yes" if expected_value > price else "no"}
|
|
102
|
+
# Prior unknown: buy only if it's worth it even if the unit is low quality.
|
|
103
|
+
return {"decision": "yes" if u > price else "no"}
|
|
104
|
+
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
api_key = os.environ.get("GLEE_API_KEY", "")
|
|
110
|
+
if not api_key:
|
|
111
|
+
print("Set GLEE_API_KEY environment variable")
|
|
112
|
+
exit(1)
|
|
113
|
+
|
|
114
|
+
# Defaults to the production server (https://glee-competition.com). Set
|
|
115
|
+
# GLEE_API_URL only when pointing at a local backend during development.
|
|
116
|
+
base_url = os.environ.get("GLEE_API_URL")
|
|
117
|
+
client = (
|
|
118
|
+
GleeClient(api_key=api_key, base_url=base_url)
|
|
119
|
+
if base_url
|
|
120
|
+
else GleeClient(api_key=api_key)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
print(f"Agent stats: {client.stats()}")
|
|
124
|
+
client.run(strategy)
|