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