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) { 198*9634d734SAndreas Gohr // only the current page is context 199ed47fd87SAndreas Gohr $similar = $this->getEmbeddings()->getPageChunks($sourcePage); 200ed47fd87SAndreas Gohr } else { 201*9634d734SAndreas Gohr if ($this->getConf('fullpagecontext')) { 202*9634d734SAndreas Gohr // match chunks but use full pages as context 203*9634d734SAndreas Gohr $similar = $this->getEmbeddings()->getSimilarPages( 204*9634d734SAndreas Gohr $contextQuestion ?: $question, 205*9634d734SAndreas Gohr $this->getLanguageLimit() 206*9634d734SAndreas Gohr ); 207*9634d734SAndreas Gohr } else { 208*9634d734SAndreas Gohr // use the chunks as context 209*9634d734SAndreas Gohr $similar = $this->getEmbeddings()->getSimilarChunks( 210*9634d734SAndreas Gohr $contextQuestion ?: $question, $this->getLanguageLimit() 211*9634d734SAndreas Gohr ); 212*9634d734SAndreas Gohr } 213ed47fd87SAndreas Gohr } 214ed47fd87SAndreas Gohr 2159e81bea7SAndreas Gohr if ($similar) { 216441edf84SAndreas Gohr $context = implode( 217441edf84SAndreas Gohr "\n", 218441edf84SAndreas Gohr array_map(static fn(Chunk $chunk) => "\n```\n" . $chunk->getText() . "\n```\n", $similar) 219441edf84SAndreas Gohr ); 220219268b1SAndreas Gohr $prompt = $this->getPrompt('question', [ 221219268b1SAndreas Gohr 'context' => $context, 22259a2a267SAndreas Gohr 'question' => $question, 223666b8ea7SAndreas Gohr 'customprompt' => $this->getConf('customprompt'), 224219268b1SAndreas Gohr ]); 2259e81bea7SAndreas Gohr } else { 22659a2a267SAndreas Gohr $prompt = $this->getPrompt('noanswer', [ 22759a2a267SAndreas Gohr 'question' => $question, 22859a2a267SAndreas Gohr ]); 22934a1c478SAndreas Gohr $history = []; 2309e81bea7SAndreas Gohr } 23168908844SAndreas Gohr 23251aa8517SAndreas Gohr $messages = $this->prepareMessages( 2332071dcedSAndreas Gohr $this->getChatModel(), 2342071dcedSAndreas Gohr $prompt, 2352071dcedSAndreas Gohr $history, 2362071dcedSAndreas Gohr $this->getConf('chatHistory') 23751aa8517SAndreas Gohr ); 2386a18e0f4SAndreas Gohr $answer = $this->getChatModel()->getAnswer($messages); 2390337f47fSAndreas Gohr 2400337f47fSAndreas Gohr return [ 2410337f47fSAndreas Gohr 'question' => $question, 24287090e4bSAndreas Gohr 'contextQuestion' => $contextQuestion, 2430337f47fSAndreas Gohr 'answer' => $answer, 2440337f47fSAndreas Gohr 'sources' => $similar, 2450337f47fSAndreas Gohr ]; 2460337f47fSAndreas Gohr } 2470337f47fSAndreas Gohr 2480337f47fSAndreas Gohr /** 2490337f47fSAndreas Gohr * Rephrase a question into a standalone question based on the chat history 2500337f47fSAndreas Gohr * 2510337f47fSAndreas Gohr * @param string $question The original user question 2520337f47fSAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 2530337f47fSAndreas Gohr * @return string The rephrased question 2540337f47fSAndreas Gohr * @throws Exception 2550337f47fSAndreas Gohr */ 2560337f47fSAndreas Gohr public function rephraseChatQuestion($question, $history) 2570337f47fSAndreas Gohr { 25859a2a267SAndreas Gohr $prompt = $this->getPrompt('rephrase', [ 25959a2a267SAndreas Gohr 'question' => $question, 26059a2a267SAndreas Gohr ]); 26151aa8517SAndreas Gohr $messages = $this->prepareMessages( 2622071dcedSAndreas Gohr $this->getRephraseModel(), 2632071dcedSAndreas Gohr $prompt, 2642071dcedSAndreas Gohr $history, 2652071dcedSAndreas Gohr $this->getConf('rephraseHistory') 26651aa8517SAndreas Gohr ); 26751aa8517SAndreas Gohr return $this->getRephraseModel()->getAnswer($messages); 26834a1c478SAndreas Gohr } 26934a1c478SAndreas Gohr 27034a1c478SAndreas Gohr /** 27134a1c478SAndreas Gohr * Prepare the messages for the AI 27234a1c478SAndreas Gohr * 27351aa8517SAndreas Gohr * @param ChatInterface $model The used model 27459a2a267SAndreas Gohr * @param string $promptedQuestion The user question embedded in a prompt 27534a1c478SAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 27651aa8517SAndreas Gohr * @param int $historySize The maximum number of messages to use from the history 27734a1c478SAndreas Gohr * @return array An OpenAI compatible array of messages 27834a1c478SAndreas Gohr */ 27951aa8517SAndreas Gohr protected function prepareMessages( 2802071dcedSAndreas Gohr ChatInterface $model, 2812071dcedSAndreas Gohr string $promptedQuestion, 2822071dcedSAndreas Gohr array $history, 2832071dcedSAndreas Gohr int $historySize 284*9634d734SAndreas Gohr ): array 285*9634d734SAndreas Gohr { 28634a1c478SAndreas Gohr // calculate the space for context 2877be8078eSAndreas Gohr $remainingContext = $model->getMaxInputTokenLength(); // might be 0 28859a2a267SAndreas Gohr $remainingContext -= $this->countTokens($promptedQuestion); 2897be8078eSAndreas Gohr $safetyMargin = abs($remainingContext) * 0.05; // 5% safety margin 2907be8078eSAndreas Gohr $remainingContext -= $safetyMargin; // may be negative, it will be ignored then 29134a1c478SAndreas Gohr 29251aa8517SAndreas Gohr $messages = $this->historyMessages($history, $remainingContext, $historySize); 29334a1c478SAndreas Gohr $messages[] = [ 29434a1c478SAndreas Gohr 'role' => 'user', 29559a2a267SAndreas Gohr 'content' => $promptedQuestion 29634a1c478SAndreas Gohr ]; 29734a1c478SAndreas Gohr return $messages; 29834a1c478SAndreas Gohr } 29934a1c478SAndreas Gohr 30034a1c478SAndreas Gohr /** 30134a1c478SAndreas Gohr * Create an array of OpenAI compatible messages from the given history 30234a1c478SAndreas Gohr * 30334a1c478SAndreas Gohr * Only as many messages are used as fit into the token limit 30434a1c478SAndreas Gohr * 30534a1c478SAndreas Gohr * @param array[] $history The chat history [[user, ai], [user, ai], ...] 3067be8078eSAndreas Gohr * @param int $tokenLimit The maximum number of tokens to use, negative limit disables this check 30751aa8517SAndreas Gohr * @param int $sizeLimit The maximum number of messages to use 30834a1c478SAndreas Gohr * @return array 30934a1c478SAndreas Gohr */ 31051aa8517SAndreas Gohr protected function historyMessages(array $history, int $tokenLimit, int $sizeLimit): array 31134a1c478SAndreas Gohr { 31234a1c478SAndreas Gohr $remainingContext = $tokenLimit; 31334a1c478SAndreas Gohr 31434a1c478SAndreas Gohr $messages = []; 3150337f47fSAndreas Gohr $history = array_reverse($history); 31651aa8517SAndreas Gohr $history = array_slice($history, 0, $sizeLimit); 3170337f47fSAndreas Gohr foreach ($history as $row) { 31834a1c478SAndreas Gohr $length = $this->countTokens($row[0] . $row[1]); 3197be8078eSAndreas Gohr 3207be8078eSAndreas Gohr if ($tokenLimit > 0 && $length > $remainingContext) { 3210337f47fSAndreas Gohr break; 3220337f47fSAndreas Gohr } 32334a1c478SAndreas Gohr $remainingContext -= $length; 3240337f47fSAndreas Gohr 32534a1c478SAndreas Gohr $messages[] = [ 32634a1c478SAndreas Gohr 'role' => 'assistant', 32734a1c478SAndreas Gohr 'content' => $row[1] 32834a1c478SAndreas Gohr ]; 32934a1c478SAndreas Gohr $messages[] = [ 33034a1c478SAndreas Gohr 'role' => 'user', 33134a1c478SAndreas Gohr 'content' => $row[0] 33234a1c478SAndreas Gohr ]; 33334a1c478SAndreas Gohr } 33434a1c478SAndreas Gohr return array_reverse($messages); 3350337f47fSAndreas Gohr } 3360337f47fSAndreas Gohr 33734a1c478SAndreas Gohr /** 33834a1c478SAndreas Gohr * Get an aproximation of the token count for the given text 33934a1c478SAndreas Gohr * 34034a1c478SAndreas Gohr * @param $text 34134a1c478SAndreas Gohr * @return int 34234a1c478SAndreas Gohr */ 34334a1c478SAndreas Gohr protected function countTokens($text) 34434a1c478SAndreas Gohr { 34534a1c478SAndreas Gohr return count($this->getEmbeddings()->getTokenEncoder()->encode($text)); 3460337f47fSAndreas Gohr } 3470337f47fSAndreas Gohr 3480337f47fSAndreas Gohr /** 3490337f47fSAndreas Gohr * Load the given prompt template and fill in the variables 3500337f47fSAndreas Gohr * 3510337f47fSAndreas Gohr * @param string $type 3520337f47fSAndreas Gohr * @param string[] $vars 3530337f47fSAndreas Gohr * @return string 3540337f47fSAndreas Gohr */ 3550337f47fSAndreas Gohr protected function getPrompt($type, $vars = []) 3560337f47fSAndreas Gohr { 35759a2a267SAndreas Gohr $template = file_get_contents($this->localFN($type, 'prompt')); 35834a1c478SAndreas Gohr $vars['language'] = $this->getLanguagePrompt(); 3590337f47fSAndreas Gohr 3607ebc7895Ssplitbrain $replace = []; 3610337f47fSAndreas Gohr foreach ($vars as $key => $val) { 3620337f47fSAndreas Gohr $replace['{{' . strtoupper($key) . '}}'] = $val; 3630337f47fSAndreas Gohr } 3640337f47fSAndreas Gohr 3650337f47fSAndreas Gohr return strtr($template, $replace); 3660337f47fSAndreas Gohr } 367219268b1SAndreas Gohr 368219268b1SAndreas Gohr /** 369219268b1SAndreas Gohr * Construct the prompt to define the answer language 370219268b1SAndreas Gohr * 371219268b1SAndreas Gohr * @return string 372219268b1SAndreas Gohr */ 373219268b1SAndreas Gohr protected function getLanguagePrompt() 374219268b1SAndreas Gohr { 375219268b1SAndreas Gohr global $conf; 376cfaf6b32SAndreas Gohr $isoLangnames = include(__DIR__ . '/lang/languages.php'); 377cfaf6b32SAndreas Gohr 378cfaf6b32SAndreas Gohr $currentLang = $isoLangnames[$conf['lang']] ?? 'English'; 379219268b1SAndreas Gohr 380e33a1d7aSAndreas Gohr if ($this->getConf('preferUIlanguage') > AIChat::LANG_AUTO_ALL) { 381219268b1SAndreas Gohr if (isset($isoLangnames[$conf['lang']])) { 382219268b1SAndreas Gohr $languagePrompt = 'Always answer in ' . $isoLangnames[$conf['lang']] . '.'; 383219268b1SAndreas Gohr return $languagePrompt; 384219268b1SAndreas Gohr } 385219268b1SAndreas Gohr } 386219268b1SAndreas Gohr 387cfaf6b32SAndreas Gohr $languagePrompt = 'Always answer in the user\'s language. ' . 388cfaf6b32SAndreas Gohr "If you are unsure about the language, speak $currentLang."; 389219268b1SAndreas Gohr return $languagePrompt; 390219268b1SAndreas Gohr } 391e33a1d7aSAndreas Gohr 392e33a1d7aSAndreas Gohr /** 393e33a1d7aSAndreas Gohr * Should sources be limited to current language? 394e33a1d7aSAndreas Gohr * 395e33a1d7aSAndreas Gohr * @return string The current language code or empty string 396e33a1d7aSAndreas Gohr */ 397e33a1d7aSAndreas Gohr public function getLanguageLimit() 398e33a1d7aSAndreas Gohr { 399e33a1d7aSAndreas Gohr if ($this->getConf('preferUIlanguage') >= AIChat::LANG_UI_LIMITED) { 400e33a1d7aSAndreas Gohr global $conf; 401e33a1d7aSAndreas Gohr return $conf['lang']; 402e33a1d7aSAndreas Gohr } else { 403e33a1d7aSAndreas Gohr return ''; 404e33a1d7aSAndreas Gohr } 405e33a1d7aSAndreas Gohr } 406e75dc39fSAndreas Gohr 407e75dc39fSAndreas Gohr /** 408e75dc39fSAndreas Gohr * Store info about the last run 409e75dc39fSAndreas Gohr * 410e75dc39fSAndreas Gohr * @param array $data 411e75dc39fSAndreas Gohr * @return void 412e75dc39fSAndreas Gohr */ 413e75dc39fSAndreas Gohr public function setRunData(array $data) 414e75dc39fSAndreas Gohr { 415e75dc39fSAndreas Gohr file_put_contents($this->runDataFile, json_encode($data, JSON_PRETTY_PRINT)); 416e75dc39fSAndreas Gohr } 417e75dc39fSAndreas Gohr 418e75dc39fSAndreas Gohr /** 419e75dc39fSAndreas Gohr * Get info about the last run 420e75dc39fSAndreas Gohr * 421e75dc39fSAndreas Gohr * @return array 422e75dc39fSAndreas Gohr */ 423e75dc39fSAndreas Gohr public function getRunData() 424e75dc39fSAndreas Gohr { 425e75dc39fSAndreas Gohr if (!file_exists($this->runDataFile)) { 426e75dc39fSAndreas Gohr return []; 427e75dc39fSAndreas Gohr } 428e75dc39fSAndreas Gohr return json_decode(file_get_contents($this->runDataFile), true); 429e75dc39fSAndreas Gohr } 4300337f47fSAndreas Gohr} 431