xref: /plugin/aichat/Model/AbstractModel.php (revision 34a1c47875552330ce367360d99f2c3f9f69af94)
1<?php
2
3namespace dokuwiki\plugin\aichat\Model;
4
5use dokuwiki\HTTP\DokuHTTPClient;
6
7/**
8 * Base class for all models
9 *
10 * Model classes also need to implement one of the following interfaces:
11 * - ChatInterface
12 * - EmbeddingInterface
13 */
14abstract class AbstractModel
15{
16    /** @var bool debug API communication */
17    protected $debug = false;
18
19
20    protected $inputTokensUsed = 0;
21    protected $outputTokensUsed = 0;
22    protected $tokensUsed = 0;
23
24    /** @var int total time spent in requests by this instance */
25    protected $timeUsed = 0;
26    /** @var int total number of requests made by this instance */
27    protected $requestsMade = 0;
28    /** @var int How often to retry a request if it fails */
29    public const MAX_RETRIES = 3;
30    /** @var DokuHTTPClient */
31    protected $http;
32    /** @var int start time of the current request chain (may be multiple when retries needed) */
33    protected $requestStart = 0;
34
35    /**
36     * This initializes a HTTP client
37     *
38     * Implementors should override this and authenticate the client.
39     *
40     * @param array $config The plugin configuration
41     */
42    public function __construct(array $config)
43    {
44        $this->http = new DokuHTTPClient();
45        $this->http->timeout = 60;
46        $this->http->headers['Content-Type'] = 'application/json';
47    }
48
49    /**
50     * When enabled, the input/output of the API will be printed to STDOUT
51     *
52     * @param bool $debug
53     */
54    public function setDebug($debug = true)
55    {
56        $this->debug = $debug;
57    }
58
59    /**
60     * This method should check the response for any errors. If the API singalled an error,
61     * this method should throw an Exception with a meaningful error message.
62     *
63     * If the response returned any info on used tokens, they should be added to $this->tokensUsed
64     *
65     * The method should return the parsed response, which will be passed to the calling method.
66     *
67     * @param mixed $response the parsed JSON response from the API
68     * @return mixed
69     * @throws \Exception when the response indicates an error
70     */
71    abstract protected function parseAPIResponse($response);
72
73    /**
74     * Send a request to the API
75     *
76     * Model classes should use this method to send requests to the API.
77     *
78     * This method will take care of retrying and logging basic statistics.
79     *
80     * It is assumed that all APIs speak JSON.
81     *
82     * @param string $method The HTTP method to use (GET, POST, PUT, DELETE, etc.)
83     * @param string $url The full URL to send the request to
84     * @param array $data Payload to send, will be encoded to JSON
85     * @param int $retry How often this request has been retried, do not set externally
86     * @return array API response as returned by parseAPIResponse
87     * @throws \Exception when anything goes wrong
88     */
89    protected function sendAPIRequest($method, $url, $data, $retry = 0)
90    {
91        // init statistics
92        if ($retry === 0) {
93            $this->requestStart = microtime(true);
94        } else {
95            sleep($retry); // wait a bit between retries
96        }
97        $this->requestsMade++;
98
99        // encode payload data
100        try {
101            $json = json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
102        } catch (\JsonException $e) {
103            $this->timeUsed += $this->requestStart - microtime(true);
104            throw new \Exception('Failed to encode JSON for API:' . $e->getMessage(), $e->getCode(), $e);
105        }
106
107        if ($this->debug) {
108            echo 'Sending ' . $method . ' request to ' . $url . ' with payload:' . "\n";
109            print_r($json);
110        }
111
112        // send request and handle retries
113        $this->http->sendRequest($url, $json, $method);
114        $response = $this->http->resp_body;
115        if ($response === false || $this->http->error) {
116            if ($retry < self::MAX_RETRIES) {
117                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
118            }
119            $this->timeUsed += microtime(true) - $this->requestStart;
120            throw new \Exception('API returned no response. ' . $this->http->error);
121        }
122
123        if ($this->debug) {
124            echo 'Received response:' . "\n";
125            print_r($response);
126        }
127
128        // decode the response
129        try {
130            $result = json_decode((string)$response, true, 512, JSON_THROW_ON_ERROR);
131        } catch (\JsonException $e) {
132            $this->timeUsed += microtime(true) - $this->requestStart;
133            throw new \Exception('API returned invalid JSON: ' . $response, 0, $e);
134        }
135
136        // parse the response, retry on error
137        try {
138            $result = $this->parseAPIResponse($result);
139        } catch (\Exception $e) {
140            if ($retry < self::MAX_RETRIES) {
141                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
142            }
143            $this->timeUsed += microtime(true) - $this->requestStart;
144            throw $e;
145        }
146
147        $this->timeUsed += microtime(true) - $this->requestStart;
148        return $result;
149    }
150
151    /**
152     * Reset the usage statistics
153     *
154     * Usually not needed when only handling one operation per request, but useful in CLI
155     */
156    public function resetUsageStats()
157    {
158        $this->tokensUsed = 0;
159        $this->timeUsed = 0;
160        $this->requestsMade = 0;
161    }
162
163    /**
164     * Get the usage statistics for this instance
165     *
166     * @return string[]
167     */
168    public function getUsageStats()
169    {
170
171        $cost = 0;
172        $cost += $this->inputTokensUsed * $this->getInputTokenPrice();
173        if ($this instanceof ChatInterface) {
174            $cost += $this->outputTokensUsed * $this->getOutputTokenPrice();
175        }
176
177        return [
178            'tokens' => $this->tokensUsed + $this->inputTokensUsed + $this->outputTokensUsed,
179            'cost' => round($cost / 1_000_000, 4),
180            'time' => round($this->timeUsed, 2),
181            'requests' => $this->requestsMade,
182        ];
183    }
184}
185