From acc33570543d2b838160109453ef4ca9f6509eed Mon Sep 17 00:00:00 2001 From: moe-mizrak Date: Wed, 26 Jun 2024 22:57:14 +0300 Subject: [PATCH] FunctionResultData is added, callFunction is modified --- README.md | 104 +++++++++++++++--- composer.json | 2 +- src/DTO/FunctionData.php | 17 +++ src/DTO/FunctionResultData.php | 28 +++++ src/Helpers/FunctionCaller.php | 13 ++- src/PromptAlchemistRequest.php | 5 +- .../Templates/ResponsePayloadTemplate.php | 2 + tests/PromptAlchemistTest.php | 81 ++++++++------ 8 files changed, 192 insertions(+), 60 deletions(-) create mode 100644 src/DTO/FunctionResultData.php diff --git a/README.md b/README.md index 5b24c14..33c883e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Laravel Prompt Alchemist -Versatile **LLM Tool Use (Function Calling)** package for Laravel, compatible with **all LLMs**. +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](https://img.shields.io/badge/packagist-v1.0-blue)](https://packagist.org/packages/moe-mizrak/laravel-prompt-alchemist) @@ -9,7 +9,8 @@ Versatile **LLM Tool Use (Function Calling)** package for Laravel, compatible wi --- > **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 which function to call based on the prompt, compatible with **all LLMs** regardless of built-in capabilities. +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 @@ -252,25 +253,76 @@ $llmReturnedFunction = [ // Sample LLM returned function ], 'class_name' => 'MoeMizrak\LaravelPromptAlchemist\Tests\Example' ]; + +// Formed LLM returned function data (FunctionData). +$llmReturnedFunctionData = LaravelPromptAlchemist::formLlmReturnedFunctionData($llmReturnedFunction); ``` -Call `validateFunctionSignature` for function signature **validation**: +Call `validateFunctionSignature` for function signature **validation** of `$llmReturnedFunctionData`: ```php -LaravelOpenRouter::validateFunctionSignature($llmReturnedFunction); +$isValid = LaravelOpenRouter::validateFunctionSignature($llmReturnedFunctionData); ``` -- And finally in your codebase, 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**). +- 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**). +
+ +(**Note:** You need to set **parameter values** to be able to call the function. **Required parameters** have to be set, otherwise **ErrorData** is returned.)
-> **Note:** Automatic function calls will be available in an **upcoming release!** Stay tuned for **updates!** +Set parameter values before calling function: +```php +// 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: +```php +$functionResultData = LaravelPromptAlchemist::callFunction($llmReturnedFunctionData); +``` + +Sample function result (**$functionResultData**) DTO object (FunctionResultData): +```php +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 return 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](#prepare-function-results-payload) for more details) ```php $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, @@ -281,19 +333,35 @@ $functionResults = [ '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, +]); -$response = LaravelPromptAlchemist::prepareFunctionResultsPayload($prompt, $functionResults); +$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`. @@ -856,7 +924,7 @@ Prompt which will be used for Tool Use (Function Calling): $prompt = 'Can tell me Mr. Boolean Bob credit score?'; $functionResults = [ - [ + new FunctionResultData([ 'function_name' => 'getFinancialData', 'result' => [ 'totalAmount' => 122, @@ -867,15 +935,15 @@ $functionResults = [ 'description' => 'food', ], ] - ], - ], - [ + ] + ]), + new FunctionResultData([ 'function_name' => 'getCreditScore', 'result' => [ 'creditScore' => 0.8, 'summary' => 'reliable', ] - ], + ]), ... ]; ``` diff --git a/composer.json b/composer.json index 8b340b3..99a25db 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "moe-mizrak/laravel-prompt-alchemist", - "description": "Versatile LLM Tool Use (Function Calling) package for Laravel, compatible with all LLMs.", + "description": "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).", "keywords": [ "Moe Mizrak", "laravel", diff --git a/src/DTO/FunctionData.php b/src/DTO/FunctionData.php index 16a6bc7..7c10c4d 100644 --- a/src/DTO/FunctionData.php +++ b/src/DTO/FunctionData.php @@ -56,4 +56,21 @@ class FunctionData extends DataTransferObject * @var string|null */ public ?string $class_name; + + /** + * Set parameter values. + * + * @param array $parameters + * + * @return void + */ + public function setParameterValues(array $parameters): void + { + foreach ($this->parameters as $llmReturnedParameter) { + $matchingParameter = array_filter($parameters, fn($param) => $param->name === $llmReturnedParameter->name); + if ($matchingParameter) { + $llmReturnedParameter->value = current($matchingParameter)->value; + } + } + } } \ No newline at end of file diff --git a/src/DTO/FunctionResultData.php b/src/DTO/FunctionResultData.php new file mode 100644 index 0000000..c4906c4 --- /dev/null +++ b/src/DTO/FunctionResultData.php @@ -0,0 +1,28 @@ +newInstance(); // Call the function and get the result. - return $method->invoke($instance, ...$params); + $result = $method->invoke($instance, ...$params); + + // Map result to DTO object + return new FunctionResultData([ + 'function_name' => $functionName, + 'result' => $result, + ]); } } \ No newline at end of file diff --git a/src/PromptAlchemistRequest.php b/src/PromptAlchemistRequest.php index 7d82819..9415431 100644 --- a/src/PromptAlchemistRequest.php +++ b/src/PromptAlchemistRequest.php @@ -5,6 +5,7 @@ use MoeMizrak\LaravelOpenrouter\Exceptions\XorValidationException; use MoeMizrak\LaravelPromptAlchemist\DTO\ErrorData; use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionData; +use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionResultData; use ReflectionException; use Spatie\DataTransferObject\Exceptions\UnknownProperties; @@ -110,11 +111,11 @@ public function generateInstructions(): mixed * * @param FunctionData $function * - * @return mixed + * @return FunctionResultData|ErrorData * @throws UnknownProperties * @throws ReflectionException */ - public function callFunction(FunctionData $function): mixed + public function callFunction(FunctionData $function): FunctionResultData|ErrorData { return $this->functionCaller->call($function); } diff --git a/src/Resources/Templates/ResponsePayloadTemplate.php b/src/Resources/Templates/ResponsePayloadTemplate.php index 877c7c4..4b8726a 100644 --- a/src/Resources/Templates/ResponsePayloadTemplate.php +++ b/src/Resources/Templates/ResponsePayloadTemplate.php @@ -24,6 +24,8 @@ final class ResponsePayloadTemplate */ public static function createPayload(string $prompt, string $instructions, ?array $functionResults, ?array $resultsSchema): array { + $functionResults = array_map(fn($functionResultData) => $functionResultData->toArray(), $functionResults); + return [ 'prompt' => $prompt, 'instructions' => $instructions, diff --git a/tests/PromptAlchemistTest.php b/tests/PromptAlchemistTest.php index 15c5fb1..d9bcb4f 100644 --- a/tests/PromptAlchemistTest.php +++ b/tests/PromptAlchemistTest.php @@ -10,6 +10,7 @@ use MoeMizrak\LaravelOpenrouter\Types\RoleType; use MoeMizrak\LaravelPromptAlchemist\DTO\ErrorData; use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionData; +use MoeMizrak\LaravelPromptAlchemist\DTO\FunctionResultData; use MoeMizrak\LaravelPromptAlchemist\DTO\ParameterData; use MoeMizrak\LaravelPromptAlchemist\DTO\ReturnData; use MoeMizrak\LaravelPromptAlchemist\Facades\LaravelPromptAlchemist; @@ -170,7 +171,7 @@ public function it_prepares_function_results_payload() { /* SETUP */ $functionResults = [ - [ + new FunctionResultData([ 'function_name' => 'getFinancialData', 'result' => [ 'totalAmount' => 122, @@ -181,22 +182,22 @@ public function it_prepares_function_results_payload() 'description' => 'food', ], ] - ], - ], - [ + ] + ]), + new FunctionResultData([ 'function_name' => 'getCreditScore', 'result' => [ 'creditScore' => 0.8, 'summary' => 'reliable', ] - ], - [ + ]), + new FunctionResultData([ 'function_name' => 'getAccountBalance', 'result' => [ 'currentBalance' => 12502, 'status' => 'active', ] - ], + ]), ]; /* EXECUTE */ @@ -217,7 +218,7 @@ public function it_sends_function_results_to_open_route_and_retrieves_formed_ans { /* SETUP */ $functionResults = [ - [ + new FunctionResultData([ 'function_name' => 'getFinancialData', 'result' => [ 'totalAmount' => 122, @@ -228,22 +229,22 @@ public function it_sends_function_results_to_open_route_and_retrieves_formed_ans 'description' => 'food', ], ] - ], - ], - [ + ] + ]), + new FunctionResultData([ 'function_name' => 'getCreditScore', 'result' => [ 'creditScore' => 0.8, 'summary' => 'reliable', ] - ], - [ + ]), + new FunctionResultData([ 'function_name' => 'getAccountBalance', 'result' => [ 'currentBalance' => 12502, 'status' => 'active', ] - ], + ]), ]; $prompt = 'Can tell me Mr. Boolean Bob credit score?'; $content = $this->request->prepareFunctionResultsPayload($prompt, $functionResults); @@ -774,19 +775,23 @@ public function it_successfully_calls_function_returned_from_llm() $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); // $llmReturnedFunctionData should be validated before function calling $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); + // Set parameter values + $parameters = [ + new ParameterData([ + 'name' => 'userId', + 'value' => 99, + ]), + new ParameterData([ + 'name' => 'startDate', + 'value' => '2023-06-01', + ]), + new ParameterData([ + 'name' => 'endDate', + 'value' => '2023-07-01', + ]), + ]; 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'; - } - } + $llmReturnedFunctionData->setParameterValues($parameters); } /* EXECUTE */ @@ -794,6 +799,7 @@ public function it_successfully_calls_function_returned_from_llm() /* ASSERT */ $this->assertNotNull($functionResult); + $this->assertEquals('getFinancialData', $functionResult->function_name); } /** @@ -824,16 +830,19 @@ public function it_successfully_calls_private_function_returned_from_llm() $llmReturnedFunctionData = $this->request->formLlmReturnedFunctionData($llmReturnedFunction); // $llmReturnedFunctionData should be validated before function calling $validationResponse = $this->request->validateFunctionSignature($llmReturnedFunctionData); + // Set parameter values + $parameters = [ + new ParameterData([ + 'name' => 'stringParam', + 'value' => $stringValue, + ]), + new ParameterData([ + 'name' => 'intParam', + 'value' => $intValue, + ]), + ]; 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; - } - } + $llmReturnedFunctionData->setParameterValues($parameters); } /* EXECUTE */ @@ -841,6 +850,6 @@ public function it_successfully_calls_private_function_returned_from_llm() /* ASSERT */ $this->assertNotNull($functionResult); - $this->assertEquals('private return value ' . $stringValue . ' ' . $intValue, $functionResult); + $this->assertEquals('private return value ' . $stringValue . ' ' . $intValue, $functionResult->result); } } \ No newline at end of file