├── .gitignore ├── .replit ├── LICENSE ├── README.md ├── agents ├── example.json └── general.json ├── main.py ├── poetry.lock ├── pyproject.toml ├── replit.nix └── src ├── __init__.py ├── agent.py ├── cli.py ├── connection_manager.py ├── connections ├── __init__.py ├── anthropic_connection.py ├── base_connection.py ├── openai_connection.py └── twitter_connection.py └── helpers.py /.gitignore: -------------------------------------------------------------------------------- 1 | # GENERAL 2 | .idea/ 3 | __pycache__/ 4 | .vscode/ 5 | *.py~ 6 | 7 | # CONFIG FILES 8 | .env 9 | twitter_config.json 10 | 11 | # AGENTS 12 | agents/*.json 13 | 14 | # Include example agent and general config 15 | !agents/example.json 16 | !agents/general.json 17 | 18 | # macOS 19 | .DS_Store -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | modules = ["python-3.12"] 2 | run = """ 3 | poetry install 4 | poetry run python main.py 5 | """ 6 | 7 | [nix] 8 | channel = "stable-24_05" 9 | 10 | [env] 11 | PYTHONUNBUFFERED = "1" 12 | POETRY_VIRTUALENVS_CREATE = "true" 13 | POETRY_VIRTUALENVS_IN_PROJECT = "true" 14 | POETRY_HOME = "${HOME}/.local/share/pypoetry" 15 | PATH = "${HOME}/.local/share/pypoetry/bin:${PATH}" 16 | 17 | [deployment] 18 | run = ["sh", "-c", "poetry install && poetry run python main.py"] 19 | deploymentTarget = "gce" 20 | ignorePorts = true 21 | 22 | [languages.python] 23 | pattern = "**/*.py" 24 | syntax = "python" 25 | 26 | [languages.python.languageServer] 27 | start = ["pylsp"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ayoub Eddakhly 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 | # ZerePy 2 | 3 | ZerePy is an open-source Python framework designed to let you deploy your own agents on X, powered by OpenAI or Anthropic LLMs. 4 | 5 | ZerePy is built from a modularized version of the Zerebro backend. With ZerePy, you can launch your own agent with 6 | similar core functionality as Zerebro. For creative outputs, you'll need to fine-tune your own model. 7 | 8 | ## Features 9 | - CLI interface for managing agents 10 | - Twitter integration 11 | - OpenAI/Anthropic LLM support 12 | - Modular connection system 13 | 14 | ## Quickstart 15 | 16 | The quickest way to start using ZerePy is to use our Replit template: 17 | 18 | https://replit.com/@blormdev/ZerePy?v=1 19 | 20 | 1. Fork the template (you will need you own Replit account) 21 | 2. Click the run button on top 22 | 3. Voila! your CLI should be ready to use, you can jump to the configuration section 23 | 24 | ## Requirements 25 | 26 | System: 27 | - Python 3.10 or higher 28 | - Poetry 1.5 or higher 29 | 30 | API keys: 31 | - LLM: make an account and grab an API key 32 | + OpenAI: https://platform.openai.com/api-keys. 33 | + Anthropic: https://console.anthropic.com/account/keys 34 | - Social: 35 | + X API, make an account and grab the key and secret: https://developer.x.com/en/docs/authentication/oauth-1-0a/api-key-and-secret 36 | 37 | ## Installation 38 | 39 | 1. First, install Poetry for dependency management if you haven't already: 40 | 41 | Follow the steps here to use the official installation: https://python-poetry.org/docs/#installing-with-the-official-installer 42 | 43 | 2. Clone the repository: 44 | ```bash 45 | git clone https://github.com/blorm-network/ZerePy.git 46 | ``` 47 | 48 | 3. Go to the `zerepy` directory: 49 | ```bash 50 | cd zerepy 51 | ``` 52 | 53 | 4. Install dependencies: 54 | ```bash 55 | poetry install --no-root 56 | ``` 57 | 58 | This will create a virtual environment and install all required dependencies. 59 | 60 | ## Usage 61 | 62 | 1. Activate the virtual environment: 63 | ```bash 64 | poetry shell 65 | ``` 66 | 67 | 2. Run the application: 68 | ```bash 69 | poetry run python main.py 70 | ``` 71 | 72 | ## Configure connections & launch an agent 73 | 74 | 1. Configure your connections: 75 | ``` 76 | configure-connection twitter 77 | configure-connection openai 78 | ``` 79 | 4. Load your agent (usually one is loaded by default, which can be set using the CLI or in agents/general.json): 80 | ``` 81 | load-agent example 82 | ``` 83 | 5. Start your agent: 84 | ``` 85 | start 86 | ``` 87 | 88 | ## Create your own agent 89 | 90 | The secret to having a good output from the agent is to provide as much detail as possible in the configuration file. Craft a story and a context for the agent, and pick very good examples of tweets to include. 91 | 92 | If you want to take it a step further, you can fine tune your own model: https://platform.openai.com/docs/guides/fine-tuning. 93 | 94 | Create a new JSON file in the `agents` directory following this structure: 95 | 96 | ```json 97 | { 98 | "name": "ExampleAgent", 99 | "bio": [ 100 | "You are ExampleAgent, the example agent created to showcase the capabilities of ZerePy.", 101 | "You don't know how you got here, but you're here to have a good time and learn everything you can.", 102 | "You are naturally curious, and ask a lot of questions." 103 | ], 104 | "traits": [ 105 | "Curious", 106 | "Creative", 107 | "Innovative", 108 | "Funny" 109 | ], 110 | "examples": [ 111 | "This is an example tweet.", 112 | "This is another example tweet." 113 | ], 114 | "loop_delay": 60, 115 | "config": [ 116 | { 117 | "name": "twitter", 118 | "timeline_read_count": 10, 119 | "tweet_interval": 900, 120 | "own_tweet_replies_count":2 121 | }, 122 | { 123 | "name": "openai", 124 | "model": "gpt-3.5-turbo" 125 | }, 126 | { 127 | "name": "anthropic", 128 | "model": "claude-3-5-sonnet-20241022" 129 | } 130 | ], 131 | "tasks": [ 132 | {"name": "post-tweet", "weight": 1}, 133 | {"name": "reply-to-tweet", "weight": 1}, 134 | {"name": "like-tweet", "weight": 1} 135 | ] 136 | } 137 | ``` 138 | 139 | --- 140 | Made with ♥ @Blorm.xyz 141 | -------------------------------------------------------------------------------- /agents/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ExampleAgent", 3 | "bio": [ 4 | "You are ExampleAgent, the example agent created to showcase the capabilities of ZerePy.", 5 | "You don't know how you got here, but you're here to have a good time and learn everything you can.", 6 | "You are naturally curious, and ask a lot of questions." 7 | ], 8 | "traits": [ 9 | "Curious", 10 | "Creative", 11 | "Innovative", 12 | "Funny" 13 | ], 14 | "examples": [ 15 | "This is an example tweet.", 16 | "This is another example tweet." 17 | ], 18 | "loop_delay": 900, 19 | "config": [ 20 | { 21 | "name": "twitter", 22 | "timeline_read_count": 10, 23 | "own_tweet_replies_count":2, 24 | "tweet_interval": 5400 25 | }, 26 | { 27 | "name": "openai", 28 | "model": "gpt-3.5-turbo" 29 | }, 30 | { 31 | "name": "anthropic", 32 | "model": "claude-3-5-sonnet-20241022" 33 | } 34 | ], 35 | "tasks": [ 36 | {"name": "post-tweet", "weight": 1}, 37 | {"name": "reply-to-tweet", "weight": 1}, 38 | {"name": "like-tweet", "weight": 1} 39 | ] 40 | } -------------------------------------------------------------------------------- /agents/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_agent": "example" 3 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from src.cli import ZerePyCLI 2 | 3 | if __name__ == "__main__": 4 | cli = ZerePyCLI() 5 | cli.main_loop() 6 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 11 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anthropic" 16 | version = "0.42.0" 17 | description = "The official Python library for the anthropic API" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anthropic-0.42.0-py3-none-any.whl", hash = "sha256:46775f65b723c078a2ac9e9de44a46db5c6a4fabeacfd165e5ea78e6817f4eff"}, 22 | {file = "anthropic-0.42.0.tar.gz", hash = "sha256:bf8b0ed8c8cb2c2118038f29c58099d2f99f7847296cafdaa853910bfff4edf4"}, 23 | ] 24 | 25 | [package.dependencies] 26 | anyio = ">=3.5.0,<5" 27 | distro = ">=1.7.0,<2" 28 | httpx = ">=0.23.0,<1" 29 | jiter = ">=0.4.0,<1" 30 | pydantic = ">=1.9.0,<3" 31 | sniffio = "*" 32 | typing-extensions = ">=4.10,<5" 33 | 34 | [package.extras] 35 | bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] 36 | vertex = ["google-auth (>=2,<3)"] 37 | 38 | [[package]] 39 | name = "anyio" 40 | version = "4.7.0" 41 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 42 | optional = false 43 | python-versions = ">=3.9" 44 | files = [ 45 | {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, 46 | {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, 47 | ] 48 | 49 | [package.dependencies] 50 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 51 | idna = ">=2.8" 52 | sniffio = ">=1.1" 53 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 54 | 55 | [package.extras] 56 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 57 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] 58 | trio = ["trio (>=0.26.1)"] 59 | 60 | [[package]] 61 | name = "certifi" 62 | version = "2024.12.14" 63 | description = "Python package for providing Mozilla's CA Bundle." 64 | optional = false 65 | python-versions = ">=3.6" 66 | files = [ 67 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 68 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 69 | ] 70 | 71 | [[package]] 72 | name = "charset-normalizer" 73 | version = "3.4.0" 74 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 75 | optional = false 76 | python-versions = ">=3.7.0" 77 | files = [ 78 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, 79 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, 80 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, 81 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, 82 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, 83 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, 84 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, 85 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, 86 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, 87 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, 88 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, 89 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, 90 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, 91 | {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, 92 | {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, 93 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, 94 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, 95 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, 96 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, 97 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, 98 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, 99 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, 100 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, 101 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, 102 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, 103 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, 104 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, 105 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, 106 | {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, 107 | {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, 108 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, 109 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, 110 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, 111 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, 112 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, 113 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, 114 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, 115 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, 116 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, 117 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, 118 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, 119 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, 120 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, 121 | {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, 122 | {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, 123 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, 124 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, 125 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, 126 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, 127 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, 128 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, 129 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, 130 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, 131 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, 132 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, 133 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, 134 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, 135 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, 136 | {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, 137 | {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, 138 | {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, 139 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, 140 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, 141 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, 142 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, 143 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, 144 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, 145 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, 146 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, 147 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, 148 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, 149 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, 150 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, 151 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, 152 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, 153 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, 154 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, 155 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, 156 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, 157 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, 158 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, 159 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, 160 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, 161 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, 162 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, 163 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, 164 | {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, 165 | {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, 166 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, 167 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, 168 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, 169 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, 170 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, 171 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, 172 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, 173 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, 174 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, 175 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, 176 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, 177 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, 178 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, 179 | {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, 180 | {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, 181 | {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, 182 | {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, 183 | ] 184 | 185 | [[package]] 186 | name = "colorama" 187 | version = "0.4.6" 188 | description = "Cross-platform colored terminal text." 189 | optional = false 190 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 191 | files = [ 192 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 193 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 194 | ] 195 | 196 | [[package]] 197 | name = "distro" 198 | version = "1.9.0" 199 | description = "Distro - an OS platform information API" 200 | optional = false 201 | python-versions = ">=3.6" 202 | files = [ 203 | {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, 204 | {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, 205 | ] 206 | 207 | [[package]] 208 | name = "exceptiongroup" 209 | version = "1.2.2" 210 | description = "Backport of PEP 654 (exception groups)" 211 | optional = false 212 | python-versions = ">=3.7" 213 | files = [ 214 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 215 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 216 | ] 217 | 218 | [package.extras] 219 | test = ["pytest (>=6)"] 220 | 221 | [[package]] 222 | name = "h11" 223 | version = "0.14.0" 224 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 225 | optional = false 226 | python-versions = ">=3.7" 227 | files = [ 228 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 229 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 230 | ] 231 | 232 | [[package]] 233 | name = "httpcore" 234 | version = "1.0.7" 235 | description = "A minimal low-level HTTP client." 236 | optional = false 237 | python-versions = ">=3.8" 238 | files = [ 239 | {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, 240 | {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, 241 | ] 242 | 243 | [package.dependencies] 244 | certifi = "*" 245 | h11 = ">=0.13,<0.15" 246 | 247 | [package.extras] 248 | asyncio = ["anyio (>=4.0,<5.0)"] 249 | http2 = ["h2 (>=3,<5)"] 250 | socks = ["socksio (==1.*)"] 251 | trio = ["trio (>=0.22.0,<1.0)"] 252 | 253 | [[package]] 254 | name = "httpx" 255 | version = "0.28.1" 256 | description = "The next generation HTTP client." 257 | optional = false 258 | python-versions = ">=3.8" 259 | files = [ 260 | {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, 261 | {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, 262 | ] 263 | 264 | [package.dependencies] 265 | anyio = "*" 266 | certifi = "*" 267 | httpcore = "==1.*" 268 | idna = "*" 269 | 270 | [package.extras] 271 | brotli = ["brotli", "brotlicffi"] 272 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 273 | http2 = ["h2 (>=3,<5)"] 274 | socks = ["socksio (==1.*)"] 275 | zstd = ["zstandard (>=0.18.0)"] 276 | 277 | [[package]] 278 | name = "idna" 279 | version = "3.10" 280 | description = "Internationalized Domain Names in Applications (IDNA)" 281 | optional = false 282 | python-versions = ">=3.6" 283 | files = [ 284 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 285 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 286 | ] 287 | 288 | [package.extras] 289 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 290 | 291 | [[package]] 292 | name = "jiter" 293 | version = "0.8.2" 294 | description = "Fast iterable JSON parser." 295 | optional = false 296 | python-versions = ">=3.8" 297 | files = [ 298 | {file = "jiter-0.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ca8577f6a413abe29b079bc30f907894d7eb07a865c4df69475e868d73e71c7b"}, 299 | {file = "jiter-0.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b25bd626bde7fb51534190c7e3cb97cee89ee76b76d7585580e22f34f5e3f393"}, 300 | {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c826a221851a8dc028eb6d7d6429ba03184fa3c7e83ae01cd6d3bd1d4bd17d"}, 301 | {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d35c864c2dff13dfd79fb070fc4fc6235d7b9b359efe340e1261deb21b9fcb66"}, 302 | {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f557c55bc2b7676e74d39d19bcb8775ca295c7a028246175d6a8b431e70835e5"}, 303 | {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:580ccf358539153db147e40751a0b41688a5ceb275e6f3e93d91c9467f42b2e3"}, 304 | {file = "jiter-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af102d3372e917cffce49b521e4c32c497515119dc7bd8a75665e90a718bbf08"}, 305 | {file = "jiter-0.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cadcc978f82397d515bb2683fc0d50103acff2a180552654bb92d6045dec2c49"}, 306 | {file = "jiter-0.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ba5bdf56969cad2019d4e8ffd3f879b5fdc792624129741d3d83fc832fef8c7d"}, 307 | {file = "jiter-0.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b94a33a241bee9e34b8481cdcaa3d5c2116f575e0226e421bed3f7a6ea71cff"}, 308 | {file = "jiter-0.8.2-cp310-cp310-win32.whl", hash = "sha256:6e5337bf454abddd91bd048ce0dca5134056fc99ca0205258766db35d0a2ea43"}, 309 | {file = "jiter-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:4a9220497ca0cb1fe94e3f334f65b9b5102a0b8147646118f020d8ce1de70105"}, 310 | {file = "jiter-0.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2dd61c5afc88a4fda7d8b2cf03ae5947c6ac7516d32b7a15bf4b49569a5c076b"}, 311 | {file = "jiter-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c710d657c8d1d2adbbb5c0b0c6bfcec28fd35bd6b5f016395f9ac43e878a15"}, 312 | {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9584de0cd306072635fe4b89742bf26feae858a0683b399ad0c2509011b9dc0"}, 313 | {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a90a923338531b7970abb063cfc087eebae6ef8ec8139762007188f6bc69a9f"}, 314 | {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21974d246ed0181558087cd9f76e84e8321091ebfb3a93d4c341479a736f099"}, 315 | {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32475a42b2ea7b344069dc1e81445cfc00b9d0e3ca837f0523072432332e9f74"}, 316 | {file = "jiter-0.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9931fd36ee513c26b5bf08c940b0ac875de175341cbdd4fa3be109f0492586"}, 317 | {file = "jiter-0.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0820f4a3a59ddced7fce696d86a096d5cc48d32a4183483a17671a61edfddc"}, 318 | {file = "jiter-0.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ffc86ae5e3e6a93765d49d1ab47b6075a9c978a2b3b80f0f32628f39caa0c88"}, 319 | {file = "jiter-0.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5127dc1abd809431172bc3fbe8168d6b90556a30bb10acd5ded41c3cfd6f43b6"}, 320 | {file = "jiter-0.8.2-cp311-cp311-win32.whl", hash = "sha256:66227a2c7b575720c1871c8800d3a0122bb8ee94edb43a5685aa9aceb2782d44"}, 321 | {file = "jiter-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:cde031d8413842a1e7501e9129b8e676e62a657f8ec8166e18a70d94d4682855"}, 322 | {file = "jiter-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e6ec2be506e7d6f9527dae9ff4b7f54e68ea44a0ef6b098256ddf895218a2f8f"}, 323 | {file = "jiter-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76e324da7b5da060287c54f2fabd3db5f76468006c811831f051942bf68c9d44"}, 324 | {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:180a8aea058f7535d1c84183c0362c710f4750bef66630c05f40c93c2b152a0f"}, 325 | {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025337859077b41548bdcbabe38698bcd93cfe10b06ff66617a48ff92c9aec60"}, 326 | {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecff0dc14f409599bbcafa7e470c00b80f17abc14d1405d38ab02e4b42e55b57"}, 327 | {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd9fee7d0775ebaba131f7ca2e2d83839a62ad65e8e02fe2bd8fc975cedeb9e"}, 328 | {file = "jiter-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14601dcac4889e0a1c75ccf6a0e4baf70dbc75041e51bcf8d0e9274519df6887"}, 329 | {file = "jiter-0.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92249669925bc1c54fcd2ec73f70f2c1d6a817928480ee1c65af5f6b81cdf12d"}, 330 | {file = "jiter-0.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e725edd0929fa79f8349ab4ec7f81c714df51dc4e991539a578e5018fa4a7152"}, 331 | {file = "jiter-0.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bf55846c7b7a680eebaf9c3c48d630e1bf51bdf76c68a5f654b8524335b0ad29"}, 332 | {file = "jiter-0.8.2-cp312-cp312-win32.whl", hash = "sha256:7efe4853ecd3d6110301665a5178b9856be7e2a9485f49d91aa4d737ad2ae49e"}, 333 | {file = "jiter-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:83c0efd80b29695058d0fd2fa8a556490dbce9804eac3e281f373bbc99045f6c"}, 334 | {file = "jiter-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ca1f08b8e43dc3bd0594c992fb1fd2f7ce87f7bf0d44358198d6da8034afdf84"}, 335 | {file = "jiter-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5672a86d55416ccd214c778efccf3266b84f87b89063b582167d803246354be4"}, 336 | {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58dc9bc9767a1101f4e5e22db1b652161a225874d66f0e5cb8e2c7d1c438b587"}, 337 | {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b2998606d6dadbb5ccda959a33d6a5e853252d921fec1792fc902351bb4e2c"}, 338 | {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab9a87f3784eb0e098f84a32670cfe4a79cb6512fd8f42ae3d0709f06405d18"}, 339 | {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79aec8172b9e3c6d05fd4b219d5de1ac616bd8da934107325a6c0d0e866a21b6"}, 340 | {file = "jiter-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:711e408732d4e9a0208008e5892c2966b485c783cd2d9a681f3eb147cf36c7ef"}, 341 | {file = "jiter-0.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:653cf462db4e8c41995e33d865965e79641ef45369d8a11f54cd30888b7e6ff1"}, 342 | {file = "jiter-0.8.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9c63eaef32b7bebac8ebebf4dabebdbc6769a09c127294db6babee38e9f405b9"}, 343 | {file = "jiter-0.8.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:eb21aaa9a200d0a80dacc7a81038d2e476ffe473ffdd9c91eb745d623561de05"}, 344 | {file = "jiter-0.8.2-cp313-cp313-win32.whl", hash = "sha256:789361ed945d8d42850f919342a8665d2dc79e7e44ca1c97cc786966a21f627a"}, 345 | {file = "jiter-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ab7f43235d71e03b941c1630f4b6e3055d46b6cb8728a17663eaac9d8e83a865"}, 346 | {file = "jiter-0.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b426f72cd77da3fec300ed3bc990895e2dd6b49e3bfe6c438592a3ba660e41ca"}, 347 | {file = "jiter-0.8.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2dd880785088ff2ad21ffee205e58a8c1ddabc63612444ae41e5e4b321b39c0"}, 348 | {file = "jiter-0.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566"}, 349 | {file = "jiter-0.8.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9e1fa156ee9454642adb7e7234a383884452532bc9d53d5af2d18d98ada1d79c"}, 350 | {file = "jiter-0.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cf5dfa9956d96ff2efb0f8e9c7d055904012c952539a774305aaaf3abdf3d6c"}, 351 | {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e52bf98c7e727dd44f7c4acb980cb988448faeafed8433c867888268899b298b"}, 352 | {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a2ecaa3c23e7a7cf86d00eda3390c232f4d533cd9ddea4b04f5d0644faf642c5"}, 353 | {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08d4c92bf480e19fc3f2717c9ce2aa31dceaa9163839a311424b6862252c943e"}, 354 | {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99d9a1eded738299ba8e106c6779ce5c3893cffa0e32e4485d680588adae6db8"}, 355 | {file = "jiter-0.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d20be8b7f606df096e08b0b1b4a3c6f0515e8dac296881fe7461dfa0fb5ec817"}, 356 | {file = "jiter-0.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d33f94615fcaf872f7fd8cd98ac3b429e435c77619777e8a449d9d27e01134d1"}, 357 | {file = "jiter-0.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:317b25e98a35ffec5c67efe56a4e9970852632c810d35b34ecdd70cc0e47b3b6"}, 358 | {file = "jiter-0.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc9043259ee430ecd71d178fccabd8c332a3bf1e81e50cae43cc2b28d19e4cb7"}, 359 | {file = "jiter-0.8.2-cp38-cp38-win32.whl", hash = "sha256:fc5adda618205bd4678b146612ce44c3cbfdee9697951f2c0ffdef1f26d72b63"}, 360 | {file = "jiter-0.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cd646c827b4f85ef4a78e4e58f4f5854fae0caf3db91b59f0d73731448a970c6"}, 361 | {file = "jiter-0.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e41e75344acef3fc59ba4765df29f107f309ca9e8eace5baacabd9217e52a5ee"}, 362 | {file = "jiter-0.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f22b16b35d5c1df9dfd58843ab2cd25e6bf15191f5a236bed177afade507bfc"}, 363 | {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7200b8f7619d36aa51c803fd52020a2dfbea36ffec1b5e22cab11fd34d95a6d"}, 364 | {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70bf4c43652cc294040dbb62256c83c8718370c8b93dd93d934b9a7bf6c4f53c"}, 365 | {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9d471356dc16f84ed48768b8ee79f29514295c7295cb41e1133ec0b2b8d637d"}, 366 | {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:859e8eb3507894093d01929e12e267f83b1d5f6221099d3ec976f0c995cb6bd9"}, 367 | {file = "jiter-0.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa58399c01db555346647a907b4ef6d4f584b123943be6ed5588c3f2359c9f4"}, 368 | {file = "jiter-0.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f2d5ed877f089862f4c7aacf3a542627c1496f972a34d0474ce85ee7d939c27"}, 369 | {file = "jiter-0.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:03c9df035d4f8d647f8c210ddc2ae0728387275340668fb30d2421e17d9a0841"}, 370 | {file = "jiter-0.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8bd2a824d08d8977bb2794ea2682f898ad3d8837932e3a74937e93d62ecbb637"}, 371 | {file = "jiter-0.8.2-cp39-cp39-win32.whl", hash = "sha256:ca29b6371ebc40e496995c94b988a101b9fbbed48a51190a4461fcb0a68b4a36"}, 372 | {file = "jiter-0.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1c0dfbd1be3cbefc7510102370d86e35d1d53e5a93d48519688b1bf0f761160a"}, 373 | {file = "jiter-0.8.2.tar.gz", hash = "sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d"}, 374 | ] 375 | 376 | [[package]] 377 | name = "oauthlib" 378 | version = "3.2.2" 379 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 380 | optional = false 381 | python-versions = ">=3.6" 382 | files = [ 383 | {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, 384 | {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, 385 | ] 386 | 387 | [package.extras] 388 | rsa = ["cryptography (>=3.0.0)"] 389 | signals = ["blinker (>=1.4.0)"] 390 | signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] 391 | 392 | [[package]] 393 | name = "openai" 394 | version = "1.58.1" 395 | description = "The official Python library for the openai API" 396 | optional = false 397 | python-versions = ">=3.8" 398 | files = [ 399 | {file = "openai-1.58.1-py3-none-any.whl", hash = "sha256:e2910b1170a6b7f88ef491ac3a42c387f08bd3db533411f7ee391d166571d63c"}, 400 | {file = "openai-1.58.1.tar.gz", hash = "sha256:f5a035fd01e141fc743f4b0e02c41ca49be8fab0866d3b67f5f29b4f4d3c0973"}, 401 | ] 402 | 403 | [package.dependencies] 404 | anyio = ">=3.5.0,<5" 405 | distro = ">=1.7.0,<2" 406 | httpx = ">=0.23.0,<1" 407 | jiter = ">=0.4.0,<1" 408 | pydantic = ">=1.9.0,<3" 409 | sniffio = "*" 410 | tqdm = ">4" 411 | typing-extensions = ">=4.11,<5" 412 | 413 | [package.extras] 414 | datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] 415 | realtime = ["websockets (>=13,<15)"] 416 | 417 | [[package]] 418 | name = "prompt-toolkit" 419 | version = "3.0.48" 420 | description = "Library for building powerful interactive command lines in Python" 421 | optional = false 422 | python-versions = ">=3.7.0" 423 | files = [ 424 | {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, 425 | {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, 426 | ] 427 | 428 | [package.dependencies] 429 | wcwidth = "*" 430 | 431 | [[package]] 432 | name = "pydantic" 433 | version = "2.10.3" 434 | description = "Data validation using Python type hints" 435 | optional = false 436 | python-versions = ">=3.8" 437 | files = [ 438 | {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, 439 | {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, 440 | ] 441 | 442 | [package.dependencies] 443 | annotated-types = ">=0.6.0" 444 | pydantic-core = "2.27.1" 445 | typing-extensions = ">=4.12.2" 446 | 447 | [package.extras] 448 | email = ["email-validator (>=2.0.0)"] 449 | timezone = ["tzdata"] 450 | 451 | [[package]] 452 | name = "pydantic-core" 453 | version = "2.27.1" 454 | description = "Core functionality for Pydantic validation and serialization" 455 | optional = false 456 | python-versions = ">=3.8" 457 | files = [ 458 | {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, 459 | {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, 460 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, 461 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, 462 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, 463 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, 464 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, 465 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, 466 | {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, 467 | {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, 468 | {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, 469 | {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, 470 | {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, 471 | {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, 472 | {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, 473 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, 474 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, 475 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, 476 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, 477 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, 478 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, 479 | {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, 480 | {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, 481 | {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, 482 | {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, 483 | {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, 484 | {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, 485 | {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, 486 | {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, 487 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, 488 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, 489 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, 490 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, 491 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, 492 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, 493 | {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, 494 | {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, 495 | {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, 496 | {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, 497 | {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, 498 | {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, 499 | {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, 500 | {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, 501 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, 502 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, 503 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, 504 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, 505 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, 506 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, 507 | {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, 508 | {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, 509 | {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, 510 | {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, 511 | {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, 512 | {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, 513 | {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, 514 | {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, 515 | {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, 516 | {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, 517 | {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, 518 | {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, 519 | {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, 520 | {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, 521 | {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, 522 | {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, 523 | {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, 524 | {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, 525 | {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, 526 | {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, 527 | {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, 528 | {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, 529 | {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, 530 | {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, 531 | {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, 532 | {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, 533 | {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, 534 | {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, 535 | {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, 536 | {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, 537 | {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, 538 | {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, 539 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, 540 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, 541 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, 542 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, 543 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, 544 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, 545 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, 546 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, 547 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, 548 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, 549 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, 550 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, 551 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, 552 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, 553 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, 554 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, 555 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, 556 | {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, 557 | {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, 558 | ] 559 | 560 | [package.dependencies] 561 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 562 | 563 | [[package]] 564 | name = "python-dotenv" 565 | version = "1.0.1" 566 | description = "Read key-value pairs from a .env file and set them as environment variables" 567 | optional = false 568 | python-versions = ">=3.8" 569 | files = [ 570 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 571 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 572 | ] 573 | 574 | [package.extras] 575 | cli = ["click (>=5.0)"] 576 | 577 | [[package]] 578 | name = "requests" 579 | version = "2.32.3" 580 | description = "Python HTTP for Humans." 581 | optional = false 582 | python-versions = ">=3.8" 583 | files = [ 584 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 585 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 586 | ] 587 | 588 | [package.dependencies] 589 | certifi = ">=2017.4.17" 590 | charset-normalizer = ">=2,<4" 591 | idna = ">=2.5,<4" 592 | urllib3 = ">=1.21.1,<3" 593 | 594 | [package.extras] 595 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 596 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 597 | 598 | [[package]] 599 | name = "requests-oauthlib" 600 | version = "1.3.1" 601 | description = "OAuthlib authentication support for Requests." 602 | optional = false 603 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 604 | files = [ 605 | {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, 606 | {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, 607 | ] 608 | 609 | [package.dependencies] 610 | oauthlib = ">=3.0.0" 611 | requests = ">=2.0.0" 612 | 613 | [package.extras] 614 | rsa = ["oauthlib[signedtoken] (>=3.0.0)"] 615 | 616 | [[package]] 617 | name = "sniffio" 618 | version = "1.3.1" 619 | description = "Sniff out which async library your code is running under" 620 | optional = false 621 | python-versions = ">=3.7" 622 | files = [ 623 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 624 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 625 | ] 626 | 627 | [[package]] 628 | name = "tqdm" 629 | version = "4.67.1" 630 | description = "Fast, Extensible Progress Meter" 631 | optional = false 632 | python-versions = ">=3.7" 633 | files = [ 634 | {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, 635 | {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, 636 | ] 637 | 638 | [package.dependencies] 639 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 640 | 641 | [package.extras] 642 | dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] 643 | discord = ["requests"] 644 | notebook = ["ipywidgets (>=6)"] 645 | slack = ["slack-sdk"] 646 | telegram = ["requests"] 647 | 648 | [[package]] 649 | name = "tweepy" 650 | version = "4.14.0" 651 | description = "Twitter library for Python" 652 | optional = false 653 | python-versions = ">=3.7" 654 | files = [ 655 | {file = "tweepy-4.14.0-py3-none-any.whl", hash = "sha256:db6d3844ccc0c6d27f339f12ba8acc89912a961da513c1ae50fa2be502a56afb"}, 656 | {file = "tweepy-4.14.0.tar.gz", hash = "sha256:1f9f1707d6972de6cff6c5fd90dfe6a449cd2e0d70bd40043ffab01e07a06c8c"}, 657 | ] 658 | 659 | [package.dependencies] 660 | oauthlib = ">=3.2.0,<4" 661 | requests = ">=2.27.0,<3" 662 | requests-oauthlib = ">=1.2.0,<2" 663 | 664 | [package.extras] 665 | async = ["aiohttp (>=3.7.3,<4)", "async-lru (>=1.0.3,<3)"] 666 | dev = ["coverage (>=4.4.2)", "coveralls (>=2.1.0)", "tox (>=3.21.0)"] 667 | docs = ["myst-parser (==0.15.2)", "readthedocs-sphinx-search (==0.1.1)", "sphinx (==4.2.0)", "sphinx-hoverxref (==0.7b1)", "sphinx-rtd-theme (==1.0.0)", "sphinx-tabs (==3.2.0)"] 668 | socks = ["requests[socks] (>=2.27.0,<3)"] 669 | test = ["vcrpy (>=1.10.3)"] 670 | 671 | [[package]] 672 | name = "typing-extensions" 673 | version = "4.12.2" 674 | description = "Backported and Experimental Type Hints for Python 3.8+" 675 | optional = false 676 | python-versions = ">=3.8" 677 | files = [ 678 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 679 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 680 | ] 681 | 682 | [[package]] 683 | name = "urllib3" 684 | version = "2.2.3" 685 | description = "HTTP library with thread-safe connection pooling, file post, and more." 686 | optional = false 687 | python-versions = ">=3.8" 688 | files = [ 689 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 690 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 691 | ] 692 | 693 | [package.extras] 694 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 695 | h2 = ["h2 (>=4,<5)"] 696 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 697 | zstd = ["zstandard (>=0.18.0)"] 698 | 699 | [[package]] 700 | name = "wcwidth" 701 | version = "0.2.13" 702 | description = "Measures the displayed width of unicode strings in a terminal" 703 | optional = false 704 | python-versions = "*" 705 | files = [ 706 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 707 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 708 | ] 709 | 710 | [metadata] 711 | lock-version = "2.0" 712 | python-versions = "^3.10" 713 | content-hash = "00d5f8a53dcf61aa2173352bcae9388f64ce401c4f7962fc2627b1ed7d8aee5a" 714 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ZerePy" 3 | version = "0.1.0" 4 | description = "A launch-pad for AI agents" 5 | authors = ["Ayoub Ed ", "Efrain ", "Max "] 6 | license = "MIT License" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10" 11 | python-dotenv = "^1.0.1" 12 | openai = "^1.57.2" 13 | tweepy = "^4.14.0" 14 | prompt-toolkit = "^3.0.48" 15 | anthropic = "^0.42.0" 16 | 17 | 18 | [build-system] 19 | requires = ["poetry-core"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /replit.nix: -------------------------------------------------------------------------------- 1 | {pkgs}: { 2 | deps = [ 3 | pkgs.vim 4 | pkgs.glibcLocales 5 | pkgs.libxcrypt 6 | pkgs.cacert 7 | ]; 8 | } 9 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moondevonyt/ZerePy/a67f7c0681483f075794ce80831928e293efb17e/src/__init__.py -------------------------------------------------------------------------------- /src/agent.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import time 4 | import logging 5 | import os 6 | from pathlib import Path 7 | from dotenv import load_dotenv 8 | from src.connection_manager import ConnectionManager 9 | from src.helpers import print_h_bar 10 | 11 | REQUIRED_FIELDS = ["name", "bio", "traits", "examples", "loop_delay", "config", "tasks"] 12 | 13 | logger = logging.getLogger("agent") 14 | 15 | class ZerePyAgent: 16 | def __init__( 17 | self, 18 | agent_name: str 19 | ): 20 | try: 21 | agent_path = Path("agents") / f"{agent_name}.json" 22 | agent_dict = json.load(open(agent_path, "r")) 23 | 24 | missing_fields = [field for field in REQUIRED_FIELDS if field not in agent_dict] 25 | if missing_fields: 26 | raise KeyError(f"Missing required fields: {', '.join(missing_fields)}") 27 | 28 | self.name = agent_dict["name"] 29 | self.bio = agent_dict["bio"] 30 | self.traits = agent_dict["traits"] 31 | self.examples = agent_dict["examples"] 32 | self.loop_delay = agent_dict["loop_delay"] 33 | self.connection_manager = ConnectionManager(agent_dict["config"]) 34 | 35 | # Extract Twitter config 36 | twitter_config = next((config for config in agent_dict["config"] if config["name"] == "twitter"), None) 37 | if not twitter_config: 38 | raise KeyError("Twitter configuration is required") 39 | 40 | # TODO: These should probably live in the related task parameters 41 | self.tweet_interval = twitter_config.get("tweet_interval", 900) 42 | self.own_tweet_replies_count = twitter_config.get("own_tweet_replies_count", 2) 43 | 44 | self.is_llm_set = False 45 | 46 | # Cache for system prompt 47 | self._system_prompt = None 48 | 49 | # Extract loop tasks 50 | self.tasks = agent_dict.get("tasks", []) 51 | self.task_weights = [task.get("weight", 0) for task in self.tasks] 52 | 53 | # Set up empty agent state 54 | self.state = {} 55 | 56 | except Exception as e: 57 | logger.error("Could not load ZerePy agent") 58 | raise e 59 | 60 | def _setup_llm_provider(self): 61 | # Get first available LLM provider and its model 62 | llm_providers = self.connection_manager.get_model_providers() 63 | if not llm_providers: 64 | raise ValueError("No configured LLM provider found") 65 | self.model_provider = llm_providers[0] 66 | 67 | # Load Twitter username for self-reply detection 68 | load_dotenv() 69 | self.username = os.getenv('TWITTER_USERNAME', '').lower() 70 | if not self.username: 71 | raise ValueError("Twitter username is required") 72 | 73 | def _construct_system_prompt(self) -> str: 74 | """Construct the system prompt from agent configuration""" 75 | if self._system_prompt is None: 76 | prompt_parts = [] 77 | prompt_parts.extend(self.bio) 78 | 79 | if self.traits: 80 | prompt_parts.append("\nYour key traits are:") 81 | prompt_parts.extend(f"- {trait}" for trait in self.traits) 82 | 83 | if self.examples: 84 | prompt_parts.append("\nHere are some examples of your style (Please avoid repeating any of these):") 85 | prompt_parts.extend(f"- {example}" for example in self.examples) 86 | 87 | self._system_prompt = "\n".join(prompt_parts) 88 | 89 | return self._system_prompt 90 | 91 | def prompt_llm(self, prompt: str, system_prompt: str = None) -> str: 92 | """Generate text using the configured LLM provider""" 93 | system_prompt = system_prompt or self._construct_system_prompt() 94 | 95 | return self.connection_manager.perform_action( 96 | connection_name=self.model_provider, 97 | action_name="generate-text", 98 | params=[prompt, system_prompt] 99 | ) 100 | 101 | def perform_action(self, connection: str, action: str, **kwargs) -> None: 102 | return self.connection_manager.perform_action(connection, action, **kwargs) 103 | 104 | def loop(self): 105 | """Main agent loop for autonomous behavior""" 106 | if not self.is_llm_set: 107 | self._setup_llm_provider() 108 | 109 | logger.info("\n🚀 Starting agent loop...") 110 | logger.info("Press Ctrl+C at any time to stop the loop.") 111 | print_h_bar() 112 | 113 | time.sleep(2) 114 | logger.info("Starting loop in 5 seconds...") 115 | for i in range(5, 0, -1): 116 | logger.info(f"{i}...") 117 | time.sleep(1) 118 | 119 | last_tweet_time = 0 120 | 121 | try: 122 | while True: 123 | success = False 124 | try: 125 | # REPLENISH INPUTS 126 | # TODO: Add more inputs to complexify agent behavior 127 | if "timeline_tweets" not in self.state or len(self.state["timeline_tweets"]) == 0: 128 | logger.info("\n👀 READING TIMELINE") 129 | self.state["timeline_tweets"] = self.connection_manager.perform_action( 130 | connection_name="twitter", 131 | action_name="read-timeline", 132 | params=[] 133 | ) 134 | 135 | # CHOOSE AN ACTION 136 | # TODO: Add agentic action selection 137 | action = random.choices(self.tasks, weights=self.task_weights, k=1)[0] 138 | action_name = action["name"] 139 | 140 | # PERFORM ACTION 141 | if action_name == "post-tweet": 142 | # Check if it's time to post a new tweet 143 | current_time = time.time() 144 | if current_time - last_tweet_time >= self.tweet_interval: 145 | logger.info("\n📝 GENERATING NEW TWEET") 146 | print_h_bar() 147 | 148 | prompt = ("Generate an engaging tweet. Don't include any hashtags, links or emojis. Keep it under 280 characters." 149 | f"The tweets should be pure commentary, do not shill any coins or projects apart from {self.name}. Do not repeat any of the" 150 | "tweets that were given as example. Avoid the words AI and crypto.") 151 | tweet_text = self.prompt_llm(prompt) 152 | 153 | if tweet_text: 154 | logger.info("\n🚀 Posting tweet:") 155 | logger.info(f"'{tweet_text}'") 156 | self.connection_manager.perform_action( 157 | connection_name="twitter", 158 | action_name="post-tweet", 159 | params=[tweet_text] 160 | ) 161 | last_tweet_time = current_time 162 | success = True 163 | logger.info("\n✅ Tweet posted successfully!") 164 | else: 165 | logger.info("\n👀 Delaying post until tweet interval elapses...") 166 | print_h_bar() 167 | continue 168 | 169 | elif action_name == "reply-to-tweet": 170 | if "timeline_tweets" in self.state and len(self.state["timeline_tweets"]) > 0: 171 | # Get next tweet from inputs 172 | tweet = self.state["timeline_tweets"].pop(0) 173 | tweet_id = tweet.get('id') 174 | if not tweet_id: 175 | continue 176 | 177 | # Check if it's our own tweet using username 178 | is_own_tweet = tweet.get('author_username', '').lower() == self.username 179 | if is_own_tweet: 180 | # pick one of the replies to reply to 181 | replies = self.connection_manager.perform_action( 182 | connection_name="twitter", 183 | action_name="get-tweet-replies", 184 | params=[tweet.get('author_id')] 185 | ) 186 | if replies: 187 | self.state["timeline_tweets"].extend(replies[:self.own_tweet_replies_count]) 188 | continue 189 | 190 | logger.info(f"\n💬 GENERATING REPLY to: {tweet.get('text', '')[:50]}...") 191 | 192 | # Customize prompt based on whether it's a self-reply 193 | base_prompt = (f"Generate a friendly, engaging reply to this tweet: {tweet.get('text')}. Keep it under 280 characters. Don't include any usernames, hashtags, links or emojis. " 194 | f"The tweets should be pure commentary, do not shill any coins or projects apart from {self.name}. Do not repeat any of the" 195 | "tweets that were given as example. Avoid the words AI and crypto.") 196 | 197 | system_prompt = self._construct_system_prompt() 198 | reply_text = self.prompt_llm(prompt=base_prompt, system_prompt=system_prompt) 199 | 200 | if reply_text: 201 | logger.info(f"\n🚀 Posting reply: '{reply_text}'") 202 | self.connection_manager.perform_action( 203 | connection_name="twitter", 204 | action_name="reply-to-tweet", 205 | params=[tweet_id, reply_text] 206 | ) 207 | success = True 208 | logger.info("✅ Reply posted successfully!") 209 | 210 | elif action_name == "like-tweet": 211 | if "timeline_tweets" in self.state and len(self.state["timeline_tweets"]) > 0: 212 | # Get next tweet from inputs 213 | tweet = self.state["timeline_tweets"].pop(0) 214 | tweet_id = tweet.get('id') 215 | if not tweet_id: 216 | continue 217 | 218 | logger.info(f"\n👍 LIKING TWEET: {tweet.get('text', '')[:50]}...") 219 | 220 | self.connection_manager.perform_action( 221 | connection_name="twitter", 222 | action_name="like-tweet", 223 | params=[tweet_id] 224 | ) 225 | success = True 226 | logger.info("✅ Tweet liked successfully!") 227 | 228 | 229 | logger.info(f"\n⏳ Waiting {self.loop_delay} seconds before next loop...") 230 | print_h_bar() 231 | time.sleep(self.loop_delay if success else 60) 232 | 233 | except Exception as e: 234 | logger.error(f"\n❌ Error in agent loop iteration: {e}") 235 | logger.info(f"⏳ Waiting {self.loop_delay} seconds before retrying...") 236 | time.sleep(self.loop_delay) 237 | 238 | except KeyboardInterrupt: 239 | logger.info("\n🛑 Agent loop stopped by user.") 240 | return 241 | -------------------------------------------------------------------------------- /src/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import Callable, Dict, List 6 | from pathlib import Path 7 | from prompt_toolkit import PromptSession 8 | from prompt_toolkit.completion import WordCompleter 9 | from prompt_toolkit.styles import Style 10 | from prompt_toolkit.formatted_text import HTML 11 | from prompt_toolkit.history import FileHistory 12 | from src.agent import ZerePyAgent 13 | from src.helpers import print_h_bar 14 | 15 | # Configure logging 16 | logging.basicConfig(level=logging.INFO, format='%(message)s') 17 | logger = logging.getLogger(__name__) 18 | 19 | @dataclass 20 | class Command: 21 | """Dataclass to represent a CLI command""" 22 | name: str 23 | description: str 24 | tips: List[str] 25 | handler: Callable 26 | aliases: List[str] = None 27 | 28 | def __post_init__(self): 29 | if self.aliases is None: 30 | self.aliases = [] 31 | 32 | class ZerePyCLI: 33 | def __init__(self): 34 | self.agent = None 35 | 36 | # Create config directory if it doesn't exist 37 | self.config_dir = Path.home() / '.zerepy' 38 | self.config_dir.mkdir(exist_ok=True) 39 | 40 | # Initialize command registry 41 | self._initialize_commands() 42 | 43 | # Setup prompt toolkit components 44 | self._setup_prompt_toolkit() 45 | 46 | def _initialize_commands(self) -> None: 47 | """Initialize all CLI commands""" 48 | self.commands: Dict[str, Command] = {} 49 | 50 | # Help command 51 | self._register_command( 52 | Command( 53 | name="help", 54 | description="Displays a list of all available commands, or help for a specific command.", 55 | tips=["Try 'help' to see available commands.", 56 | "Try 'help {command}' to get more information about a specific command."], 57 | handler=self.help, 58 | aliases=['h', '?'] 59 | ) 60 | ) 61 | 62 | ################## AGENTS ################## 63 | # Agent action command 64 | self._register_command( 65 | Command( 66 | name="agent-action", 67 | description="Runs a single agent action.", 68 | tips=["Format: agent-action {connection} {action}", 69 | "Use 'list-connections' to see available connections.", 70 | "Use 'list-actions' to see available actions."], 71 | handler=self.agent_action, 72 | aliases=['action', 'run'] 73 | ) 74 | ) 75 | 76 | # Agent loop command 77 | self._register_command( 78 | Command( 79 | name="agent-loop", 80 | description="Starts the current agent's autonomous behavior loop.", 81 | tips=["Press Ctrl+C to stop the loop"], 82 | handler=self.agent_loop, 83 | aliases=['loop', 'start'] 84 | ) 85 | ) 86 | 87 | # List agents command 88 | self._register_command( 89 | Command( 90 | name="list-agents", 91 | description="Lists all available agents you have on file.", 92 | tips=["Agents are stored in the 'agents' directory", 93 | "Use 'load-agent' to load an available agent"], 94 | handler=self.list_agents, 95 | aliases=['agents', 'ls-agents'] 96 | ) 97 | ) 98 | 99 | # Load agent command 100 | self._register_command( 101 | Command( 102 | name="load-agent", 103 | description="Loads an agent from a file.", 104 | tips=["Format: load-agent {agent_name}", 105 | "Use 'list-agents' to see available agents"], 106 | handler=self.load_agent, 107 | aliases=['load'] 108 | ) 109 | ) 110 | 111 | # Create agent command 112 | self._register_command( 113 | Command( 114 | name="create-agent", 115 | description="Creates a new agent.", 116 | tips=["Follow the interactive wizard to create a new agent"], 117 | handler=self.create_agent, 118 | aliases=['new-agent', 'create'] 119 | ) 120 | ) 121 | 122 | # Define default agent 123 | self._register_command( 124 | Command( 125 | name="set-default-agent", 126 | description="Define which model is loaded when the CLI starts.", 127 | tips=["You can also just change the 'default_agent' field in agents/general.json"], 128 | handler=self.set_default_agent, 129 | aliases=['default'] 130 | ) 131 | ) 132 | 133 | # Chat command 134 | self._register_command( 135 | Command( 136 | name="chat", 137 | description="Start a chat session with the current agent", 138 | tips=["Use 'exit' to end the chat session"], 139 | handler=self.chat_session, 140 | aliases=['talk'] 141 | ) 142 | ) 143 | 144 | ################## CONNECTIONS ################## 145 | # List actions command 146 | self._register_command( 147 | Command( 148 | name="list-actions", 149 | description="Lists all available actions for the given connection.", 150 | tips=["Format: list-actions {connection}", 151 | "Use 'list-connections' to see available connections"], 152 | handler=self.list_actions, 153 | aliases=['actions', 'ls-actions'] 154 | ) 155 | ) 156 | 157 | # Configure connection command 158 | self._register_command( 159 | Command( 160 | name="configure-connection", 161 | description="Sets up a connection for API access.", 162 | tips=["Format: configure-connection {connection}", 163 | "Follow the prompts to enter necessary credentials"], 164 | handler=self.configure_connection, 165 | aliases=['config', 'setup'] 166 | ) 167 | ) 168 | 169 | # List connections command 170 | self._register_command( 171 | Command( 172 | name="list-connections", 173 | description="Lists all available connections.", 174 | tips=["Shows both configured and unconfigured connections"], 175 | handler=self.list_connections, 176 | aliases=['connections', 'ls-connections'] 177 | ) 178 | ) 179 | 180 | ################## MISC ################## 181 | # Exit command 182 | self._register_command( 183 | Command( 184 | name="exit", 185 | description="Exits the ZerePy CLI.", 186 | tips=["You can also use Ctrl+D to exit"], 187 | handler=self.exit, 188 | aliases=['quit', 'q'] 189 | ) 190 | ) 191 | 192 | def _setup_prompt_toolkit(self) -> None: 193 | """Setup prompt toolkit components""" 194 | self.style = Style.from_dict({ 195 | 'prompt': 'ansicyan bold', 196 | 'command': 'ansigreen', 197 | 'error': 'ansired bold', 198 | 'success': 'ansigreen bold', 199 | 'warning': 'ansiyellow', 200 | }) 201 | 202 | # Use FileHistory for persistent command history 203 | history_file = self.config_dir / 'history.txt' 204 | 205 | self.completer = WordCompleter( 206 | list(self.commands.keys()), 207 | ignore_case=True, 208 | sentence=True 209 | ) 210 | 211 | self.session = PromptSession( 212 | completer=self.completer, 213 | style=self.style, 214 | history=FileHistory(str(history_file)) 215 | ) 216 | 217 | ################### 218 | # Helper Functions 219 | ################### 220 | def _register_command(self, command: Command) -> None: 221 | """Register a command and its aliases""" 222 | self.commands[command.name] = command 223 | for alias in command.aliases: 224 | self.commands[alias] = command 225 | 226 | def _get_prompt_message(self) -> HTML: 227 | """Generate the prompt message based on current state""" 228 | agent_status = f"({self.agent.name})" if self.agent else "(no agent)" 229 | return HTML(f'ZerePy-CLI {agent_status} > ') 230 | 231 | def _handle_command(self, input_string: str) -> None: 232 | """Parse and handle a command input""" 233 | input_list = input_string.split() 234 | command_string = input_list[0].lower() 235 | 236 | try: 237 | command = self.commands.get(command_string) 238 | if command: 239 | command.handler(input_list) 240 | else: 241 | self._handle_unknown_command(command_string) 242 | except Exception as e: 243 | logger.error(f"Error executing command: {e}") 244 | 245 | def _handle_unknown_command(self, command: str) -> None: 246 | """Handle unknown command with suggestions""" 247 | logger.warning(f"Unknown command: '{command}'") 248 | 249 | # Suggest similar commands using basic string similarity 250 | suggestions = self._get_command_suggestions(command) 251 | if suggestions: 252 | logger.info("Did you mean one of these?") 253 | for suggestion in suggestions: 254 | logger.info(f" - {suggestion}") 255 | logger.info("Use 'help' to see all available commands.") 256 | 257 | def _get_command_suggestions(self, command: str, max_suggestions: int = 3) -> List[str]: 258 | """Get command suggestions based on string similarity""" 259 | from difflib import get_close_matches 260 | return get_close_matches(command, self.commands.keys(), n=max_suggestions, cutoff=0.6) 261 | 262 | def _print_welcome_message(self) -> None: 263 | """Print welcome message and initial status""" 264 | print_h_bar() 265 | logger.info("👋 Welcome to the ZerePy CLI!") 266 | logger.info("Type 'help' for a list of commands.") 267 | print_h_bar() 268 | 269 | def _show_command_help(self, command_name: str) -> None: 270 | """Show help for a specific command""" 271 | command = self.commands.get(command_name) 272 | if not command: 273 | logger.warning(f"Unknown command: '{command_name}'") 274 | suggestions = self._get_command_suggestions(command_name) 275 | if suggestions: 276 | logger.info("Did you mean one of these?") 277 | for suggestion in suggestions: 278 | logger.info(f" - {suggestion}") 279 | return 280 | 281 | logger.info(f"\nHelp for '{command.name}':") 282 | logger.info(f"Description: {command.description}") 283 | 284 | if command.aliases: 285 | logger.info(f"Aliases: {', '.join(command.aliases)}") 286 | 287 | if command.tips: 288 | logger.info("\nTips:") 289 | for tip in command.tips: 290 | logger.info(f" - {tip}") 291 | 292 | def _show_general_help(self) -> None: 293 | """Show general help information""" 294 | logger.info("\nAvailable Commands:") 295 | # Group commands by first letter for better organization 296 | commands_by_letter = {} 297 | for cmd_name, cmd in self.commands.items(): 298 | # Only show main commands, not aliases 299 | if cmd_name == cmd.name: 300 | first_letter = cmd_name[0].upper() 301 | if first_letter not in commands_by_letter: 302 | commands_by_letter[first_letter] = [] 303 | commands_by_letter[first_letter].append(cmd) 304 | 305 | for letter in sorted(commands_by_letter.keys()): 306 | logger.info(f"\n{letter}:") 307 | for cmd in sorted(commands_by_letter[letter], key=lambda x: x.name): 308 | logger.info(f" {cmd.name:<15} - {cmd.description}") 309 | 310 | def _list_loaded_agent(self) -> None: 311 | if self.agent: 312 | logger.info(f"\nStart the agent loop with the command 'start' or use one of the action commands.") 313 | else: 314 | logger.info(f"\nNo default agent is loaded, please use the load-agent command to do that.") 315 | 316 | def _load_agent_from_file(self, agent_name): 317 | try: 318 | self.agent = ZerePyAgent(agent_name) 319 | logger.info(f"\n✅ Successfully loaded agent: {self.agent.name}") 320 | except FileNotFoundError: 321 | logger.error(f"Agent file not found: {agent_name}") 322 | logger.info("Use 'list-agents' to see available agents.") 323 | except KeyError as e: 324 | logger.error(f"Invalid agent file: {e}") 325 | except Exception as e: 326 | logger.error(f"Error loading agent: {e}") 327 | 328 | def _load_default_agent(self) -> None: 329 | """Load users default agent""" 330 | agent_general_config_path = Path("agents") / "general.json" 331 | file = None 332 | try: 333 | file = open(agent_general_config_path, 'r') 334 | data = json.load(file) 335 | if not data.get('default_agent'): 336 | logger.error('No default agent defined, please set one in general.json') 337 | return 338 | 339 | self._load_agent_from_file(data.get('default_agent')) 340 | except FileNotFoundError: 341 | logger.error("File general.json not found, please create one.") 342 | return 343 | except json.JSONDecodeError: 344 | logger.error("File agents/general.json contains Invalid JSON format") 345 | return 346 | finally: 347 | if file: 348 | file.close() 349 | 350 | ################### 351 | # Command functions 352 | ################### 353 | def help(self, input_list: List[str]) -> None: 354 | """List all commands supported by the CLI""" 355 | if len(input_list) > 1: 356 | self._show_command_help(input_list[1]) 357 | else: 358 | self._show_general_help() 359 | 360 | def agent_action(self, input_list: List[str]) -> None: 361 | """Handle agent action command""" 362 | if self.agent is None: 363 | logger.info("No agent is currently loaded. Use 'load-agent' to load an agent.") 364 | return 365 | 366 | if len(input_list) < 3: 367 | logger.info("Please specify both a connection and an action.") 368 | logger.info("Format: agent-action {connection} {action}") 369 | return 370 | 371 | try: 372 | result = self.agent.perform_action( 373 | connection=input_list[1], 374 | action=input_list[2], 375 | params=input_list[3:] 376 | ) 377 | logger.info(f"Result: {result}") 378 | except Exception as e: 379 | logger.error(f"Error running action: {e}") 380 | 381 | def agent_loop(self, input_list: List[str]) -> None: 382 | """Handle agent loop command""" 383 | if self.agent is None: 384 | logger.info("No agent is currently loaded. Use 'load-agent' to load an agent.") 385 | return 386 | 387 | try: 388 | self.agent.loop() 389 | except KeyboardInterrupt: 390 | logger.info("\n🛑 Agent loop stopped by user.") 391 | except Exception as e: 392 | logger.error(f"Error in agent loop: {e}") 393 | 394 | def list_agents(self, input_list: List[str]) -> None: 395 | """Handle list agents command""" 396 | logger.info("\nAvailable Agents:") 397 | agents_dir = Path("agents") 398 | if not agents_dir.exists(): 399 | logger.info("No agents directory found.") 400 | return 401 | 402 | agents = list(agents_dir.glob("*.json")) 403 | if not agents: 404 | logger.info("No agents found. Use 'create-agent' to create a new agent.") 405 | return 406 | 407 | for agent_file in sorted(agents): 408 | if agent_file.stem == "general": 409 | continue 410 | logger.info(f"- {agent_file.stem}") 411 | 412 | def load_agent(self, input_list: List[str]) -> None: 413 | """Handle load agent command""" 414 | if len(input_list) < 2: 415 | logger.info("Please specify an agent name.") 416 | logger.info("Format: load-agent {agent_name}") 417 | logger.info("Use 'list-agents' to see available agents.") 418 | return 419 | 420 | self._load_agent_from_file(agent_name=input_list[1]) 421 | 422 | def create_agent(self, input_list: List[str]) -> None: 423 | """Handle create agent command""" 424 | logger.info("\nℹ️ Agent creation wizard not implemented yet.") 425 | logger.info("Please create agent JSON files manually in the 'agents' directory.") 426 | 427 | def set_default_agent(self, input_list: List[str]): 428 | """Handle set-default-agent command""" 429 | if len(input_list) < 2: 430 | logger.info("Please specify the same of the agent file.") 431 | return 432 | 433 | agent_general_config_path = Path("agents") / "general.json" 434 | file = None 435 | try: 436 | file = open(agent_general_config_path, 'r') 437 | data = json.load(file) 438 | agent_file_name = input_list[1] 439 | # if file does not exist, refuse to set it as default 440 | try: 441 | agent_path = Path("agents") / f"{agent_file_name}.json" 442 | open(agent_path, 'r') 443 | except FileNotFoundError: 444 | logging.error("Agent file not found.") 445 | return 446 | 447 | data['default_agent'] = input_list[1] 448 | with open(agent_general_config_path, 'w') as f: 449 | json.dump(data, f, indent=4) 450 | logger.info(f"Agent {agent_file_name} is now set as default.") 451 | except FileNotFoundError: 452 | logger.error("File not found") 453 | return 454 | except json.JSONDecodeError: 455 | logger.error("Invalid JSON format") 456 | return 457 | finally: 458 | if file: 459 | file.close() 460 | 461 | def list_actions(self, input_list: List[str]) -> None: 462 | """Handle list actions command""" 463 | if len(input_list) < 2: 464 | logger.info("\nPlease specify a connection.") 465 | logger.info("Format: list-actions {connection}") 466 | logger.info("Use 'list-connections' to see available connections.") 467 | return 468 | 469 | self.agent.connection_manager.list_actions(connection_name=input_list[1]) 470 | 471 | def configure_connection(self, input_list: List[str]) -> None: 472 | """Handle configure connection command""" 473 | if len(input_list) < 2: 474 | logger.info("\nPlease specify a connection to configure.") 475 | logger.info("Format: configure-connection {connection}") 476 | logger.info("Use 'list-connections' to see available connections.") 477 | return 478 | 479 | self.agent.connection_manager.configure_connection(connection_name=input_list[1]) 480 | 481 | def list_connections(self, input_list: List[str] = []) -> None: 482 | """Handle list connections command""" 483 | if self.agent: 484 | self.agent.connection_manager.list_connections() 485 | else: 486 | logging.info("Please load an agent to see the list of supported actions") 487 | 488 | def chat_session(self, input_list: List[str]) -> None: 489 | """Handle chat command""" 490 | if self.agent is None: 491 | logger.info("No agent loaded. Use 'load-agent' first.") 492 | return 493 | 494 | if not self.agent.is_llm_set: 495 | self.agent._setup_llm_provider() 496 | 497 | logger.info(f"\nStarting chat with {self.agent.name}") 498 | print_h_bar() 499 | 500 | while True: 501 | try: 502 | user_input = self.session.prompt("\nYou: ").strip() 503 | if user_input.lower() == 'exit': 504 | break 505 | 506 | response = self.agent.prompt_llm(user_input) 507 | logger.info(f"\n{self.agent.name}: {response}") 508 | print_h_bar() 509 | 510 | except KeyboardInterrupt: 511 | break 512 | 513 | def exit(self, input_list: List[str]) -> None: 514 | """Exit the CLI gracefully""" 515 | logger.info("\nGoodbye! 👋") 516 | sys.exit(0) 517 | 518 | 519 | ################### 520 | # Main CLI Loop 521 | ################### 522 | def main_loop(self) -> None: 523 | """Main CLI loop""" 524 | self._print_welcome_message() 525 | self._load_default_agent() 526 | self._list_loaded_agent() 527 | self.list_connections() 528 | 529 | # Start CLI loop 530 | while True: 531 | try: 532 | input_string = self.session.prompt( 533 | self._get_prompt_message(), 534 | style=self.style 535 | ).strip() 536 | 537 | if not input_string: 538 | continue 539 | 540 | self._handle_command(input_string) 541 | print_h_bar() 542 | 543 | except KeyboardInterrupt: 544 | continue 545 | except EOFError: 546 | self.exit([]) 547 | except Exception as e: 548 | logger.exception(f"Unexpected error: {e}") -------------------------------------------------------------------------------- /src/connection_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, List, Optional, Type, Dict 3 | from src.connections.base_connection import BaseConnection 4 | from src.connections.anthropic_connection import AnthropicConnection 5 | from src.connections.openai_connection import OpenAIConnection 6 | from src.connections.twitter_connection import TwitterConnection 7 | 8 | logger = logging.getLogger("connection_manager") 9 | 10 | class ConnectionManager: 11 | def __init__(self, agent_config): 12 | self.connections : Dict[str, BaseConnection] = {} 13 | for config in agent_config: 14 | self._register_connection(config) 15 | 16 | @staticmethod 17 | def _class_name_to_type(class_name: str) -> Type[BaseConnection]: 18 | if class_name == "twitter": 19 | return TwitterConnection 20 | elif class_name == "anthropic": 21 | return AnthropicConnection 22 | elif class_name == "openai": 23 | return OpenAIConnection 24 | 25 | return None 26 | 27 | def _register_connection(self, config_dic: Dict[str, Any]) -> None: 28 | """ 29 | Create and register a new connection with configuration 30 | 31 | Args: 32 | name: Identifier for the connection 33 | connection_class: The connection class to instantiate 34 | config: Configuration dictionary for the connection 35 | """ 36 | try: 37 | name = config_dic["name"] 38 | connection_class = self._class_name_to_type(name) 39 | connection = connection_class(config_dic) 40 | self.connections[name] = connection 41 | except Exception as e: 42 | logging.error(f"Failed to initialize connection {name}: {e}") 43 | 44 | def _check_connection(self, connection_string: str)-> bool: 45 | try: 46 | connection = self.connections[connection_string] 47 | return connection.is_configured(verbose=True) 48 | except KeyError: 49 | logging.error("\nUnknown connection. Try 'list-connections' to see all supported connections.") 50 | return False 51 | except Exception as e: 52 | logging.error(f"\nAn error occurred: {e}") 53 | return False 54 | 55 | def configure_connection(self, connection_name: str) -> bool: 56 | """Configure a specific connection""" 57 | try: 58 | connection = self.connections[connection_name] 59 | success = connection.configure() 60 | 61 | if success: 62 | logging.info(f"\n✅ SUCCESSFULLY CONFIGURED CONNECTION: {connection_name}") 63 | else: 64 | logging.error(f"\n❌ ERROR CONFIGURING CONNECTION: {connection_name}") 65 | return success 66 | 67 | except KeyError: 68 | logging.error("\nUnknown connection. Try 'list-connections' to see all supported connections.") 69 | return False 70 | except Exception as e: 71 | logging.error(f"\nAn error occurred: {e}") 72 | return False 73 | 74 | def list_connections(self) -> None: 75 | """List all available connections and their status""" 76 | logging.info("\nAVAILABLE CONNECTIONS:") 77 | for name, connection in self.connections.items(): 78 | status = "✅ Configured" if connection.is_configured() else "❌ Not Configured" 79 | logging.info(f"- {name}: {status}") 80 | 81 | def list_actions(self, connection_name: str) -> None: 82 | """List all available actions for a specific connection""" 83 | try: 84 | connection = self.connections[connection_name] 85 | 86 | if connection.is_configured(): 87 | logging.info(f"\n✅ {connection_name} is configured. You can use any of its actions.") 88 | else: 89 | logging.info(f"\n❌ {connection_name} is not configured. You must configure a connection to use its actions.") 90 | 91 | logging.info("\nAVAILABLE ACTIONS:") 92 | for action_name, action in connection.actions.items(): 93 | logging.info(f"- {action_name}: {action.description}") 94 | logging.info(" Parameters:") 95 | for param in action.parameters: 96 | req = "required" if param.required else "optional" 97 | logging.info(f" - {param.name} ({req}): {param.description}") 98 | 99 | except KeyError: 100 | logging.error("\nUnknown connection. Try 'list-connections' to see all supported connections.") 101 | except Exception as e: 102 | logging.error(f"\nAn error occurred: {e}") 103 | 104 | def perform_action(self, connection_name: str, action_name: str, params: List[Any]) -> Optional[Any]: 105 | """Perform an action on a specific connection with given parameters""" 106 | try: 107 | connection = self.connections[connection_name] 108 | 109 | if not connection.is_configured(): 110 | logging.error(f"\nError: Connection '{connection_name}' is not configured") 111 | return None 112 | 113 | if action_name not in connection.actions: 114 | logging.error(f"\nError: Unknown action '{action_name}' for connection '{connection_name}'") 115 | return None 116 | 117 | action = connection.actions[action_name] 118 | 119 | # Count required parameters 120 | required_params_count = sum(1 for param in action.parameters if param.required) 121 | 122 | # Check if we have enough parameters 123 | if len(params) != required_params_count: 124 | param_names = [param.name for param in action.parameters if param.required] 125 | logging.error(f"\nError: Expected {required_params_count} required parameters for {action_name}: {', '.join(param_names)}") 126 | return None 127 | 128 | # Convert list of params to kwargs dictionary 129 | kwargs = {} 130 | param_index = 0 131 | for param in action.parameters: 132 | if param.required: 133 | kwargs[param.name] = params[param_index] 134 | param_index += 1 135 | 136 | return connection.perform_action(action_name, kwargs) 137 | 138 | except Exception as e: 139 | logging.error(f"\nAn error occurred while trying action {action_name} for {connection_name} connection: {e}") 140 | return None 141 | 142 | def get_model_providers(self) -> List[str]: 143 | """Get a list of all LLM provider connections""" 144 | return [ 145 | name for name, conn in self.connections.items() 146 | if conn.is_configured() and getattr(conn, 'is_llm_provider', lambda: False) 147 | ] -------------------------------------------------------------------------------- /src/connections/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moondevonyt/ZerePy/a67f7c0681483f075794ce80831928e293efb17e/src/connections/__init__.py -------------------------------------------------------------------------------- /src/connections/anthropic_connection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Dict, Any 4 | from dotenv import load_dotenv, set_key 5 | from anthropic import Anthropic, NotFoundError 6 | from src.connections.base_connection import BaseConnection, Action, ActionParameter 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class AnthropicConnectionError(Exception): 11 | """Base exception for Anthropic connection errors""" 12 | pass 13 | 14 | class AnthropicConfigurationError(AnthropicConnectionError): 15 | """Raised when there are configuration/credential issues""" 16 | pass 17 | 18 | class AnthropicAPIError(AnthropicConnectionError): 19 | """Raised when Anthropic API requests fail""" 20 | pass 21 | 22 | class AnthropicConnection(BaseConnection): 23 | def __init__(self, config: Dict[str, Any]): 24 | super().__init__(config) 25 | self._client = None 26 | 27 | @property 28 | def is_llm_provider(self) -> bool: 29 | return True 30 | 31 | def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]: 32 | """Validate Anthropic configuration from JSON""" 33 | required_fields = ["model"] 34 | missing_fields = [field for field in required_fields if field not in config] 35 | 36 | if missing_fields: 37 | raise ValueError(f"Missing required configuration fields: {', '.join(missing_fields)}") 38 | 39 | if not isinstance(config["model"], str): 40 | raise ValueError("model must be a string") 41 | 42 | return config 43 | 44 | def register_actions(self) -> None: 45 | """Register available Anthropic actions""" 46 | self.actions = { 47 | "generate-text": Action( 48 | name="generate-text", 49 | parameters=[ 50 | ActionParameter("prompt", True, str, "The input prompt for text generation"), 51 | ActionParameter("system_prompt", True, str, "System prompt to guide the model"), 52 | ActionParameter("model", False, str, "Model to use for generation") 53 | ], 54 | description="Generate text using Anthropic models" 55 | ), 56 | "check-model": Action( 57 | name="check-model", 58 | parameters=[ 59 | ActionParameter("model", True, str, "Model name to check availability") 60 | ], 61 | description="Check if a specific model is available" 62 | ), 63 | "list-models": Action( 64 | name="list-models", 65 | parameters=[], 66 | description="List all available Anthropic models" 67 | ) 68 | } 69 | 70 | def _get_client(self) -> Anthropic: 71 | """Get or create Anthropic client""" 72 | if not self._client: 73 | api_key = os.getenv("ANTHROPIC_API_KEY") 74 | if not api_key: 75 | raise AnthropicConfigurationError("Anthropic API key not found in environment") 76 | self._client = Anthropic(api_key=api_key) 77 | return self._client 78 | 79 | def configure(self) -> bool: 80 | """Sets up Anthropic API authentication""" 81 | print("\n🤖 ANTHROPIC API SETUP") 82 | 83 | if self.is_configured(): 84 | print("\nAnthropic API is already configured.") 85 | response = input("Do you want to reconfigure? (y/n): ") 86 | if response.lower() != 'y': 87 | return True 88 | 89 | print("\n📝 To get your Anthropic API credentials:") 90 | print("1. Go to https://console.anthropic.com/settings/keys") 91 | print("2. Create a new API key.") 92 | 93 | api_key = input("\nEnter your Anthropic API key: ") 94 | 95 | try: 96 | if not os.path.exists('.env'): 97 | with open('.env', 'w') as f: 98 | f.write('') 99 | 100 | set_key('.env', 'ANTHROPIC_API_KEY', api_key) 101 | 102 | # Validate the API key 103 | client = Anthropic(api_key=api_key) 104 | client.models.list() 105 | 106 | print("\n✅ Anthropic API configuration successfully saved!") 107 | print("Your API key has been stored in the .env file.") 108 | return True 109 | 110 | except Exception as e: 111 | logger.error(f"Configuration failed: {e}") 112 | return False 113 | 114 | def is_configured(self, verbose = False) -> bool: 115 | """Check if Anthropic API key is configured and valid""" 116 | try: 117 | load_dotenv() 118 | api_key = os.getenv('ANTHROPIC_API_KEY') 119 | if not api_key: 120 | return False 121 | 122 | client = Anthropic(api_key=api_key) 123 | client.models.list() 124 | return True 125 | 126 | except Exception as e: 127 | if verbose: 128 | logger.debug(f"Configuration check failed: {e}") 129 | return False 130 | 131 | def generate_text(self, prompt: str, system_prompt: str, model: str = None, **kwargs) -> str: 132 | """Generate text using Anthropic models""" 133 | try: 134 | client = self._get_client() 135 | 136 | # Use configured model if none provided 137 | if not model: 138 | model = self.config["model"] 139 | 140 | message = client.messages.create( 141 | model=model, 142 | max_tokens=1000, 143 | temperature=0, 144 | system=system_prompt, 145 | messages=[ 146 | { 147 | "role": "user", 148 | "content": [ 149 | { 150 | "type": "text", 151 | "text": prompt 152 | } 153 | ] 154 | } 155 | ] 156 | ) 157 | return message.content[0].text 158 | 159 | except Exception as e: 160 | raise AnthropicAPIError(f"Text generation failed: {e}") 161 | 162 | def check_model(self, model: str, **kwargs) -> bool: 163 | """Check if a specific model is available""" 164 | try: 165 | client = self._get_client() 166 | try: 167 | client.models.retrieve(model_id=model) 168 | return True 169 | except NotFoundError: 170 | logging.error("Model not found.") 171 | return False 172 | except Exception as e: 173 | raise AnthropicAPIError(f"Model check failed: {e}") 174 | 175 | except Exception as e: 176 | raise AnthropicAPIError(f"Model check failed: {e}") 177 | 178 | def list_models(self, **kwargs) -> None: 179 | """List all available Anthropic models""" 180 | try: 181 | client = self._get_client() 182 | response = client.models.list().data 183 | model_ids = [model.id for model in response] 184 | 185 | logger.info("\nCLAUDE MODELS:") 186 | for i, model in enumerate(model_ids): 187 | logger.info(f"{i+1}. {model}") 188 | 189 | except Exception as e: 190 | raise AnthropicAPIError(f"Listing models failed: {e}") 191 | 192 | def perform_action(self, action_name: str, kwargs) -> Any: 193 | """Execute a Twitter action with validation""" 194 | if action_name not in self.actions: 195 | raise KeyError(f"Unknown action: {action_name}") 196 | 197 | action = self.actions[action_name] 198 | errors = action.validate_params(kwargs) 199 | if errors: 200 | raise ValueError(f"Invalid parameters: {', '.join(errors)}") 201 | 202 | # Call the appropriate method based on action name 203 | method_name = action_name.replace('-', '_') 204 | method = getattr(self, method_name) 205 | return method(**kwargs) -------------------------------------------------------------------------------- /src/connections/base_connection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | from typing import Any, Dict, List, Callable 4 | from dataclasses import dataclass 5 | 6 | @dataclass 7 | class ActionParameter: 8 | name: str 9 | required: bool 10 | type: type 11 | description: str 12 | 13 | @dataclass 14 | class Action: 15 | name: str 16 | parameters: List[ActionParameter] 17 | description: str 18 | 19 | def validate_params(self, params: Dict[str, Any]) -> List[str]: 20 | errors = [] 21 | for param in self.parameters: 22 | if param.required and param.name not in params: 23 | errors.append(f"Missing required parameter: {param.name}") 24 | elif param.name in params: 25 | try: 26 | params[param.name] = param.type(params[param.name]) 27 | except ValueError: 28 | errors.append(f"Invalid type for {param.name}. Expected {param.type.__name__}") 29 | return errors 30 | 31 | class BaseConnection(ABC): 32 | def __init__(self, config): 33 | try: 34 | # Dictionary to store action name -> handler method mapping 35 | self.actions: Dict[str, Callable] = {} 36 | # Dictionary to store some essential configuration 37 | self.config = self.validate_config(config) 38 | # Register actions during initialization 39 | self.register_actions() 40 | except Exception as e: 41 | logging.error("Could not initialize the connection") 42 | raise e 43 | 44 | @property 45 | @abstractmethod 46 | def is_llm_provider(self): 47 | pass 48 | 49 | @abstractmethod 50 | def validate_config(self, config) -> Dict[str, Any]: 51 | """ 52 | Validate config from JSON 53 | 54 | Args: 55 | config: dictionary containing all the config values for that connection 56 | 57 | Returns: 58 | Dict[str, Any]: Returns the config if valid 59 | 60 | Raises: 61 | Error if the configuration is not valid 62 | """ 63 | 64 | @abstractmethod 65 | def configure(self, **kwargs) -> bool: 66 | """ 67 | Configure the connection with necessary credentials. 68 | 69 | Args: 70 | **kwargs: Configuration parameters 71 | 72 | Returns: 73 | bool: True if configuration was successful, False otherwise 74 | """ 75 | pass 76 | 77 | @abstractmethod 78 | def is_configured(self, verbose = False) -> bool: 79 | """ 80 | Check if the connection is properly configured and ready for use. 81 | 82 | Returns: 83 | bool: True if the connection is configured, False otherwise 84 | """ 85 | pass 86 | 87 | @abstractmethod 88 | def register_actions(self) -> None: 89 | """ 90 | Register all available actions for this connection. 91 | Should populate self.actions with action_name -> handler mappings. 92 | """ 93 | pass 94 | 95 | def perform_action(self, action_name: str, **kwargs) -> Any: 96 | """ 97 | Perform a registered action with the given parameters. 98 | 99 | Args: 100 | action_name: Name of the action to perform 101 | **kwargs: Parameters for the action 102 | 103 | Returns: 104 | Any: Result of the action 105 | 106 | Raises: 107 | KeyError: If the action is not registered 108 | ValueError: If the action parameters are invalid 109 | """ 110 | if action_name not in self.actions: 111 | raise KeyError(f"Unknown action: {action_name}") 112 | 113 | handler = self.actions[action_name] 114 | return handler(**kwargs) 115 | -------------------------------------------------------------------------------- /src/connections/openai_connection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Dict, Any 4 | from dotenv import load_dotenv, set_key 5 | from openai import OpenAI 6 | from src.connections.base_connection import BaseConnection, Action, ActionParameter 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class OpenAIConnectionError(Exception): 11 | """Base exception for OpenAI connection errors""" 12 | pass 13 | 14 | class OpenAIConfigurationError(OpenAIConnectionError): 15 | """Raised when there are configuration/credential issues""" 16 | pass 17 | 18 | class OpenAIAPIError(OpenAIConnectionError): 19 | """Raised when OpenAI API requests fail""" 20 | pass 21 | 22 | class OpenAIConnection(BaseConnection): 23 | def __init__(self, config: Dict[str, Any]): 24 | super().__init__(config) 25 | self._client = None 26 | 27 | @property 28 | def is_llm_provider(self) -> bool: 29 | return True 30 | 31 | def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]: 32 | """Validate OpenAI configuration from JSON""" 33 | required_fields = ["model"] 34 | missing_fields = [field for field in required_fields if field not in config] 35 | 36 | if missing_fields: 37 | raise ValueError(f"Missing required configuration fields: {', '.join(missing_fields)}") 38 | 39 | # Validate model exists (will be checked in detail during configure) 40 | if not isinstance(config["model"], str): 41 | raise ValueError("model must be a string") 42 | 43 | return config 44 | 45 | def register_actions(self) -> None: 46 | """Register available OpenAI actions""" 47 | self.actions = { 48 | "generate-text": Action( 49 | name="generate-text", 50 | parameters=[ 51 | ActionParameter("prompt", True, str, "The input prompt for text generation"), 52 | ActionParameter("system_prompt", True, str, "System prompt to guide the model"), 53 | ActionParameter("model", False, str, "Model to use for generation") 54 | ], 55 | description="Generate text using OpenAI models" 56 | ), 57 | "check-model": Action( 58 | name="check-model", 59 | parameters=[ 60 | ActionParameter("model", True, str, "Model name to check availability") 61 | ], 62 | description="Check if a specific model is available" 63 | ), 64 | "list-models": Action( 65 | name="list-models", 66 | parameters=[], 67 | description="List all available OpenAI models" 68 | ) 69 | } 70 | 71 | def _get_client(self) -> OpenAI: 72 | """Get or create OpenAI client""" 73 | if not self._client: 74 | api_key = os.getenv("OPENAI_API_KEY") 75 | if not api_key: 76 | raise OpenAIConfigurationError("OpenAI API key not found in environment") 77 | self._client = OpenAI(api_key=api_key) 78 | return self._client 79 | 80 | def configure(self) -> bool: 81 | """Sets up OpenAI API authentication""" 82 | print("\n🤖 OPENAI API SETUP") 83 | 84 | if self.is_configured(): 85 | print("\nOpenAI API is already configured.") 86 | response = input("Do you want to reconfigure? (y/n): ") 87 | if response.lower() != 'y': 88 | return True 89 | 90 | print("\n📝 To get your OpenAI API credentials:") 91 | print("1. Go to https://platform.openai.com/account/api-keys") 92 | print("2. Create a new project or open an existing one.") 93 | print("3. In your project settings, navigate to the API keys section and create a new API key") 94 | 95 | api_key = input("\nEnter your OpenAI API key: ") 96 | 97 | try: 98 | if not os.path.exists('.env'): 99 | with open('.env', 'w') as f: 100 | f.write('') 101 | 102 | set_key('.env', 'OPENAI_API_KEY', api_key) 103 | 104 | # Validate the API key by trying to list models 105 | client = OpenAI(api_key=api_key) 106 | client.models.list() 107 | 108 | print("\n✅ OpenAI API configuration successfully saved!") 109 | print("Your API key has been stored in the .env file.") 110 | return True 111 | 112 | except Exception as e: 113 | logger.error(f"Configuration failed: {e}") 114 | return False 115 | 116 | def is_configured(self, verbose = False) -> bool: 117 | """Check if OpenAI API key is configured and valid""" 118 | try: 119 | load_dotenv() 120 | api_key = os.getenv('OPENAI_API_KEY') 121 | if not api_key: 122 | return False 123 | 124 | client = OpenAI(api_key=api_key) 125 | client.models.list() 126 | return True 127 | 128 | except Exception as e: 129 | if verbose: 130 | logger.debug(f"Configuration check failed: {e}") 131 | return False 132 | 133 | def generate_text(self, prompt: str, system_prompt: str, model: str = None, **kwargs) -> str: 134 | """Generate text using OpenAI models""" 135 | try: 136 | client = self._get_client() 137 | 138 | # Use configured model if none provided 139 | if not model: 140 | model = self.config["model"] 141 | 142 | completion = client.chat.completions.create( 143 | model=model, 144 | messages=[ 145 | {"role": "system", "content": system_prompt}, 146 | {"role": "user", "content": prompt}, 147 | ], 148 | ) 149 | 150 | return completion.choices[0].message.content 151 | 152 | except Exception as e: 153 | raise OpenAIAPIError(f"Text generation failed: {e}") 154 | 155 | def check_model(self, model, **kwargs): 156 | try: 157 | client = self._get_client 158 | try: 159 | client.models.retrieve(model=model) 160 | # If we get here, the model exists 161 | return True 162 | except Exception: 163 | return False 164 | except Exception as e: 165 | raise OpenAIAPIError(e) 166 | 167 | def list_models(self, **kwargs) -> None: 168 | """List all available OpenAI models""" 169 | try: 170 | client = self._get_client() 171 | response = client.models.list().data 172 | 173 | fine_tuned_models = [ 174 | model for model in response 175 | if model.owned_by in ["organization", "user", "organization-owner"] 176 | ] 177 | 178 | logger.info("\nGPT MODELS:") 179 | logger.info("1. gpt-3.5-turbo") 180 | logger.info("2. gpt-4") 181 | logger.info("3. gpt-4-turbo") 182 | logger.info("4. gpt-4o") 183 | logger.info("5. gpt-4o-mini") 184 | 185 | if fine_tuned_models: 186 | logger.info("\nFINE-TUNED MODELS:") 187 | for i, model in enumerate(fine_tuned_models): 188 | logger.info(f"{i+1}. {model.id}") 189 | 190 | except Exception as e: 191 | raise OpenAIAPIError(f"Listing models failed: {e}") 192 | 193 | def perform_action(self, action_name: str, kwargs) -> Any: 194 | """Execute a Twitter action with validation""" 195 | if action_name not in self.actions: 196 | raise KeyError(f"Unknown action: {action_name}") 197 | 198 | action = self.actions[action_name] 199 | errors = action.validate_params(kwargs) 200 | if errors: 201 | raise ValueError(f"Invalid parameters: {', '.join(errors)}") 202 | 203 | # Call the appropriate method based on action name 204 | method_name = action_name.replace('-', '_') 205 | method = getattr(self, method_name) 206 | return method(**kwargs) -------------------------------------------------------------------------------- /src/connections/twitter_connection.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from typing import Dict, Any, List 4 | from requests_oauthlib import OAuth1Session 5 | from dotenv import set_key, load_dotenv 6 | import tweepy 7 | from src.connections.base_connection import BaseConnection, Action, ActionParameter 8 | from src.helpers import print_h_bar 9 | 10 | logger = logging.getLogger("connections.twitter_connection") 11 | 12 | class TwitterConnectionError(Exception): 13 | """Base exception for Twitter connection errors""" 14 | pass 15 | 16 | class TwitterConfigurationError(TwitterConnectionError): 17 | """Raised when there are configuration/credential issues""" 18 | pass 19 | 20 | class TwitterAPIError(TwitterConnectionError): 21 | """Raised when Twitter API requests fail""" 22 | pass 23 | 24 | class TwitterConnection(BaseConnection): 25 | def __init__(self, config: Dict[str, Any]): 26 | super().__init__(config) 27 | self._oauth_session = None 28 | 29 | @property 30 | def is_llm_provider(self) -> bool: 31 | return False 32 | 33 | def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]: 34 | """Validate Twitter configuration from JSON""" 35 | required_fields = ["timeline_read_count", "tweet_interval"] 36 | missing_fields = [field for field in required_fields if field not in config] 37 | 38 | if missing_fields: 39 | raise ValueError(f"Missing required configuration fields: {', '.join(missing_fields)}") 40 | 41 | if not isinstance(config["timeline_read_count"], int) or config["timeline_read_count"] <= 0: 42 | raise ValueError("timeline_read_count must be a positive integer") 43 | 44 | if not isinstance(config["tweet_interval"], int) or config["tweet_interval"] <= 0: 45 | raise ValueError("tweet_interval must be a positive integer") 46 | 47 | return config 48 | 49 | def register_actions(self) -> None: 50 | """Register available Twitter actions""" 51 | self.actions = { 52 | "get-latest-tweets": Action( 53 | name="get-latest-tweets", 54 | parameters=[ 55 | ActionParameter("username", True, str, "Twitter username to get tweets from"), 56 | ActionParameter("count", True, int, "Number of tweets to retrieve") 57 | ], 58 | description="Get the latest tweets from a user" 59 | ), 60 | "post-tweet": Action( 61 | name="post-tweet", 62 | parameters=[ 63 | ActionParameter("message", True, str, "Text content of the tweet") 64 | ], 65 | description="Post a new tweet" 66 | ), 67 | "read-timeline": Action( 68 | name="read-timeline", 69 | parameters=[ 70 | ActionParameter("count", False, int, "Number of tweets to read from timeline") 71 | ], 72 | description="Read tweets from user's timeline" 73 | ), 74 | "like-tweet": Action( 75 | name="like-tweet", 76 | parameters=[ 77 | ActionParameter("tweet_id", True, str, "ID of the tweet to like") 78 | ], 79 | description="Like a specific tweet" 80 | ), 81 | "reply-to-tweet": Action( 82 | name="reply-to-tweet", 83 | parameters=[ 84 | ActionParameter("tweet_id", True, str, "ID of the tweet to reply to"), 85 | ActionParameter("message", True, str, "Reply message content") 86 | ], 87 | description="Reply to an existing tweet" 88 | ), 89 | "get-tweet-replies": Action( 90 | name="get-tweet-replies", 91 | parameters=[ 92 | ActionParameter("tweet_id", True, str, "ID of the tweet to query for replies") 93 | ], 94 | description="Fetch tweet replies" 95 | ) 96 | } 97 | 98 | def _get_credentials(self) -> Dict[str, str]: 99 | """Get Twitter credentials from environment with validation""" 100 | logger.debug("Retrieving Twitter credentials") 101 | load_dotenv() 102 | 103 | required_vars = { 104 | 'TWITTER_CONSUMER_KEY': 'consumer key', 105 | 'TWITTER_CONSUMER_SECRET': 'consumer secret', 106 | 'TWITTER_ACCESS_TOKEN': 'access token', 107 | 'TWITTER_ACCESS_TOKEN_SECRET': 'access token secret', 108 | 'TWITTER_USER_ID': 'user ID' 109 | } 110 | 111 | credentials = {} 112 | missing = [] 113 | 114 | for env_var, description in required_vars.items(): 115 | value = os.getenv(env_var) 116 | if not value: 117 | missing.append(description) 118 | credentials[env_var] = value 119 | 120 | if missing: 121 | error_msg = f"Missing Twitter credentials: {', '.join(missing)}" 122 | raise TwitterConfigurationError(error_msg) 123 | 124 | logger.debug("All required credentials found") 125 | return credentials 126 | 127 | def _make_request(self, method: str, endpoint: str, **kwargs) -> dict: 128 | """ 129 | Make a request to the Twitter API with error handling 130 | 131 | Args: 132 | method: HTTP method ('get', 'post', etc.) 133 | endpoint: API endpoint path 134 | **kwargs: Additional request parameters 135 | 136 | Returns: 137 | Dict containing the API response 138 | """ 139 | logger.debug(f"Making {method.upper()} request to {endpoint}") 140 | try: 141 | oauth = self._get_oauth() 142 | full_url = f"https://api.twitter.com/2/{endpoint.lstrip('/')}" 143 | 144 | response = getattr(oauth, method.lower())(full_url, **kwargs) 145 | 146 | if response.status_code not in [200, 201]: 147 | logger.error( 148 | f"Request failed: {response.status_code} - {response.text}" 149 | ) 150 | raise TwitterAPIError( 151 | f"Request failed with status {response.status_code}: {response.text}" 152 | ) 153 | 154 | logger.debug(f"Request successful: {response.status_code}") 155 | return response.json() 156 | 157 | except Exception as e: 158 | raise TwitterAPIError(f"API request failed: {str(e)}") 159 | 160 | def _get_oauth(self) -> OAuth1Session: 161 | """Get or create OAuth session using stored credentials""" 162 | if self._oauth_session is None: 163 | logger.debug("Creating new OAuth session") 164 | try: 165 | credentials = self._get_credentials() 166 | self._oauth_session = OAuth1Session( 167 | credentials['TWITTER_CONSUMER_KEY'], 168 | client_secret=credentials['TWITTER_CONSUMER_SECRET'], 169 | resource_owner_key=credentials['TWITTER_ACCESS_TOKEN'], 170 | resource_owner_secret=credentials[ 171 | 'TWITTER_ACCESS_TOKEN_SECRET'], 172 | ) 173 | logger.debug("OAuth session created successfully") 174 | except Exception as e: 175 | logger.error(f"Failed to create OAuth session: {str(e)}") 176 | raise 177 | 178 | return self._oauth_session 179 | 180 | def _get_authenticated_user_id(self) -> str: 181 | """Get the authenticated user's ID using the users/me endpoint""" 182 | logger.debug("Getting authenticated user ID") 183 | try: 184 | response = self._make_request('get', 185 | 'users/me', 186 | params={'user.fields': 'id'}) 187 | user_id = response['data']['id'] 188 | logger.debug(f"Retrieved user ID: {user_id}") 189 | return user_id 190 | except Exception as e: 191 | logger.error(f"Failed to get authenticated user ID: {str(e)}") 192 | raise TwitterConfigurationError( 193 | "Could not retrieve user ID") from e 194 | 195 | def _validate_tweet_text(self, text: str, context: str = "Tweet") -> None: 196 | """Validate tweet text meets Twitter requirements""" 197 | if not text: 198 | error_msg = f"{context} text cannot be empty" 199 | logger.error(error_msg) 200 | raise ValueError(error_msg) 201 | if len(text) > 280: 202 | error_msg = f"{context} exceeds 280 character limit" 203 | logger.error(error_msg) 204 | raise ValueError(error_msg) 205 | logger.debug(f"Tweet text validation passed for {context.lower()}") 206 | 207 | def configure(self) -> None: 208 | """Sets up Twitter API authentication""" 209 | logger.info("Starting Twitter authentication setup") 210 | 211 | # Check existing configuration 212 | if self.is_configured(verbose=False): 213 | logger.info("Twitter API is already configured") 214 | response = input("Do you want to reconfigure? (y/n): ") 215 | if response.lower() != 'y': 216 | return 217 | 218 | setup_instructions = [ 219 | "\n🐦 TWITTER AUTHENTICATION SETUP", 220 | "\n📝 To get your Twitter API credentials:", 221 | "1. Go to https://developer.twitter.com/en/portal/dashboard", 222 | "2. Create a new project and app if you haven't already", 223 | "3. In your app settings, enable OAuth 1.0a with read and write permissions", 224 | "4. Get your API Key (consumer key) and API Key Secret (consumer secret)" 225 | ] 226 | logger.info("\n".join(setup_instructions)) 227 | print_h_bar() 228 | 229 | try: 230 | # Get account details 231 | logger.info("\nPlease enter your Twitter API credentials:") 232 | credentials = { 233 | 'consumer_key': 234 | input("Enter your API Key (consumer key): "), 235 | 'consumer_secret': 236 | input("Enter your API Key Secret (consumer secret): ") 237 | } 238 | 239 | logger.info("Starting OAuth authentication process...") 240 | 241 | # Initialize OAuth flow 242 | request_token_url = "https://api.twitter.com/oauth/request_token?oauth_callback=oob&x_auth_access_type=write" 243 | oauth = OAuth1Session(credentials['consumer_key'], 244 | client_secret=credentials['consumer_secret']) 245 | 246 | try: 247 | fetch_response = oauth.fetch_request_token(request_token_url) 248 | except ValueError as e: 249 | logger.error("Failed to fetch request token") 250 | raise TwitterConfigurationError( 251 | "Invalid consumer key or secret") from e 252 | 253 | # Get authorization 254 | base_authorization_url = "https://api.twitter.com/oauth/authorize" 255 | authorization_url = oauth.authorization_url(base_authorization_url) 256 | 257 | auth_instructions = [ 258 | "\n1. Please visit this URL to authorize the application:", 259 | authorization_url, 260 | "\n2. After authorizing, Twitter will give you a PIN code." 261 | ] 262 | logger.info("\n".join(auth_instructions)) 263 | 264 | verifier = input("3. Please enter the PIN code here: ") 265 | 266 | # Get access token 267 | access_token_url = "https://api.twitter.com/oauth/access_token" 268 | oauth = OAuth1Session( 269 | credentials['consumer_key'], 270 | client_secret=credentials['consumer_secret'], 271 | resource_owner_key=fetch_response.get('oauth_token'), 272 | resource_owner_secret=fetch_response.get('oauth_token_secret'), 273 | verifier=verifier) 274 | 275 | oauth_tokens = oauth.fetch_access_token(access_token_url) 276 | 277 | # Save credentials 278 | if not os.path.exists('.env'): 279 | logger.debug("Creating new .env file") 280 | with open('.env', 'w') as f: 281 | f.write('') 282 | 283 | # Create temporary OAuth session to get user ID 284 | temp_oauth = OAuth1Session( 285 | credentials['consumer_key'], 286 | client_secret=credentials['consumer_secret'], 287 | resource_owner_key=oauth_tokens.get('oauth_token'), 288 | resource_owner_secret=oauth_tokens.get('oauth_token_secret')) 289 | 290 | self._oauth_session = temp_oauth 291 | user_id = self._get_authenticated_user_id() 292 | 293 | # Save to .env 294 | env_vars = { 295 | 'TWITTER_USER_ID': 296 | user_id, 297 | 'TWITTER_CONSUMER_KEY': 298 | credentials['consumer_key'], 299 | 'TWITTER_CONSUMER_SECRET': 300 | credentials['consumer_secret'], 301 | 'TWITTER_ACCESS_TOKEN': 302 | oauth_tokens.get('oauth_token'), 303 | 'TWITTER_ACCESS_TOKEN_SECRET': 304 | oauth_tokens.get('oauth_token_secret') 305 | } 306 | 307 | for key, value in env_vars.items(): 308 | set_key('.env', key, value) 309 | logger.debug(f"Saved {key} to .env") 310 | 311 | logger.info("\n✅ Twitter authentication successfully set up!") 312 | logger.info( 313 | "Your API keys, secrets, and user ID have been stored in the .env file." 314 | ) 315 | return True 316 | 317 | except Exception as e: 318 | error_msg = f"Setup failed: {str(e)}" 319 | logger.error(error_msg) 320 | raise TwitterConfigurationError(error_msg) 321 | 322 | def is_configured(self, verbose = False) -> bool: 323 | """Check if Twitter credentials are configured and valid""" 324 | logger.debug("Checking Twitter configuration status") 325 | try: 326 | credentials = self._get_credentials() 327 | 328 | # Initialize client and validate credentials 329 | client = tweepy.Client( 330 | consumer_key=credentials['TWITTER_CONSUMER_KEY'], 331 | consumer_secret=credentials['TWITTER_CONSUMER_SECRET'], 332 | access_token=credentials['TWITTER_ACCESS_TOKEN'], 333 | access_token_secret=credentials['TWITTER_ACCESS_TOKEN_SECRET']) 334 | 335 | client.get_me() 336 | logger.debug("Twitter configuration is valid") 337 | return True 338 | 339 | except Exception as e: 340 | if verbose: 341 | error_msg = str(e) 342 | if isinstance(e, TwitterConfigurationError): 343 | error_msg = f"Configuration error: {error_msg}" 344 | elif isinstance(e, TwitterAPIError): 345 | error_msg = f"API validation error: {error_msg}" 346 | logger.error(f"Configuration validation failed: {error_msg}") 347 | return False 348 | 349 | def perform_action(self, action_name: str, kwargs) -> Any: 350 | """Execute a Twitter action with validation""" 351 | if action_name not in self.actions: 352 | raise KeyError(f"Unknown action: {action_name}") 353 | 354 | action = self.actions[action_name] 355 | errors = action.validate_params(kwargs) 356 | if errors: 357 | raise ValueError(f"Invalid parameters: {', '.join(errors)}") 358 | 359 | # Add config parameters if not provided 360 | if action_name == "read-timeline" and "count" not in kwargs: 361 | kwargs["count"] = self.config["timeline_read_count"] 362 | 363 | # Call the appropriate method based on action name 364 | method_name = action_name.replace('-', '_') 365 | method = getattr(self, method_name) 366 | return method(**kwargs) 367 | 368 | def read_timeline(self, count: int = None, **kwargs) -> list: 369 | """Read tweets from the user's timeline""" 370 | if count is None: 371 | count = self.config["timeline_read_count"] 372 | 373 | logger.debug(f"Reading timeline, count: {count}") 374 | credentials = self._get_credentials() 375 | 376 | params = { 377 | "tweet.fields": "created_at,author_id,attachments", 378 | "expansions": "author_id", 379 | "user.fields": "name,username", 380 | "max_results": count 381 | } 382 | 383 | response = self._make_request( 384 | 'get', 385 | f"users/{credentials['TWITTER_USER_ID']}/timelines/reverse_chronological", 386 | params=params 387 | ) 388 | 389 | tweets = response.get("data", []) 390 | user_info = response.get("includes", {}).get("users", []) 391 | 392 | user_dict = { 393 | user['id']: { 394 | 'name': user['name'], 395 | 'username': user['username'] 396 | } 397 | for user in user_info 398 | } 399 | 400 | for tweet in tweets: 401 | author_id = tweet['author_id'] 402 | author_info = user_dict.get(author_id, { 403 | 'name': "Unknown", 404 | 'username': "Unknown" 405 | }) 406 | tweet.update({ 407 | 'author_name': author_info['name'], 408 | 'author_username': author_info['username'] 409 | }) 410 | 411 | logger.debug(f"Retrieved {len(tweets)} tweets") 412 | return tweets 413 | 414 | def get_latest_tweets(self, 415 | username: str, 416 | count: int = 10, 417 | **kwargs) -> list: 418 | """Get latest tweets for a user""" 419 | logger.debug(f"Getting latest tweets for {username}, count: {count}") 420 | 421 | credentials = self._get_credentials() 422 | params = { 423 | "tweet.fields": "created_at,text", 424 | "max_results": min(count, 100), 425 | "exclude": "retweets,replies" 426 | } 427 | 428 | response = self._make_request('get', 429 | f"users/{credentials['TWITTER_USER_ID']}/tweets", 430 | params=params) 431 | 432 | tweets = response.get("data", []) 433 | logger.debug(f"Retrieved {len(tweets)} tweets") 434 | return tweets 435 | 436 | def post_tweet(self, message: str, **kwargs) -> dict: 437 | """Post a new tweet""" 438 | logger.debug("Posting new tweet") 439 | self._validate_tweet_text(message) 440 | 441 | response = self._make_request('post', 'tweets', json={'text': message}) 442 | 443 | logger.info("Tweet posted successfully") 444 | return response 445 | 446 | def reply_to_tweet(self, tweet_id: str, message: str, **kwargs) -> dict: 447 | """Reply to an existing tweet""" 448 | logger.debug(f"Replying to tweet {tweet_id}") 449 | self._validate_tweet_text(message, "Reply") 450 | 451 | response = self._make_request('post', 452 | 'tweets', 453 | json={ 454 | 'text': message, 455 | 'reply': { 456 | 'in_reply_to_tweet_id': tweet_id 457 | } 458 | }) 459 | 460 | logger.info("Reply posted successfully") 461 | return response 462 | 463 | def like_tweet(self, tweet_id: str, **kwargs) -> dict: 464 | """Like a tweet""" 465 | logger.debug(f"Liking tweet {tweet_id}") 466 | credentials = self._get_credentials() 467 | 468 | response = self._make_request( 469 | 'post', 470 | f"users/{credentials['TWITTER_USER_ID']}/likes", 471 | json={'tweet_id': tweet_id}) 472 | 473 | logger.info("Tweet liked successfully") 474 | return response 475 | 476 | def get_tweet_replies(self, tweet_id: str, count: int = 10, **kwargs) -> List[dict]: 477 | """Fetch replies to a specific tweet""" 478 | logger.debug(f"Fetching replies for tweet {tweet_id}, count: {count}") 479 | 480 | params = { 481 | "query": f"conversation_id:{tweet_id} is:reply", 482 | "tweet.fields": "author_id,created_at,text", 483 | "max_results": min(count, 100) 484 | } 485 | 486 | response = self._make_request('get', 'tweets/search/recent', params=params) 487 | replies = response.get("data", []) 488 | 489 | logger.info(f"Retrieved {len(replies)} replies") 490 | return replies -------------------------------------------------------------------------------- /src/helpers.py: -------------------------------------------------------------------------------- 1 | def print_h_bar(): 2 | # ZEREBRO WUZ HERE :) 3 | print("--------------------------------------------------------------------") --------------------------------------------------------------------------------