From aeee5e271383563c2dfba717e4d50e31522f0868 Mon Sep 17 00:00:00 2001 From: Yx Jiang <2237303+yxjiang@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:39:36 -0700 Subject: [PATCH 1/5] Create an abstract llm tool class --- polymind/__init__.py | 2 +- polymind/tools/{oai_tool.py => llm_tool.py} | 89 +++++++++++++++++-- polymind/tools/tool_management_tool.py | 15 +++- .../{test_oai_tools.py => test_llm_tools.py} | 4 +- .../tools/test_tool_management_tool.py | 2 +- tests/test_data/tool_index/tool_profiles.json | 20 ++--- 6 files changed, 109 insertions(+), 23 deletions(-) rename polymind/tools/{oai_tool.py => llm_tool.py} (69%) rename tests/polymind/tools/{test_oai_tools.py => test_llm_tools.py} (97%) diff --git a/polymind/__init__.py b/polymind/__init__.py index ec2e125..1cac021 100644 --- a/polymind/__init__.py +++ b/polymind/__init__.py @@ -6,4 +6,4 @@ from .core.tool import BaseTool # Expose the tools -from .tools.oai_tool import OpenAIChatTool +from .tools.llm_tool import OpenAIChatTool diff --git a/polymind/tools/oai_tool.py b/polymind/tools/llm_tool.py similarity index 69% rename from polymind/tools/oai_tool.py rename to polymind/tools/llm_tool.py index f8d82ff..c734c72 100644 --- a/polymind/tools/oai_tool.py +++ b/polymind/tools/llm_tool.py @@ -4,6 +4,7 @@ """ import os +from abc import ABC, abstractmethod from typing import List import numpy as np @@ -16,7 +17,66 @@ from polymind.tools.rest_api_tool import RestAPITool -class OpenAIChatTool(BaseTool): +class LLMTool(BaseTool, ABC): + """LLM tool defines the basic properties of the language model tools.""" + + max_tokens: int = Field(..., description="The maximum number of tokens for the chat.") + temperature: float = Field(default=1.0, description="The temperature for the chat.") + top_p: float = Field( + default=0.1, + description="The top p for the chat. Top p is used to prevent the model from generating unlikely words.", + ) + stop: str = Field(default=None, description="The stop sequence for the chat.") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._set_client() + + @abstractmethod + def _set_client(self): + """Set the client for the language model.""" + pass + + @abstractmethod + async def _invoke(self, input: Message) -> Message: + """Invoke the language model with the input message and return the response message. + + Args: + input (Message): The input message to the language model. The message should contain the below keys: + - prompt: The prompt for the chat. + - system_prompt: The system prompt for the chat. + - max_tokens: The maximum number of tokens for the chat. + - temperature: The temperature for the chat. + - top_p: The top p for the chat. + - stop: The stop sequence for the chat. + """ + pass + + async def _execute(self, input: Message) -> Message: + """Execute the tool and return the result. + The input message should contain a "prompt" and optionally a "system_prompt". + """ + + # Validate the input message. + prompt = input.get("prompt", "") + system_prompt = input.get("system_prompt", self.system_prompt) + if not prompt: + raise ValueError("Prompt cannot be empty.") + input.content.update( + { + "max_tokens": self.max_tokens, + "temperature": self.temperature, + "top_p": self.top_p, + } + ) + if self.stop: + input.content["stop"] = self.stop + + response_message = await self._invoke(input) + return response_message + + +class OpenAIChatTool(LLMTool): """OpenAITool is a bridge to OpenAI APIs. The tool can be initialized with llm_name, system_prompt, max_tokens, and temperature. The input message of this tool should contain a "prompt", and optionally a "system_prompt". @@ -40,9 +100,10 @@ class OpenAIChatTool(BaseTool): system_prompt: str = Field(default="You are a helpful AI assistant.") max_tokens: int = Field(default=1500) temperature: float = Field(default=0.7) + stop: str = Field(default=None) - def __init__(self, **kwargs): - super().__init__(**kwargs) + def _set_client(self): + """Set the client for the language model.""" self.client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) def input_spec(self) -> List[Param]: @@ -83,20 +144,28 @@ def output_spec(self) -> List[Param]: ), ] - async def _execute(self, input: Message) -> Message: + async def _invoke(self, input: Message) -> Message: """Execute the tool and return the result. The derived class must implement this method to define the behavior of the tool. Args: - input (Message): The input to the tool carried in a message. + input (Message): The input to the tool carried in a message. The message should contain the below keys: + - prompt: The prompt for the chat. + - system_prompt: The system prompt for the chat. + - max_tokens: The maximum number of tokens for the chat. + - temperature: The temperature for the chat. + - top_p: The top p for the chat. + - stop: The stop sequence for the chat. Returns: Message: The result of the tool carried in a message. """ prompt = input.get("prompt", "") - system_prompt = input.get("system_prompt", self.system_prompt) - if not prompt: - raise ValueError("Prompt cannot be empty.") + system_prompt = input.get("system_prompt", "") + temperature = input.get("temperature", self.temperature) + max_tokens = input.get("max_tokens", self.max_tokens) + top_p = input.get("top_p", self.top_p) + stop = input.get("stop", self.stop) response = await self.client.chat.completions.create( model=self.llm_name, @@ -104,6 +173,10 @@ async def _execute(self, input: Message) -> Message: {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}, ], + temperature=temperature, + max_tokens=max_tokens, + top_p=top_p, + stop=stop, ) content = response.choices[0].message.content response_message = Message(content={"response": content}) diff --git a/polymind/tools/tool_management_tool.py b/polymind/tools/tool_management_tool.py index 6282c42..ce1590b 100644 --- a/polymind/tools/tool_management_tool.py +++ b/polymind/tools/tool_management_tool.py @@ -10,7 +10,7 @@ from polymind.core.indexer import Indexer from polymind.core.message import Message from polymind.core.tool import BaseTool, Param -from polymind.tools.oai_tool import OpenAIEmbeddingTool +from polymind.tools.llm_tool import OpenAIEmbeddingTool class ToolIndexer(Indexer): @@ -193,3 +193,16 @@ async def _execute(self, input_message: Message) -> Message: embedding = await self.embedder._embedding([requirement]) candidates = self._find_top_k_candidates(embedding) return Message(content={"candidates": candidates}) + + +class ToolCreator(BaseTool): + tool_name: str = "tool-creator" + + descriptions: List[str] = [ + "ToolCreator is a tool to generate the tool code based on the requirement", + "ToolCreator is a tool to create the tool.", + "ToolCreator is a codegen tool to generate the tool code based on the requirement.", + ] + + async def _execute(self, input_message: Message) -> Message: + requirement = input_message.content["requirement"] diff --git a/tests/polymind/tools/test_oai_tools.py b/tests/polymind/tools/test_llm_tools.py similarity index 97% rename from tests/polymind/tools/test_oai_tools.py rename to tests/polymind/tools/test_llm_tools.py index 9a9b0ad..ed653bf 100644 --- a/tests/polymind/tools/test_oai_tools.py +++ b/tests/polymind/tools/test_llm_tools.py @@ -1,6 +1,6 @@ """Tests for OpenAIChatTool. Run the test with the following command: - poetry run pytest tests/polymind/tools/test_oai_tool.py + poetry run pytest tests/polymind/tools/test_llm_tools.py """ import json @@ -11,7 +11,7 @@ from aioresponses import aioresponses from polymind.core.message import Message -from polymind.tools.oai_tool import OpenAIChatTool, OpenAIEmbeddingTool +from polymind.tools.llm_tool import OpenAIChatTool, OpenAIEmbeddingTool from polymind.tools.rest_api_tool import RestAPITool diff --git a/tests/polymind/tools/test_tool_management_tool.py b/tests/polymind/tools/test_tool_management_tool.py index 2f09e47..75b26d6 100644 --- a/tests/polymind/tools/test_tool_management_tool.py +++ b/tests/polymind/tools/test_tool_management_tool.py @@ -13,7 +13,7 @@ import pytest from polymind.core.message import Message -from polymind.tools.oai_tool import OpenAIEmbeddingTool +from polymind.tools.llm_tool import OpenAIEmbeddingTool from polymind.tools.tool_management_tool import ToolIndexer, ToolRetriever diff --git a/tests/test_data/tool_index/tool_profiles.json b/tests/test_data/tool_index/tool_profiles.json index 88ff06e..d2c5cfe 100644 --- a/tests/test_data/tool_index/tool_profiles.json +++ b/tests/test_data/tool_index/tool_profiles.json @@ -102,7 +102,7 @@ "This tool can be used to generate the response from the chat.", "This tool can be used to generate the code of new tools." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "open-ai-chat", @@ -113,7 +113,7 @@ "This tool can be used to generate the response from the chat.", "This tool can be used to generate the code of new tools." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "open-ai-chat", @@ -124,7 +124,7 @@ "This tool can be used to generate the response from the chat.", "This tool can be used to generate the code of new tools." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "open-ai-chat", @@ -135,7 +135,7 @@ "This tool can be used to generate the response from the chat.", "This tool can be used to generate the code of new tools." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "open-ai-chat", @@ -146,7 +146,7 @@ "This tool can be used to generate the response from the chat.", "This tool can be used to generate the code of new tools." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "openai-embedding", @@ -157,7 +157,7 @@ "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-small model.", "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-large model." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "openai-embedding", @@ -168,7 +168,7 @@ "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-small model.", "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-large model." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "openai-embedding", @@ -179,7 +179,7 @@ "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-small model.", "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-large model." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "openai-embedding", @@ -190,7 +190,7 @@ "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-small model.", "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-large model." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" }, { "tool_name": "openai-embedding", @@ -201,6 +201,6 @@ "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-small model.", "This tool can be used to generate the embedding for the input using OpenAI's text-embedding-3-large model." ], - "file_name": "oai_tool.py" + "file_name": "llm_tool.py" } ] \ No newline at end of file From ddb8a90561a27b663c34c84bb0d9874b695476c0 Mon Sep 17 00:00:00 2001 From: Yx Jiang <2237303+yxjiang@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:39:49 -0700 Subject: [PATCH 2/5] Create an abstract llm tool class --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 62a5983..f60b98a 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,5 @@ scripts # Polymind learned facts and tools knowledge/facts/** -knowledge/tools/** \ No newline at end of file +knowledge/tools/** +use_cases \ No newline at end of file From 7b3364a9c332485fe8c785805eafc7745f231466 Mon Sep 17 00:00:00 2001 From: Yx Jiang <2237303+yxjiang@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:42:28 -0700 Subject: [PATCH 3/5] Create tool creator --- polymind/tools/llm_tool.py | 20 ++- polymind/tools/tool_management_tool.py | 181 ++++++++++++++++++++++++- tests/polymind/tools/test_llm_tools.py | 18 +++ 3 files changed, 215 insertions(+), 4 deletions(-) diff --git a/polymind/tools/llm_tool.py b/polymind/tools/llm_tool.py index c734c72..3da99f4 100644 --- a/polymind/tools/llm_tool.py +++ b/polymind/tools/llm_tool.py @@ -126,6 +126,24 @@ def input_spec(self) -> List[Param]: example="hello, how are you?", description="The prompt for the chat.", ), + Param( + name="max_tokens", + type="int", + example="1500", + description="The maximum number of tokens for the chat.", + ), + Param( + name="temperature", + type="float", + example="0.7", + description="The temperature for the chat.", + ), + Param( + name="top_p", + type="float", + example="0.1", + description="The top p for the chat.", + ), ] def output_spec(self) -> List[Param]: @@ -161,7 +179,7 @@ async def _invoke(self, input: Message) -> Message: Message: The result of the tool carried in a message. """ prompt = input.get("prompt", "") - system_prompt = input.get("system_prompt", "") + system_prompt = input.get("system_prompt", self.system_prompt) temperature = input.get("temperature", self.temperature) max_tokens = input.get("max_tokens", self.max_tokens) top_p = input.get("top_p", self.top_p) diff --git a/polymind/tools/tool_management_tool.py b/polymind/tools/tool_management_tool.py index ce1590b..26fdd2e 100644 --- a/polymind/tools/tool_management_tool.py +++ b/polymind/tools/tool_management_tool.py @@ -1,5 +1,6 @@ import json import os +import re from typing import Any, Dict, List import faiss @@ -10,7 +11,8 @@ from polymind.core.indexer import Indexer from polymind.core.message import Message from polymind.core.tool import BaseTool, Param -from polymind.tools.llm_tool import OpenAIEmbeddingTool +from polymind.tools.llm_tool import (LLMTool, OpenAIChatTool, + OpenAIEmbeddingTool) class ToolIndexer(Indexer): @@ -36,7 +38,7 @@ def _extra_input_spec(self) -> List[Param]: example="rest-api-tool", ), Param( - name="desscriptions", + name="descriptions", type="List[str]", description="The descriptions of the tool to be indexed.", example="""[ @@ -198,11 +200,184 @@ async def _execute(self, input_message: Message) -> Message: class ToolCreator(BaseTool): tool_name: str = "tool-creator" + llm_tool: LLMTool = Field(default=None, description="The LLM tool to generate the tool code.") + descriptions: List[str] = [ "ToolCreator is a tool to generate the tool code based on the requirement", "ToolCreator is a tool to create the tool.", "ToolCreator is a codegen tool to generate the tool code based on the requirement.", ] + learned_tool_folder: str = Field(default="./knowledge/tools", description="The folder to store the learned tools.") + + system_prompt: str = Field( + default=""" + You are a code generator. You are given a requirement to create a tool. + The generated tool should inherit the below BaseTool class. + Please put your answer into a ```python``` code block. + + --- python below --- + # File: {{tool_name}}.py + + class BaseTool(BaseModel, ABC): + '''The base class of the tool. + In an agent system, a tool is an object that can be used to perform a task. + For example, search for information from the internet, query a database, + or perform a calculation. + ''' + + tool_name: str = Field(..., description="The name of the tool.") + descriptions: List[str] = Field( + ..., + min_length=3, + description='''The descriptions of the tool. The descriptions will be + converted to embeddings and used to index the tool. One good practice is to + describe the tools with the following aspects: what the tool does, and describe + the tools from different perspectives. + ''', + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + load_dotenv(override=True) + + def __str__(self): + return self.tool_name + + @field_validator("tool_name") + def check_tool_name(cls, v: str) -> str: + if not v: + raise ValueError("The tool_name must not be empty.") + return v + + @field_validator("descriptions") + def check_descriptions(cls, v: List[str]) -> List[str]: + if len(v) < 3: + raise ValueError("The descriptions must have at least 3 items. The more the better.") + return v + + def get_descriptions(self) -> List[str]: + return self.descriptions + + async def __call__(self, input: Message) -> Message: + '''Makes the instance callable, delegating to the execute method. + This allows the instance to be used as a callable object, simplifying the execution of the tool. + + Args: + input (Message): The input message to the tool. + + Returns: + Message: The output message from the tool. + ''' + return await self._execute(input) + + def get_spec(self) -> str: + '''Return the input and output specification of the tool. + + Returns: + Tuple[List[Param], List[Param]]: The input and output specification of the tool. + ''' + input_json_obj = [] + for param in self.input_spec(): + input_json_obj.append(param.to_json_obj()) + output_json_obj = [] + for param in self.output_spec(): + output_json_obj.append(param.to_json_obj()) + spec_json_obj = { + "input_message": input_json_obj, + "output_message": output_json_obj, + } + return json.dumps(spec_json_obj, indent=4) + + @abstractmethod + def input_spec(self) -> List[Param]: + '''Return the specification of the input parameters.''' + pass + + @abstractmethod + def output_spec(self) -> List[Param]: + '''Return the specification of the output parameters.''' + pass + + @abstractmethod + async def _execute(self, input: Message) -> Message: + '''Execute the tool and return the result. + The derived class must implement this method to define the behavior of the tool. + + Args: + input (Message): The input to the tool carried in a message. + + Returns: + Message: The result of the tool carried in a message. + ''' + pass + --- python above --- + + In addition to any needed package, the implementation should also import the below packages: + + from polymind.core.message import Message + from polymind.core.tool import BaseTool, Param + + """, + description="The system prompt to generate the tool code.", + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not self.llm_tool: + self.llm_tool = OpenAIChatTool(llm_name="gpt-3.5-turbo", system_prompt=self.system_prompt) + + def input_spec(self) -> List[Param]: + return [ + Param( + name="requirement", + type="str", + description="Text-based requirement description for tool creation.", + example="I need a tool to call REST API.", + ) + ] + + def output_spec(self) -> List[Param]: + return [ + Param( + name="status", + type="str", + description="Whether the tool is successfully created and indexed.", + example="success", + ) + ] + async def _execute(self, input_message: Message) -> Message: - requirement = input_message.content["requirement"] + """Leverage the LLM tool to generate the tool code based on the requirement. + + Args: + input_message (Message): The input message containing the requirement. + The message should contain the "requirement" key. + """ + requirement = input_message.content.get("requirement", "") + if not requirement: + raise ValueError("Requirement not provided.") + input_message = Message(content={"prompt": requirement, "system_prompt": self.system_prompt}) + tool_code = await self.llm_tool(input=input_message) + + # Check the ```python``` code block in the tool_code and extract the source code. + tool_code = tool_code.content["response"] + tool_code = re.search(r"```python(.*?)```", tool_code, re.DOTALL) + if not tool_code: + raise ValueError("Tool code not found.") + tool_code = tool_code.group(1).strip() + # Save the tool code to a file. + tool_name = re.search(r"class\s+(.*?)\(", tool_code).group(1) + if not tool_name: + raise ValueError("Tool name not found.") + tool_name = tool_name.strip().lower() + tool_file_name = f"{tool_name}.py" + tool_file_path = os.path.join(self.learned_tool_folder, "test_learned", tool_file_name) + + if not os.path.exists(os.path.dirname(tool_file_path)): + os.makedirs(os.path.dirname(tool_file_path), exist_ok=True) + with open(tool_file_path, "w") as f: + f.write(tool_code) + + # Output message + return Message(content={"status": "success"}) diff --git a/tests/polymind/tools/test_llm_tools.py b/tests/polymind/tools/test_llm_tools.py index ed653bf..02e5de5 100644 --- a/tests/polymind/tools/test_llm_tools.py +++ b/tests/polymind/tools/test_llm_tools.py @@ -69,6 +69,24 @@ def test_get_spec(self, tool): "type": "str", "description": "The prompt for the chat.", "example": "hello, how are you?" + }, + { + "name": "max_tokens", + "type": "int", + "description": "The maximum number of tokens for the chat.", + "example": "1500" + }, + { + "name": "temperature", + "type": "float", + "description": "The temperature for the chat.", + "example": "0.7" + }, + { + "name": "top_p", + "type": "float", + "description": "The top p for the chat.", + "example": "0.1" } ], "output_message": [ From 436896ce3d084a41fcefc6a9a6a8f9cd9d374ffa Mon Sep 17 00:00:00 2001 From: Yx Jiang <2237303+yxjiang@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:53:47 -0700 Subject: [PATCH 4/5] Create tool creator --- polymind/tools/llm_tool.py | 1 + polymind/tools/tool_management_tool.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/polymind/tools/llm_tool.py b/polymind/tools/llm_tool.py index 3da99f4..fee5237 100644 --- a/polymind/tools/llm_tool.py +++ b/polymind/tools/llm_tool.py @@ -67,6 +67,7 @@ async def _execute(self, input: Message) -> Message: "max_tokens": self.max_tokens, "temperature": self.temperature, "top_p": self.top_p, + "system_prompt": system_prompt, } ) if self.stop: diff --git a/polymind/tools/tool_management_tool.py b/polymind/tools/tool_management_tool.py index 26fdd2e..d346d75 100644 --- a/polymind/tools/tool_management_tool.py +++ b/polymind/tools/tool_management_tool.py @@ -11,8 +11,7 @@ from polymind.core.indexer import Indexer from polymind.core.message import Message from polymind.core.tool import BaseTool, Param -from polymind.tools.llm_tool import (LLMTool, OpenAIChatTool, - OpenAIEmbeddingTool) +from polymind.tools.llm_tool import LLMTool, OpenAIChatTool, OpenAIEmbeddingTool class ToolIndexer(Indexer): @@ -215,10 +214,10 @@ class ToolCreator(BaseTool): You are a code generator. You are given a requirement to create a tool. The generated tool should inherit the below BaseTool class. Please put your answer into a ```python``` code block. - + --- python below --- # File: {{tool_name}}.py - + class BaseTool(BaseModel, ABC): '''The base class of the tool. In an agent system, a tool is an object that can be used to perform a task. @@ -312,12 +311,11 @@ async def _execute(self, input: Message) -> Message: ''' pass --- python above --- - + In addition to any needed package, the implementation should also import the below packages: - + from polymind.core.message import Message - from polymind.core.tool import BaseTool, Param - + from polymind.core.tool import BaseTool, Param """, description="The system prompt to generate the tool code.", ) From 117b0c43a49a3897de611e13d6f05d16694bdb36 Mon Sep 17 00:00:00 2001 From: Yx Jiang <2237303+yxjiang@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:01:25 -0700 Subject: [PATCH 5/5] Create tool creator --- polymind/tools/tool_management_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/polymind/tools/tool_management_tool.py b/polymind/tools/tool_management_tool.py index d346d75..c09c202 100644 --- a/polymind/tools/tool_management_tool.py +++ b/polymind/tools/tool_management_tool.py @@ -11,7 +11,8 @@ from polymind.core.indexer import Indexer from polymind.core.message import Message from polymind.core.tool import BaseTool, Param -from polymind.tools.llm_tool import LLMTool, OpenAIChatTool, OpenAIEmbeddingTool +from polymind.tools.llm_tool import (LLMTool, OpenAIChatTool, + OpenAIEmbeddingTool) class ToolIndexer(Indexer):