18817535bSAndreas Gohr<?php 28817535bSAndreas Gohr 3f6ef2e50SAndreas Gohruse dokuwiki\Extension\CLIPlugin; 4f6ef2e50SAndreas Gohruse dokuwiki\plugin\aichat\Chunk; 501f06932SAndreas Gohruse dokuwiki\Search\Indexer; 6c4584168SAndreas Gohruse splitbrain\phpcli\Colors; 78817535bSAndreas Gohruse splitbrain\phpcli\Options; 83379af09SAndreas Gohruse splitbrain\phpcli\TableFormatter; 98817535bSAndreas Gohr 108817535bSAndreas Gohr/** 118817535bSAndreas Gohr * DokuWiki Plugin aichat (CLI Component) 128817535bSAndreas Gohr * 138817535bSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 148817535bSAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 158817535bSAndreas Gohr */ 16f6ef2e50SAndreas Gohrclass cli_plugin_aichat extends CLIPlugin 178817535bSAndreas Gohr{ 180337f47fSAndreas Gohr /** @var helper_plugin_aichat */ 190337f47fSAndreas Gohr protected $helper; 200337f47fSAndreas Gohr 210337f47fSAndreas Gohr public function __construct($autocatch = true) 220337f47fSAndreas Gohr { 230337f47fSAndreas Gohr parent::__construct($autocatch); 240337f47fSAndreas Gohr $this->helper = plugin_load('helper', 'aichat'); 253379af09SAndreas Gohr $this->helper->setLogger($this); 260337f47fSAndreas Gohr } 270337f47fSAndreas Gohr 288817535bSAndreas Gohr /** @inheritDoc */ 298817535bSAndreas Gohr protected function setup(Options $options) 308817535bSAndreas Gohr { 31bddd899cSAndreas Gohr $options->useCompactHelp(); 32bddd899cSAndreas Gohr 335284515dSAndreas Gohr $options->setHelp( 345284515dSAndreas Gohr 'Manage and query the AI chatbot data. Please note that calls to your LLM provider will be made. ' . 355284515dSAndreas Gohr 'This may incur costs.' 365284515dSAndreas Gohr ); 378817535bSAndreas Gohr 385284515dSAndreas Gohr $options->registerCommand( 395284515dSAndreas Gohr 'embed', 405284515dSAndreas Gohr 'Create embeddings for all pages. This skips pages that already have embeddings' 415284515dSAndreas Gohr ); 425284515dSAndreas Gohr $options->registerOption( 435284515dSAndreas Gohr 'clear', 445284515dSAndreas Gohr 'Clear all existing embeddings before creating new ones', 457ebc7895Ssplitbrain 'c', 467ebc7895Ssplitbrain false, 477ebc7895Ssplitbrain 'embed' 485284515dSAndreas Gohr ); 498817535bSAndreas Gohr 50*e8451b21SAndreas Gohr $options->registerCommand('maintenance', 'Run storage maintenance. Refer to the documentation for details.'); 513379af09SAndreas Gohr 528817535bSAndreas Gohr $options->registerCommand('similar', 'Search for similar pages'); 538817535bSAndreas Gohr $options->registerArgument('query', 'Look up chunks similar to this query', true, 'similar'); 548817535bSAndreas Gohr 558817535bSAndreas Gohr $options->registerCommand('ask', 'Ask a question'); 568817535bSAndreas Gohr $options->registerArgument('question', 'The question to ask', true, 'ask'); 57c4584168SAndreas Gohr 58c4584168SAndreas Gohr $options->registerCommand('chat', 'Start an interactive chat session'); 59ad38c5fdSAndreas Gohr 60*e8451b21SAndreas Gohr $options->registerCommand('models', 'List available models'); 61*e8451b21SAndreas Gohr 62e75dc39fSAndreas Gohr $options->registerCommand('info', 'Get Info about the vector storage and other stats'); 638c8b7ba6SAndreas Gohr 64ad38c5fdSAndreas Gohr $options->registerCommand('split', 'Split a page into chunks (for debugging)'); 65ad38c5fdSAndreas Gohr $options->registerArgument('page', 'The page to split', true, 'split'); 665786be46SAndreas Gohr 6701f06932SAndreas Gohr $options->registerCommand('page', 'Check if chunks for a given page are available (for debugging)'); 6801f06932SAndreas Gohr $options->registerArgument('page', 'The page to check', true, 'page'); 69dc355d57SAndreas Gohr $options->registerOption('dump', 'Dump the chunks', 'd', false, 'page'); 7001f06932SAndreas Gohr 718c8b7ba6SAndreas Gohr $options->registerCommand('tsv', 'Create TSV files for visualizing at http://projector.tensorflow.org/' . 728c8b7ba6SAndreas Gohr ' Not supported on all storages.'); 738c8b7ba6SAndreas Gohr $options->registerArgument('vector.tsv', 'The vector file', false, 'tsv'); 748c8b7ba6SAndreas Gohr $options->registerArgument('meta.tsv', 'The meta file', false, 'tsv'); 758817535bSAndreas Gohr } 768817535bSAndreas Gohr 778817535bSAndreas Gohr /** @inheritDoc */ 788817535bSAndreas Gohr protected function main(Options $options) 798817535bSAndreas Gohr { 80*e8451b21SAndreas Gohr $this->loadConfig(); 813379af09SAndreas Gohr ini_set('memory_limit', -1); 828817535bSAndreas Gohr switch ($options->getCmd()) { 838817535bSAndreas Gohr case 'embed': 845284515dSAndreas Gohr $this->createEmbeddings($options->getOpt('clear')); 858817535bSAndreas Gohr break; 863379af09SAndreas Gohr case 'maintenance': 873379af09SAndreas Gohr $this->runMaintenance(); 883379af09SAndreas Gohr break; 898817535bSAndreas Gohr case 'similar': 908817535bSAndreas Gohr $this->similar($options->getArgs()[0]); 918817535bSAndreas Gohr break; 927552f1aaSAndreas Gohr case 'ask': 937552f1aaSAndreas Gohr $this->ask($options->getArgs()[0]); 947552f1aaSAndreas Gohr break; 95c4584168SAndreas Gohr case 'chat': 96c4584168SAndreas Gohr $this->chat(); 97c4584168SAndreas Gohr break; 98*e8451b21SAndreas Gohr case 'models': 99*e8451b21SAndreas Gohr $this->models(); 100*e8451b21SAndreas Gohr break; 101ad38c5fdSAndreas Gohr case 'split': 102ad38c5fdSAndreas Gohr $this->split($options->getArgs()[0]); 103ad38c5fdSAndreas Gohr break; 10401f06932SAndreas Gohr case 'page': 105dc355d57SAndreas Gohr $this->page($options->getArgs()[0], $options->getOpt('dump')); 10601f06932SAndreas Gohr break; 1075786be46SAndreas Gohr case 'info': 108f6ef2e50SAndreas Gohr $this->showinfo(); 1095786be46SAndreas Gohr break; 1108c8b7ba6SAndreas Gohr case 'tsv': 1118c8b7ba6SAndreas Gohr $args = $options->getArgs(); 1128c8b7ba6SAndreas Gohr $vector = $args[0] ?? 'vector.tsv'; 1138c8b7ba6SAndreas Gohr $meta = $args[1] ?? 'meta.tsv'; 1148c8b7ba6SAndreas Gohr $this->tsv($vector, $meta); 1158c8b7ba6SAndreas Gohr break; 1168817535bSAndreas Gohr default: 1178817535bSAndreas Gohr echo $options->help(); 1188817535bSAndreas Gohr } 1198817535bSAndreas Gohr } 1208817535bSAndreas Gohr 121c4584168SAndreas Gohr /** 1225786be46SAndreas Gohr * @return void 1235786be46SAndreas Gohr */ 124f6ef2e50SAndreas Gohr protected function showinfo() 1255786be46SAndreas Gohr { 1263379af09SAndreas Gohr $stats = [ 1273379af09SAndreas Gohr 'model' => $this->getConf('model'), 1283379af09SAndreas Gohr ]; 129e75dc39fSAndreas Gohr $stats = array_merge( 130e75dc39fSAndreas Gohr $stats, 131e75dc39fSAndreas Gohr array_map('dformat', $this->helper->getRunData()), 132e75dc39fSAndreas Gohr $this->helper->getStorage()->statistics() 133e75dc39fSAndreas Gohr ); 1343379af09SAndreas Gohr $this->printTable($stats); 1357ee8b02dSAndreas Gohr } 136911314cdSAndreas Gohr 1373379af09SAndreas Gohr /** 1383379af09SAndreas Gohr * Print key value data as tabular data 1393379af09SAndreas Gohr * 1403379af09SAndreas Gohr * @param array $data 1413379af09SAndreas Gohr * @param int $level 1423379af09SAndreas Gohr * @return void 1433379af09SAndreas Gohr */ 1443379af09SAndreas Gohr protected function printTable($data, $level = 0) 1453379af09SAndreas Gohr { 1463379af09SAndreas Gohr $tf = new TableFormatter($this->colors); 1473379af09SAndreas Gohr foreach ($data as $key => $value) { 1483379af09SAndreas Gohr if (is_array($value)) { 1493379af09SAndreas Gohr echo $tf->format( 150e75dc39fSAndreas Gohr [$level * 2, 20, '*'], 1513379af09SAndreas Gohr ['', $key, ''], 1523379af09SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE] 1533379af09SAndreas Gohr ); 1543379af09SAndreas Gohr $this->printTable($value, $level + 1); 1553379af09SAndreas Gohr } else { 1563379af09SAndreas Gohr echo $tf->format( 157e75dc39fSAndreas Gohr [$level * 2, 20, '*'], 1583379af09SAndreas Gohr ['', $key, $value], 1593379af09SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTGRAY] 1603379af09SAndreas Gohr ); 1613379af09SAndreas Gohr } 1623379af09SAndreas Gohr } 1635786be46SAndreas Gohr } 1645786be46SAndreas Gohr 1655786be46SAndreas Gohr /** 16601f06932SAndreas Gohr * Check chunk availability for a given page 16701f06932SAndreas Gohr * 16801f06932SAndreas Gohr * @param string $page 16901f06932SAndreas Gohr * @return void 17001f06932SAndreas Gohr */ 171dc355d57SAndreas Gohr protected function page($page, $dump = false) 17201f06932SAndreas Gohr { 17301f06932SAndreas Gohr $indexer = new Indexer(); 17401f06932SAndreas Gohr $pages = $indexer->getPages(); 17501f06932SAndreas Gohr $pos = array_search(cleanID($page), $pages); 17601f06932SAndreas Gohr 17701f06932SAndreas Gohr if ($pos === false) { 17801f06932SAndreas Gohr $this->error('Page not found'); 17901f06932SAndreas Gohr return; 18001f06932SAndreas Gohr } 18101f06932SAndreas Gohr 18201f06932SAndreas Gohr $storage = $this->helper->getStorage(); 18301f06932SAndreas Gohr $chunks = $storage->getPageChunks($page, $pos * 100); 18401f06932SAndreas Gohr if ($chunks) { 18501f06932SAndreas Gohr $this->success('Found ' . count($chunks) . ' chunks'); 186dc355d57SAndreas Gohr if ($dump) { 187dc355d57SAndreas Gohr echo json_encode($chunks, JSON_PRETTY_PRINT); 188dc355d57SAndreas Gohr } 18901f06932SAndreas Gohr } else { 19001f06932SAndreas Gohr $this->error('No chunks found'); 19101f06932SAndreas Gohr } 19201f06932SAndreas Gohr } 19301f06932SAndreas Gohr 19401f06932SAndreas Gohr /** 195ad38c5fdSAndreas Gohr * Split the given page into chunks and print them 196ad38c5fdSAndreas Gohr * 197ad38c5fdSAndreas Gohr * @param string $page 198ad38c5fdSAndreas Gohr * @return void 199ad38c5fdSAndreas Gohr * @throws Exception 200ad38c5fdSAndreas Gohr */ 201ad38c5fdSAndreas Gohr protected function split($page) 202ad38c5fdSAndreas Gohr { 203ad38c5fdSAndreas Gohr $text = rawWiki($page); 204ad38c5fdSAndreas Gohr $chunks = $this->helper->getEmbeddings()->splitIntoChunks($text); 205ad38c5fdSAndreas Gohr foreach ($chunks as $chunk) { 206ad38c5fdSAndreas Gohr echo $chunk; 207ad38c5fdSAndreas Gohr echo "\n"; 208ad38c5fdSAndreas Gohr $this->colors->ptln('--------------------------------', Colors::C_LIGHTPURPLE); 209ad38c5fdSAndreas Gohr } 210ad38c5fdSAndreas Gohr $this->success('Split into ' . count($chunks) . ' chunks'); 211ad38c5fdSAndreas Gohr } 212ad38c5fdSAndreas Gohr 213ad38c5fdSAndreas Gohr /** 214c4584168SAndreas Gohr * Interactive Chat Session 215c4584168SAndreas Gohr * 216c4584168SAndreas Gohr * @return void 217c4584168SAndreas Gohr * @throws Exception 218c4584168SAndreas Gohr */ 219c4584168SAndreas Gohr protected function chat() 220c4584168SAndreas Gohr { 22134a1c478SAndreas Gohr if ($this->loglevel['debug']['enabled']) { 22234a1c478SAndreas Gohr $this->helper->getChatModel()->setDebug(true); 22334a1c478SAndreas Gohr } 22434a1c478SAndreas Gohr 225c4584168SAndreas Gohr $history = []; 226c4584168SAndreas Gohr while ($q = $this->readLine('Your Question')) { 2276a18e0f4SAndreas Gohr $this->helper->getChatModel()->resetUsageStats(); 228f6ef2e50SAndreas Gohr $result = $this->helper->askChatQuestion($q, $history); 229f6ef2e50SAndreas Gohr $this->colors->ptln("Interpretation: {$result['question']}", Colors::C_LIGHTPURPLE); 230f6ef2e50SAndreas Gohr $history[] = [$result['question'], $result['answer']]; 231c4584168SAndreas Gohr $this->printAnswer($result); 232c4584168SAndreas Gohr } 233c4584168SAndreas Gohr } 234c4584168SAndreas Gohr 235*e8451b21SAndreas Gohr protected function models() 236*e8451b21SAndreas Gohr { 237*e8451b21SAndreas Gohr $result = [ 238*e8451b21SAndreas Gohr 'chat' => [], 239*e8451b21SAndreas Gohr 'embedding' => [], 240*e8451b21SAndreas Gohr ]; 241*e8451b21SAndreas Gohr 242*e8451b21SAndreas Gohr 243*e8451b21SAndreas Gohr $jsons = glob(__DIR__ . '/Model/*/models.json'); 244*e8451b21SAndreas Gohr foreach ($jsons as $json) { 245*e8451b21SAndreas Gohr $models = json_decode(file_get_contents($json), true); 246*e8451b21SAndreas Gohr foreach ($models as $type => $model) { 247*e8451b21SAndreas Gohr $namespace = basename(dirname($json)); 248*e8451b21SAndreas Gohr foreach ($model as $name => $info) { 249*e8451b21SAndreas Gohr 250*e8451b21SAndreas Gohr 251*e8451b21SAndreas Gohr $class = '\\dokuwiki\\plugin\\aichat\\Model\\' . $namespace . '\\' . ucfirst($type) . 'Model'; 252*e8451b21SAndreas Gohr try { 253*e8451b21SAndreas Gohr new $class($name, $this->conf); 254*e8451b21SAndreas Gohr $info['confok'] = true; 255*e8451b21SAndreas Gohr } catch (Exception $e) { 256*e8451b21SAndreas Gohr $info['confok'] = false; 257*e8451b21SAndreas Gohr } 258*e8451b21SAndreas Gohr 259*e8451b21SAndreas Gohr $result[$type]["$namespace $name"] = $info; 260*e8451b21SAndreas Gohr } 261*e8451b21SAndreas Gohr } 262*e8451b21SAndreas Gohr } 263*e8451b21SAndreas Gohr 264*e8451b21SAndreas Gohr $td = new TableFormatter($this->colors); 265*e8451b21SAndreas Gohr $cols = [30, 20, 20, '*']; 266*e8451b21SAndreas Gohr echo "==== Chat Models ====\n\n"; 267*e8451b21SAndreas Gohr echo $td->format( 268*e8451b21SAndreas Gohr $cols, 269*e8451b21SAndreas Gohr ['Model', 'Token Limits', 'Price USD/M', 'Description'], 270*e8451b21SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE] 271*e8451b21SAndreas Gohr ); 272*e8451b21SAndreas Gohr foreach ($result['chat'] as $name => $info) { 273*e8451b21SAndreas Gohr echo $td->format( 274*e8451b21SAndreas Gohr $cols, 275*e8451b21SAndreas Gohr [ 276*e8451b21SAndreas Gohr $name, 277*e8451b21SAndreas Gohr sprintf(" In: %7d\nOut: %7d", $info['inputTokens'], $info['outputTokens']), 278*e8451b21SAndreas Gohr sprintf(" In: %.2f\nOut: %.2f", $info['inputTokenPrice'], $info['inputTokenPrice']), 279*e8451b21SAndreas Gohr $info['description']."\n" 280*e8451b21SAndreas Gohr ], 281*e8451b21SAndreas Gohr [ 282*e8451b21SAndreas Gohr $info['confok'] ? Colors::C_LIGHTGREEN : Colors::C_LIGHTRED, 283*e8451b21SAndreas Gohr ] 284*e8451b21SAndreas Gohr ); 285*e8451b21SAndreas Gohr } 286*e8451b21SAndreas Gohr 287*e8451b21SAndreas Gohr echo "==== Embedding Models ====\n\n"; 288*e8451b21SAndreas Gohr echo $td->format( 289*e8451b21SAndreas Gohr $cols, 290*e8451b21SAndreas Gohr ['Model', 'Token Limits', 'Price USD/M', 'Description'], 291*e8451b21SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE] 292*e8451b21SAndreas Gohr ); 293*e8451b21SAndreas Gohr foreach ($result['embedding'] as $name => $info) { 294*e8451b21SAndreas Gohr echo $td->format( 295*e8451b21SAndreas Gohr $cols, 296*e8451b21SAndreas Gohr [ 297*e8451b21SAndreas Gohr $name, 298*e8451b21SAndreas Gohr sprintf("%7d", $info['inputTokens']), 299*e8451b21SAndreas Gohr sprintf("%.2f", $info['inputTokenPrice']), 300*e8451b21SAndreas Gohr $info['description']."\n" 301*e8451b21SAndreas Gohr ], 302*e8451b21SAndreas Gohr [ 303*e8451b21SAndreas Gohr $info['confok'] ? Colors::C_LIGHTGREEN : Colors::C_LIGHTRED, 304*e8451b21SAndreas Gohr ] 305*e8451b21SAndreas Gohr ); 306*e8451b21SAndreas Gohr } 307*e8451b21SAndreas Gohr 308*e8451b21SAndreas Gohr $this->colors->ptln('Current prices may differ', Colors::C_RED); 309*e8451b21SAndreas Gohr } 310*e8451b21SAndreas Gohr 311c4584168SAndreas Gohr /** 312c4584168SAndreas Gohr * Handle a single, standalone question 313c4584168SAndreas Gohr * 314c4584168SAndreas Gohr * @param string $query 315c4584168SAndreas Gohr * @return void 316c4584168SAndreas Gohr * @throws Exception 317c4584168SAndreas Gohr */ 318c4584168SAndreas Gohr protected function ask($query) 319c4584168SAndreas Gohr { 32034a1c478SAndreas Gohr if ($this->loglevel['debug']['enabled']) { 32134a1c478SAndreas Gohr $this->helper->getChatModel()->setDebug(true); 32234a1c478SAndreas Gohr } 32334a1c478SAndreas Gohr 3240337f47fSAndreas Gohr $result = $this->helper->askQuestion($query); 325c4584168SAndreas Gohr $this->printAnswer($result); 3267552f1aaSAndreas Gohr } 3277552f1aaSAndreas Gohr 328c4584168SAndreas Gohr /** 329c4584168SAndreas Gohr * Get the pages that are similar to the query 330c4584168SAndreas Gohr * 331c4584168SAndreas Gohr * @param string $query 332c4584168SAndreas Gohr * @return void 333c4584168SAndreas Gohr */ 3348817535bSAndreas Gohr protected function similar($query) 3358817535bSAndreas Gohr { 336e33a1d7aSAndreas Gohr $langlimit = $this->helper->getLanguageLimit(); 337e33a1d7aSAndreas Gohr if ($langlimit) { 338e33a1d7aSAndreas Gohr $this->info('Limiting results to {lang}', ['lang' => $langlimit]); 339e33a1d7aSAndreas Gohr } 340e33a1d7aSAndreas Gohr 341e33a1d7aSAndreas Gohr $sources = $this->helper->getEmbeddings()->getSimilarChunks($query, $langlimit); 342f6ef2e50SAndreas Gohr $this->printSources($sources); 3438817535bSAndreas Gohr } 3448817535bSAndreas Gohr 345c4584168SAndreas Gohr /** 3463379af09SAndreas Gohr * Run the maintenance tasks 3473379af09SAndreas Gohr * 3483379af09SAndreas Gohr * @return void 3493379af09SAndreas Gohr */ 3503379af09SAndreas Gohr protected function runMaintenance() 3513379af09SAndreas Gohr { 3523379af09SAndreas Gohr $start = time(); 3533379af09SAndreas Gohr $this->helper->getStorage()->runMaintenance(); 3543379af09SAndreas Gohr $this->notice('Peak memory used: {memory}', ['memory' => filesize_h(memory_get_peak_usage(true))]); 3553379af09SAndreas Gohr $this->notice('Spent time: {time}min', ['time' => round((time() - $start) / 60, 2)]); 356e75dc39fSAndreas Gohr 357e75dc39fSAndreas Gohr $data = $this->helper->getRunData(); 358e75dc39fSAndreas Gohr $data['maintenance ran at'] = time(); 359e75dc39fSAndreas Gohr $this->helper->setRunData($data); 3603379af09SAndreas Gohr } 3613379af09SAndreas Gohr 3623379af09SAndreas Gohr /** 363c4584168SAndreas Gohr * Recreate chunks and embeddings for all pages 364c4584168SAndreas Gohr * 365c4584168SAndreas Gohr * @return void 366c4584168SAndreas Gohr */ 3675284515dSAndreas Gohr protected function createEmbeddings($clear) 3688817535bSAndreas Gohr { 369d5c102b3SAndreas Gohr [$skipRE, $matchRE] = $this->getRegexps(); 370d5c102b3SAndreas Gohr 3713379af09SAndreas Gohr $start = time(); 372d5c102b3SAndreas Gohr $this->helper->getEmbeddings()->createNewIndex($skipRE, $matchRE, $clear); 373ad38c5fdSAndreas Gohr $this->notice('Peak memory used: {memory}', ['memory' => filesize_h(memory_get_peak_usage(true))]); 3743379af09SAndreas Gohr $this->notice('Spent time: {time}min', ['time' => round((time() - $start) / 60, 2)]); 375e75dc39fSAndreas Gohr 376e75dc39fSAndreas Gohr $data = $this->helper->getRunData(); 377e75dc39fSAndreas Gohr $data['embed ran at'] = time(); 378e75dc39fSAndreas Gohr $this->helper->setRunData($data); 3798817535bSAndreas Gohr } 3808817535bSAndreas Gohr 381c4584168SAndreas Gohr /** 3828c8b7ba6SAndreas Gohr * Dump TSV files for debugging 3838c8b7ba6SAndreas Gohr * 3848c8b7ba6SAndreas Gohr * @return void 3858c8b7ba6SAndreas Gohr */ 3868c8b7ba6SAndreas Gohr protected function tsv($vector, $meta) 3878c8b7ba6SAndreas Gohr { 3888c8b7ba6SAndreas Gohr 3898c8b7ba6SAndreas Gohr $storage = $this->helper->getStorage(); 3908c8b7ba6SAndreas Gohr $storage->dumpTSV($vector, $meta); 3918c8b7ba6SAndreas Gohr $this->success('written to ' . $vector . ' and ' . $meta); 3928c8b7ba6SAndreas Gohr } 3938c8b7ba6SAndreas Gohr 3948c8b7ba6SAndreas Gohr /** 39555392016SAndreas Gohr * Print the given detailed answer in a nice way 39655392016SAndreas Gohr * 39755392016SAndreas Gohr * @param array $answer 39855392016SAndreas Gohr * @return void 39955392016SAndreas Gohr */ 40055392016SAndreas Gohr protected function printAnswer($answer) 40155392016SAndreas Gohr { 40255392016SAndreas Gohr $this->colors->ptln($answer['answer'], Colors::C_LIGHTCYAN); 40355392016SAndreas Gohr echo "\n"; 404f6ef2e50SAndreas Gohr $this->printSources($answer['sources']); 40555392016SAndreas Gohr echo "\n"; 40655392016SAndreas Gohr $this->printUsage(); 40755392016SAndreas Gohr } 40855392016SAndreas Gohr 40955392016SAndreas Gohr /** 410f6ef2e50SAndreas Gohr * Print the given sources 411f6ef2e50SAndreas Gohr * 412f6ef2e50SAndreas Gohr * @param Chunk[] $sources 413f6ef2e50SAndreas Gohr * @return void 414f6ef2e50SAndreas Gohr */ 415f6ef2e50SAndreas Gohr protected function printSources($sources) 416f6ef2e50SAndreas Gohr { 417f6ef2e50SAndreas Gohr foreach ($sources as $source) { 418f6ef2e50SAndreas Gohr /** @var Chunk $source */ 4199b3d1b36SAndreas Gohr $this->colors->ptln( 4209b3d1b36SAndreas Gohr "\t" . $source->getPage() . ' ' . $source->getId() . ' (' . $source->getScore() . ')', 4219b3d1b36SAndreas Gohr Colors::C_LIGHTBLUE 4229b3d1b36SAndreas Gohr ); 423f6ef2e50SAndreas Gohr } 424f6ef2e50SAndreas Gohr } 425f6ef2e50SAndreas Gohr 426f6ef2e50SAndreas Gohr /** 42755392016SAndreas Gohr * Print the usage statistics for OpenAI 42855392016SAndreas Gohr * 42955392016SAndreas Gohr * @return void 43055392016SAndreas Gohr */ 431f6ef2e50SAndreas Gohr protected function printUsage() 432f6ef2e50SAndreas Gohr { 43355392016SAndreas Gohr $this->info( 434f6ef2e50SAndreas Gohr 'Made {requests} requests in {time}s to Model. Used {tokens} tokens for about ${cost}.', 4356a18e0f4SAndreas Gohr $this->helper->getChatModel()->getUsageStats() 43655392016SAndreas Gohr ); 43755392016SAndreas Gohr } 43855392016SAndreas Gohr 43955392016SAndreas Gohr /** 440c4584168SAndreas Gohr * Interactively ask for a value from the user 441c4584168SAndreas Gohr * 442c4584168SAndreas Gohr * @param string $prompt 443c4584168SAndreas Gohr * @return string 444c4584168SAndreas Gohr */ 445c4584168SAndreas Gohr protected function readLine($prompt) 446c4584168SAndreas Gohr { 447c4584168SAndreas Gohr $value = ''; 4488817535bSAndreas Gohr 449c4584168SAndreas Gohr while ($value === '') { 450c4584168SAndreas Gohr echo $prompt; 451c4584168SAndreas Gohr echo ': '; 452c4584168SAndreas Gohr 453c4584168SAndreas Gohr $fh = fopen('php://stdin', 'r'); 454c4584168SAndreas Gohr $value = trim(fgets($fh)); 455c4584168SAndreas Gohr fclose($fh); 456c4584168SAndreas Gohr } 457c4584168SAndreas Gohr 458c4584168SAndreas Gohr return $value; 459c4584168SAndreas Gohr } 460d5c102b3SAndreas Gohr 461d5c102b3SAndreas Gohr /** 462d5c102b3SAndreas Gohr * Read the skip and match regex from the config 463d5c102b3SAndreas Gohr * 464d5c102b3SAndreas Gohr * Ensures the regular expressions are valid 465d5c102b3SAndreas Gohr * 466d5c102b3SAndreas Gohr * @return string[] [$skipRE, $matchRE] 467d5c102b3SAndreas Gohr */ 468d5c102b3SAndreas Gohr protected function getRegexps() 469d5c102b3SAndreas Gohr { 470d5c102b3SAndreas Gohr $skip = $this->getConf('skipRegex'); 471d5c102b3SAndreas Gohr $skipRE = ''; 472d5c102b3SAndreas Gohr $match = $this->getConf('matchRegex'); 473d5c102b3SAndreas Gohr $matchRE = ''; 474d5c102b3SAndreas Gohr 475d5c102b3SAndreas Gohr if ($skip) { 476d5c102b3SAndreas Gohr $skipRE = '/' . $skip . '/'; 47749a7d3ccSsplitbrain if (@preg_match($skipRE, '') === false) { 478d5c102b3SAndreas Gohr $this->error(preg_last_error_msg()); 479d5c102b3SAndreas Gohr $this->error('Invalid regular expression in $conf[\'skipRegex\']. Ignored.'); 480d5c102b3SAndreas Gohr $skipRE = ''; 481d5c102b3SAndreas Gohr } else { 482d5c102b3SAndreas Gohr $this->success('Skipping pages matching ' . $skipRE); 483d5c102b3SAndreas Gohr } 484d5c102b3SAndreas Gohr } 485d5c102b3SAndreas Gohr 486d5c102b3SAndreas Gohr if ($match) { 487d5c102b3SAndreas Gohr $matchRE = '/' . $match . '/'; 48849a7d3ccSsplitbrain if (@preg_match($matchRE, '') === false) { 489d5c102b3SAndreas Gohr $this->error(preg_last_error_msg()); 490d5c102b3SAndreas Gohr $this->error('Invalid regular expression in $conf[\'matchRegex\']. Ignored.'); 491d5c102b3SAndreas Gohr $matchRE = ''; 492d5c102b3SAndreas Gohr } else { 493d5c102b3SAndreas Gohr $this->success('Only indexing pages matching ' . $matchRE); 494d5c102b3SAndreas Gohr } 495d5c102b3SAndreas Gohr } 496d5c102b3SAndreas Gohr return [$skipRE, $matchRE]; 497d5c102b3SAndreas Gohr } 4988817535bSAndreas Gohr} 499