From 868f53c4f0eacd9b104457f4aea2d8fa578da372 Mon Sep 17 00:00:00 2001 From: moe-mizrak Date: Tue, 25 Jun 2024 01:00:32 +0300 Subject: [PATCH 1/2] main.yml is added to github actions --- .github/workflows/main.yml | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..fdcd80d --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,56 @@ +name: Main + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ ubuntu-latest ] + php: [ 8.1 ] + laravel: [ 10.* ] + dependency-version: [ prefer-lowest, prefer-stable ] + exclude: + - php: 8.1 + laravel: 11.* + + name: Tests PHP${{ matrix.php }} - Laravel v${{ matrix.laravel }} + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Setup PHP Environment + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, mbstring, zip + coverage: none + + - name: Cache Composer Dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-php-${{ matrix.php }}-L${{ matrix.laravel }}-${{ matrix.dependency-version }}-composer-${{ hashFiles('composer.json') }} + + - name: Copy Environment Files + run: | + php -r "file_exists('.env.testing') || copy('.env.ci', '.env.testing');" + php -r "file_exists('.env') || copy('.env.ci', '.env');" + + - name: Install Composer Dependencies + run: composer install --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Run PHPUnit Tests + run: vendor/bin/phpunit --log-junit phpunit-report.xml + + - name: Upload PHPUnit Report + uses: actions/upload-artifact@v2 + with: + name: PHPUnit Test Report + path: phpunit-report.xml \ No newline at end of file From 9a58f2d4487b3c63664717496737fe888c1825ad Mon Sep 17 00:00:00 2001 From: moe-mizrak Date: Wed, 26 Jun 2024 20:01:00 +0300 Subject: [PATCH 2/2] callFunction is added to package --- composer.json | 5 +- config/laravel-prompt-alchemist.php | 4 + resources/functions.yml | 15 +- src/DTO/FunctionSignatureMappingData.php | 5 + src/DTO/ParameterData.php | 7 + src/Facades/LaravelPromptAlchemist.php | 2 + src/Helpers/FunctionCaller.php | 55 ++++++ src/Helpers/FunctionSignatureValidator.php | 67 +++++-- src/PromptAlchemistAPI.php | 3 + src/PromptAlchemistRequest.php | 37 +++- src/PromptAlchemistServiceProvider.php | 4 +- tests/PromptAlchemistTest.php | 220 ++++++++++++++++++++- 12 files changed, 387 insertions(+), 37 deletions(-) create mode 100644 src/Helpers/FunctionCaller.php diff --git a/composer.json b/composer.json index a8ccc7a..8b340b3 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/config/laravel-prompt-alchemist.php b/config/laravel-prompt-alchemist.php index 7d0a61c..17e9339 100644 --- a/config/laravel-prompt-alchemist.php +++ b/config/laravel-prompt-alchemist.php @@ -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', diff --git a/resources/functions.yml b/resources/functions.yml index a98f9df..bf28f91 100644 --- a/resources/functions.yml +++ b/resources/functions.yml @@ -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 \ No newline at end of file diff --git a/src/DTO/FunctionSignatureMappingData.php b/src/DTO/FunctionSignatureMappingData.php index 7fcde24..a79854f 100644 --- a/src/DTO/FunctionSignatureMappingData.php +++ b/src/DTO/FunctionSignatureMappingData.php @@ -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. */ diff --git a/src/DTO/ParameterData.php b/src/DTO/ParameterData.php index 3363f4c..78aee5e 100644 --- a/src/DTO/ParameterData.php +++ b/src/DTO/ParameterData.php @@ -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; } \ No newline at end of file diff --git a/src/Facades/LaravelPromptAlchemist.php b/src/Facades/LaravelPromptAlchemist.php index 898a63d..7737af5 100644 --- a/src/Facades/LaravelPromptAlchemist.php +++ b/src/Facades/LaravelPromptAlchemist.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Facade; use MoeMizrak\LaravelPromptAlchemist\DTO\ErrorData; +use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionData; /** * Facade for LaravelPromptAlchemist. @@ -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 { diff --git a/src/Helpers/FunctionCaller.php b/src/Helpers/FunctionCaller.php new file mode 100644 index 0000000..8b8705f --- /dev/null +++ b/src/Helpers/FunctionCaller.php @@ -0,0 +1,55 @@ +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); + } +} \ No newline at end of file diff --git a/src/Helpers/FunctionSignatureValidator.php b/src/Helpers/FunctionSignatureValidator.php index 1f34ca7..21d196e 100644 --- a/src/Helpers/FunctionSignatureValidator.php +++ b/src/Helpers/FunctionSignatureValidator.php @@ -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; @@ -21,21 +20,15 @@ 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); } @@ -43,14 +36,15 @@ public function signatureValidator(array $function): bool|ErrorData /** * 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. @@ -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++; } @@ -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; @@ -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. * diff --git a/src/PromptAlchemistAPI.php b/src/PromptAlchemistAPI.php index 10bc36f..7f73f2d 100644 --- a/src/PromptAlchemistAPI.php +++ b/src/PromptAlchemistAPI.php @@ -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; @@ -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, ) {} } \ No newline at end of file diff --git a/src/PromptAlchemistRequest.php b/src/PromptAlchemistRequest.php index a5d9d10..7d82819 100644 --- a/src/PromptAlchemistRequest.php +++ b/src/PromptAlchemistRequest.php @@ -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; /** @@ -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); } /** @@ -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 { @@ -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); + } } \ No newline at end of file diff --git a/src/PromptAlchemistServiceProvider.php b/src/PromptAlchemistServiceProvider.php index c58f1af..b0dc19c 100644 --- a/src/PromptAlchemistServiceProvider.php +++ b/src/PromptAlchemistServiceProvider.php @@ -6,6 +6,7 @@ use Illuminate\Support\ServiceProvider; use MoeMizrak\LaravelOpenrouter\OpenRouterServiceProvider; use MoeMizrak\LaravelPromptAlchemist\Facades\LaravelPromptAlchemist; +use MoeMizrak\LaravelPromptAlchemist\Helpers\FunctionCaller; use MoeMizrak\LaravelPromptAlchemist\Helpers\FunctionListGenerator; use MoeMizrak\LaravelPromptAlchemist\Helpers\FunctionSignatureValidator; use MoeMizrak\LaravelPromptAlchemist\Helpers\InstructionsGenerator; @@ -40,7 +41,8 @@ public function register(): void new PayloadBuilder(), new FunctionSignatureValidator(), new FunctionListGenerator(), - new InstructionsGenerator() + new InstructionsGenerator(), + new FunctionCaller() ); }); diff --git a/tests/PromptAlchemistTest.php b/tests/PromptAlchemistTest.php index c356479..15c5fb1 100644 --- a/tests/PromptAlchemistTest.php +++ b/tests/PromptAlchemistTest.php @@ -22,6 +22,7 @@ class PromptAlchemistTest extends TestCase { private PromptAlchemistRequest $request; private string|object $class; + private string|object $exampleFinanceClass; private string $testYmlFileName; /** @@ -42,6 +43,7 @@ public function setUp(): void 'role' => RoleType::USER, ]); $this->class = Example::class; + $this->exampleFinanceClass = ExampleFinance::class; $this->testYmlFileName = __DIR__ . '/../resources/test_functions.yml'; $this->request = $this->app->make(PromptAlchemistRequest::class); @@ -284,11 +286,13 @@ public function it_validates_function_signature() [ "name" => "startDate", "type" => "string"], [ "name" => "endDate", "type" => "string"], ], - 'class_name' => (string) $this->class + 'class_name' => (string) $this->exampleFinanceClass ]; + // Formed LLM returned function data (FunctionData). + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); /* EXECUTE */ - $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunction); + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); /* ASSERT */ $this->assertTrue($validationResponse); @@ -310,15 +314,44 @@ public function it_responds_with_error_data_when_class_name_is_invalid() ], 'class_name' => $className ]; + // Formed LLM returned function data (FunctionData). + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); /* EXECUTE */ - $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunction); + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); /* ASSERT */ $this->assertEquals(400, $validationResponse->code); $this->assertEquals("Function signature is wrong, unexpected function name getFinancialData or class name {$className}", $validationResponse->message); } + /** + * @test + */ + public function it_responds_with_error_data_when_parameter_type_and_type_of_the_value_mismatches() + { + /* SETUP */ + $intType = "int"; + $invalidTypeValue = 'invalid type value'; + $llmReturnedFunction = [ + "function_name" => "getFinancialData", + "parameters" => [ + [ "name" => "userId", "type" => $intType, 'value' => $invalidTypeValue], + [ "name" => "startDate", "type" => "string"], + [ "name" => "endDate", "type" => "string"], + ], + 'class_name' => (string) $this->exampleFinanceClass + ]; + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); + + /* EXECUTE */ + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); + + /* ASSERT */ + $this->assertEquals(400, $validationResponse->code); + $this->assertEquals('Function signature is wrong, parameter type ' . $intType . ' does NOT match with the value ' . $invalidTypeValue, $validationResponse->message); + } + /** * @test */ @@ -331,11 +364,13 @@ public function it_validates_function_signature_and_returns_error_required_field [ "name" => "startDate", "type" => "string"], [ "name" => "endDate", "type" => "string"], ], - 'class_name' => (string) $this->class + 'class_name' => (string) $this->exampleFinanceClass ]; + // Formed LLM returned function data (FunctionData). + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); /* EXECUTE */ - $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunction); + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); /* ASSERT */ $this->assertEquals(400, $validationResponse->code); @@ -356,11 +391,13 @@ public function it_validates_function_signature_and_returns_error_when_unexpecte [ "name" => "startDate", "type" => "string"], [ "name" => "endDate", "type" => "string"], ], - "class_name" => (string) $this->class + "class_name" => (string) $this->exampleFinanceClass ]; + // Formed LLM returned function data (FunctionData). + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); /* EXECUTE */ - $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunction); + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); /* ASSERT */ $this->assertEquals(400, $validationResponse->code); @@ -379,9 +416,11 @@ public function it_validates_function_signature_and_returns_error_when_function_ "parameters" => [], "class_name" => (string) $this->class ]; + // Formed LLM returned function data (FunctionData). + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); /* EXECUTE */ - $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunction); + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); /* ASSERT */ $this->assertEquals(400, $validationResponse->code); @@ -595,7 +634,7 @@ public function it_successfully_generates_function_list_when_no_info_is_provided /** * @test */ - public function it_successfully_responds_with_error_data_when_invalid_function_name_is_sent_to_generate_function_list() + public function it_responds_with_error_data_when_invalid_function_name_is_sent_to_generate_function_list() { /* SETUP */ // this function is for the overwriting descriptions @@ -641,4 +680,167 @@ public function it_successfully_uses_functions_with_facade() $this->assertArrayHasKey('function_payload_schema', $response); $this->assertArrayHasKey('function_payload_schema', $response); } + + /** + * @test + */ + public function it_responds_with_error_data_when_parameter_value_is_missing() + { + /* SETUP */ + $params = []; + $LLMReturnedFunction = [ + "function_name" => "getFinancialData", + "parameters" => [ + [ + "name" => "userId", + "type" => "int", + "required" => true + ], + [ + "name" => "startDate", + "type" => "string", + "required" => true + ], + [ + "name" => "endDate", + "type" => "string", + "required" => true + ] + ], + "class_name" => "MoeMizrak\LaravelPromptAlchemist\Tests\ExampleFinance" + ]; + // here set parameterData with value + foreach ($LLMReturnedFunction['parameters'] as $parameter) { + if ($parameter['type'] == 'int') { + $params[] = new ParameterData([ + 'name' => $parameter['name'], + 'type' => $parameter['type'], + 'required' => $parameter['required'], + ]); + } + + if ($parameter['type'] == 'string') { + $params[] = new ParameterData([ + 'name' => $parameter['name'], + 'type' => $parameter['type'], + 'required' => $parameter['required'], + 'value' => 'some value', + ]); + } + } + $function = new FunctionData([ + 'function_name' => $LLMReturnedFunction['function_name'], + 'parameters' => $params, + 'class_name' => $LLMReturnedFunction['class_name'] + ]); + + /* EXECUTE */ + $functionResult = LaravelPromptAlchemist::callFunction($function); + + /* ASSERT */ + $this->assertInstanceOf(ErrorData::class, $functionResult); + $this->assertEquals(400, $functionResult->code); + $this->assertEquals("Required value of parameter userId is missing in function " . $LLMReturnedFunction['function_name'] . " to be able to call the function", $functionResult->message); + } + + /** + * @test + */ + public function it_successfully_calls_function_returned_from_llm() + { + /* SETUP */ + $llmReturnedFunction = [ + "function_name" => "getFinancialData", + "parameters" => [ + [ + "name" => "userId", + "type" => "int", + "required" => true + ], + [ + "name" => "startDate", + "type" => "string", + "required" => true + ], + [ + "name" => "endDate", + "type" => "string", + "required" => true + ] + ], + "class_name" => "MoeMizrak\LaravelPromptAlchemist\Tests\ExampleFinance" + ]; + // Formed LLM returned function data (FunctionData). + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); + // $llmReturnedFunctionData should be validated before function calling + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); + if (true === $validationResponse) { + // here set values of the parameter to call them + foreach ($llmReturnedFunctionData->parameters as $key => $parameter) { + if ($parameter->name == 'userId') { + $llmReturnedFunctionData->parameters[$key]->value = 1; + } + if ($parameter->name == 'startDate') { + $llmReturnedFunctionData->parameters[$key]->value = '2023-06-01'; + } + if ($parameter->name == 'endDate') { + $llmReturnedFunctionData->parameters[$key]->value = '2023-07-01'; + } + } + } + + /* EXECUTE */ + $functionResult = LaravelPromptAlchemist::callFunction($llmReturnedFunctionData); + + /* ASSERT */ + $this->assertNotNull($functionResult); + } + + /** + * @test + */ + public function it_successfully_calls_private_function_returned_from_llm() + { + /* SETUP */ + $llmReturnedFunction = [ + "function_name" => "privateFunction", + "parameters" => [ + [ + "name" => "stringParam", + "type" => "string", + "required" => true + ], + [ + "name" => "intParam", + "type" => "int", + "required" => true + ] + ], + "class_name" => "MoeMizrak\LaravelPromptAlchemist\Tests\Example" + ]; + $intValue = 100; + $stringValue = 'string value'; + // Formed LLM returned function data (FunctionData). + $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); + // $llmReturnedFunctionData should be validated before function calling + $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); + if (true === $validationResponse) { + // here set values of the parameter to call them + foreach ($llmReturnedFunctionData->parameters as $key => $parameter) { + if ($parameter->name == 'stringParam') { + $llmReturnedFunctionData->parameters[$key]->value = $stringValue; + } + if ($parameter->name == 'intParam') { + $llmReturnedFunctionData->parameters[$key]->value = $intValue; + } + } + } + + /* EXECUTE */ + $functionResult = LaravelPromptAlchemist::callFunction($llmReturnedFunctionData); + + /* ASSERT */ + $this->assertNotNull($functionResult); + $this->assertEquals('private return value ' . $stringValue . ' ' . $intValue, $functionResult); + } } \ No newline at end of file