Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TextProcessing\SummaryTaskType;
use OCP\TaskProcessing\TaskTypes\TextToText;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
use OCP\User\IAvailabilityCoordinator;
use Psr\Log\LoggerInterface;
use Throwable;
Expand Down Expand Up @@ -337,7 +337,7 @@ public function index(): TemplateResponse {

$this->initialStateService->provideInitialState(
'llm_summaries_available',
$this->aiIntegrationsService->isLlmProcessingEnabled() && $this->aiIntegrationsService->isLlmAvailable(SummaryTaskType::class)
$this->aiIntegrationsService->isLlmProcessingEnabled() && $this->aiIntegrationsService->isLlmAvailable(TextToTextSummary::ID)
);

$this->initialStateService->provideInitialState(
Expand All @@ -347,13 +347,13 @@ public function index(): TemplateResponse {

$this->initialStateService->provideInitialState(
'llm_freeprompt_available',
$this->aiIntegrationsService->isLlmProcessingEnabled() && $this->aiIntegrationsService->isLlmAvailable(FreePromptTaskType::class)
$this->aiIntegrationsService->isLlmProcessingEnabled() && $this->aiIntegrationsService->isLlmAvailable(TextToText::ID)
);

$this->initialStateService->provideInitialState(
'llm_followup_available',
$this->aiIntegrationsService->isLlmProcessingEnabled()
&& $this->aiIntegrationsService->isLlmAvailable(FreePromptTaskType::class)
&& $this->aiIntegrationsService->isLlmAvailable(TextToText::ID)
);

$this->initialStateService->provideInitialState(
Expand Down
4 changes: 2 additions & 2 deletions lib/Listener/FollowUpClassifierListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TaskProcessing\TaskTypes\TextToText;

/**
* @template-implements IEventListener<Event|NewMessagesSynchronized>
Expand Down Expand Up @@ -46,7 +46,7 @@ public function handle(Event $event): void {
return;
}

if (!$this->aiService->isLlmAvailable(FreePromptTaskType::class)) {
if (!$this->aiService->isLlmAvailable(TextToText::ID)) {
return;
}

Expand Down
143 changes: 47 additions & 96 deletions lib/Service/AiIntegrations/AiIntegrationsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,21 @@
use OCP\TaskProcessing\IManager as TaskProcessingManager;
use OCP\TaskProcessing\Task as TaskProcessingTask;
use OCP\TaskProcessing\TaskTypes\TextToText;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TextProcessing\IManager as TextProcessingManager;
use OCP\TextProcessing\SummaryTaskType;
use OCP\TextProcessing\Task as TextProcessingTask;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
use Psr\Log\LoggerInterface;

use function array_map;
use function implode;
use function in_array;
use function json_decode;

class AiIntegrationsService {

private const EVENT_DATA_PROMPT_PREAMBLE = <<<PROMPT
I am scheduling an event based on an email thread and need an event title and agenda. Provide the result as JSON with keys for "title" and "agenda". For example ```{ "title": "Project kick-off meeting", "agenda": "* Introduction\\n* Project goals\\n* Next steps" }```.

The email contents are:

PROMPT;

public function __construct(
private LoggerInterface $logger,
private Cache $cache,
private IMAPClientFactory $clientFactory,
private IMailManager $mailManager,
private TaskProcessingManager $taskProcessingManager,
private TextProcessingManager $textProcessingManager,
private IL10N $l,
private IFactory $l10nFactory,
private IUserManager $userManager,
Expand Down Expand Up @@ -101,12 +89,7 @@
}
// construct prompt and task
$messageBody = $message->getPlainBody();
$prompt = "You are tasked with formulating a helpful summary of a email message. \r\n"
. 'The summary should be in the language of this language code ' . $language . ". \r\n"
. "The summary should be less than 160 characters. \r\n"
. "Output *ONLY* the summary itself, leave out any introduction. \r\n"
. "Here is the ***E-MAIL*** for which you must generate a helpful summary: \r\n"
. "***START_OF_E-MAIL***\r\n$messageBody\r\n***END_OF_E-MAIL***\r\n";
$prompt = sprintf(DefaultPrompts::SUMMARIZE_MESSAGE, $language, $messageBody);
$task = new TaskProcessingTask(
TextToText::ID,
[
Expand Down Expand Up @@ -135,7 +118,7 @@
* @throws ServiceException
*/
public function summarizeThread(Account $account, string $threadId, array $messages, string $currentUserId): ?string {
if (in_array(SummaryTaskType::class, $this->textProcessingManager->getAvailableTaskTypes(), true)) {
if (isset($this->taskProcessingManager->getAvailableTaskTypes()[TextToTextSummary::ID])) {
$messageIds = array_map(fn ($message) => $message->getMessageId(), $messages);
$cachedSummary = $this->cache->getValue($this->cache->buildUrlKey($messageIds));
if ($cachedSummary) {
Expand All @@ -159,9 +142,16 @@
}

$taskPrompt = implode("\n", $messagesBodies);
$summaryTask = new TextProcessingTask(SummaryTaskType::class, $taskPrompt, 'mail', $currentUserId, $threadId);
$this->textProcessingManager->runTask($summaryTask);
$summary = $summaryTask->getOutput();
$summaryTask = new TaskProcessingTask(
TextToTextSummary::ID,
['input' => $taskPrompt],
Application::APP_ID,
$currentUserId,
$threadId,
);
$this->taskProcessingManager->runTask($summaryTask);
$output = $summaryTask->getOutput()['output'] ?? null;
$summary = $output !== null ? (string)$output : null;

Check failure on line 154 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:154:42: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 154 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:154:42: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 154 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:154:42: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 154 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:154:42: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

$this->cache->addValue($this->cache->buildUrlKey($messageIds), $summary);

Expand All @@ -175,7 +165,7 @@
* @param Message[] $messages
*/
public function generateEventData(Account $account, string $threadId, array $messages, string $currentUserId): ?EventData {
if (!in_array(FreePromptTaskType::class, $this->textProcessingManager->getAvailableTaskTypes(), true)) {
if (!isset($this->taskProcessingManager->getAvailableTaskTypes()[TextToText::ID])) {
return null;
}
$client = $this->clientFactory->getClient($account);
Expand All @@ -194,14 +184,15 @@
$client->logout();
}

$task = new TextProcessingTask(
FreePromptTaskType::class,
self::EVENT_DATA_PROMPT_PREAMBLE . implode("\n\n---\n\n", $messageBodies),
'mail',
$task = new TaskProcessingTask(
TextToText::ID,
['input' => DefaultPrompts::EVENT_DATA_PREAMBLE . implode("\n\n---\n\n", $messageBodies)],
Application::APP_ID,
$currentUserId,
"event_data_$threadId",
);
$result = $this->textProcessingManager->runTask($task);
$this->taskProcessingManager->runTask($task);
$result = (string)($task->getOutput()['output'] ?? '');

Check failure on line 195 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:195:22: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 195 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:195:22: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 195 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:195:22: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 195 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:195:22: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)
try {
$decoded = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
return new EventData($decoded['title'], $decoded['agenda']);
Expand All @@ -215,7 +206,7 @@
* @throws ServiceException
*/
public function getSmartReply(Account $account, Mailbox $mailbox, Message $message, string $currentUserId): ?array {
if (in_array(FreePromptTaskType::class, $this->textProcessingManager->getAvailableTaskTypes(), true)) {
if (isset($this->taskProcessingManager->getAvailableTaskTypes()[TextToText::ID])) {
$cachedReplies = $this->cache->getValue("smartReplies_{$message->getId()}");
if ($cachedReplies) {
try {
Expand All @@ -241,25 +232,17 @@
} finally {
$client->logout();
}
$prompt = "You are tasked with formulating helpful replies or reply templates to e-mails provided that have been sent to me. If you don't know some relevant information for answering the e-mails (like my schedule) leave blanks in the text that can later be filled by me. You must write the replies from my point of view as replies to the original sender of the provided e-mail!

Formulate two extremely succinct reply suggestions to the provided ***E-MAIL***. Please, do not invent any context for the replies but, rather, leave blanks for me to fill in with relevant information where necessary. Provide the output formatted as valid JSON with the keys 'reply1' and 'reply2' for the reply suggestions.

Each suggestion must be of 25 characters or less.

Here is the ***E-MAIL*** for which you must suggest the replies to:

***START_OF_E-MAIL***" . $messageBody . "

***END_OF_E-MAIL***

Please, output *ONLY* a valid JSON string with the keys 'reply1' and 'reply2' for the reply suggestions. Leave out any other text besides the JSON! Be extremely succinct and write the replies from my point of view.
";
$task = new TextProcessingTask(FreePromptTaskType::class, $prompt, 'mail,', $currentUserId);
$this->textProcessingManager->runTask($task);
$replies = $task->getOutput();
$prompt = sprintf(DefaultPrompts::SMART_REPLY, $messageBody);
$task = new TaskProcessingTask(TextToText::ID, ['input' => $prompt], Application::APP_ID, $currentUserId);
$task = $this->taskProcessingManager->runTask($task);
$replies = trim((string)($task->getOutput()['output'] ?? ''));

Check failure on line 238 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:238:29: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 238 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:238:29: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 238 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:238:29: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 238 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:238:29: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)
if ($replies === '') {
// The task can fail or return nothing (e.g. provider timeout); treat as no replies
$this->logger->warning('Smart reply task returned no output', ['status' => $task->getStatus(), 'errorMessage' => $task->getErrorMessage()]);
return [];
}
try {
$cleaned = preg_replace('/^```json\s*|\s*```$/', '', trim($replies));
$cleaned = preg_replace('/^```json\s*|\s*```$/', '', $replies);
$decoded = json_decode($cleaned, true, 512, JSON_THROW_ON_ERROR);
$this->cache->addValue("smartReplies_{$message->getId()}", $replies);
return $decoded;
Expand All @@ -282,7 +265,7 @@
Message $message,
string $currentUserId,
): bool {
if (!in_array(FreePromptTaskType::class, $this->textProcessingManager->getAvailableTaskTypes(), true)) {
if (!isset($this->taskProcessingManager->getAvailableTaskTypes()[TextToText::ID])) {
throw new ServiceException('No language model available for smart replies');
}

Expand All @@ -306,27 +289,13 @@
$messageBody = $imapMessage->getPlainBody();
$messageBody = str_replace('"', '\"', $messageBody);

$prompt = "Consider the following TypeScript function prototype:
---
/**
* This function takes in an email text and returns a boolean indicating whether the email author expects a response.
*
* @param emailText - string with the email text
* @returns boolean true if the email expects a reply, false if not
*/
declare function doesEmailExpectReply(emailText: string): Promise<boolean>;
---
Tell me what the function outputs for the following parameters.

emailText: \"$messageBody\"
The JSON output should be in the form: {\"expectsReply\": true}
Never return null or undefined.";
$task = new TextProcessingTask(FreePromptTaskType::class, $prompt, Application::APP_ID, $currentUserId);
$prompt = sprintf(DefaultPrompts::REQUIRES_FOLLOW_UP, $messageBody);
$task = new TaskProcessingTask(TextToText::ID, ['input' => $prompt], Application::APP_ID, $currentUserId);

$this->textProcessingManager->runTask($task);
$this->taskProcessingManager->runTask($task);

// Can't use json_decode() here because the output contains additional garbage
return preg_match('/{\s*"expectsReply"\s*:\s*true\s*}/i', $task->getOutput()) === 1;
return preg_match('/{\s*"expectsReply"\s*:\s*true\s*}/i', (string)($task->getOutput()['output'] ?? '')) === 1;

Check failure on line 298 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:298:70: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 298 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:298:70: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 298 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:298:70: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 298 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:298:70: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)
}

/**
Expand All @@ -340,7 +309,7 @@
Message $message,
string $currentUserId,
): ?bool {
if (!in_array(FreePromptTaskType::class, $this->textProcessingManager->getAvailableTaskTypes(), true)) {
if (!isset($this->taskProcessingManager->getAvailableTaskTypes()[TextToText::ID])) {
$this->logger->info('No language model available for checking translation needs');
return null;
}
Expand Down Expand Up @@ -372,34 +341,16 @@
$messageBody = $imapMessage->getPlainBody();
$messageBody = str_replace('"', '\"', $messageBody);

$prompt = "Consider the following TypeScript function prototype:
---
/**
* This function takes in an email text and returns a boolean indicating whether the email needs translation from a specific language.
*
* @param emailText - string with the email text
* @param language - the language code to check against (e.g., 'en', 'de', etc.)
* @returns boolean true if the email is written in a different language than the one specified and needs translation, false if it is written in the specified language.
* only return true if whole sentences are written in a different language, not just a word or two.
*/
declare function isEmailWrittenInLanguage(emailText: string, language: string): Promise<boolean>;
---
Tell me what the function outputs for the following parameters.

emailText: \"$messageBody\"
language: \"$language\"
The JSON output should be in the form: {\"needsTranslation\": true}
Never return null or undefined.";
$task = new TextProcessingTask(FreePromptTaskType::class, $prompt, Application::APP_ID, $currentUserId);

$this->textProcessingManager->runTask($task);
$output = $task->getOutput();
$prompt = sprintf(DefaultPrompts::REQUIRES_TRANSLATION, $messageBody, $language);
$task = new TaskProcessingTask(TextToText::ID, ['input' => $prompt], Application::APP_ID, $currentUserId);

$task = $this->taskProcessingManager->runTask($task);
$output = $task->getOutput()['output'] ?? null;
$output = $output !== null ? (string)$output : null;

Check failure on line 349 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:349:40: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 349 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:349:40: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 349 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:349:40: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)

Check failure on line 349 in lib/Service/AiIntegrations/AiIntegrationsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

PossiblyInvalidCast

lib/Service/AiIntegrations/AiIntegrationsService.php:349:40: PossiblyInvalidCast: list<numeric|string> cannot be cast to string (see https://psalm.dev/190)
if ($output === null) {
throw new ServiceException('Task output is null, possibly due to an error in the task processing', [
'messageId' => $message->getId(),
'language' => $language,
'output' => $output,
]);
// The task can fail or return nothing (e.g. provider timeout); can't determine, don't cache
$this->logger->warning('Translation check task returned no output', ['status' => $task->getStatus(), 'errorMessage' => $task->getErrorMessage()]);
return null;
}
// Can't use json_decode() here because the output contains additional garbage
$result = preg_match('/{\s*"needsTranslation"\s*:\s*true\s*}/i', $output) === 1;
Expand All @@ -408,7 +359,7 @@
}

public function isLlmAvailable(string $taskType): bool {
return in_array($taskType, $this->textProcessingManager->getAvailableTaskTypes(), true);
return array_key_exists($taskType, $this->taskProcessingManager->getAvailableTaskTypes());
}

public function isTaskAvailable(string $taskName): bool {
Expand Down
Loading
Loading