xref: /plugin/aichat/cli.php (revision 911314cdcf61977f3dcec01a4980522271a96e1c)
1<?php
2
3use dokuwiki\Extension\CLIPlugin;
4use dokuwiki\plugin\aichat\Chunk;
5use splitbrain\phpcli\Colors;
6use splitbrain\phpcli\Options;
7
8
9/**
10 * DokuWiki Plugin aichat (CLI Component)
11 *
12 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
13 * @author  Andreas Gohr <gohr@cosmocode.de>
14 */
15class cli_plugin_aichat extends CLIPlugin
16{
17    /** @var helper_plugin_aichat */
18    protected $helper;
19
20    public function __construct($autocatch = true)
21    {
22        parent::__construct($autocatch);
23        $this->helper = plugin_load('helper', 'aichat');
24        $this->helper->getEmbeddings()->setLogger($this);
25    }
26
27    /** @inheritDoc */
28    protected function setup(Options $options)
29    {
30        $options->useCompactHelp();
31
32        $options->setHelp(
33            'Manage and query the AI chatbot data. Please note that calls to your LLM provider will be made. ' .
34            'This may incur costs.'
35        );
36
37        $options->registerCommand(
38            'embed',
39            'Create embeddings for all pages. This skips pages that already have embeddings'
40        );
41        $options->registerOption(
42            'clear',
43            'Clear all existing embeddings before creating new ones',
44            'c', false, 'embed'
45        );
46
47        $options->registerCommand('similar', 'Search for similar pages');
48        $options->registerArgument('query', 'Look up chunks similar to this query', true, 'similar');
49
50        $options->registerCommand('ask', 'Ask a question');
51        $options->registerArgument('question', 'The question to ask', true, 'ask');
52
53        $options->registerCommand('chat', 'Start an interactive chat session');
54
55        $options->registerCommand('split', 'Split a page into chunks (for debugging)');
56        $options->registerArgument('page', 'The page to split', true, 'split');
57
58        $options->registerCommand('info', 'Get Info about the vector storage');
59    }
60
61    /** @inheritDoc */
62    protected function main(Options $options)
63    {
64        switch ($options->getCmd()) {
65
66            case 'embed':
67                $this->createEmbeddings($options->getOpt('clear'));
68                break;
69            case 'similar':
70                $this->similar($options->getArgs()[0]);
71                break;
72            case 'ask':
73                $this->ask($options->getArgs()[0]);
74                break;
75            case 'chat':
76                $this->chat();
77                break;
78            case 'split':
79                $this->split($options->getArgs()[0]);
80                break;
81            case 'info':
82                $this->showinfo();
83                break;
84            default:
85                echo $options->help();
86        }
87    }
88
89    /**
90     * @return void
91     */
92    protected function showinfo()
93    {
94        echo 'model: ' . $this->getConf('model') . "\n";
95        $stats = $this->helper->getEmbeddings()->getStorage()->statistics();
96        foreach ($stats as $key => $value) {
97            echo $key . ': ' . $value . "\n";
98        }
99
100        //echo $this->helper->getModel()->listUpstreamModels();
101    }
102
103    /**
104     * Split the given page into chunks and print them
105     *
106     * @param string $page
107     * @return void
108     * @throws Exception
109     */
110    protected function split($page)
111    {
112        $text = rawWiki($page);
113        $chunks = $this->helper->getEmbeddings()->splitIntoChunks($text);
114        foreach ($chunks as $chunk) {
115            echo $chunk;
116            echo "\n";
117            $this->colors->ptln('--------------------------------', Colors::C_LIGHTPURPLE);
118        }
119        $this->success('Split into ' . count($chunks) . ' chunks');
120    }
121
122    /**
123     * Interactive Chat Session
124     *
125     * @return void
126     * @throws Exception
127     */
128    protected function chat()
129    {
130        $history = [];
131        while ($q = $this->readLine('Your Question')) {
132            $this->helper->getModel()->resetUsageStats();
133            $result = $this->helper->askChatQuestion($q, $history);
134            $this->colors->ptln("Interpretation: {$result['question']}", Colors::C_LIGHTPURPLE);
135            $history[] = [$result['question'], $result['answer']];
136            $this->printAnswer($result);
137        }
138    }
139
140    /**
141     * Handle a single, standalone question
142     *
143     * @param string $query
144     * @return void
145     * @throws Exception
146     */
147    protected function ask($query)
148    {
149        $result = $this->helper->askQuestion($query);
150        $this->printAnswer($result);
151    }
152
153    /**
154     * Get the pages that are similar to the query
155     *
156     * @param string $query
157     * @return void
158     */
159    protected function similar($query)
160    {
161        $sources = $this->helper->getEmbeddings()->getSimilarChunks($query);
162        $this->printSources($sources);
163    }
164
165    /**
166     * Recreate chunks and embeddings for all pages
167     *
168     * @return void
169     * @todo make skip regex configurable
170     */
171    protected function createEmbeddings($clear)
172    {
173        ini_set('memory_limit', -1); // we may need a lot of memory here
174        $this->helper->getEmbeddings()->createNewIndex('/(^|:)(playground|sandbox)(:|$)/', $clear);
175        $this->notice('Peak memory used: {memory}', ['memory' => filesize_h(memory_get_peak_usage(true))]);
176    }
177
178    /**
179     * Print the given detailed answer in a nice way
180     *
181     * @param array $answer
182     * @return void
183     */
184    protected function printAnswer($answer)
185    {
186        $this->colors->ptln($answer['answer'], Colors::C_LIGHTCYAN);
187        echo "\n";
188        $this->printSources($answer['sources']);
189        echo "\n";
190        $this->printUsage();
191    }
192
193    /**
194     * Print the given sources
195     *
196     * @param Chunk[] $sources
197     * @return void
198     */
199    protected function printSources($sources)
200    {
201        foreach ($sources as $source) {
202            /** @var Chunk $source */
203            $this->colors->ptln(
204                "\t" . $source->getPage() . ' ' . $source->getId() . ' (' . $source->getScore() . ')',
205                Colors::C_LIGHTBLUE
206            );
207        }
208    }
209
210    /**
211     * Print the usage statistics for OpenAI
212     *
213     * @return void
214     */
215    protected function printUsage()
216    {
217        $this->info(
218            'Made {requests} requests in {time}s to Model. Used {tokens} tokens for about ${cost}.',
219            $this->helper->getModel()->getUsageStats()
220        );
221    }
222
223    /**
224     * Interactively ask for a value from the user
225     *
226     * @param string $prompt
227     * @return string
228     */
229    protected function readLine($prompt)
230    {
231        $value = '';
232
233        while ($value === '') {
234            echo $prompt;
235            echo ': ';
236
237            $fh = fopen('php://stdin', 'r');
238            $value = trim(fgets($fh));
239            fclose($fh);
240        }
241
242        return $value;
243    }
244}
245
246