xref: /plugin/aichat/Model/AbstractModel.php (revision d02b793578c15c86b482725d129996df393f1890)
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{
16f6ef2e50SAndreas Gohr    /** @var int total tokens used by this instance */
17f6ef2e50SAndreas Gohr    protected $tokensUsed = 0;
18f6ef2e50SAndreas Gohr    /** @var int total time spent in requests by this instance */
19f6ef2e50SAndreas Gohr    protected $timeUsed = 0;
20f6ef2e50SAndreas Gohr    /** @var int total number of requests made by this instance */
21f6ef2e50SAndreas Gohr    protected $requestsMade = 0;
22294a9eafSAndreas Gohr    /** @var int How often to retry a request if it fails */
23294a9eafSAndreas Gohr    public const MAX_RETRIES = 3;
24294a9eafSAndreas Gohr    /** @var DokuHTTPClient */
25294a9eafSAndreas Gohr    protected $http;
26294a9eafSAndreas Gohr    /** @var int start time of the current request chain (may be multiple when retries needed) */
27294a9eafSAndreas Gohr    protected $requestStart = 0;
28f6ef2e50SAndreas Gohr
29f6ef2e50SAndreas Gohr    /**
30294a9eafSAndreas Gohr     * This initializes a HTTP client
31294a9eafSAndreas Gohr     *
32294a9eafSAndreas Gohr     * Implementors should override this and authenticate the client.
33294a9eafSAndreas Gohr     *
34294a9eafSAndreas Gohr     * @param array $config The plugin configuration
35f6ef2e50SAndreas Gohr     */
36*d02b7935SAndreas Gohr    public function __construct(array $config)
37294a9eafSAndreas Gohr    {
38294a9eafSAndreas Gohr        $this->http = new DokuHTTPClient();
39294a9eafSAndreas Gohr        $this->http->timeout = 60;
40294a9eafSAndreas Gohr        $this->http->headers['Content-Type'] = 'application/json';
41294a9eafSAndreas Gohr    }
42f6ef2e50SAndreas Gohr
43f6ef2e50SAndreas Gohr    /**
44294a9eafSAndreas Gohr     * This method should check the response for any errors. If the API singalled an error,
45294a9eafSAndreas Gohr     * this method should throw an Exception with a meaningful error message.
46294a9eafSAndreas Gohr     *
47294a9eafSAndreas Gohr     * If the response returned any info on used tokens, they should be added to $this->tokensUsed
48294a9eafSAndreas Gohr     *
49294a9eafSAndreas Gohr     * The method should return the parsed response, which will be passed to the calling method.
50294a9eafSAndreas Gohr     *
51294a9eafSAndreas Gohr     * @param mixed $response the parsed JSON response from the API
52294a9eafSAndreas Gohr     * @return mixed
53294a9eafSAndreas Gohr     * @throws \Exception when the response indicates an error
54294a9eafSAndreas Gohr     */
55294a9eafSAndreas Gohr    abstract protected function parseAPIResponse($response);
56294a9eafSAndreas Gohr
57294a9eafSAndreas Gohr    /**
58294a9eafSAndreas Gohr     * Send a request to the API
59294a9eafSAndreas Gohr     *
60294a9eafSAndreas Gohr     * Model classes should use this method to send requests to the API.
61294a9eafSAndreas Gohr     *
62294a9eafSAndreas Gohr     * This method will take care of retrying and logging basic statistics.
63294a9eafSAndreas Gohr     *
64294a9eafSAndreas Gohr     * It is assumed that all APIs speak JSON.
65294a9eafSAndreas Gohr     *
66294a9eafSAndreas Gohr     * @param string $method The HTTP method to use (GET, POST, PUT, DELETE, etc.)
67294a9eafSAndreas Gohr     * @param string $url The full URL to send the request to
68294a9eafSAndreas Gohr     * @param array $data Payload to send, will be encoded to JSON
69294a9eafSAndreas Gohr     * @param int $retry How often this request has been retried, do not set externally
70294a9eafSAndreas Gohr     * @return array API response as returned by parseAPIResponse
71294a9eafSAndreas Gohr     * @throws \Exception when anything goes wrong
72294a9eafSAndreas Gohr     */
73294a9eafSAndreas Gohr    protected function sendAPIRequest($method, $url, $data, $retry = 0)
74294a9eafSAndreas Gohr    {
75294a9eafSAndreas Gohr        // init statistics
76294a9eafSAndreas Gohr        if ($retry === 0) {
77294a9eafSAndreas Gohr            $this->requestStart = microtime(true);
78294a9eafSAndreas Gohr        } else {
79294a9eafSAndreas Gohr            sleep($retry); // wait a bit between retries
80294a9eafSAndreas Gohr        }
81294a9eafSAndreas Gohr        $this->requestsMade++;
82294a9eafSAndreas Gohr
83294a9eafSAndreas Gohr        // encode payload data
84294a9eafSAndreas Gohr        try {
85294a9eafSAndreas Gohr            $json = json_encode($data, JSON_THROW_ON_ERROR);
86294a9eafSAndreas Gohr        } catch (\JsonException $e) {
87294a9eafSAndreas Gohr            $this->timeUsed += $this->requestStart - microtime(true);
88294a9eafSAndreas Gohr            throw new \Exception('Failed to encode JSON for API:' . $e->getMessage(), $e->getCode(), $e);
89294a9eafSAndreas Gohr        }
90294a9eafSAndreas Gohr
91294a9eafSAndreas Gohr        // send request and handle retries
92294a9eafSAndreas Gohr        $this->http->sendRequest($url, $json, $method);
93294a9eafSAndreas Gohr        $response = $this->http->resp_body;
94294a9eafSAndreas Gohr        if ($response === false || $this->http->error) {
95294a9eafSAndreas Gohr            if ($retry < self::MAX_RETRIES) {
96294a9eafSAndreas Gohr                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
97294a9eafSAndreas Gohr            }
98294a9eafSAndreas Gohr            $this->timeUsed += microtime(true) - $this->requestStart;
99294a9eafSAndreas Gohr            throw new \Exception('API returned no response. ' . $this->http->error);
100294a9eafSAndreas Gohr        }
101294a9eafSAndreas Gohr
102294a9eafSAndreas Gohr        // decode the response
103294a9eafSAndreas Gohr        try {
104294a9eafSAndreas Gohr            $result = json_decode((string)$response, true, 512, JSON_THROW_ON_ERROR);
105294a9eafSAndreas Gohr        } catch (\JsonException $e) {
106294a9eafSAndreas Gohr            $this->timeUsed += microtime(true) - $this->requestStart;
107294a9eafSAndreas Gohr            throw new \Exception('API returned invalid JSON: ' . $response, 0, $e);
108294a9eafSAndreas Gohr        }
109294a9eafSAndreas Gohr
110294a9eafSAndreas Gohr        // parse the response, retry on error
111294a9eafSAndreas Gohr        try {
112294a9eafSAndreas Gohr            $result = $this->parseAPIResponse($result);
113294a9eafSAndreas Gohr        } catch (\Exception $e) {
114294a9eafSAndreas Gohr            if ($retry < self::MAX_RETRIES) {
115294a9eafSAndreas Gohr                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
116294a9eafSAndreas Gohr            }
117294a9eafSAndreas Gohr            $this->timeUsed += microtime(true) - $this->requestStart;
118294a9eafSAndreas Gohr            throw $e;
119294a9eafSAndreas Gohr        }
120294a9eafSAndreas Gohr
121294a9eafSAndreas Gohr        $this->timeUsed += microtime(true) - $this->requestStart;
122294a9eafSAndreas Gohr        return $result;
123294a9eafSAndreas Gohr    }
124294a9eafSAndreas Gohr
125f6ef2e50SAndreas Gohr    /**
126f6ef2e50SAndreas Gohr     * Reset the usage statistics
127f6ef2e50SAndreas Gohr     *
128f6ef2e50SAndreas Gohr     * Usually not needed when only handling one operation per request, but useful in CLI
129f6ef2e50SAndreas Gohr     */
130f6ef2e50SAndreas Gohr    public function resetUsageStats()
131f6ef2e50SAndreas Gohr    {
132f6ef2e50SAndreas Gohr        $this->tokensUsed = 0;
133f6ef2e50SAndreas Gohr        $this->timeUsed = 0;
134f6ef2e50SAndreas Gohr        $this->requestsMade = 0;
135f6ef2e50SAndreas Gohr    }
136f6ef2e50SAndreas Gohr
137f6ef2e50SAndreas Gohr    /**
138f6ef2e50SAndreas Gohr     * Get the usage statistics for this instance
139f6ef2e50SAndreas Gohr     *
140f6ef2e50SAndreas Gohr     * @return string[]
141f6ef2e50SAndreas Gohr     */
142f6ef2e50SAndreas Gohr    public function getUsageStats()
143f6ef2e50SAndreas Gohr    {
144f6ef2e50SAndreas Gohr        return [
145f6ef2e50SAndreas Gohr            'tokens' => $this->tokensUsed,
146*d02b7935SAndreas Gohr            'cost' => round($this->tokensUsed * $this->get1MillionTokenPrice() / 1_000_000, 4),
147f6ef2e50SAndreas Gohr            'time' => round($this->timeUsed, 2),
148f6ef2e50SAndreas Gohr            'requests' => $this->requestsMade,
149f6ef2e50SAndreas Gohr        ];
150f6ef2e50SAndreas Gohr    }
151f6ef2e50SAndreas Gohr}
152