moe-mizrak/laravel-prompt-alchemist

Versatile LLM Tool Use (Function Calling) package for Laravel, compatible with all LLMs, enabling LLM to execute actual code functions (unlike LLMs' built-in capabilities).

v2.0.1 2024-06-27 17:29 UTC

This package is not auto-updated.

Last update: 2024-11-17 18:18:28 UTC


README

Versatile LLM Tool Use (Function Calling) package for Laravel, compatible with all LLMs, enabling LLM to execute actual code functions (unlike LLMs' built-in capabilities).

Latest Version on Packagist

Unlock Powerful Large Language Model (LLM) Interactions in Your Laravel Application.

This Laravel package enables versatile LLM Tool Use (Function Calling), allowing LLMs to decide and execute function calls based on the prompt. Unlike built-in capabilities that may only list functions, this package makes the actual calls, ensuring dynamic execution. Compatible with all LLMs, it enhances automation and interactivity in your applications.

Table of Contents

🤖 Requirements

ℹī¸ OpenRouter is used as the default LLM provider in the package. The package is structured flexibly, allowing you to use your choice of LLM provider such as OpenAI, Claude, Gemini etc. (OpenRouter is a unified interface for LLMs.)

🏁 Get Started

You can install the package via composer:

composer require moe-mizrak/laravel-prompt-alchemist

You can publish the config file with:

php artisan vendor:publish --tag=laravel-prompt-alchemist

This is the contents of the published config file:

return [
    'env_variables' => [
        'api_key'       => env('OPENROUTER_API_KEY'),
        'api_endpoint'  => env('OPENROUTER_API_ENDPOINT', 'https://openrouter.ai/api/v1/'),
        'default_model' => 'Your selection of the default model',
    ],

    'functions_yml_path' => 'Path to the functions yml file',

    'schemas' => [
        'function_payload_schema_path' => 'Path to the function payload schema yml file',
        'function_results_schema_path' => 'Path to the function results schema yml file',
    ],

    'instructions' => [
        'prompt_function_instructions'  => 'Instructions for the LLM about how to make use of provided functions, prompt and function_payload_schema in order to get the desired response',
        'function_results_instructions' => 'Instructions for the LLM about how to make use of provided function_results, prompt and function_results_schema in order to get the desired response',
        'generate_prompt_function_instructions' => 'Instructions for generating specific/customised prompt_function_instructions',
    ],

    'function_signature_mapping' => 'Function signature mapping data (FunctionSignatureMappingData)',
];

⚙ī¸ Configuration

After publishing the package configuration file, steps you should be following:

➡ī¸ env_variables Add following environment variables to your .env file in case you will be using OpenRouter as LLM provider (If your choice of LLM provider is different, then this is not required):

OPENROUTER_API_ENDPOINT=https://openrouter.ai/api/v1/
OPENROUTER_API_KEY=your_api_key
OPENROUTER_DEFAULT_MODEL=default_model
  • OPENROUTER_API_ENDPOINT: The endpoint URL for the OpenRouter API (default: https://openrouter.ai/api/v1/).
  • OPENROUTER_API_KEY: Your API key for accessing the OpenRouter API. You can obtain this key from the OpenRouter Dashboard. (e.g. sk-or-v1... )
  • OPENROUTER_DEFAULT_MODEL: Default model that will be used in the package - necessary for generateInstructions functionality (In codebase, you can still specify any model that you want for OpenRouter requests). You can check model list from OpenRouter Models. (e.g. 'mistralai/mistral-7b-instruct:free')

➡ī¸ functions_yml_path Add path of the functions yml file which is the callable function list of your project. The Generate Function List section provides a deep dive into functions yml file; how to define functions, format needs to be used etc. (e.g. __DIR__ . '/../resources/functions.yml').

➡ī¸ function_signature_mapping: Add mapping for function signature namings. The Define Function Signature Mapping section provides a deep dive into function signature mapping. (e.g. new FunctionSignatureMappingData(['parameters' => new MappingData(['path' => 'input_schema.properties', 'type' => 'array']), ...]) )

➡ī¸ schemas Add path of the schemas needed for function payload and function results payload.

  • function_payload_schema_path: Path of the function payload schema. The Function Payload Schema section provides a deep dive into function payload schema. (e.g. __DIR__ . '/../resources/schemas/function_payload_schema.yml')
  • function_results_schema_path: Path of the function results schema. The Function Results Schema section provides a deep dive into function results schema. (e.g. __DIR__ . '/../resources/schemas/function_results_schema.yml')

➡ī¸ instructions: Add instructions for prompt_function_instructions, function_results_instructions and generate_prompt_function_instructions.

  • prompt_function_instructions: Instructions for the LLM used for prompt function payload. The Generate Prompt Function Instructions section provides a deep dive into prompt function instructions. (e.g. You are an AI assistant that strictly follows instructions and provides response ...)
  • function_results_instructions: Instructions for the LLM used for function results payload. The Prepare Function Results Payload section provides a deep dive into function results instructions. (e.g. You will strictly follow the instructions as ...)
  • generate_prompt_function_instructions: Instructions for generating prompt_function_instructions by using generateInstructions function. The Generate Prompt Function Instructions section provides a deep dive into generating prompt function instructions. (e.g. Your role is to analyze the provided "functions" and ...)

⚡ Quick Usage Guide

This package is designed to be flexible, but for an easy quick start, follow the steps below:

  • Create a file named functions.yml under a directory of your choice. Modify config file accordingly for functions_yml_path. (Check Generate Function List for further details).
'functions_yml_path' => __DIR__ . '/../resources/functions.yml', // functions.yml is located under resources folder in this example.
  • Generate function list as in Use generateFunctionList method. It automatically adds functions to functions.yml file with all possible function signature details/descriptions considering also function's docblock.

(Add all classes and functions that LLM should take into account for its decisions, as much info as possible in FunctionData for better performance):

$class = Example::class;

$functionDataA = new FunctionData([
    'function_name' => 'getFinancialData',
    'parameters' => [
        new ParameterData([
            'name' => 'userId',
            'type' => 'int',
            'required' => true,
            'description' => 'The unique identifier for the user.',
            'example' => 12345
        ]),
        new ParameterData([
            'name' => 'startDate',
            'type' => 'string',
            'required' => true,
            'description' => 'The starting date for the timeframe (inclusive).',
            'example' => '2023-01-01'
        ]),
        new ParameterData([
           'name' => 'endDate',
           'type' => 'string',
           'required' => true,
           'description' => 'The ending date for the timeframe (inclusive).',
           'example' => '2023-01-31'
        ]),
    ],
    'visibility' => VisibilityType::PUBLIC,
    'description' => 'Retrieves financial data for a specific user and timeframe.',
    'return' => new ReturnData([
        'type' => 'object',
        'description' => 'An object containing details like totalAmount, transactions (array), and other relevant financial data.'
    ]),
]);
   
$functionDataB = new FunctionData([
    'function_name' => 'categorizeTransactions',
    'parameters' => [
        new ParameterData([
            'name' => 'transactions',
            'type' => 'array',
            'required' => true,
            'description' => 'An array of transactions with details like amount, date, and description.',
            'example' => [
                ['amount' => 100, 'date' => '2023-01-01', 'description' => 'Groceries'],
                ['amount' => 50, 'date' => '2023-01-02', 'description' => 'Entertainment']
            ]
        ]),
    ],
    'visibility' => VisibilityType::PUBLIC,
    'description' => 'Categorizes a list of transactions based on predefined rules or machine learning models.',
    'return' => new ReturnData([
        'type' => 'array',
        'description' => 'An array of transactions with an added "category" field if successfully categorized. Each transaction may also include a "confidenceScore" field.',
        'example' => [
            ['amount' => 100, 'date' => '2023-01-01', 'description' => 'Groceries', 'category' => 'Food', 'confidenceScore' => 0.95],
            ['amount' => 50, 'date' => '2023-01-02', 'description' => 'Entertainment', 'category' => 'Leisure', 'confidenceScore' => 0.8]
        ]
    ]),
]);
   
$functions = [$functionDataA, $functionDataB];

$fileName = __DIR__ . '/../resources/functions.yml'; // Path and the name of the file that function list will be generated
// Call generateFunctionList for automated function list generation in given $fileName (Creates file in this path if not existed).
LaravelPromptAlchemist::generateFunctionList($class, $functions, $fileName);
  • Create yml file for function payload schema and modify function_payload_schema_path in config accordingly. (Check Function Payload Schema for further details).

Sample function_payload_schema.yml as:

-
  function_name: getFinancialData
  parameters: [{ name: userId, type: int }, { name: startDate, type: string }, { name: endDate, type: string }]
  class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleA
-
  function_name: getCreditScore
  parameters: [{ name: userId, type: int }]
  class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleD

Config file path for function_payload_schema_path:

'function_payload_schema_path' => __DIR__ . '/../resources/schemas/function_payload_schema.yml', // function_payload_schema.yml is located under resources/schemas folder in this example.
  • Add OPENROUTER_API_KEY, OPENROUTER_DEFAULT_MODEL and OPENROUTER_API_ENDPOINT in your env file - as defined in env_variables in config (OPENROUTER_API_ENDPOINT already has default value which can be skipped, OPENROUTER_API_KEY and OPENROUTER_DEFAULT_MODEL should be set as described in Configuration section.)
  • Leave prompt_function_instructions in config untouched or check out Generate Prompt Function Instructions in order to generate customized new instructions.
  • Make Laravel OpenRouter request for Tool Use (Function Calling) so that LLM will make decision which functions to call for the given prompt. (Note: Laravel OpenRouter already comes with this package - as default LLM provider - so no need for a separate installation)
$prompt = 'Can tell me Mr. Boolean Bob credit score?';
$model = config('laravel-prompt-alchemist.env_variables.default_model'); // Check https://openrouter.ai/docs/models for supported models
$content = LaravelPromptAlchemist::preparePromptFunctionPayload($prompt); // Prepare payload for the request.

$messageData = new MessageData([
    'content' => json_encode($content),
    'role'    => RoleType::USER,
]);

$chatData = new ChatData([
    'messages' => [
        $messageData,
    ],
    'model'       => $model,
    'temperature' => 0.1, // Set temperature low to get better result. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
]);

// Send OpenRouter request
$response = LaravelOpenRouter::chatRequest($chatData);

Sample LLM returned functions:

// $response = LaravelOpenRouter::chatRequest($chatData);
$responseContentData = str_replace("\n", "", (Arr::get($response->choices[0], 'message.content'))); // Get content from the response.
$llmReturnedFunctions = json_decode($responseContentData, true); // Functions returned from LLM.

// Foreach $llmReturnedFunctions and get each function to validate:
$llmReturnedFunction = [ // Sample LLM returned function
    "function_name" => "getFinancialData",
    "parameters" => [
        [ "name" => "userId", "type" => "int"],
        [ "name" => "startDate", "type" => "string"],
        [ "name" => "endDate", "type" => "string"],
    ],
    'class_name' => 'MoeMizrak\LaravelPromptAlchemist\Tests\Example'
];

// Formed LLM returned function data (FunctionData).
$llmReturnedFunctionData = LaravelPromptAlchemist::formLlmReturnedFunctionData($llmReturnedFunction);

Invoke validateFunctionSignature for function signature validation of $llmReturnedFunctionData:

$isValid = LaravelOpenRouter::validateFunctionSignature($llmReturnedFunctionData);
  • And finally, call functions returned from the LLM which are necessary for answering the prompt (Since function signature is validated, it is now safe to call LLM returned functions). (Check Call Function (Actual Function Call) for further details).

(Note: You need to set parameter values to be able to call the function. Required parameters have to be set, otherwise ErrorData is returned. It is a better practice to set the parameter values as checking validateFunctionSignature since type of provided values also gets validated if set, but it can also be done in this step.)

Set parameter values before calling function as below.

// Create parameter values, you just need parameter name and its value in correct type (int, string, array, object ...)
$parameters = [
    new ParameterData([
        'name' => 'userId',
        'value' => 99, // int userId
    ]),
    new ParameterData([
        'name' => 'startDate',
        'value' => '2023-06-01', // string startDate
    ]),
    new ParameterData([
        'name' => 'endDate',
        'value' => '2023-07-01', // string endDate
    ]),
];

if (true === $isValid) {
    // Set parameter values.
    $llmReturnedFunctionData->setParameterValues($parameters);
}

Call the function as following:

$functionResultData = LaravelPromptAlchemist::callFunction($llmReturnedFunctionData);

Sample function result ($functionResultData) DTO object:

output:

FunctionResultData([
    'function_name' => 'getFinancialData',
    'result'        => (object) [
        'totalAmount' => 1000.0,
        'transactions' => [
            ['amount' => 100, 'date' => '2023-01-01', 'description' => 'Groceries'],
            ['amount' => 200, 'date' => '2023-01-02', 'description' => 'Utilities'],
        ],
        'message' => 'Retrieved financial data for user 99 from 2023-06-01 to 2023-07-01'
     ],
])

Where function_name is the name of the function called as the name suggested, and result is the function call result can be anything (void, array, object, bool etc. whatever your function returns to.)

  • Optionally, you can also send function results to LLM so that regarding function_results_schema, it will return the answer. (Check Prepare Function Results Payload for further details)
$prompt = 'Can tell me Mr. Boolean Bob credit score?';
$model = config('laravel-prompt-alchemist.env_variables.default_model'); // Check https://openrouter.ai/docs/models for supported models
$functionResults = [
    new FunctionResultData([
        'function_name' => 'getFinancialData',
        'result' => [
            'totalAmount' => 122,
            'transactions' => [
                [
                    'amount'      => 12,
                    'date'        => '2023-02-02',
                    'description' => 'food',
                ],
            ]
        ]
    ]),
    new FunctionResultData([
        'function_name' => 'getCreditScore',
        'result' => [
            'creditScore' => 0.8,
            'summary' => 'reliable',
        ]
    ]),
    ...
];
// Prepare function results payload for the request.
$content = LaravelPromptAlchemist::prepareFunctionResultsPayload($prompt, $functionResults);

$messageData = new MessageData([
    'content' => json_encode($content),
    'role'    => RoleType::USER,
]);

$chatData = new ChatData([
    'messages' => [
        $messageData,
    ],
    'model'       => $model,
    'temperature' => 0.1, // Set temperature low to get better result. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
]);

// Send OpenRouter request for function results
$response = LaravelOpenRouter::chatRequest($chatData);

Where $response is the function_results_schema formed answer returned from the LLM according to function_results_instructions.

🎨 Usage

This package provides two ways to interact with the Laravel Prompt Alchemist package:

Using Facade

The LaravelPromptAlchemist facade offers a convenient way to make Laravel Prompt Alchemist requests. Following sections will lead you through further configuration and usage of LaravelPromptAlchemist facade.

Generate Function List

In order to generate function list (functions.yml), you can either:

Manually create function list

Create function signatures manually using your chosen naming convention in a flat associative array structure.

ℹī¸ Ensure coherence with function_signature_mapping, which aligns your naming practices with the package format for integration. (Note: Please refer to Define Function Signature Mapping section for which function signatures should be defined in function list yml file for better performance)

  • Create yml file for function list with any preferred naming and directory.

e.g. You can create a file named functions.yml in the resources folder under your project

__DIR__ . '/../resources/functions.yml'
  • Define functions in yml file that you will be using for the package (for Tool Use - Function Calling).

e.g. You can create functions as samples below in a flat associative array structure or your choice of function naming convention.

Recommended naming convention (generateFunctionList method for generating a function list, also outputs the function list in this format):

-
function_name: getFinancialData
parameters: [{ name: userId, type: int, required: true, description: 'The unique identifier for the user.', example: 12345 }, { name: startDate, type: string, required: true, description: 'The starting date for the timeframe (inclusive).', example: '2023-01-01' }, { name: endDate, type: string, required: true, description: 'The ending date for the timeframe (inclusive).', example: '2023-01-31' }]
visibility: public
description: 'Retrieves financial data for a specific user and timeframe. '
return: { type: object, description: 'An object containing details like totalAmount, transactions (array), and other relevant financial data.' }
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\Example
- 
function_name: categorizeTransactions
parameters: [{ name: transactions, type: array, required: true, description: 'An array of transactions with details like amount, date, and description.', example: [{ amount: 100, date: '2023-01-01', description: 'Groceries' }, { amount: 50, date: '2023-01-02', description: 'Entertainment' }] }]
visibility: public
description: 'Categorizes a list of transactions based on predefined rules or machine learning models. '
return: { type: array, description: 'An array of transactions with an added "category" field if successfully categorized. Each transaction may also include a "confidenceScore" field.', example: [{ amount: 100, date: '2023-01-01', description: 'Groceries', category: 'Food', confidenceScore: 0.95 }, { amount: 50, date: '2023-01-02', description: 'Entertainment', category: 'Leisure', confidenceScore: 0.8 }] }
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\Example

Or, another naming convention in a flat associative array structure can be as:

- function: getFinancialData
  input_schema:
    type: object
    properties:
      - name: userId
        type: int
        required: true
        description: 'The unique identifier for the user.'
      - name: startDate
        type: string
        required: true
        description: 'The starting date for the timeframe (inclusive).'
      - name: endDate
        type: string
        required: true
        description: 'The ending date for the timeframe (inclusive).'
  description: 'Retrieves financial data for a specific user and timeframe.'
  return:
    type: object
    description: 'An object containing details like totalAmount, transactions (array), and other relevant financial data.'
  class: MoeMizrak\LaravelPromptAlchemist\Tests\Example
- function: categorizeTransactions
  input_schema:
    properties:
      - name: transactions
        type: array
        required: true
        description: 'An array of transactions with details like amount, date, and description.'
  description: 'Categorizes a list of transactions based on predefined rules or machine learning models.'
  return:
    type: array
    description: 'An array of transactions with an added "category" field if successfully categorized. Each transaction may also include a "confidenceScore" field.'
  class: MoeMizrak\LaravelPromptAlchemist\Tests\Example
Use generateFunctionList method

Use generateFunctionList function to generate a detailed function list from a given class and write it to a file in yml format.
(For each different class name, you need to run the generateFunctionList to generate function list - functions.yml, list gets appended whenever the generateFunctionList is called)

ℹī¸ This is a good practice to automate the generation of function list. The fields that are defined in the function description (functionData) overwrites the function predefined fields if exists. If a function signature field is missing then it is added to function list definitions.

$class = Example::class; // Or you can give fully qualified class name string as $class = 'MoeMizrak\LaravelPromptAlchemist\Tests\Example'

// Function descriptions that will be used to generate function list (functions.yml) 
$functionDataA = new FunctionData([
    'function_name' => 'getFinancialData',
    'parameters' => [
        new ParameterData([
            'name' => 'userId',
            'type' => 'int',
            'required' => true,
            'description' => 'The unique identifier for the user.',
            'example' => 12345
        ]),
        new ParameterData([
            'name' => 'startDate',
            'type' => 'string',
            'required' => true,
            'description' => 'The starting date for the timeframe (inclusive).',
            'example' => '2023-01-01'
        ]),
        new ParameterData([
           'name' => 'endDate',
           'type' => 'string',
           'required' => true,
           'description' => 'The ending date for the timeframe (inclusive).',
           'example' => '2023-01-31'
        ]),
    ],
    'visibility' => VisibilityType::PUBLIC,
    'description' => 'Retrieves financial data for a specific user and timeframe.',
    'return' => new ReturnData([
        'type' => 'object',
        'description' => 'An object containing details like totalAmount, transactions (array), and other relevant financial data.'
    ]),
]);
   
$functionDataB = new FunctionData([
    'function_name' => 'categorizeTransactions',
    'parameters' => [
        new ParameterData([
            'name' => 'transactions',
            'type' => 'array',
            'required' => true,
            'description' => 'An array of transactions with details like amount, date, and description.',
            'example' => [
                ['amount' => 100, 'date' => '2023-01-01', 'description' => 'Groceries'],
                ['amount' => 50, 'date' => '2023-01-02', 'description' => 'Entertainment']
            ]
        ]),
    ],
    'visibility' => VisibilityType::PUBLIC,
    'description' => 'Categorizes a list of transactions based on predefined rules or machine learning models.',
    'return' => new ReturnData([
        'type' => 'array',
        'description' => 'An array of transactions with an added "category" field if successfully categorized. Each transaction may also include a "confidenceScore" field.',
        'example' => [
            ['amount' => 100, 'date' => '2023-01-01', 'description' => 'Groceries', 'category' => 'Food', 'confidenceScore' => 0.95],
            ['amount' => 50, 'date' => '2023-01-02', 'description' => 'Entertainment', 'category' => 'Leisure', 'confidenceScore' => 0.8]
        ]
    ]),
]);
   
$functions = [$functionDataA, $functionDataB];

$fileName = __DIR__ . '/../resources/functions.yml'; // Path and the name of the file that function list will be generated
// Call generateFunctionList for automated function list generation in given $fileName (Creates file in this path if not existed).
LaravelPromptAlchemist::generateFunctionList($class, $functions, $fileName);

Alternative way for creating the functions array; when function signatures and docblock are well-defined, simply adding function names suffices to create a comprehensive function list:

$class = Example::class; // Or you can give fully qualified class name string as $class = 'MoeMizrak\LaravelPromptAlchemist\Tests\Example'
// Name of the functions that will be added to the list - functions that will be used for Tool Use (Function Calling)
$functions = [
    'getFinancialData',
    'categorizeTransactions',
];
$fileName = __DIR__ . '/../resources/functions.yml'; // Path and the name of the file that function list will be generated
// Call generateFunctionList for automated function list generation in given $fileName (Creates file in this path if not existed).
LaravelPromptAlchemist::generateFunctionList($class, $functions, $fileName);

Sample of well-defined function signature and docblock. (Docblock description with enough info, type-hinted params, return type declaration and return tag description in docblock, parameter descriptions in docblock):

/**
 * This public function is intended for testing purposes. It accepts a string and int parameters and returns a string.
 * It has additional parameter descriptions and detailed docblock.
 *
 * @param string $stringParam This is the string param description
 * @param int $intParam This is the int param description
 *
 * @return string This is the return value description
 */
public function detailedDocBlockFunction(string $stringParam, int $intParam = 2): string
{
    return 'detailed docblock function return value ' . $stringParam . ' ' . $intParam;
}

Sample of poorly-defined function signature and docblock. (no docblock, no type-hint for params, no return type declaration):

function noExtraInfoProvidedFunction($stringParam, $intParam)
{
    return 'missing parameter and docblock function return value ' . $stringParam . ' ' . $intParam;
}

Sample of partially-defined function signature and docblock:

/**
 * This public function is intended for testing purposes.
 * It has missing parameter descriptions and missing type-hint.
 *
 * @return string This is the return value description
 */
public function functionWithSomeMissingDocBlockAndMissingTypeHint($stringParam, $intParam): string
{
    return 'missing parameter docblock function return value ' . $stringParam . ' ' . $intParam;
}

Based on your best practices in your codebase, you can choose how to generate function list (functions.yml).
  1. If functions are well-defined in your codebase, you can use alternative approach to send the function names in functions array with no additional info (generateFunctionList does everything for you).
  2. If functions are poorly-defined in your codebase, then it is best to use the main approach by creating FunctionData DTO for each function and setting function related info/descriptions to create functions array.
  3. If functions are partially-defined in your codebase, then similarly using the main approach is the best option since more info is better for LLM to know about your functions to make the best decision.

Note: You can just add missing/ambiguous/poorly-defined fields in FunctionData DTO and skip the descriptions/fields that are well-defined since generateFunctionList analyses function and adds all possible information about the function. Also note that fields added to FunctionData DTO overwrite the existing predefined descriptions/fields in the function . (Basically if a field is added to FunctionData DTO, it is taken into account; if a field is NOT added to FunctionData and exists in function declaration then this predefined description/field is added to function list).

Define Function Signature Mapping

Based on the function list (functions.yml), align your naming practices for functions with the package format for integration. It is better practice to set all possible fields for function signature (FunctionSignatureMappingData) for better performance since more info provided for LLM means better result.

function_signature_mapping needs to be defined in the config file as following examples:

(As shown in examples below, use '[]' for the arrays. In package, it is replaced with the index key as e.g. parameters[].name becomes parameters.{key}.name which is parameters.0.name for the first array index, parameters.1.name for the second etc.)

'function_signature_mapping' => new FunctionSignatureMappingData([
    'function_name' => new MappingData([
        'path' => 'function_name',
        'type' => 'string',
    ]),
    'function_description' => new MappingData([
        'path' => 'description',
        'type' => 'string',
    ]),
    'function_visibility' => new MappingData([
        'path' => 'visibility',
        'type' => 'string',
    ]),
    'function_return_type' => new MappingData([
        'path' => 'return.type',
        'type' => 'string',
    ]),
    'function_return_description' => new MappingData([
        'path' => 'return.description',
        'type' => 'string',
    ]),
    'function_return_example' => new MappingData([
        'path' => 'return.example',
        'type' => 'mixed',
    ]),
    'parameters' => new MappingData([
        'path' => 'parameters', // could be arguments, parameter_definitions, input_schema.properties, parameters.properties
        'type' => 'array',
    ]),
    'parameter_name' => new MappingData([
        'path' => 'parameters[].name', // since parameters field is array, '[]' states the index key which will be resolved in the package as 'parameters.0.name' for the first array and so on.
        'type' => 'string',
    ]),
    'parameter_type' => new MappingData([
        'path' => 'parameters[].type',
        'type' => 'string',
    ]),
    'parameter_required_info' => new MappingData([
        'path' => 'parameters[].required',
        'type' => 'boolean',
    ]),
    'parameter_description' => new MappingData([
        'path' => 'parameters[].description',
        'type' => 'string',
    ]),
    'parameter_example' => new MappingData([
        'path' => 'parameters[].example',
        'type' => 'mixed',
    ]),
    'parameter_default' => new MappingData([
        'path' => 'parameters[].default',
        'type' => 'mixed',
    ]),
    'class_name' => new MappingData([
        'path' => 'class_name',
        'type' => 'string',
    ])
]),
'function_signature_mapping' => new FunctionSignatureMappingData([
    'function_name' => new MappingData([
        'path' => 'function',
        'type' => 'string',
    ]),
    'function_description' => new MappingData([
        'path' => 'description',
        'type' => 'string',
    ]),
    'function_visibility' => new MappingData([
        'path' => 'visibility',
        'type' => 'string',
    ]),
    'function_return_type' => new MappingData([
        'path' => 'return.type',
        'type' => 'string',
    ]),
    'function_return_description' => new MappingData([
        'path' => 'return.description',
        'type' => 'string',
    ]),
    'function_return_example' => new MappingData([
        'path' => 'return.example',
        'type' => 'mixed',
    ]),
    'input_schema_type' => new MappingData([
        'path' => 'input_schema.type',
        'type' => 'string',
    ]),
    'parameters' => new MappingData([
        'path' => 'input_schema.properties',
        'type' => 'array',
    ]),
    'parameter_name' => new MappingData([
        'path' => 'input_schema.properties[].name', // since properties field is array, '[]' states the index key which will be resolved in the package as 'properties.0.name' for the first array and so on.
        'type' => 'string',
    ]),
    'parameter_type' => new MappingData([
        'path' => 'input_schema.properties[].type',
        'type' => 'string',
    ]),
    'parameter_required_info' => new MappingData([
        'path' => 'input_schema.properties[].required',
        'type' => 'boolean',
    ]),
    'parameter_description' => new MappingData([
        'path' => 'input_schema.properties[].description',
        'type' => 'string',
    ]),
    'parameter_example' => new MappingData([
        'path' => 'input_schema.properties[].example',
        'type' => 'mixed',
    ]),
    'parameter_default' => new MappingData([
        'path' => 'input_schema.properties[].default',
        'type' => 'mixed',
    ]),
    'class_name' => new MappingData([
        'path' => 'class',
        'type' => 'string',
    ])
]),

Regarding these 2 examples, you can define your function_signature_mapping in the configuration file depending on your choice of naming convention.

Generate Prompt Function Instructions

This section defines the instructions which gives strict description of desired format answers and how LLM provider will process provided prompt, schemas etc.

You can use your own created/generated instructions for prompt_function_instructions, or use generateInstructions method.

generateInstructions method simply generates customized instructions regarding your function list (functions.yml), function payload schema (function_payload_schema.yml) and prompt for generating instructions (generate_prompt_function_instructions in config).

(Note: generate_prompt_function_instructions is the prompt/instructions in config that describe how to generate specific/customized instructions with functions and function_payload_schema)

Sample generate_prompt_function_instructions for generateInstructions call:

You are an AI assistant tasked with providing instructions to another AI system on how to respond to a given prompt with a specific JSON format. Your role is to analyze the provided "functions" and "function_payload_schema" and create a set of instructions that will ensure the other AI system generates a response following the specified format.
            The "functions" field contains a list of available functions, their parameters, descriptions, and return types. The "function_payload_schema" field specifies the expected format for the response, which should be a JSON array listing the required fields for each function.
            Your instructions should cover the following points:
                1. Read the provided "prompt" and the list of available "functions".
                2. Identify which function(s) from the "functions" list are needed to answer the prompt.
                3. Analyze the "function_payload_schema" to determine the required fields for each function in the response JSON array. Do not add or omit any fields from the schema.
                4. Explain that the response should ONLY contain a JSON array following the exact format specified in the "function_payload_schema". No additional fields, values, text, or explanations should be included.
                5. Specify that the JSON array should list the required fields for each function, as specified in the "function_payload_schema".
                6. Emphasize that no other information beyond the JSON array matching the "function_payload_schema" format should be added.
                7. Ensure that the response can be directly used in PHP code without any modifications.
                8. For the parameter fields of each function, analyze the "function_payload_schema" and include the fields exactly as specified in the schema, without making any assumptions about the field names or structure.
                9. Clarify that no actual values for the parameters should be provided. Only the parameter fields as specified in the "function_payload_schema" should be included.
                10. If no relevant function is found in the "functions" list to answer the prompt, the response should be the string "NULL" without any additional description or text.
                11. If the other AI system cannot understand or follow the instructions, it should return the string "NULL" without any additional description or text.
            Your response should be a clear and concise set of instructions that the other AI system can follow to generate the desired JSON response format based on the provided "functions" and "function_payload_schema".
            OK, now provide the instructions as described above for the other AI system to generate the desired JSON response format based on the provided "functions" and "function_payload_schema"

In order to generate prompt_function_instructions, you can call generateInstructions method:

LaravelPromptAlchemist::generateInstructions();

Sample response of generateInstructions will look like:

You are an AI assistant that strictly follows instructions and provides responses in a specific format.
            Your task is to analyze a given prompt and identify the required functions from a provided list to answer the prompt.
            Your response should be a JSON array that lists the required function names, their parameters (name and type only), and the class_name, following the exact format specified in the "function_payload_schema".
                Do not include any additional information, explanations, or values beyond what is specified in the schema. Adhere to the following instructions:
                1. Read the provided "prompt" and the list of available "functions".
                2. Identify which function(s) from the "functions" list are needed to answer the prompt.
                3. Your response should ONLY contain a JSON array following the exact format specified in the "function_payload_schema". Do not include any additional fields, values, text, or explanations.
                4. The JSON array should list the required function names, their parameters (name and type only), and the class_name.
                5. Do not add any other information beyond the JSON array matching the "function_payload_schema" format.
                6. Ensure that the response can be directly used in PHP code without any modifications.
                7. Do not provide any actual values for the parameters. Only include the parameter names and types as specified in the "function_payload_schema".
                8. If you do not understand the instructions or cannot provide a response following the specified format, respond with "NULL".
                9. If no relevant function is found in the "functions" list to answer the prompt, the response should be the string "NULL" without any additional description or text

Define Schemas

This section defines the desired schema samples that are used for formatting the function payload and function results. Basically schemas are for deciding the response format of LLM. They are sent to LLM along with other info (prompt, functions - function results, instructions etc.) and in instructions LLM asked to give results as same format as in schema provided. In this way, LLM response can be resolved in package so that it can be used for Tool Use (Function Calling).

Function Payload Schema

Schema that defines the structure of the function payload.

  • Create yml file for function payload schema with any preferred naming and directory, add it to config file under schemas.
'schemas' => [
    'function_payload_schema_path' => __DIR__ . '/../resources/schemas/function_payload_schema.yml',
]
  • Define schema as example given below. The naming convention should align with the function list (functions.yml) because LLM response will depend on this schema and response will be validated in the package by comparing the function list (functions.yml) file.
-
  function_name: getFinancialData
  parameters: [{ name: userId, type: int }, { name: startDate, type: string }, { name: endDate, type: string }]
  class_name: MoeMizrak\LaravelPromptAlchemist\Tests\Example
-
  function_name: getCreditScore
  parameters: [{ name: userId, type: int }]
  class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleD
Function Results Schema

Schema that defines the structure of the function results. This schema is for deciding the final response where function results are sent to LLM to form the response after Tool Use (Function Calling).

This schema is not required if you will not be sending function call results to LLM. You might prefer using function results in your codebase to derive a response directly.

  • Create yml file for function results schema with any preferred naming and directory, add it to config file under schemas.
'schemas' => [
    'function_results_schema_path' => __DIR__ . '/../resources/schemas/function_results_schema.yml',
]
  • Define schema as example given below.
-
  function_name: getFinancialData
  result: [{ name: transactions, type: array, value: [{ amount: , date: '2023-02-02', description: shoes }] }, { name: totalAmount, type: int, value: 1234 }]
-
  function_name: getCreditScore
  result: [{ name: creditScore, type: float, value: 0.5 }, { name: summary, type: string, value: reliable }]

Prepare Prompt Function Payload

This method is responsible for creating a structured payload template based on a given prompt and a list of functions (functions.yml) which later will be sent to an LLM provider. This method constructs an array containing instructions, the prompt, a list of functions, and a function payload schema. The instructions detail how the prompt should be processed, ensuring the response strictly adheres to the provided format.

For the prompt which you will be used for Tool Use (Function Calling):

$prompt = 'Can tell me Mr. Boolean Bob credit score?';

This is how you can call preparePromptFunctionPayload method by using facade:

LaravelPromptAlchemist::preparePromptFunctionPayload($prompt);

And this is the expected prepared payload response sample:

[
  "prompt" => "Can tell me Mr. Boolean Bob credit score?",
  "instructions" => "You are an AI assistant that strictly follows instructions and provides responses in a specific format.\n
    Your task is to analyze a given prompt and identify the required functions from a provided list to answer the prompt.\n
    Your response should be a JSON array that lists the required function names, their parameters (name and type only), and the class_name, following the exact format specified in the \"function_payload_schema\".\n
    Do not include any additional information, explanations, or values beyond what is specified in the schema. Adhere to the following instructions:\n
    1. Read the provided \"prompt\" and the list of available \"functions\".\n
    2. Identify which function(s) from the \"functions\" list ...",
  "functions" => [
    [
        "function_name" => "getFinancialData",
        "parameters" => [
            ["name" => "userId", "type" => "int"],
            ["name" => "startDate", "type" => "string"],
            ["name" => "endDate", "type" => "string"]
        ],
        "visibility" => "public",
        "description" => "Retrieves financial data for a specific user and timeframe.",
        "return" => [
            "type" => "object",
            "description" => "An object containing details like totalAmount, transactions (array), and other relevant financial data."
        ],
        "class_name" => "MoeMizrak\LaravelPromptAlchemist\Tests\Example"
    ],
    [
        "function_name" => "getCreditScore",
        "parameters" => [
            ["name" => "userId", "type" => "int"]
        ],
        "visibility" => "public",
        "description" => "Retrieves the current credit score for a specific user.",
        "return" => [
            "type" => "object",
            "description" => "An object containing the credit score, credit report summary, and any relevant notes."
        ],
        "class_name" => "MoeMizrak\LaravelPromptAlchemist\Tests\Example"
    ],
    ...
  ],
  "function_payload_schema" => [
    [
        "function_name" => "getFinancialData",
        "parameters" => [
            ["name" => "userId", "type" => "int"],
            ["name" => "startDate", "type" => "string"],
            ["name" => "endDate", "type" => "string"]
        ],
        "class_name" => "MoeMizrak\LaravelPromptAlchemist\Tests\ExampleA"
    ],
    [
        "function_name" => "getCreditScore",
        "parameters" => [
            ["name" => "userId", "type" => "int"]
        ],
        "class_name" => "MoeMizrak\LaravelPromptAlchemist\Tests\ExampleD"
    ],
    ...
  ]
]

Send Tool Use (Function Calling) Request to OpenRouter

Since this package is designed in a flexible way, you may use Laravel OpenRouter (please check out OpenRouter github repository for more information) which is used as the default LLM provider for this package, or you may use any other LLM provider with this package to send Tool Use (Function Calling) request.
This is the sample OpenRouter request:

$prompt = 'Can tell me Mr. Boolean Bob credit score?';
$model = config('laravel-prompt-alchemist.env_variables.default_model'); // Check https://openrouter.ai/docs/models for supported models
$content = LaravelPromptAlchemist::preparePromptFunctionPayload($prompt);

$messageData = new MessageData([
    'content' => json_encode($content),
    'role'    => RoleType::USER,
]);

$chatData = new ChatData([
    'messages' => [
        $messageData,
    ],
    'model'       => $model,
    'temperature' => 0.1, // Set temperature low to get better result. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
]);

// Send OpenRouter request
$response = LaravelOpenRouter::chatRequest($chatData);

Sample Laravel OpenRouter response (ResponseData is returned):

output:

ResponseData([
    'id' => 'gen-YFd68mMgTkrfHVvkdemwYxdGSfZA',
    'model' => 'mistralai/mistral-7b-instruct:free',
    'object' => 'chat.completion',
    'created' => 1719251736,
    'choices' => [
        0 => [
            'index' => 0,
            'message' => [
                'role' => 'assistant',
                'content' => '["function_name":"getFinancialData", "parameters":[{"name":"userId","type":"int"},{"name":"startDate","type":"string"},{"name":"endDate","type":"string"}],"function_name":"categorizeTransactions", "parameters":[{"name":"transactions","type":"array"}],"function_name":"getTopCategories", "parameters":[{"name":"transactions","type":"array"}]]',
            ],
            'finish_reason' => 'stop',
        ]
    ],
    'usage' => UsageData([
        'prompt_tokens' => 1657,
        'completion_tokens' => 97,
        'total_tokens' => 1754,
    ])    
]);

Validate Function Signature

Validates a function signature returned by the LLM. It returns boolean or ErrorData for wrong function signature with missing/wrong field. You can retrieve LLM returned functions from ResponseData returned by Send Tool Use (Function Calling) Request to OpenRouter.
Sample LLM returned functions:

// $response = LaravelOpenRouter::chatRequest($chatData); as shown in  [Send Tool Use (Function Calling) Request to OpenRouter] section
$responseContentData = str_replace("\n", "", (Arr::get($response->choices[0], 'message.content'))); // Get content from the response.
$llmReturnedFunctions = json_decode($responseContentData, true); // Functions returned from LLM.

// Foreach $llmReturnedFunctions and get each function to validate:
$llmReturnedFunction = [ // Sample LLM returned function
    "function_name" => "getFinancialData",
    "parameters" => [
        [ "name" => "userId", "type" => "int"],
        [ "name" => "startDate", "type" => "string"],
        [ "name" => "endDate", "type" => "string"],
    ],
    'class_name' => 'MoeMizrak\LaravelPromptAlchemist\Tests\Example'
];

// Form LLM returned function data (FunctionData).
$llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction);

// Add parameter values if exists, only name and value of the parameter are set.
$parameters = [
    new ParameterData([
        'name' => 'userId',
        'value' => 99,
    ]),
    new ParameterData([
        'name' => 'startDate',
        'value' => '2023-06-01',
    ]),
    new ParameterData([
        'name' => 'endDate',
        'value' => '2023-07-01',
    ]),
];
// Set parameter values in $llmReturnedFunctionData DTO
$llmReturnedFunctionData->setParameterValues($parameters);

And this is how to validate a function signature returned from LLM (formed llm returned function as above, also values are set for parameters):

$isValid = LaravelOpenRouter::validateFunctionSignature($llmReturnedFunctionData); // $isValid is bool or ErrorData

In case the LLM returned function signature is invalid, this is a sample ErrorData returned:

output:

ErrorData([
    'code' => 400,
    'message' => 'Function invalidFunctionName does not exist in class MoeMizrak\LaravelPromptAlchemist\Tests\Example'
]);

Call Function (Actual Function Call)

This method make actual function call with provided function signatures along with values for parameters. Since Validate Function Signature is performed before this step, function signature is safe to make this function call (Also parameter values are set in Validate Function Signature step). Regardless of the function visibility (private, protected, public etc.), function can be called directly.

This package makes actual function calls unlike built-in capabilities of LLMs that may only list functions. Basically with this package, you can get the list of the functions regarding your prompt from LLM, and then make an actual function calls to retrieve function results.

This is how to call function:

$functionResult = LaravelPromptAlchemist::callFunction($llmReturnedFunctionData);

Result is returned as FunctionResultData DTO object as a sample result given below:

output:

FunctionResultData([
    'function_name' => 'getFinancialData',
    'result' => [
        'totalAmount' => 122,
        'transactions' => [
            [
                'amount'      => 12,
                'date'        => '2023-02-02',
                'description' => 'food',
            ],
        ]
    ]
]),

Where function_name is the name of the function called, result is whatever this function returns (array, bool, int, mixed, object, void etc.)

Prepare Function Results Payload

This method is responsible for creating a structured payload template based on a given prompt and a function results which later will be sent to an LLM provider. This method constructs an array containing instructions, the prompt, a function results, and a function results schema. The instructions detail how the prompt should be processed, ensuring the response strictly adheres to the provided format as function results schema.

Prompt which will be used for function results payload preparation:

$prompt = 'Can tell me Mr. Boolean Bob credit score?';

$functionResults = [
    new FunctionResultData([
        'function_name' => 'getFinancialData',
        'result' => [
            'totalAmount' => 122,
            'transactions' => [
                [
                    'amount'      => 12,
                    'date'        => '2023-02-02',
                    'description' => 'food',
                ],
            ]
        ]
    ]),
    new FunctionResultData([
        'function_name' => 'getCreditScore',
        'result' => [
            'creditScore' => 0.8,
            'summary' => 'reliable',
        ]
    ]),
    ...
];

This is how you can call prepareFunctionResultsPayload method by using facade:

LaravelPromptAlchemist::prepareFunctionResultsPayload($prompt, $functionResults);

And this is the expected prepared function results payload sample:

[
  "prompt" => "Can tell me Mr. Boolean Bob credit score?",
  "instructions" => "You will strictly follow the instructions:\n
    - Understand the provided prompt and answer the prompt using the function_results (needed info is provided in function_results). If function_results are not sufficient enough, then your answer will be \"Please provide more information about [missing information]\"\n
    - Respond based on the function_results_schema sample provided (Do not add any extra info, exactly the same format provided in function_results_schema).\n
    - Format the response as an array following  ...",
  "function_results" => [
    [
        "function_name" => "getFinancialData",
        "result" => [
            "totalAmount" => 122,
            "transactions" => [
                [
                "amount" => 12,
                "date" => "2023-02-02",
                "description" => "food"
                ]
            ]
        ]
    ],
    [
        "function_name" => "getCreditScore",
        "result" => [
            "creditScore" => 0.8,
            "summary" => "reliable"
        ]
    ]
    ...
  ],
  "function_results_schema" => [
    [
        "function_name" => "getFinancialData",
        "result" => [
            [
                "name" => "transactions",
                "type" => "array",
                "value" => [
                    [
                        "amount" => null,
                        "date" => "2023-02-02",
                        "description" => "shoes"
                    ]
                ]
            ],
            [
                "name" => "totalAmount",
                "type" => "int",
                "value" => 1234
            ]
      ]
    ],
    [
        "function_name" => "getCreditScore",
        "result" => [
            [
                "name" => "creditScore",
                "type" => "float",
                "value" => 0.5
            ],
            [
                "name" => "summary",
                "type" => "string",
                "value" => "reliable"
            ]
        ]
    ]
    ...
  ]
]

Send Function Results to OpenRouter

Since this package is designed in a flexible way, you may use Laravel OpenRouter (please check out OpenRouter github repository for more information) which is used as the default LLM provider for this package, or you may use any other LLM provider with this package to send Tool Use (Function Calling) request.
This is the sample OpenRouter request for function results:

$prompt = 'Can tell me Mr. Boolean Bob credit score?';
$functionResults = [
    new FunctionResultData([
        'function_name' => 'getFinancialData',
        'result' => [
            'totalAmount' => 122,
            'transactions' => [
                [
                    'amount'      => 12,
                    'date'        => '2023-02-02',
                    'description' => 'food',
                ],
            ]
        ]
    ]),
    new FunctionResultData([
        'function_name' => 'getCreditScore',
        'result' => [
            'creditScore' => 0.8,
            'summary' => 'reliable',
        ]
    ]),
    ...
];
$content = LaravelPromptAlchemist::prepareFunctionResultsPayload($prompt, $functionResults);
$model = config('laravel-prompt-alchemist.env_variables.default_model'); // Check https://openrouter.ai/docs/models for supported models

$messageData = new MessageData([
    'content' => json_encode($content),
    'role'    => RoleType::USER,
]);

$chatData = new ChatData([
    'messages' => [
        $messageData,
    ],
    'model'       => $model,
    'temperature' => 0.1, // Set temperature low to get better result. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
]);

// Send OpenRouter request
$response = LaravelOpenRouter::chatRequest($chatData);

Sample Laravel OpenRouter response (ResponseData is returned):

output:

ResponseData([
    'id' => 'gen-YFd68mMgTkrfHVvkdemwYxdGSfZA',
    'model' => 'mistralai/mistral-7b-instruct:free',
    'object' => 'chat.completion',
    'created' => 1719440793,
    'choices' => [
        0 => [
            'index' => 0,
            'message' => [
                'role' => 'assistant',
                'content' => '[{"name": "creditScore", "type": "float", "value": 0.8}, {"name": "summary", "type": "string", "value": "reliable"}]',
            ],
            'finish_reason' => 'stop',
        ]
    ],
    'usage' => UsageData([
        'prompt_tokens' => 657,
        'completion_tokens' => 69,
        'total_tokens' => 726,
    ])    
]);

Using PromptAlchemistRequest Class

You can also inject the PromptAlchemistRequest class in the constructor of your class and use its methods directly.

public function __construct(protected PromptAlchemistRequest $promptAlchemistRequest) {}

While everything else stays same with the Using Facade, with PromptAlchemistRequest you can call methods as following:

// generateFunctionList request.
$this->promptAlchemistRequest->generateFunctionList($class, $functions, $fileName);

// Generate instructions request.
$this->promptAlchemistRequest->generateInstructions();

// Prepare prompt function payload request.
$this->promptAlchemistRequest->preparePromptFunctionPayload($prompt);

// Forms LLM returned function into the FunctionData.
$this->promptAlchemistRequest->formLlmReturnedFunctionData($llmReturnedFunction);

// Validate function signature returned by the LLM request.
$this->promptAlchemistRequest->validateFunctionSignature($llmReturnedFunctionData); // $isValid is bool or ErrorData

// Make an actual function call.
$this->promptAlchemistRequest->callFunction($llmReturnedFunctionData);

// Prepare function results payload request.
$this->promptAlchemistRequest->prepareFunctionResultsPayload($prompt, $functionResults);

đŸ’Ģ Contributing

We welcome contributions! If you'd like to improve this package, simply create a pull request with your changes. Your efforts help enhance its functionality and documentation.

📜 License

Laravel Prompt Alchemist is an open-sourced software licensed under the MIT license.