Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experimental] aact-based Based Agent #221

Merged
merged 68 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
72b82c0
add aact as a dependency
ProKil Oct 4, 2024
ceaeddf
minimal demo example of running custom model
ProKil Oct 6, 2024
51cbb7e
devcontainer setup and example
ProKil Oct 6, 2024
cf9dcd6
remove default_bad_process_model to allow using custom model entirely
ProKil Oct 6, 2024
a07be86
improve the demo to show parallel execution
ProKil Oct 6, 2024
38d182d
CI: update tests trigger from pull request target to pull request
ProKil Oct 6, 2024
4620fb4
fix mypy errors
ProKil Oct 6, 2024
1975021
adding stubs to pyproject.toml
ProKil Oct 6, 2024
d059f0b
poetry lock
ProKil Oct 6, 2024
a7603b4
install all extras in the devcontainer start script
ProKil Oct 6, 2024
a4f4c02
add dev containers instruction
ProKil Oct 7, 2024
569ef11
migration to uv
ProKil Oct 7, 2024
b9fcf3d
update mypy
ProKil Oct 7, 2024
0eed5fd
Merge branch 'feature/migrate-to-uv' into feature/integrate-aact
ProKil Oct 7, 2024
312e052
Merge remote-tracking branch 'origin/main' into feature/migrate-to-uv
ProKil Oct 7, 2024
d8d4708
Update index.mdx
ProKil Oct 7, 2024
5054a97
update uv venv path in the devcontainer and contributor's guide
ProKil Oct 7, 2024
66ba0ae
Merge branch 'feature/migrate-to-uv' of github.com:sotopia-lab/sotopi…
ProKil Oct 7, 2024
7491e75
simple examples of using aact for multi-agent async communication
ProKil Oct 8, 2024
b4a4f67
Merge branch 'feature/migrate-to-uv' into feature/integrate-aact
ProKil Oct 8, 2024
b7ccf10
allowing agents' aact function to return None
ProKil Oct 8, 2024
992320a
import Self for 3.10
ProKil Oct 8, 2024
5828002
Merge remote-tracking branch 'origin/main' into feature/integrate-aact
ProKil Oct 8, 2024
5e3eb86
Create readme.md
ProKil Oct 8, 2024
2b4b00e
dockerfile
ProKil Oct 8, 2024
dba0829
record node log
ProKil Oct 8, 2024
ceebc9f
frequency -> interval
ProKil Oct 8, 2024
fd7ef39
docker compose (it works)
ProKil Oct 9, 2024
65c19a1
use published images to speed up
ProKil Oct 9, 2024
7cee92d
add ci test with docker
ProKil Oct 10, 2024
631c53c
use compose action github action
ProKil Oct 10, 2024
4d7b3e6
update docker compose file
ProKil Oct 10, 2024
b06c75e
update compose file path
ProKil Oct 10, 2024
cdbff03
use github-action-docker-compose-test-run
ProKil Oct 10, 2024
29426aa
remove unused port binding in docker-compose
ProKil Oct 10, 2024
278403a
add quotes to docker compose command
ProKil Oct 10, 2024
b18f76b
test run
ProKil Oct 10, 2024
53b1845
test run
ProKil Oct 10, 2024
3190af5
write test script in tests.sh
ProKil Oct 10, 2024
506237b
use docker compose
ProKil Oct 10, 2024
aa114fe
test run
ProKil Oct 10, 2024
6205623
--rm
ProKil Oct 10, 2024
c8cd931
./ -> .
ProKil Oct 10, 2024
8cf257b
test
ProKil Oct 10, 2024
d453d39
change to arm64
ProKil Oct 10, 2024
2865dd3
fix docker platform problem
ProKil Oct 10, 2024
9c78d12
change test os
ProKil Oct 10, 2024
65b314d
fix some build bugs
ProKil Oct 10, 2024
5810007
fix runner dir
ProKil Oct 10, 2024
6f7ba6f
fix a test case for sample
ProKil Oct 10, 2024
703147a
update cli test to test_install
ProKil Oct 10, 2024
1d1da9b
update test benchmark to improve coverage
ProKil Oct 10, 2024
836d122
remove unused and maintain structured output compatibility
ProKil Oct 10, 2024
1659b34
fix evaluator bug
ProKil Oct 10, 2024
60ba747
Merge branch 'feature/docker-compose' into feature/integrate-aact
ProKil Oct 10, 2024
06b2172
add a test script which contributors can run locally
ProKil Oct 10, 2024
2851036
Merge branch 'feature/docker-compose' into feature/integrate-aact
ProKil Oct 10, 2024
2b2c1ab
Merge remote-tracking branch 'origin/main' into feature/integrate-aact
ProKil Oct 11, 2024
37d3d9f
bump the version to 0.1.1
ProKil Oct 11, 2024
dfd861d
add langchain openai back
ProKil Oct 11, 2024
ee41bbb
add langchain openai in uv lock
ProKil Oct 11, 2024
125cc01
remove redundant cast
ProKil Oct 11, 2024
f3c27f4
add test case
ProKil Oct 11, 2024
d241513
test base agent
ProKil Oct 11, 2024
69821d6
more coverage for agent.py
ProKil Oct 12, 2024
3d581d3
add __init__ to sotopia.experimental
ProKil Oct 12, 2024
30d2d3d
chore: Add experimental page and agents documentation
ProKil Oct 12, 2024
7c467d7
Agent Documentation
ProKil Oct 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/pages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"title": "Examples",
"type": "page"
},
"experimental": {
"title": "Experimental",
"type": "page"
},
"contribution": {
"title": "Contribution",
"type": "page"
Expand Down
8 changes: 8 additions & 0 deletions docs/pages/experimental/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"index": {
"title": "Overview"
},
"agents": {
"title": "Agents"
}
}
43 changes: 43 additions & 0 deletions docs/pages/experimental/agents.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Callout } from "nextra/components"

<Callout type="warning">
This part of the documentation is for experimental features. The APIs and functionalities are subject to frequent change.
</Callout>

<Callout type="warning">
The Agent API implemented here conflicts with stable Agent API in Sotopia.
</Callout>

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 and output are of derived types of `aact.messages.DataModel`.

### Creating your own agents
To create your own agents, you need to subclass the `BaseAgent` class
and implement the asynchronous `aact` method.
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!":

```python
from aact import NodeFactory
from aact.messages import Text
from sotopia.experimental import BaseAgent

@NodeFactory.register("simple_echo_agent") # Register the agent so that it can be used in the dataflow
class SimpleEchoAgent(BaseAgent[Text, Text]):
def __init__(self, input_channel: str, output_channel: str, redis_url: str) -> None:
ProKil marked this conversation as resolved.
Show resolved Hide resolved
super().__init__( # call the constructor of the base class
input_channel_types=[(input_channel, Text)],
output_channel_types=[(output_channel, Text)],
)

async def aact(self, observation: Text) -> Text: # major agent reactive function
return Text(text=f"Hello, {observation.text}!")
```

Let me break this down for you:
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.
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.
3. Inherit the `BaseAgent` class and specify the input and output channel types in the constructor.
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, ..."

For a running example, try out `examples/experimental/tick_and_echo_agents`.
ProKil marked this conversation as resolved.
Show resolved Hide resolved
25 changes: 25 additions & 0 deletions docs/pages/experimental/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Callout } from "nextra/components"

<Callout type="warning">
This part of the documentation is for experimental features. The APIs and functionalities are subject to frequent change.
</Callout>

The experimental APIs of Sotopia are intended for quickly prototyping and experimenting with new functionalities,
without breaking the existing stable APIs. But we will still maintain the quality of the code for these features.
Feel free to raise an issue if you find any bugs or wants more features in the experimental APIs.

# Experimetal APIs
The experimental APIs are in different states:

- *scheduled*: the APIs will be merged into next minor releases.
- *implemented*: the APIs are implemented and can be used, which might be merged into the stable APIs in the next few minor releases.
- *planned*: the APIs are planned and will be implemented in the future.
- *idealized*: the APIs are idealized and might be implemented in the future.

Here are the experimental APIs:
- [Agents](/experimental/agents) (*implemented*): aact-based asynchronous agents that don't follow OpenAI Gym's turn-based formulation.
- Engines (*planned*): aact-based asynchronous environment engines. This would include
- [Orchestrator](https://github.com/sotopia-lab/sotopia/issues/231): an engine base class for engines that dictates the orders and turns of the agents.
- [Evaluator](https://github.com/sotopia-lab/sotopia/issues/232): an engine base class for engines that evaluates the agents' performance.
- API Engine: an engine that interacts with REST APIs.
- Generation APIs (*planned*): experimental generation APIs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
from typing import AsyncIterator
from aact import Message, NodeFactory
from aact.messages import Text, Tick, DataModel, DataModelFactory
from sotopia.agents.llm_agent import ainput
from sotopia.experimental.agents import BaseAgent

from sotopia.generation_utils import agenerate
from sotopia.generation_utils.generate import StrOutputParser
from sotopia.messages import ActionType

from pydantic import Field


@DataModelFactory.register("agent_action")
class AgentAction(DataModel):
agent_name: str = Field(description="the name of the agent")
action_type: ActionType = Field(
description="whether to speak at this turn or choose to not do anything"
)
argument: str = Field(
description="the utterance if choose to speak, the expression or gesture if choose non-verbal communication, or the physical action if choose action"
)

def to_natural_language(self) -> str:
match self.action_type:
case "none":
return "did nothing"
case "speak":
return f'said: "{self.argument}"'
case "non-verbal communication":
return f"[{self.action_type}] {self.argument}"
case "action":
return f"[{self.action_type}] {self.argument}"
case "leave":
return "left the conversation"


def _format_message_history(message_history: list[tuple[str, str]]) -> str:
return "\n".join(
(f"{speaker} said {message}") for speaker, message in message_history
)


@NodeFactory.register("llm_agent")
class LLMAgent(BaseAgent[AgentAction | Tick, AgentAction]):
def __init__(
self,
input_text_channels: list[str],
input_tick_channel: str,
output_channel: str,
query_interval: int,
agent_name: str,
goal: str,
model_name: str,
redis_url: str,
):
super().__init__(
[
(input_text_channel, AgentAction)
for input_text_channel in input_text_channels
]
+ [
(input_tick_channel, Tick),
],
[(output_channel, AgentAction)],
redis_url,
)
self.output_channel = output_channel
self.query_interval = query_interval
self.count_ticks = 0
self.message_history: list[tuple[str, str]] = []
self.name = agent_name
self.model_name = model_name
self.goal = goal

async def send(self, message: AgentAction) -> None:
if message.action_type == "speak":
await self.r.publish(
self.output_channel,
Message[AgentAction](data=message).model_dump_json(),
)

async def aact(self, message: AgentAction | Tick) -> AgentAction:
match message:
case Tick():
self.count_ticks += 1
if self.count_ticks % self.query_interval == 0:
agent_action: str = await agenerate(
model_name=self.model_name,
template="Imagine that you are a friend of the other persons. Here is the "
ProKil marked this conversation as resolved.
Show resolved Hide resolved
"conversation between you and them.\n"
"You are {agent_name} in the conversation.\n"
"{message_history}\n"
"and you plan to {goal}.\n"
"You can choose to interrupt the other person "
ProKil marked this conversation as resolved.
Show resolved Hide resolved
"by saying something or not to interrupt by outputting notiong. What would you say? "
"Please only output a sentence or not outputting anything."
"{format_instructions}",
input_values={
"message_history": _format_message_history(
self.message_history
),
"goal": self.goal,
"agent_name": self.name,
},
temperature=0.7,
output_parser=StrOutputParser(),
)
if agent_action != "none" and agent_action != "":
self.message_history.append((self.name, agent_action))
return AgentAction(
agent_name=self.name,
action_type="speak",
argument=agent_action,
)
else:
return AgentAction(
agent_name=self.name, action_type="none", argument=""
)
else:
return AgentAction(
agent_name=self.name, action_type="none", argument=""
)
case AgentAction(
agent_name=agent_name, action_type=action_type, argument=text
):
if action_type == "speak":
self.message_history.append((agent_name, text))
return AgentAction(
agent_name=self.name, action_type="none", argument=""
)
case _:
raise ValueError(f"Unexpected message type: {type(message)}")


@NodeFactory.register("input_node")
class InputNode(BaseAgent[AgentAction, AgentAction]):
def __init__(
self,
input_channel: str,
output_channel: str,
agent_name: str,
redis_url: str = "redis://localhost:6379/0",
):
super().__init__(
input_channel_types=[(input_channel, AgentAction)],
output_channel_types=[(output_channel, AgentAction)],
redis_url=redis_url,
)
self.input_channel = input_channel
self.agent_name = agent_name

async def event_handler(
self, channel: str, message: Message[AgentAction]
) -> AsyncIterator[tuple[str, Message[AgentAction]]]:
if channel == self.input_channel:
print(f"Received message: {message}")
else:
raise ValueError(f"Unexpected channel: {channel}")
yield self.output_channel, Text(text=message.data.argument)

async def _task_scheduler(self) -> None:
while not self.shutdown_event.is_set():
text_input = await ainput()
await self.send(
AgentAction(
agent_name=self.agent_name, action_type="speak", argument=text_input
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
redis_url = "redis://localhost:6379/0"
extra_modules = ["examples.experimental.group_discussion_agents.group_discussion_agents"]

[[nodes]]
node_name = "Jack"
node_class = "llm_agent"

[nodes.node_args]
query_interval = 5
output_channel = "Jack"
input_text_channels = ["Jane", "John"]
input_tick_channel = "tick/secs/1"
goal = "want to play pocker with your friends tonight"
model_name = "gpt-4o-mini"
agent_name = "Jack"

[[nodes]]
node_name = "Jane"
node_class = "llm_agent"

[nodes.node_args]
query_interval = 7
output_channel = "Jane"
input_text_channels = ["Jack", "John"]
input_tick_channel = "tick/secs/1"
goal = "want to play soccer with your friends tonight"
model_name = "gpt-4o-mini"
agent_name = "Jane"

[[nodes]]
node_name = "John"
node_class = "llm_agent"

[nodes.node_args]
query_interval = 10
output_channel = "John"
input_text_channels = ["Jack", "Jane"]
input_tick_channel = "tick/secs/1"
goal = "want to go to concert with your friends tonight"
model_name = "gpt-4o-mini"
agent_name = "John"

[[nodes]]
node_name = "record"
node_class = "record"

[nodes.node_args]
jsonl_file_path = "log.jsonl"

[nodes.node_args.record_channel_types]
"Jack" = "agent_action"
"Jane" = "agent_action"
"John" = "agent_action"

[[nodes]]
node_name = "tick"
node_class = "tick"
5 changes: 5 additions & 0 deletions examples/experimental/group_discussion_agents/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
To run this example, please use aact to launch.

```bash
aact run-dataflow examples/experimental/group_discussion_agents/group_discussion_agents.toml
```
Loading
Loading