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 $context = implode("\n", array_column($similar, 'text')); 77 78 $prompt = $this->getPrompt('question', ['context' => $context]); 79 $messages = [ 80 [ 81 'role' => 'system', 82 'content' => $prompt 83 ], 84 [ 85 'role' => 'user', 86 'content' => $question 87 ] 88 ]; 89 90 $answer = $this->openAI->getChatAnswer($messages); 91 92 return [ 93 'question' => $question, 94 'answer' => $answer, 95 'sources' => $similar, 96 ]; 97 } 98 99 /** 100 * Rephrase a question into a standalone question based on the chat history 101 * 102 * @param string $question The original user question 103 * @param array[] $history The chat history [[user, ai], [user, ai], ...] 104 * @return string The rephrased question 105 * @throws Exception 106 */ 107 public function rephraseChatQuestion($question, $history) 108 { 109 // go back in history as far as possible without hitting the token limit 110 $tiktok = new Encoder(); 111 $chatHistory = ''; 112 $history = array_reverse($history); 113 foreach ($history as $row) { 114 if (count($tiktok->encode($chatHistory)) > 3000) { 115 break; 116 } 117 118 $chatHistory = 119 "Human: " . $row[0] . "\n" . 120 "Assistant: " . $row[1] . "\n" . 121 $chatHistory; 122 } 123 124 // ask openAI to rephrase the question 125 $prompt = $this->getPrompt('rephrase', ['history' => $chatHistory, 'question' => $question]); 126 $messages = [['role' => 'user', 'content' => $prompt]]; 127 return $this->openAI->getChatAnswer($messages); 128 } 129 130 /** 131 * Load the given prompt template and fill in the variables 132 * 133 * @param string $type 134 * @param string[] $vars 135 * @return string 136 */ 137 protected function getPrompt($type, $vars = []) 138 { 139 $template = file_get_contents($this->localFN('prompt_' . $type)); 140 141 $replace = array(); 142 foreach ($vars as $key => $val) { 143 $replace['{{' . strtoupper($key) . '}}'] = $val; 144 } 145 146 return strtr($template, $replace); 147 } 148} 149 150