econagents 0.0.1__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 +31 -0
- econagents/_c_extension.pyi +5 -0
- econagents/core/__init__.py +7 -0
- econagents/core/agent_role.py +360 -0
- econagents/core/events.py +14 -0
- econagents/core/game_runner.py +358 -0
- econagents/core/logging_mixin.py +45 -0
- econagents/core/manager/__init__.py +5 -0
- econagents/core/manager/base.py +430 -0
- econagents/core/manager/phase.py +498 -0
- econagents/core/state/__init__.py +0 -0
- econagents/core/state/fields.py +51 -0
- econagents/core/state/game.py +222 -0
- econagents/core/state/market.py +124 -0
- econagents/core/transport.py +132 -0
- econagents/llm/__init__.py +3 -0
- econagents/llm/openai.py +61 -0
- econagents/py.typed +0 -0
- econagents-0.0.1.dist-info/METADATA +90 -0
- econagents-0.0.1.dist-info/RECORD +21 -0
- econagents-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,358 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
import queue
|
4
|
+
from contextvars import ContextVar
|
5
|
+
from logging.handlers import QueueHandler, QueueListener
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Optional, Type
|
8
|
+
|
9
|
+
from pydantic import BaseModel, Field
|
10
|
+
|
11
|
+
from econagents.core.manager.base import AgentManager
|
12
|
+
from econagents.core.manager.phase import PhaseManager
|
13
|
+
from econagents.core.state.game import GameState
|
14
|
+
from econagents.core.transport import AuthenticationMechanism, SimpleLoginPayloadAuth
|
15
|
+
|
16
|
+
ctx_agent_id: ContextVar[str] = ContextVar("agent_id", default="N/A")
|
17
|
+
|
18
|
+
|
19
|
+
class ContextInjectingFilter(logging.Filter):
|
20
|
+
"""Filter that injects agent_id context into log records."""
|
21
|
+
|
22
|
+
def filter(self, record):
|
23
|
+
record.agent_id = ctx_agent_id.get()
|
24
|
+
return True
|
25
|
+
|
26
|
+
|
27
|
+
class GameRunnerConfig(BaseModel):
|
28
|
+
"""Configuration class for GameRunner."""
|
29
|
+
|
30
|
+
# Server configuration
|
31
|
+
protocol: str = "ws"
|
32
|
+
"""Protocol to use for the server"""
|
33
|
+
hostname: str
|
34
|
+
"""Hostname of the server"""
|
35
|
+
path: str
|
36
|
+
"""Path to the server"""
|
37
|
+
port: int
|
38
|
+
|
39
|
+
# Game configuration
|
40
|
+
game_id: int
|
41
|
+
"""ID of the game"""
|
42
|
+
logs_dir: Path = Path.cwd() / "logs"
|
43
|
+
"""Directory to store logs"""
|
44
|
+
log_level: int = logging.INFO
|
45
|
+
"""Level of logging to use"""
|
46
|
+
prompts_dir: Path = Path.cwd() / "prompts"
|
47
|
+
|
48
|
+
# Authentication
|
49
|
+
auth_mechanism: Optional[AuthenticationMechanism] = SimpleLoginPayloadAuth()
|
50
|
+
"""Authentication mechanism to use"""
|
51
|
+
|
52
|
+
# Phase transition configuration
|
53
|
+
phase_transition_event: str = "phase-transition"
|
54
|
+
"""Event to use for phase transitions"""
|
55
|
+
phase_identifier_key: str = "phase"
|
56
|
+
"""Key in data to use for phase identification"""
|
57
|
+
|
58
|
+
# State configuration
|
59
|
+
state_class: Optional[Type[GameState]] = None
|
60
|
+
"""Class to use for the state"""
|
61
|
+
|
62
|
+
|
63
|
+
class TurnBasedGameRunnerConfig(GameRunnerConfig):
|
64
|
+
"""Configuration class for TurnBasedGameRunner."""
|
65
|
+
|
66
|
+
|
67
|
+
class HybridGameRunnerConfig(GameRunnerConfig):
|
68
|
+
"""Configuration class for TurnBasedGameRunner."""
|
69
|
+
|
70
|
+
continuous_phases: list[int] = Field(default_factory=list)
|
71
|
+
min_action_delay: int = Field(default=5)
|
72
|
+
max_action_delay: int = Field(default=10)
|
73
|
+
|
74
|
+
|
75
|
+
class GameRunner:
|
76
|
+
def __init__(
|
77
|
+
self,
|
78
|
+
config: GameRunnerConfig,
|
79
|
+
agents: list[PhaseManager],
|
80
|
+
):
|
81
|
+
"""
|
82
|
+
Generic game runner for managing agent connections to a game server. This can handle both turn-based and continuous games.
|
83
|
+
|
84
|
+
This class handles:
|
85
|
+
|
86
|
+
- Agent spawning and connection management
|
87
|
+
|
88
|
+
- Logging setup for game and agents
|
89
|
+
|
90
|
+
Args:
|
91
|
+
config: GameRunnerConfig instance with server and path settings
|
92
|
+
agents: List of AgentManager instances
|
93
|
+
"""
|
94
|
+
self.config = config
|
95
|
+
self.agents = agents
|
96
|
+
self.game_log_queues: dict[int, queue.Queue] = {}
|
97
|
+
self.game_log_listeners: dict[int, QueueListener] = {}
|
98
|
+
|
99
|
+
# Create log directories if it doesn't exist
|
100
|
+
if self.config.logs_dir:
|
101
|
+
self.config.logs_dir.mkdir(parents=True, exist_ok=True)
|
102
|
+
|
103
|
+
def _setup_game_log_queue(self, game_id: int) -> queue.Queue:
|
104
|
+
"""
|
105
|
+
Set up a logging queue for a game and its associated QueueListener.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
game_id: Game identifier
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
Queue used for logging
|
112
|
+
"""
|
113
|
+
if game_id in self.game_log_queues:
|
114
|
+
return self.game_log_queues[game_id]
|
115
|
+
|
116
|
+
if not self.config.logs_dir:
|
117
|
+
# If no log path, just return a queue without a listener
|
118
|
+
log_queue: queue.Queue = queue.Queue()
|
119
|
+
self.game_log_queues[game_id] = log_queue
|
120
|
+
return log_queue
|
121
|
+
|
122
|
+
# Create a game-specific directory for all logs related to this game
|
123
|
+
game_dir = self.config.logs_dir / f"game_{game_id}"
|
124
|
+
game_dir.mkdir(parents=True, exist_ok=True)
|
125
|
+
|
126
|
+
game_log_file = game_dir / "all.log"
|
127
|
+
Path(game_log_file).touch()
|
128
|
+
|
129
|
+
game_queue: queue.Queue = queue.Queue()
|
130
|
+
self.game_log_queues[game_id] = game_queue
|
131
|
+
|
132
|
+
formatter = logging.Formatter("%(asctime)s [%(levelname)s] [AGENT %(agent_id)s] %(message)s")
|
133
|
+
|
134
|
+
# Create a file handler for the game log
|
135
|
+
file_handler = logging.FileHandler(game_log_file)
|
136
|
+
file_handler.setFormatter(formatter)
|
137
|
+
|
138
|
+
# Create and start the listener
|
139
|
+
listener = QueueListener(game_queue, file_handler)
|
140
|
+
listener.start()
|
141
|
+
self.game_log_listeners[game_id] = listener
|
142
|
+
|
143
|
+
return game_queue
|
144
|
+
|
145
|
+
def get_agent_logger(self, agent_id: int, game_id: int) -> logging.Logger:
|
146
|
+
"""
|
147
|
+
Configure and return a logger for an agent.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
agent_id (int): Agent identifier
|
151
|
+
game_id (int): Game identifier
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
logging.Logger: Configured logger instance
|
155
|
+
"""
|
156
|
+
if not self.config.logs_dir:
|
157
|
+
# Return a default logger if no log path is configured
|
158
|
+
logger = logging.getLogger(f"agent_{agent_id}")
|
159
|
+
logger.setLevel(self.config.log_level)
|
160
|
+
return logger
|
161
|
+
|
162
|
+
# Ensure game log queue is set up
|
163
|
+
game_log_queue = self._setup_game_log_queue(game_id)
|
164
|
+
|
165
|
+
# Create a game-specific directory for all logs
|
166
|
+
game_dir = self.config.logs_dir / f"game_{game_id}"
|
167
|
+
game_dir.mkdir(parents=True, exist_ok=True)
|
168
|
+
|
169
|
+
# Store agent logs in the game-specific directory
|
170
|
+
agent_log_file = game_dir / f"agent_{agent_id}.log"
|
171
|
+
|
172
|
+
agent_logger = logging.getLogger(f"agent_{agent_id}")
|
173
|
+
agent_logger.setLevel(self.config.log_level)
|
174
|
+
|
175
|
+
# Clear existing handlers to avoid duplicates
|
176
|
+
for handler in agent_logger.handlers[:]:
|
177
|
+
agent_logger.removeHandler(handler)
|
178
|
+
|
179
|
+
# Create or clear the agent log file
|
180
|
+
if agent_log_file.exists():
|
181
|
+
agent_log_file.unlink()
|
182
|
+
Path(agent_log_file).touch()
|
183
|
+
|
184
|
+
formatter = logging.Formatter("%(asctime)s [%(levelname)s] [AGENT %(agent_id)s] %(message)s")
|
185
|
+
|
186
|
+
# Setup file handler for agent log
|
187
|
+
file_handler = logging.FileHandler(agent_log_file)
|
188
|
+
file_handler.setFormatter(formatter)
|
189
|
+
|
190
|
+
# Add context filter
|
191
|
+
context_filter = ContextInjectingFilter()
|
192
|
+
file_handler.addFilter(context_filter)
|
193
|
+
|
194
|
+
# Console handler
|
195
|
+
console_handler = logging.StreamHandler()
|
196
|
+
console_handler.setFormatter(formatter)
|
197
|
+
console_handler.addFilter(context_filter)
|
198
|
+
|
199
|
+
# Setup queue handler for game log
|
200
|
+
queue_handler = QueueHandler(game_log_queue)
|
201
|
+
queue_handler.addFilter(context_filter)
|
202
|
+
|
203
|
+
# Add all handlers
|
204
|
+
agent_logger.addHandler(file_handler)
|
205
|
+
agent_logger.addHandler(console_handler)
|
206
|
+
agent_logger.addHandler(queue_handler) # Use queue handler instead of direct file handler
|
207
|
+
|
208
|
+
return agent_logger
|
209
|
+
|
210
|
+
def get_game_logger(self, game_id: int) -> logging.Logger:
|
211
|
+
"""
|
212
|
+
Configure and return a logger for a game.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
game_id (int): Game identifier
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
logging.Logger: Configured logger instance
|
219
|
+
"""
|
220
|
+
if not self.config.logs_dir:
|
221
|
+
# Return a default logger if no log path is configured
|
222
|
+
logger = logging.getLogger(f"game_{game_id}")
|
223
|
+
logger.setLevel(self.config.log_level)
|
224
|
+
return logger
|
225
|
+
|
226
|
+
# Ensure game log queue is set up
|
227
|
+
game_log_queue = self._setup_game_log_queue(game_id)
|
228
|
+
|
229
|
+
game_logger = logging.getLogger(f"game_{game_id}")
|
230
|
+
game_logger.setLevel(self.config.log_level)
|
231
|
+
|
232
|
+
# Clear existing handlers to avoid duplicates
|
233
|
+
for handler in game_logger.handlers[:]:
|
234
|
+
game_logger.removeHandler(handler)
|
235
|
+
|
236
|
+
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
237
|
+
|
238
|
+
# Add context filter
|
239
|
+
context_filter = ContextInjectingFilter()
|
240
|
+
|
241
|
+
# Console handler
|
242
|
+
console_handler = logging.StreamHandler()
|
243
|
+
console_handler.setFormatter(formatter)
|
244
|
+
console_handler.addFilter(context_filter)
|
245
|
+
|
246
|
+
# Setup queue handler for game log
|
247
|
+
queue_handler = QueueHandler(game_log_queue)
|
248
|
+
|
249
|
+
# Add handlers
|
250
|
+
game_logger.addHandler(console_handler)
|
251
|
+
game_logger.addHandler(queue_handler)
|
252
|
+
|
253
|
+
return game_logger
|
254
|
+
|
255
|
+
def cleanup_logging(self) -> None:
|
256
|
+
"""
|
257
|
+
Clean up logging resources, stopping all queue listeners.
|
258
|
+
Should be called when shutting down the game runner.
|
259
|
+
"""
|
260
|
+
for game_id, listener in self.game_log_listeners.items():
|
261
|
+
try:
|
262
|
+
listener.stop()
|
263
|
+
except Exception as e:
|
264
|
+
print(f"Error stopping listener for game {game_id}: {e}")
|
265
|
+
|
266
|
+
self.game_log_listeners.clear()
|
267
|
+
self.game_log_queues.clear()
|
268
|
+
|
269
|
+
def _inject_default_config(self, agent_manager: PhaseManager) -> None:
|
270
|
+
"""
|
271
|
+
Inject default configuration into an agent manager.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
agent_manager (PhaseManager): Agent manager to inject configuration into
|
275
|
+
"""
|
276
|
+
if not agent_manager.url:
|
277
|
+
agent_manager.url = f"{self.config.protocol}://{self.config.hostname}:{self.config.port}/{self.config.path}"
|
278
|
+
agent_manager.logger.debug(f"Injected default configuration into agent manager: {agent_manager.url}")
|
279
|
+
|
280
|
+
if not agent_manager.phase_transition_event:
|
281
|
+
agent_manager.phase_transition_event = self.config.phase_transition_event
|
282
|
+
agent_manager.logger.debug(
|
283
|
+
f"Injected default phase transition event: {agent_manager.phase_transition_event}"
|
284
|
+
)
|
285
|
+
|
286
|
+
if not agent_manager.phase_identifier_key:
|
287
|
+
agent_manager.phase_identifier_key = self.config.phase_identifier_key
|
288
|
+
agent_manager.logger.debug(f"Injected default phase identifier key: {agent_manager.phase_identifier_key}")
|
289
|
+
|
290
|
+
if not agent_manager.prompts_dir:
|
291
|
+
agent_manager.prompts_dir = self.config.prompts_dir
|
292
|
+
agent_manager.logger.debug(f"Injected default prompts directory: {agent_manager.prompts_dir}")
|
293
|
+
|
294
|
+
if not agent_manager.state and self.config.state_class:
|
295
|
+
agent_manager.state = self.config.state_class()
|
296
|
+
agent_manager.logger.debug(f"Injected default state: {agent_manager.state}")
|
297
|
+
|
298
|
+
if not agent_manager.auth_mechanism:
|
299
|
+
agent_manager.auth_mechanism = self.config.auth_mechanism
|
300
|
+
agent_manager.logger.debug(f"Injected default auth mechanism: {agent_manager.auth_mechanism}")
|
301
|
+
|
302
|
+
if isinstance(self.config, HybridGameRunnerConfig):
|
303
|
+
agent_manager.continuous_phases = set(self.config.continuous_phases)
|
304
|
+
agent_manager.min_action_delay = self.config.min_action_delay
|
305
|
+
agent_manager.max_action_delay = self.config.max_action_delay
|
306
|
+
agent_manager.logger.debug(
|
307
|
+
f"Injected default continuous phases: {agent_manager.continuous_phases}, min action delay: {agent_manager.min_action_delay}, max action delay: {agent_manager.max_action_delay}"
|
308
|
+
)
|
309
|
+
|
310
|
+
def _inject_agent_logger(self, agent_manager: PhaseManager, agent_id: int) -> None:
|
311
|
+
"""
|
312
|
+
Inject a logger into an agent manager.
|
313
|
+
|
314
|
+
Args:
|
315
|
+
agent_manager (PhaseManager): Agent manager to inject logger into
|
316
|
+
agent_id (int): Agent identifier
|
317
|
+
"""
|
318
|
+
agent_logger = self.get_agent_logger(agent_id, self.config.game_id)
|
319
|
+
ctx_agent_id.set(str(agent_id)) # Convert int to str for context variable
|
320
|
+
|
321
|
+
agent_manager.logger = agent_logger
|
322
|
+
|
323
|
+
async def spawn_agent(self, agent_manager: PhaseManager, agent_id: int) -> None:
|
324
|
+
"""
|
325
|
+
Spawn an agent and connect it to the game.
|
326
|
+
|
327
|
+
Args:
|
328
|
+
agent_manager (PhaseManager): Agent manager to spawn
|
329
|
+
agent_id (int): Agent identifier
|
330
|
+
"""
|
331
|
+
try:
|
332
|
+
self._inject_agent_logger(agent_manager, agent_id)
|
333
|
+
self._inject_default_config(agent_manager)
|
334
|
+
|
335
|
+
agent_manager.logger.info(f"Connecting to WebSocket URL: {agent_manager.url}")
|
336
|
+
await agent_manager.start()
|
337
|
+
except Exception:
|
338
|
+
agent_manager.logger.exception(f"Error in simulation for Agent {agent_id}")
|
339
|
+
raise
|
340
|
+
|
341
|
+
async def run_game(self) -> None:
|
342
|
+
"""Run a game using provided game data."""
|
343
|
+
|
344
|
+
game_logger = self.get_game_logger(self.config.game_id)
|
345
|
+
game_logger.info(f"Running game with ID: {self.config.game_id}")
|
346
|
+
|
347
|
+
try:
|
348
|
+
tasks = []
|
349
|
+
game_logger.info("Starting simulations")
|
350
|
+
|
351
|
+
for i, agent_manager in enumerate(self.agents, start=1):
|
352
|
+
tasks.append(self.spawn_agent(agent_manager, i))
|
353
|
+
await asyncio.gather(*tasks)
|
354
|
+
except Exception as e:
|
355
|
+
game_logger.exception(f"Failed to run game: {e}")
|
356
|
+
raise
|
357
|
+
finally:
|
358
|
+
self.cleanup_logging()
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
|
5
|
+
class LoggerMixin:
|
6
|
+
"""
|
7
|
+
A mixin that provides standardized logger functionality.
|
8
|
+
|
9
|
+
This mixin makes logger initialization optional and provides:
|
10
|
+
- A default logger if none is set
|
11
|
+
- Property getter/setter for logger management
|
12
|
+
- Consistent logging interface across components
|
13
|
+
"""
|
14
|
+
|
15
|
+
_logger: Optional[logging.Logger] = None
|
16
|
+
|
17
|
+
@property
|
18
|
+
def logger(self) -> logging.Logger:
|
19
|
+
"""
|
20
|
+
Get the logger instance. Creates a default logger if none is set.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
logging.Logger: The logger instance
|
24
|
+
"""
|
25
|
+
if self._logger is None:
|
26
|
+
# Create a default logger with the class name
|
27
|
+
self._logger = logging.getLogger(self.__class__.__name__)
|
28
|
+
# Add basic handler if no handlers present
|
29
|
+
if not self._logger.handlers:
|
30
|
+
handler = logging.StreamHandler()
|
31
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
32
|
+
handler.setFormatter(formatter)
|
33
|
+
self._logger.addHandler(handler)
|
34
|
+
self._logger.setLevel(logging.INFO)
|
35
|
+
return self._logger
|
36
|
+
|
37
|
+
@logger.setter
|
38
|
+
def logger(self, logger: logging.Logger) -> None:
|
39
|
+
"""
|
40
|
+
Set a custom logger.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
logger: The logger instance to use
|
44
|
+
"""
|
45
|
+
self._logger = logger
|
@@ -0,0 +1,5 @@
|
|
1
|
+
from econagents.core.manager.base import AgentManager
|
2
|
+
from econagents.core.manager.phase import TurnBasedPhaseManager, PhaseManager
|
3
|
+
from econagents.core.manager.phase import HybridPhaseManager
|
4
|
+
|
5
|
+
__all__ = ["AgentManager", "PhaseManager", "TurnBasedPhaseManager", "HybridPhaseManager"]
|