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 * Access the OpenAI client 31 * 32 * @return OpenAI 33 */ 34 public function getOpenAI() 35 { 36 return $this->openAI; 37 } 38 39 /** 40 * Access the Embeddings interface 41 * 42 * @return Embeddings 43 */ 44 public function getEmbeddings() 45 { 46 return $this->embeddings; 47 } 48 49 /** 50 * Ask a question with a chat history 51 * 52 * @param string $question 53 * @param array[] $history The chat history [[user, ai], [user, ai], ...] 54 * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 55 * @throws Exception 56 */ 57 public function askChatQuestion($question, $history = []) 58 { 59 if ($history) { 60 $standaloneQuestion = $this->rephraseChatQuestion($question, $history); 61 } else { 62 $standaloneQuestion = $question; 63 } 64 return $this->askQuestion($standaloneQuestion); 65 } 66 67 /** 68 * Ask a single standalone question 69 * 70 * @param string $question 71 * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources] 72 * @throws Exception 73 */ 74 public function askQuestion($question) 75 { 76 $similar = $this->embeddings->getSimilarChunks($question); 77 78 if ($similar) { 79 $context = implode("\n", array_map(function (Chunk $chunk) { 80 return $chunk->getText(); 81 }, $similar)); 82 $prompt = $this->getPrompt('question', ['context' => $context]); 83 } else { 84 $prompt = $this->getPrompt('noanswer'); 85 } 86 $messages = [ 87 [ 88 'role' => 'system', 89 'content' => $prompt 90 ], 91 [ 92 'role' => 'user', 93 'content' => $question 94 ] 95 ]; 96 97 $answer = $this->openAI->getChatAnswer($messages); 98 99 return [ 100 'question' => $question, 101 'answer' => $answer, 102 'sources' => $similar, 103 ]; 104 } 105 106 /** 107 * Rephrase a question into a standalone question based on the chat history 108 * 109 * @param string $question The original user question 110 * @param array[] $history The chat history [[user, ai], [user, ai], ...] 111 * @return string The rephrased question 112 * @throws Exception 113 */ 114 public function rephraseChatQuestion($question, $history) 115 { 116 // go back in history as far as possible without hitting the token limit 117 $tiktok = new Encoder(); 118 $chatHistory = ''; 119 $history = array_reverse($history); 120 foreach ($history as $row) { 121 if (count($tiktok->encode($chatHistory)) > 3000) { 122 break; 123 } 124 125 $chatHistory = 126 "Human: " . $row[0] . "\n" . 127 "Assistant: " . $row[1] . "\n" . 128 $chatHistory; 129 } 130 131 // ask openAI to rephrase the question 132 $prompt = $this->getPrompt('rephrase', ['history' => $chatHistory, 'question' => $question]); 133 $messages = [['role' => 'user', 'content' => $prompt]]; 134 return $this->openAI->getChatAnswer($messages); 135 } 136 137 /** 138 * Load the given prompt template and fill in the variables 139 * 140 * @param string $type 141 * @param string[] $vars 142 * @return string 143 */ 144 protected function getPrompt($type, $vars = []) 145 { 146 $template = file_get_contents($this->localFN('prompt_' . $type)); 147 148 $replace = array(); 149 foreach ($vars as $key => $val) { 150 $replace['{{' . strtoupper($key) . '}}'] = $val; 151 } 152 153 return strtr($template, $replace); 154 } 155} 156 157