Move function format specification to function_tool.py

This commit is contained in:
Don Mahurin 2024-09-28 14:10:55 -07:00
parent af0a9faf7f
commit 8550b76f4e
2 changed files with 58 additions and 38 deletions

View file

@ -3,6 +3,8 @@
import inspect import inspect
import re import re
import json
# Extract OpenAI function calling style definitions from functions # Extract OpenAI function calling style definitions from functions
# #
# Generated with: Create a python function to to generate the OpenAI function calling definition from a given function, getting the description, parameter type and parameter description from the function documentation, assuming the function documentation contains sphynx style parameter descriptions, marked with :param. # Generated with: Create a python function to to generate the OpenAI function calling definition from a given function, getting the description, parameter type and parameter description from the function documentation, assuming the function documentation contains sphynx style parameter descriptions, marked with :param.
@ -36,7 +38,7 @@ def get_function_tool_json(func):
# Generate function definition schema from function definitions # Generate function definition schema from function definitions
# #
# This is from llama-cpp-python, llama_chat_format.py # This is from llama-cpp-python, llama_chat_format.py
def generate_schema_from_functions(functions, namespace="functions") -> str: def generate_functionary_schema_from_functions(functions, namespace="functions") -> str:
schema = ( schema = (
"// Supported function definitions that should be called when necessary.\n" "// Supported function definitions that should be called when necessary.\n"
) )
@ -61,3 +63,31 @@ def generate_schema_from_functions(functions, namespace="functions") -> str:
schema += "}} // namespace {}".format(namespace) schema += "}} // namespace {}".format(namespace)
return schema return schema
functionary_prompt_start = """<|start_header_id|>system<|end_header_id|>
You are capable of executing available function(s) if required.
Execute function(s) as needed.
The function calls are not shown in the conversation and should be called covertly to answer questions.
Ask for the required input to:recipient==all
Use JSON for function arguments.
Respond in this format:
>>>${recipient}
${content}
Available functions:
"""
functionary_prompt_end = """<|eot_id|><|start_header_id|>system<|end_header_id|>
When you send a message containing Python code to python, it will be executed in a stateful Jupyter notebook environment. python will respond with the output of the execution or time out after 60.0 seconds. The drive at '/mnt/data' can be used to save and persist user files.<|eot_id|><|start_header_id|>user<|end_header_id|>
"""
def get_chat_tool_format(args, tools):
return {
'prompt': functionary_prompt_start + generate_functionary_schema_from_functions(tools) + functionary_prompt_end,
'function_marker': '>>>',
'function_re': r'>>>([^\n]*)\n(.*)<\|eot_id\|>',
'user_start': '<|start_header_id|>user<|end_header_id|>\n',
'user_end': '<|eot_id|><|start_header_id|>assistant<|end_header_id|>' + '\n',
'tool_start': '',
'tool_end': '<|eot_id|><|start_header_id|>assistant<|end_header_id|>'
}

View file

@ -10,28 +10,11 @@ import re
import json import json
import functions import functions
from function_tool import get_function_tool_json, generate_schema_from_functions from function_tool import get_function_tool_json, get_chat_tool_format
function_name_list = [ name for name in dir(functions) if not name.startswith('_') ] function_name_list = [ name for name in dir(functions) if not name.startswith('_') ]
function_lookup = { name: getattr(functions, name) for name in function_name_list } function_lookup = { name: getattr(functions, name) for name in function_name_list }
tools = [ get_function_tool_json(f) for (n, f) in function_lookup.items() ] tools = [ get_function_tool_json(f) for (n, f) in function_lookup.items() ]
function_schema = generate_schema_from_functions(tools)
prompt = """<|start_header_id|>system<|end_header_id|>
You are capable of executing available function(s) if required.
Execute function(s) as needed.
The function calls are not shown in the conversation and should be called covertly to answer questions.
Ask for the required input to:recipient==all
Use JSON for function arguments.
Respond in this format:
>>>${recipient}
${content}
Available functions:
""" + function_schema + """<|eot_id|><|start_header_id|>system<|end_header_id|>
When you send a message containing Python code to python, it will be executed in a stateful Jupyter notebook environment. python will respond with the output of the execution or time out after 60.0 seconds. The drive at '/mnt/data' can be used to save and persist user files.<|eot_id|><|start_header_id|>user<|end_header_id|>
"""
def main(): def main():
import argparse import argparse
@ -39,13 +22,17 @@ def main():
parser = argparse.ArgumentParser(epilog='For more options: llama-cli --help') parser = argparse.ArgumentParser(epilog='For more options: llama-cli --help')
parser.add_argument('--display-prompt', action=argparse.BooleanOptionalAction, default=False) parser.add_argument('--display-prompt', action=argparse.BooleanOptionalAction, default=False)
parser.add_argument('--special', action=argparse.BooleanOptionalAction, default=False) parser.add_argument('--special', action=argparse.BooleanOptionalAction, default=False)
parser.add_argument('--reverse-prompt', type=str, default='<|start_header_id|>user<|end_header_id|>\n') parser.add_argument('--reverse-prompt', type=str)
parser.add_argument('--ctx-size', type=int, default=1024) parser.add_argument('--ctx-size', type=int, default=1024)
args, other_args = parser.parse_known_args() args, other_args = parser.parse_known_args()
if args.display_prompt: print(prompt) tool_format = get_chat_tool_format(args, tools)
if args.reverse_prompt is None: args.reverse_prompt = tool_format['user_start']
command = [ './llama-cli', '-i', '-p', prompt, '--reverse-prompt', args.reverse_prompt, '--escape', '--special', '--no-display-prompt', '--log-disable', '--simple-io', '--ctx-size', str(args.ctx_size), *other_args] if args.display_prompt: print(tool_format['prompt'])
command = [ './llama-cli', '-i', '-p', tool_format['prompt'], '--reverse-prompt', args.reverse_prompt, '--escape', '--special', '--no-display-prompt', '--log-disable', '--simple-io', '--ctx-size', str(args.ctx_size), *other_args]
print("'" + "' '".join(command) + "'")
process = subprocess.Popen( process = subprocess.Popen(
command, command,
@ -57,14 +44,14 @@ def main():
if process.stdout is not None: os.set_blocking(process.stdout.fileno(), False) if process.stdout is not None: os.set_blocking(process.stdout.fileno(), False)
try: try:
run_loop(process, args) run_loop(process, args, tool_format)
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nInterrupted by user.") print("\nInterrupted by user.")
finally: finally:
process.terminate() process.terminate()
process.wait() process.wait()
def run_loop(process, args): def run_loop(process, args, tool_format):
pbuffer = '' pbuffer = ''
skip_output_until_result = False skip_output_until_result = False
while True: while True:
@ -76,29 +63,32 @@ def run_loop(process, args):
if not pdata: continue if not pdata: continue
pbuffer += pdata pbuffer += pdata
if(match := re.search(r'>>>([^\n]*)\n(.*)<\|eot_id\|>', pbuffer, re.S)): if(match := re.search(tool_format['function_re'], pbuffer, re.S)):
if not args.special: if not args.special:
pdata = pdata[:match.pos] pdata = pdata[:match.pos]
pbuffer = '' pbuffer = ''
skip_output_until_result = False skip_output_until_result = False
try:
if 1 < len(match.groups()):
tool_name = match.group(1) tool_name = match.group(1)
tool_args = match.group(2) tool_args = json.loads(match.group(2))
else:
tool = json.loads(match.group(1))
tool_name = tool['name']
tool_args = tool['arguments']
if tool_name == 'python': if tool_name == 'python':
result = functions._run_python(tool_args); result = functions._run_python(tool_args);
else: else:
try:
tool_args = json.loads(tool_args)
result = function_lookup[tool_name](**tool_args) result = function_lookup[tool_name](**tool_args)
except ValueError as e: except ValueError as e:
result = {'error': 'unknown'} result = {'error': 'unknown'}
result = json.dumps(result) + '<|eot_id|><|start_header_id|>assistant<|end_header_id|>' result = tool_format['tool_start'] + json.dumps(result) + tool_format['tool_end']
process.stdin.write(result + '\n') process.stdin.write(result + '\n')
process.stdin.flush() process.stdin.flush()
if(args.special): pdata += '\n' + result if(args.special): pdata += '\n' + result
elif (n := pdata.find('>>>')) >= 0: elif (n := pdata.find(tool_format['function_marker'])) >= 0:
if not args.special: if not args.special:
pdata = pdata[:n] pdata = pdata[:n]
skip_output_until_result = True skip_output_until_result = True
@ -114,7 +104,7 @@ def run_loop(process, args):
user_input = sys.stdin.readline() user_input = sys.stdin.readline()
if user_input: if user_input:
user_input = user_input.rstrip() user_input = user_input.rstrip()
process.stdin.write(user_input + '<|eot_id|><|start_header_id|>assistant<|end_header_id|>' + '\n') process.stdin.write(user_input + tool_format['user_end'] + '\n')
process.stdin.flush() process.stdin.flush()
if __name__ == '__main__': if __name__ == '__main__':