-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic function calling example using a llama-cli python wrapper
- Loading branch information
Showing
4 changed files
with
257 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# llama.cpp/examples/function-calling | ||
|
||
This example shows how to do basic function calling using llama-cli and a python wrapper to declare and call functions. | ||
|
||
## Options | ||
|
||
Important options for llama-cli-function-runner.py: | ||
|
||
- `-m FNAME, --model FNAME`: Specify the path to the function calling model (e.g., `-m "$(huggingface-cli download meetkai/functionary-small-v3.2-GGUF functionary-small-v3.2.Q4_0.gguf)"`). | ||
- `--ctx-size N`: Set the size of the prompt context. The default is 1024 | ||
- `--special`: show special tokens and function calling details | ||
|
||
## Example showing showing function call details | ||
|
||
``` | ||
./examples/function-calling/llama-cli-function-runner.py -m `huggingface-cli download meetkai/functionary-small-v3.2-GGUF functionary-small-v3.2.Q4_0.gguf` -i --special | ||
What is the weather in Phoenix? | ||
Sure, I'll look that up for you. Let me just check the current weather conditions in Phoenix.>>>get_weather | ||
{"location": "Phoenix"}<|eot_id|> | ||
The current weather in Phoenix is 30C.<|eot_id|> | ||
What is 38484 + 323? | ||
Sure, let's calculate that.>>>calculate | ||
{"expression": "38484 + 323"}<|eot_id|> | ||
The sum of 38484 and 323 is 38807.<|eot_id|> | ||
What is 67 feet in meters? | ||
To convert 67 feet into meters, we use the conversion factor: 1 foot is approximately 0.3048 meters. Let's calculate it.>>>calculate | ||
{"expression": "67 * 0.3048"}<|eot_id|> | ||
67 feet is approximately 20.4216 meters.<|eot_id|> | ||
``` | ||
|
||
## Function calling example, hiding details | ||
``` | ||
./examples/function-calling/llama-cli-function-runner.py -m `huggingface-cli download meetkai/functionary-small-v3.2-GGUF functionary-small-v3.2.Q4_0.gguf` -i | ||
What is the weather in Phoenix? | ||
To provide you with the current weather in Phoenix, Arizona, I will need to check the weather data for you. Let me get that information. | ||
The current weather in Phoenix, Arizona is 30°C. If you have any more questions about weather in other locations, feel free to ask! | ||
Is it colder in Vegas? | ||
To determine if the current temperature in Las Vegas is colder than in Phoenix, which is currently 30°C, I will need to check the weather data for Las Vegas. Let's find out. | ||
The current weather in Las Vegas, Nevada is also 30°C. Therefore, there is no difference in temperature between Phoenix and Las Vegas at the moment. If you have any more questions or need further assistance, please let me know! | ||
What is 37234 times 39? | ||
To calculate 37234 times 39, I'll perform the multiplication. Let's do that. | ||
The result of multiplying 37234 by 39 is 1,452,126. If you have any more calculations or questions, feel free to ask! | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Generate function calling definitions function schemas | ||
|
||
import inspect | ||
import re | ||
import json | ||
|
||
# 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. | ||
def get_function_tool_json(func): | ||
typemap = { 'str': 'string' }; | ||
def get_type(s): | ||
return typemap[s] if s in typemap else s | ||
|
||
function_name = func.__name__ | ||
doc_parts = re.split('\n\s*:param[^:]*\s+', func.__doc__.rstrip()); | ||
Check warning on line 16 in examples/function-calling/function_tool.py GitHub Actions / pyright type-check
|
||
|
||
function_description = doc_parts[0] | ||
params_doc = [ re.split('\:\s*', param_doc, maxsplit=1) for param_doc in doc_parts[1:] ] | ||
Check warning on line 19 in examples/function-calling/function_tool.py GitHub Actions / pyright type-check
|
||
params_doc = { param: desc for param, desc in params_doc } | ||
|
||
function_def = { | ||
'name': function_name, | ||
'description': function_description, | ||
'parameters': { 'type': 'object', 'properties': {}, 'required': [] } | ||
} | ||
|
||
for param_name, param in inspect.signature(func).parameters.items(): | ||
function_def['parameters']['properties'][param_name] = { | ||
'type' : get_type(param.annotation.__name__) if param.annotation is not param.empty else '', | ||
'description': params_doc[param_name] if param_name in params_doc else '' | ||
} | ||
function_def['parameters']['required'].append(param_name); | ||
|
||
return function_def | ||
|
||
# Generate function definition schema from function definitions | ||
# | ||
# This is from llama-cpp-python, llama_chat_format.py | ||
def generate_schema_from_functions(functions, namespace="functions") -> str: | ||
schema = ( | ||
"// Supported function definitions that should be called when necessary.\n" | ||
) | ||
schema += f"namespace {namespace} {{\n\n" | ||
|
||
for function in functions: | ||
function_name = function["name"] | ||
description = function.get("description", "") | ||
parameters = function.get("parameters", {}) | ||
required_params = parameters.get("required", []) | ||
|
||
schema += f"// {description}\n" | ||
schema += f"type {function_name} = (_: {{\n" | ||
|
||
for param_name, param in parameters.get("properties", {}).items(): | ||
param_description = param.get("description", "") | ||
param_type = param.get("type", "any") | ||
optional_indicator = "" if param_name in required_params else "?" | ||
schema += f"// {param_description}\n" | ||
schema += f"{param_name}{optional_indicator}: {param_type},\n" | ||
schema += "}) => any;\n\n" | ||
|
||
schema += "}} // namespace {}".format(namespace) | ||
return schema |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
def calculate(expression: str): | ||
"""Evaluate a mathematical expression | ||
:param expression: The mathematical expression to evaluate | ||
""" | ||
try: | ||
result = eval(expression) | ||
return {"result": result} | ||
except: | ||
return {"error": "Invalid expression"} | ||
|
||
def get_weather(location: str): | ||
"""get the weather of a location | ||
:param location: where to get weather. | ||
""" | ||
return {"temperature": "30C"} | ||
|
||
def _run_python(code): | ||
allowed_globals = { '__builtins__': None, '_': None } | ||
allowed_locals = {} | ||
|
||
code = code.splitlines() | ||
code[-1] = f"_ = {code[-1]}" | ||
code = '\n'.join(code) | ||
|
||
try: | ||
exec(code, allowed_globals, allowed_locals) | ||
except Exception as e: | ||
return None | ||
|
||
return {'result': allowed_locals.get('_', None)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
#!/usr/bin/env python3 | ||
# function calling using llama-cli | ||
|
||
import subprocess | ||
import sys | ||
import select | ||
import os | ||
import re | ||
|
||
import json | ||
|
||
import functions | ||
from function_tool import get_function_tool_json, generate_schema_from_functions | ||
|
||
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 } | ||
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(): | ||
import argparse | ||
|
||
parser = argparse.ArgumentParser(epilog='For more options: llama-cli --help') | ||
parser.add_argument('--display-prompt', 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('--ctx-size', type=int, default=1024) | ||
args, other_args = parser.parse_known_args() | ||
|
||
if args.display_prompt: print(prompt) | ||
|
||
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] | ||
|
||
process = subprocess.Popen( | ||
command, | ||
stdin=subprocess.PIPE, | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE, | ||
text=True, | ||
) | ||
if process.stdout is not None: os.set_blocking(process.stdout.fileno(), False) | ||
|
||
try: | ||
run_loop(process, args) | ||
except KeyboardInterrupt: | ||
print("\nInterrupted by user.") | ||
finally: | ||
process.terminate() | ||
process.wait() | ||
|
||
def run_loop(process, args): | ||
pbuffer = '' | ||
skip_output_until_result = False | ||
while True: | ||
readable, _, _ = select.select([process.stdout, process.stderr, sys.stdin], [], []) | ||
|
||
for stream in readable: | ||
if stream == process.stdout: | ||
pdata = process.stdout.read() | ||
if not pdata: continue | ||
pbuffer += pdata | ||
|
||
if(match := re.search(r'>>>([^\n]*)\n(.*)<\|eot_id\|>', pbuffer, re.S )): | ||
if not args.special: | ||
pdata = pdata[:match.pos] | ||
pbuffer = '' | ||
skip_output_until_result = False | ||
|
||
tool_name = match.group(1) | ||
tool_args = match.group(2) | ||
try: | ||
tool_args = json.loads(tool_args) | ||
except ValueError as e: | ||
result = {'error': 'unknown'} | ||
if tool_name == 'python': | ||
result = functions._run_python(tool_args); | ||
elif tool_args is not None: | ||
result = function_lookup[tool_name](**tool_args) | ||
else: | ||
result = {'error': 'unknown'} | ||
process.stdin.write(json.dumps(result) + '<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n') | ||
process.stdin.flush() | ||
elif (n := pdata.find('>>>')) >= 0: | ||
if not args.special: | ||
pdata = pdata[:n] | ||
skip_output_until_result = True | ||
elif skip_output_until_result: | ||
pdata = '' | ||
|
||
if not args.special: | ||
pdata = re.sub(r'<\|[^\|>]*\|>', '', pdata) | ||
sys.stdout.write(pdata) | ||
sys.stdout.flush() | ||
|
||
elif stream == sys.stdin: | ||
user_input = sys.stdin.readline() | ||
if user_input: | ||
user_input = user_input.rstrip() | ||
process.stdin.write(user_input + '<|eot_id|><|start_header_id|>assistant<|end_header_id|>' + '\n') | ||
process.stdin.flush() | ||
|
||
if __name__ == '__main__': | ||
main() | ||
|