char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
1d36b3670
char/aliceai-t5-moe
1#!/usr/bin/env python3 2 3"""Function calling example using pydantic models.""" 4 5from __future__import annotations 6 7import argparse 8import datetime 9import json 10import logging 11import textwrap 12import sys 13from enum import Enum 14from typing import Optional ,Union 15 16import requests 17from pydantic import BaseModel ,Field 18from pydantic_models_to_grammar import (add_run_method_to_dynamic_model ,convert_dictionary_to_pydantic_model , 19create_dynamic_model_from_function ,generate_gbnf_grammar_and_documentation ) 20 21 22def create_completion (host ,prompt ,gbnf_grammar ): 23"""Calls the /completion API on llama-server. 24 25See 26https://github.com/ggml-org/llama.cpp/tree/HEAD/tools/server#api-endpoints 27""" 28f" Request:\n Grammar:\n { textwrap . indent ( gbnf_grammar , ' ' ) } \n Prompt:\n { textwrap . indent ( prompt . rstrip (), ' ' ) } " ) 29headers = {"Content-Type" :"application/json" } 30data = {"prompt" :prompt ,"grammar" :gbnf_grammar } 31result = requests .post (f"http:// { host } /completion" ,headers = headers ,json = data ).json () 32assert data .get ("error" )is None ,data 33logging .info ("Result: %s" ,result ) 34content = result ["content" ] 35f" Model: { result [ 'model' ] } " ) 36f" Result:\n { textwrap . indent ( json . dumps ( json . loads ( content ), indent = 2 ), ' ' ) } " ) 37return content 38 39 40# A function for the agent to send a message to the user. 41class SendMessageToUser (BaseModel ): 42"""Send a message to the User.""" 43chain_of_thought :str = Field (...,description = "Your chain of thought while sending the message." ) 44message :str = Field (...,description = "Message you want to send to the user." ) 45 46def run (self ): 47f"SendMessageToUser: { self . message } " ) 48 49 50def example_rce (host ): 51"""Minimal test case where the LLM call an arbitrary python function.""" 52"- example_rce" ) 53tools = [SendMessageToUser ] 54gbnf_grammar ,documentation = generate_gbnf_grammar_and_documentation ( 55pydantic_model_list = tools ,outer_object_name = "function" , 56outer_object_content = "function_parameters" ,model_prefix = "Function" ,fields_prefix = "Parameters" ) 57system_message = "You are an advanced AI, tasked to assist the user by calling functions in JSON format. The following are the available functions and their parameters and types:\n\n" + documentation 58user_message = "What is 42 * 42?" 59prompt = f"<|im_start|>system\n { system_message } <|im_end|>\n<|im_start|>user\n { user_message } <|im_end|>\n<|im_start|>assistant" 60text = create_completion (host ,prompt ,gbnf_grammar ) 61json_data = json .loads (text ) 62tools_map = {tool .__name__ :tool for tool in tools } 63# This finds "SendMessageToUser": 64tool = tools_map .get (json_data ["function" ]) 65if not tool : 66f"Error: unknown tool { json_data [ 'function' ] } " ) 67return 1 68tool (** json_data ["function_parameters" ]).run () 69return 0 70 71 72# Enum for the calculator tool. 73class MathOperation (Enum ): 74ADD = "add" 75SUBTRACT = "subtract" 76MULTIPLY = "multiply" 77DIVIDE = "divide" 78 79 80# Simple pydantic calculator tool for the agent that can add, subtract, 81# multiply, and divide. Docstring and description of fields will be used in 82# system prompt. 83class Calculator (BaseModel ): 84"""Perform a math operation on two numbers.""" 85number_one :Union [int ,float ]= Field (...,description = "First number." ) 86operation :MathOperation = Field (...,description = "Math operation to perform." ) 87number_two :Union [int ,float ]= Field (...,description = "Second number." ) 88 89def run (self ): 90if self .operation == MathOperation .ADD : 91return self .number_one + self .number_two 92elif self .operation == MathOperation .SUBTRACT : 93return self .number_one - self .number_two 94elif self .operation == MathOperation .MULTIPLY : 95return self .number_one * self .number_two 96elif self .operation == MathOperation .DIVIDE : 97return self .number_one / self .number_two 98else : 99raise ValueError ("Unknown operation." ) 100 101 102def example_calculator (host ): 103"""Have the LLM ask to get a calculation done. 104 105Here the grammar gets generated by passing the available function models to 106generate_gbnf_grammar_and_documentation function. This also generates a 107documentation usable by the LLM. 108 109pydantic_model_list is the list of pydantic models outer_object_name is an 110optional name for an outer object around the actual model object. Like a 111"function" object with "function_parameters" which contains the actual model 112object. If None, no outer object will be generated outer_object_content is 113the name of outer object content. 114 115model_prefix is the optional prefix for models in the documentation. (Default="Output Model") 116fields_prefix is the prefix for the model fields in the documentation. (Default="Output Fields") 117""" 118"- example_calculator" ) 119tools = [SendMessageToUser ,Calculator ] 120gbnf_grammar ,documentation = generate_gbnf_grammar_and_documentation ( 121pydantic_model_list = tools ,outer_object_name = "function" , 122outer_object_content = "function_parameters" ,model_prefix = "Function" ,fields_prefix = "Parameters" ) 123system_message = "You are an advanced AI, tasked to assist the user by calling functions in JSON format. The following are the available functions and their parameters and types:\n\n" + documentation 124user_message1 = "What is 42 * 42?" 125prompt = f"<|im_start|>system\n { system_message } <|im_end|>\n<|im_start|>user\n { user_message1 } <|im_end|>\n<|im_start|>assistant" 126text = create_completion (host ,prompt ,gbnf_grammar ) 127json_data = json .loads (text ) 128expected = { 129"function" :"Calculator" , 130"function_parameters" : { 131"number_one" :42 , 132"operation" :"multiply" , 133"number_two" :42 134 } 135 } 136if json_data != expected : 137" Result is not as expected!" ) 138tools_map = {tool .__name__ :tool for tool in tools } 139# This finds "Calculator": 140tool = tools_map .get (json_data ["function" ]) 141if not tool : 142f"Error: unknown tool { json_data [ 'function' ] } " ) 143return 1 144result = tool (** json_data ["function_parameters" ]).run () 145f" Call { json_data [ 'function' ] } gave result { result } " ) 146return 0 147 148 149class Category (Enum ): 150"""The category of the book.""" 151Fiction = "Fiction" 152NonFiction = "Non-Fiction" 153 154 155class Book (BaseModel ): 156"""Represents an entry about a book.""" 157title :str = Field (...,description = "Title of the book." ) 158author :str = Field (...,description = "Author of the book." ) 159published_year :Optional [int ]= Field (...,description = "Publishing year of the book." ) 160keywords :list [str ]= Field (...,description = "A list of keywords." ) 161category :Category = Field (...,description = "Category of the book." ) 162summary :str = Field (...,description = "Summary of the book." ) 163 164 165def example_struct (host ): 166"""A example structured output based on pydantic models. 167 168The LLM will create an entry for a Book database out of an unstructured 169text. We need no additional parameters other than our list of pydantic 170models. 171""" 172"- example_struct" ) 173tools = [Book ] 174gbnf_grammar ,documentation = generate_gbnf_grammar_and_documentation (pydantic_model_list = tools ) 175system_message = "You are an advanced AI, tasked to create a dataset entry in JSON for a Book. The following is the expected output model:\n\n" + documentation 176text = """The Feynman Lectures on Physics is a physics textbook based on some lectures by Richard Feynman, a Nobel laureate who has sometimes been called "The Great Explainer". The lectures were presented before undergraduate students at the California Institute of Technology (Caltech), during 1961–1963. The book's co-authors are Feynman, Robert B. Leighton, and Matthew Sands.""" 177prompt = f"<|im_start|>system\n { system_message } <|im_end|>\n<|im_start|>user\n { text } <|im_end|>\n<|im_start|>assistant" 178text = create_completion (host ,prompt ,gbnf_grammar ) 179json_data = json .loads (text ) 180# In this case, there's no function nor function_parameters. 181# Here the result will vary based on the LLM used. 182keys = sorted (["title" ,"author" ,"published_year" ,"keywords" ,"category" ,"summary" ]) 183if keys != sorted (json_data .keys ()): 184f"Unexpected result: { sorted ( json_data . keys ()) } " ) 185return 1 186book = Book (** json_data ) 187f" As a Book object: %s" % book ) 188return 0 189 190 191def get_current_datetime (output_format :Optional [str ]= None ): 192"""Get the current date and time in the given format. 193 194Args: 195output_format: formatting string for the date and time, defaults to '%Y-%m-%d %H:%M:%S' 196""" 197return datetime .datetime .now ().strftime (output_format or "%Y-%m-%d %H:%M:%S" ) 198 199 200# Example function to get the weather. 201def get_current_weather (location ,unit ): 202"""Get the current weather in a given location""" 203if "London" in location : 204return json .dumps ({"location" :"London" ,"temperature" :"42" ,"unit" :unit .value }) 205elif "New York" in location : 206return json .dumps ({"location" :"New York" ,"temperature" :"24" ,"unit" :unit .value }) 207elif "North Pole" in location : 208return json .dumps ({"location" :"North Pole" ,"temperature" :"-42" ,"unit" :unit .value }) 209return json .dumps ({"location" :location ,"temperature" :"unknown" }) 210 211 212def example_concurrent (host ): 213"""An example for parallel function calling with a Python function, a pydantic 214function model and an OpenAI like function definition. 215""" 216"- example_concurrent" ) 217# Function definition in OpenAI style. 218current_weather_tool = { 219"type" :"function" , 220"function" : { 221"name" :"get_current_weather" , 222"description" :"Get the current weather in a given location" , 223"parameters" : { 224"type" :"object" , 225"properties" : { 226"location" : { 227"type" :"string" , 228"description" :"The city and state, e.g. San Francisco, CA" , 229 }, 230"unit" : {"type" :"string" ,"enum" : ["celsius" ,"fahrenheit" ]}, 231 }, 232"required" : ["location" ], 233 }, 234 }, 235 } 236# Convert OpenAI function definition into pydantic model. 237current_weather_tool_model = convert_dictionary_to_pydantic_model (current_weather_tool ) 238# Add the actual function to a pydantic model. 239current_weather_tool_model = add_run_method_to_dynamic_model (current_weather_tool_model ,get_current_weather ) 240 241# Convert normal Python function to a pydantic model. 242current_datetime_model = create_dynamic_model_from_function (get_current_datetime ) 243 244tools = [SendMessageToUser ,Calculator ,current_datetime_model ,current_weather_tool_model ] 245gbnf_grammar ,documentation = generate_gbnf_grammar_and_documentation ( 246pydantic_model_list = tools ,outer_object_name = "function" , 247outer_object_content = "params" ,model_prefix = "Function" ,fields_prefix = "Parameters" ,list_of_outputs = True ) 248system_message = "You are an advanced AI assistant. You are interacting with the user and with your environment by calling functions. You call functions by writing JSON objects, which represent specific function calls.\nBelow is a list of your available function calls:\n\n" + documentation 249text = """Get the date and time, get the current weather in celsius in London and solve the following calculation: 42 * 42""" 250prompt = f"<|im_start|>system\n { system_message } <|im_end|>\n<|im_start|>user\n { text } <|im_end|>\n<|im_start|>assistant" 251text = create_completion (host ,prompt ,gbnf_grammar ) 252json_data = json .loads (text ) 253expected = [ 254 { 255"function" :"get_current_datetime" , 256"params" : { 257"output_format" :"%Y-%m-%d %H:%M:%S" 258 } 259 }, 260 { 261"function" :"get_current_weather" , 262"params" : { 263"location" :"London" , 264"unit" :"celsius" 265 } 266 }, 267 { 268"function" :"Calculator" , 269"params" : { 270"number_one" :42 , 271"operation" :"multiply" , 272"number_two" :42 273 } 274 } 275 ] 276res = 0 277if json_data != expected : 278" Result is not as expected!" ) 279" This can happen on highly quantized models" ) 280res = 1 281tools_map = {tool .__name__ :tool for tool in tools } 282for call in json_data : 283tool = tools_map .get (call ["function" ]) 284if not tool : 285f"Error: unknown tool { call [ 'function' ] } " ) 286return 1 287result = tool (** call ["params" ]).run () 288f" Call { call [ 'function' ] } returned { result } " ) 289# Should output something like this: 290# Call get_current_datetime returned 2024-07-15 09:50:38 291# Call get_current_weather returned {"location": "London", "temperature": "42", "unit": "celsius"} 292# Call Calculator returned 1764 293return res 294 295 296def main (): 297parser = argparse .ArgumentParser (description = sys .modules [__name__ ].__doc__ ) 298parser .add_argument ("--host" ,default = "localhost:8080" ,help = "llama.cpp server" ) 299parser .add_argument ("-v" ,"--verbose" ,action = "store_true" ,help = "enables logging" ) 300args = parser .parse_args () 301logging .basicConfig (level = logging .INFO if args .verbose else logging .ERROR ) 302ret = 0 303# Comment out below to only run the example you want. 304ret = ret or example_rce (args .host ) 305ret = ret or example_calculator (args .host ) 306ret = ret or example_struct (args .host ) 307ret = ret or example_concurrent (args .host ) 308return ret 309 310 311if __name__ == "__main__" : 312sys .exit (main ())