Skip to content

Commit 0def793

Browse files
BekabooKeyu-Heautofix-ci[bot]rcwang937XuhuiZhou
authored
Feat support private messages (#330)
* Add scenarios from NegotiationArena * [autofix.ci] apply automated fixes * Refactor negotiation arena examples and update .gitignore Refactored negotiation arena example scripts to use more explicit typing, improved agent profile retrieval logic, and removed unused OpenAI API key loading. Updated .gitignore to exclude negotiation_arena redis-data directory. * Fix Mypy type errors in negotiation arena files * Modify error handling and update readme * Add multi-agent support for 3+ agents * Add multi-agent test suite with documentation and examples * Fix unused variable in evaluators * Unify classes for all agent scenarios Remove separation between 2-agent and multi-agent cases by consolidating duplicate classes and logic paths into unified implementations * Support private messages (WIP) * Add example of multi-agent private message * Add field `to` for private messages * Improve private message example * Don't use special `private_message` action type, use `to` for all types * change made on PM framework. check out envs/parallel.py for updates * Remove manual backup of `envs/parallel.py` * Remove unused `_actions_to_natural_language` * Format with ruff * Add README for `multi_agents_private_dm` example * Fix typo in `test_rule_based_terminated_evaluator` * changes in dm * [autofix.ci] apply automated fixes * Fix typing * Remove commented code * Make mypy happy * Debug evaluator not collecting all agents' evaluation * Try fix evaluator * Make mypy happy * [autofix.ci] apply automated fixes * Update uv lock * Remove debug prints * Clean up evaluator agent counting * [autofix.ci] apply automated fixes * Format with ruff * fixup! Clean up evaluator agent counting * Remove unnecessary git ignores * Remove private inbox, handle DM in observation creation * Remove extraneous comments * Remove changes under `examples/experimental/multi_agent_tests` from Keyu * Add negotiation arena test * Remove unused method * Remove some comments to reduce diff * Reduce diffs * Simplify README * Fix breaking change in `to_natural_language()` * Fix typo * Make `to` instructions clearer in system prompt * Remove trick in evaluator * Validate `to` field * Make `to` error more detailed * Add test for private messages * Add test for invalid message recipients * Use multi-agents in private message routing test * Use pydantic field validator to check and filter `to` field in actions * Remove private message specific hints in general prompt * Fix action type in sampled actions being int instead of str literal * Clean up parallel env tests * [autofix.ci] apply automated fixes * Validate instead of filtering `to` field in agent actions * Regenerate when `to` field contains invalid names * [autofix.ci] apply automated fixes * Avoid unnecessary comment change * Add tests to ensure private message is not visible to non-recipients * Include private message `to` info in `AgentAction.to_natural_language()` * merging docs * modify pytest --------- Co-authored-by: Keyu(Frank) He <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: rcwang937 <[email protected]> Co-authored-by: Xuhui Zhou <[email protected]>
1 parent 93159fc commit 0def793

File tree

18 files changed

+1374
-129
lines changed

18 files changed

+1374
-129
lines changed

docs/pages/concepts/agents.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,29 @@
33
Agent is a concept in Sotopia to represent decision-making entities that can interact with each other in a social environment. Agents can be human participants, AI models, or other entities. No matter which type of agent, they have the same interface to interact with the environment: the input is an [`Observation`](/python_API/messages/message_classes#observation) and the output is an [`AgentAction`](/python_API/messages/message_classes#agentaction), each of which is a subclass of [`Message`](/python_API/messages/message_classes#message). You can think of the environment and the agents are sending messages to each other, while the message from the environment is the observation for the agents, and the message from each of the agents is the action that they want to take in the environment. In Sotopia, we are simulating the interaction between agents with social roles, which includes both human characters and various AI assistants, which are defined as profiles [`AgentProfile`](/python_API/database/persistant_profile#agentprofile-class).
44

55
### Actions of agents
6-
The types of action is defined by the `ActionType` type alias, which is a literal type that can only take one of the following values: `none`, `speak`, `non-verbal communication`, `action`, `leave`. An agent can choose to n perform physical actions (`action`), use language (`speak`) or gestures or facial expressions (`non-verbal communication`) to communicate, or choose to do nothing (`none`), or leave the interaction (`leave`).
6+
The types of action is defined by the `ActionType` type alias, which is a literal type that can only take one of the following values: `none`, `speak`, `non-verbal communication`, `action`, `leave`. An agent can choose to perform physical actions (`action`), use language (`speak`) or gestures or facial expressions (`non-verbal communication`) to communicate, or choose to do nothing (`none`), or leave the interaction (`leave`).
77

88
Apart from the type of action, the content of the action, e.g. the utterance, the concrete action, etc., is a free-form string in the `argument` attribute of the `AgentAction` class.
99

10+
#### Private Messages
11+
Agents can send private messages to specific recipients using the `to` field in `AgentAction`. When an action includes a `to` field with a list of recipient agent names:
12+
- The action is only visible to the sender and the specified recipients
13+
- Other agents will not see the private message in their observations
14+
- This enables private conversations, secret planning, and side-channel communication in multi-agent scenarios
15+
16+
Example:
17+
```python
18+
# Public message (visible to all)
19+
action = AgentAction(action_type="speak", argument="Hello everyone!")
20+
21+
# Private message (visible only to sender and "agent2")
22+
private_action = AgentAction(
23+
action_type="speak",
24+
argument="Psst, let's discuss this privately",
25+
to=["agent2"]
26+
)
27+
```
28+
1029
### Profiles of agents
1130
The profiles of agents are passed in as either of two argument of [the constructor of agents](/python_API/agents/base_agent_api_docs#constructor): `uuid_str` or `agent_profile`. The `uuid_str` is used together with the Redis database to retrieve an agent profile, while the `agent_profile` is a Pydantic `AgentProfile` object.
1231
We strong recommend to use `uuid_str`, as it can more easily be used with other sotopia tools.
@@ -28,5 +47,5 @@ from sotopia.messages.message_classes import AgentAction, Observation
2847

2948
class HelloWorldAgent(BaseAgent):
3049
async def aact(self, observation: Observation) -> AgentAction:
31-
return AgentAction(type="speak", argument="Hello, world!")
50+
return AgentAction(action_type="speak", argument="Hello, world!")
3251
```

docs/pages/examples/examples.mdx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,35 @@ python examples/benchmark_evaluator.py --push-to-db --model=<the model used to b
1919

2020
## Example 2: Generate script-like episodes
2121
See `docs/simulation_modes.md` for more information.
22+
23+
## Example 3: Multi-Agent Private Messages
24+
Sotopia supports private messaging between agents, allowing agents to send messages that are only visible to specific recipients. This enables private conversations, secret planning, and side-channel communication in multi-agent scenarios.
25+
26+
See `examples/experimental/multi_agents_private_dm/README.md` for more information and example scripts.
27+
28+
### Quick Example
29+
```python
30+
from sotopia.messages import AgentAction
31+
from sotopia.envs import ParallelSotopiaEnv
32+
33+
# Create actions with private messages
34+
actions = {
35+
"agent1": AgentAction(
36+
action_type="speak",
37+
argument="Psst, agent2, let's discuss this privately",
38+
to=["agent2"] # Only visible to agent1 and agent2
39+
),
40+
"agent2": AgentAction(
41+
action_type="speak",
42+
argument="Hello everyone!" # Public message, visible to all
43+
),
44+
"agent3": AgentAction(
45+
action_type="speak",
46+
argument="I'll talk to agent1",
47+
to=["agent1"] # Only visible to agent1 and agent3
48+
),
49+
}
50+
51+
# Each agent will see different observations based on message visibility
52+
observations, rewards, done, truncations, info = env.step(actions)
53+
```

docs/pages/index.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ with <Link href="https://sotopia.world/"><span className="font-display text-xl">
5151
<AccordionTrigger> Realistic social interaction </AccordionTrigger>
5252
<AccordionContent>
5353
<ul>
54-
<li>Agents can talk, do non-verbal communication and performance physical actions.</li>
54+
<li>Agents can talk, do non-verbal communication and perform physical actions.</li>
55+
<li>Agents can send private messages to specific recipients, enabling secret conversations and side-channel communication.</li>
5556
<li>Characters have different personalities, backgrounds, and relationships.</li>
5657
<li>Agents can have different goals and motivations.</li>
5758
</ul>

docs/pages/python_API/envs/parallel.md

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import random
1212
from typing import Any, Literal, Optional, Type, TypeVar
1313
from gin import configurable
1414
from gymnasium.spaces.dict import Dict
15-
from gymnasium.spaces.discrete import Discrete
1615
from gymnasium.spaces.text import Text
16+
from gymnasium.spaces import Space
1717
from pettingzoo.utils.env import ParallelEnv
1818
from redis_om.model.model import NotFoundError
1919
from sotopia.agents.llm_agent import Agents
@@ -95,10 +95,13 @@ def step(
9595
dict[str, dict[Any, Any]]
9696
]
9797
```
98-
Executes actions and returns new states.
98+
Executes actions and returns new states. Observations are filtered per-agent based on private message visibility.
9999

100100
##### Parameters
101-
- `actions` (dict[str, AgentAction] | dict[str, dict[str, int | str]]): Actions taken by agents.
101+
- `actions` (dict[str, AgentAction] | dict[str, dict[str, int | str]]): Actions taken by agents. Each action can be:
102+
- An `AgentAction` object
103+
- A dictionary with `action_type` (string literal like `"speak"`, `"none"`, etc.) and `argument` (string)
104+
- Optionally includes a `to` field (list of strings) for private messages
102105

103106
##### Returns
104107
- `tuple[
@@ -109,6 +112,11 @@ Executes actions and returns new states.
109112
dict[str, dict[Any, Any]]
110113
]`: Next state information, including observations, rewards, terminals, truncations, and additional info.
111114

115+
##### Private Message Visibility
116+
- **Public actions** (no `to` field or `to=None`): Visible to all agents in their observations
117+
- **Private actions** (with `to` field): Only visible to the sender and agents listed in `to`
118+
- Each agent receives a filtered observation containing only actions they can see
119+
112120
#### Usage Example
113121
```python
114122
next_obs, rewards, done, truncations, info = env.step(actions)
@@ -126,10 +134,13 @@ async def astep(
126134
dict[str, dict[Any, Any]]
127135
]
128136
```
129-
Asynchronous version of `step`.
137+
Asynchronous version of `step`. Observations are filtered per-agent based on private message visibility.
130138

131139
##### Parameters
132-
- `actions` (dict[str, AgentAction] | dict[str, dict[str, int | str]]): Actions taken by agents.
140+
- `actions` (dict[str, AgentAction] | dict[str, dict[str, int | str]]): Actions taken by agents. Each action can be:
141+
- An `AgentAction` object
142+
- A dictionary with `action_type` (string literal like `"speak"`, `"none"`, etc.) and `argument` (string)
143+
- Optionally includes a `to` field (list of strings) for private messages
133144

134145
##### Returns
135146
- `tuple[
@@ -140,6 +151,11 @@ Asynchronous version of `step`.
140151
dict[str, dict[Any, Any]]
141152
]`: Next state information, including observations, rewards, terminals, truncations, and additional info.
142153

154+
##### Private Message Visibility
155+
- **Public actions** (no `to` field or `to=None`): Visible to all agents in their observations
156+
- **Private actions** (with `to` field): Only visible to the sender and agents listed in `to`
157+
- Each agent receives a filtered observation containing only actions they can see
158+
143159
#### Usage Example
144160
```python
145161
next_obs, rewards, done, truncations, info = await env.astep(actions)
@@ -161,11 +177,13 @@ Close the environment (not implemented).
161177

162178
## Utility Functions
163179

164-
### `_actions_to_natural_language`
180+
### `_actions_to_natural_language_for_viewer`
165181
```python
166-
def _actions_to_natural_language(actions: dict[str, AgentAction]) -> str
182+
def _actions_to_natural_language_for_viewer(
183+
actions: dict[str, AgentAction], viewer: str
184+
) -> str
167185
```
168-
Converts agent actions to human-readable language.
186+
Converts agent actions to human-readable language, filtered for a specific viewer. Private messages are only included if the viewer is the sender or a recipient.
169187

170188
### `_map_gender_to_adj`
171189
```python
@@ -206,6 +224,14 @@ Renders text viewable by a specific agent using XMLRenderer.
206224

207225
---
208226

227+
## Action Space
228+
229+
The action space for each agent is a `Dict` space with:
230+
- `action_type`: A `LiteralSpace` that samples string literals (e.g., `"speak"`, `"none"`, `"action"`) from `available_action_types`
231+
- `argument`: A `Text` space (max 256 characters) for the action content
232+
233+
**Note**: The `action_type` is now a string literal, not an integer index. When sampling from the action space, you'll get strings like `"speak"` instead of integers like `0`.
234+
209235
## Usage Example
210236
Here's a typical usage example starting an episode in the environment:
211237

@@ -221,15 +247,46 @@ observations = env.reset(
221247
seed=42,
222248
agents={
223249
"agent_1": Agent(...),
224-
"agent_2": Agent(...)
250+
"agent_2": Agent(...),
251+
"agent_3": Agent(...)
225252
},
226253
omniscient=True
227254
)
228255

229-
# Perform actions
256+
# Perform public actions (visible to all)
230257
actions = {
231-
"agent_1": AgentAction(action_type="speak", argument="Hello!"),
258+
"agent_1": AgentAction(action_type="speak", argument="Hello everyone!"),
232259
"agent_2": AgentAction(action_type="action", argument="waved"),
260+
"agent_3": AgentAction(action_type="speak", argument="Hi there!"),
233261
}
234262

235263
next_obs, rewards, done, truncations, info = env.step(actions)
264+
265+
# Perform actions with private messages
266+
actions_with_private = {
267+
"agent_1": AgentAction(
268+
action_type="speak",
269+
argument="Psst, agent_2, let's discuss this privately",
270+
to=["agent_2"] # Only visible to agent_1 and agent_2
271+
),
272+
"agent_2": AgentAction(action_type="speak", argument="Hello everyone!"), # Public
273+
"agent_3": AgentAction(
274+
action_type="speak",
275+
argument="I'll talk to agent_1",
276+
to=["agent_1"] # Only visible to agent_1 and agent_3
277+
),
278+
}
279+
280+
next_obs, rewards, done, truncations, info = env.step(actions_with_private)
281+
282+
# Check observations - each agent sees different things
283+
print("Agent 1 sees:", next_obs["agent_1"].last_turn)
284+
# Includes both private messages (from agent_1 and agent_3) and public message from agent_2
285+
286+
print("Agent 2 sees:", next_obs["agent_2"].last_turn)
287+
# Includes private message from agent_1 and public message from agent_2
288+
289+
print("Agent 3 sees:", next_obs["agent_3"].last_turn)
290+
# Includes private message from agent_3 and public message from agent_2
291+
# Does NOT include the private message from agent_1 to agent_2
292+
```

docs/pages/python_API/messages/message_classes.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,32 @@ Represents the environment's response to the interaction.
7474

7575
### `AgentAction`
7676

77-
Represents an action taken by an agent.
77+
Represents an action taken by an agent. Actions can be either public (visible to all agents) or private (visible only to specific recipients).
7878

7979
#### Attributes
8080

81-
- `action_type: ActionType`: The type of action.
82-
- `argument: str`: The argument associated with the action.
81+
- `action_type: ActionType`: The type of action. Can be one of: `"none"`, `"speak"`, `"non-verbal communication"`, `"action"`, or `"leave"`.
82+
- `argument: str`: The argument associated with the action (e.g., the utterance for `"speak"`, the description for `"action"`).
83+
- `to: list[str] | None`: (Optional) List of recipient agent names. When specified, the action is a private message visible only to the sender and the listed recipients. When `None` or empty, the action is public and visible to all agents. Defaults to `None`.
8384

8485
#### Methods
8586

86-
- `to_natural_language(self) -> str`: Returns a string describing the agent's action.
87+
- `to_natural_language(self) -> str`: Returns a string describing the agent's action. Private messages are prefixed with `[private to {recipients}]`.
88+
89+
#### Private Messages
90+
91+
Private messages allow agents to communicate privately with specific recipients. When an action has a `to` field specified:
92+
93+
- The action is only visible to the sender and the agents listed in `to`
94+
- Other agents will not see the action in their observations
95+
- The `to` field is validated to ensure recipients are valid agent names and the sender cannot target themselves
96+
97+
#### Validation
98+
99+
The `to` field is validated when creating an `AgentAction` with context:
100+
- Recipients must be valid agent names in the environment
101+
- Senders cannot send private messages to themselves
102+
- Invalid recipients will raise a `ValueError` with details about allowed recipients
87103

88104
### `ScriptInteraction`
89105

@@ -134,8 +150,19 @@ response = ScriptEnvironmentResponse(
134150
)
135151
print(response.to_natural_language())
136152

153+
# Public action (visible to all agents)
137154
action = AgentAction(action_type="speak", argument="Hello, how can I help you?")
138155
print(action.to_natural_language())
156+
# Output: said: "Hello, how can I help you?"
157+
158+
# Private action (visible only to sender and specified recipients)
159+
private_action = AgentAction(
160+
action_type="speak",
161+
argument="Psst, let's discuss this privately",
162+
to=["agent2", "agent3"]
163+
)
164+
print(private_action.to_natural_language())
165+
# Output: [private to ['agent2', 'agent3']] said: "Psst, let's discuss this privately"
139166

140167
interaction_script = """
141168
Turn #1
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Multi-Agent Tests
2+
3+
This directory contains test scenarios for Sotopia's multi-agent (3+ agents)
4+
with private action support.
5+
6+
Run the demo script:
7+
8+
```sh
9+
mkdir -p examples/experimental/multi_agents_private_dm/redis-data
10+
redis-stack-server --dir examples/experimental/multi_agents_private_dm/redis-data
11+
uv run examples/experimental/multi_agents_private_dm/multi_agents_private_dm.py
12+
```

0 commit comments

Comments
 (0)