Skip to content

Commit

Permalink
Merge pull request #481 from garylin2099/human_roleplay
Browse files Browse the repository at this point in the history
allow human to play any roles
  • Loading branch information
garylin2099 authored Nov 17, 2023
2 parents f8ebfa9 + 705a3e9 commit 820fee5
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 19 deletions.
26 changes: 12 additions & 14 deletions examples/build_customized_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import fire

from metagpt.llm import LLM
from metagpt.actions import Action
from metagpt.roles import Role
from metagpt.schema import Message
Expand All @@ -19,19 +20,10 @@ class SimpleWriteCode(Action):
PROMPT_TEMPLATE = """
Write a python function that can {instruction} and provide two runnnable test cases.
Return ```python your_code_here ``` with NO other texts,
example:
```python
# function
def add(a, b):
return a + b
# test cases
print(add(1, 2))
print(add(3, 4))
```
your code:
"""

def __init__(self, name="SimpleWriteCode", context=None, llm=None):
def __init__(self, name: str = "SimpleWriteCode", context=None, llm: LLM = None):
super().__init__(name, context, llm)

async def run(self, instruction: str):
Expand All @@ -51,8 +43,9 @@ def parse_code(rsp):
code_text = match.group(1) if match else rsp
return code_text


class SimpleRunCode(Action):
def __init__(self, name="SimpleRunCode", context=None, llm=None):
def __init__(self, name: str = "SimpleRunCode", context=None, llm: LLM = None):
super().__init__(name, context, llm)

async def run(self, code_text: str):
Expand All @@ -61,6 +54,7 @@ async def run(self, code_text: str):
logger.info(f"{code_result=}")
return code_result


class SimpleCoder(Role):
def __init__(
self,
Expand All @@ -73,15 +67,16 @@ def __init__(

async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
todo = self._rc.todo
todo = self._rc.todo # todo will be SimpleWriteCode()

msg = self.get_memories(k=1)[0] # find the most recent messages

code_text = await SimpleWriteCode().run(msg.content)
msg = Message(content=code_text, role=self.profile, cause_by=todo)
code_text = await todo.run(msg.content)
msg = Message(content=code_text, role=self.profile, cause_by=type(todo))

return msg


class RunnableCoder(Role):
def __init__(
self,
Expand All @@ -95,6 +90,8 @@ def __init__(

async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
# By choosing the Action by order under the hood
# todo will be first SimpleWriteCode() then SimpleRunCode()
todo = self._rc.todo

msg = self.get_memories(k=1)[0] # find the most k recent messages
Expand All @@ -104,6 +101,7 @@ async def _act(self) -> Message:
self._rc.memory.add(msg)
return msg


def main(msg="write a function that calculates the product of a list and run it"):
# role = SimpleCoder()
role = RunnableCoder()
Expand Down
158 changes: 158 additions & 0 deletions examples/build_customized_multi_agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
'''
Filename: MetaGPT/examples/build_customized_multi_agents.py
Created Date: Wednesday, November 15th 2023, 7:12:39 pm
Author: garylin2099
'''
import re
import asyncio
import fire

from metagpt.llm import LLM
from metagpt.actions import Action, BossRequirement
from metagpt.roles import Role
from metagpt.team import Team
from metagpt.schema import Message
from metagpt.logs import logger

def parse_code(rsp):
pattern = r'```python(.*)```'
match = re.search(pattern, rsp, re.DOTALL)
code_text = match.group(1) if match else rsp
return code_text

class SimpleWriteCode(Action):

PROMPT_TEMPLATE = """
Write a python function that can {instruction}.
Return ```python your_code_here ``` with NO other texts,
your code:
"""

def __init__(self, name: str = "SimpleWriteCode", context=None, llm: LLM = None):
super().__init__(name, context, llm)

async def run(self, instruction: str):

prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)

rsp = await self._aask(prompt)

code_text = parse_code(rsp)

return code_text


class SimpleCoder(Role):
def __init__(
self,
name: str = "Alice",
profile: str = "SimpleCoder",
**kwargs,
):
super().__init__(name, profile, **kwargs)
self._watch([BossRequirement])
self._init_actions([SimpleWriteCode])


class SimpleWriteTest(Action):

PROMPT_TEMPLATE = """
Context: {context}
Write {k} unit tests using pytest for the given function, assuming you have imported it.
Return ```python your_code_here ``` with NO other texts,
your code:
"""

def __init__(self, name: str = "SimpleWriteTest", context=None, llm: LLM = None):
super().__init__(name, context, llm)

async def run(self, context: str, k: int = 3):

prompt = self.PROMPT_TEMPLATE.format(context=context, k=k)

rsp = await self._aask(prompt)

code_text = parse_code(rsp)

return code_text


class SimpleTester(Role):
def __init__(
self,
name: str = "Bob",
profile: str = "SimpleTester",
**kwargs,
):
super().__init__(name, profile, **kwargs)
self._init_actions([SimpleWriteTest])
# self._watch([SimpleWriteCode])
self._watch([SimpleWriteCode, SimpleWriteReview]) # feel free to try this too

async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
todo = self._rc.todo

# context = self.get_memories(k=1)[0].content # use the most recent memory as context
context = self.get_memories() # use all memories as context

code_text = await todo.run(context, k=5) # specify arguments
msg = Message(content=code_text, role=self.profile, cause_by=type(todo))

return msg


class SimpleWriteReview(Action):

PROMPT_TEMPLATE = """
Context: {context}
Review the test cases and provide one critical comments:
"""

def __init__(self, name: str = "SimpleWriteReview", context=None, llm: LLM = None):
super().__init__(name, context, llm)

async def run(self, context: str):

prompt = self.PROMPT_TEMPLATE.format(context=context)

rsp = await self._aask(prompt)

return rsp


class SimpleReviewer(Role):
def __init__(
self,
name: str = "Charlie",
profile: str = "SimpleReviewer",
**kwargs,
):
super().__init__(name, profile, **kwargs)
self._init_actions([SimpleWriteReview])
self._watch([SimpleWriteTest])


async def main(
idea: str = "write a function that calculates the product of a list",
investment: float = 3.0,
n_round: int = 5,
add_human: bool = False,
):
logger.info(idea)

team = Team()
team.hire(
[
SimpleCoder(),
SimpleTester(),
SimpleReviewer(is_human=add_human),
]
)

team.invest(investment=investment)
team.start_project(idea)
await team.run(n_round=n_round)

if __name__ == '__main__':
fire.Fire(main)
2 changes: 2 additions & 0 deletions examples/debate.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ async def _act(self) -> Message:
send_to=self.opponent_name,
)

self._rc.memory.add(msg)

return msg

async def debate(idea: str, investment: float = 3.0, n_round: int = 5):
Expand Down
1 change: 1 addition & 0 deletions metagpt/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from metagpt.provider.anthropic_api import Claude2 as Claude
from metagpt.provider.openai_api import OpenAIGPTAPI as LLM
from metagpt.provider.human_provider import HumanProvider

DEFAULT_LLM = LLM()
CLAUDE_LLM = Claude()
Expand Down
35 changes: 35 additions & 0 deletions metagpt/provider/human_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'''
Filename: MetaGPT/metagpt/provider/human_provider.py
Created Date: Wednesday, November 8th 2023, 11:55:46 pm
Author: garylin2099
'''
from typing import Optional
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.logs import logger

class HumanProvider(BaseGPTAPI):
"""Humans provide themselves as a 'model', which actually takes in human input as its response.
This enables replacing LLM anywhere in the framework with a human, thus introducing human interaction
"""

def ask(self, msg: str) -> str:
logger.info("It's your turn, please type in your response. You may also refer to the context below")
rsp = input(msg)
if rsp in ["exit", "quit"]:
exit()
return rsp

async def aask(self, msg: str, system_msgs: Optional[list[str]] = None) -> str:
return self.ask(msg)

def completion(self, messages: list[dict]):
"""dummy implementation of abstract method in base"""
return []

async def acompletion(self, messages: list[dict]):
"""dummy implementation of abstract method in base"""
return []

async def acompletion_text(self, messages: list[dict], stream=False) -> str:
"""dummy implementation of abstract method in base"""
return []
15 changes: 10 additions & 5 deletions metagpt/roles/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# from metagpt.environment import Environment
from metagpt.config import CONFIG
from metagpt.actions import Action, ActionOutput
from metagpt.llm import LLM
from metagpt.llm import LLM, HumanProvider
from metagpt.logs import logger
from metagpt.memory import Memory, LongTermMemory
from metagpt.schema import Message
Expand Down Expand Up @@ -65,6 +65,7 @@ class RoleSetting(BaseModel):
goal: str
constraints: str
desc: str
is_human: bool

def __str__(self):
return f"{self.name}({self.profile})"
Expand Down Expand Up @@ -106,9 +107,10 @@ def history(self) -> list[Message]:
class Role:
"""Role/Agent"""

def __init__(self, name="", profile="", goal="", constraints="", desc=""):
self._llm = LLM()
self._setting = RoleSetting(name=name, profile=profile, goal=goal, constraints=constraints, desc=desc)
def __init__(self, name="", profile="", goal="", constraints="", desc="", is_human=False):
self._llm = LLM() if not is_human else HumanProvider()
self._setting = RoleSetting(name=name, profile=profile, goal=goal,
constraints=constraints, desc=desc, is_human=is_human)
self._states = []
self._actions = []
self._role_id = str(self._setting)
Expand All @@ -122,8 +124,11 @@ def _init_actions(self, actions):
self._reset()
for idx, action in enumerate(actions):
if not isinstance(action, Action):
i = action("")
i = action("", llm=self._llm)
else:
if self._setting.is_human and not isinstance(action.llm, HumanProvider):
logger.warning(f"is_human attribute does not take effect,"
f"as Role's {str(action)} was initialized using LLM, try passing in Action classes instead of initialized instances")
i = action
i.set_prefix(self._get_prefix(), self.profile)
self._actions.append(i)
Expand Down

0 comments on commit 820fee5

Please sign in to comment.