econagents 0.0.8__tar.gz → 0.0.10__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.
Files changed (30) hide show
  1. {econagents-0.0.8 → econagents-0.0.10}/PKG-INFO +1 -1
  2. {econagents-0.0.8 → econagents-0.0.10}/econagents/__init__.py +1 -1
  3. {econagents-0.0.8 → econagents-0.0.10}/econagents/config_parser/__init__.py +0 -3
  4. {econagents-0.0.8 → econagents-0.0.10}/econagents/config_parser/base.py +56 -21
  5. {econagents-0.0.8 → econagents-0.0.10}/econagents/llm/base.py +0 -1
  6. {econagents-0.0.8 → econagents-0.0.10}/econagents/llm/ollama.py +9 -4
  7. {econagents-0.0.8 → econagents-0.0.10}/econagents/llm/openai.py +9 -4
  8. {econagents-0.0.8 → econagents-0.0.10}/pyproject.toml +6 -6
  9. econagents-0.0.8/econagents/config_parser/ibex_tudelft.py +0 -243
  10. econagents-0.0.8/econagents/core/state/market.py +0 -124
  11. {econagents-0.0.8 → econagents-0.0.10}/LICENSE +0 -0
  12. {econagents-0.0.8 → econagents-0.0.10}/README.md +0 -0
  13. {econagents-0.0.8 → econagents-0.0.10}/econagents/_c_extension.pyi +0 -0
  14. {econagents-0.0.8 → econagents-0.0.10}/econagents/cli.py +0 -0
  15. {econagents-0.0.8 → econagents-0.0.10}/econagents/config_parser/basic.py +0 -0
  16. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/__init__.py +0 -0
  17. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/agent_role.py +0 -0
  18. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/events.py +0 -0
  19. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/game_runner.py +0 -0
  20. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/logging_mixin.py +0 -0
  21. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/manager/__init__.py +0 -0
  22. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/manager/base.py +0 -0
  23. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/manager/phase.py +0 -0
  24. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/state/__init__.py +0 -0
  25. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/state/fields.py +0 -0
  26. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/state/game.py +0 -0
  27. {econagents-0.0.8 → econagents-0.0.10}/econagents/core/transport.py +0 -0
  28. {econagents-0.0.8 → econagents-0.0.10}/econagents/llm/__init__.py +0 -0
  29. {econagents-0.0.8 → econagents-0.0.10}/econagents/llm/observability.py +0 -0
  30. {econagents-0.0.8 → econagents-0.0.10}/econagents/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: econagents
3
- Version: 0.0.8
3
+ Version: 0.0.10
4
4
  Summary:
5
5
  License: MIT
6
6
  Author: Dylan Castillo
@@ -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.8"
15
+ __version__ = "0.0.10"
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 GameRunner, GameRunnerConfig, HybridGameRunnerConfig, TurnBasedGameRunnerConfig
12
- from econagents.core.manager.phase import PhaseManager, TurnBasedPhaseManager, HybridPhaseManager
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 GameState, MetaInformation, PrivateInformation, PublicInformation
15
- from econagents.core.state.market import MarketState
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", (AgentRole,), {"role": self.role_id, "name": self.name, "llm": llm_instance}
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, "MarketState": MarketState}
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
- try:
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(field.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, game_id: int, state: GameState, agent_role: Optional[AgentRole], auth_kwargs: Dict[str, Any]
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 = f"{role.name.lower()}_{base_type}_phase_{phase}.jinja2"
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(self, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
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 = {role_config.role_id: role_config for role_config in self.agent_roles}
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 = {agent_map.id: agent_map.role_id for agent_map in self.agents}
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(f"No agent role configuration found for role_id {role_id}")
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, game_id: int, state: GameState, agent_role: Optional[AgentRole], auth_kwargs: Dict[str, Any]
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(self, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
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(yaml_path: Path, login_payloads: List[Dict[str, Any]], game_id: int) -> None:
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)
@@ -47,7 +47,6 @@ class BaseLLM(ABC):
47
47
  self,
48
48
  messages: list[dict[str, Any]],
49
49
  tracing_extra: dict[str, Any],
50
- **kwargs: Any,
51
50
  ) -> str:
52
51
  """Get a response from the LLM.
53
52
 
@@ -15,6 +15,7 @@ class ChatOllama(BaseLLM):
15
15
  self,
16
16
  model_name: str,
17
17
  host: Optional[str] = None,
18
+ response_kwargs: Optional[dict[str, Any]] = None,
18
19
  ) -> None:
19
20
  """Initialize the Ollama LLM interface.
20
21
 
@@ -25,17 +26,19 @@ class ChatOllama(BaseLLM):
25
26
  self._check_ollama_available()
26
27
  self.model_name = model_name
27
28
  self.host = host
29
+ self._response_kwargs = response_kwargs or {}
28
30
 
29
31
  def _check_ollama_available(self) -> None:
30
32
  """Check if Ollama is available."""
31
33
  if not importlib.util.find_spec("ollama"):
32
- raise ImportError("Ollama is not installed. Install it with: pip install econagents[ollama]")
34
+ raise ImportError(
35
+ "Ollama is not installed. Install it with: pip install econagents[ollama]"
36
+ )
33
37
 
34
38
  async def get_response(
35
39
  self,
36
40
  messages: List[Dict[str, Any]],
37
41
  tracing_extra: Dict[str, Any],
38
- **kwargs: Any,
39
42
  ) -> str:
40
43
  """Get a response from the LLM.
41
44
 
@@ -58,7 +61,7 @@ class ChatOllama(BaseLLM):
58
61
  response = await client.chat(
59
62
  model=self.model_name,
60
63
  messages=messages,
61
- **kwargs,
64
+ **(self._response_kwargs),
62
65
  )
63
66
 
64
67
  # End the LLM run
@@ -74,4 +77,6 @@ class ChatOllama(BaseLLM):
74
77
 
75
78
  except ImportError as e:
76
79
  logger.error(f"Failed to import Ollama: {e}")
77
- raise ImportError("Ollama is not installed. Install it with: pip install econagents[ollama]") from e
80
+ raise ImportError(
81
+ "Ollama is not installed. Install it with: pip install econagents[ollama]"
82
+ ) from e
@@ -14,6 +14,7 @@ class ChatOpenAI(BaseLLM):
14
14
  self,
15
15
  model_name: str = "gpt-4o",
16
16
  api_key: Optional[str] = None,
17
+ response_kwargs: Optional[dict[str, Any]] = None,
17
18
  ) -> None:
18
19
  """Initialize the OpenAI LLM interface.
19
20
 
@@ -24,17 +25,19 @@ class ChatOpenAI(BaseLLM):
24
25
  self.model_name = model_name
25
26
  self.api_key = api_key
26
27
  self._check_openai_available()
28
+ self._response_kwargs = response_kwargs or {}
27
29
 
28
30
  def _check_openai_available(self) -> None:
29
31
  """Check if OpenAI is available."""
30
32
  if not importlib.util.find_spec("openai"):
31
- raise ImportError("OpenAI is not installed. Install it with: pip install econagents[openai]")
33
+ raise ImportError(
34
+ "OpenAI is not installed. Install it with: pip install econagents[openai]"
35
+ )
32
36
 
33
37
  async def get_response(
34
38
  self,
35
39
  messages: list[dict[str, Any]],
36
40
  tracing_extra: dict[str, Any],
37
- **kwargs: Any,
38
41
  ) -> str:
39
42
  """Get a response from the LLM.
40
43
 
@@ -59,7 +62,7 @@ class ChatOpenAI(BaseLLM):
59
62
  model=self.model_name,
60
63
  messages=messages, # type: ignore
61
64
  response_format={"type": "json_object"},
62
- **kwargs,
65
+ **(self._response_kwargs),
63
66
  )
64
67
 
65
68
  # Track the LLM call using the observability provider
@@ -74,4 +77,6 @@ class ChatOpenAI(BaseLLM):
74
77
  return response.choices[0].message.content
75
78
  except ImportError as e:
76
79
  logger.error(f"Failed to import OpenAI: {e}")
77
- raise ImportError("OpenAI is not installed. Install it with: pip install econagents[openai]") from e
80
+ raise ImportError(
81
+ "OpenAI is not installed. Install it with: pip install econagents[openai]"
82
+ ) from e
@@ -27,7 +27,7 @@ dependencies = [
27
27
 
28
28
  [tool.poetry]
29
29
  name = "econagents"
30
- version = "0.0.8" # Do not change, let poetry-dynamic-versioning handle it.
30
+ version = "0.0.10" # Do not change, let poetry-dynamic-versioning handle it.
31
31
  packages = [{include = "econagents"}]
32
32
  include = ["econagents/*.so", "econagents/*.pyd"] # Compiled extensions
33
33
  license = "MIT"
@@ -62,13 +62,13 @@ pre_commit = ">=2.16.0"
62
62
  pytest = ">=7.1.2"
63
63
  pytest-cov = ">=3.0.0"
64
64
  pytest-mock = ">=3.7.0"
65
- python-dotenv = "^1.0.1"
65
+ python-dotenv = "^1.1.1"
66
66
  jupyter = "^1.1.1"
67
67
  nest-asyncio = "^1.6.0"
68
- ruff = "^0.11.13"
69
- types-requests = "^2.32.4.20250611"
70
- pytest-asyncio = "^0.26.0"
71
- types-pyyaml = "^6.0.12.20250516"
68
+ ruff = "^0.12.12"
69
+ types-requests = "^2.32.4.20250809"
70
+ pytest-asyncio = "^1.1.0"
71
+ types-pyyaml = "^6.0.12.20250822"
72
72
 
73
73
  [tool.poetry.group.debug]
74
74
  optional = true
@@ -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