From 0153bb5416042bf449a2885b4f255627abf45402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sat, 18 Nov 2023 16:09:56 +0800 Subject: [PATCH 01/13] feat: add ask code via function. --- metagpt/provider/base_gpt_api.py | 20 ++++++++-- metagpt/provider/openai_api.py | 55 +++++++++++++++++++++++++-- metagpt/utils/function_schema.py | 29 ++++++++++++++ tests/metagpt/provider/test_openai.py | 22 +++++++++++ 4 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 metagpt/utils/function_schema.py create mode 100644 tests/metagpt/provider/test_openai.py diff --git a/metagpt/provider/base_gpt_api.py b/metagpt/provider/base_gpt_api.py index de61167b9..2cfc3fa1f 100644 --- a/metagpt/provider/base_gpt_api.py +++ b/metagpt/provider/base_gpt_api.py @@ -5,6 +5,7 @@ @Author : alexanderwu @File : base_gpt_api.py """ +import json from abc import abstractmethod from typing import Optional @@ -14,7 +15,8 @@ from metagpt.provider.base_chatbot import BaseChatbot class BaseGPTAPI(BaseChatbot): """GPT API abstract class, requiring all inheritors to provide a series of standard capabilities""" - system_prompt = 'You are a helpful assistant.' + + system_prompt = "You are a helpful assistant." def _user_msg(self, msg: str) -> dict[str, str]: return {"role": "user", "content": msg} @@ -108,11 +110,23 @@ class BaseGPTAPI(BaseChatbot): """Required to provide the first text of choice""" return rsp.get("choices")[0]["message"]["content"] + def get_choice_function(self, rsp: dict) -> dict: + """Required to provide the first function of choice. for example: + "function": { + "name": "execute", + "arguments": "{\n \"language\": \"python\",\n \"code\": \"print('Hello, World!')\"\n}" + } + """ + return rsp.get("choices")[0]["message"]["tool_calls"][0]["function"].to_dict() + + def get_choice_function_arguments(self, rsp: dict) -> dict: + """Required to provide the first function arguments of choice.""" + return json.loads(self.get_choice_function(rsp)["arguments"]) + def messages_to_prompt(self, messages: list[dict]): """[{"role": "user", "content": msg}] to user: etc.""" - return '\n'.join([f"{i['role']}: {i['content']}" for i in messages]) + return "\n".join([f"{i['role']}: {i['content']}" for i in messages]) def messages_to_dict(self, messages): """objects to [{"role": "user", "content": msg}] etc.""" return [i.to_dict() for i in messages] - \ No newline at end of file diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 6ebed2c16..b9698e77d 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -21,6 +21,7 @@ from tenacity import ( from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.provider.base_gpt_api import BaseGPTAPI +from metagpt.utils.function_schema import general_function_schema, general_tool_choice from metagpt.utils.singleton import Singleton from metagpt.utils.token_counter import ( TOKEN_COSTS, @@ -110,7 +111,6 @@ class CostManager(metaclass=Singleton): """ return self.total_completion_tokens - def get_total_cost(self): """ Get the total cost of API calls. @@ -120,7 +120,6 @@ class CostManager(metaclass=Singleton): """ return self.total_cost - def get_costs(self) -> Costs: """Get all costs""" return Costs(self.total_prompt_tokens, self.total_completion_tokens, self.total_cost, self.total_budget) @@ -181,7 +180,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): self._update_costs(usage) return full_reply_content - def _cons_kwargs(self, messages: list[dict]) -> dict: + def _cons_kwargs(self, messages: list[dict], **configs) -> dict: kwargs = { "messages": messages, "max_tokens": self.get_max_tokens(messages), @@ -190,6 +189,9 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): "temperature": 0.3, "timeout": 3, } + if configs: + kwargs.update(configs) + if CONFIG.openai_api_type == "azure": if CONFIG.deployment_name and CONFIG.deployment_id: raise ValueError("You can only use one of the `deployment_id` or `deployment_name` model") @@ -239,6 +241,53 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): rsp = await self._achat_completion(messages) return self.get_choice_text(rsp) + def _func_configs(self, messages: list[dict], **kwargs) -> dict: + if "tools" not in kwargs: + configs = { + "tools": [{"type": "function", "function": general_function_schema}], + "tool_choice": general_tool_choice, + } + kwargs.update(configs) + + return self._cons_kwargs(messages, **kwargs) + + def _chat_completion_function(self, messages: list[dict], **kwargs) -> dict: + rsp = self.llm.ChatCompletion.create(**self._func_configs(messages, **kwargs)) + self._update_costs(rsp.get("usage")) + return rsp + + async def _achat_completion_function(self, messages: list[dict], **chat_configs) -> dict: + rsp = await self.llm.ChatCompletion.acreate(**self._func_configs(messages, **chat_configs)) + self._update_costs(rsp.get("usage")) + return rsp + + def ask_code(self, messages: list[dict], **kwargs) -> dict: + """Use function of tools to ask a code. + https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools + + Examples: + + >>> llm = OpenAIGPTAPI() + >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}] + >>> llm.ask_code(msg) + {'language': 'python', 'code': "print('Hello, World!')"} + """ + rsp = self._chat_completion_function(messages, **kwargs) + return self.get_choice_function_arguments(rsp) + + async def aask_code(self, messages: list[dict], **kwargs) -> dict: + """Use function of tools to ask a code. + https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools + + Examples: + + >>> llm = OpenAIGPTAPI() + >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}] + >>> rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + """ + rsp = await self._achat_completion_function(messages, **kwargs) + return self.get_choice_function_arguments(rsp) + def _calc_usage(self, messages: list[dict], rsp: str) -> dict: usage = {} if CONFIG.calc_usage: diff --git a/metagpt/utils/function_schema.py b/metagpt/utils/function_schema.py new file mode 100644 index 000000000..9d7cef927 --- /dev/null +++ b/metagpt/utils/function_schema.py @@ -0,0 +1,29 @@ +# function in tools, https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools +general_function_schema = { + "name": "execute", + "description": "Executes code on the user's machine, **in the users local environment**, and returns the output", + "parameters": { + "type": "object", + "properties": { + "language": { + "type": "string", + "description": "The programming language (required parameter to the `execute` function)", + "enum": [ + "python", + "R", + "shell", + "applescript", + "javascript", + "html", + "powershell", + ], + }, + "code": {"type": "string", "description": "The code to execute (required)"}, + }, + "required": ["language", "code"], + }, +} + +# tool_choice value for general_function_schema +# https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice +general_tool_choice = {"type": "function", "function": {"name": "execute"}} diff --git a/tests/metagpt/provider/test_openai.py b/tests/metagpt/provider/test_openai.py new file mode 100644 index 000000000..4cbc896e0 --- /dev/null +++ b/tests/metagpt/provider/test_openai.py @@ -0,0 +1,22 @@ +import pytest + +from metagpt.provider.openai_api import OpenAIGPTAPI + + +@pytest.mark.asyncio +async def test_aask_code(): + llm = OpenAIGPTAPI() + msg = [{"role": "user", "content": "Write a python hello world code."}] + rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +def test_ask_code(): + llm = OpenAIGPTAPI() + msg = [{"role": "user", "content": "Write a python hello world code."}] + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 From aca50086c3e51c153cf24a777bd9af53030a8ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sat, 18 Nov 2023 16:59:14 +0800 Subject: [PATCH 02/13] chore: add comment for general_function_schema. --- metagpt/utils/function_schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/metagpt/utils/function_schema.py b/metagpt/utils/function_schema.py index 9d7cef927..f9b84fc60 100644 --- a/metagpt/utils/function_schema.py +++ b/metagpt/utils/function_schema.py @@ -1,4 +1,5 @@ # function in tools, https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools +# Reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.14/interpreter/llm/setup_openai_coding_llm.py general_function_schema = { "name": "execute", "description": "Executes code on the user's machine, **in the users local environment**, and returns the output", From 2a881589330a86c3c7c7fdd5d608d20679bc6cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sat, 18 Nov 2023 18:14:38 +0800 Subject: [PATCH 03/13] chore: remove and renamed function_schema.py --- metagpt/{utils/function_schema.py => provider/constant.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename metagpt/{utils/function_schema.py => provider/constant.py} (100%) diff --git a/metagpt/utils/function_schema.py b/metagpt/provider/constant.py similarity index 100% rename from metagpt/utils/function_schema.py rename to metagpt/provider/constant.py From 218eeef4b8616a47b190531db34cbd5cee26e57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sat, 18 Nov 2023 18:15:50 +0800 Subject: [PATCH 04/13] chore: messages supports more types. --- metagpt/provider/openai_api.py | 22 +++++++++++++-- tests/metagpt/provider/test_openai.py | 39 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index b9698e77d..287668c83 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -21,7 +21,8 @@ from tenacity import ( from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.provider.base_gpt_api import BaseGPTAPI -from metagpt.utils.function_schema import general_function_schema, general_tool_choice +from metagpt.provider.constant import general_function_schema, general_tool_choice +from metagpt.schema import Message from metagpt.utils.singleton import Singleton from metagpt.utils.token_counter import ( TOKEN_COSTS, @@ -261,7 +262,22 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): self._update_costs(rsp.get("usage")) return rsp - def ask_code(self, messages: list[dict], **kwargs) -> dict: + def _process_message(self, messages: Union[str, Message, list[dict]]) -> list[dict]: + """convert messages to list[dict].""" + if isinstance(messages, list): + return messages + + if isinstance(messages, Message): + messages = [messages.to_dict()] + elif isinstance(messages, str): + messages = [{"role": "user", "content": messages}] + else: + raise ValueError( + f"Only support messages type are: str, Message, list[dict], but got {type(messages).__name__}!" + ) + return messages + + def ask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict: """Use function of tools to ask a code. https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools @@ -272,6 +288,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): >>> llm.ask_code(msg) {'language': 'python', 'code': "print('Hello, World!')"} """ + messages = self._process_message(messages) rsp = self._chat_completion_function(messages, **kwargs) return self.get_choice_function_arguments(rsp) @@ -285,6 +302,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}] >>> rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} """ + messages = self._process_message(messages) rsp = await self._achat_completion_function(messages, **kwargs) return self.get_choice_function_arguments(rsp) diff --git a/tests/metagpt/provider/test_openai.py b/tests/metagpt/provider/test_openai.py index 4cbc896e0..d6d9f4f9d 100644 --- a/tests/metagpt/provider/test_openai.py +++ b/tests/metagpt/provider/test_openai.py @@ -1,6 +1,7 @@ import pytest from metagpt.provider.openai_api import OpenAIGPTAPI +from metagpt.schema import UserMessage @pytest.mark.asyncio @@ -13,6 +14,26 @@ async def test_aask_code(): assert len(rsp["code"]) > 0 +@pytest.mark.asyncio +async def test_aask_code_str(): + llm = OpenAIGPTAPI() + msg = "Write a python hello world code." + rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +@pytest.mark.asyncio +async def test_aask_code_Message(): + llm = OpenAIGPTAPI() + msg = UserMessage("Write a python hello world code.") + rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + def test_ask_code(): llm = OpenAIGPTAPI() msg = [{"role": "user", "content": "Write a python hello world code."}] @@ -20,3 +41,21 @@ def test_ask_code(): assert "language" in rsp assert "code" in rsp assert len(rsp["code"]) > 0 + + +def test_ask_code_str(): + llm = OpenAIGPTAPI() + msg = "Write a python hello world code." + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +def test_ask_code_Message(): + llm = OpenAIGPTAPI() + msg = UserMessage("Write a python hello world code.") + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 From 62254a184e485ab5bb5219b84c40660b56b216a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sat, 18 Nov 2023 18:33:00 +0800 Subject: [PATCH 05/13] chore: add examples for ask_code and aask_code. --- metagpt/provider/openai_api.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 287668c83..8f8d45d73 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -284,6 +284,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): Examples: >>> llm = OpenAIGPTAPI() + >>> llm.ask_code("Write a python hello world code.") + {'language': 'python', 'code': "print('Hello, World!')"} >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}] >>> llm.ask_code(msg) {'language': 'python', 'code': "print('Hello, World!')"} @@ -292,13 +294,16 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): rsp = self._chat_completion_function(messages, **kwargs) return self.get_choice_function_arguments(rsp) - async def aask_code(self, messages: list[dict], **kwargs) -> dict: + async def aask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict: """Use function of tools to ask a code. https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools Examples: >>> llm = OpenAIGPTAPI() + >>> rsp = await llm.ask_code("Write a python hello world code.") + >>> rsp + {'language': 'python', 'code': "print('Hello, World!')"} >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}] >>> rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} """ From daf29c41afba445bd507d75e6549e0f8fb14ecc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 11:17:33 +0800 Subject: [PATCH 06/13] chore: change name. --- metagpt/provider/constant.py | 4 ++-- metagpt/provider/openai_api.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/provider/constant.py b/metagpt/provider/constant.py index f9b84fc60..f228e7c30 100644 --- a/metagpt/provider/constant.py +++ b/metagpt/provider/constant.py @@ -1,6 +1,6 @@ # function in tools, https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools # Reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.14/interpreter/llm/setup_openai_coding_llm.py -general_function_schema = { +GENRAL_FUNCTION_SCHEMA_4_ASK_CODE = { "name": "execute", "description": "Executes code on the user's machine, **in the users local environment**, and returns the output", "parameters": { @@ -27,4 +27,4 @@ general_function_schema = { # tool_choice value for general_function_schema # https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice -general_tool_choice = {"type": "function", "function": {"name": "execute"}} +GENRAL_TOOL_CHOICE_4_ASK_CODE = {"type": "function", "function": {"name": "execute"}} diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 8f8d45d73..f950c0d67 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -21,7 +21,7 @@ from tenacity import ( from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.provider.base_gpt_api import BaseGPTAPI -from metagpt.provider.constant import general_function_schema, general_tool_choice +from metagpt.provider.constant import GENRAL_FUNCTION_SCHEMA_4_ASK_CODE, GENRAL_TOOL_CHOICE_4_ASK_CODE from metagpt.schema import Message from metagpt.utils.singleton import Singleton from metagpt.utils.token_counter import ( @@ -245,8 +245,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): def _func_configs(self, messages: list[dict], **kwargs) -> dict: if "tools" not in kwargs: configs = { - "tools": [{"type": "function", "function": general_function_schema}], - "tool_choice": general_tool_choice, + "tools": [{"type": "function", "function": GENRAL_FUNCTION_SCHEMA_4_ASK_CODE}], + "tool_choice": GENRAL_TOOL_CHOICE_4_ASK_CODE, } kwargs.update(configs) From fcc9bd6063b77c758fd10c9d78affde7a93236ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 11:17:56 +0800 Subject: [PATCH 07/13] chore: openai -> openai>=0.28.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 093298775..ff483e97f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ langchain==0.0.231 loguru==0.6.0 meilisearch==0.21.0 numpy==1.24.3 -openai +openai>=0.28.0 openpyxl beautifulsoup4==4.12.2 pandas==2.0.3 From 66f647276e8775478321cd19bddb5f7cce9000d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 11:19:43 +0800 Subject: [PATCH 08/13] Revert "chore: change name." This reverts commit daf29c41afba445bd507d75e6549e0f8fb14ecc5. --- metagpt/provider/constant.py | 4 ++-- metagpt/provider/openai_api.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/provider/constant.py b/metagpt/provider/constant.py index f228e7c30..f9b84fc60 100644 --- a/metagpt/provider/constant.py +++ b/metagpt/provider/constant.py @@ -1,6 +1,6 @@ # function in tools, https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools # Reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.14/interpreter/llm/setup_openai_coding_llm.py -GENRAL_FUNCTION_SCHEMA_4_ASK_CODE = { +general_function_schema = { "name": "execute", "description": "Executes code on the user's machine, **in the users local environment**, and returns the output", "parameters": { @@ -27,4 +27,4 @@ GENRAL_FUNCTION_SCHEMA_4_ASK_CODE = { # tool_choice value for general_function_schema # https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice -GENRAL_TOOL_CHOICE_4_ASK_CODE = {"type": "function", "function": {"name": "execute"}} +general_tool_choice = {"type": "function", "function": {"name": "execute"}} diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index f950c0d67..8f8d45d73 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -21,7 +21,7 @@ from tenacity import ( from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.provider.base_gpt_api import BaseGPTAPI -from metagpt.provider.constant import GENRAL_FUNCTION_SCHEMA_4_ASK_CODE, GENRAL_TOOL_CHOICE_4_ASK_CODE +from metagpt.provider.constant import general_function_schema, general_tool_choice from metagpt.schema import Message from metagpt.utils.singleton import Singleton from metagpt.utils.token_counter import ( @@ -245,8 +245,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): def _func_configs(self, messages: list[dict], **kwargs) -> dict: if "tools" not in kwargs: configs = { - "tools": [{"type": "function", "function": GENRAL_FUNCTION_SCHEMA_4_ASK_CODE}], - "tool_choice": GENRAL_TOOL_CHOICE_4_ASK_CODE, + "tools": [{"type": "function", "function": general_function_schema}], + "tool_choice": general_tool_choice, } kwargs.update(configs) From 4418fe8283635b5bdf64489b916c073d97695f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 11:40:15 +0800 Subject: [PATCH 09/13] feat: ask_code message support type list[Message]. --- metagpt/provider/openai_api.py | 4 ++-- tests/metagpt/provider/test_openai.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 8f8d45d73..11f429601 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -262,10 +262,10 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): self._update_costs(rsp.get("usage")) return rsp - def _process_message(self, messages: Union[str, Message, list[dict]]) -> list[dict]: + def _process_message(self, messages: Union[str, Message, list[dict], list[Message]]) -> list[dict]: """convert messages to list[dict].""" if isinstance(messages, list): - return messages + return [msg if isinstance(msg, dict) else msg.to_dict() for msg in messages] if isinstance(messages, Message): messages = [messages.to_dict()] diff --git a/tests/metagpt/provider/test_openai.py b/tests/metagpt/provider/test_openai.py index d6d9f4f9d..93ac6623c 100644 --- a/tests/metagpt/provider/test_openai.py +++ b/tests/metagpt/provider/test_openai.py @@ -59,3 +59,12 @@ def test_ask_code_Message(): assert "language" in rsp assert "code" in rsp assert len(rsp["code"]) > 0 + + +def test_ask_code_list_Message(): + llm = OpenAIGPTAPI() + msg = [UserMessage("a=[1,2,5,10,-10]"), UserMessage("写出求a中最大值的代码python")] + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': 'max_value = max(a)\nmax_value'} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 From 657571a192bab2dd092ac4bf5860411489dc9e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 12:01:28 +0800 Subject: [PATCH 10/13] feat: ask_code message support type list[str]. --- metagpt/provider/openai_api.py | 3 ++- tests/metagpt/provider/test_openai.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 11f429601..ef9b790ce 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -262,9 +262,10 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): self._update_costs(rsp.get("usage")) return rsp - def _process_message(self, messages: Union[str, Message, list[dict], list[Message]]) -> list[dict]: + def _process_message(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]: """convert messages to list[dict].""" if isinstance(messages, list): + messages = [Message(msg) if isinstance(msg, str) else msg for msg in messages] return [msg if isinstance(msg, dict) else msg.to_dict() for msg in messages] if isinstance(messages, Message): diff --git a/tests/metagpt/provider/test_openai.py b/tests/metagpt/provider/test_openai.py index 93ac6623c..2b0af37b5 100644 --- a/tests/metagpt/provider/test_openai.py +++ b/tests/metagpt/provider/test_openai.py @@ -68,3 +68,13 @@ def test_ask_code_list_Message(): assert "language" in rsp assert "code" in rsp assert len(rsp["code"]) > 0 + + +def test_ask_code_list_str(): + llm = OpenAIGPTAPI() + msg = ["a=[1,2,5,10,-10]", "写出求a中最大值的代码python"] + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': 'max_value = max(a)\nmax_value'} + print(rsp) + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 From 45655a2a28efc07fcaacd7d91106eaa1f2d12984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 16:34:12 +0800 Subject: [PATCH 11/13] chore: add function signature to get_choice_function and get_choice_function_arguments. --- metagpt/provider/base_gpt_api.py | 39 +++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/metagpt/provider/base_gpt_api.py b/metagpt/provider/base_gpt_api.py index 2cfc3fa1f..f23ef871c 100644 --- a/metagpt/provider/base_gpt_api.py +++ b/metagpt/provider/base_gpt_api.py @@ -111,16 +111,43 @@ class BaseGPTAPI(BaseChatbot): return rsp.get("choices")[0]["message"]["content"] def get_choice_function(self, rsp: dict) -> dict: - """Required to provide the first function of choice. for example: - "function": { - "name": "execute", - "arguments": "{\n \"language\": \"python\",\n \"code\": \"print('Hello, World!')\"\n}" - } + """Required to provide the first function of choice + :param dict rsp: OpenAI chat.comletion respond JSON, Note "message" must include "tool_calls", + and "tool_calls" must include "function", for example: + {... + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_Y5r6Ddr2Qc2ZrqgfwzPX5l72", + "type": "function", + "function": { + "name": "execute", + "arguments": "{\n \"language\": \"python\",\n \"code\": \"print('Hello, World!')\"\n}" + } + } + ] + }, + "finish_reason": "stop" + } + ], + ...} + :return dict: return first function of choice, for exmaple, + {'name': 'execute', 'arguments': '{\n "language": "python",\n "code": "print(\'Hello, World!\')"\n}'} """ return rsp.get("choices")[0]["message"]["tool_calls"][0]["function"].to_dict() def get_choice_function_arguments(self, rsp: dict) -> dict: - """Required to provide the first function arguments of choice.""" + """Required to provide the first function arguments of choice. + + :param dict rsp: same as in self.get_choice_function(rsp) + :return dict: return the first function arguments of choice, for example, + {'language': 'python', 'code': "print('Hello, World!')"} + """ return json.loads(self.get_choice_function(rsp)["arguments"]) def messages_to_prompt(self, messages: list[dict]): From bec4bc0e8386a9866904ab92cc55df1d155549df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 17:01:03 +0800 Subject: [PATCH 12/13] chore: add comments for `kwargs`. --- metagpt/provider/openai_api.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index ef9b790ce..af7810b62 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -243,6 +243,9 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): return self.get_choice_text(rsp) def _func_configs(self, messages: list[dict], **kwargs) -> dict: + """ + Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create + """ if "tools" not in kwargs: configs = { "tools": [{"type": "function", "function": general_function_schema}], @@ -280,7 +283,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): def ask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict: """Use function of tools to ask a code. - https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools + + Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create Examples: @@ -297,7 +301,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): async def aask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict: """Use function of tools to ask a code. - https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools + + Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create Examples: From 7a5c66b01edcd01ed26426df3ce6b7b2c0cfd733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 21 Nov 2023 17:14:39 +0800 Subject: [PATCH 13/13] chore: general_function_schema, general_tool_choice -> GENERAL_FUNCTION_SCHEMA, GENERAL_TOOL_CHOICE --- metagpt/provider/constant.py | 4 ++-- metagpt/provider/openai_api.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/provider/constant.py b/metagpt/provider/constant.py index f9b84fc60..db67847a8 100644 --- a/metagpt/provider/constant.py +++ b/metagpt/provider/constant.py @@ -1,6 +1,6 @@ # function in tools, https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools # Reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.14/interpreter/llm/setup_openai_coding_llm.py -general_function_schema = { +GENERAL_FUNCTION_SCHEMA = { "name": "execute", "description": "Executes code on the user's machine, **in the users local environment**, and returns the output", "parameters": { @@ -27,4 +27,4 @@ general_function_schema = { # tool_choice value for general_function_schema # https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice -general_tool_choice = {"type": "function", "function": {"name": "execute"}} +GENERAL_TOOL_CHOICE = {"type": "function", "function": {"name": "execute"}} diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index af7810b62..34e5693f8 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -21,7 +21,7 @@ from tenacity import ( from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.provider.base_gpt_api import BaseGPTAPI -from metagpt.provider.constant import general_function_schema, general_tool_choice +from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA, GENERAL_TOOL_CHOICE from metagpt.schema import Message from metagpt.utils.singleton import Singleton from metagpt.utils.token_counter import ( @@ -248,8 +248,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): """ if "tools" not in kwargs: configs = { - "tools": [{"type": "function", "function": general_function_schema}], - "tool_choice": general_tool_choice, + "tools": [{"type": "function", "function": GENERAL_FUNCTION_SCHEMA}], + "tool_choice": GENERAL_TOOL_CHOICE, } kwargs.update(configs)