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