1<?php
2
3namespace dokuwiki\plugin\aichat;
4
5use dokuwiki\plugin\aichat\Model\ChatInterface;
6use dokuwiki\plugin\aichat\Model\EmbeddingInterface;
7
8class ModelFactory
9{
10    /** @var array The plugin configuration */
11    protected array $config;
12
13    public $chatModel;
14    public $rephraseModel;
15    public $embeddingModel;
16
17    protected $debug = false;
18
19    /**
20     * @param array $config The plugin configuration
21     */
22    public function __construct(array $config)
23    {
24        $this->config = $config;
25    }
26
27    /**
28     * Update the configuration and reset the cached models
29     *
30     * @param array $config The new (partial) configuration
31     */
32    public function updateConfig(array $config)
33    {
34        $this->config = array_merge($this->config, $config);
35        $this->chatModel = null;
36        $this->rephraseModel = null;
37        $this->embeddingModel = null;
38    }
39
40    /**
41     * Set the debug flag for all models
42     *
43     * @param bool $debug
44     */
45    public function setDebug(bool $debug = true)
46    {
47        $this->debug = $debug;
48        $this->getChatModel()->setDebug($debug);
49        $this->getRephraseModel()->setDebug($debug);
50        $this->getEmbeddingModel()->setDebug($debug);
51    }
52
53    /**
54     * Access a cached Chat Model
55     *
56     * @return ChatInterface
57     * @throws \Exception
58     */
59    public function getChatModel()
60    {
61        if ($this->chatModel instanceof ChatInterface) {
62            return $this->chatModel;
63        }
64        $this->chatModel = $this->loadModel('chat', $this->config['chatmodel']);
65        $this->chatModel->setDebug($this->debug);
66        return $this->chatModel;
67    }
68
69    /**
70     * Access a cached Rephrase Model
71     *
72     * @return ChatInterface
73     * @throws \Exception
74     */
75    public function getRephraseModel()
76    {
77        if ($this->rephraseModel instanceof ChatInterface) {
78            return $this->rephraseModel;
79        }
80        $this->rephraseModel = $this->loadModel('chat', $this->config['chatmodel']);
81        $this->rephraseModel->setDebug($this->debug);
82        return $this->rephraseModel;
83    }
84
85    /**
86     * Access a cached Embedding Model
87     *
88     * @return EmbeddingInterface
89     */
90    public function getEmbeddingModel()
91    {
92        if ($this->embeddingModel instanceof EmbeddingInterface) {
93            return $this->embeddingModel;
94        }
95        $this->embeddingModel = $this->loadModel('embedding', $this->config['embedmodel']);
96        $this->embeddingModel->setDebug($this->debug);
97        return $this->embeddingModel;
98    }
99
100    /**
101     * Get all known models
102     *
103     * A (new) instance is returned for each model that is available through the current configuration.
104     *
105     * @param bool $availableOnly Only return models that are available
106     * @param string $typeOnly Only return models of this type ('chat' or 'embedding')
107     * @return array
108     */
109    public function getModels($availableOnly = false, $typeOnly = '')
110    {
111        $result = [
112            'chat' => [],
113            'embedding' => [],
114        ];
115
116        $jsons = glob(__DIR__ . '/Model/*/models.json');
117        foreach ($jsons as $json) {
118            $models = json_decode(file_get_contents($json), true);
119            foreach ($models as $type => $model) {
120                $namespace = basename(dirname($json));
121                foreach ($model as $name => $info) {
122                    try {
123                        $info['instance'] = $this->loadModel($type, "$namespace $name");
124                        $info['instance']->setDebug($this->debug);
125                    } catch (\Exception $e) {
126                        if ($availableOnly) continue;
127                        $info['instance'] = false;
128                    }
129
130                    $result[$type]["$namespace $name"] = $info;
131                }
132            }
133        }
134
135        return $typeOnly ? $result[$typeOnly] : $result;
136    }
137
138
139    /**
140     * Initialize a model by config name
141     *
142     * @param string $type 'chat' or 'embedding'
143     * @param string $name The full model name including provider
144     * @return ChatInterface|EmbeddingInterface
145     * @throws \Exception
146     */
147    public function loadModel(string $type, string $name)
148    {
149        $type = ucfirst(strtolower($type));
150        $prefix = '\\dokuwiki\\plugin\\aichat\\Model\\';
151        $cname = $type . 'Model';
152        $interface = $prefix . $type . 'Interface';
153
154
155        [$namespace, $model] = sexplode(' ', $name, 2, '');
156        $class = $prefix . $namespace . '\\' . $cname;
157
158        if (!class_exists($class)) {
159            throw new \Exception("No $cname found for $namespace");
160        }
161
162        try {
163            $instance = new $class($model, $this->config);
164        } catch (\Exception $e) {
165            throw new \Exception("Failed to initialize $cname for $namespace: " . $e->getMessage(), 0, $e);
166        }
167
168        if (!($instance instanceof $interface)) {
169            throw new \Exception("$cname for $namespace does not implement $interface");
170        }
171
172        return $instance;
173    }
174}
175