18817535bSAndreas Gohr<?php 28817535bSAndreas Gohr 3f6ef2e50SAndreas Gohruse dokuwiki\Extension\CLIPlugin; 40de7e020SAndreas Gohruse dokuwiki\plugin\aichat\AbstractCLI; 5f6ef2e50SAndreas Gohruse dokuwiki\plugin\aichat\Chunk; 6c2b7a1f7SAndreas Gohruse dokuwiki\plugin\aichat\ModelFactory; 701f06932SAndreas Gohruse dokuwiki\Search\Indexer; 8c4584168SAndreas Gohruse splitbrain\phpcli\Colors; 98817535bSAndreas Gohruse splitbrain\phpcli\Options; 103379af09SAndreas Gohruse splitbrain\phpcli\TableFormatter; 118817535bSAndreas Gohr 128817535bSAndreas Gohr/** 138817535bSAndreas Gohr * DokuWiki Plugin aichat (CLI Component) 148817535bSAndreas Gohr * 158817535bSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 168817535bSAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 178817535bSAndreas Gohr */ 180de7e020SAndreas Gohrclass cli_plugin_aichat extends AbstractCLI 198817535bSAndreas Gohr{ 200337f47fSAndreas Gohr /** @var helper_plugin_aichat */ 210337f47fSAndreas Gohr protected $helper; 220337f47fSAndreas Gohr 238817535bSAndreas Gohr /** @inheritDoc */ 248817535bSAndreas Gohr protected function setup(Options $options) 258817535bSAndreas Gohr { 260de7e020SAndreas Gohr parent::setup($options); 27bddd899cSAndreas Gohr 285284515dSAndreas Gohr $options->setHelp( 295284515dSAndreas Gohr 'Manage and query the AI chatbot data. Please note that calls to your LLM provider will be made. ' . 305284515dSAndreas Gohr 'This may incur costs.' 315284515dSAndreas Gohr ); 328817535bSAndreas Gohr 330de7e020SAndreas Gohr $options->registerOption( 340de7e020SAndreas Gohr 'model', 350de7e020SAndreas Gohr 'Overrides the chat and rephrasing model settings and uses this model instead', 360de7e020SAndreas Gohr '', 370de7e020SAndreas Gohr 'model' 380de7e020SAndreas Gohr ); 390de7e020SAndreas Gohr 405284515dSAndreas Gohr $options->registerCommand( 415284515dSAndreas Gohr 'embed', 425284515dSAndreas Gohr 'Create embeddings for all pages. This skips pages that already have embeddings' 435284515dSAndreas Gohr ); 445284515dSAndreas Gohr $options->registerOption( 455284515dSAndreas Gohr 'clear', 465284515dSAndreas Gohr 'Clear all existing embeddings before creating new ones', 477ebc7895Ssplitbrain 'c', 487ebc7895Ssplitbrain false, 497ebc7895Ssplitbrain 'embed' 505284515dSAndreas Gohr ); 518817535bSAndreas Gohr 52e8451b21SAndreas Gohr $options->registerCommand('maintenance', 'Run storage maintenance. Refer to the documentation for details.'); 533379af09SAndreas Gohr 548817535bSAndreas Gohr $options->registerCommand('similar', 'Search for similar pages'); 558817535bSAndreas Gohr $options->registerArgument('query', 'Look up chunks similar to this query', true, 'similar'); 568817535bSAndreas Gohr 578817535bSAndreas Gohr $options->registerCommand('ask', 'Ask a question'); 588817535bSAndreas Gohr $options->registerArgument('question', 'The question to ask', true, 'ask'); 59c4584168SAndreas Gohr 60c4584168SAndreas Gohr $options->registerCommand('chat', 'Start an interactive chat session'); 61ad38c5fdSAndreas Gohr 62e8451b21SAndreas Gohr $options->registerCommand('models', 'List available models'); 63e8451b21SAndreas Gohr 64e75dc39fSAndreas Gohr $options->registerCommand('info', 'Get Info about the vector storage and other stats'); 658c8b7ba6SAndreas Gohr 66ad38c5fdSAndreas Gohr $options->registerCommand('split', 'Split a page into chunks (for debugging)'); 67ad38c5fdSAndreas Gohr $options->registerArgument('page', 'The page to split', true, 'split'); 685786be46SAndreas Gohr 6901f06932SAndreas Gohr $options->registerCommand('page', 'Check if chunks for a given page are available (for debugging)'); 7001f06932SAndreas Gohr $options->registerArgument('page', 'The page to check', true, 'page'); 71dc355d57SAndreas Gohr $options->registerOption('dump', 'Dump the chunks', 'd', false, 'page'); 7201f06932SAndreas Gohr 738c8b7ba6SAndreas Gohr $options->registerCommand('tsv', 'Create TSV files for visualizing at http://projector.tensorflow.org/' . 748c8b7ba6SAndreas Gohr ' Not supported on all storages.'); 758c8b7ba6SAndreas Gohr $options->registerArgument('vector.tsv', 'The vector file', false, 'tsv'); 768c8b7ba6SAndreas Gohr $options->registerArgument('meta.tsv', 'The meta file', false, 'tsv'); 778817535bSAndreas Gohr } 788817535bSAndreas Gohr 798817535bSAndreas Gohr /** @inheritDoc */ 808817535bSAndreas Gohr protected function main(Options $options) 818817535bSAndreas Gohr { 820de7e020SAndreas Gohr parent::main($options); 830de7e020SAndreas Gohr 840de7e020SAndreas Gohr $model = $options->getOpt('model'); 850de7e020SAndreas Gohr if ($model) { 860de7e020SAndreas Gohr $this->helper->updateConfig( 870de7e020SAndreas Gohr ['chatmodel' => $model, 'rephasemodel' => $model] 880de7e020SAndreas Gohr ); 89c2b7a1f7SAndreas Gohr } 90c2b7a1f7SAndreas Gohr 918817535bSAndreas Gohr switch ($options->getCmd()) { 928817535bSAndreas Gohr case 'embed': 935284515dSAndreas Gohr $this->createEmbeddings($options->getOpt('clear')); 948817535bSAndreas Gohr break; 953379af09SAndreas Gohr case 'maintenance': 963379af09SAndreas Gohr $this->runMaintenance(); 973379af09SAndreas Gohr break; 988817535bSAndreas Gohr case 'similar': 998817535bSAndreas Gohr $this->similar($options->getArgs()[0]); 1008817535bSAndreas Gohr break; 1017552f1aaSAndreas Gohr case 'ask': 1027552f1aaSAndreas Gohr $this->ask($options->getArgs()[0]); 1037552f1aaSAndreas Gohr break; 104c4584168SAndreas Gohr case 'chat': 105c4584168SAndreas Gohr $this->chat(); 106c4584168SAndreas Gohr break; 107e8451b21SAndreas Gohr case 'models': 108e8451b21SAndreas Gohr $this->models(); 109e8451b21SAndreas Gohr break; 110ad38c5fdSAndreas Gohr case 'split': 111ad38c5fdSAndreas Gohr $this->split($options->getArgs()[0]); 112ad38c5fdSAndreas Gohr break; 11301f06932SAndreas Gohr case 'page': 114dc355d57SAndreas Gohr $this->page($options->getArgs()[0], $options->getOpt('dump')); 11501f06932SAndreas Gohr break; 1165786be46SAndreas Gohr case 'info': 117f6ef2e50SAndreas Gohr $this->showinfo(); 1185786be46SAndreas Gohr break; 1198c8b7ba6SAndreas Gohr case 'tsv': 1208c8b7ba6SAndreas Gohr $args = $options->getArgs(); 1218c8b7ba6SAndreas Gohr $vector = $args[0] ?? 'vector.tsv'; 1228c8b7ba6SAndreas Gohr $meta = $args[1] ?? 'meta.tsv'; 1238c8b7ba6SAndreas Gohr $this->tsv($vector, $meta); 1248c8b7ba6SAndreas Gohr break; 1258817535bSAndreas Gohr default: 1268817535bSAndreas Gohr echo $options->help(); 1278817535bSAndreas Gohr } 1288817535bSAndreas Gohr } 1298817535bSAndreas Gohr 130c4584168SAndreas Gohr /** 1315786be46SAndreas Gohr * @return void 1325786be46SAndreas Gohr */ 133f6ef2e50SAndreas Gohr protected function showinfo() 1345786be46SAndreas Gohr { 1353379af09SAndreas Gohr $stats = [ 136b446155bSAndreas Gohr 'embed model' => (string) $this->helper->getEmbeddingModel(), 137b446155bSAndreas Gohr 'rephrase model' => (string) $this->helper->getRephraseModel(), 138b446155bSAndreas Gohr 'chat model' => (string) $this->helper->getChatModel(), 1393379af09SAndreas Gohr ]; 140e75dc39fSAndreas Gohr $stats = array_merge( 141e75dc39fSAndreas Gohr $stats, 142*bae450a9SAndreas Gohr $this->helper->getRunData(), 143e75dc39fSAndreas Gohr $this->helper->getStorage()->statistics() 144e75dc39fSAndreas Gohr ); 1453379af09SAndreas Gohr $this->printTable($stats); 1467ee8b02dSAndreas Gohr } 147911314cdSAndreas Gohr 1483379af09SAndreas Gohr /** 1493379af09SAndreas Gohr * Print key value data as tabular data 1503379af09SAndreas Gohr * 1513379af09SAndreas Gohr * @param array $data 1523379af09SAndreas Gohr * @param int $level 1533379af09SAndreas Gohr * @return void 1543379af09SAndreas Gohr */ 1553379af09SAndreas Gohr protected function printTable($data, $level = 0) 1563379af09SAndreas Gohr { 1573379af09SAndreas Gohr $tf = new TableFormatter($this->colors); 1583379af09SAndreas Gohr foreach ($data as $key => $value) { 1593379af09SAndreas Gohr if (is_array($value)) { 1603379af09SAndreas Gohr echo $tf->format( 161e75dc39fSAndreas Gohr [$level * 2, 20, '*'], 1623379af09SAndreas Gohr ['', $key, ''], 1633379af09SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE] 1643379af09SAndreas Gohr ); 1653379af09SAndreas Gohr $this->printTable($value, $level + 1); 1663379af09SAndreas Gohr } else { 1673379af09SAndreas Gohr echo $tf->format( 168e75dc39fSAndreas Gohr [$level * 2, 20, '*'], 1693379af09SAndreas Gohr ['', $key, $value], 1703379af09SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTGRAY] 1713379af09SAndreas Gohr ); 1723379af09SAndreas Gohr } 1733379af09SAndreas Gohr } 1745786be46SAndreas Gohr } 1755786be46SAndreas Gohr 1765786be46SAndreas Gohr /** 17701f06932SAndreas Gohr * Check chunk availability for a given page 17801f06932SAndreas Gohr * 17901f06932SAndreas Gohr * @param string $page 18001f06932SAndreas Gohr * @return void 18101f06932SAndreas Gohr */ 182dc355d57SAndreas Gohr protected function page($page, $dump = false) 18301f06932SAndreas Gohr { 18401f06932SAndreas Gohr $indexer = new Indexer(); 18501f06932SAndreas Gohr $pages = $indexer->getPages(); 18601f06932SAndreas Gohr $pos = array_search(cleanID($page), $pages); 18701f06932SAndreas Gohr 18801f06932SAndreas Gohr if ($pos === false) { 18901f06932SAndreas Gohr $this->error('Page not found'); 19001f06932SAndreas Gohr return; 19101f06932SAndreas Gohr } 19201f06932SAndreas Gohr 19301f06932SAndreas Gohr $storage = $this->helper->getStorage(); 19401f06932SAndreas Gohr $chunks = $storage->getPageChunks($page, $pos * 100); 19501f06932SAndreas Gohr if ($chunks) { 19601f06932SAndreas Gohr $this->success('Found ' . count($chunks) . ' chunks'); 197dc355d57SAndreas Gohr if ($dump) { 198dc355d57SAndreas Gohr echo json_encode($chunks, JSON_PRETTY_PRINT); 199dc355d57SAndreas Gohr } 20001f06932SAndreas Gohr } else { 20101f06932SAndreas Gohr $this->error('No chunks found'); 20201f06932SAndreas Gohr } 20301f06932SAndreas Gohr } 20401f06932SAndreas Gohr 20501f06932SAndreas Gohr /** 206ad38c5fdSAndreas Gohr * Split the given page into chunks and print them 207ad38c5fdSAndreas Gohr * 208ad38c5fdSAndreas Gohr * @param string $page 209ad38c5fdSAndreas Gohr * @return void 210ad38c5fdSAndreas Gohr * @throws Exception 211ad38c5fdSAndreas Gohr */ 212ad38c5fdSAndreas Gohr protected function split($page) 213ad38c5fdSAndreas Gohr { 214ab1f8ddeSAndreas Gohr $chunks = $this->helper->getEmbeddings()->createPageChunks($page, 0); 215ad38c5fdSAndreas Gohr foreach ($chunks as $chunk) { 216ab1f8ddeSAndreas Gohr echo $chunk->getText(); 217ad38c5fdSAndreas Gohr echo "\n"; 218ad38c5fdSAndreas Gohr $this->colors->ptln('--------------------------------', Colors::C_LIGHTPURPLE); 219ad38c5fdSAndreas Gohr } 220ad38c5fdSAndreas Gohr $this->success('Split into ' . count($chunks) . ' chunks'); 221ad38c5fdSAndreas Gohr } 222ad38c5fdSAndreas Gohr 223ad38c5fdSAndreas Gohr /** 224c4584168SAndreas Gohr * Interactive Chat Session 225c4584168SAndreas Gohr * 226c4584168SAndreas Gohr * @return void 227c4584168SAndreas Gohr * @throws Exception 228c4584168SAndreas Gohr */ 229c4584168SAndreas Gohr protected function chat() 230c4584168SAndreas Gohr { 231c4584168SAndreas Gohr $history = []; 232c4584168SAndreas Gohr while ($q = $this->readLine('Your Question')) { 2336a18e0f4SAndreas Gohr $this->helper->getChatModel()->resetUsageStats(); 23451aa8517SAndreas Gohr $this->helper->getRephraseModel()->resetUsageStats(); 235c2b7a1f7SAndreas Gohr $this->helper->getEmbeddingModel()->resetUsageStats(); 236f6ef2e50SAndreas Gohr $result = $this->helper->askChatQuestion($q, $history); 237f6ef2e50SAndreas Gohr $this->colors->ptln("Interpretation: {$result['question']}", Colors::C_LIGHTPURPLE); 238f6ef2e50SAndreas Gohr $history[] = [$result['question'], $result['answer']]; 239c4584168SAndreas Gohr $this->printAnswer($result); 240c4584168SAndreas Gohr } 241c4584168SAndreas Gohr } 242c4584168SAndreas Gohr 243c2b7a1f7SAndreas Gohr /** 244c2b7a1f7SAndreas Gohr * Print information about the available models 245c2b7a1f7SAndreas Gohr * 246c2b7a1f7SAndreas Gohr * @return void 247c2b7a1f7SAndreas Gohr */ 248e8451b21SAndreas Gohr protected function models() 249e8451b21SAndreas Gohr { 250c2b7a1f7SAndreas Gohr $result = (new ModelFactory($this->conf))->getModels(); 251e8451b21SAndreas Gohr 252e8451b21SAndreas Gohr $td = new TableFormatter($this->colors); 253e8451b21SAndreas Gohr $cols = [30, 20, 20, '*']; 254e8451b21SAndreas Gohr echo "==== Chat Models ====\n\n"; 255e8451b21SAndreas Gohr echo $td->format( 256e8451b21SAndreas Gohr $cols, 257e8451b21SAndreas Gohr ['Model', 'Token Limits', 'Price USD/M', 'Description'], 258e8451b21SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE] 259e8451b21SAndreas Gohr ); 260e8451b21SAndreas Gohr foreach ($result['chat'] as $name => $info) { 261e8451b21SAndreas Gohr echo $td->format( 262e8451b21SAndreas Gohr $cols, 263e8451b21SAndreas Gohr [ 264e8451b21SAndreas Gohr $name, 265e8451b21SAndreas Gohr sprintf(" In: %7d\nOut: %7d", $info['inputTokens'], $info['outputTokens']), 2662045e15aSAndreas Gohr sprintf(" In: %.2f\nOut: %.2f", $info['inputTokenPrice'], $info['outputTokenPrice']), 267e8451b21SAndreas Gohr $info['description'] . "\n" 268e8451b21SAndreas Gohr ], 269e8451b21SAndreas Gohr [ 270c2b7a1f7SAndreas Gohr $info['instance'] ? Colors::C_LIGHTGREEN : Colors::C_LIGHTRED, 271e8451b21SAndreas Gohr ] 272e8451b21SAndreas Gohr ); 273e8451b21SAndreas Gohr } 274e8451b21SAndreas Gohr 27587e46484SAndreas Gohr $cols = [30, 10, 10, 10, '*']; 276e8451b21SAndreas Gohr echo "==== Embedding Models ====\n\n"; 277e8451b21SAndreas Gohr echo $td->format( 278e8451b21SAndreas Gohr $cols, 27987e46484SAndreas Gohr ['Model', 'Token Limits', 'Price USD/M', 'Dimensions', 'Description'], 28087e46484SAndreas Gohr [Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE, Colors::C_LIGHTBLUE] 281e8451b21SAndreas Gohr ); 282e8451b21SAndreas Gohr foreach ($result['embedding'] as $name => $info) { 283e8451b21SAndreas Gohr echo $td->format( 284e8451b21SAndreas Gohr $cols, 285e8451b21SAndreas Gohr [ 286e8451b21SAndreas Gohr $name, 287e8451b21SAndreas Gohr sprintf("%7d", $info['inputTokens']), 288e8451b21SAndreas Gohr sprintf("%.2f", $info['inputTokenPrice']), 28987e46484SAndreas Gohr $info['dimensions'], 290e8451b21SAndreas Gohr $info['description'] . "\n" 291e8451b21SAndreas Gohr ], 292e8451b21SAndreas Gohr [ 293c2b7a1f7SAndreas Gohr $info['instance'] ? Colors::C_LIGHTGREEN : Colors::C_LIGHTRED, 294e8451b21SAndreas Gohr ] 295e8451b21SAndreas Gohr ); 296e8451b21SAndreas Gohr } 297e8451b21SAndreas Gohr 298e8451b21SAndreas Gohr $this->colors->ptln('Current prices may differ', Colors::C_RED); 299e8451b21SAndreas Gohr } 300e8451b21SAndreas Gohr 301c4584168SAndreas Gohr /** 302c4584168SAndreas Gohr * Handle a single, standalone question 303c4584168SAndreas Gohr * 304c4584168SAndreas Gohr * @param string $query 305c4584168SAndreas Gohr * @return void 306c4584168SAndreas Gohr * @throws Exception 307c4584168SAndreas Gohr */ 308c4584168SAndreas Gohr protected function ask($query) 309c4584168SAndreas Gohr { 3100337f47fSAndreas Gohr $result = $this->helper->askQuestion($query); 311c4584168SAndreas Gohr $this->printAnswer($result); 3127552f1aaSAndreas Gohr } 3137552f1aaSAndreas Gohr 314c4584168SAndreas Gohr /** 315c4584168SAndreas Gohr * Get the pages that are similar to the query 316c4584168SAndreas Gohr * 317c4584168SAndreas Gohr * @param string $query 318c4584168SAndreas Gohr * @return void 319c4584168SAndreas Gohr */ 3208817535bSAndreas Gohr protected function similar($query) 3218817535bSAndreas Gohr { 322e33a1d7aSAndreas Gohr $langlimit = $this->helper->getLanguageLimit(); 323e33a1d7aSAndreas Gohr if ($langlimit) { 324e33a1d7aSAndreas Gohr $this->info('Limiting results to {lang}', ['lang' => $langlimit]); 325e33a1d7aSAndreas Gohr } 326e33a1d7aSAndreas Gohr 327e33a1d7aSAndreas Gohr $sources = $this->helper->getEmbeddings()->getSimilarChunks($query, $langlimit); 328f6ef2e50SAndreas Gohr $this->printSources($sources); 3298817535bSAndreas Gohr } 3308817535bSAndreas Gohr 331c4584168SAndreas Gohr /** 3323379af09SAndreas Gohr * Run the maintenance tasks 3333379af09SAndreas Gohr * 3343379af09SAndreas Gohr * @return void 3353379af09SAndreas Gohr */ 3363379af09SAndreas Gohr protected function runMaintenance() 3373379af09SAndreas Gohr { 3383379af09SAndreas Gohr $start = time(); 3393379af09SAndreas Gohr $this->helper->getStorage()->runMaintenance(); 3403379af09SAndreas Gohr $this->notice('Peak memory used: {memory}', ['memory' => filesize_h(memory_get_peak_usage(true))]); 3413379af09SAndreas Gohr $this->notice('Spent time: {time}min', ['time' => round((time() - $start) / 60, 2)]); 342e75dc39fSAndreas Gohr 343e75dc39fSAndreas Gohr $data = $this->helper->getRunData(); 344*bae450a9SAndreas Gohr $data['maintenance ran at'] = dformat(); 345e75dc39fSAndreas Gohr $this->helper->setRunData($data); 3463379af09SAndreas Gohr } 3473379af09SAndreas Gohr 3483379af09SAndreas Gohr /** 349c4584168SAndreas Gohr * Recreate chunks and embeddings for all pages 350c4584168SAndreas Gohr * 351c4584168SAndreas Gohr * @return void 352c4584168SAndreas Gohr */ 3535284515dSAndreas Gohr protected function createEmbeddings($clear) 3548817535bSAndreas Gohr { 355d5c102b3SAndreas Gohr [$skipRE, $matchRE] = $this->getRegexps(); 356d5c102b3SAndreas Gohr 357*bae450a9SAndreas Gohr $data = $this->helper->getRunData(); 358*bae450a9SAndreas Gohr $lastEmbedModel = $data['embed used'] ?? ''; 359*bae450a9SAndreas Gohr 360*bae450a9SAndreas Gohr if( 361*bae450a9SAndreas Gohr !$clear && $lastEmbedModel && 362*bae450a9SAndreas Gohr $lastEmbedModel != (string) $this->helper->getEmbeddingModel() 363*bae450a9SAndreas Gohr ){ 364*bae450a9SAndreas Gohr $this->warning('Embedding model has changed since last run. Forcing an index rebuild'); 365*bae450a9SAndreas Gohr $clear = true; 366*bae450a9SAndreas Gohr } 367*bae450a9SAndreas Gohr 3683379af09SAndreas Gohr $start = time(); 369d5c102b3SAndreas Gohr $this->helper->getEmbeddings()->createNewIndex($skipRE, $matchRE, $clear); 370ad38c5fdSAndreas Gohr $this->notice('Peak memory used: {memory}', ['memory' => filesize_h(memory_get_peak_usage(true))]); 3713379af09SAndreas Gohr $this->notice('Spent time: {time}min', ['time' => round((time() - $start) / 60, 2)]); 372e75dc39fSAndreas Gohr 373*bae450a9SAndreas Gohr 374*bae450a9SAndreas Gohr $data['embed ran at'] = dformat(); 375*bae450a9SAndreas Gohr $data['embed used'] = (string) $this->helper->getEmbeddingModel(); 376e75dc39fSAndreas Gohr $this->helper->setRunData($data); 3778817535bSAndreas Gohr } 3788817535bSAndreas Gohr 379c4584168SAndreas Gohr /** 3808c8b7ba6SAndreas Gohr * Dump TSV files for debugging 3818c8b7ba6SAndreas Gohr * 3828c8b7ba6SAndreas Gohr * @return void 3838c8b7ba6SAndreas Gohr */ 3848c8b7ba6SAndreas Gohr protected function tsv($vector, $meta) 3858c8b7ba6SAndreas Gohr { 3868c8b7ba6SAndreas Gohr 3878c8b7ba6SAndreas Gohr $storage = $this->helper->getStorage(); 3888c8b7ba6SAndreas Gohr $storage->dumpTSV($vector, $meta); 3898c8b7ba6SAndreas Gohr $this->success('written to ' . $vector . ' and ' . $meta); 3908c8b7ba6SAndreas Gohr } 3918c8b7ba6SAndreas Gohr 3928c8b7ba6SAndreas Gohr /** 39355392016SAndreas Gohr * Print the given detailed answer in a nice way 39455392016SAndreas Gohr * 39555392016SAndreas Gohr * @param array $answer 39655392016SAndreas Gohr * @return void 39755392016SAndreas Gohr */ 39855392016SAndreas Gohr protected function printAnswer($answer) 39955392016SAndreas Gohr { 40055392016SAndreas Gohr $this->colors->ptln($answer['answer'], Colors::C_LIGHTCYAN); 40155392016SAndreas Gohr echo "\n"; 402f6ef2e50SAndreas Gohr $this->printSources($answer['sources']); 40355392016SAndreas Gohr echo "\n"; 40455392016SAndreas Gohr $this->printUsage(); 40555392016SAndreas Gohr } 40655392016SAndreas Gohr 40755392016SAndreas Gohr /** 408f6ef2e50SAndreas Gohr * Print the given sources 409f6ef2e50SAndreas Gohr * 410f6ef2e50SAndreas Gohr * @param Chunk[] $sources 411f6ef2e50SAndreas Gohr * @return void 412f6ef2e50SAndreas Gohr */ 413f6ef2e50SAndreas Gohr protected function printSources($sources) 414f6ef2e50SAndreas Gohr { 415f6ef2e50SAndreas Gohr foreach ($sources as $source) { 416f6ef2e50SAndreas Gohr /** @var Chunk $source */ 4179b3d1b36SAndreas Gohr $this->colors->ptln( 4189b3d1b36SAndreas Gohr "\t" . $source->getPage() . ' ' . $source->getId() . ' (' . $source->getScore() . ')', 4199b3d1b36SAndreas Gohr Colors::C_LIGHTBLUE 4209b3d1b36SAndreas Gohr ); 421f6ef2e50SAndreas Gohr } 422f6ef2e50SAndreas Gohr } 423f6ef2e50SAndreas Gohr 424f6ef2e50SAndreas Gohr /** 42555392016SAndreas Gohr * Print the usage statistics for OpenAI 42655392016SAndreas Gohr * 42755392016SAndreas Gohr * @return void 42855392016SAndreas Gohr */ 429f6ef2e50SAndreas Gohr protected function printUsage() 430f6ef2e50SAndreas Gohr { 43151aa8517SAndreas Gohr $chat = $this->helper->getChatModel()->getUsageStats(); 43251aa8517SAndreas Gohr $rephrase = $this->helper->getRephraseModel()->getUsageStats(); 433c2b7a1f7SAndreas Gohr $embed = $this->helper->getEmbeddingModel()->getUsageStats(); 43451aa8517SAndreas Gohr 43555392016SAndreas Gohr $this->info( 43651aa8517SAndreas Gohr 'Made {requests} requests in {time}s to models. Used {tokens} tokens for about ${cost}.', 43751aa8517SAndreas Gohr [ 43851aa8517SAndreas Gohr 'requests' => $chat['requests'] + $rephrase['requests'] + $embed['requests'], 43951aa8517SAndreas Gohr 'time' => $chat['time'] + $rephrase['time'] + $embed['time'], 44051aa8517SAndreas Gohr 'tokens' => $chat['tokens'] + $chat['tokens'] + $embed['tokens'], 44151aa8517SAndreas Gohr 'cost' => $chat['cost'] + $chat['cost'] + $embed['cost'], 44251aa8517SAndreas Gohr ] 44355392016SAndreas Gohr ); 44455392016SAndreas Gohr } 44555392016SAndreas Gohr 44655392016SAndreas Gohr /** 447c4584168SAndreas Gohr * Interactively ask for a value from the user 448c4584168SAndreas Gohr * 449c4584168SAndreas Gohr * @param string $prompt 450c4584168SAndreas Gohr * @return string 451c4584168SAndreas Gohr */ 452c4584168SAndreas Gohr protected function readLine($prompt) 453c4584168SAndreas Gohr { 454c4584168SAndreas Gohr $value = ''; 4558817535bSAndreas Gohr 456c4584168SAndreas Gohr while ($value === '') { 457c4584168SAndreas Gohr echo $prompt; 458c4584168SAndreas Gohr echo ': '; 459c4584168SAndreas Gohr 460c4584168SAndreas Gohr $fh = fopen('php://stdin', 'r'); 461c4584168SAndreas Gohr $value = trim(fgets($fh)); 462c4584168SAndreas Gohr fclose($fh); 463c4584168SAndreas Gohr } 464c4584168SAndreas Gohr 465c4584168SAndreas Gohr return $value; 466c4584168SAndreas Gohr } 467d5c102b3SAndreas Gohr 468d5c102b3SAndreas Gohr /** 469d5c102b3SAndreas Gohr * Read the skip and match regex from the config 470d5c102b3SAndreas Gohr * 471d5c102b3SAndreas Gohr * Ensures the regular expressions are valid 472d5c102b3SAndreas Gohr * 473d5c102b3SAndreas Gohr * @return string[] [$skipRE, $matchRE] 474d5c102b3SAndreas Gohr */ 475d5c102b3SAndreas Gohr protected function getRegexps() 476d5c102b3SAndreas Gohr { 477d5c102b3SAndreas Gohr $skip = $this->getConf('skipRegex'); 478d5c102b3SAndreas Gohr $skipRE = ''; 479d5c102b3SAndreas Gohr $match = $this->getConf('matchRegex'); 480d5c102b3SAndreas Gohr $matchRE = ''; 481d5c102b3SAndreas Gohr 482d5c102b3SAndreas Gohr if ($skip) { 483d5c102b3SAndreas Gohr $skipRE = '/' . $skip . '/'; 48449a7d3ccSsplitbrain if (@preg_match($skipRE, '') === false) { 485d5c102b3SAndreas Gohr $this->error(preg_last_error_msg()); 486d5c102b3SAndreas Gohr $this->error('Invalid regular expression in $conf[\'skipRegex\']. Ignored.'); 487d5c102b3SAndreas Gohr $skipRE = ''; 488d5c102b3SAndreas Gohr } else { 489d5c102b3SAndreas Gohr $this->success('Skipping pages matching ' . $skipRE); 490d5c102b3SAndreas Gohr } 491d5c102b3SAndreas Gohr } 492d5c102b3SAndreas Gohr 493d5c102b3SAndreas Gohr if ($match) { 494d5c102b3SAndreas Gohr $matchRE = '/' . $match . '/'; 49549a7d3ccSsplitbrain if (@preg_match($matchRE, '') === false) { 496d5c102b3SAndreas Gohr $this->error(preg_last_error_msg()); 497d5c102b3SAndreas Gohr $this->error('Invalid regular expression in $conf[\'matchRegex\']. Ignored.'); 498d5c102b3SAndreas Gohr $matchRE = ''; 499d5c102b3SAndreas Gohr } else { 500d5c102b3SAndreas Gohr $this->success('Only indexing pages matching ' . $matchRE); 501d5c102b3SAndreas Gohr } 502d5c102b3SAndreas Gohr } 503d5c102b3SAndreas Gohr return [$skipRE, $matchRE]; 504d5c102b3SAndreas Gohr } 5058817535bSAndreas Gohr} 506