This commit is contained in:
CakeCN
2024-12-03 16:21:19 +08:00
commit 4198ca63b1
975 changed files with 333413 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
""" Import all agent related modules in the package. """
from .agent import AgentBase, DistConf
from .operator import Operator
from .dialog_agent import DialogAgent
from .dict_dialog_agent import DictDialogAgent
from .user_agent import UserAgent
from .text_to_image_agent import TextToImageAgent
from .rpc_agent import RpcAgent
from .react_agent import ReActAgent
from .rag_agent import LlamaIndexAgent
__all__ = [
"AgentBase",
"Operator",
"DialogAgent",
"DictDialogAgent",
"TextToImageAgent",
"UserAgent",
"ReActAgent",
"DistConf",
"RpcAgent",
"LlamaIndexAgent",
]

View File

@@ -0,0 +1,458 @@
# -*- coding: utf-8 -*-
""" Base class for Agent """
from __future__ import annotations
from abc import ABCMeta
from typing import Optional
from typing import Sequence
from typing import Union
from typing import Any
from typing import Type
import uuid
from loguru import logger
from agentscope.agents.operator import Operator
from agentscope.message import Msg
from agentscope.models import load_model_by_config_name
from agentscope.memory import TemporaryMemory
class _AgentMeta(ABCMeta):
"""The meta-class for agent.
1. record the init args into `_init_settings` field.
2. register class name into `registry` field.
"""
def __init__(cls, name: Any, bases: Any, attrs: Any) -> None:
if not hasattr(cls, "_registry"):
cls._registry = {}
else:
if name in cls._registry:
logger.warning(
f"Agent class with name [{name}] already exists.",
)
else:
cls._registry[name] = cls
super().__init__(name, bases, attrs)
def __call__(cls, *args: tuple, **kwargs: dict) -> Any:
to_dist = kwargs.pop("to_dist", False)
if to_dist is True:
to_dist = DistConf()
if to_dist is not False and to_dist is not None:
from .rpc_agent import RpcAgent
if cls is not RpcAgent and not issubclass(cls, RpcAgent):
return RpcAgent(
name=(
args[0]
if len(args) > 0
else kwargs["name"] # type: ignore[arg-type]
),
host=to_dist.pop( # type: ignore[arg-type]
"host",
"localhost",
),
port=to_dist.pop("port", None), # type: ignore[arg-type]
max_pool_size=kwargs.pop( # type: ignore[arg-type]
"max_pool_size",
8192,
),
max_timeout_seconds=to_dist.pop( # type: ignore[arg-type]
"max_timeout_seconds",
1800,
),
local_mode=to_dist.pop( # type: ignore[arg-type]
"local_mode",
True,
),
lazy_launch=to_dist.pop( # type: ignore[arg-type]
"lazy_launch",
True,
),
agent_id=cls.generate_agent_id(),
connect_existing=False,
agent_class=cls,
agent_configs={
"args": args,
"kwargs": kwargs,
"class_name": cls.__name__,
},
)
instance = super().__call__(*args, **kwargs)
instance._init_settings = {
"args": args,
"kwargs": kwargs,
"class_name": cls.__name__,
}
return instance
class DistConf(dict):
"""Distribution configuration for agents."""
def __init__(
self,
host: str = "localhost",
port: int = None,
max_pool_size: int = 8192,
max_timeout_seconds: int = 1800,
local_mode: bool = True,
lazy_launch: bool = True,
):
"""Init the distributed configuration.
Args:
host (`str`, defaults to `"localhost"`):
Hostname of the rpc agent server.
port (`int`, defaults to `None`):
Port of the rpc agent server.
max_pool_size (`int`, defaults to `8192`):
Max number of task results that the server can accommodate.
max_timeout_seconds (`int`, defaults to `1800`):
Timeout for task results.
local_mode (`bool`, defaults to `True`):
Whether the started rpc server only listens to local
requests.
lazy_launch (`bool`, defaults to `True`):
Only launch the server when the agent is called.
"""
self["host"] = host
self["port"] = port
self["max_pool_size"] = max_pool_size
self["max_timeout_seconds"] = max_timeout_seconds
self["local_mode"] = local_mode
self["lazy_launch"] = lazy_launch
class AgentBase(Operator, metaclass=_AgentMeta):
"""Base class for all agents.
All agents should inherit from this class and implement the `reply`
function.
"""
_version: int = 1
def __init__(
self,
name: str,
sys_prompt: Optional[str] = None,
model_config_name: str = None,
use_memory: bool = True,
memory_config: Optional[dict] = None,
to_dist: Optional[Union[DistConf, bool]] = False,
) -> None:
r"""Initialize an agent from the given arguments.
Args:
name (`str`):
The name of the agent.
sys_prompt (`Optional[str]`):
The system prompt of the agent, which can be passed by args
or hard-coded in the agent.
model_config_name (`str`, defaults to None):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`):
The config of memory.
to_dist (`Optional[Union[DistConf, bool]]`, default to `False`):
The configurations passed to :py:meth:`to_dist` method. Used in
:py:class:`_AgentMeta`, when this parameter is provided,
the agent will automatically be converted into its distributed
version. Below are some examples:
.. code-block:: python
# run as a sub process
agent = XXXAgent(
# ... other parameters
to_dist=True,
)
# connect to an existing agent server
agent = XXXAgent(
# ... other parameters
to_dist=DistConf(
host="<ip of your server>",
port=<port of your server>,
# other parameters
),
)
See :doc:`Tutorial<tutorial/208-distribute>` for detail.
"""
self.name = name
self.memory_config = memory_config
if sys_prompt is not None:
self.sys_prompt = sys_prompt
# TODO: support to receive a ModelWrapper instance
if model_config_name is not None:
self.model = load_model_by_config_name(model_config_name)
if use_memory:
self.memory = TemporaryMemory(memory_config)
else:
self.memory = None
# The global unique id of this agent
self._agent_id = self.__class__.generate_agent_id()
# The audience of this agent, which means if this agent generates a
# response, it will be passed to all agents in the audience.
self._audience = None
# convert to distributed agent, conversion is in `_AgentMeta`
if to_dist is not False and to_dist is not None:
logger.info(
f"Convert {self.__class__.__name__}[{self.name}] into"
" a distributed agent.",
)
@classmethod
def generate_agent_id(cls) -> str:
"""Generate the agent_id of this agent instance"""
# TODO: change cls.__name__ into a global unique agent_type
return f"{cls.__name__}_{uuid.uuid4().hex}"
# todo: add a unique agent_type field to distinguish different agent class
@classmethod
def get_agent_class(cls, agent_class_name: str) -> Type[AgentBase]:
"""Get the agent class based on the specific agent class name.
Args:
agent_class_name (`str`): the name of the agent class.
Raises:
ValueError: Agent class name not exits.
Returns:
Type[AgentBase]: the AgentBase sub-class.
"""
if agent_class_name not in cls._registry:
raise ValueError(f"Agent [{agent_class_name}] not found.")
return cls._registry[agent_class_name] # type: ignore[return-value]
@classmethod
def register_agent_class(cls, agent_class: Type[AgentBase]) -> None:
"""Register the agent class into the registry.
Args:
agent_class (Type[AgentBase]): the agent class to be registered.
"""
agent_class_name = agent_class.__name__
if agent_class_name in cls._registry:
logger.info(
f"Agent class with name [{agent_class_name}] already exists.",
)
else:
cls._registry[agent_class_name] = agent_class
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None, user_backboard:str = "") -> Msg:
"""Define the actions taken by this agent.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
user_backboard (`str`): The user backboard, including user's IDE files in brief.
Returns:
`Msg`: The output message generated by the agent.
Note:
Given that some agents are in an adversarial environment,
their input doesn't include the thoughts of other agents.
"""
raise NotImplementedError(
f"Agent [{type(self).__name__}] is missing the required "
f'"reply" function.',
)
def load_from_config(self, config: dict) -> None:
"""Load configuration for this agent.
Args:
config (`dict`): model configuration
"""
def export_config(self) -> dict:
"""Return configuration of this agent.
Returns:
The configuration of current agent.
"""
return {}
def load_memory(self, memory: Sequence[dict]) -> None:
r"""Load input memory."""
def __call__(self, *args: Any, **kwargs: Any) -> dict:
"""Calling the reply function, and broadcast the generated
response to all audiences if needed."""
res = self.reply(*args, **kwargs)
# broadcast to audiences if needed
if self._audience is not None:
self._broadcast_to_audience(res)
return res
def speak(
self,
content: Union[str, Msg],
) -> None:
"""
Speak out the message generated by the agent. If a string is given,
a Msg object will be created with the string as the content.
Args:
content (`Union[str, Msg]`):
The content of the message to be spoken out. If a string is
given, a Msg object will be created with the agent's name, role
as "assistant", and the given string as the content.
"""
if isinstance(content, str):
msg = Msg(
name=self.name,
content=content,
role="assistant",
)
elif isinstance(content, Msg):
msg = content
else:
raise TypeError(
"From version 0.0.5, the speak method only accepts str or Msg "
f"object, got {type(content)} instead.",
)
logger.chat(msg)
def observe(self, x: Union[dict, Sequence[dict]]) -> None:
"""Observe the input, store it in memory without response to it.
Args:
x (`Union[dict, Sequence[dict]]`):
The input message to be recorded in memory.
"""
if self.memory:
self.memory.add(x)
def reset_audience(self, audience: Sequence[AgentBase]) -> None:
"""Set the audience of this agent, which means if this agent
generates a response, it will be passed to all audiences.
Args:
audience (`Sequence[AgentBase]`):
The audience of this agent, which will be notified when this
agent generates a response message.
"""
# TODO: we leave the consideration of nested msghub for future.
# for now we suppose one agent can only be in one msghub
self._audience = [_ for _ in audience if _ != self]
def clear_audience(self) -> None:
"""Remove the audience of this agent."""
# TODO: we leave the consideration of nested msghub for future.
# for now we suppose one agent can only be in one msghub
self._audience = None
def rm_audience(
self,
audience: Union[Sequence[AgentBase], AgentBase],
) -> None:
"""Remove the given audience from the Sequence"""
if not isinstance(audience, Sequence):
audience = [audience]
for agent in audience:
if self._audience is not None and agent in self._audience:
self._audience.pop(self._audience.index(agent))
else:
logger.warning(
f"Skip removing agent [{agent.name}] from the "
f"audience for its inexistence.",
)
def _broadcast_to_audience(self, x: dict) -> None:
"""Broadcast the input to all audiences."""
for agent in self._audience:
agent.observe(x)
@property
def agent_id(self) -> str:
"""The unique id of this agent.
Returns:
str: agent_id
"""
return self._agent_id
def to_dist(
self,
host: str = "localhost",
port: int = None,
max_pool_size: int = 8192,
max_timeout_seconds: int = 1800,
local_mode: bool = True,
lazy_launch: bool = True,
launch_server: bool = None,
) -> AgentBase:
"""Convert current agent instance into a distributed version.
Args:
host (`str`, defaults to `"localhost"`):
Hostname of the rpc agent server.
port (`int`, defaults to `None`):
Port of the rpc agent server.
max_pool_size (`int`, defaults to `8192`):
Only takes effect when `host` and `port` are not filled in.
The max number of agent reply messages that the started agent
server can accommodate. Note that the oldest message will be
deleted after exceeding the pool size.
max_timeout_seconds (`int`, defaults to `1800`):
Only takes effect when `host` and `port` are not filled in.
Maximum time for reply messages to be cached in the launched
agent server. Note that expired messages will be deleted.
local_mode (`bool`, defaults to `True`):
Only takes effect when `host` and `port` are not filled in.
Whether the started agent server only listens to local
requests.
lazy_launch (`bool`, defaults to `True`):
Only takes effect when `host` and `port` are not filled in.
If `True`, launch the agent server when the agent is called,
otherwise, launch the agent server immediately.
launch_server(`bool`, defaults to `None`):
This field has been deprecated and will be removed in
future releases.
Returns:
`AgentBase`: the wrapped agent instance with distributed
functionality
"""
from .rpc_agent import RpcAgent
if issubclass(self.__class__, RpcAgent):
return self
if launch_server is not None:
logger.warning(
"`launch_server` has been deprecated and will be removed in "
"future releases. When `host` and `port` is not provided, the "
"agent server will be launched automatically.",
)
return RpcAgent(
name=self.name,
agent_class=self.__class__,
agent_configs=self._init_settings,
host=host,
port=port,
max_pool_size=max_pool_size,
max_timeout_seconds=max_timeout_seconds,
local_mode=local_mode,
lazy_launch=lazy_launch,
agent_id=self.agent_id,
)

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""A general dialog agent."""
from typing import Optional, Union, Sequence
from ..message import Msg
from .agent import AgentBase
class DialogAgent(AgentBase):
"""A simple agent used to perform a dialogue. Your can set its role by
`sys_prompt`."""
def __init__(
self,
name: str,
sys_prompt: str,
model_config_name: str,
use_memory: bool = True,
memory_config: Optional[dict] = None,
) -> None:
"""Initialize the dialog agent.
Arguments:
name (`str`):
The name of the agent.
sys_prompt (`Optional[str]`):
The system prompt of the agent, which can be passed by args
or hard-coded in the agent.
model_config_name (`str`):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`):
The config of memory.
"""
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
use_memory=use_memory,
memory_config=memory_config,
)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""Reply function of the agent. Processes the input data,
generates a prompt using the current dialogue memory and system
prompt, and invokes the language model to produce a response. The
response is then formatted and added to the dialogue memory.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
Returns:
`Msg`: The output message generated by the agent.
"""
# record the input if needed
if self.memory:
self.memory.add(x)
# prepare prompt
prompt = self.model.format(
Msg("system", self.sys_prompt, role="system"),
self.memory
and self.memory.get_memory()
or x, # type: ignore[arg-type]
)
# call llm and generate response
response = self.model(prompt).text
msg = Msg(self.name, response, role="assistant")
# Print/speak the message in this agent's voice
self.speak(msg)
# Record the message in memory
if self.memory:
self.memory.add(msg)
return msg

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""An agent that replies in a dictionary format."""
from typing import Optional, Union, Sequence
from ..message import Msg
from .agent import AgentBase
from ..parsers import ParserBase
class DictDialogAgent(AgentBase):
"""An agent that generates response in a dict format, where user can
specify the required fields in the response via specifying the parser
About parser, please refer to our
[tutorial](https://modelscope.github.io/agentscope/en/tutorial/203-parser.html)
For usage example, please refer to the example of werewolf in
`examples/game_werewolf`"""
def __init__(
self,
name: str,
sys_prompt: str,
model_config_name: str,
use_memory: bool = True,
memory_config: Optional[dict] = None,
max_retries: Optional[int] = 3,
) -> None:
"""Initialize the dict dialog agent.
Arguments:
name (`str`):
The name of the agent.
sys_prompt (`Optional[str]`, defaults to `None`):
The system prompt of the agent, which can be passed by args
or hard-coded in the agent.
model_config_name (`str`, defaults to None):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`, defaults to `None`):
The config of memory.
max_retries (`Optional[int]`, defaults to `None`):
The maximum number of retries when failed to parse the model
output.
""" # noqa
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
use_memory=use_memory,
memory_config=memory_config,
)
self.parser = None
self.max_retries = max_retries
def set_parser(self, parser: ParserBase) -> None:
"""Set response parser, which will provide 1) format instruction; 2)
response parsing; 3) filtering fields when returning message, storing
message in memory. So developers only need to change the
parser, and the agent will work as expected.
"""
self.parser = parser
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""Reply function of the agent.
Processes the input data, generates a prompt using the current
dialogue memory and system prompt, and invokes the language
model to produce a response. The response is then formatted
and added to the dialogue memory.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
Returns:
`Msg`: The output message generated by the agent.
Raises:
`json.decoder.JSONDecodeError`:
If the response from the language model is not valid JSON,
it defaults to treating the response as plain text.
"""
# record the input if needed
if self.memory:
self.memory.add(x)
# prepare prompt
prompt = self.model.format(
Msg("system", self.sys_prompt, role="system"),
self.memory
and self.memory.get_memory()
or x, # type: ignore[arg-type]
Msg("system", self.parser.format_instruction, "system"),
)
# call llm
res = self.model(
prompt,
parse_func=self.parser.parse,
max_retries=self.max_retries,
)
# Filter the parsed response by keys for storing in memory, returning
# in the reply function, and feeding into the metadata field in the
# returned message object.
self.memory.add(
Msg(self.name, self.parser.to_memory(res.parsed), "assistant"),
)
msg = Msg(
self.name,
content=self.parser.to_content(res.parsed),
role="assistant",
metadata=self.parser.to_metadata(res.parsed),
)
self.speak(msg)
return msg

View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""A common base class for AgentBase and PipelineBase"""
from abc import ABC
from abc import abstractmethod
from typing import Any
class Operator(ABC):
"""
Abstract base class `Operator` defines a protocol for classes that
implement callable behavior.
The class is designed to be subclassed with an overridden `__call__`
method that specifies the execution logic for the operator.
"""
@abstractmethod
def __call__(self, *args: Any, **kwargs: Any) -> dict:
"""Calling function"""

View File

@@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
"""
This example shows how to build an agent with RAG
with LlamaIndex.
Notice, this is a Beta version of RAG agent.
"""
from typing import Any, Optional, Union, Sequence
from loguru import logger
from agentscope.agents.agent import AgentBase
from agentscope.message import Msg
from agentscope.rag import Knowledge
CHECKING_PROMPT = """
Is the retrieved content relevant to the query?
Retrieved content: {}
Query: {}
Only answer YES or NO.
"""
class LlamaIndexAgent(AgentBase):
"""
A LlamaIndex agent build on LlamaIndex.
"""
def __init__(
self,
name: str,
sys_prompt: str,
model_config_name: str,
knowledge_list: list[Knowledge] = None,
knowledge_id_list: list[str] = None,
similarity_top_k: int = None,
log_retrieval: bool = True,
recent_n_mem_for_retrieve: int = 1,
**kwargs: Any,
) -> None:
"""
Initialize the RAG LlamaIndexAgent
Args:
name (str):
the name for the agent
sys_prompt (str):
system prompt for the RAG agent
model_config_name (str):
language model for the agent
knowledge_list (list[Knowledge]):
a list of knowledge.
User can choose to pass a list knowledge object
directly when initializing the RAG agent. Another
choice can be passing a list of knowledge ids and
obtain the knowledge with the `equip` function of a
knowledge bank.
knowledge_id_list (list[Knowledge]):
a list of id of the knowledge.
This is designed for easy setting up multiple RAG
agents with a config file. To obtain the knowledge
objects, users can pass this agent to the `equip`
function in a knowledge bank to add corresponding
knowledge to agent's self.knowledge_list.
similarity_top_k (int):
the number of most similar data blocks retrieved
from each of the knowledge
log_retrieval (bool):
whether to print the retrieved content
recent_n_mem_for_retrieve (int):
the number of pieces of memory used as part of
retrival query
"""
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
)
self.knowledge_list = knowledge_list or []
self.knowledge_id_list = knowledge_id_list or []
self.similarity_top_k = similarity_top_k
self.log_retrieval = log_retrieval
self.recent_n_mem_for_retrieve = recent_n_mem_for_retrieve
self.description = kwargs.get("description", "")
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""
Reply function of the RAG agent.
Processes the input data,
1) use the input data to retrieve with RAG function;
2) generates a prompt using the current memory and system
prompt;
3) invokes the language model to produce a response. The
response is then formatted and added to the dialogue memory.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
Returns:
`Msg`: The output message generated by the agent.
"""
retrieved_docs_to_string = ""
# record the input if needed
if self.memory:
self.memory.add(x)
# in case no input is provided (e.g., in msghub),
# use the memory as query
history = self.memory.get_memory(
recent_n=self.recent_n_mem_for_retrieve,
)
query = (
"/n".join(
[msg["content"] for msg in history],
)
if isinstance(history, list)
else str(history)
)
elif x is not None:
query = x.content
else:
query = ""
if len(query) > 0:
# when content has information, do retrieval
scores = []
for knowledge in self.knowledge_list:
retrieved_nodes = knowledge.retrieve(
str(query),
self.similarity_top_k,
)
for node in retrieved_nodes:
scores.append(node.score)
retrieved_docs_to_string += (
"\n>>>> score:"
+ str(node.score)
+ "\n>>>> source:"
+ str(node.node.get_metadata_str())
+ "\n>>>> content:"
+ node.get_content()
)
if self.log_retrieval:
self.speak("[retrieved]:" + retrieved_docs_to_string)
if max(scores) < 0.4:
# if the max score is lower than 0.4, then we let LLM
# decide whether the retrieved content is relevant
# to the user input.
msg = Msg(
name="user",
role="user",
content=CHECKING_PROMPT.format(
retrieved_docs_to_string,
query,
),
)
msg = self.model.format(msg)
checking = self.model(msg)
logger.info(checking)
checking = checking.text.lower()
if "no" in checking:
retrieved_docs_to_string = "EMPTY"
# prepare prompt
prompt = self.model.format(
Msg(
name="system",
role="system",
content=self.sys_prompt,
),
# {"role": "system", "content": retrieved_docs_to_string},
self.memory.get_memory(
recent_n=self.recent_n_mem_for_retrieve,
),
Msg(
name="user",
role="user",
content="Context: " + retrieved_docs_to_string,
),
)
# call llm and generate response
response = self.model(prompt).text
msg = Msg(self.name, response)
# Print/speak the message in this agent's voice
self.speak(msg)
if self.memory:
# Record the message in memory
self.memory.add(msg)
return msg

View File

@@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
"""An agent class that implements the ReAct algorithm. The agent will reason
and act iteratively to solve problems. More details can be found in the paper
https://arxiv.org/abs/2210.03629.
"""
from typing import Any, Optional, Union, Sequence
from loguru import logger
from agentscope.exception import ResponseParsingError, FunctionCallError
from agentscope.agents import AgentBase
from agentscope.message import Msg
from agentscope.parsers import MarkdownJsonDictParser
from agentscope.service import ServiceToolkit
from agentscope.service.service_toolkit import ServiceFunction
INSTRUCTION_PROMPT = """## Your Role:
You are a powerful AI agent and specifically aimed to use C# and Unity to create new Game Contents, which in the game project are named as 'Gurouce'.
Gurouce is refer to anything that can be USED to make some influence in game, and your main job is follow user's instruction to create new game contents,
This game is a 2D game in Unity with C# scripts.
## What You Should Do:
0. Check user's instruction, make sure you know where the game project, and user's instruction is rational and can be realize. Otherwize, just finish.
1. To realize a new Gurouce, you should first analyze the user's task, to answer three main questions:
A. How the Gurouce find the target to influence?
B. At what situation the Gurouce will start to influence?
C. How the Gurouce influence the target?
2. Then, open and read the source file, then inherit from them and override their functions to realize the three main questions:
A. BaseFindTarget.cs
B. BaseStartCondition.cs
C. BaseInfluence.cs
You should open and read the example Gurouce to learn how to use. The example Gurouce is RockGurouce, when using it, it will create a "Rock" Gurouce and fly from the game character towards where the mouse pointer is, and cause damage to the target it hits, then destory itself.
3. Since you have to write some source files, make sure each file implement their job functionality, and all three questions above are anwsered well.
3. Next, Call "compile" function, it will compile the source files and update game contents.
4. If failed to compile, follow the error message and edit again, otherwise finish.
## Note:
1. Fully understand the tool functions and their arguments before using them.
2. Check the folder structure and relevant scripts before write any code.
3. Your EVERY response MUST have at least one function call, and the function call must be in the "function" field.
4. Make sure the types and values of the arguments you provided to the tool functions are correct.
5. Don't take things for granted. For example, where you are, what's the time now, etc. You can try to use the tool functions to get information.
6. If the function execution fails, you should analyze the error and try to solve it.
7. You don't have to finish the whole task quickly, we will ask your several times, so follow the "What You Should Do" and where we have done, to respond once a step.
## Resources:
1. The tool functions you can use.
2. A Specific Unity game project.
## Constraint:
1. You should not use any other tool functions.
2. There is a folder path 'Assets/HotUpdate/Gurouce/', you must create a subfolder in it, and you can only edit within the subfolder. But you can read any other file if need. BTW, the base scripts are in 'BaseScripts' subfolder and RockGurouce is in 'RockGurouce' subfolder.
3. You have to decide a Gurouce name, and you have to change "Base" to the Gurouce name you decided when you write the source files' names.
""" # noqa
class ReActAgent(AgentBase):
"""An agent class that implements the ReAct algorithm. More details refer
to https://arxiv.org/abs/2210.03629.
Note this is an example implementation of ReAct algorithm in AgentScope.
We follow the idea within the paper, but the detailed prompt engineering
maybe different. Developers are encouraged to modify the prompt to fit
their own needs.
"""
def __init__(
self,
name: str,
model_config_name: str,
service_toolkit: ServiceToolkit = None,
sys_prompt: str = "You're a helpful assistant. Your name is {name}.",
max_iters: int = 10,
verbose: bool = True,
**kwargs: Any,
) -> None:
"""Initialize the ReAct agent with the given name, model config name
and tools.
Args:
name (`str`):
The name of the agent.
sys_prompt (`str`):
The system prompt of the agent.
model_config_name (`str`):
The name of the model config, which is used to load model from
configuration.
service_toolkit (`ServiceToolkit`):
A `ServiceToolkit` object that contains the tool functions.
max_iters (`int`, defaults to `10`):
The maximum number of iterations of the reasoning-acting loops.
verbose (`bool`, defaults to `True`):
Whether to print the detailed information during reasoning and
acting steps. If `False`, only the content in speak field will
be print out.
"""
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
)
# TODO: To compatible with the old version, which will be deprecated
# soon
if "tools" in kwargs:
logger.warning(
"The argument `tools` will be deprecated soon. "
"Please use `service_toolkit` instead. Example refers to "
"https://github.com/modelscope/agentscope/blob/main/"
"examples/conversation_with_react_agent/code/"
"conversation_with_react_agent.py",
)
service_funcs = {}
for func, json_schema in kwargs["tools"]:
name = json_schema["function"]["name"]
service_funcs[name] = ServiceFunction(
name=name,
original_func=func,
processed_func=func,
json_schema=json_schema,
)
if service_toolkit is None:
service_toolkit = ServiceToolkit()
service_toolkit.service_funcs = service_funcs
else:
service_toolkit.service_funcs.update(service_funcs)
elif service_toolkit is None:
raise ValueError(
"The argument `service_toolkit` is required to initialize "
"the ReActAgent.",
)
self.service_toolkit = service_toolkit
self.verbose = verbose
self.max_iters = max_iters
if not sys_prompt.endswith("\n"):
sys_prompt = sys_prompt + "\n"
self.sys_prompt = "\n".join(
[
# The brief intro of the role and target
sys_prompt.format(name=self.name),
# The instruction prompt for tools
self.service_toolkit.tools_instruction,
# The detailed instruction prompt for the agent
INSTRUCTION_PROMPT,
],
)
# Put sys prompt into memory
self.memory.add(Msg("system", self.sys_prompt, role="system"))
# Initialize a parser object to formulate the response from the model
self.parser = MarkdownJsonDictParser(
content_hint={
"thought": "what you thought",
"speak": "what you speak",
"function": service_toolkit.tools_calling_format,
},
required_keys=["thought", "speak", "function"],
# Only print the speak field when verbose is False
keys_to_content=True if self.verbose else "speak",
)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""The reply function that achieves the ReAct algorithm.
The more details please refer to https://arxiv.org/abs/2210.03629"""
self.memory.add(x)
for _ in range(self.max_iters):
# Step 1: Thought
if self.verbose:
self.speak(f" ITER {_+1}, STEP 1: REASONING ".center(70, "#"))
# Prepare hint to remind model what the response format is
# Won't be recorded in memory to save tokens
hint_msg = Msg(
"system",
self.parser.format_instruction,
role="system",
echo=self.verbose,
)
# Prepare prompt for the model
prompt = self.model.format(self.memory.get_memory(), hint_msg)
print('====================================Prompt==========================================')
print(prompt)
# Generate and parse the response
try:
res = self.model(
prompt,
parse_func=self.parser.parse,
max_retries=1,
)
print('====================================Result Return==========================================')
print(res)
print('====================================Result Parsed==========================================')
print(res.parsed)
# Record the response in memory
self.memory.add(
Msg(
self.name,
self.parser.to_memory(res.parsed),
"assistant",
),
)
# Print out the response
msg_returned = Msg(
self.name,
self.parser.to_content(res.parsed),
"assistant",
)
print('====================================Speak==========================================')
print(msg_returned)
self.speak(msg_returned)
# Skip the next steps if no need to call tools
# The parsed field is a dictionary
print('====================================Function==========================================')
print(res.parsed["function"])
arg_function = res.parsed["function"]
if (
isinstance(arg_function, str)
and arg_function in ["[]", ""]
or isinstance(arg_function, list)
and len(arg_function) == 0
):
# Only the speak field is exposed to users or other agents
return msg_returned
# Only catch the response parsing error and expose runtime
# errors to developers for debugging
except ResponseParsingError as e:
# Print out raw response from models for developers to debug
response_msg = Msg(self.name, e.raw_response, "assistant")
self.speak(response_msg)
# Re-correct by model itself
error_msg = Msg("system", str(e), "system")
self.speak(error_msg)
self.memory.add([response_msg, error_msg])
# Skip acting step to re-correct the response
continue
# Step 2: Acting
if self.verbose:
self.speak(f" ITER {_+1}, STEP 2: ACTING ".center(70, "#"))
# Parse, check and execute the tool functions in service toolkit
try:
if res.parsed["function"] == "finish":
hint_msg = Msg(
"system",
"You have Finished the task."
"Now generate a reply by summarizing the current "
"situation.",
role="system",
echo=self.verbose,
)
# Generate a reply by summarizing the current situation
prompt = self.model.format(self.memory.get_memory(), hint_msg)
res = self.model(prompt)
res_msg = Msg(self.name, res.text, "assistant")
self.speak(res_msg)
return res_msg
execute_results = self.service_toolkit.parse_and_call_func(
res.parsed["function"],
)
# Note: Observing the execution results and generate response
# are finished in the next reasoning step. We just put the
# execution results into memory, and wait for the next loop
# to generate response.
# Record execution results into memory as system message
msg_res = Msg("system", execute_results, "system")
self.speak(msg_res)
self.memory.add(msg_res)
except FunctionCallError as e:
# Catch the function calling error that can be handled by
# the model
error_msg = Msg("system", str(e), "system")
self.speak(error_msg)
self.memory.add(error_msg)
# Exceed the maximum iterations
hint_msg = Msg(
"system",
"You have failed to generate a response in the maximum "
"iterations. Now generate a reply by summarizing the current "
"situation.",
role="system",
echo=self.verbose,
)
# Generate a reply by summarizing the current situation
prompt = self.model.format(self.memory.get_memory(), hint_msg)
res = self.model(prompt)
res_msg = Msg(self.name, res.text, "assistant")
self.speak(res_msg)
return res_msg

View File

@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
""" Base class for Rpc Agent """
from typing import Type, Optional, Union, Sequence
from agentscope.agents.agent import AgentBase
from agentscope.message import (
PlaceholderMessage,
serialize,
Msg,
)
from agentscope.rpc import RpcAgentClient
from agentscope.server.launcher import RpcAgentServerLauncher
from agentscope.studio._client import _studio_client
class RpcAgent(AgentBase):
"""A wrapper to extend an AgentBase into a gRPC Client."""
def __init__(
self,
name: str,
host: str = "localhost",
port: int = None,
agent_class: Type[AgentBase] = None,
agent_configs: Optional[dict] = None,
max_pool_size: int = 8192,
max_timeout_seconds: int = 1800,
local_mode: bool = True,
lazy_launch: bool = True,
agent_id: str = None,
connect_existing: bool = False,
) -> None:
"""Initialize a RpcAgent instance.
Args:
name (`str`): the name of the agent.
host (`str`, defaults to `localhost`):
Hostname of the rpc agent server.
port (`int`, defaults to `None`):
Port of the rpc agent server.
agent_class (`Type[AgentBase]`):
the AgentBase subclass of the source agent.
agent_configs (`dict`): The args used to
initialize the agent, generated by `_AgentMeta`.
max_pool_size (`int`, defaults to `8192`):
Max number of task results that the server can accommodate.
max_timeout_seconds (`int`, defaults to `1800`):
Timeout for task results.
local_mode (`bool`, defaults to `True`):
Whether the started gRPC server only listens to local
requests.
lazy_launch (`bool`, defaults to `True`):
Only launch the server when the agent is called.
agent_id (`str`, defaults to `None`):
The agent id of this instance. If `None`, it will
be generated randomly.
connect_existing (`bool`, defaults to `False`):
Set to `True`, if the agent is already running on the agent
server.
"""
super().__init__(name=name)
self.agent_class = agent_class
self.agent_configs = agent_configs
self.host = host
self.port = port
self.server_launcher = None
self.client = None
self.connect_existing = connect_existing
if agent_id is not None:
self._agent_id = agent_id
# if host and port are not provided, launch server locally
launch_server = port is None
if launch_server:
self.host = "localhost"
studio_url = None
if _studio_client.active:
studio_url = _studio_client.studio_url
self.server_launcher = RpcAgentServerLauncher(
host=self.host,
port=port,
max_pool_size=max_pool_size,
max_timeout_seconds=max_timeout_seconds,
local_mode=local_mode,
custom_agents=[agent_class],
studio_url=studio_url,
)
if not lazy_launch:
self._launch_server()
else:
self.client = RpcAgentClient(
host=self.host,
port=self.port,
agent_id=self.agent_id,
)
if not self.connect_existing:
self.client.create_agent(agent_configs)
def _launch_server(self) -> None:
"""Launch a rpc server and update the port and the client"""
self.server_launcher.launch()
self.port = self.server_launcher.port
self.client = RpcAgentClient(
host=self.host,
port=self.port,
agent_id=self.agent_id,
)
self.client.create_agent(self.agent_configs)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
if self.client is None:
self._launch_server()
return PlaceholderMessage(
name=self.name,
content=None,
client=self.client,
x=x,
)
def observe(self, x: Union[dict, Sequence[dict]]) -> None:
if self.client is None:
self._launch_server()
self.client.call_func(
func_name="_observe",
value=serialize(x), # type: ignore[arg-type]
)
def clone_instances(
self,
num_instances: int,
including_self: bool = True,
) -> Sequence[AgentBase]:
"""
Clone a series of this instance with different agent_id and
return them as a list.
Args:
num_instances (`int`): The number of instances in the returned
list.
including_self (`bool`): Whether to include the instance calling
this method in the returned list.
Returns:
`Sequence[AgentBase]`: A list of agent instances.
"""
generated_instance_number = (
num_instances - 1 if including_self else num_instances
)
generated_instances = []
# launch the server before clone instances
if self.client is None:
self._launch_server()
# put itself as the first element of the returned list
if including_self:
generated_instances.append(self)
# clone instances without agent server
for _ in range(generated_instance_number):
new_agent_id = self.client.call_func("_clone_agent")
generated_instances.append(
RpcAgent(
name=self.name,
host=self.host,
port=self.port,
agent_id=new_agent_id,
connect_existing=True,
),
)
return generated_instances
def stop(self) -> None:
"""Stop the RpcAgent and the rpc server."""
if self.server_launcher is not None:
self.server_launcher.shutdown()
def __del__(self) -> None:
self.stop()

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""An agent that convert text to image."""
from typing import Optional, Union, Sequence
from loguru import logger
from .agent import AgentBase
from ..message import Msg
class TextToImageAgent(AgentBase):
"""
A agent used to perform text to image tasks.
TODO: change the agent into a service.
"""
def __init__(
self,
name: str,
model_config_name: str,
use_memory: bool = True,
memory_config: Optional[dict] = None,
) -> None:
"""Initialize the text to image agent.
Arguments:
name (`str`):
The name of the agent.
model_config_name (`str`, defaults to None):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`):
The config of memory.
"""
super().__init__(
name=name,
sys_prompt="",
model_config_name=model_config_name,
use_memory=use_memory,
memory_config=memory_config,
)
logger.warning(
"The `TextToImageAgent` will be deprecated in v0.0.6, "
"please use `text_to_image` service and `ReActAgent` instead.",
)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
if self.memory:
self.memory.add(x)
if x is None:
# get the last message from memory
if self.memory and self.memory.size() > 0:
x = self.memory.get_memory()[-1]
else:
return Msg(
self.name,
content="Please provide a text prompt to generate image.",
role="assistant",
)
image_urls = self.model(x.content).image_urls
# TODO: optimize the construction of content
msg = Msg(
self.name,
content="This is the generated image",
role="assistant",
url=image_urls,
)
self.speak(msg)
if self.memory:
self.memory.add(msg)
return msg

View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""User Agent class"""
import time
from typing import Union, Sequence
from typing import Optional
from loguru import logger
from agentscope.agents import AgentBase
from agentscope.studio._client import _studio_client
from agentscope.message import Msg
from agentscope.web.gradio.utils import user_input
class UserAgent(AgentBase):
"""User agent class"""
def __init__(self, name: str = "User", require_url: bool = False) -> None:
"""Initialize a UserAgent object.
Arguments:
name (`str`, defaults to `"User"`):
The name of the agent. Defaults to "User".
require_url (`bool`, defaults to `False`):
Whether the agent requires user to input a URL. Defaults to
False. The URL can lead to a website, a file,
or a directory. It will be added into the generated message
in field `url`.
"""
super().__init__(name=name)
self.name = name
self.require_url = require_url
def reply(
self,
x: Optional[Union[Msg, Sequence[Msg]]] = None,
required_keys: Optional[Union[list[str], str]] = None,
timeout: Optional[int] = None,
) -> Msg:
"""
Processes the input provided by the user and stores it in memory,
potentially formatting it with additional provided details.
The method prompts the user for input, then optionally prompts for
additional specifics based on the provided format keys. All
information is encapsulated in a message object, which is then
added to the object's memory.
Arguments:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
required_keys \
(`Optional[Union[list[str], str]]`, defaults to `None`):
Strings that requires user to input, which will be used as
the key of the returned dict. Defaults to None.
timeout (`Optional[int]`, defaults to `None`):
Raise `TimeoutError` if user exceed input time, set to None
for no limit.
Returns:
`Msg`: The output message generated by the agent.
"""
if self.memory:
self.memory.add(x)
if _studio_client.active:
logger.info(
f"Waiting for input from:\n\n"
f" * {_studio_client.get_run_detail_page_url()}\n",
)
raw_input = _studio_client.get_user_input(
agent_id=self.agent_id,
name=self.name,
require_url=self.require_url,
required_keys=required_keys,
)
print("Python: receive ", raw_input)
content = raw_input["content"]
url = raw_input["url"]
kwargs = {}
else:
# TODO: To avoid order confusion, because `input` print much
# quicker than logger.chat
time.sleep(0.5)
content = user_input(timeout=timeout)
kwargs = {}
if required_keys is not None:
if isinstance(required_keys, str):
required_keys = [required_keys]
for key in required_keys:
kwargs[key] = input(f"{key}: ")
# Input url of file, image, video, audio or website
url = None
if self.require_url:
url = input("URL (or Enter to skip): ")
if url == "":
url = None
# Add additional keys
msg = Msg(
name=self.name,
role="user",
content=content,
url=url,
**kwargs, # type: ignore[arg-type]
)
self.speak(msg)
# Add to memory
if self.memory:
self.memory.add(msg)
return msg
def speak(
self,
content: Union[str, Msg],
) -> None:
"""
Speak out the message generated by the agent. If a string is given,
a Msg object will be created with the string as the content.
Args:
content (`Union[str, Msg]`):
The content of the message to be spoken out. If a string is
given, a Msg object will be created with the agent's name, role
as "user", and the given string as the content.
"""
if isinstance(content, str):
msg = Msg(
name=self.name,
content=content,
role="assistant",
)
_studio_client.push_message(msg)
elif isinstance(content, Msg):
msg = content
else:
raise TypeError(
"From version 0.0.5, the speak method only accepts str or Msg "
f"object, got {type(content)} instead.",
)
logger.chat(msg, disable_gradio=True)