From 130a1508f9e63800df48cc98f5d166cef197efb5 Mon Sep 17 00:00:00 2001 From: Mateusz Charytoniuk <mateusz.charytoniuk@protonmail.com> Date: Wed, 3 Apr 2024 21:50:25 +0200 Subject: [PATCH] chore: feature extraction from string --- phpunit.xml | 15 ++-- src/DialogueNode.php | 18 +++-- src/DialogueNodeInterface.php | 2 +- src/DialogueNodeTest.php | 19 ++--- src/DialogueResponse.php | 18 +---- src/DialogueResponse/LiteralInputResponse.php | 37 +++++++++ .../LlamaCppExtractInputResponse.php | 19 +++++ src/DialogueResponseCondition.php | 7 -- .../ExactInputCondition.php | 25 ------ .../LlamaCppInputCondition.php | 24 ------ src/DialogueResponseConditionInterface.php | 12 --- src/DialogueResponseDiscriminator.php | 27 ------- ...DialogueResponseDiscriminatorInterface.php | 16 ---- src/DialogueResponseInterface.php | 4 +- src/DialogueResponseResolutionStatus.php | 6 +- src/DialogueResponseSortedIterator.php | 2 +- src/LlamaCppClientTest.php | 26 ++++++ src/LlamaCppExtractString.php | 46 +++++++++++ src/LlamaCppExtractStringTest.php | 81 +++++++++++++++++++ 19 files changed, 247 insertions(+), 157 deletions(-) create mode 100644 src/DialogueResponse/LiteralInputResponse.php create mode 100644 src/DialogueResponse/LlamaCppExtractInputResponse.php delete mode 100644 src/DialogueResponseCondition.php delete mode 100644 src/DialogueResponseCondition/ExactInputCondition.php delete mode 100644 src/DialogueResponseCondition/LlamaCppInputCondition.php delete mode 100644 src/DialogueResponseConditionInterface.php delete mode 100644 src/DialogueResponseDiscriminator.php delete mode 100644 src/DialogueResponseDiscriminatorInterface.php create mode 100644 src/LlamaCppClientTest.php create mode 100644 src/LlamaCppExtractString.php create mode 100644 src/LlamaCppExtractStringTest.php diff --git a/phpunit.xml b/phpunit.xml index 3658f468..3baead97 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,14 +8,19 @@ displayDetailsOnTestsThatTriggerWarnings="true" processIsolation="false" > - <testsuites> - <testsuite name="Unit"> - <directory suffix="Test.php">src</directory> - </testsuite> - </testsuites> + <groups> + <exclude> + <group>llamacpp</group> + </exclude> + </groups> <source> <include> <directory suffix=".php">src</directory> </include> </source> + <testsuites> + <testsuite name="Unit"> + <directory suffix="Test.php">src</directory> + </testsuite> + </testsuites> </phpunit> diff --git a/src/DialogueNode.php b/src/DialogueNode.php index 5943acf3..fab07c11 100644 --- a/src/DialogueNode.php +++ b/src/DialogueNode.php @@ -13,14 +13,12 @@ readonly class DialogueNode implements DialogueNodeInterface */ private Set $responses; - public function __construct( - private DialogueMessageProducerInterface $message, - private DialogueResponseDiscriminatorInterface $responseDiscriminator, - ) { + public function __construct(private DialogueMessageProducerInterface $message) + { $this->responses = new Set(); } - public function addResponse(DialogueResponseInterface $response): void + public function addPotentialResponse(DialogueResponseInterface $response): void { $this->responses->add($response); } @@ -30,8 +28,14 @@ readonly class DialogueNode implements DialogueNodeInterface return $this->message; } - public function respondTo(DialogueInputInterface $prompt): ?DialogueNodeInterface + public function respondTo(DialogueInputInterface $dialogueInput): ?DialogueNodeInterface { - return $this->responseDiscriminator->discriminate($this->responses, $prompt); + foreach (new DialogueResponseSortedIterator($this->responses) as $response) { + if ($response->getCondition()->isMetBy($dialogueInput)) { + return $response->getFollowUp(); + } + } + + return null; } } diff --git a/src/DialogueNodeInterface.php b/src/DialogueNodeInterface.php index 875b1c9a..54ecfe94 100644 --- a/src/DialogueNodeInterface.php +++ b/src/DialogueNodeInterface.php @@ -6,7 +6,7 @@ namespace Distantmagic\Resonance; interface DialogueNodeInterface { - public function addResponse(DialogueResponseInterface $response): void; + public function addPotentialResponse(DialogueResponseInterface $response): void; public function getMessageProducer(): DialogueMessageProducerInterface; diff --git a/src/DialogueNodeTest.php b/src/DialogueNodeTest.php index 64b86b70..34f644fb 100644 --- a/src/DialogueNodeTest.php +++ b/src/DialogueNodeTest.php @@ -6,7 +6,7 @@ namespace Distantmagic\Resonance; use Distantmagic\Resonance\DialogueInput\UserInput; use Distantmagic\Resonance\DialogueMessageProducer\ConstMessageProducer; -use Distantmagic\Resonance\DialogueResponseCondition\ExactInputCondition; +use Distantmagic\Resonance\DialogueResponse\LiteralInputResponse; use Distantmagic\Resonance\DialogueResponseCondition\LlamaCppInputCondition; use Mockery; use PHPUnit\Framework\Attributes\CoversClass; @@ -20,38 +20,33 @@ final class DialogueNodeTest extends TestCase { public function test_dialogue_produces_no_response(): void { - $responseDiscriminator = new DialogueResponseDiscriminator(); - $rootNode = new DialogueNode( message: new ConstMessageProducer('What is your current role?'), - responseDiscriminator: $responseDiscriminator, ); $marketingNode = new DialogueNode( message: new ConstMessageProducer('Hello, marketer!'), - responseDiscriminator: $responseDiscriminator, ); - $rootNode->addResponse(new DialogueResponse( + $rootNode->addPotentialResponse(new DialogueResponse( when: new LlamaCppInputCondition( Mockery::mock(LlamaCppClientInterface::class), - 'marketing' + 'User states that they are working in a marketing department', ), followUp: $marketingNode, )); - $rootNode->addResponse(new DialogueResponse( - when: new ExactInputCondition('marketing'), + $rootNode->addPotentialResponse(new LiteralInputResponse( + when: 'marketing', followUp: $marketingNode, )); $invalidNode = new DialogueNode( message: new ConstMessageProducer('nope :('), - responseDiscriminator: $responseDiscriminator, ); - $rootNode->addResponse(new DialogueResponse( - when: new ExactInputCondition('not_a_marketing'), + $rootNode->addPotentialResponse(new LiteralInputResponse( + when: 'not_a_marketing', followUp: $invalidNode, )); diff --git a/src/DialogueResponse.php b/src/DialogueResponse.php index 8dfb37d4..9940317f 100644 --- a/src/DialogueResponse.php +++ b/src/DialogueResponse.php @@ -4,20 +4,4 @@ declare(strict_types=1); namespace Distantmagic\Resonance; -readonly class DialogueResponse implements DialogueResponseInterface -{ - public function __construct( - private DialogueNodeInterface $followUp, - private DialogueResponseConditionInterface $when, - ) {} - - public function getCondition(): DialogueResponseConditionInterface - { - return $this->when; - } - - public function getFollowUp(): DialogueNodeInterface - { - return $this->followUp; - } -} +abstract readonly class DialogueResponse implements DialogueResponseInterface {} diff --git a/src/DialogueResponse/LiteralInputResponse.php b/src/DialogueResponse/LiteralInputResponse.php new file mode 100644 index 00000000..d293a91c --- /dev/null +++ b/src/DialogueResponse/LiteralInputResponse.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\DialogueResponse; + +use Distantmagic\Resonance\DialogueInputInterface; +use Distantmagic\Resonance\DialogueNodeInterface; +use Distantmagic\Resonance\DialogueResponse; +use Distantmagic\Resonance\DialogueResponseResolution; +use Distantmagic\Resonance\DialogueResponseResolutionStatus; + +readonly class LiteralInputResponse extends DialogueResponse +{ + public function __construct( + private string $when, + private DialogueNodeInterface $followUp, + ) {} + + public function getCost(): int + { + return 2; + } + + public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolution + { + if ($dialogueInput->getContent() === $this->when) { + return new DialogueResponseResolution( + status: DialogueResponseResolutionStatus::CanRespond, + ); + } + + return new DialogueResponseResolution( + status: DialogueResponseResolutionStatus::CannotRespond, + ); + } +} diff --git a/src/DialogueResponse/LlamaCppExtractInputResponse.php b/src/DialogueResponse/LlamaCppExtractInputResponse.php new file mode 100644 index 00000000..02204bcb --- /dev/null +++ b/src/DialogueResponse/LlamaCppExtractInputResponse.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\DialogueResponse; + +use Distantmagic\Resonance\DialogueInputInterface; +use Distantmagic\Resonance\DialogueResponse; +use Distantmagic\Resonance\DialogueResponseResolution; + +readonly class LlamaCppExtractInputResponse extends DialogueResponse +{ + public function getCost(): int + { + return 50; + } + + public function resolveResponse(DialogueInputInterface $prompt): DialogueResponseResolution {} +} diff --git a/src/DialogueResponseCondition.php b/src/DialogueResponseCondition.php deleted file mode 100644 index 9196226c..00000000 --- a/src/DialogueResponseCondition.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -abstract readonly class DialogueResponseCondition implements DialogueResponseConditionInterface {} diff --git a/src/DialogueResponseCondition/ExactInputCondition.php b/src/DialogueResponseCondition/ExactInputCondition.php deleted file mode 100644 index 20449344..00000000 --- a/src/DialogueResponseCondition/ExactInputCondition.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance\DialogueResponseCondition; - -use Distantmagic\Resonance\DialogueInputInterface; -use Distantmagic\Resonance\DialogueResponseCondition; - -readonly class ExactInputCondition extends DialogueResponseCondition -{ - public function __construct( - public string $content, - ) {} - - public function getCost(): int - { - return 2; - } - - public function isMetBy(DialogueInputInterface $dialogueInput): bool - { - return $dialogueInput->getContent() === $this->content; - } -} diff --git a/src/DialogueResponseCondition/LlamaCppInputCondition.php b/src/DialogueResponseCondition/LlamaCppInputCondition.php deleted file mode 100644 index 97d99fe5..00000000 --- a/src/DialogueResponseCondition/LlamaCppInputCondition.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance\DialogueResponseCondition; - -use Distantmagic\Resonance\DialogueInputInterface; -use Distantmagic\Resonance\DialogueResponseCondition; -use Distantmagic\Resonance\LlamaCppClientInterface; - -readonly class LlamaCppInputCondition extends DialogueResponseCondition -{ - public function __construct( - public LlamaCppClientInterface $llamaCppClient, - public string $content, - ) {} - - public function getCost(): int - { - return 50; - } - - public function isMetBy(DialogueInputInterface $dialogueInput): bool {} -} diff --git a/src/DialogueResponseConditionInterface.php b/src/DialogueResponseConditionInterface.php deleted file mode 100644 index 80e50ff9..00000000 --- a/src/DialogueResponseConditionInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -interface DialogueResponseConditionInterface -{ - public function getCost(): int; - - public function isMetBy(DialogueInputInterface $dialogueInput): bool; -} diff --git a/src/DialogueResponseDiscriminator.php b/src/DialogueResponseDiscriminator.php deleted file mode 100644 index 628af587..00000000 --- a/src/DialogueResponseDiscriminator.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -use Distantmagic\Resonance\Attribute\Singleton; - -#[Singleton] -readonly class DialogueResponseDiscriminator implements DialogueResponseDiscriminatorInterface -{ - /** - * @param iterable<DialogueResponseInterface> $responses - */ - public function discriminate( - iterable $responses, - DialogueInputInterface $dialogueInput, - ): ?DialogueNodeInterface { - foreach (new DialogueResponseSortedIterator($responses) as $response) { - if ($response->getCondition()->isMetBy($dialogueInput)) { - return $response->getFollowUp(); - } - } - - return null; - } -} diff --git a/src/DialogueResponseDiscriminatorInterface.php b/src/DialogueResponseDiscriminatorInterface.php deleted file mode 100644 index 60002459..00000000 --- a/src/DialogueResponseDiscriminatorInterface.php +++ /dev/null @@ -1,16 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -interface DialogueResponseDiscriminatorInterface -{ - /** - * @param iterable<DialogueResponseInterface> $responses - */ - public function discriminate( - iterable $responses, - DialogueInputInterface $dialogueInput, - ): ?DialogueNodeInterface; -} diff --git a/src/DialogueResponseInterface.php b/src/DialogueResponseInterface.php index df2c6784..8eba9961 100644 --- a/src/DialogueResponseInterface.php +++ b/src/DialogueResponseInterface.php @@ -6,7 +6,7 @@ namespace Distantmagic\Resonance; interface DialogueResponseInterface { - public function getCondition(): DialogueResponseConditionInterface; + public function getCost(): int; - public function getFollowUp(): DialogueNodeInterface; + public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolutionInterface; } diff --git a/src/DialogueResponseResolutionStatus.php b/src/DialogueResponseResolutionStatus.php index 136588fa..b4db7d82 100644 --- a/src/DialogueResponseResolutionStatus.php +++ b/src/DialogueResponseResolutionStatus.php @@ -4,4 +4,8 @@ declare(strict_types=1); namespace Distantmagic\Resonance; -enum DialogueResponseResolutionStatus {} +enum DialogueResponseResolutionStatus +{ + case CannotRespond; + case CanRespond; +} diff --git a/src/DialogueResponseSortedIterator.php b/src/DialogueResponseSortedIterator.php index de0aa579..4943da41 100644 --- a/src/DialogueResponseSortedIterator.php +++ b/src/DialogueResponseSortedIterator.php @@ -34,7 +34,7 @@ readonly class DialogueResponseSortedIterator implements IteratorAggregate foreach ($this->responses as $response) { $responsesPriorityQueue->push( $response, - $response->getCondition()->getCost(), + $response->getCost(), ); } diff --git a/src/LlamaCppClientTest.php b/src/LlamaCppClientTest.php new file mode 100644 index 00000000..f9c6a767 --- /dev/null +++ b/src/LlamaCppClientTest.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +#[CoversClass(LlamaCppClient::class)] +#[Group('llamacpp')] +final class LlamaCppClientTest extends TestCase +{ + use TestsDependencyInectionContainerTrait; + + public function test_request_header_is_parsed(): void + { + $llamaCppClient = self::$container->make(LlamaCppClient::class); + + self::assertSame(LlamaCppHealthStatus::Ok, $llamaCppClient->getHealth()); + } +} diff --git a/src/LlamaCppExtractString.php b/src/LlamaCppExtractString.php new file mode 100644 index 00000000..d76185ec --- /dev/null +++ b/src/LlamaCppExtractString.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Distantmagic\Resonance\LlmPromptTemplate\MistralInstructChat; + +readonly class LlamaCppExtractString +{ + public function __construct( + private LlamaCppClientInterface $llamaCppClient, + ) {} + + public function extract( + string $input, + string $subject, + ): ?string { + $completion = $this->llamaCppClient->generateCompletion( + new LlamaCppCompletionRequest( + promptTemplate: new MistralInstructChat(<<<PROMPT + User is about to provide the $subject. + If user provides the $subject, repeat only that $subject, without any additional comment. + If user did not provide $subject or it is not certain, write the empty string: "" + + User input: + $input + PROMPT), + ), + ); + + $ret = ''; + + foreach ($completion as $token) { + $ret .= $token; + } + + $trimmed = trim($ret, ' "'); + + if (0 === strlen($trimmed)) { + return null; + } + + return $trimmed; + } +} diff --git a/src/LlamaCppExtractStringTest.php b/src/LlamaCppExtractStringTest.php new file mode 100644 index 00000000..398cb1c1 --- /dev/null +++ b/src/LlamaCppExtractStringTest.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Generator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; +use Swoole\Event; + +/** + * @internal + */ +#[CoversClass(LlamaCppExtractString::class)] +#[Group('llamacpp')] +final class LlamaCppExtractStringTest extends TestCase +{ + use TestsDependencyInectionContainerTrait; + + public static function inputSubjectProvider(): Generator + { + yield 'application name is provided' => [ + 'application name', + 'My application is called PHP Resonance', + 'PHP Resonance', + ]; + + yield 'only application name is provided' => [ + 'application name', + 'PHP Resonance', + 'PHP Resonance', + ]; + + yield 'application name is not provided' => [ + 'application name', + 'How are you?', + null, + ]; + + yield 'not on topic' => [ + 'application name', + 'Suggest me the best application name', + null, + ]; + + yield 'not sure' => [ + 'application name', + 'I am not really sure at the moment, was thinking about PHP Resonance, but I have to ask my friends first', + null, + ]; + + yield 'feature' => [ + 'feature', + 'I want to add a blog', + 'blog', + ]; + } + + protected function tearDown(): void + { + Event::wait(); + } + + #[DataProvider('inputSubjectProvider')] + public function test_application_name_is_provided(string $subject, string $input, ?string $expected): void + { + $llamaCppExtractString = self::$container->make(LlamaCppExtractString::class); + + SwooleCoroutineHelper::mustRun(static function () use ($expected, $input, $llamaCppExtractString, $subject) { + $extracted = $llamaCppExtractString->extract( + subject: $subject, + input: $input, + ); + + self::assertSame($expected, $extracted); + }); + } +} -- GitLab