-
-
Notifications
You must be signed in to change notification settings - Fork 466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Auto function execution #285
Comments
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. ImplementationThe 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 managementExample/HasTools.php This trait basically has the following methods:
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 functionsExample\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 <?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 AppHere it's just an example (I didn't test it). You register the functions, and then call them when needed. 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.
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
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 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 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. |
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 |
@vijaythecoder Oh! I see what you were saying now. I think that we can tweak the |
Hi @samuelgjekic. Yeah, if you are using the Assistant's API, you should check for 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. |
Hello, Is it possible to add auto function execution in this lib? We need to process the function manually now.
The text was updated successfully, but these errors were encountered: