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