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