10337f47fSAndreas Gohr<?php 20337f47fSAndreas Gohr 33379af09SAndreas Gohruse dokuwiki\Extension\CLIPlugin; 45e6dd16eSAndreas Gohruse dokuwiki\Extension\Plugin; 5e33a1d7aSAndreas Gohruse dokuwiki\plugin\aichat\AIChat; 6f6ef2e50SAndreas Gohruse dokuwiki\plugin\aichat\Chunk; 70337f47fSAndreas Gohruse dokuwiki\plugin\aichat\Embeddings; 8294a9eafSAndreas Gohruse dokuwiki\plugin\aichat\Model\ChatInterface; 9294a9eafSAndreas Gohruse dokuwiki\plugin\aichat\Model\EmbeddingInterface; 1001f06932SAndreas Gohruse dokuwiki\plugin\aichat\Storage\AbstractStorage; 110337f47fSAndreas Gohr 120337f47fSAndreas Gohr/** 130337f47fSAndreas Gohr * DokuWiki Plugin aichat (Helper Component) 140337f47fSAndreas Gohr * 150337f47fSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 160337f47fSAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 170337f47fSAndreas Gohr */ 187ebc7895Ssplitbrainclass helper_plugin_aichat extends Plugin 190337f47fSAndreas Gohr{ 203379af09SAndreas Gohr /** @var CLIPlugin $logger */ 213379af09SAndreas Gohr protected $logger; 22294a9eafSAndreas Gohr /** @var ChatInterface */ 236a18e0f4SAndreas Gohr protected $chatModel; 24*51aa8517SAndreas Gohr /** @var ChatInterface */ 25*51aa8517SAndreas Gohr protected $rephraseModel; 26294a9eafSAndreas Gohr /** @var EmbeddingInterface */ 276a18e0f4SAndreas Gohr protected $embedModel; 280337f47fSAndreas Gohr /** @var Embeddings */ 290337f47fSAndreas Gohr protected $embeddings; 3001f06932SAndreas Gohr /** @var AbstractStorage */ 3101f06932SAndreas Gohr protected $storage; 320337f47fSAndreas Gohr 33e75dc39fSAndreas Gohr /** @var array where to store meta data on the last run */ 34e75dc39fSAndreas Gohr protected $runDataFile; 35e75dc39fSAndreas Gohr 36*51aa8517SAndreas Gohr 370337f47fSAndreas Gohr /** 38f8d5ae01SAndreas Gohr * Constructor. Initializes vendor autoloader 39f8d5ae01SAndreas Gohr */ 40f8d5ae01SAndreas Gohr public function __construct() 41f8d5ae01SAndreas Gohr { 42e75dc39fSAndreas Gohr require_once __DIR__ . '/vendor/autoload.php'; // FIXME obsolete from Kaos onwards 43e75dc39fSAndreas Gohr global $conf; 44e75dc39fSAndreas Gohr $this->runDataFile = $conf['metadir'] . '/aichat__run.json'; 45d02b7935SAndreas Gohr $this->loadConfig(); 46f8d5ae01SAndreas Gohr } 47f8d5ae01SAndreas Gohr 48f8d5ae01SAndreas Gohr /** 493379af09SAndreas Gohr * Use the given CLI plugin for logging 503379af09SAndreas Gohr * 513379af09SAndreas Gohr * @param CLIPlugin $logger 523379af09SAndreas Gohr * @return void 533379af09SAndreas Gohr */ 548285fff9SAndreas Gohr public function setLogger($logger) 558285fff9SAndreas Gohr { 563379af09SAndreas Gohr $this->logger = $logger; 573379af09SAndreas Gohr } 583379af09SAndreas Gohr 593379af09SAndreas Gohr /** 60c4127b8eSAndreas Gohr * Check if the current user is allowed to use the plugin (if it has been restricted) 61c4127b8eSAndreas Gohr * 62c4127b8eSAndreas Gohr * @return bool 63c4127b8eSAndreas Gohr */ 64c4127b8eSAndreas Gohr public function userMayAccess() 65c4127b8eSAndreas Gohr { 66c4127b8eSAndreas Gohr global $auth; 67c4127b8eSAndreas Gohr global $USERINFO; 68c4127b8eSAndreas Gohr global $INPUT; 69c4127b8eSAndreas Gohr 70c4127b8eSAndreas Gohr if (!$auth) return true; 71c4127b8eSAndreas Gohr if (!$this->getConf('restrict')) return true; 72c4127b8eSAndreas Gohr if (!isset($USERINFO)) return false; 73c4127b8eSAndreas Gohr 74c4127b8eSAndreas Gohr return auth_isMember($this->getConf('restrict'), $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']); 75c4127b8eSAndreas Gohr } 76c4127b8eSAndreas Gohr 77c4127b8eSAndreas Gohr /** 786a18e0f4SAndreas Gohr * Access the Chat Model 790337f47fSAndreas Gohr * 80294a9eafSAndreas Gohr * @return ChatInterface 810337f47fSAndreas Gohr */ 826a18e0f4SAndreas Gohr public function getChatModel() 830337f47fSAndreas Gohr { 84294a9eafSAndreas Gohr if ($this->chatModel instanceof ChatInterface) { 856a18e0f4SAndreas Gohr return $this->chatModel; 866a18e0f4SAndreas Gohr } 876a18e0f4SAndreas Gohr 88dce0dee5SAndreas Gohr [$namespace, $name] = sexplode(' ', $this->getConf('chatmodel'), 2); 89dce0dee5SAndreas Gohr $class = '\\dokuwiki\\plugin\\aichat\\Model\\' . $namespace . '\\ChatModel'; 90d02b7935SAndreas Gohr 919f6b34c4SAndreas Gohr if (!class_exists($class)) { 92dce0dee5SAndreas Gohr throw new \RuntimeException('No ChatModel found for ' . $namespace); 939f6b34c4SAndreas Gohr } 94d02b7935SAndreas Gohr 95dce0dee5SAndreas Gohr $this->chatModel = new $class($name, $this->conf); 966a18e0f4SAndreas Gohr return $this->chatModel; 979f6b34c4SAndreas Gohr } 989f6b34c4SAndreas Gohr 996a18e0f4SAndreas Gohr /** 100*51aa8517SAndreas Gohr * @return ChatInterface 101*51aa8517SAndreas Gohr */ 102*51aa8517SAndreas Gohr public function getRephraseModel() 103*51aa8517SAndreas Gohr { 104*51aa8517SAndreas Gohr if ($this->rephraseModel instanceof ChatInterface) { 105*51aa8517SAndreas Gohr return $this->rephraseModel; 106*51aa8517SAndreas Gohr } 107*51aa8517SAndreas Gohr 108*51aa8517SAndreas Gohr [$namespace, $name] = sexplode(' ', $this->getConf('rephrasemodel'), 2); 109*51aa8517SAndreas Gohr $class = '\\dokuwiki\\plugin\\aichat\\Model\\' . $namespace . '\\ChatModel'; 110*51aa8517SAndreas Gohr 111*51aa8517SAndreas Gohr if (!class_exists($class)) { 112*51aa8517SAndreas Gohr throw new \RuntimeException('No ChatModel found for ' . $namespace); 113*51aa8517SAndreas Gohr } 114*51aa8517SAndreas Gohr 115*51aa8517SAndreas Gohr $this->rephraseModel = new $class($name, $this->conf); 116*51aa8517SAndreas Gohr return $this->rephraseModel; 117*51aa8517SAndreas Gohr } 118*51aa8517SAndreas Gohr 119*51aa8517SAndreas Gohr /** 1206a18e0f4SAndreas Gohr * Access the Embedding Model 1216a18e0f4SAndreas Gohr * 122294a9eafSAndreas Gohr * @return EmbeddingInterface 1236a18e0f4SAndreas Gohr */ 1246a18e0f4SAndreas Gohr public function getEmbedModel() 1256a18e0f4SAndreas Gohr { 126294a9eafSAndreas Gohr if ($this->embedModel instanceof EmbeddingInterface) { 1276a18e0f4SAndreas Gohr return $this->embedModel; 1280337f47fSAndreas Gohr } 1290337f47fSAndreas Gohr 130dce0dee5SAndreas Gohr [$namespace, $name] = sexplode(' ', $this->getConf('embedmodel'), 2); 131dce0dee5SAndreas Gohr $class = '\\dokuwiki\\plugin\\aichat\\Model\\' . $namespace . '\\EmbeddingModel'; 1326a18e0f4SAndreas Gohr 133dce0dee5SAndreas Gohr if (!class_exists($class)) { 134dce0dee5SAndreas Gohr throw new \RuntimeException('No EmbeddingModel found for ' . $namespace); 135dce0dee5SAndreas Gohr } 136dce0dee5SAndreas Gohr 137dce0dee5SAndreas Gohr $this->embedModel = new $class($name, $this->conf); 1386a18e0f4SAndreas Gohr return $this->embedModel; 1396a18e0f4SAndreas Gohr } 1406a18e0f4SAndreas Gohr 1416a18e0f4SAndreas Gohr 1420337f47fSAndreas Gohr /** 1430337f47fSAndreas Gohr * Access the Embeddings interface 1440337f47fSAndreas Gohr * 1450337f47fSAndreas Gohr * @return Embeddings 1460337f47fSAndreas Gohr */ 1470337f47fSAndreas Gohr public function getEmbeddings() 1480337f47fSAndreas Gohr { 1496a18e0f4SAndreas Gohr if ($this->embeddings instanceof Embeddings) { 1506a18e0f4SAndreas Gohr return $this->embeddings; 1516a18e0f4SAndreas Gohr } 1526a18e0f4SAndreas Gohr 15334a1c478SAndreas Gohr $this->embeddings = new Embeddings( 15434a1c478SAndreas Gohr $this->getChatModel(), 15534a1c478SAndreas Gohr $this->getEmbedModel(), 15634a1c478SAndreas Gohr $this->getStorage(), 15734a1c478SAndreas Gohr $this->conf 15834a1c478SAndreas Gohr ); 1593379af09SAndreas Gohr if ($this->logger) { 1603379af09SAndreas Gohr $this->embeddings->setLogger($this->logger); 1613379af09SAndreas Gohr } 1629f6b34c4SAndreas Gohr 1630337f47fSAndreas Gohr return $this->embeddings; 1640337f47fSAndreas Gohr } 1650337f47fSAndreas Gohr 1660337f47fSAndreas Gohr /** 16701f06932SAndreas Gohr * Access the Storage interface 16801f06932SAndreas Gohr * 16901f06932SAndreas Gohr * @return AbstractStorage 17001f06932SAndreas Gohr */ 17101f06932SAndreas Gohr public function getStorage() 17201f06932SAndreas Gohr { 1736a18e0f4SAndreas Gohr if ($this->storage instanceof AbstractStorage) { 1746a18e0f4SAndreas Gohr return $this->storage; 1756a18e0f4SAndreas Gohr } 1766a18e0f4SAndreas Gohr 17704afb84fSAndreas Gohr $class = '\\dokuwiki\\plugin\\aichat\\Storage\\' . $this->getConf('storage') . 'Storage'; 17804afb84fSAndreas Gohr $this->storage = new $class($this->conf); 1798285fff9SAndreas Gohr 1803379af09SAndreas Gohr if ($this->logger) { 1813379af09SAndreas Gohr $this->storage->setLogger($this->logger); 1823379af09SAndreas Gohr } 18301f06932SAndreas Gohr 18401f06932SAndreas Gohr return $this->storage; 18501f06932SAndreas Gohr } 18601f06932SAndreas Gohr 18701f06932SAndreas Gohr /** 1880337f47fSAndreas Gohr * Ask a question with a chat history 1890337f47fSAndreas Gohr * 1900337f47fSAndreas Gohr * @param string $question 1910337f47fSAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 1920337f47fSAndreas Gohr * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 1930337f47fSAndreas Gohr * @throws Exception 1940337f47fSAndreas Gohr */ 1950337f47fSAndreas Gohr public function askChatQuestion($question, $history = []) 1960337f47fSAndreas Gohr { 197*51aa8517SAndreas Gohr if ($history && $this->getConf('rephraseHistory') > 0) { 1980337f47fSAndreas Gohr $standaloneQuestion = $this->rephraseChatQuestion($question, $history); 1990337f47fSAndreas Gohr } else { 2000337f47fSAndreas Gohr $standaloneQuestion = $question; 2010337f47fSAndreas Gohr } 20234a1c478SAndreas Gohr return $this->askQuestion($standaloneQuestion, $history); 2030337f47fSAndreas Gohr } 2040337f47fSAndreas Gohr 2050337f47fSAndreas Gohr /** 2060337f47fSAndreas Gohr * Ask a single standalone question 2070337f47fSAndreas Gohr * 2080337f47fSAndreas Gohr * @param string $question 20934a1c478SAndreas Gohr * @param array $history [user, ai] of the previous question 2100337f47fSAndreas Gohr * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 2110337f47fSAndreas Gohr * @throws Exception 2120337f47fSAndreas Gohr */ 21334a1c478SAndreas Gohr public function askQuestion($question, $history = []) 2140337f47fSAndreas Gohr { 215e33a1d7aSAndreas Gohr $similar = $this->getEmbeddings()->getSimilarChunks($question, $this->getLanguageLimit()); 2169e81bea7SAndreas Gohr if ($similar) { 217441edf84SAndreas Gohr $context = implode( 218441edf84SAndreas Gohr "\n", 219441edf84SAndreas Gohr array_map(static fn(Chunk $chunk) => "\n```\n" . $chunk->getText() . "\n```\n", $similar) 220441edf84SAndreas Gohr ); 221219268b1SAndreas Gohr $prompt = $this->getPrompt('question', [ 222219268b1SAndreas Gohr 'context' => $context, 223219268b1SAndreas Gohr ]); 2249e81bea7SAndreas Gohr } else { 22534a1c478SAndreas Gohr $prompt = $this->getPrompt('noanswer'); 22634a1c478SAndreas Gohr $history = []; 2279e81bea7SAndreas Gohr } 22868908844SAndreas Gohr 229*51aa8517SAndreas Gohr $messages = $this->prepareMessages( 230*51aa8517SAndreas Gohr $this->getChatModel(), $prompt, $question, $history, $this->getConf('chatHistory') 231*51aa8517SAndreas Gohr ); 2326a18e0f4SAndreas Gohr $answer = $this->getChatModel()->getAnswer($messages); 2330337f47fSAndreas Gohr 2340337f47fSAndreas Gohr return [ 2350337f47fSAndreas Gohr 'question' => $question, 2360337f47fSAndreas Gohr 'answer' => $answer, 2370337f47fSAndreas Gohr 'sources' => $similar, 2380337f47fSAndreas Gohr ]; 2390337f47fSAndreas Gohr } 2400337f47fSAndreas Gohr 2410337f47fSAndreas Gohr /** 2420337f47fSAndreas Gohr * Rephrase a question into a standalone question based on the chat history 2430337f47fSAndreas Gohr * 2440337f47fSAndreas Gohr * @param string $question The original user question 2450337f47fSAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 2460337f47fSAndreas Gohr * @return string The rephrased question 2470337f47fSAndreas Gohr * @throws Exception 2480337f47fSAndreas Gohr */ 2490337f47fSAndreas Gohr public function rephraseChatQuestion($question, $history) 2500337f47fSAndreas Gohr { 25134a1c478SAndreas Gohr $prompt = $this->getPrompt('rephrase'); 252*51aa8517SAndreas Gohr $messages = $this->prepareMessages( 253*51aa8517SAndreas Gohr $this->getRephraseModel(), $prompt, $question, $history, $this->getConf('rephraseHistory') 254*51aa8517SAndreas Gohr ); 255*51aa8517SAndreas Gohr return $this->getRephraseModel()->getAnswer($messages); 25634a1c478SAndreas Gohr } 25734a1c478SAndreas Gohr 25834a1c478SAndreas Gohr /** 25934a1c478SAndreas Gohr * Prepare the messages for the AI 26034a1c478SAndreas Gohr * 261*51aa8517SAndreas Gohr * @param ChatInterface $model The used model 26234a1c478SAndreas Gohr * @param string $prompt The fully prepared system prompt 26334a1c478SAndreas Gohr * @param string $question The user question 26434a1c478SAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 265*51aa8517SAndreas Gohr * @param int $historySize The maximum number of messages to use from the history 26634a1c478SAndreas Gohr * @return array An OpenAI compatible array of messages 26734a1c478SAndreas Gohr */ 268*51aa8517SAndreas Gohr protected function prepareMessages( 269*51aa8517SAndreas Gohr ChatInterface $model, string $prompt, string $question, array $history, int $historySize 270*51aa8517SAndreas Gohr ): array 27134a1c478SAndreas Gohr { 27234a1c478SAndreas Gohr // calculate the space for context 273*51aa8517SAndreas Gohr $remainingContext = $model->getMaxInputTokenLength(); 27434a1c478SAndreas Gohr $remainingContext -= $this->countTokens($prompt); 27534a1c478SAndreas Gohr $remainingContext -= $this->countTokens($question); 27634a1c478SAndreas Gohr $safetyMargin = $remainingContext * 0.05; // 5% safety margin 27734a1c478SAndreas Gohr $remainingContext -= $safetyMargin; 27834a1c478SAndreas Gohr // FIXME we may want to also have an upper limit for the history and not always use the full context 27934a1c478SAndreas Gohr 280*51aa8517SAndreas Gohr $messages = $this->historyMessages($history, $remainingContext, $historySize); 28134a1c478SAndreas Gohr $messages[] = [ 28234a1c478SAndreas Gohr 'role' => 'system', 28334a1c478SAndreas Gohr 'content' => $prompt 28434a1c478SAndreas Gohr ]; 28534a1c478SAndreas Gohr $messages[] = [ 28634a1c478SAndreas Gohr 'role' => 'user', 28734a1c478SAndreas Gohr 'content' => $question 28834a1c478SAndreas Gohr ]; 28934a1c478SAndreas Gohr return $messages; 29034a1c478SAndreas Gohr } 29134a1c478SAndreas Gohr 29234a1c478SAndreas Gohr /** 29334a1c478SAndreas Gohr * Create an array of OpenAI compatible messages from the given history 29434a1c478SAndreas Gohr * 29534a1c478SAndreas Gohr * Only as many messages are used as fit into the token limit 29634a1c478SAndreas Gohr * 29734a1c478SAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 298*51aa8517SAndreas Gohr * @param int $tokenLimit The maximum number of tokens to use 299*51aa8517SAndreas Gohr * @param int $sizeLimit The maximum number of messages to use 30034a1c478SAndreas Gohr * @return array 30134a1c478SAndreas Gohr */ 302*51aa8517SAndreas Gohr protected function historyMessages(array $history, int $tokenLimit, int $sizeLimit): array 30334a1c478SAndreas Gohr { 30434a1c478SAndreas Gohr $remainingContext = $tokenLimit; 30534a1c478SAndreas Gohr 30634a1c478SAndreas Gohr $messages = []; 3070337f47fSAndreas Gohr $history = array_reverse($history); 308*51aa8517SAndreas Gohr $history = array_slice($history, 0, $sizeLimit); 3090337f47fSAndreas Gohr foreach ($history as $row) { 31034a1c478SAndreas Gohr $length = $this->countTokens($row[0] . $row[1]); 31134a1c478SAndreas Gohr if ($length > $remainingContext) { 3120337f47fSAndreas Gohr break; 3130337f47fSAndreas Gohr } 31434a1c478SAndreas Gohr $remainingContext -= $length; 3150337f47fSAndreas Gohr 31634a1c478SAndreas Gohr $messages[] = [ 31734a1c478SAndreas Gohr 'role' => 'assistant', 31834a1c478SAndreas Gohr 'content' => $row[1] 31934a1c478SAndreas Gohr ]; 32034a1c478SAndreas Gohr $messages[] = [ 32134a1c478SAndreas Gohr 'role' => 'user', 32234a1c478SAndreas Gohr 'content' => $row[0] 32334a1c478SAndreas Gohr ]; 32434a1c478SAndreas Gohr } 32534a1c478SAndreas Gohr return array_reverse($messages); 3260337f47fSAndreas Gohr } 3270337f47fSAndreas Gohr 32834a1c478SAndreas Gohr /** 32934a1c478SAndreas Gohr * Get an aproximation of the token count for the given text 33034a1c478SAndreas Gohr * 33134a1c478SAndreas Gohr * @param $text 33234a1c478SAndreas Gohr * @return int 33334a1c478SAndreas Gohr */ 33434a1c478SAndreas Gohr protected function countTokens($text) 33534a1c478SAndreas Gohr { 33634a1c478SAndreas Gohr return count($this->getEmbeddings()->getTokenEncoder()->encode($text)); 3370337f47fSAndreas Gohr } 3380337f47fSAndreas Gohr 3390337f47fSAndreas Gohr /** 3400337f47fSAndreas Gohr * Load the given prompt template and fill in the variables 3410337f47fSAndreas Gohr * 3420337f47fSAndreas Gohr * @param string $type 3430337f47fSAndreas Gohr * @param string[] $vars 3440337f47fSAndreas Gohr * @return string 3450337f47fSAndreas Gohr */ 3460337f47fSAndreas Gohr protected function getPrompt($type, $vars = []) 3470337f47fSAndreas Gohr { 3480337f47fSAndreas Gohr $template = file_get_contents($this->localFN('prompt_' . $type)); 34934a1c478SAndreas Gohr $vars['language'] = $this->getLanguagePrompt(); 3500337f47fSAndreas Gohr 3517ebc7895Ssplitbrain $replace = []; 3520337f47fSAndreas Gohr foreach ($vars as $key => $val) { 3530337f47fSAndreas Gohr $replace['{{' . strtoupper($key) . '}}'] = $val; 3540337f47fSAndreas Gohr } 3550337f47fSAndreas Gohr 3560337f47fSAndreas Gohr return strtr($template, $replace); 3570337f47fSAndreas Gohr } 358219268b1SAndreas Gohr 359219268b1SAndreas Gohr /** 360219268b1SAndreas Gohr * Construct the prompt to define the answer language 361219268b1SAndreas Gohr * 362219268b1SAndreas Gohr * @return string 363219268b1SAndreas Gohr */ 364219268b1SAndreas Gohr protected function getLanguagePrompt() 365219268b1SAndreas Gohr { 366219268b1SAndreas Gohr global $conf; 367cfaf6b32SAndreas Gohr $isoLangnames = include(__DIR__ . '/lang/languages.php'); 368cfaf6b32SAndreas Gohr 369cfaf6b32SAndreas Gohr $currentLang = $isoLangnames[$conf['lang']] ?? 'English'; 370219268b1SAndreas Gohr 371e33a1d7aSAndreas Gohr if ($this->getConf('preferUIlanguage') > AIChat::LANG_AUTO_ALL) { 372219268b1SAndreas Gohr if (isset($isoLangnames[$conf['lang']])) { 373219268b1SAndreas Gohr $languagePrompt = 'Always answer in ' . $isoLangnames[$conf['lang']] . '.'; 374219268b1SAndreas Gohr return $languagePrompt; 375219268b1SAndreas Gohr } 376219268b1SAndreas Gohr } 377219268b1SAndreas Gohr 378cfaf6b32SAndreas Gohr $languagePrompt = 'Always answer in the user\'s language. ' . 379cfaf6b32SAndreas Gohr "If you are unsure about the language, speak $currentLang."; 380219268b1SAndreas Gohr return $languagePrompt; 381219268b1SAndreas Gohr } 382e33a1d7aSAndreas Gohr 383e33a1d7aSAndreas Gohr /** 384e33a1d7aSAndreas Gohr * Should sources be limited to current language? 385e33a1d7aSAndreas Gohr * 386e33a1d7aSAndreas Gohr * @return string The current language code or empty string 387e33a1d7aSAndreas Gohr */ 388e33a1d7aSAndreas Gohr public function getLanguageLimit() 389e33a1d7aSAndreas Gohr { 390e33a1d7aSAndreas Gohr if ($this->getConf('preferUIlanguage') >= AIChat::LANG_UI_LIMITED) { 391e33a1d7aSAndreas Gohr global $conf; 392e33a1d7aSAndreas Gohr return $conf['lang']; 393e33a1d7aSAndreas Gohr } else { 394e33a1d7aSAndreas Gohr return ''; 395e33a1d7aSAndreas Gohr } 396e33a1d7aSAndreas Gohr } 397e75dc39fSAndreas Gohr 398e75dc39fSAndreas Gohr /** 399e75dc39fSAndreas Gohr * Store info about the last run 400e75dc39fSAndreas Gohr * 401e75dc39fSAndreas Gohr * @param array $data 402e75dc39fSAndreas Gohr * @return void 403e75dc39fSAndreas Gohr */ 404e75dc39fSAndreas Gohr public function setRunData(array $data) 405e75dc39fSAndreas Gohr { 406e75dc39fSAndreas Gohr file_put_contents($this->runDataFile, json_encode($data, JSON_PRETTY_PRINT)); 407e75dc39fSAndreas Gohr } 408e75dc39fSAndreas Gohr 409e75dc39fSAndreas Gohr /** 410e75dc39fSAndreas Gohr * Get info about the last run 411e75dc39fSAndreas Gohr * 412e75dc39fSAndreas Gohr * @return array 413e75dc39fSAndreas Gohr */ 414e75dc39fSAndreas Gohr public function getRunData() 415e75dc39fSAndreas Gohr { 416e75dc39fSAndreas Gohr if (!file_exists($this->runDataFile)) { 417e75dc39fSAndreas Gohr return []; 418e75dc39fSAndreas Gohr } 419e75dc39fSAndreas Gohr return json_decode(file_get_contents($this->runDataFile), true); 420e75dc39fSAndreas Gohr } 4210337f47fSAndreas Gohr} 422