Skip to content
Merged
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
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Known providers:

More details on how to set this up in the [admin docs](https://docs.nextcloud.com/server/latest/admin_manual/ai/index.html)
]]> </description>
<version>2.4.0</version>
<version>2.5.0</version>
<licence>agpl</licence>
<author>Julien Veyssier</author>
<namespace>Assistant</namespace>
Expand Down
8 changes: 8 additions & 0 deletions lib/Db/ChattyLLM/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
* @method \void setTimestamp(int $timestamp)
* @method \int getOcpTaskId()
* @method \void setOcpTaskId(int $ocpTaskId)
* @method \string getSources()
* @method \void setSources(string $sources)
*/
class Message extends Entity implements \JsonSerializable {
/** @var int */
Expand All @@ -35,6 +37,8 @@ class Message extends Entity implements \JsonSerializable {
protected $timestamp;
/** @var int */
protected $ocpTaskId;
/** @var string */
protected $sources;

public static $columns = [
'id',
Expand All @@ -43,6 +47,7 @@ class Message extends Entity implements \JsonSerializable {
'content',
'timestamp',
'ocp_task_id',
'sources',
];
public static $fields = [
'id',
Expand All @@ -51,6 +56,7 @@ class Message extends Entity implements \JsonSerializable {
'content',
'timestamp',
'ocpTaskId',
'sources',
];

public function __construct() {
Expand All @@ -59,6 +65,7 @@ public function __construct() {
$this->addType('content', Types::STRING);
$this->addType('timestamp', Types::INTEGER);
$this->addType('ocp_task_id', Types::INTEGER);
$this->addType('sources', Types::STRING);
}

#[\ReturnTypeWillChange]
Expand All @@ -70,6 +77,7 @@ public function jsonSerialize() {
'content' => $this->content,
'timestamp' => $this->timestamp,
'ocp_task_id' => $this->ocpTaskId,
'sources' => $this->sources,
];
}
}
3 changes: 3 additions & 0 deletions lib/Listener/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OCA\Assistant\Listener;

use OCA\Assistant\AppInfo\Application;
use OCA\Assistant\Service\AssistantService;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
Expand All @@ -34,6 +35,7 @@ public function __construct(
private IAppConfig $appConfig,
private IInitialState $initialStateService,
private IEventDispatcher $eventDispatcher,
private AssistantService $assistantService,
private ?string $userId,
) {
}
Expand Down Expand Up @@ -63,6 +65,7 @@ public function handle(Event $event): void {
$this->initialStateService->provideInitialState('last-target-language', $lastTargetLanguage);
$indexingComplete = $this->appConfig->getValueInt('context_chat', 'last_indexed_time', 0) !== 0;
$this->initialStateService->provideInitialState('contextChatIndexingComplete', $indexingComplete);
$this->initialStateService->provideInitialState('contextAgentToolSources', $this->assistantService->informationSources);
}
if (class_exists(\OCA\Viewer\Event\LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new \OCA\Viewer\Event\LoadViewer());
Expand Down
2 changes: 2 additions & 0 deletions lib/Listener/ChattyLLMTaskListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public function handle(Event $event): void {
$message->setRole('assistant');
$message->setContent(trim($task->getOutput()['output'] ?? ''));
$message->setTimestamp(time());
$sources = json_encode($task->getOutput()['sources'] ?? []);
$message->setSources($sources ? $sources : '[]');
try {
$this->messageMapper->insert($message);
} catch (\OCP\DB\Exception $e) {
Expand Down
43 changes: 43 additions & 0 deletions lib/Migration/Version020500Date20250425125359.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Assistant\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version020500Date20250425125359 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$schemaChanged = false;

if ($schema->hasTable('assistant_chat_msgs')) {
$table = $schema->getTable('assistant_chat_msgs');
if (!$table->hasColumn('sources')) {
$table->addColumn('sources', Types::TEXT, [
'notnull' => true,
'default' => '[]',
]);
$schemaChanged = true;
}
}

return $schemaChanged ? $schema : null;
}
}
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
* content: string,
* timestamp: int,
* ocp_task_id: int,
* sources: string,
* }
*
* @psalm-type AssistantChatAgencyMessage = AssistantChatMessage&array{
Expand Down
32 changes: 32 additions & 0 deletions lib/Service/AssistantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class AssistantService {
TextToTextTopics::ID => 10,
];

public array $informationSources;

public function __construct(
private ITaskProcessingManager $taskProcessingManager,
private TaskNotificationMapper $taskNotificationMapper,
Expand All @@ -86,6 +88,36 @@ public function __construct(
private IConfig $config,
private IShareManager $shareManager,
) {
$this->informationSources = [
'ask_context_chat' => $this->l10n->t('Context Chat'),
'transcribe_file' => $this->l10n->t('Assistant File Transcription'),
'generate_document' => $this->l10n->t('Assistant Document Generation'),
'list_calendars' => $this->l10n->t('Nextcloud Calendar'),
'schedule_event' => $this->l10n->t('Nextcloud Calendar'),
'find_free_time_slot_in_calendar' => $this->l10n->t('Nextcloud Calendar'),
'add_task' => $this->l10n->t('Nextcloud Tasks'),
'find_details_of_current_user' => $this->l10n->t('Nextcloud User Profile'),
'list_decks' => $this->l10n->t('Nextcloud Deck'),
'add_card' => $this->l10n->t('Nextcloud Deck'),
'get_coordinates_for_address' => $this->l10n->t('OpenStreetMap'),
'get_current_weather_for_coordinates' => $this->l10n->t('Norwegian Meteorological Institute Weather Forecast'),
'get_public_transport_route_for_coordinates,' => $this->l10n->t('HERE Public Transport API'),
'get_osm_route,' => $this->l10n->t('OpenStreetMap'),
'get_osm_link,' => $this->l10n->t('OpenStreetMap'),
'get_file_content' => $this->l10n->t('Nextcloud Files'),
'get_folder_tree' => $this->l10n->t('Nextcloud Files'),
'create_public_sharing_link' => $this->l10n->t('Nextcloud Files'),
'send_email' => $this->l10n->t('Nextcloud Mail'),
'get_mail_account_list' => $this->l10n->t('Nextcloud Mail'),
'list_projects,' => $this->l10n->t('OpenProject'),
'list_assignees,' => $this->l10n->t('OpenProject'),
'create_work_package' => $this->l10n->t('OpenProject'),
'list_talk_conversations' => $this->l10n->t('Nextcloud Talk'),
'create_public_conversation' => $this->l10n->t('Nextcloud Talk'),
'send_message_to_conversation' => $this->l10n->t('Nextcloud Talk'),
'list_messages_in_conversation' => $this->l10n->t('Nextcloud Talk'),
'duckduckgo_results_json' => $this->l10n->t('DuckDuckGo Web Search'),
];
}

/**
Expand Down
6 changes: 5 additions & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"role",
"content",
"timestamp",
"ocp_task_id"
"ocp_task_id",
"sources"
],
"properties": {
"id": {
Expand All @@ -73,6 +74,9 @@
"ocp_task_id": {
"type": "integer",
"format": "int64"
},
"sources": {
"type": "string"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/ChattyLLM/ChattyLLMInputForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
sessionIdToDelete: null,
chatContent: '',
sessions: null,
// [{ id: number, session_id: number, role: string, content: string, timestamp: number }]
// [{ id: number, session_id: number, role: string, content: string, timestamp: number, sources:string }]
messages: [], // null when failed to fetch
messagesAxiosController: null, // for request cancellation
allMessagesLoaded: false,
Expand Down Expand Up @@ -559,7 +559,7 @@
this.loading.initialMessages = false
this.messagesAxiosController = null
} catch (error) {
if (axios.isCancel(error)) {

Check warning on line 562 in src/components/ChattyLLM/ChattyLLMInputForm.vue

View workflow job for this annotation

GitHub Actions / NPM build

Caution: `axios` also has a named export `isCancel`. Check if you meant to write `import {isCancel} from '@nextcloud/axios'` instead

Check warning on line 562 in src/components/ChattyLLM/ChattyLLMInputForm.vue

View workflow job for this annotation

GitHub Actions / NPM build

Caution: `axios` also has a named export `isCancel`. Check if you meant to write `import {isCancel} from '@nextcloud/axios'` instead

Check warning on line 562 in src/components/ChattyLLM/ChattyLLMInputForm.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Caution: `axios` also has a named export `isCancel`. Check if you meant to write `import {isCancel} from '@nextcloud/axios'` instead
console.debug('fetchMessages cancelled')
return
}
Expand Down
6 changes: 5 additions & 1 deletion src/components/ChattyLLM/ConversationBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
:delete-loading="loading.messageDelete && message.id === deleteMessageId"
:regenerate-loading="loading.llmGeneration && message.id === regenerateFromId"
:new-message-loading="loading.newHumanMessage && idx === (messages.length - 1)"
:information-source-names="informationSourceNames"
@regenerate="regenerate(message.id)"
@delete="deleteMessage(message.id)" />
<LoadingPlaceholder v-if="loading.llmGeneration" />
Expand All @@ -44,6 +45,8 @@ import LoadingPlaceholder from './LoadingPlaceholder.vue'
import Message from './Message.vue'
import NoSession from './NoSession.vue'

import { loadState } from '@nextcloud/initial-state'

export default {
name: 'ConversationBox',

Expand All @@ -58,7 +61,7 @@ export default {
},

props: {
// [{ id: number, session_id: number, role: string, content: string, timestamp: number }]
// [{ id: number, session_id: number, role: string, content: string, timestamp: number, sources: string }]
messages: {
type: Array,
default: null,
Expand All @@ -82,6 +85,7 @@ export default {
return {
regenerateFromId: null,
deleteMessageId: null,
informationSourceNames: loadState('assistant', 'contextAgentToolSources'),
}
},

Expand Down
60 changes: 59 additions & 1 deletion src/components/ChattyLLM/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@
<div class="message__header__role__name">
{{ message.role === 'human' ? displayName : t('assistant', 'Nextcloud Assistant') }}
</div>
<div style="display: flex">
<NcPopover v-if="parsedSources.length">
<template #trigger>
<NcButton
:aria-label="t('assistant', 'Information sources')">
<template #icon>
<InformationBox :size="20" />
</template>
</NcButton>
</template>
<template #default>
<div class="toolinfo_popover_inner">
<h6> Information sources </h6>
<ul>
<li v-for="source in parsedSources" :key="source">
{{ source }}
</li>
</ul>
</div>
</template>
</NcPopover>
</div>
</div>
<NcDateTime class="message__header__timestamp" :timestamp="new Date((message?.timestamp ?? 0) * 1000)" :ignore-seconds="true" />
</div>
Expand All @@ -48,8 +70,12 @@ import AssistantIcon from '../icons/AssistantIcon.vue'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcPopover from '@nextcloud/vue/dist/Components/NcPopover.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import { NcRichText } from '@nextcloud/vue/dist/Components/NcRichText.js'

import InformationBox from 'vue-material-design-icons/InformationBox.vue'

import MessageActions from './MessageActions.vue'

import { getCurrentUser } from '@nextcloud/auth'
Expand All @@ -70,12 +96,15 @@ export default {
NcDateTime,
NcLoadingIcon,
NcRichText,
NcPopover,
NcButton,
InformationBox,

MessageActions,
},

props: {
// { id: number, session_id: number, role: string, content: string, timestamp: number }
// { id: number, session_id: number, role: string, content: string, timestamp: number, sources: string }
message: {
type: Object,
required: true,
Expand All @@ -96,6 +125,10 @@ export default {
type: Boolean,
default: false,
},
informationSourceNames: {
type: Array,
default: null,
},
},

data: () => {
Expand All @@ -111,6 +144,14 @@ export default {
}
},

computed: {
parsedSources() {
let parsedSources = JSON.parse(this.message.sources)
parsedSources = parsedSources.map((source) => this.getSourceString(source))
return [...new Set(parsedSources)]
},
},

mounted() {
this.fetch()
},
Expand Down Expand Up @@ -138,6 +179,9 @@ export default {
})
}
},
getSourceString(source) {
return this.informationSourceNames[source] ? this.informationSourceNames[source] : source
},
},
}
</script>
Expand All @@ -155,6 +199,7 @@ export default {
&__header {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;

Expand Down Expand Up @@ -188,3 +233,16 @@ export default {
}
}
</style>

<style lang="scss">
.toolinfo_popover_inner {
margin: 12px;
h6 {
margin: 2px;
}
ul {
list-style-type: disc;
padding-left: 18px;
}
}
</style>
Loading