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