├── .env.example
├── .gitignore
├── README.md
├── ask-docs.php
├── composer.json
├── composer.lock
├── config.php
├── helpers.php
└── src
├── AskDocs.php
├── DocumentationLoader.php
└── RedisHelper.php
/.env.example:
--------------------------------------------------------------------------------
1 | OPENAI_API_KEY=
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | docs/*
3 | .env
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ask The Laravel Docs
2 |
3 |
4 |
5 | https://user-images.githubusercontent.com/24755643/236369430-ebf7c77d-38cd-4ce4-80a2-c39dfefedf93.mp4
6 |
7 |
8 |
9 | After I released a section on my blog where you can ask the Laravel docs questions (https://cosme.dev/ask-docs) using OpenAI's API and the Laravel documentation I got a lot of messages asking me how did I do it and questions about how they can add something similar to their existing projects.
10 |
11 | So I decided to make this open source repo showing how to do something similar in pure PHP (the original website implementation on my website was made with a combination of php, python and golang).
12 |
13 | If you just want to test the projects simply follow the installations instructions below. If you are looking to understand better how it works I will add an explanation below of the concepts as I understand them (I'm still learning this stuff) or you can just look at the code, I added some comments that hopefully will make it easier to understand what is going on.
14 |
15 | ## Installation
16 |
17 | ### Requirements
18 |
19 | To run this project on your computer you need to have PHP and composer installed.
20 |
21 | You also need an OpenAI API Key, you can get one here: https://platform.openai.com/account/api-keys
22 |
23 | I think you also need to have a billing method added to your OpenAI account. This project will generate spent in your account, the first time you run it, it will generate embeddings for all the Laravel docs, but that won't cost more than $0.20 or something along those lines. Asking questions are a little more expensive since it uses the completions API and we send context on each question but overall you should be able to test and play with this repo for less than $1. For more information about OpenAI pricing visit this link: https://openai.com/pricing#language-models
24 |
25 | You also need to have a Redis instance with the [RediSearch](https://redis.io/docs/stack/search/) extension running, the easiest to do this is by using the official docker image:
26 |
27 | ```
28 | docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
29 | ```
30 |
31 | This will start a Redis instance in your machine that will have everything you need for this project. If you don't have docker installed on your computer, follow the installation instructions for your OS: https://docs.docker.com/get-docker/
32 |
33 | ### Running the project
34 |
35 | First clone the github repo:
36 | ```
37 | git clone git@github.com:cosmeoes/ask-the-laravel-docs.git
38 | ```
39 |
40 | Install the dependencies:
41 | ```
42 | composer install
43 | ```
44 |
45 | Copy the .env.example file to .env:
46 |
47 | ```
48 | cp .env.example .env
49 | ```
50 |
51 | Add your OpenAI API key to the .env file:
52 | ```
53 | OPENAI_API_KEY={YOUR_API_KEY}
54 | ```
55 |
56 | If you haven't already start the Redis docker container:
57 | ```
58 | docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
59 | ```
60 |
61 | Note: If you want to connect to a Redis server that is not running own your local machine or it's running on another port, you can change the connection configuration in the `config.php` file.
62 |
63 | Run the project:
64 | ```
65 | php ask-docs.php
66 | ```
67 |
68 | This will download the documentation to `docs/` and start indexing them into the Redis instance, you'll see a progress bar and the estimated remaining time.
69 |
70 | Once it finishes you'll be presented with this prompt:
71 | ```
72 | Type your question ('quit' to stop):
73 | ```
74 |
75 | You can type any question and start using the tool :)
76 |
77 |
78 | # How does it work?
79 |
80 | The way this works might seem complicated at first but in reality is a very simple process. There are however some concepts that you need to know to understand better the way this project was made and how you can adapt it to your own projects:
81 |
82 | - Embeddings:
83 |
84 | From the OpenAI docs: "An embedding is a vector (list) of floating point numbers. The distance between two vectors measures their relatedness. Small distances suggest high relatedness and large distances suggest low relatedness."
85 |
86 | In more simple terms they are an array of numbers that represent text. We can use these numbers to calculate how related two or more pieces of texts are.
87 |
88 | - Vector Database:
89 |
90 | A vector database sounds complicated but in reality the concept is very simple. We use vector databases to store the embeddings of text, once we have those embeddings in the vector database we can make a query to get the most related texts from the database. For example, in this project we create a vector database that contains all the embeddings for the Laravel documentation, then when you ask a question via the prompt we generate the embeddings for that question and then query the database to get the 4 most relevant results. So it works in a similar way to a full text search, except we can use them to match by how similar the meaning of the two text are instead of keywords.
91 |
92 | In this project I decided to use Redis as the vector database, but there are many other options including a vector extension for PostgreSQL, so don't think you need to use Redis to create something similar.
93 |
94 | ## Step by step break down
95 |
96 | First let's start on how I insert the Laravel docs into the vector database:
97 |
98 | 1. The first step is the index definition. I added three fields to the index:
99 | 1. "text" for the text of that documentation section.
100 | 2. "url" to show the "Sources" list after the OpenAI response.
101 | 3. "content_vector" this is a vector field that will contain the embedding for each section of the docs. This is the field that we will query against.
102 |
103 | 2. The second step is breaking the database into smaller chunks. The reason for this is because the OpenAI API has a character limit when we make a request so we can't send the whole documentation at the same time.
104 |
105 | The way I decided to do this is by breaking it into each documentation section. In other words, every time there is a header or subheader in the docs I split it. In this step I also build the url for that section so we can display them as the "Sources:" links after we show the OpenAI response.
106 |
107 | 3. The third step is converting each one of those sections into embeddings and storing them into the index. We get the embeddings sending the text to the embeddings endpoint of the OpenAI API. It returns an array of floating point numbers. I send 10 sections at a time to the OpenAI API to make it faster. I choose 10 because I saw that in an example and it worked for this use case. But its not a set number and you can experiment with it.
108 |
109 | To store this in redis you have to convert them to a byte string so I do that using the `pack()` function. (See the `helpers.php` file). Then I put those bytes into `content_vector` field on each documentation section item and store them with `hset` and the key `doc:{index}` where `index` is just the index of the iteration. This is pretty specific to redis, so if you use another vector database you'll probably have to follow the documentation to know how you should store them.
110 |
111 | After we have the vector database with our documents in them we can start asking questions, the question/response flow is the following:
112 |
113 | 1. When a user inputs text we send that text to OpenAI embeddings endpoint to generate the embeddings of that text.
114 | 2. With those embeddings we query the database to get the 4 most similar documents (as in semantically similar not text search similar).
115 | 3. We take the text from those documents and create a string that contains both the documentation text and the question (you can see the [code here](https://github.com/cosmeoes/ask-the-laravel-docs/blob/c4b7891bdcb8dc07c72535964ae758270be1a7bb/src/AskDocs.php#L98-L113))
116 | 4. We send that text to OpenAI chat endpoint, in this case we use `gpt-3.5-turbo` as the model.
117 |
118 | That is the basic flow for the first question. But if you ask multiple questions you will notice that the chat "remembers" what you typed previously, this is not a feature of the OpenAI API and instead is done by storing the question and responses in an array and asking OpenAI to generate a stand alone question (you can see the [code here](https://github.com/cosmeoes/ask-the-laravel-docs/blob/c4b7891bdcb8dc07c72535964ae758270be1a7bb/src/AskDocs.php#L47-L66)). We then use that question to query the vector database and use that to get OpenAI's response.
119 |
120 |
121 | And that's it, that's the gist of how it all works, you probably should read this as you read the code and it will all make more sense, if you have any other questions you can ask me on twitter [@cosmeescobedo](https://twitter.com/cosmeescobedo) or send me an email at cosme@cosme.dev
122 |
123 |
124 |
--------------------------------------------------------------------------------
/ask-docs.php:
--------------------------------------------------------------------------------
1 | load();
7 |
8 | $askDocs = new AskDocs();
9 | $askDocs->start();
10 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cosmedev/askdocs",
3 | "type": "project",
4 | "autoload": {
5 | "psr-4": {
6 | "CosmeDev\\AskDocs\\": "src/"
7 | }
8 | },
9 | "authors": [
10 | {
11 | "name": "Cosme Escobedo",
12 | "email": "cosme@cosme.dev"
13 | }
14 | ],
15 | "require": {
16 | "macfja/redisearch": "^2.2",
17 | "predis/predis": "^2.1",
18 | "openai-php/client": "^0.4.2",
19 | "symfony/http-client": "^6.2",
20 | "nyholm/psr7": "^1.8",
21 | "macroman/terminal-progress-bar": "^0.1.7",
22 | "vlucas/phpdotenv": "^5.5"
23 | },
24 | "config": {
25 | "allow-plugins": {
26 | "php-http/discovery": true
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "292e6a2badc585a8f7a7f129adaf5760",
8 | "packages": [
9 | {
10 | "name": "composer/semver",
11 | "version": "3.3.2",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/composer/semver.git",
15 | "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9",
20 | "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "php": "^5.3.2 || ^7.0 || ^8.0"
25 | },
26 | "require-dev": {
27 | "phpstan/phpstan": "^1.4",
28 | "symfony/phpunit-bridge": "^4.2 || ^5"
29 | },
30 | "type": "library",
31 | "extra": {
32 | "branch-alias": {
33 | "dev-main": "3.x-dev"
34 | }
35 | },
36 | "autoload": {
37 | "psr-4": {
38 | "Composer\\Semver\\": "src"
39 | }
40 | },
41 | "notification-url": "https://packagist.org/downloads/",
42 | "license": [
43 | "MIT"
44 | ],
45 | "authors": [
46 | {
47 | "name": "Nils Adermann",
48 | "email": "naderman@naderman.de",
49 | "homepage": "http://www.naderman.de"
50 | },
51 | {
52 | "name": "Jordi Boggiano",
53 | "email": "j.boggiano@seld.be",
54 | "homepage": "http://seld.be"
55 | },
56 | {
57 | "name": "Rob Bast",
58 | "email": "rob.bast@gmail.com",
59 | "homepage": "http://robbast.nl"
60 | }
61 | ],
62 | "description": "Semver library that offers utilities, version constraint parsing and validation.",
63 | "keywords": [
64 | "semantic",
65 | "semver",
66 | "validation",
67 | "versioning"
68 | ],
69 | "support": {
70 | "irc": "irc://irc.freenode.org/composer",
71 | "issues": "https://github.com/composer/semver/issues",
72 | "source": "https://github.com/composer/semver/tree/3.3.2"
73 | },
74 | "funding": [
75 | {
76 | "url": "https://packagist.com",
77 | "type": "custom"
78 | },
79 | {
80 | "url": "https://github.com/composer",
81 | "type": "github"
82 | },
83 | {
84 | "url": "https://tidelift.com/funding/github/packagist/composer/composer",
85 | "type": "tidelift"
86 | }
87 | ],
88 | "time": "2022-04-01T19:23:25+00:00"
89 | },
90 | {
91 | "name": "graham-campbell/result-type",
92 | "version": "v1.1.1",
93 | "source": {
94 | "type": "git",
95 | "url": "https://github.com/GrahamCampbell/Result-Type.git",
96 | "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831"
97 | },
98 | "dist": {
99 | "type": "zip",
100 | "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831",
101 | "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831",
102 | "shasum": ""
103 | },
104 | "require": {
105 | "php": "^7.2.5 || ^8.0",
106 | "phpoption/phpoption": "^1.9.1"
107 | },
108 | "require-dev": {
109 | "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
110 | },
111 | "type": "library",
112 | "autoload": {
113 | "psr-4": {
114 | "GrahamCampbell\\ResultType\\": "src/"
115 | }
116 | },
117 | "notification-url": "https://packagist.org/downloads/",
118 | "license": [
119 | "MIT"
120 | ],
121 | "authors": [
122 | {
123 | "name": "Graham Campbell",
124 | "email": "hello@gjcampbell.co.uk",
125 | "homepage": "https://github.com/GrahamCampbell"
126 | }
127 | ],
128 | "description": "An Implementation Of The Result Type",
129 | "keywords": [
130 | "Graham Campbell",
131 | "GrahamCampbell",
132 | "Result Type",
133 | "Result-Type",
134 | "result"
135 | ],
136 | "support": {
137 | "issues": "https://github.com/GrahamCampbell/Result-Type/issues",
138 | "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1"
139 | },
140 | "funding": [
141 | {
142 | "url": "https://github.com/GrahamCampbell",
143 | "type": "github"
144 | },
145 | {
146 | "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
147 | "type": "tidelift"
148 | }
149 | ],
150 | "time": "2023-02-25T20:23:15+00:00"
151 | },
152 | {
153 | "name": "macfja/redisearch",
154 | "version": "2.2.0",
155 | "source": {
156 | "type": "git",
157 | "url": "https://github.com/MacFJA/php-redisearch.git",
158 | "reference": "ec9dec1dd34c07011224eec7ae3057e87ceb2bd4"
159 | },
160 | "dist": {
161 | "type": "zip",
162 | "url": "https://api.github.com/repos/MacFJA/php-redisearch/zipball/ec9dec1dd34c07011224eec7ae3057e87ceb2bd4",
163 | "reference": "ec9dec1dd34c07011224eec7ae3057e87ceb2bd4",
164 | "shasum": ""
165 | },
166 | "require": {
167 | "composer/semver": "^3.2",
168 | "ext-intl": "*",
169 | "php": "^7.2 || ^8.0",
170 | "respect/validation": "^2.0"
171 | },
172 | "require-dev": {
173 | "amphp/redis": "^1.0",
174 | "cheprasov/php-redis-client": "^1.10",
175 | "colinmollenhour/credis": "^1.12",
176 | "enlightn/security-checker": "^1.9",
177 | "ergebnis/composer-normalize": "^2.13",
178 | "ext-mbstring": "*",
179 | "friendsofphp/php-cs-fixer": "^3.0",
180 | "geometria-lab/rediska": "^0.5.10",
181 | "insolita/unused-scanner": "^2.3",
182 | "php-parallel-lint/php-parallel-lint": "^1.3",
183 | "phpmd/phpmd": "^2.10",
184 | "phpstan/phpstan": "^1.2.0",
185 | "phpunit/phpunit": "^8.5 || ^9.3",
186 | "predis/predis": "^1.1 || ^2.0",
187 | "ptrofimov/tinyredisclient": "^1.1",
188 | "redisent/redisent": "dev-master",
189 | "roave/security-advisories": "dev-latest",
190 | "rskuipers/php-assumptions": "^0.8.0",
191 | "sebastian/phpcpd": "^4.1 || ^6.0",
192 | "ukko/phpredis-phpdoc": "dev-master",
193 | "vimeo/psalm": "^4.7"
194 | },
195 | "suggest": {
196 | "amphp/redis": "To use AmpPhp implementation",
197 | "cheprasov/php-redis-client": "To use Cheprasov implementation",
198 | "colinmollenhour/credis": "To use Credis implementation",
199 | "ext-phpiredis": "To use Phpiredis extension implementation",
200 | "ext-redis": "To use Phpredis extension implementation",
201 | "geometria-lab/rediska": "To use Rediska implementation",
202 | "predis/predis": "To use Predis implementation",
203 | "ptrofimov/tinyredisclient": "To use TinyRedisClient implementation",
204 | "redisent/redisent": "To use Redisent implementation"
205 | },
206 | "type": "library",
207 | "autoload": {
208 | "psr-4": {
209 | "MacFJA\\RediSearch\\": "src/"
210 | }
211 | },
212 | "notification-url": "https://packagist.org/downloads/",
213 | "license": [
214 | "MIT"
215 | ],
216 | "authors": [
217 | {
218 | "name": "MacFJA"
219 | }
220 | ],
221 | "description": "PHP Client for RediSearch",
222 | "keywords": [
223 | "redis",
224 | "redisearch",
225 | "search engine",
226 | "suggestion"
227 | ],
228 | "support": {
229 | "issues": "https://github.com/MacFJA/php-redisearch/issues",
230 | "source": "https://github.com/MacFJA/php-redisearch"
231 | },
232 | "time": "2022-08-13T22:10:58+00:00"
233 | },
234 | {
235 | "name": "macroman/terminal-progress-bar",
236 | "version": "0.1.7",
237 | "source": {
238 | "type": "git",
239 | "url": "https://github.com/MacroMan/PHPTerminalProgressBar.git",
240 | "reference": "d0825d2da1515942fe45ffb0e8d9bcd4e2bdb097"
241 | },
242 | "dist": {
243 | "type": "zip",
244 | "url": "https://api.github.com/repos/MacroMan/PHPTerminalProgressBar/zipball/d0825d2da1515942fe45ffb0e8d9bcd4e2bdb097",
245 | "reference": "d0825d2da1515942fe45ffb0e8d9bcd4e2bdb097",
246 | "shasum": ""
247 | },
248 | "require": {
249 | "php": ">=5.6.0|>=8.0"
250 | },
251 | "type": "library",
252 | "autoload": {
253 | "psr-0": {
254 | "TerminalProgress": "src/"
255 | }
256 | },
257 | "notification-url": "https://packagist.org/downloads/",
258 | "license": [
259 | "GPL-3.0-or-later"
260 | ],
261 | "authors": [
262 | {
263 | "name": "David Wakelin",
264 | "email": "hello@davidwakelin.co.uk"
265 | }
266 | ],
267 | "description": "Flexible ascii progress bar.",
268 | "homepage": "https://github.com/MacroMan/PHPTerminalProgressBar",
269 | "support": {
270 | "issues": "https://github.com/MacroMan/PHPTerminalProgressBar/issues",
271 | "source": "https://github.com/MacroMan/PHPTerminalProgressBar/tree/0.1.7"
272 | },
273 | "time": "2021-12-16T12:09:47+00:00"
274 | },
275 | {
276 | "name": "nyholm/psr7",
277 | "version": "1.8.0",
278 | "source": {
279 | "type": "git",
280 | "url": "https://github.com/Nyholm/psr7.git",
281 | "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be"
282 | },
283 | "dist": {
284 | "type": "zip",
285 | "url": "https://api.github.com/repos/Nyholm/psr7/zipball/3cb4d163b58589e47b35103e8e5e6a6a475b47be",
286 | "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be",
287 | "shasum": ""
288 | },
289 | "require": {
290 | "php": ">=7.2",
291 | "psr/http-factory": "^1.0",
292 | "psr/http-message": "^1.1 || ^2.0"
293 | },
294 | "provide": {
295 | "php-http/message-factory-implementation": "1.0",
296 | "psr/http-factory-implementation": "1.0",
297 | "psr/http-message-implementation": "1.0"
298 | },
299 | "require-dev": {
300 | "http-interop/http-factory-tests": "^0.9",
301 | "php-http/message-factory": "^1.0",
302 | "php-http/psr7-integration-tests": "^1.0",
303 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4",
304 | "symfony/error-handler": "^4.4"
305 | },
306 | "type": "library",
307 | "extra": {
308 | "branch-alias": {
309 | "dev-master": "1.8-dev"
310 | }
311 | },
312 | "autoload": {
313 | "psr-4": {
314 | "Nyholm\\Psr7\\": "src/"
315 | }
316 | },
317 | "notification-url": "https://packagist.org/downloads/",
318 | "license": [
319 | "MIT"
320 | ],
321 | "authors": [
322 | {
323 | "name": "Tobias Nyholm",
324 | "email": "tobias.nyholm@gmail.com"
325 | },
326 | {
327 | "name": "Martijn van der Ven",
328 | "email": "martijn@vanderven.se"
329 | }
330 | ],
331 | "description": "A fast PHP7 implementation of PSR-7",
332 | "homepage": "https://tnyholm.se",
333 | "keywords": [
334 | "psr-17",
335 | "psr-7"
336 | ],
337 | "support": {
338 | "issues": "https://github.com/Nyholm/psr7/issues",
339 | "source": "https://github.com/Nyholm/psr7/tree/1.8.0"
340 | },
341 | "funding": [
342 | {
343 | "url": "https://github.com/Zegnat",
344 | "type": "github"
345 | },
346 | {
347 | "url": "https://github.com/nyholm",
348 | "type": "github"
349 | }
350 | ],
351 | "time": "2023-05-02T11:26:24+00:00"
352 | },
353 | {
354 | "name": "openai-php/client",
355 | "version": "v0.4.2",
356 | "source": {
357 | "type": "git",
358 | "url": "https://github.com/openai-php/client.git",
359 | "reference": "bbfbc0a28872d679d6990712d7feaae2c9f96fc2"
360 | },
361 | "dist": {
362 | "type": "zip",
363 | "url": "https://api.github.com/repos/openai-php/client/zipball/bbfbc0a28872d679d6990712d7feaae2c9f96fc2",
364 | "reference": "bbfbc0a28872d679d6990712d7feaae2c9f96fc2",
365 | "shasum": ""
366 | },
367 | "require": {
368 | "php": "^8.1.0",
369 | "php-http/discovery": "^1.15.3",
370 | "php-http/multipart-stream-builder": "^1.2.0",
371 | "psr/http-client": "^1.0.2",
372 | "psr/http-client-implementation": "^1.0.1",
373 | "psr/http-factory-implementation": "*",
374 | "psr/http-message": "^1.1.0"
375 | },
376 | "require-dev": {
377 | "guzzlehttp/guzzle": "^7.5.0",
378 | "guzzlehttp/psr7": "^2.4.4",
379 | "laravel/pint": "^1.8.0",
380 | "nunomaduro/collision": "^7.4.0",
381 | "pestphp/pest": "^2.4.0",
382 | "pestphp/pest-plugin-arch": "^2.1.1",
383 | "pestphp/pest-plugin-mock": "^2.0.0",
384 | "phpstan/phpstan": "^1.10.11",
385 | "rector/rector": "^0.14.8",
386 | "symfony/var-dumper": "^6.2.8"
387 | },
388 | "type": "library",
389 | "autoload": {
390 | "files": [
391 | "src/OpenAI.php"
392 | ],
393 | "psr-4": {
394 | "OpenAI\\": "src/"
395 | }
396 | },
397 | "notification-url": "https://packagist.org/downloads/",
398 | "license": [
399 | "MIT"
400 | ],
401 | "authors": [
402 | {
403 | "name": "Nuno Maduro",
404 | "email": "enunomaduro@gmail.com"
405 | },
406 | {
407 | "name": "Sandro Gehri"
408 | }
409 | ],
410 | "description": "OpenAI PHP is a supercharged PHP API client that allows you to interact with the Open AI API",
411 | "keywords": [
412 | "GPT-3",
413 | "api",
414 | "client",
415 | "codex",
416 | "dall-e",
417 | "language",
418 | "natural",
419 | "openai",
420 | "php",
421 | "processing",
422 | "sdk"
423 | ],
424 | "support": {
425 | "issues": "https://github.com/openai-php/client/issues",
426 | "source": "https://github.com/openai-php/client/tree/v0.4.2"
427 | },
428 | "funding": [
429 | {
430 | "url": "https://www.paypal.com/paypalme/enunomaduro",
431 | "type": "custom"
432 | },
433 | {
434 | "url": "https://github.com/gehrisandro",
435 | "type": "github"
436 | },
437 | {
438 | "url": "https://github.com/nunomaduro",
439 | "type": "github"
440 | }
441 | ],
442 | "time": "2023-04-12T04:26:02+00:00"
443 | },
444 | {
445 | "name": "php-http/discovery",
446 | "version": "1.17.0",
447 | "source": {
448 | "type": "git",
449 | "url": "https://github.com/php-http/discovery.git",
450 | "reference": "bd810d15957cf165230e65d9e1a130793265e3b7"
451 | },
452 | "dist": {
453 | "type": "zip",
454 | "url": "https://api.github.com/repos/php-http/discovery/zipball/bd810d15957cf165230e65d9e1a130793265e3b7",
455 | "reference": "bd810d15957cf165230e65d9e1a130793265e3b7",
456 | "shasum": ""
457 | },
458 | "require": {
459 | "composer-plugin-api": "^1.0|^2.0",
460 | "php": "^7.1 || ^8.0"
461 | },
462 | "conflict": {
463 | "nyholm/psr7": "<1.0",
464 | "zendframework/zend-diactoros": "*"
465 | },
466 | "provide": {
467 | "php-http/async-client-implementation": "*",
468 | "php-http/client-implementation": "*",
469 | "psr/http-client-implementation": "*",
470 | "psr/http-factory-implementation": "*",
471 | "psr/http-message-implementation": "*"
472 | },
473 | "require-dev": {
474 | "composer/composer": "^1.0.2|^2.0",
475 | "graham-campbell/phpspec-skip-example-extension": "^5.0",
476 | "php-http/httplug": "^1.0 || ^2.0",
477 | "php-http/message-factory": "^1.0",
478 | "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
479 | "symfony/phpunit-bridge": "^6.2"
480 | },
481 | "type": "composer-plugin",
482 | "extra": {
483 | "class": "Http\\Discovery\\Composer\\Plugin",
484 | "plugin-optional": true
485 | },
486 | "autoload": {
487 | "psr-4": {
488 | "Http\\Discovery\\": "src/"
489 | },
490 | "exclude-from-classmap": [
491 | "src/Composer/Plugin.php"
492 | ]
493 | },
494 | "notification-url": "https://packagist.org/downloads/",
495 | "license": [
496 | "MIT"
497 | ],
498 | "authors": [
499 | {
500 | "name": "Márk Sági-Kazár",
501 | "email": "mark.sagikazar@gmail.com"
502 | }
503 | ],
504 | "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
505 | "homepage": "http://php-http.org",
506 | "keywords": [
507 | "adapter",
508 | "client",
509 | "discovery",
510 | "factory",
511 | "http",
512 | "message",
513 | "psr17",
514 | "psr7"
515 | ],
516 | "support": {
517 | "issues": "https://github.com/php-http/discovery/issues",
518 | "source": "https://github.com/php-http/discovery/tree/1.17.0"
519 | },
520 | "time": "2023-04-26T15:39:13+00:00"
521 | },
522 | {
523 | "name": "php-http/multipart-stream-builder",
524 | "version": "1.3.0",
525 | "source": {
526 | "type": "git",
527 | "url": "https://github.com/php-http/multipart-stream-builder.git",
528 | "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a"
529 | },
530 | "dist": {
531 | "type": "zip",
532 | "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a",
533 | "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a",
534 | "shasum": ""
535 | },
536 | "require": {
537 | "php": "^7.1 || ^8.0",
538 | "php-http/discovery": "^1.15",
539 | "psr/http-factory-implementation": "^1.0"
540 | },
541 | "require-dev": {
542 | "nyholm/psr7": "^1.0",
543 | "php-http/message": "^1.5",
544 | "php-http/message-factory": "^1.0.2",
545 | "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3"
546 | },
547 | "type": "library",
548 | "autoload": {
549 | "psr-4": {
550 | "Http\\Message\\MultipartStream\\": "src/"
551 | }
552 | },
553 | "notification-url": "https://packagist.org/downloads/",
554 | "license": [
555 | "MIT"
556 | ],
557 | "authors": [
558 | {
559 | "name": "Tobias Nyholm",
560 | "email": "tobias.nyholm@gmail.com"
561 | }
562 | ],
563 | "description": "A builder class that help you create a multipart stream",
564 | "homepage": "http://php-http.org",
565 | "keywords": [
566 | "factory",
567 | "http",
568 | "message",
569 | "multipart stream",
570 | "stream"
571 | ],
572 | "support": {
573 | "issues": "https://github.com/php-http/multipart-stream-builder/issues",
574 | "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0"
575 | },
576 | "time": "2023-04-28T14:10:22+00:00"
577 | },
578 | {
579 | "name": "phpoption/phpoption",
580 | "version": "1.9.1",
581 | "source": {
582 | "type": "git",
583 | "url": "https://github.com/schmittjoh/php-option.git",
584 | "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e"
585 | },
586 | "dist": {
587 | "type": "zip",
588 | "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e",
589 | "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e",
590 | "shasum": ""
591 | },
592 | "require": {
593 | "php": "^7.2.5 || ^8.0"
594 | },
595 | "require-dev": {
596 | "bamarni/composer-bin-plugin": "^1.8.2",
597 | "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
598 | },
599 | "type": "library",
600 | "extra": {
601 | "bamarni-bin": {
602 | "bin-links": true,
603 | "forward-command": true
604 | },
605 | "branch-alias": {
606 | "dev-master": "1.9-dev"
607 | }
608 | },
609 | "autoload": {
610 | "psr-4": {
611 | "PhpOption\\": "src/PhpOption/"
612 | }
613 | },
614 | "notification-url": "https://packagist.org/downloads/",
615 | "license": [
616 | "Apache-2.0"
617 | ],
618 | "authors": [
619 | {
620 | "name": "Johannes M. Schmitt",
621 | "email": "schmittjoh@gmail.com",
622 | "homepage": "https://github.com/schmittjoh"
623 | },
624 | {
625 | "name": "Graham Campbell",
626 | "email": "hello@gjcampbell.co.uk",
627 | "homepage": "https://github.com/GrahamCampbell"
628 | }
629 | ],
630 | "description": "Option Type for PHP",
631 | "keywords": [
632 | "language",
633 | "option",
634 | "php",
635 | "type"
636 | ],
637 | "support": {
638 | "issues": "https://github.com/schmittjoh/php-option/issues",
639 | "source": "https://github.com/schmittjoh/php-option/tree/1.9.1"
640 | },
641 | "funding": [
642 | {
643 | "url": "https://github.com/GrahamCampbell",
644 | "type": "github"
645 | },
646 | {
647 | "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
648 | "type": "tidelift"
649 | }
650 | ],
651 | "time": "2023-02-25T19:38:58+00:00"
652 | },
653 | {
654 | "name": "predis/predis",
655 | "version": "v2.1.2",
656 | "source": {
657 | "type": "git",
658 | "url": "https://github.com/predis/predis.git",
659 | "reference": "a77a43913a74f9331f637bb12867eb8e274814e5"
660 | },
661 | "dist": {
662 | "type": "zip",
663 | "url": "https://api.github.com/repos/predis/predis/zipball/a77a43913a74f9331f637bb12867eb8e274814e5",
664 | "reference": "a77a43913a74f9331f637bb12867eb8e274814e5",
665 | "shasum": ""
666 | },
667 | "require": {
668 | "php": "^7.2 || ^8.0"
669 | },
670 | "require-dev": {
671 | "friendsofphp/php-cs-fixer": "^3.3",
672 | "phpstan/phpstan": "^1.9",
673 | "phpunit/phpunit": "^8.0 || ~9.4.4"
674 | },
675 | "type": "library",
676 | "autoload": {
677 | "psr-4": {
678 | "Predis\\": "src/"
679 | }
680 | },
681 | "notification-url": "https://packagist.org/downloads/",
682 | "license": [
683 | "MIT"
684 | ],
685 | "authors": [
686 | {
687 | "name": "Till Krüss",
688 | "homepage": "https://till.im",
689 | "role": "Maintainer"
690 | }
691 | ],
692 | "description": "A flexible and feature-complete Redis client for PHP.",
693 | "homepage": "http://github.com/predis/predis",
694 | "keywords": [
695 | "nosql",
696 | "predis",
697 | "redis"
698 | ],
699 | "support": {
700 | "issues": "https://github.com/predis/predis/issues",
701 | "source": "https://github.com/predis/predis/tree/v2.1.2"
702 | },
703 | "funding": [
704 | {
705 | "url": "https://github.com/sponsors/tillkruss",
706 | "type": "github"
707 | }
708 | ],
709 | "time": "2023-03-02T18:32:04+00:00"
710 | },
711 | {
712 | "name": "psr/container",
713 | "version": "2.0.2",
714 | "source": {
715 | "type": "git",
716 | "url": "https://github.com/php-fig/container.git",
717 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
718 | },
719 | "dist": {
720 | "type": "zip",
721 | "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
722 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
723 | "shasum": ""
724 | },
725 | "require": {
726 | "php": ">=7.4.0"
727 | },
728 | "type": "library",
729 | "extra": {
730 | "branch-alias": {
731 | "dev-master": "2.0.x-dev"
732 | }
733 | },
734 | "autoload": {
735 | "psr-4": {
736 | "Psr\\Container\\": "src/"
737 | }
738 | },
739 | "notification-url": "https://packagist.org/downloads/",
740 | "license": [
741 | "MIT"
742 | ],
743 | "authors": [
744 | {
745 | "name": "PHP-FIG",
746 | "homepage": "https://www.php-fig.org/"
747 | }
748 | ],
749 | "description": "Common Container Interface (PHP FIG PSR-11)",
750 | "homepage": "https://github.com/php-fig/container",
751 | "keywords": [
752 | "PSR-11",
753 | "container",
754 | "container-interface",
755 | "container-interop",
756 | "psr"
757 | ],
758 | "support": {
759 | "issues": "https://github.com/php-fig/container/issues",
760 | "source": "https://github.com/php-fig/container/tree/2.0.2"
761 | },
762 | "time": "2021-11-05T16:47:00+00:00"
763 | },
764 | {
765 | "name": "psr/http-client",
766 | "version": "1.0.2",
767 | "source": {
768 | "type": "git",
769 | "url": "https://github.com/php-fig/http-client.git",
770 | "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
771 | },
772 | "dist": {
773 | "type": "zip",
774 | "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
775 | "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
776 | "shasum": ""
777 | },
778 | "require": {
779 | "php": "^7.0 || ^8.0",
780 | "psr/http-message": "^1.0 || ^2.0"
781 | },
782 | "type": "library",
783 | "extra": {
784 | "branch-alias": {
785 | "dev-master": "1.0.x-dev"
786 | }
787 | },
788 | "autoload": {
789 | "psr-4": {
790 | "Psr\\Http\\Client\\": "src/"
791 | }
792 | },
793 | "notification-url": "https://packagist.org/downloads/",
794 | "license": [
795 | "MIT"
796 | ],
797 | "authors": [
798 | {
799 | "name": "PHP-FIG",
800 | "homepage": "https://www.php-fig.org/"
801 | }
802 | ],
803 | "description": "Common interface for HTTP clients",
804 | "homepage": "https://github.com/php-fig/http-client",
805 | "keywords": [
806 | "http",
807 | "http-client",
808 | "psr",
809 | "psr-18"
810 | ],
811 | "support": {
812 | "source": "https://github.com/php-fig/http-client/tree/1.0.2"
813 | },
814 | "time": "2023-04-10T20:12:12+00:00"
815 | },
816 | {
817 | "name": "psr/http-factory",
818 | "version": "1.0.2",
819 | "source": {
820 | "type": "git",
821 | "url": "https://github.com/php-fig/http-factory.git",
822 | "reference": "e616d01114759c4c489f93b099585439f795fe35"
823 | },
824 | "dist": {
825 | "type": "zip",
826 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
827 | "reference": "e616d01114759c4c489f93b099585439f795fe35",
828 | "shasum": ""
829 | },
830 | "require": {
831 | "php": ">=7.0.0",
832 | "psr/http-message": "^1.0 || ^2.0"
833 | },
834 | "type": "library",
835 | "extra": {
836 | "branch-alias": {
837 | "dev-master": "1.0.x-dev"
838 | }
839 | },
840 | "autoload": {
841 | "psr-4": {
842 | "Psr\\Http\\Message\\": "src/"
843 | }
844 | },
845 | "notification-url": "https://packagist.org/downloads/",
846 | "license": [
847 | "MIT"
848 | ],
849 | "authors": [
850 | {
851 | "name": "PHP-FIG",
852 | "homepage": "https://www.php-fig.org/"
853 | }
854 | ],
855 | "description": "Common interfaces for PSR-7 HTTP message factories",
856 | "keywords": [
857 | "factory",
858 | "http",
859 | "message",
860 | "psr",
861 | "psr-17",
862 | "psr-7",
863 | "request",
864 | "response"
865 | ],
866 | "support": {
867 | "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
868 | },
869 | "time": "2023-04-10T20:10:41+00:00"
870 | },
871 | {
872 | "name": "psr/http-message",
873 | "version": "1.1",
874 | "source": {
875 | "type": "git",
876 | "url": "https://github.com/php-fig/http-message.git",
877 | "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
878 | },
879 | "dist": {
880 | "type": "zip",
881 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
882 | "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
883 | "shasum": ""
884 | },
885 | "require": {
886 | "php": "^7.2 || ^8.0"
887 | },
888 | "type": "library",
889 | "extra": {
890 | "branch-alias": {
891 | "dev-master": "1.1.x-dev"
892 | }
893 | },
894 | "autoload": {
895 | "psr-4": {
896 | "Psr\\Http\\Message\\": "src/"
897 | }
898 | },
899 | "notification-url": "https://packagist.org/downloads/",
900 | "license": [
901 | "MIT"
902 | ],
903 | "authors": [
904 | {
905 | "name": "PHP-FIG",
906 | "homepage": "http://www.php-fig.org/"
907 | }
908 | ],
909 | "description": "Common interface for HTTP messages",
910 | "homepage": "https://github.com/php-fig/http-message",
911 | "keywords": [
912 | "http",
913 | "http-message",
914 | "psr",
915 | "psr-7",
916 | "request",
917 | "response"
918 | ],
919 | "support": {
920 | "source": "https://github.com/php-fig/http-message/tree/1.1"
921 | },
922 | "time": "2023-04-04T09:50:52+00:00"
923 | },
924 | {
925 | "name": "psr/log",
926 | "version": "3.0.0",
927 | "source": {
928 | "type": "git",
929 | "url": "https://github.com/php-fig/log.git",
930 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
931 | },
932 | "dist": {
933 | "type": "zip",
934 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
935 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
936 | "shasum": ""
937 | },
938 | "require": {
939 | "php": ">=8.0.0"
940 | },
941 | "type": "library",
942 | "extra": {
943 | "branch-alias": {
944 | "dev-master": "3.x-dev"
945 | }
946 | },
947 | "autoload": {
948 | "psr-4": {
949 | "Psr\\Log\\": "src"
950 | }
951 | },
952 | "notification-url": "https://packagist.org/downloads/",
953 | "license": [
954 | "MIT"
955 | ],
956 | "authors": [
957 | {
958 | "name": "PHP-FIG",
959 | "homepage": "https://www.php-fig.org/"
960 | }
961 | ],
962 | "description": "Common interface for logging libraries",
963 | "homepage": "https://github.com/php-fig/log",
964 | "keywords": [
965 | "log",
966 | "psr",
967 | "psr-3"
968 | ],
969 | "support": {
970 | "source": "https://github.com/php-fig/log/tree/3.0.0"
971 | },
972 | "time": "2021-07-14T16:46:02+00:00"
973 | },
974 | {
975 | "name": "respect/stringifier",
976 | "version": "0.2.0",
977 | "source": {
978 | "type": "git",
979 | "url": "https://github.com/Respect/Stringifier.git",
980 | "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59"
981 | },
982 | "dist": {
983 | "type": "zip",
984 | "url": "https://api.github.com/repos/Respect/Stringifier/zipball/e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
985 | "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
986 | "shasum": ""
987 | },
988 | "require": {
989 | "php": ">=7.1"
990 | },
991 | "require-dev": {
992 | "friendsofphp/php-cs-fixer": "^2.8",
993 | "malukenho/docheader": "^0.1.7",
994 | "phpunit/phpunit": "^6.4"
995 | },
996 | "type": "library",
997 | "autoload": {
998 | "files": [
999 | "src/stringify.php"
1000 | ],
1001 | "psr-4": {
1002 | "Respect\\Stringifier\\": "src/"
1003 | }
1004 | },
1005 | "notification-url": "https://packagist.org/downloads/",
1006 | "license": [
1007 | "MIT"
1008 | ],
1009 | "authors": [
1010 | {
1011 | "name": "Respect/Stringifier Contributors",
1012 | "homepage": "https://github.com/Respect/Stringifier/graphs/contributors"
1013 | }
1014 | ],
1015 | "description": "Converts any value to a string",
1016 | "homepage": "http://respect.github.io/Stringifier/",
1017 | "keywords": [
1018 | "respect",
1019 | "stringifier",
1020 | "stringify"
1021 | ],
1022 | "support": {
1023 | "issues": "https://github.com/Respect/Stringifier/issues",
1024 | "source": "https://github.com/Respect/Stringifier/tree/0.2.0"
1025 | },
1026 | "time": "2017-12-29T19:39:25+00:00"
1027 | },
1028 | {
1029 | "name": "respect/validation",
1030 | "version": "2.2.4",
1031 | "source": {
1032 | "type": "git",
1033 | "url": "https://github.com/Respect/Validation.git",
1034 | "reference": "d304ace5325efd7180daffb1f8627bb0affd4e3a"
1035 | },
1036 | "dist": {
1037 | "type": "zip",
1038 | "url": "https://api.github.com/repos/Respect/Validation/zipball/d304ace5325efd7180daffb1f8627bb0affd4e3a",
1039 | "reference": "d304ace5325efd7180daffb1f8627bb0affd4e3a",
1040 | "shasum": ""
1041 | },
1042 | "require": {
1043 | "php": "^7.4 || ^8.0 || ^8.1 || ^8.2",
1044 | "respect/stringifier": "^0.2.0",
1045 | "symfony/polyfill-mbstring": "^1.2"
1046 | },
1047 | "require-dev": {
1048 | "egulias/email-validator": "^3.0",
1049 | "malukenho/docheader": "^0.1",
1050 | "mikey179/vfsstream": "^1.6",
1051 | "phpstan/phpstan": "^1.9",
1052 | "phpstan/phpstan-deprecation-rules": "^1.1",
1053 | "phpstan/phpstan-phpunit": "^1.3",
1054 | "phpunit/phpunit": "^9.6",
1055 | "psr/http-message": "^1.0",
1056 | "respect/coding-standard": "^3.0",
1057 | "squizlabs/php_codesniffer": "^3.7",
1058 | "symfony/validator": "^3.0||^4.0"
1059 | },
1060 | "suggest": {
1061 | "egulias/email-validator": "Strict (RFC compliant) email validation",
1062 | "ext-bcmath": "Arbitrary Precision Mathematics",
1063 | "ext-fileinfo": "File Information",
1064 | "ext-mbstring": "Multibyte String Functions"
1065 | },
1066 | "type": "library",
1067 | "autoload": {
1068 | "psr-4": {
1069 | "Respect\\Validation\\": "library/"
1070 | }
1071 | },
1072 | "notification-url": "https://packagist.org/downloads/",
1073 | "license": [
1074 | "MIT"
1075 | ],
1076 | "authors": [
1077 | {
1078 | "name": "Respect/Validation Contributors",
1079 | "homepage": "https://github.com/Respect/Validation/graphs/contributors"
1080 | }
1081 | ],
1082 | "description": "The most awesome validation engine ever created for PHP",
1083 | "homepage": "http://respect.github.io/Validation/",
1084 | "keywords": [
1085 | "respect",
1086 | "validation",
1087 | "validator"
1088 | ],
1089 | "support": {
1090 | "issues": "https://github.com/Respect/Validation/issues",
1091 | "source": "https://github.com/Respect/Validation/tree/2.2.4"
1092 | },
1093 | "time": "2023-02-15T01:05:24+00:00"
1094 | },
1095 | {
1096 | "name": "symfony/deprecation-contracts",
1097 | "version": "v3.2.1",
1098 | "source": {
1099 | "type": "git",
1100 | "url": "https://github.com/symfony/deprecation-contracts.git",
1101 | "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e"
1102 | },
1103 | "dist": {
1104 | "type": "zip",
1105 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e",
1106 | "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e",
1107 | "shasum": ""
1108 | },
1109 | "require": {
1110 | "php": ">=8.1"
1111 | },
1112 | "type": "library",
1113 | "extra": {
1114 | "branch-alias": {
1115 | "dev-main": "3.3-dev"
1116 | },
1117 | "thanks": {
1118 | "name": "symfony/contracts",
1119 | "url": "https://github.com/symfony/contracts"
1120 | }
1121 | },
1122 | "autoload": {
1123 | "files": [
1124 | "function.php"
1125 | ]
1126 | },
1127 | "notification-url": "https://packagist.org/downloads/",
1128 | "license": [
1129 | "MIT"
1130 | ],
1131 | "authors": [
1132 | {
1133 | "name": "Nicolas Grekas",
1134 | "email": "p@tchwork.com"
1135 | },
1136 | {
1137 | "name": "Symfony Community",
1138 | "homepage": "https://symfony.com/contributors"
1139 | }
1140 | ],
1141 | "description": "A generic function and convention to trigger deprecation notices",
1142 | "homepage": "https://symfony.com",
1143 | "support": {
1144 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1"
1145 | },
1146 | "funding": [
1147 | {
1148 | "url": "https://symfony.com/sponsor",
1149 | "type": "custom"
1150 | },
1151 | {
1152 | "url": "https://github.com/fabpot",
1153 | "type": "github"
1154 | },
1155 | {
1156 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
1157 | "type": "tidelift"
1158 | }
1159 | ],
1160 | "time": "2023-03-01T10:25:55+00:00"
1161 | },
1162 | {
1163 | "name": "symfony/http-client",
1164 | "version": "v6.2.10",
1165 | "source": {
1166 | "type": "git",
1167 | "url": "https://github.com/symfony/http-client.git",
1168 | "reference": "3f5545a91c8e79dedd1a06c4b04e1682c80c42f9"
1169 | },
1170 | "dist": {
1171 | "type": "zip",
1172 | "url": "https://api.github.com/repos/symfony/http-client/zipball/3f5545a91c8e79dedd1a06c4b04e1682c80c42f9",
1173 | "reference": "3f5545a91c8e79dedd1a06c4b04e1682c80c42f9",
1174 | "shasum": ""
1175 | },
1176 | "require": {
1177 | "php": ">=8.1",
1178 | "psr/log": "^1|^2|^3",
1179 | "symfony/deprecation-contracts": "^2.1|^3",
1180 | "symfony/http-client-contracts": "^3",
1181 | "symfony/service-contracts": "^1.0|^2|^3"
1182 | },
1183 | "provide": {
1184 | "php-http/async-client-implementation": "*",
1185 | "php-http/client-implementation": "*",
1186 | "psr/http-client-implementation": "1.0",
1187 | "symfony/http-client-implementation": "3.0"
1188 | },
1189 | "require-dev": {
1190 | "amphp/amp": "^2.5",
1191 | "amphp/http-client": "^4.2.1",
1192 | "amphp/http-tunnel": "^1.0",
1193 | "amphp/socket": "^1.1",
1194 | "guzzlehttp/promises": "^1.4",
1195 | "nyholm/psr7": "^1.0",
1196 | "php-http/httplug": "^1.0|^2.0",
1197 | "psr/http-client": "^1.0",
1198 | "symfony/dependency-injection": "^5.4|^6.0",
1199 | "symfony/http-kernel": "^5.4|^6.0",
1200 | "symfony/process": "^5.4|^6.0",
1201 | "symfony/stopwatch": "^5.4|^6.0"
1202 | },
1203 | "type": "library",
1204 | "autoload": {
1205 | "psr-4": {
1206 | "Symfony\\Component\\HttpClient\\": ""
1207 | },
1208 | "exclude-from-classmap": [
1209 | "/Tests/"
1210 | ]
1211 | },
1212 | "notification-url": "https://packagist.org/downloads/",
1213 | "license": [
1214 | "MIT"
1215 | ],
1216 | "authors": [
1217 | {
1218 | "name": "Nicolas Grekas",
1219 | "email": "p@tchwork.com"
1220 | },
1221 | {
1222 | "name": "Symfony Community",
1223 | "homepage": "https://symfony.com/contributors"
1224 | }
1225 | ],
1226 | "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
1227 | "homepage": "https://symfony.com",
1228 | "keywords": [
1229 | "http"
1230 | ],
1231 | "support": {
1232 | "source": "https://github.com/symfony/http-client/tree/v6.2.10"
1233 | },
1234 | "funding": [
1235 | {
1236 | "url": "https://symfony.com/sponsor",
1237 | "type": "custom"
1238 | },
1239 | {
1240 | "url": "https://github.com/fabpot",
1241 | "type": "github"
1242 | },
1243 | {
1244 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
1245 | "type": "tidelift"
1246 | }
1247 | ],
1248 | "time": "2023-04-20T13:12:48+00:00"
1249 | },
1250 | {
1251 | "name": "symfony/http-client-contracts",
1252 | "version": "v3.2.1",
1253 | "source": {
1254 | "type": "git",
1255 | "url": "https://github.com/symfony/http-client-contracts.git",
1256 | "reference": "df2ecd6cb70e73c1080e6478aea85f5f4da2c48b"
1257 | },
1258 | "dist": {
1259 | "type": "zip",
1260 | "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/df2ecd6cb70e73c1080e6478aea85f5f4da2c48b",
1261 | "reference": "df2ecd6cb70e73c1080e6478aea85f5f4da2c48b",
1262 | "shasum": ""
1263 | },
1264 | "require": {
1265 | "php": ">=8.1"
1266 | },
1267 | "suggest": {
1268 | "symfony/http-client-implementation": ""
1269 | },
1270 | "type": "library",
1271 | "extra": {
1272 | "branch-alias": {
1273 | "dev-main": "3.3-dev"
1274 | },
1275 | "thanks": {
1276 | "name": "symfony/contracts",
1277 | "url": "https://github.com/symfony/contracts"
1278 | }
1279 | },
1280 | "autoload": {
1281 | "psr-4": {
1282 | "Symfony\\Contracts\\HttpClient\\": ""
1283 | },
1284 | "exclude-from-classmap": [
1285 | "/Test/"
1286 | ]
1287 | },
1288 | "notification-url": "https://packagist.org/downloads/",
1289 | "license": [
1290 | "MIT"
1291 | ],
1292 | "authors": [
1293 | {
1294 | "name": "Nicolas Grekas",
1295 | "email": "p@tchwork.com"
1296 | },
1297 | {
1298 | "name": "Symfony Community",
1299 | "homepage": "https://symfony.com/contributors"
1300 | }
1301 | ],
1302 | "description": "Generic abstractions related to HTTP clients",
1303 | "homepage": "https://symfony.com",
1304 | "keywords": [
1305 | "abstractions",
1306 | "contracts",
1307 | "decoupling",
1308 | "interfaces",
1309 | "interoperability",
1310 | "standards"
1311 | ],
1312 | "support": {
1313 | "source": "https://github.com/symfony/http-client-contracts/tree/v3.2.1"
1314 | },
1315 | "funding": [
1316 | {
1317 | "url": "https://symfony.com/sponsor",
1318 | "type": "custom"
1319 | },
1320 | {
1321 | "url": "https://github.com/fabpot",
1322 | "type": "github"
1323 | },
1324 | {
1325 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
1326 | "type": "tidelift"
1327 | }
1328 | ],
1329 | "time": "2023-03-01T10:32:47+00:00"
1330 | },
1331 | {
1332 | "name": "symfony/polyfill-ctype",
1333 | "version": "v1.27.0",
1334 | "source": {
1335 | "type": "git",
1336 | "url": "https://github.com/symfony/polyfill-ctype.git",
1337 | "reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
1338 | },
1339 | "dist": {
1340 | "type": "zip",
1341 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
1342 | "reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
1343 | "shasum": ""
1344 | },
1345 | "require": {
1346 | "php": ">=7.1"
1347 | },
1348 | "provide": {
1349 | "ext-ctype": "*"
1350 | },
1351 | "suggest": {
1352 | "ext-ctype": "For best performance"
1353 | },
1354 | "type": "library",
1355 | "extra": {
1356 | "branch-alias": {
1357 | "dev-main": "1.27-dev"
1358 | },
1359 | "thanks": {
1360 | "name": "symfony/polyfill",
1361 | "url": "https://github.com/symfony/polyfill"
1362 | }
1363 | },
1364 | "autoload": {
1365 | "files": [
1366 | "bootstrap.php"
1367 | ],
1368 | "psr-4": {
1369 | "Symfony\\Polyfill\\Ctype\\": ""
1370 | }
1371 | },
1372 | "notification-url": "https://packagist.org/downloads/",
1373 | "license": [
1374 | "MIT"
1375 | ],
1376 | "authors": [
1377 | {
1378 | "name": "Gert de Pagter",
1379 | "email": "BackEndTea@gmail.com"
1380 | },
1381 | {
1382 | "name": "Symfony Community",
1383 | "homepage": "https://symfony.com/contributors"
1384 | }
1385 | ],
1386 | "description": "Symfony polyfill for ctype functions",
1387 | "homepage": "https://symfony.com",
1388 | "keywords": [
1389 | "compatibility",
1390 | "ctype",
1391 | "polyfill",
1392 | "portable"
1393 | ],
1394 | "support": {
1395 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
1396 | },
1397 | "funding": [
1398 | {
1399 | "url": "https://symfony.com/sponsor",
1400 | "type": "custom"
1401 | },
1402 | {
1403 | "url": "https://github.com/fabpot",
1404 | "type": "github"
1405 | },
1406 | {
1407 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
1408 | "type": "tidelift"
1409 | }
1410 | ],
1411 | "time": "2022-11-03T14:55:06+00:00"
1412 | },
1413 | {
1414 | "name": "symfony/polyfill-mbstring",
1415 | "version": "v1.27.0",
1416 | "source": {
1417 | "type": "git",
1418 | "url": "https://github.com/symfony/polyfill-mbstring.git",
1419 | "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
1420 | },
1421 | "dist": {
1422 | "type": "zip",
1423 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
1424 | "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
1425 | "shasum": ""
1426 | },
1427 | "require": {
1428 | "php": ">=7.1"
1429 | },
1430 | "provide": {
1431 | "ext-mbstring": "*"
1432 | },
1433 | "suggest": {
1434 | "ext-mbstring": "For best performance"
1435 | },
1436 | "type": "library",
1437 | "extra": {
1438 | "branch-alias": {
1439 | "dev-main": "1.27-dev"
1440 | },
1441 | "thanks": {
1442 | "name": "symfony/polyfill",
1443 | "url": "https://github.com/symfony/polyfill"
1444 | }
1445 | },
1446 | "autoload": {
1447 | "files": [
1448 | "bootstrap.php"
1449 | ],
1450 | "psr-4": {
1451 | "Symfony\\Polyfill\\Mbstring\\": ""
1452 | }
1453 | },
1454 | "notification-url": "https://packagist.org/downloads/",
1455 | "license": [
1456 | "MIT"
1457 | ],
1458 | "authors": [
1459 | {
1460 | "name": "Nicolas Grekas",
1461 | "email": "p@tchwork.com"
1462 | },
1463 | {
1464 | "name": "Symfony Community",
1465 | "homepage": "https://symfony.com/contributors"
1466 | }
1467 | ],
1468 | "description": "Symfony polyfill for the Mbstring extension",
1469 | "homepage": "https://symfony.com",
1470 | "keywords": [
1471 | "compatibility",
1472 | "mbstring",
1473 | "polyfill",
1474 | "portable",
1475 | "shim"
1476 | ],
1477 | "support": {
1478 | "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
1479 | },
1480 | "funding": [
1481 | {
1482 | "url": "https://symfony.com/sponsor",
1483 | "type": "custom"
1484 | },
1485 | {
1486 | "url": "https://github.com/fabpot",
1487 | "type": "github"
1488 | },
1489 | {
1490 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
1491 | "type": "tidelift"
1492 | }
1493 | ],
1494 | "time": "2022-11-03T14:55:06+00:00"
1495 | },
1496 | {
1497 | "name": "symfony/polyfill-php80",
1498 | "version": "v1.27.0",
1499 | "source": {
1500 | "type": "git",
1501 | "url": "https://github.com/symfony/polyfill-php80.git",
1502 | "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
1503 | },
1504 | "dist": {
1505 | "type": "zip",
1506 | "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
1507 | "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
1508 | "shasum": ""
1509 | },
1510 | "require": {
1511 | "php": ">=7.1"
1512 | },
1513 | "type": "library",
1514 | "extra": {
1515 | "branch-alias": {
1516 | "dev-main": "1.27-dev"
1517 | },
1518 | "thanks": {
1519 | "name": "symfony/polyfill",
1520 | "url": "https://github.com/symfony/polyfill"
1521 | }
1522 | },
1523 | "autoload": {
1524 | "files": [
1525 | "bootstrap.php"
1526 | ],
1527 | "psr-4": {
1528 | "Symfony\\Polyfill\\Php80\\": ""
1529 | },
1530 | "classmap": [
1531 | "Resources/stubs"
1532 | ]
1533 | },
1534 | "notification-url": "https://packagist.org/downloads/",
1535 | "license": [
1536 | "MIT"
1537 | ],
1538 | "authors": [
1539 | {
1540 | "name": "Ion Bazan",
1541 | "email": "ion.bazan@gmail.com"
1542 | },
1543 | {
1544 | "name": "Nicolas Grekas",
1545 | "email": "p@tchwork.com"
1546 | },
1547 | {
1548 | "name": "Symfony Community",
1549 | "homepage": "https://symfony.com/contributors"
1550 | }
1551 | ],
1552 | "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
1553 | "homepage": "https://symfony.com",
1554 | "keywords": [
1555 | "compatibility",
1556 | "polyfill",
1557 | "portable",
1558 | "shim"
1559 | ],
1560 | "support": {
1561 | "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
1562 | },
1563 | "funding": [
1564 | {
1565 | "url": "https://symfony.com/sponsor",
1566 | "type": "custom"
1567 | },
1568 | {
1569 | "url": "https://github.com/fabpot",
1570 | "type": "github"
1571 | },
1572 | {
1573 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
1574 | "type": "tidelift"
1575 | }
1576 | ],
1577 | "time": "2022-11-03T14:55:06+00:00"
1578 | },
1579 | {
1580 | "name": "symfony/service-contracts",
1581 | "version": "v3.2.1",
1582 | "source": {
1583 | "type": "git",
1584 | "url": "https://github.com/symfony/service-contracts.git",
1585 | "reference": "a8c9cedf55f314f3a186041d19537303766df09a"
1586 | },
1587 | "dist": {
1588 | "type": "zip",
1589 | "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a",
1590 | "reference": "a8c9cedf55f314f3a186041d19537303766df09a",
1591 | "shasum": ""
1592 | },
1593 | "require": {
1594 | "php": ">=8.1",
1595 | "psr/container": "^2.0"
1596 | },
1597 | "conflict": {
1598 | "ext-psr": "<1.1|>=2"
1599 | },
1600 | "suggest": {
1601 | "symfony/service-implementation": ""
1602 | },
1603 | "type": "library",
1604 | "extra": {
1605 | "branch-alias": {
1606 | "dev-main": "3.3-dev"
1607 | },
1608 | "thanks": {
1609 | "name": "symfony/contracts",
1610 | "url": "https://github.com/symfony/contracts"
1611 | }
1612 | },
1613 | "autoload": {
1614 | "psr-4": {
1615 | "Symfony\\Contracts\\Service\\": ""
1616 | },
1617 | "exclude-from-classmap": [
1618 | "/Test/"
1619 | ]
1620 | },
1621 | "notification-url": "https://packagist.org/downloads/",
1622 | "license": [
1623 | "MIT"
1624 | ],
1625 | "authors": [
1626 | {
1627 | "name": "Nicolas Grekas",
1628 | "email": "p@tchwork.com"
1629 | },
1630 | {
1631 | "name": "Symfony Community",
1632 | "homepage": "https://symfony.com/contributors"
1633 | }
1634 | ],
1635 | "description": "Generic abstractions related to writing services",
1636 | "homepage": "https://symfony.com",
1637 | "keywords": [
1638 | "abstractions",
1639 | "contracts",
1640 | "decoupling",
1641 | "interfaces",
1642 | "interoperability",
1643 | "standards"
1644 | ],
1645 | "support": {
1646 | "source": "https://github.com/symfony/service-contracts/tree/v3.2.1"
1647 | },
1648 | "funding": [
1649 | {
1650 | "url": "https://symfony.com/sponsor",
1651 | "type": "custom"
1652 | },
1653 | {
1654 | "url": "https://github.com/fabpot",
1655 | "type": "github"
1656 | },
1657 | {
1658 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
1659 | "type": "tidelift"
1660 | }
1661 | ],
1662 | "time": "2023-03-01T10:32:47+00:00"
1663 | },
1664 | {
1665 | "name": "vlucas/phpdotenv",
1666 | "version": "v5.5.0",
1667 | "source": {
1668 | "type": "git",
1669 | "url": "https://github.com/vlucas/phpdotenv.git",
1670 | "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7"
1671 | },
1672 | "dist": {
1673 | "type": "zip",
1674 | "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7",
1675 | "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7",
1676 | "shasum": ""
1677 | },
1678 | "require": {
1679 | "ext-pcre": "*",
1680 | "graham-campbell/result-type": "^1.0.2",
1681 | "php": "^7.1.3 || ^8.0",
1682 | "phpoption/phpoption": "^1.8",
1683 | "symfony/polyfill-ctype": "^1.23",
1684 | "symfony/polyfill-mbstring": "^1.23.1",
1685 | "symfony/polyfill-php80": "^1.23.1"
1686 | },
1687 | "require-dev": {
1688 | "bamarni/composer-bin-plugin": "^1.4.1",
1689 | "ext-filter": "*",
1690 | "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25"
1691 | },
1692 | "suggest": {
1693 | "ext-filter": "Required to use the boolean validator."
1694 | },
1695 | "type": "library",
1696 | "extra": {
1697 | "bamarni-bin": {
1698 | "bin-links": true,
1699 | "forward-command": true
1700 | },
1701 | "branch-alias": {
1702 | "dev-master": "5.5-dev"
1703 | }
1704 | },
1705 | "autoload": {
1706 | "psr-4": {
1707 | "Dotenv\\": "src/"
1708 | }
1709 | },
1710 | "notification-url": "https://packagist.org/downloads/",
1711 | "license": [
1712 | "BSD-3-Clause"
1713 | ],
1714 | "authors": [
1715 | {
1716 | "name": "Graham Campbell",
1717 | "email": "hello@gjcampbell.co.uk",
1718 | "homepage": "https://github.com/GrahamCampbell"
1719 | },
1720 | {
1721 | "name": "Vance Lucas",
1722 | "email": "vance@vancelucas.com",
1723 | "homepage": "https://github.com/vlucas"
1724 | }
1725 | ],
1726 | "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
1727 | "keywords": [
1728 | "dotenv",
1729 | "env",
1730 | "environment"
1731 | ],
1732 | "support": {
1733 | "issues": "https://github.com/vlucas/phpdotenv/issues",
1734 | "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0"
1735 | },
1736 | "funding": [
1737 | {
1738 | "url": "https://github.com/GrahamCampbell",
1739 | "type": "github"
1740 | },
1741 | {
1742 | "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
1743 | "type": "tidelift"
1744 | }
1745 | ],
1746 | "time": "2022-10-16T01:01:54+00:00"
1747 | }
1748 | ],
1749 | "packages-dev": [],
1750 | "aliases": [],
1751 | "minimum-stability": "stable",
1752 | "stability-flags": [],
1753 | "prefer-stable": false,
1754 | "prefer-lowest": false,
1755 | "platform": [],
1756 | "platform-dev": [],
1757 | "plugin-api-version": "2.3.0"
1758 | }
1759 |
--------------------------------------------------------------------------------
/config.php:
--------------------------------------------------------------------------------
1 | [
5 | 'scheme' => 'tcp',
6 | 'host' => 'localhost',
7 | 'port' => 6379,
8 | 'index_name' => 'laravel-docs-index'
9 | ],
10 | 'openai' => [
11 | 'api_key' => $_ENV['OPENAI_API_KEY'],
12 | 'embeddings_model' => 'text-embedding-ada-002',
13 | 'completions_model' => 'gpt-3.5-turbo',
14 | ],
15 | 'docs_path' => 'docs/'
16 | ];
17 |
--------------------------------------------------------------------------------
/helpers.php:
--------------------------------------------------------------------------------
1 | openai = OpenAI::client(config('openai.api_key'));
16 | $this->redis = new RedisHelper();
17 | }
18 |
19 | public function start()
20 | {
21 | if (!$this->redis->hasIndex(config('redis.index_name'))) {
22 | echo "Index doesn't exist so we'll create it (this only happens one time)...\n";
23 | $docLoader = new DocumentationLoader($this->redis, $this->openai);
24 | $docLoader->loadDocs();
25 | }
26 |
27 | $chatHistroy = [];
28 | while (true) {
29 | $question = readline("Type your question ('quit' to stop): ");
30 | if (trim($question) == 'quit') {
31 | exit();
32 | }
33 |
34 | $question = $this->questionWithHistory($question, $chatHistroy);
35 | $chatHistroy[] = 'User: ' . $question;
36 | $relevantDocs = $this->relevantDocs($question);
37 | $questionWithContext = $this->questionWithContext($question, array_column($relevantDocs, 'text'));
38 | $response = $this->chat($questionWithContext);
39 | $chatHistroy[] = 'Assistant: ' . $question;
40 | $this->showResponse($response, array_column($relevantDocs, 'url'));
41 | }
42 | }
43 |
44 | // We ask OpenAI to create a question that contains the whole context of the
45 | // conversation so we can send a single question with the context and give
46 | // the illusion that it remembers the conversation history
47 | public function questionWithHistory($question, $chatHistory)
48 | {
49 | if (! $chatHistory) {
50 | return $question;
51 | }
52 |
53 | $template = <<<'TEXT'
54 | Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.
55 |
56 | Chat History:
57 | {chat_history}
58 | Follow Up Input: {question}
59 | Standalone question:
60 | TEXT;
61 |
62 | $template = str_replace('{question}', $question, $template);
63 | $template = str_replace('{chat_history}', implode("\n", $chatHistory), $template);
64 |
65 | return $this->chat($template);
66 | }
67 |
68 | public function chat($message)
69 | {
70 | $response = $this->openai->chat()->create([
71 | 'model' => config('openai.completions_model'),
72 | 'messages' => [
73 | ['role' => 'user', 'content' => $message],
74 | ],
75 | ]);
76 |
77 | return $response->choices[0]->message->content;
78 | }
79 |
80 | // We convert the question into the embedding representation using OpenAI
81 | // embeddings endpoint.
82 | // We then use those embeddings to get the 4 most semantically similar documentation sections,
83 | //
84 | // We will send those sections as context to OpenAI so it can answer the question better.
85 | public function relevantDocs($question)
86 | {
87 | $result = $this->openai->embeddings()->create([
88 | 'input' => $question,
89 | 'model' => config('openai.embeddings_model')
90 | ]);
91 |
92 | // Encode the embeddings into a byte string. See helpers.php
93 | $packed = encode($result->embeddings[0]->embedding);
94 | return $this->redis->vectorSearch(config('redis.index_name'), 4, $packed, vectorName: "content_vector", returnFields: ['text', 'url']);
95 | }
96 |
97 | // We build the propmt with the context (the previously retrieved documentation sections)
98 | public function questionWithContext($question, $context)
99 | {
100 | $template = <<<'TEXT'
101 | Answer the question based on the context below. With code samples if possible.
102 |
103 | Context:
104 | {context}
105 |
106 | question: {question}
107 | TEXT;
108 |
109 | $template = str_replace('{question}', $question, $template);
110 | $template = str_replace('{context}', implode("\n", $context), $template);
111 |
112 | return $template;
113 | }
114 |
115 | public function showResponse($response, $sources)
116 | {
117 | // The weird \e stuff is to add colors to the terminal output :)
118 | echo "\e[42m\e[30mOpenAI:\e[0m\e[32m " . $response . "\n\e[0m\e[43m\e[30mSources:\e[0m\e[96m\n" . implode("\n", $sources) . "\e[0m\n\n";
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/DocumentationLoader.php:
--------------------------------------------------------------------------------
1 | setIndex(config('redis.index_name'))
21 | ->setPrefixes(['doc'])
22 | ->addTextField('text')
23 | ->addTextField('url')
24 | // This is the field used to get the relevant documentation sections.
25 | ->addVectorField(
26 | 'content_vector',
27 | VectorFieldOption::ALGORITHM_FLAT,
28 | VectorFieldOption::TYPE_FLOAT32,
29 | // OpenAI embeddings size, as in the embedding endpoint returns
30 | // an array of 1536 items.
31 | 1536,
32 | VectorFieldOption::DISTANCE_METRIC_COSINE
33 | );
34 |
35 | $this->redis->createIndex($builder);
36 |
37 | $this->batchLoad();
38 | }
39 |
40 | // We iterate over every file in the documentation
41 | // and we split them in chunks, each chunk represents a
42 | // section of the documentation.
43 | //
44 | // it returns an array of documentation section with "text"
45 | // and "urls"
46 | public function parseDocs($path)
47 | {
48 | echo "Parsing doc files...\n";
49 | $files = scandir($path);
50 | $docs = [];
51 | $base_url = "https://laravel.com/docs/10.x/";
52 | foreach ($files as $file) {
53 | if (!str_ends_with($file, '.md')) {
54 | continue;
55 | }
56 |
57 | $url = $base_url . explode('.', $file)[0];
58 | $fullPath = rtrim($path, '/') . '/' . $file;
59 | $contents = file_get_contents($fullPath);
60 | preg_match_all("/name=\"(?.+?)\"><\/a>\n(?#.+?)(?: [
63 | 'url' => "{$url}#{$match['link']}",
64 | 'text' => $match['text'],
65 | ], $matches);
66 |
67 | $docs = array_merge($docs, $sections);
68 | }
69 |
70 | return $docs;
71 | }
72 |
73 | // Download the documentation from github
74 | // and unzip it into the given path
75 | public function downloadDocs($path)
76 | {
77 | echo "Downloading and unzipping docs...\n";
78 | $content = file_get_contents('https://github.com/laravel/docs/archive/refs/heads/10.x.zip');
79 | $file = fopen('./temp.zip', 'w+');
80 | fwrite($file, $content);
81 | fclose($file);
82 | $zip = new ZipArchive();
83 | $zip->open('./temp.zip');
84 | $zip->extractTo($path);
85 | $zip->close();
86 | unlink('./temp.zip');
87 | }
88 |
89 | // We split the documentation sections into batches
90 | // of 10 sections at a time, this is to send 10 sections at the
91 | // same time to OpenAI to get the embeddings for the text
92 | // we then put those embeddings into the 'content_vector'
93 | // field and store the section in redis
94 | public function batchLoad()
95 | {
96 | $this->downloadDocs(config('docs_path'));
97 | $docs = $this->parseDocs(config('docs_path') . '/docs-10.x');
98 | $batchSize = 10;
99 | $currentKeyCount = 0;
100 | echo "Storing documents in redis...\n";
101 | $progressBar = new Bar(count($docs));
102 | foreach (array_chunk($docs, $batchSize) as $chunk) {
103 | // Convert the text from each section into the embeddings
104 | // representation using the OpenAI embedding endpoint
105 | $texts = array_column($chunk, 'text');
106 | $response = $this->openai->embeddings()->create([
107 | 'input' => $texts,
108 | 'model' => config('openai.embeddings_model')
109 | ]);
110 |
111 | foreach ($response->embeddings as $index => $embeddingsResponse) {
112 | // We have to store the embeddings as bytes in redis so we convert
113 | // them to a byte string using the encode() function. See helpers.php
114 | $packed = encode($embeddingsResponse->embedding);
115 | $chunk[$index]['content_vector'] = $packed;
116 | $this->redis->hset("doc:{$currentKeyCount}", $chunk[$index]);
117 | $currentKeyCount++;
118 | $progressBar->tick();
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/RedisHelper.php:
--------------------------------------------------------------------------------
1 | predisClient = new Client([
21 | 'scheme' => config('redis.scheme'),
22 | 'host' => config('redis.host'),
23 | 'port' => config('redis.port'),
24 | ]);
25 |
26 | // Check Connection
27 | $this->predisClient->ping();
28 |
29 | $this->redisSearchClient = $clientFacade->getClient(new Client([
30 | 'scheme' => config('redis.scheme'),
31 | 'host' => config('redis.host'),
32 | 'port' => config('redis.port'),
33 | ]));
34 | }
35 |
36 | public function hasIndex($name): bool
37 | {
38 | $result = $this->redisSearchClient->executeRaw('FT._LIST');
39 | return in_array($name, $result);
40 | }
41 |
42 | public function createIndex(IndexBuilder $indexBuilder)
43 | {
44 | $indexBuilder->create($this->redisSearchClient, AbstractCommand::MAX_IMPLEMENTED_VERSION);
45 | }
46 |
47 | public function hset($key, $document)
48 | {
49 | foreach ($document as $field => $value) {
50 | $this->predisClient->hset($key, $field, $value);
51 | }
52 | }
53 |
54 | public function vectorSearch(string $indexName, int $k, string $blobVector, string $vectorName, array $returnFields)
55 | {
56 | $rawDocs = $this->predisClient->executeRaw([
57 | 'FT.SEARCH',
58 | $indexName,
59 | '*=>[KNN ' . $k . ' @' . $vectorName . ' $vector as vector_score]',
60 | "PARAMS", "2", "vector", $blobVector,
61 | "RETURN", count($returnFields) + 1, ...$returnFields, 'vector_score',
62 | "SORTBY", "vector_score", "DIALECT", "2"
63 | ]);
64 | // $rawDocs contains the results returned in the following format :
65 | // [
66 | // 0 => amount of results,
67 | // 1 => first result id,
68 | // 2 => [
69 | // key1,
70 | // value1,
71 | // key2,
72 | // valu2,
73 | // ...
74 | // ],
75 | // 1 => second result id,
76 | // 2 => [
77 | // key1,
78 | // value1,
79 | // key2,
80 | // valu2,
81 | // ...
82 | // ],
83 | // ...
84 | // ]
85 | $docs = [];
86 | for($x = 1; $x < count($rawDocs); $x += 2) {
87 | $doc = [];
88 | $currentDoc = $rawDocs[$x + 1];
89 |
90 | for ($y = 0; $y < count($currentDoc); $y +=2) {
91 | $doc[$currentDoc[$y]] = $currentDoc[$y + 1];
92 | }
93 |
94 | $docs[$rawDocs[$x]] = $doc;
95 | }
96 |
97 | return $docs;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------