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; 10c2b7a1f7SAndreas Gohruse dokuwiki\plugin\aichat\ModelFactory; 1101f06932SAndreas Gohruse dokuwiki\plugin\aichat\Storage\AbstractStorage; 120337f47fSAndreas Gohr 130337f47fSAndreas Gohr/** 140337f47fSAndreas Gohr * DokuWiki Plugin aichat (Helper Component) 150337f47fSAndreas Gohr * 160337f47fSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 170337f47fSAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 180337f47fSAndreas Gohr */ 197ebc7895Ssplitbrainclass helper_plugin_aichat extends Plugin 200337f47fSAndreas Gohr{ 21c2b7a1f7SAndreas Gohr /** @var ModelFactory */ 22c2b7a1f7SAndreas Gohr public $factory; 23c2b7a1f7SAndreas Gohr 243379af09SAndreas Gohr /** @var CLIPlugin $logger */ 253379af09SAndreas Gohr protected $logger; 26c2b7a1f7SAndreas Gohr 270337f47fSAndreas Gohr /** @var Embeddings */ 280337f47fSAndreas Gohr protected $embeddings; 2901f06932SAndreas Gohr /** @var AbstractStorage */ 3001f06932SAndreas Gohr protected $storage; 310337f47fSAndreas Gohr 32e75dc39fSAndreas Gohr /** @var array where to store meta data on the last run */ 33e75dc39fSAndreas Gohr protected $runDataFile; 34e75dc39fSAndreas Gohr 3551aa8517SAndreas Gohr 360337f47fSAndreas Gohr /** 37f8d5ae01SAndreas Gohr * Constructor. Initializes vendor autoloader 38f8d5ae01SAndreas Gohr */ 39f8d5ae01SAndreas Gohr public function __construct() 40f8d5ae01SAndreas Gohr { 41e75dc39fSAndreas Gohr require_once __DIR__ . '/vendor/autoload.php'; // FIXME obsolete from Kaos onwards 42e75dc39fSAndreas Gohr global $conf; 43e75dc39fSAndreas Gohr $this->runDataFile = $conf['metadir'] . '/aichat__run.json'; 44d02b7935SAndreas Gohr $this->loadConfig(); 45c2b7a1f7SAndreas Gohr $this->factory = new ModelFactory($this->conf); 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 /** 600de7e020SAndreas Gohr * Update the configuration 610de7e020SAndreas Gohr * 620de7e020SAndreas Gohr * @param array $config 630de7e020SAndreas Gohr * @return void 640de7e020SAndreas Gohr */ 650de7e020SAndreas Gohr public function updateConfig(array $config) 660de7e020SAndreas Gohr { 670de7e020SAndreas Gohr $this->conf = array_merge($this->conf, $config); 680de7e020SAndreas Gohr $this->factory->updateConfig($config); 690de7e020SAndreas Gohr } 700de7e020SAndreas Gohr 710de7e020SAndreas Gohr /** 72c4127b8eSAndreas Gohr * Check if the current user is allowed to use the plugin (if it has been restricted) 73c4127b8eSAndreas Gohr * 74c4127b8eSAndreas Gohr * @return bool 75c4127b8eSAndreas Gohr */ 76c4127b8eSAndreas Gohr public function userMayAccess() 77c4127b8eSAndreas Gohr { 78c4127b8eSAndreas Gohr global $auth; 79c4127b8eSAndreas Gohr global $USERINFO; 80c4127b8eSAndreas Gohr global $INPUT; 81c4127b8eSAndreas Gohr 82c4127b8eSAndreas Gohr if (!$auth) return true; 83c4127b8eSAndreas Gohr if (!$this->getConf('restrict')) return true; 84c4127b8eSAndreas Gohr if (!isset($USERINFO)) return false; 85c4127b8eSAndreas Gohr 86c4127b8eSAndreas Gohr return auth_isMember($this->getConf('restrict'), $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']); 87c4127b8eSAndreas Gohr } 88c4127b8eSAndreas Gohr 89c4127b8eSAndreas Gohr /** 906a18e0f4SAndreas Gohr * Access the Chat Model 910337f47fSAndreas Gohr * 92294a9eafSAndreas Gohr * @return ChatInterface 930337f47fSAndreas Gohr */ 946a18e0f4SAndreas Gohr public function getChatModel() 950337f47fSAndreas Gohr { 96c2b7a1f7SAndreas Gohr return $this->factory->getChatModel(); 979f6b34c4SAndreas Gohr } 989f6b34c4SAndreas Gohr 996a18e0f4SAndreas Gohr /** 10051aa8517SAndreas Gohr * @return ChatInterface 10151aa8517SAndreas Gohr */ 10251aa8517SAndreas Gohr public function getRephraseModel() 10351aa8517SAndreas Gohr { 104c2b7a1f7SAndreas Gohr return $this->factory->getRephraseModel(); 10551aa8517SAndreas Gohr } 10651aa8517SAndreas Gohr 10751aa8517SAndreas Gohr /** 1086a18e0f4SAndreas Gohr * Access the Embedding Model 1096a18e0f4SAndreas Gohr * 110294a9eafSAndreas Gohr * @return EmbeddingInterface 1116a18e0f4SAndreas Gohr */ 112c2b7a1f7SAndreas Gohr public function getEmbeddingModel() 1136a18e0f4SAndreas Gohr { 114c2b7a1f7SAndreas Gohr return $this->factory->getEmbeddingModel(); 1150337f47fSAndreas Gohr } 1160337f47fSAndreas Gohr 1170337f47fSAndreas Gohr /** 1180337f47fSAndreas Gohr * Access the Embeddings interface 1190337f47fSAndreas Gohr * 1200337f47fSAndreas Gohr * @return Embeddings 1210337f47fSAndreas Gohr */ 1220337f47fSAndreas Gohr public function getEmbeddings() 1230337f47fSAndreas Gohr { 1246a18e0f4SAndreas Gohr if ($this->embeddings instanceof Embeddings) { 1256a18e0f4SAndreas Gohr return $this->embeddings; 1266a18e0f4SAndreas Gohr } 1276a18e0f4SAndreas Gohr 12834a1c478SAndreas Gohr $this->embeddings = new Embeddings( 12934a1c478SAndreas Gohr $this->getChatModel(), 130c2b7a1f7SAndreas Gohr $this->getEmbeddingModel(), 13134a1c478SAndreas Gohr $this->getStorage(), 13234a1c478SAndreas Gohr $this->conf 13334a1c478SAndreas Gohr ); 1343379af09SAndreas Gohr if ($this->logger) { 1353379af09SAndreas Gohr $this->embeddings->setLogger($this->logger); 1363379af09SAndreas Gohr } 1379f6b34c4SAndreas Gohr 1380337f47fSAndreas Gohr return $this->embeddings; 1390337f47fSAndreas Gohr } 1400337f47fSAndreas Gohr 1410337f47fSAndreas Gohr /** 14201f06932SAndreas Gohr * Access the Storage interface 14301f06932SAndreas Gohr * 14401f06932SAndreas Gohr * @return AbstractStorage 14501f06932SAndreas Gohr */ 14601f06932SAndreas Gohr public function getStorage() 14701f06932SAndreas Gohr { 1486a18e0f4SAndreas Gohr if ($this->storage instanceof AbstractStorage) { 1496a18e0f4SAndreas Gohr return $this->storage; 1506a18e0f4SAndreas Gohr } 1516a18e0f4SAndreas Gohr 15204afb84fSAndreas Gohr $class = '\\dokuwiki\\plugin\\aichat\\Storage\\' . $this->getConf('storage') . 'Storage'; 15304afb84fSAndreas Gohr $this->storage = new $class($this->conf); 1548285fff9SAndreas Gohr 1553379af09SAndreas Gohr if ($this->logger) { 1563379af09SAndreas Gohr $this->storage->setLogger($this->logger); 1573379af09SAndreas Gohr } 15801f06932SAndreas Gohr 15901f06932SAndreas Gohr return $this->storage; 16001f06932SAndreas Gohr } 16101f06932SAndreas Gohr 16201f06932SAndreas Gohr /** 1630337f47fSAndreas Gohr * Ask a question with a chat history 1640337f47fSAndreas Gohr * 1650337f47fSAndreas Gohr * @param string $question 1660337f47fSAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 1670337f47fSAndreas Gohr * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 1680337f47fSAndreas Gohr * @throws Exception 1690337f47fSAndreas Gohr */ 170ed47fd87SAndreas Gohr public function askChatQuestion($question, $history = [], $sourcePage = '') 1710337f47fSAndreas Gohr { 17251aa8517SAndreas Gohr if ($history && $this->getConf('rephraseHistory') > 0) { 17387090e4bSAndreas Gohr $contextQuestion = $this->rephraseChatQuestion($question, $history); 17487090e4bSAndreas Gohr 17587090e4bSAndreas Gohr // Only use the rephrased question if it has more history than the chat history provides 17687090e4bSAndreas Gohr if ($this->getConf('rephraseHistory') > $this->getConf('chatHistory')) { 17787090e4bSAndreas Gohr $question = $contextQuestion; 1780337f47fSAndreas Gohr } 17987090e4bSAndreas Gohr } else { 18087090e4bSAndreas Gohr $contextQuestion = $question; 18187090e4bSAndreas Gohr } 182ed47fd87SAndreas Gohr return $this->askQuestion($question, $history, $contextQuestion, $sourcePage); 1830337f47fSAndreas Gohr } 1840337f47fSAndreas Gohr 1850337f47fSAndreas Gohr /** 1860337f47fSAndreas Gohr * Ask a single standalone question 1870337f47fSAndreas Gohr * 18887090e4bSAndreas Gohr * @param string $question The question to ask 18934a1c478SAndreas Gohr * @param array $history [user, ai] of the previous question 19087090e4bSAndreas Gohr * @param string $contextQuestion The question to use for context search 191ed47fd87SAndreas Gohr * @param string $sourcePage The page the question was asked on 1920337f47fSAndreas Gohr * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 1930337f47fSAndreas Gohr * @throws Exception 1940337f47fSAndreas Gohr */ 195ed47fd87SAndreas Gohr public function askQuestion($question, $history = [], $contextQuestion = '', $sourcePage = '') 1960337f47fSAndreas Gohr { 197ed47fd87SAndreas Gohr if($sourcePage) { 198ed47fd87SAndreas Gohr $similar = $this->getEmbeddings()->getPageChunks($sourcePage); 199ed47fd87SAndreas Gohr } else { 20087090e4bSAndreas Gohr $similar = $this->getEmbeddings()->getSimilarChunks($contextQuestion ?: $question, $this->getLanguageLimit()); 201ed47fd87SAndreas Gohr } 202ed47fd87SAndreas Gohr 2039e81bea7SAndreas Gohr if ($similar) { 204441edf84SAndreas Gohr $context = implode( 205441edf84SAndreas Gohr "\n", 206441edf84SAndreas Gohr array_map(static fn(Chunk $chunk) => "\n```\n" . $chunk->getText() . "\n```\n", $similar) 207441edf84SAndreas Gohr ); 208219268b1SAndreas Gohr $prompt = $this->getPrompt('question', [ 209219268b1SAndreas Gohr 'context' => $context, 21059a2a267SAndreas Gohr 'question' => $question, 211666b8ea7SAndreas Gohr 'customprompt' => $this->getConf('customprompt'), 212219268b1SAndreas Gohr ]); 2139e81bea7SAndreas Gohr } else { 21459a2a267SAndreas Gohr $prompt = $this->getPrompt('noanswer', [ 21559a2a267SAndreas Gohr 'question' => $question, 21659a2a267SAndreas Gohr ]); 21734a1c478SAndreas Gohr $history = []; 2189e81bea7SAndreas Gohr } 21968908844SAndreas Gohr 22051aa8517SAndreas Gohr $messages = $this->prepareMessages( 2212071dcedSAndreas Gohr $this->getChatModel(), 2222071dcedSAndreas Gohr $prompt, 2232071dcedSAndreas Gohr $history, 2242071dcedSAndreas Gohr $this->getConf('chatHistory') 22551aa8517SAndreas Gohr ); 2266a18e0f4SAndreas Gohr $answer = $this->getChatModel()->getAnswer($messages); 2270337f47fSAndreas Gohr 2280337f47fSAndreas Gohr return [ 2290337f47fSAndreas Gohr 'question' => $question, 23087090e4bSAndreas Gohr 'contextQuestion' => $contextQuestion, 2310337f47fSAndreas Gohr 'answer' => $answer, 2320337f47fSAndreas Gohr 'sources' => $similar, 2330337f47fSAndreas Gohr ]; 2340337f47fSAndreas Gohr } 2350337f47fSAndreas Gohr 2360337f47fSAndreas Gohr /** 2370337f47fSAndreas Gohr * Rephrase a question into a standalone question based on the chat history 2380337f47fSAndreas Gohr * 2390337f47fSAndreas Gohr * @param string $question The original user question 2400337f47fSAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 2410337f47fSAndreas Gohr * @return string The rephrased question 2420337f47fSAndreas Gohr * @throws Exception 2430337f47fSAndreas Gohr */ 2440337f47fSAndreas Gohr public function rephraseChatQuestion($question, $history) 2450337f47fSAndreas Gohr { 24659a2a267SAndreas Gohr $prompt = $this->getPrompt('rephrase', [ 24759a2a267SAndreas Gohr 'question' => $question, 24859a2a267SAndreas Gohr ]); 24951aa8517SAndreas Gohr $messages = $this->prepareMessages( 2502071dcedSAndreas Gohr $this->getRephraseModel(), 2512071dcedSAndreas Gohr $prompt, 2522071dcedSAndreas Gohr $history, 2532071dcedSAndreas Gohr $this->getConf('rephraseHistory') 25451aa8517SAndreas Gohr ); 25551aa8517SAndreas Gohr return $this->getRephraseModel()->getAnswer($messages); 25634a1c478SAndreas Gohr } 25734a1c478SAndreas Gohr 25834a1c478SAndreas Gohr /** 25934a1c478SAndreas Gohr * Prepare the messages for the AI 26034a1c478SAndreas Gohr * 26151aa8517SAndreas Gohr * @param ChatInterface $model The used model 26259a2a267SAndreas Gohr * @param string $promptedQuestion The user question embedded in a prompt 26334a1c478SAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 26451aa8517SAndreas Gohr * @param int $historySize The maximum number of messages to use from the history 26534a1c478SAndreas Gohr * @return array An OpenAI compatible array of messages 26634a1c478SAndreas Gohr */ 26751aa8517SAndreas Gohr protected function prepareMessages( 2682071dcedSAndreas Gohr ChatInterface $model, 2692071dcedSAndreas Gohr string $promptedQuestion, 2702071dcedSAndreas Gohr array $history, 2712071dcedSAndreas Gohr int $historySize 2728c08cb3fSAndreas Gohr ): array { 27334a1c478SAndreas Gohr // calculate the space for context 274*7be8078eSAndreas Gohr $remainingContext = $model->getMaxInputTokenLength(); // might be 0 27559a2a267SAndreas Gohr $remainingContext -= $this->countTokens($promptedQuestion); 276*7be8078eSAndreas Gohr $safetyMargin = abs($remainingContext) * 0.05; // 5% safety margin 277*7be8078eSAndreas Gohr $remainingContext -= $safetyMargin; // may be negative, it will be ignored then 27834a1c478SAndreas Gohr 27951aa8517SAndreas Gohr $messages = $this->historyMessages($history, $remainingContext, $historySize); 28034a1c478SAndreas Gohr $messages[] = [ 28134a1c478SAndreas Gohr 'role' => 'user', 28259a2a267SAndreas Gohr 'content' => $promptedQuestion 28334a1c478SAndreas Gohr ]; 28434a1c478SAndreas Gohr return $messages; 28534a1c478SAndreas Gohr } 28634a1c478SAndreas Gohr 28734a1c478SAndreas Gohr /** 28834a1c478SAndreas Gohr * Create an array of OpenAI compatible messages from the given history 28934a1c478SAndreas Gohr * 29034a1c478SAndreas Gohr * Only as many messages are used as fit into the token limit 29134a1c478SAndreas Gohr * 29234a1c478SAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 293*7be8078eSAndreas Gohr * @param int $tokenLimit The maximum number of tokens to use, negative limit disables this check 29451aa8517SAndreas Gohr * @param int $sizeLimit The maximum number of messages to use 29534a1c478SAndreas Gohr * @return array 29634a1c478SAndreas Gohr */ 29751aa8517SAndreas Gohr protected function historyMessages(array $history, int $tokenLimit, int $sizeLimit): array 29834a1c478SAndreas Gohr { 29934a1c478SAndreas Gohr $remainingContext = $tokenLimit; 30034a1c478SAndreas Gohr 30134a1c478SAndreas Gohr $messages = []; 3020337f47fSAndreas Gohr $history = array_reverse($history); 30351aa8517SAndreas Gohr $history = array_slice($history, 0, $sizeLimit); 3040337f47fSAndreas Gohr foreach ($history as $row) { 30534a1c478SAndreas Gohr $length = $this->countTokens($row[0] . $row[1]); 306*7be8078eSAndreas Gohr 307*7be8078eSAndreas Gohr if ($tokenLimit > 0 && $length > $remainingContext) { 3080337f47fSAndreas Gohr break; 3090337f47fSAndreas Gohr } 31034a1c478SAndreas Gohr $remainingContext -= $length; 3110337f47fSAndreas Gohr 31234a1c478SAndreas Gohr $messages[] = [ 31334a1c478SAndreas Gohr 'role' => 'assistant', 31434a1c478SAndreas Gohr 'content' => $row[1] 31534a1c478SAndreas Gohr ]; 31634a1c478SAndreas Gohr $messages[] = [ 31734a1c478SAndreas Gohr 'role' => 'user', 31834a1c478SAndreas Gohr 'content' => $row[0] 31934a1c478SAndreas Gohr ]; 32034a1c478SAndreas Gohr } 32134a1c478SAndreas Gohr return array_reverse($messages); 3220337f47fSAndreas Gohr } 3230337f47fSAndreas Gohr 32434a1c478SAndreas Gohr /** 32534a1c478SAndreas Gohr * Get an aproximation of the token count for the given text 32634a1c478SAndreas Gohr * 32734a1c478SAndreas Gohr * @param $text 32834a1c478SAndreas Gohr * @return int 32934a1c478SAndreas Gohr */ 33034a1c478SAndreas Gohr protected function countTokens($text) 33134a1c478SAndreas Gohr { 33234a1c478SAndreas Gohr return count($this->getEmbeddings()->getTokenEncoder()->encode($text)); 3330337f47fSAndreas Gohr } 3340337f47fSAndreas Gohr 3350337f47fSAndreas Gohr /** 3360337f47fSAndreas Gohr * Load the given prompt template and fill in the variables 3370337f47fSAndreas Gohr * 3380337f47fSAndreas Gohr * @param string $type 3390337f47fSAndreas Gohr * @param string[] $vars 3400337f47fSAndreas Gohr * @return string 3410337f47fSAndreas Gohr */ 3420337f47fSAndreas Gohr protected function getPrompt($type, $vars = []) 3430337f47fSAndreas Gohr { 34459a2a267SAndreas Gohr $template = file_get_contents($this->localFN($type, 'prompt')); 34534a1c478SAndreas Gohr $vars['language'] = $this->getLanguagePrompt(); 3460337f47fSAndreas Gohr 3477ebc7895Ssplitbrain $replace = []; 3480337f47fSAndreas Gohr foreach ($vars as $key => $val) { 3490337f47fSAndreas Gohr $replace['{{' . strtoupper($key) . '}}'] = $val; 3500337f47fSAndreas Gohr } 3510337f47fSAndreas Gohr 3520337f47fSAndreas Gohr return strtr($template, $replace); 3530337f47fSAndreas Gohr } 354219268b1SAndreas Gohr 355219268b1SAndreas Gohr /** 356219268b1SAndreas Gohr * Construct the prompt to define the answer language 357219268b1SAndreas Gohr * 358219268b1SAndreas Gohr * @return string 359219268b1SAndreas Gohr */ 360219268b1SAndreas Gohr protected function getLanguagePrompt() 361219268b1SAndreas Gohr { 362219268b1SAndreas Gohr global $conf; 363cfaf6b32SAndreas Gohr $isoLangnames = include(__DIR__ . '/lang/languages.php'); 364cfaf6b32SAndreas Gohr 365cfaf6b32SAndreas Gohr $currentLang = $isoLangnames[$conf['lang']] ?? 'English'; 366219268b1SAndreas Gohr 367e33a1d7aSAndreas Gohr if ($this->getConf('preferUIlanguage') > AIChat::LANG_AUTO_ALL) { 368219268b1SAndreas Gohr if (isset($isoLangnames[$conf['lang']])) { 369219268b1SAndreas Gohr $languagePrompt = 'Always answer in ' . $isoLangnames[$conf['lang']] . '.'; 370219268b1SAndreas Gohr return $languagePrompt; 371219268b1SAndreas Gohr } 372219268b1SAndreas Gohr } 373219268b1SAndreas Gohr 374cfaf6b32SAndreas Gohr $languagePrompt = 'Always answer in the user\'s language. ' . 375cfaf6b32SAndreas Gohr "If you are unsure about the language, speak $currentLang."; 376219268b1SAndreas Gohr return $languagePrompt; 377219268b1SAndreas Gohr } 378e33a1d7aSAndreas Gohr 379e33a1d7aSAndreas Gohr /** 380e33a1d7aSAndreas Gohr * Should sources be limited to current language? 381e33a1d7aSAndreas Gohr * 382e33a1d7aSAndreas Gohr * @return string The current language code or empty string 383e33a1d7aSAndreas Gohr */ 384e33a1d7aSAndreas Gohr public function getLanguageLimit() 385e33a1d7aSAndreas Gohr { 386e33a1d7aSAndreas Gohr if ($this->getConf('preferUIlanguage') >= AIChat::LANG_UI_LIMITED) { 387e33a1d7aSAndreas Gohr global $conf; 388e33a1d7aSAndreas Gohr return $conf['lang']; 389e33a1d7aSAndreas Gohr } else { 390e33a1d7aSAndreas Gohr return ''; 391e33a1d7aSAndreas Gohr } 392e33a1d7aSAndreas Gohr } 393e75dc39fSAndreas Gohr 394e75dc39fSAndreas Gohr /** 395e75dc39fSAndreas Gohr * Store info about the last run 396e75dc39fSAndreas Gohr * 397e75dc39fSAndreas Gohr * @param array $data 398e75dc39fSAndreas Gohr * @return void 399e75dc39fSAndreas Gohr */ 400e75dc39fSAndreas Gohr public function setRunData(array $data) 401e75dc39fSAndreas Gohr { 402e75dc39fSAndreas Gohr file_put_contents($this->runDataFile, json_encode($data, JSON_PRETTY_PRINT)); 403e75dc39fSAndreas Gohr } 404e75dc39fSAndreas Gohr 405e75dc39fSAndreas Gohr /** 406e75dc39fSAndreas Gohr * Get info about the last run 407e75dc39fSAndreas Gohr * 408e75dc39fSAndreas Gohr * @return array 409e75dc39fSAndreas Gohr */ 410e75dc39fSAndreas Gohr public function getRunData() 411e75dc39fSAndreas Gohr { 412e75dc39fSAndreas Gohr if (!file_exists($this->runDataFile)) { 413e75dc39fSAndreas Gohr return []; 414e75dc39fSAndreas Gohr } 415e75dc39fSAndreas Gohr return json_decode(file_get_contents($this->runDataFile), true); 416e75dc39fSAndreas Gohr } 4170337f47fSAndreas Gohr} 418