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