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.
This commit is contained in:
Ziyang Liu 2023-09-19 19:37:20 -04:00 committed by GitHub
parent a29cd89923
commit 931b292126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 55 additions and 2 deletions

View File

@ -19,10 +19,12 @@ from langchain.agents.agent_toolkits.openapi.planner_prompt import (
PARSING_GET_PROMPT, PARSING_GET_PROMPT,
PARSING_PATCH_PROMPT, PARSING_PATCH_PROMPT,
PARSING_POST_PROMPT, PARSING_POST_PROMPT,
PARSING_PUT_PROMPT,
REQUESTS_DELETE_TOOL_DESCRIPTION, REQUESTS_DELETE_TOOL_DESCRIPTION,
REQUESTS_GET_TOOL_DESCRIPTION, REQUESTS_GET_TOOL_DESCRIPTION,
REQUESTS_PATCH_TOOL_DESCRIPTION, REQUESTS_PATCH_TOOL_DESCRIPTION,
REQUESTS_POST_TOOL_DESCRIPTION, REQUESTS_POST_TOOL_DESCRIPTION,
REQUESTS_PUT_TOOL_DESCRIPTION,
) )
from langchain.agents.agent_toolkits.openapi.spec import ReducedOpenAPISpec from langchain.agents.agent_toolkits.openapi.spec import ReducedOpenAPISpec
from langchain.agents.mrkl.base import ZeroShotAgent from langchain.agents.mrkl.base import ZeroShotAgent
@ -151,6 +153,35 @@ class RequestsPatchToolWithParsing(BaseRequestsTool, BaseTool):
raise NotImplementedError() 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): class RequestsDeleteToolWithParsing(BaseRequestsTool, BaseTool):
"""A tool that sends a DELETE request and parses the response.""" """A tool that sends a DELETE request and parses the response."""

View File

@ -24,6 +24,7 @@ GET /user to get information about the current user
GET /products/search search across products GET /products/search search across products
POST /users/{{id}}/cart to add products to a user's cart POST /users/{{id}}/cart to add products to a user's cart
PATCH /users/{{id}}/cart to update 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 DELETE /users/{{id}}/cart to delete a user's cart
User query: tell me a joke 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 2. GET /user to find the user's id
3. PATCH /users/{{id}}/cart to add a lamp to the user's cart 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 User query: I want to delete my cart
Plan: 1. GET /user to find the user's id Plan: 1. GET /user to find the user's id
2. DELETE required. Did user specify DELETE or previously authorize? Yes, proceed. 2. DELETE required. Did user specify DELETE or previously authorize? Yes, proceed.
@ -195,6 +200,23 @@ Output:""",
input_variables=["response", "instructions"], 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. 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". 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 "url" should be a string.

View File

@ -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 I was hoping https://openapi.tools/ would have some useful bits
to this end, but doesn't seem so. 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 = [ endpoints = [
(f"{operation_name.upper()} {route}", docs.get("description"), docs) (f"{operation_name.upper()} {route}", docs.get("description"), docs)
for route, operation in spec["paths"].items() for route, operation in spec["paths"].items()
for operation_name, docs in operation.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. # 2. Replace any refs so that complete docs are retrieved.