Skip to content

Commit c7adc42

Browse files
committed
implement function calling example
1 parent bc86a00 commit c7adc42

File tree

7 files changed

+272
-0
lines changed

7 files changed

+272
-0
lines changed

config/services.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ services:
2222
$baseUrl: '%env(WEBHOOK_BASE_URL)%'
2323
$token: '%env(TELEGRAM_TOKEN)%'
2424

25+
App\SymfonyConBot\ToolBox\SerpApi\SerpApiFunction:
26+
$apiKey: '%env(SERP_API_KEY)%'
27+
2528
Probots\Pinecone\Client:
2629
$apiKey: '%env(PINECONE_API_KEY)%'
2730
$environment: '%env(PINECONE_ENVIRONMENT)%'

src/Command/DemoChatCommand.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Command;
6+
7+
use App\SymfonyConBot\Message\Message;
8+
use App\SymfonyConBot\Message\MessageBag;
9+
use App\SymfonyConBot\OpenAI\ChatModel;
10+
use Symfony\Component\Console\Attribute\AsCommand;
11+
use Symfony\Component\Console\Command\Command;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
use Symfony\Component\Console\Style\SymfonyStyle;
15+
16+
#[AsCommand('app:demo:chat', description: 'Command for testing chat')]
17+
final class DemoChatCommand extends Command
18+
{
19+
public function __construct(private readonly ChatModel $model)
20+
{
21+
parent::__construct();
22+
}
23+
24+
protected function execute(InputInterface $input, OutputInterface $output): int
25+
{
26+
$io = new SymfonyStyle($input, $output);
27+
$io->title('Demo Chat');
28+
29+
$prompt = $io->ask('What do you want to know?', 'What is the latest Symfony version?');
30+
$response = $this->model->call(new MessageBag(Message::ofUser($prompt)));
31+
32+
$io->block($response['choices'][0]['message']['content']);
33+
34+
return 0;
35+
}
36+
}

src/Command/DemoFunctionsCommand.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Command;
6+
7+
use App\SymfonyConBot\Message\Message;
8+
use App\SymfonyConBot\Message\MessageBag;
9+
use App\SymfonyConBot\OpenAI\FunctionChain;
10+
use Symfony\Component\Console\Attribute\AsCommand;
11+
use Symfony\Component\Console\Command\Command;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
use Symfony\Component\Console\Style\SymfonyStyle;
15+
16+
#[AsCommand('app:demo:functions', description: 'Command for testing function calls')]
17+
final class DemoFunctionsCommand extends Command
18+
{
19+
public function __construct(private readonly FunctionChain $chain)
20+
{
21+
parent::__construct();
22+
}
23+
24+
protected function execute(InputInterface $input, OutputInterface $output): int
25+
{
26+
$io = new SymfonyStyle($input, $output);
27+
$io->title('Demo Functions');
28+
29+
$prompt = $io->ask('What do you want to know?', 'What is the latest Symfony version?');
30+
$response = $this->chain->call(new MessageBag(Message::ofUser($prompt)));
31+
32+
$io->writeln($response);
33+
34+
return 0;
35+
}
36+
}
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\SymfonyConBot\OpenAI;
6+
7+
use App\SymfonyConBot\Message\Message;
8+
use App\SymfonyConBot\Message\MessageBag;
9+
use App\SymfonyConBot\ToolBox\FunctionRegistry;
10+
11+
final readonly class FunctionChain
12+
{
13+
public function __construct(
14+
private ChatModel $model,
15+
private FunctionRegistry $functionRegistry,
16+
) {
17+
}
18+
19+
public function call(MessageBag $messages): string
20+
{
21+
$response = $this->model->call($messages, [
22+
'functions' => $this->functionRegistry->getMap(),
23+
]);
24+
25+
while ('function_call' === $response['choices'][0]['finish_reason']) {
26+
['name' => $name, 'arguments' => $arguments] = $response['choices'][0]['message']['function_call'];
27+
$result = $this->functionRegistry->execute($name, $arguments);
28+
29+
$messages[] = Message::ofAssistant(functionCall: [
30+
'name' => $name,
31+
'arguments' => $arguments,
32+
]);
33+
$messages[] = Message::ofFunctionCall($name, $result);
34+
35+
$response = $this->model->call($messages);
36+
}
37+
38+
return $response['choices'][0]['message']['content'];
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\SymfonyConBot\ToolBox;
6+
7+
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
8+
9+
/**
10+
* @phpstan-type FunctionParameterDefinition array{
11+
* type: 'object',
12+
* properties: array<string, array{type: string, description: string}>,
13+
* required: list<string>,
14+
* }
15+
*/
16+
#[AutoconfigureTag('llm_chain.function')]
17+
interface FunctionInterface
18+
{
19+
public static function getName(): string;
20+
21+
public static function getDescription(): string;
22+
23+
/**
24+
* @return FunctionParameterDefinition
25+
*/
26+
public static function getParametersDefinition(): array;
27+
28+
/**
29+
* @param array<string, mixed> $arguments
30+
*/
31+
public function execute(array $arguments): string;
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\SymfonyConBot\ToolBox;
6+
7+
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
8+
9+
/**
10+
* @phpstan-import-type FunctionParameterDefinition from FunctionInterface
11+
*/
12+
final readonly class FunctionRegistry
13+
{
14+
/**
15+
* @var array<string, FunctionInterface>
16+
*/
17+
private array $functions;
18+
19+
/**
20+
* @param iterable<FunctionInterface> $functions
21+
*/
22+
public function __construct(
23+
#[TaggedIterator('llm_chain.function', defaultIndexMethod: 'getName')]
24+
iterable $functions,
25+
) {
26+
$this->functions = $functions instanceof \Traversable ? iterator_to_array($functions) : $functions;
27+
}
28+
29+
/**
30+
* @return list<array{
31+
* name: string,
32+
* description: string,
33+
* parameters: FunctionParameterDefinition,
34+
* }>
35+
*/
36+
public function getMap(): array
37+
{
38+
$functionsMap = [];
39+
40+
foreach ($this->functions as $function) {
41+
$functionsMap[] = [
42+
'name' => $function->getName(),
43+
'description' => $function->getDescription(),
44+
'parameters' => $function->getParametersDefinition(),
45+
];
46+
}
47+
48+
return $functionsMap;
49+
}
50+
51+
public function execute(string $name, string $arguments): string
52+
{
53+
return $this->functions[$name]->execute(json_decode($arguments, true));
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\SymfonyConBot\ToolBox\SerpApi;
6+
7+
use App\SymfonyConBot\ToolBox\FunctionInterface;
8+
use Symfony\Contracts\HttpClient\HttpClientInterface;
9+
10+
/**
11+
* @phpstan-import-type FunctionParameterDefinition from FunctionInterface
12+
*/
13+
final readonly class SerpApiFunction implements FunctionInterface
14+
{
15+
public function __construct(
16+
private HttpClientInterface $httpClient,
17+
private string $apiKey,
18+
) {
19+
}
20+
21+
public static function getName(): string
22+
{
23+
return 'serpapi';
24+
}
25+
26+
public static function getDescription(): string
27+
{
28+
return 'search for information on the internet';
29+
}
30+
31+
/**
32+
* @return FunctionParameterDefinition
33+
*/
34+
public static function getParametersDefinition(): array
35+
{
36+
return [
37+
'type' => 'object',
38+
'properties' => [
39+
'query' => [
40+
'type' => 'string',
41+
'description' => 'The search query to use',
42+
],
43+
],
44+
'required' => ['query'],
45+
];
46+
}
47+
48+
/**
49+
* @param array{query: string} $arguments
50+
*/
51+
public function execute(array $arguments): string
52+
{
53+
$response = $this->httpClient->request('GET', 'https://serpapi.com/search', [
54+
'query' => [
55+
'q' => $arguments['query'],
56+
'api_key' => $this->apiKey,
57+
],
58+
]);
59+
60+
return sprintf('Results for "%s" are "%s".', $arguments['query'], $this->extractBestResponse($response->toArray()));
61+
}
62+
63+
/**
64+
* @param array<string, mixed> $results
65+
*/
66+
private function extractBestResponse(array $results): string
67+
{
68+
return implode(PHP_EOL, array_map(fn ($story) => sprintf('%s: %s', $story['title'], $story['snippet']), $results['organic_results']));
69+
}
70+
}

0 commit comments

Comments
 (0)