From f47932b0ecdbb9e7ac689c61103b2a9127da8c59 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 28 Feb 2024 20:31:58 +0530 Subject: [PATCH] add support for scoped context in query Signed-off-by: Anupam Kumar --- lib/AppInfo/Application.php | 2 + lib/Command/Prompt.php | 43 ++++++++- lib/Command/ScanFiles.php | 2 +- lib/Service/LangRopeService.php | 24 +++++ lib/TextProcessing/ContextChatProvider.php | 6 +- lib/TextProcessing/FreePromptProvider.php | 4 + .../ScopedContextChatProvider.php | 92 +++++++++++++++++++ .../ScopedContextChatTaskType.php | 52 +++++++++++ 8 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 lib/TextProcessing/ScopedContextChatProvider.php create mode 100644 lib/TextProcessing/ScopedContextChatTaskType.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 8eb17da..b547e86 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -13,6 +13,7 @@ use OCA\ContextChat\Listener\FileListener; use OCA\ContextChat\TextProcessing\ContextChatProvider; use OCA\ContextChat\TextProcessing\FreePromptProvider; +use OCA\ContextChat\TextProcessing\ScopedContextChatProvider; use OCP\App\Events\AppDisableEvent; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -72,6 +73,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(AppDisableEvent::class, AppDisableListener::class); $context->registerTextProcessingProvider(ContextChatProvider::class); $context->registerTextProcessingProvider(FreePromptProvider::class); + $context->registerTextProcessingProvider(ScopedContextChatProvider::class); } public function boot(IBootContext $context): void { diff --git a/lib/Command/Prompt.php b/lib/Command/Prompt.php index efdd4de..1d5da0f 100644 --- a/lib/Command/Prompt.php +++ b/lib/Command/Prompt.php @@ -12,7 +12,9 @@ namespace OCA\ContextChat\Command; +use OCA\ContextChat\Service\ScopeType; use OCA\ContextChat\TextProcessing\ContextChatTaskType; +use OCA\ContextChat\TextProcessing\ScopedContextChatTaskType; use OCP\TextProcessing\FreePromptTaskType; use OCP\TextProcessing\IManager; use OCP\TextProcessing\Task; @@ -43,16 +45,55 @@ protected function configure() { InputArgument::REQUIRED, 'The prompt' ) - ->addOption('no-context', null, InputOption::VALUE_NONE, 'Do not use context'); + ->addOption( + 'no-context', + null, + InputOption::VALUE_NONE, + 'Do not use context' + ) + ->addOption( + 'context-sources', + null, + InputOption::VALUE_REQUIRED, + 'Context sources to use', + ) + ->addOption( + 'context-providers', + null, + InputOption::VALUE_REQUIRED, + 'Context provider to use', + ); } protected function execute(InputInterface $input, OutputInterface $output) { $userId = $input->getArgument('uid'); $prompt = $input->getArgument('prompt'); $noContext = $input->getOption('no-context'); + $contextSources = $input->getOption('context-sources'); + $contextProviders = $input->getOption('context-providers'); + + if ($noContext && (!empty($contextSources) || !empty($contextProviders))) { + throw new \InvalidArgumentException('Cannot use --no-context with --context-sources or --context-provider'); + } + + if (!empty($contextSources) && !empty($contextProviders)) { + throw new \InvalidArgumentException('Cannot use --context-sources with --context-provider'); + } if ($noContext) { $task = new Task(FreePromptTaskType::class, $prompt, 'context_chat', $userId); + } elseif (!empty($contextSources)) { + $task = new Task(ScopedContextChatTaskType::class, json_encode([ + 'scopeType' => ScopeType::SOURCE, + 'scopeList' => explode(',', $contextSources), + 'prompt' => $prompt, + ]), 'context_chat', $userId); + } elseif (!empty($contextProviders)) { + $task = new Task(ScopedContextChatTaskType::class, json_encode([ + 'scopeType' => ScopeType::PROVIDER, + 'scopeList' => explode(',', $contextProviders), + 'prompt' => $prompt, + ]), 'context_chat', $userId); } else { $task = new Task(ContextChatTaskType::class, $prompt, 'context_chat', $userId); } diff --git a/lib/Command/ScanFiles.php b/lib/Command/ScanFiles.php index 7d458ac..7c6b6ed 100644 --- a/lib/Command/ScanFiles.php +++ b/lib/Command/ScanFiles.php @@ -36,7 +36,7 @@ protected function configure() { InputArgument::REQUIRED, 'The user ID to scan the storage of' ) - ->addOption('mimetype', 'm', InputOption::VALUE_OPTIONAL, 'The mime type filter'); + ->addOption('mimetype', 'm', InputOption::VALUE_REQUIRED, 'The mime type filter'); } protected function execute(InputInterface $input, OutputInterface $output) { diff --git a/lib/Service/LangRopeService.php b/lib/Service/LangRopeService.php index 50c81af..1f57d1e 100644 --- a/lib/Service/LangRopeService.php +++ b/lib/Service/LangRopeService.php @@ -27,6 +27,11 @@ use Psr\Log\LoggerInterface; use RuntimeException; +enum ScopeType: string { + case PROVIDER = 'provider'; + case SOURCE = 'source'; +} + class LangRopeService { public function __construct( private LoggerInterface $logger, @@ -187,6 +192,25 @@ public function query(string $userId, string $prompt, bool $useContext = true): return ['message' => $this->getWithPresentableSources($response['output'] ?? '', ...($response['sources'] ?? []))]; } + /** + * @param string $userId + * @param string $prompt + * @param ScopeType $scopeType + * @param array $scopeList + * @return array + */ + public function scopedQuery(string $userId, string $prompt, ScopeType $scopeType, array $scopeList): array { + $params = [ + 'query' => $prompt, + 'userId' => $userId, + 'scopeType' => $scopeType, + 'scopeList' => $scopeList, + ]; + + $response = $this->requestToExApp('/scopedQuery', 'POST', $params); + return ['message' => $this->getWithPresentableSources($response['output'] ?? '', ...($response['sources'] ?? []))]; + } + public function getWithPresentableSources(string $llmResponse, string ...$sourceRefs): string { if (count($sourceRefs) === 0) { return $llmResponse; diff --git a/lib/TextProcessing/ContextChatProvider.php b/lib/TextProcessing/ContextChatProvider.php index 641401a..13521ab 100644 --- a/lib/TextProcessing/ContextChatProvider.php +++ b/lib/TextProcessing/ContextChatProvider.php @@ -23,10 +23,14 @@ public function __construct( } public function getName(): string { - return $this->l10n->t('Nextcloud Assistant Context Chat provider'); + return $this->l10n->t('Nextcloud Assistant Context Chat Provider'); } public function process(string $prompt): string { + if ($this->userId === null) { + throw new \RuntimeException('User ID is required to process the prompt.'); + } + $response = $this->langRopeService->query($this->userId, $prompt); if (isset($response['error'])) { throw new \RuntimeException('No result in ContextChat response. ' . $response['error']); diff --git a/lib/TextProcessing/FreePromptProvider.php b/lib/TextProcessing/FreePromptProvider.php index b95ef8e..1edc490 100644 --- a/lib/TextProcessing/FreePromptProvider.php +++ b/lib/TextProcessing/FreePromptProvider.php @@ -28,6 +28,10 @@ public function getName(): string { } public function process(string $prompt): string { + if ($this->userId === null) { + throw new \RuntimeException('User ID is required to process the prompt.'); + } + $response = $this->langRopeService->query($this->userId, $prompt, false); if (isset($response['error'])) { throw new \RuntimeException('No result in ContextChat response. ' . $response['error']); diff --git a/lib/TextProcessing/ScopedContextChatProvider.php b/lib/TextProcessing/ScopedContextChatProvider.php new file mode 100644 index 0000000..60398ba --- /dev/null +++ b/lib/TextProcessing/ScopedContextChatProvider.php @@ -0,0 +1,92 @@ + + * @template-implements IProvider + */ +class ScopedContextChatProvider implements IProvider, IProviderWithUserId { + + private ?string $userId = null; + + public function __construct( + private LangRopeService $langRopeService, + private IL10N $l10n, + ) { + } + + public function getName(): string { + return $this->l10n->t('Nextcloud Assistant Scoped Context Chat Provider'); + } + + /** + * @param string $prompt JSON string with the following structure: + * { + * "scopeType": string, + * "scopeList": list[string], + * "prompt": string, + * } + * + * @return string + */ + public function process(string $prompt): string { + if ($this->userId === null) { + throw new \RuntimeException('User ID is required to process the prompt.'); + } + + try { + $parsedData = json_decode($prompt, true, flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE); + } catch (\JsonException $e) { + throw new \RuntimeException( + 'Invalid JSON string, expected { "scopeType": string, "scopeList": list[string], "prompt": string }', + intval($e->getCode()), $e, + ); + } + + if ( + !is_array($parsedData) + || !isset($parsedData['scopeType']) + || !is_string($parsedData['scopeType']) + || !isset($parsedData['scopeList']) + || !is_array($parsedData['scopeList']) + || !isset($parsedData['prompt']) + || !is_string($parsedData['prompt']) + ) { + throw new \RuntimeException('Invalid JSON string, expected { "scopeType": string, "scopeList": list[string], "prompt": string }'); + } + + $scopeTypeEnum = ScopeType::tryFrom($parsedData['scopeType']); + if ($scopeTypeEnum === null) { + throw new \RuntimeException('Invalid scope type: ' . $parsedData['scopeType']); + } + + $response = $this->langRopeService->scopedQuery( + $this->userId, + $parsedData['prompt'], + $scopeTypeEnum, + $parsedData['scopeList'], + ); + + if (isset($response['error'])) { + throw new \RuntimeException('No result in ContextChat response. ' . $response['error']); + } + + return $response['message'] ?? ''; + } + + public function getTaskType(): string { + return ScopedContextChatTaskType::class; + } + + public function setUserId(?string $userId): void { + $this->userId = $userId; + } +} diff --git a/lib/TextProcessing/ScopedContextChatTaskType.php b/lib/TextProcessing/ScopedContextChatTaskType.php new file mode 100644 index 0000000..fc204de --- /dev/null +++ b/lib/TextProcessing/ScopedContextChatTaskType.php @@ -0,0 +1,52 @@ + + * + * @author Julien Veyssier + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\ContextChat\TextProcessing; + +use OCP\IL10N; +use OCP\TextProcessing\ITaskType; + +class ScopedContextChatTaskType implements ITaskType { + public function __construct( + private IL10N $l, + ) { + } + + /** + * @inheritDoc + * @since 27.1.0 + */ + public function getName(): string { + return $this->l->t('Scoped Context Chat'); + } + + /** + * @inheritDoc + * @since 27.1.0 + */ + public function getDescription(): string { + return $this->l->t('Ask a question about the data selected by you.'); + } +}