Skip to content

Commit

Permalink
feat: chatty ui
Browse files Browse the repository at this point in the history
Signed-off-by: Anupam Kumar <[email protected]>
  • Loading branch information
kyteinsky committed May 9, 2024
1 parent d7f3572 commit c21090f
Show file tree
Hide file tree
Showing 20 changed files with 2,108 additions and 32 deletions.
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Known providers:
* [OpenAi/LocalAI integration](https://apps.nextcloud.com/apps/integration_openai)
* [Local Whisper Speech-To-Text](https://apps.nextcloud.com/apps/stt_whisper)
]]> </description>
<version>1.0.9</version>
<version>1.1.0</version>
<licence>agpl</licence>
<author>Julien Veyssier</author>
<namespace>Assistant</namespace>
Expand Down
11 changes: 11 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@
['name' => 'SpeechToText#getResultPage', 'url' => '/stt/result-page/{metaTaskId}', 'verb' => 'GET'],

['name' => 'preview#getFileImage', 'url' => '/preview', 'verb' => 'GET'],

['name' => 'chattyLLM#newSession', 'url' => '/chat/new_session', 'verb' => 'PUT'],
['name' => 'chattyLLM#updateSession', 'url' => '/chat/update_session', 'verb' => 'PATCH'],
['name' => 'chattyLLM#deleteSession', 'url' => '/chat/delete_session', 'verb' => 'DELETE'],
['name' => 'chattyLLM#getSessions', 'url' => '/chat/sessions', 'verb' => 'GET'],
['name' => 'chattyLLM#newMessage', 'url' => '/chat/new_message', 'verb' => 'PUT'],
['name' => 'chattyLLM#deleteMessage', 'url' => '/chat/delete_message', 'verb' => 'DELETE'],
['name' => 'chattyLLM#getMessages', 'url' => '/chat/messages', 'verb' => 'GET'],
['name' => 'chattyLLM#generate', 'url' => '/chat/generate', 'verb' => 'GET'],
['name' => 'chattyLLM#regenerate', 'url' => '/chat/regenerate', 'verb' => 'GET'],
['name' => 'chattyLLM#generateTitle', 'url' => '/chat/generate_title', 'verb' => 'GET'],
],
'ocs' => [
['name' => 'assistantApi#getAvailableTaskTypes', 'url' => '/api/{apiVersion}/task-types', 'verb' => 'GET', 'requirements' => $requirements],
Expand Down
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class Application extends App implements IBootstrap {
public const TASK_CATEGORY_TEXT_TO_IMAGE = 1;
public const TASK_CATEGORY_SPEECH_TO_TEXT = 2;

public const CHAT_USER_INSTRUCTIONS = 'This is a conversation between {user} and you, Nextcloud Assistant. You are a kind, polite and helpful AI that helps {user} to the best of its abilities. If you do not understand something, you will ask for clarification. Remember to verify the premise presented {user}. In a rare case, it might be wrong.';
public const CHAT_USER_INSTRUCTIONS_TITLE = 'Above is a chat session between {user} and you, Nextcloud Assitant. A suitable title summarizing the conversation could be "';

public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
}
Expand Down
362 changes: 362 additions & 0 deletions lib/Controller/ChattyLLMController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
<?php

namespace OCA\Assistant\Controller;

use OCA\Assistant\AppInfo\Application;
use OCA\Assistant\Db\ChattyLLM\Message;
use OCA\Assistant\Db\ChattyLLM\MessageMapper;
use OCA\Assistant\Db\ChattyLLM\Session;
use OCA\Assistant\Db\ChattyLLM\SessionMapper;
use OCA\Assistant\Service\AssistantService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TextProcessing\Task as TextProcessingTask;
use OCP\TextProcessing\IManager as ITextProcessingManager;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;

#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class ChattyLLMController extends Controller {

public function __construct(
string $appName,
IRequest $request,
private SessionMapper $sessionMapper,
private MessageMapper $messageMapper,
private IL10N $l10n,
private LoggerInterface $logger,
private AssistantService $assistantService,
private ITextProcessingManager $textProcessingManager,
private IConfig $config,
private IUserManager $userManager,
private ?string $userId,
) {
parent::__construct($appName, $request);
}

/**
* @param string $content
* @param int $timestamp
* @param ?string $title
* @return JSONResponse
*/
#[NoAdminRequired]
public function newSession(string $content, int $timestamp, ?string $title = null): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

$user = $this->userManager->get($this->userId);
if ($user === null) {
return new JSONResponse(['error' => $this->l10n->t('User not found')], Http::STATUS_UNAUTHORIZED);
}

$userInstructions = $this->config->getAppValue(
Application::APP_ID,
'chat_user_instructions',
$this->l10n->t(Application::CHAT_USER_INSTRUCTIONS),
);
$userInstructions = str_replace('{user}', $user->getDisplayName(), $userInstructions);

try {
$session = new Session();
$session->setUserId($this->userId);
$session->setTitle($title);

Check failure on line 71 in lib/Controller/ChattyLLMController.php

View workflow job for this annotation

GitHub Actions / Psalm check

InvalidArgument

lib/Controller/ChattyLLMController.php:71:23: InvalidArgument: Argument 1 of setTitle expects int, but null|string provided (see https://psalm.dev/004)
$session->setTimestamp($timestamp);
$this->sessionMapper->insert($session);

$systemMsg = new Message();
$systemMsg->setSessionId($session->getId());
$systemMsg->setRole($this->l10n->t('system'));
$systemMsg->setContent($userInstructions);
$systemMsg->setTimestamp($session->getTimestamp());
$this->messageMapper->insert($systemMsg);

$humanMsg = new Message();
$humanMsg->setSessionId($session->getId());
$humanMsg->setRole($this->l10n->t('human'));
$humanMsg->setContent($content);
$humanMsg->setTimestamp($session->getTimestamp());
$this->messageMapper->insert($humanMsg);

return new JSONResponse($session);
} catch (\OCP\DB\Exception | \RuntimeException $e) {
$this->logger->warning('Failed to create a chat session', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to create a chat session')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param integer $sessionId
* @param string $title
* @return JSONResponse
*/
#[NoAdminRequired]
public function updateSession(int $sessionId, string $title): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$this->sessionMapper->updateSession($sessionId, $title);
return new JSONResponse();
} catch (\OCP\DB\Exception | \RuntimeException $e) {
$this->logger->warning('Failed to update the chat session', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to update the chat session')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param integer $sessionId
* @return JSONResponse
*/
#[NoAdminRequired]
public function deleteSession(int $sessionId): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$this->sessionMapper->deleteSession($sessionId);
$this->messageMapper->deleteMessagesBySession($sessionId);
return new JSONResponse();
} catch (\OCP\DB\Exception | \RuntimeException $e) {
$this->logger->warning('Failed to delete the chat session', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to delete the chat session')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @return JSONResponse
*/
#[NoAdminRequired]
public function getSessions(): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$sessions = $this->sessionMapper->getUserSessions($this->userId);
return new JSONResponse($sessions);
} catch (\OCP\DB\Exception $e) {
$this->logger->warning('Failed to get chat sessions', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to get chat sessions')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param int $sessionId
* @param string $role
* @param string $content
* @param int $timestamp
* @return JSONResponse
*/
#[NoAdminRequired]
public function newMessage(int $sessionId, string $role, string $content, int $timestamp): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$sessionExists = $this->sessionMapper->exists($sessionId);
if (!$sessionExists) {
return new JSONResponse(['error' => $this->l10n->t('Session not found')], Http::STATUS_NOT_FOUND);
}

$message = new Message();
$message->setSessionId($sessionId);
$message->setRole($role);
$message->setContent($content);
$message->setTimestamp($timestamp);
$this->messageMapper->insert($message);
return new JSONResponse();
} catch (\OCP\DB\Exception $e) {
$this->logger->warning('Failed to add a chat message', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to add a chat message')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param int $sessionId
* @param int $limit
* @param int $cursor
* @return JSONResponse
*/
#[NoAdminRequired]
public function getMessages(int $sessionId, int $limit = 20, int $cursor = 0): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$sessionExists = $this->sessionMapper->exists($sessionId);
if (!$sessionExists) {
return new JSONResponse(['error' => $this->l10n->t('Session not found')], Http::STATUS_NOT_FOUND);
}

$messages = $this->messageMapper->getMessages($sessionId, $cursor, $limit);
if ($messages[0]->getRole() === $this->l10n->t('system')) {
array_shift($messages);
}

return new JSONResponse($messages);
} catch (\OCP\DB\Exception $e) {
$this->logger->warning('Failed to get chat messages', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to get chat messages')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param integer $messageId
* @return JSONResponse
*/
#[NoAdminRequired]
public function deleteMessage(int $messageId): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$this->messageMapper->deleteMessageById($messageId);
return new JSONResponse();
} catch (\OCP\DB\Exception | \RuntimeException $e) {
$this->logger->warning('Failed to delete a chat message', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to delete a chat message')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param integer $sessionId
* @return JSONResponse
*/
#[NoAdminRequired]
public function generate(int $sessionId): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$sessionExists = $this->sessionMapper->exists($sessionId);
if (!$sessionExists) {
return new JSONResponse(['error' => $this->l10n->t('Session not found')], Http::STATUS_NOT_FOUND);
}

$stichedPrompt =
$this->getStichedMessages($sessionId)
. PHP_EOL
. $this->l10n->t('assistant') . ': ';

$result = $this->queryLLM($stichedPrompt);

$message = new Message();
$message->setSessionId($sessionId);
$message->setRole($this->l10n->t('assistant'));
$message->setContent($result);
$message->setTimestamp(time());
$this->messageMapper->insert($message);
} catch (\OCP\DB\Exception $e) {
$this->logger->warning('Failed to add a chat message into DB', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to add a chat message into DB')], Http::STATUS_INTERNAL_SERVER_ERROR);
}

return new JSONResponse($message);
}

/**
* @param integer $sessionId
* @param integer $messageId
* @return JSONResponse
*/
#[NoAdminRequired]
public function regenerate(int $sessionId, int $messageId): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

try {
$sessionExists = $this->sessionMapper->exists($sessionId);
if (!$sessionExists) {
return new JSONResponse(['error' => $this->l10n->t('Session not found')], Http::STATUS_NOT_FOUND);
}

$this->messageMapper->deleteMessagesSinceId($sessionId, $messageId);
return $this->generate($sessionId);
} catch (\OCP\DB\Exception $e) {
$this->logger->warning('Failed to add a chat message into DB', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to add a chat message into DB')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* @param integer $sessionId
* @return JSONResponse
*/
#[NoAdminRequired]
public function generateTitle(int $sessionId): JSONResponse {
if ($this->userId === null) {
return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED);
}

$user = $this->userManager->get($this->userId);
if ($user === null) {
return new JSONResponse(['error' => $this->l10n->t('User not found')], Http::STATUS_UNAUTHORIZED);
}

try {
$sessionExists = $this->sessionMapper->exists($sessionId);
if (!$sessionExists) {
return new JSONResponse(['error' => $this->l10n->t('Session not found')], Http::STATUS_NOT_FOUND);
}

$userInstructions = $this->config->getAppValue(
Application::APP_ID,
'chat_user_instructions_title',
$this->l10n->t(Application::CHAT_USER_INSTRUCTIONS_TITLE),
);
$userInstructions = str_replace('{user}', $user->getDisplayName(), $userInstructions);

$stichedPrompt = $this->getStichedMessages($sessionId)
. PHP_EOL . PHP_EOL
. $userInstructions;

$result = $this->queryLLM($stichedPrompt);
$title = str_replace($userInstructions, '', $result);
$title = str_replace('"', '', $title);
$title = explode(PHP_EOL, $title)[0];
$title = trim($title);

$this->sessionMapper->updateSession($sessionId, $title);

return new JSONResponse(['result' => $title]);
} catch (\OCP\DB\Exception $e) {
$this->logger->warning('Failed to generate a title for the chat session', ['exception' => $e]);
return new JSONResponse(['error' => $this->l10n->t('Failed to generate a title for the chat session')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

private function getStichedMessages(int $sessionId): string {
$lastNMessages = intval($this->config->getAppValue(Application::APP_ID, 'chat_last_n_messages', 10));

Check failure on line 345 in lib/Controller/ChattyLLMController.php

View workflow job for this annotation

GitHub Actions / Psalm check

InvalidArgument

lib/Controller/ChattyLLMController.php:345:99: InvalidArgument: Argument 3 of OCP\IConfig::getAppValue expects string, but 10 provided (see https://psalm.dev/004)
$messages = $this->messageMapper->getMessages($sessionId, 0, $lastNMessages);

$stichedPrompt = implode(PHP_EOL, array_map(function ($message) {
if ($message->getRole() === $this->l10n->t('system')) {
return $message->getContent() . PHP_EOL;
}
return $message->getRole() . ': ' . $message->getContent();
}, $messages));

return $stichedPrompt;
}

private function queryLLM(string $content): string {
$task = new TextProcessingTask(FreePromptTaskType::class, $content, Application::APP_ID, $this->userId);
return trim($this->textProcessingManager->runTask($task));
}
}
Loading

0 comments on commit c21090f

Please sign in to comment.