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