From 931b2921267fd44af13fdd4c9d1ba984d496425a Mon Sep 17 00:00:00 2001 From: Ziyang Liu Date: Tue, 19 Sep 2023 19:37:20 -0400 Subject: [PATCH] Add support for HTTP PUT in the open api agent prompt (#10763) **Description:** This PR adds HTTP PUT support for the langchain openapi agent toolkit by leveraging existing structure and HTTP put request wrapper. The PUT method is almost identical to HTTP POST but should be idempotent and therefore tighter than POST which is not idempotent. Some APIs may consider to use PUT instead of POST which is unfortunately not supported with the current toolkit yet. --- .../agents/agent_toolkits/openapi/planner.py | 31 +++++++++++++++++++ .../agent_toolkits/openapi/planner_prompt.py | 22 +++++++++++++ .../agents/agent_toolkits/openapi/spec.py | 4 +-- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/libs/langchain/langchain/agents/agent_toolkits/openapi/planner.py b/libs/langchain/langchain/agents/agent_toolkits/openapi/planner.py index 6e2ab4483d..759891d030 100644 --- a/libs/langchain/langchain/agents/agent_toolkits/openapi/planner.py +++ b/libs/langchain/langchain/agents/agent_toolkits/openapi/planner.py @@ -19,10 +19,12 @@ from langchain.agents.agent_toolkits.openapi.planner_prompt import ( PARSING_GET_PROMPT, PARSING_PATCH_PROMPT, PARSING_POST_PROMPT, + PARSING_PUT_PROMPT, REQUESTS_DELETE_TOOL_DESCRIPTION, REQUESTS_GET_TOOL_DESCRIPTION, REQUESTS_PATCH_TOOL_DESCRIPTION, REQUESTS_POST_TOOL_DESCRIPTION, + REQUESTS_PUT_TOOL_DESCRIPTION, ) from langchain.agents.agent_toolkits.openapi.spec import ReducedOpenAPISpec from langchain.agents.mrkl.base import ZeroShotAgent @@ -151,6 +153,35 @@ class RequestsPatchToolWithParsing(BaseRequestsTool, BaseTool): raise NotImplementedError() +class RequestsPutToolWithParsing(BaseRequestsTool, BaseTool): + """Requests PUT tool with LLM-instructed extraction of truncated responses.""" + + name: str = "requests_put" + """Tool name.""" + description = REQUESTS_PUT_TOOL_DESCRIPTION + """Tool description.""" + response_length: Optional[int] = MAX_RESPONSE_LENGTH + """Maximum length of the response to be returned.""" + llm_chain: LLMChain = Field( + default_factory=_get_default_llm_chain_factory(PARSING_PUT_PROMPT) + ) + """LLMChain used to extract the response.""" + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + response = self.requests_wrapper.put(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): """A tool that sends a DELETE request and parses the response.""" diff --git a/libs/langchain/langchain/agents/agent_toolkits/openapi/planner_prompt.py b/libs/langchain/langchain/agents/agent_toolkits/openapi/planner_prompt.py index 9d1688d853..c7263e68be 100644 --- a/libs/langchain/langchain/agents/agent_toolkits/openapi/planner_prompt.py +++ b/libs/langchain/langchain/agents/agent_toolkits/openapi/planner_prompt.py @@ -24,6 +24,7 @@ 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 +PUT /users/{{id}}/coupon to apply idempotent coupon to a user's cart DELETE /users/{{id}}/cart to delete a user's cart User query: tell me a joke @@ -39,6 +40,10 @@ 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 add a coupon to my cart +Plan: 1. GET /user to find the user's id +2. PUT /users/{{id}}/coupon to apply the coupon + 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. @@ -195,6 +200,23 @@ Output:""", input_variables=["response", "instructions"], ) +REQUESTS_PUT_TOOL_DESCRIPTION = """Use this when you want to PUT 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 PUT to 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 PUT request creates. +Always use double quotes for strings in the json string.""" + +PARSING_PUT_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. diff --git a/libs/langchain/langchain/agents/agent_toolkits/openapi/spec.py b/libs/langchain/langchain/agents/agent_toolkits/openapi/spec.py index 35b104c4a3..8f5699390c 100644 --- a/libs/langchain/langchain/agents/agent_toolkits/openapi/spec.py +++ b/libs/langchain/langchain/agents/agent_toolkits/openapi/spec.py @@ -31,12 +31,12 @@ def reduce_openapi_spec(spec: dict, dereference: bool = True) -> ReducedOpenAPIS I was hoping https://openapi.tools/ would have some useful bits to this end, but doesn't seem so. """ - # 1. Consider only get, post, patch, delete endpoints. + # 1. Consider only get, post, patch, put, delete endpoints. endpoints = [ (f"{operation_name.upper()} {route}", docs.get("description"), docs) for route, operation in spec["paths"].items() for operation_name, docs in operation.items() - if operation_name in ["get", "post", "patch", "delete"] + if operation_name in ["get", "post", "patch", "put", "delete"] ] # 2. Replace any refs so that complete docs are retrieved.