xref: /plugin/aichat/Model/AbstractModel.php (revision 34a1c47875552330ce367360d99f2c3f9f69af94)
1f6ef2e50SAndreas Gohr<?php
2f6ef2e50SAndreas Gohr
3f6ef2e50SAndreas Gohrnamespace dokuwiki\plugin\aichat\Model;
4f6ef2e50SAndreas Gohr
5294a9eafSAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient;
6294a9eafSAndreas Gohr
7294a9eafSAndreas Gohr/**
8294a9eafSAndreas Gohr * Base class for all models
9294a9eafSAndreas Gohr *
10294a9eafSAndreas Gohr * Model classes also need to implement one of the following interfaces:
11294a9eafSAndreas Gohr * - ChatInterface
12294a9eafSAndreas Gohr * - EmbeddingInterface
13294a9eafSAndreas Gohr */
147ebc7895Ssplitbrainabstract class AbstractModel
157ebc7895Ssplitbrain{
16*34a1c478SAndreas Gohr    /** @var bool debug API communication */
17*34a1c478SAndreas Gohr    protected $debug = false;
18*34a1c478SAndreas Gohr
19*34a1c478SAndreas Gohr
20*34a1c478SAndreas Gohr    protected $inputTokensUsed = 0;
21*34a1c478SAndreas Gohr    protected $outputTokensUsed = 0;
22f6ef2e50SAndreas Gohr    protected $tokensUsed = 0;
23*34a1c478SAndreas Gohr
24f6ef2e50SAndreas Gohr    /** @var int total time spent in requests by this instance */
25f6ef2e50SAndreas Gohr    protected $timeUsed = 0;
26f6ef2e50SAndreas Gohr    /** @var int total number of requests made by this instance */
27f6ef2e50SAndreas Gohr    protected $requestsMade = 0;
28294a9eafSAndreas Gohr    /** @var int How often to retry a request if it fails */
29294a9eafSAndreas Gohr    public const MAX_RETRIES = 3;
30294a9eafSAndreas Gohr    /** @var DokuHTTPClient */
31294a9eafSAndreas Gohr    protected $http;
32294a9eafSAndreas Gohr    /** @var int start time of the current request chain (may be multiple when retries needed) */
33294a9eafSAndreas Gohr    protected $requestStart = 0;
34f6ef2e50SAndreas Gohr
35f6ef2e50SAndreas Gohr    /**
36294a9eafSAndreas Gohr     * This initializes a HTTP client
37294a9eafSAndreas Gohr     *
38294a9eafSAndreas Gohr     * Implementors should override this and authenticate the client.
39294a9eafSAndreas Gohr     *
40294a9eafSAndreas Gohr     * @param array $config The plugin configuration
41f6ef2e50SAndreas Gohr     */
42d02b7935SAndreas Gohr    public function __construct(array $config)
43294a9eafSAndreas Gohr    {
44294a9eafSAndreas Gohr        $this->http = new DokuHTTPClient();
45294a9eafSAndreas Gohr        $this->http->timeout = 60;
46294a9eafSAndreas Gohr        $this->http->headers['Content-Type'] = 'application/json';
47294a9eafSAndreas Gohr    }
48f6ef2e50SAndreas Gohr
49f6ef2e50SAndreas Gohr    /**
50*34a1c478SAndreas Gohr     * When enabled, the input/output of the API will be printed to STDOUT
51*34a1c478SAndreas Gohr     *
52*34a1c478SAndreas Gohr     * @param bool $debug
53*34a1c478SAndreas Gohr     */
54*34a1c478SAndreas Gohr    public function setDebug($debug = true)
55*34a1c478SAndreas Gohr    {
56*34a1c478SAndreas Gohr        $this->debug = $debug;
57*34a1c478SAndreas Gohr    }
58*34a1c478SAndreas Gohr
59*34a1c478SAndreas Gohr    /**
60294a9eafSAndreas Gohr     * This method should check the response for any errors. If the API singalled an error,
61294a9eafSAndreas Gohr     * this method should throw an Exception with a meaningful error message.
62294a9eafSAndreas Gohr     *
63294a9eafSAndreas Gohr     * If the response returned any info on used tokens, they should be added to $this->tokensUsed
64294a9eafSAndreas Gohr     *
65294a9eafSAndreas Gohr     * The method should return the parsed response, which will be passed to the calling method.
66294a9eafSAndreas Gohr     *
67294a9eafSAndreas Gohr     * @param mixed $response the parsed JSON response from the API
68294a9eafSAndreas Gohr     * @return mixed
69294a9eafSAndreas Gohr     * @throws \Exception when the response indicates an error
70294a9eafSAndreas Gohr     */
71294a9eafSAndreas Gohr    abstract protected function parseAPIResponse($response);
72294a9eafSAndreas Gohr
73294a9eafSAndreas Gohr    /**
74294a9eafSAndreas Gohr     * Send a request to the API
75294a9eafSAndreas Gohr     *
76294a9eafSAndreas Gohr     * Model classes should use this method to send requests to the API.
77294a9eafSAndreas Gohr     *
78294a9eafSAndreas Gohr     * This method will take care of retrying and logging basic statistics.
79294a9eafSAndreas Gohr     *
80294a9eafSAndreas Gohr     * It is assumed that all APIs speak JSON.
81294a9eafSAndreas Gohr     *
82294a9eafSAndreas Gohr     * @param string $method The HTTP method to use (GET, POST, PUT, DELETE, etc.)
83294a9eafSAndreas Gohr     * @param string $url The full URL to send the request to
84294a9eafSAndreas Gohr     * @param array $data Payload to send, will be encoded to JSON
85294a9eafSAndreas Gohr     * @param int $retry How often this request has been retried, do not set externally
86294a9eafSAndreas Gohr     * @return array API response as returned by parseAPIResponse
87294a9eafSAndreas Gohr     * @throws \Exception when anything goes wrong
88294a9eafSAndreas Gohr     */
89294a9eafSAndreas Gohr    protected function sendAPIRequest($method, $url, $data, $retry = 0)
90294a9eafSAndreas Gohr    {
91294a9eafSAndreas Gohr        // init statistics
92294a9eafSAndreas Gohr        if ($retry === 0) {
93294a9eafSAndreas Gohr            $this->requestStart = microtime(true);
94294a9eafSAndreas Gohr        } else {
95294a9eafSAndreas Gohr            sleep($retry); // wait a bit between retries
96294a9eafSAndreas Gohr        }
97294a9eafSAndreas Gohr        $this->requestsMade++;
98294a9eafSAndreas Gohr
99294a9eafSAndreas Gohr        // encode payload data
100294a9eafSAndreas Gohr        try {
101*34a1c478SAndreas Gohr            $json = json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
102294a9eafSAndreas Gohr        } catch (\JsonException $e) {
103294a9eafSAndreas Gohr            $this->timeUsed += $this->requestStart - microtime(true);
104294a9eafSAndreas Gohr            throw new \Exception('Failed to encode JSON for API:' . $e->getMessage(), $e->getCode(), $e);
105294a9eafSAndreas Gohr        }
106294a9eafSAndreas Gohr
107*34a1c478SAndreas Gohr        if ($this->debug) {
108*34a1c478SAndreas Gohr            echo 'Sending ' . $method . ' request to ' . $url . ' with payload:' . "\n";
109*34a1c478SAndreas Gohr            print_r($json);
110*34a1c478SAndreas Gohr        }
111*34a1c478SAndreas Gohr
112294a9eafSAndreas Gohr        // send request and handle retries
113294a9eafSAndreas Gohr        $this->http->sendRequest($url, $json, $method);
114294a9eafSAndreas Gohr        $response = $this->http->resp_body;
115294a9eafSAndreas Gohr        if ($response === false || $this->http->error) {
116294a9eafSAndreas Gohr            if ($retry < self::MAX_RETRIES) {
117294a9eafSAndreas Gohr                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
118294a9eafSAndreas Gohr            }
119294a9eafSAndreas Gohr            $this->timeUsed += microtime(true) - $this->requestStart;
120294a9eafSAndreas Gohr            throw new \Exception('API returned no response. ' . $this->http->error);
121294a9eafSAndreas Gohr        }
122294a9eafSAndreas Gohr
123*34a1c478SAndreas Gohr        if ($this->debug) {
124*34a1c478SAndreas Gohr            echo 'Received response:' . "\n";
125*34a1c478SAndreas Gohr            print_r($response);
126*34a1c478SAndreas Gohr        }
127*34a1c478SAndreas Gohr
128294a9eafSAndreas Gohr        // decode the response
129294a9eafSAndreas Gohr        try {
130294a9eafSAndreas Gohr            $result = json_decode((string)$response, true, 512, JSON_THROW_ON_ERROR);
131294a9eafSAndreas Gohr        } catch (\JsonException $e) {
132294a9eafSAndreas Gohr            $this->timeUsed += microtime(true) - $this->requestStart;
133294a9eafSAndreas Gohr            throw new \Exception('API returned invalid JSON: ' . $response, 0, $e);
134294a9eafSAndreas Gohr        }
135294a9eafSAndreas Gohr
136294a9eafSAndreas Gohr        // parse the response, retry on error
137294a9eafSAndreas Gohr        try {
138294a9eafSAndreas Gohr            $result = $this->parseAPIResponse($result);
139294a9eafSAndreas Gohr        } catch (\Exception $e) {
140294a9eafSAndreas Gohr            if ($retry < self::MAX_RETRIES) {
141294a9eafSAndreas Gohr                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
142294a9eafSAndreas Gohr            }
143294a9eafSAndreas Gohr            $this->timeUsed += microtime(true) - $this->requestStart;
144294a9eafSAndreas Gohr            throw $e;
145294a9eafSAndreas Gohr        }
146294a9eafSAndreas Gohr
147294a9eafSAndreas Gohr        $this->timeUsed += microtime(true) - $this->requestStart;
148294a9eafSAndreas Gohr        return $result;
149294a9eafSAndreas Gohr    }
150294a9eafSAndreas Gohr
151f6ef2e50SAndreas Gohr    /**
152f6ef2e50SAndreas Gohr     * Reset the usage statistics
153f6ef2e50SAndreas Gohr     *
154f6ef2e50SAndreas Gohr     * Usually not needed when only handling one operation per request, but useful in CLI
155f6ef2e50SAndreas Gohr     */
156f6ef2e50SAndreas Gohr    public function resetUsageStats()
157f6ef2e50SAndreas Gohr    {
158f6ef2e50SAndreas Gohr        $this->tokensUsed = 0;
159f6ef2e50SAndreas Gohr        $this->timeUsed = 0;
160f6ef2e50SAndreas Gohr        $this->requestsMade = 0;
161f6ef2e50SAndreas Gohr    }
162f6ef2e50SAndreas Gohr
163f6ef2e50SAndreas Gohr    /**
164f6ef2e50SAndreas Gohr     * Get the usage statistics for this instance
165f6ef2e50SAndreas Gohr     *
166f6ef2e50SAndreas Gohr     * @return string[]
167f6ef2e50SAndreas Gohr     */
168f6ef2e50SAndreas Gohr    public function getUsageStats()
169f6ef2e50SAndreas Gohr    {
170*34a1c478SAndreas Gohr
171*34a1c478SAndreas Gohr        $cost = 0;
172*34a1c478SAndreas Gohr        $cost += $this->inputTokensUsed * $this->getInputTokenPrice();
173*34a1c478SAndreas Gohr        if ($this instanceof ChatInterface) {
174*34a1c478SAndreas Gohr            $cost += $this->outputTokensUsed * $this->getOutputTokenPrice();
175*34a1c478SAndreas Gohr        }
176*34a1c478SAndreas Gohr
177f6ef2e50SAndreas Gohr        return [
178*34a1c478SAndreas Gohr            'tokens' => $this->tokensUsed + $this->inputTokensUsed + $this->outputTokensUsed,
179*34a1c478SAndreas Gohr            'cost' => round($cost / 1_000_000, 4),
180f6ef2e50SAndreas Gohr            'time' => round($this->timeUsed, 2),
181f6ef2e50SAndreas Gohr            'requests' => $this->requestsMade,
182f6ef2e50SAndreas Gohr        ];
183f6ef2e50SAndreas Gohr    }
184f6ef2e50SAndreas Gohr}
185