-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from xp-forge/feature/tool-calling
Implement function calling
- Loading branch information
Showing
8 changed files
with
511 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php namespace com\openai; | ||
|
||
use com\openai\tools\Functions; | ||
|
||
/** | ||
* Tools | ||
* | ||
* @test com.openai.unittest.ToolsTest | ||
* @see https://platform.openai.com/docs/assistants/tools | ||
*/ | ||
class Tools { | ||
public $selection= []; | ||
|
||
/** | ||
* Creates a new tools list from tools like `file_search` and `code_interpeter` | ||
* as well as user functions register in a `Functions` instance. | ||
* | ||
* @param (string|[:var]|com.openai.tools.Functions)... $selected | ||
*/ | ||
public function __construct(...$selected) { | ||
foreach ($selected as $select) { | ||
if ($select instanceof Functions) { | ||
foreach ($select->schema() as $name => $function) { | ||
$this->selection[]= ['type' => 'function', 'function' => [ | ||
'name' => $name, | ||
'description' => $function['description'], | ||
'parameters' => $function['input'], | ||
]]; | ||
} | ||
} else { | ||
$this->selection[]= is_string($select) ? ['type' => $select] : $select; | ||
} | ||
} | ||
} | ||
|
||
/** @return var */ | ||
public function __serialize() { return $this->selection; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<?php namespace com\openai\tools; | ||
|
||
use com\openai\tools\{Param, Context}; | ||
use lang\reflection\TargetException; | ||
use lang\{Type, Reflection, Value, IllegalArgumentException}; | ||
use util\{Comparison, Objects}; | ||
|
||
/** | ||
* Function calling | ||
* | ||
* @test com.openai.unittest.FunctionsTest | ||
*/ | ||
class Functions implements Value { | ||
use Comparison; | ||
|
||
private $instances= []; | ||
private $methods= []; | ||
|
||
/** | ||
* Registers public instance methods for a given implementation | ||
* | ||
* @param string $namespace | ||
* @param string|object $impl | ||
* @return self | ||
*/ | ||
private function methods($namespace, $impl) { | ||
$this->methods[$namespace]= []; | ||
foreach (Reflection::type($impl)->methods() as $name => $method) { | ||
$mod= $method->modifiers(); | ||
if ($mod->isStatic() || !$mod->isPublic()) continue; | ||
|
||
$this->methods[$namespace][$name]= $method; | ||
} | ||
return $this; | ||
} | ||
|
||
/** Registers the given instance using a defined namespace */ | ||
public function register(string $namespace, object $instance): self { | ||
$this->instances[$namespace]= [$instance, null]; | ||
return $this->methods($namespace, $instance); | ||
} | ||
|
||
/** | ||
* Register the given type using a defined namespace | ||
* | ||
* @param string $namespace | ||
* @param string|lang.Type $impl | ||
* @param ?function(lang.Type): object $new | ||
* @return self | ||
*/ | ||
public function with(string $namespace, $impl, $new= null): self { | ||
$t= $impl instanceof Type ? $impl : Type::forName($impl); | ||
$this->instances[$namespace]= [null, fn() => $new ? $new($t) : $t->newInstance()]; | ||
return $this->methods($namespace, $impl); | ||
} | ||
|
||
/** Selects functions matching the given selectors */ | ||
public function select(array $selectors): self { | ||
$self= new self(); | ||
foreach ($selectors as $selector) { | ||
[$namespace, $name]= explode('_', $selector); | ||
if (!isset($this->methods[$namespace])) continue; | ||
|
||
$methods= $this->methods[$namespace]; | ||
if ('*' === $name) { | ||
$self->methods[$namespace]= $methods; | ||
} else if ($method= $methods[$name] ?? null) { | ||
$self->methods[$namespace]??= []; | ||
$self->methods[$namespace][$name]= $method; | ||
} | ||
} | ||
return $self; | ||
} | ||
|
||
/** Yields descriptions for all methods registered */ | ||
public function schema(): iterable { | ||
foreach ($this->methods as $namespace => $methods) { | ||
foreach ($methods as $name => $method) { | ||
|
||
// Use annotated parameters if possible | ||
$properties= $required= []; | ||
foreach ($method->parameters() as $param => $reflect) { | ||
$annotations= $reflect->annotations(); | ||
if ($annotations->provides(Context::class)) { | ||
continue; | ||
} else if ($annotation= $annotations->type(Param::class)) { | ||
$properties[$param]= $annotation->newInstance()->schema(); | ||
} else { | ||
$properties[$param]= ['type' => 'string', 'description' => ucfirst($param)]; | ||
} | ||
$reflect->optional() || $required[]= $param; | ||
} | ||
|
||
yield $namespace.'_'.$name => [ | ||
'description' => $method->comment() ?? ucfirst($name), | ||
'input' => [ | ||
'type' => 'object', | ||
'properties' => $properties ?: (object)[], | ||
'required' => $required, | ||
], | ||
]; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Invokes the given tool from a tool call | ||
* | ||
* @param string $call | ||
* @param [:var] $arguments | ||
* @param [:var] $context | ||
* @return var | ||
* @throws lang.Throwable | ||
*/ | ||
public function invoke(string $call, array $arguments, array $context= []) { | ||
try { | ||
sscanf($call, "%[^_]_%[^\r]", $namespace, $name); | ||
if (null === ($method= $this->methods[$namespace][$name] ?? null)) { | ||
throw new IllegalArgumentException(isset($this->methods[$namespace]) | ||
? "Unknown function {$name} in {$namespace}" | ||
: "Unknown namespace {$namespace}" | ||
); | ||
} | ||
|
||
// Lazily create instance | ||
list(&$instance, $new)= $this->instances[$namespace]; | ||
$instance??= $new(); | ||
|
||
$pass= []; | ||
foreach ($method->parameters() as $param => $reflect) { | ||
if ($reflect->annotations()->provides(Context::class)) { | ||
$ptr= &$context[$param]; | ||
} else { | ||
$ptr= &$arguments[$param]; | ||
} | ||
|
||
if (isset($ptr)) { | ||
$pass[]= $ptr; | ||
} else if ($reflect->optional()) { | ||
$pass[]= $reflect->default(); | ||
} else { | ||
throw new IllegalArgumentException("Missing argument {$param} for {$call}"); | ||
} | ||
} | ||
return $method->invoke($instance, $pass); | ||
} catch (TargetException $e) { | ||
throw $e->getCause(); | ||
} | ||
} | ||
|
||
/** @return string */ | ||
public function toString() { | ||
$s= nameof($this)." [\n"; | ||
foreach ($this->descriptions() as $name => $description) { | ||
$s.= ' '.$name.'() -> '.Objects::stringOf($description, ' ')."\n"; | ||
} | ||
return $s.']'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php namespace com\openai\tools; | ||
|
||
class Param { | ||
private $description, $type; | ||
|
||
/** | ||
* Creates a new param annotation | ||
* | ||
* @see https://json-schema.org/understanding-json-schema/reference/type | ||
* @see https://json-schema.org/understanding-json-schema/reference/enum | ||
* @param ?string $description | ||
* @param string|[:var] $type | ||
*/ | ||
public function __construct($description= null, $type= 'string') { | ||
$this->description= $description; | ||
$this->type= is_array($type) ? $type : ['type' => $type]; | ||
} | ||
|
||
/** Returns parameter schema */ | ||
public function schema(): array { | ||
return null === $this->description ? $this->type : $this->type + ['description' => $this->description]; | ||
} | ||
} |
Oops, something went wrong.