1<?php 2 3use dokuwiki\Extension\CLIPlugin; 4use dokuwiki\plugin\aichat\Chunk; 5use splitbrain\phpcli\Colors; 6use splitbrain\phpcli\Options; 7 8 9/** 10 * DokuWiki Plugin aichat (CLI Component) 11 * 12 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 13 * @author Andreas Gohr <gohr@cosmocode.de> 14 */ 15class cli_plugin_aichat extends CLIPlugin 16{ 17 /** @var helper_plugin_aichat */ 18 protected $helper; 19 20 public function __construct($autocatch = true) 21 { 22 parent::__construct($autocatch); 23 $this->helper = plugin_load('helper', 'aichat'); 24 $this->helper->getEmbeddings()->setLogger($this); 25 } 26 27 /** @inheritDoc */ 28 protected function setup(Options $options) 29 { 30 $options->useCompactHelp(); 31 32 $options->setHelp( 33 'Manage and query the AI chatbot data. Please note that calls to your LLM provider will be made. ' . 34 'This may incur costs.' 35 ); 36 37 $options->registerCommand( 38 'embed', 39 'Create embeddings for all pages. This skips pages that already have embeddings' 40 ); 41 $options->registerOption( 42 'clear', 43 'Clear all existing embeddings before creating new ones', 44 'c', false, 'embed' 45 ); 46 47 $options->registerCommand('similar', 'Search for similar pages'); 48 $options->registerArgument('query', 'Look up chunks similar to this query', true, 'similar'); 49 50 $options->registerCommand('ask', 'Ask a question'); 51 $options->registerArgument('question', 'The question to ask', true, 'ask'); 52 53 $options->registerCommand('chat', 'Start an interactive chat session'); 54 55 $options->registerCommand('split', 'Split a page into chunks (for debugging)'); 56 $options->registerArgument('page', 'The page to split', true, 'split'); 57 58 $options->registerCommand('info', 'Get Info about the vector storage'); 59 } 60 61 /** @inheritDoc */ 62 protected function main(Options $options) 63 { 64 switch ($options->getCmd()) { 65 66 case 'embed': 67 $this->createEmbeddings($options->getOpt('clear')); 68 break; 69 case 'similar': 70 $this->similar($options->getArgs()[0]); 71 break; 72 case 'ask': 73 $this->ask($options->getArgs()[0]); 74 break; 75 case 'chat': 76 $this->chat(); 77 break; 78 case 'split': 79 $this->split($options->getArgs()[0]); 80 break; 81 case 'info': 82 $this->showinfo(); 83 break; 84 default: 85 echo $options->help(); 86 } 87 } 88 89 /** 90 * @return void 91 */ 92 protected function showinfo() 93 { 94 echo 'model: ' . $this->getConf('model') . "\n"; 95 $stats = $this->helper->getEmbeddings()->getStorage()->statistics(); 96 foreach ($stats as $key => $value) { 97 echo $key . ': ' . $value . "\n"; 98 } 99 100 //echo $this->helper->getModel()->listUpstreamModels(); 101 } 102 103 /** 104 * Split the given page into chunks and print them 105 * 106 * @param string $page 107 * @return void 108 * @throws Exception 109 */ 110 protected function split($page) 111 { 112 $text = rawWiki($page); 113 $chunks = $this->helper->getEmbeddings()->splitIntoChunks($text); 114 foreach ($chunks as $chunk) { 115 echo $chunk; 116 echo "\n"; 117 $this->colors->ptln('--------------------------------', Colors::C_LIGHTPURPLE); 118 } 119 $this->success('Split into ' . count($chunks) . ' chunks'); 120 } 121 122 /** 123 * Interactive Chat Session 124 * 125 * @return void 126 * @throws Exception 127 */ 128 protected function chat() 129 { 130 $history = []; 131 while ($q = $this->readLine('Your Question')) { 132 $this->helper->getModel()->resetUsageStats(); 133 $result = $this->helper->askChatQuestion($q, $history); 134 $this->colors->ptln("Interpretation: {$result['question']}", Colors::C_LIGHTPURPLE); 135 $history[] = [$result['question'], $result['answer']]; 136 $this->printAnswer($result); 137 } 138 } 139 140 /** 141 * Handle a single, standalone question 142 * 143 * @param string $query 144 * @return void 145 * @throws Exception 146 */ 147 protected function ask($query) 148 { 149 $result = $this->helper->askQuestion($query); 150 $this->printAnswer($result); 151 } 152 153 /** 154 * Get the pages that are similar to the query 155 * 156 * @param string $query 157 * @return void 158 */ 159 protected function similar($query) 160 { 161 $sources = $this->helper->getEmbeddings()->getSimilarChunks($query); 162 $this->printSources($sources); 163 } 164 165 /** 166 * Recreate chunks and embeddings for all pages 167 * 168 * @return void 169 * @todo make skip regex configurable 170 */ 171 protected function createEmbeddings($clear) 172 { 173 ini_set('memory_limit', -1); // we may need a lot of memory here 174 $this->helper->getEmbeddings()->createNewIndex('/(^|:)(playground|sandbox)(:|$)/', $clear); 175 $this->notice('Peak memory used: {memory}', ['memory' => filesize_h(memory_get_peak_usage(true))]); 176 } 177 178 /** 179 * Print the given detailed answer in a nice way 180 * 181 * @param array $answer 182 * @return void 183 */ 184 protected function printAnswer($answer) 185 { 186 $this->colors->ptln($answer['answer'], Colors::C_LIGHTCYAN); 187 echo "\n"; 188 $this->printSources($answer['sources']); 189 echo "\n"; 190 $this->printUsage(); 191 } 192 193 /** 194 * Print the given sources 195 * 196 * @param Chunk[] $sources 197 * @return void 198 */ 199 protected function printSources($sources) 200 { 201 foreach ($sources as $source) { 202 /** @var Chunk $source */ 203 $this->colors->ptln( 204 "\t" . $source->getPage() . ' ' . $source->getId() . ' (' . $source->getScore() . ')', 205 Colors::C_LIGHTBLUE 206 ); 207 } 208 } 209 210 /** 211 * Print the usage statistics for OpenAI 212 * 213 * @return void 214 */ 215 protected function printUsage() 216 { 217 $this->info( 218 'Made {requests} requests in {time}s to Model. Used {tokens} tokens for about ${cost}.', 219 $this->helper->getModel()->getUsageStats() 220 ); 221 } 222 223 /** 224 * Interactively ask for a value from the user 225 * 226 * @param string $prompt 227 * @return string 228 */ 229 protected function readLine($prompt) 230 { 231 $value = ''; 232 233 while ($value === '') { 234 echo $prompt; 235 echo ': '; 236 237 $fh = fopen('php://stdin', 'r'); 238 $value = trim(fgets($fh)); 239 fclose($fh); 240 } 241 242 return $value; 243 } 244} 245 246