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