From 446c3d586c03901c3f617df3d22ac4bf865baf6f Mon Sep 17 00:00:00 2001 From: Adam McCabe Date: Tue, 11 Apr 2023 16:26:04 -0400 Subject: [PATCH] Add PATCH and DELETE to OpenAPI Agent (#2729) This PR proposes an update to the OpenAPI Planner and Planner Prompts to make Patch and Delete available to the planner and executor. I followed the same patterns as for GET and POST, and made some updates to the examples available to the Planner and Orchestrator. Of note, I tried to write prompts for DELETE such that the model will only execute that job if the User specifically asks for a 'Delete' (see the Prompt_planner.py examples to see specificity), or if the User had previously authorized the Delete in the Conversation memory. Although PATCH also modifies existing data, I considered it lower risk and so did not try to enforce the same restrictions on the Planner. --- .../agents/agent_toolkits/openapi/planner.py | 56 +++++++++++++++++- .../agent_toolkits/openapi/planner_prompt.py | 58 ++++++++++++++++++- 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/langchain/agents/agent_toolkits/openapi/planner.py b/langchain/agents/agent_toolkits/openapi/planner.py index 33752ef561..8865bc4222 100644 --- a/langchain/agents/agent_toolkits/openapi/planner.py +++ b/langchain/agents/agent_toolkits/openapi/planner.py @@ -14,9 +14,13 @@ from langchain.agents.agent_toolkits.openapi.planner_prompt import ( API_PLANNER_PROMPT, API_PLANNER_TOOL_DESCRIPTION, API_PLANNER_TOOL_NAME, + PARSING_DELETE_PROMPT, PARSING_GET_PROMPT, + PARSING_PATCH_PROMPT, PARSING_POST_PROMPT, + REQUESTS_DELETE_TOOL_DESCRIPTION, REQUESTS_GET_TOOL_DESCRIPTION, + REQUESTS_PATCH_TOOL_DESCRIPTION, REQUESTS_POST_TOOL_DESCRIPTION, ) from langchain.agents.agent_toolkits.openapi.spec import ReducedOpenAPISpec @@ -90,6 +94,56 @@ class RequestsPostToolWithParsing(BaseRequestsTool, BaseTool): raise NotImplementedError() +class RequestsPatchToolWithParsing(BaseRequestsTool, BaseTool): + name = "requests_patch" + description = REQUESTS_PATCH_TOOL_DESCRIPTION + + response_length: Optional[int] = MAX_RESPONSE_LENGTH + llm_chain = LLMChain( + llm=OpenAI(), + prompt=PARSING_PATCH_PROMPT, + ) + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + response = self.requests_wrapper.patch(data["url"], data["data"]) + response = response[: self.response_length] + return self.llm_chain.predict( + response=response, instructions=data["output_instructions"] + ).strip() + + async def _arun(self, text: str) -> str: + raise NotImplementedError() + + +class RequestsDeleteToolWithParsing(BaseRequestsTool, BaseTool): + name = "requests_delete" + description = REQUESTS_DELETE_TOOL_DESCRIPTION + + response_length: Optional[int] = MAX_RESPONSE_LENGTH + llm_chain = LLMChain( + llm=OpenAI(), + prompt=PARSING_DELETE_PROMPT, + ) + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + response = self.requests_wrapper.delete(data["url"]) + response = response[: self.response_length] + return self.llm_chain.predict( + response=response, instructions=data["output_instructions"] + ).strip() + + async def _arun(self, text: str) -> str: + raise NotImplementedError() + + # # Orchestrator, planner, controller. # @@ -157,7 +211,7 @@ def _create_api_controller_tool( base_url = api_spec.servers[0]["url"] # TODO: do better. def _create_and_run_api_controller_agent(plan_str: str) -> str: - pattern = r"\b(GET|POST)\s+(/\S+)*" + pattern = r"\b(GET|POST|PATCH|DELETE)\s+(/\S+)*" matches = re.findall(pattern, plan_str) endpoint_names = [ "{method} {route}".format(method=method, route=route.split("?")[0]) diff --git a/langchain/agents/agent_toolkits/openapi/planner_prompt.py b/langchain/agents/agent_toolkits/openapi/planner_prompt.py index 4db0fd8d02..49b7b514c7 100644 --- a/langchain/agents/agent_toolkits/openapi/planner_prompt.py +++ b/langchain/agents/agent_toolkits/openapi/planner_prompt.py @@ -2,13 +2,16 @@ from langchain.prompts.prompt import PromptTemplate + API_PLANNER_PROMPT = """You are a planner that plans a sequence of API calls to assist with user queries against an API. You should: 1) evaluate whether the user query can be solved by the API documentated below. If no, say why. 2) if yes, generate a plan of API calls and say what they are doing step by step. +3) If the plan includes a DELETE call, you should always return an ask from the User for authorization first unless the User has specifically asked to delete something. You should only use API endpoints documented below ("Endpoints you can use:"). +You can only use the DELETE tool if the User has specifically asked to delete something. Otherwise, you should return a request authorization from the User first. Some user queries can be resolved in a single API call, but some will require several API calls. The plan will be passed to an API controller that can format it into web requests and return the responses. @@ -20,15 +23,31 @@ Fake endpoints for examples: GET /user to get information about the current user GET /products/search search across products POST /users/{{id}}/cart to add products to a user's cart +PATCH /users/{{id}}/cart to update a user's cart +DELETE /users/{{id}}/cart to delete a user's cart User query: tell me a joke Plan: Sorry, this API's domain is shopping, not comedy. Usery query: I want to buy a couch -Plan: 1. GET /products/search to search for couches +Plan: 1. GET /products with a query param to search for couches 2. GET /user to find the user's id 3. POST /users/{{id}}/cart to add a couch to the user's cart +User query: I want to add a lamp to my cart +Plan: 1. GET /products with a query param to search for lamps +2. GET /user to find the user's id +3. PATCH /users/{{id}}/cart to add a lamp to the user's cart + +User query: I want to delete my cart +Plan: 1. GET /user to find the user's id +2. DELETE required. Did user specify DELETE or previously authorize? Yes, proceed. +3. DELETE /users/{{id}}/cart to delete the user's cart + +User query: I want to start a new cart +Plan: 1. GET /user to find the user's id +2. DELETE required. Did user specify DELETE or previously authorize? No, ask for authorization. +3. Are you sure you want to delete your cart? ---- Here are endpoints you can use. Do not reference any of the endpoints above. @@ -83,6 +102,7 @@ API_CONTROLLER_TOOL_DESCRIPTION = f"Can be used to execute a plan of API calls, API_ORCHESTRATOR_PROMPT = """You are an agent that assists with user queries against API, things like querying information or creating resources. Some user queries can be resolved in a single API call, particularly if you can find appropriate params from the OpenAPI spec; though some require several API call. You should always plan your API calls first, and then execute the plan second. +If the plan includes a DELETE call, be sure to ask the User for authorization first unless the User has specifically asked to delete something. You should never return information without executing the api_controller tool. @@ -145,7 +165,7 @@ REQUESTS_POST_TOOL_DESCRIPTION = """Use this when you want to POST to a website. Input to the tool should be a json string with 3 keys: "url", "data", and "output_instructions". The value of "url" should be a string. The value of "data" should be a dictionary of key-value pairs you want to POST to the url. -The value of "summary_instructions" should be instructions on what information to extract from the response, for example the id(s) for a resource(s) that the POST request creates. +The value of "output_instructions" should be instructions on what information to extract from the response, for example the id(s) for a resource(s) that the POST request creates. Always use double quotes for strings in the json string.""" PARSING_POST_PROMPT = PromptTemplate( @@ -157,3 +177,37 @@ If the response indicates an error, you should instead output a summary of the e Output:""", input_variables=["response", "instructions"], ) + +REQUESTS_PATCH_TOOL_DESCRIPTION = """Use this when you want to PATCH content on a website. +Input to the tool should be a json string with 3 keys: "url", "data", and "output_instructions". +The value of "url" should be a string. +The value of "data" should be a dictionary of key-value pairs of the body params available in the OpenAPI spec you want to PATCH the content with at the url. +The value of "output_instructions" should be instructions on what information to extract from the response, for example the id(s) for a resource(s) that the PATCH request creates. +Always use double quotes for strings in the json string.""" + +PARSING_PATCH_PROMPT = PromptTemplate( + template="""Here is an API response:\n\n{response}\n\n==== +Your task is to extract some information according to these instructions: {instructions} +When working with API objects, you should usually use ids over names. Do not return any ids or names that are not in the response. +If the response indicates an error, you should instead output a summary of the error. + +Output:""", + input_variables=["response", "instructions"], +) + +REQUESTS_DELETE_TOOL_DESCRIPTION = """ONLY USE THIS TOOL WHEN THE USER HAS SPECIFICALLY REQUESTED TO DELETE CONTENT FROM A WEBSITE. +Input to the tool should be a json string with 2 keys: "url", and "output_instructions". +The value of "url" should be a string. +The value of "output_instructions" should be instructions on what information to extract from the response, for example the id(s) for a resource(s) that the DELETE request creates. +Always use double quotes for strings in the json string. +ONLY USE THIS TOOL IF THE USER HAS SPECIFICALLY REQUESTED TO DELETE SOMETHING.""" + +PARSING_DELETE_PROMPT = PromptTemplate( + template="""Here is an API response:\n\n{response}\n\n==== +Your task is to extract some information according to these instructions: {instructions} +When working with API objects, you should usually use ids over names. Do not return any ids or names that are not in the response. +If the response indicates an error, you should instead output a summary of the error. + +Output:""", + input_variables=["response", "instructions"], +)