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