Skip to content

Commit

Permalink
Merge pull request #12 from moe-mizrak/dev
Browse files Browse the repository at this point in the history
callFunction is added to package
  • Loading branch information
moe-mizrak committed Jun 26, 2024
2 parents 8efa8ca + 9a58f2d commit d8df772
Show file tree
Hide file tree
Showing 12 changed files with 387 additions and 37 deletions.
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@
"spatie/data-transfer-object": "^3.9.1",
"guzzlehttp/guzzle": "^7.8",
"caseyamcl/guzzle_retry_middleware": "^2.9",
"moe-mizrak/laravel-openrouter": "^1.0",
"spatie/invade": "^2.1",
"phpdocumentor/reflection-docblock": "^5.4"
"phpdocumentor/reflection-docblock": "^5.4",
"moe-mizrak/laravel-openrouter": "^1.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
Expand Down
4 changes: 4 additions & 0 deletions config/laravel-prompt-alchemist.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@
'path' => 'parameters[].default',
'type' => 'mixed',
]),
'parameter_value' => new MappingData([
'path' => 'parameters[].value',
'type' => 'mixed',
]),
'class_name' => new MappingData([
'path' => 'class_name',
'type' => 'string',
Expand Down
15 changes: 11 additions & 4 deletions resources/functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,39 @@
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
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleFinance
-
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
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleFinance
-
function_name: getTopCategories
parameters: [{ name: transactions, type: array, required: true, description: 'An array of categorized transactions.', example: [{ amount: 100, date: '2023-01-01', description: 'Groceries', category: 'Food' }, { amount: 50, date: '2023-01-02', description: 'Entertainment', category: 'Leisure' }] }]
visibility: public
description: 'Finds the top spending categories from a list of categorized transactions. '
return: { type: object, description: 'A DTO object containing details of the top spending category.', example: { name: 'Food', totalAmount: 300.0 } }
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\Example
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleFinance
-
function_name: getCreditScore
parameters: [{ name: userId, type: int, required: true, description: 'The unique identifier of the user.', example: 67890 }]
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.', example: { creditScore: 750, creditReportSummary: 'positive' } }
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\Example
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleFinance
-
function_name: getAccountBalance
parameters: [{ name: accountId, type: int, required: true, description: 'The unique identifier of the account.', example: 98765 }, { name: asOfDate, type: string, required: false, description: 'The date for which the balance is requested (optional).', example: '2023-06-01' }]
visibility: public
description: 'Retrieves the current balance for a specific user account. '
return: { type: object, description: 'An object containing the current balance and the account status.' }
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\ExampleFinance
-
function_name: privateFunction
parameters: [{ name: stringParam, type: string, required: true, description: 'String parameter of private function' }, { name: intParam, type: int, required: true, description: 'Integer parameter of private function' }]
visibility: private
description: 'This private function is intended for testing purposes. It accepts a string and int parameters and returns a string.'
return: { type: string, description: 'Private return value' }
class_name: MoeMizrak\LaravelPromptAlchemist\Tests\Example
5 changes: 5 additions & 0 deletions src/DTO/FunctionSignatureMappingData.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ class FunctionSignatureMappingData extends DataTransferObject
*/
public ?MappingData $parameter_example;

/**
* @var MappingData|null Value of the parameter if provided.
*/
public ?MappingData $parameter_value;

/**
* @var MappingData|null The class name that function belongs to.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/DTO/ParameterData.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,11 @@ class ParameterData extends DataTransferObject
* @var mixed|null
*/
public mixed $default;

/**
* Value of the parameter if exists.
*
* @var mixed
*/
public mixed $value;
}
2 changes: 2 additions & 0 deletions src/Facades/LaravelPromptAlchemist.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Facades\Facade;
use MoeMizrak\LaravelPromptAlchemist\DTO\ErrorData;
use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionData;

/**
* Facade for LaravelPromptAlchemist.
Expand All @@ -13,6 +14,7 @@
* @method static bool|ErrorData validateFunctionSignature(array $llmReturnedFunction) Validates a function signature returned by the LLM.
* @method static bool|ErrorData generateFunctionList(string|object $class, array $functions, string $fileName) Generates a detailed function list from a given class and writes it to a file in YAML format.
* @method static mixed generateInstructions() Generates instructions that can be used in config prompt_function_instructions.
* @method static mixed callFunction(FunctionData $function) Calls function and returns the function result.
*/
class LaravelPromptAlchemist extends Facade
{
Expand Down
55 changes: 55 additions & 0 deletions src/Helpers/FunctionCaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace MoeMizrak\LaravelPromptAlchemist\Helpers;

use MoeMizrak\LaravelPromptAlchemist\DTO\ErrorData;
use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionData;
use ReflectionClass;
use ReflectionException;
use Spatie\DataTransferObject\Exceptions\UnknownProperties;

/**
* This is a helper class for calling function with given function signature and parameters.
*
* Class FunctionCaller
* @package MoeMizrak\LaravelPromptAlchemist\Helpers
*/
class FunctionCaller
{
/**
* Call function with given function signature and parameters.
*
* @param FunctionData $function
*
* @return mixed
* @throws ReflectionException
* @throws UnknownProperties
*/
public function call(FunctionData $function): mixed
{
$params = [];
// Initialize variables.
$functionName = $function->function_name;
$className = $function->class_name;
$class = new ReflectionClass($className);
// Retrieve the method from function_name.
$method = $class->getMethod($functionName);

foreach ($function->parameters as $parameter) {
if ($parameter->required && ! isset($parameter->value)) {
return new ErrorData([
'code' => 400,
'message' => 'Required value of parameter '. $parameter->name .' is missing in function '. $functionName . ' to be able to call the function',
]);
}

$params[$parameter->name] = $parameter->value;
}

// Create an instance of the class
$instance = $class->newInstance();

// Call the function and get the result.
return $method->invoke($instance, ...$params);
}
}
67 changes: 51 additions & 16 deletions src/Helpers/FunctionSignatureValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Illuminate\Support\Arr;
use MoeMizrak\LaravelPromptAlchemist\DTO\ErrorData;
use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionData;
use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionSignatureMappingData;
use MoeMizrak\LaravelPromptAlchemist\DTO\ParameterData;
use Spatie\DataTransferObject\Exceptions\UnknownProperties;
use Symfony\Component\Yaml\Yaml;
Expand All @@ -21,36 +20,31 @@ class FunctionSignatureValidator
/**
* Validate function signature.
*
* @param array $function
* @param FunctionData $llmReturnedFunctionData
*
* @return bool|ErrorData
* @throws UnknownProperties
*/
public function signatureValidator(array $function): bool|ErrorData
public function signatureValidator(FunctionData $llmReturnedFunctionData): bool|ErrorData
{
// Function signature mapping data (FunctionSignatureMappingData).
$signatureMappingData = config('laravel-prompt-alchemist.function_signature_mapping');

// Formed LLM returned function data (FunctionData).
$llmReturnedFunctionData = $this->formLlmReturnedFunctionData($signatureMappingData, $function);

// Formed callable function data (FunctionData).
$callableFunctionData = $this->formCallableFunctionData($signatureMappingData, $llmReturnedFunctionData);
$callableFunctionData = $this->formCallableFunctionData($llmReturnedFunctionData);

return $this->validate($callableFunctionData, $llmReturnedFunctionData);
}

/**
* Forms data from the function returned by the LLM based on the signature mapping.
*
* @param FunctionSignatureMappingData $signatureMappingData - This is for the signature mapping (path and type info of fields in array)
* @param array $llmReturnedFunction - This is the function that will be formed in function data.
*
* @return FunctionData
* @throws UnknownProperties
*/
private function formLlmReturnedFunctionData(FunctionSignatureMappingData $signatureMappingData, array $llmReturnedFunction): FunctionData
public function formLlmReturnedFunctionData(array $llmReturnedFunction): FunctionData
{
// Get the signature mapping (path and type info of fields in array)
$signatureMappingData = config('laravel-prompt-alchemist.function_signature_mapping');
// LLM returned function name.
$llmReturnedFunctionName = Arr::get($llmReturnedFunction, $signatureMappingData->function_name->path);
// LLM returned parameters.
Expand All @@ -63,9 +57,11 @@ private function formLlmReturnedFunctionData(FunctionSignatureMappingData $signa
$parameters = [];
foreach ($llmReturnedParameters as $_) {
$parameters[] = new ParameterData([
'name' => Arr::get($llmReturnedFunction, $this->modifyMappingPath($signatureMappingData->parameter_name->path, $key)),
'type' => Arr::get($llmReturnedFunction, $this->modifyMappingPath($signatureMappingData->parameter_type->path, $key)),
'name' => Arr::get($llmReturnedFunction, $this->modifyMappingPath($signatureMappingData->parameter_name->path, $key)),
'type' => Arr::get($llmReturnedFunction, $this->modifyMappingPath($signatureMappingData->parameter_type->path, $key)),
'value' => Arr::get($llmReturnedFunction, $this->modifyMappingPath($signatureMappingData->parameter_value->path, $key)),
]);

$key++;
}

Expand All @@ -80,14 +76,15 @@ private function formLlmReturnedFunctionData(FunctionSignatureMappingData $signa
/**
* Forms callable function data based on the signature mapping.
*
* @param FunctionSignatureMappingData $signatureMappingData - This is for the signature mapping (path and type info of fields in array)
* @param FunctionData $llmReturnedFunctionData - This is for retrieving the correct function from function list by comparing with $llmReturnedFunctionName and $llmReturnedClassName.
*
* @return FunctionData
* @throws UnknownProperties
*/
private function formCallableFunctionData(FunctionSignatureMappingData $signatureMappingData, FunctionData $llmReturnedFunctionData): FunctionData
private function formCallableFunctionData(FunctionData $llmReturnedFunctionData): FunctionData
{
// Get the signature mapping (path and type info of fields in array)
$signatureMappingData = config('laravel-prompt-alchemist.function_signature_mapping');
// Function list with signatures (names, parameters etc.). This is the function that will be formed in function data.
$callableFunctions = Yaml::parseFile(config('laravel-prompt-alchemist.functions_yml_path'));
$callableFunction = null;
Expand Down Expand Up @@ -189,9 +186,47 @@ private function validate(FunctionData $callableFunctionData, FunctionData $llmR
}
}

// Loop through the LLM returned function parameters and check if the value is in correct type. If value is not set, skip this check.
foreach ($llmReturnedFunctionData->parameters as $llmReturnedParameter) {
// Check if value is set, skip the check if not set.
if (isset($llmReturnedParameter->value)) {
// Check if parameter type and type of the value matches.
if (! $this->isParameterValueInCorrectType($llmReturnedParameter)) {
return new ErrorData([
'code' => 400,
'message' => 'Function signature is wrong, parameter type ' . $llmReturnedParameter->type . ' does NOT match with the value ' . $llmReturnedParameter->value,
]);
}
}
}

return true;
}

/**
* Checks if parameter type and type of the value matches.
*
* @param ParameterData $llmReturnedParameter
*
* @return bool
*/
private function isParameterValueInCorrectType(ParameterData $llmReturnedParameter): bool
{
$value = $llmReturnedParameter->value;
$type = $llmReturnedParameter->type;

return match ($type) {
'int' => is_int($value),
'string' => is_string($value),
'float' => is_float($value),
'bool' => is_bool($value),
'array' => is_array($value),
'object' => is_object($value),
'null' => is_null($value),
default => false,
};
}

/**
* Checks if a callable parameter is name and type matches LLM returned parameters.
*
Expand Down
3 changes: 3 additions & 0 deletions src/PromptAlchemistAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace MoeMizrak\LaravelPromptAlchemist;

use MoeMizrak\LaravelPromptAlchemist\Helpers\FunctionCaller;
use MoeMizrak\LaravelPromptAlchemist\Helpers\FunctionListGenerator;
use MoeMizrak\LaravelPromptAlchemist\Helpers\FunctionSignatureValidator;
use MoeMizrak\LaravelPromptAlchemist\Helpers\InstructionsGenerator;
Expand All @@ -22,11 +23,13 @@ abstract class PromptAlchemistAPI
* @param FunctionSignatureValidator $functionSignatureValidator
* @param FunctionListGenerator $functionListGenerator
* @param InstructionsGenerator $instructionsGenerator
* @param FunctionCaller $functionCaller
*/
public function __construct(
protected PayloadBuilder $payloadBuilder,
protected FunctionSignatureValidator $functionSignatureValidator,
protected FunctionListGenerator $functionListGenerator,
protected InstructionsGenerator $instructionsGenerator,
protected FunctionCaller $functionCaller,
) {}
}
37 changes: 33 additions & 4 deletions src/PromptAlchemistRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use MoeMizrak\LaravelOpenrouter\Exceptions\XorValidationException;
use MoeMizrak\LaravelPromptAlchemist\DTO\ErrorData;
use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionData;
use ReflectionException;
use Spatie\DataTransferObject\Exceptions\UnknownProperties;

/**
Expand Down Expand Up @@ -48,14 +50,27 @@ public function prepareFunctionResultsPayload(string $prompt, array $functionRes
/**
* Validates a function signature returned by the LLM.
*
* @param array $llmReturnedFunction
* @param FunctionData $llmReturnedFunctionData
*
* @return bool|ErrorData
* @throws UnknownProperties
*/
public function validateFunctionSignature(array $llmReturnedFunction): bool|ErrorData
public function validateFunctionSignature(FunctionData $llmReturnedFunctionData): bool|ErrorData
{
return $this->functionSignatureValidator->signatureValidator($llmReturnedFunctionData);
}

/**
* Forms data from the function returned by the LLM based on the signature mapping.
* Gives FunctionData formed result.
*
* @param array $llmReturnedFunction
* @return FunctionData
* @throws UnknownProperties
*/
public function formLlmReturnedFunctionData(array $llmReturnedFunction): FunctionData
{
return $this->functionSignatureValidator->signatureValidator($llmReturnedFunction);
return $this->functionSignatureValidator->formLlmReturnedFunctionData($llmReturnedFunction);
}

/**
Expand All @@ -71,7 +86,7 @@ public function validateFunctionSignature(array $llmReturnedFunction): bool|Erro
*
* @return bool|ErrorData
* @throws UnknownProperties
* @throws \ReflectionException
* @throws ReflectionException
*/
public function generateFunctionList(string|object $class, array $functions, string $fileName): bool|ErrorData
{
Expand All @@ -89,4 +104,18 @@ public function generateInstructions(): mixed
{
return $this->instructionsGenerator->generate();
}

/**
* Call the function where signature and parameter values are provided.
*
* @param FunctionData $function
*
* @return mixed
* @throws UnknownProperties
* @throws ReflectionException
*/
public function callFunction(FunctionData $function): mixed
{
return $this->functionCaller->call($function);
}
}
Loading

0 comments on commit d8df772

Please sign in to comment.