GPT na Slacku

Tak, tak, wiem, że są gotowe aplikacje dodające GPT do Slacka. Tylko, że to nie dla mnie. Dodanie GPT do slacka to początek moich planów, a nie osiągnięcie jakiegoś celu. Poza tym w procesie uczenia czasem warto zrobić coś samemu, a co było już zrobione. Przynajmniej mamy pewność, że się da i możemy porównać wypracowane rozwiązania, a przede wszystkim zdobyć zrozumienie i wiedzę w trakcie procesu twórczego.

Szerszym planem jest stworzenie asystenta, który integruje i automatyzuje różne usługi i procesy. Ma być on krojony na moje / mojego zespołu potrzeby.

Krok 1. Połączenie ze Slackiem

Rozwiązanie buduję na Symfony. W pierwszej kolejności tworzę appkę na slacku i chcę umożliwić bezpośrednią komunikację z botem (przez wiadomość bezpośrednią). Konfiguruję aplikację wg dokumentacji. Mogę już wysyłać wiadomości do mojego asystenta, ale on jeszcze nie jest w stanie mi odpowiedzieć:

Aby umożliwić Iris komunikację ze mną konieczne jest skonfigurowanie subskrypcji na event message.im. W pierwszej kolejności dodaję do Symfony przydatny bundle do kreacji obiektów i od razu tworzę controller:

% composer require --dev symfony/maker-bundle
% bin/console make:controller

 Choose a name for your controller class (e.g. OrangeElephantController):
 > SlackEventSubscriber

 created: src/Controller/SlackEventSubscriberController.php

           
  Success! 
           

 Next: Open your new controller class and add some pages!

Aby można było ten controller zarejestrować do odbierania eventów ze Slacka musi on właściwie odpowiadać na challange wg dokumentacji. Najpierw tworzę UnitTest, aby mieć pewność, że controller, który dostarczę będzie spełniał założenia z dokumentacji:

public function testIndex()
{
    // Arrange
    $challengeCode = '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P';
    $request = $this->createMock(Request::class);
    $request->method('getContent')
        ->willReturn(
            '{"token":"Jhj5dZrVaK7ZwHHjRyZWjbDl","challenge":"' . $challengeCode . '","type":"url_verification"}'
        );

    $containerMock = $this->createMock(ContainerInterface::class);
    $object = new SlackEventSubscriberController();
    $object->setContainer($containerMock);

    // Act
    $response = $object->index($request);

    // Assert
    $this->assertInstanceOf(JsonResponse::class, $response);
    $this->assertEquals(200, $response->getStatusCode());
    $this->assertEquals(
        '{"challenge":"' . $challengeCode . '"}',
        $response->getContent()
    );
}

Sam controller jest oczywiście bardzo prosty (przynajmniej na razie):

#[Route('/slack/event/subscriber', name: 'app_slack_event_subscriber')]
public function index(Request $request): JsonResponse
{
    $content = json_decode($request->getContent());

    return $this->json(
        [
            'challenge' => $content->challenge
        ]
    );
}

Wrzucam aplikację na serwer i adres controllera ustawiam jako URL do nasłuchiwania na eventy. Endpoint prawidłowo się zarejestrował. Na koniec ustawień po stronie aplikacji slack konfiguruję subskrypcję na event message.im

Krok 2. Echo

Zanim podłączę GPT pod asystenta chcę upewnić się, że potrafię odbierać i wysyłać wiadomości w konwersacji. W tym celu mój asystent będzie realizował funkcję echo, wysyłając dokładnie ten sam tekst, który do niego przyszedł. Dokumentacja Slacka dokładnie wskazuje jakiego obiektu na wejściu należy się spodziewać.

Aby ułatwić sobie zadanie wykorzystuję paczkę Botman razem z driverem do Slacka.

% composer require botman/botman
% composer require botman/driver-slack

Do mojego controllera dodaję sobie taki fragment (na razie w celach testowych, cała logika rozpoznawania wiadomości z pewnością będzie wydzielona na zewnątrz tej klasy)

DriverManager::loadDriver(\BotMan\Drivers\Slack\SlackDriver::class);
$config = [
    'slack' => [
        'token' => '...' // tymczasowo tutaj
    ]
];
$botman = BotManFactory::create($config);

$botman->fallback(function (BotMan $bot): void {
    $bot->reply($bot->getMessage()->getText());
});
$botman->listen();

W ramach testów wysyłam więc wiadomość do Iris i oto jest odpowiedź:

Nie jest to oczywiście rozmowa, ale mam już pewność, że jest połączenie i mogę przejść do sedna, czyli podpięcia GPT-4.

Krok 3. GPT-4

Pozostała wisienka na torcie. Dodajmy naszej Iris nieco inteligencji. Akcja botmana będzie wyglądać teraz tak:

$botman->fallback(function (BotMan $bot): void {
    $client = OpenAI::client(
        "sk-IM...."
    );
    $response = $client->chat()->create([
        model' => 'gpt-4-1106-preview',
        'messages' => [
            ['role' => 'system', 'content' => 'Your name is Iris. You are a helpful assistant. Your favorite band is AC/DC'],
            ['role' => 'user', 'content' => $bot->getMessage()->getText()],
        ],
    ]);
    $bot->reply($response->choices[0]->message->content);
});

I voilà, Iris potrafi odpowiadać na pytania.

Na koniec wprowadziłem jeszcze zabezpieczenie, aby obecnie jedynie z mojego konta możliwa była rozmowa z Iris. Każde zapytanie do modelu generuje koszty, muszę zachować kontrolę nad moimi wydatkami. Poza tym nie podłączyłem tutaj moderation API i chciałbym uniknąć banicji w OpenAI

Standardowo, jest to dopiero początek, zaledwie POC. Iris ma obecnie pamięć złotej rybki, ponieważ nie otrzymuje do swojego kontekstu historii rozmowy, więc każda kolejna wiadomość generowana jest „na świeżo”. Daleko jeszcze temu rozwiązaniu do zastąpienia okienka ChatGPT. Zaleta polega na tym, że to teraz ja jako twórca będę miał pełną kontrolę nad tym jakie dane Iris otrzyma oraz jakie możliwości będzie posiadać. Jakieś sugestie względem funkcji które warto dodać w pierwszej kolejności?