diff --git a/README.md b/README.md index 68377bd..5213517 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ docker compose exec app bin/console app:blog:embed -vv Now you should be able to run the test command and get some results: ```shell -docker compose exec app bin/console app:chroma:test +docker compose exec app bin/console app:blog:query ``` **Don't forget to set up the project in your favorite IDE or editor.** @@ -89,4 +89,4 @@ docker compose exec app bin/console app:chroma:test * The chatbot application is a simple and small Symfony 7.2 application. * The UI is coupled to a [Twig LiveComponent](https://symfony.com/bundles/ux-live-component/current/index.html), that integrates different `Chat` implementations on top of the user's session. * You can reset the chat context by hitting the `Reset` button in the top right corner. -* You find three different usage scenarios in the upper navbar. \ No newline at end of file +* You find three different usage scenarios in the upper navbar. diff --git a/assets/app.js b/assets/app.js index dda9570..82c28fd 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,3 +1,6 @@ import './bootstrap.js'; import 'bootstrap/dist/css/bootstrap.min.css'; import './styles/app.css'; +import './styles/blog.css'; +import './styles/youtube.css'; +import './styles/wikipedia.css'; diff --git a/assets/icons/entypo/chat.svg b/assets/icons/entypo/chat.svg deleted file mode 100644 index b1f6939..0000000 --- a/assets/icons/entypo/chat.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/mdi/symfony.svg b/assets/icons/mdi/symfony.svg new file mode 100644 index 0000000..5ed8680 --- /dev/null +++ b/assets/icons/mdi/symfony.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/mingcute/ai-fill.svg b/assets/icons/mingcute/ai-fill.svg deleted file mode 100644 index 03ddb45..0000000 --- a/assets/icons/mingcute/ai-fill.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/styles/app.css b/assets/styles/app.css index a546e8d..f6151b1 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -1,16 +1,5 @@ body { min-height: 100vh; - &.rag, .rag .card-img-top { - background: rgb(220,139,110); - background: linear-gradient(0deg, rgba(220,139,110,1) 0%, rgba(244,233,115,1) 100%); - } - &.youtube, .youtube .card-img-top { - background: rgb(34,34,34); - background: linear-gradient(0deg, rgb(0, 0, 0) 0%, rgb(71, 71, 71) 100%); - } - &.wikipedia, .wikipedia .card-img-top { - background: url('/wiki.png') no-repeat right 50px bottom 50px fixed, linear-gradient(0deg, rgb(246, 246, 246) 0%, rgb(197, 197, 197) 100%); - } footer, footer a { color: #6c757d; @@ -20,10 +9,6 @@ body { .index { .card-img-top { text-align: center; - - .youtube & { - color: #ff0000; - } } } @@ -44,55 +29,19 @@ body { .card-body { height: 700px; - .wikipedia & { - background-image: linear-gradient(135deg, #f2f2f2 16.67%, #ebebeb 16.67%, #ebebeb 50%, #f2f2f2 50%, #f2f2f2 66.67%, #ebebeb 66.67%, #ebebeb 100%); - background-size: 21.21px 21.21px; - } - .user-message { border-radius: 10px 10px 0 10px; color: #292929; - - .rag & { - background: #f4e973; - } - - .youtube & { - background: #3e2926; - color: #fafafa; - } - - .wikipedia & { - background: #ffffff; - } } .bot-message { border-radius: 10px 10px 10px 0; + color: #292929; - .rag & { - background: #dc8b6e; - - a { - color: #f4e973; - } - } - - .youtube & { - background: #df3535; - - a { - color: #3e2926; - } - } - - .wikipedia & { - background: #ffffff; - color: #292929 !important; + &.loading { + color: rgba(41, 41, 41, 0.5); } - color: #fff; - p { margin-bottom: 0; } @@ -102,31 +51,6 @@ body { width: 50px; height: 50px; border: 2px solid white; - - .rag &.bot { - outline: 1px solid #ffdacc; - background: #ffdacc; - } - - .rag &.user { - outline: 1px solid #fffad1; - background: #fffad1; - } - - .youtube &.bot { - outline: 1px solid #ffcccc; - background: #ffcccc; - } - - .youtube &.user { - outline: 1px solid #9e8282; - background: #9e8282; - } - - .wikipedia &.bot, .wikipedia &.user { - outline: 1px solid #eaeaea; - background: #eaeaea; - } } } @@ -138,28 +62,4 @@ body { box-shadow: none !important; } } - - #welcome { - h4 { - .rag & { - color: #f97b62; - } - - .youtube & { - color: #ff0000; - } - } - } - - #chat-reset, #chat-submit { - .rag &:hover { - background: #f97b62; - border-color: #f97b62; - } - - .youtube &:hover { - background: #ff0000; - border-color: #ff0000; - } - } } diff --git a/assets/styles/blog.css b/assets/styles/blog.css new file mode 100644 index 0000000..7750ecc --- /dev/null +++ b/assets/styles/blog.css @@ -0,0 +1,61 @@ +.blog { + body&, .card-img-top { + background: #2c5282; + background: linear-gradient(0deg, #2c5282 0%, #3c366b 100%); + } + + .card-img-top { + color: #ffffff; + } + + &.chat { + .user-message { + background: #d5054e; + color: #ffffff; + } + + .bot-message { + color: #ffffff; + background: #3182ce; + + &.loading { + color: rgba(255, 255, 255, 0.5); + } + + a { + color: #c8d8ef; + + &:hover { + color: #ffffff; + } + } + + code { + color: #ffb1ca; + } + } + + .avatar { + &.bot { + outline: 1px solid #b8d8fb; + background: #b8d8fb; + } + + &.user { + outline: 1px solid #ffb1ca; + background: #ffb1ca; + } + } + + #welcome h4 { + color: #2c5282; + } + + #chat-reset, #chat-submit { + &:hover { + background: #d5054e; + border-color: #d5054e; + } + } + } +} diff --git a/assets/styles/wikipedia.css b/assets/styles/wikipedia.css new file mode 100644 index 0000000..a1eec15 --- /dev/null +++ b/assets/styles/wikipedia.css @@ -0,0 +1,31 @@ +.wikipedia { + body&, .card-img-top { + background: url('/wiki.png') no-repeat right 50px bottom 50px fixed, linear-gradient(0deg, rgb(246, 246, 246) 0%, rgb(197, 197, 197) 100%); + } + + &.chat { + .card-body { + background-image: linear-gradient(135deg, #f2f2f2 16.67%, #ebebeb 16.67%, #ebebeb 50%, #f2f2f2 50%, #f2f2f2 66.67%, #ebebeb 66.67%, #ebebeb 100%); + background-size: 21.21px 21.21px; + } + + .user-message { + background: #ffffff; + } + + .bot-message { + background: #ffffff; + + a { + color: #3e2926; + } + } + + .avatar { + &.bot, &.user { + outline: 1px solid #eaeaea; + background: #eaeaea; + } + } + } +} diff --git a/assets/styles/youtube.css b/assets/styles/youtube.css new file mode 100644 index 0000000..85967ea --- /dev/null +++ b/assets/styles/youtube.css @@ -0,0 +1,49 @@ +.youtube { + body&, .card-img-top { + background: rgb(34,34,34); + background: linear-gradient(0deg, rgb(0, 0, 0) 0%, rgb(71, 71, 71) 100%); + } + + .card-img-top { + color: #ff0000; + } + + &.chat { + .user-message { + background: #3e2926; + color: #fafafa; + } + + .bot-message { + color: #ffffff; + background: #df3535; + + &.loading { + color: rgba(255, 255, 255, 0.5); + } + } + + .avatar { + &.bot { + outline: 1px solid #ffcccc; + background: #ffcccc; + } + + &.user { + outline: 1px solid #9e8282; + background: #9e8282; + } + } + + #welcome h4 { + color: #ff0000; + } + + #chat-reset, #chat-submit { + &:hover { + background: #ff0000; + border-color: #ff0000; + } + } + } +} diff --git a/config/packages/llm_chain.yaml b/config/packages/llm_chain.yaml index 8b5bdc6..809b96b 100644 --- a/config/packages/llm_chain.yaml +++ b/config/packages/llm_chain.yaml @@ -16,7 +16,7 @@ llm_chain: openai: api_key: '%env(OPENAI_API_KEY)%' chain: - rag: + blog: # platform: 'llm_chain.platform.anthropic' model: name: 'GPT' diff --git a/config/routes.yaml b/config/routes.yaml index 86dad1e..0da2b06 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -4,20 +4,23 @@ index: defaults: template: 'index.html.twig' -rag: - path: '/rag' +blog: + path: '/blog' controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController' defaults: - template: 'chat/rag.html.twig' + template: 'chat.html.twig' + context: { chat: 'blog' } youtube: path: '/youtube' controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController' defaults: - template: 'chat/youtube.html.twig' + template: 'chat.html.twig' + context: { chat: 'youtube' } wikipedia: path: '/wikipedia' controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController' defaults: - template: 'chat/wikipedia.html.twig' + template: 'chat.html.twig' + context: { chat: 'wikipedia' } diff --git a/demo.png b/demo.png index 85fdbad..703b245 100644 Binary files a/demo.png and b/demo.png differ diff --git a/src/Blog/Chat/Blog.php b/src/Blog/Chat.php similarity index 93% rename from src/Blog/Chat/Blog.php rename to src/Blog/Chat.php index 44b8c69..54f1027 100644 --- a/src/Blog/Chat/Blog.php +++ b/src/Blog/Chat.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Blog\Chat; +namespace App\Blog; use PhpLlm\LlmChain\ChainInterface; use PhpLlm\LlmChain\Model\Message\Message; @@ -11,13 +11,13 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\RequestStack; -final class Blog +final class Chat { - private const SESSION_KEY = 'rag-chat'; + private const SESSION_KEY = 'blog-chat'; public function __construct( private readonly RequestStack $requestStack, - #[Autowire(service: 'llm_chain.chain.rag')] + #[Autowire(service: 'llm_chain.chain.blog')] private readonly ChainInterface $chain, ) { } diff --git a/src/Blog/Command/BlogEmbedCommand.php b/src/Blog/Command/EmbedCommand.php similarity index 95% rename from src/Blog/Command/BlogEmbedCommand.php rename to src/Blog/Command/EmbedCommand.php index 81ec7aa..81651da 100644 --- a/src/Blog/Command/BlogEmbedCommand.php +++ b/src/Blog/Command/EmbedCommand.php @@ -12,7 +12,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand('app:blog:embed', description: 'Create embeddings for Symfony blog and push to ChromaDB.')] -final class BlogEmbedCommand extends Command +final class EmbedCommand extends Command { public function __construct( private readonly Embedder $embedder, diff --git a/src/Command/ChromaTestCommand.php b/src/Blog/Command/QueryCommand.php similarity index 69% rename from src/Command/ChromaTestCommand.php rename to src/Blog/Command/QueryCommand.php index 7ff37b5..ca49404 100644 --- a/src/Command/ChromaTestCommand.php +++ b/src/Blog/Command/QueryCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Command; +namespace App\Blog\Command; use Codewithkyrian\ChromaDB\Client; use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings; @@ -15,8 +15,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -#[AsCommand('app:chroma:test', description: 'Testing Chroma DB connection.')] -final class ChromaTestCommand extends Command +#[AsCommand('app:blog:query', description: 'Test command for querying the blog collection in Chroma DB.')] +final class QueryCommand extends Command { public function __construct( private readonly Client $chromaClient, @@ -31,23 +31,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->title('Testing Chroma DB Connection'); $io->comment('Connecting to Chroma DB ...'); - - // Check current ChromaDB version - $version = $this->chromaClient->version(); - - // Get WSC Collection $collection = $this->chromaClient->getOrCreateCollection('symfony_blog'); - $io->table(['Key', 'Value'], [ - ['ChromaDB Version', $version], + ['ChromaDB Version', $this->chromaClient->version()], ['Collection Name', $collection->name], ['Collection ID', $collection->id], ['Total Documents', $collection->count()], ]); - $io->comment('Searching for content about "New Symfony Features" ...'); + $search = $io->ask('What do you want to know about?', 'New Symfony Features'); + $io->comment(sprintf('Converting "%s" to vector & searching in Chroma DB ...', $search)); + $io->comment('Results are limited to 4 most similar documents.'); - $platformResponse = $this->platform->request(new Embeddings(), 'New Symfony Features'); + $platformResponse = $this->platform->request(new Embeddings(), $search); assert($platformResponse instanceof AsyncResponse); $platformResponse = $platformResponse->unwrap(); assert($platformResponse instanceof VectorResponse); @@ -62,16 +58,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $io->table(['ID', 'Title'], [ - /* @phpstan-ignore-next-line */ - [$queryResponse->ids[0][0], $queryResponse->metadatas[0][0]['title']], + foreach ($queryResponse->ids[0] as $i => $id) { /* @phpstan-ignore-next-line */ - [$queryResponse->ids[0][1], $queryResponse->metadatas[0][1]['title']], + $io->section($queryResponse->metadatas[0][$i]['title']); /* @phpstan-ignore-next-line */ - [$queryResponse->ids[0][2], $queryResponse->metadatas[0][2]['title']], - /* @phpstan-ignore-next-line */ - [$queryResponse->ids[0][3], $queryResponse->metadatas[0][3]['title']], - ]); + $io->block($queryResponse->metadatas[0][$i]['description']); + } $io->success('Chroma DB Connection & Similarity Search Test Successful!'); diff --git a/src/Blog/Embedder.php b/src/Blog/Embedder.php index 86343fd..c9f5e7e 100644 --- a/src/Blog/Embedder.php +++ b/src/Blog/Embedder.php @@ -11,7 +11,7 @@ final readonly class Embedder { public function __construct( - private Loader $loader, + private FeedLoader $loader, private LlmChainEmbedder $embedder, ) { } diff --git a/src/Blog/Loader.php b/src/Blog/FeedLoader.php similarity index 98% rename from src/Blog/Loader.php rename to src/Blog/FeedLoader.php index 866e7cc..6b12baa 100644 --- a/src/Blog/Loader.php +++ b/src/Blog/FeedLoader.php @@ -8,7 +8,7 @@ use Symfony\Component\Uid\Uuid; use Symfony\Contracts\HttpClient\HttpClientInterface; -class Loader +class FeedLoader { public function __construct( private HttpClientInterface $httpClient, diff --git a/src/Blog/Twig/BlogComponent.php b/src/Blog/TwigComponent.php similarity index 84% rename from src/Blog/Twig/BlogComponent.php rename to src/Blog/TwigComponent.php index ae92d6c..1b0d342 100644 --- a/src/Blog/Twig/BlogComponent.php +++ b/src/Blog/TwigComponent.php @@ -2,22 +2,21 @@ declare(strict_types=1); -namespace App\Blog\Twig; +namespace App\Blog; -use App\Blog\Chat\Blog; use PhpLlm\LlmChain\Model\Message\MessageBag; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\DefaultActionTrait; -#[AsLiveComponent('rag')] -final class BlogComponent +#[AsLiveComponent('blog')] +final class TwigComponent { use DefaultActionTrait; public function __construct( - private readonly Blog $chat, + private readonly Chat $chat, ) { } diff --git a/src/Wikipedia/Chat/Wikipedia.php b/src/Wikipedia/Chat.php similarity index 96% rename from src/Wikipedia/Chat/Wikipedia.php rename to src/Wikipedia/Chat.php index 10734ef..796a477 100644 --- a/src/Wikipedia/Chat/Wikipedia.php +++ b/src/Wikipedia/Chat.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Wikipedia\Chat; +namespace App\Wikipedia; use PhpLlm\LlmChain\ChainInterface; use PhpLlm\LlmChain\Model\Message\Message; @@ -11,7 +11,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\RequestStack; -final class Wikipedia +final class Chat { private const SESSION_KEY = 'wikipedia-chat'; diff --git a/src/Wikipedia/Twig/WikipediaComponent.php b/src/Wikipedia/TwigComponent.php similarity index 84% rename from src/Wikipedia/Twig/WikipediaComponent.php rename to src/Wikipedia/TwigComponent.php index 0c0489c..c726a86 100644 --- a/src/Wikipedia/Twig/WikipediaComponent.php +++ b/src/Wikipedia/TwigComponent.php @@ -2,9 +2,8 @@ declare(strict_types=1); -namespace App\Wikipedia\Twig; +namespace App\Wikipedia; -use App\Wikipedia\Chat\Wikipedia; use PhpLlm\LlmChain\Model\Message\MessageBag; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; @@ -12,12 +11,12 @@ use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent('wikipedia')] -final class WikipediaComponent +final class TwigComponent { use DefaultActionTrait; public function __construct( - private readonly Wikipedia $wikipedia, + private readonly Chat $wikipedia, ) { } diff --git a/src/YouTube/Chat/YouTube.php b/src/YouTube/Chat.php similarity index 96% rename from src/YouTube/Chat/YouTube.php rename to src/YouTube/Chat.php index ea69f4e..b3dec92 100644 --- a/src/YouTube/Chat/YouTube.php +++ b/src/YouTube/Chat.php @@ -2,9 +2,8 @@ declare(strict_types=1); -namespace App\YouTube\Chat; +namespace App\YouTube; -use App\YouTube\TranscriptFetcher; use PhpLlm\LlmChain\ChainInterface; use PhpLlm\LlmChain\Model\Message\Message; use PhpLlm\LlmChain\Model\Message\MessageBag; @@ -12,7 +11,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\RequestStack; -final class YouTube +final class Chat { private const SESSION_KEY = 'youtube-chat'; diff --git a/src/YouTube/Twig/YouTubeComponent.php b/src/YouTube/TwigComponent.php similarity index 92% rename from src/YouTube/Twig/YouTubeComponent.php rename to src/YouTube/TwigComponent.php index dd09894..01072c1 100644 --- a/src/YouTube/Twig/YouTubeComponent.php +++ b/src/YouTube/TwigComponent.php @@ -2,9 +2,8 @@ declare(strict_types=1); -namespace App\YouTube\Twig; +namespace App\YouTube; -use App\YouTube\Chat\YouTube; use PhpLlm\LlmChain\Model\Message\MessageBag; use Psr\Log\LoggerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; @@ -15,12 +14,12 @@ use function Symfony\Component\String\u; #[AsLiveComponent('youtube')] -final class YouTubeComponent +final class TwigComponent { use DefaultActionTrait; public function __construct( - private readonly YouTube $youTube, + private readonly Chat $youTube, private readonly LoggerInterface $logger, ) { } diff --git a/templates/components/_message.html.twig b/templates/_message.html.twig similarity index 94% rename from templates/components/_message.html.twig rename to templates/_message.html.twig index 4583ade..46c3cc8 100644 --- a/templates/components/_message.html.twig +++ b/templates/_message.html.twig @@ -11,7 +11,7 @@