econagents 0.0.3__py3-none-any.whl → 0.0.7__py3-none-any.whl
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/__init__.py +1 -1
- econagents/cli.py +136 -0
- econagents/config_parser/__init__.py +18 -0
- econagents/config_parser/base.py +518 -0
- econagents/config_parser/basic.py +45 -0
- econagents/config_parser/ibex_tudelft.py +243 -0
- econagents/core/game_runner.py +112 -13
- econagents/core/manager/base.py +4 -8
- econagents/core/transport.py +62 -28
- {econagents-0.0.3.dist-info → econagents-0.0.7.dist-info}/METADATA +18 -10
- {econagents-0.0.3.dist-info → econagents-0.0.7.dist-info}/RECORD +14 -8
- {econagents-0.0.3.dist-info → econagents-0.0.7.dist-info}/WHEEL +1 -1
- econagents-0.0.7.dist-info/entry_points.txt +3 -0
- {econagents-0.0.3.dist-info → econagents-0.0.7.dist-info}/LICENSE +0 -0
econagents/__init__.py
CHANGED
@@ -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.7"
|
16
16
|
|
17
17
|
__all__: list[str] = [
|
18
18
|
"AgentRole",
|
econagents/cli.py
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
import argparse
|
2
|
+
import asyncio
|
3
|
+
import sys
|
4
|
+
import json
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Dict, List, Any
|
7
|
+
|
8
|
+
from econagents.config_parser.base import BaseConfigParser
|
9
|
+
|
10
|
+
|
11
|
+
async def async_main(args: argparse.Namespace):
|
12
|
+
"""Asynchronous main function to run the experiment."""
|
13
|
+
config_path = Path(args.config_path).resolve()
|
14
|
+
login_payloads_path = Path(args.login_payloads_file).resolve()
|
15
|
+
|
16
|
+
if not config_path.is_file():
|
17
|
+
print(f"Error: Configuration file not found at {config_path}", file=sys.stderr)
|
18
|
+
sys.exit(1)
|
19
|
+
|
20
|
+
if not login_payloads_path.is_file():
|
21
|
+
print(f"Error: Login payloads file not found at {login_payloads_path}", file=sys.stderr)
|
22
|
+
sys.exit(1)
|
23
|
+
|
24
|
+
try:
|
25
|
+
# Load configuration using the base parser
|
26
|
+
parser = BaseConfigParser(config_path)
|
27
|
+
config = parser.config
|
28
|
+
except Exception as e:
|
29
|
+
print(f"Error loading or parsing config file {config_path}: {e}", file=sys.stderr)
|
30
|
+
sys.exit(1)
|
31
|
+
|
32
|
+
login_payloads: List[Dict[str, Any]] = [] # Initialize empty list
|
33
|
+
try:
|
34
|
+
with open(login_payloads_path, "r") as f:
|
35
|
+
for line_num, line in enumerate(f, 1):
|
36
|
+
line = line.strip() # Remove leading/trailing whitespace
|
37
|
+
if not line: # Skip empty lines
|
38
|
+
continue
|
39
|
+
try:
|
40
|
+
payload = json.loads(line)
|
41
|
+
if not isinstance(payload, dict):
|
42
|
+
raise ValueError("Each line must be a valid JSON object.")
|
43
|
+
login_payloads.append(payload)
|
44
|
+
except json.JSONDecodeError as e:
|
45
|
+
print(f"Error decoding JSON on line {line_num} in {login_payloads_path}: {e}", file=sys.stderr)
|
46
|
+
sys.exit(1)
|
47
|
+
except ValueError as e:
|
48
|
+
print(f"Error processing line {line_num} in {login_payloads_path}: {e}", file=sys.stderr)
|
49
|
+
sys.exit(1)
|
50
|
+
|
51
|
+
if not login_payloads: # Check if any payloads were loaded
|
52
|
+
print(f"Error: No valid login payloads found in {login_payloads_path}", file=sys.stderr)
|
53
|
+
sys.exit(1)
|
54
|
+
|
55
|
+
except Exception as e:
|
56
|
+
print(f"Error reading login payloads file {login_payloads_path}: {e}", file=sys.stderr)
|
57
|
+
sys.exit(1)
|
58
|
+
|
59
|
+
# Ensure the number of payloads matches the number of agents defined
|
60
|
+
num_defined_agents = len(config.agents)
|
61
|
+
if len(login_payloads) != num_defined_agents:
|
62
|
+
print(
|
63
|
+
f"Error: Number of login payloads ({len(login_payloads)}) in {login_payloads_path} "
|
64
|
+
f"does not match the number of agents defined in the config ({num_defined_agents}).",
|
65
|
+
file=sys.stderr,
|
66
|
+
)
|
67
|
+
sys.exit(1)
|
68
|
+
|
69
|
+
# Extract gameId from the first payload for display purposes (assuming all payloads are for the same game)
|
70
|
+
# Add basic check to ensure payloads exist and have gameId
|
71
|
+
game_id_display = "N/A"
|
72
|
+
if login_payloads and isinstance(login_payloads[0], dict) and "gameId" in login_payloads[0]:
|
73
|
+
game_id_display = login_payloads[0]["gameId"]
|
74
|
+
|
75
|
+
print(f"Starting experiment '{config.name}' with Game ID: {game_id_display}...")
|
76
|
+
print(f"Using config: {config_path}")
|
77
|
+
print(f"Using login payloads from: {login_payloads_path}")
|
78
|
+
print(f"Number of agents: {len(login_payloads)}")
|
79
|
+
|
80
|
+
try:
|
81
|
+
await parser.run_experiment(login_payloads)
|
82
|
+
print("Experiment finished.")
|
83
|
+
except Exception as e:
|
84
|
+
print(f"Error running experiment: {e}", file=sys.stderr)
|
85
|
+
sys.exit(1)
|
86
|
+
|
87
|
+
|
88
|
+
def run_cli():
|
89
|
+
"""Entry point function for the CLI script."""
|
90
|
+
parser = argparse.ArgumentParser(
|
91
|
+
description=(
|
92
|
+
"Economic Agents CLI - Run experiments with AI agents in economic simulations.\n\n"
|
93
|
+
"Example usage:\n"
|
94
|
+
" econagents run config.yaml --login-payloads-file payloads.jsonl\n\n"
|
95
|
+
"The config file should be a YAML file defining the experiment setup.\n"
|
96
|
+
"The login payloads file should be a JSONL file with login credentials for each agent."
|
97
|
+
),
|
98
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
99
|
+
)
|
100
|
+
subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands")
|
101
|
+
|
102
|
+
# --- Run Command ---
|
103
|
+
run_parser = subparsers.add_parser(
|
104
|
+
"run",
|
105
|
+
help="Run an experiment defined by a YAML configuration file.",
|
106
|
+
description=(
|
107
|
+
"Run an economic agent experiment using the specified configuration.\n\n"
|
108
|
+
"Example:\n"
|
109
|
+
" econagents run experiments/market_sim.yaml --login-payloads-file credentials.jsonl"
|
110
|
+
),
|
111
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
112
|
+
)
|
113
|
+
run_parser.add_argument(
|
114
|
+
"config_path",
|
115
|
+
type=str,
|
116
|
+
help="Path to the experiment configuration YAML file that defines the agent behaviors and experiment parameters."
|
117
|
+
)
|
118
|
+
run_parser.add_argument(
|
119
|
+
"--login-payloads-file",
|
120
|
+
required=True,
|
121
|
+
type=str,
|
122
|
+
help="Path to a JSON Lines (.jsonl) file containing login credentials for each agent, one per line."
|
123
|
+
)
|
124
|
+
|
125
|
+
args = parser.parse_args()
|
126
|
+
|
127
|
+
if args.command == "run":
|
128
|
+
try:
|
129
|
+
asyncio.run(async_main(args))
|
130
|
+
except KeyboardInterrupt:
|
131
|
+
print("\nExperiment interrupted by user.")
|
132
|
+
sys.exit(0)
|
133
|
+
|
134
|
+
|
135
|
+
if __name__ == "__main__":
|
136
|
+
run_cli()
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"""
|
2
|
+
Configuration parsers for different server types.
|
3
|
+
|
4
|
+
This package provides configuration parsers for different server types:
|
5
|
+
- Base: No custom event handlers
|
6
|
+
- Basic: Custom event handler for player-is-ready messages
|
7
|
+
- IBEX-TUDelft: Handles player-is-ready messages and role assignment
|
8
|
+
"""
|
9
|
+
|
10
|
+
from econagents.config_parser.base import BaseConfigParser
|
11
|
+
from econagents.config_parser.basic import BasicConfigParser
|
12
|
+
from econagents.config_parser.ibex_tudelft import IbexTudelftConfigParser
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"BaseConfigParser",
|
16
|
+
"BasicConfigParser",
|
17
|
+
"IbexTudelftConfigParser",
|
18
|
+
]
|
@@ -0,0 +1,518 @@
|
|
1
|
+
import importlib
|
2
|
+
import logging
|
3
|
+
import tempfile
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Dict, List, Literal, Optional, Type, cast
|
6
|
+
from datetime import datetime, date, time
|
7
|
+
|
8
|
+
import yaml
|
9
|
+
from pydantic import BaseModel, Field, create_model
|
10
|
+
|
11
|
+
from econagents.core.game_runner import GameRunner, GameRunnerConfig, HybridGameRunnerConfig, TurnBasedGameRunnerConfig
|
12
|
+
from econagents.core.manager.phase import PhaseManager, TurnBasedPhaseManager, HybridPhaseManager
|
13
|
+
from econagents.core.state.fields import EventField
|
14
|
+
from econagents.core.state.game import GameState, MetaInformation, PrivateInformation, PublicInformation
|
15
|
+
from econagents.core.state.market import MarketState
|
16
|
+
from econagents.core.agent_role import AgentRole
|
17
|
+
|
18
|
+
TYPE_MAPPING = {
|
19
|
+
"str": str,
|
20
|
+
"int": int,
|
21
|
+
"float": float,
|
22
|
+
"bool": bool,
|
23
|
+
"list": list,
|
24
|
+
"dict": dict,
|
25
|
+
"datetime": datetime,
|
26
|
+
"date": date,
|
27
|
+
"time": time,
|
28
|
+
"any": Any,
|
29
|
+
"MarketState": MarketState,
|
30
|
+
}
|
31
|
+
|
32
|
+
|
33
|
+
class EventHandler(BaseModel):
|
34
|
+
"""Configuration for an event handler."""
|
35
|
+
|
36
|
+
event: str
|
37
|
+
custom_code: Optional[str] = None
|
38
|
+
custom_module: Optional[str] = None
|
39
|
+
custom_function: Optional[str] = None
|
40
|
+
|
41
|
+
|
42
|
+
class AgentRoleConfig(BaseModel):
|
43
|
+
"""Configuration for an agent role."""
|
44
|
+
|
45
|
+
role_id: int
|
46
|
+
name: str
|
47
|
+
llm_type: str = "ChatOpenAI"
|
48
|
+
llm_params: Dict[str, Any] = Field(default_factory=dict)
|
49
|
+
prompts: List[Dict[str, str]] = Field(default_factory=list)
|
50
|
+
task_phases: List[int] = Field(default_factory=list)
|
51
|
+
task_phases_excluded: List[int] = Field(default_factory=list)
|
52
|
+
|
53
|
+
def create_agent_role(self) -> AgentRole:
|
54
|
+
"""Create an AgentRole instance from this configuration."""
|
55
|
+
# Dynamically create the LLM provider
|
56
|
+
llm_class = getattr(importlib.import_module("econagents.llm"), self.llm_type)
|
57
|
+
llm_instance = llm_class(**self.llm_params)
|
58
|
+
|
59
|
+
# Create a dynamic AgentRole subclass
|
60
|
+
agent_role_attrs = {
|
61
|
+
"role": self.role_id,
|
62
|
+
"name": self.name,
|
63
|
+
"llm": llm_instance,
|
64
|
+
"task_phases": self.task_phases,
|
65
|
+
"task_phases_excluded": self.task_phases_excluded,
|
66
|
+
}
|
67
|
+
agent_role = type(
|
68
|
+
f"Dynamic{self.name}Role",
|
69
|
+
(AgentRole,),
|
70
|
+
agent_role_attrs,
|
71
|
+
)
|
72
|
+
|
73
|
+
return agent_role()
|
74
|
+
|
75
|
+
|
76
|
+
class AgentMappingConfig(BaseModel):
|
77
|
+
"""Configuration mapping agent IDs to role IDs."""
|
78
|
+
|
79
|
+
id: int
|
80
|
+
role_id: int
|
81
|
+
|
82
|
+
|
83
|
+
class AgentConfig(BaseModel):
|
84
|
+
"""Configuration for an agent role."""
|
85
|
+
|
86
|
+
role_id: int
|
87
|
+
name: str
|
88
|
+
llm_type: str = "ChatOpenAI"
|
89
|
+
llm_params: Dict[str, Any] = Field(default_factory=dict)
|
90
|
+
|
91
|
+
def create_agent_role(self) -> AgentRole:
|
92
|
+
"""Create an AgentRole instance from this configuration."""
|
93
|
+
# Dynamically create the LLM provider
|
94
|
+
llm_class = getattr(importlib.import_module("econagents.llm"), self.llm_type)
|
95
|
+
llm_instance = llm_class(**self.llm_params)
|
96
|
+
|
97
|
+
# Create a dynamic AgentRole subclass
|
98
|
+
agent_role = type(
|
99
|
+
f"Dynamic{self.name}Role", (AgentRole,), {"role": self.role_id, "name": self.name, "llm": llm_instance}
|
100
|
+
)
|
101
|
+
|
102
|
+
return agent_role()
|
103
|
+
|
104
|
+
|
105
|
+
class StateFieldConfig(BaseModel):
|
106
|
+
"""Configuration for a field in the state."""
|
107
|
+
|
108
|
+
name: str
|
109
|
+
type: str
|
110
|
+
default: Any = None
|
111
|
+
default_factory: Optional[str] = None
|
112
|
+
event_key: Optional[str] = None
|
113
|
+
exclude_from_mapping: bool = False
|
114
|
+
optional: bool = False
|
115
|
+
events: Optional[List[str]] = None
|
116
|
+
exclude_events: Optional[List[str]] = None
|
117
|
+
|
118
|
+
|
119
|
+
class StateConfig(BaseModel):
|
120
|
+
"""Configuration for a game state."""
|
121
|
+
|
122
|
+
meta_information: List[StateFieldConfig] = Field(default_factory=list)
|
123
|
+
private_information: List[StateFieldConfig] = Field(default_factory=list)
|
124
|
+
public_information: List[StateFieldConfig] = Field(default_factory=list)
|
125
|
+
|
126
|
+
def create_state_class(self) -> Type[GameState]:
|
127
|
+
"""Create a GameState subclass from this configuration using create_model."""
|
128
|
+
|
129
|
+
def resolve_field_type(field_type_str: str) -> Any:
|
130
|
+
"""Resolve type string to Python type."""
|
131
|
+
if field_type_str in TYPE_MAPPING:
|
132
|
+
return TYPE_MAPPING[field_type_str]
|
133
|
+
else:
|
134
|
+
try:
|
135
|
+
resolved_type = eval(
|
136
|
+
field_type_str, {"list": list, "dict": dict, "Any": Any, "MarketState": MarketState}
|
137
|
+
)
|
138
|
+
return resolved_type
|
139
|
+
except (NameError, SyntaxError):
|
140
|
+
raise ValueError(f"Unsupported field type: {field_type_str}")
|
141
|
+
|
142
|
+
def get_default_factory(factory_name: str) -> Any:
|
143
|
+
"""Get default factory function."""
|
144
|
+
if factory_name == "list":
|
145
|
+
return list
|
146
|
+
elif factory_name == "dict":
|
147
|
+
return dict
|
148
|
+
else:
|
149
|
+
try:
|
150
|
+
return eval(factory_name, {"MarketState": MarketState})
|
151
|
+
except (NameError, SyntaxError):
|
152
|
+
raise ValueError(f"Unsupported default_factory: {factory_name}")
|
153
|
+
|
154
|
+
def create_fields_dict(field_configs: List[StateFieldConfig]) -> Dict[str, Any]:
|
155
|
+
"""Create a dictionary of field definitions for create_model."""
|
156
|
+
fields = {}
|
157
|
+
for field in field_configs:
|
158
|
+
base_type = resolve_field_type(field.type)
|
159
|
+
field_type = Optional[base_type] if field.optional else base_type
|
160
|
+
|
161
|
+
event_field_args = {
|
162
|
+
"event_key": field.event_key,
|
163
|
+
"exclude_from_mapping": field.exclude_from_mapping,
|
164
|
+
"events": field.events,
|
165
|
+
"exclude_events": field.exclude_events,
|
166
|
+
}
|
167
|
+
# Handle default vs default_factory
|
168
|
+
if field.default_factory:
|
169
|
+
event_field_args["default_factory"] = get_default_factory(field.default_factory)
|
170
|
+
else:
|
171
|
+
# Pydantic handles Optional defaults correctly (None if optional and no default)
|
172
|
+
event_field_args["default"] = field.default
|
173
|
+
|
174
|
+
# EventField needs to be the default value passed to create_model
|
175
|
+
field_definition = EventField(**event_field_args) # type: ignore
|
176
|
+
fields[field.name] = (field_type, field_definition)
|
177
|
+
return fields
|
178
|
+
|
179
|
+
# Create dynamic classes using create_model
|
180
|
+
meta_fields = create_fields_dict(self.meta_information)
|
181
|
+
DynamicMeta = create_model(
|
182
|
+
"DynamicMeta",
|
183
|
+
__base__=MetaInformation,
|
184
|
+
**meta_fields,
|
185
|
+
)
|
186
|
+
|
187
|
+
private_fields = create_fields_dict(self.private_information)
|
188
|
+
DynamicPrivate = create_model(
|
189
|
+
"DynamicPrivate",
|
190
|
+
__base__=PrivateInformation,
|
191
|
+
**private_fields,
|
192
|
+
)
|
193
|
+
|
194
|
+
public_fields = create_fields_dict(self.public_information)
|
195
|
+
DynamicPublic = create_model(
|
196
|
+
"DynamicPublic",
|
197
|
+
__base__=PublicInformation,
|
198
|
+
**public_fields,
|
199
|
+
)
|
200
|
+
|
201
|
+
# Create the final game state class
|
202
|
+
DynamicGameState = create_model(
|
203
|
+
"DynamicGameState",
|
204
|
+
__base__=GameState,
|
205
|
+
meta=(DynamicMeta, Field(default_factory=DynamicMeta)),
|
206
|
+
private_information=(DynamicPrivate, Field(default_factory=DynamicPrivate)),
|
207
|
+
public_information=(DynamicPublic, Field(default_factory=DynamicPublic)),
|
208
|
+
)
|
209
|
+
|
210
|
+
# Cast to Type[GameState] for type hinting
|
211
|
+
return cast(Type[GameState], DynamicGameState)
|
212
|
+
|
213
|
+
|
214
|
+
class ManagerConfig(BaseModel):
|
215
|
+
"""Configuration for a manager."""
|
216
|
+
|
217
|
+
type: str = "TurnBasedPhaseManager"
|
218
|
+
event_handlers: List[EventHandler] = Field(default_factory=list)
|
219
|
+
|
220
|
+
def create_manager(
|
221
|
+
self, game_id: int, state: GameState, agent_role: Optional[AgentRole], auth_kwargs: Dict[str, Any]
|
222
|
+
) -> PhaseManager:
|
223
|
+
"""Create a PhaseManager instance from this configuration."""
|
224
|
+
|
225
|
+
manager_class: Type[PhaseManager]
|
226
|
+
|
227
|
+
if self.type == "TurnBasedPhaseManager":
|
228
|
+
manager_class = TurnBasedPhaseManager
|
229
|
+
elif self.type == "HybridPhaseManager":
|
230
|
+
manager_class = HybridPhaseManager
|
231
|
+
else:
|
232
|
+
raise ValueError(f"Invalid manager type: {self.type}")
|
233
|
+
|
234
|
+
# Create the manager instance
|
235
|
+
manager = manager_class(
|
236
|
+
auth_mechanism_kwargs=auth_kwargs,
|
237
|
+
state=state,
|
238
|
+
agent_role=agent_role,
|
239
|
+
)
|
240
|
+
|
241
|
+
# Set Game ID
|
242
|
+
if hasattr(manager, "game_id"):
|
243
|
+
setattr(manager, "game_id", game_id)
|
244
|
+
|
245
|
+
# Register event handlers
|
246
|
+
for handler in self.event_handlers:
|
247
|
+
# Create a handler function based on the configuration
|
248
|
+
async def create_handler(message, handler=handler):
|
249
|
+
# Execute custom code if specified
|
250
|
+
if handler.custom_code:
|
251
|
+
# Use exec to run the custom code with access to manager and message
|
252
|
+
local_vars = {"manager": manager, "message": message}
|
253
|
+
exec(handler.custom_code, globals(), local_vars)
|
254
|
+
|
255
|
+
# Import and execute custom function if specified
|
256
|
+
if handler.custom_module and handler.custom_function:
|
257
|
+
try:
|
258
|
+
module = importlib.import_module(handler.custom_module)
|
259
|
+
func = getattr(module, handler.custom_function)
|
260
|
+
await func(manager, message)
|
261
|
+
except (ImportError, AttributeError) as e:
|
262
|
+
manager.logger.error(f"Error importing custom handler: {e}")
|
263
|
+
|
264
|
+
# Register the handler
|
265
|
+
manager.register_event_handler(handler.event, create_handler)
|
266
|
+
|
267
|
+
return manager
|
268
|
+
|
269
|
+
|
270
|
+
class RunnerConfig(BaseModel):
|
271
|
+
"""Configuration for a game runner."""
|
272
|
+
|
273
|
+
type: str = "GameRunner"
|
274
|
+
protocol: str = "ws"
|
275
|
+
hostname: str
|
276
|
+
path: str = "wss"
|
277
|
+
port: int
|
278
|
+
game_id: int
|
279
|
+
logs_dir: str = "logs"
|
280
|
+
log_level: str = "INFO"
|
281
|
+
prompts_dir: str = "prompts"
|
282
|
+
phase_transition_event: str = "phase-transition"
|
283
|
+
phase_identifier_key: str = "phase"
|
284
|
+
observability_provider: Optional[Literal["langsmith", "langfuse"]] = None
|
285
|
+
|
286
|
+
# For hybrid game runners
|
287
|
+
continuous_phases: List[int] = Field(default_factory=list)
|
288
|
+
min_action_delay: int = 5
|
289
|
+
max_action_delay: int = 10
|
290
|
+
|
291
|
+
def create_runner_config(self) -> GameRunnerConfig:
|
292
|
+
"""Create a GameRunnerConfig instance from this configuration."""
|
293
|
+
# Map string log level to int
|
294
|
+
log_levels = {
|
295
|
+
"DEBUG": logging.DEBUG,
|
296
|
+
"INFO": logging.INFO,
|
297
|
+
"WARNING": logging.WARNING,
|
298
|
+
"ERROR": logging.ERROR,
|
299
|
+
"CRITICAL": logging.CRITICAL,
|
300
|
+
}
|
301
|
+
|
302
|
+
log_level_int = log_levels.get(self.log_level.upper(), logging.INFO)
|
303
|
+
|
304
|
+
# Base arguments for constructor - explicitly defining each parameter
|
305
|
+
if self.type == "TurnBasedGameRunner":
|
306
|
+
return TurnBasedGameRunnerConfig(
|
307
|
+
protocol=self.protocol,
|
308
|
+
hostname=self.hostname,
|
309
|
+
path=self.path,
|
310
|
+
port=self.port,
|
311
|
+
game_id=self.game_id,
|
312
|
+
logs_dir=Path.cwd() / self.logs_dir,
|
313
|
+
log_level=log_level_int,
|
314
|
+
prompts_dir=Path.cwd() / self.prompts_dir,
|
315
|
+
phase_transition_event=self.phase_transition_event,
|
316
|
+
phase_identifier_key=self.phase_identifier_key,
|
317
|
+
observability_provider=self.observability_provider,
|
318
|
+
state_class=None,
|
319
|
+
)
|
320
|
+
elif self.type == "HybridGameRunner":
|
321
|
+
return HybridGameRunnerConfig(
|
322
|
+
protocol=self.protocol,
|
323
|
+
hostname=self.hostname,
|
324
|
+
path=self.path,
|
325
|
+
port=self.port,
|
326
|
+
game_id=self.game_id,
|
327
|
+
logs_dir=Path.cwd() / self.logs_dir,
|
328
|
+
log_level=log_level_int,
|
329
|
+
prompts_dir=Path.cwd() / self.prompts_dir,
|
330
|
+
phase_transition_event=self.phase_transition_event,
|
331
|
+
phase_identifier_key=self.phase_identifier_key,
|
332
|
+
observability_provider=self.observability_provider,
|
333
|
+
continuous_phases=self.continuous_phases,
|
334
|
+
min_action_delay=self.min_action_delay,
|
335
|
+
max_action_delay=self.max_action_delay,
|
336
|
+
)
|
337
|
+
else:
|
338
|
+
raise ValueError(f"Invalid runner type: {self.type}")
|
339
|
+
|
340
|
+
|
341
|
+
class ExperimentConfig(BaseModel):
|
342
|
+
"""Configuration for an entire experiment."""
|
343
|
+
|
344
|
+
name: str
|
345
|
+
description: str = ""
|
346
|
+
prompt_partials: List[Dict[str, str]] = Field(default_factory=list)
|
347
|
+
agent_roles: List[AgentRoleConfig] = Field(default_factory=list)
|
348
|
+
agents: List[AgentMappingConfig] = Field(default_factory=list)
|
349
|
+
state: StateConfig
|
350
|
+
manager: ManagerConfig
|
351
|
+
runner: RunnerConfig
|
352
|
+
_temp_prompts_dir: Optional[Path] = None
|
353
|
+
|
354
|
+
def _compile_inline_prompts(self) -> Path:
|
355
|
+
"""Compile prompts from config into a temporary directory.
|
356
|
+
|
357
|
+
Returns:
|
358
|
+
Path to the temporary directory containing compiled prompts
|
359
|
+
"""
|
360
|
+
# Create a temporary directory for prompts
|
361
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="econagents_prompts_"))
|
362
|
+
self._temp_prompts_dir = temp_dir
|
363
|
+
|
364
|
+
# Create _partials directory
|
365
|
+
partials_dir = temp_dir / "_partials"
|
366
|
+
partials_dir.mkdir(parents=True, exist_ok=True)
|
367
|
+
|
368
|
+
# Write prompt partials
|
369
|
+
for partial in self.prompt_partials:
|
370
|
+
partial_file = partials_dir / f"{partial['name']}.jinja2"
|
371
|
+
partial_file.write_text(partial["content"])
|
372
|
+
|
373
|
+
# Write prompts for each agent role
|
374
|
+
for role in self.agent_roles:
|
375
|
+
if not hasattr(role, "prompts") or not role.prompts:
|
376
|
+
continue
|
377
|
+
|
378
|
+
for prompt in role.prompts:
|
379
|
+
# Each prompt should be a dict with one key (type) and one value (content)
|
380
|
+
for prompt_type, content in prompt.items():
|
381
|
+
# Parse the prompt type to get the base type and phase
|
382
|
+
parts = prompt_type.split("_phase_")
|
383
|
+
base_type = parts[0] # system or user
|
384
|
+
phase = parts[1] if len(parts) > 1 else None
|
385
|
+
|
386
|
+
# Create the prompt file name
|
387
|
+
if phase:
|
388
|
+
file_name = f"{role.name.lower()}_{base_type}_phase_{phase}.jinja2"
|
389
|
+
else:
|
390
|
+
file_name = f"{role.name.lower()}_{base_type}.jinja2"
|
391
|
+
|
392
|
+
# Write the prompt file
|
393
|
+
prompt_file = temp_dir / file_name
|
394
|
+
prompt_file.write_text(content)
|
395
|
+
|
396
|
+
return temp_dir
|
397
|
+
|
398
|
+
async def run_experiment(self, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
|
399
|
+
"""Run the experiment from this configuration."""
|
400
|
+
# Create state class
|
401
|
+
state_class = self.state.create_state_class()
|
402
|
+
role_configs = {role_config.role_id: role_config for role_config in self.agent_roles}
|
403
|
+
|
404
|
+
if not self.agent_roles and self.agents:
|
405
|
+
raise ValueError(
|
406
|
+
"Configuration has 'agents' but no 'agent_roles'. Cannot determine agent role configurations."
|
407
|
+
)
|
408
|
+
|
409
|
+
agent_to_role_map = {agent_map.id: agent_map.role_id for agent_map in self.agents}
|
410
|
+
|
411
|
+
# Create managers for each agent
|
412
|
+
agents = []
|
413
|
+
for payload in login_payloads:
|
414
|
+
agent_id = payload.get("agent_id")
|
415
|
+
if agent_id is None:
|
416
|
+
raise ValueError(f"Login payload missing 'agent_id' field: {payload}")
|
417
|
+
|
418
|
+
role_id = agent_to_role_map.get(agent_id)
|
419
|
+
if role_id is None:
|
420
|
+
raise ValueError(f"No role_id mapping found for agent {agent_id}")
|
421
|
+
|
422
|
+
if role_id not in role_configs:
|
423
|
+
raise ValueError(f"No agent role configuration found for role_id {role_id}")
|
424
|
+
|
425
|
+
agent_role_instance = role_configs[role_id].create_agent_role()
|
426
|
+
|
427
|
+
agents.append(
|
428
|
+
self.manager.create_manager(
|
429
|
+
game_id=game_id,
|
430
|
+
state=state_class(game_id=game_id),
|
431
|
+
agent_role=agent_role_instance,
|
432
|
+
auth_kwargs=payload,
|
433
|
+
)
|
434
|
+
)
|
435
|
+
|
436
|
+
# Create runner config
|
437
|
+
runner_config = self.runner.create_runner_config()
|
438
|
+
runner_config.state_class = state_class
|
439
|
+
runner_config.game_id = game_id
|
440
|
+
|
441
|
+
# Compile inline prompts if needed
|
442
|
+
if any(hasattr(role, "prompts") and role.prompts for role in self.agent_roles):
|
443
|
+
prompts_dir = self._compile_inline_prompts()
|
444
|
+
runner_config.prompts_dir = prompts_dir
|
445
|
+
|
446
|
+
# Create and run game runner
|
447
|
+
runner = GameRunner(config=runner_config, agents=agents)
|
448
|
+
await runner.run_game()
|
449
|
+
|
450
|
+
# Clean up temporary prompts directory
|
451
|
+
if self._temp_prompts_dir and self._temp_prompts_dir.exists():
|
452
|
+
import shutil
|
453
|
+
|
454
|
+
shutil.rmtree(self._temp_prompts_dir)
|
455
|
+
|
456
|
+
|
457
|
+
class BaseConfigParser:
|
458
|
+
"""Base configuration parser with no custom event handlers."""
|
459
|
+
|
460
|
+
def __init__(self, config_path: Path):
|
461
|
+
"""
|
462
|
+
Initialize the config parser with a path to a YAML configuration file.
|
463
|
+
|
464
|
+
Args:
|
465
|
+
config_path: Path to the YAML configuration file
|
466
|
+
"""
|
467
|
+
self.config_path = config_path
|
468
|
+
self.config = self.load_config()
|
469
|
+
|
470
|
+
def load_config(self) -> ExperimentConfig:
|
471
|
+
"""Load the experiment configuration from the YAML file."""
|
472
|
+
with open(self.config_path, "r") as file:
|
473
|
+
config_data = yaml.safe_load(file)
|
474
|
+
|
475
|
+
# Handle backward compatibility with old format
|
476
|
+
if not config_data.get("agent_roles") and "agents" in config_data:
|
477
|
+
# Check if the agents field contains role configurations
|
478
|
+
if config_data["agents"] and "name" in config_data["agents"][0]:
|
479
|
+
# Old format with agent configurations in "agents" field
|
480
|
+
config_data["agent_roles"] = config_data.pop("agents")
|
481
|
+
config_data["agents"] = []
|
482
|
+
|
483
|
+
return ExperimentConfig(**config_data)
|
484
|
+
|
485
|
+
def create_manager(
|
486
|
+
self, game_id: int, state: GameState, agent_role: Optional[AgentRole], auth_kwargs: Dict[str, Any]
|
487
|
+
) -> PhaseManager:
|
488
|
+
"""
|
489
|
+
Create a manager instance based on the configuration.
|
490
|
+
This base implementation has no custom event handlers.
|
491
|
+
|
492
|
+
Args:
|
493
|
+
game_id: The game ID
|
494
|
+
state: The game state instance
|
495
|
+
agent_role: The agent role instance
|
496
|
+
auth_kwargs: Authentication mechanism keyword arguments
|
497
|
+
|
498
|
+
Returns:
|
499
|
+
A PhaseManager instance
|
500
|
+
"""
|
501
|
+
return self.config.manager.create_manager(
|
502
|
+
game_id=game_id, state=state, agent_role=agent_role, auth_kwargs=auth_kwargs
|
503
|
+
)
|
504
|
+
|
505
|
+
async def run_experiment(self, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
|
506
|
+
"""
|
507
|
+
Run the experiment from this configuration.
|
508
|
+
|
509
|
+
Args:
|
510
|
+
login_payloads: A list of dictionaries containing login information for each agent
|
511
|
+
"""
|
512
|
+
await self.config.run_experiment(login_payloads, game_id)
|
513
|
+
|
514
|
+
|
515
|
+
async def run_experiment_from_yaml(yaml_path: Path, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
|
516
|
+
"""Run an experiment from a YAML configuration file."""
|
517
|
+
parser = BaseConfigParser(yaml_path)
|
518
|
+
await parser.run_experiment(login_payloads, game_id)
|