xref: /plugin/aichat/helper.php (revision 553920162c56b922c6ae5be71ff4442e666a63d3)
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