xref: /plugin/aichat/helper.php (revision 9f6b34c4eab8e8478b8540a3880c4bbeacb98e9d)
1<?php
2
3use dokuwiki\plugin\aichat\Model\AbstractModel;
4use dokuwiki\plugin\aichat\Chunk;
5use dokuwiki\plugin\aichat\Embeddings;
6use dokuwiki\plugin\aichat\Model\OpenAI\GPT35Turbo;
7use dokuwiki\plugin\aichat\Storage\SQLiteStorage;
8
9require_once __DIR__ . '/vendor/autoload.php';
10
11/**
12 * DokuWiki Plugin aichat (Helper Component)
13 *
14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
15 * @author  Andreas Gohr <gohr@cosmocode.de>
16 */
17class helper_plugin_aichat extends \dokuwiki\Extension\Plugin
18{
19    /** @var AbstractModel */
20    protected $model;
21    /** @var Embeddings */
22    protected $embeddings;
23
24    /**
25     * Check if the current user is allowed to use the plugin (if it has been restricted)
26     *
27     * @return bool
28     */
29    public function userMayAccess()
30    {
31        global $auth;
32        global $USERINFO;
33        global $INPUT;
34
35        if (!$auth) return true;
36        if (!$this->getConf('restrict')) return true;
37        if (!isset($USERINFO)) return false;
38
39        return auth_isMember($this->getConf('restrict'), $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']);
40    }
41
42    /**
43     * Access the OpenAI client
44     *
45     * @return GPT35Turbo
46     */
47    public function getModel()
48    {
49        if ($this->model === null) {
50            $class = '\\dokuwiki\\plugin\\aichat\\Model\\' . $this->getConf('model');
51
52            if (!class_exists($class)) {
53                throw new \RuntimeException('Configured model not found: ' . $class);
54            }
55            // FIXME for now we only have OpenAI models, so we can hardcode the auth setup
56            $this->model = new $class([
57                'key' => $this->getConf('openaikey'),
58                'org' => $this->getConf('openaiorg')
59            ]);
60        }
61
62        return $this->model;
63    }
64
65    /**
66     * Access the Embeddings interface
67     *
68     * @return Embeddings
69     */
70    public function getEmbeddings()
71    {
72        if ($this->embeddings === null) {
73            // FIXME we currently have only one storage backend, so we can hardcode it
74            $this->embeddings = new Embeddings($this->getModel(), new SQLiteStorage());
75        }
76
77        return $this->embeddings;
78    }
79
80    /**
81     * Ask a question with a chat history
82     *
83     * @param string $question
84     * @param array[] $history The chat history [[user, ai], [user, ai], ...]
85     * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources]
86     * @throws Exception
87     */
88    public function askChatQuestion($question, $history = [])
89    {
90        if ($history) {
91            $standaloneQuestion = $this->rephraseChatQuestion($question, $history);
92        } else {
93            $standaloneQuestion = $question;
94        }
95        return $this->askQuestion($standaloneQuestion);
96    }
97
98    /**
99     * Ask a single standalone question
100     *
101     * @param string $question
102     * @return array ['question' => $question, 'answer' => $answer, 'sources' => $sources]
103     * @throws Exception
104     */
105    public function askQuestion($question)
106    {
107        $similar = $this->getEmbeddings()->getSimilarChunks($question);
108        if ($similar) {
109            $context = implode("\n", array_map(function (Chunk $chunk) {
110                return "\n```\n" . $chunk->getText() . "\n```\n";
111            }, $similar));
112            $prompt = $this->getPrompt('question', ['context' => $context]);
113        } else {
114            $prompt = $this->getPrompt('noanswer');
115        }
116
117        $messages = [
118            [
119                'role' => 'system',
120                'content' => $prompt
121            ],
122            [
123                'role' => 'user',
124                'content' => $question
125            ]
126        ];
127
128        $answer = $this->getModel()->getAnswer($messages);
129
130        return [
131            'question' => $question,
132            'answer' => $answer,
133            'sources' => $similar,
134        ];
135    }
136
137    /**
138     * Rephrase a question into a standalone question based on the chat history
139     *
140     * @param string $question The original user question
141     * @param array[] $history The chat history [[user, ai], [user, ai], ...]
142     * @return string The rephrased question
143     * @throws Exception
144     */
145    public function rephraseChatQuestion($question, $history)
146    {
147        // go back in history as far as possible without hitting the token limit
148        $chatHistory = '';
149        $history = array_reverse($history);
150        foreach ($history as $row) {
151            if (
152                count($this->getEmbeddings()->getTokenEncoder()->encode($chatHistory)) >
153                $this->getModel()->getMaxRephrasingTokenLength()
154            ) {
155                break;
156            }
157
158            $chatHistory =
159                "Human: " . $row[0] . "\n" .
160                "Assistant: " . $row[1] . "\n" .
161                $chatHistory;
162        }
163
164        // ask openAI to rephrase the question
165        $prompt = $this->getPrompt('rephrase', ['history' => $chatHistory, 'question' => $question]);
166        $messages = [['role' => 'user', 'content' => $prompt]];
167        return $this->getModel()->getRephrasedQuestion($messages);
168    }
169
170    /**
171     * Load the given prompt template and fill in the variables
172     *
173     * @param string $type
174     * @param string[] $vars
175     * @return string
176     */
177    protected function getPrompt($type, $vars = [])
178    {
179        $template = file_get_contents($this->localFN('prompt_' . $type));
180
181        $replace = array();
182        foreach ($vars as $key => $val) {
183            $replace['{{' . strtoupper($key) . '}}'] = $val;
184        }
185
186        return strtr($template, $replace);
187    }
188}
189
190