econagents 0.0.7__tar.gz → 0.0.9__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.0.7 → econagents-0.0.9}/PKG-INFO +8 -1
- {econagents-0.0.7 → econagents-0.0.9}/econagents/__init__.py +1 -1
- {econagents-0.0.7 → econagents-0.0.9}/econagents/config_parser/__init__.py +0 -3
- {econagents-0.0.7 → econagents-0.0.9}/econagents/config_parser/base.py +56 -21
- {econagents-0.0.7 → econagents-0.0.9}/pyproject.toml +21 -31
- econagents-0.0.7/econagents/config_parser/ibex_tudelft.py +0 -243
- econagents-0.0.7/econagents/core/state/market.py +0 -124
- {econagents-0.0.7 → econagents-0.0.9}/LICENSE +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/README.md +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/_c_extension.pyi +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/cli.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/config_parser/basic.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/__init__.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/agent_role.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/events.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/game_runner.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/logging_mixin.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/manager/__init__.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/manager/base.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/manager/phase.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/state/__init__.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/state/fields.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/state/game.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/core/transport.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/llm/__init__.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/llm/base.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/llm/observability.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/llm/ollama.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/llm/openai.py +0 -0
- {econagents-0.0.7 → econagents-0.0.9}/econagents/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: econagents
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.9
|
4
4
|
Summary:
|
5
5
|
License: MIT
|
6
6
|
Author: Dylan Castillo
|
@@ -18,16 +18,23 @@ Provides-Extra: langsmith
|
|
18
18
|
Provides-Extra: ollama
|
19
19
|
Provides-Extra: openai
|
20
20
|
Provides-Extra: standard
|
21
|
+
Requires-Dist: langfuse (>=3.0.3)
|
21
22
|
Requires-Dist: langfuse ; extra == "all"
|
22
23
|
Requires-Dist: langfuse ; extra == "langfuse"
|
24
|
+
Requires-Dist: langsmith (>=0.4.1)
|
23
25
|
Requires-Dist: langsmith ; extra == "all"
|
24
26
|
Requires-Dist: langsmith ; extra == "langsmith"
|
25
27
|
Requires-Dist: langsmith ; extra == "standard"
|
28
|
+
Requires-Dist: ollama (>=0.5.1)
|
26
29
|
Requires-Dist: ollama ; extra == "all"
|
27
30
|
Requires-Dist: ollama ; extra == "ollama"
|
31
|
+
Requires-Dist: openai (>=1.89.0)
|
28
32
|
Requires-Dist: openai ; extra == "all"
|
29
33
|
Requires-Dist: openai ; extra == "openai"
|
30
34
|
Requires-Dist: openai ; extra == "standard"
|
35
|
+
Requires-Dist: pydantic (>=2.11.5)
|
36
|
+
Requires-Dist: requests (>=2.32.4)
|
37
|
+
Requires-Dist: websockets (>=15.0)
|
31
38
|
Description-Content-Type: text/markdown
|
32
39
|
|
33
40
|
<div align="center">
|
@@ -12,7 +12,7 @@ from econagents.core.state.game import GameState, MetaInformation, PrivateInform
|
|
12
12
|
from econagents.llm.openai import ChatOpenAI
|
13
13
|
|
14
14
|
# Don't manually change, let poetry-dynamic-versioning handle it.
|
15
|
-
__version__ = "0.0.
|
15
|
+
__version__ = "0.0.9"
|
16
16
|
|
17
17
|
__all__: list[str] = [
|
18
18
|
"AgentRole",
|
@@ -4,15 +4,12 @@ Configuration parsers for different server types.
|
|
4
4
|
This package provides configuration parsers for different server types:
|
5
5
|
- Base: No custom event handlers
|
6
6
|
- Basic: Custom event handler for player-is-ready messages
|
7
|
-
- IBEX-TUDelft: Handles player-is-ready messages and role assignment
|
8
7
|
"""
|
9
8
|
|
10
9
|
from econagents.config_parser.base import BaseConfigParser
|
11
10
|
from econagents.config_parser.basic import BasicConfigParser
|
12
|
-
from econagents.config_parser.ibex_tudelft import IbexTudelftConfigParser
|
13
11
|
|
14
12
|
__all__ = [
|
15
13
|
"BaseConfigParser",
|
16
14
|
"BasicConfigParser",
|
17
|
-
"IbexTudelftConfigParser",
|
18
15
|
]
|
@@ -8,11 +8,24 @@ from datetime import datetime, date, time
|
|
8
8
|
import yaml
|
9
9
|
from pydantic import BaseModel, Field, create_model
|
10
10
|
|
11
|
-
from econagents.core.game_runner import
|
12
|
-
|
11
|
+
from econagents.core.game_runner import (
|
12
|
+
GameRunner,
|
13
|
+
GameRunnerConfig,
|
14
|
+
HybridGameRunnerConfig,
|
15
|
+
TurnBasedGameRunnerConfig,
|
16
|
+
)
|
17
|
+
from econagents.core.manager.phase import (
|
18
|
+
PhaseManager,
|
19
|
+
TurnBasedPhaseManager,
|
20
|
+
HybridPhaseManager,
|
21
|
+
)
|
13
22
|
from econagents.core.state.fields import EventField
|
14
|
-
from econagents.core.state.game import
|
15
|
-
|
23
|
+
from econagents.core.state.game import (
|
24
|
+
GameState,
|
25
|
+
MetaInformation,
|
26
|
+
PrivateInformation,
|
27
|
+
PublicInformation,
|
28
|
+
)
|
16
29
|
from econagents.core.agent_role import AgentRole
|
17
30
|
|
18
31
|
TYPE_MAPPING = {
|
@@ -26,7 +39,6 @@ TYPE_MAPPING = {
|
|
26
39
|
"date": date,
|
27
40
|
"time": time,
|
28
41
|
"any": Any,
|
29
|
-
"MarketState": MarketState,
|
30
42
|
}
|
31
43
|
|
32
44
|
|
@@ -96,7 +108,9 @@ class AgentConfig(BaseModel):
|
|
96
108
|
|
97
109
|
# Create a dynamic AgentRole subclass
|
98
110
|
agent_role = type(
|
99
|
-
f"Dynamic{self.name}Role",
|
111
|
+
f"Dynamic{self.name}Role",
|
112
|
+
(AgentRole,),
|
113
|
+
{"role": self.role_id, "name": self.name, "llm": llm_instance},
|
100
114
|
)
|
101
115
|
|
102
116
|
return agent_role()
|
@@ -133,7 +147,7 @@ class StateConfig(BaseModel):
|
|
133
147
|
else:
|
134
148
|
try:
|
135
149
|
resolved_type = eval(
|
136
|
-
field_type_str, {"list": list, "dict": dict, "Any": Any
|
150
|
+
field_type_str, {"list": list, "dict": dict, "Any": Any}
|
137
151
|
)
|
138
152
|
return resolved_type
|
139
153
|
except (NameError, SyntaxError):
|
@@ -146,10 +160,7 @@ class StateConfig(BaseModel):
|
|
146
160
|
elif factory_name == "dict":
|
147
161
|
return dict
|
148
162
|
else:
|
149
|
-
|
150
|
-
return eval(factory_name, {"MarketState": MarketState})
|
151
|
-
except (NameError, SyntaxError):
|
152
|
-
raise ValueError(f"Unsupported default_factory: {factory_name}")
|
163
|
+
raise ValueError(f"Unsupported default_factory: {factory_name}")
|
153
164
|
|
154
165
|
def create_fields_dict(field_configs: List[StateFieldConfig]) -> Dict[str, Any]:
|
155
166
|
"""Create a dictionary of field definitions for create_model."""
|
@@ -166,7 +177,9 @@ class StateConfig(BaseModel):
|
|
166
177
|
}
|
167
178
|
# Handle default vs default_factory
|
168
179
|
if field.default_factory:
|
169
|
-
event_field_args["default_factory"] = get_default_factory(
|
180
|
+
event_field_args["default_factory"] = get_default_factory(
|
181
|
+
field.default_factory
|
182
|
+
)
|
170
183
|
else:
|
171
184
|
# Pydantic handles Optional defaults correctly (None if optional and no default)
|
172
185
|
event_field_args["default"] = field.default
|
@@ -218,7 +231,11 @@ class ManagerConfig(BaseModel):
|
|
218
231
|
event_handlers: List[EventHandler] = Field(default_factory=list)
|
219
232
|
|
220
233
|
def create_manager(
|
221
|
-
self,
|
234
|
+
self,
|
235
|
+
game_id: int,
|
236
|
+
state: GameState,
|
237
|
+
agent_role: Optional[AgentRole],
|
238
|
+
auth_kwargs: Dict[str, Any],
|
222
239
|
) -> PhaseManager:
|
223
240
|
"""Create a PhaseManager instance from this configuration."""
|
224
241
|
|
@@ -385,7 +402,9 @@ class ExperimentConfig(BaseModel):
|
|
385
402
|
|
386
403
|
# Create the prompt file name
|
387
404
|
if phase:
|
388
|
-
file_name =
|
405
|
+
file_name = (
|
406
|
+
f"{role.name.lower()}_{base_type}_phase_{phase}.jinja2"
|
407
|
+
)
|
389
408
|
else:
|
390
409
|
file_name = f"{role.name.lower()}_{base_type}.jinja2"
|
391
410
|
|
@@ -395,18 +414,24 @@ class ExperimentConfig(BaseModel):
|
|
395
414
|
|
396
415
|
return temp_dir
|
397
416
|
|
398
|
-
async def run_experiment(
|
417
|
+
async def run_experiment(
|
418
|
+
self, login_payloads: List[Dict[str, Any]], game_id: int
|
419
|
+
) -> None:
|
399
420
|
"""Run the experiment from this configuration."""
|
400
421
|
# Create state class
|
401
422
|
state_class = self.state.create_state_class()
|
402
|
-
role_configs = {
|
423
|
+
role_configs = {
|
424
|
+
role_config.role_id: role_config for role_config in self.agent_roles
|
425
|
+
}
|
403
426
|
|
404
427
|
if not self.agent_roles and self.agents:
|
405
428
|
raise ValueError(
|
406
429
|
"Configuration has 'agents' but no 'agent_roles'. Cannot determine agent role configurations."
|
407
430
|
)
|
408
431
|
|
409
|
-
agent_to_role_map = {
|
432
|
+
agent_to_role_map = {
|
433
|
+
agent_map.id: agent_map.role_id for agent_map in self.agents
|
434
|
+
}
|
410
435
|
|
411
436
|
# Create managers for each agent
|
412
437
|
agents = []
|
@@ -420,7 +445,9 @@ class ExperimentConfig(BaseModel):
|
|
420
445
|
raise ValueError(f"No role_id mapping found for agent {agent_id}")
|
421
446
|
|
422
447
|
if role_id not in role_configs:
|
423
|
-
raise ValueError(
|
448
|
+
raise ValueError(
|
449
|
+
f"No agent role configuration found for role_id {role_id}"
|
450
|
+
)
|
424
451
|
|
425
452
|
agent_role_instance = role_configs[role_id].create_agent_role()
|
426
453
|
|
@@ -483,7 +510,11 @@ class BaseConfigParser:
|
|
483
510
|
return ExperimentConfig(**config_data)
|
484
511
|
|
485
512
|
def create_manager(
|
486
|
-
self,
|
513
|
+
self,
|
514
|
+
game_id: int,
|
515
|
+
state: GameState,
|
516
|
+
agent_role: Optional[AgentRole],
|
517
|
+
auth_kwargs: Dict[str, Any],
|
487
518
|
) -> PhaseManager:
|
488
519
|
"""
|
489
520
|
Create a manager instance based on the configuration.
|
@@ -502,7 +533,9 @@ class BaseConfigParser:
|
|
502
533
|
game_id=game_id, state=state, agent_role=agent_role, auth_kwargs=auth_kwargs
|
503
534
|
)
|
504
535
|
|
505
|
-
async def run_experiment(
|
536
|
+
async def run_experiment(
|
537
|
+
self, login_payloads: List[Dict[str, Any]], game_id: int
|
538
|
+
) -> None:
|
506
539
|
"""
|
507
540
|
Run the experiment from this configuration.
|
508
541
|
|
@@ -512,7 +545,9 @@ class BaseConfigParser:
|
|
512
545
|
await self.config.run_experiment(login_payloads, game_id)
|
513
546
|
|
514
547
|
|
515
|
-
async def run_experiment_from_yaml(
|
548
|
+
async def run_experiment_from_yaml(
|
549
|
+
yaml_path: Path, login_payloads: List[Dict[str, Any]], game_id: int
|
550
|
+
) -> None:
|
516
551
|
"""Run an experiment from a YAML configuration file."""
|
517
552
|
parser = BaseConfigParser(yaml_path)
|
518
553
|
await parser.run_experiment(login_payloads, game_id)
|
@@ -14,18 +14,20 @@ readme = "README.md"
|
|
14
14
|
homepage = "https://github.com/IBEX-TUDelft/econagents"
|
15
15
|
repository = "https://github.com/IBEX-TUDelft/econagents"
|
16
16
|
dynamic = ["version"]
|
17
|
-
|
18
|
-
[
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
requires-python = ">=3.10,<4"
|
18
|
+
dependencies = [
|
19
|
+
"pydantic>=2.11.5",
|
20
|
+
"requests>=2.32.4",
|
21
|
+
"websockets>=15.0",
|
22
|
+
"openai>=1.89.0",
|
23
|
+
"ollama>=0.5.1",
|
24
|
+
"langsmith>=0.4.1",
|
25
|
+
"langfuse>=3.0.3",
|
26
|
+
]
|
25
27
|
|
26
28
|
[tool.poetry]
|
27
29
|
name = "econagents"
|
28
|
-
version = "0.0.
|
30
|
+
version = "0.0.9" # Do not change, let poetry-dynamic-versioning handle it.
|
29
31
|
packages = [{include = "econagents"}]
|
30
32
|
include = ["econagents/*.so", "econagents/*.pyd"] # Compiled extensions
|
31
33
|
license = "MIT"
|
@@ -38,17 +40,13 @@ generate-setup-file = false
|
|
38
40
|
[tool.poetry.scripts]
|
39
41
|
econagents = "econagents.cli:run_cli"
|
40
42
|
|
41
|
-
[
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
ollama = {version = "^0.5.1", optional = true}
|
49
|
-
langsmith = {version = "^0.4.1", optional = true}
|
50
|
-
langfuse = {version = "^3.0.3", optional = true}
|
51
|
-
|
43
|
+
[project.optional-dependencies]
|
44
|
+
openai = ["openai"]
|
45
|
+
ollama = ["ollama"]
|
46
|
+
langsmith = ["langsmith"]
|
47
|
+
langfuse = ["langfuse"]
|
48
|
+
standard = ["openai", "langsmith"]
|
49
|
+
all = ["openai", "ollama", "langsmith", "langfuse"]
|
52
50
|
|
53
51
|
[tool.poetry.group.docs.dependencies]
|
54
52
|
myst-parser = {extras = ["linkify"], version = "^4.0.1"}
|
@@ -64,12 +62,12 @@ pre_commit = ">=2.16.0"
|
|
64
62
|
pytest = ">=7.1.2"
|
65
63
|
pytest-cov = ">=3.0.0"
|
66
64
|
pytest-mock = ">=3.7.0"
|
67
|
-
python-dotenv = "^1.
|
65
|
+
python-dotenv = "^1.1.1"
|
68
66
|
jupyter = "^1.1.1"
|
69
67
|
nest-asyncio = "^1.6.0"
|
70
|
-
ruff = "^0.
|
68
|
+
ruff = "^0.12.7"
|
71
69
|
types-requests = "^2.32.4.20250611"
|
72
|
-
pytest-asyncio = "^
|
70
|
+
pytest-asyncio = "^1.1.0"
|
73
71
|
types-pyyaml = "^6.0.12.20250516"
|
74
72
|
|
75
73
|
[tool.poetry.group.debug]
|
@@ -170,11 +168,3 @@ ignore = [
|
|
170
168
|
|
171
169
|
[tool.codespell]
|
172
170
|
skip = 'poetry.lock,'
|
173
|
-
|
174
|
-
[tool.poetry.extras]
|
175
|
-
openai = ["openai"]
|
176
|
-
ollama = ["ollama"]
|
177
|
-
langsmith = ["langsmith"]
|
178
|
-
langfuse = ["langfuse"]
|
179
|
-
standard = ["openai", "langsmith"]
|
180
|
-
all = ["openai", "ollama", "langsmith", "langfuse"]
|
@@ -1,243 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from pathlib import Path
|
3
|
-
from typing import Any, Dict, Optional, Type, List, Callable, cast
|
4
|
-
from pydantic import create_model
|
5
|
-
|
6
|
-
from econagents import AgentRole
|
7
|
-
from econagents.core.events import Message
|
8
|
-
from econagents.core.manager.phase import PhaseManager
|
9
|
-
from econagents.core.state.game import GameState
|
10
|
-
from econagents.core.game_runner import GameRunner
|
11
|
-
from econagents.config_parser.base import BaseConfigParser
|
12
|
-
from econagents.llm.observability import get_observability_provider
|
13
|
-
|
14
|
-
|
15
|
-
def handle_market_event_impl(self: GameState, event_type: str, data: dict[str, Any]) -> None:
|
16
|
-
"""Handles market-related events by delegating to the MarketState instance."""
|
17
|
-
try:
|
18
|
-
getattr(self.public_information, self.meta._market_state_variable_name).process_event( # type: ignore
|
19
|
-
event_type=event_type, data=data
|
20
|
-
)
|
21
|
-
except Exception as e:
|
22
|
-
raise ValueError(f"Error processing market event: {e}") from e
|
23
|
-
|
24
|
-
|
25
|
-
def handle_asset_movement_event_impl(self: GameState, event_type: str, data: dict[str, Any]) -> None:
|
26
|
-
"""Handles asset-movement events by delegating to the MarketState instance."""
|
27
|
-
try:
|
28
|
-
winning_condition = self.public_information.winning_condition # type: ignore
|
29
|
-
self.private_information.wallet[winning_condition]["balance"] = data["balance"] # type: ignore
|
30
|
-
self.private_information.wallet[winning_condition]["shares"] = data["shares"] # type: ignore
|
31
|
-
except Exception as e:
|
32
|
-
raise ValueError(f"Error processing asset-movement event: {e}") from e
|
33
|
-
|
34
|
-
|
35
|
-
def get_custom_handlers_impl(self: GameState) -> Dict[str, Callable[[Message], None]]:
|
36
|
-
"""Returns custom handlers for market events."""
|
37
|
-
market_events = ["add-order", "update-order", "delete-order", "contract-fulfilled"]
|
38
|
-
asset_movement_event_handlers: Dict[str, Callable[[Message], None]] = {
|
39
|
-
"asset-movement": self._handle_asset_movement_event # type: ignore
|
40
|
-
}
|
41
|
-
market_event_handlers: Dict[str, Callable[[Message], None]] = {
|
42
|
-
event: self._handle_market_event # type: ignore
|
43
|
-
for event in market_events
|
44
|
-
}
|
45
|
-
return {**asset_movement_event_handlers, **market_event_handlers}
|
46
|
-
|
47
|
-
|
48
|
-
class IbexTudelftConfigParser(BaseConfigParser):
|
49
|
-
"""
|
50
|
-
IBEX-TUDelft configuration parser that extends the BasicConfigParser
|
51
|
-
and adds role assignment functionality.
|
52
|
-
"""
|
53
|
-
|
54
|
-
def __init__(self, config_path: Path):
|
55
|
-
"""
|
56
|
-
Initialize the IBEX-TUDelft config parser.
|
57
|
-
|
58
|
-
Args:
|
59
|
-
config_path: Path to the YAML configuration file
|
60
|
-
"""
|
61
|
-
super().__init__(config_path)
|
62
|
-
self._role_classes: Dict[int, Type[AgentRole]] = {}
|
63
|
-
|
64
|
-
def register_role_class(self, role_id: int, role_class: Type[AgentRole]) -> None:
|
65
|
-
"""
|
66
|
-
Register a role class for a specific role ID.
|
67
|
-
|
68
|
-
Args:
|
69
|
-
role_id: The role ID
|
70
|
-
role_class: The agent role class
|
71
|
-
"""
|
72
|
-
self._role_classes[role_id] = role_class
|
73
|
-
|
74
|
-
def create_manager(
|
75
|
-
self, game_id: int, state: GameState, agent_role: Optional[AgentRole], auth_kwargs: Dict[str, Any]
|
76
|
-
) -> PhaseManager:
|
77
|
-
"""
|
78
|
-
Create a manager instance with custom event handlers for assign-name and assign-role events.
|
79
|
-
The manager won't have a role initially, but will be assigned one during the game.
|
80
|
-
|
81
|
-
Args:
|
82
|
-
game_id: The game ID
|
83
|
-
state: The game state instance
|
84
|
-
agent_role: The agent role instance (may be None)
|
85
|
-
auth_kwargs: Authentication mechanism keyword arguments
|
86
|
-
|
87
|
-
Returns:
|
88
|
-
A PhaseManager instance with custom event handlers
|
89
|
-
"""
|
90
|
-
# Get the manager with the name assignment handler
|
91
|
-
manager = super().create_manager(
|
92
|
-
game_id=game_id,
|
93
|
-
state=state,
|
94
|
-
agent_role=None,
|
95
|
-
auth_kwargs=auth_kwargs,
|
96
|
-
)
|
97
|
-
|
98
|
-
# Register custom event handler for assign-name event
|
99
|
-
async def handle_name_assignment(message: Message) -> None:
|
100
|
-
"""Handle the name assignment event."""
|
101
|
-
# Include the agent ID from auth_kwargs in the ready message
|
102
|
-
agent_id = auth_kwargs.get("agent_id")
|
103
|
-
ready_msg = {"gameId": game_id, "type": "player-is-ready", "agentId": agent_id}
|
104
|
-
await manager.send_message(json.dumps(ready_msg))
|
105
|
-
|
106
|
-
# Register custom event handler for assign-role event
|
107
|
-
async def handle_role_assignment(message: Message) -> None:
|
108
|
-
"""Handle the role assignment event."""
|
109
|
-
role_id = int(message.data.get("role", 0))
|
110
|
-
manager.logger.info(f"Role assigned: {role_id}")
|
111
|
-
|
112
|
-
# Initialize the agent based on the assigned role
|
113
|
-
self._initialize_agent(manager, role_id)
|
114
|
-
|
115
|
-
manager.register_event_handler("assign-role", handle_role_assignment)
|
116
|
-
manager.register_event_handler("assign-name", handle_name_assignment)
|
117
|
-
|
118
|
-
return manager
|
119
|
-
|
120
|
-
def _detect_market_state_in_config(self) -> tuple[bool, Optional[tuple[str, str]]]:
|
121
|
-
"""
|
122
|
-
Detects if MarketState is used in the state configuration.
|
123
|
-
|
124
|
-
Returns:
|
125
|
-
A tuple: (has_market_state_field, market_state_details).
|
126
|
-
market_state_details is (field_name_on_section, section_attribute_name_on_gamestate)
|
127
|
-
or None if not found.
|
128
|
-
"""
|
129
|
-
state_conf = self.config.state
|
130
|
-
# Check public_information first, then private, then meta
|
131
|
-
for field_conf in state_conf.public_information:
|
132
|
-
if field_conf.type == "MarketState":
|
133
|
-
return True, (field_conf.name, "public_information")
|
134
|
-
for field_conf in state_conf.private_information:
|
135
|
-
if field_conf.type == "MarketState":
|
136
|
-
return True, (field_conf.name, "private_information")
|
137
|
-
for field_conf in state_conf.meta_information:
|
138
|
-
if field_conf.type == "MarketState":
|
139
|
-
return True, (field_conf.name, "meta")
|
140
|
-
return False, None
|
141
|
-
|
142
|
-
def _create_enhanced_state_class(self, base_class: Type[GameState]) -> Type[GameState]:
|
143
|
-
"""
|
144
|
-
Creates an enhanced GameState class by subclassing base_class and injecting market event handlers.
|
145
|
-
"""
|
146
|
-
enhanced_class_name = f"Enhanced{base_class.__name__}"
|
147
|
-
enhanced_class = create_model(
|
148
|
-
enhanced_class_name,
|
149
|
-
__base__=base_class,
|
150
|
-
)
|
151
|
-
setattr(enhanced_class, "_handle_market_event", handle_market_event_impl)
|
152
|
-
setattr(enhanced_class, "_handle_asset_movement_event", handle_asset_movement_event_impl)
|
153
|
-
setattr(enhanced_class, "get_custom_handlers", get_custom_handlers_impl)
|
154
|
-
return cast(Type[GameState], enhanced_class)
|
155
|
-
|
156
|
-
def _check_additional_required_fields(self, base_dynamic_state_class: Type[GameState]) -> None:
|
157
|
-
public_fields = base_dynamic_state_class().public_information.model_json_schema()["properties"] # type: ignore
|
158
|
-
private_fields = base_dynamic_state_class().private_information.model_json_schema()["properties"] # type: ignore
|
159
|
-
|
160
|
-
if (
|
161
|
-
"winning_condition" not in public_fields.keys() # type: ignore
|
162
|
-
or "wallet" not in private_fields.keys() # type: ignore
|
163
|
-
):
|
164
|
-
raise ValueError("Winning condition or wallet is not present in the config")
|
165
|
-
|
166
|
-
async def run_experiment(self, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
|
167
|
-
"""
|
168
|
-
Run the experiment from this configuration, potentially enhancing the GameState
|
169
|
-
class with market event handlers if MarketState is specified in the config.
|
170
|
-
"""
|
171
|
-
# Step 1: Get the base state class from the original StateConfig
|
172
|
-
base_dynamic_state_class = self.config.state.create_state_class()
|
173
|
-
|
174
|
-
# Step 2: Detect if MarketState is used and get details
|
175
|
-
has_market_state_field, market_state_details = self._detect_market_state_in_config()
|
176
|
-
|
177
|
-
# Step 3: If MarketState is present, create an enhanced state class
|
178
|
-
if has_market_state_field and market_state_details:
|
179
|
-
self._check_additional_required_fields(base_dynamic_state_class)
|
180
|
-
final_state_class = self._create_enhanced_state_class(base_dynamic_state_class)
|
181
|
-
else:
|
182
|
-
final_state_class = base_dynamic_state_class
|
183
|
-
|
184
|
-
if not self.config.agent_roles:
|
185
|
-
raise ValueError("Configuration has no 'agent_roles'.")
|
186
|
-
|
187
|
-
# Create managers for each agent
|
188
|
-
agents_for_runner = []
|
189
|
-
for payload in login_payloads:
|
190
|
-
current_agent_manager = self.create_manager(
|
191
|
-
game_id=game_id,
|
192
|
-
state=final_state_class(),
|
193
|
-
agent_role=None,
|
194
|
-
auth_kwargs=payload,
|
195
|
-
)
|
196
|
-
current_agent_manager.state.meta.game_id = game_id
|
197
|
-
if market_state_details:
|
198
|
-
setattr(current_agent_manager.state.meta, "_market_state_variable_name", market_state_details[0])
|
199
|
-
agents_for_runner.append(current_agent_manager)
|
200
|
-
|
201
|
-
# Create runner config
|
202
|
-
runner_config_instance = self.config.runner.create_runner_config()
|
203
|
-
runner_config_instance.state_class = final_state_class
|
204
|
-
runner_config_instance.game_id = game_id
|
205
|
-
|
206
|
-
if any(hasattr(role, "prompts") and role.prompts for role in self.config.agent_roles):
|
207
|
-
prompts_dir = self.config._compile_inline_prompts()
|
208
|
-
runner_config_instance.prompts_dir = prompts_dir
|
209
|
-
|
210
|
-
runner = GameRunner(config=runner_config_instance, agents=agents_for_runner)
|
211
|
-
await runner.run_game()
|
212
|
-
|
213
|
-
if self.config._temp_prompts_dir and self.config._temp_prompts_dir.exists():
|
214
|
-
import shutil
|
215
|
-
|
216
|
-
shutil.rmtree(self.config._temp_prompts_dir)
|
217
|
-
|
218
|
-
def _initialize_agent(self, manager: PhaseManager, role_id: int) -> None:
|
219
|
-
"""
|
220
|
-
Initialize the agent instance based on the assigned role.
|
221
|
-
|
222
|
-
Args:
|
223
|
-
manager: The phase manager instance
|
224
|
-
role_id: The role ID
|
225
|
-
"""
|
226
|
-
agent_roles = self.config.agent_roles
|
227
|
-
agent_role = next((role for role in agent_roles if role.role_id == role_id), None)
|
228
|
-
if agent_role:
|
229
|
-
manager.agent_role = agent_role.create_agent_role()
|
230
|
-
manager.agent_role.logger = manager.logger # type: ignore
|
231
|
-
if self.config.runner.observability_provider:
|
232
|
-
manager.agent_role.llm.observability = get_observability_provider(
|
233
|
-
self.config.runner.observability_provider
|
234
|
-
)
|
235
|
-
else:
|
236
|
-
manager.logger.error("Invalid role assigned; cannot initialize agent.")
|
237
|
-
raise ValueError("Invalid role for agent initialization.")
|
238
|
-
|
239
|
-
|
240
|
-
async def run_experiment_from_yaml(yaml_path: Path, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
|
241
|
-
"""Run an experiment from a YAML configuration file."""
|
242
|
-
parser = IbexTudelftConfigParser(yaml_path)
|
243
|
-
await parser.run_experiment(login_payloads, game_id)
|
@@ -1,124 +0,0 @@
|
|
1
|
-
from typing import Optional
|
2
|
-
|
3
|
-
from pydantic import BaseModel, Field, computed_field
|
4
|
-
|
5
|
-
|
6
|
-
class Order(BaseModel):
|
7
|
-
id: int
|
8
|
-
sender: int
|
9
|
-
price: float
|
10
|
-
quantity: float
|
11
|
-
type: str
|
12
|
-
condition: int
|
13
|
-
now: bool = False
|
14
|
-
|
15
|
-
|
16
|
-
class Trade(BaseModel):
|
17
|
-
from_id: int
|
18
|
-
to_id: int
|
19
|
-
price: float
|
20
|
-
quantity: float
|
21
|
-
condition: int
|
22
|
-
median: Optional[float] = None
|
23
|
-
|
24
|
-
|
25
|
-
class MarketState(BaseModel):
|
26
|
-
"""
|
27
|
-
Represents the current state of the market:
|
28
|
-
- Active orders in an order book
|
29
|
-
- History of recent trades
|
30
|
-
"""
|
31
|
-
|
32
|
-
orders: dict[int, Order] = Field(default_factory=dict)
|
33
|
-
trades: list[Trade] = Field(default_factory=list)
|
34
|
-
|
35
|
-
@computed_field
|
36
|
-
def order_book(self) -> str:
|
37
|
-
asks = sorted(
|
38
|
-
[order for order in self.orders.values() if order.type == "ask"],
|
39
|
-
key=lambda x: x.price,
|
40
|
-
reverse=True,
|
41
|
-
)
|
42
|
-
bids = sorted(
|
43
|
-
[order for order in self.orders.values() if order.type == "bid"],
|
44
|
-
key=lambda x: x.price,
|
45
|
-
reverse=True,
|
46
|
-
)
|
47
|
-
sorted_orders = asks + bids
|
48
|
-
return "\n".join([str(order) for order in sorted_orders])
|
49
|
-
|
50
|
-
def process_event(self, event_type: str, data: dict):
|
51
|
-
"""
|
52
|
-
Update the MarketState based on the eventType and
|
53
|
-
event data from the server.
|
54
|
-
"""
|
55
|
-
if event_type == "add-order":
|
56
|
-
self._on_add_order(data["order"])
|
57
|
-
|
58
|
-
elif event_type == "update-order":
|
59
|
-
self._on_update_order(data["order"])
|
60
|
-
|
61
|
-
elif event_type == "delete-order":
|
62
|
-
self._on_delete_order(data["order"])
|
63
|
-
|
64
|
-
elif event_type == "contract-fulfilled":
|
65
|
-
self._on_contract_fulfilled(data)
|
66
|
-
|
67
|
-
def get_orders_from_player(self, player_id: int) -> list[Order]:
|
68
|
-
"""Get all orders from a specific player."""
|
69
|
-
return [order for order in self.orders.values() if order.sender == player_id]
|
70
|
-
|
71
|
-
def _on_add_order(self, order_data: dict):
|
72
|
-
"""
|
73
|
-
The server is telling us a new order has been added.
|
74
|
-
We'll store it in self.orders by ID.
|
75
|
-
"""
|
76
|
-
order_id = order_data["id"]
|
77
|
-
new_order = Order(
|
78
|
-
id=order_id,
|
79
|
-
sender=order_data["sender"],
|
80
|
-
price=order_data["price"],
|
81
|
-
quantity=order_data["quantity"],
|
82
|
-
type=order_data["type"],
|
83
|
-
condition=order_data["condition"],
|
84
|
-
now=order_data.get("now", False),
|
85
|
-
)
|
86
|
-
self.orders[order_id] = new_order
|
87
|
-
|
88
|
-
def _on_update_order(self, order_data: dict):
|
89
|
-
"""
|
90
|
-
The server is telling us the order's quantity or other fields
|
91
|
-
have changed (often due to partial fills).
|
92
|
-
"""
|
93
|
-
order_id = order_data["id"]
|
94
|
-
if order_id in self.orders:
|
95
|
-
existing = self.orders[order_id]
|
96
|
-
existing.quantity = order_data.get("quantity", existing.quantity)
|
97
|
-
self.orders[order_id] = existing
|
98
|
-
|
99
|
-
def _on_delete_order(self, order_data: dict):
|
100
|
-
"""
|
101
|
-
The server is telling us this order is removed
|
102
|
-
from the order book (fully filled or canceled).
|
103
|
-
"""
|
104
|
-
order_id = order_data["id"]
|
105
|
-
if order_id in self.orders:
|
106
|
-
del self.orders[order_id]
|
107
|
-
|
108
|
-
def _on_contract_fulfilled(self, data: dict):
|
109
|
-
"""
|
110
|
-
This indicates a trade has happened between 'from' and 'to'.
|
111
|
-
The server might also send update-order or delete-order events
|
112
|
-
to reflect the fill on the order book.
|
113
|
-
We track the trade in self.trades, but we typically rely
|
114
|
-
on update-order or delete-order to fix the order's quantity.
|
115
|
-
"""
|
116
|
-
new_trade = Trade(
|
117
|
-
from_id=data["from"],
|
118
|
-
to_id=data["to"],
|
119
|
-
price=data["price"],
|
120
|
-
quantity=data.get("quantity", 1.0),
|
121
|
-
condition=data["condition"],
|
122
|
-
median=data.get("median"),
|
123
|
-
)
|
124
|
-
self.trades.append(new_trade)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|