1<?php 2 3use dokuwiki\plugin\aichat\Model\AbstractModel; 4use dokuwiki\plugin\aichat\Chunk; 5use dokuwiki\plugin\aichat\Embeddings; 6use dokuwiki\plugin\aichat\Model\OpenAI\GPT35Turbo; 7use dokuwiki\plugin\aichat\Storage\AbstractStorage; 8use dokuwiki\plugin\aichat\Storage\SQLiteStorage; 9 10require_once __DIR__ . '/vendor/autoload.php'; 11 12/** 13 * DokuWiki Plugin aichat (Helper Component) 14 * 15 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 16 * @author Andreas Gohr <gohr@cosmocode.de> 17 */ 18class helper_plugin_aichat extends \dokuwiki\Extension\Plugin 19{ 20 /** @var AbstractModel */ 21 protected $model; 22 /** @var Embeddings */ 23 protected $embeddings; 24 /** @var AbstractStorage */ 25 protected $storage; 26 27 /** 28 * Check if the current user is allowed to use the plugin (if it has been restricted) 29 * 30 * @return bool 31 */ 32 public function userMayAccess() 33 { 34 global $auth; 35 global $USERINFO; 36 global $INPUT; 37 38 if (!$auth) return true; 39 if (!$this->getConf('restrict')) return true; 40 if (!isset($USERINFO)) return false; 41 42 return auth_isMember($this->getConf('restrict'), $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']); 43 } 44 45 /** 46 * Access the OpenAI client 47 * 48 * @return GPT35Turbo 49 */ 50 public function getModel() 51 { 52 if ($this->model === null) { 53 $class = '\\dokuwiki\\plugin\\aichat\\Model\\' . $this->getConf('model'); 54 55 if (!class_exists($class)) { 56 throw new \RuntimeException('Configured model not found: ' . $class); 57 } 58 // FIXME for now we only have OpenAI models, so we can hardcode the auth setup 59 $this->model = new $class([ 60 'key' => $this->getConf('openaikey'), 61 'org' => $this->getConf('openaiorg') 62 ]); 63 } 64 65 return $this->model; 66 } 67 68 /** 69 * Access the Embeddings interface 70 * 71 * @return Embeddings 72 */ 73 public function getEmbeddings() 74 { 75 if ($this->embeddings === null) { 76 // FIXME we currently have only one storage backend, so we can hardcode it 77 $this->embeddings = new Embeddings($this->getModel(), $this->getStorage()); 78 } 79 80 return $this->embeddings; 81 } 82 83 /** 84 * Access the Storage interface 85 * 86 * @return AbstractStorage 87 */ 88 public function getStorage() 89 { 90 if ($this->storage === null) { 91 $this->storage = new SQLiteStorage(); 92 } 93 94 return $this->storage; 95 } 96 97 /** 98 * Ask a question with a chat history 99 * 100 * @param string $question 101 * @param array[] $history The chat history [[user, ai], [user, ai], ...] 102 * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 103 * @throws Exception 104 */ 105 public function askChatQuestion($question, $history = []) 106 { 107 if ($history) { 108 $standaloneQuestion = $this->rephraseChatQuestion($question, $history); 109 } else { 110 $standaloneQuestion = $question; 111 } 112 return $this->askQuestion($standaloneQuestion); 113 } 114 115 /** 116 * Ask a single standalone question 117 * 118 * @param string $question 119 * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 120 * @throws Exception 121 */ 122 public function askQuestion($question) 123 { 124 $similar = $this->getEmbeddings()->getSimilarChunks($question); 125 if ($similar) { 126 $context = implode("\n", array_map(function (Chunk $chunk) { 127 return "\n```\n" . $chunk->getText() . "\n```\n"; 128 }, $similar)); 129 $prompt = $this->getPrompt('question', ['context' => $context]); 130 } else { 131 $prompt = $this->getPrompt('noanswer'); 132 } 133 134 $messages = [ 135 [ 136 'role' => 'system', 137 'content' => $prompt 138 ], 139 [ 140 'role' => 'user', 141 'content' => $question 142 ] 143 ]; 144 145 $answer = $this->getModel()->getAnswer($messages); 146 147 return [ 148 'question' => $question, 149 'answer' => $answer, 150 'sources' => $similar, 151 ]; 152 } 153 154 /** 155 * Rephrase a question into a standalone question based on the chat history 156 * 157 * @param string $question The original user question 158 * @param array[] $history The chat history [[user, ai], [user, ai], ...] 159 * @return string The rephrased question 160 * @throws Exception 161 */ 162 public function rephraseChatQuestion($question, $history) 163 { 164 // go back in history as far as possible without hitting the token limit 165 $chatHistory = ''; 166 $history = array_reverse($history); 167 foreach ($history as $row) { 168 if ( 169 count($this->getEmbeddings()->getTokenEncoder()->encode($chatHistory)) > 170 $this->getModel()->getMaxRephrasingTokenLength() 171 ) { 172 break; 173 } 174 175 $chatHistory = 176 "Human: " . $row[0] . "\n" . 177 "Assistant: " . $row[1] . "\n" . 178 $chatHistory; 179 } 180 181 // ask openAI to rephrase the question 182 $prompt = $this->getPrompt('rephrase', ['history' => $chatHistory, 'question' => $question]); 183 $messages = [['role' => 'user', 'content' => $prompt]]; 184 return $this->getModel()->getRephrasedQuestion($messages); 185 } 186 187 /** 188 * Load the given prompt template and fill in the variables 189 * 190 * @param string $type 191 * @param string[] $vars 192 * @return string 193 */ 194 protected function getPrompt($type, $vars = []) 195 { 196 $template = file_get_contents($this->localFN('prompt_' . $type)); 197 198 $replace = array(); 199 foreach ($vars as $key => $val) { 200 $replace['{{' . strtoupper($key) . '}}'] = $val; 201 } 202 203 return strtr($template, $replace); 204 } 205} 206 207