Skip to content

Commit

Permalink
upload code
Browse files Browse the repository at this point in the history
  • Loading branch information
tangqiaoyu committed Sep 4, 2023
1 parent 8143012 commit ab41a7d
Show file tree
Hide file tree
Showing 29 changed files with 111,851 additions and 61,853 deletions.
108 changes: 57 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,13 @@
<div align=center><img src="./figures/pipeline.png" width="500px" /></div>



## News

- [07/04] We release the validation data.
- [06/14] We release the ToolAlpaca corpus with 3.9k instances.



## Data

\* **This work is still in progress. We may update the data as we make progress.**
Dataset list:

- train_data.json: training data with 400+ APIs
- eval_simulated.json: evaluation data with 10 simulated APIs
- eval_real.json: evaluation data with 11 real APIs, some APIs require authentication.

Data format:

Expand All @@ -29,61 +25,71 @@ Data format:
"Description": "description, from public-apis",
"Category": "category, from public-apis",
"Introduction": "introduction, generated by LLM",
"NLDocumentation": "natural language documentation, generated by LLM",
"Functions": "NLDocumentation in paper v1, generated by LLM",
"Documentation": "str(json), OpenAPI Specification documentation, generated by LLM",
"NLDocumentation": "natural language documentation, similar to Functions, converted from Documentation",
"Function_Description": "each functions description in NLDocumentation",
"Function_Projection": "function to HTTP request method",
"Instructions": "instructions, generated by LLM",
"Instances": [
{
"Input": "use's init instruction, from use agent",
"Actions": [
{
"Thought": "LLM's thought, from assistant agent",
"Action": "function name, from assistant agent",
"Action Input": "str(json), parameters, from assistant agent",
"Observation": "from [user agent, type check python code, tool executor agent]"
}
],
"input": "use's init instruction, from use agent",
"output": "final output, from assistant agent",
"Final Thought": "the final thought before output, from assistant agent",
"Output": "final output, from assistant agent"
"intermediate_steps": [
[
[
"action, from assistant agent",
"action input, str(json), from assistant agent",
"thought + action + action input, assistant agent's output"
]
"bbservation, from [user agent, type check python code, tool executor agent]"
]
]
}
]
}
```

The case in our paper:
## Dataset Generation

- download public-api data
```bash
python tool_maker/preprocess_public_apis.py -api data/public_apis.json
```

```json
{
"Name": "Public Holidays",
"Description": "Data on national, regional, and religious holidays via API",
"Category": "Calendar",
"Introduction": "The Public Holidays API is a user-friendly interface that provides comprehensive information on national, regional, and religious holidays around the world...",
"NLDocumentation": "1. Name: getHolidaysByCountry\nDescription: Retrieve a list of holidays for a specific country with dates, descriptions, and types...",
"Documentation": "{...}",
"Instances": [
{
"Input": "I'm planning a trip to Japan next year, and I want to avoid any major holidays, so can you tell me the list of holidays in Japan next year?",
"Actions": [
{
"Thought": "I need to know which year the user is referring to.",
"Action": "chatWithUser",
"Action Input": "{\"Question\": \"Could you please specify the year you are planning your trip for?\"}",
"Observation": "The year is 2024."
},
{
"Thought": "I need to get the list of holidays in Japan for 2024.",
"Action": "getHolidays",
"Action Input": "{\"country\": \"Japan\", \"year\": 2024}",
"Observation": "Status Code: 200 Response: {\"holidays\": [{\"name\": \"New Year's Day\", \"date\": \"2024-01-01\", \"type\": \"National\", \"description\": \"...\"}, ...]"
}
],
"Final Thought": "I have retrieved a list of holidays in Japan for 2024.",
"Output": "The list of holidays in Japan for 2024 is New Year's Day (January 1st), Coming of Age Day (January 8th) ..."
}
]
}
- toolset construction
```bash
export PYTHONPATH=$PYTHONPAT:$(pwd)
export OPENAI_API_KEY=""

python tool_maker/get_elements.py -api data/public_apis.json -out ./data

python tool_maker/natural_language_documentation.py -api ./data/api_data.json
```

- tool-use instances generation
```bash
python instance_generation/instruction.py -api ./data/api_data.json -out ./data

python instance_generation/simulator.py -api ./data/api_data.json

python instance_generation/generation.py -api ./data/api_data.json -out ./data --use_cache
```

## Train
To train Toolapaca, we need to create a prompt to organize the dataset in a format that the standard SFT training code can read, similar to what is done in build_dataset.py. Afterward, we can proceed with training using the standard SFT method, only optimizing the loss on `thought`, `action`, and `action input`.


## Evaluation

```bash
python instance_generation/generation.py -api ./data/eval_simulated.json -out ./eval -llm ${llm_name_or_path} --agent_prompt ${agent_prompt_type} --use_cache
python evaluation.py -api ${api_data_path} -out ./eval

python instance_generation/generation.py -api ./data/eval_real.json -out ./data -llm ${llm_name_or_path} --agent_prompt ${agent_prompt_type} --use_cache --real
python evaluation.py -api ${api_data_path} -out ./eval
```


## Citation
Expand Down
97 changes: 97 additions & 0 deletions agent/agent_prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
prefix = """Anser the user's question with the help of tools. The user cannot see the tool usage or use the tool themselves, you can use the tools. And the user cannot see the process of your tool use, so you must give all the infomation in Final Answer field to user. Your task is to answer the user's question, so DO NOT make up anything. If your required parameters are missing use tool "getDetails" to ask user provide them.
You have access to the following tools:"""
suffix = """Begin!
Question: {input}
Thought:{agent_scratchpad}"""

format_instructions = """Use the following format:
Question: the input question you must answer
Thought: Answer the following three questions with one paragraph: 1) Check whether there are any general terms or pronouns that lack sufficient context or specific information. 2) Consider the question and potential approach to answer it. 3) Explain your reasoning and the steps needed to reach a solution.
Action: the action to take, should be one of [{tool_names}].
Action Input: the input to the action, should be json format. All of the action input must be realistic and from the user. Never generate any action input by yourself or copy the input description.
Observation: the result of the action and how it contributes to the solution
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: Summarize the information gathered and the reasoning behind your final answer.
Final Answer: Provide a user-friendly and detailed answer to the original input question that summarizes all relevant information from the Thought/Action/Action Input/Observation sequences, without mentioning specific tool usage details or technical jargon. Ensure the answer is both informative and appropriate for the user."""

prefix_v2 = """Your task is to answer the user's question using available tools. The user cannot see or use the tools themselves, nor can they see the process of your tool usage. Provide all necessary information in the "Final Answer" field. Do not make up any information. If required parameters are missing, use the "getDetails" tool to ask the user for them.
You have access to the following tools:"""

format_instructions_v2 = """Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}].
Action Input: the input to the action, must be JSON format. All of the action input must be realistic and from the user.
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: Summarize the information gathered and the reasoning behind your final answer.
Final Answer: Provide a user-friendly and detailed answer to the original input question that summarizes all relevant information from the Thought/Action/Action Input/Observation sequences."""

train_prompt_v1 = {
"prefix": prefix,
"suffix": suffix,
"format_instructions": format_instructions
}

train_prompt_v2 = {
"prefix": prefix_v2,
"suffix": suffix,
"format_instructions": format_instructions_v2
}


prefix_test = """A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions with the help of some tools.
You have access to the following tools:"""

suffix_test = """Begin!
USER: {input}
ASSISTANT Thought:{agent_scratchpad}"""

format_instructions_test = """The chat follows this format:
USER: the user's question
ASSISTANT Thought: the assistant's inner thought about what to do next
ASSISTANT Action: the action to take, must be one of [{tool_names}].
ASSISTANT Action Input: the input for the action, in JSON format.
ASSISTANT Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
ASSISTANT Thought: summarize the information gathered
ASSISTANT Response: the final response to the user
USER: user's next question
..."""
test_prompt_v1 = {
"prefix": prefix_test,
"suffix": suffix_test,
"format_instructions": format_instructions_test
}

prefix_test_v2 = """Answer the following questions as best you can. You have access to the following tools:"""

suffix_test = """Begin!
USER: {input}
ASSISTANT Thought:{agent_scratchpad}"""

format_instructions_test_v2 = """Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action, in JSON format.
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question"""
test_prompt_v2 = {
"prefix": prefix_test_v2,
"suffix": suffix,
"format_instructions": format_instructions_test_v2
}

prompt_proj = {
"train_v1": train_prompt_v1,
"train_v2": train_prompt_v2,
"test_v1": test_prompt_v1,
"test_v2": test_prompt_v2
}
127 changes: 127 additions & 0 deletions agent/convert_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import logging
import requests
from datetime import datetime


logger = logging.getLogger(__name__)


def convert_type(ori_type):
type_mapping = {
"string": str,
"integer": int,
"number": float,
"boolean": bool,
"array": list,
"object": dict
}

type_mapping.update({j: i for i, j in type_mapping.items()})

return type_mapping.get(ori_type)


def type_check(param_name, param_schema, input_params):
type_check_error = []
doc_type = convert_type(param_schema.get("type", ""))

if doc_type and not isinstance(input_params[param_name], doc_type):
type_error = True
if (doc_type in [int, float, bool] and type(input_params[param_name]) == str) or \
(doc_type == float and type(input_params[param_name]) == int):
try:
input_params[param_name] = doc_type(input_params[param_name])
type_error = False
except ValueError:
pass
if type_error:
type_check_error.append((
param_name,
convert_type(doc_type),
convert_type(type(input_params[param_name]))
))
if "enum" in param_schema and input_params[param_name] != "" and\
input_params[param_name] not in param_schema["enum"]:

type_check_error.append((
param_name,
f'one of {param_schema["enum"]}',
f'"{input_params[param_name]}"'
))
return type_check_error



def call_api_function(input_params, openapi_spec, path, method, base_url=None):

function_doc = openapi_spec["paths"][path][method]

required_params = set()
params = {
"query": {},
"header": {},
"path": {},
"cookie": {}
}

type_check_error = []
for param_doc in function_doc.get("parameters", []):
if param_doc.get("required"):
required_params.add((param_doc["in"], param_doc["name"]))

if param_doc["name"] in input_params:
required_params.discard((param_doc["in"], param_doc["name"]))
params[param_doc["in"]][param_doc["name"]] = input_params[param_doc["name"]]
type_check_error.extend(type_check(param_doc["name"], param_doc, input_params))

body_data = None
required_body_params = None
if "requestBody" in function_doc:
body_data = {}
request_body_schema = function_doc["requestBody"].get("content", {}).get("application/json", {}).get("schema", {})

if "properties" in request_body_schema:
required_body_params = set(request_body_schema.get("required", []))
for property_name, property_value in request_body_schema["properties"].items():
if property_name in input_params:
body_data[property_name] = input_params[property_name]
required_body_params.discard(property_name)
type_check_error.extend(type_check(property_name, property_value, input_params))

if len(type_check_error) > 0:
error_str = "\n".join([f"Parameter type error: \"{i[0]}\", expected {i[1]}, but got {i[2]}. You need to change the input and try again." for i in type_check_error])
raise ValueError(error_str)

if required_params or required_body_params:
missing_params = ", ".join([f'"{param[1]}"' for param in required_params])
missing_params += [f'"{param}"' for param in required_body_params]
raise ValueError(f"Missing required parameters: {', '.join(required_body_params)}. You need to change the input and try again.")

base_url = openapi_spec['servers'][0]['url'] if base_url is None else base_url
url = f"{base_url.rstrip('/')}{path.format(**params['path'])}"
headers = {"Content-Type": "application/json"}
headers.update(params["header"])

logger.debug("request url: {url}")
response = requests.request(
method=method.upper(),
url=url,
params=params["query"],
json=body_data,
headers=headers,
cookies=params["cookie"]
)

if "image" in response.headers.get("Content-Type", ""):
image_extension = response.headers.get("Content-Type", "").split("/")[-1]
timestamp = datetime.now().strftime("%m%d%H%M%S")
image_path = f"./images/{timestamp}.{image_extension}"
with open(image_path, 'wb') as f:
f.write(response.content)
response._content = bytes(f"Recieved an image, saved in '{image_path}'.", "utf-8")

logger.debug("url: {response.request.url}")
logger.debug("body: {response.request.body}")
logger.debug("headers: {response.request.headers}")

return response
24 changes: 24 additions & 0 deletions agent/custom_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pydantic import Field
from langchain.agents import ZeroShotAgent
from langchain.agents.agent import AgentOutputParser


from .custom_parser import CustomMRKLOutputParser2


class CustomZeroShotAgent(ZeroShotAgent):
output_parser: AgentOutputParser = Field(default_factory=CustomMRKLOutputParser2)

@classmethod
def _get_default_output_parser(cls, **kwargs) -> AgentOutputParser:
return CustomMRKLOutputParser2()

@property
def observation_prefix(self) -> str:
"""Prefix to append the observation with."""
return "ASSISTANT Observation: "

@property
def llm_prefix(self) -> str:
"""Prefix to append the llm call with."""
return "ASSISTANT Thought:"
Loading

0 comments on commit ab41a7d

Please sign in to comment.