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 public function __construct() 25 { 26 $class = '\\dokuwiki\\plugin\\aichat\\Model\\' . $this->getConf('model'); 27 28 if (class_exists($class)) { 29 // FIXME for now we only have OpenAI models, so we can hardcode the auth setup 30 $this->model = new $class([ 31 'key' => $this->getConf('openaikey'), 32 'org' => $this->getConf('openaiorg') 33 ]); 34 } else { 35 throw new \Exception('Configured model not found: ' . $class); 36 } 37 38 // FIXME we currently have only one storage backend, so we can hardcode it 39 $this->embeddings = new Embeddings($this->model, new SQLiteStorage()); 40 } 41 42 /** 43 * Check if the current user is allowed to use the plugin (if it has been restricted) 44 * 45 * @return bool 46 */ 47 public function userMayAccess() 48 { 49 global $auth; 50 global $USERINFO; 51 global $INPUT; 52 53 if (!$auth) return true; 54 if (!$this->getConf('restrict')) return true; 55 if (!isset($USERINFO)) return false; 56 57 return auth_isMember($this->getConf('restrict'), $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']); 58 } 59 60 /** 61 * Access the OpenAI client 62 * 63 * @return GPT35Turbo 64 */ 65 public function getModel() 66 { 67 return $this->model; 68 } 69 70 /** 71 * Access the Embeddings interface 72 * 73 * @return Embeddings 74 */ 75 public function getEmbeddings() 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->embeddings->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->model->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->embeddings->getTokenEncoder()->encode($chatHistory)) > 153 $this->model->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->model->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