├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples
├── README.md
├── advanced
│ ├── README.md
│ └── src
│ │ ├── customvectordb.py
│ │ ├── functions
│ │ ├── get_random_number
│ │ │ ├── function.py
│ │ │ └── test.json
│ │ └── weather
│ │ │ ├── get_current_weather
│ │ │ ├── function.py
│ │ │ ├── test.json
│ │ │ └── types.py
│ │ │ └── get_forecast_weather
│ │ │ ├── function.py
│ │ │ ├── test.json
│ │ │ └── types.py
│ │ └── main.py
└── basic
│ ├── README.md
│ └── src
│ ├── functions
│ └── get_current_weather
│ │ ├── function.py
│ │ └── test.json
│ └── main.py
├── poetry.lock
├── pyproject.toml
└── sageai
├── __init__.py
├── config.py
├── sageai.py
├── services
├── defaultvectordb_service.py
└── openai_service.py
├── tests
├── integration.py
├── main.py
├── unit.py
└── utils.py
├── types
├── abstract_vectordb.py
├── function.py
└── log_level.py
└── utils
├── file_utilities.py
├── format_config_args.py
├── inspection_utilities.py
├── logger.py
├── model_utilities.py
└── openai_utilities.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | .idea/
157 |
158 | # SageAI
159 | sageai/env.json
160 |
161 | # VSCode
162 | .vscode/
163 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xnenlabs/SageAI/55ad6f1201dd84aa75115a1636ddd094659b7bdd/CHANGELOG.md
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When it comes to open source, there are different ways you can contribute, all
4 | of which are valuable. Here's few guidelines that should help you as you prepare
5 | your contribution.
6 |
7 | ## Initial steps
8 |
9 | Before you start working on a contribution, create an issue describing what you want to build. It's possible someone
10 | else is already working on something similar, or perhaps there is a reason that feature isn't implemented.
11 | The maintainers will point you in the right direction.
12 |
13 |
22 |
23 | ### Documentation
24 |
25 | The SageAI documentation lives in the README.md. Be sure to document any API changes you implement.
26 |
27 | ## License
28 |
29 | By contributing your code to the SageAI GitHub repository, you agree to
30 | license your contribution under the MIT license.
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 0xnenlabs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 | Folder-based functions for ChatGPT's function calling with Pydantic support 🚀
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | SageAI is a framework for GPT 3.5/4 function calling for creating folder-based functions that is easy to organize and
17 | scale.
18 |
19 | With a built-in vector database used to store and retrieve functions, the number of tokens sent to the model is
20 | significantly reduced, making it faster and cheaper to call your functions.
21 |
22 | Read the blog post for a more in-depth explanation of the motivation behind SageAI [here](https://0xnen.com/blog/sageai).
23 |
24 | ## Table of Contents
25 |
26 | - [Key Features](#key-features)
27 | - [Requirements](#requirements)
28 | - [Installation](#installation)
29 | - [Design](#design)
30 | - [Setup](#setup)
31 | - [Convention](#convention)
32 | - [API](#api)
33 | - [SageAI Initialize](#sageai-initialize)
34 | - [SageAI Methods](#sageai-methods)
35 | - [Vector DB](#vector-db)
36 | - [Testing](#testing)
37 | - [Unit Tests](#unit-tests)
38 | - [Integration Tests](#integration-tests)
39 | - [Output Equality](#output-equality)
40 | - [CLI](#cli)
41 | - [Examples](#examples)
42 | - [Roadmap](#roadmap)
43 | - [Contributing](#contributing)
44 |
45 | ## Key Features
46 |
47 | - Function organization through folder-centric functions.
48 | - Strong typing for functions using Pydantic.
49 | - Built-in Qdrant vector database with in-memory support for function storage and retrieval, with the option to
50 | integrate your own.
51 | - Easily test each function with an associated `test.json` file, supporting both unit and integration tests.
52 | - Built with CI/CD in mind, ensuring synchronicity between your vector db and the functions directory across all
53 | environments using the `index` method.
54 | - Lightweight implementation with only three dependencies:
55 | - `openai`
56 | - `pydantic`
57 | - `qdrant-client`
58 |
59 | ## Requirements
60 |
61 | ```
62 | python >=3.9, <3.12
63 | pydantic >=1.6, <=1.10.12
64 | openai >=0.27.0
65 | qdrant-client >=1.4.0
66 | ```
67 |
68 | ## Installation
69 |
70 | ```bash
71 | # pip
72 | $ pip install sageai
73 |
74 | # poetry
75 | $ poetry add sageai
76 | ```
77 |
78 | ## Design
79 |
80 | 
81 |
82 | SageAI is built around the concept of a `functions` directory, which contains all of your functions. Each function is
83 | defined in a Python file `function.py`, and is associated with an optional `test.json` file for testing.
84 |
85 | The format of the `function.py` file must contain two things in order for SageAI to work:
86 |
87 | 1. The function itself
88 | 2. The `Function` object
89 |
90 | Input and output types may be defined using Pydantic models, and are automatically validated by SageAI. They can also be
91 | defined outside the `function.py` file, and imported into the file.
92 |
93 | Here is a simplified example of how SageAI might handle a function that fetches the current weather for a given
94 | location.
95 |
96 | ```python
97 | # functions/get_current_weather/function.py
98 | from enum import Enum
99 | from typing import Optional
100 |
101 | from pydantic import BaseModel, Field
102 |
103 | from sageai.types.function import Function
104 |
105 |
106 | class UnitTypes(str, Enum):
107 | CELSIUS = "Celsius"
108 | FAHRENHEIT = "Fahrenheit"
109 |
110 |
111 | class FunctionInput(BaseModel):
112 | location: str = Field(
113 | ..., description="The city, e.g. San Francisco"
114 | )
115 | unit: Optional[UnitTypes] = Field(
116 | UnitTypes.CELSIUS, description="The unit of temperature."
117 | )
118 |
119 |
120 | class FunctionOutput(BaseModel):
121 | weather: str
122 |
123 | def __eq__(self, other):
124 | if not isinstance(other, FunctionOutput):
125 | return False
126 | return self.weather == other.weather
127 |
128 |
129 | def get_current_weather(params: FunctionInput) -> FunctionOutput:
130 | weather = (
131 | f"The weather in {params.location} is currently 22 degrees {params.unit.value}."
132 | )
133 | return FunctionOutput(weather=weather)
134 |
135 |
136 | function = Function(
137 | function=get_current_weather,
138 | description="Get the current weather in a given location.",
139 | )
140 | ```
141 |
142 | We'll break down the above example into its components below.
143 |
144 | ## Setup
145 |
146 | Create a `functions` directory in the root directory, and add your functions as described in [Design](#design).
147 |
148 | Then initialize `SageAI`.
149 |
150 | ```python
151 | from sageai import SageAI
152 |
153 | sage = SageAI(openai_key="")
154 | ```
155 |
156 | Then index the vector database.
157 |
158 | ```python
159 | sage.index()
160 | ```
161 |
162 | That's it! You're now set up and ready to interact with SageAI through natural language queries. 🚀
163 |
164 | ```python
165 | message = "What's the weather like in Toronto right now?"
166 | response = sage.chat(
167 | messages=[dict(role="user", content=message)],
168 | model="gpt-3.5-turbo-0613",
169 | top_n=5,
170 | )
171 | # response:
172 | # {
173 | # 'name': 'get_current_weather',
174 | # 'args': {'location': 'Toronto'},
175 | # 'result': {'weather': 'The weather in Toronto is currently 22 degrees Celsius.'}
176 | # }
177 | ```
178 |
179 | ## Convention
180 |
181 | SageAI follows a convention over configuration approach to make it easy to define functions.
182 |
183 | Ensure that your `function.py` file contains the following:
184 |
185 | 1. A `function` object that is an instance of `Function`.
186 | 2. A function that is the actual function that will be called by ChatGPT.
187 | 3. The function **must** have typed input and output Pydantic models.
188 | 4. Each field in the input model **must** have a description.
189 |
190 | Minimal example:
191 |
192 | ```python
193 | def my_function(params: PydanticInput) -> PydanticOutput:
194 | return PydanticOutput(...)
195 |
196 |
197 | function = Function(
198 | function=my_function,
199 | description="My function description.",
200 | )
201 | ```
202 |
203 | ## API
204 |
205 | ### SageAI Initialize
206 |
207 | The `SageAI` constructor accepts the following parameters:
208 |
209 | | Parameter | Description | Defaults |
210 | |-------------------------|-----------------------------------------------------------------------------|--------------------------|
211 | | **openai_key** | The API key for OpenAI. | _Required_ |
212 | | **functions_directory** | Directory containing functions. | `/functions` |
213 | | **vectordb** | An implementation of the `AbstractVectorDB` for vector database operations. | `DefaultVectorDBService` |
214 | | **log_level** | Desired log level for the operations. | `ERROR` |
215 |
216 | ### SageAI Methods
217 |
218 | #### 1. `chat`
219 |
220 | Initiate a chat using OpenAI's API and the provided parameters.
221 |
222 | **Parameters**:
223 |
224 | | Parameter | Description | Defaults |
225 | |-----------|---------------------------------------------------------------------------------------------------------------------|------------|
226 | | - | Accepts the same parameters as OpenAI's [chat endpoint](https://platform.openai.com/docs/api-reference/chat/create) | - |
227 | | **top_n** | The number of top functions to consider from the vector database. | _Required_ |
228 |
229 | **Returns**:
230 |
231 | ```python
232 | dict(
233 | name="function_name",
234 | args={"arg1": "value1", "arg2": "value2"},
235 | result={"out1": "value1", "out2": "value2"}, # Optional
236 | error="", # Optional
237 | )
238 | ```
239 |
240 | > Either `result` or `error` will be present in the response, but not both.
241 |
242 | ---
243 |
244 | #### 2. `get_top_n_functions`
245 |
246 | Get the top `n` functions from the vector database based on a query.
247 |
248 | **Parameters**:
249 |
250 | | Parameter | Description | Defaults |
251 | |-----------|------------------------------------|------------|
252 | | **query** | The query to search against. | _Required_ |
253 | | **top_n** | The number of functions to return. | _Required_ |
254 |
255 | **Returns**:
256 |
257 | - A dict of function names to `Function` definitions.
258 |
259 | ---
260 |
261 | #### 3. `run_function`
262 |
263 | Execute a function based on its name and provided arguments.
264 |
265 | **Parameters**:
266 |
267 | | Parameter | Description | Defaults |
268 | |-----------|------------------------------------|------------|
269 | | **name** | Name of the function. | _Required_ |
270 | | **args** | Arguments to pass to the function. | _Required_ |
271 |
272 | **Returns**:
273 |
274 | - The function result as a dict.
275 |
276 | ---
277 |
278 | #### 4. `call_openai`
279 |
280 | Calls the OpenAI API with the provided parameters.
281 |
282 | **Parameters**:
283 |
284 | | Parameter | Description | Defaults |
285 | |-------------------|---------------------------------------------------------------------------------------------------------------------|------------|
286 | | **openai_args** | Accepts the same parameters as OpenAI's [chat endpoint](https://platform.openai.com/docs/api-reference/chat/create) | _Required_ |
287 | | **top_functions** | List of dicts that is a representation of your functions. | _Required_ |
288 |
289 | **Returns**:
290 |
291 | - A tuple of the function name and the function args.
292 |
293 | ---
294 |
295 | #### 5. `index`
296 |
297 | Index the vector database based on the functions directory.
298 | This method is useful to update the vectordb when new functions are added or existing ones are updated.
299 |
300 | ---
301 |
302 | Want more control?
303 |
304 | > The `chat` function uses `get_top_n_functions`, `run_function`, and `call_openai` internally.
305 | > However, we also expose these methods incase you wish to use them directly to implement your own `chat` logic.
306 |
307 | ---
308 |
309 | ### Vector DB
310 |
311 | SageAI comes with a built-in in-memory vector database, Qdrant, which is used to store and retrieve functions.
312 |
313 | If you wish to use your own vector database, you can implement the `AbstractVectorDB` class and pass it into the
314 | `SageAI` constructor.
315 |
316 | See the [advanced example](/examples/advanced) for an example of how to integrate your own vector database.
317 |
318 | ## Testing
319 |
320 | As for the optional `test.json` file in each function, follow this structure:
321 |
322 | ```json
323 | [
324 | {
325 | "message": "What's the weather like in Toronto right now?",
326 | "input": {
327 | "location": "Toronto",
328 | "unit": "Celsius"
329 | },
330 | "output": {
331 | "weather": "The weather in Toronto is currently 22 degrees Celsius."
332 | }
333 | }
334 | ]
335 | ```
336 |
337 | - Each object in the array represents a test case.
338 | - The `message` field is the natural language message that will be sent
339 | to ChatGPT, and the `input` field is the expected input that will be passed to the function.
340 | - The `output` field is the
341 | expected output of the function.
342 |
343 | SageAI offers unit and integration tests.
344 |
345 | ---
346 |
347 | ### Unit Tests
348 |
349 | > Unit tests do not call the vector database nor ChatGPT, and **will not** cost you OpenAI credits.
350 |
351 | - Unit tests are used to ensure your functions directory is valid, and it tests the function in isolation.
352 | - It tests whether:
353 | - the `functions` directory exists.
354 | - each function has a `function.py` file.
355 | - each `function.py` file has a `Function` object.
356 | - and more!
357 | - It also tests whether the input and output types are valid, and whether the function returns the expected output based
358 | on
359 | the input alone by calling `func(test_case["input"]) == test_case["output"]`.
360 |
361 | ---
362 |
363 | ### Integration Tests
364 |
365 | > Integration tests will call the vector database and ChatGPT, and **will** cost you OpenAI credits.
366 |
367 | - Integration tests are used to test the function by calling ChatGPT and the vector database.
368 | - They test whether the vector database is able to retrieve the function, and whether ChatGPT can call the function
369 | with the given input and return the expected output.
370 |
371 | > Because ChatGPT's responses can vary, integration tests may return different results each time.
372 | > It's important to use integration tests as a tool to ensure ChatGPT is able to call the right function with the right
373 | > input, and not as a definitive test to measure the test rate of your functions.
374 |
375 | ---
376 |
377 | ### Output Equality
378 |
379 | You can customize how to determine equality between the expected and actual output by overriding the `__eq__`
380 | method in the output model.
381 |
382 | ```python
383 | class FunctionOutput(BaseModel):
384 | weather: str
385 | temperature: int
386 |
387 | def __eq__(self, other):
388 | if not isinstance(other, FunctionOutput):
389 | return False
390 | return self.weather == other.weather
391 | ```
392 |
393 | In the case above, we only care about the `weather` field, and not the `temperature` field. Therefore, we only compare
394 | the `weather` field in the `__eq__` method.
395 |
396 | This is especially useful when you are returning an object from a database, for example, and you only care to test
397 | against a subset of the fields (for example, the `id` field).
398 |
399 | ---
400 |
401 | ### CLI
402 |
403 | ```bash
404 | # To run unit and integration tests for all functions:
405 | poetry run sageai-tests --apikey=openapikey --directory=path/to/functions
406 |
407 | # To run unit tests only for all functions:
408 | poetry run sageai-tests --apikey=openapikey --directory=path/to/functions --unit
409 |
410 | # To run integration tests only for all functions:
411 | poetry run sageai-tests --apikey=openapikey --directory=path/to/functions --integration
412 |
413 | # To run unit and integration tests for a specific function:
414 | poetry run sageai-tests --apikey=openapikey --directory=path/to/functions/get_current_weather
415 | ```
416 |
417 | | Parameter | Description | Defaults |
418 | |-------------------|---------------------------------------------------------------|--------------|
419 | | **--apikey** | OpenAI API key. | _Required_ |
420 | | **--directory** | Directory of the functions or of the specific function to run | _/functions_ |
421 | | **--unit** | Only run unit tests | false |
422 | | **--integration** | Only run integration tests | false |
423 |
424 | ## Examples
425 |
426 | 1. [Basic](/examples/basic) - Get started with a simple SageAI function.
427 | 2. [Advanced](/examples/advanced) - Dive deeper with more intricate functionalities and use-cases.
428 |
429 | ## Roadmap
430 |
431 | - [ ] Add tests and code coverage
432 | - [ ] Support multiple function calls
433 | - [ ] Support streaming
434 | - [ ] Support asyncio
435 | - [ ] Support Pydantic V2
436 | - [ ] Write Chainlit example
437 | - [ ] Write fullstack example
438 |
439 | ## Contributing
440 |
441 | Interested in contributing to SageAI? Please see our [CONTRIBUTING.md](/CONTRIBUTING.md) for guidelines, coding
442 | standards, and other details.
443 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | Check out the examples of how to use `SageAI`! 🚀
4 |
--------------------------------------------------------------------------------
/examples/advanced/README.md:
--------------------------------------------------------------------------------
1 | ## Advanced Example
2 |
3 | This example shows a more advanced version of `SageAI` with 3 functions, `get_current_weather`, `get_forecast_weather`,
4 | and `get_random_number`.
5 |
6 | You can also nest your function folders; for example, you can have a folder called `weather` and
7 | inside that folder you can have folders called `get_current_weather` and `get_forecast_weather`, each containing their
8 | own implementation. This is useful if you have a lot of functions and want to organize them into folders.
9 |
10 | We also demonstrate how to use your own custom vector database implementation, which in this example uses `vectordb`
11 | instead of `qdrant`.
12 |
--------------------------------------------------------------------------------
/examples/advanced/src/customvectordb.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from vectordb import Memory
4 |
5 | from sageai.types.abstract_vectordb import AbstractVectorDB
6 |
7 |
8 | class CustomVectorDB(AbstractVectorDB):
9 | def __init__(self):
10 | super().__init__()
11 | self.memory = Memory()
12 |
13 | def index(self):
14 | self.memory.clear()
15 | for func_name, func in self.function_map.items():
16 | self.memory.save(func_name, func)
17 |
18 | def search(self, query: str, n: int) -> List[str]:
19 | return self.memory.search(query, top_n=n)
20 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/get_random_number/function.py:
--------------------------------------------------------------------------------
1 | import random
2 | from enum import Enum
3 | from typing import Optional
4 |
5 | from pydantic import BaseModel, Field
6 |
7 | from sageai.types.function import Function
8 |
9 |
10 | class UnitTypes(str, Enum):
11 | CELSIUS = "celsius"
12 | FAHRENHEIT = "fahrenheit"
13 |
14 |
15 | class FunctionInput(BaseModel):
16 | min: Optional[int] = Field(0, description="The minimum number.")
17 | max: Optional[int] = Field(100, description="The maximum number.")
18 |
19 |
20 | class FunctionOutput(BaseModel):
21 | number: int
22 |
23 | # In this case, we don't have a way to determine if the output is correct, so we just return True to make
24 | # the tests pass.
25 | def __eq__(self, other):
26 | if not isinstance(other, FunctionOutput):
27 | return False
28 | return True
29 |
30 |
31 | def get_random_number(params: FunctionInput) -> FunctionOutput:
32 | number = random.randint(params.min, params.max)
33 | return FunctionOutput(number=number)
34 |
35 |
36 | function = Function(
37 | function=get_random_number,
38 | description="Get a random number.",
39 | )
40 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/get_random_number/test.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/weather/get_current_weather/function.py:
--------------------------------------------------------------------------------
1 | from sageai.types.function import Function
2 | from examples.advanced.src.functions.weather.get_current_weather.types import (
3 | FunctionInput,
4 | FunctionOutput,
5 | )
6 |
7 |
8 | def get_current_weather(params: FunctionInput) -> FunctionOutput:
9 | weather = (
10 | f"The weather in {params.location} is currently 22 degrees {params.unit.value}."
11 | )
12 | return FunctionOutput(weather=weather)
13 |
14 |
15 | function = Function(
16 | function=get_current_weather,
17 | description="Get the current weather in a given location.",
18 | )
19 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/weather/get_current_weather/test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "What's the weather like in Toronto right now?",
4 | "input": {
5 | "location": "Toronto",
6 | "unit": "Celsius"
7 | },
8 | "output": {
9 | "weather": "The weather in Toronto is currently 22 degrees Celsius."
10 | }
11 | },
12 | {
13 | "message": "What's the temperature in New York, NY right now in Fahrenheit?",
14 | "input": {
15 | "location": "New York, NY",
16 | "unit": "Fahrenheit"
17 | },
18 | "output": {
19 | "weather": "The weather in New York, NY is currently 22 degrees Fahrenheit."
20 | }
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/weather/get_current_weather/types.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Optional
3 |
4 | from pydantic import BaseModel, Field
5 |
6 |
7 | class UnitTypes(str, Enum):
8 | CELSIUS = "Celsius"
9 | FAHRENHEIT = "Fahrenheit"
10 |
11 |
12 | class FunctionInput(BaseModel):
13 | location: str = Field(..., description="The city, e.g. San Francisco")
14 | unit: Optional[UnitTypes] = Field(
15 | UnitTypes.CELSIUS, description="The unit of temperature."
16 | )
17 |
18 |
19 | class FunctionOutput(BaseModel):
20 | weather: str
21 |
22 | def __eq__(self, other):
23 | if not isinstance(other, FunctionOutput):
24 | return False
25 | return self.weather == other.weather
26 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/weather/get_forecast_weather/function.py:
--------------------------------------------------------------------------------
1 | from sageai.types.function import Function
2 | from examples.advanced.src.functions.weather.get_forecast_weather.types import (
3 | FunctionInput,
4 | FunctionOutput,
5 | )
6 |
7 |
8 | def some_helper(params: FunctionInput) -> str:
9 | return (
10 | f"The weather {params.forecast} in {params.location} is going to be 22 degrees."
11 | )
12 |
13 |
14 | def get_forecast_weather(params: FunctionInput) -> FunctionOutput:
15 | forecast = some_helper(params)
16 | return FunctionOutput(forecast=forecast)
17 |
18 |
19 | function = Function(
20 | function=get_forecast_weather,
21 | description="Get the forecasted weather in a given location.",
22 | )
23 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/weather/get_forecast_weather/test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "What's the weather forecast in Toronto tomorrow?",
4 | "input": {
5 | "location": "Toronto",
6 | "forecast": "tomorrow"
7 | },
8 | "output": {
9 | "forecast": "The weather tomorrow in Toronto is going to be 22 degrees."
10 | }
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/examples/advanced/src/functions/weather/get_forecast_weather/types.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 |
4 | class FunctionInput(BaseModel):
5 | location: str = Field(..., description="The city, e.g. San Francisco")
6 | forecast: str = Field(..., description="The forecast, e.g. today, tomorrow.")
7 |
8 |
9 | class FunctionOutput(BaseModel):
10 | forecast: str
11 |
12 | def __eq__(self, other):
13 | if not isinstance(other, FunctionOutput):
14 | return False
15 | return self.forecast == other.forecast
16 |
--------------------------------------------------------------------------------
/examples/advanced/src/main.py:
--------------------------------------------------------------------------------
1 | from sageai import SageAI
2 |
3 | from customvectordb import CustomVectorDB
4 |
5 | # Init on startup
6 | sage = SageAI(
7 | openai_key="",
8 | vectordb=CustomVectorDB,
9 | )
10 |
11 | # In a CI/CD pipeline or in dev mode on startup/hot reload
12 | sage.index()
13 |
14 | # Anywhere in the codebase
15 | message = "What's the weather like in Toronto right now?"
16 | print(f"Message: {message}")
17 | response = sage.chat(
18 | messages=[dict(role="user", content=message)],
19 | model="gpt-3.5-turbo-0613",
20 | top_n=5,
21 | )
22 | print(f"Response: {response}")
23 |
24 | message = "Give me a number between 1 and 10."
25 | print(f"\nMessage: {message}")
26 | response = sage.chat(
27 | messages=[dict(role="user", content=message)],
28 | model="gpt-3.5-turbo-0613",
29 | top_n=5,
30 | )
31 | print(f"Response: {response}")
32 |
33 | message = "What's the weather tomorrow in Toronto?"
34 | print(f"\nMessage: {message}")
35 | response = sage.chat(
36 | messages=[dict(role="user", content=message)],
37 | model="gpt-3.5-turbo-0613",
38 | top_n=5,
39 | )
40 | print(f"Response: {response}")
41 |
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | ## Basic Example
2 |
3 | This example shows a simple version of `SageAI` with 1 function, `get_current_weather`, that is inspired by OpenAI's
4 | [example](https://platform.openai.com/docs/guides/gpt/function-calling) of function calling.
5 |
--------------------------------------------------------------------------------
/examples/basic/src/functions/get_current_weather/function.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Optional
3 |
4 | from pydantic import BaseModel, Field
5 |
6 | from sageai.types.function import Function
7 |
8 |
9 | class UnitTypes(str, Enum):
10 | CELSIUS = "Celsius"
11 | FAHRENHEIT = "Fahrenheit"
12 |
13 |
14 | class FunctionInput(BaseModel):
15 | location: str = Field(..., description="The city, e.g. San Francisco")
16 | unit: Optional[UnitTypes] = Field(
17 | UnitTypes.CELSIUS, description="The unit of temperature."
18 | )
19 |
20 |
21 | class FunctionOutput(BaseModel):
22 | weather: str
23 |
24 | def __eq__(self, other):
25 | if not isinstance(other, FunctionOutput):
26 | return False
27 | return self.weather == other.weather
28 |
29 |
30 | def get_current_weather(params: FunctionInput) -> FunctionOutput:
31 | weather = (
32 | f"The weather in {params.location} is currently 22 degrees {params.unit.value}."
33 | )
34 |
35 | return FunctionOutput(weather=weather)
36 |
37 |
38 | function = Function(
39 | function=get_current_weather,
40 | description="Get the current weather in a given location.",
41 | )
42 |
--------------------------------------------------------------------------------
/examples/basic/src/functions/get_current_weather/test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "What's the weather like in Toronto right now?",
4 | "input": {
5 | "location": "Toronto",
6 | "unit": "Celsius"
7 | },
8 | "output": {
9 | "weather": "The weather in Toronto is currently 22 degrees Celsius."
10 | }
11 | },
12 | {
13 | "message": "What's the temperature in New York right now in Fahrenheit?",
14 | "input": {
15 | "location": "New York, NY",
16 | "unit": "Fahrenheit"
17 | },
18 | "output": {
19 | "weather": "The weather in New York is currently 22 degrees Fahrenheit."
20 | }
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/examples/basic/src/main.py:
--------------------------------------------------------------------------------
1 | from sageai import SageAI
2 |
3 | # Init on startup
4 | sage = SageAI(openai_key="")
5 |
6 | # In a CI/CD pipeline or in dev mode on startup/hot reload
7 | sage.index()
8 |
9 | # Anywhere in the codebase
10 | message = "What's the weather like in Toronto right now?"
11 | response = sage.chat(
12 | messages=[dict(role="user", content=message)],
13 | model="gpt-3.5-turbo-0613",
14 | top_n=5,
15 | )
16 |
17 | print(f"Message: {message}")
18 | print(f"Response: {response}")
19 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "anyio"
5 | version = "3.7.1"
6 | description = "High level compatibility layer for multiple asynchronous event loop implementations"
7 | optional = false
8 | python-versions = ">=3.7"
9 | files = [
10 | {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
11 | {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
12 | ]
13 |
14 | [package.dependencies]
15 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
16 | idna = ">=2.8"
17 | sniffio = ">=1.1"
18 |
19 | [package.extras]
20 | doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
21 | test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
22 | trio = ["trio (<0.22)"]
23 |
24 | [[package]]
25 | name = "black"
26 | version = "23.11.0"
27 | description = "The uncompromising code formatter."
28 | optional = false
29 | python-versions = ">=3.8"
30 | files = [
31 | {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"},
32 | {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"},
33 | {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"},
34 | {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"},
35 | {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
36 | {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
37 | {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
38 | {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
39 | {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"},
40 | {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"},
41 | {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"},
42 | {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"},
43 | {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"},
44 | {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"},
45 | {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"},
46 | {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"},
47 | {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
48 | {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
49 | ]
50 |
51 | [package.dependencies]
52 | click = ">=8.0.0"
53 | mypy-extensions = ">=0.4.3"
54 | packaging = ">=22.0"
55 | pathspec = ">=0.9.0"
56 | platformdirs = ">=2"
57 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
58 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
59 |
60 | [package.extras]
61 | colorama = ["colorama (>=0.4.3)"]
62 | d = ["aiohttp (>=3.7.4)"]
63 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
64 | uvloop = ["uvloop (>=0.15.2)"]
65 |
66 | [[package]]
67 | name = "certifi"
68 | version = "2023.7.22"
69 | description = "Python package for providing Mozilla's CA Bundle."
70 | optional = false
71 | python-versions = ">=3.6"
72 | files = [
73 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
74 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
75 | ]
76 |
77 | [[package]]
78 | name = "click"
79 | version = "8.1.7"
80 | description = "Composable command line interface toolkit"
81 | optional = false
82 | python-versions = ">=3.7"
83 | files = [
84 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
85 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
86 | ]
87 |
88 | [package.dependencies]
89 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
90 |
91 | [[package]]
92 | name = "colorama"
93 | version = "0.4.6"
94 | description = "Cross-platform colored terminal text."
95 | optional = false
96 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
97 | files = [
98 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
99 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
100 | ]
101 |
102 | [[package]]
103 | name = "distro"
104 | version = "1.8.0"
105 | description = "Distro - an OS platform information API"
106 | optional = false
107 | python-versions = ">=3.6"
108 | files = [
109 | {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"},
110 | {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"},
111 | ]
112 |
113 | [[package]]
114 | name = "exceptiongroup"
115 | version = "1.1.3"
116 | description = "Backport of PEP 654 (exception groups)"
117 | optional = false
118 | python-versions = ">=3.7"
119 | files = [
120 | {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
121 | {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
122 | ]
123 |
124 | [package.extras]
125 | test = ["pytest (>=6)"]
126 |
127 | [[package]]
128 | name = "grpcio"
129 | version = "1.59.2"
130 | description = "HTTP/2-based RPC framework"
131 | optional = false
132 | python-versions = ">=3.7"
133 | files = [
134 | {file = "grpcio-1.59.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:d2fa68a96a30dd240be80bbad838a0ac81a61770611ff7952b889485970c4c71"},
135 | {file = "grpcio-1.59.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:cf0dead5a2c5a3347af2cfec7131d4f2a2e03c934af28989c9078f8241a491fa"},
136 | {file = "grpcio-1.59.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e420ced29b5904cdf9ee5545e23f9406189d8acb6750916c2db4793dada065c6"},
137 | {file = "grpcio-1.59.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b230028a008ae1d0f430acb227d323ff8a619017415cf334c38b457f814119f"},
138 | {file = "grpcio-1.59.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4a3833c0e067f3558538727235cd8a49709bff1003200bbdefa2f09334e4b1"},
139 | {file = "grpcio-1.59.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6b25ed37c27e652db01be341af93fbcea03d296c024d8a0e680017a268eb85dd"},
140 | {file = "grpcio-1.59.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73abb8584b0cf74d37f5ef61c10722adc7275502ab71789a8fe3cb7ef04cf6e2"},
141 | {file = "grpcio-1.59.2-cp310-cp310-win32.whl", hash = "sha256:d6f70406695e3220f09cd7a2f879333279d91aa4a8a1d34303b56d61a8180137"},
142 | {file = "grpcio-1.59.2-cp310-cp310-win_amd64.whl", hash = "sha256:3c61d641d4f409c5ae46bfdd89ea42ce5ea233dcf69e74ce9ba32b503c727e29"},
143 | {file = "grpcio-1.59.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:3059668df17627f0e0fa680e9ef8c995c946c792612e9518f5cc1503be14e90b"},
144 | {file = "grpcio-1.59.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:72ca2399097c0b758198f2ff30f7178d680de8a5cfcf3d9b73a63cf87455532e"},
145 | {file = "grpcio-1.59.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c978f864b35f2261e0819f5cd88b9830b04dc51bcf055aac3c601e525a10d2ba"},
146 | {file = "grpcio-1.59.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9411e24328a2302e279e70cae6e479f1fddde79629fcb14e03e6d94b3956eabf"},
147 | {file = "grpcio-1.59.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb7e0fe6ad73b7f06d7e2b689c19a71cf5cc48f0c2bf8608469e51ffe0bd2867"},
148 | {file = "grpcio-1.59.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c2504eed520958a5b77cc99458297cb7906308cb92327f35fb7fbbad4e9b2188"},
149 | {file = "grpcio-1.59.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2171c39f355ba5b551c5d5928d65aa6c69807fae195b86ef4a7d125bcdb860a9"},
150 | {file = "grpcio-1.59.2-cp311-cp311-win32.whl", hash = "sha256:d2794f0e68b3085d99b4f6ff9c089f6fdd02b32b9d3efdfbb55beac1bf22d516"},
151 | {file = "grpcio-1.59.2-cp311-cp311-win_amd64.whl", hash = "sha256:2067274c88bc6de89c278a672a652b4247d088811ece781a4858b09bdf8448e3"},
152 | {file = "grpcio-1.59.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:535561990e075fa6bd4b16c4c3c1096b9581b7bb35d96fac4650f1181e428268"},
153 | {file = "grpcio-1.59.2-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:a213acfbf186b9f35803b52e4ca9addb153fc0b67f82a48f961be7000ecf6721"},
154 | {file = "grpcio-1.59.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:6959fb07e8351e20501ffb8cc4074c39a0b7ef123e1c850a7f8f3afdc3a3da01"},
155 | {file = "grpcio-1.59.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e82c5cf1495244adf5252f925ac5932e5fd288b3e5ab6b70bec5593074b7236c"},
156 | {file = "grpcio-1.59.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023088764012411affe7db183d1ada3ad9daf2e23ddc719ff46d7061de661340"},
157 | {file = "grpcio-1.59.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:da2d94c15f88cd40d7e67f7919d4f60110d2b9d5b1e08cf354c2be773ab13479"},
158 | {file = "grpcio-1.59.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6009386a2df66159f64ac9f20425ae25229b29b9dd0e1d3dd60043f037e2ad7e"},
159 | {file = "grpcio-1.59.2-cp312-cp312-win32.whl", hash = "sha256:75c6ecb70e809cf1504465174343113f51f24bc61e22a80ae1c859f3f7034c6d"},
160 | {file = "grpcio-1.59.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbe946b3e6e60a7b4618f091e62a029cb082b109a9d6b53962dd305087c6e4fd"},
161 | {file = "grpcio-1.59.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:f8753a6c88d1d0ba64302309eecf20f70d2770f65ca02d83c2452279085bfcd3"},
162 | {file = "grpcio-1.59.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:f1ef0d39bc1feb420caf549b3c657c871cad4ebbcf0580c4d03816b0590de0cf"},
163 | {file = "grpcio-1.59.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:4c93f4abbb54321ee6471e04a00139c80c754eda51064187963ddf98f5cf36a4"},
164 | {file = "grpcio-1.59.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08d77e682f2bf730a4961eea330e56d2f423c6a9b91ca222e5b1eb24a357b19f"},
165 | {file = "grpcio-1.59.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff16d68bf453275466a9a46739061a63584d92f18a0f5b33d19fc97eb69867c"},
166 | {file = "grpcio-1.59.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4abb717e320e74959517dc8e84a9f48fbe90e9abe19c248541e9418b1ce60acd"},
167 | {file = "grpcio-1.59.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:36f53c2b3449c015880e7d55a89c992c357f176327b0d2873cdaaf9628a37c69"},
168 | {file = "grpcio-1.59.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cc3e4cd087f07758b16bef8f31d88dbb1b5da5671d2f03685ab52dece3d7a16e"},
169 | {file = "grpcio-1.59.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:27f879ae604a7fcf371e59fba6f3ff4635a4c2a64768bd83ff0cac503142fef4"},
170 | {file = "grpcio-1.59.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:7cf05053242f61ba94014dd3a986e11a083400a32664058f80bf4cf817c0b3a1"},
171 | {file = "grpcio-1.59.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e1727c1c0e394096bb9af185c6923e8ea55a5095b8af44f06903bcc0e06800a2"},
172 | {file = "grpcio-1.59.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d573e70a6fe77555fb6143c12d3a7d3fa306632a3034b4e7c59ca09721546f8"},
173 | {file = "grpcio-1.59.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31176aa88f36020055ace9adff2405a33c8bdbfa72a9c4980e25d91b2f196873"},
174 | {file = "grpcio-1.59.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11168ef43e4a43ff1b1a65859f3e0ef1a173e277349e7fb16923ff108160a8cd"},
175 | {file = "grpcio-1.59.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:53c9aa5ddd6857c0a1cd0287225a2a25873a8e09727c2e95c4aebb1be83a766a"},
176 | {file = "grpcio-1.59.2-cp38-cp38-win32.whl", hash = "sha256:3b4368b33908f683a363f376dfb747d40af3463a6e5044afee07cf9436addf96"},
177 | {file = "grpcio-1.59.2-cp38-cp38-win_amd64.whl", hash = "sha256:0a754aff9e3af63bdc4c75c234b86b9d14e14a28a30c4e324aed1a9b873d755f"},
178 | {file = "grpcio-1.59.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:1f9524d1d701e399462d2c90ba7c193e49d1711cf429c0d3d97c966856e03d00"},
179 | {file = "grpcio-1.59.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:f93dbf58f03146164048be5426ffde298b237a5e059144847e4940f5b80172c3"},
180 | {file = "grpcio-1.59.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:6da6dea3a1bacf99b3c2187e296db9a83029ed9c38fd4c52b7c9b7326d13c828"},
181 | {file = "grpcio-1.59.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5f09cffa619adfb44799fa4a81c2a1ad77c887187613fb0a8f201ab38d89ba1"},
182 | {file = "grpcio-1.59.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c35aa9657f5d5116d23b934568e0956bd50c615127810fffe3ac356a914c176a"},
183 | {file = "grpcio-1.59.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:74100fecaec8a535e380cf5f2fb556ff84957d481c13e54051c52e5baac70541"},
184 | {file = "grpcio-1.59.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:128e20f57c5f27cb0157e73756d1586b83c1b513ebecc83ea0ac37e4b0e4e758"},
185 | {file = "grpcio-1.59.2-cp39-cp39-win32.whl", hash = "sha256:686e975a5d16602dc0982c7c703948d17184bd1397e16c8ee03511ecb8c4cdda"},
186 | {file = "grpcio-1.59.2-cp39-cp39-win_amd64.whl", hash = "sha256:242adc47725b9a499ee77c6a2e36688fa6c96484611f33b1be4c57ab075a92dd"},
187 | {file = "grpcio-1.59.2.tar.gz", hash = "sha256:d8f9cd4ad1be90b0cf350a2f04a38a36e44a026cac1e036ac593dc48efe91d52"},
188 | ]
189 |
190 | [package.extras]
191 | protobuf = ["grpcio-tools (>=1.59.2)"]
192 |
193 | [[package]]
194 | name = "grpcio-tools"
195 | version = "1.59.2"
196 | description = "Protobuf code generator for gRPC"
197 | optional = false
198 | python-versions = ">=3.7"
199 | files = [
200 | {file = "grpcio-tools-1.59.2.tar.gz", hash = "sha256:75905266cf90f1866b322575c2edcd4b36532c33fc512bb1b380dc58d84b1030"},
201 | {file = "grpcio_tools-1.59.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:9b2885c0e2c9a97bde33497a919032afbd8b5c6dc2f8d4dd4198e77226e0de05"},
202 | {file = "grpcio_tools-1.59.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2f410375830a9bb7140a07da4d75bf380e0958377bed50d77d1dae302de4314e"},
203 | {file = "grpcio_tools-1.59.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e21fc172522d2dda815223a359b2aca9bc317a1b5e5dea5a58cd5079333af133"},
204 | {file = "grpcio_tools-1.59.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:072a7ce979ea4f7579c3c99fcbde3d1882c3d1942a3b51d159f67af83b714cd8"},
205 | {file = "grpcio_tools-1.59.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b38f8edb2909702c2478b52f6213982c21e4f66f739ac953b91f97863ba2c06a"},
206 | {file = "grpcio_tools-1.59.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:12fdee2de80d83eadb1294e0f8a0cb6cefcd2e4988ed680038ab09cd04361ee4"},
207 | {file = "grpcio_tools-1.59.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a3cb707da722a0b6c4021fc2cc1c005a8d4037d8ad0252f93df318b9b8a6b4f3"},
208 | {file = "grpcio_tools-1.59.2-cp310-cp310-win32.whl", hash = "sha256:ec2fbb02ebb9f2ae1b1c69cccf913dee8c41f5acad94014d3ce11b53720376e3"},
209 | {file = "grpcio_tools-1.59.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0dc271a200dbab6547b2c73fcbdb7efe94c31cb633aa20d073f7cf4493493e1"},
210 | {file = "grpcio_tools-1.59.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:d634b65cc8ee769edccf1647d8a16861a27e0d8cbd787c711168d2c5e9bddbd1"},
211 | {file = "grpcio_tools-1.59.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:b0b712acec00a9cbc2204c271d638062a2cb8ce74f25d158b023ff6e93182659"},
212 | {file = "grpcio_tools-1.59.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:dd5c78f8e7c6e721b9009c92481a0e3b30a9926ef721120723a03b8a34a34fb9"},
213 | {file = "grpcio_tools-1.59.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:724f4f0eecc17fa66216eebfff145631070f04ed7fb4ddf7a7d1c4f954ecc2a1"},
214 | {file = "grpcio_tools-1.59.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77ec33ddee691e60511e2a7c793aad4cf172ae20e08d95c786cbba395f6203a7"},
215 | {file = "grpcio_tools-1.59.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fa1b9dee7811fad081816e884d063c4dd4946dba61aa54243b4c76c311090c48"},
216 | {file = "grpcio_tools-1.59.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba8dba19e7b2b6f7369004533866f222ba483b9e14d2d152ecf9339c0df1283a"},
217 | {file = "grpcio_tools-1.59.2-cp311-cp311-win32.whl", hash = "sha256:df35d145bc2f6e5f57b74cb69f66526675a5f2dcf7d54617ce0deff0c82cca0a"},
218 | {file = "grpcio_tools-1.59.2-cp311-cp311-win_amd64.whl", hash = "sha256:99ddc0f5304071a355c261ae49ea5d29b9e9b6dcf422dfc55ada70a243e27e8f"},
219 | {file = "grpcio_tools-1.59.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:670f5889853215999eb3511a623dd7dff01b1ce1a64610d13366e0fd337f8c79"},
220 | {file = "grpcio_tools-1.59.2-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:1e949e66d4555ce319fd7acef90df625138078d8729c4dc6f6a9f05925034433"},
221 | {file = "grpcio_tools-1.59.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:09d809ca88999b2578119683f9f0f6a9b42de95ea21550852114a1540b6a642c"},
222 | {file = "grpcio_tools-1.59.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0925545180223fabd6da9b34513efac83aa16673ef8b1cb0cc678e8cf0923c"},
223 | {file = "grpcio_tools-1.59.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2ccb59dfbf2ebd668a5a7c4b7bb2b859859641d2b199114b557cd045aac6102"},
224 | {file = "grpcio_tools-1.59.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:12cc7698fad48866f68fdef831685cb31ef5814ac605d248c4e5fc964a6fb3f6"},
225 | {file = "grpcio_tools-1.59.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:55c401599d5093c4cfa83b8f0ee9757b4d6d3029b10bd67be2cffeada7a44961"},
226 | {file = "grpcio_tools-1.59.2-cp312-cp312-win32.whl", hash = "sha256:896f5cdf58f658025a4f7e4ea96c81183b4b6a4b1b4d92ae66d112ac91f062f1"},
227 | {file = "grpcio_tools-1.59.2-cp312-cp312-win_amd64.whl", hash = "sha256:b53db1523015a3acda75722357df6c94afae37f6023800c608e09a5c05393804"},
228 | {file = "grpcio_tools-1.59.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:d08b398509ea4d544bcecddd9a21f59dc556396916c3915904cac206af2db72b"},
229 | {file = "grpcio_tools-1.59.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:09749e832e06493841000275248b031f7154665900d1e1b0e42fc17a64bf904d"},
230 | {file = "grpcio_tools-1.59.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:e972746000aa192521715f776fab617a3437bed29e90fe0e0fd0d0d6f498d7d4"},
231 | {file = "grpcio_tools-1.59.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cbeeb3d8ec4cb25c92e17bfbdcef3c3669e85c5ee787a6e581cb942bc0ae2b88"},
232 | {file = "grpcio_tools-1.59.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed8e6632d8d839456332d97b96db10bd2dbf3078e728d063394ac2d54597ad80"},
233 | {file = "grpcio_tools-1.59.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:531f87c8e884c6a2e58f040039dfbfe997a4e33baa58f7c7d9993db37b1f5ad0"},
234 | {file = "grpcio_tools-1.59.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:feca316e17cfead823af6eae0fc20c0d5299a94d71cfb7531a0e92d050a5fb2f"},
235 | {file = "grpcio_tools-1.59.2-cp37-cp37m-win_amd64.whl", hash = "sha256:41b5dd6a06c2563ac3b3adda6d875b15e63eb7b1629e85fc9af608c3a76c4c82"},
236 | {file = "grpcio_tools-1.59.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:7ec536cdae870a74080c665cfb1dca8d0784a931aa3c26376ef971a3a51b59d4"},
237 | {file = "grpcio_tools-1.59.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:9c106ebbed0db446f59f0efe5c3fce33a0a21bf75b392966585e4b5934891b92"},
238 | {file = "grpcio_tools-1.59.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:32141ef309543a446337e934f0b7a2565a6fca890ff4e543630a09ef72c8d00b"},
239 | {file = "grpcio_tools-1.59.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f2ce5ecd63c492949b03af73b1dd6d502c567cc2f9c2057137e518b0c702a01"},
240 | {file = "grpcio_tools-1.59.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9ce2a209871ed1c5ae2229e6f4f5a3ea96d83b7871df5d9773d72a72545683"},
241 | {file = "grpcio_tools-1.59.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7f0e26af7c07bfa906c91ca9f5932514928a7f032f5f20aecad6b5541037de7e"},
242 | {file = "grpcio_tools-1.59.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:48782727c5cff8b8c96e028a8a58614ff6a37eadc0db85866516210c7aafe9ae"},
243 | {file = "grpcio_tools-1.59.2-cp38-cp38-win32.whl", hash = "sha256:4a1810bc5de51cc162a19ed3c11da8ddc64d8cfcba049ef337c20fcb397f048b"},
244 | {file = "grpcio_tools-1.59.2-cp38-cp38-win_amd64.whl", hash = "sha256:3cf9949a2aadcece3c1e0dd59249aea53dbfc8cc94f7d707797acd67cf6cf931"},
245 | {file = "grpcio_tools-1.59.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:f52e0ce8f2dcf1f160c847304016c446075a83ab925d98933d4681bfa8af2962"},
246 | {file = "grpcio_tools-1.59.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:eb597d6bf9f5bfa54d00546e828f0d4e2c69250d1bc17c27903c0c7b66372135"},
247 | {file = "grpcio_tools-1.59.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:17ef468836d7cf0b2419f4d5c7ac84ec2d598a1ae410773585313edacf7c393e"},
248 | {file = "grpcio_tools-1.59.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dee5f7e7a56177234e61a483c70ca2ae34e73128372c801bb7039993870889f1"},
249 | {file = "grpcio_tools-1.59.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f50ff312b88918c5a6461e45c5e03869749a066b1c24a7327e8e13e117efe4fc"},
250 | {file = "grpcio_tools-1.59.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a85da4200295ee17e3c1ae068189a43844420ed7e9d531a042440f52de486dfb"},
251 | {file = "grpcio_tools-1.59.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f518f22a3082de00f0d7a216e96366a87e6973111085ba1603c3bfa7dba2e728"},
252 | {file = "grpcio_tools-1.59.2-cp39-cp39-win32.whl", hash = "sha256:6e735a26e8ea8bb89dc69343d1d00ea607449c6d81e21f339ee118562f3d1931"},
253 | {file = "grpcio_tools-1.59.2-cp39-cp39-win_amd64.whl", hash = "sha256:3491cb69c909d586c23d7e6d0ac87844ca22f496f505ce429c0d3301234f2cf3"},
254 | ]
255 |
256 | [package.dependencies]
257 | grpcio = ">=1.59.2"
258 | protobuf = ">=4.21.6,<5.0dev"
259 | setuptools = "*"
260 |
261 | [[package]]
262 | name = "h11"
263 | version = "0.14.0"
264 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
265 | optional = false
266 | python-versions = ">=3.7"
267 | files = [
268 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
269 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
270 | ]
271 |
272 | [[package]]
273 | name = "h2"
274 | version = "4.1.0"
275 | description = "HTTP/2 State-Machine based protocol implementation"
276 | optional = false
277 | python-versions = ">=3.6.1"
278 | files = [
279 | {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"},
280 | {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"},
281 | ]
282 |
283 | [package.dependencies]
284 | hpack = ">=4.0,<5"
285 | hyperframe = ">=6.0,<7"
286 |
287 | [[package]]
288 | name = "hpack"
289 | version = "4.0.0"
290 | description = "Pure-Python HPACK header compression"
291 | optional = false
292 | python-versions = ">=3.6.1"
293 | files = [
294 | {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
295 | {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
296 | ]
297 |
298 | [[package]]
299 | name = "httpcore"
300 | version = "1.0.2"
301 | description = "A minimal low-level HTTP client."
302 | optional = false
303 | python-versions = ">=3.8"
304 | files = [
305 | {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
306 | {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
307 | ]
308 |
309 | [package.dependencies]
310 | certifi = "*"
311 | h11 = ">=0.13,<0.15"
312 |
313 | [package.extras]
314 | asyncio = ["anyio (>=4.0,<5.0)"]
315 | http2 = ["h2 (>=3,<5)"]
316 | socks = ["socksio (==1.*)"]
317 | trio = ["trio (>=0.22.0,<0.23.0)"]
318 |
319 | [[package]]
320 | name = "httpx"
321 | version = "0.25.1"
322 | description = "The next generation HTTP client."
323 | optional = false
324 | python-versions = ">=3.8"
325 | files = [
326 | {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"},
327 | {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"},
328 | ]
329 |
330 | [package.dependencies]
331 | anyio = "*"
332 | certifi = "*"
333 | h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""}
334 | httpcore = "*"
335 | idna = "*"
336 | sniffio = "*"
337 |
338 | [package.extras]
339 | brotli = ["brotli", "brotlicffi"]
340 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
341 | http2 = ["h2 (>=3,<5)"]
342 | socks = ["socksio (==1.*)"]
343 |
344 | [[package]]
345 | name = "hyperframe"
346 | version = "6.0.1"
347 | description = "HTTP/2 framing layer for Python"
348 | optional = false
349 | python-versions = ">=3.6.1"
350 | files = [
351 | {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"},
352 | {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
353 | ]
354 |
355 | [[package]]
356 | name = "idna"
357 | version = "3.4"
358 | description = "Internationalized Domain Names in Applications (IDNA)"
359 | optional = false
360 | python-versions = ">=3.5"
361 | files = [
362 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
363 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
364 | ]
365 |
366 | [[package]]
367 | name = "iniconfig"
368 | version = "2.0.0"
369 | description = "brain-dead simple config-ini parsing"
370 | optional = false
371 | python-versions = ">=3.7"
372 | files = [
373 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
374 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
375 | ]
376 |
377 | [[package]]
378 | name = "isort"
379 | version = "5.12.0"
380 | description = "A Python utility / library to sort Python imports."
381 | optional = false
382 | python-versions = ">=3.8.0"
383 | files = [
384 | {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
385 | {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
386 | ]
387 |
388 | [package.extras]
389 | colors = ["colorama (>=0.4.3)"]
390 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
391 | plugins = ["setuptools"]
392 | requirements-deprecated-finder = ["pip-api", "pipreqs"]
393 |
394 | [[package]]
395 | name = "mypy-extensions"
396 | version = "1.0.0"
397 | description = "Type system extensions for programs checked with the mypy type checker."
398 | optional = false
399 | python-versions = ">=3.5"
400 | files = [
401 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
402 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
403 | ]
404 |
405 | [[package]]
406 | name = "numpy"
407 | version = "1.26.2"
408 | description = "Fundamental package for array computing in Python"
409 | optional = false
410 | python-versions = ">=3.9"
411 | files = [
412 | {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"},
413 | {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"},
414 | {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"},
415 | {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"},
416 | {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"},
417 | {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"},
418 | {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"},
419 | {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"},
420 | {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"},
421 | {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"},
422 | {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"},
423 | {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"},
424 | {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"},
425 | {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"},
426 | {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"},
427 | {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"},
428 | {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"},
429 | {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"},
430 | {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"},
431 | {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"},
432 | {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"},
433 | {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"},
434 | {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"},
435 | {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"},
436 | {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"},
437 | {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"},
438 | {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"},
439 | {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"},
440 | {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"},
441 | {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"},
442 | {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"},
443 | {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"},
444 | {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"},
445 | {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"},
446 | {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"},
447 | {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"},
448 | ]
449 |
450 | [[package]]
451 | name = "openai"
452 | version = "1.2.4"
453 | description = "The official Python library for the openai API"
454 | optional = false
455 | python-versions = ">=3.7.1"
456 | files = [
457 | {file = "openai-1.2.4-py3-none-any.whl", hash = "sha256:53927a2ca276eec0a0efdc1ae829f74a51f49b7d3e14cc6f820aeafb0abfd802"},
458 | {file = "openai-1.2.4.tar.gz", hash = "sha256:d99a474049376be431d9b4dec3a5c895dd76e19165748c5944e80b7905d1b1ff"},
459 | ]
460 |
461 | [package.dependencies]
462 | anyio = ">=3.5.0,<4"
463 | distro = ">=1.7.0,<2"
464 | httpx = ">=0.23.0,<1"
465 | pydantic = ">=1.9.0,<3"
466 | tqdm = ">4"
467 | typing-extensions = ">=4.5,<5"
468 |
469 | [package.extras]
470 | datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
471 |
472 | [[package]]
473 | name = "packaging"
474 | version = "23.2"
475 | description = "Core utilities for Python packages"
476 | optional = false
477 | python-versions = ">=3.7"
478 | files = [
479 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
480 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
481 | ]
482 |
483 | [[package]]
484 | name = "pathspec"
485 | version = "0.11.2"
486 | description = "Utility library for gitignore style pattern matching of file paths."
487 | optional = false
488 | python-versions = ">=3.7"
489 | files = [
490 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
491 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
492 | ]
493 |
494 | [[package]]
495 | name = "platformdirs"
496 | version = "4.0.0"
497 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
498 | optional = false
499 | python-versions = ">=3.7"
500 | files = [
501 | {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"},
502 | {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"},
503 | ]
504 |
505 | [package.extras]
506 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
507 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
508 |
509 | [[package]]
510 | name = "pluggy"
511 | version = "1.3.0"
512 | description = "plugin and hook calling mechanisms for python"
513 | optional = false
514 | python-versions = ">=3.8"
515 | files = [
516 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
517 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
518 | ]
519 |
520 | [package.extras]
521 | dev = ["pre-commit", "tox"]
522 | testing = ["pytest", "pytest-benchmark"]
523 |
524 | [[package]]
525 | name = "portalocker"
526 | version = "2.8.2"
527 | description = "Wraps the portalocker recipe for easy usage"
528 | optional = false
529 | python-versions = ">=3.8"
530 | files = [
531 | {file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"},
532 | {file = "portalocker-2.8.2.tar.gz", hash = "sha256:2b035aa7828e46c58e9b31390ee1f169b98e1066ab10b9a6a861fe7e25ee4f33"},
533 | ]
534 |
535 | [package.dependencies]
536 | pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""}
537 |
538 | [package.extras]
539 | docs = ["sphinx (>=1.7.1)"]
540 | redis = ["redis"]
541 | tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"]
542 |
543 | [[package]]
544 | name = "protobuf"
545 | version = "4.25.0"
546 | description = ""
547 | optional = false
548 | python-versions = ">=3.8"
549 | files = [
550 | {file = "protobuf-4.25.0-cp310-abi3-win32.whl", hash = "sha256:5c1203ac9f50e4853b0a0bfffd32c67118ef552a33942982eeab543f5c634395"},
551 | {file = "protobuf-4.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:c40ff8f00aa737938c5378d461637d15c442a12275a81019cc2fef06d81c9419"},
552 | {file = "protobuf-4.25.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:cf21faba64cd2c9a3ed92b7a67f226296b10159dbb8fbc5e854fc90657d908e4"},
553 | {file = "protobuf-4.25.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:32ac2100b0e23412413d948c03060184d34a7c50b3e5d7524ee96ac2b10acf51"},
554 | {file = "protobuf-4.25.0-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:683dc44c61f2620b32ce4927de2108f3ebe8ccf2fd716e1e684e5a50da154054"},
555 | {file = "protobuf-4.25.0-cp38-cp38-win32.whl", hash = "sha256:1a3ba712877e6d37013cdc3476040ea1e313a6c2e1580836a94f76b3c176d575"},
556 | {file = "protobuf-4.25.0-cp38-cp38-win_amd64.whl", hash = "sha256:b2cf8b5d381f9378afe84618288b239e75665fe58d0f3fd5db400959274296e9"},
557 | {file = "protobuf-4.25.0-cp39-cp39-win32.whl", hash = "sha256:63714e79b761a37048c9701a37438aa29945cd2417a97076048232c1df07b701"},
558 | {file = "protobuf-4.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:d94a33db8b7ddbd0af7c467475fb9fde0c705fb315a8433c0e2020942b863a1f"},
559 | {file = "protobuf-4.25.0-py3-none-any.whl", hash = "sha256:1a53d6f64b00eecf53b65ff4a8c23dc95df1fa1e97bb06b8122e5a64f49fc90a"},
560 | {file = "protobuf-4.25.0.tar.gz", hash = "sha256:68f7caf0d4f012fd194a301420cf6aa258366144d814f358c5b32558228afa7c"},
561 | ]
562 |
563 | [[package]]
564 | name = "pydantic"
565 | version = "1.10.12"
566 | description = "Data validation and settings management using python type hints"
567 | optional = false
568 | python-versions = ">=3.7"
569 | files = [
570 | {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"},
571 | {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"},
572 | {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"},
573 | {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"},
574 | {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"},
575 | {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"},
576 | {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"},
577 | {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"},
578 | {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"},
579 | {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"},
580 | {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"},
581 | {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"},
582 | {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"},
583 | {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"},
584 | {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"},
585 | {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"},
586 | {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"},
587 | {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"},
588 | {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"},
589 | {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"},
590 | {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"},
591 | {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"},
592 | {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"},
593 | {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"},
594 | {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"},
595 | {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"},
596 | {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"},
597 | {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"},
598 | {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"},
599 | {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"},
600 | {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"},
601 | {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"},
602 | {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"},
603 | {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"},
604 | {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"},
605 | {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"},
606 | ]
607 |
608 | [package.dependencies]
609 | typing-extensions = ">=4.2.0"
610 |
611 | [package.extras]
612 | dotenv = ["python-dotenv (>=0.10.4)"]
613 | email = ["email-validator (>=1.0.3)"]
614 |
615 | [[package]]
616 | name = "pytest"
617 | version = "7.4.3"
618 | description = "pytest: simple powerful testing with Python"
619 | optional = false
620 | python-versions = ">=3.7"
621 | files = [
622 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
623 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
624 | ]
625 |
626 | [package.dependencies]
627 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
628 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
629 | iniconfig = "*"
630 | packaging = "*"
631 | pluggy = ">=0.12,<2.0"
632 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
633 |
634 | [package.extras]
635 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
636 |
637 | [[package]]
638 | name = "pywin32"
639 | version = "306"
640 | description = "Python for Window Extensions"
641 | optional = false
642 | python-versions = "*"
643 | files = [
644 | {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
645 | {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"},
646 | {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"},
647 | {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"},
648 | {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"},
649 | {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"},
650 | {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"},
651 | {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"},
652 | {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"},
653 | {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"},
654 | {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"},
655 | {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"},
656 | {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"},
657 | {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
658 | ]
659 |
660 | [[package]]
661 | name = "qdrant-client"
662 | version = "1.6.6"
663 | description = "Client library for the Qdrant vector search engine"
664 | optional = false
665 | python-versions = ">=3.8,<3.13"
666 | files = [
667 | {file = "qdrant_client-1.6.6-py3-none-any.whl", hash = "sha256:e38c92899667e721b35299b70305c7caa4cdeec0912c9da84da839a2c8fac0d3"},
668 | {file = "qdrant_client-1.6.6.tar.gz", hash = "sha256:20f4344df29b8f696fc96e3bf92725abee86635f27ff1eebfe0adec7780e78b8"},
669 | ]
670 |
671 | [package.dependencies]
672 | grpcio = ">=1.41.0"
673 | grpcio-tools = ">=1.41.0"
674 | httpx = {version = ">=0.14.0", extras = ["http2"]}
675 | numpy = {version = ">=1.21", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}
676 | portalocker = ">=2.7.0,<3.0.0"
677 | pydantic = ">=1.10.8"
678 | urllib3 = ">=1.26.14,<2.0.0"
679 |
680 | [package.extras]
681 | fastembed = ["fastembed (==0.1.1)"]
682 |
683 | [[package]]
684 | name = "setuptools"
685 | version = "68.2.2"
686 | description = "Easily download, build, install, upgrade, and uninstall Python packages"
687 | optional = false
688 | python-versions = ">=3.8"
689 | files = [
690 | {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
691 | {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
692 | ]
693 |
694 | [package.extras]
695 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
696 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
697 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
698 |
699 | [[package]]
700 | name = "sniffio"
701 | version = "1.3.0"
702 | description = "Sniff out which async library your code is running under"
703 | optional = false
704 | python-versions = ">=3.7"
705 | files = [
706 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
707 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
708 | ]
709 |
710 | [[package]]
711 | name = "tomli"
712 | version = "2.0.1"
713 | description = "A lil' TOML parser"
714 | optional = false
715 | python-versions = ">=3.7"
716 | files = [
717 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
718 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
719 | ]
720 |
721 | [[package]]
722 | name = "tqdm"
723 | version = "4.66.1"
724 | description = "Fast, Extensible Progress Meter"
725 | optional = false
726 | python-versions = ">=3.7"
727 | files = [
728 | {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
729 | {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
730 | ]
731 |
732 | [package.dependencies]
733 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
734 |
735 | [package.extras]
736 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
737 | notebook = ["ipywidgets (>=6)"]
738 | slack = ["slack-sdk"]
739 | telegram = ["requests"]
740 |
741 | [[package]]
742 | name = "typing-extensions"
743 | version = "4.8.0"
744 | description = "Backported and Experimental Type Hints for Python 3.8+"
745 | optional = false
746 | python-versions = ">=3.8"
747 | files = [
748 | {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
749 | {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
750 | ]
751 |
752 | [[package]]
753 | name = "urllib3"
754 | version = "1.26.18"
755 | description = "HTTP library with thread-safe connection pooling, file post, and more."
756 | optional = false
757 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
758 | files = [
759 | {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"},
760 | {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"},
761 | ]
762 |
763 | [package.extras]
764 | brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
765 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
766 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
767 |
768 | [metadata]
769 | lock-version = "2.0"
770 | python-versions = ">=3.9,<3.12"
771 | content-hash = "48fc2eaa7050ae5cb9a767fe3c630d5ad1173d5003a039b26a3d337d357f590c"
772 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "sageai"
3 | version = "1.0.5"
4 | description = "Folder-based functions for GPT 3.5/4 function calling with Pydantic support"
5 | authors = ["JUNIORCO ", "SamaniMK "]
6 | license = "MIT"
7 | readme = "README.md"
8 | keywords = ["python", "openai", "functions", "chatgpt", "gpt4", "genai", "function-calling"]
9 |
10 | [tool.poetry.dependencies]
11 | python = ">=3.9,<3.12"
12 | pydantic = ">=1.6,<=1.10.12"
13 | openai = ">=1.2.0"
14 | qdrant-client = ">=1.4.0"
15 |
16 | [tool.poetry.group.dev.dependencies]
17 | black = "^23.9.1"
18 | isort = "^5.12.0"
19 | pytest = "^7.4.2"
20 |
21 | [tool.poetry.scripts]
22 | sageai-tests = "sageai.tests.main:main"
23 |
24 | [tool.isort]
25 | profile = "black"
26 |
27 | [build-system]
28 | requires = ["poetry-core"]
29 | build-backend = "poetry.core.masonry.api"
30 |
--------------------------------------------------------------------------------
/sageai/__init__.py:
--------------------------------------------------------------------------------
1 | from sageai.sageai import SageAI
2 |
--------------------------------------------------------------------------------
/sageai/config.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Type
2 |
3 | from pydantic import BaseModel, Field, ValidationError
4 |
5 | from sageai.services.defaultvectordb_service import DefaultVectorDBService
6 | from sageai.types.abstract_vectordb import AbstractVectorDB
7 | from sageai.types.log_level import LogLevel
8 | from sageai.utils.file_utilities import generate_functions_map
9 | from sageai.utils.format_config_args import format_config_args
10 |
11 |
12 | class Config(BaseModel):
13 | openai_key: str
14 | functions_directory: Optional[str] = Field(
15 | "functions", description="The directory of functions."
16 | )
17 | vectordb: Optional[Type[AbstractVectorDB]] = Field(
18 | DefaultVectorDBService, description="VectorDB class reference."
19 | )
20 | log_level: Optional[LogLevel] = Field(
21 | LogLevel.ERROR, description="The desired log level for output."
22 | )
23 |
24 | class Config:
25 | arbitrary_types_allowed = True
26 |
27 |
28 | _config = Config(openai_key="")
29 | function_map = {}
30 |
31 |
32 | def set_config(**kwargs):
33 | """Set the configuration parameters."""
34 | global _config
35 | global function_map
36 | try:
37 | kwargs = format_config_args(**kwargs)
38 | _config = Config(**kwargs)
39 | function_map = generate_functions_map()
40 | return _config
41 | except ValidationError as e:
42 | raise ValidationError(f"Invalid configuration: {e}")
43 |
44 |
45 | def get_config():
46 | """Retrieve the configuration."""
47 | return _config
48 |
49 |
50 | def get_function_map():
51 | return function_map
52 |
--------------------------------------------------------------------------------
/sageai/sageai.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Any, Dict, Optional, Tuple, Type
3 |
4 | from sageai.config import LogLevel, get_config, get_function_map, set_config
5 | from sageai.services.openai_service import OpenAIService
6 | from sageai.types.abstract_vectordb import AbstractVectorDB
7 | from sageai.utils.inspection_utilities import get_input_parameter_type
8 | from sageai.utils.openai_utilities import get_latest_user_message
9 |
10 | __all__ = ["SageAI"]
11 |
12 |
13 | class SageAI:
14 | def __init__(
15 | self,
16 | *,
17 | openai_key: str,
18 | functions_directory: Optional[str] = None,
19 | vectordb: Optional[Type[AbstractVectorDB]] = None,
20 | log_level: Optional[LogLevel] = None,
21 | ):
22 | if openai_key is None:
23 | raise Exception("No OpenAI key provided.")
24 |
25 | config_args = {"openai_key": openai_key}
26 |
27 | if functions_directory is not None:
28 | config_args["functions_directory"] = functions_directory
29 | if vectordb is not None:
30 | config_args["vectordb"] = vectordb
31 | if log_level is not None:
32 | config_args["log_level"] = LogLevel(log_level)
33 |
34 | set_config(**config_args)
35 | self.config = get_config()
36 |
37 | self.openai = OpenAIService()
38 | self.vectordb = self.config.vectordb()
39 |
40 | def index(self):
41 | self.vectordb.index()
42 |
43 | def chat(self, *args, **kwargs) -> Dict[str, Any]:
44 | merged = {i: v for i, v in enumerate(args)}
45 | merged.update(kwargs)
46 |
47 | top_n = merged.pop("top_n") if "top_n" in merged else None
48 |
49 | if merged.get("model") is None:
50 | raise Exception("No model provided.")
51 |
52 | if merged.get("messages") is None:
53 | raise Exception("No messages provided.")
54 |
55 | if top_n is None:
56 | raise Exception("No top_n provided.")
57 |
58 | latest_user_message = get_latest_user_message(merged.get("messages"))
59 | if latest_user_message is None:
60 | raise Exception("No user message found.")
61 |
62 | top_functions = self.get_top_n_functions(
63 | query=latest_user_message["content"], top_n=top_n
64 | )
65 |
66 | function_name, function_args = self.call_openai(merged, top_functions)
67 | function_response = self.run_function(name=function_name, args=function_args)
68 |
69 | base_return = dict(name=function_name, args=function_args)
70 |
71 | if "error" in function_response:
72 | base_return["error"] = function_response["error"]
73 | else:
74 | base_return["result"] = function_response
75 |
76 | return base_return
77 |
78 | def get_top_n_functions(self, *, query: str, top_n: int):
79 | return self.vectordb.search_impl(query=query, top_n=top_n)
80 |
81 | def call_openai(
82 | self, openai_args: Dict[str, Any], top_functions: list[Dict[str, Any]]
83 | ) -> Tuple[str, Dict[str, Any]]:
84 | openai_result = self.openai.chat(**openai_args, functions=top_functions)
85 |
86 | if not openai_result.function_call:
87 | raise Exception("No function call found in OpenAI response.")
88 |
89 | function_name = openai_result.function_call.name
90 | function_args = json.loads(openai_result.function_call.arguments)
91 | return function_name, function_args
92 |
93 | @staticmethod
94 | def run_function(*, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
95 | try:
96 | function_map = get_function_map()
97 | func = function_map[name]
98 | function_input_type = get_input_parameter_type(func.function)
99 | func_args = function_input_type(**args)
100 | func_result = func(func_args)
101 | return func_result.dict()
102 | except Exception as e:
103 | return dict(error=str(e))
104 |
--------------------------------------------------------------------------------
/sageai/services/defaultvectordb_service.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from qdrant_client import QdrantClient
4 | from qdrant_client.http.models import models
5 |
6 | from sageai.services.openai_service import OpenAIService
7 | from sageai.types.abstract_vectordb import AbstractVectorDB
8 | from sageai.types.function import Function
9 |
10 |
11 | class DefaultVectorDBService(AbstractVectorDB):
12 | def __init__(self):
13 | super().__init__()
14 |
15 | self.openai = OpenAIService()
16 | self.client = QdrantClient(":memory:")
17 | self.collection = "functions"
18 |
19 | def index(self):
20 | self.client.recreate_collection(
21 | collection_name=self.collection,
22 | vectors_config=models.VectorParams(
23 | size=self.openai.get_embeddings_size(),
24 | distance=models.Distance.COSINE,
25 | ),
26 | )
27 |
28 | def format_func_embedding(func: Function) -> str:
29 | return func.name.replace("_", " ") + " - " + func.description
30 |
31 | records = [
32 | models.Record(
33 | id=idx,
34 | vector=self.openai.create_embeddings(
35 | model="text-embedding-ada-002", input=format_func_embedding(func)
36 | ),
37 | payload={"func_name": func_name},
38 | )
39 | for idx, (func_name, func) in enumerate(self.function_map.items())
40 | ]
41 | self.client.upload_records(
42 | collection_name=self.collection,
43 | records=records,
44 | )
45 |
46 | def search(self, *, query: str, top_n: int) -> List[str]:
47 | query_embedding = self.openai.create_embeddings(
48 | model="text-embedding-ada-002",
49 | input=query,
50 | )
51 | hits = self.client.search(
52 | collection_name=self.collection,
53 | query_vector=query_embedding,
54 | limit=top_n,
55 | )
56 | func_names = [hit.payload["func_name"] for hit in hits]
57 | return func_names
58 |
--------------------------------------------------------------------------------
/sageai/services/openai_service.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, List, Literal, Optional, Union
2 |
3 | from openai import OpenAI
4 | from openai._types import NOT_GIVEN
5 |
6 |
7 | class OpenAIService:
8 | def __init__(self):
9 | from sageai.config import get_config
10 |
11 | config = get_config()
12 |
13 | self.client = OpenAI(api_key=config.openai_key)
14 |
15 | def create_embeddings(
16 | self,
17 | *,
18 | input: Union[str, List[str], List[int], List[List[int]]],
19 | model: Union[str, Literal["text-embedding-ada-002"]],
20 | encoding_format: Optional[Literal["float", "base64"]] = NOT_GIVEN,
21 | user: Optional[str] = NOT_GIVEN,
22 | **kwargs,
23 | ) -> List[float]:
24 | response = self.client.embeddings.create(
25 | input=input,
26 | model=model,
27 | encoding_format=encoding_format,
28 | user=user,
29 | **kwargs,
30 | )
31 | embeddings = response.data[0].embedding
32 | return embeddings
33 |
34 | def chat(
35 | self,
36 | *,
37 | messages: List[Dict[str, str]],
38 | model: str,
39 | function_call: Optional[Dict[str, Any]] = NOT_GIVEN,
40 | functions: Dict[str, Any] = NOT_GIVEN,
41 | max_tokens: Optional[int] = NOT_GIVEN,
42 | response_format: Optional[Literal["string", "json"]] = NOT_GIVEN,
43 | temperature: Optional[float] = NOT_GIVEN,
44 | **kwargs,
45 | ) -> Dict[str, Any]:
46 | response = self.client.chat.completions.create(
47 | messages=messages,
48 | model=model,
49 | function_call=function_call,
50 | functions=functions,
51 | max_tokens=max_tokens,
52 | response_format=response_format,
53 | temperature=temperature,
54 | **kwargs,
55 | )
56 | response_message = response.choices[0].message
57 | return response_message
58 |
59 | @staticmethod
60 | def get_embeddings_size():
61 | return 1536
62 |
--------------------------------------------------------------------------------
/sageai/tests/integration.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from time import sleep
4 | from typing import cast, get_type_hints
5 | import pydantic
6 |
7 | import pytest
8 |
9 | from sageai.sageai import SageAI
10 | from sageai.types.function import Function
11 | from sageai.types.log_level import LogLevel
12 | from sageai.utils.file_utilities import get_functions_directories, load_module_from_file
13 | from sageai.utils.logger import get_logger
14 |
15 | logger = get_logger("IntegrationTest", LogLevel.INFO)
16 |
17 |
18 | @pytest.mark.parametrize(
19 | "dirpath",
20 | get_functions_directories(
21 | logger=logger, functions_directory_path=os.getenv("TEST_DIRECTORY")
22 | ),
23 | )
24 | def test_integration(dirpath):
25 | sleep(2)
26 | test_file = os.path.join(dirpath, "test.json")
27 | function_file = os.path.join(dirpath, "function.py")
28 | function_module = load_module_from_file("function", function_file)
29 | function_to_test = cast(Function, getattr(function_module, "function"))
30 |
31 | directory_full_path = os.path.abspath(os.getcwd())
32 | functions_directory = os.path.join(directory_full_path, dirpath)
33 |
34 | openai_key = os.environ["OPENAI_KEY"]
35 | sageai = SageAI(openai_key=openai_key, functions_directory=functions_directory)
36 | sageai.index()
37 |
38 | try:
39 | output_data_model_class = get_type_hints(function_to_test.function)["return"]
40 |
41 | with open(test_file) as f:
42 | test_cases = json.load(f)
43 | for i, test_case in enumerate(test_cases):
44 | logger.info(
45 | f"{function_module.function.name} - Running test case {i+1}/{len(test_cases)}: {test_case['message']}",
46 | )
47 | try:
48 | vector_db_result = sageai.vectordb.search(
49 | query=test_case["message"], top_n=5
50 | )
51 | result = sageai.chat(
52 | messages=[dict(role="user", content=test_case["message"])],
53 | model="gpt-3.5-turbo-0613",
54 | top_n=5,
55 | )
56 | if result is None:
57 | logger.error(
58 | "No result returned from Sennin for function: {}".format(
59 | function_module.function.name
60 | )
61 | )
62 | continue
63 |
64 | test_output = None
65 | try:
66 | test_output = output_data_model_class(
67 | **test_case["output"]
68 | ).dict()
69 | except pydantic.ValidationError as ve:
70 | expected_fields = ", ".join(
71 | get_type_hints(output_data_model_class).keys()
72 | )
73 | actual_fields = test_case["output"]
74 | logger.error(
75 | f"Validation error for function output {function_module.function.name}:\nExpected fields: {expected_fields}\nGot: {actual_fields}.\n{str(ve)}"
76 | )
77 | continue
78 |
79 | if function_module.function.name not in vector_db_result:
80 | logger.error(
81 | f"VectorDB did not return expected function for {function_module.function.name}. Expected: {function_module.function.name} in {vector_db_result}."
82 | )
83 | continue
84 | if result["name"] != function_module.function.name:
85 | logger.error(
86 | f"Wrong function chosen by model for {function_module.function.name}. Expected: {function_module.function.name}, Got: {result['name']}"
87 | )
88 | continue
89 | if result["result"] != test_output:
90 | logger.error(
91 | f"Output mismatch for {function_module.function.name}. Expected: {test_output}, Got: {result['result']}"
92 | )
93 | continue
94 |
95 | except Exception as e:
96 | logger.error(
97 | f"Function {function_module.function.name} execution failed: {str(e)}"
98 | )
99 |
100 | except Exception as e:
101 | logger.error(f"Entire test failed: {str(e)} for directory {dirpath}")
102 |
--------------------------------------------------------------------------------
/sageai/tests/main.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 |
4 | import pytest
5 |
6 |
7 | def main():
8 | parser = argparse.ArgumentParser(description="Run tests for functions.")
9 | parser.add_argument("--directory", type=str, help="Name of function to test")
10 | parser.add_argument(
11 | "--apikey", type=str, help="OpenAI API key for integration tests"
12 | )
13 | parser.add_argument("--unit", action="store_true", help="Run only the unit tests")
14 | parser.add_argument(
15 | "--integration", action="store_true", help="Run only the integration tests"
16 | )
17 | args = parser.parse_args()
18 |
19 | if args.directory:
20 | os.environ["TEST_DIRECTORY"] = args.directory
21 | if args.apikey:
22 | os.environ["OPENAI_KEY"] = args.apikey
23 |
24 | current_directory = os.path.dirname(os.path.abspath(__file__))
25 |
26 | if not args.integration or args.unit:
27 | unit_test_path = os.path.join(current_directory, "unit.py")
28 | pytest.main(["-x", "-s", unit_test_path, "--disable-warnings"])
29 | if not args.unit or args.integration:
30 | integration_test_path = os.path.join(current_directory, "integration.py")
31 | pytest.main(["-x", "-s", integration_test_path, "--disable-warnings"])
32 |
--------------------------------------------------------------------------------
/sageai/tests/unit.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from typing import cast, get_type_hints
4 | import pydantic
5 |
6 | import pytest
7 |
8 | from sageai.types.function import Function
9 | from sageai.types.log_level import LogLevel
10 | from sageai.utils.file_utilities import get_functions_directories, load_module_from_file
11 | from sageai.utils.logger import get_logger
12 |
13 | logger = get_logger("UnitTest", LogLevel.INFO)
14 |
15 |
16 | def assert_function_module_correct(folder_name, function_module):
17 | if not hasattr(function_module, "function"):
18 | msg = f"{folder_name}/function.py does not export a variable called 'function'"
19 | raise ValueError(msg)
20 |
21 |
22 | def assert_test_cases_correct(test_cases, folder_name):
23 | for idx, test_case in enumerate(test_cases):
24 | if not isinstance(test_case, dict):
25 | msg = f"Test case #{idx + 1} in {folder_name}/test.json is not an object"
26 | raise ValueError(msg)
27 |
28 | missing_properties = [
29 | prop for prop in ["input", "output", "message"] if prop not in test_case
30 | ]
31 | if missing_properties:
32 | msg = f"Test case #{idx + 1} in {folder_name}/tests.json missing properties: {', '.join(missing_properties)}"
33 | raise ValueError(msg)
34 |
35 | if not isinstance(test_case.get("message", ""), str):
36 | msg = f"Test case #{idx + 1} in {folder_name}/tests.json has a 'message' property which is not of type string"
37 | raise ValueError(msg)
38 |
39 |
40 | @pytest.mark.parametrize(
41 | "dirpath",
42 | get_functions_directories(
43 | logger, functions_directory_path=os.getenv("TEST_DIRECTORY")
44 | ),
45 | )
46 | def test_unit(dirpath):
47 | folder_name = os.path.basename(dirpath)
48 | function_file = os.path.join(dirpath, "function.py")
49 | test_file = os.path.join(dirpath, "test.json")
50 |
51 | try:
52 | # Check necessary files
53 | if not os.path.exists(function_file):
54 | msg = f"Missing function.py in {folder_name}"
55 | raise ValueError(msg)
56 |
57 | if not os.path.exists(test_file):
58 | msg = f"Missing test.json in {folder_name}"
59 | raise ValueError(msg)
60 |
61 | function_module = load_module_from_file("function", function_file)
62 | function_to_test = cast(Function, getattr(function_module, "function"))
63 | assert_function_module_correct(folder_name, function_module)
64 |
65 | input_model_class = function_to_test.input_type
66 | output_data_model_class = get_type_hints(function_to_test.function)["return"]
67 |
68 | with open(test_file) as f:
69 | test_cases = json.load(f)
70 | assert_test_cases_correct(test_cases, folder_name)
71 |
72 | for test_case in test_cases:
73 | logger.info(f"Running message {test_case['message']}")
74 | func_output = function_to_test(input_model_class(**test_case["input"]))
75 |
76 | test_output = None
77 | try:
78 | test_output = output_data_model_class(**test_case["output"]).dict()
79 | except pydantic.ValidationError as ve:
80 | expected_fields = ", ".join(
81 | get_type_hints(output_data_model_class).keys()
82 | )
83 | actual_fields = test_case["output"]
84 | logger.error(
85 | f"Validation error for function output {function_module.function.name}:\nExpected fields: {expected_fields}\nGot: {actual_fields}.\n{str(ve)}"
86 | )
87 | continue
88 |
89 | if func_output != test_output:
90 | logger.error(
91 | f"Function {folder_name} output mismatch for message '{test_case['message']}':"
92 | )
93 | logger.error(f"Expected: {test_output}")
94 | logger.error(f"Got: {func_output}")
95 | raise AssertionError("Expected output does not match actual output")
96 |
97 | except Exception as e:
98 | logger.error(f"Function {folder_name} failed. Error: {str(e)}")
99 |
--------------------------------------------------------------------------------
/sageai/tests/utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import List
3 |
4 | from lasso_sennin.utils.address_mapping_utilities import test_address_mappings
5 |
6 |
7 | def replace_with_index(string: str, pattern: str, replacement_list: List[str]):
8 | for match in re.finditer(pattern, string):
9 | index = int(match.group(1)) - 1
10 | string = string.replace(match.group(0), replacement_list[index])
11 | return string
12 |
13 |
14 | def replace_without_index(string: str, pattern: str, replacement: str):
15 | return string.replace(pattern, replacement)
16 |
17 |
18 | def replace_addresses_with_test_addresses(string: str):
19 | for key, value in test_address_mappings.items():
20 | pattern_base = re.escape(
21 | key[:-1],
22 | )
23 | string = replace_with_index(
24 | string,
25 | pattern_base + r"_(\d+)>",
26 | value,
27 | )
28 | string = replace_without_index(
29 | string,
30 | pattern_base + r">",
31 | value[0],
32 | )
33 | return string
34 |
--------------------------------------------------------------------------------
/sageai/types/abstract_vectordb.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any, Dict, List
3 |
4 |
5 | class AbstractVectorDB(ABC):
6 | def __init__(self):
7 | from sageai.config import get_function_map
8 |
9 | self.function_map = get_function_map()
10 |
11 | @abstractmethod
12 | def index(self) -> None:
13 | """Indexes the vector db based on the functions directory."""
14 | pass
15 |
16 | @abstractmethod
17 | def search(self, *, query: str, top_n: int) -> List[str]:
18 | """Actual search logic, which should be implemented in derived classes.
19 | It should return a list of function names
20 | """
21 | pass
22 |
23 | def format_search_result(
24 | self, *, function_names: List[str]
25 | ) -> List[Dict[str, Any]]:
26 | potential_functions = [
27 | self.function_map[func_name].parameters for func_name in function_names
28 | ]
29 | return potential_functions
30 |
31 | def search_impl(self, *, query: str, top_n: int) -> List[Dict[str, Any]]:
32 | """Search vector db based on a query and return top n function names."""
33 | results = self.search(query=query, top_n=top_n)
34 | return self.format_search_result(function_names=results)
35 |
--------------------------------------------------------------------------------
/sageai/types/function.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, Dict, List, Type
2 |
3 | from pydantic import BaseModel
4 |
5 | from sageai.utils.inspection_utilities import get_input_parameter_type
6 | from sageai.utils.model_utilities import (
7 | get_array_item_type,
8 | get_enum_array_values,
9 | get_possible_values,
10 | is_array,
11 | is_enum,
12 | is_enum_array,
13 | is_optional,
14 | )
15 |
16 |
17 | class Function(BaseModel):
18 | # required
19 | function: Callable
20 | description: str
21 |
22 | # generated from input
23 | name: str
24 | parameters: Dict[str, Any]
25 | input_type: Type[BaseModel]
26 |
27 | def __init__(
28 | self,
29 | function: Callable,
30 | description: str,
31 | ) -> None:
32 | name = function.__name__
33 | input_parameter_type = get_input_parameter_type(function)
34 | formatted_parameters = self._format_parameters(
35 | input_parameter_type=input_parameter_type,
36 | name=name,
37 | description=description,
38 | )
39 |
40 | super().__init__(
41 | function=function,
42 | description=description,
43 | name=name,
44 | parameters=formatted_parameters,
45 | input_type=input_parameter_type,
46 | )
47 |
48 | @staticmethod
49 | def _format_parameters(input_parameter_type, name: str, description: str):
50 | parameters = input_parameter_type.schema()
51 | new_schema = {
52 | "name": name,
53 | "description": description,
54 | "parameters": {
55 | "type": "object",
56 | "properties": {},
57 | "required": [],
58 | },
59 | }
60 |
61 | for key, value in parameters["properties"].items():
62 | if is_enum_array(input_parameter_type, key):
63 | enum_values = get_enum_array_values(input_parameter_type, key)
64 | enum_type = "integer" if isinstance(enum_values[0], int) else "string"
65 | property_schema = {
66 | "type": "array",
67 | "description": value.get("description", ""),
68 | "items": {"type": enum_type, "enum": enum_values},
69 | }
70 | elif is_enum(input_parameter_type, key):
71 | property_schema = {
72 | "type": "string",
73 | "description": value["description"],
74 | "enum": get_possible_values(input_parameter_type, key),
75 | }
76 | elif is_array(input_parameter_type, key):
77 | property_schema = {
78 | "type": "array",
79 | "description": value.get("description", ""),
80 | "items": {
81 | "type": get_array_item_type(input_parameter_type, key),
82 | },
83 | }
84 | else:
85 | property_type = value.get("type")
86 | if property_type is None:
87 | raise Exception("Property type is None")
88 | property_schema = {
89 | "type": property_type,
90 | "description": value.get("description", ""),
91 | }
92 |
93 | new_schema["parameters"]["properties"][key] = property_schema
94 |
95 | # Check if the property is required
96 | if not is_optional(input_parameter_type, key):
97 | new_schema["parameters"]["required"].append(key)
98 |
99 | return new_schema
100 |
101 | def __call__(self, *args, **kwargs):
102 | return self.function(*args, **kwargs)
103 |
104 | def __str__(self):
105 | return f"{self.name}: {self.description}"
106 |
--------------------------------------------------------------------------------
/sageai/types/log_level.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class LogLevel(str, Enum):
5 | DEBUG = "DEBUG"
6 | INFO = "INFO"
7 | WARNING = "WARNING"
8 | ERROR = "ERROR"
9 | CRITICAL = "CRITICAL"
10 |
--------------------------------------------------------------------------------
/sageai/utils/file_utilities.py:
--------------------------------------------------------------------------------
1 | import os
2 | from importlib import util
3 | from types import ModuleType
4 | from typing import List
5 |
6 | from sageai.types.function import Function
7 | from sageai.utils.logger import get_logger
8 |
9 |
10 | def load_module_from_file(module_name: str, filepath: str) -> ModuleType:
11 | spec = util.spec_from_file_location(module_name, filepath)
12 | module = util.module_from_spec(spec)
13 | spec.loader.exec_module(module)
14 | return module
15 |
16 |
17 | def get_functions_directories(
18 | logger,
19 | functions_directory_path: str = None,
20 | ) -> List[str]:
21 | logger.info(
22 | f"Getting functions directories from {functions_directory_path}",
23 | )
24 |
25 | function_directories = [
26 | dirpath
27 | for dirpath, dirnames, filenames in os.walk(functions_directory_path)
28 | if "function.py" in filenames
29 | ]
30 | logger.info(f"Found {len(function_directories)} function directories")
31 | return sorted(function_directories)
32 |
33 |
34 | def generate_functions_map() -> dict[str, Function]:
35 | from sageai.config import get_config
36 |
37 | config = get_config()
38 | functions_directory_path = config.functions_directory
39 | log_level = config.log_level
40 | logger = get_logger("Utils", log_level)
41 | available_functions = {}
42 |
43 | logger.info("Generating function map")
44 | functions_directory = get_functions_directories(logger, functions_directory_path)
45 |
46 | for dirpath in functions_directory:
47 | folder_name = os.path.basename(dirpath)
48 | function_file = os.path.join(dirpath, "function.py")
49 | function_module = load_module_from_file(folder_name, function_file)
50 |
51 | if not hasattr(function_module, "function"):
52 | raise Exception(
53 | f"Function {folder_name} does not have a function attribute",
54 | )
55 |
56 | available_functions[function_module.function.name] = function_module.function
57 |
58 | if len(available_functions) == 0:
59 | raise Exception("No functions found")
60 | logger.info(f"Function map generated with {len(available_functions)} functions")
61 | available_functions = dict(sorted(available_functions.items()))
62 |
63 | return available_functions
64 |
--------------------------------------------------------------------------------
/sageai/utils/format_config_args.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import json
3 | import os
4 |
5 |
6 | def get_user_package_path():
7 | caller_frame = inspect.stack()[4]
8 | caller_module = inspect.getmodule(caller_frame[0])
9 |
10 | if caller_module is None or not hasattr(caller_module, "__file__"):
11 | raise ValueError("Cannot determine the path of the caller's module")
12 |
13 | module_path = os.path.abspath(caller_module.__file__)
14 | return os.path.dirname(module_path)
15 |
16 |
17 | def inject_env_vars(**kwargs):
18 | current_directory = os.path.dirname(os.path.abspath(__file__))
19 | env_file_path = os.path.join(current_directory, "../env.json")
20 |
21 | if not os.path.exists(env_file_path):
22 | return kwargs
23 |
24 | with open(env_file_path) as f:
25 | env_vars = json.load(f)
26 |
27 | if env_vars["OPENAI_KEY"]:
28 | kwargs["openai_key"] = env_vars["OPENAI_KEY"]
29 |
30 | return kwargs
31 |
32 |
33 | def format_config_args(**kwargs):
34 | kwargs = inject_env_vars(**kwargs)
35 |
36 | if "functions_directory" not in kwargs:
37 | kwargs["functions_directory"] = "functions"
38 | if not os.path.isabs(kwargs["functions_directory"]) and not kwargs[
39 | "functions_directory"
40 | ].startswith("./"):
41 | base_path = get_user_package_path()
42 | kwargs["functions_directory"] = os.path.join(
43 | base_path, kwargs["functions_directory"]
44 | )
45 | elif kwargs["functions_directory"].startswith("./"):
46 | base_path = get_user_package_path()
47 | # Strip './' from the beginning and then join with the base path
48 | relative_path = kwargs["functions_directory"][2:]
49 | kwargs["functions_directory"] = os.path.join(base_path, relative_path)
50 | return kwargs
51 |
--------------------------------------------------------------------------------
/sageai/utils/inspection_utilities.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from typing import Callable, Type
3 |
4 | from pydantic import BaseModel
5 |
6 |
7 | def get_input_parameter_type(function: Callable) -> Type[BaseModel]:
8 | signature = inspect.signature(function)
9 | for name, param in signature.parameters.items():
10 | if "Input" in str(param.annotation):
11 | return param.annotation
12 | raise Exception(f"Could not find Input parameter type from function arguments.")
13 |
--------------------------------------------------------------------------------
/sageai/utils/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from sageai.types.log_level import LogLevel
3 |
4 |
5 | def get_logger(logger_name: str, log_level: LogLevel):
6 | logger = logging.getLogger(logger_name)
7 |
8 | log_level_value = log_level.value
9 | logger.setLevel(log_level_value)
10 |
11 | formatter = logging.Formatter("%(asctime)s | [%(levelname)s] %(message)s")
12 |
13 | if not logger.handlers:
14 | console_handler = logging.StreamHandler()
15 | console_handler.setLevel(log_level_value)
16 | console_handler.setFormatter(formatter)
17 | logger.addHandler(console_handler)
18 | else:
19 | logger.handlers[0].setLevel(log_level_value) # assumes only one handler
20 |
21 | return logger
22 |
--------------------------------------------------------------------------------
/sageai/utils/model_utilities.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from types import ModuleType
3 | from typing import Any, List, Optional, Type, Union, get_args, get_origin
4 |
5 | from pydantic import BaseModel
6 |
7 |
8 | def find_pydantic_model(
9 | module: ModuleType,
10 | keyword: str,
11 | ) -> Type[BaseModel] | None:
12 | models = [
13 | cls
14 | for name, cls in vars(module).items()
15 | if isinstance(cls, type)
16 | and issubclass(cls, BaseModel)
17 | and name.endswith(
18 | keyword,
19 | )
20 | ]
21 | return models[0] if models else None
22 |
23 |
24 | def get_input_model(module: ModuleType) -> Type[BaseModel]:
25 | return find_pydantic_model(module, keyword="Input")
26 |
27 |
28 | def get_output_data_model(module: ModuleType) -> Type[BaseModel]:
29 | return find_pydantic_model(module, keyword="Data")
30 |
31 |
32 | def is_enum(model: Type[BaseModel], key: str) -> bool:
33 | if key in model.__annotations__:
34 | annotation = model.__annotations__[key]
35 | # Check if annotation is a class
36 | if isinstance(annotation, type):
37 | return issubclass(annotation, Enum)
38 | # Handle Optional[Enum] and List[Enum]
39 | origin = get_origin(annotation)
40 | if origin in {Optional, List, Union}:
41 | args = get_args(annotation)
42 | if args and isinstance(args[0], type):
43 | return issubclass(args[0], Enum)
44 | return False
45 |
46 |
47 | def get_possible_values(model: Type[BaseModel], key: str) -> List[str] | List[int]:
48 | if is_enum(model, key) or is_enum_array(model, key):
49 | enum_type = model.__annotations__[key]
50 | if get_origin(enum_type) in {Optional, Union, List}:
51 | enum_type = get_args(enum_type)[0]
52 | return [e.value for e in enum_type]
53 | return []
54 |
55 |
56 | def is_optional(model: Type[BaseModel], key: str) -> bool:
57 | annotation = model.__annotations__[key]
58 | return type(None) in get_args(annotation)
59 |
60 |
61 | def is_array(model: Type[BaseModel], key: str) -> bool:
62 | if key in model.__annotations__:
63 | annotation = model.__annotations__[key]
64 | if get_origin(annotation) is list:
65 | return True
66 |
67 | if get_origin(annotation) in {Optional, Union}:
68 | args = get_args(annotation)
69 | return any(get_origin(arg) is list for arg in args)
70 | return False
71 |
72 |
73 | def get_array_item_type(model: Type[BaseModel], key: str) -> str | None:
74 | type_map = {"str": "string", "int": "integer"}
75 | if not is_array(model, key):
76 | raise Exception(f"{key} is not an array")
77 | field = model.__annotations__[key]
78 | if get_origin(field) is list:
79 | return type_map[get_args(field)[0].__name__]
80 | elif get_origin(field) in {Optional, Union}:
81 | args = get_args(field)
82 | for arg in args:
83 | if get_origin(arg) is list:
84 | return type_map[get_args(arg)[0].__name__]
85 | return None
86 |
87 |
88 | def is_enum_array(model: Type[BaseModel], key: str) -> bool:
89 | enum_type = get_array_item_enum_type(model, key)
90 | return issubclass(enum_type, Enum) if enum_type is not None else False
91 |
92 |
93 | def get_enum_type_from_annotation(field) -> Type[Enum] | None:
94 | enum_type = None
95 | if get_origin(field) is list:
96 | enum_type = get_args(field)[0]
97 | elif get_origin(field) in {Optional, Union}:
98 | args = get_args(field)
99 | for arg in args:
100 | if get_origin(arg) is list:
101 | enum_type = get_args(arg)[0]
102 | if enum_type and issubclass(enum_type, Enum):
103 | return enum_type
104 | return None
105 |
106 |
107 | def get_array_item_enum_type(model: Type[BaseModel], key: str) -> Type | None:
108 | if not is_array(model, key):
109 | return None
110 | field = model.__annotations__[key]
111 | return get_enum_type_from_annotation(field)
112 |
113 |
114 | def get_enum_array_values(
115 | model: Type[BaseModel],
116 | key: str,
117 | ) -> List[str] | List[int]:
118 | if not is_enum_array(model, key):
119 | raise Exception(f"{key} is not an array of Enums")
120 | field = model.__annotations__[key]
121 | enum_type = get_enum_type_from_annotation(field)
122 | if enum_type:
123 | return [e.value for e in enum_type.__members__.values()]
124 | return []
125 |
126 |
127 | def get_default_value(model: Type[BaseModel], key: str) -> Any:
128 | if is_enum(model, key):
129 | default_value = model.__fields__[key].default
130 | if is_enum(model, key) and isinstance(default_value, Enum):
131 | return default_value.value
132 | return default_value
133 |
134 | if key in model.__fields__:
135 | default_value = model.__fields__[key].default
136 | if default_value == "None" or default_value is None:
137 | return None
138 | return default_value
139 |
140 | return None
141 |
--------------------------------------------------------------------------------
/sageai/utils/openai_utilities.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 |
3 |
4 | def get_latest_user_message(messages: List[Dict]) -> Dict | None:
5 | for message in reversed(messages):
6 | if message.get("role") == "user":
7 | return message
8 | return None
9 |
--------------------------------------------------------------------------------