From f7b5eed2adb56e00350b30f1c171e0b0fd4f8832 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 3 Dec 2023 20:43:34 +0100 Subject: [PATCH] implement function calling example --- config/services.yaml | 3 + src/Command/DemoChatCommand.php | 36 ++++++++++ src/Command/DemoFunctionsCommand.php | 36 ++++++++++ src/SymfonyConBot/OpenAI/FunctionChain.php | 40 +++++++++++ .../ToolBox/FunctionInterface.php | 32 +++++++++ .../ToolBox/FunctionRegistry.php | 55 +++++++++++++++ .../ToolBox/SerpApi/SerpApiFunction.php | 70 +++++++++++++++++++ 7 files changed, 272 insertions(+) create mode 100644 src/Command/DemoChatCommand.php create mode 100644 src/Command/DemoFunctionsCommand.php create mode 100644 src/SymfonyConBot/OpenAI/FunctionChain.php create mode 100644 src/SymfonyConBot/ToolBox/FunctionInterface.php create mode 100644 src/SymfonyConBot/ToolBox/FunctionRegistry.php create mode 100644 src/SymfonyConBot/ToolBox/SerpApi/SerpApiFunction.php diff --git a/config/services.yaml b/config/services.yaml index c29973f..9fb5db4 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -22,6 +22,9 @@ services: $baseUrl: '%env(WEBHOOK_BASE_URL)%' $token: '%env(TELEGRAM_TOKEN)%' + App\SymfonyConBot\ToolBox\SerpApi\SerpApiFunction: + $apiKey: '%env(SERP_API_KEY)%' + Probots\Pinecone\Client: $apiKey: '%env(PINECONE_API_KEY)%' $environment: '%env(PINECONE_ENVIRONMENT)%' diff --git a/src/Command/DemoChatCommand.php b/src/Command/DemoChatCommand.php new file mode 100644 index 0000000..cd21674 --- /dev/null +++ b/src/Command/DemoChatCommand.php @@ -0,0 +1,36 @@ +title('Demo Chat'); + + $prompt = $io->ask('What do you want to know?', 'What is the latest Symfony version?'); + $response = $this->model->call(new MessageBag(Message::ofUser($prompt))); + + $io->block($response['choices'][0]['message']['content']); + + return 0; + } +} diff --git a/src/Command/DemoFunctionsCommand.php b/src/Command/DemoFunctionsCommand.php new file mode 100644 index 0000000..aba499c --- /dev/null +++ b/src/Command/DemoFunctionsCommand.php @@ -0,0 +1,36 @@ +title('Demo Functions'); + + $prompt = $io->ask('What do you want to know?', 'What is the latest Symfony version?'); + $response = $this->chain->call(new MessageBag(Message::ofUser($prompt))); + + $io->writeln($response); + + return 0; + } +} diff --git a/src/SymfonyConBot/OpenAI/FunctionChain.php b/src/SymfonyConBot/OpenAI/FunctionChain.php new file mode 100644 index 0000000..d46d89c --- /dev/null +++ b/src/SymfonyConBot/OpenAI/FunctionChain.php @@ -0,0 +1,40 @@ +model->call($messages, [ + 'functions' => $this->functionRegistry->getMap(), + ]); + + while ('function_call' === $response['choices'][0]['finish_reason']) { + ['name' => $name, 'arguments' => $arguments] = $response['choices'][0]['message']['function_call']; + $result = $this->functionRegistry->execute($name, $arguments); + + $messages[] = Message::ofAssistant(functionCall: [ + 'name' => $name, + 'arguments' => $arguments, + ]); + $messages[] = Message::ofFunctionCall($name, $result); + + $response = $this->model->call($messages); + } + + return $response['choices'][0]['message']['content']; + } +} diff --git a/src/SymfonyConBot/ToolBox/FunctionInterface.php b/src/SymfonyConBot/ToolBox/FunctionInterface.php new file mode 100644 index 0000000..3c986b8 --- /dev/null +++ b/src/SymfonyConBot/ToolBox/FunctionInterface.php @@ -0,0 +1,32 @@ +, + * required: list, + * } + */ +#[AutoconfigureTag('llm_chain.function')] +interface FunctionInterface +{ + public static function getName(): string; + + public static function getDescription(): string; + + /** + * @return FunctionParameterDefinition + */ + public static function getParametersDefinition(): array; + + /** + * @param array $arguments + */ + public function execute(array $arguments): string; +} diff --git a/src/SymfonyConBot/ToolBox/FunctionRegistry.php b/src/SymfonyConBot/ToolBox/FunctionRegistry.php new file mode 100644 index 0000000..953ef78 --- /dev/null +++ b/src/SymfonyConBot/ToolBox/FunctionRegistry.php @@ -0,0 +1,55 @@ + + */ + private array $functions; + + /** + * @param iterable $functions + */ + public function __construct( + #[TaggedIterator('llm_chain.function', defaultIndexMethod: 'getName')] + iterable $functions, + ) { + $this->functions = $functions instanceof \Traversable ? iterator_to_array($functions) : $functions; + } + + /** + * @return list + */ + public function getMap(): array + { + $functionsMap = []; + + foreach ($this->functions as $function) { + $functionsMap[] = [ + 'name' => $function->getName(), + 'description' => $function->getDescription(), + 'parameters' => $function->getParametersDefinition(), + ]; + } + + return $functionsMap; + } + + public function execute(string $name, string $arguments): string + { + return $this->functions[$name]->execute(json_decode($arguments, true)); + } +} diff --git a/src/SymfonyConBot/ToolBox/SerpApi/SerpApiFunction.php b/src/SymfonyConBot/ToolBox/SerpApi/SerpApiFunction.php new file mode 100644 index 0000000..49704e6 --- /dev/null +++ b/src/SymfonyConBot/ToolBox/SerpApi/SerpApiFunction.php @@ -0,0 +1,70 @@ + 'object', + 'properties' => [ + 'query' => [ + 'type' => 'string', + 'description' => 'The search query to use', + ], + ], + 'required' => ['query'], + ]; + } + + /** + * @param array{query: string} $arguments + */ + public function execute(array $arguments): string + { + $response = $this->httpClient->request('GET', 'https://serpapi.com/search', [ + 'query' => [ + 'q' => $arguments['query'], + 'api_key' => $this->apiKey, + ], + ]); + + return sprintf('Results for "%s" are "%s".', $arguments['query'], $this->extractBestResponse($response->toArray())); + } + + /** + * @param array $results + */ + private function extractBestResponse(array $results): string + { + return implode(PHP_EOL, array_map(fn ($story) => sprintf('%s: %s', $story['title'], $story['snippet']), $results['organic_results'])); + } +}