├── .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 | ![Logo](https://github.com/0xnenlabs/SageAI/assets/45445790/750fb3f9-0830-4948-9a86-61e59d933b45) 2 | 3 |

4 | Folder-based functions for ChatGPT's function calling with Pydantic support 🚀 5 |

6 | 7 |

8 | 9 | Package version 10 | 11 | 12 | Supported Python versions 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 | ![Design](https://github.com/0xnenlabs/SageAI/assets/45445790/eb81d280-5b69-472a-b45a-4a9275fcf341) 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 | --------------------------------------------------------------------------------