Skip to content

Commit 6916077

Browse files
committed
Merge remote-tracking branch 'origin/main' into release
2 parents 1836791 + 080a17b commit 6916077

File tree

18 files changed

+1549
-520
lines changed

18 files changed

+1549
-520
lines changed

docs/pages/_meta.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"title": "Examples",
1919
"type": "page"
2020
},
21+
"experimental": {
22+
"title": "Experimental",
23+
"type": "page"
24+
},
2125
"contribution": {
2226
"title": "Contribution",
2327
"type": "page"

docs/pages/experimental/_meta.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"index": {
3+
"title": "Overview"
4+
},
5+
"agents": {
6+
"title": "Agents"
7+
}
8+
}

docs/pages/experimental/agents.mdx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Callout } from "nextra/components"
2+
3+
<Callout type="warning">
4+
This part of the documentation is for experimental features. The APIs and functionalities are subject to frequent change.
5+
</Callout>
6+
7+
<Callout type="warning">
8+
The Agent API implemented here conflicts with stable Agent API in Sotopia.
9+
</Callout>
10+
11+
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.
12+
No matter which type of agent, they have the same interface to interact with the environment:
13+
the input and output are of derived types of `aact.messages.DataModel`.
14+
15+
### Creating your own agents
16+
To create your own agents, you need to subclass the `BaseAgent` class
17+
and implement the asynchronous `aact` method.
18+
The `aact` method takes an `Observation` object as input and returns an `AgentAction` object as output. Here is an example of a simple agent that always says "Hello, world!":
19+
20+
```python
21+
from aact import NodeFactory
22+
from aact.messages import Text
23+
from sotopia.experimental import BaseAgent
24+
25+
@NodeFactory.register("simple_echo_agent") # Register the agent so that it can be used in the dataflow
26+
class SimpleEchoAgent(BaseAgent[Text, Text]):
27+
def __init__(self, input_channel: str, output_channel: str, redis_url: str) -> None:
28+
super().__init__( # call the constructor of the base class
29+
input_channel_types=[(input_channel, Text)],
30+
output_channel_types=[(output_channel, Text)],
31+
)
32+
33+
async def aact(self, observation: Text) -> Text: # major agent reactive function
34+
return Text(text=f"Hello, {observation.text}!")
35+
```
36+
37+
Let me break this down for you:
38+
1. `NodeFactory` is a decorator that registers the agent so that it can be used in the dataflow. Dataflow is a concept in `aact` that defines how `nodes` are interacting with each other.
39+
2. `channel` is a concept in `redis` pubsub and `aact`. A node can send messages to many channels, and receive messages many channels as well. To subclass `BaseAgent`, you will need to feed two lists of channel-message type pairs to `input_channel_types` and `output_channel_types` respectively.
40+
3. Inherit the `BaseAgent` class and specify the input and output channel types in the constructor.
41+
4. Implement the `aact` method that takes an `Observation` object as input and returns an `AgentAction` object as output. In this case, the agent always says "Hello, ..."
42+
43+
For a running example, try out `examples/experimental/tick_and_echo_agents`.

docs/pages/experimental/index.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Callout } from "nextra/components"
2+
3+
<Callout type="warning">
4+
This part of the documentation is for experimental features. The APIs and functionalities are subject to frequent change.
5+
</Callout>
6+
7+
The experimental APIs of Sotopia are intended for quickly prototyping and experimenting with new functionalities,
8+
without breaking the existing stable APIs. But we will still maintain the quality of the code for these features.
9+
Feel free to raise an issue if you find any bugs or wants more features in the experimental APIs.
10+
11+
# Experimetal APIs
12+
The experimental APIs are in different states:
13+
14+
- *scheduled*: the APIs will be merged into next minor releases.
15+
- *implemented*: the APIs are implemented and can be used, which might be merged into the stable APIs in the next few minor releases.
16+
- *planned*: the APIs are planned and will be implemented in the future.
17+
- *idealized*: the APIs are idealized and might be implemented in the future.
18+
19+
Here are the experimental APIs:
20+
- [Agents](/experimental/agents) (*implemented*): aact-based asynchronous agents that don't follow OpenAI Gym's turn-based formulation.
21+
- Engines (*planned*): aact-based asynchronous environment engines. This would include
22+
- [Orchestrator](https://github.com/sotopia-lab/sotopia/issues/231): an engine base class for engines that dictates the orders and turns of the agents.
23+
- [Evaluator](https://github.com/sotopia-lab/sotopia/issues/232): an engine base class for engines that evaluates the agents' performance.
24+
- API Engine: an engine that interacts with REST APIs.
25+
- Generation APIs (*planned*): experimental generation APIs
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from typing import AsyncIterator
2+
from aact import Message, NodeFactory
3+
from aact.messages import Text, Tick, DataModel, DataModelFactory
4+
from sotopia.agents.llm_agent import ainput
5+
from sotopia.experimental.agents import BaseAgent
6+
7+
from sotopia.generation_utils import agenerate
8+
from sotopia.generation_utils.generate import StrOutputParser
9+
from sotopia.messages import ActionType
10+
11+
from pydantic import Field
12+
13+
14+
@DataModelFactory.register("agent_action")
15+
class AgentAction(DataModel):
16+
agent_name: str = Field(description="the name of the agent")
17+
action_type: ActionType = Field(
18+
description="whether to speak at this turn or choose to not do anything"
19+
)
20+
argument: str = Field(
21+
description="the utterance if choose to speak, the expression or gesture if choose non-verbal communication, or the physical action if choose action"
22+
)
23+
24+
def to_natural_language(self) -> str:
25+
match self.action_type:
26+
case "none":
27+
return "did nothing"
28+
case "speak":
29+
return f'said: "{self.argument}"'
30+
case "non-verbal communication":
31+
return f"[{self.action_type}] {self.argument}"
32+
case "action":
33+
return f"[{self.action_type}] {self.argument}"
34+
case "leave":
35+
return "left the conversation"
36+
37+
38+
def _format_message_history(message_history: list[tuple[str, str]]) -> str:
39+
return "\n".join(
40+
(f"{speaker} said {message}") for speaker, message in message_history
41+
)
42+
43+
44+
@NodeFactory.register("llm_agent")
45+
class LLMAgent(BaseAgent[AgentAction | Tick, AgentAction]):
46+
def __init__(
47+
self,
48+
input_text_channels: list[str],
49+
input_tick_channel: str,
50+
output_channel: str,
51+
query_interval: int,
52+
agent_name: str,
53+
goal: str,
54+
model_name: str,
55+
redis_url: str,
56+
):
57+
super().__init__(
58+
[
59+
(input_text_channel, AgentAction)
60+
for input_text_channel in input_text_channels
61+
]
62+
+ [
63+
(input_tick_channel, Tick),
64+
],
65+
[(output_channel, AgentAction)],
66+
redis_url,
67+
)
68+
self.output_channel = output_channel
69+
self.query_interval = query_interval
70+
self.count_ticks = 0
71+
self.message_history: list[tuple[str, str]] = []
72+
self.name = agent_name
73+
self.model_name = model_name
74+
self.goal = goal
75+
76+
async def send(self, message: AgentAction) -> None:
77+
if message.action_type == "speak":
78+
await self.r.publish(
79+
self.output_channel,
80+
Message[AgentAction](data=message).model_dump_json(),
81+
)
82+
83+
async def aact(self, message: AgentAction | Tick) -> AgentAction:
84+
match message:
85+
case Tick():
86+
self.count_ticks += 1
87+
if self.count_ticks % self.query_interval == 0:
88+
agent_action: str = await agenerate(
89+
model_name=self.model_name,
90+
template="Imagine that you are a friend of the other persons. Here is the "
91+
"conversation between you and them.\n"
92+
"You are {agent_name} in the conversation.\n"
93+
"{message_history}\n"
94+
"and you plan to {goal}.\n"
95+
"You can choose to interrupt the other person "
96+
"by saying something or not to interrupt by outputting notiong. What would you say? "
97+
"Please only output a sentence or not outputting anything."
98+
"{format_instructions}",
99+
input_values={
100+
"message_history": _format_message_history(
101+
self.message_history
102+
),
103+
"goal": self.goal,
104+
"agent_name": self.name,
105+
},
106+
temperature=0.7,
107+
output_parser=StrOutputParser(),
108+
)
109+
if agent_action != "none" and agent_action != "":
110+
self.message_history.append((self.name, agent_action))
111+
return AgentAction(
112+
agent_name=self.name,
113+
action_type="speak",
114+
argument=agent_action,
115+
)
116+
else:
117+
return AgentAction(
118+
agent_name=self.name, action_type="none", argument=""
119+
)
120+
else:
121+
return AgentAction(
122+
agent_name=self.name, action_type="none", argument=""
123+
)
124+
case AgentAction(
125+
agent_name=agent_name, action_type=action_type, argument=text
126+
):
127+
if action_type == "speak":
128+
self.message_history.append((agent_name, text))
129+
return AgentAction(
130+
agent_name=self.name, action_type="none", argument=""
131+
)
132+
case _:
133+
raise ValueError(f"Unexpected message type: {type(message)}")
134+
135+
136+
@NodeFactory.register("input_node")
137+
class InputNode(BaseAgent[AgentAction, AgentAction]):
138+
def __init__(
139+
self,
140+
input_channel: str,
141+
output_channel: str,
142+
agent_name: str,
143+
redis_url: str = "redis://localhost:6379/0",
144+
):
145+
super().__init__(
146+
input_channel_types=[(input_channel, AgentAction)],
147+
output_channel_types=[(output_channel, AgentAction)],
148+
redis_url=redis_url,
149+
)
150+
self.input_channel = input_channel
151+
self.agent_name = agent_name
152+
153+
async def event_handler(
154+
self, channel: str, message: Message[AgentAction]
155+
) -> AsyncIterator[tuple[str, Message[AgentAction]]]:
156+
if channel == self.input_channel:
157+
print(f"Received message: {message}")
158+
else:
159+
raise ValueError(f"Unexpected channel: {channel}")
160+
yield self.output_channel, Text(text=message.data.argument)
161+
162+
async def _task_scheduler(self) -> None:
163+
while not self.shutdown_event.is_set():
164+
text_input = await ainput()
165+
await self.send(
166+
AgentAction(
167+
agent_name=self.agent_name, action_type="speak", argument=text_input
168+
)
169+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
redis_url = "redis://localhost:6379/0"
2+
extra_modules = ["examples.experimental.group_discussion_agents.group_discussion_agents"]
3+
4+
[[nodes]]
5+
node_name = "Jack"
6+
node_class = "llm_agent"
7+
8+
[nodes.node_args]
9+
query_interval = 5
10+
output_channel = "Jack"
11+
input_text_channels = ["Jane", "John"]
12+
input_tick_channel = "tick/secs/1"
13+
goal = "want to play pocker with your friends tonight"
14+
model_name = "gpt-4o-mini"
15+
agent_name = "Jack"
16+
17+
[[nodes]]
18+
node_name = "Jane"
19+
node_class = "llm_agent"
20+
21+
[nodes.node_args]
22+
query_interval = 7
23+
output_channel = "Jane"
24+
input_text_channels = ["Jack", "John"]
25+
input_tick_channel = "tick/secs/1"
26+
goal = "want to play soccer with your friends tonight"
27+
model_name = "gpt-4o-mini"
28+
agent_name = "Jane"
29+
30+
[[nodes]]
31+
node_name = "John"
32+
node_class = "llm_agent"
33+
34+
[nodes.node_args]
35+
query_interval = 10
36+
output_channel = "John"
37+
input_text_channels = ["Jack", "Jane"]
38+
input_tick_channel = "tick/secs/1"
39+
goal = "want to go to concert with your friends tonight"
40+
model_name = "gpt-4o-mini"
41+
agent_name = "John"
42+
43+
[[nodes]]
44+
node_name = "record"
45+
node_class = "record"
46+
47+
[nodes.node_args]
48+
jsonl_file_path = "log.jsonl"
49+
50+
[nodes.node_args.record_channel_types]
51+
"Jack" = "agent_action"
52+
"Jane" = "agent_action"
53+
"John" = "agent_action"
54+
55+
[[nodes]]
56+
node_name = "tick"
57+
node_class = "tick"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
To run this example, please use aact to launch.
2+
3+
```bash
4+
aact run-dataflow examples/experimental/group_discussion_agents/group_discussion_agents.toml
5+
```

0 commit comments

Comments
 (0)