econagents 0.0.2__tar.gz → 0.0.5__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 (25) hide show
  1. {econagents-0.0.2 → econagents-0.0.5}/PKG-INFO +14 -27
  2. {econagents-0.0.2 → econagents-0.0.5}/econagents/__init__.py +1 -1
  3. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/agent_role.py +13 -4
  4. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/game_runner.py +11 -2
  5. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/manager/base.py +64 -3
  6. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/manager/phase.py +2 -2
  7. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/state/game.py +23 -1
  8. {econagents-0.0.2 → econagents-0.0.5}/pyproject.toml +13 -31
  9. {econagents-0.0.2 → econagents-0.0.5}/LICENSE +0 -0
  10. {econagents-0.0.2 → econagents-0.0.5}/README.md +0 -0
  11. {econagents-0.0.2 → econagents-0.0.5}/econagents/_c_extension.pyi +0 -0
  12. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/__init__.py +0 -0
  13. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/events.py +0 -0
  14. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/logging_mixin.py +0 -0
  15. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/manager/__init__.py +0 -0
  16. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/state/__init__.py +0 -0
  17. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/state/fields.py +0 -0
  18. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/state/market.py +0 -0
  19. {econagents-0.0.2 → econagents-0.0.5}/econagents/core/transport.py +0 -0
  20. {econagents-0.0.2 → econagents-0.0.5}/econagents/llm/__init__.py +0 -0
  21. {econagents-0.0.2 → econagents-0.0.5}/econagents/llm/base.py +0 -0
  22. {econagents-0.0.2 → econagents-0.0.5}/econagents/llm/observability.py +0 -0
  23. {econagents-0.0.2 → econagents-0.0.5}/econagents/llm/ollama.py +0 -0
  24. {econagents-0.0.2 → econagents-0.0.5}/econagents/llm/openai.py +0 -0
  25. {econagents-0.0.2 → econagents-0.0.5}/econagents/py.typed +0 -0
@@ -1,41 +1,28 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: econagents
3
- Version: 0.0.2
4
- Summary: econagents is a Python library that lets you use LLM agents in economic experiments. The framework connects LLM agents to game servers through WebSockets and provides a flexible architecture for designing, customizing, and running economic simulations.
5
- License: MIT License
6
-
7
- Copyright (c) Delft University of Technology
8
-
9
- Permission is hereby granted, free of charge, to any person obtaining a copy
10
- of this software and associated documentation files (the "Software"), to deal
11
- in the Software without restriction, including without limitation the rights
12
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
- copies of the Software, and to permit persons to whom the Software is
14
- furnished to do so, subject to the following conditions:
15
-
16
- The above copyright notice and this permission notice shall be included in all
17
- copies or substantial portions of the Software.
18
-
19
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
- SOFTWARE.
26
- Requires-Python: >=3.10,<3.13
27
- Classifier: License :: Other/Proprietary License
3
+ Version: 0.0.5
4
+ Summary:
5
+ License: MIT
6
+ Author: Dylan Castillo
7
+ Author-email: dylan@iwanalabs.com
8
+ Requires-Python: >=3.10,<4
9
+ Classifier: License :: OSI Approved :: MIT License
28
10
  Classifier: Programming Language :: Python :: 3
29
11
  Classifier: Programming Language :: Python :: 3.10
30
12
  Classifier: Programming Language :: Python :: 3.11
31
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
32
15
  Provides-Extra: all
33
- Provides-Extra: default
34
16
  Provides-Extra: langfuse
35
17
  Provides-Extra: langsmith
36
18
  Provides-Extra: ollama
37
19
  Provides-Extra: openai
38
- Requires-Dist: pydantic (>=2.10.6,<3.0.0)
20
+ Provides-Extra: standard
21
+ Requires-Dist: langfuse (>=2.60.3,<3.0.0) ; extra == "langfuse" or extra == "all"
22
+ Requires-Dist: langsmith (>=0.3.33,<0.4.0) ; extra == "langsmith" or extra == "standard" or extra == "all"
23
+ Requires-Dist: ollama (>=0.4.8,<0.5.0) ; extra == "ollama" or extra == "all"
24
+ Requires-Dist: openai (>=1.68.2,<2.0.0) ; extra == "openai" or extra == "standard" or extra == "all"
25
+ Requires-Dist: pydantic (>=2.11.3,<3.0.0)
39
26
  Requires-Dist: requests (>=2.32.3,<3.0.0)
40
27
  Requires-Dist: websockets (>=15.0,<16.0)
41
28
  Description-Content-Type: text/markdown
@@ -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.2"
15
+ __version__ = "0.0.5"
16
16
 
17
17
  __all__: list[str] = [
18
18
  "AgentRole",
@@ -3,6 +3,7 @@ import logging
3
3
  import re
4
4
  from abc import ABC
5
5
  from pathlib import Path
6
+ from jinja2 import FileSystemLoader
6
7
  from typing import Any, Callable, ClassVar, Dict, Generic, Literal, Optional, Pattern, Protocol, TypeVar
7
8
 
8
9
  from jinja2.sandbox import SandboxedEnvironment
@@ -129,12 +130,20 @@ class AgentRole(ABC, Generic[StateT_contra], LoggerMixin):
129
130
  Raises:
130
131
  FileNotFoundError: If no matching prompt template is found
131
132
  """
133
+ # Initialize Jinja environment with a file system loader
134
+ env = SandboxedEnvironment(loader=FileSystemLoader(prompts_path))
135
+
132
136
  # Try role-specific prompt first, then fall back to 'all'
133
137
  for role in [self.name, "all"]:
134
- if prompt_file := self._resolve_prompt_file(prompt_type, phase, role, prompts_path):
135
- with prompt_file.open() as f:
136
- template = SandboxedEnvironment().from_string(f.read())
137
- return template.render(**context)
138
+ if prompt_file_path := self._resolve_prompt_file(prompt_type, phase, role, prompts_path):
139
+ # Get filename relative to the prompts_path for the loader
140
+ template_filename = str(prompt_file_path.relative_to(prompts_path))
141
+ try:
142
+ template = env.get_template(template_filename)
143
+ return template.render(**context)
144
+ except Exception as e: # Catch potential Jinja errors during loading/rendering
145
+ self.logger.error(f"Error loading/rendering template {template_filename}: {e}")
146
+ raise # Re-raise after logging
138
147
 
139
148
  raise FileNotFoundError(
140
149
  f"No prompt template found for type={prompt_type}, phase={phase}, "
@@ -63,6 +63,10 @@ class GameRunnerConfig(BaseModel):
63
63
  observability_provider: Optional[Literal["langsmith", "langfuse"]] = None
64
64
  """Name of the observability provider to use. Options: 'langsmith' or 'langfuse'"""
65
65
 
66
+ # Agent stop configuration
67
+ end_game_event: str = "game-over"
68
+ """Event type that signals the end of the game and should stop the agent."""
69
+
66
70
 
67
71
  class TurnBasedGameRunnerConfig(GameRunnerConfig):
68
72
  """Configuration class for TurnBasedGameRunner."""
@@ -303,6 +307,10 @@ class GameRunner:
303
307
  agent_manager.auth_mechanism = self.config.auth_mechanism
304
308
  agent_manager.logger.debug(f"Injected default auth mechanism: {agent_manager.auth_mechanism}")
305
309
 
310
+ if agent_manager.end_game_event_type != self.config.end_game_event:
311
+ agent_manager.end_game_event_type = self.config.end_game_event
312
+ agent_manager.logger.debug(f"Injected default end game event: {agent_manager.end_game_event_type}")
313
+
306
314
  if agent_manager.llm_provider and self.config.observability_provider:
307
315
  try:
308
316
  provider = get_observability_provider(self.config.observability_provider)
@@ -349,7 +357,7 @@ class GameRunner:
349
357
  agent_manager.logger.info(f"Connecting to WebSocket URL: {agent_manager.url}")
350
358
  await agent_manager.start()
351
359
  except Exception:
352
- agent_manager.logger.exception(f"Error in simulation for Agent {agent_id}")
360
+ agent_manager.logger.exception(f"Error in game for Agent {agent_id}")
353
361
  raise
354
362
 
355
363
  async def run_game(self) -> None:
@@ -360,7 +368,7 @@ class GameRunner:
360
368
 
361
369
  try:
362
370
  tasks = []
363
- game_logger.info("Starting simulations")
371
+ game_logger.info("Starting game")
364
372
 
365
373
  for i, agent_manager in enumerate(self.agents, start=1):
366
374
  tasks.append(self.spawn_agent(agent_manager, i))
@@ -369,4 +377,5 @@ class GameRunner:
369
377
  game_logger.exception(f"Failed to run game: {e}")
370
378
  raise
371
379
  finally:
380
+ game_logger.info("Game over")
372
381
  self.cleanup_logging()
@@ -68,6 +68,10 @@ class AgentManager(LoggerMixin):
68
68
  self._global_pre_event_hooks: list[Callable[[Message], Any]] = []
69
69
  self._global_post_event_hooks: list[Callable[[Message], Any]] = []
70
70
 
71
+ # Default event type to trigger stopping the agent
72
+ self._end_game_event_type: str = "game-over"
73
+ self.register_event_handler(self._end_game_event_type, self._handle_end_game)
74
+
71
75
  # Initialize transport if URL is provided
72
76
  if url:
73
77
  self._initialize_transport()
@@ -156,6 +160,30 @@ class AgentManager(LoggerMixin):
156
160
  return None
157
161
  return Message(message_type=message_type, event_type=event_type, data=data)
158
162
 
163
+ @property
164
+ def end_game_event_type(self) -> str:
165
+ """Get the event type that triggers the agent to stop."""
166
+ return self._end_game_event_type
167
+
168
+ @end_game_event_type.setter
169
+ def end_game_event_type(self, value: str):
170
+ """
171
+ Set the event type that triggers the agent to stop.
172
+
173
+ Unregisters the stop handler from the old event type and registers it
174
+ for the new event type.
175
+
176
+ Args:
177
+ value (str): The new event type to listen for.
178
+ """
179
+ if self._end_game_event_type != value:
180
+ # Unregister from the old event type
181
+ self.unregister_event_handler(self._end_game_event_type, self._handle_end_game)
182
+ # Register for the new event type
183
+ self._end_game_event_type = value
184
+ self.register_event_handler(self._end_game_event_type, self._handle_end_game)
185
+ self.logger.info(f"End game event type set to: {value}")
186
+
159
187
  async def on_message(self, message: Message):
160
188
  """
161
189
  Default implementation to handle incoming messages from the server.
@@ -202,6 +230,7 @@ class AgentManager(LoggerMixin):
202
230
  self.running = False
203
231
  if self.transport:
204
232
  await self.transport.stop()
233
+ self.logger.info("Agent manager stopped and connection closed.")
205
234
 
206
235
  async def on_event(self, message: Message):
207
236
  """
@@ -270,6 +299,16 @@ class AgentManager(LoggerMixin):
270
299
  if hasattr(result, "__await__"):
271
300
  await result
272
301
 
302
+ async def _handle_end_game(self, message: Message):
303
+ """
304
+ Default handler for the 'end_game_event_type'. Stops the agent manager.
305
+
306
+ Args:
307
+ message (Message): The event message triggering the end game.
308
+ """
309
+ self.logger.info(f"Received end game event ({message.event_type}). Stopping agent manager...")
310
+ await self.stop()
311
+
273
312
  # Event handler registration
274
313
  def register_event_handler(self, event_type: str, handler: Callable[[Message], Any]):
275
314
  """
@@ -355,7 +394,17 @@ class AgentManager(LoggerMixin):
355
394
  if handler is None:
356
395
  self._event_handlers.pop(event_type)
357
396
  else:
358
- self._event_handlers[event_type] = [h for h in self._event_handlers[event_type] if h != handler]
397
+ # Use list comprehension to avoid modifying list while iterating
398
+ handlers_to_keep = [h for h in self._event_handlers[event_type] if h != handler]
399
+ if not handlers_to_keep:
400
+ # Remove the key if the list becomes empty
401
+ self._event_handlers.pop(event_type, None)
402
+ else:
403
+ self._event_handlers[event_type] = handlers_to_keep
404
+ # Explicitly handle removal of the default end game handler
405
+ if event_type == self._end_game_event_type and handler == self._handle_end_game:
406
+ self.logger.warning(f"Default end game handler for '{event_type}' unregistered.")
407
+
359
408
  return self
360
409
 
361
410
  def unregister_global_event_handler(self, handler: Optional[Callable] = None):
@@ -384,7 +433,13 @@ class AgentManager(LoggerMixin):
384
433
  if hook is None:
385
434
  self._pre_event_hooks.pop(event_type)
386
435
  else:
387
- self._pre_event_hooks[event_type] = [h for h in self._pre_event_hooks[event_type] if h != hook]
436
+ # Use list comprehension to avoid modifying list while iterating
437
+ hooks_to_keep = [h for h in self._pre_event_hooks[event_type] if h != hook]
438
+ if not hooks_to_keep:
439
+ # Remove the key if the list becomes empty
440
+ self._pre_event_hooks.pop(event_type, None)
441
+ else:
442
+ self._pre_event_hooks[event_type] = hooks_to_keep
388
443
  return self
389
444
 
390
445
  def unregister_global_pre_event_hook(self, hook: Optional[Callable] = None):
@@ -413,7 +468,13 @@ class AgentManager(LoggerMixin):
413
468
  if hook is None:
414
469
  self._post_event_hooks.pop(event_type)
415
470
  else:
416
- self._post_event_hooks[event_type] = [h for h in self._post_event_hooks[event_type] if h != hook]
471
+ # Use list comprehension to avoid modifying list while iterating
472
+ hooks_to_keep = [h for h in self._post_event_hooks[event_type] if h != hook]
473
+ if not hooks_to_keep:
474
+ # Remove the key if the list becomes empty
475
+ self._post_event_hooks.pop(event_type, None)
476
+ else:
477
+ self._post_event_hooks[event_type] = hooks_to_keep
417
478
  return self
418
479
 
419
480
  def unregister_global_post_event_hook(self, hook: Optional[Callable] = None):
@@ -3,14 +3,14 @@ import json
3
3
  import logging
4
4
  import random
5
5
  from abc import ABC, abstractmethod
6
- from typing import Any, Callable, Optional
7
6
  from pathlib import Path
7
+ from typing import Any, Callable, Optional
8
8
 
9
9
  from econagents.core.agent_role import AgentRole
10
10
  from econagents.core.events import Message
11
11
  from econagents.core.manager.base import AgentManager
12
12
  from econagents.core.state.game import GameState
13
- from econagents.core.transport import AuthenticationMechanism, SimpleLoginPayloadAuth
13
+ from econagents.core.transport import AuthenticationMechanism
14
14
 
15
15
 
16
16
  class PhaseManager(AgentManager, ABC):
@@ -1,4 +1,4 @@
1
- from typing import Any, Callable, Optional, Protocol, Type, TypeVar
1
+ from typing import Any, Callable, Optional, Protocol, Type, TypeVar, cast
2
2
 
3
3
  from pydantic import BaseModel, ConfigDict
4
4
 
@@ -220,3 +220,25 @@ class GameState(BaseModel):
220
220
  dict[str, EventHandler]: A mapping of event types to handler functions.
221
221
  """
222
222
  return {}
223
+
224
+ def reset(self) -> None:
225
+ """
226
+ Resets meta, private_information, and public_information
227
+ to their initial state by re-initializing them using their default factories.
228
+ This effectively removes any dynamically added attributes.
229
+ """
230
+ # Re-initialize components using their default factories from the GameState model definition
231
+ meta_field = self.__class__.model_fields["meta"]
232
+ if meta_field.default_factory:
233
+ fac = cast("Callable[[], Any]", meta_field.default_factory)
234
+ self.meta = fac()
235
+
236
+ private_field = self.__class__.model_fields["private_information"]
237
+ if private_field.default_factory:
238
+ fac = cast("Callable[[], Any]", private_field.default_factory)
239
+ self.private_information = fac()
240
+
241
+ public_field = self.__class__.model_fields["public_information"]
242
+ if public_field.default_factory:
243
+ fac = cast("Callable[[], Any]", public_field.default_factory)
244
+ self.public_information = fac()
@@ -9,8 +9,7 @@ style = "semver"
9
9
 
10
10
  [project]
11
11
  name = "econagents"
12
- license = { file = "LICENSE" }
13
- description = "econagents is a Python library that lets you use LLM agents in economic experiments. The framework connects LLM agents to game servers through WebSockets and provides a flexible architecture for designing, customizing, and running economic simulations."
12
+ license = "MIT"
14
13
  readme = "README.md"
15
14
  homepage = "https://github.com/iwanalabs/econagents"
16
15
  repository = "https://github.com/iwanalabs/econagents"
@@ -18,44 +17,27 @@ dynamic = ["version"]
18
17
 
19
18
  [tool.poetry]
20
19
  name = "econagents"
21
- version = "0.0.2" # Do not change, let poetry-dynamic-versioning handle it.
20
+ version = "0.0.5" # Do not change, let poetry-dynamic-versioning handle it.
22
21
  packages = [{include = "econagents"}]
23
22
  include = ["econagents/*.so", "econagents/*.pyd"] # Compiled extensions
23
+ license = "MIT"
24
+ description = ""
25
+ authors = ["Dylan Castillo <dylan@iwanalabs.com>"]
24
26
 
25
27
  [tool.poetry.build]
26
28
  generate-setup-file = false
27
29
 
28
30
  [tool.poetry.dependencies]
29
31
  # Be as loose as possible if writing a library.
30
- python = ">=3.10,<3.13"
31
- pydantic = "^2.10.6"
32
+ python = ">=3.10,<4"
33
+ pydantic = "^2.11.3"
32
34
  requests = "^2.32.3"
33
35
  websockets = "^15.0"
36
+ openai = {version = "^1.68.2", optional = true}
37
+ ollama = {version = "^0.4.8", optional = true}
38
+ langsmith = {version = "^0.3.33", optional = true}
39
+ langfuse = {version = "^2.60.3", optional = true}
34
40
 
35
- # Optional dependencies
36
- [tool.poetry.group.openai]
37
- optional = true
38
-
39
- [tool.poetry.group.openai.dependencies]
40
- openai = "^1.68.2"
41
-
42
- [tool.poetry.group.ollama]
43
- optional = true
44
-
45
- [tool.poetry.group.ollama.dependencies]
46
- ollama = "^0.1.9"
47
-
48
- [tool.poetry.group.langsmith]
49
- optional = true
50
-
51
- [tool.poetry.group.langsmith.dependencies]
52
- langsmith = "^0.3.19"
53
-
54
- [tool.poetry.group.langfuse]
55
- optional = true
56
-
57
- [tool.poetry.group.langfuse.dependencies]
58
- langfuse = "^2.60.2"
59
41
 
60
42
  [tool.poetry.group.docs.dependencies]
61
43
  myst-parser = {extras = ["linkify"], version = "^4.0.1"}
@@ -74,7 +56,7 @@ pytest-mock = ">=3.7.0"
74
56
  python-dotenv = "^1.0.1"
75
57
  jupyter = "^1.1.1"
76
58
  nest-asyncio = "^1.6.0"
77
- ruff = "^0.11.2"
59
+ ruff = "^0.11.6"
78
60
  types-requests = "^2.32.0.20250306"
79
61
  pytest-asyncio = "^0.25.3"
80
62
 
@@ -179,5 +161,5 @@ openai = ["openai"]
179
161
  ollama = ["ollama"]
180
162
  langsmith = ["langsmith"]
181
163
  langfuse = ["langfuse"]
182
- default = ["openai", "langsmith"]
164
+ standard = ["openai", "langsmith"]
183
165
  all = ["openai", "ollama", "langsmith", "langfuse"]
File without changes
File without changes