Files
nuqloud-ai/lib/Jobs/CleanupOrphanedSourcesJob.php
T
Pascal Kienast 0739d3da6a Initial open-source release of Talk AI
Talk AI is a multi-bot AI assistant manager for Nextcloud Talk:
per-bot prompts and models, agentic tool calling (MCP + built-in
tools), RAG over Nextcloud files, room-document search, vision and
speech-to-text attachments, persistent bot wikis, approval workflows,
rate limiting, and multi-provider LLM support (any OpenAI-compatible
endpoint).

Developed within EDUC - the European Digital UniverCity
(https://educalliance.eu), where it runs as the 'EDUC AI' assistant on
the alliance-wide Nextcloud portal. This public repository is the
upstream point of truth; deployment-specific tools plug in via the
tool-provider extension point (docs/TOOL_PROVIDERS.md).

License: AGPL-3.0-or-later.
2026-07-08 21:13:13 +02:00

149 lines
5.0 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\EducAI\Jobs;
use Exception;
use OCA\EducAI\Db\BotSource;
use OCA\EducAI\Db\BotSourceMapper;
use OCA\EducAI\Db\EmbeddingMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\Files\IRootFolder;
use Psr\Log\LoggerInterface;
/**
* Background job to clean up orphaned RAG sources.
*
* This job runs periodically to:
* 1. Detect sources pointing to deleted files/folders
* 2. Clean up their embeddings from the database
* 3. Mark them as errors so users can remove them
*
* Runs every 6 hours.
*/
class CleanupOrphanedSourcesJob extends TimedJob {
private BotSourceMapper $botSourceMapper;
private EmbeddingMapper $embeddingMapper;
private IRootFolder $rootFolder;
private LoggerInterface $logger;
public function __construct(
ITimeFactory $time,
BotSourceMapper $botSourceMapper,
EmbeddingMapper $embeddingMapper,
IRootFolder $rootFolder,
LoggerInterface $logger
) {
parent::__construct($time);
$this->botSourceMapper = $botSourceMapper;
$this->embeddingMapper = $embeddingMapper;
$this->rootFolder = $rootFolder;
$this->logger = $logger;
// Run every 6 hours
$this->setInterval(6 * 60 * 60);
}
protected function run($arguments): void {
$this->logger->info('EducAI: Starting orphaned RAG sources cleanup');
$cleanedCount = 0;
$errorCount = 0;
try {
$sources = $this->botSourceMapper->findAll();
foreach ($sources as $source) {
try {
$this->checkAndCleanSource($source);
} catch (Exception $e) {
$errorCount++;
$this->logger->warning('EducAI: Error checking source for cleanup', [
'sourceId' => $source->getId(),
'error' => $e->getMessage(),
]);
}
}
$this->logger->info('EducAI: Orphaned sources cleanup completed', [
'cleaned' => $cleanedCount,
'errors' => $errorCount,
]);
} catch (Exception $e) {
$this->logger->error('EducAI: Failed to run orphaned sources cleanup', [
'exception' => $e,
]);
}
}
private function checkAndCleanSource(BotSource $source): bool {
// Skip sources already marked as error with "no longer exists" message
if ($source->getStatus() === 'error' &&
str_contains($source->getErrorMessage() ?? '', 'no longer exists')) {
return false;
}
// Skip URL sources - they don't have a file to check
// URL sources have node_type = 'url' and node_id = 0
if ($source->getNodeType() === 'url') {
return false;
}
$ownerUid = $source->getOwnerUid();
if ($ownerUid === null || $ownerUid === '') {
return false;
}
try {
$userFolder = $this->rootFolder->getUserFolder($ownerUid);
$nodes = $userFolder->getById($source->getNodeId());
// If nodes array is empty or node doesn't exist, the file was deleted
if (!is_array($nodes) || count($nodes) === 0) {
$this->cleanupDeletedSource($source);
return true;
}
} catch (Exception $e) {
// User might not exist anymore, or other filesystem issues
// Mark as error but don't delete the source record
$this->logger->info('EducAI: Cannot access source, marking as orphaned', [
'sourceId' => $source->getId(),
'nodeId' => $source->getNodeId(),
'ownerUid' => $ownerUid,
'error' => $e->getMessage(),
]);
$this->cleanupDeletedSource($source);
return true;
}
return false;
}
private function cleanupDeletedSource(BotSource $source): void {
$embeddingCount = count($this->embeddingMapper->findBySource($source->getId()));
// Delete all embeddings for this source
$this->embeddingMapper->deleteBySource($source->getId());
// Update source status
$source->setStatus('error');
$source->setErrorMessage('File or folder no longer exists. ' . $embeddingCount . ' embedding(s) cleaned up. You can remove this source.');
$source->setChecksum(null);
$source->setUpdatedAt(time());
$source->setProgress(null);
$source->setProgressCurrent(null);
$source->setProgressTotal(null);
$source->setProgressStage(null);
$this->botSourceMapper->update($source);
$this->logger->info('EducAI: Cleaned up orphaned source', [
'sourceId' => $source->getId(),
'nodeId' => $source->getNodeId(),
'embeddingsDeleted' => $embeddingCount,
]);
}
}