econagents 0.2.2__tar.gz → 0.2.3__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.
- econagents-0.2.3/CHANGELOG.md +63 -0
- {econagents-0.2.2 → econagents-0.2.3}/PKG-INFO +3 -2
- {econagents-0.2.2 → econagents-0.2.3}/README.md +2 -1
- {econagents-0.2.2 → econagents-0.2.3}/econagents/_version.py +2 -2
- {econagents-0.2.2 → econagents-0.2.3}/econagents/runtime/game_runner.py +3 -0
- econagents-0.2.3/examples/continuous_double_auction/README.md +51 -0
- econagents-0.2.3/examples/continuous_double_auction/__init__.py +1 -0
- econagents-0.2.3/examples/continuous_double_auction/agents.py +90 -0
- econagents-0.2.3/examples/continuous_double_auction/prompts/trader_system.jinja2 +13 -0
- econagents-0.2.3/examples/continuous_double_auction/prompts/trader_user_phase_market.jinja2 +28 -0
- econagents-0.2.3/examples/continuous_double_auction/run_game.py +55 -0
- econagents-0.2.3/examples/continuous_double_auction/server/__init__.py +1 -0
- econagents-0.2.3/examples/continuous_double_auction/server/create_game.py +108 -0
- econagents-0.2.3/examples/continuous_double_auction/server/server.py +543 -0
- econagents-0.2.3/examples/continuous_double_auction/state.py +49 -0
- econagents-0.2.3/examples/continuous_double_auction/verify.py +205 -0
- econagents-0.2.3/examples/dictator/README.md +54 -0
- econagents-0.2.3/examples/dictator/agents.py +99 -0
- econagents-0.2.3/examples/dictator/prompts/dictator_system.jinja2 +16 -0
- econagents-0.2.3/examples/dictator/prompts/dictator_user_phase_1.jinja2 +11 -0
- econagents-0.2.3/examples/dictator/prompts/dictator_user_phase_2.jinja2 +12 -0
- econagents-0.2.3/examples/dictator/prompts/receiver_system.jinja2 +4 -0
- econagents-0.2.3/examples/dictator/prompts/receiver_user_phase_2.jinja2 +10 -0
- econagents-0.2.3/examples/dictator/run_game.py +43 -0
- econagents-0.2.3/examples/dictator/server/__init__.py +0 -0
- econagents-0.2.3/examples/dictator/server/create_game.py +85 -0
- econagents-0.2.3/examples/dictator/server/server.py +389 -0
- econagents-0.2.3/examples/dictator/state.py +43 -0
- econagents-0.2.3/examples/dictator/verify.py +155 -0
- econagents-0.2.3/examples/prisoner/agents.py +72 -0
- econagents-0.2.3/examples/prisoner/prisoner.yaml +153 -0
- econagents-0.2.3/examples/prisoner/prompts/all_system.jinja2 +12 -0
- econagents-0.2.3/examples/prisoner/prompts/all_user.jinja2 +33 -0
- econagents-0.2.3/examples/prisoner/run_game.py +39 -0
- econagents-0.2.3/examples/prisoner/run_game_from_yaml.py +22 -0
- econagents-0.2.3/examples/prisoner/server/__init__.py +0 -0
- econagents-0.2.3/examples/prisoner/server/create_game.py +80 -0
- econagents-0.2.3/examples/prisoner/server/server.py +358 -0
- econagents-0.2.3/examples/prisoner/state.py +36 -0
- econagents-0.2.3/examples/prisoner/verify.py +141 -0
- econagents-0.2.3/examples/prisoner_personas/__init__.py +0 -0
- econagents-0.2.3/examples/prisoner_personas/agents.py +82 -0
- econagents-0.2.3/examples/prisoner_personas/personas/marcus-strategic-44.yaml +8 -0
- econagents-0.2.3/examples/prisoner_personas/personas/maria-cooperative-37.yaml +8 -0
- econagents-0.2.3/examples/prisoner_personas/prisoner.yaml +147 -0
- econagents-0.2.3/examples/prisoner_personas/prompts/all_system.jinja2 +12 -0
- econagents-0.2.3/examples/prisoner_personas/prompts/all_user.jinja2 +33 -0
- econagents-0.2.3/examples/prisoner_personas/run_game.py +125 -0
- econagents-0.2.3/examples/prisoner_personas/run_game_from_yaml.py +39 -0
- econagents-0.2.3/examples/public_goods/README.md +118 -0
- econagents-0.2.3/examples/public_goods/agents.py +79 -0
- econagents-0.2.3/examples/public_goods/prompts/_partials/_cooperative.jinja2 +1 -0
- econagents-0.2.3/examples/public_goods/prompts/_partials/_selfish.jinja2 +1 -0
- econagents-0.2.3/examples/public_goods/prompts/player_system.jinja2 +23 -0
- econagents-0.2.3/examples/public_goods/prompts/player_user_phase_1.jinja2 +11 -0
- econagents-0.2.3/examples/public_goods/prompts/player_user_phase_2.jinja2 +27 -0
- econagents-0.2.3/examples/public_goods/run_game.py +59 -0
- econagents-0.2.3/examples/public_goods/server/__init__.py +0 -0
- econagents-0.2.3/examples/public_goods/server/create_game.py +87 -0
- econagents-0.2.3/examples/public_goods/server/server.py +410 -0
- econagents-0.2.3/examples/public_goods/state.py +47 -0
- econagents-0.2.3/examples/public_goods/verify.py +150 -0
- {econagents-0.2.2 → econagents-0.2.3}/pyproject.toml +6 -0
- {econagents-0.2.2 → econagents-0.2.3}/.gitignore +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/LICENSE +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/_c_extension.pyi +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/config/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/config/yaml.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/llm/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/llm/base.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/llm/observability.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/llm/ollama.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/llm/openai.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/parsing/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/parsing/json.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/prompts/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/prompts/jinja.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/protocol/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/protocol/flat.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/protocol/ibex.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/state/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/state/event_field.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/tools/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/tools/base.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/tools/calculator.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/tools/python_exec.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/tools/registry.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/transport/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/adapters/transport/websocket.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/cli.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/events.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/logging.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/messages.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/role.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/state/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/state/fields.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/domain/state/game.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/altruist.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/conditional-cooperator.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/free-rider.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/inequity-averse.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/risk-averse.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/risk-seeking.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/spiteful.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/archetypes/unconditional-cooperator.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/demographics/entrepreneur-ke-33.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/demographics/parent-br-42.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/demographics/professional-de-35.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/demographics/retiree-jp-68.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/demographics/student-us-21.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/demographics/teacher-no-48.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/personas/library/demographics/worker-in-29.yaml +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/codec.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/llm.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/parsing.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/prompts.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/state.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/tools.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/ports/transport.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/py.typed +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/runtime/__init__.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/runtime/agent.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/runtime/experiment_factory.py +0 -0
- {econagents-0.2.2 → econagents-0.2.3}/econagents/runtime/phase_engine.py +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to econagents are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.3] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added a local continuous double auction example with LLM-backed traders,
|
|
12
|
+
structured market actions, a continuous `market` phase, a receive-only
|
|
13
|
+
`summary` phase, local verification, and OpenAI-backed run instructions.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Updated game runner logging so an agent's WebSocket transport uses the
|
|
18
|
+
per-agent run logger, capturing transport send/receive events in the same
|
|
19
|
+
per-game log files as agent lifecycle events.
|
|
20
|
+
- Included local examples in source distributions while excluding generated
|
|
21
|
+
local run logs and game specs.
|
|
22
|
+
|
|
23
|
+
## [0.2.2] - 2026-06-25
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Added local verification scripts for the prisoner, dictator, and public goods
|
|
28
|
+
examples so each example can be exercised end-to-end against its local server
|
|
29
|
+
without making external LLM calls.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Organized the package around explicit hexagonal boundaries:
|
|
34
|
+
`domain`, `ports`, `runtime`, and `adapters`.
|
|
35
|
+
- Moved protocol, transport, configuration, prompt, parser, state projection,
|
|
36
|
+
and LLM provider implementations under `econagents.adapters`.
|
|
37
|
+
- Moved roles, state models, events, and stable message types under
|
|
38
|
+
`econagents.domain`.
|
|
39
|
+
- Moved runtime orchestration, phase handling, experiment factories, and game
|
|
40
|
+
supervision under `econagents.runtime`.
|
|
41
|
+
- Renamed the participant runtime and behavior policy concepts to `Agent` and
|
|
42
|
+
`Role`.
|
|
43
|
+
- Renamed the YAML configuration entry point to `YamlExperimentLoader` and the
|
|
44
|
+
loaded YAML models to `ExperimentSpec`, `RoleSpec`, `AgentSpec`,
|
|
45
|
+
`StateSpec`, `RuntimeSpec`, and `RunnerSpec`.
|
|
46
|
+
- Renamed the default response parser to `JsonResponseParser`.
|
|
47
|
+
- Updated local examples and local servers for the refactored runtime,
|
|
48
|
+
transport, prompt rendering, and state projection APIs.
|
|
49
|
+
- Updated OpenAI-backed examples and documentation snippets to use
|
|
50
|
+
`gpt-5.4-mini`.
|
|
51
|
+
- Updated the prisoner YAML examples to use the standard submit-choice envelope
|
|
52
|
+
and explicit `phase` and `round` state fields.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Fixed prompt state resolution in local examples so prompts render the current
|
|
57
|
+
event-projected state, including per-round prisoner prompts, dictator payout
|
|
58
|
+
prompts, and public goods personality and payoff prompts.
|
|
59
|
+
- Fixed dictator local server payout ordering so phase-two prompts receive the
|
|
60
|
+
resolved decision and payout state before the payout phase starts.
|
|
61
|
+
|
|
62
|
+
[0.2.3]: https://github.com/IBEX-TUDelft/econagents/compare/v0.2.2...v0.2.3
|
|
63
|
+
[0.2.2]: https://github.com/IBEX-TUDelft/econagents/compare/v0.2.1...v0.2.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: econagents
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A Python library that lets you use LLM agents in economic experiments.
|
|
5
5
|
Project-URL: Homepage, https://github.com/IBEX-TUDelft/econagents
|
|
6
6
|
Project-URL: Repository, https://github.com/IBEX-TUDelft/econagents
|
|
@@ -81,11 +81,12 @@ econagents consists of these main components:
|
|
|
81
81
|
|
|
82
82
|
## Example Experiments
|
|
83
83
|
|
|
84
|
-
The repository includes
|
|
84
|
+
The repository includes four example games:
|
|
85
85
|
|
|
86
86
|
1. **`prisoner`**: An iterated Prisoner's Dilemma game with 5 rounds and 2 LLM agents, runs on a local python server (included).
|
|
87
87
|
2. **`dictator`**: A modified Dictator game with 2 LLM agents that runs on a local python server (included).
|
|
88
88
|
3. **`public_goods`**: A public goods game with 4 players that runs on a local python server (included).
|
|
89
|
+
4. **`continuous_double_auction`**: A classical continuous double auction with LLM-backed traders using a continuous market phase.
|
|
89
90
|
|
|
90
91
|
### Running the Prisoner's Dilemma game
|
|
91
92
|
|
|
@@ -48,11 +48,12 @@ econagents consists of these main components:
|
|
|
48
48
|
|
|
49
49
|
## Example Experiments
|
|
50
50
|
|
|
51
|
-
The repository includes
|
|
51
|
+
The repository includes four example games:
|
|
52
52
|
|
|
53
53
|
1. **`prisoner`**: An iterated Prisoner's Dilemma game with 5 rounds and 2 LLM agents, runs on a local python server (included).
|
|
54
54
|
2. **`dictator`**: A modified Dictator game with 2 LLM agents that runs on a local python server (included).
|
|
55
55
|
3. **`public_goods`**: A public goods game with 4 players that runs on a local python server (included).
|
|
56
|
+
4. **`continuous_double_auction`**: A classical continuous double auction with LLM-backed traders using a continuous market phase.
|
|
56
57
|
|
|
57
58
|
### Running the Prisoner's Dilemma game
|
|
58
59
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -289,6 +289,9 @@ class GameRunner:
|
|
|
289
289
|
"""
|
|
290
290
|
agent_logger = self.get_agent_logger(agent_id, self.config.game_id)
|
|
291
291
|
agent.logger = agent_logger
|
|
292
|
+
transport = getattr(agent, "transport", None)
|
|
293
|
+
if transport is not None and hasattr(transport, "logger"):
|
|
294
|
+
transport.logger = agent_logger
|
|
292
295
|
|
|
293
296
|
async def spawn_agent(self, agent: Agent, agent_id: int) -> None:
|
|
294
297
|
"""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Continuous Double Auction
|
|
2
|
+
|
|
3
|
+
This local example implements a continuous double auction, a standard market institution in experimental economics. Buyers have induced private values and cash endowments, sellers have induced private costs, and LLM-backed agents submit bids or asks while the market phase remains open.
|
|
4
|
+
|
|
5
|
+
The important runtime feature is the continuous phase:
|
|
6
|
+
|
|
7
|
+
- The server emits `phase-transition` with `phase="market"`.
|
|
8
|
+
- Each agent is configured with `PhaseEngine(continuous_phases={"market"}, min_action_delay=8, max_action_delay=8)`.
|
|
9
|
+
- The agent acts once on entry, then keeps calling the role's LLM-backed market phase until the server moves out of `market`.
|
|
10
|
+
- The default induced schedules give each of the two buyers and two sellers five marginal units. That allows up to ten total unit trades, with several more surplus-preserving trades than the original two-unit schedule.
|
|
11
|
+
- The default local run keeps the market open for 60 seconds, unless all possible units trade first.
|
|
12
|
+
- After the market closes, the server emits a receive-only `summary` phase with final trades, cash balances, and surplus. Agents do not send an action in this phase.
|
|
13
|
+
|
|
14
|
+
The traders use `ChatOpenAI` and structured output for market orders, so set `OPENAI_API_KEY` before running the LLM-backed experiment. You can set `OPENAI_MODEL` to override the default model.
|
|
15
|
+
|
|
16
|
+
## Running The Example
|
|
17
|
+
|
|
18
|
+
Start the local WebSocket server:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run python examples/continuous_double_auction/server/server.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
In a separate terminal, run the agents:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv run python examples/continuous_double_auction/run_game.py
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The runner writes per-game and per-agent logs under `examples/continuous_double_auction/logs/game_<game_id>/`.
|
|
31
|
+
The local server writes submitted orders and trades to `examples/continuous_double_auction/logs/server.log`.
|
|
32
|
+
|
|
33
|
+
You can also run the end-to-end verification, which starts the server and agents in the same process:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv run python examples/continuous_double_auction/verify.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Files
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
continuous_double_auction/
|
|
43
|
+
├── agents.py # LLM-backed trader role and structured action schema
|
|
44
|
+
├── run_game.py # Local runner using HybridGameRunnerConfig
|
|
45
|
+
├── prompts/ # Trader prompts used by the LLM role
|
|
46
|
+
├── state.py # EventField-backed market state
|
|
47
|
+
├── verify.py # Local end-to-end check with a deterministic LLM stub
|
|
48
|
+
└── server/
|
|
49
|
+
├── create_game.py # Game spec and induced value/cost schedule
|
|
50
|
+
└── server.py # WebSocket CDA server
|
|
51
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Continuous double auction example."""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from econagents import Role
|
|
10
|
+
from econagents.adapters.llm import ChatOpenAI
|
|
11
|
+
from econagents.runtime import Agent, PhaseEngine, create_game_state
|
|
12
|
+
from econagents.runtime.game_runner import GameRunnerConfig, HybridGameRunnerConfig
|
|
13
|
+
|
|
14
|
+
from examples.continuous_double_auction.state import CDAGameState
|
|
15
|
+
|
|
16
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
17
|
+
load_dotenv(REPO_ROOT / ".env", override=True)
|
|
18
|
+
|
|
19
|
+
MARKET_PHASE = "market"
|
|
20
|
+
SUMMARY_PHASE = "summary"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OrderComponent(BaseModel):
|
|
24
|
+
"""Component marker for CDA order actions."""
|
|
25
|
+
|
|
26
|
+
type: Literal["continuous-double-auction:order"] = "continuous-double-auction:order"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OrderMeta(BaseModel):
|
|
30
|
+
"""Outbound action metadata emitted by the trader LLM."""
|
|
31
|
+
|
|
32
|
+
type: Literal["submit-order", "hold"]
|
|
33
|
+
component: OrderComponent = Field(default_factory=OrderComponent)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OrderPayload(BaseModel):
|
|
37
|
+
"""Outbound action payload emitted by the trader LLM."""
|
|
38
|
+
|
|
39
|
+
gameId: int
|
|
40
|
+
side: Literal["buy", "sell"] | None = None
|
|
41
|
+
price: float | None = Field(default=None, description="Bid or ask price. Required for submit-order.")
|
|
42
|
+
reason: str | None = Field(default=None, description="Short reason when holding instead of quoting.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MarketAction(BaseModel):
|
|
46
|
+
"""Structured action emitted by an LLM trader during the continuous market."""
|
|
47
|
+
|
|
48
|
+
meta: OrderMeta
|
|
49
|
+
payload: OrderPayload
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Trader(Role[CDAGameState]):
|
|
53
|
+
"""LLM-backed trader role for the local continuous double auction."""
|
|
54
|
+
|
|
55
|
+
role = 1
|
|
56
|
+
name = "trader"
|
|
57
|
+
llm = ChatOpenAI(
|
|
58
|
+
model_name=os.getenv("OPENAI_MODEL", "gpt-5.4-mini"),
|
|
59
|
+
response_kwargs={"max_output_tokens": 300},
|
|
60
|
+
)
|
|
61
|
+
task_phases = [MARKET_PHASE]
|
|
62
|
+
response_schemas = {MARKET_PHASE: MarketAction}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_cda_agent(*, config: GameRunnerConfig, recovery_code: str) -> Agent:
|
|
66
|
+
"""Create one LLM-backed continuous double auction trader."""
|
|
67
|
+
if not isinstance(config, HybridGameRunnerConfig):
|
|
68
|
+
raise TypeError("The continuous double auction example requires HybridGameRunnerConfig.")
|
|
69
|
+
|
|
70
|
+
return Agent(
|
|
71
|
+
url=config.server_url(),
|
|
72
|
+
auth_mechanism=config.auth_mechanism,
|
|
73
|
+
auth_mechanism_kwargs={"gameId": config.game_id, "recovery": recovery_code},
|
|
74
|
+
state=create_game_state(CDAGameState, game_id=config.game_id),
|
|
75
|
+
role=Trader(),
|
|
76
|
+
prompts_dir=config.prompts_dir,
|
|
77
|
+
phase_transition_event=config.phase_transition_event,
|
|
78
|
+
phase_identifier_key=config.phase_identifier_key,
|
|
79
|
+
phase_engine=PhaseEngine(
|
|
80
|
+
continuous_phases=set(config.continuous_phases),
|
|
81
|
+
min_action_delay=config.min_action_delay,
|
|
82
|
+
max_action_delay=config.max_action_delay,
|
|
83
|
+
),
|
|
84
|
+
end_game_event=config.end_game_event,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_cda_agents(config: GameRunnerConfig, recovery_codes: Iterable[str]) -> list[Agent]:
|
|
89
|
+
"""Create all traders for a runner config."""
|
|
90
|
+
return [create_cda_agent(config=config, recovery_code=code) for code in recovery_codes]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You are a trader in a continuous double auction, a standard experimental economics market.
|
|
2
|
+
|
|
3
|
+
You have a private role:
|
|
4
|
+
- If you are a buyer, your limit price is your current private value. Submit buy bids at or below both that value and your cash balance.
|
|
5
|
+
- If you are a seller, your limit price is your current private cost. Submit sell asks at or above that cost.
|
|
6
|
+
|
|
7
|
+
Your goal is to earn surplus on each unit:
|
|
8
|
+
- Buyer surplus = value - transaction price.
|
|
9
|
+
- Seller surplus = transaction price - cost.
|
|
10
|
+
|
|
11
|
+
Cash balance is a real budget constraint for buyers. Cash flow is the net payments or receipts from trades. Realized surplus is your payoff so far.
|
|
12
|
+
|
|
13
|
+
Quote one order at a time. Keep prices valid for your role. Hold only if you have no remaining units, if your role is not assigned, or if every possible quote would violate your private limit.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Current market state:
|
|
2
|
+
- Game ID: {{ meta.game_id }}
|
|
3
|
+
- Trader role: {{ private_information.trader_type }}
|
|
4
|
+
- Current limit price: {{ private_information.limit_price }}
|
|
5
|
+
- Remaining units: {{ private_information.remaining_units }}
|
|
6
|
+
- Cash balance: {{ private_information.cash_balance }}
|
|
7
|
+
- Net cash flow: {{ private_information.cash_flow }}
|
|
8
|
+
- Realized surplus: {{ private_information.surplus }}
|
|
9
|
+
- Inventory: {{ private_information.inventory }}
|
|
10
|
+
- Your actions submitted so far: {{ private_information.actions_submitted }}
|
|
11
|
+
- Best bid: {{ public_information.best_bid }}
|
|
12
|
+
- Best ask: {{ public_information.best_ask }}
|
|
13
|
+
- Last transaction price: {{ public_information.last_price }}
|
|
14
|
+
- Total orders submitted: {{ public_information.order_count }}
|
|
15
|
+
- Transactions so far: {{ public_information.transactions }}
|
|
16
|
+
|
|
17
|
+
Return one structured action.
|
|
18
|
+
|
|
19
|
+
If you submit an order:
|
|
20
|
+
- Use meta.type = "submit-order".
|
|
21
|
+
- Use payload.gameId = {{ meta.game_id }}.
|
|
22
|
+
- Use payload.side = "buy" if you are a buyer, or "sell" if you are a seller.
|
|
23
|
+
- Use payload.price as your bid or ask.
|
|
24
|
+
|
|
25
|
+
If you hold:
|
|
26
|
+
- Use meta.type = "hold".
|
|
27
|
+
- Use payload.gameId = {{ meta.game_id }}.
|
|
28
|
+
- Use payload.reason to explain briefly.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
from econagents.runtime.game_runner import GameRunner, HybridGameRunnerConfig
|
|
9
|
+
from examples.continuous_double_auction.agents import MARKET_PHASE, create_cda_agents
|
|
10
|
+
from examples.continuous_double_auction.server.create_game import create_game_from_specs
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("continuous_double_auction")
|
|
13
|
+
|
|
14
|
+
MARKET_DURATION_SECONDS = 60
|
|
15
|
+
SUMMARY_DURATION_SECONDS = 2
|
|
16
|
+
ACTION_DELAY_SECONDS = 8
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def main() -> None:
|
|
20
|
+
"""Run a local continuous double auction with LLM-backed traders."""
|
|
21
|
+
logger.info("Starting Continuous Double Auction example")
|
|
22
|
+
env_path = Path(__file__).resolve().parents[2] / ".env"
|
|
23
|
+
load_dotenv(env_path, override=True)
|
|
24
|
+
logger.info(
|
|
25
|
+
"Loaded environment from %s; OPENAI_API_KEY is %s",
|
|
26
|
+
env_path,
|
|
27
|
+
"set" if os.getenv("OPENAI_API_KEY") else "not set",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
game_specs = create_game_from_specs(
|
|
31
|
+
market_duration=MARKET_DURATION_SECONDS,
|
|
32
|
+
summary_duration=SUMMARY_DURATION_SECONDS,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
config = HybridGameRunnerConfig(
|
|
36
|
+
game_id=game_specs["game_id"],
|
|
37
|
+
logs_dir=Path(__file__).parent / "logs",
|
|
38
|
+
prompts_dir=Path(__file__).parent / "prompts",
|
|
39
|
+
log_level=logging.DEBUG,
|
|
40
|
+
hostname="localhost",
|
|
41
|
+
port=8766,
|
|
42
|
+
path="",
|
|
43
|
+
continuous_phases=[MARKET_PHASE],
|
|
44
|
+
min_action_delay=ACTION_DELAY_SECONDS,
|
|
45
|
+
max_action_delay=ACTION_DELAY_SECONDS,
|
|
46
|
+
max_game_duration=game_specs["market_duration"] + game_specs["summary_duration"] + 45,
|
|
47
|
+
)
|
|
48
|
+
agents = create_cda_agents(config, game_specs["recovery_codes"])
|
|
49
|
+
runner = GameRunner(config=config, agents=agents)
|
|
50
|
+
|
|
51
|
+
await runner.run_game()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
asyncio.run(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Server for the continuous double auction example."""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def default_trader_specs() -> list[dict[str, Any]]:
|
|
17
|
+
"""Return a small induced-value schedule for a classic double auction."""
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
"name": "Buyer 1",
|
|
21
|
+
"trader_type": "buyer",
|
|
22
|
+
"limit_prices": [110.0, 100.0, 90.0, 80.0, 70.0],
|
|
23
|
+
"cash_endowment": 450.0,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "Buyer 2",
|
|
27
|
+
"trader_type": "buyer",
|
|
28
|
+
"limit_prices": [105.0, 95.0, 85.0, 75.0, 65.0],
|
|
29
|
+
"cash_endowment": 425.0,
|
|
30
|
+
},
|
|
31
|
+
{"name": "Seller 1", "trader_type": "seller", "limit_prices": [35.0, 45.0, 55.0, 65.0, 75.0]},
|
|
32
|
+
{"name": "Seller 2", "trader_type": "seller", "limit_prices": [40.0, 50.0, 60.0, 70.0, 80.0]},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_recovery_codes(num_players: int) -> list[str]:
|
|
37
|
+
"""Generate recovery codes for all traders."""
|
|
38
|
+
return [str(uuid.uuid4()) for _ in range(num_players)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_game_data(
|
|
42
|
+
specs_path: Path,
|
|
43
|
+
game_id: int,
|
|
44
|
+
game_name: str,
|
|
45
|
+
trader_specs: list[dict[str, Any]],
|
|
46
|
+
market_duration: int,
|
|
47
|
+
summary_duration: int,
|
|
48
|
+
) -> Path:
|
|
49
|
+
"""Save game data to the server specs directory."""
|
|
50
|
+
specs_dir = specs_path.parent / "games"
|
|
51
|
+
specs_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
recovery_codes = generate_recovery_codes(len(trader_specs))
|
|
54
|
+
traders = []
|
|
55
|
+
for spec, recovery in zip(trader_specs, recovery_codes):
|
|
56
|
+
traders.append({**spec, "recovery": recovery})
|
|
57
|
+
|
|
58
|
+
game_data = {
|
|
59
|
+
"game_id": game_id,
|
|
60
|
+
"game_name": game_name,
|
|
61
|
+
"num_players": len(traders),
|
|
62
|
+
"recovery_codes": recovery_codes,
|
|
63
|
+
"traders": traders,
|
|
64
|
+
"market_duration": market_duration,
|
|
65
|
+
"summary_duration": summary_duration,
|
|
66
|
+
"created_at": datetime.now().isoformat(),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
output_file = specs_dir / f"game_{game_id}.json"
|
|
70
|
+
with output_file.open("w") as f:
|
|
71
|
+
json.dump(game_data, f, indent=2)
|
|
72
|
+
logger.info(f"Game data saved to {output_file}")
|
|
73
|
+
return output_file
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_game_from_specs(
|
|
77
|
+
trader_specs: list[dict[str, Any]] | None = None,
|
|
78
|
+
market_duration: int = 60,
|
|
79
|
+
summary_duration: int = 2,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
"""Create a local continuous double auction game spec."""
|
|
82
|
+
game_id = int(datetime.now().timestamp())
|
|
83
|
+
game_name = f"Continuous Double Auction {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
84
|
+
trader_specs = trader_specs or default_trader_specs()
|
|
85
|
+
|
|
86
|
+
save_game_data(
|
|
87
|
+
specs_path=Path(__file__).parent / "games",
|
|
88
|
+
game_id=game_id,
|
|
89
|
+
game_name=game_name,
|
|
90
|
+
trader_specs=trader_specs,
|
|
91
|
+
market_duration=market_duration,
|
|
92
|
+
summary_duration=summary_duration,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
recovery_codes = []
|
|
96
|
+
game_file = Path(__file__).parent / "games" / f"game_{game_id}.json"
|
|
97
|
+
with game_file.open("r") as f:
|
|
98
|
+
recovery_codes = json.load(f)["recovery_codes"]
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"game_id": game_id,
|
|
102
|
+
"game_name": game_name,
|
|
103
|
+
"num_players": len(trader_specs),
|
|
104
|
+
"recovery_codes": recovery_codes,
|
|
105
|
+
"market_duration": market_duration,
|
|
106
|
+
"summary_duration": summary_duration,
|
|
107
|
+
"created_at": datetime.now().isoformat(),
|
|
108
|
+
}
|