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,222 @@
|
|
1
|
+
from typing import Any, Callable, Optional, Protocol, Type, TypeVar
|
2
|
+
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
4
|
+
|
5
|
+
from econagents.core.events import Message
|
6
|
+
from econagents.core.state.fields import EventField
|
7
|
+
|
8
|
+
EventHandler = Callable[[str, dict[str, Any]], None]
|
9
|
+
T = TypeVar("T", bound="GameState")
|
10
|
+
|
11
|
+
|
12
|
+
class PropertyMapping(BaseModel):
|
13
|
+
"""Mapping between event data and state properties
|
14
|
+
|
15
|
+
Args:
|
16
|
+
event_key: Key in the event data
|
17
|
+
state_key: Key in the state object
|
18
|
+
state_type: Whether to update private or public information ("private" or "public")
|
19
|
+
phases: Optional list of phases where this mapping should be applied. If None, applies to all phases.
|
20
|
+
exclude_phases: Optional list of phases where this mapping should not be applied.
|
21
|
+
Cannot be used together with phases.
|
22
|
+
"""
|
23
|
+
|
24
|
+
event_key: str
|
25
|
+
state_key: str
|
26
|
+
state_type: str = "private"
|
27
|
+
events: list[str] | None = None
|
28
|
+
exclude_events: list[str] | None = None
|
29
|
+
|
30
|
+
def model_post_init(self, __context: Any) -> None:
|
31
|
+
"""Validate that events and exclude_events are not both specified"""
|
32
|
+
if self.events is not None and self.exclude_events is not None:
|
33
|
+
raise ValueError("Cannot specify both events and exclude_events")
|
34
|
+
|
35
|
+
def should_apply_in_event(self, current_event: str) -> bool:
|
36
|
+
"""Determine if this mapping should be applied in the current event"""
|
37
|
+
if self.events is not None:
|
38
|
+
return current_event in self.events
|
39
|
+
if self.exclude_events is not None:
|
40
|
+
return current_event not in self.exclude_events
|
41
|
+
return True
|
42
|
+
|
43
|
+
|
44
|
+
class PrivateInformation(BaseModel):
|
45
|
+
"""Private information for each agent in the game"""
|
46
|
+
|
47
|
+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=False)
|
48
|
+
|
49
|
+
|
50
|
+
class PublicInformation(BaseModel):
|
51
|
+
"""Public information for the game"""
|
52
|
+
|
53
|
+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=False)
|
54
|
+
|
55
|
+
|
56
|
+
class MetaInformation(BaseModel):
|
57
|
+
"""Meta information for the game"""
|
58
|
+
|
59
|
+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=False)
|
60
|
+
|
61
|
+
game_id: int = EventField(default=0)
|
62
|
+
"""ID of the game"""
|
63
|
+
player_name: Optional[str] = EventField(default=None)
|
64
|
+
"""Name of the player"""
|
65
|
+
player_number: Optional[int] = EventField(default=None)
|
66
|
+
"""Number of the player"""
|
67
|
+
players: list[dict[str, Any]] = EventField(default_factory=list)
|
68
|
+
"""List of players in the game"""
|
69
|
+
phase: int = EventField(default=0)
|
70
|
+
"""Current phase of the game"""
|
71
|
+
|
72
|
+
|
73
|
+
class GameStateProtocol(Protocol):
|
74
|
+
meta: MetaInformation
|
75
|
+
private_information: PrivateInformation
|
76
|
+
public_information: PublicInformation
|
77
|
+
|
78
|
+
def model_dump(self) -> dict[str, Any]: ...
|
79
|
+
|
80
|
+
def model_dump_json(self) -> str: ...
|
81
|
+
|
82
|
+
|
83
|
+
class GameState(BaseModel):
|
84
|
+
"""Game state for a given game"""
|
85
|
+
|
86
|
+
meta: MetaInformation = EventField(default_factory=MetaInformation)
|
87
|
+
"""Meta information for the game"""
|
88
|
+
private_information: PrivateInformation = EventField(default_factory=PrivateInformation)
|
89
|
+
"""Private information for each agent in the game"""
|
90
|
+
public_information: PublicInformation = EventField(default_factory=PublicInformation)
|
91
|
+
"""Public information for the game"""
|
92
|
+
|
93
|
+
def __init__(self, **kwargs: Any):
|
94
|
+
super().__init__(**kwargs)
|
95
|
+
self._property_mappings = self._get_property_mappings()
|
96
|
+
|
97
|
+
def update(self, event: Message) -> None:
|
98
|
+
"""
|
99
|
+
Generic state update method that handles both property mappings and custom event handlers.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
event (Message): The event message containing event_type and data
|
103
|
+
|
104
|
+
This method will:
|
105
|
+
1. Check for custom event handlers first
|
106
|
+
2. Fall back to property mappings if no custom handler exists
|
107
|
+
3. Update state based on property mappings, considering phase restrictions
|
108
|
+
"""
|
109
|
+
# Get custom event handlers from child class
|
110
|
+
custom_handlers = self.get_custom_handlers()
|
111
|
+
|
112
|
+
# Check if there's a custom handler for this event type
|
113
|
+
if event.event_type in custom_handlers:
|
114
|
+
custom_handlers[event.event_type](event.event_type, event.data)
|
115
|
+
return
|
116
|
+
|
117
|
+
# Update state based on mappings
|
118
|
+
for mapping in self._property_mappings:
|
119
|
+
# Skip if mapping shouldn't be applied in current event
|
120
|
+
if not mapping.should_apply_in_event(event.event_type):
|
121
|
+
continue
|
122
|
+
|
123
|
+
# Skip if the event key isn't in the event data
|
124
|
+
if mapping.event_key not in event.data:
|
125
|
+
continue
|
126
|
+
|
127
|
+
value = event.data[mapping.event_key]
|
128
|
+
|
129
|
+
# Update the appropriate state object based on state_type
|
130
|
+
if mapping.state_type == "meta":
|
131
|
+
setattr(self.meta, mapping.state_key, value)
|
132
|
+
elif mapping.state_type == "private":
|
133
|
+
setattr(self.private_information, mapping.state_key, value)
|
134
|
+
elif mapping.state_type == "public":
|
135
|
+
setattr(self.public_information, mapping.state_key, value)
|
136
|
+
|
137
|
+
def _get_property_mappings(self) -> list[PropertyMapping]:
|
138
|
+
"""
|
139
|
+
Default implementation that generates property mappings from EventField metadata.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
list[PropertyMapping]: List of PropertyMapping objects generated from field metadata.
|
143
|
+
"""
|
144
|
+
mappings = []
|
145
|
+
|
146
|
+
# Generate mappings from meta information fields
|
147
|
+
mappings.extend(self._generate_mappings_from_model(self.meta.__class__, "meta"))
|
148
|
+
|
149
|
+
# Generate mappings from private information fields
|
150
|
+
mappings.extend(self._generate_mappings_from_model(self.private_information.__class__, "private"))
|
151
|
+
|
152
|
+
# Generate mappings from public information fields
|
153
|
+
mappings.extend(self._generate_mappings_from_model(self.public_information.__class__, "public"))
|
154
|
+
|
155
|
+
return mappings
|
156
|
+
|
157
|
+
def _generate_mappings_from_model(self, model_class: Type, state_type: str) -> list[PropertyMapping]:
|
158
|
+
"""
|
159
|
+
Generate property mappings from a Pydantic model class.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
model_class (Type): The Pydantic model class to inspect
|
163
|
+
state_type (str): The state type ("meta", "private", or "public")
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
list[PropertyMapping]: List of PropertyMapping objects
|
167
|
+
"""
|
168
|
+
mappings = []
|
169
|
+
|
170
|
+
for field_name, field_info in model_class.model_fields.items():
|
171
|
+
# Skip fields with exclude_from_mapping=True
|
172
|
+
exclude_from_mapping = False
|
173
|
+
if hasattr(field_info, "json_schema_extra") and "event_metadata" in field_info.json_schema_extra:
|
174
|
+
exclude_from_mapping = field_info.json_schema_extra["event_metadata"]["exclude_from_mapping"]
|
175
|
+
elif hasattr(field_info, "exclude_from_mapping"): # For backward compatibility
|
176
|
+
exclude_from_mapping = field_info.exclude_from_mapping
|
177
|
+
|
178
|
+
if exclude_from_mapping:
|
179
|
+
continue
|
180
|
+
|
181
|
+
# Get event key from event_key if provided, otherwise use field name
|
182
|
+
event_key = None
|
183
|
+
if hasattr(field_info, "json_schema_extra") and "event_metadata" in field_info.json_schema_extra:
|
184
|
+
event_key = field_info.json_schema_extra["event_metadata"]["event_key"]
|
185
|
+
elif hasattr(field_info, "event_key"): # For backward compatibility
|
186
|
+
event_key = field_info.event_key
|
187
|
+
|
188
|
+
if event_key is None:
|
189
|
+
event_key = field_name
|
190
|
+
|
191
|
+
# Get events and exclude_events if provided
|
192
|
+
events = None
|
193
|
+
exclude_events = None
|
194
|
+
|
195
|
+
if hasattr(field_info, "json_schema_extra") and "event_metadata" in field_info.json_schema_extra:
|
196
|
+
events = field_info.json_schema_extra["event_metadata"]["events"]
|
197
|
+
exclude_events = field_info.json_schema_extra["event_metadata"]["exclude_events"]
|
198
|
+
else:
|
199
|
+
# For backward compatibility
|
200
|
+
events = getattr(field_info, "events", None)
|
201
|
+
exclude_events = getattr(field_info, "exclude_events", None)
|
202
|
+
|
203
|
+
mappings.append(
|
204
|
+
PropertyMapping(
|
205
|
+
event_key=event_key,
|
206
|
+
state_key=field_name,
|
207
|
+
state_type=state_type,
|
208
|
+
events=events,
|
209
|
+
exclude_events=exclude_events,
|
210
|
+
)
|
211
|
+
)
|
212
|
+
|
213
|
+
return mappings
|
214
|
+
|
215
|
+
def get_custom_handlers(self) -> dict[str, EventHandler]:
|
216
|
+
"""
|
217
|
+
Override this method to provide custom event handlers.
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
dict[str, EventHandler]: A mapping of event types to handler functions.
|
221
|
+
"""
|
222
|
+
return {}
|
@@ -0,0 +1,124 @@
|
|
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)
|
@@ -0,0 +1,132 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
import asyncio
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
from typing import Any, Callable, Optional
|
6
|
+
|
7
|
+
import websockets
|
8
|
+
from websockets.asyncio.client import ClientConnection
|
9
|
+
from websockets.exceptions import ConnectionClosed
|
10
|
+
|
11
|
+
from econagents.core.logging_mixin import LoggerMixin
|
12
|
+
|
13
|
+
|
14
|
+
class AuthenticationMechanism(ABC):
|
15
|
+
"""Abstract base class for authentication mechanisms."""
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
async def authenticate(self, transport: "WebSocketTransport", **kwargs) -> bool:
|
19
|
+
"""Authenticate the transport."""
|
20
|
+
pass
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def __get_pydantic_core_schema__(cls, _source_type, _handler):
|
24
|
+
from pydantic_core import core_schema
|
25
|
+
|
26
|
+
return core_schema.is_instance_schema(AuthenticationMechanism)
|
27
|
+
|
28
|
+
|
29
|
+
class SimpleLoginPayloadAuth(AuthenticationMechanism):
|
30
|
+
"""Authentication mechanism that sends a login payload as the first message."""
|
31
|
+
|
32
|
+
async def authenticate(self, transport: "WebSocketTransport", **kwargs) -> bool:
|
33
|
+
"""Send the login payload as a JSON message."""
|
34
|
+
initial_message = json.dumps(kwargs)
|
35
|
+
await transport.send(initial_message)
|
36
|
+
return True
|
37
|
+
|
38
|
+
|
39
|
+
class WebSocketTransport(LoggerMixin):
|
40
|
+
"""
|
41
|
+
Responsible for connecting to a WebSocket, sending/receiving messages,
|
42
|
+
and reporting received messages to a callback function.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
url: str,
|
48
|
+
logger: Optional[logging.Logger] = None,
|
49
|
+
auth_mechanism: Optional[AuthenticationMechanism] = None,
|
50
|
+
auth_mechanism_kwargs: Optional[dict[str, Any]] = None,
|
51
|
+
on_message_callback: Optional[Callable[[str], Any]] = None,
|
52
|
+
):
|
53
|
+
"""
|
54
|
+
Initialize the WebSocket transport.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
url: WebSocket server URL
|
58
|
+
logger: (Optional) Logger instance
|
59
|
+
auth_mechanism: (Optional) Authentication mechanism
|
60
|
+
auth_mechanism_kwargs: (Optional) Keyword arguments to pass to auth_mechanism during authentication
|
61
|
+
on_message_callback: Callback function that receives raw message strings.
|
62
|
+
Can be synchronous or asynchronous.
|
63
|
+
"""
|
64
|
+
self.url = url
|
65
|
+
self.auth_mechanism = auth_mechanism
|
66
|
+
self.auth_mechanism_kwargs = auth_mechanism_kwargs
|
67
|
+
if logger:
|
68
|
+
self.logger = logger
|
69
|
+
self.on_message_callback = on_message_callback
|
70
|
+
self.ws: Optional[ClientConnection] = None
|
71
|
+
self._running = False
|
72
|
+
|
73
|
+
async def connect(self) -> bool:
|
74
|
+
"""Establish the WebSocket connection and authenticate."""
|
75
|
+
try:
|
76
|
+
self.ws = await websockets.connect(self.url, ping_interval=30, ping_timeout=10)
|
77
|
+
self.logger.info("WebSocketTransport: connection opened.")
|
78
|
+
|
79
|
+
# Perform authentication using the callback
|
80
|
+
if self.auth_mechanism:
|
81
|
+
if not self.auth_mechanism_kwargs:
|
82
|
+
self.auth_mechanism_kwargs = {}
|
83
|
+
auth_success = await self.auth_mechanism.authenticate(self, **self.auth_mechanism_kwargs)
|
84
|
+
if not auth_success:
|
85
|
+
self.logger.error("Authentication failed")
|
86
|
+
await self.stop()
|
87
|
+
self.ws = None # Ensure ws is set to None after stopping
|
88
|
+
return False
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
self.logger.exception(f"Transport connection error: {e}")
|
92
|
+
return False
|
93
|
+
else:
|
94
|
+
return True
|
95
|
+
|
96
|
+
async def start_listening(self):
|
97
|
+
"""Begin receiving messages in a loop."""
|
98
|
+
self._running = True
|
99
|
+
while self._running and self.ws:
|
100
|
+
try:
|
101
|
+
message_str = await self.ws.recv()
|
102
|
+
if self.on_message_callback:
|
103
|
+
# Call the callback, supporting both sync and async functions
|
104
|
+
self.logger.debug(f"<-- Transport received: {message_str}")
|
105
|
+
result = self.on_message_callback(message_str)
|
106
|
+
# If the callback is a coroutine function, await it
|
107
|
+
if asyncio.iscoroutine(result):
|
108
|
+
asyncio.create_task(result)
|
109
|
+
except ConnectionClosed:
|
110
|
+
self.logger.info("WebSocket connection closed by remote.")
|
111
|
+
break
|
112
|
+
except Exception:
|
113
|
+
self.logger.exception("Error in receive loop.")
|
114
|
+
break
|
115
|
+
self._running = False
|
116
|
+
|
117
|
+
async def send(self, message: str):
|
118
|
+
"""Send a raw string message to the WebSocket."""
|
119
|
+
if self.ws:
|
120
|
+
try:
|
121
|
+
self.logger.debug(f"--> Transport sending: {message}")
|
122
|
+
await self.ws.send(message)
|
123
|
+
except Exception:
|
124
|
+
self.logger.exception("Error sending message.")
|
125
|
+
|
126
|
+
async def stop(self):
|
127
|
+
"""Gracefully close the WebSocket connection."""
|
128
|
+
self._running = False
|
129
|
+
if self.ws:
|
130
|
+
await self.ws.close()
|
131
|
+
self.logger.info("WebSocketTransport: connection closed.")
|
132
|
+
self.ws = None # Set ws to None after closing
|
econagents/llm/openai.py
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
from typing import Any, Optional
|
2
|
+
|
3
|
+
from langsmith import traceable
|
4
|
+
from langsmith.wrappers import wrap_openai
|
5
|
+
from openai import AsyncOpenAI
|
6
|
+
|
7
|
+
|
8
|
+
class ChatOpenAI:
|
9
|
+
"""
|
10
|
+
A simple wrapper for LLM queries, e.g. using OpenAI and LangSmith.
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
model_name: str = "gpt-4o",
|
16
|
+
api_key: Optional[str] = None,
|
17
|
+
) -> None:
|
18
|
+
"""Initialize the LLM interface."""
|
19
|
+
self.model_name = model_name
|
20
|
+
self.api_key = api_key
|
21
|
+
|
22
|
+
def build_messages(self, system_prompt: str, user_prompt: str):
|
23
|
+
"""Build messages for the LLM.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
system_prompt (str): The system prompt for the LLM.
|
27
|
+
user_prompt (str): The user prompt for the LLM.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
list[dict[str, Any]]: The messages for the LLM.
|
31
|
+
"""
|
32
|
+
return [
|
33
|
+
{"role": "system", "content": system_prompt},
|
34
|
+
{"role": "user", "content": user_prompt},
|
35
|
+
]
|
36
|
+
|
37
|
+
@traceable
|
38
|
+
async def get_response(
|
39
|
+
self,
|
40
|
+
messages: list[dict[str, Any]],
|
41
|
+
tracing_extra: dict[str, Any],
|
42
|
+
**kwargs: Any,
|
43
|
+
):
|
44
|
+
"""Get a response from the LLM.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
messages (list[dict[str, Any]]): The messages for the LLM.
|
48
|
+
tracing_extra (dict[str, Any]): The extra tracing information.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: The response from the LLM.
|
52
|
+
"""
|
53
|
+
client = wrap_openai(AsyncOpenAI(api_key=self.api_key))
|
54
|
+
response = await client.chat.completions.create(
|
55
|
+
messages=messages, # type: ignore
|
56
|
+
model=self.model_name,
|
57
|
+
response_format={"type": "json_object"},
|
58
|
+
langsmith_extra=tracing_extra,
|
59
|
+
**kwargs,
|
60
|
+
)
|
61
|
+
return response.choices[0].message.content
|
econagents/py.typed
ADDED
File without changes
|
@@ -0,0 +1,90 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: econagents
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary:
|
5
|
+
License: Apache-2.0
|
6
|
+
Author: Dylan
|
7
|
+
Requires-Python: >=3.10,<3.13
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Requires-Dist: langsmith (>=0.3.13,<0.4.0)
|
14
|
+
Requires-Dist: numpy (>=2.2.3,<3.0.0)
|
15
|
+
Requires-Dist: openai (>=1.65.5,<2.0.0)
|
16
|
+
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
|
17
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
18
|
+
Requires-Dist: typing-extensions (>=4.12.2,<5.0.0)
|
19
|
+
Requires-Dist: websockets (>=15.0,<16.0)
|
20
|
+
Project-URL: Homepage, https://github.com/iwanalabs/econagents
|
21
|
+
Project-URL: Repository, https://github.com/iwanalabs/econagents
|
22
|
+
Description-Content-Type: text/markdown
|
23
|
+
|
24
|
+
<div align="center">
|
25
|
+
<img src="https://raw.githubusercontent.com/iwanalabs/econagents/main/assets/logo_200w.png">
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div align="center">
|
29
|
+
|
30
|
+

|
31
|
+
[](https://pypi.python.org/pypi/econagents)
|
32
|
+
[](https://github.com/iwanalabs/econagents/actions?query=workflow%3Atests)
|
33
|
+
[](https://econagents.readthedocs.io/en/latest/?badge=latest)
|
34
|
+
|
35
|
+
</div>
|
36
|
+
|
37
|
+
---
|
38
|
+
|
39
|
+
# econagents
|
40
|
+
|
41
|
+
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.
|
42
|
+
|
43
|
+
## Installation
|
44
|
+
|
45
|
+
```shell
|
46
|
+
# Install from PyPI
|
47
|
+
pip install econagents
|
48
|
+
|
49
|
+
# Or install directly from GitHub
|
50
|
+
pip install git+https://github.com/iwanalabs/econagents.git
|
51
|
+
```
|
52
|
+
|
53
|
+
## Framework Components
|
54
|
+
|
55
|
+
econagents consists of four key components:
|
56
|
+
|
57
|
+
1. **Agent Roles**: Define player roles with customizable behaviors using a flexible prompt system.
|
58
|
+
2. **Game State**: Hierarchical state management with automatic event-driven updates.
|
59
|
+
3. **Agent Managers**: Manage agent connections to game servers and handle event processing.
|
60
|
+
4. **Game Runner**: Orchestrates experiments by gluing together the other components.
|
61
|
+
|
62
|
+
## Example Experiments
|
63
|
+
|
64
|
+
The repository includes two example experiments:
|
65
|
+
|
66
|
+
1. **`prisoner`**: An iterated Prisoner's Dilemma game with 5 rounds and 2 LLM agents.
|
67
|
+
2. **`tudeflt/harberger`**: A Harberger Tax simulation with LLM agents.
|
68
|
+
3. **`tudeflt/futarchy`**: A Futarchy simulation with LLM agents.
|
69
|
+
|
70
|
+
### Running the Prisoner's Dilemma Experiment
|
71
|
+
|
72
|
+
```shell
|
73
|
+
# Run the server
|
74
|
+
python examples/server/prisoner/server.py
|
75
|
+
|
76
|
+
# Run the experiment (in a separate terminal)
|
77
|
+
python examples/prisoner/run_game.py
|
78
|
+
```
|
79
|
+
|
80
|
+
## Key Features
|
81
|
+
|
82
|
+
- **Flexible Agent Customization**: Customize agent behavior with Jinja templates or custom Python methods
|
83
|
+
- **Event-Driven State Management**: Automatically update game state based on server events
|
84
|
+
- **Turn-Based and Continuous Action Support**: Handle both turn-based games and continuous action phases
|
85
|
+
- **LangChain Integration**: Built-in support for LangChain's agent capabilities
|
86
|
+
|
87
|
+
## Documentation
|
88
|
+
|
89
|
+
For detailed guides and API reference, visit [the documentation](https://econagents.readthedocs.io/en/latest/).
|
90
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
econagents/__init__.py,sha256=7oAI7W8akjmKDPArgmCjA7SDpWqvI7eLPbGeBRTiEzc,1050
|
2
|
+
econagents/_c_extension.pyi,sha256=evVvDNUCGqyMPrNViPF7QXfGUNNIMbUdY5HemRNQ1_o,113
|
3
|
+
econagents/core/__init__.py,sha256=QZoOp6n5CX1j-Ob6PZgyCNY78vi2kWmd_LVLrJUj1TU,393
|
4
|
+
econagents/core/agent_role.py,sha256=viT7V6U9AmwDplgr4xGIVQvm3vLMtIFpGaMcGKM4oWE,15216
|
5
|
+
econagents/core/events.py,sha256=hx-Ru_NoSISuN--7ZFC3CIql5hry3AATSnHZJJv3Kds,294
|
6
|
+
econagents/core/game_runner.py,sha256=CToHQWIWQ1dwFLWRBxgFGlNecOjtlQUx3YbuS4cfOFQ,12869
|
7
|
+
econagents/core/logging_mixin.py,sha256=tYsRc5ngW-hzfElrb838KO-9-BGOPyUv2v5LLuJToBE,1421
|
8
|
+
econagents/core/manager/__init__.py,sha256=bDpCQlFcw_E-js575X3Xl6iwZ1uILC18An1vt6oE7S4,284
|
9
|
+
econagents/core/manager/base.py,sha256=IMGkyCrghHlkJnkQLGUVfUSSr7sqZsKHYsmgf4UtGlI,16186
|
10
|
+
econagents/core/manager/phase.py,sha256=M7s7jyA99BESXC1V9VNBuAOcvVncGoN5lF1nK8dh0Eo,19632
|
11
|
+
econagents/core/state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
econagents/core/state/fields.py,sha256=YxVOqdriaHRHoyeXsIB8ZDHygneMJD1OikOyeILK_oA,1854
|
13
|
+
econagents/core/state/game.py,sha256=Ux0s7WhOxu0aFvwgX_LM0Aiho-aa1N3yh1ManwWBRP4,8681
|
14
|
+
econagents/core/state/market.py,sha256=Jg-X9mYH6B3cYOwxzjFDV5PDbCIYxipx2UN4ecfyyDE,3909
|
15
|
+
econagents/core/transport.py,sha256=7eq31nb2KY67RuL5i2kxJrcGtwfcVm5qy0eVj4_xWQw,5063
|
16
|
+
econagents/llm/__init__.py,sha256=-tgv6qf77EdceWENIX6pDWXxu2AumhuUCjLiv4FmGKk,82
|
17
|
+
econagents/llm/openai.py,sha256=1w8nHr8Ge2aEzO8lEsxKO3tUgLPEaGhYLwYu12653GY,1782
|
18
|
+
econagents/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
econagents-0.0.1.dist-info/METADATA,sha256=N9v2yhoCGJ_2JdYq-lZQzoE7QKtMHqDamEDVAQVjAno,3431
|
20
|
+
econagents-0.0.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
21
|
+
econagents-0.0.1.dist-info/RECORD,,
|