client
client copied to clipboard
Add Auto function execution
Hello, Is it possible to add auto function execution in this lib? We need to process the function manually now.
Hi @samratfkt
Can you give an example how the API should look like?
I don't think we can do it via api, but currently I am checking the function call and then using "call_user_func_array($functionName, $argumants)" function to processing the complete calling. So I think if you guys can add the whole function calling into it, it will be more helpful and easy to integrate.
Maybe https://github.com/theodo-group/LLPhant#function can help here
@samratfkt I meant, how you would like to use the package in an ideal world.
@yguedidi Thank you for the link.
A feature to convert PHP functions into a proper request and then execute them when requested is on our todo list.
Hello guys!
If it helps, I'll like to share what I did for the functions registration and execution.
I used Reflection to parse, register and execute the functions. This way, I can define the functions as classes and keep the code clean.
Implementation
The implementation is something like this:
(I removed some parts to make it more readable, since it's a Laravel project, and I'm using models and other stuff to manage the Assistants and Functions from an admin panel):
Example/Description.php
This is just an attribute helper class to define the tool descriptions (see below).
<?php declare(strict_types=1);
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PARAMETER)]
final class Description {
public function __construct(
public string $value,
) {}
}
Functions management
Example/HasTools.php
This trait basically has the following methods:
-
register(array $function_classes)
- Used to register the functions classes. -
call(string $function_name, array $parameters = [])
- Used to call a registered function.
Skip to next section before I overwhelm you with the implementation, then come back here 😅.
<?php declare(strict_types=1);
namespace Example;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionEnum;
use ReflectionException;
use ReflectionParameter;
use RuntimeException;
trait HasFunctions {
private array $registered_functions = [];
/**
* @throws ReflectionException
*/
public function register(array $function_classes): void {
foreach ($function_classes as $class_name) {
if ( !class_exists($class_name)) {
continue;
}
$function_class = new ReflectionClass($class_name);
$function_name = Str::snake(basename(str_replace('\\', '/', $class_name)));
if (! $function_class->hasMethod('handle')) {
Log::warning(sprintf('Function class %s has no "handle" method', $function_class));
continue;
}
$tool_definition = [
'type' => 'function',
'function' => [ 'name' => $function_name ],
];
// set function description, if it has one
if ( !empty($descriptions = $function_class->getAttributes(Description::class))) {
$tool_definition['function']['description'] = implode(
separator: "\n",
array: array_map(static fn($td) => $td->newInstance()->value, $descriptions),
);
}
if ($function_class->getMethod('handle')->getNumberOfParameters() > 0) {
$tool_definition['function']['parameters'] = $this->parseFunctionParameters($function_class);
}
$this->registered_functions[ $class_name ] = $tool_definition;
}
}
/**
* @throws ReflectionException
*/
public function call(string $function_name, array $arguments = []): mixed {
if (null === $function_class = array_key_first(array_filter($this->registered_functions, static fn($registered_function) => $registered_function['function']['name'] === $function_name))) {
return null;
}
$function_class = new ReflectionClass($function_class);
$handle_method = $function_class->getMethod('handle');
$params = [];
foreach ($handle_method->getParameters() as $parameter) {
if ( !array_key_exists($parameter->name, $arguments) && !$parameter->isOptional() && !$parameter->isDefaultValueAvailable()) {
throw new RuntimeException(sprintf('Parameter %s is required', $parameter->name));
}
// check if parameter type is an Enum and add fetch a valid value
if (($parameter_type = $parameter->getType()) !== null && !$parameter_type->isBuiltin()) {
if (enum_exists($parameter_type->getName())) {
$params[$parameter->name] = $parameter_type->getName()::tryFrom($arguments[$parameter->name]) ?? $parameter->getDefaultValue();
continue;
}
}
$params[$parameter->name] = $arguments[$parameter->name] ?? $parameter->getDefaultValue();
}
return $handle_method->invoke(new $function_class->name, ...$params);
}
/**
* @throws ReflectionException
*/
private function parseFunctionParameters(ReflectionClass $tool): array {
$parameters = [ 'type' => 'object' ];
if (count($method_parameters = $tool->getMethod('handle')->getParameters()) > 0) {
$parameters['properties'] = [];
}
foreach ($method_parameters as $method_parameter) {
$property = [ 'type' => $this->getFunctionParameterType($method_parameter) ];
// set property description, if it has one
if (!empty($descriptions = $method_parameter->getAttributes(Description::class))) {
$property['description'] = implode(
separator: "\n",
array: array_map(static fn($pd) => $pd->newInstance()->value, $descriptions),
);
}
// register parameter to the required properties list if it's not optional
if ( !$method_parameter->isOptional()) {
$parameters['required'] ??= [];
$parameters['required'][] = $method_parameter->getName();
}
// check if parameter type is an Enum and add it's valid values to the property
if (($parameter_type = $method_parameter->getType()) !== null && !$parameter_type->isBuiltin()) {
if (enum_exists($parameter_type->getName())) {
$property['type'] = 'string';
$property['enum'] = array_column((new ReflectionEnum($parameter_type->getName()))->getConstants(), 'value');
}
}
$parameters['properties'][$method_parameter->getName()] = $property;
}
return $parameters;
}
private function getFunctionParameterType(ReflectionParameter $parameter): string {
if (null === $parameter_type = $parameter->getType()) {
return 'string';
}
if ( !$parameter_type->isBuiltin()) {
return $parameter_type->getName();
}
return match ($parameter_type->getName()) {
'bool' => 'boolean',
'int' => 'integer',
'float' => 'number',
default => 'string',
};
}
}
Example functions
Example\Functions\MakeLead.php
That allows you to define your functions as a simple class, and all the above process will take care of building the definitions to register it for OpenAI's API (You may want to customize the Trait to your needs).
<?php declare(strict_types=1);
namespace Example\Functions;
use Example\Description;
#[Description('Having all the necessary information, create the lead for the Sales department')]
final class MakeLead {
public function handle(
#[Description('Customer\'s name')]
string $name,
#[Description('Customer\'s surname')]
string $surname,
#[Description('Customers\' email')]
string $email,
#[Description('Customer\'s phone number')]
string $phone,
#[Description('Chosen product by the customer for purchase')]
int $product_id,
): ?array {
// TODO: do your function logic gere
return null;
}
}
Example App
Here it's just an example (I didn't test it). You register the functions, and then call them when needed. Again, there is a lot of code that I removed for brevity (and still is a lot of it 😅).
The main idea is to keep your code organized and readable.
Example\App.php
<?php declare(strict_types=1);
namespace Example;
use Example\HasFunctions;
use Example\Functions\MakeLead;
use Example\Functions\SearchProducts;
use Example\Functions\AnotherUsefulFunction;
class App {
use HasFunctions;
public function __construct() {
$this->register([
MakeLead::class,
SearchProducts::class,
AnotherUsefulFunction::class,
// ...
]);
}
public function run() {
$response = $client->chat()->create([
'model' => 'gpt-3.5-turbo-0613',
'messages' => [
// ... message history
[ 'role' => 'user', 'content' => 'Yes, I want to purchase one coffee machine' ],
],
'tools' => array_values($this->registered_functions),
]);
$choice = $response->choices[0];
if ($choice->finishReason === 'tool_calls') {
foreach ($choice->message->toolCalls as $tool_call) {
// execute the function call
$result = $this->call($tool_call->function->name, json_decode($tool_call->function->arguments));
// TODO: do something with the result ...
}
}
}
}
Hope that this helps you!
@hschimpf Love what you did for Function management. I am looking to organize my functions for so long and this is good starter, did you also add support for arrays? or nested parameter structure? would love to see your thoughts on how you approach.
Hello @vijaythecoder, the documentation of OpenAI about function calling is constantly changing. As right now, there is no explanation about how the function's parameters must be specified.
Only browsing the examples of calling functions have something to work with.
... did you also add support for arrays? or nested parameter structure? ...
IDK if the model supports array as parameter (I didn't test it), cuz the docs regarding tool's parameters are vague for the moment. But if you need to process multiple items, for example the weather in different locations, the model will respond with multiple tool calls to make. Reference: Parallel function calling.
Also, I think that keeping the tool's definition simple will help with compatibility between the constant change of the models training. Maybe you manage to work with array parameters in one model, but then in the next model's version it may stop working. One way that I think you can try is to fine-tune a model to support array parameters.
Is the above being implemented already into this library?
Even if i am manually calling functions, how would it look like in php ? I know how to add the functions to the assistant, but how would i be able to make the ai run a php function i declared ? In .net i have done it however with a library built for it.
Hi @samuelgjekic, I showed an example in my response above
Even if i am manually calling functions, how would it look like in php ? I know how to add the functions to the assistant, but how would i be able to make the ai run a php function i declared ? In .net i have done it however with a library built for it.
See the Example App part of my response. After registering the functions and requesting a completion to the chat API, you check if the response has tool_calls
and just call them using the call()
method of the example trait.
Hope it helps.
@hschimpf Thanks for getting back, I really appreciate. May be my explanation is bad. Currently, I am trying to achieve the following. I have a tool(function) that has the following schema.
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'type' => 'string',
'description' => 'The name of the item',
],
'variation' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
'description' => 'id of the variation',
],
],
],
],
]
the part variations
is an object or a nested parameter for Open AI. With your code, I am not sure how to define it as parameter along with name
public function handle(
#[Description('The name of the item')]
string $name,
#[Description('Variation of the item')]
string $variation, // But I need this to represent as an object parameter with child items.
) {
...
let me know if you have a work around for that.
Hi @samuelgjekic, I showed an example in my response above
Even if i am manually calling functions, how would it look like in php ? I know how to add the functions to the assistant, but how would i be able to make the ai run a php function i declared ? In .net i have done it however with a library built for it.
See the Example App part of my response. After registering the functions and requesting a completion to the chat API, you check if the response has
tool_calls
and just call them using thecall()
method of the example trait.Hope it helps.
Thanks, after your reply and after looking into the app example i am starting to understand this a bit better. However i am using assistans api so i would check for tool calls in the thread or am i confused still?
`When you initiate a Run with a user Message that triggers the function, the Run will enter a pending status. After it processes, the run will enter a requires_action state which you can verify by retrieving the Run. The model can provide multiple functions to call at once using parallel function calling:
`
The above is from the assistant docs on function calling, so maybe its the "requires_action" state that i need to check for ? @hschimpf
the part
variations
is an object or a nested parameter for Open AI. With your code, I am not sure how to define it as parameter along with namepublic function handle( #[Description('The name of the item')] string $name, #[Description('Variation of the item')] string $variation, // But I need this to represent as an object parameter with child items. ) { ...
@vijaythecoder Oh! I see what you were saying now.
I think that we can tweak the parseFunctionParameters
to allow the use of classes as parameter type. If the parameters is a class, we can access the properties of it thorugh ReflectionClass::getProperties()
method, and doing this in a loop will allow support for infinite nested params.
When you initiate a Run with a user Message that triggers the function, the Run will enter a pending status. After it processes, the run will enter a requires_action state which you can verify by retrieving the Run. The model can provide multiple functions to call at once using parallel function calling:
The above is from the assistant docs on function calling, so maybe its the "requires_action" state that i need to check for ? @hschimpf
Hi @samuelgjekic. Yeah, if you are using the Assistant's API, you should check for required_action.type = "submit_tool_outputs"
.
You will need to tweak a bit my example, but the general functionality is the same. Check for tool calls, execute them and submit the results.
When you initiate a Run with a user Message that triggers the function, the Run will enter a pending status. After it processes, the run will enter a requires_action state which you can verify by retrieving the Run. The model can provide multiple functions to call at once using parallel function calling:
The above is from the assistant docs on function calling, so maybe its the "requires_action" state that i need to check for ? @hschimpf
Hi @samuelgjekic. Yeah, if you are using the Assistant's API, you should check for
required_action.type = "submit_tool_outputs"
.You will need to tweak a bit my example, but the general functionality is the same. Check for tool calls, execute them and submit the results.
Thank you ! I got it working
Wonderful, discussion.